{"id":988,"date":"2026-03-06T14:39:56","date_gmt":"2026-03-06T14:39:56","guid":{"rendered":"https:\/\/digiom.fi\/kradi\/?page_id=988"},"modified":"2026-08-11T14:36:43","modified_gmt":"2026-08-11T11:36:43","slug":"platform","status":"publish","type":"page","link":"https:\/\/marshallai.com\/fi\/platform\/","title":{"rendered":"Platform"},"content":{"rendered":"\n<div class=\"mai-section gb-element-ebab9859\">\n<div class=\"gb-element-26248652\">\n<div class=\"mai-perspective-wrapper\">\n  \n  <div class=\"mai-3d-stack\" id=\"mai-animated-stack\">\n    \n    <img decoding=\"async\" loading=\"eager\" src=\"https:\/\/marshallai.com\/wp-content\/uploads\/2026\/03\/afsa_program_editor_empty_medium-v2.webp\" alt=\"Base Layer\" class=\"mai-layer mai-layer-1\">\n    \n    <img decoding=\"async\" loading=\"eager\" src=\"https:\/\/marshallai.com\/wp-content\/uploads\/2026\/03\/afsa_program_node1.webp\" alt=\"Layer 2\" class=\"mai-layer mai-layer-2\">\n    <img decoding=\"async\" loading=\"eager\" src=\"https:\/\/marshallai.com\/wp-content\/uploads\/2026\/03\/afsa_program_node2.webp\" alt=\"Layer 3\" class=\"mai-layer mai-layer-3\">\n    <img decoding=\"async\" loading=\"eager\" src=\"https:\/\/marshallai.com\/wp-content\/uploads\/2026\/03\/afsa_program_node3.webp\" alt=\"Layer 4\" class=\"mai-layer mai-layer-4\">\n    \n  <\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div class=\"mai-section gb-element-44db7b15\">\n<div class=\"mai-container_inner-content\">\n<h1 class=\"gb-text mai-headline mai-reveal\">The Zero-Code <em>Machine Vision Platform<\/em><\/h1>\n<\/div>\n\n\n\n<div class=\"mai-container_inner-content\">\n<div class=\"gb-element-900f0c3b\">\n<div>\n<h3 class=\"wp-block-heading pl-col-label\">AFSA &#8211; Agile Focused Signal Analysis <\/h3>\n\n\n\n<p class=\"gb-text\">AFSA is MarshallAI&#8217;s proprietary <strong>end-to-end deep learning platform<\/strong> built for high-performance and versatility.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Data annotation, classification, and model training are all handled natively within the same interface. No third party tools necessary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every stage of the processing pipeline \u2014 camera input, preprocessing, model inference, and output \u2014 is a connectable node that operators can wire together without writing a single line of code.<\/p>\n<\/div>\n\n\n\n<div>\n<h3 class=\"wp-block-heading pl-col-label\">Deployment<\/h3>\n\n\n\n<p class=\"gb-text\">The platform runs on any compatible on-premise hardware or our robust <a href=\"https:\/\/marshallai.com\/reid-edge-device\/\" data-type=\"page\" data-id=\"993\">REID-4 Edge device<\/a>. MarshallAI integrates directly with your existing production systems through native industrial protocols.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>No cloud connection is required at any stage.<\/strong>&nbsp;Your training data, production models or real-time detections never have to leave your facility.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div class=\"mai-section mai-bg-grid\">\n<div class=\"mai-container_inner-content\">\n<h2 class=\"gb-text sh-headline mai-reveal is-visible\">Any input.<br>Any standard.<br><em>One unified pipeline<\/em><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">MarshallAI&#8217;s multimodal pipeline handles all input signals inside the same node graph, on the same device, in the same inference cycle.\u00a0<strong>The output possibilities are virtually limitless<\/strong>. A typical output can be a dashboard, database, PLC, SCADA or rejection system.<\/p>\n<\/div>\n\n\n\n<div class=\"gb-element-cef94818\">\n<style>\n\/* \u2500\u2500 MULTIMODAL ROOT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-root {\n  font-family: var(--font-body);\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  box-sizing: border-box;\n  color: var(--mai-text);\n  position: relative;\n  padding-bottom: 8px;\n}\n.mm-root::before {\n  content: '';\n  position: absolute;\n  inset: 0;\n  background-image:\n    linear-gradient(rgba(13,177,209,0.025) 1px, transparent 1px),\n    linear-gradient(90deg, rgba(13,177,209,0.025) 1px, transparent 1px);\n  background-size: 44px 44px;\n  pointer-events: none;\n}\n\n\/* \u2500\u2500 SCENE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-scene {\n  position: relative;\n  width: 100%;\n  margin: 0 auto;\n}\n.mm-connections {\n  position: absolute;\n  inset: 0;\n  width: 100%; height: 100%;\n  pointer-events: none;\n  overflow: visible;\n  z-index: 1;\n}\n.mm-core-glow {\n  position: absolute;\n  border-radius: 50%;\n  background: radial-gradient(circle, rgba(13,177,209,0.08) 0%, transparent 70%);\n  pointer-events: none;\n  z-index: 0;\n}\n\n\/* \u2500\u2500 COLUMN LABELS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-col-label {\n  position: absolute;\n  font-family: var(--font-mono);\n  font-size: var(--font-xs);\n  letter-spacing: 0.18em;\n  text-transform: uppercase;\n  color: var(--mai-sub2);\n  opacity: 0;\n  animation: mm-fadeIn 0.8s 0.5s forwards;\n  white-space: nowrap;\n  transform: translateX(-50%);\n}\n\n\/* \u2500\u2500 INPUT NODES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-input-node {\n  position: absolute;\n  border-radius: 14px;\n  background: var(--mai-surface);\n  border: 1.5px solid var(--mai-border);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  \/* FIX 2: no padding \u2014 inner elements sized relative to node via JS *\/\n  box-sizing: border-box;\n  opacity: 0;\n  transform: translateX(-16px);\n  transition: border-color 0.3s, box-shadow 0.3s,\n              opacity 0.4s cubic-bezier(0.34,1.2,0.64,1),\n              transform 0.4s cubic-bezier(0.34,1.2,0.64,1);\n  z-index: 2;\n  overflow: hidden;\n}\n.mm-input-node.visible {\n  opacity: 1;\n  transform: translateX(0);\n}\n.mm-input-node:hover {\n  border-color: var(--node-color, var(--mai-accent));\n  box-shadow: 0 0 20px rgba(13,177,209,0.1);\n}\n.mm-input-node::before {\n  content: '';\n  position: absolute;\n  top: 7px; left: 7px;\n  width: 10px; height: 10px;\n  border-top: 1.5px solid var(--node-color, var(--mai-accent));\n  border-left: 1.5px solid var(--node-color, var(--mai-accent));\n  border-radius: 2px 0 0 0;\n  opacity: 0.5;\n}\n\n.mm-input-icon {\n  border-radius: 10px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  \/* FIX 2: max-width so icon never exceeds node bounds *\/\n  max-width: 80%;\n}\n.mm-input-icon svg { display: block; }\n\n.mm-input-title {\n  font-family: var(--font-body);\n  font-weight: 700;\n  letter-spacing: 0.07em;\n  text-transform: uppercase;\n  text-align: center;\n  \/* FIX 3: enforced via JS floor, class exists for overrides *\/\n}\n\n.mm-input-badge {\n  font-family: var(--font-mono);\n  color: var(--mai-sub2);\n  text-align: center;\n  \/* FIX 3: enforced via JS floor *\/\n}\n\n\/* Audio wave bars \u2014 FIX 7: restored *\/\n.mm-wave-bars { display: flex; gap: 2px; align-items: flex-end; }\n.mm-wave-bar {\n  width: 3px; border-radius: 2px;\n  background: var(--node-color, var(--mai-accent));\n  opacity: 0.75;\n  animation: mm-waveAnim 0.8s ease-in-out infinite alternate;\n}\n.mm-wave-bar:nth-child(1) { height: 5px;  animation-delay: 0s;    }\n.mm-wave-bar:nth-child(2) { height: 11px; animation-delay: 0.1s;  }\n.mm-wave-bar:nth-child(3) { height: 7px;  animation-delay: 0.2s;  }\n.mm-wave-bar:nth-child(4) { height: 13px; animation-delay: 0.05s; }\n.mm-wave-bar:nth-child(5) { height: 4px;  animation-delay: 0.15s; }\n\n\/* \u2500\u2500 CORE NODE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-core-wrap {\n  position: absolute;\n  z-index: 2;\n  opacity: 0;\n  \/* FIX 6: keep translate in transform so .visible doesn't clobber centering *\/\n  transform: translate(-50%, -50%) scale(0.8);\n  transition: opacity 0.6s ease,\n              transform 0.6s cubic-bezier(0.34,1.5,0.64,1);\n}\n.mm-core-wrap.visible {\n  opacity: 1;\n  transform: translate(-50%, -50%) scale(1);\n}\n.mm-core {\n  border-radius: 24px;\n  background: var(--mai-surface);\n  border: 1.5px solid var(--mai-border);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05),\n              0 0 40px var(--mai-accent-10);\n  overflow: hidden;\n  transition: background var(--mai-transition), border-color var(--mai-transition);\n}\n.mm-core::before, .mm-core::after {\n  content: '';\n  position: absolute;\n  width: 14px; height: 14px;\n  border-color: var(--mai-accent);\n  border-style: solid;\n  opacity: 0.6;\n}\n.mm-core::before { top:9px;   left:9px;  border-width:2px 0 0 2px; border-radius:4px 0 0 0; }\n.mm-core::after  { bottom:9px; right:9px; border-width:0 2px 2px 0; border-radius:0 0 4px 0; }\n\n.mm-core-title {\n  font-family: var(--font-body);\n  font-weight: 800;\n  background: linear-gradient(135deg, var(--mai-text), var(--mai-sky));\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  text-align: center;\n  line-height: 1.2;\n}\n\n.mm-core-sub {\n  font-family: var(--font-mono);\n  color: var(--mai-sub2);\n  text-transform: uppercase;\n  letter-spacing: 0.1em;\n  text-align: center;\n}\n\n.mm-proc-dots { \ndisplay: flex; gap: 5px; align-items: center; \n}\n\n.mm-proc-dot {\n  width: 5px; height: 5px; border-radius: 50%;\n  background: var(--mai-accent);\n  animation: mm-dotPulse 1.5s ease-in-out infinite;\n}\n\n.mm-proc-dot:nth-child(2) { \nanimation-delay: 0.2s; \n}\n\n.mm-proc-dot:nth-child(3) { \nanimation-delay: 0.4s; \n}\n\n\/* \u2500\u2500 OUTPUT PANEL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-output-panel {\n  position: absolute;\n  display: flex;\n  flex-direction: column;\n  z-index: 2;\n  \/* FIX 6: centering via top+marginTop in JS, no transform needed *\/\n  opacity: 0;\n  transition: opacity 0.5s ease;\n}\n.mm-output-panel.visible { opacity: 1; }\n\n.mm-panel-header {\n  background: var(--mai-surface);\n  border: 1.5px solid var(--mai-border);\n  border-radius: 12px 12px 0 0;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 0 12px;\n  flex-shrink: 0;\n}\n.mm-panel-header-title {\n  font-family: var(--font-mono);\n  font-size: var(--font-xs);\n  letter-spacing: 0.16em;\n  text-transform: uppercase;\n  color: var(--mai-sub2);\n}\n.mm-panel-status {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  font-family: var(--font-mono);\n  font-size: 10px;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n}\n.mm-panel-status-dot {\n  width: 6px; height: 6px; border-radius: 50%;\n  animation: mm-statusBlink 2s ease-in-out infinite;\n}\n\n.mm-panel-body {\n  background: var(--mai-surface);\n  border: 1.5px solid var(--mai-border);\n  border-top: none;\n  border-radius: 0 0 12px 12px;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n\/* \u2500\u2500 LOG ROWS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-log-row {\n  display: flex;\n  align-items: flex-start;\n  border-bottom: 1px solid var(--mai-border);\n  opacity: 0;\n  transform: translateY(-6px);\n  transition: opacity 0.4s ease, transform 0.4s ease;\n}\n.mm-log-row:last-child { border-bottom: none; }\n.mm-log-row.visible { opacity: 1; transform: translateY(0); }\n\n.mm-log-type {\n  font-family: var(--font-mono);\n  font-weight: 700;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  font-size: 10px;           \/* FIX 3: hard floor, never scaled below 10 *\/\n  flex-shrink: 0;\n  line-height: 1.4;\n}\n.mm-log-content {\n  flex: 1;\n  min-width: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n.mm-log-main {\n  font-family: var(--font-body);\n  font-weight: 700;\n  color: var(--mai-text);\n  line-height: 1.3;\n  \/* font-size set via JS with 10px floor *\/\n}\n.mm-log-detail {\n  font-family: var(--font-mono);\n  color: var(--mai-sub);\n  line-height: 1.4;\n  \/* font-size set via JS with 10px floor *\/\n}\n.mm-log-time {\n  font-family: var(--font-mono);\n  color: var(--mai-sub2);\n  flex-shrink: 0;\n  text-align: right;\n  line-height: 1.4;\n  \/* font-size set via JS with 10px floor *\/\n}\n\n\/* FIX 10: type-colour classes *\/\n.mm-log-type--detect { color: #0DB1D1; }\n.mm-log-type--warn   { color: #FFAA00; }\n.mm-log-type--action { color: #FF6D01; }\n.mm-log-type--alert  { color: #EE4444; }\n.mm-log-type--log    { color: #00FF9D; }\n\n\/* \u2500\u2500 MOBILE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n@media (max-width: 600px) {\n  .mm-log-row {\n    flex-direction: row;\n    flex-wrap: wrap;\n    gap: 2px;\n  }\n  .mm-log-time {\n    width: 100%;\n    text-align: left;\n    margin-top: 2px;\n  }\n}\n\n\/* \u2500\u2500 SCENE SELECTORS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-scene-selectors {\n  display: flex;\n  gap: 8px;\n  flex-wrap: wrap;\n  justify-content: center;\n  margin-top: 24px;\n  position: relative;\n  z-index: 10;\n}\n@media (max-width: 700px) {\n  .mm-scene-selectors { margin-top: 148px; }\n}\n\n.mm-scene-btn {\n  padding: 8px 16px;\n  border: 1.5px solid var(--mai-border);\n  border-radius: var(--mai-radius-md);\n  background: var(--mai-surface);\n  color: var(--mai-sub2);\n  font-family: var(--font-mono);\n  font-size: var(--font-sm);\nfont-weight: var(--fw-extrabold);\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  cursor: pointer;\n  transition: border-color 0.2s, color 0.2s, background 0.2s;\n  white-space: nowrap;\n}\n.mm-scene-btn:hover { \nbackground-color: var(--mai-surface-2);\nborder-color: #0DB1D1; \ncolor: #0DB1D1; \n}\n.mm-scene-btn.active {\n  border-color: #0DB1D1;\n  color: #0DB1D1;\n  background-color: var(--mai-surface-2);\n}\n\n\/* \u2500\u2500 PATHS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.mm-conn-path { fill: none; stroke-width: 1.5; stroke-linecap: round; }\n\n\/* \u2500\u2500 KEYFRAMES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n@keyframes mm-fadeIn    { to { opacity: 1; } }\n@keyframes mm-dotPulse  { 0%,100%{opacity:0.2;transform:scale(0.85);} 50%{opacity:1;transform:scale(1.1);} }\n@keyframes mm-statusBlink { 0%,100%{opacity:0.3;} 50%{opacity:1;} }\n@keyframes mm-waveAnim  { from{transform:scaleY(1);} to{transform:scaleY(0.25);} }\n<\/style>\n\n<div class=\"mm-root\">\n  <div id=\"mm-scene\" class=\"mm-scene\"><\/div>\n  <div class=\"mm-scene-selectors\" id=\"mm-scene-selectors\">\n    <button class=\"mm-scene-btn active\" data-scene=\"0\">Defect detection<\/button>\n    <button class=\"mm-scene-btn\" data-scene=\"1\">Process monitoring<\/button>\n    <button class=\"mm-scene-btn\" data-scene=\"2\">Traffic control<\/button>\n    <button class=\"mm-scene-btn\" data-scene=\"3\">Perimeter security<\/button>\n  <\/div>\n<\/div>\n\n<script>\n(function () {\n  'use strict';\n\n  var C = {\n    cyan:'#0DB1D1', orange:'#FF6D01', green:'#00FF9D',\n    amber:'#FFAA00', red:'#EE4444',   sky:'#4D9FFF'\n  };\n\n  \/* FIX 10: type\u2192colour map *\/\n  var TYPE_COLOR = {\n    DETECT: C.cyan, WARN: C.amber, ACTION: C.orange,\n    ALERT: C.red,   LOG: C.green\n  };\n  var TYPE_CLASS = {\n    DETECT:'mm-log-type--detect', WARN:'mm-log-type--warn',\n    ACTION:'mm-log-type--action', ALERT:'mm-log-type--alert',\n    LOG:'mm-log-type--log'\n  };\n\n  var SCENES = [\n    {\n      inputs: [{ color:C.cyan,  label:'Video',   badge:'Line 2 \u00b7 Cam 3', icon:'video',   wave:false }],\n      status: { color:C.red,    label:'DEFECT' },\n      logs: [\n        { type:'DETECT', main:'Defect detected \u2014 Line 2', detail:'Surface crack \u00b7 Part ID #A4-8812', time:'14:32:07.441' },\n        { type:'ACTION', main:'Part ejected',              detail:'Pneumatic reject arm triggered',   time:'14:32:07.501' },\n        { type:'LOG',    main:'Database updated',          detail:'QA record written \u00b7 ERP synced',  time:'14:32:07.612' }\n      ]\n    },\n    {\n      inputs: [{ color:C.cyan,  label:'Video',   badge:'Line 4 \u00b7 Cam 1', icon:'video',   wave:false }],\n      status: { color:C.amber,  label:'WARNING' },\n      logs: [\n        { type:'WARN',   main:'Section jam \u2014 Line 4',  detail:'Blockage detected \u00b7 Zone C2',    time:'09:14:22.118' },\n        { type:'ACTION', main:'Conveyor stopped',       detail:'E-stop signal via OPC UA',       time:'09:14:22.134' },\n        { type:'LOG',    main:'Incident logged',        detail:'Downtime event \u00b7 MES notified',  time:'09:14:22.311' }\n      ]\n    },\n    {\n      inputs: [\n        { color:C.cyan,  label:'Video', badge:'Junction 4A', icon:'video', wave:false },\n        { color:C.green, label:'Audio', badge:null,          icon:'audio', wave:true  }\n      ],\n      status: { color:C.red, label:'INCIDENT' },\n      logs: [\n        { type:'DETECT', main:'Vehicle collision',       detail:'Impact signature confirmed',      time:'11:07:53.002' },\n        { type:'ALERT',  main:'SMS alert issued',        detail:'Traffic control + emergency svc', time:'11:07:53.180' },\n        { type:'LOG',    main:'Incident report created', detail:'Clip saved \u00b7 Authority notified', time:'11:07:53.340' }\n      ]\n    },\n    {\n      inputs: [\n        { color:C.cyan,   label:'Video',   badge:'Grid 7 \u00b7 Cam 9', icon:'video',   wave:false },\n        { color:C.green,  label:'Audio',   badge:null,             icon:'audio',   wave:true  },\n        { color:C.orange, label:'Thermal', badge:'IR \u00b7 8\u201314\u03bcm',   icon:'thermal', wave:false }\n      ],\n      status: { color:C.red, label:'INTRUSION' },\n      logs: [\n        { type:'DETECT', main:'Perimeter intrusion \u2014 Grid 7', detail:'Human signature confirmed \u00b7 Cam 9', time:'02:41:18.774' },\n        { type:'ACTION', main:'Camera 9 tasked',              detail:'PTZ tracking engaged \u00b7 Grid 7',    time:'02:41:18.810' },\n        { type:'LOG',    main:'Event recorded',               detail:'Clip + thermal overlay archived',  time:'02:41:18.950' }\n      ]\n    }\n  ];\n\n  var _timers = [], _packets = [], _currentScene = 0;\n  function _ct(fn, ms) { _timers.push(setTimeout(fn, ms)); }\n  function _clearAll() {\n    _timers.forEach(clearTimeout); _timers = [];\n    _packets.forEach(function(p){ p.running = false; }); _packets = [];\n  }\n\n  \/* FIX 3: font size with enforced 10px floor *\/\n  function fs(val) { return Math.max(10, Math.round(val)) + 'px'; }\n\n  function getScale(w) {\n    if (w <= 480) return 0.55;\n    if (w <= 700) return 0.72;\n    if (w <= 900) return 0.86;\n    return 1.0;\n  }\n\n  function mmIcon(type, size, color) {\n    var s = Math.round(size), sw = '1.8';\n    var b = '<svg width=\"'+s+'\" height=\"'+s+'\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"'+color+'\" stroke-width=\"'+sw+'\" stroke-linecap=\"round\" stroke-linejoin=\"round\">';\n    if (type === 'video')\n      return b+'<rect x=\"2\" y=\"7\" width=\"20\" height=\"15\" rx=\"2\"\/><path d=\"M17 7V5a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2\"\/><circle cx=\"12\" cy=\"14\" r=\"3\"\/><\/svg>';\n    if (type === 'audio')\n      return b+'<path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\"\/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8\"\/><\/svg>';\n    if (type === 'thermal')\n      return b+'<path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\/><circle cx=\"12\" cy=\"12\" r=\"2.5\"\/><path d=\"M12 7v2M12 15v2M7 12h2M15 12h2\"\/><\/svg>';\n    return '';\n  }\n\n  \/* FIX 1: append path first, then measure *\/\n  function drawLine(scene, svg, fromEl, toEl, color, delay) {\n    _ct(function() {\n      var sr = scene.getBoundingClientRect();\n      var fr = fromEl.getBoundingClientRect();\n      var tr = toEl.getBoundingClientRect();\n      var x1 = fr.right  - sr.left, y1 = fr.top - sr.top + fr.height\/2;\n      var x2 = tr.left   - sr.left, y2 = tr.top - sr.top + tr.height\/2;\n      var dx = x2 - x1;\n      var d  = 'M'+x1+','+y1+' C'+(x1+dx*0.5)+','+y1+' '+(x2-dx*0.5)+','+y2+' '+x2+','+y2;\n      var path = document.createElementNS('http:\/\/www.w3.org\/2000\/svg','path');\n      path.setAttribute('d', d);\n      path.setAttribute('class', 'mm-conn-path');\n      path.setAttribute('stroke', color);\n      path.setAttribute('stroke-opacity', '0.65');\n      svg.appendChild(path); \/* FIX 1: append before measuring *\/\n      var len = path.getTotalLength();\n      path.style.strokeDasharray  = len;\n      path.style.strokeDashoffset = len;\n      requestAnimationFrame(function() { requestAnimationFrame(function() {\n        path.style.transition = 'stroke-dashoffset 0.55s cubic-bezier(0.4,0,0.2,1)';\n        path.style.strokeDashoffset = '0';\n      }); });\n    }, delay);\n  }\n\n  function mmBuild(idx) {\n    _clearAll();\n    _currentScene = idx;\n\n    var root  = document.querySelector('.mm-root');\n    var scene = document.getElementById('mm-scene');\n    if (!root || root.offsetWidth === 0) { \/* FIX 9: dimension guard *\/\n      setTimeout(function(){ mmBuild(idx); }, 150); return;\n    }\n\n    document.querySelectorAll('.mm-scene-btn').forEach(function(b){\n      b.classList.toggle('active', parseInt(b.dataset.scene) === idx);\n    });\n\n    var rootW = root.offsetWidth;\n    var sc    = getScale(rootW);\n    var def   = SCENES[idx];\n    var ni    = def.inputs.length;\n\n    var NW = Math.round(140*sc);\n    var CW = Math.round(176*sc), CH = Math.round(176*sc);\n    var PW = Math.round(258*sc);\n    var GAP = Math.round(18*sc);\n\n    \/* Icon and content sizes \u2014 NH derived from content so nothing overflows *\/\n    var iconWrapSz = Math.round(Math.min(NW * 0.42, 48));\n    var iconSvgSz  = Math.round(iconWrapSz * 0.52);\n    var titleH     = Math.max(13, Math.round(11*sc));   \/* mm-input-title *\/\n    var badgeH     = Math.max(10, Math.round(9*sc));    \/* badge or wave *\/\n    var nodeGap    = Math.round(7*sc);\n    var nodePad    = Math.round(10*sc);\n    \/* NH = top-pad + icon + gap + title + gap + badge + bottom-pad *\/\n    var NH = nodePad + iconWrapSz + nodeGap + titleH + nodeGap + badgeH + nodePad;\n\n    var headerH  = Math.round(36*sc);\n    var rowH     = Math.round(62*sc);\n    var panelH   = headerH + def.logs.length * rowH;\n\n    var totalInputH = ni*NH + (ni-1)*GAP;\n    var SH = Math.max(totalInputH, CH, panelH) + Math.round(64*sc);\n\n    \/* FIX 4: clamp panel so it never bleeds off right edge *\/\n    var panelX = Math.min(rootW - PW - 4, rootW - PW);\n    var inputX = 4;\n    var coreX  = Math.round(rootW \/ 2);\n\n    scene.style.height = SH + 'px';\n    scene.innerHTML = '';\n\n    \/* Glow *\/\n    var glow = document.createElement('div');\n    glow.className = 'mm-core-glow';\n    glow.style.cssText = 'width:'+Math.round(260*sc)+'px;height:'+Math.round(260*sc)+'px;left:'+coreX+'px;top:50%;transform:translate(-50%,-50%);';\n    scene.appendChild(glow);\n\n    \/* SVG overlay *\/\n    var svg = document.createElementNS('http:\/\/www.w3.org\/2000\/svg','svg');\n    svg.setAttribute('class','mm-connections');\n    scene.appendChild(svg);\n\n    \/* Column labels \u2014 FIX 4: right label anchored from right *\/\n    var labelTop = Math.round(8*sc) + 'px';\n    function makeLabel(text, leftPx) {\n      var el = document.createElement('span');\n      el.className = 'mm-col-label';\n      el.textContent = text;\n      el.style.left = leftPx + 'px';\n      el.style.top  = labelTop;\n      scene.appendChild(el);\n    }\n    makeLabel('Sensor inputs', inputX + NW\/2);\n    makeLabel('MarshallAI',    coreX);\n    \/* FIX 4: right label \u2014 use right-anchor instead of left+translate *\/\n    var rlabel = document.createElement('span');\n    rlabel.className = 'mm-col-label';\n    rlabel.textContent = 'System output';\n    rlabel.style.right     = '4px';\n    rlabel.style.top       = labelTop;\n    rlabel.style.transform = 'none';\n    rlabel.style.left      = 'auto';\n    scene.appendChild(rlabel);\n\n    \/* Inputs *\/\n    var inputEls = [];\n    var inputTopY = (SH - totalInputH) \/ 2;\n    def.inputs.forEach(function(inp, i) {\n      var el = document.createElement('div');\n      el.className = 'mm-input-node';\n      el.style.cssText = '--node-color:'+inp.color+';left:'+inputX+'px;top:'+Math.round(inputTopY + i*(NH+GAP))+'px;width:'+NW+'px;height:'+NH+'px;gap:'+nodeGap+'px;padding:'+nodePad+'px 6px;';\n\n      var iconWrap = document.createElement('div');\n      iconWrap.className = 'mm-input-icon';\n      iconWrap.style.cssText = 'width:'+iconWrapSz+'px;height:'+iconWrapSz+'px;background:'+inp.color+'18;border-radius:'+Math.round(9*sc)+'px;';\n      iconWrap.innerHTML = mmIcon(inp.icon, iconSvgSz, inp.color);\n      el.appendChild(iconWrap);\n\n      var title = document.createElement('div');\n      title.className = 'mm-input-title';\n      title.style.cssText = 'color:'+inp.color+';font-size:'+fs(11*sc)+';';\n      title.textContent = inp.label;\n      el.appendChild(title);\n\n      \/* FIX 7: audio gets wave bars, others get badge *\/\n      if (inp.wave) {\n        var wave = document.createElement('div');\n        wave.className = 'mm-wave-bars';\n        wave.style.cssText = '--node-color:'+inp.color+';height:'+Math.round(14*sc)+'px;';\n        wave.innerHTML = '<div class=\"mm-wave-bar\"><\/div><div class=\"mm-wave-bar\"><\/div><div class=\"mm-wave-bar\"><\/div><div class=\"mm-wave-bar\"><\/div><div class=\"mm-wave-bar\"><\/div>';\n        el.appendChild(wave);\n      } else if (inp.badge) {\n        var badge = document.createElement('div');\n        badge.className = 'mm-input-badge';\n        badge.style.fontSize = fs(9*sc);\n        badge.textContent = inp.badge;\n        el.appendChild(badge);\n      }\n\n      scene.appendChild(el);\n      inputEls.push(el);\n    });\n\n    \/* Core *\/\n    var coreWrap = document.createElement('div');\n    coreWrap.className = 'mm-core-wrap';\n    coreWrap.style.left = coreX + 'px';\n    coreWrap.style.top  = '50%';\n    var dotSz = Math.round(5*sc);\n    var coreSub = Math.round(14*sc);\n    coreWrap.innerHTML = '<div class=\"mm-core\" style=\"width:'+CW+'px;height:'+CH+'px;border-radius:'+Math.round(22*sc)+'px;gap:'+Math.round(9*sc)+'px;\">'\n      +'<div class=\"mm-core-title\" style=\"font-size:'+fs(14*sc)+';\">MarshallAI<\/div>'\n      +'<div class=\"mm-proc-dots\" style=\"gap:'+Math.round(5*sc)+'px;margin:'+Math.round(4*sc)+'px 0;\">'\n      +'<div class=\"mm-proc-dot\" style=\"width:'+dotSz+'px;height:'+dotSz+'px;\"><\/div>'\n      +'<div class=\"mm-proc-dot\" style=\"width:'+dotSz+'px;height:'+dotSz+'px;\"><\/div>'\n      +'<div class=\"mm-proc-dot\" style=\"width:'+dotSz+'px;height:'+dotSz+'px;\"><\/div>'\n      +'<\/div>'\n      +'<div class=\"mm-core-sub\" style=\"font-size:'+fs(9*sc)+';\">Processing<\/div>'\n      +'<\/div>';\n    scene.appendChild(coreWrap);\n\n    \/* Output panel \u2014 FIX 6: centre via top + negative margin instead of transform *\/\n    var panelTopY = Math.round((SH - panelH) \/ 2);\n    var panel = document.createElement('div');\n    panel.className = 'mm-output-panel';\n    panel.style.cssText = 'left:'+panelX+'px;top:'+panelTopY+'px;width:'+PW+'px;';\n\n    var hdr = document.createElement('div');\n    hdr.className = 'mm-panel-header';\n    hdr.style.height = headerH + 'px';\n    hdr.innerHTML = '<span class=\"mm-panel-header-title\">Output feed<\/span>'\n      +'<div class=\"mm-panel-status\" style=\"color:'+def.status.color+';\">'\n      +'<span class=\"mm-panel-status-dot\" style=\"background:'+def.status.color+';box-shadow:0 0 6px '+def.status.color+';\"><\/span>'\n      +def.status.label+'<\/div>';\n\n    var body = document.createElement('div');\n    body.className = 'mm-panel-body';\n\n    \/* FIX 3 + FIX 10: scaled text with 10px floor, type-specific colours and classes *\/\n    var mainFs   = fs(10.5*sc);\n    var detailFs = fs(9*sc);\n    var typeFs   = fs(9*sc);\n    var logEls = def.logs.map(function(log) {\n      var row = document.createElement('div');\n      row.className = 'mm-log-row';\n      row.style.cssText = 'padding:'+Math.round(8*sc)+'px '+Math.round(10*sc)+'px;gap:'+Math.round(8*sc)+'px;min-height:'+rowH+'px;box-sizing:border-box;';\n      var typeClass = TYPE_CLASS[log.type] || 'mm-log-type--log';\n      row.innerHTML = '<div class=\"mm-log-type '+typeClass+'\" style=\"font-size:'+typeFs+';min-width:'+Math.round(46*sc)+'px;\">'+log.type+'<\/div>'\n        +'<div class=\"mm-log-content\">'\n        +'<div class=\"mm-log-main\" style=\"font-size:'+mainFs+';\">'+log.main+'<\/div>'\n        +'<div class=\"mm-log-detail\" style=\"font-size:'+detailFs+';\">'+log.detail+'<\/div>'\n        +'<\/div>'\n        +'<div class=\"mm-log-time\" style=\"font-size:'+detailFs+';\">'+log.time+'<\/div>';\n      body.appendChild(row);\n      return row;\n    });\n\n    panel.appendChild(hdr);\n    panel.appendChild(body);\n    scene.appendChild(panel);\n\n    \/* \u2500\u2500 Sequence \u2500\u2500 *\/\n    var BASE = 200, ISTEP = 320;\n    inputEls.forEach(function(el, i) {\n      _ct(function(){ el.classList.add('visible'); }, BASE + i*ISTEP);\n    });\n\n    var coreAt = BASE + ni*ISTEP;\n    _ct(function(){ coreWrap.classList.add('visible'); }, coreAt);\n\n    var linesAt = coreAt + 420;\n    inputEls.forEach(function(el, i) {\n      drawLine(scene, svg, el, coreWrap, def.inputs[i].color, linesAt + i*180);\n    });\n\n    var panelAt = linesAt + ni*180 + 480;\n    _ct(function(){\n      panel.classList.add('visible');\n      drawLine(scene, svg, coreWrap, panel, def.status.color, 80);\n      logEls.forEach(function(row, i) {\n        _ct(function(){ row.classList.add('visible'); }, 380 + i*280);\n      });\n    }, panelAt);\n  }\n\n  \/* Scene selector *\/\n  document.getElementById('mm-scene-selectors').addEventListener('click', function(e) {\n    var btn = e.target.closest('.mm-scene-btn');\n    if (!btn) return;\n    var idx = parseInt(btn.dataset.scene);\n    if (idx === _currentScene) return;\n    mmBuild(idx);\n  });\n\n  \/* FIX 9: robust init \u2014 works whether script runs before or after load *\/\n  var _started = false;\n  function mmInit() {\n    if (_started) return;\n    if (!document.getElementById('mm-scene')) return;\n    _started = true;\n    setTimeout(function(){ mmBuild(0); }, 100);\n  }\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', mmInit);\n  } else {\n    mmInit();\n  }\n  window.addEventListener('load', function() {\n    if (!_started) { mmInit(); }\n    else { setTimeout(function(){ mmBuild(_currentScene); }, 50); }\n  });\n\n  var _rt;\n  window.addEventListener('resize', function() {\n    clearTimeout(_rt);\n    _rt = setTimeout(function(){ mmBuild(_currentScene); }, 240);\n  });\n\n})();\n<\/script>\n<\/div>\n<\/div>\n\n\n\n<div class=\"mai-section\">\n<div class=\"mai-container__inner-narrow gb-element-d329fb42\">\n<h2 class=\"sh-headline mai-reveal\">\n    Industrial grade <em>AI machine vision<\/em>\n  <\/h2>\n\n\n\n<section class=\"bento-section\">\n\n  <div class=\"bento-grid\" id=\"bento-grid\">\n\n    <!-- \u2500\u2500 1. HIGH SPEED \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n    <div class=\"bento-card\" data-card=\"speed\" style=\"--card-color: var(--c0);\">\n      <div class=\"card-grid-bg\"><\/div>\n      <div class=\"card-glow\" style=\"top:-60px; left:-40px;\"><\/div>\n\n      <div class=\"card-icon-wrap\">\n        <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <path d=\"M13 2L3 14h9l-1 8 10-12h-9l1-8z\"><\/path>\n        <\/svg>\n      <\/div>\n      <div class=\"card-label\">Performance<\/div>\n      <div class=\"card-title\">High-Speed Processing<\/div>\n      <div class=\"card-body\">Real-time analysis at production line speeds without bottlenecks or frame drops.<\/div>\n      <div class=\"card-stat\">\n        <span class=\"card-stat-num\">2500+<\/span>\n        <span class=\"card-stat-unit\">parts \/ min<\/span>\n      <\/div>\n    <\/div>\n\n    <!-- \u2500\u2500 2. SECURE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n    <div class=\"bento-card\" data-card=\"secure\" style=\"--card-color: var(--c2);\">\n      <div class=\"card-grid-bg\"><\/div>\n      <div class=\"card-glow\" style=\"top:-40px; right:-40px;\"><\/div>\n\n      <div class=\"card-icon-wrap\">\n        <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"><\/path>\n        <\/svg>\n      <\/div>\n      <div class=\"card-label\">Security<\/div>\n      <div class=\"card-title\">Air-Gapped &amp; Secure<\/div>\n      <div class=\"card-body\">Fully on-premise processing. No cloud dependency, no data leaves the facility.<\/div>\n      <div class=\"card-tags\">\n        <span class=\"card-tag\">Air-gapped<\/span>\n        <span class=\"card-tag\">On-premise<\/span>\n        <span class=\"card-tag\">Zero egress<\/span>\n      <\/div>\n\n      <!-- decorative lock -->\n      <div class=\"lock-wrap\">\n        <svg width=\"52\" height=\"52\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"color: var(--c2);\">\n          <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"><\/rect>\n          <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"><\/path>\n        <\/svg>\n      <\/div>\n    <\/div>\n\n    <!-- \u2500\u2500 3. ACCURATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n    <div class=\"bento-card\" data-card=\"accurate\" style=\"--card-color: var(--c3);\">\n      <div class=\"card-grid-bg\"><\/div>\n      <div class=\"card-glow\" style=\"bottom:-40px; left:-20px;\"><\/div>\n\n      <div class=\"card-icon-wrap\">\n        <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <circle cx=\"12\" cy=\"12\" r=\"10\"><\/circle>\n          <circle cx=\"12\" cy=\"12\" r=\"4\"><\/circle>\n          <line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"4\"><\/line>\n          <line x1=\"12\" y1=\"20\" x2=\"12\" y2=\"22\"><\/line>\n          <line x1=\"2\" y1=\"12\" x2=\"4\" y2=\"12\"><\/line>\n          <line x1=\"20\" y1=\"12\" x2=\"22\" y2=\"12\"><\/line>\n        <\/svg>\n      <\/div>\n      <div class=\"card-label\">Accuracy<\/div>\n      <div class=\"card-title\">99.99% Detection Accuracy<\/div>\n      <div class=\"card-body\">Validated across millions of samples in real-world environments.<\/div>\n      <div class=\"card-stat\">\n        <span class=\"card-stat-num\">99.99<\/span>\n        <span class=\"card-stat-unit\">%<\/span>\n      <\/div>\n    <\/div>\n\n    <!-- \u2500\u2500 4. INTEGRATIONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n    <div class=\"bento-card\" data-card=\"integrations\" style=\"--card-color: var(--c1);\">\n      <div class=\"card-grid-bg\"><\/div>\n      <div class=\"card-glow\" style=\"top:-40px; left:-20px;\"><\/div>\n\n      <div class=\"card-icon-wrap\">\n        <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <rect x=\"2\" y=\"2\" width=\"8\" height=\"8\" rx=\"1\"><\/rect>\n          <rect x=\"14\" y=\"2\" width=\"8\" height=\"8\" rx=\"1\"><\/rect>\n          <rect x=\"2\" y=\"14\" width=\"8\" height=\"8\" rx=\"1\"><\/rect>\n          <path d=\"M18 14v2a2 2 0 0 1-2 2h-2M22 18h-4M18 22v-4\"><\/path>\n        <\/svg>\n      <\/div>\n      <div class=\"card-label\">Integrations<\/div>\n      <div class=\"card-title\">Industry Standard Protocols<\/div>\n      <div class=\"card-body\">Native support for leading industrial and IoT communication standards.<\/div>\n      <div class=\"card-tags\">\n        <span class=\"card-tag\">OPC UA<\/span>\n        <span class=\"card-tag\">REST<\/span>\n        <span class=\"card-tag\">MQTT<\/span>\n        <span class=\"card-tag\">MOXA<\/span>\n      <\/div>\n    <\/div>\n\n    <!-- \u2500\u2500 5. MULTIMODAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n    <div class=\"bento-card\" data-card=\"multimodal\" style=\"--card-color: var(--c4);\">\n      <div class=\"card-grid-bg\"><\/div>\n      <div class=\"card-glow\" style=\"top:-60px; right:-20px;\"><\/div>\n\n      <div class=\"card-icon-wrap\">\n        <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n          <path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"><\/path>\n          <polyline points=\"3.27 6.96 12 12.01 20.73 6.96\"><\/polyline>\n          <line x1=\"12\" y1=\"22.08\" x2=\"12\" y2=\"12\"><\/line>\n        <\/svg>\n      <\/div>\n      <div class=\"card-label\">Input Flexibility<\/div>\n      <div class=\"card-title\">Multi-Modal Sensor Fusion<\/div>\n      <div class=\"card-body\">Ingest, synchronise and analyse data from any combination of sensors simultaneously through a unified node-based pipeline.<\/div>\n\n      <div class=\"sensor-row\">\n\n        <div class=\"sensor-chip\">\n          <div class=\"sensor-chip-icon\" style=\"background: rgba(0,212,255,0.1);\">\n            <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#00d4ff\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n              <rect x=\"2\" y=\"7\" width=\"20\" height=\"15\" rx=\"2\"><\/rect>\n              <path d=\"M17 7V5a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2\"><\/path>\n              <circle cx=\"12\" cy=\"14\" r=\"3\"><\/circle>\n            <\/svg>\n          <\/div>\n          <div>\n            <div class=\"sensor-chip-label\">Video<\/div>\n            <div class=\"sensor-chip-sub\">1\u2013400 fps<\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"sensor-chip\">\n          <div class=\"sensor-chip-icon\" style=\"background: rgba(0,255,157,0.08);\">\n            <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#00ff9d\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n              <path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\"><\/path>\n              <path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8\"><\/path>\n            <\/svg>\n          <\/div>\n          <div>\n            <div class=\"sensor-chip-label\">Audio<\/div>\n            <div class=\"sensor-chip-sub\">48 kHz \u00b7 PCM<\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"sensor-chip\">\n          <div class=\"sensor-chip-icon\" style=\"background: rgba(255,149,0,0.08);\">\n            <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ff9500\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n              <path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"><\/path>\n              <circle cx=\"12\" cy=\"12\" r=\"2.5\"><\/circle>\n            <\/svg>\n          <\/div>\n          <div>\n            <div class=\"sensor-chip-label\">Multispectral<\/div>\n            <div class=\"sensor-chip-sub\">UV \u00b7 IR \u00b7 SWIR<\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"sensor-chip\">\n          <div class=\"sensor-chip-icon\" style=\"background: rgba(180,79,255,0.1);\">\n            <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#b44fff\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n              <circle cx=\"12\" cy=\"12\" r=\"2\"><\/circle>\n              <path d=\"M16.24 7.76a6 6 0 0 1 0 8.49M7.76 16.24a6 6 0 0 1 0-8.49M20.07 4a10 10 0 0 1 0 16M3.93 20a10 10 0 0 1 0-16\"><\/path>\n            <\/svg>\n          <\/div>\n          <div>\n            <div class=\"sensor-chip-label\">Any Sensor<\/div>\n            <div class=\"sensor-chip-sub\">Custom input<\/div>\n          <\/div>\n        <\/div>\n\n      <\/div><!-- .sensor-row -->\n    <\/div>\n\n  <\/div><!-- .bento-grid -->\n<\/section>\n<\/div>\n<\/div>\n\n\n\n<div class=\"mai-section mai-bg-grid\">\n<div class=\"mai-container_inner-content\">\n<h2 class=\"gb-text sh-headline mai-reveal is-visible\"><em>A fully customizable<\/em> processing path<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each detection program is composed as a graph. Nodes represent discrete operations \u2014 eg. a camera source, a resize transform, a neural network inference block, a threshold filter, an OPC UA write.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You connect nodes in the order your process requires, set the parameters for each node, and the platform executes the graph in real time on every incoming frame.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When a product or process changes, you simply modify the relevant nodes. This means&nbsp;<strong>new SKUs can be added in minutes<\/strong>.<\/p>\n<\/div>\n\n\n\n<div class=\"gb-element-846096bf\">\n<style>\n\/* \u2500\u2500 nn-root: layout wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   Background, grid texture and flex centering live here.\n   Font, colour tokens come from mai-system.css via :root.\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-root {\n  font-family: var(--font-body);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  color: var(--nn-text);\n  overflow-x: hidden;\n  position: relative;\n}\n.nn-root::before {\n  content: '';\n  position: absolute;\n  inset: 0;\n  background-image:\n    linear-gradient(rgba(0,212,255,0.03) 1px, transparent 1px),\n    linear-gradient(90deg, rgba(0,212,255,0.03) 1px, transparent 1px);\n  background-size: 40px 40px;\n  pointer-events: none;\n}\n\n\/* \u2500\u2500 Canvas layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-canvas-wrap {\n  position: relative;\n  width: 96%;\n  max-width: 1100px;\n  margin-top: 130px; \/* FIX: Creates clear space for top tooltips *\/\n  margin-bottom: 20px;\n}\n#nn-svg-connections {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  overflow: visible;\n  z-index: 1;\n}\n#nn-nodes-container {\n  position: absolute;\n  inset: 0;\n  z-index: 2;\n  pointer-events: none;\n}\n\n\/* \u2500\u2500 Node \u2014 animation state (extends .nn-node in mai-system.css) \u2500\u2500 *\/\n.nn-node {\n  position: absolute;\n  border-radius: 14px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  cursor: pointer;\n  pointer-events: all;\n  opacity: 0;\n  transform: scale(0.65) translateY(16px);\n}\n.nn-node.visible {\n  animation: nn-nodeAppear 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;\n}\n.nn-node:hover {\n  border-color: var(--mai-orange);\n  box-shadow: 0 0 24px rgba(255,140,53,0.2);\n  z-index: 100; \/* FIX: Forces the hovered node and its tooltip to the very front *\/\n}\n.nn-node::before, .nn-node::after {\n  content: '';\n  position: absolute;\n  width: 10px; height: 10px;\n  border-color: var(--nn-accent);\n  border-style: solid;\n  opacity: 0.4;\n}\n.nn-node::before { top:6px; left:6px;     border-width:1.5px 0 0 1.5px; border-radius:2px 0 0 0; }\n.nn-node::after  { bottom:6px; right:6px; border-width:0 1.5px 1.5px 0; border-radius:0 0 2px 0; }\n\n\/* \u2500\u2500 Pulse ring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-pulse-ring {\n  position: absolute;\n  inset: -5px;\n  border-radius: 18px;\n  border: 1.5px solid var(--mai-orange);\n  opacity: 0;\n  pointer-events: none;\n}\n.nn-node.pulsing .nn-pulse-ring {\n  animation: nn-pulseRing 1.1s ease-out forwards;\n}\n\n\/* \u2500\u2500 Icon wrap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-icon-wrap {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: box-shadow 0.25s;\n  flex-shrink: 0;\n}\n.nn-node:hover .nn-icon-wrap {\n  box-shadow: 0 0 14px rgba(255,140,53,0.35);\n}\n\n\/* \u2500\u2500 Node text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-node-title {\n  font-family: var(--font-body);\n  font-weight: 700;\n  letter-spacing: 0.06em;\n  text-transform: uppercase;\n  color: var(--nn-text);\n  text-align: center;\n  line-height: 1.2;\n  padding: 0 4px;\n}\n\/* Override mai-system nn-node-badge: animation needs font-weight:300, no padding *\/\n.nn-root .nn-node-badge {\n  font-family: var(--font-mono);\n  font-weight: 300;\n  color: var(--nn-subtext);\n  letter-spacing: 0.04em;\n  text-align: center;\n  padding: 0;\n}\n\n\/* \u2500\u2500 Output tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-output-tags {\n  display: flex;\n  gap: 3px;\n  flex-wrap: wrap;\n  justify-content: center;\n  padding: 0 5px;\n}\n.nn-output-tag {\n  font-family: var(--font-mono);\n  font-weight: 500;\n  padding: 2px 5px;\n  border-radius: 4px;\n  border: 1px solid rgba(255,170,0,0.35);\n  color: var(--mai-amber);\n  background: rgba(255,170,0,0.07);\n  letter-spacing: 0.04em;\n  white-space: nowrap;\n}\n\n\/* \u2500\u2500 Node colour classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-nc0 .nn-icon-wrap { background: rgba(0,212,255,0.10);  color: var(--mai-accent);  }\n.nn-nc1 .nn-icon-wrap { background: rgba(0,102,255,0.12);  color: var(--mai-sky);     }\n.nn-nc2 .nn-icon-wrap { background: rgba(0,255,157,0.10);  color: var(--mai-green);   }\n.nn-nc3 .nn-icon-wrap { background: rgba(255,170,0,0.10);  color: var(--mai-amber);   }\n.nn-nc4 .nn-icon-wrap { background: rgba(180,79,255,0.10); color: var(--mai-purple);  }\n.nn-nc5 .nn-icon-wrap { background: rgba(255,107,53,0.10); color: var(--mai-coral);   }\n\n\/* \u2500\u2500 Connection line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-conn-line {\n  fill: none;\n  stroke: var(--mai-orange);\n  stroke-width: 1.8;\n  stroke-linecap: round;\n  stroke-opacity: 0.85;\n}\n\n\/* \u2500\u2500 Preset button row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-btn-row {\n  display: flex;\n  gap: 10px;\n  margin-top: 18px;\n  opacity: 0;\n  animation: nn-fadeIn 0.8s 0.7s forwards;\n  flex-wrap: wrap;\n  justify-content: center;\n  padding: 0 12px;\n}\n.nn-preset-btn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 8px;\n  padding: 13px 22px 11px;\n  border: 1.5px solid var(--nn-node-border);\n  border-radius: 12px;\n  background: var(--nn-node-bg);\n  color: var(--nn-subtext);\n  font-family: var(--font-body);\n  font-size: var(--font-md);\n  font-weight: 700;\n  letter-spacing: 0.08em;\n  cursor: pointer;\n  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s, color 0.2s;\n  text-transform: uppercase;\n  min-width: 128px;\n}\n.nn-preset-btn .nn-btn-label { \nfont-size: var(--font-md); \n}\n\n.nn-preset-btn .nn-btn-sub {\n  font-family: var(--font-mono);\n  font-size: var(--font-xs);\n  font-weight: 400;\n  letter-spacing: 0.08em;\n  color: var(--nn-subtext2);\n  transition: color 0.2s;\n}\n.nn-preset-btn:hover,\n.nn-preset-btn.active {\n  background-color:rgba(255,140,53,0.06);\nborder-color: var(--mai-orange);\n  color: var(--nn-text);\n  box-shadow: 0 0 20px rgba(255,140,53,0.15);\n}\n.nn-preset-btn.active           { background: rgba(255,140,53,0.06); }\n.nn-preset-btn.active .nn-btn-sub { color: var(--mai-orange); }\n\n\/* \u2500\u2500 Button diagram dots\/lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n.nn-btn-diagram { display:flex; align-items:center; gap:3px; }\n.nn-bd  { width:7px; height:7px; border-radius:2px; background:var(--nn-subtext2); flex-shrink:0; transition:background 0.2s; }\n.nn-bl  { width:8px; height:1.5px; background:var(--nn-subtext2); border-radius:1px; flex-shrink:0; transition:background 0.2s; opacity:.7; }\n.nn-bstack { display:flex; flex-direction:column; gap:3px; align-items:flex-start; }\n.nn-brow   { display:flex; align-items:center; gap:3px; }\n.nn-preset-btn:hover .nn-bd,\n.nn-preset-btn.active .nn-bd { background: var(--mai-orange); }\n.nn-preset-btn:hover .nn-bl,\n.nn-preset-btn.active .nn-bl { background: var(--mai-orange); }\n\n\/* \u2500\u2500 Keyframes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n@keyframes nn-nodeAppear {\n  0%   { opacity:0; transform: scale(0.65) translateY(16px); }\n  60%  { opacity:1; transform: scale(1.05) translateY(-3px); }\n  100% { opacity:1; transform: scale(1)    translateY(0);    }\n}\n@keyframes nn-pulseRing {\n  0%   { opacity:0.75; inset:-5px;  }\n  100% { opacity:0;    inset:-20px; }\n}\n@keyframes nn-fadeIn { to { opacity:1; } }\n\n@media (max-width: 600px) {\n  .nn-btn-row    { gap:7px; margin-top:12px; }\n  .nn-preset-btn { padding:10px 14px 9px; min-width:100px; font-size:10px; gap:6px; }\n  .nn-preset-btn .nn-btn-label { font-size:11px; }\n  .nn-preset-btn .nn-btn-sub   { font-size:8px; }\n}\n<\/style>\n\n<div class=\"nn-root\">\n  <div class=\"nn-canvas-wrap\" id=\"nn-canvas-wrap\">\n    <svg id=\"nn-svg-connections\"><\/svg>\n    <div id=\"nn-nodes-container\"><\/div>\n  <\/div>\n\n  <div class=\"nn-btn-row\">\n  <button class=\"nn-preset-btn active\" id=\"nn-btn-0\" onclick=\"nnSelectPreset(0)\">\n    <div class=\"nn-btn-diagram\">\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div>\n    <\/div>\n    <span class=\"nn-btn-label\">4 Nodes<\/span>\n    <span class=\"nn-btn-sub\">Linear Path<\/span>\n  <\/button>\n  <button class=\"nn-preset-btn\" id=\"nn-btn-1\" onclick=\"nnSelectPreset(1)\">\n    <div class=\"nn-btn-diagram\">\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bstack\">\n        <div class=\"nn-brow\"><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><\/div>\n        <div class=\"nn-brow\"><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><\/div>\n      <\/div>\n      <div class=\"nn-bd\"><\/div>\n    <\/div>\n    <span class=\"nn-btn-label\">6 Nodes<\/span>\n    <span class=\"nn-btn-sub\">Branched Path<\/span>\n  <\/button>\n  <button class=\"nn-preset-btn\" id=\"nn-btn-2\" onclick=\"nnSelectPreset(2)\">\n    <div class=\"nn-btn-diagram\">\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bstack\">\n        <div class=\"nn-brow\"><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><\/div>\n        <div class=\"nn-brow\"><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div><\/div>\n      <\/div>\n      <div class=\"nn-bd\"><\/div><div class=\"nn-bl\"><\/div>\n      <div class=\"nn-bd\"><\/div>\n    <\/div>\n    <span class=\"nn-btn-label\">9 Nodes<\/span>\n    <span class=\"nn-btn-sub\">Tree Path<\/span>\n  <\/button>\n<\/div>\n\n  <p class=\"nn-scene-label\">Node-Based Vision Pipeline<\/p>\n\n<\/div>\n\n<script>\nconst NN_ICONS = {\n  crop:     `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><path d=\"M8 3v18M16 3v18M3 8h18M3 16h18\"\/><\/svg>`,\n  identify: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2\"\/><rect x=\"7\" y=\"7\" width=\"10\" height=\"10\" rx=\"1\"\/><\/svg>`,\n  classify: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"3\"\/><path d=\"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83\"\/><\/svg>`,\n  output:   `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"\/><\/svg>`,\n  filter:   `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"\/><\/svg>`,\n  detect:   `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>`,\n  track:    `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"22\" y1=\"12\" x2=\"18\" y2=\"12\"\/><line x1=\"6\" y1=\"12\" x2=\"2\" y2=\"12\"\/><line x1=\"12\" y1=\"6\" x2=\"12\" y2=\"2\"\/><line x1=\"12\" y1=\"22\" x2=\"12\" y2=\"18\"\/><\/svg>`,\n  validate: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"\/><polyline points=\"22 4 12 14.01 9 11.01\"\/><\/svg>`,\n  merge:    `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"18\" cy=\"18\" r=\"3\"\/><circle cx=\"6\" cy=\"6\" r=\"3\"\/><path d=\"M6 21V9a9 9 0 0 0 9 9\"\/><\/svg>`,\n};\n\nconst NN_TOOLTIP_DATA = {\n  crop:     { img: 'url(https:\/\/marshallai.com\/wp-content\/uploads\/2026\/04\/2026-04_MarshallAI_traffic-surveillance3_crop-v2.webp)', code: '{\"action\": \"crop\",\\n \"roi\": [150,300,800,600]}' },\n  classify: { img: 'url(https:\/\/marshallai.com\/wp-content\/uploads\/2026\/04\/2026-04_MarshallAI_traffic-surveillance3_crop_bb.webp)', code: '{\"class\": \"vehicle\",\\n \"conf\": 0.98}' },\n  track:    { img: 'none', code: '{\"action\": \"assign_id\",\\n \"track_id\": \"v_8403\"}' },\n  identify: { img: 'url(https:\/\/marshallai.com\/wp-content\/uploads\/2026\/04\/2026-04_MarshallAI_traffic-surveillance3_crop_bb.webp)', code: '{\"make\": \"Toyota\",\\n \"color\": \"red\"}' },\n  filter:   { img: 'none', code: 'if (color == \"red\")\\n { log_attribute(); }' },\n  detect:   { img: 'url(https:\/\/marshallai.com\/wp-content\/uploads\/2026\/04\/2026-04_MarshallAI_traffic-surveillance3_licenseplate.webp)', code: '{\"feat\": \"ALPR\",\\n \"plate\": \"XYZ-987\"}' },\n  validate: { img: 'none', code: '{\"db\": \"LEZ_registry\",\\n \"plate\": \"XYZ-987\",\\n \"status\": \"low_emission\"}' },\n  merge:    { img: 'none', code: '{\"id\": \"v_8403\",\\n \"plate\": \"XYZ-987\",\\n \"access\": \"granted\"}' },\n  output:   { img: 'none', code: 'POST \/api\/traffic\/log\\n200 OK' }\n};\n\nfunction nnMakeSizes(scale) {\n  return {\n    lg: { w:Math.round(148*scale), h:Math.round(148*scale), icon:Math.round(46*scale), iconR:Math.round(12*scale), titleF:+(11*scale).toFixed(1), badgeF:+(9*scale).toFixed(1),   tagF:+(8*scale).toFixed(1)   },\n    md: { w:Math.round(124*scale), h:Math.round(124*scale), icon:Math.round(38*scale), iconR:Math.round(10*scale), titleF:+(10*scale).toFixed(1), badgeF:+(8.5*scale).toFixed(1), tagF:+(7.5*scale).toFixed(1) },\n    sm: { w:Math.round(104*scale), h:Math.round(104*scale), icon:Math.round(32*scale), iconR:Math.round(9*scale),  titleF:+(9*scale).toFixed(1),  badgeF:+(7.5*scale).toFixed(1), tagF:+(6.5*scale).toFixed(1) },\n  };\n}\n\nconst NN_PRESETS = [\n  {\n    nodes: [\n      { id:0, title:'Image Crop', badge:'Pre-Process', icon:'crop',     fx:0.08, fy:0.5,  size:'lg', col:0 },\n      { id:1, title:'Classifier', badge:'Classification',icon:'classify', fx:0.36, fy:0.5,  size:'lg', col:2 },\n      { id:2, title:'Identifier', badge:'Analysis',    icon:'identify', fx:0.64, fy:0.5,  size:'lg', col:1 },\n      { id:3, title:'Output',     badge:'Result',      icon:'output',   fx:0.92, fy:0.5,  size:'lg', col:3, isOutput:true },\n    ],\n    edges:[ {from:0,to:1},{from:1,to:2},{from:2,to:3} ]\n  },\n  {\n    nodes: [\n      { id:0, title:'Image Crop', badge:'Pre-Process', icon:'crop',     fx:0.06, fy:0.50, size:'md', col:0 },\n      { id:1, title:'Classifier', badge:'Classification',icon:'classify', fx:0.24, fy:0.50, size:'md', col:2 },\n      { id:2, title:'Tracker',    badge:'Identity',    icon:'track',    fx:0.42, fy:0.50, size:'md', col:4 },\n      { id:3, title:'Identify',   badge:'Brand & Color',    icon:'identify', fx:0.66, fy:0.20, size:'md', col:1 },\n      { id:4, title:'Detect',     badge:'ALPR',    icon:'detect',   fx:0.66, fy:0.80, size:'md', col:5 },\n      { id:5, title:'Output',     badge:'Result',      icon:'output',   fx:0.92, fy:0.50, size:'md', col:3, isOutput:true },\n    ],\n    edges:[ {from:0,to:1},{from:1,to:2},{from:2,to:3},{from:2,to:4},{from:3,to:5},{from:4,to:5} ]\n  },\n  {\n    nodes: [\n      { id:0, title:'Image Crop', badge:'Pre-Process', icon:'crop',     fx:0.04, fy:0.50, size:'sm', col:0 },\n      { id:1, title:'Classifier', badge:'Classification',icon:'classify', fx:0.18, fy:0.50, size:'sm', col:2 },\n      { id:2, title:'Tracker',    badge:'Identity',    icon:'track',    fx:0.32, fy:0.50, size:'sm', col:4 },\n      { id:3, title:'Identify',   badge:'Brand & Color',     icon:'identify', fx:0.48, fy:0.20, size:'sm', col:1 },\n      { id:4, title:'Filter',     badge:'Logic Rule',  icon:'filter',   fx:0.66, fy:0.20, size:'sm', col:4 },\n      { id:5, title:'Detect',     badge:'ALPR',        icon:'detect',   fx:0.48, fy:0.80, size:'sm', col:5 },\n      { id:6, title:'Validate',   badge:'DB Check',    icon:'validate', fx:0.66, fy:0.80, size:'sm', col:1 },\n      { id:7, title:'Merge',      badge:'Collate',     icon:'merge',    fx:0.82, fy:0.50, size:'sm', col:2 },\n      { id:8, title:'Output',     badge:'Result',      icon:'output',   fx:0.96, fy:0.50, size:'sm', col:3, isOutput:true },\n    ],\n    edges:[ {from:0,to:1},{from:1,to:2},{from:2,to:3},{from:2,to:5},{from:3,to:4},{from:5,to:6},{from:4,to:7},{from:6,to:7},{from:7,to:8} ]\n  }\n];\n\nlet nnTimers  = [];\nlet nnPackets = [];\nlet nnCurrentPreset = 0;\nlet nnIsVisible = false; \/\/ For the performance saver\n\nfunction nnCt(fn, ms) { const id = setTimeout(fn, ms); nnTimers.push(id); return id; }\n\nfunction nnClearAll() {\n  nnTimers.forEach(clearTimeout);\n  nnTimers = [];\n  nnPackets.forEach(p => { p.running = false; });\n  nnPackets = [];\n  document.getElementById('nn-nodes-container').innerHTML = '';\n  const svg = document.getElementById('nn-svg-connections');\n  while (svg.firstChild) svg.removeChild(svg.firstChild);\n}\n\nfunction nnSelectPreset(idx) {\n  nnCurrentPreset = idx;\n  for (let i = 0; i < 3; i++) {\n    document.getElementById('nn-btn-' + i).classList.toggle('active', i === idx);\n  }\n  nnStartAnimation();\n}\n\nfunction nnGetScale() {\n  const vw = window.innerWidth;\n  if (vw <= 380) return 0.42;\n  if (vw <= 480) return 0.50;\n  if (vw <= 580) return 0.60;\n  if (vw <= 700) return 0.72;\n  if (vw <= 900) return 0.85;\n  return 1.0;\n}\n\nfunction nnStartAnimation() {\n  nnClearAll();\n  const scale  = nnGetScale();\n  const SIZES  = nnMakeSizes(scale);\n  const preset = NN_PRESETS[nnCurrentPreset];\n  const canvasWrap = document.getElementById('nn-canvas-wrap');\n  const CW = canvasWrap.clientWidth;\n  const CH = Math.round(520 * scale);\n  canvasWrap.style.height = CH + 'px';\n\n  const pos = {};\n  preset.nodes.forEach(nd => {\n    const sz = SIZES[nd.size];\n    pos[nd.id] = { cx: nd.fx * CW, cy: nd.fy * CH, x: nd.fx * CW - sz.w \/ 2, y: nd.fy * CH - sz.h \/ 2, w: sz.w, h: sz.h };\n  });\n\n  const adj = {};\n  preset.nodes.forEach(n => { adj[n.id] = []; });\n  preset.edges.forEach(e => adj[e.from].push(e.to));\n\n  const visited = new Set(), order = [], q = [0];\n  while (q.length) {\n    const id = q.shift();\n    if (visited.has(id)) continue;\n    visited.add(id); order.push(id);\n    (adj[id] || []).forEach(nb => q.push(nb));\n  }\n\n  const seqOf = {};\n  order.forEach((id, i) => { seqOf[id] = i; });\n\n  const container = document.getElementById('nn-nodes-container');\n  preset.nodes.forEach(nd => {\n    const sz = SIZES[nd.size];\n    const p  = pos[nd.id];\n    const el = document.createElement('div');\n    el.id = 'nn-node-' + nd.id;\n    el.className = 'nn-node nn-nc' + (nd.col % 6);\n    el.style.cssText = 'left:' + p.x + 'px;top:' + p.y + 'px;width:' + sz.w + 'px;height:' + sz.h + 'px;';\n\n    const iconSvg = NN_ICONS[nd.icon].replace('<svg ', '<svg width=\"' + sz.icon + '\" height=\"' + sz.icon + '\" ');\n    const bottomContent = nd.isOutput\n      ? '<div class=\"nn-output-tags\"><span class=\"nn-output-tag\" style=\"font-size:' + sz.tagF + 'px\">JSON<\/span><span class=\"nn-output-tag\" style=\"font-size:' + sz.tagF + 'px\">HTTPS<\/span><\/div>'\n      : '<div class=\"nn-node-badge\" style=\"font-size:' + sz.badgeF + 'px\">' + nd.badge + '<\/div>';\n\n    \/\/ Tooltip injection\n    const tData = NN_TOOLTIP_DATA[nd.icon];\n    const imgHtml = tData.img !== 'none' ? `<div class=\"nn-tooltip-img\" style=\"background-image: ${tData.img}\"><\/div>` : '';\n    const tooltipHtml = `<div class=\"nn-tooltip\">${imgHtml}<div class=\"nn-tooltip-code\">${tData.code}<\/div><\/div>`;\n\n    el.innerHTML = tooltipHtml + '<div class=\"nn-pulse-ring\"><\/div>'\n      + '<div class=\"nn-icon-wrap\" style=\"width:' + sz.icon + 'px;height:' + sz.icon + 'px;border-radius:' + sz.iconR + 'px\">' + iconSvg + '<\/div>'\n      + '<div class=\"nn-node-title\" style=\"font-size:' + sz.titleF + 'px\">' + nd.title + '<\/div>'\n      + bottomContent;\n    container.appendChild(el);\n  });\n\n  const BASE_STEP = 420;\n  order.forEach((nodeId, seq) => {\n    nnCt(() => {\n      const el = document.getElementById('nn-node-' + nodeId);\n      el.classList.add('visible');\n      nnCt(() => {\n        el.classList.add('pulsing');\n        nnCt(() => el.classList.remove('pulsing'), 1100);\n      }, 340);\n    }, 180 + seq * BASE_STEP);\n  });\n\n  preset.edges.forEach((edge, ei) => {\n    const srcSeq = seqOf[edge.from] !== undefined ? seqOf[edge.from] : 0;\n    const dstSeq = seqOf[edge.to]   !== undefined ? seqOf[edge.to]   : 0;\n    const laterSeq = Math.max(srcSeq, dstSeq);\n    const delay = 180 + laterSeq * BASE_STEP + 480 + ei * 40;\n    nnCt(() => nnDrawEdge(edge, pos), delay);\n  });\n}\n\nfunction nnDrawEdge(edge, pos) {\n  const svg  = document.getElementById('nn-svg-connections');\n  const from = pos[edge.from];\n  const to   = pos[edge.to];\n  if (!from || !to) return;\n\n  const x1 = from.x + from.w, y1 = from.cy;\n  const x2 = to.x,             y2 = to.cy;\n  const dx = x2 - x1;\n  const d  = 'M' + x1 + ',' + y1 + ' C' + (x1+dx*0.5) + ',' + y1 + ' ' + (x2-dx*0.5) + ',' + y2 + ' ' + x2 + ',' + y2;\n\n  const tmp = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'path');\n  tmp.setAttribute('d', d); svg.appendChild(tmp);\n  const len = tmp.getTotalLength();\n  svg.removeChild(tmp);\n\n  const path = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'path');\n  path.setAttribute('d', d);\n  path.setAttribute('class', 'nn-conn-line');\n  path.style.strokeDasharray  = len;\n  path.style.strokeDashoffset = len;\n  svg.appendChild(path);\n\n  requestAnimationFrame(() => requestAnimationFrame(() => {\n    path.style.transition = 'stroke-dashoffset 0.55s cubic-bezier(0.4,0,0.2,1)';\n    path.style.strokeDashoffset = '0';\n  }));\n\n  nnCt(() => nnSpawnPacket(svg, d, len), 620);\n}\n\nfunction nnSpawnPacket(svg, pathD, len) {\n  const mp = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'path');\n  mp.setAttribute('d', pathD); mp.setAttribute('fill', 'none'); mp.setAttribute('stroke', 'none');\n  svg.appendChild(mp);\n\n  const circ = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'circle');\n  circ.setAttribute('r', '3');\n  circ.setAttribute('fill', '#ff8c35');\n  circ.setAttribute('filter', 'drop-shadow(0 0 4px #ff8c35)');\n  svg.appendChild(circ);\n\n  const state = { running: true };\n  nnPackets.push(state);\n  const dur = 800 + Math.random() * 400;\n\n  function run() {\n    if (!state.running) return;\n    \n    \/\/ Performance Saver: Pause animation if not visible\n    if (!nnIsVisible) {\n        requestAnimationFrame(run);\n        return;\n    }\n\n    let start = null;\n    function step(ts) {\n      if (!state.running) { circ.setAttribute('opacity', 0); return; }\n      if (!start) start = ts;\n      const p = Math.min((ts - start) \/ dur, 1);\n      const e = p < 0.5 ? 2*p*p : -1+(4-2*p)*p;\n      try {\n        const pt = mp.getPointAtLength(e * len);\n        circ.setAttribute('cx', pt.x); circ.setAttribute('cy', pt.y);\n        circ.setAttribute('opacity', p < 0.08 ? p\/0.08 : p > 0.88 ? (1-p)\/0.12 : 1);\n      } catch (_) { state.running = false; return; }\n      \n      if (p < 1) requestAnimationFrame(step);\n      else { circ.setAttribute('opacity', 0); setTimeout(run, 500 + Math.random() * 500); }\n    }\n    requestAnimationFrame(step);\n  }\n  run();\n}\n\n\/\/ === INTERSECTION OBSERVER (Performance Saver) ===\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    let hasStartedOnce = false;\n    const observer = new IntersectionObserver((entries) => {\n        entries.forEach(entry => {\n            nnIsVisible = entry.isIntersecting;\n            \/\/ Only trigger the heavy initial draw when it scrolls into view the first time\n            if (nnIsVisible && !hasStartedOnce) {\n                setTimeout(nnStartAnimation, 200);\n                hasStartedOnce = true;\n            }\n        });\n    }, { threshold: 0.1 });\n\n    const wrap = document.getElementById('nn-canvas-wrap');\n    if (wrap) observer.observe(wrap);\n});\n\nvar nnResizeTimer;\nwindow.addEventListener('resize', function() {\n  clearTimeout(nnResizeTimer);\n  nnResizeTimer = setTimeout(nnStartAnimation, 200);\n});\n<\/script>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>The Zero-Code Machine Vision Platform AFSA &#8211; Agile Focused Signal Analysis AFSA is MarshallAI&#8217;s proprietary end-to-end deep learning platform built for high-performance and versatility. Data annotation, classification, and model training are all handled natively within the same interface. No third party tools necessary. Every stage of the processing pipeline \u2014 camera input, preprocessing, model inference, &#8230; <a title=\"Platform\" class=\"read-more\" href=\"https:\/\/marshallai.com\/fi\/platform\/\" aria-label=\"Lue lis\u00e4\u00e4 aiheesta Platform\">Lue lis\u00e4\u00e4<\/a><\/p>","protected":false},"author":1,"featured_media":2619,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"content-type":"","page_theme":"","footnotes":""},"class_list":["post-988","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/pages\/988","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/comments?post=988"}],"version-history":[{"count":15,"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/pages\/988\/revisions"}],"predecessor-version":[{"id":3273,"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/pages\/988\/revisions\/3273"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/media\/2619"}],"wp:attachment":[{"href":"https:\/\/marshallai.com\/fi\/wp-json\/wp\/v2\/media?parent=988"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}