Compare commits

..
94 Commits
Author SHA1 Message Date
dejanandClaude Opus 5.5 07deacde41 Post 9: Varianten überarbeitet, Variante 6 und 7 nach Noahs Arbeitsheft
- Variante 2/3: Webinar statt HiveGrid, Noahs Satz zum Kopf des Chefs,
  vorher und heute am Beispiel „Angebot nachfassen“ als Schema
- Variante 4/5: drei verschiedene Grafiken, Wabenanimation gelb dann rot,
  Titel mit Wabenwelle animiert
- Variante 6/7: Aussagen, Zeichnungen und Schriften aus „Jede Frage lief
  über meinen Tisch“, Titel dunkel, Pfeile werden gezeichnet (Test)
- post9-hub.mjs legt alle Varianten als Bilder und Videos ins Marketing-Hub
- Menüs aller Posts und Übersicht mit Variante 6 und 7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:51:51 +01:00
dejanandClaude Opus 5.5 f9f48609dd Bildunterschriften für Post 7 und 8
Post 7: Form Builder, auf Instagram erschienen, Facebook offen.
Post 8: Text freigegeben, Post vorerst nicht veröffentlicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:51:50 +01:00
dejanandClaude Opus 5.5 466b094c0b Post 9 Variante 4/5: Noah gespiegelt, Wabengrafiken animiert
- Titelslide: Noah an x 800 gespiegelt, er schaut zum Text
- Slide 2 und 3 als Video (6 s, 1:1): die gelben Waben ploppen
  nacheinander auf und ziehen sich zum Schleifenende zurück
- Im Streifen laufen die Animationen mit, Standbild bleibt der Endstand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:22:27 +01:00
dejanandClaude Opus 5.5 5b768537a2 Post 9: Format 1:1 für die Anzeige und vier neue Varianten
- Format-Schalter 4:5 / 1:1 in Post 9, Stand auf 1:1 (Meta nimmt ein
  Carousel mit Video als Anzeige nur quadratisch), Video in 1:1
- animation.mjs baut das Format der Seite, Datei mit -1x1
- Variante 2/3: Kundengeschichte nach dem weclapp-Karussell, hell und dunkel
- Variante 4/5: Zahlen-Karussell nach HERO Insights mit KfW- und
  Bitkom-Zahlen, Noah als Teaser, hell und dunkel
- post9-varianten.html zeigt alle Varianten untereinander
- Menüs und Übersicht um die Varianten ergänzt, Regel zu 1:1 in rules.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:55:03 +01:00
dejanandClaude Opus 5.5 cafefc9f1b komponenten.md: Form Builder und gleiche Hoehe im Carousel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:26:21 +02:00
dejanandClaude Opus 5.5 fc69bebe96 Post 7: neu gebaut nach dem echten Form Builder
Kopf, Reiter, Fragekarten, offene Frage mit Streifen, "Nur anzeigen,
wenn", Werkzeugleiste und "Felder zuweisen" aus screens/forms mit den
Komponenten aus komponenten.md. Raus, weil es sie im Produkt nicht gibt:
die Pipeline am Lead, die Fusszeile mit "Zuletzt gespeichert" und
"Bereit zum Veroeffentlichen", die alten Feldtypen-Namen. Slide-Texte
angepasst, Video neu exportiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:26:21 +02:00
dejanandClaude Opus 5.5 4dd2ea1d18 Regel: Oberflaechen immer aus den Produkt-Komponenten
rules.md §0 macht es zur Pflicht, komponenten.md ist das Nachschlagewerk
mit Tokens, Komponenten, Bausteinen und Vorgehen, CLAUDE.md weist jeden
Agenten darauf hin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:03:33 +02:00
dejanandClaude Opus 5.5 35e68db15b Post 8: neu gebaut aus den Komponenten von hivegrid-cloud
Jede Zeichnung bildet jetzt einen echten Screen nach: Kampagnenbericht,
Zielgruppe bearbeiten, Vorlagen, Bereit zum Versand, Gestaltung. Schrift
Instrument Sans, Icons Lucide, Masse und Farben aus ui/theme.css und den
Subframe-Komponenten. Raus, weil es sie im Produkt nicht gibt: Reiter,
Haekchen-Chips, Produktkacheln. Instrument Sans wird im PNG-Export
eingebettet. Video neu exportiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:03:33 +02:00
dejanandClaude Opus 5.5 cd74224880 Post 9: Folien im Look der Webinar-Slides, ruhigerer Sprecherwechsel
Teil 1 dunkel mit G-Waben wie die Kapitel-Slides, die drei danach hell mit
dem zweiten Wabenmuster. Die Zeichen sitzen als grosse gelbe Wabe im Muster,
je ein Wort im Titel in Gelb. Die Live-Marke entfaellt, der Titel sagt es
schon. Noah spricht bei Teil 1 und 2, David bei 3 und 4. Video neu exportiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:04:53 +02:00
dejanandClaude Opus 5.5 78a6fcba78 Export: klare Meldung, wenn PostGrid nicht auf 4321 laeuft
Vorher hiess das nur "kennt keine Animationen".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:04:53 +02:00
dejanandClaude Opus 5.5 f38cbfe12c YouTube-Banner, vorlaeufig
2560 x 1440, Botschaft in der Mitte fuer alle Geraete, G-Waben drumherum.
Rendern mit kanaele/rendern.sh, die Bereiche je Geraet mit "zonen".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:55:49 +02:00
dejanandClaude Opus 5.5 b61f029a36 Regeln: hivegrid-cloud als neue Basis, Redaktionsplan mit Post 7 fertig
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:55:49 +02:00
dejanandClaude Opus 5.5 bc2a05012d Post 9: Webinar
Bewegtbild der vier Teile mit Noah und David, dazu die Slide zum Anmelden.
Termin, Uhrzeit und Dauer sind Platzhalter in WEBINAR. Die Uebersicht kennt
Post 8 und Post 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:55:49 +02:00
dejanandClaude Opus 5.5 ffa01fefcd Post 8: E-Mail-Marketing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:55:49 +02:00
dejanandClaude Opus 5.5 76ac3e4cce Pruefung der Texte: weniger Ausnahmen, Geviertstriche, Navigation bis Post 9
ae/oe/ue gilt hinter einem Vokal nicht mehr als fehlender Umlaut, Mailadressen
und Domains sind ausgenommen, Geviertstriche werden gemeldet. Das Menue nennt
Post 8 und Post 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:55:49 +02:00
dejanandClaude Opus 5.5 5a65b5444c PostGrid: fester Port 4321, die Wurzel fuehrt zur Uebersicht
Ist der Port belegt, bricht der Server mit einem Hinweis ab, statt
auszuweichen. So stimmt der Link aus dem Marketing-Dashboard immer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:40:58 +02:00
dejanandClaude Opus 5 c93702e87c Post 7: der Knopf federt nicht mehr zurueck, wenn die Frage aufklappt
Bei Sekunde 2 fuhr "Frage hinzufuegen" runter, neun Pixel wieder hoch
und ab t = 0,40 erneut runter -- eine Richtungsumkehr in vier Zehnteln.
Gemessen ueber alle 180 Bilder:

  Bild 43 - 60   681 -> 782   runter
  Bild 60 - 72   782 -> 773   hoch
  Bild 73 - 80   776 -> 800   runter

Schuld war rueck, also back.out(1,7): die Kurve ueberschwingt auf 1,099
und pendelt zurueck. Das passt zu etwas, das aufspringt. Der Knopf
springt aber nicht, er wird von der neuen Frage nach unten geschoben,
und Geschobenes federt nicht zurueck. Jetzt raus, power2.out: faengt
genauso schnell an -- der Klick soll sofort etwas ausloesen -- nur ohne
Rueckweg.

Nur die Kurve zu tauschen haette eine zweite Unsauberkeit hinterlassen:
das Aufklappen laeuft aus, der Knopf steht ein Zehntel still, dann setzt
er ruckartig wieder an. Der Platz fuer die Optionen waechst deshalb ab
0,36 statt ab 0,40 und geht in die auslaufende Bewegung ueber. Die
langsamste Stelle sind jetzt 0,9 px je Bild statt null; die Optionen
selbst erscheinen unveraendert ab 0,40.

Ueber alle 180 Bilder kein Schritt mehr rueckwaerts. Bewegtbild neu
gebaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-07 15:31:18 +02:00
dejanandClaude Opus 5 4a08cc361f Post 7: die Animation ist die Titelslide und traegt deren Ueberschrift
Sie ist das erste Element im Carousel, also muss sie die ganze Sache
tragen und nicht eine Einzelheit daraus. "Das Formular baust du selbst"
beschrieb die Bedienung -- das sieht man in der Animation ohnehin.

Fliesstext faellt weg, wie auf jeder Startslide. Damit sitzt die
Ueberschrift hoeher, und die Karte muss mit: visBottom 268 statt 168
macht aus 218 px Luft dazwischen 118, den Wert aller anderen Slides.

Die drei Pixel Hoehenversatz sind kein Wert aus der Vorlage, sondern
stecken bei Post 2 bis 6 im Projektstand als headline.dy -- gesetzt von
applyEdits, und da geht das Bewegtbild nicht durch. Gemessen sassen die
Startslides bei y 95, diese bei 92. Jetzt beide bei 95: im Stand fuer
die Slides, im animSlideBauen fuer die Animation. Dort direkt als
transform, weil die Regel hinter --ey an [data-edit] haengt und das
Attribut tagEditable() setzt -- noch eine Stelle, die das Bewegtbild
auslaesst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-07 15:31:18 +02:00
dejanandClaude Opus 5 2b91d2b177 Post 7: Stand
Aus dem Canvas: die Zeichnungen aller drei Slides nach oben gezogen
(dy -28, -126, -162). Sie schliessen damit dichter an den Fliesstext an,
und der Raum steht unten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-06 01:47:57 +02:00
dejanandClaude Opus 5 e2f44d61f2 Post 7: Fusszeile und Kopfabstand der Karte
DIE FUSSZEILE STAND AUS DER KARTE HERAUS. "Bereit zum Veroeffentlichen"
endete bei x 797, die Karte bei 788 -- neun Einheiten daneben. Ursache
war ein fester Versatz von 138, gegriffen statt gerechnet. Jetzt wird
die Textbreite gemessen und das Ende an die innere Kante gehaengt, egal
wie lang der Satz ist.

Dazu der Abstand: 24 oben und 22 unten, die Zeile klebte am Rand und die
Karte sah unten beschnitten aus. Jetzt 28 zu 28.

DER KOPF WAR ZU ENG. Die Trennlinie unter den Reitern liegt bei 92, der
Inhalt begann bei 96 -- vier Einheiten, und die erste Frage klebte an der
Linie. Der Kopf steht jetzt als FB_KOPF auf 120, also 28 Einheiten Luft,
derselbe Wert wie unten. Die feste Kastenhoehe des Bewegtbilds rechnet
mit derselben Konstante statt mit einer wiederholten Zahl.

Bewegtbild neu gebaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-06 01:46:35 +02:00
dejanandClaude Opus 5 96dcd080e1 Post 7: die Ueberschrift im Bedienfeld hiess noch Post 6
Beim Anlegen aus post6.html sind title und PROJECT geaendert worden, die
sichtbare <h1> im Bedienfeld nicht. Sie ist die dritte Stelle, an der der
Projektname steht -- und die einzige, die man beim Arbeiten dauernd
ansieht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-06 01:18:06 +02:00
dejanandClaude Opus 5 841408c6fa Redaktionsplan: der Form Builder steht auf Mo 07.09.
Er lag auf Mi 02.09., also zwei Tage nach dem Team-Chat. Die beiden
Termine 02.09. und 04.09. fallen aus, alles ab dort rueckt zwei Slots:

  Mo 07.09.  Form Builder            (war Mi 02.09.)
  Mi 09.09.  Zettelwirtschaft
  ...
  Mi 23.09.  Tempo und Uebersicht, das erste Reel   (war Fr 18.09.)

FERTIG steht auf 6: mit dem Form Builder liegen sechs Beitraege gebaut
im Generator. Die Zahlwort-Tabelle ging nur bis fuenf und waere beim
naechsten Beitrag mit einem KeyError abgebrochen -- sie reicht jetzt bis
acht.

PDF neu gebaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-02 19:48:30 +02:00
dejanandClaude Opus 5 4d227a01c9 Post 7: Form Builder, mit dem Bewegtbild als Titelelement
Anders als die Beitraege davor: das Bewegtbild ist das ERSTE Element im
Carousel und kein Anhang am Ende. Ein Form Builder ist ein Werkzeug, das
man bedient -- was er kann, sieht man am Bauen und nicht an einem
Standbild davon. Die drei Slides erklaeren danach, was das Gebaute
leistet, und tragen deshalb alle Ueberschrift und Fliesstext.

  animForm   Das Formular baust du selbst.       6 s, der Editor beim Bauen
  1          Frag genau das, was du wissen musst. die fuenf Fragen mit ihren Typen
  2          Fragen, die nur kommen, wenn sie
             noetig sind.                         das bedingte Feld
  3          Aus der Antwort wird ein Lead.       das Hinweisfeld mit Pipeline

Zwei Folgen davon, die man wissen muss: die Kachel im Profilraster ist
jetzt das erste Bild der Animation und nicht mehr eine eigens dafuer
gebaute Slide -- Bild 0 zeigt deshalb schon ein Formular mit zwei Fragen
und keine leere Flaeche. Und die Ueberschrift des Bewegtbilds ist die
Ueberschrift des Beitrags.

QUELLE
S02EinfacherFragenEditor.tsx aus dem Pitch-Paket, derselbe Bildschirm wie
assets/screens/form-builder.png. Die fuenf Fragen, ihre Feldtypen, die
Marke "Bedingt sichtbar" und der Satz im Hinweisfeld stehen woertlich
dort. Die Farben des Hinweisfelds sind ueber die Token-Kette aufgeloest
(surface-feedback-subtle-information -> feedback-information-100 ->
ocean-100 = #E1F4FD), nicht aus dem Bild geschaetzt. Elf zusaetzliche
Zeichen aus demselben Phosphor-bold-Satz.

ABWEICHUNGEN, ALLE DOKUMENTIERT
Die Quelle schreibt {general ? "Website-Anfragen" : "Vertrieb SHK"} --
hier steht Vertrieb SHK, weil der Beitrag durchgehend das Gewerk zeigt.
"Sanitaer" und "Klima / Lueftung" stehen mit Umlaut. "Deaktivieren" ist
weg, weil ein Ausschalter nichts zur Aussage beitraegt und dem
Pipeline-Satz den Platz nahm.

DREI SACHEN, DIE ERST AM GERENDERTEN BLATT AUFFIELEN
Die Karte wuchs und schrumpfte zugleich: der Kasten wurde mit dem Inhalt
hoeher, und bei fester Breite skaliert die Zeichnung dabei kleiner. Die
Animation hat jetzt eine feste Kastenhoehe.

Der Pipeline-Satz lief unter den Knopf und endete bei "Vertrieb S".
Jetzt zweizeilig statt gekuerzt -- der Pipelinename ist die halbe
Aussage des Felds.

Eine Ueberschrift mass 936 px und lief 45 px ueber die Schutzzone. Am
gerenderten Bild nachgemessen, nicht geschaetzt; die kuerzere Fassung
liegt bei 846.

Erreichbar ueber die Toolbar in allen Projekten und in uebersicht.html.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-01 19:46:34 +02:00
dejanandClaude Opus 5 344645cb77 Umlaute: der Export prueft sie, statt sich aufs Erinnern zu verlassen
Auf einer Slide stand "Sanitaer" und "Klima / Lueftung". Beides kam
woertlich aus dem Klickdummy, wo es schludrig eingetippt ist, und beides
las sich im fertigen Beitrag wie ein Fehler -- weil es einer ist.
Gefunden hat es niemand beim Bauen, sondern jemand, der auf den fertigen
Post geschaut hat.

Ein reines Muster genuegt als Pruefung nicht: Deutsch schreibt "neue",
"Dauer" und "Michael" voellig zu Recht mit ae/oe/ue. Nachgezaehlt ueber
den gezeichneten Text aller sieben Projekte sind es aber nur acht solche
Woerter, alle davon richtig oder gewollt (die Kanal-Handles). Also
Muster plus kurze Ausnahmeliste -- null Fehlalarme, "Sanitaer" faellt
trotzdem auf.

Bei ss ist es umgekehrt: 15 Treffer im gezeichneten Text, alle richtig
(dass, Schluss, Netzanschluss). Deshalb dort eine Liste der wenigen
Woerter, die ein scharfes s brauchen, statt eines Musters.

Geprueft wird an zwei Stellen, weil es zwei Wege zum fertigen Bild gibt:

  renderSlide()   bricht ab, bevor ein PNG entsteht
  animation.mjs   an 24 Stellen ueber den Ablauf

Der zweite Punkt war ein eigener Fehler: der erste Anlauf prueft nur den
Anfangszustand und fand nichts, weil die falschen Woerter dort erst ab
t = 0,4 erscheinen. Der Text einer Animation aendert sich mit der Zeit,
die Pruefung muss es auch.

Beide Tore sind mit absichtlich wieder eingebautem "Sanitaer" scheitern
gesehen worden. Eine Pruefung, die man nie hat scheitern sehen, ist
keine -- der erste Test lief durch, weil mein Testskript htmlToImage
direkt aufruft und renderSlide dabei umgeht.

Regel und Begruendung stehen in rules.md 4, die Falle in 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-09-01 19:45:08 +02:00
dejanandClaude Opus 5 6b9f7a4ccf Redaktionsplan: der Team-Chat ist am 31.08. erschienen
Geplant war Mi 26.08. Der Beitrag lag noch beim Feinschliff, damit sind
zwei Termine ausgefallen -- 26.08. und 28.08. Beide stehen jetzt in
AUSGELASSEN, und alles ab dort rueckt zwei Slots nach hinten:

  Mo 31.08.  Team-Chat        (war Mi 26.08.)
  Mi 02.09.  Form Builder     (war Fr 28.08.)
  ...
  Fr 18.09.  Tempo und Uebersicht, das erste Reel   (war Mo 14.09.)

Die vier Beitraege davor behalten ihr Datum, sie sind erschienen. Das
ist dieselbe Regel wie beim Ausfall am 24.08.: den Start zu verschieben
haette rueckwirkend andere Termine behauptet.

FERTIG steht auf 5. Mit dem Team-Chat sind fuenf Beitraege gesetzt und
gebaut; die Tabelle hebt sie hervor, und der Satz darueber nennt die
Zahl aus derselben Konstante.

Der Kommentar am Reel-Termin nennt keine Zahl mehr. Er stand einmal auf
"09.09." und war schon damals falsch, dann auf "11.09." und war es nach
der naechsten Verschiebung wieder. Ein Datum, das sich aus AUSGELASSEN
ergibt, gehoert nirgendwo als Zahl hingeschrieben -- auch nicht in einen
Kommentar. Im Fliesstext steht es ohnehin als {REEL} und ist auf 18.09.
mitgewandert.

PDF neu gebaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-31 16:06:39 +02:00
dejanandClaude Opus 5 dfbf2ce2f3 Post 6: Stand
Aus dem Canvas: die Zeichnung auf Slide 2 groesser (950) und versetzt,
Slide 4 um acht Pixel nachgerueckt, Slide 5 zurueckgesetzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 20:51:07 +02:00
dejanandClaude Opus 5 b422b492e6 Slide 4: das Menue haengt sichtbar am Pluszeichen
Vier Sachen an derselben Slide.

BREITE AUS DEM INHALT. Sie stand als feste 470 in der Zeichenfunktion,
gemessen fuer Beschriftungen mit "senden", und liess ein Drittel
Leerraum rechts. Sie kommt jetzt aus dem laengsten Eintrag. Dabei
kippte die Zeichnung von quer auf hochkant, und ein hochkantes Bild
richtet sich nach der Hoehe: bei den alten 940 px wurde der Kasten
1093 hoch und lief in die Ueberschrift. Jetzt 680.

Gerechnet ist die Breite am sichtbaren Blatt und nicht am Kasten -- der
ist um den Stapelrand groesser als das, was man sieht, und mit seiner
Oberkante gerechnet stand die Zeichnung noch einmal 60 px zu hoch.

OBEN OHNE VERB. Wer das Menue ueber das Pluszeichen geoeffnet hat,
weiss, dass er etwas schickt; "Dokument senden", "Fotos und Videos
senden", "Audio senden", "Kontakt teilen" schreiben es viermal daneben.
Jetzt stehen dort die Dinge: Dokumente, Medien, Audio, Kontakt. Unten
bleibt das Verb, weil es etwas hinzufuegt -- "Termin" allein waere ein
Anhang, "Termin erstellen" ist eine Handlung. Das ist eine Abweichung
vom Composer und steht als solche in POST-6.md 4.

DIE EINGABEZEILE IST ZURUECK. Sie war weggenommen worden mit der
Begruendung, die Slide zeige ja, was man anhaengen kann. Das stimmte
nur, solange man wusste, worauf man schaut: ohne das Feld ist die Karte
eine Liste im Nichts. Das Pluszeichen traegt jetzt die gruene Marke wie
ein gedruecktes Element, der Zeiger steht darauf, und das Menue klappt
nach oben auf -- so wie im Werkzeug.

TEILEN STATT TELEFONHOERER. Ein Hoerer sagt "anrufen"; bei "Kontakt"
wird eine Karte weitergegeben. ShareNetwork aus demselben
Phosphor-bold-Satz, entnommen aus @phosphor-icons/react im
Website-Repo statt abgemalt. "phone" stand danach nirgends mehr und ist
aus der Tabelle heraus: 19 Zeichen, jedes in Gebrauch.

Das Bewegtbild ist nicht betroffen, es nutzt das Nachrichtenmenue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 20:51:07 +02:00
dejanandClaude Opus 5 9c1d900da9 Chat: die Menschen im Verlauf bekommen Profilbilder
Vier Unsplash-Aufnahmen unter assets/eigene, die avatarKreis() nimmt,
sobald sie da sind -- vorher stand dort das Kuerzel. Nur Menschen
bekommen eins, Projektkanal und Gruppe behalten ihr Sinnbild.

  portrait-sk  Sandra Koch        unsplash.com/photos/SJvDxw0azqw
  portrait-mb  Maike Berger       unsplash.com/photos/ZAo0cKz_IKM
  portrait-tw  Tobias Wagner      unsplash.com/photos/TTdJV-K1IUg
  portrait-ms  Michael Schuster   unsplash.com/photos/7YVZYZeITc8

Ausgewaehlt nicht nach Gesicht, sondern nach Unterscheidbarkeit im Kreis
von 42 px: dort zaehlt der Gesamtwert und die Silhouette. Die vier
decken hell, mittelhell, dunkel und mittel ab, dazu kurze Locken,
schulterlang, Schiebermuetze mit Bart, kurz und glatt.

WAS DARAN OFFEN BLEIBT, UND ZWAR WISSENTLICH
Die Unsplash-Lizenz deckt das Urheberrecht des Fotografen, kein Model
Release. Ein fremdes Gesicht als "Sandra Koch, Projektleitung" auf dem
Firmenkanal beruehrt 22 KUG und die DSGVO. Ein Zwischenstand wich
deshalb auf erzeugte Gesichter aus (StyleGAN2) und ist zugunsten echter
Aufnahmen zurueckgenommen worden. Rechtssicher waere nur
model-released Stock. Steht in POST-6.md 4.

Geprueft ist jede der vier Seiten einzeln: Unsplash-Lizenz vermerkt,
keine KI-Kennzeichnung, benannter Fotograf, Datum zwischen 2017 und
2023.

DREI FALLEN
  Von 56 Suchtreffern waren 32 Profilbilder der Fotografen, nicht deren
  Aufnahmen -- ein figure img greift das Avatarbild des Urhebers mit ab,
  und die sehen aus wie Portraits, weil sie welche sind. Zwei davon
  waren schon eingesetzt, bevor es beim Aufschreiben der Herkunft
  auffiel. Erkennbar an photo- gegen profile- in der URL.

  Zwei gegenlaeufige Teilstuecke in einem Pfad loeschen ihren Ueberlapp
  weg, und ein Rechteck in Zeichnungsgroesse trifft nicht, was darueber
  hinaussteht. Beides gehoert zum Band davor, beides fiel erst hier auf.

Zuschnitt braucht keinen Fuellton: ein Business-Portrait hat Luft ueber
dem Kopf. Skaliert mit lanczos plus leichtem unsharp, 320 x 320 JPEG --
was beim Verkleinern an Schaerfe verlorengeht, holt keine
Renderqualitaet zurueck.

Bewegtbild neu gebaut, die Avatare stehen auch darin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 16:33:37 +02:00
dejanandClaude Opus 5 16503edfa2 Slide 5: das Band legt sich um den Stapel, statt darauf zu liegen
Der Knick sass 70 Einheiten vor der Plattenkante und lag damit mitten
auf dem Bildschirm -- das sah aus wie ein Spruchband, das jemand
daraufgeklebt und in der Mitte geknickt hat. Ein Band knickt nicht um
nichts: der Knick gehoert an die Kante, weil dort etwas ist, worum es
sich legen kann.

Jetzt knickt es genau dort um, wo die Platte aufhoert, laeuft dahinter
weiter und kommt an ihrer Kante wieder hervor: tiefer, weil es
umgeschlagen ist, niedriger, weil es wegfuehrt, flacher getont, weil man
von dort seine Unterseite sieht.

Die Stelle wird nicht gerechnet. Beide Stuecke reichen absichtlich weit
in die Platte hinein, und zwei Masken schneiden sie an deren Kante ab --
bandSilhouette die vordere Flaeche innen, bandAussen die Stuecke
draussen. Der Knick sitzt damit immer auf der Kante, auch wenn sich die
Platten einmal verschieben.

Zwei Fehler steckten in der Naht dazwischen, beide erst im erzeugten
SVG und beim Auszaehlen der Pixel sichtbar:

  Beide Masken kamen aus derselben Kontur, aber die innere trug einen
  schwarzen Strich darauf und war damit um eine halbe Haarlinie kleiner
  als die aeussere gross war. In der Luecke war das Band von beiden
  Seiten halb durchsichtig -- ein blasser Streifen genau im Falz. Die
  Falzkante kommt jetzt als eigener Strich darueber.

  Die Abdunklung lag auf einem Rechteck 0 0 W H. Die Stuecke stehen
  ausserhalb von 0..W und wurden nie getroffen, waren also heller als
  das Band daneben -- verkehrt herum fuer etwas, das weiter hinten
  liegt. Nachgemessen quer ueber die Naht: Stueck 125, Band 132.

Der Lichtabfall ist auf die Haelfte zurueckgenommen (0,80 -> 0,42):
zusammen mit dem dunkleren Stueck wurde daraus sonst ein brauner
Schatten statt eines Bandes.

Ohne die zwei Festlichkeiten des klassischen Spruchbands: keine Zacken,
kein Bogen. Ein gewoelbtes Band ueber einer flachen Oberflaeche sieht
nach Urkunde aus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 16:32:46 +02:00
dejanandClaude Opus 5 77734b6da2 Server: Assets werden nachgefragt statt jedes Mal neu geladen
Auf allem stand no-store -- im Server und noch einmal in preloadAssets().
Der Browser durfte also nichts behalten, und jeder Reload lud die vollen
Assets neu: 3 MB Screenshots, 3,3 MB Mockups, Schriften, Bibliotheken.

Am schlimmsten in uebersicht.html. Die laedt die sechs Projekte als
iframes, und weil keiner etwas behalten durfte, holte sich jeder iframe
dieselben Dateien noch einmal: 38,1 MB in 435 Anfragen.

  uebersicht.html erster Aufruf   38,1 MB  ->  2,2 MB
  uebersicht.html Reload          32,8 MB  ->  1,9 MB
  post6.html Reload                5,4 MB  ->  0,3 MB

Die Absicht hinter no-store war richtig: ein Bild, das unter demselben
Namen ausgetauscht wird, muss sofort ankommen. no-cache leistet das
billiger -- der Browser behaelt seine Kopie, fragt aber vor jeder
Benutzung nach. Nachgeprueft: unveraendert antwortet der Server mit 304
und 0 Bytes, nach einem touch mit 200 und der vollen Datei.

Seite, Stand und Auskuenfte (/bilder, /state) bleiben bei no-store. Die
sind klein und duerfen nie aus einer Kopie kommen.

Die eine Zeile stand in allen sieben Projektdateien.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 16:31:25 +02:00
dejanandClaude Opus 5 e8f5686f30 Bewegtbild: die Kurven von GSAP, der Zeiger faehrt einen Bogen
Die Website macht ihre Bewegungen mit gsap 3.14.2. Uebernommen sind die
Kurven, nicht das Paket: was GSAP darueber hinaus kann -- Zeitleisten,
ScrollTrigger, MotionPath -- setzt voraus, dass etwas laeuft. Hier
laeuft nichts, animChat(i, t) bekommt seinen Fortschritt von aussen und
ist damit selbst schon die Zeitleiste. Ein paused timeline mit
.progress(t) waere dieselbe Konstruktion mit 70 KB darunter, und ein
Easing ist eine Zeile.

Zwei der Kurven waren ohnehin GSAPs: daempfen ist Wort fuer Wort
power2.inOut, rueck ist back.out(1,70158). Die Namen stehen jetzt
daneben, dazu kommen raus (power2.out) und knall (expo.out).

  Der Zeiger fuhr auf power2.inOut, also langsam an, in der Mitte
  schnell, wieder abbremsend -- die Bewegung eines Schlittens. Eine
  Hand ist sofort schnell und wird zum Ziel hin langsam.

  Und er fuhr auf der Geraden. Jetzt auf einer quadratischen Bezier,
  Stuetzpunkt 13 % der Strecke quer nach aussen, vom Verlauf weg. Rund
  27 px Ausbuchtung auf 410 px Weg: es faellt nicht auf, das ist der
  Punkt.

  Der Klickring geht auf zwei Kurven, Radius auf expo.out und die
  Deckung linear hinterher. Beides auf derselben sah aus wie eine
  Blase, die sich aufblaest.

  Der Anlauf ist von 0,6 s auf 0,18 s geschrumpft. Am Ende steht das
  Bild eine halbe Sekunde still, damit man das Ergebnis liest; lief
  vorn nochmal eine halbe, waren das ueber den Schleifenstoss hinweg
  1,2 von 6 Sekunden Stillstand am Stueck.

MP4 neu gebaut, Werte unveraendert: H.264, 1080x1350, 30 fps, 6,0 s,
stumme AAC-Spur. Geprueft ist das an Kontaktabzuegen ueber die 180
Bilder, nicht am Lauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-28 20:37:21 +02:00
dejanandClaude Opus 5 7804587ca0 Slide 5: das Absperrband laeuft hinter die Ausschnitte
Vorher lief es ueber die ganze Zeichnung hinaus und endete an der
Bildkante, quer zur eigenen Laufrichtung abgeschnitten -- das ist das
Zeichen fuer ein Ende, so liest sich ein Aufkleber. Jetzt bricht es an
einer senkrechten Plattenkante ab, waehrend es mit 13 Grad ankommt, und
das ist keines: dort laeuft es hinter die Platte, um den Stapel herum.

Beschnitten wird an der Silhouette beider Ausschnitte. Damit macht es
die Website nach, die ihr Band am Rand des Telefons abschneidet ("so
the ends cut off like real tape"), und der Gegensatz "absperren oder
beschneiden", den der Kommentar hier behauptete, faellt weg: die
Silhouette beider Platten ist die ganze Sache.

Drei Dinge tragen es, keines verbiegt die Form:
  die Schnittkante ist die Plattenkante und steht schraeg zum Band
  sie ist weicher als eine Linie und haerter als die Platten (1,1)
  die letzten 56 Einheiten verlieren Licht, mit dem Hoehepunkt als
    Plateau davor -- auf der Kante selbst blendet die Maske schon aus

Der Verlauf steht dafuer in den Koordinaten der Zeichnung statt in
denen des gedrehten Bandes: er soll parallel zu der Kante stehen, an
der er endet.

Mit weg sind zwei Kommentarbloecke, von denen einer einen Stand
beschrieb, der einen Commit vorher zurueckgenommen worden war, und mit
ihnen WEB.bandKnick und WEB.bandSpitze.

Geprueft ueber beide Wege, Browser-Screenshot und html-to-image: die
Maske ueberlebt den Export, das Bild ist identisch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-28 20:37:05 +02:00
dejanandClaude Opus 5 d8b010fbdc Slide 5: die Tiefe des Bandes kommt aus Licht statt aus Geometrie
Zwei Anlaeufe haben die Form verbogen: erst die Enden zur Spitze verjuengt,
dann um eine Knickstelle gekippt. Das erste sah aus wie eine Schleife, das
zweite wie eine Welle. Ein Absperrband ist flach und straff -- was ihm gefehlt
hat, war Woelbung, nicht ein Knick.

Jetzt quer ueber die Hoehe ein Verlauf von #F8C64A ueber #F1B729 nach #D89F1B,
knapp unter der Oberkante ein Glanzlicht, und zu beiden Enden hin 20 % Abdunkeln.
Die untere Laengskante ist einen Pixel dicker als die obere, weil sie im
Schatten der Woelbung liegt. Die Form bleibt das Rechteck, das sie auf der
Website ist.

Helm und Schrift stehen weiterhin einmal und mittig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:16:39 +02:00
dejanandClaude Opus 5 bfe6b5cb4b POST-6: die zwei Abweichungen am Absperrband nachtragen
Der Code war im vorigen Commit, die Beschreibung ist ins Leere gelaufen, weil
ich sie an einen Absatz haengen wollte, den es so nicht gab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:08:28 +02:00
dejanandClaude Opus 5 1836a90a1a Slide 5: das Absperrband knickt an den Enden weg
Es war ein Rechteck mit gleichmaessig durchlaufender Schrift und las sich damit
als Aufkleber auf der Flaeche, nicht als Band darueber.

Jetzt laufen die Aussenkanten zur Mitte zusammen (10 % der Bandhoehe je Kante),
die Knickflaechen sind dunkler (#D6A11E statt #F1B729), an der Knickstelle
steht eine Kante, und die dunklen Laengskanten folgen dem Knick mit statt
darueber zu schweben.

Ein erster Anlauf knickte bei 21 % der Laenge und mit 17 % Verjuengung -- das
sah aus wie eine Schleife. Bei 12 % und 10 % bleibt die Mitte flach und nur die
Enden fuehren weg.

Helm und Schrift stehen einmal, mittig auf der geraden Flaeche. Wiederholt und
am linken Rand beginnend passte der Satz zu einem endlosen Band; auf einem,
dessen Enden sichtbar wegknicken, gehoert er in die Mitte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:07:24 +02:00
dejanandClaude Opus 5 20d26d3383 Chat: Avatare koennen Profilbilder tragen
avatarKreis() zeigt ein Foto, sobald eines unter
assets/eigene/portrait-<kuerzel>.<endung> liegt -- portrait-sk, portrait-mb,
portrait-tw, portrait-ms -- rund beschnitten, mit demselben Rand wie der
Kuerzelkreis. Ist keines da, steht wie bisher das Kuerzel im Kreis. Hochladen
geht ueber "Eigene Bilder" in der Seite; preloadAssets() holt alles aus
assets/eigene ohnehin mit, der Export bekommt sie damit als Daten-URL.

Mit einem Testbild durchgefahren und wieder entfernt.

Keine Fotos im Repo, und zwar mit Absicht: das Pitch-Paket laedt seine
Portraits von Unsplash -- vier Aufnahmen echter, erkennbarer Menschen. Die
Lizenz deckt das Foto, nicht das Recht am eigenen Bild. Ein fremdes Gesicht als
"Sandra Koch, Projektleitung" in einen oeffentlichen Beitrag zu setzen, ist
etwas anderes als ein Klickdummy fuer einen Kundentermin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:57:00 +02:00
dejanandClaude Opus 5 a94c1985fc Chat: eine gruene Marke fuer alle Chatarten, Symbole in bold
Projektkanal, Gruppe und Direktnachricht tragen jetzt dieselbe Marke -- Flaeche
#DCFCE4, Rand #B7EDC6, Zeichen #148134 -- in der Liste wie im Verlauf. Vorher
trug der Kanal Lila, die Gruppe Grau und jede Person ihre eigene Farbe: vier
Familien in einer Liste, und keine sagte etwas, das die Zeile nicht schon sagt.
Die Kuerzel unterscheiden die Personen, dafuer braucht es keine vier Farbtoene.

Dazu die Symbolgewichtung zurueck auf bold, diesmal gemessen statt geschaetzt.
Der Tausch von Feather auf Phosphor hat keinen Hexwert angefasst, sah aber
trotzdem anders aus -- zu Recht: duennere Formen haben weniger voll gedeckte
Pixel, das liest sich blasser. Im Kanalsymbol, Ausschnitt 44 x 44:

  Feather, wie es vorher war   59 volle Pixel, Deckung 180
  Phosphor regular             23 volle Pixel, Deckung 151  (16 % zu blass)
  Phosphor bold                62 volle Pixel, Deckung 192  ( 6 % darueber)

Ich hatte auf regular zurueckgestellt, weil es "im Menue leichter aussah".
Geschaetzt statt gemessen, und daneben. Wer eine optische Staerke beurteilt,
zaehlt Pixel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:27:34 +02:00
dejanandClaude Opus 5 b250b34b07 Symbole: Gewichtung regular statt bold
Getauscht sind nur die Formen, kein einziger Farbwert -- Plakettengruen
#148134, Lila der Projekte #691CD4, Grau der Nadel #9C9CA3 sind vor und nach
dem Wechsel identisch. Was sich geaendert hatte, war die Strichstaerke: bold
sind gefuellte fette Formen, und die lesen sich neben demselben Text dunkler
als duenne Striche. Das sieht aus wie eine andere Farbe, ist aber keine.

regular trifft die optische Staerke, die Feather dort hatte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:11:31 +02:00
dejanandClaude Opus 5 35ad9cc104 Symbole: Phosphor statt Feather, im ganzen Post
Der Feather-Satz steckt in so ziemlich jeder Oberflaeche der letzten Jahre; man
erkennt ihn sofort wieder, und damit sieht eine Zeichnung aus wie jede andere.
Am deutlichsten war das am Gruppensymbol.

Phosphor ist dieselbe Familie von Strichsymbolen, nur mit eigener Handschrift:
runder, gleichmaessiger, andere Metaphern (UsersThree statt Users,
PaperPlaneRight statt Send, CaretDown statt ChevronDown). Es ist dasselbe
Paket, das die Website benutzt -- @phosphor-icons/react steht neben
lucide-react in ihrer package.json -- also keine dritte Handschrift im Haus.

Die 19 Pfade sind direkt aus dem installierten Paket gezogen und nicht
abgeschrieben: Gewichtung bold, Zeichenflaeche 256 x 256. Regular war neben dem
Text zu leicht. Anders als bei Feather sind es gefuellte Formen, ikon() faerbt
deshalb ueber fill statt ueber stroke; der Parameter fuer die Strichstaerke
faellt weg. FEATHER und feather() sind raus.

Der vorige Versuch tauschte nur das eine Gruppensymbol gegen zwei ueberlappende
Scheiben. Das war keine Antwort auf die Frage: die Frage war der Satz, nicht
das einzelne Zeichen.

Nebenbei: anhangKarte() und uiKachel() hatten einen Parameter namens "ikon" und
verdeckten damit die neue Funktion. Heisst jetzt "zeichen".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:01:56 +02:00
dejanandClaude Opus 5 6a7cdc1580 Das Bewegtbild war versehentlich mitgeloescht
Im vorigen Commit stand animation/post6-animChat.mp4 als geloescht drin. Der
Inhalt hat sich nicht geaendert -- die Chatliste steckt in chatZeile(), die
Animation zeichnet mit blase() -- also zurueckgeholt statt neu gebaut, Byte
fuer Byte der Stand aus efcb864.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:42:41 +02:00
dejanandClaude Opus 5 a2cb7cbf6c Chatliste: gruene Plakette mit Zahl, Gruppe ohne Allerwelts-Symbol
Der Punkt fuer Ungelesenes war violett und leer. Violett ist im Produkt die
Farbe der Projekte -- das Kanalsymbol in derselben Zeile traegt sie schon, und
ein zweites violettes Zeichen daneben konkurriert damit, statt etwas anderes zu
sagen. Ein farbloser Punkt sagt ausserdem nur "da war was". Jetzt gruen, mit
der Zahl der ungelesenen Nachrichten darin, wie in jedem Messenger, den ein
Betrieb kennt. Die Uhrzeit derselben Zeile faerbt mit.

Die Zahlen (3, 2, 1) stehen nicht in der Quelle, dort ist unread ein
Wahrheitswert. Sie sind die einzigen erfundenen Werte im Post und stehen so in
POST-6.md.

Die Gruppe trug FeatherUsers -- das Zwei-Personen-Piktogramm, das in jeder
zweiten Oberflaeche steckt und danach aussieht. Jetzt zwei ueberlappende
Avatarscheiben, wie Teams und Linear sie an derselben Stelle zeigen; sie
sprechen dieselbe Sprache wie die Kuerzelkreise der Direktnachrichten daneben.
Ohne Buchstaben, weil die Mitglieder nirgends benannt sind.

Dabei gefunden: nach dem Vergroessern des Ausschnitts auf Slide 2 hatte ich
kollision.mjs mit Argument 1 laufen lassen -- das ist Slide 1. Auf Slide 2 ragte
das hinterste, gedrehte Blatt des Stapels fuenf Einheiten unter den Kasten.
stapelRand() rechnet den Ueberstand fuer die unskalierte Groesse, die
Skalierung vergroessert ihn mit. Der Kasten kommt jetzt aus dem skalierten
Stapel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:41:39 +02:00
dejanandClaude Opus 5 efcb864db1 Bewegtbild: das Kontextmenue schnappt auf statt zu kriechen
Es nahm dieselbe gedaempfte Kurve wie alles andere, ueber 0,8 Sekunden. Die
Deckung war darin nach fuenf Bildern fertig, waehrend die Skalierung noch eine
halbe Sekunde von 0,96 auf 1,00 weiterlief. Zu sehen war davon fast nichts, zu
spueren war es als Haenger -- und zwar genau in dem Moment, in dem geklickt
wird.

Jetzt rueck statt daempfen und ueber 0,34 statt 0,40 der Laufzeit: drei Bilder
Aufblenden, ein ganz leichtes Ueberschwingen, dann Ruhe. Die Deckung laeuft
dafuer mit Faktor 2,5 statt 4,5, sonst waeren es zwei Bilder und es knallte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:14:24 +02:00
dejanandClaude Opus 5 03d137a667 Post 6: Projektstand
Aus dem Browser: Muster auf Slide 4 auf kraeftig, Zeichnung auf Slide 2 und 4
verschoben (vis dy -155 und -67).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:58:11 +02:00
dejanandClaude Opus 5 be2f93a1fa Slide 4: drei der sechs Eintraege nennen, nicht alle
"Versende mehr als nur Textnachrichten: Dokumente, Fotos, Sprachnachrichten."
Drei Beispiele sagen, worum es geht; die vollstaendige Liste steht im Bild
daneben.

Zu spezifisch war an der alten Fassung nicht das Aufzaehlen, sondern das
Durchdeklinieren ("... Soll daraus etwas werden: Termin oder Aufgabe."). Ich
habe daraufhin zweimal in die andere Richtung uebersteuert und gar nichts mehr
genannt. Ein Fliesstext auf einer Feature-Slide erklaert das Feature: er
umschreibt es nicht und handelt es auch nicht in vier Woertern ab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:14 +02:00
dejanandClaude Opus 5 28095f667d Slide 4: "Versende mehr als nur Textnachrichten."
Was das Feature kann, in einem Satz. Die Eintraege stehen im Bild und muessen
daneben weder aufgezaehlt noch erklaert werden.

Davor drei Fassungen, die alle daneben lagen: erst die Aufzaehlung samt
Erklaerung ("Dokument, Fotos, Sprachnachricht, Kontakt. Soll daraus etwas
werden: Termin oder Aufgabe."), dann zwei Versuche, es allgemeiner zu fassen,
die sprachlich verstiegen waren. Ein Fliesstext auf einer Feature-Slide
erklaert das Feature, er formuliert es nicht aus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:52:19 +02:00
dejanandClaude Opus 5 732e890989 Slide 4: "Hinter dem Plus steckt mehr als das geschriebene Wort."
Der Satz fuehrt die Ueberschrift "Nicht nur Text." weiter und benennt dabei
nichts Einzelnes. Vorher stand dort die Aufzaehlung der Menueeintraege, danach
kurz eine Fassung ueber die zwei Haelften des Menues -- beides sagte, was
drinsteht, statt zu sagen, worum es geht.

Gemessen mit fliess.mjs im Schnitt des Fliesstextes und nicht in dem der
Ueberschrift, weil das eine andere Groesse und ein anderes Gewicht ist: 780 px
von 830 verfuegbaren, also eine Zeile. Eine Zwischenfassung ("... alles, was
sonst noch zum Projekt gehoert.") kam auf 903 px und liess "gehoert." allein
in der zweiten Zeile stehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:49:40 +02:00
dejanandClaude Opus 5 668f4036df Slide 4: der Fliesstext zaehlt die Menueeintraege nicht mehr auf
Dort stand "Dokument, Fotos, Sprachnachricht, Kontakt. Soll daraus etwas
werden: Termin oder Aufgabe." Damit stand die Liste doppelt -- im Bild und
daneben -- und wer sie mitschreibt, legt sich zugleich darauf fest, was
drinsteht. Aendert sich das Menue, luegt der Satz.

Jetzt "Was du teilst und was du erstellst, bleibt am Projekt." Das sind die
zwei Haelften, die das Menue selbst zieht: ueber dem Trenner steht, was man
sendet und teilt, darunter, was man erstellt. Beide Verben stehen so in den
Eintraegen.

Der Textblock wird dadurch eine Zeile kuerzer, die Luft zur Zeichnung waechst
von 155 auf 199. Die Zeichnung bleibt, wie sie ist: sie steht mit 96 und 97 px
schon knapp an der Schutzzone und kann nicht mehr breiter werden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:45:23 +02:00
dejanandClaude Opus 5 ac702c239b Bewegtbild: "Chatnachrichten werden Aufgaben im Projekt."
Zusammengeschrieben und nicht mit Bindestrich, weil die Breite entscheidet:
"Chat-Nachrichten werden" misst 916 px und laeuft ueber die 896 der
Schutzzone, "Chatnachrichten werden" sind 879. Beide Schreibweisen sind
korrekt.

Ebenfalls verworfen: "Aus Chat-Nachrichten werden" (1063 px) und "Aus
Chatnachrichten werden" (1027 px) -- mit "Aus" passt die Zeile nicht mehr,
ohne bleibt der Satz vollstaendig.

Nach dem Einsetzen am gerenderten Blatt nachgezaehlt statt nur gerechnet:
zwei Zeilen zu 879 und 733 px, Gesamthoehe 158 = zweimal die Zeilenhoehe.
Kein Umbruch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:31:06 +02:00
dejanandClaude Opus 5 64675688b2 Bewegtbild: neue Ueberschrift, kein Fliesstext, groesseres Bild
"Aus Nachrichten werden / Aufgaben im Projekt." statt "Getippt, / und schon im
Projekt." -- die Aussage steht damit in der Ueberschrift statt nur im Bild.
Mit breite.mjs gemessen, bevor sie in die Datei kam: 860 px in der ersten und
733 in der zweiten Zeile bei 896 erlaubten. "Aufgaben im richtigen Projekt."
waere mit 1067 px uebergelaufen.

Die Beschreibung faellt weg. Die Ueberschrift sagt es, die Bewegung zeigt es,
und die Systemzeile im Bild bestaetigt es -- ein Satz darunter waere die dritte
Stelle fuer dieselbe Aussage.

Die gut hundert Pixel, die dadurch frei werden, gehen an die Zeichnung: visW
von 700 auf 776, mittig bei visLeft 152. Luft zum Text 118, derselbe Wert wie
auf Slide 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:25:37 +02:00
dejanandClaude Opus 5 9ae27488d6 Slide 2: der Ausschnitt steht groesser da als die Zeichnung
Beide waren ungefaehr gleich gross und lasen sich damit als gleichwertig. Sie
sind es nicht: der Ausschnitt traegt das Produkt, die Plaettchen tragen nur
die Ausgangslage.

Platz war da. Gemessen endet die linke Haelfte samt Leitung bei x = 344, der
Kasten begann erst bei 464 -- also 120 Einheiten ungenutzt. Der Kasten steht
jetzt bei 388 und ist um 1,28 skaliert; die Zeichnung wird dadurch nur
geringfuegig kleiner, das Verhaeltnis aendert sich um rund ein Drittel.

Die Projektnummer misst dadurch wieder 15,8 px statt 12,4. Sie ist der Beleg
fuer den Fliesstext ("Jeder Kanal haengt an seiner Projektnummer") und muss
lesbar bleiben.

visBottom von 120 auf 70: die Zeichnung sass sonst hoch, mit 204 px Leere
unten und nur 99 zum Text. Jetzt 154 und 149, also in einer Reihe mit Slide 3
und 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:31:08 +02:00
dejanandClaude Opus 5 7fb1c3dfcb Vorschau: der Abspielen-Knopf tat beim ersten Klick nichts
animAbspielen rechnete performance.now() - ANIM_T[i] * dauer, damit die
Vorschau dort weiterlaeuft, wo der Regler steht. ANIM_T[i] ist aber undefined,
solange der Regler nie bewegt wurde: daraus wird NaN, der Fortschritt wird
NaN, und animT() liefert wegen seines eigenen "|| 0" brav wieder 0. Die
Vorschau stand still, ohne dass in der Konsole etwas auftauchte.

Bemerkt wurde es nicht, weil jede Pruefung vorher animSetzen() aufgerufen
hatte, um irgendetwas zu messen -- und damit genau den Zustand herstellte, in
dem es funktioniert. Ein Pruefskript, das den Anfangszustand anfasst, prueft
ihn nicht mehr.

animSetzen weist NaN jetzt zusaetzlich mit Number.isFinite ab, damit so ein
Wert gar nicht erst in den Stand kommt. In allen sechs Projekten nachgezogen.

Der Export war davon nicht betroffen; der Weg ueber POST /anim ist noch einmal
vom Knopf bis zur fertigen Datei durchgefahren worden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:21:46 +02:00
dejanandClaude Opus 5 9fe7b848ee Slide 3: der Rahmen um die Aufgabe ist weg
Duenner half nicht, im Beitrag sah es aus wie vorher. Die Slide braucht ihn
auch nicht: sie besteht aus drei Nachrichten, die aufeinander folgen, und die
Aufgabenkarte ist das einzige Element mit einer gelben Marke ("Offen") -- der
Blick geht ohnehin dorthin. Ein Rahmen sagt nochmal, was die Anordnung schon
sagt, und kostet dafuer die Ruhe der Flaeche.

Mit ihm faellt letzterAnhang weg: blase() merkte sich die Lage der zuletzt
gezeichneten Anhangkarte, damit der Rahmen nicht aus einer zweiten Rechnung
kommt. Das liest jetzt niemand mehr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:58:11 +02:00
dejanandClaude Opus 5 a884df4cda Slide 3: der Rahmen um die Aufgabe wird duenner
Sechs Pixel waren plump -- er trug mehr Farbe als die Karte, auf die er zeigen
soll, und im Profilraster wurde daraus ein gelber Balken. Jetzt drei.

Vier Alternativen geprueft und gerendert: ganz weg, ein weicher Schein, eine
gelbe Kante links, ein duenner Ring. Jede zusaetzlich auf Feed-Breite (480 px)
heruntergerechnet, weil dort entschieden wird, ob eine Linie noch etwas sagt.
Duenner als drei haelt das nicht aus: bei 480 px sind drei Pixel noch gut
einer, darunter verschwindet die Linie.

Der duenne Ring gewinnt, weil er als einziger die Aufgabe des alten behaelt,
naemlich auf genau diese Karte zu zeigen. "Weg" nimmt der Slide ihren Zeiger,
Schein und Kante markieren einen Zustand statt hinzuweisen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:44:11 +02:00
dejanandClaude Opus 5 35c998b7e1 Slide 2: die Leitung faengt am ersten Plaettchen an
Sie hatte einen Anlauf, der links vor dem Nullpunkt begann -- ein Rest aus der
Fassung, in der die Zeichnung ueber zwei Slides lief und auf Slide 1 schon
sichtbar war. Die ist laengst zurueckgenommen; ohne Nachbarslide kam das
Stueck aus dem Nichts und versprach einen Zusammenhang, den es nicht gibt.
Jetzt beginnt sie bei "Angebot" und endet bei "Materialliste", also an beiden
Enden an etwas.

Die Raender werden dadurch gleichmaessiger: 99 links zu 107 rechts statt 92 zu
106.

Dazu drei Stellen, die noch den zweiseitigen Aufbau beschrieben, obwohl er
nicht mehr existiert: der Kommentar am Slide-Eintrag sprach von einem
negativen visLeft (er ist 92), die Slide-Tabelle in POST-6.md behauptete "die
Leitung beginnt schon auf Slide 1", und ein Absatz erklaerte die Sonderregel
fuer die Slide-Kante. Ausserdem ein doppelter Kommentar in visKanaele.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:30:42 +02:00
dejanandClaude Opus 5 82913d78b8 Animation: die Leiste laesst sich zuklappen
Ein Klick auf den Kopf klappt sie zu, dann bleiben 40 px und der Canvas hat
den Platz. Sie ist dafuer ein <details> mit dem Kopf als <summary> -- damit
ist die ganze Zeile die Trefferflaeche und die Tastaturbedienung kommt von
selbst, statt dass ein eigener Knopf eine zweite Flaeche fuer dieselbe Sache
aufmacht.

Ob sie offen ist, merkt sich der Browser je Projekt im localStorage und nicht
der Projektstand: das ist eine Vorliebe und keine Gestaltung, in
state/<projekt>.json wuerde sie bei jedem einen Diff machen.

In allen sechs Projekten nachgezogen. Im Streifenmodus faellt sie weiterhin
ganz weg, die Uebersicht zeigt alle sieben Rahmen unveraendert, und der
Bildexport laeuft wie vorher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:22:46 +02:00
dejanandClaude Opus 5 a8f79c1224 Bewegtbild: die Aufgabe entsteht an der Nachricht, nicht im Plus-Menue
Geprueft im laufenden Team-Chat: an jeder Nachricht haengt ein Menue mit
Reagieren, Antworten, Im Thread antworten, Anpinnen, Aufgabe erstellen,
Bearbeiten, Loeschen. Das ist die Funktion, die die Website meint mit "Aus
'mach das mal' wird mit einem Klick eine echte Aufgabe".

Die Animation zeigt jetzt genau das: Tobias' Nachricht anfassen, der Pfeil
erscheint an ihr, Menue auf, "Aufgabe erstellen", die Aufgabenkarte klappt in
der Nachricht auf, die Systemzeile bestaetigt sie im Projekt.

Tobias' Nachricht und keine andere: die Aufgabenkarte aus der Quelle traegt
"Tobias Wagner · offen" als Zustaendigkeit. An einer fremden Nachricht muesste
der Name erfunden werden. So bleiben alle vier Felder der Karte genau die aus
GeneralTeamChatPage.tsx.

Der Vorgaenger nahm das Menue hinter dem Pluszeichen. Das war sauber belegt --
Datei, Zeile, Grep ueber beide Repos, dazu die Staging-API, die eine Aufgabe
nur mit contact/company/lead/project verknuepft und eine Nachricht als Bezug
gar nicht kennt -- und trotzdem falsch. Ein Pitch-Paket vom Juli belegt, was im
Juli gebaut war, nicht was das Produkt heute kann. Wo eine Aussage der Website
keine Entsprechung im vorliegenden Code hat, ist das ein Grund nachzufragen und
nicht, die Aussage auf den Code zu biegen. Steht so in POST-6.md §6.

Sechs statt acht Sekunden: das Tippen faellt mit dem richtigen Weg weg.
Instagram nimmt je Carousel-Element drei bis sechzig Sekunden an, die Laenge
richtet sich also allein nach dem Ablauf.

Dazu in den Bausteinen: fuenf Symbole fuer das Menue, und eine Blase kann eine
zweite Anhangkarte tragen, die ueber einen Anteil aufklappt -- aufgedeckt per
clipPath, nicht gestaucht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:42:53 +02:00
dejanandClaude Opus 5 d4feaa175b Bewegtbild: der richtige Weg von der Nachricht zur Aufgabe
Die erste Fassung hatte den Ablauf im Kreis: getippt wurde "Danke. Ich habe
dazu direkt eine Aufgabe erstellt.", abgeschickt -- und *danach* wuchs an
dieser Nachricht eine Aufgabenkarte. Sie meldete also eine Aufgabe, die es
noch gar nicht gab, und wurde anschliessend selbst zu einer.

Nachgesehen in GeneralTeamChatPage.tsx: eine Aufgabe entsteht ueber das Menue
hinter dem Pluszeichen ("Aufgabe erstellen", Zeile 275). Das Ergebnis ist
Nachricht m4 -- Text UND angehaengte Aufgabenkarte in einem -- gefolgt von der
Systemzeile m5. Ein Kontextmenue an einer bestehenden Nachricht gibt es dort
nicht; eine Umwandlung "Nachricht -> Aufgabe" zu zeigen waere eine Funktion,
die wir nicht haben.

Jetzt also: Notiz tippen, Pluszeichen, "Aufgabe erstellen" waehlen, und beides
geht zusammen als eine Nachricht raus, danach die Bestaetigung im Projekt. Der
Satz im Perfekt stimmt damit auch -- wenn die Nachricht ankommt, haengt die
Aufgabe daran.

Am Ende wird nichts mehr zurueckgenommen. Der Vorgaenger blendete die neue
Nachricht wieder aus, damit der Schleifenstoss unsichtbar wird; das sah aus,
als loesche sich eine abgeschickte Nachricht von selbst. Eine Schleife darf
springen, eine Nachricht darf nicht verschwinden.

Dazu ein Mauszeiger, der die Bedienung erzaehlt -- ohne ihn geht ein Menue
auf und man weiss nicht, warum -- und acht statt fuenf Sekunden: der Ablauf
hat jetzt fuenf Schritte, und jeder braucht seinen Moment.

Was ein Standbild traegt, traegt eine Animation nicht automatisch mit: auf
Slide 3 liest sich dieselbe Nachricht einwandfrei, weil dort ein Zustand steht
und keine Reihenfolge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 00:09:45 +02:00
dejanandClaude Opus 5 9e8b2ea607 Animation: das MP4 landet auch in Downloads, wie die Bilder
Der Export legte die Datei nur unter animation/ im Repo ab. Die Bilder gehen
seit jeher nach ~/Downloads, und von dort laedt man hoch -- ein Video an einem
anderen Ort ist eine Inkonsequenz, die man erst merkt, wenn man es sucht.

Jetzt beides: Downloads zum Hochladen, animation/ zum Versionieren. Wie beim
Bildexport wird in Downloads nichts ueberschrieben, eine zweite Ausgabe heisst
"-1" -- eine Datei, die vielleicht schon hochgeladen wurde, gehoert nicht
ersetzt.

Der Weg ueber die Kommandozeile schreibt weiterhin nur ins Repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 23:55:55 +02:00
dejanandClaude Opus 5 c7c05a85cc Animation: eigener Bereich unter dem Canvas, getrennt vom Bildexport
Jedes Projekt hat jetzt unter dem Canvas einen Bereich "Animation", im selben
Fenster. Er zeigt jede Animation als echtes Blatt in Vorschau, mit Zeitregler,
Abspielen und "MP4 exportieren".

Eine Animation ist keine Slide. Sie steht in ANIMS und nicht in SLIDES, liegt
also nicht im Streifen, taucht in der Slide-Auswahl nicht auf und wird vom
Bildexport nicht angefasst. Im Carousel ist sie ein eigenes Element, und ob
sie mitkommt, entscheidet sich beim Posten. Ein erster Anlauf machte sie zur
sechsten Slide; damit war sie in der Zaehlung und im ZIP mit drin.

Der Export laeuft ueber den neuen Endpunkt POST /anim in serve.py. Er startet
animation.mjs in einem eigenen Faden, die Seite fragt den Stand ueber
GET /anim?id= ab und zeigt Fortschritt und Pfad. Ein Browser kann kein MP4
schreiben, und eine offene Anfrage ueber zwei Minuten geht nicht -- zumal sich
animation.mjs waehrenddessen selbst Seiten von diesem Server holt.

Aufgenommen wird im neuen Modus ?nur=anim&k=<key>: dort zeigt die Seite allein
das Blatt der Animation, in 1080 x 1350 an 0,0. Damit entfaellt jede
Ausschnittsrechnung und mit ihr die Falle, dass ein zu schmales Fenster ein
leeres Bild liefert.

Als erstes Beispiel post6-animChat, fuenf Sekunden: die Antwort wird Zeichen
fuer Zeichen getippt, abgeschickt, waechst als Blase in den Verlauf, die
Aufgabenkarte klappt auf, die Systemzeile bestaetigt, alles blendet zurueck
auf den Anfang. Getippt wird VERLAUF[3].text, also die Nachricht, die im
Werkzeug an dieser Stelle ohnehin folgt.

H.264 High, yuv420p, 1080 x 1350, 30 fps, stumme AAC-Spur, +faststart: das
nimmt Instagram fuer ein animiertes Element im Carousel an. GIF nicht, das
landet als Standbild. Ein Video im Carousel wird auch kein Reel, das passiert
nur bei einem einzeln geposteten Video.

Nebenbei:
- post4, post5 und post6 trugen im Panel noch den Kopf ihrer Vorlage.
- video/README.md und video/bauen.mjs behaupteten, ffmpeg sei hier nicht
  installiert. Stimmt seit 9.0.1 nicht mehr; die Bildsequenz bleibt dort
  trotzdem, mit Begruendung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 17:58:29 +02:00
dejanandClaude Opus 5 ce87a7fb57 Post 6: fuenfte Slide fuer den mobilen Chat, Tiefe ueber den Stapel
Zwei Nachbesserungen an Post 6: eine Slide mehr und Zeichnungen, die nicht
mehr flach aussehen.

DIE FUENFTE SLIDE KOMMT VON DER WEBSITE
Die App gibt es noch nicht, und die Website sagt das bereits. In
components/sections/ChatDemoSection.tsx steht unter der Fahne comingSoon
genau dieser Zustand: ein Handy-Mockup, verwischt, und darueber ein
Absperrband aus dem Bau -- gedreht um -13 Grad, in accent-yellow F1B729 mit
Kanten in 171106, darauf immer wieder Schutzhelm und BALD VERFUEGBAR.

Ein Zwischenstand hatte hier einen selbst erfundenen gelben Aufkleber
"In Arbeit". Das war eine zweite Bildsprache neben einer vorhandenen.
Uebernommen ist jetzt die der Website, samt Winkel, Farben und Wortlaut.

Auch der Inhalt des Schirms kommt von dort und nicht aus dem Pitch-Paket:
Heizungsprojekt Schulz, "4 Mitglieder · 2 online", die vier Nachrichten aus
seedMessages, die Blasenfarben 0D6E6E fuer eigene und FFFFFF mit Rand E9E7E5
fuer fremde, der warme Grund hsl(30 17% 95.5%). Ueberschrift und Fliesstext
ebenso: "Der Chat fuer unterwegs wird gerade gebaut."

Das Geraet selbst ist eine bewusste Abweichung. Die Website zeichnet nur
einen hellen Umriss, border 4px D1D5DB; auf einer Slide, die selbst hell ist,
faellt der in sich zusammen und sieht nach Platzhalter aus. Hier steht ein
richtiges Geraet: dunkler Korpus, heller Grat, Schirm eingelassen, Insel und
Tasten. Die Abweichung betrifft den Rahmen, nicht den Inhalt. Verwischt wird
schwaecher als dort -- auf der Website liegt das Handy gross neben Fliesstext
und darf unlesbar sein, hier ist es klein, und bei voller Staerke blieb Brei.

DIE TIEFE
Zwei Anlaeufe waren falsch. Der erste legte graue Flaechen hinter die
Elemente: das nimmt den Zeichnungen ihre Durchsichtigkeit und setzt einen
Kasten auf den Punktgrund, der dort nicht hingehoert. Wieder raus, die
Zeichnungen sind wieder durchsichtig.

Der zweite waren bessere Schatten allein. Richtig, aber zu wenig. Die
Schatten des Produkts sind in ui/theme.css gestaffelt und nicht einlagig:

  --shadow-md  0 4px 16px -2px  +  0 2px 4px -1px
  --shadow-lg  0 12px 32px -4px +  0 4px 8px -2px

Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante setzt
das Element auf die Flaeche, die weite helle hebt es davon ab. In SVG zwei
feDropShadow im selben Filter. Vorher stand dort einer mit dy 7.

Gebracht hat es der Stapel. post1.html hat ihn als Bildrahmen im CSS, fuer
Screenshots; hier ist er in SVG nachgebaut, mit denselben Werten:

  .sheet-mid   rotate(-2.4deg) translate(-2.2%, 3%)  scale(.975)
  .sheet-back  rotate( 3.2deg) translate( 3%,  5.5%) scale(.95)

Er gibt Tiefe, die ein Schatten allein nicht schafft, und sagt, dass hinter
der einen Ansicht weitere liegen. Die hinteren Blaetter sind leer --
Andeutung, keine Kopie. Auf Slide 1 und 2, wo ein Bildschirm zu sehen ist;
nicht auf Slide 3, wo ein Ausschnitt einer Unterhaltung steht, und nicht auf
Slide 4, die eine Ueberlagerung ist.

AUS DER QUELLE NACHGEHOLT
Beim gruendlichen Durchgang durch GeneralTeamChatPage.tsx sind drei Dinge
aufgefallen, die vorher fehlten oder falsch waren:

- Die Datumsmarke "Heute · 30.07.2026" ueber dem ersten Beitrag und die Zeile
  "Maike Berger schreibt gerade ..." ueber dem Eingabefeld. Beides gibt der
  Unterhaltung Struktur, ohne eine Flaeche zu behaupten.
- Der offene Kanal ist grau hinterlegt mit Rand und shadow-sm, nicht violett.
- Die Blase muss so breit sein, wie ihr Anhang es braucht. Vorher richtete
  sie sich nur nach dem Fliesstext, und "Materialliste pruefen · faellig
  31.07.2026" wurde abgeschnitten -- an einer Stelle, an der der Titel die
  Aussage traegt.

ZWEI FALLEN
Gedrehte Blaetter ragen aus dem Kasten. Das hinterste steht um 3,2 Grad
gedreht und um 3 und 5,5 Prozent verschoben; seine Ecken lagen bis zu 24 px
ausserhalb der 92-px-Schutzzone. Der Ueberstand wird jetzt gerechnet:
stapelRand() schickt die vier Ecken durch dieselbe Kette wie das Blatt.

getBBox() kennt keine Beschneidung. Das Absperrband war erst auf die 175 %
der Website gezogen und am Geraet abgeschnitten -- im Bild richtig, in der
Messung ein Ueberstand von 146 px, den es nicht gab. Es ist jetzt nur so
lang, wie es bei -13 Grad quer ueber das Geraet sein muss.

GEPRUEFT
Raender 93 / 227 / 110 / 140 / 350, Luft 116 / 79 / 190 / 141 / 77 px, alle
Ueberschriften innerhalb der Schutzzone, 214 CSS-Regeln, Konsole ohne Fehler,
fuenf PNGs aus dem echten Ausgabeweg bei 2160 x 2700. Downloads blieb leer,
der Projektstand unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:24:45 +02:00
dejanandClaude Opus 5 7723e5bf5a Post 6: der Team-Chat, aus der Nachricht wird die Aufgabe
Nummer 5 im Redaktionsplan, erscheint Mi 26.08. Carousel, vier Slides,
Theme modern, Variante hell, Punktgrund.

DER BEITRAG
  1  Bueero und Baustelle, endlich im selben Chat.  Liste und Verlauf
  2  Ein Kanal pro Projekt.                         die Chatliste gross
  3  Foto rein, Aufgabe raus.                       Fotos, Aufgabe, Bestaetigung
  4  Nicht nur Text.                                das Menue hinterm Plus

Slide 3 ist der Kern und keine Konstruktion: im Werkzeug folgen die drei
Nachrichten genau so aufeinander. Tobias schickt die Baustellenfotos, darauf
eine Nachricht mit angehaengter Aufgabe, darauf die Systemzeile
"Aufgabe Materialliste pruefen im Projekt erstellt". Die letzte Zeile macht
den Unterschied -- die Aufgabe liegt danach im Projekt, nicht nur im Verlauf.
Das ist woertlich der Auftrag des Redaktionsplans.

Vier Slides statt fuenf. Der fuenfte Punkt waere DSGVO und deutsche Server
gewesen, und dafuer gibt es keinen Bildschirm. Eine Behauptung ohne Bild
gehoert in die Bildunterschrift, dort steht sie woertlich wie auf der Website.

WOHER ES KOMMT
Alles aus components/general/GeneralTeamChatPage.tsx des Pitch-Pakets: die
acht Kanaele aus CHATS, der Verlauf aus INITIAL_MESSAGES, der Aufbau einer
Zeile und einer Blase aus der Quelle selbst, die Filterzeile aus dem Typ
ChatFilter, das Plus-Menue aus Composer. Symbole aus @subframe/core, Farben
aus ui/theme.css.

Vom Chat gibt es nur eine Fassung, unter /allgemein/team-chat -- anders als
bei der Pipeline mit ihren vier eigenen SHK-Bildschirmen. Die Inhalte darin
sind trotzdem aus dem Gewerk: Waermepumpe Mueller, Montagetermin,
Baustellenfotos.

WO BEWUSST ABGEWICHEN WIRD
- Slide 1 zeigt drei von sechs Nachrichten, Slide 2 fuenf von acht Kanaelen.
  Ein Chat und eine Liste scrollen beide. Slide 2 behaelt alle drei Sorten:
  Projektkanal, Direktnachricht, Gruppe.
- Die Bildergalerie ist eine Anhangkarte, kein Bild. Im Werkzeug laedt
  GalleryPreview zwei Fotos von Unsplash. Fremde Fotos in einen Beitrag zu
  setzen, der von der eigenen Baustelle spricht, waere eine Behauptung ueber
  Material, das wir nicht haben.

DREI FALLEN
Die Listenzeile hat drei Zeilen, nicht zwei. Der erste Nachbau schob Nadel
und Ungelesen-Punkt neben die Vorschau, wo sie an den Text stiessen -- und
die Zuordnung fehlte ganz. Das war nicht nur Layout: der Fliesstext
behauptete "Jeder Kanal haengt an seiner Projektnummer", und die Nummer war
nirgends zu sehen. Im Original steht sie als dritte Zeile.

Eine feste Obergrenze fuer die Blasenbreite hat ein Zitat gekuerzt. Fuer die
Titelslide musste sie klein sein, damit die Unterhaltung neben die Liste
passt; auf Slide 3 stand deshalb "Materialliste pruefen · faelli..." Die
Grenze haengt jetzt an der Spalte, in der die Blase steht.

Beinahe haette ich ein Zitat ohne Grund gekuerzt. Die Website-Ueberschrift
lautet "Bueero und Baustelle, endlich im selben Chat." Das "endlich" war
gestrichen mit der Begruendung, die Zeile laufe sonst ueber die Schutzzone --
und diese Begruendung sogar mit einer Zahl versehen, die nicht gemessen war.
Nachgemessen sind es 818 px von 896 erlaubten: sie passt. Das Wort ist wieder
drin.

GEPRUEFT
Luft 102 / 61 / 254 / 141 px, alle Zeichnungen innerhalb der 92-px-
Schutzzone, breiteste Ueberschriftzeile 818 von 896, 214 CSS-Regeln
vollstaendig, Konsole ohne Fehler, vier PNGs aus dem echten Ausgabeweg bei
2160 x 2700. Downloads blieb leer, der Projektstand unveraendert.

Die Pruefskripte im Sitzungsordner waren geloescht und sind neu angelegt,
diesmal mit gemeinsamem CDP-Kern: pruef.mjs misst ueber getBBox die
tatsaechlich gezeichnete Flaeche und die Breite jeder Ueberschriftzeile,
schuss.mjs rendert einzelne Slides, breite.mjs misst eine Formulierung, bevor
sie in die Datei kommt, export.mjs faehrt den Ausgabeweg und faengt /save ab.

NEBENBEI
Die Projektliste steht jetzt achtmal im Repo, einmal je Post-Seite plus
uebersicht.html, dort zusaetzlich in PROJEKTE. Post 6 ist ueberall
eingetragen. Wichtig fuer den offenen Tab: der Eintrag steht im Markup und
entsteht nicht zur Laufzeit -- eine Seite, die schon geladen war, zeigt ihn
erst nach dem Neuladen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 22:41:53 +02:00
dejanandClaude Opus 5 fcf720a1ee Redaktionsplan: Team-Chat auf Mittwoch, alles danach einen Termin weiter
Der Team-Chat war zum Montag nicht fertig. Er rueckt auf Mi 26.08., und
alle Beitraege danach ruecken einen Slot mit. Die Spanne endet damit am
18.09. statt am 16.09.

  Team-Chat              Mo 24.08.  ->  Mi 26.08.
  Form Builder           Mi 26.08.  ->  Fr 28.08.
  Zettelwirtschaft       Fr 28.08.  ->  Mo 31.08.
  ...
  Haeufige Fragen        Mi 16.09.  ->  Fr 18.09.

NICHT UEBER START VERSCHOBEN
Der kuerzere Weg waere gewesen, START um einen Termin nach hinten zu
setzen -- so ist es beim letzten Mal gemacht worden, als sich der Start
selbst verspaetete. Diesmal geht das nicht: die ersten vier Beitraege
sind am 14., 17., 19. und 21.08. tatsaechlich erschienen. Ein Plan, der
rueckwirkend andere Termine behauptet, ist keiner.

Stattdessen gibt es AUSGELASSEN, eine Menge von Terminen, die slots()
ueberspringt. Beim naechsten Ausfall ist es ein Eintrag mehr, und die
Historie bleibt richtig.

DREI STELLEN HINGEN AN DER ALTEN RECHNUNG
- Die gelbe Markierung stand fest auf zwei Zeilen ("if i < 2"). Fertig
  im Generator sind inzwischen vier. Jetzt FERTIG = 4, und der Satz
  darueber nimmt sein Zahlwort aus derselben Konstante -- vorher haette
  man beides von Hand gleich halten muessen.
- "Das erste Reel steht am 09.09." war schon vor dieser Verschiebung
  falsch: der Reel lag auf dem 11.09. Vermutlich ein Rest davon, dass
  Workflow-Engine und Projektmanagement einmal die Plaetze getauscht
  haben. Das Datum wird jetzt aus dem Plan gerechnet und steht bei
  14.09.
- Stand im Kopf auf 23.08.2026.

Instagram-Redaktionsplan.html ist per .gitignore nicht versioniert, nur
die Quelle und das PDF. Wer die Datumsangaben braucht, baut mit
python3 redaktionsplan/bauen.py neu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 20:21:41 +02:00
dejanandClaude Opus 5 c26efa3f0c Varianten-Blatt fuer die Titel-Slide mit aufgenommen
Lag seit dem 12.08. unversioniert im Arbeitsverzeichnis: eine Seite, die
neun Rahmen-Varianten einer Titel-Slide nebeneinanderstellt. Nichts im
Repo verweist darauf, sie laeuft fuer sich.

Kommt als eigener Commit, damit sie sich ohne den Beitrag zurueckziehen
laesst, falls sie nur ein Zwischenstand war.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 23:00:15 +02:00
dejanandClaude Opus 5 32206e7352 Post 5: das Projektmanagement, vier Bildschirme einer Pipeline
Nummer 4 im Redaktionsplan, erscheint Fr 21.08. Carousel, fuenf Slides,
Theme modern, Variante hell, Punktgrund. Stichwort des Plans: "Der
Ueberblick, den jeder Betrieb vermisst."

DER BEITRAG
Das Demo-Projekt haelt fuer die Pipeline vier eigene Bildschirme bereit --
Schablone, Board, Liste, Detail. Jeder bekommt eine Slide, dazu die Karte
aus der Naehe:

  1  Von der Anfrage bis zur Abnahme.  4A  das Board, drei Phasen
  2  Alles Wichtige auf einer Karte.   4A  eine Karte gross
  3  Vom Verkauf in die Abwicklung.    4B  das Projekt in der Abwicklung
  4  Board oder Liste.                 4C  dieselben Projekte als Tabelle
  5  Du bestimmst, was draufsteht.     3   der Feldeditor

ES SIND ZWEI PIPELINES, NICHT EINE
Das ist die Pointe und der Grund fuer Slide 3. Das Board gehoert zu
"Vertrieb: Anlagen-Verkauf", das Projekt in der Detailansicht liegt in
"Auftragsabwicklung SHK", und ein Knopf in der Kopfzeile schiebt es von
der einen in die andere. Beide Namen stehen woertlich in
PipelineBoardInteractive.tsx und S02PipelinesDetailansicht.tsx.

Warum das eine Software sein kann, beantwortet Slide 5: die Felder
haengen an der Pipeline, nicht am Programm. Siebzehn Feldtypen und drei
Stufen unter "Pflicht ab Board-Spalte", im Screen offen auf "Aufmass
erledigt". Ein SHK-Betrieb legt sich das an, ein anderer etwas anderes.

EINE RICHTIGSTELLUNG
Ein Zwischenstand behauptete, die Detailansicht sei nur ein Entwurfs-PNG.
Geprueft worden waren die Beschriftungen aus 07_Screen-4b_Karten-Detail.png
-- KUNDENDATEN, OBJEKTDATEN, Wahrscheinlichkeit, Heizlast, Lead-Quelle --
und die kommen im Code wirklich nirgends vor. Daraus wurde der falsche
Schluss gezogen, es gebe den Bildschirm nicht.

Es gibt ihn: app/(screens)/4b/ rendert S02PipelinesDetailansicht.tsx, 3150
Zeilen, mit den Rubriken GRUNDDATEN, PLANUNG & VERANTWORTLICHE,
PROJEKTKONTAKTDATEN, KONTAKTE, FIRMENDATEN, ANSPRECHPARTNER,
PROJEKTADRESSE und ZUSATZADRESSEN. Das PNG ist ein aelterer Entwurf
desselben Bildschirms, nicht sein Beleg. Die Abwesenheit einer
Zeichenkette beweist die Abwesenheit eines Bildschirms nicht.

DIE KARTE KOMMT AUS DEM CODE, NICHT AUS DEM SCREENSHOT
Der erste Anlauf war von assets/screens/pipelines.png abgemalt und lag an
drei Stellen daneben, von denen im Bild keine zu erkennen war:
Prioritaet, Balken und Kuerzel stehen als Gruppe rechts und haengen ueber
die Kante des Fusses (absolute -bottom-4 right-2), im Fuss steht ueber
dem Termin die Zeitspanne des Projekts, und die Balken sitzen in einem
eigenen umrandeten Kasten. Erst ProjectCard.tsx hat das gezeigt. Die
Karte ist jetzt Klasse fuer Klasse daraus nachgebaut, 320 breit wie w-80,
Titel auf hoechstens zwei Zeilen wie line-clamp-2.

Anders als bei Post 4 wird nichts umgefaerbt. Eine Pipeline lebt davon,
dass man das Gewerk an der Farbe erkennt, ohne zu lesen, und dass der
Fuss sich mit der Prioritaet faerbt. Zuordnung aus CATEGORY_TONES, Toene
aus ui/theme.css, Kuerzelfarbe aus avatarColor je Projekt.

ALLE ZAHLEN SIND GERECHNET
Anzahl und Summe je Spalte kommen aus denselben 41 Datensaetzen wie die
Karten: Angebot 189.800, Auftrag 224.800, In Ausfuehrung 352.300 Euro.
Wer eine aendert, muss die andere mitziehen.

WO BEWUSST ABGEWICHEN WIRD
- Slide 1 zeigt zwei bis drei Karten je Spalte, der Kopf zaehlt weiter
  vier. So ist es im Werkzeug auch, eine Spalte zeigt, was aufs Bild
  passt. Gleich hohe Saeulen sahen aus wie eine Tabelle.
- Slide 4 zeigt vier von neun Tabellenspalten und sieben von elf Zeilen.
  Die Spalte Sachbearbeiter ist entfallen, damit "Auftragswert gesperrt"
  ungekuerzt hineinpasst -- der Wortlaut ist der Grund, warum die Spalte
  ueberhaupt auf der Slide steht.
- Slide 3 stellt die Rubriken untereinander statt nebeneinander, und zwei
  fehlen: PDS-INTEGRATION, weil der Beitrag sich an Betriebe allgemein
  richtet und nicht an die mit diesem einen Branchenprogramm, und
  "ADRESSDATEN . TEST A (ZUSATZADRESSEN AN)", weil das ein Arbeitstitel
  aus der Entwicklung ist und kein Bildschirmtext.
- Ein "Kalender" im Umschalter stand kurz drin und ist raus: die
  ToggleGroup des Boards kennt board und list, mehr nicht.

DIE MASSSTAEBE SIND ABGESTIMMT
Slide 1, 3, 4 und 5 zeigen ihre Schrift zwischen 16 und 23 px. Slide 2 ist
als Nahaufnahme gedacht und liegt bei 47. Vorher stand sie auf 59 bis 73,
und eine einzelne Karte wirkte damit groesser als das ganze Board davor.

Der Kopfbereich ueber dem Board -- Seitentitel, Umschalter, Filterleiste --
ist wieder entfallen. Im Profilraster ist die Slide eine Kachel von rund
150 px, dort liest niemand eine Bedienoberflaeche. Wie die Pipeline heisst,
sagt Slide 3; dass es eine Liste gibt, sagt Slide 4.

EIN PRUEFSKRIPT HAT DEN PROJEKTSTAND VERSTELLT
export.mjs schaltet die Slides mit der Pfeiltaste weiter. Die Seite startet
aber im Bearbeiten-Modus, und dort verschieben Pfeiltasten das gewaehlte
Element statt die Slide zu wechseln. Jeder Prueflauf hat die Zeichnungen
geschoben und das in state/post5.json geschrieben: zuletzt vis dx 3 dy -69
auf Slide 1 und auf Slide 2 ein size 762, das die im Code eingestellten 720
ueberschrieb. Die Karte, ueber deren Groesse zwei Runden lang geredet wurde,
war beim Messen also gar nicht die aus dem Code.

Gemerkt an Messwerten, die im Code nirgends standen -- 89 px Rand rechts bei
eingestellten 92, 177 px unten bei eingestellten 120. Zahlen, die man sich
nicht erklaeren kann, sind ein Befund und keine Messungenauigkeit. Der Stand
ist bereinigt, das Skript verlaesst vor dem Blaettern den Bearbeiten-Modus
und schreibt die Datei am Ende zurueck.

NEBENBEI
- Plaettchenbreiten werden gemessen statt geschaetzt. Zeichenzahl mal
  0,52 em stimmt fuer "Heizung" und geht bei "Waermepumpe" daneben, das
  Wort stand ueber dem Rand. Ein Canvas misst dieselbe Schrift, die das
  SVG spaeter zeichnet; aus derselben Messung kommt der Titelumbruch.
- Die Ueberschrift von Slide 1 steht auf denselben Werten wie die von
  post4.html, nachgemessen: 76 px, Gewicht 800, -2,432 px Sperrung,
  zentriert, Oberkante 95, Blockhoehe 158, Versatz dy 3.
- Die Projektliste steht siebenmal im Repo, einmal je Post-Seite plus
  uebersicht.html, dort zusaetzlich in PROJEKTE. Post 5 ist ueberall
  eingetragen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 23:00:02 +02:00
dejanandClaude Opus 5 23a9ffc4cf Post 4: die Bildunterschrift steht, Uebergabe in POST-4.md
Der Text lag bisher nur im Gespraech, und genau davor schuetzt POST-2.md seit
Post 2. Also dieselbe Form: Slides, Bildunterschrift, Herkunft der Texte,
offene Punkte, Fallen.

Gegen die Vorgaben des Redaktionsplans geprueft und nicht geschaetzt: Haken im
ersten Absatz mit 106 von 125 Zeichen, Fliesstext 671 von erlaubten 300 bis
800, acht Hashtags unten abgesetzt, und die Suchwoerter stehen zusaetzlich im
Fliesstext -- Handwerk, Automatisierung, Workflow, Ausloeser, Aktionen,
Vorlage, Anfrage, Aufgabe.

"Handwerk" kam zuerst nur in den Hashtags vor. Der Plan verlangt es
ausdruecklich auch im Text, deshalb steht jetzt "Handwerksbetrieben" im Haken.

Drei Wendungen kommen bewusst nicht vor: "Verbinden statt ersetzen" gehoert
Post 3, "Wenn-dann statt Handarbeit" ist die Ueberschrift von Post 1, und
Em-Dashes verbietet rules.md 4. Erfundene Uhrzeiten oder Vorgaenge ebenso
wenig.

Nebenbei stand in der README noch "3 Slides" aus einem Zwischenstand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:55:41 +02:00
dejanandClaude Opus 5 bae57e64ce Post 4: die Workflow-Engine, drei Ansichten derselben Oberflaeche
Nummer 3 im Redaktionsplan, erscheint Mi 19.08. Carousel, vier Slides,
Theme modern, Variante hell.

DER BEITRAG
Ein Ablauf, die Bausteine, aus denen er entsteht, und der Hinweis, dass man
nicht bei null anfaengt:

  1  Automatisiere deinen Betrieb.   ein Ablauf auf dem Canvas
  2  Erst der Ausloeser.             die Ausloeser-Auswahl
  3  Dann die Aktionen.              die Aktionen-Auswahl
  4  Fang nicht bei null an.         drei fertige Vorlagen

Slide 2 und 3 teilen sich eine Funktion mit zwei Rubriken, weil es im Produkt
eine Auswahl ist: "Ausloeser" und "Aktionen" stehen dort untereinander im
selben Feld. Getrennt gezeichnet waeren es zwei Bilder, die zufaellig gleich
aussehen.

Vier Slides statt der fuenf bis sieben aus dem Redaktionsplan. Das ist eine
Spanne und kein Soll: eine Slide, die nichts hinzufuegt, kostet einen Wisch
und gibt nichts zurueck.

WAS AUS DEM PRODUKT KOMMT UND WAS NICHT
Der Knoten ist NodeItem aus ui/components/NodeWorkflowEngine des Pitch-Pakets:
quadratisch 128 x 128, rounded-lg, 6 px Rand, Titel und Beschreibung unter dem
Feld, beim Ausloeser die linke Flanke voll gerundet. Genau so rendert es auch
assets/screens/workflow.png.

Ein Zwischenstand zeichnete stattdessen breite Karten mit farbiger Kante links
und einem Block "BESCHREIBUNG" -- abgemalt vom Pitch-Bild 02_Praesentation/03.
Im Code des Projekts gibt es das nirgends: weder "BESCHREIBUNG" als Label noch
eine linke Farbkante, beides null Treffer. Der Kommentar behauptete trotzdem,
es sei ein Nachbau der Komponente. Das ist berichtigt.

Die Farben sind die des Produkts, aufgeloest aus ui/theme.css ueber die ganze
Token-Kette und nicht aus dem Bild geschaetzt. Sie tragen Bedeutung: gruen
faengt an, rot aendert Daten, blau meldet, amber macht eine Aufgabe daraus,
violett entscheidet. Ein Zwischenstand hatte sie durch Markenfarben ersetzt,
weil rules.md 2 diese Toene nicht kennt -- zurueckgenommen, der Beitrag zeigt
die Oberflaeche, und umgefaerbt waere es eine Nacherzaehlung.

Alle Produkttexte stehen woertlich in der Quelle. Wo ein Name nicht in eine
Zeile passte, steht er zweizeilig: aus "Daten vereinheitlichen" ein "Pruefen"
zu machen ist keine Kuerzung, sondern ein anderer Name.

DREI STELLEN, AN DENEN BEWUSST ABGEWICHEN WIRD
- Slide 1 gabelt nach dem zweiten Knoten. Alle drei echten Ablaeufe verzweigen
  erst an Position 3 oder 4, und das fuellt ein hochkantes Format schlecht --
  solange der Stamm laeuft, steht ueber und unter ihm nichts. Dafuer fehlt
  zwischen "Formular eingegangen" und "Passender Lead?" der Schritt "Daten
  vereinheitlichen". Ein Ablauf, den es genau so gibt, ist es damit nicht mehr;
  einer, den es geben koennte, schon.
- Slide 4 nennt die Demo-Ablaeufe "Vorlagen". Dass es eine Vorlagen-Bibliothek
  gibt, steht als Merkmal auf der Website; dass sie diese drei enthaelt, steht
  nirgends. Im Werkzeug werden sie ueber workflowId geladen, nicht als Galerie
  gezeigt, und das Wort "Vorlage" kommt in der Komponente nicht vor.
- Kein PDS im Beitrag. Von den sechs Vorlagen nennen drei das Branchenprogramm.
  Uebrig bleibt genau die Gruppe GENERAL_WORKFLOW_PRESETS, in der es kein
  einziges Mal vorkommt -- also keine Auswahl nach Geschmack, sondern die
  vollstaendige zweite Gruppe. Gekuerzt wurde nichts.

Erfundene Vorgaenge sind raus. Ein Zwischenstand hatte eine Slide mit einem
Aktivitaetenverlauf samt Uhrzeiten. Nichts davon war gemessen, und bei einem
Produkt vor dem Start ist eine erfundene Uhrzeit kein Beispiel, sondern ein
Beleg, den es nicht gibt.

DER PUNKTGRUND
Die Arbeitsflaeche des Workflow-Editors, Werte aus WorkflowEditor.tsx der
Website. Zwei begruendete Abweichungen: die Teilung ist verdoppelt, weil ein
24er Raster im Feed auf 9 px zusammenfaellt und zu grauem Schleier wird, und
der Grund bleibt weiss statt getoent, weil rules.md 1a das verbietet.

EIN FEHLER, DER FAST DURCHGERUTSCHT WAERE
Beim Entfernen der Karten-Regeln von Post 2 blieben rund neunzig Zeilen
Deklarationen ohne Selektor stehen, dazu ein Kommentar ohne Ende: nur die
Selektorzeilen waren weggeschnitten. CSS faengt sich davon nicht wieder, der
Parser ueberspringt bis zur naechsten Marke -- und dabei verschwand die Regel
[data-edit] darunter. Damit war das Verschieben von Elementen im Canvas tot,
ohne dass die Seite einen Fehler zeigte. Sichtbar wurde es erst, als ein
gespeicherter Versatz von 3 px nicht mehr ankam.

Behoben, und an der Stelle steht jetzt eine Warnung: Regeln als ganzen Block
entfernen und danach nachsehen, ob document.styleSheets sie noch alle kennt.

NEBENBEI
- Die Ueberschriften stehen auf denselben Werten wie post3.html, bis auf den
  Pixel nachgemessen: 76 px, -0,032em, nowrap und der Versatz dy 3, den auch
  post2, post2-alt und post3 auf jeder Slide tragen.
- Die Projektliste steht sechsmal im Repo, einmal je Post-Seite plus
  uebersicht.html. Post 4 ist ueberall eingetragen. Wer einen siebten Beitrag
  anlegt, muss an alle sechs Stellen denken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:52:19 +02:00
dejanandClaude Opus 5 e02e87b7e2 Video: das Wabenfeld baut sich auf, gerenderte Dateien raus
Das Video ist geschnitten. Die Einblendungen liegen auf einer externen
Platte, im Repo bleibt die Werkbank: 381 Dateien und 24 MB gerenderte
Ausgabe sind raus, png/ und sequenz/ legt bauen.mjs beim naechsten Lauf
selbst wieder an.

WABENFELD
Es blendete als Ganzes auf, und das ist bei der einen Zeichnung, die
etwas behauptet, zu wenig. Jetzt steht zuerst die Nabe, dann wachsen die
Waben in fuenf Wellen nach aussen, und die Straenge ziehen jeweils kurz
nach ihrer Zelle nach. Rueckwaerts zieht sich das Feld in die Marke
zurueck, statt von aussen zu zerfallen.

Die Bewegung ist die introTimeline aus HoneycombSection.tsx, uebernommen
und nicht erfunden: scale 0,16 auf 1, back.out(1.75), gestaffelt nach
Abstand zur Mitte, danach die Straenge mit eigener Deckungsrampe. Weil
unsere Belegung nur fuenf verschiedene Abstaende kennt, sind es fuenf
Wellen und nicht achtzehn Einzelereignisse, und das liest sich als eine
Bewegung.

Dafuer brauchte der Zeitverlauf ein zweites Mass. Bisher frass der
Verzug vom Rest der Zeit: die letzte von achtzehn Zellen haette ein
Zehntel der Zeit fuer dieselbe Bewegung gehabt und waere hereingeschnappt,
waehrend die erste hereinschwebt. Mit data-d bekommt ein Element ein
eigenes Fenster fester Laenge. Ohne data-d bleibt alles wie es war, die
anderen neun Komponenten laufen unveraendert durch den alten Pfad.

50 Bilder statt 12, bei 25 fps also zwei Sekunden herein und 1,2 hinaus.
Die Bewegung ist bei 0,94 fertig, die letzten Bilder stehen still. Masse
und Ausschnitt sind unveraendert, 2168 x 1906.

DIE PNGS WAREN IN DER ERSATZSCHRIFT
KOMP_BEREIT wartete auf document.fonts.ready, bevor die Karte im DOM
stand. Zu dem Zeitpunkt braucht die Seite keine Manrope, es laeuft kein
Ladevorgang, und das Versprechen loest sofort auf -- erfuellt, ohne dass
ein Byte Schrift da waere. Danach wurde gebaut, und ob die Schrift bis
zur Messung ankam, entschied der Zufall: dieselbe Komponente
hintereinander gemessen kam abwechselnd 984 und 1010 px breit heraus.

Das ist die Falle aus rules.md 4, nur eine Ebene frueher als dort
beschrieben. document.fonts.load() holt eine Schriftlage unabhaengig
davon, ob gerade jemand sie benutzt, erst danach ist ready eine Aussage.
Dieselbe Wette steckte in der Wortmarke von logo-bug, einem img mit
width:auto, deshalb wartet der Aufbau jetzt auch auf die Bilder.

Sieben der zehn zuletzt gebauten Dateien waren betroffen und 2 bis 4
Prozent zu schmal. Die reinen Zeichnungen verraten es nicht, ihr SVG hat
feste Masse -- ring-kern war gegen einen frischen Render bildgleich.

STANDBILD KOMMT AUS DER SEQUENZ
Der Dreiteiler im Schnitt haengt daran, dass -ein, Standbild und -aus an
den Stoessen dasselbe Bild zeigen. Als zweite Aufnahme war das bisher nur
wahr, weil es zufaellig stimmte. Beim Wabenfeld stimmte es nicht mehr:
dort tragen die Zellen waehrend der Bewegung ein transform, und nach dem
Wegnehmen rastert Chrome eine Spur anders als bei einem Element, das nie
eines hatte. Der Unterschied lag bei einer Stufe auf verstreuten Pixeln,
also unsichtbar, aber die Zusage war weg. bauen.mjs schreibt das
Standbild jetzt aus dem letzten Bild der Einblendung: dieselbe Datei
statt derselbe Anblick.

NEBENBEI
- Bauchbinde Betrieb, Chip "Vierte Generation" und Endkarte sind raus.
  Die Bauchbinde liess sich ueber ?name= und ?rolle= ohnehin aus der
  ersten erzeugen, ein Schlussbild gehoert in den Schnitt und nicht als
  Einblendung darauf.
- Das Wabenfeld steht ohne das blasse Feld aus leeren Waben. Auf dem
  weissen Blatt traegt das die Zeichnung, ueber Videomaterial ist ein
  Schleier aus halbdurchsichtigen Sechsecken im Weg. Der Ausschnitt wird
  seitdem aus den belegten Waben gerechnet. In post3.html bleibt beides.
- strangForm() heisst hier strangPfad() und liefert nur den Pfad: die
  Form muss je Bild neu gerechnet werden, weil ein Ende wandert. In den
  Post-Seiten bleibt strangForm(), die Rechnung darin ist dieselbe.
- liste.json und README stehen auf dem Stand der zehn Komponenten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:29:00 +02:00
dejanandClaude Opus 5 ceb63fec38 Post 3: zweite Slide, und eine Komponenten-Library fuers Video
POST 3
Slide 2 zeigt den Aufbau: HiveGrid in der Mitte, die eigenen Module auf dem
gelben Ring, die fremden Programme aussen am gestrichelten Kreis. Nachbau des
Schaubilds aus dem Prototyp, aber in den Markenfarben -- Magenta ist keine
(rules.md 2) -- und ohne Produktnamen im Kern.

Achtung: das ist eine andere Aussage als in post2-alt.html, wo in der Mitte
das Branchenprogramm liegt und HiveGrid ausdruecklich der Ring ist. Steht als
Warnung im Code. Die Ueberschrift "Der Kern bleibt." passt seit dem Wechsel
der Mitte nicht mehr zum Bild und ist noch offen.

Slide 1 traegt nur noch die Ueberschrift; ein leerer Fliesstext erzeugt jetzt
keinen leeren Absatz mehr. Das Wabenfeld ist enger geschnitten (Faktor 1,301
statt 1,227) und steht am Anschlag: 96 px zur Slide-Kante, die Sicherheitszone
liegt bei 92. Ueberschriften in 76 px und 3 px versetzt wie in post2-alt --
gemessen, nicht geschaetzt.

Zwei Fehler, die dabei aufgefallen sind:

- Die Andock-Kacheln lagen auf denselben Strahlen wie die Module und
  beruehrten sich. Dieselbe Ueberlappung hat der Exportpruefung eine Spanne
  von 37 vorgetaeuscht, wo eine fehlende Marke 0 ergeben muss -- der
  Gegentest kam durch, obwohl die Marke fehlte. 22,5 Grad Versatz behebt
  beides.
- Die Pruefung kannte nur den Ausschnitt des Wabenfelds und haette auf
  Slide 2 an der falschen Stelle gemessen. Der Ausschnitt wandert jetzt mit
  in die Ablage, wabenZonen heisst darum markenZonen.

VIDEO
Neuer Ordner video/ mit 13 Einblendungen fuer den Schnitt, jede als Standbild
und als Bildsequenz mit Ein- und Ausblendung, alle mit Alphakanal. Gerendert
aus komponenten.html von bauen.mjs ueber CDP; kein ffmpeg noetig, weil eine
Bildsequenz dieselbe Transparenz traegt wie ProRes 4444.

Der Schatten fehlte zunaechst ganz: die Karte trug clip-path fuer die Ecken
und box-shadow fuer den Schatten, und clip-path beschneidet ihn mit. Genau so
steht es in rules.md 3. Sitzt jetzt als drop-shadow auf einer Huelle.

NEBENBEI
- Das ZIP hiess in allen Projektdateien "hivegrid-post1-loesung", weil sie
  voneinander kopiert wurden. Kommt jetzt aus PROJECT.
- Das rechte Element-Feld liess sich nicht schliessen, wenn es nicht ganz ins
  Fenster passte: sein Knopf rutschte als erster hinaus. Knopf nach vorn,
  Escape hebt die Auswahl auf, max-width auf den Feldern.
- uebersicht.html schrieb "1 Slides".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:55:18 +02:00
dejanandClaude Opus 5 a5ec0c565e Post 3: Verbinden statt ersetzen, ein Einzelbild mit dem Wabenfeld
Nummer 2 im Redaktionsplan, im Generator das dritte Projekt. Kein Carousel:
eine Haltung, die man durchwischen muss, ist keine.

Das Bild ist das Wabenfeld der Startseite, uebernommen aus
HoneycombSection.tsx und nicht nachempfunden -- derselbe Umkreisradius,
dieselbe Achsrechnung, dieselben Zellnummern. Damit stimmen WABE_MARKEN und
HONEYCOMB_DEFAULT_LOGO_PLACEMENTS ueber dieselben Nummern ueberein. Die vier
Waben "In Arbeit" sind raus: auf der Startseite ein Versprechen, im Feed ein
Eingestaendnis.

Der Ausschnitt ist enger als die Slide (880 statt 1080), das Feld also um
1,227 vergroessert -- ein Ring leerer Waben rundherum kostete nur Platz.
Text mittig wie in post2-alt.html.

Sechs der 14 Marken sind PNG und damit dieselbe Wette wie ein Screenshot
(rules.md 7). wabenZonen() prueft sie deshalb einzeln nach: ein Punktraster
ueber den ganzen Markenkasten, dessen Ecken das Gelb der Wabe tragen. Fehlt
die Marke, ist der Kasten einfarbig und der Export wiederholt. Gegengetestet,
indem DATEV absichtlich weggelassen wurde.

Die sechs Zeichnungen von Post 2 sind aus der Kopie raus, 5237 auf 4270
Zeilen. Die Band-Maschinerie bleibt, laeuft bei einer Slide aber leer -- im
Kommentar steht jetzt, dass ihre Stuetzpunkte an Post 2 geeicht sind.

Nebenbei: uebersicht.html schrieb "1 Slides", faellt erst mit dem ersten
Einzelbild auf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:25:09 +02:00
dejanandClaude Opus 5 06479c54b3 Redaktionsplan: Workflow-Engine vor Projektmanagement
Dazu der schon offene Umbau: der Starttermin steht als Konstante oben,
Datumsspanne und Stand ziehen sich daraus statt fest im Text zu kleben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 19:53:59 +02:00
dejanandClaude Opus 5 35f05cf213 Post 2: die Bildunterschrift steht
Grundlage ist Noahs Absatz von der Website, aber aus der Ich-Form geholt:
das Konto ist hivegrid.de, nicht Noah. Er wird in der dritten Person
eingefuehrt, danach traegt "wir" den Text -- das sind die Gruender, und
Noah ist einer davon.

793 Zeichen, Haken im ersten Absatz mit 121, neun Hashtags. Gemessen und
gegen die Vorgaben aus dem Redaktionsplan geprueft, nicht geschaetzt.
"Verbinden statt ersetzen" bleibt fuer den 14.08. reserviert und kommt
darin nicht vor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:03:51 +02:00
dejanandClaude Opus 5 79821775b0 Uebersicht: kein Zoom mehr, nur noch scrollen
Zwei Zoom-Ebenen lagen uebereinander, und beide waren falsch. Aussen ein
Rad-Zoom, der nur die iframes skaliert hat -- Beschriftung, Abstaende und
Polster stehen ausserhalb der Transformation und blieben stehen, also
standen beim Herauszoomen riesige Ueberschriften ueber winzigen Streifen.
Und innen reagierte die Post-Seite im iframe weiter auf ihr eigenes
Mausrad, weil sie ihren Canvas mitbringt.

Jetzt: jeder Streifen wird auf die Fensterbreite eingepasst, gescrollt
wird nur hoch und runter. Die iframes bekommen pointer-events: none --
die Uebersicht ist zum Ansehen da, bedient wird im Projekt selbst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:08:39 +02:00
dejanandClaude Opus 5 16b85a285a Projektuebersicht: alle Streifen untereinander auf einer Flaeche
Neu unter Projekt -> "Alle Projekte im Streifen". uebersicht.html zeigt die
Streifen aller drei Projekte gestapelt, ohne Sidebar, auf derselben Art
Flaeche wie der Generator: ziehen schiebt, das Rad zoomt, Fit passt ein.

Die Seite baut die Slides nicht nach. Jedes Projekt kommt als iframe mit
?nur=streifen; in diesem Modus blendet die Post-Seite ihre Bedienoberflaeche
aus und zeigt den Streifen in natuerlicher Groesse, skaliert wird aussen.
Ein Nachbau wuerde irgendwann von dem abweichen, was der Generator zeigt,
und waere dann schlimmer als keine Uebersicht.

Drei Dinge, die dieser Modus regelt:

- Es wird nichts gespeichert (stateLocked). Drei Kacheln, die beim
  Schliessen ihren Stand zurueckschreiben, waeren genau die Falle aus
  POST-2.md 6.
- Die Ansicht steht fest auf "streifen". Post 2 stand im Stand auf
  "einzeln" und meldete deshalb 1080 px statt der vollen Breite.
- Der Auswahlrahmen faellt weg; in der Uebersicht ist keine Slide "die
  aktuelle".

Die Streifenbreite kommt per postMessage von der Post-Seite: die Zahl der
Slides ist je Projekt anders und soll nicht zweimal gepflegt werden.

Beim Einpassen zaehlen Beschriftung, Abstaende und Polster nicht mit --
die skalieren nicht, weil sie ausserhalb des transformierten iframes
stehen. Ohne diese Rechnung lief die letzte Reihe unten hinaus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:04:38 +02:00
dejanandClaude Opus 5 0c4933d6b5 Theme modern: Balken unter dem Akzentwort, Raster- und Mittig-Schalter
Das Akzentwort bekommt den Balken aus der Landingpage
(LandingPageClient.tsx: h-[3px], -bottom-1, bg-current, im Ruhezustand auf
22 % gestaucht, bei Hover voll). Wir nehmen die volle Breite, ein Standbild
kennt kein Hover. Die Masse stehen in em, weil der Balken dort unter 62 px
Schrift liegt und hier unter 76 -- auf der Titel-Slide unter 88.

Zum Ausprobieren zwei Schalter unter "Versuch": das Raster aus post1.html
laesst sich wieder einschalten, und Ueberschrift plus Fliesstext lassen
sich zentrieren. Beides steckt im Projektstand, die alte Komposition ist
also einen Klick entfernt.

Ausserdem:

- Der Rahmen um Noahs Foto traegt jetzt --accent-teal statt der
  ausgewaschenen Fassung --accent-teal-light.
- Die vier schwebenden Kacheln auf Slide 2 sind raus.
- Das Band ist halb so dick und faengt auf Slide 3 erst hinter der Karte
  an, kommt also dahinter hervor statt links schon dazuzuliegen.
  BAND_ANSCHNITT und BAND_AUSLAUF steuern das, ohne die Stuetzpunkte zu
  verschieben -- die Kurve bleibt an der Naht verankert.
- Auf Slide 6 stehen die Texte buendig zur Oberkante der Wabe. Die
  Grundlinie lag 19 px zu tief, das ist die Versalhoehe von 32 px Manrope.
- .vis svg bekommt overflow: visible. Ohne das wurde der Schatten an der
  Kartenkante gekappt: aussen ein grauer Streifen, in den Chamfer-Kerben
  graue Keile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:54:52 +02:00
dejanandClaude Opus 5 d578d12991 Theme modern: flaches Band im Slack-Look, Karten mit Schatten
Der metallisch schattierte Zylinder ist raus. Er sah fotorealistisch aus
und damit wie ein Fremdkoerper auf einem flachen, ruhigen Blatt. An seiner
Stelle steht eine deckende Flaeche mit einer blassen zweiten dahinter --
kein Verlauf, keine Woelbung, keine Glanzkante.

Zwei Toene, umschaltbar unter "Band" im Bedienfeld: Orange #F1B729 und
"HiveGrid Teal", was --dark-bg #30313D meint und nicht --accent-teal.
Der Unterschied steht jetzt in rules.md, damit die Verwechslung nicht
noch einmal passiert.

Das Band laeuft ueber Slide 3 bis 5 (BAND_VON / BAND_BIS). Slide 1 und 6
sollen ruhig sein, Slide 2 traegt ihre eigene orangene Leitung. Gezeichnet
als geschlossene Flaeche statt als dicke Linie: eine Linie bekommt am Ende
eine Kappe rechtwinklig zur Laufrichtung, und die stand bei schraeger Bahn
seitlich ueber die Naht.

Schatten sind fuer dieses Theme erlaubt, entgegen 5. Ohne sie stehen die
Karten gestochen auf dem Grund statt darueber. Als SVG-feDropShadow, weil
die Karten gezeichnet sind und ein CSS-Schatten vom clip-path beschnitten
wuerde. Noch nicht 3-fach exportgeprueft -- das steht als Auflage in der
Regel.

Ausserdem:

- Slide 1: die G-Marke in der Bauchbinde statt einer leeren Wabe, das
  Badge der Landingpage (Monospace, 10 Grad gedreht) statt des Chips, und
  das Foto fuellt die Inhaltsspalte statt 620 von 1080 px.
- Slide 6: das selbst gezeichnete Konfetti war ein Kleckshaufen und ist
  jetzt "rocket" aus dem Feather-Satz.
- Kacheln haben wieder einen Boden statt harter Kontur.
- migrateState 7 raeumt die gespeicherten Vis-Positionen einmalig weg:
  die Masse haben sich geaendert, und der offene Tab schreibt die Datei
  sonst zurueck (POST-2.md 6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:02:47 +02:00
dejanandClaude Opus 5 5fe1d2945f Theme modern: Metallband, Icon-Satz und echte G-Marke
Das Band laeuft als durchgehende Zeichnung ueber den ganzen Streifen,
jede Slide zeigt ihren Ausschnitt ueber --slide-i. An der Naht laeuft es
weiter statt neu anzusetzen -- der Uebergang, den ein Carousel sonst
nicht hat.

Es ist ein Rohr, kein Farbband: vierzig Streifen aus einer Rampe, die
Glanzstelle eingerueckt bei 20 % und fast weiss, der Kernschatten bei
78 % statt an der Unterkante, darunter Licht vom Grund zurueck. Der
erste Anlauf mit vier weichen Streifen sah nach Bonbon aus.

Damit das Band quer durch die Komposition laufen darf, liegt jede
Visualisierung jetzt auf einer deckenden Karte. Linienwerk auf
Transparenz verschwindet darin. Slide 5 musste dafuer von 988 auf 896
schrumpfen, Slide 6 bekam links Einzug.

Ausserdem:

- Feather-Icons aus @subframe/core statt selbst gezeichneter Zeichen,
  auf Slide 2 bis 5. Slide 6 behaelt ihre Nachbauten aus WaitlistNext.
- Die echte G-Marke auf dem Ring, als Polygon statt als Bilddatei.
- Die 01/02/03 auf Slide 6 sind raus.
- Slide 1 uebernimmt den Rahmen der Ueber-uns-Seite: Radius 38 auf
  teal-hellem Grund, Foto 4,2 % eingerueckt.
- Der Grund ist wieder #FFFFFF, wie Post 1 und Post 2. Den Kontrast
  tragen die Karten jetzt ueber die Kante (#D5DBE1).
- Slide 4 und 5 hiessen abweichend vom Original; Beschriftungen und
  Fliesstext gleichen jetzt post2.html.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:19:08 +02:00
dejanandClaude Opus 5 af6afe8e6e Theme modern lief nicht: formWorkflow fehlte
Die Seite baute nicht mehr auf, weder headless noch im Browser. Der Grund
stand im Statusfeld, aber meine Proben haben zu frueh gemessen und ihn nie
zu sehen bekommen: "Fehler beim Laden: formWorkflow is not defined".

Meine Ersetzung fuer Slide 3 lief von "function visWechseln" bis zum
Kommentar "Slide 4". Genau dazwischen lagen die Formen fuer Workflow,
Trichter und Megafon. Sie sind wieder da, diesmal als Einfuegung vor dem
Kommentar statt als Bereichsersetzung.

Das war derselbe Fehlertyp zum dritten Mal in diesem Projekt: ueber einen
Bereich zwischen zwei Ankern loeschen, zwischen denen mehr steht als
gedacht. Ich ersetze ab jetzt nur noch ueber den genauen Textabschnitt und
fuege sonst ein.

Zur Diagnose: node --check faellt hier nicht auf, weil eine fehlende
Funktion erst zur Laufzeit auffliegt. Was es gefunden haette, ist eine
Probe, die lange genug wartet und den Statustext ausliest -- die Seite
schreibt ihren Ladefehler dort hinein.

Slide 3 hat mehr Luft bekommen: Karte 580 statt 500 hoch, Zeilenabstand 78
statt 66, Innenrand 52 statt 48.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 19:20:15 +02:00
dejanandClaude Opus 5 43a4a394f2 Theme modern: Titel-Slide und Slide 3 neu gedacht, Badges raus
Die Badges ueber den Ueberschriften sind wieder aus. Auf einer Website
ordnet so eine Pille die Sektion ein, auf einer Slide nimmt sie Platz weg,
ohne etwas zu sagen, was die Ueberschrift nicht schon sagt.

Slide 1 war bisher nur entkritzelt: derselbe Aufbau, nur ohne Zeichnungen.
Jetzt ist das Foto selbst chamfert -- die Markenform, und ein dunkles Bild
auf hellem Grund braucht keinen Rahmen. Darauf zwei Auszeichnungen, die
Inhalt tragen statt zu verzieren: ein Chip oben rechts mit "Vierte
Generation" und eine schwebende weisse Infokarte unten links mit Wabe,
Namen und Rolle. Das ist das Muster aus Autarc und Phase0. Der
Helm-Umschalter entfaellt hier.

Slide 3 ist kein Bild fuer Chaos mehr, sondern der Vorgang selbst: eine
Migration, die nicht durchlaeuft. Fortschritt bei 34 Prozent, drei
Statuszeilen (Stammdaten uebertragen, Auftraege 1.284 Konflikte,
Buchhaltung abgebrochen) und als Fusszeile Noahs eigenes Argument, die
geschaetzte Dauer von einem halben Jahr. Das ist die Sprache unserer
eigenen Oberflaeche; im Pitch-Screen 1 stehen fehlgeschlagene Laeufe
genauso. Die Vorfassungen waren Metaphern -- gerissene Leine, Sackkarre,
Knaeuel -- und ein Betrieb, der ueber einen Wechsel nachdenkt, versteht
eine abgebrochene Uebernahme ohne Sinnbild.

Der Fehlerton kommt aus den Tokens der Website (accent-orange #E07B54),
nicht aus eigener Erfindung.

Slide 3 ist NICHT im Bild geprueft. Die Maschine hat nur noch rund 0,3 GB
freien Speicher, headless Chrome bekommt die Assets nicht mehr als
Data-URIs eingelesen und bleibt bei "Lade Screens ..." stehen -- bei
post2.html genauso, die seit dem letzten Commit unveraendert ist. Das
Skript selbst ist mit node --check geprueft.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 19:12:14 +02:00
dejanandClaude Opus 5 9b405ce931 Theme modern: Slide 5 und 6 aus den echten Quellen gebaut
Bisher habe ich die Skizzen entschraffiert statt neu gedacht. Das ist bei
Slide 6 offensichtlich geworden: eine Wabe ohne Zittern ist immer noch
meine Wabe und nicht die Sektion, die es auf der Website schon gibt.

Slide 6 ist jetzt ein Nachbau von app/waitlist/WaitlistNext.tsx. Alle Werte
von dort, mit 1,6 hochskaliert, weil die Liste dort 560 breit ist und hier
896: Wabe 54x48 wird 86x77, Schiene links 26 wird 43 also die Wabenmitte,
Abstand 56 wird 90, Nummer 13 wird 21, Titel 20 wird 32, Text 14,5 wird 23.
Auf der Website leuchten die Waben beim Scrollen nacheinander auf und der
dritte ist blass; ein Standbild kennt keinen Scrollstand, hier sind alle
drei aktiv.

Slide 5 ist ein Nachbau des Architektur-Diagramms aus dem Pitch-Paket:
Kern als gefuellter Kreis in der Mitte, farbiger Ring darum, Pille mit dem
Produktnamen auf dem Ring, Module als Kacheln aussen. Das Diagramm sagt
genau, was die Ueberschrift sagt -- der Kern bleibt, der Ring kommt
drumherum. Meine Wabe hatte dieselbe Aussage, aber in einer Form, die es in
unserer Software nirgends gibt.

Zwei eigene Fehler dabei behoben:

Ich hatte fuer das Theme Farben erfunden (#0E1F1E, #F7FAF9). Die Website
hat ein vollstaendiges Token-Set in app/globals.css. Jetzt stehen dort
dessen Werte: surface, text-primary, text-secondary, container-bg,
border-neutral, accent-yellow-light, accent-yellow-dark, accent-teal-light.

Meine Karten hatten Chamfer und Haarlinie zugleich. In lib/chamfer.ts der
Website steht ausdruecklich, dass clip-path den Rand mitbeschneidet und man
stattdessen fuellen soll. Die Karten sind jetzt gefuellt, ohne Rand.

Das Chamfer-Badge der Website ist zurueck. Es steht dort ueber jeder
Sektion, und im Theme modern traegt es die Einordnung: Die Geschichte, Der
Alltag, Der Wechsel, Was gefehlt hat, Die Loesung, So geht's weiter.

Neu im Vorgehen: nach jeder groesseren Aenderung wird das Skript aus der
Datei geschnitten und mit node --check geprueft. Ein doppelt vergebener
Name hat die Seite still lahmgelegt, und die Pruefung findet so etwas in
einer Sekunde statt in einem Renderlauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:57:44 +02:00
dejanandClaude Opus 5 993dc3e5ef Themes als eigene Ebene, Theme modern angelegt
Projekt und Theme waren bisher dasselbe. Jetzt nicht mehr: das Projekt sagt,
welcher Beitrag, das Theme, wie er aussieht. notebook ist die
Zeichenblock-Optik mit Raster, Marker und handgezogenen Formen, modern der
ruhige Grund mit farbigem Akzentwort und schwebenden Karten. Beide Themes
teilen Aufbau, Texte und Kompositionen. rules.md Paragraf 1a haelt fest, was
sie genau trennt.

Das Theme modern folgt Vorbildern aus dem Feed: Zapier, Slack, Autarc,
Photovate, Plancraft. Was ich daraus uebernehme, ist nicht die Optik der
einzelnen Marke, sondern das Gemeinsame -- ruhiger Grund ohne Textur, eine
sehr grosse Ueberschrift mit einem farbigen Wort statt einer Markierung,
schwebende Karten mit Haarlinie, Ablaufketten mit Knoten, gefuellte
Kreis-Haken.

Drei Farben, wie besprochen: Weiss als Grund, Orange als Akzent, Teal als
Zweitfarbe. Teal stand in rules.md bisher als "ungenutzt" und traegt jetzt
Zeichen und Kacheln. Die Schriftfarbe ist ein sehr dunkles Teal statt reinem
Schwarz, damit sie zur Palette gehoert.

Das Raster faellt weg. Die Ebene bleibt im Markup, damit beide Themes
denselben Aufbau haben, sie zeichnet nur nichts mehr.

Slide 4 ist der erste Umbau: der Notizzettel ist raus, die Aufzaehlung bleibt
und wird zur Kette aus Karten mit Zeichenkachel, Beschriftung und Haken,
dazwischen ein Verbinder mit Knoten. Die Form sagt zusaetzlich etwas aus --
die vier Dinge haengen zusammen, statt nur untereinander zu stehen.

Der Menueeintrag zur Alternative fehlte in post1.html und post2.html; er
stand nur in der Kopie selbst. Von dort kam man also nie hin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:36:07 +02:00
dejanandClaude Opus 5 84b3905d71 Post 2 Alternative: spitz auf Workflow-Engine, CRM, Vertrieb, Marketing
Rueckmeldung aus dem Team: "Wenn ich das als Handwerker sehe, sehe ich zu
viele Dinge die meine Handwerkersoftware schon kann. Wir muessen ganz spitz
auf die Workflow-Engine, dass das ERP bleibt und CRM/Vertrieb/Marketing
gehen. Die Nische besetzen dann nur wir."

Genau das stand bisher auf Slide 4 und 5: Projektmanagement, Chat,
Mitarbeiter-Management, Projekte, Formulare. Das kann ein
Branchenprogramm alles schon, und damit las sich der Beitrag als
Konkurrenz zu ihm -- im Widerspruch zur eigenen Ueberschrift "Dein
Branchenprogramm bleibt, wo es ist".

Slide 4 nennt jetzt Workflow-Engine, CRM, Vertrieb, Marketing. Slide 5
zeigt dieselben vier plus Automatisierung und KI/MCP, und in der Mitte
steht "Dein Programm" statt HiveGrid: der Kern bleibt, aussen kommt dazu.
Das Wort ERP taucht weiterhin nicht auf, die Aussage ist seine, das Wort
unseres.

Dafuer drei neue Formen: Workflow als Knoten mit Verzweigung, Vertrieb als
Trichter, Marketing als Megafon. Jede nimmt einen Massstab entgegen und
taugt damit als kleines Listenzeichen und als grosser Satellit.

Nur die Alternative ist geaendert. post2.html bleibt unangetastet, damit
sich die beiden Fassungen in Stil und Zuspitzung vergleichen lassen.

Zwei Kollisionen kamen dabei heraus. "Dein Programm" ist laenger als
"HiveGrid" und lief in die drei Fensterpunkte; die sind entfallen, in
dieser Formensprache ist Fensterchrom ohnehin fehl am Platz. Und die
laengeren Namen der beiden oberen Satelliten stiessen an ihre Knoten, sie
stehen jetzt seitlich versetzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:58:53 +02:00
dejanandClaude Opus 5 e88afc61a4 Post 2 als zweite Fassung: praezise Formen auf dunklem Grund
Rueckmeldung aus dem Team war, das Design duerfe "futuristischer" sein.
Statt den bestehenden Beitrag umzubauen liegt er jetzt zweimal vor:
post2.html bleibt die Skizzenfassung, post2-alt.html zeigt denselben
Beitrag mit denselben Texten in praeziser Formensprache.

Technisch ist es kein zweiter Entwurf, sondern dieselbe Zeichnung mit
anderen Primitiven. strich zieht eine Gerade statt eines schwingenden
Zuges, kringel eine exakte Ellipse statt eines offenen Kringels, rechteck
und chamfer haben keine Ecken mehr, die ueberschiessen. Die Kompositionen
sind Zeile fuer Zeile dieselben. Wer eine Visualisierung aendert, muss
deshalb an beide Dateien denken -- steht in der README.

Was in der Alternative entfaellt: die Randnotizen der Titel-Slide
(Gluehbirne, Zettel, Maulschluessel), der versetzte Zweitrahmen mit
schraffiertem Band, die Bodenschatten unter den Ikonen und alle
Schraffuren als Fuellung. An ihre Stelle treten ein sauberer
Chamfer-Rahmen mit Eckwinkeln in Gelb und gefuellte Flaechen.

Die dunkle Variante hat zwei Fehler sichtbar gemacht, die in der hellen
Fassung nie aufgefallen waeren:

Das Akzentwort steht auf Dunkel in #1A1A1A und ist nur dort lesbar, wo die
gelbe Markerflaeche darunterliegt. Die deckte 62 Prozent der Zeilenhoehe
ab, also rund 49 px bei einer Versalhoehe von etwa 53 -- die obere Haelfte
der Buchstaben verschwand im Grund. Sie deckt jetzt die ganze Zeile.

Die Beschreibungen der drei Schritte auf der letzten Slide standen fest auf
#555555, einem Wert der hellen Variante. Sekundaerer Text folgt jetzt der
Variante, wie TINTE und GELB auch. Dafuer zieht applyAll die Farben nach
und baut alle Seiten neu, nicht nur die Titel-Seiten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:52:01 +02:00
dejanandClaude Opus 5 f465749c8f Post 2: Slide 4 nennt Projektmanagement statt CRM
Im Fliesstext von Slide 4 steht jetzt "KI-Loesungen,
Projektmanagement, Automatisierungen" statt "ein CRM". Der Satz bleibt
zweizeilig, gemessen 89 px.

Damit taucht Projektmanagement zweimal auf der Slide auf: im Fliesstext
und als zweiter Punkt in der Liste darunter. Bei CRM gab es das nicht,
weil das Wort in der Liste nicht vorkam. Der Kommentar ueber der Liste
behauptete bisher, sie nenne die Kategorien des Fliesstextes nicht noch
einmal -- das stimmt so nicht mehr und ist mit angepasst.

Der Projektstand kommt mit, er traegt eine weitere Verschiebung der
Zeichnung auf der letzten Slide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:36:02 +02:00
dejanandClaude Opus 5 a7c347070e Post 2: Ueberschriften auf eine Hoehe, Slide 2 leitet ueber
Alle sechs Ueberschriften stehen jetzt bei 59, gemessen an der Titel-Slide
von Post 1. Post 2 sass 125 px tiefer, und die Ursache war das
ausgeblendete Tag: es stand auf visibility: hidden und hielt damit weiter
54 px Hoehe plus 38 px Abstand im Textfluss. Jetzt steht es auf display:
none und der obere Abstand kommt direkt aus dem padding von .content. Wer
das Tag je sichtbar macht, muss das padding mitziehen -- steht als
Kommentar dabei.

Die Titel-Slide bekommt 88 px, wie Post 1. Das gilt nur fuer sie und liegt
deshalb als Ueberschreibung im Projektstand, nicht im CSS; genauso macht
Post 1 es bei seiner Tagline. Fuer alle sechs ginge 88 nicht: gemessen ist
"Dein Branchenprogramm" dann 1022 px breit bei 896 verfuegbaren, und die
Ueberschriften von Slide 2, 4 und 5 wuerden dreizeilig. Die Grundgroesse
bleibt deshalb 74.

Slide 2 schnitt bisher hart hinter der Vorstellung von Noah. Jetzt leitet
sie ueber, und zwar mit Noahs eigener Bewegung: er raeumt die Erfahrung
erst ein und dreht dann. "Wer lange dabei ist, hat nicht nur Erfahrung",
und der Fliesstext haengt mit "Auch Ablaeufe ..." direkt daran. Das
nachgestellte "Nicht, weil ..." ist bei ihm ein Stilmittel, keine
abgehackte Halbzeile.

Das Wort Zettelwirtschaft ist aus der Ueberschrift raus. Die Zeile sagt
"nicht nur Erfahrung", und was das andere ist, zeigt die Zeichnung
daneben.

Der Projektstand kommt mit. Er traegt eigene Feinjustierungen an
Ueberschriften, Fliesstexten und Zeichnungen, darunter die Einrueckung der
Titel-Ueberschrift von 37 px: damit endet die Zeile bei rund 985 und
bleibt innerhalb der Safe Zone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:59:59 +02:00
dejanandClaude Opus 5 3781cc95d9 Post 2: Slide 3 wird eine Leserichtung
Wechsel-Symbol, Pfeil, Chaos. Was drinhaengt, steht im Symbol; wohin ein
Wechsel fuehrt, steht rechts. Der Fliesstext sagt dasselbe in derselben
Reihenfolge.

Vier Fassungen liegen dahinter, und sie sind lehrreich, weil sie
verschieden falsch waren. Ein verwurzelter Kasten mit gerissener Leine und
eine beladene Sackkarre erzaehlten je eine kleine Szene -- zu viel fuer
eine Slide, die im Vorbeiscrollen sitzen muss. Eine dritte reihte Block,
Pfeile und eine schraffierte Wand nebeneinander; die Wand las niemand als
Wand, sondern als ausgemalten Kasten. Die vierte war ein durchgestrichenes
Symbol, aber der Strich lief quer durch die Woerter.

Was allen vieren fehlte, war eine Richtung. Drei Stationen von links nach
rechts liest man in einer Sekunde, drei Teile nebeneinander nicht.

Das Chaos ist eine Bahn durch lauter zufaellige Punkte, weich gezogen, die
sich dadurch staendig selbst kreuzt. Zwei Durchgaenge fuer die Dichte,
dazu lose Enden nach aussen: ein Knaeuel ohne Enden sieht aus wie eine
Wolke. Der Zufallskeim liegt fest, das Knaeuel ist bei jedem Export
dasselbe.

Die Zeichenflaeche folgt hier der Zeichnung statt umgekehrt. Drei Formen
nebeneinander duerfen in 896 Breite hoechstens rund 200 Radius haben, eine
730 hohe Box waere damit zu drei Vierteln leer gewesen. Sie ist jetzt 520
hoch und sitzt mittig in der freien Hoehe unter dem Text.

"Die Leute, die es kennen" ist raus, aus Bild und Text. Ohne die lange
Zeile durfte die Schrift groesser werden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:22:50 +02:00
dejanandClaude Opus 5 8590fa7e6e Post 2: Texte an Noahs Original, Pfeil ueber die Naht
Die Texte lasen sich maschinell, und der Grund war nicht die Kuerze. Alle
fuenf Ueberschriften hatten dieselbe Form: zwei abgehackte Fragmente mit
Punkt dazwischen, fuenfmal hintereinander, und die Fliesstexte machten es
nach. Jetzt wechseln sich Aussage mit Wendung, Frage und Antwort, Paar und
durchlaufender Satz ab. Die Formulierungen kommen aus Noahs Text statt aus
dem Nichts: "Und wechseln? Kannst du vergessen", "Stammdaten, Auftraegen
und Buchhaltung", "kein Ersatz, sondern der Aufbau drumherum".

Das Wort ERP kommt nicht mehr vor, es heisst Branchenprogramm wie bei
Noah. Slide 2 traegt die Enthuellung aus seinem Absatz, dass ein alter
Betrieb nicht nur Erfahrung vererbt. Slide 6 traegt die Marken-Tagline,
ausgezeichnet wie in Post 1: der Marker sitzt auf dem zweiten Handwerker,
nicht auf der ganzen Zeile.

Der Beitrag ist Noahs Konzept, sagt das aber nirgends geradeheraus. Slide
2 stellt fest, dass hinter den gewachsenen Ablaeufen keines steckt, Slide
5 liefert eines. Das Wort faellt genau einmal.

Slide 1 bekommt Randnotizen: Gluehbirne, Zettel, Maulschluessel. Der
Schluessel brauchte zwei Anlaeufe, erst war er ein Knochen, weil glatt()
die Kanten wegrundet. Ein Werkzeug lebt von scharfen Kanten. Aus CEO wird
Gruender.

Slide 4 zeigt statt einer Wunschliste vier Funktionen mit Haken und
Zeichen. Die Zeichen stehen in einer Spalte statt hinter dem Wort, sonst
haetten die unterschiedlich langen Zeilen eine ausgefranste Kante ergeben.

Zwischen Slide 4 und 5 laeuft ein Pfeil ueber die Naht, auf halber
Slide-Hoehe. Umriss gezeichnet, innen mit Schraegstrichen ausgemalt, in
Tinte statt Gelb: Gelb bedeutet hier Verbindung und gehoert der Zeichnung,
der Pfeil weist nur weiter. Dafuer reichen beide Zeichenflaechen jetzt bis
an die Slide-Kanten, Slide 5 beginnt bei 0 statt bei 92.

Dabei kam ein Fehler heraus, der jede kuenftige Slide getroffen haette:

    "--vis-left:" + (sl.visLeft || 92)

Die 0 ist falsy, also hat || sie als fehlenden Wert behandelt und den
Vorgabewert eingesetzt. Steht jetzt auf ??, ebenso bei visW und visBottom.
Aufgefallen ist es nur beim Nachmessen, nicht beim Hinsehen.

Der Projektstand kommt mit. Er traegt eigene Textfassungen fuer Slide 3
und 6 sowie die Groesse und Lage der Karte auf Slide 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:52:32 +02:00
dejanandClaude Opus 5 9cc82e59ee Post 2: Titel-Slide gezeichnet, Helmlage umschaltbar
Das Foto bleibt roh, gezeichnet wird drumherum. Der Rahmen ist die
Markenform, dazu ein versetzter Zweitzug und eine schraffierte Kante.
Die drei greifen ineinander statt nebeneinanderher zu laufen: der
versetzte Zug ist die Aussenkante des Schattens, die Schraffur fuellt das
Band dazwischen. Zur Schattenfrage steht ein Hinweis im Code -- Paragraf
5 in rules.md verbietet weichgezeichnete Schatten, und die dort notierte
Ausnahme sagt, eine Linie ist kein Schatten. Eine Schraffur besteht aus
Linien.

Der Helm sitzt wahlweise an der abgeschnittenen Rahmenecke oder auf
Noahs Kopf. Das haengt am Layout-Schalter der Slide, also am vorhandenen
Geruest, und nicht an einem eigenen Sonderschalter. In der Ecken-Lage
kippt er nach rechts, damit die Oeffnung ueber der Ecke liegt: nach links
gekippt sah er aus, als falle er herunter.

Drei Anlaeufe stecken im Helm, die Zwischenschritte stehen als Kommentar
dabei. Erst war die Kuppel flach und die Krempe eine geschlossene Scheibe
darunter, zusammen eine Schirmmuetze. Dann kam Schraffur dazu, und die
Waagerechten kreuzten die Grate zu einem Gitter, also einem Korb. Was
traegt, sind viele Stuetzpunkte, ein First, zwei Nebenrippen und eine
Krempe mit Dicke.

Drei Dinge waren technisch noetig:

Die Zeichenebene ist rechts breiter geworden, von 114,8 auf 128,4
Prozent. In der Ecken-Lage ragt der Helm ueber den Rahmen hinaus und
waere sonst beim Export abgeschnitten worden.

renderPage erneuerte bisher nur den Textblock. Ein Layoutwechsel haette
die Zeichnung nicht angefasst und der Schalter waere wirkungslos
geblieben. Jetzt wird die Visualisierung mitgezogen; der Zufallskeim ist
gesetzt, es kommt derselbe Strich heraus.

VIS-Funktionen bekommen den Slide-Index, sonst kann eine Zeichnung ihren
eigenen Layout-Zustand nicht lesen.

Im Foto ist der Eckenradius von 38 auf 8 runter und der Navy-Rand
entfallen. Mit dem gezeichneten Rahmen waeren das sonst drei Rahmen
uebereinander gewesen. Das weicht vom Nachbau der Website-Karte ab und
ist bewusst so.

Der Projektstand kommt mit: Slide 1 traegt jetzt manuelle Anpassungen aus
dem Generator. Der Fliesstext ist geleert, Ueberschrift und Karte sind
verschoben, das Foto steht dadurch allein.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:34:01 +02:00
dejanandClaude Opus 5 87834304fa Post 2: Slide 3 und 4 gezeichnet, altes Layout-CSS raus
Damit sind alle sechs Slides im selben Stil. Slide 1 bleibt Noahs Foto,
das ist Absicht: ein echtes Gesicht zwischen fuenf Zeichnungen ist ein
Kontrast, kein Bruch.

Slide 3 hat zwei Haelften im Fliesstext, und beide brauchten ein Bild.
"Jahre an Daten und Wissen haengen drin" wurde zu Wurzeln im Boden, "am
Ende dasselbe, nur woanders" zu demselben Kasten noch einmal, nur
gestrichelt. Das leere Ziel der alten Fassung war schlicht die falsche
Aussage. Der Bruch sitzt weiterhin auf dem Weg und nicht auf dem Ziel,
nur ist aus dem Kreuz eine gerissene Leine geworden.

Zwei Anlaeufe waren dafuer noetig. Die Wurzeln endeten zuerst oberhalb
der Erdlinie und sahen deshalb aus wie Beine; sie muessen die Linie
kreuzen. Und die Fasern der gerissenen Leine zeigten aufeinander zu, das
ergab zwei Pfeilspitzen und damit die gegenteilige Aussage. Jetzt haengen
beide Enden nach unten.

Slide 4 zeigte vier Kaesten mit "fehlt". Das ist der Mangel, die Slide
handelt aber vom Wunsch. Jetzt ein Blatt mit einer Liste, in den Worten
eines Betriebs statt in Feature-Namen: die stehen schon im Fliesstext.
Die Kaestchen bleiben leer, erfuellt wird der Wunsch auf Slide 5.

Das Layout-CSS aller alten Visualisierungen ist entfernt, dazu die
verwaisten Kasten-Regeln und der kasten-Baustein. Die Klassen haengen
weiter am Container, und die Regeln haben dreimal zugeschlagen: zuletzt
schob .vis-lose mit zwei Spalten das SVG auf ein Drittel Breite. An der
Stelle steht jetzt ein Hinweis statt einer Leerzeile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:58:29 +02:00
dejanandClaude Opus 5 6f58896f12 Post 2: Slide 2 wird ein Knaeuel
Statt drei Kaesten an einer Schiene jetzt fuenf beschriftete Kanaele mit
einer gestrichelten Leitung dazwischen. Vorbild war ein fremder Beitrag,
uebersetzt in unsere Handschrift und ohne fremde Produktnamen: E-Mails,
Zettel im Auto, Tabellen, Fotos auf dem Handy, Ordner im Regal. Das ist
naeher am SHK-Betrieb als WhatsApp und Excel.

Bewusst ohne Aufloesung. Das Vorbild packt Problem und Antwort in ein
Motiv, bei uns ist die Antwort Slide 5. Zweimal dieselbe Pointe waere
einmal zu viel.

Die Anbindung der Leitung war im ersten Anlauf falsch, und zwar aus zwei
Gruenden. Die Stuetzpunkte lagen direkt auf der Kastenkante, danach lief
die Bahn irgendwohin: die Linie streifte den Kasten, statt an ihm
anzusetzen. Und wo zwei Stuetzpunkte fast aufeinanderlagen, warf die
weiche Kurve einen Zipfel. Jetzt bekommt jedes Stueck vor und hinter dem
Kasten einen Punkt auf gleicher Hoehe, 52 px daneben. Die Bahn laeuft
dadurch waagerecht heraus und waagerecht hinein und haengt sichtbar am
Kasten.

Die Leitung traegt den Akzent in Gelb. Damit steht ein Gegensatz, der
auch fuer die naechsten Beitraege taugt: gestrichelt und gelb heisst
gewachsen, durchgezogen und gelb heisst verbunden. Dieselbe Farbe, die
Strichart trennt die Aussage.

Die Kaesten sind chamfert, also die Markenform, nicht die runden Chips
des Vorbilds. Neu als Primitiv dazu: chamfer als gezeichneter Achtkant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:14:20 +02:00
dejanandClaude Opus 5 45342df1bd Post 2: Slide 5 und 6 als Handzeichnung
Die grauen Kaesten sind raus. Beide Slides tragen jetzt gezeichnete
Visualisierungen aus einer kleinen Primitiv-Bibliothek: Strich, Rechteck,
Kringel, Wabe, Schraffur, Pfeil, Bodenschatten. Alles Inline-SVG, keine
neue Bibliothek, kein Netz.

Der Zufallskeim ist fest verdrahtet, und das ist kein Schoenheitsfehler:
der Export rendert bei fehlgeschlagener Pruefung bis zu fuenfmal neu. Mit
ungesetztem Math.random zittert jeder Durchgang anders, und die Kontrolle
vergleicht gegen ein Bild, das es so nie wieder gibt.

Slide 5 zeigt HiveGrid als Wabe in der Mitte, sechs Satelliten drumherum.
Die Winkel sind nicht frei gewaehlt: eine Flat-Top-Wabe mit Verhaeltnis
2/Wurzel3 ist ein regelmaessiges Sechseck, ihre Ecken liegen also auf
0/60/120/180/240/300 Grad. Jede Speiche setzt dadurch an einer Ecke an
statt irgendwo auf einer Kante.

Slide 6 uebersetzt die Sektion "So geht's weiter" von der Website: drei
Schritte, Pfeile dazwischen, Brief, Sprechblase, Knallbonbon. Die auf der
Website gefuellten Sechsecke sind hier schraffiert. In einer Zeichnung ist
Schraffur die Fuellung, und der Raster-Grund bleibt dabei sichtbar.

Das Layout-CSS beider alter Visualisierungen ist entfernt. Die Klassen
haengen weiter am Container, und .vis-verbinden mit seinem display:grid
hat das neue SVG erst als Rasterzelle auf ein Drittel zusammengeschoben.
Wer eine Visualisierung auf SVG umstellt, muss ihr Layout-CSS mitnehmen.
An beiden Stellen steht jetzt ein Warnkommentar.

Das SVG traegt width und height als Attribute, nicht nur den viewBox.
Ohne sie hat Chrome die Hoehe nicht aus dem Seitenverhaeltnis abgeleitet
und die Zeichnung lief in den Fliesstext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 23:40:48 +02:00
dejanandClaude Opus 5 0c08b18dd9 Uebergabe zu Post 2
Alles zum Beitrag vom 12.08. an einer Stelle: die sechs Slides mit Text und
Akzentwort, was die Visualisierungen zeigen und warum, die gemessenen Maße
samt Anleitung zum Nachmessen, der offene Rest und die Fallen, die schon
zugeschnappt sind.

Damit laesst sich ohne den Gespraechsverlauf weiterarbeiten. Die README
verweist darauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:29:24 +02:00
dejanandClaude Opus 5 ce56735077 Post 2: Visualisierungen fuellen das Format
Gemessen statt geschaetzt: der Inhaltsblock endet bei y 415 bis 460, unten
bleiben 92 px Rand, also rund 800 px freie Hoehe. Die Visualisierungen waren
115 bis 393 hoch, der Rest war tote Flaeche. Jetzt fuellen alle sechs das
Format mit 70 bis 133 px Luft zum Text. Slide 6 bleibt bewusst kleiner und
steht mittig: ein Formularfeld auf 700 px aufgeblasen sieht aufgeblasen aus.

Slide 2 hat eine echte Schiene mit gedrehter Beschriftung statt zweier
unerklaerlicher Balken. Der Riegel aus der alten Fassung war noch im Markup,
sein CSS aber schon weg. Die Schritte heissen jetzt "abtippen, noch mal
abtippen, und noch mal" statt dreimal "so wie immer".

Der gelbe Akzent auf "Chat" ist raus. Die vier Bausteine sind gleichwertig,
und Gelb bedeutet auf den anderen Slides etwas anderes.

Im Stand lagen Probier-Verschiebungen, die Noahs Karte auf 700 px vergroessert
und 250 px nach unten geschoben hatten. Deshalb war sie unten abgeschnitten,
obwohl die Vorgabe im Code stimmte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:29:24 +02:00
dejanandClaude Opus 5 71a7ac79a9 Server verschluckt keine Fehler mehr
Der Filter in log_message pruefte args[0] mit "in", bekommt bei send_error aber
einen HTTPStatus statt der Anfragezeile. Der TypeError riss den Handler-Thread
mit, der Browser wartete auf eine Antwort, die nie kam, und die Seite blieb
ohne Meldung bei "Lade Screens ..." stehen.

args[0] wird jetzt in str() gewickelt, und alles, was keine gewoehnliche
Anfragezeile ist, wird geloggt statt verschluckt. Genau das haette die Suche
vorher abgekuerzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:29:02 +02:00
dejanandClaude Opus 5 3f72d892c5 Redaktionsplan Instagram als PDF
Vier Seiten: Rahmen und Rhythmus, der Plan vom 12.08. bis 14.09., Bildsprache,
taegliche Interaktion und Auswertung. Layout, Schrift und Farben wie beim
Webinar-Vergleich.

Der Kanal ist in dieser Phase kein Reichweiten-, sondern ein
Darstellungskanal: fuer Leute, die ohnehin auf uns stossen. Das steht in der
Praemisse und traegt die Entscheidungen, etwa warum Carousels vor Reels
kommen, obwohl Reels unter 50.000 Followern mehr Reichweite bringen.

15 Termine, drei pro Woche um 12:00. Sechs Sorten im Wechsel, damit sich das
Konto nicht wie ein Prospekt liest: acht Funktionen, zwei zu Menschen und
Betrieb, zwei Einwaende, je einmal Haltung, Wissen ohne Produkt und Aufruf.
Das erste Reel am 09.09. ist eine Bildschirmaufnahme, dafuer braucht es keine
Kamera.

Gebaut wird mit "python3 redaktionsplan/bauen.py". Schrift und Logo kommen aus
den Assets dieses Repos, nicht aus einem Nachbarordner, damit sich das PDF
ueberall bauen laesst. Die erzeugte HTML ist ignoriert, sie entsteht neu.

Zahlen zu Ausspielung und Posting-Zeiten sind belegt und als Groessenordnung
gekennzeichnet, nicht als Zusage. Sie stammen aus Auswertungen Dritter, nicht
von Meta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:08:28 +02:00
98 changed files with 115888 additions and 244 deletions

No files matched your search

+9
View File
@@ -1 +1,10 @@
.DS_Store
# Zwischenschritt beim PDF-Bau, entsteht neu aus bauen.py
redaktionsplan/*.html
# Wegwerf-Profil des Browsers beim Bau der Video-Komponenten
video/.chrome/
# Python-Zwischendateien von serve.py
__pycache__/
+13
View File
@@ -0,0 +1,13 @@
# posts (PostGrid)
Hier entstehen die Bild- und Animationsposts von HiveGrid. Regeln in `rules.md`.
**PFLICHT, ohne Ausnahme:** Jede Oberfläche in einem Post wird aus den echten Komponenten von `hivegrid-cloud`
nachgebaut, mit deren Maßen, Farben, Schrift, Icons und Beschriftungen, und bildet einen Screen nach, den es im Produkt
gibt. Keine ausgedachten, „vibecoded“ Grafiken. Vor jedem neuen Post `komponenten.md` lesen und die Bausteine aus
`post8.html` übernehmen (rules.md §0).
- Code von hivegrid-cloud immer von `origin/main` lesen (`git show origin/main:<pfad>`), die Arbeitskopie ist veraltet
und wird nicht angefasst.
- PostGrid starten: `./serve.sh`, dann http://127.0.0.1:4321/. Der Export (`node animation.mjs`) lädt immer von Port 4321.
- Texte: schlichtes Deutsch, keine Geviertstriche.
+190
View File
@@ -0,0 +1,190 @@
# Post 2 · Die Geschichte
**Erscheint Mi 12.08.2026, 12:00 Uhr.** Carousel, 6 Slides, 1080 × 1350 px.
Zweiter Beitrag des Kontos `hivegrid.de` (nach „Vom Handwerker für Handwerker").
Diese Datei ist die Übergabe: alles, was zum Beitrag bekannt ist, damit man ohne
den Gesprächsverlauf weiterarbeiten kann. Der Redaktionsplan drumherum steht in
[`redaktionsplan/`](./redaktionsplan/).
---
## 1. Worum es geht
Die Herkunft, nicht die Funktionen. Warum es HiveGrid gibt: ein SHK-Betrieb in
vierter Generation, der sein Branchenprogramm nicht wechseln konnte und deshalb
drumherum gebaut hat.
**Vorlage ist der Text von der Website** (`HiveGrid-Website`,
`app/playground/PlaygroundClient.tsx`, Abschnitt „Warum es HiveGrid gibt").
Der vollständige Text gehört **unter** den Beitrag, nicht auf die Slides.
> Auf den Slides steht die Zuspitzung, in der Bildunterschrift der ganze Text.
> Ein früherer Stand hatte die Website-Absätze wörtlich auf den Slides. Das war
> falsch: Auf einer Slide muss eine Zeile im Vorbeiscrollen sitzen.
---
## 2. Die sechs Slides
| # | Schlüssel | Überschrift | Fließtext | Visualisierung |
|---|---|---|---|---|
| 1 | `titel` | Die Geschichte fängt **auf der Baustelle an.** | Noah Geerkens, SHK-Betrieb in vierter Generation. | `karte` |
| 2 | `prozesse` | Nicht sinnvoll. **Nur so gewachsen.** | Die Software gibt den Takt vor. Nicht der Betrieb. | `schiene` |
| 3 | `wechseln` | Wechseln? **Kannst du vergessen.** | Jahre an Daten und Wissen hängen drin. Am Ende dasselbe, nur woanders. | `ersetzen` |
| 4 | `mehr` | Wir wollten nicht weg. **Wir wollten mehr.** | KI, CRM, Automatisierung. Und Tools, die miteinander reden. | `lose` |
| 5 | `verbinden` | Dein ERP bleibt. **Der Rest kommt dazu.** | Nichts wird ersetzt. Alles wird verbunden. | `verbinden` |
| 6 | `warteliste` | Gebaut im Betrieb. **Für den Betrieb.** | Setz dich auf die Warteliste. | `warteliste` |
Fett = das mit dem Marker hervorgehobene Akzentwort (`<span class="a">`).
**Zeilenlängen sind geprüft.** Bei 74 px und `-0.03em` passen rund 26 Zeichen in
eine Zeile, mit Marker etwas weniger. Keine Zeile hier ist länger als 22, es
bricht also nichts um und alle Überschriften bleiben zweizeilig. Gemessen wurde
mit der echten Manrope (variables Latin-Subset, Gewicht 800) über die
Vorschubbreiten, nicht geschätzt.
---
## 3. Die Visualisierungen
**Kein Bildmaterial außer Noahs Karte.** Alles andere ist HTML und CSS. Damit
bleibt es beim 3-fachen Export so scharf wie die Schrift, und jede
Visualisierung lässt sich im Canvas verschieben und in der Breite ändern wie
jedes andere Element. Sie heißt dort **„Visualisierung"**.
Gebaut aus derselben Formensprache wie die Slides: chamferte Kästen, Sechsecke,
der eine gelbe Akzent. Durchgezogene Balken heißen verbunden, gestrichelte offen.
| Name | Was zu sehen ist | Gedanke dahinter |
|---|---|---|
| `karte` | Noahs Teamkarte von der Über-uns-Seite | Ein Gesicht, kein Symbol |
| `schiene` | Drei Schritte an einer Schiene mit der Beschriftung „seit Jahren so" | Die Schiene gibt die Reihenfolge vor, nicht der Betrieb |
| `ersetzen` | Schwerer Bestand, durchgestrichener Pfeil, leeres Ziel | Das Kreuz liegt auf dem **Pfeil**, nicht auf dem Ziel: der Wechsel scheitert, nicht das neue System |
| `lose` | Vier Kacheln, deren gestrichelte Stummel ins Leere laufen | KI, CRM, Automation, ERP wissen nichts voneinander |
| `verbinden` | ERP als Sechseck in der Mitte, das Neue drumherum | Der Kern bleibt, außen kommt dazu |
| `warteliste` | Eingabefeld, Knopf, Adresse | Der Abschluss ist ein Feld, kein Bild |
### Noahs Karte
Nachbau der Karte von der Über-uns-Seite **im aktiven Zustand**. Werte direkt aus
`HiveGrid-Website/app/ueber-uns/TeamCards.tsx`:
```
Hintergrund grayscale(.04) saturate(.74) brightness(.73)
Person saturate(1) contrast(1.01)
Kartengrund #0f1a2e Radius außen 38 px, innen 27 px
Namensschild gelb, links unten, "Noah" + "CEO"
```
Bilder liegen unter `assets/team/noah-bg.webp` und `noah-person.webp`.
> **Ohne die weichgezeichnete Ellipse des Originals.** Die hängt an
> `backdrop-filter`, und was `html-to-image` daraus macht, ist nicht verlässlich.
---
## 4. Maße, gemessen
Der Inhaltsblock (Überschrift plus Fließtext) endet je nach Textlänge bei
**y 415 bis 460**. Unten bleiben 92 px Rand. Für die Visualisierung stehen also
rund **800 px Höhe** zur Verfügung, und die soll sie auch füllen.
| Slide | Visualisierung: oben | Höhe | Luft zum Text | Abstand nach unten |
|---|---|---|---|---|
| 1 | 499 | 775 | 84 | 76 |
| 2 | 548 | 682 | 133 | 120 |
| 3 | 566 | 664 | 106 | 120 |
| 4 | 502 | 700 | 70 | 120 |
| 5 | 470 | 732 | 83 | 120 |
| 6 | 603 | 466 | 188 | 281 |
Slide 6 ist bewusst kleiner und steht dafür mittig in der freien Fläche. Ein
Formularfeld auf 700 px aufzublasen sieht aufgeblasen aus.
**Wie nachmessen:** Eine Kopie von `post2.html` anlegen, vor `</body>` ein
Skript hängen, das `getBoundingClientRect` der Elemente in `document.title`
schreibt, und mit
`Google\ Chrome --headless --dump-dom` auslesen. Auf `.holder .slide` warten,
die Seite baut asynchron auf.
---
## 5. Stand der Dinge
**Fertig:** Alle sechs Slides sind gebaut, die Texte auf Slide-Länge, die
Visualisierungen füllen das Format, Noahs Karte sitzt.
**Offen:**
1. **Export und Kontrolle.** Über `Datei → Alle Slides als ZIP`, 3-fach.
Danach jede Slide ansehen: der Export hat schon zweimal Bilder verschluckt,
die Prüfung meldet das inzwischen, aber ein Blick schadet nicht.
2. **Mitgestalter.** Falls Noahs Betriebsprofil mit drauf soll, **beim Posten**
einladen. Nachträglich geht es zwar, aber der Beitrag erscheint dann erst
nach der Annahme bei dessen Followern und die Ausspielung ist vorbei.
---
## 5a. Die Bildunterschrift
Grundlage ist Noahs Absatz von der Website, gekürzt und **aus der Ich-Form
geholt**: das Konto ist `hivegrid.de`, nicht Noah. Er wird deshalb in der
dritten Person eingeführt, danach trägt „wir" den Text — das sind die Gründer,
und Noah ist einer davon.
```
Ein Betrieb in vierter Generation hat nicht nur Erfahrung. Er hat auch Abläufe, die laufen, weil die Software es vorgibt.
Noah Geerkens führt so einen Betrieb, SHK, mit seiner Schwester. Da fängt die Geschichte von HiveGrid an.
Und wechseln? Kannst du vergessen. Da hängen Jahre an Stammdaten, Aufträgen und Buchhaltung drin. Kein Chef fährt seinen Betrieb ein halbes Jahr an die Wand, um am Ende dasselbe zu haben, nur woanders.
Wollten wir auch nicht. Wir wollten mehr: KI, Projektmanagement, Automatisierungen. Dass die Tools im Handwerksbetrieb endlich miteinander reden.
Genau dafür gibt es HiveGrid: kein Ersatz für deine Branchensoftware, sondern der Aufbau drumherum. Dein ERP bleibt, wo es ist.
Kein Startup-Projekt. Entstanden im Handwerksalltag, gebaut für den Handwerksalltag.
#handwerk #shk #handwerksbetrieb #sanitärheizungklima #branchensoftware #digitalisierungimhandwerk #erp #ki #hivegrid
```
**Gegen die Vorgaben aus dem Redaktionsplan geprüft:**
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 121 Zeichen |
| 300 bis 800 Zeichen | 793 |
| 5 bis 10 Hashtags, unten abgesetzt | 9 |
| Suchwörter zusätzlich im Fließtext | Handwerk, SHK, Branchensoftware, ERP, KI, Automatisierungen |
Zwei Dinge, die nicht drinstehen dürfen und geprüft sind: **„Verbinden statt
ersetzen"** ist für den Beitrag am 14.08. reserviert (§7), und **„Was kaum
einer sagt"** aus dem Website-Text ist raus — die Wendung war schon auf den
Slides gestrichen.
---
## 6. Fallen, die schon zugeschnappt sind
- **Der offene Tab überschreibt die Datei.** `state/post2.json` gewinnt nur
**beim Laden**. Wer die Datei von außen ändert und dann im offenen Tab klickt,
bekommt den alten Stand zurückgeschrieben. Also erst neu laden, dann klicken.
Änderungen, die sicher ankommen müssen, gehören in den Ladepfad (`migrateState`).
- **Probier-Verschiebungen bleiben im Stand.** Slide 1 war unten abgeschnitten,
weil die Karte auf 700 px vergrößert und 250 px nach unten geschoben war. Wenn
eine Vorgabe im Code nicht wirkt, zuerst `edits` im Stand prüfen.
- **Der Server verschluckt Fehler.** Ein Filter in `log_message` ist über
`HTTPStatus` gestolpert und hat den Thread mitgerissen; die Seite blieb dann
ohne Meldung bei „Lade Screens ..." stehen. Behoben, aber wenn die Seite
wieder hängt: zuerst `serve.py` neu starten und in die Ausgabe schauen.
- **Texte von der Website gehören nicht auf Slides.** Siehe oben.
---
## 7. Was danach kommt
Der Redaktionsplan sieht als nächsten Beitrag **Fr 14.08. „Verbinden statt
ersetzen"** vor, ein Einzelbild mit der Kernaussage. Der Satz „Verbinden statt
ersetzen" ist dafür reserviert und taucht deshalb in keiner Bildunterschrift
vorher wörtlich auf.
+120
View File
@@ -0,0 +1,120 @@
# Post 4 · Workflow-Engine
**Erscheint Mi 19.08.2026, 12:00 Uhr.** Carousel, 4 Slides, 1080 × 1350 px.
Nummer 3 im Redaktionsplan, im Generator das vierte Projekt.
Diese Datei ist die Übergabe: alles, was zum Beitrag bekannt ist, damit man ohne
den Gesprächsverlauf weiterarbeiten kann. Der Redaktionsplan drumherum steht in
[`redaktionsplan/`](./redaktionsplan/), die Gestaltungsregeln in
[`rules.md`](./rules.md), der Aufbau der Datei in [`README.md`](./README.md).
---
## 1. Die vier Slides
| # | Schlüssel | Überschrift | Fließtext | Bild |
|---|---|---|---|---|
| 1 | `anfrage` | Automatisiere **deinen Betrieb.** | — | ein Ablauf auf dem Canvas |
| 2 | `ausloeser` | Erst der **Auslöser.** | Was den Ablauf startet. | Auslöser-Auswahl |
| 3 | `aktionen` | Dann die **Aktionen.** | Was danach passiert. | Aktionen-Auswahl |
| 4 | `vorlagen` | Fang nicht bei **null an.** | Starte mit einer fertigen Vorlage. | drei Vorlagen |
Fett = das Akzentwort. Die Titel-Slide trägt keinen Fließtext: der Ablauf
darunter sagt, was gemeint ist.
**Vier statt fünf bis sieben.** Der Redaktionsplan nennt eine Spanne, kein Soll.
Eine Slide, die nichts hinzufügt, kostet einen Wisch und gibt nichts zurück.
---
## 2. Die Bildunterschrift
```
In den meisten Handwerksbetrieben sitzt zwischen der Anfrage und der Aufgabe ein Mensch, der klicken muss.
Genau den nimmt die Workflow-Engine aus der Kette. Du wählst einen Auslöser: neue Anfrage, Kontakt geändert, Termin in Kürze, Zahlungsziel überschritten. Dann hängst du Aktionen dran: Aufgabe anlegen, Person zuweisen, Benachrichtigung senden. Ab da läuft die Automatisierung von selbst.
Und wenn die Angaben mal nicht reichen, geht es den anderen Weg: nachfragen statt liegenlassen. Kein Vorgang fällt hinten runter.
Zusammengeklickt, Kästchen für Kästchen. Ohne eine Zeile Code. Und wer nicht bei null anfangen will, nimmt eine fertige Vorlage und baut sie um.
#handwerk #shk #handwerksbetrieb #digitalisierungimhandwerk #automatisierung #workflow #prozesse #hivegrid
```
**Gegen die Vorgaben aus dem Redaktionsplan geprüft:**
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 106 Zeichen |
| 300 bis 800 Zeichen | 671 |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | Handwerk, Automatisierung, Workflow, Auslöser, Aktionen, Vorlage, Anfrage, Aufgabe |
Drei Wendungen kommen bewusst **nicht** vor: „Verbinden statt ersetzen" gehört
Post 3, „Wenn-dann statt Handarbeit" ist die Überschrift von Post 1, und
Em-Dashes verbietet `rules.md` §4.
---
## 3. Woher die Texte kommen
Alle Produktbegriffe stehen wörtlich in der Quelle:
| Was | Quelle |
|---|---|
| Knotennamen und ihre Farben | `assets/screens/workflow.png` |
| Bausteinliste, Rubriken, Beschreibungen | `assets/screens/workflow-bausteine.png` |
| Artnamen, Zeichen, Farbzuordnung | `NodeWorkflowEngine.tsx` im Pitch-Paket |
| Vorlagennamen und Startbausteine | `GENERAL_WORKFLOW_PRESETS` im Click-Dummy |
| „Auslöser wählen, Aktionen dranhängen, fertig" | Website, `LandingPageClient.tsx` |
| „Starte mit einer fertigen Vorlage …" | Website, `lib/produkt-daten.ts` |
Überschriften und Fließtexte sind Zuspitzung. Sie behaupten nichts, was nicht in
einer dieser Quellen steht.
---
## 4. Drei Stellen, an denen bewusst abgewichen wird
1. **Slide 1 lässt einen Schritt aus.** Zwischen „Formular eingegangen" und
„Passender Lead?" steht im Screenshot noch „Daten vereinheitlichen". Alle
drei echten Abläufe verzweigen erst an Position 3 oder 4, und das füllt ein
hochkantes Format schlecht. Ein Ablauf, den es genau so gibt, ist es damit
nicht mehr; einer, den es geben könnte, schon.
2. **Slide 4 nennt Demo-Abläufe „Vorlagen".** Dass es eine Vorlagen-Bibliothek
gibt, steht als Merkmal auf der Website; dass sie diese drei enthält, steht
nirgends. Im Werkzeug werden sie über `workflowId` geladen, nicht als
Galerie gezeigt.
3. **Kein PDS im Beitrag.** Von den sechs Vorlagen nennen drei das
Branchenprogramm. Übrig bleibt genau `GENERAL_WORKFLOW_PRESETS` — keine
Auswahl nach Geschmack, sondern die vollständige zweite Gruppe. Gekürzt
wurde nichts.
---
## 5. Stand der Dinge
**Fertig:** Alle vier Slides sind gebaut und exportieren sauber, je
2160 × 2700 px (`01-anfrage-hell.png` bis `04-vorlagen-hell.png`).
**Offen:**
1. **Export und Kontrolle.** Über *Datei → Alle Slides als ZIP*. Der Export
prüft Screenshot-Flächen nach; dieser Beitrag hat keine, die Prüfung läuft
also leer durch. Ein Blick auf die Dateien schadet trotzdem nicht.
2. **Mitgestalter.** Falls jemand mit drauf soll, **beim Posten** einladen.
---
## 6. Eine Falle, die schon zugeschnappt ist
**Regeln aus dem CSS nimmt man als ganzen Block.** Beim Entfernen der
Karten-Regeln von Post 2 blieben rund neunzig Zeilen Deklarationen ohne
Selektor stehen, weil nur die Selektorzeilen weggeschnitten waren. CSS fängt
sich davon nicht wieder: der Parser überspringt bis zur nächsten Marke, und
dabei verschwand `[data-edit]`. Damit war das Verschieben von Elementen im
Canvas tot, ohne dass die Seite einen Fehler zeigte.
Sichtbar wurde es erst, als ein gespeicherter Versatz von 3 px nicht mehr
ankam. Wer hier Regeln entfernt, sieht danach im Browser nach, ob
`document.styleSheets` sie noch alle kennt.
+224
View File
@@ -0,0 +1,224 @@
# Post 5 · Projektmanagement
Vierter Beitrag der Produktreihe, Termin **Fr 21.08.** laut Redaktionsplan,
Stichwort dort: „Der Überblick, den jeder Betrieb vermisst."
Hell, Theme `modern`, Punktgrund, Text zentriert — dieselbe Anlage wie Post 4,
damit die Reihe als Reihe erkennbar bleibt. Datei `post5.html`, Stand in
`state/post5.json`, erreichbar über das Projektmenü jeder Seite und über
`uebersicht.html`.
**Fünf Slides, und jede zeigt einen anderen Bildschirm.** Das Demo-Projekt hat
für die Pipeline vier davon — Schablone, Board, Liste, Detail — und die tragen
zusammen mehr als ein einzelner. Die fünfte Slide ist die Karte aus der Nähe.
---
## 1. Die fünf Slides
| # | Tag | Überschrift | Screen | Was zu sehen ist |
|---|---|---|---|---|
| 1 | Projektmanagement | Von der Anfrage **bis zur Abnahme.** | 4A | Das Board: drei Phasen mit Anzahl, Summe und Karten |
| 2 | Die Karte | Alles Wichtige **auf einer Karte.** | 4A | Eine Karte groß |
| 3 | Zwei Pipelines | Vom Verkauf **in die Abwicklung.** | 4B | Ein Projekt in „Auftragsabwicklung SHK", mit dem Knopf für den Pipelinewechsel |
| 4 | Die Liste | Board oder **Liste.** | 4C | Dieselben Projekte als Tabelle, mit gesperrten Auftragswerten |
| 5 | Die Schablone | Deine Felder, **deine Regeln.** | 3 | Der Feldeditor, offen auf „Aufmaß erledigt" |
**Slide 3 ist die Pointe.** Das Demo hat nicht eine Pipeline, sondern zwei:
`Vertrieb: Anlagen-Verkauf` und `Auftragsabwicklung SHK`. Ein Projekt liegt in
einer davon und wechselt über den Knopf `Pipeline` in die andere. Das ist der
Unterschied zwischen Vertriebs- und Abwicklungs-Projektmanagement, und er ist
keine Auslegung: die beiden Namen stehen in
`PipelineBoardInteractive.tsx` und `S02PipelinesDetailansicht.tsx`.
**Slide 5 erklärt, warum das eine Software sein kann.** Die Felder hängen an
der Pipeline, nicht am Programm. Siebzehn Feldtypen, dazu drei Stufen unter
`Pflicht ab Board-Spalte`. Ein SHK-Betrieb legt sich „Aufmaß erledigt" an,
ein anderer etwas anderes.
Slide 1 trägt das Board, weil sie im Profilraster eine Kachel von rund 150 px
ist. Dort ist kein Wort lesbar; was ankommt, sind Säulen aus Kärtchen.
Sie trägt **keinen Fließtext**, wie die Titelbilder von Post 3 und Post 4.
„Jedes Projekt im Griff" stand zuerst als Überschrift da: zwei Zeilen von
dreizehn und neun Zeichen liegen als schmaler Block in einer breiten Fläche,
und die Slide wirkte dadurch leer. Jetzt trägt sie die zweite Hälfte derselben
Überschrift aus `lib/produkt-daten.ts`. Die Lücke über dem Board schließt
`visBottom: 215` statt eines Satzes.
---
## 2. Die Bildunterschrift
```
Die häufigste Frage im Handwerksbüro ist keine fachliche. Sie lautet: wo steht das eigentlich gerade?
In HiveGrid steht jedes Projekt in genau einer Phase. Auf der Karte siehst du Gewerk, Auftragswert, Priorität, wer zuständig ist und was als Nächstes ansteht, ohne sie zu öffnen.
Sobald der Auftrag steht, wechselt das Projekt die Pipeline. Aus dem Verkauf wird die Abwicklung, mit denselben Daten. Verkauft ist nicht fertig.
Ob Board oder Liste, entscheidest du. Welche Felder eine Pipeline trägt, ebenfalls: ein SHK-Betrieb legt sich ein Häkchen fürs Aufmaß an, ein anderer Betrieb etwas anderes.
Kein Suchen, kein Nachfragen, kein Zettelchaos.
#handwerk #shk #handwerksbetrieb #digitalisierungimhandwerk #projektmanagement #auftragsabwicklung #vertrieb #hivegrid
```
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 101 Zeichen |
| 300 bis 800 Zeichen | 651 |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | Handwerk, Projekt, Phase, Auftrag, Vertrieb, Abwicklung, Pipeline, Aufmaß |
„Kein Suchen, kein Nachfragen, kein Zettelchaos" steht wörtlich so schon im
Fließtext der Projektmanagement-Slide von Post 1. Dieselbe Reihe darf denselben
Satz zweimal benutzen.
---
## 3. Woher die Angaben kommen
Quelle ist durchgehend das Pitch-Paket
(*fertig bombo · Kundentermin 30.07.2026*), Ordner `hivegrid-pitch`.
| Was | Woher |
|---|---|
| Die zehn Phasennamen und ihre Reihenfolge | `PipelineBoardInteractive.tsx`, `STAGES` |
| Board / Liste | die `ToggleGroup` in der Kopfzeile, sie kennt genau diese zwei |
| „Vertrieb: Anlagen-Verkauf" | Seitentitel desselben Screens |
| Alle Projekte auf Slide 1 und 2 | `INITIAL_PROJECTS`, wörtlich |
| Anzahl und Summe je Spalte | aus denselben 41 Datensätzen gezählt und addiert |
| Gewerk zu Farbton | `CATEGORY_TONES` |
| Hoch / Mittel / Niedrig, Balkenzahl, Farbrollen | `ProjectCard.tsx`, `PriorityBadge` und `PriorityIndicator` |
| Aufbau der Karte, Umbruch auf zwei Zeilen, getönter Fuß | `ProjectCardRoot`, Klasse für Klasse |
| Farbe des Kürzels | `avatarColor` je Projekt, aufgelöst über `Avatar.tsx` |
| Projekt, Pipeline, Nummer und Rubriken auf Slide 3 | `S02PipelinesDetailansicht.tsx` |
| Zeilen und Spaltenköpfe auf Slide 4 | `S03ProjektListe.tsx` |
| Feldtypen und `Pflicht ab Board-Spalte` auf Slide 5 | `S05DetailSchabloneEditMode.tsx` |
| Plättchen-, Rand- und Textfarben | `ui/theme.css` |
| Alle Symbole | `@subframe/core`, Pfade aus dem Paket kopiert |
| „Jedes Projekt im Griff" | Überschrift in `lib/produkt-daten.ts` der Website |
---
## 4. Wo bewusst abgewichen wird
**Slide 1 zeigt nur das Board selbst.** Seitentitel, der Umschalter
Board/Liste und die Filterleiste standen einmal darüber und sind alle drei
wieder entfallen: ein Titelbild soll in einem Blick sagen, worum es geht, und
was im Profilraster ankommt, sind Säulen aus Kärtchen. Drei Reihen
Bedienoberfläche darüber liest niemand. Wie die Pipeline heißt, sagt Slide 3;
dass es auch eine Liste gibt, sagt Slide 4.
**Zwei bis drei Karten je Spalte, der Kopf zählt vier.** Außen zwei, in der
Mitte drei. So ist es im Werkzeug auch: eine Spalte zeigt, was aufs
Bild passt, der Rest kommt beim Scrollen. Gleich hohe Säulen sahen aus wie eine
Tabelle, ungleiche sehen aus wie ein Board, auf dem gearbeitet wird.
**Slide 4 zeigt vier von neun Tabellenspalten und sieben von elf Zeilen.** Die
Tabelle hat Projekt-Nr., Projektname, Board, Spalte, Auftragswert,
Kunde / Kontaktrolle, Termin / Hinweis, Tags und Sachbearbeiter. Neun
nebeneinander wären auf 1080 px unter zehn Pixel Schrift.
Die Spalte Sachbearbeiter ist zuletzt noch entfallen, damit
„Auftragswert gesperrt" ungekürzt hineinpasst. Der Wortlaut ist der Grund,
warum diese Spalte überhaupt auf der Slide steht — ihn auf „gesperrt" zu
stutzen wäre der bequeme Weg gewesen und kein Zitat mehr. Weggelassen ist nur
Vorhandenes, dazugedichtet nichts.
**Slide 3 stellt die Rubriken untereinander, das Werkzeug stellt sie
nebeneinander.** Hochkant ist untereinander lesbar, die Reihenfolge bleibt.
**Zwei Rubriken fehlen auf Slide 3.** `PDS-INTEGRATION`, weil der Beitrag sich
an Betriebe allgemein richtet und nicht an die, die zufällig dieses eine
Branchenprogramm fahren — dieselbe Entscheidung wie bei Post 4. Und
`ADRESSDATEN · TEST A (ZUSATZADRESSEN AN)`, weil das ein Arbeitstitel aus der
Entwicklung ist und kein Bildschirmtext.
**Die Karte ist 320 breit wie im Original (`w-80`), der Betrag steht auf 26
statt größer.** Bei „Niedrig" ist das Prioritätsplättchen breiter als bei
„Hoch", und der Betrag stieß dagegen.
**Die Maßstäbe sind aufeinander abgestimmt.** Slide 1, 3, 4 und 5 zeigen ihre
Schrift zwischen 16 und 23 px; nur Slide 2 ist als Nahaufnahme gedacht und
liegt bei 47 px. Vorher stand sie auf 59 bis 73 px — eine einzelne Karte wirkte
damit größer als das ganze Board davor, und die Reihe kippte. Wer an einer
Zeichnung dreht, sollte die Tabelle in §6 danach nachrechnen.
---
## 5. Eine Richtigstellung
**Ein früherer Stand dieses Beitrags behauptete, die Detailansicht sei nur ein
Entwurfs-PNG.** Das war falsch. Geprüft worden waren die Beschriftungen des
Bildes `07_Screen-4b_Karten-Detail.png` — `KUNDENDATEN`, `OBJEKTDATEN`,
`AUFTRAG & FÖRDERUNG`, `Wahrscheinlichkeit`, `Heizlast`, `Lead-Quelle` —, und
die kommen im Code tatsächlich nirgends vor. Daraus wurde der falsche Schluss
gezogen, es gebe den Bildschirm nicht.
Es gibt ihn: `app/(screens)/4b/page.tsx` rendert
`S02PipelinesDetailansicht.tsx`, 3150 Zeilen, mit den Rubriken `GRUNDDATEN`,
`PLANUNG & VERANTWORTLICHE`, `PROJEKTKONTAKTDATEN`, `KONTAKTE`, `FIRMENDATEN`,
`ANSPRECHPARTNER`, `PROJEKTADRESSE` und `ZUSATZADRESSEN`. Das PNG ist ein
älterer Entwurf desselben Bildschirms, nicht sein Beleg.
**Die Lehre ist nicht „Screenshots lügen", sondern: die Abwesenheit einer
Zeichenkette beweist die Abwesenheit eines Bildschirms nicht.** Für die zweite
Frage muss man die Routen ansehen, nicht die Wörter.
---
## 6. Stand der Dinge
| | |
|---|---|
| Slides | 5 |
| Format | 1080 × 1350, Ausgabe zweifach: 2160 × 2700 |
| Luft zwischen Text und Zeichnung | 224 / 239 / 123 / 187 / 133 px (Grenze 40) |
| Zeichnung ragt über die Seite | nein, alle innerhalb der 92-px-Schutzzone |
| Stylesheet vollständig | 214 Regeln, `[data-edit]` vorhanden |
| Konsole | keine Fehler |
| Ausgabeweg | geprüft, `01-board-hell.png` bis `05-schablone-hell.png` |
Geprüft wird mit den Hilfsskripten aus dem Sitzungsordner: `flaeche.mjs` misst
über `getBBox()` die tatsächlich gezeichnete Fläche je Slide, `kopf5.mjs` die
Breite jeder Überschriftzeile (sie ist `nowrap`, eine zu lange Zeile bricht
nicht um, sondern läuft über den Rand), `css5.mjs` zählt die Regeln im
Stylesheet und liest die Konsole mit, `export.mjs` fährt den echten Ausgabeweg
und fängt `/save` ab, damit beim Prüfen nichts in *Downloads* landet.
---
## 7. Zwei Fallen aus diesem Bau
**Ein Screenshot ist keine Quelle.** Die Karte war zuerst aus `pipelines.png`
abgemalt. Drei Sachen lagen daneben, und keine davon war im Bild zu erkennen:
Priorität, Balken und Kürzel stehen im Original als Gruppe rechts und hängen
über die Kante des Fußes, im Fuß steht über dem Termin die Zeitspanne des
Projekts, und die Balken sitzen in einem eigenen umrandeten Kasten. Erst
`ProjectCard.tsx` hat das gezeigt.
**Ein Prüfskript hat den Projektstand verstellt.** `export.mjs` schaltet die
Slides mit der Pfeiltaste weiter. Die Seite startet aber im Bearbeiten-Modus
(`editMode = "an"`), und dort verschieben Pfeiltasten das gewählte Element,
statt die Slide zu wechseln. Jeder Prüflauf hat die Zeichnungen also ein Stück
gerückt und das in `state/post5.json` geschrieben: am Ende stand dort
`vis: {dx: 3, dy: -69}` auf Slide 1 und auf Slide 2 sogar ein `size: 762`, das
die im Code eingestellten 720 überschrieb.
Gemerkt habe ich es an Messwerten, die im Code nirgends standen — eine Slide
meldete 89 px Rand rechts bei eingestellten 92, eine andere 177 px unten bei
eingestellten 120. **Zahlen, die man sich nicht erklären kann, sind ein Befund
und keine Messungenauigkeit.** `export.mjs` verlässt jetzt vor dem Blättern den
Bearbeiten-Modus und schreibt den Stand am Ende zurück, falls er sich doch
verändert hat.
**SVG bricht keinen Text um, und Breite schätzen geht schief.** Die
Plättchenbreite war anfangs aus der Zeichenzahl mal 0,52 em gerechnet. Das
stimmt für „Heizung" und geht bei „Wärmepumpe" daneben; das Wort stand über dem
Rand seines Plättchens. Gemessen wird jetzt mit einem Canvas in derselben
Schrift, die das SVG später zeichnet. Aus derselben Messung kommt der
Zeilenumbruch der Titel, der im Original `line-clamp-2` heißt.
+938
View File
@@ -0,0 +1,938 @@
# Post 6 · Team-Chat
Fünfter Beitrag der Produktreihe, Termin **Mi 26.08.** laut Redaktionsplan
(von Mo 24.08. verschoben). Stichwort dort: „Ein Kanal pro Projekt, Foto rein,
Aufgabe raus."
Hell, Theme `modern`, Punktgrund, Text zentriert — dieselbe Anlage wie Post 4
und 5. Datei `post6.html`, Stand in `state/post6.json`, erreichbar über das
Projektmenü jeder Seite und über `uebersicht.html`.
**Fünf Slides, dazu ein Bewegtbild.** DSGVO und deutsche Server hätten einen
weiteren Punkt ergeben, dafür gibt es aber keinen Bildschirm. Der steht in der
Bildunterschrift, wo eine Aussage ohne Bild hingehört.
Dazu kommt die erste Animation im Repo. Sie ist **keine Slide**: sie steht in
`ANIMS` statt in `SLIDES`, liegt nicht im Streifen und wird vom Bildexport
nicht angefasst. Im Carousel ist sie ein eigenes Element, das mitkommen kann
oder eben nicht. Bedient wird sie im Bereich unter dem Canvas, gebaut mit
einem eigenen Knopf. Wie das funktioniert, steht im
[README unter „Animation"](./README.md#animation); was diese eine zeigt, in
§7 weiter unten.
---
## 1. Die fünf Slides und das Bewegtbild
| # | Tag | Überschrift | Was zu sehen ist |
|---|---|---|---|
| 1 | Team-Chat | Büro und Baustelle, **endlich im selben Chat.** | Kanalliste und Verlauf nebeneinander, mit Filterzeile und Eingabefeld |
| 2 | Die Kanäle | Ein Kanal **pro Projekt.** | Links das Durcheinander als Zeichnung, rechts die Kanalliste |
| 3 | Die Aufgabe | Foto rein, **Aufgabe raus.** | Baustellenfotos, die Aufgabe daraus, die Bestätigung im Projekt |
| 4 | Das Pluszeichen | Nicht nur **Text.** | Das Menü über der Eingabezeile, aus der es aufklappt |
| 5 | Bald verfügbar | Der Chat für unterwegs **wird gerade gebaut.** | Zwei Ausschnitte der Oberfläche, versetzt übereinander, mit dem Absperrband darüber |
Dazu, außerhalb des Streifens:
| Schlüssel | Tag | Überschrift | Was zu sehen ist |
|---|---|---|---|
| `animChat` | Bewegtbild | Chatnachrichten werden **Aufgaben im Projekt.** | 6 s Animation: eine Nachricht wird angeklickt, im Menü „Aufgabe erstellen" gewählt, und die Aufgabe klappt in der Nachricht auf |
**Slide 3 ist der Kern.** Der Redaktionsplan verlangt „Foto rein, Aufgabe
raus", und genau das liegt im Werkzeug als Abfolge vor: Tobias schickt die
Baustellenfotos, darauf folgt eine Nachricht mit angehängter Aufgabe, darauf
eine Systemzeile „Aufgabe „Materialliste prüfen" im Projekt erstellt". Die
letzte Zeile macht den Unterschied: die Aufgabe liegt danach im Projekt und
nicht nur im Verlauf.
Slide 1 trägt den ganzen Chat, weil sie im Profilraster eine Kachel von rund
150 px ist. Dort ist kein Wort lesbar; was ankommt, sind zwei Spalten mit
Blasen.
---
## 2. Die Bildunterschrift
```
Im Handwerk läuft die halbe Baustelle über WhatsApp. Das Foto von letzter Woche findet trotzdem keiner wieder.
In HiveGrid hat jedes Projekt seinen eigenen Kanal, mit Projektnummer. Daneben stehen Direktnachrichten und Gruppen, aber eben nicht alles in einem Verlauf.
Ein Foto von der Baustelle geht rein, alle sehen es sofort. Aus „mach das mal“ wird mit einem Klick eine echte Aufgabe: mit Frist, mit Namen, im Projekt.
Dokument, Sprachnachricht, Kontakt, Termin. Was sonst im Chat-Verlauf verschwindet, hängt hier am Projekt.
Kundendaten gehören nicht in eine WhatsApp-Gruppe. Bei HiveGrid liegen sie verschlüsselt auf deutschen Servern.
#handwerk #shk #handwerksbetrieb #digitalisierungimhandwerk #teamchat #baustelle #kommunikation #hivegrid
```
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 110 |
| 300 bis 800 Zeichen | 644 |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | Handwerk, Baustelle, Projekt, Kanal, Aufgabe, Chat, Kontakt, Termin |
Der letzte Absatz steht **wörtlich** so als Merkmal „DSGVO-konform" in
`lib/produkt-daten.ts` der Website. „Aus „mach das mal" wird mit einem Klick
eine echte Aufgabe" ebenfalls, als Merkmal „Aufgaben-Integration".
---
## 3. Woher die Angaben kommen
Quelle ist `components/general/GeneralTeamChatPage.tsx` aus dem Pitch-Paket
(*fertig bombo · Kundentermin 30.07.2026*).
| Was | Woher |
|---|---|
| Alle acht Kanäle: Titel, Vorschau, Zeit, Zuordnung, Nadel, Ungelesen | `CHATS`, wörtlich |
| Der Verlauf: Autoren, Texte, Zeiten, Anhänge | `INITIAL_MESSAGES.waermepumpe`, wörtlich |
| Aufbau einer Listenzeile (drei Zeilen) | die Zeilen 170 bis 172 der Quelle |
| Aufbau einer Nachricht, Anhangkarte, Systemzeile | `MessageBubble` und `AttachmentCard` |
| Sinnbild je Kanalsorte (Koffer, Personen, Kürzel) | `ChatAvatar` |
| Kopfzeile „P-2026-0042 · 8 Mitglieder · aktiv" | `ChatHeader` |
| Filterzeile Alle / Projekte / Direkt / Ungelesen | der Typ `ChatFilter` |
| Das Menü hinter dem Plus, samt Trenner und Reihenfolge | `Composer` (Beschriftungen oben geändert, §4) |
| Eingabefeld „Nachricht schreiben …" | derselbe Ort |
| Farben, Ränder, Plättchentöne | `ui/theme.css` |
| Alle Symbole | `@subframe/core`, Pfade aus dem Paket kopiert |
| „Büro und Baustelle, endlich im selben Chat." | Überschrift in `lib/produkt-daten.ts` |
**Vom Chat gibt es nur eine Fassung.** Anders als bei der Pipeline, die mit
Board, Liste, Detail und Schablone vier eigene SHK-Bildschirme hat, liegt der
Chat allein unter `/allgemein/team-chat`. Die Inhalte darin sind trotzdem aus
dem Gewerk: Wärmepumpe Müller, Montagetermin, Baustellenfotos.
---
## 4. Wo bewusst abgewichen wird
**Alles, was einen Chat kennzeichnet, ist grün.** Projektkanal, Gruppe und
Direktnachricht tragen dieselbe Marke (`MARKE`, Fläche `#DCFCE4`, Rand
`#B7EDC6`, Zeichen `#148134`) — in der Liste wie im Verlauf. Vorher trug der
Projektkanal Lila, die Gruppe Grau und jede Person ihre eigene Farbe: vier
Familien in einer Liste, und keine davon sagte etwas, das die Zeile nicht schon
sagt. Die Kürzel unterscheiden die Personen, dafür braucht es keine vier
Farbtöne.
**Die vier Profilbilder zeigen niemanden.** `avatarKreis()` nimmt ein Foto,
sobald eines unter `assets/eigene/portrait-<kürzel>.<endung>` liegt (also
`portrait-sk`, `portrait-mb`, `portrait-tw`, `portrait-ms`), rund beschnitten
mit demselben Rand wie der Kürzelkreis; sonst steht dort weiter das Kürzel.
`preloadAssets()` holt alles aus `assets/eigene` ohnehin mit, und der Export
bekommt sie damit als Daten-URL statt als Verweis. Nur Menschen bekommen eins
— Projektkanal und Gruppe behalten ihr Sinnbild.
**Es sind Aufnahmen von Unsplash, keine erzeugten Gesichter.** Vier Portraits,
über die Suche nach `professional headshot` gefunden:
| Datei | Rolle | Quelle |
|---|---|---|
| `portrait-sk` | Sandra Koch, Projektleitung | [SJvDxw0azqw](https://unsplash.com/photos/SJvDxw0azqw) |
| `portrait-mb` | Maike Berger | [ZAo0cKz_IKM](https://unsplash.com/photos/ZAo0cKz_IKM) |
| `portrait-tw` | Tobias Wagner, Montage | [TTdJV-K1IUg](https://unsplash.com/photos/TTdJV-K1IUg) |
| `portrait-ms` | Michael Schuster, Kundendienst | [7YVZYZeITc8](https://unsplash.com/photos/7YVZYZeITc8) |
Ausgewählt wurde nicht nach Gesicht, sondern nach **Unterscheidbarkeit im
Kreis von 42 px**: dort zählt der Gesamtwert und die Haarkontur, nicht die
Züge. Die vier decken hell, mittelhell, dunkel und mittel ab, dazu vier
verschiedene Silhouetten — kurze Locken, schulterlang, Schiebermütze mit
Bart, kurz und glatt. Bei Tobias trägt die Mütze die Erkennbarkeit fast
allein; sie ist auf dieser Größe deutlicher als jedes Gesichtsmerkmal.
> **Was dabei offen bleibt, und zwar wissentlich.** Die Unsplash-Lizenz deckt
> das Urheberrecht des Fotografen — **kein Model Release**. Nachgelesen an der
> Quelle: Unsplash sagt zu erkennbaren Personen nichts, Pexels schreibt
> „Identifiable people may not appear in a bad light or in a way that is
> offensive", was eine Einschränkung ist und keine Erlaubnis. Ein fremdes
> Gesicht als „Sandra Koch, Projektleitung" auf dem Firmenkanal berührt §22
> KUG und die DSGVO, nicht das Urheberrecht. Ein Zwischenstand wich deshalb
> auf erzeugte Gesichter aus (StyleGAN2 über `thispersondoesnotexist.com`);
> das ist zugunsten echter Aufnahmen zurückgenommen worden. Wer es
> rechtssicher will, braucht **model-released Stock** (Adobe Stock, iStock,
> kostenpflichtig) — das ist der einzige Weg, der die Frage beseitigt statt
> sie zu tragen.
**Der Zuschnitt braucht keinen Füllton mehr.** Ein ordentliches
Business-Portrait hat Luft über dem Kopf; ein quadratischer Ausschnitt, auf
das Gesicht gesetzt, füllt den Kreis von selbst. Die erzeugten Gesichter
davor waren bis an den Rand beschnitten und brauchten deshalb erst ein
Herauszoomen und dann etwas, womit der freiwerdende Rand zu füllen war — zwei
Anläufe, die beide nur Folgen der Quelle waren:
> **Erst hineingezoomt, dann weichgezeichnet.** Der erste Anlauf schnitt
> 800 × 800 aus dem 1024er, also **enger** — die Köpfe waren an Haaransatz
> und Schläfen abgeschnitten, was bei 42 px nicht als Zuschnitt liest,
> sondern als Fehler. Der zweite zoomte richtig heraus und füllte den Rand
> mit einer weichgezeichneten Kopie des Bildes. Das sah aus, als sei das
> ganze Bild unscharf: ein Schleier, dessen Machart sich deutlich vom eigenen
> Bokeh der Aufnahme unterschied. Mit richtigen Vorlagen fällt beides weg.
**Geschärft wird beim Verkleinern, nicht danach am Ergebnis.** Skaliert mit
`lanczos` statt bikubisch, dazu ein leichtes `unsharp=5:5:0.7`. Jedes
Verkleinern kostet Kanten, und der Avatar misst am Ende 42 px — was dort an
Schärfe fehlt, holt keine Renderqualität zurück.
Abgelegt als **320 × 320 JPEG** bei Qualität 2. Der Avatar misst 42 px auf der
Slide, bei Renderqualität 3 also 126 — 320 lässt Luft, und JPEG ist für Fotos
das richtige Format; als PNG wäre es das Fünffache. Zusammen 100 KB.
**Im Menü hinter dem Plus fällt oben das Verb weg.** Im Werkzeug heißen die
vier oberen Einträge „Dokument senden", „Fotos und Videos senden", „Audio
senden" und „Kontakt teilen" — viermal ein Wort, das der Ort schon sagt: wer
das Menü über das Pluszeichen geöffnet hat, weiß, dass er etwas schickt. Hier
stehen nur die Dinge: **Dokumente, Medien, Audio, Kontakt.** Unten bleibt das
Verb, weil es dort etwas hinzufügt — „Termin" allein wäre ein Anhang, „Termin
erstellen" ist eine Handlung.
„Dokument" steht dabei in der Mehrzahl, weil man mehrere anhängen kann; Audio
und Kontakt bleiben einzeln, man schickt eine Aufnahme und einen Kontakt. Aus
„Fotos und Videos" wird **„Medien"** — dasselbe in einem Wort.
**Bei „Kontakt" steht jetzt das Teilen-Zeichen statt des Telefonhörers.** Ein
Hörer sagt „anrufen"; hier wird eine Karte weitergegeben. `ShareNetwork` aus
demselben Phosphor-bold-Satz wie alle anderen Zeichen, entnommen aus
`@phosphor-icons/react` im Website-Repo statt abgemalt. `phone` stand danach
nirgends mehr und ist aus der Tabelle heraus — sie zählt jetzt 19 Zeichen, und
jedes davon wird benutzt.
**Und die Eingabezeile ist zurück.** Sie stand einmal unter dem Menü und war
weggenommen worden mit der Begründung, die Slide zeige, was man anhängen kann,
und dafür brauche es das Feld nicht. Das stimmte nur so lange, wie man wusste,
worauf man schaut: ohne das Feld ist die Karte eine Liste im Nichts, und dass
sie aufklappt, wenn man im Chat auf das Pluszeichen tippt, stand nirgends.
Das Pluszeichen trägt jetzt die grüne Marke wie ein gedrücktes Element, und
der Zeiger steht darauf — derselbe, der im Bewegtbild die Nachricht anklickt.
Beides zusammen sagt den Zusammenhang, ohne dass ein Wort danebenstehen muss.
Das Menü öffnet nach oben, weil es das im Werkzeug auch tut.
Mit den kürzeren Beschriftungen kommt die Breite der Karte jetzt aus dem
längsten Eintrag statt aus einer festen Zahl. Vorher standen dort 470 Einheiten
— gemessen für Beschriftungen mit „senden" —, und die Karte lief mit einem
Drittel Leerraum quer über die Slide.
> **Ein hochkantes Bild richtet sich nach der Höhe, nicht nach der Breite.**
> Der Kasten misst 460 × 535. Bei der alten Breite von 940 px wurde er 1093 px
> hoch und lief in die Überschrift. Gerechnet wird die neue Breite außerdem am
> **sichtbaren Blatt** und nicht am Kasten: der ist um den Stapelrand größer
> als das, was man sieht, und mit seiner Oberkante gerechnet stand die
> Zeichnung noch einmal 60 px zu hoch und berührte den Fließtext. Bei 680 px
> bleiben rund 115 px Luft.
**Die Plakette für Ungelesenes trägt eine Zahl und ist grün.** In der Quelle
ist `unread` ein Wahrheitswert und der Punkt violett. Beides ist geändert:
violett ist im Produkt die Farbe der Projekte, das Kanalsymbol in derselben
Zeile trägt sie schon, und ein zweites violettes Zeichen daneben konkurriert
damit, statt etwas anderes zu sagen. Ein farbloser Punkt sagt außerdem nur
„da war was", eine Zahl sagt, wie viel — so macht es jeder Messenger, den ein
Betrieb kennt.
Die Zahlen (3, 2, 1) stehen **nicht** in der Quelle; sie ist an dieser Stelle
binär. Sie sind die einzigen erfundenen Werte im ganzen Post.
**Die Symbole kommen aus Phosphor, nicht aus Feather.** Der Feather-Satz
steckt in so ziemlich jeder Oberfläche der letzten Jahre; man erkennt ihn
sofort wieder, und damit sieht eine Zeichnung aus wie jede andere. Am
deutlichsten war das am Gruppensymbol — zwei Köpfe nebeneinander, das
Piktogramm schlechthin.
Phosphor ist dieselbe Familie von Strichsymbolen, nur mit eigener Handschrift:
runder, gleichmäßiger, andere Metaphern (`UsersThree` statt `Users`,
`PaperPlaneRight` statt `Send`, `CaretDown` statt `ChevronDown`). Es ist
dasselbe Paket, das die Website benutzt — `@phosphor-icons/react` steht neben
`lucide-react` in ihrer `package.json` — also keine dritte Handschrift im Haus.
Die Pfade sind **direkt aus dem installierten Paket gezogen**, nicht
abgeschrieben: Gewichtung `bold`, Zeichenfläche 256 × 256. Anders als bei
Feather sind es gefüllte Formen und keine Striche, `ikon()` färbt deshalb über
`fill` statt über `stroke`.
**Die Gewichtung ist gemessen, nicht geschätzt.** Kein Farbwert hat sich beim
Tausch geändert — trotzdem sah es anders aus, und das zu Recht: ein Symbol aus
dünneren Formen hat weniger voll gedeckte Pixel und mehr angeschnittene, es
liest sich blasser, obwohl derselbe Hexwert darinsteht. Im Kanalsymbol,
Ausschnitt 44 × 44:
| | volle Pixel | Deckung |
|---|---|---|
| Feather, wie es vorher war | 59 | 180 |
| Phosphor `regular` | 23 | 151 (16 % zu blass) |
| Phosphor `bold` | 62 | 192 (6 % darüber) |
Ein Zwischenstand hatte `regular`, weil es „im Menü leichter aussah". Geschätzt
statt gemessen, und daneben. Wer eine optische Stärke beurteilt, zählt Pixel.
Ein Zwischenstand ersetzte nur das eine Gruppensymbol durch zwei überlappende
Scheiben. Das war keine Antwort auf die Frage: die Frage war der Satz, nicht
das einzelne Zeichen.
**Slide 4 nennt drei der sechs Einträge, nicht alle.** „Versende mehr als nur
Textnachrichten: Dokumente, Fotos, Sprachnachrichten." Drei Beispiele sagen,
worum es geht; die vollständige Liste steht im Bild daneben.
Vorher stand dort die ganze Liste **mit Erklärung** („Dokument, Fotos,
Sprachnachricht, Kontakt. Soll daraus etwas werden: Termin oder Aufgabe."). Zu
spezifisch war nicht das Aufzählen, sondern das Durchdeklinieren. Auf dem Weg
zur jetzigen Fassung habe ich zweimal in die andere Richtung übersteuert und
gar nichts mehr genannt — ein Fließtext auf einer Feature-Slide soll das
Feature erklären, nicht umschreiben und auch nicht in vier Wörtern abhandeln.
**Slide 4 zeigt das Menü ohne das Eingabefeld, an dem es hängt.** Das Feld
stand einmal darunter, damit man sieht, woher das Menü kommt. Es ist weg: die
Slide zeigt, was man anhängen kann, und dafür braucht es das Feld nicht. Der
Stapel sagt ohnehin, dass hier ein Bildschirm liegt und kein Kasten im Nichts.
**Slide 1 zeigt drei von sechs Nachrichten, Slide 2 fünf von acht Kanälen.**
Was nicht ins Bild passt, fällt weg; ein Chat und eine Liste scrollen beide.
Slide 2 behält alle drei Sorten: Projektkanal, Direktnachricht, Gruppe.
**Die Bildergalerie ist eine Anhangkarte, kein Bild.** Im Werkzeug zeigt
`GalleryPreview` zwei Fotos, geladen von Unsplash. Fremde Fotos in einen
Beitrag zu setzen, der von der eigenen Baustelle spricht, wäre eine Behauptung
über Material, das wir nicht haben. Stattdessen steht dort die Karte
„2 Baustellenfotos · Galerie · heute" — der Text ist der der Quelle.
---
## 5. Stand der Dinge
| | |
|---|---|
| Slides | 5, dazu 1 Animation außerhalb des Streifens |
| Format | 1080 × 1350, Ausgabe zweifach: 2160 × 2700 |
| Luft zwischen Text und Zeichnung | 118 / 170 / 153 / 155 / 55 px, Animation 84 (Grenze 40) |
| Zeichnung ragt über die Seite | nein, alle innerhalb der 92-px-Schutzzone |
| Breiteste Überschriftzeile | 818 px von 896 |
| Konsole | keine Fehler |
| Überschneidungen | keine, `kollision.mjs` über alle Slides |
| Ausgabeweg | geprüft, `01-chat-hell.png` bis `05-mobil-hell.png` |
| Bewegtbild | `animation/post6-animChat.mp4`, Ränder 177/176/123, Luft 118, ffprobe siehe §7 |
Geprüft wird mit den Hilfsskripten aus dem Sitzungsordner: `pruef.mjs` misst
über `getBBox()` die tatsächlich gezeichnete Fläche je Slide und die Breite
jeder Überschriftzeile, zählt die Regeln im Stylesheet und liest die Konsole
mit; `schuss.mjs` rendert einzelne Slides; `export.mjs` fährt den echten
Ausgabeweg und fängt `/save` ab; `leiste.mjs` prüft in jedem Projekt, ob die
Animationsleiste an der richtigen Stelle steht und ob die Seite fehlerfrei
anläuft.
---
## 5a. Slide 5 kommt von der Website, nicht von mir
**Kein Gerät.** Vorbild ist die Machart, die Signal für genau solche Slides
benutzt: zwei Ausschnitte der Oberfläche, versetzt übereinander, jeder
mit eigenem Schatten. Man sieht zwei Zustände nebeneinander, ohne dass ein
Telefonrahmen die halbe Fläche frisst.
Ein Zwischenstand hatte hier ein nachgebautes Handy samt Insel und Tasten,
ganz verwischt. Das kostet viel Platz für etwas, das nichts über das Produkt
sagt, und der Rahmen war ohnehin frei erfunden. Die Unschärfe ist damit auch
weg: das Band sagt deutlich genug, dass es die App noch nicht gibt, und die
Ausschnitte sollen lesbar sein.
Links die Kanalliste von Slide 2, rechts der Verlauf von Slide 1, beide schmal
gesetzt und am Rand angeschnitten. Gezeigt wird also nichts, was nicht vorher
schon zu sehen war. Beide sind **leicht verwischt** (`stdDeviation 2.2`):
erkennbar, aber nicht zu lesen. Scharf bleibt allein das Band, und genau das
soll es auch sein.
**Das Absperrband kommt von der Website.** In
`components/sections/ChatDemoSection.tsx` steht unter der Fahne `comingSoon`
genau dieses Element: ein **Absperrband** aus dem Bau, gedreht um **-13°**, in
`--accent-yellow` `#F1B729` mit Kanten in `#171106`, darauf immer wieder
Schutzhelm und **BALD VERFÜGBAR**. Auf der Website ist es an den Rand des
Telefons beschnitten — „*clipped to the phone's rounded bounds so the ends cut
off like real tape*" steht als Kommentar daneben.
Ein Zwischenstand hatte hier einen selbst erfundenen gelben Aufkleber
„In Arbeit". Das war eine eigene Bildsprache neben einer vorhandenen.
Übernommen ist jetzt die der Website, samt Winkel, Farben und Wortlaut.
**Das Band läuft hinter die Ausschnitte.** Es liegt über beiden und
verschwindet an beiden Enden hinter ihnen: links hinter der linken Kante der
hinteren Platte, rechts hinter der rechten der vorderen. Beschnitten wird also
an der Silhouette **beider** Ausschnitte, und die ist die ganze Sache — der
Gegensatz „absperren oder beschneiden", den ein früherer Stand hier
behauptete, gibt es nicht.
Vorher lief das Band über die ganze Zeichnung hinaus und endete an der
Bildkante, quer zur eigenen Laufrichtung abgeschnitten. Genau das ist das
Zeichen für *ein Ende*: so liest sich ein Aufkleber, der zufällig dort aufhört,
wo das Bild aufhört. Ein Ende, das an einer **senkrechten Plattenkante**
abbricht, während das Band mit 13° ankommt, ist keines.
**Drei Dinge tragen das, und keines verbiegt die Form.**
*Die Schnittkante ist die Plattenkante.* Getroffen wird die Silhouette an genau
zwei Stellen, beide dort gerade — die Rundungen liegen weit weg — und beide
senkrecht.
*Sie ist weicher als eine gezogene Linie und härter als die Platten.* Mit deren
`stdDeviation 2.2` lief das Band über vier Einheiten ins Weiße aus und sah nach
Ausblenden aus statt nach Verdecken; ganz scharf stand eine gestochene Kante an
einer unscharfen Platte. `1.1` liegt dazwischen.
*Die letzten Einheiten verlieren Licht,* weil die Fläche sich wegdreht: von
`#F1B82A` auf `#815F11` über 56 Einheiten, mit dem Höhepunkt als Plateau dicht
vor der Kante. Der Ton ist ein warmes Dunkelbraun und kein Schwarz — ein Band,
das sich wegdreht, wird nicht farblos.
**Die Wölbung.** Quer über die Höhe der vorderen Fläche liegt ein Verlauf von
`#F8C64A` über `#F1B729` nach `#D89F1B`, knapp unter der Oberkante ein
Glanzlicht. Die untere Längskante ist einen Pixel dicker als die obere, weil
sie im Schatten der Wölbung liegt.
**Und es ist kein Rechteck mehr, sondern ein Spruchband, das sich um den
Stapel legt.** Das ist die größte Abweichung von der Website in diesem
Beitrag. Dort ist die Form ein durchgehendes Rechteck; hier knickt sie
**genau an der Plattenkante** um, läuft dahinter weiter und kommt an der
Kante wieder hervor — tiefer, weil sie dabei umgeschlagen ist, niedriger,
weil sie vom Betrachter wegführt, und flacher getönt, weil man von dort ihre
Unterseite sieht.
| | |
|---|---|
| `HERVOR` 78 | wie weit das Stück hinter der Platte hervorsteht |
| `VERSATZ` 40 | wie tief es dabei hängt |
| `SCHMAL` 0,72 | seine Höhe: verkürzt, weil es wegführt |
| `NEIG` 10 | zusätzlicher Fall nach außen |
| `#DFA81E` | sein Ton — eine Fläche, die wegführt, fängt weniger Licht |
**Die Stelle des Knicks wird nicht gerechnet.** Beide Stücke reichen
absichtlich weit in die Platte hinein, und **zwei Masken** schneiden sie
exakt an deren Kante ab: `bandSilhouette` lässt die vordere Fläche innerhalb
der Silhouette stehen, `bandAussen` die Stücke außerhalb. Der Knick sitzt
damit immer auf der Kante, auch wenn sich die Platten einmal verschieben.
Beide kommen aus **derselben Kontur, ohne Strich darauf** — so ergänzen sich
ihre Deckungen an jeder Stelle zu genau eins. Darüber liegt die **Falzkante**
als eigener Strich (`bandGesamt`, 4,5 breit, `#171106`): senkrecht auf der
Plattenkante, während das Band mit 13° ankommt, und über Fläche *und* Stück
hinweg, weil beide dort zusammenstoßen. Ohne sie brechen die dunklen
Längskanten an der Naht einfach ab, und der Falz sieht zerschnitten aus statt
geknickt.
Weggelassen sind die zwei Festlichkeiten des klassischen Spruchbands: **keine
Zacken** an den Enden und **kein Bogen** über die Länge. Ein gewölbtes Band
über einer flachen Oberfläche sieht nach Urkunde aus.
> **Drei Anläufe, und woran die ersten beiden lagen.** Erst waren die Enden
> zur Spitze verjüngt, dann war das Band um eine Knickstelle gekippt — das
> eine sah aus wie eine Schleife, das andere wie eine Welle. Beide verbogen
> das Band **als Ganzes** und ließen es nirgendwo hin.
>
> Der dritte Anlauf hatte den Knick schon richtig, saß aber **70 Einheiten
> vor** der Kante. Damit lag er mitten auf dem Bildschirm, und das Ganze sah
> aus wie ein Spruchband, das jemand daraufgeklebt und in der Mitte geknickt
> hat. Ein Band knickt nicht um nichts: der Knick gehört an die Kante, weil
> dort etwas ist, worum es sich legen kann.
Der Lichtabfall vor der Kante ist dabei auf die Hälfte zurückgenommen worden
(`0,80` → `0,42` an der Kante). Zusammen mit dem ohnehin dunkleren
umgeschlagenen Stück wurde daraus sonst ein brauner Schatten statt eines
Bandes. Er gilt **beiden**: nachgemessen liegt das Stück an der Naht bei
Helligkeit 125, das Band daneben bei 132 — das Stück ist also eine Spur
dunkler, und das ist die richtige Richtung, denn es liegt weiter hinten.
*Helm und Schrift stehen einmal, mittig auf der geraden Fläche.* Wiederholt und
am linken Rand beginnend passte der Satz zu einem endlosen Band. Er sitzt in
der Mitte des sichtbaren Laufs, weil dessen Mitte und die Drehmitte des Bandes
dieselbe Stelle sind: beide Kanten liegen gleich weit von ihr weg.
**Vom Absperrband abgesehen kommt der Schirm aus dem Pitch-Paket.** Eine
Zwischenfassung zeigte dort den Chat der Website (Heizungsprojekt Schulz,
`seedMessages`, Blasen in `#0D6E6E`). Der ist echt, gehört aber zu einer
anderen Oberfläche, und der Beitrag zeigt sonst durchgehend das Werkzeug.
Jetzt steht auf dem Schirm derselbe Verlauf wie auf Slide 1 und 3, gezeichnet
mit derselben `blase()`, nur schmaler. Man sieht es kaum, weil das Gerät
verwischt ist — das ist kein Grund, etwas anderes hinzulegen: was unter der
Unschärfe liegt, soll stimmen.
Überschrift und Fließtext sind ebenfalls die der Website: „Der Chat für
unterwegs wird gerade gebaut." und „Die mobile App legen wir gerade auf die
Baustelle. Am Desktop bist du schon voll dabei, die Version fürs Handy folgt
in Kürze." Der Em-Dash der Vorlage ist durch ein Komma ersetzt, `rules.md` §4.
---
## 5d. Slide 2 stellt gegenüber statt zu zeigen
Links das Durcheinander als Zeichnung, rechts die Oberfläche als Ausschnitt.
Die Aussage steckt in der Gegenüberstellung und nicht in einem der beiden
Teile — der Satz der Website ist genau diese Zweiteilung:
„Schluss mit dem einen Verlauf, in dem alles durcheinandergeht. Ein Kanal pro
Projekt."
**Die linke Hälfte gibt es in diesem Repo schon**, in `post2.html` auf Slide 2
(`visProzesse`): beschriftete Plättchen, die eine geschlängelte gestrichelte
Linie verbindet. Dort ist sie im Zeichenblock-Stil mit schiefen Kanten, hier
glatt — das Theme ist ein anderes, und dieselbe Form zweimal in zwei
Anmutungen wäre schlechter als zwei Formen mit derselben Idee.
Die fünf Plättchen (Angebot, Baustellenfoto, Termin, Rückfrage, Materialliste)
sind **Redaktionstext und keine Produktstrings**. Sie beschreiben die
Wirklichkeit im Betrieb, nicht die Oberfläche; dieselbe Lage wie bei
„E-Mails" und „Zettel im Auto" in Post 2. Gewählt sind Wörter, die im Beitrag
ohnehin vorkommen.
**Die Zeichnung steht ganz auf ihrer eigenen Seite.** Ein Zwischenstand zog
sie über die Kante nach links, sodass zwei Plättchen auf Slide 1 lagen und die
Leitung hindurchlief. Das ist zurückgenommen, und mit ihm der negative
`visLeft` und die Sonderregel für die Slide-Kante. `visLeft` ist wieder 92 wie
überall.
**Die Leitung fängt am ersten Plättchen an.** Sie hatte einen Anlauf, der links
vor dem Nullpunkt begann — ein Rest aus der Fassung über zwei Slides. Ohne die
Nachbarslide kam er aus dem Nichts und versprach einen Zusammenhang, den es
nicht gibt. Jetzt beginnt sie bei „Angebot" und endet bei „Materialliste", also
an beiden Enden an etwas.
**Kein Pfeil zwischen den Hälften.** Er stand einmal dort und machte die Sache
zu ordentlich — eine Zeichnung, die Durcheinander zeigt, braucht keinen
Wegweiser, und die Leitung führt ohnehin von oben nach unten. Die Plättchen
sind aus demselben Grund gestreut und leicht gekippt statt in einer geraden Spalte
gesetzt. Die Werte dafür stehen fest im Code und sind nicht gewürfelt: ein
Zufallsgenerator gäbe bei jedem Neubau ein anderes Bild, und die Slide soll
zweimal gleich aussehen.
**Der Ausschnitt steht größer da als die Zeichnung daneben** (`LISTE_SKALA`,
1,28). Er trägt das Produkt, die Plättchen tragen nur die Ausgangslage; gleich
groß gesetzt lasen sich beide als gleichwertig, und das sind sie nicht. Platz
dafür war da: gemessen endet die linke Hälfte samt Leitung bei x = 344, der
Kasten begann erst bei 464.
**Die Projektnummer misst dadurch wieder 15,8 px** statt 12,4 in der halben
Breite. Sie ist der Beleg für den Fließtext („Jeder Kanal hängt an seiner
Projektnummer") und muss lesbar bleiben — wer sie verkleinert, nimmt der Slide
ihre eigene Aussage.
---
## 5c. Slide 3 kommt ohne Hervorhebung aus
Um die Anhangkarte der Aufgabe lag einmal ein gelber Rahmen, geborgt aus den
Slack-Beiträgen: rund, über die Blase hinausragend, auf genau eine Sache
zeigend. Er ist weg.
Bei sechs Pixeln war er plump — er trug mehr Farbe als die Karte, auf die er
zeigen sollte. Der Versuch, ihn zu retten, ging über vier Varianten (ganz weg,
weicher Schein, gelbe Kante links, dünner Ring), jeweils gerendert und auf
Feed-Breite heruntergerechnet; gewählt war am Ende derselbe Ring mit drei
statt sechs Pixeln. Im Beitrag sah das aus wie vorher.
**Die Slide braucht ihn nicht.** Sie besteht aus drei Nachrichten, die
aufeinander folgen, und die Aufgabenkarte ist das einzige Element mit einer
gelben Marke („Offen") — der Blick geht ohnehin dorthin. Ein Rahmen sagt
nochmal, was die Anordnung schon sagt, und kostet dafür die Ruhe der Fläche.
Mit ihm ist `letzterAnhang` weggefallen: `blase()` merkte sich die Lage der
zuletzt gezeichneten Anhangkarte, damit der Rahmen nicht aus einer zweiten
Rechnung kommt. Ohne Rahmen liest das niemand mehr.
---
## 5b. Woher die Tiefe kommt
Zwei Anläufe waren falsch, bevor es saß.
**Erster Versuch: graue Flächen hinter die Elemente.** Das nimmt den
Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den Punktgrund,
der dort nicht hingehört. Wieder raus.
**Zweiter Versuch: nur bessere Schatten.** Richtig, aber zu wenig. Die
Schatten des Produkts sind in `ui/theme.css` **gestaffelt** und nicht
einlagig, und das war tatsächlich ein Fehler im ersten Nachbau:
```
--shadow-sm 0 1px 2px rgb(0 0 0 / .08)
--shadow-md 0 4px 16px -2px + 0 2px 4px -1px
--shadow-lg 0 12px 32px -4px + 0 4px 8px -2px
```
Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante setzt
das Element auf die Fläche, die weite helle hebt es davon ab. In SVG zwei
`feDropShadow` im selben Filter. Vorher stand dort einer mit `dy 7` und
`stdDeviation 10`.
**Was es gebracht hat: der Stapel.** `post1.html` hat ihn als Bildrahmen
`stapel` im CSS, für Screenshots. Hier ist er in SVG nachgebaut, mit genau
denselben Werten, damit die Reihe zusammenpasst:
```
.sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975)
.sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95)
```
Er trägt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein nicht schafft,
und er sagt, dass hinter der einen Ansicht weitere liegen. Die hinteren
Blätter sind deshalb **leer** — Andeutung, keine Kopie.
Auf Slide 1, 2 und 4 liegt er, weil dort ein Bildschirm oder ein Menü zu sehen
ist. Auf Slide 3 nicht: ein Ausschnitt aus einer Unterhaltung ist kein Blatt,
dort tragen die Blasen ihre eigene Erhebung.
Dazu zwei Dinge aus der Quelle, die der Unterhaltung Struktur geben, ohne eine
Fläche zu behaupten: die Datumsmarke **Heute · 30.07.2026** über dem ersten
Beitrag und die Zeile **„Maike Berger schreibt gerade …"** über dem
Eingabefeld. Beides steht so in `GeneralTeamChatPage.tsx`.
Nebenbei richtiggestellt: **der offene Kanal ist grau hinterlegt, nicht
violett.** Im Original steht dort `bg-surface-neutral-primary` mit Rand und
`shadow-sm`.
---
## 6. Vierundzwanzig Fallen aus diesem Bau
**Die Listenzeile hat drei Zeilen, nicht zwei.** Der erste Anlauf setzte Titel
und Vorschau untereinander und schob Nadel und Ungelesen-Punkt rechts neben
die Vorschau. Dort stießen sie an den Text — und die Zuordnung fehlte ganz.
Im Original steht sie als dritte Zeile: `P-2026-0042` links, die Nadel rechts.
Das war nicht nur ein Layoutfehler: der Fließtext behauptete „Jeder Kanal hängt
an seiner Projektnummer", und die Nummer war nirgends zu sehen.
**Eine Anhangkarte auf eine Zeile zu zwingen kürzt Zitate.** In der schmalen
Spalte auf Slide 5 stand „Materialliste prüfen · fälli…". Der Titel darf jetzt
auf zwei Zeilen, und die Karte wächst mit; die Blasenhöhe rechnet das mit.
Ebenso bricht die Systemzeile um, statt aus der Fläche zu laufen.
**Eine feste Obergrenze für die Blasenbreite hat ein Zitat gekürzt.** Für die
Titelslide musste sie klein sein, damit die Unterhaltung neben die Liste passt.
Auf Slide 3 stand deshalb „Materialliste prüfen · fälli…" statt
„… · fällig 31.07.2026". Die Grenze hängt jetzt an der Spalte, in der die Blase
steht.
**Beinahe hätte ich ein Zitat ohne Grund gekürzt.** Die Website-Überschrift
lautet „Büro und Baustelle, endlich im selben Chat." Ich hatte das „endlich"
gestrichen mit der Begründung, die Zeile laufe sonst über die Schutzzone, und
diese Begründung sogar mit einer Zahl versehen, die ich nicht gemessen hatte.
Nachgemessen sind es 818 px von 896 erlaubten: sie passt. Das Wort ist wieder
drin. Dafür gibt es jetzt `breite.mjs`, das eine Formulierung misst, bevor sie
in die Datei kommt.
**Eine Verschiebung mitten in der Zeichenfunktion erfasst nur die Hälfte.**
Die Zeile, die den Inhalt um `PAD` einrückt, stand mitten in `visChat()`.
Alles, was danach gezeichnet wurde — Datumsmarke, Blasen, Tippzeile,
Eingabefeld — lag um 26 Einheiten daneben, und die Kürzel schnitten die
Trennlinie zwischen den Spalten. Auf der ganzen Slide sieht man das nicht, im
Ausschnitt sofort. Die Verschiebung steht jetzt unmittelbar vor der Rückgabe.
Dafür gibt es `kollision.mjs`: es nimmt jedes gezeichnete Element, rechnet
seinen Kasten in Nutzereinheiten und meldet, was über den viewBox hinausragt
oder die senkrechte Trennlinie schneidet. Die Stapelblätter sind ausgenommen,
sie dürfen alles schneiden.
**Ein Prüfskript, das in den falschen Einheiten rechnet, ist schlimmer als
keines.** `kollision.mjs` benutzte zuerst `getCTM()`. Das enthält die
Skalierung der viewBox auf die Anzeigebreite — die Maße kamen also in
Bildschirmpixeln heraus und wurden gegen die viewBox in Nutzereinheiten
geprüft. Auf Slide 1 fiel es nicht auf, weil der Faktor dort zufällig bei 0,93
lag; auf Slide 2 meldete es 26 Fehler, die keine waren. Gerechnet wird jetzt
über `svg.getScreenCTM().inverse().multiply(el.getScreenCTM())`.
**Gedrehte Blätter ragen aus dem Kasten.** Der Stapel dreht das hinterste
Blatt um 3,2° und verschiebt es um 3 % und 5,5 % der eigenen Größe. Die Ecken
stehen damit über das vordere Blatt hinaus, und die Zeichnung lag bis zu 24 px
außerhalb der 92-px-Schutzzone. Der Überstand wird jetzt gerechnet statt
geschätzt: `stapelRand()` schickt die vier Ecken durch dieselbe Kette wie das
Blatt und nimmt den größten Wert. `stapelKasten()` macht den Kasten
entsprechend größer.
**Ein Maß, das aus einer Konstante kommt statt aus dem Gezeichneten, veraltet
still.** Der Pfeil auf Slide 2 rechnete seine Lage aus einer Blockbreite. Als
die Plättchen weiter nach rechts rückten, stimmte sie nicht mehr, und der Pfeil
zerfiel in zwei Striche. Er leitet seine Lage jetzt aus den Plättchen selbst
ab, so wie der Rahmen auf Slide 3 seine aus `blase()` bekommt.
**Eine Sicherung, die zu spät greift, sichert den Fehler mit.** `export.mjs`
schaltete die Slides mit der Pfeiltaste weiter und schrieb dabei im
Bearbeiten-Modus `vis`-Versätze in `state/post6.json`. Die Gegenmaßnahme war,
den Stand vor dem Lauf zu lesen und danach zurückzuschreiben — nur stand die
Drift beim nächsten Lauf schon in der Datei und wurde mitgesichert. Jetzt
werden die Slides über `#slides-nav` angeklickt statt durchgetastet; ohne
Tasten gibt es das Problem gar nicht erst.
**Ein gedrehtes Element wandert seitlich aus, wenn der Drehpunkt woanders
liegt.** Das Band sitzt 24 Einheiten unter der Mitte der Zeichnung, gedreht
wurde aber um die Mitte. Dadurch verschiebt sich sein Kasten um
24·sin(13°) ≈ 5 Einheiten zur Seite und stand über dem Rand. Gedreht wird
jetzt um die eigene Mitte des Bandes.
**`getBBox()` kennt keine Beschneidung.** Das Absperrband war zuerst auf die
175 % der Website gezogen und per `clip-path` am Gerät abgeschnitten. Im Bild
stimmte das, die Randmessung meldete aber einen Überstand von 146 px, den es
gar nicht gab: `getBBox()` liefert die Geometrie, nicht das Sichtbare. Das
Band ist jetzt nur so lang, wie es bei -13° quer über das Gerät sein muss,
also `B / cos(13°)` plus Zugabe. Messung und Bild sagen wieder dasselbe.
**Das Prüfskript nahm eine leere Slide auf.** `schuss.mjs` stellte für eine
einzelne Slide ein Fenster von 1400 px. Im durchgehenden Blatt liegt Seite 3
aber bei x = 2160, und `captureBeyondViewport` fing das nicht zuverlässig ab.
Die Bühne steht jetzt immer auf 7000 px.
**Eine ungültige `var()` fällt lautlos durch.** Die Animationsleiste war in
Farben gesetzt, die es in diesen Dateien nie gab — `var(--line)`,
`var(--panel)`, `var(--ink)`. Eine Variable ohne Wert und ohne Ersatzwert macht
nicht etwa die Farbe kaputt, sondern lässt **die ganze Angabe** fallen: kein
Rahmen, kein Grund, keine Meldung in der Konsole, kein Fehler in irgendeinem
Prüfskript. Aufgefallen ist es erst auf dem Bildschirmfoto. Die Leiste trägt
jetzt dieselben festen Werte wie der Rest der Oberfläche (`#17181F`, `#24262F`,
`#1D1F26`, `#2E313B`).
**Ein Geschwister hinter einem absolut liegenden Element landet nicht
darunter.** Die Leiste wurde als nächstes Element nach `#scroller` eingehängt
und lag prompt oben über dem Streifen: `.scroller` ist `position: absolute;
inset: 0` und nimmt keinen Platz im Fluss ein. Die Bühne ist deshalb jetzt eine
Spalte aus zwei Zeilen — oben eine neu eingezogene `#stageflaeche` mit Scroller
und Fußleiste, darunter die Leiste. Eingezogen wird sie beim Start per Skript,
damit der HTML-Rumpf jeder Datei unverändert bleibt.
**Eine Animation deckt auf, was ein Standbild verdeckt.** Auf Slide 3 steht
die Nachricht „Danke. Ich habe dazu direkt eine Aufgabe erstellt." mit
angehängter Aufgabenkarte, und das liest sich einwandfrei: ein Zustand, eine
Momentaufnahme. Sobald derselbe Inhalt einen Ablauf bekommt, fällt auf, dass
Text und Anhang einander bedingen — die Nachricht meldete eine Aufgabe, die im
selben Zug erst entstand. Der Fehler lag nicht in der Quelle, sondern darin,
eine Momentaufnahme für eine Reihenfolge zu halten. Mehr dazu in §7.
**Das Pitch-Paket ist ein Stand, nicht das Produkt.** Der zweite Anlauf für die
Animation nahm das Menü hinter dem Pluszeichen, weil `GeneralTeamChatPage.tsx`
den einzigen dort ausgezeichneten Weg zu einer Aufgabe so zeigt. Belegt war das
sauber — Datei, Zeile, Grep über beide Repos und über die Staging-API, die eine
Aufgabe nur mit `contact | company | lead | project` verknüpft und eine
Nachricht als Bezug gar nicht kennt. Trotzdem falsch: im laufenden Team-Chat
hängt an jeder Nachricht ein Menü mit „Aufgabe erstellen".
Die Lehre ist nicht „schlechter prüfen", sondern **wissen, was eine Quelle
belegt.** Ein Pitch-Paket vom Juli belegt, was im Juli gebaut war. Es belegt
nicht, was das Produkt heute kann. Wo eine Aussage der Website (hier:
„Aus „mach das mal" wird mit einem Klick eine echte Aufgabe") keine Entsprechung
im vorliegenden Code hat, ist das ein Grund nachzufragen und nicht, die Aussage
auf den vorhandenen Code zu biegen.
**Ich habe die falsche Slide geprüft.** Nach dem Vergrößern des Ausschnitts auf
Slide 2 lief `kollision.mjs` mit dem Argument `1` — das ist Slide 1. Auf Slide 2
ragte das hinterste, gedrehte Blatt des Stapels fünf Einheiten unter den Kasten:
`stapelRand()` rechnet den Überstand für die unskalierte Größe, und die
Skalierung vergrößert ihn mit. Der Kasten wird jetzt aus dem skalierten Stapel
gerechnet. Gefunden wurde es erst zwei Commits später, bei einer Prüfung, die
alle fünf Slides durchging.
**Ein `|| 0` an der falschen Stelle verdeckt ein NaN.** Der Abspielen-Knopf tat
nichts, sobald man ihn nach dem Laden als Erstes anfasste. `animAbspielen`
rechnete `performance.now() - ANIM_T[i] * dauer`, damit die Vorschau dort
weiterläuft, wo der Regler steht — nur ist `ANIM_T[i]` undefined, solange der
Regler nie bewegt wurde. Daraus wird NaN, der Fortschritt wird NaN, und
`animT()` liefert wegen seines eigenen `|| 0` brav wieder 0: die Vorschau stand
still, ohne dass in der Konsole etwas auftauchte.
Der Fehler war vier Commits alt und wurde nicht bemerkt, weil jede Prüfung
vorher `animSetzen()` aufgerufen hatte, um irgendetwas zu messen — und damit
genau den Zustand herstellte, in dem es funktioniert. **Ein Prüfskript, das den
Anfangszustand anfasst, prüft ihn nicht mehr.** `animSetzen` weist NaN jetzt
zusätzlich mit `Number.isFinite` ab, damit ein solcher Wert gar nicht erst in
den Stand kommt.
**Ein weicher Schnitt und ein Schatten an derselben Stelle heben sich auf.**
Die Falte des Absperrbands hatte ihren dunkelsten Punkt genau auf der
Plattenkante. Dort blendet die Maske das Band aber schon aus — das Dunkelste
lag also im Durchsichtigen und mischte sich mit dem Weiß der Platte zu einem
blassen Streifen, der die Falte gerade wieder aufhob. Der Höhepunkt sitzt jetzt
als Plateau ein Stück davor, wo das Band noch voll deckt.
**Ein Verlauf gehört in die Koordinaten dessen, woran er endet.** Das Abdunkeln
der Bandenden stand zuerst im gedrehten Bandkasten und lief damit 13° schräg
zur senkrechten Plattenkante, an der es aufhören soll. Über die Bandhöhe
verschob sich der Höhepunkt dadurch um 21 Einheiten. Es steht jetzt in den
Koordinaten der Zeichnung, waagerecht, parallel zur Kante.
**Mehr als die Hälfte der „Fotos" von einer Bildersuche waren Profilbilder
der Fotografen.** Beim Einsammeln der Unsplash-Treffer greift ein
`figure img[src*='images.unsplash.com']` auch das Avatarbild des Urhebers ab,
das mit im `<figure>` steht. Von 56 Kandidaten waren **32** solche
Profilbilder — und sie sehen aus wie Portraits, weil sie welche sind. Zwei
davon waren schon eingesetzt und gerendert, bevor es beim Aufschreiben der
Herkunft auffiel: ihre URL beginnt mit `profile-` statt mit `photo-`. Ein
Profilbild ist nicht von der Unsplash-Lizenz gedeckt. Gefiltert wird jetzt
auf `/photo-` in der Bild-URL.
**Zwei gegenläufige Teilstücke in einem Pfad löschen ihren Überlapp weg.**
Der Beschnitt für die Falzkante war ein einziger `<path>` aus Bandrechteck und
umgeschlagenem Stück. Das Rechteck läuft im Uhrzeigersinn, das Stück dagegen —
und die Füllregel `nonzero` macht daraus ein **Loch**, genau dort, wo beide
sich überlappen. Und genau dort sollte die Falzkante hin. Sie fehlte, ohne dass
irgendetwas einen Fehler meldete. Die Kinder einer `clipPath` werden dagegen
vereinigt: jetzt stehen dort zwei `<path>`-Elemente statt eines.
**Ein Rechteck in Zeichnungsgröße trifft nicht, was über die Zeichnung
hinaussteht.** Die Abdunklung der umgeschlagenen Stücke lag auf einem Rechteck
`0 0 W H`. Die Stücke stehen aber außerhalb von `0..W` — sie ragen ja über die
Plattenkante hinaus. Das Rechteck traf sie nie, und das Stück blieb heller als
das Band daneben: verkehrt herum für etwas, das weiter hinten liegt. Aufgefallen
ist es erst beim Auszählen der Pixel quer über die Naht, nicht am Bild.
**Ein Kommentar, der einen zurückgenommenen Stand beschreibt, ist schlimmer als
keiner.** Über dem Band standen zwei Kommentarblöcke übereinander: der eine
sagte, es rage beidseitig hinaus, der andere, es knicke an den Enden weg. Das
erste stimmte noch, das zweite war einen Commit vorher zurückgenommen worden.
Wer danach etwas ändert, glaubt dem Kommentar. Mit ihnen weg sind auch
`WEB.bandKnick` und `WEB.bandSpitze`, die Farben des Knicks, die niemand mehr
las.
---
## 7. Das Bewegtbild
Sechs Sekunden, 180 Bilder, `animation/post6-animChat.mp4`. Gezeichnet wird es
von `animChat(i, t)` in `post6.html`, mit denselben Bausteinen wie jede Slide —
nur bekommt die Funktion neben der Nummer den Fortschritt `t` und leitet daraus
alles ab. Der Eintrag steht in `ANIMS`, nicht in `SLIDES`.
**Gezeigt wird der Weg, den das laufende Werkzeug hat:** Nachricht anfassen,
Menü öffnet sich, „Aufgabe erstellen", die Aufgabe klappt in der Nachricht auf,
das Projekt bestätigt sie. Das ist die Funktion, die die Website meint mit
„Aus „mach das mal" wird mit einem Klick eine echte Aufgabe."
| Zeit | Was passiert |
|---|---|
| 0,0 – 0,2 s | Ruhe. Der Verlauf steht: Sandra, Maike, Tobias |
| 0,2 – 1,1 s | Der Zeiger fährt auf Tobias' Nachricht, der Pfeil erscheint an ihr |
| 1,3 – 1,6 s | Klick |
| 1,5 – 2,0 s | Das Menü schnappt nach oben auf: Reagieren, Antworten, Im Thread antworten, Anpinnen, Aufgabe erstellen, Bearbeiten, Löschen |
| 2,4 – 3,4 s | Der Zeiger geht auf „Aufgabe erstellen", die Zeile hebt sich, Klick |
| 3,6 – 4,8 s | Das Menü schließt, die Aufgabenkarte klappt in Tobias' Nachricht auf, der Verlauf rutscht nach |
| 4,8 – 5,4 s | Die Systemzeile „Aufgabe „Materialliste prüfen" im Projekt erstellt" erscheint |
| 5,4 – 6,0 s | Halt auf dem Ergebnis |
### Die Kurven sind die von GSAP, die Bibliothek ist es nicht
Die Website macht ihre Bewegungen mit **GSAP** (`gsap@3.14.2`, Standard-Lizenz;
`HeroSection`, `Navbar`, `HoneycombSection`, `FAQSection` und weitere). Die
Frage lag also nahe, es hier auch zu tun. Die Antwort ist: **die Kurven ja, die
Bibliothek nein.**
Was GSAP über eine Kurve hinaus kann — Zeitleisten, ScrollTrigger,
MotionPathPlugin —, setzt voraus, dass etwas *läuft*. Hier läuft nichts:
`animChat(i, t)` bekommt seinen Fortschritt von außen und zeichnet daraus den
Stand. Die Funktion **ist** schon die Zeitleiste, und `animation.mjs` sampelt
sie Bild für Bild. Ein `gsap.timeline({paused: true})` mit `.progress(t)` wäre
genau dieselbe Konstruktion, nur mit 70 KB darunter — und das Repo ist autark,
jede Datei in `vendor/` liegt dort, weil sie etwas kann, das man nicht in fünf
Zeilen schreibt (DOM nach Canvas, ZIP). Ein Easing ist eine Zeile.
Dazu kommt: **zwei der Kurven waren längst GSAPs.** `daempfen` ist Wort für
Wort `power2.inOut`, `rueck` ist `back.out(1.70158)`. Die Datei sprach die
Sprache schon, sie sagte es nur nicht. Jetzt stehen die Namen daneben, und der
Satz ist vollständig:
| in der Datei | GSAP | wofür |
|---|---|---|
| `daempfen` | `power2.inOut` | Wege mit Anfang und Ende: das Wandern zur Menüzeile, das Aufklappen der Karte |
| `rueck` | `back.out(1.7)` | das Menü, das aufschnappt und einmal leicht überschwingt |
| `raus` | `power2.out` | alles, was ankommt und liegen bleibt: der Zeiger, die Systemzeile |
| `knall` | `expo.out` | der Klickring |
**Der Zeiger fährt jetzt wie eine Hand und nicht wie ein Schlitten.** Er lag
auf `power2.inOut` — langsam an, in der Mitte schnell, wieder abbremsend. So
bewegt sich ein Schlitten auf einer Schiene. Eine Hand ist sofort schnell und
wird zum Ziel hin langsam, also `power2.out`.
**Und er fährt einen Bogen.** Auf der Geraden zwischen Start und Ziel liegt kein
Zeiger, den je ein Mensch geführt hat. Der Stützpunkt einer quadratischen
Bézierkurve sitzt quer zur Strecke, 13 % ihrer Länge nach außen — vom Verlauf
weg, damit der Zeiger nicht über die Blasen streicht. Das sind rund 27 px
Ausbuchtung auf 410 px Weg; es fällt nicht auf, und genau das ist der Punkt.
Bei `p = 1` liegt die Bahn exakt auf dem Ziel, das anschließende Wandern zur
Menüzeile setzt also sauber daran an.
**Der Klickring geht auf zwei Kurven.** Der Radius auf `expo.out`, also fast
sofort draußen, die Deckung linear hinterher. Beides auf derselben Kurve sah
aus wie eine Blase, die sich aufbläst — ein Klick ist ein Stoß und kein
Aufpumpen.
**Der Anlauf ist von 0,6 s auf 0,18 s geschrumpft.** Am Ende steht das Bild
eine halbe Sekunde still, damit man das Ergebnis liest. Lief vorne noch einmal
eine halbe Sekunde nichts, waren das über den Schleifenstoß hinweg 1,2 von
6 Sekunden Stillstand am Stück, und die Schleife sah aus, als hänge sie. Der
Halt am Ende bleibt, der am Anfang ist auf ein Achtel geschrumpft.
> **Nachgeprüft ist das an Einzelbildern, nicht am Lauf.** Kontaktabzüge über
> die 180 Bilder und eine Nahaufnahme des Klickrings zeigen, was die Kurven
> rechnen. Ob die Bewegung *läuft*, sieht man erst im Abspielen — dieser eine
> Schritt steht noch aus.
### Warum Tobias' Nachricht
Nicht irgendeine: die Aufgabenkarte aus der Quelle trägt **„Tobias Wagner ·
offen"** als Zuständigkeit. Wer sie an einer fremden Nachricht entstehen lässt,
muss den Namen erfinden. So bleiben alle vier Felder der Karte
(`square-check`, „Materialliste prüfen · fällig 31.07.2026", „Tobias Wagner ·
offen", „Offen") genau die aus `GeneralTeamChatPage.tsx`.
### Vier Entscheidungen
- **Der Verlauf hängt am unteren Rand, nicht am oberen.** Damit erzeugt jedes
Wachstum das Hochschieben von selbst, ohne eine zweite Rechnung für das
Scrollen. Wenn die Aufgabenkarte aufklappt, rutscht der Verlauf nach.
- **Die Karte wird aufgedeckt, nicht gestaucht.** Sie liegt in voller Größe da
und wird über eine `clipPath` von oben freigegeben. Eine gestauchte Karte
sieht kaputt aus, eine aufgedeckte sieht nach Aufklappen aus.
- **Der Mauszeiger ist erzählerisches Mittel, kein Bestandteil des Werkzeugs.**
Ohne ihn öffnet sich ein Menü und man weiß nicht, warum. Weiß mit dunkler
Kante, damit er auf Verlauf wie Menü liest, und nach dem zweiten Klick zügig
weg — sonst zieht er den Blick von der aufklappenden Aufgabe ab.
- **Am Ende wird nichts zurückgenommen.** Ein früherer Stand blendete die neue
Nachricht wieder aus, damit der Schleifenstoß unsichtbar wird. Das sah aus,
als lösche sich eine abgeschickte Nachricht von selbst. Eine Schleife darf
springen.
### Ohne Fließtext
Die Animation trägt als einzige keine Beschreibung. Die Überschrift sagt es,
die Bewegung zeigt es, und die Systemzeile im Bild bestätigt es — ein Satz
darunter wäre die dritte Stelle für dieselbe Aussage. Ein leerer `body` lässt
`contentHtml` den Absatz ganz weg, sonst schöbe sein Zeilenabstand die
Zeichnung nach unten, ohne dass etwas dasteht.
Die gut hundert Pixel, die dadurch frei werden, gehen an die Zeichnung:
`visW` von 700 auf 776, mittig gesetzt bei `visLeft: 152`. Luft zum Text 118,
also derselbe Wert wie auf Slide 1.
Die Überschrift „Chatnachrichten werden / **Aufgaben im Projekt.**" misst
879 px in der ersten und 733 in der zweiten Zeile, bei 896 erlaubten. Gemessen
mit `breite.mjs`, bevor sie in die Datei kam, und nach dem Einsetzen noch
einmal am gerenderten Blatt nachgezählt: zwei Zeilen, kein Umbruch.
**„Chatnachrichten" zusammengeschrieben, nicht mit Bindestrich.** Beides ist
korrekt, also entscheidet die Breite: „Chat-Nachrichten werden" misst 916 px
und läuft über die Schutzzone, zusammengeschrieben sind es 879. Verworfen
wurden außerdem „Aufgaben im richtigen Projekt." (1067 px) und „Aus
Chat-Nachrichten werden" (1063 px).
**Das Menü schnappt auf, es kriecht nicht.** Ein Zwischenstand nahm dafür
dieselbe gedämpfte Kurve wie alles andere, über 0,8 Sekunden. Die Deckung war
darin nach fünf Bildern fertig, während die Skalierung noch eine halbe Sekunde
von 0,96 auf 1,00 weiterlief. Zu sehen war davon fast nichts, zu spüren war es
als Hänger genau in dem Moment, in dem geklickt wird. Jetzt öffnet es in einer
Drittelsekunde mit `rueck`, schwingt einmal ganz leicht über und steht: drei
Bilder Aufblenden, dann Ruhe.
### Zur Länge
Instagram nimmt je Carousel-Element **3 bis 60 Sekunden** an. Sechs liegt weit
von beiden Grenzen weg; die Länge richtet sich also allein nach dem Ablauf.
Vier Schritte — anfassen, wählen, aufklappen, bestätigen — brauchen etwa so
lange. Eine kurze Schleife wiederholt sich außerdem öfter, während jemand liest.
Eine Zwischenfassung lief acht Sekunden, weil dort zusätzlich eine Nachricht
getippt wurde; das fällt mit dem richtigen Weg weg.
Die Symbole im Menü sind unsere Strichsymbole. Im Werkzeug steht bei
„Reagieren" ein Emoji; ein Emoji zwischen sechs Strichsymbolen bricht die
Bildsprache, und die Slides tragen durchgehend Feather-Symbole.
Nachgeprüft mit `ffprobe`: H.264 High, `yuv420p`, 1080 × 1350, 30 fps, 180
Bilder, Dauer exakt 6,000000 s, stumme AAC-Spur, `moov` am Dateianfang, 1,1 MB.
+37
View File
@@ -0,0 +1,37 @@
# Post 7 · Form Builder
Erster Beitrag nach der Pause, **erschienen auf Instagram am Fr 23.10.2026**
(Redaktionsplan, Slot s02). Datei `post7.html`, Stand in `state/post7.json`.
**Facebook:** noch offen. Dejan stellt den Beitrag dort selbst ein (Stand
02.11.2026), mit demselben Text.
---
## Die Bildunterschrift
So am 22.10.2026 geliefert und für Instagram verwendet. Ob beim Posten noch
etwas geändert wurde, ist hier nicht vermerkt.
```
Eine Anfrage kommt per Mail. Keine Adresse, keine Fotos, keine Telefonnummer. Dann geht das Hinterhertelefonieren los.
Im Form Builder von HiveGrid baust du dein Anfrageformular selbst: Text, Auswahl, Datei, Datum und mehr. Du bestimmst, was gefragt wird und in welcher Reihenfolge.
Fragen, die nicht für jeden gelten, tauchen nur auf, wenn die Antwort davor sie verlangt. So füllt keiner Felder aus, die ihn nichts angehen.
Jede neue Antwort wird als Lead gespeichert, mit den Feldern, die du zuweist. Abgetippt wird nichts mehr.
Das Formular bindest du auf deiner Website ein oder teilst es per QR-Code.
Was fehlt bei deinen Anfragen am häufigsten?
#handwerk #shk #handwerksbetrieb #digitalisierungimhandwerk #kundenanfragen #onlineformular #formbuilder #hivegrid
```
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 118 Zeichen |
| 300 bis 800 Zeichen | 655 ohne Hashtags |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | Anfrage, Formular, Fotos, Lead, Website, QR-Code |
+45
View File
@@ -0,0 +1,45 @@
# Post 8 · E-Mail-Marketing
Datei `post8.html`, Stand in `state/post8.json`. Slides und Bildunterschrift
sind fertig. Geplant war Mo 26.10.2026 auf Instagram (Redaktionsplan, Slot
s14).
**Status: vorerst nicht gepostet** (Dejan, 02.11.2026). Der Text ist
freigegeben („der Text ist gut“) und wird so verwendet, wenn der Beitrag
kommt, auf Instagram wie auf Facebook.
---
## Die Bildunterschrift
```
Du hast keine Zeit, dich bei jedem Kunden zu melden. HiveGrid schon.
Die richtige E-Mail zur richtigen Zeit: Wartungserinnerung, Nachfassen, Angebot. Direkt aus deiner Kundenliste, ohne zweite Adressliste.
Nach Gewerk, Region oder Projektstand filtern, und die Mail landet bei den Richtigen. Nicht bei allen.
Vorlagen für Angebot, Wartungserinnerung und Newsletter sind dabei. Den Rest baust du aus Bausteinen: Überschrift, Text, Bild, Button.
HiveGrid hält jede Kampagne an, bis sie DSGVO-konform ist. Was das Gesetz verlangt, musst du nicht auswendig können.
Danach siehst du auf einen Blick, wer geöffnet und wer geklickt hat.
#handwerk #shk #handwerksbetrieb #emailmarketing #kundenbindung #wartung #digitalisierungimhandwerk #hivegrid
```
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 68 Zeichen |
| 300 bis 800 Zeichen | 634 ohne Hashtags, 745 mit |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | E-Mail, Wartungserinnerung, Angebot, Kundenliste, Newsletter, DSGVO |
**Woher die Sätze kommen:**
- Der Haken und „Wartungserinnerung, Nachfassen, Angebot … direkt aus deiner
Kundenliste“: Website, `lib/produkt-daten.ts`, Seite `email-marketing`
(Headline und Subtext).
- Absatz 3 bis 5 folgen den Slides Zielgruppe, Vorlagen, Baukasten,
Rechtssicher in `post8.html`.
- Der letzte Satz gehört zum Kampagnenbericht im Bewegtbild; auf der Website
„Wer hat geöffnet, wer geklickt, wer reagiert? Alles auf einen Blick.“
+86
View File
@@ -0,0 +1,86 @@
# Webinar-Post: Outline
**Stand:** 17.10.2026. Gebaut als `post9.html`, Termin und Uhrzeit sind noch Platzhalter.
## Gebaut (17.10.)
Aufbau nach der Ad-Recherche im Hub (`webinar/Ad-Recherche.md`): zuerst das Bewegtbild, dann eine Slide zum Anmelden. Als Anzeige laufen beide hintereinander, die zweite ist das Endbild.
| # | Element | Inhalt |
|---|---|---|
| 1 | **Bewegtbild** `animWebinar`, 8 s, `animation/post9-animWebinar.mp4` | Tag „Live-Webinar“, Überschrift „Automatisierung im Handwerk.“ Darunter ein Webinar-Fenster: Live-Marke, Termin oben rechts, die vier Teile von „Was heute kommt“ laufen als Folien durch. Unten Noah und David. Wer gerade spricht, hat einen dunklen Rand, wie im Skript: Noah eröffnet jedes Kapitel, dann David. |
| 2 | **Anmeldung** | Tag „Jetzt anmelden“, Überschrift „Live zuschauen, live fragen.“, Untertitel des Webinars als Text. Karte mit Titel, Datum, Uhrzeit, Ort, beiden Sprechern, Knopf „Jetzt anmelden“ und „Link in der Bio“. |
- **Platzhalter** stehen an einer Stelle, in `WEBINAR` in `post9.html`: Datum, Wochentag, Uhrzeit, Dauer.
- **Nicht im Bild,** weil offen: „kostenlos“ und „mit Aufzeichnung“.
- **David Bertram:** Nachname aus `app/ueber-uns/team.ts` der Website. Noch bestätigen, dass er spricht.
- **Fotos:** `assets/eigene/portrait-ng.jpg` und `portrait-db.jpg`, ausgeschnitten aus den Teamkarten der Website.
- **Für die Anzeige** muss „Link in der Bio“ raus, die Anzeige hat ihren eigenen Knopf. Außerdem fehlen 9:16 und das zusammengesetzte Video (Bewegtbild plus Endbild).
- **Später:** Noah spricht rund 30 s in die Kamera (Variante 4 der Ad-Recherche).
## Ursprüngliche Outline (12.10.)
## Worum es geht
- HiveGrid hostet ein Webinar auf WebinarGeek. Thema, Termin und Inhalt stehen
noch nicht fest.
- Dejan schreibt zuerst Slides und Skript des Webinars. Danach bauen wir
diesen Post.
- Der Post lädt zur Anmeldung ein. Er ist eine Ankündigung, wie F15 in der
Formatbibliothek des Hubs.
- Er entsteht im neuen Look aus `hivegrid-cloud` (rules.md §1).
## Leitlinien
- **Der Titel nennt ein Problem aus dem Betrieb, keine Funktion.** So steht es
im Webinar-Konzept (`HiveGrid-Website/webinar.md` §1.2).
- **Nur Fakten, die feststehen.** „Kostenlos“ oder „mit Aufzeichnung“ nur, wenn
es stimmt. Kein Countdown, keine erfundenen Zahlen.
- **Ein Ziel: die Anmeldung.** Auf Instagram über den Link in der Bio, auf
LinkedIn direkt im Text.
- **Format und Aufbau** wie in rules.md §5: 1080 × 1350, Tag, Überschrift,
Beschreibung, Bild.
## Slide 1: die Einladung
Sie muss allein tragen. Wer nicht weiterwischt, weiß trotzdem, was es ist,
wann es ist und wo man sich anmeldet.
| Baustein | Inhalt |
|---|---|
| Tag | WEBINAR |
| Überschrift | der Titel des Webinars, ein Wort markiert |
| Beschreibung | ein Satz: für wen und worum |
| Bild | der Termin als Karte im Look der App. Datum groß, Uhrzeit, Dauer, „Online“. Unten ein schwarzer Knopf „Anmelden“ mit dem Hinweis auf den Link in der Bio. Gebaut aus Card, Badge und Button von `hivegrid-cloud`. |
## Slide 2: was du mitnimmst
| Baustein | Inhalt |
|---|---|
| Tag | DAS ERWARTET DICH |
| Überschrift | der Nutzen in einem Satz |
| Bild | drei Punkte aus dem Ablauf untereinander, als StepperVertical oder Liste in einer Card. Darunter, wer spricht: Foto, Name, Rolle (Avatar). Am Ende derselbe Knopf wie auf Slide 1. |
## Als Einzelbild
Reicht eine Slide, bekommt Slide 1 drei Zeilen Ablauf in die Karte. Der Rest
kommt in die Bildunterschrift.
## Kanäle und Zeitpunkt
- Instagram und Facebook als Carousel oder Bild.
- LinkedIn als L4 Anlass, mit dem Anmeldelink im Text.
- Erscheinen kann der Post, sobald der Termin steht. Das Webinar-Konzept rät,
rund drei Wochen vorher zu werben. Die Hälfte der Anmeldungen kommt dort in
der letzten Woche (§1.4).
## Was noch fehlt
Kommt aus Dejans Slides und Skript:
- Titel und das Problem dahinter
- Datum, Uhrzeit, Dauer
- wer spricht
- drei Punkte aus dem Ablauf
- der Anmeldelink bei WebinarGeek
- ob es kostenlos ist und ob es eine Aufzeichnung gibt
+431 -4
View File
@@ -18,9 +18,309 @@ Downloads landen direkt in `~/Downloads`, ohne Browser-Dialog.
## Projekte
| Projekt | Datei | Inhalt |
| Projekt | Datei | Inhalt | Übergabe |
|---|---|---|---|
| Post 1 · Lösung | `post1.html` | Carousel, Anzahl frei | |
| Post 2 · Die Geschichte | `post2.html` | Carousel, 6 Slides, erscheint 12.08. | [`POST-2.md`](./POST-2.md) |
| Post 2 · Alternative | `post2-alt.html` | Derselbe Beitrag im Theme `modern` | |
| Post 3 · Verbinden statt ersetzen | `post3.html` | Einzelbild, Theme `modern`, erscheint 17.08. | |
| Post 4 · Workflow-Engine | `post4.html` | Carousel, 4 Slides, Theme `modern`, erscheint 19.08. | [`POST-4.md`](./POST-4.md) |
| Post 5 · Projektmanagement | `post5.html` | Carousel, 5 Slides, Theme `modern`, erscheint 21.08. | [`POST-5.md`](./POST-5.md) |
| Post 6 · Team-Chat | `post6.html` | Carousel, 5 Slides, Theme `modern`, erscheint 26.08. | [`POST-6.md`](./POST-6.md) |
**Alle Streifen nebeneinander** zeigt `uebersicht.html`, erreichbar über
*Projekt → Alle Projekte im Streifen*. Die Seite baut die Slides nicht selbst
nach: jedes Projekt kommt als `iframe` mit `?nur=streifen`, und in diesem Modus
blendet die Post-Seite ihre Bedienoberfläche aus, zeigt den Streifen in
natürlicher Größe und **speichert nichts** — drei Kacheln, die beim Schließen
ihren Stand zurückschreiben, wären genau die Falle aus [`POST-2.md`](./POST-2.md) §6.
Jeder Streifen ist auf die Fensterbreite eingepasst, gescrollt wird nur
hoch und runter — kein Zoom, weder außen noch im Streifen.
**Post 2 gibt es zweimal, in zwei Themes.** Projekt und Theme sind zwei
verschiedene Dinge: das Projekt sagt, welcher Beitrag, das Theme, wie er
aussieht. `notebook` ist die Zeichenblock-Optik mit Raster und Marker,
`modern` der ruhige Grund mit farbigem Akzentwort und schwebenden Karten.
Beide zeigen denselben Beitrag mit denselben Texten.
Die Themes stehen in [`rules.md`](./rules.md) §1a mit ihren Unterschieden.
Wer eine Visualisierung ändert, muss an beide Dateien denken.
**Post 3 ist ein Einzelbild**, keine Slidefolge: eine Haltung, die man
durchwischen muss, ist keine. Sein Bild ist das **Wabenfeld der Startseite**,
nachgebaut aus `HoneycombSection.tsx` — dieselbe Achsrechnung, dieselben
Zellnummern, dieselbe Belegung, damit es wiedererkennbar bleibt und keine
Anmutung davon ist. Die vier Waben „In Arbeit" fehlen: auf der Startseite sind
sie ein Versprechen, im Beitrag wären sie ein Eingeständnis.
**Post 4 zeigt einen einzigen echten Ablauf**, nicht sechs Behauptungen über
Automatisierung: den Vorgang aus `assets/screens/workflow.png`. Die Bausteine
sind nachgebaut aus `NodeWorkflowEngine` und `WorkflowEngineClickDummy` des
Pitch-Pakets, in Markenfarben statt in den UI-Farben des Produkts — Teal trägt
die Arbeit, Gelb den einen Akzent, Anthrazit die Entscheidung.
**Vier Slides.** Der Redaktionsplan nennt fünf bis sieben, aber das ist eine
Spanne und kein Soll: eine Slide, die nichts hinzufügt, kostet einen Wisch und
gibt nichts zurück.
| Slide | Was sie leistet | Ansicht | Quelle der Texte |
|---|---|---|---|
| 1 | was es ist | ein Ablauf auf dem Canvas | Knoten aus `assets/screens/workflow.png`, Notiz aus `WORKFLOW_PRESETS`, Anordnung eigen |
| 2 | was ihn startet | Auslöser-Auswahl | `assets/screens/workflow-bausteine.png` |
| 3 | was dann passiert | Aktionen-Auswahl | `NodeWorkflowEngine`, Rubriken des `Katalog` |
| 4 | dass man nicht bei null anfängt | drei Vorlagen | `GENERAL_WORKFLOW_PRESETS` |
Slide 2 und 3 sind **eine Funktion mit zwei Rubriken**, weil es im Produkt eine
Auswahl ist: „Auslöser" und „Aktionen" stehen dort untereinander im selben
Feld. Getrennt gezeichnet wären es zwei Bilder, die zufällig gleich aussehen.
Die Kachelfarbe kommt dabei aus der **Kategorie**, nicht aus dem Knotentyp —
im Screenshot tragen alle Auslöser eine gelbe Kachel und die Aktion eine
blaue, samt Punkt vor der Rubrik. Auf dem Canvas ist es anders, dort färbt die
Wurzelkomponente je Art. Beides stimmt, es sind zwei Ansichten.
**Das Farbsystem stammt aus der Wurzelkomponente** und ist keine Dekoration:
grün fängt an, rot ändert Daten, blau meldet, amber macht eine Aufgabe daraus,
violett entscheidet, grau ist ein Fremdsystem. Deshalb sind auf Slide 3 alle
Kacheln grün — jede Vorlage startet mit einem Auslöser.
> **Die Kartenform auf Slide 3 ist nicht aus dem Projekt.** Im Werkzeug werden
> Vorlagen nur geladen, nicht als Galerie gezeigt; es gibt dort keine Ansicht,
> an die man sich halten könnte. Wörtlich sind die Namen, Beschreibungen und
> Startbausteine, gebaut ist die Form. Der Unterschied gehört benannt.
**Text mittig, Titel ohne Fließtext.** Die Visualisierungen sitzen 896 px breit
bei x 92, also links wie rechts 92 px Rand — ein linksbündiger Textblock
darüber steht sichtbar aus der Achse. Die Titel-Slide trägt nur die
Überschrift: der Ablauf darunter sagt, was gemeint ist, und ein Fließtext
wiederholte es in Prosa. Dafür fiel hier dieselbe Korrektur an wie in
`post3.html` — ein leerer Fließtext darf keinen leeren Absatz erzeugen, sonst
schiebt sein Zeilenabstand die Zeichnung nach unten.
> **Die Gabel auf Slide 1 kommt früher als im Screenshot, und ein Schritt
> fehlt.** Alle drei echten Abläufe verzweigen erst an Position 3 oder 4 —
> das füllt ein hochkantes Format schlecht, weil über und unter dem Stamm
> nichts steht, solange er läuft. Hier gabelt es nach dem zweiten Knoten, und
> zwischen „Formular eingegangen" und „Passender Lead?" fehlt dadurch „Daten
> vereinheitlichen". Alle sechs Knoten heißen wörtlich wie in `workflow.png`
> und tragen dessen Farben, aber diese eine Kante ist zusammengezogen: ein
> Ablauf, den es genau so gibt, ist es nicht mehr — einer, den es geben
> könnte, schon. Die Anordnung ist ohnehin eigen, im Werkzeug zieht man
> Knoten mit der Maus.
**Kein PDS im Beitrag.** Von den sechs Vorlagen nennen drei das
Branchenprogramm — zwei im Namen, eine in der Beschreibung. Der Beitrag
richtet sich an Betriebe allgemein, nicht an die, die zufällig dieses eine
Programm fahren. Übrig bleibt genau die Gruppe `GENERAL_WORKFLOW_PRESETS`, in
der das Wort kein einziges Mal vorkommt — also keine Auswahl nach Geschmack,
sondern die vollständige zweite Gruppe. Gekürzt wurde nichts: aus „Offene
Angebote aus PDS erkennen" ein „Offene Angebote erkennen" zu machen wäre die
bequeme Lösung und kein Zitat mehr. Lieber eine Karte weniger.
**Alle Produkttexte auf den Slides sind wortgleich aus der Quelle**, Titel wie
Beschreibungen. Wo ein Name nicht in eine Zeile passte, steht er zweizeilig —
gekürzt wird nicht, denn aus „Daten vereinheitlichen" ein „Prüfen" zu machen
ist keine Kürzung, sondern ein anderer Name.
**Der Knoten ist die echte Komponente.** `NodeItem` aus
`NodeWorkflowEngine.tsx` des Pitch-Pakets: quadratisch 128 × 128, `rounded-lg`,
6 px Rand, Titel und Beschreibung **unter** dem Feld, beim Auslöser die linke
Flanke voll gerundet. Genau so rendert es auch `assets/screens/workflow.png`.
Ein Zwischenstand zeichnete stattdessen breite Karten mit farbiger Kante links
und einem Block „BESCHREIBUNG" — abgemalt vom Pitch-Bild `02_Präsentation/03`.
Im Code des Projekts gibt es das nirgends, und der Kommentar behauptete
trotzdem, es sei ein Nachbau der Komponente.
**Die Farben sind die des Produkts**, nicht die der Marke. Aufgelöst aus
`ui/theme.css` über die ganze Token-Kette (`surface-ui-subtle-green-default`
→ `--color-ui-green-100` → `--color-green-100`), nicht aus dem Bild geschätzt:
| Rolle | Fläche | Zeichen |
|---|---|---|
| Post 1 · Lösung | `post1.html` | Carousel, Anzahl frei |
| Auslöser | `#DCFCE4` | `#148134` |
| Daten | `#FEE2E2` | `#C52020` |
| Entscheidung | `#F0E7FF` | `#691CD4` |
| Meldung | `#D5EFFF` | `#003BEB` |
| Abschluss | `#FCF6C5` | `#9B5E0D` |
Ein Zwischenstand hatte sie durch Markenfarben ersetzt, weil `rules.md` §2 diese
Töne nicht kennt. Das ist zurückgenommen: der Beitrag zeigt die Oberfläche, und
ihre Farbe trägt Bedeutung — grün fängt an, rot arbeitet an Daten, violett
entscheidet, blau meldet, gelb schließt ab. Umgefärbt wäre es eine
Nacherzählung. Der Rand ist `border-default` `#E5E5E5`, also der Wert, vor dem
`rules.md` §5 warnt; hier trägt er, weil die Felder farbig gefüllt sind.
**Der Notizzettel auf Slide 1** ist `StickyNote.tsx` aus dem Pitch-Paket
(`rounded-md`, Kopfleiste `h-8`, Text `#424242`, sechs Farbpaare) mit dem
grünen Zettel aus dem Preset `form-to-pds`, Text unverändert. Zettel sind das,
was einen Ablauf im Werkzeug von einem Diagramm unterscheidet.
**Was hier bewusst nicht steht: ein erfundener Vorgang.** Ein Zwischenstand
hatte eine Slide mit einem Aktivitätenverlauf samt Uhrzeiten. Nichts davon war
gemessen. Bei einem Produkt vor dem Start ist eine erfundene Uhrzeit keine
Veranschaulichung, sondern ein Beleg, den es nicht gibt.
**Slide 1 trägt einen Ablauf, kein Stichwort.** Im Profilraster ist sie eine
Kachel von rund 150 px, auf 3:4 beschnitten — dort ist kein Wort lesbar. Was
ankommt, ist die Form: eine Kette, die sich gabelt.
Was über die 1080 px einer Seite hinausragt, landet auf der Nachbarslide — das
durchgehende Blatt aus `rules.md` §5 liegt in jeder Slide. Ein Prüfskript misst
das nach; alle sechs Zeichnungen bleiben auf der Sicherheitszone.
Sein Grund ist **gepunktet statt leer**: die Arbeitsfläche des Workflow-Editors,
mit den Werten aus `WorkflowEditor.tsx` der Website. Der Punktgrund steht als
dritte Stellung unter *Versuch → Punkte* und ist die Voreinstellung dieses
Projekts. Warum das Theme `modern` hier von seinem ruhigen Grund abweicht, steht
im CSS bei `.layer-raster`.
**Post 5 zeigt die Pipeline von vier Seiten.** Das Demo-Projekt hat dafür vier
Bildschirme — Schablone, Board, Liste, Detail — und jeder bekommt eine Slide,
dazu die Karte aus der Nähe. Anders als bei Post 4 wird **nichts umgefärbt**:
eine Pipeline lebt davon, dass man das Gewerk an der Farbe erkennt, ohne zu
lesen, und dass der Fuß einer Karte sich mit ihrer Priorität färbt. Die
Zuordnung Gewerk zu Ton ist `CATEGORY_TONES`, die Plättchenfarben sind
`ui/theme.css`.
| Slide | Screen | Was sie leistet |
|---|---|---|
| 1 | 4A | das Board selbst, drei Phasen mit Anzahl, Summe und Karten |
| 2 | 4A | eine Karte groß |
| 3 | 4B | „Vom Verkauf in die Abwicklung": dasselbe Projekt in `Auftragsabwicklung SHK`, mit dem Knopf für den Pipelinewechsel |
| 4 | 4C | dieselben Projekte als Tabelle |
| 5 | 3 | der Feldeditor, offen auf „Aufmaß erledigt" |
**Es sind zwei Pipelines, nicht eine.** Das Board gehört zu
`Vertrieb: Anlagen-Verkauf`, das Projekt in der Detailansicht liegt in
`Auftragsabwicklung SHK`, und ein Knopf schiebt es von der einen in die andere.
Das ist der Unterschied zwischen Vertriebs- und Abwicklungs-Projektmanagement,
und Slide 3 ist deshalb die Pointe des Beitrags. Möglich ist beides in einer
Software, weil die Felder an der Pipeline hängen und nicht am Programm — das
zeigt Slide 5.
**Alle Zahlen sind gerechnet, keine ist gesetzt.** Anzahl und Summe je Spalte
kommen aus denselben Datensätzen wie die Karten.
**Die Karte ist aus dem Code nachgebaut, nicht aus dem Screenshot.** Der erste
Anlauf war von `assets/screens/pipelines.png` abgemalt und lag an drei Stellen
daneben, die im Bild nicht zu erkennen waren. Der Rest steht in
[`POST-5.md`](./POST-5.md) §7.
> **Eine Richtigstellung.** Ein früherer Stand behauptete hier, die
> Detailansicht sei nur ein Entwurfs-PNG. Falsch: geprüft worden waren die
> Beschriftungen des Bildes `07_Screen-4b_Karten-Detail.png`, und die kommen im
> Code wirklich nicht vor — den Bildschirm gibt es trotzdem, unter
> `app/(screens)/4b/`, nur anders beschriftet. Die Abwesenheit einer
> Zeichenkette beweist die Abwesenheit eines Bildschirms nicht.
**Post 6 zeigt den Team-Chat**, nachgebaut aus `GeneralTeamChatPage` des
Pitch-Pakets. Anders als die Pipeline mit ihren vier eigenen Bildschirmen gibt
es vom Chat nur eine Fassung, unter `/allgemein/team-chat`. Die Inhalte darin
sind trotzdem aus dem Gewerk: Wärmepumpe Müller, Montagetermin,
Baustellenfotos.
| Slide | Was sie leistet |
|---|---|
| 1 | Kanalliste und Verlauf nebeneinander, mit Filterzeile und Eingabefeld |
| 2 | links das Durcheinander als Zeichnung, rechts die Kanalliste |
| 3 | Fotos, die Aufgabe daraus, die Bestätigung im Projekt |
| 4 | das Menü hinter dem Plus im Eingabefeld |
| 5 | zwei Ausschnitte der Oberfläche, versetzt übereinander, mit Absperrband |
**Slide 3 ist der Kern**, und sie ist keine Konstruktion: im Werkzeug folgen
die drei Nachrichten genau so aufeinander. Tobias schickt die Baustellenfotos,
darauf eine Nachricht mit angehängter Aufgabe, darauf die Systemzeile
„Aufgabe „Materialliste prüfen" im Projekt erstellt". Die letzte Zeile macht
den Unterschied: die Aufgabe liegt danach im Projekt, nicht nur im Verlauf.
**Slide 5 zeigt zwei Ausschnitte statt eines Geräts.** Die Machart ist die,
die Signal für solche Slides benutzt: zwei Teile der Oberfläche,
versetzt übereinander, jeder mit eigenem Schatten. Ein Zwischenstand hatte hier
ein nachgebautes Handy samt Insel und Tasten; das kostet viel Platz für etwas,
das nichts über das Produkt sagt, und der Rahmen war frei erfunden.
**Slide 2 stellt gegenüber statt zu zeigen:** links das Durcheinander als
Zeichnung, rechts die Oberfläche als Ausschnitt. Die linke Hälfte gibt es in
diesem Repo schon, in `post2.html` auf Slide 2 (`visProzesse`) — dort im
Zeichenblock-Stil, hier glatt. Die Plättchen sind Redaktionstext und keine
Produktstrings; sie beschreiben die Wirklichkeit im Betrieb.
**Die Leitung läuft über den Wisch hinweg:** sie beginnt links außerhalb der
Slide, und mit `visLeft: -102` landet dieses Stück auf Slide 1. Das ist der
Überstand, vor dem `rules.md` §5 sonst warnt — `.page` hat kein `overflow` —
hier aber gewollt. `pruef.mjs` liest einen negativen `--vis-left` als Ansage
und meldet ihn nicht mehr als Fehler.
Beide Ausschnitte auf Slide 5 sind leicht verwischt: erkennbar, aber nicht zu lesen.
Scharf bleibt allein das Band. Ein zweites Mittel von Slack, ein dicker gelber
Rahmen um die Anhangkarte auf Slide 3, ist wieder weg: die Karte trägt als
einzige eine gelbe Marke, der Blick geht ohnehin dorthin, und ein Rahmen sagt
nochmal, was die Anordnung schon sagt — [`POST-6.md`](./POST-6.md) §5c.
Dass die App noch nicht da ist, sagt das **Absperrband** von der Website:
`ChatDemoSection.tsx` hat es unter der Fahne `comingSoon`, -13° gedreht, in
`#F1B729` mit Kanten in `#171106`, darauf Schutzhelm und „BALD VERFÜGBAR". Auch
Überschrift und Fließtext sind die der Website; der Inhalt der Ausschnitte
kommt aus dem Pitch-Paket wie auf allen anderen Slides.
**Das Band läuft hinter die Ausschnitte** und ist an ihrer Silhouette
beschnitten — so hält es die Website auch, dort am Rand des Telefons. Es endet
damit nicht mehr quer zur eigenen Laufrichtung an der Bildkante, wie ein
Aufkleber, sondern an einer senkrechten Plattenkante, hinter der es
verschwindet. Die Schnittkante ist weicher als eine gezogene Linie und härter
als die unscharfen Platten.
**Und es legt sich um den Stapel, statt darauf zu liegen** — die größte
Abweichung von der Website in diesem Beitrag. Genau an der Plattenkante knickt
es um, läuft dahinter weiter und kommt an der Kante wieder hervor: tiefer,
weil es umgeschlagen ist, niedriger, weil es wegführt, flacher getönt, weil
man von dort seine Unterseite sieht. Ohne die zwei Festlichkeiten des
klassischen Spruchbands: keine Zacken, kein Bogen.
Die Stelle des Knicks wird dabei nicht gerechnet. Beide Stücke reichen weit in
die Platte hinein, und zwei Masken aus derselben Kontur schneiden sie an deren
Kante ab — die eine lässt die Fläche innen stehen, die andere die Stücke
außen, und ihre Deckungen ergänzen sich zu eins. Darüber liegt die Falzkante
als eigener Strich, senkrecht auf der Plattenkante und über beide hinweg. Drei
Anläufe hat es bis dahin gebraucht; woran die ersten beiden lagen, steht in
[`POST-6.md`](./POST-6.md) §5a.
**Die Tiefe kommt aus dem Stapel und gestaffelten Schatten, nicht aus
Flächen.** Ein Zwischenstand legte graue Flächen hinter die Elemente; das nimmt
den Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den
Punktgrund. Stattdessen zweierlei: die echten Schatten aus `ui/theme.css`, die
dort **gestaffelt** sind (enge dunkle Kante plus weite helle, in SVG zwei
`feDropShadow` im selben Filter), und der **Stapel** aus `post1.html`, in SVG
nachgebaut mit denselben Winkeln, die dort im CSS stehen. Die hinteren Blätter
sind leer: Andeutung, keine Kopie. Auf Slide 1 und 2, wo ein Bildschirm zu
sehen ist; nicht auf Slide 3, wo ein Ausschnitt einer Unterhaltung steht.
Dazu die Datumsmarke und die Zeile „Maike Berger schreibt gerade …" aus der
Quelle, die der Unterhaltung Struktur geben, ohne eine Fläche zu behaupten.
**Die Bildergalerie ist eine Anhangkarte, kein Bild.** Im Werkzeug lädt
`GalleryPreview` zwei Fotos von Unsplash. Fremde Fotos in einen Beitrag zu
setzen, der von der eigenen Baustelle spricht, wäre eine Behauptung über
Material, das wir nicht haben.
**Die vier Profilbilder sind Unsplash-Aufnahmen.** Sie liegen unter
`assets/eigene/` als `portrait-sk`, `-mb`, `-tw`, `-ms`, ausgewählt nicht nach
Gesicht, sondern nach Unterscheidbarkeit im Kreis von 42 px — dort zählt der
Gesamtwert und die Haarkontur, nicht die Züge.
Zum Absatz darüber steht das in einer Spannung, die benannt gehört: die
Unsplash-Lizenz deckt das Urheberrecht des Fotografen, **kein Model Release**.
Ein Zwischenstand wich deshalb auf erzeugte Gesichter aus und ist zugunsten
echter Aufnahmen zurückgenommen worden. Herkunft, Rechtslage und der einzige
Weg, der die Frage wirklich beseitigt, stehen in
[`POST-6.md`](./POST-6.md) §4.
Die vierzehn Partner-Marken liegen unter `assets/partner/` und sind, wie alles
andere hier, aus der Website **gespiegelt statt verlinkt**. Sechs davon sind
PNG und damit dieselbe Wette wie ein Screenshot (§7 in `rules.md`), deshalb
prüft der Export sie einzeln nach — `wabenZonen()` in `post3.html`.
Der Redaktionsplan für alle weiteren Beiträge liegt in
[`redaktionsplan/`](./redaktionsplan/), gebaut mit `python3 redaktionsplan/bauen.py`.
### Slides anlegen und wegnehmen
@@ -94,6 +394,102 @@ Server nicht läuft. Wer den Stand ändert, muss also committen und pushen —
sonst sieht ihn niemand sonst. Gibt es die Datei noch nicht, wird der Stand
aus dem Browser einmalig hineingeschrieben.
## Animation
Unter dem Canvas liegt in **jedem** Projekt, im selben Fenster, ein Bereich
„Animation". Er zeigt jede Animation als eigenes Blatt in Vorschau, mit
Zeitregler, `Abspielen` und `MP4 exportieren`.
**Ein Klick auf den Kopf klappt ihn zu**, dann bleibt eine Zeile von 40 px und
der Canvas hat den Platz. Ob er offen ist, merkt sich der Browser je Projekt
(`localStorage`) und nicht der Projektstand: es ist eine Vorliebe und keine
Gestaltung, sie hat in `state/<projekt>.json` nichts verloren, wo sie bei jedem
einen Diff machen würde.
**Eine Animation ist keine Slide.** Sie steht in `ANIMS` und nicht in
`SLIDES`, liegt also nicht im Streifen, taucht in der Slide-Auswahl nicht auf
und wird vom Bildexport nicht angefasst. Das ist der ganze Punkt: im Carousel
ist sie ein eigenes Element, und ob sie mitkommt, entscheidest du beim Posten.
```js
const ANIMS = [
{ key: "animChat", type: "feature", dauer: 6,
eyebrow: "Bewegtbild",
head: 'Getippt,<br><span class="a">und schon im Projekt.</span>',
body: "Eine Nachricht, ein Klick, und die Aufgabe hängt am richtigen Projekt.",
vis: "animChat", visW: 700, visLeft: 190, visBottom: 120 },
];
```
Dieselben Felder wie bei einer Slide, dazu `dauer` in Sekunden. Die
Zeichenfunktion aus `VIS` bekommt einen zweiten Wert `t` zwischen 0 und 1 und
zeichnet daraus den Stand zu diesem Zeitpunkt. Blatt, Überschrift,
Punktgrund und Schutzzone sind dieselben wie überall.
### Exportieren
**`MP4 exportieren`** im Bereich unter dem Canvas. Der Browser kann kein MP4
schreiben, also stößt die Seite den Bau auf dem Server an (`POST /anim`) und
fragt den Stand ab; die Zeile unter den Knöpfen zeigt den Fortschritt und am
Ende den Pfad. Getrennt vom Bildexport, in beide Richtungen: „Alle Slides als
ZIP" enthält kein Video, und dieser Knopf rendert kein Bild.
**Die Datei landet zweimal.** In `~/Downloads`, wie die Bilder, zum Hochladen.
Und in `animation/` im Repo, wo sie versioniert ist und nach einem Pull auf
der anderen Maschine liegt. In Downloads wird nichts überschrieben: eine
zweite Ausgabe heißt `…-1.mp4`, genau wie beim Bildexport.
Auf der Kommandozeile geht dasselbe:
```sh
./serve.sh # muss laufen
node animation.mjs post6.html # alle Animationen des Projekts
node animation.mjs post6.html animChat # nur diese
```
Heraus kommt `animation/post6-animChat.mp4` (auf der Kommandozeile nur dort,
ohne die Kopie in Downloads). Angesprochen wird die Animation
über ihren Schlüssel, nicht über eine Nummer: sie hat keine Position im
Streifen, die sich verschieben könnte.
Für die Aufnahme lädt das Skript die Seite als `?nur=anim&k=<key>`. In diesem
Modus zeigt sie allein das Blatt der Animation, in natürlicher Größe an 0,0 —
damit entfällt jede Ausschnittsrechnung.
### Was Instagram bei so einem Beitrag tatsächlich annimmt
Kurz: **ein MP4, kein GIF.**
| | |
|---|---|
| Format | MP4, H.264 High, `yuv420p`, `+faststart` |
| Maße | 1080 × 1350 (4:5), wie die Standbilder |
| Bildrate | 30 fps, Schlüsselbild jede Sekunde |
| Datenrate | rund 12 Mbit/s |
| Länge | mindestens 3 s, höchstens 60 s je Carousel-Element |
| Ton | eine stumme AAC-Spur |
Vier Dinge, die den Aufbau bestimmen:
- **GIF geht nicht.** Instagram nimmt keine GIF-Uploads an; die Datei landet
als Standbild. Was in der App wie ein animierter Beitrag aussieht, ist ein
kurzes Video.
- **Ein Video im Carousel wird kein Reel.** Seit 2022 wandert nur ein
*einzeln* geposteter Video-Beitrag automatisch in die Reels. Liegt das Video
in einem Carousel, bleibt der Beitrag ein Beitrag.
- **Der erste Beitrag im Carousel bestimmt den Ausschnitt für alle.** Ein
Element in einem anderen Seitenverhältnis wird beschnitten, nicht
eingepasst. Deshalb rendert die Animation in genau denselben 1080 × 1350 wie
jedes Standbild.
- **Eine stumme Tonspur muss mit.** Manche Upload-Wege lehnen ein Video ohne
Audiostrom ab. Die Spur kostet 128 kbit/s und erspart die Fehlersuche.
Die Bewegung läuft in Schleife. Das letzte Bild ist deshalb **nicht** dasselbe
wie das erste (`t = n / anzahl`, nicht `n / (anzahl − 1)`) — sonst steht der
Stoß eine Dreißigstelsekunde still.
## Bedienung
| Taste | Wirkung |
@@ -146,17 +542,48 @@ Maßstab zurück.
```
assets/
├── brand/ Wortmarke hell/dunkel, G-Marke (SVG + PNG), Wabe, Geerkens
├── partner/ DATEV, KWP, Google Calendar, Google Drive, MS Outlook, MS Teams
├── partner/ die 14 Marken des Wabenfelds (DATEV, KWP, pds, Bauhow, Aircall,
│ Onepage, PandaDoc, Hallo Petra, Claude, OpenAI, Google Drive,
│ Google Calendar, MS Outlook, MS Teams)
├── mockups/ Geräte-Mockups (Displays ausgeknockt, Screenshot liegt dahinter)
├── screens/ Software-Screenshots
├── eigene/ selbst importierte Bilder
└── fonts/ Manrope 400–800, lokal
vendor/ html-to-image, JSZip
state/ Projektstände, gehören ins Repo
serve.py Statischer Server + /save, /state, /upload, /bilder
animation.mjs baut aus einem ANIMS-Eintrag ein MP4 (braucht ffmpeg)
animation/ die fertigen MP4s, ein Carousel-Element je Datei
serve.py Statischer Server + /save, /state, /upload, /bilder, /anim
rules.md Gestaltungsregeln, Farbwerte, Export-Wissen
```
### Assets werden nachgefragt, nicht neu geladen
Ein Bild, das unter demselben Namen ausgetauscht wird, muss sofort ankommen —
sonst arbeitet man gegen eine alte Kopie. Dafür stand auf allem `no-store`:
im Server und noch einmal in `preloadAssets()`. Der Browser durfte nichts
behalten.
Das Ziel war richtig, der Preis zu hoch. Jeder Reload lud alles neu, und weil
`uebersicht.html` die sechs Projekte als iframes lädt und jeder iframe für
sich vorlud, kam die Seite auf **38 MB in 435 Anfragen**.
`no-cache` leistet dasselbe billiger: der Browser behält seine Kopie, fragt
aber vor jeder Benutzung nach. Eine ausgetauschte Datei kommt genauso sofort
an, eine unveränderte kostet ein 304 ohne Rumpf — und die sechs iframes teilen
sich eine Kopie statt sechs zu ziehen.
| | vorher | jetzt |
|---|---|---|
| `uebersicht.html`, erster Aufruf | 38,1 MB | 2,2 MB |
| `uebersicht.html`, Reload | 32,8 MB | 1,9 MB |
| `post6.html`, Reload | 5,4 MB | 0,3 MB |
Nachgeprüft ist beides: unverändert antwortet der Server mit `304` und
0 Bytes, nach einem `touch` mit `200` und der vollen Datei. **Seite, Stand und
Auskünfte (`/bilder`, `/state`) bleiben bei `no-store`** — die sind klein und
dürfen nie aus einer Kopie kommen.
## Regeln
**`rules.md` ist die Arbeitsgrundlage.** Farbwerte, Geometrie, Typografie,
+975
View File
@@ -0,0 +1,975 @@
<!doctype html>
<meta charset="utf-8"><title>Varianten Titel-Slide</title>
<link rel="stylesheet" href="./assets/fonts/manrope.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#fff;font-family:'Manrope',sans-serif;padding:44px 40px 50px}
h1{font-size:30px;font-weight:800;letter-spacing:-.025em;margin-bottom:6px}
h1+.hin{margin-bottom:32px}
.hin{font-size:14px;color:#666;line-height:1.45;max-width:920px}
hr{border:0;border-top:1px solid #E2E6EA;margin:50px 0 40px}
.gitter{display:grid;grid-template-columns:repeat(3,1fr);gap:36px 26px}
figure{display:flex;flex-direction:column;gap:12px}
.buehne{height:452px;position:relative}
.karte-halter{position:absolute;left:20px;top:70px;width:620px;
transform:scale(.46);transform-origin:top left}
.karte-halter.leer{height:775px}
svg.karte-skizze{position:absolute;z-index:6;left:-7.42%;width:114.84%;top:-19.35%;height:125.29%;
overflow:visible}
.karte{position:relative;aspect-ratio:4/5;overflow:hidden;border-radius:8px;background:#0f1a2e;isolation:isolate}
.karte-grund{position:absolute;inset:0;z-index:1;overflow:hidden;border-radius:8px;background:#2b2d2a;
filter:grayscale(.04) saturate(.74) brightness(.73)}
.karte-grund img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.karte-wash{position:absolute;inset:0;background:linear-gradient(180deg,rgba(15,26,46,.01),rgba(15,26,46,.12))}
.karte-person{position:absolute;inset:0;z-index:4;width:100%;height:100%;object-fit:cover;
filter:saturate(1) contrast(1.01)}
.karte-name{position:absolute;left:6%;bottom:5.7%;z-index:5;display:flex;align-items:baseline;gap:13px;
padding:13px 17px;background:#F1B729;color:#101010}
.karte-name span{font-size:27px;font-weight:700;letter-spacing:-.03em;line-height:1.15}
.karte-name small{font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
/* Halb hell, halb dunkel: der Helm liegt im Post auf beidem. */
.probe{width:300px;height:250px;background:linear-gradient(90deg,#fff 50%,#14161A 50%);
border:1px solid #E2E6EA}
.probe svg{display:block}
figcaption{display:flex;flex-direction:column;gap:3px}
figcaption b{font-size:15px;font-weight:800;letter-spacing:-.01em;line-height:1.25}
figcaption span{font-size:12.5px;color:#666;line-height:1.35}
</style>
<h1>1 &middot; Rahmen</h1>
<p class="hin">Ohne Foto, damit nur die Zeichnung zu sehen ist. In Originalgroesse gezogen und fuer die
Ansicht auf 46 % verkleinert, damit die Strichstaerken zueinander stimmen. Nummer 7 ist die gewaehlte
Mischung und steckt bereits in post2.html.</p>
<div class="gitter"><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r0"></svg></div></div><figcaption><b>1 · Fein</b><span>Zwei Zuege, der zweite als leichter Nachzieher.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r1"></svg></div></div><figcaption><b>2 · Kraeftig</b><span>Ein einziger fetter Zug, enger am Bild.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r2"></svg></div></div><figcaption><b>3 · Versetzt</b><span>Zweiter Rahmen nach unten rechts.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r3"></svg></div></div><figcaption><b>4 · Passepartout</b><span>Duenner Aussenrahmen mit Abstand, fetter Innenrahmen.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r4"></svg></div></div><figcaption><b>5 · Chamfer</b><span>Die Markenform, von Hand gezogen.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r5"></svg></div></div><figcaption><b>6 · Schattenkante</b><span>Fetter Rahmen, aussen an zwei Seiten schraffiert.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter leer"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="r6"></svg></div></div><figcaption><b>7 · Gewaehlt: Chamfer + Versetzt + Schattenkante</b><span>Der versetzte Zug ist die Aussenkante des Schattens, die Schraffur fuellt das Band dazwischen.</span></figcaption></figure></div>
<hr>
<h1>2 &middot; Helme</h1>
<p class="hin">Jeder Helm auf halb hellem, halb dunklem Grund: im Post liegt er auf beidem, deshalb ist
er ganz in Gelb gezogen. Alle in derselben Breite, damit die Entwuerfe vergleichbar sind.</p>
<div class="gitter"><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k0"></svg></div><figcaption><b>H1 · Einfach</b><span>Kuppel, Krempe, drei Grate. Der Helm, der im Post steckt.</span></figcaption></figure><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k1"></svg></div><figcaption><b>H2 · Fein</b><span>Mehr Stuetzpunkte, First, Nebenrippen, Krempendicke, Schattierung.</span></figcaption></figure><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k2"></svg></div><figcaption><b>H3 · Schraffiert</b><span>Gefuellt statt schattiert, dafuer ohne Rippen.</span></figcaption></figure><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k3"></svg></div><figcaption><b>H4 · Dreiviertel</b><span>Krempe als Ellipse, dadurch raeumlich statt flach.</span></figcaption></figure><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k4"></svg></div><figcaption><b>H5 · Mit Kinnriemen</b><span>Wie Fein, dazu Riemen und Schnalle.</span></figcaption></figure><figure><div class="probe"><svg viewBox="0 0 300 250" width="300" height="250" id="k5"></svg></div><figcaption><b>H6 · Kontur</b><span>Nur die Silhouette, zwei kraeftige Zuege.</span></figcaption></figure></div>
<hr>
<h1>3 &middot; Fertiges Konzept</h1>
<p class="hin">Rahmen, Foto und Helm 2 zusammen. Nur Lage und Groesse des Helms unterscheiden sich.</p>
<div class="gitter"><figure><div class="buehne"><div class="karte-halter"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="f0"></svg><div class="karte"><div class="karte-grund"><img alt="" src="./assets/team/noah-bg.webp"><span class="karte-wash"></span></div><img class="karte-person" alt="" src="./assets/team/noah-person.webp"><div class="karte-name"><span>Noah</span><small>CEO</small></div></div></div></div><figcaption><b>A · Ueber der Ecke</b><span>Helm 2, ueber der oberen rechten Ecke.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="f1"></svg><div class="karte"><div class="karte-grund"><img alt="" src="./assets/team/noah-bg.webp"><span class="karte-wash"></span></div><img class="karte-person" alt="" src="./assets/team/noah-person.webp"><div class="karte-name"><span>Noah</span><small>CEO</small></div></div></div></div><figcaption><b>B · Auf dem Kopf, einfach</b><span>Helm 2 auf dem Kopf, kleinere Breite (186).</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="f2"></svg><div class="karte"><div class="karte-grund"><img alt="" src="./assets/team/noah-bg.webp"><span class="karte-wash"></span></div><img class="karte-person" alt="" src="./assets/team/noah-person.webp"><div class="karte-name"><span>Noah</span><small>CEO</small></div></div></div></div><figcaption><b>C · Auf der Ecke</b><span>Helm 2, nach rechts gekippt, Oeffnung ueber der Ecke.</span></figcaption></figure><figure><div class="buehne"><div class="karte-halter"><svg viewBox="0 0 712 971" width="712" height="971" class="karte-skizze" id="f3"></svg><div class="karte"><div class="karte-grund"><img alt="" src="./assets/team/noah-bg.webp"><span class="karte-wash"></span></div><img class="karte-person" alt="" src="./assets/team/noah-person.webp"><div class="karte-name"><span>Noah</span><small>CEO</small></div></div></div></div><figcaption><b>D · Auf dem Kopf, fein</b><span>Helm 2 auf dem Kopf, groessere Breite (222).</span></figcaption></figure></div>
<script>
/* ─── Skizzen-Primitive ─────────────────────────────────────────────────────
Handzeichnung aus gesetztem Zufall. Der Keim ist fest: der Export rendert
bei fehlgeschlagener Pruefung bis zu fuenfmal neu, und jeder Durchgang muss
denselben Strich ergeben. Mit ungesetztem Math.random zittert jedes Bild
anders und die Kontrolle vergleicht gegen etwas, das es nie wieder gibt. */
let _s = 0;
function keim(n) { _s = n >>> 0; }
function rnd() {
_s = (_s + 0x6D2B79F5) >>> 0;
let t = Math.imul(_s ^ (_s >>> 15), 1 | _s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const zit = m => (rnd() * 2 - 1) * m;
const n1 = v => Math.round(v * 10) / 10;
/* Ein Strich, leicht gebogen und an den Enden verwackelt. Der Bogen waechst
mit der Laenge: eine kurze Kante zittert, eine lange schwingt. */
function strich(x1, y1, x2, y2, o) {
o = o || {};
const j = o.j == null ? 1.5 : o.j;
const ue = o.ue == null ? 0 : o.ue; // Ueberschuss an den Enden
let dx = x2 - x1, dy = y2 - y1;
const len = Math.hypot(dx, dy) || 1, ux = dx / len, uy = dy / len;
const ax = x1 - ux * rnd() * ue + zit(j), ay = y1 - uy * rnd() * ue + zit(j);
const bx = x2 + ux * rnd() * ue + zit(j), by = y2 + uy * rnd() * ue + zit(j);
dx = bx - ax; dy = by - ay;
const nx = -dy / len, ny = dx / len;
const b = o.b == null ? len * 0.008 + 1 : o.b;
const o1 = zit(b), o2 = zit(b);
return "M" + n1(ax) + "," + n1(ay) + "C" +
n1(ax + dx / 3 + nx * o1) + "," + n1(ay + dy / 3 + ny * o1) + " " +
n1(ax + dx * 2 / 3 + nx * o2) + "," + n1(ay + dy * 2 / 3 + ny * o2) + " " +
n1(bx) + "," + n1(by);
}
/* Punktfolge zu einer weichen Kurve (Catmull-Rom als Bezier). */
function glatt(pts, zu) {
const n = pts.length;
const at = i => pts[zu ? (i + n) % n : Math.max(0, Math.min(n - 1, i))];
let d = "M" + n1(pts[0][0]) + "," + n1(pts[0][1]);
const bis = zu ? n : n - 1;
for (let i = 0; i < bis; i++) {
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
d += "C" + n1(p1[0] + (p2[0] - p0[0]) / 6) + "," + n1(p1[1] + (p2[1] - p0[1]) / 6) +
" " + n1(p2[0] - (p3[0] - p1[0]) / 6) + "," + n1(p2[1] - (p3[1] - p1[1]) / 6) +
" " + n1(p2[0]) + "," + n1(p2[1]);
}
return d;
}
/* Kringel statt Kreis: der Zug faengt irgendwo an, laeuft einmal herum und
schiesst ein Stueck ueber den Anfang hinaus. Genau das macht die Hand. */
function kringel(cx, cy, rx, ry, o) {
o = o || {};
ry = ry == null ? rx : ry;
const wack = o.wack == null ? Math.min(rx, ry) * 0.02 + 1 : o.wack;
const a0 = o.a0 == null ? rnd() * 6.283 : o.a0;
const ueber = o.ueber == null ? 0.16 + rnd() * 0.2 : o.ueber;
const schritte = 18, pts = [];
const spanne = 6.283 + ueber;
for (let i = 0; i <= schritte; i++) {
const a = a0 + spanne * (i / schritte);
pts.push([cx + Math.cos(a) * (rx + zit(wack)), cy + Math.sin(a) * (ry + zit(wack))]);
}
return glatt(pts, false);
}
/* Vier einzelne Kanten mit Ueberschuss an den Ecken. Der Ueberschuss ist das
eigentliche Erkennungsmerkmal von Handzeichnung, nicht das Zittern. */
function rechteck(x, y, w, h, o) {
o = o || {};
const ue = o.ue == null ? 3.5 : o.ue;
return [
strich(x, y, x + w, y, { ue: ue }),
strich(x + w, y, x + w, y + h, { ue: ue }),
strich(x + w, y + h, x, y + h, { ue: ue }),
strich(x, y + h, x, y, { ue: ue }),
].join("");
}
/* Der Chamfer als gezeichneter Kasten: acht Kanten statt vier. Das ist die
Markenform aus rules.md Paragraf 3, hier von Hand gezogen. */
function chamfer(x, y, w, h, c, o) {
o = o || {};
const ue = o.ue == null ? 2.5 : o.ue;
const p = [[x + c, y], [x + w - c, y], [x + w, y + c], [x + w, y + h - c],
[x + w - c, y + h], [x + c, y + h], [x, y + h - c], [x, y + c]];
let d = "";
for (let i = 0; i < 8; i++) {
const a = p[i], b = p[(i + 1) % 8];
d += strich(a[0], a[1], b[0], b[1], { ue: i % 2 ? 1 : ue });
}
return d;
}
/* Schraffur als Schattierung: parallele Striche, an den Enden kuerzer, damit
das Feld nicht als Block steht. */
function schraff(x, y, w, h, abstand, neig) {
neig = neig == null ? 0.55 : neig;
let d = "";
for (let t = -h * neig; t < w; t += abstand) {
const x1 = x + t, x2 = x + t + h * neig;
const a = Math.max(x, Math.min(x + w, x1)), b = Math.max(x, Math.min(x + w, x2));
if (b - a < 3) continue;
const y1 = y + h - (a - x1) / neig, y2 = y + h - (b - x1) / neig;
d += strich(a, Math.min(y + h, Math.max(y, y1)), b, Math.min(y + h, Math.max(y, y2)), { j: .8, b: .8 });
}
return d;
}
/* Bodenschatten: ein paar kurze Striche unter dem Gegenstand. Sie sitzen eng
am Fuss, sonst schwebt der Gegenstand ueber seinem eigenen Schatten. */
function boden(cx, y, w) {
let d = "";
for (let i = 0; i < 4; i++) {
const f = 1 - Math.abs(i - 1.5) * 0.26;
const b = w * f * (0.62 + rnd() * 0.26);
d += strich(cx - b / 2 + zit(3), y + i * 3.6, cx + b / 2 + zit(3), y + i * 3.6 - 4, { j: .9, b: 1.2 });
}
return d;
}
/* ─── Bausteine ────────────────────────────────────────────────────────── */
const TINTE = "#2A2C33", GELB = "#E9A81C";
const zug = (d, o) => {
o = o || {};
return '<path d="' + d + '" fill="none" stroke="' + (o.f || TINTE) +
'" stroke-width="' + (o.w || 3.1) + '" stroke-linecap="round" stroke-linejoin="round"' +
(o.op ? ' stroke-opacity="' + o.op + '"' : '') +
(o.strichel ? ' stroke-dasharray="' + o.strichel + '"' : '') + '/>';
};
const schatten = d => zug(d, { w: 1.9, op: .34 });
/* ─── Die Satelliten ─────────────────────────────────────────────────────
Jedes Ikon meldet zurueck, wie weit es reicht (rad) und wo seine
Beschriftung hingehoert (unten). Sonst haengt der Knoten am kleinen Ikon
zu weit weg und am grossen klebt er drin. */
function ikonKi(cx, cy) {
const zack = (x, y, r) => {
const k = r * 0.28, p = [];
for (let i = 0; i < 8; i++) {
const a = -Math.PI / 2 + i * Math.PI / 4, rr = i % 2 ? k : r;
p.push([x + Math.cos(a) * rr + zit(1.4), y + Math.sin(a) * rr + zit(1.4)]);
}
return glatt(p, true);
};
return zug(zack(cx - 8, cy - 6, 46), { w: 3.4 }) +
zug(zack(cx + 38, cy + 30, 17), { w: 2.7 }) +
zug(zack(cx + 34, cy - 34, 12), { w: 2.5 }) +
schatten(boden(cx, cy + 54, 88));
}
ikonKi.rad = 52; ikonKi.unten = 92;
function ikonChat(cx, cy) {
const blase = (x, y, rx, ry, sx) =>
zug(kringel(x, y, rx, ry, { ueber: .12 })) +
zug(glatt([[x + sx * rx * .42, y + ry * .82], [x + sx * rx * .86, y + ry * 1.6],
[x + sx * rx * .08, y + ry * .98]], false));
return blase(cx + 20, cy - 4, 38, 30, 1) +
blase(cx - 22, cy - 16, 47, 37, -1) +
[-18, 0, 18].map(o => zug(kringel(cx - 22 + o, cy - 16, 4.6), { w: 2.5 })).join("") +
schatten(boden(cx, cy + 52, 100));
}
ikonChat.rad = 58; ikonChat.unten = 90;
function ikonCrm(cx, cy) {
const kopf = (x, y, r, w) => zug(kringel(x, y, r), { w: w }) +
zug(glatt([[x - r * 1.55, y + r * 2.7], [x - r * 1.5, y + r * 1.05],
[x, y + r * .55], [x + r * 1.5, y + r * 1.05], [x + r * 1.55, y + r * 2.7]], false), { w: w });
return kopf(cx - 42, cy - 16, 17, 2.8) + kopf(cx + 42, cy - 16, 17, 2.8) +
kopf(cx, cy + 2, 22, 3.3) +
schatten(boden(cx, cy + 62, 116));
}
ikonCrm.rad = 72; ikonCrm.unten = 92;
function ikonAuto(cx, cy) {
const r = 39, zaehne = 10;
let d = "";
for (let i = 0; i < zaehne; i++) {
const a = i * 6.283 / zaehne, br = 0.22;
const p = [];
[[-br, r], [-br * .8, r + 14], [br * .8, r + 14], [br, r]].forEach(v => {
const aa = a + v[0];
p.push([cx + Math.cos(aa) * v[1] + zit(.9), cy + Math.sin(aa) * v[1] + zit(.9)]);
});
d += glatt(p, false);
}
return zug(kringel(cx, cy, r)) + zug(d, { w: 2.9 }) + zug(kringel(cx, cy, 15), { w: 2.7 }) +
schatten(boden(cx, cy + 60, 96));
}
ikonAuto.rad = 56; ikonAuto.unten = 88;
function ikonBoard(cx, cy) {
const w = 116, h = 88, x = cx - w / 2, y = cy - h / 2;
let d = rechteck(x, y, w, h);
let k = "";
for (let i = 1; i < 3; i++) k += strich(x + w * i / 3, y + 7, x + w * i / 3, y + h - 7, { j: 1, b: 1 });
const karte = (sx, sy, sh) => rechteck(x + sx, y + sy, w / 3 - 16, sh, { ue: 2 });
k += karte(8, 11, 19) + karte(8, 36, 19) + karte(w / 3 + 8, 11, 19) + karte(2 * w / 3 + 8, 11, 31);
return zug(d) + zug(k, { w: 2.4 }) + schatten(boden(cx, cy + h / 2 + 7, 116));
}
ikonBoard.rad = 64; ikonBoard.unten = 84;
function ikonFormular(cx, cy) {
const w = 90, h = 116, x = cx - w / 2, y = cy - h / 2;
let d = rechteck(x, y, w, h);
d += rechteck(x + w / 2 - 19, y - 12, 38, 22, { ue: 2 });
let k = "";
[32, 50, 68].forEach(o => { k += strich(x + 15, y + o, x + w - 34, y + o, { j: 1, b: 1 }); });
k += rechteck(x + 15, y + 80, 23, 23, { ue: 2 });
const hak = [[x + 19, y + 91], [x + 25, y + 98], [x + 35, y + 81]];
return zug(d) + zug(k, { w: 2.4 }) + zug(glatt(hak, false), { w: 3.6, f: GELB }) +
schatten(boden(cx, cy + h / 2 + 7, 92));
}
ikonFormular.rad = 62; ikonFormular.unten = 96;
/* Ein Strich mit Spitze am Ende. Die Spitze sind zwei kurze Zuege, keine
gefuellte Flaeche: gezeichnet wird sie so, gefuellt sieht sie gesetzt aus. */
function pfeil(x1, y1, x2, y2, gr) {
gr = gr == null ? 16 : gr;
const a = Math.atan2(y2 - y1, x2 - x1), oeff = 0.42;
return strich(x1, y1, x2, y2, { j: 1.1 }) +
strich(x2, y2, x2 - Math.cos(a - oeff) * gr, y2 - Math.sin(a - oeff) * gr, { j: .8, b: .8 }) +
strich(x2, y2, x2 - Math.cos(a + oeff) * gr, y2 - Math.sin(a + oeff) * gr, { j: .8, b: .8 });
}
/* Schraffur innerhalb einer Wabe. Eine gezeichnete Flaeche wird nicht
gefuellt, sie wird schraffiert: so bleibt der Raster-Grund sichtbar und
es sieht trotzdem nach "voll" aus wie auf der Website. */
function wabeSchraff(cx, cy, r, abstand) {
const h = r * 0.866;
let d = "";
for (let y = -h + abstand; y < h - abstand * 0.5; y += abstand) {
const halb = r - (r / 2) * (Math.abs(y) / h) - 8;
if (halb < 7) continue;
d += strich(cx - halb, cy + y - 3, cx + halb, cy + y + 3, { j: .9, b: 1 });
}
return d;
}
function ikonBrief(cx, cy, f) {
const w = 46, h = 31, x = cx - w / 2, y = cy - h / 2;
return zug(rechteck(x, y, w, h, { ue: 2.5 }), { w: 2.9, f: f }) +
zug(glatt([[x + 3, y + 4], [cx, y + h * 0.66], [x + w - 3, y + 4]], false),
{ w: 2.7, f: f });
}
function ikonBlaseKlein(cx, cy, f) {
return zug(kringel(cx, cy - 5, 25, 19, { ueber: .1 }), { w: 2.9, f: f }) +
zug(glatt([[cx - 8, cy + 12], [cx - 13, cy + 23], [cx + 3, cy + 13]], false),
{ w: 2.7, f: f }) +
[-9, 0, 9].map(o => zug(kringel(cx + o, cy - 5, 2.8), { w: 2.2, f: f })).join("");
}
function ikonKonfetti(cx, cy, f) {
// Knallbonbon: Tuete unten links, Funken nach oben rechts.
const tuete = glatt([[cx - 28, cy + 24], [cx - 6, cy - 2],
[cx + 10, cy + 19], [cx - 28, cy + 24]], false) +
strich(cx - 20, cy + 15, cx - 2, cy + 19, { j: .9, b: .8 });
let funken = "";
[-1.42, -1.06, -0.68, -0.28].forEach((a, i) => {
const r0 = 16, r1 = 30 + (i % 2) * 7;
funken += strich(cx + 2 + Math.cos(a) * r0, cy + 3 + Math.sin(a) * r0,
cx + 2 + Math.cos(a) * r1, cy + 3 + Math.sin(a) * r1, { j: 1 });
});
let punkte = "";
[[20, -27], [31, -11], [26, -35], [8, -33]].forEach(v => {
punkte += kringel(cx + v[0], cy + v[1], 3.1);
});
return zug(tuete, { w: 2.9, f: f }) + zug(funken, { w: 2.4, f: f }) +
zug(punkte, { w: 2.2, f: f });
}
/* Die Wabe, flat-top, Seitenverhaeltnis 2/Wurzel3 wie in rules.md Paragraf 3.
Bei dieser Form liegen die sechs Ecken auf einem Kreis vom Radius r. Genau
deshalb ist sie hier richtig: jede Speiche setzt an einer Ecke an, nicht
irgendwo auf einer Kante. Gezogen wird sie aus sechs einzelnen Strichen
mit Ueberschuss, nicht als geschlossenes Polygon. */
function wabeEcken(cx, cy, r) {
const p = [];
for (let i = 0; i < 6; i++) {
const a = i * Math.PI / 3;
p.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]);
}
return p;
}
function wabe(cx, cy, r) {
const p = wabeEcken(cx, cy, r);
let d = "";
for (let i = 0; i < 6; i++) {
const q = p[i], w = p[(i + 1) % 6];
d += strich(q[0], q[1], w[0], w[1], { ue: 5, j: 1.8 });
}
return d;
}
/* ─── Die Mitte: das bestehende System ───────────────────────────────────
Bewusst als Liste gezeichnet, nicht als Kachel-Dashboard: so sieht eine
Warenwirtschaft aus. Und bewusst der schwerste Strich auf der Slide, denn
sie ist das, was bleibt. Das Fenster passt ganz in den Ring, sonst kreuzen
sich Ringbogen und Fensterecke, und weil der Grund durchscheinen soll gibt
es keine Fuellung, die das verdecken koennte. */
function nabe(cx, cy) {
const w = 216, h = 150, x = cx - w / 2, y = cy - h / 2, kopf = 40;
let d = rechteck(x, y, w, h, { ue: 4 });
d += strich(x, y + kopf, x + w, y + kopf, { ue: 3 });
let k = "";
[0, 1, 2].forEach(i => { k += kringel(x + 18 + i * 15, y + kopf / 2, 4.4, 4.4, { ueber: .1 }); });
let z = "";
for (let i = 0; i < 4; i++) {
const yy = y + kopf + 22 + i * 23;
z += strich(x + 17, yy, x + 59, yy, { j: 1, b: 1 });
z += strich(x + 72, yy, x + w - 48, yy, { j: 1, b: 1 });
z += strich(x + w - 38, yy, x + w - 17, yy, { j: 1, b: 1 });
}
return zug(d, { w: 3.6 }) + zug(k, { w: 2.4 }) + zug(z, { w: 2.2, op: .8 }) +
'<text x="' + cx + '" y="' + (y + kopf / 2 + 8) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="24" font-weight="800" ' +
'letter-spacing="-0.01em" fill="' + TINTE + '">HiveGrid</text>';
}
/* ─── Aufbau ───────────────────────────────────────────────────────────── */
/* ─── Slide 2: nur so gewachsen ──────────────────────────────────────────
Ein Knaeuel aus Kanaelen, durch das sich eine gestrichelte Leitung windet.
Bewusst ohne Aufloesung: das Gegenbild dazu ist Slide 5, und die Pointe
soll nicht zweimal vorkommen.
Die Namen sind allgemein gehalten, keine fremden Produkte. */
function glyBrief(cx, cy) {
const w = 30, h = 21, x = cx - w / 2, y = cy - h / 2;
return rechteck(x, y, w, h, { ue: 1.6 }) +
glatt([[x + 2, y + 3], [cx, y + h * 0.68], [x + w - 2, y + 3]], false);
}
function glyZettel(cx, cy) {
const w = 26, h = 32, x = cx - w / 2, y = cy - h / 2, e = 11;
return glatt([[x, y], [x + w - e, y], [x + w, y + e], [x + w, y + h],
[x, y + h], [x, y]], false) +
glatt([[x + w - e, y], [x + w - e, y + e], [x + w, y + e]], false);
}
function glyTabelle(cx, cy) {
const w = 30, h = 24, x = cx - w / 2, y = cy - h / 2;
let d = rechteck(x, y, w, h, { ue: 1.6 });
d += strich(x, y + h / 3, x + w, y + h / 3, { j: .7, b: .6 });
d += strich(x, y + h * 2 / 3, x + w, y + h * 2 / 3, { j: .7, b: .6 });
d += strich(x + w / 3, y, x + w / 3, y + h, { j: .7, b: .6 });
return d;
}
function glyHandy(cx, cy) {
const w = 20, h = 32, x = cx - w / 2, y = cy - h / 2;
return rechteck(x, y, w, h, { ue: 1.4 }) +
strich(x + 6, y + 4, x + w - 6, y + 4, { j: .6, b: .5 }) +
kringel(cx, y + h - 5, 1.9);
}
function glyOrdner(cx, cy) {
const w = 30, h = 26, x = cx - w / 2, y = cy - h / 2;
return rechteck(x, y + 6, w, h - 6, { ue: 1.4 }) +
glatt([[x + 1, y + 6], [x + 3, y], [x + 13, y], [x + 15, y + 6]], false);
}
/* Die kleinen Kacheln, die im Vorbild lose dazwischen schweben. */
function kachel(cx, cy, art, gr) {
const s = gr || 52, x = cx - s / 2, y = cy - s / 2;
let d = rechteck(x, y, s, s, { ue: 2.5 });
const f = s / 52;
if (art === "blatt") {
d += rechteck(x + 16 * f, y + 13 * f, 20 * f, 26 * f, { ue: 1.4 });
[20, 26, 32].forEach(o => {
d += strich(x + 20 * f, y + o * f, x + 32 * f, y + o * f, { j: .6, b: .5 });
});
} else if (art === "bild") {
d += rechteck(x + 13 * f, y + 15 * f, 26 * f, 22 * f, { ue: 1.4 });
d += glatt([[x + 16 * f, y + 33 * f], [x + 23 * f, y + 23 * f], [x + 29 * f, y + 30 * f],
[x + 32 * f, y + 26 * f], [x + 36 * f, y + 33 * f]], false);
} else {
d += kringel(cx, y + 19 * f, 6 * f);
d += glatt([[x + 14 * f, y + 40 * f], [x + 15 * f, y + 32 * f], [cx, y + 28 * f],
[x + s - 15 * f, y + 32 * f], [x + s - 14 * f, y + 40 * f]], false);
}
return d;
}
/* Eine ausholende Schlaufe zwischen zwei Ankern. Sie bauscht immer von der
Kachelspalte weg, damit die Leitung keine Beschriftung kreuzt: auf einem
durchscheinenden Grund kann kein Kasten sie verdecken. */
function schlaufe(ax, ay, bx, by, weite, seite) {
const dx = bx - ax, dy = by - ay, l = Math.hypot(dx, dy) || 1;
const nx = (dy / l) * seite, ny = (-dx / l) * seite;
return [
[ax, ay],
[ax + nx * weite * 0.62 + dx * 0.06, ay + ny * weite * 0.62 + dy * 0.06],
[(ax + bx) / 2 + nx * weite, (ay + by) / 2 + ny * weite],
[bx + nx * weite * 0.62 - dx * 0.06, by + ny * weite * 0.62 - dy * 0.06],
[bx, by],
];
}
/* Ein Stueck Leitung von Kasten zu Kasten.
Der erste Versuch legte die Stuetzpunkte direkt auf die Kastenkante und
liess die Bahn danach irgendwohin laufen. Das sah aus, als streife die
Linie den Kasten zufaellig, und wo zwei Punkte fast aufeinanderlagen,
warf die Kurve einen Zipfel. Beides zusammen war der Grund, warum die
Anbindung nicht stimmte.
Jetzt bekommt jedes Stueck vor dem Kasten einen Punkt auf gleicher Hoehe,
52 px daneben. Die Kurve laeuft dadurch waagerecht aus dem Kasten heraus
und waagerecht in den naechsten hinein: sie haengt sichtbar an ihm, statt
an ihm vorbeizukommen. */
function leitungsStueck(vonX, vonY, nachX, nachY, seite, abstand, zwischen) {
return [[vonX, vonY], [vonX + seite * abstand, vonY]]
.concat(zwischen)
.concat([[nachX + seite * abstand, nachY], [nachX, nachY]]);
}
function visProzesse() {
keim(20260813);
const W = 896, H = 730, HOCH = 62;
/* Die x-Werte sind so gewaehlt, dass die rechten Kanten dort landen, wo
die Leitung sie erwartet. Wer eine Beschriftung aendert, aendert die
Kastenbreite und muss die Ankerpunkte unten mitziehen. */
const chips = [
{ x: 34, y: 60, t: "E-Mails", g: glyBrief },
{ x: 104, y: 200, t: "Zettel im Auto", g: glyZettel },
{ x: 48, y: 340, t: "Tabellen", g: glyTabelle },
{ x: 128, y: 480, t: "Fotos auf dem Handy", g: glyHandy },
{ x: 60, y: 620, t: "Ordner im Regal", g: glyOrdner },
];
chips.forEach(c => {
c.w = Math.round(84 + c.t.length * 14.2);
c.r = c.x + c.w;
});
let raus = "";
// Rechte Kette: von rechter Kante zu rechter Kante, mit weiten Bogen.
const rechts = [
[[470, 34], [590, 72], [624, 150], [540, 186]],
[[576, 244], [672, 312], [604, 392], [492, 372], [432, 308]],
[[352, 398], [452, 424], [600, 406], [688, 470]],
[[664, 540], [708, 624], [606, 684], [478, 660]],
];
let leitung = "";
rechts.forEach((zw, i) => {
leitung += glatt(leitungsStueck(chips[i].r, chips[i].y, chips[i + 1].r,
chips[i + 1].y, 1, 52, zw)
.map(q => [q[0] + zit(2.5), q[1] + zit(2.5)]), false);
});
// Linke Kette: dort ist wenig Platz, also zwei knappe Bogen.
[[1, 2, [[24, 244], [14, 302]]], [3, 4, [[22, 522], [16, 576]]]].forEach(v => {
leitung += glatt(leitungsStueck(chips[v[0]].x, chips[v[0]].y, chips[v[1]].x,
chips[v[1]].y, -1, 34, v[2])
.map(q => [q[0] + zit(2), q[1] + zit(2)]), false);
});
/* Die Leitung traegt den Akzent. Gestrichelt und gelb heisst hier
gewachsen, durchgezogen und gelb auf Slide 5 heisst verbunden: dieselbe
Farbe, aber die Strichart trennt die beiden Aussagen. Etwas kraeftiger
als in Grau, weil eine unterbrochene Linie weniger Flaeche hat. */
raus += zug(leitung, { f: GELB, w: 2.9, op: .78, strichel: "11 13" });
[[700, 96, "blatt", 52], [770, 236, "bild", 46], [760, 430, "person", 50],
[250, 690, "blatt", 44]]
.forEach(k => { raus += zug(kachel(k[0], k[1], k[2], k[3]), { w: 2.5, op: .8 }); });
chips.forEach(c => {
const y = c.y - HOCH / 2;
raus += zug(chamfer(c.x, y, c.w, HOCH, 13), { w: 3.4 });
raus += zug(c.g(c.x + 34, c.y), { w: 2.6 });
raus += '<text x="' + (c.x + 64) + '" y="' + (c.y + 10) + '" ' +
'font-family="Manrope, sans-serif" font-size="27" font-weight="600" ' +
'letter-spacing="-0.01em" fill="' + TINTE + '">' + c.t + '</text>';
});
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
}
/* ─── Slide 1: Foto im gezeichneten Rahmen ───────────────────────────────
Das Foto bleibt roh, gezeichnet wird nur drumherum. Der Helm sitzt wie
die Krone beim Zeichenspiel auf der oberen rechten Ecke, angeschraegt und
halb ueber der Kante.
Er ist ganz in Gelb gezogen, Umriss wie Schraffur. Das hat einen Grund:
er liegt zur Haelfte auf dem hellen Grund und zur Haelfte auf dem dunklen
Foto. Eine Tintenlinie waere auf dem Foto verschwunden. Gelb traegt auf
beidem, und ein Bauhelm ist ohnehin gelb. */
function bauhelm(breite) {
const b = breite / 2, kd = b * 0.70, h = b * 1.12;
/* Zwei Anlaeufe waren noetig. Erst war die Kuppel flach und die Krempe
eine geschlossene Scheibe darunter: zusammen eine Schirmmuetze. Dann
kam Schraffur dazu, und die Waagerechten kreuzten die Grate zu einem
Gitter: ein Korb. Ein Bauhelm braucht wenige Striche, hoch und leer. */
const kuppel = glatt([[-kd, 2], [-kd * 1.0, -h * 0.4], [-kd * 0.6, -h * 0.85],
[0, -h], [kd * 0.6, -h * 0.85], [kd * 1.0, -h * 0.4],
[kd, 2]], false);
// Krempe: zwei Zuege, unten und oben, also eine Lippe mit Dicke.
const krempe = glatt([[-b, 1], [-b * 0.52, 13], [0, 16], [b * 0.52, 13], [b, 1]], false) +
glatt([[-b, 1], [-b * 0.52, 4], [0, 6], [b * 0.52, 4], [b, 1]], false);
// Der mittlere Grat traegt die Form, die beiden aeusseren geben Rundung.
let grate = glatt([[0, -h + 6], [0, -h * 0.45], [0, -1]], false);
[-1, 1].forEach(v => {
grate += glatt([[v * kd * 0.44, -h * 0.82], [v * kd * 0.62, -h * 0.42],
[v * kd * 0.68, -1]], false);
});
return zug(kuppel, { f: GELB, w: 5 }) +
zug(grate, { f: GELB, w: 2.4, op: .6 }) +
zug(krempe, { f: GELB, w: 5 });
}
function visKarte() {
keim(20260811);
const W = 712, H = 971;
// Das Foto liegt bei x 46..666 und y 150..925. Der Rahmen laeuft 16 px
// aussen herum, zweimal gezogen: einmal fest, einmal als leichter
// Nachzieher, wie beim Nachfahren einer Kante.
/* Drei Sachen zusammen: die Markenform, ein versetzter Zweitzug und eine
schraffierte Kante. Sie greifen ineinander statt nebeneinanderher zu
laufen, denn der versetzte Rahmen ist die Aussenkante des Schattens und
die Schraffur fuellt das Band dazwischen. So ist es eine Geste und
nicht dreimal dasselbe uebereinander.
Zur Schattenfrage: rules.md Paragraf 5 verbietet Schatten, meint damit
aber weichgezeichnete. Die dort notierte Ausnahme sagt, eine Linie ist
kein Schatten. Eine Schraffur besteht aus Linien. */
let band = "";
for (let y = 178; y < 928; y += 13) band += strich(683, y, 699, y + 16, { j: .7, b: .7 });
for (let x = 72; x < 666; x += 13) band += strich(x, 942, x + 16, 958, { j: .7, b: .7 });
const raus =
zug(chamfer(48, 152, 652, 807, 30, { ue: 5 }), { w: 3, op: .38 }) +
zug(band, { w: 1.9, op: .34 }) +
zug(chamfer(30, 134, 652, 807, 30, { ue: 9 }), { w: 6 }) +
'<g transform="translate(562 154) rotate(-14)">' + bauhelm(196) + '</g>';
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg" class="karte-skizze">' + raus + '</svg>';
}
/* ─── Slide 3: wechseln kannst du vergessen ──────────────────────────────
Der Fliesstext hat zwei Haelften, und beide brauchen ein Bild: "Jahre an
Daten und Wissen haengen drin" wird zu Wurzeln im Boden, "am Ende
dasselbe, nur woanders" zu demselben Kasten noch einmal, nur gestrichelt.
Dazwischen eine gerissene Leine.
Das Kreuz der alten Fassung lag auf dem Pfeil und nicht auf dem Ziel, aus
gutem Grund: nicht das neue System scheitert, sondern der Wechsel. Das
bleibt so. Aus dem Kreuz ist eine gerissene Leine geworden und aus dem
leeren Ziel ein gleiches, denn leer war die falsche Aussage. */
function wurzeln(x, w, oben, tief) {
let raus = "";
const n = 8;
for (let i = 0; i < n; i++) {
const t = (i + 0.5) / n;
const sx = x + w * (0.07 + 0.86 * t);
const laenge = tief * (0.5 + rnd() * 0.5);
const drift = (t - 0.5) * 165 + zit(26);
raus += zug(glatt([[sx, oben],
[sx + drift * 0.18 + zit(9), oben + laenge * 0.3],
[sx + drift * 0.62 + zit(11), oben + laenge * 0.66],
[sx + drift, oben + laenge]], false),
{ w: 3.1 - Math.abs(t - 0.5) * 1.7 });
const az = oben + laenge * 0.5, ax = sx + drift * 0.28;
const seite = t < 0.5 ? -1 : 1;
raus += zug(glatt([[ax, az],
[ax + seite * 20 + zit(7), az + laenge * 0.2],
[ax + seite * 33 + zit(9), az + laenge * 0.33]], false), { w: 1.8 });
}
return raus;
}
/* Erdreich: ungleich lange Schraegstriche. Die gleichmaessige Schraffur aus
schraff() sah nach technischer Zeichnung aus, nicht nach Boden. */
function erdreich(x1, x2, y) {
let d = "";
for (let x = x1; x < x2; x += 13 + rnd() * 6) {
const l = 11 + rnd() * 13;
d += strich(x, y + 2, x - l * 0.55, y + 2 + l, { j: .8, b: .8 });
}
return d;
}
function visWechseln() {
keim(20260815);
const W = 896, H = 730, GRAU = "#A9AFB7";
const BY = 100, BH = 360, BW = 320, LX = 30, RX = 555;
const zeilen = ["Stammdaten", "Aufträge", "Historie", "Wissen im Kopf"];
const mitte = BY + BH / 2;
const inhalt = (x, farbe) => zeilen.map((z, i) =>
'<text x="' + (x + 60) + '" y="' + (BY + 96 + i * 66) + '" ' +
'font-family="Manrope, sans-serif" font-size="28" font-weight="600" ' +
'letter-spacing="-0.01em" fill="' + farbe + '">' + z + '</text>').join("");
const striche = x => zeilen.map((z, i) =>
strich(x + 28, BY + 87 + i * 66, x + 48, BY + 87 + i * 66, { j: .8, b: .8 })).join("");
let raus = "";
// Der Bestand: schwerer Strich, doppelte Unterkante.
raus += zug(rechteck(LX, BY, BW, BH, { ue: 4.5 }), { w: 3.8 });
raus += zug(strich(LX + 6, BY + BH + 6, LX + BW - 6, BY + BH + 6, { ue: 2 }), { w: 3 });
raus += zug(striche(LX), { w: 2.6 });
/* Die Erdoberflaeche liegt dicht unter dem Kasten, nicht am unteren Rand.
Im ersten Wurf lag sie 120 px tiefer, dadurch endeten die Wurzeln in der
Luft und sahen aus wie Beine. Sie muessen die Linie kreuzen, sonst
stecken sie nicht drin. */
const boden = BY + BH + 18;
raus += zug(strich(0, boden, 500, boden, { ue: 4 }), { w: 3 });
raus += zug(erdreich(6, 494, boden), { w: 1.9, op: .45 });
raus += wurzeln(LX, BW, BY + BH + 6, 142);
/* Die Leine reisst, und der Akzent sitzt genau dort, wo die Aussage sitzt.
Entscheidend ist, dass beide Enden nach unten haengen und die Fasern in
dieselbe Richtung weiter ausfransen. Im ersten Wurf zeigten sie
aufeinander zu: das ergab zwei Pfeilspitzen, also die gegenteilige
Aussage. Eine gerissene Leine faellt, sie zielt nicht. */
const endL = [418, mitte + 62], endR = [496, mitte + 48];
raus += zug(glatt([[LX + BW, mitte - 8], [388, mitte + 2], [412, mitte + 28],
endL], false), { f: GELB, w: 4.4 });
raus += zug(glatt([[RX, mitte - 8], [528, mitte + 0], [502, mitte + 22],
endR], false), { f: GELB, w: 4.4 });
let fransen = "";
[endL, endR].forEach(e => {
[-0.42, -0.05, 0.36].forEach(o => {
fransen += strich(e[0], e[1] - 4,
e[0] + Math.sin(o) * 26, e[1] + Math.cos(o) * 26, { j: .9, b: 1.1 });
});
});
raus += zug(fransen, { f: GELB, w: 2.8 });
// Dasselbe noch einmal, nur woanders: gleiche Groesse, gleiche Zeilen.
raus += zug(rechteck(RX, BY, BW, BH, { ue: 3 }), { w: 3, op: .6, strichel: "14 13", f: GRAU });
raus += zug(striche(RX), { w: 2.4, f: GRAU });
raus += inhalt(LX, TINTE) + inhalt(RX, GRAU);
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
}
/* ─── Slide 4: wir wollten mehr ──────────────────────────────────────────
Die alte Fassung zeigte vier Kaesten mit "fehlt". Das ist der Mangel, die
Slide handelt aber vom Wunsch. Also ein Blatt mit einer Liste.
Die Punkte nennen nicht noch einmal KI, CRM und Automatisierung, das steht
schon im Fliesstext darueber. Sie sagen dasselbe in den Worten eines
Betriebs. Die Kaestchen bleiben leer: erfuellt wird der Wunsch erst auf
Slide 5, und genau davon lebt der Uebergang.
Gelb tragen die Kaestchen. Es ist der einzige Akzent hier und bleibt damit
bei seiner Rolle aus den anderen Slides: Struktur, keine Verzierung. */
function visMehr() {
keim(20260816);
const W = 896, H = 730;
const LX = 60, RX = 800, OY = 70, UY = 650, KNICK = 58;
const wuensche = [
"Angebot schreibt sich selbst",
"Kunde sieht seinen Stand",
"Nichts doppelt eintippen",
"Alles redet miteinander",
];
// Blatt mit abgeknickter Ecke unten rechts.
const blatt = strich(LX, OY, RX, OY, { ue: 4 }) +
strich(RX, OY, RX, UY - KNICK, { ue: 3 }) +
strich(RX, UY - KNICK, RX - KNICK, UY, { ue: 2 }) +
strich(RX - KNICK, UY, LX, UY, { ue: 4 }) +
strich(LX, UY, LX, OY, { ue: 4 });
const knick = glatt([[RX, UY - KNICK], [RX - KNICK + 8, UY - KNICK + 12],
[RX - KNICK, UY]], false);
let raus = zug(blatt, { w: 3.4 }) + zug(knick, { w: 2.6, op: .75 });
wuensche.forEach((t, i) => {
const y = 180 + i * 120;
raus += zug(strich(LX + 60, y + 26, RX - 60, y + 26, { j: 1, b: 1 }), { w: 2, op: .22 });
raus += zug(rechteck(LX + 60, y - 30, 38, 38, { ue: 3 }), { f: GELB, w: 3.4 });
raus += '<text x="' + (LX + 126) + '" y="' + y + '" font-family="Manrope, sans-serif" ' +
'font-size="32" font-weight="600" letter-spacing="-0.015em" fill="' + TINTE + '">' +
t + '</text>';
});
/* Leicht schief aufs Blatt gelegt. Genau senkrecht wirkt gesetzt, und ein
Zettel liegt nie gerade. */
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' +
'<g transform="rotate(-1.3 448 360)">' + raus + '</g></svg>';
}
/* ─── Slide 6: was nach der Anmeldung passiert ───────────────────────────
Uebernahme der Sektion "So geht\'s weiter" von der Website, in derselben
Handschrift wie Slide 5. Drei Schritte an einer Leitung, Wabe statt Kreis,
der dritte Schritt blass: er hat noch nicht stattgefunden. */
function visWarteliste() {
keim(20260814);
const W = 896, H = 730, HX = 66, R = 54, TX = 176, HOEH = R * 0.866;
const schritte = [
{ y: 112, titel: "Du trägst dich ein", ikon: ikonBrief,
zeilen: ["Eine E-Mail genügt. Kein Vertrag,", "keine Kreditkarte, unverbindlich."] },
{ y: 352, titel: "Wir melden uns", ikon: ikonBlaseKlein,
zeilen: ["Sobald HiveGrid für deinen Betrieb", "bereit ist, persönlich und ohne Spam."] },
{ y: 592, titel: "Du bist von Anfang an dabei", ikon: ikonKonfetti,
zeilen: ["Unter den Ersten, die HiveGrid", "nutzen und mitgestalten."] },
];
const txt = (x, y, groesse, gewicht, farbe, sperr, inhalt) =>
'<text x="' + x + '" y="' + y + '" font-family="Manrope, sans-serif" font-size="' +
groesse + '" font-weight="' + gewicht + '" letter-spacing="' + sperr +
'" fill="' + farbe + '">' + inhalt + '</text>';
let raus = "";
// Von Wabe zu Wabe ein Pfeil. Er endet kurz vor der naechsten Wabe, sonst
// laeuft die Spitze in die Kante und ist nicht mehr als Spitze zu lesen.
let pfeile = "";
for (let i = 0; i < schritte.length - 1; i++) {
pfeile += pfeil(HX, schritte[i].y + HOEH + 8, HX, schritte[i + 1].y - HOEH - 9);
}
raus += zug(pfeile, { f: GELB, w: 3.8 });
schritte.forEach(sc => {
raus += zug(wabeSchraff(HX, sc.y, R, 11), { f: GELB, w: 2, op: .5 });
raus += zug(wabe(HX, sc.y, R), { f: GELB, w: 4 });
raus += sc.ikon(HX, sc.y, TINTE);
raus += txt(TX, sc.y - 26, 40, 800, TINTE, "-0.02em", sc.titel);
sc.zeilen.forEach((z, k) => {
raus += txt(TX, sc.y + 24 + k * 34, 26, 500, "#555555", "0", z);
});
});
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
}
function visVerbinden() {
keim(20260812);
const W = 896, H = 730, cx = 448, cy = 350, R = 160;
const RX = 352, RY = 300;
// Die Winkel sind nicht frei gewaehlt: sie sind die sechs Eckwinkel der
// Wabe. Dadurch sitzt jeder Knoten auf einer Ecke.
const sats = [
{ a: 240, name: "KI", ikon: ikonKi },
{ a: 300, name: "Chat", ikon: ikonChat },
{ a: 0, name: "Mitarbeiter", ikon: ikonCrm },
{ a: 60, name: "Automation", ikon: ikonAuto },
{ a: 120, name: "Projekte", ikon: ikonBoard },
{ a: 180, name: "Formulare", ikon: ikonFormular },
];
let speichen = "", knoten = "", bilder = "", schrift = "";
sats.forEach(s => {
const r = s.a * Math.PI / 180;
const sx = cx + Math.cos(r) * RX, sy = cy + Math.sin(r) * RY;
const ix = cx + Math.cos(r) * R, iy = cy + Math.sin(r) * R; // Ecke der Wabe
const vx = sx - ix, vy = sy - iy, vl = Math.hypot(vx, vy) || 1;
const ux = vx / vl, uy = vy / vl;
const ax = sx - ux * (s.ikon.rad + 20), ay = sy - uy * (s.ikon.rad + 20);
speichen += strich(ix + ux * 12, iy + uy * 12, ax - ux * 11, ay - uy * 11, { j: 1.2 });
knoten += kringel(ix, iy, 10.5, 10.5, { ueber: .1 }) + kringel(ax, ay, 10.5, 10.5, { ueber: .1 });
bilder += s.ikon(sx, sy);
schrift += '<text x="' + n1(sx) + '" y="' + n1(sy + s.ikon.unten) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="28" font-weight="700" ' +
'letter-spacing="-0.01em" fill="' + TINTE + '">' + s.name + '</text>';
});
// Die Wabe wird zweimal gezogen. Ein zweiter Umriss auf groesserem Radius
// sah nach Versehen aus, derselbe Radius nach zweitem Zug.
// Breite und Hoehe stehen als Attribute mit dran, nicht nur im viewBox.
// Ohne sie hat Chrome die Hoehe nicht aus dem Seitenverhaeltnis abgeleitet,
// das SVG wurde zu hoch und die Zeichnung lief in den Fliesstext.
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' +
bilder + schrift +
zug(speichen, { f: GELB, w: 4.4 }) +
zug(wabe(cx, cy, R), { f: GELB, w: 4.8 }) +
zug(wabe(cx, cy, R - 2), { f: GELB, w: 3.2, op: .72 }) +
zug(knoten, { f: GELB, w: 3.8 }) +
nabe(cx, cy) +
'</svg>';
}
/* ─── Helm-Entwuerfe ──────────────────────────────────────────────────────
Alle in Gelb, weil der Helm im Post zur Haelfte auf hellem Grund und zur
Haelfte auf dem dunklen Foto liegt. Eine Tintenlinie waere dort weg. */
/* H2: mehr Stuetzpunkte, First, Nebenrippen, Krempe mit Dicke, Schattierung. */
function bauhelmFein(breite) {
const b = breite / 2, kd = b * 0.76, h = b * 1.0;
const kuppel = glatt([
[-kd, 2], [-kd * 1.03, -h * 0.28], [-kd * 0.93, -h * 0.58],
[-kd * 0.62, -h * 0.86], [-kd * 0.22, -h * 0.99], [kd * 0.22, -h * 0.99],
[kd * 0.62, -h * 0.86], [kd * 0.93, -h * 0.58], [kd * 1.03, -h * 0.28], [kd, 2],
], false);
const first = glatt([[-kd * 0.04, -h * 0.99], [0, -h * 0.62],
[kd * 0.03, -h * 0.22], [kd * 0.04, 1]], false);
let rippen = "";
[-1, 1].forEach(v => {
rippen += glatt([[v * kd * 0.36, -h * 0.92], [v * kd * 0.58, -h * 0.56],
[v * kd * 0.68, -h * 0.17], [v * kd * 0.7, 1]], false);
});
const krempeU = glatt([[-b, 4], [-b * 0.62, 16], [0, 21], [b * 0.62, 16], [b, 4]], false);
const krempeO = glatt([[-b, 4], [-b * 0.62, 7], [0, 10], [b * 0.62, 7], [b, 4]], false);
let schatten = "";
for (let i = 0; i < 4; i++) {
const f = 0.94 - i * 0.1;
schatten += glatt([[-kd * f, -h * 0.16], [-kd * f * 0.94, -h * 0.46],
[-kd * f * 0.7, -h * 0.72]], false);
}
return zug(schatten, { f: GELB, w: 2.1, op: .45 }) + zug(kuppel, { f: GELB, w: 5 }) +
zug(first, { f: GELB, w: 3, op: .8 }) + zug(rippen, { f: GELB, w: 2.3, op: .55 }) +
zug(krempeU, { f: GELB, w: 5 }) + zug(krempeO, { f: GELB, w: 3.4, op: .85 });
}
/* H3: gefuellt ueber Schraffur, dafuer ohne Rippen. Schraffur und Rippen
zusammen ergaben in einem frueheren Versuch ein Gitter, also einen Korb. */
function bauhelmSchraffiert(breite) {
const b = breite / 2, kd = b * 0.76, h = b * 1.0;
const kuppel = glatt([
[-kd, 2], [-kd * 1.03, -h * 0.28], [-kd * 0.93, -h * 0.58],
[-kd * 0.62, -h * 0.86], [-kd * 0.22, -h * 0.99], [kd * 0.22, -h * 0.99],
[kd * 0.62, -h * 0.86], [kd * 0.93, -h * 0.58], [kd * 1.03, -h * 0.28], [kd, 2],
], false);
const krempeU = glatt([[-b, 4], [-b * 0.62, 16], [0, 21], [b * 0.62, 16], [b, 4]], false);
const krempeO = glatt([[-b, 4], [-b * 0.62, 7], [0, 10], [b * 0.62, 7], [b, 4]], false);
let fuellung = "";
for (let y = -h + 15; y < -8; y += 13) {
const halb = kd * Math.sqrt(Math.max(0, 1 - (y / h) * (y / h))) - 9;
if (halb < 8) continue;
fuellung += strich(-halb, y - 2, halb, y + 2, { j: .8, b: .9 });
}
return zug(fuellung, { f: GELB, w: 2.1, op: .5 }) + zug(kuppel, { f: GELB, w: 5 }) +
zug(krempeU, { f: GELB, w: 5 }) + zug(krempeO, { f: GELB, w: 3.4, op: .85 });
}
/* H4: Dreiviertelansicht. Die Krempe ist eine Ellipse statt einer Linie,
dadurch sieht man ihre Unterseite und der Helm bekommt Tiefe. Der hintere
Bogen wird nur dort gezogen, wo ihn die Kuppel nicht verdeckt. */
function bauhelmDreiviertel(breite) {
const b = breite / 2, ry = b * 0.23, kd = b * 0.74, h = b * 0.98;
const bogen = (von, bis) => {
const p = [];
for (let i = 0; i <= 10; i++) {
const a = von + (bis - von) * (i / 10);
p.push([Math.cos(a) * b, Math.sin(a) * ry]);
}
return glatt(p, false);
};
const vorn = bogen(0, Math.PI); // untere Haelfte
const hinten = bogen(Math.PI + 0.28, Math.PI + 0.72) +
bogen(-0.72, -0.28); // nur die freien Stuecke
const kuppel = glatt([
[-kd, -ry * 0.5], [-kd * 1.0, -h * 0.34], [-kd * 0.84, -h * 0.68],
[-kd * 0.42, -h * 0.95], [0, -h], [kd * 0.46, -h * 0.93],
[kd * 0.86, -h * 0.64], [kd * 1.0, -h * 0.3], [kd, -ry * 0.5],
], false);
const first = glatt([[-kd * 0.5, -h * 0.78], [0, -h * 0.98],
[kd * 0.5, -h * 0.76], [kd * 0.86, -h * 0.4]], false);
return zug(kuppel, { f: GELB, w: 5 }) + zug(first, { f: GELB, w: 2.6, op: .7 }) +
zug(hinten, { f: GELB, w: 2.6, op: .55 }) + zug(vorn, { f: GELB, w: 5 });
}
/* H5: wie H2, dazu ein Kinnriemen mit Schnalle. */
function bauhelmRiemen(breite) {
const b = breite / 2;
const riemen = glatt([[-b * 0.82, 12], [-b * 0.68, b * 0.52], [-b * 0.2, b * 0.78],
[b * 0.3, b * 0.7], [b * 0.76, b * 0.3], [b * 0.84, 12]], false);
const schnalle = rechteck(-b * 0.16, b * 0.62, b * 0.3, b * 0.17, { ue: 1.5 });
return bauhelmFein(breite) + zug(riemen, { f: GELB, w: 2.8, op: .8 }) +
zug(schnalle, { f: GELB, w: 2.4, op: .8 });
}
/* H6: nur die Silhouette, zwei kraeftige Zuege, sonst nichts. */
function bauhelmKontur(breite) {
const b = breite / 2, kd = b * 0.76, h = b * 1.02;
const kuppel = glatt([
[-kd, 2], [-kd * 1.02, -h * 0.3], [-kd * 0.9, -h * 0.62],
[-kd * 0.5, -h * 0.92], [0, -h], [kd * 0.5, -h * 0.92],
[kd * 0.9, -h * 0.62], [kd * 1.02, -h * 0.3], [kd, 2],
], false);
const krempe = glatt([[-b, 3], [-b * 0.6, 15], [0, 20], [b * 0.6, 15], [b, 3]], false);
return zug(kuppel, { f: GELB, w: 6.5 }) + zug(krempe, { f: GELB, w: 6.5 });
}
/* Der gewaehlte Rahmen, gleich aufgebaut wie in post2.html. */
function rahmenMix() {
let band = "";
for (let y = 178; y < 928; y += 13) band += strich(683, y, 699, y + 16, { j: .7, b: .7 });
for (let x = 72; x < 666; x += 13) band += strich(x, 942, x + 16, 958, { j: .7, b: .7 });
return zug(chamfer(48, 152, 652, 807, 30, { ue: 5 }), { w: 3, op: .38 }) +
zug(band, { w: 1.9, op: .34 }) +
zug(chamfer(30, 134, 652, 807, 30, { ue: 9 }), { w: 6 });
}
document.getElementById("r0").innerHTML = (function(){ keim(20260811); return zug(rechteck(30,134,652,807,{ue:7}),{w:3.8}) + zug(rechteck(31,136,650,804,{ue:5}),{w:2.4,op:.4}); })();
document.getElementById("r1").innerHTML = (function(){ keim(20260811); return zug(rechteck(34,138,644,799,{ue:10}),{w:7}); })();
document.getElementById("r2").innerHTML = (function(){ keim(20260811); return zug(rechteck(44,148,652,807,{ue:6}),{w:3.2,op:.34}) + zug(rechteck(30,134,652,807,{ue:9}),{w:6}); })();
document.getElementById("r3").innerHTML = (function(){ keim(20260811); return zug(rechteck(12,116,688,843,{ue:6}),{w:2.6,op:.5}) + zug(rechteck(34,138,644,799,{ue:9}),{w:6}); })();
document.getElementById("r4").innerHTML = (function(){ keim(20260811); return zug(chamfer(30,134,652,807,30,{ue:8}),{w:6}); })();
document.getElementById("r5").innerHTML = (function(){ keim(20260811); return zug(rechteck(30,134,652,807,{ue:9}),{w:6}) + zug(schraff(684,152,26,796,11),{w:1.9,op:.34}) + zug(schraff(52,943,650,26,11),{w:1.9,op:.34}); })();
document.getElementById("r6").innerHTML = (function(){ keim(20260811); return rahmenMix(); })();
document.getElementById("k0").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelm(200) + '</g>'; })();
document.getElementById("k1").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelmFein(200) + '</g>'; })();
document.getElementById("k2").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelmSchraffiert(200) + '</g>'; })();
document.getElementById("k3").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelmDreiviertel(200) + '</g>'; })();
document.getElementById("k4").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelmRiemen(200) + '</g>'; })();
document.getElementById("k5").innerHTML = (function(){ keim(20260811); return '<g transform="translate(150 168)">' + bauhelmKontur(200) + '</g>'; })();
document.getElementById("f0").innerHTML = (function(){ keim(20260811); return rahmenMix() + '<g transform="translate(562 154) rotate(-14)">' + bauhelmFein(196) + '</g>'; })();
document.getElementById("f1").innerHTML = (function(){ keim(20260811); return rahmenMix() + '<g transform="translate(391 322) rotate(-6)">' + bauhelmFein(186) + '</g>'; })();
document.getElementById("f2").innerHTML = (function(){ keim(20260811); return rahmenMix() + '<g transform="translate(690 168) rotate(28)">' + bauhelmFein(196) + '</g>'; })();
document.getElementById("f3").innerHTML = (function(){ keim(20260811); return rahmenMix() + '<g transform="translate(386 350) rotate(-4)">' + bauhelmFein(222) + '</g>'; })();
</script>
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env node
/* Baut aus einer animierten Slide ein MP4 fuers Carousel.
*
* node animation.mjs post6.html alle animierten Slides
* node animation.mjs post6.html 5 nur Slide 6 (nullbasiert: 5)
* node animation.mjs post9.html animWebinar 1x1 im Format 1:1
*
* WARUM MP4 UND NICHT GIF
* Instagram nimmt kein GIF an. Was im Feed als "kurzer animierter Beitrag"
* laeuft und trotzdem kein Reel ist, ist ein Video in einem Carousel: Meta
* schreibt "Carousels are limited to 10 images, videos, or a mix of the two",
* und ein Video darin bleibt ein Carousel-Element. Nur ein einzeln gepostetes
* Video wird seit 2022 zum Reel.
*
* DIE WERTE
* Behaelter MP4, H.264 High, yuv420p
* Bild 1080 x 1350, also 4:5 wie unsere Standbilder -- ein Carousel
* schneidet alle Elemente auf das Format des ersten zu.
* Kennt die Seite ein zweites Format (window.SLIDE_FORMAT, bisher
* nur Post 9 mit 1:1 fuer die Anzeige), gilt dessen Hoehe. Das
* Format kommt aus dem Projektstand oder als drittes Argument,
* die Datei traegt dann ein Kennzeichen (-1x1).
* Bildrate 30 fps
* Laenge 3 s ist das Minimum, 60 s je Element das Maximum; wir liegen
* bei 5
* Datenrate 12 Mbit/s. Instagram rechnet ohnehin neu; was man hineingibt,
* ist die Obergrenze dessen, was herauskommen kann
* Ton eine stille AAC-Spur. Ein Video ganz ohne Tonspur wird von
* manchen Uploadwegen abgelehnt, und still ist es so oder so
* faststart der moov-Atom nach vorn, sonst laedt das Video erst, wenn es
* ganz da ist
*
* Die Bilder kommen aus derselben Seite wie die Standbilder, ueber dieselbe
* Tuer wie die Vorschau (window.animSetzen). Was im Browser laeuft, ist also
* das, was im Video landet.
*/
import { spawn } from "node:child_process";
import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
const SEITE = process.argv[2] || "post6.html";
/* Der Schluessel der Animation, nicht ihre Nummer. Eine Animation ist keine
Slide mehr und hat deshalb auch keine Position im Streifen, die sich
verschieben koennte. Ohne Angabe werden alle des Projekts gebaut. */
const NUR = process.argv[3] || null;
/* Format, wenn die Seite mehrere kennt. Ohne Angabe gilt der Projektstand. */
const FORMAT = process.argv[4] || null;
const ZUSATZ = FORMAT ? "&format=" + encodeURIComponent(FORMAT) : "";
const PROJEKT = SEITE.replace(/\.html$/, "");
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const PORT = 9530;
const FPS = 30;
const BREITE = 1080;
let HOEHE = 1350, DATEI_ZUSATZ = "";
const DATENRATE = "12M";
const ZIEL = path.join(process.cwd(), "animation");
/* ── CDP, das Noetigste ──────────────────────────────────────────────────── */
async function verbinden(port) {
let ziele = null;
for (let i = 0; i < 60; i++) {
ziele = await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()).catch(() => null);
if (ziele && ziele.some(t => t.type === "page")) break;
await new Promise(r => setTimeout(r, 200));
}
const seite = ziele && ziele.find(t => t.type === "page");
if (!seite) throw new Error("Chrome antwortet nicht auf " + port);
const ws = new WebSocket(seite.webSocketDebuggerUrl);
const offen = new Map(), horcher = [];
ws.onmessage = e => {
const m = JSON.parse(e.data);
if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id); }
else if (m.method) horcher.forEach(h => h(m));
};
await new Promise((ok, weg) => { ws.onopen = ok; ws.onerror = weg; });
let n = 0;
const ruf = (method, params = {}) => new Promise((ok, weg) => {
const id = ++n;
offen.set(id, m => m.error ? weg(new Error(method + ": " + m.error.message)) : ok(m.result));
ws.send(JSON.stringify({ id, method, params }));
});
const werte = async ausdruck => {
const r = await ruf("Runtime.evaluate", { expression: ausdruck, awaitPromise: true, returnByValue: true });
if (r.exceptionDetails) throw new Error("in der Seite: " +
(r.exceptionDetails.exception?.description || r.exceptionDetails.text));
return r.result.value;
};
return { ruf, werte, zu: () => ws.close() };
}
/* ── ffmpeg ──────────────────────────────────────────────────────────────── */
function kodieren(ordner, datei, bilder) {
return new Promise((ok, weg) => {
const f = spawn("ffmpeg", [
"-y", "-hide_banner", "-loglevel", "error",
"-framerate", String(FPS),
"-i", path.join(ordner, "%05d.png"),
// Eine stille Tonspur dazu, sonst mag mancher Uploadweg die Datei nicht.
"-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100",
"-shortest",
"-c:v", "libx264", "-profile:v", "high", "-level", "4.1",
"-pix_fmt", "yuv420p",
"-b:v", DATENRATE, "-maxrate", DATENRATE, "-bufsize", "24M",
// Ein Schluesselbild je Sekunde: die Schleife setzt sauber wieder an.
"-g", String(FPS), "-keyint_min", String(FPS), "-sc_threshold", "0",
"-c:a", "aac", "-b:a", "128k", "-ar", "44100",
"-movflags", "+faststart",
datei,
], { stdio: ["ignore", "inherit", "inherit"] });
f.on("exit", c => c === 0 ? ok() : weg(new Error("ffmpeg endete mit " + c)));
});
}
/* ── Ablauf ──────────────────────────────────────────────────────────────── */
const chrome = spawn(CHROME, [
"--headless", "--disable-gpu", "--hide-scrollbars",
"--remote-debugging-port=" + PORT,
"--user-data-dir=" + path.join(tmpdir(), "postgrid-anim"),
"about:blank",
], { stdio: "ignore" });
let cdp;
try {
cdp = await verbinden(PORT);
await cdp.ruf("Page.enable");
await cdp.ruf("Runtime.enable");
/* Das Fenster hat genau die Groesse eines Blattes. Im Modus ?nur=anim
zeigt die Seite allein die Animation, an 0,0 -- damit entfaellt jede
Ausschnittsrechnung, und mit ihr die Falle, dass ein zu schmales Fenster
fuer eine hintere Seite ein leeres Bild liefert. */
await cdp.ruf("Emulation.setDeviceMetricsOverride",
{ width: BREITE, height: HOEHE, deviceScaleFactor: 1, mobile: false });
const laden = async adresse => {
const antwort = await cdp.ruf("Page.navigate", { url: "http://127.0.0.1:4321/" + adresse });
/* Laeuft PostGrid nicht auf 4321, laedt die Seite gar nicht. Ohne diese
Pruefung hiess das spaeter nur "kennt keine Animationen". */
if (antwort && antwort.errorText) {
throw new Error("PostGrid ist unter http://127.0.0.1:4321/ nicht erreichbar (" +
antwort.errorText + "). PostGrid mit ./serve.sh starten und den Post dort oeffnen.");
}
await new Promise(r => setTimeout(r, 4000));
};
// Erst die Liste holen. Ohne k rendert der Modus nichts, das Skript laeuft
// aber vollstaendig durch und window.ANIM_LISTE steht bereit.
await laden(SEITE + "?nur=anim" + ZUSATZ);
const alle = await cdp.werte("window.ANIM_LISTE ? window.ANIM_LISTE() : null");
/* Das Format der Seite. Posts ohne zweites Format melden nichts und
bleiben bei 4:5. */
const fmt = await cdp.werte("window.SLIDE_FORMAT ? window.SLIDE_FORMAT() : null");
if (fmt) {
HOEHE = fmt.hoehe; DATEI_ZUSATZ = fmt.datei || "";
await cdp.ruf("Emulation.setDeviceMetricsOverride",
{ width: BREITE, height: HOEHE, deviceScaleFactor: 1, mobile: false });
}
if (!alle) throw new Error(SEITE + " kennt keine Animationen (window.ANIM_LISTE fehlt)");
const liste = alle.filter(a => NUR == null || a.key === NUR);
if (!liste.length) throw new Error("keine Animation" +
(NUR ? ' "' + NUR + '"' : "") + " in " + SEITE +
(alle.length ? " — vorhanden: " + alle.map(a => a.key).join(", ") : ""));
await mkdir(ZIEL, { recursive: true });
for (const a of liste) {
const anzahl = Math.round(a.dauer * FPS);
await laden(SEITE + "?nur=anim&k=" + encodeURIComponent(a.key) + ZUSATZ);
/* Nachsehen, dass wirklich ein Blatt dasteht und es die richtigen Masse
hat. Ein leeres Fenster faellt sonst erst am fertigen Video auf. */
const kasten = await cdp.werte(`(() => {
const el = document.querySelector(".slide[data-animi]");
if (!el) return null;
const r = el.getBoundingClientRect();
return { x: Math.round(r.left), y: Math.round(r.top),
w: Math.round(r.width), h: Math.round(r.height) };
})()`);
if (!kasten) throw new Error(`${a.key}: kein Blatt im Modus ?nur=anim`);
if (kasten.w !== BREITE || kasten.h !== HOEHE)
throw new Error(`${a.key} misst ${kasten.w}x${kasten.h}, erwartet ${BREITE}x${HOEHE}`);
if (kasten.x !== 0 || kasten.y !== 0)
throw new Error(`${a.key} sitzt bei ${kasten.x},${kasten.y} statt 0,0`);
/* Dieselbe Umlautpruefung wie der Bildexport. Das Bewegtbild laeuft nicht
durch renderSlide und waere sonst die Luecke: auf einer Slide faellt
"Sanitaer" beim Export auf, in 180 Bildern niemandem.
Ueber den ganzen Ablauf und nicht nur einmal: der Text einer Animation
aendert sich mit t. Beim ersten Versuch lief die Pruefung nur auf dem
Anfangsbild und fand nichts -- die fehlerhaften Optionen erscheinen
erst ab t = 0,4. Vierundzwanzig Stellen decken jeden Zustand ab und
kosten zusammen weniger als eine Sekunde. */
const gesehen = new Set();
for (let p = 0; p < 24; p++) {
await cdp.werte(`window.animSetzen(${a.i}, ${p / 24})`);
const f = await cdp.werte("window.TEXT_PRUEFUNG ? window.TEXT_PRUEFUNG() : []");
(f || []).forEach(w => gesehen.add(w));
}
if (gesehen.size)
throw new Error(`${a.key}: ${[...gesehen].join(", ")}. ` +
"Fehlt ein Umlaut, schreib ihn; ist das Wort so richtig, gehoert es in " +
"UMLAUT_ERLAUBT. Einen Geviertstrich ersetzt ein Punkt oder ein Komma.");
const ordner = await mkdtemp(path.join(tmpdir(), "postgrid-bilder-"));
process.stdout.write(` ${a.key}: ${anzahl} Bilder, ${BREITE} x ${HOEHE} `);
for (let n = 0; n < anzahl; n++) {
/* Das letzte Bild ist nicht t = 1, sondern eins davor: bei einer
Schleife waeren t = 0 und t = 1 dasselbe Bild und es haekelte kurz. */
await cdp.werte(`window.animSetzen(${a.i}, ${n / anzahl})`);
const r = await cdp.ruf("Page.captureScreenshot", { format: "png" });
await writeFile(path.join(ordner, String(n + 1).padStart(5, "0") + ".png"),
Buffer.from(r.data, "base64"));
if (n % 15 === 0) process.stdout.write(".");
}
const datei = path.join(ZIEL, PROJEKT + "-" + a.key + DATEI_ZUSATZ + ".mp4");
process.stdout.write(" kodiere ");
await kodieren(ordner, datei, anzahl);
await rm(ordner, { recursive: true, force: true });
console.log("→ " + path.relative(process.cwd(), datei));
}
} finally {
if (cdp) cdp.zu();
chrome.kill();
await new Promise(ok => chrome.once("exit", ok));
await rm(path.join(tmpdir(), "postgrid-anim"), { recursive: true, force: true });
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 302 302" style="enable-background:new 0 0 302 302;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#00B388;}
.st1{fill:#FFFFFF;}
</style>
<title>grid</title>
<desc>Created with Sketch.</desc>
<g id="book">
<g id="slide_07" transform="translate(-230.000000, -233.000000)">
<g id="logo" transform="translate(230.000000, 233.000000)">
<g id="icon" transform="translate(51.000000, 51.000000)">
<g id="bg">
<path class="st0" d="M150.8,4.3C139.5,1.7,121,0,100,0S60.5,1.7,49.2,4.3C26.7,9,9,26.7,4.3,49.2C1.7,60.5,0,79,0,100
s1.7,39.5,4.3,50.8C9,173.3,26.7,191,49.2,195.7C60.5,198.3,79,200,100,200s39.5-1.7,50.8-4.3c22.5-4.7,40.2-22.4,44.9-44.9
c2.6-11.3,4.3-29.8,4.3-50.8s-1.7-39.5-4.3-50.8C191,26.7,173.3,9,150.8,4.3z"/>
</g>
<path id="Path" class="st1" d="M124,153.5c-1.9-4.5-5.9-7.9-10.8-8.9c-2.9-0.7-7.8-1.1-13.2-1.1s-10.3,0.4-13.2,1.1
c-4.9,1-8.9,4.4-10.8,8.9l0,0l0,0c-0.7,1.6-2.3,2.5-3.9,2.3c-0.4,0-0.8-0.1-1.1-0.1c-7.8-0.8-14-1.9-17.6-3.1l0,0
c-2.4-0.9-4.1-3.2-4.1-5.9c0-0.1,0-0.1,0-0.2c0-0.1,0-0.2,0-0.3v-0.1c0-0.1,0-0.1,0-0.2c0.6-8.2,5.6-27.3,13-49.1
c8.5-24.9,17.3-45.1,21.3-49.1l0,0c0.6-0.7,1.4-1.2,2.2-1.6c0.3-0.2,0.7-0.4,1.2-0.5l0,0c2.6-0.9,7.5-1.5,13-1.5l0,0
c5.5,0,10.4,0.6,13,1.5l0,0l0,0c0.5,0.2,0.9,0.3,1.2,0.5c0.8,0.4,1.5,1,2.2,1.6l0,0c4,3.9,12.8,24.1,21.3,49.1
c7.4,21.8,12.4,40.9,13,49.1c0,0.1,0,0.1,0,0.2v0.1c0,0.1,0,0.2,0,0.3s0,0.1,0,0.2c0,2.7-1.7,5-4.1,5.9l0,0
c-3.6,1.3-9.7,2.4-17.6,3.1c-0.3,0-0.8,0.1-1.2,0.1l0,0C126.2,155.9,124.7,155,124,153.5L124,153.5z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+16
View File
@@ -0,0 +1,16 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2124_86)">
<rect width="128" height="128" rx="30" fill="url(#paint0_linear_2124_86)"/>
<rect opacity="0.35" width="128" height="128" fill="#D97757"/>
<path d="M35.0157 79.6937L53.4322 69.3667L53.7357 68.4622L53.4322 67.9585H52.5215L49.4352 67.7708L38.9115 67.4891L29.8045 67.1136L20.9504 66.6442L18.7243 66.1748L16.6499 63.4053L16.8523 62.044L18.7243 60.7766L21.4058 61.0113L27.3254 61.4337L36.23 62.044L42.6555 62.4195L52.2179 63.4053H53.7357L53.9381 62.795L53.4322 62.4195L53.0274 62.044L43.8192 55.8008L33.8521 49.2291L28.6408 45.4269L25.8581 43.5024L24.4415 41.7186L23.8343 37.7756L26.3641 34.9591L29.8045 35.1938L30.6646 35.4285L34.1556 38.1042L41.593 43.8779L51.3072 51.0129L52.7239 52.1864L53.2943 51.8014L53.3816 51.5292L52.7239 50.4496L47.462 40.9206L41.846 31.2039L39.3163 27.1669L38.6586 24.773C38.4038 23.7683 38.2538 22.9371 38.2538 21.9096L41.1377 17.9665L42.7567 17.4502L46.6525 17.9665L48.2715 19.3748L50.7001 24.9138L54.5958 33.5978L60.6672 45.4269L62.438 48.9475L63.3993 52.1864L63.7535 53.1722H64.3606V52.6089L64.8666 45.9433L65.7773 37.7756L66.688 27.2608L66.9915 24.3036L68.4588 20.7361L71.3933 18.8115L73.67 19.8911L75.542 22.5667L75.289 24.3036L74.176 31.5324L72.0004 42.8452L70.5837 50.4496H71.3933L72.3546 49.4638L76.1997 44.3942L82.6252 36.3204L85.4585 33.1284L88.7978 29.6079L90.9228 27.918H94.9703L97.9048 32.3304L96.5894 36.8837L92.4406 42.1411L89.0002 46.6004L84.0672 53.2095L81.0062 58.5234L81.2807 58.9618L82.0181 58.8989L93.1489 56.505L99.1697 55.4253L106.354 54.2049L109.592 55.707L109.946 57.256L108.681 60.401L100.991 62.2787L91.9853 64.1094L78.5744 67.2664L78.4259 67.3858L78.6012 67.6458L84.649 68.1932L87.2294 68.334H93.5537L105.342 69.2259L108.428 71.2444L110.25 73.7322L109.946 75.6568L105.19 78.0508L98.8155 76.5487L83.8901 72.9812L78.7801 71.7138H78.0717V72.1362L82.3217 76.314L90.1639 83.3551L99.9286 92.4616L100.435 94.7148L99.1697 96.4985L97.8542 96.3108L89.2532 89.8329L85.9139 86.9226L78.4259 80.5856H77.92V81.2428L79.6402 83.7776L88.7978 97.5312L89.2532 101.756L88.5954 103.117L86.2175 103.962L83.6371 103.493L78.2235 95.9352L72.7087 87.4859L68.2564 79.8815L67.7187 80.221L65.0689 108.515L63.8547 109.971L61.0214 111.05L58.6434 109.266L57.3786 106.356L58.6434 100.582L60.1613 93.0719L61.3755 87.1104L62.4886 79.6937L63.1684 77.2161L63.1083 77.0503L62.5653 77.1416L56.9738 84.8103L48.4739 96.3108L41.7448 103.493L40.1258 104.15L37.3431 102.695L37.5961 100.113L39.1645 97.8129L48.4739 85.9838L54.0899 78.6141L57.7094 74.3837L57.6742 73.7718L57.4744 73.7548L32.739 89.8799L28.3373 90.4432L26.4147 88.6594L26.6676 85.7491L27.5783 84.8103L35.0157 79.6937Z" fill="#FAF9F5"/>
</g>
<defs>
<linearGradient id="paint0_linear_2124_86" x1="52.5" y1="128" x2="52.5" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#DC6038"/>
<stop offset="1" stop-color="#D97757"/>
</linearGradient>
<clipPath id="clip0_2124_86">
<rect width="128" height="128" rx="30" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="146 226 269 267" role="img" aria-label="OpenAI">
<rect x="146" y="226" width="269" height="267" rx="62" fill="#111111"/>
<g transform="translate(280.5 359.5) scale(.82) translate(-280.5 -359.5)">
<path d="M249.176 323.434V298.276C249.176 296.158 249.971 294.569 251.825 293.509L302.406 264.381C309.29 260.409 317.5 258.555 325.973 258.555C357.75 258.555 377.877 283.185 377.877 309.399C377.877 311.253 377.877 313.371 377.611 315.49L325.178 284.771C322.001 282.919 318.822 282.919 315.645 284.771L249.176 323.434ZM367.283 421.415V361.301C367.283 357.592 365.694 354.945 362.516 353.092L296.048 314.43L317.763 301.982C319.617 300.925 321.206 300.925 323.058 301.982L373.639 331.112C388.205 339.586 398.003 357.592 398.003 375.069C398.003 395.195 386.087 413.733 367.283 421.412V421.415ZM233.553 368.452L211.838 355.742C209.986 354.684 209.19 353.095 209.19 350.975V292.718C209.19 264.383 230.905 242.932 260.301 242.932C271.423 242.932 281.748 246.641 290.49 253.26L238.321 283.449C235.146 285.303 233.555 287.951 233.555 291.659V368.455L233.553 368.452ZM280.292 395.462L249.176 377.985V340.913L280.292 323.436L311.407 340.913V377.985L280.292 395.462ZM300.286 475.968C289.163 475.968 278.837 472.259 270.097 465.64L322.264 435.449C325.441 433.597 327.03 430.949 327.03 427.239V350.445L349.011 363.155C350.865 364.213 351.66 365.802 351.66 367.922V426.179C351.66 454.514 329.679 475.965 300.286 475.965V475.968ZM237.525 416.915L186.944 387.785C172.378 379.31 162.582 361.305 162.582 343.827C162.582 323.436 174.763 305.164 193.563 297.485V357.861C193.563 361.571 195.154 364.217 198.33 366.071L264.535 404.467L242.82 416.915C240.967 417.972 239.377 417.972 237.525 416.915ZM234.614 460.343C204.689 460.343 182.71 437.833 182.71 410.028C182.71 407.91 182.976 405.792 183.238 403.672L235.405 433.863C238.582 435.715 241.763 435.715 244.938 433.863L311.407 395.466V420.622C311.407 422.742 310.612 424.331 308.758 425.389L258.179 454.519C251.293 458.491 243.083 460.343 234.611 460.343H234.614ZM300.286 491.854C332.329 491.854 359.073 469.082 365.167 438.892C394.825 431.211 413.892 403.406 413.892 375.073C413.892 356.535 405.948 338.529 391.648 325.552C392.972 319.991 393.766 314.43 393.766 308.87C393.766 271.003 363.048 242.666 327.562 242.666C320.413 242.666 313.528 243.723 306.644 246.109C294.725 234.457 278.307 227.042 260.301 227.042C228.258 227.042 201.513 249.815 195.42 280.004C165.761 287.685 146.694 315.49 146.694 343.824C146.694 362.362 154.638 380.368 168.938 393.344C167.613 398.906 166.819 404.467 166.819 410.027C166.819 447.894 197.538 476.231 233.024 476.231C240.172 476.231 247.058 475.173 253.943 472.788C265.859 484.441 282.278 491.854 300.286 491.854Z" fill="#FFFFFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1181.705 585.818" role="img" aria-labelledby="pds-title">
<title id="pds-title">pds</title>
<g>
<defs>
<path id="pds-swoosh-left" d="M772.086 206.642l-105.035 8.411c45.016 41.834 87.543 75.766 127.371 102.59 27.018 8.992 62.736 12.163 104.491-4.697-30.703-25.227-50.565-48.661-86.763-88.302-16.24-17.811-39.056-18.002-40.03-18.002h-.034z"/>
<clipPath id="pds-swoosh-left-clip">
<use xlink:href="#pds-swoosh-left"/>
</clipPath>
<linearGradient id="pds-swoosh-left-gradient" x1="452.9609" x2="458.5785" y1="234.8403" y2="234.8403" gradientTransform="matrix(38.4954 22.2253 22.2253 -38.4954 -21983.3574 -822.2328)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d50a3b"/>
<stop offset=".2" stop-color="#d50a3b"/>
<stop offset=".8" stop-color="#94190e"/>
<stop offset="1" stop-color="#94190e"/>
</linearGradient>
<path id="pds-swoosh-right" d="M982.978 254.984c-78.122 76.53-144.787 77.232-188.556 62.659 44.006 27.159 103.202 56.553 159.283 60.762.938.077 1.883.125 2.826.158h5.46c80.146-2.82 188.527-127.453 213.695-229.967.229-.959.471-1.854.691-2.751 0-.051 0-.095.017-.219 5.311-21.627-2.354-38.337-6.048-44.743C1150.984 70.19 1109.181 0 1109.181 0c-7.671 61.885-53.602 183.874-126.203 254.984z"/>
<clipPath id="pds-swoosh-right-clip">
<use xlink:href="#pds-swoosh-right"/>
</clipPath>
<linearGradient id="pds-swoosh-right-gradient" x1="453.6846" x2="459.3051" y1="233.9976" y2="233.9976" gradientTransform="matrix(68.3072 0 0 -68.3072 -30195.5273 16172.9473)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d50a3b"/>
<stop offset=".3" stop-color="#d50a3b"/>
<stop offset=".9" stop-color="#ea9893"/>
<stop offset="1" stop-color="#ea9893"/>
</linearGradient>
</defs>
<polygon fill="url(#pds-swoosh-left-gradient)" points="725.021 106.244 952.239 237.433 840.945 430.206 613.72 299.015" clip-path="url(#pds-swoosh-left-clip)"/>
<rect width="387.283" height="378.563" x="794.422" fill="url(#pds-swoosh-right-gradient)" clip-path="url(#pds-swoosh-right-clip)"/>
</g>
<path fill="#575656" d="M586.085 451.803c0-19.771-4.644-39.134-37.172-58.887l-50.756-30.985c-9.664-5.829-15.493-9.682-15.493-15.886 0-11.999 8.147-16.276 26.733-16.276 22.063 0 53.058 3.085 63.914 4.652h1.551c3.878 0 6.196-2.709 6.196-7.38v-22.438c0-4.662-2.318-7.764-6.979-8.914-19.754-4.262-37.556-7.772-68.942-7.772-49.197 0-73.595 19.396-73.595 58.128 0 22.464 13.558 41.825 32.537 53.45l53.066 32.536c12.399 7.737 17.053 12.408 17.053 19.771 0 10.447-3.486 18.577-27.5 18.577-11.641 0-46.496-1.926-65.099-4.262h-1.543c-3.869 0-6.596 3.103-6.596 6.205v23.247c0 4.652 2.727 7.755 7.372 8.896 21.688 5.813 54.225 7.772 69.334 7.772 50.756 0 75.939-26.742 75.939-60.437M341.66 467.67c-7.363 1.551-23.248 2.71-37.189 2.71-33.321 0-41.433-23.623-41.433-55.393v-29.826c0-31.753 8.112-55.393 41.433-55.393 9.297 0 26.35 1.177 37.189 2.727zm51.132 24.032V222.069c0-4.261-3.485-7.729-7.746-7.729h-35.639c-4.261 0-7.747 3.469-7.747 7.729v68.576c-14.334-1.942-27.892-2.728-37.189-2.728-62.363 0-92.59 36.807-92.59 97.244v29.826c0 60.421 30.227 97.252 92.59 97.252 24.798 0 53.858-3.886 77.873-10.09 8.897-2.334 10.448-4.651 10.448-10.447M129.763 414.988c0 31.77-8.13 55.393-41.451 55.393-9.297 0-26.333-1.159-37.189-2.71V332.496c7.363-1.55 23.248-2.727 37.189-2.727 33.321 0 41.451 23.64 41.451 55.393zm51.14 0v-29.826c0-60.438-30.21-97.244-92.591-97.244-24.782 0-53.841 3.895-77.856 10.09C1.534 300.326 0 302.653 0 308.463V578.08c0 4.244 3.477 7.738 7.729 7.738h35.665c4.252 0 7.729-3.494 7.729-7.738v-68.566c14.326 1.926 27.892 2.727 37.189 2.727 62.381 0 92.591-36.831 92.591-97.252"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Rendert das YouTube-Banner als PNG in 2560 x 1440, mit Chrome ohne Fenster.
# ./rendern.sh youtube-banner.png
# ./rendern.sh zonen youtube-banner-zonen.png, mit den Bereichen je Gerät
cd "$(dirname "$0")" || exit 1
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
NAME=youtube-banner
SUCHE=""
if [ "$1" = "zonen" ]; then NAME=youtube-banner-zonen; SUCHE="?zonen"; fi
"$CHROME" --headless=new --disable-gpu --hide-scrollbars --allow-file-access-from-files \
--force-device-scale-factor=1 --window-size=2560,1440 --virtual-time-budget=4000 \
--screenshot="$PWD/$NAME.png" "file://$PWD/youtube-banner.html$SUCHE" 2>/dev/null
ls -la "$NAME.png"
Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

+105
View File
@@ -0,0 +1,105 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>YouTube-Banner HiveGrid</title>
<link rel="stylesheet" href="../assets/fonts/manrope.css">
<style>
/* YouTube-Banner, 2560 x 1440 (Empfehlung von Google, Stand 10/2026).
Was jedes Gerät sieht:
Fernseher das ganze Bild
Desktop der Streifen in der Mitte, 2560 x 423
Handy nur 1546 x 423 in der Mitte. Darin steht alles Wichtige.
Keine Schatten und Rahmen, das verbietet Google ausdrücklich.
Gerendert mit ./rendern.sh. Mit ?zonen sieht man die Bereiche. */
:root {
--navy: #0F1A2E;
--gelb: #F1B729;
--weiss: #FFFFFF;
--grau: #B9BDC9;
}
html, body { margin: 0; background: var(--navy); }
.banner {
position: relative; width: 2560px; height: 1440px; overflow: hidden;
background: var(--navy); font-family: Manrope, sans-serif;
}
#waben { position: absolute; inset: 0; }
.mitte {
position: absolute; left: 507px; top: 508px; width: 1546px; height: 423px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
text-align: center;
}
.wortmarke { height: 76px; display: block; margin-bottom: 34px; }
h1 {
margin: 0; color: var(--weiss); font-weight: 800; font-size: 80px;
line-height: 1.08; letter-spacing: -0.025em;
}
/* Akzent wie im Theme modern: das Wort in Gelb mit einem Balken darunter. */
h1 .a { color: var(--gelb); position: relative; white-space: nowrap; }
h1 .a::after {
content: ""; position: absolute; left: 0; right: 0.12em; bottom: -0.04em;
height: 0.09em; background: var(--gelb);
}
.unter {
margin: 34px 0 0; color: var(--grau); font-weight: 600; font-size: 38px;
letter-spacing: -0.005em;
}
/* Hilfslinien, nur mit ?zonen */
.zone { position: absolute; border: 3px dashed; pointer-events: none; display: none; }
.zonen .zone { display: block; }
.zone span { position: absolute; left: 12px; top: 8px; font: 700 26px Manrope, sans-serif; }
.z-handy { left: 507px; top: 508px; width: 1546px; height: 423px; border-color: #FF4D4D; color: #FF4D4D; }
.z-tablet { left: 352px; top: 508px; width: 1855px; height: 423px; border-color: #4DD2FF; color: #4DD2FF; }
.z-desktop { left: 0; top: 508px; width: 2560px; height: 423px; border-color: #7CFF4D; color: #7CFF4D; }
</style>
</head>
<body>
<div class="banner" id="banner">
<svg id="waben" width="2560" height="1440" viewBox="0 0 2560 1440" aria-hidden="true"></svg>
<div class="mitte">
<img class="wortmarke" src="../assets/brand/logo-white.svg" alt="HiveGrid">
<h1>Dein Branchenprogramm bleibt.<br>HiveGrid kommt <span class="a">drumherum.</span></h1>
<p class="unter">Software für Handwerksbetriebe, entstanden in einem SHK-Betrieb</p>
</div>
<div class="zone z-desktop"><span>Desktop 2560 × 423</span></div>
<div class="zone z-tablet"><span>Tablet 1855 × 423</span></div>
<div class="zone z-handy"><span>Handy, alle Geräte 1546 × 423</span></div>
</div>
<script>
// Das Wabenfeld der Website (cta-honeycomb.svg): gelbe G-Waben auf Dunkel.
// Außen volle G-Waben, zur Mitte hin nur noch der Umriss der Wabe, immer
// dünner, dann nichts mehr, damit die Schrift frei steht. Kein halbdurch-
// sichtiges Gelb: Das wird auf Navy trüb.
// Fester Zufall, damit jedes Rendern gleich aussieht.
const G = "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";
const WABE = "384.5,166.5 288.3,332.9 96.1,332.9 0,166.5 96.1,0 288.3,0";
const B = 168, S = B / 384.5, H = 332.9 * S, LUECKE = 22;
const DX = 0.75 * B + LUECKE, DY = H + LUECKE * 1.1;
let saat = 7;
const zufall = () => ((saat = (saat * 16807) % 2147483647) / 2147483647);
const svg = document.getElementById("waben");
let inhalt = "";
for (let spalte = -1; spalte * DX < 2560 + B; spalte++) {
for (let zeile = -1; zeile * DY < 1440 + H; zeile++) {
const x = spalte * DX, y = zeile * DY + (spalte % 2 ? DY / 2 : 0);
const mx = x + B / 2, my = y + H / 2;
// 0 in der Mitte, 1 am Rand. Waagerecht ab 640 px vom Zentrum, senkrecht ab 300.
const quer = Math.max(Math.abs(mx - 1280) - 640, 0) / 600;
const hoch = Math.max(Math.abs(my - 720) - 300, 0) / 420;
const staerke = Math.min(1, Math.max(quer, hoch * 0.85)) + 0.06 * (zufall() - 0.5);
const lage = `transform="translate(${x.toFixed(1)} ${y.toFixed(1)}) scale(${S.toFixed(5)})"`;
if (staerke >= 0.5) {
inhalt += `<polygon points="${G}" fill="#F1B729" ${lage}/>`;
} else if (staerke >= 0.12) {
// Strichstärke in Pixeln, über die Skalierung zurückgerechnet.
const strich = (1.5 + 5 * (staerke - 0.12) / 0.38) / S;
inhalt += `<polygon points="${WABE}" fill="none" stroke="#F1B729" stroke-width="${strich.toFixed(1)}" stroke-linejoin="miter" ${lage}/>`;
}
}
}
svg.innerHTML = inhalt;
if (location.search.includes("zonen")) document.getElementById("banner").classList.add("zonen");
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

+250
View File
@@ -0,0 +1,250 @@
# Produkt-Komponenten für Posts
**Stand:** 20.10.2026. Quelle: `hivegrid-cloud`, Stand `origin/main` vom 19.10.2026.
**Referenz-Umsetzung:** `post8.html`, Abschnitt „Post 8 · E-Mail-Marketing“.
Diese Datei ist das Nachschlagewerk für jede Zeichnung, die in einem Post die Software zeigt. Die Regel dazu steht in
`rules.md` §0 und gilt ohne Ausnahme.
---
## 1. Die Regel
**Jede Oberfläche in einem Post wird aus den echten Komponenten von hivegrid-cloud nachgebaut, mit ihren Maßen,
Farben, Schriften, Icons und Beschriftungen. Keine eigenen, ausgedachten Grafiken.**
- Jede Zeichnung bildet einen Screen nach, den es im Produkt gibt. Im Kommentar steht die Datei, aus der er stammt.
- Beschriftungen stehen wörtlich in `apps/web/public/locales/de/translations.json`.
- Was im Produkt nicht existiert, wird nicht gezeichnet. Auch nicht, wenn es gut aussähe.
- Was nicht auf die Slide passt, wird weggelassen, nicht umgebaut. Im Kommentar steht, was fehlt und warum.
- Erfunden werden darf nur Demo-Inhalt: Namen, Betreffzeilen, Texte, Daten, Zahlen. Die Zahlen müssen aufgehen.
**Warum:** Die erste Fassung von Post 8 hatte ein eigenes Design (Pillen mit Rand, Häkchen-Chips, Reiter und Kacheln, die
es im Produkt nicht gibt). Dejan: „sieht total vibecoded aus“. Nach dem Umbau auf die Komponenten: „sieht jetzt genau so
aus wie die Software“ (20.10.2026).
---
## 2. So gehst du vor
1. **Screens finden.** Für das Thema des Posts das Modul in hivegrid-cloud suchen:
`apps/web/app/components/screens/<modul>/`, `apps/web/app/modules/<modul>/`, die Routen unter
`apps/web/app/routes/app.$tenant/`, die Texte in `translations.json`.
2. **Immer `origin/main` lesen, nie die Arbeitskopie.** Die lokale Kopie von hivegrid-cloud ist veraltet und wird nicht
angefasst (kein pull, kein checkout ohne Dejan).
- Einzelne Datei: `git -C ../hivegrid-cloud show origin/main:<pfad>`
- Für viele Dateien ein Schnappschuss ins Scratchpad:
`git -C ../hivegrid-cloud archive origin/main apps/web/app apps/web/public/locales/de | tar -x -C <scratchpad>/hg-main`
- Vorher `git -C ../hivegrid-cloud fetch` (holt nur, ändert nichts).
3. **Für jede Slide einen echten Screen wählen.** Aufbau von oben nach unten aus dem Code lesen: Kopf, Knöpfe, Tabs,
Felder, Tabellenspalten, welche Badge für welchen Status.
4. **Mit den Bausteinen zeichnen** (Abschnitt 3). Sie stehen in `post8.html` und werden in den neuen Post kopiert, weil
jeder Post eine einzelne Datei ist.
5. **Prüfen:**
- Streifen-Ansicht `?nur=streifen`: Die Karte darf nicht in den Text der Slide ragen.
- PNG-Export: Die Oberfläche muss in Instrument Sans stehen, nicht in einer Ersatzschrift (siehe Fallen).
- MP4 mit `node animation.mjs postN.html <key>`.
---
## 3. Bausteine in `post8.html`
Alle Werte sind Produkt-Pixel und werden mit `PZ = 1.15` vergrößert, wie ein Browserzoom. Koordinaten sind Pixel der
Zeichnung. Nur Schriften, Icons und Komponentenhöhen laufen über `pz()`.
| Baustein | Bildet nach | Aufruf |
|---|---|---|
| `P`, `P_TON`, `P_KNOPF`, `P_KARTE` | Rollen aus `ui/theme.css` | Farbkonstanten |
| `pText(x, y, t, gr, gew, farbe, o)` | Text in Instrument Sans | `o.anker` für Ausrichtung |
| `pMess(t, gr, gew)` | Textbreite in Instrument Sans | |
| `pMitte(ym, gr)` | Grundlinie für Text mittig in einem Kasten | |
| `pIkon(name, cx, cy, gr, farbe)` | Feather/Lucide-Icon, Strich 2 auf 24 | Namen aus `LUCIDE` |
| `pKarte(x, y, b, h, art, radius)` | `Card`, `InlineMessage` | `art`: std, weiss, neutral, info, warnung |
| `pBadge(x, ym, t, ton, klein)` | `BadgeFeedback` subtle | `ton`: positiv, warnung, info, neutral |
| `pKnopf(x, y, t, art, o)` | `Button` | `art`: primaer, sekundaer, tertiaer, geist, aus; `o.ikon`, `o.ikonRechts`, `o.klein` |
| `pFeld(x, y, b, o)` | `TextInput`, `Select`, `TextArea` | `o.label`, `o.wert`, `o.platzhalter`, `o.auswahl`, `o.hoch`, `o.hilfe` |
| `pTabs(x, y, b, reiter, aktiv, innen)` | `Tabs` | |
| `pUmschalter(xRechts, y, eintraege, aktiv)` | `ToggleGroup` klein | Einträge `[icon, text]` |
| `pTabelle(x, y, spalten, zeilen, zh, zelle)` | `Table` | Spalten `[titel, icon, breite]` |
| `pRahmen(h, inhalt, kopf)` | Seite mit `PageHeader` | `kopf.ikon`, `kopf.titel`, `kopf.badge`, `kopf.ikonKasten`, `kopf.rechts` |
| `pKopfKnopf(t, art, o)` | Knopf rechts im Kopf | |
| `emKpi(...)` | `WidgetRahmen` mit `KPICard` | |
**Icons:** `LUCIDE` enthält die Formen aus `lucide-static@0.460.0`. Subframes `Feather*`-Icons haben dieselben Formen.
Fehlt ein Icon, die SVG von `https://cdn.jsdelivr.net/npm/lucide-static@0.460.0/icons/<name>.svg` holen und das Innere
der `<svg>` in die Tabelle eintragen. Phosphor (`ikon()`) gehört nur noch in ältere Posts, nicht in Oberflächen.
**Schrift:** Die Dateien liegen in `assets/fonts/instrument-sans-*.woff2`, dieselben wie in
`hivegrid-cloud/apps/web/public/fonts/`. Der Post braucht dafür drei Dinge:
1. den `@font-face`-Block im `<head>`
2. beim Start `document.fonts.load(...)` für 400, 500 und 600, bevor zum ersten Mal gemessen wird
3. `instrumentSansCss()`, angehängt an `fontEmbedCss`, sonst fehlt die Schrift im PNG
Alles drei steht in `post8.html`.
---
## 4. Tokens, aufgelöst
### Schrift
Instrument Sans, variabel 400 bis 700. Laufweite überall 0.
| Token | px / Zeilenhöhe / Gewicht | Wofür |
|---|---|---|
| `subtitle` | 32/40/600 | Seitentitel im `PageHeader`, Wert in `StatsCard` |
| `heading-` | 24/32/500 | Dialogtitel |
| `body-large` | 20/24 (‑0: 26)/500 | Abschnitte wie „E-Mail gestalten“, EmptyState-Titel |
| `body-default-regular` | 16/24 (‑0: 22)/400 | **Knopfbeschriftung**, Eingaben, Fließtext |
| `body-default-medium` | 16/24/500 | Feldbeschriftungen |
| `body-default-semibold` | 16/24/600 | Kartentitel |
| `body-small-regular` | 14/21 (‑0: 18)/400 | Hilfetexte, Tabs, Seitenleiste |
| `body-small-medium` | 14/21/500 | Badges, Menüeinträge, Checkbox-Beschriftung |
| `body-extra-small` | 12/18 (‑0: 16)/400 bis 600 | Tabellenkopf (500), Tooltip |
`text-heading-2` und `text-heading-3` werden im Code benutzt, sind aber nirgends definiert. Sie rendern als 16/24/400.
### Farben
**Neutral:** 0 `#FAFAFA` · 50 `#F5F4F4` · 100 `#F0F0F0` · 200 `#E5E5E5` · 300 `#CECECF` · 400 `#B5B5B7` · 500 `#9D9DA0` ·
600 `#858589` · 700 `#6D6D71` · 800 `#505057` · 850 `#37373E` · 900 `#24242B` · 950 `#111114`
| Rolle | Wert |
|---|---|
| Seite (`surface-neutral-primary`) | `#F5F4F4` |
| Karte, Kopfzeile, Tabellenzeile (`secondary`) | `#FAFAFA` |
| Tabellenkopf, Hover (`tertiary`) | `#F0F0F0` |
| Rand, Rand stark | `#E5E5E5`, `#B5B5B7` (Eingabefelder) |
| Text primär, sekundär, tertiär | `#24242B`, `#505057`, `#6D6D71` |
| Platzhalter, deaktiviert | `#CECECF` |
| Link | `#08739C` |
| Icons primär, sekundär, tertiär | `#24242B`, `#505057`, `#858589` |
| Akzent für aktiv und ausgewählt | `#E1F4FD` (information subtle) |
| Sekundärknopf | `#E3F1FF` |
| Ton | 100 | 200 | 500 | 600 | 700 |
|---|---|---|---|---|---|
| Erfolg (grün) | `#DCFCE4` | `#BAF8CB` | `#20C54F` | `#15A43E` | `#148134` |
| Warnung (gelb, nicht Marke) | `#FCF6C5` | `#FAEB8E` | `#E1AB11` | `#C2840C` | `#9B5E0D` |
| Fehler (rot) | `#FEE2E2` | `#FDCBCB` | `#ED4646` | `#DA2828` | `#C52020` |
| Information (ocean) | `#E1F4FD` | `#BCEAFB` | `#15B1E2` | `#0890C1` | `#08739C` |
**Kein Markengelb in der Oberfläche.** Die Rolle „brand“ ist im Produkt auf Grau gemappt, Primärknöpfe sind `#24242B`.
Gelb gehört auf der Slide nur zur Überschrift und zu Marken-Elementen außerhalb der Oberfläche.
### Radien und Schatten
- `rounded-sm` 4 px (Eingabefelder) · `rounded-md` **6 px** (Knöpfe, Karten, Tabs) · `rounded-lg` 8 px (Widgets) ·
`rounded-xl` 12 px. In `theme.css` steht md = 8, `globals.css` überschreibt das danach mit 6.
- Schatten im Produkt: sm `0 1px 2px rgb(0 0 0 / .08)`, md und lg größer. In den Zeichnungen lassen wir sie weg
(rules.md §5). Der Stapel um die Karte hat seinen eigenen Schatten und bleibt.
---
## 5. Komponenten
| Komponente | Maße und Farben |
|---|---|
| **Button** | klein h32, px12, Abstand 8 · mittel h36, px16, Abstand 12 · groß h48. Radius md. Beschriftung 16/400, Icon 16. primär `#24242B`/`#FAFAFA` · sekundär `#E3F1FF`/`#24242B` · tertiär `#F5F4F4` mit Rand `#E5E5E5` · ghost transparent · information `#0890C1` · destructive `#DA2828` · deaktiviert `#F0F0F0`/`#CECECF` |
| **IconButton** | rund, quadratisch mit `square` (md). 24/32/36/48 Kasten, Icon 16/20/20/24 |
| **BadgeFeedback** (alle Status) | Pille **ohne Rand**, h24 (klein 20), px10, Abstand 8, Punkt 4 px, Schrift 14/500. positiv `#DCFCE4`, Text und Punkt `#148134` · Warnung `#FCF6C5`, Text `#24242B`, Punkt `#9B5E0D` · information `#E1F4FD`, Text `#24242B`, Punkt `#08739C` · neutral `#F0F0F0`, Text und Punkt `#24242B` · negativ `#FEE2E2`, `#C52020` |
| **Badge** (Farbtöne) | Pille h24 mit 1-px-Rand, sonst wie oben. Standard `#F0F0F0`, Rand `#E5E5E5` |
| **Tabs** | Reiter px16, py10 (rund 40 hoch), oben rund, `#FAFAFA`, unten 2 px `#E5E5E5`, Schrift 14/400 `#505057`. **Aktiv:** Fläche `#E1F4FD`, unten 2 px `#24242B`, Schrift 14/500 `#24242B` |
| **TextInput / Select** | h36, px12, Radius 4, Rand `#B5B5B7`, Fläche `#F5F4F4`. Beschriftung 16/500 `#505057`, 6 px darüber. Wert 16 `#505057`, Platzhalter `#CECECF`, Hilfe 14 `#6D6D71`. Pfeil 16 `#858589` |
| **TextArea** | wie oben, mindestens 96 hoch, Innenabstand 12×8 |
| **Checkbox** | 16, Radius 4, Rand 2 px `#B5B5B7`. An: `#24242B` mit Haken `#FAFAFA`. Beschriftung 14/500 |
| **Switch** | 36×22, rund. Aus: `#F5F4F4` mit dunklem Knopf. An: `#24242B` mit hellem Knopf |
| **Table** | Außenrand `#E5E5E5`. Kopf h32, `#F0F0F0`, Schrift 12/500 `#505057`, Icon 12 `#858589`. Zeilen `#FAFAFA` mit Trennlinien `#E5E5E5`, Höhe aus dem Inhalt (rund 31) |
| **Avatar** | rund, 24/32/40/48/64. Ohne Bild `#F0F0F0`, Kürzel 16/500 `#505057` |
| **IconWithBackground** | Radius 4. Kasten 22/24/28/32/40, Icon 14/16/20/24/32. information `#E1F4FD` mit `#08739C` |
| **Card** | Radius md, Rand `#E5E5E5`, Fläche `#FAFAFA`, Innenabstand 16 (lg 24). Varianten information, warning, negative, positive, neutral |
| **InlineMessage** | Card in Ton-Variante, px16, py12, Text 16 `#24242B`. Standard information |
| **Alert** | Radius md, Rand, px16, py20, Icon 24. Titel 16/500 im Ton, Text 16 `#505057` |
| **Widget** (`WidgetRahmen`) | Radius lg, Rand, weiß. Kopf h44 mit Titel 13/600. KPI: Wert halbfett, Fußzeile 11 `#6D6D71` über einer Linie |
| **ToggleGroup** | h36 (klein 32), Innenabstand 2, Rand, `#F0F0F0`. Aktiv: `#24242B` mit Schrift `#FAFAFA` |
| **DropdownMenu** | `#FAFAFA`, Rand, Radius md, Innenabstand 4. Einträge 14/500 `#6D6D71`, Hover `#F0F0F0` |
| **Tooltip** | `#505057`, Schrift 12/500 `#FAFAFA`, Radius 4 |
| **Progress** | 6 px, rund, Spur `#F0F0F0`, Füllung `#24242B` |
| **EmptyState** | Titel 20/500, Text 16 `#505057`. Umrandet: Rand und `#F5F4F4` |
| **Dialog** | weiß, Rand, Radius md, Overlay `rgb(0 0 0 / .4)`. DecisionDialog 420 oder 560 breit, Titel 24/500 |
### App-Rahmen
- **Seite:** Hintergrund `#F5F4F4`. Kopfleiste h64, `#FAFAFA`, Linie unten.
- **PageHeader:** `#FAFAFA`, px24, py20, Linie unten. Icon 24 in `#858589`, Abstand 8, Titel 32/600 `#24242B`.
Knöpfe rechts mit Abstand 8. In den Zeichnungen Titel 24, weil die Karte schmaler ist als ein Bildschirm.
- **Filterleiste:** `#F5F4F4`, px24, py10. Suchfeld `#FAFAFA`, Facetten-Chips h24 mit Rand `#B5B5B7`. Aktiv `#BCEAFB`.
- **Seitenleiste:** 240 breit, `#FAFAFA`. Einträge h40, Icon 20 mit Strich 1.5, Schrift 14. Gewählt: `#E1F4FD`.
---
## 6. Fallen
1. **Schrift im PNG.** html-to-image bettet nur die Schrift ein, die im HTML vorkommt, also Manrope. Instrument Sans
steckt nur im SVG und fehlte im Export. Lösung: `instrumentSansCss()` an `fontEmbedCss` hängen. Prüfen, indem man
Export und Vorschau übereinanderlegt.
2. **Messen vor dem Laden.** `pMess()` speichert Breiten zwischen. Wird vor dem Laden der Schrift gemessen, bleibt die
Breite der Ersatzschrift hängen. Darum `document.fonts.load` am Anfang von `boot()`.
3. **Was in den Übersetzungen steht, ist nicht automatisch zu sehen.** Beispiel: „Pre-Send-Prüfung“ und die sechs
Prüfpunkte stehen in `translations.json`, angezeigt werden sie nirgends. Immer im Screen-Code nachsehen, ob ein Text
wirklich gerendert wird.
4. **Website und Produkt unterscheiden.** Die Website zeigt manches, was es im Produkt nicht gibt (in Post 8 die
Produktkacheln im Mail-Baukasten). Maßgeblich ist das Produkt.
5. **Höhe der Karte.** Zu hohe Karten ragen in den Text der Slide. Lieber Schritte oder Hilfetexte weglassen als die
Komponenten zu verkleinern.
6. **Feste Höhe im Bewegtbild.** Die Höhe der Zeichnung darf nicht vom Zeitpunkt abhängen, sonst ändert sich der
viewBox und alles springt.
---
## 7. Beispiel: E-Mail-Marketing (Post 8)
| Slide | Screen | Datei in `apps/web/app/components/screens/email-marketing/` |
|---|---|---|
| Bewegtbild | Kampagnenbericht | `EmailMarketingDetailScreens.tsx` |
| Zielgruppe | Dialog „Zielgruppe bearbeiten“ | `SegmentEditorDialog.tsx`, `SegmentFilterBuilder.tsx` |
| Vorlagen | Vorlagenseite | `EmailMarketingTemplateScreens.tsx` |
| Rechtssicher | „Empfänger & Versand“ mit „Bereit zum Versand?“ | `EmailMarketingCampaignEditorPage.tsx` |
| Baukasten | Gestaltung | `CampaignContentBuilder.tsx`, `CampaignBuilderSidebar.tsx`, `campaign-mail-look.ts` |
Status-Badges dort: „Gesendet“ positiv · „Geplant“, „Pausiert“ Warnung · „Wird gesendet“ information · „Entwurf“,
„Archiviert“ neutral. Empfänger: „Zugestellt“, „Geöffnet“, „Geklickt“ positiv · „Ausstehend“ Warnung · „Gebounced“
negativ. Vorlagen: „Standard“ information · „Eigen“ neutral. Versandbereitschaft: „Erledigt“ positiv · „Offen“ neutral.
---
## 8. Beispiel: Form Builder (Post 7)
Seit 20.10.2026 nach dem echten Editor gebaut. Vorher kam Post 7 aus dem Klickdummy des Pitch-Pakets
(`S02EinfacherFragenEditor.tsx`) und zeigte Dinge, die es nicht gibt.
| Slide | Screen | Datei in `apps/web/app/components/screens/forms/` |
|---|---|---|
| Bewegtbild | Reiter „Fragen“, neue Frage klappt auf | `editor/questions-tab/QuestionsTabView.tsx`, `SortableQuestionCard.tsx` |
| Die Fragen | zugeklappte Fragekarten, Werkzeugleiste | `SortableQuestionCard.tsx:121`, `QuestionsTabView.tsx` |
| Bedingt sichtbar | offene Frage mit „Nur anzeigen, wenn“ | `editor/questions-tab/QuestionEditors.tsx:123` |
| Lead | Ansicht „Felder zuweisen“ | `editor/LeadBanner.tsx`, `editor/LeadMappingView.tsx` |
- **Kopf** (`FormEditorPage.tsx:411`): kein PageHeader, eine eigene Zeile mit `clipboard-list`, Name 20/500, Badge
„Entwurf“ (neutral), „Veröffentlicht“ (positiv) oder „Änderungen nicht live“ (Warnung). Rechts klein: „Einbetten & QR“
(sekundär, nur extern), „Vorschau“ (tertiär), „Veröffentlichen“ (primär), ⋯. Autosave „Automatisch gespeichert“ steht
dort und blendet nach 3 s aus.
- **Reiter:** „Fragen“ (`list-checks`), „Design“ (`palette`), „Antworten“ (`inbox`), gleich breit.
- **Fragekarte zu:** Radius 8, px20 py16, Griff, Typ-Icon, Titel 16/500 mit rotem `*` bei Pflicht, darunter der Typ
und bei optionalen „ · optional“. Kopieren grau, Löschen rot `#C52020`. Bedingte Fragen: Badge „Bedingt sichtbar“
klein neutral.
- **Fragekarte offen:** kein farbiger Rand, sondern 6-px-Streifen links in `#0791F0`. Optionen nur hier.
- **Feldtypen:** Text, Mehrzeiliger Text, Auswahl, Mehrfachauswahl, Checkbox, E-Mail, Telefon, Zahl, Datum, Uhrzeit,
Datum und Uhrzeit, Datei (`paperclip`), Foto (Kamera, nur intern), Unterschrift. **Nicht:** „Kurzantwort“, „Absatz“,
„Multiple-Choice-Frage“.
- **„Frage hinzufügen“:** gestrichelte Fläche, 2 px `#B5B5B7`, Icon 24, Schrift 20/500. Neue Frage: Typ „Text“, Name
„Unbenannte Frage“, neue Optionen „Auswahl A“, „Auswahl B“.
- **Lead:** Banner „Lead-Verknüpfung“ mit „Aktiv“, Text „Neue Antworten werden als Lead gespeichert.“, Knöpfe „Felder
zuweisen“ (primär) und „Deaktivieren“ (tertiär). **Eine Pipeline gibt es im Form Builder nicht.**
- **Gibt es nicht:** eine Fußzeile, „Zuletzt gespeichert vor wenigen Sekunden“, „Bereit zum Veröffentlichen“.
Bausteine dazu in `post7.html`: `fbRahmen`, `fbFrage`, `fbLeiste`, `fbNeu`, `fbFeld`, `fbOffenKopf`, `fbOffenKarte`,
`fbOption`, `fbOptionNeu`, `fbSchalter`, `pKurz` (Text mit „…“ kürzen).
## 9. PostGrid-Eigenheit: gleiche Höhe im Carousel
Alle Grafiken eines Posts bekommen dieselbe SVG-Höhe, damit die Karten auf allen Slides oben auf einer Linie stehen.
Eine kürzere Karte lässt deshalb unten Platz frei. Das ist gewollt. Zu hoch darf keine werden: Sie ragt sonst in den
Text. Das Bewegtbild ohne Fließtext hat ein eigenes `visBottom`.
+194 -8
View File
@@ -1,6 +1,13 @@
<!doctype html>
<html lang="de">
<head>
<script>
/* Uebersichts-Modus: die Seite laeuft dann als Kachel in uebersicht.html.
Das Attribut muss vor dem ersten Zeichnen stehen, sonst blitzt die
Bedienoberflaeche kurz auf. Deshalb hier oben und nicht im Hauptskript. */
if (location.search.indexOf("nur=streifen") >= 0)
document.documentElement.dataset.nur = "streifen";
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PostGrid Post 1 · Lösung</title>
@@ -139,8 +146,14 @@
.forget:hover { color: #EB7C6B; }
/* ─── Control panel ──────────────────────────────────────────────────── */
/* max-width statt nur width: bei einem schmalen Fenster kaempfen linkes
Feld, Buehne und rechtes Feld um denselben Platz, und ohne Obergrenze
schiebt sich das rechte aus dem Fenster. Dann ist es nicht mehr
zuzumachen, weil sein Knopf mit hinausrutscht.
overflow statt overflow-y aus demselben Grund: wird das Feld schmal,
muss sein Inhalt waagerecht erreichbar bleiben. */
.panel {
width: 306px; flex: none; height: 100%; overflow-y: auto;
width: 306px; max-width: 42vw; flex: none; height: 100%; overflow: auto;
padding: 28px 22px 40px 28px;
border-right: 1px solid #24262F; background: #17181F;
}
@@ -753,6 +766,26 @@
--guide-axis: rgba(48,49,61,.45);
--guide-mid: rgba(200,144,26,.75);
}
/* ─── Uebersichts-Modus ──────────────────────────────────────────────────
Die Seite zeigt dann nur den Streifen, in natuerlicher Groesse und ohne
eigenen Rahmen: die Uebersicht setzt sie als iframe ein und skaliert
selbst. Alles Bedienbare faellt weg, auch die Hilfslinien. */
html[data-nur="streifen"] { overflow: hidden; }
html[data-nur="streifen"] .menubar,
html[data-nur="streifen"] .panel,
html[data-nur="streifen"] .hud,
html[data-nur="streifen"] .guides { display: none !important; }
/* Der Auswahlrahmen ist eine Bedienhilfe und hat in der Uebersicht nichts
verloren -- dort ist keine Slide "die aktuelle". */
html[data-nur="streifen"] .holder.sel::after { display: none; }
html[data-nur="streifen"] .stage { background: none; height: auto; }
html[data-nur="streifen"] .scroller { position: static; overflow: visible; }
html[data-nur="streifen"] .canvas { padding: 0; display: block;
width: max-content; min-width: 0; height: max-content; min-height: 0; }
html[data-nur="streifen"] body, html[data-nur="streifen"] .app {
height: auto; min-height: 0; background: none; overflow: visible;
}
</style>
</head>
<body>
@@ -765,6 +798,23 @@
<div class="liste">
<button type="button" data-gehe="./post1.html">Post 1</button>
<button type="button" data-gehe="./post2.html">Post 2</button>
<button type="button" data-gehe="./post2-alt.html">Post 2 · Alternative</button>
<button type="button" data-gehe="./post3.html">Post 3 · Verbinden statt ersetzen</button>
<button type="button" data-gehe="./post4.html">Post 4 · Workflow-Engine</button>
<button type="button" data-gehe="./post5.html">Post 5 · Projektmanagement</button>
<button type="button" data-gehe="./post6.html">Post 6 · Team-Chat</button>
<button type="button" data-gehe="./post7.html">Post 7 · Form Builder</button>
<button type="button" data-gehe="./post8.html">Post 8 · E-Mail-Marketing</button>
<button type="button" data-gehe="./post9.html">Post 9 · Variante 1 · Webinar-Fenster</button>
<button type="button" data-gehe="./post9-v2.html">Post 9 · Variante 2 · Kundengeschichte</button>
<button type="button" data-gehe="./post9-v3.html">Post 9 · Variante 3 · Kundengeschichte dunkel</button>
<button type="button" data-gehe="./post9-v4.html">Post 9 · Variante 4 · Zahlen</button>
<button type="button" data-gehe="./post9-v5.html">Post 9 · Variante 5 · Zahlen dunkel</button>
<button type="button" data-gehe="./post9-v6.html">Post 9 · Variante 6 · Arbeitsheft</button>
<button type="button" data-gehe="./post9-v7.html">Post 9 · Variante 7 · Arbeitsheft dunkel</button>
<button type="button" data-gehe="./post9-varianten.html">Post 9 · Varianten im Vergleich</button>
<hr>
<button type="button" data-gehe="./uebersicht.html">Alle Projekte im Streifen</button>
<hr>
<button type="button" data-klick="snap-save">Stand merken</button>
<button type="button" data-klick="snap-load">Zurück zum gemerkten Stand</button>
@@ -971,9 +1021,13 @@
</main>
<aside class="panel panel-rechts" id="element-panel">
<!-- Der Schliessknopf steht vor der Ueberschrift und nicht dahinter. Am
rechten Rand war er der erste, der verschwand, sobald das Feld nicht
ganz ins Fenster passte -- und damit war das Feld nicht mehr
zuzumachen. Vorn ist er immer da. -->
<div class="el-kopf">
<button class="el-zu" id="el-zu" title="Auswahl aufheben (Esc)">✕</button>
<h1 id="el-titel">Element</h1>
<button class="el-zu" id="el-zu" title="Auswahl aufheben">✕</button>
</div>
<div id="el-inhalt"></div>
</aside>
@@ -1295,7 +1349,12 @@
Voreinstellung an und schreibt die beim ersten Klick ueber genau die
Datei, die gerade nicht verstanden wurde. Solange die Sperre steht, geht
nichts mehr raus, weder in die Datei noch in den localStorage. */
let stateLocked = false;
/* Im Uebersichts-Modus wird nichts gespeichert: drei Kacheln, die beim
Schliessen ihren Stand zurueckschreiben, waeren genau die Falle aus
POST-2.md 6. Die Konstante steht hier oben, weil stateLocked sie
braucht -- weiter unten waere sie noch nicht initialisiert. */
const NUR_STREIFEN = document.documentElement.dataset.nur === "streifen";
let stateLocked = NUR_STREIFEN;
function lockState(grund) {
stateLocked = true;
clearTimeout(pushTimer); pushTimer = null;
@@ -1486,7 +1545,10 @@
variant = d.variant || variant;
tilt = d.tilt || tilt;
superSample = d.superSample || superSample;
view = d.view || view;
/* Der Uebersichts-Modus zeigt immer den Streifen. Steht im Stand
"einzeln", waere sonst nur die zuletzt gewaehlte Slide zu sehen --
und stripWidth() meldete 1080 statt der vollen Breite. */
view = NUR_STREIFEN ? "streifen" : (d.view || view);
tagOut = d.tagOut || tagOut;
frameStyle = d.frameStyle || frameStyle;
current = Number.isInteger(d.current) ? Math.min(d.current, SLIDES.length - 1) : 0;
@@ -1525,7 +1587,8 @@
springt. */
/* zoom === null bedeutet: automatisch einpassen. Sobald der Nutzer zoomt,
hält der Wert, bis er auf "Fit" zurückgeht. */
let zoom = null;
// Im Uebersichts-Modus steht der Streifen fest auf 100 % -- skaliert wird aussen.
let zoom = NUR_STREIFEN ? 1 : null;
function stripWidth() { return view === "streifen" ? 1080 * SLIDES.length : 1080; }
@@ -1536,6 +1599,16 @@
return Math.min(availW / stripWidth(), availH / 1350, 0.45);
}
/* Die Uebersicht muss wissen, wie breit dieser Streifen ist -- die Zahl
der Slides ist je Projekt anders. Sie kommt per postMessage statt ueber
einen Griff ins iframe-Dokument: so bleibt die Uebersicht von den
Interna der Post-Seite unabhaengig. */
function groesseMelden() {
if (!NUR_STREIFEN || parent === window) return;
parent.postMessage({ hg: "streifen", w: stripWidth(), h: 1350,
titel: document.title }, "*");
}
function fitStrip() {
const wrap = document.getElementById("strip-wrap");
const k = zoom || autoFit();
@@ -1544,6 +1617,7 @@
wrap.style.height = Math.round(1350 * k) + "px";
const lvl = document.getElementById("zoom-fit");
if (lvl) lvl.textContent = zoom ? Math.round(k * 100) + " %" : "Fit";
groesseMelden();
}
function setZoom(k, anchor) {
@@ -1881,7 +1955,13 @@
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]);
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png");
for (const path of [...new Set(paths)]) {
const res = await fetch(path, { cache: "no-store" }).catch(() => null);
/* no-cache statt no-store: der Browser behaelt seine Kopie und fragt
nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso
sofort an, ein unveraendertes kostet ein 304 statt seiner vollen
Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber
uebersicht.html, die sechs Projekte als iframes laedt, waren das
38 MB und 21 Sekunden. */
const res = await fetch(path, { cache: "no-cache" }).catch(() => null);
if (!res || !res.ok) {
if (optional.has(path)) continue;
throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff"));
@@ -2193,6 +2273,92 @@
return n < 20 ? 1 : dunkel / n;
}
/* ─── Umlaute im gezeichneten Text ───────────────────────────────────────
Auf einer Slide stand einmal "Sanitaer" und "Klima / Lueftung". Beides
kam woertlich aus der Quelle -- im Klickdummy war es dort schludrig
eingetippt --, und beides las sich im fertigen Beitrag wie ein Fehler,
weil es einer ist. Gefunden hat es niemand beim Bauen, sondern erst
jemand, der auf den fertigen Post geschaut hat.
Deshalb prueft es jetzt der Export. Ein reines Muster genuegt dafuer
nicht: Deutsch schreibt "neue", "Dauer", "Michael" voellig zu Recht mit
ae/oe/ue. Nachgezaehlt ueber den gezeichneten Text aller Projekte sind es
aber nur eine Handvoll solcher Woerter -- eine Ausnahmeliste ist also
kurz und trifft trotzdem alles andere.
ss ist der umgekehrte Fall: fast immer richtig (dass, Schluss, Adresse,
Netzanschluss), nur ein paar Woerter brauchen ein scharfes s. Die stehen
deshalb einzeln als Liste und nicht als Muster.
Kanalnamen sind Absicht: "# waermepumpe-mueller" ist ein Handle, und die
schreiben Chatwerkzeuge klein und ohne Umlaute. */
/* Woerter, in denen ae/oe/ue kein verkruepppelter Umlaut ist, sondern zwei
Laute an einer Silbengrenze. Die Eintraege wirken als Wortanfang.
Die Liste ist kurz geworden, weil die Regel darunter den groessten Teil
der Ausnahmen von selbst erledigt: ein ersatzweise geschriebener Umlaut
steht nie hinter einem Vokal. Vorher standen hier dauer, neue, steuer,
feuer, bauer -- alle nach demselben Muster, und die Liste waere mit
jedem Beitrag weitergewachsen. Was bleibt, sind die echten Sonderfaelle:
"Michael" (ae hinter h), "aktuell" (ue hinter t). */
const UMLAUT_ERLAUBT = new Set([
"michael", "aktuell", "museum",
"waermepumpe-mueller", "waermepumpe-abnahme",
]);
const BRAUCHT_SCHARFES_S = new Set([
"aufmass", "strasse", "groesse", "groesser", "fuss", "fussboden",
"mass", "masse", "weiss", "heisst", "aussen", "gross", "grosse", "spass",
]);
function umlautFehler(wurzel) {
const raus = [];
wurzel.querySelectorAll("text, .headline, .body, .badge").forEach(e => {
/* Nicht textContent, sondern die Textknoten mit Leerzeichen dazwischen.
Eine Ueberschrift ist "Formulare selbst bauen.<br><span>Per
Drag-and-drop.</span>" -- textContent klebt daraus "bauen.Per"
zusammen, ein Wort, das es nicht gibt. Ein Umbruch und eine
Elementgrenze sind Wortgrenzen. */
const stuecke = [];
const lauf = document.createTreeWalker(e, NodeFilter.SHOW_TEXT);
for (let k = lauf.nextNode(); k; k = lauf.nextNode()) stuecke.push(k.nodeValue);
const ganz = stuecke.join(" ");
/* Geviertstriche gehoeren nicht in unsere Texte. Der Halbgeviertstrich
als Gedankenstrich ist richtiges Deutsch und bleibt erlaubt; der
lange Strich ist eine englische Angewohnheit und liest sich auf einer
deutschen Slide fremd. Er kam nicht aus einer Quelle, sondern beim
Schreiben mit hinein. */
if (ganz.indexOf("\u2014") >= 0) {
const stelle = "Geviertstrich in „" + ganz.trim().slice(0, 40) + "“";
if (!raus.includes(stelle)) raus.push(stelle);
}
ganz.split(/[^A-Za-zÄÖÜäöüß0-9./#@-]+/).forEach(w => {
if (!w) return;
const k = w.toLowerCase().replace(/^[@#]+/, "").replace(/[.,;:!?)(]+$/, "");
if (!k || raus.includes(w)) return;
/* Mailadressen und Domains sind ausgenommen. Dort ist die
umlautfreie Schreibung keine Schludrigkeit, sondern Pflicht:
"buero@muster-shk.de" kann gar kein ue ersetzen. Ohne diese
Ausnahme meldete Post 8 drei Adressen als Fehler. */
if (/@/.test(k) || /\.(de|com|net|org|eu|at|ch|io)$/.test(k)) return;
if ([...UMLAUT_ERLAUBT].some(a => k.startsWith(a))) return;
/* ae/oe/ue nur dann als verunglueckter Umlaut, wenn kein Vokal
davorsteht. Ein ae, oe oder ue, das fuer ae, oe oder ue steht,
folgt immer einem Konsonanten oder steht am Wortanfang: "fuer",
"ueber", "gruen", "Sanitaer", "Lueftung". Hinter einem Vokal sind
es dagegen immer zwei eigene Laute: bauen, Bauer, neue, Dauer,
Steuer, Feuer, Mauer, genauer. Die Regel ersetzt sechs Eintraege
in UMLAUT_ERLAUBT und alle, die dort noch gelandet waeren. */
if (/(^|[^aeiou])(ae|oe|ue)/.test(k) || BRAUCHT_SCHARFES_S.has(k)) raus.push(w);
});
});
return raus;
}
/* Fuer animation.mjs: das Bewegtbild laeuft nicht durch renderSlide und
braucht deshalb eine eigene Tuer zu derselben Pruefung. */
window.TEXT_PRUEFUNG = () => umlautFehler(document);
async function renderSlide(i) {
// Hilfslinien sind Werkzeug: koennten sie aus einem Tastendruck noch
// stehen, landeten sie im Bild.
@@ -2209,6 +2375,12 @@
el.dataset.editmode = "aus";
try {
await document.fonts.ready;
/* Den Text pruefen, bevor gerendert wird. Lieber ein Abbruch mit
klarer Meldung als ein fertiges PNG mit "Sanitaer" darauf. */
const uf = umlautFehler(el);
if (uf.length) throw new Error("Slide " + (i + 1) + ": " + uf.join(", ") +
". Fehlt ein Umlaut, schreib ihn; ist das Wort so richtig, gehoert es " +
"in UMLAUT_ERLAUBT. Einen Geviertstrich ersetzt ein Punkt oder ein Komma.");
const imgs = [...el.querySelectorAll("img")];
await Promise.all(imgs.map(im =>
im.complete ? im.decode().catch(() => {})
@@ -2447,7 +2619,7 @@
// Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt.
async function bildBereitstellen(pfad) {
if (assets[pfad]) return true;
const res = await fetch(pfad, { cache: "no-store" }).catch(() => null);
const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null);
if (!res || !res.ok) return false;
const blob = await res.blob();
assets[pfad] = await new Promise((ok, fail) => {
@@ -2991,10 +3163,24 @@
if (c) { put("#f-dx", c.x); put("#f-dy", c.y); }
}
document.getElementById("el-zu").addEventListener("click", () => {
/* Auswahl aufheben, und damit das rechte Feld zu. Zwei Wege, weil einer
zu wenig war: der Knopf setzt voraus, dass man ihn sieht, und genau das
war nicht immer der Fall. Escape geht immer.
Nicht wenn gerade in einem Feld getippt wird -- dort soll Escape die
Eingabe verlassen und nicht die Auswahl wegnehmen. */
function auswahlAufheben() {
if (!picked) return;
picked = null;
deck.querySelectorAll("[data-edit]").forEach(n => n.classList.remove("picked"));
syncEditPanel();
}
document.getElementById("el-zu").addEventListener("click", auswahlAufheben);
document.addEventListener("keydown", e => {
if (e.key !== "Escape") return;
const z = document.activeElement;
if (z && /^(INPUT|TEXTAREA|SELECT)$/.test(z.tagName)) return;
auswahlAufheben();
});
document.getElementById("el-inhalt").addEventListener("click", ev => {
+5774
View File
File diff suppressed because it is too large. Load diff
+1740 -202
View File
File diff suppressed because it is too large. Load diff
+5121
View File
File diff suppressed because it is too large. Load diff
+5908
View File
File diff suppressed because it is too large. Load diff
+6150
View File
File diff suppressed because it is too large. Load diff
+7091
View File
File diff suppressed because it is too large. Load diff
+7836
View File
File diff suppressed because it is too large. Load diff
+7875
View File
File diff suppressed because it is too large. Load diff
+111
View File
@@ -0,0 +1,111 @@
// Legt alle Varianten von Post 9 ins Marketing-Hub, damit Noah sie auf
// hivegrid-marketing.pages.dev sieht (Dejan, 04.11.2026). Die Seiten in
// PostGrid laufen nur lokal, deshalb kommen dort Bilder und Videos an:
//
// jede Slide als PNG, 1080 x 1080, so wie der Streifen sie zeigt
// die animierten Slides als MP4 aus animation/
// varianten.json mit Reihenfolge und Texten fuer die Webinar-Seite
//
// Aufruf, wenn PostGrid laeuft (./serve.sh): node post9-hub.mjs
// Danach im Hub committen, pushen und ./veroeffentlichen.sh.
//
// Vorher die Animationen neu bauen, wenn sich an ihnen etwas geaendert hat:
// node animation.mjs post9.html animWebinar 1x1
// node animation.mjs post9-v4.html animBuerokratie 1x1 (ebenso animKi, v5)
import { spawn } from "node:child_process";
import { writeFile, mkdir, copyFile, rm, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
const HUB = path.resolve("../HiveGrid Marketing /webinar/anzeigen/post9");
const POSTGRID = "http://127.0.0.1:4321/";
/* Reihenfolge im Carousel. "bild:n" ist Slide n des Streifens, "video:x"
eine Datei aus animation/. In Variante 1 steht das Video vorn, in 4 und 5
ersetzen die Videos Slide 2 und 3 des Streifens (dort steht ihr Endstand). */
const VARIANTEN = [
{ nr: 1, datei: "post9.html", name: "Webinar-Fenster",
text: "Das Bewegtbild zeigt das Webinar mit seinen Teilen und beiden Sprechern, danach die Anmeldung.",
folge: ["video:post9-animWebinar-1x1.mp4", "bild:0"] },
{ nr: 2, datei: "post9-v2.html", name: "Kundengeschichte",
text: "Angelehnt an ein Karussell von weclapp. Noah als Gesicht, dann vorher und heute am Beispiel „Angebot nachfassen“, dann die Anmeldung.",
folge: ["bild:0", "bild:1", "bild:2", "bild:3", "bild:4"] },
{ nr: 3, datei: "post9-v3.html", name: "Kundengeschichte dunkel",
text: "Wie Variante 2, auf dunklem Grund.",
folge: ["bild:0", "bild:1", "bild:2", "bild:3", "bild:4"] },
{ nr: 4, datei: "post9-v4.html", name: "Zahlen",
text: "Angelehnt an HERO Insights. Noah als Teaser, dann drei Zahlen aus Studien zur Zeit im Handwerk, dann die Anmeldung. Slide 1 bis 3 sind kurze Videos.",
folge: ["video:post9-v4-animTitel-1x1.mp4", "video:post9-v4-animBuerokratie-1x1.mp4", "video:post9-v4-animKi-1x1.mp4", "bild:3", "bild:4"] },
{ nr: 5, datei: "post9-v5.html", name: "Zahlen dunkel",
text: "Wie Variante 4, auf dunklem Grund.",
folge: ["video:post9-v5-animTitel-1x1.mp4", "video:post9-v5-animBuerokratie-1x1.mp4", "video:post9-v5-animKi-1x1.mp4", "bild:3", "bild:4"] },
{ nr: 6, datei: "post9-v6.html", name: "Arbeitsheft",
text: "Aussagen und Zeichnungen aus Noahs Arbeitsheft „Jede Frage lief über meinen Tisch“, im Look des Hefts, der Titel dunkel wie der Umschlag. Vom Engpass über die Inseln und den Bewegungsmelder bis zu den zehn Handgriffen, dann die Anmeldung. Auf dem Titel werden die Pfeile gezeichnet (Test).",
folge: ["video:post9-v6-animTitel-1x1.mp4", "bild:1", "bild:2", "bild:3", "bild:4", "bild:5"] },
{ nr: 7, datei: "post9-v7.html", name: "Arbeitsheft dunkel",
text: "Wie Variante 6, auf dunklem Grund.",
folge: ["video:post9-v7-animTitel-1x1.mp4", "bild:1", "bild:2", "bild:3", "bild:4", "bild:5"] },
];
const PORT = 9542;
const profil = path.join(tmpdir(), "pg-hub");
const chrome = spawn("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
["--headless", "--disable-gpu", "--hide-scrollbars", "--remote-debugging-port=" + PORT,
"--user-data-dir=" + profil, "about:blank"], { stdio: "ignore" });
let ziele;
for (let i = 0; i < 60; i++) {
ziele = await fetch(`http://127.0.0.1:${PORT}/json/list`).then(r => r.json()).catch(() => null);
if (ziele && ziele.some(t => t.type === "page")) break;
await new Promise(r => setTimeout(r, 200));
}
const ws = new WebSocket(ziele.find(t => t.type === "page").webSocketDebuggerUrl);
const offen = new Map(); let n = 0;
ws.onmessage = e => { const m = JSON.parse(e.data); if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id); } };
await new Promise(ok => ws.onopen = ok);
const ruf = (method, params = {}) => new Promise((ok, weg) => { const id = ++n;
offen.set(id, m => m.error ? weg(new Error(m.error.message)) : ok(m.result)); ws.send(JSON.stringify({ id, method, params })); });
const warte = ms => new Promise(r => setTimeout(r, ms));
await ruf("Page.enable"); await ruf("Runtime.enable");
await rm(HUB, { recursive: true, force: true });
await mkdir(HUB, { recursive: true });
const liste = [];
for (const v of VARIANTEN) {
/* Breit genug fuer jeden Streifen, danach auf die echte Breite stellen. */
await ruf("Emulation.setDeviceMetricsOverride", { width: 7560, height: 1080, deviceScaleFactor: 1, mobile: false });
await ruf("Page.navigate", { url: POSTGRID + v.datei + "?nur=streifen&format=1x1" });
await warte(4500);
const breite = (await ruf("Runtime.evaluate", { expression: "document.documentElement.scrollWidth", returnByValue: true })).result.value;
const anzahl = Math.round(breite / 1080);
const slides = [];
for (const [i, teil] of v.folge.entries()) {
const [art, wert] = teil.split(":");
const name = `v${v.nr}-${i + 1}` + (art === "video" ? ".mp4" : ".png");
if (art === "video") {
await copyFile(path.join("animation", wert), path.join(HUB, name));
} else {
const k = Number(wert);
if (k >= anzahl) throw new Error(`${v.datei}: Slide ${k + 1} fehlt im Streifen (${anzahl} Slides)`);
const bild = await ruf("Page.captureScreenshot", { format: "png",
clip: { x: k * 1080, y: 0, width: 1080, height: 1080, scale: 1 } });
await writeFile(path.join(HUB, name), Buffer.from(bild.data, "base64"));
}
slides.push({ art: art === "video" ? "video" : "bild", datei: name });
}
liste.push({ nr: v.nr, name: v.name, text: v.text, slides });
console.log(`Variante ${v.nr}: ${slides.length} Elemente`);
}
const heute = new Date().toISOString().slice(0, 10);
await writeFile(path.join(HUB, "varianten.json"), JSON.stringify({
titel: "Post 9 · Webinar-Anzeige",
stand: heute,
hinweis: "Format 1:1 für die Anzeige. Datum und Uhrzeit des Webinars sind noch Platzhalter.",
varianten: liste,
}, null, 1) + "\n");
console.log("Fertig:", (await readdir(HUB)).length, "Dateien in", HUB);
ws.close();
if (chrome.exitCode === null) { const zu = new Promise(ok => chrome.once("exit", ok)); chrome.kill(); await zu; }
await rm(profil, { recursive: true, force: true }).catch(() => {});
+8486
View File
File diff suppressed because it is too large. Load diff
+8490
View File
File diff suppressed because it is too large. Load diff
+8714
View File
File diff suppressed because it is too large. Load diff
+8717
View File
File diff suppressed because it is too large. Load diff
+8876
View File
File diff suppressed because it is too large. Load diff
+8879
View File
File diff suppressed because it is too large. Load diff
+208
View File
@@ -0,0 +1,208 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PostGrid · Post 9 · Varianten</title>
<link rel="stylesheet" href="./assets/fonts/manrope.css">
<style>
/* ─── Post 9 im Vergleich ─────────────────────────────────────────────────
Die Varianten der Webinar-Anzeige untereinander, wie die Uebersicht aller
Projekte: jede Variante kommt als iframe im Modus ?nur=streifen und zeigt
genau, was der Generator zeigt. Variante 1 beginnt mit dem Bewegtbild.
Das steht nicht im Streifen (Animationen liegen ausserhalb), deshalb
laeuft davor das exportierte MP4 im passenden Format. */
* { box-sizing: border-box; }
:root { color-scheme: dark; }
html, body { height: 100%; }
body {
margin: 0; font-family: Manrope, system-ui, sans-serif;
background: #101116; color: #C7CCD6; overflow: hidden;
}
.menubar {
position: relative; z-index: 10; display: flex; align-items: center; gap: 2px;
height: 34px; padding: 0 10px; background: #16181F;
border-bottom: 1px solid #23262F; font-size: 13px; user-select: none;
}
.menubar .marke { font-weight: 800; color: #FFFFFF; margin-right: 14px; letter-spacing: -.01em; }
.menubar a {
color: #C7CCD6; text-decoration: none; padding: 5px 10px; border-radius: 6px;
}
.menubar a:hover { background: #23262F; color: #FFFFFF; }
.stage { position: absolute; inset: 34px 0 0; overflow-y: auto; overflow-x: hidden;
background-color: #101116;
background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px);
background-size: 26px 26px;
}
.flaeche { padding: 48px 54px 80px; display: flex; flex-direction: column; gap: 56px; }
.kopf h1 { margin: 0; font-size: 26px; font-weight: 800; color: #FFFFFF; letter-spacing: -.01em; }
.kopf p { margin: 6px 0 0; font-size: 14px; color: #6E7480; }
.variante { display: flex; flex-direction: column; gap: 14px; }
.variante h2 {
margin: 0; font-size: 13px; font-weight: 800; letter-spacing: .14em;
text-transform: uppercase; color: #6E7480;
}
.variante h2 b { color: #F1B729; font-weight: 800; }
.variante h2 span { color: #FFFFFF; }
.variante p { margin: 0; max-width: 820px; font-size: 14px; line-height: 1.55; color: #9AA0AC; }
.reihe { display: flex; gap: 0; align-items: flex-start; }
.rahmen { overflow: hidden; background: #FFFFFF; border: 1px solid #2E313B; flex: none; }
.rahmen + .rahmen { border-left: 0; }
.rahmen iframe {
display: block; border: 0; transform-origin: 0 0; background: #FFFFFF; pointer-events: none;
}
.rahmen video { display: block; width: 100%; height: 100%; object-fit: cover; }
.marke-video {
font-size: 11px; color: #6E7480; letter-spacing: .06em; text-transform: uppercase; margin-top: 8px;
}
</style>
</head>
<body>
<div class="menubar">
<span class="marke">PostGrid</span>
<a href="./uebersicht.html">Alle Projekte</a>
<a href="./post9.html">Variante 1 bearbeiten</a>
<a href="./post9-v2.html">Variante 2 bearbeiten</a>
<a href="./post9-v3.html">Variante 3 bearbeiten</a>
<a href="./post9-v4.html">Variante 4 bearbeiten</a>
<a href="./post9-v5.html">Variante 5 bearbeiten</a>
<a href="./post9-v6.html">Variante 6 bearbeiten</a>
<a href="./post9-v7.html">Variante 7 bearbeiten</a>
</div>
<main class="stage" id="stage">
<div class="flaeche">
<div class="kopf">
<h1>Post 9 · Webinar-Anzeige</h1>
<p id="meta">Sieben Varianten derselben Anzeige. Lade Streifen ...</p>
</div>
<section class="variante">
<h2><b>Variante 1</b> · <span>Webinar-Fenster</span> · fertig seit 17.10.</h2>
<p>Das Bewegtbild zeigt das Webinar mit seinen vier Teilen und beiden Sprechern, danach die Anmeldung.
Zwei Elemente, das erste ist ein Video. Als Anzeige deshalb in 1:1.</p>
<div class="reihe" id="reihe1">
<div class="rahmen" id="video1"><video id="anim1" autoplay muted loop playsinline></video></div>
<div class="rahmen"><iframe id="f1" src="./post9.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 2</b> · <span>Kundengeschichte</span> · angelehnt an weclapp</h2>
<p>Komposition wie das Karussell von weclapp zu PV Green (Werbebibliothek, ID 792638160291622): Etikett
„Live-Webinar“ und Foto, Zitat mit freigestellter Person, Vorher, Heute, Anmeldung. Seit 04.11. ohne HiveGrid
im Text und ohne Fachwörter. Die Geschichte: Noah kriegt nicht in die Ausführung, was in seinem Kopf ist.
Vorher hing das Nachfassen am Chef (Karte wie bei weclapp), heute laufen dieselben drei Schritte als Kette von selbst
(Schema wie bei Zapier), wie der Ablauf „Angebot nachfassen“ im Webinar Teil 2. Farben, Schrift und Formen von uns. Fünf Bilder, kein Video, als Anzeige also auch in
4:5 möglich.</p>
<div class="reihe">
<div class="rahmen"><iframe id="f2" src="./post9-v2.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 3</b> · <span>Kundengeschichte dunkel</span> · wie Variante 2</h2>
<p>Dieselben fünf Slides wie Variante 2 auf dem dunklen Grund des Posts. Texte in Weiß, Formen und Wabenband in
einem helleren Blau, Gelb bleibt. Was auf Gelb und im Geerkens-Chip steht, bleibt dunkel.</p>
<div class="reihe">
<div class="rahmen"><iframe id="f3" src="./post9-v3.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 4</b> · <span>Zahlen</span> · angelehnt an HERO Insights</h2>
<p>Komposition wie das Karussell „HERO Insights“ (Werbebibliothek, ID 3818182885142025): dunkles Titelbild,
dann Karten mit Titel, schwarzem und gelbem Balken und einer Grafik. Titelbild mit Noah als Teaser fürs Webinar,
dann 100 Waben als Band für 8 Stunden Bürokratie, eine große 4 mit den 100 Waben als Block für KI, zuletzt 72 %
„zu ausgelastet“ groß im gelben Kasten. Jede Karte sieht anders aus. Titel, Bürokratie und KI laufen als Video:
eine gelbe Welle durchs Wabenmuster, dann die Waben erst gelb, die wichtigen zuletzt rot.
Zahlen aus KfW Research (April 2025) und Bitkom (504 Betriebe, 2025), Quelle auf jeder Karte.</p>
<div class="reihe">
<div class="rahmen"><iframe id="f4" src="./post9-v4.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 5</b> · <span>Zahlen dunkel</span> · wie Variante 4</h2>
<p>Dieselben fünf Karten wie Variante 4 auf dem dunklen Grund des Posts.</p>
<div class="reihe">
<div class="rahmen"><iframe id="f5" src="./post9-v5.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 6</b> · <span>Arbeitsheft</span> · angelehnt an Noahs E-Book</h2>
<p>Aussagen und Zeichnungen aus Noahs Arbeitsheft „Jede Frage lief über meinen Tisch“: Titel dunkel wie der
Umschlag, mit den drei Fragen in Handschrift, der Rest weiß, dann Engpass, Inseln, Bewegungsmelder, zehn Handgriffe bei einer Wärmepumpe
und die Anmeldung. Manrope, Literata und Caveat wie im Heft, Skizzen-Kästen und gelbe Pfeile nach der Website.
Der Titel läuft als Video, die Pfeile werden wie mit dem Stift gezogen (Test).</p>
<div class="reihe">
<div class="rahmen"><iframe id="f6" src="./post9-v6.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
<section class="variante">
<h2><b>Variante 7</b> · <span>Arbeitsheft dunkel</span> · wie Variante 6</h2>
<p>Dieselben sechs Slides auf dem dunklen Grund. Die Zeichnungen liegen auf weißen Karten, wie im Heft auf den
dunklen Seiten.</p>
<div class="reihe">
<div class="rahmen"><iframe id="f7" src="./post9-v7.html?nur=streifen" loading="eager"></iframe></div>
</div>
</section>
</div>
</main>
<script>
(() => {
"use strict";
const stage = document.getElementById("stage");
const meta = document.getElementById("meta");
/* Breite und Hoehe melden die Streifen selbst, sobald sie stehen. */
const st = {
f1: { el: document.getElementById("f1"), w: 1080, h: 1080, da: false },
f2: { el: document.getElementById("f2"), w: 5400, h: 1080, da: false },
f3: { el: document.getElementById("f3"), w: 5400, h: 1080, da: false },
f4: { el: document.getElementById("f4"), w: 5400, h: 1080, da: false },
f5: { el: document.getElementById("f5"), w: 5400, h: 1080, da: false },
f6: { el: document.getElementById("f6"), w: 6480, h: 1080, da: false },
f7: { el: document.getElementById("f7"), w: 6480, h: 1080, da: false },
};
const video = document.getElementById("anim1"), videoRahmen = document.getElementById("video1");
window.addEventListener("message", e => {
const d = e.data;
if (!d || d.hg !== "streifen") return;
const r = Object.values(st).find(x => x.el.contentWindow === e.source);
if (!r) return;
r.w = d.w; r.h = d.h; r.da = true;
legen();
});
/* Alle Slides im selben Massstab, eingepasst an die laengste Reihe. */
function legen() {
const breit = Math.max(st.f1.w + 1080, st.f2.w, st.f3.w, st.f4.w, st.f5.w, st.f6.w, st.f7.w);
const k = Math.min((stage.clientWidth - 108) / breit, 0.42);
Object.values(st).forEach(r => {
r.el.style.width = r.w + "px";
r.el.style.height = r.h + "px";
r.el.style.transform = "scale(" + k + ")";
r.el.parentElement.style.width = Math.round(r.w * k) + "px";
r.el.parentElement.style.height = Math.round(r.h * k) + "px";
});
/* Das Video im Format der Variante 1: 1:1 oder 4:5. */
videoRahmen.style.width = Math.round(1080 * k) + "px";
videoRahmen.style.height = Math.round(st.f1.h * k) + "px";
const quelle = "./animation/post9-animWebinar" + (st.f1.h === 1080 ? "-1x1" : "") + ".mp4";
if (!video.src.endsWith(quelle.slice(1))) { video.src = quelle; video.play().catch(() => {}); }
const fertig = Object.values(st).every(r => r.da);
meta.textContent = fertig
? "Sieben Varianten derselben Anzeige · Variante 1: 1 Video, 1 Bild · Varianten 2 bis 5: je 5 Elemente · 6 und 7: je 6 Bilder · Format " +
Object.values(st).map(r => r.h === 1080 ? "1:1" : "4:5").join(", ")
: "Sieben Varianten derselben Anzeige. Lade Streifen ...";
}
window.addEventListener("resize", legen);
legen();
})();
</script>
</body>
</html>
+8114
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+535
View File
@@ -0,0 +1,535 @@
#!/usr/bin/env python3
"""Baut den Redaktionsplan als A4-PDF.
Aufbau wie beim Webinar-Vergleich: eine HTML-Datei mit eingebettetem Manrope
und eingebettetem Logo, die von Chrome nach PDF gedruckt wird. Eingebettet
heißt hier wörtlich, die Datei läuft ohne Netz und ohne Nachbarordner.
python3 redaktionsplan/bauen.py
"""
import base64
import subprocess
from datetime import date, timedelta
from pathlib import Path
HIER = Path(__file__).resolve().parent
REPO = HIER.parent
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# Schrift und Logo kommen aus dem Repo selbst, nicht aus einem Nachbarordner:
# das PDF soll sich auf jedem Rechner bauen lassen, der nur dieses Repo hat.
# Manrope liegt als variables Latin-Subset vor, 200 bis 800.
FONT = base64.b64encode(
(REPO / "assets/fonts/xn7gYHE41ni1AdIRggexSvfedN4.woff2").read_bytes()).decode()
LOGO = base64.b64encode(
(REPO / "assets/brand/logo.svg").read_bytes()).decode()
TAGE = {0: "Mo", 2: "Mi", 4: "Fr"}
# Der erste Termin. Am 12.08. war der Start geplant, erschienen ist "Die
# Geschichte" aber erst am Freitag darauf -- der ganze Plan ist damit um
# einen Termin nach hinten gerutscht.
START = date(2026, 8, 14)
# Termine, die ausfallen. Der Plan rutscht ab dort um einen Slot nach hinten,
# die Beitraege davor behalten ihr Datum -- sie sind schon erschienen.
#
# Warum nicht einfach START verschieben: das haette auch die vier Beitraege
# mitgenommen, die am 14., 17., 19. und 21.08. tatsaechlich draussen waren.
# Ein Plan, der rueckwirkend andere Termine behauptet, ist keiner.
#
# 24.08. Team-Chat war nicht fertig, rueckt auf Mi 26.08.
# 26.08. und weiter geschoben: der Beitrag lag noch beim Feinschliff
# 28.08. erschienen ist er am Mo 31.08.
# 02.09. Form Builder ruecken wir bewusst auf Mo 07.09., statt ihn zwei
# 04.09. Tage nach dem Team-Chat hinterherzuschicken
AUSGELASSEN = {"24.08.", "26.08.", "28.08.", "02.09.", "04.09."}
def slots(start: date, anzahl: int, auslassen=frozenset()):
"""Die Mo/Mi/Fr-Termine ab dem Starttag, ohne die ausgelassenen."""
raus, d = [], start
while len(raus) < anzahl:
if d.weekday() in TAGE:
stempel = d.strftime("%d.%m.")
if stempel not in auslassen:
raus.append((TAGE[d.weekday()], stempel))
d += timedelta(days=1)
return raus
# Thema, Format, Ziel. Reihenfolge ist nicht beliebig: erst warum es uns gibt,
# dann die Haltung, dann die Funktionen von der täglichen Last nach hinten zu
# dem, was Vorwissen braucht.
# E-Mail-Marketing und Zettelwirtschaft sind am 11.09.2026 getauscht worden.
# Die Reihenfolge geht sonst von der taeglichen Last nach hinten zu dem, was
# Vorwissen braucht -- Zettelwirtschaft ist aber der einzige Beitrag ohne
# Produkt dahinter und muss deshalb erst gebaut werden, waehrend E-Mail-
# Marketing eine fertige Oberflaeche zum Zeigen hat.
PLAN = [
("Die Geschichte", "Carousel, 6", "Herkunft. Warum es HiveGrid gibt."),
("Verbinden statt ersetzen", "Einzelbild", "Haltung. Die Kernaussage in einem Satz."),
("Workflow-Engine", "Carousel", "Wenn-dann statt Handarbeit."),
("Projektmanagement", "Carousel", "Der Überblick, den jeder Betrieb vermisst."),
("Team-Chat", "Carousel", "Ein Kanal pro Projekt, Foto rein, Aufgabe raus."),
("Form Builder", "Carousel", "Vom Kontaktformular bis zum Aufmaß."),
("E-Mail-Marketing", "Carousel", "Aus Anfragen werden Kontakte, aus Kontakten Aufträge."),
("Zettelwirtschaft", "Carousel", "Wissen ohne Produkt. Wo im Betrieb Zeit verloren geht."),
("Mitarbeiter-Management", "Carousel", "Wer kann was, wer ist wo, wer braucht was."),
("Der Betrieb dahinter", "Carousel", "Vierte Generation SHK. Keine Studio-Bilder."),
("KI-Anbindung, MCP", "Carousel", "Externe KI darf mitlesen, ohne Datenabfluss."),
("Dein ERP bleibt", "Einzelbild", "Der Einwand Nummer eins, direkt beantwortet."),
("Tempo und Übersicht", "Reel", "Die Oberfläche selbst. Wenige Klicks, kein Suchen."),
("Warteliste", "Carousel", "Aufruf. Was die ersten Betriebe bekommen."),
("Häufige Fragen", "Carousel", "Die fünf Einwände, die immer kommen."),
]
TERMINE = slots(START, len(PLAN), AUSGELASSEN)
# Wie viele Beitraege fertig im Generator liegen. Sie sind in der Tabelle
# gelb hinterlegt, und der Satz darueber nennt die Zahl -- beides kommt aus
# derselben Konstante, damit es nicht auseinanderlaeuft.
FERTIG = 7
ZAHLWORT = {1: "Der eine markierte Termin ist", 2: "Die beiden markierten Termine sind",
3: "Die drei markierten Termine sind", 4: "Die vier markierten Termine sind",
5: "Die fünf markierten Termine sind", 6: "Die sechs markierten Termine sind",
7: "Die sieben markierten Termine sind",
8: "Die acht markierten Termine sind"}[FERTIG]
# Der erste Reel-Termin, gerechnet statt abgeschrieben. Im Text stand er
# einmal fest als "09.09." und stimmte schon damals nicht. Auch der Kommentar,
# der ihn danach auf dem 11.09. verortete, war nach der naechsten Verschiebung
# wieder falsch -- ein Datum, das sich aus AUSGELASSEN ergibt, gehoert
# nirgendwo als Zahl hingeschrieben, auch nicht in einen Kommentar.
REEL = next(d for p, (t, d) in zip(PLAN, TERMINE) if p[1] == "Reel")
SPANNE = TERMINE[0][1] + " bis " + TERMINE[-1][1] + START.strftime("%Y")
ZEILEN = "".join(
'<tr{hi}><td class="dt"><b>{tag}</b> {datum}</td><td><b>{thema}</b></td>'
'<td class="fm">{format}</td><td class="zl">{ziel}</td></tr>'.format(
hi=' class="hi"' if i < FERTIG else "",
tag=t, datum=d, thema=p[0], format=p[1], ziel=p[2],
)
for i, (p, (t, d)) in enumerate(zip(PLAN, TERMINE))
)
HTML = f"""<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Instagram: Redaktionsplan und Vorgehen</title>
<style>
@font-face{{font-family:'Manrope';font-style:normal;font-weight:200 800;
src:url(data:font/woff2;base64,{FONT}) format('woff2');font-display:block}}
:root{{
--ink:#14161A; --ink2:#4A5158; --ink3:#868D95; --ink4:#AAB1B8;
--line:#E2E6EA; --line-soft:#EFF2F4;
--paper2:#F6F8FA; --yellow:#F1B729; --yellow-soft:#FDF3DC;
--navy:#0F1A2E; --teal:#0D6E6E; --rot:#C0554B;
}}
*{{box-sizing:border-box;margin:0;padding:0}}
html{{-webkit-font-smoothing:antialiased}}
body{{font-family:'Manrope',system-ui,sans-serif;color:var(--ink);background:#8A9099;
font-size:12.4px;line-height:1.5;font-variant-numeric:tabular-nums;letter-spacing:-.003em}}
@page{{size:A4;margin:0}}
.page{{width:210mm;height:296.6mm;background:#fff;position:relative;overflow:hidden;
padding:16mm 16mm 13mm;page-break-after:always;margin:0 auto 8mm;display:flex;flex-direction:column}}
.page:last-child{{page-break-after:auto;margin-bottom:0}}
@media print{{body{{background:#fff}}.page{{margin:0}}}}
h1{{font-size:29px;font-weight:800;line-height:1.08;letter-spacing:-.03em}}
h2{{font-size:16px;font-weight:800;letter-spacing:-.018em;line-height:1.2}}
h3{{font-size:12.4px;font-weight:800;letter-spacing:-.01em}}
p{{margin-bottom:7px}}p:last-child{{margin-bottom:0}}
strong{{font-weight:700}}
.lead{{font-size:13.6px;line-height:1.48;color:var(--ink2);font-weight:500;letter-spacing:-.01em}}
.small{{font-size:10.8px;line-height:1.45;color:var(--ink2)}}
.micro{{font-size:9.4px;line-height:1.4;color:var(--ink3)}}
.eyebrow{{font-size:8.4px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--ink3)}}
.top{{display:flex;justify-content:space-between;align-items:center;
border-bottom:1px solid var(--line);padding-bottom:8px;margin-bottom:16px;flex:0 0 auto}}
.foot{{position:absolute;left:16mm;right:16mm;bottom:7mm;display:flex;justify-content:space-between;
font-size:8.4px;letter-spacing:.05em;color:var(--ink4);border-top:1px solid var(--line-soft);padding-top:5px}}
.body{{flex:1 1 auto;min-height:0}}
.chamfer{{clip-path:polygon(10px 0,calc(100% - 10px) 0,100% 10px,100% calc(100% - 10px),
calc(100% - 10px) 100%,10px 100%,0 calc(100% - 10px),0 10px)}}
.grid2{{display:grid;grid-template-columns:1fr 1fr;gap:13px}}
.grid3{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:13px}}
.card{{background:var(--paper2);padding:13px 15px}}
.card--line{{background:#fff;border:1px solid var(--line);padding:13px 15px}}
.reco{{background:var(--navy);color:#fff;padding:14px 18px;display:flex;align-items:center;gap:22px}}
.reco .lbl{{font-size:8.4px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--yellow);margin-bottom:4px}}
.reco .big{{font-size:19px;font-weight:800;letter-spacing:-.025em;line-height:1}}
.reco .sep{{width:1px;height:34px;background:rgba(255,255,255,.2)}}
.stat{{padding:12px 14px;background:var(--paper2)}}
.stat .k{{font-size:8.2px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--ink3);margin-bottom:6px}}
.stat .v{{font-size:27px;font-weight:800;letter-spacing:-.035em;line-height:1}}
.stat .v .u{{font-size:14px;font-weight:700}}
.stat .s{{font-size:9.4px;color:var(--ink3);margin-top:5px;line-height:1.3}}
.stat--acc{{background:var(--navy);color:#fff}}
.stat--acc .k{{color:#7E8CA0}}.stat--acc .v{{color:var(--yellow)}}.stat--acc .s{{color:#9AA6B6}}
table{{width:100%;border-collapse:collapse;font-size:11px}}
th{{text-align:left;font-size:8.4px;font-weight:800;letter-spacing:.11em;text-transform:uppercase;
color:var(--ink3);padding:0 8px 5px;border-bottom:1.5px solid var(--ink);vertical-align:bottom}}
td{{padding:6px 8px;border-bottom:1px solid var(--line-soft);vertical-align:top;line-height:1.35}}
tr:last-child td{{border-bottom:none}}
th:first-child,td:first-child{{padding-left:0}}
th:last-child,td:last-child{{padding-right:0}}
tr.hi td{{background:var(--yellow-soft)}}
tr.hi td:first-child{{box-shadow:inset 3px 0 0 var(--yellow);padding-left:9px}}
td.dt{{white-space:nowrap;color:var(--ink2)}}
td.fm{{white-space:nowrap;color:var(--ink2);font-size:10.4px}}
td.zl{{color:var(--ink2)}}
ul{{list-style:none}}
li{{position:relative;padding-left:14px;margin-bottom:5px;font-size:11px;line-height:1.42;color:var(--ink2)}}
li:last-child{{margin-bottom:0}}
li::before{{content:"";position:absolute;left:0;top:6px;width:5px;height:5px;background:var(--yellow)}}
li b{{color:var(--ink)}}
ul.num{{counter-reset:n}}
ul.num li{{padding-left:20px;counter-increment:n}}
ul.num li::before{{content:counter(n);background:none;color:var(--ink3);font-size:9.4px;
font-weight:800;top:0;left:0;width:auto;height:auto}}
</style>
</head>
<body>
<!-- SEITE 1 -->
<section class="page">
<div class="top">
<img src="data:image/svg+xml;base64,{LOGO}" style="height:17px" alt="HiveGrid">
<div class="eyebrow">Instagram &middot; Redaktionsplan &amp; Vorgehen &middot; Stand 23.08.2026</div>
</div>
<div class="body">
<h1 style="margin-bottom:9px">Rahmen und Rhythmus</h1>
<p class="lead" style="margin-bottom:14px;max-width:660px">
In dieser Phase erwarten wir über den Kanal keine nennenswerte Reichweite. Er dient
zuerst der <strong style="color:var(--ink)">Darstellung der Software</strong> für Leute,
die ohnehin auf uns stoßen: mögliche Kunden, Partner, Investoren. Dieses Dokument legt
fest, was wann erscheint, wie es aussieht und was wir nach vier Wochen ansehen.
</p>
<div class="reco chamfer" style="margin-bottom:15px">
<div>
<div class="lbl">Rhythmus</div>
<div class="big">Mo &middot; Mi &middot; Fr</div>
</div>
<div class="sep"></div>
<div>
<div class="lbl" style="color:#7A879B">Uhrzeit</div>
<div class="big" style="color:var(--yellow)">12:00</div>
</div>
<div class="sep"></div>
<div style="flex:1">
<div class="lbl" style="color:#7A879B">Worauf es ankommt</div>
<div style="font-size:11px;color:#C8D0DC;line-height:1.4">
Auf dem Eindruck der ersten neun Kacheln. Die sieht jeder, der zum ersten Mal
auf dem Profil landet, bevor er einen einzelnen Beitrag öffnet.</div>
</div>
</div>
<div class="grid3" style="margin-bottom:16px">
<div class="stat chamfer">
<div class="k">Beiträge</div>
<div class="v">3<span class="u"> / Woche</span></div>
<div class="s">12 in vier Wochen. Mehr als fünf ginge auf die Qualität.</div>
</div>
<div class="stat chamfer">
<div class="k">Format zuerst</div>
<div class="v">Carousel</div>
<div class="s">Trägt Erklärung und lässt sich im Generator wiederholbar bauen.</div>
</div>
<div class="stat stat--acc chamfer">
<div class="k">Durchgängig</div>
<div class="v">Ein Raster</div>
<div class="s">Gleiche Schrift, gleicher Grund, gleiche Bildsprache über alle Beiträge.</div>
</div>
</div>
<h2 style="margin-bottom:8px">Wofür der Kanal da ist</h2>
<div class="grid2" style="margin-bottom:15px">
<div class="card chamfer">
<h3 style="margin-bottom:5px">Zeigen statt behaupten</h3>
<p class="small">Jeder Beitrag zeigt etwas Konkretes aus der Software oder aus dem
Betrieb: einen Screen, einen Ablauf, eine Aufnahme von der Baustelle. Keine
Behauptungen ohne Bild dazu.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:5px">Titelbilder tragen allein</h3>
<p class="small">Im Raster ist von einem Carousel nur die erste Slide zu sehen.
Die trägt deshalb einen vollständigen Gedanken, nicht den Anfang von einem. Der Rest
steht innen und in der Bildunterschrift.</p>
</div>
</div>
<h2 style="margin-bottom:8px">Warum drei Tage und warum 12:00</h2>
<div class="grid3">
<div class="card chamfer">
<h3 style="margin-bottom:5px">Drei ist die untere Grenze</h3>
<p class="small">Unter drei Beiträgen pro Woche wirkt ein Profil inaktiv. Über
fünf leidet die Qualität.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:5px">12:00 ist eine Annahme</h3>
<p class="small">Für handwerklich geprägte Betriebe sind drei Fenster belegt: 6 bis 8,
12 bis 13 und 19 bis 21 Uhr. Die Mittagspause passt am besten.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:5px">Montag ist der schwächste</h3>
<p class="small">Die Studienlage nennt Dienstag bis Donnerstag. Wir starten trotzdem
mit Mo, Mi, Fr und schauen es uns in vier Wochen an.</p>
</div>
</div>
</div>
<div class="foot"><span>HiveGrid &middot; Social Media</span><span>Seite 1 von 4</span></div>
</section>
<!-- SEITE 2: REDAKTIONSPLAN -->
<section class="page">
<div class="top">
<img src="data:image/svg+xml;base64,{LOGO}" style="height:17px" alt="HiveGrid">
<div class="eyebrow">Redaktionsplan &middot; {SPANNE}</div>
</div>
<div class="body">
<h1 style="margin-bottom:9px">Was wann erscheint</h1>
<p class="lead" style="margin-bottom:13px;max-width:660px">
Fünfzehn Termine, alle um 12:00 Uhr. Die Reihenfolge ist nicht beliebig: erst warum
es uns gibt, dann die Haltung, dann die Funktionen. Innerhalb der Funktionen von der
täglichen Last nach hinten zu dem, was Vorwissen braucht. {ZAHLWORT} gesetzt und
liegen fertig im Generator, alles danach darf sich verschieben.
</p>
<table style="margin-bottom:14px">
<thead><tr>
<th style="width:76px">Termin</th>
<th style="width:186px">Thema</th>
<th style="width:92px">Format</th>
<th>Wofür der Beitrag da ist</th>
</tr></thead>
<tbody>{ZEILEN}</tbody>
</table>
<h2 style="margin-bottom:8px">Sechs Sorten im Wechsel</h2>
<table style="margin-bottom:15px">
<thead><tr>
<th style="width:150px">Sorte</th>
<th style="width:64px" class="t-num">Anteil</th>
<th>Beispiel aus dem Plan</th>
</tr></thead>
<tbody>
<tr><td><b>Funktion erklärt</b></td><td class="t-num">8</td>
<td>Workflow-Engine, Form Builder, Team-Chat</td></tr>
<tr><td><b>Menschen und Betrieb</b></td><td class="t-num">2</td>
<td>Die Geschichte, Der Betrieb dahinter</td></tr>
<tr><td><b>Haltung</b></td><td class="t-num">1</td>
<td>Verbinden statt ersetzen</td></tr>
<tr><td><b>Einwand beantwortet</b></td><td class="t-num">2</td>
<td>Dein ERP bleibt, Häufige Fragen</td></tr>
<tr><td><b>Wissen ohne Produkt</b></td><td class="t-num">1</td>
<td>Zettelwirtschaft</td></tr>
<tr><td><b>Aufruf</b></td><td class="t-num">1</td>
<td>Warteliste</td></tr>
</tbody>
</table>
<h2 style="margin-bottom:8px">Was unter jedem Beitrag steht</h2>
<div class="grid3">
<div class="card chamfer">
<h3 style="margin-bottom:5px">Die ersten 125 Zeichen</h3>
<p class="small">Mehr sieht niemand, bevor er auf "mehr" tippt. Der Haken gehört
dorthin, nicht in den letzten Absatz.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:5px">300 bis 800 Zeichen</h3>
<p class="small">Der Bereich, in dem erklärende Beiträge am besten laufen. Darunter
wirkt es dünn, darüber liest es niemand zu Ende.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:5px">5 bis 10 Hashtags</h3>
<p class="small">Ganz unten, durch Leerzeilen abgesetzt. Schlägt sowohl drei als auch
dreißig. Die Suchwörter gehören zusätzlich in den Fließtext.</p>
</div>
</div>
</div>
<div class="foot"><span>HiveGrid &middot; Social Media</span><span>Seite 2 von 4</span></div>
</section>
<!-- SEITE 3: BILDSPRACHE -->
<section class="page">
<div class="top">
<img src="data:image/svg+xml;base64,{LOGO}" style="height:17px" alt="HiveGrid">
<div class="eyebrow">Bildsprache &amp; Format</div>
</div>
<div class="body">
<h1 style="margin-bottom:9px">Bildsprache</h1>
<p class="lead" style="margin-bottom:14px;max-width:660px">
Das Profil zeigt neun Kacheln auf einmal. Was gleich bleibt und was wechselt, ist
deshalb festgelegt und nicht von Beitrag zu Beitrag neu entschieden.
</p>
<div class="grid2" style="margin-bottom:16px">
<div class="card chamfer">
<h3 style="margin-bottom:6px">Was gleich bleibt</h3>
<ul>
<li><b>1080 × 1350 px</b>, das hochkantigste Feed-Format</li>
<li><b>Manrope</b>, dieselbe Schrift wie auf der Website</li>
<li><b>Ein Grund</b>, das Raster, sonst nichts</li>
<li><b>Ein Akzentwort</b> je Überschrift, per Marker</li>
<li><b>Chamfer</b> an Rahmen und Tags, die Markenform</li>
</ul>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:6px">Was von Beitrag zu Beitrag wechselt</h3>
<ul>
<li><b>Hell und dunkel</b> im Wechsel, damit das Raster atmet</li>
<li><b>Screens oder Visualisierung</b>, je nach Thema</li>
<li><b>Die Anzahl der Slides</b>, fünf bis sieben</li>
</ul>
<p class="micro" style="margin-top:7px">Im Profil-Raster wird auf 3:4 beschnitten,
nicht auf 4:5. Was mittig sitzt, überlebt.</p>
</div>
</div>
<h2 style="margin-bottom:8px">Statt behaupten, zeigen</h2>
<table style="margin-bottom:16px">
<thead><tr>
<th style="width:210px">Statt</th>
<th>Zeigen wir</th>
</tr></thead>
<tbody>
<tr><td>"Alles auf einen Blick"</td>
<td>Das Board mit echten Aufträgen, Summen und Terminen.</td></tr>
<tr><td>"Spart Zeit"</td>
<td>Den Ablauf: Anfrage rein, Aufgabe raus, Bestätigung an den Kunden.</td></tr>
<tr><td>"Einfach zu bedienen"</td>
<td>Das Formular, das per Drag-and-drop entsteht.</td></tr>
<tr><td>"Aus dem Handwerk"</td>
<td>Den Betrieb, die Baustelle, die Menschen dahinter.</td></tr>
<tr><td>"Verbindet deine Tools"</td>
<td>Die Visualisierung: ERP in der Mitte, das Neue drumherum.</td></tr>
</tbody>
</table>
<h2 style="margin-bottom:8px">Carousel jetzt, Reels später</h2>
<div class="card--line chamfer">
<p class="small" style="margin-bottom:6px">Für Konten unter 50.000 Followern bringen
Reels mehr Reichweite als Carousels. Das ist die Datenlage. Für uns ist sie zweitrangig,
weil Reichweite in dieser Phase nicht das Ziel ist.</p>
<p class="small">Carousels passen besser zu dem, was der Kanal leisten soll: Sie
erklären, sie lassen sich im Generator sauber und wiederholbar bauen, und sie werden
gespeichert. Das erste Reel steht am {REEL} und ist eine Bildschirmaufnahme, dafür
braucht es keine Kamera. Danach kommt Material aus dem Betrieb dazu. Aus drei Beiträgen
werden dann zwei Carousels und ein Reel, nicht vier Beiträge.</p>
</div>
</div>
<div class="foot"><span>HiveGrid &middot; Social Media</span><span>Seite 3 von 4</span></div>
</section>
<!-- SEITE 4: INTERAKTION -->
<section class="page">
<div class="top">
<img src="data:image/svg+xml;base64,{LOGO}" style="height:17px" alt="HiveGrid">
<div class="eyebrow">Tägliche Arbeit &amp; Auswertung</div>
</div>
<div class="body">
<h1 style="margin-bottom:9px">Tägliche Interaktion</h1>
<p class="lead" style="margin-bottom:14px;max-width:660px">
Instagram sortiert ein Konto auch danach ein, womit es sich selbst beschäftigt.
Rund zehn Minuten am Tag, aufgeteilt auf die drei Posting-Tage.
</p>
<div class="grid2" style="margin-bottom:16px">
<div class="card chamfer">
<h3 style="margin-bottom:6px">Die Routine pro Beitrag</h3>
<ul>
<li><b>5 Beiträge liken</b> aus dem eigenen Umfeld</li>
<li><b>3 echte Kommentare</b> schreiben, keine Emojis</li>
<li><b>1 Konto folgen</b>, das wirklich passt</li>
</ul>
<p class="micro" style="margin-top:7px">Zehn gute Kommentare bei den richtigen Konten
wirken mehr als fünfzig verstreute. In den Zahlen sichtbar wird das nach vier bis
acht Wochen.</p>
</div>
<div class="card chamfer">
<h3 style="margin-bottom:6px">Wem wir folgen</h3>
<ul>
<li><b>SHK-, Elektro- und Dachbetriebe</b>, die selbst posten</li>
<li><b>Innungen, Kreishandwerkerschaften, Kammern</b></li>
<li><b>Handwerksmessen und Fachmedien</b></li>
<li><b>Hersteller</b>, deren Produkte im Betrieb verbaut werden</li>
<li><b>Andere Handwerkssoftware</b>, deren Kommentarspalten voll sind</li>
</ul>
</div>
</div>
<div class="card--line chamfer" style="margin-bottom:16px">
<h3 style="margin-bottom:6px">Wie ein Kommentar aussieht</h3>
<p class="small" style="margin-bottom:6px">Am meisten bringt ein Kommentar in den ersten
Minuten nach einem Beitrag. Deshalb folgen wir gezielt Konten, die regelmäßig posten,
und schauen einmal täglich zur Mittagszeit rein.</p>
<p class="small">Ein Kommentar ist gut, wenn er ohne unseren Namen funktioniert. Also
eine Erfahrung, eine Nachfrage, ein Widerspruch. Nicht "Toller Beitrag", und nichts,
was auf uns verweist.</p>
</div>
<h2 style="margin-bottom:8px">Was wir nach vier Wochen ansehen</h2>
<table style="margin-bottom:14px">
<thead><tr>
<th style="width:150px">Kennzahl</th>
<th style="width:150px">Wo sie steht</th>
<th>Was sie uns sagt</th>
</tr></thead>
<tbody>
<tr class="hi"><td><b>Profilaufrufe je Beitrag</b></td><td>Insights, Interaktionen</td>
<td>Ob ein Beitrag jemanden dazu bringt, sich das Profil anzusehen. Das ist unser Zweck.</td></tr>
<tr><td><b>Sendungen</b></td><td>Beitragsinsights</td>
<td>Welches Thema jemand weiterschickt. Wiegt beim Ausspielen mehr als ein Like.</td></tr>
<tr><td><b>Speicherungen</b></td><td>Beitragsinsights</td>
<td>Ob ein Carousel als Nachschlagewerk taugt.</td></tr>
<tr><td><b>Konten erreicht,<br>davon keine Follower</b></td><td>Insights, Reichweite</td>
<td>Ob wir aus der eigenen Blase herauskommen.</td></tr>
<tr><td><b>Aktivste Zeiten</b></td><td>Insights, Zielgruppe</td>
<td>Ob 12:00 stimmt. Erst ab etwa 100 Followern belastbar.</td></tr>
<tr><td><b>Anmeldungen Warteliste</b></td><td>Backend</td>
<td>Ob aus dem Eindruck ein Kontakt wird.</td></tr>
</tbody>
</table>
<div class="card chamfer">
<h3 style="margin-bottom:5px">Nach vier Wochen</h3>
<p class="small">Bleibt der Montag oder wird es Dienstag? Bleibt 12:00? Und wie viele
Reels ersetzen ab dann Carousels? Bis dahin lassen wir den Rhythmus laufen, sonst
lässt sich nichts vergleichen.</p>
</div>
<p class="micro" style="margin-top:12px">Quellen zu den Ausspielungssignalen und
Posting-Zeiten: Sprout Social, Buffer, Later und Hootsuite (Instagram-Algorithmus 2026),
adconsulting.de und socialmediaakademie.de (Posting-Zeiten B2B und Handwerk, Deutschland).
Zahlen zu Sendungen und Reichweite stammen aus Auswertungen Dritter, nicht von Meta selbst,
und sind Größenordnungen, keine Garantien.</p>
</div>
<div class="foot"><span>HiveGrid &middot; Social Media</span><span>Seite 4 von 4</span></div>
</section>
</body>
</html>
"""
ZIEL_HTML = HIER / "Instagram-Redaktionsplan.html"
ZIEL_PDF = HIER / "Instagram-Redaktionsplan.pdf"
ZIEL_HTML.write_text(HTML, encoding="utf-8")
subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--no-pdf-header-footer",
f"--print-to-pdf={ZIEL_PDF}", ZIEL_HTML.as_uri()],
check=True, capture_output=True,
)
print(f"{ZIEL_PDF.name} {ZIEL_PDF.stat().st_size / 1024:.0f} KB")
+286 -2
View File
@@ -1,15 +1,76 @@
# Post-Generator — Regeln & Konventionen
Arbeitsgrundlage für alles, was in diesem Repo an Social-Grafiken entsteht.
**Stand:** 31.07.2026
**Stand:** 20.10.2026
Der Generator läuft lokal: `./serve.sh` → <http://127.0.0.1:4321/post1.html>
(Post 1, Carousel).
---
## 0. PFLICHT: Oberflächen aus den Produkt-Komponenten, nie eigene Grafik
> **Diese Regel gilt für jeden Post und hat Vorrang vor allem anderen in dieser Datei.**
>
> Jede Zeichnung, die die Software zeigt, wird aus den echten Komponenten von
> `hivegrid-cloud` nachgebaut: mit deren Maßen, Farben, Schrift (Instrument Sans),
> Icons (Feather/Lucide) und Beschriftungen aus `translations.json`. Jede Zeichnung
> bildet einen Screen nach, den es im Produkt wirklich gibt.
>
> **Verboten:** ausgedachte Oberflächen, eigene Marken-Pillen, Häkchen-Chips,
> Reiter, Kacheln oder Effekte, die es im Produkt nicht gibt. Kurz: keine
> „vibecoded“ Grafiken.
>
> - Nachschlagewerk mit allen Tokens, Komponenten, Bausteinen und dem Vorgehen:
> **`komponenten.md`**. Vor jedem neuen Post lesen.
> - Referenz-Umsetzung: `post8.html` (seit 20.10.2026). Die Bausteine `pText`,
> `pBadge`, `pKnopf`, `pFeld`, `pTabs`, `pTabelle`, `pRahmen` und so weiter von
> dort übernehmen.
> - Code immer von `origin/main` lesen (`git show origin/main:<pfad>`), nicht aus
> der veralteten Arbeitskopie.
> - Was nicht auf die Slide passt, weglassen und im Kommentar vermerken. Nicht
> umbauen, nicht erfinden. Erfunden werden darf nur Demo-Inhalt.
>
> **Warum:** Post 8 sah mit eigenem Design „total vibecoded“ aus. Nachgebaut aus
> den Komponenten „sieht jetzt genau so aus wie die Software“ (Dejan, 20.10.2026).
---
## 1. Wo die Wahrheit steht
> **Neue Basis ab 12.10.2026: `hivegrid-cloud`.** Das ist die aktuelle Version
> der Software, mit den neuen Komponenten. Neue Posts bauen auf ihrem Look auf,
> nicht mehr auf der Website. Bei Zweifel dort nachschauen:
>
> | Was | Quelle in `hivegrid-cloud/` |
> |---|---|
> | Farben, Schriftgrößen, Radien, Schatten | `apps/web/app/ui/theme.css` |
> | Komponenten (109, aus Subframe) | `apps/web/app/ui/components/` |
> | Subframe-Projekt | `f023064a5ca0`, siehe `.subframe/sync.json` |
> | Schrift-Dateien | `apps/web/public/fonts/instrument-sans-*.woff2` |
> | Module und Screens | `apps/web/app/modules/` |
> | Logo | `apps/web/app/ui/components/Logo/` |
>
> Die wichtigsten Werte aus `theme.css`:
>
> | Was | Wert |
> |---|---|
> | Schrift | Instrument Sans: in allen Oberflächen-Zeichnungen (§0), die Slide-Überschriften bleiben Manrope |
> | Text, Knöpfe | `#24242B` (grey-900), Knöpfe sind fast schwarz |
> | Text zweitrangig | `#505057`, drittrangig `#6D6D71` |
> | Markengelb | `#F1B729` (brand-600), unverändert |
> | Zweitfarbe | Sky `#0791F0` (brand-secondary-500) |
> | Flächen | `#FAFAFA` und `#F5F4F4` |
> | Rahmen | `#E5E5E5` (neutral-200) |
> | Radien | 4, 8, 12 px |
> | Größen | Hero 72/72, Seite 48/56, Untertitel 32/40, alle 600 |
> | Symbole | Feather über `@subframe/core`: in Oberflächen als Lucide-Formen (§0), Phosphor nur noch in älteren Posts |
>
> Entschieden am 20.10.2026: Oberflächen nehmen Rahmen `#E5E5E5` und
> Feather/Lucide aus dem Produkt (§0). Offen bleiben die Schatten
> (`--shadow-md`, `--shadow-lg`) gegen §5 „Keine Schatten“: In den Zeichnungen
> lassen wir sie weg, siehe `komponenten.md`.
Alle Marken-Werte stammen aus der Website, nicht aus diesem Repo. Bei Zweifel
**dort nachschauen, nicht hier raten**:
@@ -20,6 +81,7 @@ Alle Marken-Werte stammen aus der Website, nicht aus diesem Repo. Bei Zweifel
| Schrift | `HiveGrid-Website/app/layout.tsx` |
| Badge-Styling | `HiveGrid-Website/components/sections/ScreenshotAccordion.tsx` |
| Feature-Texte | `HiveGrid-Website/app/playground/PlaygroundClient.tsx` → `solutionModules` |
| Wabenfeld: Geometrie, Zellnummern, Belegung | `HiveGrid-Website/components/landing/HoneycombSection.tsx` |
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
@@ -27,6 +89,120 @@ werden** — das passiert nicht automatisch.
---
## 1a. Themes
**Projekt und Theme sind zwei verschiedene Dinge.** Das Projekt sagt, *welcher*
Beitrag; das Theme sagt, *wie er aussieht*. Beide Themes teilen Aufbau, Texte
und Kompositionen der Visualisierungen.
> **Der Grund ist über alle Beiträge hinweg derselbe: `--surface` `#FFFFFF`.**
> Kein Theme darf ihn ändern. Im Profilraster stehen die Beiträge nebeneinander,
> und schon ein leicht getönter Grund fällt dort als Fehler auf. Ein Theme, das
> mehr Kontrast für seine Karten braucht, holt ihn sich über die Kante
> (`--border-neutral` `#D5DBE1`), nicht über den Grund.
| Theme | Erkennungszeichen | Datei |
|---|---|---|
| `notebook` | Rasterhintergrund, Zeichenblock-Optik, Marker in der Überschrift, handgezogene Formen | `post2.html` |
| `modern` | Ruhiger Grund ohne Textur, sehr große Überschrift mit einem farbigen Wort, schwebende Karten mit Haarlinie, präzise Formen | `post2-alt.html`, `post3.html` |
Jede Datei trägt oben eine Konstante `THEME`. Sie steuert nichts, sie benennt
nur — die Unterschiede stecken in den CSS-Werten und in den Zeichen-Primitiven.
**Was beide Themes trennt, ist genau festgelegt:**
| | notebook | modern |
|---|---|---|
| Grund | Raster, 78 px | weiß, keine Textur |
| Über alle Slides | — | ein durchgehendes Band |
| Akzent in der Überschrift | Marker-Fläche | farbiges Wort |
| Formen | `strich` schwingt, Ecken schießen über | exakt, keine Abweichung |
| Flächen | Schraffur | Füllung |
| Zweitfarbe | keine | Teal `#0D6E6E` |
| Zeichen | selbst gezeichnet | Feather aus `@subframe/core` |
**Karten stehen weiß auf weiß und tragen über Kante und Schatten.** Kante
`#D5DBE1`, nicht `#E5E5E5` — die hellere Linie verschwindet auf Weiß. Den
Grund zu tönen wäre der einfachere Weg und ist verboten, siehe oben. Der
Schatten ist die Ausnahme von §5, die dieses Theme sich nimmt; dort steht,
warum und mit welchen Werten.
**Das Band ist ein einziges Bild über den ganzen Streifen.** Jede Slide zeigt
ihren Ausschnitt, verschoben über `--slide-i`. An der Naht läuft es deshalb
weiter, statt neu anzusetzen — das ist der Übergang, den ein Carousel sonst
nicht hat, weil jede Slide für sich steht. Drei Punkte, die daran hängen:
- **Die Wellenlänge darf kein Vielfaches von 1080 sein.** Sonst liegt an jeder
Naht derselbe Punkt der Welle, meist ein Scheitel, und ein waagerechtes Band
an der Kante sagt dem Auge nichts.
- **Flach und deckend, wie die Formen bei Slack.** Eine kräftige Fläche,
dahinter eine blasse zweite als Tiefe. Kein Verlauf, keine Wölbung, keine
Glanzkante. Ein Zwischenstand hatte hier einen metallisch schattierten
Zylinder aus vierzig Streifen — fotorealistisch und damit ein Fremdkörper
auf einem flachen, ruhigen Blatt. Er steht im Commit *„Theme modern:
Metallband, Icon-Satz und echte G-Marke"*, falls ihn jemand wiederhaben will.
- **Zwei Töne, umschaltbar** unter *Band* im Bedienfeld (`BAND_TOENE`):
| Ton | Fläche | Blasse zweite |
|---|---|---|
| `orange` | `--accent-yellow` `#F1B729` | `#FCE7B4` |
| `teal` | `--dark-bg` `#30313D` | `#D3D5DE` |
> **„HiveGrid Teal" ist `--dark-bg` `#30313D`, nicht `--accent-teal`.** Im
> Haus heißt der dunkle Ton des Story-Abschnitts auf der Website so. Er ist
> ein blaustichiges Anthrazit; `--accent-teal` `#0D6E6E` ist ein anderes
> Grün und trägt im Theme `modern` die Zeichenkacheln. Beim Reden über
> Farben lohnt die Nachfrage, welches gemeint ist.
- **Der Umschalter greift an `.bg`, nicht über `applyAll`.** Das Band setzt
`buildSlide`, nicht der Neuaufbau der Seiten; der Handler tauscht deshalb
`.bg .layer-band` direkt aus.
- **Die Bahn steht als Stützpunkte da, nicht als Formel** (`BAND_BAHN`, einer
je halbe Slide). Eine Sinuskurve war kürzer, ließ sich aber nicht steuern:
auf Slide 5 lief sie quer durch die Kacheln, und dort verschwinden die
orangenen Zeichen. Jeder Wert ist am gerenderten Bild geprüft, nicht
gerechnet. Wer `BAND_VON` oder `BAND_BIS` ändert, muss die Liste mitziehen:
die Punkte verteilen sich gleichmäßig über die neue Strecke, und alle
Werte verschieben sich.
- **Wo das Band läuft, steht in `BAND_VON` / `BAND_BIS`** — Anzahl Slides ohne
Band am Anfang und am Ende. Bei Post 2 sind das 2 und 1, das Band trägt also
Slide 3 bis 5. Slide 1 und die letzte sollen ruhig sein, Slide 2 trägt ihre
eigene orangene Leitung und würde sich damit beißen.
- **Als gefüllte Fläche gezeichnet, nicht als dicke Linie.** Eine Linie bekommt
am Ende eine Kappe rechtwinklig zur Laufrichtung; bei schräger Bahn steht die
seitlich über die Naht hinaus, und auf der Nachbarslide bleibt ein Splitter
stehen. Eine geschlossene Kontur endet exakt senkrecht — dann genügen zwei
Pixel Abstand zur Naht.
**Was über dem Band liegt, ist eine Karte mit Schatten.** Nicht randlos: der
Versuch mit reinen Konturen sah gestochen aus. Kleine Karten über einem Band
sind die Vorlage; eine randlose Karte über die ganze Fläche war der Fehler
davor, weil das Band dahinter komplett verschwand.
> **Eine Ausnahme bleibt: Slide 3.** Deren Karte *ist* die Visualisierung, ein
> Oberflächen-Panel mit Fortschrittsbalken und Statuszeilen. Die kann nicht
> randlos sein, also zeigt sich das Band dort nur in den Seitenrändern.
**Die Zeichen kommen aus dem Icon-Satz der Software, nicht aus der Hand.**
`@subframe/core`, dieselben Feather-Icons, mit denen die Oberfläche gebaut ist
(im Pitch-Paket unter `node_modules/@subframe/core/…/icons/final/`). Sie liegen
als `FEATHER`-Tabelle in der Datei, gezogen mit dem Skript aus dem
Gesprächsverlauf. Zwei Fallen beim Ziehen: die Props stehen teils als Zahl ohne
Anführungszeichen (`x: 3`), und bei Icons mit nur einem Kind muss der
`<svg>`-Rahmen weg, sonst rendert nichts.
Ausnahme ist Slide 6 in `post2-alt.html`: deren drei Zeichen sind Nachbauten
aus `WaitlistNext.tsx` und bleiben es.
**Teal ist die Zweitfarbe des Themes `modern`.** In `notebook` kommt sie nicht
vor. Damit hat der bisher ungenutzte Marken-Ton eine Aufgabe: er trägt
Zeichen und Kacheln, während Orange den einen Akzent setzt.
**Eine Änderung an einer Visualisierung betrifft beide Dateien.** Das ist der
Preis dafür, dass die Themes echte Alternativen sind und nicht Varianten
derselben Datei.
---
## 2. Farben
### Marke
@@ -36,7 +212,7 @@ werden** — das passiert nicht automatisch.
| `--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 |
| `--accent-teal` | `#0D6E6E` | Sekundär; im Theme `modern` die Zeichenkacheln |
| `--dark-bg` | `#30313D` | Dunkle Variante + Formen-Ebene |
### Flächen & Text
@@ -141,6 +317,38 @@ 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.
### Wabenfeld
Nicht zu verwechseln mit dem Wabenraster: das ist die Textur im Hintergrund,
das Wabenfeld ist die **Grafik von der Startseite** — die Marke als Nabe, die
fremden Programme als Waben darum, verbunden über Stränge. Sie steht in
`post3.html` als `visWabenfeld()` und ist aus `HoneycombSection.tsx`
übernommen, nicht nachempfunden:
| | Wert | Warum genau der |
|---|---|---|
| Umkreisradius | 62 | alles andere leitet sich davon ab |
| Flächenfaktor | 0,84 | lässt die Fuge zwischen zwei Waben offen |
| Achsrechnung | `x = 1,5·R·q`, `y = √3·R·(r + q/2)` | flat-top |
| Zellnummern | 1 Nabe, 2–7 innerer Ring, 8–19 Deko-Ring, dann das Feld | die Belegung hängt daran |
| Marken-Größe | sichtbar 56,5 px | Claude und OpenAI sind die Eichmarken |
**Die Zellnummern sind die Schnittstelle.** Welche Marke wo sitzt, steht auf
der Website in `HONEYCOMB_DEFAULT_LOGO_PLACEMENTS` und hier in `WABE_MARKEN`,
beide über dieselben Nummern. Wer den Ringgang in `wabeRing()` umstellt,
verschiebt damit stillschweigend jede Marke.
**Eine Marke wird über zwei Zahlen eingepasst**, nicht über eine: die
Grundfläche ihres Renderpfads (Feld 59,52 px, Deko 50,44 px) mal ihre eigene
Skalierung. Das Verhältnis 1,18 zwischen beiden Pfaden stammt aus der Website
und ist der Grund, warum dieselbe sichtbare Größe zwei verschiedene Faktoren
braucht. Für eine neue Marke gilt: `gr = Grundfläche / Anteil, den die Datei in
ihrer eigenen Fläche ausfüllt` — eine Datei mit Luft ringsum braucht mehr.
**Die vier Waben „In Arbeit" gehören nicht in einen Beitrag.** Auf der
Startseite sind sie ein Versprechen, im Feed wären sie ein Eingeständnis. Es
sind die Zellen 29, 41, 46 und 59, und sie bleiben gewöhnliche leere Waben.
---
## 4. Typografie
@@ -200,6 +408,56 @@ schon zweimal zu viel.
- **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**
- **Umlaute sind Umlaute.** Kein `Sanitaer`, kein `Aufmass`. Das gilt auch
dann, wenn die Quelle es so schreibt: eine ASCII-Ersatzschreibung im
Klickdummy ist ein Tippfehler und kein Produktbegriff, den man zitiert
#### Der Export prüft das
Aufgefallen ist es an „Sanitaer“ und „Klima / Lueftung“ auf einer fertigen
Slide — beim Bauen hatte es niemand gesehen, weil beides wörtlich aus der
Quelle kam. Seitdem prüft es die Maschine, an zwei Stellen:
| Weg | Wo |
|---|---|
| Bildexport | `renderSlide()` bricht ab, bevor ein PNG entsteht |
| Bewegtbild | `animation.mjs` prüft an 24 Stellen über den Ablauf |
Geprüft wird beides: fehlende Umlaute **und** Em-Dashes. Die Em-Dash-Regel
stand von Anfang an hier und wurde in Post 8 trotzdem gebrochen — eine Regel,
die nur im Dokument steht, hält nur so lange, wie jemand daran denkt.
**Bei den Umlauten genügt ein reines Muster nicht**, aber fast. Ein
ersatzweise geschriebenes `ae`/`oe`/`ue` steht nie hinter einem Vokal: „für“,
„über“, „grün“, „Sanitär“, „Lüftung“ folgen alle auf einen Konsonanten oder
stehen am Wortanfang. Hinter einem Vokal sind es dagegen immer zwei eigene
Laute — bauen, Bauer, neue, Dauer, Steuer, Mauer, genauer. Das Muster prüft
deshalb `(^|[^aeiou])(ae|oe|ue)`, und die Ausnahmeliste `UMLAUT_ERLAUBT` ist
auf die echten Sonderfälle geschrumpft: „Michael“ (ae hinter h), „aktuell“
(ue hinter t). Vorher standen dort sechs Wörter nach demselben Muster, und
die Liste wäre mit jedem Beitrag weitergewachsen.
Bei `ss` ist es umgekehrt: fast immer richtig (dass, Schluss, Netzanschluss),
nur wenige Wörter brauchen ein scharfes s. Die stehen einzeln in
`BRAUCHT_SCHARFES_S`.
**Mailadressen und Domains sind ausgenommen.** In `buero@muster-shk.de` kann
gar kein ü stehen; die umlautfreie Schreibung ist dort keine Schludrigkeit,
sondern Pflicht.
**Ein Zeilenumbruch ist eine Wortgrenze.** Die Prüfung liest nicht
`textContent`, sondern die einzelnen Textknoten mit Leerzeichen dazwischen.
Sonst klebt eine Überschrift wie „Formulare selbst bauen.`<br>`Per
Drag-and-drop.“ zu `bauen.Per` zusammen — ein Wort, das es nicht gibt, das
aber `ue` enthält und die Prüfung auslöst.
Schlägt die Prüfung bei einem Wort an, das so richtig ist, gehört es in die
Ausnahmeliste — nicht die Prüfung abgeschaltet.
> **Beim Bewegtbild reicht ein Blick auf das erste Bild nicht.** Der erste
> Anlauf prüfte nur den Anfangszustand und fand nichts: die fehlerhaften
> Wörter erscheinen dort erst ab `t = 0,4`. Der Text einer Animation ändert
> sich mit der Zeit, die Prüfung muss es auch.
---
@@ -224,6 +482,16 @@ behalten die Originalauflösung, alles außerhalb wird beschnitten.
im Grid fehlen links und rechts je ~33 px. Für die drei angepinnten Cover
relevant, im Feed nicht
**1080 × 1080 px (1:1) für Anzeigen mit Video im Carousel.** Ein normaler
Beitrag darf ein Video im 4:5-Carousel haben. Als Anzeige nicht: Meta schreibt
im Ads Guide für den Instagram-Feed „Derzeit wird bei Carousels, die Videos
enthalten, nur das Format 1:1 unterstützt.“ Reine Bild-Carousels dürfen 4:5
bleiben. In Reels laufen nur Bild-Carousels (9:16). Abgerufen 02.11.2026.
Post 9 hat dafür den Schalter „Format“ (4:5 · Beitrag, 1:1 · Anzeige), die
Karten tragen ihre 1:1-Maße unter `quadrat`, und
`node animation.mjs post9.html animWebinar 1x1` baut das Video in 1:1
(Datei mit `-1x1` im Namen).
### Layout-Template: Tag, Überschrift, Beschreibung, Bild
Jede Feature-Slide besteht aus denselben vier Bausteinen in derselben Reihenfolge.
@@ -314,6 +582,21 @@ aussehen lässt. Flächig gesetzt wirkt es ruhiger und passt zur Website, die
denselben Weg geht. Nebenbei ist es beim Export billiger — jeder unscharfe
Filter muss beim 3-fachen Rendern dreifach berechnet werden.
> **Für das Theme `modern` ist diese Regel aufgehoben.** Dort schweben Karten
> über einem durchlaufenden Band, und ohne Schatten stehen sie gestochen auf
> dem Grund statt darüber. Der Schatten ist ein SVG-`feDropShadow`
> (`dy 7`, `stdDeviation 10`, `#101828` bei 14 %), kein `box-shadow`: die
> Karten sind gezeichnet und nicht im DOM, und ein CSS-Schatten würde vom
> `clip-path` ohnehin mit beschnitten. Der Kostenpunkt aus dem Absatz oben
> bleibt — **nach jeder Änderung daran einmal 3-fach exportieren und
> hinsehen.** Im Theme `notebook` gilt die Regel unverändert.
>
> **`.vis svg` braucht dafür `overflow: visible`.** Ein SVG beschneidet an
> seiner viewBox, und die Karten füllen ihre Zeichnung randlos aus. Ohne die
> Regel wird der Schatten genau an der Kartenkante gekappt: außen ein grauer
> Streifen mit hartem Schnitt, in den Chamfer-Kerben graue Keile. Am Layout
> ändert sich nichts, gezeichnet wird weiterhin nur, was in der viewBox steht.
### Was global gilt und was pro Slide
| Einstellung | Geltung | Warum |
@@ -711,6 +994,7 @@ etwas ins Statusfeld schreibt. **Nicht entfernen.**
| 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) |
| „Sanitaer“ auf dem fertigen Beitrag | Wörtlich aus der Quelle übernommen, die es selbst falsch schreibt. §4 |
---
+164 -10
View File
@@ -23,8 +23,12 @@ import http.server
import json
import os
import re
import shutil
import socketserver
import subprocess
import threading
import urllib.parse
import uuid
from pathlib import Path
PORT = 4321
@@ -40,6 +44,15 @@ MAX_BILD = 40 * 1024 * 1024
# angefasst, deshalb bleibt ein PNG genau das PNG, das der Finder geliefert hat.
BILD_ENDUNGEN = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"}
BILD_ORDNER = ("assets/screens", "assets/eigene")
ANIM_DIR = ROOT / "animation"
SAFE_KEY = re.compile(r"^[A-Za-z][A-Za-z0-9-]{0,39}$")
ANIM_FEHLER = re.compile(r"^[A-Za-z]*Error: ")
# Laufende und abgeschlossene Videobauten, nach Kennung. Der Bau dauert je
# nach Laenge ein bis drei Minuten -- zu lang fuer eine offene Anfrage, und
# animation.mjs holt sich waehrenddessen selbst Seiten von diesem Server.
# Deshalb laeuft er in einem eigenen Faden und die Seite fragt den Stand ab.
ANIM_LAEUFE = {}
ANIM_SPERRE = threading.Lock()
class Handler(http.server.SimpleHTTPRequestHandler):
@@ -104,11 +117,61 @@ class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
"""Vor der Dateiauslieferung nur die Bilderliste abfangen."""
if urllib.parse.urlparse(self.path).path == "/bilder":
zerlegt = urllib.parse.urlparse(self.path)
if zerlegt.path == "/":
# Die Wurzel fuehrt zur Uebersicht aller Projekte statt zur
# Dateiliste. So reicht ein Link auf http://127.0.0.1:4321/,
# zum Beispiel aus dem Marketing-Dashboard.
self.send_response(302)
self.send_header("Location", "/uebersicht.html")
self.end_headers()
return
if zerlegt.path == "/bilder":
self._liste_bilder()
return
if zerlegt.path == "/anim":
self._anim_stand(urllib.parse.parse_qs(zerlegt.query).get("id", [""])[0])
return
super().do_GET()
def _anim_stand(self, kennung):
"""Stand eines Videobaus. Unbekannte Kennung ist kein Serverfehler,
sondern eine Antwort: der Lauf ist weg, mehr steht dazu nicht an."""
with ANIM_SPERRE:
lauf = ANIM_LAEUFE.get(kennung)
if lauf is None:
self._json(404, json.dumps({"laeuft": False, "fehler": "Lauf unbekannt"}))
return
self._json(200, json.dumps(lauf, ensure_ascii=False))
def _anim_bauen(self):
"""Ein einzelnes MP4 bauen lassen. Getrennt vom Bildexport: die
Animation ist ein eigenes Element im Carousel und soll auch einzeln
herauskommen."""
length = int(self.headers.get("Content-Length", 0))
if length <= 0 or length > 4096:
self._json(400, '{"error":"ungueltige Anfrage"}')
return
try:
wunsch = json.loads(self.rfile.read(length))
projekt, key = wunsch["projekt"], wunsch["key"]
except Exception:
self._json(400, '{"error":"kein gueltiges JSON"}')
return
if not SAFE_PROJECT.match(str(projekt)) or not SAFE_KEY.match(str(key)):
self._json(400, '{"error":"unzulaessiger Name"}')
return
if not (ROOT / (projekt + ".html")).is_file():
self._json(404, '{"error":"Projekt nicht gefunden"}')
return
kennung = uuid.uuid4().hex[:12]
with ANIM_SPERRE:
ANIM_LAEUFE[kennung] = {"laeuft": True, "zeile": "startet …"}
threading.Thread(target=_anim_lauf, args=(kennung, projekt, key),
daemon=True).start()
self._json(200, json.dumps({"id": kennung}))
def _liste_bilder(self):
"""Alle verwendbaren Bilder, damit die Auswahl im Generator nicht im
Code gepflegt werden muss und importierte sofort auftauchen."""
@@ -171,6 +234,9 @@ class Handler(http.server.SimpleHTTPRequestHandler):
if path == "/upload":
self._upload()
return
if path == "/anim":
self._anim_bauen()
return
if path != "/save":
self.send_error(404)
return
@@ -209,18 +275,99 @@ class Handler(http.server.SimpleHTTPRequestHandler):
self.wfile.write(payload)
def end_headers(self):
# Werkzeug, kein Auslieferungsserver: Assets werden hier laufend
# ausgetauscht, und ein gecachter Screenshot unter gleichem Dateinamen
# kostet mehr Zeit als jeder erneute Ladevorgang.
self.send_header("Cache-Control", "no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
# Werkzeug, kein Auslieferungsserver: ein Asset, das unter demselben
# Namen ausgetauscht wird, muss sofort ankommen.
#
# Dafuer stand hier no-store auf allem. Das Ziel war richtig, der
# Preis zu hoch: no-store verbietet dem Browser die Kopie ueberhaupt,
# und damit lud jeder Reload die vollen rund 6 MB neu -- 3 MB
# Screenshots, 3,3 MB Mockups, die Schriften, die Bibliotheken. Am
# Ende wartete man beim Neuladen auf Dateien, die sich seit Wochen
# nicht geaendert hatten.
#
# no-cache leistet dasselbe billiger: der Browser darf die Kopie
# behalten, muss aber vor jeder Benutzung nachfragen. Eine
# ausgetauschte Datei kommt damit genauso sofort an, eine
# unveraenderte kostet ein 304 ohne Rumpf. Geprueft: der Handler
# beantwortet If-Modified-Since bereits mit 304 und 0 Bytes.
#
# Seite, Stand und Auskuenfte bleiben bei no-store. Die sind klein,
# und sie duerfen nie aus einer Kopie kommen.
pfad = urllib.parse.urlparse(self.path).path
if pfad.startswith(("/assets/", "/vendor/", "/animation/")):
self.send_header("Cache-Control", "no-cache")
else:
self.send_header("Cache-Control", "no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
super().end_headers()
def log_message(self, fmt, *args):
first = args[0] if args else ""
# args[0] ist normalerweise die Anfragezeile, bei send_error aber ein
# HTTPStatus. Ohne str() flog hier ein TypeError, der den Handler-Thread
# abbrach: der Browser wartete dann ewig auf eine Antwort, die nie kam,
# und die Seite blieb bei "Lade Screens ..." stehen.
first = str(args[0]) if args else ""
if "POST /save" in first or "DELETE" in first:
super().log_message(fmt, *args)
elif not first.startswith(("GET", "POST", "HEAD")):
# Fehler und Warnungen gehoeren gesehen, sonst sucht man lange.
super().log_message(fmt, *args)
def _anim_lauf(kennung, projekt, key):
"""animation.mjs in einem eigenen Faden. Seine Ausgabe wandert zeilenweise
in den Stand, damit die Seite den Fortschritt zeigen kann statt zwei
Minuten lang nichts."""
def melden(**neu):
with ANIM_SPERRE:
ANIM_LAEUFE.setdefault(kennung, {}).update(neu)
try:
proc = subprocess.Popen(
["node", "animation.mjs", projekt + ".html", key],
cwd=str(ROOT), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1)
ausgabe = []
for zeile in proc.stdout:
zeile = zeile.strip()
if zeile:
ausgabe.append(zeile)
melden(zeile=zeile)
code = proc.wait()
if code != 0:
# Die letzte Zeile ist bei node die Versionsangabe und sagt nichts,
# und der Auszug aus dem Quelltext steht vor der eigentlichen
# Meldung. Gesucht ist die Zeile, die mit dem Fehlernamen anfaengt.
grund = next((z for z in ausgabe if ANIM_FEHLER.match(z)), "")
melden(laeuft=False,
fehler=grund or (ausgabe[-1] if ausgabe else "node endete mit " + str(code)))
return
ziel = ANIM_DIR / (projekt + "-" + key + ".mp4")
if not ziel.is_file():
melden(laeuft=False, fehler="kein MP4 entstanden")
return
groesse = ziel.stat().st_size
# Zum Hochladen liegt die Datei am selben Ort wie die Bilder. Im Repo
# bleibt sie trotzdem: dort ist sie versioniert und hat auf der anderen
# Maschine nach einem Pull denselben Stand. Ueberschrieben wird in
# Downloads nichts, genau wie beim Bildexport -- eine Datei, die
# vielleicht schon hochgeladen wurde, gehoert nicht ersetzt.
TARGET.mkdir(parents=True, exist_ok=True)
abgabe = TARGET / ziel.name
n = 1
while abgabe.exists():
abgabe = TARGET / f"{ziel.stem}-{n}{ziel.suffix}"
n += 1
shutil.copy2(ziel, abgabe)
melden(laeuft=False, pfad="~/Downloads/" + abgabe.name,
groesse=f"{groesse/1024/1024:.1f} MB · auch animation/{ziel.name}",
zeile="fertig")
print(f"gespeichert: {abgabe} ({groesse/1024/1024:.1f} MB)")
except FileNotFoundError:
melden(laeuft=False, fehler="node nicht gefunden")
except Exception as e: # der Faden darf den Server nicht mitnehmen
melden(laeuft=False, fehler=str(e))
class Server(socketserver.ThreadingTCPServer):
@@ -228,7 +375,14 @@ class Server(socketserver.ThreadingTCPServer):
if __name__ == "__main__":
print(f"Generator: http://127.0.0.1:{PORT}/post1.html")
# Der Port ist fest, damit Links von aussen (Marketing-Dashboard,
# Lesezeichen) immer stimmen. Ist er belegt, laeuft PostGrid meist schon.
try:
httpd = Server(("127.0.0.1", PORT), Handler)
except OSError:
print(f"Port {PORT} ist belegt. Laeuft PostGrid schon? Dann einfach http://127.0.0.1:{PORT}/ oeffnen.")
raise SystemExit(1)
print(f"PostGrid: http://127.0.0.1:{PORT}/")
print(f"Speichert nach: {TARGET}")
with Server(("127.0.0.1", PORT), Handler) as httpd:
with httpd:
httpd.serve_forever()
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
# Serve the generator on a free port, with the /save endpoint.
# Startet PostGrid immer auf dem festen Port 4321: http://127.0.0.1:4321/
cd "$(dirname "$0")" && exec python3 serve.py
+158
View File
@@ -0,0 +1,158 @@
{
"bandTon": "teal",
"bgState": [
{
"bg": "raster",
"int": "kraeftig"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 4,
"deck": [
{
"ref": "titel",
"werte": {
"body": "<br>"
}
},
{
"ref": "prozesse",
"werte": {}
},
{
"ref": "wechseln",
"werte": {}
},
{
"ref": "mehr",
"werte": {}
},
{
"ref": "verbinden",
"werte": {}
},
{
"ref": "warteliste",
"werte": {
"body": "Setz dich auf die Warteliste, dann melden wir uns."
}
}
],
"edits": [
{
"body": {
"dx": 102,
"dy": 0
},
"headline": {
"dx": 37,
"dy": 3,
"size": 88
},
"vis": {
"dx": 3,
"dy": -9
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 113,
"dy": -59
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": -3,
"dy": -88
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {
"dx": -68,
"dy": -25,
"size": 1028
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 33,
"dy": -138
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 12,
"dy": 74,
"size": 1184
}
}
],
"frameStyle": "aus",
"layoutState": [
"standard",
"standard",
"standard",
"standard",
"standard",
"standard"
],
"superSample": 3,
"tagOut": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+160
View File
@@ -0,0 +1,160 @@
{
"bandTon": "teal",
"bgState": [
{
"bg": "raster",
"int": "kraeftig"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "titel",
"werte": {
"body": "<br>"
}
},
{
"ref": "prozesse",
"werte": {}
},
{
"ref": "wechseln",
"werte": {}
},
{
"ref": "mehr",
"werte": {}
},
{
"ref": "verbinden",
"werte": {}
},
{
"ref": "warteliste",
"werte": {
"body": "Setz dich auf die Warteliste, dann melden wir uns."
}
}
],
"edits": [
{
"body": {
"dx": 102,
"dy": 0
},
"headline": {
"dx": 15,
"dy": 3,
"size": 88
},
"vis": {
"dx": 3,
"dy": -9
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 113,
"dy": -59
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": -3,
"dy": -88
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {
"dx": -68,
"dy": -25,
"size": 1028
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 33,
"dy": -138
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 102,
"dy": 70,
"size": 1184
}
}
],
"frameStyle": "aus",
"layoutState": [
"standard",
"standard",
"standard",
"standard",
"standard",
"standard"
],
"rasterAn": "aus",
"superSample": 3,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "einzeln"
}
+99
View File
@@ -0,0 +1,99 @@
{
"bgState": [
{
"bg": "raster",
"int": "kraeftig"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "titel",
"werte": {
"body": "<br>"
}
},
{
"ref": "prozesse",
"werte": {}
},
{
"ref": "wechseln",
"werte": {}
},
{
"ref": "mehr",
"werte": {}
},
{
"ref": "verbinden",
"werte": {}
},
{
"ref": "warteliste",
"werte": {}
}
],
"edits": [
{
"body": {
"dx": 102,
"dy": -95
},
"headline": {
"dx": 102,
"dy": -96
},
"vis": {
"dx": -38,
"dy": -79,
"size": 692
}
},
{
"vis": {
"dx": 0,
"dy": 0
}
},
{},
{},
{},
{}
],
"frameStyle": "rund",
"layoutState": [
"ecke",
"standard",
"standard",
"standard",
"standard",
"standard"
],
"superSample": 2,
"tagOut": "an",
"tilt": "an",
"v": 6,
"variant": "hell",
"view": "streifen"
}
+73 -17
View File
@@ -2,7 +2,7 @@
"bgState": [
{
"bg": "raster",
"int": "dezent"
"int": "kraeftig"
},
{
"bg": "raster",
@@ -25,11 +25,13 @@
"int": "dezent"
}
],
"current": 0,
"current": 5,
"deck": [
{
"ref": "titel",
"werte": {}
"werte": {
"body": "<br>"
}
},
{
"ref": "prozesse",
@@ -49,36 +51,90 @@
},
{
"ref": "warteliste",
"werte": {}
"werte": {
"body": "Setz dich auf die Warteliste, dann melden wir uns."
}
}
],
"edits": [
{
"body": {
"dx": 102,
"dy": 0
},
"headline": {
"dx": 37,
"dy": 3,
"size": 88
},
"vis": {
"dx": -100,
"dy": 250,
"size": 700
"dx": -38,
"dy": -79,
"size": 692
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": -7,
"dy": -19
"dx": 0,
"dy": 16
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 19,
"dy": -27
"dx": 0,
"dy": -109
}
},
{},
{},
{}
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 3,
"dy": -88
}
}
],
"frameStyle": "chamfer",
"frameStyle": "rund",
"layoutState": [
"standard",
"ecke",
"standard",
"standard",
"standard",
@@ -90,5 +146,5 @@
"tilt": "an",
"v": 6,
"variant": "hell",
"view": "streifen"
"view": "einzeln"
}
+59
View File
@@ -0,0 +1,59 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 1,
"deck": [
{
"ref": "verbinden",
"werte": {}
},
{
"ref": "aufbau",
"werte": {}
}
],
"edits": [
{
"headline": {
"dy": 3
},
"vis": {
"dx": -2,
"dy": -67
}
},
{
"body": {},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {
"dx": 2,
"dy": -93
}
}
],
"frameStyle": "chamfer",
"layoutState": [
"standard",
"standard"
],
"rasterAn": "aus",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "einzeln"
}
+78
View File
@@ -0,0 +1,78 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 2,
"deck": [
{
"ref": "anfrage",
"werte": {}
},
{
"ref": "ausloeser",
"werte": {}
},
{
"ref": "aktionen",
"werte": {}
},
{
"ref": "vorlagen",
"werte": {}
}
],
"edits": [
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dx": 0,
"dy": 3
}
},
{
"headline": {
"dy": 3
}
}
],
"frameStyle": "chamfer",
"layoutState": [
"standard",
"standard",
"standard",
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+91
View File
@@ -0,0 +1,91 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "board",
"werte": {}
},
{
"ref": "karte",
"werte": {}
},
{
"ref": "detail",
"werte": {}
},
{
"ref": "liste",
"werte": {}
},
{
"ref": "schablone",
"werte": {}
}
],
"edits": [
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
}
}
],
"frameStyle": "chamfer",
"layoutState": [
"standard",
"standard",
"standard",
"standard",
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+102
View File
@@ -0,0 +1,102 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "kraeftig"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "chat",
"werte": {}
},
{
"ref": "kanaele",
"werte": {}
},
{
"ref": "aufgabe",
"werte": {}
},
{
"ref": "menue",
"werte": {}
},
{
"ref": "mobil",
"werte": {}
}
],
"edits": [
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
},
"vis": {
"dx": -27,
"dy": -124,
"size": 950
}
},
{
"headline": {
"dy": 3
}
},
{
"body": {},
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": -8
}
},
{
"headline": {
"dy": 3
},
"vis": {}
}
],
"frameStyle": "chamfer",
"layoutState": [
"standard",
"standard",
"standard",
"standard",
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+79
View File
@@ -0,0 +1,79 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "felder",
"werte": {}
},
{
"ref": "bedingt",
"werte": {}
},
{
"ref": "lead",
"werte": {}
}
],
"edits": [
{
"body": {
"dx": 0,
"dy": 0
},
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": -28
}
},
{
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": -126
}
},
{
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": -162
}
}
],
"frameStyle": "rund",
"layoutState": [
"standard",
"standard",
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "aus",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+85
View File
@@ -0,0 +1,85 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
}
],
"current": 3,
"deck": [
{
"ref": "segment",
"werte": {}
},
{
"ref": "vorlagen",
"werte": {}
},
{
"ref": "pruefung",
"werte": {}
},
{
"ref": "mail",
"werte": {}
}
],
"edits": [
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": 0
}
},
{
"headline": {
"dy": 3
}
},
{
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": 0
}
}
],
"frameStyle": "rund",
"layoutState": [
"standard",
"standard",
"standard",
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "aus",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+32
View File
@@ -0,0 +1,32 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
}
],
"current": 0,
"deck": [
{
"ref": "anmeldung",
"werte": {}
}
],
"edits": [
{}
],
"format": "1x1",
"frameStyle": "chamfer",
"layoutState": [
"standard"
],
"rasterAn": "punkte",
"superSample": 2,
"tagOut": "an",
"textMitte": "an",
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
}
+255
View File
@@ -0,0 +1,255 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PostGrid · Alle Projekte</title>
<link rel="stylesheet" href="./assets/fonts/manrope.css">
<style>
/* ─── Projektübersicht ────────────────────────────────────────────────────
Alle Streifen untereinander auf einer Fläche. Die Seite baut die Slides
nicht selbst: jedes Projekt kommt als iframe im Modus ?nur=streifen, in
dem die Post-Seite ihre Bedienoberfläche ausblendet und nur den Streifen
in natürlicher Größe zeigt. Skaliert wird hier außen.
Warum nicht nachbauen: die Streifen müssen genau das zeigen, was der
Generator zeigt — bis auf den letzten gespeicherten Versatz. Jede zweite
Implementierung würde irgendwann abweichen, und dann wäre die Übersicht
schlimmer als keine. */
* { box-sizing: border-box; }
:root { color-scheme: dark; }
html, body { height: 100%; }
body {
margin: 0; font-family: Manrope, system-ui, sans-serif;
background: #101116; color: #C7CCD6; overflow: hidden;
}
.menubar {
position: relative; z-index: 10; display: flex; align-items: center; gap: 2px;
height: 34px; padding: 0 10px; background: #16181F;
border-bottom: 1px solid #23262F;
font-size: 13px; user-select: none;
}
.menubar .marke { font-weight: 800; color: #FFFFFF; margin-right: 14px; letter-spacing: -.01em; }
.menu { position: relative; }
.menu > button {
font: inherit; color: #C7CCD6; background: none; border: 0;
padding: 5px 10px; border-radius: 6px; cursor: pointer;
}
.menu > button:hover, .menu[data-auf] > button { background: #23262F; color: #FFFFFF; }
.menu .liste {
display: none; position: absolute; left: 0; top: 100%; min-width: 250px;
padding: 5px; background: #1B1E26; border: 1px solid #2E313B;
border-radius: 9px; flex-direction: column;
}
.menu[data-auf] .liste { display: flex; }
.menu .liste button {
font: inherit; text-align: left; color: #C7CCD6; background: none;
border: 0; padding: 7px 10px; border-radius: 6px; cursor: pointer;
}
.menu .liste button:hover { background: #262A34; color: #FFFFFF; }
.menu .liste hr { border: 0; border-top: 1px solid #2E313B; margin: 5px 2px; }
/* Derselbe gepunktete Grund wie im Generator, aber ohne dessen Canvas:
hier wird nur gescrollt. */
.stage { position: absolute; inset: 34px 0 0; overflow: hidden;
background-color: #101116;
background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px);
background-size: 26px 26px;
}
/* Nur hoch und runter. Die Streifen sind auf die Breite eingepasst,
also gibt es waagerecht nichts zu holen. */
.scroller { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden;
overscroll-behavior: contain; }
.flaeche {
width: max-content; min-width: 100%; height: max-content; min-height: 100%;
padding: 54px; display: flex; flex-direction: column; gap: 46px;
align-items: flex-start;
}
.reihe { display: flex; flex-direction: column; gap: 10px; }
.reihe h2 {
margin: 0; font-size: 12px; font-weight: 800; letter-spacing: .14em;
text-transform: uppercase; color: #6E7480;
}
.reihe h2 b { color: #C7CCD6; font-weight: 800; }
/* Der Rahmen bekommt die Größe des skalierten Streifens, das iframe darin
bleibt in Originalgröße und wird nur transformiert. Ohne den Rahmen
würde das ungeskalierte iframe die Fläche aufblähen. */
.rahmen { overflow: hidden; background: #FFFFFF; border: 1px solid #2E313B; }
/* pointer-events: none -- sonst zoomt das Mausrad im Streifen die
Post-Seite darin, die ja ihren eigenen Canvas mitbringt. Die Uebersicht
ist zum Ansehen da, bedient wird im Projekt selbst. */
.rahmen iframe {
display: block; border: 0; transform-origin: 0 0; background: #FFFFFF;
pointer-events: none;
}
.reihe.fehlt .rahmen { display: grid; place-items: center; width: 420px; height: 90px;
background: #16181F; color: #6E7480; font-size: 13px; }
.hud {
position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
display: flex; align-items: center; gap: 12px;
padding: 8px 12px; background: rgba(16,17,22,.92);
border-top: 1px solid #23262F;
}
.hud p { margin: 0; font-size: 12px; color: #6E7480; letter-spacing: .04em; }
</style>
</head>
<body>
<div class="menubar">
<span class="marke">PostGrid</span>
<div class="menu" data-menu="projekt">
<button type="button">Projekt</button>
<div class="liste">
<button type="button" data-gehe="./post1.html">Post 1 · Lösung</button>
<button type="button" data-gehe="./post2.html">Post 2 · Die Geschichte</button>
<button type="button" data-gehe="./post2-alt.html">Post 2 · Alternative</button>
<button type="button" data-gehe="./post3.html">Post 3 · Verbinden statt ersetzen</button>
<button type="button" data-gehe="./post4.html">Post 4 · Workflow-Engine</button>
<button type="button" data-gehe="./post5.html">Post 5 · Projektmanagement</button>
<button type="button" data-gehe="./post6.html">Post 6 · Team-Chat</button>
<button type="button" data-gehe="./post7.html">Post 7 · Form Builder</button>
<button type="button" data-gehe="./post8.html">Post 8 · E-Mail-Marketing</button>
<button type="button" data-gehe="./post9.html">Post 9 · Variante 1 · Webinar-Fenster</button>
<button type="button" data-gehe="./post9-v2.html">Post 9 · Variante 2 · Kundengeschichte</button>
<button type="button" data-gehe="./post9-v3.html">Post 9 · Variante 3 · Kundengeschichte dunkel</button>
<button type="button" data-gehe="./post9-v4.html">Post 9 · Variante 4 · Zahlen</button>
<button type="button" data-gehe="./post9-v5.html">Post 9 · Variante 5 · Zahlen dunkel</button>
<button type="button" data-gehe="./post9-v6.html">Post 9 · Variante 6 · Arbeitsheft</button>
<button type="button" data-gehe="./post9-v7.html">Post 9 · Variante 7 · Arbeitsheft dunkel</button>
<button type="button" data-gehe="./post9-varianten.html">Post 9 · Varianten im Vergleich</button>
<hr>
<button type="button" data-klick="neu">Streifen neu laden</button>
</div>
</div>
</div>
<main class="stage">
<div class="scroller" id="scroller">
<div class="flaeche" id="flaeche"></div>
</div>
<div class="hud">
<p id="meta">Lade Streifen ...</p>
</div>
</main>
<script>
(() => {
"use strict";
const PROJEKTE = [
{ datei: "./post1.html", name: "Post 1 · Lösung" },
{ datei: "./post2.html", name: "Post 2 · Die Geschichte", theme: "notebook" },
{ datei: "./post2-alt.html", name: "Post 2 · Alternative", theme: "modern" },
{ datei: "./post3.html", name: "Post 3 · Verbinden statt ersetzen", theme: "modern" },
{ datei: "./post4.html", name: "Post 4 · Workflow-Engine", theme: "modern" },
{ datei: "./post5.html", name: "Post 5 · Projektmanagement", theme: "modern" },
{ datei: "./post6.html", name: "Post 6 · Team-Chat", theme: "modern" },
{ datei: "./post7.html", name: "Post 7 · Form Builder", theme: "modern" },
{ datei: "./post8.html", name: "Post 8 · E-Mail-Marketing", theme: "modern" },
{ datei: "./post9.html", name: "Post 9 · Variante 1 · Webinar-Fenster", theme: "modern" },
{ datei: "./post9-v2.html", name: "Post 9 · Variante 2 · Kundengeschichte", theme: "modern" },
{ datei: "./post9-v3.html", name: "Post 9 · Variante 3 · Kundengeschichte dunkel", theme: "modern" },
{ datei: "./post9-v4.html", name: "Post 9 · Variante 4 · Zahlen", theme: "modern" },
{ datei: "./post9-v5.html", name: "Post 9 · Variante 5 · Zahlen dunkel", theme: "modern" },
{ datei: "./post9-v6.html", name: "Post 9 · Variante 6 · Arbeitsheft", theme: "modern" },
{ datei: "./post9-v7.html", name: "Post 9 · Variante 7 · Arbeitsheft dunkel", theme: "modern" },
];
const flaeche = document.getElementById("flaeche");
const scroller = document.getElementById("scroller");
const meta = document.getElementById("meta");
/* Kein Zoom. Ein Streifen ist bis zu 6480 px breit und wird immer auf die
Fensterbreite eingepasst; gescrollt wird nur hoch und runter. Ein
Zwischenstand hatte Rad-Zoom und Zoomknöpfe -- das war doppelt gemoppelt,
weil die Post-Seite im iframe ihren eigenen Canvas mitbringt, und die
Beschriftungen skalierten nicht mit, standen beim Herauszoomen also
riesig über winzigen Streifen. */
const reihen = [];
function bauen() {
flaeche.innerHTML = "";
reihen.length = 0;
PROJEKTE.forEach(pr => {
const reihe = document.createElement("div");
reihe.className = "reihe";
const h2 = document.createElement("h2");
h2.innerHTML = "<b>" + pr.name + "</b>" + (pr.theme ? " · Theme " + pr.theme : "");
const rahmen = document.createElement("div");
rahmen.className = "rahmen";
const ram = document.createElement("iframe");
ram.src = pr.datei + "?nur=streifen";
ram.loading = "eager";
rahmen.appendChild(ram);
reihe.appendChild(h2); reihe.appendChild(rahmen);
flaeche.appendChild(reihe);
reihen.push({ pr, reihe, rahmen, ram, w: 6480, h: 1350, da: false });
});
}
/* Jede Post-Seite meldet ihre Streifenbreite, sobald sie steht. Die Zahl
der Slides ist je Projekt anders und kann sich ändern, deshalb wird sie
nicht hier gepflegt. */
window.addEventListener("message", e => {
const d = e.data;
if (!d || d.hg !== "streifen") return;
const r = reihen.find(x => x.ram.contentWindow === e.source);
if (!r) return;
r.w = d.w; r.h = d.h; r.da = true;
legen();
});
// Eingepasst wird nur in die Breite, die Höhe ergibt sich und wird gescrollt.
const POLSTER = 54 * 2;
function passend() {
const breit = Math.max(...reihen.map(r => r.w), 1080);
return Math.min((scroller.clientWidth - POLSTER) / breit, 0.5);
}
function legen() {
const k = passend();
reihen.forEach(r => {
r.ram.style.width = r.w + "px";
r.ram.style.height = r.h + "px";
r.ram.style.transform = "scale(" + k + ")";
r.rahmen.style.width = Math.round(r.w * k) + "px";
r.rahmen.style.height = Math.round(r.h * k) + "px";
});
const offen = reihen.filter(r => r.da).length;
meta.textContent = offen < reihen.length
? "Lade Streifen ... " + offen + " von " + reihen.length
// Seit Post 3 gibt es Einzelbilder: "1 Slides" stand vorher da.
: reihen.length + " Projekte · " + reihen.map(r => {
const n = Math.round(r.w / 1080);
return n + (n === 1 ? " Slide" : " Slides");
}).join(" · ");
}
// Kein Ziehen, kein Rad-Zoom: die Seite scrollt wie eine gewoehnliche Seite.
window.addEventListener("resize", legen);
// ─── Menü ────────────────────────────────────────────────────────────────
document.querySelectorAll(".menu").forEach(m => {
m.querySelector("button").addEventListener("click", e => {
e.stopPropagation();
const auf = m.hasAttribute("data-auf");
document.querySelectorAll(".menu").forEach(x => x.removeAttribute("data-auf"));
if (!auf) m.setAttribute("data-auf", "");
});
});
document.addEventListener("click", e => {
const b = e.target.closest("button[data-gehe], button[data-klick]");
document.querySelectorAll(".menu").forEach(x => x.removeAttribute("data-auf"));
if (!b) return;
if (b.dataset.gehe) { location.href = b.dataset.gehe; return; }
if (b.dataset.klick === "neu") { bauen(); legen(); }
});
bauen();
legen();
})();
</script>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
# Video-Komponenten
Einblendungen für den Videoschnitt, in derselben Bildsprache wie die Posts:
weiße Karten mit Haarlinie und weichem Schatten, ein gelber Akzent, Manrope.
Gedacht für Noahs Beitrag über den Betrieb, taugt aber für jeden Schnitt.
Jede Komponente liegt zweimal vor — als **Standbild** und als **Bildsequenz**
mit fertiger Ein- und Ausblendung. Alles mit Alphakanal, alles freigestellt:
kein Rand, keine Leinwand, keine Farbe hinter der Karte.
```
video/
├── komponenten.html die Vorlage, hier steht die Gestaltung
├── bauen.mjs rendert daraus die Dateien
├── liste.json was zuletzt gebaut wurde, mit Maßen
├── png/ Standbilder
└── sequenz/ je Komponente ein Ordner "-ein" und einer "-aus"
```
## In Premiere holen
**Standbild:** `png/<name>.png` in die Timeline ziehen. Ein- und Ausblendung
machst du selbst über Position und Deckkraft. Das ist der flexiblere Weg.
**Sequenz:** *Datei → Importieren*, im Ordner `sequenz/<name>-ein/` das **erste**
Bild anklicken und unten **„Bildsequenz"** ankreuzen. Premiere legt daraus
einen einzigen Clip an, nicht 20 Einzelbilder.
> **Eine Bildsequenz hat keine eigene Bildrate.** Die Dateien wissen nichts
> davon, wie schnell sie laufen sollen — Premiere vergibt sie beim Import aus
> *Einstellungen → Medien → Zeitbasis für unbestimmte Medien*. Ab Werk stehen
> dort meist 30 fps. Was das für die Dauer heißt, steht in der Tabelle unten.
> Ändern lässt es sich jederzeit: Rechtsklick auf den Clip im Projektfenster →
> *Ändern → Videomaterial interpretieren → Bildrate annehmen*. Die Bewegung
> wird davon nur schneller oder langsamer, nie ruckelig — es liegt für jedes
> Bild eine eigene Datei vor.
Der übliche Aufbau im Schnitt sind drei Teile hintereinander:
`<name>-ein` → `png/<name>.png` beliebig lang gezogen → `<name>-aus`.
Das letzte Bild von `-ein` und das erste von `-aus` sind das Standbild, die
Stöße sind also unsichtbar. Alle drei Teile haben dieselben Maße und denselben
Bildausschnitt — was du am ersten an Position und Größe einstellst, kannst du
auf die beiden anderen einfügen (*Bearbeiten → Einfügen → Attribute*).
Der Alphakanal kommt mit. Kein Keying, keine Maske, kein Mischmodus: die Karte
liegt auf einer eigenen Spur über dem Bild und der Rest ist durchsichtig.
### Dauer je Komponente
| Komponente | Bilder ein / aus | bei 25 fps | bei 30 fps |
|---|---|---|---|
| `bauchbinde-noah` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
| alle `satz-*` | 18 / 11 | 0,72 s / 0,44 s | 0,60 s / 0,37 s |
| beide `stapel-*` | 22 / 13 | 0,88 s / 0,52 s | 0,73 s / 0,43 s |
| `ring-kern` | 12 / 7 | 0,48 s / 0,28 s | 0,40 s / 0,23 s |
| `logo-bug` | 14 / 8 | 0,56 s / 0,32 s | 0,47 s / 0,27 s |
| `wabenfeld` | **50 / 30** | **2,00 s / 1,20 s** | **1,67 s / 1,00 s** |
Mehr Bilder heißt längere Bewegung, nicht flüssigere: die Staffelung steckt in
Anteilen des Verlaufs, nicht in Zeitwerten. Wer eine langsamere Einblendung
will, erhöht `bilder` bei der Komponente in `komponenten.html` und baut neu.
**Das Wabenfeld tanzt aus der Reihe, und das ist Absicht.** Alle anderen
Einblendungen setzen ein Stichwort, während Noah spricht — die dürfen nichts
wollen und blenden deshalb schlicht auf. Das Wabenfeld ist das Bild zur
Aussage und baut sich auf: die Nabe steht, dann wachsen die Waben in fünf
Wellen von innen nach außen, und die Stränge ziehen jeweils kurz danach nach.
Rückwärts zieht sich alles wieder in die Marke zurück.
Die Bewegung ist die `introTimeline` aus `HoneycombSection.tsx` der Website,
nicht neu erfunden: `scale` von 0,16 auf 1, `back.out(1.75)`, gestaffelt nach
Abstand zur Mitte, danach die Stränge mit einer eigenen Deckungsrampe. Wer an
der Zeitrechnung dreht, findet sie als `WABE_ZEIT` in `komponenten.html`.
> **Nicht skalieren, wo es nicht sein muss.** Die Dateien sind in doppelter
> Größe gerendert (`FAKTOR` in `bauen.mjs`). In einer Zeitleiste mit
> 1920 × 1080 setzt du sie also auf rund 50 % — dann sitzt jeder Pixel, und
> nach oben ist bis 4K Luft.
## Was es gibt
| Schlüssel | Was es zeigt | Wofür im Skript |
|---|---|---|
| `bauchbinde-noah` | Noah Geerkens · Gründer | wenn er sich vorstellt |
| `satz-erp-bleibt` | „Dein ERP **bleibt**." | der Einwand, direkt beantwortet |
| `satz-kein-ersatz` | „Kein **Ersatz**." / „Der Aufbau drumherum." | die Kernaussage |
| `satz-miteinander-reden` | „Die Tools reden **miteinander**." | was gefehlt hat |
| `satz-handwerksalltag` | „Kein **Startup**-Projekt." | der Abbinder |
| `stapel-haengt-drin` | Stammdaten · Aufträge · Buchhaltung | was einen Wechsel unmöglich macht |
| `stapel-was-fehlte` | KI-Lösungen · CRM · Automatisierungen | was sie wollten |
| `ring-kern` | der Kern bleibt, HiveGrid liegt darum | „kein Ersatz, sondern der Aufbau drumherum" |
| `wabenfeld` | die Marke in der Mitte, die Programme drumherum | „dass die Tools endlich miteinander reden" |
| `logo-bug` | Wortmarke für die Ecke | durchgehend oder am Anfang |
> Eine Bauchbinde für den Betrieb ließ sich über `?name=` und `?rolle=` ohnehin
> aus der ersten erzeugen und ist deshalb weg, mit ihr der Stempel „Vierte
> Generation". Eine Endkarte gab es auch einmal — ein Schlussbild gehört in den
> Schnitt und nicht als Einblendung darauf.
**Wenig Text ist Absicht.** Noah spricht das Skript, die Einblendung setzt das
Stichwort. Zwei Sätze auf einer Karte lesen sich nicht, während jemand redet.
## Texte ändern
Die Texte hängen an der Adresse, nicht am Code. Server starten (`./serve.sh`),
dann zum Beispiel:
```
http://127.0.0.1:4321/video/komponenten.html?k=satz-erp-bleibt&zeile=Dein *CRM* bleibt.
http://127.0.0.1:4321/video/komponenten.html?k=bauchbinde-noah&name=Lena Geerkens&rolle=Geschäftsführung
```
`*Wort*` setzt den gelben Akzent, **höchstens einer je Zeile** (rules.md §4).
Welche Felder eine Komponente kennt, steht bei ihr unter `felder` in
`komponenten.html`. `?alle=1` zeigt alle nebeneinander auf einem Karo-Grund.
Wer einen geänderten Text als Datei braucht, trägt ihn in `komponenten.html`
ein und baut neu — die Adresse wirkt nur im Browser.
## Neu bauen
```sh
./serve.sh # muss laufen, in einem zweiten Terminal
node video/bauen.mjs # alles
node video/bauen.mjs bauchbinde satz # nur passende Schlüssel
```
Ein paar Entscheidungen, die dahinterstehen:
- **Bildsequenz statt Video mit Alphakanal.** Ein Video mit echter
Transparenz wäre ProRes 4444. Als das hier entstand, war kein ffmpeg auf der
Maschine; inzwischen schon (siehe `animation.mjs` im Wurzelverzeichnis).
Geblieben ist die Bildsequenz trotzdem: Premiere importiert sie genauso, sie
behält die Transparenz ohne Codec-Frage, und jedes Einzelbild lässt sich
ansehen. Ein ProRes-Ausgang wäre jetzt möglich, ist aber kein Gewinn.
- **Node statt Python**, anders als sonst im Repo. Gebraucht wird eine
Steuerverbindung zu Chrome, und die hat Node ohne Zusatzpaket an Bord.
Chrome pro Einzelbild neu zu starten wäre die Alternative gewesen — bei rund
380 Bildern sind das zehn Minuten Warten für nichts.
- **Der Ausschnitt wird gemessen, nicht angegeben.** `KOMP_KASTEN()` misst
nach dem Rendern, was tatsächlich dasteht, und legt 52 px Zugabe drumherum:
der Schatten liegt außerhalb der Box, und was aufblendet, startet 26 px
tiefer. Ohne die Zugabe schneidet das PNG beides ab.
- **Die Ausblendung ist die Einblendung rückwärts**, nur kürzer. Ein eigener
Ausgang sähe eine Spur besser aus und kostete doppelt so viele Bilder.
## Drei Fallen, in die wir hier schon getappt sind
**`document.fonts.ready` war erfüllt, bevor es etwas zu laden gab.** Die Seite
hat auf die Schrift gewartet, *bevor* die Karte im DOM stand — zu dem Zeitpunkt
brauchte niemand Manrope, es lief also kein Ladevorgang, und das Versprechen
löste sofort auf. Danach wurde die Karte gebaut, Chrome fing an zu laden, und ob
es bis zur Messung fertig war, entschied der Zufall. Dieselbe Komponente
hintereinander gemessen kam abwechselnd 984 und 1010 px breit heraus.
Das ist die Falle aus `rules.md` §4, nur eine Ebene früher: dort steht „auf
`document.fonts.ready` warten", und genau das tat die Seite — nur zu früh.
`document.fonts.load()` holt eine Schriftlage unabhängig davon, ob gerade jemand
sie benutzt; erst danach ist `ready` eine Aussage. Dieselbe Wette steckt in
jedem `<img>` mit `width:auto`, deshalb wartet der Aufbau jetzt auch auf die
Bilder.
> **Woran man es merkt:** an den Maßen in `liste.json`. Eine Komponente, die
> beim Neubau plötzlich 2 bis 4 Prozent schmaler wird, hat keine Textänderung
> hinter sich, sondern die Ersatzschrift. Die reinen Zeichnungen (`ring-kern`,
> `wabenfeld`) verraten es nicht — ihr SVG hat feste Maße.
**Der Schatten war nicht da.** Die Karte trug `clip-path` für die
abgeschnittenen Ecken und `box-shadow` für den Schatten — und `clip-path`
beschneidet den Schatten mit. Genau so steht es in `rules.md` §3, und genau so
ist es passiert. Am gerenderten PNG fiel die Deckung an der Kartenkante von
255 auf 0, ohne einen einzigen Zwischenwert. Der Schatten sitzt jetzt als
`filter: drop-shadow()` auf einer Hülle. Nebenbei folgt der der Alphamaske und
legt sich um die abgeschnittenen Ecken herum statt um ein Rechteck.
**`inset(0 0 0 0)` ist nicht dasselbe wie kein Beschnitt.** Die Einblendung
zieht die Karte über `clip-path: inset()` von links auf. Am Ende steht dort
rechnerisch null — das schneidet aber weiterhin exakt auf die Box und kappt
damit wieder den Schatten. Bei vollem Verlauf fällt der Beschnitt deshalb ganz
weg.
## Was fehlt
Der Grund, warum die Marken im Wabenfeld sitzen, steht in `rules.md` §3
unter *Wabenfeld*. Wer dort etwas ändert, muss an drei Dateien denken:
`post2-alt.html`, `post3.html` und diese hier. Das ist der Preis dafür, dass
jede Datei für sich lauffähig bleibt.
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env node
/* Rendert komponenten.html zu PNGs mit Alphakanal und zu Bildsequenzen.
*
* node video/bauen.mjs alles neu
* node video/bauen.mjs bauchbinde nur passende Schluessel
*
* Warum Node und nicht Python wie sonst im Repo: gebraucht wird eine
* Steuerverbindung zu Chrome (CDP, ueber WebSocket), und die hat Node ohne
* Zusatzpaket an Bord, Python nicht. Der Umweg ueber "Chrome pro Bild neu
* starten" waere die Alternative gewesen -- bei rund 400 Einzelbildern und
* anderthalb Sekunden Startzeit sind das zehn Minuten Warten fuer nichts.
*
* Es laeuft ueber den Server aus dem Repo und nicht ueber file://, weil die
* Seite Schrift und Marken per fetch holt und das lokale Dateiprotokoll das
* verbietet.
*
* Kein ffmpeg noetig. Ein Video mit echtem Alphakanal waere ProRes 4444; eine
* Bildsequenz kann Premiere genauso importieren, behaelt die Transparenz ohne
* Codec-Frage und laesst sich Bild fuer Bild ansehen. (ffmpeg liegt inzwischen
* auf der Maschine -- animation.mjs im Wurzelverzeichnis benutzt es fuer die
* MP4s der Posts. Hier braucht es das trotzdem nicht.)
*/
import { spawn } from "node:child_process";
import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HIER = path.dirname(fileURLToPath(import.meta.url));
const BASIS = "http://127.0.0.1:4321/video/komponenten.html";
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const PORT = 9333;
/* Aufloesung. Die Komponenten sind fuer eine Zeitleiste in 1920 x 1080
entworfen -- eine Bauchbinde misst dort rund 500 px. Zweifach gerendert
traegt sie damit auch in 4K und haelt eine Vergroesserung im Schnitt aus,
ohne dass die Sequenzen den Ordner sprengen. Standbild und Sequenz haben
denselben Faktor, sonst passen sie im Schnitt nicht uebereinander. */
const FAKTOR = 2;
/* Die Ausblendung ist die Einblendung rueckwaerts, nur kuerzer. Ein eigener
Ausgang saehe eine Spur besser aus und kostete die doppelte Zahl an
Bildern; bei einer halben Sekunde sieht das niemand. */
const AUS_ANTEIL = 0.6;
const filter = process.argv.slice(2);
const passt = key => !filter.length || filter.some(f => key.includes(f));
/* ── CDP, das Noetigste ──────────────────────────────────────────────────── */
async function verbinden(port) {
let ziele = null;
for (let i = 0; i < 50; i++) {
ziele = await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()).catch(() => null);
if (ziele && ziele.some(t => t.type === "page")) break;
await new Promise(r => setTimeout(r, 200));
}
const seite = ziele && ziele.find(t => t.type === "page");
if (!seite) throw new Error("Chrome antwortet nicht auf " + port);
const ws = new WebSocket(seite.webSocketDebuggerUrl);
const offen = new Map();
const horcher = [];
ws.onmessage = e => {
const m = JSON.parse(e.data);
if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id); }
else if (m.method) horcher.forEach(h => h(m));
};
await new Promise((ok, weg) => { ws.onopen = ok; ws.onerror = weg; });
let n = 0;
const ruf = (method, params = {}) => new Promise((ok, weg) => {
const id = ++n;
offen.set(id, m => m.error ? weg(new Error(method + ": " + m.error.message)) : ok(m.result));
ws.send(JSON.stringify({ id, method, params }));
});
const einmal = ereignis => new Promise(ok => {
const h = m => { if (m.method === ereignis) { horcher.splice(horcher.indexOf(h), 1); ok(m); } };
horcher.push(h);
});
return { ruf, einmal, zu: () => ws.close() };
}
/* Ein Ausdruck in der Seite. Fehler dort sollen hier ankommen und nicht als
stilles undefined durchrutschen. */
async function werte(cdp, ausdruck) {
const r = await cdp.ruf("Runtime.evaluate", {
expression: ausdruck, awaitPromise: true, returnByValue: true,
});
if (r.exceptionDetails) {
const d = r.exceptionDetails;
throw new Error("in der Seite: " + (d.exception?.description || d.text));
}
return r.result.value;
}
async function aufnehmen(cdp, kasten) {
const r = await cdp.ruf("Page.captureScreenshot", {
format: "png",
captureBeyondViewport: true,
clip: { x: kasten.x, y: kasten.y, width: kasten.w, height: kasten.h, scale: FAKTOR },
});
return Buffer.from(r.data, "base64");
}
/* ── Ablauf ──────────────────────────────────────────────────────────────── */
const chrome = spawn(CHROME, [
"--headless", "--disable-gpu", "--hide-scrollbars",
"--remote-debugging-port=" + PORT,
"--user-data-dir=" + path.join(HIER, ".chrome"),
"about:blank",
], { stdio: "ignore" });
let cdp;
try {
cdp = await verbinden(PORT);
await cdp.ruf("Page.enable");
await cdp.ruf("Runtime.enable");
// Ohne das steht Weiss unter der Seite und jedes PNG waere deckend.
await cdp.ruf("Emulation.setDefaultBackgroundColorOverride",
{ color: { r: 0, g: 0, b: 0, a: 0 } });
const laden = async adresse => {
await cdp.ruf("Page.navigate", { url: adresse });
await cdp.einmal("Page.loadEventFired");
await werte(cdp, "window.KOMP_BEREIT");
};
await laden(BASIS + "?k=");
const liste = (await werte(cdp, "window.KOMP_LISTE")).filter(k => passt(k.key));
if (!liste.length) throw new Error("kein Schluessel passt auf: " + filter.join(", "));
const pngDir = path.join(HIER, "png");
const seqDir = path.join(HIER, "sequenz");
await mkdir(pngDir, { recursive: true });
await mkdir(seqDir, { recursive: true });
let dateien = 0, bytes = 0;
const bericht = [];
for (const k of liste) {
// Die Buehne gross genug stellen, sonst schneidet der Viewport ab, bevor
// ueberhaupt gemessen werden kann.
await cdp.ruf("Emulation.setDeviceMetricsOverride", {
width: k.w, height: k.h, deviceScaleFactor: 1, mobile: false,
});
await laden(`${BASIS}?k=${encodeURIComponent(k.key)}&t=1`);
const kasten = await werte(cdp, "window.KOMP_KASTEN()");
// Sequenzen. Ein bis der Kasten steht, aus rueckwaerts und knapper.
const einN = k.bilder;
const ausN = Math.max(4, Math.round(k.bilder * AUS_ANTEIL));
let gehalten = null;
for (const [name, anzahl, richtung] of [["ein", einN, 1], ["aus", ausN, -1]]) {
const ordner = path.join(seqDir, k.key + "-" + name);
await rm(ordner, { recursive: true, force: true });
await mkdir(ordner, { recursive: true });
for (let i = 0; i < anzahl; i++) {
const p = i / (anzahl - 1);
await werte(cdp, "window.setT(" + (richtung > 0 ? p : 1 - p) + ")");
const bild = await aufnehmen(cdp, kasten);
if (richtung > 0 && i === anzahl - 1) gehalten = bild;
await writeFile(path.join(ordner, `${k.key}-${name}_${String(i).padStart(4, "0")}.png`), bild);
dateien++; bytes += bild.length;
}
}
/* Das Standbild ist das letzte Bild der Einblendung, nicht eine eigene
Aufnahme. Der ganze Dreiteiler im Schnitt haengt daran, dass -ein, das
Standbild und -aus an den Stoessen dasselbe Bild zeigen; als zweite
Aufnahme war das bisher nur wahr, weil es zufaellig stimmte.
Beim Wabenfeld stimmte es dann nicht mehr. Dort tragen die Zellen
waehrend der Bewegung ein transform-Attribut, das am Ende wieder
weggenommen wird -- danach rastert Chrome eine Spur anders als bei einem
Element, das nie eines hatte. Der Unterschied lag bei einer Stufe auf
verstreuten Pixeln, also unsichtbar, aber die Zusage war weg. So wird
sie eingehalten, statt geprueft: es ist dieselbe Datei. */
await writeFile(path.join(pngDir, k.key + ".png"), gehalten);
dateien++; bytes += gehalten.length;
const zeile = `${k.key.padEnd(24)} ${String(kasten.w * FAKTOR).padStart(5)} x ` +
`${String(kasten.h * FAKTOR).padEnd(5)} still + ${einN} ein + ${ausN} aus`;
console.log(" " + zeile);
bericht.push({ key: k.key, name: k.name, w: kasten.w * FAKTOR, h: kasten.h * FAKTOR,
ein: einN, aus: ausN });
}
/* Der Teilbau darf die Liste nicht auf sich selbst eindampfen: wer nur die
Bauchbinde neu baut, will danach trotzdem alle dreizehn darin finden.
Also wird zusammengefuehrt statt ueberschrieben, in der Reihenfolge der
Vorlage -- die bestimmt, was zusammengehoert, nicht der Zufall des
letzten Aufrufs. */
const vorher = JSON.parse(
await readFile(path.join(HIER, "liste.json"), "utf8").catch(() => '{"komponenten":[]}'));
const nachKey = new Map(vorher.komponenten.map(k => [k.key, k]));
bericht.forEach(k => nachKey.set(k.key, k));
const alle = (await werte(cdp, "window.KOMP_LISTE")).map(k => nachKey.get(k.key)).filter(Boolean);
await writeFile(path.join(HIER, "liste.json"),
JSON.stringify({ faktor: FAKTOR, komponenten: alle }, null, 1) + "\n");
console.log(`\n${liste.length} Komponenten, ${dateien} Dateien, ` +
`${(bytes / 1024 / 1024).toFixed(1)} MB`);
} finally {
if (cdp) cdp.zu();
chrome.kill();
/* Erst warten, bis Chrome wirklich weg ist. Ein Kill ist eine Bitte, kein
Vollzug: beim Herunterfahren schreibt der Browser noch in sein Profil,
und ein rm davor liess den Ordner wieder auferstehen. */
await new Promise(ok => chrome.once("exit", ok));
// Das Profil ist Wegwerfware und hat im Repo nichts verloren.
await rm(path.join(HIER, ".chrome"), { recursive: true, force: true });
}
File diff suppressed because it is too large. Load diff
+85
View File
@@ -0,0 +1,85 @@
{
"faktor": 2,
"komponenten": [
{
"key": "bauchbinde-noah",
"name": "Bauchbinde · Noah",
"w": 1202,
"h": 490,
"ein": 20,
"aus": 12
},
{
"key": "satz-erp-bleibt",
"name": "Satz · Dein ERP bleibt",
"w": 1140,
"h": 490,
"ein": 18,
"aus": 11
},
{
"key": "satz-kein-ersatz",
"name": "Satz · Kein Ersatz",
"w": 984,
"h": 588,
"ein": 18,
"aus": 11
},
{
"key": "satz-miteinander-reden",
"name": "Satz · Miteinander reden",
"w": 1722,
"h": 490,
"ein": 18,
"aus": 11
},
{
"key": "satz-handwerksalltag",
"name": "Satz · Handwerksalltag",
"w": 1390,
"h": 588,
"ein": 18,
"aus": 11
},
{
"key": "stapel-haengt-drin",
"name": "Stapel · Was drin haengt",
"w": 858,
"h": 824,
"ein": 22,
"aus": 13
},
{
"key": "stapel-was-fehlte",
"name": "Stapel · Was gefehlt hat",
"w": 1038,
"h": 824,
"ein": 22,
"aus": 13
},
{
"key": "ring-kern",
"name": "Ring · Dein Programm bleibt",
"w": 2184,
"h": 1668,
"ein": 12,
"aus": 7
},
{
"key": "wabenfeld",
"name": "Wabenfeld · Alles verbunden",
"w": 2168,
"h": 1906,
"ein": 50,
"aus": 30
},
{
"key": "logo-bug",
"name": "Logo · Ecke",
"w": 1620,
"h": 380,
"ein": 14,
"aus": 8
}
]
}