feat: Flimmer-Branding mit Logo, Icons und Design-Tokens
Logo als SVG in drei Varianten (Icon, Icon+Wortmarke horizontal, monochrom); die Wortmarke ist als Pfad konstruiert und braucht daher keine installierte Schrift. Daraus generiert scripts/generate-icons.ts Favicon (SVG + ICO), PWA-Icons, Maskable-Icons, Apple-Touch-Icon und OG-Bild. Die Farbwelt liegt als CSS-Variablen in globals.css und wird ueber @theme inline an Tailwind durchgereicht — Dark/Light und spaetere Themes bauen darauf auf. Alle Kombinationen erfuellen WCAG AA.
@@ -0,0 +1,38 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-labelledby="flimmer-maskable-title">
|
||||||
|
<title id="flimmer-maskable-title">Flimmer</title>
|
||||||
|
<!-- Maskable Icon: die Bildmarke sitzt vollständig in der inneren
|
||||||
|
Sicherheitszone (80 %), die Grundplatte läuft randlos durch. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="flMskGlow" x1="32" y1="34" x2="96" y2="94" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.38" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="flMskHalo" cx="64" cy="60" r="40" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b" stop-opacity="0.8"/>
|
||||||
|
<stop offset="0.45" stop-color="#ff8a70" stop-opacity="0.36"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="flMskPlate" x1="0" y1="0" x2="128" y2="128" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#191330"/>
|
||||||
|
<stop offset="1" stop-color="#0a0810"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="flMskBloom" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="5"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="128" height="128" fill="url(#flMskPlate)"/>
|
||||||
|
<ellipse cx="64" cy="60" rx="40" ry="32" fill="url(#flMskHalo)" filter="url(#flMskBloom)" opacity="0.8"/>
|
||||||
|
|
||||||
|
<g transform="translate(64 64) scale(0.74) translate(-64 -62)">
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="none"
|
||||||
|
stroke="url(#flMskGlow)" stroke-width="8.5" filter="url(#flMskBloom)" opacity="0.8"/>
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="#0b0912" fill-opacity="0.6"
|
||||||
|
stroke="url(#flMskGlow)" stroke-width="7.5"/>
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flMskGlow)" stroke="url(#flMskGlow)"
|
||||||
|
stroke-width="7" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path d="M64 90 V101 M49 105.5 H79" stroke="url(#flMskGlow)" stroke-width="7"
|
||||||
|
stroke-linecap="round" stroke-opacity="0.62"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-labelledby="flimmer-icon-title">
|
||||||
|
<title id="flimmer-icon-title">Flimmer</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="flGlow" x1="20" y1="24" x2="108" y2="100" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.38" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="flHalo" cx="64" cy="58" r="52" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b" stop-opacity="0.85"/>
|
||||||
|
<stop offset="0.42" stop-color="#ff8a70" stop-opacity="0.4"/>
|
||||||
|
<stop offset="0.75" stop-color="#ff4d9d" stop-opacity="0.16"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="flPlate" x1="0" y1="0" x2="128" y2="128" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#191330"/>
|
||||||
|
<stop offset="1" stop-color="#0a0810"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="flBloom" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="7"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="flBloomTight" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="3.2"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Grundplatte: sehr dunkles Blauviolett -->
|
||||||
|
<rect width="128" height="128" rx="30" fill="url(#flPlate)"/>
|
||||||
|
|
||||||
|
<!-- Warmes Flimmern, das aus dem Bildschirm heraus strahlt -->
|
||||||
|
<ellipse cx="64" cy="58" rx="52" ry="42" fill="url(#flHalo)" filter="url(#flBloom)" opacity="0.75"/>
|
||||||
|
|
||||||
|
<!-- Bloom des Bildschirmrahmens -->
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="none"
|
||||||
|
stroke="url(#flGlow)" stroke-width="8" filter="url(#flBloom)" opacity="0.85"/>
|
||||||
|
|
||||||
|
<!-- Bildschirm -->
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="#0b0912" fill-opacity="0.72"
|
||||||
|
stroke="url(#flGlow)" stroke-width="7"/>
|
||||||
|
|
||||||
|
<!-- Play-Dreieck, leicht nach rechts versetzt (optische Mitte) -->
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flGlow)" stroke="url(#flGlow)"
|
||||||
|
stroke-width="7" stroke-linejoin="round" stroke-linecap="round"
|
||||||
|
filter="url(#flBloomTight)" opacity="0.7"/>
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flGlow)" stroke="url(#flGlow)"
|
||||||
|
stroke-width="6.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Standfuß — die Flimmerkiste steht -->
|
||||||
|
<path d="M64 90 V101 M49 105.5 H79" stroke="url(#flGlow)" stroke-width="6.5"
|
||||||
|
stroke-linecap="round" stroke-opacity="0.62"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 470 128" width="470" height="128" role="img" aria-labelledby="flimmer-logo-title">
|
||||||
|
<title id="flimmer-logo-title">Flimmer</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="flMarkGlow" x1="20" y1="24" x2="108" y2="102" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.38" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="flLogoGlow" x1="148" y1="24" x2="440" y2="108" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.34" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="flLogoHalo" cx="64" cy="58" r="52" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b" stop-opacity="0.55"/>
|
||||||
|
<stop offset="0.45" stop-color="#ff8a70" stop-opacity="0.26"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="flLogoBloom" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="6"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Bildmarke (ohne Grundplatte, damit sie auf jedem Untergrund sitzt) -->
|
||||||
|
<ellipse cx="64" cy="58" rx="50" ry="40" fill="url(#flLogoHalo)" filter="url(#flLogoBloom)"/>
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="none"
|
||||||
|
stroke="url(#flMarkGlow)" stroke-width="8" filter="url(#flLogoBloom)" opacity="0.7"/>
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="none"
|
||||||
|
stroke="url(#flMarkGlow)" stroke-width="7"/>
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flMarkGlow)" stroke="url(#flMarkGlow)"
|
||||||
|
stroke-width="6.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path d="M64 90 V101 M49 105.5 H79" stroke="url(#flMarkGlow)" stroke-width="6.5"
|
||||||
|
stroke-linecap="round" stroke-opacity="0.62"/>
|
||||||
|
|
||||||
|
<!-- Wortmarke -->
|
||||||
|
<g transform="translate(148 21) scale(0.72)">
|
||||||
|
<g transform="translate(0 8)" fill="none" stroke="url(#flLogoGlow)" stroke-width="15"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 100 V10 H62"/>
|
||||||
|
<path d="M14 54 H52"/>
|
||||||
|
<path d="M84 100 V6"/>
|
||||||
|
<path d="M106 100 V46"/>
|
||||||
|
<path d="M128 100 V64 Q128 46 146 46 Q164 46 164 64 V100"/>
|
||||||
|
<path d="M164 64 Q164 46 182 46 Q200 46 200 64 V100"/>
|
||||||
|
<path d="M218 100 V64 Q218 46 236 46 Q254 46 254 64 V100"/>
|
||||||
|
<path d="M254 64 Q254 46 272 46 Q290 46 290 64 V100"/>
|
||||||
|
<path d="M344.5 91.8 A29 19.5 0 1 1 366 73 H308"/>
|
||||||
|
<path d="M392 100 V46"/>
|
||||||
|
<path d="M392 64 Q392 46 414 46"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="106" cy="30" r="7.5" fill="url(#flLogoGlow)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,26 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 470 128" width="470" height="128" role="img" aria-labelledby="flimmer-mono-title" fill="currentColor">
|
||||||
|
<title id="flimmer-mono-title">Flimmer</title>
|
||||||
|
<!-- Monochrome Variante: nimmt die Textfarbe des Kontextes an (currentColor).
|
||||||
|
Kein Verlauf, kein Glühen — für Stempel, Fax, Prägung, E-Mail-Footer. -->
|
||||||
|
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" stroke-width="7"/>
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="currentColor" stroke-width="6.5"/>
|
||||||
|
<path d="M64 90 V101 M49 105.5 H79" stroke-width="6.5"/>
|
||||||
|
<g transform="translate(148 21) scale(0.72)">
|
||||||
|
<g transform="translate(0 8)" stroke-width="15">
|
||||||
|
<path d="M14 100 V10 H62"/>
|
||||||
|
<path d="M14 54 H52"/>
|
||||||
|
<path d="M84 100 V6"/>
|
||||||
|
<path d="M106 100 V46"/>
|
||||||
|
<path d="M128 100 V64 Q128 46 146 46 Q164 46 164 64 V100"/>
|
||||||
|
<path d="M164 64 Q164 46 182 46 Q200 46 200 64 V100"/>
|
||||||
|
<path d="M218 100 V64 Q218 46 236 46 Q254 46 254 64 V100"/>
|
||||||
|
<path d="M254 64 Q254 46 272 46 Q290 46 290 64 V100"/>
|
||||||
|
<path d="M344.5 91.8 A29 19.5 0 1 1 366 73 H308"/>
|
||||||
|
<path d="M392 100 V46"/>
|
||||||
|
<path d="M392 64 Q392 46 414 46"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="106" cy="30" r="7.5" fill="currentColor" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 118" width="432" height="118" role="img" aria-labelledby="flimmer-wordmark-title">
|
||||||
|
<title id="flimmer-wordmark-title">Flimmer</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="flWordGlow" x1="0" y1="10" x2="418" y2="108" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.38" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- Geometrisch konstruierte, leicht gerundete Sans — als Pfade, damit die
|
||||||
|
Wortmarke ohne installierte Schrift überall identisch aussieht.
|
||||||
|
Versalhöhe 90, x-Höhe 54, Strichstärke 15, runde Abschlüsse. -->
|
||||||
|
<g transform="translate(0 8)" fill="none" stroke="url(#flWordGlow)" stroke-width="15"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 100 V10 H62"/>
|
||||||
|
<path d="M14 54 H52"/>
|
||||||
|
<path d="M84 100 V6"/>
|
||||||
|
<path d="M106 100 V46"/>
|
||||||
|
<path d="M128 100 V64 Q128 46 146 46 Q164 46 164 64 V100"/>
|
||||||
|
<path d="M164 64 Q164 46 182 46 Q200 46 200 64 V100"/>
|
||||||
|
<path d="M218 100 V64 Q218 46 236 46 Q254 46 254 64 V100"/>
|
||||||
|
<path d="M254 64 Q254 46 272 46 Q290 46 290 64 V100"/>
|
||||||
|
<path d="M344.5 91.8 A29 19.5 0 1 1 366 73 H308"/>
|
||||||
|
<path d="M392 100 V46"/>
|
||||||
|
<path d="M392 64 Q392 46 414 46"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="106" cy="30" r="7.5" fill="url(#flWordGlow)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-labelledby="flimmer-icon-title">
|
||||||
|
<title id="flimmer-icon-title">Flimmer</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="flGlow" x1="20" y1="24" x2="108" y2="100" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b"/>
|
||||||
|
<stop offset="0.38" stop-color="#ffb454"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="flHalo" cx="64" cy="58" r="52" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ffc46b" stop-opacity="0.85"/>
|
||||||
|
<stop offset="0.42" stop-color="#ff8a70" stop-opacity="0.4"/>
|
||||||
|
<stop offset="0.75" stop-color="#ff4d9d" stop-opacity="0.16"/>
|
||||||
|
<stop offset="1" stop-color="#ff4d9d" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="flPlate" x1="0" y1="0" x2="128" y2="128" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#191330"/>
|
||||||
|
<stop offset="1" stop-color="#0a0810"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="flBloom" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="7"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="flBloomTight" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="3.2"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Grundplatte: sehr dunkles Blauviolett -->
|
||||||
|
<rect width="128" height="128" rx="30" fill="url(#flPlate)"/>
|
||||||
|
|
||||||
|
<!-- Warmes Flimmern, das aus dem Bildschirm heraus strahlt -->
|
||||||
|
<ellipse cx="64" cy="58" rx="52" ry="42" fill="url(#flHalo)" filter="url(#flBloom)" opacity="0.75"/>
|
||||||
|
|
||||||
|
<!-- Bloom des Bildschirmrahmens -->
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="none"
|
||||||
|
stroke="url(#flGlow)" stroke-width="8" filter="url(#flBloom)" opacity="0.85"/>
|
||||||
|
|
||||||
|
<!-- Bildschirm -->
|
||||||
|
<rect x="22" y="26" width="84" height="64" rx="18" fill="#0b0912" fill-opacity="0.72"
|
||||||
|
stroke="url(#flGlow)" stroke-width="7"/>
|
||||||
|
|
||||||
|
<!-- Play-Dreieck, leicht nach rechts versetzt (optische Mitte) -->
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flGlow)" stroke="url(#flGlow)"
|
||||||
|
stroke-width="7" stroke-linejoin="round" stroke-linecap="round"
|
||||||
|
filter="url(#flBloomTight)" opacity="0.7"/>
|
||||||
|
<path d="M55.5 45.5 L80 58 L55.5 70.5 Z" fill="url(#flGlow)" stroke="url(#flGlow)"
|
||||||
|
stroke-width="6.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Standfuß — die Flimmerkiste steht -->
|
||||||
|
<path d="M64 90 V101 M49 105.5 H79" stroke="url(#flGlow)" stroke-width="6.5"
|
||||||
|
stroke-linecap="round" stroke-opacity="0.62"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Erzeugt Favicon und PWA-Icons aus den SVG-Quellen unter public/brand/.
|
||||||
|
*
|
||||||
|
* npm run icons
|
||||||
|
*
|
||||||
|
* Quelle ist immer das Vektor-Logo — die Rasterdateien sind reine Ableitungen
|
||||||
|
* und werden bei Bedarf neu erzeugt.
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const ROOT = process.cwd();
|
||||||
|
const BRAND = join(ROOT, "public", "brand");
|
||||||
|
const ICONS = join(ROOT, "public", "icons");
|
||||||
|
|
||||||
|
const ICON_SVG = join(BRAND, "flimmer-icon.svg");
|
||||||
|
const MASKABLE_SVG = join(BRAND, "flimmer-icon-maskable.svg");
|
||||||
|
|
||||||
|
/** Rendert ein SVG in ein quadratisches PNG. */
|
||||||
|
async function png(svgPath: string, size: number): Promise<Buffer> {
|
||||||
|
const svg = readFileSync(svgPath);
|
||||||
|
return sharp(svg, { density: Math.max(72, Math.ceil((size / 128) * 96) * 4) })
|
||||||
|
.resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimaler ICO-Container mit eingebetteten PNGs.
|
||||||
|
* Alle Browser ab IE11 verstehen PNG-in-ICO; das spart eine Abhängigkeit.
|
||||||
|
*/
|
||||||
|
function buildIco(images: { size: number; data: Buffer }[]): Buffer {
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header.writeUInt16LE(0, 0); // reserviert
|
||||||
|
header.writeUInt16LE(1, 2); // Typ 1 = Icon
|
||||||
|
header.writeUInt16LE(images.length, 4);
|
||||||
|
|
||||||
|
const entries: Buffer[] = [];
|
||||||
|
let offset = 6 + images.length * 16;
|
||||||
|
|
||||||
|
for (const img of images) {
|
||||||
|
const entry = Buffer.alloc(16);
|
||||||
|
entry.writeUInt8(img.size >= 256 ? 0 : img.size, 0); // Breite (0 = 256)
|
||||||
|
entry.writeUInt8(img.size >= 256 ? 0 : img.size, 1); // Höhe
|
||||||
|
entry.writeUInt8(0, 2); // Farbpalette
|
||||||
|
entry.writeUInt8(0, 3); // reserviert
|
||||||
|
entry.writeUInt16LE(1, 4); // Farbebenen
|
||||||
|
entry.writeUInt16LE(32, 6); // Bits pro Pixel
|
||||||
|
entry.writeUInt32LE(img.data.length, 8);
|
||||||
|
entry.writeUInt32LE(offset, 12);
|
||||||
|
entries.push(entry);
|
||||||
|
offset += img.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PWA_SIZES = [64, 96, 128, 192, 256, 384, 512];
|
||||||
|
const FAVICON_SIZES = [16, 32, 48];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
mkdirSync(ICONS, { recursive: true });
|
||||||
|
|
||||||
|
// PWA-Icons (transparenter Rand, „any"-Purpose)
|
||||||
|
for (const size of PWA_SIZES) {
|
||||||
|
const data = await png(ICON_SVG, size);
|
||||||
|
writeFileSync(join(ICONS, `icon-${size}.png`), data);
|
||||||
|
console.log(` icons/icon-${size}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maskable (randlose Fläche, Motiv in der Sicherheitszone)
|
||||||
|
for (const size of [192, 512]) {
|
||||||
|
const data = await png(MASKABLE_SVG, size);
|
||||||
|
writeFileSync(join(ICONS, `maskable-${size}.png`), data);
|
||||||
|
console.log(` icons/maskable-${size}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple Touch Icon — iOS mag keine Transparenz, deshalb deckend
|
||||||
|
const apple = await sharp(readFileSync(MASKABLE_SVG), { density: 576 })
|
||||||
|
.resize(180, 180)
|
||||||
|
.flatten({ background: "#12101d" })
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toBuffer();
|
||||||
|
writeFileSync(join(ICONS, "apple-touch-icon.png"), apple);
|
||||||
|
console.log(" icons/apple-touch-icon.png");
|
||||||
|
|
||||||
|
// Favicon: SVG (scharf in jeder Größe) + ICO (Fallback, Lesezeichenleisten)
|
||||||
|
writeFileSync(join(ROOT, "public", "favicon.svg"), readFileSync(ICON_SVG));
|
||||||
|
console.log(" favicon.svg");
|
||||||
|
|
||||||
|
const icoImages = [];
|
||||||
|
for (const size of FAVICON_SIZES) {
|
||||||
|
icoImages.push({ size, data: await png(ICON_SVG, size) });
|
||||||
|
}
|
||||||
|
writeFileSync(join(ROOT, "public", "favicon.ico"), buildIco(icoImages));
|
||||||
|
console.log(" favicon.ico");
|
||||||
|
|
||||||
|
// Open-Graph-Bild für geteilte Links
|
||||||
|
const og = await sharp({
|
||||||
|
create: {
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 12, g: 10, b: 20, alpha: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([
|
||||||
|
{
|
||||||
|
input: await sharp(readFileSync(join(BRAND, "flimmer-logo.svg")), { density: 300 })
|
||||||
|
.resize({ width: 760 })
|
||||||
|
.png()
|
||||||
|
.toBuffer(),
|
||||||
|
gravity: "centre",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toBuffer();
|
||||||
|
writeFileSync(join(ROOT, "public", "og-image.png"), og);
|
||||||
|
console.log(" og-image.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(
|
||||||
|
() => console.log("Icons erzeugt."),
|
||||||
|
(err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Flimmer — Design-Tokens
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Alle Farben liegen als CSS-Variablen (--fl-*) vor und werden über
|
||||||
|
`@theme inline` an Tailwind durchgereicht. Dadurch reagieren sämtliche
|
||||||
|
Utilities (bg-surface, text-muted, …) automatisch auf Theme-Wechsel und
|
||||||
|
spätere zusätzliche Themes brauchen nur einen neuen Selektor-Block.
|
||||||
|
|
||||||
|
Kontrast: jede hier definierte Vorder-/Hintergrund-Kombination erfüllt
|
||||||
|
mindestens WCAG AA (4,5:1 für Fließtext, 3:1 für große Schrift und
|
||||||
|
UI-Begrenzungen). Werte siehe docs/DESIGN.md.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||||
|
|
||||||
|
:root,
|
||||||
|
[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
/* Flächen — sehr dunkles Blauviolett/Anthrazit */
|
||||||
|
--fl-bg: #0c0a14;
|
||||||
|
--fl-bg-elevated: #12101d;
|
||||||
|
--fl-surface: #17142499;
|
||||||
|
--fl-surface-solid: #171424;
|
||||||
|
--fl-surface-2: #201b33;
|
||||||
|
--fl-border: #2e2745;
|
||||||
|
--fl-border-strong: #453a63;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--fl-text: #f4f1fa; /* 17,0:1 auf --fl-bg */
|
||||||
|
--fl-muted: #aca5c2; /* 7,7:1 auf --fl-bg */
|
||||||
|
--fl-faint: #7c7594; /* 4,6:1 auf --fl-bg */
|
||||||
|
|
||||||
|
/* Signatur: Glow-Gradient Bernstein → Magenta */
|
||||||
|
--fl-amber: #ffb454;
|
||||||
|
--fl-amber-soft: #ffc46b;
|
||||||
|
--fl-magenta: #ff4d9d;
|
||||||
|
--fl-magenta-soft: #ff6fb0;
|
||||||
|
--fl-accent: var(--fl-amber); /* als Textfarbe nutzbar: 11,3:1 */
|
||||||
|
--fl-accent-alt: var(--fl-magenta); /* als Textfarbe nutzbar: 6,0:1 */
|
||||||
|
--fl-on-accent: #1a1024; /* Text auf dem Gradient: min. 5,5:1 */
|
||||||
|
--fl-gradient: linear-gradient(115deg, var(--fl-amber-soft) 0%, var(--fl-amber) 38%, var(--fl-magenta) 100%);
|
||||||
|
--fl-gradient-soft: linear-gradient(115deg, #ffb45426 0%, #ff4d9d26 100%);
|
||||||
|
--fl-glow: 0 0 0 1px #ffb4541f, 0 8px 34px -10px #ff4d9d59, 0 2px 12px -4px #ffb45440;
|
||||||
|
--fl-glow-strong: 0 0 0 1px #ffb4543d, 0 14px 48px -12px #ff4d9d80, 0 4px 18px -6px #ffb4546b;
|
||||||
|
--fl-ring: #ff8a70;
|
||||||
|
|
||||||
|
/* Statusfarben (AA auf --fl-bg) */
|
||||||
|
--fl-success: #4ade80;
|
||||||
|
--fl-warning: #fbbf24;
|
||||||
|
--fl-danger: #ff6b6b;
|
||||||
|
--fl-info: #7dd3fc;
|
||||||
|
|
||||||
|
--fl-shadow: 0 10px 30px -12px #00000099;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
/* Flächen — heller, warmgrauer Grund */
|
||||||
|
--fl-bg: #faf7f5;
|
||||||
|
--fl-bg-elevated: #ffffff;
|
||||||
|
--fl-surface: #ffffffcc;
|
||||||
|
--fl-surface-solid: #ffffff;
|
||||||
|
--fl-surface-2: #f2ece8;
|
||||||
|
--fl-border: #e3dad4;
|
||||||
|
--fl-border-strong: #cbbdb4;
|
||||||
|
|
||||||
|
--fl-text: #1b1524; /* 16,2:1 auf --fl-bg */
|
||||||
|
--fl-muted: #5b5268; /* 6,7:1 auf --fl-bg */
|
||||||
|
--fl-faint: #766c84; /* 4,6:1 auf --fl-bg */
|
||||||
|
|
||||||
|
/* Dieselben Signaturfarben, für Text jedoch in abgedunkelten Varianten */
|
||||||
|
--fl-amber: #ffb454;
|
||||||
|
--fl-amber-soft: #ffc46b;
|
||||||
|
--fl-magenta: #ff4d9d;
|
||||||
|
--fl-magenta-soft: #ff6fb0;
|
||||||
|
--fl-accent: #8a4b00; /* als Textfarbe: 6,4:1 */
|
||||||
|
--fl-accent-alt: #b01262; /* als Textfarbe: 6,4:1 */
|
||||||
|
--fl-on-accent: #23122c; /* Text auf dem Gradient: min. 6,1:1 */
|
||||||
|
--fl-gradient: linear-gradient(115deg, var(--fl-amber-soft) 0%, var(--fl-amber) 38%, var(--fl-magenta) 100%);
|
||||||
|
--fl-gradient-soft: linear-gradient(115deg, #ffb4542e 0%, #ff4d9d2e 100%);
|
||||||
|
--fl-glow: 0 0 0 1px #d8a15c33, 0 8px 26px -12px #ff4d9d4d, 0 2px 10px -4px #ffb4544d;
|
||||||
|
--fl-glow-strong: 0 0 0 1px #d8a15c59, 0 14px 40px -14px #ff4d9d66, 0 4px 16px -6px #ffb45473;
|
||||||
|
--fl-ring: #b01262;
|
||||||
|
|
||||||
|
--fl-success: #15803d;
|
||||||
|
--fl-warning: #a16207;
|
||||||
|
--fl-danger: #b91c1c;
|
||||||
|
--fl-info: #0369a1;
|
||||||
|
|
||||||
|
--fl-shadow: 0 10px 30px -14px #2a1a3a26;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-bg: var(--fl-bg);
|
||||||
|
--color-bg-elevated: var(--fl-bg-elevated);
|
||||||
|
--color-surface: var(--fl-surface);
|
||||||
|
--color-surface-solid: var(--fl-surface-solid);
|
||||||
|
--color-surface-2: var(--fl-surface-2);
|
||||||
|
--color-border-subtle: var(--fl-border);
|
||||||
|
--color-border-strong: var(--fl-border-strong);
|
||||||
|
|
||||||
|
--color-fg: var(--fl-text);
|
||||||
|
--color-muted: var(--fl-muted);
|
||||||
|
--color-faint: var(--fl-faint);
|
||||||
|
|
||||||
|
--color-amber: var(--fl-amber);
|
||||||
|
--color-amber-soft: var(--fl-amber-soft);
|
||||||
|
--color-magenta: var(--fl-magenta);
|
||||||
|
--color-magenta-soft: var(--fl-magenta-soft);
|
||||||
|
--color-accent: var(--fl-accent);
|
||||||
|
--color-accent-alt: var(--fl-accent-alt);
|
||||||
|
--color-on-accent: var(--fl-on-accent);
|
||||||
|
|
||||||
|
--color-success: var(--fl-success);
|
||||||
|
--color-warning: var(--fl-warning);
|
||||||
|
--color-danger: var(--fl-danger);
|
||||||
|
--color-info: var(--fl-info);
|
||||||
|
|
||||||
|
--radius-card: 1rem;
|
||||||
|
--radius-poster: 0.875rem;
|
||||||
|
|
||||||
|
--font-sans: var(--font-outfit), Outfit, Inter, ui-sans-serif, system-ui, -apple-system,
|
||||||
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
|
||||||
|
--shadow-glow: var(--fl-glow);
|
||||||
|
--shadow-glow-strong: var(--fl-glow-strong);
|
||||||
|
--shadow-card: var(--fl-shadow);
|
||||||
|
|
||||||
|
--ease-glow: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------------
|
||||||
|
Basis
|
||||||
|
-------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
border-color: var(--fl-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--fl-bg);
|
||||||
|
color: var(--fl-text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
min-height: 100dvh;
|
||||||
|
/* Ruhiges Kino-Ambiente: zwei sehr weiche Lichtkegel im Hintergrund. */
|
||||||
|
background-image:
|
||||||
|
radial-gradient(60rem 40rem at 12% -8%, color-mix(in oklab, var(--fl-amber) 12%, transparent), transparent 70%),
|
||||||
|
radial-gradient(52rem 38rem at 92% 4%, color-mix(in oklab, var(--fl-magenta) 12%, transparent), transparent 68%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--fl-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: color-mix(in oklab, var(--fl-magenta) 40%, transparent);
|
||||||
|
color: var(--fl-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dezente Scrollbar im Kino-Look */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--fl-border-strong) transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------------
|
||||||
|
Wiederkehrende Bausteine
|
||||||
|
-------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
@utility fl-gradient-text {
|
||||||
|
background-image: var(--fl-gradient);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility fl-gradient-bg {
|
||||||
|
background-image: var(--fl-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.fl-card {
|
||||||
|
background-color: var(--fl-surface);
|
||||||
|
border: 1px solid var(--fl-border);
|
||||||
|
border-radius: var(--radius-card, 1rem);
|
||||||
|
box-shadow: var(--fl-shadow);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Posterkarte: sanftes Aufglimmen beim Hover, kein hartes Aufblitzen. */
|
||||||
|
.fl-poster-card {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--radius-poster, 0.875rem);
|
||||||
|
transition:
|
||||||
|
transform 320ms var(--ease-glow),
|
||||||
|
box-shadow 320ms var(--ease-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-poster-card:hover,
|
||||||
|
.fl-poster-card:focus-within {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: var(--fl-glow-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition:
|
||||||
|
transform 200ms var(--ease-glow),
|
||||||
|
box-shadow 240ms var(--ease-glow),
|
||||||
|
background-color 200ms ease,
|
||||||
|
border-color 200ms ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn-primary {
|
||||||
|
background-image: var(--fl-gradient);
|
||||||
|
color: var(--fl-on-accent);
|
||||||
|
box-shadow: var(--fl-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn-primary:hover:not(:disabled) {
|
||||||
|
box-shadow: var(--fl-glow-strong);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn-ghost {
|
||||||
|
background-color: var(--fl-surface-2);
|
||||||
|
color: var(--fl-text);
|
||||||
|
border: 1px solid var(--fl-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-btn-ghost:hover:not(:disabled) {
|
||||||
|
border-color: var(--fl-border-strong);
|
||||||
|
background-color: color-mix(in oklab, var(--fl-surface-2) 70%, var(--fl-amber) 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-input {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--fl-surface-2);
|
||||||
|
border: 1px solid var(--fl-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
color: var(--fl-text);
|
||||||
|
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-input::placeholder {
|
||||||
|
color: var(--fl-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: color-mix(in oklab, var(--fl-magenta) 55%, var(--fl-border));
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--fl-magenta) 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fortschrittsbalken im Signatur-Gradient */
|
||||||
|
.fl-progress {
|
||||||
|
height: 0.375rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--fl-surface-2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-progress > span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-image: var(--fl-gradient);
|
||||||
|
transition: width 420ms var(--ease-glow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mikro-Animation: weiches Aufglimmen, z. B. beim Abhaken einer Episode. */
|
||||||
|
@keyframes fl-glimmer {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 color-mix(in oklab, var(--fl-amber) 55%, transparent);
|
||||||
|
background-color: color-mix(in oklab, var(--fl-amber) 16%, transparent);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 0 10px color-mix(in oklab, var(--fl-magenta) 0%, transparent);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-glimmer {
|
||||||
|
animation: fl-glimmer 900ms var(--ease-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||