Replace the tier legend with a glowing road visual inside each card

Removed the bottom legend row per feedback - the ask was for a visual
inside the tiles, not an explanatory key below them. Each card now has an
abstract flowing light-path drawn as SVG (no stock photography), colored
with that tier's accent and escalating from barely visible at the
cheapest tier to a bright glow at the priciest, matching the reference
mockup. Content sits in its own stacking context above the path so text
stays legible regardless of the background art.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-08 10:58:52 +02:00
parent d9cf9516c1
commit 3868a258e4
3 changed files with 102 additions and 98 deletions

View file

@ -156,15 +156,6 @@ export default {
requestContact: 'Kontakt anfragen', requestContact: 'Kontakt anfragen',
featuredBadge: 'Empfohlen', featuredBadge: 'Empfohlen',
tierLabels: ['Einstieg', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Referenz'], tierLabels: ['Einstieg', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Referenz'],
tierDescriptions: [
'Solider Sound und spürbare Verbesserung für jeden Tag.',
'Mehr Klarheit und Dynamik für ein intensiveres Hörerlebnis.',
'Spürbar mehr Leistung und Kontrolle für echte Fahrfreude.',
'Premium-Komponenten perfekt abgestimmt für höchste Ansprüche.',
'High-End Performance und individuelle Abstimmung auf höchstem Niveau.',
'Maßgeschneiderte Lösungen und einzigartige Integration für Kenner.',
'Außergewöhnliche Projekte. Grenzenlos in Technik und Design.',
],
tiersHeading: (count) => `${count} Stufen. Ein Ziel: perfekter Klang.`, tiersHeading: (count) => `${count} Stufen. Ein Ziel: perfekter Klang.`,
tiersSubheading: 'Von der soliden Basis bis zum kompromisslosen Meisterwerk finde dein Paket.', tiersSubheading: 'Von der soliden Basis bis zum kompromisslosen Meisterwerk finde dein Paket.',
}, },

View file

@ -156,15 +156,6 @@ export default {
requestContact: 'Request contact', requestContact: 'Request contact',
featuredBadge: 'Recommended', featuredBadge: 'Recommended',
tierLabels: ['Entry', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Reference'], tierLabels: ['Entry', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Reference'],
tierDescriptions: [
'Solid sound and a noticeable improvement for every day.',
'More clarity and dynamics for a richer listening experience.',
'Noticeably more power and control for real driving enjoyment.',
'Premium components perfectly tuned for the highest demands.',
'High-end performance and individual tuning at the highest level.',
'Tailored solutions and unique integration for connoisseurs.',
'Exceptional projects. Limitless in technology and design.',
],
tiersHeading: (count) => `${count} tiers. One goal: perfect sound.`, tiersHeading: (count) => `${count} tiers. One goal: perfect sound.`,
tiersSubheading: 'From a solid foundation to an uncompromising masterpiece find your package.', tiersSubheading: 'From a solid foundation to an uncompromising masterpiece find your package.',
}, },

View file

@ -48,10 +48,6 @@ const PACKAGE_THEMES = {
], ],
}; };
// Ein Icon je Kategorie-Stufe (nicht pro Paket - siehe tierLabels), fuer die erklaerende
// Legende unter der Kachel-Reihe. Reihenfolge muss zu modelPage.tierLabels passen.
const TIER_ICONS = ['route', 'activity', 'gauge', 'shield-check', 'gem', 'crown', 'star'];
const hslToRgb = (h, s, l) => { const hslToRgb = (h, s, l) => {
s /= 100; s /= 100;
l /= 100; l /= 100;
@ -130,15 +126,6 @@ export default function ModelPage() {
// oben. Das braucht keine zusaetzliche Admin-Einstellung und passt sich automatisch // oben. Das braucht keine zusaetzliche Admin-Einstellung und passt sich automatisch
// an jede Paketanzahl an. // an jede Paketanzahl an.
const tierLabels = t('modelPage.tierLabels'); const tierLabels = t('modelPage.tierLabels');
const tierDescriptions = t('modelPage.tierDescriptions');
// Erklaerende Legende unter den Karten: ein Eintrag je Kategorie-Stufe (nicht je Paket),
// Akzentfarbe an einem repraesentativen Punkt der jeweiligen Kategorie-Spanne entnommen,
// damit die Legende farblich zu den Karten oben passt.
const tierLegend = tierLabels.map((label, i) => {
const legendT = (i + 0.5) / tierLabels.length;
const accentRgb = materialRgbAt(MATERIALS, legendT, 'accent').map(Math.round);
return { label, description: tierDescriptions[i], icon: TIER_ICONS[i], color: `rgb(${accentRgb.join(', ')})` };
});
const rankById = new Map( const rankById = new Map(
[...packages].sort((a, b) => a.total_price - b.total_price).map((p, i) => [p.id, i]) [...packages].sort((a, b) => a.total_price - b.total_price).map((p, i) => [p.id, i])
); );
@ -172,6 +159,10 @@ export default function ModelPage() {
// gleicher Helligkeit wie der Hintergrund landen und unsichtbar werden koennte. // gleicher Helligkeit wie der Hintergrund landen und unsichtbar werden koennte.
const mutedColor = `rgb(${hslToRgb(0, 0, contrastingL(38)).join(', ')})`; const mutedColor = `rgb(${hslToRgb(0, 0, contrastingL(38)).join(', ')})`;
const glowAlpha = (0.1 + tierT * 0.3).toFixed(2); const glowAlpha = (0.1 + tierT * 0.3).toFixed(2);
// Der "Lichtweg" im Kartenhintergrund faengt bei der Einstiegsstufe kaum sichtbar an
// und wird zur teuersten Stufe hin kraeftiger/leuchtender - genau wie der Rahmen-Glow.
const roadOpacity = 0.14 + tierT * 0.22;
const roadGlowOpacity = 0.15 + tierT * 0.55;
// Leichter "Lichtschein" von oben rechts statt flacher Flaeche - kommt von oben // Leichter "Lichtschein" von oben rechts statt flacher Flaeche - kommt von oben
// rechts, weil dort nie Text steht (Titel/Preis/Liste sind linksbuendig), damit die // rechts, weil dort nie Text steht (Titel/Preis/Liste sind linksbuendig), damit die
@ -184,6 +175,8 @@ export default function ModelPage() {
tierLabel, tierLabel,
accentColor: `rgb(${accentRgb.join(', ')})`, accentColor: `rgb(${accentRgb.join(', ')})`,
priceColor: `rgb(${priceRgb.join(', ')})`, priceColor: `rgb(${priceRgb.join(', ')})`,
roadOpacity,
roadGlowOpacity,
iconChipStyle: { iconChipStyle: {
backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`, backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`,
color: `rgb(${priceRgb.join(', ')})`, color: `rgb(${priceRgb.join(', ')})`,
@ -248,17 +241,57 @@ export default function ModelPage() {
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{packages.map((pkg) => { {packages.map((pkg) => {
const { style, priceColor, mutedColor, iconChipStyle, tierNumber, tierLabel, accentColor, glowLineStyle } = const {
styleOf(pkg); style,
priceColor,
mutedColor,
iconChipStyle,
tierNumber,
tierLabel,
accentColor,
glowLineStyle,
roadOpacity,
roadGlowOpacity,
} = styleOf(pkg);
return ( return (
<div key={pkg.id} style={style} className="relative flex flex-col rounded-xl border p-8"> <div key={pkg.id} style={style} className="relative flex flex-col overflow-hidden rounded-xl border p-8">
{/* Abstrakter "Lichtweg" im Hintergrund - je teurer die Stufe, desto praesenter/leuchtender.
Fixe Aspect-Ratio + "slice" statt "none", damit Strich/Kurve bei jeder Kartenhoehe
gleichmaessig aussieht statt verzerrt gestreckt zu werden. */}
<svg
viewBox="0 0 200 260"
preserveAspectRatio="xMidYMid slice"
className="pointer-events-none absolute inset-0 h-full w-full"
aria-hidden="true"
>
<path
d="M 20 250 C 60 250 70 190 100 150 C 130 110 160 130 175 90 C 185 65 190 40 195 10"
fill="none"
stroke={accentColor}
strokeWidth="14"
strokeLinecap="round"
style={{ opacity: roadGlowOpacity, filter: 'blur(7px)' }}
/>
<path
d="M 20 250 C 60 250 70 190 100 150 C 130 110 160 130 175 90 C 185 65 190 40 195 10"
fill="none"
stroke={accentColor}
strokeWidth="2.5"
strokeLinecap="round"
style={{ opacity: roadOpacity }}
/>
</svg>
{pkg.is_featured && ( {pkg.is_featured && (
<span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-3 py-1 text-xs font-bold text-white shadow"> <span className="absolute -top-3 left-1/2 z-10 -translate-x-1/2 rounded-full bg-brand-500 px-3 py-1 text-xs font-bold text-white shadow">
{t('modelPage.featuredBadge')} {t('modelPage.featuredBadge')}
</span> </span>
)} )}
{/* Eigener Stapelkontext ueber dem Lichtweg-SVG, damit der Text IMMER lesbar
oben liegt statt vom absolut positionierten Hintergrund verdeckt zu werden. */}
<div className="relative z-10 flex flex-1 flex-col">
<div className="mb-4 flex items-baseline gap-3"> <div className="mb-4 flex items-baseline gap-3">
<span style={{ color: mutedColor }} className="text-3xl font-extralight leading-none"> <span style={{ color: mutedColor }} className="text-3xl font-extralight leading-none">
{tierNumber} {tierNumber}
@ -322,21 +355,10 @@ export default function ModelPage() {
{t('modelPage.requestContact')} {t('modelPage.requestContact')}
</Link> </Link>
</div> </div>
</div>
); );
})} })}
</div> </div>
<div className="mt-14 grid grid-cols-2 gap-x-6 gap-y-8 border-t border-white/10 pt-10 sm:grid-cols-4 lg:grid-cols-7">
{tierLegend.map((item) => (
<div key={item.label}>
<DynamicIcon name={item.icon} className="mb-2 h-6 w-6" style={{ color: item.color }} />
<p style={{ color: item.color }} className="text-xs font-semibold uppercase tracking-wider">
{item.label}
</p>
<p className="mt-1 text-xs text-neutral-400">{item.description}</p>
</div>
))}
</div>
</div> </div>
</section> </section>
)} )}