# Post-Generator — Regeln & Konventionen Arbeitsgrundlage für alles, was in diesem Repo an Social-Grafiken entsteht. **Stand:** 31.07.2026 Der Generator läuft lokal: `./serve.sh` → (Post 1, Carousel) bzw. `/index.html` (Post 2, Statement). --- ## 1. Wo die Wahrheit steht Alle Marken-Werte stammen aus der Website, nicht aus diesem Repo. Bei Zweifel **dort nachschauen, nicht hier raten**: | Was | Quelle | |---|---| | Farben, Radien, Abstände | `HiveGrid-Website/app/globals.css` (`:root`) | | Chamfer-Form | `HiveGrid-Website/lib/chamfer.ts` | | Schrift | `HiveGrid-Website/app/layout.tsx` | | Badge-Styling | `HiveGrid-Website/components/sections/ScreenshotAccordion.tsx` | | Feature-Texte | `HiveGrid-Website/app/playground/PlaygroundClient.tsx` → `solutionModules` | Die Werte sind hier gespiegelt, weil der Export keine Tailwind-/oklch-Farben verträgt (siehe §7). Ändert sich die Website, müssen sie **hier nachgezogen werden** — das passiert nicht automatisch. --- ## 2. Farben ### Marke | Token | Wert | Verwendung | |---|---|---| | `--accent-yellow` | `#F1B729` | Primärer Akzent, Marker, G-Marke | | `--accent-yellow-light` | `#FFE082` | Badge-Hintergrund | | `--accent-yellow-dark` | `#3E2C0A` | Badge-Text | | `--accent-teal` | `#0D6E6E` | Sekundär, aktuell ungenutzt | | `--dark-bg` | `#30313D` | Dunkle Variante + Formen-Ebene | ### Flächen & Text | Token | Wert | |---|---| | `--surface` | `#FFFFFF` | | `--bg-primary` | `#FAFAFA` | | `--text-primary` | `#1A1A1A` | | `--text-secondary` | `#555555` | | `--text-on-dark` | `#C8D0DC` | ### Pro Variante **Hell** (Grund `#FFFFFF`) ``` --head-color #1A1A1A --frame-bg #E8EBF0 --body-color #555555 --shape-fill #30313D (deckend!) --accent-on #C8901A --shape-line rgba(48,49,61,.13) --marker #F1B729 --shape-accent #F1B729 --pat-color #30313D --gmark-color rgba(48,49,61,.16) ``` **Dunkel** (Grund `#30313D`) ``` --head-color #FFFFFF --frame-bg rgba(255,255,255,.11) --body-color #B9BDC9 --shape-fill #414252 --accent-on #F1B729 --shape-line rgba(255,255,255,.10) --marker #F1B729 --shape-accent #F1B729 --pat-color #FFFFFF --gmark-color rgba(255,255,255,.13) ``` > **`--accent-on` ist nicht `#F1B729`, sondern `#C8901A` auf Hell.** Reines > Markengelb trägt auf Weiß bei kleinem Text nicht. Der abgedunkelte Ton ist > Absicht, kein Fehler. > **Der Block oben rechts ist auf Hell deckendes `#30313D`.** Das ist derselbe > Wert wie die dunkle Story-Sektion der Website. Er darf **nicht** über die > Intensitätsstufe aufgehellt werden, sonst ist es nicht mehr die Markenfarbe > (siehe §5). --- ## 3. Geometrie ### Chamfer Die Marken-Form: abgeschnittene Ecken, Echo der Honigwabe. Auf der Website 16 px, hier hochskaliert, weil die Fläche viermal so groß ist. ```css clip-path: polygon( Xpx 0, calc(100% - Xpx) 0, 100% Xpx, 100% calc(100% - Xpx), calc(100% - Xpx) 100%, Xpx 100%, 0 calc(100% - Xpx), 0 Xpx ); ``` | Element | X | |---|---| | Badge | 11 px | | Mockup-Rahmen außen | 22 px | | Mockup-Rahmen innen | 14 px | **Drei Fallstricke** (stehen auch in `chamfer.ts`): - `clip-path` beschneidet **auch den Schatten** → Tiefe über `filter: drop-shadow()`, nie `box-shadow` - `clip-path` beschneidet **auch den Rahmen** → Rand über eine Hintergrundfläche lösen, nicht über `border` - Kinder, die aus der Box ragen (Popups, Tooltips), werden abgeschnitten → solche Elemente nicht chamfern ### Hexagon Flat-Top, also flache Ober- und Unterkante mit Spitzen links und rechts: ```css clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); ``` **Seitenverhältnis 384,5 : 332,9 = 1,155 (= 2/√3).** Aus der Logomarke abgeleitet, nicht geschätzt. Wer davon abweicht, staucht das Sechseck. ### G-Marke Ein einziges Polygon, aus `logo.svg` extrahiert (dort die einzige Fläche mit `class="st3"`), auf 0,0 normalisiert: ``` 384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 144.2,249.7 96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 0,166.5 96.1,332.9 288.3,332.9 ``` Immer **inline** als SVG einsetzen, nie als `` — nur so lässt sie sich per `currentColor` pro Variante umfärben, und nur so überlebt sie den Export zuverlässig (§7). ### Wabenraster Nahtlose Kachel aus derselben G-Form. Bei Flat-Top-Hexagonen springen die Spalten um ¾ der Breite, benachbarte Spalten sitzen eine halbe Reihe versetzt. Eine Kachel = zwei Spalten breit, eine Reihe hoch. **Die über den Kachelrand hinausragenden Kopien sind Pflicht** — ohne sie sieht man die Nähte. --- ## 4. Typografie ### Die Schrift **Manrope** — dieselbe wie auf der Website. Bewusst nicht Inter: Der Kommentar in `app/layout.tsx` nennt Inter „the default AI/website-builder font"; Manrope ist eine Spur wärmer und geometrischer. ```css font-family: 'Manrope', system-ui, sans-serif; ``` | | | |---|---| | Schnitte | 400, 500, 600, 700, 800 | | Ablage | `assets/fonts/manrope.css` + 6 `.woff2` | | Subsets | latin, latin-ext, cyrillic, cyrillic-ext, greek, vietnamese | | Herkunft | Google Fonts, einmalig heruntergeladen | **Warum lokal und nicht per CDN:** Der Export darf zur Laufzeit an keiner fremden Origin hängen. Ein Font von `fonts.gstatic.com` wäre cross-origin, würde den Export vom Netz abhängig machen und im Zweifel den Canvas verunreinigen. Die URLs in `manrope.css` sind deshalb auf relative Pfade umgeschrieben. > **Vor jedem Export auf `document.fonts.ready` warten.** Sonst rendert der erste > Durchgang still in einer System-Fallback-Schrift, und man sieht es erst in der > fertigen Datei. Der Download-Button bleibt bis dahin deaktiviert. ### Größen | Element | Größe | Weight | Sonstiges | |---|---|---|---| | Headline | 74 px | 800 | `line-height: 1.06`, `letter-spacing: -0.03em` | | Titel-Tagline | 74 px | 800 | identisch zur Headline, bewusst | | Fließtext | 30 px | 500 | `line-height: 1.48`, `max-width: 830px` | | Badge | 26 px | 600 | `letter-spacing: 0.1em` | | Claim (Titel) | 31 px | 500 | `line-height: 1.44` | ### Akzent-Wörter Zwei Behandlungen, **nie beide im selben Textblock**: - **Marker** — handgezeichneter SVG-Strich als `background-image`, wellige Kanten, angeschrägtes linkes Ende. Für Headlines und Payoffs. Ein CSS-Verlauf ist hier **verboten**: er kann geometrisch nur ein perfektes Rechteck erzeugen und sieht gedruckt statt gemalt aus. - **Farbe** (`--accent-on`) — für kleinere Zeilen. Pro Slide höchstens **ein** markiertes Wort. Zwei Marker in einem Block waren schon zweimal zu viel. ### Sprache - Alles Sichtbare auf **Deutsch**, Code-Kommentare auf **Englisch** - **Keine Em-Dashes (—)** in sichtbaren Texten. Komma, Punkt oder `·` - Feature-Texte wortgleich von der Website. Headlines dürfen gekürzt und umgestellt werden, aber **keine neuen Wörter erfinden** --- ## 5. Aufbau der Slides ### Formate **1080 × 1350 px (4:5)** — das hochkantigste offiziell unterstützte Feed-Format. Belegt: Instagram unterstützt 1.91:1 bis 4:5, Breiten 320–1080 behalten die Originalauflösung, alles außerhalb wird beschnitten. - **Alle Slides eines Carousels müssen dasselbe Seitenverhältnis haben** — die erste Slide setzt es, der Rest wird hart darauf beschnitten - Safe Zone: nichts Lesbares näher als ~92 px an den Rand - Der **Profil-Grid** zeigt seit 2025 Hochformat und beschneidet auf 3:4 — im Grid fehlen links und rechts je ~33 px. Für die drei angepinnten Cover relevant, im Feed nicht ### Layout-Template: Tag, Überschrift, Beschreibung, Bild Jede Feature-Slide besteht aus denselben vier Bausteinen in derselben Reihenfolge. Das ist keine Geschmacksfrage — Wiedererkennung über fünf Slides entsteht genau dadurch, dass sich das Gerüst nicht bewegt. ``` 92 ┌──────────────────────────────────────┐ │ ▌ TAG │ ← ausgerückt (siehe unten) │ │ │ Überschrift, zwei Zeilen │ ← dominiert die Slide │ mit einem markierten Wort │ │ │ │ Beschreibung, zwei bis drei │ ← kürzer als man denkt │ Zeilen Fließtext │ │ │ │ ┌────────────────────────────────── │ │ │ Bild │ ← ein dominanter Blickfang │ │ │ └──┴───────────────────────────────────┘ ``` **Größenverhältnisse.** Die Hierarchie muss ohne Nachdenken lesbar sein — was zuerst, was zweitens, was drittens. Bei uns: Überschrift 74 px, Fließtext 30 px, Tag 26 px. Das Verhältnis Überschrift zu Fließtext liegt bei rund **2,5 : 1**; gängige Empfehlungen für Carousels nennen Überschrift 48–72 pt gegen Fließtext 24–36 pt, also dieselbe Größenordnung. Weniger Abstand als Faktor 2 und die Hierarchie kippt. **Ein Blickfang pro Slide.** Genau ein Element, auf das das Auge sofort springt — bei uns die Überschrift, danach das Bild. Zwei gleich starke Elemente lassen den Betrachter raten und wegwischen. **Weißraum ist kein verschenkter Platz**, sondern das, was die Blöcke trennt. Der Abstand zwischen Tag und Überschrift ist deshalb größer als der zwischen Überschrift und Beschreibung: getrennte Gedanken brauchen mehr Luft als zusammengehörige. #### Das Tag wird ausgerückt Der Tag hat 20 px Innenabstand. Steht sein **Kasten** bündig zur Inhaltskante, beginnt sein **Text** 20 px weiter rechts als die Überschrift — und genau das sieht man: ``` vorher nachher 92 │▌ WORKFLOW-ENGINE 72 │▌ WORKFLOW-ENGINE 92 │Wenn-dann 92 │Wenn-dann └ Kasten bündig, └ Text bündig, Text 20 px versetzt Kasten ragt aus ``` Gelöst über `margin-left: -20px`, also genau den Innenabstand. Das ist dasselbe Prinzip wie hängende Interpunktion im Satz: Ausgerichtet wird nach der **wahrgenommenen** Kante, nicht nach der geometrischen. Der Kasten steht dann bei x 72 — immer noch weit innerhalb der Safe Zone. > Wer die Innenabstände des Tags ändert, muss das negative Margin mitziehen. > Die beiden Werte gehören zusammen. ### Ebenen (von hinten nach vorn) | z-index | Ebene | |---|---| | 0 | Hintergrund: Formen, Wabenraster, Punkte, Raster, G-Marke | | 1 | Mockup | | 2 | Inhalt: Badge, Headline, Fließtext | | 3 | Swipe-Hinweis | ### Was global gilt und was pro Slide | Einstellung | Geltung | Warum | |---|---|---| | Variante (Hell/Dunkel) | **global** | gemischt fällt das Carousel auseinander | | Mockup-Perspektive | **global** | dito | | Renderqualität | **global** | Exportstufe, kein Gestaltungsmittel (§7) | | Layout | pro Slide | jede Slide hat einen eigenen Katalog | | Muster | pro Slide | | | Intensität | pro Slide | | ### Durchgehendes Carousel (Panorama) Der Post ist dann kein Stapel von fünf Bildern mehr, sondern **ein einziges Blatt von 5400 × 1350 px**, das in fünf 1080er Fenster geschnitten wird. Wer swipet, sieht Elemente über die Kante hinauslaufen und auf der nächsten Slide weitergehen. ``` Slide 1 │ Slide 2 │ Slide 3 │ Slide 4 │ Slide 5 0 ─────── 1080 ────── 2160 ────── 3240 ────── 4320 ────── 5400 ↑ ↑ ↑ ↑ die vier Nähte ``` **Technisch:** Eine Ebene von 5400 px Breite liegt in jeder Slide, verschoben um `index × -1080 px`. Jede Slide zeigt dadurch ihr Fenster desselben Blatts. Kein Zuschneiden von Hand, kein Splitter-Tool — die Slides *sind* die Schnitte. **Regeln, die dabei zählen:** 1. **Slide 1 muss allein funktionieren.** Sie ist das Cover im Profil-Grid. Sie darf nicht wie „das linke Fünftel von etwas" wirken 2. **Die Naht ist nie unsichtbar.** Instagram zeigt beim Swipen ein paar Pixel Abstand zwischen den Slides. Das lässt sich nicht abstellen und ist auch nicht schlimm — der Effekt trägt trotzdem 3. **Nichts Wichtiges auf die Naht legen.** Keine Schrift, kein Gesicht, kein Logo. Der Schnitt gehört in ruhige Zonen 4. **Aber etwas Auffälliges über die Naht laufen lassen.** Eine halbierte Form zieht das Auge weiter und lenkt vom Spalt ab. Genau dafür sitzen unsere Sechsecke **mittig auf 1080 / 2160 / 3240 / 4320** — je zur Hälfte links und rechts der Naht 5. **Nur Standbilder.** Ein Video mitten im Carousel bricht den Fluss **Was bei uns auf dem Blatt liegt:** vier Sechsecke auf den vier Nähten (abwechselnd massiv und Kontur, unterschiedliche Höhen), die G-Marke auf der Naht 2|3, und ein durchgehendes Band über die volle Breite als Klammer. Das ist ein **globaler** Schalter, kein Muster pro Slide — er wirkt naturgemäß auf alle fünf. Die Muster pro Slide bleiben zusätzlich verfügbar; für den reinen Panorama-Look überall auf „Aus" stellen. > Gängige Empfehlung sind 2–4 Slides fürs Panorama. Wir haben fünf. Das > funktioniert, verlangt aber eine ruhigere Gestaltung — bei fünf Fenstern wird > jede zusätzliche Form schnell unruhig. ### Intensität Die Stufe „Dezent" (Faktor 0,55) wirkt **nur auf Nebenelemente** — Hexagon-Kontur, Waben, Punkt- und Linienraster, G-Marke. Der große Block oben rechts bleibt immer deckend, sonst wäre er nicht mehr exakt `#30313D`. --- ## 6. Mockups Kein fotorealistischer Laptop-Rahmen für die Feature-Slides: Die Website rahmt jeden Screenshot mit dem Chamfer (`.screenshot-frame` in `globals.css`). Das ist die markeneigene Mockup-Form und verbindet die Posts mit der Seite. Der Laptop auf der Titel-Slide ist die Ausnahme und bewusst ein Blickfang. ### Geometrie pro Slide Breite, Seitenverhältnis und Position sind CSS-Variablen mit Defaults: ``` --mock-w 1090px (ragt ~100 px über die rechte Kante) --mock-ar 1.6 (16:10) --mock-left 92px ``` **Regel: Das Format des Rahmens folgt dem Inhalt des Screenshots, nicht umgekehrt.** Der Form Builder ist eine schmale, zentrierte Spalte mit breiten leeren Rändern — in einem 16:10-Rahmen zeigt er vor allem Leere. Er bekommt deshalb 540 × 720 (3:4), mittig gesetzt, und einen Zoom von 3,8 direkt auf die Spalte. ### Screenshots zuschneiden Quellen sind ~2000 px breit und im Vollbild unlesbar. Jeder Screen wird auf seinen aussagekräftigen Bereich gezoomt. Die Werte sind gerechnet, nicht geraten: ``` Rahmenbreite = 1 Einheit, Bild bei scale 1 = 1 Einheit breit Bildhöhe = 1/BildAR Rahmenhöhe = 1/RahmenAR Punkt fx horizontal mittig: tx = 0.5/s − fx Punkt fy vertikal mittig: ty = (RahmenH/2)/(s · BildH) − fy ``` Mindest-Zoom zum Füllen: `RahmenH / BildH`. Darunter entstehen Lücken. **Ausnahme Titel-Slide:** Der Laptop zeigt den Screen **1:1**, ohne Zoom. Dort soll ein Bildschirm zeigen, was ein Bildschirm zeigt. Die Displayfläche misst 739 × 489 (AR 1,511), `workflow.png` hat 1,504 — passt praktisch deckungsgleich. ### Neue Geräte-Mockups einbauen 1. Braucht **echte Transparenz**. Ein Screenshot eines transparenten PNGs ist keine Transparenz — das Karomuster steckt dann als Pixel drin und lässt sich nicht sauber entfernen (einmal versucht, Ergebnis unbrauchbar) 2. Ist die Displayfläche deckend, muss sie **ausgeknockt** werden, damit der Screenshot dahinter liegen kann. Nur so bleiben Notch und Rundungen oben 3. Displayfläche ausmessen und als Prozentwerte eintragen — sie hängt am konkreten Bild 4. Schatten über `filter: drop-shadow()` auf dem Container, nicht im PNG --- ## 7. Export & Bildqualität ### Die zwei Regeln, aus denen alles folgt **1. Genau 1080 px breit hochladen.** Instagram behält bei Breiten von 320 bis 1080 die Originalauflösung. Alles darüber wird **von Instagram selbst** verkleinert und danach neu komprimiert — Qualitätsverlust in zwei Runden statt einer. Ein 4000-px-Upload ist deshalb schlechter als ein 1080-px-Upload, nicht besser. **2. Trotzdem größer rendern.** Wir rastern intern bei **2×** (2160 × 2700) und skalieren selbst auf exakt 1080 × 1350 herunter. Das ist kein Widerspruch zu Regel 1, sondern der eigentliche Trick: Wir kontrollieren das Downsampling, statt es Instagram zu überlassen. ``` 2160 × 2700 → Halbierung → 1080 × 1350 → Upload render exakt ``` ### Warum das Supersampling hier hilft Nicht wegen des Textes — den rastert der Browser bei nativer Größe ohnehin sauber, teils sogar schärfer als supersampled. Der Gewinn liegt woanders: - **Transformationen** — der gekippte Laptop, gedrehte Formen, Chamfer-Kanten und Hexagon-Clips werden bei doppelter Auflösung gerastert und danach gemittelt. Das ist echtes Anti-Aliasing statt Treppenkanten - **Screenshots** — unsere Quellen sind ~2000 px breit und werden auf ~1090 px gezeigt. Bei 2× zeichnet der Browser sie näher an ihrer nativen Auflösung, das anschließende Downsampling ist unser eigenes und damit besser - **Marker und Wabenraster** — SVG-Kanten profitieren identisch > Bei reinen Textslides ohne Mockup ist der Unterschied gering bis null. Deshalb > ist die Stufe umschaltbar (1× / 2× / 3×), nicht fest verdrahtet. **2× ist der > Standard**, 3× kostet spürbar Zeit und Speicher bei kaum sichtbarem Gewinn. ### Warum stufenweises Halbieren Browser skalieren Canvas standardmäßig schnell statt gut: Pixel werden direkt übernommen, Nachbarinformationen gehen verloren, das Ergebnis rauscht. Zwei Gegenmaßnahmen, beide nötig: ```js ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; // Default ist "low" ``` Und: **in Halbierungsschritten** verkleinern statt in einem Sprung. Ein direkter Sprung von 3240 auf 1080 überspringt Zwischenpixel; zweimal halbieren mittelt jeweils über 2 × 2 und kommt einer echten Flächenmittelung nahe. `createImageBitmap()` mit `resizeQuality` wäre der elegantere Weg, ist aber in Browsern uneinheitlich umgesetzt — teils wird die Option ignoriert. `drawImage` mit `imageSmoothingQuality` ist der verlässliche Pfad. ### Format **PNG, nicht JPEG.** Instagram konvertiert am Ende ohnehin zu JPEG — aber unsere Slides sind Text, flache Flächen und UI-Screenshots mit harten Kanten. Genau die zerlegt JPEG-Kompression zuerst. Wer als PNG hochlädt, gibt Instagram ein verlustfreies Original und zahlt nur **einmal** Kompression statt zweimal. Für Fotoinhalte wäre JPEG bei 85–95 % die bessere Wahl. Haben wir hier nicht. ### Farbraum **sRGB, ausschließlich.** Instagram wirft andere Profile weg und konvertiert nach sRGB. Wer in Adobe RGB oder P3 exportiert, bekommt Farbverschiebungen bei dieser Konvertierung. Canvas-Exporte aus dem Browser sind standardmäßig sRGB — also nichts zu tun, solange niemand ein Farbprofil anfasst. ### Weiteres - Auf `document.fonts.ready` warten, sonst rendert der erste Export in einer Fallback-Schrift - Dateinamen mit `01-`…`05-` präfixen: Instagram übernimmt beim Hochladen die Dateisortierung - Dateigröße im Auge behalten. Für JPEG gilt die Faustregel „unter 1 MB", sonst komprimiert Instagram härter. Für unsere PNGs mit Screenshots ist das nicht zu halten und auch nicht nötig - Vom Desktop hochladen, wenn möglich — gilt als schonender als der mobile Upload ### Quellenlage, ehrlich **Von Meta dokumentiert:** die unterstützten Seitenverhältnisse (1.91:1 bis 4:5), die Auflösungsgrenze 320–1080, der Beschnitt außerhalb, und dass alle Carousel-Slides auf die erste beschnitten werden. **Community-Konsens, nicht offiziell:** die konkreten Kompressionsprozente, die 1-MB-Faustregel, Desktop-vs-Mobile, und PNG-vs-JPEG-Empfehlungen. Plausibel und breit reproduziert, aber von Meta nirgends bestätigt. Im Zweifel selbst gegentesten: dieselbe Slide in zwei Varianten hochladen und vergleichen. ### Vier Dinge, die den Export brechen **1. Tailwind- und `oklch()`-Farben.** Die Export-Bibliothek kann sie nicht serialisieren. Der Slide-Baum benutzt deshalb ausschließlich Hex und `rgba()`. **2. PNGs im `foreignObject`.** Der Export serialisiert die Slide in eine `data:image/svg+xml`-URL und malt sie als **ein** Bild. Raster-Kinder dekodieren asynchron und verpassen diesen Paint regelmäßig — der Screenshot fehlt dann, während der Rest stimmt. SVGs dekodieren synchron und sind immer da. > Deshalb ist der Export **verifiziert statt gehofft**: Jede Screenshot-Fläche > wird nach dem Rendern abgetastet. Eine solche Fläche ist nie glatt, sie trägt > UI und Farbe. Liegt die Luminanz-Spanne unter 12, fehlt das Bild und es wird > neu gerendert, bis zu fünf Versuche. **3. Browser-Downloads.** `data:`-URLs öffneten einen leeren Tab, `blob:`-URLs das Bild im Tab — das `download`-Attribut wurde beide Male ignoriert. Der Generator schickt die Bytes deshalb per POST an **`/save`**, und `serve.py` schreibt direkt nach `~/Downloads`. **4. Stille Skriptfehler.** Ein Fehler auf oberster Ebene (Syntax, Temporal Dead Zone) tötet das ganze Skript: Das Panel rendert aus statischem HTML weiter, aber nichts wird gebaut. Beide Seiten haben einen globalen `error`-Listener, der so etwas ins Statusfeld schreibt. **Nicht entfernen.** --- ## 8. Fallen, in die wir schon getappt sind | Symptom | Ursache | |---|---| | Logo unsichtbar auf Weiß | Sichtbarkeitsregel vertauscht — auf Hell wurde `logo-white.svg` gezeigt | | Logo bildschirmbreit statt zentriert | `.laptop-screen img` galt auch fürs Logo und hat es überschrieben. Solche Regeln **immer auf eine Klasse eingrenzen** | | Element eine Zeile zu tief | `max-width` erzwang einen zusätzlichen Umbruch. Bei gesetzten `
` gehört `white-space: nowrap` dazu | | Farbe „stimmt nicht" | Deckkraft der Ebene lag darüber. Eine Farbe ist nur exakt, wenn nichts sie multipliziert | | Muster wirkt zufällig hingeworfen | Formen liefen quer durch den Text. Dekoration bleibt in den Rändern, verankert und angeschnitten | | Nichts lädt, Bühne leer | Konstante vor ihrer Deklaration benutzt (TDZ) | --- ## 9. Ordner ``` posts/ ├── serve.py / serve.sh Statischer Server + /save-Endpunkt (Port 4321) ├── post1.html Post 1 · Lösung (Carousel, 5 Slides) ├── index.html Post 2 · Statement (Einzelbild) ├── rules.md diese Datei ├── assets/ │ ├── logo.svg, logo-white.svg, cta-honeycomb.svg │ ├── laptop.png Mockup, Display ausgeknockt │ ├── fonts/ Manrope, lokal │ └── screens/ Software-Screenshots └── vendor/ html-to-image, JSZip (lokal, kein CDN) ``` Alles läuft **offline**. Keine CDN-Verweise, keine Google-Fonts zur Laufzeit — sonst hängt der Export an einer Netzwerkverbindung und an fremden Origins.