diff --git a/public/brand/flimmer-icon-maskable.svg b/public/brand/flimmer-icon-maskable.svg new file mode 100644 index 0000000..1936bec --- /dev/null +++ b/public/brand/flimmer-icon-maskable.svg @@ -0,0 +1,38 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/brand/flimmer-icon.svg b/public/brand/flimmer-icon.svg new file mode 100644 index 0000000..bc90d57 --- /dev/null +++ b/public/brand/flimmer-icon.svg @@ -0,0 +1,51 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/brand/flimmer-logo.svg b/public/brand/flimmer-logo.svg new file mode 100644 index 0000000..8d0d155 --- /dev/null +++ b/public/brand/flimmer-logo.svg @@ -0,0 +1,53 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/brand/flimmer-mono.svg b/public/brand/flimmer-mono.svg new file mode 100644 index 0000000..a25761c --- /dev/null +++ b/public/brand/flimmer-mono.svg @@ -0,0 +1,26 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/brand/flimmer-wordmark.svg b/public/brand/flimmer-wordmark.svg new file mode 100644 index 0000000..ac82753 --- /dev/null +++ b/public/brand/flimmer-wordmark.svg @@ -0,0 +1,28 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..a3db894 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..bc90d57 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,51 @@ + + Flimmer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..2340ecb Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-128.png b/public/icons/icon-128.png new file mode 100644 index 0000000..a0ed7fb Binary files /dev/null and b/public/icons/icon-128.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..d55dde1 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-256.png b/public/icons/icon-256.png new file mode 100644 index 0000000..79aaa68 Binary files /dev/null and b/public/icons/icon-256.png differ diff --git a/public/icons/icon-384.png b/public/icons/icon-384.png new file mode 100644 index 0000000..009e3e1 Binary files /dev/null and b/public/icons/icon-384.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..f7d8268 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-64.png b/public/icons/icon-64.png new file mode 100644 index 0000000..eacbb46 Binary files /dev/null and b/public/icons/icon-64.png differ diff --git a/public/icons/icon-96.png b/public/icons/icon-96.png new file mode 100644 index 0000000..62757ef Binary files /dev/null and b/public/icons/icon-96.png differ diff --git a/public/icons/maskable-192.png b/public/icons/maskable-192.png new file mode 100644 index 0000000..54dbfb4 Binary files /dev/null and b/public/icons/maskable-192.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000..0ae55eb Binary files /dev/null and b/public/icons/maskable-512.png differ diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..024f36d Binary files /dev/null and b/public/og-image.png differ diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts new file mode 100644 index 0000000..fefb728 --- /dev/null +++ b/scripts/generate-icons.ts @@ -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 { + 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); + }, +); diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..3dc2f39 --- /dev/null +++ b/src/app/globals.css @@ -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; + } +}