diff --git a/agent/templates/visual_45.html b/agent/templates/visual_45.html
index 7eb9fd1..f2b75e2 100644
--- a/agent/templates/visual_45.html
+++ b/agent/templates/visual_45.html
@@ -4,327 +4,63 @@
 <meta charset="UTF-8">
 <style>
 * { margin: 0; padding: 0; box-sizing: border-box; }
-
 body {
-  width: 1080px;
-  height: 1350px;
-  overflow: hidden;
-  font-family: -apple-system, 'Segoe UI', Arial, sans-serif;
-  color: #ffffff;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  background: #1c1208;
+  width: 1080px; height: 1350px; overflow: hidden; position: relative;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+  color: #fff; background: #111;
 }
-
-.scene-bg {
-  position: absolute;
-  top: 0; left: 0;
-  width: 1080px;
-  height: 1350px;
-  z-index: 0;
+.photo {
+  position: absolute; inset: 0; width: 100%; height: 100%;
+  object-fit: cover; object-position: center; z-index: 0;
 }
-
-.scrim {
-  position: absolute;
-  inset: 0;
-  background: linear-gradient(
-    155deg,
-    rgba(0,0,0,.62) 0%,
-    rgba(0,0,0,.46) 48%,
-    rgba(0,0,0,.18) 100%
-  );
-  z-index: 1;
-  pointer-events: none;
+.grade {
+  position: absolute; inset: 0; z-index: 1;
+  background: linear-gradient(180deg, rgba(7,13,16,.22) 0%, rgba(7,13,16,.02) 42%, rgba(7,13,16,.14) 62%, rgba(7,13,16,.94) 100%);
 }
-
-.top-strip {
-  height: 62px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 64px;
-  background: rgba(0,0,0,.35);
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
-  border-bottom: 1px solid rgba(255,255,255,.08);
+.brand {
+  position: absolute; top: 54px; left: 60px; z-index: 2;
+  font-size: 17px; font-weight: 750; letter-spacing: .13em; text-transform: uppercase;
+  text-shadow: 0 2px 16px rgba(0,0,0,.8);
 }
-.top-brand {
-  font-size: 14px;
-  font-weight: 800;
-  letter-spacing: .16em;
-  color: {{ accent_light }};
-  text-transform: uppercase;
-  opacity: .55;
+.story {
+  position: absolute; left: 60px; right: 60px; bottom: 54px; z-index: 2;
 }
-.top-date {
-  font-size: 14px;
-  color: rgba(255,255,255,.28);
-  font-weight: 500;
-  letter-spacing: .08em;
-  text-transform: uppercase;
-}
-
-.risk-banner {
-  background: #E63946;
-  color: #fff;
-  padding: 14px 64px;
-  font-size: 22px;
-  font-weight: 700;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
-}
-
-.main {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  padding: 58px 64px 44px;
-  position: relative;
-  z-index: 2;
-}
-
 .eyebrow {
-  display: inline-flex;
-  align-items: center;
-  gap: 8px;
-  background: {{ accent_color }};
-  color: #fff;
-  padding: 7px 18px 7px 13px;
-  border-radius: 4px;
-  font-size: 15px;
-  font-weight: 800;
-  letter-spacing: .10em;
-  text-transform: uppercase;
-  align-self: flex-start;
-  margin-bottom: 32px;
+  display: inline-block; margin-bottom: 20px; padding: 9px 13px;
+  border-left: 4px solid {{ accent_color }}; background: rgba(5,10,12,.62);
+  font-size: 15px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase;
 }
-
 .hook {
-  font-size: {{ hook_size }};
-  font-weight: 900;
-  line-height: 1.06;
-  letter-spacing: -.02em;
-  color: #ffffff;
-  text-transform: uppercase;
-  text-shadow: 0 2px 18px rgba(0,0,0,.85), 0 0 44px rgba(0,0,0,.60);
-  white-space: pre-line;
-  max-width: 860px;
-  margin-bottom: 30px;
+  max-width: 800px; font-size: 66px; line-height: 1.03; font-weight: 780;
+  letter-spacing: -.025em; white-space: pre-line; text-shadow: 0 3px 26px rgba(0,0,0,.86);
 }
-
 .context {
-  font-size: 28px;
-  font-weight: 500;
-  line-height: 1.44;
-  color: {{ accent_light }};
-  opacity: .88;
-  max-width: 700px;
-  margin-bottom: 26px;
-  text-shadow: 0 1px 8px rgba(0,0,0,.80);
-}
-
-.subline {
-  font-size: 22px;
-  font-weight: 400;
-  line-height: 1.52;
-  color: rgba(255,255,255,.52);
-  max-width: 720px;
-  border-left: 4px solid {{ accent_color }};
-  padding-left: 20px;
-  margin-bottom: auto;
-  text-shadow: 0 1px 6px rgba(0,0,0,.70);
-}
-
-.nutzen-box {
-  background: rgba(0,0,0,.52);
-  border: 1px solid rgba(255,255,255,.14);
-  border-left: 5px solid {{ accent_color }};
-  padding: 22px 26px;
-  border-radius: 6px;
-  margin-bottom: 0;
-}
-.nutzen-label {
-  font-size: 11px;
-  font-weight: 800;
-  letter-spacing: .12em;
-  text-transform: uppercase;
-  color: {{ accent_color }};
-  margin-bottom: 8px;
-}
-.nutzen-text {
-  font-size: 24px;
-  font-weight: 500;
-  color: rgba(255,255,255,.88);
-  line-height: 1.44;
+  max-width: 830px; margin-top: 19px; font-size: 25px; line-height: 1.36;
+  color: rgba(255,255,255,.88); text-shadow: 0 2px 14px rgba(0,0,0,.92);
 }
-
-.footer {
-  height: 94px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 64px;
-  background: rgba(0,0,0,.60);
-  border-top: 4px solid {{ accent_color }};
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
+.meta {
+  display: flex; align-items: center; justify-content: space-between; margin-top: 30px;
+  padding-top: 20px; border-top: 1px solid rgba(255,255,255,.28);
 }
-.brand-name {
-  font-size: 20px;
-  font-weight: 800;
-  letter-spacing: .06em;
-  color: {{ accent_light }};
-  text-transform: uppercase;
-  opacity: .80;
-}
-.brand-url {
-  font-size: 13px;
-  color: rgba(255,255,255,.28);
-  margin-top: 3px;
-}
-.cta-btn {
-  font-size: 18px;
-  font-weight: 800;
-  color: #fff;
-  background: {{ accent_color }};
-  padding: 15px 30px;
-  border-radius: 6px;
-  letter-spacing: .05em;
-  text-transform: uppercase;
-  white-space: nowrap;
+.url { font-size: 15px; color: rgba(255,255,255,.70); letter-spacing: .04em; }
+.cta {
+  padding: 12px 18px; border-radius: 5px; background: {{ accent_color }};
+  font-size: 15px; font-weight: 750;
 }
 </style>
 </head>
 <body>
-
-<!-- Szene: Mallorca-Innenraum mit sichtbarem Schimmelschaden (4:5) -->
-<svg class="scene-bg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1350">
-  <defs>
-    <linearGradient id="wall" x1="0" y1="0" x2="0.8" y2="1">
-      <stop offset="0%" stop-color="#c8aa82"/>
-      <stop offset="40%" stop-color="#b89870"/>
-      <stop offset="100%" stop-color="#8a7050"/>
-    </linearGradient>
-    <radialGradient id="damp1" cx="10%" cy="96%" r="48%">
-      <stop offset="0%" stop-color="#4a6252" stop-opacity="0.78"/>
-      <stop offset="55%" stop-color="#587060" stop-opacity="0.40"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <radialGradient id="damp2" cx="7%" cy="72%" r="25%">
-      <stop offset="0%" stop-color="#3a5244" stop-opacity="0.60"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <radialGradient id="winLight" cx="88%" cy="8%" r="34%">
-      <stop offset="0%" stop-color="#fff8d0" stop-opacity="0.55"/>
-      <stop offset="60%" stop-color="#ffd870" stop-opacity="0.18"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <filter id="mf" x="-25%" y="-25%" width="150%" height="150%">
-      <feTurbulence type="fractalNoise" baseFrequency="0.024" numOctaves="5" seed="12" result="noise"/>
-      <feDisplacementMap in="SourceGraphic" in2="noise" scale="24" xChannelSelector="R" yChannelSelector="G"/>
-    </filter>
-    <filter id="mf2" x="-20%" y="-20%" width="140%" height="140%">
-      <feTurbulence type="fractalNoise" baseFrequency="0.040" numOctaves="4" seed="3" result="noise"/>
-      <feDisplacementMap in="SourceGraphic" in2="noise" scale="16" xChannelSelector="G" yChannelSelector="B"/>
-    </filter>
-  </defs>
-
-  <!-- Wand -->
-  <rect width="1080" height="1350" fill="url(#wall)"/>
-
-  <!-- Feuchtigkeitshöfe -->
-  <ellipse cx="140" cy="1305" rx="490" ry="320" fill="url(#damp1)"/>
-  <ellipse cx="90" cy="960" rx="245" ry="330" fill="url(#damp2)"/>
-
-  <!-- Schimmelschaden Hauptmasse -->
-  <g filter="url(#mf)">
-    <ellipse cx="60" cy="1340" rx="270" ry="155" fill="#070d07" opacity="0.97"/>
-    <ellipse cx="22" cy="1295" rx="220" ry="185" fill="#090f09" opacity="0.94"/>
-    <ellipse cx="100" cy="1322" rx="205" ry="138" fill="#050c05" opacity="0.92"/>
-    <ellipse cx="125" cy="1342" rx="172" ry="114" fill="#0b130b" opacity="0.88"/>
-    <ellipse cx="268" cy="1320" rx="138" ry="92" fill="#0d170d" opacity="0.80"/>
-    <ellipse cx="342" cy="1332" rx="105" ry="74" fill="#0f190f" opacity="0.74"/>
-    <ellipse cx="405" cy="1325" rx="82" ry="60" fill="#111b11" opacity="0.66"/>
-    <!-- Wand hoch -->
-    <ellipse cx="55" cy="1185" rx="155" ry="128" fill="#090f09" opacity="0.84"/>
-    <ellipse cx="36" cy="1075" rx="118" ry="104" fill="#0b130b" opacity="0.74"/>
-    <ellipse cx="58" cy="978" rx="96" ry="86" fill="#0d170d" opacity="0.64"/>
-    <ellipse cx="40" cy="892" rx="76" ry="68" fill="#0f190f" opacity="0.54"/>
-    <ellipse cx="62" cy="818" rx="60" ry="55" fill="#111b11" opacity="0.44"/>
-    <ellipse cx="46" cy="752" rx="46" ry="42" fill="#131d13" opacity="0.35"/>
-    <ellipse cx="62" cy="695" rx="35" ry="32" fill="#151f15" opacity="0.26"/>
-  </g>
-
-  <!-- Mittlere Streuung -->
-  <g filter="url(#mf2)">
-    <ellipse cx="435" cy="1312" rx="50" ry="40" fill="#0d170d" opacity="0.62"/>
-    <ellipse cx="465" cy="1328" rx="40" ry="32" fill="#0f190f" opacity="0.57"/>
-    <circle cx="488" cy="1308" r="22" fill="#0b150b" opacity="0.54"/>
-    <circle cx="510" cy="1320" r="16" fill="#0d190d" opacity="0.50"/>
-    <circle cx="498" cy="1334" r="12" fill="#111d11" opacity="0.46"/>
-    <circle cx="525" cy="1305" r="9"  fill="#0f1b0f" opacity="0.43"/>
-    <circle cx="538" cy="1316" r="6"  fill="#131f13" opacity="0.39"/>
-    <!-- Sekundärbefall oben -->
-    <ellipse cx="32" cy="665" rx="36" ry="30" fill="#0b150b" opacity="0.40"/>
-    <ellipse cx="18" cy="642" rx="26" ry="23" fill="#0d190d" opacity="0.34"/>
-    <circle cx="46" cy="682" r="13" fill="#0f1b0f" opacity="0.30"/>
-  </g>
-
-  <!-- Mediterranes Fensterlicht -->
-  <ellipse cx="965" cy="135" rx="390" ry="295" fill="url(#winLight)"/>
-  <rect x="844" y="55" width="218" height="328" rx="4"
-        fill="none" stroke="rgba(210,172,105,0.28)" stroke-width="3"/>
-  <line x1="844" y1="220" x2="1062" y2="220" stroke="rgba(210,172,105,0.20)" stroke-width="2"/>
-  <line x1="953" y1="55" x2="953" y2="383" stroke="rgba(210,172,105,0.20)" stroke-width="2"/>
-</svg>
-
-<div class="scrim"></div>
-
-<div class="top-strip">
-  <span class="top-brand">Mallorca AirServices</span>
-  <span class="top-date">{{ date }}</span>
-</div>
-
-{% if risk_flag %}
-<div class="risk-banner">⚠️ Manuell prüfen — Risiko-Signal</div>
-{% endif %}
-
-<div class="main">
-  <div class="eyebrow">
-    <span>{{ cat_icon }}</span>
-    <span>{{ cat_label }}</span>
-  </div>
-
+<img class="photo" src="{{ photo_data_uri }}" alt="">
+<div class="grade"></div>
+<div class="brand">Mallorca AirServices</div>
+<div class="story">
+  <div class="eyebrow">{{ cat_label }}</div>
   <div class="hook">{{ hook_line }}</div>
-
   <div class="context">{{ headline }}</div>
-
-  {% if subline %}
-  <div class="subline">{{ subline }}</div>
-  {% endif %}
-
-  {% if nutzen %}
-  <div class="nutzen-box">
-    <div class="nutzen-label">Was das für Sie bedeutet</div>
-    <div class="nutzen-text">{{ nutzen }}</div>
-  </div>
-  {% endif %}
-</div>
-
-<div class="footer">
-  <div>
-    <div class="brand-name">Mallorca AirServices</div>
-    <div class="brand-url">mallorca-airservices.com</div>
+  <div class="meta">
+    <div class="url">mallorca-airservices.com</div>
+    <div class="cta">{{ cta_text }} →</div>
   </div>
-  <div class="cta-btn">{{ cta_text }} →</div>
 </div>
-
 </body>
 </html>
diff --git a/agent/templates/visual_square.html b/agent/templates/visual_square.html
index 47814e1..c82d14d 100644
--- a/agent/templates/visual_square.html
+++ b/agent/templates/visual_square.html
@@ -4,306 +4,63 @@
 <meta charset="UTF-8">
 <style>
 * { margin: 0; padding: 0; box-sizing: border-box; }
-
 body {
-  width: 1080px;
-  height: 1080px;
-  overflow: hidden;
-  font-family: -apple-system, 'Segoe UI', Arial, sans-serif;
-  color: #ffffff;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  background: #1c1208;
+  width: 1080px; height: 1080px; overflow: hidden; position: relative;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+  color: #fff; background: #111;
 }
-
-.scene-bg {
-  position: absolute;
-  top: 0; left: 0;
-  width: 1080px;
-  height: 1080px;
-  z-index: 0;
+.photo {
+  position: absolute; inset: 0; width: 100%; height: 100%;
+  object-fit: cover; object-position: center; z-index: 0;
 }
-
-.scrim {
-  position: absolute;
-  inset: 0;
-  background: linear-gradient(
-    155deg,
-    rgba(0,0,0,.62) 0%,
-    rgba(0,0,0,.46) 48%,
-    rgba(0,0,0,.18) 100%
-  );
-  z-index: 1;
-  pointer-events: none;
+.grade {
+  position: absolute; inset: 0; z-index: 1;
+  background: linear-gradient(180deg, rgba(7,13,16,.25) 0%, rgba(7,13,16,.03) 38%, rgba(7,13,16,.18) 58%, rgba(7,13,16,.92) 100%);
 }
-
-.top-strip {
-  height: 54px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 60px;
-  background: rgba(0,0,0,.35);
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
-  border-bottom: 1px solid rgba(255,255,255,.08);
+.brand {
+  position: absolute; top: 48px; left: 54px; z-index: 2;
+  font-size: 16px; font-weight: 750; letter-spacing: .13em; text-transform: uppercase;
+  text-shadow: 0 2px 16px rgba(0,0,0,.8);
 }
-.top-brand {
-  font-size: 12px;
-  font-weight: 800;
-  letter-spacing: .16em;
-  color: {{ accent_light }};
-  text-transform: uppercase;
-  opacity: .55;
+.story {
+  position: absolute; left: 54px; right: 54px; bottom: 48px; z-index: 2;
 }
-.top-date {
-  font-size: 12px;
-  color: rgba(255,255,255,.28);
-  font-weight: 500;
-  letter-spacing: .08em;
-  text-transform: uppercase;
-}
-
-.risk-banner {
-  background: #E63946;
-  color: #fff;
-  padding: 12px 60px;
-  font-size: 20px;
-  font-weight: 700;
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
-}
-
-.main {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  padding: 48px 60px 36px;
-  position: relative;
-  z-index: 2;
-}
-
 .eyebrow {
-  display: inline-flex;
-  align-items: center;
-  gap: 7px;
-  background: {{ accent_color }};
-  color: #fff;
-  padding: 6px 16px 6px 11px;
-  border-radius: 4px;
-  font-size: 14px;
-  font-weight: 800;
-  letter-spacing: .10em;
-  text-transform: uppercase;
-  align-self: flex-start;
-  margin-bottom: 28px;
+  display: inline-block; margin-bottom: 18px; padding: 8px 12px;
+  border-left: 4px solid {{ accent_color }}; background: rgba(5,10,12,.62);
+  font-size: 14px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase;
 }
-
 .hook {
-  font-size: {{ hook_size }};
-  font-weight: 900;
-  line-height: 1.06;
-  letter-spacing: -.02em;
-  color: #ffffff;
-  text-transform: uppercase;
-  text-shadow: 0 2px 16px rgba(0,0,0,.85), 0 0 40px rgba(0,0,0,.60);
-  white-space: pre-line;
-  max-width: 820px;
-  margin-bottom: 28px;
+  max-width: 760px; font-size: 60px; line-height: 1.02; font-weight: 780;
+  letter-spacing: -.025em; white-space: pre-line; text-shadow: 0 3px 24px rgba(0,0,0,.85);
 }
-
 .context {
-  font-size: 24px;
-  font-weight: 500;
-  line-height: 1.44;
-  color: {{ accent_light }};
-  opacity: .88;
-  max-width: 640px;
-  margin-bottom: auto;
-  text-shadow: 0 1px 8px rgba(0,0,0,.80);
-}
-
-.nutzen {
-  display: flex;
-  align-items: flex-start;
-  gap: 10px;
-  background: rgba(0,0,0,.52);
-  border: 1px solid rgba(255,255,255,.14);
-  border-left: 4px solid {{ accent_color }};
-  padding: 14px 20px;
-  border-radius: 6px;
-  font-size: 20px;
-  font-weight: 500;
-  color: rgba(255,255,255,.88);
-  max-width: 700px;
-  line-height: 1.44;
-  align-self: flex-start;
-}
-
-.footer {
-  height: 92px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 60px;
-  background: rgba(0,0,0,.60);
-  border-top: 3px solid {{ accent_color }};
-  flex-shrink: 0;
-  position: relative;
-  z-index: 2;
+  max-width: 790px; margin-top: 17px; font-size: 23px; line-height: 1.35;
+  color: rgba(255,255,255,.88); text-shadow: 0 2px 12px rgba(0,0,0,.9);
 }
-.brand-name {
-  font-size: 18px;
-  font-weight: 800;
-  letter-spacing: .06em;
-  color: {{ accent_light }};
-  text-transform: uppercase;
-  opacity: .80;
+.meta {
+  display: flex; align-items: center; justify-content: space-between; margin-top: 26px;
+  padding-top: 18px; border-top: 1px solid rgba(255,255,255,.28);
 }
-.brand-url {
-  font-size: 12px;
-  color: rgba(255,255,255,.28);
-  margin-top: 3px;
-}
-.cta-btn {
-  font-size: 17px;
-  font-weight: 800;
-  color: #fff;
-  background: {{ accent_color }};
-  padding: 14px 28px;
-  border-radius: 6px;
-  letter-spacing: .05em;
-  text-transform: uppercase;
-  white-space: nowrap;
+.url { font-size: 14px; color: rgba(255,255,255,.70); letter-spacing: .04em; }
+.cta {
+  padding: 11px 16px; border-radius: 5px; background: {{ accent_color }};
+  font-size: 14px; font-weight: 750;
 }
 </style>
 </head>
 <body>
-
-<!-- Szene: Mallorca-Innenraum mit sichtbarem Schimmelschaden -->
-<svg class="scene-bg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080">
-  <defs>
-    <linearGradient id="wall" x1="0" y1="0" x2="0.8" y2="1">
-      <stop offset="0%" stop-color="#c8aa82"/>
-      <stop offset="40%" stop-color="#b89870"/>
-      <stop offset="100%" stop-color="#8a7050"/>
-    </linearGradient>
-    <radialGradient id="damp1" cx="10%" cy="95%" r="50%">
-      <stop offset="0%" stop-color="#4a6252" stop-opacity="0.78"/>
-      <stop offset="55%" stop-color="#587060" stop-opacity="0.40"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <radialGradient id="damp2" cx="7%" cy="68%" r="28%">
-      <stop offset="0%" stop-color="#3a5244" stop-opacity="0.60"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <radialGradient id="winLight" cx="88%" cy="10%" r="38%">
-      <stop offset="0%" stop-color="#fff8d0" stop-opacity="0.55"/>
-      <stop offset="60%" stop-color="#ffd870" stop-opacity="0.18"/>
-      <stop offset="100%" stop-color="transparent"/>
-    </radialGradient>
-    <!-- Organische Schimmel-Textur -->
-    <filter id="mf" x="-25%" y="-25%" width="150%" height="150%">
-      <feTurbulence type="fractalNoise" baseFrequency="0.028" numOctaves="5" seed="12" result="noise"/>
-      <feDisplacementMap in="SourceGraphic" in2="noise" scale="22" xChannelSelector="R" yChannelSelector="G"/>
-    </filter>
-    <filter id="mf2" x="-20%" y="-20%" width="140%" height="140%">
-      <feTurbulence type="fractalNoise" baseFrequency="0.045" numOctaves="4" seed="3" result="noise"/>
-      <feDisplacementMap in="SourceGraphic" in2="noise" scale="14" xChannelSelector="G" yChannelSelector="B"/>
-    </filter>
-  </defs>
-
-  <!-- Wand -->
-  <rect width="1080" height="1080" fill="url(#wall)"/>
-
-  <!-- Feuchtigkeitshöfe -->
-  <ellipse cx="140" cy="1040" rx="460" ry="310" fill="url(#damp1)"/>
-  <ellipse cx="90" cy="740" rx="230" ry="300" fill="url(#damp2)"/>
-
-  <!-- Schimmelschaden — Hauptmasse (Ecke) -->
-  <g filter="url(#mf)">
-    <ellipse cx="60" cy="1068" rx="260" ry="150" fill="#070d07" opacity="0.97"/>
-    <ellipse cx="25" cy="1030" rx="210" ry="175" fill="#090f09" opacity="0.94"/>
-    <ellipse cx="95" cy="1055" rx="195" ry="130" fill="#050c05" opacity="0.92"/>
-    <ellipse cx="120" cy="1072" rx="165" ry="108" fill="#0b130b" opacity="0.88"/>
-    <!-- Horizontale Ausbreitung -->
-    <ellipse cx="260" cy="1052" rx="130" ry="88" fill="#0d170d" opacity="0.80"/>
-    <ellipse cx="330" cy="1062" rx="100" ry="70" fill="#0f190f" opacity="0.74"/>
-    <ellipse cx="390" cy="1055" rx="78" ry="56" fill="#111b11" opacity="0.66"/>
-    <!-- Vertikale Ausbreitung — Wand hoch -->
-    <ellipse cx="55" cy="945" rx="145" ry="120" fill="#090f09" opacity="0.84"/>
-    <ellipse cx="38" cy="848" rx="112" ry="98" fill="#0b130b" opacity="0.74"/>
-    <ellipse cx="58" cy="768" rx="90" ry="82" fill="#0d170d" opacity="0.64"/>
-    <ellipse cx="42" cy="698" rx="72" ry="65" fill="#0f190f" opacity="0.54"/>
-    <ellipse cx="62" cy="638" rx="55" ry="52" fill="#111b11" opacity="0.44"/>
-    <ellipse cx="46" cy="585" rx="42" ry="38" fill="#131d13" opacity="0.34"/>
-  </g>
-
-  <!-- Schimmelflecken — mittlere Streuung -->
-  <g filter="url(#mf2)">
-    <ellipse cx="420" cy="1045" rx="48" ry="38" fill="#0d170d" opacity="0.62"/>
-    <ellipse cx="448" cy="1058" rx="38" ry="30" fill="#0f190f" opacity="0.57"/>
-    <circle cx="468" cy="1040" r="20" fill="#0b150b" opacity="0.54"/>
-    <circle cx="488" cy="1050" r="15" fill="#0d190d" opacity="0.50"/>
-    <circle cx="478" cy="1062" r="11" fill="#111d11" opacity="0.46"/>
-    <circle cx="500" cy="1035" r="8"  fill="#0f1b0f" opacity="0.43"/>
-    <circle cx="512" cy="1044" r="6"  fill="#131f13" opacity="0.40"/>
-    <circle cx="522" cy="1030" r="4"  fill="#111d11" opacity="0.36"/>
-    <!-- Kleinfleck oben (Sekundärbefall) -->
-    <ellipse cx="32" cy="530" rx="34" ry="28" fill="#0b150b" opacity="0.40"/>
-    <ellipse cx="20" cy="510" rx="24" ry="22" fill="#0d190d" opacity="0.34"/>
-    <circle cx="44" cy="548" r="12" fill="#0f1b0f" opacity="0.30"/>
-    <circle cx="55" cy="520" r="8"  fill="#111d11" opacity="0.26"/>
-  </g>
-
-  <!-- Mediterranes Fensterlicht oben rechts -->
-  <ellipse cx="965" cy="115" rx="370" ry="275" fill="url(#winLight)"/>
-  <!-- Fensterrahmen-Andeutung -->
-  <rect x="844" y="48" width="218" height="310" rx="4"
-        fill="none" stroke="rgba(210,172,105,0.28)" stroke-width="3"/>
-  <line x1="844" y1="204" x2="1062" y2="204" stroke="rgba(210,172,105,0.20)" stroke-width="2"/>
-  <line x1="953" y1="48" x2="953" y2="358" stroke="rgba(210,172,105,0.20)" stroke-width="2"/>
-</svg>
-
-<div class="scrim"></div>
-
-<div class="top-strip">
-  <span class="top-brand">Mallorca AirServices</span>
-  <span class="top-date">{{ date }}</span>
-</div>
-
-{% if risk_flag %}
-<div class="risk-banner">⚠️ Manuell prüfen — Risiko-Signal</div>
-{% endif %}
-
-<div class="main">
-  <div class="eyebrow">
-    <span>{{ cat_icon }}</span>
-    <span>{{ cat_label }}</span>
-  </div>
-
+<img class="photo" src="{{ photo_data_uri }}" alt="">
+<div class="grade"></div>
+<div class="brand">Mallorca AirServices</div>
+<div class="story">
+  <div class="eyebrow">{{ cat_label }}</div>
   <div class="hook">{{ hook_line }}</div>
-
   <div class="context">{{ headline }}</div>
-
-  {% if nutzen %}
-  <div class="nutzen">{{ nutzen }}</div>
-  {% endif %}
-</div>
-
-<div class="footer">
-  <div>
-    <div class="brand-name">Mallorca AirServices</div>
-    <div class="brand-url">mallorca-airservices.com</div>
+  <div class="meta">
+    <div class="url">mallorca-airservices.com</div>
+    <div class="cta">{{ cta_text }} →</div>
   </div>
-  <div class="cta-btn">{{ cta_text }} →</div>
 </div>
-
 </body>
 </html>
diff --git a/agent/visual_render.py b/agent/visual_render.py
index b61d276..a3cf35e 100644
--- a/agent/visual_render.py
+++ b/agent/visual_render.py
@@ -6,6 +6,8 @@ Aufruf:
   python3 visual_render.py <signal_id> [1x1|4x5]
   python3 visual_render.py --test   (5 Testsignale)
 """
+import base64
+import mimetypes
 import sys, os, sqlite3, math, textwrap
 from pathlib import Path
 from radar_database import RADAR_DB_PATH
@@ -18,6 +20,7 @@ RADAR_DIR    = Path(os.environ.get("SMA_RADAR_DIR", "/opt/struktur/social-media-
 DB_RADAR     = RADAR_DB_PATH
 TEMPLATE_DIR = BASE_DIR / "templates"
 OUTPUT_DIR   = Path(os.environ.get("SMA_VISUALS_DIR", str(BASE_DIR / "generated_visuals"))).resolve()
+PHOTO_DIR    = Path(os.environ.get("SMA_PHOTO_DIR", str(BASE_DIR / "photo_assets"))).resolve()
 OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 
 # visual_gate liegt im Radar-Verzeichnis
@@ -28,9 +31,9 @@ from topic_normalize import normalize_topic
 # ── Kategorie-Konfig ──────────────────────────────────────────────────────
 CAT_CFG = {
     "feuchte": {
-        "icon": "💧", "label": "Schimmel kommt",
+        "icon": "💧", "label": "Feuchtigkeit im Leerstand",
         "bg": "#1c1208", "accent": "#c8402a", "light": "#edc878",
-        "text": "#ffffff", "cta": "Kostenlosen Check anfragen",
+        "text": "#ffffff", "cta": "Feuchte-Check anfragen",
     },
     "wetter_klima": {
         "icon": "⛈️", "label": "Wetter-Alarm",
@@ -65,6 +68,19 @@ FORMATS = {
 }
 
 
+def load_photo_data_uri(signal: dict, photo_dir: Path = PHOTO_DIR) -> str | None:
+    """Load an explicitly selected local photo asset for a signal."""
+    sid = (signal.get("signal_id") or "unknown")[:8]
+    for extension in (".jpg", ".jpeg", ".png", ".webp"):
+        asset = photo_dir / f"{sid}{extension}"
+        if not asset.is_file():
+            continue
+        mime = mimetypes.guess_type(asset.name)[0] or "image/jpeg"
+        encoded = base64.b64encode(asset.read_bytes()).decode("ascii")
+        return f"data:{mime};base64,{encoded}"
+    return None
+
+
 def _headline_size(text: str, fmt: str) -> str:
     n = len(text)
     if fmt == "1x1":
@@ -97,7 +113,7 @@ def _source_short(source_name: str) -> str:
 
 # ── Visual Hook Lines (Feed-Impact) ──────────────────────────────────────────
 HOOK_LINES = {
-    "feuchte":          "Schimmel entsteht,\nbevor man ihn sieht.",
+    "feuchte":          "Kondensation\nfrüh erkennen.",
     "schimmel_risiko":  "Erster Fleck an der Wand —\nda ist schon mehr.",
     "wetter_klima":     "Nach dem Sturm:\nWas Ihre Immobilie jetzt zeigt.",
     "ferienimmobilie":  "3 Monate leer —\nund das Problem beginnt.",
@@ -175,6 +191,14 @@ def render_signal(signal: dict, fmt: str = "1x1") -> dict:
     if fmt not in FORMATS:
         return {"success": False, "path": None, "reason": f"Unbekanntes Format: {fmt}"}
 
+    photo_data_uri = load_photo_data_uri(signal, PHOTO_DIR)
+    if not photo_data_uri:
+        return {
+            "success": False,
+            "path": None,
+            "reason": f"Fotorealistisches Bild fehlt: {sid[:8]}",
+        }
+
     fmt_cfg  = FORMATS[fmt]
     tmpl_path = TEMPLATE_DIR / fmt_cfg["template"]
     if not tmpl_path.exists():
@@ -182,6 +206,7 @@ def render_signal(signal: dict, fmt: str = "1x1") -> dict:
 
     # Kontext aufbauen
     ctx = build_context(signal, fmt, gate)
+    ctx["photo_data_uri"] = photo_data_uri
 
     # Template rendern
     raw_html = tmpl_path.read_text(encoding="utf-8")
diff --git a/radar/visual_gate.py b/radar/visual_gate.py
index 4a8d4ba..6aaea24 100644
--- a/radar/visual_gate.py
+++ b/radar/visual_gate.py
@@ -16,11 +16,11 @@ ETHICAL_BLOCK = [
 ]
 
 CATEGORY_VISUAL_TYPE = {
-    "feuchte":         "symbolgrafik",
-    "wetter_klima":    "warnbanner",
-    "mallorca_news":   "news_karte",
-    "ferienimmobilie": "news_karte",
-    "schimmel_risiko": "warnbanner",
+    "feuchte":         "photo",
+    "wetter_klima":    "photo",
+    "mallorca_news":   "photo",
+    "ferienimmobilie": "photo",
+    "schimmel_risiko": "photo",
 }
 
 
diff --git a/staging/sync_live_signals.py b/staging/sync_live_signals.py
new file mode 100644
index 0000000..a4d1cde
--- /dev/null
+++ b/staging/sync_live_signals.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""Mirror the live SMA radar read API into the isolated staging database.
+
+The upstream is read-only. No status, pin, content, package, or publishing endpoint
+is called. Local staging workflow fields are preserved on repeated syncs.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sqlite3
+import urllib.parse
+import urllib.request
+from pathlib import Path
+from typing import Any
+
+DEFAULT_UPSTREAM = "https://sma.agentsolutions-mallorca.com/api/signals"
+DEFAULT_DATABASE = Path(__file__).resolve().parent / "data" / "signals.db"
+REQUIRED_FIELDS = {"signal_id", "topic", "created_at", "radar_score", "source_name"}
+PRESERVE_ON_UPDATE = {"signal_id", "status", "pinned", "publish_status", "publish_status_at"}
+
+
+def fetch_live_signals(
+    upstream: str = DEFAULT_UPSTREAM,
+    *,
+    days: int = 14,
+    limit: int = 500,
+    timeout: float = 30.0,
+) -> list[dict[str, Any]]:
+    parsed = urllib.parse.urlparse(upstream)
+    if parsed.scheme != "https":
+        raise ValueError("live radar upstream must use HTTPS")
+    query = urllib.parse.urlencode({"days": days, "limit": limit, "sort": "newest"})
+    separator = "&" if parsed.query else "?"
+    request = urllib.request.Request(
+        f"{upstream}{separator}{query}",
+        headers={"User-Agent": "SMA-Staging-Live-Mirror/1.0"},
+    )
+    with urllib.request.urlopen(request, timeout=timeout) as response:
+        payload = json.loads(response.read().decode("utf-8"))
+    if not isinstance(payload, list):
+        raise ValueError("live radar API did not return a list")
+    return payload
+
+
+def _json_value(value: Any) -> Any:
+    if isinstance(value, (dict, list)):
+        return json.dumps(value, ensure_ascii=False, sort_keys=True)
+    return value
+
+
+def _validate_payload(payload: list[dict[str, Any]]) -> None:
+    if not payload:
+        raise ValueError("live radar returned no signals; staging database left unchanged")
+    for index, item in enumerate(payload):
+        if not isinstance(item, dict):
+            raise ValueError(f"signal {index} is not an object")
+        missing = sorted(field for field in REQUIRED_FIELDS if item.get(field) in (None, ""))
+        if missing:
+            raise ValueError(f"signal {index} is missing required fields: {', '.join(missing)}")
+
+
+def sync_payload(database: str | Path, payload: list[dict[str, Any]]) -> dict[str, int]:
+    """Atomically replace the live-mirror window while preserving local review state."""
+    _validate_payload(payload)
+    database = Path(database).resolve()
+    if not database.is_file():
+        raise FileNotFoundError(database)
+
+    with sqlite3.connect(database) as connection:
+        columns = {row[1] for row in connection.execute("PRAGMA table_info(signals)")}
+        if "source_type" not in columns:
+            raise RuntimeError("signals table has no source_type column")
+
+        incoming_ids: list[str] = []
+        stored = 0
+        for item in payload:
+            incoming = dict(item)
+            incoming["source_type"] = "live_mirror"
+            incoming.setdefault("timestamp", incoming.get("created_at"))
+            incoming.setdefault("language", "de")
+            incoming.setdefault("signal_category", "mallorca_news")
+            incoming.setdefault("status", "neu")
+
+            values = {
+                key: _json_value(value)
+                for key, value in incoming.items()
+                if key in columns and key != "extra_sources"
+            }
+            values["signal_id"] = str(incoming["signal_id"])
+            incoming_ids.append(values["signal_id"])
+
+            names = sorted(values)
+            placeholders = ",".join(f":{name}" for name in names)
+            updates = [name for name in names if name not in PRESERVE_ON_UPDATE]
+            update_sql = ",".join(f"{name}=excluded.{name}" for name in updates)
+            connection.execute(
+                f"INSERT INTO signals ({','.join(names)}) VALUES ({placeholders}) "
+                f"ON CONFLICT(signal_id) DO UPDATE SET {update_sql}",
+                values,
+            )
+            stored += 1
+
+        placeholders = ",".join("?" for _ in incoming_ids)
+        connection.execute("DELETE FROM signals WHERE source_type='staging_fixture'")
+        connection.execute(
+            f"DELETE FROM signals WHERE source_type='live_mirror' "
+            f"AND signal_id NOT IN ({placeholders})",
+            incoming_ids,
+        )
+
+    return {"received": len(payload), "stored": stored}
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Read-only live radar mirror for SMA staging")
+    parser.add_argument("--database", type=Path, default=DEFAULT_DATABASE)
+    parser.add_argument("--upstream", default=DEFAULT_UPSTREAM)
+    parser.add_argument("--days", type=int, default=14)
+    parser.add_argument("--limit", type=int, default=500)
+    parser.add_argument("--min-items", type=int, default=5)
+    args = parser.parse_args()
+
+    payload = fetch_live_signals(args.upstream, days=args.days, limit=args.limit)
+    if len(payload) < args.min_items:
+        raise RuntimeError(
+            f"live radar returned only {len(payload)} signals; minimum is {args.min_items}; "
+            "staging database left unchanged"
+        )
+    result = sync_payload(args.database, payload)
+    print(json.dumps({**result, "days": args.days, "upstream": args.upstream}, ensure_ascii=False))
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/tests/test_live_signal_sync.py b/tests/test_live_signal_sync.py
new file mode 100644
index 0000000..fd9937d
--- /dev/null
+++ b/tests/test_live_signal_sync.py
@@ -0,0 +1,108 @@
+import sqlite3
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+
+
+class LiveSignalSyncTests(unittest.TestCase):
+    def _bootstrap(self, data_dir: Path) -> None:
+        completed = subprocess.run(
+            [sys.executable, str(ROOT / "staging" / "bootstrap.py"), "--data-dir", str(data_dir)],
+            cwd=ROOT,
+            text=True,
+            capture_output=True,
+        )
+        self.assertEqual(completed.returncode, 0, completed.stderr)
+
+    def test_successful_sync_replaces_fixtures_with_live_signals(self):
+        from staging.sync_live_signals import sync_payload
+
+        with tempfile.TemporaryDirectory() as tmp:
+            data_dir = Path(tmp)
+            self._bootstrap(data_dir)
+            database = data_dir / "signals.db"
+            payload = [
+                {
+                    "signal_id": "live-1",
+                    "timestamp": "2026-08-20T07:30:00+00:00",
+                    "created_at": "2026-08-20T07:30:00+00:00",
+                    "expires_at": "2026-08-22T07:30:00+00:00",
+                    "source_name": "Mallorca Zeitung RSS",
+                    "source_url": "https://example.invalid/live-1",
+                    "language": "de",
+                    "signal_category": "wetter_klima",
+                    "topic": "Unwetterwarnung für Mallorca",
+                    "topic_de": "Unwetterwarnung für Mallorca",
+                    "short_summary": "Starker Regen auf Mallorca.",
+                    "radar_score": 73,
+                    "mallorca_relevance": 25,
+                    "topic_class": "kern",
+                    "status": "neu",
+                    "score_breakdown": {"final": 73},
+                    "extra_sources": [],
+                },
+                {
+                    "signal_id": "live-2",
+                    "timestamp": "2026-08-20T08:00:00+00:00",
+                    "created_at": "2026-08-20T08:00:00+00:00",
+                    "expires_at": "2026-08-22T08:00:00+00:00",
+                    "source_name": "Google News: Feuchte Immobilien Mallorca (DE)",
+                    "source_url": "https://example.invalid/live-2",
+                    "language": "de",
+                    "signal_category": "feuchte",
+                    "topic": "Feuchtigkeit in einer Mallorca-Wohnung",
+                    "topic_de": "Feuchtigkeit in einer Mallorca-Wohnung",
+                    "short_summary": "Feuchtigkeit in einer Ferienwohnung.",
+                    "radar_score": 81,
+                    "mallorca_relevance": 25,
+                    "topic_class": "kern",
+                    "status": "neu",
+                    "score_breakdown": {"final": 81},
+                    "extra_sources": [],
+                },
+            ]
+
+            result = sync_payload(database, payload)
+
+            with sqlite3.connect(database) as connection:
+                rows = connection.execute(
+                    "SELECT signal_id, source_type, topic FROM signals ORDER BY signal_id"
+                ).fetchall()
+
+        self.assertEqual(result["received"], 2)
+        self.assertEqual(result["stored"], 2)
+        self.assertEqual(
+            rows,
+            [
+                ("live-1", "live_mirror", "Unwetterwarnung für Mallorca"),
+                ("live-2", "live_mirror", "Feuchtigkeit in einer Mallorca-Wohnung"),
+            ],
+        )
+
+    def test_empty_upstream_does_not_delete_existing_staging_data(self):
+        from staging.sync_live_signals import sync_payload
+
+        with tempfile.TemporaryDirectory() as tmp:
+            data_dir = Path(tmp)
+            self._bootstrap(data_dir)
+            database = data_dir / "signals.db"
+            with sqlite3.connect(database) as connection:
+                before = connection.execute("SELECT COUNT(*) FROM signals").fetchone()[0]
+
+            with self.assertRaisesRegex(ValueError, "returned no signals"):
+                sync_payload(database, [])
+
+            with sqlite3.connect(database) as connection:
+                after = connection.execute("SELECT COUNT(*) FROM signals").fetchone()[0]
+
+        self.assertEqual(before, 3)
+        self.assertEqual(after, before)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_visual_photo_pipeline.py b/tests/test_visual_photo_pipeline.py
new file mode 100644
index 0000000..f7ea6c4
--- /dev/null
+++ b/tests/test_visual_photo_pipeline.py
@@ -0,0 +1,97 @@
+import os
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+AGENT = ROOT / "agent"
+RADAR = ROOT / "radar"
+sys.path.insert(0, str(AGENT))
+sys.path.insert(0, str(RADAR))
+
+from visual_gate import can_generate_visual
+
+
+class VisualPhotoPipelineTests(unittest.TestCase):
+    def test_feuchte_signals_require_a_photo_visual(self):
+        signal = {
+            "signal_id": "photo-test-1",
+            "signal_category": "feuchte",
+            "briefing_freigabe": "geeignet",
+            "briefing_winkel": "Kondensation am Fenster früh erkennen",
+            "mallorca_relevance": 25,
+            "radar_score": 88,
+        }
+
+        result = can_generate_visual(signal)
+
+        self.assertTrue(result["allowed"])
+        self.assertEqual(result["visual_type"], "photo")
+        self.assertIn("photo", result["reason"])
+
+    def test_photo_asset_is_embedded_as_data_uri(self):
+        import visual_render
+
+        with tempfile.TemporaryDirectory() as tmp:
+            photo_dir = Path(tmp)
+            (photo_dir / "photo-te.jpg").write_bytes(b"\xff\xd8\xff\xe0test-photo")
+            signal = {"signal_id": "photo-test-1"}
+
+            data_uri = visual_render.load_photo_data_uri(signal, photo_dir)
+
+        self.assertTrue(data_uri.startswith("data:image/jpeg;base64,"))
+        self.assertNotIn("test-photo", data_uri)
+
+    def test_render_is_blocked_when_no_real_photo_is_selected(self):
+        import visual_render
+
+        signal = {
+            "signal_id": "missing-photo-1",
+            "signal_category": "feuchte",
+            "briefing_freigabe": "geeignet",
+            "briefing_winkel": "Kondensation am Fenster früh erkennen",
+            "briefing_nutzen": "Feuchteprobleme frühzeitig erkennen",
+            "mallorca_relevance": 25,
+            "radar_score": 88,
+        }
+        with tempfile.TemporaryDirectory() as tmp:
+            original = visual_render.PHOTO_DIR
+            visual_render.PHOTO_DIR = Path(tmp)
+            try:
+                result = visual_render.render_signal(signal, "1x1")
+            finally:
+                visual_render.PHOTO_DIR = original
+
+        self.assertFalse(result["success"])
+        self.assertIsNone(result["path"])
+        self.assertIn("Fotorealistisches Bild fehlt", result["reason"])
+
+    def test_feuchte_copy_is_factual_and_not_alarmist(self):
+        import visual_render
+
+        signal = {
+            "signal_category": "feuchte",
+            "topic_de": "Hohe Luftfeuchtigkeit auf Mallorca: Kondensation in leerstehender Ferienwohnung",
+            "briefing_winkel": "Feuchteprobleme in Mallorca-Wohnungen: Ursachen und Lösung",
+            "briefing_nutzen": "Eigentümer erkennt Feuchteprobleme frühzeitig",
+        }
+        gate = {"category_class": "cat-feuchte"}
+
+        context = visual_render.build_context(signal, "4x5", gate)
+
+        self.assertEqual(context["cat_label"], "Feuchtigkeit im Leerstand")
+        self.assertEqual(context["hook_line"], "Kondensation\nfrüh erkennen.")
+        self.assertNotIn("Schimmel kommt", " ".join(map(str, context.values())))
+
+    def test_templates_use_a_real_photo_and_no_svg_scene(self):
+        for name in ("visual_square.html", "visual_45.html"):
+            template = (AGENT / "templates" / name).read_text(encoding="utf-8")
+            self.assertIn("{{ photo_data_uri }}", template)
+            self.assertIn("object-fit: cover", template)
+            self.assertNotIn("<svg", template)
+            self.assertNotIn("feTurbulence", template)
+
+
+if __name__ == "__main__":
+    unittest.main()
