Fix package strip layout width and replace the road glow with a generated texture

Cards were fixed-width and forced to scroll even when the row had room to
show all of them; they now flex between 190-240px and the strip row gets
its own wider container instead of the site's normal 1152px content width,
so a full tier lineup fits on typical desktop screens without scrolling.
The scrollbar itself is now a slim themed bar for the cases where scrolling
is still needed.

Also swaps the thin procedural SVG line for a Higgsfield-generated glow
texture, masked per card into that tier's accent color. Along the way,
fixes a real CSS bug where the WebP mask rendered as a solid block instead
of the intended thin line, because the image's alpha channel made the
browser default to alpha-based masking instead of reading its luminance -
fixed by forcing mask-mode: luminance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-09 00:36:23 +02:00
parent 3e237421c6
commit 2c4689471d
2 changed files with 53 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -11,6 +11,7 @@ import { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
import textureGraphite from '../../assets/textures/metal-graphite.webp'; import textureGraphite from '../../assets/textures/metal-graphite.webp';
import textureDeepBlue from '../../assets/textures/metal-deep-blue.webp'; import textureDeepBlue from '../../assets/textures/metal-deep-blue.webp';
import textureWarmBronze from '../../assets/textures/metal-warm-bronze.webp'; import textureWarmBronze from '../../assets/textures/metal-warm-bronze.webp';
import roadGlowTexture from '../../assets/textures/road-glow.webp';
// Eine generierte, gebuerstete Metall-Oberflaechenstruktur je Theme (statt Fotos pro // Eine generierte, gebuerstete Metall-Oberflaechenstruktur je Theme (statt Fotos pro
// Paket, die es fuer echte Kundenpakete nicht gibt) - wird per "overlay"-Blendmodus mit // Paket, die es fuer echte Kundenpakete nicht gibt) - wird per "overlay"-Blendmodus mit
@ -172,10 +173,29 @@ 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 // Der "Lichtweg" im Kartenhintergrund ist ein generiertes, fotografisches Glow-Motiv
// und wird zur teuersten Stufe hin kraeftiger/leuchtender - genau wie der Rahmen-Glow. // (weisser Lichtstreif auf Schwarz) statt einer duennen Vektorlinie - per CSS-Maske
const roadOpacity = 0.14 + tierT * 0.22; // wird die weisse Kurve zur Alphamaske fuer die jeweilige Akzentfarbe, sodass das
const roadGlowOpacity = 0.15 + tierT * 0.55; // Motiv bei JEDER Preisstufe/JEDEM Theme automatisch in der richtigen Farbe leuchtet.
// Schon die Einstiegsstufe bleibt sichtbar praesent statt fast unsichtbar - die
// Preis-Staffelung zeigt sich in der Intensitaet, nicht im "Ob ueberhaupt sichtbar".
const roadOpacity = (0.55 + tierT * 0.45).toFixed(2);
const roadGlowOpacity = (0.2 + tierT * 0.45).toFixed(2);
const roadMaskStyle = {
backgroundColor: `rgb(${accentRgb.join(', ')})`,
WebkitMaskImage: `url(${roadGlowTexture})`,
maskImage: `url(${roadGlowTexture})`,
// WebP-Dateien bringen einen (durchgehend deckenden) Alphakanal mit - ohne explizites
// "luminance" bevorzugt CSS per Default den Alphakanal statt der Helligkeit, wodurch
// die Maske komplett deckend (= volle Flaeche statt duenner Kurve) gerendert wuerde.
maskMode: 'luminance',
WebkitMaskSize: 'cover',
maskSize: 'cover',
WebkitMaskPosition: 'center',
maskPosition: 'center',
WebkitMaskRepeat: 'no-repeat',
maskRepeat: 'no-repeat',
};
// Die Metall-Textur wird als eigene Ebene mit eigener Deckkraft gefahren (statt fest // Die Metall-Textur wird als eigene Ebene mit eigener Deckkraft gefahren (statt fest
// per background-blend-mode verschmolzen) - bei der hellen Einstiegsstufe bleibt sie // per background-blend-mode verschmolzen) - bei der hellen Einstiegsstufe bleibt sie
// kaum sichtbar, damit die Karte wirklich hell/weiss bleibt statt durch die dunkle // kaum sichtbar, damit die Karte wirklich hell/weiss bleibt statt durch die dunkle
@ -194,6 +214,7 @@ export default function ModelPage() {
priceColor: `rgb(${priceRgb.join(', ')})`, priceColor: `rgb(${priceRgb.join(', ')})`,
roadOpacity, roadOpacity,
roadGlowOpacity, roadGlowOpacity,
roadMaskStyle,
textureStyle: { textureStyle: {
backgroundImage: `url(${cardTexture})`, backgroundImage: `url(${cardTexture})`,
backgroundSize: '480px 480px', backgroundSize: '480px 480px',
@ -256,11 +277,22 @@ export default function ModelPage() {
<h2 className="text-2xl font-bold text-white sm:text-3xl">{t('modelPage.tiersHeading')(packages.length)}</h2> <h2 className="text-2xl font-bold text-white sm:text-3xl">{t('modelPage.tiersHeading')(packages.length)}</h2>
<p className="mx-auto mt-2 max-w-xl text-sm text-neutral-400">{t('modelPage.tiersSubheading')}</p> <p className="mx-auto mt-2 max-w-xl text-sm text-neutral-400">{t('modelPage.tiersSubheading')}</p>
</div> </div>
</div>
{/* Eigener, deutlich breiterer Rahmen fuer die Kartenreihe im Scroll-Band-Modus:
die restliche Seite haelt sich an die normale Lesebreite (max-w-6xl), aber
die Kacheln sollen auf breiten Bildschirmen wirklich die volle Breite nutzen
koennen, statt unnoetig frueh in den Scroll-Modus zu wechseln. */}
<div className={isStripLayout ? 'mx-auto max-w-[1800px] px-4 sm:px-6' : 'mx-auto max-w-6xl px-4 sm:px-6'}>
<div <div
className={ className={
isStripLayout isStripLayout
? 'flex items-stretch gap-5 overflow-x-auto pb-2 snap-x snap-mandatory' ? // flex statt fixer Breite: Kacheln teilen sich die volle Zeilenbreite und
// wachsen bis max-w, sobald genug Platz da ist - erst wenn selbst die
// Mindestbreite nicht mehr fuer alle passt, greift das seitliche Scrollen.
// Der Scrollbalken bekommt ein dezentes, zum dunklen Design passendes
// Styling statt des vollen System-Scrollbalkens.
'flex items-stretch gap-5 overflow-x-auto pb-3 snap-x snap-mandatory [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.25)_transparent] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-white/25'
: 'grid gap-6 sm:grid-cols-2 xl:grid-cols-3' : 'grid gap-6 sm:grid-cols-2 xl:grid-cols-3'
} }
> >
@ -269,10 +301,10 @@ export default function ModelPage() {
style, style,
priceColor, priceColor,
mutedColor, mutedColor,
accentColor,
glowLineStyle, glowLineStyle,
roadOpacity, roadOpacity,
roadGlowOpacity, roadGlowOpacity,
roadMaskStyle,
textureStyle, textureStyle,
isDarkCard, isDarkCard,
} = styleOf(pkg); } = styleOf(pkg);
@ -283,36 +315,25 @@ export default function ModelPage() {
style={style} style={style}
className={ className={
isStripLayout isStripLayout
? 'relative flex w-[190px] shrink-0 snap-start flex-col overflow-hidden rounded-xl border p-6 sm:w-[210px]' ? 'relative flex grow shrink basis-[190px] min-w-[190px] max-w-[240px] snap-start flex-col overflow-hidden rounded-xl border p-6 sm:basis-[210px] sm:p-7'
: 'relative flex flex-col overflow-hidden rounded-xl border p-8' : 'relative flex flex-col overflow-hidden rounded-xl border p-8'
} }
> >
{/* Abstrakter "Lichtweg" im Hintergrund - je teurer die Stufe, desto praesenter/leuchtender. {/* Abstrakter "Lichtweg" im Hintergrund - ein generiertes Foto-Glow-Motiv
Fixe Aspect-Ratio + "slice" statt "none", damit Strich/Kurve bei jeder Kartenhoehe (weisser Lichtstreif auf Schwarz), per CSS-Maske in die jeweilige
gleichmaessig aussieht statt verzerrt gestreckt zu werden. */} Akzentfarbe eingefaerbt. Zwei Ebenen: eine weich verwischte Halo-Ebene
<svg fuer die Tiefe darunter, eine scharfe Ebene fuer die Kontur darueber -
viewBox="0 0 200 260" je teurer die Stufe, desto praesenter/leuchtender beide. */}
preserveAspectRatio="xMidYMid slice" <div
className="pointer-events-none absolute inset-0 h-full w-full" className="pointer-events-none absolute inset-0"
style={{ ...roadMaskStyle, opacity: roadGlowOpacity, filter: 'blur(18px)' }}
aria-hidden="true" aria-hidden="true"
> />
<path <div
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" className="pointer-events-none absolute inset-0"
fill="none" style={{ ...roadMaskStyle, opacity: roadOpacity }}
stroke={accentColor} aria-hidden="true"
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>
{/* Metall-Textur als eigene Ebene mit eigener, stufenabhaengiger Deckkraft - {/* Metall-Textur als eigene Ebene mit eigener, stufenabhaengiger Deckkraft -
so bleibt die helle Einstiegsstufe wirklich hell statt durch die dunkle so bleibt die helle Einstiegsstufe wirklich hell statt durch die dunkle