{
  "id": "Rmv4Ht895SiIgUOC",
  "name": "Gemini Mockup Generator v1.1",
  "active": true,
  "isArchived": false,
  "tags": [],
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Berlin",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "executionTimeout": 600,
    "errorWorkflow": "2gTu1lSGwsONtPSH",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "createdAt": "2026-04-16T12:16:09.400Z",
  "updatedAt": "2026-07-21T16:53:37.862Z",
  "activeVersionId": "e4cba1cf-9988-48d7-a26f-7fff19c13fc5",
  "versionCreatedAt": "2026-07-21T16:53:37.868Z",
  "versionName": null,
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      },
      "id": "n01",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        1080
      ]
    },
    {
      "parameters": {
        "url": "https://api.trello.com/1/lists/659ffc4e5f8bffd67fe38265/cards",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "customFieldItems",
              "value": "true"
            },
            {
              "name": "attachments",
              "value": "true"
            },
            {
              "name": "attachment_fields",
              "value": "id,name,mimeType,date,url"
            },
            {
              "name": "fields",
              "value": "id,name,url,labels,desc,idBoard"
            }
          ]
        },
        "options": {}
      },
      "id": "n02",
      "name": "Get Quote Ready Cards",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        224,
        1080
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 10000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const cards = $input.all().map(item => item.json);\n\nconst PROCESSING_LABEL_ID = '69e72e4817206131923d4fcc';\nconst UPLOADED_LABEL_ID = '69eb6348d13470a412a6ab9b';\nconst MANUAL_REVIEW_LABEL_ID = '69ecda6ec3c70150828ccc15';\nconst DESIGN_ISSUE_LABEL_ID = '69ecfa31a36fbbc35b69e217';\nconst QC_VERIFIED_LABEL_ID = '69471d255442a0100e28a93f';\nconst QC_FAILED_LABEL_ID = '69471d337a802712e1c8d56c';\nconst MAX_RETRIES = 3;\nconst SOURCE_MOCKUP_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst UPDATE_SOURCE_BATCH_MAX_AGE_MS = 30 * 60 * 1000;\nconst SOURCE_MOCKUP_CLOCK_SKEW_MS = 5 * 60 * 1000;\n\nfunction attachmentCreatedAtMs(attachment) {\n  const explicitDate = Date.parse(String((attachment && attachment.date) || ''));\n  if (Number.isFinite(explicitDate)) return explicitDate;\n  const id = String((attachment && attachment.id) || '');\n  if (!/^[0-9a-f]{24}$/i.test(id)) return null;\n  const objectIdTimestamp = parseInt(id.slice(0, 8), 16) * 1000;\n  return Number.isFinite(objectIdTimestamp) ? objectIdTimestamp : null;\n}\n\nfunction isSourceMockupInLatestBatch(attachment, newestSourceCreatedAtMs, maxAgeMs) {\n  const createdAtMs = attachmentCreatedAtMs(attachment);\n  const effectiveMaxAgeMs = Number.isFinite(maxAgeMs) ? maxAgeMs : SOURCE_MOCKUP_MAX_AGE_MS;\n  if (!Number.isFinite(createdAtMs) || !Number.isFinite(newestSourceCreatedAtMs)) return false;\n  return createdAtMs >= newestSourceCreatedAtMs - effectiveMaxAgeMs\n    && createdAtMs <= newestSourceCreatedAtMs + SOURCE_MOCKUP_CLOCK_SKEW_MS;\n}\nconst MAX_ACTIVE_PROCESSING_CARDS = 10;\nconst GENERATED_REGEX = /^Mockup[\\d_.]+_ai_\\d+\\.jpe?g$/i;\nconst TABLETOP_REGEX = /^(?:Tabletop|Tischgeraet|Tischger\u00e4t)(?:\\d+)?(?:_ai_\\d+)?\\.(?:jpe?g|png|webp)$/i;\nconst BROKEN_REGEX    = /^Mockup(undefined|NaN|null)\\.jpg$/i;\n// Source-Mockups vom Browser: mockup3969.jpg, mockup_1234.jpeg, mockup_0512.1916.png (mit Punkten/Underscores)\nconst SOURCE_REGEX    = /^mockup[\\d_.]+\\.(?:jpe?g|png|webp)$/i;\nconst MODERN_3D_SOURCE_VISUALIZATION_REGEX = /^mockup_\\d{4}_\\d{4}\\.jpe?g$/i;\nconst SINGLE_SOURCE_MOCKUPS = 3;\nconst MULTI_SOURCE_MOCKUPS_PER_SOURCE = 2;\nconst DEFAULT_MAX_SOURCE_MOCKUPS = 4;\nconst THREE_D_MAX_SOURCE_MOCKUPS = 12;\nconst LARGE_REQUEST_SOURCE_THRESHOLD = 4;\nconst LARGE_REQUEST_MAX_SOURCE_MOCKUPS = 40;\nconst MAX_PER_RUN = MULTI_SOURCE_MOCKUPS_PER_SOURCE * LARGE_REQUEST_MAX_SOURCE_MOCKUPS;\nconst HARD_LIMIT_TOTAL = MAX_PER_RUN;\nconst BACKBOARD_FIELD_IDS = {\n  1: '67ab333359b643271da233e1',\n  2: '67ab333c476749520e9e31ec',\n  3: '67f4fac25f71afb659744bce',\n  4: '67f4fb1601e710fe13ee8f23',\n};\nconst LIGHT_COLOR_FIELD_IDS = {\n  1: '67989cac3cb7e28e19d3dbfe',\n  2: '67989cb3c85879336601b578',\n  3: '67f4fab7590a76e3d66734ff',\n  4: '67f4fb0c593bcdebe2a99775',\n};\n\nfunction generatedSlotNumber(name) {\n  const m = String(name || '').match(/^Mockup[\\d_.]+_ai_(\\d+)\\.jpe?g$/i);\n  return m ? parseInt(m[1], 10) : null;\n}\n\nfunction isTargetGeneratedMockup(attachment) {\n  return GENERATED_REGEX.test(String((attachment && attachment.name) || ''));\n}\n\nfunction extractSourceNum(name) {\n  const m = (name || '').match(/^mockup([\\d_.]+)\\.(?:jpe?g|png|webp)$/i);\n  if (!m) return -1;\n  const digits = m[1].replace(/[^\\d]/g, '');\n  return digits ? parseInt(digits, 10) : -1;\n}\n\nfunction sourceBaseName(name) {\n  const raw = String(name || '').trim().replace(/\\.[^.]+$/i, '');\n  const match = raw.match(/^mockup([\\d_.]+)$/i);\n  if (!match) return 'Mockup' + Date.now();\n  return 'Mockup' + match[1].replace(/[^\\d_.]/g, '');\n}\n\nfunction aiMockupFileName(sourceName, variantSlot) {\n  return sourceBaseName(sourceName) + '_ai_' + String(variantSlot) + '.jpg';\n}\n\nfunction aiMockupPublicUrl(requestId, fileName) {\n  return 'https://klibiejfisijpagzkxls.supabase.co/storage/v1/object/public/pandadoc-mockups/' + encodeURIComponent(requestId || '') + '/' + encodeURIComponent(fileName);\n}\n\nfunction aiMockupNameParts(name) {\n  // Must accept every base emitted by sourceBaseName(), including dot/underscore Trello source names.\n  const m = String(name || '').match(/^(Mockup[\\d_.]+)_ai_(\\d+)\\.(?:jpe?g|png|webp)$/i);\n  if (!m) return null;\n  const index = parseInt(m[2], 10);\n  if (!Number.isFinite(index) || index < 1) return null;\n  return { base: m[1], baseKey: m[1].toLowerCase(), index };\n}\n\nfunction aiMockupNumbersBySourceBase(attachments) {\n  const map = new Map();\n  for (const attachment of attachments || []) {\n    const parts = aiMockupNameParts(attachment && attachment.name);\n    if (!parts) continue;\n    if (!map.has(parts.baseKey)) map.set(parts.baseKey, new Set());\n    map.get(parts.baseKey).add(parts.index);\n  }\n  return map;\n}\n\nfunction nextAiMockupFileNameForSource(sourceName, usedByBase, reservedByBase) {\n  const base = sourceBaseName(sourceName);\n  const baseKey = base.toLowerCase();\n  const used = usedByBase.get(baseKey) || new Set();\n  const reserved = reservedByBase.get(baseKey) || new Set();\n  let next = 1;\n  while (used.has(next) || reserved.has(next)) next++;\n  reserved.add(next);\n  reservedByBase.set(baseKey, reserved);\n  return base + '_ai_' + String(next) + '.jpg';\n}\n\nfunction canonicalSignType(text) {\n  const lower = (text || '').toLowerCase();\n  // NEONTRIP PRODUCT CONTRACT v1: an explicit non-lit title must win over generic neon wording elsewhere.\n  if (/\\b(?:3d\\s*)?(?:non[-\\s]?lit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(?:beleuchtung|licht))\\b/.test(lower)) return '3D Non-Lit';\n  if (/\\b3d\\s*frontlit\\b/.test(lower)) return '3D Frontlit';\n  if (/\\b3d\\s*backlit\\b/.test(lower)) return '3D Backlit';\n  if (/\\b3d\\s*halo\\b/.test(lower)) return '3D Halo';\n  if (/\\b3d\\b/.test(lower)) return '3D Leuchtbuchstaben';\n  if (/\\bhalo\\s*lit\\b/.test(lower) || /\\bhalo\\b/.test(lower)) return 'Halo Lit';\n  if (/\\bdouble\\s*sided\\b/.test(lower) || /\\bnasenschild\\b/.test(lower)) return 'Double Sided Lightbox';\n  if (/\\blight\\s*box\\b/.test(lower) || /\\blightbox\\b/.test(lower) || /\\bleuchtkasten\\b/.test(lower)) return 'Lightbox';\n  if (/\\bacrylic\\b/.test(lower)) return 'Acrylic LED';\n  if (/\\bfull\\s*glow\\b/.test(lower)) return 'Full Glow LED Flex';\n  if (/\\bfront\\s*glow\\b/.test(lower)) return 'Front Glow LED Flex';\n  if (/\\bback\\s*glow\\b/.test(lower)) return 'Back Glow LED Flex';\n  if (/\\bled\\s*flex\\b/.test(lower) || /\\bneon\\s*flex\\b/.test(lower)) return 'LED Flex';\n  if (/\\bled\\b/.test(lower) || /\\bneon\\b/.test(lower)) return 'LED Neon';\n  return null;\n}\n\nfunction extractModifiers(text) {\n  const lower = (text || '').toLowerCase();\n  const mods = [];\n  if (/\\buv[-\\s]?print\\b/.test(lower)) mods.push('UV_PRINT');\n  if (/\\bcut\\s*to\\s*shape\\b/.test(lower)) mods.push('CUT_TO_SHAPE');\n  if (/\\bloose\\s*letters?\\b/.test(lower) || /\\blose\\s*buchstaben\\b/.test(lower)) mods.push('LOOSE_LETTERS');\n  if (/\\bon\\s*backboard\\b/.test(lower) || /\\bauf\\s*backboard\\b/.test(lower)) mods.push('BACKBOARD');\n  return mods;\n}\n\nfunction customFieldTextById(customFields, id) {\n  const field = (customFields || []).find(item => item && item.idCustomField === id);\n  return field && field.value ? String(field.value.text || field.value.name || field.value.value || '').trim() : '';\n}\n\nfunction customFieldTextValues(customFields) {\n  return (customFields || [])\n    .map(field => field && field.value ? (field.value.text || field.value.name || field.value.value || '') : '')\n    .map(value => String(value || '').trim())\n    .filter(Boolean);\n}\n\nfunction normalizeColorText(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00df/g, 'ss')\n    .replace(/[_-]+/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nfunction colorFromTitle(value) {\n  const text = normalizeColorText(value);\n  if (/\\b(kaltweiss|cool white|cold white|6000\\s*k)\\b/i.test(text)) return 'Kaltwei\u00df';\n  if (/\\b(warmweiss|warm white|3000\\s*k)\\b/i.test(text)) return 'Warmwei\u00df';\n  if (/\\b(goldgelb|golden yellow)\\b/i.test(text)) return 'Goldgelb';\n  if (/\\b(zitronengelb|lemon yellow)\\b/i.test(text)) return 'Zitronengelb';\n  if (/\\b(gelb|yellow)\\b/i.test(text)) return 'Gelb';\n  if (/\\b(color|colour|farbe)\\s+as\\s+(logo|design)\\b/i.test(text)) return 'Wie im Logo';\n  return '';\n}\n\nfunction buildColorLockPromptBlock(value, signType) {\n  const label = String(value || '').trim();\n  if (!label) return '';\n  const color = normalizeColorText(label);\n  const product = normalizeColorText(signType);\n  const common = [\n    'COLOR LOCK - AUTHORITATIVE TRELLO VALUE: ' + label + '.',\n    'This explicit Trello color value overrides any color inferred from the source image, wall, ambient light, reflections, camera white balance or artistic color grading.',\n    'Keep this exact sign color stable in every generated view. Never shift its hue or color temperature because of the surrounding scene.',\n    'Use neutral photographic white balance so warm room lighting or sunset light does not recolor the sign.'\n  ];\n  if (/non\\s*lit|nonlit|unbeleuchtet/.test(product)) {\n    return common.concat([\n      'This product is non-illuminated: apply the requested value only as the opaque surface/paint color.',\n      'No LEDs, no glow, no halo and no emitted light.'\n    ]).join('\\n');\n  }\n  if (/kaltweiss|cool white|cold white|6000\\s*k/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as KALTWEISS / COOL WHITE at approximately 6000 Kelvin: crisp neutral-to-cool white.',\n      'Forbidden: warm white, cream, beige, yellow, amber, orange or golden light. Do not add a blue or cyan tint either.'\n    ]).join('\\n');\n  }\n  if (/warmweiss|warm white|3000\\s*k/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as WARMWEISS / WARM WHITE at approximately 3000 Kelvin: a controlled warm white light.',\n      'Forbidden: cool white, icy white, blue, cyan or neutral 6000K light. It must remain white light, not saturated yellow or orange.'\n    ]).join('\\n');\n  }\n  if (/zitronengelb|lemon yellow/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated bright LEMON YELLOW.',\n      'Forbidden: warm white, cream, beige, white, amber or orange. Yellow must remain visibly yellow.'\n    ]).join('\\n');\n  }\n  if (/goldgelb|golden yellow/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated GOLDEN YELLOW.',\n      'Forbidden: warm white, cream, beige, plain white or orange. Golden yellow must remain visibly yellow, not white light.'\n    ]).join('\\n');\n  }\n  if (/\\bgelb\\b|\\byellow\\b/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated YELLOW.',\n      'Forbidden: warm white, cream, beige, plain white, amber or orange. Yellow must remain visibly yellow.'\n    ]).join('\\n');\n  }\n  if (/wie im logo|wie im design|as logo|as design|color as|colour as|farbe wie/.test(color)) {\n    return common.concat([\n      'Preserve the exact single- or multi-color mapping from the supplied logo/design.',\n      'Each original colored element must keep its own hue and position; never collapse yellow into warm white or swap warm and cool white.'\n    ]).join('\\n');\n  }\n  return common.concat([\n    'Render the illuminated parts exactly in the requested color: ' + label + '.',\n    'For multi-color values, preserve every listed color and its original element-to-color mapping; do not merge or substitute colors.'\n  ]).join('\\n');\n}\n\nfunction normalizeBackboardText(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00df/g, 'ss')\n    .trim();\n}\n\nfunction extractBackboardValue(card, customFields) {\n  const values = customFieldTextValues(customFields);\n  const explicit = values.find(value => /einzelne\\s+buchstaben|loose\\s+letters?|individual\\s+letters?|ohne\\s+(?:rueckplatte|ruckplatte|backboard|platte)|no\\s+(?:backboard|backing\\s*plate|backplate)|feinschnitt|formzuschnitt|rechteck|rectang|square|quadratisch|viereck|kontur|contour|cut\\s*to\\s*shape|cut-to-shape|shape\\s*cut|rueckplatte|ruckplatte|backboard|backing\\s*plate|backplate|platte/i.test(value));\n  if (explicit) return explicit;\n  const desc = String((card && card.desc) || '');\n  const match = desc.match(/(?:Rueckplatte|R\u00fcckplatte|Backboard|Zuschnitt)\\s*:\\s*([^\\n]+)/i);\n  return match ? String(match[1] || '').trim() : '';\n}\n\nfunction buildBackboardPromptBlock(backboardValue) {\n  const normalized = normalizeBackboardText(backboardValue);\n  if (/uv[-\\s]?print|uv\\s*druck|mit\\s+uv\\s*druck/i.test(normalized)) {\n    return 'NEONTRIP UV-PRINT BACKBOARD CONTRACT v2 - AUTHORITATIVE:\\n'\n      + 'The explicit UV-Print request overrides every generic clear/transparent acrylic rule.\\n'\n      + 'Preserve the exact source backboard silhouette, material, opacity and color.\\n'\n      + 'The UV-printed backing is one continuous solid opaque panel, never transparent or clear acrylic.\\n'\n      + 'If the source panel is black, keep the complete area inside its outer contour solid opaque black; never show the wall, window or scene through it.\\n'\n      + 'Only separate LED/neon lines mounted above the UV print may glow. The print and backing panel stay flat and non-illuminated.';\n  }\n  if (!normalized || /einzelne\\s+buchstaben|loose\\s+letters?|individual\\s+letters?|ohne\\s+(?:ruckplatte|rueckplatte|backboard|platte)|no\\s+(?:backboard|backing\\s*plate|backplate)/i.test(normalized)) {\n    return '3D BACKBOARD / RUECKPLATTE - EINZELNE BUCHSTABEN (DEFAULT):\\n'\n      + 'For 3D frontlit, backlit, halo and non-lit signs the default is individual loose letters without any backing plate.\\n'\n      + 'If Backboard is empty or says Einzelne Buchstaben / loose letters, render separate stainless steel/acrylic letters mounted directly to the wall.\\n'\n      + 'Do not render a rectangular plate, contour-cut acrylic panel, shared transparent sheet, white panel or any visible backboard.\\n'\n      + 'The wall must remain visible in the gaps between all letters and logo elements; only discreet hidden mounting or tiny standoffs are allowed.';\n  }\n  if (/feinschnitt|fine\\s*cut|minimal\\s*(?:acryl|acrylic)/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - FEINSCHNITT:\\n'\n      + 'Do not show a large acrylic backing plate.\\n'\n      + 'Do not show a contour-cut acrylic board around the full logo.\\n'\n      + 'The neon tubes should have only minimal transparent acrylic directly behind the tubes.\\n'\n      + 'If separate letters or logo parts need support, connect them with very thin, nearly invisible transparent acrylic bridges.\\n'\n      + 'The result must still be one manufacturable sign, but with as little visible acrylic as technically possible.';\n  }\n  if (/rechteck|rectang|square|quadratisch|viereck/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - RECTANGULAR ACRYLIC:\\n'\n      + 'Show the complete design mounted on one clear rectangular or square acrylic glass plate.\\n'\n      + 'Use straight clean edges and proportions that fit the design.\\n'\n      + 'Do not make the acrylic backing follow the logo contour.';\n  }\n  if (/formzuschnitt|form\\s*zuschnitt|kontur|contour|cut\\s*to\\s*shape|shape\\s*cut|cut-to-shape/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - FORMZUSCHNITT / CONTOUR CUT:\\n'\n      + 'Show a transparent acrylic backing plate that roughly follows the outside contour of the logo.\\n'\n      + 'The contour-cut acrylic may be visible, but it must look clean, premium and intentionally shaped.\\n'\n      + 'Do not turn this into a large rectangular plate unless the request explicitly says rectangular backing.';\n  }\n  return '';\n}\n\nfunction extractDesignText(name) {\n  const text = String(name || '');\n  const matches = [...text.matchAll(/\u00b7\\s*Design:\\s*([^\u00b7]+)/giu)]\n    .map(match => String(match[1] || '').trim())\n    .filter(Boolean);\n  return matches.length ? matches[matches.length - 1].slice(0, 300) : '';\n}\n\nfunction stripTitlePrefixes(name) {\n  let cur = name || '';\n  let prev = null;\n  while (cur !== prev) {\n    prev = cur;\n    // Step 1: ALLE trailing \"\u00b7 Design: \u2026\" Suffixe ZUERST entfernen (auch Doppel-Append).\n    cur = cur.replace(/\\s*\u00b7\\s*Design:\\s*[^\u00b7]+$/u, '').replace(/\\s+$/, '');\n    // Step 2: Leading Status-Marker\n    cur = cur.replace(/^[\\s\\u{1F501}\\u{2705}\\u{FE0F}]+/u, '');\n    cur = cur.replace(/^\\s*AI MOCKUP FEHLER\\s*(\\[\\d+\\/\\d+\\])?[^\u00b7\\-]*[\u00b7\\-]\\s*/i, '');\n    cur = cur.replace(/^\\s*\u274c\\s*(\\[\\d+\\/\\d+\\])?[^\u00b7]*\u00b7\\s*/, '');\n    cur = cur.replace(/^\\s*\u23f8\ufe0f[^\u00b7]*\u00b7\\s*/, '');\n    cur = cur.replace(/^\\s*\u26a0\ufe0f\\s*MOCKUP\\s*PR\u00dcFEN[^\u00b7]*\u00b7\\s*/i, '');\n    cur = cur.replace(/^\\s*\ud83c\udfa8\\s*DESIGN\\s*ABWEICHUNG[^\u00b7]*\u00b7\\s*/i, '');\n  }\n  return cur;\n}\n\nconst activeProcessingCards = cards.filter(card =>\n  (card.labels || []).some(label => label.id === PROCESSING_LABEL_ID)\n);\nif (activeProcessingCards.length >= MAX_ACTIVE_PROCESSING_CARDS) {\n  return [];\n}\n\nconst validCards = [];\nlet firstInvalid = null;\n\nfor (const card of cards) {\n  const labels = card.labels || [];\n  const labelIds = labels.map(l => l.id);\n  const labelNames = labels.map(l => (l.name || '').toLowerCase());\n  const hasRetryLabel = labelNames.includes('retry') || labelNames.some(n => n.includes('video generieren'));\n\n  const attachments = card.attachments || [];\n  const cardContext = [card.name, card.desc].filter(Boolean).join(' ').toLowerCase();\n  const twoSourceMockupProduct = /\\b3d\\b|ultra\\s*thin|ultrathin|double\\s*sided|nasenschild|light\\s*box|lightbox|leuchtkasten|acrylic|full\\s*glow|front\\s*glow|back\\s*glow|non\\s*lit|nonlit|unbeleuchtet/.test(cardContext);\n  const modernSourceVisualizationCount = attachments.filter(a =>\n    a.name && a.mimeType && a.mimeType.startsWith('image/') &&\n    MODERN_3D_SOURCE_VISUALIZATION_REGEX.test(String(a.name))\n  ).length;\n\n  // 3D/Ultra-Thin/Non-Lit cards intentionally have two source visualizations per design.\n  // They still need AI mockups; do not skip them just because the two source images exist.\n  // Existing generated attachments are filtered later so re-entry does not duplicate completed slots.\n\n  const TABLETOP_TARGET_COUNT = 1;\n  const existingTabletopMockups = attachments.filter(a => a.name && TABLETOP_REGEX.test(a.name));\n  const existingTabletopNames = new Set(existingTabletopMockups.map(a => String(a.name || '').toLowerCase()));\n  const missingTabletopNumbers = [];\n  for (let tabletopNumber = 1; tabletopNumber <= TABLETOP_TARGET_COUNT; tabletopNumber++) {\n    const expectedName = 'Tabletop' + String(tabletopNumber).padStart(2, '0') + '.png';\n    if (!existingTabletopNames.has(expectedName.toLowerCase())) missingTabletopNumbers.push(tabletopNumber);\n  }\n  const needsTabletopMockup = missingTabletopNumbers.length > 0;\n  const hasMockupUploadedLabel = labelIds.includes(UPLOADED_LABEL_ID);\n\n  // Only the explicit \"Mockup Uploaded\" label is terminal for mockup generation.\n  // Other labels such as manual review, design issue or QC labels must not block Quote Ready mockups.\n  // To rerun a completed card, remove \"Mockup Uploaded\" first.\n  if (labelIds.includes(PROCESSING_LABEL_ID)) continue;\n  if (hasMockupUploadedLabel) continue;\n\n  if (attachments.some(a => a.name && BROKEN_REGEX.test(a.name))) {\n    if (!firstInvalid) {\n      firstInvalid = { json: {\n        valid: false, needsAI: false,\n        cardId: card.id, cardName: card.name, cardUrl: card.url,\n        errorReason: 'Mockupundefined.jpg gefunden \u2014 manuell aufr\u00e4umen n\u00f6tig'\n      } };\n    }\n    continue;\n  }\n\n  const allMockupFiles = attachments.filter(a =>\n    a.name && a.mimeType && a.mimeType.startsWith('image/') && SOURCE_REGEX.test(a.name)\n  );\n  const isUpdateCard = /\\bupdate\\b/i.test(String(card.name || ''));\n  const sourceBatchMaxAgeMs = isUpdateCard\n    ? UPDATE_SOURCE_BATCH_MAX_AGE_MS\n    : SOURCE_MOCKUP_MAX_AGE_MS;\n  const sourceTimestamps = allMockupFiles\n    .map(attachment => attachmentCreatedAtMs(attachment))\n    .filter(value => Number.isFinite(value));\n  const newestSourceCreatedAtMs = sourceTimestamps.length ? Math.max(...sourceTimestamps) : null;\n  const sourceBatchMockups = Number.isFinite(newestSourceCreatedAtMs)\n    ? allMockupFiles.filter(attachment =>\n        isSourceMockupInLatestBatch(attachment, newestSourceCreatedAtMs, sourceBatchMaxAgeMs)\n      )\n    : [];\n  const sourceBatchIds = new Set(sourceBatchMockups.map(attachment => attachment.id));\n  const staleSourceMockups = allMockupFiles.filter(attachment => !sourceBatchIds.has(attachment.id));\n  const totalMockupFiles = sourceBatchMockups.length;\n  const largeRequestCandidate = totalMockupFiles > LARGE_REQUEST_SOURCE_THRESHOLD;\n\n  const generatedMockups = attachments.filter(a => a.name && GENERATED_REGEX.test(a.name));\n  const existingAiNumbersByBase = aiMockupNumbersBySourceBase(generatedMockups);\n  const reservedAiNumbersByBase = new Map();\n  function existingAiCountForSource(sourceName) {\n    const baseKey = sourceBaseName(sourceName).toLowerCase();\n    return (existingAiNumbersByBase.get(baseKey) || new Set()).size;\n  }\n  let sourceMockups = sourceBatchMockups\n    .filter(a => hasRetryLabel || !GENERATED_REGEX.test(a.name))\n    .sort((a, b) => {\n      const diff = extractSourceNum(a.name) - extractSourceNum(b.name);\n      if (diff !== 0) return diff;\n      return (b.id || '').localeCompare(a.id || '');\n    });\n\n  const titleRaw = card.name || '';\n  const designText = extractDesignText(titleRaw);\n  const titleStripped = titleRaw.replace(/^[\\s\\u{1F501}\\u{2705}\\u{FE0F}]+/u, '');\n  const hasErrorMarker = titleStripped.includes('AI MOCKUP FEHLER') || /^\\s*\u274c/.test(titleStripped);\n  const hasWaitingMarker = /^\\s*\u23f8\ufe0f/.test(titleStripped);\n\n  const retryMatch = titleRaw.match(/\\[(\\d+)\\/\\d+\\]/);\n  const retryCount = retryMatch ? parseInt(retryMatch[1], 10) : (hasErrorMarker ? 1 : 0);\n  if (!hasRetryLabel && retryCount >= MAX_RETRIES) continue;\n\n  if ((hasErrorMarker || hasWaitingMarker) && !hasRetryLabel) continue;\n\n  let cardName = stripTitlePrefixes(titleRaw);\n  cardName = cardName.replace(/[\\u{1F501}]\\s*/gu, '').trim();\n  if (!cardName) cardName = titleRaw;\n\n  const cardDesc = card.desc || '';\n\n  let signType = canonicalSignType(cardName) || 'LED Neon Sign';\n  const modifiers = extractModifiers(cardName);\n\n  const promptHints = [];\n  if (signType === '3D Non-Lit') promptHints.push(\"NON-LIT / UNBELEUCHTET - HIGHEST PRIORITY PRODUCT CONTRACT: This sign has no lighting components and must remain completely unilluminated in every generated variation. No LED or neon-tube appearance, no emitted light, no halo, no glow, no backlighting and no illuminated face. Show only solid 3D material, natural daylight reflections and physically plausible contact shadows. Even if the source rendering looks bright, never interpret brightness as illumination.\");\n  if (modifiers.includes('UV_PRINT')) promptHints.push(\"NEONTRIP UV-PRINT BACKBOARD CONTRACT v2 - HIGHEST PRIORITY: The explicit UV-Print request overrides every generic transparent-acrylic or cut-to-shape rule that appears anywhere else in the prompt. Preserve the exact backboard silhouette, material, opacity and color visible in the source mockup. The UV-printed backing is one continuous solid opaque panel, not clear acrylic and not an open frame. If the source backboard is black, every pixel inside its visible outer contour that is black in the source must remain solid opaque black in every generated view; the wall or scene must never be visible through that black area. Only the separate LED/neon lines mounted above the print may emit light. UV-printed artwork and the black panel remain flat, non-transparent and non-illuminated.\");\n  if (modifiers.includes('CUT_TO_SHAPE')) promptHints.push('CUT-TO-SHAPE: Das Schild ist entlang der Design-Kontur ausgeschnitten (keine rechteckige Platte, die Aussenkontur folgt dem Design). Behalte die Schnittkante wie im Originalbild.');\n  if (modifiers.includes('LOOSE_LETTERS')) promptHints.push('LOOSE LETTERS: Einzelne Buchstaben, keine Backboard/Platte dahinter. Zwischen den Buchstaben sieht man die Wand. Abst\u00e4nde wie im Originalbild erhalten.');\n  if (modifiers.includes('BACKBOARD')) promptHints.push('BACKBOARD: Das Schild hat eine sichtbare R\u00fcckplatte / Tr\u00e4gerplatte. Form und Gr\u00f6sse wie im Originalbild.');\n\n  if (sourceMockups.length === 0) continue;\n\n  const isLargeRequest = sourceMockups.length > LARGE_REQUEST_SOURCE_THRESHOLD;\n  if (isLargeRequest && sourceMockups.length > LARGE_REQUEST_MAX_SOURCE_MOCKUPS) {\n    if (!firstInvalid) firstInvalid = { json: {\n      valid: false, needsAI: false,\n      cardId: card.id, cardName, cardUrl: card.url,\n      errorReason: sourceMockups.length + ' Ausgangsmockups erkannt; Sicherheitsgrenze ist ' + LARGE_REQUEST_MAX_SOURCE_MOCKUPS + '. Bitte Sonderfall pr\u00fcfen.'\n    } };\n    continue;\n  }\n  const maxSourceMockupsForCard = isLargeRequest\n    ? LARGE_REQUEST_MAX_SOURCE_MOCKUPS\n    : (twoSourceMockupProduct ? THREE_D_MAX_SOURCE_MOCKUPS : DEFAULT_MAX_SOURCE_MOCKUPS);\n  const selectedSources = sourceMockups.slice(0, maxSourceMockupsForCard);\n  const isStandardNeonFlex = !twoSourceMockupProduct && /(?:LED Flex|LED Neon|Front Glow LED Flex|Back Glow LED Flex)/i.test(signType) && !/full\\s*glow/i.test(cardContext);\n  const mockupsPerSelectedSource = isLargeRequest\n    ? MULTI_SOURCE_MOCKUPS_PER_SOURCE\n    : isStandardNeonFlex\n      ? 4\n      : selectedSources.length > 1\n        ? MULTI_SOURCE_MOCKUPS_PER_SOURCE\n        : SINGLE_SOURCE_MOCKUPS;\n  const currentTotal = generatedMockups.length;\n  const slotsToProcess = [];\n  let globalSlot = 1;\n\n  for (let sourceIndex = 0; sourceIndex < selectedSources.length; sourceIndex++) {\n    const source = selectedSources[sourceIndex];\n    const existingForSource = existingAiCountForSource(source.name);\n    const variantsToCreate = Math.max(0, mockupsPerSelectedSource - existingForSource);\n    for (let variantOffset = 0; variantOffset < variantsToCreate; variantOffset++) {\n      const variantSlot = (variantOffset % mockupsPerSelectedSource) + 1;\n      slotsToProcess.push({\n        slot: globalSlot++,\n        variantSlot,\n        sourceIndex: sourceIndex + 1,\n        linkedItemIndex: Math.floor(sourceIndex / (twoSourceMockupProduct ? 2 : 1)),\n        linkedItemTitle: 'Leuchtschild Design ' + String(Math.floor(sourceIndex / (twoSourceMockupProduct ? 2 : 1)) + 1),\n        source\n      });\n    }\n  }\n\n  const numToGenerate = slotsToProcess.length;\n  const finalizationOnly = slotsToProcess.length === 0 && !needsTabletopMockup;\n  if (finalizationOnly) {\n    validCards.push({ json: {\n      valid: true,\n      needsAI: false,\n      finalizationOnly: true,\n      cardId: card.id,\n      boardId: card.idBoard,\n      cardName: card.name,\n      cardUrl: card.url,\n      generatedMockupCount: generatedMockups.length,\n      existingTabletopMockupCount: existingTabletopMockups.length\n    } });\n    continue;\n  }\n\n  const ioMatch = cardDesc.match(/Indoor\\/Outdoor:\\s*(Indoor|Outdoor)/i);\n  let parsedUsage = '';\n  if (ioMatch) parsedUsage = ioMatch[1].toLowerCase() === 'indoor' ? 'Innen' : 'Au\u00dfen';\n\n  const customFields = card.customFieldItems || [];\n  const requestIdField = customFields.find(f => f.idCustomField === '67a200bd23b1ffd304e63335');\n  const requestId = requestIdField && requestIdField.value ? (requestIdField.value.text || '') : '';\n  const usageField = customFields.find(f => f.idCustomField === '67b714e5a7b5e806684eb8be');\n  const descField = customFields.find(f => f.idCustomField === '699d986489ebd38030a0d1eb');\n  const backboard = extractBackboardValue(card, customFields);\n  const backboardPromptBlock = buildBackboardPromptBlock(backboard);\n  const backboardsByIndex = {};\n  for (const [index, fieldId] of Object.entries(BACKBOARD_FIELD_IDS)) {\n    const value = customFieldTextById(customFields, fieldId);\n    if (value) backboardsByIndex[Number(index)] = value;\n  }\n  const lightColorsByIndex = {};\n  for (const [index, fieldId] of Object.entries(LIGHT_COLOR_FIELD_IDS)) {\n    const value = customFieldTextById(customFields, fieldId);\n    if (value) lightColorsByIndex[Number(index)] = value;\n  }\n  const titleColor = colorFromTitle(cardName);\n\n  let usage = (usageField && usageField.value ? usageField.value.text : '') || parsedUsage;\n  const usageNorm0 = usage.toLowerCase().trim();\n  if (usageNorm0 === 'indoor') usage = 'Innen';\n  else if (usageNorm0 === 'outdoor') usage = 'Au\u00dfen';\n  let description = descField && descField.value ? (descField.value.text || '') : '';\n\n  const errors = [];\n  const usageLower = usage.toLowerCase().trim();\n  if (!usageLower || (usageLower !== 'innen' && usageLower !== 'au\u00dfen' && usageLower !== 'aussen')) errors.push('Usage fehlt');\n  const descriptionMissing = !description || description.trim().length < 3;\n\n  if (descriptionMissing) {\n    errors.push('Description fehlt');\n  }\n  if (errors.length > 0) {\n    if (!firstInvalid) firstInvalid = { json: { valid: false, needsAI: false, cardId: card.id, cardName, cardUrl: card.url, errorReason: errors.join('; ') } };\n    continue;\n  }\n\n  let slotPlans = slotsToProcess.map(p => {\n    const fn = p.source.fileName || p.source.name;\n    const fileName = nextAiMockupFileNameForSource(p.source.name || fn, existingAiNumbersByBase, reservedAiNumbersByBase);\n    const planBackboard = backboardsByIndex[p.linkedItemIndex + 1] || backboard;\n    const planBackboardPromptBlock = buildBackboardPromptBlock(planBackboard);\n    const requestedLightColor = lightColorsByIndex[p.linkedItemIndex + 1] || titleColor;\n    const colorBlock = buildColorLockPromptBlock(requestedLightColor, signType);\n    return {\n      slot: p.slot,\n      variantSlot: p.variantSlot,\n      sourceIndex: p.sourceIndex,\n      linkedItemIndex: p.linkedItemIndex,\n      linkedItemTitle: p.linkedItemTitle,\n      requestedLightColor,\n      colorBlock,\n      backboard: planBackboard,\n      backboardPromptBlock: planBackboardPromptBlock,\n      promptHints: [promptHints.join('\\\\n\\\\n'), planBackboardPromptBlock].filter(Boolean).join('\\\\n\\\\n'),\n      sourceDownloadUrl: 'https://api.trello.com/1/cards/' + card.id + '/attachments/' + p.source.id + '/download/' + encodeURIComponent(fn),\n      sourceName: p.source.name,\n      sourceId: p.source.id,\n      sourceBaseName: sourceBaseName(p.source.name || fn),\n      aiMockupFileName: fileName,\n      aiMockupPublicUrl: aiMockupPublicUrl(requestId, fileName),\n    };\n  });\n\n  if (needsTabletopMockup) {\n    const source = selectedSources[0];\n    const fn = source.fileName || source.name;\n    for (const tabletopNumber of missingTabletopNumbers) {\n      const tabletopFileName = 'Tabletop' + String(tabletopNumber).padStart(2, '0') + '.png';\n      slotPlans.push({\n        slot: 9000 + tabletopNumber,\n        variantSlot: 98 + tabletopNumber,\n        sourceIndex: 1,\n        tabletopDevice: true,\n        outputFileName: tabletopFileName,\n        linkedItemIndex: null,\n        linkedItemTitle: 'Acryl LED Tischger\u00e4t',\n        sourceDownloadUrl: 'https://api.trello.com/1/cards/' + card.id + '/attachments/' + source.id + '/download/' + encodeURIComponent(fn),\n        sourceName: source.name,\n        sourceId: source.id,\n        sourceBaseName: 'Tabletop',\n        aiMockupFileName: tabletopFileName,\n        aiMockupPublicUrl: aiMockupPublicUrl(requestId, tabletopFileName),\n      });\n    }\n  }\n\n  validCards.push({ json: {\n    valid: true,\n    cardId: card.id,\n    boardId: card.idBoard,\n    cardName,\n    designText,\n    cardUrl: card.url,\n    cardDesc,\n    usage: usage.trim(),\n    description: description.trim(),\n    backboard,\n    backboardPromptBlock,\n    lightColorsByIndex,\n    titleColor,\n    signType,\n    modifiers,\n    promptHints: [promptHints.join('\\n\\n'), backboardPromptBlock].filter(Boolean).join('\\n\\n'),\n    mockupSlots: slotsToProcess.map(p => p.slot),\n    slotPlans,\n    totalMockupsNeeded: slotPlans.filter(plan => plan && plan.tabletopDevice !== true).length,\n    totalSlotsToProcess: slotPlans.length,\n    maxNormalMockupSlots: isLargeRequest ? selectedSources.length * mockupsPerSelectedSource : (isStandardNeonFlex ? 16 : 8),\n    isLargeRequest,\n    mockupsPerSource: mockupsPerSelectedSource,\n    sourceMockupCount: sourceMockups.length,\n    staleSourceMockupCount: staleSourceMockups.length,\n    sourceMockupFreshnessHours: sourceBatchMaxAgeMs / (60 * 60 * 1000),\n    sourceSelectionMode: isUpdateCard ? 'update_latest_batch' : 'latest_24h_batch',\n    processedSourceMockupCount: selectedSources.length,\n    totalMockupFilesOnCard: currentTotal,\n    hasRetryLabel,\n    requestId,\n    existingMockups: [],\n    existingTabletopMockups: existingTabletopMockups.map(a => ({ id: a.id, name: a.name })),\n    needsTabletopMockup,\n    isRetry: hasErrorMarker || hasWaitingMarker,\n    retryCount: hasRetryLabel ? 0 : retryCount\n  } });\n}\n\nif (validCards.length > 0) {\n  validCards.sort((a, b) => {\n    const aLarge = Boolean(a && a.json && a.json.isLargeRequest);\n    const bLarge = Boolean(b && b.json && b.json.isLargeRequest);\n    if (aLarge !== bLarge) return aLarge ? 1 : -1;\n\n    const aRetry = Boolean(a && a.json && a.json.hasRetryLabel);\n    const bRetry = Boolean(b && b.json && b.json.hasRetryLabel);\n    if (aRetry !== bRetry) return aRetry ? -1 : 1;\n\n    const aSlots = Number((a && a.json && a.json.totalSlotsToProcess) || 0);\n    const bSlots = Number((b && b.json && b.json.totalSlotsToProcess) || 0);\n    return aSlots - bSlots;\n  });\n  return [validCards[0]];\n}\nif (firstInvalid) return [firstInvalid];\nreturn [];"
      },
      "id": "n03",
      "name": "Extract & Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        1080
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond1",
              "leftValue": "={{ $json.valid }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "n04",
      "name": "IF Valid",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        672,
        1080
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.sourceDownloadUrl }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": false,
        "options": {
          "redirect": {
            "redirect": {
              "maxRedirects": 5
            }
          },
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "n05",
      "name": "Download Mockup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1792,
        864
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "edit",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-pro-image"
        },
        "prompt": "=NEONTRIP DESIGN LOCK v2 - HIGHEST PRIORITY:\nTreat only the actual customer sign/logo artwork in the source as an immutable design layer.\nDo not redraw, reinterpret, beautify, simplify, vectorize, typeset, correct, complete, or recreate it from memory.\nPreserve every glyph outline, counter/opening, curve, corner, serif, stroke path, endpoint, line break, kerning, spacing, icon position, component count, and relative proportion exactly.\nOnly a single uniform scale plus physically correct camera perspective may be applied to the complete design. Never deform individual letters, symbols, lines, or logo parts.\nMaterial depth, mounting, scene, shadows, and glow may be added around the locked front-face design, but must not alter its geometry.\nTechnical measurements, arrows, prices, labels, tables, and page backgrounds are not part of the design and must still be removed.\nIf exact preservation is not possible, fail without an image instead of inventing or approximating any part of the design.\n\nWICHTIGSTE REGEL:\nDas Leuchtschild aus dem Input-Bild MUSS UNVER\u00c4NDERT bleiben.\n\u00c4ndere AUSSCHLIESSLICH die Umgebung um das Schild herum.\n\nNiemals Text, Buchstaben, Schriftart, Logo, Form, Proportionen oder Design-Element des Schildes ver\u00e4ndern.\nNiemals eigenen Text hinzuf\u00fcgen oder bestehenden Text ersetzen.\nWelche Teile leuchten bzw. nicht leuchten, bleibt EXAKT wie im Input.\n\n\nWICHTIG BEI TECHNISCHEN REFERENZBILDERN:\nDas Input-Bild ist eine technische Vorlage.\nAlle Ma\u00dfe, Ziffern, Ma\u00dflinien, Pfeile, Gr\u00f6\u00dfenangaben, Preise, Codes, Labels, Raster, wei\u00dfen Zeichenfl\u00e4chen oder Zusatztexte geh\u00f6ren NICHT zum finalen Foto und m\u00fcssen vollst\u00e4ndig entfernt werden.\n\u00dcbernimm niemals die technische Zeichnung im unteren Bereich, keine Zahlen, keine cm-Angaben, keine Skizzen, keine Blueprint-Optik.\n\nNur die tats\u00e4chliche Schildform, das Logo/Design und die echte Leucht-/Materialwirkung d\u00fcrfen \u00fcbernommen werden.\n\nWenn das Input mehrere Gr\u00f6ssen-Varianten desselben Schildes nebeneinander zeigt:\nRendere NUR EIN Schild im finalen Mockup (das gr\u00f6sste / prominenteste).\nNiemals mehrere Gr\u00f6ssen nebeneinander darstellen.\n\n\nAUFGABE:\nPlatziere das Schild in einer realistischen {{ $('Extract & Validate').item.json.usage }} Umgebung\n(Ort: {{ $('Extract & Validate').item.json.description }}).\nDas Ergebnis soll wie ein hochwertiges, fotorealistisches Werbebild aussehen.\n\nSZENARIO JE MOCKUP:\n{{ (() => {\n  const slot = Number($('Loop Over Slots').item.json.variantSlot || $('Loop Over Slots').item.json.mockupSlot || $json.variantSlot || $json.mockupSlot || 1);\n  const photoAnchor = `\n- echtes hochwertiges Werbefoto, keine 3D-Visualisierung, kein CGI, kein Konzeptbild, kein Render-Look\n- Schild gross, nah und klar lesbar; die Umgebung ist nur ruhiger Kontext\n- keine Totale, keine Weitwinkelaufnahme, keine Menschenmenge, keine Banner, keine erfundenen Texte`;\n  const rawUsageTop = String($('Extract & Validate').item.json.usage || '').toLowerCase();\n  const rawDescriptionTop = String($('Extract & Validate').item.json.description || '').toLowerCase();\n  const rawSignTypeTop = String($('Extract & Validate').item.json.signType || '').toLowerCase();\n  const rawCardNameTop = String($('Extract & Validate').item.json.cardName || '').toLowerCase();\n  const neonContextTop = [rawUsageTop, rawDescriptionTop, rawSignTypeTop, rawCardNameTop].join(' ');\n  const isOutdoorTop = /(au\u00dfen|aussen|outdoor|outside|exterior|fassade|ladenfront|storefront|au\u00dfenbereich|aussenbereich)/i.test(neonContextTop);\n  const isExcludedProductTop = /\\b3d\\b|frontlit|backlit|halo|full\\s*glow|ultra\\s*thin|ultrathin|light\\s*box|lightbox|leuchtkasten|double\\s*sided|nasenschild|non\\s*lit|nonlit|unbeleuchtet/i.test(neonContextTop);\n  const isStandardNeonFlexTop = !isExcludedProductTop && /(led\\s*flex|neon\\s*flex|led\\s*neon|neonschild|neon\\s*sign|front\\s*glow|back\\s*glow)/i.test(neonContextTop);\n  if (isStandardNeonFlexTop) {\n    const indoorScenarios = {\n      1: `MOCKUP 01 - WANDMONTAGE IM SETTING:\\n${photoAnchor}\\n- Description und Usage bestimmen den Ort, z.B. Indoor-Surfhalle, Restaurant, Studio, Praxis oder Shop\\n- Schild ist sauber an einer passenden Innenwand montiert, nicht freischwebend, keine Kabel sichtbar\\n- reales hochwertiges Foto, Schild vollst\u00e4ndig sichtbar und klar lesbar`,\n      2: `MOCKUP 02 - ABGEH\u00c4NGT VOR FENSTER/GLAS:\\n${photoAnchor}\\n- im selben Setting aus der Description, aber mit Drahtseilen sauber vor einem Fenster, Glasbereich oder heller Raum\u00f6ffnung abgeh\u00e4ngt\\n- Drahtseile wirken realistisch und dezent; keine Kabel, kein Netzteil, kein Chaos\\n- Schild vollst\u00e4ndig sichtbar, nicht zu seitlich, hochwertiges echtes Foto`,\n      3: `MOCKUP 03 - FRONTALE ANSICHT:\\n${photoAnchor}\\n- relativ frontal, nur minimale Perspektive, damit Logo/Text maximal klar lesbar bleibt\\n- Setting weiterhin aus Description und Usage ableiten\\n- keine extreme Seitenansicht, kein schr\u00e4ger Winkel bei dem man das Design kaum erkennt`,\n      4: `MOCKUP 04 - MOOSWAND:\\n${photoAnchor}\\n- hochwertige echte Indoor-Naturmooswand oder Pflanzenwand, aber nur als ruhiger Hintergrund\\n- Schild fest auf der Mooswand montiert, nicht schwebend, keine Kabel sichtbar\\n- realistisches Premium-Foto, kein k\u00fcnstlicher Render-Look`\n    };\n    const outdoorScenarios = {\n      1: `MOCKUP 01 - AUSSEN AN HAUSWAND/FASSADE:\\n${photoAnchor}\\n- Usage Outdoor/Au\u00dfen hat Vorrang: echte Au\u00dfenfassade, Hauswand, Ladenfront, Eingang oder Geb\u00e4udewand\\n- Schild wetterfest und glaubw\u00fcrdig an der Wand montiert, keine Mooswand, kein Innenraum\\n- reale Tageslicht- oder Abendstimmung, klare Lesbarkeit`,\n      2: `MOCKUP 02 - AUSSEN \u00dcBER LADENLOKAL/EINGANG:\\n${photoAnchor}\\n- passend zur Description als Au\u00dfenwerbung \u00fcber T\u00fcr, Schaufenster, Eingang oder Ladenfront\\n- Schild ist vollst\u00e4ndig sichtbar, prominent und kaufentscheidend inszeniert\\n- keine Menschenmenge, keine erfundenen Banner, keine zus\u00e4tzlichen Texte`,\n      3: `MOCKUP 03 - FRONTALE AUSSENANSICHT:\\n${photoAnchor}\\n- relativ frontal auf Au\u00dfenwand oder Fassade, damit Logo/Text maximal klar lesbar bleibt\\n- keine extreme Seitenansicht, keine Perspektive bei der man das Design kaum erkennt\\n- reale Schatten, Wandkontakt und wetterfeste Montage`,\n      4: `MOCKUP 04 - OUTDOOR-EVENT / AUSSENKONTEXT:\\n${photoAnchor}\\n- wenn die Description Event, Festival, Terrasse, Outdoor-Bar oder Veranstaltung nahelegt: hochwertiger Outdoor-Event-Kontext\\n- sonst hochwertige Au\u00dfenwand/Fassade als sichere Alternative\\n- keine Mooswand, kein Innenraum, keine Menschenmenge; Schild bleibt Hauptmotiv`\n    };\n    const selectedScenarios = isOutdoorTop ? outdoorScenarios : indoorScenarios;\n    return selectedScenarios[slot] || selectedScenarios[4];\n  }\n  const isDoubleSidedTop = /double\\s*sided|nasenschild/i.test(neonContextTop);\n  const isUltraThinTop = /ultra\\s*thin|ultrathin|acrylic\\s*lightbox|acryl\\s*lightbox/i.test(neonContextTop);\n  const isLightboxTop = !isDoubleSidedTop && !isUltraThinTop && /light\\s*box|lightbox|leuchtkasten/i.test(neonContextTop);\n  const isFullGlowTop = /full\\s*glow/i.test(neonContextTop);\n  const is3dTop = /\\b3d\\b|leuchtbuchstaben|buchstaben/i.test(neonContextTop);\n  const isNonLitTop = /non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(beleuchtung|licht)/i.test(neonContextTop);\n  const isBacklitTop = /backlit|r\u00fcckbeleuchtet|rueckbeleuchtet|halo/i.test(neonContextTop);\n  const isFrontlitTop = /frontlit|frontbeleuchtet/i.test(neonContextTop);\n  if (isDoubleSidedTop) {\n    const scenes = {\n      1: `DOUBLE SIDED LIGHTBOX - AUSSEN NASENSCHILD FRONT: ${photoAnchor}\\n- echtes beidseitiges Nasenschild au\u00dfen an einer Ladenfront, seitliche stabile Wandhalterung sichtbar\\n- immer Au\u00dfenkontext, niemals Mooswand, niemals Drahtseile, niemals Innenraum\\n- Schild vollst\u00e4ndig lesbar, frontal genug, echte Schatten und wetterfeste Montage`,\n      2: `DOUBLE SIDED LIGHTBOX - SEITLICHE HALTERUNG: ${photoAnchor}\\n- Kamera leicht seitlich, damit die Wandhalterung und beide leuchtenden Seiten plausibel sichtbar sind\\n- Au\u00dfenfassade, Ladenlokal oder Eingang, keine frei schwebende Box\\n- keine Zusatztexte, keine Banner, kein CGI`,\n      3: `DOUBLE SIDED LIGHTBOX - FRONTALE AUSSENANSICHT: ${photoAnchor}\\n- relativ frontal an Au\u00dfenwand oder Ladenfront, sehr klare Lesbarkeit\\n- seitliche Halterung darf sichtbar sein, Schild h\u00e4ngt physisch korrekt an der Wand\\n- keine Mooswand, keine Drahtseile`,\n      4: `DOUBLE SIDED LIGHTBOX - ABEND AUSSEN: ${photoAnchor}\\n- Au\u00dfenfassade bei D\u00e4mmerung/Abend, gleichm\u00e4\u00dfig beleuchtete beidseitige Lightbox\\n- reale Wandhalterung, kein Schweben, keine Kabel oder Netzteile`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  if (isUltraThinTop || isLightboxTop || isFullGlowTop) {\n    const productName = isUltraThinTop ? 'ULTRA THIN ACRYLIC LIGHTBOX' : (isFullGlowTop ? 'FULL GLOW LIGHTBOX' : 'LIGHTBOX');\n    const scenes = {\n      1: `${productName} - FLACH AN WAND/FASSADE: ${photoAnchor}\\n- d\u00fcnner, gleichm\u00e4\u00dfig leuchtender Lichtk\u00f6rper flach an einer durchgehenden Wand oder Fassade montiert\\n- keine Drahtseile, keine Mooswand, kein frei schwebendes Schild\\n- saubere Kanten, echte Schatten, hochwertige reale Fotografie`,\n      2: `${productName} - \u00dcBER EINGANG/SHOPFRONT: ${photoAnchor}\\n- als hochwertige Au\u00dfen- oder Innenwerbung \u00fcber Eingang, Empfang oder Shopfront\\n- gleichm\u00e4\u00dfige Lichtfl\u00e4che, keine Neonr\u00f6hren, keine sichtbaren Kabel\\n- physisch plausibel befestigt`,\n      3: `${productName} - FRONTALE LESBARE ANSICHT: ${photoAnchor}\\n- relativ frontal, minimale Verzerrung, Logo/Text maximal klar lesbar\\n- vollst\u00e4ndiger Lichtkasten sichtbar, keine abgeschnittenen R\u00e4nder\\n- echtes Foto, kein Render`,\n      4: `${productName} - PREMIUM DETAILWINKEL: ${photoAnchor}\\n- milder Seitenwinkel, damit d\u00fcnne Materialtiefe sichtbar wird, aber Logo lesbar bleibt\\n- Wandkontakt und Schatten plausibel, keine Drahtseile, keine Mooswand`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  if (is3dTop) {\n    const family = isNonLitTop ? '3D NON-LIT / UNBELEUCHTET' : (isBacklitTop ? '3D BACKLIT / HALO' : (isFrontlitTop ? '3D FRONTLIT' : '3D LEUCHTBUCHSTABEN'));\n    const wallMaterial = isOutdoorTop ? 'Au\u00dfenfassade, Ladenfront, Stein, Putz, Beton oder Metallfassade' : 'hochwertige Innenwand aus Putz, Beton, Stein, Holz oder Metall';\n    const scenes = {\n      1: `${family} - FESTE WANDMONTAGE: ${photoAnchor}\\n- ${wallMaterial}; alle Buchstaben physisch fest an EINER durchgehenden Wand montiert\\n- niemals Drahtseile, niemals Deckenabh\u00e4ngung, niemals freischwebend, niemals vor einer Wandkante\\n- ${isNonLitTop ? 'kein Leuchten, kein Glow, nur echte Materialoberfl\u00e4che und nat\u00fcrliche Schatten' : isBacklitTop ? 'Halo-Licht strahlt indirekt nach hinten auf die Wand, Front bleibt materialgerecht' : 'Licht strahlt nach vorne, Materialtiefe bleibt sichtbar'}`,\n      2: `${family} - MATERIALTIEFE SICHTBAR: ${photoAnchor}\\n- milder 15-25\u00b0 Seitenwinkel, Tiefe und Wandabstand sichtbar, aber Schrift/Logo klar lesbar\\n- reale Kontakt-Schatten, keine Kabel, keine Netzteile\\n- keine Mooswand au\u00dfer bei eindeutig passendem Indoor-Wellness/Spa-Kontext`,\n      3: `${family} - FRONTALE ANSICHT: ${photoAnchor}\\n- straight-on frontal view, minimale Perspektivverzerrung, vollst\u00e4ndig lesbar\\n- alle Elemente sauber an der Wand befestigt, nichts schwebt\\n- ${isNonLitTop ? 'absolut keine Leuchteffekte' : 'korrektes Lichtverhalten passend zu frontlit/backlit'}`,\n      4: `${family} - PREMIUM KONTEXT: ${photoAnchor}\\n- ${isOutdoorTop ? 'hochwertiger Au\u00dfenkontext an Fassade oder Eingang, keine Mooswand' : (isBacklitTop ? 'optional hochwertige Mooswand nur wenn es realistisch zum Segment passt, sonst Premium-Innenwand' : 'Premium-Innenwand passend zur Description, keine Drahtseile')}\\n- Foto wirkt wie echte fertige Installation, keine KI-Optik, keine erfundenen Elemente`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  const scenarios = {\n    1: `MOCKUP 01 - WIE BISHER:\n- nutze die Kartenbeschreibung und Usage als wichtigste Umgebungsvorgabe\n- keine neue Sonder-Szene erzwingen, insbesondere keine Messe/Booth/Event-Szene ohne ausdr\u00fccklichen Hinweis in der Beschreibung\n- maximal fotorealistisch, nah, hochwertig, vollst\u00e4ndig sichtbar und klar lesbar`,\n    2: (() => {\n      const rawUsage = String($('Extract & Validate').item.json.usage || '').toLowerCase();\n      const rawDescription = String($('Extract & Validate').item.json.description || '').toLowerCase();\n      const rawSignType = String($('Extract & Validate').item.json.signType || '').toLowerCase();\n      const rawCardName = String($('Extract & Validate').item.json.cardName || '').toLowerCase();\n      const context = [rawUsage, rawDescription, rawSignType, rawCardName].join(' ');\n      const isOutdoor = /(au\u00dfen|aussen|outdoor|outside|exterior|fassade|ladenfront|storefront)/i.test(context);\n      const is3D = /\\b3d\\b|frontlit|backlit|halo|leuchtbuchstaben|buchstaben/i.test(context);\n      const isNonLit = /(non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(beleuchtung|licht))/i.test(context);\n      const wellnessIndoor = /(spa|wellness|beauty|kosmetik|hotel|lobby|praxis|medical|physio|physiotherapie|salon)/i.test(context) && !isOutdoor;\n      const mossAllowed = wellnessIndoor && !isNonLit;\n\n      if (isOutdoor) {\n        return `MOCKUP 02 - OUTDOOR / FASSADE:\n${photoAnchor}\n- Usage hat Vorrang: Au\u00dfen/Outdoor bedeutet echte Au\u00dfenfassade, Ladenfront, Eingangswand, Stein-, Beton-, Putz- oder Metallfassade\n- niemals Naturmooswand, Pflanzenwand, Spa-Interior, Sofa, Showroom oder Innenraum verwenden\n- Schild ist fest und glaubw\u00fcrdig an EINER durchgehenden vertikalen Au\u00dfenwand montiert, nicht vor der Wand, nicht freischwebend\n- reale Tageslicht- oder Abend-Fassadenstimmung, wetterfeste Installation, echte Schatten und Wandkontakt\n- kein CGI, keine Architekturvisualisierung, keine erfundenen Zusatztexte`;\n      }\n\n      if (is3D && !mossAllowed) {\n        return `MOCKUP 02 - PREMIUM WANDINSTALLATION:\n${photoAnchor}\n- deutlich anderes Motiv als Mockup 01: hochwertige reale Wand aus Putz, Beton, Stein, Holz oder Metall, passend zu Ort und Branche\n- keine Naturmooswand, keine Pflanzenwand, kein gr\u00fcner Fake-Hintergrund, au\u00dfer der Kontext ist eindeutig Indoor-Wellness/Praxis/Spa\n- 3D-Elemente sind physisch fest an EINER durchgehenden vertikalen Wand montiert, mit Kontakt-Schatten, Tiefe und realem Wandabstand\n- niemals schwebend, niemals vor einer Wandkante, niemals \u00fcber T\u00fcr-/Fensterkante, niemals in offener Luft oder auf unsichtbarer Halterung\n- mild seitlicher 15-25\u00b0 Fotowinkel: Tiefe sichtbar, aber Logo/Text bleibt klar lesbar`;\n      }\n\n      return `MOCKUP 02 - PREMIUM INTERIOR:\n${photoAnchor}\n- hochwertige reale Innenwand passend zur Beschreibung; Naturmooswand nur wenn es wie eine echte, dichte, flache, vollfl\u00e4chige Indoor-Mooswand wirkt\n- Schild ist sauber und fest an einer durchgehenden Wand montiert, nicht abgeh\u00e4ngt und nicht freischwebend\n- keine Drahtseile, keine Stahlseile, keine Deckenabh\u00e4ngung, keine sichtbaren Aufh\u00e4ngeseile, keine Saugn\u00e4pfe, keine Glasmontage\n- nat\u00fcrliche Wandstruktur, hochwertige ruhige Lichtstimmung, Schild bleibt gross, nah und klar lesbar\n- keine erfundenen \u00d6ffnungszeiten, keine Preisaufkleber, keine Zusatztexte`;\n    })(),\n    3: `MOCKUP 03 - WEITERE PREMIUM-ANSICHT:\n${photoAnchor}\n- Beschreibung und Usage haben Vorrang: dieselbe Branche, derselbe Ort und dieselbe Nutzung wie aus der Kartenbeschreibung ableiten\n- keine automatische Messe-, Booth-, Event- oder Showroom-Szene; nur wenn die Kartenbeschreibung ausdr\u00fccklich Messe/Messestand/Exhibition/Booth nennt\n- hochwertiger realistischer Wand-/Raum-/Fassadenkontext passend zur Beschreibung; Schild bleibt gro\u00df, nah, vollst\u00e4ndig sichtbar und klar lesbar`\n  };\n  return scenarios[slot] || scenarios[3];\n})() }}\n\nQUALIT\u00c4T F\u00dcR DIESES EINE MOCKUP:\n- Mockup 01 ist der Qualit\u00e4tsanker: jedes weitere Mockup muss genauso realistisch, nah, hochwertig und fotografisch wirken.\n- Erzeuge ein echtes Premium-Werbefoto mit realistischer Linse, nat\u00fcrlicher Tiefensch\u00e4rfe, echten Oberfl\u00e4chen, echten Schatten und echten Lichtreflexionen.\n- Niemals 3D-Rendering, Architekturvisualisierung, Produktvisualisierung, KI-Concept-Art, flaches CGI, cartoonartige Szene oder generisches Stockfoto.\n- Die Szene nicht zu gross erz\u00e4hlen. Das Schild bleibt Hauptmotiv; Umgebung bleibt sekund\u00e4r.\n- Keine zweite Szene, keine Mehrfachansicht, kein Raster, keine Collage, keine Zusatzlogos und keine erfundenen Texte.\n\n\nKAMERA \u2014 VARIANT A:\n- milder 15\u201325\u00b0 Drei-Viertel-Winkel von LINKS\n- nat\u00fcrliche Augenh\u00f6he, realistische 35mm- oder 50mm-Fotoperspektive\n- sichtbare Tiefe (Kantendicke, Wandabstand), aber Logo/Text bleibt klar lesbar\n- realistischer Kontakt-Schatten direkt auf der Wand\n- nicht extrem seitlich, nicht gekippt, keine Perspektive bei der man das Design kaum lesen kann\n- Schild muss wie ein echtes Foto einer fertigen Installation aussehen\n\n\nUMGEBUNG & LICHT:\n- realistisches Setting passend zu {{ $('Extract & Validate').item.json.description }}\n- kein Studio-Freisteller\n- nat\u00fcrliche Raumbeleuchtung der Umgebung\n- Glow des Schildes wirkt realistisch auf Wand und Umgebung\n- leichte Tiefensch\u00e4rfe: Schild scharf, Hintergrund weich\n\n\n{{ $('Loop Over Slots').item.json.colorBlock || $('Parse Color Result').first().json.colorBlock }}\n\n\n{{ $('Loop Over Slots').item.json.promptHints || $('Extract & Validate').item.json.promptHints || '' }}\n\n\n{{ $('Loop Over Slots').item.json.backboardPromptBlock || $('Extract & Validate').item.json.backboardPromptBlock || '' }}\n\n\nNEGATIVREGELN:\nKeine Ma\u00dfzeichnung, keine cm-Zahlen, keine Pfeile, keine technischen Linien, keine wei\u00dfe Konstruktionsfl\u00e4che, keine Preis-/Code-Texte, keine Skizze aus dem unteren Referenzbereich.\n\nMONTAGE:\nSchild realistisch fest an EINER durchgehenden Wandfl\u00e4che befestigt.\nAlle Buchstaben/Logo-Elemente haben Wandkontakt, reale Kontakt-Schatten und glaubw\u00fcrdige Tiefe.\nKeine Kabel sichtbar.\nNicht schwebend, nicht freistehend, nicht vor der Wand, nicht auf unsichtbarer Halterung.\nNicht auf Glas.",
        "images": {
          "values": [
            {}
          ]
        },
        "options": {
          "binaryPropertyOutput": "mockupImage"
        }
      },
      "id": "n06",
      "name": "Gemini Image Edit (Variant A)",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1.1,
      "position": [
        2464,
        592
      ],
      "retryOnFail": false,
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond2",
              "leftValue": "={{ $json.error }}",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "n07",
      "name": "Check Gemini Result",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2688,
        672
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/attachments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $('Loop Over Slots').item.json.aiMockupPublicUrl }}"
            },
            {
              "name": "name",
              "value": "={{ $('Loop Over Slots').item.json.aiMockupFileName }}"
            }
          ]
        }
      },
      "id": "n10",
      "name": "Upload Mockup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2912,
        432
      ],
      "retryOnFail": true,
      "maxTries": 6,
      "waitBetweenTries": 10000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "name",
              "value": "={{ $(\"Extract & Validate\").item.json.cardName }}"
            },
            {
              "name": "idAttachmentCover",
              "value": "={{ $json.coverId }}"
            }
          ]
        }
      },
      "id": "n11",
      "name": "Reset Title + Set Cover",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2464,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond3",
              "leftValue": "={{ $json.needsAI }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "n12",
      "name": "IF needsAI",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        896,
        1800
      ]
    },
    {
      "parameters": {
        "url": "=https://klibiejfisijpagzkxls.supabase.co/rest/v1/master_requests?request_id=eq.{{ $json.requestId }}&select=email,customer_id,master_customers(email,first_name,last_name,company_name)",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "options": {}
      },
      "id": "n13",
      "name": "Supabase: Get Customer",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1120,
        1800
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const prev = $('Extract & Validate').first().json;\nconst supabaseResult = $input.first().json;\n\nlet customerEmail = '';\nlet companyName = '';\n\nif (Array.isArray(supabaseResult) && supabaseResult.length > 0) {\n  customerEmail = supabaseResult[0].email || '';\n  if (supabaseResult[0].master_customers) {\n    companyName = supabaseResult[0].master_customers.company_name || '';\n    if (!customerEmail) customerEmail = supabaseResult[0].master_customers.email || '';\n  }\n} else if (supabaseResult && supabaseResult.email) {\n  customerEmail = supabaseResult.email || '';\n}\n\nconst emailDomain = customerEmail.includes('@') ? customerEmail.split('@')[1] : '';\nconst usageText = String(prev.usage || prev.parsedUsage || '').toLowerCase();\nconst defaultDesc = prev.defaultDesc || (usageText.includes('au\u00dfen') || usageText.includes('aussen') ? 'Hausfassade' : 'B\u00fcro');\n\nreturn [{ json: {\n  cardId: prev.cardId,\n  cardName: prev.cardName,\n  designText: prev.designText || '',\n  cardDesc: prev.cardDesc,\n  usage: prev.usage,\n  parsedUsage: prev.parsedUsage,\n  customerEmail,\n  emailDomain,\n  companyName,\n  defaultDesc,\n  forceDefaultDesc: false\n} }];"
      },
      "id": "n14",
      "name": "Build Outlook Search",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        1800
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "limit": 3,
        "filtersUI": {
          "values": {
            "filters": {
              "receivedAfter": "={{ $now.minus({ days: 90 }).toISO() }}",
              "sender": "={{ $json.customerEmail }}"
            }
          }
        },
        "options": {}
      },
      "id": "n15",
      "name": "Outlook: Search Email",
      "type": "n8n-nodes-base.microsoftOutlook",
      "typeVersion": 2,
      "position": [
        1568,
        1800
      ],
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('Build Outlook Search').first().json;\nconst emailItems = $input.all();\n\nlet emailContent = '';\ntry {\n  const emails = emailItems.map(i => i.json);\n  if (emails && emails.length > 0 && !emails[0].error) {\n    emailContent = emails.map(e => (\n      'Betreff: ' + (e.subject || '') + '\\nNachricht: ' + (e.bodyPreview || '')\n    )).join('\\n---\\n');\n  }\n} catch (e) {\n  emailContent = '';\n}\n\nconst allowedSettings = [\n  'B\u00fcro',\n  'B\u00e4ckerei',\n  'Caf\u00e9',\n  'Restaurant',\n  'Bar',\n  'Hotel',\n  'Bankfiliale',\n  'Vereinsheim',\n  'Fitnessstudio',\n  'Praxis',\n  'Friseursalon',\n  'Ladenlokal',\n  'Schaufenster',\n  'Empfang',\n  'Messewand',\n  'Werkstatt',\n  'Wohnzimmer',\n  'Hausfassade'\n];\n\nfunction normalizeText(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00e4/g, 'ae')\n    .replace(/\u00f6/g, 'oe')\n    .replace(/\u00fc/g, 'ue')\n    .replace(/\u00df/g, 'ss');\n}\n\nfunction joinedEvidence() {\n  return normalizeText([\n    ctx.designText,\n    ctx.companyName,\n    ctx.emailDomain,\n    ctx.cardName,\n    ctx.cardDesc,\n    emailContent\n  ].filter(Boolean).join('\\n'));\n}\n\nfunction identityEvidence() {\n  return normalizeText([\n    ctx.designText,\n    ctx.companyName,\n    ctx.emailDomain,\n    ctx.cardName\n  ].filter(Boolean).join('\\n'));\n}\n\nfunction deterministicSetting() {\n  const evidence = joinedEvidence();\n  const identity = identityEvidence();\n  const design = normalizeText(ctx.designText);\n  const usage = normalizeText(ctx.usage || ctx.parsedUsage);\n\n  const has = (pattern) => pattern.test(evidence);\n  const identityHas = (pattern) => pattern.test(identity);\n  const designHas = (pattern) => pattern.test(design);\n\n  if (identityHas(/\\b(sparkasse|volksbank|raiffeisen|bankfiliale|bankhaus|deutsche\\s+bank|commerzbank)\\b/)) return { setting: 'Bankfiliale', reason: 'strong_bank_identity_keyword' };\n  if (has(/\\b(fussball|fussballverein|sportverein|vereinsheim|clubheim)\\b/) || designHas(/\\b(fc|sv|tsv|sc)\\s+[a-z0-9]/)) return { setting: 'Vereinsheim', reason: 'sports_club_keyword' };\n  if (has(/\\b(backerei|baeckerei|backkaffee|brotchen|broetchen|brot|konditorei|backshop)\\b/)) return { setting: 'B\u00e4ckerei', reason: 'bakery_keyword' };\n  if (has(/\\b(speakeasy|cocktail|pub|tapasbar|weinbar|bar)\\b/)) return { setting: 'Bar', reason: 'bar_keyword' };\n  if (has(/\\b(cafe|kaffee|espresso|roesterei)\\b/)) return { setting: 'Caf\u00e9', reason: 'cafe_keyword' };\n  if (has(/\\b(restaurant|bistro|pizzeria|grill|imbiss|doener|d\u00f6ner|sushi|kueche|kitchen)\\b/)) return { setting: 'Restaurant', reason: 'restaurant_keyword' };\n  if (has(/\\b(hotel|hostel|ferienwohnung|apartmenthotel)\\b/)) return { setting: 'Hotel', reason: 'hotel_keyword' };\n  if (has(/\\b(zahnarzt|arzt|praxis|physio|therapie|klinik|kosmetikstudio|aesthetik|\u00e4sthetik)\\b/)) return { setting: 'Praxis', reason: 'practice_keyword' };\n  if (has(/\\b(friseur|friseur|barber|haarsalon|salon)\\b/)) return { setting: 'Friseursalon', reason: 'hair_salon_keyword' };\n  if (has(/\\b(fitness|gym|yoga|pilates|crossfit|sportstudio)\\b/)) return { setting: 'Fitnessstudio', reason: 'fitness_keyword' };\n  if (has(/\\b(werkstatt|garage|autohaus|carwash|lackiererei)\\b/)) return { setting: 'Werkstatt', reason: 'workshop_keyword' };\n  if (has(/\\b(messe|expo|messestand|booth|ausstellung)\\b/)) return { setting: 'Messewand', reason: 'trade_fair_keyword' };\n  if (has(/\\b(shop|store|laden|boutique|showroom|retail)\\b/)) return { setting: 'Ladenlokal', reason: 'retail_keyword' };\n\n  if (usage.includes('aussen') || usage.includes('outdoor')) return { setting: 'Hausfassade', reason: 'outdoor_default' };\n  return null;\n}\n\nconst result = {\n  usage: ctx.usage,\n  cardId: ctx.cardId,\n  parsedUsage: ctx.parsedUsage,\n  defaultDesc: ctx.defaultDesc || 'B\u00fcro'\n};\n\nconst deterministic = deterministicSetting();\nif (deterministic) {\n  result.description = deterministic.setting;\n  result.skipGemini = true;\n  result.descriptionSource = 'deterministic_' + deterministic.reason;\n  return [{ json: result }];\n}\n\nif (!emailContent && !ctx.customerEmail && !ctx.designText && !ctx.companyName) {\n  return [];\n}\n\nresult.skipGemini = false;\nresult.descriptionSource = 'gemini_environment_classifier';\nresult.prompt = 'Analysiere diese Kundenanfrage f\u00fcr ein LED Leuchtschild und bestimme genau EIN passendes Mockup-Setting f\u00fcr die Umgebung. Nutze den Design-Text als starkes Signal, weil er oft die Branche verr\u00e4t. Antworte ausschlie\u00dflich mit einem Wert aus dieser Liste: ' + allowedSettings.join(', ') + '.\\n\\n' +\n  'Beispiele:\\n' +\n  '- Design enth\u00e4lt Kaffee, Br\u00f6tchen, Backkaffee, Brot oder Snacks => B\u00e4ckerei\\n' +\n  '- Sparkasse, Volksbank, Raiffeisen, Deutsche Bank oder Bankfiliale => Bankfiliale\\n' +\n  '- Fu\u00dfballverein, FC, SV, TSV oder Sportverein => Vereinsheim\\n' +\n  '- Speakeasy, Cocktail, Pub oder Bar => Bar\\n' +\n  '- Zahnarzt, Arzt, Physio oder Praxis => Praxis\\n' +\n  '- Keine klare Branche und Indoor => B\u00fcro\\n\\n' +\n  'Kartenname ohne Status: ' + (ctx.cardName || '') + '\\n' +\n  'Design-Text aus Kartenname: ' + (ctx.designText || 'nicht vorhanden') + '\\n' +\n  'Firmenname: ' + (ctx.companyName || 'unbekannt') + '\\n' +\n  'E-Mail-Domain: ' + (ctx.emailDomain || 'unbekannt') + '\\n' +\n  'Indoor/Outdoor: ' + (ctx.usage || '') + '\\n\\n' +\n  'E-Mail-Korrespondenz:\\n' + (emailContent || 'Keine E-Mails gefunden') + '\\n\\n' +\n  'Kartenbeschreibung:\\n' + (ctx.cardDesc || '').substring(0, 800) + '\\n\\n' +\n  'Wenn du nicht sicher bist oder keine Branche erkennbar ist, antworte mit: UNSICHER\\n\\n' +\n  'Antworte NUR mit dem Setting-Wert, ohne Erkl\u00e4rung.';\n\nreturn [{ json: result }];"
      },
      "id": "n16",
      "name": "Build Gemini Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1792,
        1800
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $json.skipGemini ? 'Antworte mit: ' + $json.description : $json.prompt }] }] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "n17",
      "name": "Gemini: Analyze Description",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2016,
        1800
      ],
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const prev = $('Build Gemini Prompt').first().json;\nconst geminiResponse = $input.first().json;\n\nconst allowed = new Map([\n  ['buro', 'B\u00fcro'],\n  ['buero', 'B\u00fcro'],\n  ['office', 'B\u00fcro'],\n  ['backerei', 'B\u00e4ckerei'],\n  ['baeckerei', 'B\u00e4ckerei'],\n  ['bakery', 'B\u00e4ckerei'],\n  ['cafe', 'Caf\u00e9'],\n  ['kaffee', 'Caf\u00e9'],\n  ['restaurant', 'Restaurant'],\n  ['bar', 'Bar'],\n  ['hotel', 'Hotel'],\n  ['bankfiliale', 'Bankfiliale'],\n  ['sparkasse', 'Bankfiliale'],\n  ['vereinsheim', 'Vereinsheim'],\n  ['clubheim', 'Vereinsheim'],\n  ['fussballverein', 'Vereinsheim'],\n  ['fitnessstudio', 'Fitnessstudio'],\n  ['fitness', 'Fitnessstudio'],\n  ['gym', 'Fitnessstudio'],\n  ['praxis', 'Praxis'],\n  ['arztpraxis', 'Praxis'],\n  ['friseursalon', 'Friseursalon'],\n  ['friseur', 'Friseursalon'],\n  ['barber', 'Friseursalon'],\n  ['ladenlokal', 'Ladenlokal'],\n  ['laden', 'Ladenlokal'],\n  ['shop', 'Ladenlokal'],\n  ['schaufenster', 'Schaufenster'],\n  ['empfang', 'Empfang'],\n  ['rezeption', 'Empfang'],\n  ['messewand', 'Messewand'],\n  ['messestand', 'Messewand'],\n  ['werkstatt', 'Werkstatt'],\n  ['wohnzimmer', 'Wohnzimmer'],\n  ['hausfassade', 'Hausfassade'],\n  ['fassade', 'Hausfassade']\n]);\n\nfunction key(value) {\n  return String(value || '')\n    .trim()\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00e4/g, 'ae')\n    .replace(/\u00f6/g, 'oe')\n    .replace(/\u00fc/g, 'ue')\n    .replace(/\u00df/g, 'ss')\n    .replace(/[^a-z]/g, '');\n}\n\nfunction normalizeSetting(value) {\n  const k = key(value);\n  if (!k || k === 'unsicher' || k === 'unknown' || k === 'unklar') return '';\n  return allowed.get(k) || '';\n}\n\nlet rawAnswer = '';\nlet description = '';\n\nif (prev.skipGemini) {\n  rawAnswer = prev.description;\n  description = normalizeSetting(prev.description);\n} else if (geminiResponse && geminiResponse.candidates && geminiResponse.candidates[0]) {\n  const parts = geminiResponse.candidates[0].content ? geminiResponse.candidates[0].content.parts : [];\n  const textPart = parts.find(p => p.text);\n  if (textPart) {\n    rawAnswer = textPart.text.trim();\n    description = normalizeSetting(rawAnswer);\n  }\n}\n\nif (!description) {\n  return [];\n}\n\nreturn [{ json: {\n  description,\n  rawDescriptionAnswer: rawAnswer || description,\n  descriptionSource: prev.descriptionSource || (prev.skipGemini ? 'deterministic' : 'gemini_environment_classifier'),\n  usage: prev.usage || prev.parsedUsage,\n  cardId: prev.cardId\n} }];"
      },
      "id": "n18",
      "name": "Parse Answer & Write",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        1800
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Parse Answer & Write').first().json.cardId }}/customField/67b714e5a7b5e806684eb8be/item",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ value: { text: $('Parse Answer & Write').first().json.usage } }) }}",
        "options": {}
      },
      "id": "n20",
      "name": "Write Usage to Trello",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2688,
        1800
      ],
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $(\"Extract & Validate\").item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: \"\u274c [\" + (($(\"Extract & Validate\").item.json.retryCount || 0) + 1) + \"/3] \" + (String($json.error || \"\").toLowerCase().includes(\"too many\") ? \"Rate Limit\" : String($json.error || \"\").toLowerCase().includes(\"quota\") ? \"Quota\" : String($json.error || \"\").toLowerCase().includes(\"unauthorized\") || String($json.error || \"\").toLowerCase().includes(\"permission\") ? \"Auth Fehler\" : \"Upload Fail\") + \" \u00b7 \" + $(\"Extract & Validate\").item.json.cardName }) }}",
        "options": {}
      },
      "id": "n21",
      "name": "Set Error Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3424,
        1032
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $json.cardId }}/idLabels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ value: '69e72e4817206131923d4fcc' }) }}",
        "options": {}
      },
      "id": "n23",
      "name": "Set Processing Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        896,
        864
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Computes mockup slots from Extract & Validate attachment snapshot.\n// No Trello API calls and no secret material.\n\nconst MAX_TOTAL = 8;\n\nfunction withoutSecretFields(value) {\n  const data = { ...value };\n  for (const field of Object.keys(data)) {\n    if (/^api(key|token)$/i.test(field)) delete data[field];\n  }\n  return data;\n}\n\nconst eaItems = $('Extract & Validate').all();\nconst out = [];\n\nfor (let i = 0; i < eaItems.length; i++) {\n  const item = eaItems[i];\n  const data = withoutSecretFields(item.json);\n  const plans = Array.isArray(data.slotPlans) ? data.slotPlans : [];\n  const hasTabletopPlan = plans.some(plan => plan && plan.tabletopDevice === true);\n  const normalPlans = plans.filter(plan => plan && plan.tabletopDevice !== true);\n  const maxTotal = Number.isFinite(Number(data.maxNormalMockupSlots)) ? Number(data.maxNormalMockupSlots) : MAX_TOTAL;\n  const freeSlots = Math.min(maxTotal, normalPlans.length);\n  data.deletedMockups = 0;\n  data.deleteErrors = [];\n  data.liveCheckFreieSlots = freeSlots;\n\n  if (freeSlots === 0 && !hasTabletopPlan) continue;\n\n  let emitted = 0;\n  for (const plan of plans) {\n    if (!plan || !plan.sourceDownloadUrl) continue;\n    const isTabletop = plan.tabletopDevice === true;\n    if (!isTabletop) {\n      if (emitted >= freeSlots) continue;\n      if (typeof plan.slot !== 'number') continue;\n      emitted++;\n    }\n\n    out.push({\n      json: {\n        ...data,\n        mockupSlot: plan.slot,\n        variantSlot: isTabletop ? 99 : (plan.variantSlot || (((plan.slot - 1) % 3) + 1)),\n        tabletopDevice: isTabletop,\n        outputFileName: plan.outputFileName,\n        sourceIndex: plan.sourceIndex || Math.floor((plan.slot - 1) / 3) + 1,\n        sourceDownloadUrl: plan.sourceDownloadUrl,\n        sourceName: plan.sourceName,\n        sourceId: plan.sourceId,\n        sourceBaseName: plan.sourceBaseName,\n        aiMockupFileName: plan.aiMockupFileName,\n        aiMockupPublicUrl: plan.aiMockupPublicUrl,\n        linkedItemIndex: plan.linkedItemIndex,\n        linkedItemTitle: plan.linkedItemTitle,\n        requestedLightColor: plan.requestedLightColor || '',\n        colorBlock: plan.colorBlock || '',\n        backboard: plan.backboard || data.backboard,\n        backboardPromptBlock: plan.backboardPromptBlock || data.backboardPromptBlock,\n        promptHints: plan.promptHints || data.promptHints,\n      },\n      pairedItem: { item: i },\n    });\n  }\n}\n\nreturn out;\n"
      },
      "id": "n25",
      "name": "Prep Mockups",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        864
      ]
    },
    {
      "parameters": {
        "batchSize": 2,
        "options": {}
      },
      "id": "n29",
      "name": "Loop Over Slots",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1568,
        864
      ]
    },
    {
      "parameters": {
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/attachments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "id,name,mimeType,date"
            }
          ]
        },
        "options": {}
      },
      "id": "n30",
      "name": "Get Card Attachments",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2016,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/69e72e4817206131923d4fcc",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "n31",
      "name": "Remove Processing Label - Success",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2688,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/69e72e4817206131923d4fcc",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "n32",
      "name": "Remove Processing Label - Error",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3712,
        1032
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Cover = generated AI mockup image. Prefer the first generated image for the\n// first source mockup, e.g. Mockup4222_ai_1.jpg. Never use source mockups.\nfunction attachmentItems() {\n  const out = [];\n  for (const item of $input.all()) {\n    const value = item && item.json;\n    if (!value) continue;\n\n    if (Array.isArray(value)) {\n      out.push(...value);\n      continue;\n    }\n\n    if (Array.isArray(value.attachments)) {\n      out.push(...value.attachments);\n      continue;\n    }\n\n    if (value.id && (value.name || value.fileName)) {\n      out.push(value);\n    }\n  }\n  return out.filter(Boolean);\n}\n\nfunction isImage(attachment) {\n  const mime = String(attachment.mimeType || '').toLowerCase();\n  if (mime) return mime.startsWith('image/');\n  const name = String(attachment.name || attachment.fileName || '');\n  return /\\.(jpe?g|png|webp)$/i.test(name);\n}\n\nfunction aiMockupParts(attachment) {\n  const names = [attachment.name, attachment.fileName].filter(Boolean);\n  for (const raw of names) {\n    const match = String(raw).trim().match(/^(Mockup[\\d_.]+)_ai_(\\d+)\\.jpe?g$/i);\n    if (match) {\n      return {\n        base: match[1].replace(/^mockup/i, 'Mockup'),\n        variant: parseInt(match[2], 10),\n        fileName: String(raw).trim()\n      };\n    }\n  }\n  return null;\n}\n\nfunction timestamp(attachment) {\n  const raw = attachment.date || attachment.dateLastActivity || attachment.createdAt || '';\n  const parsed = raw ? Date.parse(raw) : NaN;\n  return Number.isFinite(parsed) ? parsed : 0;\n}\n\nconst extract = $('Extract & Validate').first().json || {};\nconst plans = Array.isArray(extract.slotPlans) ? extract.slotPlans : [];\nconst wantedNames = plans\n  .slice()\n  .sort((a, b) => (a.linkedItemIndex ?? 999) - (b.linkedItemIndex ?? 999) || (a.variantSlot ?? 999) - (b.variantSlot ?? 999))\n  .map(plan => String(plan.aiMockupFileName || '').toLowerCase())\n  .filter(Boolean);\n\nconst attachments = attachmentItems();\nconst generated = attachments\n  .map(attachment => ({ attachment, parts: aiMockupParts(attachment) }))\n  .filter(item => item.attachment && item.attachment.id && isImage(item.attachment) && item.parts)\n  .sort((a, b) => {\n    const aWanted = wantedNames.indexOf(String(a.parts.fileName || '').toLowerCase());\n    const bWanted = wantedNames.indexOf(String(b.parts.fileName || '').toLowerCase());\n    const aRank = aWanted === -1 ? 9999 : aWanted;\n    const bRank = bWanted === -1 ? 9999 : bWanted;\n    if (aRank !== bRank) return aRank - bRank;\n    if (a.parts.base !== b.parts.base) return a.parts.base.localeCompare(b.parts.base, 'de');\n    if (a.parts.variant !== b.parts.variant) return a.parts.variant - b.parts.variant;\n    const byDate = timestamp(b.attachment) - timestamp(a.attachment);\n    if (byDate !== 0) return byDate;\n    return String(b.attachment.id || '').localeCompare(String(a.attachment.id || ''));\n  });\n\nconst selected = generated[0] || null;\nconst cover = selected ? selected.attachment : null;\n\nreturn [{ json: {\n  coverId: cover ? cover.id : null,\n  coverName: cover ? (cover.name || cover.fileName || null) : null,\n  coverSourceBase: selected ? selected.parts.base : null,\n  coverVariant: selected ? selected.parts.variant : null,\n  coverDate: cover ? (cover.date || null) : null,\n  coverFound: Boolean(cover && cover.id),\n  attachmentCount: attachments.length,\n  generatedCandidateCount: generated.length,\n} }];\n"
      },
      "id": "n34",
      "name": "Find Cover Attachment",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        144
      ]
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/695e2a57bbd748b676988739",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "n35",
      "name": "Remove RETRY Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3136,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/69e9e3ce317fe0ab8582be18",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "n36",
      "name": "Remove Video Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3424,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://klibiejfisijpagzkxls.supabase.co/storage/v1/object/pandadoc-mockups/{{ $('Extract & Validate').first().json.requestId }}/{{ $('Loop Over Slots').item.json.aiMockupFileName }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "image/jpeg"
            },
            {
              "name": "x-upsert",
              "value": "true"
            }
          ]
        },
        "sendBody": true,
        "contentType": "binaryData",
        "inputDataFieldName": "mockupImage",
        "options": {}
      },
      "id": "sup-upload",
      "name": "Upload to Supabase Storage",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3136,
        336
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000,
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://klibiejfisijpagzkxls.supabase.co/rest/v1/followup_queue?request_id=eq.{{ $('Extract & Validate').first().json.requestId }}&status=eq.pending",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify((() => { const plans = ($('Extract & Validate').first().json.slotPlans || []); const urls = plans.map(p => p.aiMockupPublicUrl).filter(Boolean); return { mockup_url: urls[0] || null, mockup_url_2: urls[1] || null, mockup_url_3: urls[2] || null, updated_at: $now.toISO() }; })()) }}",
        "options": {}
      },
      "id": "fq-update",
      "name": "Update FU Queue",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4608,
        1440
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000,
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "=https://api.trello.com/1/boards/{{ $('Extract & Validate').item.json.boardId }}/labels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "name,color"
            },
            {
              "name": "limit",
              "value": "1000"
            }
          ]
        },
        "options": {}
      },
      "id": "8593d2c0-648f-4bbd-97c1-0429ccb0d229",
      "name": "Get Board Labels for Uploaded",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2912,
        144
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/695e2a57bbd748b676988739",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "0cf956ea-81b7-4958-bec5-047a05a6ad5a",
      "name": "Remove RETRY Label - Error",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3936,
        1032
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/69e72e4817206131923d4fcc",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "39c5b157-135d-4d4e-9201-98d48055bf41",
      "name": "Skip - Gemini Not Ready",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3712,
        720
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "is-retry",
              "leftValue": "={{ $('Extract & Validate').item.json.isRetry }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "743e4679-94b0-42bf-9005-e0a35129668f",
      "name": "IF - Is Retry Run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3136,
        528
      ]
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels/695e2a57bbd748b676988739",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "196a691e-e91e-4a6a-896f-894d04fa0fd9",
      "name": "Remove RETRY Label - Start",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1120,
        864
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: \"\u23f8\ufe0f KI Mockup pausiert \u00b7 \" + $(\"Extract & Validate\").item.json.cardName }) }}",
        "options": {}
      },
      "id": "4fb0a883-b7af-46fc-ba6e-49234fabc16a",
      "name": "Set Waiting Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3424,
        720
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "variant-slot-1-to-3-gemini-a",
              "leftValue": "={{ $json.variantSlot || $json.mockupSlot }}",
              "rightValue": 3,
              "operator": {
                "type": "number",
                "operation": "lte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9b2e0715-24c8-4ed9-a5c5-940cd34c1cdf",
      "name": "Slot Router (A/B)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2240,
        792
      ]
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "edit",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-pro-image"
        },
        "prompt": "=WICHTIGSTE REGEL:\nDas Leuchtschild aus dem Input-Bild MUSS UNVER\u00c4NDERT bleiben.\n\u00c4ndere AUSSCHLIESSLICH die Umgebung um das Schild herum.\n\nNiemals Text, Buchstaben, Schriftart, Logo, Form, Proportionen oder Design-Element des Schildes ver\u00e4ndern.\nNiemals eigenen Text hinzuf\u00fcgen oder bestehenden Text ersetzen.\nWelche Teile leuchten bzw. nicht leuchten, bleibt EXAKT wie im Input.\n\n\nWICHTIG BEI TECHNISCHEN REFERENZBILDERN:\nDas Input-Bild ist eine technische Vorlage.\nAlle Masse, Preise, Codes, Labels oder Zusatztexte geh\u00f6ren NICHT zum Schild und m\u00fcssen vollst\u00e4ndig entfernt werden.\n\nNur die tats\u00e4chliche Form und das Design des Schildes d\u00fcrfen \u00fcbernommen werden.\n\nWenn das Input mehrere Gr\u00f6ssen-Varianten desselben Schildes nebeneinander zeigt:\nRendere NUR EIN Schild im finalen Mockup (das gr\u00f6sste / prominenteste).\nNiemals mehrere Gr\u00f6ssen nebeneinander darstellen.\n\n\nAUFGABE:\nPlatziere das Schild in einer realistischen {{ $('Extract & Validate').item.json.usage }} Umgebung\n(Ort: {{ $('Extract & Validate').item.json.description }}).\nDas Ergebnis soll wie ein hochwertiges, fotorealistisches Werbebild aussehen.\n\n\nKAMERA \u2014 VARIANT B:\n- 15\u00b0 schr\u00e4g von RECHTS\n- nat\u00fcrliche Augenh\u00f6he\n- sichtbare Tiefe (Kantendicke, Wandabstand)\n- realistischer Schattenwurf\n- nicht frontal\n- Schild muss klar lesbar bleiben\n\n\nUMGEBUNG & LICHT:\n- realistisches Setting passend zu {{ $('Extract & Validate').item.json.description }}\n- kein Studio-Freisteller\n- nat\u00fcrliche Lichtstimmung (Tageslicht oder warmes Innenlicht)\n- Glow des Schildes wirkt realistisch auf Wand und Umgebung\n- leichte Tiefensch\u00e4rfe: Schild scharf, Hintergrund weich\n\n\nFARBE (SEHR WICHTIG):\nOberfl\u00e4che und Licht sind zwei getrennte Ebenen.\n\n- Oberfl\u00e4chenfarbe bleibt exakt wie im Input\n- Lichtfarbe bleibt exakt wie im Input\n- NIEMALS Lichtfarbe aus der Oberfl\u00e4chenfarbe ableiten\n\nBeispiel:\nRoter Buchstabe + warmweisses Licht = roter Buchstabe + weisser Glow (nicht rot)\n\n\n{{ $('Extract & Validate').item.json.promptHints || '' }}\n\n\n{{ $('Extract & Validate').item.json.backboardPromptBlock || '' }}\n\n\nMONTAGE:\nSchild realistisch befestigt.\nKeine Kabel sichtbar.\nNicht schwebend.\nNicht auf Glas.",
        "images": {
          "values": [
            {}
          ]
        },
        "options": {
          "binaryPropertyOutput": "mockupImage"
        }
      },
      "id": "234b7dae-ab5e-404e-b6f2-fa3b73342583",
      "name": "Gemini Image Edit (Variant B)",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1.1,
      "position": [
        2464,
        768
      ],
      "retryOnFail": false,
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "qc-problem",
              "leftValue": "={{ $json.problem }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "bd2c4c7c-7157-4f71-8db4-e1892727ebdd",
      "name": "IF QC Problem",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3936,
        1224
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ value: '69ecda6ec3c70150828ccc15' }) }}",
        "options": {}
      },
      "id": "daa97e03-9d23-4b8d-bae9-79a346527f19",
      "name": "Set Manual Review Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4160,
        1440
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "idLabels"
            }
          ]
        },
        "options": {}
      },
      "id": "70459c9d-c2b9-4942-bcf7-9eef52589a62",
      "name": "Get Card Labels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3936,
        144
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "manual-review-check",
              "leftValue": "={{ ($json.idLabels || []).includes('69ecda6ec3c70150828ccc15') }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2ff0cad9-613e-4102-9a8b-aee38b78b3df",
      "name": "IF Manual Review",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4384,
        48
      ]
    },
    {
      "parameters": {
        "jsCode": "// Aggregate Slots \u2014 wartet bis ALLE Slots der Karte durch sind, dann emittet 1 Item.\n// Nutzt $getWorkflowStaticData('node') als persistenter Counter.\n// Funktioniert sowohl wenn Loop done branch 1x mit N items feuert\n// als auch wenn er N mal mit 1 item feuert.\n\nconst items = $input.all();\nif (items.length === 0) return [];\n\nconst sd = $getWorkflowStaticData('node');\nconst extract = $('Extract & Validate').first().json;\nconst totalSlotsToProcess = Number(extract.totalSlotsToProcess);\nconst totalMockupsNeeded = Number(extract.totalMockupsNeeded);\nconst totalNeeded = Number.isFinite(totalSlotsToProcess) && totalSlotsToProcess > 0\n  ? totalSlotsToProcess\n  : (Number.isFinite(totalMockupsNeeded) && totalMockupsNeeded > 0\n    ? totalMockupsNeeded\n    : (Array.isArray(extract.mockupSlots) ? extract.mockupSlots.length : 2));\nconst reqId = extract.requestId || extract.cardId || 'fallback';\nconst key = 'aggSlots_' + reqId;\n\nsd[key] = (sd[key] || 0) + items.length;\n\nif (sd[key] >= totalNeeded) {\n  delete sd[key];\n  return [items[0]];\n}\nreturn [];"
      },
      "id": "ebddbb35-497b-4001-88b4-43c9ba4768a0",
      "name": "Aggregate Slots",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1792,
        144
      ]
    },
    {
      "parameters": {
        "jsCode": "// Parse QC Result mit Retry-Logik bei leerer GPT-4o Response.\n// Bei leerer Antwort: bis zu 2 Retries via OpenAI Chat Completions API.\n// Wenn alle 3 Versuche leer \u2192 contentNotChecked Flag setzen (anderer Comment-Text downstream).\n\nconst items = $input.all();\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const j = item.json || {};\n\n  // Build hat skip gemacht \u2192 propagiere direkt\n  if (j.skip === true) {\n    out.push({ json: { problem: !!j.problem, found: j.found || [], raw: '' }, pairedItem: item.pairedItem });\n    continue;\n  }\n\n  let text = j?.choices?.[0]?.message?.content\n          || j?.candidates?.[0]?.content?.parts?.[0]?.text\n          || j?.content?.parts?.[0]?.text\n          || j?.text || '';\n\n  // RETRY-LOGIK: Wenn keine Antwort \u2192 bis zu 2 Retries\n  if (!text) {\n    let body = null;\n    try {\n      const buildOut = $('Build Single-Image QC Body').itemMatching\n        ? $('Build Single-Image QC Body').itemMatching(i)\n        : $('Build Single-Image QC Body').first();\n      body = buildOut?.json?.body || null;\n    } catch (e) { /* ignore */ }\n\n    if (body) {\n      for (let attempt = 1; attempt <= 2 && !text; attempt++) {\n        await new Promise(r => setTimeout(r, 800));\n        try {\n          const resp = await this.helpers.requestWithAuthentication.call(this, 'openAiApi', {\n            method: 'POST',\n            url: 'https://api.openai.com/v1/chat/completions',\n            body: body,\n            json: true\n          });\n          text = resp?.choices?.[0]?.message?.content || '';\n        } catch (e) { /* try next */ }\n      }\n    }\n  }\n\n  // Wenn nach Retries IMMER NOCH leer \u2192 contentNotChecked\n  if (!text) {\n    out.push({\n      json: {\n        problem: true,\n        contentNotChecked: true,\n        found: ['OpenAI Vision API antwortete nicht (initial + 2 Retries leer)'],\n        raw: ''\n      },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  // JSON parsen\n  let parsed;\n  try {\n    const clean = String(text).replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```\\s*$/i, '').trim();\n    parsed = JSON.parse(clean);\n  } catch (e) {\n    out.push({\n      json: { problem: true, found: ['QC parse error: ' + e.message + ' | raw: ' + String(text).slice(0, 200)], raw: text },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  out.push({\n    json: {\n      problem: parsed.problem === true,\n      found: Array.isArray(parsed.found) ? parsed.found : [],\n      raw: text\n    },\n    pairedItem: item.pairedItem\n  });\n}\n\nreturn out;"
      },
      "id": "6b04d2ca-34a5-4c25-9946-1abf1ba592f7",
      "name": "Parse QC Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3712,
        1224
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: \"\u26a0\ufe0f MOCKUP PR\u00dcFEN \u00b7 \" + $('Extract & Validate').item.json.cardName }) }}",
        "options": {}
      },
      "id": "ac8a6a5a-e4e0-4723-9c84-65d22c1ec69e",
      "name": "Set Manual Review Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4608,
        240
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Sichert das Source-Bild-Binary unter eigenem Property-Namen,\n// damit es Gemini Image Edit (das binary.data \u00fcberschreibt) \u00fcberlebt.\nreturn $input.all().map(item => ({\n  json: item.json,\n  binary: item.binary ? { ...item.binary, sourceImage: item.binary.data } : item.binary,\n  pairedItem: item.pairedItem\n}));"
      },
      "id": "2ff39553-4289-46d4-9048-e208320a8c9f",
      "name": "Save Source Binary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        792
      ]
    },
    {
      "parameters": {
        "jsCode": "// Liest Source + generiertes Mockup direkt aus Item-Binaries (kein externer Download).\n// Baut OpenAI Chat Completions Body mit Multi-Image Vision.\nconst items = $input.all();\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const binary = item.binary || {};\n\n  if (!binary.sourceImage || !binary.mockupImage) {\n    out.push({\n      json: {\n        skipQC: false,\n        body: null,\n        designMismatch: false,\n        otherIssues: true,\n        designReasons: [],\n        otherReasons: ['QC abort: Source- oder Mockup-Binary fehlt im Item']\n      },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  let sourceBuf, mockupBuf;\n  try {\n    sourceBuf = await this.helpers.getBinaryDataBuffer(i, 'sourceImage');\n    mockupBuf = await this.helpers.getBinaryDataBuffer(i, 'mockupImage');\n  } catch (e) {\n    out.push({\n      json: {\n        skipQC: false,\n        body: null,\n        designMismatch: false,\n        otherIssues: true,\n        designReasons: [],\n        otherReasons: ['QC: Binary nicht lesbar: ' + (e.message || 'unknown')]\n      },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  const sourceBase64 = sourceBuf.toString('base64');\n  const mockupBase64 = mockupBuf.toString('base64');\n  const sourceMime = binary.sourceImage.mimeType || 'image/jpeg';\n  const mockupMime = binary.mockupImage.mimeType || 'image/jpeg';\n\n  // NEONTRIP PRODUCT CONTRACT v1: carry deterministic product rules into visual QC.\n  const productContractContext = [\n    item.json?.signType,\n    item.json?.cardName,\n    item.json?.backboard,\n    item.json?.promptHints\n  ].filter(Boolean).join(' ').toLowerCase();\n  const expectsNonLit = /non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(?:beleuchtung|licht)/i.test(productContractContext);\n  const expectsOpaqueUvBackboard = /uv[-\\s]?print|opaque\\s*backboard|opake?\\s*(?:rueckplatte|ruckplatte|backboard)|solid\\s+opaque\\s+black/i.test(productContractContext);\n  const productContractRules = [\n    'NEONTRIP PRODUCT CONTRACT v1 - AUTHORITATIVE:',\n    'These product contract rules override every generic QC example below, especially generic assumptions about glow or transparent acrylic.',\n    expectsNonLit\n      ? 'NON-LIT CONTRACT: BILD 2 must be completely unilluminated. Any LED/neon tube appearance, emitted light, halo, glow or illuminated face is designMismatch=true.'\n      : 'Illumination must follow the explicit product type and source; do not invent another lighting technology.',\n    expectsOpaqueUvBackboard\n      ? 'NEONTRIP UV-PRINT QC CONTRACT v2: BILD 1 explicitly requires an opaque UV-print panel. In BILD 2 the complete panel silhouette and every source-black area inside it must remain solid opaque black. If wall, window, furniture or scene is visible through any source-black panel area, or the panel becomes clear/transparent/open-frame acrylic, set designMismatch=true. Printed artwork must remain flat and non-illuminated; only separate LED/neon lines may glow.'\n      : 'BACKBOARD CONTRACT: preserve the source and explicit Trello backboard construction; do not invent or remove a plate.'\n  ].join('\\n');\n\n  const promptText = `Du bist QC-Pr\u00fcfer f\u00fcr NEONTRIP Mockup-Bilder. Antworte AUSSCHLIESSLICH als JSON (kein Markdown).\n\nDu bekommst ZWEI Bilder, klar markiert mit \"BILD 1\" und \"BILD 2\":\n\n- BILD 1 = Source / Vorlage (Spec-Sheet, technische Skizze). NUR als Referenz f\u00fcr das gew\u00fcnschte Schild-Design. WIRD NICHT BEWERTET.\n- BILD 2 = Mockup (das fertige Render-Ergebnis). NUR DIESES Bild wird auf Probleme gepr\u00fcft.\n\nWICHTIG: Inhalte die NUR in BILD 1 stehen (Bemassungen, Preise, Hersteller-Codes auf dem Spec-Sheet, mehrere Gr\u00f6ssen-Varianten nebeneinander, technische Annotationen) sind KEINE Probleme. Sie geh\u00f6ren zur Vorlage. Probleme nur melden, wenn sie tats\u00e4chlich in BILD 2 sichtbar sind.\n\n${productContractRules}\n\nNEONTRIP DESIGN GEOMETRY CHECK v2 - STRICT AND FAIL-CLOSED:\nMentally rectify only the global camera perspective and uniform scale, then compare the complete two-dimensional front-face design geometry in BILD 1 and BILD 2.\nCompare exact glyph/logo silhouettes and topology: outlines, counters/openings, curves, corners, serifs, stroke paths, endpoints, line breaks, kerning, spacing, relative stroke-thickness ratios, icon positions, distances between components, component count, and relative proportions.\nAny visible local change, even slight, is a design mismatch. Examples include a subtly wider letter, changed curve, shifted icon, altered gap, closed opening, missing connector, different line ending, changed stroke-thickness ratio, or locally stretched element.\nIgnore only global uniform scale, global position, camera rotation/perspective, physical extrusion depth, scene/render style, and glow softness or intensity. These ignored effects must never be used to excuse a changed front-face silhouette or spacing.\nIf the comparison is uncertain because the generated design is obscured, cropped, blurred, or too oblique, set otherIssues to true; never approve by guessing.\n\n\nGESICHT / SMILEY / FIGUREN-CHECK (kritisch):\n- Wenn BILD 2 Augen, Mund, Gesicht, Smiley, Emoji-artige Gesichtsz\u00fcge, cartoonartige Mimik oder eine Figur zeigt, die in BILD 1 nicht eindeutig Teil des Schild-Designs ist \u2192 designMismatch TRUE.\n- Wenn ein abstraktes Logo, Buchstabe oder Schriftzug aus BILD 1 in BILD 2 als Gesicht/Maskottchen/Smiley interpretiert wurde \u2192 designMismatch TRUE.\n- Wenn BILD 1 ein neutrales Logo/Text zeigt, darf BILD 2 keine Augen, keinen Mund und keine Gesichtssymmetrie hinzuf\u00fcgen.\n- Reason-Format: \"Mockup f\u00fcgt Gesicht/Smiley-Elemente hinzu, die in der Source nicht vorhanden sind\".\n\nSCHRIFTART-CHECK (kritisch \u2014 h\u00e4ufigster Fehler!):\n\nVergleiche die EXAKTE Schriftart der Buchstaben in BILD 1 vs BILD 2.\n\nNICHT NUR Schriftart-Familie (Serif vs Sans-Serif vs Cursive) pr\u00fcfen, sondern auch:\n- Spezifischer Buchstaben-Stil (eckige Block-Schrift, runde Geometric Sans, gotisch, modern, italic, kursiv, handgemalt, etc.)\n- Buchstaben-Proportionen (schmal/breit, hoch/flach, Strichdicke-Verh\u00e4ltnis)\n- Charakteristische Details (spitze vs abgerundete Ecken, doppelstrichige vs einstrichige Linien, Serifen-An-/Abwesenheit, Italic-Slope)\n\nBei JEGLICHEM erkennbarem Schriftart-Unterschied \u2192 designMismatch TRUE.\nWenn du nicht 100% sicher sagen kannst \"das ist EXAKT dieselbe Schrift\" \u2192 designMismatch TRUE.\nLieber zu streng als zu mild bei Schriftart.\n\nSPEZIAL-CHECK: KALTWEISS vs WARMWEISS Lichtfarbe (h\u00e4ufiger Fehler):\n\n- KALTWEISS = schneewei\u00dfer Schein, KEIN Blaustich, KEIN Gelbstich, rein-neutral-wei\u00df\n- WARMWEISS = 3000K, gelblich-warmer Schein\n\nWenn BILD 1 KALTWEISS (schneewei\u00df) zeigt und BILD 2 WARMWEISS oder Blaustich/Cyan zeigt \u2192 designMismatch TRUE.\nWenn BILD 1 WARMWEISS zeigt und BILD 2 KALTWEISS oder Blaustich zeigt \u2192 designMismatch TRUE.\nWenn BILD 2 einen Blaustich/Cyan-Schimmer hat obwohl BILD 1 schneewei\u00df ist \u2192 designMismatch TRUE.\n\nAndere Farben (rot, blau-Schild, gr\u00fcn, mehrfarbig) NICHT auf Lichtfarbe pr\u00fcfen \u2014 nur Kaltwei\u00df\u2194Warmwei\u00df\u2194Blaustich.\n\nReason-Format f\u00fcr Lichtfarbe:\n\"Lichtfarbe Source = kaltwei\u00df (schneewei\u00df), Mockup = warmwei\u00df\" oder \"Mockup hat Blaustich obwohl Source schneewei\u00df ist\"\n\nLiefere ZWEI getrennte Flags:\n\nA) designMismatch: TRUE wenn das Schild-Design in BILD 2 vom Schild-Design in BILD 1 abweicht:\n- Schild-Symbol weicht ab (z.B. BILD 1 zeigt X \u2192 BILD 2 zeigt Plus, Stern, anderes Symbol)\n- W\u00f6rter / Text-Inhalt anders (z.B. BILD 1 \"OPEN\" \u2192 BILD 2 \"CLOSED\")\n- SCHRIFTART unterschiedlich \u2014 siehe SCHRIFTART-CHECK oben (egal ob nur subtil; bei Unsicherheit IMMER TRUE)\n- BUCHSTABEN-FORM anders (eckig \u2192 schwungvoll, gerade \u2192 schr\u00e4g-kursiv, schmal \u2192 breit)\n- Anzahl Symbole/Buchstaben/W\u00f6rter weicht ab\n- Layout / Grundstruktur fundamental anders (z.B. einzeilig \u2192 zweizeilig, horizontale \u2192 vertikale Anordnung)\n- Kontur / Zuschnitt-Form abweichend (z.B. rechteckig statt cut-to-shape)\n- KALTWEISS\u2194WARMWEISS Lichtfarbe-Mismatch (siehe Spezial-Check oben)\n- Blaustich/Cyan-Schimmer in Mockup obwohl Source schneewei\u00df ist (siehe Spezial-Check oben)\n- hinzugef\u00fcgte Augen, Mund, Gesicht, Smiley oder Maskottchen-Elemente, wenn diese nicht klar in BILD 1 vorhanden sind\n\nB) otherIssues: TRUE wenn BILD 2 (NUR BILD 2) andere Qualit\u00e4tsprobleme hat:\n- Preise, W\u00e4hrungssymbole, Hersteller-Codes (\"GE 0425\"), Bemassungen IM MOCKUP eingebrannt\n- Eingebrannte Spec-Daten IM MOCKUP (\"Product-Typ: neon flex\", \"Material: silicone\")\n- Mehrere Schilder IM MOCKUP statt einem (z.B. Spec-Sheet-Layout statt einzelnes Render)\n- R\u00fcckplattenmaterial, -farbe oder -transparenz widerspricht dem PRODUCT CONTRACT oder BILD 1\n- Erfundene Hardware IM MOCKUP (extra Schrauben, Halterungen, Kabel)\n\nNICHT als problem flaggen:\n- Inhalte die nur in BILD 1 (Source/Spec-Sheet) sichtbar sind \u2014 DIE GEH\u00d6REN ZUR VORLAGE\n- Leuchten/Glow ist nur dann unproblematisch, wenn der PRODUCT CONTRACT kein Non-Lit/Unbeleuchtet verlangt\n- Realistische Umgebung, M\u00f6bel in BILD 2\n- Texte die Teil des Schild-Designs sind (\"OPEN\", Markennamen)\n- WATERMARK / LOGO / DISCLAIMER unten rechts oder am Bildrand: KOMPLETT IGNORIEREN. Egal ob vorhanden oder nicht, egal welcher Text/Logo, egal ob er gl\u00fcht oder flach ist, egal ob der Disclaimer-Text falsch ist \u2014 das wird hier NICHT als Fehler gewertet.\n\nAntwortformat (strikt JSON):\n{\"designMismatch\": true, \"otherIssues\": false, \"designReasons\": [\"Schriftart in Mockup unterscheidet sich von Source: Source = eckige Block-Schrift, Mockup = runde Geometric Sans\"], \"otherReasons\": []}\noder\n{\"designMismatch\": false, \"otherIssues\": true, \"designReasons\": [], \"otherReasons\": [\"Preis '60\u20ac' im Mockup eingebrannt\"]}\noder\n{\"designMismatch\": false, \"otherIssues\": false, \"designReasons\": [], \"otherReasons\": []}`;\n\n  const body = {\n    model: 'gpt-4o',\n    messages: [\n      {\n        role: 'user',\n        content: [\n          { type: 'text', text: promptText },\n          { type: 'text', text: '=== BILD 1 \u2014 SOURCE / VORLAGE (NICHT bewerten, nur als Vergleichs-Referenz nutzen): ===' },\n          { type: 'image_url', image_url: { url: 'data:' + sourceMime + ';base64,' + sourceBase64, detail: 'high' } },\n          { type: 'text', text: '=== BILD 2 \u2014 MOCKUP (das fertige Render-Ergebnis, DIESES Bild bewerten): ===' },\n          { type: 'image_url', image_url: { url: 'data:' + mockupMime + ';base64,' + mockupBase64, detail: 'high' } }\n        ]\n      }\n    ],\n    response_format: { type: 'json_object' },\n    max_tokens: 4096,\n    temperature: 0.1\n  };\n\n  out.push({ json: { body, sourceLoaded: true, mockupLoaded: true }, pairedItem: item.pairedItem });\n}\n\nreturn out;"
      },
      "id": "182195fc-3b46-4ad9-90d6-f2e7e4a92d1a",
      "name": "Build Compare QC Slot Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2912,
        1056
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "89d66a4b-a3cd-4495-8b82-d4af0462a066",
      "name": "Compare QC Slot Vision",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3136,
        1056
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000,
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn items.map(item => {\n  const j = item.json || {};\n\n  // Build-Step hat bereits geflagged\n  if (j.designMismatch === true || j.otherIssues === true) {\n    return {\n      json: {\n        designMismatch: !!j.designMismatch,\n        otherIssues: !!j.otherIssues,\n        designReasons: Array.isArray(j.designReasons) ? j.designReasons : [],\n        otherReasons: Array.isArray(j.otherReasons) ? j.otherReasons : []\n      },\n      pairedItem: item.pairedItem\n    };\n  }\n\n  const text = j?.choices?.[0]?.message?.content\n            || j?.candidates?.[0]?.content?.parts?.[0]?.text\n            || j?.content?.parts?.[0]?.text\n            || j?.text || '';\n\n  if (!text) {\n    return {\n      json: {\n        designMismatch: false,\n        otherIssues: true,\n        designReasons: [],\n        otherReasons: ['QC: keine Vision-Antwort \u2014 Mail blockiert zur Sicherheit']\n      },\n      pairedItem: item.pairedItem\n    };\n  }\n\n  let parsed;\n  try {\n    const clean = String(text).replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```\\s*$/i, '').trim();\n    parsed = JSON.parse(clean);\n  } catch (e) {\n    return {\n      json: {\n        designMismatch: false,\n        otherIssues: true,\n        designReasons: [],\n        otherReasons: ['QC parse error: ' + e.message]\n      },\n      pairedItem: item.pairedItem\n    };\n  }\n\n  return {\n    json: {\n      designMismatch: parsed.designMismatch === true,\n      otherIssues: parsed.otherIssues === true,\n      designReasons: Array.isArray(parsed.designReasons) ? parsed.designReasons : [],\n      otherReasons: Array.isArray(parsed.otherReasons) ? parsed.otherReasons : []\n    },\n    pairedItem: item.pairedItem\n  };\n});"
      },
      "id": "f14253a2-87a0-4668-b6d5-22341ba4b051",
      "name": "Parse Compare QC Slot",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3424,
        1512
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "design-mismatch",
              "leftValue": "={{ $json.designMismatch }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3bfbd871-7b81-409b-88d4-32bbc2650434",
      "name": "IF Design Mismatch",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3712,
        1512
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "other-issues",
              "leftValue": "={{ $json.otherIssues }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3a20325e-b8db-42ce-a6c3-983daf22540f",
      "name": "IF Other Issues",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3936,
        1608
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/idLabels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ value: '69ecfa31a36fbbc35b69e217' }) }}",
        "options": {}
      },
      "id": "843ed873-a6e8-4916-abfc-d276091e0a63",
      "name": "Set Design Issue Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3936,
        1416
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "design-issue-label",
              "leftValue": "={{ ($json.idLabels || []).includes('69ecfa31a36fbbc35b69e217') }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "82bc8842-e5e6-493f-9566-7c1fb6eb94ba",
      "name": "IF Design Issue",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4160,
        144
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: \"\ud83c\udfa8 DESIGN ABWEICHUNG \u00b7 \" + $('Extract & Validate').item.json.cardName }) }}",
        "options": {}
      },
      "id": "2550872a-1c88-4427-ae76-bca1fc678918",
      "name": "Set Design Issue Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4384,
        456
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "e21fde7a-aa23-4bb2-aa52-2eb3f0f43a8a",
      "name": "Inject Source Binary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2688,
        1056
      ],
      "parameters": {
        "jsCode": "// Holt das Source-Binary aus dem Save Source Binary Node (vor Gemini)\n// und f\u00fcgt es als binary.sourceImage zum aktuellen Item (post-Gemini) hinzu.\n// Filesystem-v2 Storage bleibt f\u00fcr die ganze Execution erreichbar.\nconst items = $input.all();\nlet sourceItems;\ntry {\n  sourceItems = $('Save Source Binary').all();\n} catch (e) {\n  sourceItems = [];\n}\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const sourceItem = sourceItems[i] || sourceItems[0];\n  const sourceJson = (sourceItem && sourceItem.json) || {};\n  const sourceBinaryDesc = sourceItem && sourceItem.binary\n    ? (sourceItem.binary.sourceImage || sourceItem.binary.data)\n    : null;\n  const merged = {\n    json: { ...sourceJson, ...(item.json || {}) },\n    binary: { ...(item.binary || {}) },\n    pairedItem: item.pairedItem\n  };\n  if (sourceBinaryDesc) {\n    merged.binary.sourceImage = sourceBinaryDesc;\n  }\n  out.push(merged);\n}\nreturn out;"
      }
    },
    {
      "id": "0a34ec86-e3de-47f9-95de-10d61671227e",
      "name": "IF Limit Reached",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1040,
        980
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "limit-reached",
              "leftValue": "={{ $json.limitReached }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "8aeaed61-2105-48bc-a37e-c614dc01276c",
      "name": "Comment Design Issue Reason",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4160,
        1620
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/actions/comments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: '\ud83c\udfa8 DESIGN ABWEICHUNG \u2014 Schild weicht vom Source-Bild ab:\\n\\n' + (($('Parse Compare QC Slot').first().json.designReasons || []).map(r => '- ' + r).join('\\n')) }) }}",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "63cde822-f6aa-47b5-bf2a-34c342958db1",
      "name": "Comment Manual Review Reason (Compare-QC)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4160,
        1700
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/actions/comments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: '\u26a0\ufe0f MOCKUP PR\u00dcFEN \u2014 Compare-QC hat Probleme im Mockup gefunden:\\n\\n' + (($('Parse Compare QC Slot').first().json.otherReasons || []).map(r => '- ' + r).join('\\n')) }) }}",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "feb844ac-5eee-4311-84a4-7c6f37622c02",
      "name": "Comment Manual Review Reason (Single-Image)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4160,
        1340
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/actions/comments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: ($('Parse QC Result').first().json.contentNotChecked ? '\u26a0\ufe0f MOCKUP-CONTENT NICHT GEPR\u00dcFT \u2014 Single-Image QC konnte nicht ausgef\u00fchrt werden:\\n\\n' : '\u26a0\ufe0f MOCKUP PR\u00dcFEN \u2014 QC-Check hat Probleme im Mockup gefunden:\\n\\n') + (($('Parse QC Result').first().json.found || []).map(r => '- ' + r).join('\\n')) }) }}",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "da0f52a5-5332-4302-9f46-d2a87786df77",
      "name": "Remove Manual Review Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1216,
        700
      ],
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/idLabels/69ecda6ec3c70150828ccc15",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "365ad11c-2f1d-46e5-956c-57f0339b84a3",
      "name": "Remove Design Issue Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1280,
        800
      ],
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/idLabels/69ecfa31a36fbbc35b69e217",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "0d1db80f-eb41-4ad6-a297-e89d25406fb3",
      "name": "Download Source for Detect",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2016,
        380
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Extract & Validate').first().json.slotPlans[0].sourceDownloadUrl }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": false,
        "options": {
          "redirect": {
            "redirect": {
              "maxRedirects": 5
            }
          },
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "be31743c-d583-41f7-8f72-4da04b10eac7",
      "name": "Build Detect Design Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        380
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  if (!item.binary || !item.binary.data) {\n    out.push({ json: { skip: true, reason: 'Source-Binary fehlt' }, pairedItem: item.pairedItem });\n    continue;\n  }\n  let buf;\n  try {\n    buf = await this.helpers.getBinaryDataBuffer(i, 'data');\n  } catch (e) {\n    out.push({ json: { skip: true, reason: 'Binary nicht lesbar: ' + (e.message || 'unknown') }, pairedItem: item.pairedItem });\n    continue;\n  }\n  const b64 = buf.toString('base64');\n  const mime = item.binary.data.mimeType || 'image/jpeg';\n\n  const promptText = `Du analysierst das Source-Bild eines LED-Schild-Designs. Sag mir KURZ was zu sehen ist.\\n\\nAntwortformat: Genau eine Zeile, beginnend mit \"Design: \"\\nNach \"Design: \" MAXIMAL 3 W\u00d6RTER.\\n\\nBeispiele:\\n- \"Design: Amazon\" (bekannte Marke)\\n- \"Design: Schwarzkopf\"\\n- \"Design: Heinebauer GmbH\"\\n- \"Design: Nautic Club\"\\n- \"Design: OPEN\" (Schriftzug erkennbar)\\n- \"Design: Katze\" (Tier/Objekt erkennbar)\\n- \"Design: Rotes Plus\" (Symbol erkennbar)\\n- \"Design: Unbek. Schriftzug\" (Text vorhanden aber nicht zuordenbar)\\n- \"Design: Unbek. Logo\" (Logo-Form aber nicht zuordenbar)\\n- \"Design: Unbek. Logo + Schriftzug\" (beides vorhanden, nicht zuordenbar)\\n\\nREGELN:\\n- MAX 3 W\u00f6rter nach \"Design: \"\\n- Bei bekanntem Markennamen \u2192 nur Markenname (z.B. \"Amazon\", NICHT \"Amazon Logo\")\\n- Bei klarem Text \u2192 den Text (z.B. \"OPEN\", \"Nautic Club\")\\n- Bei klarem Tier/Objekt \u2192 das benennen (z.B. \"Katze\", \"Auto\")\\n- Bei Symbol \u2192 kurz beschreiben (z.B. \"Rotes Plus\")\\n- Wenn unklar ob Marke/Text \u2192 IMMER abk\u00fcrzen mit \"Unbek.\" (NICHT \"Unbekannt\" / \"Unbekannter\" / \"Unbekanntes\"): \"Unbek. Schriftzug\" / \"Unbek. Logo\" / \"Unbek. Logo + Schriftzug\"\\n- Bei Mischung benutze \"+\" (NICHT \"plus\")\\n- Bemassungen, Preise, Codes, Spec-Daten KOMPLETT ignorieren\\n- Material/Farbe nicht erw\u00e4hnen wenn nicht n\u00f6tig (z.B. KEIN \"auf Acrylglas\")\\n- Antworte deutsch`;\n\n  const body = {\n    model: 'gpt-4o',\n    messages: [\n      {\n        role: 'user',\n        content: [\n          { type: 'text', text: promptText },\n          { type: 'image_url', image_url: { url: 'data:' + mime + ';base64,' + b64 } }\n        ]\n      }\n    ],\n    max_tokens: 30,\n    temperature: 0.1\n  };\n\n  out.push({ json: { skip: false, body }, pairedItem: item.pairedItem });\n}\nreturn out;"
      }
    },
    {
      "id": "4873bb64-dc5c-4e8b-b329-816efcc3912d",
      "name": "Detect Design",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2464,
        380
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 60000
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000
    },
    {
      "id": "2cf64c19-1c0a-4945-9842-b8d9e2e8f374",
      "name": "Build Single-Image QC Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3136,
        1224
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  if (!item.binary || !item.binary.mockupImage) {\n    out.push({ json: { skip: true, problem: false, found: ['QC abort: Mockup-Binary fehlt'] }, pairedItem: item.pairedItem });\n    continue;\n  }\n  let mockupBuf;\n  try { mockupBuf = await this.helpers.getBinaryDataBuffer(i, 'mockupImage'); }\n  catch (e) {\n    out.push({ json: { skip: true, problem: false, found: ['QC: Mockup-Buffer nicht lesbar: ' + (e.message || 'unknown')] }, pairedItem: item.pairedItem });\n    continue;\n  }\n  const b64 = mockupBuf.toString('base64');\n  const mime = item.binary.mockupImage.mimeType || 'image/jpeg';\n\n  // NEONTRIP SINGLE-IMAGE PRODUCT CONTRACT v2: never approve the wrong lighting technology or backboard.\n  const singleImageProductContext = [\n    item.json?.signType,\n    item.json?.cardName,\n    item.json?.backboard,\n    item.json?.promptHints\n  ].filter(Boolean).join(' ').toLowerCase();\n  const singleImageExpectsNonLit = /non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(?:beleuchtung|licht)/i.test(singleImageProductContext);\n  const singleImageExpectsOpaqueUvBackboard = /uv[-\\s]?print|uv\\s*druck|opaque\\s*backboard|solid\\s+opaque\\s+black/i.test(singleImageProductContext);\n  const singleImageProductContractRules = [\n    'NEONTRIP SINGLE-IMAGE PRODUCT CONTRACT v2 - AUTHORITATIVE:',\n    singleImageExpectsNonLit\n      ? 'NON-LIT: This sign must be completely unilluminated. Any LED/neon-tube appearance, emitted light, halo, glow, backlighting or illuminated face is a clear problem=true.'\n      : 'Lighting must match the explicit product type. Do not approve a different lighting technology.',\n    singleImageExpectsOpaqueUvBackboard\n      ? 'UV-PRINT: the complete UV-printed panel must be visibly solid and opaque. A transparent/clear/open-frame panel, or background visible through the source-black panel area, is a clear problem=true. Only separate LED/neon lines may glow.'\n      : 'STANDARD LED FLEX WITHOUT UV PRINT: follow the explicit backboard contract; do not invent an opaque dark panel.'\n  ].join('\\n');\n\n\n  const promptText = `${singleImageProductContractRules}\\n\\nDu bist QC-Pr\u00fcfer f\u00fcr ein NEONTRIP Mockup-Bild. Antworte AUSSCHLIESSLICH als JSON.\\n\\nSchau dir das Mockup an. Flagge problem: true wenn Mockup ENTH\u00c4LT:\\n\\n1. UNGEW\u00dcNSCHTE TEXTE/ZAHLEN im Bild eingebrannt:\\n- Preise, W\u00e4hrungssymbole, Geldbetr\u00e4ge\\n- Hersteller-Codes / Artikelnummern (z.B. \"GE 0425\")\\n- Lorem-Ipsum oder Platzhaltertext (z.B. \"LOGO HERE\")\\n- Andere Beschriftungen die nicht Teil des Schild-Designs sind\\n\\n2. STRUKTUR-PROBLEME:\\n- Mehrere Schilder im Bild statt einem\\n- R\u00fcckplattenmaterial, -farbe oder -transparenz widerspricht dem SINGLE-IMAGE PRODUCT CONTRACT; bei UV-Print ist eine transparente/klare/offene R\u00fcckplatte oder sichtbarer Hintergrund durch die opake Druckfl\u00e4che immer problem=true\\n- Erfundene Hardware (extra Schrauben, Halterungen, Kabel)\\n\\nNICHT als problem flaggen:\\n- Texte die Teil des Schild-Designs sind (\"OPEN\", Markennamen, gewollte Logos)\\n- M\u00f6bel, W\u00e4nde, Pflanzen im Hintergrund\\n- Leuchten/Glow ist nur dann unproblematisch, wenn der SINGLE-IMAGE PRODUCT CONTRACT kein Non-Lit/Unbeleuchtet verlangt\\n- WATERMARK / LOGO / DISCLAIMER unten rechts: KOMPLETT IGNORIEREN\\n- Reine Bemassungs-Annotationen / Ma\u00dfangaben in cm (z.B. \"100 cm\", \"52.3 cm\", \"75x50cm\", Pfeil-Linien mit Ma\u00dfzahlen): KOMPLETT IGNORIEREN, auch wenn sie sichtbar im Mockup sind\\n\\nAntwortformat (strikt JSON):\\n{\"problem\": true, \"found\": [\"Preis '60\u20ac' im Bild sichtbar\"]}\\noder\\n{\"problem\": false, \"found\": []}\\n\\nSei NICHT \u00fcberm\u00e4ssig streng. Halluziniere nichts. Nur klare, eindeutige Probleme melden die WIRKLICH im Bild sind.`;\n\n  const body = {\n    model: 'gpt-4o-mini',\n    messages: [\n      { role: 'user', content: [\n        { type: 'text', text: promptText },\n        { type: 'image_url', image_url: { url: 'data:' + mime + ';base64,' + b64 } }\n      ]}\n    ],\n    response_format: { type: 'json_object' },\n    max_tokens: 2048,\n    temperature: 0.1\n  };\n\n  out.push({ json: { skip: false, body }, pairedItem: item.pairedItem });\n}\nreturn out;"
      }
    },
    {
      "id": "40319b49-872b-4cd2-9453-a68606983bc4",
      "name": "Single-Image QC Vision",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3360,
        1224
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 60000
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000
    },
    {
      "id": "351bc846-1232-4106-b93a-e509cbebd74d",
      "name": "Build Variant B Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2400,
        800
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst out = [];\n\n// Bildanalyse ist nur Fallback. Ein expliziter Trello-Farbwert je Design hat Vorrang.\nlet detectedColorBlock = '';\ntry {\n  detectedColorBlock = $('Parse Color Result').first().json?.colorBlock || '';\n} catch (e) {\n  detectedColorBlock = '';\n}\nconst genericColorBlock = `FARBE (SEHR WICHTIG):\nOberfl\u00e4che und Licht sind zwei getrennte Ebenen.\n\n- Oberfl\u00e4chenfarbe bleibt exakt wie im Input\n- Lichtfarbe bleibt exakt wie im Input\n- NIEMALS Lichtfarbe aus der Oberfl\u00e4chenfarbe ableiten`;\n\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const colorBlock = item.json?.colorBlock || detectedColorBlock || genericColorBlock;\n\n  const srcBin = item.binary?.sourceImage || item.binary?.data;\n  if (!srcBin) {\n    out.push({\n      json: { error: 'Source-Binary fehlt (weder sourceImage noch data)' },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  let buf;\n  try {\n    buf = await this.helpers.getBinaryDataBuffer(i, item.binary?.sourceImage ? 'sourceImage' : 'data');\n  } catch (e) {\n    out.push({\n      json: { error: 'Binary nicht lesbar: ' + (e.message || 'unknown') },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  const usage = item.json?.usage || 'Innen';\n  const isTabletopDevice = item.json?.tabletopDevice === true;\n  if (isTabletopDevice) {\n    const source = String(item.json?.designText || item.json?.cardName || 'customer logo/design').slice(0, 260);\n    const promptText = `Create a photorealistic premium product photo of a small acrylic LED tabletop device.\n\nNEONTRIP TABLETOP DESIGN LOCK v2 - HIGHEST PRIORITY:\nUse the exact customer logo/artwork as the immutable engraving geometry. Do not redraw, simplify, typeset, beautify, complete, or reinterpret it.\nPreserve every glyph outline, opening, curve, endpoint, spacing, icon position, component count, and relative proportion exactly.\nThe outer acrylic plate is separate product geometry. Follow the later product rules for that plate, but never let its shape change the engraved customer artwork.\nTechnical measurements, labels, prices, and page backgrounds are not part of the engraving.\nIf exact preservation is not possible, fail without an image instead of inventing or approximating the logo.\n\nUse the supplied customer design as laser engraving inside ONE clear acrylic glass plate inserted into a round cylindrical matte black LED base, like a small circular puck/base from the reference photo.\nThe acrylic plate outer shape must be simple and manufacturable: round, square, or rectangle depending on the design proportions. Never contour-cut, never logo-shaped, never cut-to-shape, never following the exact outside logo silhouette.\nThe engraving and edge light must use exactly one single RGB color only. Do not preserve multiple logo colors.\nThe logo is frosted laser engraving inside acrylic: not miniature neon tubes, not LED strips, not raised luminous wires, not a printed sticker.\nThe base emits subtle edge light upward through the acrylic; the frosted engraving catches the light softly and diffusely.\nShow the device standing on a warm wooden event or dinner table with softly blurred glasses, plates and premium ambient lighting.\nNo visible cable, no power adapter, no wall mounting, no hanging wires, no extra text, no watermark. The base must clearly be round/circular from the camera angle, with a visible curved front edge and circular top rim.\nKeep the product realistic, physically plausible, sharp, and close enough to understand the acrylic plate and LED base.\nReference design: ${source}.`;\n\n    const b64 = buf.toString('base64');\n    const mime = srcBin.mimeType || 'image/jpeg';\n    out.push({\n      json: { promptText, tabletopDevice: true, outputFileName: item.json?.outputFileName || item.json?.aiMockupFileName || 'Tabletop01.png' },\n      binary: {\n        sourceForGpt: { data: b64, mimeType: mime, fileName: 'source.jpg', fileExtension: 'jpg' },\n        data:         { data: b64, mimeType: mime, fileName: 'source.jpg', fileExtension: 'jpg' }\n      },\n      pairedItem: item.pairedItem\n    });\n    continue;\n  }\n\n  const slot = Number(item.json?.variantSlot || item.json?.mockupSlot || 1);\n  const photoAnchor = 'echtes hochwertiges Werbefoto; keine 3D-Visualisierung, kein CGI, keine Totale, keine Menschenmenge; Schild gross, nah und klar lesbar.';\n  const rawUsage = String(item.json?.usage || '').toLowerCase();\n  const rawDescription = String(item.json?.description || '').toLowerCase();\n  const rawSignType = String(item.json?.signType || '').toLowerCase();\n  const rawCardName = String(item.json?.cardName || '').toLowerCase();\n  const context = [rawUsage, rawDescription, rawSignType, rawCardName].join(' ');\n  const backboardPromptBlock = String(item.json?.backboardPromptBlock || '').trim();\n  const productPromptHints = String(item.json?.promptHints || '').trim();\n  const isOutdoor = /(au\u00dfen|aussen|outdoor|outside|exterior|fassade|ladenfront|storefront)/i.test(context);\n  const is3D = /\\b3d\\b|frontlit|backlit|halo|leuchtbuchstaben|buchstaben/i.test(context);\n  const isNonLit = /(non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(beleuchtung|licht))/i.test(context);\n  const wellnessIndoor = /(spa|wellness|beauty|kosmetik|hotel|lobby|praxis|medical|physio|physiotherapie|salon)/i.test(context) && !isOutdoor;\n  const mossAllowed = wellnessIndoor && !isNonLit;\n  const isStandardNeonFlex = !is3D && !/full\\s*glow|ultra\\s*thin|ultrathin|light\\s*box|lightbox|leuchtkasten|double\\s*sided|nasenschild|non\\s*lit|nonlit|unbeleuchtet/i.test(context) && /(led\\s*flex|neon\\s*flex|led\\s*neon|neonschild|neon\\s*sign|front\\s*glow|back\\s*glow)/i.test(context);\n  const neonIndoorScenarios = {\n    1: 'MOCKUP 01 - WANDMONTAGE IM SETTING: ' + photoAnchor + ' Description und Usage bestimmen den Ort, z.B. Indoor-Surfhalle, Restaurant, Studio, Praxis oder Shop. Schild sauber an passender Innenwand montiert, nicht freischwebend, keine Kabel sichtbar. Vollst\u00e4ndig sichtbar und klar lesbar.',\n    2: 'MOCKUP 02 - ABGEH\u00c4NGT VOR FENSTER/GLAS: ' + photoAnchor + ' Im selben Setting aus der Description, mit Drahtseilen sauber vor Fenster, Glasbereich oder heller Raum\u00f6ffnung abgeh\u00e4ngt. Drahtseile realistisch und dezent; keine Kabel, kein Netzteil. Nicht zu seitlich, Schild vollst\u00e4ndig sichtbar.',\n    3: 'MOCKUP 03 - FRONTALE ANSICHT: ' + photoAnchor + ' Relativ frontal, nur minimale Perspektive, damit Logo/Text maximal klar lesbar bleibt. Setting aus Description und Usage ableiten. Keine extreme Seitenansicht.',\n    4: 'MOCKUP 04 - MOOSWAND: ' + photoAnchor + ' Hochwertige echte Indoor-Naturmooswand oder Pflanzenwand als ruhiger Hintergrund. Schild fest auf der Mooswand montiert, nicht schwebend, keine Kabel sichtbar.'\n  };\n  const neonOutdoorScenarios = {\n    1: 'MOCKUP 01 - AUSSEN AN HAUSWAND/FASSADE: ' + photoAnchor + ' Usage Outdoor/Au\u00dfen hat Vorrang: echte Au\u00dfenfassade, Hauswand, Ladenfront, Eingang oder Geb\u00e4udewand. Wetterfest und glaubw\u00fcrdig an der Wand montiert. Keine Mooswand, kein Innenraum.',\n    2: 'MOCKUP 02 - AUSSEN \u00dcBER LADENLOKAL/EINGANG: ' + photoAnchor + ' Passend zur Description als Au\u00dfenwerbung \u00fcber T\u00fcr, Schaufenster, Eingang oder Ladenfront. Schild vollst\u00e4ndig sichtbar, prominent und kaufentscheidend inszeniert. Keine Menschenmenge, keine erfundenen Banner.',\n    3: 'MOCKUP 03 - FRONTALE AUSSENANSICHT: ' + photoAnchor + ' Relativ frontal auf Au\u00dfenwand oder Fassade, damit Logo/Text maximal klar lesbar bleibt. Keine extreme Seitenansicht. Reale Schatten, Wandkontakt und wetterfeste Montage.',\n    4: 'MOCKUP 04 - OUTDOOR-EVENT / AUSSENKONTEXT: ' + photoAnchor + ' Wenn die Description Event, Festival, Terrasse, Outdoor-Bar oder Veranstaltung nahelegt: hochwertiger Outdoor-Event-Kontext. Sonst hochwertige Au\u00dfenwand/Fassade als sichere Alternative. Keine Mooswand, kein Innenraum.'\n  };\n  const isDoubleSided = /double\\s*sided|nasenschild/i.test(context);\n  const isUltraThin = /ultra\\s*thin|ultrathin|acrylic\\s*lightbox|acryl\\s*lightbox/i.test(context);\n  const isLightbox = !isDoubleSided && !isUltraThin && /light\\s*box|lightbox|leuchtkasten/i.test(context);\n  const isFullGlow = /full\\s*glow/i.test(context);\n  const isBacklit3D = is3D && /backlit|r\u00fcckbeleuchtet|rueckbeleuchtet|halo/i.test(context);\n  const isFrontlit3D = is3D && /frontlit|frontbeleuchtet/i.test(context);\n  const productScenario = (() => {\n    if (isDoubleSided) {\n      const scenes = {\n        1: 'DOUBLE SIDED LIGHTBOX - AUSSEN NASENSCHILD FRONT: ' + photoAnchor + ' Echtes beidseitiges Nasenschild au\u00dfen an Ladenfront, stabile seitliche Wandhalterung sichtbar. Immer Au\u00dfenkontext, niemals Mooswand, niemals Drahtseile, niemals Innenraum. Schild vollst\u00e4ndig lesbar, frontal genug, echte Schatten und wetterfeste Montage.',\n        2: 'DOUBLE SIDED LIGHTBOX - SEITLICHE HALTERUNG: ' + photoAnchor + ' Kamera leicht seitlich, damit Wandhalterung und beide leuchtenden Seiten plausibel sichtbar sind. Au\u00dfenfassade, Ladenlokal oder Eingang, keine frei schwebende Box, keine Zusatztexte.',\n        3: 'DOUBLE SIDED LIGHTBOX - FRONTALE AUSSENANSICHT: ' + photoAnchor + ' Relativ frontal an Au\u00dfenwand oder Ladenfront, sehr klare Lesbarkeit. Seitliche Halterung darf sichtbar sein, Schild h\u00e4ngt physisch korrekt an der Wand. Keine Mooswand, keine Drahtseile.',\n        4: 'DOUBLE SIDED LIGHTBOX - ABEND AUSSEN: ' + photoAnchor + ' Au\u00dfenfassade bei D\u00e4mmerung/Abend, gleichm\u00e4\u00dfig beleuchtete beidseitige Lightbox. Reale Wandhalterung, kein Schweben, keine Kabel oder Netzteile.'\n      };\n      return scenes[slot] || scenes[3];\n    }\n    if (isUltraThin || isLightbox || isFullGlow) {\n      const productName = isUltraThin ? 'ULTRA THIN ACRYLIC LIGHTBOX' : (isFullGlow ? 'FULL GLOW LIGHTBOX' : 'LIGHTBOX');\n      const scenes = {\n        1: productName + ' - FLACH AN WAND/FASSADE: ' + photoAnchor + ' D\u00fcnner, gleichm\u00e4\u00dfig leuchtender Lichtk\u00f6rper flach an einer durchgehenden Wand oder Fassade montiert. Keine Drahtseile, keine Mooswand, kein frei schwebendes Schild. Saubere Kanten, echte Schatten, hochwertiges reales Foto.',\n        2: productName + ' - \u00dcBER EINGANG/SHOPFRONT: ' + photoAnchor + ' Als hochwertige Au\u00dfen- oder Innenwerbung \u00fcber Eingang, Empfang oder Shopfront. Gleichm\u00e4\u00dfige Lichtfl\u00e4che, keine Neonr\u00f6hren, keine sichtbaren Kabel. Physisch plausibel befestigt.',\n        3: productName + ' - FRONTALE LESBARE ANSICHT: ' + photoAnchor + ' Relativ frontal, minimale Verzerrung, Logo/Text maximal klar lesbar. Vollst\u00e4ndiger Lichtkasten sichtbar, keine abgeschnittenen R\u00e4nder. Echtes Foto, kein Render.',\n        4: productName + ' - PREMIUM DETAILWINKEL: ' + photoAnchor + ' Milder Seitenwinkel, damit d\u00fcnne Materialtiefe sichtbar wird, aber Logo lesbar bleibt. Wandkontakt und Schatten plausibel, keine Drahtseile, keine Mooswand.'\n      };\n      return scenes[slot] || scenes[3];\n    }\n    if (is3D) {\n      const family = isNonLit ? '3D NON-LIT / UNBELEUCHTET' : (isBacklit3D ? '3D BACKLIT / HALO' : (isFrontlit3D ? '3D FRONTLIT' : '3D LEUCHTBUCHSTABEN'));\n      const wallMaterial = isOutdoor ? 'Au\u00dfenfassade, Ladenfront, Stein, Putz, Beton oder Metallfassade' : 'hochwertige Innenwand aus Putz, Beton, Stein, Holz oder Metall';\n      const lightRule = isNonLit ? 'kein Leuchten, kein Glow, nur echte Materialoberfl\u00e4che und nat\u00fcrliche Schatten' : (isBacklit3D ? 'Halo-Licht strahlt indirekt nach hinten auf die Wand, Front bleibt materialgerecht' : 'Licht strahlt nach vorne, Materialtiefe bleibt sichtbar');\n      const scenes = {\n        1: family + ' - FESTE WANDMONTAGE: ' + photoAnchor + ' ' + wallMaterial + '; alle Buchstaben physisch fest an EINER durchgehenden Wand montiert. Niemals Drahtseile, niemals Deckenabh\u00e4ngung, niemals freischwebend, niemals vor einer Wandkante. ' + lightRule + '.',\n        2: family + ' - MATERIALTIEFE SICHTBAR: ' + photoAnchor + ' Milder 15-25\u00b0 Seitenwinkel, Tiefe und Wandabstand sichtbar, aber Schrift/Logo klar lesbar. Reale Kontakt-Schatten, keine Kabel, keine Netzteile. Keine Mooswand au\u00dfer bei eindeutig passendem Indoor-Wellness/Spa-Kontext.',\n        3: family + ' - FRONTALE ANSICHT: ' + photoAnchor + ' Straight-on frontal view, minimale Perspektivverzerrung, vollst\u00e4ndig lesbar. Alle Elemente sauber an der Wand befestigt, nichts schwebt. ' + (isNonLit ? 'Absolut keine Leuchteffekte.' : 'Korrektes Lichtverhalten passend zu frontlit/backlit.'),\n        4: family + ' - PREMIUM KONTEXT: ' + photoAnchor + ' ' + (isOutdoor ? 'Hochwertiger Au\u00dfenkontext an Fassade oder Eingang, keine Mooswand.' : (isBacklit3D ? 'Optional hochwertige Mooswand nur wenn es realistisch zum Segment passt, sonst Premium-Innenwand.' : 'Premium-Innenwand passend zur Description, keine Drahtseile.')) + ' Foto wirkt wie echte fertige Installation, keine KI-Optik, keine erfundenen Elemente.'\n      };\n      return scenes[slot] || scenes[3];\n    }\n    return null;\n  })();\n  let mockup02Scene;\n  if (isOutdoor) {\n    mockup02Scene = 'MOCKUP 02 - OUTDOOR / FASSADE: ' + photoAnchor + ' Usage hat Vorrang: echte Au\u00dfenfassade, Ladenfront, Eingangswand, Stein-, Beton-, Putz- oder Metallfassade. Niemals Naturmooswand, Pflanzenwand, Spa-Interior, Sofa, Showroom oder Innenraum. Schild fest an EINER durchgehenden vertikalen Au\u00dfenwand montiert, nicht freischwebend, nicht vor der Wand, nicht auf unsichtbarer Halterung. Reale Tageslicht- oder Abend-Fassadenstimmung, echte Schatten und Wandkontakt.';\n  } else if (is3D && !mossAllowed) {\n    mockup02Scene = 'MOCKUP 02 - PREMIUM WANDINSTALLATION: ' + photoAnchor + ' Hochwertige reale Wand aus Putz, Beton, Stein, Holz oder Metall passend zu Ort und Branche. Keine Naturmooswand, keine Pflanzenwand, kein gr\u00fcner Fake-Hintergrund, au\u00dfer eindeutig Indoor-Wellness/Praxis/Spa. 3D-Elemente physisch fest an EINER durchgehenden vertikalen Wand montiert, mit Kontakt-Schatten, Tiefe und realem Wandabstand. Niemals schwebend, niemals vor einer Wandkante, niemals \u00fcber T\u00fcr-/Fensterkante, niemals in offener Luft. Milder 15-25\u00b0 Fotowinkel, Tiefe sichtbar, Logo/Text klar lesbar.';\n  } else {\n    mockup02Scene = 'MOCKUP 02 - PREMIUM INTERIOR: ' + photoAnchor + ' Hochwertige reale Innenwand passend zur Beschreibung; Naturmooswand nur wenn sie wie eine echte, dichte, flache, vollfl\u00e4chige Indoor-Mooswand wirkt. Schild sauber und fest an einer durchgehenden Wand montiert, nicht abgeh\u00e4ngt, nicht freischwebend, keine Drahtseile, keine Stahlseile, keine Deckenabh\u00e4ngung, keine Saugn\u00e4pfe, keine Glasmontage. Nat\u00fcrliche Wandstruktur, hochwertige ruhige Lichtstimmung, keine Zusatztexte.';\n  }\n  const scenarioText = productScenario || (isStandardNeonFlex\n    ? ((isOutdoor ? neonOutdoorScenarios : neonIndoorScenarios)[slot] || (isOutdoor ? neonOutdoorScenarios : neonIndoorScenarios)[4])\n    : slot === 1\n      ? 'MOCKUP 01 - WIE BISHER: Kartenbeschreibung und Usage als wichtigste Umgebungsvorgabe nutzen, keine Sonder-Szene erzwingen, insbesondere keine Messe/Booth/Event-Szene ohne ausdr\u00fccklichen Hinweis; maximal fotorealistisch, nah, vollst\u00e4ndig sichtbar und klar lesbar.'\n      : (slot === 2\n        ? mockup02Scene\n        : 'MOCKUP 03 - WEITERE PREMIUM-ANSICHT: ' + photoAnchor + ' Beschreibung und Usage haben Vorrang: dieselbe Branche, derselbe Ort und dieselbe Nutzung aus der Kartenbeschreibung ableiten. Keine automatische Messe-, Booth-, Event- oder Showroom-Szene; nur wenn die Kartenbeschreibung ausdr\u00fccklich Messe/Messestand/Exhibition/Booth nennt. Hochwertiger realistischer Wand-/Raum-/Fassadenkontext passend zur Beschreibung, anderer ruhiger Fotowinkel als Mockup 01 und 02. Schild bleibt gro\u00df, nah, vollst\u00e4ndig sichtbar und klar lesbar; keine Menschenmenge, keine Banner, keine erfundenen Zusatztexte.'));\n  const promptText = `NEONTRIP DESIGN LOCK v2 - HIGHEST PRIORITY:\nTreat only the actual customer sign/logo artwork in the source as an immutable design layer.\nDo not redraw, reinterpret, beautify, simplify, vectorize, typeset, correct, complete, or recreate it from memory.\nPreserve every glyph outline, counter/opening, curve, corner, serif, stroke path, endpoint, line break, kerning, spacing, icon position, component count, and relative proportion exactly.\nOnly a single uniform scale plus physically correct camera perspective may be applied to the complete design. Never deform individual letters, symbols, lines, or logo parts.\nMaterial depth, mounting, scene, shadows, and glow may be added around the locked front-face design, but must not alter its geometry.\nTechnical measurements, arrows, prices, labels, tables, and page backgrounds are not part of the design and must still be removed.\nIf exact preservation is not possible, fail without an image instead of inventing or approximating any part of the design.\n\nWICHTIGSTE REGEL:\nDas Leuchtschild aus dem Input-Bild MUSS UNVER\u00c4NDERT bleiben.\n\u00c4ndere AUSSCHLIESSLICH die Umgebung um das Schild herum.\n\nNiemals Text, Buchstaben, Schriftart, Logo, Form, Proportionen oder Design-Element des Schildes ver\u00e4ndern.\nNiemals eigenen Text hinzuf\u00fcgen oder bestehenden Text ersetzen.\nWelche Teile leuchten bzw. nicht leuchten, bleibt EXAKT wie im Input.\n\nWICHTIG BEI TECHNISCHEN REFERENZBILDERN:\nDas Input-Bild ist eine technische Vorlage.\nAlle Ma\u00dfe, Ziffern, Ma\u00dflinien, Pfeile, Gr\u00f6\u00dfenangaben, Preise, Codes, Labels, Raster, wei\u00dfen Zeichenfl\u00e4chen oder Zusatztexte geh\u00f6ren NICHT zum Schild und m\u00fcssen vollst\u00e4ndig entfernt werden.\n\u00dcbernimm niemals die technische Zeichnung im unteren Bereich, keine Zahlen, keine cm-Angaben, keine Skizzen, keine Blueprint-Optik.\\nNur die tats\u00e4chliche Schildform, das Logo/Design und die echte Leucht-/Materialwirkung d\u00fcrfen \u00fcbernommen werden.\nWenn das Input mehrere Gr\u00f6ssen-Varianten desselben Schildes nebeneinander zeigt: Rendere NUR EIN Schild im finalen Mockup (das gr\u00f6sste / prominenteste). Niemals mehrere Gr\u00f6ssen nebeneinander darstellen.\n\nAUFGABE:\nPlatziere das Schild in einer realistischen ${usage} Umgebung.\nDas Ergebnis soll wie ein hochwertiges, fotorealistisches Werbebild aussehen.\n\nSLOT-SZENARIO:\n${scenarioText}\n\nFOTOQUALIT\u00c4T:\nMockup 01 ist der Qualit\u00e4tsanker. Dieses Bild muss genauso realistisch, nah und hochwertig aussehen. Keine 3D-Visualisierung, keine Architekturvisualisierung, kein CGI, keine Totale, keine Menschenmenge; Schild gross und klar wie Mockup 01.\n\nKAMERA \u2014 VARIANT B:\n- milder 15\u201325\u00b0 Drei-Viertel-Winkel von RECHTS, sofern das Slot-Szenario keinen anderen Blickwinkel verlangt\n- nat\u00fcrliche Augenh\u00f6he, realistische 35mm- oder 50mm-Fotoperspektive\n- sichtbare Tiefe (Kantendicke, Wandabstand), aber Logo/Text bleibt klar lesbar\n- realistischer Kontakt-Schatten direkt auf der Wand\n- nicht extrem seitlich, nicht gekippt, keine Perspektive bei der man das Design kaum lesen kann\n- Schild muss wie ein echtes Foto einer fertigen Installation aussehen\n\nUMGEBUNG & LICHT:\n- realistisches Setting\n- kein Studio-Freisteller\n- nat\u00fcrliche Raumbeleuchtung der Umgebung\n- Glow des Schildes wirkt realistisch auf Wand und Umgebung\n- leichte Tiefensch\u00e4rfe: Schild scharf, Hintergrund weich\n\n${colorBlock}\n\n${productPromptHints}\n\n${backboardPromptBlock}\n\nNEGATIVREGELN:\\nKeine Ma\u00dfzeichnung, keine cm-Zahlen, keine Pfeile, keine technischen Linien, keine wei\u00dfe Konstruktionsfl\u00e4che, keine Preis-/Code-Texte, keine Skizze aus dem unteren Referenzbereich.\\n\\nMONTAGE: Schild realistisch fest an EINER durchgehenden Wandfl\u00e4che befestigt. Alle Buchstaben/Logo-Elemente haben Wandkontakt, reale Kontakt-Schatten und glaubw\u00fcrdige Tiefe. Keine Kabel sichtbar. Nicht schwebend, nicht freistehend, nicht vor der Wand, nicht auf unsichtbarer Halterung. Nicht auf Glas.`;\n\n  const b64 = buf.toString('base64');\n  const mime = srcBin.mimeType || 'image/jpeg';\n\n  out.push({\n    json: { promptText },\n    binary: {\n      sourceForGpt: { data: b64, mimeType: mime, fileName: 'source.jpg', fileExtension: 'jpg' },\n      data:         { data: b64, mimeType: mime, fileName: 'source.jpg', fileExtension: 'jpg' }\n    },\n    pairedItem: item.pairedItem\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "96f5f5a9-64e5-4d43-a0fa-33eb3b4be398",
      "name": "Variant B Vision",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2624,
        800
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/images/edits",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "model",
              "value": "gpt-image-2"
            },
            {
              "name": "prompt",
              "value": "={{ $json.promptText }}"
            },
            {
              "name": "image",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "sourceForGpt"
            },
            {
              "name": "size",
              "value": "1024x1024"
            },
            {
              "name": "n",
              "value": "1"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000
    },
    {
      "id": "84debf3d-efc3-4462-aae7-a58aadd2e66a",
      "name": "Parse Variant B Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2848,
        800
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn items.map((item, i) => {\n  const j = item.json || {};\n  const data = j?.data?.[0]?.b64_json;\n  if (!data) {\n    return {\n      json: { error: 'Kein Bild in OpenAI Antwort: ' + JSON.stringify(j).slice(0, 300) },\n      pairedItem: item.pairedItem\n    };\n  }\n  return {\n    json: { mimeType: 'image/png' },\n    binary: {\n      mockupImage: {\n        data: data,\n        mimeType: 'image/png',\n        fileName: 'mockup.png',\n        fileExtension: 'png'\n      }\n    },\n    pairedItem: item.pairedItem\n  };\n});"
      }
    },
    {
      "id": "0cea4953-65c0-4719-b556-0d015afae858",
      "name": "Variant B Has Image?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3072,
        800
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "vb-has-img-mimetype",
              "leftValue": "={{ $json.mimeType }}",
              "rightValue": "image/",
              "operator": {
                "type": "string",
                "operation": "startsWith"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "7cc3eaa9-b581-4e12-a21f-01c02f3fbed2",
      "name": "Prep Gemini Fallback",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3296,
        880
      ],
      "parameters": {
        "jsCode": "// Restore binary.data for Gemini langchain Node from Build Variant B Body's output.\n// PLUS: Setze staticData Flag dass Fallback genutzt wurde, damit am Ende ein Trello-Comment dazu gesetzt werden kann.\n\nconst sd = $getWorkflowStaticData('global');\ntry {\n  const ext = $('Extract & Validate').first().json;\n  const reqId = ext.requestId || ext.cardId || 'fallback';\n  sd['fallbackUsed_' + reqId] = true;\n} catch (e) { /* ignore */ }\n\nconst items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  let buildBody;\n  try { buildBody = $('Build Variant B Body').itemMatching(i); }\n  catch (e) { buildBody = $('Build Variant B Body').first(); }\n  const srcBin = buildBody?.binary?.data || buildBody?.binary?.sourceForGpt;\n  if (!srcBin) {\n    out.push({ json: { error: 'Source binary not found for Gemini fallback' }, pairedItem: item.pairedItem });\n    continue;\n  }\n  out.push({ json: item.json || {}, binary: { data: srcBin }, pairedItem: item.pairedItem });\n}\nreturn out;"
      }
    },
    {
      "id": "cc06c3de-0c21-4f54-8e2b-511d09231389",
      "name": "Remove Mockup Uploaded Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1344,
        800
      ],
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/idLabels/69eb6348d13470a412a6ab9b",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "0ce7051c-5336-4b7c-9327-aff219f3b238",
      "name": "Download Source for Color Detect",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1500,
        600
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Extract & Validate').first().json.slotPlans[0].sourceDownloadUrl }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": false,
        "options": {
          "redirect": {
            "redirect": {
              "maxRedirects": 5
            }
          },
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000,
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "33a8930f-5091-42ee-8fb5-8779f3a2e5cd",
      "name": "Build Color Detect Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1700,
        600
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  if (!item.binary?.data) {\n    out.push({ json: { skip: true, body: null }, pairedItem: item.pairedItem });\n    continue;\n  }\n  let buf;\n  try { buf = await this.helpers.getBinaryDataBuffer(i, 'data'); }\n  catch (e) { out.push({ json: { skip: true, body: null }, pairedItem: item.pairedItem }); continue; }\n  const b64 = buf.toString('base64');\n  const mime = item.binary.data.mimeType || 'image/jpeg';\n  const promptText = `Schau dir das Schild im Bild an. Bestimme die Lichtfarbe des Schildes selbst (NICHT des Hintergrunds).\n\nAntworte AUSSCHLIESSLICH als JSON:\n- Schild leuchtet einfarbig KALTWEISS (schneewei\u00df, ohne Blau- oder Gelbstich): {\"color\": \"kaltwei\u00df\"}\n- Schild leuchtet einfarbig WARMWEISS (gelblich, ~3000K): {\"color\": \"warmwei\u00df\"}\n- Schild leuchtet klar GELB oder ZITRONENGELB: {\"color\": \"gelb\"}\n- Schild leuchtet klar GOLDGELB: {\"color\": \"goldgelb\"}\n- Schild leuchtet ORANGE: {\"color\": \"orange\"}\n- Schild leuchtet ROT, BLAU, GR\u00dcN, EISBLAU, PINK, LILA oder T\u00dcRKIS: antworte mit genau dieser Farbe, z. B. {\"color\": \"blau\"}\n- Schild ist mehrfarbig: {\"color\": \"mehrfarbig\"}\n- Bei Unsicherheit: {\"color\": \"unklar\"}`;\n  const body = {\n    model: 'gpt-4o',\n    messages: [\n      { role: 'user', content: [\n        { type: 'text', text: promptText },\n        { type: 'image_url', image_url: { url: 'data:' + mime + ';base64,' + b64 } }\n      ]}\n    ],\n    response_format: { type: 'json_object' },\n    max_tokens: 50,\n    temperature: 0\n  };\n  out.push({ json: { skip: false, body }, pairedItem: item.pairedItem });\n}\nreturn out;"
      }
    },
    {
      "id": "fd9c977a-5128-4cec-a842-f610ab456e90",
      "name": "Detect Light Color",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1900,
        600
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    },
    {
      "id": "ebc395c3-a2b1-4471-bc70-bf33cbf131b5",
      "name": "Parse Color Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2100,
        600
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfunction normalized(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00df/g, 'ss')\n    .trim();\n}\n\nfunction detectedColorBlock(value) {\n  const color = normalized(value);\n  const common = [\n    'FARBE (SEHR WICHTIG):',\n    'Oberfl\u00e4che und Licht sind zwei getrennte Ebenen.',\n    'Die erkannte Lichtfarbe ist nur ein Fallback, wenn kein Trello-Farbwert vorhanden ist.',\n    'Die Lichtfarbe muss in allen Ansichten stabil bleiben; Umgebungslicht und Wei\u00dfabgleich d\u00fcrfen sie nicht ver\u00e4ndern.'\n  ];\n  if (color === 'kaltweiss') {\n    return common.concat([\n      'DAS SCHILD MUSS KALTWEISS LEUCHTEN - etwa 6000 Kelvin, neutral bis k\u00fchl wei\u00df.',\n      'Niemals warmwei\u00df, gelb, creme, beige, amber, orange oder golden rendern; auch keinen Blau-/Cyanstich erfinden.'\n    ]).join('\\n');\n  }\n  if (color === 'warmweiss') {\n    return common.concat([\n      'DAS SCHILD MUSS WARMWEISS LEUCHTEN - etwa 3000 Kelvin, kontrolliert warmwei\u00df.',\n      'Niemals kaltwei\u00df, eisig, blau oder cyan rendern; trotzdem wei\u00dfes Licht, nicht ges\u00e4ttigt gelb oder orange.'\n    ]).join('\\n');\n  }\n  if (color === 'gelb' || color === 'zitronengelb' || color === 'yellow') {\n    return common.concat([\n      'DAS SCHILD MUSS KLAR GES\u00c4TTIGT GELB LEUCHTEN.',\n      'Niemals warmwei\u00df, creme, beige, wei\u00df, amber oder orange rendern. Gelb muss sichtbar gelb bleiben.'\n    ]).join('\\n');\n  }\n  if (color === 'goldgelb' || color === 'golden yellow') {\n    return common.concat([\n      'DAS SCHILD MUSS KLAR GES\u00c4TTIGT GOLDGELB LEUCHTEN.',\n      'Niemals warmwei\u00df, creme, beige, wei\u00df oder orange rendern. Goldgelb muss sichtbar gelb bleiben.'\n    ]).join('\\n');\n  }\n  if (color && color !== 'unklar' && color !== 'farbig' && color !== 'mehrfarbig') {\n    return common.concat([\n      'DAS SCHILD MUSS EXAKT IN DIESER ERKANNTEN FARBE LEUCHTEN: ' + value + '.',\n      'Diese Farbe nicht durch warmwei\u00df, kaltwei\u00df oder eine \u00e4hnliche Farbe ersetzen.'\n    ]).join('\\n');\n  }\n  return common.concat([\n    'Oberfl\u00e4chenfarbe bleibt exakt wie im Input.',\n    'Lichtfarbe und die Zuordnung mehrerer Farben bleiben exakt wie im Input.',\n    'Niemals Gelb zu Warmwei\u00df machen und niemals Warmwei\u00df/Kaltwei\u00df vertauschen.'\n  ]).join('\\n');\n}\n\nreturn items.map((item) => {\n  const text = item.json?.choices?.[0]?.message?.content || '';\n  let color = 'unklar';\n  try { color = JSON.parse(text).color || 'unklar'; }\n  catch (e) { /* Fallback bleibt unklar. */ }\n  return { json: { color: normalized(color), colorBlock: detectedColorBlock(color) }, pairedItem: item.pairedItem };\n});"
      }
    },
    {
      "id": "73e97f06-fa91-48a7-b55f-ebbb0c4d68bd",
      "name": "Check Fallback Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3104,
        144
      ],
      "parameters": {
        "jsCode": "// Check ob Variant B Gemini-Fallback benutzt wurde.\n// Liest staticData Flag (gesetzt von Prep Gemini Fallback) und r\u00e4umt es auf.\n\nconst sd = $getWorkflowStaticData('global');\nlet fallbackUsed = false;\nlet reqId = '';\ntry {\n  const ext = $('Extract & Validate').first().json;\n  reqId = ext.requestId || ext.cardId || 'fallback';\n  if (sd['fallbackUsed_' + reqId] === true) {\n    fallbackUsed = true;\n    delete sd['fallbackUsed_' + reqId];\n  }\n} catch (e) { /* ignore */ }\n\nreturn $input.all().map(item => ({\n  json: { ...(item.json || {}), fallbackUsed, reqId },\n  binary: item.binary,\n  pairedItem: item.pairedItem\n}));"
      }
    },
    {
      "id": "460a822f-abef-4f80-8ab3-13ed800dfe9d",
      "name": "IF Fallback Used?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3296,
        144
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "fb-used",
              "leftValue": "={{ $json.fallbackUsed }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a9063fcc-c980-4776-9177-dc597acfc897",
      "name": "Comment Gemini Fallback",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3488,
        80
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/actions/comments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: '\u2139\ufe0f 2\u00d7 Gemini Mockups uploaded \u2014 gpt-image-2 (Variant B) war nicht verf\u00fcgbar, Gemini-Fallback wurde genutzt.' }) }}",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "fdd7ca0c-2d3d-4a1c-8df0-f48b8997f717",
      "name": "Reduce to Single Item",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        600
      ],
      "parameters": {
        "jsCode": "// Emit a fresh item only. Downstream color detect reads source URL from Extract & Validate.\n// Do not forward Trello DELETE artificial recovered items.\nreturn [{ json: {} }];"
      }
    },
    {
      "id": "qr_autofill_remove_processing",
      "name": "Remove Processing Label - Autofill",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2912,
        1800
      ],
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "parameters": {
        "method": "DELETE",
        "url": "={{ 'https://api.trello.com/1/cards/' + ($json.idModel || $('Parse Answer & Write').first().json.cardId) + '/idLabels/69e72e4817206131923d4fcc' }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "append-title-get-card",
      "name": "Get Card for Design Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4860,
        600
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "name"
            }
          ]
        },
        "options": {
          "timeout": 30000
        }
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "append-title-compute",
      "name": "Compute Design Title",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5080,
        600
      ],
      "parameters": {
        "jsCode": "// Compute idempotent design title without Trello secrets.\nlet extractData = {};\ntry {\n  const ev = $('Extract & Validate').first();\n  if (ev && ev.json) extractData = ev.json;\n} catch (e) {\n  return [{ json: { skip: true, shouldUpdate: false, reason: 'Extract & Validate not executed' } }];\n}\n\nconst cardId = extractData.cardId;\nif (!cardId) {\n  return [{ json: { skip: true, shouldUpdate: false, reason: 'Missing cardId' } }];\n}\n\nlet detectResp = {};\ntry {\n  const dd = $('Detect Design').first();\n  if (dd && dd.json) detectResp = dd.json;\n} catch (e) {\n  return [{ json: { skip: true, shouldUpdate: false, cardId, reason: 'Detect Design not executed' } }];\n}\n\nconst designText = (detectResp?.choices?.[0]?.message?.content || '').trim();\nif (!designText) {\n  return [{ json: { skip: true, shouldUpdate: false, cardId, reason: 'No design text from Detect Design' } }];\n}\n\nconst card = $input.first().json || {};\nif (card.error) {\n  return [{ json: { skip: true, shouldUpdate: false, cardId, designText, reason: 'GET fail: ' + (card.error.message || card.error.description || 'unknown') } }];\n}\n\nlet baseName = card.name || '';\n{\n  let prev = null;\n  while (baseName !== prev) {\n    prev = baseName;\n    baseName = baseName.replace(/\\s*\u00b7\\s*Design:\\s*[^\u00b7]+$/u, '').replace(/\\s+$/, '');\n  }\n}\n\nif (!baseName) {\n  return [{ json: { skip: true, shouldUpdate: false, cardId, designText, reason: 'Empty base name', cardNameLive: card.name } }];\n}\n\nconst newName = baseName + '  \u00b7  ' + designText;\nif (card.name === newName) {\n  return [{ json: { skip: true, shouldUpdate: false, cardId, newName, designText, previousName: card.name, reason: 'Already set' } }];\n}\n\nreturn [{ json: { skip: false, shouldUpdate: true, cardId, newName, designText, previousName: card.name } }];"
      }
    },
    {
      "id": "append-title-if-update",
      "name": "Should Update Design Title?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        5300,
        600
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "should-update-design-title",
              "leftValue": "={{ $json.shouldUpdate }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "append-title-update-card",
      "name": "Update Design Title",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        5520,
        520
      ],
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: $json.newName }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "amount": 20
      },
      "id": "quote-ready-cover-wait-20s",
      "name": "Wait for Trello Attachment Consistency",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1904,
        144
      ]
    },
    {
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "cover-found",
              "leftValue": "={{ $json.coverFound === true && !!$json.coverId }}",
              "operator": {
                "operation": "true",
                "singleValue": true,
                "type": "boolean"
              },
              "rightValue": ""
            }
          ],
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          }
        },
        "options": {}
      },
      "id": "quote-ready-if-cover-found",
      "name": "IF Cover Found",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2400,
        144
      ]
    },
    {
      "parameters": {
        "authentication": "predefinedCredentialType",
        "method": "PUT",
        "nodeCredentialType": "trelloApi",
        "options": {},
        "queryParameters": {
          "parameters": [
            {
              "name": "name",
              "value": "={{ $(\"Extract & Validate\").item.json.cardName }}"
            }
          ]
        },
        "sendQuery": true,
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}"
      },
      "id": "quote-ready-reset-title-only-no-cover",
      "name": "Reset Title Only - No Cover",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2624,
        288
      ],
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "quote-ready-wait-after-uploaded-label",
      "name": "Wait After Uploaded Label",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3024,
        320
      ],
      "parameters": {
        "amount": 5
      }
    },
    {
      "parameters": {
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/attachments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "id,name,mimeType,date"
            }
          ]
        },
        "options": {}
      },
      "id": "quote-ready-get-attachments-after-uploaded",
      "name": "Get Card Attachments After Uploaded Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3248,
        320
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Cover = generated AI mockup image. Prefer the first generated image for the\n// first source mockup, e.g. Mockup4222_ai_1.jpg. Never use source mockups.\nfunction attachmentItems() {\n  const out = [];\n  for (const item of $input.all()) {\n    const value = item && item.json;\n    if (!value) continue;\n\n    if (Array.isArray(value)) {\n      out.push(...value);\n      continue;\n    }\n\n    if (Array.isArray(value.attachments)) {\n      out.push(...value.attachments);\n      continue;\n    }\n\n    if (value.id && (value.name || value.fileName)) {\n      out.push(value);\n    }\n  }\n  return out.filter(Boolean);\n}\n\nfunction isImage(attachment) {\n  const mime = String(attachment.mimeType || '').toLowerCase();\n  if (mime) return mime.startsWith('image/');\n  const name = String(attachment.name || attachment.fileName || '');\n  return /\\.(jpe?g|png|webp)$/i.test(name);\n}\n\nfunction aiMockupParts(attachment) {\n  const names = [attachment.name, attachment.fileName].filter(Boolean);\n  for (const raw of names) {\n    const match = String(raw).trim().match(/^(Mockup[\\d_.]+)_ai_(\\d+)\\.jpe?g$/i);\n    if (match) {\n      return {\n        base: match[1].replace(/^mockup/i, 'Mockup'),\n        variant: parseInt(match[2], 10),\n        fileName: String(raw).trim()\n      };\n    }\n  }\n  return null;\n}\n\nfunction timestamp(attachment) {\n  const raw = attachment.date || attachment.dateLastActivity || attachment.createdAt || '';\n  const parsed = raw ? Date.parse(raw) : NaN;\n  return Number.isFinite(parsed) ? parsed : 0;\n}\n\nconst extract = $('Extract & Validate').first().json || {};\nconst plans = Array.isArray(extract.slotPlans) ? extract.slotPlans : [];\nconst wantedNames = plans\n  .slice()\n  .sort((a, b) => (a.linkedItemIndex ?? 999) - (b.linkedItemIndex ?? 999) || (a.variantSlot ?? 999) - (b.variantSlot ?? 999))\n  .map(plan => String(plan.aiMockupFileName || '').toLowerCase())\n  .filter(Boolean);\n\nconst attachments = attachmentItems();\nconst generated = attachments\n  .map(attachment => ({ attachment, parts: aiMockupParts(attachment) }))\n  .filter(item => item.attachment && item.attachment.id && isImage(item.attachment) && item.parts)\n  .sort((a, b) => {\n    const aWanted = wantedNames.indexOf(String(a.parts.fileName || '').toLowerCase());\n    const bWanted = wantedNames.indexOf(String(b.parts.fileName || '').toLowerCase());\n    const aRank = aWanted === -1 ? 9999 : aWanted;\n    const bRank = bWanted === -1 ? 9999 : bWanted;\n    if (aRank !== bRank) return aRank - bRank;\n    if (a.parts.base !== b.parts.base) return a.parts.base.localeCompare(b.parts.base, 'de');\n    if (a.parts.variant !== b.parts.variant) return a.parts.variant - b.parts.variant;\n    const byDate = timestamp(b.attachment) - timestamp(a.attachment);\n    if (byDate !== 0) return byDate;\n    return String(b.attachment.id || '').localeCompare(String(a.attachment.id || ''));\n  });\n\nconst selected = generated[0] || null;\nconst cover = selected ? selected.attachment : null;\n\nreturn [{ json: {\n  coverId: cover ? cover.id : null,\n  coverName: cover ? (cover.name || cover.fileName || null) : null,\n  coverSourceBase: selected ? selected.parts.base : null,\n  coverVariant: selected ? selected.parts.variant : null,\n  coverDate: cover ? (cover.date || null) : null,\n  coverFound: Boolean(cover && cover.id),\n  attachmentCount: attachments.length,\n  generatedCandidateCount: generated.length,\n} }];\n"
      },
      "id": "quote-ready-find-cover-after-uploaded",
      "name": "Find Cover Attachment After Uploaded Label",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3472,
        320
      ]
    },
    {
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "cover-found",
              "leftValue": "={{ $json.coverFound === true && !!$json.coverId }}",
              "operator": {
                "operation": "true",
                "singleValue": true,
                "type": "boolean"
              },
              "rightValue": ""
            }
          ],
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          }
        },
        "options": {}
      },
      "id": "quote-ready-if-cover-found-after-uploaded",
      "name": "IF Cover Found After Uploaded Label",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3696,
        320
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "name",
              "value": "={{ $(\"Extract & Validate\").item.json.cardName }}"
            },
            {
              "name": "idAttachmentCover",
              "value": "={{ $json.coverId }}"
            }
          ]
        }
      },
      "id": "quote-ready-set-cover-after-uploaded",
      "name": "Set Mockup01 Cover After Uploaded Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3920,
        240
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').first().json.cardId }}/idLabels/69e72e4817206131923d4fcc",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {}
      },
      "id": "remove-processing-label-review-end",
      "name": "Remove Processing Label - Review End",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5860,
        760
      ],
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "resolve-uploaded-label-dynamic",
      "name": "Resolve Mockup Uploaded Label",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3136,
        144
      ],
      "parameters": {
        "jsCode": "const labels = $input.all().flatMap((item) => Array.isArray(item.json) ? item.json : [item.json]);\nconst normalize = (value) => String(value || '').trim().toLowerCase().replace(/\\s+/g, ' ');\nconst aliases = new Set(['mockup uploaded', 'mockup hochgeladen']);\nconst match = labels.find((label) => label && aliases.has(normalize(label.name)));\nconst source = $('Extract & Validate').item.json;\nreturn [{ json: {\n  cardId: source.cardId,\n  boardId: source.boardId,\n  labelFound: Boolean(match && match.id),\n  labelId: match && match.id ? match.id : null,\n  labelName: match && match.name ? match.name : 'Mockup Uploaded'\n} }];"
      }
    },
    {
      "id": "if-uploaded-label-exists",
      "name": "Mockup Uploaded Label Exists?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3360,
        144
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "uploaded-label-found",
              "leftValue": "={{ $json.labelFound }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "add-existing-uploaded-label",
      "name": "Add Existing Mockup Uploaded Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3584,
        64
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $json.cardId }}/idLabels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ value: $json.labelId }) }}",
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "create-uploaded-label-on-card",
      "name": "Create Missing Mockup Uploaded Label",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3584,
        224
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $json.cardId }}/labels",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "name",
              "value": "Mockup Uploaded"
            },
            {
              "name": "color",
              "value": "green"
            }
          ]
        },
        "options": {}
      },
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "if-mockup-finalization-only",
      "name": "Finalization Only?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        784,
        864
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "finalization-only",
              "leftValue": "={{ $json.finalizationOnly === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Quote Ready Cards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Quote Ready Cards": {
      "main": [
        [
          {
            "node": "Extract & Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract & Validate": {
      "main": [
        [
          {
            "node": "IF Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Valid": {
      "main": [
        [
          {
            "node": "Finalization Only?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF needsAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Slots": {
      "main": [
        [
          {
            "node": "Aggregate Slots",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Mockup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Mockup": {
      "main": [
        [
          {
            "node": "Save Source Binary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Waiting Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Gemini Result": {
      "main": [
        [
          {
            "node": "Inject Source Binary",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Single-Image QC Body",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upload to Supabase Storage",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Waiting Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Mockup": {
      "main": [
        [
          {
            "node": "Loop Over Slots",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF - Is Retry Run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Card Attachments": {
      "main": [
        [
          {
            "node": "Find Cover Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reset Title + Set Cover": {
      "main": [
        [
          {
            "node": "Remove Processing Label - Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Error Title": {
      "main": [
        [
          {
            "node": "Remove Processing Label - Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF needsAI": {
      "main": [
        [
          {
            "node": "Supabase: Get Customer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase: Get Customer": {
      "main": [
        [
          {
            "node": "Build Outlook Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Outlook Search": {
      "main": [
        [
          {
            "node": "Outlook: Search Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Outlook: Search Email": {
      "main": [
        [
          {
            "node": "Build Gemini Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Gemini Prompt": {
      "main": [
        [
          {
            "node": "Gemini: Analyze Description",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini: Analyze Description": {
      "main": [
        [
          {
            "node": "Parse Answer & Write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Answer & Write": {
      "main": [
        [
          {
            "node": "Write Usage to Trello",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Cover Attachment": {
      "main": [
        [
          {
            "node": "IF Cover Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Mockups": {
      "main": [
        [
          {
            "node": "Loop Over Slots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove RETRY Label": {
      "main": [
        [
          {
            "node": "Remove Video Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to Supabase Storage": {
      "main": [
        [
          {
            "node": "Upload Mockup",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Error Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Processing Label - Error": {
      "main": [
        [
          {
            "node": "Remove RETRY Label - Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Is Retry Run?": {
      "main": [
        [
          {
            "node": "Set Waiting Title",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Error Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Processing Label": {
      "main": [
        [
          {
            "node": "Remove RETRY Label - Start",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Waiting Title": {
      "main": [
        [
          {
            "node": "Skip - Gemini Not Ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slot Router (A/B)": {
      "main": [
        [
          {
            "node": "Gemini Image Edit (Variant A)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Variant B Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Image Edit (Variant A)": {
      "main": [
        [
          {
            "node": "Check Gemini Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Waiting Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Image Edit (Variant B)": {
      "main": [
        [
          {
            "node": "Check Gemini Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Waiting Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF QC Problem": {
      "main": [
        [
          {
            "node": "Set Manual Review Label",
            "type": "main",
            "index": 0
          },
          {
            "node": "Comment Manual Review Reason (Single-Image)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Slots": {
      "main": [
        [
          {
            "node": "Download Source for Detect",
            "type": "main",
            "index": 0
          },
          {
            "node": "Wait for Trello Attachment Consistency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse QC Result": {
      "main": [
        [
          {
            "node": "IF QC Problem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Manual Review Label": {
      "main": [
        [
          {
            "node": "Set Manual Review Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Source Binary": {
      "main": [
        [
          {
            "node": "Slot Router (A/B)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Compare QC Slot Body": {
      "main": [
        [
          {
            "node": "Compare QC Slot Vision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare QC Slot Vision": {
      "main": [
        [
          {
            "node": "Parse Compare QC Slot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Compare QC Slot": {
      "main": [
        [
          {
            "node": "IF Design Mismatch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Design Mismatch": {
      "main": [
        [
          {
            "node": "Set Design Issue Label",
            "type": "main",
            "index": 0
          },
          {
            "node": "Comment Design Issue Reason",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF Other Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Other Issues": {
      "main": [
        [
          {
            "node": "Set Manual Review Label",
            "type": "main",
            "index": 0
          },
          {
            "node": "Comment Manual Review Reason (Compare-QC)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Card Labels": {
      "main": [
        [
          {
            "node": "IF Design Issue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Design Issue": {
      "main": [
        [
          {
            "node": "Set Design Issue Title",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF Manual Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Manual Review": {
      "main": [
        [
          {
            "node": "Set Manual Review Title",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update FU Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inject Source Binary": {
      "main": [
        [
          {
            "node": "Build Compare QC Slot Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Limit Reached": {
      "main": [
        [
          {
            "node": "Get Board Labels for Uploaded",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Error Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove RETRY Label - Start": {
      "main": [
        [
          {
            "node": "Remove Manual Review Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Manual Review Label": {
      "main": [
        [
          {
            "node": "Remove Design Issue Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Source for Detect": {
      "main": [
        [
          {
            "node": "Build Detect Design Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Detect Design Body": {
      "main": [
        [
          {
            "node": "Detect Design",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Single-Image QC Body": {
      "main": [
        [
          {
            "node": "Single-Image QC Vision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Single-Image QC Vision": {
      "main": [
        [
          {
            "node": "Parse QC Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Variant B Body": {
      "main": [
        [
          {
            "node": "Variant B Vision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Variant B Vision": {
      "main": [
        [
          {
            "node": "Parse Variant B Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prep Gemini Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Variant B Result": {
      "main": [
        [
          {
            "node": "Variant B Has Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Variant B Has Image?": {
      "main": [
        [
          {
            "node": "Check Gemini Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prep Gemini Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Gemini Fallback": {
      "main": [
        [
          {
            "node": "Gemini Image Edit (Variant B)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Design Issue Label": {
      "main": [
        [
          {
            "node": "Remove Mockup Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Processing Label - Success": {
      "main": [
        [
          {
            "node": "Get Board Labels for Uploaded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Source for Color Detect": {
      "main": [
        [
          {
            "node": "Build Color Detect Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Color Detect Body": {
      "main": [
        [
          {
            "node": "Detect Light Color",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Light Color": {
      "main": [
        [
          {
            "node": "Parse Color Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Color Result": {
      "main": [
        [
          {
            "node": "Prep Mockups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Fallback Status": {
      "main": [
        [
          {
            "node": "IF Fallback Used?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Fallback Used?": {
      "main": [
        [
          {
            "node": "Comment Gemini Fallback",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Remove RETRY Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Comment Gemini Fallback": {
      "main": [
        [
          {
            "node": "Remove RETRY Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Mockup Uploaded Label": {
      "main": [
        [
          {
            "node": "Reduce to Single Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reduce to Single Item": {
      "main": [
        [
          {
            "node": "Download Source for Color Detect",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Usage to Trello": {
      "main": [
        [
          {
            "node": "Remove Processing Label - Autofill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Design": {
      "main": [
        [
          {
            "node": "Get Card for Design Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update FU Queue": {
      "main": [
        [
          {
            "node": "Get Card for Design Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Manual Review Title": {
      "main": [
        [
          {
            "node": "Get Card for Design Title",
            "type": "main",
            "index": 0
          },
          {
            "node": "Remove Processing Label - Review End",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Design Issue Title": {
      "main": [
        [
          {
            "node": "Get Card for Design Title",
            "type": "main",
            "index": 0
          },
          {
            "node": "Remove Processing Label - Review End",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Card for Design Title": {
      "main": [
        [
          {
            "node": "Compute Design Title",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Design Title": {
      "main": [
        [
          {
            "node": "Should Update Design Title?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Update Design Title?": {
      "main": [
        [
          {
            "node": "Update Design Title",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Remove Processing Label - Review End",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Trello Attachment Consistency": {
      "main": [
        [
          {
            "node": "Get Card Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Cover Found": {
      "main": [
        [
          {
            "node": "Reset Title + Set Cover",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reset Title Only - No Cover",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reset Title Only - No Cover": {
      "main": [
        [
          {
            "node": "Remove Processing Label - Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait After Uploaded Label": {
      "main": [
        [
          {
            "node": "Get Card Attachments After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Card Attachments After Uploaded Label": {
      "main": [
        [
          {
            "node": "Find Cover Attachment After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Cover Attachment After Uploaded Label": {
      "main": [
        [
          {
            "node": "IF Cover Found After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Cover Found After Uploaded Label": {
      "main": [
        [
          {
            "node": "Set Mockup01 Cover After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Fallback Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Mockup01 Cover After Uploaded Label": {
      "main": [
        [
          {
            "node": "Check Fallback Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Design Title": {
      "main": [
        [
          {
            "node": "Remove Processing Label - Review End",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Video Label": {
      "main": [
        [
          {
            "node": "Get Card Labels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Board Labels for Uploaded": {
      "main": [
        [
          {
            "node": "Resolve Mockup Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Mockup Uploaded Label": {
      "main": [
        [
          {
            "node": "Mockup Uploaded Label Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mockup Uploaded Label Exists?": {
      "main": [
        [
          {
            "node": "Add Existing Mockup Uploaded Label",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Missing Mockup Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Existing Mockup Uploaded Label": {
      "main": [
        [
          {
            "node": "Wait After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Missing Mockup Uploaded Label": {
      "main": [
        [
          {
            "node": "Wait After Uploaded Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finalization Only?": {
      "main": [
        [
          {
            "node": "Get Board Labels for Uploaded",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Processing Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}