{"context":{"domain":"natura-geomarketing"},"layers":{"ApplicationLayer":{"reference":"ApplicationLayer","availableInstances":[{"reference":"ComponentsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"SystemsDataSet","params":{"filter":{"domain":"data2"},"pagination":{"size":1},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSource"}}}},{"reference":"ExternalComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"SystemsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"5e3b796d6228227ca637d0f4","name":"data2","domain":"data2","organization":"62292184611f480b3191c37c","visibility":"public","updatedAt":"2026-07-27T14:19:36.810Z"}]}},{"reference":"OrganizationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"DataComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"OneFileComponentInstancesDataSet","params":{"pagination":{"size":1},"master":{"dataset":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSet"}},"localField":"system","masterField":"_id"},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"FileComponentInstancesDataSource"}}}},{"reference":"ApplicationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ServiceComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@Data2TableDataSource","reference":"FileComponentInstancesDataSource","params":{"table":"fileComponentInstances","schema":[{"component":"data2-core-types@Field","params":{"key":"component","title":"Component","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"displayName","title":"Display Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"params","title":"Params","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"reference","title":"Reference","component":"data2-core-primitives@String"}}]}},{"component":"data2-core@MemoryDataSource","reference":"EmptyDataSource","params":{"initialData":[]}},{"reference":"ComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"UserComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"DataComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ServiceComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ExternalComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ApplicationsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}}],"processesParams":{}},"HostApplicationLayer":{"reference":"HostApplicationLayer","availableInstances":[{"_id":"5f15ff190528b92601890f71","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"SystemsTableDataSource"}},"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"domain"}}},"pk":"_id","fields":[{"component":"data2-core-types@FieldReference","params":{"key":"_id"}},{"component":"data2-core-types@FieldReference","params":{"key":"name"}},{"component":"data2-core-types@FieldReference","params":{"key":"domain"}},{"component":"data2-core-types@FieldReference","params":{"key":"visibility"}},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization._id"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.name"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.billingStatus"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.publicTrial.active"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"settings.defaultAccessPolicy"}}},"break":true}],"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"staticPagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"policies":[{"component":"data2-core-auth@Policy","params":{"allow":{"params":{"actions":[{"params":{"components":[{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"_id"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"domain"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"}]},"component":"data2-core-logical-operators@Or"}]},"component":"data2-core-flow@Series"}}}]},"reference":"SystemsDataSet","updatedAt":"2026-08-07T21:51:24.037Z","createdAt":"2020-07-20T20:31:21.838Z"},{"_id":"5f1601ce0528b92601890f7c","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":1000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"sort":{"params.package":1,"params.component":1},"pk":"_id","autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"staticFilter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"filter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}}},"reference":"ComponentsDataSet","updatedAt":"2025-11-05T19:37:01.935Z","createdAt":"2020-07-20T20:42:54.898Z"},{"_id":"5f9041dbc8da12be8b94aea4","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":2000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"UserComponentsDataSet","updatedAt":"2025-11-06T21:06:58.095Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"65d8c4fabc9dbb8f94de8d53","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ExternalComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ExternalComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:23.742Z","createdAt":"2024-02-23T16:17:01.816Z"},{"reference":"SystemsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a57cfe9673eb7e3c5e57784","name":"Geomarketing","domain":"natura-geomarketing","visibility":"public","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-14T20:13:15.077Z","createdAt":"2026-07-15T18:22:33.158Z"}]}},{"reference":"ComponentsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"5f52a278a575399a45d239b4","component":"data2-core-types@Component","params":{"package":"data2-core-primitives","component":"Router","interactions":{"component":"data2-core-types@FieldSet","params":{"fields":{"goBack":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.goBack","args":[]}}}},"icon":{"component":"data2-core-primitives@String","params":{"content":"arrow-left"}},"visibility":"public"}},"goTo":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.push","args":[{"component":"data2-core-types@Reference","params":{"reference":"url"}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"preserveQuery"}}}}]}},"parameters":[{"component":"data2-core-primitives@String","params":{"content":"url"}},{"component":"data2-core-primitives@String","params":{"content":"preserveQuery"}}]},"break":true},"icon":{"component":"data2-core-primitives@String","params":{"content":"share"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Path to route"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Path to redirect user"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Boolean"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Preserve Query"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Preserve query string parameters  preserve query parameters on navigating."}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"}]}},"open":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.push","args":[{"pathname":{"component":"data2-core-types@Reference","params":{"reference":"value"}}}]}},"parameters":["value"]}},"icon":{"component":"data2-core-primitives@String","params":{"content":"share"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Link to open"}},"description":{"component":"data2-core-primitives@String","params":{"content":""}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Target"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Where to open the link"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"oneOf":[{"params":{"content":"_blank"},"component":"data2-core-primitives@String"},{"params":{"content":"_self"},"component":"data2-core-primitives@String"},{"params":{"content":"_parent"},"component":"data2-core-primitives@String"},{"params":{"content":"_top"},"component":"data2-core-primitives@String"}]},"component":"data2-core-types@ComponentParameter"}]}},"close":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":"close"}},"parameters":["value"]}},"icon":{"component":"data2-core-primitives@String","params":{"content":"times"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Link to open"}},"description":{"component":"data2-core-primitives@String","params":{"content":""}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Target"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Where to open the link"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"oneOf":[{"params":{"content":"_blank"},"component":"data2-core-primitives@String"},{"params":{"content":"_self"},"component":"data2-core-primitives@String"},{"params":{"content":"_parent"},"component":"data2-core-primitives@String"},{"params":{"content":"_top"},"component":"data2-core-primitives@String"}]},"component":"data2-core-types@ComponentParameter"}],"description":{"component":"data2-core-primitives@String","params":{"content":"Closes a window that was opened via router.open."}}}}}}}}},{"_id":"620d28658271aa4129919d89","component":"data2-core-types@Component","params":{"package":"data2-interface-services","component":"DeviceController","interactions":{"params":{"fields":{"postMessage":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@String","title":{"component":"data2-core-primitives@String","params":{"content":"data"}}}}]},"component":"data2-core-types@ComponentParameter"},"print":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"parameters":{"zoom":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Number"}},"title":{"component":"data2-core-primitives@String","params":{"content":"zoom"}},"visibility":"public"}}},"visibility":"public"},"component":"data2-core-types@ComponentParameter"}],"visibility":"public"},"component":"data2-core-types@ComponentParameter"},"share":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}}},"component":"data2-core-types@ComponentParameter"},"clipboard.copy":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}}}}]},"component":"data2-core-types@ComponentParameter"},"clipboard.paste":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}}}}]},"component":"data2-core-types@ComponentParameter"},"download":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Content"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Options"}},"parameters":{"params":{"fields":{"type":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Mimetype"}}}}}},"component":"data2-core-types@FieldSet"}}}]},"component":"data2-core-types@ComponentParameter"},"downloadUrl":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Url"}}}}]},"component":"data2-core-types@ComponentParameter"},"promptFiles":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@Object","flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"parameters":{"params":{"fields":{"multiple":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Multiple"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Whether or not to allow selecting multiple files"}},"component":"data2-core-primitives@Boolean"}},"acceptedFormats":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Accepted Formats"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Accepted file formats"}},"component":"data2-core-primitives@String","list":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"timeout":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Timeout"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Max milliseconds to wait for file upload"}},"component":"data2-core-primitives@Number"}}}},"component":"data2-core-types@FieldSet"}}}],"visibility":"public"},"component":"data2-core-types@ComponentParameter"},"meetingRecord":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","definition":{"params":{"actions":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"meeting"}},"script":{"component":"data2-core-primitives@String","params":{"content":"(function() {\n  let mediaRecorder;\n  let audioChunks = [];\n  let combinedStream;\n  let audioContext;\n  \n  window.meetingCapture = async function() {\n    try {\n      // Solicitar acesso ao áudio da guia (e vídeo, que será ignorado depois)\n      const tabStream = await navigator.mediaDevices.getDisplayMedia({ \n        video: true, // Necessário para alguns navegadores\n        audio: true\n      });\n      \n      // Remover a trilha de vídeo\n      const tabAudioTrack = tabStream.getAudioTracks()[0];\n      \n      // Solicitar acesso ao microfone\n      const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n      const micAudioTrack = micStream.getAudioTracks()[0];\n      \n      // Criar um novo MediaStream com ambas as trilhas de áudio\n      combinedStream = new MediaStream([tabAudioTrack, micAudioTrack]);\n      \n      // Criar um contexto de áudio para misturar os streams\n      audioContext = new AudioContext();\n      const destination = audioContext.createMediaStreamDestination();\n      \n      // Conectar a trilha de áudio da guia\n      const tabSource = audioContext.createMediaStreamSource(new MediaStream([tabAudioTrack]));\n      tabSource.connect(destination);\n      \n      // Conectar a trilha de áudio do microfone\n      const micSource = audioContext.createMediaStreamSource(new MediaStream([micAudioTrack]));\n      micSource.connect(destination);\n      \n      // Iniciar gravação com o stream misturado\n      mediaRecorder = new MediaRecorder(destination.stream);\n      audioChunks = [];\n      mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);\n      mediaRecorder.start();\n      \n      console.log('Gravação iniciada. Chame meetingCaptureStop() para finalizar.');\n    } catch (error) {\n      console.error('Erro ao iniciar a captura:', error);\n    }\n  };\n  \n  window.meetingCaptureStop = function() {\n    return new Promise((resolve, reject) => {\n      if (!mediaRecorder) {\n        reject('Nenhuma gravação em andamento.');\n        return;\n      }\n      \n      mediaRecorder.onstop = () => {\n        const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });\n        combinedStream.getTracks().forEach(track => track.stop());\n        audioContext.close();\n        \n        resolve(audioBlob);\n        console.log('Gravação finalizada. O arquivo de áudio está pronto para upload.');\n      };\n      \n      mediaRecorder.stop();\n    });\n  };\n})();"}}},"component":"data2-core-web@Script"},{"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":{"component":"data2-core-primitives@String","params":{"content":"meetingCapture"}}}}]},"component":"data2-core-flow@Series"}},"component":"data2-core-types@ComponentParameter"},"meetingRecordStop":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Content"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Options"}},"parameters":{"params":{"fields":{"type":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Mimetype"}}}}}},"component":"data2-core-types@FieldSet"}}}],"definition":{"params":{"actions":[{"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":{"component":"data2-core-primitives@String","params":{"content":"meetingCaptureStop"}}}}]},"component":"data2-core-flow@Series"}},"component":"data2-core-types@ComponentParameter"},"notify":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"private","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@Object","parameters":{"title":{"params":{"component":"data2-core-primitives@String","index":{"component":"data2-core-primitives@Number","params":{"content":0}},"title":{"component":"data2-core-primitives@String","params":{"content":"title"}}},"component":"data2-core-types@ComponentParameter"},"sound":{"params":{"component":"data2-core-types@FileComponentUrlReference","index":{"component":"data2-core-primitives@Number","params":{"content":1}},"title":{"component":"data2-core-primitives@String","params":{"content":"sound"}}},"component":"data2-core-types@ComponentParameter"}},"flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}}]},"component":"data2-core-types@ComponentParameter"}}},"component":"data2-core-types@FieldSet"}}},{"_id":"6a6997e39279f4a770dce0a0","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"yJwIwtjYNeYGoGF68aYQM","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst PALETTE=['#8b7fd6','#d1b048','#67bd5b','#5aa9d6','#d67fa8','#c98a4b'];\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%;box-sizing:border-box}\n    .wrap{position:relative;height:100%;display:flex;flex-direction:column;font:12px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Inter,sans-serif;background:#f4f4f8}\n    .stage{flex:1;min-height:0;position:relative;cursor:grab}\n    .stage:active{cursor:grabbing}\n    svg{width:100%;height:100%;display:block}\n    .muni{stroke:#0b0d12;stroke-width:1.1;transition:filter .1s}\n    .muni.ctx{cursor:default}\n    .muni:not(.ctx){cursor:pointer}\n    .muni:not(.ctx):hover{filter:brightness(1.25)}\n    .wedge{cursor:pointer;stroke:#fff;stroke-width:0.6;transition:filter .1s}\n    .wedge:hover{filter:brightness(1.25)}\n    .bg{fill:#ffffff;stroke:#1a1a1a;stroke-width:.5;opacity:.5}\n    .mlabel{fill:#1a1a1a;font-size:9px;font-weight:700;pointer-events:none;user-select:none;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.6);stroke-width:0.5px}\n    .legend{display:flex;gap:10px;flex-wrap:wrap;padding:8px 10px;border-top:1px solid #e7e7ef;background:#fff}\n    .legend span{display:inline-flex;align-items:center;gap:5px}\n    .legend b{width:10px;height:10px;border-radius:3px;display:inline-block}\n    .load{position:absolute;inset:0;background:rgba(244,244,248,.92);display:flex;align-items:center;justify-content:center;z-index:5;color:#71717f;text-align:center;padding:16px;cursor:default}\n    .load.err{cursor:pointer;text-decoration:underline;color:#c8104e}\n    .load.hidden{display:none}\n    .tip{position:fixed;pointer-events:none;background:#000;color:#fff;border-radius:7px;padding:6px 8px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:220px}\n  `,\n  render(){\n    return `<div class=\"wrap\">\n      <div class=\"stage\"><svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg></div>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"tip\" id=\"tip\"></div>\n      <div class=\"load\" id=\"loader\">carregando malhas…</div>\n    </div>`;\n  },\n  mounted(d2, root){\n\n    const $ = s => root.querySelector(s);\n    const NS = 'http://www.w3.org/2000/svg';\n    const norm = s => (s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\n    const esc = s => (s==null?'':String(s)).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n    const splitUfs = s => (s||'').split(',').map(x=>x.trim().toUpperCase()).filter(Boolean);\n\n    async function geoUF(uf){\n      const code = UF_CODE[uf] || uf;\n      const r = await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');\n      if (!r.ok) throw new Error('geo ' + uf);\n      return r.json();\n    }\n    function buildAdj(features){\n      const pts = f => {\n        const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates], s=new Set();\n        ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));\n        return s;\n      };\n      const feat = {}, idx = {};\n      features.forEach(f => { const nm = norm(f.properties.name || f.properties.NOME || f.properties.nome); feat[nm] = { f, pts: pts(f) }; });\n      for (const nm in feat) feat[nm].pts.forEach(p => (idx[p] = idx[p] || []).push(nm));\n      const nbrs = nm => {\n        const c = {};\n        feat[nm].pts.forEach(p => idx[p].forEach(o => { if (o !== nm) c[o] = (c[o] || 0) + 1; }));\n        return Object.entries(c).filter(([, n]) => n >= 2).map(([o]) => o);\n      };\n      return { feat, nbrs };\n    }\n    function project(feats){\n      let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n      const each = cb => feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n      each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n      const mlat=(mny+mxy)/2, kx=Math.cos(mlat*Math.PI/180), W=700,H=500,pad=22;\n      const gw=(mxx-mnx)*kx, gh=(mxy-mny), sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n      const ox=(W-gw*sc)/2, oy=(H-gh*sc)/2;\n      const pr = p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n      pr.scale = sc;\n      return pr;\n    }\n    function pathFor(f,pr){\n      const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');\n    }\n    function cxy(f,pr){\n      let x=0,y=0,n=0; const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));\n      return [x/n, y/n];\n    }\n    // Clips a projected ring (screen-space [x,y] points) against a single straight half-plane\n    // (side(x,y) >= 0 is kept) via Sutherland-Hodgman. Unlike a wedge radiating from one apex —\n    // which only covers a shape the apex can \"see\" the whole border of — a straight-line cut is\n    // exact for ANY simple polygon, convex or concave, so stacking these never leaves a gap.\n    function clipHalfPlane(ring, side){\n      const out = [];\n      const n = ring.length;\n      for (let i = 0; i < n; i++){\n        const curr = ring[i], prev = ring[(i - 1 + n) % n];\n        const sCurr = side(curr[0], curr[1]);\n        const sPrev = side(prev[0], prev[1]);\n        const currIn = sCurr >= -1e-9;\n        const prevIn = sPrev >= -1e-9;\n        if (currIn) {\n          if (!prevIn) out.push(intersectHalfPlane(prev, curr, side));\n          out.push(curr);\n        } else if (prevIn) {\n          out.push(intersectHalfPlane(prev, curr, side));\n        }\n      }\n      return out;\n    }\n    function intersectHalfPlane(p1, p2, side){\n      const s1 = side(p1[0], p1[1]), s2 = side(p2[0], p2[1]);\n      const t = s1 / (s1 - s2);\n      return [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];\n    }\n    // Full projected bounding box of a município — used to lay bands across its ENTIRE geometry\n    // (all MultiPolygon parts included), so the bands always tile the whole shape, islands too.\n    function bboxOf(f, pr){\n      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n      const g = f.geometry, ps = g.type === 'MultiPolygon' ? g.coordinates : [g.coordinates];\n      ps.forEach(poly => poly.forEach(ring => ring.forEach(p => {\n        const q = pr(p);\n        minX = Math.min(minX, q[0]); maxX = Math.max(maxX, q[0]);\n        minY = Math.min(minY, q[1]); maxY = Math.max(maxY, q[1]);\n      })));\n      return [minX, minY, maxX, maxY];\n    }\n    // A vertical band [x0,x1) of the município, clipped against its REAL border on both sides.\n    function stripeClipPath(f, pr, x0, x1){\n      const g = f.geometry, ps = g.type === 'MultiPolygon' ? g.coordinates : [g.coordinates];\n      let d = '';\n      ps.forEach(poly => poly.forEach(ring => {\n        let pts = ring.map(p => pr(p));\n        pts = clipHalfPlane(pts, (x) => x - x0);\n        if (pts.length) pts = clipHalfPlane(pts, (x) => x1 - x);\n        if (pts.length >= 3) d += 'M' + pts.map(q => q[0].toFixed(1) + ',' + q[1].toFixed(1)).join('L') + 'Z';\n      }));\n      return d;\n    }\n    const loader = (on, t, isErr) => {\n      const l = $('#loader');\n      l.classList.toggle('hidden', !on);\n      l.classList.toggle('err', !!isErr);\n      if (t != null) l.textContent = t;\n    };\n\n    let S = { assign: { ...(d2.param('assignments', {}) || {}) }, targets: new Set(), clickable: new Set(), closeFeats: {}, nbrFeats: [], labels: {}, pr: null, view: { scale: 1, tx: 0, ty: 0 }, groupAssign: {} };\n    let dragStart = null, dragMoved = false;\n\n    // Live sector for a município: the Store (if bound) is the source of truth —\n    // reads are tracked, so an external change to the Store re-renders this map.\n    // `S.assign` is only the bootstrap default (from `assignments`) before the\n    // Store has an entry, or the fallback when no `store` param is bound at all.\n    function getAssign(key){\n      const storeRef = d2.param('store');\n      if (!storeRef) return S.assign[key];\n      const prop = d2.param('prop');\n      if (prop) {\n        const obj = d2.store(storeRef).get(prop, {}) || {};\n        return key in obj ? obj[key] : S.assign[key];\n      }\n      return d2.store(storeRef).get(key, S.assign[key]);\n    }\n\n    // Writes back through the same `prop` indirection as getAssign(): when `prop` is set,\n    // values land on store[prop][key] (a nested object) instead of store[key] directly —\n    // the same shape a Setter targeting `@Store.prop` would produce.\n    function setAssign(key, value){\n      S.assign[key] = value;\n      const storeRef = d2.param('store');\n      if (!storeRef) return;\n      const prop = d2.param('prop');\n      if (prop) {\n        const obj = { ...(d2.store(storeRef).get(prop, {}) || {}) };\n        obj[key] = value;\n        d2.store(storeRef).set(prop, obj);\n      } else {\n        d2.store(storeRef).set(key, value);\n      }\n    }\n\n    // A município can hold MULTIPLE sectors at once — stored as an array instead of\n    // a single string. cycleAt() advances just one slot of that array (one pie wedge)\n    // through `groups`, padding the array if the clicked index didn't exist yet.\n    // `groups` is now an array of { sector, municipio } — sector is the value cycled\n    // through on click; municipio (comma-separated) statically pins that sector's color\n    // onto municípios that aren't part of the interactive store-backed assignment.\n    function sectorNames(){\n      return (d2.param('groups', []) || []).map(g => g.sector);\n    }\n\n    function cycleAt(key, idx, arrLen){\n      const gs = sectorNames();\n      if (!gs.length) return null;\n      let arr = getAssign(key);\n      arr = Array.isArray(arr) ? [...arr] : [];\n      while (arr.length < arrLen) arr.push('');\n      const next = gs[(gs.indexOf(arr[idx]) + 1) % gs.length];\n      arr[idx] = next;\n      setAssign(key, arr);\n      return next;\n    }\n\n    function svgPoint(evt){\n      const svg = $('#map');\n      const pt = svg.createSVGPoint();\n      pt.x = evt.clientX; pt.y = evt.clientY;\n      const ctm = svg.getScreenCTM();\n      if (!ctm) return { x: 0, y: 0 };\n      const p = pt.matrixTransform(ctm.inverse());\n      return { x: p.x, y: p.y };\n    }\n    function applyTransform(){\n      const g = $('#viewport');\n      if (g) g.setAttribute('transform', `translate(${S.view.tx} ${S.view.ty}) scale(${S.view.scale})`);\n    }\n    d2.on('.stage', 'wheel', e => {\n      e.preventDefault();\n      const p = svgPoint(e);\n      const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;\n      const next = Math.min(8, Math.max(0.15, S.view.scale * factor));\n      const k = next / S.view.scale;\n      S.view.tx = p.x - k * (p.x - S.view.tx);\n      S.view.ty = p.y - k * (p.y - S.view.ty);\n      S.view.scale = next;\n      applyTransform();\n    });\n    d2.on('.stage', 'mousedown', e => {\n      if (e.button !== 0) return;\n      const p = svgPoint(e);\n      dragStart = { x: p.x, y: p.y, tx: S.view.tx, ty: S.view.ty };\n      dragMoved = false;\n    });\n    d2.on(window, 'mousemove', e => {\n      if (!dragStart) return;\n      const p = svgPoint(e);\n      const dx = p.x - dragStart.x, dy = p.y - dragStart.y;\n      if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) dragMoved = true;\n      S.view.tx = dragStart.tx + dx;\n      S.view.ty = dragStart.ty + dy;\n      applyTransform();\n    });\n    d2.on(window, 'mouseup', () => { dragStart = null; });\n\n    // This component paints imperatively (mounted-driven SVG, not the reactive\n    // render() string), so a Store read here isn't auto-tracked the way it would\n    // be inside render(). Poll the bound Store's sector values every frame and\n    // redraw when any of them changed — this is what makes an EXTERNAL write to\n    // the Store (not just this map's own clicks) show up on the map.\n    let lastAssignSnapshot = {};\n    d2.raf(() => {\n      const storeRef = d2.param('store');\n      if (!storeRef) return;\n      let changed = false;\n      Object.keys(S.closeFeats).forEach(key => {\n        const v = getAssign(key);\n        // Arrays (multi-sector municípios) are compared by a joined signature, not by\n        // reference — a store that clones on read would otherwise look \"changed\" every frame.\n        const sig = Array.isArray(v) ? v.join('\u0001') : v;\n        if (lastAssignSnapshot[key] !== sig) { changed = true; lastAssignSnapshot[key] = sig; }\n      });\n      if (changed) drawMap();\n    });\n\n    function drawMap(){\n      const svg = $('#map'); svg.innerHTML = '';\n      const viewport = document.createElementNS(NS, 'g');\n      viewport.setAttribute('id', 'viewport');\n      svg.appendChild(viewport);\n      applyTransform();\n      const groupDefs = d2.param('groups', []) || [];\n      const sectors = groupDefs.map(g => g.sector);\n      const colors = d2.param('colors', {});\n      const colorFor = g => colors[g] || PALETTE[sectors.indexOf(g) % PALETTE.length] || '#94a3b8';\n      // Base label size tracks the projection's own fit scale, so labels stay proportionate\n      // to the shapes whether the auto-fit is tight (one neighborhood) or wide (distant targets).\n      const labelFontSize = Math.max(4, Math.min(14, 9 * ((S.pr.scale || 1000) / 1000)));\n\n      S.nbrFeats.forEach(f => {\n        const p = document.createElementNS(NS, 'path');\n        p.setAttribute('d', pathFor(f, S.pr));\n        p.setAttribute('class', 'bg');\n        p.setAttribute('data-key', 'bg-' + (f.properties.name || f.properties.NOME || f.properties.nome || ''));\n        viewport.appendChild(p);\n      });\n\n      Object.entries(S.closeFeats).forEach(([key, f]) => {\n        const clickable = S.clickable.has(key);\n        const groupSectors = !clickable ? S.groupAssign[key] : null;\n        const groupSector = groupSectors ? groupSectors[0] : null;\n        const currentVal = getAssign(key);\n        const arrVal = Array.isArray(currentVal) ? currentVal : null;\n        const isPie = !!(arrVal && arrVal.length >= 2 && clickable);\n        // Static group pins can also stack more than one sector onto the same município\n        // (e.g. two `groups[]` entries both listing it) — split those into a pie too.\n        const isGroupPie = !!(groupSectors && groupSectors.length >= 2);\n        const soloSector = arrVal ? arrVal[0] : currentVal;\n        const p = document.createElementNS(NS, 'path');\n        p.setAttribute('d', pathFor(f, S.pr));\n        p.setAttribute('data-key', key);\n        p.setAttribute('class', ['muni', clickable ? '' : 'ctx'].filter(Boolean).join(' '));\n\n        if (isPie || isGroupPie) {\n          // Multiple sectors share this município — the shape itself is a neutral\n          // safety-net backdrop; the wedges below (clipped to the REAL border, one per\n          // array entry) carry the actual colors and fully cover the município's area.\n          const pieSectors = isPie ? arrVal : groupSectors;\n          p.setAttribute('fill', '#eef0f5');\n          p.onmousemove = e => {\n            const tip = $('#tip');\n            tip.style.opacity = 1; tip.style.left = (e.clientX + 12) + 'px'; tip.style.top = (e.clientY + 12) + 'px';\n            const names = pieSectors.map(v => v || 'sem destino').join(', ');\n            tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b><br>${pieSectors.length} setores: ${esc(names)}`;\n          };\n          p.onmouseleave = () => { $('#tip').style.opacity = 0; };\n        } else {\n          if (groupSector) {\n            p.setAttribute('fill', colorFor(groupSector));\n            p.setAttribute('fill-opacity', '0.3');\n          } else {\n            p.setAttribute('fill', soloSector ? colorFor(soloSector) : (clickable ? '#94a3b8' : '#dcdfe8'));\n          }\n          if (clickable) {\n            p.onclick = () => {\n              if (dragMoved) { dragMoved = false; return; }\n              let next;\n              if (arrVal) {\n                next = cycleAt(key, 0, arrVal.length || 1);\n              } else {\n                const gs = sectorNames();\n                if (!gs.length) return;\n                next = gs[(gs.indexOf(getAssign(key)) + 1) % gs.length];\n                setAssign(key, next);\n              }\n              drawMap();\n              d2.emit('onChange', arrVal ? { municipio: key, sectorIndex: 0, setor: next } : { municipio: key, setor: next });\n            };\n          }\n          p.onmousemove = e => {\n            const tip = $('#tip');\n            tip.style.opacity = 1; tip.style.left = (e.clientX + 12) + 'px'; tip.style.top = (e.clientY + 12) + 'px';\n            const label = groupSector || soloSector || (clickable ? 'sem destino' : 'apenas contexto');\n            tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b><br>${esc(label)}`;\n          };\n          p.onmouseleave = () => { $('#tip').style.opacity = 0; };\n        }\n        viewport.appendChild(p);\n\n        const [cx, cy] = cxy(f, S.pr);\n\n        if (isPie || isGroupPie) {\n          const wedgeSectors = isPie ? arrVal : groupSectors;\n          const n = wedgeSectors.length;\n          // Bands across the FULL bounding box of this município, not wedges around a center —\n          // a center point can't always \"see\" a concave shape's whole border, which used to leave\n          // slivers uncovered; straight parallel cuts always tile the entire shape exactly.\n          const [bx0, , bx1] = bboxOf(f, S.pr);\n          const stripeW = (bx1 - bx0) / n;\n          wedgeSectors.forEach((sector, i) => {\n            const d = stripeClipPath(f, S.pr, bx0 + i * stripeW, bx0 + (i + 1) * stripeW);\n            if (!d) return;\n            const wedge = document.createElementNS(NS, 'path');\n            wedge.setAttribute('d', d);\n            wedge.setAttribute('class', 'wedge');\n            wedge.setAttribute('fill', sector ? colorFor(sector) : '#94a3b8');\n            // Static group pins (`groups[]`) aren't interactable — keep them at ~30% opacity so\n            // they read as context, distinct from a real Store-backed sector assignment.\n            wedge.setAttribute('fill-opacity', isPie ? '1' : '0.3');\n            wedge.style.cursor = isPie ? 'pointer' : 'default';\n            if (isPie) {\n              wedge.onclick = () => {\n                if (dragMoved) { dragMoved = false; return; }\n                const next = cycleAt(key, i, n);\n                drawMap();\n                d2.emit('onChange', { municipio: key, sectorIndex: i, setor: next });\n              };\n            }\n            wedge.onmousemove = e => {\n              const tip = $('#tip');\n              tip.style.opacity = 1; tip.style.left = (e.clientX + 12) + 'px'; tip.style.top = (e.clientY + 12) + 'px';\n              tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b> — setor ${i + 1}/${n}<br>${esc(sector || 'sem destino')}`;\n            };\n            wedge.onmouseleave = () => { $('#tip').style.opacity = 0; };\n            viewport.appendChild(wedge);\n          });\n        }\n\n        const t = document.createElementNS(NS, 'text');\n        t.setAttribute('class', 'mlabel'); t.setAttribute('x', cx); t.setAttribute('y', cy);\n        t.style.fontSize = labelFontSize + 'px';\n        t.textContent = (S.labels[key] || key).slice(0, 13);\n        viewport.appendChild(t);\n      });\n\n      $('#legend').innerHTML = sectors.map(g => `<span><b style=\"background:${colorFor(g)}\"></b>${esc(g)}</span>`).join('');\n    }\n\n    async function load(){\n      loader(true, 'carregando malhas…', false);\n      try {\n        const ufs = splitUfs(d2.param('ufs', 'PR'));\n        if (!ufs.length) throw new Error('informe ao menos uma UF em \"ufs\"');\n        const manualNbr = splitUfs(d2.param('neighborUfs', ''));\n        const autoNbr = ufs.flatMap(u => UF_BORDERS[u] || []);\n        const nbrUfs = [...new Set(manualNbr.length ? manualNbr : autoNbr)].filter(u => !ufs.includes(u));\n        const allUfs = [...new Set([...ufs, ...nbrUfs])];\n        const geos = await Promise.all(allUfs.map(u => geoUF(u).catch(() => null)));\n\n        const closeUfSet = new Set(ufs);\n        const allFeats = [];\n        const stateNames = [];\n        allUfs.forEach((u, i) => {\n          const g = geos[i]; if (!g) return;\n          const inClose = closeUfSet.has(u);\n          g.features.forEach(f => {\n            allFeats.push(f);\n            if (inClose) stateNames.push(norm(f.properties.name || f.properties.NOME || f.properties.nome || ''));\n          });\n        });\n        if (!allFeats.length) throw new Error('nenhuma malha carregada para essas UFs');\n\n        const adj = buildAdj(allFeats);\n\n        const targetsRaw = (d2.param('municipio', '') || '').split(',').map(x => x.trim()).filter(Boolean);\n        const targetNames = targetsRaw.map(norm);\n        const foundTargets = targetNames.filter(t => adj.feat[t]);\n        if (targetNames.length && !foundTargets.length) throw new Error('município(s) não encontrado(s) na(s) UF(s) informada(s): ' + targetsRaw.join(', '));\n        S.targets = new Set(foundTargets);\n\n        // Interactive scope (\"close\"): each target município (comma-separated in `municipio`)\n        // + its direct neighbors — NOT the whole state. Falls back to the whole state when no\n        // `municipio` is set.\n        const neighborhoodNames = foundTargets.length\n          ? [...new Set(foundTargets.flatMap(t => [t, ...adj.nbrs(t)]))]\n          : stateNames;\n\n        // Statically pins a sector's color onto whatever municípios are listed in that\n        // group's `municipio` field — these are display-only, not interactively assignable.\n        // A município can be listed by more than one group (e.g. Astorga under both Setor A\n        // and Setor B) — keep every sector that claims it so drawMap() can split it into a pie.\n        const groupAssign = {};\n        (d2.param('groups', []) || []).forEach(g => {\n          (g.municipio || '').split(',').map(x => x.trim()).filter(Boolean).forEach(n => {\n            const key = norm(n);\n            (groupAssign[key] = groupAssign[key] || []).push(g.sector);\n          });\n        });\n\n        // A município already carrying a sector assignment joins the interactive set even when\n        // it's not adjacent to the target — this is what `neighborUfs` is for: letting a sector\n        // reach into a município from a bordering state, as long as that UF was loaded.\n        const assignedSet = new Set(Object.keys(S.assign).map(norm));\n        const assignedLoaded = [...assignedSet].filter(n => adj.feat[n]);\n        const groupLoaded = Object.keys(groupAssign).filter(n => adj.feat[n]);\n        const closingNames = [...new Set([...neighborhoodNames, ...assignedLoaded, ...groupLoaded])];\n        const closingSet = new Set(closingNames);\n\n        S.closeFeats = {}; S.labels = {};\n        closingNames.forEach(n => { if (adj.feat[n]) { S.closeFeats[n] = adj.feat[n].f; S.labels[n] = adj.feat[n].f.properties.name || n; } });\n\n        S.clickable = new Set(closingNames.filter(n => assignedSet.has(n)));\n        S.groupAssign = groupAssign;\n\n        // Everything else loaded (rest of the state / neighbor states) is faint background context.\n        const restNames = new Set();\n        allFeats.forEach(f => {\n          const n = norm(f.properties.name || f.properties.NOME || f.properties.nome || '');\n          if (adj.feat[n] && !closingSet.has(n)) restNames.add(n);\n        });\n        S.nbrFeats = [...restNames].map(n => adj.feat[n].f);\n\n        const zoomFeats = Object.values(S.closeFeats);\n        S.pr = project(zoomFeats.length ? zoomFeats : allFeats);\n        S.view = { scale: 1, tx: 0, ty: 0 };\n        drawMap();\n        loader(false);\n      } catch (e) {\n        loader(true, 'erro: ' + e.message + ' (clique p/ tentar de novo)', true);\n      }\n    }\n\n    $('#loader').onclick = () => {\n      if (!$('#loader').classList.contains('err')) return;\n      S.closeFeats = {}; S.nbrFeats = []; S.labels = {};\n      load();\n    };\n\n    load();\n  },\n};"}},"assignments":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"assignments"}}}},"colors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"colors"}}}},"groups":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"groups"}}}},"municipio":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipio"}}}},"neighborUfs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"neighborUfs"}}}},"onChange":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onChange"}}}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"ufs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ufs"}}}},"prop":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prop"}}}}}}}}}},{"_id":"6a71ddb4a9d7cf0ea99224f1","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"FjpgPxu84sgMMjMKJhyUm","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst UF_NAMES={RO:'Rondônia',AC:'Acre',AM:'Amazonas',RR:'Roraima',PA:'Pará',AP:'Amapá',TO:'Tocantins',\n MA:'Maranhão',PI:'Piauí',CE:'Ceará',RN:'Rio Grande do Norte',PB:'Paraíba',PE:'Pernambuco',AL:'Alagoas',\n SE:'Sergipe',BA:'Bahia',MG:'Minas Gerais',ES:'Espírito Santo',RJ:'Rio de Janeiro',SP:'São Paulo',\n PR:'Paraná',SC:'Santa Catarina',RS:'Rio Grande do Sul',MS:'Mato Grosso do Sul',MT:'Mato Grosso',\n GO:'Goiás',DF:'Distrito Federal'};\nconst REGION_UFS={N:['RO','AC','AM','RR','PA','AP','TO'],NE:['MA','PI','CE','RN','PB','PE','AL','SE','BA'],\n CO:['MS','MT','GO','DF'],SE:['MG','ES','RJ','SP'],S:['PR','SC','RS']};\nconst REGION_NAMES={N:'Norte',NE:'Nordeste',CO:'Centro-Oeste',SE:'Sudeste',S:'Sul'};\nconst PALETTE=['#8b7fd6','#d1b048','#67bd5b','#5aa9d6','#d67fa8','#c98a4b'];\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%;box-sizing:border-box}\n    .wrap{position:relative;height:100%;display:flex;flex-direction:column;font:13px -apple-system,BlinkMacSystemFont,\"SF Pro Text\",Inter,sans-serif;background:transparent;overflow:hidden;border-radius:0px;border:0px solid rgba(0,0,0,.08);box-shadow:0 6px 20px rgba(0,0,0,.12)}\n    .stage{flex:1;min-height:0;position:relative;cursor:grab;touch-action:none}\n    .stage:active{cursor:grabbing}\n    svg{width:100%;height:100%;display:block}\n    .area{stroke:rgba(255,255,255,.2);stroke-width:.85;stroke-linejoin:round;stroke-linecap:round}\n    .wedge{stroke:rgba(255,255,255,.7);stroke-width:.5;stroke-linejoin:round;cursor:pointer;transition:filter .15s ease}\n    .wedge:hover{filter:brightness(1.16)}\n    .wedge.touchy{stroke-width:.9}\n    .lbl{fill:#1D1D1F;font-size:9px;font-weight:600;pointer-events:none;user-select:none;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.7);stroke-width:.6px}\n    .legend{display:none!important;position:absolute;left:10px;bottom:10px;max-width:64%;max-height:70%;overflow:auto;flex-direction:column;gap:8px;padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.78);-webkit-backdrop-filter:blur(30px) saturate(180%);backdrop-filter:blur(30px) saturate(180%);border:1px solid rgba(0,0,0,.08);font-size:11px;box-shadow:0 8px 24px rgba(0,0,0,.20)}\n    .legend .grp{display:flex;flex-direction:column;gap:3px}\n    .legend .grpname{font-weight:700;color:rgba(0,0,0,.5);font-size:10px;text-transform:uppercase;letter-spacing:.06em}\n    .legend .row{display:flex;align-items:center;gap:6px;color:#1D1D1F}\n    .legend b{width:10px;height:10px;border-radius:3px;display:inline-block;flex:0 0 auto;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}\n    .tip{display:none!important;position:fixed;pointer-events:none;background:rgba(255,255,252,.95);-webkit-backdrop-filter:blur(20px) saturate(150%);backdrop-filter:blur(20px) saturate(150%);color:#1D1D1F;border:1px solid rgba(0,0,0,.08);border-radius:6px;padding:6px 9px;font-size:11px;opacity:0;transition:opacity .12s ease;box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:9;max-width:220px}\n    .infobar{display:none!important;position:absolute;left:0;right:0;bottom:0;background:rgba(246,246,247,.85);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);border-top:1px solid rgba(0,0,0,.1);color:#1D1D1F;padding:10px 14px;font-size:13px;display:flex;justify-content:space-between;align-items:center;gap:10px;transform:translateY(100%);transition:transform .18s ease;z-index:8}\n    .infobar.show{transform:translateY(0)}\n    .infobar .x{opacity:.5;cursor:pointer;padding:2px 7px;border-radius:5px;transition:background .12s ease,opacity .12s ease}\n    .infobar .x:hover{opacity:.9;background:rgba(0,0,0,.06)}\n    .load{position:absolute;inset:0;background:rgba(236,236,236,.94);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:5;color:rgba(0,0,0,.5);text-align:center;padding:16px;cursor:default;font-size:13px}\n    .load.err{cursor:pointer;text-decoration:underline;color:#FF3B30}\n    .empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(0,0,0,.5);text-align:center;padding:24px;font-size:13px}\n    .hidden{display:none}\n  `,\n  render(){\n    return `<div class=\"wrap\">\n      <div class=\"stage\"><svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg></div>\n      <div class=\"legend hidden\" id=\"legend\"></div>\n      <div class=\"tip\" id=\"tip\"></div>\n      <div class=\"infobar\" id=\"infobar\"><span id=\"infobarText\"></span><span class=\"x\" id=\"infobarClose\">✕</span></div>\n      <div class=\"load\" id=\"loader\">carregando malhas do Brasil…</div>\n      <div class=\"empty hidden\" id=\"empty\">Nenhuma localização declarada — informe country_list, region_list, state_list, city_list ou municipality_list.</div>\n    </div>`;\n  },\n  mounted(d2, root){\n\n    const $ = s => root.querySelector(s);\n    const NS = 'http://www.w3.org/2000/svg';\n    const norm = s => (s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\n    const esc = s => (s==null?'':String(s)).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n    const toArr = v => Array.isArray(v) ? v : [];\n\n    function matchUf(idOrName){\n      if(!idOrName) return null;\n      const up = String(idOrName).trim().toUpperCase();\n      if (UF_CODE[up]) return up;\n      const n = norm(idOrName);\n      const hit = Object.keys(UF_NAMES).find(code => norm(UF_NAMES[code]) === n);\n      return hit || null;\n    }\n    function matchRegion(idOrName){\n      if(!idOrName) return null;\n      const up = String(idOrName).trim().toUpperCase();\n      if (REGION_UFS[up]) return up;\n      const n = norm(idOrName);\n      const hit = Object.keys(REGION_NAMES).find(code => norm(REGION_NAMES[code]) === n);\n      return hit || null;\n    }\n\n    async function geoUF(uf){\n      const code = UF_CODE[uf] || uf;\n      const r = await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');\n      if (!r.ok) throw new Error('geo ' + uf);\n      return r.json();\n    }\n    function ptsOf(f){\n      const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates], s=new Set();\n      ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));\n      return s;\n    }\n    function buildIndex(features){\n      const recs = features.map(f => ({\n        name: norm(f.properties.name || f.properties.NOME || f.properties.nome || ''),\n        label: f.properties.name || f.properties.NOME || f.properties.nome || '',\n        uf: f.__uf, f, pts: ptsOf(f)\n      }));\n      const ptIdx = {};\n      recs.forEach((r,i) => r.pts.forEach(p => (ptIdx[p]=ptIdx[p]||[]).push(i)));\n      const nbrsOf = i => {\n        const c={};\n        recs[i].pts.forEach(p => ptIdx[p].forEach(j => { if (j!==i) c[j]=(c[j]||0)+1; }));\n        return Object.entries(c).filter(([,n])=>n>=2).map(([j])=>+j);\n      };\n      const byUfName = {};\n      recs.forEach((r,i) => { byUfName[r.uf]=byUfName[r.uf]||{}; byUfName[r.uf][r.name]=i; });\n      const anyName = {};\n      recs.forEach((r,i) => { (anyName[r.name]=anyName[r.name]||[]).push(i); });\n      return { recs, nbrsOf, byUfName, anyName };\n    }\n    function project(feats){\n      let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n      const each = cb => feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n      each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n      const mlat=(mny+mxy)/2, kx=Math.cos(mlat*Math.PI/180), W=700,H=500,pad=22;\n      const gw=(mxx-mnx)*kx, gh=(mxy-mny), sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n      const ox=(W-gw*sc)/2, oy=(H-gh*sc)/2;\n      const pr = p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n      pr.scale = sc;\n      return pr;\n    }\n    function pathFor(f,pr){\n      const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');\n    }\n    function cxy(f,pr){\n      let x=0,y=0,n=0; const g=f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));\n      return [x/n, y/n];\n    }\n    function ringsOf(f){\n      const g = f.geometry, ps = g.type==='MultiPolygon' ? g.coordinates : [g.coordinates];\n      const rings = [];\n      ps.forEach(poly => poly.forEach(ring => rings.push(ring)));\n      return rings;\n    }\n    const quant = p => [Math.round(p[0]*1000)/1000, Math.round(p[1]*1000)/1000];\n    const keyOf = p => p[0] + ',' + p[1];\n    // Dissolves the municipio polygons in `idxs` into their outer boundary (+ any real holes)\n    // by cancelling every edge shared by two included polygons — the border between two\n    // adjacent included municipios disappears, leaving only the union's true edge. Exact for\n    // this dataset because neighboring municipio files share literal boundary coordinates (the\n    // same tolerance ptsOf already relies on for adjacency).\n    function dissolveRings(idxs, recs){\n      if (idxs.length === 1) return ringsOf(recs[idxs[0]].f);\n      const edgeCount = new Map(), edges = [];\n      idxs.forEach(i => {\n        ringsOf(recs[i].f).forEach(ring => {\n          const q = ring.map(quant);\n          for (let k = 0; k < q.length - 1; k++){\n            const a = q[k], b = q[k+1], ka = keyOf(a), kb = keyOf(b);\n            if (ka === kb) continue;\n            const uk = ka < kb ? ka+'|'+kb : kb+'|'+ka;\n            edgeCount.set(uk, (edgeCount.get(uk)||0) + 1);\n            edges.push({ a, b, ka, kb, uk });\n          }\n        });\n      });\n      const kept = edges.filter(e => edgeCount.get(e.uk) === 1);\n      const byStart = new Map();\n      kept.forEach(e => {\n        if (!byStart.has(e.ka)) byStart.set(e.ka, []);\n        byStart.get(e.ka).push(e);\n      });\n      const used = new Set(), rings = [];\n      kept.forEach(seed => {\n        if (used.has(seed)) return;\n        const ring = [seed.a];\n        let cur = seed, guard = 0;\n        while (guard++ < 50000) {\n          used.add(cur);\n          ring.push(cur.b);\n          if (cur.kb === seed.ka) break;\n          const next = (byStart.get(cur.kb) || []).find(e => !used.has(e));\n          if (!next) break;\n          cur = next;\n        }\n        if (ring.length >= 4 && keyOf(ring[0]) === keyOf(ring[ring.length-1])) rings.push(ring);\n      });\n      return rings;\n    }\n    function pathFromRings(rings, pr){\n      return rings.map(ring => 'M'+ring.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('');\n    }\n    function stripeClipRings(rings, pr, x0, x1){\n      let d = '';\n      rings.forEach(ring => {\n        let pts = ring.map(p => pr(p));\n        pts = clipHalfPlane(pts, (x) => x - x0);\n        if (pts.length) pts = clipHalfPlane(pts, (x) => x1 - x);\n        if (pts.length >= 3) d += 'M' + pts.map(q => q[0].toFixed(1) + ',' + q[1].toFixed(1)).join('L') + 'Z';\n      });\n      return d;\n    }\n    function mergeCentroid(idxs, pr, recs){\n      let x=0,y=0,n=0;\n      idxs.forEach(i=>{ const [cx,cy]=cxy(recs[i].f,pr); x+=cx; y+=cy; n++; });\n      return n ? [x/n, y/n] : [0,0];\n    }\n    function mergedBbox(idxs, pr, recs){\n      let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;\n      idxs.forEach(i => {\n        const g=recs[i].f.geometry, ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n        ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>{\n          const q=pr(p);\n          minX=Math.min(minX,q[0]); maxX=Math.max(maxX,q[0]);\n          minY=Math.min(minY,q[1]); maxY=Math.max(maxY,q[1]);\n        })));\n      });\n      return [minX,minY,maxX,maxY];\n    }\n    // Sutherland-Hodgman clip against a straight half-plane (side(x,y)>=0 kept). A straight-line\n    // cut is exact for ANY simple polygon, convex or concave, so stacking bands never leaves a gap\n    // — unlike an angular wedge radiating from one apex, which can't \"see\" a concave shape's whole border.\n    function clipHalfPlane(ring, side){\n      const out = [];\n      const n = ring.length;\n      for (let i = 0; i < n; i++){\n        const curr = ring[i], prev = ring[(i - 1 + n) % n];\n        const sCurr = side(curr[0], curr[1]);\n        const sPrev = side(prev[0], prev[1]);\n        const currIn = sCurr >= -1e-9;\n        const prevIn = sPrev >= -1e-9;\n        if (currIn) {\n          if (!prevIn) out.push(intersectHalfPlane(prev, curr, side));\n          out.push(curr);\n        } else if (prevIn) {\n          out.push(intersectHalfPlane(prev, curr, side));\n        }\n      }\n      return out;\n    }\n    function intersectHalfPlane(p1, p2, side){\n      const s1 = side(p1[0], p1[1]), s2 = side(p2[0], p2[1]);\n      const t = s1 / (s1 - s2);\n      return [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];\n    }\n\n    const loader = (on, t, isErr) => {\n      const l = $('#loader');\n      l.classList.toggle('hidden', !on);\n      l.classList.toggle('err', !!isErr);\n      if (t != null) l.textContent = t;\n    };\n\n    let IDX = null;\n    let S = { pr: null, view: { scale: 1, tx: 0, ty: 0 }, focus: [], ctx: [] };\n    let dragStart = null, dragMoved = false;\n    let pointers = new Map();\n    let pinchDist = null;\n\n    function svgPoint(evt){\n      const svg = $('#map');\n      const pt = svg.createSVGPoint();\n      pt.x = evt.clientX; pt.y = evt.clientY;\n      const ctm = svg.getScreenCTM();\n      if (!ctm) return { x: 0, y: 0 };\n      const p = pt.matrixTransform(ctm.inverse());\n      return { x: p.x, y: p.y };\n    }\n    function applyTransform(){\n      const g = $('#viewport');\n      if (g) g.setAttribute('transform', `translate(${S.view.tx} ${S.view.ty}) scale(${S.view.scale})`);\n    }\n    function zoomAt(p, factor){\n      const next = Math.min(8, Math.max(0.15, S.view.scale * factor));\n      const k = next / S.view.scale;\n      S.view.tx = p.x - k * (p.x - S.view.tx);\n      S.view.ty = p.y - k * (p.y - S.view.ty);\n      S.view.scale = next;\n      applyTransform();\n    }\n    d2.on('.stage', 'wheel', e => { e.preventDefault(); zoomAt(svgPoint(e), e.deltaY < 0 ? 1.15 : 1/1.15); });\n    d2.on('.stage', 'pointerdown', (e, el) => {\n      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n      if (pointers.size === 1) {\n        const p = svgPoint(e);\n        dragStart = { x: p.x, y: p.y, tx: S.view.tx, ty: S.view.ty, pointerId: e.pointerId, el, sx: e.clientX, sy: e.clientY };\n        dragMoved = false;\n      } else {\n        dragStart = null;\n        pinchDist = null;\n      }\n    });\n    d2.on(window, 'pointermove', e => {\n      if (!pointers.has(e.pointerId)) return;\n      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n      if (pointers.size === 2) {\n        const pts = [...pointers.values()];\n        const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n        if (pinchDist) {\n          const mid = { clientX: (pts[0].x + pts[1].x) / 2, clientY: (pts[0].y + pts[1].y) / 2 };\n          zoomAt(svgPoint(mid), dist / pinchDist);\n          dragMoved = true;\n        }\n        pinchDist = dist;\n        return;\n      }\n      if (!dragStart) return;\n      const p = svgPoint(e);\n      const dx = p.x - dragStart.x, dy = p.y - dragStart.y;\n      // Capture the pointer only once real drag movement is detected. Capturing eagerly on\n      // every pointerdown (the previous behavior) redirects the native `click` event's target\n      // to the capturing element (a browser click-synthesis quirk with setPointerCapture), so a\n      // plain tap on a color wedge never reached its own onclick. Deferring capture until the\n      // gesture crosses the drag threshold lets a no-movement click hit-test normally.\n      //\n      // The threshold is measured in SCREEN pixels (clientX/clientY), not viewBox units: a mouse\n      // cursor is sub-pixel precise, but a real finger tap naturally drifts several pixels, and\n      // the old 0.5-viewBox-unit threshold (~0.6 screen px, shrinking further when zoomed in)\n      // misread almost every touch tap as a drag — silently swallowing the color's onClick.\n      // Only widen the tolerance when the instance opts into touch_controls — that param is\n      // already the explicit signal this instance is being driven by touch, so gate on it\n      // instead of sniffing e.pointerType (unreliable on hybrid/stylus devices anyway).\n      if (!dragMoved) {\n        const sdx = e.clientX - dragStart.sx, sdy = e.clientY - dragStart.sy;\n        const touchMode = !!d2.param('touch_controls', false);\n        const threshold = touchMode ? 10 : 4;\n        if (Math.hypot(sdx, sdy) > threshold) {\n          dragMoved = true;\n          try { dragStart.el.setPointerCapture?.(dragStart.pointerId); } catch (_) {}\n        }\n      }\n      if (dragMoved) {\n        S.view.tx = dragStart.tx + dx; S.view.ty = dragStart.ty + dy;\n        applyTransform();\n      }\n    });\n    d2.on(window, 'pointerup', e => { pointers.delete(e.pointerId); pinchDist = null; if (!pointers.size) dragStart = null; });\n    d2.on(window, 'pointercancel', e => { pointers.delete(e.pointerId); pinchDist = null; dragStart = null; });\n\n    function showInfo(desc, evt){\n      const touch = !!d2.param('touch_controls', false);\n      const sub = desc.color ? esc(desc.color.color_label || desc.color.color_id) : 'apenas contexto';\n      if (touch) {\n        $('#infobarText').textContent = `${desc.label}${desc.color ? ' · ' + sub : ''}`;\n        $('#infobar').classList.add('show');\n      } else {\n        const tip = $('#tip');\n        if (evt) { tip.style.left = (evt.clientX + 12) + 'px'; tip.style.top = (evt.clientY + 12) + 'px'; }\n        tip.innerHTML = `<b>${esc(desc.label)}</b>${desc.color ? '<br>' + sub : ''}`;\n        tip.style.opacity = 1;\n      }\n    }\n    function hideInfo(){ $('#tip').style.opacity = 0; }\n    $('#infobarClose').onclick = () => $('#infobar').classList.remove('show');\n\n    function addCountry(list, focus, usedUfs){\n      list.forEach((entry, ei) => {\n        if (!entry) return;\n        if (String(entry.location_id||'').trim().toUpperCase() !== 'BR') {\n          d2.log('multiescala: country_list só suporta BR por enquanto (' + entry.location_id + ')');\n          return;\n        }\n        const ufs = Object.keys(UF_CODE);\n        ufs.forEach(u => usedUfs.add(u));\n        const idxs = ufs.flatMap(u => IDX.byUfName[u] ? Object.values(IDX.byUfName[u]) : []);\n        if (!idxs.length) return;\n        focus.push({ key: `country-${ei}`, idxs, level: 'country', entry, label: entry.location_label || 'Brasil', colors: toArr(entry.location_colors) });\n      });\n    }\n    function addRegion(list, focus, usedUfs, usedRegions){\n      list.forEach((entry, ei) => {\n        if (!entry) return;\n        const rc = matchRegion(entry.location_id);\n        if (!rc) { d2.log('multiescala: region_list id não reconhecido (' + entry.location_id + ')'); return; }\n        usedRegions.add(rc);\n        REGION_UFS[rc].forEach(u => usedUfs.add(u));\n        const idxs = REGION_UFS[rc].flatMap(u => IDX.byUfName[u] ? Object.values(IDX.byUfName[u]) : []);\n        if (!idxs.length) return;\n        focus.push({ key: `region-${ei}`, idxs, level: 'region', entry, label: entry.location_label || REGION_NAMES[rc], colors: toArr(entry.location_colors) });\n      });\n    }\n    function addState(list, focus, usedUfs){\n      list.forEach((entry, ei) => {\n        if (!entry) return;\n        const uf = matchUf(entry.location_id);\n        if (!uf) { d2.log('multiescala: state_list id não reconhecido (' + entry.location_id + ')'); return; }\n        usedUfs.add(uf);\n        const idxs = IDX.byUfName[uf] ? Object.values(IDX.byUfName[uf]) : [];\n        if (!idxs.length) return;\n        focus.push({ key: `state-${ei}`, idxs, level: 'state', entry, label: entry.location_label || UF_NAMES[uf] || uf, colors: toArr(entry.location_colors) });\n      });\n    }\n    function addLeaf(list, level, focus, usedUfs){\n      list.forEach((entry, ei) => {\n        if (!entry) return;\n        const n = norm(entry.location_id);\n        const matches = IDX.anyName[n];\n        if (!matches || !matches.length) { d2.log('multiescala: ' + level + ' não encontrado (' + entry.location_id + ')'); return; }\n        const i = matches[0];\n        usedUfs.add(IDX.recs[i].uf);\n        focus.push({ key: `${level}-${ei}`, idxs: [i], level, entry, label: entry.location_label || IDX.recs[i].label, colors: toArr(entry.location_colors) });\n      });\n    }\n\n    function buildScene(){\n      const countryList = toArr(d2.param('country_list', []));\n      const regionList = toArr(d2.param('region_list', []));\n      const stateList = toArr(d2.param('state_list', []));\n      const cityList = toArr(d2.param('city_list', []));\n      const muniList = toArr(d2.param('municipality_list', []));\n      const wantNeighbors = !!d2.param('should_render_neighbors', false);\n\n      const focus = [];\n      const usedUfs = new Set();\n      const usedRegions = new Set();\n      const lowest = (cityList.length || muniList.length) ? 'leaf'\n        : stateList.length ? 'state'\n        : regionList.length ? 'region'\n        : countryList.length ? 'country' : null;\n\n      addCountry(countryList, focus, usedUfs);\n      addRegion(regionList, focus, usedUfs, usedRegions);\n      addState(stateList, focus, usedUfs);\n      addLeaf(cityList, 'city', focus, usedUfs);\n      addLeaf(muniList, 'municipality', focus, usedUfs);\n\n      const ctx = [];\n      const focusIdxSet = new Set(focus.flatMap(s => s.idxs));\n      if (wantNeighbors && lowest) {\n        if (lowest === 'leaf') {\n          const nbrIdx = new Set();\n          focus.filter(s => s.level === 'municipality' || s.level === 'city')\n            .forEach(s => IDX.nbrsOf(s.idxs[0]).forEach(j => { if (!focusIdxSet.has(j)) nbrIdx.add(j); }));\n          if (nbrIdx.size) ctx.push({ idxs: [...nbrIdx] });\n        } else if (lowest === 'state') {\n          const nbrUfs = new Set([...usedUfs].flatMap(u => UF_BORDERS[u] || []).filter(u => !usedUfs.has(u)));\n          nbrUfs.forEach(u => { if (IDX.byUfName[u]) ctx.push({ idxs: Object.values(IDX.byUfName[u]) }); });\n        } else if (lowest === 'region') {\n          const nbrRegions = new Set();\n          [...usedUfs].forEach(u => (UF_BORDERS[u] || []).forEach(b => {\n            const br = Object.keys(REGION_UFS).find(r => REGION_UFS[r].includes(b));\n            if (br && !usedRegions.has(br)) nbrRegions.add(br);\n          }));\n          nbrRegions.forEach(r => {\n            const idxs = REGION_UFS[r].flatMap(u => IDX.byUfName[u] ? Object.values(IDX.byUfName[u]) : []);\n            if (idxs.length) ctx.push({ idxs });\n          });\n        }\n      }\n\n      S.focus = focus; S.ctx = ctx;\n\n      if (!focus.length && !ctx.length) {\n        $('#empty').classList.remove('hidden');\n        $('#legend').classList.add('hidden');\n        $('#map').innerHTML = '';\n        return;\n      }\n      $('#empty').classList.add('hidden');\n\n      const zoomIdxs = focus.length ? focus.flatMap(s => s.idxs) : ctx.flatMap(s => s.idxs);\n      S.pr = project(zoomIdxs.map(i => IDX.recs[i].f));\n      S.view = { scale: 1, tx: 100, ty: 0 };\n      drawMap();\n    }\n\n    function drawMap(){\n      const svg = $('#map'); svg.innerHTML = '';\n      const viewport = document.createElementNS(NS, 'g');\n      viewport.setAttribute('id', 'viewport');\n      svg.appendChild(viewport);\n      applyTransform();\n      const touch = !!d2.param('touch_controls', false);\n      const labelFontSize = Math.max(4, Math.min(14, 9 * ((S.pr && S.pr.scale || 1000) / 1000)));\n\n      S.ctx.forEach((c, i) => {\n        const p = document.createElementNS(NS, 'path');\n        p.setAttribute('d', pathFromRings(dissolveRings(c.idxs, IDX.recs), S.pr));\n        p.setAttribute('data-key', 'ctx-' + i);\n        p.setAttribute('class', 'area');\n        p.setAttribute('fill', '#dcdfe8');\n        p.setAttribute('opacity', '.55');\n        viewport.appendChild(p);\n      });\n\n      const legendGroups = [];\n      S.focus.forEach(item => {\n        const rings = dissolveRings(item.idxs, IDX.recs);\n        const dFull = pathFromRings(rings, S.pr);\n        const base = document.createElementNS(NS, 'path');\n        base.setAttribute('d', dFull);\n        base.setAttribute('data-key', item.key + '-base');\n        base.setAttribute('class', 'area');\n        base.setAttribute('fill', item.colors.length ? '#eef0f5' : '#94a3b8');\n        base.onmousemove = e => showInfo({ label: item.label, color: null }, e);\n        base.onmouseleave = hideInfo;\n        viewport.appendChild(base);\n\n        if (item.colors.length) {\n          const [bx0, , bx1] = mergedBbox(item.idxs, S.pr, IDX.recs);\n          const n = item.colors.length;\n          const stripeW = (bx1 - bx0) / n;\n          item.colors.forEach((c, ci) => {\n            const d = stripeClipRings(rings, S.pr, bx0 + ci * stripeW, bx0 + (ci + 1) * stripeW);\n            if (!d) return;\n            const wedge = document.createElementNS(NS, 'path');\n            wedge.setAttribute('d', d);\n            wedge.setAttribute('data-key', item.key + '-w' + ci);\n            wedge.setAttribute('class', 'wedge' + (touch ? ' touchy' : ''));\n            wedge.setAttribute('fill', c.color || PALETTE[ci % PALETTE.length]);\n            wedge.onclick = (e) => {\n              if (dragMoved) { dragMoved = false; return; }\n              if (touch) showInfo({ label: item.label, color: c });\n              // The ONLY click entry point — a color with no `onClick` just isn't actionable.\n              // Augment the REAL click event (not a bare payload object) so APIs that need a\n              // genuine DOM event as their first arg (e.g. @__DialogsService, which walks\n              // $event.parentElement to find the modal mount point) still work, while\n              // `$event.color_id` etc. remain readable.\n              if (typeof c.onClick === 'function') {\n                c.onClick(Object.assign(e, {\n                  level: item.level,\n                  location_id: item.entry.location_id,\n                  location_label: item.entry.location_label,\n                  color_id: c.color_id,\n                  color_label: c.color_label,\n                  color: c.color,\n                }));\n              }\n            };\n            if (!touch) {\n              wedge.onmousemove = e => showInfo({ label: item.label, color: c }, e);\n              wedge.onmouseleave = hideInfo;\n            }\n            viewport.appendChild(wedge);\n          });\n        }\n\n        const [cx, cy] = item.idxs.length > 1 ? mergeCentroid(item.idxs, S.pr, IDX.recs) : cxy(IDX.recs[item.idxs[0]].f, S.pr);\n        const t = document.createElementNS(NS, 'text');\n        t.setAttribute('class', 'lbl'); t.setAttribute('x', cx); t.setAttribute('y', cy);\n        t.style.fontSize = labelFontSize + 'px';\n        t.textContent = (item.label || '').slice(0, 16);\n        viewport.appendChild(t);\n\n        if (item.colors.length) legendGroups.push({ entry: item.entry, items: item.colors });\n      });\n\n      renderLegend(legendGroups);\n    }\n    function renderLegend(groups){\n      const el = $('#legend');\n      if (!groups.length) { el.classList.add('hidden'); return; }\n      el.classList.remove('hidden');\n      el.innerHTML = groups.map(g => {\n        const rows = g.items.map(c => `<div class=\"row\"><b style=\"background:${esc(c.color)}\"></b>${esc(c.color_label || c.color_id)}</div>`).join('');\n        return `<div class=\"grp\"><div class=\"grpname\">${esc(g.entry.location_label || g.entry.location_id || '')}</div>${rows}</div>`;\n      }).join('');\n    }\n\n    async function load(){\n      loader(true, 'carregando malhas do Brasil (27 UFs)…', false);\n      try {\n        const ufs = Object.keys(UF_CODE);\n        const geos = await Promise.all(ufs.map(u => geoUF(u).catch(() => null)));\n        const allFeat = [];\n        ufs.forEach((u, i) => { const g = geos[i]; if (!g) return; g.features.forEach(f => { f.__uf = u; allFeat.push(f); }); });\n        if (!allFeat.length) throw new Error('nenhuma malha carregada');\n        IDX = buildIndex(allFeat);\n        buildScene();\n        loader(false);\n      } catch (e) {\n        loader(true, 'erro: ' + e.message + ' (clique p/ tentar de novo)', true);\n      }\n    }\n    $('#loader').onclick = () => { if (!$('#loader').classList.contains('err')) return; IDX = null; load(); };\n\n    load();\n  },\n};"}},"country_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"country_list"}}}},"region_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"region_list"}}}},"state_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"state_list"}}}},"city_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"city_list"}}}},"municipality_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipality_list"}}}},"should_render_neighbors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"should_render_neighbors"}}}},"touch_controls":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"touch_controls"}}}}}}}},"extends":"data2-core-primitives@Block"}},{"_id":"6a7381e544b23b3f85949815","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"syKO63JC71F1tqr5T_HJx","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default {\n  css: `\n  :host{display:block;height:100%;--bg:#f4f4f8;--panel:#ffffff;--panel2:#f7f7fb;--line:#e7e7ef;--ink:#1c1c2b;--mut:#71717f;--acc:#c8104e; --closing:#c8104e;--ok:#1f9d57;--grad:linear-gradient(100deg,#c8104e,#8f0b39)}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13.5px/1.45 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink)}\n  header{padding:14px 20px;background:var(--grad);color:#fff;display:flex;align-items:center;gap:12px;flex-wrap:wrap;box-shadow:0 2px 14px rgba(140,11,57,.25)}\n  header h1{font-size:16px;margin:0;font-weight:700;letter-spacing:.2px}\n  header select,header input{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.3);color:#fff}\n  header select option{color:#1c1c2b}\n  header .status{color:rgba(255,255,255,.85)}\n  .howto{padding:9px 20px;background:#fff;border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:11px;padding:3px 9px;border-radius:99px;border:1px solid rgba(255,255,255,.35);color:rgba(255,255,255,.92);background:rgba(255,255,255,.12)}\n  .pill.live{color:#fff}.pill.ro{color:#fff}\n  header button.act{background:#fff;color:var(--acc)}\n  header button.ghost{background:rgba(255,255,255,.14);color:#fff;border-color:rgba(255,255,255,.3)}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:6px 8px}\n  button.act{background:var(--acc);color:#fff;border:0;border-radius:8px;padding:7px 12px;font-weight:700;cursor:pointer}\n  button.ghost{background:transparent;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:6px 11px;cursor:pointer}\n  button.ghost.on{border-color:var(--acc);color:var(--acc)}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 18px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(20,20,40,.05),0 6px 20px rgba(20,20,40,.04)}\n  .card h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);margin:0 0 9px;font-weight:700}\n  svg{width:100%;height:auto;display:block}\n  .muni{stroke:#0b0d12;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.28)}\n  .muni.sel{stroke:#fff;stroke-width:2.3}\n  .nbr{fill:#e6e6ee;stroke:#cfcfda;stroke-width:.8;opacity:.95}\n  .mlabel{fill:#0d0f14;font-size:10px;font-weight:700;pointer-events:none;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.55);stroke-width:2.4px}\n  .nlabel{fill:#6b7183;font-size:8.5px;pointer-events:none;text-anchor:middle}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:20px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#f0c2cb}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bfe6cd}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:700;font-size:12px}\n  .bar-track{flex:1;height:19px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:700;cursor:pointer}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  .toggle{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:12px}\n  .toggle button{background:transparent;border:0;color:var(--mut);padding:6px 10px;cursor:pointer;font-weight:600}\n  .toggle button.on{background:var(--panel2);color:var(--ink)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .scroll{max-height:240px;overflow:auto}\n  .tip{position:fixed;pointer-events:none;background:#000;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px}\n  .tabs button{background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600}\n  .tabs button.on{background:var(--panel2);color:var(--ink);border-color:var(--acc)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(248,248,251,.85);display:flex;align-items:center;justify-content:center;z-index:20;font-size:14px;color:var(--acc);font-weight:600}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header>\n  <h1>Simulador de Realocação de Setores</h1>\n  <select id=\"sectorPick\" style=\"min-width:230px\"></select>\n  <button class=\"ghost\" id=\"rand\">🎲 aleatório</button>\n  <button class=\"act\" id=\"go\">Fechar setor ▶</button>\n  <span class=\"pill live\" id=\"liveState\">● dado ao vivo</span>\n  <span class=\"pill ro\">read-only · nunca escreve</span>\n  <span class=\"status\" id=\"ds\"></span>\n</header>\n\n<div class=\"howto\">\n  <b>Como usar &nbsp;→&nbsp;</b>\n  <span><b>1.</b> Escolha um setor pra <b>fechar</b> (ou 🎲).</span>\n  <span><b>2.</b> O simulador puxa o dado real, acha os <b>setores vizinhos</b> (geografia, cruzando estados) e sugere o destino de cada município.</span>\n  <span><b>3.</b> Clique <b style=\"color:var(--acc)\">★ Modelo ÓTIMO</b> p/ minimizar quebra de relação, ou ajuste no <b>mapa</b>/botões.</span>\n  <span><b>4.</b> Veja o impacto <b>antes → depois</b> no relatório.</span>\n</div>\n<div class=\"wrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <h2 id=\"mapTitle\" style=\"margin:0\">Mapa — clique num município p/ trocar o setor destino</h2>\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura atual (setor inteiro) · TOBE = após a redistribuição\">\n          <button data-v=\"asis\">◧ ASIS (hoje)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado (cor = destino). Cinza = vizinhos (contexto). Shapes reais IBGE/tbrugz.</div>\n    </div>\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"report\">Relatório de impacto (6 ciclos · antes/depois)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1) GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa todas alocações contíguas e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (contíguo; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Indicadores atualizam ao vivo.</div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Municípios → setor destino</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor (viram “fora do setor” ou “quebra de relação”).</div>\n      <div class=\"scroll\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div></div>`;\n  },\n  mounted(d2, root){\n\nconst T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst SNAP=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\nconst PALETTE=['#8b7fd6','#d1b048','#67bd5b','#5aa9d6','#d67fa8','#c98a4b'];\nconst CLOSE_COLOR='#475569';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\n\nlet S={sector:null, matrix:[], munis:{}, leaders:[], color:{}, dest:[], assign:{}, allowed:{},\n       geoFeat:{}, nbrFeat:[], baseMetrics:{}, closingMetrics:{}, trend:[], levels:[], policy:'fora', selected:null, sectorsList:[], view:'tobe'};\nconst closingCbs=()=>Object.entries(S.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0);\n\nasync function q(sql){ return await d2.rush('A0kvMekKfk4Yk0da7NLET').invoke({sql, skip_monitoring:true, country:'BR'}); }\nfunction rows(resp){const c=(resp.manifest?.schema?.columns||[]).map(x=>x.name);\n  return (resp.result?.data_array||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);return r.json();}\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\n\nfunction buildAdj(features){\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const feat={},idx={};\n  features.forEach(f=>{const nm=norm(f.properties.name||f.properties.NOME||f.properties.nome);feat[nm]={f,pts:pts(f)};});\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  const nbrs=nm=>{const c={};feat[nm].pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    return Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));};\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\nasync function closeSector(sec){\n  loader(true,'fechando setor '+sec+' — puxando dado real…');\n  $('#liveState').textContent='● consultando';$('#ds').classList.remove('err');\n  try{\n    S.sector=sec;\n    const mSQL=`SELECT DS_MUNICIPIO municipio, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n       COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n       ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n       ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP}\n      GROUP BY DS_MUNICIPIO, CD_GRUPO`;\n    const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n    const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n       COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n       ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n      FROM ${ELO} WHERE country_code=1 AND sector_code=${sec} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n    const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n    const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, DS_NVL_PLAN_CRESC`;\n    const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n    S.matrix=rows(mR).filter(r=>+r.cbs>0);\n    S.levelByMuni={}; rows(lmR).forEach(r=>{const k=norm(r.municipio);(S.levelByMuni[k]=S.levelByMuni[k]||{})[r.nivel]=+r.cbs;});\n    S.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n    S.levels=rows(lR);\n    const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n    const domUF=ufList[0]||'PR';\n\n    const ufs=[domUF,...(UF_BORDERS[domUF]||[])];\n    loader(true,'malhas '+ufs.join('/')+' + adjacência…');\n    const geos=await Promise.all(ufs.map(u=>geoUF(u).catch(()=>null)));\n    const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n    const adj=buildAdj(allFeat);\n    const totBy={}; S.matrix.forEach(r=>{const n=norm(r.municipio);totBy[n]=(totBy[n]||0)+ +r.cbs;});\n    const totalSec=Object.values(totBy).reduce((a,b)=>a+b,0);\n    const floor=Math.max(10, totalSec*0.008);\n    const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>=floor).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([m])=>m));\n    const closingNames=[...footprint].filter(n=>adj.feat[n]);\n    const closingSet=new Set(closingNames);\n    S.geoFeat={};closingNames.forEach(n=>S.geoFeat[n]=adj.feat[n].f);\n    const extVotes={}; const nbrNames=new Set();\n    closingNames.forEach(n=>{adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });});\n    S.nbrFeat=[...nbrNames].map(n=>adj.feat[n].f);\n    let neigh=[];\n    if(nbrNames.size){\n      const inList=[...nbrNames].map(n=>`'${n.replace(/'/g,\"''\")}'`).join(',');\n      const nSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs\n        FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR<>${sec} AND CD_SETOR>0 AND DT_HISTORICO=${SNAP} AND DS_MUNICIPIO IN (${inList}) GROUP BY DS_MUNICIPIO, CD_SETOR`;\n      neigh=rows(await q(nSQL));\n    }\n    const domSecOf={};\n    const byM={}; neigh.forEach(r=>{const m=norm(r.municipio);(byM[m]=byM[m]||[]).push(r);});\n    Object.entries(byM).forEach(([m,rs])=>{rs.sort((a,b)=>+b.cbs-+a.cbs);domSecOf[m]=String(rs[0].setor);});\n    S.allowed={}; const secWeight={};\n    closingNames.forEach(n=>{const v={};(extVotes[n]||[]).forEach(({o,w})=>{const s=domSecOf[o];if(s){v[s]=(v[s]||0)+w;secWeight[s]=(secWeight[s]||0)+w;}});S.allowed[n]=v;});\n    S.dest=Object.keys(secWeight).sort((a,b)=>secWeight[b]-secWeight[a]).slice(0,6);\n    if(!S.dest.length){\n      S.dest=[...new Set(Object.values(domSecOf))].slice(0,6);\n      closingNames.forEach(n=>{if(!Object.keys(S.allowed[n]).length){S.dest.forEach(s=>S.allowed[n][s]=1);}});\n    }\n    S.color={};S.dest.forEach((s,i)=>S.color[s]=PALETTE[i%PALETTE.length]);\n    const acc={};Object.entries(domSecOf).forEach(([nb,s])=>{if(!S.dest.includes(s))return;const f=adj.feat[nb]?.f;if(!f)return;const c=centroidLL(f);acc[s]=acc[s]||[0,0,0];acc[s][0]+=c[0];acc[s][1]+=c[1];acc[s][2]++;});\n    S.secCentroid={};S.dest.forEach(s=>{if(acc[s])S.secCentroid[s]=[acc[s][0]/acc[s][2],acc[s][1]/acc[s][2]];});\n    S.muniCentroid={};closingNames.forEach(n=>{S.muniCentroid[n]=centroidLL(adj.feat[n].f);});\n    S.closeAdj={};closingNames.forEach(n=>{S.closeAdj[n]=adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o);});\n    const comp={};let ci=0;closingNames.forEach(n=>{if(comp[n]!=null)return;const st=[n];comp[n]=ci;while(st.length){const x=st.pop();(S.closeAdj[x]||[]).forEach(o=>{if(comp[o]==null){comp[o]=ci;st.push(o);}});}ci++;});\n    const compAllowed={};closingNames.forEach(n=>{const cid=comp[n];(compAllowed[cid]=compAllowed[cid]||new Set());Object.keys(S.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s));});\n    S.reachable={};closingNames.forEach(n=>{S.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>S.dest.includes(s));});\n\n    loader(true,'métricas base dos setores receptores…');\n    const rList=S.dest.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    S.baseMetrics={};rows(bR).forEach(r=>S.baseMetrics[String(r.setor)]=r);\n    S.baseLevel={};rows(blR).forEach(r=>{const s=String(r.setor);(S.baseLevel[s]=S.baseLevel[s]||{})[r.nivel]=+r.cbs;});\n\n    aggregate();\n    S.assign=optimize();\n    S.selected=null;\n    const dt=new Date().toLocaleTimeString('pt-BR');\n    $('#ds').textContent=`setor ${sec} · UF ${domUF} · ${closingNames.length} munic. · atualizado ${dt}`;\n    $('#liveState').textContent='● dado ao vivo';\n    $('#mapTitle').textContent=`Fechando setor ${sec} — clique num município p/ trocar destino`;\n    renderAll();\n  }catch(e){ $('#ds').textContent='erro: '+e.message; $('#ds').classList.add('err'); $('#liveState').textContent='● erro'; }\n  finally{ loader(false); }\n}\n\nfunction aggregate(){\n  const munis={}, leaders={};\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  const blank=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n  S.matrix.forEach(r=>{\n    const m=norm(r.municipio); if(!S.geoFeat[m]) {r._out=true;}\n    const mm=r._out?'__OUTROS__':m;\n    munis[mm]=munis[mm]||{...blank(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n  });\n  S.munis=munis; S.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssign(){\n  const a={};\n  Object.keys(S.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=S.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else { const c=S.muniCentroid[m]; let best=S.dest[0],bd=Infinity;\n      for(const s of S.dest){ const sc=S.secCentroid[s]; if(!sc)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(assign){\n  let d=0;\n  S.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=S.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,S.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanSpread(assign){\n  const inc={};S.dest.forEach(s=>inc[s]=0);\n  Object.entries(S.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;if(assign[m])inc[assign[m]]+=o.cbs;});\n  const sp=S.dest.map(s=>(+ (S.baseMetrics[s]?.cbs||0))+inc[s]);\n  return Math.max(...sp)-Math.min(...sp);\n}\nfunction contigBad(assign){\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(S.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(S.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\nfunction maxSpanOf(a){const inc={};S.dest.forEach(s=>inc[s]=0);Object.entries(S.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;if(a[m])inc[a[m]]+=o.cbs;});return Math.max(...S.dest.map(s=>(+ (S.baseMetrics[s]?.cbs||0))+inc[s]));}\nconst OPT_K=1.5;\nfunction optScore(a){return maxSpanOf(a)+OPT_K*deslocFor(a);}\nfunction optimize(){\n  const ms=Object.keys(S.munis).filter(m=>m!=='__OUTROS__');\n  const opts=ms.map(m=>{const al=(S.reachable[m]&&S.reachable[m].length)?S.reachable[m]:S.dest.slice();return al;});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  if(combos>300000){ $('#optBadge').textContent='espaço grande ('+fmt(combos)+') — heurística'; return greedy(ms,opts); }\n  let best=null,bestSc=Infinity,bestD=Infinity,valid=0;\n  const rec=(i,cur)=>{ if(i===ms.length){ if(contigBad(cur).length) return; valid++; const sc=optScore(cur),d=deslocFor(cur); if(sc<bestSc||(sc===bestSc&&d<bestD)){bestSc=sc;bestD=d;best={...cur};}return;}\n    for(const s of opts[i]){cur[ms[i]]=s;rec(i+1,cur);} };\n  rec(0,{});\n  if(!best){ $('#optBadge').textContent='sem solução contígua — heurística'; return greedy(ms,opts); }\n  $('#optBadge').textContent=`ótimo · pico ${fmt(maxSpanOf(best))} CBs · quebra ${fmt(bestD)} · ${fmt(valid)}/${fmt(combos)} contíguas`;\n  return best;\n}\nfunction greedy(ms,opts){const a={};ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;for(const s of opts[i]){a[m]=s;const d=optScore(a)+(contigBad(a).length?1e9:0);if(d<bd){bd=d;best=s;}}a[m]=best;});return a;}\n\nfunction compute(){\n  const inc={},gained={};S.dest.forEach(s=>{inc[s]=0;gained[s]=0;});\n  Object.entries(S.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=S.assign[m];if(s)inc[s]+=o.cbs;});\n  let desloc=0;\n  const L2=S.leaders.map(L=>{const byS={};S.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=S.assign[m];if(s){byS[s]+=c;tot+=c;}}\n    const home=S.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,S.dest[0]);const dl=tot-byS[home];desloc+=dl;gained[home]++;\n    return {...L,byS,home,desloc:dl,assignedTotal:tot};});\n  const span={};S.dest.forEach(s=>span[s]=(+ (S.baseMetrics[s]?.cbs||0))+inc[s]);\n  return {inc,gained,desloc,leaders:L2,span};\n}\nfunction renderAll(){drawMap();renderKpis();renderContig();renderSpans();renderMuni();renderLeaders();renderMatrix();renderPolicy();renderReport();renderPlan();}\nfunction renderContig(){const el=$('#contig');if(!el)return;\n  if(S.view==='asis'){el.innerHTML='';return;}\n  const bad=contigBad(S.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Contiguidade quebrada em ${bad.join(', ')} — município(s) desconectado(s) do território do setor. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Contíguo — cada setor forma uma região conectada.</div>`;\n}\nfunction renderKpis(){const c=compute();\n  if(S.view==='asis'){const mc=Object.keys(S.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(closingCbs()),l:`CBs no setor ${S.sector}`,t:'Total de CBs cadastradas no setor hoje (estrutura atual).'},\n      {n:fmt(mc),l:'Municípios do setor',t:'Municípios que compõem o território do setor hoje.'},\n      {n:fmt(S.leaders.length),l:'Líderes (grupos)',t:'Líderes ativas dentro do setor hoje.'},\n      {n:fmt(S.dest.length),l:'Setores adjacentes',t:'Setores vizinhos que poderiam receber os municípios se o setor fechar.'},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=S.dest.map(s=>c.span[s]);const spread=Math.max(...sv)-Math.min(...sv);\n  const zero=S.dest.filter(s=>c.gained[s]===0);const totCbs=Object.entries(S.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0);\n  $('#kpis').innerHTML=[\n    {n:fmt(c.desloc),l:S.policy==='fora'?'CBs fora do setor':'CBs c/ quebra de relação',cls:'warn',t:'CBs cuja líder acabou noutro setor. Menor = menos ruptura. É o custo humano da migração.'},\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:'Diferença de tamanho entre o maior e o menor setor depois. Menor = carga mais equilibrada entre as GNs.'},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs do setor ${S.sector} sendo redistribuídas para os setores vizinhos.`},\n    {n:zero.length?zero.join(', '):'nenhum',l:'Setor sem líder herdada',cls:zero.length?'warn':'ok',t:'Setor que recebe município(s) mas nenhuma líder — as CBs de lá terão que se ligar a uma líder pré-existente do setor.'},\n  ].map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const c=compute();\n  if(S.view==='asis'){const items=[{s:S.sector,val:closingCbs(),color:CLOSE_COLOR}].concat(S.dest.map(s=>({s,val:+(S.baseMetrics[s]?.cbs||0),color:S.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura atual — o setor <b>${S.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const max=Math.max(...S.dest.map(s=>c.span[s]),1);\n  $('#spans').innerHTML=S.dest.map(s=>{const base=+(S.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${S.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${S.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${S.color[s]}\"></div></div>\n      <div class=\"bar-val\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span></div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base atual · cheio = CBs entrantes do ${S.sector}.</div>`;\n}\nfunction renderMuni(){const c=compute();const ms=Object.keys(S.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>S.munis[b].cbs-S.munis[a].cbs);\n  if(S.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(S.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">setor ${S.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje todos pertencem ao setor <b>${S.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`;return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(S.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${S.dest.map(s=>`<button data-s=\"${s}\" class=\"${S.assign[m]===s?'on':''}\" style=\"${S.assign[m]===s?'background:'+S.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +(S.munis.__OUTROS__?`<div class=\"note\">+ ${fmt(S.munis.__OUTROS__.cbs)} CBs em municípios sem shape na UF (tratar à parte).</div>`:'');\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{S.assign[b.parentElement.dataset.m]=b.dataset.s;renderAll();});\n}\nfunction renderLeaders(){const c=compute();\n  if(S.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>Setor</th></tr></thead><tbody>`\n    +S.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${S.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(S.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${S.dest.map(s=>`<th>${s}</th>`).join('')}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td><span class=\"home\" style=\"background:${S.color[L.home]}\"></span>${L.home}</td>\n     ${S.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+S.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${S.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const c=compute();const ms=Object.keys(S.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>S.munis[b].cbs-S.munis[a].cbs);\n  const cell=(m,g)=>{const r=S.matrix.find(x=>(x._out?'__OUTROS__':norm(x.municipio))===m&&String(x.grupo)===g);return r?+r.cbs:0;};\n  const mx=Math.max(...ms.flatMap(m=>S.leaders.map(L=>cell(m,L.grupo))),1);\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th><th>→</th>${S.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td><td><span class=\"home\" style=\"background:${S.color[S.assign[m]]}\"></span>${S.assign[m]||'–'}</td>`\n     +S.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(S.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\nfunction renderPolicy(){const c=compute();\n  $('#policyNote').innerHTML=S.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutro setor</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder do setor destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const c=compute();const ms=Object.keys(S.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  S._plan=ms.map(m=>({municipio:m,cbs:S.munis[m].cbs,destino:S.assign[m]}));\n  $('#plan').innerHTML=`Setor <b>${S.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${S.color[S.assign[m]]}\">${S.assign[m]}</b>`).join(' · ')}<br>\n   Política: <b>${S.policy==='fora'?'manter vínculo (CBs fora do setor)':'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas.`;\n}\nfunction renderReport(){const c=compute();\n  S.metric=S.metric||'cbs';\n  const metrics=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\n  const KEYS=metrics.map(m=>m[0]);\n  const incBy={};S.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(S.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=S.assign[m];if(!s)return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(S.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  const trend=S.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=S.dest.map(s=>({label:s,color:S.color[s],antes:+(S.baseMetrics[s]?.[mk]||0),depois:+(S.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:S.sector+'✕',color:'#c8104e',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#1f9d57':'#c8104e'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois por setor</h2>\n      <select id=\"metricSel\">${metrics.map(m=>`<option value=\"${m[0]}\" ${m[0]===S.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes</b> · barra cheia = <b>depois</b> (base + entrantes do ${S.sector}) · <b style=\"color:var(--acc)\">${S.sector}✕</b> = setor que fecha · % = variação</div>\n    ${chartHTML(S.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por setor receptor (snapshot atual)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${metrics.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  S.dest.forEach(s=>{const b=S.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${S.color[s]}\"></span>${s}</td>`+\n      metrics.map(([k])=>{const bv=+b[k]||0,av=bv+ (inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=metrics.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${S.sector} (sai)</td>`+metrics.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};S.dest.forEach(s=>incLvl[s]={});\n  Object.keys(S.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=S.assign[m];if(!s)return;const lv=S.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por setor receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  S.dest.forEach(s=>{const base=S.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${S.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> da migração · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${S.sector}. Base + entrantes por nível.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${S.sector} — últimos ${trend.length} ciclos (tendência)</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Como o setor que fecha vinha performando — pra dimensionar o que está sendo redistribuído.</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{S.metric=sel.value; renderReport();};\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=700,H=500,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\nfunction drawMap(){const c=compute();const NS='http://www.w3.org/2000/svg';const svg=$('#map');svg.innerHTML='';\n  const feats=[...S.nbrFeat,...Object.values(S.geoFeat)];if(!feats.length)return;const pr=project(feats);\n  S.nbrFeat.forEach(f=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));p.setAttribute('class','nbr');svg.appendChild(p);\n    const ct=cxy(f,pr);const t=document.createElementNS(NS,'text');t.setAttribute('class','nlabel');t.setAttribute('x',ct[0]);t.setAttribute('y',ct[1]);t.textContent=(f.properties.name||'').slice(0,14);svg.appendChild(t);});\n  const asis=S.view==='asis';\n  Object.entries(S.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n    p.setAttribute('class','muni'+(S.selected===m?' sel':''));p.setAttribute('fill',asis?CLOSE_COLOR:(S.color[S.assign[m]]||'#444'));\n    if(!asis) p.onclick=()=>{const i=S.dest.indexOf(S.assign[m]);S.assign[m]=S.dest[(i+1)%S.dest.length];S.selected=m;renderAll();};\n    p.style.cursor=asis?'default':'pointer';\n    p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n      t.innerHTML=asis?`<b>${m}</b><br>${fmt(S.munis[m]?.cbs||0)} CBs · setor ${S.sector}`:`<b>${m}</b><br>${fmt(S.munis[m]?.cbs||0)} CBs → ${S.assign[m]}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n    p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n    const ct=cxy(f,pr);const t=document.createElementNS(NS,'text');t.setAttribute('class','mlabel');t.setAttribute('x',ct[0]);t.setAttribute('y',ct[1]);\n    t.innerHTML=`${m.slice(0,13)}<tspan x=\"${ct[0]}\" dy=\"11\" font-size=\"8.5\">${fmt(S.munis[m]?.cbs||0)}${asis?'':'·'+S.assign[m]}</tspan>`;svg.appendChild(t);});\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>Setor ${S.sector} (estrutura atual — ${Object.keys(S.geoFeat).length} municípios)</span>`\n    :S.dest.map(s=>`<span><b style=\"background:${S.color[s]}\"></b>Setor ${s} — ${c.gained[s]} líder(es)</span>`).join('');\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  S.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n  const sel=$('#sectorPick');sel.innerHTML=S.sectorsList.map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n  sel.value='3504';\n}\n$('#go').onclick=()=>closeSector($('#sectorPick').value);\n$('#rand').onclick=()=>{const cand=S.sectorsList.filter(s=>s.munis>=3&&s.munis<=10&&s.cbs>300);const pick=(cand.length?cand:S.sectorsList)[Math.floor(Math.random()*(cand.length?cand.length:S.sectorsList.length))];$('#sectorPick').value=pick.setor;closeSector(pick.setor);};\n$('#optim').onclick=()=>{S.assign=optimize();renderAll();};\n$('#autoTobe').onclick=()=>{S.assign=autoAssign();$('#optBadge').textContent='TOBE por adjacência';renderAll();};\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{S.view=b.dataset.v;$('#viewToggle').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));renderAll();});\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{S.policy=b.dataset.p;$('#policy').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','report','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{navigator.clipboard.writeText(JSON.stringify({setor:S.sector,politica:S.policy,plano:S._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const csv='municipio,cbs,setor_destino\\n'+S._plan.map(p=>`${p.municipio},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${S.sector}.csv`;a.click();};\n\n(async()=>{loader(true,'carregando setores…');try{await loadSectors();await closeSector('3504');}catch(e){$('#ds').textContent='erro init: '+e.message;}loader(false);})();\n\n  }\n}"}}}}}},"extends":"data2-core-primitives@Block"}},{"_id":"6a74ae9d8bf67595bba6de82","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","component":"timeline-sim-login","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const AUTH='timelineSimAuthFinal';\n// SSP origin is derived at runtime from a known listener's resolved URL; this is only the fallback.\nconst SSP_FALLBACK='https://natura-geomarketing.data2apis.com';\nconst RUSH_REF='AwW_11Nkg-dKksP7ic8V8';\nconst ROUTER='timelineSimRouter';\n\nexport default {\n  css: `\n  :host{display:block;height:100%}\n  *{box-sizing:border-box}\n  .lroot{--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#34c759;\n    min-height:100%;display:flex;align-items:center;justify-content:center;padding:36px 20px;background:var(--bg);\n    font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}\n  .card{width:100%;max-width:400px;background:var(--panel);border:1px solid var(--line);border-radius:14px;\n    box-shadow:0 12px 34px rgba(0,0,0,.07),0 1px 2px rgba(0,0,0,.04);padding:26px 26px 22px}\n  .pill{display:inline-block;font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  h1{font-size:17px;font-weight:600;letter-spacing:-.25px;margin:13px 0 5px}\n  .sub{font-size:12px;color:var(--mut);margin:0 0 20px;line-height:1.5}\n  .fl{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin:0 0 5px}\n  .inp{width:100%;background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font:inherit;font-size:13px}\n  .inp:focus{outline:2px solid rgba(0,122,255,.32);outline-offset:0;border-color:var(--acc)}\n  .inp:disabled{opacity:.55}\n  .row{margin-bottom:14px}\n  .act{width:100%;background:var(--acc);color:#fff;border:1px solid transparent;border-radius:8px;padding:10px 14px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 1px rgba(0,0,0,.08);margin-top:4px}\n  .act:hover{background:var(--acc2)}\n  .act:disabled{opacity:.5;cursor:not-allowed}\n  .ghost{width:100%;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 12px;font:inherit;font-size:12.5px;cursor:pointer;margin-top:8px}\n  .ghost:hover{background:var(--panel2)}\n  .okbox{margin:12px 0 0;padding:8px 10px;border-radius:8px;background:#eefaf0;border:1px solid #bdeecb;color:#1c7c33;font-size:11.5px;font-weight:600}\n  .err{margin:12px 0 0;padding:8px 10px;border-radius:8px;background:#fff1f0;border:1px solid #ffd1cc;color:var(--closing);font-size:11.5px;font-weight:600}\n  .who{font-size:12px;color:var(--mut);margin:0 0 16px}\n  .who b{color:var(--ink)}\n  .foot{margin-top:18px;padding-top:14px;border-top:1px solid var(--line);font-size:10.5px;color:var(--mut);line-height:1.55}\n`,\n  render(d2){\n    const esc=s=>(s==null?'':String(s)).replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n    const busy=!!d2.state.busy;\n    const err=d2.state.error||'';\n    const a=d2.auth(AUTH);\n    if(!a){\n      return `<div class=\"lroot\"><div class=\"card\">\n        <span class=\"pill\">configuração</span>\n        <h1>Estratégia de autenticação não encontrada</h1>\n        <p class=\"sub\">Não foi possível resolver <b>${esc(AUTH)}</b>. Confirme a referência da AuthenticationStrategy do sistema.</p>\n      </div></div>`;\n    }\n    // Deliberately NOT branching on a.isAuthed. The Router renders this artboard ONLY from its\n    // `login` slot, i.e. only when the route policy rejected the caller — so anyone looking at it\n    // is unauthenticated by construction. isAuthed also reads TRUE under a builder token, which\n    // would show an \"already signed in\" card that bounces to / , gets rejected, and loops.\n    return `<div class=\"lroot\"><div class=\"card\">\n      <span class=\"pill\">acesso restrito</span>\n      <h1>Simulador de Timeline de Fechamentos</h1>\n      <p class=\"sub\">Entre com suas credenciais pra abrir a timeline de fechamentos de setores.</p>\n      <div class=\"row\">\n        <label class=\"fl\" for=\"em\">E-mail</label>\n        <input class=\"inp\" id=\"em\" type=\"email\" autocomplete=\"username\" autocapitalize=\"off\" autocorrect=\"off\" spellcheck=\"false\" placeholder=\"voce@natura.com.br\"${busy?' disabled':''}>\n      </div>\n      <div class=\"row\">\n        <label class=\"fl\" for=\"pw\">Senha</label>\n        <input class=\"inp\" id=\"pw\" type=\"password\" autocomplete=\"current-password\" placeholder=\"••••••••\"${busy?' disabled':''}>\n      </div>\n      <button class=\"act\" id=\"go\"${busy?' disabled':''}>${busy?'Entrando…':'Entrar'}</button>\n      ${err?`<div class=\"err\">⚠ ${esc(err)}</div>`:''}\n      ${d2.state.ok?`<div class=\"okbox\">✓ Autenticado. No app publicado o redirecionamento é automático.</div>`:''}\n      <div class=\"foot\">Geomarketing · Natura. O simulador é read-only sobre o Databricks — cada sessão salva fica presa ao snapshot congelado da simulação.</div>\n    </div></div>`;\n  },\n  mounted(d2, root){\n    // The router re-runs its policy hook when the session changes, so a successful login normally\n    // swaps the gated content on its own. If it does not (same path), reload as a last resort —\n    // cancelled on unmount, so the reload only ever fires when the swap did NOT happen.\n    // Login now runs through the strategy, so the runtime DOES hold the session in memory and the\n    // route policy would re-evaluate on its own. The hard navigation is kept deliberately: it is\n    // the one path that cannot land on stale client state. In the builder there is no app root to\n    // go to, so we just report success instead of navigating out of /preview/<ref>. [agent:ad78d19d]\n    const enter=()=>{\n      if(d2.context.isBuilder){ d2.setState({busy:false,error:'',ok:true}); return; }\n      try{ window.location.replace('/'); }\n      catch(e){ try{ window.location.reload(); }catch(e2){} }\n    };\n    // pt-BR (ABNT2) keyboards put ^ ~ ´ ¨ on DEAD keys: pressing one before a character that cannot\n    // take the accent (like @) emits the bare mark, or leaves a combining codepoint behind. Either\n    // way it must never reach /id/ln, so strip it plus zero-width paste junk and any whitespace.\n    // The PASSWORD is deliberately left untouched — these characters are legitimate in a password.\n    const cleanEmail=s=>String(s==null?'':s)\n      .normalize('NFKC')\n      .replace(/[̀-ͯ]/g,'')\n      .replace(/[​-‍﻿]/g,'')\n      .replace(/[\\^~´¨`]/g,'')\n      .replace(/\\s+/g,'');\n    // A failed login must NEVER navigate or reload: keep the card, keep what was typed (the morph\n    // patches in place so input values survive), show the error and put the cursor back in the\n    // password so it can be corrected immediately.\n    const failWith=msg=>{\n      d2.setState({busy:false,ok:false,error:msg});\n      setTimeout(()=>{ const p=d2.$('#pw'); if(p){ try{ p.focus(); p.select(); }catch(e){} } },0);\n    };\n    const submit=async()=>{\n      if(d2.state.busy) return;\n      const rawEm=(d2.$('#em')||{}).value||'';\n      const em=cleanEmail(rawEm);\n      if(em!==rawEm && d2.$('#em')) d2.$('#em').value=em;   // show the cleaned address back to the user\n      const pw=((d2.$('#pw')||{}).value||'');\n      if(!em||!pw){ failWith('Preencha e-mail e senha.'); return; }\n      d2.setState({busy:true,error:'',ok:false});\n      try{\n        // Direct interaction with the AuthenticationStrategy — no hand-rolled fetch. The runtime\n        // owns the session envelope, so IT attaches `authorization: Bearer <jwt>` (and the session\n        // cookie) to the login POST. The previous hand-rolled fetch could not: it derived the jwt\n        // from document.cookie on the APP origin while the cookie lives on the SSP origin, so the\n        // header was silently dropped on every attempt — the 401 in devtools. [agent:ad78d19d]\n        // ⚠️ `credentials` MUST stay NESTED — a flat {email,password} authenticates nobody and\n        // fails as an UNCATCHABLE 400/403 (no exception, no rejected promise).\n        // ⚠️ A rejected login is NOT an exception either — after the promise settles, read\n        // `isAuthed`. Do not gate the error UI on catch().\n        const a=d2.auth(AUTH);\n        if(!a){ failWith('Estratégia de autenticação não encontrada ('+AUTH+').'); return; }\n        await a.login({credentials:{email:em,password:pw}});\n        if(a.isAuthed){ enter(); return; }\n        failWith('E-mail ou senha inválidos.');\n      }catch(e){\n        failWith('Falha ao autenticar: '+((e&&e.message)||'erro desconhecido'));\n      }\n    };\n    d2.on('#go','click',submit);\n    d2.on('.inp','keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); submit(); } });\n  }\n}"}}}}}}}},{"_id":"6a74b62bdad5912765098ff0","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","component":"timeline_sim_session_read","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\n\nexport default {\n  css: `\n  :host{display:block;height:100%}\n  *{box-sizing:border-box}\n  .rroot{--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--closing:#ff3b30;--ok:#34c759;--warn:#ff9500;\n    height:100%;overflow:auto;background:var(--bg);color:var(--ink);\n    font:13px/1.5 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;-webkit-font-smoothing:antialiased}\n  .shell{max-width:1080px;margin:0 auto;padding:26px 22px 70px}\n  header.top{margin-bottom:20px}\n  .eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);font-weight:600}\n  h1{font-size:25px;font-weight:600;letter-spacing:-.5px;margin:7px 0 10px}\n  .pills{display:flex;gap:7px;flex-wrap:wrap;align-items:center}\n  .pill{font-size:10.5px;padding:3px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel)}\n  .pill b{color:var(--ink);font-weight:600}\n  .pill.go{cursor:pointer;border-color:var(--acc);color:var(--acc);background:#eaf3ff;font-weight:600}\n  .pill.go:hover{background:#dbe9ff}\n  .goact{margin-top:15px;background:var(--acc);color:#fff;border:1px solid transparent;border-radius:8px;\n    padding:9px 16px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer}\n  .goact:hover{background:#0060df}\n  .strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:18px 0 22px}\n  .st{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .st .n{font-size:23px;font-weight:700;letter-spacing:-.5px}\n  .st .l{font-size:10.5px;color:var(--mut);margin-top:3px;line-height:1.4}\n  .st.warn .n{color:var(--closing)}\n  .stepper{position:sticky;top:0;z-index:5;display:flex;gap:7px;overflow-x:auto;padding:11px 0;margin-bottom:6px;\n    background:rgba(245,245,247,.9);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}\n  .sc{flex:0 0 auto;display:flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);\n    border-radius:99px;padding:5px 12px 5px 6px;cursor:pointer;font-size:12px}\n  .sc:hover{border-color:var(--acc)}\n  .sc .k{width:19px;height:19px;border-radius:99px;background:var(--ink);color:#fff;font-size:10px;font-weight:700;\n    display:flex;align-items:center;justify-content:center}\n  .sc .arrow{color:var(--mut)}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:16px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card > h2{font-size:15px;font-weight:600;letter-spacing:-.2px;margin:0 0 3px}\n  .card .sub{font-size:11.5px;color:var(--mut);margin:0 0 15px}\n  h3{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:20px 0 9px;font-weight:600}\n  h3:first-of-type{margin-top:0}\n  .split{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:20px;align-items:start}\n  @media(max-width:820px){.split{grid-template-columns:1fr}}\n  svg.map{width:100%;height:auto;display:block;background:var(--panel2);border-radius:10px;border:1px solid var(--line);cursor:grab;touch-action:none}\n  svg.map.dragging{cursor:grabbing}\n  .map-wrap{position:relative}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 2px 8px rgba(0,0,0,.14);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  .muni{stroke:#fff;stroke-width:1.1}\n  .nbr{fill:#e8e8ec;stroke:#d4d4d9;stroke-width:.7}\n  .exc{fill:#f7e7d0;stroke:var(--warn);stroke-width:.9;stroke-dasharray:2 2}\n  .mlabel{fill:#1c1c1e;font-size:9.5px;font-weight:600;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.7);stroke-width:2.4px}\n  .nlabel{fill:#9a9aa0;font-size:7.5px;text-anchor:middle}\n  .legend{display:flex;gap:11px;flex-wrap:wrap;margin-top:9px;font-size:11px;color:var(--mut)}\n  .legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .bar{display:flex;align-items:center;gap:9px;margin:7px 0}\n  .bar .tag{width:46px;font-weight:600;font-size:12px}\n  .bar .track{flex:1;height:17px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex}\n  .bar .b1{height:100%;opacity:.38}.bar .b2{height:100%}.bar .b3{height:100%;opacity:.7}\n  .bar .v{width:120px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  .mrow{display:flex;align-items:center;gap:9px;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:6px 10px;margin-bottom:5px}\n  .mrow .nm{flex:1;font-weight:600;font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .mrow .cb{color:var(--mut);font-size:11px;font-variant-numeric:tabular-nums}\n  .chip{font-size:11px;font-weight:700;color:#fff;border-radius:6px;padding:2px 8px}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:6px 7px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px}\n  .dot{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .note{font-size:10.5px;color:var(--mut);line-height:1.55;margin-top:9px}\n  .flag{margin-top:11px;padding:9px 11px;border-radius:9px;font-size:11.5px;line-height:1.55;background:#fff8ef;border:1px solid #ffe0bd;color:#8a5200}\n  .scroll{max-height:300px;overflow:auto}\n  .center{display:flex;align-items:center;justify-content:center;height:100%;padding:40px}\n  .msg{max-width:420px;text-align:center}\n  .msg h2{font-size:17px;font-weight:600;margin:0 0 7px}\n  .msg p{font-size:12.5px;color:var(--mut);margin:0;line-height:1.55}\n`,\n  render(d2){\n    // Hierarquia mercado > GV > setor, espelhando o simulador ao vivo. Tudo pré-computado (label,\n    // minúscula, plural, gênero) pra nunca minusculizar \"GV\" (sigla) virando \"gv\", e pra concordar\n    // gênero certo em cada frase — GV é feminina (a Gerência de Vendas), o resto é masculino.\n    const LVL={\n      setor:{label:'Setor',low:'setor',fem:false, child:'Município',childLow:'município',childPlural:'Municípios',childFem:false, dest:'Setor',destLow:'setor'},\n      gv:{label:'GV',low:'GV',fem:true, child:'Setor',childLow:'setor',childPlural:'Setores',childFem:false, dest:'GV',destLow:'GV'},\n      mercado:{label:'Mercado',low:'mercado',fem:false, child:'GV',childLow:'GV',childPlural:'GVs',childFem:true, dest:'Mercado',destLow:'mercado'},\n    };\n    const lv=s=>LVL[(s&&s.level)||'setor'];\n    const esc=s=>(s==null?'':String(s)).replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n    const fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\n    const money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\n    const msg=(t,b,act)=>`<div class=\"rroot\"><div class=\"center\"><div class=\"msg\"><h2>${t}</h2><p>${b}</p>${act?`<button class=\"goact\" id=\"goSim\">Abrir o simulador</button>`:''}</div></div></div>`;\n\n    if(d2.state.error) return msg('Não foi possível abrir a sessão',esc(d2.state.error),true);\n    if(!d2.state.ready) return msg('Carregando sessão…','Buscando o registro compartilhado.');\n    const r=d2.state.rec;\n    if(!r) return msg('Sessão não encontrada','O link pode ter expirado ou o registro foi removido. Peça um novo link a quem compartilhou.',true);\n\n    const stages=Array.isArray(r.stages)?r.stages:[];\n    const sm=r.summary||{};\n    const st=r.settings||{};\n    const ruleDest=st.destRule==='dominant'?`destino: dominantes ≥${fmt(st.destMinCbs)} CBs`:'destino: qualquer município';\n    const ruleReal=st.realocRule==='relevant'?`realocação: ≥${fmt(st.realocMinCbs)} CBs`:'realocação: todos';\n    const spanBits=[];\n    if(st.targetSpan)spanBits.push('alvo '+fmt(st.targetSpan));\n    if(st.maxSpan)spanBits.push('máx '+fmt(st.maxSpan));\n    if(st.minSpan)spanBits.push('mín '+fmt(st.minSpan));\n\n    let h=`<div class=\"rroot\"><div class=\"shell\">\n      <header class=\"top\">\n        <div class=\"eyebrow\">Simulador de timeline de fechamentos · sessão compartilhada</div>\n        <h1>${esc(r.label||'Sessão sem nome')}</h1>\n        <div class=\"pills\">\n          <span class=\"pill\">● snapshot congelado · <b>${esc(r.snapshotRaw||'—')}</b></span>\n          <span class=\"pill\">◇ ${esc(ruleReal)}</span>\n          <span class=\"pill\">◈ ${esc(ruleDest)}</span>\n          <span class=\"pill\">◎ span: <b>${spanBits.length?esc(spanBits.join(' · ')):'livre'}</b></span>\n          ${r.sharedBy?`<span class=\"pill\">por <b>${esc(r.sharedBy)}</b></span>`:''}\n          ${r.sharedAt?`<span class=\"pill\">${esc(new Date(r.sharedAt).toLocaleString('pt-BR'))}</span>`:''}\n          <span class=\"pill go\" id=\"goSim\" role=\"button\" tabindex=\"0\" title=\"Ir para o simulador\">↗ abrir o simulador</span>\n        </div>\n      </header>`;\n\n    const sessLevel=(stages[0]&&stages[0].level)||'setor';\n    h+=`<div class=\"strip\">\n      <div class=\"st\"><div class=\"n\">${fmt(stages.length)}</div><div class=\"l\">Etapa(s) de fechamento</div></div>\n      <div class=\"st\"><div class=\"n\">${fmt(sm.totalCbs)}</div><div class=\"l\">CBs realocadas na cadeia</div></div>\n      ${sessLevel==='setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs deslocadas (quebra de relação)</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs com líder dividida entre destinos</div></div>`}\n      <div class=\"st\"><div class=\"n\" style=\"font-size:15px;line-height:1.5;font-weight:600\">${esc((sm.sectors||[]).join(' → ')||'—')}</div><div class=\"l\">Ordem de fechamento</div></div>\n    </div>`;\n\n    if(stages.length>1){\n      h+=`<div class=\"stepper\">`+stages.map((s,i)=>`<div class=\"sc\" data-goto=\"stage-${i}\"><span class=\"k\">${i+1}</span>${lv(s).label} ${esc(s.sector)}<span class=\"arrow\">→</span>${esc((s.dest||[]).join(', ')||'–')}</div>`).join('')+`</div>`;\n    }\n\n    // ---- projection helpers (pure; mirrors the simulator's) ----\n    const project=feats=>{let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n      feats.forEach(f=>{const g=f&&f.geometry;if(!g)return;const ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n        ps.forEach(p=>p.forEach(rg=>rg.forEach(pt=>{mnx=Math.min(mnx,pt[0]);mxx=Math.max(mxx,pt[0]);mny=Math.min(mny,pt[1]);mxy=Math.max(mxy,pt[1]);})));});\n      if(!isFinite(mnx)) return null;\n      const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=680,H=440,pad=16;\n      const gw=Math.max((mxx-mnx)*kx,1e-9),gh=Math.max(mxy-mny,1e-9),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n      const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n      return {W,H,f:pt=>[ox+(pt[0]-mnx)*kx*sc, oy+(mxy-pt[1])*sc]};};\n    const pathFor=(f,pr)=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      return ps.map(poly=>poly.map(rg=>'M'+rg.map(pt=>{const q=pr(pt);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');};\n    const cxy=(f,pr)=>{let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      ps.forEach(poly=>poly[0].forEach(pt=>{const q=pr(pt);x+=q[0];y+=q[1];n++;}));return n?[x/n,y/n]:[0,0];};\n\n    stages.forEach((s,i)=>{\n      const color=s.color||{};\n      const k=s.kpis||{};\n      const LV=lv(s);\n      h+=`<div class=\"card\" id=\"stage-${i}\" data-key=\"stage-${i}\">\n        <h2>Etapa ${i+1} · ${LV.low} <b>${esc(s.sector)}</b> fechad${LV.fem?'a':'o'}</h2>\n        <p class=\"sub\">Território redistribuído para ${esc((s.dest||[]).join(', ')||'—')}${s.domUF?' · '+esc(s.domUF):''}${s.policy?' · política: '+(s.policy==='fora'?'manter vínculo':'quebrar relação'):''}</p>\n        <div class=\"strip\" style=\"margin:0 0 18px\">\n          <div class=\"st\"><div class=\"n\">${fmt(k.cbsRealocadas)}</div><div class=\"l\">CBs realocadas</div></div>\n          ${LV.label==='Setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs deslocadas</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs com líder dividida</div></div>`}\n          <div class=\"st\"><div class=\"n\">${fmt(k.municipios)}</div><div class=\"l\">${LV.childPlural} movid${LV.childFem?'as':'os'}</div></div>\n          <div class=\"st\"><div class=\"n\">${fmt(k.lideres)}</div><div class=\"l\">Líderes envolvidas</div></div>\n        </div>\n        <div class=\"split\"><div>`;\n\n      // map\n      const gf=s.geoFeat||{}, nb=Array.isArray(s.nbrFeat)?s.nbrFeat:[], ex=Array.isArray(s.excludedFeat)?s.excludedFeat:[];\n      const all=[...nb,...ex,...Object.values(gf)].filter(Boolean);\n      const P=project(all);\n      const unitOf=m=>LV.label==='Setor'?m:((s.unitOfMuni||{})[m]||m);\n      if(P){\n        let g='';\n        nb.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"nbr\" d=\"${pathFor(f,P.f)}\"/>`;\n          const c=cxy(f,P.f);const nm=(f.properties&&f.properties.name)||'';\n          if(nm) g+=`<text class=\"nlabel\" data-fs=\"7.5\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(nm.slice(0,13))}</text>`; });\n        ex.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"exc\" d=\"${pathFor(f,P.f)}\"/>`; });\n        if(LV.label==='Setor'){\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return;\n            const dest=(s.assign||{})[m];\n            g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${esc(color[dest]||'#8e8e93')}\"/>`;\n            const c=cxy(f,P.f);\n            g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(m.slice(0,14))}<tspan data-fs=\"8\" x=\"${c[0].toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`; });\n        } else {\n          // GV/mercado: vários municípios do mesmo setor/GV são UMA unidade só na tela — mesma cor,\n          // mesmo stroke (a costura entre elas some), um rótulo só no centro do grupo em vez de um\n          // por município. Espelha o mapa ao vivo do simulador.\n          const groups={};\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return; const u=unitOf(m); (groups[u]=groups[u]||[]).push(f); });\n          Object.entries(groups).forEach(([u,feats])=>{\n            const dest=(s.assign||{})[u]; const col=esc(color[dest]||'#8e8e93');\n            let cx=0,cy=0,n=0;\n            feats.forEach(f=>{ g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${col}\" stroke=\"${col}\"/>`;\n              const c=cxy(f,P.f); cx+=c[0]; cy+=c[1]; n++; });\n            if(n){ cx/=n; cy/=n;\n              g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${cx.toFixed(1)}\" y=\"${cy.toFixed(1)}\">${esc(u)}<tspan data-fs=\"8\" x=\"${cx.toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`;\n            }\n          });\n        }\n        h+=`<h3>Mapa da redistribuição</h3><div class=\"map-wrap\">\n          <svg class=\"map\" id=\"map-${i}\" data-w=\"${P.W}\" data-h=\"${P.H}\" viewBox=\"0 0 ${P.W} ${P.H}\" preserveAspectRatio=\"xMidYMid meet\">${g}</svg>\n          <div class=\"zoom-pill\"><button type=\"button\" data-zoom=\"out\" data-i=\"${i}\" title=\"Diminuir zoom\">−</button><span class=\"zoom-div\"></span><button type=\"button\" data-zoom=\"in\" data-i=\"${i}\" title=\"Aumentar zoom\">+</button></div>\n          </div>\n          <div class=\"legend\">${(s.dest||[]).map(d=>`<span><i style=\"background:${esc(color[d]||'#8e8e93')}\"></i>${LV.dest} ${esc(d)}</span>`).join('')}\n          <span><i style=\"background:#e8e8ec;border:1px solid #d4d4d9\"></i>vizinhos (contexto)</span>\n          ${ex.length?`<span><i style=\"background:#f7e7d0;border:1px dashed #ff9500\"></i>fora da realocação</span>`:''}</div>`;\n      }\n      h+=`<h3>${LV.childPlural} → ${LV.destLow} destino</h3>`;\n      h+=(s.municipios||[]).map(m=>`<div class=\"mrow\"><span class=\"nm\">${esc(m.nome)}</span><span class=\"cb\">${fmt(m.cbs)} CBs</span>\n        <span class=\"chip\" style=\"background:${esc(color[m.destino]||'#8e8e93')}\">${esc(m.destino||'–')}</span></div>`).join('')||`<div class=\"note\">Nenhum${LV.childFem?'a':''} ${LV.childLow} realocad${LV.childFem?'a':'o'} nesta etapa.</div>`;\n\n      h+=`</div><div>`;\n      // spans\n      const ds=s.destinos||[];\n      const mx=Math.max(...ds.map(d=>+d.span||0),1);\n      h+=`<h3>Tamanho d${LV.fem?'a':'o'} ${LV.destLow} depois da etapa</h3>`;\n      h+=ds.map(d=>{const base=+d.base||0,inc=+d.inc||0,orph=+d.orph||0;\n        return `<div class=\"bar\"><span class=\"tag\" style=\"color:${esc(color[d.s]||'#8e8e93')}\">${esc(d.s)}</span>\n          <span class=\"track\"><span class=\"b1\" style=\"width:${(base/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span><span class=\"b2\" style=\"width:${(inc/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>${orph?`<span class=\"b3\" style=\"width:${(orph/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>`:''}</span>\n          <span class=\"v\">${fmt(d.span)} <span style=\"opacity:.6\">(+${fmt(inc+orph)})</span></span></div>`;}).join('')||`<div class=\"note\">Sem ${LV.destLow} destino nesta etapa.</div>`;\n      h+=`<div class=\"note\">Claro = base antes da etapa · cheio = CBs entrantes${(s.orphanTotal?' · faixa final = CBs que seguiram suas líderes':'')}.</div>`;\n\n      // leaders\n      const L=s.lideres||[];\n      if(L.length){\n        const showOrph=L.some(x=>+x.orphan>0);\n        h+=`<h3>Líderes</h3><div class=\"scroll\"><table><thead><tr><th>Líder</th><th>CBs</th><th>${LV.dest} final</th>${showOrph?'<th>Órfãs</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n          +L.map(x=>`<tr><td>${esc(x.lider||'—')}</td><td>${fmt(x.total)}</td>\n            <td>${x.home?`<span class=\"dot\" style=\"background:${esc(color[x.home]||'#8e8e93')}\"></span>${esc(x.home)}`:'–'}</td>\n            ${showOrph?`<td>${x.orphan?fmt(x.orphan):'·'}</td>`:''}\n            <td style=\"color:${+x.desloc?'var(--closing)':'var(--mut)'};font-weight:${+x.desloc?700:400}\">${fmt(x.desloc||0)}</td></tr>`).join('')\n          +`</tbody></table></div>`;\n      }\n      h+=`</div></div>`;\n\n      // antes -> depois\n      const M=s.metricas||[];\n      if(M.length){\n        h+=`<h3>Impacto antes → depois por ${LV.destLow} ${LV.fem?'receptora':'receptor'}</h3><div class=\"scroll\"><table><thead><tr><th>${LV.dest}</th>${M.map(m=>`<th>${esc(m.label)}</th>`).join('')}</tr></thead><tbody>`;\n        (s.dest||[]).forEach(sec=>{\n          h+=`<tr><td><span class=\"dot\" style=\"background:${esc(color[sec]||'#8e8e93')}\"></span>${esc(sec)}</td>`;\n          h+=M.map(m=>{const row=(m.rows||[]).find(x=>String(x.s)===String(sec))||{};\n            const a=+row.antes||0,b=+row.depois||0,d=a?((b-a)/a*100):0;const f=m.money?money:fmt;\n            return `<td>${f(b)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;\n        });\n        h+=`<tr style=\"opacity:.75\"><td>${esc(s.sector)} (sai)</td>`+M.map(m=>`<td>${(m.money?money:fmt)(m.sai||0)}</td>`).join('')+`</tr>`;\n        h+=`</tbody></table></div>`;\n      }\n\n      // caveats\n      const fl=[];\n      if(s.excluidos&&s.excluidos.length) fl.push(`<b>${fmt(s.excluidos.length)} município(s)</b> com poucas CBs ficaram fora da realocação geográfica (quebra de vínculo): ${esc(s.excluidos.slice(0,6).map(x=>x.muni+' ('+x.cbs+')').join(', '))}${s.excluidos.length>6?' +'+(s.excluidos.length-6):''}. As CBs seguiram a líder do seu grupo.`);\n      if(s.orphanStranded) fl.push(`<b>${fmt(s.orphanStranded)} CB(s)</b> sem setor a seguir — a líder não ficou com nenhum município realocado.`);\n      if(s.noShapeCbs) fl.push(`<b>${fmt(s.noShapeCbs)} CB(s)</b> em municípios sem shape na UF, fora do mapa e de qualquer setor destino.`);\n      if(s.optTier===1) fl.push(`O span mínimo não era atingível nesta etapa e foi relaxado pelo modelo.`);\n      if(s.optTier===2) fl.push(`Os limites de span não eram atingíveis nesta etapa e foram relaxados pelo modelo.`);\n      if(s.optTier===-1) fl.push(`Etapa resolvida por heurística (espaço de busca grande) — os limites de span não são garantidos.`);\n      if(s.destRuleFallback) fl.push(`Nenhum município dominante nesta etapa: a regra de destino foi relaxada para \"qualquer município\".`);\n      if(s.realocFallback) fl.push(`Todos os municípios ficariam abaixo do limite: a regra de realocação foi relaxada para \"realocar todos\".`);\n      if(fl.length) h+=`<div class=\"flag\">⚠ ${fl.join('<br>')}</div>`;\n      h+=`</div>`;\n    });\n\n    // cumulative\n    // Este resumo é calculado no rastro de município (world.muniOverride) — hoje só existe pro\n    // fechamento de setor; GV/mercado têm o rastro equivalente (secOverride/gvOverride) mas essa\n    // agregação ainda não foi refeita nesse grão, então avisa em vez de mostrar tabela vazia.\n    const rc=sm.receivers||[];\n    if(!rc.length && sessLevel!=='setor'){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"note\" style=\"margin-top:0\">Ainda não disponível pra sessões de ${sessLevel==='gv'?'GV':'mercado'} — veja o relatório de cada etapa acima.</p></div>`;\n    }\n    if(rc.length){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"sub\">Efeito de <b>todas</b> as ${fmt(stages.length)} etapa(s) somadas, contra o snapshot congelado sem nenhum fechamento aplicado.</p>\n        <div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th><th>CBs antes</th><th>CBs depois</th><th>Variação</th></tr></thead><tbody>`\n        +rc.map(x=>{const a=+x.antes||0,b=+x.depois||0,d=a?((b-a)/a*100):0;\n          return `<tr><td>${esc(x.s)}</td><td>${esc(x.situacao||'recebeu território')}</td><td>${fmt(a)}</td><td>${fmt(b)}</td>\n            <td class=\"delta ${d>=0?'up':'dn'}\" style=\"font-size:11.5px\">${d>0?'+':''}${d.toFixed(0)}%</td></tr>`;}).join('')\n        +`</tbody></table></div></div>`;\n    }\n\n    h+=`<div class=\"note\" style=\"text-align:center;margin-top:22px\">Visualização somente leitura de uma simulação congelada. Os números refletem o snapshot <b>${esc(r.snapshotRaw||'—')}</b> e as regras vigentes no momento do compartilhamento — não acompanham o dado ao vivo.</div>`;\n    return h+`</div></div>`;\n  },\n  mounted(d2, root){\n    d2.on('#goSim','click',()=>{\n      try{ (d2.router('timelineSimRouter')||d2.router()).goTo('/'); }catch(e){ try{ d2.router().goTo('/'); }catch(e2){} }\n    });\n    d2.on('.sc','click',e=>{\n      const el=e.target.closest('.sc'); if(!el) return;\n      const t=d2.$('#'+el.dataset.goto); if(t) t.scrollIntoView({behavior:'smooth',block:'start'});\n    });\n    // ---- map zoom/pan (viewBox só — texto e polígonos vivem no mesmo sistema de coordenadas, então\n    // escalam juntos automaticamente ao encolher/esticar o viewBox, sem lógica extra pra fonte).\n    // Delegado em `root` (nunca substituído por re-render) em vez de nos <svg> por id, porque cada\n    // atualização de estado reconstrói o HTML das etapas do zero.\n    const mapViews={};\n    const getMapView=(i,W,H)=>mapViews[i]||(mapViews[i]={x:0,y:0,w:W,h:H});\n    const applyMapView=(svg,i)=>{ const v=mapViews[i]; if(!v) return;\n      svg.setAttribute('viewBox', v.x.toFixed(2)+' '+v.y.toFixed(2)+' '+v.w.toFixed(2)+' '+v.h.toFixed(2));\n      // Rótulo tem tamanho em unidades do viewBox — sem isso ele cresceria junto com o zoom até virar\n      // um borrão. Encolher na mesma proporção que o viewBox encolhe cancela o zoom: o texto fica do\n      // mesmo tamanho na tela em qualquer nível. style.fontSize (não o atributo) porque uma regra de\n      // classe como .mlabel{font-size:...} outrankaria um atributo comum.\n      const W=+svg.dataset.w||700;\n      const fsScale=v.w/W;\n      svg.querySelectorAll('[data-fs]').forEach(t=>{\n        const base=+t.getAttribute('data-fs')||8;\n        t.style.fontSize=(base*fsScale).toFixed(2)+'px';\n      });\n      // Halo do texto (contorno branco que garante legibilidade sobre qualquer cor de fundo) tem\n      // largura de traço fixa em unidades do viewBox — sem reescalar junto, no zoom alto ele vira\n      // uma mancha cobrindo a letra inteira em vez de um contorno fino.\n      svg.querySelectorAll('[data-sw]').forEach(t=>{\n        const base=+t.getAttribute('data-sw')||1;\n        t.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n      });\n    };\n    const zoomStageMap=(i,factor)=>{\n      const svg=root.querySelector('#map-'+i); if(!svg) return;\n      const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      const cx=v.x+v.w/2, cy=v.y+v.h/2;\n      let w=v.w/factor, h=v.h/factor;\n      w=Math.min(W, Math.max(W/8, w)); h=Math.min(H, Math.max(H/8, h));\n      let x=cx-w/2, y=cy-h/2;\n      x=Math.min(W-w, Math.max(0,x)); y=Math.min(H-h, Math.max(0,y));\n      mapViews[i]={x,y,w,h};\n      applyMapView(svg,i);\n    };\n    d2.on('.zoom-pill button','click',e=>{\n      const b=e.target.closest('button'); if(!b) return;\n      zoomStageMap(b.dataset.i, b.dataset.zoom==='in'?1.4:1/1.4);\n    });\n    let mapDrag=null;\n    root.addEventListener('pointerdown',e=>{\n      const svg=e.target.closest('svg.map'); if(!svg) return;\n      const i=svg.id.replace('map-',''); const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      mapDrag={svg,i,W,H,x0:e.clientX,y0:e.clientY,vx0:v.x,vy0:v.y,rect:svg.getBoundingClientRect()};\n      svg.classList.add('dragging');\n    });\n    root.addEventListener('pointermove',e=>{\n      if(!mapDrag||!mapDrag.rect.width||!mapDrag.rect.height) return;\n      const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n      const v=getMapView(mapDrag.i,mapDrag.W,mapDrag.H);\n      const sx=v.w/mapDrag.rect.width, sy=v.h/mapDrag.rect.height;\n      let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n      x=Math.min(mapDrag.W-v.w, Math.max(0,x)); y=Math.min(mapDrag.H-v.h, Math.max(0,y));\n      mapViews[mapDrag.i]={...v,x,y};\n      applyMapView(mapDrag.svg,mapDrag.i);\n    });\n    const endMapDrag=()=>{ if(mapDrag){ mapDrag.svg.classList.remove('dragging'); mapDrag=null; } };\n    root.addEventListener('pointerup',endMapDrag);\n    root.addEventListener('pointerleave',endMapDrag);\n    (async()=>{\n      try{\n        // RACE, not a missing dataset. This DataSet is gated by timelineSimAuthFinal, so its\n        // Process is only define()d once the auth session resolves — a Process waits for its\n        // dependencies' setup before its own fires. In the field the auth lookup lands ~200ms\n        // and this DataSet's first fetch ~700-900ms, while mounted() runs immediately. d2.data()\n        // returns null for a module that is not defined YET, so bailing on the first miss threw\n        // \"dataset não disponível\" on every share link even though nothing was wrong.\n        // Poll briefly instead. Two lookups because either may win: the declared `shared` param,\n        // or the global-process path via the reference the instance binds — keep that binding.\n        let dead=false; d2.onUnmount(()=>{ dead=true; });\n        let ds=null;\n        for(let i=0;i<40 && !ds && !dead;i++){\n          ds=d2.data('shared')||d2.data('timeline-shared-sessionsDataSet');\n          if(!ds) await new Promise(r=>setTimeout(r,100));\n        }\n        if(dead) return;\n        if(!ds){ d2.setState({error:'Dataset de sessões compartilhadas não disponível.',ready:true}); return; }\n        // Name the router explicitly. Bare d2.router() looks for the conventional\n        // 'Router'/'MainRouter'; this one is 'timelineSimRouter', so the bare call can hand back a\n        // handle whose .params is empty — which is how a valid /sessions/<id> link ended up\n        // reporting \"o link não traz o identificador\". The goSim handler below already passes the\n        // ref for the same reason.\n        const rt=d2.router('timelineSimRouter')||d2.router();\n        const p=(rt&&rt.params)||{};\n        // ?id= lets the artboard be opened directly, outside the route.\n        let q=null;\n        try{ q=new URLSearchParams(window.location.search).get('id'); }catch(e){}\n        // Last resort, and the only one that cannot fail: the id is sitting in the path. Route\n        // params need the Router handle to resolve AND to have matched by the time mounted() runs\n        // (.params is a reactive getter, read once here); the pathname needs neither. Matches the\n        // current /sessions/:session_id route and the legacy /sessao/:id one.\n        let seg=null;\n        try{ const m=window.location.pathname.match(/\\/sess(?:ions|ao)\\/([^/?#]+)/); seg=m&&m[1]; }catch(e){}\n        const id=p.session_id||p.id||p.sessao||p.session||seg||q;\n        if(!id){ d2.setState({error:'O link não traz o identificador da sessão.',ready:true}); return; }\n        // .query() is a direct read and returns the rows — no refresh/loadAll ordering trap here\n        const rows=await ds.query({_id:id});\n        const rec=Array.isArray(rows)?rows[0]:(rows&&rows.data&&rows.data[0]);\n        d2.setState({rec:rec||null,ready:true,error:''});\n      }catch(err){\n        d2.setState({error:(err&&err.message)||'Falha ao carregar a sessão compartilhada.',ready:true});\n      }\n    })();\n  }\n}"}},"shared":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shared"}}}}}}}}}},{"_id":"6a75620d9f1ece2343b4d4da","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","component":"cb-senso-de-rede","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// ---------- Senso de rede — afinidades entre consultoras de beleza ----------\n// Cada nó é uma CB; cada laço é uma AFINIDADE DERIVADA do snapshot mais recente, nunca um\n// cadastro de relacionamento — não existe tabela de \"quem conhece quem\". Por isso o peso de\n// cada dimensão é o INVERSO DA SUA FREQUÊNCIA na amostra: dividir a mesma líder diz muito mais\n// sobre duas CBs do que ambas estarem ativas no ciclo, e é o peso que decide a cor, a espessura\n// e quais laços sobrevivem ao corte por nó. Sem isso o grafo vira uma malha uniforme onde tudo\n// se liga a tudo e nada significa nada.\nconst T = 'product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst RUSH_FALLBACK = 'AwW_11Nkg-dKksP7ic8V8';\nconst SNAP = `(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n// Paleta = cores de sistema do macOS, as mesmas do simulador de timeline, pra que os dois módulos\n// leiam como um produto só e não como duas ferramentas diferentes coladas.\nconst BG = '#f5f5f7';\n\nconst DIMS = [\n  { k:'origem',     label:'Origem',     color:'#ff9500', hint:'mesmo município' },\n  { k:'territorio', label:'Território', color:'#34c759', hint:'mesmo setor' },\n  { k:'rede',       label:'Rede',       color:'#007aff', hint:'mesma líder' },\n  { k:'trajetoria', label:'Trajetória', color:'#af52de', hint:'mesmo nível do plano' },\n  { k:'ritmo',      label:'Ritmo',      color:'#ff2d55', hint:'pedidos por ciclo parecidos' },\n  { k:'alcance',    label:'Alcance',    color:'#ffcc00', hint:'pontos parecidos' },\n  { k:'porte',      label:'Porte',      color:'#30b0c7', hint:'receita parecida' },\n  { k:'marca',      label:'Marca',      color:'#a2845e', hint:'mix Natura/Avon parecido' },\n  { k:'constancia', label:'Constância', color:'#8e8e93', hint:'ativas no ciclo' },\n];\nconst DIM_BY = {}; DIMS.forEach(d => DIM_BY[d.k] = d);\nconst TINTS = ['#e6f0ff','#e6f7ea','#fff2e0','#f6ecfd','#ffe9ee','#e6f5f9','#f2f2f7'];\n\nconst esc = s => (s == null ? '' : String(s)).replace(/[&<>\"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\nconst digits = s => String(s == null ? '' : s).replace(/[^0-9]/g, '');\nconst letters = s => String(s == null ? '' : s).replace(/[^A-Za-zÀ-ÿ]/g, '');\nconst hash = s => { let h = 0; const t = String(s || ''); for (let i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) | 0; return Math.abs(h); };\nconst titleCase = s => String(s || '').toLowerCase().replace(/(^|[\\s'-])([a-zà-ÿ])/g, (m, a, b) => a + b.toUpperCase());\n\nfunction rowsOf(resp) {\n  const c = (resp.columns || []).map(x => x.name);\n  return (resp.rows || []).map(a => Object.fromEntries(c.map((k, i) => [k, a[i]])));\n}\nasync function q(sql) {\n  const ref = d2.params.consultaRush || RUSH_FALLBACK;\n  let dbx;\n  try {\n    const res = await d2.rush(ref).invoke({ sql });\n    dbx = res.data.result.data;              // envelope do listener: res.data, depois o payload do Databricks\n  } catch (e) {\n    throw new Error('consulta indisponível (rush ' + ref + '): ' + (e && e.message ? e.message : e));\n  }\n  if (dbx.state && dbx.state !== 'SUCCEEDED') {\n    throw new Error('Databricks ' + dbx.state + ': ' + ((dbx.error && dbx.error.message) || 'erro desconhecido'));\n  }\n  return dbx;\n}\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--closing:#ff3b30}\n  *{box-sizing:border-box}\n  .root{display:flex;flex-direction:column;height:100%;background:var(--bg);color:var(--ink);\n    font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);\n    border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap;flex:0 0 auto}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .spacer{flex:1}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2);white-space:nowrap}\n  .pill b{color:var(--ink);font-weight:600}\n  .status{font-size:11px;color:var(--mut)}\n  .status.err{color:var(--closing)}\n  .btn{font:inherit;font-size:12px;padding:4.5px 11px;border-radius:7px;border:1px solid var(--line);background:var(--panel);color:var(--ink);cursor:pointer}\n  .btn:hover{border-color:#d5d5da;background:#fbfbfd}\n  .btn:active{background:#f0f0f4}\n  .btn:disabled{opacity:.5;cursor:progress}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;\n    font-size:11.5px;color:var(--mut);flex:0 0 auto}\n  .howto b{color:var(--ink)} .howto span{max-width:360px}\n  .body{flex:1;display:flex;min-height:0}\n  .stage{flex:1;position:relative;min-width:0}\n  canvas{width:100%;height:100%;display:block}\n  .side{width:334px;flex:0 0 334px;border-left:1px solid var(--line);padding:12px;display:flex;flex-direction:column;gap:12px;overflow:auto}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 13px}\n  .card h2{margin:0 0 9px;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);font-weight:600}\n  .card h3{margin:0 0 2px;font-size:15px;font-weight:600;letter-spacing:-.2px}\n  .card .sub{font-size:11.5px;color:var(--mut);margin-bottom:10px}\n  .facts{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}\n  .fact{background:var(--panel2);border-radius:8px;padding:6px 8px}\n  .fact b{display:block;font-size:13px;font-weight:600}\n  .fact span{font-size:9.5px;color:var(--mut);letter-spacing:.05em;text-transform:uppercase}\n  .conn{border-top:1px solid var(--line);padding-top:8px}\n  .conn h4{margin:0 0 4px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);font-weight:600}\n  .crow{display:flex;align-items:center;gap:8px;padding:5px 6px;margin:0 -6px;cursor:pointer;border-radius:7px}\n  .crow:hover{background:var(--panel2)}\n  .cdot{width:22px;height:22px;border-radius:50%;flex:0 0 22px;display:flex;align-items:center;justify-content:center;\n    font-size:9px;font-weight:600;color:#3a3a3c;border:1px solid rgba(60,60,67,.12)}\n  .cname{flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .tags{display:flex;gap:3px;flex-wrap:wrap;justify-content:flex-end;max-width:110px}\n  .tag{width:7px;height:7px;border-radius:50%}\n  .hintline{font-size:11.5px;color:var(--mut);line-height:1.5}\n  .hintline b{color:var(--ink)}\n  .legend{display:flex;flex-wrap:wrap;gap:6px}\n  .chip{display:inline-flex;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--line);border-radius:99px;\n    padding:4px 10px 4px 7px;font:inherit;font-size:11.5px;color:var(--ink);cursor:pointer;user-select:none;\n    transition:opacity .12s,border-color .12s,color .12s}\n  .chip i{width:8px;height:8px;border-radius:50%;flex:0 0 8px}\n  .chip b{font-weight:500;color:var(--mut);font-size:10px}\n  .chip:hover{border-color:var(--acc);color:var(--acc)}\n  .chip:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .chip.off{opacity:.4}\n  .tip{position:absolute;pointer-events:none;background:rgba(28,28,30,.94);color:#fff;border-radius:8px;padding:6px 9px;\n    font-size:11.5px;line-height:1.35;opacity:0;transition:opacity .1s;max-width:230px;z-index:5;box-shadow:0 8px 24px rgba(0,0,0,.18)}\n  .tip.on{opacity:1}\n  .tip em{font-style:normal;color:#aeaeb2;display:block;font-size:10.5px;margin-top:2px}\n  `,\n\n  // Shell estático de propósito: há um <canvas> aqui, e qualquer leitura reativa dentro de\n  // render() re-executa o render inteiro e devolve o canvas ao default 300x150 (checklist §15).\n  // Tudo que muda — contagem, cards, chips — é escrito por DOM dentro de mounted().\n  render() {\n    const chips = DIMS.map(d =>\n      `<button class=\"chip\" data-k=\"${d.k}\" title=\"${esc(d.hint)}\"><i style=\"background:${d.color}\"></i>${d.label} <b data-n=\"${d.k}\"></b></button>`\n    ).join('');\n    return `<div class=\"root\">\n      <header>\n        <h1>Senso de rede — consultoras</h1>\n        <span class=\"pill\"><b id=\"count\">—</b> pessoas</span>\n        <span class=\"status\" id=\"status\">consultando…</span>\n        <div class=\"spacer\"></div>\n        <button class=\"btn\" id=\"reload\">Recarregar</button>\n      </header>\n      <div class=\"howto\">\n        <span><b>1.</b> Cada nó é uma CB — o tamanho segue os pontos do ciclo, o anel marca quem está ativa.</span>\n        <span><b>2.</b> Cada laço é uma afinidade calculada; as contas no meio contam quantas dimensões as duas dividem.</span>\n        <span><b>3.</b> Clique num nó pra ver quem ele alcança — arraste pra reorganizar.</span>\n        <span><b>4.</b> Ligue e desligue dimensões nas marcações à direita.</span>\n      </div>\n      <div class=\"body\">\n        <div class=\"stage\" data-d2-static>\n          <canvas id=\"net\"></canvas>\n          <div class=\"tip\" id=\"tip\"></div>\n        </div>\n        <aside class=\"side\">\n          <section class=\"card\">\n            <h2>Afinidades</h2>\n            <div class=\"legend\" id=\"legend\">${chips}</div>\n          </section>\n          <section class=\"card\" id=\"detail\"></section>\n        </aside>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    const $ = s => root.querySelector(s);\n    const cv = $('#net'), ctx = cv.getContext('2d'), tip = $('#tip');\n    const S = {\n      nodes: [], edges: [], adj: new Map(), W: {}, on: {},\n      hover: null, sel: null, drag: null, alpha: 0, w: 0, h: 0\n    };\n    DIMS.forEach(d => S.on[d.k] = true);\n\n    const say = (msg, err) => { const el = $('#status'); el.textContent = msg || ''; el.classList.toggle('err', !!err); };\n\n    // ---------- canvas ----------\n    function resize() {\n      const r = cv.getBoundingClientRect();\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      S.w = r.width; S.h = r.height;\n      cv.width = Math.max(1, Math.round(r.width * dpr));\n      cv.height = Math.max(1, Math.round(r.height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);   // o resize zera a matriz — reaplicar sempre\n    }\n    const ro = new ResizeObserver(resize); ro.observe(cv);\n    d2.onUnmount(() => ro.disconnect());\n    resize();\n\n    // ---------- amostra ----------\n    async function nameColumn() {\n      // O nome da CB não está no contrato conhecido da tabela, então em vez de chutar um\n      // DS_NOME que pode não existir (e derrubar a query inteira), descobrimos a coluna.\n      try {\n        const d = await q(`DESCRIBE ${T}`);\n        const cols = rowsOf(d).map(r => String(r.col_name || r.name || '')).filter(Boolean);\n        const cand = cols.filter(c => /NOME|NAME/i.test(c) &&\n          !/LEADER|LIDER|MUNIC|ESTAD|SETOR|GRUPO|CANAL|MARCA|PAIS|REGIA|GV|NVL/i.test(c));\n        cand.sort((a, b) => (/CONSULT|CB|PESSOA/i.test(b) ? 1 : 0) - (/CONSULT|CB|PESSOA/i.test(a) ? 1 : 0));\n        return cand[0] || null;\n      } catch (e) { return null; }\n    }\n    function sqlFor(nomeCol, where, order, lim) {\n      const nome = nomeCol ? `MAX(${nomeCol}) nome,` : '';\n      return `SELECT CD_CONSULTORA cb, ${nome}\n        MAX(DS_MUNICIPIO) municipio, MAX(DS_ESTADO) uf, MAX(CD_SETOR) setor,\n        MAX(CD_GRUPO) grupo, MAX(DS_LEADER_NAME) lider, MAX(DS_NVL_PLAN_CRESC) nivel,\n        SUM(QT_PEDIDOS_CICLO) pedidos, SUM(QT_PONTOS_CICLO) pontos,\n        SUM(VL_PEDIDOS_NATURA_CICLO) rec_nat, SUM(VL_PEDIDOS_AVON_CICLO) rec_avon,\n        MAX(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativa\n       FROM ${T} WHERE ${where} GROUP BY CD_CONSULTORA ${order} LIMIT ${lim}`;\n    }\n\n    // Faixas relativas à AMOSTRA, não absolutas: \"ritmo parecido\" só quer dizer alguma coisa\n    // comparado com as outras CBs desta consulta.\n    function bucketer(vals) {\n      const v = vals.filter(x => isFinite(x)).sort((a, b) => a - b);\n      if (!v.length) return () => 0;\n      const at = p => v[Math.min(v.length - 1, Math.floor(p * v.length))];\n      const q1 = at(.25), q2 = at(.5), q3 = at(.75);\n      return x => (x <= q1 ? 0 : x <= q2 ? 1 : x <= q3 ? 2 : 3);\n    }\n\n    // Amostra sintética — existe para revisar layout/física sem sessão do simulador (o listener do\n    // Databricks é gated). Nunca é o default e se anuncia na tela: um grafo bonito com gente que não\n    // existe é pior do que um erro honesto.\n    function demoRows(n) {\n      const NOMES = ['Ana Ribeiro','Marta Souza','Cláudia Lima','Rita Alves','Vera Nunes','Sônia Braga','Ivone Castro','Lúcia Prado','Neide Rocha','Tânia Melo','Dora Pires','Elis Faria','Bruna Antunes','Carla Moraes','Selma Dutra','Joana Bastos','Regina Paiva','Marli Cardoso','Fátima Goulart','Simone Teles'];\n      const MUN = ['Brasília','Formosa','Buritis','Unaí','Paracatu','Arinos','Riachinho','Bonfinópolis','Chapada Gaúcha','Urucuia'];\n      const NIV = ['Bronze','Cristal','Prata','Ouro','Diamante'];\n      const out = [];\n      for (let i = 0; i < n; i++) {\n        const h = hash('cb' + i);\n        const mun = MUN[h % MUN.length];\n        const setor = 3200 + ((h >> 3) % 6) * 7;\n        const grupo = 100 + ((h >> 5) % 14);\n        const escala = .25 + ((h >> 7) % 100) / 100 * 1.8;\n        out.push({\n          cb: 900000 + i,\n          nome: NOMES[h % NOMES.length] + ' ' + String.fromCharCode(65 + (h >> 11) % 26) + '.',\n          municipio: mun, uf: mun === 'Chapada Gaúcha' || mun === 'Urucuia' ? 'MG' : 'GO',\n          setor: setor, grupo: grupo, lider: 'Líder ' + grupo,\n          nivel: NIV[(h >> 13) % NIV.length],\n          pedidos: Math.round(4 + escala * 22), pontos: Math.round(120 + escala * 900),\n          rec_nat: Math.round(escala * 3200), rec_avon: Math.round(escala * 3200 * (((h >> 17) % 100) / 100)),\n          ativa: (h >> 19) % 10 > 2 ? 1 : 0\n        });\n      }\n      return out;\n    }\n\n    async function load() {\n      say('consultando o snapshot mais recente…');\n      const lim = Math.max(12, Math.min(400, +d2.param('limite', 82) || 82));\n      const setor = digits(d2.param('setor', ''));\n      const uf = letters(d2.param('uf', '')).toUpperCase();\n      const aleat = String(d2.param('amostra', 'top') || 'top').toLowerCase().indexOf('aleat') === 0;\n      let where = `CD_PAIS=1 AND CD_GRUPO>0 AND DT_HISTORICO=${SNAP}`;\n      if (setor) where += ` AND CD_SETOR=${setor}`;\n      if (uf) where += ` AND UPPER(DS_ESTADO)='${uf}'`;\n      const order = aleat ? 'ORDER BY RAND()' : 'ORDER BY pontos DESC';\n\n      // `amostra: \"demo\"` em vez de um param novo — a declaração já existe e o modo fica visível\n      // no mesmo lugar onde se escolhe top/aleatória, não escondido num booleano à parte.\n      const demo = String(d2.param('amostra', 'top') || '').toLowerCase().indexOf('demo') === 0;\n      let raw;\n      if (demo) {\n        raw = demoRows(lim);\n      } else {\n        const nc = await nameColumn();\n        const attempts = nc ? [nc, null] : [null];\n        let data = null, lastErr = null;\n        for (const col of attempts) {\n          try { data = await q(sqlFor(col, where, order, lim)); break; }\n          catch (e) { lastErr = e; }\n        }\n        if (!data) throw lastErr || new Error('sem resposta da consulta');\n        raw = rowsOf(data);\n      }\n      if (!raw.length) throw new Error('nenhuma CB encontrada com esses filtros' + (setor ? ' (setor ' + setor + ')' : '') + (uf ? ' (UF ' + uf + ')' : ''));\n\n      const bPed = bucketer(raw.map(r => +r.pedidos || 0));\n      const bPon = bucketer(raw.map(r => +r.pontos || 0));\n      const bRec = bucketer(raw.map(r => (+r.rec_nat || 0) + (+r.rec_avon || 0)));\n\n      S.nodes = raw.map((r, i) => {\n        const nat = +r.rec_nat || 0, avon = +r.rec_avon || 0, rec = nat + avon;\n        const nome = titleCase(r.nome || '');\n        const cb = String(r.cb == null ? '' : r.cb);\n        const ini = nome ? d2.fmt.initials(nome, 2) : cb.slice(-2);\n        return {\n          i, cb, nome, ini,\n          municipio: titleCase(r.municipio || ''), uf: String(r.uf || ''),\n          setor: String(r.setor == null ? '' : r.setor), grupo: String(r.grupo == null ? '' : r.grupo),\n          lider: titleCase(r.lider || ''), nivel: String(r.nivel || ''),\n          pedidos: +r.pedidos || 0, pontos: +r.pontos || 0, receita: rec,\n          ativa: +r.ativa === 1 ? 1 : 0,\n          bRitmo: bPed(+r.pedidos || 0), bAlcance: bPon(+r.pontos || 0), bPorte: bRec(rec),\n          bMarca: rec > 0 ? Math.min(3, Math.floor((nat / rec) * 4)) : null,\n          tint: TINTS[hash(String(r.setor) + '|' + String(r.grupo)) % TINTS.length],\n          r: 0, x: 0, y: 0, vx: 0, vy: 0\n        };\n      });\n\n      const maxP = Math.max(...S.nodes.map(n => n.pontos), 1);\n      S.nodes.forEach(n => {\n        n.r = 7 + 17 * Math.sqrt(Math.max(0, n.pontos) / maxP);\n        const a = (n.i / S.nodes.length) * Math.PI * 2, rad = 140 + (hash(n.cb) % 220);\n        n.x = S.w / 2 + Math.cos(a) * rad; n.y = S.h / 2 + Math.sin(a) * rad;\n      });\n\n      DIMS.forEach(d => S.W[d.k] = weightOf(d.k));\n      rebuild();\n      $('#count').textContent = d2.fmt.number(S.nodes.length);\n      say((demo ? '⚠ AMOSTRA SINTÉTICA (dadosDemo) — ' : '') + `${S.nodes.length} CBs · ${S.edges.length} laços · ${S.nodes.filter(n => n.ativa).length} ativas no ciclo`, demo);\n      renderDetail(null);\n    }\n\n    // ---------- afinidades ----------\n    function keyOf(k, n) {\n      if (k === 'origem') return n.municipio ? n.uf + '/' + n.municipio : null;\n      if (k === 'territorio') return n.setor || null;\n      if (k === 'rede') return n.grupo || null;\n      if (k === 'trajetoria') return n.nivel || null;\n      if (k === 'ritmo') return 'r' + n.bRitmo;\n      if (k === 'alcance') return 'a' + n.bAlcance;\n      if (k === 'porte') return 'p' + n.bPorte;\n      if (k === 'marca') return n.bMarca == null ? null : 'm' + n.bMarca;\n      if (k === 'constancia') return n.ativa ? 'ativa' : null;\n      return null;\n    }\n    // Peso = raridade. p = chance de dois nós sorteados caírem no mesmo grupo desta dimensão;\n    // log2(1/p) transforma isso em bits de informação, então \"mesma líder\" pesa muito mais que\n    // \"ambas ativas\" sem que ninguém precise arbitrar uma tabela de pesos à mão.\n    function weightOf(k) {\n      const g = {}, n = S.nodes.length;\n      S.nodes.forEach(x => { const key = keyOf(k, x); if (key != null) g[key] = (g[key] || 0) + 1; });\n      let p = 0; Object.values(g).forEach(c => { p += (c / n) * (c / n); });\n      return p > 0 ? Math.max(.35, Math.min(6, Math.log2(1 / p))) : 0;\n    }\n    function sharedDims(a, b) {\n      const out = [];\n      for (const d of DIMS) {\n        if (!S.on[d.k]) continue;\n        const ka = keyOf(d.k, a); if (ka == null) continue;\n        if (ka === keyOf(d.k, b)) out.push(d.k);\n      }\n      return out;\n    }\n    function rebuild() {\n      const ns = S.nodes, K = Math.max(1, Math.min(8, +d2.param('lacos', 4) || 4));\n      const cand = ns.map(() => []);\n      for (let i = 0; i < ns.length; i++) {\n        for (let j = i + 1; j < ns.length; j++) {\n          const dims = sharedDims(ns[i], ns[j]);\n          if (!dims.length) continue;\n          let sc = 0; for (const k of dims) sc += S.W[k] || 0;\n          if (sc <= 0) continue;\n          cand[i].push({ o: j, sc, dims }); cand[j].push({ o: i, sc, dims });\n        }\n      }\n      // Corte por nó, não global: um corte global deixaria as CBs de município grande com\n      // dezenas de laços e as demais órfãs — cada uma guarda seus K vínculos mais fortes.\n      const seen = new Set(), out = [];\n      cand.forEach((list, i) => {\n        list.sort((a, b) => b.sc - a.sc).slice(0, K).forEach(e => {\n          const a = Math.min(i, e.o), b = Math.max(i, e.o), key = a + ':' + b;\n          if (seen.has(key)) return; seen.add(key);\n          const top = e.dims.slice().sort((x, y) => (S.W[y] || 0) - (S.W[x] || 0))[0];\n          out.push({ a, b, sc: e.sc, dims: e.dims, top });\n        });\n      });\n      S.edges = out;\n      S.adj = new Map();\n      out.forEach((e, idx) => {\n        if (!S.adj.has(e.a)) S.adj.set(e.a, []);\n        if (!S.adj.has(e.b)) S.adj.set(e.b, []);\n        S.adj.get(e.a).push(idx); S.adj.get(e.b).push(idx);\n      });\n      const maxSc = Math.max(...out.map(e => e.sc), 1);\n      out.forEach(e => e.n = e.sc / maxSc);\n      const counts = {}; DIMS.forEach(d => counts[d.k] = 0);\n      out.forEach(e => e.dims.forEach(k => counts[k]++));\n      DIMS.forEach(d => { const el = root.querySelector(`[data-n=\"${d.k}\"]`); if (el) el.textContent = counts[d.k] ? counts[d.k] : ''; });\n      S.alpha = 1;\n    }\n\n    // ---------- física ----------\n    function tick() {\n      const ns = S.nodes, cx = S.w / 2, cy = S.h / 2;\n      if (S.alpha < .02 || !ns.length) return;\n      const k = S.alpha;\n      for (let i = 0; i < ns.length; i++) {\n        const a = ns[i];\n        for (let j = i + 1; j < ns.length; j++) {\n          const b = ns[j];\n          let dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);\n          if (d < .01) {   // coincidentes: desempata com um deslocamento determinístico\n            dx = ((hash(a.cb + b.cb) % 21) - 10) / 10;\n            dy = ((hash(b.cb + a.cb) % 21) - 10) / 10;\n            d = Math.hypot(dx, dy) || 1;\n          }\n          const ux = dx / d, uy = dy / d;\n          const rep = Math.min(4, 9000 / (d * d)) * k;\n          a.vx -= ux * rep; a.vy -= uy * rep;\n          b.vx += ux * rep; b.vy += uy * rep;\n          // Colisão dura, resolvida na POSIÇÃO e não na velocidade: só repulsão deixa os círculos\n          // se sobreporem enquanto o sistema esfria, e as iniciais viram sopa.\n          const min = a.r + b.r + 6;\n          if (d < min) {\n            const push = (min - d) * .5;\n            a.x -= ux * push; a.y -= uy * push;\n            b.x += ux * push; b.y += uy * push;\n          }\n        }\n      }\n      for (const e of S.edges) {\n        const a = ns[e.a], b = ns[e.b];\n        const dx = b.x - a.x, dy = b.y - a.y, d = Math.max(.01, Math.hypot(dx, dy));\n        const target = 170 - 95 * e.n;          // laço forte = mais curto\n        const f = (d - target) * .022 * (.4 + .6 * e.n) * k;\n        const ux = dx / d, uy = dy / d;\n        a.vx += ux * f; a.vy += uy * f;\n        b.vx -= ux * f; b.vy -= uy * f;\n      }\n      for (const n of ns) {\n        if (S.drag === n) { n.vx = 0; n.vy = 0; continue; }\n        n.vx += (cx - n.x) * .002 * k; n.vy += (cy - n.y) * .002 * k;\n        n.vx *= .84; n.vy *= .84;\n        n.x += Math.max(-9, Math.min(9, n.vx));\n        n.y += Math.max(-9, Math.min(9, n.vy));\n      }\n      S.alpha *= .994;\n    }\n\n    // Enquadramento roda TODO frame, fora do laço de física: se ficasse dentro, congelaria junto\n    // com o alpha e o grafo terminaria transbordando o palco (ou torto) exatamente onde parou.\n    // Também é o que reenquadra depois de um resize, sem precisar reaquecer a simulação.\n    function fit() {\n      const ns = S.nodes;\n      if (!ns.length || S.drag) return;\n      let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n      for (const n of ns) {\n        x0 = Math.min(x0, n.x - n.r); y0 = Math.min(y0, n.y - n.r);\n        x1 = Math.max(x1, n.x + n.r); y1 = Math.max(y1, n.y + n.r);\n      }\n      const cx = S.w / 2, cy = S.h / 2;\n      const ox = cx - (x0 + x1) / 2, oy = cy - (y0 + y1) / 2;\n      if (Math.abs(ox) > .4 || Math.abs(oy) > .4) for (const n of ns) { n.x += ox * .12; n.y += oy * .12; }\n      const sc = Math.min(1, (S.w - 56) / Math.max(1, x1 - x0), (S.h - 56) / Math.max(1, y1 - y0));\n      if (sc < .999) for (const n of ns) {\n        n.x = cx + (n.x - cx) * (1 - (1 - sc) * .12);\n        n.y = cy + (n.y - cy) * (1 - (1 - sc) * .12);\n      }\n    }\n\n    // ---------- desenho ----------\n    function draw() {\n      ctx.clearRect(0, 0, S.w, S.h);\n      ctx.fillStyle = BG; ctx.fillRect(0, 0, S.w, S.h);\n      if (!S.nodes.length) return;\n      const focus = S.sel != null ? S.sel : S.hover;\n      const near = new Set();\n      if (focus != null) { near.add(focus); (S.adj.get(focus) || []).forEach(i => { near.add(S.edges[i].a); near.add(S.edges[i].b); }); }\n\n      S.edges.forEach((e, idx) => {\n        const a = S.nodes[e.a], b = S.nodes[e.b];\n        const lit = focus == null || e.a === focus || e.b === focus;\n        const col = (DIM_BY[e.top] || DIMS[0]).color;\n        ctx.globalAlpha = focus == null ? .16 + .34 * e.n : (lit ? .5 + .4 * e.n : .05);\n        ctx.strokeStyle = col;\n        ctx.lineWidth = .5 + 1.7 * e.n;\n        ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();\n        // uma conta por dimensão compartilhada, no meio do laço — é a leitura rápida de\n        // \"quantas coisas estas duas têm em comum\" sem precisar do hover\n        const dims = e.dims;\n        for (let i = 0; i < dims.length; i++) {\n          const t = (i + 1) / (dims.length + 1);\n          ctx.globalAlpha = focus == null ? .55 : (lit ? .95 : .08);\n          ctx.fillStyle = (DIM_BY[dims[i]] || DIMS[0]).color;\n          ctx.beginPath(); ctx.arc(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, 1.9 + 1.3 * e.n, 0, 6.284); ctx.fill();\n        }\n      });\n\n      S.nodes.forEach(n => {\n        const dim = focus != null && !near.has(n.i);\n        ctx.globalAlpha = dim ? .18 : 1;\n        ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, 6.284);\n        ctx.fillStyle = n.tint; ctx.fill();\n        ctx.lineWidth = n.i === focus ? 2.5 : 1;\n        ctx.strokeStyle = n.i === focus ? '#007aff' : (n.ativa ? 'rgba(60,60,67,.38)' : 'rgba(60,60,67,.14)');\n        ctx.stroke();\n        if (n.r >= 12) {   // nós pequenos ficam como pontos, sem inicial ilegível dentro\n          ctx.globalAlpha = dim ? .2 : .85;\n          ctx.fillStyle = '#3a3a3c';\n          ctx.font = `600 ${Math.round(n.r * .78)}px -apple-system,\"Segoe UI\",Roboto,sans-serif`;\n          ctx.textAlign = 'center'; ctx.textBaseline = 'middle';\n          ctx.fillText(n.ini, n.x, n.y + .5);\n        }\n      });\n      ctx.globalAlpha = 1;\n    }\n\n    d2.raf(() => { tick(); fit(); draw(); });\n\n    // ---------- interação ----------\n    function hit(px, py) {\n      let best = null, bd = Infinity;\n      for (const n of S.nodes) {\n        const d = Math.hypot(n.x - px, n.y - py);\n        if (d < n.r + 5 && d < bd) { bd = d; best = n; }\n      }\n      return best;\n    }\n    const rel = ev => { const r = cv.getBoundingClientRect(); return [ev.clientX - r.left, ev.clientY - r.top]; };\n\n    d2.on(cv, 'pointermove', ev => {\n      const [x, y] = rel(ev);\n      if (S.drag) { S.drag.x = x; S.drag.y = y; S.alpha = Math.max(S.alpha, .35); return; }\n      const n = hit(x, y);\n      S.hover = n ? n.i : null;\n      cv.style.cursor = n ? 'pointer' : 'default';\n      if (n) {\n        tip.classList.add('on');\n        tip.innerHTML = `<b>${esc(n.nome || 'CB ' + n.cb)}</b><em>${esc(n.municipio)}${n.uf ? '/' + esc(n.uf) : ''} · setor ${esc(n.setor)} · ${d2.fmt.number(n.pontos)} pontos</em>`;\n        const tw = tip.offsetWidth || 200;\n        tip.style.left = Math.max(6, Math.min(S.w - tw - 6, x + 14)) + 'px';\n        tip.style.top = Math.max(6, y - 42) + 'px';\n      } else tip.classList.remove('on');\n    });\n    d2.on(cv, 'pointerdown', ev => {\n      const [x, y] = rel(ev); const n = hit(x, y);\n      if (n) { S.drag = n; cv.setPointerCapture(ev.pointerId); }\n    });\n    d2.on(cv, 'pointerup', ev => {\n      const [x, y] = rel(ev); const n = hit(x, y);\n      S.drag = null;\n      if (n) { S.sel = (S.sel === n.i ? null : n.i); renderDetail(S.sel); }\n      else { S.sel = null; renderDetail(null); }\n    });\n    d2.on(cv, 'pointerleave', () => { S.hover = null; S.drag = null; tip.classList.remove('on'); });\n\n    d2.on('.chip', 'click', (e, el) => {\n      const k = el.dataset.k;\n      S.on[k] = !S.on[k];\n      el.classList.toggle('off', !S.on[k]);\n      if (S.nodes.length) { rebuild(); renderDetail(S.sel); }\n    });\n    d2.on('.crow', 'click', (e, el) => { S.sel = +el.dataset.i; renderDetail(S.sel); });\n    d2.on('#reload', 'click', (e, el) => {\n      el.disabled = true;\n      S.sel = null; S.hover = null;\n      load().catch(err => say(err && err.message ? err.message : String(err), true))\n        .then(() => { el.disabled = false; });\n    });\n\n    // ---------- painel ----------\n    function renderDetail(i) {\n      const el = $('#detail');\n      if (i == null || !S.nodes[i]) {\n        const on = DIMS.filter(d => S.on[d.k]);\n        el.innerHTML = `<div class=\"hintline\">Cada laço é uma afinidade calculada, não um cadastro: duas CBs se ligam quando compartilham origem, território, líder, trajetória ou um padrão de trabalho parecido.<br><br>O peso de cada dimensão é o inverso da sua frequência — por isso <b>${esc((on.slice().sort((a, b) => (S.W[b.k] || 0) - (S.W[a.k] || 0))[0] || {}).label || '—')}</b> puxa mais que as demais nesta amostra.<br><br>Clique num nó para ver quem ele alcança. Arraste para reorganizar.</div>`;\n        return;\n      }\n      const n = S.nodes[i];\n      const mine = (S.adj.get(i) || []).map(x => S.edges[x]).sort((a, b) => b.sc - a.sc).slice(0, 8);\n      const fact = (v, l) => `<div class=\"fact\"><b>${v}</b><span>${l}</span></div>`;        el.innerHTML = `<div>\n        <h3>${esc(n.nome || 'CB ' + n.cb)}</h3>\n        <div class=\"sub\">${esc(n.municipio)}${n.uf ? ' · ' + esc(n.uf) : ''} · setor ${esc(n.setor)}${n.lider ? ' · líder ' + esc(n.lider) : ''}</div>\n        <div class=\"facts\">\n          ${fact(d2.fmt.number(n.pontos), 'pontos')}\n          ${fact(d2.fmt.number(n.pedidos), 'pedidos')}\n          ${fact(d2.fmt.currency(n.receita, 'BRL'), 'receita')}\n          ${fact(esc(n.nivel || '—'), 'nível')}\n        </div>\n        <div class=\"conn\"><h4>${mine.length} afinidades mais fortes</h4>\n          ${mine.map(e => {\n            const o = S.nodes[e.a === i ? e.b : e.a];\n            return `<div class=\"crow\" data-i=\"${o.i}\" title=\"${esc(e.dims.map(k => (DIM_BY[k] || {}).hint).join(' · '))}\">\n              <span class=\"cdot\" style=\"background:${o.tint}\">${esc(o.ini)}</span>\n              <span class=\"cname\">${esc(o.nome || 'CB ' + o.cb)}</span>\n              <span class=\"tags\">${e.dims.map(k => `<i class=\"tag\" style=\"background:${(DIM_BY[k] || {}).color}\"></i>`).join('')}</span>\n            </div>`;\n          }).join('')}\n        </div>\n      </div>`;\n    }\n\n    load().catch(err => {\n      say(err && err.message ? err.message : String(err), true);\n      $('#detail').innerHTML = `<div class=\"hintline\">A consulta roda no listener do Databricks, que exige sessão do simulador. Se a mensagem acima fala em 401/permissão, é preciso estar logado no app.</div>`;\n    });\n  }\n};"}},"limite":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"limite"}}}},"setor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"setor"}}}},"uf":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"uf"}}}},"amostra":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"amostra"}}}},"lacos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"lacos"}}}},"consultaRush":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"consultaRush"}}}}}}}}}},{"_id":"6a7a38bbc9940e37623ae676","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"bsaEubO9EjIykK7MU-EnV","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst PALETTE=['#5856d6','#ff9500','#34c759','#af52de','#ff2d55','#a2845e'];\nconst CLOSE_COLOR='#8e8e93';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\nconst METRICS_DEF=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\nconst OPT_K=1.5;\nconst SESSIONS_DS='timeline-shared-sessionsDataSet';   // same table as SHARED_DS — see notes at listSessions()\n// Span defaults are the real operating band for a setor receptor, not placeholders: 2200 ≤ span ≤ 3500\n// with 2700 as the target. null meant \"sem limite\", so every new session started with the optimiser\n// balancing against nothing. Existing sessions are NOT touched — their persisted settings carry explicit\n// values that spread OVER these in openOptions/applyOptions, which is deliberate: their etapas were\n// computed under the old band and changing it silently would leave the stored result and the rule that\n// produced it out of sync.\nconst DEFAULT_SETTINGS={destRule:'any',destMinCbs:9,targetSpan:2700,maxSpan:3500,minSpan:2200};\nconst DOM_MEDIA_BASE='https://natura-geomarketing.data2.media/file/';\nconst DOM_CSV_FALLBACK=DOM_MEDIA_BASE+'yw_TQdp0QXEk6JsrliAqP'; \nconst numOrNull=v=>{const n=Math.round(+v);return (isFinite(n)&&n>0)?n:null;};\nconst spanShort=st=>{const p=[];if(st&&st.targetSpan)p.push('alvo '+st.targetSpan);if(st&&st.maxSpan)p.push('máx '+st.maxSpan);if(st&&st.minSpan)p.push('mín '+st.minSpan);return p.length?p.join(' · '):'livre';};\nconst ruleShort=st=>((st&&st.destRule)==='dominant')?('dominantes ≥'+((st&&st.destMinCbs)||DEFAULT_SETTINGS.destMinCbs)+' CBs'):'qualquer municipio';\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#34c759}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .mac-spacer{flex:1}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  .pill.live{color:var(--ink);cursor:pointer;user-select:none;transition:border-color .1s,color .1s,background .1s}\n  .pill.live:hover{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  .pill.live:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .pill.live.busy{border-color:var(--acc);color:var(--acc);cursor:progress}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:5.5px 9px}\n  select:focus,input:focus{outline:2px solid rgba(0,122,255,.35);outline-offset:0;border-color:var(--acc)}\n  button.act{background:var(--acc);color:#fff;border:1px solid transparent;border-radius:7px;padding:6px 13px;font-weight:600;cursor:pointer;box-shadow:0 1px 1px rgba(0,0,0,.08)}\n  button.act:hover{background:var(--acc2)}\n  button.ghost{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5.5px 12px;cursor:pointer}\n  button.ghost:hover{background:var(--panel2)}\n  button.ghost.on{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  button.ghost:disabled,button.act:disabled{opacity:.4;cursor:not-allowed}\n  .sessbar{padding:9px 20px;background:var(--panel2);border-bottom:1px solid var(--line);display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n  .rail{padding:9px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:center;overflow-x:auto}\n  #movementChips{display:flex;gap:8px;flex:0 0 auto}\n  #addMovementGroup{display:flex;gap:8px;align-items:center;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--line)}\n  #addMovementGroup input{min-width:150px}\n  .tchip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px;flex:0 0 auto}\n  .tchip.on{border-color:var(--acc);background:#eaf3ff}\n  .tchip.stale{border-style:dashed;opacity:.85}\n  .tchip .tstale{color:#ff9500;font-weight:700}\n  .tchip .tnum{background:var(--ink);color:#fff;border-radius:99px;width:16px;height:16px;font-size:9.5px;display:flex;align-items:center;justify-content:center;font-weight:700}\n  .tchip .tsec{font-weight:600}\n  .tchip .tstat{color:var(--mut)}\n  .tchip .trm{color:var(--mut);font-weight:700;padding:0 3px}\n  .tchip .trm:hover{color:var(--closing)}\n  .rail .empty{color:var(--mut);font-size:12px}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 16px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:0 0 9px;font-weight:600}\n  /* user-select, not pointer-events. .lbl already sets pointer-events:none, which stops the text from\n     RECEIVING clicks but does not exclude it from the browser's selection range — so cycling a\n     município's destination with repeated clicks kept dragging a double-click selection across the\n     labels. Nothing drawn in an svg here is text anyone needs to copy. */\n  svg{width:100%;height:auto;display:block;user-select:none;-webkit-user-select:none}\n  /* White hairlines inside the closing sector: the municipios read as one territory cut into parts\n     rather than as separate outlined units, and the saturated fills carry the grouping instead of the\n     ink. Neighbour sectors keep their own coloured strokes (.sect) — that contrast is what separates\n     what is being reallocated from what is only context. Selection no longer draws an outline; the\n     white border was the click affordance AND the default is now white, so it had nothing left to say. */\n  .muni{stroke:#fff;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.15)}\n  .sect{stroke-width:1;fill-opacity:.32;stroke-opacity:.6}\n  .sect.plain{fill:#e8e8ec;stroke:#d4d4d9;fill-opacity:1;stroke-opacity:1}\n  .sect.closed-ctx{fill:#e2e2e7;stroke:#aeaeb2;stroke-dasharray:3 2;fill-opacity:1;stroke-opacity:1}\n  .lbl{pointer-events:none}\n  /* No plate behind the text: a halo painted UNDER the glyphs (paint-order) keeps a name legible on any\n     fill without stamping an opaque box over the territory it names. Ink and halo are chosen per label\n     from the luminance of the fill underneath, so they are deliberately NOT declared here — a\n     stylesheet rule outranks a presentation attribute and would silently override that choice. */\n  .lbl text{text-anchor:middle;paint-order:stroke fill;stroke-width:2.2px;stroke-linejoin:round;stroke-linecap:round}\n  .lbl.big text{stroke-width:2.9px}\n  .lbl line.lead{stroke-opacity:.5;stroke-width:.8}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:19px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#ffd1cc}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bdeecb}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:600;font-size:12px}\n  .bar-track{flex:1;height:18px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex;position:relative}\n  .bar-mark{position:absolute;top:0;bottom:0;width:2px;opacity:.9;z-index:2}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  .toggle{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:12px}\n  .toggle button{background:transparent;border:0;color:var(--mut);padding:6px 10px;cursor:pointer;font-weight:600}\n  .toggle button.on{background:var(--panel2);color:var(--ink)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .scroll{max-height:240px;overflow:auto}\n  /* Tables meant to be READ in full rather than sampled. Only the vertical cap is dropped — overflow\n     stays auto so the wide grupo grid can still scroll sideways inside its card, which is the one axis\n     that genuinely cannot fit. */\n  .scroll.grow{max-height:none}\n  .tip{position:fixed;pointer-events:none;background:#1c1c1e;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}\n  .tabs button{background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600}\n  .tabs button.on{background:var(--panel2);color:var(--ink);border-color:var(--acc)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(245,245,247,.85);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:20;font-size:13px;color:var(--acc);font-weight:600;text-align:center;padding:20px}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n  .sheet-bg{position:fixed;inset:0;background:rgba(28,28,30,.34);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:40;padding:24px}\n  .sheet-bg.hidden{display:none}\n  .sheet{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.22);width:min(580px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden}\n  .sheet-hd{display:flex;align-items:center;gap:9px;padding:12px 16px;border-bottom:1px solid var(--line)}\n  .sheet-hd h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-.1px}\n  .sheet-x{background:transparent;border:0;color:var(--mut);font-size:13px;cursor:pointer;padding:3px 7px;border-radius:6px}\n  .sheet-x:hover{background:var(--panel2);color:var(--ink)}\n  .sheet-bd{padding:14px 16px;overflow:auto}\n  .sheet-ft{display:flex;align-items:center;gap:8px;padding:11px 16px;border-top:1px solid var(--line);background:var(--panel2)}\n  .opt-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600}\n  .opt-sub{font-size:11.5px;color:var(--mut);margin:4px 0 10px;line-height:1.5}\n  .opt-row{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;cursor:pointer;background:var(--panel)}\n  .opt-row:hover{background:var(--panel2)}\n  .opt-row.on{border-color:var(--acc);background:#eaf3ff}\n  .opt-row input[type=radio]{margin:2px 0 0;accent-color:var(--acc);flex:0 0 auto}\n  .opt-t{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n  .opt-d{font-size:11.5px;color:var(--mut);margin-top:3px;line-height:1.55}\n  .opt-inline{font-size:11.5px;color:var(--mut);margin-top:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}\n  .opt-inline.off{opacity:.4;pointer-events:none}\n  .opt-note{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:9px 11px;font-size:11.5px;color:var(--mut);line-height:1.6;margin-top:12px}\n  .opt-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:2px}\n  .opt-fld{display:flex;flex-direction:column;gap:5px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;background:var(--panel2)}\n  .opt-fld span{font-size:11px;color:var(--mut);font-weight:600;line-height:1.35}\n  .opt-fld i{font-style:normal;font-weight:400;opacity:.72}\n  .opt-fld input{width:100%;background:var(--panel)}\n  .opt-err{margin-top:9px;font-size:11.5px;color:var(--closing);font-weight:600}\n  .opt-hr{height:1px;background:var(--line);margin:17px 0 0}\n  .shareurl{margin-top:9px;padding:9px 10px;border-radius:8px;background:var(--panel);border:1px solid var(--acc);\n    font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--ink);word-break:break-all;user-select:all}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header id=\"topHeader\" class=\"hidden\">\n  <h1>Simulador de Timeline de Fechamentos</h1>\n  <span class=\"pill live\" id=\"liveState\" role=\"button\" tabindex=\"0\" title=\"Clique pra buscar o snapshot mais recente e recalcular a timeline inteira\">● iniciando</span>\n  <span class=\"pill ro\">read-only · nunca escreve</span>\n  <span class=\"pill\" id=\"basePill\"></span>\n  <span class=\"pill\" id=\"rulePill\"></span>\n  <span class=\"pill\" id=\"spanPill\"></span>\n  <span class=\"status\" id=\"ds\"></span>\n  <div class=\"mac-spacer\"></div>\n  <select id=\"sessionPick\" style=\"min-width:200px\"></select>\n  <button class=\"ghost\" id=\"openOptions\" title=\"Configurações desta sessão\">⚙ Opções</button>\n  <button class=\"act\" id=\"saveSession\">Salvar sessão</button>\n  <button class=\"ghost\" id=\"dupSession\" title=\"Cria uma cópia independente desta sessão — a original fica intacta e a cópia passa a ser a sessão ativa\">⧉ Duplicar</button>\n  <button class=\"ghost\" id=\"openShare\" title=\"Publicar uma cópia congelada desta sessão em modo leitura\">⇪ Compartilhar</button>\n  <span class=\"status\" id=\"sessStatus\"></span>\n</header>\n\n<div class=\"rail hidden\" id=\"timelineRail\">\n  <div id=\"movementChips\"></div>\n  <div id=\"addMovementGroup\">\n    <input id=\"sectorPick\" list=\"sectorPickList\" placeholder=\"código do setor\" style=\"min-width:150px\">\n    <datalist id=\"sectorPickList\"></datalist>\n    <button class=\"ghost\" id=\"rand\" title=\"Setor aleatório\">🎲</button>\n    <button class=\"act\" id=\"go\">Fechar setor ▶ (nova etapa)</button>\n  </div>\n</div>\n\n<div class=\"card\" id=\"confirmCard\" style=\"margin:14px 20px\">\n  <h2 style=\"margin:0 0 9px\">Confirmar snapshot antes de simular</h2>\n  <div class=\"note\" id=\"confirmBody\">congelando snapshot…</div>\n  <button class=\"act\" id=\"confirmSnap\" style=\"margin-top:12px\">Confirmar snapshot ▶ ir pra simulação</button>\n  <div class=\"note\" style=\"margin-top:16px\">ou continue uma sessão salva:</div>\n  <select id=\"confirmSessionPick\" style=\"min-width:260px;margin-top:4px\"></select>\n  <div class=\"status\" id=\"confirmStatus\" style=\"margin-top:8px\"></div>\n</div>\n\n<div class=\"howto hidden\" id=\"howtoBox\">\n  <b>Como usar &nbsp;→&nbsp;</b>\n  <span><b>1.</b> Escolha um setor pra <b>fechar</b> (ou 🎲) — isso cria a etapa 1 da timeline.</span>\n  <span><b>2.</b> Feche outro setor pra criar a etapa 2 — ela já considera pra onde a etapa 1 mandou cada município.</span>\n  <span><b>3.</b> Clique numa etapa (chip acima) pra revisitar/editar — editar uma etapa recalcula automaticamente todas as etapas seguintes.</span>\n  <span><b>4.</b> Veja o efeito acumulado da cadeia inteira na aba <b>Impacto acumulado</b>.</span>\n</div>\n<div class=\"wrap hidden\" id=\"mainWrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <h2 id=\"mapTitle\" style=\"margin:0\">Mapa — clique num município p/ trocar o setor destino</h2>\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura do setor antes desta etapa (já considerando etapas anteriores) · TOBE = após a redistribuição desta etapa\">\n          <button data-v=\"asis\">◧ ASIS (antes desta etapa)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado nesta etapa (cor = destino). Ao redor, cada área é um <b>setor vizinho inteiro</b> com o código no meio — não municípios soltos: cor = setor destino, cinza = contexto, cinza tracejado = setor já fechado numa etapa anterior. Shapes reais IBGE/tbrugz.</div>\n      <div class=\"note\" id=\"mapIgnoredNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas (nesta etapa)</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor.</div>\n      <div class=\"scroll grow\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"tobe\">Matriz TOBE</button>\n        <button data-tab=\"report\">Relatório de impacto (etapa)</button>\n        <button data-tab=\"cumulative\">Impacto acumulado (timeline)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll grow\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-tobe\" class=\"hidden\"><div class=\"scroll grow\"><table id=\"matrixTobe\"></table></div>\n        <div class=\"note\" id=\"tobeNote\"></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-cumulative\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"note\" style=\"margin:-4px 0 8px\">Roda contra o snapshot congelado desta sessão (não \"ao vivo\") — mesma coerência da timeline.</div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa todas alocações contíguas e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (contíguo; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Editar esta etapa recalcula automaticamente as etapas seguintes da timeline.</div>\n      <div class=\"note\" id=\"ruleNote\" style=\"margin:-6px 0 11px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px\"></div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <div id=\"spanRule\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Municípios → setor destino</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração (etapa selecionada)</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"shareBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"shareTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"shareTitle\">Compartilhar sessão</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"shareX\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\" id=\"shareBody\"></div>\n    <div class=\"sheet-ft\">\n      <span class=\"status\" id=\"shareStatus\"></span>\n      <div class=\"mac-spacer\"></div>\n      <button class=\"ghost hidden\" id=\"shareCopy\">Copiar link</button>\n      <button class=\"ghost hidden\" id=\"shareOpen\">Abrir</button>\n      <button class=\"ghost\" id=\"shareCancel\">Cancelar</button>\n      <button class=\"act\" id=\"shareGo\">Compartilhar</button>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"optionsBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"optTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"optTitle\">Opções da sessão</h3><span class=\"badge\" id=\"optSessName\"></span><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"optClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Regra rígida — seleção dos setores destino</div>\n      <div class=\"opt-sub\">Define quais setores vizinhos podem entrar na lista de destinos de cada etapa. Vale pra toda a timeline desta sessão.</div>\n      <label class=\"opt-row\" data-r=\"any\">\n        <input type=\"radio\" name=\"destRule\" value=\"any\">\n        <div>\n          <div class=\"opt-t\">Adjacente a qualquer município <span class=\"badge\">padrão</span></div>\n          <div class=\"opt-d\">Um setor entra na lista de destinos se tiver município vizinho a <b>qualquer</b> município do setor fechado — inclusive os menores. É o comportamento de hoje.</div>\n        </div>\n      </label>\n      <label class=\"opt-row\" data-r=\"dominant\">\n        <input type=\"radio\" name=\"destRule\" value=\"dominant\">\n        <div>\n          <div class=\"opt-t\">Adjacente a municípios dominantes</div>\n          <div class=\"opt-d\">Só os municípios do setor fechado com <b>CBs suficientes</b> valem como ponte. Os pequenos continuam sendo redistribuídos normalmente — só não trazem setor novo pra lista de destinos.</div>\n          <div class=\"opt-inline\" id=\"optMinCbsWrap\">Ignorar municípios com menos de <input type=\"number\" id=\"optMinCbs\" min=\"1\" max=\"9999\" step=\"1\" style=\"width:74px\"> CBs</div>\n        </div>\n      </label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\">Pré-requisitos do setor destino — tamanho em CBs</div>\n      <div class=\"opt-sub\">Vale pro tamanho de cada setor receptor <b>depois</b> da redistribuição (base + entrantes). Em branco = desligado.</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhum setor acima)</i></span><input type=\"number\" id=\"optMaxSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhum setor abaixo)</i></span><input type=\"number\" id=\"optMinSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-err hidden\" id=\"optSpanErr\"></div>\n      <div class=\"opt-note\" id=\"optPreview\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"optStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"optCancel\">Cancelar</button><button class=\"act\" id=\"optApply\">Aplicar</button></div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div>\n<span id=\"ready\" style=\"display:none\"></span></div>`;\n  },\n  mounted(d2, root){\n\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\nconst blankM=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n// ---------- município identity ----------\n// Brazil has many municipalities sharing a name (Bom Jesus alone exists in PI, RN, RS, SC, PB, GO).\n// A stage loads the dominant UF PLUS every bordering UF, so those collisions land in the SAME\n// feature set routinely: keyed by name alone the last shape loaded won, the map could draw a\n// município from the wrong state, buildAdj merged two unrelated municipalities into one node, and\n// GROUP BY DS_MUNICIPIO summed two real municipalities into one row. The key is NAME/UF everywhere\n// — SQL rows, GeoJSON features, the dominantes base, assignments, persisted sessions. norm() maps\n// every non-alphanumeric to a space, so '/' can never occur inside a name and the split is exact.\n// A missing UF degrades to a bare NAME key rather than inventing one.\nconst mkey=(name,uf)=>{const n=norm(name),u=String(uf==null?'':uf).trim().toUpperCase();return u?n+'/'+u:n;};\nconst mName=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?s:s.slice(0,i);};\nconst mUf=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?'':s.slice(i+1);};\nconst mDisp=k=>{const u=mUf(k);return u?mName(k)+' ('+u+')':mName(k);};\n// The base CSV is the ONLY source of a município's UF now, so its column is accepted in whatever\n// shape the base is maintained in: sigla, IBGE numeric code, or full state name. Anything that does\n// not resolve to a real sigla is treated as UNKNOWN rather than passed through — a bogus UF would key\n// a município to a shape that does not exist and drop it off the map without a word.\nconst UF_OF_CODE={}; Object.keys(UF_CODE).forEach(k=>{ UF_OF_CODE[UF_CODE[k]]=k; });\nconst UF_OF_NAME={ACRE:'AC',ALAGOAS:'AL',AMAPA:'AP',AMAZONAS:'AM',BAHIA:'BA',CEARA:'CE','DISTRITO FEDERAL':'DF','ESPIRITO SANTO':'ES',GOIAS:'GO',MARANHAO:'MA','MATO GROSSO':'MT','MATO GROSSO DO SUL':'MS','MINAS GERAIS':'MG',PARA:'PA',PARAIBA:'PB',PARANA:'PR',PERNAMBUCO:'PE',PIAUI:'PI','RIO DE JANEIRO':'RJ','RIO GRANDE DO NORTE':'RN','RIO GRANDE DO SUL':'RS',RONDONIA:'RO',RORAIMA:'RR','SANTA CATARINA':'SC','SAO PAULO':'SP',SERGIPE:'SE',TOCANTINS:'TO'};\nconst ufNorm=v=>{const s=norm(v); if(!s) return '';\n  if(UF_CODE[s]) return s;\n  if(UF_OF_CODE[s]) return UF_OF_CODE[s];\n  return UF_OF_NAME[s]||'';};\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\nconst setError=(msg)=>{ const t=$('#ds'); if(t){t.textContent=msg;t.classList.add('err');} const c=$('#confirmStatus'); if(c){c.textContent=msg;c.classList.add('err');} };\n\n// ---------- base de municípios dominantes (CSV) ----------\n// WHICH municipalities belong to a sector is a business fact, not something to infer from CB volume:\n// it comes from a maintained CSV (one row per setor x município) linked through the `dominantesCsvUrl`\n// param. Anything the closing sector has CBs in but that the base does NOT assign to it is spillover:\n// not realocated, not drawn on the map, and its CBs follow their leader. Loaded once per URL and\n// cached for the whole session; a failure is thrown, never swallowed — a silent fallback here would\n// quietly change every number on the screen.\nconst DOM={url:null,promise:null,bySetor:null,ufBySetorMuni:null,rows:0,setores:0,hasUf:false,ufCol:null,badUf:0,clashes:0,headers:null,error:null};\nfunction parseCsv(text){\n  const out=[]; let row=[],cur='',q=false;\n  const push=()=>{ row.push(cur); cur=''; };\n  const endRow=()=>{ push(); if(row.length>1||row[0]!=='') out.push(row); row=[]; };\n  for(let i=0;i<text.length;i++){ const ch=text[i];\n    if(q){ if(ch==='\"'){ if(text[i+1]==='\"'){ cur+='\"'; i++; } else q=false; } else cur+=ch; }\n    else if(ch==='\"') q=true;\n    else if(ch===',') push();\n    else if(ch==='\\n') endRow();\n    else if(ch!=='\\r') cur+=ch;\n  }\n  if(cur!==''||row.length) endRow();\n  return out;\n}\n// `dominantesCsvUrl` is declared as a FileComponentUrlReference, so the instance points at the Drive\n// FILE, not at a URL literal — the Drive is policy-gated behind the app's auth strategy and a baked\n// string would be both stale and unauthorised. What the resolver hands back varies by where it is\n// resolved (URL string / file record / bare reference), so accept every shape instead of silently\n// reading undefined and falling through to the fallback.\n// The Drive is gated by the app's AuthenticationStrategy, so this read has to travel as the LOGGED-IN\n// USER — an anonymous CDN GET is exactly what the policy is there to refuse. `<system>.data2.media` is\n// only a cache in front of the SSP: on a cache miss it 302s to `<system>.data2apis.com`, which is where\n// the policy is actually evaluated.\n// COOKIES CANNOT CARRY THAT SESSION, which is why the previous `credentials:'include'` attempt could\n// never have worked: the app runs on <app>.<system>.data2.app and the SSP on <system>.data2apis.com —\n// cross-site — and the SSP answers `Access-Control-Allow-Origin: *` with no `Allow-Credentials`, so the\n// browser rejects a credentialed CORS read before it ever leaves the page. What the SSP actually reads\n// is `Authorization: Bearer <token>`: the runtime keeps that token in localStorage under\n// `__data2-session-<systemDomain>` (`__data2-session-data2` inside the builder preview) and replays it\n// on every dataset and rush call — same token, same header, same session, so the Drive policy sees the\n// logged-in user. Preflight passes because the SSP echoes whatever header the request asks for.\nfunction d2SessionToken(sysDomain){\n  const read=k=>{ try{ return (k&&window.localStorage.getItem(k))||''; }catch(e){ return ''; } };\n  return read(sysDomain?('__data2-session-'+sysDomain):'')||read('__data2-session-data2')||'';\n}\nfunction domCsvSystemDomain(url){\n  const m=/^https?:\\/\\/([^.\\/]+)\\.data2\\.(?:media|app)\\//i.exec(url)||/^https?:\\/\\/([^.\\/]+)\\.data2apis\\.com\\//i.exec(url);\n  return m?m[1]:'';\n}\nfunction domCsvCandidates(url){\n  const out=[];\n  const isMedia=/^https?:\\/\\/[^.\\/]+\\.data2\\.media\\//i.test(url);\n  const api=isMedia?url.replace(/^(https?:\\/\\/[^.\\/]+)\\.data2\\.media\\//i,'$1.data2apis.com/'):null;\n  const tok=d2SessionToken(domCsvSystemDomain(url));\n  const auth=tok?{authorization:'Bearer '+tok}:null;\n  // Authenticated origin read first — the only candidate a GATED Drive can actually answer.\n  if(api&&auth) out.push({url:api,headers:auth,label:'api+sessão'});\n  if(!api&&auth) out.push({url:url,headers:auth,label:'origem+sessão'});\n  // Anonymous last: only ever succeeds on an UNGATED Drive — or on a stale CDN cache hit, which is NOT\n  // proof the file is public (the origin keeps answering 401 once the edge entry expires).\n  if(api) out.push({url:api,headers:null,label:'api anônimo'});\n  out.push({url:url,headers:null,label:'origem anônima'});\n  return out;\n}\nfunction domCsvUrl(){\n  const pick=v=>{\n    if(!v) return '';\n    if(typeof v==='string') return v;\n    const q=v.params||{};\n    const ref=q.reference||v.reference;\n    return q.fileUrl||q.url||v.fileUrl||v.url||(ref?DOM_MEDIA_BASE+ref:'');\n  };\n  return String(pick(d2.param('dominantesCsvUrl'))||DOM_CSV_FALLBACK||'').trim();\n}\nasync function ensureDominantes(){\n  const url=domCsvUrl();\n  if(!url) throw new Error('base de municípios dominantes não configurada — ligue o arquivo CSV no parâmetro dominantesCsvUrl da instância do componente');\n  if(DOM.url===url && DOM.bySetor) return DOM.bySetor;\n  if(DOM.url===url && DOM.promise) return DOM.promise;\n  DOM.url=url; DOM.error=null;\n  DOM.promise=(async()=>{\n    // Self-diagnosing on purpose: every candidate reports its own outcome and the message names the\n    // session it was carrying. \"Unauthorized\" alone cannot distinguish \"not logged into THIS strategy\"\n    // from \"cookie not sent cross-site\" from \"param never bound, so it tried the fallback URL\" — and\n    // guessing between those from the outside costs a round trip each time.\n    let r=null; const tried=[];\n    for(const c of domCsvCandidates(url)){\n      let res=null;\n      try{ res=await fetch(c.url,{cache:'no-store',credentials:'omit',headers:c.headers||undefined}); }\n      catch(e){ tried.push(c.label+' → rede/CORS'); continue; }\n      if(res.ok){ r=res; break; }\n      tried.push(c.label+' → '+res.status);\n    }\n    if(!r){\n      let sess='sessão desconhecida';\n      try{\n        const a=d2.auth(AUTH_REF);\n        sess=(a&&a.isAuthed)?('logado como '+((a.user&&(a.user.email||a.user.nome))||'?')):('NÃO logado na estratégia '+AUTH_REF);\n      }catch(e){ sess='estratégia '+AUTH_REF+' não resolvida'; }\n      const tk=d2SessionToken(domCsvSystemDomain(url));\n      throw new Error('base de municípios dominantes: não foi possível ler o arquivo — '+sess+' · token de sessão '+(tk?'presente':'AUSENTE no localStorage')+'. Tentativas: '+tried.join(' · '));\n    }\n    const table=parseCsv(await r.text());\n    if(table.length<2) throw new Error('base de municípios dominantes vazia');\n    const head=table[0].map(h=>norm(h));\n    const iS=head.indexOf('CD SETOR'), iM=head.indexOf('MUNICIPIO');\n    if(iS<0||iM<0) throw new Error('base de municípios dominantes sem as colunas CD_SETOR / MUNICIPIO');\n    // CD_GV is the gerência de vendas the setor answers to, and it is the authority for the hard rule\n    // that a closing sector only ever hands territory to sectors of its OWN gerência. Its absence is\n    // fatal rather than a warning: with no GV there is no way to PROVE two sectors share one, and\n    // falling through unchecked is exactly the leak this column exists to close.\n    const iG=['CD GV','CDGV','GV','CD GERENCIA','CD GERENCIA VENDAS','CD GERENCIA DE VENDAS','GERENCIA DE VENDAS','GERENCIA'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iG==null) throw new Error('base de municípios dominantes sem a coluna CD_GV — a regra de gerência de vendas não pode ser aplicada. Colunas lidas: '+head.slice(0,40).join(', '));\n    // The UF column is what the whole keying now rests on — SQL no longer carries DS_ESTADO — so its\n    // absence is a first-class finding, not a footnote. The header actually matched is reported on the\n    // base pill, and when nothing matches the pill lists every column read, because guessing a header\n    // name from the outside costs a round trip each time.\n    const iU=['UF','SIGLA UF','SG UF','CD UF','ESTADO','DS ESTADO','SIGLA','ESTADO SIGLA','UF MUNICIPIO'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const by={},ufBy={},gvBy={}; let n=0,bad=0; const clash={},gvClash={};\n    for(let i=1;i<table.length;i++){\n      const line=table[i]; if(!line) continue;\n      const s=String(line[iS]==null?'':line[iS]).trim(), m=norm(line[iM]);\n      if(!s||!m) continue;\n      // One setor spans many rows (one per município) and every one of them repeats its CD_GV. Two\n      // DIFFERENT values for the same setor means the base itself disagrees about which gerência owns\n      // it — counted and reported, never keyed to whichever row happened to be read last.\n      const gvv=String(line[iG]==null?'':line[iG]).trim();\n      if(gvv){ if(gvBy[s]!==undefined && gvBy[s]!==gvv) gvClash[s]=true; else gvBy[s]=gvv; }\n      const raw=(iU==null)?'':line[iU];\n      const u=(iU==null)?'':ufNorm(raw);\n      if(iU!=null && String(raw==null?'':raw).trim() && !u) bad++;\n      // The same name twice in ONE setor under two UFs is the single case the reverted SQL cannot\n      // survive: mSQL groups by DS_MUNICIPIO alone, so both municipalities arrive summed into one row\n      // and no lookup can split them again. Counted and reported — never quietly keyed to whichever\n      // UF happened to be read last.\n      const kk=s+'|'+m;\n      // An UNRESOLVED uf must never outrank a resolved one. Reading a blank/bogus UF row first wrote ''\n      // here, and the next row carrying the real sigla was then rejected as a clash — so the key stayed\n      // falsy for good, ufFor() fell through, and the município degraded to a bare NAME key that matches\n      // no shape and no dominance entry. With DISTRITO/ZONA in the base a município spans several rows,\n      // so which row lands first is pure luck. A clash is only two RESOLVED siglas that disagree.\n      if(!u){ if(ufBy[kk]===undefined) ufBy[kk]=''; }\n      else if(!ufBy[kk]) ufBy[kk]=u;\n      else if(ufBy[kk]!==u) clash[kk]=true;\n      (by[s]=by[s]||new Set()).add(u?m+'/'+u:m); n++;\n    }\n    if(!n) throw new Error('base de municípios dominantes sem linhas válidas');\n    DOM.bySetor=by; DOM.ufBySetorMuni=ufBy; DOM.rows=n; DOM.setores=Object.keys(by).length;\n    DOM.gvBySetor=gvBy; DOM.gvClashes=Object.keys(gvClash).length; DOM.gvCol=String(table[0][iG]||'').trim();\n    DOM.gvSetores=Object.keys(gvBy).length;\n    DOM.hasUf=(iU!=null); DOM.ufCol=(iU==null?null:String(table[0][iU]||'').trim());\n    DOM.badUf=bad; DOM.clashes=Object.keys(clash).length; DOM.headers=head.slice(0,40); DOM.error=null;\n    return by;\n  })();\n  DOM.promise.catch(e=>{ DOM.promise=null; DOM.bySetor=null; DOM.error=e.message; renderAll(); });\n  return DOM.promise;\n}\n\nlet SESS={ snapshotRaw:null, snapshotSyncedAt:null, snapLiteral:null, eloMaxCycle:null, timeline:[], selected:0,\n           sessionId:null, sessionLabel:null, sessionsList:[], sectorsList:[], sectorBaseCache:{}, stage:'loading', dirtyFrom:null,\n           snapBusy:false, settings:{...DEFAULT_SETTINGS} };\n\nasync function q(sql){\n  let dbx,res;\n  try{\n    res=await d2.rush('AwW_11Nkg-dKksP7ic8V8').invoke({sql});\n  }catch(e){\n    throw new Error('Databricks indisponível (rush AwW_11Nkg-dKksP7ic8V8): '+e.message);\n  }\n  // The listener answers 200 with {sql} and NO `result` when its upstream probe times out: the\n  // handler builds `result: $result.data`, so an empty upstream simply drops the key rather than\n  // failing. Reading straight through to res.data.result.data reported \"Cannot read properties of\n  // undefined (reading 'data')\" — which reads like a bug in THIS component and sent a debugging\n  // session after the wrong thing. Name the layer that actually failed instead.\n  const payload=res&&res.data;\n  if(!payload) throw new Error('rush AwW_11Nkg-dKksP7ic8V8 não devolveu corpo — listener fora do ar?');\n  if(!payload.result) throw new Error('a sonda Databricks não respondeu: o listener AwW_11Nkg-dKksP7ic8V8 devolveu 200 sem `result`, o que normalmente é timeout do upstream (natura-x-app). O SQL não chegou a rodar.');\n  dbx=payload.result.data;\n  if(!dbx) throw new Error('sonda Databricks respondeu sem dados (result.data vazio).');\n  if(dbx.state && dbx.state!=='SUCCEEDED') throw new Error('Databricks '+dbx.state+': '+(dbx.error?.message||JSON.stringify(dbx.error)||'erro desconhecido'));\n  return dbx;\n}\nfunction rows(resp){const c=(resp.columns||[]).map(x=>x.name);\n  return (resp.rows||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\n// Stamps the UF onto every feature at load: the fetch is per-UF, so this is the ONLY point where a\n// shape's state is known for certain. Downstream the features are flattened into one array and the\n// origin would be unrecoverable. Persisted with the feature in saved sessions.\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);\n  const g=await r.json(); const U=String(uf).toUpperCase();\n  (g.features||[]).forEach(f=>{ f.__uf=U; });\n  return g;}\n\nfunction buildAdj(features){\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const feat={},idx={};\n  features.forEach(f=>{const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome,f.__uf);feat[nm]={f,pts:pts(f)};});\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  const nbrs=nm=>{const c={};feat[nm].pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    return Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));};\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\n// ---------- snapshot pinning ----------\nasync function resolveSnapshotPin(){\n  const [rawR,liveCountR,eloR]=await Promise.all([\n    q(`SELECT MAX(DT_HISTORICO) snap FROM ${T} WHERE CD_PAIS=1`).then(rows),\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`).then(rows),\n    q(`SELECT MAX(operational_cycle) mx FROM ${ELO} WHERE country_code=1`).then(rows).catch(()=>[]),\n  ]);\n  const raw=rawR[0]&&rawR[0].snap;\n  const liveCount=+((liveCountR[0]||{}).n||0);\n  const eloMax=+((eloR[0]||{}).mx||0);\n  const LIVE_SUBQ=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n  const dbTs=(raw||'').replace('T',' ').replace(/Z$/,'');\n  const candidates=[`TIMESTAMP'${dbTs}'`,`'${dbTs}'`,`DATE'${dbTs}'`];\n  const tests=await Promise.all(candidates.map(c=>\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=${c}`).then(rows).then(r=>+((r[0]||{}).n||0)).catch(()=>-1)\n  ));\n  let chosen=LIVE_SUBQ;\n  for(let i=0;i<candidates.length;i++){ if(liveCount>0 && tests[i]===liveCount){ chosen=candidates[i]; break; } }\n  return {snapshotRaw:raw, snapLiteral:chosen, eloMaxCycle:eloMax, syncedAt:new Date().toISOString()};\n}\n// DISPLAY CONTRACT: the UI never prints the snapshot's own DT_HISTORICO (snapshotRaw). What the user\n// sees is `snapshotSyncedAt` — the date AND hour at which THIS instance last pulled/pinned the\n// snapshot it is querying. snapshotRaw is still resolved, still drives every SQL literal, and is\n// still persisted by saveSession/doShare; it is only kept off the screen.\nfunction fmtSyncedAt(iso){\n  if(!iso) return '—';\n  const d=new Date(iso);\n  if(isNaN(d.getTime())) return String(iso);\n  return d.toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});\n}\nasync function ensurePinned(){\n  if(SESS.snapLiteral) return;\n  const p=await resolveSnapshotPin();\n  SESS.snapshotRaw=p.snapshotRaw; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n  SESS.snapshotSyncedAt=p.syncedAt;\n}\n\n// ---------- world-state fold ----------\nfunction accMetrics(base,add){\n  const out=base?{...base}:blankM();\n  Object.keys(blankM()).forEach(k=>{ out[k]=(out[k]||0)+((add&&add[k])||0); });\n  return out;\n}\nfunction foldWorldState(timeline,uptoIdx){\n  const closedSectors=new Set(),muniOverride={},muniGeo={},muniRow={},muniLevel={},sectorInflow={},sectorInflowLevel={},sectorOrphanRows={};\n  for(let k=0;k<uptoIdx;k++){\n    const mv=timeline[k]; if(!mv||!mv.sector||!mv.munis) continue;\n    closedSectors.add(String(mv.sector));\n    Object.keys(mv.munis).forEach(m=>{\n      if(m==='__OUTROS__') return;\n      const dest=mv.assign[m]; if(!dest) return;\n      muniOverride[m]=String(dest);\n      if(mv.geoFeat&&mv.geoFeat[m]) muniGeo[m]=mv.geoFeat[m];\n      const rws=(mv.matrix||[]).filter(r=>mkey(r.municipio,r.uf)===m);\n      muniRow[m]=(muniRow[m]||[]).concat(rws);\n      if(mv.levelByMuni&&mv.levelByMuni[m]) muniLevel[m]={...(muniLevel[m]||{}),...mv.levelByMuni[m]};\n      sectorInflow[dest]=accMetrics(sectorInflow[dest],mv.munis[m]);\n      const lv=(mv.levelByMuni&&mv.levelByMuni[m])||{};\n      sectorInflowLevel[dest]=sectorInflowLevel[dest]||{};\n      Object.entries(lv).forEach(([nv,c])=>{ sectorInflowLevel[dest][nv]=(sectorInflowLevel[dest][nv]||0)+c; });\n    });\n    // Orphan CBs follow their leader's home sector, so later etapas must inherit them the same way\n    // the allocated municipalities are inherited — otherwise a stage's span would not match the\n    // base carried into the next one.\n    if(mv.orphanMetricsByGroup && Object.keys(mv.orphanMetricsByGroup).length){\n      const d=computeDerived(mv);\n      d.leaders.forEach(L=>{\n        if(!L.home) return;\n        const om=mv.orphanMetricsByGroup[L.grupo];\n        if(om) sectorInflow[L.home]=accMetrics(sectorInflow[L.home],om);\n        // sectorInflow only reaches a sector while it is a DESTINATION (it lands in baseMetrics).\n        // Keep the rows as well, so the sector still carries these CBs on the day it is the one\n        // being closed — that read goes through mv.matrix, which baseMetrics never touches.\n        const orws=mv.orphanRowsByGroup&&mv.orphanRowsByGroup[L.grupo];\n        if(orws&&orws.length) sectorOrphanRows[L.home]=(sectorOrphanRows[L.home]||[]).concat(orws);\n        const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n        if(ol){ sectorInflowLevel[L.home]=sectorInflowLevel[L.home]||{};\n          Object.entries(ol).forEach(([nv,c])=>{ sectorInflowLevel[L.home][nv]=(sectorInflowLevel[L.home][nv]||0)+c; }); }\n      });\n    }\n  }\n  return {closedSectors,muniOverride,muniGeo,muniRow,muniLevel,sectorInflow,sectorInflowLevel,sectorOrphanRows};\n}\n\n// ---------- per-movement compute ----------\nfunction aggregateMovement(mv){\n  const munis={},leaders={};\n  const exSet=new Set((mv.excludedMunis||[]).map(x=>x.muni));\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  // Orphans = CBs in municipalities outside the sector's dominance base. They are NOT allocated\n  // geographically; they follow their own leader's home sector. Kept per leader group so every\n  // consumer (span, cascade, impact report) can route them the same way.\n  mv.orphanByGroup={}; mv.orphanMetricsByGroup={}; mv.orphanRowsByGroup={};\n  mv.matrix.forEach(r=>{\n    const m=mkey(r.municipio,r.uf);\n    r._out=!mv.geoFeat[m];   // assign, never just set: absorbed rows are reused across etapas\n    const mm=r._out?'__OUTROS__':m;\n    munis[mm]=munis[mm]||{...blankM(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n    if(r._out && exSet.has(m)){\n      mv.orphanByGroup[g]=(mv.orphanByGroup[g]||0)+ +r.cbs;\n      // The ROW too, not just the totals: when the leader's home sector is itself closed in a later\n      // etapa, that etapa rebuilds its matrix from SQL + absorbed municípios, and these CBs are in\n      // neither (their município never moved). Without the row they silently vanish from the sector\n      // they were just credited to — see sectorOrphanRows in foldWorldState.\n      (mv.orphanRowsByGroup[g]=mv.orphanRowsByGroup[g]||[]).push(r);\n      mv.orphanMetricsByGroup[g]=accMetrics(mv.orphanMetricsByGroup[g],\n        {cbs:+r.cbs,receita:+r.receita||0,rec_nat:+r.rec_nat||0,rec_avon:+r.rec_avon||0,pontos:+r.pontos||0,pedidos:+r.pedidos||0,ativas:+r.ativas||0});\n    }\n  });\n  // Level counts only exist per municipality, never per leader, so an excluded municipality's level\n  // mix is credited to the group holding most of its CBs (sub-threshold by definition, so usually one).\n  mv.orphanLevelByGroup={};\n  (mv.excludedMunis||[]).forEach(({muni})=>{\n    const lv=(mv.levelByMuni&&mv.levelByMuni[muni])||{}; if(!Object.keys(lv).length) return;\n    let bg=null,bc=-1;\n    mv.matrix.forEach(r=>{ if(mkey(r.municipio,r.uf)!==muni) return; const cc=+r.cbs; if(cc>bc){bc=cc;bg=String(r.grupo);} });\n    if(!bg) return;\n    mv.orphanLevelByGroup[bg]=mv.orphanLevelByGroup[bg]||{};\n    Object.entries(lv).forEach(([nv,cc])=>{ mv.orphanLevelByGroup[bg][nv]=(mv.orphanLevelByGroup[bg][nv]||0)+cc; });\n  });\n  mv.munis=munis; mv.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssignFor(mv){\n  const a={};\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=mv.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else if(mv.dest.length){ const c=mv.muniCentroid[m]; let best=mv.dest[0],bd=Infinity;\n      for(const s of mv.dest){ const sc=mv.secCentroid[s]; if(!sc||!c)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(mv,assign){\n  let d=0;\n  mv.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=mv.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,mv.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanMapFor(mv,a){const inc={};mv.dest.forEach(s=>inc[s]=0);\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=a[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  // Orphan CBs follow their leader's home sector, and that home depends on THIS candidate\n  // assignment — so they have to be counted here, or the optimizer would score and constrain a\n  // span that differs from the one the UI shows. Skipped entirely when there are no orphans.\n  if(mv.orphanByGroup&&Object.keys(mv.orphanByGroup).length){\n    mv.leaders.forEach(L=>{\n      const orphan=mv.orphanByGroup[L.grupo]||0; if(!orphan) return;\n      const byS={}; let tot=0;\n      for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s&&inc[s]!=null){ byS[s]=(byS[s]||0)+c; tot+=c; } }\n      if(!tot) return;   // no home to follow -> stranded, counted nowhere\n      const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n      if(inc[home]!=null) inc[home]+=orphan;\n    });\n  }\n  const sp={};mv.dest.forEach(s=>sp[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]);return sp;}\nfunction maxSpanOfFor(mv,a){return Math.max(...Object.values(spanMapFor(mv,a)),0);}\nfunction spanLimits(){const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  return {target:numOrNull(st.targetSpan),max:numOrNull(st.maxSpan),min:numOrNull(st.minSpan)};}\nfunction spanLimitsOn(lim){return !!(lim.target||lim.max||lim.min);}\nfunction spanViolationsOf(sp,lim){const over=[],under=[];\n  Object.entries(sp).forEach(([s,v])=>{ if(lim.max&&v>lim.max)over.push(s); if(lim.min&&v<lim.min)under.push(s); });\n  return {over,under};}\nfunction spanViolationsFor(mv,a){return spanViolationsOf(spanMapFor(mv,a),spanLimits());}\n// With a target span the objective becomes RMS distance from the ideal size (punishes too big AND\n// too small). With no target we keep the legacy objective: shave the biggest sector.\nfunction spanCostFor(mv,a){const lim=spanLimits(),vals=Object.values(spanMapFor(mv,a));\n  if(!vals.length) return 0;\n  if(lim.target) return Math.sqrt(vals.reduce((s,v)=>s+(v-lim.target)**2,0)/vals.length);\n  return Math.max(...vals);}\n// ---------- ÓTIMO: objetivo multi-critério ----------\n// Three normalised objectives, each 0..1, so the weights below are directly comparable. The old\n// score added a span cost measured in CBs to a desloc measured in CBs, so on a large sector the\n// balance term was numerically ~100x the desloc term and silently decided everything on its own.\n//   balance — RMS spread of the resulting spans (against the target span when one is set,\n//             otherwise against their own mean), over that reference. 0 = every destination\n//             sector ends up the same size.\n//   dist    — how far each município travels, CB-weighted: half from the share of its outer\n//             border that faces the sector it was given to, half from centroid distance. 0 =\n//             every município went to a sector it physically touches. Adjacency used to be a\n//             pure yes/no constraint (contiguity); this is what makes it a preference too.\n//   orphan  — stranded orphan CBs (leader left without a home sector, so they land nowhere at\n//             all) plus CBs split away from their leader's home, over the CBs at stake.\n//             Stranded ones count OPT_STRAND_K times: a stranded CB leaves the model entirely,\n//             a deslocated one merely changes hands.\nconst OPT_W={balance:1, dist:0.85, orphan:1};\nconst OPT_STRAND_K=3;\nconst OPT_SIMS=24;             // simultaneous simulations per run, scaled down on big stages\nconst OPT_PASSES=14;           // local-search sweeps inside one simulation\nconst OPT_EVAL_BUDGET=120000;  // total candidate evaluations across the whole portfolio\nconst OPT_EXHAUSTIVE_MAX=60000;\nfunction optDistPrep(mv){\n  let mx=0;\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__') return; const c=mv.muniCentroid[m]; if(!c) return;\n    mv.dest.forEach(s=>{ const sc=mv.secCentroid[s]; if(!sc) return;\n      const d=Math.hypot(c[0]-sc[0],c[1]-sc[1]); if(d>mx) mx=d; }); });\n  return mx||1;\n}\nfunction optDistCost(mv,m,s,dmax){\n  const v=mv.allowed[m]||{}; let tot=0; for(const k in v) tot+=v[k];\n  const share=tot?((v[s]||0)/tot):0;\n  const c=mv.muniCentroid[m], sc=mv.secCentroid[s];\n  // Missing centroid on either side is neutral (0.5), never 0 — an unmeasurable move must not\n  // score as the cheapest one on the board.\n  const geo=(c&&sc)?Math.min(1,Math.hypot(c[0]-sc[0],c[1]-sc[1])/dmax):0.5;\n  return 0.5*(1-share)+0.5*geo;\n}\nfunction optEval(mv,a,dmax){\n  const lim=spanLimits();\n  const sp=spanMapFor(mv,a), vals=Object.values(sp);\n  let balance=0;\n  if(vals.length){\n    const ref=lim.target||(vals.reduce((x,y)=>x+y,0)/vals.length);\n    if(ref) balance=Math.min(1,Math.sqrt(vals.reduce((s,v)=>s+(v-ref)**2,0)/vals.length)/ref);\n  }\n  let cbs=0,dist=0;\n  for(const m in a){ const o=mv.munis[m]; if(!o) continue; const w=+o.cbs||0; cbs+=w; dist+=w*optDistCost(mv,m,a[m],dmax); }\n  dist=cbs?(dist/cbs):0;\n  let stranded=0,desloc=0,tot=0;\n  mv.leaders.forEach(L=>{\n    const byS={}; let t=0;\n    for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s){ byS[s]=(byS[s]||0)+c; t+=c; } }\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    tot+=t+orphan;\n    if(!t){ stranded+=orphan; return; }   // no allocated CBs -> no home -> these orphans land nowhere\n    const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n    desloc+=t-(byS[home]||0);\n  });\n  const orphanTerm=tot?Math.min(1,(desloc+OPT_STRAND_K*stranded)/tot):0;\n  return { cost:OPT_W.balance*balance+OPT_W.dist*dist+OPT_W.orphan*orphanTerm,\n           balance, dist, desloc, stranded, sp, viol:spanViolationsOf(sp,lim) };\n}\nfunction optScoreFor(mv,a){ return optEval(mv,a,mv.__optDmax||optDistPrep(mv)).cost; }\n// Seeded PRNG on purpose. computeMovement runs the optimizer for EVERY stage and recomputeFrom\n// re-runs every stage downstream of an edit, so a Math.random() portfolio would hand back a\n// different allocation on each pass and a saved timeline would never reproduce.\nfunction optRng(seed){ let a=seed>>>0;\n  return ()=>{ a=(a+0x6D2B79F5)>>>0; let t=Math.imul(a^(a>>>15),1|a);\n    t=(t+Math.imul(t^(t>>>7),61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }\nfunction optSeed(mv,ms){ let h=2166136261>>>0;\n  const s=String(mv.sector)+'|'+ms.join(',')+'|'+mv.dest.join(',');\n  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);\n  return h>>>0; }\nfunction contigBadFor(mv,assign){\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(mv.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(mv.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\nfunction greedyFor(mv,ms,opts){const lim=spanLimits();const a={};\n  ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;\n    for(const s of opts[i]){a[m]=s;\n      // over-max only grows as we keep assigning, so it is safe to penalise mid-build;\n      // under-min shrinks, so penalising it here would just chase noise.\n      const over=lim.max?spanViolationsOf(spanMapFor(mv,a),{max:lim.max,min:null}).over.length:0;\n      const d=optScoreFor(mv,a)+(contigBadFor(mv,a).length?1e9:0)+over*1e6;\n      if(d<bd){bd=d;best=s;}}\n    a[m]=best;});\n  return a;}\nfunction optimizeFor(mv){\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__');\n  if(!ms.length||!mv.dest.length){ mv.optBadge=mv.dest.length?'':'sem setor destino identificado'; return {}; }\n  const opts=ms.map(m=>{const al=(mv.reachable[m]&&mv.reachable[m].length)?mv.reachable[m]:mv.dest.slice();return al.length?al:mv.dest.slice();});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  const lim=spanLimits();\n  const dmax=optDistPrep(mv); mv.__optDmax=dmax;\n\n  // Feasibility ladder — same meaning as before, because renderSpanRule and the saved sessions\n  // both read mv.optTier: 0 = contíguo + max + min span, 1 = contíguo + max span, 2 = contíguo\n  // only. Every candidate any simulation produces is offered to all three tiers; we ship the\n  // strictest tier that got a solution, so an unreachable min/max relaxes loudly instead of\n  // stranding the stage with nothing.\n  const tier=[null,null,null],tSc=[Infinity,Infinity,Infinity],tEv=[null,null,null];\n  let feasible=0,evals=0;\n  const offer=(a,ev)=>{\n    if(contigBadFor(mv,a).length) return false;\n    feasible++;\n    const put=t=>{ if(ev.cost<tSc[t]){ tSc[t]=ev.cost; tier[t]={...a}; tEv[t]=ev; } };\n    put(2);\n    if(!ev.viol.over.length){ put(1); if(!ev.viol.under.length) put(0); }\n    return true;\n  };\n  const evalOf=a=>{ evals++; return optEval(mv,a,dmax); };\n\n  // Small enough to settle the question outright: enumerate every contiguous allocation and the\n  // winner is provably the best one under this objective. No search needed, so none is run.\n  const exhaustive=combos<=OPT_EXHAUSTIVE_MAX;\n  let sims=0;\n  if(exhaustive){\n    const cur={};\n    const rec=i=>{ if(i===ms.length){ offer(cur,evalOf(cur)); return; }\n      for(const s of opts[i]){ cur[ms[i]]=s; rec(i+1); } };\n    rec(0);\n  } else {\n    // Too big to enumerate, so run a portfolio of independent simulations instead. Each one is a\n    // seed assignment, then hill-climbing on single-município reassignments, then one perturbation\n    // and another climb to escape whatever local optimum it settled into. Different seeds land in\n    // different basins; the ladder above keeps the best across all of them.\n    const improve=a=>{\n      let ev=evalOf(a); offer(a,ev);\n      for(let pass=0;pass<OPT_PASSES;pass++){\n        let moved=false;\n        for(let i=0;i<ms.length;i++){\n          const m=ms[i],cur=a[m]; let best=cur,bev=ev;\n          for(const s of opts[i]){\n            if(s===cur) continue;\n            a[m]=s;\n            const e=evalOf(a);\n            // contiguity is a hard constraint, and the cheaper test runs first on purpose\n            if(e.cost<bev.cost-1e-12 && !contigBadFor(mv,a).length){ best=s; bev=e; }\n          }\n          a[m]=best;\n          if(best!==cur){ ev=bev; moved=true; offer(a,ev); }\n        }\n        if(!moved) break;\n      }\n    };\n    const rng=optRng(optSeed(mv,ms));\n    const perSim=Math.max(1,ms.length*Math.max(1,mv.dest.length)*OPT_PASSES);\n    const budget=Math.max(4,Math.min(OPT_SIMS,Math.floor(OPT_EVAL_BUDGET/perSim)||4));\n    const seeds=[];\n    if(mv.assign&&ms.every(m=>mv.assign[m])) seeds.push({...mv.assign});   // never regress on what is already on screen\n    seeds.push(autoAssignFor(mv));                                        // adjacency-first seed\n    seeds.push(greedyFor(mv,ms,opts));                                    // balance-first seed\n    while(seeds.length<budget){\n      // randomised construction biased toward nearby sectors — the bias keeps each seed sane, the\n      // randomness is what sends the runs into different basins\n      const a={};\n      ms.forEach((m,i)=>{ let bs=opts[i][0],bw=-Infinity;\n        for(const s of opts[i]){ const w=(1-optDistCost(mv,m,s,dmax))+rng()*0.9; if(w>bw){bw=w;bs=s;} }\n        a[m]=bs; });\n      seeds.push(a);\n    }\n    seeds.forEach(seed=>{\n      const a={...seed};\n      // a restored or legacy assign can still name a sector this stage no longer offers\n      ms.forEach((m,i)=>{ if(!opts[i].includes(a[m])) a[m]=opts[i][0]; });\n      improve(a);\n      const kick=Math.max(1,Math.round(ms.length*0.25));\n      for(let k=0;k<kick;k++){ const i=Math.floor(rng()*ms.length); a[ms[i]]=opts[i][Math.floor(rng()*opts[i].length)]; }\n      improve(a);\n    });\n    sims=seeds.length;\n  }\n\n  const t=tier.findIndex(x=>x);\n  if(t<0){ mv.optTier=-1; mv.optBadge='sem solução contígua — heurística'; return greedyFor(mv,ms,opts); }\n  const best=tier[t], ev=tEv[t]; mv.optTier=t;\n  const relax=[];\n  if(t>0 && lim.min) relax.push('span mínimo '+fmt(lim.min));\n  if(t>1 && lim.max) relax.push('span máximo '+fmt(lim.max));\n  mv.optStats={balance:ev.balance,dist:ev.dist,desloc:ev.desloc,stranded:ev.stranded,sims,evals,feasible,combos,exhaustive};\n  mv.optBadge=`ótimo · ${exhaustive?`espaço completo (${fmt(combos)})`:`${fmt(sims)} simulações`}`\n    +` · equilíbrio ±${Math.round(ev.balance*100)}% · adjacência ${Math.round((1-ev.dist)*100)}%`\n    +` · quebra ${fmt(ev.desloc)} CBs · órfãs sem destino ${fmt(ev.stranded)}`\n    +` · pico ${fmt(maxSpanOfFor(mv,best))} CBs`\n    +(lim.target?` · alvo ${fmt(lim.target)}`:'')\n    +(relax.length?` · ⚠ inatingível: ${relax.join(' + ')}`:(spanLimitsOn(lim)?' · limites ok':''));\n  return best;\n}\nfunction computeDerived(mv){\n  const inc={},gained={},orph={};mv.dest.forEach(s=>{inc[s]=0;gained[s]=0;orph[s]=0;});\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  let desloc=0,orphanTotal=0,orphanStranded=0;\n  const L2=mv.leaders.map(L=>{const byS={};mv.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=mv.assign[m];if(s&&byS[s]!=null){byS[s]+=c;tot+=c;}}\n    // no allocated CBs => no home. Picking dest[0] by default would silently dump this leader's\n    // orphan CBs into an arbitrary sector, so we leave it null and report them as stranded.\n    const home=(tot>0&&mv.dest.length)?mv.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,mv.dest[0]):null;\n    const dl=tot-(home?byS[home]:0);desloc+=dl;if(home)gained[home]++;\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    orphanTotal+=orphan;\n    if(orphan){ if(home&&orph[home]!=null) orph[home]+=orphan; else orphanStranded+=orphan; }\n    return {...L,byS,home,desloc:dl,assignedTotal:tot,orphan,orphanHome:(orphan&&home)?home:null};});\n  const span={};mv.dest.forEach(s=>span[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]+orph[s]);\n  return {inc,gained,desloc,leaders:L2,span,orph,orphanTotal,orphanStranded};\n}\n\nasync function computeMovement(sec,world){\n  sec=String(sec);\n  const mv={sector:sec,policy:'fora',view:'tobe',selMuni:null,optBadge:''};\n  const SNAP=SESS.snapLiteral;\n  const mSQL=`SELECT DS_MUNICIPIO municipio, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n     COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n     ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n     SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP}\n    GROUP BY DS_MUNICIPIO, CD_GRUPO`;\n  const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n  const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n     COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n     ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n    FROM ${ELO} WHERE country_code=1 AND sector_code=${sec} AND operational_cycle<=${SESS.eloMaxCycle||999999} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n  const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n  const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, DS_NVL_PLAN_CRESC`;\n  const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n\n  const ownMatrix=rows(mR).filter(r=>+r.cbs>0);\n  mv.pureOwnMetrics=blankM();\n  ownMatrix.forEach(r=>{ mv.pureOwnMetrics.cbs+=+r.cbs; mv.pureOwnMetrics.receita+=+r.receita||0; mv.pureOwnMetrics.rec_nat+=+r.rec_nat||0; mv.pureOwnMetrics.rec_avon+=+r.rec_avon||0; mv.pureOwnMetrics.pontos+=+r.pontos||0; mv.pureOwnMetrics.pedidos+=+r.pedidos||0; mv.pureOwnMetrics.ativas+=+r.ativas||0; });\n\n  mv.levelByMuni={};\n  Object.entries(world.muniLevel||{}).forEach(([m,lv])=>{ if(world.muniOverride[m]===sec) mv.levelByMuni[m]={...lv}; });\n  // keyed further down, once the UF resolver exists — see the stamp block after buildAdj.\n\n  mv.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n  mv.levels=rows(lR);\n\n  const absorbedRows=[];\n  Object.entries(world.muniOverride).forEach(([m,destSec])=>{ if(destSec===sec && world.muniRow[m]) absorbedRows.push(...world.muniRow[m]); });\n  // CBs an earlier etapa sent here by FOLLOWING THEIR LEADER (quebra de vínculo) carry no\n  // muniOverride — the município stayed where it was, only the CBs moved — so absorbedRows misses\n  // them. Leaving them out made this sector's ASIS total smaller than the span the previous etapa's\n  // TOBE promised for it, by exactly that etapa's \"CBs ignoradas\", and dropped those CBs from the\n  // redistribution as if they had never existed.\n  const inheritedOrphanRows=(world.sectorOrphanRows&&world.sectorOrphanRows[sec])||[];\n  mv.matrix=ownMatrix.concat(absorbedRows,inheritedOrphanRows);\n\n  const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n  const domUF=ufList[0]||'PR';\n  mv.domUF=domUF;\n  const ufs=[domUF,...(UF_BORDERS[domUF]||[])];\n  const geos=await Promise.all(ufs.map(u=>geoUF(u).catch(()=>null)));\n  const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n  const adj=buildAdj(allFeat);\n\n  // ---- UF resolution ----\n  // SQL no longer selects DS_ESTADO. The base CSV is the authority on a município's UF, indexed by the\n  // (setor, município) pair it already maintains, and rows are stamped ONCE here so every consumer\n  // downstream keeps reading r.uf exactly as it did before.\n  // Fallback for a município the base does not carry — spillover, where the sector has CBs but no\n  // dominance: adopt the UF of the loaded shape when that name is unique across the UFs in this\n  // window. A name shared by two of them stays unresolved instead of guessed; it is spillover either\n  // way, and a guess would put a shape from the wrong state on the map.\n  const featUfByName={};\n  Object.keys(adj.feat).forEach(k=>{ const nm=mName(k),u=mUf(k);\n    if(!(nm in featUfByName)) featUfByName[nm]=u; else if(featUfByName[nm]!==u) featUfByName[nm]=null; });\n  const ufGuessed={};\n  const ufFor=(setorOf,muni)=>{const nm=norm(muni);\n    const fromCsv=DOM.ufBySetorMuni&&DOM.ufBySetorMuni[String(setorOf)+'|'+nm];\n    if(fromCsv) return fromCsv;\n    const uniq=featUfByName[nm];\n    if(uniq) return uniq;\n    // uniq===null means the name is shared by two of the LOADED UFs — Terra Nova exists in BA and in PE,\n    // and a BA sector loads PE as a border — so the unique-name fallback gave up. Giving up here too\n    // yields a bare NAME key, which matches NO shape: the município leaves the map without a word. The\n    // sector's own dominant UF is a real signal rather than a coin flip: when a shape of this name\n    // exists in domUF, that is the one this sector means. Recorded so the inference is never invisible.\n    if(uniq===null && adj.feat[mkey(nm,domUF)]){ ufGuessed[nm]=domUF; return domUF; }\n    return '';};\n  // Absorbed and orphan rows inherited from an earlier etapa were stamped when THAT etapa ran, under\n  // the setor they belonged to then; `if(!r.uf)` keeps that original resolution intact.\n  mv.matrix.forEach(r=>{ if(!r.uf) r.uf=ufFor(sec,r.municipio); });\n  rows(lmR).forEach(r=>{ const k=mkey(r.municipio,ufFor(sec,r.municipio)); (mv.levelByMuni[k]=mv.levelByMuni[k]||{})[r.nivel]=+r.cbs; });\n\n  const totBy={}; mv.matrix.forEach(r=>{const n=mkey(r.municipio,r.uf); totBy[n]=(totBy[n]||0)+ +r.cbs;});\n  const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>0).map(([m])=>m));\n  const withShape=[...footprint].filter(n=>adj.feat[n]||world.muniGeo[n]);\n  mv.cbsByMuni={...totBy}; mv.shapeMunis=withShape.slice();\n  mv.noShapeMunis=[...footprint].filter(n=>!(adj.feat[n]||world.muniGeo[n])).map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.noShapeCbs=mv.noShapeMunis.reduce((a,x)=>a+x.cbs,0);\n\n  // ---- RULE: which municipalities belong to this sector ----\n  // Comes from the dominantes CSV (setor x município), NOT from CB volume. A municipality where the\n  // closing sector happens to have a handful of CBs but which the base assigns to ANOTHER sector is\n  // spillover: it is not reallocated, it is not drawn, and its CBs follow their leader (quebra de\n  // vínculo). Municipalities an EARLIER etapa moved into this sector count as belonging to it too —\n  // otherwise the cascade would throw away the territory the sector just received.\n  const domBase=await ensureDominantes();\n  const domOwn=domBase[String(sec)];\n  const inherited=new Set(Object.keys(world.muniOverride).filter(m=>world.muniOverride[m]===sec));\n  if((!domOwn||!domOwn.size)&&!inherited.size) throw new Error('setor '+sec+' não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar este setor');\n  const belongs=n=>(domOwn&&(domOwn.has(n)||(!DOM.hasUf&&domOwn.has(mName(n)))))||inherited.has(n);\n  mv.domCsvMunis=(domOwn&&domOwn.size)||0;\n  const dropped=withShape.filter(n=>!belongs(n));\n  if(withShape.length && dropped.length===withShape.length) throw new Error('setor '+sec+': nenhum dos municípios com CBs está na base de dominantes — nada pra realocar');\n  const droppedSet=new Set(dropped);\n  mv.excludedMunis=dropped.map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.excludedCbs=dropped.reduce((a,n)=>a+(totBy[n]||0),0);\n\n  const closingNames=withShape.filter(n=>!droppedSet.has(n));\n  const closingSet=new Set(closingNames);\n  mv.geoFeat={}; closingNames.forEach(n=>{ mv.geoFeat[n]=(adj.feat[n]&&adj.feat[n].f)||world.muniGeo[n]; });\n\n  // ---- why is the map short of the base? ----\n  // Three gates drop a município between the CSV and the map: footprint (no CBs under this setor at\n  // the snapshot), withShape (no geometry in the loaded UFs) and belongs (key mismatch). Only the last\n  // two had a counter, so the FIRST one was invisible — the operator counted 5 shapes against a base\n  // that says 6 and had nothing on screen explaining the missing one. Named here, with its reason.\n  const drawnCsv=new Set(closingNames.map(n=>DOM.hasUf?n:mName(n)));\n  const footKeyOf=k=>DOM.hasUf?(footprint.has(k)?k:null):([...footprint].find(x=>mName(x)===k)||null);\n  mv.csvMunisTotal=(domOwn&&domOwn.size)||0;\n  mv.ufGuessed={...ufGuessed};\n  mv.missingMunis=[...(domOwn||[])].filter(k=>!drawnCsv.has(k)).map(k=>{\n    const fk=footKeyOf(k);\n    const hasShape=!!(adj.feat[fk||k]||world.muniGeo[fk||k]);\n    // The SAME name present in footprint under a DIFFERENT key means UF resolution disagreed with the\n    // base: the CBs are there, they just landed on a key that no shape and no dominance entry can\n    // match. That is a different failure from \"this município has no CBs\", and conflating the two sends\n    // the next investigation into the CB base when the answer is in the UF column.\n    const alias=[...footprint].filter(x=>x!==k&&mName(x)===mName(k));\n    let reason;\n    if(!fk&&alias.length) reason=`UF divergente — as CBs entraram na chave ${alias.join(' / ')} e a base diz ${k}`;\n    else if(!fk) reason=hasShape?'sem CBs neste setor no snapshot — desenhado como vizinho':'sem CBs neste setor no snapshot';\n    else if(!hasShape) reason='sem shape nas UFs carregadas';\n    else reason='chave não casou com a base de dominantes';\n    return {muni:k,cbs:(fk?totBy[fk]:alias.reduce((a,x)=>a+(totBy[x]||0),0))||0,reason:reason};\n  });\n\n  const extVotes={}; const nbrNames=new Set();\n  closingNames.forEach(n=>{\n    if(!adj.feat[n]) return;\n    // droppedSet is excluded too: a municipality outside this sector's dominance base must not\n    // nominate a destination sector, and it is not drawn on the map at all.\n    adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o) && !droppedSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });\n  });\n  // mv.nbrFeat is assembled far below, once mv.dest is final: the map draws only the closing sector's\n  // municípios and the destination sectors' territory, so the feature list cannot be built until the\n  // neighbour query has resolved each neighbour's sector AND the destination list has settled.\n  let neigh=[];\n  if(nbrNames.size){\n    // The IN-list carries bare names — that is what the column holds, and the query is back to not\n    // selecting DS_ESTADO. Every returned row names its own CD_SETOR, which is exactly the key the\n    // base CSV is indexed by, so each row's UF is resolved from the base right after the fetch.\n    const inList=[...new Set([...nbrNames].map(n=>mName(n)))].map(n=>`'${n.replace(/'/g,\"''\")}'`).join(',');\n    const nSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR<>${sec} AND CD_SETOR>0 AND DT_HISTORICO=${SNAP} AND DS_MUNICIPIO IN (${inList}) GROUP BY DS_MUNICIPIO, CD_SETOR`;\n    neigh=rows(await q(nSQL));\n  }\n  const domSecOf={};\n  neigh.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n  const byM={}; neigh.forEach(r=>{const m=mkey(r.municipio,r.uf); if(!nbrNames.has(m)) return; (byM[m]=byM[m]||[]).push(r);});\n  Object.entries(byM).forEach(([m,rs])=>{ rs.sort((a,b)=>+b.cbs-+a.cbs); domSecOf[m]=String(rs[0].setor); });\n  nbrNames.forEach(n=>{ if(world.muniOverride[n]) domSecOf[n]=world.muniOverride[n]; });\n  mv.nbrCurrentSector=domSecOf;\n\n  // ---- HARD RULE: which sectors are eligible as destinations ----\n  // 'any'      = a sector qualifies if it borders ANY municipality of the closing sector (legacy default)\n  // 'dominant' = only municipalities with >= destMinCbs CBs act as a bridge; smaller ones are still\n  //              redistributed, they just cannot pull a new sector into the destination list.\n  //\n  // ---- HARD RULE (não negociável): destino tem de ser da MESMA gerência de vendas ----\n  // Território não atravessa fronteira de GV num fechamento real: outra gerência é outra gestora,\n  // outra meta e outro P&L, então oferecer um setor de fora produz um plano que ninguém pode\n  // executar. CD_GV da base de dominantes é a autoridade, e o filtro roda ANTES de qualquer peso,\n  // regra de ponte ou corte de top-6 — nenhum caminho a jusante pode reintroduzir um setor de fora.\n  const gvBySetor=DOM.gvBySetor||{};\n  const gvOf=s=>gvBySetor[String(s)]||'';\n  const ownGv=gvOf(sec);\n  if(!ownGv) throw new Error('setor '+sec+' não tem CD_GV na base de municípios dominantes — sem a gerência de vendas do setor que fecha não há como garantir que os destinos sejam da mesma gerência');\n  // Um candidato que a base não carrega NÃO pode ser provado como da mesma gerência, então é recusado\n  // em vez de presumido: a regra só vale se nunca vazar.\n  const sameGv=s=>gvOf(s)===ownGv;\n  mv.gv=ownGv;\n  // Foreign-GV neighbours are not drawn AT ALL. Filtering them only out of the destination list still\n  // left them sitting on the map as context, which invites the exact question the rule exists to\n  // settle — \"why is 3471 right there if nothing can be sent to it?\". A neighbour whose sector is\n  // UNKNOWN is not being asserted to be foreign, so it stays as plain geography.\n  // Dropping them also tightens the frame: project() fits the bounding box of whatever survives, so the\n  // closing sector is drawn larger once foreign territory stops padding the extent.\n  mv.gvRejected=[];\n  const gvHidden=new Set();\n  nbrNames.forEach(n=>{ const s=domSecOf[n];\n    if(s && !sameGv(s)){ gvHidden.add(n); if(mv.gvRejected.indexOf(s)<0) mv.gvRejected.push(s); } });\n  gvHidden.forEach(n=>{ nbrNames.delete(n); delete domSecOf[n]; });\n  Object.keys(extVotes).forEach(n=>{ extVotes[n]=extVotes[n].filter(v=>!gvHidden.has(v.o)); });\n  const destRule=(SESS.settings&&SESS.settings.destRule)||DEFAULT_SETTINGS.destRule;\n  const destMinCbs=Math.max(1,Math.round(+((SESS.settings||{}).destMinCbs)||DEFAULT_SETTINGS.destMinCbs));\n  mv.destRule=destRule; mv.destMinCbs=destMinCbs; mv.destRuleFallback=false;\n  const isBridge=n=>destRule!=='dominant'||(totBy[n]||0)>=destMinCbs;\n  mv.bridgeMunis=closingNames.filter(isBridge);\n  mv.ignoredBridges=closingNames.filter(n=>!isBridge(n));\n\n  mv.allowed={}; mv.allowedRaw={};\n  const wAny={}, wDom={};\n  closingNames.forEach(n=>{\n    const v={}, bridge=isBridge(n);\n    (extVotes[n]||[]).forEach(({o,w})=>{\n      const s=domSecOf[o];\n      if(!s || s===sec || world.closedSectors.has(s)) return;\n      // Belt to the braces above: domSecOf has already had every foreign-GV neighbour removed, so this\n      // cannot fire any more. It stays because it costs one comparison and the failure it guards — a\n      // destination in the wrong gerência — is the whole reason this code exists.\n      if(!sameGv(s)) return;\n      v[s]=(v[s]||0)+w;\n      wAny[s]=(wAny[s]||0)+w;\n      if(bridge) wDom[s]=(wDom[s]||0)+w;\n    });\n    mv.allowed[n]=v; mv.allowedRaw[n]={...v};\n  });\n  let secWeight=wAny;\n  if(destRule==='dominant'){\n    if(Object.keys(wDom).length) secWeight=wDom;\n    else { secWeight=wAny; mv.destRuleFallback=true; }   // no dominant municipality at all -> relax, never strand the stage\n  }\n  mv.dest=Object.keys(secWeight).sort((a,b)=>secWeight[b]-secWeight[a]).slice(0,6);\n  if(!mv.dest.length){\n    // O fallback de emergência lê domSecOf direto, contornando os votos — então repete o filtro de GV\n    // aqui. Sem isso a regra vazaria exatamente no caso em que ninguém está olhando.\n    mv.dest=[...new Set(Object.values(domSecOf))].filter(s=>s!==sec&&!world.closedSectors.has(s)&&sameGv(s)).slice(0,6);\n    closingNames.forEach(n=>{ if(!Object.keys(mv.allowed[n]).length){ mv.dest.forEach(s=>mv.allowed[n][s]=1); } });\n  }\n  // Cinto e suspensório: qualquer rota que tenha preenchido mv.dest (pesos, fallback, um branch novo\n  // amanhã) passa por esta linha, e nada de fora da gerência do setor que fecha sobrevive a ela.\n  mv.dest=mv.dest.filter(sameGv);\n  // WHAT GETS DRAWN — decided here because it depends on the final destination list. Two things only:\n  // the closing sector's municípios and the territory of the sectors that can actually receive them.\n  // Every other neighbour was geographic filler that stretched the frame and settled no question.\n  // nbrCurrentSector deliberately keeps its full mapping: drawMap groups whatever survives in nbrFeat\n  // by it, so trimming the feature list is the entire change.\n  const drawSet=new Set(mv.dest);\n  mv.nbrFeat=[...nbrNames].filter(n=>drawSet.has(domSecOf[n])).map(n=>adj.feat[n]&&adj.feat[n].f).filter(Boolean);\n  // votes may reference a sector that did not survive the rule / top-6 cut — drop them so\n  // autoAssign + contiguity seeds can never point at a sector outside mv.dest\n  closingNames.forEach(n=>{ Object.keys(mv.allowed[n]||{}).forEach(s=>{ if(!mv.dest.includes(s)) delete mv.allowed[n][s]; }); });\n  mv.color={}; mv.dest.forEach((s,i)=>mv.color[s]=PALETTE[i%PALETTE.length]);\n\n  const accC={}; Object.entries(domSecOf).forEach(([nb,s])=>{ if(!mv.dest.includes(s))return; const f=(adj.feat[nb]&&adj.feat[nb].f)||world.muniGeo[nb]; if(!f)return; const c=centroidLL(f); accC[s]=accC[s]||[0,0,0]; accC[s][0]+=c[0]; accC[s][1]+=c[1]; accC[s][2]++; });\n  mv.secCentroid={}; mv.dest.forEach(s=>{ if(accC[s]) mv.secCentroid[s]=[accC[s][0]/accC[s][2],accC[s][1]/accC[s][2]]; });\n  mv.muniCentroid={}; closingNames.forEach(n=>{ if(mv.geoFeat[n]) mv.muniCentroid[n]=centroidLL(mv.geoFeat[n]); });\n  mv.closeAdj={}; closingNames.forEach(n=>{ mv.closeAdj[n]=adj.feat[n]?adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o):[]; });\n  const comp={}; let ci=0; closingNames.forEach(n=>{ if(comp[n]!=null)return; const st=[n]; comp[n]=ci; while(st.length){ const x=st.pop(); (mv.closeAdj[x]||[]).forEach(o=>{ if(comp[o]==null){ comp[o]=ci; st.push(o); } }); } ci++; });\n  const compAllowed={}; closingNames.forEach(n=>{ const cid=comp[n]; (compAllowed[cid]=compAllowed[cid]||new Set()); Object.keys(mv.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s)); });\n  mv.reachable={}; closingNames.forEach(n=>{ mv.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>mv.dest.includes(s)); });\n\n  if(mv.dest.length){\n    const rList=mv.dest.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    mv.baseMetrics={}; rows(bR).forEach(r=>{ mv.baseMetrics[String(r.setor)]=r; if(!SESS.sectorBaseCache[String(r.setor)]) SESS.sectorBaseCache[String(r.setor)]=r; });\n    mv.baseLevel={}; rows(blR).forEach(r=>{ const s=String(r.setor); (mv.baseLevel[s]=mv.baseLevel[s]||{})[r.nivel]=+r.cbs; });\n  } else { mv.baseMetrics={}; mv.baseLevel={}; }\n\n  mv.dest.forEach(s=>{\n    if(!mv.baseMetrics[s]) mv.baseMetrics[s]={setor:s,cbs:0,ativas:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0};\n    const infl=world.sectorInflow[s];\n    if(infl) Object.keys(blankM()).forEach(k=>{ mv.baseMetrics[s][k]=(+mv.baseMetrics[s][k]||0)+(infl[k]||0); });\n    const inflLv=world.sectorInflowLevel[s];\n    if(inflLv){ mv.baseLevel[s]=mv.baseLevel[s]||{}; Object.entries(inflLv).forEach(([nv,c])=>{ mv.baseLevel[s][nv]=(mv.baseLevel[s][nv]||0)+c; }); }\n  });\n\n  aggregateMovement(mv);\n  mv.assign=optimizeFor(mv);\n  return mv;\n}\n\n// ---------- cascade engine ----------\nasync function computeAt(i){\n  await ensurePinned();\n  const world=foldWorldState(SESS.timeline,i);\n  const sec=SESS.timeline[i].sector;\n  const mv=await computeMovement(sec,world);\n  SESS.timeline[i]=mv;\n}\nasync function recomputeFrom(i){\n  for(let k=i;k<SESS.timeline.length;k++){\n    loader(true,`recalculando etapa ${k+1} de ${SESS.timeline.length}…`);\n    try{ await computeAt(k); }\n    catch(e){\n      // A stage that failed to recompute still holds its PREVIOUS result, so the timeline would\n      // silently mix pre- and post-change etapas. Flag this stage and everything after it as stale\n      // (dashed chip + ⟳) so the state is visible and flushDirty retries from here.\n      SESS.dirtyFrom = SESS.dirtyFrom==null ? k : Math.min(SESS.dirtyFrom,k);\n      throw e;\n    }\n  }\n}\nasync function flushDirty(){\n  if(SESS.dirtyFrom==null) return;\n  const from=SESS.dirtyFrom; SESS.dirtyFrom=null;\n  loader(true,'recalculando etapas seguintes…');\n  try{ await recomputeFrom(from); }\n  catch(e){ $('#ds').textContent='erro ao recalcular cadeia: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n}\nasync function addMovement(sec){\n  sec=(sec==null?'':String(sec)).trim();\n  if(!sec) return;\n  const closedNow=new Set(SESS.timeline.map(mv=>mv.sector));\n  if(closedNow.has(sec)){ $('#ds').textContent='setor '+sec+' já fechado nesta timeline.'; $('#ds').classList.add('err'); return; }\n  if(!SESS.sectorsList.some(s=>s.setor===sec)){ $('#ds').textContent='setor \"'+sec+'\" não existe no snapshot congelado.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  loader(true,'fechando setor '+sec+' — puxando dado do snapshot congelado…');\n  $('#liveState').classList.remove('err');\n  try{\n    SESS.timeline.push({sector:String(sec)});\n    await computeAt(SESS.timeline.length-1);\n    SESS.selected=SESS.timeline.length-1;\n    $('#ds').classList.remove('err'); $('#ds').textContent='';\n  }catch(e){\n    SESS.timeline.pop();\n    $('#ds').textContent='erro ao fechar setor '+sec+': '+e.message; $('#ds').classList.add('err');\n  } finally { loader(false); renderAll(); }\n}\nfunction removeFrom(i){\n  SESS.timeline.length=i;\n  SESS.selected=Math.min(SESS.selected,Math.max(0,SESS.timeline.length-1));\n  if(SESS.dirtyFrom!=null && SESS.dirtyFrom>=SESS.timeline.length) SESS.dirtyFrom=null;\n  renderAll();\n}\nasync function editSelected(mutator){\n  const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  mutator(mv);\n  if(SESS.selected<SESS.timeline.length-1){\n    SESS.dirtyFrom = SESS.dirtyFrom==null ? SESS.selected+1 : Math.min(SESS.dirtyFrom, SESS.selected+1);\n  }\n  renderAll();\n}\n\n// ---------- sectors list / picker ----------\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1) GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  SESS.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n}\nfunction renderSectorPicker(){\n  const closed=new Set(SESS.timeline.map(mv=>mv.sector));\n  const avail=SESS.sectorsList.filter(s=>!closed.has(s.setor));\n  const dl=$('#sectorPickList');\n  if(dl) dl.innerHTML=avail.map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n  const ready=SESS.stage==='main';\n  $('#rand').disabled=!ready||!avail.length; $('#go').disabled=!ready||!avail.length; $('#sectorPick').disabled=!ready;\n}\nfunction renderStage(){\n  const isMain=SESS.stage==='main';\n  const cc=$('#confirmCard'), hb=$('#howtoBox'), mw=$('#mainWrap');\n  const hdr=$('#topHeader'), rl=$('#timelineRail');\n  if(hdr) hdr.classList.toggle('hidden', !isMain);\n  if(rl) rl.classList.toggle('hidden', !isMain);\n  if(cc) cc.classList.toggle('hidden', isMain);\n  if(hb) hb.classList.toggle('hidden', !isMain);\n  if(mw) mw.classList.toggle('hidden', !isMain);\n  const cb=$('#confirmBody');\n  if(cb && !isMain) cb.innerHTML = SESS.snapshotRaw\n    ? `Snapshot congelado · atualizado em <b>${fmtSyncedAt(SESS.snapshotSyncedAt)}</b> · <b>${SESS.sectorsList.length}</b> setor(es) disponível(eis) pra fechar. Nenhuma consulta de detalhe roda até você confirmar e escolher o primeiro setor.`\n    : 'congelando snapshot…';\n}\n\n// ---------- session persistence ----------\n// ONE table (`timeline-shared-sessions`) holds BOTH kinds of row, so SESSIONS_DS and SHARED_DS\n// deliberately resolve to the SAME DataSet — the two names mark intent, not two stores:\n//   · working session  — carries `movements`; saveSession updates it in place\n//   · shared snapshot  — carries `sharedAt` + `stages`; doShare CREATES it and nothing updates it\n// listSessions MUST exclude the shared rows. They are write-once by contract, and if one showed\n// up in the picker, loading it and pressing Salvar would fire ds.update() straight at a frozen\n// snapshot. Filtering here is what keeps that unreachable — it is the guard, not a cosmetic.\n// Both lookups stay because a policy-gated DataSet's Process resolves late; same race the reader\n// component polls for. SESSIONS_DS lives at module scope so no call site can hit it in the TDZ.\nasync function listSessions(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return [];\n  await ds.refresh();\n  const recs=ds.records()||[];\n  return [...recs].filter(r=>!r.sharedAt).sort((a,b)=>new Date(b.updatedAt||0)-new Date(a.updatedAt||0));\n}\nasync function refreshSessionPicker(){\n  const list=await listSessions().catch(()=>[]);\n  SESS.sessionsList=list;\n  const opts=list.map(s=>`<option value=\"${s._id}\">${esc(s.label||'sem nome')} · ${fmt((s.movements||[]).length)} etapa(s)</option>`).join('')+`<option value=\"\">+ Nova sessão</option>`;\n  const sel=$('#sessionPick'); if(sel){ sel.innerHTML=opts; sel.value=SESS.sessionId||''; }\n  const csel=$('#confirmSessionPick'); if(csel){ csel.innerHTML=opts; csel.value=SESS.sessionId||''; }\n}\nasync function saveSession(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS));\n  if(!ds){ $('#sessStatus').textContent='dataset de sessões indisponível'; return; }\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra salvar ainda'; return; }\n  await flushDirty();\n  const label=SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR'));\n  const nowIso=new Date().toISOString();\n  const payload={\n    label,\n    settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n    snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt, snapLiteral:SESS.snapLiteral, eloMaxCycle:SESS.eloMaxCycle,\n    movements:SESS.timeline.map(mv=>({\n      sector:mv.sector, assign:mv.assign, policy:mv.policy,\n      matrix:mv.matrix, trend:mv.trend, levels:mv.levels, levelByMuni:mv.levelByMuni,\n      pureOwnMetrics:mv.pureOwnMetrics, domUF:mv.domUF, dest:mv.dest, color:mv.color,\n      gv:mv.gv, gvRejected:mv.gvRejected,\n      missingMunis:mv.missingMunis, csvMunisTotal:mv.csvMunisTotal, ufGuessed:mv.ufGuessed,\n      baseMetrics:mv.baseMetrics, baseLevel:mv.baseLevel, geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat,\n      nbrCurrentSector:mv.nbrCurrentSector, allowed:mv.allowed, allowedRaw:mv.allowedRaw, reachable:mv.reachable,\n      destRule:mv.destRule, destMinCbs:mv.destMinCbs, destRuleFallback:mv.destRuleFallback, optTier:mv.optTier,\n      cbsByMuni:mv.cbsByMuni, shapeMunis:mv.shapeMunis, excludedMunis:mv.excludedMunis, domCsvMunis:mv.domCsvMunis,\n      excludedCbs:mv.excludedCbs, noShapeMunis:mv.noShapeMunis, noShapeCbs:mv.noShapeCbs,\n      secCentroid:mv.secCentroid, muniCentroid:mv.muniCentroid, closeAdj:mv.closeAdj, optBadge:mv.optBadge,\n    })),\n    updatedAt:nowIso,\n  };\n  $('#sessStatus').textContent='salvando…';\n  try{\n    if(SESS.sessionId){\n      await ds.update({_id:SESS.sessionId,...payload});\n    } else {\n      payload.createdAt=nowIso;\n      const created=await ds.create(payload);\n      SESS.sessionId=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    }\n    SESS.sessionLabel=label;\n    await refreshSessionPicker();\n    $('#sessStatus').textContent='salvo ✓';\n  }catch(e){ $('#sessStatus').textContent='erro ao salvar: '+e.message; }\n  setTimeout(()=>{ if($('#sessStatus')) $('#sessStatus').textContent=''; },2200);\n}\n// Duplicate = drop sessionId so saveSession takes its CREATE branch instead of its update branch.\n// The source record is never written to, the copy gets a fresh _id and its own createdAt, and the\n// copy becomes the active session (so the next Salvar edits the copy, not the original).\nasync function duplicateSession(){\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra duplicar ainda'; return; }\n  const prevId=SESS.sessionId, prevLabel=SESS.sessionLabel;\n  SESS.sessionId=null;\n  SESS.sessionLabel=(SESS.sessionLabel||'Sessão')+' (cópia)';\n  await saveSession();\n  if(!SESS.sessionId){\n    // saveSession swallows its own errors into #sessStatus, so a still-null id after the await is\n    // the only failure signal we get. Put the original identity back — otherwise a failed copy\n    // silently leaves the session detached, and the next Salvar would fork a new record instead\n    // of updating the one the user thought they were editing.\n    SESS.sessionId=prevId; SESS.sessionLabel=prevLabel;\n    await refreshSessionPicker().catch(()=>{});\n    return;\n  }\n  if($('#sessStatus')) $('#sessStatus').textContent='duplicado ✓';\n}\nasync function loadSession(id){\n  if(!id) return newSession();\n  const list=(SESS.sessionsList&&SESS.sessionsList.length)?SESS.sessionsList:await listSessions().catch(()=>[]);\n  const rec=list.find(s=>String(s._id)===String(id));\n  if(!rec) return newSession();\n  loader(true,'carregando sessão…');\n  SESS.sessionId=rec._id; SESS.sessionLabel=rec.label;\n  SESS.snapshotRaw=rec.snapshotRaw; SESS.snapshotSyncedAt=rec.snapshotSyncedAt||null; SESS.snapLiteral=rec.snapLiteral; SESS.eloMaxCycle=rec.eloMaxCycle;\n  SESS.settings={...DEFAULT_SETTINGS,...(rec.settings||{})};\n  SESS.timeline=(rec.movements||[]).map(m=>({sector:String(m.sector)}));\n  SESS.selected=0; SESS.sectorBaseCache={}; SESS.stage='main'; SESS.dirtyFrom=null;\n  try{\n    for(let i=0;i<SESS.timeline.length;i++){\n      const saved=(rec.movements||[])[i]||{};\n      // Sessions saved before the UF became part of the key hold bare-name keys in matrix rows,\n      // assign, geoFeat and closeAdj alike. Restoring that cache would mix key shapes the moment the\n      // stage is recomputed — and it would preserve the very collision this fixes — so those stages\n      // are replayed against the pinned snapshot and the manual assignment is remapped by name.\n      const legacyKeys=!!(saved.matrix&&saved.matrix.length&&saved.matrix.every(r=>r&&r.uf==null));\n      // Sessões salvas antes da regra de gerência podem carregar um destino de outra GV no cache.\n      // Restaurar esse cache manteria o setor proibido na tela para sempre, então essas etapas são\n      // reprocessadas contra o snapshot fixado — igual ao tratamento das chaves legadas.\n      const crossGv=await savedCrossesGv(saved);\n      if(saved.matrix&&!legacyKeys&&!crossGv){\n        loader(true,`restaurando etapa ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector} (cache, sem consulta)…`);\n        const mv={sector:String(saved.sector), policy:saved.policy||'fora', view:'tobe', selMuni:null, optBadge:saved.optBadge||''};\n        mv.matrix=saved.matrix; mv.trend=saved.trend||[]; mv.levels=saved.levels||[]; mv.levelByMuni=saved.levelByMuni||{};\n        mv.pureOwnMetrics=saved.pureOwnMetrics||blankM(); mv.domUF=saved.domUF; mv.dest=saved.dest||[]; mv.color=saved.color||{};\n        mv.baseMetrics=saved.baseMetrics||{}; mv.baseLevel=saved.baseLevel||{}; mv.geoFeat=saved.geoFeat||{}; mv.nbrFeat=saved.nbrFeat||[];\n        mv.nbrCurrentSector=saved.nbrCurrentSector||{}; mv.allowed=saved.allowed||{}; mv.reachable=saved.reachable||{};\n        mv.allowedRaw=saved.allowedRaw||saved.allowed||{};\n        mv.destRule=saved.destRule||SESS.settings.destRule; mv.destMinCbs=saved.destMinCbs||SESS.settings.destMinCbs; mv.destRuleFallback=!!saved.destRuleFallback;\n        mv.optTier=(saved.optTier==null?0:saved.optTier);\n        mv.gv=saved.gv||''; mv.gvRejected=saved.gvRejected||[];\n        mv.missingMunis=saved.missingMunis||[]; mv.csvMunisTotal=saved.csvMunisTotal||0;\n        mv.ufGuessed=saved.ufGuessed||{};\n        mv.cbsByMuni=saved.cbsByMuni||{}; mv.shapeMunis=saved.shapeMunis||[];\n        mv.excludedMunis=saved.excludedMunis||[]; mv.excludedCbs=saved.excludedCbs||0; mv.domCsvMunis=saved.domCsvMunis||0;\n        mv.noShapeMunis=saved.noShapeMunis||[]; mv.noShapeCbs=saved.noShapeCbs||0;\n        mv.secCentroid=saved.secCentroid||{}; mv.muniCentroid=saved.muniCentroid||{}; mv.closeAdj=saved.closeAdj||{};\n        aggregateMovement(mv);\n        mv.assign=saved.assign||{};\n        mv.dest.forEach(s=>{ if(!SESS.sectorBaseCache[s]) SESS.sectorBaseCache[s]=mv.baseMetrics[s]; });\n        SESS.timeline[i]=mv;\n      } else {\n        loader(true,`replay ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector}…`);\n        await computeAt(i);\n        if(saved.assign) SESS.timeline[i].assign=legacyKeys?remapAssign(SESS.timeline[i],saved.assign):saved.assign;\n        if(saved.policy) SESS.timeline[i].policy=saved.policy;\n      }\n    }\n    await loadSectors();\n  }catch(e){ $('#ds').textContent='erro ao carregar sessão: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n  renderAll();\n}\n// Re-points a legacy bare-name assignment at the NAME/UF key it can only mean. When a name resolves\n// to more than one município in the recomputed stage the manual choice is genuinely ambiguous —\n// dropping it lets the optimizer allocate instead of guessing on the operator's behalf.\n// True when a cached stage names a destination outside the closing sector's gerência de vendas.\n// Judges the cache by TODAY's rule, reading the (memoised) dominantes base rather than trusting what\n// the session was saved with. Unknown GV on the closing sector answers false: the stage then loads\n// from cache instead of being replayed into the hard throw in computeMovement, which would leave the\n// operator staring at a broken session with no way back.\nasync function savedCrossesGv(saved){\n  if(!saved||!saved.sector||!(saved.dest||[]).length) return false;\n  let gvBy={};\n  try{ await ensureDominantes(); gvBy=DOM.gvBySetor||{}; }catch(e){ return false; }\n  const own=gvBy[String(saved.sector)]||'';\n  if(!own) return false;\n  if(saved.dest.some(s=>(gvBy[String(s)]||'')!==own)) return true;\n  // A stage saved before foreign-GV neighbours stopped being drawn can hold a perfectly clean dest and\n  // STILL carry their shapes in nbrFeat/nbrCurrentSector — it would repaint the old map straight from\n  // cache. An unknown gerência is not evidence of a foreign one, so only a known, differing GV replays.\n  return Object.values(saved.nbrCurrentSector||{}).some(s=>{ const g=gvBy[String(s)]||''; return g&&g!==own; });\n}\nfunction remapAssign(mv,savedAssign){\n  const keys=Object.keys(mv.munis||{}).filter(k=>k!=='__OUTROS__');\n  const out={};\n  Object.entries(savedAssign||{}).forEach(([k,v])=>{\n    if(keys.indexOf(k)>=0){ out[k]=v; return; }\n    const cand=keys.filter(x=>mName(x)===mName(k));\n    if(cand.length===1) out[cand[0]]=v;\n  });\n  return out;\n}\nfunction newSession(){\n  SESS.sessionId=null; SESS.sessionLabel=null; SESS.timeline=[]; SESS.selected=0; SESS.sectorBaseCache={};\n  SESS.settings={...DEFAULT_SETTINGS};\n  SESS.snapshotRaw=null; SESS.snapshotSyncedAt=null; SESS.snapLiteral=null; SESS.eloMaxCycle=null; SESS.stage='loading'; SESS.dirtyFrom=null;\n  (async()=>{\n    loader(true,'iniciando nova sessão…');\n    try{ await Promise.all([ensurePinned(), loadSectors()]); SESS.stage='confirm'; }\n    catch(e){ setError('erro init: '+e.message); }\n    finally{ loader(false); renderAll(); }\n  })();\n}\n\n// ---------- snapshot refresh ----------\n// Deliberately breaks the frozen-snapshot guarantee, on demand: re-resolves DT_HISTORICO and\n// replays the whole timeline against the new one. Skips the replay when the snapshot has not\n// actually moved, so a stray click on a long timeline costs one cheap probe instead of ~8 queries\n// per etapa.\nasync function refreshSnapshot(){\n  if(SESS.stage!=='main' || SESS.snapBusy) return;\n  SESS.snapBusy=true;\n  const prevRaw=SESS.snapshotRaw;\n  const pill=$('#liveState'); if(pill){ pill.classList.add('busy'); pill.textContent='◌ atualizando snapshot…'; }\n  loader(true,'buscando o snapshot mais recente…');\n  let ok=false;\n  try{\n    const p=await resolveSnapshotPin();\n    if(!p.snapshotRaw) throw new Error('não foi possível resolver DT_HISTORICO');\n    const same=String(p.snapshotRaw)===String(prevRaw);\n    SESS.snapshotRaw=p.snapshotRaw; SESS.snapshotSyncedAt=p.syncedAt; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n    await loadSectors();   // the sector list follows the live max, so it can have moved too\n    if(same){\n      $('#ds').textContent=`snapshot já era o mais recente · atualizado em ${fmtSyncedAt(p.syncedAt)} — nada recalculado`;\n    } else if(SESS.timeline.length){\n      SESS.dirtyFrom=null;\n      SESS.sectorBaseCache={};   // baselines come from the snapshot, so every one of them changed\n      loader(true,'snapshot novo — recalculando a timeline…');\n      await recomputeFrom(0);\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)} · ${fmt(SESS.timeline.length)} etapa(s) recalculada(s)`;\n    } else {\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)}`;\n    }\n    $('#ds').classList.remove('err');\n    ok=true;\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?null:SESS.dirtyFrom+1);\n    $('#ds').textContent='erro ao atualizar o snapshot'+(at?` na etapa ${at} — etapa ${at} em diante ficou desatualizada (⟳)`:'')+': '+e.message;\n    $('#ds').classList.add('err');\n  }finally{\n    SESS.snapBusy=false;\n    if(pill) pill.classList.remove('busy');\n    loader(false); renderAll();\n    if(ok) setTimeout(()=>{ const d=$('#ds'); if(d&&!d.classList.contains('err')) d.textContent=''; },7000);\n  }\n}\n\n// ---------- session options ----------\nfunction previewDest(mv,rule,minCbs){\n  if(!mv||!mv.allowedRaw||!mv.munis) return null;\n  const w={}; let relaxed=false;\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const add=m=>Object.entries(mv.allowedRaw[m]||{}).forEach(([s,v])=>{ w[s]=(w[s]||0)+v; });\n  // allowedRaw only carries municipalities already inside the sector's dominance base, so there is\n  // nothing left to filter out here beyond the destination rule itself.\n  Object.keys(mv.allowedRaw).forEach(m=>{ const cbs=cbsOf(m);\n    if(rule==='dominant'&&cbs<minCbs) return; add(m); });\n  if(rule==='dominant'&&!Object.keys(w).length){ Object.keys(mv.allowedRaw).forEach(add); relaxed=true; }\n  return {dest:Object.keys(w).sort((a,b)=>w[b]-w[a]).slice(0,6),relaxed};\n}\nfunction readOptForm(){\n  const r=root.querySelector('input[name=\"destRule\"]:checked');\n  let n=Math.round(+$('#optMinCbs').value);\n  if(!(n>0)) n=DEFAULT_SETTINGS.destMinCbs;\n  return {destRule:(r&&r.value)||DEFAULT_SETTINGS.destRule, destMinCbs:n,\n          targetSpan:numOrNull($('#optTargetSpan').value),\n          maxSpan:numOrNull($('#optMaxSpan').value),\n          minSpan:numOrNull($('#optMinSpan').value)};\n}\nfunction spanFormError(f){\n  if(f.minSpan&&f.maxSpan&&f.minSpan>f.maxSpan) return `Span mínimo (${fmt(f.minSpan)}) não pode ser maior que o máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.maxSpan&&f.targetSpan>f.maxSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode passar do máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.minSpan&&f.targetSpan<f.minSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode ficar abaixo do mínimo (${fmt(f.minSpan)}).`;\n  return '';\n}\nfunction renderOptPreview(){\n  const el=$('#optPreview'); if(!el) return;\n  const f=readOptForm();\n  root.querySelectorAll('.opt-row').forEach(r=>{\n    if(r.dataset.r!=null) r.classList.toggle('on',r.dataset.r===f.destRule);\n  });\n  const wrap=$('#optMinCbsWrap'); if(wrap) wrap.classList.toggle('off',f.destRule!=='dominant');\n  const err=spanFormError(f), ee=$('#optSpanErr');\n  if(ee){ ee.textContent=err; ee.classList.toggle('hidden',!err); }\n  const ap=$('#optApply'); if(ap) ap.disabled=!!err;\n  const mv=SESS.timeline[SESS.selected];\n  if(!mv||!mv.munis){ el.innerHTML='Nenhuma etapa calculada ainda — as regras passam a valer no próximo fechamento.'; return; }\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const ms=Object.keys(mv.geoFeat||{});\n  const ig=f.destRule==='dominant'?ms.filter(m=>cbsOf(m)<f.destMinCbs):[];\n  const pv=previewDest(mv,f.destRule,f.destMinCbs);\n  let h=`<b style=\"color:var(--ink)\">Prévia — etapa ${SESS.selected+1}, setor ${mv.sector}</b><br>`;\n  const exN=(mv.excludedMunis||[]).length;\n  if(exN) h+=`Base de dominantes: <b>${fmt(ms.length)}</b> município(s) no mapa · <span style=\"color:var(--closing)\">${fmt(exN)} fora da base — ${fmt(mv.excludedCbs||0)} CBs seguem suas líderes</span><br>`;\n  h+=`${fmt(ms.length-ig.length)} de ${fmt(ms.length)} município(s) valem como ponte`;\n  if(ig.length) h+=` · <span style=\"color:var(--closing)\">ignorados:</span> ${ig.slice(0,8).join(', ')}${ig.length>8?' +'+(ig.length-8):''}`;\n  if(pv){\n    h+=`<br>Setores destino com esta regra: <b style=\"color:var(--ink)\">${pv.dest.join(', ')||'–'}</b>`;\n    const cur=(mv.dest||[]).join(', ');\n    if(pv.dest.join(',')!==(mv.dest||[]).join(',')) h+=` <span style=\"color:var(--closing)\">(hoje: ${cur||'–'})</span>`;\n    if(pv.relaxed) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — a regra relaxa pra \"qualquer município\" pra não deixar a etapa sem destino.</b>`;\n  }\n  if(f.targetSpan||f.maxSpan||f.minSpan){\n    const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,{target:f.targetSpan,max:f.maxSpan,min:f.minSpan});\n    h+=`<br><br>Span dos destinos como está hoje: ${mv.dest.map(s=>s+' <b>'+fmt(sp[s])+'</b>').join(' · ')||'–'}`;\n    if(v.over.length||v.under.length) h+=`<br><span style=\"color:var(--closing)\">Fora dos limites: ${[...v.over.map(s=>s+' ▲ acima do máx'),...v.under.map(s=>s+' ▼ abaixo do mín')].join(', ')}</span> — o modelo ÓTIMO tenta corrigir no recálculo.`;\n    else h+=` <span style=\"color:var(--ok)\">— já dentro dos limites</span>`;\n  }\n  if(SESS.timeline.length) h+=`<br><span style=\"margin-top:4px;display:inline-block\">Aplicar <b>recalcula as ${fmt(SESS.timeline.length)} etapa(s)</b> contra o snapshot congelado — os destinos ajustados à mão serão refeitos pelo modelo ÓTIMO.</span>`;\n  el.innerHTML=h;\n}\nfunction openOptions(){\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  root.querySelectorAll('input[name=\"destRule\"]').forEach(r=>{ r.checked=(r.value===st.destRule); });\n  $('#optMinCbs').value=st.destMinCbs;\n  $('#optTargetSpan').value=st.targetSpan||'';\n  $('#optMaxSpan').value=st.maxSpan||'';\n  $('#optMinSpan').value=st.minSpan||'';\n  $('#optApply').disabled=false;\n  $('#optSessName').textContent=SESS.sessionLabel||'sessão não salva';\n  $('#optStatus').textContent='';\n  $('#optionsBg').classList.remove('hidden');\n  renderOptPreview();\n  const c=$('#optCancel'); if(c) c.focus();\n}\nfunction closeOptions(){ const b=$('#optionsBg'); if(b) b.classList.add('hidden'); }\nasync function applyOptions(){\n  const f=readOptForm();\n  if(spanFormError(f)) return;\n  const prev={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const changed = prev.destRule!==f.destRule\n    || (f.destRule==='dominant' && prev.destMinCbs!==f.destMinCbs)\n    || numOrNull(prev.targetSpan)!==f.targetSpan\n    || numOrNull(prev.maxSpan)!==f.maxSpan\n    || numOrNull(prev.minSpan)!==f.minSpan;\n  SESS.settings={...prev,...f};\n  closeOptions();\n  if(!changed || !SESS.timeline.length){ renderAll(); return; }\n  SESS.dirtyFrom=null;\n  SESS.sectorBaseCache={};   // the recompute re-derives every baseline; keeping the old ones would\n                             // leave \"Impacto acumulado\" comparing against pre-change numbers\n  loader(true,'aplicando nova regra de destino — recalculando timeline…');\n  try{\n    await recomputeFrom(0);\n    $('#ds').textContent=''; $('#ds').classList.remove('err');\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?0:SESS.dirtyFrom)+1;\n    $('#ds').textContent=`erro ao aplicar a regra na etapa ${at}: ${e.message} — etapa ${at} em diante ficou desatualizada (⟳)`;\n    $('#ds').classList.add('err');\n  }finally{ loader(false); renderAll(); }\n}\n\nconst SHARED_DS='timeline-shared-sessionsDataSet';\nconst AUTH_REF='timelineSimAuthFinal';   // single source of truth for the auth strategy reference\nlet SHARE={busy:false,url:null,last:null};\n// Serialise a PRESENTATION model, not the raw session, so the reader stays purely presentational\n// and never re-derives spans/desloc/orphans — no second copy of this engine to keep in sync.\n// NOTE: METRICS_DEF[2] is the money marker; read it as !!md[2] (a literal dollar sign in this\n// position trips the upsert's parse check).\nfunction buildShareStages(){\n  return SESS.timeline.map((mv,i)=>{\n    const c=computeDerived(mv);\n    const incBy={};\n    mv.dest.forEach(s=>{ incBy[s]=Object.fromEntries(METRICS_DEF.map(md=>[md[0],0])); });\n    Object.entries(mv.munis).forEach(([m,o])=>{\n      if(m==='__OUTROS__') return;\n      const s=mv.assign[m]; if(!s||!incBy[s]) return;\n      METRICS_DEF.forEach(md=>{ incBy[s][md[0]]+=o[md[0]]||0; });\n    });\n    const saiTot={};\n    METRICS_DEF.forEach(md=>{\n      const k=md[0];\n      saiTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,p)=>a+((p[1][k])||0),0);\n    });\n    // orphans follow their leader's home: they enter that sector AND leave the closing one\n    c.leaders.forEach(L=>{\n      const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n      if(!om||!L.home) return;\n      METRICS_DEF.forEach(md=>{ const k=md[0];\n        if(incBy[L.home]) incBy[L.home][k]+=om[k]||0;\n        saiTot[k]+=om[k]||0; });\n    });\n    const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n    const sv=mv.dest.map(s=>c.span[s]);\n    const baseOf=(s,k)=>{ const b=mv.baseMetrics[s]||{}; return +(b[k]||0); };\n    return {\n      i:i, sector:mv.sector, policy:mv.policy, domUF:mv.domUF,\n      optTier:(mv.optTier==null?0:mv.optTier),\n      destRuleFallback:!!mv.destRuleFallback,\n      dest:mv.dest.slice(), color:Object.assign({},mv.color), assign:Object.assign({},mv.assign),\n      kpis:{ cbsRealocadas:closingCbsOf(mv), desloc:c.desloc,\n             spread:(sv.length?Math.max.apply(null,sv)-Math.min.apply(null,sv):0),\n             municipios:ms.length, lideres:mv.leaders.length },\n      destinos:mv.dest.map(s=>({ s:s, base:baseOf(s,'cbs'), inc:c.inc[s]||0, orph:c.orph[s]||0, span:c.span[s]||0, gained:c.gained[s]||0 })),\n      municipios:ms.map(m=>({ nome:mName(m), uf:mUf(m), cbs:mv.munis[m].cbs, destino:mv.assign[m]||null })),\n      excluidos:(mv.excludedMunis||[]).map(x=>({ muni:x.muni, cbs:x.cbs })),\n      orphanTotal:c.orphanTotal||0, orphanStranded:c.orphanStranded||0, noShapeCbs:mv.noShapeCbs||0,\n      lideres:c.leaders.map(L=>({ lider:(L.lider||'').split(' ').slice(0,2).join(' '), grupo:L.grupo,\n        total:L.total, home:L.home, desloc:L.desloc, orphan:L.orphan||0 })),\n      metricas:METRICS_DEF.map(md=>{ const k=md[0];\n        return { key:k, label:md[1], money:!!md[2],\n          rows:mv.dest.map(s=>({ s:s, antes:baseOf(s,k), depois:baseOf(s,k)+((incBy[s]&&incBy[s][k])||0) })),\n          sai:saiTot[k]||0 }; }),\n      geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat, excludedFeat:[]\n    };\n  });\n}\nfunction buildShareSummary(stages){\n  const world=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const receivers=[];\n  SESS.timeline.forEach((mv,i)=>{\n    const own=mv.pureOwnMetrics||{};\n    receivers.push({ s:mv.sector, situacao:'fechado (etapa '+(i+1)+')', antes:+(own.cbs||0), depois:0 });\n  });\n  const seen={};\n  Object.values(world.muniOverride).forEach(s=>{\n    if(seen[s]) return; seen[s]=1;\n    const base=SESS.sectorBaseCache[s]||{}, infl=world.sectorInflow[s]||{};\n    receivers.push({ s:s, situacao:'recebeu território', antes:+(base.cbs||0), depois:(+(base.cbs||0))+(infl.cbs||0) });\n  });\n  return { stages:stages.length, sectors:SESS.timeline.map(mv=>mv.sector),\n    totalCbs:stages.reduce((a,s)=>a+(s.kpis.cbsRealocadas||0),0),\n    totalDesloc:stages.reduce((a,s)=>a+(s.kpis.desloc||0),0), receivers:receivers };\n}\nfunction renderShareBody(){\n  const el=$('#shareBody'); if(!el) return;\n  const go=$('#shareGo'), cp=$('#shareCopy'), op=$('#shareOpen'), cc=$('#shareCancel');\n  if(SHARE.url){\n    el.innerHTML=`<div class=\"opt-note\" style=\"margin-top:0\"><b style=\"color:var(--ink)\">Sessão compartilhada ✓</b><br>\n      Link de leitura desta simulação congelada. Quem abrir precisa ter login no simulador.\n      <div class=\"shareurl\">${esc(SHARE.url)}</div></div>`;\n    if(go) go.classList.add('hidden');\n    if(cp) cp.classList.remove('hidden');\n    if(op) op.classList.remove('hidden');\n    if(cc) cc.textContent='Fechar';\n    return;\n  }\n  if(go) go.classList.remove('hidden');\n  if(cp) cp.classList.add('hidden');\n  if(op) op.classList.add('hidden');\n  if(cc) cc.textContent='Cancelar';\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const desloc=SESS.timeline.reduce((a,mv)=>a+(mv.munis?computeDerived(mv).desloc:0),0);\n  const destTxt=st.destRule==='dominant'?('dominantes ≥'+fmt(st.destMinCbs)+' CBs'):'qualquer município';\n  el.innerHTML=`<div class=\"opt-lbl\">Confirmar compartilhamento</div>\n    <div class=\"opt-sub\">Publica uma <b>cópia congelada</b> desta sessão, exatamente no estado atual.</div>\n    <div class=\"opt-note\" style=\"margin-top:0\">\n      · <b>${fmt(SESS.timeline.length)}</b> etapa(s) — ${esc(SESS.timeline.map(mv=>mv.sector).join(' → '))}<br>\n      · snapshot atualizado em <b>${esc(fmtSyncedAt(SESS.snapshotSyncedAt))}</b><br>\n      · base: <b>municípios dominantes (CSV)</b> · destino: <b>${esc(destTxt)}</b> · span: <b>${esc(spanShort(st))}</b><br>\n      · <b>${fmt(desloc)}</b> CBs deslocadas somando a cadeia\n    </div>\n    <div class=\"opt-note\">A cópia <b>não</b> acompanha edições posteriores desta sessão — pra atualizar, compartilhe de novo e use o link novo. O link exige login no simulador; não é público.</div>\n    ${SHARE.last?`<div class=\"opt-note\"><b style=\"color:var(--ink)\">Último link gerado nesta sessão</b> — aponta pro estado de <b>antes</b> das edições feitas depois dele.<div class=\"shareurl\">${esc(SHARE.last)}</div></div>`:''}`;\n}\nasync function openShare(){\n  if(!SESS.timeline.length){ $('#ds').textContent='nada pra compartilhar — feche pelo menos um setor primeiro.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  SHARE.url=null;\n  const s=$('#shareStatus'); if(s){ s.textContent=''; s.classList.remove('err'); }\n  renderShareBody();\n  $('#shareBg').classList.remove('hidden');\n  const b=$('#shareCancel'); if(b) b.focus();\n}\nfunction closeShare(){ const b=$('#shareBg'); if(b) b.classList.add('hidden'); }\nasync function doShare(){\n  if(SHARE.busy) return;\n  const go=$('#shareGo'), stx=$('#shareStatus');\n  SHARE.busy=true;\n  if(go){ go.disabled=true; go.textContent='Compartilhando…'; }\n  if(stx){ stx.textContent=''; stx.classList.remove('err'); }\n  try{\n    // Resolved as a GLOBAL PROCESS, not a declared param — which only works because the block\n    // instance on the `main` artboard binds `sharedDataset: @timeline-shared-sessionsDataSet`.\n    // That binding is load-bearing: a Process is only define()d once something on the rendering\n    // tree references it, so without it this lookup returns null and sharing dies on the guard\n    // below. Do NOT remove the instance param because this file never reads d2.params.sharedDataset.\n    const ds=d2.data(SHARED_DS);\n    if(!ds) throw new Error('dataset de sessões compartilhadas indisponível');\n    await flushDirty();\n    const stages=buildShareStages();\n    const summary=buildShareSummary(stages);\n    let who='';\n    try{ const a=d2.auth(AUTH_REF); who=(a&&a.user&&(a.user.email||a.user.nome))||''; }catch(e){}\n    const created=await ds.create({\n      label:SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR')),\n      snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt,\n      settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n      stages:stages, summary:summary, sharedBy:who, sharedAt:new Date().toISOString()\n    });\n    const id=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    if(!id) throw new Error('registro gravado, mas sem _id na resposta — não foi possível montar o link');\n    SHARE.url=window.location.origin+'/sessions/'+id;\n    SHARE.last=SHARE.url;\n    renderShareBody();\n  }catch(e){\n    if(stx){ stx.textContent='erro ao compartilhar: '+e.message; stx.classList.add('err'); }\n  }finally{\n    SHARE.busy=false;\n    if(go){ go.disabled=false; go.textContent='Compartilhar'; }\n  }\n}\n\n// ---------- rendering ----------\nfunction renderTimeline(){\n  const el=$('#movementChips'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<span class=\"empty\">Nenhuma etapa ainda — use \"Fechar setor ▶\" à direita.</span>'; return; }\n  el.innerHTML=SESS.timeline.map((mv,i)=>{\n    const ready=mv.dest!==undefined;\n    const c=ready?computeDerived(mv):null;\n    const stale=SESS.dirtyFrom!=null && i>=SESS.dirtyFrom;\n    return `<div class=\"tchip ${i===SESS.selected?'on':''} ${stale?'stale':''}\" data-i=\"${i}\">\n      <span class=\"tnum\">${i+1}</span><span class=\"tsec\">Setor ${mv.sector}</span>\n      <span class=\"tstat\">${ready?fmt(c.desloc)+' desloc.':'…'}</span>\n      ${stale?'<span class=\"tstale\" title=\"recalcula ao selecionar outra etapa\">⟳</span>':''}\n      <span class=\"trm\" data-rm=\"${i}\" title=\"remover esta etapa e as seguintes\">✕</span>\n    </div>`;\n  }).join('');\n  el.querySelectorAll('.tchip').forEach(ch=>{\n    ch.addEventListener('click',async (e)=>{\n      if(e.target.dataset.rm!=null && e.target.dataset.rm!==''){ removeFrom(+e.target.dataset.rm); return; }\n      const i=+ch.dataset.i;\n      if(i!==SESS.selected) await flushDirty();\n      SESS.selected=i; renderAll();\n    });\n  });\n}\nfunction renderRuleNote(){\n  const el=$('#ruleNote'); if(!el) return;\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const mv=SESS.timeline[SESS.selected];\n  let h='';\n  h+=`<b style=\"color:var(--ink)\">Base de municípios dominantes:</b> só os municípios que a base (CSV) atribui ao setor entram no mapa e na alocação`;\n  if(mv&&mv.domCsvMunis) h+=` · <b>${fmt(mv.domCsvMunis)}</b> na base do setor ${mv.sector}`;\n  if(mv&&mv.excludedCbs) h+=` · <b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs)} CBs</b> fora da base nesta etapa — não aparecem no mapa e as CBs seguem suas líderes (quebra de vínculo)`;\n  if(DOM.error) h+=` · <b style=\"color:var(--closing)\">⚠ base indisponível: ${esc(DOM.error)}</b>`;\n  h+=`<br>`;\n  h+=`<b style=\"color:var(--ink)\">Regra de destino:</b> `;\n  h+= st.destRule==='dominant'\n    ? `só municípios com <b>≥${fmt(st.destMinCbs)} CBs</b> trazem setor pra lista de destinos`\n    : `qualquer município do setor fechado vale como ponte <span style=\"opacity:.7\">(padrão)</span>`;\n  if(mv&&mv.munis&&st.destRule==='dominant'){\n    const ig=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__'&&mv.munis[m].cbs<st.destMinCbs);\n    if(ig.length) h+=` · <b style=\"color:var(--closing)\">${fmt(ig.length)}</b> ignorado(s) como ponte (ainda redistribuído(s)): ${ig.slice(0,5).join(', ')}${ig.length>5?' +'+(ig.length-5):''}`;\n    if(mv.destRuleFallback) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — regra relaxada pra \"qualquer município\".</b>`;\n  }\n  el.innerHTML=h;\n}\nfunction renderSpanRule(){\n  const el=$('#spanRule'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected], lim=spanLimits();\n  if(!mv||!mv.munis||mv.view==='asis'||!spanLimitsOn(lim)){ el.innerHTML=''; return; }\n  const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,lim);\n  const bits=[];\n  if(lim.target) bits.push(`alvo <b>${fmt(lim.target)}</b>`);\n  if(lim.max) bits.push(`máx <b>${fmt(lim.max)}</b>`);\n  if(lim.min) bits.push(`mín <b>${fmt(lim.min)}</b>`);\n  let h=`Pré-requisito de span: ${bits.join(' · ')} CBs — `;\n  if(v.over.length||v.under.length){\n    h+=`<b style=\"color:var(--closing)\">⚠ `;\n    if(v.over.length) h+=`acima do máximo: ${v.over.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    if(v.over.length&&v.under.length) h+=' · ';\n    if(v.under.length) h+=`abaixo do mínimo: ${v.under.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    h+=`</b>`;\n    if(mv.optTier===1) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação contígua respeita o span mínimo nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===2) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação contígua respeita os limites nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===-1) h+=`<br><span style=\"opacity:.85\">Etapa resolvida por heurística — os limites não são garantidos.</span>`;\n  } else h+=`<b style=\"color:var(--ok)\">✓ todos os setores receptores dentro dos limites</b>`;\n  el.innerHTML=`<div class=\"note\" style=\"margin:-2px 0 6px\">${h}</div>`;\n}\nfunction renderContig(){const mv=SESS.timeline[SESS.selected];const el=$('#contig');if(!el||!mv)return;\n  if(mv.view==='asis'){el.innerHTML='';return;}\n  const bad=contigBadFor(mv,mv.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Contiguidade quebrada em ${bad.join(', ')} — município(s) desconectado(s) do território do setor. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Contíguo — cada setor forma uma região conectada.</div>`;\n}\nfunction closingCbsOf(mv){ return Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0); }\nfunction sectorTotalCbs(mv){ return Object.values(mv.munis).reduce((a,o)=>a+o.cbs,0); }\nfunction renderKpis(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){const mc=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(sectorTotalCbs(mv)),l:`CBs no setor ${mv.sector} (antes desta etapa)`,t:'Total de CBs no setor hoje — já considerando o que etapas anteriores da timeline mandaram pra cá.'},\n      {n:mv.csvMunisTotal?`${fmt(mc)}/${fmt(mv.csvMunisTotal)}`:fmt(mc),l:'Municípios do setor',t:`Municípios desenhados nesta etapa${mv.csvMunisTotal?` de ${fmt(mv.csvMunisTotal)} que a base de dominantes atribui ao setor`:''}.${(mv.missingMunis||[]).length?' Fora do mapa: '+mv.missingMunis.map(x=>x.muni+' ('+x.reason+')').join(' · '):''}${Object.keys(mv.ufGuessed||{}).length?' UF inferida da UF dominante do setor (nome repetido em outra UF carregada): '+Object.entries(mv.ufGuessed).map(([nm,u])=>nm+'→'+u).join(', '):''}`},\n      {n:fmt(mv.leaders.length),l:'Líderes (grupos)',t:'Líderes presentes no setor nesta etapa.'},\n      {n:fmt(mv.dest.length),l:'Setores adjacentes',t:`Setores vizinhos (ainda abertos) da MESMA gerência de vendas (CD_GV ${mv.gv||'?'}) que poderiam receber os municípios.${(mv.gvRejected||[]).length?' Fora do mapa, por serem de outra gerência: '+mv.gvRejected.join(', ')+'.':''}`},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=mv.dest.map(s=>c.span[s]);const spread=sv.length?Math.max(...sv)-Math.min(...sv):0;\n  const zero=mv.dest.filter(s=>c.gained[s]===0);const totCbs=closingCbsOf(mv);\n  $('#kpis').innerHTML=[\n    {n:fmt(c.desloc),l:mv.policy==='fora'?'CBs fora do setor':'CBs c/ quebra de relação',cls:'warn',t:'CBs cuja líder acabou noutro setor nesta etapa.'},\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:'Diferença de tamanho entre o maior e o menor setor depois desta etapa.'},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs do setor ${mv.sector} sendo redistribuídas nesta etapa.`},\n    {n:zero.length?zero.join(', '):'nenhum',l:'Setor sem líder herdada',cls:zero.length?'warn':'ok',t:'Setor que recebe município(s) mas nenhuma líder nesta etapa.'},\n  ].concat(mv.excludedCbs?[{n:fmt(mv.excludedCbs),l:`CBs ignoradas · ${fmt(mv.excludedMunis.length)} município(s) fora da base`,cls:'warn',t:'CBs em municípios que a base de dominantes não atribui a este setor: ficam fora do mapa e da alocação geográfica e seguem a líder do seu grupo.'}]:[])\n   .concat((mv.missingMunis||[]).length?[{n:fmt(mv.missingMunis.length),l:`Município(s) da base fora do mapa · ${fmt(mv.csvMunisTotal)} na base`,cls:'warn',t:`A base de dominantes atribui ${fmt(mv.csvMunisTotal)} município(s) ao setor ${mv.sector} e ${fmt(mv.csvMunisTotal-mv.missingMunis.length)} chegaram ao mapa. Fora: ${mv.missingMunis.map(x=>x.muni+' — '+x.reason+(x.cbs?' ('+fmt(x.cbs)+' CBs)':'')).join(' · ')}`}]:[])\n   .map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){const items=[{s:mv.sector,val:sectorTotalCbs(mv),color:CLOSE_COLOR}].concat(mv.dest.map(s=>({s,val:+(mv.baseMetrics[s]?.cbs||0),color:mv.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura antes desta etapa — o setor <b>${mv.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const lim=spanLimits(),on=spanLimitsOn(lim);\n  const rawMax=Math.max(...mv.dest.map(s=>c.span[s]),lim.target||0,lim.max||0,1);\n  const max=on?rawMax*1.04:rawMax;   // headroom so a marker sitting at the scale top stays visible\n  const mk=(v,color,label)=>v?`<div class=\"bar-mark\" style=\"left:${Math.min(99.6,v/max*100).toFixed(1)}%;background:${color}\" title=\"${label}\"></div>`:'';\n  const marks=mk(lim.min,'#ff9500','span mínimo '+fmt(lim.min))+mk(lim.target,'#007aff','span alvo '+fmt(lim.target))+mk(lim.max,'#ff3b30','span máximo '+fmt(lim.max));\n  $('#spans').innerHTML=mv.dest.map(s=>{const base=+(mv.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    const over=lim.max&&c.span[s]>lim.max,under=lim.min&&c.span[s]<lim.min;\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${mv.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${mv.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${mv.color[s]}\"></div>${marks}</div>\n      <div class=\"bar-val\" style=\"${over?'color:var(--closing);font-weight:700':(under?'color:#ff9500;font-weight:700':'')}\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span>${over?' ▲':''}${under?' ▼':''}</div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base (já incluindo etapas anteriores) · cheio = CBs entrantes do ${mv.sector} nesta etapa.${c.orphanTotal?` Inclui <b>${fmt(c.orphanTotal-c.orphanStranded)}</b> CB(s) de municípios fora da base de dominantes que seguiram suas líderes.`:''}${on?' Marcas: <b style=\"color:#007aff\">│</b> alvo · <b style=\"color:#ff3b30\">│</b> máx · <b style=\"color:#ff9500\">│</b> mín.':''}</div>`;\n}\n// Two very different reasons land rows in the __OUTROS__ bucket — no IBGE shape, or outside the\n// sector's dominance base. Same bucket computationally, but they must not read as the same thing.\nfunction outsideNote(mv){\n  const bits=[];\n  if(mv.noShapeCbs) bits.push(`<b>${fmt(mv.noShapeCbs)}</b> CBs em ${fmt(mv.noShapeMunis.length)} município(s) sem shape na UF`);\n  if(mv.excludedCbs) bits.push(`<b>${fmt(mv.excludedCbs)}</b> CBs em ${fmt(mv.excludedMunis.length)} município(s) <b>fora da base de dominantes</b> deste setor — <b style=\"color:var(--closing)\">quebra de vínculo</b>`);\n  if(!bits.length) return '';\n  let h=`<div class=\"note\">Fora do mapa e da alocação geográfica: ${bits.join(' · ')}.`;\n  if(mv.excludedCbs){\n    const c=computeDerived(mv);\n    h+=` As <b>${fmt(mv.excludedCbs)}</b> CBs em quebra de vínculo <b>seguem a líder do seu grupo</b>`;\n    const byS=mv.dest.filter(s=>c.orph[s]).map(s=>`<b style=\"color:${mv.color[s]}\">${s}</b> (${fmt(c.orph[s])})`);\n    h+=byS.length?`: ${byS.join(' · ')}.`:'.';\n    if(c.orphanStranded) h+=` <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} CB(s) sem destino</b> — a líder não ficou com nenhum município realocado, então não há setor pra seguir.`;\n  }\n  if(mv.noShapeCbs) h+=` As CBs sem shape na UF continuam fora de qualquer setor destino.`;\n  if(mv.excludedMunis&&mv.excludedMunis.length) h+=`<br><span style=\"opacity:.85\">${mv.excludedMunis.slice(0,8).map(x=>x.muni+' ('+fmt(x.cbs)+')').join(', ')}${mv.excludedMunis.length>8?' +'+(mv.excludedMunis.length-8):''}</span>`;\n  return h+`</div>`;\n}\nfunction renderMuni(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  if(mv.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">setor ${mv.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje todos pertencem ao setor <b>${mv.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`+outsideNote(mv);return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${mv.dest.map(s=>`<button data-s=\"${s}\" class=\"${mv.assign[m]===s?'on':''}\" style=\"${mv.assign[m]===s?'background:'+mv.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +outsideNote(mv);\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{const m=b.parentElement.dataset.m,s=b.dataset.s;editSelected(m2=>{m2.assign[m]=s;});});\n}\nfunction renderLeaders(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>Setor</th></tr></thead><tbody>`\n    +mv.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${mv.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(mv.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  const showOrph=c.orphanTotal>0;\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${mv.dest.map(s=>`<th>${s}</th>`).join('')}${showOrph?'<th title=\"CBs em municípios fora da base de dominantes que seguiram esta líder\">Órfãs</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td>${L.home?`<span class=\"home\" style=\"background:${mv.color[L.home]}\"></span>${L.home}`:'–'}</td>\n     ${mv.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+mv.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     ${showOrph?`<td class=\"cell\" style=\"${L.orphan&&!L.orphanHome?'color:var(--closing);font-weight:700':''}\" title=\"${L.orphan?(L.orphanHome?'seguem a líder para o setor '+L.orphanHome:'sem home — CBs sem setor pra seguir'):''}\">${L.orphan?fmt(L.orphan)+(L.orphanHome?'':' ⚠'):'·'}</td>`:''}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${mv.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}${showOrph?`<td>${fmt(c.orphanTotal)}</td>`:''}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // `_out` is a flag stamped on the ROW, and rows are shared between etapas (absorbed + inherited\n  // orphans), so the last etapa to aggregate wins and reading it here can describe the wrong etapa.\n  // Derive the bucket from THIS mv instead. Summing rather than .find() because several out-municípios\n  // of the same group all collapse into __OUTROS__, and only the first was being counted.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cell=(m,g)=>mv.matrix.reduce((a,x)=>a+((((outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))===m)&&String(x.grupo)===g)?+x.cbs:0),0);\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  // No destination column here. This is the AS-IS matrix: every município in it still belongs to the\n  // closing sector, so showing where it is HEADED mixed the two states of the world in one table and\n  // made the ASIS view read like a plan. The destination lives on the map and in \"Municípios → setor\n  // destino\", which are about the to-be.\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\n// TOBE counterpart of the AS-IS matrix: the same município × grupo grid plus the two columns the AS-IS\n// view deliberately refuses to carry — where the município ends up, and under which líder.\n// \"Líder resultante\" is the líder holding the most CBs in that município. A município is served by several\n// grupos at once and the data names no single owner, so the dominant one is the honest answer and its\n// share is printed beside it instead of being hidden behind a single name.\nfunction renderMatrixTobe(){const mv=SESS.timeline[SESS.selected]; const el=$('#matrixTobe'); const nt=$('#tobeNote');\n  if(!el) return;\n  if(!mv||!mv.munis){ el.innerHTML=''; if(nt) nt.innerHTML=''; return; }\n  const c=computeDerived(mv);\n  const homeOf={}; c.leaders.forEach(L=>{ homeOf[L.grupo]=L.home; });\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // One pass over the matrix instead of a scan per cell: the AS-IS table can afford the naive version\n  // on six municípios, but this one asks for the dominant líder too, which multiplies the lookups.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cellBy={}; mv.matrix.forEach(x=>{ const k=(outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))+'|'+String(x.grupo);\n    cellBy[k]=(cellBy[k]||0)+ +x.cbs; });\n  const cell=(m,g)=>cellBy[m+'|'+g]||0;\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  const domLeader=m=>{ let best=null,bc=0;\n    mv.leaders.forEach(L=>{ const v=cell(m,L.grupo); if(v>bc){ bc=v; best=L; } });\n    return best?{L:best,cbs:bc}:null; };\n  const short=L=>(L.lider||'').split(' ').slice(0,2).join(' ')||('grupo '+L.grupo);\n  // NOT named `rows` — that is the module-level Databricks response helper, and shadowing it inside a\n  // render function is a trap for whoever adds a query here later.\n  const recs=ms.map(m=>{ const d=mv.assign[m]||null, dl=domLeader(m);\n    const home=dl?(homeOf[dl.L.grupo]||null):null;\n    // The mismatch worth seeing: the territory goes to one setor while the líder serving it ends up in\n    // another. Under \"manter vínculo\" those CBs stay with her, which is exactly what \"CBs fora do setor\"\n    // counts — so this column is where that KPI becomes a list of names.\n    return {m:m,d:d,dl:dl,home:home,off:!!(dl&&home&&d&&home!==d)}; });\n  el.innerHTML=`<thead><tr><th>Município</th><th>→ Setor</th><th>Líder resultante</th>${mv.leaders.map(L=>`<th title=\"${esc(L.lider||'')}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +recs.map(r=>`<tr><td>${r.m}</td>`\n     +`<td>${r.d?`<span class=\"home\" style=\"background:${mv.color[r.d]}\"></span>${r.d}`:'–'}</td>`\n     +`<td${r.off?' style=\"color:var(--closing)\"':''} title=\"${r.dl?esc(short(r.dl.L)+' · grupo '+r.dl.L.grupo+' · '+fmt(r.dl.cbs)+' CBs neste município'+(r.home?' · termina no setor '+r.home:' · sem setor de destino')):'nenhuma CB neste município'}\">`\n       +(r.dl?`${esc(short(r.dl.L))} <span class=\"badge\">${r.home||'sem setor'}</span>${r.off?' ⚠':''}`:'–')\n     +`</td>`\n     +mv.leaders.map(L=>{const v=cell(r.m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[r.m].cbs)}</td></tr>`).join('')+`</tbody>`;\n  if(nt){\n    const offN=recs.filter(r=>r.off).length;\n    nt.innerHTML=(mv.policy!=='fora')\n      ? `Política <b>quebrar relação</b>: as CBs deixam a líder atual e passam à estrutura do setor destino — a coluna mostra quem as atende <b>hoje</b>, não quem assume depois.`\n      : `<b>Líder resultante</b> = a líder com mais CBs no município; o chip é o setor onde ela termina. `\n        +(offN?`<span style=\"color:var(--closing)\">${fmt(offN)} município(s) ⚠</span> vão para um setor diferente do da sua líder — as CBs seguem a líder (é disso que o KPI “CBs fora do setor” é feito).`\n          :`Em todos os municípios a líder termina no mesmo setor que recebe o território.`);\n  }\n}\nfunction renderPolicy(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  $('#policyNote').innerHTML=mv.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutro setor</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder do setor destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  mv._plan=ms.map(m=>({municipio:mName(m),uf:mUf(m),cbs:mv.munis[m].cbs,destino:mv.assign[m]}));\n  $('#plan').innerHTML=`Etapa ${SESS.selected+1} · Setor <b>${mv.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${mv.color[mv.assign[m]]}\">${mv.assign[m]||'–'}</b>`).join(' · ')}<br>\n   Política: <b>${mv.policy==='fora'?'manter vínculo (CBs fora do setor)':'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas nesta etapa.`\n   +(c.orphanTotal?`<br>Fora da base de dominantes: ${fmt(c.orphanTotal)} CB(s) seguem suas líderes — ${mv.dest.filter(s=>c.orph[s]).map(s=>`${s} (${fmt(c.orph[s])})`).join(' · ')||'–'}${c.orphanStranded?` · <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} sem líder com destino</b>`:''}. Não entram no CSV, que é por município.`:'');\n}\nfunction renderReport(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  mv.metric=mv.metric||'cbs';\n  const KEYS=METRICS_DEF.map(m=>m[0]);\n  const incBy={};mv.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incBy[s])return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  // orphan CBs land on their leader's home sector, not on a geographic destination\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home&&incBy[L.home]) KEYS.forEach(k=>incBy[L.home][k]+=om[k]||0); });\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  // they leave the closing sector too, so antes → depois still balances\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home) KEYS.forEach(k=>closeTot[k]+=om[k]||0); });\n  const trend=mv.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=mv.dest.map(s=>({label:s,color:mv.color[s],antes:+(mv.baseMetrics[s]?.[mk]||0),depois:+(mv.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:mv.sector+'✕',color:'#ff3b30',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#34c759':'#ff3b30'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois desta etapa</h2>\n      <select id=\"metricSel\">${METRICS_DEF.map(m=>`<option value=\"${m[0]}\" ${m[0]===mv.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes desta etapa</b> (já incluindo etapas anteriores) · barra cheia = <b>depois</b> · <b style=\"color:var(--acc)\">${mv.sector}✕</b> = setor fechado nesta etapa · % = variação</div>\n    ${chartHTML(mv.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por setor receptor (snapshot congelado)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${METRICS_DEF.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const b=mv.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      METRICS_DEF.map(([k])=>{const bv=+b[k]||0,av=bv+(inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=METRICS_DEF.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${mv.sector} (sai)</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};mv.dest.forEach(s=>incLvl[s]={});\n  Object.keys(mv.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incLvl[s])return;const lv=mv.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  c.leaders.forEach(L=>{ const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n    if(ol&&L.home&&incLvl[L.home]) Object.entries(ol).forEach(([nv,n])=>incLvl[L.home][nv]=(incLvl[L.home][nv]||0)+n); });\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por setor receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const base=mv.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> desta etapa · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${mv.sector}.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${mv.sector} — últimos ${trend.length} ciclos até o snapshot congelado</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Tendência histórica <b>do próprio setor</b> — não ajustada pela absorção hipotética de etapas anteriores (limitação conhecida: tendência é por setor, não decomponível por município).</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{mv.metric=sel.value; renderReport();};\n}\nfunction renderCumulative(){\n  const el=$('#tab-cumulative'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<div class=\"note\">Nenhuma etapa na timeline ainda.</div>'; return; }\n  const ready=SESS.timeline.every(mv=>mv.munis);\n  if(!ready){ el.innerHTML='<div class=\"note\">Aguardando etapas terminarem de calcular…</div>'; return; }\n  const finalWorld=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const openSectors=[...new Set(Object.values(finalWorld.muniOverride))];\n  let h=`<h2 style=\"margin:0 0 8px\">Linha do tempo — ${SESS.timeline.length} fechamento(s)</h2>\n    <div class=\"note\" style=\"margin:-2px 0 10px\">Setores fechados, em ordem: ${SESS.timeline.map(mv=>mv.sector).join(' → ')}. Cada linha abaixo soma o efeito de <b>toda a cadeia</b>, não apenas da última etapa.</div>`;\n  h+=`<div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th>${METRICS_DEF.map(([,l])=>`<th>${l}</th>`).join('')}</tr></thead><tbody>`;\n  SESS.timeline.forEach((mv,i)=>{\n    h+=`<tr style=\"opacity:.75\"><td>${mv.sector}</td><td>fechado (etapa ${i+1})</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(mv.pureOwnMetrics?.[k]||0)}</td>`).join('')+`</tr>`;\n  });\n  openSectors.forEach(s=>{\n    const base=SESS.sectorBaseCache[s]||{};\n    const infl=finalWorld.sectorInflow[s]||{};\n    h+=`<tr><td>${s}</td><td>recebeu território</td>`+METRICS_DEF.map(([k,,t])=>{\n      const bv=+base[k]||0, av=bv+(infl[k]||0); const d=bv?((av-bv)/bv*100):0; const f=(t==='$'?money:fmt);\n      return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;\n    }).join('')+`</tr>`;\n  });\n  h+=`</tbody></table></div><div class=\"note\">\"Antes\" de um setor receptor é o snapshot congelado (sem nenhum fechamento aplicado); \"depois\" soma tudo que a cadeia inteira jogou pra ele.</div>`;\n  el.innerHTML=h;\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=700,H=500,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\n// ---------- label placement ----------\n// Pole of inaccessibility: the interior point farthest from ANY boundary of the dissolved area. A\n// centroid is only a mean — on a concave sector, one split across a river, or a member whose vertex\n// average falls in a bay, it lands outside the very territory it is supposed to name, which is what\n// pushed the sector numbers off their own shapes. Even-odd across every ring of every member is a\n// correct union test here because municipalities tile rather than overlap.\n// The search box is clipped to the viewBox, so the result is inside the territory AND on screen.\n// Rings are decimated first — this is a label, not a measurement — which keeps a 20-município sector\n// to a few million ops, and the result is cached per stage: geometry never changes between renders,\n// only the fill does.\nfunction labelPointFor(fs,pr){\n  const rings=[];\n  (fs||[]).forEach(f=>{ if(!f||!f.geometry) return;\n    const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>{\n      if(!r||r.length<4) return;\n      const step=Math.max(1,Math.floor(r.length/120)),pts=[];\n      for(let i=0;i<r.length;i+=step) pts.push(pr(r[i]));\n      if(pts.length>3) rings.push(pts);\n    }));});\n  if(!rings.length) return null;\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  mnx=Math.max(mnx,2); mny=Math.max(mny,2); mxx=Math.min(mxx,698); mxy=Math.min(mxy,498);\n  if(!(mxx>mnx&&mxy>mny)) return null;\n  const inside=(x,y)=>{ let c=false;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      if(((a[1]>y)!==(b[1]>y)) && (x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])) c=!c; }\n    return c; };\n  const edgeDist=(x,y)=>{ let m=Infinity;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      const dx=b[0]-a[0],dy=b[1]-a[1],L=dx*dx+dy*dy;\n      let t=L?(((x-a[0])*dx+(y-a[1])*dy)/L):0; t=t<0?0:(t>1?1:t);\n      const ex=a[0]+t*dx-x,ey=a[1]+t*dy-y,d=ex*ex+ey*ey; if(d<m)m=d; }\n    return Math.sqrt(m); };\n  const W=mxx-mnx,H=mxy-mny,N=26;\n  let best=null,bd=-1;\n  for(let i=0;i<=N;i++) for(let j=0;j<=N;j++){\n    const x=mnx+W*i/N,y=mny+H*j/N;\n    if(!inside(x,y)) continue;\n    const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; }\n  }\n  if(!best) return null;\n  // Hill-climb off the coarse grid: the grid only has to land somewhere in the right lobe, the\n  // refinement finds the deep point inside it — halving the step whenever no neighbour improves.\n  let step=Math.max(W,H)/N/2;\n  for(let k=0;k<7;k++){\n    let moved=false;\n    for(const o of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]){\n      const x=best[0]+o[0]*step,y=best[1]+o[1]*step;\n      if(x<mnx||x>mxx||y<mny||y>mxy||!inside(x,y)) continue;\n      const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; moved=true; }\n    }\n    if(!moved) step/=2;\n  }\n  // The clearance travels with the point: it is the radius of the largest circle that fits inside the\n  // territory there, which is exactly the budget a caller needs to decide how much text will fit.\n  return {p:best,r:bd};\n}\nfunction labelPointOf(mv,key,fs,pr){\n  if(!mv._lbl) mv._lbl={};\n  if(!(key in mv._lbl)) mv._lbl[key]=labelPointFor(fs,pr);\n  return mv._lbl[key];\n}\nfunction drawMap(){\n  const mv=SESS.timeline[SESS.selected]; const svg=$('#map'); const ignNote=$('#mapIgnoredNote');\n  if(!mv){ svg.innerHTML=''; if(ignNote) ignNote.innerHTML=''; return; }\n  const c=computeDerived(mv);const NS='http://www.w3.org/2000/svg'; svg.innerHTML='';\n  const feats=[...mv.nbrFeat,...Object.values(mv.geoFeat)]; if(!feats.length) return;\n  const pr=project(feats);\n  const world=foldWorldState(SESS.timeline,SESS.selected);\n  // The context layer is ONE area per SECTOR, not one shape per município. The unit of this decision\n  // is the destination sector; a scatter of individually outlined municipalities carrying their own\n  // names invited exactly the wrong reading of the map. Members of a sector are filled AND stroked in\n  // the same colour, so the shared borders vanish and the group reads as a single dissolved\n  // territory — the IBGE shapes do not share vertices precisely enough to extract a clean union\n  // outline, and carrying a real polygon-clipping union for a context layer is not worth it.\n  // Labels are COLLECTED here and drawn last, after every path. Drawn inline they were buried by the\n  // next sector's fill, and placement cannot avoid a collision it has not seen yet.\n  const labels=[],placed=[];\n  const textW=(s,fs)=>String(s).length*fs*0.56;\n  // WCAG relative luminance of the fill a label sits on, so ink and halo can invert per label. \"Would\n  // white be better?\" has no single answer on this map: white reads on #5856d6 (L≈0.14) and disappears\n  // on #ff9500 and #34c759 (both L≈0.43, where black wins ~9:1 against white's ~2.2:1) and on every\n  // context fill, which is .32 opacity over white. Decided per label, never globally.\n  const lum=col=>{let h=String(col||'').replace('#','');\n    if(h.length===3) h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];\n    if(h.length!==6) return 1;\n    const c=[0,2,4].map(i=>parseInt(h.slice(i,i+2),16)/255)\n      .map(v=>v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4));\n    return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2];};\n  // Centred on (x,y) in BOTH axes. The old box hung off the first line's baseline, so every two-line\n  // label sat half its own height below the point it was supposed to mark.\n  const rectOf=(x,y,lines)=>{const w=Math.max.apply(null,lines.map(l=>textW(l.t,l.size)));\n    const h=lines.reduce((a,l)=>a+l.size*1.2,0);\n    return {x1:x-w/2-2.5,x2:x+w/2+2.5,y1:y-h/2-1.5,y2:y+h/2+1.5};};\n  const nbrBySec={};\n  mv.nbrFeat.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf);\n    const s=String((mv.nbrCurrentSector&&mv.nbrCurrentSector[nm])||'');\n    (nbrBySec[s]=nbrBySec[s]||[]).push(f);\n  });\n  Object.keys(nbrBySec).forEach(s=>{\n    const fs=nbrBySec[s];\n    const isDest=!!s&&mv.dest.includes(s);\n    const closedCtx=!!s&&world.closedSectors.has(s);\n    const col=isDest?(mv.color[s]||null):null;\n    fs.forEach(f=>{\n      const p=document.createElementNS(NS,'path');\n      p.setAttribute('d',pathFor(f,pr));\n      p.setAttribute('class','sect'+(isDest?'':(closedCtx?' closed-ctx':' plain')));\n      if(col){ p.setAttribute('fill',col); p.setAttribute('stroke',col); }\n      svg.appendChild(p);\n    });\n    if(!s) return;\n    // One number per sector, at the pole of inaccessibility of its DISSOLVED area — the whole group\n    // of members at once, not a representative member, so the number sits in the visual middle of the\n    // territory the operator actually sees.\n    const lp=labelPointOf(mv,'s:'+s,fs,pr);\n    // Context areas always read LIGHT — .sect paints dest colours at fill-opacity .32 over white, and\n    // plain/closed-ctx are near-white greys — so a sector number never wants white ink, whatever colour\n    // the sector itself was assigned.\n    if(lp&&lp.p) labels.push({anchor:lp.p,big:true,dark:false,lines:[{t:s,size:11.5,weight:700}]});\n  });\n  const asis=mv.view==='asis';\n  // The UF prefix is information only when the stage actually straddles a border. On a single-UF setor\n  // it is the same three characters repeated on every label — a large share of the clutter and none of\n  // the meaning. The hover tooltip carries the UF either way.\n  const showUf=new Set(Object.keys(mv.geoFeat).map(k=>mUf(k)).filter(Boolean)).size>1;\n  Object.entries(mv.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n    const fillCol=asis?CLOSE_COLOR:(mv.color[mv.assign[m]]||'#444');\n    p.setAttribute('class','muni');p.setAttribute('fill',fillCol);\n    if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[m]);if(m2.dest.length)m2.assign[m]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=m;});};\n    p.style.cursor=asis?'default':'pointer';\n    p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n      t.innerHTML=asis?`<b>${mDisp(m)}</b><br>${fmt(mv.munis[m]?.cbs||0)} CBs · setor ${mv.sector}`:`<b>${mDisp(m)}</b><br>${fmt(mv.munis[m]?.cbs||0)} CBs → ${mv.assign[m]||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n    p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n    // The name is never cut and never dropped. The pole of inaccessibility is still where the label\n    // WANTS to sit — the visual middle of the territory rather than a centroid that can fall outside a\n    // concave shape — but its clearance no longer gates anything: cutting BA-SAO SEBASTIAO DO PASSE\n    // down to BA-SAO SEBAST saved space by destroying the one thing the label existed to convey, and\n    // measuring the name against the inscribed circle rejected the centre for nearly every município.\n    const lp=labelPointOf(mv,'m:'+m,[f],pr);\n    const at=(lp&&lp.p)||cxy(f,pr);\n    const nmFull=(showUf&&mUf(m)?mUf(m)+'-':'')+mName(m);\n    const num=fmt(mv.munis[m]?.cbs||0)+(asis?'':'·'+(mv.assign[m]||'–'));\n    labels.push({anchor:at,cbs:+(mv.munis[m]?.cbs||0),dark:lum(fillCol)<0.18,\n      lines:[{t:nmFull,size:8.2,weight:700},{t:num,size:7,weight:500,mut:true}]});\n  });\n  // Placement, not de-cluttering. Every label IS drawn: it starts at its anchor and, if the text does\n  // not fit the territory or the slot is already taken, walks outward ring by ring until it finds\n  // clear canvas, trailing a hairline leader back to the shape it names. The previous pass dropped the\n  // loser of every collision outright, which is why municípios silently had no name at all.\n  // Order still matters — sector numbers first, then municípios by CB weight — because the earlier a\n  // label is placed the closer to home it lands.\n  const CW=700,CH=500,CPAD=2;\n  const inBounds=r=>r.x1>=CPAD&&r.x2<=CW-CPAD&&r.y1>=CPAD&&r.y2<=CH-CPAD;\n  const hits=r=>placed.reduce((a,q)=>a+((r.x2<q.x1||r.x1>q.x2||r.y2<q.y1||r.y1>q.y2)?0:1),0);\n  // Ring 0 is the anchor itself. Rings step 9u out over 16 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs.\n  const CAND=[[0,0]];\n  for(let ring=1;ring<=7;ring++){ const d=ring*9;\n    for(let a=0;a<16;a++){ const th=a*Math.PI/8; CAND.push([Math.cos(th)*d*1.35,Math.sin(th)*d]); } }\n  labels.sort((a,b)=>(b.big?1:0)-(a.big?1:0)||((b.cbs||0)-(a.cbs||0)));\n  labels.forEach(L=>{\n    let chosen=null,best=null,bestN=Infinity;\n    // Centred on the anchor is ALWAYS the first choice. An earlier cut here refused the anchor whenever\n    // the text was wider than the município's inscribed circle — true of almost every real name at this\n    // scale (BA-SAO SEBASTIAO DO PASSE is ~140u against ~80u of clearance), so every single label got\n    // nudged outside and nothing ended up centred on anything. Spilling past a border is normal on a\n    // choropleth and the halo keeps it readable; only a genuine COLLISION earns a move.\n    for(const off of CAND){\n      const x=L.anchor[0]+off[0], y=L.anchor[1]+off[1];\n      const r=rectOf(x,y,L.lines);\n      if(!inBounds(r)) continue;\n      const n=hits(r);\n      if(!n){ chosen={x,y,r,off}; break; }\n      // No clear slot yet — remember the LEAST crowded one. Falling back to the first in-bounds\n      // candidate meant falling back to the anchor every time, so a crowded map piled every loser onto\n      // the same point. CAND is ordered nearest-first and the compare is strict, so ties keep the\n      // closest slot.\n      if(n<bestN){ bestN=n; best={x,y,r,off}; }\n    }\n    // Canvas genuinely full: take the nearest in-bounds slot and accept the overlap. A label sitting\n    // on top of another is recoverable by eye; a name that was never drawn is not.\n    if(!chosen) chosen=best||{x:L.anchor[0],y:L.anchor[1],r:rectOf(L.anchor[0],L.anchor[1],L.lines),off:[0,0]};\n    placed.push(chosen.r);\n    const g=document.createElementNS(NS,'g'); g.setAttribute('class','lbl'+(L.big?' big':''));\n    if(Math.abs(chosen.off[0])>0.5||Math.abs(chosen.off[1])>0.5){\n      // The leader stops at the label's edge rather than its centre, so it never runs under the glyphs.\n      const cx=(chosen.r.x1+chosen.r.x2)/2, cy=(chosen.r.y1+chosen.r.y2)/2;\n      const hw=(chosen.r.x2-chosen.r.x1)/2, hh=(chosen.r.y2-chosen.r.y1)/2;\n      const dx=L.anchor[0]-cx, dy=L.anchor[1]-cy;\n      const tt=Math.min(dx?hw/Math.abs(dx):Infinity, dy?hh/Math.abs(dy):Infinity);\n      const ln=document.createElementNS(NS,'line'); ln.setAttribute('class','lead');\n      ln.style.stroke=L.dark?'#fff':'#1c1c1e';\n      ln.setAttribute('x1',L.anchor[0].toFixed(1)); ln.setAttribute('y1',L.anchor[1].toFixed(1));\n      ln.setAttribute('x2',(cx+dx*tt).toFixed(1)); ln.setAttribute('y2',(cy+dy*tt).toFixed(1));\n      g.appendChild(ln);\n    }\n    const hTot=L.lines.reduce((a,l)=>a+l.size*1.2,0);\n    let ty=chosen.y-hTot/2+L.lines[0].size*0.86;\n    // Inline style rather than a class: a stylesheet rule would outrank a presentation attribute and\n    // undo the per-label contrast decision made above.\n    const ink=L.dark?'#fff':'#1c1c1e', inkMut=L.dark?'rgba(255,255,255,.85)':'#48484d';\n    const halo=L.dark?'rgba(0,0,0,.6)':'#fff';\n    L.lines.forEach(l=>{ const t=document.createElementNS(NS,'text');\n      t.setAttribute('x',chosen.x.toFixed(1)); t.setAttribute('y',ty.toFixed(1));\n      t.setAttribute('font-size',l.size); t.setAttribute('font-weight',l.weight||600);\n      t.style.fill=l.mut?inkMut:ink; t.style.stroke=halo;\n      t.textContent=l.t; g.appendChild(t); ty+=l.size*1.2; });\n    svg.appendChild(g);\n  });\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>Setor ${mv.sector} (estrutura antes desta etapa — ${Object.keys(mv.geoFeat).length} municípios)</span>`\n    // No \"vizinhos (contexto)\" or \"setor já fechado antes\" entry any more: every feature still drawn\n    // belongs to a destination sector, so .sect.plain and .sect.closed-ctx can no longer be produced\n    // and a legend key for them would point at nothing on the map.\n    :mv.dest.map(s=>`<span><b style=\"background:${mv.color[s]}\"></b>Setor ${s} — ${c.gained[s]} líder(es)</span>`).join('');\n  // Municipalities outside the dominance base are deliberately NOT drawn — no shape, no hover, no\n  // legend entry. They are reported in the panel instead, so the map only ever shows territory that\n  // is actually being reallocated.\n  if(ignNote) ignNote.innerHTML=(mv.excludedMunis&&mv.excludedMunis.length)\n    ? `<b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs||0)} CBs</b> fora da base de dominantes deste setor não são desenhados no mapa — as CBs seguem a líder do seu grupo (detalhe em “Municípios → setor destino”).`\n    : '';\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{ await ensurePinned(); const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nfunction renderAll(){\n  renderStage();\n  renderTimeline(); renderSectorPicker();\n  const shb=$('#openShare'); if(shb) shb.disabled=!SESS.timeline.length;\n  const dpb=$('#dupSession'); if(dpb) dpb.disabled=!SESS.timeline.length;\n  const mv=SESS.timeline[SESS.selected];\n  // resolveSnapshotPin falls back to a live sub-select when it cannot pin a literal; saying\n  // \"congelado\" in that case would be a lie, so the pill reports what is actually in the SQL.\n  const pinned=SESS.snapLiteral && !/^\\(SELECT/i.test(SESS.snapLiteral);\n  $('#liveState').textContent = SESS.snapshotRaw\n    ? (pinned?'● snapshot congelado · ':'◍ snapshot ao vivo (não fixado) · ')+'atualizado em '+fmtSyncedAt(SESS.snapshotSyncedAt)+' · ↻'\n    : '● iniciando';\n  const mp=$('#basePill');\n  if(mp){\n    const warn=[];\n    if(DOM.bySetor&&!DOM.hasUf) warn.push('⚠ sem coluna UF');\n    if(DOM.badUf) warn.push('⚠ '+fmt(DOM.badUf)+' UF não reconhecida(s)');\n    if(DOM.clashes) warn.push('⚠ '+fmt(DOM.clashes)+' homônimo(s) no mesmo setor');\n    mp.textContent=DOM.bySetor?('◇ base dominantes: '+fmt(DOM.setores)+' setores · '+fmt(DOM.rows)+' municípios'+(warn.length?' · '+warn.join(' · '):'')):(DOM.error?'◇ base dominantes: indisponível':'◇ base dominantes: carregando…');\n    mp.title=DOM.error||(DOM.bySetor?((DOM.hasUf\n        ?('Coluna de UF usada: \"'+DOM.ufCol+'\".')\n        :('SEM coluna de UF. Colunas lidas: '+((DOM.headers||[]).join(' | ')||'?')+'. A UF de cada município vem desta base — sem ela, homônimos voltam a ser casados só pelo nome.'))\n      +(DOM.badUf?('\\n'+DOM.badUf+' linha(s) com UF irreconhecível — tratadas como desconhecida.'):'')\n      +(DOM.clashes?('\\n'+DOM.clashes+' município(s) aparecem no mesmo setor com UFs diferentes. O SQL agrupa só por DS_MUNICIPIO, então esses chegam somados numa linha única e não há como separá-los.'):'')\n      ):'')||DOM.url||''; }\n  const rp=$('#rulePill'); if(rp) rp.textContent='◈ destino: '+ruleShort(SESS.settings);\n  const sp=$('#spanPill'); if(sp) sp.textContent='◎ span: '+spanShort(SESS.settings);\n  renderRuleNote(); renderSpanRule();\n  if(!mv){ ['#map','#matrix','#matrixTobe','#tobeNote','#leaders'].forEach(s=>{const el=$(s);if(el)el.innerHTML='';}); $('#kpis').innerHTML=''; $('#spans').innerHTML=''; $('#muniList').innerHTML=''; $('#plan').innerHTML=''; return; }\n  $('#mapTitle').textContent=`Etapa ${SESS.selected+1}/${SESS.timeline.length} — fechando setor ${mv.sector}`;\n  $('#viewToggle').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.v===mv.view));\n  $('#policy').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===mv.policy));\n  $('#optBadge').textContent=mv.optBadge||'';\n  drawMap(); renderContig(); renderSpanRule(); renderKpis(); renderSpans(); renderMuni(); renderLeaders(); renderMatrix(); renderMatrixTobe(); renderPolicy(); renderPlan(); renderReport(); renderCumulative();\n}\n\n// ---------- event wiring ----------\n$('#go').onclick=()=>addMovement($('#sectorPick').value);\n$('#confirmSnap').onclick=()=>{ SESS.stage='main'; renderAll(); };\n$('#rand').onclick=()=>{\n  const closed=new Set(SESS.timeline.map(mv=>mv.sector));\n  const pool=SESS.sectorsList.filter(s=>!closed.has(s.setor));\n  const cand=pool.filter(s=>s.munis>=3&&s.munis<=10&&s.cbs>300);\n  const list=cand.length?cand:pool; if(!list.length) return;\n  const pick=list[Math.floor(Math.random()*list.length)];\n  $('#sectorPick').value=pick.setor;\n  addMovement(pick.setor);\n};\n$('#sectorPick').addEventListener('keydown',e=>{ if(e.key==='Enter') addMovement($('#sectorPick').value); });\n$('#confirmSessionPick').onchange=()=>loadSession($('#confirmSessionPick').value);\n$('#optim').onclick=()=>editSelected(mv=>{ mv.assign=optimizeFor(mv); });\n$('#autoTobe').onclick=()=>editSelected(mv=>{ mv.assign=autoAssignFor(mv); mv.optBadge='TOBE por adjacência'; });\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.view=b.dataset.v;renderAll();});\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.policy=b.dataset.p;renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','tobe','report','cumulative','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;navigator.clipboard.writeText(JSON.stringify({setor:mv.sector,politica:mv.policy,plano:mv._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;const csv='municipio,uf,cbs,setor_destino\\n'+mv._plan.map(p=>`${p.municipio},${p.uf||''},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${mv.sector}.csv`;a.click();};\n$('#saveSession').onclick=saveSession;\n$('#dupSession').onclick=duplicateSession;\n$('#sessionPick').onchange=()=>loadSession($('#sessionPick').value);\n$('#liveState').onclick=refreshSnapshot;\n$('#liveState').addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); refreshSnapshot(); } });\n$('#openOptions').onclick=openOptions;\n$('#optClose').onclick=closeOptions;\n$('#optCancel').onclick=closeOptions;\n$('#optApply').onclick=applyOptions;\n$('#optMinCbs').addEventListener('input',renderOptPreview);\n['#optTargetSpan','#optMaxSpan','#optMinSpan'].forEach(id=>$(id).addEventListener('input',renderOptPreview));\nroot.querySelectorAll('input[name=\"destRule\"]').forEach(r=>r.addEventListener('change',renderOptPreview));\n$('#optionsBg').addEventListener('click',e=>{ if(e.target.id==='optionsBg') closeOptions(); });\nroot.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeOptions(); closeShare(); } });\n\n$('#openShare').onclick=openShare;\n$('#shareX').onclick=closeShare;\n$('#shareCancel').onclick=closeShare;\n$('#shareGo').onclick=doShare;\n$('#shareCopy').onclick=()=>{ if(!SHARE.url) return; navigator.clipboard.writeText(SHARE.url);\n  const b=$('#shareCopy'); if(b){ b.textContent='copiado ✓'; setTimeout(()=>{ const x=$('#shareCopy'); if(x) x.textContent='Copiar link'; },1500); } };\n$('#shareOpen').onclick=()=>{ if(SHARE.url) window.open(SHARE.url,'_blank'); };\n$('#shareBg').addEventListener('click',e=>{ if(e.target.id==='shareBg') closeShare(); });\n\n// ---------- init ----------\n(async()=>{\n  loader(true,'congelando snapshot da sessão…');\n  try{\n    await Promise.all([ensurePinned(), loadSectors(), refreshSessionPicker()]);\n    SESS.stage='confirm';\n  }catch(e){ setError('erro init: '+e.message); }\n  finally{ loader(false); renderAll(); const rd=$('#ready'); if(rd) rd.setAttribute('data-loaded','1'); }\n})();\n\n  }\n}\n"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}},"dominantesCsvUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dominantesCsvUrl"}}}}}}}}}},{"_id":"6a7ca2f7572546f02874bc0e","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"RhYKiJbwSgOb93uEW9R5f","package":"natura-geomarketing","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst PALETTE=['#5856d6','#ff9500','#34c759','#af52de','#ff2d55','#a2845e'];\nconst CLOSE_COLOR='#8e8e93';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\nconst METRICS_DEF=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\nconst OPT_K=1.5;\nconst SESSIONS_DS='timeline-shared-sessionsDataSet';   // same table as SHARED_DS — see notes at listSessions()\n// Span defaults are the real operating band for a setor receptor, not placeholders: 2200 ≤ span ≤ 3500\n// with 2700 as the target. null meant \"sem limite\", so every new session started with the optimiser\n// balancing against nothing. Existing sessions are NOT touched — their persisted settings carry explicit\n// values that spread OVER these in openOptions/applyOptions, which is deliberate: their etapas were\n// computed under the old band and changing it silently would leave the stored result and the rule that\n// produced it out of sync.\nconst DEFAULT_SETTINGS={destRule:'any',destMinCbs:9,targetSpan:2700,maxSpan:3500,minSpan:2200};\nconst DOM_MEDIA_BASE='https://natura-geomarketing.data2.media/file/';\nconst DOM_CSV_FALLBACK=DOM_MEDIA_BASE+'yw_TQdp0QXEk6JsrliAqP'; \nconst numOrNull=v=>{const n=Math.round(+v);return (isFinite(n)&&n>0)?n:null;};\nconst spanShort=st=>{const p=[];if(st&&st.targetSpan)p.push('alvo '+st.targetSpan);if(st&&st.maxSpan)p.push('máx '+st.maxSpan);if(st&&st.minSpan)p.push('mín '+st.minSpan);return p.length?p.join(' · '):'livre';};\nconst ruleShort=st=>((st&&st.destRule)==='dominant')?('dominantes ≥'+((st&&st.destMinCbs)||DEFAULT_SETTINGS.destMinCbs)+' CBs'):'qualquer municipio';\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#34c759}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .mac-spacer{flex:1}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  .pill.live{color:var(--ink);cursor:pointer;user-select:none;transition:border-color .1s,color .1s,background .1s}\n  .pill.live:hover{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  .pill.live:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .pill.live.busy{border-color:var(--acc);color:var(--acc);cursor:progress}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:5.5px 9px}\n  select:focus,input:focus{outline:2px solid rgba(0,122,255,.35);outline-offset:0;border-color:var(--acc)}\n  button.act{background:var(--acc);color:#fff;border:1px solid transparent;border-radius:7px;padding:6px 13px;font-weight:600;cursor:pointer;box-shadow:0 1px 1px rgba(0,0,0,.08)}\n  button.act:hover{background:var(--acc2)}\n  button.ghost{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5.5px 12px;cursor:pointer}\n  button.ghost:hover{background:var(--panel2)}\n  button.ghost.on{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  button.ghost:disabled,button.act:disabled{opacity:.4;cursor:not-allowed}\n  .sessbar{padding:9px 20px;background:var(--panel2);border-bottom:1px solid var(--line);display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n  .rail{padding:9px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:center;overflow-x:auto}\n  #movementChips{display:flex;gap:8px;flex:0 0 auto}\n  #addMovementGroup{display:flex;gap:8px;align-items:center;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--line)}\n  #addMovementGroup input{min-width:150px}\n  .tchip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px;flex:0 0 auto}\n  .tchip.on{border-color:var(--acc);background:#eaf3ff}\n  .tchip.stale{border-style:dashed;opacity:.85}\n  .tchip .tstale{color:#ff9500;font-weight:700}\n  .tchip .tnum{background:var(--ink);color:#fff;border-radius:99px;width:16px;height:16px;font-size:9.5px;display:flex;align-items:center;justify-content:center;font-weight:700}\n  .tchip .tsec{font-weight:600}\n  .tchip .tstat{color:var(--mut)}\n  .tchip .trm{color:var(--mut);font-weight:700;padding:0 3px}\n  .tchip .trm:hover{color:var(--closing)}\n  .rail .empty{color:var(--mut);font-size:12px}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 16px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:0 0 9px;font-weight:600}\n  /* user-select, not pointer-events. .lbl already sets pointer-events:none, which stops the text from\n     RECEIVING clicks but does not exclude it from the browser's selection range — so cycling a\n     município's destination with repeated clicks kept dragging a double-click selection across the\n     labels. Nothing drawn in an svg here is text anyone needs to copy. */\n  svg{width:100%;height:auto;display:block;user-select:none;-webkit-user-select:none}\n  /* White hairlines inside the closing sector: the municipios read as one territory cut into parts\n     rather than as separate outlined units, and the saturated fills carry the grouping instead of the\n     ink. Neighbour sectors keep their own coloured strokes (.sect) — that contrast is what separates\n     what is being reallocated from what is only context. Selection no longer draws an outline; the\n     white border was the click affordance AND the default is now white, so it had nothing left to say. */\n  .muni{stroke:#fff;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.15)}\n  .sect{stroke-width:1;fill-opacity:.32;stroke-opacity:.6}\n  .sect.plain{fill:#e8e8ec;stroke:#d4d4d9;fill-opacity:1;stroke-opacity:1}\n  .sect.closed-ctx{fill:#e2e2e7;stroke:#aeaeb2;stroke-dasharray:3 2;fill-opacity:1;stroke-opacity:1}\n  .lbl{pointer-events:none}\n  /* No plate behind the text: a halo painted UNDER the glyphs (paint-order) keeps a name legible on any\n     fill without stamping an opaque box over the territory it names. Ink and halo are chosen per label\n     from the luminance of the fill underneath, so they are deliberately NOT declared here — a\n     stylesheet rule outranks a presentation attribute and would silently override that choice. */\n  .lbl text{text-anchor:middle;paint-order:stroke fill;stroke-width:2.2px;stroke-linejoin:round;stroke-linecap:round}\n  .lbl.big text{stroke-width:2.9px}\n  .lbl line.lead{stroke-opacity:.5;stroke-width:.8}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:19px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#ffd1cc}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bdeecb}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:600;font-size:12px}\n  .bar-track{flex:1;height:18px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex;position:relative}\n  .bar-mark{position:absolute;top:0;bottom:0;width:2px;opacity:.9;z-index:2}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  .toggle{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:12px}\n  .toggle button{background:transparent;border:0;color:var(--mut);padding:6px 10px;cursor:pointer;font-weight:600}\n  .toggle button.on{background:var(--panel2);color:var(--ink)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .lider-foot td{background:var(--panel2);font-weight:600;font-size:11px;color:var(--ink);border-bottom:2px solid var(--line);padding:5px 6px;text-align:left}\n  .scroll{max-height:240px;overflow:auto}\n  /* Tables meant to be READ in full rather than sampled. Only the vertical cap is dropped — overflow\n     stays auto so the wide grupo grid can still scroll sideways inside its card, which is the one axis\n     that genuinely cannot fit. */\n  .scroll.grow{max-height:none}\n  .tip{position:fixed;pointer-events:none;background:#1c1c1e;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}\n  .tabs button{background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600}\n  .tabs button.on{background:var(--panel2);color:var(--ink);border-color:var(--acc)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(245,245,247,.85);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:20;font-size:13px;color:var(--acc);font-weight:600;text-align:center;padding:20px}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n  .sheet-bg{position:fixed;inset:0;background:rgba(28,28,30,.34);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:40;padding:24px}\n  .sheet-bg.hidden{display:none}\n  .sheet{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.22);width:min(580px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden}\n  .sheet-hd{display:flex;align-items:center;gap:9px;padding:12px 16px;border-bottom:1px solid var(--line)}\n  .sheet-hd h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-.1px}\n  .sheet-x{background:transparent;border:0;color:var(--mut);font-size:13px;cursor:pointer;padding:3px 7px;border-radius:6px}\n  .sheet-x:hover{background:var(--panel2);color:var(--ink)}\n  .sheet-bd{padding:14px 16px;overflow:auto}\n  .sheet-ft{display:flex;align-items:center;gap:8px;padding:11px 16px;border-top:1px solid var(--line);background:var(--panel2)}\n  .opt-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600}\n  .opt-sub{font-size:11.5px;color:var(--mut);margin:4px 0 10px;line-height:1.5}\n  .opt-row{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;cursor:pointer;background:var(--panel)}\n  .opt-row:hover{background:var(--panel2)}\n  .opt-row.on{border-color:var(--acc);background:#eaf3ff}\n  .opt-row input[type=radio]{margin:2px 0 0;accent-color:var(--acc);flex:0 0 auto}\n  .opt-t{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n  .opt-d{font-size:11.5px;color:var(--mut);margin-top:3px;line-height:1.55}\n  .opt-inline{font-size:11.5px;color:var(--mut);margin-top:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}\n  .opt-inline.off{opacity:.4;pointer-events:none}\n  .opt-note{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:9px 11px;font-size:11.5px;color:var(--mut);line-height:1.6;margin-top:12px}\n  .opt-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:2px}\n  .opt-fld{display:flex;flex-direction:column;gap:5px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;background:var(--panel2)}\n  .opt-fld span{font-size:11px;color:var(--mut);font-weight:600;line-height:1.35}\n  .opt-fld i{font-style:normal;font-weight:400;opacity:.72}\n  .opt-fld input{width:100%;background:var(--panel)}\n  .opt-err{margin-top:9px;font-size:11.5px;color:var(--closing);font-weight:600}\n  .opt-hr{height:1px;background:var(--line);margin:17px 0 0}\n  .shareurl{margin-top:9px;padding:9px 10px;border-radius:8px;background:var(--panel);border:1px solid var(--acc);\n    font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--ink);word-break:break-all;user-select:all}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header id=\"topHeader\" class=\"hidden\">\n  <h1>Simulador de Timeline de Fechamentos</h1>\n  <span class=\"pill live\" id=\"liveState\" role=\"button\" tabindex=\"0\" title=\"Clique pra buscar o snapshot mais recente e recalcular a timeline inteira\">● iniciando</span>\n  <span class=\"pill ro\">read-only · nunca escreve</span>\n  <span class=\"pill\" id=\"basePill\"></span>\n  <span class=\"pill\" id=\"rulePill\"></span>\n  <span class=\"pill\" id=\"spanPill\"></span>\n  <span class=\"status\" id=\"ds\"></span>\n  <div class=\"mac-spacer\"></div>\n  <select id=\"sessionPick\" style=\"min-width:200px\"></select>\n  <button class=\"ghost\" id=\"openOptions\" title=\"Configurações desta sessão\">⚙ Opções</button>\n  <button class=\"act\" id=\"saveSession\">Salvar sessão</button>\n  <button class=\"ghost\" id=\"dupSession\" title=\"Cria uma cópia independente desta sessão — a original fica intacta e a cópia passa a ser a sessão ativa\">⧉ Duplicar</button>\n  <button class=\"ghost\" id=\"openShare\" title=\"Publicar uma cópia congelada desta sessão em modo leitura\">⇪ Compartilhar</button>\n  <span class=\"status\" id=\"sessStatus\"></span>\n</header>\n\n<div class=\"rail hidden\" id=\"timelineRail\">\n  <div id=\"movementChips\"></div>\n  <div id=\"addMovementGroup\">\n    <input id=\"sectorPick\" list=\"sectorPickList\" placeholder=\"código do setor\" style=\"min-width:150px\">\n    <datalist id=\"sectorPickList\"></datalist>\n    <button class=\"ghost\" id=\"rand\" title=\"Setor aleatório\">🎲</button>\n    <button class=\"act\" id=\"go\">Fechar setor ▶ (nova etapa)</button>\n  </div>\n</div>\n\n<div class=\"card\" id=\"confirmCard\" style=\"margin:14px 20px\">\n  <h2 style=\"margin:0 0 9px\">Confirmar snapshot antes de simular</h2>\n  <div class=\"note\" id=\"confirmBody\">congelando snapshot…</div>\n  <button class=\"act\" id=\"confirmSnap\" style=\"margin-top:12px\">Confirmar snapshot ▶ ir pra simulação</button>\n  <div class=\"note\" style=\"margin-top:16px\">ou continue uma sessão salva:</div>\n  <select id=\"confirmSessionPick\" style=\"min-width:260px;margin-top:4px\"></select>\n  <div class=\"status\" id=\"confirmStatus\" style=\"margin-top:8px\"></div>\n</div>\n\n<div class=\"howto hidden\" id=\"howtoBox\">\n  <b>Como usar &nbsp;→&nbsp;</b>\n  <span><b>1.</b> Escolha um setor pra <b>fechar</b> (ou 🎲) — isso cria a etapa 1 da timeline.</span>\n  <span><b>2.</b> Feche outro setor pra criar a etapa 2 — ela já considera pra onde a etapa 1 mandou cada município.</span>\n  <span><b>3.</b> Clique numa etapa (chip acima) pra revisitar/editar — editar uma etapa recalcula automaticamente todas as etapas seguintes.</span>\n  <span><b>4.</b> Veja o efeito acumulado da cadeia inteira na aba <b>Impacto acumulado</b>.</span>\n</div>\n<div class=\"wrap hidden\" id=\"mainWrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <h2 id=\"mapTitle\" style=\"margin:0\">Mapa — clique num município p/ trocar o setor destino</h2>\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura do setor antes desta etapa (já considerando etapas anteriores) · TOBE = após a redistribuição desta etapa\">\n          <button data-v=\"asis\">◧ ASIS (antes desta etapa)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado nesta etapa (cor = destino). Ao redor, cada área é um <b>setor vizinho inteiro</b> com o código no meio — não municípios soltos: cor = setor destino, cinza = contexto, cinza tracejado = setor já fechado numa etapa anterior. Shapes reais IBGE/tbrugz.</div>\n      <div class=\"note\" id=\"mapIgnoredNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas (nesta etapa)</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor.</div>\n      <div class=\"scroll grow\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"tobe\">Matriz TOBE</button>\n        <button data-tab=\"report\">Relatório de impacto (etapa)</button>\n        <button data-tab=\"cumulative\">Impacto acumulado (timeline)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll grow\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-tobe\" class=\"hidden\"><div class=\"scroll grow\"><table id=\"matrixTobe\"></table></div>\n        <div class=\"note\" id=\"tobeNote\"></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-cumulative\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"note\" style=\"margin:-4px 0 8px\">Roda contra o snapshot congelado desta sessão (não \"ao vivo\") — mesma coerência da timeline.</div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa todas alocações contíguas e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (contíguo; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Editar esta etapa recalcula automaticamente as etapas seguintes da timeline.</div>\n      <div class=\"note\" id=\"ruleNote\" style=\"margin:-6px 0 11px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px\"></div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <div id=\"spanRule\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Municípios → setor destino</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração (etapa selecionada)</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"shareBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"shareTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"shareTitle\">Compartilhar sessão</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"shareX\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\" id=\"shareBody\"></div>\n    <div class=\"sheet-ft\">\n      <span class=\"status\" id=\"shareStatus\"></span>\n      <div class=\"mac-spacer\"></div>\n      <button class=\"ghost hidden\" id=\"shareCopy\">Copiar link</button>\n      <button class=\"ghost hidden\" id=\"shareOpen\">Abrir</button>\n      <button class=\"ghost\" id=\"shareCancel\">Cancelar</button>\n      <button class=\"act\" id=\"shareGo\">Compartilhar</button>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"optionsBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"optTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"optTitle\">Opções da sessão</h3><span class=\"badge\" id=\"optSessName\"></span><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"optClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Regra rígida — seleção dos setores destino</div>\n      <div class=\"opt-sub\">Define quais setores vizinhos podem entrar na lista de destinos de cada etapa. Vale pra toda a timeline desta sessão.</div>\n      <label class=\"opt-row\" data-r=\"any\">\n        <input type=\"radio\" name=\"destRule\" value=\"any\">\n        <div>\n          <div class=\"opt-t\">Adjacente a qualquer município <span class=\"badge\">padrão</span></div>\n          <div class=\"opt-d\">Um setor entra na lista de destinos se tiver município vizinho a <b>qualquer</b> município do setor fechado — inclusive os menores. É o comportamento de hoje.</div>\n        </div>\n      </label>\n      <label class=\"opt-row\" data-r=\"dominant\">\n        <input type=\"radio\" name=\"destRule\" value=\"dominant\">\n        <div>\n          <div class=\"opt-t\">Adjacente a municípios dominantes</div>\n          <div class=\"opt-d\">Só os municípios do setor fechado com <b>CBs suficientes</b> valem como ponte. Os pequenos continuam sendo redistribuídos normalmente — só não trazem setor novo pra lista de destinos.</div>\n          <div class=\"opt-inline\" id=\"optMinCbsWrap\">Ignorar municípios com menos de <input type=\"number\" id=\"optMinCbs\" min=\"1\" max=\"9999\" step=\"1\" style=\"width:74px\"> CBs</div>\n        </div>\n      </label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\">Pré-requisitos do setor destino — tamanho em CBs</div>\n      <div class=\"opt-sub\">Vale pro tamanho de cada setor receptor <b>depois</b> da redistribuição (base + entrantes). Em branco = desligado.</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhum setor acima)</i></span><input type=\"number\" id=\"optMaxSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhum setor abaixo)</i></span><input type=\"number\" id=\"optMinSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-err hidden\" id=\"optSpanErr\"></div>\n      <div class=\"opt-note\" id=\"optPreview\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"optStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"optCancel\">Cancelar</button><button class=\"act\" id=\"optApply\">Aplicar</button></div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div>\n<span id=\"ready\" style=\"display:none\"></span></div>`;\n  },\n  mounted(d2, root){\n\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\nconst blankM=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n// ---------- município identity ----------\n// Brazil has many municipalities sharing a name (Bom Jesus alone exists in PI, RN, RS, SC, PB, GO).\n// A stage loads the dominant UF PLUS every bordering UF, so those collisions land in the SAME\n// feature set routinely: keyed by name alone the last shape loaded won, the map could draw a\n// município from the wrong state, buildAdj merged two unrelated municipalities into one node, and\n// GROUP BY DS_MUNICIPIO summed two real municipalities into one row. The key is NAME/UF everywhere\n// — SQL rows, GeoJSON features, the dominantes base, assignments, persisted sessions. norm() maps\n// every non-alphanumeric to a space, so '/' can never occur inside a name and the split is exact.\n// A missing UF degrades to a bare NAME key rather than inventing one.\nconst mkey=(name,uf)=>{const n=norm(name),u=String(uf==null?'':uf).trim().toUpperCase();return u?n+'/'+u:n;};\nconst mName=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?s:s.slice(0,i);};\nconst mUf=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?'':s.slice(i+1);};\nconst mDisp=k=>{const u=mUf(k);return u?mName(k)+' ('+u+')':mName(k);};\n// The base CSV is the ONLY source of a município's UF now, so its column is accepted in whatever\n// shape the base is maintained in: sigla, IBGE numeric code, or full state name. Anything that does\n// not resolve to a real sigla is treated as UNKNOWN rather than passed through — a bogus UF would key\n// a município to a shape that does not exist and drop it off the map without a word.\nconst UF_OF_CODE={}; Object.keys(UF_CODE).forEach(k=>{ UF_OF_CODE[UF_CODE[k]]=k; });\nconst UF_OF_NAME={ACRE:'AC',ALAGOAS:'AL',AMAPA:'AP',AMAZONAS:'AM',BAHIA:'BA',CEARA:'CE','DISTRITO FEDERAL':'DF','ESPIRITO SANTO':'ES',GOIAS:'GO',MARANHAO:'MA','MATO GROSSO':'MT','MATO GROSSO DO SUL':'MS','MINAS GERAIS':'MG',PARA:'PA',PARAIBA:'PB',PARANA:'PR',PERNAMBUCO:'PE',PIAUI:'PI','RIO DE JANEIRO':'RJ','RIO GRANDE DO NORTE':'RN','RIO GRANDE DO SUL':'RS',RONDONIA:'RO',RORAIMA:'RR','SANTA CATARINA':'SC','SAO PAULO':'SP',SERGIPE:'SE',TOCANTINS:'TO'};\nconst ufNorm=v=>{const s=norm(v); if(!s) return '';\n  if(UF_CODE[s]) return s;\n  if(UF_OF_CODE[s]) return UF_OF_CODE[s];\n  return UF_OF_NAME[s]||'';};\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\nconst setError=(msg)=>{ const t=$('#ds'); if(t){t.textContent=msg;t.classList.add('err');} const c=$('#confirmStatus'); if(c){c.textContent=msg;c.classList.add('err');} };\n\n// ---------- base de municípios dominantes (CSV) ----------\n// WHICH municipalities belong to a sector is a business fact, not something to infer from CB volume:\n// it comes from a maintained CSV (one row per setor x município) linked through the `dominantesCsvUrl`\n// param. Anything the closing sector has CBs in but that the base does NOT assign to it is spillover:\n// not realocated, not drawn on the map, and its CBs follow their leader. Loaded once per URL and\n// cached for the whole session; a failure is thrown, never swallowed — a silent fallback here would\n// quietly change every number on the screen.\nconst DOM={url:null,promise:null,bySetor:null,ufBySetorMuni:null,rows:0,setores:0,hasUf:false,ufCol:null,badUf:0,clashes:0,headers:null,error:null};\nfunction parseCsv(text){\n  const out=[]; let row=[],cur='',q=false;\n  const push=()=>{ row.push(cur); cur=''; };\n  const endRow=()=>{ push(); if(row.length>1||row[0]!=='') out.push(row); row=[]; };\n  for(let i=0;i<text.length;i++){ const ch=text[i];\n    if(q){ if(ch==='\"'){ if(text[i+1]==='\"'){ cur+='\"'; i++; } else q=false; } else cur+=ch; }\n    else if(ch==='\"') q=true;\n    else if(ch===',') push();\n    else if(ch==='\\n') endRow();\n    else if(ch!=='\\r') cur+=ch;\n  }\n  if(cur!==''||row.length) endRow();\n  return out;\n}\n// `dominantesCsvUrl` is declared as a FileComponentUrlReference, so the instance points at the Drive\n// FILE, not at a URL literal — the Drive is policy-gated behind the app's auth strategy and a baked\n// string would be both stale and unauthorised. What the resolver hands back varies by where it is\n// resolved (URL string / file record / bare reference), so accept every shape instead of silently\n// reading undefined and falling through to the fallback.\n// The Drive is gated by the app's AuthenticationStrategy, so this read has to travel as the LOGGED-IN\n// USER — an anonymous CDN GET is exactly what the policy is there to refuse. `<system>.data2.media` is\n// only a cache in front of the SSP: on a cache miss it 302s to `<system>.data2apis.com`, which is where\n// the policy is actually evaluated.\n// COOKIES CANNOT CARRY THAT SESSION, which is why the previous `credentials:'include'` attempt could\n// never have worked: the app runs on <app>.<system>.data2.app and the SSP on <system>.data2apis.com —\n// cross-site — and the SSP answers `Access-Control-Allow-Origin: *` with no `Allow-Credentials`, so the\n// browser rejects a credentialed CORS read before it ever leaves the page. What the SSP actually reads\n// is `Authorization: Bearer <token>`: the runtime keeps that token in localStorage under\n// `__data2-session-<systemDomain>` (`__data2-session-data2` inside the builder preview) and replays it\n// on every dataset and rush call — same token, same header, same session, so the Drive policy sees the\n// logged-in user. Preflight passes because the SSP echoes whatever header the request asks for.\nfunction d2SessionToken(sysDomain){\n  const read=k=>{ try{ return (k&&window.localStorage.getItem(k))||''; }catch(e){ return ''; } };\n  return read(sysDomain?('__data2-session-'+sysDomain):'')||read('__data2-session-data2')||'';\n}\nfunction domCsvSystemDomain(url){\n  const m=/^https?:\\/\\/([^.\\/]+)\\.data2\\.(?:media|app)\\//i.exec(url)||/^https?:\\/\\/([^.\\/]+)\\.data2apis\\.com\\//i.exec(url);\n  return m?m[1]:'';\n}\nfunction domCsvCandidates(url){\n  const out=[];\n  const isMedia=/^https?:\\/\\/[^.\\/]+\\.data2\\.media\\//i.test(url);\n  const api=isMedia?url.replace(/^(https?:\\/\\/[^.\\/]+)\\.data2\\.media\\//i,'$1.data2apis.com/'):null;\n  const tok=d2SessionToken(domCsvSystemDomain(url));\n  const auth=tok?{authorization:'Bearer '+tok}:null;\n  // Authenticated origin read first — the only candidate a GATED Drive can actually answer.\n  if(api&&auth) out.push({url:api,headers:auth,label:'api+sessão'});\n  if(!api&&auth) out.push({url:url,headers:auth,label:'origem+sessão'});\n  // Anonymous last: only ever succeeds on an UNGATED Drive — or on a stale CDN cache hit, which is NOT\n  // proof the file is public (the origin keeps answering 401 once the edge entry expires).\n  if(api) out.push({url:api,headers:null,label:'api anônimo'});\n  out.push({url:url,headers:null,label:'origem anônima'});\n  return out;\n}\nfunction domCsvUrl(){\n  const pick=v=>{\n    if(!v) return '';\n    if(typeof v==='string') return v;\n    const q=v.params||{};\n    const ref=q.reference||v.reference;\n    return q.fileUrl||q.url||v.fileUrl||v.url||(ref?DOM_MEDIA_BASE+ref:'');\n  };\n  return String(pick(d2.param('dominantesCsvUrl'))||DOM_CSV_FALLBACK||'').trim();\n}\nasync function ensureDominantes(){\n  const url=domCsvUrl();\n  if(!url) throw new Error('base de municípios dominantes não configurada — ligue o arquivo CSV no parâmetro dominantesCsvUrl da instância do componente');\n  if(DOM.url===url && DOM.bySetor) return DOM.bySetor;\n  if(DOM.url===url && DOM.promise) return DOM.promise;\n  DOM.url=url; DOM.error=null;\n  DOM.promise=(async()=>{\n    // Self-diagnosing on purpose: every candidate reports its own outcome and the message names the\n    // session it was carrying. \"Unauthorized\" alone cannot distinguish \"not logged into THIS strategy\"\n    // from \"cookie not sent cross-site\" from \"param never bound, so it tried the fallback URL\" — and\n    // guessing between those from the outside costs a round trip each time.\n    let r=null; const tried=[];\n    for(const c of domCsvCandidates(url)){\n      let res=null;\n      try{ res=await fetch(c.url,{cache:'no-store',credentials:'omit',headers:c.headers||undefined}); }\n      catch(e){ tried.push(c.label+' → rede/CORS'); continue; }\n      if(res.ok){ r=res; break; }\n      tried.push(c.label+' → '+res.status);\n    }\n    if(!r){\n      let sess='sessão desconhecida';\n      try{\n        const a=d2.auth(AUTH_REF);\n        sess=(a&&a.isAuthed)?('logado como '+((a.user&&(a.user.email||a.user.nome))||'?')):('NÃO logado na estratégia '+AUTH_REF);\n      }catch(e){ sess='estratégia '+AUTH_REF+' não resolvida'; }\n      const tk=d2SessionToken(domCsvSystemDomain(url));\n      throw new Error('base de municípios dominantes: não foi possível ler o arquivo — '+sess+' · token de sessão '+(tk?'presente':'AUSENTE no localStorage')+'. Tentativas: '+tried.join(' · '));\n    }\n    const table=parseCsv(await r.text());\n    if(table.length<2) throw new Error('base de municípios dominantes vazia');\n    const head=table[0].map(h=>norm(h));\n    const iS=head.indexOf('CD SETOR'), iM=head.indexOf('MUNICIPIO');\n    if(iS<0||iM<0) throw new Error('base de municípios dominantes sem as colunas CD_SETOR / MUNICIPIO');\n    // CD_GV is the gerência de vendas the setor answers to, and it is the authority for the hard rule\n    // that a closing sector only ever hands territory to sectors of its OWN gerência. Its absence is\n    // fatal rather than a warning: with no GV there is no way to PROVE two sectors share one, and\n    // falling through unchecked is exactly the leak this column exists to close.\n    const iG=['CD GV','CDGV','GV','CD GERENCIA','CD GERENCIA VENDAS','CD GERENCIA DE VENDAS','GERENCIA DE VENDAS','GERENCIA'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iG==null) throw new Error('base de municípios dominantes sem a coluna CD_GV — a regra de gerência de vendas não pode ser aplicada. Colunas lidas: '+head.slice(0,40).join(', '));\n    // The UF column is what the whole keying now rests on — SQL no longer carries DS_ESTADO — so its\n    // absence is a first-class finding, not a footnote. The header actually matched is reported on the\n    // base pill, and when nothing matches the pill lists every column read, because guessing a header\n    // name from the outside costs a round trip each time.\n    const iU=['UF','SIGLA UF','SG UF','CD UF','ESTADO','DS ESTADO','SIGLA','ESTADO SIGLA','UF MUNICIPIO'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const by={},ufBy={},gvBy={}; let n=0,bad=0; const clash={},gvClash={};\n    for(let i=1;i<table.length;i++){\n      const line=table[i]; if(!line) continue;\n      const s=String(line[iS]==null?'':line[iS]).trim(), m=norm(line[iM]);\n      if(!s||!m) continue;\n      // One setor spans many rows (one per município) and every one of them repeats its CD_GV. Two\n      // DIFFERENT values for the same setor means the base itself disagrees about which gerência owns\n      // it — counted and reported, never keyed to whichever row happened to be read last.\n      const gvv=String(line[iG]==null?'':line[iG]).trim();\n      if(gvv){ if(gvBy[s]!==undefined && gvBy[s]!==gvv) gvClash[s]=true; else gvBy[s]=gvv; }\n      const raw=(iU==null)?'':line[iU];\n      const u=(iU==null)?'':ufNorm(raw);\n      if(iU!=null && String(raw==null?'':raw).trim() && !u) bad++;\n      // The same name twice in ONE setor under two UFs is the single case the reverted SQL cannot\n      // survive: mSQL groups by DS_MUNICIPIO alone, so both municipalities arrive summed into one row\n      // and no lookup can split them again. Counted and reported — never quietly keyed to whichever\n      // UF happened to be read last.\n      const kk=s+'|'+m;\n      // An UNRESOLVED uf must never outrank a resolved one. Reading a blank/bogus UF row first wrote ''\n      // here, and the next row carrying the real sigla was then rejected as a clash — so the key stayed\n      // falsy for good, ufFor() fell through, and the município degraded to a bare NAME key that matches\n      // no shape and no dominance entry. With DISTRITO/ZONA in the base a município spans several rows,\n      // so which row lands first is pure luck. A clash is only two RESOLVED siglas that disagree.\n      if(!u){ if(ufBy[kk]===undefined) ufBy[kk]=''; }\n      else if(!ufBy[kk]) ufBy[kk]=u;\n      else if(ufBy[kk]!==u) clash[kk]=true;\n      (by[s]=by[s]||new Set()).add(u?m+'/'+u:m); n++;\n    }\n    if(!n) throw new Error('base de municípios dominantes sem linhas válidas');\n    DOM.bySetor=by; DOM.ufBySetorMuni=ufBy; DOM.rows=n; DOM.setores=Object.keys(by).length;\n    DOM.gvBySetor=gvBy; DOM.gvClashes=Object.keys(gvClash).length; DOM.gvCol=String(table[0][iG]||'').trim();\n    DOM.gvSetores=Object.keys(gvBy).length;\n    DOM.hasUf=(iU!=null); DOM.ufCol=(iU==null?null:String(table[0][iU]||'').trim());\n    DOM.badUf=bad; DOM.clashes=Object.keys(clash).length; DOM.headers=head.slice(0,40); DOM.error=null;\n    return by;\n  })();\n  DOM.promise.catch(e=>{ DOM.promise=null; DOM.bySetor=null; DOM.error=e.message; renderAll(); });\n  return DOM.promise;\n}\n\nlet SESS={ snapshotRaw:null, snapshotSyncedAt:null, snapLiteral:null, eloMaxCycle:null, timeline:[], selected:0,\n           sessionId:null, sessionLabel:null, sessionsList:[], sectorsList:[], sectorBaseCache:{}, stage:'loading', dirtyFrom:null,\n           snapBusy:false, settings:{...DEFAULT_SETTINGS} };\n\nasync function q(sql){\n  let dbx,res;\n  try{\n    res=await d2.rush('AwW_11Nkg-dKksP7ic8V8').invoke({sql});\n  }catch(e){\n    throw new Error('Databricks indisponível (rush AwW_11Nkg-dKksP7ic8V8): '+e.message);\n  }\n  // The listener answers 200 with {sql} and NO `result` when its upstream probe times out: the\n  // handler builds `result: $result.data`, so an empty upstream simply drops the key rather than\n  // failing. Reading straight through to res.data.result.data reported \"Cannot read properties of\n  // undefined (reading 'data')\" — which reads like a bug in THIS component and sent a debugging\n  // session after the wrong thing. Name the layer that actually failed instead.\n  const payload=res&&res.data;\n  if(!payload) throw new Error('rush AwW_11Nkg-dKksP7ic8V8 não devolveu corpo — listener fora do ar?');\n  if(!payload.result) throw new Error('a sonda Databricks não respondeu: o listener AwW_11Nkg-dKksP7ic8V8 devolveu 200 sem `result`, o que normalmente é timeout do upstream (natura-x-app). O SQL não chegou a rodar.');\n  dbx=payload.result.data;\n  if(!dbx) throw new Error('sonda Databricks respondeu sem dados (result.data vazio).');\n  if(dbx.state && dbx.state!=='SUCCEEDED') throw new Error('Databricks '+dbx.state+': '+(dbx.error?.message||JSON.stringify(dbx.error)||'erro desconhecido'));\n  return dbx;\n}\nfunction rows(resp){const c=(resp.columns||[]).map(x=>x.name);\n  return (resp.rows||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\n// Stamps the UF onto every feature at load: the fetch is per-UF, so this is the ONLY point where a\n// shape's state is known for certain. Downstream the features are flattened into one array and the\n// origin would be unrecoverable. Persisted with the feature in saved sessions.\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);\n  const g=await r.json(); const U=String(uf).toUpperCase();\n  (g.features||[]).forEach(f=>{ f.__uf=U; });\n  return g;}\n\nfunction buildAdj(features){\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const feat={},idx={};\n  features.forEach(f=>{const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome,f.__uf);feat[nm]={f,pts:pts(f)};});\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  const nbrs=nm=>{const c={};feat[nm].pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    return Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));};\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\n// ---------- snapshot pinning ----------\nasync function resolveSnapshotPin(){\n  const [rawR,liveCountR,eloR]=await Promise.all([\n    q(`SELECT MAX(DT_HISTORICO) snap FROM ${T} WHERE CD_PAIS=1`).then(rows),\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`).then(rows),\n    q(`SELECT MAX(operational_cycle) mx FROM ${ELO} WHERE country_code=1`).then(rows).catch(()=>[]),\n  ]);\n  const raw=rawR[0]&&rawR[0].snap;\n  const liveCount=+((liveCountR[0]||{}).n||0);\n  const eloMax=+((eloR[0]||{}).mx||0);\n  const LIVE_SUBQ=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n  const dbTs=(raw||'').replace('T',' ').replace(/Z$/,'');\n  const candidates=[`TIMESTAMP'${dbTs}'`,`'${dbTs}'`,`DATE'${dbTs}'`];\n  const tests=await Promise.all(candidates.map(c=>\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=${c}`).then(rows).then(r=>+((r[0]||{}).n||0)).catch(()=>-1)\n  ));\n  let chosen=LIVE_SUBQ;\n  for(let i=0;i<candidates.length;i++){ if(liveCount>0 && tests[i]===liveCount){ chosen=candidates[i]; break; } }\n  return {snapshotRaw:raw, snapLiteral:chosen, eloMaxCycle:eloMax, syncedAt:new Date().toISOString()};\n}\n// DISPLAY CONTRACT: the UI never prints the snapshot's own DT_HISTORICO (snapshotRaw). What the user\n// sees is `snapshotSyncedAt` — the date AND hour at which THIS instance last pulled/pinned the\n// snapshot it is querying. snapshotRaw is still resolved, still drives every SQL literal, and is\n// still persisted by saveSession/doShare; it is only kept off the screen.\nfunction fmtSyncedAt(iso){\n  if(!iso) return '—';\n  const d=new Date(iso);\n  if(isNaN(d.getTime())) return String(iso);\n  return d.toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});\n}\nasync function ensurePinned(){\n  if(SESS.snapLiteral) return;\n  const p=await resolveSnapshotPin();\n  SESS.snapshotRaw=p.snapshotRaw; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n  SESS.snapshotSyncedAt=p.syncedAt;\n}\n\n// ---------- world-state fold ----------\nfunction accMetrics(base,add){\n  const out=base?{...base}:blankM();\n  Object.keys(blankM()).forEach(k=>{ out[k]=(out[k]||0)+((add&&add[k])||0); });\n  return out;\n}\nfunction foldWorldState(timeline,uptoIdx){\n  const closedSectors=new Set(),muniOverride={},muniGeo={},muniRow={},muniLevel={},sectorInflow={},sectorInflowLevel={},sectorOrphanRows={};\n  for(let k=0;k<uptoIdx;k++){\n    const mv=timeline[k]; if(!mv||!mv.sector||!mv.munis) continue;\n    closedSectors.add(String(mv.sector));\n    Object.keys(mv.munis).forEach(m=>{\n      if(m==='__OUTROS__') return;\n      const dest=mv.assign[m]; if(!dest) return;\n      muniOverride[m]=String(dest);\n      if(mv.geoFeat&&mv.geoFeat[m]) muniGeo[m]=mv.geoFeat[m];\n      const rws=(mv.matrix||[]).filter(r=>mkey(r.municipio,r.uf)===m);\n      muniRow[m]=(muniRow[m]||[]).concat(rws);\n      if(mv.levelByMuni&&mv.levelByMuni[m]) muniLevel[m]={...(muniLevel[m]||{}),...mv.levelByMuni[m]};\n      sectorInflow[dest]=accMetrics(sectorInflow[dest],mv.munis[m]);\n      const lv=(mv.levelByMuni&&mv.levelByMuni[m])||{};\n      sectorInflowLevel[dest]=sectorInflowLevel[dest]||{};\n      Object.entries(lv).forEach(([nv,c])=>{ sectorInflowLevel[dest][nv]=(sectorInflowLevel[dest][nv]||0)+c; });\n    });\n    // Orphan CBs follow their leader's home sector, so later etapas must inherit them the same way\n    // the allocated municipalities are inherited — otherwise a stage's span would not match the\n    // base carried into the next one.\n    if(mv.orphanMetricsByGroup && Object.keys(mv.orphanMetricsByGroup).length){\n      const d=computeDerived(mv);\n      d.leaders.forEach(L=>{\n        if(!L.home) return;\n        const om=mv.orphanMetricsByGroup[L.grupo];\n        if(om) sectorInflow[L.home]=accMetrics(sectorInflow[L.home],om);\n        // sectorInflow only reaches a sector while it is a DESTINATION (it lands in baseMetrics).\n        // Keep the rows as well, so the sector still carries these CBs on the day it is the one\n        // being closed — that read goes through mv.matrix, which baseMetrics never touches.\n        const orws=mv.orphanRowsByGroup&&mv.orphanRowsByGroup[L.grupo];\n        if(orws&&orws.length) sectorOrphanRows[L.home]=(sectorOrphanRows[L.home]||[]).concat(orws);\n        const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n        if(ol){ sectorInflowLevel[L.home]=sectorInflowLevel[L.home]||{};\n          Object.entries(ol).forEach(([nv,c])=>{ sectorInflowLevel[L.home][nv]=(sectorInflowLevel[L.home][nv]||0)+c; }); }\n      });\n    }\n  }\n  return {closedSectors,muniOverride,muniGeo,muniRow,muniLevel,sectorInflow,sectorInflowLevel,sectorOrphanRows};\n}\n\n// ---------- per-movement compute ----------\nfunction aggregateMovement(mv){\n  const munis={},leaders={};\n  const exSet=new Set((mv.excludedMunis||[]).map(x=>x.muni));\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  // Orphans = CBs in municipalities outside the sector's dominance base. They are NOT allocated\n  // geographically; they follow their own leader's home sector. Kept per leader group so every\n  // consumer (span, cascade, impact report) can route them the same way.\n  mv.orphanByGroup={}; mv.orphanMetricsByGroup={}; mv.orphanRowsByGroup={};\n  mv.matrix.forEach(r=>{\n    const m=mkey(r.municipio,r.uf);\n    r._out=!mv.geoFeat[m];   // assign, never just set: absorbed rows are reused across etapas\n    const mm=r._out?'__OUTROS__':m;\n    munis[mm]=munis[mm]||{...blankM(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n    if(r._out && exSet.has(m)){\n      mv.orphanByGroup[g]=(mv.orphanByGroup[g]||0)+ +r.cbs;\n      // The ROW too, not just the totals: when the leader's home sector is itself closed in a later\n      // etapa, that etapa rebuilds its matrix from SQL + absorbed municípios, and these CBs are in\n      // neither (their município never moved). Without the row they silently vanish from the sector\n      // they were just credited to — see sectorOrphanRows in foldWorldState.\n      (mv.orphanRowsByGroup[g]=mv.orphanRowsByGroup[g]||[]).push(r);\n      mv.orphanMetricsByGroup[g]=accMetrics(mv.orphanMetricsByGroup[g],\n        {cbs:+r.cbs,receita:+r.receita||0,rec_nat:+r.rec_nat||0,rec_avon:+r.rec_avon||0,pontos:+r.pontos||0,pedidos:+r.pedidos||0,ativas:+r.ativas||0});\n    }\n  });\n  // Level counts only exist per municipality, never per leader, so an excluded municipality's level\n  // mix is credited to the group holding most of its CBs (sub-threshold by definition, so usually one).\n  mv.orphanLevelByGroup={};\n  (mv.excludedMunis||[]).forEach(({muni})=>{\n    const lv=(mv.levelByMuni&&mv.levelByMuni[muni])||{}; if(!Object.keys(lv).length) return;\n    let bg=null,bc=-1;\n    mv.matrix.forEach(r=>{ if(mkey(r.municipio,r.uf)!==muni) return; const cc=+r.cbs; if(cc>bc){bc=cc;bg=String(r.grupo);} });\n    if(!bg) return;\n    mv.orphanLevelByGroup[bg]=mv.orphanLevelByGroup[bg]||{};\n    Object.entries(lv).forEach(([nv,cc])=>{ mv.orphanLevelByGroup[bg][nv]=(mv.orphanLevelByGroup[bg][nv]||0)+cc; });\n  });\n  mv.munis=munis; mv.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssignFor(mv){\n  const a={};\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=mv.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else if(mv.dest.length){ const c=mv.muniCentroid[m]; let best=mv.dest[0],bd=Infinity;\n      for(const s of mv.dest){ const sc=mv.secCentroid[s]; if(!sc||!c)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(mv,assign){\n  let d=0;\n  mv.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=mv.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,mv.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanMapFor(mv,a){const inc={};mv.dest.forEach(s=>inc[s]=0);\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=a[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  // Orphan CBs follow their leader's home sector, and that home depends on THIS candidate\n  // assignment — so they have to be counted here, or the optimizer would score and constrain a\n  // span that differs from the one the UI shows. Skipped entirely when there are no orphans.\n  if(mv.orphanByGroup&&Object.keys(mv.orphanByGroup).length){\n    mv.leaders.forEach(L=>{\n      const orphan=mv.orphanByGroup[L.grupo]||0; if(!orphan) return;\n      const byS={}; let tot=0;\n      for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s&&inc[s]!=null){ byS[s]=(byS[s]||0)+c; tot+=c; } }\n      if(!tot) return;   // no home to follow -> stranded, counted nowhere\n      const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n      if(inc[home]!=null) inc[home]+=orphan;\n    });\n  }\n  const sp={};mv.dest.forEach(s=>sp[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]);return sp;}\nfunction maxSpanOfFor(mv,a){return Math.max(...Object.values(spanMapFor(mv,a)),0);}\nfunction spanLimits(){const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  return {target:numOrNull(st.targetSpan),max:numOrNull(st.maxSpan),min:numOrNull(st.minSpan)};}\nfunction spanLimitsOn(lim){return !!(lim.target||lim.max||lim.min);}\nfunction spanViolationsOf(sp,lim){const over=[],under=[];\n  Object.entries(sp).forEach(([s,v])=>{ if(lim.max&&v>lim.max)over.push(s); if(lim.min&&v<lim.min)under.push(s); });\n  return {over,under};}\nfunction spanViolationsFor(mv,a){return spanViolationsOf(spanMapFor(mv,a),spanLimits());}\n// With a target span the objective becomes RMS distance from the ideal size (punishes too big AND\n// too small). With no target we keep the legacy objective: shave the biggest sector.\nfunction spanCostFor(mv,a){const lim=spanLimits(),vals=Object.values(spanMapFor(mv,a));\n  if(!vals.length) return 0;\n  if(lim.target) return Math.sqrt(vals.reduce((s,v)=>s+(v-lim.target)**2,0)/vals.length);\n  return Math.max(...vals);}\n// ---------- ÓTIMO: objetivo multi-critério ----------\n// Three normalised objectives, each 0..1, so the weights below are directly comparable. The old\n// score added a span cost measured in CBs to a desloc measured in CBs, so on a large sector the\n// balance term was numerically ~100x the desloc term and silently decided everything on its own.\n//   balance — RMS spread of the resulting spans (against the target span when one is set,\n//             otherwise against their own mean), over that reference. 0 = every destination\n//             sector ends up the same size.\n//   dist    — how far each município travels, CB-weighted: half from the share of its outer\n//             border that faces the sector it was given to, half from centroid distance. 0 =\n//             every município went to a sector it physically touches. Adjacency used to be a\n//             pure yes/no constraint (contiguity); this is what makes it a preference too.\n//   orphan  — stranded orphan CBs (leader left without a home sector, so they land nowhere at\n//             all) plus CBs split away from their leader's home, over the CBs at stake.\n//             Stranded ones count OPT_STRAND_K times: a stranded CB leaves the model entirely,\n//             a deslocated one merely changes hands.\nconst OPT_W={balance:1, dist:0.85, orphan:1};\nconst OPT_STRAND_K=3;\nconst OPT_SIMS=24;             // simultaneous simulations per run, scaled down on big stages\nconst OPT_PASSES=14;           // local-search sweeps inside one simulation\nconst OPT_EVAL_BUDGET=120000;  // total candidate evaluations across the whole portfolio\nconst OPT_EXHAUSTIVE_MAX=60000;\nfunction optDistPrep(mv){\n  let mx=0;\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__') return; const c=mv.muniCentroid[m]; if(!c) return;\n    mv.dest.forEach(s=>{ const sc=mv.secCentroid[s]; if(!sc) return;\n      const d=Math.hypot(c[0]-sc[0],c[1]-sc[1]); if(d>mx) mx=d; }); });\n  return mx||1;\n}\nfunction optDistCost(mv,m,s,dmax){\n  const v=mv.allowed[m]||{}; let tot=0; for(const k in v) tot+=v[k];\n  const share=tot?((v[s]||0)/tot):0;\n  const c=mv.muniCentroid[m], sc=mv.secCentroid[s];\n  // Missing centroid on either side is neutral (0.5), never 0 — an unmeasurable move must not\n  // score as the cheapest one on the board.\n  const geo=(c&&sc)?Math.min(1,Math.hypot(c[0]-sc[0],c[1]-sc[1])/dmax):0.5;\n  return 0.5*(1-share)+0.5*geo;\n}\nfunction optEval(mv,a,dmax){\n  const lim=spanLimits();\n  const sp=spanMapFor(mv,a), vals=Object.values(sp);\n  let balance=0;\n  if(vals.length){\n    const ref=lim.target||(vals.reduce((x,y)=>x+y,0)/vals.length);\n    if(ref) balance=Math.min(1,Math.sqrt(vals.reduce((s,v)=>s+(v-ref)**2,0)/vals.length)/ref);\n  }\n  let cbs=0,dist=0;\n  for(const m in a){ const o=mv.munis[m]; if(!o) continue; const w=+o.cbs||0; cbs+=w; dist+=w*optDistCost(mv,m,a[m],dmax); }\n  dist=cbs?(dist/cbs):0;\n  let stranded=0,desloc=0,tot=0;\n  mv.leaders.forEach(L=>{\n    const byS={}; let t=0;\n    for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s){ byS[s]=(byS[s]||0)+c; t+=c; } }\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    tot+=t+orphan;\n    if(!t){ stranded+=orphan; return; }   // no allocated CBs -> no home -> these orphans land nowhere\n    const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n    desloc+=t-(byS[home]||0);\n  });\n  const orphanTerm=tot?Math.min(1,(desloc+OPT_STRAND_K*stranded)/tot):0;\n  return { cost:OPT_W.balance*balance+OPT_W.dist*dist+OPT_W.orphan*orphanTerm,\n           balance, dist, desloc, stranded, sp, viol:spanViolationsOf(sp,lim) };\n}\nfunction optScoreFor(mv,a){ return optEval(mv,a,mv.__optDmax||optDistPrep(mv)).cost; }\n// Seeded PRNG on purpose. computeMovement runs the optimizer for EVERY stage and recomputeFrom\n// re-runs every stage downstream of an edit, so a Math.random() portfolio would hand back a\n// different allocation on each pass and a saved timeline would never reproduce.\nfunction optRng(seed){ let a=seed>>>0;\n  return ()=>{ a=(a+0x6D2B79F5)>>>0; let t=Math.imul(a^(a>>>15),1|a);\n    t=(t+Math.imul(t^(t>>>7),61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }\nfunction optSeed(mv,ms){ let h=2166136261>>>0;\n  const s=String(mv.sector)+'|'+ms.join(',')+'|'+mv.dest.join(',');\n  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);\n  return h>>>0; }\nfunction contigBadFor(mv,assign){\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(mv.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(mv.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\nfunction greedyFor(mv,ms,opts){const lim=spanLimits();const a={};\n  ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;\n    for(const s of opts[i]){a[m]=s;\n      // over-max only grows as we keep assigning, so it is safe to penalise mid-build;\n      // under-min shrinks, so penalising it here would just chase noise.\n      const over=lim.max?spanViolationsOf(spanMapFor(mv,a),{max:lim.max,min:null}).over.length:0;\n      const d=optScoreFor(mv,a)+(contigBadFor(mv,a).length?1e9:0)+over*1e6;\n      if(d<bd){bd=d;best=s;}}\n    a[m]=best;});\n  return a;}\nfunction optimizeFor(mv){\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__');\n  if(!ms.length||!mv.dest.length){ mv.optBadge=mv.dest.length?'':'sem setor destino identificado'; return {}; }\n  const opts=ms.map(m=>{const al=(mv.reachable[m]&&mv.reachable[m].length)?mv.reachable[m]:mv.dest.slice();return al.length?al:mv.dest.slice();});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  const lim=spanLimits();\n  const dmax=optDistPrep(mv); mv.__optDmax=dmax;\n\n  // Feasibility ladder — same meaning as before, because renderSpanRule and the saved sessions\n  // both read mv.optTier: 0 = contíguo + max + min span, 1 = contíguo + max span, 2 = contíguo\n  // only. Every candidate any simulation produces is offered to all three tiers; we ship the\n  // strictest tier that got a solution, so an unreachable min/max relaxes loudly instead of\n  // stranding the stage with nothing.\n  const tier=[null,null,null],tSc=[Infinity,Infinity,Infinity],tEv=[null,null,null];\n  let feasible=0,evals=0;\n  const offer=(a,ev)=>{\n    if(contigBadFor(mv,a).length) return false;\n    feasible++;\n    const put=t=>{ if(ev.cost<tSc[t]){ tSc[t]=ev.cost; tier[t]={...a}; tEv[t]=ev; } };\n    put(2);\n    if(!ev.viol.over.length){ put(1); if(!ev.viol.under.length) put(0); }\n    return true;\n  };\n  const evalOf=a=>{ evals++; return optEval(mv,a,dmax); };\n\n  // Small enough to settle the question outright: enumerate every contiguous allocation and the\n  // winner is provably the best one under this objective. No search needed, so none is run.\n  const exhaustive=combos<=OPT_EXHAUSTIVE_MAX;\n  let sims=0;\n  if(exhaustive){\n    const cur={};\n    const rec=i=>{ if(i===ms.length){ offer(cur,evalOf(cur)); return; }\n      for(const s of opts[i]){ cur[ms[i]]=s; rec(i+1); } };\n    rec(0);\n  } else {\n    // Too big to enumerate, so run a portfolio of independent simulations instead. Each one is a\n    // seed assignment, then hill-climbing on single-município reassignments, then one perturbation\n    // and another climb to escape whatever local optimum it settled into. Different seeds land in\n    // different basins; the ladder above keeps the best across all of them.\n    const improve=a=>{\n      let ev=evalOf(a); offer(a,ev);\n      for(let pass=0;pass<OPT_PASSES;pass++){\n        let moved=false;\n        for(let i=0;i<ms.length;i++){\n          const m=ms[i],cur=a[m]; let best=cur,bev=ev;\n          for(const s of opts[i]){\n            if(s===cur) continue;\n            a[m]=s;\n            const e=evalOf(a);\n            // contiguity is a hard constraint, and the cheaper test runs first on purpose\n            if(e.cost<bev.cost-1e-12 && !contigBadFor(mv,a).length){ best=s; bev=e; }\n          }\n          a[m]=best;\n          if(best!==cur){ ev=bev; moved=true; offer(a,ev); }\n        }\n        if(!moved) break;\n      }\n    };\n    const rng=optRng(optSeed(mv,ms));\n    const perSim=Math.max(1,ms.length*Math.max(1,mv.dest.length)*OPT_PASSES);\n    const budget=Math.max(4,Math.min(OPT_SIMS,Math.floor(OPT_EVAL_BUDGET/perSim)||4));\n    const seeds=[];\n    if(mv.assign&&ms.every(m=>mv.assign[m])) seeds.push({...mv.assign});   // never regress on what is already on screen\n    seeds.push(autoAssignFor(mv));                                        // adjacency-first seed\n    seeds.push(greedyFor(mv,ms,opts));                                    // balance-first seed\n    while(seeds.length<budget){\n      // randomised construction biased toward nearby sectors — the bias keeps each seed sane, the\n      // randomness is what sends the runs into different basins\n      const a={};\n      ms.forEach((m,i)=>{ let bs=opts[i][0],bw=-Infinity;\n        for(const s of opts[i]){ const w=(1-optDistCost(mv,m,s,dmax))+rng()*0.9; if(w>bw){bw=w;bs=s;} }\n        a[m]=bs; });\n      seeds.push(a);\n    }\n    seeds.forEach(seed=>{\n      const a={...seed};\n      // a restored or legacy assign can still name a sector this stage no longer offers\n      ms.forEach((m,i)=>{ if(!opts[i].includes(a[m])) a[m]=opts[i][0]; });\n      improve(a);\n      const kick=Math.max(1,Math.round(ms.length*0.25));\n      for(let k=0;k<kick;k++){ const i=Math.floor(rng()*ms.length); a[ms[i]]=opts[i][Math.floor(rng()*opts[i].length)]; }\n      improve(a);\n    });\n    sims=seeds.length;\n  }\n\n  const t=tier.findIndex(x=>x);\n  if(t<0){ mv.optTier=-1; mv.optBadge='sem solução contígua — heurística'; return greedyFor(mv,ms,opts); }\n  const best=tier[t], ev=tEv[t]; mv.optTier=t;\n  const relax=[];\n  if(t>0 && lim.min) relax.push('span mínimo '+fmt(lim.min));\n  if(t>1 && lim.max) relax.push('span máximo '+fmt(lim.max));\n  mv.optStats={balance:ev.balance,dist:ev.dist,desloc:ev.desloc,stranded:ev.stranded,sims,evals,feasible,combos,exhaustive};\n  mv.optBadge=`ótimo · ${exhaustive?`espaço completo (${fmt(combos)})`:`${fmt(sims)} simulações`}`\n    +` · equilíbrio ±${Math.round(ev.balance*100)}% · adjacência ${Math.round((1-ev.dist)*100)}%`\n    +` · quebra ${fmt(ev.desloc)} CBs · órfãs sem destino ${fmt(ev.stranded)}`\n    +` · pico ${fmt(maxSpanOfFor(mv,best))} CBs`\n    +(lim.target?` · alvo ${fmt(lim.target)}`:'')\n    +(relax.length?` · ⚠ inatingível: ${relax.join(' + ')}`:(spanLimitsOn(lim)?' · limites ok':''));\n  return best;\n}\nfunction computeDerived(mv){\n  const inc={},gained={},orph={};mv.dest.forEach(s=>{inc[s]=0;gained[s]=0;orph[s]=0;});\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  let desloc=0,orphanTotal=0,orphanStranded=0;\n  const L2=mv.leaders.map(L=>{const byS={};mv.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=mv.assign[m];if(s&&byS[s]!=null){byS[s]+=c;tot+=c;}}\n    // no allocated CBs => no home. Picking dest[0] by default would silently dump this leader's\n    // orphan CBs into an arbitrary sector, so we leave it null and report them as stranded.\n    const home=(tot>0&&mv.dest.length)?mv.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,mv.dest[0]):null;\n    const dl=tot-(home?byS[home]:0);desloc+=dl;if(home)gained[home]++;\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    orphanTotal+=orphan;\n    if(orphan){ if(home&&orph[home]!=null) orph[home]+=orphan; else orphanStranded+=orphan; }\n    return {...L,byS,home,desloc:dl,assignedTotal:tot,orphan,orphanHome:(orphan&&home)?home:null};});\n  const span={};mv.dest.forEach(s=>span[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]+orph[s]);\n  return {inc,gained,desloc,leaders:L2,span,orph,orphanTotal,orphanStranded};\n}\n\nasync function computeMovement(sec,world){\n  sec=String(sec);\n  const mv={sector:sec,policy:'fora',view:'tobe',selMuni:null,optBadge:''};\n  const SNAP=SESS.snapLiteral;\n  const mSQL=`SELECT DS_MUNICIPIO municipio, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n     COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n     ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n     SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP}\n    GROUP BY DS_MUNICIPIO, CD_GRUPO`;\n  const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n  const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n     COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n     ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n    FROM ${ELO} WHERE country_code=1 AND sector_code=${sec} AND operational_cycle<=${SESS.eloMaxCycle||999999} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n  const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n  const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=${sec} AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, DS_NVL_PLAN_CRESC`;\n  const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n\n  const ownMatrix=rows(mR).filter(r=>+r.cbs>0);\n  mv.pureOwnMetrics=blankM();\n  ownMatrix.forEach(r=>{ mv.pureOwnMetrics.cbs+=+r.cbs; mv.pureOwnMetrics.receita+=+r.receita||0; mv.pureOwnMetrics.rec_nat+=+r.rec_nat||0; mv.pureOwnMetrics.rec_avon+=+r.rec_avon||0; mv.pureOwnMetrics.pontos+=+r.pontos||0; mv.pureOwnMetrics.pedidos+=+r.pedidos||0; mv.pureOwnMetrics.ativas+=+r.ativas||0; });\n\n  mv.levelByMuni={};\n  Object.entries(world.muniLevel||{}).forEach(([m,lv])=>{ if(world.muniOverride[m]===sec) mv.levelByMuni[m]={...lv}; });\n  // keyed further down, once the UF resolver exists — see the stamp block after buildAdj.\n\n  mv.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n  mv.levels=rows(lR);\n\n  const absorbedRows=[];\n  Object.entries(world.muniOverride).forEach(([m,destSec])=>{ if(destSec===sec && world.muniRow[m]) absorbedRows.push(...world.muniRow[m]); });\n  // CBs an earlier etapa sent here by FOLLOWING THEIR LEADER (quebra de vínculo) carry no\n  // muniOverride — the município stayed where it was, only the CBs moved — so absorbedRows misses\n  // them. Leaving them out made this sector's ASIS total smaller than the span the previous etapa's\n  // TOBE promised for it, by exactly that etapa's \"CBs ignoradas\", and dropped those CBs from the\n  // redistribution as if they had never existed.\n  const inheritedOrphanRows=(world.sectorOrphanRows&&world.sectorOrphanRows[sec])||[];\n  mv.matrix=ownMatrix.concat(absorbedRows,inheritedOrphanRows);\n\n  const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n  const domUF=ufList[0]||'PR';\n  mv.domUF=domUF;\n  const ufs=[domUF,...(UF_BORDERS[domUF]||[])];\n  const geos=await Promise.all(ufs.map(u=>geoUF(u).catch(()=>null)));\n  const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n  const adj=buildAdj(allFeat);\n\n  // ---- UF resolution ----\n  // SQL no longer selects DS_ESTADO. The base CSV is the authority on a município's UF, indexed by the\n  // (setor, município) pair it already maintains, and rows are stamped ONCE here so every consumer\n  // downstream keeps reading r.uf exactly as it did before.\n  // Fallback for a município the base does not carry — spillover, where the sector has CBs but no\n  // dominance: adopt the UF of the loaded shape when that name is unique across the UFs in this\n  // window. A name shared by two of them stays unresolved instead of guessed; it is spillover either\n  // way, and a guess would put a shape from the wrong state on the map.\n  const featUfByName={};\n  Object.keys(adj.feat).forEach(k=>{ const nm=mName(k),u=mUf(k);\n    if(!(nm in featUfByName)) featUfByName[nm]=u; else if(featUfByName[nm]!==u) featUfByName[nm]=null; });\n  const ufGuessed={};\n  const ufFor=(setorOf,muni)=>{const nm=norm(muni);\n    const fromCsv=DOM.ufBySetorMuni&&DOM.ufBySetorMuni[String(setorOf)+'|'+nm];\n    if(fromCsv) return fromCsv;\n    const uniq=featUfByName[nm];\n    if(uniq) return uniq;\n    // uniq===null means the name is shared by two of the LOADED UFs — Terra Nova exists in BA and in PE,\n    // and a BA sector loads PE as a border — so the unique-name fallback gave up. Giving up here too\n    // yields a bare NAME key, which matches NO shape: the município leaves the map without a word. The\n    // sector's own dominant UF is a real signal rather than a coin flip: when a shape of this name\n    // exists in domUF, that is the one this sector means. Recorded so the inference is never invisible.\n    if(uniq===null && adj.feat[mkey(nm,domUF)]){ ufGuessed[nm]=domUF; return domUF; }\n    return '';};\n  // Absorbed and orphan rows inherited from an earlier etapa were stamped when THAT etapa ran, under\n  // the setor they belonged to then; `if(!r.uf)` keeps that original resolution intact.\n  mv.matrix.forEach(r=>{ if(!r.uf) r.uf=ufFor(sec,r.municipio); });\n  rows(lmR).forEach(r=>{ const k=mkey(r.municipio,ufFor(sec,r.municipio)); (mv.levelByMuni[k]=mv.levelByMuni[k]||{})[r.nivel]=+r.cbs; });\n\n  const totBy={}; mv.matrix.forEach(r=>{const n=mkey(r.municipio,r.uf); totBy[n]=(totBy[n]||0)+ +r.cbs;});\n  const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>0).map(([m])=>m));\n  const withShape=[...footprint].filter(n=>adj.feat[n]||world.muniGeo[n]);\n  mv.cbsByMuni={...totBy}; mv.shapeMunis=withShape.slice();\n  mv.noShapeMunis=[...footprint].filter(n=>!(adj.feat[n]||world.muniGeo[n])).map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.noShapeCbs=mv.noShapeMunis.reduce((a,x)=>a+x.cbs,0);\n\n  // ---- RULE: which municipalities belong to this sector ----\n  // Comes from the dominantes CSV (setor x município), NOT from CB volume. A municipality where the\n  // closing sector happens to have a handful of CBs but which the base assigns to ANOTHER sector is\n  // spillover: it is not reallocated, it is not drawn, and its CBs follow their leader (quebra de\n  // vínculo). Municipalities an EARLIER etapa moved into this sector count as belonging to it too —\n  // otherwise the cascade would throw away the territory the sector just received.\n  const domBase=await ensureDominantes();\n  const domOwn=domBase[String(sec)];\n  const inherited=new Set(Object.keys(world.muniOverride).filter(m=>world.muniOverride[m]===sec));\n  if((!domOwn||!domOwn.size)&&!inherited.size) throw new Error('setor '+sec+' não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar este setor');\n  const belongs=n=>(domOwn&&(domOwn.has(n)||(!DOM.hasUf&&domOwn.has(mName(n)))))||inherited.has(n);\n  mv.domCsvMunis=(domOwn&&domOwn.size)||0;\n  const dropped=withShape.filter(n=>!belongs(n));\n  if(withShape.length && dropped.length===withShape.length) throw new Error('setor '+sec+': nenhum dos municípios com CBs está na base de dominantes — nada pra realocar');\n  const droppedSet=new Set(dropped);\n  mv.excludedMunis=dropped.map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.excludedCbs=dropped.reduce((a,n)=>a+(totBy[n]||0),0);\n\n  const closingNames=withShape.filter(n=>!droppedSet.has(n));\n  const closingSet=new Set(closingNames);\n  mv.geoFeat={}; closingNames.forEach(n=>{ mv.geoFeat[n]=(adj.feat[n]&&adj.feat[n].f)||world.muniGeo[n]; });\n\n  // ---- why is the map short of the base? ----\n  // Three gates drop a município between the CSV and the map: footprint (no CBs under this setor at\n  // the snapshot), withShape (no geometry in the loaded UFs) and belongs (key mismatch). Only the last\n  // two had a counter, so the FIRST one was invisible — the operator counted 5 shapes against a base\n  // that says 6 and had nothing on screen explaining the missing one. Named here, with its reason.\n  const drawnCsv=new Set(closingNames.map(n=>DOM.hasUf?n:mName(n)));\n  const footKeyOf=k=>DOM.hasUf?(footprint.has(k)?k:null):([...footprint].find(x=>mName(x)===k)||null);\n  mv.csvMunisTotal=(domOwn&&domOwn.size)||0;\n  mv.ufGuessed={...ufGuessed};\n  mv.missingMunis=[...(domOwn||[])].filter(k=>!drawnCsv.has(k)).map(k=>{\n    const fk=footKeyOf(k);\n    const hasShape=!!(adj.feat[fk||k]||world.muniGeo[fk||k]);\n    // The SAME name present in footprint under a DIFFERENT key means UF resolution disagreed with the\n    // base: the CBs are there, they just landed on a key that no shape and no dominance entry can\n    // match. That is a different failure from \"this município has no CBs\", and conflating the two sends\n    // the next investigation into the CB base when the answer is in the UF column.\n    const alias=[...footprint].filter(x=>x!==k&&mName(x)===mName(k));\n    let reason;\n    if(!fk&&alias.length) reason=`UF divergente — as CBs entraram na chave ${alias.join(' / ')} e a base diz ${k}`;\n    else if(!fk) reason=hasShape?'sem CBs neste setor no snapshot — desenhado como vizinho':'sem CBs neste setor no snapshot';\n    else if(!hasShape) reason='sem shape nas UFs carregadas';\n    else reason='chave não casou com a base de dominantes';\n    return {muni:k,cbs:(fk?totBy[fk]:alias.reduce((a,x)=>a+(totBy[x]||0),0))||0,reason:reason};\n  });\n\n  const extVotes={}; const nbrNames=new Set();\n  closingNames.forEach(n=>{\n    if(!adj.feat[n]) return;\n    // droppedSet is excluded too: a municipality outside this sector's dominance base must not\n    // nominate a destination sector, and it is not drawn on the map at all.\n    adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o) && !droppedSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });\n  });\n  // mv.nbrFeat is assembled far below, once mv.dest is final: the map draws only the closing sector's\n  // municípios and the destination sectors' territory, so the feature list cannot be built until the\n  // neighbour query has resolved each neighbour's sector AND the destination list has settled.\n  let neigh=[];\n  if(nbrNames.size){\n    // The IN-list carries bare names — that is what the column holds, and the query is back to not\n    // selecting DS_ESTADO. Every returned row names its own CD_SETOR, which is exactly the key the\n    // base CSV is indexed by, so each row's UF is resolved from the base right after the fetch.\n    const inList=[...new Set([...nbrNames].map(n=>mName(n)))].map(n=>`'${n.replace(/'/g,\"''\")}'`).join(',');\n    const nSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR<>${sec} AND CD_SETOR>0 AND DT_HISTORICO=${SNAP} AND DS_MUNICIPIO IN (${inList}) GROUP BY DS_MUNICIPIO, CD_SETOR`;\n    neigh=rows(await q(nSQL));\n  }\n  const domSecOf={};\n  neigh.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n  const byM={}; neigh.forEach(r=>{const m=mkey(r.municipio,r.uf); if(!nbrNames.has(m)) return; (byM[m]=byM[m]||[]).push(r);});\n  Object.entries(byM).forEach(([m,rs])=>{ rs.sort((a,b)=>+b.cbs-+a.cbs); domSecOf[m]=String(rs[0].setor); });\n  nbrNames.forEach(n=>{ if(world.muniOverride[n]) domSecOf[n]=world.muniOverride[n]; });\n  mv.nbrCurrentSector=domSecOf;\n\n  // ---- HARD RULE: which sectors are eligible as destinations ----\n  // 'any'      = a sector qualifies if it borders ANY municipality of the closing sector (legacy default)\n  // 'dominant' = only municipalities with >= destMinCbs CBs act as a bridge; smaller ones are still\n  //              redistributed, they just cannot pull a new sector into the destination list.\n  //\n  // ---- HARD RULE (não negociável): destino tem de ser da MESMA gerência de vendas ----\n  // Território não atravessa fronteira de GV num fechamento real: outra gerência é outra gestora,\n  // outra meta e outro P&L, então oferecer um setor de fora produz um plano que ninguém pode\n  // executar. CD_GV da base de dominantes é a autoridade, e o filtro roda ANTES de qualquer peso,\n  // regra de ponte ou corte de top-6 — nenhum caminho a jusante pode reintroduzir um setor de fora.\n  const gvBySetor=DOM.gvBySetor||{};\n  const gvOf=s=>gvBySetor[String(s)]||'';\n  const ownGv=gvOf(sec);\n  if(!ownGv) throw new Error('setor '+sec+' não tem CD_GV na base de municípios dominantes — sem a gerência de vendas do setor que fecha não há como garantir que os destinos sejam da mesma gerência');\n  // Um candidato que a base não carrega NÃO pode ser provado como da mesma gerência, então é recusado\n  // em vez de presumido: a regra só vale se nunca vazar.\n  const sameGv=s=>gvOf(s)===ownGv;\n  mv.gv=ownGv;\n  // Foreign-GV neighbours are not drawn AT ALL. Filtering them only out of the destination list still\n  // left them sitting on the map as context, which invites the exact question the rule exists to\n  // settle — \"why is 3471 right there if nothing can be sent to it?\". A neighbour whose sector is\n  // UNKNOWN is not being asserted to be foreign, so it stays as plain geography.\n  // Dropping them also tightens the frame: project() fits the bounding box of whatever survives, so the\n  // closing sector is drawn larger once foreign territory stops padding the extent.\n  mv.gvRejected=[];\n  const gvHidden=new Set();\n  nbrNames.forEach(n=>{ const s=domSecOf[n];\n    if(s && !sameGv(s)){ gvHidden.add(n); if(mv.gvRejected.indexOf(s)<0) mv.gvRejected.push(s); } });\n  gvHidden.forEach(n=>{ nbrNames.delete(n); delete domSecOf[n]; });\n  Object.keys(extVotes).forEach(n=>{ extVotes[n]=extVotes[n].filter(v=>!gvHidden.has(v.o)); });\n  const destRule=(SESS.settings&&SESS.settings.destRule)||DEFAULT_SETTINGS.destRule;\n  const destMinCbs=Math.max(1,Math.round(+((SESS.settings||{}).destMinCbs)||DEFAULT_SETTINGS.destMinCbs));\n  mv.destRule=destRule; mv.destMinCbs=destMinCbs; mv.destRuleFallback=false;\n  const isBridge=n=>destRule!=='dominant'||(totBy[n]||0)>=destMinCbs;\n  mv.bridgeMunis=closingNames.filter(isBridge);\n  mv.ignoredBridges=closingNames.filter(n=>!isBridge(n));\n\n  mv.allowed={}; mv.allowedRaw={};\n  const wAny={}, wDom={};\n  closingNames.forEach(n=>{\n    const v={}, bridge=isBridge(n);\n    (extVotes[n]||[]).forEach(({o,w})=>{\n      const s=domSecOf[o];\n      if(!s || s===sec || world.closedSectors.has(s)) return;\n      // Belt to the braces above: domSecOf has already had every foreign-GV neighbour removed, so this\n      // cannot fire any more. It stays because it costs one comparison and the failure it guards — a\n      // destination in the wrong gerência — is the whole reason this code exists.\n      if(!sameGv(s)) return;\n      v[s]=(v[s]||0)+w;\n      wAny[s]=(wAny[s]||0)+w;\n      if(bridge) wDom[s]=(wDom[s]||0)+w;\n    });\n    mv.allowed[n]=v; mv.allowedRaw[n]={...v};\n  });\n  let secWeight=wAny;\n  if(destRule==='dominant'){\n    if(Object.keys(wDom).length) secWeight=wDom;\n    else { secWeight=wAny; mv.destRuleFallback=true; }   // no dominant municipality at all -> relax, never strand the stage\n  }\n  mv.dest=Object.keys(secWeight).sort((a,b)=>secWeight[b]-secWeight[a]).slice(0,6);\n  if(!mv.dest.length){\n    // O fallback de emergência lê domSecOf direto, contornando os votos — então repete o filtro de GV\n    // aqui. Sem isso a regra vazaria exatamente no caso em que ninguém está olhando.\n    mv.dest=[...new Set(Object.values(domSecOf))].filter(s=>s!==sec&&!world.closedSectors.has(s)&&sameGv(s)).slice(0,6);\n    closingNames.forEach(n=>{ if(!Object.keys(mv.allowed[n]).length){ mv.dest.forEach(s=>mv.allowed[n][s]=1); } });\n  }\n  // Cinto e suspensório: qualquer rota que tenha preenchido mv.dest (pesos, fallback, um branch novo\n  // amanhã) passa por esta linha, e nada de fora da gerência do setor que fecha sobrevive a ela.\n  mv.dest=mv.dest.filter(sameGv);\n  // WHAT GETS DRAWN — decided here because it depends on the final destination list. The closing\n  // sector's municípios draw as before. For each destination sector we now draw its WHOLE municipality\n  // set, not just the municípios that happened to border the closing sector — sourced from the SAME\n  // dominantes CSV (domBase) already used above for the closing sector's own municípios, never from\n  // CD_SETOR in the CRM table: that code is not a stable key there (reused across unrelated setores),\n  // so pulling by it drew scattered territory from the wrong parts of the country. The adjacency query\n  // above still decides WHICH sectors qualify as destinations (that vote is unchanged); this only\n  // widens what gets drawn once that list is settled, so a destination sector renders as its real CSV\n  // territory instead of a rim of border municípios.\n  // nbrCurrentSector deliberately keeps its full mapping: drawMap groups whatever survives in nbrFeat\n  // by it.\n  const drawSet=new Set(mv.dest);\n  mv.dest.forEach(s=>{\n    (domBase[String(s)]||new Set()).forEach(n=>{\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n); domSecOf[n]=world.muniOverride[n]||s;\n    });\n  });\n  mv.nbrFeat=[...nbrNames].filter(n=>drawSet.has(domSecOf[n])).map(n=>adj.feat[n]&&adj.feat[n].f).filter(Boolean);\n  // votes may reference a sector that did not survive the rule / top-6 cut — drop them so\n  // autoAssign + contiguity seeds can never point at a sector outside mv.dest\n  closingNames.forEach(n=>{ Object.keys(mv.allowed[n]||{}).forEach(s=>{ if(!mv.dest.includes(s)) delete mv.allowed[n][s]; }); });\n  mv.color={}; mv.dest.forEach((s,i)=>mv.color[s]=PALETTE[i%PALETTE.length]);\n\n  const accC={}; Object.entries(domSecOf).forEach(([nb,s])=>{ if(!mv.dest.includes(s))return; const f=(adj.feat[nb]&&adj.feat[nb].f)||world.muniGeo[nb]; if(!f)return; const c=centroidLL(f); accC[s]=accC[s]||[0,0,0]; accC[s][0]+=c[0]; accC[s][1]+=c[1]; accC[s][2]++; });\n  mv.secCentroid={}; mv.dest.forEach(s=>{ if(accC[s]) mv.secCentroid[s]=[accC[s][0]/accC[s][2],accC[s][1]/accC[s][2]]; });\n  mv.muniCentroid={}; closingNames.forEach(n=>{ if(mv.geoFeat[n]) mv.muniCentroid[n]=centroidLL(mv.geoFeat[n]); });\n  mv.closeAdj={}; closingNames.forEach(n=>{ mv.closeAdj[n]=adj.feat[n]?adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o):[]; });\n  const comp={}; let ci=0; closingNames.forEach(n=>{ if(comp[n]!=null)return; const st=[n]; comp[n]=ci; while(st.length){ const x=st.pop(); (mv.closeAdj[x]||[]).forEach(o=>{ if(comp[o]==null){ comp[o]=ci; st.push(o); } }); } ci++; });\n  const compAllowed={}; closingNames.forEach(n=>{ const cid=comp[n]; (compAllowed[cid]=compAllowed[cid]||new Set()); Object.keys(mv.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s)); });\n  mv.reachable={}; closingNames.forEach(n=>{ mv.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>mv.dest.includes(s)); });\n\n  if(mv.dest.length){\n    const rList=mv.dest.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    mv.baseMetrics={}; rows(bR).forEach(r=>{ mv.baseMetrics[String(r.setor)]=r; if(!SESS.sectorBaseCache[String(r.setor)]) SESS.sectorBaseCache[String(r.setor)]=r; });\n    mv.baseLevel={}; rows(blR).forEach(r=>{ const s=String(r.setor); (mv.baseLevel[s]=mv.baseLevel[s]||{})[r.nivel]=+r.cbs; });\n  } else { mv.baseMetrics={}; mv.baseLevel={}; }\n\n  mv.dest.forEach(s=>{\n    if(!mv.baseMetrics[s]) mv.baseMetrics[s]={setor:s,cbs:0,ativas:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0};\n    const infl=world.sectorInflow[s];\n    if(infl) Object.keys(blankM()).forEach(k=>{ mv.baseMetrics[s][k]=(+mv.baseMetrics[s][k]||0)+(infl[k]||0); });\n    const inflLv=world.sectorInflowLevel[s];\n    if(inflLv){ mv.baseLevel[s]=mv.baseLevel[s]||{}; Object.entries(inflLv).forEach(([nv,c])=>{ mv.baseLevel[s][nv]=(mv.baseLevel[s][nv]||0)+c; }); }\n  });\n\n  aggregateMovement(mv);\n  mv.assign=optimizeFor(mv);\n  return mv;\n}\n\n// ---------- cascade engine ----------\nasync function computeAt(i){\n  await ensurePinned();\n  const world=foldWorldState(SESS.timeline,i);\n  const sec=SESS.timeline[i].sector;\n  const mv=await computeMovement(sec,world);\n  SESS.timeline[i]=mv;\n}\nasync function recomputeFrom(i){\n  for(let k=i;k<SESS.timeline.length;k++){\n    loader(true,`recalculando etapa ${k+1} de ${SESS.timeline.length}…`);\n    try{ await computeAt(k); }\n    catch(e){\n      // A stage that failed to recompute still holds its PREVIOUS result, so the timeline would\n      // silently mix pre- and post-change etapas. Flag this stage and everything after it as stale\n      // (dashed chip + ⟳) so the state is visible and flushDirty retries from here.\n      SESS.dirtyFrom = SESS.dirtyFrom==null ? k : Math.min(SESS.dirtyFrom,k);\n      throw e;\n    }\n  }\n}\nasync function flushDirty(){\n  if(SESS.dirtyFrom==null) return;\n  const from=SESS.dirtyFrom; SESS.dirtyFrom=null;\n  loader(true,'recalculando etapas seguintes…');\n  try{ await recomputeFrom(from); }\n  catch(e){ $('#ds').textContent='erro ao recalcular cadeia: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n}\nasync function addMovement(sec){\n  sec=(sec==null?'':String(sec)).trim();\n  if(!sec) return;\n  const closedNow=new Set(SESS.timeline.map(mv=>mv.sector));\n  if(closedNow.has(sec)){ $('#ds').textContent='setor '+sec+' já fechado nesta timeline.'; $('#ds').classList.add('err'); return; }\n  if(!SESS.sectorsList.some(s=>s.setor===sec)){ $('#ds').textContent='setor \"'+sec+'\" não existe no snapshot congelado.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  loader(true,'fechando setor '+sec+' — puxando dado do snapshot congelado…');\n  $('#liveState').classList.remove('err');\n  try{\n    SESS.timeline.push({sector:String(sec)});\n    await computeAt(SESS.timeline.length-1);\n    SESS.selected=SESS.timeline.length-1;\n    $('#ds').classList.remove('err'); $('#ds').textContent='';\n  }catch(e){\n    SESS.timeline.pop();\n    $('#ds').textContent='erro ao fechar setor '+sec+': '+e.message; $('#ds').classList.add('err');\n  } finally { loader(false); renderAll(); }\n}\nfunction removeFrom(i){\n  SESS.timeline.length=i;\n  SESS.selected=Math.min(SESS.selected,Math.max(0,SESS.timeline.length-1));\n  if(SESS.dirtyFrom!=null && SESS.dirtyFrom>=SESS.timeline.length) SESS.dirtyFrom=null;\n  renderAll();\n}\nasync function editSelected(mutator){\n  const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  mutator(mv);\n  if(SESS.selected<SESS.timeline.length-1){\n    SESS.dirtyFrom = SESS.dirtyFrom==null ? SESS.selected+1 : Math.min(SESS.dirtyFrom, SESS.selected+1);\n  }\n  renderAll();\n}\n\n// ---------- sectors list / picker ----------\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1) GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  SESS.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n}\nfunction renderSectorPicker(){\n  const closed=new Set(SESS.timeline.map(mv=>mv.sector));\n  const avail=SESS.sectorsList.filter(s=>!closed.has(s.setor));\n  const dl=$('#sectorPickList');\n  if(dl) dl.innerHTML=avail.map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n  const ready=SESS.stage==='main';\n  $('#rand').disabled=!ready||!avail.length; $('#go').disabled=!ready||!avail.length; $('#sectorPick').disabled=!ready;\n}\nfunction renderStage(){\n  const isMain=SESS.stage==='main';\n  const cc=$('#confirmCard'), hb=$('#howtoBox'), mw=$('#mainWrap');\n  const hdr=$('#topHeader'), rl=$('#timelineRail');\n  if(hdr) hdr.classList.toggle('hidden', !isMain);\n  if(rl) rl.classList.toggle('hidden', !isMain);\n  if(cc) cc.classList.toggle('hidden', isMain);\n  if(hb) hb.classList.toggle('hidden', !isMain);\n  if(mw) mw.classList.toggle('hidden', !isMain);\n  const cb=$('#confirmBody');\n  if(cb && !isMain) cb.innerHTML = SESS.snapshotRaw\n    ? `Snapshot congelado · atualizado em <b>${fmtSyncedAt(SESS.snapshotSyncedAt)}</b> · <b>${SESS.sectorsList.length}</b> setor(es) disponível(eis) pra fechar. Nenhuma consulta de detalhe roda até você confirmar e escolher o primeiro setor.`\n    : 'congelando snapshot…';\n}\n\n// ---------- session persistence ----------\n// ONE table (`timeline-shared-sessions`) holds BOTH kinds of row, so SESSIONS_DS and SHARED_DS\n// deliberately resolve to the SAME DataSet — the two names mark intent, not two stores:\n//   · working session  — carries `movements`; saveSession updates it in place\n//   · shared snapshot  — carries `sharedAt` + `stages`; doShare CREATES it and nothing updates it\n// listSessions MUST exclude the shared rows. They are write-once by contract, and if one showed\n// up in the picker, loading it and pressing Salvar would fire ds.update() straight at a frozen\n// snapshot. Filtering here is what keeps that unreachable — it is the guard, not a cosmetic.\n// Both lookups stay because a policy-gated DataSet's Process resolves late; same race the reader\n// component polls for. SESSIONS_DS lives at module scope so no call site can hit it in the TDZ.\nasync function listSessions(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return [];\n  await ds.refresh();\n  const recs=ds.records()||[];\n  return [...recs].filter(r=>!r.sharedAt).sort((a,b)=>new Date(b.updatedAt||0)-new Date(a.updatedAt||0));\n}\nasync function refreshSessionPicker(){\n  const list=await listSessions().catch(()=>[]);\n  SESS.sessionsList=list;\n  const opts=list.map(s=>`<option value=\"${s._id}\">${esc(s.label||'sem nome')} · ${fmt((s.movements||[]).length)} etapa(s)</option>`).join('')+`<option value=\"\">+ Nova sessão</option>`;\n  const sel=$('#sessionPick'); if(sel){ sel.innerHTML=opts; sel.value=SESS.sessionId||''; }\n  const csel=$('#confirmSessionPick'); if(csel){ csel.innerHTML=opts; csel.value=SESS.sessionId||''; }\n}\nasync function saveSession(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS));\n  if(!ds){ $('#sessStatus').textContent='dataset de sessões indisponível'; return; }\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra salvar ainda'; return; }\n  await flushDirty();\n  const label=SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR'));\n  const nowIso=new Date().toISOString();\n  const payload={\n    label,\n    settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n    snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt, snapLiteral:SESS.snapLiteral, eloMaxCycle:SESS.eloMaxCycle,\n    movements:SESS.timeline.map(mv=>({\n      sector:mv.sector, assign:mv.assign, policy:mv.policy,\n      matrix:mv.matrix, trend:mv.trend, levels:mv.levels, levelByMuni:mv.levelByMuni,\n      pureOwnMetrics:mv.pureOwnMetrics, domUF:mv.domUF, dest:mv.dest, color:mv.color,\n      gv:mv.gv, gvRejected:mv.gvRejected,\n      missingMunis:mv.missingMunis, csvMunisTotal:mv.csvMunisTotal, ufGuessed:mv.ufGuessed,\n      baseMetrics:mv.baseMetrics, baseLevel:mv.baseLevel, geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat,\n      nbrCurrentSector:mv.nbrCurrentSector, allowed:mv.allowed, allowedRaw:mv.allowedRaw, reachable:mv.reachable,\n      destRule:mv.destRule, destMinCbs:mv.destMinCbs, destRuleFallback:mv.destRuleFallback, optTier:mv.optTier,\n      cbsByMuni:mv.cbsByMuni, shapeMunis:mv.shapeMunis, excludedMunis:mv.excludedMunis, domCsvMunis:mv.domCsvMunis,\n      excludedCbs:mv.excludedCbs, noShapeMunis:mv.noShapeMunis, noShapeCbs:mv.noShapeCbs,\n      secCentroid:mv.secCentroid, muniCentroid:mv.muniCentroid, closeAdj:mv.closeAdj, optBadge:mv.optBadge,\n    })),\n    updatedAt:nowIso,\n  };\n  $('#sessStatus').textContent='salvando…';\n  try{\n    if(SESS.sessionId){\n      await ds.update({_id:SESS.sessionId,...payload});\n    } else {\n      payload.createdAt=nowIso;\n      const created=await ds.create(payload);\n      SESS.sessionId=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    }\n    SESS.sessionLabel=label;\n    await refreshSessionPicker();\n    $('#sessStatus').textContent='salvo ✓';\n  }catch(e){ $('#sessStatus').textContent='erro ao salvar: '+e.message; }\n  setTimeout(()=>{ if($('#sessStatus')) $('#sessStatus').textContent=''; },2200);\n}\n// Duplicate = drop sessionId so saveSession takes its CREATE branch instead of its update branch.\n// The source record is never written to, the copy gets a fresh _id and its own createdAt, and the\n// copy becomes the active session (so the next Salvar edits the copy, not the original).\nasync function duplicateSession(){\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra duplicar ainda'; return; }\n  const prevId=SESS.sessionId, prevLabel=SESS.sessionLabel;\n  SESS.sessionId=null;\n  SESS.sessionLabel=(SESS.sessionLabel||'Sessão')+' (cópia)';\n  await saveSession();\n  if(!SESS.sessionId){\n    // saveSession swallows its own errors into #sessStatus, so a still-null id after the await is\n    // the only failure signal we get. Put the original identity back — otherwise a failed copy\n    // silently leaves the session detached, and the next Salvar would fork a new record instead\n    // of updating the one the user thought they were editing.\n    SESS.sessionId=prevId; SESS.sessionLabel=prevLabel;\n    await refreshSessionPicker().catch(()=>{});\n    return;\n  }\n  if($('#sessStatus')) $('#sessStatus').textContent='duplicado ✓';\n}\nasync function loadSession(id){\n  if(!id) return newSession();\n  const list=(SESS.sessionsList&&SESS.sessionsList.length)?SESS.sessionsList:await listSessions().catch(()=>[]);\n  const rec=list.find(s=>String(s._id)===String(id));\n  if(!rec) return newSession();\n  loader(true,'carregando sessão…');\n  SESS.sessionId=rec._id; SESS.sessionLabel=rec.label;\n  SESS.snapshotRaw=rec.snapshotRaw; SESS.snapshotSyncedAt=rec.snapshotSyncedAt||null; SESS.snapLiteral=rec.snapLiteral; SESS.eloMaxCycle=rec.eloMaxCycle;\n  SESS.settings={...DEFAULT_SETTINGS,...(rec.settings||{})};\n  SESS.timeline=(rec.movements||[]).map(m=>({sector:String(m.sector)}));\n  SESS.selected=0; SESS.sectorBaseCache={}; SESS.stage='main'; SESS.dirtyFrom=null;\n  try{\n    for(let i=0;i<SESS.timeline.length;i++){\n      const saved=(rec.movements||[])[i]||{};\n      // Sessions saved before the UF became part of the key hold bare-name keys in matrix rows,\n      // assign, geoFeat and closeAdj alike. Restoring that cache would mix key shapes the moment the\n      // stage is recomputed — and it would preserve the very collision this fixes — so those stages\n      // are replayed against the pinned snapshot and the manual assignment is remapped by name.\n      const legacyKeys=!!(saved.matrix&&saved.matrix.length&&saved.matrix.every(r=>r&&r.uf==null));\n      // Sessões salvas antes da regra de gerência podem carregar um destino de outra GV no cache.\n      // Restaurar esse cache manteria o setor proibido na tela para sempre, então essas etapas são\n      // reprocessadas contra o snapshot fixado — igual ao tratamento das chaves legadas.\n      const crossGv=await savedCrossesGv(saved);\n      if(saved.matrix&&!legacyKeys&&!crossGv){\n        loader(true,`restaurando etapa ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector} (cache, sem consulta)…`);\n        const mv={sector:String(saved.sector), policy:saved.policy||'fora', view:'tobe', selMuni:null, optBadge:saved.optBadge||''};\n        mv.matrix=saved.matrix; mv.trend=saved.trend||[]; mv.levels=saved.levels||[]; mv.levelByMuni=saved.levelByMuni||{};\n        mv.pureOwnMetrics=saved.pureOwnMetrics||blankM(); mv.domUF=saved.domUF; mv.dest=saved.dest||[]; mv.color=saved.color||{};\n        mv.baseMetrics=saved.baseMetrics||{}; mv.baseLevel=saved.baseLevel||{}; mv.geoFeat=saved.geoFeat||{}; mv.nbrFeat=saved.nbrFeat||[];\n        mv.nbrCurrentSector=saved.nbrCurrentSector||{}; mv.allowed=saved.allowed||{}; mv.reachable=saved.reachable||{};\n        mv.allowedRaw=saved.allowedRaw||saved.allowed||{};\n        mv.destRule=saved.destRule||SESS.settings.destRule; mv.destMinCbs=saved.destMinCbs||SESS.settings.destMinCbs; mv.destRuleFallback=!!saved.destRuleFallback;\n        mv.optTier=(saved.optTier==null?0:saved.optTier);\n        mv.gv=saved.gv||''; mv.gvRejected=saved.gvRejected||[];\n        mv.missingMunis=saved.missingMunis||[]; mv.csvMunisTotal=saved.csvMunisTotal||0;\n        mv.ufGuessed=saved.ufGuessed||{};\n        mv.cbsByMuni=saved.cbsByMuni||{}; mv.shapeMunis=saved.shapeMunis||[];\n        mv.excludedMunis=saved.excludedMunis||[]; mv.excludedCbs=saved.excludedCbs||0; mv.domCsvMunis=saved.domCsvMunis||0;\n        mv.noShapeMunis=saved.noShapeMunis||[]; mv.noShapeCbs=saved.noShapeCbs||0;\n        mv.secCentroid=saved.secCentroid||{}; mv.muniCentroid=saved.muniCentroid||{}; mv.closeAdj=saved.closeAdj||{};\n        aggregateMovement(mv);\n        mv.assign=saved.assign||{};\n        mv.dest.forEach(s=>{ if(!SESS.sectorBaseCache[s]) SESS.sectorBaseCache[s]=mv.baseMetrics[s]; });\n        SESS.timeline[i]=mv;\n      } else {\n        loader(true,`replay ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector}…`);\n        await computeAt(i);\n        if(saved.assign) SESS.timeline[i].assign=legacyKeys?remapAssign(SESS.timeline[i],saved.assign):saved.assign;\n        if(saved.policy) SESS.timeline[i].policy=saved.policy;\n      }\n    }\n    await loadSectors();\n  }catch(e){ $('#ds').textContent='erro ao carregar sessão: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n  renderAll();\n}\n// Re-points a legacy bare-name assignment at the NAME/UF key it can only mean. When a name resolves\n// to more than one município in the recomputed stage the manual choice is genuinely ambiguous —\n// dropping it lets the optimizer allocate instead of guessing on the operator's behalf.\n// True when a cached stage names a destination outside the closing sector's gerência de vendas.\n// Judges the cache by TODAY's rule, reading the (memoised) dominantes base rather than trusting what\n// the session was saved with. Unknown GV on the closing sector answers false: the stage then loads\n// from cache instead of being replayed into the hard throw in computeMovement, which would leave the\n// operator staring at a broken session with no way back.\nasync function savedCrossesGv(saved){\n  if(!saved||!saved.sector||!(saved.dest||[]).length) return false;\n  let gvBy={};\n  try{ await ensureDominantes(); gvBy=DOM.gvBySetor||{}; }catch(e){ return false; }\n  const own=gvBy[String(saved.sector)]||'';\n  if(!own) return false;\n  if(saved.dest.some(s=>(gvBy[String(s)]||'')!==own)) return true;\n  // A stage saved before foreign-GV neighbours stopped being drawn can hold a perfectly clean dest and\n  // STILL carry their shapes in nbrFeat/nbrCurrentSector — it would repaint the old map straight from\n  // cache. An unknown gerência is not evidence of a foreign one, so only a known, differing GV replays.\n  return Object.values(saved.nbrCurrentSector||{}).some(s=>{ const g=gvBy[String(s)]||''; return g&&g!==own; });\n}\nfunction remapAssign(mv,savedAssign){\n  const keys=Object.keys(mv.munis||{}).filter(k=>k!=='__OUTROS__');\n  const out={};\n  Object.entries(savedAssign||{}).forEach(([k,v])=>{\n    if(keys.indexOf(k)>=0){ out[k]=v; return; }\n    const cand=keys.filter(x=>mName(x)===mName(k));\n    if(cand.length===1) out[cand[0]]=v;\n  });\n  return out;\n}\nfunction newSession(){\n  SESS.sessionId=null; SESS.sessionLabel=null; SESS.timeline=[]; SESS.selected=0; SESS.sectorBaseCache={};\n  SESS.settings={...DEFAULT_SETTINGS};\n  SESS.snapshotRaw=null; SESS.snapshotSyncedAt=null; SESS.snapLiteral=null; SESS.eloMaxCycle=null; SESS.stage='loading'; SESS.dirtyFrom=null;\n  (async()=>{\n    loader(true,'iniciando nova sessão…');\n    try{ await Promise.all([ensurePinned(), loadSectors()]); SESS.stage='confirm'; }\n    catch(e){ setError('erro init: '+e.message); }\n    finally{ loader(false); renderAll(); }\n  })();\n}\n\n// ---------- snapshot refresh ----------\n// Deliberately breaks the frozen-snapshot guarantee, on demand: re-resolves DT_HISTORICO and\n// replays the whole timeline against the new one. Skips the replay when the snapshot has not\n// actually moved, so a stray click on a long timeline costs one cheap probe instead of ~8 queries\n// per etapa.\nasync function refreshSnapshot(){\n  if(SESS.stage!=='main' || SESS.snapBusy) return;\n  SESS.snapBusy=true;\n  const prevRaw=SESS.snapshotRaw;\n  const pill=$('#liveState'); if(pill){ pill.classList.add('busy'); pill.textContent='◌ atualizando snapshot…'; }\n  loader(true,'buscando o snapshot mais recente…');\n  let ok=false;\n  try{\n    const p=await resolveSnapshotPin();\n    if(!p.snapshotRaw) throw new Error('não foi possível resolver DT_HISTORICO');\n    const same=String(p.snapshotRaw)===String(prevRaw);\n    SESS.snapshotRaw=p.snapshotRaw; SESS.snapshotSyncedAt=p.syncedAt; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n    await loadSectors();   // the sector list follows the live max, so it can have moved too\n    if(same){\n      $('#ds').textContent=`snapshot já era o mais recente · atualizado em ${fmtSyncedAt(p.syncedAt)} — nada recalculado`;\n    } else if(SESS.timeline.length){\n      SESS.dirtyFrom=null;\n      SESS.sectorBaseCache={};   // baselines come from the snapshot, so every one of them changed\n      loader(true,'snapshot novo — recalculando a timeline…');\n      await recomputeFrom(0);\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)} · ${fmt(SESS.timeline.length)} etapa(s) recalculada(s)`;\n    } else {\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)}`;\n    }\n    $('#ds').classList.remove('err');\n    ok=true;\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?null:SESS.dirtyFrom+1);\n    $('#ds').textContent='erro ao atualizar o snapshot'+(at?` na etapa ${at} — etapa ${at} em diante ficou desatualizada (⟳)`:'')+': '+e.message;\n    $('#ds').classList.add('err');\n  }finally{\n    SESS.snapBusy=false;\n    if(pill) pill.classList.remove('busy');\n    loader(false); renderAll();\n    if(ok) setTimeout(()=>{ const d=$('#ds'); if(d&&!d.classList.contains('err')) d.textContent=''; },7000);\n  }\n}\n\n// ---------- session options ----------\nfunction previewDest(mv,rule,minCbs){\n  if(!mv||!mv.allowedRaw||!mv.munis) return null;\n  const w={}; let relaxed=false;\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const add=m=>Object.entries(mv.allowedRaw[m]||{}).forEach(([s,v])=>{ w[s]=(w[s]||0)+v; });\n  // allowedRaw only carries municipalities already inside the sector's dominance base, so there is\n  // nothing left to filter out here beyond the destination rule itself.\n  Object.keys(mv.allowedRaw).forEach(m=>{ const cbs=cbsOf(m);\n    if(rule==='dominant'&&cbs<minCbs) return; add(m); });\n  if(rule==='dominant'&&!Object.keys(w).length){ Object.keys(mv.allowedRaw).forEach(add); relaxed=true; }\n  return {dest:Object.keys(w).sort((a,b)=>w[b]-w[a]).slice(0,6),relaxed};\n}\nfunction readOptForm(){\n  const r=root.querySelector('input[name=\"destRule\"]:checked');\n  let n=Math.round(+$('#optMinCbs').value);\n  if(!(n>0)) n=DEFAULT_SETTINGS.destMinCbs;\n  return {destRule:(r&&r.value)||DEFAULT_SETTINGS.destRule, destMinCbs:n,\n          targetSpan:numOrNull($('#optTargetSpan').value),\n          maxSpan:numOrNull($('#optMaxSpan').value),\n          minSpan:numOrNull($('#optMinSpan').value)};\n}\nfunction spanFormError(f){\n  if(f.minSpan&&f.maxSpan&&f.minSpan>f.maxSpan) return `Span mínimo (${fmt(f.minSpan)}) não pode ser maior que o máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.maxSpan&&f.targetSpan>f.maxSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode passar do máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.minSpan&&f.targetSpan<f.minSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode ficar abaixo do mínimo (${fmt(f.minSpan)}).`;\n  return '';\n}\nfunction renderOptPreview(){\n  const el=$('#optPreview'); if(!el) return;\n  const f=readOptForm();\n  root.querySelectorAll('.opt-row').forEach(r=>{\n    if(r.dataset.r!=null) r.classList.toggle('on',r.dataset.r===f.destRule);\n  });\n  const wrap=$('#optMinCbsWrap'); if(wrap) wrap.classList.toggle('off',f.destRule!=='dominant');\n  const err=spanFormError(f), ee=$('#optSpanErr');\n  if(ee){ ee.textContent=err; ee.classList.toggle('hidden',!err); }\n  const ap=$('#optApply'); if(ap) ap.disabled=!!err;\n  const mv=SESS.timeline[SESS.selected];\n  if(!mv||!mv.munis){ el.innerHTML='Nenhuma etapa calculada ainda — as regras passam a valer no próximo fechamento.'; return; }\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const ms=Object.keys(mv.geoFeat||{});\n  const ig=f.destRule==='dominant'?ms.filter(m=>cbsOf(m)<f.destMinCbs):[];\n  const pv=previewDest(mv,f.destRule,f.destMinCbs);\n  let h=`<b style=\"color:var(--ink)\">Prévia — etapa ${SESS.selected+1}, setor ${mv.sector}</b><br>`;\n  const exN=(mv.excludedMunis||[]).length;\n  if(exN) h+=`Base de dominantes: <b>${fmt(ms.length)}</b> município(s) no mapa · <span style=\"color:var(--closing)\">${fmt(exN)} fora da base — ${fmt(mv.excludedCbs||0)} CBs seguem suas líderes</span><br>`;\n  h+=`${fmt(ms.length-ig.length)} de ${fmt(ms.length)} município(s) valem como ponte`;\n  if(ig.length) h+=` · <span style=\"color:var(--closing)\">ignorados:</span> ${ig.slice(0,8).join(', ')}${ig.length>8?' +'+(ig.length-8):''}`;\n  if(pv){\n    h+=`<br>Setores destino com esta regra: <b style=\"color:var(--ink)\">${pv.dest.join(', ')||'–'}</b>`;\n    const cur=(mv.dest||[]).join(', ');\n    if(pv.dest.join(',')!==(mv.dest||[]).join(',')) h+=` <span style=\"color:var(--closing)\">(hoje: ${cur||'–'})</span>`;\n    if(pv.relaxed) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — a regra relaxa pra \"qualquer município\" pra não deixar a etapa sem destino.</b>`;\n  }\n  if(f.targetSpan||f.maxSpan||f.minSpan){\n    const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,{target:f.targetSpan,max:f.maxSpan,min:f.minSpan});\n    h+=`<br><br>Span dos destinos como está hoje: ${mv.dest.map(s=>s+' <b>'+fmt(sp[s])+'</b>').join(' · ')||'–'}`;\n    if(v.over.length||v.under.length) h+=`<br><span style=\"color:var(--closing)\">Fora dos limites: ${[...v.over.map(s=>s+' ▲ acima do máx'),...v.under.map(s=>s+' ▼ abaixo do mín')].join(', ')}</span> — o modelo ÓTIMO tenta corrigir no recálculo.`;\n    else h+=` <span style=\"color:var(--ok)\">— já dentro dos limites</span>`;\n  }\n  if(SESS.timeline.length) h+=`<br><span style=\"margin-top:4px;display:inline-block\">Aplicar <b>recalcula as ${fmt(SESS.timeline.length)} etapa(s)</b> contra o snapshot congelado — os destinos ajustados à mão serão refeitos pelo modelo ÓTIMO.</span>`;\n  el.innerHTML=h;\n}\nfunction openOptions(){\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  root.querySelectorAll('input[name=\"destRule\"]').forEach(r=>{ r.checked=(r.value===st.destRule); });\n  $('#optMinCbs').value=st.destMinCbs;\n  $('#optTargetSpan').value=st.targetSpan||'';\n  $('#optMaxSpan').value=st.maxSpan||'';\n  $('#optMinSpan').value=st.minSpan||'';\n  $('#optApply').disabled=false;\n  $('#optSessName').textContent=SESS.sessionLabel||'sessão não salva';\n  $('#optStatus').textContent='';\n  $('#optionsBg').classList.remove('hidden');\n  renderOptPreview();\n  const c=$('#optCancel'); if(c) c.focus();\n}\nfunction closeOptions(){ const b=$('#optionsBg'); if(b) b.classList.add('hidden'); }\nasync function applyOptions(){\n  const f=readOptForm();\n  if(spanFormError(f)) return;\n  const prev={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const changed = prev.destRule!==f.destRule\n    || (f.destRule==='dominant' && prev.destMinCbs!==f.destMinCbs)\n    || numOrNull(prev.targetSpan)!==f.targetSpan\n    || numOrNull(prev.maxSpan)!==f.maxSpan\n    || numOrNull(prev.minSpan)!==f.minSpan;\n  SESS.settings={...prev,...f};\n  closeOptions();\n  if(!changed || !SESS.timeline.length){ renderAll(); return; }\n  SESS.dirtyFrom=null;\n  SESS.sectorBaseCache={};   // the recompute re-derives every baseline; keeping the old ones would\n                             // leave \"Impacto acumulado\" comparing against pre-change numbers\n  loader(true,'aplicando nova regra de destino — recalculando timeline…');\n  try{\n    await recomputeFrom(0);\n    $('#ds').textContent=''; $('#ds').classList.remove('err');\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?0:SESS.dirtyFrom)+1;\n    $('#ds').textContent=`erro ao aplicar a regra na etapa ${at}: ${e.message} — etapa ${at} em diante ficou desatualizada (⟳)`;\n    $('#ds').classList.add('err');\n  }finally{ loader(false); renderAll(); }\n}\n\nconst SHARED_DS='timeline-shared-sessionsDataSet';\nconst AUTH_REF='timelineSimAuthFinal';   // single source of truth for the auth strategy reference\nlet SHARE={busy:false,url:null,last:null};\n// Serialise a PRESENTATION model, not the raw session, so the reader stays purely presentational\n// and never re-derives spans/desloc/orphans — no second copy of this engine to keep in sync.\n// NOTE: METRICS_DEF[2] is the money marker; read it as !!md[2] (a literal dollar sign in this\n// position trips the upsert's parse check).\nfunction buildShareStages(){\n  return SESS.timeline.map((mv,i)=>{\n    const c=computeDerived(mv);\n    const incBy={};\n    mv.dest.forEach(s=>{ incBy[s]=Object.fromEntries(METRICS_DEF.map(md=>[md[0],0])); });\n    Object.entries(mv.munis).forEach(([m,o])=>{\n      if(m==='__OUTROS__') return;\n      const s=mv.assign[m]; if(!s||!incBy[s]) return;\n      METRICS_DEF.forEach(md=>{ incBy[s][md[0]]+=o[md[0]]||0; });\n    });\n    const saiTot={};\n    METRICS_DEF.forEach(md=>{\n      const k=md[0];\n      saiTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,p)=>a+((p[1][k])||0),0);\n    });\n    // orphans follow their leader's home: they enter that sector AND leave the closing one\n    c.leaders.forEach(L=>{\n      const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n      if(!om||!L.home) return;\n      METRICS_DEF.forEach(md=>{ const k=md[0];\n        if(incBy[L.home]) incBy[L.home][k]+=om[k]||0;\n        saiTot[k]+=om[k]||0; });\n    });\n    const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n    const sv=mv.dest.map(s=>c.span[s]);\n    const baseOf=(s,k)=>{ const b=mv.baseMetrics[s]||{}; return +(b[k]||0); };\n    return {\n      i:i, sector:mv.sector, policy:mv.policy, domUF:mv.domUF,\n      optTier:(mv.optTier==null?0:mv.optTier),\n      destRuleFallback:!!mv.destRuleFallback,\n      dest:mv.dest.slice(), color:Object.assign({},mv.color), assign:Object.assign({},mv.assign),\n      kpis:{ cbsRealocadas:closingCbsOf(mv), desloc:c.desloc,\n             spread:(sv.length?Math.max.apply(null,sv)-Math.min.apply(null,sv):0),\n             municipios:ms.length, lideres:mv.leaders.length },\n      destinos:mv.dest.map(s=>({ s:s, base:baseOf(s,'cbs'), inc:c.inc[s]||0, orph:c.orph[s]||0, span:c.span[s]||0, gained:c.gained[s]||0 })),\n      municipios:ms.map(m=>({ nome:mName(m), uf:mUf(m), cbs:mv.munis[m].cbs, destino:mv.assign[m]||null })),\n      excluidos:(mv.excludedMunis||[]).map(x=>({ muni:x.muni, cbs:x.cbs })),\n      orphanTotal:c.orphanTotal||0, orphanStranded:c.orphanStranded||0, noShapeCbs:mv.noShapeCbs||0,\n      lideres:c.leaders.map(L=>({ lider:(L.lider||'').split(' ').slice(0,2).join(' '), grupo:L.grupo,\n        total:L.total, home:L.home, desloc:L.desloc, orphan:L.orphan||0 })),\n      metricas:METRICS_DEF.map(md=>{ const k=md[0];\n        return { key:k, label:md[1], money:!!md[2],\n          rows:mv.dest.map(s=>({ s:s, antes:baseOf(s,k), depois:baseOf(s,k)+((incBy[s]&&incBy[s][k])||0) })),\n          sai:saiTot[k]||0 }; }),\n      geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat, excludedFeat:[]\n    };\n  });\n}\nfunction buildShareSummary(stages){\n  const world=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const receivers=[];\n  SESS.timeline.forEach((mv,i)=>{\n    const own=mv.pureOwnMetrics||{};\n    receivers.push({ s:mv.sector, situacao:'fechado (etapa '+(i+1)+')', antes:+(own.cbs||0), depois:0 });\n  });\n  const seen={};\n  Object.values(world.muniOverride).forEach(s=>{\n    if(seen[s]) return; seen[s]=1;\n    const base=SESS.sectorBaseCache[s]||{}, infl=world.sectorInflow[s]||{};\n    receivers.push({ s:s, situacao:'recebeu território', antes:+(base.cbs||0), depois:(+(base.cbs||0))+(infl.cbs||0) });\n  });\n  return { stages:stages.length, sectors:SESS.timeline.map(mv=>mv.sector),\n    totalCbs:stages.reduce((a,s)=>a+(s.kpis.cbsRealocadas||0),0),\n    totalDesloc:stages.reduce((a,s)=>a+(s.kpis.desloc||0),0), receivers:receivers };\n}\nfunction renderShareBody(){\n  const el=$('#shareBody'); if(!el) return;\n  const go=$('#shareGo'), cp=$('#shareCopy'), op=$('#shareOpen'), cc=$('#shareCancel');\n  if(SHARE.url){\n    el.innerHTML=`<div class=\"opt-note\" style=\"margin-top:0\"><b style=\"color:var(--ink)\">Sessão compartilhada ✓</b><br>\n      Link de leitura desta simulação congelada. Quem abrir precisa ter login no simulador.\n      <div class=\"shareurl\">${esc(SHARE.url)}</div></div>`;\n    if(go) go.classList.add('hidden');\n    if(cp) cp.classList.remove('hidden');\n    if(op) op.classList.remove('hidden');\n    if(cc) cc.textContent='Fechar';\n    return;\n  }\n  if(go) go.classList.remove('hidden');\n  if(cp) cp.classList.add('hidden');\n  if(op) op.classList.add('hidden');\n  if(cc) cc.textContent='Cancelar';\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const desloc=SESS.timeline.reduce((a,mv)=>a+(mv.munis?computeDerived(mv).desloc:0),0);\n  const destTxt=st.destRule==='dominant'?('dominantes ≥'+fmt(st.destMinCbs)+' CBs'):'qualquer município';\n  el.innerHTML=`<div class=\"opt-lbl\">Confirmar compartilhamento</div>\n    <div class=\"opt-sub\">Publica uma <b>cópia congelada</b> desta sessão, exatamente no estado atual.</div>\n    <div class=\"opt-note\" style=\"margin-top:0\">\n      · <b>${fmt(SESS.timeline.length)}</b> etapa(s) — ${esc(SESS.timeline.map(mv=>mv.sector).join(' → '))}<br>\n      · snapshot atualizado em <b>${esc(fmtSyncedAt(SESS.snapshotSyncedAt))}</b><br>\n      · base: <b>municípios dominantes (CSV)</b> · destino: <b>${esc(destTxt)}</b> · span: <b>${esc(spanShort(st))}</b><br>\n      · <b>${fmt(desloc)}</b> CBs deslocadas somando a cadeia\n    </div>\n    <div class=\"opt-note\">A cópia <b>não</b> acompanha edições posteriores desta sessão — pra atualizar, compartilhe de novo e use o link novo. O link exige login no simulador; não é público.</div>\n    ${SHARE.last?`<div class=\"opt-note\"><b style=\"color:var(--ink)\">Último link gerado nesta sessão</b> — aponta pro estado de <b>antes</b> das edições feitas depois dele.<div class=\"shareurl\">${esc(SHARE.last)}</div></div>`:''}`;\n}\nasync function openShare(){\n  if(!SESS.timeline.length){ $('#ds').textContent='nada pra compartilhar — feche pelo menos um setor primeiro.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  SHARE.url=null;\n  const s=$('#shareStatus'); if(s){ s.textContent=''; s.classList.remove('err'); }\n  renderShareBody();\n  $('#shareBg').classList.remove('hidden');\n  const b=$('#shareCancel'); if(b) b.focus();\n}\nfunction closeShare(){ const b=$('#shareBg'); if(b) b.classList.add('hidden'); }\nasync function doShare(){\n  if(SHARE.busy) return;\n  const go=$('#shareGo'), stx=$('#shareStatus');\n  SHARE.busy=true;\n  if(go){ go.disabled=true; go.textContent='Compartilhando…'; }\n  if(stx){ stx.textContent=''; stx.classList.remove('err'); }\n  try{\n    // Resolved as a GLOBAL PROCESS, not a declared param — which only works because the block\n    // instance on the `main` artboard binds `sharedDataset: @timeline-shared-sessionsDataSet`.\n    // That binding is load-bearing: a Process is only define()d once something on the rendering\n    // tree references it, so without it this lookup returns null and sharing dies on the guard\n    // below. Do NOT remove the instance param because this file never reads d2.params.sharedDataset.\n    const ds=d2.data(SHARED_DS);\n    if(!ds) throw new Error('dataset de sessões compartilhadas indisponível');\n    await flushDirty();\n    const stages=buildShareStages();\n    const summary=buildShareSummary(stages);\n    let who='';\n    try{ const a=d2.auth(AUTH_REF); who=(a&&a.user&&(a.user.email||a.user.nome))||''; }catch(e){}\n    const created=await ds.create({\n      label:SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR')),\n      snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt,\n      settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n      stages:stages, summary:summary, sharedBy:who, sharedAt:new Date().toISOString()\n    });\n    const id=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    if(!id) throw new Error('registro gravado, mas sem _id na resposta — não foi possível montar o link');\n    SHARE.url=window.location.origin+'/sessions/'+id;\n    SHARE.last=SHARE.url;\n    renderShareBody();\n  }catch(e){\n    if(stx){ stx.textContent='erro ao compartilhar: '+e.message; stx.classList.add('err'); }\n  }finally{\n    SHARE.busy=false;\n    if(go){ go.disabled=false; go.textContent='Compartilhar'; }\n  }\n}\n\n// ---------- rendering ----------\nfunction renderTimeline(){\n  const el=$('#movementChips'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<span class=\"empty\">Nenhuma etapa ainda — use \"Fechar setor ▶\" à direita.</span>'; return; }\n  el.innerHTML=SESS.timeline.map((mv,i)=>{\n    const ready=mv.dest!==undefined;\n    const c=ready?computeDerived(mv):null;\n    const stale=SESS.dirtyFrom!=null && i>=SESS.dirtyFrom;\n    return `<div class=\"tchip ${i===SESS.selected?'on':''} ${stale?'stale':''}\" data-i=\"${i}\">\n      <span class=\"tnum\">${i+1}</span><span class=\"tsec\">Setor ${mv.sector}</span>\n      <span class=\"tstat\">${ready?fmt(c.desloc)+' desloc.':'…'}</span>\n      ${stale?'<span class=\"tstale\" title=\"recalcula ao selecionar outra etapa\">⟳</span>':''}\n      <span class=\"trm\" data-rm=\"${i}\" title=\"remover esta etapa e as seguintes\">✕</span>\n    </div>`;\n  }).join('');\n  el.querySelectorAll('.tchip').forEach(ch=>{\n    ch.addEventListener('click',async (e)=>{\n      if(e.target.dataset.rm!=null && e.target.dataset.rm!==''){ removeFrom(+e.target.dataset.rm); return; }\n      const i=+ch.dataset.i;\n      if(i!==SESS.selected) await flushDirty();\n      SESS.selected=i; renderAll();\n    });\n  });\n}\nfunction renderRuleNote(){\n  const el=$('#ruleNote'); if(!el) return;\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const mv=SESS.timeline[SESS.selected];\n  let h='';\n  h+=`<b style=\"color:var(--ink)\">Base de municípios dominantes:</b> só os municípios que a base (CSV) atribui ao setor entram no mapa e na alocação`;\n  if(mv&&mv.domCsvMunis) h+=` · <b>${fmt(mv.domCsvMunis)}</b> na base do setor ${mv.sector}`;\n  if(mv&&mv.excludedCbs) h+=` · <b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs)} CBs</b> fora da base nesta etapa — não aparecem no mapa e as CBs seguem suas líderes (quebra de vínculo)`;\n  if(DOM.error) h+=` · <b style=\"color:var(--closing)\">⚠ base indisponível: ${esc(DOM.error)}</b>`;\n  h+=`<br>`;\n  h+=`<b style=\"color:var(--ink)\">Regra de destino:</b> `;\n  h+= st.destRule==='dominant'\n    ? `só municípios com <b>≥${fmt(st.destMinCbs)} CBs</b> trazem setor pra lista de destinos`\n    : `qualquer município do setor fechado vale como ponte <span style=\"opacity:.7\">(padrão)</span>`;\n  if(mv&&mv.munis&&st.destRule==='dominant'){\n    const ig=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__'&&mv.munis[m].cbs<st.destMinCbs);\n    if(ig.length) h+=` · <b style=\"color:var(--closing)\">${fmt(ig.length)}</b> ignorado(s) como ponte (ainda redistribuído(s)): ${ig.slice(0,5).join(', ')}${ig.length>5?' +'+(ig.length-5):''}`;\n    if(mv.destRuleFallback) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — regra relaxada pra \"qualquer município\".</b>`;\n  }\n  el.innerHTML=h;\n}\nfunction renderSpanRule(){\n  const el=$('#spanRule'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected], lim=spanLimits();\n  if(!mv||!mv.munis||mv.view==='asis'||!spanLimitsOn(lim)){ el.innerHTML=''; return; }\n  const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,lim);\n  const bits=[];\n  if(lim.target) bits.push(`alvo <b>${fmt(lim.target)}</b>`);\n  if(lim.max) bits.push(`máx <b>${fmt(lim.max)}</b>`);\n  if(lim.min) bits.push(`mín <b>${fmt(lim.min)}</b>`);\n  let h=`Pré-requisito de span: ${bits.join(' · ')} CBs — `;\n  if(v.over.length||v.under.length){\n    h+=`<b style=\"color:var(--closing)\">⚠ `;\n    if(v.over.length) h+=`acima do máximo: ${v.over.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    if(v.over.length&&v.under.length) h+=' · ';\n    if(v.under.length) h+=`abaixo do mínimo: ${v.under.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    h+=`</b>`;\n    if(mv.optTier===1) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação contígua respeita o span mínimo nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===2) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação contígua respeita os limites nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===-1) h+=`<br><span style=\"opacity:.85\">Etapa resolvida por heurística — os limites não são garantidos.</span>`;\n  } else h+=`<b style=\"color:var(--ok)\">✓ todos os setores receptores dentro dos limites</b>`;\n  el.innerHTML=`<div class=\"note\" style=\"margin:-2px 0 6px\">${h}</div>`;\n}\nfunction renderContig(){const mv=SESS.timeline[SESS.selected];const el=$('#contig');if(!el||!mv)return;\n  if(mv.view==='asis'){el.innerHTML='';return;}\n  const bad=contigBadFor(mv,mv.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Contiguidade quebrada em ${bad.join(', ')} — município(s) desconectado(s) do território do setor. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Contíguo — cada setor forma uma região conectada.</div>`;\n}\nfunction closingCbsOf(mv){ return Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0); }\nfunction sectorTotalCbs(mv){ return Object.values(mv.munis).reduce((a,o)=>a+o.cbs,0); }\nfunction renderKpis(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){const mc=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(sectorTotalCbs(mv)),l:`CBs no setor ${mv.sector} (antes desta etapa)`,t:'Total de CBs no setor hoje — já considerando o que etapas anteriores da timeline mandaram pra cá.'},\n      {n:mv.csvMunisTotal?`${fmt(mc)}/${fmt(mv.csvMunisTotal)}`:fmt(mc),l:'Municípios do setor',t:`Municípios desenhados nesta etapa${mv.csvMunisTotal?` de ${fmt(mv.csvMunisTotal)} que a base de dominantes atribui ao setor`:''}.${(mv.missingMunis||[]).length?' Fora do mapa: '+mv.missingMunis.map(x=>x.muni+' ('+x.reason+')').join(' · '):''}${Object.keys(mv.ufGuessed||{}).length?' UF inferida da UF dominante do setor (nome repetido em outra UF carregada): '+Object.entries(mv.ufGuessed).map(([nm,u])=>nm+'→'+u).join(', '):''}`},\n      {n:fmt(mv.leaders.length),l:'Líderes (grupos)',t:'Líderes presentes no setor nesta etapa.'},\n      {n:fmt(mv.dest.length),l:'Setores adjacentes',t:`Setores vizinhos (ainda abertos) da MESMA gerência de vendas (CD_GV ${mv.gv||'?'}) que poderiam receber os municípios.${(mv.gvRejected||[]).length?' Fora do mapa, por serem de outra gerência: '+mv.gvRejected.join(', ')+'.':''}`},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=mv.dest.map(s=>c.span[s]);const spread=sv.length?Math.max(...sv)-Math.min(...sv):0;\n  const zero=mv.dest.filter(s=>c.gained[s]===0);const totCbs=closingCbsOf(mv);\n  $('#kpis').innerHTML=[\n    {n:fmt(c.desloc),l:mv.policy==='fora'?'CBs fora do setor':'CBs c/ quebra de relação',cls:'warn',t:'CBs cuja líder acabou noutro setor nesta etapa.'},\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:'Diferença de tamanho entre o maior e o menor setor depois desta etapa.'},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs do setor ${mv.sector} sendo redistribuídas nesta etapa.`},\n    {n:zero.length?zero.join(', '):'nenhum',l:'Setor sem líder herdada',cls:zero.length?'warn':'ok',t:'Setor que recebe município(s) mas nenhuma líder nesta etapa.'},\n  ].concat(mv.excludedCbs?[{n:fmt(mv.excludedCbs),l:`CBs ignoradas · ${fmt(mv.excludedMunis.length)} município(s) fora da base`,cls:'warn',t:'CBs em municípios que a base de dominantes não atribui a este setor: ficam fora do mapa e da alocação geográfica e seguem a líder do seu grupo.'}]:[])\n   .concat((mv.missingMunis||[]).length?[{n:fmt(mv.missingMunis.length),l:`Município(s) da base fora do mapa · ${fmt(mv.csvMunisTotal)} na base`,cls:'warn',t:`A base de dominantes atribui ${fmt(mv.csvMunisTotal)} município(s) ao setor ${mv.sector} e ${fmt(mv.csvMunisTotal-mv.missingMunis.length)} chegaram ao mapa. Fora: ${mv.missingMunis.map(x=>x.muni+' — '+x.reason+(x.cbs?' ('+fmt(x.cbs)+' CBs)':'')).join(' · ')}`}]:[])\n   .map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){const items=[{s:mv.sector,val:sectorTotalCbs(mv),color:CLOSE_COLOR}].concat(mv.dest.map(s=>({s,val:+(mv.baseMetrics[s]?.cbs||0),color:mv.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura antes desta etapa — o setor <b>${mv.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const lim=spanLimits(),on=spanLimitsOn(lim);\n  const rawMax=Math.max(...mv.dest.map(s=>c.span[s]),lim.target||0,lim.max||0,1);\n  const max=on?rawMax*1.04:rawMax;   // headroom so a marker sitting at the scale top stays visible\n  const mk=(v,color,label)=>v?`<div class=\"bar-mark\" style=\"left:${Math.min(99.6,v/max*100).toFixed(1)}%;background:${color}\" title=\"${label}\"></div>`:'';\n  const marks=mk(lim.min,'#ff9500','span mínimo '+fmt(lim.min))+mk(lim.target,'#007aff','span alvo '+fmt(lim.target))+mk(lim.max,'#ff3b30','span máximo '+fmt(lim.max));\n  $('#spans').innerHTML=mv.dest.map(s=>{const base=+(mv.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    const over=lim.max&&c.span[s]>lim.max,under=lim.min&&c.span[s]<lim.min;\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${mv.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${mv.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${mv.color[s]}\"></div>${marks}</div>\n      <div class=\"bar-val\" style=\"${over?'color:var(--closing);font-weight:700':(under?'color:#ff9500;font-weight:700':'')}\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span>${over?' ▲':''}${under?' ▼':''}</div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base (já incluindo etapas anteriores) · cheio = CBs entrantes do ${mv.sector} nesta etapa.${c.orphanTotal?` Inclui <b>${fmt(c.orphanTotal-c.orphanStranded)}</b> CB(s) de municípios fora da base de dominantes que seguiram suas líderes.`:''}${on?' Marcas: <b style=\"color:#007aff\">│</b> alvo · <b style=\"color:#ff3b30\">│</b> máx · <b style=\"color:#ff9500\">│</b> mín.':''}</div>`;\n}\n// Two very different reasons land rows in the __OUTROS__ bucket — no IBGE shape, or outside the\n// sector's dominance base. Same bucket computationally, but they must not read as the same thing.\nfunction outsideNote(mv){\n  const bits=[];\n  if(mv.noShapeCbs) bits.push(`<b>${fmt(mv.noShapeCbs)}</b> CBs em ${fmt(mv.noShapeMunis.length)} município(s) sem shape na UF`);\n  if(mv.excludedCbs) bits.push(`<b>${fmt(mv.excludedCbs)}</b> CBs em ${fmt(mv.excludedMunis.length)} município(s) <b>fora da base de dominantes</b> deste setor — <b style=\"color:var(--closing)\">quebra de vínculo</b>`);\n  if(!bits.length) return '';\n  let h=`<div class=\"note\">Fora do mapa e da alocação geográfica: ${bits.join(' · ')}.`;\n  if(mv.excludedCbs){\n    const c=computeDerived(mv);\n    h+=` As <b>${fmt(mv.excludedCbs)}</b> CBs em quebra de vínculo <b>seguem a líder do seu grupo</b>`;\n    const byS=mv.dest.filter(s=>c.orph[s]).map(s=>`<b style=\"color:${mv.color[s]}\">${s}</b> (${fmt(c.orph[s])})`);\n    h+=byS.length?`: ${byS.join(' · ')}.`:'.';\n    if(c.orphanStranded) h+=` <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} CB(s) sem destino</b> — a líder não ficou com nenhum município realocado, então não há setor pra seguir.`;\n  }\n  if(mv.noShapeCbs) h+=` As CBs sem shape na UF continuam fora de qualquer setor destino.`;\n  if(mv.excludedMunis&&mv.excludedMunis.length) h+=`<br><span style=\"opacity:.85\">${mv.excludedMunis.slice(0,8).map(x=>x.muni+' ('+fmt(x.cbs)+')').join(', ')}${mv.excludedMunis.length>8?' +'+(mv.excludedMunis.length-8):''}</span>`;\n  return h+`</div>`;\n}\nfunction renderMuni(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  if(mv.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">setor ${mv.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje todos pertencem ao setor <b>${mv.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`+outsideNote(mv);return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${mv.dest.map(s=>`<button data-s=\"${s}\" class=\"${mv.assign[m]===s?'on':''}\" style=\"${mv.assign[m]===s?'background:'+mv.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +outsideNote(mv);\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{const m=b.parentElement.dataset.m,s=b.dataset.s;editSelected(m2=>{m2.assign[m]=s;});});\n}\nfunction renderLeaders(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>Setor</th></tr></thead><tbody>`\n    +mv.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${mv.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(mv.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  const showOrph=c.orphanTotal>0;\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${mv.dest.map(s=>`<th>${s}</th>`).join('')}${showOrph?'<th title=\"CBs em municípios fora da base de dominantes que seguiram esta líder\">Órfãs</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td>${L.home?`<span class=\"home\" style=\"background:${mv.color[L.home]}\"></span>${L.home}`:'–'}</td>\n     ${mv.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+mv.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     ${showOrph?`<td class=\"cell\" style=\"${L.orphan&&!L.orphanHome?'color:var(--closing);font-weight:700':''}\" title=\"${L.orphan?(L.orphanHome?'seguem a líder para o setor '+L.orphanHome:'sem home — CBs sem setor pra seguir'):''}\">${L.orphan?fmt(L.orphan)+(L.orphanHome?'':' ⚠'):'·'}</td>`:''}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${mv.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}${showOrph?`<td>${fmt(c.orphanTotal)}</td>`:''}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // `_out` is a flag stamped on the ROW, and rows are shared between etapas (absorbed + inherited\n  // orphans), so the last etapa to aggregate wins and reading it here can describe the wrong etapa.\n  // Derive the bucket from THIS mv instead. Summing rather than .find() because several out-municípios\n  // of the same group all collapse into __OUTROS__, and only the first was being counted.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cell=(m,g)=>mv.matrix.reduce((a,x)=>a+((((outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))===m)&&String(x.grupo)===g)?+x.cbs:0),0);\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  // No destination column here. This is the AS-IS matrix: every município in it still belongs to the\n  // closing sector, so showing where it is HEADED mixed the two states of the world in one table and\n  // made the ASIS view read like a plan. The destination lives on the map and in \"Municípios → setor\n  // destino\", which are about the to-be.\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\n// TOBE counterpart of the AS-IS matrix: the same município × grupo grid plus the two columns the AS-IS\n// view deliberately refuses to carry — where the município ends up, and under which líder.\n// \"Líder resultante\" is the líder holding the most CBs in that município. A município is served by several\n// grupos at once and the data names no single owner, so the dominant one is the honest answer and its\n// share is printed beside it instead of being hidden behind a single name.\nfunction renderMatrixTobe(){const mv=SESS.timeline[SESS.selected]; const el=$('#matrixTobe'); const nt=$('#tobeNote');\n  if(!el) return;\n  if(!mv||!mv.munis){ el.innerHTML=''; if(nt) nt.innerHTML=''; return; }\n  const c=computeDerived(mv);\n  const homeOf={}; c.leaders.forEach(L=>{ homeOf[L.grupo]=L.home; });\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // One pass over the matrix instead of a scan per cell: the AS-IS table can afford the naive version\n  // on six municípios, but this one asks for the dominant líder too, which multiplies the lookups.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cellBy={}; mv.matrix.forEach(x=>{ const k=(outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))+'|'+String(x.grupo);\n    cellBy[k]=(cellBy[k]||0)+ +x.cbs; });\n  const cell=(m,g)=>cellBy[m+'|'+g]||0;\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  const domLeader=m=>{ let best=null,bc=0;\n    mv.leaders.forEach(L=>{ const v=cell(m,L.grupo); if(v>bc){ bc=v; best=L; } });\n    return best?{L:best,cbs:bc}:null; };\n  const short=L=>(L.lider||'').split(' ').slice(0,2).join(' ')||('grupo '+L.grupo);\n  // NOT named `rows` — that is the module-level Databricks response helper, and shadowing it inside a\n  // render function is a trap for whoever adds a query here later.\n  const recs=ms.map(m=>{ const d=mv.assign[m]||null, dl=domLeader(m);\n    const home=dl?(homeOf[dl.L.grupo]||null):null;\n    // The mismatch worth seeing: the territory goes to one setor while the líder serving it ends up in\n    // another. Under \"manter vínculo\" those CBs stay with her, which is exactly what \"CBs fora do setor\"\n    // counts — so the → Setor cell carries the warning; her identity lives in the líder-per-grupo\n    // footer below, not in a per-município column.\n    return {m:m,d:d,dl:dl,home:home,off:!!(dl&&home&&d&&home!==d)}; });\n  el.innerHTML=`<thead><tr><th>Município</th><th>→ Setor</th>${mv.leaders.map(L=>`<th title=\"${esc(L.lider||'')}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +recs.map(r=>`<tr><td>${r.m}</td>`\n     +`<td${r.off?' style=\"color:var(--closing)\"':''} title=\"${r.off?'a líder dominante deste município termina noutro setor':''}\">${r.d?`<span class=\"home\" style=\"background:${mv.color[r.d]}\"></span>${r.d}`:'–'}${r.off?' ⚠':''}</td>`\n     +mv.leaders.map(L=>{const v=cell(r.m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[r.m].cbs)}</td></tr>`).join('')\n   // One line at the very bottom of the table, not one per município: each grupo COLUMN belongs to\n   // exactly one líder — that is fixed per column, never varies row to row — so her name sits once,\n   // under her own column, instead of being repeated on every município she happens to dominate.\n   +`<tr class=\"lider-foot\"><td colspan=\"2\">Líder do grupo</td>`\n     +mv.leaders.map(L=>`<td title=\"${esc(L.lider||'')}\">${esc(short(L))}</td>`).join('')\n     +`<td></td></tr>`\n   +`</tbody>`;\n  if(nt){\n    const offN=recs.filter(r=>r.off).length;\n    nt.innerHTML=(mv.policy!=='fora')\n      ? `Política <b>quebrar relação</b>: as CBs deixam a líder atual e passam à estrutura do setor destino — a líder de cada grupo (rodapé) mostra quem as atende <b>hoje</b>, não quem assume depois.`\n      : `<b>Líder do grupo</b> = a líder titular de cada grupo (coluna), mostrada uma vez no rodapé da tabela. `\n        +(offN?`<span style=\"color:var(--closing)\">${fmt(offN)} município(s) ⚠</span> têm a líder dominante indo para um setor diferente do dela — as CBs seguem a líder (é disso que o KPI “CBs fora do setor” é feito).`\n          :`Em todos os municípios a líder dominante termina no mesmo setor que recebe o território.`);\n  }\n}\nfunction renderPolicy(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  $('#policyNote').innerHTML=mv.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutro setor</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder do setor destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  mv._plan=ms.map(m=>({municipio:mName(m),uf:mUf(m),cbs:mv.munis[m].cbs,destino:mv.assign[m]}));\n  $('#plan').innerHTML=`Etapa ${SESS.selected+1} · Setor <b>${mv.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${mv.color[mv.assign[m]]}\">${mv.assign[m]||'–'}</b>`).join(' · ')}<br>\n   Política: <b>${mv.policy==='fora'?'manter vínculo (CBs fora do setor)':'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas nesta etapa.`\n   +(c.orphanTotal?`<br>Fora da base de dominantes: ${fmt(c.orphanTotal)} CB(s) seguem suas líderes — ${mv.dest.filter(s=>c.orph[s]).map(s=>`${s} (${fmt(c.orph[s])})`).join(' · ')||'–'}${c.orphanStranded?` · <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} sem líder com destino</b>`:''}. Não entram no CSV, que é por município.`:'');\n}\nfunction renderReport(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  mv.metric=mv.metric||'cbs';\n  const KEYS=METRICS_DEF.map(m=>m[0]);\n  const incBy={};mv.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incBy[s])return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  // orphan CBs land on their leader's home sector, not on a geographic destination\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home&&incBy[L.home]) KEYS.forEach(k=>incBy[L.home][k]+=om[k]||0); });\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  // they leave the closing sector too, so antes → depois still balances\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home) KEYS.forEach(k=>closeTot[k]+=om[k]||0); });\n  const trend=mv.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=mv.dest.map(s=>({label:s,color:mv.color[s],antes:+(mv.baseMetrics[s]?.[mk]||0),depois:+(mv.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:mv.sector+'✕',color:'#ff3b30',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#34c759':'#ff3b30'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois desta etapa</h2>\n      <select id=\"metricSel\">${METRICS_DEF.map(m=>`<option value=\"${m[0]}\" ${m[0]===mv.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes desta etapa</b> (já incluindo etapas anteriores) · barra cheia = <b>depois</b> · <b style=\"color:var(--acc)\">${mv.sector}✕</b> = setor fechado nesta etapa · % = variação</div>\n    ${chartHTML(mv.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por setor receptor (snapshot congelado)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${METRICS_DEF.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const b=mv.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      METRICS_DEF.map(([k])=>{const bv=+b[k]||0,av=bv+(inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=METRICS_DEF.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${mv.sector} (sai)</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};mv.dest.forEach(s=>incLvl[s]={});\n  Object.keys(mv.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incLvl[s])return;const lv=mv.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  c.leaders.forEach(L=>{ const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n    if(ol&&L.home&&incLvl[L.home]) Object.entries(ol).forEach(([nv,n])=>incLvl[L.home][nv]=(incLvl[L.home][nv]||0)+n); });\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por setor receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>Setor</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const base=mv.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> desta etapa · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${mv.sector}.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${mv.sector} — últimos ${trend.length} ciclos até o snapshot congelado</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Tendência histórica <b>do próprio setor</b> — não ajustada pela absorção hipotética de etapas anteriores (limitação conhecida: tendência é por setor, não decomponível por município).</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{mv.metric=sel.value; renderReport();};\n}\nfunction renderCumulative(){\n  const el=$('#tab-cumulative'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<div class=\"note\">Nenhuma etapa na timeline ainda.</div>'; return; }\n  const ready=SESS.timeline.every(mv=>mv.munis);\n  if(!ready){ el.innerHTML='<div class=\"note\">Aguardando etapas terminarem de calcular…</div>'; return; }\n  const finalWorld=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const openSectors=[...new Set(Object.values(finalWorld.muniOverride))];\n  let h=`<h2 style=\"margin:0 0 8px\">Linha do tempo — ${SESS.timeline.length} fechamento(s)</h2>\n    <div class=\"note\" style=\"margin:-2px 0 10px\">Setores fechados, em ordem: ${SESS.timeline.map(mv=>mv.sector).join(' → ')}. Cada linha abaixo soma o efeito de <b>toda a cadeia</b>, não apenas da última etapa.</div>`;\n  h+=`<div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th>${METRICS_DEF.map(([,l])=>`<th>${l}</th>`).join('')}</tr></thead><tbody>`;\n  SESS.timeline.forEach((mv,i)=>{\n    h+=`<tr style=\"opacity:.75\"><td>${mv.sector}</td><td>fechado (etapa ${i+1})</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(mv.pureOwnMetrics?.[k]||0)}</td>`).join('')+`</tr>`;\n  });\n  openSectors.forEach(s=>{\n    const base=SESS.sectorBaseCache[s]||{};\n    const infl=finalWorld.sectorInflow[s]||{};\n    h+=`<tr><td>${s}</td><td>recebeu território</td>`+METRICS_DEF.map(([k,,t])=>{\n      const bv=+base[k]||0, av=bv+(infl[k]||0); const d=bv?((av-bv)/bv*100):0; const f=(t==='$'?money:fmt);\n      return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;\n    }).join('')+`</tr>`;\n  });\n  h+=`</tbody></table></div><div class=\"note\">\"Antes\" de um setor receptor é o snapshot congelado (sem nenhum fechamento aplicado); \"depois\" soma tudo que a cadeia inteira jogou pra ele.</div>`;\n  el.innerHTML=h;\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=700,H=500,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\n// ---------- label placement ----------\n// Pole of inaccessibility: the interior point farthest from ANY boundary of the dissolved area. A\n// centroid is only a mean — on a concave sector, one split across a river, or a member whose vertex\n// average falls in a bay, it lands outside the very territory it is supposed to name, which is what\n// pushed the sector numbers off their own shapes. Even-odd across every ring of every member is a\n// correct union test here because municipalities tile rather than overlap.\n// The search box is clipped to the viewBox, so the result is inside the territory AND on screen.\n// Rings are decimated first — this is a label, not a measurement — which keeps a 20-município sector\n// to a few million ops, and the result is cached per stage: geometry never changes between renders,\n// only the fill does.\nfunction labelPointFor(fs,pr){\n  const rings=[];\n  (fs||[]).forEach(f=>{ if(!f||!f.geometry) return;\n    const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>{\n      if(!r||r.length<4) return;\n      const step=Math.max(1,Math.floor(r.length/120)),pts=[];\n      for(let i=0;i<r.length;i+=step) pts.push(pr(r[i]));\n      if(pts.length>3) rings.push(pts);\n    }));});\n  if(!rings.length) return null;\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  mnx=Math.max(mnx,2); mny=Math.max(mny,2); mxx=Math.min(mxx,698); mxy=Math.min(mxy,498);\n  if(!(mxx>mnx&&mxy>mny)) return null;\n  const inside=(x,y)=>{ let c=false;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      if(((a[1]>y)!==(b[1]>y)) && (x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])) c=!c; }\n    return c; };\n  const edgeDist=(x,y)=>{ let m=Infinity;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      const dx=b[0]-a[0],dy=b[1]-a[1],L=dx*dx+dy*dy;\n      let t=L?(((x-a[0])*dx+(y-a[1])*dy)/L):0; t=t<0?0:(t>1?1:t);\n      const ex=a[0]+t*dx-x,ey=a[1]+t*dy-y,d=ex*ex+ey*ey; if(d<m)m=d; }\n    return Math.sqrt(m); };\n  const W=mxx-mnx,H=mxy-mny,N=26;\n  let best=null,bd=-1;\n  for(let i=0;i<=N;i++) for(let j=0;j<=N;j++){\n    const x=mnx+W*i/N,y=mny+H*j/N;\n    if(!inside(x,y)) continue;\n    const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; }\n  }\n  if(!best) return null;\n  // Hill-climb off the coarse grid: the grid only has to land somewhere in the right lobe, the\n  // refinement finds the deep point inside it — halving the step whenever no neighbour improves.\n  let step=Math.max(W,H)/N/2;\n  for(let k=0;k<7;k++){\n    let moved=false;\n    for(const o of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]){\n      const x=best[0]+o[0]*step,y=best[1]+o[1]*step;\n      if(x<mnx||x>mxx||y<mny||y>mxy||!inside(x,y)) continue;\n      const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; moved=true; }\n    }\n    if(!moved) step/=2;\n  }\n  // The clearance travels with the point: it is the radius of the largest circle that fits inside the\n  // territory there, which is exactly the budget a caller needs to decide how much text will fit.\n  return {p:best,r:bd};\n}\nfunction labelPointOf(mv,key,fs,pr){\n  if(!mv._lbl) mv._lbl={};\n  if(!(key in mv._lbl)) mv._lbl[key]=labelPointFor(fs,pr);\n  return mv._lbl[key];\n}\nfunction drawMap(){\n  const mv=SESS.timeline[SESS.selected]; const svg=$('#map'); const ignNote=$('#mapIgnoredNote');\n  if(!mv){ svg.innerHTML=''; if(ignNote) ignNote.innerHTML=''; return; }\n  const c=computeDerived(mv);const NS='http://www.w3.org/2000/svg'; svg.innerHTML='';\n  const feats=[...mv.nbrFeat,...Object.values(mv.geoFeat)]; if(!feats.length) return;\n  const pr=project(feats);\n  const world=foldWorldState(SESS.timeline,SESS.selected);\n  // The context layer is ONE area per SECTOR, not one shape per município. The unit of this decision\n  // is the destination sector; a scatter of individually outlined municipalities carrying their own\n  // names invited exactly the wrong reading of the map. Members of a sector are filled AND stroked in\n  // the same colour, so the shared borders vanish and the group reads as a single dissolved\n  // territory — the IBGE shapes do not share vertices precisely enough to extract a clean union\n  // outline, and carrying a real polygon-clipping union for a context layer is not worth it.\n  // Labels are COLLECTED here and drawn last, after every path. Drawn inline they were buried by the\n  // next sector's fill, and placement cannot avoid a collision it has not seen yet.\n  const labels=[],placed=[];\n  const textW=(s,fs)=>String(s).length*fs*0.56;\n  // WCAG relative luminance of the fill a label sits on, so ink and halo can invert per label. \"Would\n  // white be better?\" has no single answer on this map: white reads on #5856d6 (L≈0.14) and disappears\n  // on #ff9500 and #34c759 (both L≈0.43, where black wins ~9:1 against white's ~2.2:1) and on every\n  // context fill, which is .32 opacity over white. Decided per label, never globally.\n  const lum=col=>{let h=String(col||'').replace('#','');\n    if(h.length===3) h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];\n    if(h.length!==6) return 1;\n    const c=[0,2,4].map(i=>parseInt(h.slice(i,i+2),16)/255)\n      .map(v=>v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4));\n    return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2];};\n  // Centred on (x,y) in BOTH axes. The old box hung off the first line's baseline, so every two-line\n  // label sat half its own height below the point it was supposed to mark.\n  const rectOf=(x,y,lines)=>{const w=Math.max.apply(null,lines.map(l=>textW(l.t,l.size)));\n    const h=lines.reduce((a,l)=>a+l.size*1.2,0);\n    return {x1:x-w/2-2.5,x2:x+w/2+2.5,y1:y-h/2-1.5,y2:y+h/2+1.5};};\n  const nbrBySec={};\n  mv.nbrFeat.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf);\n    const s=String((mv.nbrCurrentSector&&mv.nbrCurrentSector[nm])||'');\n    (nbrBySec[s]=nbrBySec[s]||[]).push(f);\n  });\n  Object.keys(nbrBySec).forEach(s=>{\n    const fs=nbrBySec[s];\n    const isDest=!!s&&mv.dest.includes(s);\n    const closedCtx=!!s&&world.closedSectors.has(s);\n    const col=isDest?(mv.color[s]||null):null;\n    fs.forEach(f=>{\n      const p=document.createElementNS(NS,'path');\n      p.setAttribute('d',pathFor(f,pr));\n      p.setAttribute('class','sect'+(isDest?'':(closedCtx?' closed-ctx':' plain')));\n      if(col){ p.setAttribute('fill',col); p.setAttribute('stroke',col); }\n      svg.appendChild(p);\n    });\n    if(!s) return;\n    // One number per sector, at the pole of inaccessibility of its DISSOLVED area — the whole group\n    // of members at once, not a representative member, so the number sits in the visual middle of the\n    // territory the operator actually sees.\n    const lp=labelPointOf(mv,'s:'+s,fs,pr);\n    // Context areas always read LIGHT — .sect paints dest colours at fill-opacity .32 over white, and\n    // plain/closed-ctx are near-white greys — so a sector number never wants white ink, whatever colour\n    // the sector itself was assigned.\n    if(lp&&lp.p) labels.push({anchor:lp.p,big:true,dark:false,lines:[{t:s,size:11.5,weight:700}]});\n  });\n  const asis=mv.view==='asis';\n  // The UF prefix is information only when the stage actually straddles a border. On a single-UF setor\n  // it is the same three characters repeated on every label — a large share of the clutter and none of\n  // the meaning. The hover tooltip carries the UF either way.\n  const showUf=new Set(Object.keys(mv.geoFeat).map(k=>mUf(k)).filter(Boolean)).size>1;\n  Object.entries(mv.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n    const fillCol=asis?CLOSE_COLOR:(mv.color[mv.assign[m]]||'#444');\n    p.setAttribute('class','muni');p.setAttribute('fill',fillCol);\n    if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[m]);if(m2.dest.length)m2.assign[m]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=m;});};\n    p.style.cursor=asis?'default':'pointer';\n    p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n      t.innerHTML=asis?`<b>${mDisp(m)}</b><br>${fmt(mv.munis[m]?.cbs||0)} CBs · setor ${mv.sector}`:`<b>${mDisp(m)}</b><br>${fmt(mv.munis[m]?.cbs||0)} CBs → ${mv.assign[m]||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n    p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n    // The name is never cut and never dropped. The pole of inaccessibility is still where the label\n    // WANTS to sit — the visual middle of the territory rather than a centroid that can fall outside a\n    // concave shape — but its clearance no longer gates anything: cutting BA-SAO SEBASTIAO DO PASSE\n    // down to BA-SAO SEBAST saved space by destroying the one thing the label existed to convey, and\n    // measuring the name against the inscribed circle rejected the centre for nearly every município.\n    const lp=labelPointOf(mv,'m:'+m,[f],pr);\n    const at=(lp&&lp.p)||cxy(f,pr);\n    const nmFull=(showUf&&mUf(m)?mUf(m)+'-':'')+mName(m);\n    const num=fmt(mv.munis[m]?.cbs||0)+(asis?'':'·'+(mv.assign[m]||'–'));\n    labels.push({anchor:at,cbs:+(mv.munis[m]?.cbs||0),dark:lum(fillCol)<0.18,\n      lines:[{t:nmFull,size:8.2,weight:700},{t:num,size:7,weight:500,mut:true}]});\n  });\n  // Placement, not de-cluttering. Every label IS drawn: it starts at its anchor and, if the text does\n  // not fit the territory or the slot is already taken, walks outward ring by ring until it finds\n  // clear canvas, trailing a hairline leader back to the shape it names. The previous pass dropped the\n  // loser of every collision outright, which is why municípios silently had no name at all.\n  // Order still matters — sector numbers first, then municípios by CB weight — because the earlier a\n  // label is placed the closer to home it lands.\n  const CW=700,CH=500,CPAD=2;\n  const inBounds=r=>r.x1>=CPAD&&r.x2<=CW-CPAD&&r.y1>=CPAD&&r.y2<=CH-CPAD;\n  const hits=r=>placed.reduce((a,q)=>a+((r.x2<q.x1||r.x1>q.x2||r.y2<q.y1||r.y1>q.y2)?0:1),0);\n  // Ring 0 is the anchor itself. Rings step 9u out over 16 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs.\n  const CAND=[[0,0]];\n  for(let ring=1;ring<=7;ring++){ const d=ring*9;\n    for(let a=0;a<16;a++){ const th=a*Math.PI/8; CAND.push([Math.cos(th)*d*1.35,Math.sin(th)*d]); } }\n  labels.sort((a,b)=>(b.big?1:0)-(a.big?1:0)||((b.cbs||0)-(a.cbs||0)));\n  labels.forEach(L=>{\n    let chosen=null,best=null,bestN=Infinity;\n    // Centred on the anchor is ALWAYS the first choice. An earlier cut here refused the anchor whenever\n    // the text was wider than the município's inscribed circle — true of almost every real name at this\n    // scale (BA-SAO SEBASTIAO DO PASSE is ~140u against ~80u of clearance), so every single label got\n    // nudged outside and nothing ended up centred on anything. Spilling past a border is normal on a\n    // choropleth and the halo keeps it readable; only a genuine COLLISION earns a move.\n    for(const off of CAND){\n      const x=L.anchor[0]+off[0], y=L.anchor[1]+off[1];\n      const r=rectOf(x,y,L.lines);\n      if(!inBounds(r)) continue;\n      const n=hits(r);\n      if(!n){ chosen={x,y,r,off}; break; }\n      // No clear slot yet — remember the LEAST crowded one. Falling back to the first in-bounds\n      // candidate meant falling back to the anchor every time, so a crowded map piled every loser onto\n      // the same point. CAND is ordered nearest-first and the compare is strict, so ties keep the\n      // closest slot.\n      if(n<bestN){ bestN=n; best={x,y,r,off}; }\n    }\n    // Canvas genuinely full: take the nearest in-bounds slot and accept the overlap. A label sitting\n    // on top of another is recoverable by eye; a name that was never drawn is not.\n    if(!chosen) chosen=best||{x:L.anchor[0],y:L.anchor[1],r:rectOf(L.anchor[0],L.anchor[1],L.lines),off:[0,0]};\n    placed.push(chosen.r);\n    const g=document.createElementNS(NS,'g'); g.setAttribute('class','lbl'+(L.big?' big':''));\n    if(Math.abs(chosen.off[0])>0.5||Math.abs(chosen.off[1])>0.5){\n      // The leader stops at the label's edge rather than its centre, so it never runs under the glyphs.\n      const cx=(chosen.r.x1+chosen.r.x2)/2, cy=(chosen.r.y1+chosen.r.y2)/2;\n      const hw=(chosen.r.x2-chosen.r.x1)/2, hh=(chosen.r.y2-chosen.r.y1)/2;\n      const dx=L.anchor[0]-cx, dy=L.anchor[1]-cy;\n      const tt=Math.min(dx?hw/Math.abs(dx):Infinity, dy?hh/Math.abs(dy):Infinity);\n      const ln=document.createElementNS(NS,'line'); ln.setAttribute('class','lead');\n      ln.style.stroke=L.dark?'#fff':'#1c1c1e';\n      ln.setAttribute('x1',L.anchor[0].toFixed(1)); ln.setAttribute('y1',L.anchor[1].toFixed(1));\n      ln.setAttribute('x2',(cx+dx*tt).toFixed(1)); ln.setAttribute('y2',(cy+dy*tt).toFixed(1));\n      g.appendChild(ln);\n    }\n    const hTot=L.lines.reduce((a,l)=>a+l.size*1.2,0);\n    let ty=chosen.y-hTot/2+L.lines[0].size*0.86;\n    // Inline style rather than a class: a stylesheet rule would outrank a presentation attribute and\n    // undo the per-label contrast decision made above.\n    const ink=L.dark?'#fff':'#1c1c1e', inkMut=L.dark?'rgba(255,255,255,.85)':'#48484d';\n    const halo=L.dark?'rgba(0,0,0,.6)':'#fff';\n    L.lines.forEach(l=>{ const t=document.createElementNS(NS,'text');\n      t.setAttribute('x',chosen.x.toFixed(1)); t.setAttribute('y',ty.toFixed(1));\n      t.setAttribute('font-size',l.size); t.setAttribute('font-weight',l.weight||600);\n      t.style.fill=l.mut?inkMut:ink; t.style.stroke=halo;\n      t.textContent=l.t; g.appendChild(t); ty+=l.size*1.2; });\n    svg.appendChild(g);\n  });\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>Setor ${mv.sector} (estrutura antes desta etapa — ${Object.keys(mv.geoFeat).length} municípios)</span>`\n    // No \"vizinhos (contexto)\" or \"setor já fechado antes\" entry any more: every feature still drawn\n    // belongs to a destination sector, so .sect.plain and .sect.closed-ctx can no longer be produced\n    // and a legend key for them would point at nothing on the map.\n    :mv.dest.map(s=>`<span><b style=\"background:${mv.color[s]}\"></b>Setor ${s} — ${c.gained[s]} líder(es)</span>`).join('');\n  // Municipalities outside the dominance base are deliberately NOT drawn — no shape, no hover, no\n  // legend entry. They are reported in the panel instead, so the map only ever shows territory that\n  // is actually being reallocated.\n  if(ignNote) ignNote.innerHTML=(mv.excludedMunis&&mv.excludedMunis.length)\n    ? `<b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs||0)} CBs</b> fora da base de dominantes deste setor não são desenhados no mapa — as CBs seguem a líder do seu grupo (detalhe em “Municípios → setor destino”).`\n    : '';\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{ await ensurePinned(); const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nfunction renderAll(){\n  renderStage();\n  renderTimeline(); renderSectorPicker();\n  const shb=$('#openShare'); if(shb) shb.disabled=!SESS.timeline.length;\n  const dpb=$('#dupSession'); if(dpb) dpb.disabled=!SESS.timeline.length;\n  const mv=SESS.timeline[SESS.selected];\n  // resolveSnapshotPin falls back to a live sub-select when it cannot pin a literal; saying\n  // \"congelado\" in that case would be a lie, so the pill reports what is actually in the SQL.\n  const pinned=SESS.snapLiteral && !/^\\(SELECT/i.test(SESS.snapLiteral);\n  $('#liveState').textContent = SESS.snapshotRaw\n    ? (pinned?'● snapshot congelado · ':'◍ snapshot ao vivo (não fixado) · ')+'atualizado em '+fmtSyncedAt(SESS.snapshotSyncedAt)+' · ↻'\n    : '● iniciando';\n  const mp=$('#basePill');\n  if(mp){\n    const warn=[];\n    if(DOM.bySetor&&!DOM.hasUf) warn.push('⚠ sem coluna UF');\n    if(DOM.badUf) warn.push('⚠ '+fmt(DOM.badUf)+' UF não reconhecida(s)');\n    if(DOM.clashes) warn.push('⚠ '+fmt(DOM.clashes)+' homônimo(s) no mesmo setor');\n    mp.textContent=DOM.bySetor?('◇ base dominantes: '+fmt(DOM.setores)+' setores · '+fmt(DOM.rows)+' municípios'+(warn.length?' · '+warn.join(' · '):'')):(DOM.error?'◇ base dominantes: indisponível':'◇ base dominantes: carregando…');\n    mp.title=DOM.error||(DOM.bySetor?((DOM.hasUf\n        ?('Coluna de UF usada: \"'+DOM.ufCol+'\".')\n        :('SEM coluna de UF. Colunas lidas: '+((DOM.headers||[]).join(' | ')||'?')+'. A UF de cada município vem desta base — sem ela, homônimos voltam a ser casados só pelo nome.'))\n      +(DOM.badUf?('\\n'+DOM.badUf+' linha(s) com UF irreconhecível — tratadas como desconhecida.'):'')\n      +(DOM.clashes?('\\n'+DOM.clashes+' município(s) aparecem no mesmo setor com UFs diferentes. O SQL agrupa só por DS_MUNICIPIO, então esses chegam somados numa linha única e não há como separá-los.'):'')\n      ):'')||DOM.url||''; }\n  const rp=$('#rulePill'); if(rp) rp.textContent='◈ destino: '+ruleShort(SESS.settings);\n  const sp=$('#spanPill'); if(sp) sp.textContent='◎ span: '+spanShort(SESS.settings);\n  renderRuleNote(); renderSpanRule();\n  if(!mv){ ['#map','#matrix','#matrixTobe','#tobeNote','#leaders'].forEach(s=>{const el=$(s);if(el)el.innerHTML='';}); $('#kpis').innerHTML=''; $('#spans').innerHTML=''; $('#muniList').innerHTML=''; $('#plan').innerHTML=''; return; }\n  $('#mapTitle').textContent=`Etapa ${SESS.selected+1}/${SESS.timeline.length} — fechando setor ${mv.sector}`;\n  $('#viewToggle').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.v===mv.view));\n  $('#policy').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===mv.policy));\n  $('#optBadge').textContent=mv.optBadge||'';\n  drawMap(); renderContig(); renderSpanRule(); renderKpis(); renderSpans(); renderMuni(); renderLeaders(); renderMatrix(); renderMatrixTobe(); renderPolicy(); renderPlan(); renderReport(); renderCumulative();\n}\n\n// ---------- event wiring ----------\n$('#go').onclick=()=>addMovement($('#sectorPick').value);\n$('#confirmSnap').onclick=()=>{ SESS.stage='main'; renderAll(); };\n$('#rand').onclick=()=>{\n  const closed=new Set(SESS.timeline.map(mv=>mv.sector));\n  const pool=SESS.sectorsList.filter(s=>!closed.has(s.setor));\n  const cand=pool.filter(s=>s.munis>=3&&s.munis<=10&&s.cbs>300);\n  const list=cand.length?cand:pool; if(!list.length) return;\n  const pick=list[Math.floor(Math.random()*list.length)];\n  $('#sectorPick').value=pick.setor;\n  addMovement(pick.setor);\n};\n$('#sectorPick').addEventListener('keydown',e=>{ if(e.key==='Enter') addMovement($('#sectorPick').value); });\n$('#confirmSessionPick').onchange=()=>loadSession($('#confirmSessionPick').value);\n$('#optim').onclick=()=>editSelected(mv=>{ mv.assign=optimizeFor(mv); });\n$('#autoTobe').onclick=()=>editSelected(mv=>{ mv.assign=autoAssignFor(mv); mv.optBadge='TOBE por adjacência'; });\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.view=b.dataset.v;renderAll();});\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.policy=b.dataset.p;renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','tobe','report','cumulative','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;navigator.clipboard.writeText(JSON.stringify({setor:mv.sector,politica:mv.policy,plano:mv._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;const csv='municipio,uf,cbs,setor_destino\\n'+mv._plan.map(p=>`${p.municipio},${p.uf||''},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${mv.sector}.csv`;a.click();};\n$('#saveSession').onclick=saveSession;\n$('#dupSession').onclick=duplicateSession;\n$('#sessionPick').onchange=()=>loadSession($('#sessionPick').value);\n$('#liveState').onclick=refreshSnapshot;\n$('#liveState').addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); refreshSnapshot(); } });\n$('#openOptions').onclick=openOptions;\n$('#optClose').onclick=closeOptions;\n$('#optCancel').onclick=closeOptions;\n$('#optApply').onclick=applyOptions;\n$('#optMinCbs').addEventListener('input',renderOptPreview);\n['#optTargetSpan','#optMaxSpan','#optMinSpan'].forEach(id=>$(id).addEventListener('input',renderOptPreview));\nroot.querySelectorAll('input[name=\"destRule\"]').forEach(r=>r.addEventListener('change',renderOptPreview));\n$('#optionsBg').addEventListener('click',e=>{ if(e.target.id==='optionsBg') closeOptions(); });\nroot.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeOptions(); closeShare(); } });\n\n$('#openShare').onclick=openShare;\n$('#shareX').onclick=closeShare;\n$('#shareCancel').onclick=closeShare;\n$('#shareGo').onclick=doShare;\n$('#shareCopy').onclick=()=>{ if(!SHARE.url) return; navigator.clipboard.writeText(SHARE.url);\n  const b=$('#shareCopy'); if(b){ b.textContent='copiado ✓'; setTimeout(()=>{ const x=$('#shareCopy'); if(x) x.textContent='Copiar link'; },1500); } };\n$('#shareOpen').onclick=()=>{ if(SHARE.url) window.open(SHARE.url,'_blank'); };\n$('#shareBg').addEventListener('click',e=>{ if(e.target.id==='shareBg') closeShare(); });\n\n// ---------- init ----------\n(async()=>{\n  loader(true,'congelando snapshot da sessão…');\n  try{\n    await Promise.all([ensurePinned(), loadSectors(), refreshSessionPicker()]);\n    SESS.stage='confirm';\n  }catch(e){ setError('erro init: '+e.message); }\n  finally{ loader(false); renderAll(); const rd=$('#ready'); if(rd) rd.setAttribute('data-loaded','1'); }\n})();\n\n  }\n}\n"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}},"dominantesCsvUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dominantesCsvUrl"}}}}}}}},"extends":"data2-core-primitives@Block"}},{"_id":"6a7d08050c6a9916bf509fb5","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"xoLKR_Mb_n2vkwfmdExLH","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst PALETTE=['#5856d6','#ff9500','#34c759','#af52de','#ff2d55','#a2845e'];\nconst CLOSE_COLOR='#8e8e93';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\nconst METRICS_DEF=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\nconst OPT_K=1.5;\nconst SESSIONS_DS='timeline-shared-sessionsDataSet';   // same table as SHARED_DS — see notes at listSessions()\n// Span defaults are the real operating band for a setor receptor, not placeholders: 2200 ≤ span ≤ 3500\n// with 2700 as the target. null meant \"sem limite\", so every new session started with the optimiser\n// balancing against nothing. Existing sessions are NOT touched — their persisted settings carry explicit\n// values that spread OVER these in openOptions/applyOptions, which is deliberate: their etapas were\n// computed under the old band and changing it silently would leave the stored result and the rule that\n// produced it out of sync.\nconst DEFAULT_SETTINGS={destRule:'any',destMinCbs:9,targetSpan:2700,maxSpan:3500,minSpan:2200};\nconst DOM_MEDIA_BASE='https://natura-geomarketing.data2.media/file/';\nconst DOM_CSV_FALLBACK=DOM_MEDIA_BASE+'yw_TQdp0QXEk6JsrliAqP'; \nconst numOrNull=v=>{const n=Math.round(+v);return (isFinite(n)&&n>0)?n:null;};\nconst spanShort=st=>{const p=[];if(st&&st.targetSpan)p.push('alvo '+st.targetSpan);if(st&&st.maxSpan)p.push('máx '+st.maxSpan);if(st&&st.minSpan)p.push('mín '+st.minSpan);return p.length?p.join(' · '):'livre';};\nconst ruleShort=st=>((st&&st.destRule)==='dominant')?('dominantes ≥'+((st&&st.destMinCbs)||DEFAULT_SETTINGS.destMinCbs)+' CBs'):'qualquer municipio';\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#34c759}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .mac-spacer{flex:1}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  .pill.live{color:var(--ink);cursor:pointer;user-select:none;transition:border-color .1s,color .1s,background .1s}\n  .pill.live:hover{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  .pill.live:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .pill.live.busy{border-color:var(--acc);color:var(--acc);cursor:progress}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:5.5px 9px}\n  select:focus,input:focus{outline:2px solid rgba(0,122,255,.35);outline-offset:0;border-color:var(--acc)}\n  button.act{background:var(--acc);color:#fff;border:1px solid transparent;border-radius:7px;padding:6px 13px;font-weight:600;cursor:pointer;box-shadow:0 1px 1px rgba(0,0,0,.08)}\n  button.act:hover{background:var(--acc2)}\n  button.ghost{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5.5px 12px;cursor:pointer}\n  button.ghost:hover{background:var(--panel2)}\n  button.ghost.on{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  button.ghost:disabled,button.act:disabled{opacity:.4;cursor:not-allowed}\n  .sessbar{padding:9px 20px;background:var(--panel2);border-bottom:1px solid var(--line);display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n  .rail{padding:9px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:center;overflow-x:auto}\n  #movementChips{display:flex;gap:8px;flex:0 0 auto}\n  #addMovementGroup{display:flex;gap:8px;align-items:center;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--line)}\n  #addMovementGroup input{min-width:150px}\n  .tchip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px;flex:0 0 auto}\n  .tchip.on{border-color:var(--acc);background:#eaf3ff}\n  .tchip.stale{border-style:dashed;opacity:.85}\n  .tchip .tstale{color:#ff9500;font-weight:700}\n  .tchip .tnum{background:var(--ink);color:#fff;border-radius:99px;width:16px;height:16px;font-size:9.5px;display:flex;align-items:center;justify-content:center;font-weight:700}\n  .tchip .tsec{font-weight:600}\n  .tchip .tstat{color:var(--mut)}\n  .tchip .trm{color:var(--mut);font-weight:700;padding:0 3px}\n  .tchip .trm:hover{color:var(--closing)}\n  .rail .empty{color:var(--mut);font-size:12px}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 16px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:0 0 9px;font-weight:600}\n  /* user-select, not pointer-events. .lbl already sets pointer-events:none, which stops the text from\n     RECEIVING clicks but does not exclude it from the browser's selection range — so cycling a\n     município's destination with repeated clicks kept dragging a double-click selection across the\n     labels. Nothing drawn in an svg here is text anyone needs to copy. */\n  svg{width:100%;height:auto;display:block;user-select:none;-webkit-user-select:none}\n  .map-wrap{position:relative}\n  #map{cursor:grab;touch-action:none}\n  #map.dragging{cursor:grabbing}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 2px 8px rgba(0,0,0,.14);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  /* White hairlines inside the closing sector: the municipios read as one territory cut into parts\n     rather than as separate outlined units, and the saturated fills carry the grouping instead of the\n     ink. Neighbour sectors keep their own coloured strokes (.sect) — that contrast is what separates\n     what is being reallocated from what is only context. Selection no longer draws an outline; the\n     white border was the click affordance AND the default is now white, so it had nothing left to say. */\n  .muni{stroke:#fff;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.15)}.muni.munihl{filter:brightness(1.15)}\n  .sect{stroke-width:1;fill-opacity:.32;stroke-opacity:.6}\n  .sect.plain{fill:#e8e8ec;stroke:#d4d4d9;fill-opacity:1;stroke-opacity:1}\n  .sect.closed-ctx{fill:#e2e2e7;stroke:#aeaeb2;stroke-dasharray:3 2;fill-opacity:1;stroke-opacity:1}\n  .lbl{pointer-events:none}\n  /* No plate behind the text: a halo painted UNDER the glyphs (paint-order) keeps a name legible on any\n     fill without stamping an opaque box over the territory it names. Ink and halo are chosen per label\n     from the luminance of the fill underneath, so they are deliberately NOT declared here — a\n     stylesheet rule outranks a presentation attribute and would silently override that choice. */\n  .lbl text{text-anchor:middle;paint-order:stroke fill;stroke-width:2.2px;stroke-linejoin:round;stroke-linecap:round}\n  .lbl.big text{stroke-width:2.9px}\n  .lbl line.lead{stroke-opacity:.5;stroke-width:.8}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:19px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#ffd1cc}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bdeecb}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:600;font-size:12px}\n  .bar-track{flex:1;height:18px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex;position:relative}\n  .bar-mark{position:absolute;top:0;bottom:0;width:2px;opacity:.9;z-index:2}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  .toggle{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:12px}\n  .toggle button{background:transparent;border:0;color:var(--mut);padding:6px 10px;cursor:pointer;font-weight:600}\n  .toggle button.on{background:var(--panel2);color:var(--ink)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .lider-foot td{background:var(--panel2);font-weight:600;font-size:11px;color:var(--ink);border-bottom:2px solid var(--line);padding:5px 6px;text-align:left}\n  .scroll{max-height:240px;overflow:auto}\n  /* Tables meant to be READ in full rather than sampled. Only the vertical cap is dropped — overflow\n     stays auto so the wide grupo grid can still scroll sideways inside its card, which is the one axis\n     that genuinely cannot fit. */\n  .scroll.grow{max-height:none}\n  .tip{position:fixed;pointer-events:none;background:#1c1c1e;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}\n  .tabs button{background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600}\n  .tabs button.on{background:var(--panel2);color:var(--ink);border-color:var(--acc)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(245,245,247,.85);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:20;font-size:13px;color:var(--acc);font-weight:600;text-align:center;padding:20px}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n  .sheet-bg{position:fixed;inset:0;background:rgba(28,28,30,.34);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:40;padding:24px}\n  .sheet-bg.hidden{display:none}\n  .sheet{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.22);width:min(580px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden}\n  .sheet-hd{display:flex;align-items:center;gap:9px;padding:12px 16px;border-bottom:1px solid var(--line)}\n  .sheet-hd h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-.1px}\n  .sheet-x{background:transparent;border:0;color:var(--mut);font-size:13px;cursor:pointer;padding:3px 7px;border-radius:6px}\n  .sheet-x:hover{background:var(--panel2);color:var(--ink)}\n  .sheet-bd{padding:14px 16px;overflow:auto}\n  .sheet-ft{display:flex;align-items:center;gap:8px;padding:11px 16px;border-top:1px solid var(--line);background:var(--panel2)}\n  .opt-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600}\n  .opt-sub{font-size:11.5px;color:var(--mut);margin:4px 0 10px;line-height:1.5}\n  .opt-row{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;cursor:pointer;background:var(--panel)}\n  .opt-row:hover{background:var(--panel2)}\n  .opt-row.on{border-color:var(--acc);background:#eaf3ff}\n  .opt-row:has(input:disabled){opacity:.5;cursor:not-allowed}\n  .opt-row input[type=radio]{margin:2px 0 0;accent-color:var(--acc);flex:0 0 auto}\n  .opt-t{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n  .opt-d{font-size:11.5px;color:var(--mut);margin-top:3px;line-height:1.55}\n  .opt-inline{font-size:11.5px;color:var(--mut);margin-top:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}\n  .opt-inline.off{opacity:.4;pointer-events:none}\n  .opt-note{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:9px 11px;font-size:11.5px;color:var(--mut);line-height:1.6;margin-top:12px}\n  .opt-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:2px}\n  .opt-fld{display:flex;flex-direction:column;gap:5px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;background:var(--panel2)}\n  .opt-fld span{font-size:11px;color:var(--mut);font-weight:600;line-height:1.35}\n  .opt-fld i{font-style:normal;font-weight:400;opacity:.72}\n  .opt-fld input{width:100%;background:var(--panel)}\n  .opt-err{margin-top:9px;font-size:11.5px;color:var(--closing);font-weight:600}\n  .opt-hr{height:1px;background:var(--line);margin:17px 0 0}\n  .shareurl{margin-top:9px;padding:9px 10px;border-radius:8px;background:var(--panel);border:1px solid var(--acc);\n    font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--ink);word-break:break-all;user-select:all}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header id=\"topHeader\" class=\"hidden\">\n  <h1>Simulador de Timeline de Fechamentos</h1>\n  <span class=\"pill live\" id=\"liveState\" role=\"button\" tabindex=\"0\" title=\"Clique pra buscar o snapshot mais recente e recalcular a timeline inteira\">● iniciando</span>\n  <span class=\"pill ro\">read-only · nunca escreve</span>\n  <span class=\"pill\" id=\"basePill\"></span>\n  <span class=\"pill\" id=\"rulePill\"></span>\n  <span class=\"pill\" id=\"spanPill\"></span>\n  <span class=\"status\" id=\"ds\"></span>\n  <div class=\"mac-spacer\"></div>\n  <select id=\"sessionPick\" style=\"min-width:200px\"></select>\n  <button class=\"ghost\" id=\"openOptions\" title=\"Configurações desta sessão\">⚙ Opções</button>\n  <button class=\"act\" id=\"saveSession\">Salvar sessão</button>\n  <button class=\"ghost\" id=\"dupSession\" title=\"Cria uma cópia independente desta sessão — a original fica intacta e a cópia passa a ser a sessão ativa\">⧉ Duplicar</button>\n  <button class=\"ghost\" id=\"openShare\" title=\"Publicar uma cópia congelada desta sessão em modo leitura\">⇪ Compartilhar</button>\n  <span class=\"status\" id=\"sessStatus\"></span>\n</header>\n\n<div class=\"rail hidden\" id=\"timelineRail\">\n  <div id=\"movementChips\"></div>\n  <div id=\"addMovementGroup\">\n    <button class=\"act\" id=\"openMove\">Adicionar movimentação</button>\n  </div>\n</div>\n\n<div class=\"card\" id=\"confirmCard\" style=\"margin:14px 20px\">\n  <h2 style=\"margin:0 0 9px\">Confirmar snapshot antes de simular</h2>\n  <div class=\"note\" id=\"confirmBody\">congelando snapshot…</div>\n  <button class=\"act\" id=\"confirmSnap\" style=\"margin-top:12px\">Confirmar snapshot ▶ ir pra simulação</button>\n  <div class=\"note\" style=\"margin-top:16px\">ou continue uma sessão salva:</div>\n  <select id=\"confirmSessionPick\" style=\"min-width:260px;margin-top:4px\"></select>\n  <div class=\"status\" id=\"confirmStatus\" style=\"margin-top:8px\"></div>\n</div>\n\n<div class=\"wrap hidden\" id=\"mainWrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <h2 id=\"mapTitle\" style=\"margin:0\">Mapa — clique num município p/ trocar o setor destino</h2>\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura do setor antes desta etapa (já considerando etapas anteriores) · TOBE = após a redistribuição desta etapa\">\n          <button data-v=\"asis\">◧ ASIS (antes desta etapa)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <div class=\"map-wrap\">\n        <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n        <div class=\"zoom-pill\" id=\"mapZoom\">\n          <button type=\"button\" id=\"mapZoomOut\" title=\"Diminuir zoom\">−</button>\n          <span class=\"zoom-div\"></span>\n          <button type=\"button\" id=\"mapZoomIn\" title=\"Aumentar zoom\">+</button>\n        </div>\n      </div>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado nesta etapa (cor = destino). Ao redor, cada área é um <b>setor vizinho inteiro</b> com o código no meio — não municípios soltos: cor = setor destino, cinza = contexto, cinza tracejado = setor já fechado numa etapa anterior. Shapes reais IBGE/tbrugz.</div>\n      <div class=\"note\" id=\"mapIgnoredNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas (nesta etapa)</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor.</div>\n      <div class=\"scroll grow\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"tobe\">Matriz TOBE</button>\n        <button data-tab=\"report\">Relatório de impacto (etapa)</button>\n        <button data-tab=\"cumulative\">Impacto acumulado (timeline)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll grow\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-tobe\" class=\"hidden\"><div class=\"scroll grow\"><table id=\"matrixTobe\"></table></div>\n        <div class=\"note\" id=\"tobeNote\"></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-cumulative\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"note\" style=\"margin:-4px 0 8px\">Roda contra o snapshot congelado desta sessão (não \"ao vivo\") — mesma coerência da timeline.</div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa alocações que mantêm cada setor conectado e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (mantém o território conectado; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Editar esta etapa recalcula automaticamente as etapas seguintes da timeline.</div>\n      <div class=\"note\" id=\"ruleNote\" style=\"margin:-6px 0 11px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px\"></div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <div id=\"spanRule\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div id=\"policySection\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n      </div>\n    </div>\n\n    <div class=\"card\">\n      <h2 id=\"muniListTitle\">Municípios → setor destino</h2>\n      <div class=\"note\" id=\"muniListNote\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração (etapa selecionada)</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"shareBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"shareTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"shareTitle\">Compartilhar sessão</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"shareX\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\" id=\"shareBody\"></div>\n    <div class=\"sheet-ft\">\n      <span class=\"status\" id=\"shareStatus\"></span>\n      <div class=\"mac-spacer\"></div>\n      <button class=\"ghost hidden\" id=\"shareCopy\">Copiar link</button>\n      <button class=\"ghost hidden\" id=\"shareOpen\">Abrir</button>\n      <button class=\"ghost\" id=\"shareCancel\">Cancelar</button>\n      <button class=\"act\" id=\"shareGo\">Compartilhar</button>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"optionsBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"optTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"optTitle\">Opções da sessão</h3><span class=\"badge\" id=\"optSessName\"></span><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"optClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Regra rígida — seleção dos setores destino</div>\n      <div class=\"opt-sub\">Define quais setores vizinhos podem entrar na lista de destinos de cada etapa. Vale pra toda a timeline desta sessão.</div>\n      <label class=\"opt-row\" data-r=\"any\">\n        <input type=\"radio\" name=\"destRule\" value=\"any\">\n        <div>\n          <div class=\"opt-t\">Adjacente a qualquer município <span class=\"badge\">padrão</span></div>\n          <div class=\"opt-d\">Um setor entra na lista de destinos se tiver município vizinho a <b>qualquer</b> município do setor fechado — inclusive os menores. É o comportamento de hoje.</div>\n        </div>\n      </label>\n      <label class=\"opt-row\" data-r=\"dominant\">\n        <input type=\"radio\" name=\"destRule\" value=\"dominant\">\n        <div>\n          <div class=\"opt-t\">Adjacente a municípios dominantes</div>\n          <div class=\"opt-d\">Só os municípios do setor fechado com <b>CBs suficientes</b> valem como ponte. Os pequenos continuam sendo redistribuídos normalmente — só não trazem setor novo pra lista de destinos.</div>\n          <div class=\"opt-inline\" id=\"optMinCbsWrap\">Ignorar municípios com menos de <input type=\"number\" id=\"optMinCbs\" min=\"1\" max=\"9999\" step=\"1\" style=\"width:74px\"> CBs</div>\n        </div>\n      </label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\">Pré-requisitos do setor destino — tamanho em CBs</div>\n      <div class=\"opt-sub\">Vale pro tamanho de cada setor receptor <b>depois</b> da redistribuição (base + entrantes). Em branco = desligado.</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhum setor acima)</i></span><input type=\"number\" id=\"optMaxSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhum setor abaixo)</i></span><input type=\"number\" id=\"optMinSpan\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-err hidden\" id=\"optSpanErr\"></div>\n      <div class=\"opt-note\" id=\"optPreview\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"optStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"optCancel\">Cancelar</button><button class=\"act\" id=\"optApply\">Aplicar</button></div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"moveBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"moveTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"moveTitle\">Adicionar movimentação</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"moveClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Tipo de movimentação</div>\n      <div class=\"opt-sub\" id=\"moveLockNote\">Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.</div>\n      <label class=\"opt-row\" data-lvl=\"setor\"><input type=\"radio\" name=\"moveLevel\" value=\"setor\"><div><div class=\"opt-t\">Fechar Setor</div><div class=\"opt-d\">Redistribui os municípios do setor entre setores vizinhos da mesma GV.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"gv\"><input type=\"radio\" name=\"moveLevel\" value=\"gv\"><div><div class=\"opt-t\">Fechar GV</div><div class=\"opt-d\">Redistribui os setores (com municípios e CBs) da GV entre GVs vizinhas do mesmo mercado.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"mercado\"><input type=\"radio\" name=\"moveLevel\" value=\"mercado\"><div><div class=\"opt-t\">Fechar Mercado</div><div class=\"opt-d\">Redistribui as GVs (com setores, municípios e CBs) do mercado entre mercados vizinhos.</div></div></label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\" id=\"moveCodeLbl\">Setor a fechar</div>\n      <div class=\"opt-inline\" id=\"moveCodeRow\">\n        <input id=\"moveCode\" list=\"moveCodeList\" placeholder=\"código\" style=\"min-width:150px\">\n        <datalist id=\"moveCodeList\"></datalist>\n        <button class=\"ghost\" id=\"moveRand\" title=\"Aleatório\">🎲</button>\n      </div>\n      <div class=\"opt-err hidden\" id=\"moveErr\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"moveStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"moveCancel\">Cancelar</button><button class=\"act\" id=\"moveGo\">Fechar ▶ (nova etapa)</button></div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div>\n<span id=\"ready\" style=\"display:none\"></span></div>`;\n  },\n  mounted(d2, root){\n\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\nconst blankM=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n// ---------- município identity ----------\n// Brazil has many municipalities sharing a name (Bom Jesus alone exists in PI, RN, RS, SC, PB, GO).\n// A stage loads the dominant UF PLUS every bordering UF, so those collisions land in the SAME\n// feature set routinely: keyed by name alone the last shape loaded won, the map could draw a\n// município from the wrong state, buildAdj merged two unrelated municipalities into one node, and\n// GROUP BY DS_MUNICIPIO summed two real municipalities into one row. The key is NAME/UF everywhere\n// — SQL rows, GeoJSON features, the dominantes base, assignments, persisted sessions. norm() maps\n// every non-alphanumeric to a space, so '/' can never occur inside a name and the split is exact.\n// A missing UF degrades to a bare NAME key rather than inventing one.\nconst mkey=(name,uf)=>{const n=norm(name),u=String(uf==null?'':uf).trim().toUpperCase();return u?n+'/'+u:n;};\nconst mName=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?s:s.slice(0,i);};\nconst mUf=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?'':s.slice(i+1);};\nconst mDisp=k=>{const u=mUf(k);return u?mName(k)+' ('+u+')':mName(k);};\n// The base CSV is the ONLY source of a município's UF now, so its column is accepted in whatever\n// shape the base is maintained in: sigla, IBGE numeric code, or full state name. Anything that does\n// not resolve to a real sigla is treated as UNKNOWN rather than passed through — a bogus UF would key\n// a município to a shape that does not exist and drop it off the map without a word.\nconst UF_OF_CODE={}; Object.keys(UF_CODE).forEach(k=>{ UF_OF_CODE[UF_CODE[k]]=k; });\nconst UF_OF_NAME={ACRE:'AC',ALAGOAS:'AL',AMAPA:'AP',AMAZONAS:'AM',BAHIA:'BA',CEARA:'CE','DISTRITO FEDERAL':'DF','ESPIRITO SANTO':'ES',GOIAS:'GO',MARANHAO:'MA','MATO GROSSO':'MT','MATO GROSSO DO SUL':'MS','MINAS GERAIS':'MG',PARA:'PA',PARAIBA:'PB',PARANA:'PR',PERNAMBUCO:'PE',PIAUI:'PI','RIO DE JANEIRO':'RJ','RIO GRANDE DO NORTE':'RN','RIO GRANDE DO SUL':'RS',RONDONIA:'RO',RORAIMA:'RR','SANTA CATARINA':'SC','SAO PAULO':'SP',SERGIPE:'SE',TOCANTINS:'TO'};\nconst ufNorm=v=>{const s=norm(v); if(!s) return '';\n  if(UF_CODE[s]) return s;\n  if(UF_OF_CODE[s]) return UF_OF_CODE[s];\n  return UF_OF_NAME[s]||'';};\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\nconst setError=(msg)=>{ const t=$('#ds'); if(t){t.textContent=msg;t.classList.add('err');} const c=$('#confirmStatus'); if(c){c.textContent=msg;c.classList.add('err');} };\n\n// ---------- base de municípios dominantes (CSV) ----------\n// WHICH municipalities belong to a sector is a business fact, not something to infer from CB volume:\n// it comes from a maintained CSV (one row per setor x município) linked through the `dominantesCsvUrl`\n// param. Anything the closing sector has CBs in but that the base does NOT assign to it is spillover:\n// not realocated, not drawn on the map, and its CBs follow their leader. Loaded once per URL and\n// cached for the whole session; a failure is thrown, never swallowed — a silent fallback here would\n// quietly change every number on the screen.\nconst DOM={url:null,promise:null,bySetor:null,ufBySetorMuni:null,rows:0,setores:0,hasUf:false,ufCol:null,badUf:0,clashes:0,headers:null,error:null};\nfunction parseCsv(text){\n  const out=[]; let row=[],cur='',q=false;\n  const push=()=>{ row.push(cur); cur=''; };\n  const endRow=()=>{ push(); if(row.length>1||row[0]!=='') out.push(row); row=[]; };\n  for(let i=0;i<text.length;i++){ const ch=text[i];\n    if(q){ if(ch==='\"'){ if(text[i+1]==='\"'){ cur+='\"'; i++; } else q=false; } else cur+=ch; }\n    else if(ch==='\"') q=true;\n    else if(ch===',') push();\n    else if(ch==='\\n') endRow();\n    else if(ch!=='\\r') cur+=ch;\n  }\n  if(cur!==''||row.length) endRow();\n  return out;\n}\n// `dominantesCsvUrl` is declared as a FileComponentUrlReference, so the instance points at the Drive\n// FILE, not at a URL literal — the Drive is policy-gated behind the app's auth strategy and a baked\n// string would be both stale and unauthorised. What the resolver hands back varies by where it is\n// resolved (URL string / file record / bare reference), so accept every shape instead of silently\n// reading undefined and falling through to the fallback.\n// The Drive is gated by the app's AuthenticationStrategy, so this read has to travel as the LOGGED-IN\n// USER — an anonymous CDN GET is exactly what the policy is there to refuse. `<system>.data2.media` is\n// only a cache in front of the SSP: on a cache miss it 302s to `<system>.data2apis.com`, which is where\n// the policy is actually evaluated.\n// COOKIES CANNOT CARRY THAT SESSION, which is why the previous `credentials:'include'` attempt could\n// never have worked: the app runs on <app>.<system>.data2.app and the SSP on <system>.data2apis.com —\n// cross-site — and the SSP answers `Access-Control-Allow-Origin: *` with no `Allow-Credentials`, so the\n// browser rejects a credentialed CORS read before it ever leaves the page. What the SSP actually reads\n// is `Authorization: Bearer <token>`: the runtime keeps that token in localStorage under\n// `__data2-session-<systemDomain>` (`__data2-session-data2` inside the builder preview) and replays it\n// on every dataset and rush call — same token, same header, same session, so the Drive policy sees the\n// logged-in user. Preflight passes because the SSP echoes whatever header the request asks for.\nfunction d2SessionToken(sysDomain){\n  const read=k=>{ try{ return (k&&window.localStorage.getItem(k))||''; }catch(e){ return ''; } };\n  return read(sysDomain?('__data2-session-'+sysDomain):'')||read('__data2-session-data2')||'';\n}\nfunction domCsvSystemDomain(url){\n  const m=/^https?:\\/\\/([^.\\/]+)\\.data2\\.(?:media|app)\\//i.exec(url)||/^https?:\\/\\/([^.\\/]+)\\.data2apis\\.com\\//i.exec(url);\n  return m?m[1]:'';\n}\nfunction domCsvCandidates(url){\n  const out=[];\n  const isMedia=/^https?:\\/\\/[^.\\/]+\\.data2\\.media\\//i.test(url);\n  const api=isMedia?url.replace(/^(https?:\\/\\/[^.\\/]+)\\.data2\\.media\\//i,'$1.data2apis.com/'):null;\n  const tok=d2SessionToken(domCsvSystemDomain(url));\n  const auth=tok?{authorization:'Bearer '+tok}:null;\n  // Authenticated origin read first — the only candidate a GATED Drive can actually answer.\n  if(api&&auth) out.push({url:api,headers:auth,label:'api+sessão'});\n  if(!api&&auth) out.push({url:url,headers:auth,label:'origem+sessão'});\n  // Anonymous last: only ever succeeds on an UNGATED Drive — or on a stale CDN cache hit, which is NOT\n  // proof the file is public (the origin keeps answering 401 once the edge entry expires).\n  if(api) out.push({url:api,headers:null,label:'api anônimo'});\n  out.push({url:url,headers:null,label:'origem anônima'});\n  return out;\n}\nfunction domCsvUrl(){\n  const pick=v=>{\n    if(!v) return '';\n    if(typeof v==='string') return v;\n    const q=v.params||{};\n    const ref=q.reference||v.reference;\n    return q.fileUrl||q.url||v.fileUrl||v.url||(ref?DOM_MEDIA_BASE+ref:'');\n  };\n  return String(pick(d2.param('dominantesCsvUrl'))||DOM_CSV_FALLBACK||'').trim();\n}\nasync function ensureDominantes(){\n  const url=domCsvUrl();\n  if(!url) throw new Error('base de municípios dominantes não configurada — ligue o arquivo CSV no parâmetro dominantesCsvUrl da instância do componente');\n  if(DOM.url===url && DOM.bySetor) return DOM.bySetor;\n  if(DOM.url===url && DOM.promise) return DOM.promise;\n  DOM.url=url; DOM.error=null;\n  DOM.promise=(async()=>{\n    // Self-diagnosing on purpose: every candidate reports its own outcome and the message names the\n    // session it was carrying. \"Unauthorized\" alone cannot distinguish \"not logged into THIS strategy\"\n    // from \"cookie not sent cross-site\" from \"param never bound, so it tried the fallback URL\" — and\n    // guessing between those from the outside costs a round trip each time.\n    let r=null; const tried=[];\n    for(const c of domCsvCandidates(url)){\n      let res=null;\n      try{ res=await fetch(c.url,{cache:'no-store',credentials:'omit',headers:c.headers||undefined}); }\n      catch(e){ tried.push(c.label+' → rede/CORS'); continue; }\n      if(res.ok){ r=res; break; }\n      tried.push(c.label+' → '+res.status);\n    }\n    if(!r){\n      let sess='sessão desconhecida';\n      try{\n        const a=d2.auth(AUTH_REF);\n        sess=(a&&a.isAuthed)?('logado como '+((a.user&&(a.user.email||a.user.nome))||'?')):('NÃO logado na estratégia '+AUTH_REF);\n      }catch(e){ sess='estratégia '+AUTH_REF+' não resolvida'; }\n      const tk=d2SessionToken(domCsvSystemDomain(url));\n      throw new Error('base de municípios dominantes: não foi possível ler o arquivo — '+sess+' · token de sessão '+(tk?'presente':'AUSENTE no localStorage')+'. Tentativas: '+tried.join(' · '));\n    }\n    const table=parseCsv(await r.text());\n    if(table.length<2) throw new Error('base de municípios dominantes vazia');\n    const head=table[0].map(h=>norm(h));\n    const iS=head.indexOf('CD SETOR'), iM=head.indexOf('MUNICIPIO');\n    if(iS<0||iM<0) throw new Error('base de municípios dominantes sem as colunas CD_SETOR / MUNICIPIO');\n    // CD_GV is the gerência de vendas the setor answers to, and it is the authority for the hard rule\n    // that a closing sector only ever hands territory to sectors of its OWN gerência. Its absence is\n    // fatal rather than a warning: with no GV there is no way to PROVE two sectors share one, and\n    // falling through unchecked is exactly the leak this column exists to close.\n    const iG=['CD GV','CDGV','GV','CD GERENCIA','CD GERENCIA VENDAS','CD GERENCIA DE VENDAS','GERENCIA DE VENDAS','GERENCIA'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iG==null) throw new Error('base de municípios dominantes sem a coluna CD_GV — a regra de gerência de vendas não pode ser aplicada. Colunas lidas: '+head.slice(0,40).join(', '));\n    // MERCADO fica ACIMA de GV na hierarquia (mercado > GV > setor) — fechamento de Mercado e a regra\n    // \"GV só fecha pra GV do mesmo mercado\" dependem dela do mesmo jeito que a regra de GV depende\n    // de CD_GV, então a ausência é fatal pelo mesmo motivo.\n    const iK=['MERCADO','MERCADO REGIONAL','MKT','MARKET'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iK==null) throw new Error('base de municípios dominantes sem a coluna MERCADO — fechamento de GV/Mercado precisa dela. Colunas lidas: '+head.slice(0,40).join(', '));\n    // Nomes de exibição — opcionais: a hierarquia funciona só com os códigos, mas o código sozinho\n    // não diz nada pro operador escolhendo o que fechar.\n    const iDS=['DC SETOR','NOME SETOR','DESCRICAO SETOR','NM SETOR'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const iDG=['DC GV','NOME GV','DESCRICAO GV','NM GV','GV NOME'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    // The UF column is what the whole keying now rests on — SQL no longer carries DS_ESTADO — so its\n    // absence is a first-class finding, not a footnote. The header actually matched is reported on the\n    // base pill, and when nothing matches the pill lists every column read, because guessing a header\n    // name from the outside costs a round trip each time.\n    const iU=['UF','SIGLA UF','SG UF','CD UF','ESTADO','DS ESTADO','SIGLA','ESTADO SIGLA','UF MUNICIPIO'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const by={},ufBy={},gvBy={},mkBySetor={},setorLabel={},gvLabel={}; let n=0,bad=0; const clash={},gvClash={},mkClash={};\n    for(let i=1;i<table.length;i++){\n      const line=table[i]; if(!line) continue;\n      const s=String(line[iS]==null?'':line[iS]).trim(), m=norm(line[iM]);\n      if(!s||!m) continue;\n      // One setor spans many rows (one per município) and every one of them repeats its CD_GV. Two\n      // DIFFERENT values for the same setor means the base itself disagrees about which gerência owns\n      // it — counted and reported, never keyed to whichever row happened to be read last.\n      const gvv=String(line[iG]==null?'':line[iG]).trim();\n      if(gvv){ if(gvBy[s]!==undefined && gvBy[s]!==gvv) gvClash[s]=true; else gvBy[s]=gvv; }\n      const mkv=String(line[iK]==null?'':line[iK]).trim();\n      if(mkv){ if(mkBySetor[s]!==undefined && mkBySetor[s]!==mkv) mkClash[s]=true; else mkBySetor[s]=mkv; }\n      if(iDS!=null){ const lbl=String(line[iDS]==null?'':line[iDS]).trim(); if(lbl && !setorLabel[s]) setorLabel[s]=lbl; }\n      if(iDG!=null && gvv){ const lbl=String(line[iDG]==null?'':line[iDG]).trim(); if(lbl && !gvLabel[gvv]) gvLabel[gvv]=lbl; }\n      const raw=(iU==null)?'':line[iU];\n      const u=(iU==null)?'':ufNorm(raw);\n      if(iU!=null && String(raw==null?'':raw).trim() && !u) bad++;\n      // The same name twice in ONE setor under two UFs is the single case the reverted SQL cannot\n      // survive: mSQL groups by DS_MUNICIPIO alone, so both municipalities arrive summed into one row\n      // and no lookup can split them again. Counted and reported — never quietly keyed to whichever\n      // UF happened to be read last.\n      const kk=s+'|'+m;\n      // An UNRESOLVED uf must never outrank a resolved one. Reading a blank/bogus UF row first wrote ''\n      // here, and the next row carrying the real sigla was then rejected as a clash — so the key stayed\n      // falsy for good, ufFor() fell through, and the município degraded to a bare NAME key that matches\n      // no shape and no dominance entry. With DISTRITO/ZONA in the base a município spans several rows,\n      // so which row lands first is pure luck. A clash is only two RESOLVED siglas that disagree.\n      if(!u){ if(ufBy[kk]===undefined) ufBy[kk]=''; }\n      else if(!ufBy[kk]) ufBy[kk]=u;\n      else if(ufBy[kk]!==u) clash[kk]=true;\n      (by[s]=by[s]||new Set()).add(u?m+'/'+u:m); n++;\n    }\n    if(!n) throw new Error('base de municípios dominantes sem linhas válidas');\n    DOM.bySetor=by; DOM.ufBySetorMuni=ufBy; DOM.rows=n; DOM.setores=Object.keys(by).length;\n    DOM.gvBySetor=gvBy; DOM.gvClashes=Object.keys(gvClash).length; DOM.gvCol=String(table[0][iG]||'').trim();\n    DOM.gvSetores=Object.keys(gvBy).length;\n    DOM.hasUf=(iU!=null); DOM.ufCol=(iU==null?null:String(table[0][iU]||'').trim());\n    DOM.badUf=bad; DOM.clashes=Object.keys(clash).length; DOM.headers=head.slice(0,40); DOM.error=null;\n    // ---- hierarquia: setor ⊂ GV ⊂ mercado, tudo derivado do MESMO csv que a regra de setor já\n    // confia — fechamento de GV/Mercado lê a mesma fonte, nunca uma segunda base paralela.\n    DOM.mercadoBySetor=mkBySetor; DOM.mercadoClashes=Object.keys(mkClash).length; DOM.mercadoCol=String(table[0][iK]||'').trim();\n    DOM.setorLabel=setorLabel; DOM.gvLabel=gvLabel;\n    const sectorsByGv={},sectorsByMercado={},gvsByMercado={},mercadoByGv={},gvMercClash={};\n    Object.keys(by).forEach(s=>{\n      const gv=gvBy[s], mk=mkBySetor[s];\n      if(gv) (sectorsByGv[gv]=sectorsByGv[gv]||new Set()).add(s);\n      if(mk) (sectorsByMercado[mk]=sectorsByMercado[mk]||new Set()).add(s);\n      if(gv&&mk){ if(mercadoByGv[gv]!==undefined && mercadoByGv[gv]!==mk) gvMercClash[gv]=true; else mercadoByGv[gv]=mk; }\n    });\n    Object.entries(mercadoByGv).forEach(([gv,mk])=>(gvsByMercado[mk]=gvsByMercado[mk]||new Set()).add(gv));\n    DOM.sectorsByGv=sectorsByGv; DOM.sectorsByMercado=sectorsByMercado; DOM.gvsByMercado=gvsByMercado;\n    DOM.mercadoByGv=mercadoByGv; DOM.gvMercadoClashes=Object.keys(gvMercClash).length;\n    return by;\n  })();\n  DOM.promise.catch(e=>{ DOM.promise=null; DOM.bySetor=null; DOM.error=e.message; renderAll(); });\n  return DOM.promise;\n}\n\nlet SESS={ snapshotRaw:null, snapshotSyncedAt:null, snapLiteral:null, eloMaxCycle:null, timeline:[], selected:0,\n           sessionId:null, sessionLabel:null, sessionsList:[], sectorsList:[], sectorBaseCache:{}, stage:'loading', dirtyFrom:null,\n           snapBusy:false, settings:{...DEFAULT_SETTINGS} };\n\nasync function q(sql){\n  let dbx,res;\n  try{\n    res=await d2.rush('AwW_11Nkg-dKksP7ic8V8').invoke({sql});\n  }catch(e){\n    throw new Error('Databricks indisponível (rush AwW_11Nkg-dKksP7ic8V8): '+e.message);\n  }\n  // The listener answers 200 with {sql} and NO `result` when its upstream probe times out: the\n  // handler builds `result: $result.data`, so an empty upstream simply drops the key rather than\n  // failing. Reading straight through to res.data.result.data reported \"Cannot read properties of\n  // undefined (reading 'data')\" — which reads like a bug in THIS component and sent a debugging\n  // session after the wrong thing. Name the layer that actually failed instead.\n  const payload=res&&res.data;\n  if(!payload) throw new Error('rush AwW_11Nkg-dKksP7ic8V8 não devolveu corpo — listener fora do ar?');\n  if(!payload.result) throw new Error('a sonda Databricks não respondeu: o listener AwW_11Nkg-dKksP7ic8V8 devolveu 200 sem `result`, o que normalmente é timeout do upstream (natura-x-app). O SQL não chegou a rodar.');\n  dbx=payload.result.data;\n  if(!dbx) throw new Error('sonda Databricks respondeu sem dados (result.data vazio).');\n  if(dbx.state && dbx.state!=='SUCCEEDED') throw new Error('Databricks '+dbx.state+': '+(dbx.error?.message||JSON.stringify(dbx.error)||'erro desconhecido'));\n  return dbx;\n}\nfunction rows(resp){const c=(resp.columns||[]).map(x=>x.name);\n  return (resp.rows||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\n// Stamps the UF onto every feature at load: the fetch is per-UF, so this is the ONLY point where a\n// shape's state is known for certain. Downstream the features are flattened into one array and the\n// origin would be unrecoverable. Persisted with the feature in saved sessions.\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);\n  const g=await r.json(); const U=String(uf).toUpperCase();\n  (g.features||[]).forEach(f=>{ f.__uf=U; });\n  return g;}\n\nfunction buildAdj(features){\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const feat={},idx={};\n  features.forEach(f=>{const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome,f.__uf);feat[nm]={f,pts:pts(f)};});\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  const nbrs=nm=>{const c={};feat[nm].pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    return Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));};\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\n// ---------- snapshot pinning ----------\nasync function resolveSnapshotPin(){\n  const [rawR,liveCountR,eloR]=await Promise.all([\n    q(`SELECT MAX(DT_HISTORICO) snap FROM ${T} WHERE CD_PAIS=1`).then(rows),\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`).then(rows),\n    q(`SELECT MAX(operational_cycle) mx FROM ${ELO} WHERE country_code=1`).then(rows).catch(()=>[]),\n  ]);\n  const raw=rawR[0]&&rawR[0].snap;\n  const liveCount=+((liveCountR[0]||{}).n||0);\n  const eloMax=+((eloR[0]||{}).mx||0);\n  const LIVE_SUBQ=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n  const dbTs=(raw||'').replace('T',' ').replace(/Z$/,'');\n  const candidates=[`TIMESTAMP'${dbTs}'`,`'${dbTs}'`,`DATE'${dbTs}'`];\n  const tests=await Promise.all(candidates.map(c=>\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=${c}`).then(rows).then(r=>+((r[0]||{}).n||0)).catch(()=>-1)\n  ));\n  let chosen=LIVE_SUBQ;\n  for(let i=0;i<candidates.length;i++){ if(liveCount>0 && tests[i]===liveCount){ chosen=candidates[i]; break; } }\n  return {snapshotRaw:raw, snapLiteral:chosen, eloMaxCycle:eloMax, syncedAt:new Date().toISOString()};\n}\n// DISPLAY CONTRACT: the UI never prints the snapshot's own DT_HISTORICO (snapshotRaw). What the user\n// sees is `snapshotSyncedAt` — the date AND hour at which THIS instance last pulled/pinned the\n// snapshot it is querying. snapshotRaw is still resolved, still drives every SQL literal, and is\n// still persisted by saveSession/doShare; it is only kept off the screen.\nfunction fmtSyncedAt(iso){\n  if(!iso) return '—';\n  const d=new Date(iso);\n  if(isNaN(d.getTime())) return String(iso);\n  return d.toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});\n}\nasync function ensurePinned(){\n  if(SESS.snapLiteral) return;\n  const p=await resolveSnapshotPin();\n  SESS.snapshotRaw=p.snapshotRaw; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n  SESS.snapshotSyncedAt=p.syncedAt;\n}\n\n// ---------- world-state fold ----------\nfunction accMetrics(base,add){\n  const out=base?{...base}:blankM();\n  Object.keys(blankM()).forEach(k=>{ out[k]=(out[k]||0)+((add&&add[k])||0); });\n  return out;\n}\nfunction foldWorldState(timeline,uptoIdx){\n  const closedSectors=new Set(),muniOverride={},muniGeo={},muniRow={},muniLevel={},sectorInflow={},sectorInflowLevel={},sectorOrphanRows={};\n  // GV/mercado: setores e GVs não se dissolvem quando o dono deles fecha — eles só passam a\n  // responder a um destino novo. Por isso o rastro do cascade mora numa camada PRÓPRIA\n  // (secOverride/gvOverride), separada da camada de município do fechamento de setor — a\n  // hierarquia trava a sessão num nível só, então nunca as duas camadas têm conteúdo ao mesmo tempo.\n  const closedGvs=new Set(),secOverride={},gvInflow={};\n  const closedMercados=new Set(),gvOverride={},mercadoInflow={};\n  for(let k=0;k<uptoIdx;k++){\n    const mv=timeline[k]; if(!mv||!mv.sector||!mv.munis) continue;\n    const level=mv.level||'setor';\n    if(level==='gv'){\n      closedGvs.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        secOverride[u]=String(dest);\n        gvInflow[dest]=accMetrics(gvInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    if(level==='mercado'){\n      closedMercados.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        gvOverride[u]=String(dest);\n        mercadoInflow[dest]=accMetrics(mercadoInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    closedSectors.add(String(mv.sector));\n    Object.keys(mv.munis).forEach(m=>{\n      if(m==='__OUTROS__') return;\n      const dest=mv.assign[m]; if(!dest) return;\n      muniOverride[m]=String(dest);\n      if(mv.geoFeat&&mv.geoFeat[m]) muniGeo[m]=mv.geoFeat[m];\n      const rws=(mv.matrix||[]).filter(r=>mkey(r.municipio,r.uf)===m);\n      muniRow[m]=(muniRow[m]||[]).concat(rws);\n      if(mv.levelByMuni&&mv.levelByMuni[m]) muniLevel[m]={...(muniLevel[m]||{}),...mv.levelByMuni[m]};\n      sectorInflow[dest]=accMetrics(sectorInflow[dest],mv.munis[m]);\n      const lv=(mv.levelByMuni&&mv.levelByMuni[m])||{};\n      sectorInflowLevel[dest]=sectorInflowLevel[dest]||{};\n      Object.entries(lv).forEach(([nv,c])=>{ sectorInflowLevel[dest][nv]=(sectorInflowLevel[dest][nv]||0)+c; });\n    });\n    // Orphan CBs follow their leader's home sector, so later etapas must inherit them the same way\n    // the allocated municipalities are inherited — otherwise a stage's span would not match the\n    // base carried into the next one.\n    if(mv.orphanMetricsByGroup && Object.keys(mv.orphanMetricsByGroup).length){\n      const d=computeDerived(mv);\n      d.leaders.forEach(L=>{\n        if(!L.home) return;\n        const om=mv.orphanMetricsByGroup[L.grupo];\n        if(om) sectorInflow[L.home]=accMetrics(sectorInflow[L.home],om);\n        // sectorInflow only reaches a sector while it is a DESTINATION (it lands in baseMetrics).\n        // Keep the rows as well, so the sector still carries these CBs on the day it is the one\n        // being closed — that read goes through mv.matrix, which baseMetrics never touches.\n        const orws=mv.orphanRowsByGroup&&mv.orphanRowsByGroup[L.grupo];\n        if(orws&&orws.length) sectorOrphanRows[L.home]=(sectorOrphanRows[L.home]||[]).concat(orws);\n        const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n        if(ol){ sectorInflowLevel[L.home]=sectorInflowLevel[L.home]||{};\n          Object.entries(ol).forEach(([nv,c])=>{ sectorInflowLevel[L.home][nv]=(sectorInflowLevel[L.home][nv]||0)+c; }); }\n      });\n    }\n  }\n  return {closedSectors,muniOverride,muniGeo,muniRow,muniLevel,sectorInflow,sectorInflowLevel,sectorOrphanRows,\n          closedGvs,secOverride,gvInflow,closedMercados,gvOverride,mercadoInflow};\n}\n\n// ---------- per-movement compute ----------\nfunction aggregateMovement(mv){\n  const level=mv.level||'setor';\n  const munis={},leaders={};\n  const exSet=new Set((mv.excludedMunis||[]).map(x=>x.muni));\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  // Orphans = CBs in municipalities outside the sector's dominance base. They are NOT allocated\n  // geographically; they follow their own leader's home sector. Kept per leader group so every\n  // consumer (span, cascade, impact report) can route them the same way. Só existe no nível setor —\n  // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base).\n  mv.orphanByGroup={}; mv.orphanMetricsByGroup={}; mv.orphanRowsByGroup={};\n  // unidade que este nível redistribui: a própria município (setor fecha), o setor da linha (GV\n  // fecha), a GV do setor da linha (mercado fecha).\n  const unitOf=r=>level==='setor'?null:(level==='gv'?String(r.setor):(DOM.gvBySetor[String(r.setor)]||''));\n  mv.matrix.forEach(r=>{\n    const m=mkey(r.municipio,r.uf);\n    r._out=!mv.geoFeat[m];   // assign, never just set: absorbed rows are reused across etapas\n    const mm=level==='setor'?(r._out?'__OUTROS__':m):(unitOf(r)||'__OUTROS__');\n    munis[mm]=munis[mm]||{...blankM(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n    if(level==='setor' && r._out && exSet.has(m)){\n      mv.orphanByGroup[g]=(mv.orphanByGroup[g]||0)+ +r.cbs;\n      // The ROW too, not just the totals: when the leader's home sector is itself closed in a later\n      // etapa, that etapa rebuilds its matrix from SQL + absorbed municípios, and these CBs are in\n      // neither (their município never moved). Without the row they silently vanish from the sector\n      // they were just credited to — see sectorOrphanRows in foldWorldState.\n      (mv.orphanRowsByGroup[g]=mv.orphanRowsByGroup[g]||[]).push(r);\n      mv.orphanMetricsByGroup[g]=accMetrics(mv.orphanMetricsByGroup[g],\n        {cbs:+r.cbs,receita:+r.receita||0,rec_nat:+r.rec_nat||0,rec_avon:+r.rec_avon||0,pontos:+r.pontos||0,pedidos:+r.pedidos||0,ativas:+r.ativas||0});\n    }\n  });\n  // Level counts only exist per municipality, never per leader, so an excluded municipality's level\n  // mix is credited to the group holding most of its CBs (sub-threshold by definition, so usually one).\n  mv.orphanLevelByGroup={};\n  (mv.excludedMunis||[]).forEach(({muni})=>{\n    const lv=(mv.levelByMuni&&mv.levelByMuni[muni])||{}; if(!Object.keys(lv).length) return;\n    let bg=null,bc=-1;\n    mv.matrix.forEach(r=>{ if(mkey(r.municipio,r.uf)!==muni) return; const cc=+r.cbs; if(cc>bc){bc=cc;bg=String(r.grupo);} });\n    if(!bg) return;\n    mv.orphanLevelByGroup[bg]=mv.orphanLevelByGroup[bg]||{};\n    Object.entries(lv).forEach(([nv,cc])=>{ mv.orphanLevelByGroup[bg][nv]=(mv.orphanLevelByGroup[bg][nv]||0)+cc; });\n  });\n  mv.munis=munis; mv.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssignFor(mv){\n  const a={};\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=mv.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else if(mv.dest.length){ const c=mv.muniCentroid[m]; let best=mv.dest[0],bd=Infinity;\n      for(const s of mv.dest){ const sc=mv.secCentroid[s]; if(!sc||!c)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(mv,assign){\n  let d=0;\n  mv.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=mv.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,mv.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanMapFor(mv,a){const inc={};mv.dest.forEach(s=>inc[s]=0);\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=a[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  // Orphan CBs follow their leader's home sector, and that home depends on THIS candidate\n  // assignment — so they have to be counted here, or the optimizer would score and constrain a\n  // span that differs from the one the UI shows. Skipped entirely when there are no orphans.\n  if(mv.orphanByGroup&&Object.keys(mv.orphanByGroup).length){\n    mv.leaders.forEach(L=>{\n      const orphan=mv.orphanByGroup[L.grupo]||0; if(!orphan) return;\n      const byS={}; let tot=0;\n      for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s&&inc[s]!=null){ byS[s]=(byS[s]||0)+c; tot+=c; } }\n      if(!tot) return;   // no home to follow -> stranded, counted nowhere\n      const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n      if(inc[home]!=null) inc[home]+=orphan;\n    });\n  }\n  const sp={};mv.dest.forEach(s=>sp[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]);return sp;}\nfunction maxSpanOfFor(mv,a){return Math.max(...Object.values(spanMapFor(mv,a)),0);}\nfunction spanLimits(mv){\n  // Span alvo/máximo/mínimo é uma regra de fechamento de SETOR (tamanho ideal de território) —\n  // GV/mercado não têm essa noção e ignoram qualquer valor configurado nas opções da sessão.\n  if(mv && (mv.level||'setor')!=='setor') return {target:null,max:null,min:null};\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  return {target:numOrNull(st.targetSpan),max:numOrNull(st.maxSpan),min:numOrNull(st.minSpan)};}\nfunction spanLimitsOn(lim){return !!(lim.target||lim.max||lim.min);}\nfunction spanViolationsOf(sp,lim){const over=[],under=[];\n  Object.entries(sp).forEach(([s,v])=>{ if(lim.max&&v>lim.max)over.push(s); if(lim.min&&v<lim.min)under.push(s); });\n  return {over,under};}\nfunction spanViolationsFor(mv,a){return spanViolationsOf(spanMapFor(mv,a),spanLimits(mv));}\n// With a target span the objective becomes RMS distance from the ideal size (punishes too big AND\n// too small). With no target we keep the legacy objective: shave the biggest sector.\nfunction spanCostFor(mv,a){const lim=spanLimits(mv),vals=Object.values(spanMapFor(mv,a));\n  if(!vals.length) return 0;\n  if(lim.target) return Math.sqrt(vals.reduce((s,v)=>s+(v-lim.target)**2,0)/vals.length);\n  return Math.max(...vals);}\n// ---------- ÓTIMO: objetivo multi-critério ----------\n// Three normalised objectives, each 0..1, so the weights below are directly comparable. The old\n// score added a span cost measured in CBs to a desloc measured in CBs, so on a large sector the\n// balance term was numerically ~100x the desloc term and silently decided everything on its own.\n//   balance — RMS spread of the resulting spans (against the target span when one is set,\n//             otherwise against their own mean), over that reference. 0 = every destination\n//             sector ends up the same size.\n//   dist    — how far each município travels, CB-weighted: half from the share of its outer\n//             border that faces the sector it was given to, half from centroid distance. 0 =\n//             every município went to a sector it physically touches. Adjacency used to be a\n//             pure yes/no constraint (contiguity); this is what makes it a preference too.\n//   orphan  — stranded orphan CBs (leader left without a home sector, so they land nowhere at\n//             all) plus CBs split away from their leader's home, over the CBs at stake.\n//             Stranded ones count OPT_STRAND_K times: a stranded CB leaves the model entirely,\n//             a deslocated one merely changes hands.\nconst OPT_W={balance:1, dist:0.85, orphan:1};\nconst OPT_STRAND_K=3;\nconst OPT_SIMS=24;             // simultaneous simulations per run, scaled down on big stages\nconst OPT_PASSES=14;           // local-search sweeps inside one simulation\nconst OPT_EVAL_BUDGET=120000;  // total candidate evaluations across the whole portfolio\nconst OPT_EXHAUSTIVE_MAX=60000;\nfunction optDistPrep(mv){\n  let mx=0;\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__') return; const c=mv.muniCentroid[m]; if(!c) return;\n    mv.dest.forEach(s=>{ const sc=mv.secCentroid[s]; if(!sc) return;\n      const d=Math.hypot(c[0]-sc[0],c[1]-sc[1]); if(d>mx) mx=d; }); });\n  return mx||1;\n}\nfunction optDistCost(mv,m,s,dmax){\n  const v=mv.allowed[m]||{}; let tot=0; for(const k in v) tot+=v[k];\n  const share=tot?((v[s]||0)/tot):0;\n  const c=mv.muniCentroid[m], sc=mv.secCentroid[s];\n  // Missing centroid on either side is neutral (0.5), never 0 — an unmeasurable move must not\n  // score as the cheapest one on the board.\n  const geo=(c&&sc)?Math.min(1,Math.hypot(c[0]-sc[0],c[1]-sc[1])/dmax):0.5;\n  return 0.5*(1-share)+0.5*geo;\n}\nfunction optEval(mv,a,dmax){\n  const lim=spanLimits(mv);\n  const sp=spanMapFor(mv,a), vals=Object.values(sp);\n  let balance=0;\n  if(vals.length){\n    const ref=lim.target||(vals.reduce((x,y)=>x+y,0)/vals.length);\n    if(ref) balance=Math.min(1,Math.sqrt(vals.reduce((s,v)=>s+(v-ref)**2,0)/vals.length)/ref);\n  }\n  let cbs=0,dist=0;\n  for(const m in a){ const o=mv.munis[m]; if(!o) continue; const w=+o.cbs||0; cbs+=w; dist+=w*optDistCost(mv,m,a[m],dmax); }\n  dist=cbs?(dist/cbs):0;\n  let stranded=0,desloc=0,tot=0;\n  mv.leaders.forEach(L=>{\n    const byS={}; let t=0;\n    for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s){ byS[s]=(byS[s]||0)+c; t+=c; } }\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    tot+=t+orphan;\n    if(!t){ stranded+=orphan; return; }   // no allocated CBs -> no home -> these orphans land nowhere\n    const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n    desloc+=t-(byS[home]||0);\n  });\n  const orphanTerm=tot?Math.min(1,(desloc+OPT_STRAND_K*stranded)/tot):0;\n  return { cost:OPT_W.balance*balance+OPT_W.dist*dist+OPT_W.orphan*orphanTerm,\n           balance, dist, desloc, stranded, sp, viol:spanViolationsOf(sp,lim) };\n}\nfunction optScoreFor(mv,a){ return optEval(mv,a,mv.__optDmax||optDistPrep(mv)).cost; }\n// Seeded PRNG on purpose. computeMovement runs the optimizer for EVERY stage and recomputeFrom\n// re-runs every stage downstream of an edit, so a Math.random() portfolio would hand back a\n// different allocation on each pass and a saved timeline would never reproduce.\nfunction optRng(seed){ let a=seed>>>0;\n  return ()=>{ a=(a+0x6D2B79F5)>>>0; let t=Math.imul(a^(a>>>15),1|a);\n    t=(t+Math.imul(t^(t>>>7),61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }\nfunction optSeed(mv,ms){ let h=2166136261>>>0;\n  const s=String(mv.sector)+'|'+ms.join(',')+'|'+mv.dest.join(',');\n  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);\n  return h>>>0; }\nfunction contigBadFor(mv,assign){\n  // Contiguidade geográfica só faz sentido no grão de município (setor fechando): setores e GVs\n  // não precisam formar uma mancha contínua quando GV/mercado fecha — cada setor/GV vai pro destino\n  // que faz sentido pra ele, sem exigir que o conjunto redistribuído continue \"grudado\".\n  if((mv.level||'setor')!=='setor') return [];\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(mv.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(mv.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\nfunction greedyFor(mv,ms,opts){const lim=spanLimits(mv);const a={};\n  ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;\n    for(const s of opts[i]){a[m]=s;\n      // over-max only grows as we keep assigning, so it is safe to penalise mid-build;\n      // under-min shrinks, so penalising it here would just chase noise.\n      const over=lim.max?spanViolationsOf(spanMapFor(mv,a),{max:lim.max,min:null}).over.length:0;\n      const d=optScoreFor(mv,a)+(contigBadFor(mv,a).length?1e9:0)+over*1e6;\n      if(d<bd){bd=d;best=s;}}\n    a[m]=best;});\n  return a;}\nfunction optimizeFor(mv){\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__');\n  if(!ms.length||!mv.dest.length){ mv.optBadge=mv.dest.length?'':'sem setor destino identificado'; return {}; }\n  const opts=ms.map(m=>{const al=(mv.reachable[m]&&mv.reachable[m].length)?mv.reachable[m]:mv.dest.slice();return al.length?al:mv.dest.slice();});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  const lim=spanLimits(mv);\n  const dmax=optDistPrep(mv); mv.__optDmax=dmax;\n\n  // Feasibility ladder — same meaning as before, because renderSpanRule and the saved sessions\n  // both read mv.optTier: 0 = contíguo + max + min span, 1 = contíguo + max span, 2 = contíguo\n  // only. Every candidate any simulation produces is offered to all three tiers; we ship the\n  // strictest tier that got a solution, so an unreachable min/max relaxes loudly instead of\n  // stranding the stage with nothing.\n  const tier=[null,null,null],tSc=[Infinity,Infinity,Infinity],tEv=[null,null,null];\n  let feasible=0,evals=0;\n  const offer=(a,ev)=>{\n    if(contigBadFor(mv,a).length) return false;\n    feasible++;\n    const put=t=>{ if(ev.cost<tSc[t]){ tSc[t]=ev.cost; tier[t]={...a}; tEv[t]=ev; } };\n    put(2);\n    if(!ev.viol.over.length){ put(1); if(!ev.viol.under.length) put(0); }\n    return true;\n  };\n  const evalOf=a=>{ evals++; return optEval(mv,a,dmax); };\n\n  // Small enough to settle the question outright: enumerate every contiguous allocation and the\n  // winner is provably the best one under this objective. No search needed, so none is run.\n  const exhaustive=combos<=OPT_EXHAUSTIVE_MAX;\n  let sims=0;\n  if(exhaustive){\n    const cur={};\n    const rec=i=>{ if(i===ms.length){ offer(cur,evalOf(cur)); return; }\n      for(const s of opts[i]){ cur[ms[i]]=s; rec(i+1); } };\n    rec(0);\n  } else {\n    // Too big to enumerate, so run a portfolio of independent simulations instead. Each one is a\n    // seed assignment, then hill-climbing on single-município reassignments, then one perturbation\n    // and another climb to escape whatever local optimum it settled into. Different seeds land in\n    // different basins; the ladder above keeps the best across all of them.\n    const improve=a=>{\n      let ev=evalOf(a); offer(a,ev);\n      for(let pass=0;pass<OPT_PASSES;pass++){\n        let moved=false;\n        for(let i=0;i<ms.length;i++){\n          const m=ms[i],cur=a[m]; let best=cur,bev=ev;\n          for(const s of opts[i]){\n            if(s===cur) continue;\n            a[m]=s;\n            const e=evalOf(a);\n            // contiguity is a hard constraint, and the cheaper test runs first on purpose\n            if(e.cost<bev.cost-1e-12 && !contigBadFor(mv,a).length){ best=s; bev=e; }\n          }\n          a[m]=best;\n          if(best!==cur){ ev=bev; moved=true; offer(a,ev); }\n        }\n        if(!moved) break;\n      }\n    };\n    const rng=optRng(optSeed(mv,ms));\n    const perSim=Math.max(1,ms.length*Math.max(1,mv.dest.length)*OPT_PASSES);\n    const budget=Math.max(4,Math.min(OPT_SIMS,Math.floor(OPT_EVAL_BUDGET/perSim)||4));\n    const seeds=[];\n    if(mv.assign&&ms.every(m=>mv.assign[m])) seeds.push({...mv.assign});   // never regress on what is already on screen\n    seeds.push(autoAssignFor(mv));                                        // adjacency-first seed\n    seeds.push(greedyFor(mv,ms,opts));                                    // balance-first seed\n    while(seeds.length<budget){\n      // randomised construction biased toward nearby sectors — the bias keeps each seed sane, the\n      // randomness is what sends the runs into different basins\n      const a={};\n      ms.forEach((m,i)=>{ let bs=opts[i][0],bw=-Infinity;\n        for(const s of opts[i]){ const w=(1-optDistCost(mv,m,s,dmax))+rng()*0.9; if(w>bw){bw=w;bs=s;} }\n        a[m]=bs; });\n      seeds.push(a);\n    }\n    seeds.forEach(seed=>{\n      const a={...seed};\n      // a restored or legacy assign can still name a sector this stage no longer offers\n      ms.forEach((m,i)=>{ if(!opts[i].includes(a[m])) a[m]=opts[i][0]; });\n      improve(a);\n      const kick=Math.max(1,Math.round(ms.length*0.25));\n      for(let k=0;k<kick;k++){ const i=Math.floor(rng()*ms.length); a[ms[i]]=opts[i][Math.floor(rng()*opts[i].length)]; }\n      improve(a);\n    });\n    sims=seeds.length;\n  }\n\n  const t=tier.findIndex(x=>x);\n  if(t<0){ mv.optTier=-1; mv.optBadge='sem solução conectada — heurística'; return greedyFor(mv,ms,opts); }\n  const best=tier[t], ev=tEv[t]; mv.optTier=t;\n  const relax=[];\n  if(t>0 && lim.min) relax.push('span mínimo '+fmt(lim.min));\n  if(t>1 && lim.max) relax.push('span máximo '+fmt(lim.max));\n  mv.optStats={balance:ev.balance,dist:ev.dist,desloc:ev.desloc,stranded:ev.stranded,sims,evals,feasible,combos,exhaustive};\n  mv.optBadge=`ótimo · ${exhaustive?`espaço completo (${fmt(combos)})`:`${fmt(sims)} simulações`}`\n    +` · equilíbrio ±${Math.round(ev.balance*100)}% · adjacência ${Math.round((1-ev.dist)*100)}%`\n    +` · quebra ${fmt(ev.desloc)} CBs · ${fmt(ev.stranded)} fora do setor sem destino`\n    +` · pico ${fmt(maxSpanOfFor(mv,best))} CBs`\n    +(lim.target?` · alvo ${fmt(lim.target)}`:'')\n    +(relax.length?` · ⚠ inatingível: ${relax.join(' + ')}`:(spanLimitsOn(lim)?' · limites ok':''));\n  return best;\n}\nfunction computeDerived(mv){\n  const inc={},gained={},orph={};mv.dest.forEach(s=>{inc[s]=0;gained[s]=0;orph[s]=0;});\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  let desloc=0,orphanTotal=0,orphanStranded=0;\n  const L2=mv.leaders.map(L=>{const byS={};mv.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=mv.assign[m];if(s&&byS[s]!=null){byS[s]+=c;tot+=c;}}\n    // no allocated CBs => no home. Picking dest[0] by default would silently dump this leader's\n    // orphan CBs into an arbitrary sector, so we leave it null and report them as stranded.\n    const home=(tot>0&&mv.dest.length)?mv.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,mv.dest[0]):null;\n    const dl=tot-(home?byS[home]:0);desloc+=dl;if(home)gained[home]++;\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    orphanTotal+=orphan;\n    if(orphan){ if(home&&orph[home]!=null) orph[home]+=orphan; else orphanStranded+=orphan; }\n    return {...L,byS,home,desloc:dl,assignedTotal:tot,orphan,orphanHome:(orphan&&home)?home:null};});\n  const span={};mv.dest.forEach(s=>span[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]+orph[s]);\n  return {inc,gained,desloc,leaders:L2,span,orph,orphanTotal,orphanStranded};\n}\n\nasync function computeMovement(code,level,world){\n  level=level||'setor';\n  code=String(code);\n  const mv={sector:code,level,policy:'fora',view:'tobe',selMuni:null,optBadge:''};\n  const SNAP=SESS.snapLiteral;\n  // GV/mercado precisam da base ANTES de montar a query (pra saber quais CD_SETOR entram no IN);\n  // setor não precisa — mantém o timing original (a query dispara sem esperar o CSV).\n  let domBase=level==='setor'?null:await ensureDominantes();\n  const memberSectors=level==='setor'?[code]:memberSectorsOf(level,code,world);\n  if(!memberSectors.length) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const secList=memberSectors.join(',');\n\n  const mSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n     COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n     ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n     SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP}\n    GROUP BY DS_MUNICIPIO, CD_SETOR, CD_GRUPO`;\n  const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n  const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n     COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n     ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n    FROM ${ELO} WHERE country_code=1 AND sector_code IN (${secList}) AND operational_cycle<=${SESS.eloMaxCycle||999999} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n  const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n  const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, CD_SETOR, DS_NVL_PLAN_CRESC`;\n  const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n\n  const ownMatrix=rows(mR).filter(r=>+r.cbs>0);\n  mv.pureOwnMetrics=blankM();\n  ownMatrix.forEach(r=>{ mv.pureOwnMetrics.cbs+=+r.cbs; mv.pureOwnMetrics.receita+=+r.receita||0; mv.pureOwnMetrics.rec_nat+=+r.rec_nat||0; mv.pureOwnMetrics.rec_avon+=+r.rec_avon||0; mv.pureOwnMetrics.pontos+=+r.pontos||0; mv.pureOwnMetrics.pedidos+=+r.pedidos||0; mv.pureOwnMetrics.ativas+=+r.ativas||0; });\n\n  mv.levelByMuni={};\n  if(level==='setor') Object.entries(world.muniLevel||{}).forEach(([m,lv])=>{ if(world.muniOverride[m]===code) mv.levelByMuni[m]={...lv}; });\n\n  mv.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n  mv.levels=rows(lR);\n\n  let absorbedRows=[],inheritedOrphanRows=[];\n  if(level==='setor'){\n    Object.entries(world.muniOverride).forEach(([m,destSec])=>{ if(destSec===code && world.muniRow[m]) absorbedRows.push(...world.muniRow[m]); });\n    inheritedOrphanRows=(world.sectorOrphanRows&&world.sectorOrphanRows[code])||[];\n  }\n  mv.matrix=ownMatrix.concat(absorbedRows,inheritedOrphanRows);\n\n  const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n  const domUF=ufList[0]||'PR';\n  mv.domUF=domUF;\n  domBase=domBase||await ensureDominantes();\n  // Uma GV ou mercado pode espalhar por mais UFs do que \"a UF dominante + suas vizinhas\" cobre —\n  // toda UF que aparece na base de dominantes de QUALQUER setor-membro entra na janela de shapes.\n  const memberUFs=new Set([domUF]);\n  memberSectors.forEach(s=>{ (domBase[String(s)]||new Set()).forEach(entry=>{ const u=mUf(entry); if(u) memberUFs.add(u); }); });\n  const ufSet=new Set(memberUFs);\n  memberUFs.forEach(u=>(UF_BORDERS[u]||[]).forEach(b=>ufSet.add(b)));\n  const geos=await Promise.all([...ufSet].map(u=>geoUF(u).catch(()=>null)));\n  const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n  const adj=buildAdj(allFeat);\n\n  // A UF só vem da base de dominantes (setor real da linha × município), nunca de \"esse nome só\n  // apareceu numa UF carregada\" ou de um chute pra UF dominante do fechamento inteiro. Os dois\n  // eram seguros quando só 1-3 UFs entravam na janela (fechamento de setor); GV e principalmente\n  // mercado carregam dezenas de UFs de uma vez, então \"único entre as UFs carregadas\" deixa de\n  // significar \"único no Brasil\" — era exatamente esse chute que colocava um homônimo de outra UF\n  // espalhado longe do resto do mapa. Sem entrada na base, o município fica sem UF (mesmo\n  // tratamento de \"sem shape\" que já existe) em vez de arriscar aparecer no lugar errado.\n  const ufGuessed={};\n  const ufFor=(setorOf,muni)=>{const nm=norm(muni);\n    return (DOM.ufBySetorMuni&&DOM.ufBySetorMuni[String(setorOf)+'|'+nm])||'';};\n  mv.matrix.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n  rows(lmR).forEach(r=>{ const k=mkey(r.municipio,ufFor(r.setor,r.municipio)); (mv.levelByMuni[k]=mv.levelByMuni[k]||{})[r.nivel]=+r.cbs; });\n\n  const totBy={}; mv.matrix.forEach(r=>{const n=mkey(r.municipio,r.uf); totBy[n]=(totBy[n]||0)+ +r.cbs;});\n  const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>0).map(([m])=>m));\n  const withShape=[...footprint].filter(n=>adj.feat[n]||world.muniGeo[n]);\n  mv.cbsByMuni={...totBy}; mv.shapeMunis=withShape.slice();\n  mv.noShapeMunis=[...footprint].filter(n=>!(adj.feat[n]||world.muniGeo[n])).map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.noShapeCbs=mv.noShapeMunis.reduce((a,x)=>a+x.cbs,0);\n\n  // ---- RULE: quais municípios pertencem a este fechamento ----\n  // setor: exatamente a regra de hoje. GV/mercado: união da base de TODOS os setores-membro — a\n  // base já garante que um setor pertence a uma única GV/mercado, então não existe aqui o spillover\n  // fuzzy que município tem contra setor (por isso o orphan-handling abaixo é pulado nesses níveis).\n  let domOwn;\n  if(level==='setor'){\n    domOwn=domBase[code]||new Set();\n  } else {\n    domOwn=new Set();\n    memberSectors.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>domOwn.add(n)));\n  }\n  const inherited=level==='setor'?new Set(Object.keys(world.muniOverride||{}).filter(m=>world.muniOverride[m]===code)):new Set();\n  if(!domOwn.size && !inherited.size) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const belongs=n=>domOwn.has(n)||(!DOM.hasUf&&domOwn.has(mName(n)))||inherited.has(n);\n  mv.domCsvMunis=domOwn.size;\n  const dropped=level==='setor'?withShape.filter(n=>!belongs(n)):[];\n  if(withShape.length && dropped.length===withShape.length) throw new Error(`${LEVELS[level].label} ${code}: nenhum dos municípios com CBs está na base de dominantes — nada pra realocar`);\n  const droppedSet=new Set(dropped);\n  mv.excludedMunis=dropped.map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.excludedCbs=dropped.reduce((a,n)=>a+(totBy[n]||0),0);\n\n  const closingNames=withShape.filter(n=>!droppedSet.has(n));\n  const closingSet=new Set(closingNames);\n  mv.geoFeat={}; closingNames.forEach(n=>{ mv.geoFeat[n]=(adj.feat[n]&&adj.feat[n].f)||world.muniGeo[n]; });\n\n  // ---- unidade de realocação: a geometria é sempre município; a UNIDADE que troca de dono é o\n  // próprio município (setor fecha), o setor inteiro (GV fecha) ou a GV inteira (mercado fecha).\n  const municipioSetor={}; mv.matrix.forEach(r=>{ const n=mkey(r.municipio,r.uf); if(!municipioSetor[n]) municipioSetor[n]=String(r.setor); });\n  const resolveUnit=n=>{\n    if(level==='setor') return n;\n    const sOf=municipioSetor[n]; if(!sOf) return '';\n    return level==='gv'?sOf:(DOM.gvBySetor[sOf]||'');\n  };\n  mv.unitOfMuni={}; closingNames.forEach(n=>{ mv.unitOfMuni[n]=resolveUnit(n); });\n  mv.unitMunis={}; closingNames.forEach(n=>{ const u=mv.unitOfMuni[n]; if(!u) return; (mv.unitMunis[u]=mv.unitMunis[u]||[]).push(n); });\n  const units=level==='setor'?closingNames:Object.keys(mv.unitMunis);\n\n  const drawnCsv=new Set(closingNames.map(n=>DOM.hasUf?n:mName(n)));\n  const footKeyOf=k=>DOM.hasUf?(footprint.has(k)?k:null):([...footprint].find(x=>mName(x)===k)||null);\n  mv.csvMunisTotal=domOwn.size;\n  mv.ufGuessed={...ufGuessed};\n  if(level==='setor'){\n    mv.missingMunis=[...domOwn].filter(k=>!drawnCsv.has(k)).map(k=>{\n      const fk=footKeyOf(k);\n      const hasShape=!!(adj.feat[fk||k]||world.muniGeo[fk||k]);\n      const alias=[...footprint].filter(x=>x!==k&&mName(x)===mName(k));\n      let reason;\n      if(!fk&&alias.length) reason=`UF divergente — as CBs entraram na chave ${alias.join(' / ')} e a base diz ${k}`;\n      else if(!fk) reason=hasShape?'sem CBs neste setor no snapshot — desenhado como vizinho':'sem CBs neste setor no snapshot';\n      else if(!hasShape) reason='sem shape nas UFs carregadas';\n      else reason='chave não casou com a base de dominantes';\n      return {muni:k,cbs:(fk?totBy[fk]:alias.reduce((a,x)=>a+(totBy[x]||0),0))||0,reason:reason};\n    });\n  } else {\n    mv.missingMunis=[];\n  }\n\n  // ---- HARD RULE: destino tem de compartilhar a fronteira certa acima do que está fechando ----\n  // setor → mesma GV; GV → mesmo mercado; mercado → nada acima, qualquer vizinho vale.\n  const ownParent=parentOf(level,code);\n  if(level!=='mercado' && !ownParent) throw new Error(`${LEVELS[level].label} ${code} não tem ${level==='setor'?'CD_GV':'MERCADO'} na base de municípios dominantes — sem essa fronteira não há como garantir que os destinos ficam do lado certo da hierarquia`);\n  const sameParent=destCode=>!ownParent||parentOf(level,destCode)===ownParent;\n  mv.gv=ownParent;\n  const alreadyClosed=destCode=>level==='setor'?world.closedSectors.has(destCode):(level==='gv'?(world.closedGvs||new Set()).has(destCode):(world.closedMercados||new Set()).has(destCode));\n\n  const destRule=(SESS.settings&&SESS.settings.destRule)||DEFAULT_SETTINGS.destRule;\n  const destMinCbs=Math.max(1,Math.round(+((SESS.settings||{}).destMinCbs)||DEFAULT_SETTINGS.destMinCbs));\n  mv.destRule=destRule; mv.destMinCbs=destMinCbs; mv.destRuleFallback=false;\n  mv.allowed={}; mv.allowedRaw={};\n  units.forEach(u=>{ mv.allowed[u]={}; mv.allowedRaw[u]={}; });\n\n  const extVotes={}; const nbrNames=new Set(); let domSecOf={};\n  if(level==='setor'){\n    // setor: destino descoberto por ADJACÊNCIA GEOGRÁFICA de município — uma query ao vivo por\n    // vizinho, mas o perímetro de UM setor é pequeno o bastante pra valer a pena.\n    closingNames.forEach(n=>{\n      if(!adj.feat[n]) return;\n      adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o) && !droppedSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });\n    });\n    let neigh=[];\n    if(nbrNames.size){\n      const inList=[...new Set([...nbrNames].map(n=>mName(n)))].map(n=>`'${n.replace(/'/g,\"''\")}'`).join(',');\n      const nSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs\n        FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR NOT IN (${secList}) AND CD_SETOR>0 AND DT_HISTORICO=${SNAP} AND DS_MUNICIPIO IN (${inList}) GROUP BY DS_MUNICIPIO, CD_SETOR`;\n      neigh=rows(await q(nSQL));\n    }\n    neigh.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n    const byM={}; neigh.forEach(r=>{const m=mkey(r.municipio,r.uf); if(!nbrNames.has(m)) return; (byM[m]=byM[m]||[]).push(r);});\n    Object.entries(byM).forEach(([m,rs])=>{ rs.sort((a,b)=>+b.cbs-+a.cbs); domSecOf[m]=String(rs[0].setor); });\n    nbrNames.forEach(n=>{ if(world.muniOverride && world.muniOverride[n]) domSecOf[n]=world.muniOverride[n]; });\n\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    const gvHidden=new Set();\n    nbrNames.forEach(n=>{\n      const destCode=destUnitOfSector(level,domSecOf[n],world);\n      if(destCode && !sameParent(destCode)){ gvHidden.add(n); if(mv.gvRejected.indexOf(destCode)<0) mv.gvRejected.push(destCode); return; }\n      if(domSecOf[n]) mv.nbrCurrentSector[n]=destCode;\n    });\n    gvHidden.forEach(n=>{ nbrNames.delete(n); delete domSecOf[n]; delete mv.nbrCurrentSector[n]; });\n    Object.keys(extVotes).forEach(n=>{ extVotes[n]=extVotes[n].filter(v=>!gvHidden.has(v.o)); });\n\n    const isBridge=n=>destRule!=='dominant'||(totBy[n]||0)>=destMinCbs;\n    mv.bridgeMunis=closingNames.filter(isBridge);\n    mv.ignoredBridges=closingNames.filter(n=>!isBridge(n));\n\n    const wAny={}, wDom={};\n    closingNames.forEach(n=>{\n      const u=n;\n      if(!mv.allowed[u]) return;\n      const bridge=isBridge(n);\n      (extVotes[n]||[]).forEach(({o,w})=>{\n        const destCode=mv.nbrCurrentSector[o];\n        if(!destCode || destCode===code || alreadyClosed(destCode)) return;\n        mv.allowed[u][destCode]=(mv.allowed[u][destCode]||0)+w;\n        mv.allowedRaw[u][destCode]=(mv.allowedRaw[u][destCode]||0)+w;\n        wAny[destCode]=(wAny[destCode]||0)+w;\n        if(bridge) wDom[destCode]=(wDom[destCode]||0)+w;\n      });\n    });\n    let secWeight=wAny;\n    if(destRule==='dominant'){\n      if(Object.keys(wDom).length) secWeight=wDom;\n      else { secWeight=wAny; mv.destRuleFallback=true; }\n    }\n    mv.dest=Object.keys(secWeight).sort((a,b)=>secWeight[b]-secWeight[a]).slice(0,6);\n    if(!mv.dest.length){\n      mv.dest=[...new Set(Object.values(mv.nbrCurrentSector))].filter(destCode=>destCode&&destCode!==code&&!alreadyClosed(destCode)&&sameParent(destCode)).slice(0,6);\n      units.forEach(u=>{ if(mv.allowed[u]&&!Object.keys(mv.allowed[u]).length){ mv.dest.forEach(destCode=>mv.allowed[u][destCode]=1); } });\n    }\n  } else {\n    // GV/mercado: destino é SEMPRE todo irmão válido na hierarquia — NUNCA descoberto por\n    // adjacência geográfica de município. Uma GV/mercado pode ter território espalhado por\n    // dezenas de UFs sem qualquer conexão física entre as partes (a hierarquia aqui é\n    // organizacional, não geográfica) — \"vizinho por fronteira de município\" não é só caro (a\n    // query ao vivo vira um IN-list de centenas de municípios e estoura timeout: foi exatamente\n    // isso que quebrou o fechamento de um mercado espalhado) — é a pergunta errada. A fronteira que\n    // já importa (mesma GV / mesmo mercado) é imposta pela regra de parentesco logo acima; dentro\n    // dela QUALQUER irmão é candidato, e o otimizador decide pelo equilíbrio de span e continuidade\n    // de líder, nunca por proximidade física — geografia não pesa aqui.\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    mv.bridgeMunis=closingNames.slice();\n    mv.ignoredBridges=[];\n    const siblings=level==='gv'\n      ? [...(DOM.gvsByMercado[ownParent]||[])]\n      : Object.keys(DOM.sectorsByMercado||{});\n    mv.dest=siblings.filter(destCode=>destCode!==code && !alreadyClosed(destCode) && sameParent(destCode));\n    units.forEach(u=>{ mv.dest.forEach(destCode=>{ mv.allowed[u][destCode]=1; mv.allowedRaw[u][destCode]=1; }); });\n  }\n  mv.dest=mv.dest.filter(sameParent);\n\n  // ---- O QUE É DESENHADO: território COMPLETO de cada destino (não só a borda), sempre lido da\n  // mesma base de dominantes já usada pro lado que fecha.\n  const territoryOf=destCode=>{\n    const secs=memberSectorsOf(level,destCode,world);\n    const out=new Set();\n    secs.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>out.add(n)));\n    return out;\n  };\n  const drawSet=new Set(mv.dest);\n  mv.dest.forEach(destCode=>{\n    territoryOf(destCode).forEach(n=>{\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  mv.nbrFeat=[...nbrNames].filter(n=>drawSet.has(mv.nbrCurrentSector[n])).map(n=>adj.feat[n]&&adj.feat[n].f).filter(Boolean);\n  units.forEach(u=>{ Object.keys(mv.allowed[u]||{}).forEach(destCode=>{ if(!mv.dest.includes(destCode)) delete mv.allowed[u][destCode]; }); });\n  mv.color={}; mv.dest.forEach((destCode,i)=>mv.color[destCode]=PALETTE[i%PALETTE.length]);\n\n  const accC={}; Object.entries(mv.nbrCurrentSector).forEach(([nb,destCode])=>{ if(!mv.dest.includes(destCode))return; const f=(adj.feat[nb]&&adj.feat[nb].f)||world.muniGeo[nb]; if(!f)return; const c=centroidLL(f); accC[destCode]=accC[destCode]||[0,0,0]; accC[destCode][0]+=c[0]; accC[destCode][1]+=c[1]; accC[destCode][2]++; });\n  mv.secCentroid={}; mv.dest.forEach(destCode=>{ if(accC[destCode]) mv.secCentroid[destCode]=[accC[destCode][0]/accC[destCode][2],accC[destCode][1]/accC[destCode][2]]; });\n\n  if(level==='setor'){\n    mv.muniCentroid={}; closingNames.forEach(n=>{ if(mv.geoFeat[n]) mv.muniCentroid[n]=centroidLL(mv.geoFeat[n]); });\n    mv.closeAdj={}; closingNames.forEach(n=>{ mv.closeAdj[n]=adj.feat[n]?adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o):[]; });\n  } else {\n    mv.muniCentroid={};\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const cs=ms.map(n=>mv.geoFeat[n]&&centroidLL(mv.geoFeat[n])).filter(Boolean);\n      if(cs.length) mv.muniCentroid[u]=[cs.reduce((a,c)=>a+c[0],0)/cs.length,cs.reduce((a,c)=>a+c[1],0)/cs.length];\n    });\n    mv.closeAdj={};\n    const unitSet=new Set(units);\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const touch=new Set();\n      ms.forEach(n=>{ if(!adj.feat[n]) return; adj.nbrs(n).forEach(({o})=>{ const ou=mv.unitOfMuni[o]; if(ou&&ou!==u&&unitSet.has(ou)) touch.add(ou); }); });\n      mv.closeAdj[u]=[...touch];\n    });\n  }\n  const comp={}; let ci=0; units.forEach(n=>{ if(comp[n]!=null)return; const st=[n]; comp[n]=ci; while(st.length){ const x=st.pop(); (mv.closeAdj[x]||[]).forEach(o=>{ if(comp[o]==null){ comp[o]=ci; st.push(o); } }); } ci++; });\n  const compAllowed={}; units.forEach(n=>{ const cid=comp[n]; (compAllowed[cid]=compAllowed[cid]||new Set()); Object.keys(mv.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s)); });\n  mv.reachable={}; units.forEach(n=>{ mv.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>mv.dest.includes(s)); });\n\n  if(mv.dest.length){\n    const destSectorMap={}; mv.dest.forEach(destCode=>{ destSectorMap[destCode]=memberSectorsOf(level,destCode,world); });\n    const allDestSecs=[...new Set(Object.values(destSectorMap).flat())];\n    const rList=allDestSecs.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    const secMetrics={}; rows(bR).forEach(r=>{ secMetrics[String(r.setor)]=r; if(!SESS.sectorBaseCache[String(r.setor)]) SESS.sectorBaseCache[String(r.setor)]=r; });\n    const secLevel={}; rows(blR).forEach(r=>{ const s=String(r.setor); (secLevel[s]=secLevel[s]||{})[r.nivel]=+r.cbs; });\n    mv.baseMetrics={}; mv.baseLevel={};\n    mv.dest.forEach(destCode=>{\n      const secs=destSectorMap[destCode];\n      const acc=blankM(); acc.setor=destCode;\n      secs.forEach(s=>{ const r=secMetrics[String(s)]; if(!r) return; Object.keys(blankM()).forEach(k=>{ acc[k]=(+acc[k]||0)+(+r[k]||0); }); });\n      mv.baseMetrics[destCode]=acc;\n      const lv={}; secs.forEach(s=>{ const l=secLevel[String(s)]; if(!l) return; Object.entries(l).forEach(([nv,c])=>{ lv[nv]=(lv[nv]||0)+c; }); });\n      mv.baseLevel[destCode]=lv;\n    });\n  } else { mv.baseMetrics={}; mv.baseLevel={}; }\n\n  mv.dest.forEach(destCode=>{\n    if(!mv.baseMetrics[destCode]) mv.baseMetrics[destCode]={setor:destCode,cbs:0,ativas:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0};\n    const inflowSrc=level==='setor'?world.sectorInflow:(level==='gv'?world.gvInflow:world.mercadoInflow);\n    const infl=inflowSrc&&inflowSrc[destCode];\n    if(infl) Object.keys(blankM()).forEach(k=>{ mv.baseMetrics[destCode][k]=(+mv.baseMetrics[destCode][k]||0)+(infl[k]||0); });\n    if(level==='setor'){\n      const inflLv=world.sectorInflowLevel&&world.sectorInflowLevel[destCode];\n      if(inflLv){ mv.baseLevel[destCode]=mv.baseLevel[destCode]||{}; Object.entries(inflLv).forEach(([nv,c])=>{ mv.baseLevel[destCode][nv]=(mv.baseLevel[destCode][nv]||0)+c; }); }\n    }\n  });\n\n  aggregateMovement(mv);\n  mv.assign=optimizeFor(mv);\n  return mv;\n}\n\n// ---------- cascade engine ----------\nasync function computeAt(i){\n  await ensurePinned();\n  const world=foldWorldState(SESS.timeline,i);\n  const sec=SESS.timeline[i].sector;\n  const level=SESS.timeline[i].level||'setor';\n  const mv=await computeMovement(sec,level,world);\n  SESS.timeline[i]=mv;\n}\nasync function recomputeFrom(i){\n  for(let k=i;k<SESS.timeline.length;k++){\n    loader(true,`recalculando etapa ${k+1} de ${SESS.timeline.length}…`);\n    try{ await computeAt(k); }\n    catch(e){\n      // A stage that failed to recompute still holds its PREVIOUS result, so the timeline would\n      // silently mix pre- and post-change etapas. Flag this stage and everything after it as stale\n      // (dashed chip + ⟳) so the state is visible and flushDirty retries from here.\n      SESS.dirtyFrom = SESS.dirtyFrom==null ? k : Math.min(SESS.dirtyFrom,k);\n      throw e;\n    }\n  }\n}\nasync function flushDirty(){\n  if(SESS.dirtyFrom==null) return;\n  const from=SESS.dirtyFrom; SESS.dirtyFrom=null;\n  loader(true,'recalculando etapas seguintes…');\n  try{ await recomputeFrom(from); }\n  catch(e){ $('#ds').textContent='erro ao recalcular cadeia: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n}\nasync function addMovement(level,sec){\n  level=level||'setor';\n  sec=(sec==null?'':String(sec)).trim();\n  if(!sec) return;\n  if(!movementAvailable(level)){ $('#ds').textContent=`Fechar ${LEVELS[level].label} ainda não está disponível.`; $('#ds').classList.add('err'); return; }\n  if(!hierarchyAllows(level)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; $('#ds').textContent=`esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`; $('#ds').classList.add('err'); return; }\n  const closedNow=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===level).map(mv=>mv.sector));\n  if(closedNow.has(sec)){ $('#ds').textContent=LEVELS[level].label+' '+sec+' já fechado(a) nesta timeline.'; $('#ds').classList.add('err'); return; }\n  if(level==='setor' && !SESS.sectorsList.some(s=>s.setor===sec)){ $('#ds').textContent='setor \"'+sec+'\" não existe no snapshot congelado.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  loader(true,`fechando ${lc(LEVELS[level].label)} `+sec+' — puxando dado do snapshot congelado…');\n  $('#liveState').classList.remove('err');\n  try{\n    SESS.timeline.push({sector:String(sec),level:level});\n    await computeAt(SESS.timeline.length-1);\n    SESS.selected=SESS.timeline.length-1;\n    $('#ds').classList.remove('err'); $('#ds').textContent='';\n  }catch(e){\n    SESS.timeline.pop();\n    $('#ds').textContent=`erro ao fechar ${lc(LEVELS[level].label)} `+sec+': '+e.message; $('#ds').classList.add('err');\n  } finally { loader(false); renderAll(); }\n}\nfunction removeFrom(i){\n  SESS.timeline.length=i;\n  SESS.selected=Math.min(SESS.selected,Math.max(0,SESS.timeline.length-1));\n  if(SESS.dirtyFrom!=null && SESS.dirtyFrom>=SESS.timeline.length) SESS.dirtyFrom=null;\n  renderAll();\n}\nasync function editSelected(mutator){\n  const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  mutator(mv);\n  if(SESS.selected<SESS.timeline.length-1){\n    SESS.dirtyFrom = SESS.dirtyFrom==null ? SESS.selected+1 : Math.min(SESS.dirtyFrom, SESS.selected+1);\n  }\n  renderAll();\n}\n\n// ---------- sectors list / picker ----------\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1) GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  SESS.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n}\nfunction renderSectorPicker(){\n  const ready=SESS.stage==='main';\n  const ob=$('#openMove'); if(ob) ob.disabled=!ready;\n}\n\n// ---------- movement modal (setor / GV / mercado) ----------\n// Hierarquia mercado > GV > setor: a sessão trava no nível da primeira movimentação. Misturar\n// níveis quebraria a leitura de \"antes/depois\" da cadeia — fechar uma GV já redistribui setores\n// inteiros pra outras GVs; fechar um setor isolado depois não bate mais com o mundo que sobrou.\n// de/a/no: contrações de \"de\"/\"a\"/\"em\" com o artigo de cada nível — GV é feminina (a Gerência de\n// Vendas), setor e mercado são masculinos. Errar o gênero aqui é o tipo de coisa que grita \"isto foi\n// feito pra setor e só remendado pros outros níveis\", exatamente o que este objeto existe pra evitar.\nconst LEVELS={\n  setor:{label:'Setor',codeLbl:'Setor a fechar',placeholder:'código do setor',childLabel:'Município',destLabel:'Setor',de:'do',a:'ao',no:'no',parentLabel:'GV',parentPhrase:'da mesma GV',parentWord:'gerência'},\n  gv:{label:'GV',codeLbl:'GV a fechar',placeholder:'código da GV',childLabel:'Setor',destLabel:'GV',de:'da',a:'à',no:'na',parentLabel:'Mercado',parentPhrase:'do mesmo mercado',parentWord:'mercado'},\n  mercado:{label:'Mercado',codeLbl:'Mercado a fechar',placeholder:'nome do mercado',childLabel:'GV',destLabel:'Mercado',de:'do',a:'ao',no:'no',parentLabel:null,parentPhrase:null,parentWord:null},\n};\n// GV é sigla — .toLowerCase() a transformaria em \"gv\", errado em qualquer posição da frase. Palavras\n// normais (Setor/Mercado/Município) minúsculam à vontade no meio de uma frase; GV nunca.\nconst LOWER={Setor:'setor',Mercado:'mercado',Município:'município',GV:'GV'};\nconst lc=w=>LOWER[w]||w.toLowerCase();\n// Hierarquia é uma catraca, não um cadeado: mercado(0) > GV(1) > setor(2). Fechar um nível mais\n// alto (número menor) trava PRA SEMPRE nesta sessão qualquer nível mais baixo (número maior) —\n// mas o próprio nível, ou um ainda mais alto, continua liberado. É assim que \"posso fechar setores\n// até fechar uma GV; posso fechar GVs até fechar um mercado\" funciona: nunca destrava de volta.\nconst LEVEL_RANK={mercado:0,gv:1,setor:2};\nfunction sessionMinRank(){\n  let r=LEVEL_RANK.setor;\n  SESS.timeline.forEach(mv=>{ const rk=LEVEL_RANK[mv.level||'setor']; if(rk<r) r=rk; });\n  return r;\n}\nfunction hierarchyAllows(lvl){ return LEVEL_RANK[lvl]<=sessionMinRank(); }\nfunction movementAvailable(lvl){ return lvl==='setor'||lvl==='gv'||lvl==='mercado'; }\n// ---------- hierarquia: setor ⊂ GV ⊂ mercado — tudo derivado da base de dominantes (DOM) ----------\n// memberSectors: o escopo de CD_SETOR que a query no CRM precisa cobrir pra fechar este código, em\n// qualquer nível — um setor é ele mesmo; uma GV é todo setor que a base lhe atribui; um mercado é\n// todo setor de toda GV que a base atribui a ele.\n// membros ATUAIS de um GV/mercado, um nível abaixo (setores de uma GV; GVs de um mercado) — nunca\n// só a foto estática do CSV: uma etapa anterior NESTA MESMA sessão pode já ter movido um filho pra\n// dentro ou pra fora deste código, e um fechamento seguinte tem de enxergar o mundo como ele está\n// agora, não como o CSV descreveu antes de qualquer movimentação.\nfunction currentMembersOf(level,code,world){\n  code=String(code);\n  // \"gv\": setores da GV (DOM.sectorsByGv). \"mercado\": GVs do mercado (DOM.gvsByMercado) — NUNCA\n  // sectorsByMercado, que pula a camada de GV inteira e faz memberSectorsOf tratar códigos de\n  // setor como se fossem códigos de GV (não batem em DOM.sectorsByGv, e o fechamento morre com\n  // \"não existe na base\" mesmo quando o mercado está lá).\n  const staticMembers=level==='gv'?(DOM.sectorsByGv[code]||new Set()):(DOM.gvsByMercado[code]||new Set());\n  const overrideMap=level==='gv'?(world&&world.secOverride):(world&&world.gvOverride);\n  const out=new Set(staticMembers);\n  if(overrideMap){\n    Object.entries(overrideMap).forEach(([child,dest])=>{\n      if(dest===code) out.add(child);\n      else if(staticMembers.has(child)) out.delete(child);\n    });\n  }\n  return [...out];\n}\n// setores que a query no CRM precisa cobrir pra fechar este código, em qualquer nível — setor é\n// ele mesmo; GV é seus setores ATUAIS; mercado é todo setor de toda GV ATUALMENTE sua (setor não\n// troca de GV sozinho numa sessão de mercado — só GV inteira troca de mercado).\nfunction memberSectorsOf(level,code,world){\n  code=String(code);\n  if(level==='setor') return [code];\n  if(level==='gv') return currentMembersOf('gv',code,world);\n  const gvs=currentMembersOf('mercado',code,world);\n  const out=new Set();\n  gvs.forEach(gv=>(DOM.sectorsByGv[gv]||new Set()).forEach(s=>out.add(s)));\n  return [...out];\n}\n// fronteira que um destino tem de respeitar: GV do setor que fecha, mercado da GV que fecha —\n// mercado não tem nada acima, então não existe regra pra ele. Estática de propósito: um setor não\n// troca de GV nem uma GV troca de mercado só por existirem etapas anteriores nesta sessão — quem\n// muda de dono é o NÍVEL ABAIXO do que está fechando, nunca o próprio código sendo avaliado aqui.\nfunction parentOf(level,code){\n  code=String(code);\n  if(level==='setor') return DOM.gvBySetor[code]||'';\n  if(level==='gv') return DOM.mercadoByGv[code]||'';\n  return '';\n}\n// dado o setor que uma município vizinha atualmente pertence (fato ao vivo do CRM, nunca muda de\n// verdade), a que CANDIDATO A DESTINO isso corresponde neste nível — considerando que uma etapa\n// anterior desta sessão pode já ter movido esse setor (GV fechando) ou essa GV (mercado fechando)\n// pra outro dono, o que a foto estática do CSV sozinha não enxergaria.\nfunction destUnitOfSector(level,setorCode,world){\n  setorCode=String(setorCode);\n  if(level==='setor') return setorCode;\n  if(level==='gv'){ const ov=world&&world.secOverride&&world.secOverride[setorCode]; return ov||DOM.gvBySetor[setorCode]||''; }\n  const gv=DOM.gvBySetor[setorCode]||'';\n  const ov=world&&world.gvOverride&&world.gvOverride[gv];\n  return ov||DOM.mercadoBySetor[setorCode]||'';\n}\nfunction currentMoveLevel(){ const r=root.querySelector('input[name=\"moveLevel\"]:checked'); return (r&&r.value)||'setor'; }\nfunction openMove(){\n  const minRank=sessionMinRank();\n  const lastLevel=SESS.timeline.length?(SESS.timeline[SESS.timeline.length-1].level||'setor'):'setor';\n  root.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>{\n    const lvl=r.value;\n    r.disabled=!movementAvailable(lvl)||!hierarchyAllows(lvl);\n    r.checked=hierarchyAllows(lastLevel)?lvl===lastLevel:lvl===Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank);\n  });\n  const note=$('#moveLockNote');\n  const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank)].label;\n  if(note) note.textContent=minRank<LEVEL_RANK.setor\n    ? `Esta sessão já fechou um nível mais alto na hierarquia — só \"${minLabel}\" (ou acima) continua disponível daqui pra frente.`\n    : 'Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.';\n  $('#moveCode').value='';\n  const err=$('#moveErr'); if(err){ err.textContent=''; err.classList.add('hidden'); }\n  const st=$('#moveStatus'); if(st) st.textContent='';\n  $('#moveBg').classList.remove('hidden');\n  renderMoveForm();\n  const c=$('#moveCancel'); if(c) c.focus();\n}\nfunction closeMove(){ const b=$('#moveBg'); if(b) b.classList.add('hidden'); }\nfunction renderMoveForm(){\n  root.querySelectorAll('#moveBg .opt-row').forEach(r=>r.classList.toggle('on',r.dataset.lvl===currentMoveLevel()));\n  const lvl=LEVELS[currentMoveLevel()];\n  $('#moveCodeLbl').textContent=lvl.codeLbl;\n  $('#moveCode').placeholder=lvl.placeholder;\n  loadMoveOptions();\n}\nasync function loadMoveOptions(){\n  const lvl=currentMoveLevel();\n  const dl=$('#moveCodeList'); if(!dl) return;\n  const closedSet=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===lvl).map(mv=>mv.sector));\n  if(lvl==='setor'){\n    dl.innerHTML=SESS.sectorsList.filter(s=>!closedSet.has(s.setor))\n      .map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n    return;\n  }\n  dl.innerHTML='';\n  try{ await ensureDominantes(); }catch(e){ return; }\n  if(lvl==='gv'){\n    const gvs=Object.keys(DOM.sectorsByGv||{}).filter(g=>!closedSet.has(g)).sort();\n    dl.innerHTML=gvs.map(g=>`<option value=\"${g}\">${g}${DOM.gvLabel[g]?' · '+esc(DOM.gvLabel[g]):''} · ${DOM.sectorsByGv[g].size} setor(es)</option>`).join('');\n  } else if(lvl==='mercado'){\n    const mks=Object.keys(DOM.sectorsByMercado||{}).filter(m=>!closedSet.has(m)).sort();\n    dl.innerHTML=mks.map(m=>`<option value=\"${m}\">${m} · ${(DOM.gvsByMercado[m]||new Set()).size} GV(s) · ${DOM.sectorsByMercado[m].size} setor(es)</option>`).join('');\n  }\n}\nfunction moveRandPick(){\n  const opts=[...($('#moveCodeList').options||[])].map(o=>o.value);\n  if(!opts.length) return;\n  $('#moveCode').value=opts[Math.floor(Math.random()*opts.length)];\n}\nasync function submitMove(){\n  const lvl=currentMoveLevel();\n  const code=($('#moveCode').value||'').trim();\n  const err=$('#moveErr');\n  const fail=msg=>{ if(err){ err.textContent=msg; err.classList.remove('hidden'); } };\n  if(err){ err.textContent=''; err.classList.add('hidden'); }\n  if(!movementAvailable(lvl)) return fail(`Fechar ${LEVELS[lvl].label} ainda não está disponível.`);\n  if(!hierarchyAllows(lvl)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; return fail(`Esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`); }\n  if(!code) return fail('Informe um código.');\n  closeMove();\n  await addMovement(lvl,code);\n}\nfunction renderStage(){\n  const isMain=SESS.stage==='main';\n  const cc=$('#confirmCard'), mw=$('#mainWrap');\n  const hdr=$('#topHeader'), rl=$('#timelineRail');\n  if(hdr) hdr.classList.toggle('hidden', !isMain);\n  if(rl) rl.classList.toggle('hidden', !isMain);\n  if(cc) cc.classList.toggle('hidden', isMain);\n  if(mw) mw.classList.toggle('hidden', !isMain);\n  const cb=$('#confirmBody');\n  if(cb && !isMain) cb.innerHTML = SESS.snapshotRaw\n    ? `Snapshot congelado · atualizado em <b>${fmtSyncedAt(SESS.snapshotSyncedAt)}</b> · <b>${SESS.sectorsList.length}</b> setor(es) disponível(eis) pra fechar. Nenhuma consulta de detalhe roda até você confirmar e escolher o primeiro setor.`\n    : 'congelando snapshot…';\n}\n\n// ---------- session persistence ----------\n// ONE table (`timeline-shared-sessions`) holds BOTH kinds of row, so SESSIONS_DS and SHARED_DS\n// deliberately resolve to the SAME DataSet — the two names mark intent, not two stores:\n//   · working session  — carries `movements`; saveSession updates it in place\n//   · shared snapshot  — carries `sharedAt` + `stages`; doShare CREATES it and nothing updates it\n// listSessions MUST exclude the shared rows. They are write-once by contract, and if one showed\n// up in the picker, loading it and pressing Salvar would fire ds.update() straight at a frozen\n// snapshot. Filtering here is what keeps that unreachable — it is the guard, not a cosmetic.\n// Both lookups stay because a policy-gated DataSet's Process resolves late; same race the reader\n// component polls for. SESSIONS_DS lives at module scope so no call site can hit it in the TDZ.\nasync function listSessions(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return [];\n  await ds.refresh();\n  const recs=ds.records()||[];\n  return [...recs].filter(r=>!r.sharedAt).sort((a,b)=>new Date(b.updatedAt||0)-new Date(a.updatedAt||0));\n}\nasync function refreshSessionPicker(){\n  const list=await listSessions().catch(()=>[]);\n  SESS.sessionsList=list;\n  const opts=list.map(s=>`<option value=\"${s._id}\">${esc(s.label||'sem nome')} · ${fmt((s.movements||[]).length)} etapa(s)</option>`).join('')+`<option value=\"\">+ Nova sessão</option>`;\n  const sel=$('#sessionPick'); if(sel){ sel.innerHTML=opts; sel.value=SESS.sessionId||''; }\n  const csel=$('#confirmSessionPick'); if(csel){ csel.innerHTML=opts; csel.value=SESS.sessionId||''; }\n}\nasync function saveSession(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS));\n  if(!ds){ $('#sessStatus').textContent='dataset de sessões indisponível'; return; }\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra salvar ainda'; return; }\n  await flushDirty();\n  const label=SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR'));\n  const nowIso=new Date().toISOString();\n  const payload={\n    label,\n    settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n    snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt, snapLiteral:SESS.snapLiteral, eloMaxCycle:SESS.eloMaxCycle,\n    movements:SESS.timeline.map(mv=>({\n      sector:mv.sector, level:mv.level||'setor', assign:mv.assign, policy:mv.policy,\n      matrix:mv.matrix, trend:mv.trend, levels:mv.levels, levelByMuni:mv.levelByMuni,\n      pureOwnMetrics:mv.pureOwnMetrics, domUF:mv.domUF, dest:mv.dest, color:mv.color,\n      gv:mv.gv, gvRejected:mv.gvRejected,\n      missingMunis:mv.missingMunis, csvMunisTotal:mv.csvMunisTotal, ufGuessed:mv.ufGuessed,\n      baseMetrics:mv.baseMetrics, baseLevel:mv.baseLevel, geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat,\n      nbrCurrentSector:mv.nbrCurrentSector, allowed:mv.allowed, allowedRaw:mv.allowedRaw, reachable:mv.reachable,\n      destRule:mv.destRule, destMinCbs:mv.destMinCbs, destRuleFallback:mv.destRuleFallback, optTier:mv.optTier,\n      cbsByMuni:mv.cbsByMuni, shapeMunis:mv.shapeMunis, excludedMunis:mv.excludedMunis, domCsvMunis:mv.domCsvMunis,\n      excludedCbs:mv.excludedCbs, noShapeMunis:mv.noShapeMunis, noShapeCbs:mv.noShapeCbs,\n      secCentroid:mv.secCentroid, muniCentroid:mv.muniCentroid, closeAdj:mv.closeAdj, optBadge:mv.optBadge,\n    })),\n    updatedAt:nowIso,\n  };\n  $('#sessStatus').textContent='salvando…';\n  try{\n    if(SESS.sessionId){\n      await ds.update({_id:SESS.sessionId,...payload});\n    } else {\n      payload.createdAt=nowIso;\n      const created=await ds.create(payload);\n      SESS.sessionId=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    }\n    SESS.sessionLabel=label;\n    await refreshSessionPicker();\n    $('#sessStatus').textContent='salvo ✓';\n  }catch(e){ $('#sessStatus').textContent='erro ao salvar: '+e.message; }\n  setTimeout(()=>{ if($('#sessStatus')) $('#sessStatus').textContent=''; },2200);\n}\n// Duplicate = drop sessionId so saveSession takes its CREATE branch instead of its update branch.\n// The source record is never written to, the copy gets a fresh _id and its own createdAt, and the\n// copy becomes the active session (so the next Salvar edits the copy, not the original).\nasync function duplicateSession(){\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra duplicar ainda'; return; }\n  const prevId=SESS.sessionId, prevLabel=SESS.sessionLabel;\n  SESS.sessionId=null;\n  SESS.sessionLabel=(SESS.sessionLabel||'Sessão')+' (cópia)';\n  await saveSession();\n  if(!SESS.sessionId){\n    // saveSession swallows its own errors into #sessStatus, so a still-null id after the await is\n    // the only failure signal we get. Put the original identity back — otherwise a failed copy\n    // silently leaves the session detached, and the next Salvar would fork a new record instead\n    // of updating the one the user thought they were editing.\n    SESS.sessionId=prevId; SESS.sessionLabel=prevLabel;\n    await refreshSessionPicker().catch(()=>{});\n    return;\n  }\n  if($('#sessStatus')) $('#sessStatus').textContent='duplicado ✓';\n}\nasync function loadSession(id){\n  if(!id) return newSession();\n  const list=(SESS.sessionsList&&SESS.sessionsList.length)?SESS.sessionsList:await listSessions().catch(()=>[]);\n  const rec=list.find(s=>String(s._id)===String(id));\n  if(!rec) return newSession();\n  loader(true,'carregando sessão…');\n  SESS.sessionId=rec._id; SESS.sessionLabel=rec.label;\n  SESS.snapshotRaw=rec.snapshotRaw; SESS.snapshotSyncedAt=rec.snapshotSyncedAt||null; SESS.snapLiteral=rec.snapLiteral; SESS.eloMaxCycle=rec.eloMaxCycle;\n  SESS.settings={...DEFAULT_SETTINGS,...(rec.settings||{})};\n  SESS.timeline=(rec.movements||[]).map(m=>({sector:String(m.sector),level:m.level||'setor'}));\n  SESS.selected=0; SESS.sectorBaseCache={}; SESS.stage='main'; SESS.dirtyFrom=null;\n  try{\n    for(let i=0;i<SESS.timeline.length;i++){\n      const saved=(rec.movements||[])[i]||{};\n      // Sessions saved before the UF became part of the key hold bare-name keys in matrix rows,\n      // assign, geoFeat and closeAdj alike. Restoring that cache would mix key shapes the moment the\n      // stage is recomputed — and it would preserve the very collision this fixes — so those stages\n      // are replayed against the pinned snapshot and the manual assignment is remapped by name.\n      const legacyKeys=!!(saved.matrix&&saved.matrix.length&&saved.matrix.every(r=>r&&r.uf==null));\n      // Sessões salvas antes da regra de gerência podem carregar um destino de outra GV no cache.\n      // Restaurar esse cache manteria o setor proibido na tela para sempre, então essas etapas são\n      // reprocessadas contra o snapshot fixado — igual ao tratamento das chaves legadas.\n      const crossGv=await savedCrossesGv(saved);\n      if(saved.matrix&&!legacyKeys&&!crossGv){\n        loader(true,`restaurando etapa ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector} (cache, sem consulta)…`);\n        const mv={sector:String(saved.sector), level:saved.level||'setor', policy:saved.policy||'fora', view:'tobe', selMuni:null, optBadge:saved.optBadge||''};\n        mv.matrix=saved.matrix; mv.trend=saved.trend||[]; mv.levels=saved.levels||[]; mv.levelByMuni=saved.levelByMuni||{};\n        mv.pureOwnMetrics=saved.pureOwnMetrics||blankM(); mv.domUF=saved.domUF; mv.dest=saved.dest||[]; mv.color=saved.color||{};\n        mv.baseMetrics=saved.baseMetrics||{}; mv.baseLevel=saved.baseLevel||{}; mv.geoFeat=saved.geoFeat||{}; mv.nbrFeat=saved.nbrFeat||[];\n        mv.nbrCurrentSector=saved.nbrCurrentSector||{}; mv.allowed=saved.allowed||{}; mv.reachable=saved.reachable||{};\n        mv.allowedRaw=saved.allowedRaw||saved.allowed||{};\n        mv.destRule=saved.destRule||SESS.settings.destRule; mv.destMinCbs=saved.destMinCbs||SESS.settings.destMinCbs; mv.destRuleFallback=!!saved.destRuleFallback;\n        mv.optTier=(saved.optTier==null?0:saved.optTier);\n        mv.gv=saved.gv||''; mv.gvRejected=saved.gvRejected||[];\n        mv.missingMunis=saved.missingMunis||[]; mv.csvMunisTotal=saved.csvMunisTotal||0;\n        mv.ufGuessed=saved.ufGuessed||{};\n        mv.cbsByMuni=saved.cbsByMuni||{}; mv.shapeMunis=saved.shapeMunis||[];\n        mv.excludedMunis=saved.excludedMunis||[]; mv.excludedCbs=saved.excludedCbs||0; mv.domCsvMunis=saved.domCsvMunis||0;\n        mv.noShapeMunis=saved.noShapeMunis||[]; mv.noShapeCbs=saved.noShapeCbs||0;\n        mv.secCentroid=saved.secCentroid||{}; mv.muniCentroid=saved.muniCentroid||{}; mv.closeAdj=saved.closeAdj||{};\n        aggregateMovement(mv);\n        mv.assign=saved.assign||{};\n        if((mv.level||'setor')==='setor') mv.dest.forEach(s=>{ if(!SESS.sectorBaseCache[s]) SESS.sectorBaseCache[s]=mv.baseMetrics[s]; });\n        SESS.timeline[i]=mv;\n      } else {\n        loader(true,`replay ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector}…`);\n        await computeAt(i);\n        if(saved.assign) SESS.timeline[i].assign=legacyKeys?remapAssign(SESS.timeline[i],saved.assign):saved.assign;\n        if(saved.policy) SESS.timeline[i].policy=saved.policy;\n      }\n    }\n    await loadSectors();\n  }catch(e){ $('#ds').textContent='erro ao carregar sessão: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n  renderAll();\n}\n// Re-points a legacy bare-name assignment at the NAME/UF key it can only mean. When a name resolves\n// to more than one município in the recomputed stage the manual choice is genuinely ambiguous —\n// dropping it lets the optimizer allocate instead of guessing on the operator's behalf.\n// True when a cached stage names a destination outside the closing sector's gerência de vendas.\n// Judges the cache by TODAY's rule, reading the (memoised) dominantes base rather than trusting what\n// the session was saved with. Unknown GV on the closing sector answers false: the stage then loads\n// from cache instead of being replayed into the hard throw in computeMovement, which would leave the\n// operator staring at a broken session with no way back.\nasync function savedCrossesGv(saved){\n  if(!saved||!saved.sector||!(saved.dest||[]).length) return false;\n  let gvBy={};\n  try{ await ensureDominantes(); gvBy=DOM.gvBySetor||{}; }catch(e){ return false; }\n  const own=gvBy[String(saved.sector)]||'';\n  if(!own) return false;\n  if(saved.dest.some(s=>(gvBy[String(s)]||'')!==own)) return true;\n  // A stage saved before foreign-GV neighbours stopped being drawn can hold a perfectly clean dest and\n  // STILL carry their shapes in nbrFeat/nbrCurrentSector — it would repaint the old map straight from\n  // cache. An unknown gerência is not evidence of a foreign one, so only a known, differing GV replays.\n  return Object.values(saved.nbrCurrentSector||{}).some(s=>{ const g=gvBy[String(s)]||''; return g&&g!==own; });\n}\nfunction remapAssign(mv,savedAssign){\n  const keys=Object.keys(mv.munis||{}).filter(k=>k!=='__OUTROS__');\n  const out={};\n  Object.entries(savedAssign||{}).forEach(([k,v])=>{\n    if(keys.indexOf(k)>=0){ out[k]=v; return; }\n    const cand=keys.filter(x=>mName(x)===mName(k));\n    if(cand.length===1) out[cand[0]]=v;\n  });\n  return out;\n}\nfunction newSession(){\n  SESS.sessionId=null; SESS.sessionLabel=null; SESS.timeline=[]; SESS.selected=0; SESS.sectorBaseCache={};\n  SESS.settings={...DEFAULT_SETTINGS};\n  SESS.snapshotRaw=null; SESS.snapshotSyncedAt=null; SESS.snapLiteral=null; SESS.eloMaxCycle=null; SESS.stage='loading'; SESS.dirtyFrom=null;\n  (async()=>{\n    loader(true,'iniciando nova sessão…');\n    try{ await Promise.all([ensurePinned(), loadSectors()]); SESS.stage='confirm'; }\n    catch(e){ setError('erro init: '+e.message); }\n    finally{ loader(false); renderAll(); }\n  })();\n}\n\n// ---------- snapshot refresh ----------\n// Deliberately breaks the frozen-snapshot guarantee, on demand: re-resolves DT_HISTORICO and\n// replays the whole timeline against the new one. Skips the replay when the snapshot has not\n// actually moved, so a stray click on a long timeline costs one cheap probe instead of ~8 queries\n// per etapa.\nasync function refreshSnapshot(){\n  if(SESS.stage!=='main' || SESS.snapBusy) return;\n  SESS.snapBusy=true;\n  const prevRaw=SESS.snapshotRaw;\n  const pill=$('#liveState'); if(pill){ pill.classList.add('busy'); pill.textContent='◌ atualizando snapshot…'; }\n  loader(true,'buscando o snapshot mais recente…');\n  let ok=false;\n  try{\n    const p=await resolveSnapshotPin();\n    if(!p.snapshotRaw) throw new Error('não foi possível resolver DT_HISTORICO');\n    const same=String(p.snapshotRaw)===String(prevRaw);\n    SESS.snapshotRaw=p.snapshotRaw; SESS.snapshotSyncedAt=p.syncedAt; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n    await loadSectors();   // the sector list follows the live max, so it can have moved too\n    if(same){\n      $('#ds').textContent=`snapshot já era o mais recente · atualizado em ${fmtSyncedAt(p.syncedAt)} — nada recalculado`;\n    } else if(SESS.timeline.length){\n      SESS.dirtyFrom=null;\n      SESS.sectorBaseCache={};   // baselines come from the snapshot, so every one of them changed\n      loader(true,'snapshot novo — recalculando a timeline…');\n      await recomputeFrom(0);\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)} · ${fmt(SESS.timeline.length)} etapa(s) recalculada(s)`;\n    } else {\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)}`;\n    }\n    $('#ds').classList.remove('err');\n    ok=true;\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?null:SESS.dirtyFrom+1);\n    $('#ds').textContent='erro ao atualizar o snapshot'+(at?` na etapa ${at} — etapa ${at} em diante ficou desatualizada (⟳)`:'')+': '+e.message;\n    $('#ds').classList.add('err');\n  }finally{\n    SESS.snapBusy=false;\n    if(pill) pill.classList.remove('busy');\n    loader(false); renderAll();\n    if(ok) setTimeout(()=>{ const d=$('#ds'); if(d&&!d.classList.contains('err')) d.textContent=''; },7000);\n  }\n}\n\n// ---------- session options ----------\nfunction previewDest(mv,rule,minCbs){\n  if(!mv||!mv.allowedRaw||!mv.munis) return null;\n  const w={}; let relaxed=false;\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const add=m=>Object.entries(mv.allowedRaw[m]||{}).forEach(([s,v])=>{ w[s]=(w[s]||0)+v; });\n  // allowedRaw only carries municipalities already inside the sector's dominance base, so there is\n  // nothing left to filter out here beyond the destination rule itself.\n  Object.keys(mv.allowedRaw).forEach(m=>{ const cbs=cbsOf(m);\n    if(rule==='dominant'&&cbs<minCbs) return; add(m); });\n  if(rule==='dominant'&&!Object.keys(w).length){ Object.keys(mv.allowedRaw).forEach(add); relaxed=true; }\n  return {dest:Object.keys(w).sort((a,b)=>w[b]-w[a]).slice(0,6),relaxed};\n}\nfunction readOptForm(){\n  const r=root.querySelector('input[name=\"destRule\"]:checked');\n  let n=Math.round(+$('#optMinCbs').value);\n  if(!(n>0)) n=DEFAULT_SETTINGS.destMinCbs;\n  return {destRule:(r&&r.value)||DEFAULT_SETTINGS.destRule, destMinCbs:n,\n          targetSpan:numOrNull($('#optTargetSpan').value),\n          maxSpan:numOrNull($('#optMaxSpan').value),\n          minSpan:numOrNull($('#optMinSpan').value)};\n}\nfunction spanFormError(f){\n  if(f.minSpan&&f.maxSpan&&f.minSpan>f.maxSpan) return `Span mínimo (${fmt(f.minSpan)}) não pode ser maior que o máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.maxSpan&&f.targetSpan>f.maxSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode passar do máximo (${fmt(f.maxSpan)}).`;\n  if(f.targetSpan&&f.minSpan&&f.targetSpan<f.minSpan) return `Span alvo (${fmt(f.targetSpan)}) não pode ficar abaixo do mínimo (${fmt(f.minSpan)}).`;\n  return '';\n}\nfunction renderOptPreview(){\n  const el=$('#optPreview'); if(!el) return;\n  const f=readOptForm();\n  root.querySelectorAll('.opt-row').forEach(r=>{\n    if(r.dataset.r!=null) r.classList.toggle('on',r.dataset.r===f.destRule);\n  });\n  const wrap=$('#optMinCbsWrap'); if(wrap) wrap.classList.toggle('off',f.destRule!=='dominant');\n  const err=spanFormError(f), ee=$('#optSpanErr');\n  if(ee){ ee.textContent=err; ee.classList.toggle('hidden',!err); }\n  const ap=$('#optApply'); if(ap) ap.disabled=!!err;\n  const mv=SESS.timeline[SESS.selected];\n  // Prévia inteira é sobre conceitos de setor (ponte por município, span alvo/máx/mín) que não\n  // existem em GV/mercado — mostrar aqui só confundiria com números que não valem nada.\n  if(mv && (mv.level||'setor')!=='setor'){ el.innerHTML=''; return; }\n  if(!mv||!mv.munis){ el.innerHTML='Nenhuma etapa calculada ainda — as regras passam a valer no próximo fechamento.'; return; }\n  const cbsOf=m=>(mv.cbsByMuni&&mv.cbsByMuni[m]!=null)?mv.cbsByMuni[m]:((mv.munis[m]&&mv.munis[m].cbs)||0);\n  const ms=Object.keys(mv.geoFeat||{});\n  const ig=f.destRule==='dominant'?ms.filter(m=>cbsOf(m)<f.destMinCbs):[];\n  const pv=previewDest(mv,f.destRule,f.destMinCbs);\n  let h=`<b style=\"color:var(--ink)\">Prévia — etapa ${SESS.selected+1}, setor ${mv.sector}</b><br>`;\n  const exN=(mv.excludedMunis||[]).length;\n  if(exN) h+=`Base de dominantes: <b>${fmt(ms.length)}</b> município(s) no mapa · <span style=\"color:var(--closing)\">${fmt(exN)} fora da base — ${fmt(mv.excludedCbs||0)} CBs seguem suas líderes</span><br>`;\n  h+=`${fmt(ms.length-ig.length)} de ${fmt(ms.length)} município(s) valem como ponte`;\n  if(ig.length) h+=` · <span style=\"color:var(--closing)\">ignorados:</span> ${ig.slice(0,8).join(', ')}${ig.length>8?' +'+(ig.length-8):''}`;\n  if(pv){\n    h+=`<br>Setores destino com esta regra: <b style=\"color:var(--ink)\">${pv.dest.join(', ')||'–'}</b>`;\n    const cur=(mv.dest||[]).join(', ');\n    if(pv.dest.join(',')!==(mv.dest||[]).join(',')) h+=` <span style=\"color:var(--closing)\">(hoje: ${cur||'–'})</span>`;\n    if(pv.relaxed) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — a regra relaxa pra \"qualquer município\" pra não deixar a etapa sem destino.</b>`;\n  }\n  if(f.targetSpan||f.maxSpan||f.minSpan){\n    const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,{target:f.targetSpan,max:f.maxSpan,min:f.minSpan});\n    h+=`<br><br>Span dos destinos como está hoje: ${mv.dest.map(s=>s+' <b>'+fmt(sp[s])+'</b>').join(' · ')||'–'}`;\n    if(v.over.length||v.under.length) h+=`<br><span style=\"color:var(--closing)\">Fora dos limites: ${[...v.over.map(s=>s+' ▲ acima do máx'),...v.under.map(s=>s+' ▼ abaixo do mín')].join(', ')}</span> — o modelo ÓTIMO tenta corrigir no recálculo.`;\n    else h+=` <span style=\"color:var(--ok)\">— já dentro dos limites</span>`;\n  }\n  if(SESS.timeline.length) h+=`<br><span style=\"margin-top:4px;display:inline-block\">Aplicar <b>recalcula as ${fmt(SESS.timeline.length)} etapa(s)</b> contra o snapshot congelado — os destinos ajustados à mão serão refeitos pelo modelo ÓTIMO.</span>`;\n  el.innerHTML=h;\n}\nfunction openOptions(){\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  root.querySelectorAll('input[name=\"destRule\"]').forEach(r=>{ r.checked=(r.value===st.destRule); });\n  $('#optMinCbs').value=st.destMinCbs;\n  $('#optTargetSpan').value=st.targetSpan||'';\n  $('#optMaxSpan').value=st.maxSpan||'';\n  $('#optMinSpan').value=st.minSpan||'';\n  $('#optApply').disabled=false;\n  $('#optSessName').textContent=SESS.sessionLabel||'sessão não salva';\n  $('#optStatus').textContent='';\n  $('#optionsBg').classList.remove('hidden');\n  renderOptPreview();\n  const c=$('#optCancel'); if(c) c.focus();\n}\nfunction closeOptions(){ const b=$('#optionsBg'); if(b) b.classList.add('hidden'); }\nasync function applyOptions(){\n  const f=readOptForm();\n  if(spanFormError(f)) return;\n  const prev={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const changed = prev.destRule!==f.destRule\n    || (f.destRule==='dominant' && prev.destMinCbs!==f.destMinCbs)\n    || numOrNull(prev.targetSpan)!==f.targetSpan\n    || numOrNull(prev.maxSpan)!==f.maxSpan\n    || numOrNull(prev.minSpan)!==f.minSpan;\n  SESS.settings={...prev,...f};\n  closeOptions();\n  if(!changed || !SESS.timeline.length){ renderAll(); return; }\n  SESS.dirtyFrom=null;\n  SESS.sectorBaseCache={};   // the recompute re-derives every baseline; keeping the old ones would\n                             // leave \"Impacto acumulado\" comparing against pre-change numbers\n  loader(true,'aplicando nova regra de destino — recalculando timeline…');\n  try{\n    await recomputeFrom(0);\n    $('#ds').textContent=''; $('#ds').classList.remove('err');\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?0:SESS.dirtyFrom)+1;\n    $('#ds').textContent=`erro ao aplicar a regra na etapa ${at}: ${e.message} — etapa ${at} em diante ficou desatualizada (⟳)`;\n    $('#ds').classList.add('err');\n  }finally{ loader(false); renderAll(); }\n}\n\nconst SHARED_DS='timeline-shared-sessionsDataSet';\nconst AUTH_REF='timelineSimAuthFinal';   // single source of truth for the auth strategy reference\nlet SHARE={busy:false,url:null,last:null};\n// Serialise a PRESENTATION model, not the raw session, so the reader stays purely presentational\n// and never re-derives spans/desloc/orphans — no second copy of this engine to keep in sync.\n// NOTE: METRICS_DEF[2] is the money marker; read it as !!md[2] (a literal dollar sign in this\n// position trips the upsert's parse check).\nfunction buildShareStages(){\n  return SESS.timeline.map((mv,i)=>{\n    const c=computeDerived(mv);\n    const incBy={};\n    mv.dest.forEach(s=>{ incBy[s]=Object.fromEntries(METRICS_DEF.map(md=>[md[0],0])); });\n    Object.entries(mv.munis).forEach(([m,o])=>{\n      if(m==='__OUTROS__') return;\n      const s=mv.assign[m]; if(!s||!incBy[s]) return;\n      METRICS_DEF.forEach(md=>{ incBy[s][md[0]]+=o[md[0]]||0; });\n    });\n    const saiTot={};\n    METRICS_DEF.forEach(md=>{\n      const k=md[0];\n      saiTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,p)=>a+((p[1][k])||0),0);\n    });\n    // orphans follow their leader's home: they enter that sector AND leave the closing one\n    c.leaders.forEach(L=>{\n      const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n      if(!om||!L.home) return;\n      METRICS_DEF.forEach(md=>{ const k=md[0];\n        if(incBy[L.home]) incBy[L.home][k]+=om[k]||0;\n        saiTot[k]+=om[k]||0; });\n    });\n    const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n    const sv=mv.dest.map(s=>c.span[s]);\n    const baseOf=(s,k)=>{ const b=mv.baseMetrics[s]||{}; return +(b[k]||0); };\n    return {\n      i:i, sector:mv.sector, level:mv.level||'setor', unitOfMuni:mv.unitOfMuni||{}, policy:mv.policy, domUF:mv.domUF,\n      optTier:(mv.optTier==null?0:mv.optTier),\n      destRuleFallback:!!mv.destRuleFallback,\n      dest:mv.dest.slice(), color:Object.assign({},mv.color), assign:Object.assign({},mv.assign),\n      kpis:{ cbsRealocadas:closingCbsOf(mv), desloc:c.desloc,\n             spread:(sv.length?Math.max.apply(null,sv)-Math.min.apply(null,sv):0),\n             municipios:ms.length, lideres:mv.leaders.length },\n      destinos:mv.dest.map(s=>({ s:s, base:baseOf(s,'cbs'), inc:c.inc[s]||0, orph:c.orph[s]||0, span:c.span[s]||0, gained:c.gained[s]||0 })),\n      municipios:ms.map(m=>(mv.level&&mv.level!=='setor')?{ nome:m, uf:'', cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }:{ nome:mName(m), uf:mUf(m), cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }),\n      excluidos:(mv.excludedMunis||[]).map(x=>({ muni:x.muni, cbs:x.cbs })),\n      orphanTotal:c.orphanTotal||0, orphanStranded:c.orphanStranded||0, noShapeCbs:mv.noShapeCbs||0,\n      lideres:c.leaders.map(L=>({ lider:(L.lider||'').split(' ').slice(0,2).join(' '), grupo:L.grupo,\n        total:L.total, home:L.home, desloc:L.desloc, orphan:L.orphan||0 })),\n      metricas:METRICS_DEF.map(md=>{ const k=md[0];\n        return { key:k, label:md[1], money:!!md[2],\n          rows:mv.dest.map(s=>({ s:s, antes:baseOf(s,k), depois:baseOf(s,k)+((incBy[s]&&incBy[s][k])||0) })),\n          sai:saiTot[k]||0 }; }),\n      geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat, excludedFeat:[]\n    };\n  });\n}\nfunction buildShareSummary(stages){\n  const world=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const receivers=[];\n  SESS.timeline.forEach((mv,i)=>{\n    const own=mv.pureOwnMetrics||{};\n    receivers.push({ s:mv.sector, situacao:'fechado (etapa '+(i+1)+')', antes:+(own.cbs||0), depois:0 });\n  });\n  const seen={};\n  Object.values(world.muniOverride).forEach(s=>{\n    if(seen[s]) return; seen[s]=1;\n    const base=SESS.sectorBaseCache[s]||{}, infl=world.sectorInflow[s]||{};\n    receivers.push({ s:s, situacao:'recebeu território', antes:+(base.cbs||0), depois:(+(base.cbs||0))+(infl.cbs||0) });\n  });\n  return { stages:stages.length, sectors:SESS.timeline.map(mv=>mv.sector),\n    totalCbs:stages.reduce((a,s)=>a+(s.kpis.cbsRealocadas||0),0),\n    totalDesloc:stages.reduce((a,s)=>a+(s.kpis.desloc||0),0), receivers:receivers };\n}\nfunction renderShareBody(){\n  const el=$('#shareBody'); if(!el) return;\n  const go=$('#shareGo'), cp=$('#shareCopy'), op=$('#shareOpen'), cc=$('#shareCancel');\n  if(SHARE.url){\n    el.innerHTML=`<div class=\"opt-note\" style=\"margin-top:0\"><b style=\"color:var(--ink)\">Sessão compartilhada ✓</b><br>\n      Link de leitura desta simulação congelada. Quem abrir precisa ter login no simulador.\n      <div class=\"shareurl\">${esc(SHARE.url)}</div></div>`;\n    if(go) go.classList.add('hidden');\n    if(cp) cp.classList.remove('hidden');\n    if(op) op.classList.remove('hidden');\n    if(cc) cc.textContent='Fechar';\n    return;\n  }\n  if(go) go.classList.remove('hidden');\n  if(cp) cp.classList.add('hidden');\n  if(op) op.classList.add('hidden');\n  if(cc) cc.textContent='Cancelar';\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const desloc=SESS.timeline.reduce((a,mv)=>a+(mv.munis?computeDerived(mv).desloc:0),0);\n  const destTxt=st.destRule==='dominant'?('dominantes ≥'+fmt(st.destMinCbs)+' CBs'):'qualquer município';\n  el.innerHTML=`<div class=\"opt-lbl\">Confirmar compartilhamento</div>\n    <div class=\"opt-sub\">Publica uma <b>cópia congelada</b> desta sessão, exatamente no estado atual.</div>\n    <div class=\"opt-note\" style=\"margin-top:0\">\n      · <b>${fmt(SESS.timeline.length)}</b> etapa(s) — ${esc(SESS.timeline.map(mv=>mv.sector).join(' → '))}<br>\n      · snapshot atualizado em <b>${esc(fmtSyncedAt(SESS.snapshotSyncedAt))}</b><br>\n      · base: <b>municípios dominantes (CSV)</b> · destino: <b>${esc(destTxt)}</b> · span: <b>${esc(spanShort(st))}</b><br>\n      · <b>${fmt(desloc)}</b> CBs deslocadas somando a cadeia\n    </div>\n    <div class=\"opt-note\">A cópia <b>não</b> acompanha edições posteriores desta sessão — pra atualizar, compartilhe de novo e use o link novo. O link exige login no simulador; não é público.</div>\n    ${SHARE.last?`<div class=\"opt-note\"><b style=\"color:var(--ink)\">Último link gerado nesta sessão</b> — aponta pro estado de <b>antes</b> das edições feitas depois dele.<div class=\"shareurl\">${esc(SHARE.last)}</div></div>`:''}`;\n}\nasync function openShare(){\n  if(!SESS.timeline.length){ $('#ds').textContent='nada pra compartilhar — feche pelo menos um setor primeiro.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  SHARE.url=null;\n  const s=$('#shareStatus'); if(s){ s.textContent=''; s.classList.remove('err'); }\n  renderShareBody();\n  $('#shareBg').classList.remove('hidden');\n  const b=$('#shareCancel'); if(b) b.focus();\n}\nfunction closeShare(){ const b=$('#shareBg'); if(b) b.classList.add('hidden'); }\nasync function doShare(){\n  if(SHARE.busy) return;\n  const go=$('#shareGo'), stx=$('#shareStatus');\n  SHARE.busy=true;\n  if(go){ go.disabled=true; go.textContent='Compartilhando…'; }\n  if(stx){ stx.textContent=''; stx.classList.remove('err'); }\n  try{\n    // Resolved as a GLOBAL PROCESS, not a declared param — which only works because the block\n    // instance on the `main` artboard binds `sharedDataset: @timeline-shared-sessionsDataSet`.\n    // That binding is load-bearing: a Process is only define()d once something on the rendering\n    // tree references it, so without it this lookup returns null and sharing dies on the guard\n    // below. Do NOT remove the instance param because this file never reads d2.params.sharedDataset.\n    const ds=d2.data(SHARED_DS);\n    if(!ds) throw new Error('dataset de sessões compartilhadas indisponível');\n    await flushDirty();\n    const stages=buildShareStages();\n    const summary=buildShareSummary(stages);\n    let who='';\n    try{ const a=d2.auth(AUTH_REF); who=(a&&a.user&&(a.user.email||a.user.nome))||''; }catch(e){}\n    const created=await ds.create({\n      label:SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR')),\n      snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt,\n      settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n      stages:stages, summary:summary, sharedBy:who, sharedAt:new Date().toISOString()\n    });\n    const id=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    if(!id) throw new Error('registro gravado, mas sem _id na resposta — não foi possível montar o link');\n    SHARE.url=window.location.origin+'/sessions/'+id;\n    SHARE.last=SHARE.url;\n    renderShareBody();\n  }catch(e){\n    if(stx){ stx.textContent='erro ao compartilhar: '+e.message; stx.classList.add('err'); }\n  }finally{\n    SHARE.busy=false;\n    if(go){ go.disabled=false; go.textContent='Compartilhar'; }\n  }\n}\n\n// ---------- rendering ----------\nfunction renderTimeline(){\n  const el=$('#movementChips'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<span class=\"empty\">Nenhuma etapa ainda — use \"Adicionar movimentação\" à direita.</span>'; return; }\n  el.innerHTML=SESS.timeline.map((mv,i)=>{\n    const ready=mv.dest!==undefined;\n    const c=ready?computeDerived(mv):null;\n    const stale=SESS.dirtyFrom!=null && i>=SESS.dirtyFrom;\n    return `<div class=\"tchip ${i===SESS.selected?'on':''} ${stale?'stale':''}\" data-i=\"${i}\">\n      <span class=\"tnum\">${i+1}</span><span class=\"tsec\">${LEVELS[mv.level||'setor'].label} ${mv.sector}</span>\n      <span class=\"tstat\">${ready?((mv.level||'setor')==='setor'?fmt(c.desloc)+' desloc.':fmt(closingCbsOf(mv))+' CBs'):'…'}</span>\n      ${stale?'<span class=\"tstale\" title=\"recalcula ao selecionar outra etapa\">⟳</span>':''}\n      <span class=\"trm\" data-rm=\"${i}\" title=\"remover esta etapa e as seguintes\">✕</span>\n    </div>`;\n  }).join('');\n  el.querySelectorAll('.tchip').forEach(ch=>{\n    ch.addEventListener('click',async (e)=>{\n      if(e.target.dataset.rm!=null && e.target.dataset.rm!==''){ removeFrom(+e.target.dataset.rm); return; }\n      const i=+ch.dataset.i;\n      if(i!==SESS.selected) await flushDirty();\n      SESS.selected=i; renderAll();\n    });\n  });\n}\nfunction renderRuleNote(){\n  const el=$('#ruleNote'); if(!el) return;\n  const st={...DEFAULT_SETTINGS,...(SESS.settings||{})};\n  const mv=SESS.timeline[SESS.selected];\n  const lvl=LEVELS[(mv&&mv.level)||'setor'];\n  let h='';\n  if(!mv || (mv.level||'setor')==='setor'){\n    h+=`<b style=\"color:var(--ink)\">Base de municípios dominantes:</b> só os municípios que a base (CSV) atribui ao setor entram no mapa e na alocação`;\n    if(mv&&mv.domCsvMunis) h+=` · <b>${fmt(mv.domCsvMunis)}</b> na base do setor ${mv.sector}`;\n    if(mv&&mv.excludedCbs) h+=` · <b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs)} CBs</b> fora da base nesta etapa — não aparecem no mapa e as CBs seguem suas líderes (quebra de vínculo)`;\n  } else {\n    // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base), então\n    // não existe \"excluído\" pra reportar aqui — só o tamanho da base e em quantas unidades ela se agrupa.\n    h+=`<b style=\"color:var(--ink)\">Base de ${lc(lvl.childLabel)}s dominantes:</b> ${mv.domCsvMunis!=null?`<b>${fmt(mv.domCsvMunis)}</b> município(s) `:''}na base ${lvl.de} ${lc(lvl.label)} ${mv.sector}, agrupados em <b>${fmt(Object.keys(mv.unitMunis||{}).length)}</b> ${lc(lvl.childLabel)}(s)`;\n  }\n  if(DOM.error) h+=` · <b style=\"color:var(--closing)\">⚠ base indisponível: ${esc(DOM.error)}</b>`;\n  h+=`<br>`;\n  h+=`<b style=\"color:var(--ink)\">Regra de destino:</b> `;\n  h+= st.destRule==='dominant'\n    ? `só municípios com <b>≥${fmt(st.destMinCbs)} CBs</b> trazem ${lc(lvl.destLabel)} pra lista de destinos`\n    : `qualquer município ${lvl.de} ${lc(lvl.label)} fechad${lvl.a==='à'?'a':'o'} vale como ponte <span style=\"opacity:.7\">(padrão)</span>`;\n  if(mv&&st.destRule==='dominant'){\n    // A ponte é sempre por MUNICÍPIO, em qualquer nível — mv.ignoredBridges já é a lista definitiva\n    // que computeMovement calculou, então lê-la aqui em vez de recalcular contra mv.munis, que em\n    // GV/mercado está no grão de setor/GV e daria o número errado.\n    const ig=mv.ignoredBridges||[];\n    if(ig.length) h+=` · <b style=\"color:var(--closing)\">${fmt(ig.length)}</b> ignorado(s) como ponte (ainda redistribuído(s)): ${ig.slice(0,5).join(', ')}${ig.length>5?' +'+(ig.length-5):''}`;\n    if(mv.destRuleFallback) h+=`<br><b style=\"color:var(--closing)\">⚠ nenhum município dominante nesta etapa — regra relaxada pra \"qualquer município\".</b>`;\n  }\n  el.innerHTML=h;\n}\nfunction renderSpanRule(){\n  const el=$('#spanRule'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected], lim=spanLimits(mv);\n  if(!mv||!mv.munis||mv.view==='asis'||!spanLimitsOn(lim)){ el.innerHTML=''; return; }\n  const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,lim);\n  const bits=[];\n  if(lim.target) bits.push(`alvo <b>${fmt(lim.target)}</b>`);\n  if(lim.max) bits.push(`máx <b>${fmt(lim.max)}</b>`);\n  if(lim.min) bits.push(`mín <b>${fmt(lim.min)}</b>`);\n  let h=`Pré-requisito de span: ${bits.join(' · ')} CBs — `;\n  if(v.over.length||v.under.length){\n    h+=`<b style=\"color:var(--closing)\">⚠ `;\n    if(v.over.length) h+=`acima do máximo: ${v.over.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    if(v.over.length&&v.under.length) h+=' · ';\n    if(v.under.length) h+=`abaixo do mínimo: ${v.under.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    h+=`</b>`;\n    if(mv.optTier===1) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita o span mínimo nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===2) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita os limites nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===-1) h+=`<br><span style=\"opacity:.85\">Etapa resolvida por heurística — os limites não são garantidos.</span>`;\n  } else h+=`<b style=\"color:var(--ok)\">✓ todos os setores receptores dentro dos limites</b>`;\n  el.innerHTML=`<div class=\"note\" style=\"margin:-2px 0 6px\">${h}</div>`;\n}\nfunction renderContig(){const mv=SESS.timeline[SESS.selected];const el=$('#contig');if(!el||!mv)return;\n  if(mv.view==='asis'){el.innerHTML='';return;}\n  const lvl=LEVELS[mv.level||'setor'];\n  // Indicador de conectividade geográfica só se aplica a fechamento de setor — ver contigBadFor.\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=''; return; }\n  const bad=contigBadFor(mv,mv.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Território desconectado em ${bad.join(', ')} — ${lc(lvl.childLabel)}(s) desconectado(s) do território ${lvl.de} ${lc(lvl.label)}. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Sem quebras — cada ${lc(lvl.label)} forma uma região conectada.</div>`;\n}\nfunction closingCbsOf(mv){ return Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0); }\nfunction sectorTotalCbs(mv){ return Object.values(mv.munis).reduce((a,o)=>a+o.cbs,0); }\nfunction renderKpis(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  const childPlural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  if(mv.view==='asis'){const mc=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(sectorTotalCbs(mv)),l:`CBs ${lvl.no} ${lc(lvl.label)} ${mv.sector} (antes desta etapa)`,t:`Total de CBs ${lvl.no} ${lc(lvl.label)} hoje — já considerando o que etapas anteriores da timeline mandaram pra cá.`},\n      {n:mv.csvMunisTotal?`${fmt(mc)}/${fmt(mv.csvMunisTotal)}`:fmt(mc),l:`${childPlural} ${lvl.de} ${lc(lvl.label)}`,t:`${lvl.childLabel}(s) desenhados nesta etapa${mv.csvMunisTotal?` de ${fmt(mv.csvMunisTotal)} que a base de dominantes atribui ${lvl.a} ${lc(lvl.label)}`:''}.${(mv.missingMunis||[]).length?' Fora do mapa: '+mv.missingMunis.map(x=>x.muni+' ('+x.reason+')').join(' · '):''}${Object.keys(mv.ufGuessed||{}).length?' UF inferida da UF dominante do setor (nome repetido em outra UF carregada): '+Object.entries(mv.ufGuessed).map(([nm,u])=>nm+'→'+u).join(', '):''}`},\n      {n:fmt(mv.leaders.length),l:'Líderes (grupos)',t:`Líderes presentes ${lvl.no} ${lc(lvl.label)} nesta etapa.`},\n      {n:fmt(mv.dest.length),l:`${lvl.destLabel}s adjacentes`,t:`${lvl.destLabel}s vizinh${lvl.destLabel==='GV'?'as':'os'} (ainda abertas)${lvl.parentPhrase?` ${lvl.parentPhrase}`:''}${mv.gv?` (${mv.gv})`:''} que poderiam receber os ${lc(lvl.childLabel)}s.${(mv.gvRejected||[]).length&&lvl.parentWord?' Fora do mapa, por serem de outr'+(lvl.parentWord==='gerência'?'a':'o')+' '+lvl.parentWord+': '+mv.gvRejected.join(', ')+'.':''}`},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=mv.dest.map(s=>c.span[s]);const spread=sv.length?Math.max(...sv)-Math.min(...sv):0;\n  const zero=mv.dest.filter(s=>c.gained[s]===0);const totCbs=closingCbsOf(mv);\n  $('#kpis').innerHTML=[\n    // \"CBs fora do X\" só faz sentido pra fechamento de setor — a mecânica de vínculo (CBs que\n    // seguem a líder pra fora do território) é uma coisa de setor/orphan, não de GV/mercado.\n    (mv.level||'setor')==='setor'?{n:fmt(c.desloc),l:mv.policy==='fora'?`CBs fora ${lvl.de} ${lc(lvl.destLabel)}`:'CBs c/ quebra de relação',cls:'warn',t:`CBs cuja líder acabou noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} nesta etapa.`}:null,\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:`Diferença de tamanho entre ${lvl.destLabel==='GV'?'a maior e a menor':'o maior e o menor'} ${lc(lvl.destLabel)} depois desta etapa.`},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs ${lvl.de} ${lc(lvl.label)} ${mv.sector} sendo redistribuídas nesta etapa.`},\n    (mv.level||'setor')==='setor'?{n:zero.length?zero.join(', '):'nenhum',l:`${lvl.destLabel} sem líder herdada`,cls:zero.length?'warn':'ok',t:`${lvl.destLabel} que recebe ${lc(lvl.childLabel)}(s) mas nenhuma líder nesta etapa.`}:null,\n  ].filter(Boolean).concat(mv.excludedCbs?[{n:fmt(mv.excludedCbs),l:`CBs fora do setor · ${fmt(mv.excludedMunis.length)} município(s) fora da base`,cls:'warn',t:'CBs em municípios que a base de dominantes não atribui a este setor: ficam fora do mapa e da alocação geográfica e seguem a líder do seu grupo.'}]:[])\n   .concat((mv.missingMunis||[]).length?[{n:fmt(mv.missingMunis.length),l:`Município(s) da base fora do mapa · ${fmt(mv.csvMunisTotal)} na base`,cls:'warn',t:`A base de dominantes atribui ${fmt(mv.csvMunisTotal)} município(s) ao setor ${mv.sector} e ${fmt(mv.csvMunisTotal-mv.missingMunis.length)} chegaram ao mapa. Fora: ${mv.missingMunis.map(x=>x.muni+' — '+x.reason+(x.cbs?' ('+fmt(x.cbs)+' CBs)':'')).join(' · ')}`}]:[])\n   .map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  if(mv.view==='asis'){const items=[{s:mv.sector,val:sectorTotalCbs(mv),color:CLOSE_COLOR}].concat(mv.dest.map(s=>({s,val:+(mv.baseMetrics[s]?.cbs||0),color:mv.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura antes desta etapa — ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const lim=spanLimits(mv),on=spanLimitsOn(lim);\n  const rawMax=Math.max(...mv.dest.map(s=>c.span[s]),lim.target||0,lim.max||0,1);\n  const max=on?rawMax*1.04:rawMax;   // headroom so a marker sitting at the scale top stays visible\n  const marks='';\n  $('#spans').innerHTML=mv.dest.map(s=>{const base=+(mv.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    const over=lim.max&&c.span[s]>lim.max,under=lim.min&&c.span[s]<lim.min;\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${mv.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${mv.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${mv.color[s]}\"></div>${marks}</div>\n      <div class=\"bar-val\" style=\"${over?'color:var(--closing);font-weight:700':(under?'color:#ff9500;font-weight:700':'')}\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span>${over?' ▲':''}${under?' ▼':''}</div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base (já incluindo etapas anteriores) · cheio = CBs entrantes ${lvl.de} ${mv.sector} nesta etapa.${c.orphanTotal?` Inclui <b>${fmt(c.orphanTotal-c.orphanStranded)}</b> CB(s) de municípios fora da base de dominantes que seguiram suas líderes.`:''}</div>`;\n}\n// Two very different reasons land rows in the __OUTROS__ bucket — no IBGE shape, or outside the\n// sector's dominance base. Same bucket computationally, but they must not read as the same thing.\nfunction outsideNote(mv){\n  const bits=[];\n  if(mv.noShapeCbs) bits.push(`<b>${fmt(mv.noShapeCbs)}</b> CBs em ${fmt(mv.noShapeMunis.length)} município(s) sem shape na UF`);\n  if(mv.excludedCbs) bits.push(`<b>${fmt(mv.excludedCbs)}</b> CBs em ${fmt(mv.excludedMunis.length)} município(s) <b>fora da base de dominantes</b> deste setor — <b style=\"color:var(--closing)\">quebra de vínculo</b>`);\n  if(!bits.length) return '';\n  let h=`<div class=\"note\">Fora do mapa e da alocação geográfica: ${bits.join(' · ')}.`;\n  if(mv.excludedCbs){\n    const c=computeDerived(mv);\n    h+=` As <b>${fmt(mv.excludedCbs)}</b> CBs em quebra de vínculo <b>seguem a líder do seu grupo</b>`;\n    const byS=mv.dest.filter(s=>c.orph[s]).map(s=>`<b style=\"color:${mv.color[s]}\">${s}</b> (${fmt(c.orph[s])})`);\n    h+=byS.length?`: ${byS.join(' · ')}.`:'.';\n    if(c.orphanStranded) h+=` <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} CB(s) sem destino</b> — a líder não ficou com nenhum município realocado, então não há setor pra seguir.`;\n  }\n  if(mv.noShapeCbs) h+=` As CBs sem shape na UF continuam fora de qualquer setor destino.`;\n  if(mv.excludedMunis&&mv.excludedMunis.length) h+=`<br><span style=\"opacity:.85\">${mv.excludedMunis.slice(0,8).map(x=>x.muni+' ('+fmt(x.cbs)+')').join(', ')}${mv.excludedMunis.length>8?' +'+(mv.excludedMunis.length-8):''}</span>`;\n  return h+`</div>`;\n}\nfunction renderMuni(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const lvl=LEVELS[mv.level||'setor'];\n  const plural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  const fem=lvl.childLabel==='GV';\n  const t=$('#muniListTitle'); if(t) t.textContent=`${plural} → ${lc(lvl.destLabel)} destino`;\n  const nt=$('#muniListNote'); if(nt) nt.innerHTML=`Clique no <b>código ${lvl.a} ${lc(lvl.destLabel)}</b> pra mandar ${lvl.a} ${lc(lvl.childLabel)} <b>inteir${fem?'a':'o'}</b> pra lá (regra de negócio). A cor acompanha o mapa.`;\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  if(mv.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">${lc(lvl.label)} ${mv.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje tod${fem?'as':'os'} pertencem ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`+outsideNote(mv);return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${mv.dest.map(s=>`<button data-s=\"${s}\" class=\"${mv.assign[m]===s?'on':''}\" style=\"${mv.assign[m]===s?'background:'+mv.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +outsideNote(mv);\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{const m=b.parentElement.dataset.m,s=b.dataset.s;editSelected(m2=>{m2.assign[m]=s;});});\n}\nfunction renderLeaders(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>${LEVELS[mv.level||'setor'].label}</th></tr></thead><tbody>`\n    +mv.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${mv.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(mv.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  const showOrph=c.orphanTotal>0;\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${mv.dest.map(s=>`<th>${s}</th>`).join('')}${showOrph?'<th title=\"CBs em municípios fora da base de dominantes que seguiram esta líder\">Fora do setor</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td>${L.home?`<span class=\"home\" style=\"background:${mv.color[L.home]}\"></span>${L.home}`:'–'}</td>\n     ${mv.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+mv.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     ${showOrph?`<td class=\"cell\" style=\"${L.orphan&&!L.orphanHome?'color:var(--closing);font-weight:700':''}\" title=\"${L.orphan?(L.orphanHome?'seguem a líder para o setor '+L.orphanHome:'sem home — CBs sem setor pra seguir'):''}\">${L.orphan?fmt(L.orphan)+(L.orphanHome?'':' ⚠'):'·'}</td>`:''}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${mv.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}${showOrph?`<td>${fmt(c.orphanTotal)}</td>`:''}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  // Esta matriz cruza LINHAS DO CRM (município x grupo) — no fechamento de GV/mercado a unidade\n  // que troca de dono é setor/GV, não município, e reconstruir a matriz nesse grão é trabalho ainda\n  // não feito; mostrar a tabela como se fosse por município aqui daria linhas vazias sem avisar.\n  if((mv.level||'setor')!=='setor'){ $('#matrix').innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; return; }\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // `_out` is a flag stamped on the ROW, and rows are shared between etapas (absorbed + inherited\n  // orphans), so the last etapa to aggregate wins and reading it here can describe the wrong etapa.\n  // Derive the bucket from THIS mv instead. Summing rather than .find() because several out-municípios\n  // of the same group all collapse into __OUTROS__, and only the first was being counted.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cell=(m,g)=>mv.matrix.reduce((a,x)=>a+((((outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))===m)&&String(x.grupo)===g)?+x.cbs:0),0);\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  // No destination column here. This is the AS-IS matrix: every município in it still belongs to the\n  // closing sector, so showing where it is HEADED mixed the two states of the world in one table and\n  // made the ASIS view read like a plan. The destination lives on the map and in \"Municípios → setor\n  // destino\", which are about the to-be.\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\n// TOBE counterpart of the AS-IS matrix: the same município × grupo grid plus the two columns the AS-IS\n// view deliberately refuses to carry — where the município ends up, and under which líder.\n// \"Líder resultante\" is the líder holding the most CBs in that município. A município is served by several\n// grupos at once and the data names no single owner, so the dominant one is the honest answer and its\n// share is printed beside it instead of being hidden behind a single name.\nfunction renderMatrixTobe(){const mv=SESS.timeline[SESS.selected]; const el=$('#matrixTobe'); const nt=$('#tobeNote');\n  if(!el) return;\n  if(!mv||!mv.munis){ el.innerHTML=''; if(nt) nt.innerHTML=''; return; }\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; if(nt) nt.innerHTML=''; return; }\n  const c=computeDerived(mv);\n  const homeOf={}; c.leaders.forEach(L=>{ homeOf[L.grupo]=L.home; });\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // One pass over the matrix instead of a scan per cell: the AS-IS table can afford the naive version\n  // on six municípios, but this one asks for the dominant líder too, which multiplies the lookups.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cellBy={}; mv.matrix.forEach(x=>{ const k=(outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))+'|'+String(x.grupo);\n    cellBy[k]=(cellBy[k]||0)+ +x.cbs; });\n  const cell=(m,g)=>cellBy[m+'|'+g]||0;\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  const domLeader=m=>{ let best=null,bc=0;\n    mv.leaders.forEach(L=>{ const v=cell(m,L.grupo); if(v>bc){ bc=v; best=L; } });\n    return best?{L:best,cbs:bc}:null; };\n  const short=L=>(L.lider||'').split(' ').slice(0,2).join(' ')||('grupo '+L.grupo);\n  // NOT named `rows` — that is the module-level Databricks response helper, and shadowing it inside a\n  // render function is a trap for whoever adds a query here later.\n  // Off-mismatch ainda calculado pra alimentar a nota abaixo da tabela — só não vira mais coluna: o\n  // destino agora é \"Setor destino\" por GRUPO no rodapé, não mais por município na grade.\n  const recs=ms.map(m=>{ const d=mv.assign[m]||null, dl=domLeader(m);\n    const home=dl?(homeOf[dl.L.grupo]||null):null;\n    return {m:m,off:!!(dl&&home&&d&&home!==d)}; });\n  el.innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${esc(L.lider||'')}\">${L.grupo}<br><span style=\"font-weight:400;font-size:10px;color:var(--mut)\">${esc(short(L))}</span></th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')\n   // Um rótulo de destino por GRUPO (coluna), não por município (linha) — cada grupo tem UM home\n   // fixo (computeDerived já calculou), então ele mora uma vez no rodapé, igual o nome já morava.\n   +`<tr class=\"lider-foot\"><td>Setor destino</td>`\n     +mv.leaders.map(L=>{const h=homeOf[L.grupo]; return `<td>${h?`<span class=\"home\" style=\"background:${mv.color[h]}\"></span>${h}`:'–'}</td>`;}).join('')\n     +`<td></td></tr>`\n   +`</tbody>`;\n  if(nt){\n    const offN=recs.filter(r=>r.off).length;\n    nt.innerHTML=(mv.policy!=='fora')\n      ? `Política <b>quebrar relação</b>: as CBs deixam a líder atual e passam à estrutura do setor destino — o rodapé mostra pra onde cada grupo vai.`\n      : `<b>Setor destino</b> = o setor que recebe a maior parte das CBs de cada líder (rodapé, uma vez por grupo). `\n        +(offN?`<span style=\"color:var(--closing)\">${fmt(offN)} município(s) ⚠</span> têm a líder dominante indo para um setor diferente do dela — as CBs seguem a líder (é disso que o KPI “CBs fora do setor” é feito).`\n          :`Em todos os municípios a líder dominante termina no mesmo setor que recebe o território.`);\n  }\n}\nfunction renderPolicy(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  // O toggle fora-do-setor/quebra-de-relação é uma decisão de setor fechando — em GV/mercado a\n  // seção some inteira em vez de mostrar um controle que não corresponde a nada calculado.\n  const sec=$('#policySection'); if(sec) sec.classList.toggle('hidden',(mv.level||'setor')!=='setor');\n  if((mv.level||'setor')!=='setor') return;\n  $('#policyNote').innerHTML=mv.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)}</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder d${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor']; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  // GV/mercado: a chave \"m\" aqui é um código de setor/GV, não uma chave município/UF — mName/mUf\n  // parseariam lixo, então o CSV de plano exporta o código cru nesses níveis.\n  mv._plan=ms.map(m=>(mv.level&&mv.level!=='setor')?{municipio:m,uf:'',cbs:mv.munis[m].cbs,destino:mv.assign[m]}:{municipio:mName(m),uf:mUf(m),cbs:mv.munis[m].cbs,destino:mv.assign[m]});\n  $('#plan').innerHTML=`Etapa ${SESS.selected+1} · ${lvl.label} <b>${mv.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${mv.color[mv.assign[m]]}\">${mv.assign[m]||'–'}</b>`).join(' · ')}<br>\n   Política: <b>${mv.policy==='fora'?`manter vínculo (CBs fora ${lvl.de} ${lc(lvl.destLabel)})`:'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas nesta etapa.`\n   +(c.orphanTotal?`<br>Fora da base de dominantes: ${fmt(c.orphanTotal)} CB(s) seguem suas líderes — ${mv.dest.filter(s=>c.orph[s]).map(s=>`${s} (${fmt(c.orph[s])})`).join(' · ')||'–'}${c.orphanStranded?` · <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} sem líder com destino</b>`:''}. Não entram no CSV, que é por município.`:'');\n}\nfunction renderReport(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  mv.metric=mv.metric||'cbs';\n  const KEYS=METRICS_DEF.map(m=>m[0]);\n  const incBy={};mv.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incBy[s])return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  // orphan CBs land on their leader's home sector, not on a geographic destination\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home&&incBy[L.home]) KEYS.forEach(k=>incBy[L.home][k]+=om[k]||0); });\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  // they leave the closing sector too, so antes → depois still balances\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home) KEYS.forEach(k=>closeTot[k]+=om[k]||0); });\n  const trend=mv.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=mv.dest.map(s=>({label:s,color:mv.color[s],antes:+(mv.baseMetrics[s]?.[mk]||0),depois:+(mv.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:mv.sector+'✕',color:'#ff3b30',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#34c759':'#ff3b30'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois desta etapa</h2>\n      <select id=\"metricSel\">${METRICS_DEF.map(m=>`<option value=\"${m[0]}\" ${m[0]===mv.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes desta etapa</b> (já incluindo etapas anteriores) · barra cheia = <b>depois</b> · <b style=\"color:var(--acc)\">${mv.sector}✕</b> = ${lc(lvl.label)} fechad${lvl.a==='à'?'a':'o'} nesta etapa · % = variação</div>\n    ${chartHTML(mv.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por ${lc(lvl.destLabel)} receptor (snapshot congelado)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${METRICS_DEF.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const b=mv.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      METRICS_DEF.map(([k])=>{const bv=+b[k]||0,av=bv+(inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=METRICS_DEF.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${mv.sector} (sai)</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};mv.dest.forEach(s=>incLvl[s]={});\n  Object.keys(mv.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incLvl[s])return;const lv=mv.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  c.leaders.forEach(L=>{ const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n    if(ol&&L.home&&incLvl[L.home]) Object.entries(ol).forEach(([nv,n])=>incLvl[L.home][nv]=(incLvl[L.home][nv]||0)+n); });\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por ${lc(lvl.destLabel)} receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const base=mv.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> desta etapa · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${mv.sector}.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${mv.sector} — últimos ${trend.length} ciclos até o snapshot congelado</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Tendência histórica <b>do próprio setor</b> — não ajustada pela absorção hipotética de etapas anteriores (limitação conhecida: tendência é por setor, não decomponível por município).</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{mv.metric=sel.value; renderReport();};\n}\nfunction renderCumulative(){\n  const el=$('#tab-cumulative'); if(!el) return;\n  // Esta view soma a cadeia inteira via foldWorldState().muniOverride/sectorBaseCache — hoje só\n  // populados pelo fechamento de setor. GV/mercado têm o rastro equivalente (secOverride/gvOverride),\n  // mas essa agregação ainda não foi refeita nesse grão; melhor avisar do que mostrar tabela vazia.\n  const lockLvl=SESS.timeline.length?(SESS.timeline[0].level||'setor'):'setor';\n  if(lockLvl!=='setor'){ el.innerHTML=`<div class=\"note\">Impacto acumulado da cadeia ainda não disponível pra sessões de ${lc(LEVELS[lockLvl].label)} — use o relatório da etapa (aba anterior) etapa a etapa.</div>`; return; }\n  if(!SESS.timeline.length){ el.innerHTML='<div class=\"note\">Nenhuma etapa na timeline ainda.</div>'; return; }\n  const ready=SESS.timeline.every(mv=>mv.munis);\n  if(!ready){ el.innerHTML='<div class=\"note\">Aguardando etapas terminarem de calcular…</div>'; return; }\n  const finalWorld=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const openSectors=[...new Set(Object.values(finalWorld.muniOverride))];\n  let h=`<h2 style=\"margin:0 0 8px\">Linha do tempo — ${SESS.timeline.length} fechamento(s)</h2>\n    <div class=\"note\" style=\"margin:-2px 0 10px\">Setores fechados, em ordem: ${SESS.timeline.map(mv=>mv.sector).join(' → ')}. Cada linha abaixo soma o efeito de <b>toda a cadeia</b>, não apenas da última etapa.</div>`;\n  h+=`<div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th>${METRICS_DEF.map(([,l])=>`<th>${l}</th>`).join('')}</tr></thead><tbody>`;\n  SESS.timeline.forEach((mv,i)=>{\n    h+=`<tr style=\"opacity:.75\"><td>${mv.sector}</td><td>fechado (etapa ${i+1})</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(mv.pureOwnMetrics?.[k]||0)}</td>`).join('')+`</tr>`;\n  });\n  openSectors.forEach(s=>{\n    const base=SESS.sectorBaseCache[s]||{};\n    const infl=finalWorld.sectorInflow[s]||{};\n    h+=`<tr><td>${s}</td><td>recebeu território</td>`+METRICS_DEF.map(([k,,t])=>{\n      const bv=+base[k]||0, av=bv+(infl[k]||0); const d=bv?((av-bv)/bv*100):0; const f=(t==='$'?money:fmt);\n      return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;\n    }).join('')+`</tr>`;\n  });\n  h+=`</tbody></table></div><div class=\"note\">\"Antes\" de um setor receptor é o snapshot congelado (sem nenhum fechamento aplicado); \"depois\" soma tudo que a cadeia inteira jogou pra ele.</div>`;\n  el.innerHTML=h;\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=700,H=500,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\n// ---------- label placement ----------\n// Pole of inaccessibility: the interior point farthest from ANY boundary of the dissolved area. A\n// centroid is only a mean — on a concave sector, one split across a river, or a member whose vertex\n// average falls in a bay, it lands outside the very territory it is supposed to name, which is what\n// pushed the sector numbers off their own shapes. Even-odd across every ring of every member is a\n// correct union test here because municipalities tile rather than overlap.\n// The search box is clipped to the viewBox, so the result is inside the territory AND on screen.\n// Rings are decimated first — this is a label, not a measurement — which keeps a 20-município sector\n// to a few million ops, and the result is cached per stage: geometry never changes between renders,\n// only the fill does.\nfunction labelPointFor(fs,pr){\n  const rings=[];\n  (fs||[]).forEach(f=>{ if(!f||!f.geometry) return;\n    const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>{\n      if(!r||r.length<4) return;\n      const step=Math.max(1,Math.floor(r.length/120)),pts=[];\n      for(let i=0;i<r.length;i+=step) pts.push(pr(r[i]));\n      if(pts.length>3) rings.push(pts);\n    }));});\n  if(!rings.length) return null;\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  mnx=Math.max(mnx,2); mny=Math.max(mny,2); mxx=Math.min(mxx,698); mxy=Math.min(mxy,498);\n  if(!(mxx>mnx&&mxy>mny)) return null;\n  const inside=(x,y)=>{ let c=false;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      if(((a[1]>y)!==(b[1]>y)) && (x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])) c=!c; }\n    return c; };\n  const edgeDist=(x,y)=>{ let m=Infinity;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      const dx=b[0]-a[0],dy=b[1]-a[1],L=dx*dx+dy*dy;\n      let t=L?(((x-a[0])*dx+(y-a[1])*dy)/L):0; t=t<0?0:(t>1?1:t);\n      const ex=a[0]+t*dx-x,ey=a[1]+t*dy-y,d=ex*ex+ey*ey; if(d<m)m=d; }\n    return Math.sqrt(m); };\n  const W=mxx-mnx,H=mxy-mny,N=26;\n  let best=null,bd=-1;\n  for(let i=0;i<=N;i++) for(let j=0;j<=N;j++){\n    const x=mnx+W*i/N,y=mny+H*j/N;\n    if(!inside(x,y)) continue;\n    const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; }\n  }\n  if(!best) return null;\n  // Hill-climb off the coarse grid: the grid only has to land somewhere in the right lobe, the\n  // refinement finds the deep point inside it — halving the step whenever no neighbour improves.\n  let step=Math.max(W,H)/N/2;\n  for(let k=0;k<7;k++){\n    let moved=false;\n    for(const o of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]){\n      const x=best[0]+o[0]*step,y=best[1]+o[1]*step;\n      if(x<mnx||x>mxx||y<mny||y>mxy||!inside(x,y)) continue;\n      const d=edgeDist(x,y); if(d>bd){ bd=d; best=[x,y]; moved=true; }\n    }\n    if(!moved) step/=2;\n  }\n  // The clearance travels with the point: it is the radius of the largest circle that fits inside the\n  // territory there, which is exactly the budget a caller needs to decide how much text will fit.\n  return {p:best,r:bd};\n}\nfunction labelPointOf(mv,key,fs,pr){\n  if(!mv._lbl) mv._lbl={};\n  if(!(key in mv._lbl)) mv._lbl[key]=labelPointFor(fs,pr);\n  return mv._lbl[key];\n}\n// ---------- map zoom/pan ----------\n// Zoom é feito manipulando o viewBox, nunca um transform CSS — assim rótulos e polígonos vivem no\n// MESMO sistema de coordenadas e escalam juntos automaticamente (texto tem font-size em unidades do\n// viewBox, então encolher o viewBox amplia tudo por igual, sem lógica extra pro tamanho da fonte).\nconst MAP_W=700, MAP_H=500, MAP_ZOOM_MAX=8;\nlet mapView={x:0,y:0,w:MAP_W,h:MAP_H};\nfunction applyMapView(){\n  const svg=$('#map'); if(!svg) return;\n  svg.setAttribute('viewBox', mapView.x.toFixed(2)+' '+mapView.y.toFixed(2)+' '+mapView.w.toFixed(2)+' '+mapView.h.toFixed(2));\n  // Rótulo tem font-size em unidades do viewBox, então sem isso ele cresceria junto com o zoom até\n  // virar um borrão — encolhendo o font-size na mesma proporção que o viewBox encolhe cancela\n  // exatamente o zoom, e o texto fica do MESMO tamanho na tela em qualquer nível de zoom.\n  const fsScale=mapView.w/MAP_W;\n  svg.querySelectorAll('text[data-fs]').forEach(t=>{\n    const base=+t.getAttribute('data-fs')||8;\n    t.setAttribute('font-size',(base*fsScale).toFixed(2));\n  });\n  // Mesmo raciocínio pro halo do texto e pro fio que liga um rótulo deslocado ao seu município —\n  // os dois têm largura de traço fixa em unidades do viewBox, então sem reescalar iam virar uma\n  // mancha grossa (halo) ou uma barra (fio) no zoom alto em vez de ficarem do mesmo tamanho na tela.\n  svg.querySelectorAll('[data-sw]').forEach(el=>{\n    const base=+el.getAttribute('data-sw')||1;\n    el.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n  });\n}\nfunction zoomMapBy(factor){\n  const cx=mapView.x+mapView.w/2, cy=mapView.y+mapView.h/2;\n  let w=mapView.w/factor, h=mapView.h/factor;\n  w=Math.min(MAP_W, Math.max(MAP_W/MAP_ZOOM_MAX, w));\n  h=Math.min(MAP_H, Math.max(MAP_H/MAP_ZOOM_MAX, h));\n  let x=cx-w/2, y=cy-h/2;\n  x=Math.min(MAP_W-w, Math.max(0,x));\n  y=Math.min(MAP_H-h, Math.max(0,y));\n  mapView={x,y,w,h};\n  applyMapView();\n}\n// Arrastar sempre fica DENTRO do canvas de 700x500 (clamp em [0, MAP_W-w] / [0, MAP_H-h]) — nunca\n// existe um x/y que deixa o viewBox parcialmente fora da área com conteúdo, então o mapa nunca\n// \"some\" da tela, só pode chegar até a borda do que existe pra ver.\nlet mapDrag=null, mapDidDrag=false;\nfunction mapPointerDown(e){\n  const svg=$('#map'); if(!svg) return;\n  mapDrag={x0:e.clientX,y0:e.clientY,vx0:mapView.x,vy0:mapView.y,rect:svg.getBoundingClientRect()};\n  mapDidDrag=false;\n  svg.classList.add('dragging');\n  // NÃO setPointerCapture aqui: capturar o ponteiro no <svg> reencaminha o click resultante pro\n  // próprio <svg> em vez do <path> do município embaixo do cursor — todo clique parava de chegar\n  // no onclick de reatribuição, não só depois de um arrasto de verdade. move/up escutam em `root`\n  // (nunca substituído por renderAll) em vez de no <svg>, então o arrasto continua funcionando\n  // mesmo se o ponteiro sair da área do mapa no meio do gesto.\n}\nfunction mapPointerMove(e){\n  if(!mapDrag) return;\n  const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n  if(!mapDidDrag && Math.hypot(dxPix,dyPix)>4) mapDidDrag=true;\n  if(!mapDrag.rect.width||!mapDrag.rect.height) return;\n  const sx=mapView.w/mapDrag.rect.width, sy=mapView.h/mapDrag.rect.height;\n  let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n  x=Math.min(MAP_W-mapView.w, Math.max(0,x));\n  y=Math.min(MAP_H-mapView.h, Math.max(0,y));\n  mapView={...mapView,x,y};\n  applyMapView();\n}\nfunction mapPointerUp(){\n  if(!mapDrag) return;\n  mapDrag=null;\n  const svg=$('#map'); if(svg) svg.classList.remove('dragging');\n}\nfunction drawMap(){\n  const mv=SESS.timeline[SESS.selected]; const svg=$('#map'); const ignNote=$('#mapIgnoredNote');\n  if(!mv){ svg.innerHTML=''; if(ignNote) ignNote.innerHTML=''; return; }\n  const level=mv.level||'setor';\n  // setor fecha: a unidade É o município. GV/mercado fecham: cada município herda a atribuição do\n  // setor/GV a que pertence — municípios do mesmo setor sempre têm a MESMA cor e giram JUNTOS no\n  // clique, porque é o setor inteiro (não o município) que troca de dono nesses níveis.\n  const unitAssign=m=>level==='setor'?mv.assign[m]:mv.assign[(mv.unitOfMuni||{})[m]];\n  const c=computeDerived(mv);const NS='http://www.w3.org/2000/svg'; svg.innerHTML='';\n  const feats=[...mv.nbrFeat,...Object.values(mv.geoFeat)]; if(!feats.length) return;\n  const pr=project(feats);\n  const world=foldWorldState(SESS.timeline,SESS.selected);\n  // The context layer is ONE area per SECTOR, not one shape per município. The unit of this decision\n  // is the destination sector; a scatter of individually outlined municipalities carrying their own\n  // names invited exactly the wrong reading of the map. Members of a sector are filled AND stroked in\n  // the same colour, so the shared borders vanish and the group reads as a single dissolved\n  // territory — the IBGE shapes do not share vertices precisely enough to extract a clean union\n  // outline, and carrying a real polygon-clipping union for a context layer is not worth it.\n  // Labels are COLLECTED here and drawn last, after every path. Drawn inline they were buried by the\n  // next sector's fill, and placement cannot avoid a collision it has not seen yet.\n  const labels=[],placed=[];\n  const textW=(s,fs)=>String(s).length*fs*0.56;\n  // WCAG relative luminance of the fill a label sits on, so ink and halo can invert per label. \"Would\n  // white be better?\" has no single answer on this map: white reads on #5856d6 (L≈0.14) and disappears\n  // on #ff9500 and #34c759 (both L≈0.43, where black wins ~9:1 against white's ~2.2:1) and on every\n  // context fill, which is .32 opacity over white. Decided per label, never globally.\n  const lum=col=>{let h=String(col||'').replace('#','');\n    if(h.length===3) h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];\n    if(h.length!==6) return 1;\n    const c=[0,2,4].map(i=>parseInt(h.slice(i,i+2),16)/255)\n      .map(v=>v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4));\n    return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2];};\n  // Centred on (x,y) in BOTH axes. The old box hung off the first line's baseline, so every two-line\n  // label sat half its own height below the point it was supposed to mark.\n  const rectOf=(x,y,lines)=>{const w=Math.max.apply(null,lines.map(l=>textW(l.t,l.size)));\n    const h=lines.reduce((a,l)=>a+l.size*1.2,0);\n    return {x1:x-w/2-2.5,x2:x+w/2+2.5,y1:y-h/2-1.5,y2:y+h/2+1.5};};\n  const nbrBySec={};\n  mv.nbrFeat.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf);\n    const s=String((mv.nbrCurrentSector&&mv.nbrCurrentSector[nm])||'');\n    (nbrBySec[s]=nbrBySec[s]||[]).push(f);\n  });\n  Object.keys(nbrBySec).forEach(s=>{\n    const fs=nbrBySec[s];\n    const isDest=!!s&&mv.dest.includes(s);\n    const closedCtx=!!s&&world.closedSectors.has(s);\n    const col=isDest?(mv.color[s]||null):null;\n    fs.forEach(f=>{\n      const p=document.createElementNS(NS,'path');\n      p.setAttribute('d',pathFor(f,pr));\n      p.setAttribute('class','sect'+(isDest?'':(closedCtx?' closed-ctx':' plain')));\n      if(col){ p.setAttribute('fill',col); p.setAttribute('stroke',col); }\n      svg.appendChild(p);\n    });\n    if(!s) return;\n    // One number per sector, at the pole of inaccessibility of its DISSOLVED area — the whole group\n    // of members at once, not a representative member, so the number sits in the visual middle of the\n    // territory the operator actually sees.\n    const lp=labelPointOf(mv,'s:'+s,fs,pr);\n    // Context areas always read LIGHT — .sect paints dest colours at fill-opacity .32 over white, and\n    // plain/closed-ctx are near-white greys — so a sector number never wants white ink, whatever colour\n    // the sector itself was assigned.\n    if(lp&&lp.p) labels.push({anchor:lp.p,big:true,dark:false,lines:[{t:s,size:11.5,weight:700}]});\n  });\n  const asis=mv.view==='asis';\n  // The UF prefix is information only when the stage actually straddles a border. On a single-UF setor\n  // it is the same three characters repeated on every label — a large share of the clutter and none of\n  // the meaning. The hover tooltip carries the UF either way.\n  const showUf=new Set(Object.keys(mv.geoFeat).map(k=>mUf(k)).filter(Boolean)).size>1;\n  if(level==='setor'){\n    // Setor fechando: a UNIDADE é o próprio município — cada um se comporta, colore e rotula\n    // individualmente, exatamente como sempre foi.\n    Object.entries(mv.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n      const fillCol=asis?CLOSE_COLOR:(mv.color[unitAssign(m)]||'#444');\n      p.setAttribute('class','muni');p.setAttribute('fill',fillCol);\n      if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[m]);if(m2.dest.length)m2.assign[m]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=m;});};\n      p.style.cursor=asis?'default':'pointer';\n      p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n        t.innerHTML=asis?`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs · setor ${mv.sector}`:`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs → ${unitAssign(m)||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n      p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n      const lp=labelPointOf(mv,'m:'+m,[f],pr);\n      const at=(lp&&lp.p)||cxy(f,pr);\n      const nmFull=(showUf&&mUf(m)?mUf(m)+'-':'')+mName(m);\n      const num=fmt(mv.cbsByMuni[m]||0)+(asis?'':'·'+(unitAssign(m)||'–'));\n      labels.push({anchor:at,cbs:+(mv.cbsByMuni[m]||0),dark:lum(fillCol)<0.18,\n        lines:[{t:nmFull,size:8.2,weight:700},{t:num,size:7,weight:500,mut:true}]});\n    });\n  } else {\n    // GV/mercado fechando: a UNIDADE é o setor (ou a GV) inteiro, nunca o município isolado —\n    // vários municípios do mesmo setor têm de ler como UMA área só, nunca como municípios separados\n    // que por acaso têm a mesma cor. Mesmo truque do contexto vizinho logo acima: stroke = fill via\n    // estilo inline (uma regra de classe como .muni{stroke:#fff} outrank­aria um atributo stroke\n    // comum, então a costura entre membros só some setando o style direto) — e UM rótulo só por\n    // unidade, no ponto de inacessibilidade da área dissolvida, nunca um rótulo por município.\n    Object.entries(mv.unitMunis||{}).forEach(([u,ms])=>{\n      const fillCol=asis?CLOSE_COLOR:(mv.color[mv.assign[u]]||'#444');\n      const feats=ms.map(m=>mv.geoFeat[m]).filter(Boolean);\n      // Todo município deste u é UMA unidade só na tela — passar o mouse sobre qualquer pedaço tem\n      // que acender o grupo inteiro, não só o pedaço que o cursor está tocando. :hover no CSS só\n      // sabe acender o próprio <path>; o grupo inteiro precisa de JS guardando os irmãos.\n      const els=[];\n      ms.forEach(m=>{\n        const f=mv.geoFeat[m]; if(!f) return;\n        const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n        p.setAttribute('class','muni');p.setAttribute('fill',fillCol);p.style.stroke=fillCol;\n        els.push(p);\n        if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[u]);if(m2.dest.length)m2.assign[u]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=u;});};\n        p.style.cursor=asis?'default':'pointer';\n        p.onmouseenter=()=>{ els.forEach(e=>e.classList.add('munihl')); };\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n          t.innerHTML=asis?`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs · ${LEVELS[level].label} ${mv.sector}`:`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs → ${mv.assign[u]||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n        p.onmouseleave=()=>{ els.forEach(e=>e.classList.remove('munihl')); $('#tip').style.opacity=0; };svg.appendChild(p);\n      });\n      const lp=labelPointOf(mv,'u:'+u,feats,pr);\n      const at=(lp&&lp.p)||(feats[0]&&cxy(feats[0],pr));\n      if(at){\n        const num=fmt(mv.munis[u]?.cbs||0)+(asis?'':'·'+(mv.assign[u]||'–'));\n        labels.push({anchor:at,big:true,cbs:+(mv.munis[u]?.cbs||0),dark:lum(fillCol)<0.18,\n          lines:[{t:`${LEVELS[level].childLabel} ${u}`,size:9,weight:700},{t:num,size:7.5,weight:500,mut:true}]});\n      }\n    });\n  }\n  // Placement, not de-cluttering. Every label IS drawn: it starts at its anchor and, if the text does\n  // not fit the territory or the slot is already taken, walks outward ring by ring until it finds\n  // clear canvas, trailing a hairline leader back to the shape it names. The previous pass dropped the\n  // loser of every collision outright, which is why municípios silently had no name at all.\n  // Order still matters — sector numbers first, then municípios by CB weight — because the earlier a\n  // label is placed the closer to home it lands.\n  const CW=700,CH=500,CPAD=2;\n  const inBounds=r=>r.x1>=CPAD&&r.x2<=CW-CPAD&&r.y1>=CPAD&&r.y2<=CH-CPAD;\n  const hits=r=>placed.reduce((a,q)=>a+((r.x2<q.x1||r.x1>q.x2||r.y2<q.y1||r.y1>q.y2)?0:1),0);\n  // Ring 0 is the anchor itself. Rings step 9u out over 16 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs.\n  const CAND=[[0,0]];\n  for(let ring=1;ring<=7;ring++){ const d=ring*9;\n    for(let a=0;a<16;a++){ const th=a*Math.PI/8; CAND.push([Math.cos(th)*d*1.35,Math.sin(th)*d]); } }\n  labels.sort((a,b)=>(b.big?1:0)-(a.big?1:0)||((b.cbs||0)-(a.cbs||0)));\n  labels.forEach(L=>{\n    let chosen=null,best=null,bestN=Infinity;\n    // Centred on the anchor is ALWAYS the first choice. An earlier cut here refused the anchor whenever\n    // the text was wider than the município's inscribed circle — true of almost every real name at this\n    // scale (BA-SAO SEBASTIAO DO PASSE is ~140u against ~80u of clearance), so every single label got\n    // nudged outside and nothing ended up centred on anything. Spilling past a border is normal on a\n    // choropleth and the halo keeps it readable; only a genuine COLLISION earns a move.\n    for(const off of CAND){\n      const x=L.anchor[0]+off[0], y=L.anchor[1]+off[1];\n      const r=rectOf(x,y,L.lines);\n      if(!inBounds(r)) continue;\n      const n=hits(r);\n      if(!n){ chosen={x,y,r,off}; break; }\n      // No clear slot yet — remember the LEAST crowded one. Falling back to the first in-bounds\n      // candidate meant falling back to the anchor every time, so a crowded map piled every loser onto\n      // the same point. CAND is ordered nearest-first and the compare is strict, so ties keep the\n      // closest slot.\n      if(n<bestN){ bestN=n; best={x,y,r,off}; }\n    }\n    // Canvas genuinely full: take the nearest in-bounds slot and accept the overlap. A label sitting\n    // on top of another is recoverable by eye; a name that was never drawn is not.\n    if(!chosen) chosen=best||{x:L.anchor[0],y:L.anchor[1],r:rectOf(L.anchor[0],L.anchor[1],L.lines),off:[0,0]};\n    placed.push(chosen.r);\n    const g=document.createElementNS(NS,'g'); g.setAttribute('class','lbl'+(L.big?' big':''));\n    if(Math.abs(chosen.off[0])>0.5||Math.abs(chosen.off[1])>0.5){\n      // The leader stops at the label's edge rather than its centre, so it never runs under the glyphs.\n      const cx=(chosen.r.x1+chosen.r.x2)/2, cy=(chosen.r.y1+chosen.r.y2)/2;\n      const hw=(chosen.r.x2-chosen.r.x1)/2, hh=(chosen.r.y2-chosen.r.y1)/2;\n      const dx=L.anchor[0]-cx, dy=L.anchor[1]-cy;\n      const tt=Math.min(dx?hw/Math.abs(dx):Infinity, dy?hh/Math.abs(dy):Infinity);\n      const ln=document.createElementNS(NS,'line'); ln.setAttribute('class','lead');\n      ln.style.stroke=L.dark?'#fff':'#1c1c1e';\n      // A largura do fio também é fixa em unidades do viewBox, igual o halo do texto — sem\n      // reescalar junto, no zoom alto ela vira uma barra grossa em vez de um fio fino.\n      ln.setAttribute('data-sw','0.8'); ln.style.strokeWidth='0.8px';\n      ln.setAttribute('x1',L.anchor[0].toFixed(1)); ln.setAttribute('y1',L.anchor[1].toFixed(1));\n      ln.setAttribute('x2',(cx+dx*tt).toFixed(1)); ln.setAttribute('y2',(cy+dy*tt).toFixed(1));\n      g.appendChild(ln);\n    }\n    const hTot=L.lines.reduce((a,l)=>a+l.size*1.2,0);\n    let ty=chosen.y-hTot/2+L.lines[0].size*0.86;\n    // Inline style rather than a class: a stylesheet rule would outrank a presentation attribute and\n    // undo the per-label contrast decision made above.\n    const ink=L.dark?'#fff':'#1c1c1e', inkMut=L.dark?'rgba(255,255,255,.85)':'#48484d';\n    const halo=L.dark?'rgba(0,0,0,.6)':'#fff';\n    // Halo do texto (o \"contorno\" que garante legibilidade sobre qualquer cor de fundo) também é\n    // fixo em unidades do viewBox via .lbl text{stroke-width}/.lbl.big text{stroke-width} — sem\n    // reescalar junto com o font-size, no zoom alto ele vira uma mancha escura cobrindo a letra\n    // inteira em vez de um contorno fino, exatamente o \"Setor 3570\" ilegível reportado.\n    const swBase=L.big?2.9:2.2;\n    L.lines.forEach(l=>{ const t=document.createElementNS(NS,'text');\n      t.setAttribute('x',chosen.x.toFixed(1)); t.setAttribute('y',ty.toFixed(1));\n      t.setAttribute('font-size',l.size); t.setAttribute('data-fs',l.size); t.setAttribute('font-weight',l.weight||600);\n      t.setAttribute('data-sw',swBase); t.style.strokeWidth=swBase+'px';\n      t.style.fill=l.mut?inkMut:ink; t.style.stroke=halo;\n      t.textContent=l.t; g.appendChild(t); ty+=l.size*1.2; });\n    svg.appendChild(g);\n  });\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>${LEVELS[level].label} ${mv.sector} (estrutura antes desta etapa — ${level==='setor'?Object.keys(mv.geoFeat).length+' municípios':Object.keys(mv.unitMunis||{}).length+' '+LEVELS[level].childLabel.toLowerCase()+'(s)'})</span>`\n    // No \"vizinhos (contexto)\" or \"setor já fechado antes\" entry any more: every feature still drawn\n    // belongs to a destination sector, so .sect.plain and .sect.closed-ctx can no longer be produced\n    // and a legend key for them would point at nothing on the map.\n    :mv.dest.map(s=>`<span><b style=\"background:${mv.color[s]}\"></b>${LEVELS[level].destLabel} ${s} — ${c.gained[s]} líder(es)</span>`).join('');\n  // Municipalities outside the dominance base are deliberately NOT drawn — no shape, no hover, no\n  // legend entry. They are reported in the panel instead, so the map only ever shows territory that\n  // is actually being reallocated.\n  if(ignNote) ignNote.innerHTML=(mv.excludedMunis&&mv.excludedMunis.length)\n    ? `<b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs||0)} CBs</b> fora da base de dominantes deste setor não são desenhados no mapa — as CBs seguem a líder do seu grupo (detalhe em “Municípios → setor destino”).`\n    : '';\n  applyMapView();\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{ await ensurePinned(); const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nfunction renderAll(){\n  renderStage();\n  renderTimeline(); renderSectorPicker();\n  const shb=$('#openShare'); if(shb) shb.disabled=!SESS.timeline.length;\n  const dpb=$('#dupSession'); if(dpb) dpb.disabled=!SESS.timeline.length;\n  const mv=SESS.timeline[SESS.selected];\n  // resolveSnapshotPin falls back to a live sub-select when it cannot pin a literal; saying\n  // \"congelado\" in that case would be a lie, so the pill reports what is actually in the SQL.\n  const pinned=SESS.snapLiteral && !/^\\(SELECT/i.test(SESS.snapLiteral);\n  $('#liveState').textContent = SESS.snapshotRaw\n    ? (pinned?'● snapshot congelado · ':'◍ snapshot ao vivo (não fixado) · ')+'atualizado em '+fmtSyncedAt(SESS.snapshotSyncedAt)+' · ↻'\n    : '● iniciando';\n  const mp=$('#basePill');\n  if(mp){\n    const warn=[];\n    if(DOM.bySetor&&!DOM.hasUf) warn.push('⚠ sem coluna UF');\n    if(DOM.badUf) warn.push('⚠ '+fmt(DOM.badUf)+' UF não reconhecida(s)');\n    if(DOM.clashes) warn.push('⚠ '+fmt(DOM.clashes)+' homônimo(s) no mesmo setor');\n    mp.textContent=DOM.bySetor?('◇ base dominantes: '+fmt(DOM.setores)+' setores · '+fmt(DOM.rows)+' municípios'+(warn.length?' · '+warn.join(' · '):'')):(DOM.error?'◇ base dominantes: indisponível':'◇ base dominantes: carregando…');\n    mp.title=DOM.error||(DOM.bySetor?((DOM.hasUf\n        ?('Coluna de UF usada: \"'+DOM.ufCol+'\".')\n        :('SEM coluna de UF. Colunas lidas: '+((DOM.headers||[]).join(' | ')||'?')+'. A UF de cada município vem desta base — sem ela, homônimos voltam a ser casados só pelo nome.'))\n      +(DOM.badUf?('\\n'+DOM.badUf+' linha(s) com UF irreconhecível — tratadas como desconhecida.'):'')\n      +(DOM.clashes?('\\n'+DOM.clashes+' município(s) aparecem no mesmo setor com UFs diferentes. O SQL agrupa só por DS_MUNICIPIO, então esses chegam somados numa linha única e não há como separá-los.'):'')\n      ):'')||DOM.url||''; }\n  const rp=$('#rulePill'); if(rp) rp.textContent='◈ destino: '+ruleShort(SESS.settings);\n  const sp=$('#spanPill'); if(sp) sp.textContent='◎ span: '+spanShort(SESS.settings);\n  renderRuleNote(); renderSpanRule();\n  if(!mv){ ['#map','#matrix','#matrixTobe','#tobeNote','#leaders'].forEach(s=>{const el=$(s);if(el)el.innerHTML='';}); $('#kpis').innerHTML=''; $('#spans').innerHTML=''; $('#muniList').innerHTML=''; $('#plan').innerHTML=''; return; }\n  $('#mapTitle').textContent=`Etapa ${SESS.selected+1}/${SESS.timeline.length} — fechando ${lc(LEVELS[mv.level||'setor'].label)} ${mv.sector}`;\n  $('#viewToggle').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.v===mv.view));\n  $('#policy').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===mv.policy));\n  $('#optBadge').textContent=mv.optBadge||'';\n  drawMap(); renderContig(); renderSpanRule(); renderKpis(); renderSpans(); renderMuni(); renderLeaders(); renderMatrix(); renderMatrixTobe(); renderPolicy(); renderPlan(); renderReport(); renderCumulative();\n}\n\n// ---------- event wiring ----------\n$('#confirmSnap').onclick=()=>{ SESS.stage='main'; renderAll(); };\n$('#openMove').onclick=openMove;\n$('#moveClose').onclick=closeMove;\n$('#moveCancel').onclick=closeMove;\n$('#moveBg').addEventListener('click',e=>{ if(e.target.id==='moveBg') closeMove(); });\nroot.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>r.addEventListener('change',renderMoveForm));\n$('#moveRand').onclick=moveRandPick;\n$('#moveGo').onclick=submitMove;\n$('#moveCode').addEventListener('keydown',e=>{ if(e.key==='Enter') submitMove(); });\n$('#confirmSessionPick').onchange=()=>loadSession($('#confirmSessionPick').value);\n$('#optim').onclick=()=>editSelected(mv=>{ mv.assign=optimizeFor(mv); });\n$('#autoTobe').onclick=()=>editSelected(mv=>{ mv.assign=autoAssignFor(mv); mv.optBadge='TOBE por adjacência'; });\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.view=b.dataset.v;renderAll();});\n$('#mapZoomIn').onclick=()=>zoomMapBy(1.4);\n$('#mapZoomOut').onclick=()=>zoomMapBy(1/1.4);\n$('#map').addEventListener('pointerdown',mapPointerDown);\nroot.addEventListener('pointermove',mapPointerMove);\nroot.addEventListener('pointerup',mapPointerUp);\nroot.addEventListener('pointerleave',mapPointerUp);\n// Arrastar não pode virar \"cliquei num município e troquei o destino sem querer\": se o ponteiro se\n// moveu além do limiar de arrasto, este clique (capture, antes de chegar no <path>) é engolido.\n$('#map').addEventListener('click',e=>{ if(mapDidDrag){ e.stopPropagation(); e.preventDefault(); mapDidDrag=false; } },true);\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.policy=b.dataset.p;renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','tobe','report','cumulative','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;navigator.clipboard.writeText(JSON.stringify({setor:mv.sector,politica:mv.policy,plano:mv._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;const csv='municipio,uf,cbs,setor_destino\\n'+mv._plan.map(p=>`${p.municipio},${p.uf||''},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${mv.sector}.csv`;a.click();};\n$('#saveSession').onclick=saveSession;\n$('#dupSession').onclick=duplicateSession;\n$('#sessionPick').onchange=()=>loadSession($('#sessionPick').value);\n$('#liveState').onclick=refreshSnapshot;\n$('#liveState').addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); refreshSnapshot(); } });\n$('#openOptions').onclick=openOptions;\n$('#optClose').onclick=closeOptions;\n$('#optCancel').onclick=closeOptions;\n$('#optApply').onclick=applyOptions;\n$('#optMinCbs').addEventListener('input',renderOptPreview);\n['#optTargetSpan','#optMaxSpan','#optMinSpan'].forEach(id=>$(id).addEventListener('input',renderOptPreview));\nroot.querySelectorAll('input[name=\"destRule\"]').forEach(r=>r.addEventListener('change',renderOptPreview));\n$('#optionsBg').addEventListener('click',e=>{ if(e.target.id==='optionsBg') closeOptions(); });\nroot.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeOptions(); closeShare(); closeMove(); } });\n\n$('#openShare').onclick=openShare;\n$('#shareX').onclick=closeShare;\n$('#shareCancel').onclick=closeShare;\n$('#shareGo').onclick=doShare;\n$('#shareCopy').onclick=()=>{ if(!SHARE.url) return; navigator.clipboard.writeText(SHARE.url);\n  const b=$('#shareCopy'); if(b){ b.textContent='copiado ✓'; setTimeout(()=>{ const x=$('#shareCopy'); if(x) x.textContent='Copiar link'; },1500); } };\n$('#shareOpen').onclick=()=>{ if(SHARE.url) window.open(SHARE.url,'_blank'); };\n$('#shareBg').addEventListener('click',e=>{ if(e.target.id==='shareBg') closeShare(); });\n\n// ---------- init ----------\n(async()=>{\n  loader(true,'congelando snapshot da sessão…');\n  try{\n    await Promise.all([ensurePinned(), loadSectors(), refreshSessionPicker()]);\n    SESS.stage='confirm';\n  }catch(e){ setError('erro init: '+e.message); }\n  finally{ loader(false); renderAll(); const rd=$('#ready'); if(rd) rd.setAttribute('data-loaded','1'); }\n})();\n\n  }\n}\n"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}},"dominantesCsvUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dominantesCsvUrl"}}}}}}}}}},{"_id":"6a7f2f11a14a6f1480484324","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"KRgNUar3hCwC4PglklYuS","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst PALETTE=['#5856d6','#ff9500','#28cd41','#af52de','#ff2d55','#a2845e'];\nconst CLOSE_COLOR='#8e8e93';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\nconst METRICS_DEF=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\nconst OPT_K=1.5;\nconst SESSIONS_DS='timeline-shared-sessionsDataSet';   // same table as SHARED_DS — see notes at listSessions()\n// Span defaults are the real operating band, not placeholders: setor 2200 ≤ span ≤ 3500 (alvo 2700),\n// GV 10000 ≤ span ≤ 22000 (alvo 16000). Mercado não tem um histórico equivalente ainda, então nasce\n// sem limite (null) até alguém configurar. Span agora é uma escala POR NÍVEL: fechar um setor só olha\n// span.setor, fechar uma GV só olha span.gv, fechar um mercado só olha span.mercado — nunca se misturam.\n// Existing sessions are NOT touched — their persisted settings carry explicit values that spread OVER\n// these in openOptions/applyOptions, which is deliberate: their etapas were computed under the old\n// band and changing it silently would leave the stored result and the rule that produced it out of sync.\nconst DEFAULT_SETTINGS={span:{\n  setor:{targetSpan:2700,maxSpan:3500,minSpan:2200},\n  gv:{targetSpan:16000,maxSpan:22000,minSpan:10000},\n  mercado:{targetSpan:null,maxSpan:null,minSpan:null},\n},\n  forceContiguity:true,\n};\nconst DOM_MEDIA_BASE='https://natura-geomarketing.data2.media/file/';\nconst DOM_CSV_FALLBACK=DOM_MEDIA_BASE+'yw_TQdp0QXEk6JsrliAqP'; \nconst numOrNull=v=>{const n=Math.round(+v);return (isFinite(n)&&n>0)?n:null;};\nconst spanShort=st=>{const p=[];if(st&&st.targetSpan)p.push('alvo '+st.targetSpan);if(st&&st.maxSpan)p.push('máx '+st.maxSpan);if(st&&st.minSpan)p.push('mín '+st.minSpan);return p.length?p.join(' · '):'livre';};\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#28cd41;\n    /* Tokens do AppKit real (macOS Big Sur): raio 6 e sombra y1/blur3/15% preto são os valores\n       documentados de botão/controle do sistema, não um chute — usados em todo botão, select,\n       segmented control e tab pra ler como controle nativo em vez de elemento web plano. */\n    --radius-ctrl:6px;--shadow-ctrl:0 1px 3px rgba(0,0,0,.15)}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px) saturate(1.8);-webkit-backdrop-filter:blur(20px) saturate(1.8);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .mac-spacer{flex:1}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  .pill.live{color:var(--ink);cursor:pointer;user-select:none;transition:border-color .1s,color .1s,background .1s}\n  .pill.live:hover{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  .pill.live:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .pill.live.busy{border-color:var(--acc);color:var(--acc);cursor:progress}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:var(--radius-ctrl);padding:5.5px 9px;box-shadow:var(--shadow-ctrl)}\n  /* Seta nativa do <select> cola na borda direita sem respiro nenhum — appearance:none tira a seta\n     do sistema e desenha uma própria, com o mesmo respiro (9px) do padding horizontal do resto. */\n  select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:26px;\n    background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5.5 8L10 12.5L14.5 8' fill='none' stroke='%238e8e93' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n    background-repeat:no-repeat;background-position:right 9px center;background-size:13px}\n  select:focus,input:focus{outline:2px solid rgba(0,122,255,.35);outline-offset:0;border-color:var(--acc)}\n  button.act{background:var(--acc);color:#fff;border:1px solid var(--acc2);border-radius:var(--radius-ctrl);padding:6px 13px;font-weight:600;cursor:pointer;box-shadow:var(--shadow-ctrl);transition:background .1s}\n  button.act:hover{background:var(--acc2)}\n  button.act:active{background:#0052c4;box-shadow:inset 0 1px 2px rgba(0,0,0,.25)}\n  button.ghost{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-ctrl);padding:5.5px 12px;cursor:pointer;box-shadow:var(--shadow-ctrl);transition:background .1s}\n  button.ghost:hover{background:var(--panel2)}\n  button.ghost:active{background:#e5e5ea;box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}\n  button.ghost.on{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  button.ghost:disabled,button.act:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}\n  .sessbar{padding:9px 20px;background:var(--panel2);border-bottom:1px solid var(--line);display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n  .rail{padding:9px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:center;overflow-x:auto}\n  #movementChips{display:flex;gap:8px;flex:0 0 auto}\n  #addMovementGroup{display:flex;gap:8px;align-items:center;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--line)}\n  #addMovementGroup input{min-width:150px}\n  .tchip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px;flex:0 0 auto}\n  .tchip.on{border-color:var(--acc);background:#eaf3ff}\n  .tchip.stale{border-style:dashed;opacity:.85}\n  .tchip .tstale{color:#ff9500;font-weight:700}\n  .tchip .tnum{background:var(--ink);color:#fff;border-radius:99px;width:16px;height:16px;font-size:9.5px;display:flex;align-items:center;justify-content:center;font-weight:700}\n  .tchip .tsec{font-weight:600}\n  .tchip .tstat{color:var(--mut)}\n  .tchip .trm{color:var(--mut);font-weight:700;padding:0 3px}\n  .tchip .trm:hover{color:var(--closing)}\n  .rail .empty{color:var(--mut);font-size:12px}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 16px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,.08)}\n  .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:0 0 9px;font-weight:600}\n  /* user-select, not pointer-events. .lbl already sets pointer-events:none, which stops the text from\n     RECEIVING clicks but does not exclude it from the browser's selection range — so cycling a\n     município's destination with repeated clicks kept dragging a double-click selection across the\n     labels. Nothing drawn in an svg here is text anyone needs to copy. */\n  svg{width:100%;height:auto;display:block;user-select:none;-webkit-user-select:none}\n  /* O <svg> tinha width:100%;height:auto — a altura era só uma função da LARGURA da coluna, então\n     numa tela larga e curta o mapa virava mais alto que o espaço vertical sobrando e a página\n     inteira crescia além da tela. max-height no wrapper + height:100%;width:auto;max-width:100% no\n     próprio svg é o equivalente de object-fit:contain: o navegador encaixa o viewBox 700x500 (via\n     preserveAspectRatio, que já existia) dentro do que sobrar de altura OU largura, o que for menor\n     primeiro, sem distorcer nem cortar nada. */\n  /* Altura real vem de fitMapHeight() (JS): mede quanto sobrou de viewport abaixo do topo do\n     card e aplica isso como altura inline — assim o mapa para exatamente na borda da tela\n     independente do que tem acima (cabeçalho, trilho de etapas, toolbar) ou abaixo dele.\n     60vh aqui é só o valor antes do primeiro paint do JS rodar. */\n  .map-wrap{position:relative;height:60vh;display:flex;justify-content:center;overflow:hidden}\n  #map{cursor:grab;touch-action:none;width:100%;height:100%}\n  #map.dragging{cursor:grabbing}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 4px 14px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.08);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  .osm-panel{position:absolute;left:10px;top:10px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 4px 14px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.08);padding:8px 10px;font-size:11.5px;z-index:3;min-width:104px}\n  .osm-panel-hd{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin-bottom:6px}\n  .osm-group-hd{font-size:9.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin:2px 0 4px}\n  .osm-group-hd.sep{margin-top:8px;padding-top:8px;border-top:1px solid var(--line)}\n  .osm-row{display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer;color:var(--ink)}\n  .osm-row input{accent-color:var(--acc);margin:0}\n  .osm-status{margin-top:6px;font-size:10px;color:var(--mut);max-width:150px;line-height:1.4}\n  /* White hairlines inside the closing sector: the municipios read as one territory cut into parts\n     rather than as separate outlined units, and the saturated fills carry the grouping instead of the\n     ink. Neighbour sectors keep their own coloured strokes (.sect) — that contrast is what separates\n     what is being reallocated from what is only context. Selection no longer draws an outline; the\n     white border was the click affordance AND the default is now white, so it had nothing left to say. */\n  .muni{stroke:#fff;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.15)}.muni.munihl{filter:brightness(1.15)}\n  .sect{stroke-width:1;fill-opacity:.32;stroke-opacity:.6}\n  .sect.plain{fill:#e8e8ec;stroke:#d4d4d9;fill-opacity:1;stroke-opacity:1}\n  .sect.closed-ctx{fill:#e2e2e7;stroke:#aeaeb2;stroke-dasharray:3 2;fill-opacity:1;stroke-opacity:1}\n  .sect.rejected{fill:#e4e4ea;fill-opacity:1;cursor:help}\n  .sect-rejected-border{fill:none;stroke:#9797a3;stroke-width:1.4;pointer-events:none}\n  .sect.unresolved{fill:#fbeceb;stroke:#e8a29c;stroke-dasharray:2 2;fill-opacity:1;stroke-opacity:1;cursor:help}\n  .lbl{pointer-events:none;filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.16))}\n  /* No plate behind the text: a halo painted UNDER the glyphs (paint-order) keeps a name legible on any\n     fill without stamping an opaque box over the territory it names. Ink and halo are chosen per label\n     from the luminance of the fill underneath, so they are deliberately NOT declared here — a\n     stylesheet rule outranks a presentation attribute and would silently override that choice. */\n  .lbl text{text-anchor:middle;paint-order:stroke fill;stroke-width:1.8px;stroke-linejoin:round;stroke-linecap:round;letter-spacing:-.1px;font-family:-apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif}\n  .lbl.big text{stroke-width:2.4px}\n  .lbl line.lead{stroke-opacity:.6;stroke-width:.8;stroke-linecap:round}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:4px;margin-right:5px;vertical-align:-1px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:19px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#ffd1cc}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bdeecb}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:600;font-size:12px}\n  .bar-track{flex:1;height:18px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex;position:relative}\n  .bar-mark{position:absolute;top:0;bottom:0;width:2px;opacity:.9;z-index:2}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;background:var(--panel2);border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;border-right:1px solid var(--line);background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer}\n  .seg button:last-child{border-right:0}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  /* Segmented control de verdade: trilho cinza (panel2) + o segmento ativo vira uma cápsula branca\n     flutuando por cima (shadow-ctrl), não só uma mudança de cor plana. */\n  .toggle{display:flex;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:2px;gap:1px;font-size:12px}\n  .toggle button{background:transparent;border:0;border-radius:6px;color:var(--mut);padding:5px 10px;cursor:pointer;font-weight:600;transition:background .12s,box-shadow .12s,color .12s}\n  .toggle button.on{background:#fff;color:var(--ink);box-shadow:var(--shadow-ctrl)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .lider-foot td{background:var(--panel2);font-weight:600;font-size:11px;color:var(--ink);border-bottom:2px solid var(--line);padding:5px 6px;text-align:left}\n  .scroll{max-height:240px;overflow:auto}\n  /* Tables meant to be READ in full rather than sampled. Only the vertical cap is dropped — overflow\n     stays auto so the wide grupo grid can still scroll sideways inside its card, which is the one axis\n     that genuinely cannot fit. */\n  .scroll.grow{max-height:none}\n  .tip{position:fixed;pointer-events:none;background:#1c1c1e;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px;box-shadow:0 2px 8px rgba(0,0,0,.28)}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}\n  .tabs button{background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600;box-shadow:var(--shadow-ctrl);transition:background .1s,box-shadow .1s}\n  .tabs button.on{background:#fff;color:var(--ink);border-color:var(--acc);box-shadow:0 1px 3px rgba(0,0,0,.2)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(245,245,247,.85);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:20;font-size:13px;color:var(--acc);font-weight:600;text-align:center;padding:20px}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n  /* #optBadge é o único badge que costuma quebrar em várias linhas (resumo inteiro do otimizador) —\n     o padding vertical de 1px do .badge genérico (pensado pra um badge de uma linha só) deixa o\n     texto quase colado nas bordas de cima/baixo quando quebra; display:inline-block faz o padding\n     valer pra caixa inteira, não só a ponta da primeira/última linha de um span inline. */\n  #optBadge{display:inline-block;padding:5px 8px;line-height:1.5}\n  .sheet-bg{position:fixed;inset:0;background:rgba(28,28,30,.34);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:40;padding:24px}\n  .sheet-bg.hidden{display:none}\n  .sheet{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.22);width:min(580px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden}\n  .sheet-hd{display:flex;align-items:center;gap:9px;padding:12px 16px;border-bottom:1px solid var(--line)}\n  .sheet-hd h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-.1px}\n  .sheet-x{background:transparent;border:0;color:var(--mut);font-size:13px;cursor:pointer;padding:3px 7px;border-radius:6px}\n  .sheet-x:hover{background:var(--panel2);color:var(--ink)}\n  .sheet-bd{padding:14px 16px;overflow:auto}\n  .sheet-ft{display:flex;align-items:center;gap:8px;padding:11px 16px;border-top:1px solid var(--line);background:var(--panel2)}\n  .opt-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600}\n  .opt-sub{font-size:11.5px;color:var(--mut);margin:4px 0 10px;line-height:1.5}\n  .opt-row{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;cursor:pointer;background:var(--panel)}\n  .opt-row:hover{background:var(--panel2)}\n  .opt-row.on{border-color:var(--acc);background:#eaf3ff}\n  .opt-row:has(input:disabled){opacity:.5;cursor:not-allowed}\n  .opt-row input[type=radio]{margin:2px 0 0;accent-color:var(--acc);flex:0 0 auto}\n  .opt-t{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n  .opt-d{font-size:11.5px;color:var(--mut);margin-top:3px;line-height:1.55}\n  .opt-inline{font-size:11.5px;color:var(--mut);margin-top:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}\n  .opt-inline.off{opacity:.4;pointer-events:none}\n  .opt-note{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:9px 11px;font-size:11.5px;color:var(--mut);line-height:1.6;margin-top:12px}\n  .opt-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:2px}\n  .opt-fld{display:flex;flex-direction:column;gap:5px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;background:var(--panel2)}\n  .opt-fld span{font-size:11px;color:var(--mut);font-weight:600;line-height:1.35}\n  .opt-fld i{font-style:normal;font-weight:400;opacity:.72}\n  .opt-fld input{width:100%;background:var(--panel)}\n  .opt-err{margin-top:9px;font-size:11.5px;color:var(--closing);font-weight:600}\n  .opt-hr{height:1px;background:var(--line);margin:17px 0 0}\n  .shareurl{margin-top:9px;padding:9px 10px;border-radius:8px;background:var(--panel);border:1px solid var(--acc);\n    font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--ink);word-break:break-all;user-select:all}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header id=\"topHeader\" class=\"hidden\">\n  <h1>SEC | Natura Geomarketing</h1>\n  <span class=\"pill live\" id=\"liveState\" role=\"button\" tabindex=\"0\" title=\"Clique pra buscar o snapshot mais recente e recalcular a timeline inteira\">● iniciando</span>\n  <span class=\"status\" id=\"ds\"></span>\n  <div class=\"mac-spacer\"></div>\n  <select id=\"sessionPick\" style=\"min-width:200px\"></select>\n  <button class=\"ghost\" id=\"renameSession\" title=\"Renomear esta sessão\">✎</button>\n  <button class=\"ghost\" id=\"openOptions\" title=\"Configurações desta sessão\">⚙ Opções</button>\n  <button class=\"act\" id=\"saveSession\">Salvar sessão</button>\n  <button class=\"ghost\" id=\"dupSession\" title=\"Cria uma cópia independente desta sessão — a original fica intacta e a cópia passa a ser a sessão ativa\">⧉ Duplicar</button>\n  <button class=\"ghost\" id=\"openShare\" title=\"Publicar uma cópia congelada desta sessão em modo leitura\">⇪ Compartilhar</button>\n  <span class=\"status\" id=\"sessStatus\"></span>\n</header>\n\n<div class=\"rail hidden\" id=\"timelineRail\">\n  <div id=\"movementChips\"></div>\n  <div id=\"addMovementGroup\">\n    <button class=\"act\" id=\"openMove\">Adicionar movimentação</button>\n  </div>\n</div>\n\n<div class=\"card\" id=\"confirmCard\" style=\"margin:14px 20px\">\n  <h2 style=\"margin:0 0 9px\">Confirmar snapshot antes de simular</h2>\n  <div class=\"note\" id=\"confirmBody\">congelando snapshot…</div>\n  <button class=\"act\" id=\"confirmSnap\" style=\"margin-top:12px\">Confirmar snapshot ▶ ir pra simulação</button>\n  <div class=\"note\" style=\"margin-top:16px\">ou continue uma sessão salva:</div>\n  <select id=\"confirmSessionPick\" style=\"min-width:260px;margin-top:4px\"></select>\n  <div class=\"status\" id=\"confirmStatus\" style=\"margin-top:8px\"></div>\n</div>\n\n<div class=\"wrap hidden\" id=\"mainWrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura do setor antes desta etapa (já considerando etapas anteriores) · TOBE = após a redistribuição desta etapa\">\n          <button data-v=\"asis\">◧ ASIS (antes desta etapa)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <div class=\"map-wrap\">\n        <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n        <div class=\"osm-panel\" id=\"osmPanel\" title=\"Camadas exibidas no mapa, limitadas à área desta etapa\">\n          <div class=\"osm-panel-hd\">Camadas</div>\n          <div class=\"osm-group-hd\">Navegação</div>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"osmHighways\"><span>Rodovias</span></label>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"osmRivers\"><span>Rios</span></label>\n          <div class=\"osm-group-hd sep\">Legendas</div>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"legendMuni\" checked><span id=\"legendMuniLbl\">Nome de municípios</span></label>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"legendCtx\"><span>Código de setores adjacentes</span></label>\n          <div class=\"osm-status\" id=\"osmStatus\"></div>\n        </div>\n        <div class=\"zoom-pill\" id=\"mapZoom\">\n          <button type=\"button\" id=\"mapZoomOut\" title=\"Diminuir zoom\">−</button>\n          <span class=\"zoom-div\"></span>\n          <button type=\"button\" id=\"mapZoomIn\" title=\"Aumentar zoom\">+</button>\n        </div>\n      </div>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado nesta etapa (cor = destino). Ao redor, cada área é um <b>setor vizinho inteiro</b> com o código no meio — não municípios soltos: cor = setor destino, cinza = contexto, cinza tracejado = setor já fechado numa etapa anterior. Shapes reais IBGE/tbrugz.</div>\n      <div class=\"note\" id=\"mapIgnoredNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas (nesta etapa)</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor.</div>\n      <div class=\"scroll grow\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"tobe\">Matriz TOBE</button>\n        <button data-tab=\"report\">Relatório de impacto (etapa)</button>\n        <button data-tab=\"cumulative\">Impacto acumulado (timeline)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll grow\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-tobe\" class=\"hidden\"><div class=\"scroll grow\"><table id=\"matrixTobe\"></table></div>\n        <div class=\"note\" id=\"tobeNote\"></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-cumulative\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"note\" style=\"margin:-4px 0 8px\">Roda contra o snapshot congelado desta sessão (não \"ao vivo\") — mesma coerência da timeline.</div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa alocações que mantêm cada setor conectado e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (mantém o território conectado; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Editar esta etapa recalcula automaticamente as etapas seguintes da timeline.</div>\n      <div class=\"note\" id=\"ruleNote\" style=\"margin:-6px 0 11px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px\"></div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <div id=\"spanRule\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div id=\"policySection\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n      </div>\n    </div>\n\n    <div class=\"card\">\n      <h2 id=\"muniListTitle\">Municípios → setor destino</h2>\n      <div class=\"note\" id=\"muniListNote\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração (etapa selecionada)</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"shareBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"shareTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"shareTitle\">Compartilhar sessão</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"shareX\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\" id=\"shareBody\"></div>\n    <div class=\"sheet-ft\">\n      <span class=\"status\" id=\"shareStatus\"></span>\n      <div class=\"mac-spacer\"></div>\n      <button class=\"ghost hidden\" id=\"shareCopy\">Copiar link</button>\n      <button class=\"ghost hidden\" id=\"shareOpen\">Abrir</button>\n      <button class=\"ghost\" id=\"shareCancel\">Cancelar</button>\n      <button class=\"act\" id=\"shareGo\">Compartilhar</button>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"optionsBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"optTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"optTitle\">Opções da sessão</h3><span class=\"badge\" id=\"optSessName\"></span><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"optClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Pré-requisitos do destino — tamanho em CBs</div>\n      <div class=\"opt-sub\">Vale pro tamanho de cada unidade receptora <b>depois</b> da redistribuição (base + entrantes) — uma escala por nível de fechamento. Em branco = desligado.</div>\n      <div class=\"opt-t\" style=\"margin-top:2px\">Setor</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhum setor acima)</i></span><input type=\"number\" id=\"optMaxSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhum setor abaixo)</i></span><input type=\"number\" id=\"optMinSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-t\" style=\"margin-top:12px\">GV</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhuma GV acima)</i></span><input type=\"number\" id=\"optMaxSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhuma GV abaixo)</i></span><input type=\"number\" id=\"optMinSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:14px\">Conectividade de região</div>\n      <div class=\"opt-sub\">Só se aplica a fechamento de Setor. Controla o que acontece quando um clique no mapa deixaria algum setor com o território partido em pedaços desconectados — o aviso <b style=\"color:var(--closing)\">⚠ Território desconectado</b> abaixo do mapa.</div>\n      <label class=\"opt-row\"><input type=\"radio\" name=\"forceContig\" value=\"0\"><div><div class=\"opt-t\">Livre</div><div class=\"opt-d\">Clicar num município avança pro próximo setor destino da lista, mesmo que isso deixe o território de algum setor desconectado. Resolva depois com ★ ÓTIMO ou reassociando manualmente no mapa.</div></div></label>\n      <label class=\"opt-row\"><input type=\"radio\" name=\"forceContig\" value=\"1\"><div><div class=\"opt-t\">Forçar conectividade de território <i style=\"font-weight:400;color:var(--mut)\">(padrão)</i></div><div class=\"opt-d\">Clicar num município nunca deixa um setor desconectado: pula o(s) destino(s) da lista que quebrariam a conectividade e escolhe o próximo que mantém todo setor conectado. Se nenhum destino for seguro, o clique não muda nada.</div></div></label>\n      <div class=\"opt-err hidden\" id=\"optSpanErr\"></div>\n      <div class=\"opt-note\" id=\"optPreview\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"optStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"optCancel\">Cancelar</button><button class=\"act\" id=\"optApply\">Aplicar</button></div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"moveBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"moveTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"moveTitle\">Adicionar movimentação</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"moveClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Tipo de movimentação</div>\n      <div class=\"opt-sub\" id=\"moveLockNote\">Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.</div>\n      <label class=\"opt-row\" data-lvl=\"setor\"><input type=\"radio\" name=\"moveLevel\" value=\"setor\"><div><div class=\"opt-t\">Fechar Setor</div><div class=\"opt-d\">Redistribui os municípios do setor entre setores vizinhos da mesma GV.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"gv\"><input type=\"radio\" name=\"moveLevel\" value=\"gv\"><div><div class=\"opt-t\">Fechar GV</div><div class=\"opt-d\">Redistribui os setores (com municípios e CBs) da GV entre GVs vizinhas do mesmo mercado.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"mercado\"><input type=\"radio\" name=\"moveLevel\" value=\"mercado\"><div><div class=\"opt-t\">Fechar Mercado</div><div class=\"opt-d\">Redistribui as GVs (com setores, municípios e CBs) do mercado entre mercados vizinhos.</div></div></label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\" id=\"moveCodeLbl\">Setor a fechar</div>\n      <div class=\"opt-inline\" id=\"moveCodeRow\">\n        <input id=\"moveCode\" list=\"moveCodeList\" placeholder=\"código\" style=\"min-width:150px\">\n        <datalist id=\"moveCodeList\"></datalist>\n        <button class=\"ghost\" id=\"moveRand\" title=\"Aleatório\">🎲</button>\n      </div>\n      <div class=\"opt-err hidden\" id=\"moveErr\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"moveStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"moveCancel\">Cancelar</button><button class=\"act\" id=\"moveGo\">Fechar ▶ (nova etapa)</button></div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div>\n<span id=\"ready\" style=\"display:none\"></span></div>`;\n  },\n  mounted(d2, root){\n\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\nconst blankM=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n// ---------- município identity ----------\n// Brazil has many municipalities sharing a name (Bom Jesus alone exists in PI, RN, RS, SC, PB, GO).\n// A stage loads the dominant UF PLUS every bordering UF, so those collisions land in the SAME\n// feature set routinely: keyed by name alone the last shape loaded won, the map could draw a\n// município from the wrong state, buildAdj merged two unrelated municipalities into one node, and\n// GROUP BY DS_MUNICIPIO summed two real municipalities into one row. The key is NAME/UF everywhere\n// — SQL rows, GeoJSON features, the dominantes base, assignments, persisted sessions. norm() maps\n// every non-alphanumeric to a space, so '/' can never occur inside a name and the split is exact.\n// A missing UF degrades to a bare NAME key rather than inventing one.\n// A handful of municípios carry two spellings in the wild — an older one still used internally (CRM\n// export, this dominantes CSV) and the current official IBGE spelling used by the geodata mirror.\n// Left alone, the two spellings key to different places: the shape draws with no dominance entry\n// (\"setor não encontrado\") even though the base has it under the other spelling. Canonicalized here,\n// once, to whichever spelling the CRM base uses — so display stays consistent with every other\n// município name in this app, which comes from the CRM, never from IBGE.\nconst MUNI_ALIAS={BRASOPOLIS:'BRAZOPOLIS','DONA EUSEBIA':'DONA EUZEBIA'};\nconst muniN=s=>{const n=norm(s);return MUNI_ALIAS[n]||n;};\nconst mkey=(name,uf)=>{const n=muniN(name),u=String(uf==null?'':uf).trim().toUpperCase();return u?n+'/'+u:n;};\nconst mName=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?s:s.slice(0,i);};\nconst mUf=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?'':s.slice(i+1);};\nconst mDisp=k=>{const u=mUf(k);return u?mName(k)+' ('+u+')':mName(k);};\n// The base CSV is the ONLY source of a município's UF now, so its column is accepted in whatever\n// shape the base is maintained in: sigla, IBGE numeric code, or full state name. Anything that does\n// not resolve to a real sigla is treated as UNKNOWN rather than passed through — a bogus UF would key\n// a município to a shape that does not exist and drop it off the map without a word.\nconst UF_OF_CODE={}; Object.keys(UF_CODE).forEach(k=>{ UF_OF_CODE[UF_CODE[k]]=k; });\nconst UF_OF_NAME={ACRE:'AC',ALAGOAS:'AL',AMAPA:'AP',AMAZONAS:'AM',BAHIA:'BA',CEARA:'CE','DISTRITO FEDERAL':'DF','ESPIRITO SANTO':'ES',GOIAS:'GO',MARANHAO:'MA','MATO GROSSO':'MT','MATO GROSSO DO SUL':'MS','MINAS GERAIS':'MG',PARA:'PA',PARAIBA:'PB',PARANA:'PR',PERNAMBUCO:'PE',PIAUI:'PI','RIO DE JANEIRO':'RJ','RIO GRANDE DO NORTE':'RN','RIO GRANDE DO SUL':'RS',RONDONIA:'RO',RORAIMA:'RR','SANTA CATARINA':'SC','SAO PAULO':'SP',SERGIPE:'SE',TOCANTINS:'TO'};\nconst ufNorm=v=>{const s=norm(v); if(!s) return '';\n  if(UF_CODE[s]) return s;\n  if(UF_OF_CODE[s]) return UF_OF_CODE[s];\n  return UF_OF_NAME[s]||'';};\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\nconst setError=(msg)=>{ const t=$('#ds'); if(t){t.textContent=msg;t.classList.add('err');} const c=$('#confirmStatus'); if(c){c.textContent=msg;c.classList.add('err');} };\n\n// ---------- base de municípios dominantes (CSV) ----------\n// WHICH municipalities belong to a sector is a business fact, not something to infer from CB volume:\n// it comes from a maintained CSV (one row per setor x município) linked through the `dominantesCsvUrl`\n// param. Anything the closing sector has CBs in but that the base does NOT assign to it is spillover:\n// not realocated, not drawn on the map, and its CBs follow their leader. Loaded once per URL and\n// cached for the whole session; a failure is thrown, never swallowed — a silent fallback here would\n// quietly change every number on the screen.\nconst DOM={url:null,promise:null,bySetor:null,ufBySetorMuni:null,rows:0,setores:0,hasUf:false,ufCol:null,badUf:0,clashes:0,headers:null,error:null};\nfunction parseCsv(text){\n  const out=[]; let row=[],cur='',q=false;\n  const push=()=>{ row.push(cur); cur=''; };\n  const endRow=()=>{ push(); if(row.length>1||row[0]!=='') out.push(row); row=[]; };\n  for(let i=0;i<text.length;i++){ const ch=text[i];\n    if(q){ if(ch==='\"'){ if(text[i+1]==='\"'){ cur+='\"'; i++; } else q=false; } else cur+=ch; }\n    else if(ch==='\"') q=true;\n    else if(ch===',') push();\n    else if(ch==='\\n') endRow();\n    else if(ch!=='\\r') cur+=ch;\n  }\n  if(cur!==''||row.length) endRow();\n  return out;\n}\n// `dominantesCsvUrl` is declared as a FileComponentUrlReference, so the instance points at the Drive\n// FILE, not at a URL literal — the Drive is policy-gated behind the app's auth strategy and a baked\n// string would be both stale and unauthorised. What the resolver hands back varies by where it is\n// resolved (URL string / file record / bare reference), so accept every shape instead of silently\n// reading undefined and falling through to the fallback.\n// The Drive is gated by the app's AuthenticationStrategy, so this read has to travel as the LOGGED-IN\n// USER — an anonymous CDN GET is exactly what the policy is there to refuse. `<system>.data2.media` is\n// only a cache in front of the SSP: on a cache miss it 302s to `<system>.data2apis.com`, which is where\n// the policy is actually evaluated.\n// COOKIES CANNOT CARRY THAT SESSION, which is why the previous `credentials:'include'` attempt could\n// never have worked: the app runs on <app>.<system>.data2.app and the SSP on <system>.data2apis.com —\n// cross-site — and the SSP answers `Access-Control-Allow-Origin: *` with no `Allow-Credentials`, so the\n// browser rejects a credentialed CORS read before it ever leaves the page. What the SSP actually reads\n// is `Authorization: Bearer <token>`: the runtime keeps that token in localStorage under\n// `__data2-session-<systemDomain>` (`__data2-session-data2` inside the builder preview) and replays it\n// on every dataset and rush call — same token, same header, same session, so the Drive policy sees the\n// logged-in user. Preflight passes because the SSP echoes whatever header the request asks for.\nfunction d2SessionToken(sysDomain){\n  const read=k=>{ try{ return (k&&window.localStorage.getItem(k))||''; }catch(e){ return ''; } };\n  return read(sysDomain?('__data2-session-'+sysDomain):'')||read('__data2-session-data2')||'';\n}\nfunction domCsvSystemDomain(url){\n  const m=/^https?:\\/\\/([^.\\/]+)\\.data2\\.(?:media|app)\\//i.exec(url)||/^https?:\\/\\/([^.\\/]+)\\.data2apis\\.com\\//i.exec(url);\n  return m?m[1]:'';\n}\nfunction domCsvCandidates(url){\n  const out=[];\n  const isMedia=/^https?:\\/\\/[^.\\/]+\\.data2\\.media\\//i.test(url);\n  const api=isMedia?url.replace(/^(https?:\\/\\/[^.\\/]+)\\.data2\\.media\\//i,'$1.data2apis.com/'):null;\n  const tok=d2SessionToken(domCsvSystemDomain(url));\n  const auth=tok?{authorization:'Bearer '+tok}:null;\n  // Authenticated origin read first — the only candidate a GATED Drive can actually answer.\n  if(api&&auth) out.push({url:api,headers:auth,label:'api+sessão'});\n  if(!api&&auth) out.push({url:url,headers:auth,label:'origem+sessão'});\n  // Anonymous last: only ever succeeds on an UNGATED Drive — or on a stale CDN cache hit, which is NOT\n  // proof the file is public (the origin keeps answering 401 once the edge entry expires).\n  if(api) out.push({url:api,headers:null,label:'api anônimo'});\n  out.push({url:url,headers:null,label:'origem anônima'});\n  return out;\n}\nfunction domCsvUrl(){\n  const pick=v=>{\n    if(!v) return '';\n    if(typeof v==='string') return v;\n    const q=v.params||{};\n    const ref=q.reference||v.reference;\n    return q.fileUrl||q.url||v.fileUrl||v.url||(ref?DOM_MEDIA_BASE+ref:'');\n  };\n  return String(pick(d2.param('dominantesCsvUrl'))||DOM_CSV_FALLBACK||'').trim();\n}\nasync function ensureDominantes(){\n  const url=domCsvUrl();\n  if(!url) throw new Error('base de municípios dominantes não configurada — ligue o arquivo CSV no parâmetro dominantesCsvUrl da instância do componente');\n  if(DOM.url===url && DOM.bySetor) return DOM.bySetor;\n  if(DOM.url===url && DOM.promise) return DOM.promise;\n  DOM.url=url; DOM.error=null;\n  DOM.promise=(async()=>{\n    // Self-diagnosing on purpose: every candidate reports its own outcome and the message names the\n    // session it was carrying. \"Unauthorized\" alone cannot distinguish \"not logged into THIS strategy\"\n    // from \"cookie not sent cross-site\" from \"param never bound, so it tried the fallback URL\" — and\n    // guessing between those from the outside costs a round trip each time.\n    let r=null; const tried=[];\n    for(const c of domCsvCandidates(url)){\n      let res=null;\n      try{ res=await fetch(c.url,{cache:'no-store',credentials:'omit',headers:c.headers||undefined}); }\n      catch(e){ tried.push(c.label+' → rede/CORS'); continue; }\n      if(res.ok){ r=res; break; }\n      tried.push(c.label+' → '+res.status);\n    }\n    if(!r){\n      let sess='sessão desconhecida';\n      try{\n        const a=d2.auth(AUTH_REF);\n        sess=(a&&a.isAuthed)?('logado como '+((a.user&&(a.user.email||a.user.nome))||'?')):('NÃO logado na estratégia '+AUTH_REF);\n      }catch(e){ sess='estratégia '+AUTH_REF+' não resolvida'; }\n      const tk=d2SessionToken(domCsvSystemDomain(url));\n      throw new Error('base de municípios dominantes: não foi possível ler o arquivo — '+sess+' · token de sessão '+(tk?'presente':'AUSENTE no localStorage')+'. Tentativas: '+tried.join(' · '));\n    }\n    const table=parseCsv(await r.text());\n    if(table.length<2) throw new Error('base de municípios dominantes vazia');\n    const head=table[0].map(h=>norm(h));\n    const iS=head.indexOf('CD SETOR'), iM=head.indexOf('MUNICIPIO');\n    if(iS<0||iM<0) throw new Error('base de municípios dominantes sem as colunas CD_SETOR / MUNICIPIO');\n    // CD_GV is the gerência de vendas the setor answers to, and it is the authority for the hard rule\n    // that a closing sector only ever hands territory to sectors of its OWN gerência. Its absence is\n    // fatal rather than a warning: with no GV there is no way to PROVE two sectors share one, and\n    // falling through unchecked is exactly the leak this column exists to close.\n    const iG=['CD GV','CDGV','GV','CD GERENCIA','CD GERENCIA VENDAS','CD GERENCIA DE VENDAS','GERENCIA DE VENDAS','GERENCIA'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iG==null) throw new Error('base de municípios dominantes sem a coluna CD_GV — a regra de gerência de vendas não pode ser aplicada. Colunas lidas: '+head.slice(0,40).join(', '));\n    // MERCADO fica ACIMA de GV na hierarquia (mercado > GV > setor) — fechamento de Mercado e a regra\n    // \"GV só fecha pra GV do mesmo mercado\" dependem dela do mesmo jeito que a regra de GV depende\n    // de CD_GV, então a ausência é fatal pelo mesmo motivo.\n    const iK=['MERCADO','MERCADO REGIONAL','MKT','MARKET'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iK==null) throw new Error('base de municípios dominantes sem a coluna MERCADO — fechamento de GV/Mercado precisa dela. Colunas lidas: '+head.slice(0,40).join(', '));\n    // Nomes de exibição — opcionais: a hierarquia funciona só com os códigos, mas o código sozinho\n    // não diz nada pro operador escolhendo o que fechar.\n    const iDS=['DC SETOR','NOME SETOR','DESCRICAO SETOR','NM SETOR'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const iDG=['DC GV','NOME GV','DESCRICAO GV','NM GV','GV NOME'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    // The UF column is what the whole keying now rests on — SQL no longer carries DS_ESTADO — so its\n    // absence is a first-class finding, not a footnote. The header actually matched is reported on the\n    // base pill, and when nothing matches the pill lists every column read, because guessing a header\n    // name from the outside costs a round trip each time.\n    const iU=['UF','SIGLA UF','SG UF','CD UF','ESTADO','DS ESTADO','SIGLA','ESTADO SIGLA','UF MUNICIPIO'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const by={},ufBy={},gvBy={},mkBySetor={},setorLabel={},gvLabel={}; let n=0,bad=0; const clash={},gvClash={},mkClash={};\n    for(let i=1;i<table.length;i++){\n      const line=table[i]; if(!line) continue;\n      const s=String(line[iS]==null?'':line[iS]).trim(), m=muniN(line[iM]);\n      if(!s||!m) continue;\n      // One setor spans many rows (one per município) and every one of them repeats its CD_GV. Two\n      // DIFFERENT values for the same setor means the base itself disagrees about which gerência owns\n      // it — counted and reported, never keyed to whichever row happened to be read last.\n      const gvv=String(line[iG]==null?'':line[iG]).trim();\n      if(gvv){ if(gvBy[s]!==undefined && gvBy[s]!==gvv) gvClash[s]=true; else gvBy[s]=gvv; }\n      const mkv=String(line[iK]==null?'':line[iK]).trim();\n      if(mkv){ if(mkBySetor[s]!==undefined && mkBySetor[s]!==mkv) mkClash[s]=true; else mkBySetor[s]=mkv; }\n      if(iDS!=null){ const lbl=String(line[iDS]==null?'':line[iDS]).trim(); if(lbl && !setorLabel[s]) setorLabel[s]=lbl; }\n      if(iDG!=null && gvv){ const lbl=String(line[iDG]==null?'':line[iDG]).trim(); if(lbl && !gvLabel[gvv]) gvLabel[gvv]=lbl; }\n      const raw=(iU==null)?'':line[iU];\n      const u=(iU==null)?'':ufNorm(raw);\n      if(iU!=null && String(raw==null?'':raw).trim() && !u) bad++;\n      // The same name twice in ONE setor under two UFs is the single case the reverted SQL cannot\n      // survive: mSQL groups by DS_MUNICIPIO alone, so both municipalities arrive summed into one row\n      // and no lookup can split them again. Counted and reported — never quietly keyed to whichever\n      // UF happened to be read last.\n      const kk=s+'|'+m;\n      // An UNRESOLVED uf must never outrank a resolved one. Reading a blank/bogus UF row first wrote ''\n      // here, and the next row carrying the real sigla was then rejected as a clash — so the key stayed\n      // falsy for good, ufFor() fell through, and the município degraded to a bare NAME key that matches\n      // no shape and no dominance entry. With DISTRITO/ZONA in the base a município spans several rows,\n      // so which row lands first is pure luck. A clash is only two RESOLVED siglas that disagree.\n      if(!u){ if(ufBy[kk]===undefined) ufBy[kk]=''; }\n      else if(!ufBy[kk]) ufBy[kk]=u;\n      else if(ufBy[kk]!==u) clash[kk]=true;\n      (by[s]=by[s]||new Set()).add(u?m+'/'+u:m); n++;\n    }\n    if(!n) throw new Error('base de municípios dominantes sem linhas válidas');\n    DOM.bySetor=by; DOM.ufBySetorMuni=ufBy; DOM.rows=n; DOM.setores=Object.keys(by).length;\n    // Reverse município→setor index (static, built once from the same CSV) — lets the GV-closing\n    // map gray out a geographically-touching GV with no live query, matching the \"GV/mercado\n    // hierarchy is organizational, read from the CSV only\" rule the rest of this level already follows.\n    { const revBy={},revAll={}; Object.entries(by).forEach(([s,ms])=>ms.forEach(m=>{ if(!revBy[m]) revBy[m]=s; (revAll[m]=revAll[m]||new Set()).add(s); })); DOM.setorByMuni=revBy; DOM.setoresByMuni=revAll; }\n    DOM.gvBySetor=gvBy; DOM.gvClashes=Object.keys(gvClash).length; DOM.gvCol=String(table[0][iG]||'').trim();\n    DOM.gvSetores=Object.keys(gvBy).length;\n    DOM.hasUf=(iU!=null); DOM.ufCol=(iU==null?null:String(table[0][iU]||'').trim());\n    DOM.badUf=bad; DOM.clashes=Object.keys(clash).length; DOM.headers=head.slice(0,40); DOM.error=null;\n    // ---- hierarquia: setor ⊂ GV ⊂ mercado, tudo derivado do MESMO csv que a regra de setor já\n    // confia — fechamento de GV/Mercado lê a mesma fonte, nunca uma segunda base paralela.\n    DOM.mercadoBySetor=mkBySetor; DOM.mercadoClashes=Object.keys(mkClash).length; DOM.mercadoCol=String(table[0][iK]||'').trim();\n    DOM.setorLabel=setorLabel; DOM.gvLabel=gvLabel;\n    const sectorsByGv={},sectorsByMercado={},gvsByMercado={},mercadoByGv={},gvMercClash={};\n    Object.keys(by).forEach(s=>{\n      const gv=gvBy[s], mk=mkBySetor[s];\n      if(gv) (sectorsByGv[gv]=sectorsByGv[gv]||new Set()).add(s);\n      if(mk) (sectorsByMercado[mk]=sectorsByMercado[mk]||new Set()).add(s);\n      if(gv&&mk){ if(mercadoByGv[gv]!==undefined && mercadoByGv[gv]!==mk) gvMercClash[gv]=true; else mercadoByGv[gv]=mk; }\n    });\n    Object.entries(mercadoByGv).forEach(([gv,mk])=>(gvsByMercado[mk]=gvsByMercado[mk]||new Set()).add(gv));\n    DOM.sectorsByGv=sectorsByGv; DOM.sectorsByMercado=sectorsByMercado; DOM.gvsByMercado=gvsByMercado;\n    DOM.mercadoByGv=mercadoByGv; DOM.gvMercadoClashes=Object.keys(gvMercClash).length;\n    return by;\n  })();\n  DOM.promise.catch(e=>{ DOM.promise=null; DOM.bySetor=null; DOM.error=e.message; renderAll(); });\n  return DOM.promise;\n}\n\nlet SESS={ snapshotRaw:null, snapshotSyncedAt:null, snapLiteral:null, eloMaxCycle:null, timeline:[], selected:0,\n           sessionId:null, sessionLabel:null, sessionsList:[], sectorsList:[], sectorBaseCache:{}, stage:'loading', dirtyFrom:null,\n           snapBusy:false, settings:{...DEFAULT_SETTINGS} };\n\nasync function q(sql){\n  let dbx,res;\n  try{\n    res=await d2.rush('AwW_11Nkg-dKksP7ic8V8').invoke({sql});\n  }catch(e){\n    throw new Error('Databricks indisponível (rush AwW_11Nkg-dKksP7ic8V8): '+e.message);\n  }\n  // The listener answers 200 with {sql} and NO `result` when its upstream probe times out: the\n  // handler builds `result: $result.data`, so an empty upstream simply drops the key rather than\n  // failing. Reading straight through to res.data.result.data reported \"Cannot read properties of\n  // undefined (reading 'data')\" — which reads like a bug in THIS component and sent a debugging\n  // session after the wrong thing. Name the layer that actually failed instead.\n  const payload=res&&res.data;\n  if(!payload) throw new Error('rush AwW_11Nkg-dKksP7ic8V8 não devolveu corpo — listener fora do ar?');\n  if(!payload.result) throw new Error('a sonda Databricks não respondeu: o listener AwW_11Nkg-dKksP7ic8V8 devolveu 200 sem `result`, o que normalmente é timeout do upstream (natura-x-app). O SQL não chegou a rodar.');\n  dbx=payload.result.data;\n  if(!dbx) throw new Error('sonda Databricks respondeu sem dados (result.data vazio).');\n  if(dbx.state && dbx.state!=='SUCCEEDED') throw new Error('Databricks '+dbx.state+': '+(dbx.error?.message||JSON.stringify(dbx.error)||'erro desconhecido'));\n  return dbx;\n}\nfunction rows(resp){const c=(resp.columns||[]).map(x=>x.name);\n  return (resp.rows||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\n// Stamps the UF onto every feature at load: the fetch is per-UF, so this is the ONLY point where a\n// shape's state is known for certain. Downstream the features are flattened into one array and the\n// origin would be unrecoverable. Persisted with the feature in saved sessions.\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);\n  const g=await r.json(); const U=String(uf).toUpperCase();\n  (g.features||[]).forEach(f=>{ f.__uf=U; });\n  return g;}\n\nfunction buildAdj(features){\n  // Encaixe exato de vértice (grid ~111m) resolve vizinhança DENTRO de uma UF, porque todos os\n  // municípios de uma UF saem do MESMO arquivo geoJSON, com a mesma malha de pontos. Atravessando\n  // fronteira de UF, os dois lados vêm de arquivos buscados/digitalizados SEPARADAMENTE — a mesma\n  // linha real de fronteira quase nunca cai nos mesmos vértices nos dois arquivos, e alargar o grid\n  // pra compensar (tentativa anterior) só troca \"não encontra vizinho de UF diferente\" por \"pode\n  // grudar município que só está perto, dentro da própria UF\" — não é uma correção geométrica de\n  // verdade, só empurra o problema. A solução de verdade é uma segunda checagem, geométrica (menor\n  // distância real entre os dois contornos, em km), aplicada SÓ a pares de UF diferente que o\n  // encaixe exato não achou — dentro da mesma UF o encaixe exato continua sendo a única fonte,\n  // sem alteração de comportamento nem de peso de voto.\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const rawPts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],out=[];\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>out.push(p))));return out;};\n  const feat={},idx={};\n  features.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome,f.__uf);\n    const raw=rawPts(f);\n    let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n    raw.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; });\n    feat[nm]={f,pts:pts(f),raw,bbox:{mnx,mny,mxx,mxy},uf:f.__uf};\n  });\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  // Limiar de \"encosta de verdade\" cruzando UF: cobre a folga típica entre dois arquivos\n  // digitalizados separadamente pra MESMA linha real de fronteira, sem juntar município que só\n  // está na vizinhança geral mas não faz fronteira de fato.\n  const TOUCH_KM=1.2;\n  const nbrs=nm=>{\n    const A=feat[nm]; if(!A) return [];\n    const c={};\n    A.pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    const exact=Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));\n    const seen=new Set(exact.map(e=>e.o));\n    const latK=Math.max(0.2,Math.cos(((A.bbox.mny+A.bbox.mxy)/2)*Math.PI/180));\n    const padLon=TOUCH_KM/111/latK, padLat=TOUCH_KM/111;\n    const extra=[];\n    for(const o in feat){\n      if(o===nm||seen.has(o)) continue;\n      const B=feat[o];\n      if(B.uf===A.uf) continue; // mesma UF: se o encaixe exato não achou, não está encostando\n      if(B.bbox.mxx<A.bbox.mnx-padLon||B.bbox.mnx>A.bbox.mxx+padLon||B.bbox.mxy<A.bbox.mny-padLat||B.bbox.mny>A.bbox.mxy+padLat) continue;\n      let best=Infinity;\n      outer: for(const p of A.raw){\n        for(const q of B.raw){\n          const d=Math.hypot((p[0]-q[0])*latK*111,(p[1]-q[1])*111);\n          if(d<best){ best=d; if(best<0.05) break outer; }\n        }\n      }\n      if(best<=TOUCH_KM) extra.push({o,w:1});\n    }\n    return exact.concat(extra);\n  };\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\n// ---------- snapshot pinning ----------\nasync function resolveSnapshotPin(){\n  const [rawR,liveCountR,eloR]=await Promise.all([\n    q(`SELECT MAX(DT_HISTORICO) snap FROM ${T} WHERE CD_PAIS=1`).then(rows),\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`).then(rows),\n    q(`SELECT MAX(operational_cycle) mx FROM ${ELO} WHERE country_code=1`).then(rows).catch(()=>[]),\n  ]);\n  const raw=rawR[0]&&rawR[0].snap;\n  const liveCount=+((liveCountR[0]||{}).n||0);\n  const eloMax=+((eloR[0]||{}).mx||0);\n  const LIVE_SUBQ=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n  const dbTs=(raw||'').replace('T',' ').replace(/Z$/,'');\n  const candidates=[`TIMESTAMP'${dbTs}'`,`'${dbTs}'`,`DATE'${dbTs}'`];\n  const tests=await Promise.all(candidates.map(c=>\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=${c}`).then(rows).then(r=>+((r[0]||{}).n||0)).catch(()=>-1)\n  ));\n  let chosen=LIVE_SUBQ;\n  for(let i=0;i<candidates.length;i++){ if(liveCount>0 && tests[i]===liveCount){ chosen=candidates[i]; break; } }\n  return {snapshotRaw:raw, snapLiteral:chosen, eloMaxCycle:eloMax, syncedAt:new Date().toISOString()};\n}\n// DISPLAY CONTRACT: the UI never prints the snapshot's own DT_HISTORICO (snapshotRaw). What the user\n// sees is `snapshotSyncedAt` — the date AND hour at which THIS instance last pulled/pinned the\n// snapshot it is querying. snapshotRaw is still resolved, still drives every SQL literal, and is\n// still persisted by saveSession/doShare; it is only kept off the screen.\nfunction fmtSyncedAt(iso){\n  if(!iso) return '—';\n  const d=new Date(iso);\n  if(isNaN(d.getTime())) return String(iso);\n  return d.toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});\n}\nasync function ensurePinned(){\n  if(SESS.snapLiteral) return;\n  const p=await resolveSnapshotPin();\n  SESS.snapshotRaw=p.snapshotRaw; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n  SESS.snapshotSyncedAt=p.syncedAt;\n}\n\n// ---------- world-state fold ----------\nfunction accMetrics(base,add){\n  const out=base?{...base}:blankM();\n  Object.keys(blankM()).forEach(k=>{ out[k]=(out[k]||0)+((add&&add[k])||0); });\n  return out;\n}\nfunction foldWorldState(timeline,uptoIdx){\n  const closedSectors=new Set(),muniOverride={},muniGeo={},muniRow={},muniLevel={},sectorInflow={},sectorInflowLevel={},sectorOrphanRows={};\n  // GV/mercado: setores e GVs não se dissolvem quando o dono deles fecha — eles só passam a\n  // responder a um destino novo. Por isso o rastro do cascade mora numa camada PRÓPRIA\n  // (secOverride/gvOverride), separada da camada de município do fechamento de setor — a\n  // hierarquia trava a sessão num nível só, então nunca as duas camadas têm conteúdo ao mesmo tempo.\n  const closedGvs=new Set(),secOverride={},gvInflow={};\n  const closedMercados=new Set(),gvOverride={},mercadoInflow={};\n  for(let k=0;k<uptoIdx;k++){\n    const mv=timeline[k]; if(!mv||!mv.sector||!mv.munis) continue;\n    const level=mv.level||'setor';\n    if(level==='gv'){\n      closedGvs.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        secOverride[u]=String(dest);\n        gvInflow[dest]=accMetrics(gvInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    if(level==='mercado'){\n      closedMercados.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        gvOverride[u]=String(dest);\n        mercadoInflow[dest]=accMetrics(mercadoInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    closedSectors.add(String(mv.sector));\n    Object.keys(mv.munis).forEach(m=>{\n      if(m==='__OUTROS__') return;\n      const dest=mv.assign[m]; if(!dest) return;\n      muniOverride[m]=String(dest);\n      if(mv.geoFeat&&mv.geoFeat[m]) muniGeo[m]=mv.geoFeat[m];\n      const rws=(mv.matrix||[]).filter(r=>mkey(r.municipio,r.uf)===m);\n      muniRow[m]=(muniRow[m]||[]).concat(rws);\n      if(mv.levelByMuni&&mv.levelByMuni[m]) muniLevel[m]={...(muniLevel[m]||{}),...mv.levelByMuni[m]};\n      sectorInflow[dest]=accMetrics(sectorInflow[dest],mv.munis[m]);\n      const lv=(mv.levelByMuni&&mv.levelByMuni[m])||{};\n      sectorInflowLevel[dest]=sectorInflowLevel[dest]||{};\n      Object.entries(lv).forEach(([nv,c])=>{ sectorInflowLevel[dest][nv]=(sectorInflowLevel[dest][nv]||0)+c; });\n    });\n    // Orphan CBs follow their leader's home sector, so later etapas must inherit them the same way\n    // the allocated municipalities are inherited — otherwise a stage's span would not match the\n    // base carried into the next one.\n    if(mv.orphanMetricsByGroup && Object.keys(mv.orphanMetricsByGroup).length){\n      const d=computeDerived(mv);\n      d.leaders.forEach(L=>{\n        if(!L.home) return;\n        const om=mv.orphanMetricsByGroup[L.grupo];\n        if(om) sectorInflow[L.home]=accMetrics(sectorInflow[L.home],om);\n        // sectorInflow only reaches a sector while it is a DESTINATION (it lands in baseMetrics).\n        // Keep the rows as well, so the sector still carries these CBs on the day it is the one\n        // being closed — that read goes through mv.matrix, which baseMetrics never touches.\n        const orws=mv.orphanRowsByGroup&&mv.orphanRowsByGroup[L.grupo];\n        if(orws&&orws.length) sectorOrphanRows[L.home]=(sectorOrphanRows[L.home]||[]).concat(orws);\n        const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n        if(ol){ sectorInflowLevel[L.home]=sectorInflowLevel[L.home]||{};\n          Object.entries(ol).forEach(([nv,c])=>{ sectorInflowLevel[L.home][nv]=(sectorInflowLevel[L.home][nv]||0)+c; }); }\n      });\n    }\n  }\n  return {closedSectors,muniOverride,muniGeo,muniRow,muniLevel,sectorInflow,sectorInflowLevel,sectorOrphanRows,\n          closedGvs,secOverride,gvInflow,closedMercados,gvOverride,mercadoInflow};\n}\n\n// ---------- per-movement compute ----------\nfunction aggregateMovement(mv){\n  const level=mv.level||'setor';\n  const munis={},leaders={};\n  const exSet=new Set((mv.excludedMunis||[]).map(x=>x.muni));\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  // Orphans = CBs in municipalities outside the sector's dominance base. They are NOT allocated\n  // geographically; they follow their own leader's home sector. Kept per leader group so every\n  // consumer (span, cascade, impact report) can route them the same way. Só existe no nível setor —\n  // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base).\n  mv.orphanByGroup={}; mv.orphanMetricsByGroup={}; mv.orphanRowsByGroup={};\n  // unidade que este nível redistribui: a própria município (setor fecha), o setor da linha (GV\n  // fecha), a GV do setor da linha (mercado fecha).\n  const unitOf=r=>level==='setor'?null:(level==='gv'?String(r.setor):(DOM.gvBySetor[String(r.setor)]||''));\n  mv.matrix.forEach(r=>{\n    const m=mkey(r.municipio,r.uf);\n    r._out=!mv.geoFeat[m];   // assign, never just set: absorbed rows are reused across etapas\n    const mm=level==='setor'?(r._out?'__OUTROS__':m):(unitOf(r)||'__OUTROS__');\n    munis[mm]=munis[mm]||{...blankM(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n    if(level==='setor' && r._out && exSet.has(m)){\n      mv.orphanByGroup[g]=(mv.orphanByGroup[g]||0)+ +r.cbs;\n      // The ROW too, not just the totals: when the leader's home sector is itself closed in a later\n      // etapa, that etapa rebuilds its matrix from SQL + absorbed municípios, and these CBs are in\n      // neither (their município never moved). Without the row they silently vanish from the sector\n      // they were just credited to — see sectorOrphanRows in foldWorldState.\n      (mv.orphanRowsByGroup[g]=mv.orphanRowsByGroup[g]||[]).push(r);\n      mv.orphanMetricsByGroup[g]=accMetrics(mv.orphanMetricsByGroup[g],\n        {cbs:+r.cbs,receita:+r.receita||0,rec_nat:+r.rec_nat||0,rec_avon:+r.rec_avon||0,pontos:+r.pontos||0,pedidos:+r.pedidos||0,ativas:+r.ativas||0});\n    }\n  });\n  // Level counts only exist per municipality, never per leader, so an excluded municipality's level\n  // mix is credited to the group holding most of its CBs (sub-threshold by definition, so usually one).\n  mv.orphanLevelByGroup={};\n  (mv.excludedMunis||[]).forEach(({muni})=>{\n    const lv=(mv.levelByMuni&&mv.levelByMuni[muni])||{}; if(!Object.keys(lv).length) return;\n    let bg=null,bc=-1;\n    mv.matrix.forEach(r=>{ if(mkey(r.municipio,r.uf)!==muni) return; const cc=+r.cbs; if(cc>bc){bc=cc;bg=String(r.grupo);} });\n    if(!bg) return;\n    mv.orphanLevelByGroup[bg]=mv.orphanLevelByGroup[bg]||{};\n    Object.entries(lv).forEach(([nv,cc])=>{ mv.orphanLevelByGroup[bg][nv]=(mv.orphanLevelByGroup[bg][nv]||0)+cc; });\n  });\n  mv.munis=munis; mv.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssignFor(mv){\n  const a={};\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=mv.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else if(mv.dest.length){ const c=mv.muniCentroid[m]; let best=mv.dest[0],bd=Infinity;\n      for(const s of mv.dest){ const sc=mv.secCentroid[s]; if(!sc||!c)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(mv,assign){\n  let d=0;\n  mv.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=mv.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,mv.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanMapFor(mv,a){const inc={};mv.dest.forEach(s=>inc[s]=0);\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=a[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  // Orphan CBs follow their leader's home sector, and that home depends on THIS candidate\n  // assignment — so they have to be counted here, or the optimizer would score and constrain a\n  // span that differs from the one the UI shows. Skipped entirely when there are no orphans.\n  if(mv.orphanByGroup&&Object.keys(mv.orphanByGroup).length){\n    mv.leaders.forEach(L=>{\n      const orphan=mv.orphanByGroup[L.grupo]||0; if(!orphan) return;\n      const byS={}; let tot=0;\n      for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s&&inc[s]!=null){ byS[s]=(byS[s]||0)+c; tot+=c; } }\n      if(!tot) return;   // no home to follow -> stranded, counted nowhere\n      const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n      if(inc[home]!=null) inc[home]+=orphan;\n    });\n  }\n  const sp={};mv.dest.forEach(s=>sp[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]);return sp;}\nfunction maxSpanOfFor(mv,a){return Math.max(...Object.values(spanMapFor(mv,a)),0);}\nfunction mergedSpanAll(){\n  const st=(SESS.settings&&SESS.settings.span)||{};\n  return {\n    setor:{...DEFAULT_SETTINGS.span.setor,...(st.setor||{})},\n    gv:{...DEFAULT_SETTINGS.span.gv,...(st.gv||{})},\n    mercado:{...DEFAULT_SETTINGS.span.mercado,...(st.mercado||{})},\n  };\n}\nfunction spanShortAll(){\n  const sp=mergedSpanAll();\n  const parts=['setor','gv','mercado'].map(lvl=>{ const s=spanShort(sp[lvl]); return s==='livre'?null:(LEVELS[lvl].label+' '+s); }).filter(Boolean);\n  return parts.length?parts.join(' · '):'livre';\n}\n// Sessões salvas antes de existir escala por nível guardavam targetSpan/maxSpan/minSpan soltos no\n// topo de settings, e esses três campos sempre foram só do setor — migra pra span.setor uma vez,\n// preservando null explícito (desligado) em vez de recair no padrão 2700/3500/2200.\nfunction migrateSpanSettings(raw){\n  raw=raw||{};\n  if(raw.span) return {\n    setor:{...DEFAULT_SETTINGS.span.setor,...raw.span.setor},\n    gv:{...DEFAULT_SETTINGS.span.gv,...raw.span.gv},\n    mercado:{...DEFAULT_SETTINGS.span.mercado,...raw.span.mercado},\n  };\n  const legacy=('targetSpan' in raw)||('maxSpan' in raw)||('minSpan' in raw);\n  return {\n    setor: legacy?{targetSpan:raw.targetSpan??null,maxSpan:raw.maxSpan??null,minSpan:raw.minSpan??null}:{...DEFAULT_SETTINGS.span.setor},\n    gv:{...DEFAULT_SETTINGS.span.gv},\n    mercado:{...DEFAULT_SETTINGS.span.mercado},\n  };\n}\nfunction spanLimits(mv){\n  // Span alvo/máximo/mínimo é uma escala POR NÍVEL de fechamento — cada nível olha só a sua.\n  const lvl=(mv&&mv.level)||'setor';\n  const st=mergedSpanAll()[lvl];\n  return {target:numOrNull(st.targetSpan),max:numOrNull(st.maxSpan),min:numOrNull(st.minSpan)};}\nfunction spanLimitsOn(lim){return !!(lim.target||lim.max||lim.min);}\nfunction spanViolationsOf(sp,lim){const over=[],under=[];\n  Object.entries(sp).forEach(([s,v])=>{ if(lim.max&&v>lim.max)over.push(s); if(lim.min&&v<lim.min)under.push(s); });\n  return {over,under};}\nfunction spanViolationsFor(mv,a){return spanViolationsOf(spanMapFor(mv,a),spanLimits(mv));}\n// With a target span the objective becomes RMS distance from the ideal size (punishes too big AND\n// too small). With no target we keep the legacy objective: shave the biggest sector.\nfunction spanCostFor(mv,a){const lim=spanLimits(mv),vals=Object.values(spanMapFor(mv,a));\n  if(!vals.length) return 0;\n  if(lim.target) return Math.sqrt(vals.reduce((s,v)=>s+(v-lim.target)**2,0)/vals.length);\n  return Math.max(...vals);}\n// ---------- ÓTIMO: objetivo multi-critério ----------\n// Three normalised objectives, each 0..1, so the weights below are directly comparable. The old\n// score added a span cost measured in CBs to a desloc measured in CBs, so on a large sector the\n// balance term was numerically ~100x the desloc term and silently decided everything on its own.\n//   balance — RMS spread of the resulting spans (against the target span when one is set,\n//             otherwise against their own mean), over that reference. 0 = every destination\n//             sector ends up the same size.\n//   dist    — how far each município travels, CB-weighted: half from the share of its outer\n//             border that faces the sector it was given to, half from centroid distance. 0 =\n//             every município went to a sector it physically touches. Adjacency used to be a\n//             pure yes/no constraint (contiguity); this is what makes it a preference too.\n//   orphan  — stranded orphan CBs (leader left without a home sector, so they land nowhere at\n//             all) plus CBs split away from their leader's home, over the CBs at stake.\n//             Stranded ones count OPT_STRAND_K times: a stranded CB leaves the model entirely,\n//             a deslocated one merely changes hands.\nconst OPT_W={balance:1, dist:0.85, orphan:1};\nconst OPT_STRAND_K=3;\nconst OPT_SIMS=24;             // simultaneous simulations per run, scaled down on big stages\nconst OPT_PASSES=14;           // local-search sweeps inside one simulation\nconst OPT_EVAL_BUDGET=120000;  // total candidate evaluations across the whole portfolio\nconst OPT_EXHAUSTIVE_MAX=60000;\nfunction optDistPrep(mv){\n  let mx=0;\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__') return; const c=mv.muniCentroid[m]; if(!c) return;\n    mv.dest.forEach(s=>{ const sc=mv.secCentroid[s]; if(!sc) return;\n      const d=Math.hypot(c[0]-sc[0],c[1]-sc[1]); if(d>mx) mx=d; }); });\n  return mx||1;\n}\nfunction optDistCost(mv,m,s,dmax){\n  const v=mv.allowed[m]||{}; let tot=0; for(const k in v) tot+=v[k];\n  const share=tot?((v[s]||0)/tot):0;\n  const c=mv.muniCentroid[m], sc=mv.secCentroid[s];\n  // Missing centroid on either side is neutral (0.5), never 0 — an unmeasurable move must not\n  // score as the cheapest one on the board.\n  const geo=(c&&sc)?Math.min(1,Math.hypot(c[0]-sc[0],c[1]-sc[1])/dmax):0.5;\n  return 0.5*(1-share)+0.5*geo;\n}\nfunction optEval(mv,a,dmax){\n  const lim=spanLimits(mv);\n  const sp=spanMapFor(mv,a), vals=Object.values(sp);\n  let balance=0;\n  if(vals.length){\n    const ref=lim.target||(vals.reduce((x,y)=>x+y,0)/vals.length);\n    if(ref) balance=Math.min(1,Math.sqrt(vals.reduce((s,v)=>s+(v-ref)**2,0)/vals.length)/ref);\n  }\n  let cbs=0,dist=0;\n  for(const m in a){ const o=mv.munis[m]; if(!o) continue; const w=+o.cbs||0; cbs+=w; dist+=w*optDistCost(mv,m,a[m],dmax); }\n  dist=cbs?(dist/cbs):0;\n  let stranded=0,desloc=0,tot=0;\n  mv.leaders.forEach(L=>{\n    const byS={}; let t=0;\n    for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s){ byS[s]=(byS[s]||0)+c; t+=c; } }\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    tot+=t+orphan;\n    if(!t){ stranded+=orphan; return; }   // no allocated CBs -> no home -> these orphans land nowhere\n    const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n    desloc+=t-(byS[home]||0);\n  });\n  const orphanTerm=tot?Math.min(1,(desloc+OPT_STRAND_K*stranded)/tot):0;\n  return { cost:OPT_W.balance*balance+OPT_W.dist*dist+OPT_W.orphan*orphanTerm,\n           balance, dist, desloc, stranded, sp, viol:spanViolationsOf(sp,lim) };\n}\nfunction optScoreFor(mv,a){ return optEval(mv,a,mv.__optDmax||optDistPrep(mv)).cost; }\n// Seeded PRNG on purpose. computeMovement runs the optimizer for EVERY stage and recomputeFrom\n// re-runs every stage downstream of an edit, so a Math.random() portfolio would hand back a\n// different allocation on each pass and a saved timeline would never reproduce.\nfunction optRng(seed){ let a=seed>>>0;\n  return ()=>{ a=(a+0x6D2B79F5)>>>0; let t=Math.imul(a^(a>>>15),1|a);\n    t=(t+Math.imul(t^(t>>>7),61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }\nfunction optSeed(mv,ms){ let h=2166136261>>>0;\n  const s=String(mv.sector)+'|'+ms.join(',')+'|'+mv.dest.join(',');\n  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);\n  return h>>>0; }\nfunction contigBadFor(mv,assign){\n  // Contiguidade geográfica só faz sentido no grão de município (setor fechando): setores e GVs\n  // não precisam formar uma mancha contínua quando GV/mercado fecha — cada setor/GV vai pro destino\n  // que faz sentido pra ele, sem exigir que o conjunto redistribuído continue \"grudado\".\n  if((mv.level||'setor')!=='setor') return [];\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(mv.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(mv.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\n// Usada pelo clique no mapa quando \"forçar conectividade de território\" está ligado: em vez de\n// avançar cegamente pro próximo destino da lista, pula candidatos que introduziriam uma quebra NOVA\n// (compara contra o baseBad de ANTES do clique, então uma quebra pré-existente alheia a este\n// município não trava o ciclo pra sempre). Sem candidato seguro, mantém o destino atual.\nfunction nextContigSafeDest(mv,m){\n  const n=mv.dest.length; if(!n) return mv.assign[m];\n  const cur=mv.assign[m];\n  const baseBad=new Set(contigBadFor(mv,mv.assign));\n  const i0=mv.dest.indexOf(cur);\n  for(let k=1;k<=n;k++){\n    const cand=mv.dest[((i0<0?0:i0)+k)%n];\n    if(cand===cur) continue;\n    const trial={...mv.assign,[m]:cand};\n    if(!contigBadFor(mv,trial).some(s=>!baseBad.has(s))) return cand;\n  }\n  return cur;\n}\nfunction greedyFor(mv,ms,opts){const lim=spanLimits(mv);const a={};\n  ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;\n    for(const s of opts[i]){a[m]=s;\n      // over-max only grows as we keep assigning, so it is safe to penalise mid-build;\n      // under-min shrinks, so penalising it here would just chase noise.\n      const over=lim.max?spanViolationsOf(spanMapFor(mv,a),{max:lim.max,min:null}).over.length:0;\n      const d=optScoreFor(mv,a)+(contigBadFor(mv,a).length?1e9:0)+over*1e6;\n      if(d<bd){bd=d;best=s;}}\n    a[m]=best;});\n  return a;}\nfunction optimizeFor(mv){\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__');\n  if(!ms.length||!mv.dest.length){ mv.optBadge=mv.dest.length?'':'sem setor destino identificado'; return {}; }\n  const opts=ms.map(m=>{const al=(mv.reachable[m]&&mv.reachable[m].length)?mv.reachable[m]:mv.dest.slice();return al.length?al:mv.dest.slice();});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  const lim=spanLimits(mv);\n  const dmax=optDistPrep(mv); mv.__optDmax=dmax;\n\n  // Feasibility ladder — same meaning as before, because renderSpanRule and the saved sessions\n  // both read mv.optTier: 0 = contíguo + max + min span, 1 = contíguo + max span, 2 = contíguo\n  // only. Every candidate any simulation produces is offered to all three tiers; we ship the\n  // strictest tier that got a solution, so an unreachable min/max relaxes loudly instead of\n  // stranding the stage with nothing.\n  const tier=[null,null,null],tSc=[Infinity,Infinity,Infinity],tEv=[null,null,null];\n  let feasible=0,evals=0;\n  const offer=(a,ev)=>{\n    if(contigBadFor(mv,a).length) return false;\n    feasible++;\n    const put=t=>{ if(ev.cost<tSc[t]){ tSc[t]=ev.cost; tier[t]={...a}; tEv[t]=ev; } };\n    put(2);\n    if(!ev.viol.over.length){ put(1); if(!ev.viol.under.length) put(0); }\n    return true;\n  };\n  const evalOf=a=>{ evals++; return optEval(mv,a,dmax); };\n\n  // Small enough to settle the question outright: enumerate every contiguous allocation and the\n  // winner is provably the best one under this objective. No search needed, so none is run.\n  const exhaustive=combos<=OPT_EXHAUSTIVE_MAX;\n  let sims=0;\n  if(exhaustive){\n    const cur={};\n    const rec=i=>{ if(i===ms.length){ offer(cur,evalOf(cur)); return; }\n      for(const s of opts[i]){ cur[ms[i]]=s; rec(i+1); } };\n    rec(0);\n  } else {\n    // Too big to enumerate, so run a portfolio of independent simulations instead. Each one is a\n    // seed assignment, then hill-climbing on single-município reassignments, then one perturbation\n    // and another climb to escape whatever local optimum it settled into. Different seeds land in\n    // different basins; the ladder above keeps the best across all of them.\n    const improve=a=>{\n      let ev=evalOf(a); offer(a,ev);\n      for(let pass=0;pass<OPT_PASSES;pass++){\n        let moved=false;\n        for(let i=0;i<ms.length;i++){\n          const m=ms[i],cur=a[m]; let best=cur,bev=ev;\n          for(const s of opts[i]){\n            if(s===cur) continue;\n            a[m]=s;\n            const e=evalOf(a);\n            // contiguity is a hard constraint, and the cheaper test runs first on purpose\n            if(e.cost<bev.cost-1e-12 && !contigBadFor(mv,a).length){ best=s; bev=e; }\n          }\n          a[m]=best;\n          if(best!==cur){ ev=bev; moved=true; offer(a,ev); }\n        }\n        if(!moved) break;\n      }\n    };\n    const rng=optRng(optSeed(mv,ms));\n    const perSim=Math.max(1,ms.length*Math.max(1,mv.dest.length)*OPT_PASSES);\n    const budget=Math.max(4,Math.min(OPT_SIMS,Math.floor(OPT_EVAL_BUDGET/perSim)||4));\n    const seeds=[];\n    if(mv.assign&&ms.every(m=>mv.assign[m])) seeds.push({...mv.assign});   // never regress on what is already on screen\n    seeds.push(autoAssignFor(mv));                                        // adjacency-first seed\n    seeds.push(greedyFor(mv,ms,opts));                                    // balance-first seed\n    while(seeds.length<budget){\n      // randomised construction biased toward nearby sectors — the bias keeps each seed sane, the\n      // randomness is what sends the runs into different basins\n      const a={};\n      ms.forEach((m,i)=>{ let bs=opts[i][0],bw=-Infinity;\n        for(const s of opts[i]){ const w=(1-optDistCost(mv,m,s,dmax))+rng()*0.9; if(w>bw){bw=w;bs=s;} }\n        a[m]=bs; });\n      seeds.push(a);\n    }\n    seeds.forEach(seed=>{\n      const a={...seed};\n      // a restored or legacy assign can still name a sector this stage no longer offers\n      ms.forEach((m,i)=>{ if(!opts[i].includes(a[m])) a[m]=opts[i][0]; });\n      improve(a);\n      const kick=Math.max(1,Math.round(ms.length*0.25));\n      for(let k=0;k<kick;k++){ const i=Math.floor(rng()*ms.length); a[ms[i]]=opts[i][Math.floor(rng()*opts[i].length)]; }\n      improve(a);\n    });\n    sims=seeds.length;\n  }\n\n  const t=tier.findIndex(x=>x);\n  if(t<0){ mv.optTier=-1; mv.optBadge='sem solução conectada — heurística'; return greedyFor(mv,ms,opts); }\n  const best=tier[t], ev=tEv[t]; mv.optTier=t;\n  const relax=[];\n  if(t>0 && lim.min) relax.push('span mínimo '+fmt(lim.min));\n  if(t>1 && lim.max) relax.push('span máximo '+fmt(lim.max));\n  mv.optStats={balance:ev.balance,dist:ev.dist,desloc:ev.desloc,stranded:ev.stranded,sims,evals,feasible,combos,exhaustive};\n  mv.optBadge=`ótimo · ${exhaustive?`espaço completo (${fmt(combos)})`:`${fmt(sims)} simulações`}`\n    +` · equilíbrio ±${Math.round(ev.balance*100)}% · adjacência ${Math.round((1-ev.dist)*100)}%`\n    +` · quebra ${fmt(ev.desloc)} CBs · ${fmt(ev.stranded)} fora do setor sem destino`\n    +` · pico ${fmt(maxSpanOfFor(mv,best))} CBs`\n    +(lim.target?` · alvo ${fmt(lim.target)}`:'')\n    +(relax.length?` · ⚠ inatingível: ${relax.join(' + ')}`:(spanLimitsOn(lim)?' · limites ok':''));\n  return best;\n}\nfunction computeDerived(mv){\n  const inc={},gained={},orph={};mv.dest.forEach(s=>{inc[s]=0;gained[s]=0;orph[s]=0;});\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  let desloc=0,orphanTotal=0,orphanStranded=0;\n  const L2=mv.leaders.map(L=>{const byS={};mv.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=mv.assign[m];if(s&&byS[s]!=null){byS[s]+=c;tot+=c;}}\n    // no allocated CBs => no home. Picking dest[0] by default would silently dump this leader's\n    // orphan CBs into an arbitrary sector, so we leave it null and report them as stranded.\n    const home=(tot>0&&mv.dest.length)?mv.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,mv.dest[0]):null;\n    const dl=tot-(home?byS[home]:0);desloc+=dl;if(home)gained[home]++;\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    orphanTotal+=orphan;\n    if(orphan){ if(home&&orph[home]!=null) orph[home]+=orphan; else orphanStranded+=orphan; }\n    return {...L,byS,home,desloc:dl,assignedTotal:tot,orphan,orphanHome:(orphan&&home)?home:null};});\n  const span={};mv.dest.forEach(s=>span[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]+orph[s]);\n  return {inc,gained,desloc,leaders:L2,span,orph,orphanTotal,orphanStranded};\n}\n\nasync function computeMovement(code,level,world){\n  level=level||'setor';\n  code=String(code);\n  const mv={sector:code,level,policy:'fora',view:'tobe',selMuni:null,optBadge:''};\n  const SNAP=SESS.snapLiteral;\n  // GV/mercado precisam da base ANTES de montar a query (pra saber quais CD_SETOR entram no IN);\n  // setor não precisa — mantém o timing original (a query dispara sem esperar o CSV).\n  let domBase=level==='setor'?null:await ensureDominantes();\n  const memberSectors=level==='setor'?[code]:memberSectorsOf(level,code,world);\n  if(!memberSectors.length) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const secList=memberSectors.join(',');\n\n  const mSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n     COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n     ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n     SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP}\n    GROUP BY DS_MUNICIPIO, CD_SETOR, CD_GRUPO`;\n  const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n  const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n     COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n     ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n    FROM ${ELO} WHERE country_code=1 AND sector_code IN (${secList}) AND operational_cycle<=${SESS.eloMaxCycle||999999} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n  const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n  const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, CD_SETOR, DS_NVL_PLAN_CRESC`;\n  const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n\n  const ownMatrix=rows(mR).filter(r=>+r.cbs>0);\n  mv.pureOwnMetrics=blankM();\n  ownMatrix.forEach(r=>{ mv.pureOwnMetrics.cbs+=+r.cbs; mv.pureOwnMetrics.receita+=+r.receita||0; mv.pureOwnMetrics.rec_nat+=+r.rec_nat||0; mv.pureOwnMetrics.rec_avon+=+r.rec_avon||0; mv.pureOwnMetrics.pontos+=+r.pontos||0; mv.pureOwnMetrics.pedidos+=+r.pedidos||0; mv.pureOwnMetrics.ativas+=+r.ativas||0; });\n\n  mv.levelByMuni={};\n  if(level==='setor') Object.entries(world.muniLevel||{}).forEach(([m,lv])=>{ if(world.muniOverride[m]===code) mv.levelByMuni[m]={...lv}; });\n\n  mv.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n  mv.levels=rows(lR);\n\n  let absorbedRows=[],inheritedOrphanRows=[];\n  if(level==='setor'){\n    Object.entries(world.muniOverride).forEach(([m,destSec])=>{ if(destSec===code && world.muniRow[m]) absorbedRows.push(...world.muniRow[m]); });\n    inheritedOrphanRows=(world.sectorOrphanRows&&world.sectorOrphanRows[code])||[];\n  }\n  mv.matrix=ownMatrix.concat(absorbedRows,inheritedOrphanRows);\n\n  const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n  const domUF=ufList[0]||'PR';\n  mv.domUF=domUF;\n  domBase=domBase||await ensureDominantes();\n  // Uma GV ou mercado pode espalhar por mais UFs do que \"a UF dominante + suas vizinhas\" cobre —\n  // toda UF que aparece na base de dominantes de QUALQUER setor-membro entra na janela de shapes.\n  const memberUFs=new Set([domUF]);\n  memberSectors.forEach(s=>{ (domBase[String(s)]||new Set()).forEach(entry=>{ const u=mUf(entry); if(u) memberUFs.add(u); }); });\n  const ufSet=new Set(memberUFs);\n  memberUFs.forEach(u=>(UF_BORDERS[u]||[]).forEach(b=>ufSet.add(b)));\n  const geos=await Promise.all([...ufSet].map(u=>geoUF(u).catch(()=>null)));\n  const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n  const adj=buildAdj(allFeat);\n\n  // A UF só vem da base de dominantes (setor real da linha × município), nunca de \"esse nome só\n  // apareceu numa UF carregada\" ou de um chute pra UF dominante do fechamento inteiro. Os dois\n  // eram seguros quando só 1-3 UFs entravam na janela (fechamento de setor); GV e principalmente\n  // mercado carregam dezenas de UFs de uma vez, então \"único entre as UFs carregadas\" deixa de\n  // significar \"único no Brasil\" — era exatamente esse chute que colocava um homônimo de outra UF\n  // espalhado longe do resto do mapa. Sem entrada na base, o município fica sem UF (mesmo\n  // tratamento de \"sem shape\" que já existe) em vez de arriscar aparecer no lugar errado.\n  const ufGuessed={};\n  const ufFor=(setorOf,muni)=>{const nm=norm(muni);\n    return (DOM.ufBySetorMuni&&DOM.ufBySetorMuni[String(setorOf)+'|'+nm])||'';};\n  mv.matrix.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n  rows(lmR).forEach(r=>{ const k=mkey(r.municipio,ufFor(r.setor,r.municipio)); (mv.levelByMuni[k]=mv.levelByMuni[k]||{})[r.nivel]=+r.cbs; });\n\n  const totBy={}; mv.matrix.forEach(r=>{const n=mkey(r.municipio,r.uf); totBy[n]=(totBy[n]||0)+ +r.cbs;});\n  const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>0).map(([m])=>m));\n  const withShape=[...footprint].filter(n=>adj.feat[n]||world.muniGeo[n]);\n  mv.cbsByMuni={...totBy}; mv.shapeMunis=withShape.slice();\n  mv.noShapeMunis=[...footprint].filter(n=>!(adj.feat[n]||world.muniGeo[n])).map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.noShapeCbs=mv.noShapeMunis.reduce((a,x)=>a+x.cbs,0);\n\n  // ---- RULE: quais municípios pertencem a este fechamento ----\n  // setor: exatamente a regra de hoje. GV/mercado: união da base de TODOS os setores-membro — a\n  // base já garante que um setor pertence a uma única GV/mercado, então não existe aqui o spillover\n  // fuzzy que município tem contra setor (por isso o orphan-handling abaixo é pulado nesses níveis).\n  let domOwn;\n  if(level==='setor'){\n    domOwn=domBase[code]||new Set();\n  } else {\n    domOwn=new Set();\n    memberSectors.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>domOwn.add(n)));\n  }\n  const inherited=level==='setor'?new Set(Object.keys(world.muniOverride||{}).filter(m=>world.muniOverride[m]===code)):new Set();\n  if(!domOwn.size && !inherited.size) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const belongs=n=>domOwn.has(n)||(!DOM.hasUf&&domOwn.has(mName(n)))||inherited.has(n);\n  mv.domCsvMunis=domOwn.size;\n  const dropped=level==='setor'?withShape.filter(n=>!belongs(n)):[];\n  if(withShape.length && dropped.length===withShape.length) throw new Error(`${LEVELS[level].label} ${code}: nenhum dos municípios com CBs está na base de dominantes — nada pra realocar`);\n  const droppedSet=new Set(dropped);\n  mv.excludedMunis=dropped.map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.excludedCbs=dropped.reduce((a,n)=>a+(totBy[n]||0),0);\n\n  const closingNames=withShape.filter(n=>!droppedSet.has(n));\n  const closingSet=new Set(closingNames);\n  mv.geoFeat={}; closingNames.forEach(n=>{ mv.geoFeat[n]=(adj.feat[n]&&adj.feat[n].f)||world.muniGeo[n]; });\n\n  // ---- município compartilhado por vários setores (São Paulo etc.): o setor fechando é dono só de\n  // UMA fatia dele, não da cidade inteira — sem isto a camada `.muni` interativa pintava (e deixava\n  // clicar) o município INTEIRO com a cor do destino escolhido, mesmo a maior parte dele pertencendo\n  // a outros setores sem nenhuma relação com este fechamento. mv.sharedMuniSlices carrega o resto da\n  // cidade pra camada de contexto — o GV-check e a classificação ficam pra mais adiante, quando\n  // sameParent/mv.rejectedCodes já existem nesta função.\n  mv.sharedMuniSlices=[];\n  if(level==='setor'){\n    closingNames.forEach(n=>{\n      const siblings=[...(DOM.setoresByMuni&&DOM.setoresByMuni[n]||new Set())].filter(s=>s!==code);\n      if(!siblings.length||!mv.geoFeat[n]) return;\n      const owners=[code,...siblings].sort((a,b)=>(+a-+b)||(a<b?-1:a>b?1:0));\n      const slices=sliceFeatureEqual(mv.geoFeat[n],owners.length);\n      const mine=slices[owners.indexOf(code)];\n      if(mine&&!mine.__empty) mv.geoFeat[n]=mine;\n      owners.forEach((s,i)=>{\n        if(s===code) return;\n        const sl=slices[i];\n        if(sl&&!sl.__empty) mv.sharedMuniSlices.push({feature:sl,sector:s});\n      });\n    });\n  }\n\n  // ---- unidade de realocação: a geometria é sempre município; a UNIDADE que troca de dono é o\n  // próprio município (setor fecha), o setor inteiro (GV fecha) ou a GV inteira (mercado fecha).\n  const municipioSetor={}; mv.matrix.forEach(r=>{ const n=mkey(r.municipio,r.uf); if(!municipioSetor[n]) municipioSetor[n]=String(r.setor); });\n  const resolveUnit=n=>{\n    if(level==='setor') return n;\n    const sOf=municipioSetor[n]; if(!sOf) return '';\n    return level==='gv'?sOf:(DOM.gvBySetor[sOf]||'');\n  };\n  mv.unitOfMuni={}; closingNames.forEach(n=>{ mv.unitOfMuni[n]=resolveUnit(n); });\n  mv.unitMunis={}; closingNames.forEach(n=>{ const u=mv.unitOfMuni[n]; if(!u) return; (mv.unitMunis[u]=mv.unitMunis[u]||[]).push(n); });\n  const units=level==='setor'?closingNames:Object.keys(mv.unitMunis);\n\n  const drawnCsv=new Set(closingNames.map(n=>DOM.hasUf?n:mName(n)));\n  const footKeyOf=k=>DOM.hasUf?(footprint.has(k)?k:null):([...footprint].find(x=>mName(x)===k)||null);\n  mv.csvMunisTotal=domOwn.size;\n  mv.ufGuessed={...ufGuessed};\n  if(level==='setor'){\n    mv.missingMunis=[...domOwn].filter(k=>!drawnCsv.has(k)).map(k=>{\n      const fk=footKeyOf(k);\n      const hasShape=!!(adj.feat[fk||k]||world.muniGeo[fk||k]);\n      const alias=[...footprint].filter(x=>x!==k&&mName(x)===mName(k));\n      let reason;\n      if(!fk&&alias.length) reason=`UF divergente — as CBs entraram na chave ${alias.join(' / ')} e a base diz ${k}`;\n      else if(!fk) reason=hasShape?'sem CBs neste setor no snapshot — desenhado como vizinho':'sem CBs neste setor no snapshot';\n      else if(!hasShape) reason='sem shape nas UFs carregadas';\n      else reason='chave não casou com a base de dominantes';\n      return {muni:k,cbs:(fk?totBy[fk]:alias.reduce((a,x)=>a+(totBy[x]||0),0))||0,reason:reason};\n    });\n  } else {\n    mv.missingMunis=[];\n  }\n\n  // ---- HARD RULE: destino tem de compartilhar a fronteira certa acima do que está fechando ----\n  // setor → mesma GV; GV → mesmo mercado; mercado → nada acima, qualquer vizinho vale.\n  const ownParent=parentOf(level,code);\n  if(level!=='mercado' && !ownParent) throw new Error(`${LEVELS[level].label} ${code} não tem ${level==='setor'?'CD_GV':'MERCADO'} na base de municípios dominantes — sem essa fronteira não há como garantir que os destinos ficam do lado certo da hierarquia`);\n  const sameParent=destCode=>!ownParent||parentOf(level,destCode)===ownParent;\n  mv.gv=ownParent;\n  const alreadyClosed=destCode=>level==='setor'?world.closedSectors.has(destCode):(level==='gv'?(world.closedGvs||new Set()).has(destCode):(world.closedMercados||new Set()).has(destCode));\n\n  mv.allowed={}; mv.allowedRaw={};\n  units.forEach(u=>{ mv.allowed[u]={}; mv.allowedRaw[u]={}; });\n  mv.rejectedCodes=new Set(); mv.rejectedInfo={};\n\n  const extVotes={}; const nbrNames=new Set(); let domSecOf={};\n  if(level==='setor'){\n    // setor: destino descoberto por ADJACÊNCIA GEOGRÁFICA de município — uma query ao vivo por\n    // vizinho, mas o perímetro de UM setor é pequeno o bastante pra valer a pena.\n    closingNames.forEach(n=>{\n      if(!adj.feat[n]) return;\n      adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o) && !droppedSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });\n    });\n    // Setor dominante de cada vizinho vem da base ESTÁTICA (DOM.setorByMuni), não de uma consulta ao\n    // vivo por contagem de CBs: a consulta antiga só devolvia linha pro município com CB ativo NESTE\n    // snapshot — um vizinho geograficamente real mas sem CB agora simplesmente não aparecia, então\n    // nunca virava rejeitado nem contexto (sumia do mapa em vez de aparecer cinza). A base de\n    // dominantes cobre todo município, com ou sem CB no momento, então nenhum vizinho fica de fora.\n    nbrNames.forEach(n=>{ domSecOf[n]=DOM.setorByMuni[n]; });\n    nbrNames.forEach(n=>{ if(world.muniOverride && world.muniOverride[n]) domSecOf[n]=world.muniOverride[n]; });\n\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    const gvHidden=new Set();\n    nbrNames.forEach(n=>{\n      const destCode=destUnitOfSector(level,domSecOf[n],world);\n      if(destCode && !sameParent(destCode)){\n        gvHidden.add(n);\n        mv.nbrCurrentSector[n]=destCode;\n        mv.rejectedCodes.add(destCode);\n        if(!mv.rejectedInfo[destCode]) mv.rejectedInfo[destCode]={code:destCode,parent:parentOf('setor',destCode)};\n        if(mv.gvRejected.indexOf(destCode)<0) mv.gvRejected.push(destCode);\n        return;\n      }\n      if(domSecOf[n]) mv.nbrCurrentSector[n]=destCode;\n    });\n    // Rejected neighbours KEEP their nbrNames/nbrCurrentSector entry — only their votes are stripped\n    // below, never their geometry — so the map can gray them in with a hover explaining the mismatch\n    // instead of just erasing them from the world.\n    Object.keys(extVotes).forEach(n=>{ extVotes[n]=extVotes[n].filter(v=>!gvHidden.has(v.o)); });\n\n    // Shared-município siblings (mv.sharedMuniSlices, built earlier) get the SAME hard GV rule as any\n    // other destination candidate — a slice of São Paulo owned by a different-GV setor is rejected\n    // context, never a silently-available destination.\n    mv.sharedMuniSlices.forEach(sl=>{\n      if(!sameParent(sl.sector)){\n        mv.rejectedCodes.add(sl.sector);\n        if(!mv.rejectedInfo[sl.sector]) mv.rejectedInfo[sl.sector]={code:sl.sector,parent:parentOf('setor',sl.sector)};\n        if(mv.gvRejected.indexOf(sl.sector)<0) mv.gvRejected.push(sl.sector);\n      }\n    });\n\n    mv.bridgeMunis=closingNames.slice();\n    mv.ignoredBridges=[];\n\n    const wAny={};\n    closingNames.forEach(n=>{\n      const u=n;\n      if(!mv.allowed[u]) return;\n      (extVotes[n]||[]).forEach(({o,w})=>{\n        const destCode=mv.nbrCurrentSector[o];\n        if(!destCode || destCode===code || alreadyClosed(destCode)) return;\n        mv.allowed[u][destCode]=(mv.allowed[u][destCode]||0)+w;\n        mv.allowedRaw[u][destCode]=(mv.allowedRaw[u][destCode]||0)+w;\n        wAny[destCode]=(wAny[destCode]||0)+w;\n      });\n    });\n    mv.dest=Object.keys(wAny).sort((a,b)=>wAny[b]-wAny[a]).slice(0,6);\n    if(!mv.dest.length){\n      mv.dest=[...new Set(Object.values(mv.nbrCurrentSector))].filter(destCode=>destCode&&destCode!==code&&!alreadyClosed(destCode)&&sameParent(destCode)).slice(0,6);\n      units.forEach(u=>{ if(mv.allowed[u]&&!Object.keys(mv.allowed[u]).length){ mv.dest.forEach(destCode=>mv.allowed[u][destCode]=1); } });\n    }\n  } else {\n    // GV/mercado: destino é SEMPRE todo irmão válido na hierarquia — NUNCA descoberto por\n    // adjacência geográfica de município. Uma GV/mercado pode ter território espalhado por\n    // dezenas de UFs sem qualquer conexão física entre as partes (a hierarquia aqui é\n    // organizacional, não geográfica) — \"vizinho por fronteira de município\" não é só caro (a\n    // query ao vivo vira um IN-list de centenas de municípios e estoura timeout: foi exatamente\n    // isso que quebrou o fechamento de um mercado espalhado) — é a pergunta errada. A fronteira que\n    // já importa (mesma GV / mesmo mercado) é imposta pela regra de parentesco logo acima; dentro\n    // dela QUALQUER irmão é candidato, e o otimizador decide pelo equilíbrio de span e continuidade\n    // de líder, nunca por proximidade física — geografia não pesa aqui.\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    mv.bridgeMunis=closingNames.slice();\n    mv.ignoredBridges=[];\n    const siblings=level==='gv'\n      ? [...(DOM.gvsByMercado[ownParent]||[])]\n      : Object.keys(DOM.sectorsByMercado||{});\n    mv.dest=siblings.filter(destCode=>destCode!==code && !alreadyClosed(destCode) && sameParent(destCode));\n    units.forEach(u=>{ mv.dest.forEach(destCode=>{ mv.allowed[u][destCode]=1; mv.allowedRaw[u][destCode]=1; }); });\n\n    // Geographically-touching GVs that fail the hierarchy rule (different mercado) — grayed on the\n    // map with a hover explaining why, using ONLY the static dominantes CSV (DOM.setorByMuni /\n    // DOM.gvBySetor / DOM.mercadoByGv, already in memory) — no live query, so the timeout that broke\n    // geographic discovery for a spread-out mercado closing (comment above) never re-enters here.\n    if(level==='gv'){\n      closingNames.forEach(n=>{\n        if(!adj.feat[n]) return;\n        adj.nbrs(n).forEach(({o})=>{\n          if(closingSet.has(o) || nbrNames.has(o)) return;\n          const sOf=DOM.setorByMuni[o]; if(!sOf) return;\n          const nGv=DOM.gvBySetor[sOf]; if(!nGv || nGv===code || sameParent(nGv)) return;\n          nbrNames.add(o);\n          mv.nbrCurrentSector[o]=nGv;\n          mv.rejectedCodes.add(nGv);\n          if(!mv.rejectedInfo[nGv]) mv.rejectedInfo[nGv]={code:nGv,parent:DOM.mercadoByGv[nGv]||''};\n          if(mv.gvRejected.indexOf(nGv)<0) mv.gvRejected.push(nGv);\n        });\n      });\n    }\n  }\n  mv.dest=mv.dest.filter(sameParent);\n\n  // ---- O QUE É DESENHADO: território COMPLETO de cada destino (não só a borda), sempre lido da\n  // mesma base de dominantes já usada pro lado que fecha.\n  const territoryOf=destCode=>{\n    const secs=memberSectorsOf(level,destCode,world);\n    const out=new Set();\n    secs.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>out.add(n)));\n    return out;\n  };\n  mv.muniOwners={};\n  mv.dest.forEach(destCode=>{\n    territoryOf(destCode).forEach(n=>{\n      // A município that's part of the CLOSING sector itself is already drawn (and clicked) via\n      // mv.geoFeat/unitAssign, further down — it isn't up-for-grabs context. Without this guard, a\n      // multi-sector city (São Paulo etc.) that also happens to be one of the closing sector's OWN\n      // municípios got its single \"current assignment\" fill painted right over the sliced context\n      // layer underneath — same IBGE polygon, and the closing layer draws on top.\n      if(closingSet.has(n)) return;\n      (mv.muniOwners[n]=mv.muniOwners[n]||[]).push(destCode);\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  // Draw every município we actually resolved a current sector for — not just the top-voted\n  // mv.dest candidates + rejectedCodes. A real same-GV neighbour sector that lost the vote race\n  // (more than 6 distinct neighbours, or simply outvoted) still gets classified further down as\n  // 'plain' context; filtering it out HERE instead left it entirely undrawn — a blank gap on the\n  // map exactly where a real, valid neighbouring sector is, instead of the grey context it should be.\n  // A neighbour whose current sector never resolved (no CRM row for it in this snapshot, a name\n  // mismatch, whatever) used to be dropped here entirely — invisible, no way to tell it apart from\n  // \"there's genuinely nothing there\". It still keeps its real shape; the render step below tags it\n  // 'unresolved' and shows the actual município name on hover instead, which is what you want when\n  // debugging why something didn't resolve.\n  // Um setor/GV rejeitado só entrava no mapa pelos municípios que sobreviveram à varredura de\n  // adjacência (os poucos que realmente encostam no que está fechando) — o resto do território dele,\n  // segundo a base de dominantes, nunca aparecia. Mesma expansão que mv.dest já recebe logo acima:\n  // território COMPLETO do setor/GV rejeitado, não só a fatia que faz fronteira.\n  mv.rejectedCodes.forEach(destCode=>{\n    territoryOf(destCode).forEach(n=>{\n      if(closingSet.has(n)) return;\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  // Setor fechando: um vizinho da MESMA GV que perdeu a votação (mais de 6 candidatos, ou simplesmente\n  // menos votado) nunca vira mv.dest nem mv.rejectedCodes — fica só como o único município que de fato\n  // encosta no fechamento, sem a expansão de território, e o resto dele sobra como buraco no mapa\n  // (nem colorido, nem cinza, nem contexto — simplesmente ausente). mv.allowedRaw carrega TODO\n  // candidato válido já descoberto, não só o top-6, então dá pra expandir os que sobraram também.\n  const plainCandidates=new Set();\n  units.forEach(u=>Object.keys(mv.allowedRaw[u]||{}).forEach(dc=>plainCandidates.add(dc)));\n  plainCandidates.forEach(destCode=>{\n    if(mv.dest.includes(destCode)) return;\n    territoryOf(destCode).forEach(n=>{\n      if(closingSet.has(n)) return;\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  mv.nbrFeat=[...nbrNames].map(n=>adj.feat[n]&&adj.feat[n].f).filter(Boolean);\n  units.forEach(u=>{ Object.keys(mv.allowed[u]||{}).forEach(destCode=>{ if(!mv.dest.includes(destCode)) delete mv.allowed[u][destCode]; }); });\n  mv.color={}; mv.dest.forEach((destCode,i)=>mv.color[destCode]=PALETTE[i%PALETTE.length]);\n\n  const accC={}; Object.entries(mv.nbrCurrentSector).forEach(([nb,destCode])=>{ if(!mv.dest.includes(destCode))return; const f=(adj.feat[nb]&&adj.feat[nb].f)||world.muniGeo[nb]; if(!f)return; const c=centroidLL(f); accC[destCode]=accC[destCode]||[0,0,0]; accC[destCode][0]+=c[0]; accC[destCode][1]+=c[1]; accC[destCode][2]++; });\n  mv.secCentroid={}; mv.dest.forEach(destCode=>{ if(accC[destCode]) mv.secCentroid[destCode]=[accC[destCode][0]/accC[destCode][2],accC[destCode][1]/accC[destCode][2]]; });\n\n  if(level==='setor'){\n    mv.muniCentroid={}; closingNames.forEach(n=>{ if(mv.geoFeat[n]) mv.muniCentroid[n]=centroidLL(mv.geoFeat[n]); });\n    mv.closeAdj={}; closingNames.forEach(n=>{ mv.closeAdj[n]=adj.feat[n]?adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o):[]; });\n  } else {\n    mv.muniCentroid={};\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const cs=ms.map(n=>mv.geoFeat[n]&&centroidLL(mv.geoFeat[n])).filter(Boolean);\n      if(cs.length) mv.muniCentroid[u]=[cs.reduce((a,c)=>a+c[0],0)/cs.length,cs.reduce((a,c)=>a+c[1],0)/cs.length];\n    });\n    mv.closeAdj={};\n    const unitSet=new Set(units);\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const touch=new Set();\n      ms.forEach(n=>{ if(!adj.feat[n]) return; adj.nbrs(n).forEach(({o})=>{ const ou=mv.unitOfMuni[o]; if(ou&&ou!==u&&unitSet.has(ou)) touch.add(ou); }); });\n      mv.closeAdj[u]=[...touch];\n    });\n  }\n  const comp={}; let ci=0; units.forEach(n=>{ if(comp[n]!=null)return; const st=[n]; comp[n]=ci; while(st.length){ const x=st.pop(); (mv.closeAdj[x]||[]).forEach(o=>{ if(comp[o]==null){ comp[o]=ci; st.push(o); } }); } ci++; });\n  const compAllowed={}; units.forEach(n=>{ const cid=comp[n]; (compAllowed[cid]=compAllowed[cid]||new Set()); Object.keys(mv.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s)); });\n  mv.reachable={}; units.forEach(n=>{ mv.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>mv.dest.includes(s)); });\n\n  if(mv.dest.length){\n    const destSectorMap={}; mv.dest.forEach(destCode=>{ destSectorMap[destCode]=memberSectorsOf(level,destCode,world); });\n    const allDestSecs=[...new Set(Object.values(destSectorMap).flat())];\n    const rList=allDestSecs.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    const secMetrics={}; rows(bR).forEach(r=>{ secMetrics[String(r.setor)]=r; if(!SESS.sectorBaseCache[String(r.setor)]) SESS.sectorBaseCache[String(r.setor)]=r; });\n    const secLevel={}; rows(blR).forEach(r=>{ const s=String(r.setor); (secLevel[s]=secLevel[s]||{})[r.nivel]=+r.cbs; });\n    mv.baseMetrics={}; mv.baseLevel={};\n    mv.dest.forEach(destCode=>{\n      const secs=destSectorMap[destCode];\n      const acc=blankM(); acc.setor=destCode;\n      secs.forEach(s=>{ const r=secMetrics[String(s)]; if(!r) return; Object.keys(blankM()).forEach(k=>{ acc[k]=(+acc[k]||0)+(+r[k]||0); }); });\n      mv.baseMetrics[destCode]=acc;\n      const lv={}; secs.forEach(s=>{ const l=secLevel[String(s)]; if(!l) return; Object.entries(l).forEach(([nv,c])=>{ lv[nv]=(lv[nv]||0)+c; }); });\n      mv.baseLevel[destCode]=lv;\n    });\n  } else { mv.baseMetrics={}; mv.baseLevel={}; }\n\n  mv.dest.forEach(destCode=>{\n    if(!mv.baseMetrics[destCode]) mv.baseMetrics[destCode]={setor:destCode,cbs:0,ativas:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0};\n    const inflowSrc=level==='setor'?world.sectorInflow:(level==='gv'?world.gvInflow:world.mercadoInflow);\n    const infl=inflowSrc&&inflowSrc[destCode];\n    if(infl) Object.keys(blankM()).forEach(k=>{ mv.baseMetrics[destCode][k]=(+mv.baseMetrics[destCode][k]||0)+(infl[k]||0); });\n    if(level==='setor'){\n      const inflLv=world.sectorInflowLevel&&world.sectorInflowLevel[destCode];\n      if(inflLv){ mv.baseLevel[destCode]=mv.baseLevel[destCode]||{}; Object.entries(inflLv).forEach(([nv,c])=>{ mv.baseLevel[destCode][nv]=(mv.baseLevel[destCode][nv]||0)+c; }); }\n    }\n  });\n\n  aggregateMovement(mv);\n  mv.assign=optimizeFor(mv);\n  return mv;\n}\n\n// ---------- cascade engine ----------\nasync function computeAt(i){\n  await ensurePinned();\n  const world=foldWorldState(SESS.timeline,i);\n  const sec=SESS.timeline[i].sector;\n  const level=SESS.timeline[i].level||'setor';\n  const mv=await computeMovement(sec,level,world);\n  SESS.timeline[i]=mv;\n}\nasync function recomputeFrom(i){\n  for(let k=i;k<SESS.timeline.length;k++){\n    loader(true,`recalculando etapa ${k+1} de ${SESS.timeline.length}…`);\n    try{ await computeAt(k); }\n    catch(e){\n      // A stage that failed to recompute still holds its PREVIOUS result, so the timeline would\n      // silently mix pre- and post-change etapas. Flag this stage and everything after it as stale\n      // (dashed chip + ⟳) so the state is visible and flushDirty retries from here.\n      SESS.dirtyFrom = SESS.dirtyFrom==null ? k : Math.min(SESS.dirtyFrom,k);\n      throw e;\n    }\n  }\n}\nasync function flushDirty(){\n  if(SESS.dirtyFrom==null) return;\n  const from=SESS.dirtyFrom; SESS.dirtyFrom=null;\n  loader(true,'recalculando etapas seguintes…');\n  try{ await recomputeFrom(from); }\n  catch(e){ $('#ds').textContent='erro ao recalcular cadeia: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n}\nasync function addMovement(level,sec){\n  level=level||'setor';\n  sec=(sec==null?'':String(sec)).trim();\n  if(!sec) return;\n  if(!movementAvailable(level)){ $('#ds').textContent=`Fechar ${LEVELS[level].label} ainda não está disponível.`; $('#ds').classList.add('err'); return; }\n  if(!hierarchyAllows(level)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; $('#ds').textContent=`esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`; $('#ds').classList.add('err'); return; }\n  const closedNow=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===level).map(mv=>mv.sector));\n  if(closedNow.has(sec)){ $('#ds').textContent=LEVELS[level].label+' '+sec+' já fechado(a) nesta timeline.'; $('#ds').classList.add('err'); return; }\n  if(level==='setor' && !SESS.sectorsList.some(s=>s.setor===sec)){ $('#ds').textContent='setor \"'+sec+'\" não existe no snapshot congelado.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  loader(true,`fechando ${lc(LEVELS[level].label)} `+sec+' — puxando dado do snapshot congelado…');\n  $('#liveState').classList.remove('err');\n  try{\n    SESS.timeline.push({sector:String(sec),level:level});\n    await computeAt(SESS.timeline.length-1);\n    SESS.selected=SESS.timeline.length-1;\n    $('#ds').classList.remove('err'); $('#ds').textContent='';\n  }catch(e){\n    SESS.timeline.pop();\n    $('#ds').textContent=`erro ao fechar ${lc(LEVELS[level].label)} `+sec+': '+e.message; $('#ds').classList.add('err');\n  } finally { loader(false); renderAll(); }\n}\nfunction removeFrom(i){\n  SESS.timeline.length=i;\n  SESS.selected=Math.min(SESS.selected,Math.max(0,SESS.timeline.length-1));\n  if(SESS.dirtyFrom!=null && SESS.dirtyFrom>=SESS.timeline.length) SESS.dirtyFrom=null;\n  renderAll();\n}\nasync function editSelected(mutator){\n  const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  mutator(mv);\n  if(SESS.selected<SESS.timeline.length-1){\n    SESS.dirtyFrom = SESS.dirtyFrom==null ? SESS.selected+1 : Math.min(SESS.dirtyFrom, SESS.selected+1);\n  }\n  renderAll();\n}\n\n// ---------- sectors list / picker ----------\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1) GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  SESS.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n}\nfunction renderSectorPicker(){\n  const ready=SESS.stage==='main';\n  const ob=$('#openMove'); if(ob) ob.disabled=!ready;\n}\n\n// ---------- movement modal (setor / GV / mercado) ----------\n// Hierarquia mercado > GV > setor: a sessão trava no nível da primeira movimentação. Misturar\n// níveis quebraria a leitura de \"antes/depois\" da cadeia — fechar uma GV já redistribui setores\n// inteiros pra outras GVs; fechar um setor isolado depois não bate mais com o mundo que sobrou.\n// de/a/no: contrações de \"de\"/\"a\"/\"em\" com o artigo de cada nível — GV é feminina (a Gerência de\n// Vendas), setor e mercado são masculinos. Errar o gênero aqui é o tipo de coisa que grita \"isto foi\n// feito pra setor e só remendado pros outros níveis\", exatamente o que este objeto existe pra evitar.\nconst LEVELS={\n  setor:{label:'Setor',codeLbl:'Setor a fechar',placeholder:'código do setor',childLabel:'Município',destLabel:'Setor',de:'do',a:'ao',no:'no',parentLabel:'GV',parentPhrase:'da mesma GV',parentWord:'gerência'},\n  gv:{label:'GV',codeLbl:'GV a fechar',placeholder:'código da GV',childLabel:'Setor',destLabel:'GV',de:'da',a:'à',no:'na',parentLabel:'Mercado',parentPhrase:'do mesmo mercado',parentWord:'mercado'},\n  mercado:{label:'Mercado',codeLbl:'Mercado a fechar',placeholder:'nome do mercado',childLabel:'GV',destLabel:'Mercado',de:'do',a:'ao',no:'no',parentLabel:null,parentPhrase:null,parentWord:null},\n};\n// GV é sigla — .toLowerCase() a transformaria em \"gv\", errado em qualquer posição da frase. Palavras\n// normais (Setor/Mercado/Município) minúsculam à vontade no meio de uma frase; GV nunca.\nconst LOWER={Setor:'setor',Mercado:'mercado',Município:'município',GV:'GV'};\nconst lc=w=>LOWER[w]||w.toLowerCase();\n// Hierarquia é uma catraca, não um cadeado: mercado(0) > GV(1) > setor(2). Fechar um nível mais\n// alto (número menor) trava PRA SEMPRE nesta sessão qualquer nível mais baixo (número maior) —\n// mas o próprio nível, ou um ainda mais alto, continua liberado. É assim que \"posso fechar setores\n// até fechar uma GV; posso fechar GVs até fechar um mercado\" funciona: nunca destrava de volta.\nconst LEVEL_RANK={mercado:0,gv:1,setor:2};\nfunction sessionMinRank(){\n  let r=LEVEL_RANK.setor;\n  SESS.timeline.forEach(mv=>{ const rk=LEVEL_RANK[mv.level||'setor']; if(rk<r) r=rk; });\n  return r;\n}\nfunction hierarchyAllows(lvl){ return LEVEL_RANK[lvl]<=sessionMinRank(); }\nfunction movementAvailable(lvl){ return lvl==='setor'||lvl==='gv'||lvl==='mercado'; }\n// ---------- hierarquia: setor ⊂ GV ⊂ mercado — tudo derivado da base de dominantes (DOM) ----------\n// memberSectors: o escopo de CD_SETOR que a query no CRM precisa cobrir pra fechar este código, em\n// qualquer nível — um setor é ele mesmo; uma GV é todo setor que a base lhe atribui; um mercado é\n// todo setor de toda GV que a base atribui a ele.\n// membros ATUAIS de um GV/mercado, um nível abaixo (setores de uma GV; GVs de um mercado) — nunca\n// só a foto estática do CSV: uma etapa anterior NESTA MESMA sessão pode já ter movido um filho pra\n// dentro ou pra fora deste código, e um fechamento seguinte tem de enxergar o mundo como ele está\n// agora, não como o CSV descreveu antes de qualquer movimentação.\nfunction currentMembersOf(level,code,world){\n  code=String(code);\n  // \"gv\": setores da GV (DOM.sectorsByGv). \"mercado\": GVs do mercado (DOM.gvsByMercado) — NUNCA\n  // sectorsByMercado, que pula a camada de GV inteira e faz memberSectorsOf tratar códigos de\n  // setor como se fossem códigos de GV (não batem em DOM.sectorsByGv, e o fechamento morre com\n  // \"não existe na base\" mesmo quando o mercado está lá).\n  const staticMembers=level==='gv'?(DOM.sectorsByGv[code]||new Set()):(DOM.gvsByMercado[code]||new Set());\n  const overrideMap=level==='gv'?(world&&world.secOverride):(world&&world.gvOverride);\n  const out=new Set(staticMembers);\n  if(overrideMap){\n    Object.entries(overrideMap).forEach(([child,dest])=>{\n      if(dest===code) out.add(child);\n      else if(staticMembers.has(child)) out.delete(child);\n    });\n  }\n  return [...out];\n}\n// setores que a query no CRM precisa cobrir pra fechar este código, em qualquer nível — setor é\n// ele mesmo; GV é seus setores ATUAIS; mercado é todo setor de toda GV ATUALMENTE sua (setor não\n// troca de GV sozinho numa sessão de mercado — só GV inteira troca de mercado).\nfunction memberSectorsOf(level,code,world){\n  code=String(code);\n  if(level==='setor') return [code];\n  if(level==='gv') return currentMembersOf('gv',code,world);\n  const gvs=currentMembersOf('mercado',code,world);\n  const out=new Set();\n  gvs.forEach(gv=>(DOM.sectorsByGv[gv]||new Set()).forEach(s=>out.add(s)));\n  return [...out];\n}\n// fronteira que um destino tem de respeitar: GV do setor que fecha, mercado da GV que fecha —\n// mercado não tem nada acima, então não existe regra pra ele. Estática de propósito: um setor não\n// troca de GV nem uma GV troca de mercado só por existirem etapas anteriores nesta sessão — quem\n// muda de dono é o NÍVEL ABAIXO do que está fechando, nunca o próprio código sendo avaliado aqui.\nfunction parentOf(level,code){\n  code=String(code);\n  if(level==='setor') return DOM.gvBySetor[code]||'';\n  if(level==='gv') return DOM.mercadoByGv[code]||'';\n  return '';\n}\n// dado o setor que uma município vizinha atualmente pertence (fato ao vivo do CRM, nunca muda de\n// verdade), a que CANDIDATO A DESTINO isso corresponde neste nível — considerando que uma etapa\n// anterior desta sessão pode já ter movido esse setor (GV fechando) ou essa GV (mercado fechando)\n// pra outro dono, o que a foto estática do CSV sozinha não enxergaria.\nfunction destUnitOfSector(level,setorCode,world){\n  // setorCode is undefined when the live neighbour query found no row for that município (e.g. zero\n  // CBs in this snapshot) — String(undefined) would silently turn that into the literal text\n  // \"undefined\" and the caller would treat it as a real (bogus) sector: shown as a fake rejected\n  // sector on the map, tooltip and all. Bail out to '' — no resolvable sector — instead.\n  if(setorCode==null||setorCode==='') return '';\n  setorCode=String(setorCode);\n  if(level==='setor') return setorCode;\n  if(level==='gv'){ const ov=world&&world.secOverride&&world.secOverride[setorCode]; return ov||DOM.gvBySetor[setorCode]||''; }\n  const gv=DOM.gvBySetor[setorCode]||'';\n  const ov=world&&world.gvOverride&&world.gvOverride[gv];\n  return ov||DOM.mercadoBySetor[setorCode]||'';\n}\nfunction currentMoveLevel(){ const r=root.querySelector('input[name=\"moveLevel\"]:checked'); return (r&&r.value)||'setor'; }\nfunction openMove(){\n  const minRank=sessionMinRank();\n  const lastLevel=SESS.timeline.length?(SESS.timeline[SESS.timeline.length-1].level||'setor'):'setor';\n  root.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>{\n    const lvl=r.value;\n    r.disabled=!movementAvailable(lvl)||!hierarchyAllows(lvl);\n    r.checked=hierarchyAllows(lastLevel)?lvl===lastLevel:lvl===Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank);\n  });\n  const note=$('#moveLockNote');\n  const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank)].label;\n  if(note) note.textContent=minRank<LEVEL_RANK.setor\n    ? `Esta sessão já fechou um nível mais alto na hierarquia — só \"${minLabel}\" (ou acima) continua disponível daqui pra frente.`\n    : 'Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.';\n  $('#moveCode').value='';\n  const err=$('#moveErr'); if(err){ err.textContent=''; err.classList.add('hidden'); }\n  const st=$('#moveStatus'); if(st) st.textContent='';\n  $('#moveBg').classList.remove('hidden');\n  renderMoveForm();\n  const c=$('#moveCancel'); if(c) c.focus();\n}\nfunction closeMove(){ const b=$('#moveBg'); if(b) b.classList.add('hidden'); }\nfunction renderMoveForm(){\n  root.querySelectorAll('#moveBg .opt-row').forEach(r=>r.classList.toggle('on',r.dataset.lvl===currentMoveLevel()));\n  const lvl=LEVELS[currentMoveLevel()];\n  $('#moveCodeLbl').textContent=lvl.codeLbl;\n  $('#moveCode').placeholder=lvl.placeholder;\n  loadMoveOptions();\n}\nasync function loadMoveOptions(){\n  const lvl=currentMoveLevel();\n  const dl=$('#moveCodeList'); if(!dl) return;\n  const closedSet=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===lvl).map(mv=>mv.sector));\n  if(lvl==='setor'){\n    dl.innerHTML=SESS.sectorsList.filter(s=>!closedSet.has(s.setor))\n      .map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n    return;\n  }\n  dl.innerHTML='';\n  try{ await ensureDominantes(); }catch(e){ return; }\n  if(lvl==='gv'){\n    const gvs=Object.keys(DOM.sectorsByGv||{}).filter(g=>!closedSet.has(g)).sort();\n    dl.innerHTML=gvs.map(g=>`<option value=\"${g}\">${g}${DOM.gvLabel[g]?' · '+esc(DOM.gvLabel[g]):''} · ${DOM.sectorsByGv[g].size} setor(es)</option>`).join('');\n  } else if(lvl==='mercado'){\n    const mks=Object.keys(DOM.sectorsByMercado||{}).filter(m=>!closedSet.has(m)).sort();\n    dl.innerHTML=mks.map(m=>`<option value=\"${m}\">${m} · ${(DOM.gvsByMercado[m]||new Set()).size} GV(s) · ${DOM.sectorsByMercado[m].size} setor(es)</option>`).join('');\n  }\n}\nfunction moveRandPick(){\n  const opts=[...($('#moveCodeList').options||[])].map(o=>o.value);\n  if(!opts.length) return;\n  $('#moveCode').value=opts[Math.floor(Math.random()*opts.length)];\n}\nasync function submitMove(){\n  const lvl=currentMoveLevel();\n  const code=($('#moveCode').value||'').trim();\n  const err=$('#moveErr');\n  const fail=msg=>{ if(err){ err.textContent=msg; err.classList.remove('hidden'); } };\n  if(err){ err.textContent=''; err.classList.add('hidden'); }\n  if(!movementAvailable(lvl)) return fail(`Fechar ${LEVELS[lvl].label} ainda não está disponível.`);\n  if(!hierarchyAllows(lvl)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; return fail(`Esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`); }\n  if(!code) return fail('Informe um código.');\n  closeMove();\n  await addMovement(lvl,code);\n}\nfunction renderStage(){\n  const isMain=SESS.stage==='main';\n  const cc=$('#confirmCard'), mw=$('#mainWrap');\n  const hdr=$('#topHeader'), rl=$('#timelineRail');\n  if(hdr) hdr.classList.toggle('hidden', !isMain);\n  if(rl) rl.classList.toggle('hidden', !isMain);\n  if(cc) cc.classList.toggle('hidden', isMain);\n  if(mw) mw.classList.toggle('hidden', !isMain || !SESS.timeline.length);\n  const cb=$('#confirmBody');\n  if(cb && !isMain) cb.innerHTML = SESS.snapshotRaw\n    ? `Snapshot congelado · atualizado em <b>${fmtSyncedAt(SESS.snapshotSyncedAt)}</b> · <b>${SESS.sectorsList.length}</b> setor(es) disponível(eis) pra fechar. Nenhuma consulta de detalhe roda até você confirmar e escolher o primeiro setor.`\n    : 'congelando snapshot…';\n}\n\n// ---------- session persistence ----------\n// ONE table (`timeline-shared-sessions`) holds BOTH kinds of row, so SESSIONS_DS and SHARED_DS\n// deliberately resolve to the SAME DataSet — the two names mark intent, not two stores:\n//   · working session  — carries `movements`; saveSession updates it in place\n//   · shared snapshot  — carries `sharedAt` + `stages`; doShare CREATES it and nothing updates it\n// listSessions MUST exclude the shared rows. They are write-once by contract, and if one showed\n// up in the picker, loading it and pressing Salvar would fire ds.update() straight at a frozen\n// snapshot. Filtering here is what keeps that unreachable — it is the guard, not a cosmetic.\n// Both lookups stay because a policy-gated DataSet's Process resolves late; same race the reader\n// component polls for. SESSIONS_DS lives at module scope so no call site can hit it in the TDZ.\nasync function listSessions(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return [];\n  await ds.refresh();\n  const recs=ds.records()||[];\n  return [...recs].filter(r=>!r.sharedAt).sort((a,b)=>new Date(b.updatedAt||0)-new Date(a.updatedAt||0));\n}\nasync function refreshSessionPicker(){\n  const list=await listSessions().catch(()=>[]);\n  SESS.sessionsList=list;\n  const opts=list.map(s=>`<option value=\"${s._id}\">${esc(s.label||'sem nome')} · ${fmt((s.movements||[]).length)} etapa(s)</option>`).join('')+`<option value=\"\">+ Nova sessão</option>`;\n  const sel=$('#sessionPick'); if(sel){ sel.innerHTML=opts; sel.value=SESS.sessionId||''; }\n  const csel=$('#confirmSessionPick'); if(csel){ csel.innerHTML=opts; csel.value=SESS.sessionId||''; }\n}\nasync function saveSession(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS));\n  if(!ds){ $('#sessStatus').textContent='dataset de sessões indisponível'; return; }\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra salvar ainda'; return; }\n  await flushDirty();\n  const label=SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR'));\n  const nowIso=new Date().toISOString();\n  const payload={\n    label,\n    settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n    snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt, snapLiteral:SESS.snapLiteral, eloMaxCycle:SESS.eloMaxCycle,\n    movements:SESS.timeline.map(mv=>({\n      sector:mv.sector, level:mv.level||'setor', assign:mv.assign, policy:mv.policy,\n      matrix:mv.matrix, trend:mv.trend, levels:mv.levels, levelByMuni:mv.levelByMuni,\n      pureOwnMetrics:mv.pureOwnMetrics, domUF:mv.domUF, dest:mv.dest, color:mv.color,\n      gv:mv.gv, gvRejected:mv.gvRejected,\n      missingMunis:mv.missingMunis, csvMunisTotal:mv.csvMunisTotal, ufGuessed:mv.ufGuessed,\n      baseMetrics:mv.baseMetrics, baseLevel:mv.baseLevel, geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat,\n      nbrCurrentSector:mv.nbrCurrentSector, allowed:mv.allowed, allowedRaw:mv.allowedRaw, reachable:mv.reachable,\n      optTier:mv.optTier,\n      cbsByMuni:mv.cbsByMuni, shapeMunis:mv.shapeMunis, excludedMunis:mv.excludedMunis, domCsvMunis:mv.domCsvMunis,\n      excludedCbs:mv.excludedCbs, noShapeMunis:mv.noShapeMunis, noShapeCbs:mv.noShapeCbs,\n      secCentroid:mv.secCentroid, muniCentroid:mv.muniCentroid, closeAdj:mv.closeAdj, optBadge:mv.optBadge,\n    })),\n    updatedAt:nowIso,\n  };\n  $('#sessStatus').textContent='salvando…';\n  try{\n    if(SESS.sessionId){\n      await ds.update({_id:SESS.sessionId,...payload});\n    } else {\n      payload.createdAt=nowIso;\n      const created=await ds.create(payload);\n      SESS.sessionId=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    }\n    SESS.sessionLabel=label;\n    await refreshSessionPicker();\n    $('#sessStatus').textContent='salvo ✓';\n  }catch(e){ $('#sessStatus').textContent='erro ao salvar: '+e.message; }\n  setTimeout(()=>{ if($('#sessStatus')) $('#sessStatus').textContent=''; },2200);\n}\n// Duplicate = drop sessionId so saveSession takes its CREATE branch instead of its update branch.\n// The source record is never written to, the copy gets a fresh _id and its own createdAt, and the\n// copy becomes the active session (so the next Salvar edits the copy, not the original).\nasync function renameSession(){\n  const next=prompt('Novo nome da sessão:', SESS.sessionLabel||'');\n  if(next==null) return;\n  const trimmed=next.trim(); if(!trimmed) return;\n  SESS.sessionLabel=trimmed;\n  if(!SESS.sessionId) return;   // ainda não salva — o novo nome só pega valer no próximo \"Salvar sessão\"\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return;\n  if($('#sessStatus')) $('#sessStatus').textContent='renomeando…';\n  try{\n    await ds.update({_id:SESS.sessionId, label:trimmed, updatedAt:new Date().toISOString()});\n    await refreshSessionPicker();\n    if($('#sessStatus')) $('#sessStatus').textContent='renomeado ✓';\n  }catch(e){ if($('#sessStatus')) $('#sessStatus').textContent='erro ao renomear: '+e.message; }\n  setTimeout(()=>{ const s=$('#sessStatus'); if(s&&(s.textContent==='renomeado ✓')) s.textContent=''; }, 2000);\n}\nasync function duplicateSession(){\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra duplicar ainda'; return; }\n  const prevId=SESS.sessionId, prevLabel=SESS.sessionLabel;\n  SESS.sessionId=null;\n  SESS.sessionLabel=(SESS.sessionLabel||'Sessão')+' (cópia)';\n  await saveSession();\n  if(!SESS.sessionId){\n    // saveSession swallows its own errors into #sessStatus, so a still-null id after the await is\n    // the only failure signal we get. Put the original identity back — otherwise a failed copy\n    // silently leaves the session detached, and the next Salvar would fork a new record instead\n    // of updating the one the user thought they were editing.\n    SESS.sessionId=prevId; SESS.sessionLabel=prevLabel;\n    await refreshSessionPicker().catch(()=>{});\n    return;\n  }\n  if($('#sessStatus')) $('#sessStatus').textContent='duplicado ✓';\n}\nasync function loadSession(id){\n  if(!id) return newSession();\n  const list=(SESS.sessionsList&&SESS.sessionsList.length)?SESS.sessionsList:await listSessions().catch(()=>[]);\n  const rec=list.find(s=>String(s._id)===String(id));\n  if(!rec) return newSession();\n  loader(true,'carregando sessão…');\n  SESS.sessionId=rec._id; SESS.sessionLabel=rec.label;\n  SESS.snapshotRaw=rec.snapshotRaw; SESS.snapshotSyncedAt=rec.snapshotSyncedAt||null; SESS.snapLiteral=rec.snapLiteral; SESS.eloMaxCycle=rec.eloMaxCycle;\n  SESS.settings={...DEFAULT_SETTINGS,...(rec.settings||{}),span:migrateSpanSettings(rec.settings)};\n  SESS.timeline=(rec.movements||[]).map(m=>({sector:String(m.sector),level:m.level||'setor'}));\n  SESS.selected=0; SESS.sectorBaseCache={}; SESS.stage='main'; SESS.dirtyFrom=null;\n  try{\n    for(let i=0;i<SESS.timeline.length;i++){\n      const saved=(rec.movements||[])[i]||{};\n      // Sessions saved before the UF became part of the key hold bare-name keys in matrix rows,\n      // assign, geoFeat and closeAdj alike. Restoring that cache would mix key shapes the moment the\n      // stage is recomputed — and it would preserve the very collision this fixes — so those stages\n      // are replayed against the pinned snapshot and the manual assignment is remapped by name.\n      const legacyKeys=!!(saved.matrix&&saved.matrix.length&&saved.matrix.every(r=>r&&r.uf==null));\n      // Sessões salvas antes da regra de gerência podem carregar um destino de outra GV no cache.\n      // Restaurar esse cache manteria o setor proibido na tela para sempre, então essas etapas são\n      // reprocessadas contra o snapshot fixado — igual ao tratamento das chaves legadas.\n      const crossGv=await savedCrossesGv(saved);\n      if(saved.matrix&&!legacyKeys&&!crossGv){\n        loader(true,`restaurando etapa ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector} (cache, sem consulta)…`);\n        const mv={sector:String(saved.sector), level:saved.level||'setor', policy:saved.policy||'fora', view:'tobe', selMuni:null, optBadge:saved.optBadge||''};\n        mv.matrix=saved.matrix; mv.trend=saved.trend||[]; mv.levels=saved.levels||[]; mv.levelByMuni=saved.levelByMuni||{};\n        mv.pureOwnMetrics=saved.pureOwnMetrics||blankM(); mv.domUF=saved.domUF; mv.dest=saved.dest||[]; mv.color=saved.color||{};\n        mv.baseMetrics=saved.baseMetrics||{}; mv.baseLevel=saved.baseLevel||{}; mv.geoFeat=saved.geoFeat||{}; mv.nbrFeat=saved.nbrFeat||[];\n        mv.nbrCurrentSector=saved.nbrCurrentSector||{}; mv.allowed=saved.allowed||{}; mv.reachable=saved.reachable||{};\n        mv.allowedRaw=saved.allowedRaw||saved.allowed||{};\n        mv.optTier=(saved.optTier==null?0:saved.optTier);\n        mv.gv=saved.gv||''; mv.gvRejected=saved.gvRejected||[];\n        mv.missingMunis=saved.missingMunis||[]; mv.csvMunisTotal=saved.csvMunisTotal||0;\n        mv.ufGuessed=saved.ufGuessed||{};\n        mv.cbsByMuni=saved.cbsByMuni||{}; mv.shapeMunis=saved.shapeMunis||[];\n        mv.excludedMunis=saved.excludedMunis||[]; mv.excludedCbs=saved.excludedCbs||0; mv.domCsvMunis=saved.domCsvMunis||0;\n        mv.noShapeMunis=saved.noShapeMunis||[]; mv.noShapeCbs=saved.noShapeCbs||0;\n        mv.secCentroid=saved.secCentroid||{}; mv.muniCentroid=saved.muniCentroid||{}; mv.closeAdj=saved.closeAdj||{};\n        aggregateMovement(mv);\n        mv.assign=saved.assign||{};\n        if((mv.level||'setor')==='setor') mv.dest.forEach(s=>{ if(!SESS.sectorBaseCache[s]) SESS.sectorBaseCache[s]=mv.baseMetrics[s]; });\n        SESS.timeline[i]=mv;\n      } else {\n        loader(true,`replay ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector}…`);\n        await computeAt(i);\n        if(saved.assign) SESS.timeline[i].assign=legacyKeys?remapAssign(SESS.timeline[i],saved.assign):saved.assign;\n        if(saved.policy) SESS.timeline[i].policy=saved.policy;\n      }\n    }\n    await loadSectors();\n  }catch(e){ $('#ds').textContent='erro ao carregar sessão: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n  renderAll();\n}\n// Re-points a legacy bare-name assignment at the NAME/UF key it can only mean. When a name resolves\n// to more than one município in the recomputed stage the manual choice is genuinely ambiguous —\n// dropping it lets the optimizer allocate instead of guessing on the operator's behalf.\n// True when a cached stage names a destination outside the closing sector's gerência de vendas.\n// Judges the cache by TODAY's rule, reading the (memoised) dominantes base rather than trusting what\n// the session was saved with. Unknown GV on the closing sector answers false: the stage then loads\n// from cache instead of being replayed into the hard throw in computeMovement, which would leave the\n// operator staring at a broken session with no way back.\nasync function savedCrossesGv(saved){\n  if(!saved||!saved.sector||!(saved.dest||[]).length) return false;\n  let gvBy={};\n  try{ await ensureDominantes(); gvBy=DOM.gvBySetor||{}; }catch(e){ return false; }\n  const own=gvBy[String(saved.sector)]||'';\n  if(!own) return false;\n  if(saved.dest.some(s=>(gvBy[String(s)]||'')!==own)) return true;\n  // A stage saved before foreign-GV neighbours stopped being drawn can hold a perfectly clean dest and\n  // STILL carry their shapes in nbrFeat/nbrCurrentSector — it would repaint the old map straight from\n  // cache. An unknown gerência is not evidence of a foreign one, so only a known, differing GV replays.\n  return Object.values(saved.nbrCurrentSector||{}).some(s=>{ const g=gvBy[String(s)]||''; return g&&g!==own; });\n}\nfunction remapAssign(mv,savedAssign){\n  const keys=Object.keys(mv.munis||{}).filter(k=>k!=='__OUTROS__');\n  const out={};\n  Object.entries(savedAssign||{}).forEach(([k,v])=>{\n    if(keys.indexOf(k)>=0){ out[k]=v; return; }\n    const cand=keys.filter(x=>mName(x)===mName(k));\n    if(cand.length===1) out[cand[0]]=v;\n  });\n  return out;\n}\nfunction newSession(){\n  SESS.sessionId=null; SESS.sessionLabel=null; SESS.timeline=[]; SESS.selected=0; SESS.sectorBaseCache={};\n  SESS.settings={...DEFAULT_SETTINGS};\n  SESS.snapshotRaw=null; SESS.snapshotSyncedAt=null; SESS.snapLiteral=null; SESS.eloMaxCycle=null; SESS.stage='loading'; SESS.dirtyFrom=null;\n  (async()=>{\n    loader(true,'iniciando nova sessão…');\n    try{ await Promise.all([ensurePinned(), loadSectors()]); SESS.stage='confirm'; }\n    catch(e){ setError('erro init: '+e.message); }\n    finally{ loader(false); renderAll(); }\n  })();\n}\n\n// ---------- snapshot refresh ----------\n// Deliberately breaks the frozen-snapshot guarantee, on demand: re-resolves DT_HISTORICO and\n// replays the whole timeline against the new one. Skips the replay when the snapshot has not\n// actually moved, so a stray click on a long timeline costs one cheap probe instead of ~8 queries\n// per etapa.\nasync function refreshSnapshot(){\n  if(SESS.stage!=='main' || SESS.snapBusy) return;\n  SESS.snapBusy=true;\n  const prevRaw=SESS.snapshotRaw;\n  const pill=$('#liveState'); if(pill){ pill.classList.add('busy'); pill.textContent='◌ atualizando snapshot…'; }\n  loader(true,'buscando o snapshot mais recente…');\n  let ok=false;\n  try{\n    const p=await resolveSnapshotPin();\n    if(!p.snapshotRaw) throw new Error('não foi possível resolver DT_HISTORICO');\n    const same=String(p.snapshotRaw)===String(prevRaw);\n    SESS.snapshotRaw=p.snapshotRaw; SESS.snapshotSyncedAt=p.syncedAt; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n    await loadSectors();   // the sector list follows the live max, so it can have moved too\n    if(same){\n      $('#ds').textContent=`snapshot já era o mais recente · atualizado em ${fmtSyncedAt(p.syncedAt)} — nada recalculado`;\n    } else if(SESS.timeline.length){\n      SESS.dirtyFrom=null;\n      SESS.sectorBaseCache={};   // baselines come from the snapshot, so every one of them changed\n      loader(true,'snapshot novo — recalculando a timeline…');\n      await recomputeFrom(0);\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)} · ${fmt(SESS.timeline.length)} etapa(s) recalculada(s)`;\n    } else {\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)}`;\n    }\n    $('#ds').classList.remove('err');\n    ok=true;\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?null:SESS.dirtyFrom+1);\n    $('#ds').textContent='erro ao atualizar o snapshot'+(at?` na etapa ${at} — etapa ${at} em diante ficou desatualizada (⟳)`:'')+': '+e.message;\n    $('#ds').classList.add('err');\n  }finally{\n    SESS.snapBusy=false;\n    if(pill) pill.classList.remove('busy');\n    loader(false); renderAll();\n    if(ok) setTimeout(()=>{ const d=$('#ds'); if(d&&!d.classList.contains('err')) d.textContent=''; },7000);\n  }\n}\n\n// ---------- session options ----------\nfunction readOptForm(){\n  const scopeOf=lvl=>({\n    targetSpan:numOrNull($('#optTargetSpan_'+lvl).value),\n    maxSpan:numOrNull($('#optMaxSpan_'+lvl).value),\n    minSpan:numOrNull($('#optMinSpan_'+lvl).value),\n  });\n  // Mercado span has no UI control anymore — carry the session's current value through untouched\n  // instead of reading DOM inputs that no longer exist.\n  const fc=root.querySelector('input[name=\"forceContig\"]:checked');\n  return {span:{setor:scopeOf('setor'), gv:scopeOf('gv'), mercado:mergedSpanAll().mercado},\n    forceContiguity: fc?fc.value==='1':!!(SESS.settings&&SESS.settings.forceContiguity)};\n}\nfunction spanScopeError(sc,label){\n  if(sc.minSpan&&sc.maxSpan&&sc.minSpan>sc.maxSpan) return `${label} — span mínimo (${fmt(sc.minSpan)}) não pode ser maior que o máximo (${fmt(sc.maxSpan)}).`;\n  if(sc.targetSpan&&sc.maxSpan&&sc.targetSpan>sc.maxSpan) return `${label} — span alvo (${fmt(sc.targetSpan)}) não pode passar do máximo (${fmt(sc.maxSpan)}).`;\n  if(sc.targetSpan&&sc.minSpan&&sc.targetSpan<sc.minSpan) return `${label} — span alvo (${fmt(sc.targetSpan)}) não pode ficar abaixo do mínimo (${fmt(sc.minSpan)}).`;\n  return '';\n}\nfunction spanFormError(f){\n  return spanScopeError(f.span.setor,'Setor')||spanScopeError(f.span.gv,'GV')||spanScopeError(f.span.mercado,'Mercado');\n}\nfunction renderOptPreview(){\n  const el=$('#optPreview'); if(!el) return;\n  const f=readOptForm();\n  const err=spanFormError(f), ee=$('#optSpanErr');\n  if(ee){ ee.textContent=err; ee.classList.toggle('hidden',!err); }\n  const ap=$('#optApply'); if(ap) ap.disabled=!!err;\n  const mv=SESS.timeline[SESS.selected];\n  if(!mv||!mv.munis){ el.innerHTML='Nenhuma etapa calculada ainda — as regras passam a valer no próximo fechamento.'; return; }\n  const lvl=mv.level||'setor', sc=f.span[lvl];\n  let h=`<b style=\"color:var(--ink)\">Prévia — etapa ${SESS.selected+1}, ${lc(LEVELS[lvl].label)} ${mv.sector}</b><br>`;\n  if(sc.targetSpan||sc.maxSpan||sc.minSpan){\n    const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,{target:sc.targetSpan,max:sc.maxSpan,min:sc.minSpan});\n    h+=`Span dos destinos como está hoje: ${mv.dest.map(s=>s+' <b>'+fmt(sp[s])+'</b>').join(' · ')||'–'}`;\n    if(v.over.length||v.under.length) h+=`<br><span style=\"color:var(--closing)\">Fora dos limites: ${[...v.over.map(s=>s+' ▲ acima do máx'),...v.under.map(s=>s+' ▼ abaixo do mín')].join(', ')}</span> — o modelo ÓTIMO tenta corrigir no recálculo.`;\n    else h+=` <span style=\"color:var(--ok)\">— já dentro dos limites</span>`;\n  } else {\n    h+=`Sem limite de span pra ${lc(LEVELS[lvl].label)} — ${lc(LEVELS[lvl].destLabel)} destino pode ficar de qualquer tamanho.`;\n  }\n  if(SESS.timeline.length) h+=`<br><span style=\"margin-top:4px;display:inline-block\">Aplicar <b>recalcula as ${fmt(SESS.timeline.length)} etapa(s)</b> contra o snapshot congelado — os destinos ajustados à mão serão refeitos pelo modelo ÓTIMO.</span>`;\n  el.innerHTML=h;\n}\nfunction openOptions(){\n  const sp=mergedSpanAll();\n  ['setor','gv'].forEach(lvl=>{\n    $('#optTargetSpan_'+lvl).value=sp[lvl].targetSpan||'';\n    $('#optMaxSpan_'+lvl).value=sp[lvl].maxSpan||'';\n    $('#optMinSpan_'+lvl).value=sp[lvl].minSpan||'';\n  });\n  const fcVal=(SESS.settings&&SESS.settings.forceContiguity)?'1':'0';\n  const fcEl=root.querySelector('input[name=\"forceContig\"][value=\"'+fcVal+'\"]');\n  if(fcEl) fcEl.checked=true;\n  $('#optApply').disabled=false;\n  $('#optSessName').textContent=SESS.sessionLabel||'sessão não salva';\n  $('#optStatus').textContent='';\n  $('#optionsBg').classList.remove('hidden');\n  renderOptPreview();\n  const c=$('#optCancel'); if(c) c.focus();\n}\nfunction closeOptions(){ const b=$('#optionsBg'); if(b) b.classList.add('hidden'); }\nasync function applyOptions(){\n  const f=readOptForm();\n  if(spanFormError(f)) return;\n  const prevSp=mergedSpanAll();\n  const changed=['setor','gv','mercado'].some(lvl=>\n    numOrNull(prevSp[lvl].targetSpan)!==f.span[lvl].targetSpan\n    || numOrNull(prevSp[lvl].maxSpan)!==f.span[lvl].maxSpan\n    || numOrNull(prevSp[lvl].minSpan)!==f.span[lvl].minSpan\n  );\n  SESS.settings={...SESS.settings,span:f.span,forceContiguity:f.forceContiguity};\n  closeOptions();\n  if(!changed || !SESS.timeline.length){ renderAll(); return; }\n  SESS.dirtyFrom=null;\n  SESS.sectorBaseCache={};   // the recompute re-derives every baseline; keeping the old ones would\n                             // leave \"Impacto acumulado\" comparing against pre-change numbers\n  loader(true,'aplicando novos limites de span — recalculando timeline…');\n  try{\n    await recomputeFrom(0);\n    $('#ds').textContent=''; $('#ds').classList.remove('err');\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?0:SESS.dirtyFrom)+1;\n    $('#ds').textContent=`erro ao aplicar o limite na etapa ${at}: ${e.message} — etapa ${at} em diante ficou desatualizada (⟳)`;\n    $('#ds').classList.add('err');\n  }finally{ loader(false); renderAll(); }\n}\n\nconst SHARED_DS='timeline-shared-sessionsDataSet';\nconst AUTH_REF='timelineSimAuthFinal';   // single source of truth for the auth strategy reference\nlet SHARE={busy:false,url:null,last:null};\n// Serialise a PRESENTATION model, not the raw session, so the reader stays purely presentational\n// and never re-derives spans/desloc/orphans — no second copy of this engine to keep in sync.\n// NOTE: METRICS_DEF[2] is the money marker; read it as !!md[2] (a literal dollar sign in this\n// position trips the upsert's parse check).\nfunction buildShareStages(){\n  return SESS.timeline.map((mv,i)=>{\n    const c=computeDerived(mv);\n    const incBy={};\n    mv.dest.forEach(s=>{ incBy[s]=Object.fromEntries(METRICS_DEF.map(md=>[md[0],0])); });\n    Object.entries(mv.munis).forEach(([m,o])=>{\n      if(m==='__OUTROS__') return;\n      const s=mv.assign[m]; if(!s||!incBy[s]) return;\n      METRICS_DEF.forEach(md=>{ incBy[s][md[0]]+=o[md[0]]||0; });\n    });\n    const saiTot={};\n    METRICS_DEF.forEach(md=>{\n      const k=md[0];\n      saiTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,p)=>a+((p[1][k])||0),0);\n    });\n    // orphans follow their leader's home: they enter that sector AND leave the closing one\n    c.leaders.forEach(L=>{\n      const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n      if(!om||!L.home) return;\n      METRICS_DEF.forEach(md=>{ const k=md[0];\n        if(incBy[L.home]) incBy[L.home][k]+=om[k]||0;\n        saiTot[k]+=om[k]||0; });\n    });\n    const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n    const sv=mv.dest.map(s=>c.span[s]);\n    const baseOf=(s,k)=>{ const b=mv.baseMetrics[s]||{}; return +(b[k]||0); };\n    return {\n      i:i, sector:mv.sector, level:mv.level||'setor', unitOfMuni:mv.unitOfMuni||{}, policy:mv.policy, domUF:mv.domUF,\n      optTier:(mv.optTier==null?0:mv.optTier),\n      dest:mv.dest.slice(), color:Object.assign({},mv.color), assign:Object.assign({},mv.assign),\n      kpis:{ cbsRealocadas:closingCbsOf(mv), desloc:c.desloc,\n             spread:(sv.length?Math.max.apply(null,sv)-Math.min.apply(null,sv):0),\n             municipios:ms.length, lideres:mv.leaders.length },\n      destinos:mv.dest.map(s=>({ s:s, base:baseOf(s,'cbs'), inc:c.inc[s]||0, orph:c.orph[s]||0, span:c.span[s]||0, gained:c.gained[s]||0 })),\n      municipios:ms.map(m=>(mv.level&&mv.level!=='setor')?{ nome:m, uf:'', cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }:{ nome:mName(m), uf:mUf(m), cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }),\n      excluidos:(mv.excludedMunis||[]).map(x=>({ muni:x.muni, cbs:x.cbs })),\n      orphanTotal:c.orphanTotal||0, orphanStranded:c.orphanStranded||0, noShapeCbs:mv.noShapeCbs||0,\n      lideres:c.leaders.map(L=>({ lider:(L.lider||'').split(' ').slice(0,2).join(' '), grupo:L.grupo,\n        total:L.total, home:L.home, desloc:L.desloc, orphan:L.orphan||0 })),\n      metricas:METRICS_DEF.map(md=>{ const k=md[0];\n        return { key:k, label:md[1], money:!!md[2],\n          rows:mv.dest.map(s=>({ s:s, antes:baseOf(s,k), depois:baseOf(s,k)+((incBy[s]&&incBy[s][k])||0) })),\n          sai:saiTot[k]||0 }; }),\n      geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat, excludedFeat:[]\n    };\n  });\n}\nfunction buildShareSummary(stages){\n  const world=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const receivers=[];\n  SESS.timeline.forEach((mv,i)=>{\n    const own=mv.pureOwnMetrics||{};\n    receivers.push({ s:mv.sector, situacao:'fechado (etapa '+(i+1)+')', antes:+(own.cbs||0), depois:0 });\n  });\n  const seen={};\n  Object.values(world.muniOverride).forEach(s=>{\n    if(seen[s]) return; seen[s]=1;\n    const base=SESS.sectorBaseCache[s]||{}, infl=world.sectorInflow[s]||{};\n    receivers.push({ s:s, situacao:'recebeu território', antes:+(base.cbs||0), depois:(+(base.cbs||0))+(infl.cbs||0) });\n  });\n  return { stages:stages.length, sectors:SESS.timeline.map(mv=>mv.sector),\n    totalCbs:stages.reduce((a,s)=>a+(s.kpis.cbsRealocadas||0),0),\n    totalDesloc:stages.reduce((a,s)=>a+(s.kpis.desloc||0),0), receivers:receivers };\n}\nfunction renderShareBody(){\n  const el=$('#shareBody'); if(!el) return;\n  const go=$('#shareGo'), cp=$('#shareCopy'), op=$('#shareOpen'), cc=$('#shareCancel');\n  if(SHARE.url){\n    el.innerHTML=`<div class=\"opt-note\" style=\"margin-top:0\"><b style=\"color:var(--ink)\">Sessão compartilhada ✓</b><br>\n      Link de leitura desta simulação congelada. Quem abrir precisa ter login no simulador.\n      <div class=\"shareurl\">${esc(SHARE.url)}</div></div>`;\n    if(go) go.classList.add('hidden');\n    if(cp) cp.classList.remove('hidden');\n    if(op) op.classList.remove('hidden');\n    if(cc) cc.textContent='Fechar';\n    return;\n  }\n  if(go) go.classList.remove('hidden');\n  if(cp) cp.classList.add('hidden');\n  if(op) op.classList.add('hidden');\n  if(cc) cc.textContent='Cancelar';\n  const desloc=SESS.timeline.reduce((a,mv)=>a+(mv.munis?computeDerived(mv).desloc:0),0);\n  el.innerHTML=`<div class=\"opt-lbl\">Confirmar compartilhamento</div>\n    <div class=\"opt-sub\">Publica uma <b>cópia congelada</b> desta sessão, exatamente no estado atual.</div>\n    <div class=\"opt-note\" style=\"margin-top:0\">\n      · <b>${fmt(SESS.timeline.length)}</b> etapa(s) — ${esc(SESS.timeline.map(mv=>mv.sector).join(' → '))}<br>\n      · snapshot atualizado em <b>${esc(fmtSyncedAt(SESS.snapshotSyncedAt))}</b><br>\n      · base: <b>municípios dominantes (CSV)</b> · destino: <b>qualquer município</b> · span: <b>${esc(spanShortAll())}</b><br>\n      · <b>${fmt(desloc)}</b> CBs deslocadas somando a cadeia\n    </div>\n    <div class=\"opt-note\">A cópia <b>não</b> acompanha edições posteriores desta sessão — pra atualizar, compartilhe de novo e use o link novo. O link exige login no simulador; não é público.</div>\n    ${SHARE.last?`<div class=\"opt-note\"><b style=\"color:var(--ink)\">Último link gerado nesta sessão</b> — aponta pro estado de <b>antes</b> das edições feitas depois dele.<div class=\"shareurl\">${esc(SHARE.last)}</div></div>`:''}`;\n}\nasync function openShare(){\n  if(!SESS.timeline.length){ $('#ds').textContent='nada pra compartilhar — feche pelo menos um setor primeiro.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  SHARE.url=null;\n  const s=$('#shareStatus'); if(s){ s.textContent=''; s.classList.remove('err'); }\n  renderShareBody();\n  $('#shareBg').classList.remove('hidden');\n  const b=$('#shareCancel'); if(b) b.focus();\n}\nfunction closeShare(){ const b=$('#shareBg'); if(b) b.classList.add('hidden'); }\nasync function doShare(){\n  if(SHARE.busy) return;\n  const go=$('#shareGo'), stx=$('#shareStatus');\n  SHARE.busy=true;\n  if(go){ go.disabled=true; go.textContent='Compartilhando…'; }\n  if(stx){ stx.textContent=''; stx.classList.remove('err'); }\n  try{\n    // Resolved as a GLOBAL PROCESS, not a declared param — which only works because the block\n    // instance on the `main` artboard binds `sharedDataset: @timeline-shared-sessionsDataSet`.\n    // That binding is load-bearing: a Process is only define()d once something on the rendering\n    // tree references it, so without it this lookup returns null and sharing dies on the guard\n    // below. Do NOT remove the instance param because this file never reads d2.params.sharedDataset.\n    const ds=d2.data(SHARED_DS);\n    if(!ds) throw new Error('dataset de sessões compartilhadas indisponível');\n    await flushDirty();\n    const stages=buildShareStages();\n    const summary=buildShareSummary(stages);\n    let who='';\n    try{ const a=d2.auth(AUTH_REF); who=(a&&a.user&&(a.user.email||a.user.nome))||''; }catch(e){}\n    const created=await ds.create({\n      label:SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR')),\n      snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt,\n      settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n      stages:stages, summary:summary, sharedBy:who, sharedAt:new Date().toISOString()\n    });\n    const id=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    if(!id) throw new Error('registro gravado, mas sem _id na resposta — não foi possível montar o link');\n    SHARE.url=window.location.origin+'/sessions/'+id;\n    SHARE.last=SHARE.url;\n    renderShareBody();\n  }catch(e){\n    if(stx){ stx.textContent='erro ao compartilhar: '+e.message; stx.classList.add('err'); }\n  }finally{\n    SHARE.busy=false;\n    if(go){ go.disabled=false; go.textContent='Compartilhar'; }\n  }\n}\n\n// ---------- rendering ----------\nfunction renderTimeline(){\n  const el=$('#movementChips'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<span class=\"empty\">Nenhuma etapa ainda — use \"Adicionar movimentação\" à direita.</span>'; return; }\n  el.innerHTML=SESS.timeline.map((mv,i)=>{\n    const stale=SESS.dirtyFrom!=null && i>=SESS.dirtyFrom;\n    return `<div class=\"tchip ${i===SESS.selected?'on':''} ${stale?'stale':''}\" data-i=\"${i}\">\n      <span class=\"tnum\">${i+1}</span><span class=\"tsec\">${LEVELS[mv.level||'setor'].label} ${mv.sector}</span>\n      ${stale?'<span class=\"tstale\" title=\"recalcula ao selecionar outra etapa\">⟳</span>':''}\n      <span class=\"trm\" data-rm=\"${i}\" title=\"remover esta etapa e as seguintes\">✕</span>\n    </div>`;\n  }).join('');\n  el.querySelectorAll('.tchip').forEach(ch=>{\n    ch.addEventListener('click',async (e)=>{\n      if(e.target.dataset.rm!=null && e.target.dataset.rm!==''){ removeFrom(+e.target.dataset.rm); return; }\n      const i=+ch.dataset.i;\n      if(i!==SESS.selected) await flushDirty();\n      SESS.selected=i; renderAll();\n    });\n  });\n}\nfunction renderRuleNote(){\n  const el=$('#ruleNote'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected];\n  const lvl=LEVELS[(mv&&mv.level)||'setor'];\n  let h='';\n  if(!mv || (mv.level||'setor')==='setor'){\n    h+=`<b style=\"color:var(--ink)\">Base de municípios dominantes:</b> só os municípios que a base (CSV) atribui ao setor entram no mapa e na alocação`;\n    if(mv&&mv.domCsvMunis) h+=` · <b>${fmt(mv.domCsvMunis)}</b> na base do setor ${mv.sector}`;\n    if(mv&&mv.excludedCbs) h+=` · <b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs)} CBs</b> fora da base nesta etapa — não aparecem no mapa e as CBs seguem suas líderes (quebra de vínculo)`;\n  } else {\n    // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base), então\n    // não existe \"excluído\" pra reportar aqui — só o tamanho da base e em quantas unidades ela se agrupa.\n    h+=`<b style=\"color:var(--ink)\">Base de ${lc(lvl.childLabel)}s dominantes:</b> ${mv.domCsvMunis!=null?`<b>${fmt(mv.domCsvMunis)}</b> município(s) `:''}na base ${lvl.de} ${lc(lvl.label)} ${mv.sector}, agrupados em <b>${fmt(Object.keys(mv.unitMunis||{}).length)}</b> ${lc(lvl.childLabel)}(s)`;\n  }\n  if(DOM.error) h+=` · <b style=\"color:var(--closing)\">⚠ base indisponível: ${esc(DOM.error)}</b>`;\n  el.innerHTML=h;\n}\nfunction renderSpanRule(){\n  const el=$('#spanRule'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected], lim=spanLimits(mv);\n  if(!mv||!mv.munis||mv.view==='asis'||!spanLimitsOn(lim)){ el.innerHTML=''; return; }\n  const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,lim);\n  const bits=[];\n  if(lim.target) bits.push(`alvo <b>${fmt(lim.target)}</b>`);\n  if(lim.max) bits.push(`máx <b>${fmt(lim.max)}</b>`);\n  if(lim.min) bits.push(`mín <b>${fmt(lim.min)}</b>`);\n  let h=`Pré-requisito de span: ${bits.join(' · ')} CBs — `;\n  if(v.over.length||v.under.length){\n    h+=`<b style=\"color:var(--closing)\">⚠ `;\n    if(v.over.length) h+=`acima do máximo: ${v.over.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    if(v.over.length&&v.under.length) h+=' · ';\n    if(v.under.length) h+=`abaixo do mínimo: ${v.under.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    h+=`</b>`;\n    if(mv.optTier===1) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita o span mínimo nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===2) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita os limites nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===-1) h+=`<br><span style=\"opacity:.85\">Etapa resolvida por heurística — os limites não são garantidos.</span>`;\n  } else h+=`<b style=\"color:var(--ok)\">✓ todos os setores receptores dentro dos limites</b>`;\n  el.innerHTML=`<div class=\"note\" style=\"margin:-2px 0 6px\">${h}</div>`;\n}\nfunction renderContig(){const mv=SESS.timeline[SESS.selected];const el=$('#contig');if(!el||!mv)return;\n  if(mv.view==='asis'){el.innerHTML='';return;}\n  const lvl=LEVELS[mv.level||'setor'];\n  // Indicador de conectividade geográfica só se aplica a fechamento de setor — ver contigBadFor.\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=''; return; }\n  const bad=contigBadFor(mv,mv.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Território desconectado em ${bad.join(', ')} — ${lc(lvl.childLabel)}(s) desconectado(s) do território ${lvl.de} ${lc(lvl.label)}. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Sem quebras — cada ${lc(lvl.label)} forma uma região conectada.</div>`;\n}\nfunction closingCbsOf(mv){ return Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0); }\nfunction sectorTotalCbs(mv){ return Object.values(mv.munis).reduce((a,o)=>a+o.cbs,0); }\nfunction renderKpis(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  const childPlural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  if(mv.view==='asis'){const mc=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(sectorTotalCbs(mv)),l:`CBs ${lvl.no} ${lc(lvl.label)} ${mv.sector} (antes desta etapa)`,t:`Total de CBs ${lvl.no} ${lc(lvl.label)} hoje — já considerando o que etapas anteriores da timeline mandaram pra cá.`},\n      {n:mv.csvMunisTotal?`${fmt(mc)}/${fmt(mv.csvMunisTotal)}`:fmt(mc),l:`${childPlural} ${lvl.de} ${lc(lvl.label)}`,t:`${lvl.childLabel}(s) desenhados nesta etapa${mv.csvMunisTotal?` de ${fmt(mv.csvMunisTotal)} que a base de dominantes atribui ${lvl.a} ${lc(lvl.label)}`:''}.${(mv.missingMunis||[]).length?' Fora do mapa: '+mv.missingMunis.map(x=>x.muni+' ('+x.reason+')').join(' · '):''}${Object.keys(mv.ufGuessed||{}).length?' UF inferida da UF dominante do setor (nome repetido em outra UF carregada): '+Object.entries(mv.ufGuessed).map(([nm,u])=>nm+'→'+u).join(', '):''}`},\n      {n:fmt(mv.leaders.length),l:'Líderes (grupos)',t:`Líderes presentes ${lvl.no} ${lc(lvl.label)} nesta etapa.`},\n      {n:fmt(mv.dest.length),l:`${lvl.destLabel}s adjacentes`,t:`${lvl.destLabel}s vizinh${lvl.destLabel==='GV'?'as':'os'} (ainda abertas)${lvl.parentPhrase?` ${lvl.parentPhrase}`:''}${mv.gv?` (${mv.gv})`:''} que poderiam receber os ${lc(lvl.childLabel)}s.${(mv.gvRejected||[]).length&&lvl.parentWord?' Fora do mapa, por serem de outr'+(lvl.parentWord==='gerência'?'a':'o')+' '+lvl.parentWord+': '+mv.gvRejected.join(', ')+'.':''}`},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=mv.dest.map(s=>c.span[s]);const spread=sv.length?Math.max(...sv)-Math.min(...sv):0;\n  const zero=mv.dest.filter(s=>c.gained[s]===0);const totCbs=closingCbsOf(mv);\n  $('#kpis').innerHTML=[\n    // \"CBs fora do X\" só faz sentido pra fechamento de setor — a mecânica de vínculo (CBs que\n    // seguem a líder pra fora do território) é uma coisa de setor/orphan, não de GV/mercado.\n    (mv.level||'setor')==='setor'?{n:fmt(c.desloc),l:mv.policy==='fora'?`CBs fora ${lvl.de} ${lc(lvl.destLabel)}`:'CBs c/ quebra de relação',cls:'warn',t:`CBs cuja líder acabou noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} nesta etapa.`}:null,\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:`Diferença de tamanho entre ${lvl.destLabel==='GV'?'a maior e a menor':'o maior e o menor'} ${lc(lvl.destLabel)} depois desta etapa.`},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs ${lvl.de} ${lc(lvl.label)} ${mv.sector} sendo redistribuídas nesta etapa.`},\n    (mv.level||'setor')==='setor'?{n:zero.length?zero.join(', '):'nenhum',l:`${lvl.destLabel} sem líder herdada`,cls:zero.length?'warn':'ok',t:`${lvl.destLabel} que recebe ${lc(lvl.childLabel)}(s) mas nenhuma líder nesta etapa.`}:null,\n  ].filter(Boolean).concat(mv.excludedCbs?[{n:fmt(mv.excludedCbs),l:`CBs fora do setor · ${fmt(mv.excludedMunis.length)} município(s) fora da base`,cls:'warn',t:'CBs em municípios que a base de dominantes não atribui a este setor: ficam fora do mapa e da alocação geográfica e seguem a líder do seu grupo.'}]:[])\n   .concat((mv.missingMunis||[]).length?[{n:fmt(mv.missingMunis.length),l:`Município(s) da base fora do mapa · ${fmt(mv.csvMunisTotal)} na base`,cls:'warn',t:`A base de dominantes atribui ${fmt(mv.csvMunisTotal)} município(s) ao setor ${mv.sector} e ${fmt(mv.csvMunisTotal-mv.missingMunis.length)} chegaram ao mapa. Fora: ${mv.missingMunis.map(x=>x.muni+' — '+x.reason+(x.cbs?' ('+fmt(x.cbs)+' CBs)':'')).join(' · ')}`}]:[])\n   .map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  if(mv.view==='asis'){const items=[{s:mv.sector,val:sectorTotalCbs(mv),color:CLOSE_COLOR}].concat(mv.dest.map(s=>({s,val:+(mv.baseMetrics[s]?.cbs||0),color:mv.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura antes desta etapa — ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const lim=spanLimits(mv),on=spanLimitsOn(lim);\n  const rawMax=Math.max(...mv.dest.map(s=>c.span[s]),lim.target||0,lim.max||0,1);\n  const max=on?rawMax*1.04:rawMax;   // headroom so a marker sitting at the scale top stays visible\n  const marks='';\n  $('#spans').innerHTML=mv.dest.map(s=>{const base=+(mv.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    const over=lim.max&&c.span[s]>lim.max,under=lim.min&&c.span[s]<lim.min;\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${mv.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${mv.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${mv.color[s]}\"></div>${marks}</div>\n      <div class=\"bar-val\" style=\"${over?'color:var(--closing);font-weight:700':(under?'color:#ff9500;font-weight:700':'')}\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span>${over?' ▲':''}${under?' ▼':''}</div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base (já incluindo etapas anteriores) · cheio = CBs entrantes ${lvl.de} ${mv.sector} nesta etapa.${c.orphanTotal?` Inclui <b>${fmt(c.orphanTotal-c.orphanStranded)}</b> CB(s) de municípios fora da base de dominantes que seguiram suas líderes.`:''}</div>`;\n}\n// Two very different reasons land rows in the __OUTROS__ bucket — no IBGE shape, or outside the\n// sector's dominance base. Same bucket computationally, but they must not read as the same thing.\nfunction outsideNote(mv){\n  const bits=[];\n  if(mv.noShapeCbs) bits.push(`<b>${fmt(mv.noShapeCbs)}</b> CBs em ${fmt(mv.noShapeMunis.length)} município(s) sem shape na UF`);\n  if(mv.excludedCbs) bits.push(`<b>${fmt(mv.excludedCbs)}</b> CBs em ${fmt(mv.excludedMunis.length)} município(s) <b>fora da base de dominantes</b> deste setor — <b style=\"color:var(--closing)\">quebra de vínculo</b>`);\n  if(!bits.length) return '';\n  let h=`<div class=\"note\">Fora do mapa e da alocação geográfica: ${bits.join(' · ')}.`;\n  if(mv.excludedCbs){\n    const c=computeDerived(mv);\n    h+=` As <b>${fmt(mv.excludedCbs)}</b> CBs em quebra de vínculo <b>seguem a líder do seu grupo</b>`;\n    const byS=mv.dest.filter(s=>c.orph[s]).map(s=>`<b style=\"color:${mv.color[s]}\">${s}</b> (${fmt(c.orph[s])})`);\n    h+=byS.length?`: ${byS.join(' · ')}.`:'.';\n    if(c.orphanStranded) h+=` <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} CB(s) sem destino</b> — a líder não ficou com nenhum município realocado, então não há setor pra seguir.`;\n  }\n  if(mv.noShapeCbs) h+=` As CBs sem shape na UF continuam fora de qualquer setor destino.`;\n  if(mv.excludedMunis&&mv.excludedMunis.length) h+=`<br><span style=\"opacity:.85\">${mv.excludedMunis.slice(0,8).map(x=>x.muni+' ('+fmt(x.cbs)+')').join(', ')}${mv.excludedMunis.length>8?' +'+(mv.excludedMunis.length-8):''}</span>`;\n  return h+`</div>`;\n}\nfunction renderMuni(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const lvl=LEVELS[mv.level||'setor'];\n  const plural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  const fem=lvl.childLabel==='GV';\n  const t=$('#muniListTitle'); if(t) t.textContent=`${plural} → ${lc(lvl.destLabel)} destino`;\n  const nt=$('#muniListNote'); if(nt) nt.innerHTML=`Clique no <b>código ${lvl.a} ${lc(lvl.destLabel)}</b> pra mandar ${lvl.a} ${lc(lvl.childLabel)} <b>inteir${fem?'a':'o'}</b> pra lá (regra de negócio). A cor acompanha o mapa.`;\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  if(mv.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">${lc(lvl.label)} ${mv.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje tod${fem?'as':'os'} pertencem ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`+outsideNote(mv);return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${mv.dest.map(s=>`<button data-s=\"${s}\" class=\"${mv.assign[m]===s?'on':''}\" style=\"${mv.assign[m]===s?'background:'+mv.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +outsideNote(mv);\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{const m=b.parentElement.dataset.m,s=b.dataset.s;editSelected(m2=>{m2.assign[m]=s;});});\n}\nfunction renderLeaders(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>${LEVELS[mv.level||'setor'].label}</th></tr></thead><tbody>`\n    +mv.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${mv.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(mv.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  const showOrph=c.orphanTotal>0;\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${mv.dest.map(s=>`<th>${s}</th>`).join('')}${showOrph?'<th title=\"CBs em municípios fora da base de dominantes que seguiram esta líder\">Fora do setor</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td>${L.home?`<span class=\"home\" style=\"background:${mv.color[L.home]}\"></span>${L.home}`:'–'}</td>\n     ${mv.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+mv.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     ${showOrph?`<td class=\"cell\" style=\"${L.orphan&&!L.orphanHome?'color:var(--closing);font-weight:700':''}\" title=\"${L.orphan?(L.orphanHome?'seguem a líder para o setor '+L.orphanHome:'sem home — CBs sem setor pra seguir'):''}\">${L.orphan?fmt(L.orphan)+(L.orphanHome?'':' ⚠'):'·'}</td>`:''}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${mv.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}${showOrph?`<td>${fmt(c.orphanTotal)}</td>`:''}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  // Esta matriz cruza LINHAS DO CRM (município x grupo) — no fechamento de GV/mercado a unidade\n  // que troca de dono é setor/GV, não município, e reconstruir a matriz nesse grão é trabalho ainda\n  // não feito; mostrar a tabela como se fosse por município aqui daria linhas vazias sem avisar.\n  if((mv.level||'setor')!=='setor'){ $('#matrix').innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; return; }\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // `_out` is a flag stamped on the ROW, and rows are shared between etapas (absorbed + inherited\n  // orphans), so the last etapa to aggregate wins and reading it here can describe the wrong etapa.\n  // Derive the bucket from THIS mv instead. Summing rather than .find() because several out-municípios\n  // of the same group all collapse into __OUTROS__, and only the first was being counted.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cell=(m,g)=>mv.matrix.reduce((a,x)=>a+((((outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))===m)&&String(x.grupo)===g)?+x.cbs:0),0);\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  // No destination column here. This is the AS-IS matrix: every município in it still belongs to the\n  // closing sector, so showing where it is HEADED mixed the two states of the world in one table and\n  // made the ASIS view read like a plan. The destination lives on the map and in \"Municípios → setor\n  // destino\", which are about the to-be.\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\n// TOBE counterpart of the AS-IS matrix: the same município × grupo grid plus the two columns the AS-IS\n// view deliberately refuses to carry — where the município ends up, and under which líder.\n// \"Líder resultante\" is the líder holding the most CBs in that município. A município is served by several\n// grupos at once and the data names no single owner, so the dominant one is the honest answer and its\n// share is printed beside it instead of being hidden behind a single name.\nfunction renderMatrixTobe(){const mv=SESS.timeline[SESS.selected]; const el=$('#matrixTobe'); const nt=$('#tobeNote');\n  if(!el) return;\n  if(!mv||!mv.munis){ el.innerHTML=''; if(nt) nt.innerHTML=''; return; }\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; if(nt) nt.innerHTML=''; return; }\n  const c=computeDerived(mv);\n  const homeOf={}; c.leaders.forEach(L=>{ homeOf[L.grupo]=L.home; });\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // One pass over the matrix instead of a scan per cell: the AS-IS table can afford the naive version\n  // on six municípios, but this one asks for the dominant líder too, which multiplies the lookups.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cellBy={}; mv.matrix.forEach(x=>{ const k=(outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))+'|'+String(x.grupo);\n    cellBy[k]=(cellBy[k]||0)+ +x.cbs; });\n  const cell=(m,g)=>cellBy[m+'|'+g]||0;\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  const domLeader=m=>{ let best=null,bc=0;\n    mv.leaders.forEach(L=>{ const v=cell(m,L.grupo); if(v>bc){ bc=v; best=L; } });\n    return best?{L:best,cbs:bc}:null; };\n  const short=L=>(L.lider||'').split(' ').slice(0,2).join(' ')||('grupo '+L.grupo);\n  // NOT named `rows` — that is the module-level Databricks response helper, and shadowing it inside a\n  // render function is a trap for whoever adds a query here later.\n  // Off-mismatch ainda calculado pra alimentar a nota abaixo da tabela — só não vira mais coluna: o\n  // destino agora é \"Setor destino\" por GRUPO no rodapé, não mais por município na grade.\n  const recs=ms.map(m=>{ const d=mv.assign[m]||null, dl=domLeader(m);\n    const home=dl?(homeOf[dl.L.grupo]||null):null;\n    return {m:m,off:!!(dl&&home&&d&&home!==d)}; });\n  el.innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${esc(L.lider||'')}\">${L.grupo}<br><span style=\"font-weight:400;font-size:10px;color:var(--mut)\">${esc(short(L))}</span></th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')\n   // Um rótulo de destino por GRUPO (coluna), não por município (linha) — cada grupo tem UM home\n   // fixo (computeDerived já calculou), então ele mora uma vez no rodapé, igual o nome já morava.\n   +`<tr class=\"lider-foot\"><td>Setor destino</td>`\n     +mv.leaders.map(L=>{const h=homeOf[L.grupo]; return `<td>${h?`<span class=\"home\" style=\"background:${mv.color[h]}\"></span>${h}`:'–'}</td>`;}).join('')\n     +`<td></td></tr>`\n   +`</tbody>`;\n  if(nt){\n    const offN=recs.filter(r=>r.off).length;\n    nt.innerHTML=(mv.policy!=='fora')\n      ? `Política <b>quebrar relação</b>: as CBs deixam a líder atual e passam à estrutura do setor destino — o rodapé mostra pra onde cada grupo vai.`\n      : `<b>Setor destino</b> = o setor que recebe a maior parte das CBs de cada líder (rodapé, uma vez por grupo). `\n        +(offN?`<span style=\"color:var(--closing)\">${fmt(offN)} município(s) ⚠</span> têm a líder dominante indo para um setor diferente do dela — as CBs seguem a líder (é disso que o KPI “CBs fora do setor” é feito).`\n          :`Em todos os municípios a líder dominante termina no mesmo setor que recebe o território.`);\n  }\n}\nfunction renderPolicy(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  // O toggle fora-do-setor/quebra-de-relação é uma decisão de setor fechando — em GV/mercado a\n  // seção some inteira em vez de mostrar um controle que não corresponde a nada calculado.\n  const sec=$('#policySection'); if(sec) sec.classList.toggle('hidden',(mv.level||'setor')!=='setor');\n  if((mv.level||'setor')!=='setor') return;\n  $('#policyNote').innerHTML=mv.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)}</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder d${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor']; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  // GV/mercado: a chave \"m\" aqui é um código de setor/GV, não uma chave município/UF — mName/mUf\n  // parseariam lixo, então o CSV de plano exporta o código cru nesses níveis.\n  mv._plan=ms.map(m=>(mv.level&&mv.level!=='setor')?{municipio:m,uf:'',cbs:mv.munis[m].cbs,destino:mv.assign[m]}:{municipio:mName(m),uf:mUf(m),cbs:mv.munis[m].cbs,destino:mv.assign[m]});\n  $('#plan').innerHTML=`Etapa ${SESS.selected+1} · ${lvl.label} <b>${mv.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${mv.color[mv.assign[m]]}\">${mv.assign[m]||'–'}</b>`).join(' · ')}<br>\n   Política: <b>${mv.policy==='fora'?`manter vínculo (CBs fora ${lvl.de} ${lc(lvl.destLabel)})`:'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas nesta etapa.`\n   +(c.orphanTotal?`<br>Fora da base de dominantes: ${fmt(c.orphanTotal)} CB(s) seguem suas líderes — ${mv.dest.filter(s=>c.orph[s]).map(s=>`${s} (${fmt(c.orph[s])})`).join(' · ')||'–'}${c.orphanStranded?` · <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} sem líder com destino</b>`:''}. Não entram no CSV, que é por município.`:'');\n}\nfunction renderReport(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  mv.metric=mv.metric||'cbs';\n  const KEYS=METRICS_DEF.map(m=>m[0]);\n  const incBy={};mv.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incBy[s])return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  // orphan CBs land on their leader's home sector, not on a geographic destination\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home&&incBy[L.home]) KEYS.forEach(k=>incBy[L.home][k]+=om[k]||0); });\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  // they leave the closing sector too, so antes → depois still balances\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home) KEYS.forEach(k=>closeTot[k]+=om[k]||0); });\n  const trend=mv.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=mv.dest.map(s=>({label:s,color:mv.color[s],antes:+(mv.baseMetrics[s]?.[mk]||0),depois:+(mv.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:mv.sector+'✕',color:'#ff3b30',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#34c759':'#ff3b30'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois desta etapa</h2>\n      <select id=\"metricSel\">${METRICS_DEF.map(m=>`<option value=\"${m[0]}\" ${m[0]===mv.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes desta etapa</b> (já incluindo etapas anteriores) · barra cheia = <b>depois</b> · <b style=\"color:var(--acc)\">${mv.sector}✕</b> = ${lc(lvl.label)} fechad${lvl.a==='à'?'a':'o'} nesta etapa · % = variação</div>\n    ${chartHTML(mv.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por ${lc(lvl.destLabel)} receptor (snapshot congelado)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${METRICS_DEF.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const b=mv.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      METRICS_DEF.map(([k])=>{const bv=+b[k]||0,av=bv+(inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=METRICS_DEF.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${mv.sector} (sai)</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};mv.dest.forEach(s=>incLvl[s]={});\n  Object.keys(mv.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incLvl[s])return;const lv=mv.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  c.leaders.forEach(L=>{ const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n    if(ol&&L.home&&incLvl[L.home]) Object.entries(ol).forEach(([nv,n])=>incLvl[L.home][nv]=(incLvl[L.home][nv]||0)+n); });\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por ${lc(lvl.destLabel)} receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const base=mv.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> desta etapa · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${mv.sector}.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${mv.sector} — últimos ${trend.length} ciclos até o snapshot congelado</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Tendência histórica <b>do próprio setor</b> — não ajustada pela absorção hipotética de etapas anteriores (limitação conhecida: tendência é por setor, não decomponível por município).</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{mv.metric=sel.value; renderReport();};\n}\nfunction renderCumulative(){\n  const el=$('#tab-cumulative'); if(!el) return;\n  // Esta view soma a cadeia inteira via foldWorldState().muniOverride/sectorBaseCache — hoje só\n  // populados pelo fechamento de setor. GV/mercado têm o rastro equivalente (secOverride/gvOverride),\n  // mas essa agregação ainda não foi refeita nesse grão; melhor avisar do que mostrar tabela vazia.\n  const lockLvl=SESS.timeline.length?(SESS.timeline[0].level||'setor'):'setor';\n  if(lockLvl!=='setor'){ el.innerHTML=`<div class=\"note\">Impacto acumulado da cadeia ainda não disponível pra sessões de ${lc(LEVELS[lockLvl].label)} — use o relatório da etapa (aba anterior) etapa a etapa.</div>`; return; }\n  if(!SESS.timeline.length){ el.innerHTML='<div class=\"note\">Nenhuma etapa na timeline ainda.</div>'; return; }\n  const ready=SESS.timeline.every(mv=>mv.munis);\n  if(!ready){ el.innerHTML='<div class=\"note\">Aguardando etapas terminarem de calcular…</div>'; return; }\n  const finalWorld=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const openSectors=[...new Set(Object.values(finalWorld.muniOverride))];\n  let h=`<h2 style=\"margin:0 0 8px\">Linha do tempo — ${SESS.timeline.length} fechamento(s)</h2>\n    <div class=\"note\" style=\"margin:-2px 0 10px\">Setores fechados, em ordem: ${SESS.timeline.map(mv=>mv.sector).join(' → ')}. Cada linha abaixo soma o efeito de <b>toda a cadeia</b>, não apenas da última etapa.</div>`;\n  h+=`<div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th>${METRICS_DEF.map(([,l])=>`<th>${l}</th>`).join('')}</tr></thead><tbody>`;\n  SESS.timeline.forEach((mv,i)=>{\n    h+=`<tr style=\"opacity:.75\"><td>${mv.sector}</td><td>fechado (etapa ${i+1})</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(mv.pureOwnMetrics?.[k]||0)}</td>`).join('')+`</tr>`;\n  });\n  openSectors.forEach(s=>{\n    const base=SESS.sectorBaseCache[s]||{};\n    const infl=finalWorld.sectorInflow[s]||{};\n    h+=`<tr><td>${s}</td><td>recebeu território</td>`+METRICS_DEF.map(([k,,t])=>{\n      const bv=+base[k]||0, av=bv+(infl[k]||0); const d=bv?((av-bv)/bv*100):0; const f=(t==='$'?money:fmt);\n      return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;\n    }).join('')+`</tr>`;\n  });\n  h+=`</tbody></table></div><div class=\"note\">\"Antes\" de um setor receptor é o snapshot congelado (sem nenhum fechamento aplicado); \"depois\" soma tudo que a cadeia inteira jogou pra ele.</div>`;\n  el.innerHTML=h;\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=MAP_W,H=MAP_H,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\n// ---------- shared-município slicing ----------\n// A huge urban município (São Paulo etc.) can be the CSV's dominant município for MANY different\n// setores at once — Natura subdivides one big city into dozens of commercial sectors while IBGE's\n// shapefile only has ONE polygon for the whole city. Drawing that single polygon once per claiming\n// sector stacked identical copies on top of each other: only whichever sector was processed first\n// ever \"won\" the shape, and every other sector's label floated over territory it didn't actually own\n// (mv.muniOwners below records every claimant, not just the first). Slice the polygon into N\n// equal-width strips instead — one per claiming sector, ordered by sector code — along whichever\n// axis the shape is longer on, so each sector genuinely gets its own area instead of losing the race.\nfunction clipRingHalf(ring,axis,keepGe,val){\n  const out=[],n=ring.length; if(!n) return out;\n  const side=p=>keepGe?p[axis]>=val:p[axis]<=val;\n  for(let i=0;i<n;i++){\n    const cur=ring[i],prev=ring[(i-1+n)%n];\n    const curIn=side(cur),prevIn=side(prev);\n    if(curIn!==prevIn){\n      const t=(val-prev[axis])/(cur[axis]-prev[axis]);\n      const other=1-axis,pt=[0,0]; pt[axis]=val; pt[other]=prev[other]+t*(cur[other]-prev[other]);\n      out.push(pt);\n    }\n    if(curIn) out.push(cur);\n  }\n  return out;\n}\nfunction sliceFeatureEqual(feature,n){\n  if(n<=1) return [feature];\n  const g=feature.geometry,parts=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  const rings=[]; parts.forEach(poly=>poly.forEach(r=>{ if(r&&r.length>=4) rings.push(r); }));\n  let mnx=Infinity,mxx=-Infinity,mny=Infinity,mxy=-Infinity;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  if(!isFinite(mnx)) return Array.from({length:n},()=>({...feature,__empty:true}));\n  const wide=(mxx-mnx)>=(mxy-mny), axis=wide?0:1;\n  const lo=wide?mnx:mny, hi=wide?mxx:mxy, span=(hi-lo)||1;\n  const out=[];\n  for(let i=0;i<n;i++){\n    const a=lo+span*i/n, b=lo+span*(i+1)/n;\n    const polys=[];\n    rings.forEach(r=>{\n      let c=clipRingHalf(r,axis,true,a);\n      c=clipRingHalf(c,axis,false,b);\n      if(c.length>=3) polys.push([[...c,c[0]]]);\n    });\n    out.push(polys.length?{type:'Feature',properties:feature.properties,__uf:feature.__uf,geometry:{type:'MultiPolygon',coordinates:polys}}:{...feature,__empty:true});\n  }\n  return out;\n}\n// ---------- label placement ----------\n// Pole of inaccessibility: the interior point farthest from ANY boundary of the dissolved area. A\n// centroid is only a mean — on a concave sector, one split across a river, or a member whose vertex\n// average falls in a bay, it lands outside the very territory it is supposed to name, which is what\n// pushed the sector numbers off their own shapes. Even-odd across every ring of every member is a\n// correct union test here because municipalities tile rather than overlap.\n// The search box is clipped to the viewBox, so the result is inside the territory AND on screen.\n// Rings are decimated first — this is a label, not a measurement — which keeps a 20-município sector\n// to a few million ops, and the result is cached per stage: geometry never changes between renders,\n// only the fill does.\nfunction labelPointFor(fs,pr){\n  const rings=[];\n  (fs||[]).forEach(f=>{ if(!f||!f.geometry) return;\n    const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>{\n      if(!r||r.length<4) return;\n      const step=Math.max(1,Math.floor(r.length/120)),pts=[];\n      for(let i=0;i<r.length;i+=step) pts.push(pr(r[i]));\n      if(pts.length>3) rings.push(pts);\n    }));});\n  if(!rings.length) return null;\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  mnx=Math.max(mnx,2); mny=Math.max(mny,2); mxx=Math.min(mxx,698); mxy=Math.min(mxy,498);\n  if(!(mxx>mnx&&mxy>mny)) return null;\n  const inside=(x,y)=>{ let c=false;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      if(((a[1]>y)!==(b[1]>y)) && (x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])) c=!c; }\n    return c; };\n  const edgeDist=(x,y)=>{ let m=Infinity;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      const dx=b[0]-a[0],dy=b[1]-a[1],L=dx*dx+dy*dy;\n      let t=L?(((x-a[0])*dx+(y-a[1])*dy)/L):0; t=t<0?0:(t>1?1:t);\n      const ex=a[0]+t*dx-x,ey=a[1]+t*dy-y,d=ex*ex+ey*ey; if(d<m)m=d; }\n    return Math.sqrt(m); };\n  const W=mxx-mnx,H=mxy-mny,N=26;\n  // Centroide (média simples dos vértices) do território inteiro — só usado pra desempatar A FAVOR\n  // do meio quando um ponto mais central tem folga quase tão boa quanto o campeão isolado. Um setor\n  // com um lóbulo bem mais gordo de um lado sempre ganhava o rótulo puxado pra esse lado, mesmo\n  // quando um ponto quase tão espaçoso e bem mais central também servia — o peso é pequeno de\n  // propósito, nunca vale mais que uma folga real melhor.\n  let ccx=0,ccy=0,cn=0;\n  rings.forEach(r=>r.forEach(p=>{ ccx+=p[0]; ccy+=p[1]; cn++; }));\n  ccx/=cn||1; ccy/=cn||1;\n  const CENTER_PULL=0.22;\n  const scoreAt=(x,y,d)=>d-CENTER_PULL*Math.hypot(x-ccx,y-ccy);\n  let best=null,bd=-1,bs=-Infinity;\n  for(let i=0;i<=N;i++) for(let j=0;j<=N;j++){\n    const x=mnx+W*i/N,y=mny+H*j/N;\n    if(!inside(x,y)) continue;\n    const d=edgeDist(x,y),s=scoreAt(x,y,d); if(s>bs){ bs=s; bd=d; best=[x,y]; }\n  }\n  if(!best) return null;\n  // Hill-climb off the coarse grid: the grid only has to land somewhere in the right lobe, the\n  // refinement finds the deep point inside it — halving the step whenever no neighbour improves.\n  let step=Math.max(W,H)/N/2;\n  for(let k=0;k<7;k++){\n    let moved=false;\n    for(const o of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]){\n      const x=best[0]+o[0]*step,y=best[1]+o[1]*step;\n      if(x<mnx||x>mxx||y<mny||y>mxy||!inside(x,y)) continue;\n      const d=edgeDist(x,y),s=scoreAt(x,y,d); if(s>bs){ bs=s; bd=d; best=[x,y]; moved=true; }\n    }\n    if(!moved) step/=2;\n  }\n  // The clearance travels with the point: it is the radius of the largest circle that fits inside the\n  // territory there, which is exactly the budget a caller needs to decide how much text will fit.\n  return {p:best,r:bd};\n}\nfunction labelPointOf(mv,key,fs,pr){\n  if(!mv._lbl) mv._lbl={};\n  if(!(key in mv._lbl)) mv._lbl[key]=labelPointFor(fs,pr);\n  return mv._lbl[key];\n}\n// ---------- map zoom/pan ----------\n// Zoom é feito manipulando o viewBox, nunca um transform CSS — assim rótulos e polígonos vivem no\n// MESMO sistema de coordenadas e escalam juntos automaticamente (texto tem font-size em unidades do\n// viewBox, então encolher o viewBox amplia tudo por igual, sem lógica extra pro tamanho da fonte).\nconst MAP_W=700; let MAP_H=500; const MAP_ZOOM_MAX=8;\nlet mapView={x:0,y:0,w:MAP_W,h:MAP_H};\n// MAP_H acompanha a proporção real do card na tela (MAP_W fica fixo) — sem isso o \"mundo\" de\n// desenho tinha sempre proporção 700x500 fixa, e o <svg> (que preserva essa proporção) ficava\n// menor que o card sempre que o card fosse bem mais alto que largo, sobrando um vão em branco\n// embaixo — a sensação de \"mapa preso num quadrado\" dentro de um espaço bem maior.\nfunction syncMapDims(){\n  const wrap=$('.map-wrap'); if(!wrap) return;\n  const cw=wrap.clientWidth, ch=wrap.clientHeight; if(!cw||!ch) return;\n  const newH=Math.round(MAP_W*(ch/cw));\n  if(Math.abs(newH-MAP_H)<2) return;\n  const fracW=mapView.w/MAP_W, cx=(mapView.x+mapView.w/2)/MAP_W, cy=(mapView.y+mapView.h/2)/MAP_H;\n  MAP_H=newH;\n  const w=Math.min(MAP_W,Math.max(MAP_W/MAP_ZOOM_MAX,MAP_W*fracW));\n  const h=Math.min(MAP_H,Math.max(MAP_H/MAP_ZOOM_MAX,MAP_H*fracW));\n  let x=cx*MAP_W-w/2, y=cy*MAP_H-h/2;\n  x=Math.min(MAP_W-w,Math.max(0,x)); y=Math.min(MAP_H-h,Math.max(0,y));\n  mapView={x,y,w,h};\n}\nfunction applyMapView(){\n  const svg=$('#map'); if(!svg) return;\n  svg.setAttribute('viewBox', mapView.x.toFixed(2)+' '+mapView.y.toFixed(2)+' '+mapView.w.toFixed(2)+' '+mapView.h.toFixed(2));\n  // Rótulo tem font-size em unidades do viewBox, então sem isso ele cresceria junto com o zoom até\n  // virar um borrão — encolhendo o font-size na mesma proporção que o viewBox encolhe cancela\n  // exatamente o zoom, e o texto fica do MESMO tamanho na tela em qualquer nível de zoom.\n  const fsScale=mapView.w/MAP_W;\n  svg.querySelectorAll('text[data-fs]').forEach(t=>{\n    const base=+t.getAttribute('data-fs')||8;\n    t.setAttribute('font-size',(base*fsScale).toFixed(2));\n  });\n  // Mesmo raciocínio pro halo do texto e pro fio que liga um rótulo deslocado ao seu município —\n  // os dois têm largura de traço fixa em unidades do viewBox, então sem reescalar iam virar uma\n  // mancha grossa (halo) ou uma barra (fio) no zoom alto em vez de ficarem do mesmo tamanho na tela.\n  svg.querySelectorAll('[data-sw]').forEach(el=>{\n    const base=+el.getAttribute('data-sw')||1;\n    el.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n  });\n  // Fio de rótulo deslocado é calculado UMA vez, no mundo inteiro de 700x500, pra evitar colisão\n  // com TODO outro rótulo — inclusive os que, no zoom atual, nem estão mais na tela. Quanto mais\n  // fundo o zoom, maior a fração da área visível que esse mesmo fio (comprimento fixo em unidades\n  // do mundo) passa a cobrir, virando um risco diagonal cortando o mapa até um rótulo que já saiu\n  // do enquadramento. Esconder o fio quando ele passa de uma fração do que está visível agora\n  // resolve isso sem precisar recalcular posição de rótulo a cada passo de zoom.\n  const viewDiag=Math.hypot(mapView.w,mapView.h);\n  svg.querySelectorAll('line.lead').forEach(ln=>{\n    const dx=(+ln.getAttribute('x2'))-(+ln.getAttribute('x1')), dy=(+ln.getAttribute('y2'))-(+ln.getAttribute('y1'));\n    ln.style.display=(Math.hypot(dx,dy)>viewDiag*0.35)?'none':'';\n  });\n}\nfunction zoomMapBy(factor){\n  const cx=mapView.x+mapView.w/2, cy=mapView.y+mapView.h/2;\n  let w=mapView.w/factor, h=mapView.h/factor;\n  w=Math.min(MAP_W, Math.max(MAP_W/MAP_ZOOM_MAX, w));\n  h=Math.min(MAP_H, Math.max(MAP_H/MAP_ZOOM_MAX, h));\n  let x=cx-w/2, y=cy-h/2;\n  x=Math.min(MAP_W-w, Math.max(0,x));\n  y=Math.min(MAP_H-h, Math.max(0,y));\n  mapView={x,y,w,h};\n  applyMapView();\n}\n// Mesma matemática do zoomMapBy, mas ancorada no cursor em vez do centro da view — o ponto do mapa\n// que está sob o mouse continua sob o mouse depois do zoom, o que faz o scroll parecer\n// contínuo/\"seamless\" em vez de recentralizar o mapa a cada girada da roda.\nfunction zoomMapAt(factor, clientX, clientY){\n  const svg=$('#map'); if(!svg) return;\n  const rect=svg.getBoundingClientRect(); if(!rect.width||!rect.height) return;\n  const fx=(clientX-rect.left)/rect.width, fy=(clientY-rect.top)/rect.height;\n  const wx=mapView.x+fx*mapView.w, wy=mapView.y+fy*mapView.h;\n  let w=mapView.w/factor, h=mapView.h/factor;\n  w=Math.min(MAP_W, Math.max(MAP_W/MAP_ZOOM_MAX, w));\n  h=Math.min(MAP_H, Math.max(MAP_H/MAP_ZOOM_MAX, h));\n  let x=wx-fx*w, y=wy-fy*h;\n  x=Math.min(MAP_W-w, Math.max(0,x));\n  y=Math.min(MAP_H-h, Math.max(0,y));\n  mapView={x,y,w,h};\n  applyMapView();\n}\n// Arrastar sempre fica DENTRO do canvas de 700x500 (clamp em [0, MAP_W-w] / [0, MAP_H-h]) — nunca\n// existe um x/y que deixa o viewBox parcialmente fora da área com conteúdo, então o mapa nunca\n// \"some\" da tela, só pode chegar até a borda do que existe pra ver.\nlet mapDrag=null, mapDidDrag=false;\nfunction mapPointerDown(e){\n  const svg=$('#map'); if(!svg) return;\n  mapDrag={x0:e.clientX,y0:e.clientY,vx0:mapView.x,vy0:mapView.y,rect:svg.getBoundingClientRect()};\n  mapDidDrag=false;\n  svg.classList.add('dragging');\n  // NÃO setPointerCapture aqui: capturar o ponteiro no <svg> reencaminha o click resultante pro\n  // próprio <svg> em vez do <path> do município embaixo do cursor — todo clique parava de chegar\n  // no onclick de reatribuição, não só depois de um arrasto de verdade. move/up escutam em `root`\n  // (nunca substituído por renderAll) em vez de no <svg>, então o arrasto continua funcionando\n  // mesmo se o ponteiro sair da área do mapa no meio do gesto.\n}\nfunction mapPointerMove(e){\n  if(!mapDrag) return;\n  const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n  if(!mapDidDrag && Math.hypot(dxPix,dyPix)>4) mapDidDrag=true;\n  if(!mapDrag.rect.width||!mapDrag.rect.height) return;\n  const sx=mapView.w/mapDrag.rect.width, sy=mapView.h/mapDrag.rect.height;\n  let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n  x=Math.min(MAP_W-mapView.w, Math.max(0,x));\n  y=Math.min(MAP_H-mapView.h, Math.max(0,y));\n  mapView={...mapView,x,y};\n  applyMapView();\n}\nfunction mapPointerUp(){\n  if(!mapDrag) return;\n  mapDrag=null;\n  const svg=$('#map'); if(svg) svg.classList.remove('dragging');\n}\n// ---------- OSM reference layers (rodovias/rios/picos) ----------\n// This map is a hand-projected SVG, not a Leaflet/Mapbox instance — there is no tile layer to drop a\n// basemap into. Vector geometry is the only shape that can ride the SAME pr() projection every\n// município shape already uses and land in the right spot. Scoped to the bbox of what mv.geoFeat +\n// mv.nbrFeat actually put on screen for THIS stage — never the whole country — because Overpass's\n// road/river graph for Brazil is enormous and the ask here is é \"what's around this closure\", not a\n// national basemap. Cached by (layer, bbox) rather than by session/stage index: two stages that\n// happen to look at the same patch of ground share one fetch, and switching sessions can never serve\n// a stale layer under a reused stage index.\nconst OSM_LAYERS=[\n  {key:'highways',label:'Rodovias',color:'#8e8e93',sw:0.9,\n   ql:b=>`way[\"highway\"~\"^(motorway|trunk|primary|secondary|tertiary)$\"](${b});`},\n  // Só rio PERMANENTE conta como barreira de verdade pra distância real entre municípios — riacho\n  // é atravessado por qualquer bueiro, e um rio \"intermitente\" (comum no semiárido nordestino) fica\n  // seco a maior parte do ano, então nenhum dos dois muda quanto tempo leva pra ir de um lugar ao\n  // outro. \"Picos\" saiu de vez: um ponto de pico não diz nada sobre se existe estrada ali ou quanto\n  // se precisa desviar pra contornar — a camada de rodovias já mostra isso diretamente.\n  {key:'rivers',label:'Rios',color:'#0a84ff',sw:1.3,\n   ql:b=>`way[\"waterway\"=\"river\"][\"intermittent\"!=\"yes\"](${b});`},\n];\nconst OSM_ON={highways:false,rivers:false};\n// Legendas default ON — é o comportamento de sempre (rótulo em toda unidade/área de contexto);\n// os checkboxes só existem pra permitir DESLIGAR num mapa carregado, não pra ligar algo novo.\nconst LEGEND_ON={muni:true,ctx:false};\nconst CHILD_PLURAL={Município:'municípios',Setor:'setores',GV:'GVs'};\nconst OSM_CACHE={};\nconst OSM_MAX_SPAN=6; // graus — acima disso (fechamento de mercado muito grande) não busca sozinho\nfunction osmBboxOf(feats){\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; })));});\n  if(!isFinite(mnx)) return null;\n  return {south:mny,west:mnx,north:mxy,east:mxx};\n}\n// Calling overpass-api.de straight from the browser died with a bare \"Failed to fetch\" on the\n// operator's own network (proved: the exact same call succeeded from a different network) — a\n// corporate firewall/proxy blocking that third-party domain, most likely. Routing through Data2's own\n// Rush listener (osmOverpassProxy → HttpClient → overpass-api.de) moves the outbound hop to Data2's\n// backend, which never touches the operator's restricted network at all.\nasync function ensureOsmLayer(def,bbox,ck){\n  OSM_CACHE[ck]={status:'loading',data:null,error:null};\n  const b=bbox.south.toFixed(4)+','+bbox.west.toFixed(4)+','+bbox.north.toFixed(4)+','+bbox.east.toFixed(4);\n  const query='[out:json][timeout:25];('+def.ql(b)+');out geom;';\n  const rec=OSM_CACHE[ck];\n  try{\n    const res=await d2.rush('osmOverpassProxy').invoke({data:query});\n    const env=res&&res.data;\n    if(!env) throw new Error('proxy sem resposta');\n    if(!env.ok) throw new Error(env.error||'falha desconhecida no proxy');\n    rec.data=env.elements||[]; rec.status='ok';\n  }catch(e){\n    rec.status='error'; rec.error=(e&&e.message)||'falha no proxy';\n    // Falha (504 do Overpass sobrecarregado, timeout etc.) desmarca o checkbox e esquece este cache\n    // — religar exige um novo clique, e esse clique já cai em ensureOsmLayer de novo (sem registro\n    // em OSM_CACHE pra essa bbox), disparando uma tentativa nova em vez de repetir o erro guardado.\n    delete OSM_CACHE[ck];\n    OSM_ON[def.key]=false;\n    const el=$('#osm'+def.key[0].toUpperCase()+def.key.slice(1)); if(el) el.checked=false;\n  }\n  drawMap();\n}\nfunction drawOsmLayer(svg,pr,feats,NS){\n  const st=$('#osmStatus');\n  const active=OSM_LAYERS.filter(l=>OSM_ON[l.key]); if(!active.length){ if(st) st.textContent=''; return; }\n  const bbox=osmBboxOf(feats); if(!bbox){ if(st) st.textContent=''; return; }\n  const tooBig=(bbox.north-bbox.south)>OSM_MAX_SPAN||(bbox.east-bbox.west)>OSM_MAX_SPAN;\n  const fsScale=mapView.w/MAP_W, msgs=[];\n  active.forEach(def=>{\n    if(tooBig){ msgs.push(def.label+': área grande demais p/ carregar automaticamente'); return; }\n    const bk=bbox.south.toFixed(3)+'_'+bbox.west.toFixed(3)+'_'+bbox.north.toFixed(3)+'_'+bbox.east.toFixed(3);\n    const ck=def.key+'|'+bk;\n    let rec=OSM_CACHE[ck];\n    if(!rec){ ensureOsmLayer(def,bbox,ck); rec=OSM_CACHE[ck]; }\n    if(rec.status==='loading') msgs.push(def.label+': carregando…');\n    else if(rec.status==='error') msgs.push(def.label+': falhou ('+rec.error+')');\n    if(rec.status!=='ok') return;\n    const g=document.createElementNS(NS,'g'); g.style.pointerEvents='none';\n    rec.data.forEach(el=>{\n      // Toda camada que sobrou é way (linha) — o único node-layer (picos) saiu do OSM_LAYERS acima.\n      if(!el.geometry||!el.geometry.length) return;\n      const d='M'+el.geometry.filter(Boolean).map(p=>{const q=pr([p.lon,p.lat]);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L');\n      const p=document.createElementNS(NS,'path'); p.setAttribute('d',d); p.setAttribute('fill','none');\n      p.setAttribute('stroke',def.color); p.setAttribute('opacity','0.72');\n      p.setAttribute('data-sw',def.sw); p.style.strokeWidth=(def.sw*fsScale).toFixed(3)+'px';\n      g.appendChild(p);\n    });\n    svg.appendChild(g);\n  });\n  if(st) st.textContent=msgs.join(' · ');\n}\n// Mede o espaço de viewport que sobra abaixo do topo do card do mapa e usa isso como altura —\n// não um percentual fixo — porque o que existe ACIMA do mapa varia (trilho de etapas e toolbar só\n// aparecem quando a sessão já tem movimentações), então só medir de verdade para exatamente na\n// borda da tela em qualquer estado.\nfunction fitMapHeight(){\n  const wrap=$('.map-wrap'); if(!wrap) return;\n  const top=wrap.getBoundingClientRect().top;\n  if(top<=0) return;\n  // Margem generosa de propósito: sobra um pedaço visível do card seguinte (líderes/matriz) na\n  // borda inferior da tela, como sinal de que tem mais conteúdo abaixo pra rolar.\n  const margin=80;\n  wrap.style.height=Math.max(220, window.innerHeight-top-margin)+'px';\n}\nfunction drawMap(){\n  const mv=SESS.timeline[SESS.selected]; const svg=$('#map'); const ignNote=$('#mapIgnoredNote');\n  if(!mv){ svg.innerHTML=''; if(ignNote) ignNote.innerHTML=''; return; }\n  const level=mv.level||'setor';\n  const legendMuniLbl=$('#legendMuniLbl');\n  if(legendMuniLbl) legendMuniLbl.textContent = level==='setor' ? 'Nome de municípios' : `Código de ${CHILD_PLURAL[LEVELS[level].childLabel]||LEVELS[level].childLabel}`;\n  // setor fecha: a unidade É o município. GV/mercado fecham: cada município herda a atribuição do\n  // setor/GV a que pertence — municípios do mesmo setor sempre têm a MESMA cor e giram JUNTOS no\n  // clique, porque é o setor inteiro (não o município) que troca de dono nesses níveis.\n  const unitAssign=m=>level==='setor'?mv.assign[m]:mv.assign[(mv.unitOfMuni||{})[m]];\n  const c=computeDerived(mv);const NS='http://www.w3.org/2000/svg'; svg.innerHTML='';\n  const feats=[...mv.nbrFeat,...Object.values(mv.geoFeat)]; if(!feats.length) return;\n  syncMapDims();\n  const pr=project(feats);\n  const world=foldWorldState(SESS.timeline,SESS.selected);\n  // The context layer is ONE area per SECTOR, not one shape per município. The unit of this decision\n  // is the destination sector; a scatter of individually outlined municipalities carrying their own\n  // names invited exactly the wrong reading of the map. Members of a sector are filled AND stroked in\n  // the same colour, so the shared borders vanish and the group reads as a single dissolved\n  // territory — the IBGE shapes do not share vertices precisely enough to extract a clean union\n  // outline, and carrying a real polygon-clipping union for a context layer is not worth it.\n  // Labels are COLLECTED here and drawn last, after every path. Drawn inline they were buried by the\n  // next sector's fill, and placement cannot avoid a collision it has not seen yet.\n  const labels=[],placed=[];\n  const textW=(s,fs)=>String(s).length*fs*0.56;\n  // Centred on (x,y) in BOTH axes. The old box hung off the first line's baseline, so every two-line\n  // label sat half its own height below the point it was supposed to mark.\n  const rectOf=(x,y,lines)=>{const w=Math.max.apply(null,lines.map(l=>textW(l.t,l.size)));\n    const h=lines.reduce((a,l)=>a+l.size*1.2,0);\n    return {x1:x-w/2-2.5,x2:x+w/2+2.5,y1:y-h/2-1.5,y2:y+h/2+1.5};};\n  const nbrBySec={};\n  mv.nbrFeat.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf);\n    const owners=mv.muniOwners&&mv.muniOwners[nm]?[...new Set(mv.muniOwners[nm])]:[];\n    if(owners.length>1){\n      owners.sort((a,b)=>(+a-+b)||(a<b?-1:a>b?1:0));\n      sliceFeatureEqual(f,owners.length).forEach((slice,i)=>{\n        if(slice.__empty) return;\n        (nbrBySec[owners[i]]=nbrBySec[owners[i]]||[]).push(slice);\n      });\n      return;\n    }\n    const s=String((mv.nbrCurrentSector&&mv.nbrCurrentSector[nm])||'');\n    (nbrBySec[s]=nbrBySec[s]||[]).push(f);\n  });\n  (mv.sharedMuniSlices||[]).forEach(sl=>{ (nbrBySec[sl.sector]=nbrBySec[sl.sector]||[]).push(sl.feature); });\n  Object.keys(nbrBySec).forEach(s=>{\n    const fs=nbrBySec[s];\n    const isDest=!!s&&mv.dest.includes(s);\n    const isRejected=!!s&&mv.rejectedCodes&&mv.rejectedCodes.has(s);\n    const closedCtx=!!s&&world.closedSectors.has(s);\n    const isUnresolved=!s;\n    const col=isDest?(mv.color[s]||null):null;\n    const rej=isRejected?(mv.rejectedInfo&&mv.rejectedInfo[s]):null;\n    // Sem união de polígono de verdade, a única forma de emular UM contorno em volta do setor\n    // inteiro é desenhar essa borda ANTES do preenchimento: cada aresta INTERNA (entre dois membros\n    // do mesmo setor) fica coberta pelo preenchimento opaco do vizinho por cima, e só a aresta\n    // EXTERNA (sem vizinho do mesmo setor do outro lado) continua visível — e, se dois setores\n    // \"inválidos\" diferentes se tocam, cada um desenha sua própria borda naquele limite, então a\n    // linha aparece do mesmo jeito, sem sumir.\n    if(isRejected) fs.forEach(f=>{\n      const bp=document.createElementNS(NS,'path');\n      bp.setAttribute('d',pathFor(f,pr));\n      bp.setAttribute('class','sect-rejected-border');\n      svg.appendChild(bp);\n    });\n    fs.forEach(f=>{\n      const p=document.createElementNS(NS,'path');\n      p.setAttribute('d',pathFor(f,pr));\n      p.setAttribute('class','sect'+(isDest?'':(isRejected?' rejected':(closedCtx?' closed-ctx':(isUnresolved?' unresolved':' plain')))));\n      // isDest: NO stroke at all, fill only. Matching stroke to fill (even at the same opacity)\n      // still doubles up at every shared município edge — two adjacent members each paint their own\n      // translucent stroke on top of the shared line, so it composites darker than the interior even\n      // when both strokes are the exact fill colour. Only an absent stroke truly stops compositing.\n      if(col){ p.setAttribute('fill',col); }\n      if(rej){\n        const why=level==='setor'\n          ? `${LEVELS.setor.destLabel} ${s} é da GV ${rej.parent||'?'} — a GV do setor fechando é a ${mv.gv||'?'}, então não entra como destino.`\n          : `${LEVELS.gv.destLabel} ${s} é do mercado ${rej.parent||'?'} — o mercado da GV fechando é o ${mv.gv||'?'}, então não entra como destino.`;\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';t.innerHTML=why;};\n        p.onmouseleave=()=>$('#tip').style.opacity=0;\n      }\n      if(isUnresolved){\n        // Setor não encontrado pro município vizinho — mostra o NOME real do município no hover em vez\n        // de deixar em branco ou fingir um destino. Estado de debug de propósito: ajuda a achar rápido\n        // qual município ficou sem sector resolvido na base/consulta ao vivo.\n        const nmDisp=mDisp(mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf));\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n          t.innerHTML=`<b>${nmDisp}</b><br><span style=\"color:var(--mut)\">setor não encontrado — sem CBs neste snapshot ou não bateu com a base</span>`;};\n        p.onmouseleave=()=>$('#tip').style.opacity=0;\n      }\n      svg.appendChild(p);\n    });\n    if(!s) return;\n    // One number per sector, at the pole of inaccessibility of its DISSOLVED area — the whole group\n    // of members at once, not a representative member, so the number sits in the visual middle of the\n    // territory the operator actually sees.\n    const lp=labelPointOf(mv,'s:'+s,fs,pr);\n    // Context areas always read LIGHT — .sect paints dest colours at fill-opacity .32 over white, and\n    // plain/closed-ctx are near-white greys — so a sector number never wants white ink, whatever colour\n    // the sector itself was assigned.\n    // A leader line is only worth drawing when there's real room to write into — otherwise a big\n    // multi-sector município (São Paulo etc., dozens of slivers) turns into a canvas-wide tangle of\n    // lines, one per sliver too thin to ever hold readable text. lp.r is the radius of the biggest\n    // circle that fits inside the slice; below this it isn't worth labelling at all.\n    if(LEGEND_ON.ctx && lp&&lp.p&&lp.r>=5) labels.push({anchor:lp.p,big:true,lines:[{t:s,size:11.5,weight:700}]});\n  });\n  const asis=mv.view==='asis';\n  // The UF prefix is information only when the stage actually straddles a border. On a single-UF setor\n  // it is the same three characters repeated on every label — a large share of the clutter and none of\n  // the meaning. The hover tooltip carries the UF either way.\n  const showUf=new Set(Object.keys(mv.geoFeat).map(k=>mUf(k)).filter(Boolean)).size>1;\n  if(level==='setor'){\n    // Setor fechando: a UNIDADE é o próprio município — cada um se comporta, colore e rotula\n    // individualmente, exatamente como sempre foi.\n    Object.entries(mv.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n      const fillCol=asis?CLOSE_COLOR:(mv.color[unitAssign(m)]||'#444');\n      p.setAttribute('class','muni');p.setAttribute('fill',fillCol);\n      if(!asis) p.onclick=()=>{editSelected(m2=>{if(m2.dest.length)m2.assign[m]=(SESS.settings&&SESS.settings.forceContiguity)?nextContigSafeDest(m2,m):m2.dest[(m2.dest.indexOf(m2.assign[m])+1)%m2.dest.length];m2.selMuni=m;});};\n      p.style.cursor=asis?'default':'pointer';\n      p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n        t.innerHTML=asis?`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs · setor ${mv.sector}`:`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs → ${unitAssign(m)||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n      p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n      const lp=labelPointOf(mv,'m:'+m,[f],pr);\n      const at=(lp&&lp.p)||cxy(f,pr);\n      const nmFull=(showUf&&mUf(m)?mUf(m)+'-':'')+mName(m);\n      if(LEGEND_ON.muni) labels.push({anchor:at,cbs:+(mv.cbsByMuni[m]||0),\n        lines:[{t:nmFull,size:8.2,weight:700}]});\n    });\n  } else {\n    // GV/mercado fechando: a UNIDADE é o setor (ou a GV) inteiro, nunca o município isolado — vários\n    // municípios do MESMO setor têm de ler como UMA área só (stroke = fill inline, opaco, então não há\n    // o artefato de dupla-composição da camada de destino: aqui a cor é sólida, não translúcida).\n    // A fronteira que o operador precisa ver é setor-a-setor, não município-a-município, e isso já\n    // sai de graça da própria cor: setores com destinos diferentes contrastam sem precisar de traço\n    // algum. O único caso que fica sem contorno é dois setores vizinhos caindo no MESMO destino — sem\n    // union real de polígono (ver nota da camada de contexto, acima) não há como bordear só o setor\n    // sem também bordear cada município dentro dele, e a segunda opção é a bagunça pontilhada que essa\n    // troca veio corrigir. UM rótulo só por unidade, no ponto de inacessibilidade da área do grupo,\n    // nunca um rótulo por município.\n    Object.entries(mv.unitMunis||{}).forEach(([u,ms])=>{\n      const fillCol=asis?CLOSE_COLOR:(mv.color[mv.assign[u]]||'#444');\n      const feats=ms.map(m=>mv.geoFeat[m]).filter(Boolean);\n      // Todo município deste u é UMA unidade só na tela — passar o mouse sobre qualquer pedaço tem\n      // que acender o grupo inteiro, não só o pedaço que o cursor está tocando. :hover no CSS só\n      // sabe acender o próprio <path>; o grupo inteiro precisa de JS guardando os irmãos.\n      const els=[];\n      ms.forEach(m=>{\n        const f=mv.geoFeat[m]; if(!f) return;\n        const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n        p.setAttribute('class','muni');p.setAttribute('fill',fillCol);p.style.stroke=fillCol;\n        els.push(p);\n        if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[u]);if(m2.dest.length)m2.assign[u]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=u;});};\n        p.style.cursor=asis?'default':'pointer';\n        p.onmouseenter=()=>{ els.forEach(e=>e.classList.add('munihl')); };\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n          t.innerHTML=asis?`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs · ${LEVELS[level].label} ${mv.sector}`:`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs → ${mv.assign[u]||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n        p.onmouseleave=()=>{ els.forEach(e=>e.classList.remove('munihl')); $('#tip').style.opacity=0; };svg.appendChild(p);\n      });\n      const lp=labelPointOf(mv,'u:'+u,feats,pr);\n      const at=(lp&&lp.p)||(feats[0]&&cxy(feats[0],pr));\n      if(at && LEGEND_ON.muni){\n        const num=fmt(mv.munis[u]?.cbs||0)+(asis?'':'·'+(mv.assign[u]||'–'));\n        labels.push({anchor:at,big:true,cbs:+(mv.munis[u]?.cbs||0),\n          lines:[{t:`${LEVELS[level].childLabel} ${u}`,size:9,weight:700},{t:num,size:7.5,weight:500,mut:true}]});\n      }\n    });\n  }\n  // Placement, not de-cluttering. Every label IS drawn: it starts at its anchor and, if the text does\n  // not fit the territory or the slot is already taken, walks outward ring by ring until it finds\n  // clear canvas, trailing a hairline leader back to the shape it names. The previous pass dropped the\n  // loser of every collision outright, which is why municípios silently had no name at all.\n  // Order still matters — sector numbers first, then municípios by CB weight — because the earlier a\n  // label is placed the closer to home it lands.\n  const CW=700,CH=500,CPAD=2;\n  const inBounds=r=>r.x1>=CPAD&&r.x2<=CW-CPAD&&r.y1>=CPAD&&r.y2<=CH-CPAD;\n  const hits=r=>placed.reduce((a,q)=>a+((r.x2<q.x1||r.x1>q.x2||r.y2<q.y1||r.y1>q.y2)?0:1),0);\n  // Um rótulo deslocado não pode só EVITAR a caixa de outro — o fio que o liga de volta ao seu\n  // ponto de origem também não pode atravessar por cima do texto de um rótulo já colocado, senão o\n  // fio some por baixo das letras (ou risca por cima delas) do vizinho.\n  const segSeg=(ax,ay,bx,by,cx,cy,dx,dy)=>{\n    const d1=(dx-cx)*(ay-cy)-(dy-cy)*(ax-cx), d2=(dx-cx)*(by-cy)-(dy-cy)*(bx-cx);\n    const d3=(bx-ax)*(cy-ay)-(by-ay)*(cx-ax), d4=(bx-ax)*(dy-ay)-(by-ay)*(dx-ax);\n    return ((d1>0)!==(d2>0))&&((d3>0)!==(d4>0));\n  };\n  const segRect=(x1,y1,x2,y2,r)=>{\n    const inR=(x,y)=>x>=r.x1&&x<=r.x2&&y>=r.y1&&y<=r.y2;\n    if(inR(x1,y1)||inR(x2,y2)) return true;\n    return segSeg(x1,y1,x2,y2,r.x1,r.y1,r.x2,r.y1)||segSeg(x1,y1,x2,y2,r.x2,r.y1,r.x2,r.y2)\n        ||segSeg(x1,y1,x2,y2,r.x2,r.y2,r.x1,r.y2)||segSeg(x1,y1,x2,y2,r.x1,r.y2,r.x1,r.y1);\n  };\n  // Ring 0 is the anchor itself. Rings step 9u out over 16 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs.\n  // Ring 0 is the anchor itself. Rings step 8u out over 20 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs. Pushed to 16\n  // rings (was 7, 16 directions) — \"labels should never overlap\" only holds if the search genuinely\n  // exhausts the nearby space before giving up; a shallower search gave up too soon on crowded\n  // clusters and just stacked labels on top of each other instead of finding a real clear spot.\n  const CAND=[[0,0]];\n  for(let ring=1;ring<=16;ring++){ const d=ring*8;\n    for(let a=0;a<20;a++){ const th=a*Math.PI/10; CAND.push([Math.cos(th)*d*1.35,Math.sin(th)*d]); } }\n  drawOsmLayer(svg,pr,feats,NS);\n  labels.sort((a,b)=>(b.big?1:0)-(a.big?1:0)||((b.cbs||0)-(a.cbs||0)));\n  labels.forEach(L=>{\n    let chosen=null;\n    // Centred on the anchor is ALWAYS the first choice tried (CAND[0]=[0,0]), at full size. If the\n    // whole ringed search comes up empty, shrink the label once (82%) and search again — a smaller\n    // box clears crowded clusters no position at full size ever could.\n    for(const shrink of [1,0.82]){\n      const lines=shrink===1?L.lines:L.lines.map(l=>({...l,size:l.size*shrink}));\n      for(const off of CAND){\n        const x=L.anchor[0]+off[0], y=L.anchor[1]+off[1];\n        const r=rectOf(x,y,lines);\n        if(!inBounds(r)) continue;\n        if(hits(r)) continue;\n        if(Math.abs(off[0])>0.5||Math.abs(off[1])>0.5){\n          const cx=(r.x1+r.x2)/2, cy=(r.y1+r.y2)/2;\n          if(placed.some(q=>segRect(L.anchor[0],L.anchor[1],cx,cy,q))) continue;\n        }\n        chosen={x,y,r,off,lines}; break;\n      }\n      if(chosen) break;\n    }\n    // No slot anywhere — even shrunk — earns dropping the label entirely rather than stamping it on\n    // top of a neighbour. An absent label is legible; an overlapping one is exactly the mess this\n    // pass exists to remove, so it never gets a \"least-bad\" fallback the way it used to.\n    if(!chosen) return;\n    placed.push(chosen.r);\n    const g=document.createElementNS(NS,'g'); g.setAttribute('class','lbl'+(L.big?' big':''));\n    if(Math.abs(chosen.off[0])>0.5||Math.abs(chosen.off[1])>0.5){\n      // The leader stops at the label's edge rather than its centre, so it never runs under the glyphs.\n      const cx=(chosen.r.x1+chosen.r.x2)/2, cy=(chosen.r.y1+chosen.r.y2)/2;\n      const hw=(chosen.r.x2-chosen.r.x1)/2, hh=(chosen.r.y2-chosen.r.y1)/2;\n      const dx=L.anchor[0]-cx, dy=L.anchor[1]-cy;\n      const tt=Math.min(dx?hw/Math.abs(dx):Infinity, dy?hh/Math.abs(dy):Infinity);\n      const ln=document.createElementNS(NS,'line'); ln.setAttribute('class','lead');\n      ln.style.stroke='#1c1c1e'; ln.style.strokeOpacity='0.2';\n      // A largura do fio também é fixa em unidades do viewBox, igual o halo do texto — sem\n      // reescalar junto, no zoom alto ela vira uma barra grossa em vez de um fio fino.\n      ln.setAttribute('data-sw','0.8'); ln.style.strokeWidth='0.8px';\n      ln.setAttribute('x1',L.anchor[0].toFixed(1)); ln.setAttribute('y1',L.anchor[1].toFixed(1));\n      ln.setAttribute('x2',(cx+dx*tt).toFixed(1)); ln.setAttribute('y2',(cy+dy*tt).toFixed(1));\n      g.appendChild(ln);\n    }\n    const hTot=chosen.lines.reduce((a,l)=>a+l.size*1.2,0);\n    let ty=chosen.y-hTot/2+chosen.lines[0].size*0.86;\n    // Inline style rather than a class: a stylesheet rule would outrank a presentation attribute and\n    // undo the per-label contrast decision made above.\n    // One consistent style for every label, regardless of what's underneath — white text, black\n    // outline, always. Mixed ink/halo colors chosen per-fill (dark labels on dark fills, black labels\n    // on light ones) read as inconsistent across a crowded map; a single style reads as one system.\n    const ink='#fff', inkMut='rgba(255,255,255,.85)';\n    const halo='rgba(0,0,0,.72)';\n    // Halo do texto (o \"contorno\" que garante legibilidade sobre qualquer cor de fundo) também é\n    // fixo em unidades do viewBox via .lbl text{stroke-width}/.lbl.big text{stroke-width} — sem\n    // reescalar junto com o font-size, no zoom alto ele vira uma mancha escura cobrindo a letra\n    // inteira em vez de um contorno fino, exatamente o \"Setor 3570\" ilegível reportado.\n    const swBase=L.big?2.4:1.8;\n    chosen.lines.forEach(l=>{ const t=document.createElementNS(NS,'text');\n      t.setAttribute('x',chosen.x.toFixed(1)); t.setAttribute('y',ty.toFixed(1));\n      t.setAttribute('font-size',l.size); t.setAttribute('data-fs',l.size); t.setAttribute('font-weight',l.weight||600);\n      t.setAttribute('data-sw',swBase); t.style.strokeWidth=swBase+'px';\n      t.style.fill=l.mut?inkMut:ink; t.style.stroke=halo;\n      t.textContent=l.t; g.appendChild(t); ty+=l.size*1.2; });\n    svg.appendChild(g);\n  });\n  const rejectedLegend=(!asis && mv.gvRejected && mv.gvRejected.length)\n    ? `<span><b style=\"background:#e4e4ea;border:1px solid #9797a3\"></b>fora da hierarquia (${mv.gvRejected.length}) — passe o mouse no mapa</span>`\n    : '';\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>${LEVELS[level].label} ${mv.sector} (estrutura antes desta etapa — ${level==='setor'?Object.keys(mv.geoFeat).length+' municípios':Object.keys(mv.unitMunis||{}).length+' '+LEVELS[level].childLabel.toLowerCase()+'(s)'})</span>`\n    // \"vizinhos (contexto)\"/\"setor já fechado antes\" (.sect.plain/.sect.closed-ctx) still never\n    // legend — those stay dead. \"rejected\" is the one gray key that's real again: it marks a\n    // geographically-touching setor/GV excluded for failing the hierarchy rule, appended below.\n    :mv.dest.map(s=>`<span><b style=\"background:${mv.color[s]}\"></b>${LEVELS[level].destLabel} ${s} — ${c.gained[s]} líder(es)</span>`).join('')+rejectedLegend;\n  // Municipalities outside the dominance base are deliberately NOT drawn — no shape, no hover, no\n  // legend entry. They are reported in the panel instead, so the map only ever shows territory that\n  // is actually being reallocated.\n  if(ignNote) ignNote.innerHTML=(mv.excludedMunis&&mv.excludedMunis.length)\n    ? `<b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs||0)} CBs</b> fora da base de dominantes deste setor não são desenhados no mapa — as CBs seguem a líder do seu grupo (detalhe em “Municípios → setor destino”).`\n    : '';\n  applyMapView();\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{ await ensurePinned(); const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nfunction renderAll(){\n  renderStage();\n  fitMapHeight();\n  renderTimeline(); renderSectorPicker();\n  const shb=$('#openShare'); if(shb) shb.disabled=!SESS.timeline.length;\n  const dpb=$('#dupSession'); if(dpb) dpb.disabled=!SESS.timeline.length;\n  const rnb=$('#renameSession'); if(rnb) rnb.disabled=!SESS.timeline.length;\n  const mv=SESS.timeline[SESS.selected];\n  $('#liveState').textContent = SESS.snapshotRaw\n    ? 'snapshot atualizado em '+fmtSyncedAt(SESS.snapshotSyncedAt)\n    : '● iniciando';\n  renderRuleNote(); renderSpanRule();\n  if(!mv){ ['#map','#matrix','#matrixTobe','#tobeNote','#leaders'].forEach(s=>{const el=$(s);if(el)el.innerHTML='';}); $('#kpis').innerHTML=''; $('#spans').innerHTML=''; $('#muniList').innerHTML=''; $('#plan').innerHTML=''; return; }\n  $('#viewToggle').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.v===mv.view));\n  $('#policy').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===mv.policy));\n  $('#optBadge').textContent=mv.optBadge||'';\n  drawMap(); renderContig(); renderSpanRule(); renderKpis(); renderSpans(); renderMuni(); renderLeaders(); renderMatrix(); renderMatrixTobe(); renderPolicy(); renderPlan(); renderReport(); renderCumulative();\n}\n\n// ---------- event wiring ----------\nwindow.addEventListener('resize', ()=>{ fitMapHeight(); drawMap(); });\n$('#confirmSnap').onclick=()=>{ SESS.stage='main'; renderAll(); };\n$('#openMove').onclick=openMove;\n$('#moveClose').onclick=closeMove;\n$('#moveCancel').onclick=closeMove;\n$('#moveBg').addEventListener('click',e=>{ if(e.target.id==='moveBg') closeMove(); });\nroot.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>r.addEventListener('change',renderMoveForm));\n$('#moveRand').onclick=moveRandPick;\n$('#moveGo').onclick=submitMove;\n$('#moveCode').addEventListener('keydown',e=>{ if(e.key==='Enter') submitMove(); });\n$('#confirmSessionPick').onchange=()=>loadSession($('#confirmSessionPick').value);\n$('#optim').onclick=()=>editSelected(mv=>{ mv.assign=optimizeFor(mv); });\n$('#autoTobe').onclick=()=>editSelected(mv=>{ mv.assign=autoAssignFor(mv); mv.optBadge='TOBE por adjacência'; });\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.view=b.dataset.v;renderAll();});\n$('#mapZoomIn').onclick=()=>zoomMapBy(1.4);\n$('#mapZoomOut').onclick=()=>zoomMapBy(1/1.4);\n['highways','rivers'].forEach(k=>{ const el=$('#osm'+k[0].toUpperCase()+k.slice(1)); if(el) el.onchange=()=>{ OSM_ON[k]=el.checked; drawMap(); }; });\n['muni','ctx'].forEach(k=>{ const el=$('#legend'+(k==='muni'?'Muni':'Ctx')); if(el) el.onchange=()=>{ LEGEND_ON[k]=el.checked; drawMap(); }; });\n$('#map').addEventListener('pointerdown',mapPointerDown);\n$('#map').addEventListener('wheel',e=>{ e.preventDefault(); zoomMapAt(e.deltaY<0?1.15:1/1.15, e.clientX, e.clientY); }, {passive:false});\nroot.addEventListener('pointermove',mapPointerMove);\nroot.addEventListener('pointerup',mapPointerUp);\nroot.addEventListener('pointerleave',mapPointerUp);\n// Arrastar não pode virar \"cliquei num município e troquei o destino sem querer\": se o ponteiro se\n// moveu além do limiar de arrasto, este clique (capture, antes de chegar no <path>) é engolido.\n$('#map').addEventListener('click',e=>{ if(mapDidDrag){ e.stopPropagation(); e.preventDefault(); mapDidDrag=false; } },true);\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.policy=b.dataset.p;\n  $('#policy').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));\n  renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','tobe','report','cumulative','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;navigator.clipboard.writeText(JSON.stringify({setor:mv.sector,politica:mv.policy,plano:mv._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;const csv='municipio,uf,cbs,setor_destino\\n'+mv._plan.map(p=>`${p.municipio},${p.uf||''},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${mv.sector}.csv`;a.click();};\n$('#saveSession').onclick=saveSession;\n$('#dupSession').onclick=duplicateSession;\n$('#renameSession').onclick=renameSession;\n$('#sessionPick').onchange=()=>loadSession($('#sessionPick').value);\n$('#liveState').onclick=refreshSnapshot;\n$('#liveState').addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); refreshSnapshot(); } });\n$('#openOptions').onclick=openOptions;\n$('#optClose').onclick=closeOptions;\n$('#optCancel').onclick=closeOptions;\n$('#optApply').onclick=applyOptions;\n['setor','gv'].forEach(lvl=>['#optTargetSpan_','#optMaxSpan_','#optMinSpan_'].forEach(pfx=>$(pfx+lvl).addEventListener('input',renderOptPreview)));\n$('#optionsBg').addEventListener('click',e=>{ if(e.target.id==='optionsBg') closeOptions(); });\nroot.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeOptions(); closeShare(); closeMove(); } });\n\n$('#openShare').onclick=openShare;\n$('#shareX').onclick=closeShare;\n$('#shareCancel').onclick=closeShare;\n$('#shareGo').onclick=doShare;\n$('#shareCopy').onclick=()=>{ if(!SHARE.url) return; navigator.clipboard.writeText(SHARE.url);\n  const b=$('#shareCopy'); if(b){ b.textContent='copiado ✓'; setTimeout(()=>{ const x=$('#shareCopy'); if(x) x.textContent='Copiar link'; },1500); } };\n$('#shareOpen').onclick=()=>{ if(SHARE.url) window.open(SHARE.url,'_blank'); };\n$('#shareBg').addEventListener('click',e=>{ if(e.target.id==='shareBg') closeShare(); });\n\n// ---------- init ----------\n(async()=>{\n  loader(true,'congelando snapshot da sessão…');\n  try{\n    await Promise.all([ensurePinned(), loadSectors(), refreshSessionPicker()]);\n    SESS.stage='confirm';\n  }catch(e){ setError('erro init: '+e.message); }\n  finally{ loader(false); renderAll(); const rd=$('#ready'); if(rd) rd.setAttribute('data-loaded','1'); }\n})();\n\n  }\n}\n"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}},"dominantesCsvUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dominantesCsvUrl"}}}}}}}}}},{"_id":"6a7f2f3de811f18f500dca71","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"pS1W650RpCdVhLm9IkwFi","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const T='product.perfil_cb.crm_perfil_cb_diaria_historico';\nconst ELO='product.elo_modelo_combinado.commercial_consultant_index';\nconst PALETTE=['#5856d6','#ff9500','#28cd41','#af52de','#ff2d55','#a2845e'];\nconst CLOSE_COLOR='#8e8e93';\nconst UF_CODE={RO:'11',AC:'12',AM:'13',RR:'14',PA:'15',AP:'16',TO:'17',MA:'21',PI:'22',CE:'23',RN:'24',PB:'25',PE:'26',AL:'27',SE:'28',BA:'29',MG:'31',ES:'32',RJ:'33',SP:'35',PR:'41',SC:'42',RS:'43',MS:'50',MT:'51',GO:'52',DF:'53'};\nconst UF_BORDERS={AC:['AM','RO'],AL:['BA','PE','SE'],AM:['AC','RO','RR','PA','MT'],AP:['PA'],\n BA:['AL','SE','PE','PI','TO','GO','MG','ES'],CE:['PI','PB','PE','RN'],DF:['GO','MG'],\n ES:['BA','MG','RJ'],GO:['DF','MT','MS','MG','BA','TO'],MA:['PA','TO','PI'],\n MG:['BA','GO','DF','MS','SP','RJ','ES'],MS:['MT','GO','MG','SP','PR'],MT:['RO','AM','PA','TO','GO','MS'],\n PA:['AP','AM','MT','TO','MA','RR'],PB:['RN','CE','PE'],PE:['PB','CE','PI','BA','AL'],\n PI:['MA','TO','BA','PE','CE'],PR:['SP','MS','SC'],RJ:['ES','MG','SP'],RN:['CE','PB'],\n RO:['AC','AM','MT'],RR:['AM','PA'],RS:['SC'],SC:['PR','RS'],SE:['BA','AL'],\n SP:['MG','RJ','PR','MS'],TO:['PA','MA','PI','BA','GO','MT']};\nconst NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\nconst METRICS_DEF=[['cbs','CBs'],['ativas','Ativas'],['pedidos','Pedidos'],['pontos','Pontos'],['receita','Receita','$'],['rec_nat','Rec. Natura','$'],['rec_avon','Rec. Avon','$']];\nconst OPT_K=1.5;\nconst SESSIONS_DS='timeline-shared-sessionsDataSet';   // same table as SHARED_DS — see notes at listSessions()\n// Span defaults are the real operating band, not placeholders: setor 2200 ≤ span ≤ 3500 (alvo 2700),\n// GV 10000 ≤ span ≤ 22000 (alvo 16000). Mercado não tem um histórico equivalente ainda, então nasce\n// sem limite (null) até alguém configurar. Span agora é uma escala POR NÍVEL: fechar um setor só olha\n// span.setor, fechar uma GV só olha span.gv, fechar um mercado só olha span.mercado — nunca se misturam.\n// Existing sessions are NOT touched — their persisted settings carry explicit values that spread OVER\n// these in openOptions/applyOptions, which is deliberate: their etapas were computed under the old\n// band and changing it silently would leave the stored result and the rule that produced it out of sync.\nconst DEFAULT_SETTINGS={span:{\n  setor:{targetSpan:2700,maxSpan:3500,minSpan:2200},\n  gv:{targetSpan:16000,maxSpan:22000,minSpan:10000},\n  mercado:{targetSpan:null,maxSpan:null,minSpan:null},\n},\n  forceContiguity:true,\n};\nconst DOM_MEDIA_BASE='https://natura-geomarketing.data2.media/file/';\nconst DOM_CSV_FALLBACK=DOM_MEDIA_BASE+'yw_TQdp0QXEk6JsrliAqP'; \nconst numOrNull=v=>{const n=Math.round(+v);return (isFinite(n)&&n>0)?n:null;};\nconst spanShort=st=>{const p=[];if(st&&st.targetSpan)p.push('alvo '+st.targetSpan);if(st&&st.maxSpan)p.push('máx '+st.maxSpan);if(st&&st.minSpan)p.push('mín '+st.minSpan);return p.length?p.join(' · '):'livre';};\n\nexport default {\n  css: `\n  :host{display:block;height:100%;--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#28cd41;\n    /* Tokens do AppKit real (macOS Big Sur): raio 6 e sombra y1/blur3/15% preto são os valores\n       documentados de botão/controle do sistema, não um chute — usados em todo botão, select,\n       segmented control e tab pra ler como controle nativo em vez de elemento web plano. */\n    --radius-ctrl:6px;--shadow-ctrl:0 1px 3px rgba(0,0,0,.15)}\n  *{box-sizing:border-box}\n  .simroot{position:relative;height:100%;overflow:auto;font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}\n  header{padding:10px 16px;background:rgba(246,246,248,.85);backdrop-filter:blur(20px) saturate(1.8);-webkit-backdrop-filter:blur(20px) saturate(1.8);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n  header h1{font-size:14.5px;margin:0;font-weight:600;letter-spacing:-.1px;color:var(--ink)}\n  .mac-spacer{flex:1}\n  .howto{padding:8px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--mut);align-items:center}\n  .howto b{color:var(--ink)} .howto span{max-width:340px}\n  .pill{font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  .pill.live{color:var(--ink);cursor:pointer;user-select:none;transition:border-color .1s,color .1s,background .1s}\n  .pill.live:hover{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  .pill.live:focus-visible{outline:2px solid rgba(0,122,255,.35);outline-offset:1px}\n  .pill.live.busy{border-color:var(--acc);color:var(--acc);cursor:progress}\n  .status{font-size:11px;color:var(--mut)}.status.err{color:var(--closing)}\n  select,input,textarea,button{font:inherit}\n  select,input{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:var(--radius-ctrl);padding:5.5px 9px;box-shadow:var(--shadow-ctrl)}\n  /* Seta nativa do <select> cola na borda direita sem respiro nenhum — appearance:none tira a seta\n     do sistema e desenha uma própria, com o mesmo respiro (9px) do padding horizontal do resto. */\n  select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:26px;\n    background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5.5 8L10 12.5L14.5 8' fill='none' stroke='%238e8e93' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n    background-repeat:no-repeat;background-position:right 9px center;background-size:13px}\n  select:focus,input:focus{outline:2px solid rgba(0,122,255,.35);outline-offset:0;border-color:var(--acc)}\n  button.act{background:var(--acc);color:#fff;border:1px solid var(--acc2);border-radius:var(--radius-ctrl);padding:6px 13px;font-weight:600;cursor:pointer;box-shadow:var(--shadow-ctrl);transition:background .1s}\n  button.act:hover{background:var(--acc2)}\n  button.act:active{background:#0052c4;box-shadow:inset 0 1px 2px rgba(0,0,0,.25)}\n  button.ghost{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-ctrl);padding:5.5px 12px;cursor:pointer;box-shadow:var(--shadow-ctrl);transition:background .1s}\n  button.ghost:hover{background:var(--panel2)}\n  button.ghost:active{background:#e5e5ea;box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}\n  button.ghost.on{border-color:var(--acc);color:var(--acc);background:#eaf3ff}\n  button.ghost:disabled,button.act:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}\n  .sessbar{padding:9px 20px;background:var(--panel2);border-bottom:1px solid var(--line);display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n  .rail{padding:9px 16px;background:var(--panel);border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:center;overflow-x:auto}\n  #movementChips{display:flex;gap:8px;flex:0 0 auto}\n  #addMovementGroup{display:flex;gap:8px;align-items:center;flex:0 0 auto;margin-left:auto;padding-left:12px;border-left:1px solid var(--line)}\n  #addMovementGroup input{min-width:150px}\n  .tchip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px;flex:0 0 auto}\n  .tchip.on{border-color:var(--acc);background:#eaf3ff}\n  .tchip.stale{border-style:dashed;opacity:.85}\n  .tchip .tstale{color:#ff9500;font-weight:700}\n  .tchip .tnum{background:var(--ink);color:#fff;border-radius:99px;width:16px;height:16px;font-size:9.5px;display:flex;align-items:center;justify-content:center;font-weight:700}\n  .tchip .tsec{font-weight:600}\n  .tchip .tstat{color:var(--mut)}\n  .tchip .trm{color:var(--mut);font-weight:700;padding:0 3px}\n  .tchip .trm:hover{color:var(--closing)}\n  .rail .empty{color:var(--mut);font-size:12px}\n  .wrap{display:grid;grid-template-columns:minmax(360px,1fr) 430px;gap:14px;padding:14px 16px;align-items:start}\n  @media(max-width:1150px){.wrap{grid-template-columns:1fr}}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,.08)}\n  .card h2{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:0 0 9px;font-weight:600}\n  /* user-select, not pointer-events. .lbl already sets pointer-events:none, which stops the text from\n     RECEIVING clicks but does not exclude it from the browser's selection range — so cycling a\n     município's destination with repeated clicks kept dragging a double-click selection across the\n     labels. Nothing drawn in an svg here is text anyone needs to copy. */\n  svg{width:100%;height:auto;display:block;user-select:none;-webkit-user-select:none}\n  /* O <svg> tinha width:100%;height:auto — a altura era só uma função da LARGURA da coluna, então\n     numa tela larga e curta o mapa virava mais alto que o espaço vertical sobrando e a página\n     inteira crescia além da tela. max-height no wrapper + height:100%;width:auto;max-width:100% no\n     próprio svg é o equivalente de object-fit:contain: o navegador encaixa o viewBox 700x500 (via\n     preserveAspectRatio, que já existia) dentro do que sobrar de altura OU largura, o que for menor\n     primeiro, sem distorcer nem cortar nada. */\n  /* Altura real vem de fitMapHeight() (JS): mede quanto sobrou de viewport abaixo do topo do\n     card e aplica isso como altura inline — assim o mapa para exatamente na borda da tela\n     independente do que tem acima (cabeçalho, trilho de etapas, toolbar) ou abaixo dele.\n     60vh aqui é só o valor antes do primeiro paint do JS rodar. */\n  .map-wrap{position:relative;height:60vh;display:flex;justify-content:center;overflow:hidden}\n  #map{cursor:grab;touch-action:none;width:100%;height:100%}\n  #map.dragging{cursor:grabbing}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 4px 14px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.08);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  .osm-panel{position:absolute;left:10px;top:10px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 4px 14px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.08);padding:8px 10px;font-size:11.5px;z-index:3;min-width:104px}\n  .osm-panel-hd{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin-bottom:6px}\n  .osm-group-hd{font-size:9.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin:2px 0 4px}\n  .osm-group-hd.sep{margin-top:8px;padding-top:8px;border-top:1px solid var(--line)}\n  .osm-row{display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer;color:var(--ink)}\n  .osm-row.osm-sub{padding-left:14px;font-size:10.5px;color:var(--mut)}\n  .osm-row input{accent-color:var(--acc);margin:0}\n  .osm-status{margin-top:6px;font-size:10px;color:var(--mut);max-width:150px;line-height:1.4}\n  /* White hairlines inside the closing sector: the municipios read as one territory cut into parts\n     rather than as separate outlined units, and the saturated fills carry the grouping instead of the\n     ink. Neighbour sectors keep their own coloured strokes (.sect) — that contrast is what separates\n     what is being reallocated from what is only context. Selection no longer draws an outline; the\n     white border was the click affordance AND the default is now white, so it had nothing left to say. */\n  .muni{stroke:#fff;stroke-width:1.1;cursor:pointer;transition:filter .1s}.muni:hover{filter:brightness(1.15)}.muni.munihl{filter:brightness(1.15)}\n  .sect{stroke-width:1;fill-opacity:.32;stroke-opacity:.6}\n  .sect.plain{fill:#e8e8ec;stroke:#d4d4d9;fill-opacity:1;stroke-opacity:1}\n  .sect.closed-ctx{fill:#e2e2e7;stroke:#aeaeb2;stroke-dasharray:3 2;fill-opacity:1;stroke-opacity:1}\n  .sect.rejected{fill:#e4e4ea;fill-opacity:1;cursor:help}\n  .sect-rejected-border{fill:none;stroke:#9797a3;stroke-width:1.4;pointer-events:none}\n  .sect.unresolved{fill:#fbeceb;stroke:#e8a29c;stroke-dasharray:2 2;fill-opacity:1;stroke-opacity:1;cursor:help}\n  .lbl{pointer-events:none;filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.16))}\n  /* No plate behind the text: a halo painted UNDER the glyphs (paint-order) keeps a name legible on any\n     fill without stamping an opaque box over the territory it names. Ink and halo are chosen per label\n     from the luminance of the fill underneath, so they are deliberately NOT declared here — a\n     stylesheet rule outranks a presentation attribute and would silently override that choice. */\n  .lbl text{text-anchor:middle;paint-order:stroke fill;stroke-width:1.8px;stroke-linejoin:round;stroke-linecap:round;letter-spacing:-.1px;font-family:-apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif}\n  .lbl.big text{stroke-width:2.4px}\n  .lbl line.lead{stroke-opacity:.6;stroke-width:.8;stroke-linecap:round}\n  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px}\n  .legend b{display:inline-block;width:11px;height:11px;border-radius:4px;margin-right:5px;vertical-align:-1px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}\n  .kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}\n  .kpi{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:9px 11px}\n  .kpi .n{font-size:19px;font-weight:700}.kpi .l{font-size:10.5px;color:var(--mut);margin-top:2px}\n  .kpi.warn{border-color:#ffd1cc}.kpi.warn .n{color:var(--closing)}.kpi.ok{border-color:#bdeecb}.kpi.ok .n{color:var(--ok)}\n  .bar-row{display:flex;align-items:center;gap:8px;margin:6px 0}\n  .bar-row .tag{width:42px;font-weight:600;font-size:12px}\n  .bar-track{flex:1;height:18px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex;position:relative}\n  .bar-mark{position:absolute;top:0;bottom:0;width:2px;opacity:.9;z-index:2}\n  .bar-base{height:100%;opacity:.4}.bar-inc{height:100%}\n  .bar-val{width:132px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:5px 6px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  .home{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px}\n  .muni-item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:5px 8px;margin-bottom:6px}\n  .muni-item .nm{flex:1;font-weight:600;font-size:12px}.muni-item .ct{color:var(--mut);font-size:11px}\n  .seg{display:flex;background:var(--panel2);border:1px solid var(--line);border-radius:7px;overflow:hidden}\n  .seg button{border:0;border-right:1px solid var(--line);background:transparent;color:var(--mut);padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer}\n  .seg button:last-child{border-right:0}\n  .seg button.on{color:#fff}\n  .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n  /* Segmented control de verdade: trilho cinza (panel2) + o segmento ativo vira uma cápsula branca\n     flutuando por cima (shadow-ctrl), não só uma mudança de cor plana. */\n  .toggle{display:flex;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:2px;gap:1px;font-size:12px}\n  .toggle button{background:transparent;border:0;border-radius:6px;color:var(--mut);padding:5px 10px;cursor:pointer;font-weight:600;transition:background .12s,box-shadow .12s,color .12s}\n  .toggle button.on{background:#fff;color:var(--ink);box-shadow:var(--shadow-ctrl)}\n  .note{color:var(--mut);font-size:10.5px;margin-top:7px;line-height:1.5}\n  .cell{font-variant-numeric:tabular-nums}\n  .lider-foot td{background:var(--panel2);font-weight:600;font-size:11px;color:var(--ink);border-bottom:2px solid var(--line);padding:5px 6px;text-align:left}\n  .scroll{max-height:240px;overflow:auto}\n  /* Tables meant to be READ in full rather than sampled. Only the vertical cap is dropped — overflow\n     stays auto so the wide grupo grid can still scroll sideways inside its card, which is the one axis\n     that genuinely cannot fit. */\n  .scroll.grow{max-height:none}\n  .tip{position:fixed;pointer-events:none;background:#1c1c1e;color:#fff;border-radius:8px;padding:7px 9px;font-size:11px;opacity:0;transition:opacity .1s;z-index:9;max-width:230px;box-shadow:0 2px 8px rgba(0,0,0,.28)}\n  .spark{display:flex;align-items:flex-end;gap:3px;height:44px}\n  .spark .b{flex:1;background:var(--acc);border-radius:2px 2px 0 0;min-height:2px;opacity:.85}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}\n  .tabs button{background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--mut);padding:6px 12px;cursor:pointer;font-weight:600;box-shadow:var(--shadow-ctrl);transition:background .1s,box-shadow .1s}\n  .tabs button.on{background:#fff;color:var(--ink);border-color:var(--acc);box-shadow:0 1px 3px rgba(0,0,0,.2)}\n  .hidden{display:none}\n  .sqlbox{width:100%;height:60px;font-family:ui-monospace,Menlo,monospace;font-size:11px;resize:vertical}\n  .load{position:absolute;inset:0;background:rgba(245,245,247,.85);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:20;font-size:13px;color:var(--acc);font-weight:600;text-align:center;padding:20px}\n  .load.hidden{display:none}\n  .badge{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--panel2);border:1px solid var(--line);color:var(--mut)}\n  /* #optBadge é o único badge que costuma quebrar em várias linhas (resumo inteiro do otimizador) —\n     o padding vertical de 1px do .badge genérico (pensado pra um badge de uma linha só) deixa o\n     texto quase colado nas bordas de cima/baixo quando quebra; display:inline-block faz o padding\n     valer pra caixa inteira, não só a ponta da primeira/última linha de um span inline. */\n  #optBadge{display:inline-block;padding:5px 8px;line-height:1.5}\n  .sheet-bg{position:fixed;inset:0;background:rgba(28,28,30,.34);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:40;padding:24px}\n  .sheet-bg.hidden{display:none}\n  .sheet{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.22);width:min(580px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden}\n  .sheet-hd{display:flex;align-items:center;gap:9px;padding:12px 16px;border-bottom:1px solid var(--line)}\n  .sheet-hd h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-.1px}\n  .sheet-x{background:transparent;border:0;color:var(--mut);font-size:13px;cursor:pointer;padding:3px 7px;border-radius:6px}\n  .sheet-x:hover{background:var(--panel2);color:var(--ink)}\n  .sheet-bd{padding:14px 16px;overflow:auto}\n  .sheet-ft{display:flex;align-items:center;gap:8px;padding:11px 16px;border-top:1px solid var(--line);background:var(--panel2)}\n  .opt-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600}\n  .opt-sub{font-size:11.5px;color:var(--mut);margin:4px 0 10px;line-height:1.5}\n  .opt-row{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;cursor:pointer;background:var(--panel)}\n  .opt-row:hover{background:var(--panel2)}\n  .opt-row.on{border-color:var(--acc);background:#eaf3ff}\n  .opt-row:has(input:disabled){opacity:.5;cursor:not-allowed}\n  .opt-row input[type=radio]{margin:2px 0 0;accent-color:var(--acc);flex:0 0 auto}\n  .opt-t{font-size:12.5px;font-weight:600;display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n  .opt-d{font-size:11.5px;color:var(--mut);margin-top:3px;line-height:1.55}\n  .opt-inline{font-size:11.5px;color:var(--mut);margin-top:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}\n  .opt-inline.off{opacity:.4;pointer-events:none}\n  .opt-note{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:9px 11px;font-size:11.5px;color:var(--mut);line-height:1.6;margin-top:12px}\n  .opt-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:2px}\n  .opt-fld{display:flex;flex-direction:column;gap:5px;border:1px solid var(--line);border-radius:9px;padding:8px 10px;background:var(--panel2)}\n  .opt-fld span{font-size:11px;color:var(--mut);font-weight:600;line-height:1.35}\n  .opt-fld i{font-style:normal;font-weight:400;opacity:.72}\n  .opt-fld input{width:100%;background:var(--panel)}\n  .opt-err{margin-top:9px;font-size:11.5px;color:var(--closing);font-weight:600}\n  .opt-hr{height:1px;background:var(--line);margin:17px 0 0}\n  .shareurl{margin-top:9px;padding:9px 10px;border-radius:8px;background:var(--panel);border:1px solid var(--acc);\n    font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--ink);word-break:break-all;user-select:all}\n`,\n  render(d2){\n    return `<div class=\"simroot\"><header id=\"topHeader\" class=\"hidden\">\n  <h1>SEC | Natura Geomarketing</h1>\n  <span class=\"pill live\" id=\"liveState\" role=\"button\" tabindex=\"0\" title=\"Clique pra buscar o snapshot mais recente e recalcular a timeline inteira\">● iniciando</span>\n  <span class=\"status\" id=\"ds\"></span>\n  <div class=\"mac-spacer\"></div>\n  <select id=\"sessionPick\" style=\"min-width:200px\"></select>\n  <button class=\"ghost\" id=\"renameSession\" title=\"Renomear esta sessão\">✎</button>\n  <button class=\"ghost\" id=\"openOptions\" title=\"Configurações desta sessão\">⚙ Opções</button>\n  <button class=\"act\" id=\"saveSession\">Salvar sessão</button>\n  <button class=\"ghost\" id=\"dupSession\" title=\"Cria uma cópia independente desta sessão — a original fica intacta e a cópia passa a ser a sessão ativa\">⧉ Duplicar</button>\n  <button class=\"ghost\" id=\"openShare\" title=\"Publicar uma cópia congelada desta sessão em modo leitura\">⇪ Compartilhar</button>\n  <span class=\"status\" id=\"sessStatus\"></span>\n</header>\n\n<div class=\"rail hidden\" id=\"timelineRail\">\n  <div id=\"movementChips\"></div>\n  <div id=\"addMovementGroup\">\n    <button class=\"act\" id=\"openMove\">Adicionar movimentação</button>\n  </div>\n</div>\n\n<div class=\"card\" id=\"confirmCard\" style=\"margin:14px 20px\">\n  <h2 style=\"margin:0 0 9px\">Confirmar snapshot antes de simular</h2>\n  <div class=\"note\" id=\"confirmBody\">congelando snapshot…</div>\n  <button class=\"act\" id=\"confirmSnap\" style=\"margin-top:12px\">Confirmar snapshot ▶ ir pra simulação</button>\n  <div class=\"note\" style=\"margin-top:16px\">ou continue uma sessão salva:</div>\n  <select id=\"confirmSessionPick\" style=\"min-width:260px;margin-top:4px\"></select>\n  <div class=\"status\" id=\"confirmStatus\" style=\"margin-top:8px\"></div>\n</div>\n\n<div class=\"wrap hidden\" id=\"mainWrap\">\n  <div>\n    <div class=\"card\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:9px\">\n        <div class=\"toggle\" id=\"viewToggle\" title=\"ASIS = estrutura do setor antes desta etapa (já considerando etapas anteriores) · TOBE = após a redistribuição desta etapa\">\n          <button data-v=\"asis\">◧ ASIS (antes desta etapa)</button>\n          <button data-v=\"tobe\" class=\"on\">◨ TOBE (simulação)</button>\n        </div>\n      </div>\n      <div class=\"map-wrap\">\n        <svg id=\"map\" viewBox=\"0 0 700 500\" preserveAspectRatio=\"xMidYMid meet\"></svg>\n        <div class=\"osm-panel\" id=\"osmPanel\" title=\"Camadas exibidas no mapa, limitadas à área desta etapa\">\n          <div class=\"osm-panel-hd\">Camadas</div>\n          <div class=\"osm-group-hd\">Navegação</div>\n          <label class=\"osm-row\" title=\"Marca liga todos os níveis viários de uma vez; depois de carregado, esconda um nível por vez abaixo\"><input type=\"checkbox\" id=\"osmRodovias\"><span>Rodovias</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=motorway\"><input type=\"checkbox\" id=\"osmMotorway\"><span>Rodovia expressa</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=motorway_link\"><input type=\"checkbox\" id=\"osmMotorwayLink\"><span>Acesso de rodovia expressa</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=trunk\"><input type=\"checkbox\" id=\"osmTrunk\"><span>Via arterial</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=trunk_link\"><input type=\"checkbox\" id=\"osmTrunkLink\"><span>Acesso de via arterial</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=primary\"><input type=\"checkbox\" id=\"osmPrimary\"><span>Rodovia principal</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=primary_link\"><input type=\"checkbox\" id=\"osmPrimaryLink\"><span>Acesso de rodovia principal</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=secondary\"><input type=\"checkbox\" id=\"osmSecondary\"><span>Rodovia secundária</span></label>\n          <label class=\"osm-row osm-sub\" title=\"highway=secondary_link\"><input type=\"checkbox\" id=\"osmSecondaryLink\"><span>Acesso de rodovia secundária</span></label>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"osmRivers\"><span>Rios</span></label>\n          <div class=\"osm-group-hd sep\">Legendas</div>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"legendMuni\" checked><span id=\"legendMuniLbl\">Nome de municípios</span></label>\n          <label class=\"osm-row\"><input type=\"checkbox\" id=\"legendCtx\"><span>Código de setores adjacentes</span></label>\n          <div class=\"osm-status\" id=\"osmStatus\"></div>\n        </div>\n        <div class=\"zoom-pill\" id=\"mapZoom\">\n          <button type=\"button\" id=\"mapZoomOut\" title=\"Diminuir zoom\">−</button>\n          <span class=\"zoom-div\"></span>\n          <button type=\"button\" id=\"mapZoomIn\" title=\"Aumentar zoom\">+</button>\n        </div>\n      </div>\n      <div class=\"legend\" id=\"legend\"></div>\n      <div class=\"note\">Coloridos = municípios do setor fechado nesta etapa (cor = destino). Ao redor, cada área é um <b>setor vizinho inteiro</b> com o código no meio — não municípios soltos: cor = setor destino, cinza = contexto, cinza tracejado = setor já fechado numa etapa anterior. Shapes reais IBGE/tbrugz.</div>\n      <div class=\"note\" id=\"mapIgnoredNote\"></div>\n    </div>\n\n    <div class=\"card\">\n      <h2>Líderes — destino e CBs deslocadas (nesta etapa)</h2>\n      <div class=\"note\" style=\"margin:-4px 0 9px\"><b>Home</b> = setor que ficou com a maioria das CBs da líder. <b style=\"color:var(--closing)\">Deslocadas</b> = CBs dela que caíram noutro setor.</div>\n      <div class=\"scroll grow\"><table id=\"leaders\"></table></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"tabs\">\n        <button data-tab=\"matrix\" class=\"on\">Matriz ASIS</button>\n        <button data-tab=\"tobe\">Matriz TOBE</button>\n        <button data-tab=\"report\">Relatório de impacto (etapa)</button>\n        <button data-tab=\"cumulative\">Impacto acumulado (timeline)</button>\n        <button data-tab=\"sql\">Console SQL</button>\n      </div>\n      <div id=\"tab-matrix\"><div class=\"scroll grow\"><table id=\"matrix\"></table></div></div>\n      <div id=\"tab-tobe\" class=\"hidden\"><div class=\"scroll grow\"><table id=\"matrixTobe\"></table></div>\n        <div class=\"note\" id=\"tobeNote\"></div></div>\n      <div id=\"tab-report\" class=\"hidden\"></div>\n      <div id=\"tab-cumulative\" class=\"hidden\"></div>\n      <div id=\"tab-sql\" class=\"hidden\">\n        <textarea class=\"sqlbox\" id=\"sqlIn\">SELECT DS_MUNICIPIO, COUNT(DISTINCT CD_CONSULTORA) qtd FROM product.perfil_cb.crm_perfil_cb_diaria_historico WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR=3504 GROUP BY DS_MUNICIPIO ORDER BY qtd DESC</textarea>\n        <div class=\"toolbar\" style=\"margin:8px 0\"><button class=\"act\" id=\"runSql\">▶ Rodar</button><span class=\"status\" id=\"sqlStatus\"></span></div>\n        <div class=\"note\" style=\"margin:-4px 0 8px\">Roda contra o snapshot congelado desta sessão (não \"ao vivo\") — mesma coerência da timeline.</div>\n        <div class=\"scroll\"><table id=\"sqlOut\"></table></div>\n      </div>\n    </div>\n  </div>\n\n  <div>\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"margin-bottom:11px\">\n        <button class=\"ghost\" id=\"optim\" title=\"testa alocações que mantêm cada setor conectado e escolhe a que equilibra a carga entre setores + minimiza a quebra (evita empilhar tudo num setor)\">★ Modelo ÓTIMO (equilíbrio + quebra)</button>\n        <button class=\"ghost\" id=\"autoTobe\" title=\"preenche cada município pelo setor vizinho mais próximo (geografia)\">TOBE por adjacência</button>\n        <span class=\"badge\" id=\"optBadge\"></span>\n      </div>\n      <div class=\"note\" style=\"margin:-4px 0 10px\"><b style=\"color:var(--ink)\">★ ÓTIMO</b> = equilibra a carga entre os setores <b>e</b> minimiza a quebra (mantém o território conectado; não empilha tudo num setor). <b style=\"color:var(--ink)\">TOBE</b> = sugestão por vizinhança. Editar esta etapa recalcula automaticamente as etapas seguintes da timeline.</div>\n      <div class=\"note\" id=\"ruleNote\" style=\"margin:-6px 0 11px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px\"></div>\n      <div class=\"kpis\" id=\"kpis\"></div>\n      <div id=\"contig\"></div>\n      <div id=\"spanRule\"></div>\n      <h2>Span por setor (CBs) — base + entrantes</h2>\n      <div id=\"spans\"></div>\n      <div id=\"policySection\">\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin:14px 0 7px\">\n        <h2 style=\"margin:0\">CBs deslocadas</h2>\n        <div class=\"toggle\" id=\"policy\">\n          <button data-p=\"fora\" class=\"on\">= fora do setor</button>\n          <button data-p=\"quebra\">= quebra de relação</button>\n        </div>\n      </div>\n      <div class=\"note\" id=\"policyNote\"></div>\n      </div>\n    </div>\n\n    <div class=\"card\">\n      <h2 id=\"muniListTitle\">Municípios → setor destino</h2>\n      <div class=\"note\" id=\"muniListNote\" style=\"margin:-4px 0 9px\">Clique no <b>código do setor</b> pra mandar o município <b>inteiro</b> pra lá (regra de negócio). A cor acompanha o mapa.</div>\n      <div id=\"muniList\"></div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"toolbar\" style=\"justify-content:space-between\">\n        <h2 style=\"margin:0\">Plano de migração (etapa selecionada)</h2>\n        <div><button class=\"ghost\" id=\"copyPlan\">Copiar</button> <button class=\"ghost\" id=\"csvPlan\">CSV</button></div>\n      </div>\n      <div id=\"plan\" class=\"note\"></div>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"shareBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"shareTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"shareTitle\">Compartilhar sessão</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"shareX\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\" id=\"shareBody\"></div>\n    <div class=\"sheet-ft\">\n      <span class=\"status\" id=\"shareStatus\"></span>\n      <div class=\"mac-spacer\"></div>\n      <button class=\"ghost hidden\" id=\"shareCopy\">Copiar link</button>\n      <button class=\"ghost hidden\" id=\"shareOpen\">Abrir</button>\n      <button class=\"ghost\" id=\"shareCancel\">Cancelar</button>\n      <button class=\"act\" id=\"shareGo\">Compartilhar</button>\n    </div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"optionsBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"optTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"optTitle\">Opções da sessão</h3><span class=\"badge\" id=\"optSessName\"></span><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"optClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Pré-requisitos do destino — tamanho em CBs</div>\n      <div class=\"opt-sub\">Vale pro tamanho de cada unidade receptora <b>depois</b> da redistribuição (base + entrantes) — uma escala por nível de fechamento. Em branco = desligado.</div>\n      <div class=\"opt-t\" style=\"margin-top:2px\">Setor</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhum setor acima)</i></span><input type=\"number\" id=\"optMaxSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhum setor abaixo)</i></span><input type=\"number\" id=\"optMinSpan_setor\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-t\" style=\"margin-top:12px\">GV</div>\n      <div class=\"opt-grid\">\n        <label class=\"opt-fld\"><span>Span alvo <i>(tamanho ideal)</i></span><input type=\"number\" id=\"optTargetSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"livre\"></label>\n        <label class=\"opt-fld\"><span>Span máximo <i>(nenhuma GV acima)</i></span><input type=\"number\" id=\"optMaxSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n        <label class=\"opt-fld\"><span>Span mínimo <i>(nenhuma GV abaixo)</i></span><input type=\"number\" id=\"optMinSpan_gv\" min=\"1\" max=\"99999\" step=\"1\" placeholder=\"sem limite\"></label>\n      </div>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:14px\">Conectividade de região</div>\n      <div class=\"opt-sub\">Só se aplica a fechamento de Setor. Controla o que acontece quando um clique no mapa deixaria algum setor com o território partido em pedaços desconectados — o aviso <b style=\"color:var(--closing)\">⚠ Território desconectado</b> abaixo do mapa.</div>\n      <label class=\"opt-row\"><input type=\"radio\" name=\"forceContig\" value=\"0\"><div><div class=\"opt-t\">Livre</div><div class=\"opt-d\">Clicar num município avança pro próximo setor destino da lista, mesmo que isso deixe o território de algum setor desconectado. Resolva depois com ★ ÓTIMO ou reassociando manualmente no mapa.</div></div></label>\n      <label class=\"opt-row\"><input type=\"radio\" name=\"forceContig\" value=\"1\"><div><div class=\"opt-t\">Forçar conectividade de território <i style=\"font-weight:400;color:var(--mut)\">(padrão)</i></div><div class=\"opt-d\">Clicar num município nunca deixa um setor desconectado: pula o(s) destino(s) da lista que quebrariam a conectividade e escolhe o próximo que mantém todo setor conectado. Se nenhum destino for seguro, o clique não muda nada.</div></div></label>\n      <div class=\"opt-err hidden\" id=\"optSpanErr\"></div>\n      <div class=\"opt-note\" id=\"optPreview\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"optStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"optCancel\">Cancelar</button><button class=\"act\" id=\"optApply\">Aplicar</button></div>\n  </div>\n</div>\n\n<div class=\"sheet-bg hidden\" id=\"moveBg\">\n  <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"moveTitle\">\n    <div class=\"sheet-hd\"><h3 id=\"moveTitle\">Adicionar movimentação</h3><div class=\"mac-spacer\"></div><button class=\"sheet-x\" id=\"moveClose\" title=\"Fechar\">✕</button></div>\n    <div class=\"sheet-bd\">\n      <div class=\"opt-lbl\">Tipo de movimentação</div>\n      <div class=\"opt-sub\" id=\"moveLockNote\">Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.</div>\n      <label class=\"opt-row\" data-lvl=\"setor\"><input type=\"radio\" name=\"moveLevel\" value=\"setor\"><div><div class=\"opt-t\">Fechar Setor</div><div class=\"opt-d\">Redistribui os municípios do setor entre setores vizinhos da mesma GV.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"gv\"><input type=\"radio\" name=\"moveLevel\" value=\"gv\"><div><div class=\"opt-t\">Fechar GV</div><div class=\"opt-d\">Redistribui os setores (com municípios e CBs) da GV entre GVs vizinhas do mesmo mercado.</div></div></label>\n      <label class=\"opt-row\" data-lvl=\"mercado\"><input type=\"radio\" name=\"moveLevel\" value=\"mercado\"><div><div class=\"opt-t\">Fechar Mercado</div><div class=\"opt-d\">Redistribui as GVs (com setores, municípios e CBs) do mercado entre mercados vizinhos.</div></div></label>\n      <div class=\"opt-hr\"></div>\n      <div class=\"opt-lbl\" style=\"margin-top:15px\" id=\"moveCodeLbl\">Setor a fechar</div>\n      <div class=\"opt-inline\" id=\"moveCodeRow\">\n        <input id=\"moveCode\" list=\"moveCodeList\" placeholder=\"código\" style=\"min-width:150px\">\n        <datalist id=\"moveCodeList\"></datalist>\n        <button class=\"ghost\" id=\"moveRand\" title=\"Aleatório\">🎲</button>\n      </div>\n      <div class=\"opt-err hidden\" id=\"moveErr\"></div>\n    </div>\n    <div class=\"sheet-ft\"><span class=\"status\" id=\"moveStatus\"></span><div class=\"mac-spacer\"></div><button class=\"ghost\" id=\"moveCancel\">Cancelar</button><button class=\"act\" id=\"moveGo\">Fechar ▶ (nova etapa)</button></div>\n  </div>\n</div>\n\n<div class=\"tip\" id=\"tip\"></div>\n<div class=\"load hidden\" id=\"loader\">carregando…</div>\n<span id=\"ready\" style=\"display:none\"></span></div>`;\n  },\n  mounted(d2, root){\n\nconst $=s=>root.querySelector(s);\nconst fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\nconst money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\nconst norm=s=>(s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst esc=s=>(s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\nconst blankM=()=>({cbs:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0,ativas:0});\n// ---------- município identity ----------\n// Brazil has many municipalities sharing a name (Bom Jesus alone exists in PI, RN, RS, SC, PB, GO).\n// A stage loads the dominant UF PLUS every bordering UF, so those collisions land in the SAME\n// feature set routinely: keyed by name alone the last shape loaded won, the map could draw a\n// município from the wrong state, buildAdj merged two unrelated municipalities into one node, and\n// GROUP BY DS_MUNICIPIO summed two real municipalities into one row. The key is NAME/UF everywhere\n// — SQL rows, GeoJSON features, the dominantes base, assignments, persisted sessions. norm() maps\n// every non-alphanumeric to a space, so '/' can never occur inside a name and the split is exact.\n// A missing UF degrades to a bare NAME key rather than inventing one.\n// A handful of municípios carry two spellings in the wild — an older one still used internally (CRM\n// export, this dominantes CSV) and the current official IBGE spelling used by the geodata mirror.\n// Left alone, the two spellings key to different places: the shape draws with no dominance entry\n// (\"setor não encontrado\") even though the base has it under the other spelling. Canonicalized here,\n// once, to whichever spelling the CRM base uses — so display stays consistent with every other\n// município name in this app, which comes from the CRM, never from IBGE.\n// Aliases base (embutidos, sempre valem mesmo se a tabela abaixo estiver vazia ou fora do ar).\n// A tabela Data2 (MUNI_ALIAS_DS) deixa o time acrescentar/corrigir grafias sem precisar mexer\n// neste componente — carregada uma vez no boot (loadMuniAliases) e mesclada por cima destes.\nlet MUNI_ALIAS={BRASOPOLIS:'BRAZOPOLIS','DONA EUSEBIA':'DONA EUZEBIA'};\nconst MUNI_ALIAS_DS='gb9Mu-5_9AcCDK-myWwSyDataSet';\nasync function loadMuniAliases(){\n  const ds=(d2.data('muniAliasDataset')||d2.data(MUNI_ALIAS_DS)); if(!ds) return;\n  try{\n    await ds.refresh();\n    const recs=await ds.loadAll();\n    recs.forEach(r=>{ const de=norm(r.de), pa=norm(r.para); if(de&&pa) MUNI_ALIAS[de]=pa; });\n  }catch(e){ /* tabela de aliases é um complemento opcional — falha aqui não trava o boot */ }\n}\nconst muniN=s=>{const n=norm(s);return MUNI_ALIAS[n]||n;};\nconst mkey=(name,uf)=>{const n=muniN(name),u=String(uf==null?'':uf).trim().toUpperCase();return u?n+'/'+u:n;};\nconst mName=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?s:s.slice(0,i);};\nconst mUf=k=>{const s=String(k==null?'':k),i=s.lastIndexOf('/');return i<0?'':s.slice(i+1);};\nconst mDisp=k=>{const u=mUf(k);return u?mName(k)+' ('+u+')':mName(k);};\n// The base CSV is the ONLY source of a município's UF now, so its column is accepted in whatever\n// shape the base is maintained in: sigla, IBGE numeric code, or full state name. Anything that does\n// not resolve to a real sigla is treated as UNKNOWN rather than passed through — a bogus UF would key\n// a município to a shape that does not exist and drop it off the map without a word.\nconst UF_OF_CODE={}; Object.keys(UF_CODE).forEach(k=>{ UF_OF_CODE[UF_CODE[k]]=k; });\nconst UF_OF_NAME={ACRE:'AC',ALAGOAS:'AL',AMAPA:'AP',AMAZONAS:'AM',BAHIA:'BA',CEARA:'CE','DISTRITO FEDERAL':'DF','ESPIRITO SANTO':'ES',GOIAS:'GO',MARANHAO:'MA','MATO GROSSO':'MT','MATO GROSSO DO SUL':'MS','MINAS GERAIS':'MG',PARA:'PA',PARAIBA:'PB',PARANA:'PR',PERNAMBUCO:'PE',PIAUI:'PI','RIO DE JANEIRO':'RJ','RIO GRANDE DO NORTE':'RN','RIO GRANDE DO SUL':'RS',RONDONIA:'RO',RORAIMA:'RR','SANTA CATARINA':'SC','SAO PAULO':'SP',SERGIPE:'SE',TOCANTINS:'TO'};\nconst ufNorm=v=>{const s=norm(v); if(!s) return '';\n  if(UF_CODE[s]) return s;\n  if(UF_OF_CODE[s]) return UF_OF_CODE[s];\n  return UF_OF_NAME[s]||'';};\nconst loader=(on,t)=>{const l=$('#loader');l.classList.toggle('hidden',!on);if(t)l.textContent=t;};\nconst setError=(msg)=>{ const t=$('#ds'); if(t){t.textContent=msg;t.classList.add('err');} const c=$('#confirmStatus'); if(c){c.textContent=msg;c.classList.add('err');} };\n\n// ---------- base de municípios dominantes (CSV) ----------\n// WHICH municipalities belong to a sector is a business fact, not something to infer from CB volume:\n// it comes from a maintained CSV (one row per setor x município) linked through the `dominantesCsvUrl`\n// param. Anything the closing sector has CBs in but that the base does NOT assign to it is spillover:\n// not realocated, not drawn on the map, and its CBs follow their leader. Loaded once per URL and\n// cached for the whole session; a failure is thrown, never swallowed — a silent fallback here would\n// quietly change every number on the screen.\nconst DOM={url:null,promise:null,bySetor:null,ufBySetorMuni:null,rows:0,setores:0,hasUf:false,ufCol:null,badUf:0,clashes:0,headers:null,error:null};\nfunction parseCsv(text){\n  const out=[]; let row=[],cur='',q=false;\n  const push=()=>{ row.push(cur); cur=''; };\n  const endRow=()=>{ push(); if(row.length>1||row[0]!=='') out.push(row); row=[]; };\n  for(let i=0;i<text.length;i++){ const ch=text[i];\n    if(q){ if(ch==='\"'){ if(text[i+1]==='\"'){ cur+='\"'; i++; } else q=false; } else cur+=ch; }\n    else if(ch==='\"') q=true;\n    else if(ch===',') push();\n    else if(ch==='\\n') endRow();\n    else if(ch!=='\\r') cur+=ch;\n  }\n  if(cur!==''||row.length) endRow();\n  return out;\n}\n// `dominantesCsvUrl` is declared as a FileComponentUrlReference, so the instance points at the Drive\n// FILE, not at a URL literal — the Drive is policy-gated behind the app's auth strategy and a baked\n// string would be both stale and unauthorised. What the resolver hands back varies by where it is\n// resolved (URL string / file record / bare reference), so accept every shape instead of silently\n// reading undefined and falling through to the fallback.\n// The Drive is gated by the app's AuthenticationStrategy, so this read has to travel as the LOGGED-IN\n// USER — an anonymous CDN GET is exactly what the policy is there to refuse. `<system>.data2.media` is\n// only a cache in front of the SSP: on a cache miss it 302s to `<system>.data2apis.com`, which is where\n// the policy is actually evaluated.\n// COOKIES CANNOT CARRY THAT SESSION, which is why the previous `credentials:'include'` attempt could\n// never have worked: the app runs on <app>.<system>.data2.app and the SSP on <system>.data2apis.com —\n// cross-site — and the SSP answers `Access-Control-Allow-Origin: *` with no `Allow-Credentials`, so the\n// browser rejects a credentialed CORS read before it ever leaves the page. What the SSP actually reads\n// is `Authorization: Bearer <token>`: the runtime keeps that token in localStorage under\n// `__data2-session-<systemDomain>` (`__data2-session-data2` inside the builder preview) and replays it\n// on every dataset and rush call — same token, same header, same session, so the Drive policy sees the\n// logged-in user. Preflight passes because the SSP echoes whatever header the request asks for.\nfunction d2SessionToken(sysDomain){\n  const read=k=>{ try{ return (k&&window.localStorage.getItem(k))||''; }catch(e){ return ''; } };\n  return read(sysDomain?('__data2-session-'+sysDomain):'')||read('__data2-session-data2')||'';\n}\nfunction domCsvSystemDomain(url){\n  const m=/^https?:\\/\\/([^.\\/]+)\\.data2\\.(?:media|app)\\//i.exec(url)||/^https?:\\/\\/([^.\\/]+)\\.data2apis\\.com\\//i.exec(url);\n  return m?m[1]:'';\n}\nfunction domCsvCandidates(url){\n  const out=[];\n  const isMedia=/^https?:\\/\\/[^.\\/]+\\.data2\\.media\\//i.test(url);\n  const api=isMedia?url.replace(/^(https?:\\/\\/[^.\\/]+)\\.data2\\.media\\//i,'$1.data2apis.com/'):null;\n  const tok=d2SessionToken(domCsvSystemDomain(url));\n  const auth=tok?{authorization:'Bearer '+tok}:null;\n  // Authenticated origin read first — the only candidate a GATED Drive can actually answer.\n  if(api&&auth) out.push({url:api,headers:auth,label:'api+sessão'});\n  if(!api&&auth) out.push({url:url,headers:auth,label:'origem+sessão'});\n  // Anonymous last: only ever succeeds on an UNGATED Drive — or on a stale CDN cache hit, which is NOT\n  // proof the file is public (the origin keeps answering 401 once the edge entry expires).\n  if(api) out.push({url:api,headers:null,label:'api anônimo'});\n  out.push({url:url,headers:null,label:'origem anônima'});\n  return out;\n}\nfunction domCsvUrl(){\n  const pick=v=>{\n    if(!v) return '';\n    if(typeof v==='string') return v;\n    const q=v.params||{};\n    const ref=q.reference||v.reference;\n    return q.fileUrl||q.url||v.fileUrl||v.url||(ref?DOM_MEDIA_BASE+ref:'');\n  };\n  return String(pick(d2.param('dominantesCsvUrl'))||DOM_CSV_FALLBACK||'').trim();\n}\nasync function ensureDominantes(){\n  const url=domCsvUrl();\n  if(!url) throw new Error('base de municípios dominantes não configurada — ligue o arquivo CSV no parâmetro dominantesCsvUrl da instância do componente');\n  if(DOM.url===url && DOM.bySetor) return DOM.bySetor;\n  if(DOM.url===url && DOM.promise) return DOM.promise;\n  DOM.url=url; DOM.error=null;\n  DOM.promise=(async()=>{\n    // Self-diagnosing on purpose: every candidate reports its own outcome and the message names the\n    // session it was carrying. \"Unauthorized\" alone cannot distinguish \"not logged into THIS strategy\"\n    // from \"cookie not sent cross-site\" from \"param never bound, so it tried the fallback URL\" — and\n    // guessing between those from the outside costs a round trip each time.\n    let r=null; const tried=[];\n    for(const c of domCsvCandidates(url)){\n      let res=null;\n      try{ res=await fetch(c.url,{cache:'no-store',credentials:'omit',headers:c.headers||undefined}); }\n      catch(e){ tried.push(c.label+' → rede/CORS'); continue; }\n      if(res.ok){ r=res; break; }\n      tried.push(c.label+' → '+res.status);\n    }\n    if(!r){\n      let sess='sessão desconhecida';\n      try{\n        const a=d2.auth(AUTH_REF);\n        sess=(a&&a.isAuthed)?('logado como '+((a.user&&(a.user.email||a.user.nome))||'?')):('NÃO logado na estratégia '+AUTH_REF);\n      }catch(e){ sess='estratégia '+AUTH_REF+' não resolvida'; }\n      const tk=d2SessionToken(domCsvSystemDomain(url));\n      throw new Error('base de municípios dominantes: não foi possível ler o arquivo — '+sess+' · token de sessão '+(tk?'presente':'AUSENTE no localStorage')+'. Tentativas: '+tried.join(' · '));\n    }\n    const table=parseCsv(await r.text());\n    if(table.length<2) throw new Error('base de municípios dominantes vazia');\n    const head=table[0].map(h=>norm(h));\n    const iS=head.indexOf('CD SETOR'), iM=head.indexOf('MUNICIPIO');\n    if(iS<0||iM<0) throw new Error('base de municípios dominantes sem as colunas CD_SETOR / MUNICIPIO');\n    // CD_GV is the gerência de vendas the setor answers to, and it is the authority for the hard rule\n    // that a closing sector only ever hands territory to sectors of its OWN gerência. Its absence is\n    // fatal rather than a warning: with no GV there is no way to PROVE two sectors share one, and\n    // falling through unchecked is exactly the leak this column exists to close.\n    const iG=['CD GV','CDGV','GV','CD GERENCIA','CD GERENCIA VENDAS','CD GERENCIA DE VENDAS','GERENCIA DE VENDAS','GERENCIA'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iG==null) throw new Error('base de municípios dominantes sem a coluna CD_GV — a regra de gerência de vendas não pode ser aplicada. Colunas lidas: '+head.slice(0,40).join(', '));\n    // MERCADO fica ACIMA de GV na hierarquia (mercado > GV > setor) — fechamento de Mercado e a regra\n    // \"GV só fecha pra GV do mesmo mercado\" dependem dela do mesmo jeito que a regra de GV depende\n    // de CD_GV, então a ausência é fatal pelo mesmo motivo.\n    const iK=['MERCADO','MERCADO REGIONAL','MKT','MARKET'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    if(iK==null) throw new Error('base de municípios dominantes sem a coluna MERCADO — fechamento de GV/Mercado precisa dela. Colunas lidas: '+head.slice(0,40).join(', '));\n    // Nomes de exibição — opcionais: a hierarquia funciona só com os códigos, mas o código sozinho\n    // não diz nada pro operador escolhendo o que fechar.\n    const iDS=['DC SETOR','NOME SETOR','DESCRICAO SETOR','NM SETOR'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const iDG=['DC GV','NOME GV','DESCRICAO GV','NM GV','GV NOME'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    // The UF column is what the whole keying now rests on — SQL no longer carries DS_ESTADO — so its\n    // absence is a first-class finding, not a footnote. The header actually matched is reported on the\n    // base pill, and when nothing matches the pill lists every column read, because guessing a header\n    // name from the outside costs a round trip each time.\n    const iU=['UF','SIGLA UF','SG UF','CD UF','ESTADO','DS ESTADO','SIGLA','ESTADO SIGLA','UF MUNICIPIO'].map(h=>head.indexOf(h)).find(i=>i>=0);\n    const by={},ufBy={},gvBy={},mkBySetor={},setorLabel={},gvLabel={}; let n=0,bad=0; const clash={},gvClash={},mkClash={};\n    for(let i=1;i<table.length;i++){\n      const line=table[i]; if(!line) continue;\n      const s=String(line[iS]==null?'':line[iS]).trim(), m=muniN(line[iM]);\n      if(!s||!m) continue;\n      // One setor spans many rows (one per município) and every one of them repeats its CD_GV. Two\n      // DIFFERENT values for the same setor means the base itself disagrees about which gerência owns\n      // it — counted and reported, never keyed to whichever row happened to be read last.\n      const gvv=String(line[iG]==null?'':line[iG]).trim();\n      if(gvv){ if(gvBy[s]!==undefined && gvBy[s]!==gvv) gvClash[s]=true; else gvBy[s]=gvv; }\n      const mkv=String(line[iK]==null?'':line[iK]).trim();\n      if(mkv){ if(mkBySetor[s]!==undefined && mkBySetor[s]!==mkv) mkClash[s]=true; else mkBySetor[s]=mkv; }\n      if(iDS!=null){ const lbl=String(line[iDS]==null?'':line[iDS]).trim(); if(lbl && !setorLabel[s]) setorLabel[s]=lbl; }\n      if(iDG!=null && gvv){ const lbl=String(line[iDG]==null?'':line[iDG]).trim(); if(lbl && !gvLabel[gvv]) gvLabel[gvv]=lbl; }\n      const raw=(iU==null)?'':line[iU];\n      const u=(iU==null)?'':ufNorm(raw);\n      if(iU!=null && String(raw==null?'':raw).trim() && !u) bad++;\n      // The same name twice in ONE setor under two UFs is the single case the reverted SQL cannot\n      // survive: mSQL groups by DS_MUNICIPIO alone, so both municipalities arrive summed into one row\n      // and no lookup can split them again. Counted and reported — never quietly keyed to whichever\n      // UF happened to be read last.\n      const kk=s+'|'+m;\n      // An UNRESOLVED uf must never outrank a resolved one. Reading a blank/bogus UF row first wrote ''\n      // here, and the next row carrying the real sigla was then rejected as a clash — so the key stayed\n      // falsy for good, ufFor() fell through, and the município degraded to a bare NAME key that matches\n      // no shape and no dominance entry. With DISTRITO/ZONA in the base a município spans several rows,\n      // so which row lands first is pure luck. A clash is only two RESOLVED siglas that disagree.\n      if(!u){ if(ufBy[kk]===undefined) ufBy[kk]=''; }\n      else if(!ufBy[kk]) ufBy[kk]=u;\n      else if(ufBy[kk]!==u) clash[kk]=true;\n      (by[s]=by[s]||new Set()).add(u?m+'/'+u:m); n++;\n    }\n    if(!n) throw new Error('base de municípios dominantes sem linhas válidas');\n    DOM.bySetor=by; DOM.ufBySetorMuni=ufBy; DOM.rows=n; DOM.setores=Object.keys(by).length;\n    // Reverse município→setor index (static, built once from the same CSV) — lets the GV-closing\n    // map gray out a geographically-touching GV with no live query, matching the \"GV/mercado\n    // hierarchy is organizational, read from the CSV only\" rule the rest of this level already follows.\n    { const revBy={},revAll={}; Object.entries(by).forEach(([s,ms])=>ms.forEach(m=>{ if(!revBy[m]) revBy[m]=s; (revAll[m]=revAll[m]||new Set()).add(s); })); DOM.setorByMuni=revBy; DOM.setoresByMuni=revAll; }\n    DOM.gvBySetor=gvBy; DOM.gvClashes=Object.keys(gvClash).length; DOM.gvCol=String(table[0][iG]||'').trim();\n    DOM.gvSetores=Object.keys(gvBy).length;\n    DOM.hasUf=(iU!=null); DOM.ufCol=(iU==null?null:String(table[0][iU]||'').trim());\n    DOM.badUf=bad; DOM.clashes=Object.keys(clash).length; DOM.headers=head.slice(0,40); DOM.error=null;\n    // ---- hierarquia: setor ⊂ GV ⊂ mercado, tudo derivado do MESMO csv que a regra de setor já\n    // confia — fechamento de GV/Mercado lê a mesma fonte, nunca uma segunda base paralela.\n    DOM.mercadoBySetor=mkBySetor; DOM.mercadoClashes=Object.keys(mkClash).length; DOM.mercadoCol=String(table[0][iK]||'').trim();\n    DOM.setorLabel=setorLabel; DOM.gvLabel=gvLabel;\n    const sectorsByGv={},sectorsByMercado={},gvsByMercado={},mercadoByGv={},gvMercClash={};\n    Object.keys(by).forEach(s=>{\n      const gv=gvBy[s], mk=mkBySetor[s];\n      if(gv) (sectorsByGv[gv]=sectorsByGv[gv]||new Set()).add(s);\n      if(mk) (sectorsByMercado[mk]=sectorsByMercado[mk]||new Set()).add(s);\n      if(gv&&mk){ if(mercadoByGv[gv]!==undefined && mercadoByGv[gv]!==mk) gvMercClash[gv]=true; else mercadoByGv[gv]=mk; }\n    });\n    Object.entries(mercadoByGv).forEach(([gv,mk])=>(gvsByMercado[mk]=gvsByMercado[mk]||new Set()).add(gv));\n    DOM.sectorsByGv=sectorsByGv; DOM.sectorsByMercado=sectorsByMercado; DOM.gvsByMercado=gvsByMercado;\n    DOM.mercadoByGv=mercadoByGv; DOM.gvMercadoClashes=Object.keys(gvMercClash).length;\n    return by;\n  })();\n  DOM.promise.catch(e=>{ DOM.promise=null; DOM.bySetor=null; DOM.error=e.message; renderAll(); });\n  return DOM.promise;\n}\n\nlet SESS={ snapshotRaw:null, snapshotSyncedAt:null, snapLiteral:null, eloMaxCycle:null, timeline:[], selected:0,\n           sessionId:null, sessionLabel:null, sessionsList:[], sectorsList:[], sectorBaseCache:{}, stage:'loading', dirtyFrom:null,\n           snapBusy:false, settings:{...DEFAULT_SETTINGS} };\n\nasync function q(sql){\n  let dbx,res;\n  try{\n    res=await d2.rush('AwW_11Nkg-dKksP7ic8V8').invoke({sql});\n  }catch(e){\n    throw new Error('Databricks indisponível (rush AwW_11Nkg-dKksP7ic8V8): '+e.message);\n  }\n  // The listener answers 200 with {sql} and NO `result` when its upstream probe times out: the\n  // handler builds `result: $result.data`, so an empty upstream simply drops the key rather than\n  // failing. Reading straight through to res.data.result.data reported \"Cannot read properties of\n  // undefined (reading 'data')\" — which reads like a bug in THIS component and sent a debugging\n  // session after the wrong thing. Name the layer that actually failed instead.\n  const payload=res&&res.data;\n  if(!payload) throw new Error('rush AwW_11Nkg-dKksP7ic8V8 não devolveu corpo — listener fora do ar?');\n  if(!payload.result) throw new Error('a sonda Databricks não respondeu: o listener AwW_11Nkg-dKksP7ic8V8 devolveu 200 sem `result`, o que normalmente é timeout do upstream (natura-x-app). O SQL não chegou a rodar.');\n  dbx=payload.result.data;\n  if(!dbx) throw new Error('sonda Databricks respondeu sem dados (result.data vazio).');\n  if(dbx.state && dbx.state!=='SUCCEEDED') throw new Error('Databricks '+dbx.state+': '+(dbx.error?.message||JSON.stringify(dbx.error)||'erro desconhecido'));\n  return dbx;\n}\nfunction rows(resp){const c=(resp.columns||[]).map(x=>x.name);\n  return (resp.rows||[]).map(a=>Object.fromEntries(c.map((k,i)=>[k,a[i]])));}\n// Stamps the UF onto every feature at load: the fetch is per-UF, so this is the ONLY point where a\n// shape's state is known for certain. Downstream the features are flattened into one array and the\n// origin would be unrecoverable. Persisted with the feature in saved sessions.\nasync function geoUF(uf){const code=UF_CODE[uf]||uf;const r=await fetch('https://raw.githubusercontent.com/tbrugz/geodata-br/master/geojson/geojs-'+code+'-mun.json');if(!r.ok)throw new Error('geo '+uf);\n  const g=await r.json(); const U=String(uf).toUpperCase();\n  (g.features||[]).forEach(f=>{ f.__uf=U; });\n  return g;}\n\nfunction buildAdj(features){\n  // Encaixe exato de vértice (grid ~111m) resolve vizinhança DENTRO de uma UF, porque todos os\n  // municípios de uma UF saem do MESMO arquivo geoJSON, com a mesma malha de pontos. Atravessando\n  // fronteira de UF, os dois lados vêm de arquivos buscados/digitalizados SEPARADAMENTE — a mesma\n  // linha real de fronteira quase nunca cai nos mesmos vértices nos dois arquivos, e alargar o grid\n  // pra compensar (tentativa anterior) só troca \"não encontra vizinho de UF diferente\" por \"pode\n  // grudar município que só está perto, dentro da própria UF\" — não é uma correção geométrica de\n  // verdade, só empurra o problema. A solução de verdade é uma segunda checagem, geométrica (menor\n  // distância real entre os dois contornos, em km), aplicada SÓ a pares de UF diferente que o\n  // encaixe exato não achou — dentro da mesma UF o encaixe exato continua sendo a única fonte,\n  // sem alteração de comportamento nem de peso de voto.\n  const pts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],s=new Set();\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>s.add(Math.round(p[0]*1000)/1000+','+Math.round(p[1]*1000)/1000))));return s;};\n  const rawPts=f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates],out=[];\n    ps.forEach(poly=>poly.forEach(ring=>ring.forEach(p=>out.push(p))));return out;};\n  const feat={},idx={};\n  features.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome,f.__uf);\n    const raw=rawPts(f);\n    let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n    raw.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; });\n    feat[nm]={f,pts:pts(f),raw,bbox:{mnx,mny,mxx,mxy},uf:f.__uf};\n  });\n  for(const nm in feat) feat[nm].pts.forEach(p=>(idx[p]=idx[p]||[]).push(nm));\n  // Limiar de \"encosta de verdade\" cruzando UF: cobre a folga típica entre dois arquivos\n  // digitalizados separadamente pra MESMA linha real de fronteira, sem juntar município que só\n  // está na vizinhança geral mas não faz fronteira de fato.\n  const TOUCH_KM=1.2;\n  const nbrs=nm=>{\n    const A=feat[nm]; if(!A) return [];\n    const c={};\n    A.pts.forEach(p=>idx[p].forEach(o=>{if(o!==nm)c[o]=(c[o]||0)+1;}));\n    const exact=Object.entries(c).filter(([,n])=>n>=2).map(([o,n])=>({o,w:n}));\n    const seen=new Set(exact.map(e=>e.o));\n    const latK=Math.max(0.2,Math.cos(((A.bbox.mny+A.bbox.mxy)/2)*Math.PI/180));\n    const padLon=TOUCH_KM/111/latK, padLat=TOUCH_KM/111;\n    const extra=[];\n    for(const o in feat){\n      if(o===nm||seen.has(o)) continue;\n      const B=feat[o];\n      if(B.uf===A.uf) continue; // mesma UF: se o encaixe exato não achou, não está encostando\n      if(B.bbox.mxx<A.bbox.mnx-padLon||B.bbox.mnx>A.bbox.mxx+padLon||B.bbox.mxy<A.bbox.mny-padLat||B.bbox.mny>A.bbox.mxy+padLat) continue;\n      let best=Infinity;\n      outer: for(const p of A.raw){\n        for(const q of B.raw){\n          const d=Math.hypot((p[0]-q[0])*latK*111,(p[1]-q[1])*111);\n          if(d<best){ best=d; if(best<0.05) break outer; }\n        }\n      }\n      if(best<=TOUCH_KM) extra.push({o,w:1});\n    }\n    return exact.concat(extra);\n  };\n  return {feat,nbrs};\n}\nfunction centroidLL(f){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  ps.forEach(poly=>poly[0].forEach(p=>{x+=p[0];y+=p[1];n++;}));return[x/n,y/n];}\n\n// ---------- snapshot pinning ----------\nasync function resolveSnapshotPin(){\n  const [rawR,liveCountR,eloR]=await Promise.all([\n    q(`SELECT MAX(DT_HISTORICO) snap FROM ${T} WHERE CD_PAIS=1`).then(rows),\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`).then(rows),\n    q(`SELECT MAX(operational_cycle) mx FROM ${ELO} WHERE country_code=1`).then(rows).catch(()=>[]),\n  ]);\n  const raw=rawR[0]&&rawR[0].snap;\n  const liveCount=+((liveCountR[0]||{}).n||0);\n  const eloMax=+((eloR[0]||{}).mx||0);\n  const LIVE_SUBQ=`(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1)`;\n  const dbTs=(raw||'').replace('T',' ').replace(/Z$/,'');\n  const candidates=[`TIMESTAMP'${dbTs}'`,`'${dbTs}'`,`DATE'${dbTs}'`];\n  const tests=await Promise.all(candidates.map(c=>\n    q(`SELECT COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND DT_HISTORICO=${c}`).then(rows).then(r=>+((r[0]||{}).n||0)).catch(()=>-1)\n  ));\n  let chosen=LIVE_SUBQ;\n  for(let i=0;i<candidates.length;i++){ if(liveCount>0 && tests[i]===liveCount){ chosen=candidates[i]; break; } }\n  return {snapshotRaw:raw, snapLiteral:chosen, eloMaxCycle:eloMax, syncedAt:new Date().toISOString()};\n}\n// DISPLAY CONTRACT: the UI never prints the snapshot's own DT_HISTORICO (snapshotRaw). What the user\n// sees is `snapshotSyncedAt` — the date AND hour at which THIS instance last pulled/pinned the\n// snapshot it is querying. snapshotRaw is still resolved, still drives every SQL literal, and is\n// still persisted by saveSession/doShare; it is only kept off the screen.\nfunction fmtSyncedAt(iso){\n  if(!iso) return '—';\n  const d=new Date(iso);\n  if(isNaN(d.getTime())) return String(iso);\n  return d.toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});\n}\nasync function ensurePinned(){\n  if(SESS.snapLiteral) return;\n  const p=await resolveSnapshotPin();\n  SESS.snapshotRaw=p.snapshotRaw; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n  SESS.snapshotSyncedAt=p.syncedAt;\n}\n\n// ---------- world-state fold ----------\nfunction accMetrics(base,add){\n  const out=base?{...base}:blankM();\n  Object.keys(blankM()).forEach(k=>{ out[k]=(out[k]||0)+((add&&add[k])||0); });\n  return out;\n}\nfunction foldWorldState(timeline,uptoIdx){\n  const closedSectors=new Set(),muniOverride={},muniGeo={},muniRow={},muniLevel={},sectorInflow={},sectorInflowLevel={},sectorOrphanRows={};\n  // GV/mercado: setores e GVs não se dissolvem quando o dono deles fecha — eles só passam a\n  // responder a um destino novo. Por isso o rastro do cascade mora numa camada PRÓPRIA\n  // (secOverride/gvOverride), separada da camada de município do fechamento de setor — a\n  // hierarquia trava a sessão num nível só, então nunca as duas camadas têm conteúdo ao mesmo tempo.\n  const closedGvs=new Set(),secOverride={},gvInflow={};\n  const closedMercados=new Set(),gvOverride={},mercadoInflow={};\n  for(let k=0;k<uptoIdx;k++){\n    const mv=timeline[k]; if(!mv||!mv.sector||!mv.munis) continue;\n    const level=mv.level||'setor';\n    if(level==='gv'){\n      closedGvs.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        secOverride[u]=String(dest);\n        gvInflow[dest]=accMetrics(gvInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    if(level==='mercado'){\n      closedMercados.add(String(mv.sector));\n      Object.keys(mv.munis).forEach(u=>{\n        if(u==='__OUTROS__') return;\n        const dest=mv.assign[u]; if(!dest) return;\n        gvOverride[u]=String(dest);\n        mercadoInflow[dest]=accMetrics(mercadoInflow[dest],mv.munis[u]);\n      });\n      continue;\n    }\n    closedSectors.add(String(mv.sector));\n    Object.keys(mv.munis).forEach(m=>{\n      if(m==='__OUTROS__') return;\n      const dest=mv.assign[m]; if(!dest) return;\n      muniOverride[m]=String(dest);\n      if(mv.geoFeat&&mv.geoFeat[m]) muniGeo[m]=mv.geoFeat[m];\n      const rws=(mv.matrix||[]).filter(r=>mkey(r.municipio,r.uf)===m);\n      muniRow[m]=(muniRow[m]||[]).concat(rws);\n      if(mv.levelByMuni&&mv.levelByMuni[m]) muniLevel[m]={...(muniLevel[m]||{}),...mv.levelByMuni[m]};\n      sectorInflow[dest]=accMetrics(sectorInflow[dest],mv.munis[m]);\n      const lv=(mv.levelByMuni&&mv.levelByMuni[m])||{};\n      sectorInflowLevel[dest]=sectorInflowLevel[dest]||{};\n      Object.entries(lv).forEach(([nv,c])=>{ sectorInflowLevel[dest][nv]=(sectorInflowLevel[dest][nv]||0)+c; });\n    });\n    // Orphan CBs follow their leader's home sector, so later etapas must inherit them the same way\n    // the allocated municipalities are inherited — otherwise a stage's span would not match the\n    // base carried into the next one.\n    if(mv.orphanMetricsByGroup && Object.keys(mv.orphanMetricsByGroup).length){\n      const d=computeDerived(mv);\n      d.leaders.forEach(L=>{\n        if(!L.home) return;\n        const om=mv.orphanMetricsByGroup[L.grupo];\n        if(om) sectorInflow[L.home]=accMetrics(sectorInflow[L.home],om);\n        // sectorInflow only reaches a sector while it is a DESTINATION (it lands in baseMetrics).\n        // Keep the rows as well, so the sector still carries these CBs on the day it is the one\n        // being closed — that read goes through mv.matrix, which baseMetrics never touches.\n        const orws=mv.orphanRowsByGroup&&mv.orphanRowsByGroup[L.grupo];\n        if(orws&&orws.length) sectorOrphanRows[L.home]=(sectorOrphanRows[L.home]||[]).concat(orws);\n        const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n        if(ol){ sectorInflowLevel[L.home]=sectorInflowLevel[L.home]||{};\n          Object.entries(ol).forEach(([nv,c])=>{ sectorInflowLevel[L.home][nv]=(sectorInflowLevel[L.home][nv]||0)+c; }); }\n      });\n    }\n  }\n  return {closedSectors,muniOverride,muniGeo,muniRow,muniLevel,sectorInflow,sectorInflowLevel,sectorOrphanRows,\n          closedGvs,secOverride,gvInflow,closedMercados,gvOverride,mercadoInflow};\n}\n\n// ---------- per-movement compute ----------\nfunction aggregateMovement(mv){\n  const level=mv.level||'setor';\n  const munis={},leaders={};\n  const exSet=new Set((mv.excludedMunis||[]).map(x=>x.muni));\n  const acc=(o,r)=>{o.cbs+=+r.cbs;o.receita+=+r.receita||0;o.rec_nat+=+r.rec_nat||0;o.rec_avon+=+r.rec_avon||0;o.pontos+=+r.pontos||0;o.pedidos+=+r.pedidos||0;o.ativas+=+r.ativas||0;};\n  // Orphans = CBs in municipalities outside the sector's dominance base. They are NOT allocated\n  // geographically; they follow their own leader's home sector. Kept per leader group so every\n  // consumer (span, cascade, impact report) can route them the same way. Só existe no nível setor —\n  // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base).\n  mv.orphanByGroup={}; mv.orphanMetricsByGroup={}; mv.orphanRowsByGroup={};\n  // unidade que este nível redistribui: a própria município (setor fecha), o setor da linha (GV\n  // fecha), a GV do setor da linha (mercado fecha).\n  const unitOf=r=>level==='setor'?null:(level==='gv'?String(r.setor):(DOM.gvBySetor[String(r.setor)]||''));\n  mv.matrix.forEach(r=>{\n    const m=mkey(r.municipio,r.uf);\n    r._out=!mv.geoFeat[m];   // assign, never just set: absorbed rows are reused across etapas\n    const mm=level==='setor'?(r._out?'__OUTROS__':m):(unitOf(r)||'__OUTROS__');\n    munis[mm]=munis[mm]||{...blankM(),name:mm}; acc(munis[mm],r);\n    const g=String(r.grupo); leaders[g]=leaders[g]||{grupo:g,lider:r.lider,total:0,byMuni:{}};\n    leaders[g].total+=+r.cbs; leaders[g].byMuni[mm]=(leaders[g].byMuni[mm]||0)+ +r.cbs;\n    if(level==='setor' && r._out && exSet.has(m)){\n      mv.orphanByGroup[g]=(mv.orphanByGroup[g]||0)+ +r.cbs;\n      // The ROW too, not just the totals: when the leader's home sector is itself closed in a later\n      // etapa, that etapa rebuilds its matrix from SQL + absorbed municípios, and these CBs are in\n      // neither (their município never moved). Without the row they silently vanish from the sector\n      // they were just credited to — see sectorOrphanRows in foldWorldState.\n      (mv.orphanRowsByGroup[g]=mv.orphanRowsByGroup[g]||[]).push(r);\n      mv.orphanMetricsByGroup[g]=accMetrics(mv.orphanMetricsByGroup[g],\n        {cbs:+r.cbs,receita:+r.receita||0,rec_nat:+r.rec_nat||0,rec_avon:+r.rec_avon||0,pontos:+r.pontos||0,pedidos:+r.pedidos||0,ativas:+r.ativas||0});\n    }\n  });\n  // Level counts only exist per municipality, never per leader, so an excluded municipality's level\n  // mix is credited to the group holding most of its CBs (sub-threshold by definition, so usually one).\n  mv.orphanLevelByGroup={};\n  (mv.excludedMunis||[]).forEach(({muni})=>{\n    const lv=(mv.levelByMuni&&mv.levelByMuni[muni])||{}; if(!Object.keys(lv).length) return;\n    let bg=null,bc=-1;\n    mv.matrix.forEach(r=>{ if(mkey(r.municipio,r.uf)!==muni) return; const cc=+r.cbs; if(cc>bc){bc=cc;bg=String(r.grupo);} });\n    if(!bg) return;\n    mv.orphanLevelByGroup[bg]=mv.orphanLevelByGroup[bg]||{};\n    Object.entries(lv).forEach(([nv,cc])=>{ mv.orphanLevelByGroup[bg][nv]=(mv.orphanLevelByGroup[bg][nv]||0)+cc; });\n  });\n  mv.munis=munis; mv.leaders=Object.values(leaders).sort((a,b)=>b.total-a.total);\n}\nfunction autoAssignFor(mv){\n  const a={};\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__')return;\n    const v=mv.allowed[m]||{}; const votes=Object.keys(v);\n    if(votes.length){ let best=votes[0],bw=-1; for(const s of votes) if(v[s]>bw){bw=v[s];best=s;} a[m]=best; }\n    else if(mv.dest.length){ const c=mv.muniCentroid[m]; let best=mv.dest[0],bd=Infinity;\n      for(const s of mv.dest){ const sc=mv.secCentroid[s]; if(!sc||!c)continue; const d=(c[0]-sc[0])**2+(c[1]-sc[1])**2; if(d<bd){bd=d;best=s;} }\n      a[m]=best; }\n  });\n  return a;\n}\nfunction deslocFor(mv,assign){\n  let d=0;\n  mv.leaders.forEach(L=>{const byS={};let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=assign[m];if(s){byS[s]=(byS[s]||0)+c;tot+=c;}}\n    const home=mv.dest.reduce((a,s)=>(byS[s]||0)>(byS[a]||0)?s:a,mv.dest[0]);\n    d+=tot-(byS[home]||0);});\n  return d;\n}\nfunction spanMapFor(mv,a){const inc={};mv.dest.forEach(s=>inc[s]=0);\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=a[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  // Orphan CBs follow their leader's home sector, and that home depends on THIS candidate\n  // assignment — so they have to be counted here, or the optimizer would score and constrain a\n  // span that differs from the one the UI shows. Skipped entirely when there are no orphans.\n  if(mv.orphanByGroup&&Object.keys(mv.orphanByGroup).length){\n    mv.leaders.forEach(L=>{\n      const orphan=mv.orphanByGroup[L.grupo]||0; if(!orphan) return;\n      const byS={}; let tot=0;\n      for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s&&inc[s]!=null){ byS[s]=(byS[s]||0)+c; tot+=c; } }\n      if(!tot) return;   // no home to follow -> stranded, counted nowhere\n      const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n      if(inc[home]!=null) inc[home]+=orphan;\n    });\n  }\n  const sp={};mv.dest.forEach(s=>sp[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]);return sp;}\nfunction maxSpanOfFor(mv,a){return Math.max(...Object.values(spanMapFor(mv,a)),0);}\nfunction mergedSpanAll(){\n  const st=(SESS.settings&&SESS.settings.span)||{};\n  return {\n    setor:{...DEFAULT_SETTINGS.span.setor,...(st.setor||{})},\n    gv:{...DEFAULT_SETTINGS.span.gv,...(st.gv||{})},\n    mercado:{...DEFAULT_SETTINGS.span.mercado,...(st.mercado||{})},\n  };\n}\nfunction spanShortAll(){\n  const sp=mergedSpanAll();\n  const parts=['setor','gv','mercado'].map(lvl=>{ const s=spanShort(sp[lvl]); return s==='livre'?null:(LEVELS[lvl].label+' '+s); }).filter(Boolean);\n  return parts.length?parts.join(' · '):'livre';\n}\n// Sessões salvas antes de existir escala por nível guardavam targetSpan/maxSpan/minSpan soltos no\n// topo de settings, e esses três campos sempre foram só do setor — migra pra span.setor uma vez,\n// preservando null explícito (desligado) em vez de recair no padrão 2700/3500/2200.\nfunction migrateSpanSettings(raw){\n  raw=raw||{};\n  if(raw.span) return {\n    setor:{...DEFAULT_SETTINGS.span.setor,...raw.span.setor},\n    gv:{...DEFAULT_SETTINGS.span.gv,...raw.span.gv},\n    mercado:{...DEFAULT_SETTINGS.span.mercado,...raw.span.mercado},\n  };\n  const legacy=('targetSpan' in raw)||('maxSpan' in raw)||('minSpan' in raw);\n  return {\n    setor: legacy?{targetSpan:raw.targetSpan??null,maxSpan:raw.maxSpan??null,minSpan:raw.minSpan??null}:{...DEFAULT_SETTINGS.span.setor},\n    gv:{...DEFAULT_SETTINGS.span.gv},\n    mercado:{...DEFAULT_SETTINGS.span.mercado},\n  };\n}\nfunction spanLimits(mv){\n  // Span alvo/máximo/mínimo é uma escala POR NÍVEL de fechamento — cada nível olha só a sua.\n  const lvl=(mv&&mv.level)||'setor';\n  const st=mergedSpanAll()[lvl];\n  return {target:numOrNull(st.targetSpan),max:numOrNull(st.maxSpan),min:numOrNull(st.minSpan)};}\nfunction spanLimitsOn(lim){return !!(lim.target||lim.max||lim.min);}\nfunction spanViolationsOf(sp,lim){const over=[],under=[];\n  Object.entries(sp).forEach(([s,v])=>{ if(lim.max&&v>lim.max)over.push(s); if(lim.min&&v<lim.min)under.push(s); });\n  return {over,under};}\nfunction spanViolationsFor(mv,a){return spanViolationsOf(spanMapFor(mv,a),spanLimits(mv));}\n// With a target span the objective becomes RMS distance from the ideal size (punishes too big AND\n// too small). With no target we keep the legacy objective: shave the biggest sector.\nfunction spanCostFor(mv,a){const lim=spanLimits(mv),vals=Object.values(spanMapFor(mv,a));\n  if(!vals.length) return 0;\n  if(lim.target) return Math.sqrt(vals.reduce((s,v)=>s+(v-lim.target)**2,0)/vals.length);\n  return Math.max(...vals);}\n// ---------- ÓTIMO: objetivo multi-critério ----------\n// Three normalised objectives, each 0..1, so the weights below are directly comparable. The old\n// score added a span cost measured in CBs to a desloc measured in CBs, so on a large sector the\n// balance term was numerically ~100x the desloc term and silently decided everything on its own.\n//   balance — RMS spread of the resulting spans (against the target span when one is set,\n//             otherwise against their own mean), over that reference. 0 = every destination\n//             sector ends up the same size.\n//   dist    — how far each município travels, CB-weighted: half from the share of its outer\n//             border that faces the sector it was given to, half from centroid distance. 0 =\n//             every município went to a sector it physically touches. Adjacency used to be a\n//             pure yes/no constraint (contiguity); this is what makes it a preference too.\n//   orphan  — stranded orphan CBs (leader left without a home sector, so they land nowhere at\n//             all) plus CBs split away from their leader's home, over the CBs at stake.\n//             Stranded ones count OPT_STRAND_K times: a stranded CB leaves the model entirely,\n//             a deslocated one merely changes hands.\nconst OPT_W={balance:1, dist:0.85, orphan:1};\nconst OPT_STRAND_K=3;\nconst OPT_SIMS=24;             // simultaneous simulations per run, scaled down on big stages\nconst OPT_PASSES=14;           // local-search sweeps inside one simulation\nconst OPT_EVAL_BUDGET=120000;  // total candidate evaluations across the whole portfolio\nconst OPT_EXHAUSTIVE_MAX=60000;\nfunction optDistPrep(mv){\n  let mx=0;\n  Object.keys(mv.munis).forEach(m=>{ if(m==='__OUTROS__') return; const c=mv.muniCentroid[m]; if(!c) return;\n    mv.dest.forEach(s=>{ const sc=mv.secCentroid[s]; if(!sc) return;\n      const d=Math.hypot(c[0]-sc[0],c[1]-sc[1]); if(d>mx) mx=d; }); });\n  return mx||1;\n}\nfunction optDistCost(mv,m,s,dmax){\n  const v=mv.allowed[m]||{}; let tot=0; for(const k in v) tot+=v[k];\n  const share=tot?((v[s]||0)/tot):0;\n  const c=mv.muniCentroid[m], sc=mv.secCentroid[s];\n  // Missing centroid on either side is neutral (0.5), never 0 — an unmeasurable move must not\n  // score as the cheapest one on the board.\n  const geo=(c&&sc)?Math.min(1,Math.hypot(c[0]-sc[0],c[1]-sc[1])/dmax):0.5;\n  return 0.5*(1-share)+0.5*geo;\n}\nfunction optEval(mv,a,dmax){\n  const lim=spanLimits(mv);\n  const sp=spanMapFor(mv,a), vals=Object.values(sp);\n  let balance=0;\n  if(vals.length){\n    const ref=lim.target||(vals.reduce((x,y)=>x+y,0)/vals.length);\n    if(ref) balance=Math.min(1,Math.sqrt(vals.reduce((s,v)=>s+(v-ref)**2,0)/vals.length)/ref);\n  }\n  let cbs=0,dist=0;\n  for(const m in a){ const o=mv.munis[m]; if(!o) continue; const w=+o.cbs||0; cbs+=w; dist+=w*optDistCost(mv,m,a[m],dmax); }\n  dist=cbs?(dist/cbs):0;\n  let stranded=0,desloc=0,tot=0;\n  mv.leaders.forEach(L=>{\n    const byS={}; let t=0;\n    for(const [m,c] of Object.entries(L.byMuni)){ if(m==='__OUTROS__') continue; const s=a[m]; if(s){ byS[s]=(byS[s]||0)+c; t+=c; } }\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    tot+=t+orphan;\n    if(!t){ stranded+=orphan; return; }   // no allocated CBs -> no home -> these orphans land nowhere\n    const home=mv.dest.reduce((x,s)=>(byS[s]||0)>(byS[x]||0)?s:x,mv.dest[0]);\n    desloc+=t-(byS[home]||0);\n  });\n  const orphanTerm=tot?Math.min(1,(desloc+OPT_STRAND_K*stranded)/tot):0;\n  return { cost:OPT_W.balance*balance+OPT_W.dist*dist+OPT_W.orphan*orphanTerm,\n           balance, dist, desloc, stranded, sp, viol:spanViolationsOf(sp,lim) };\n}\nfunction optScoreFor(mv,a){ return optEval(mv,a,mv.__optDmax||optDistPrep(mv)).cost; }\n// Seeded PRNG on purpose. computeMovement runs the optimizer for EVERY stage and recomputeFrom\n// re-runs every stage downstream of an edit, so a Math.random() portfolio would hand back a\n// different allocation on each pass and a saved timeline would never reproduce.\nfunction optRng(seed){ let a=seed>>>0;\n  return ()=>{ a=(a+0x6D2B79F5)>>>0; let t=Math.imul(a^(a>>>15),1|a);\n    t=(t+Math.imul(t^(t>>>7),61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }\nfunction optSeed(mv,ms){ let h=2166136261>>>0;\n  const s=String(mv.sector)+'|'+ms.join(',')+'|'+mv.dest.join(',');\n  for(let i=0;i<s.length;i++) h=Math.imul(h^s.charCodeAt(i),16777619);\n  return h>>>0; }\nfunction contigBadFor(mv,assign){\n  // Contiguidade geográfica só faz sentido no grão de município (setor fechando): setores e GVs\n  // não precisam formar uma mancha contínua quando GV/mercado fecha — cada setor/GV vai pro destino\n  // que faz sentido pra ele, sem exigir que o conjunto redistribuído continue \"grudado\".\n  if((mv.level||'setor')!=='setor') return [];\n  const bySec={}; for(const m in assign){(bySec[assign[m]]=bySec[assign[m]]||[]).push(m);}\n  const bad=[];\n  for(const s in bySec){const grp=bySec[s],inGrp=new Set(grp);\n    const seeds=grp.filter(m=>(mv.allowed[m]||{})[s]);\n    if(!seeds.length){bad.push(s);continue;}\n    const seen=new Set(seeds),st=[...seeds];\n    while(st.length){const m=st.pop();(mv.closeAdj[m]||[]).forEach(o=>{if(inGrp.has(o)&&!seen.has(o)){seen.add(o);st.push(o);}});}\n    if(seen.size!==grp.length)bad.push(s);}\n  return bad;\n}\n// Usada pelo clique no mapa quando \"forçar conectividade de território\" está ligado: em vez de\n// avançar cegamente pro próximo destino da lista, pula candidatos que introduziriam uma quebra NOVA\n// (compara contra o baseBad de ANTES do clique, então uma quebra pré-existente alheia a este\n// município não trava o ciclo pra sempre). Sem candidato seguro, mantém o destino atual.\nfunction nextContigSafeDest(mv,m){\n  const n=mv.dest.length; if(!n) return mv.assign[m];\n  const cur=mv.assign[m];\n  const baseBad=new Set(contigBadFor(mv,mv.assign));\n  const i0=mv.dest.indexOf(cur);\n  for(let k=1;k<=n;k++){\n    const cand=mv.dest[((i0<0?0:i0)+k)%n];\n    if(cand===cur) continue;\n    const trial={...mv.assign,[m]:cand};\n    if(!contigBadFor(mv,trial).some(s=>!baseBad.has(s))) return cand;\n  }\n  return cur;\n}\nfunction greedyFor(mv,ms,opts){const lim=spanLimits(mv);const a={};\n  ms.forEach((m,i)=>{let best=opts[i][0],bd=Infinity;\n    for(const s of opts[i]){a[m]=s;\n      // over-max only grows as we keep assigning, so it is safe to penalise mid-build;\n      // under-min shrinks, so penalising it here would just chase noise.\n      const over=lim.max?spanViolationsOf(spanMapFor(mv,a),{max:lim.max,min:null}).over.length:0;\n      const d=optScoreFor(mv,a)+(contigBadFor(mv,a).length?1e9:0)+over*1e6;\n      if(d<bd){bd=d;best=s;}}\n    a[m]=best;});\n  return a;}\nfunction optimizeFor(mv){\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__');\n  if(!ms.length||!mv.dest.length){ mv.optBadge=mv.dest.length?'':'sem setor destino identificado'; return {}; }\n  const opts=ms.map(m=>{const al=(mv.reachable[m]&&mv.reachable[m].length)?mv.reachable[m]:mv.dest.slice();return al.length?al:mv.dest.slice();});\n  const combos=opts.reduce((a,o)=>a*o.length,1);\n  const lim=spanLimits(mv);\n  const dmax=optDistPrep(mv); mv.__optDmax=dmax;\n\n  // Feasibility ladder — same meaning as before, because renderSpanRule and the saved sessions\n  // both read mv.optTier: 0 = contíguo + max + min span, 1 = contíguo + max span, 2 = contíguo\n  // only. Every candidate any simulation produces is offered to all three tiers; we ship the\n  // strictest tier that got a solution, so an unreachable min/max relaxes loudly instead of\n  // stranding the stage with nothing.\n  const tier=[null,null,null],tSc=[Infinity,Infinity,Infinity],tEv=[null,null,null];\n  let feasible=0,evals=0;\n  const offer=(a,ev)=>{\n    if(contigBadFor(mv,a).length) return false;\n    feasible++;\n    const put=t=>{ if(ev.cost<tSc[t]){ tSc[t]=ev.cost; tier[t]={...a}; tEv[t]=ev; } };\n    put(2);\n    if(!ev.viol.over.length){ put(1); if(!ev.viol.under.length) put(0); }\n    return true;\n  };\n  const evalOf=a=>{ evals++; return optEval(mv,a,dmax); };\n\n  // Small enough to settle the question outright: enumerate every contiguous allocation and the\n  // winner is provably the best one under this objective. No search needed, so none is run.\n  const exhaustive=combos<=OPT_EXHAUSTIVE_MAX;\n  let sims=0;\n  if(exhaustive){\n    const cur={};\n    const rec=i=>{ if(i===ms.length){ offer(cur,evalOf(cur)); return; }\n      for(const s of opts[i]){ cur[ms[i]]=s; rec(i+1); } };\n    rec(0);\n  } else {\n    // Too big to enumerate, so run a portfolio of independent simulations instead. Each one is a\n    // seed assignment, then hill-climbing on single-município reassignments, then one perturbation\n    // and another climb to escape whatever local optimum it settled into. Different seeds land in\n    // different basins; the ladder above keeps the best across all of them.\n    const improve=a=>{\n      let ev=evalOf(a); offer(a,ev);\n      for(let pass=0;pass<OPT_PASSES;pass++){\n        let moved=false;\n        for(let i=0;i<ms.length;i++){\n          const m=ms[i],cur=a[m]; let best=cur,bev=ev;\n          for(const s of opts[i]){\n            if(s===cur) continue;\n            a[m]=s;\n            const e=evalOf(a);\n            // contiguity is a hard constraint, and the cheaper test runs first on purpose\n            if(e.cost<bev.cost-1e-12 && !contigBadFor(mv,a).length){ best=s; bev=e; }\n          }\n          a[m]=best;\n          if(best!==cur){ ev=bev; moved=true; offer(a,ev); }\n        }\n        if(!moved) break;\n      }\n    };\n    const rng=optRng(optSeed(mv,ms));\n    const perSim=Math.max(1,ms.length*Math.max(1,mv.dest.length)*OPT_PASSES);\n    const budget=Math.max(4,Math.min(OPT_SIMS,Math.floor(OPT_EVAL_BUDGET/perSim)||4));\n    const seeds=[];\n    if(mv.assign&&ms.every(m=>mv.assign[m])) seeds.push({...mv.assign});   // never regress on what is already on screen\n    seeds.push(autoAssignFor(mv));                                        // adjacency-first seed\n    seeds.push(greedyFor(mv,ms,opts));                                    // balance-first seed\n    while(seeds.length<budget){\n      // randomised construction biased toward nearby sectors — the bias keeps each seed sane, the\n      // randomness is what sends the runs into different basins\n      const a={};\n      ms.forEach((m,i)=>{ let bs=opts[i][0],bw=-Infinity;\n        for(const s of opts[i]){ const w=(1-optDistCost(mv,m,s,dmax))+rng()*0.9; if(w>bw){bw=w;bs=s;} }\n        a[m]=bs; });\n      seeds.push(a);\n    }\n    seeds.forEach(seed=>{\n      const a={...seed};\n      // a restored or legacy assign can still name a sector this stage no longer offers\n      ms.forEach((m,i)=>{ if(!opts[i].includes(a[m])) a[m]=opts[i][0]; });\n      improve(a);\n      const kick=Math.max(1,Math.round(ms.length*0.25));\n      for(let k=0;k<kick;k++){ const i=Math.floor(rng()*ms.length); a[ms[i]]=opts[i][Math.floor(rng()*opts[i].length)]; }\n      improve(a);\n    });\n    sims=seeds.length;\n  }\n\n  const t=tier.findIndex(x=>x);\n  if(t<0){ mv.optTier=-1; mv.optBadge='sem solução conectada — heurística'; return greedyFor(mv,ms,opts); }\n  const best=tier[t], ev=tEv[t]; mv.optTier=t;\n  const relax=[];\n  if(t>0 && lim.min) relax.push('span mínimo '+fmt(lim.min));\n  if(t>1 && lim.max) relax.push('span máximo '+fmt(lim.max));\n  mv.optStats={balance:ev.balance,dist:ev.dist,desloc:ev.desloc,stranded:ev.stranded,sims,evals,feasible,combos,exhaustive};\n  mv.optBadge=`ótimo · ${exhaustive?`espaço completo (${fmt(combos)})`:`${fmt(sims)} simulações`}`\n    +` · equilíbrio ±${Math.round(ev.balance*100)}% · adjacência ${Math.round((1-ev.dist)*100)}%`\n    +` · quebra ${fmt(ev.desloc)} CBs · ${fmt(ev.stranded)} fora do setor sem destino`\n    +` · pico ${fmt(maxSpanOfFor(mv,best))} CBs`\n    +(lim.target?` · alvo ${fmt(lim.target)}`:'')\n    +(relax.length?` · ⚠ inatingível: ${relax.join(' + ')}`:(spanLimitsOn(lim)?' · limites ok':''));\n  return best;\n}\nfunction computeDerived(mv){\n  const inc={},gained={},orph={};mv.dest.forEach(s=>{inc[s]=0;gained[s]=0;orph[s]=0;});\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(s&&inc[s]!=null)inc[s]+=o.cbs;});\n  let desloc=0,orphanTotal=0,orphanStranded=0;\n  const L2=mv.leaders.map(L=>{const byS={};mv.dest.forEach(s=>byS[s]=0);let tot=0;\n    for(const [m,c] of Object.entries(L.byMuni)){if(m==='__OUTROS__')continue;const s=mv.assign[m];if(s&&byS[s]!=null){byS[s]+=c;tot+=c;}}\n    // no allocated CBs => no home. Picking dest[0] by default would silently dump this leader's\n    // orphan CBs into an arbitrary sector, so we leave it null and report them as stranded.\n    const home=(tot>0&&mv.dest.length)?mv.dest.reduce((a,s)=>byS[s]>byS[a]?s:a,mv.dest[0]):null;\n    const dl=tot-(home?byS[home]:0);desloc+=dl;if(home)gained[home]++;\n    const orphan=(mv.orphanByGroup&&mv.orphanByGroup[L.grupo])||0;\n    orphanTotal+=orphan;\n    if(orphan){ if(home&&orph[home]!=null) orph[home]+=orphan; else orphanStranded+=orphan; }\n    return {...L,byS,home,desloc:dl,assignedTotal:tot,orphan,orphanHome:(orphan&&home)?home:null};});\n  const span={};mv.dest.forEach(s=>span[s]=(+(mv.baseMetrics[s]?.cbs||0))+inc[s]+orph[s]);\n  return {inc,gained,desloc,leaders:L2,span,orph,orphanTotal,orphanStranded};\n}\n\nasync function computeMovement(code,level,world){\n  level=level||'setor';\n  code=String(code);\n  const mv={sector:code,level,policy:'fora',view:'tobe',selMuni:null,optBadge:''};\n  const SNAP=SESS.snapLiteral;\n  // GV/mercado precisam da base ANTES de montar a query (pra saber quais CD_SETOR entram no IN);\n  // setor não precisa — mantém o timing original (a query dispara sem esperar o CSV).\n  let domBase=level==='setor'?null:await ensureDominantes();\n  const memberSectors=level==='setor'?[code]:memberSectorsOf(level,code,world);\n  if(!memberSectors.length) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const secList=memberSectors.join(',');\n\n  const mSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, CD_GRUPO grupo, MAX(DS_LEADER_NAME) lider,\n     COUNT(DISTINCT CD_CONSULTORA) cbs, ROUND(SUM(VL_PEDIDOS_CICLO),0) receita,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon,\n     ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos,\n     SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP}\n    GROUP BY DS_MUNICIPIO, CD_SETOR, CD_GRUPO`;\n  const ufSQL=`SELECT DS_ESTADO uf, COUNT(*) n FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DT_HISTORICO=${SNAP} GROUP BY DS_ESTADO ORDER BY n DESC`;\n  const trendSQL=`SELECT operational_cycle ciclo, COUNT(DISTINCT person_code) cbs,\n     COUNT(DISTINCT CASE WHEN registered_bc_ind=1 THEN person_code END) ativas,\n     ROUND(SUM(revenue_nat_ss_amt),0) receita, ROUND(SUM(points_nat_ss_amt),0) pontos\n    FROM ${ELO} WHERE country_code=1 AND sector_code IN (${secList}) AND operational_cycle<=${SESS.eloMaxCycle||999999} GROUP BY operational_cycle ORDER BY operational_cycle DESC LIMIT 8`;\n  const lvlSQL=`SELECT DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs,\n     ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat, ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_NVL_PLAN_CRESC ORDER BY cbs DESC`;\n  const lvlMuniSQL=`SELECT DS_MUNICIPIO municipio, CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${secList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY DS_MUNICIPIO, CD_SETOR, DS_NVL_PLAN_CRESC`;\n  const [mR,ufR,tR,lR,lmR]=await Promise.all([q(mSQL),q(ufSQL),q(trendSQL),q(lvlSQL),q(lvlMuniSQL)]);\n\n  const ownMatrix=rows(mR).filter(r=>+r.cbs>0);\n  mv.pureOwnMetrics=blankM();\n  ownMatrix.forEach(r=>{ mv.pureOwnMetrics.cbs+=+r.cbs; mv.pureOwnMetrics.receita+=+r.receita||0; mv.pureOwnMetrics.rec_nat+=+r.rec_nat||0; mv.pureOwnMetrics.rec_avon+=+r.rec_avon||0; mv.pureOwnMetrics.pontos+=+r.pontos||0; mv.pureOwnMetrics.pedidos+=+r.pedidos||0; mv.pureOwnMetrics.ativas+=+r.ativas||0; });\n\n  mv.levelByMuni={};\n  if(level==='setor') Object.entries(world.muniLevel||{}).forEach(([m,lv])=>{ if(world.muniOverride[m]===code) mv.levelByMuni[m]={...lv}; });\n\n  mv.trend=rows(tR).map(r=>({...r,cbs:+r.cbs,ativas:+r.ativas,receita:+r.receita,pontos:+r.pontos})).filter(r=>r.cbs>10).reverse();\n  mv.levels=rows(lR);\n\n  let absorbedRows=[],inheritedOrphanRows=[];\n  if(level==='setor'){\n    Object.entries(world.muniOverride).forEach(([m,destSec])=>{ if(destSec===code && world.muniRow[m]) absorbedRows.push(...world.muniRow[m]); });\n    inheritedOrphanRows=(world.sectorOrphanRows&&world.sectorOrphanRows[code])||[];\n  }\n  mv.matrix=ownMatrix.concat(absorbedRows,inheritedOrphanRows);\n\n  const ufList=rows(ufR).map(r=>r.uf).filter(Boolean);\n  const domUF=ufList[0]||'PR';\n  mv.domUF=domUF;\n  domBase=domBase||await ensureDominantes();\n  // Uma GV ou mercado pode espalhar por mais UFs do que \"a UF dominante + suas vizinhas\" cobre —\n  // toda UF que aparece na base de dominantes de QUALQUER setor-membro entra na janela de shapes.\n  const memberUFs=new Set([domUF]);\n  memberSectors.forEach(s=>{ (domBase[String(s)]||new Set()).forEach(entry=>{ const u=mUf(entry); if(u) memberUFs.add(u); }); });\n  const ufSet=new Set(memberUFs);\n  memberUFs.forEach(u=>(UF_BORDERS[u]||[]).forEach(b=>ufSet.add(b)));\n  const geos=await Promise.all([...ufSet].map(u=>geoUF(u).catch(()=>null)));\n  const allFeat=geos.filter(Boolean).flatMap(g=>g.features);\n  const adj=buildAdj(allFeat);\n\n  // A UF só vem da base de dominantes (setor real da linha × município), nunca de \"esse nome só\n  // apareceu numa UF carregada\" ou de um chute pra UF dominante do fechamento inteiro. Os dois\n  // eram seguros quando só 1-3 UFs entravam na janela (fechamento de setor); GV e principalmente\n  // mercado carregam dezenas de UFs de uma vez, então \"único entre as UFs carregadas\" deixa de\n  // significar \"único no Brasil\" — era exatamente esse chute que colocava um homônimo de outra UF\n  // espalhado longe do resto do mapa. Sem entrada na base, o município fica sem UF (mesmo\n  // tratamento de \"sem shape\" que já existe) em vez de arriscar aparecer no lugar errado.\n  const ufGuessed={};\n  const ufFor=(setorOf,muni)=>{const nm=norm(muni);\n    return (DOM.ufBySetorMuni&&DOM.ufBySetorMuni[String(setorOf)+'|'+nm])||'';};\n  mv.matrix.forEach(r=>{ if(!r.uf) r.uf=ufFor(r.setor,r.municipio); });\n  rows(lmR).forEach(r=>{ const k=mkey(r.municipio,ufFor(r.setor,r.municipio)); (mv.levelByMuni[k]=mv.levelByMuni[k]||{})[r.nivel]=+r.cbs; });\n\n  const totBy={}; mv.matrix.forEach(r=>{const n=mkey(r.municipio,r.uf); totBy[n]=(totBy[n]||0)+ +r.cbs;});\n  const footprint=new Set(Object.entries(totBy).filter(([,c])=>c>0).map(([m])=>m));\n  const withShape=[...footprint].filter(n=>adj.feat[n]||world.muniGeo[n]);\n  mv.cbsByMuni={...totBy}; mv.shapeMunis=withShape.slice();\n  mv.noShapeMunis=[...footprint].filter(n=>!(adj.feat[n]||world.muniGeo[n])).map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.noShapeCbs=mv.noShapeMunis.reduce((a,x)=>a+x.cbs,0);\n\n  // ---- RULE: quais municípios pertencem a este fechamento ----\n  // setor: exatamente a regra de hoje. GV/mercado: união da base de TODOS os setores-membro — a\n  // base já garante que um setor pertence a uma única GV/mercado, então não existe aqui o spillover\n  // fuzzy que município tem contra setor (por isso o orphan-handling abaixo é pulado nesses níveis).\n  let domOwn;\n  if(level==='setor'){\n    domOwn=domBase[code]||new Set();\n  } else {\n    domOwn=new Set();\n    memberSectors.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>domOwn.add(n)));\n  }\n  const inherited=level==='setor'?new Set(Object.keys(world.muniOverride||{}).filter(m=>world.muniOverride[m]===code)):new Set();\n  if(!domOwn.size && !inherited.size) throw new Error(`${LEVELS[level].label} ${code} não existe na base de municípios dominantes (CSV) — atualize a base antes de fechar`);\n  const belongs=n=>domOwn.has(n)||(!DOM.hasUf&&domOwn.has(mName(n)))||inherited.has(n);\n  mv.domCsvMunis=domOwn.size;\n  const dropped=level==='setor'?withShape.filter(n=>!belongs(n)):[];\n  if(withShape.length && dropped.length===withShape.length) throw new Error(`${LEVELS[level].label} ${code}: nenhum dos municípios com CBs está na base de dominantes — nada pra realocar`);\n  const droppedSet=new Set(dropped);\n  mv.excludedMunis=dropped.map(n=>({muni:n,cbs:totBy[n]||0})).sort((a,b)=>b.cbs-a.cbs);\n  mv.excludedCbs=dropped.reduce((a,n)=>a+(totBy[n]||0),0);\n\n  const closingNames=withShape.filter(n=>!droppedSet.has(n));\n  const closingSet=new Set(closingNames);\n  mv.geoFeat={}; closingNames.forEach(n=>{ mv.geoFeat[n]=(adj.feat[n]&&adj.feat[n].f)||world.muniGeo[n]; });\n\n  // ---- município compartilhado por vários setores (São Paulo etc.): o setor fechando é dono só de\n  // UMA fatia dele, não da cidade inteira — sem isto a camada `.muni` interativa pintava (e deixava\n  // clicar) o município INTEIRO com a cor do destino escolhido, mesmo a maior parte dele pertencendo\n  // a outros setores sem nenhuma relação com este fechamento. mv.sharedMuniSlices carrega o resto da\n  // cidade pra camada de contexto — o GV-check e a classificação ficam pra mais adiante, quando\n  // sameParent/mv.rejectedCodes já existem nesta função.\n  mv.sharedMuniSlices=[];\n  if(level==='setor'){\n    closingNames.forEach(n=>{\n      const siblings=[...(DOM.setoresByMuni&&DOM.setoresByMuni[n]||new Set())].filter(s=>s!==code);\n      if(!siblings.length||!mv.geoFeat[n]) return;\n      const owners=[code,...siblings].sort((a,b)=>(+a-+b)||(a<b?-1:a>b?1:0));\n      const slices=sliceFeatureEqual(mv.geoFeat[n],owners.length);\n      const mine=slices[owners.indexOf(code)];\n      if(mine&&!mine.__empty) mv.geoFeat[n]=mine;\n      owners.forEach((s,i)=>{\n        if(s===code) return;\n        const sl=slices[i];\n        if(sl&&!sl.__empty) mv.sharedMuniSlices.push({feature:sl,sector:s});\n      });\n    });\n  }\n\n  // ---- unidade de realocação: a geometria é sempre município; a UNIDADE que troca de dono é o\n  // próprio município (setor fecha), o setor inteiro (GV fecha) ou a GV inteira (mercado fecha).\n  const municipioSetor={}; mv.matrix.forEach(r=>{ const n=mkey(r.municipio,r.uf); if(!municipioSetor[n]) municipioSetor[n]=String(r.setor); });\n  const resolveUnit=n=>{\n    if(level==='setor') return n;\n    const sOf=municipioSetor[n]; if(!sOf) return '';\n    return level==='gv'?sOf:(DOM.gvBySetor[sOf]||'');\n  };\n  mv.unitOfMuni={}; closingNames.forEach(n=>{ mv.unitOfMuni[n]=resolveUnit(n); });\n  mv.unitMunis={}; closingNames.forEach(n=>{ const u=mv.unitOfMuni[n]; if(!u) return; (mv.unitMunis[u]=mv.unitMunis[u]||[]).push(n); });\n  const units=level==='setor'?closingNames:Object.keys(mv.unitMunis);\n\n  const drawnCsv=new Set(closingNames.map(n=>DOM.hasUf?n:mName(n)));\n  const footKeyOf=k=>DOM.hasUf?(footprint.has(k)?k:null):([...footprint].find(x=>mName(x)===k)||null);\n  mv.csvMunisTotal=domOwn.size;\n  mv.ufGuessed={...ufGuessed};\n  if(level==='setor'){\n    mv.missingMunis=[...domOwn].filter(k=>!drawnCsv.has(k)).map(k=>{\n      const fk=footKeyOf(k);\n      const hasShape=!!(adj.feat[fk||k]||world.muniGeo[fk||k]);\n      const alias=[...footprint].filter(x=>x!==k&&mName(x)===mName(k));\n      let reason;\n      if(!fk&&alias.length) reason=`UF divergente — as CBs entraram na chave ${alias.join(' / ')} e a base diz ${k}`;\n      else if(!fk) reason=hasShape?'sem CBs neste setor no snapshot — desenhado como vizinho':'sem CBs neste setor no snapshot';\n      else if(!hasShape) reason='sem shape nas UFs carregadas';\n      else reason='chave não casou com a base de dominantes';\n      return {muni:k,cbs:(fk?totBy[fk]:alias.reduce((a,x)=>a+(totBy[x]||0),0))||0,reason:reason};\n    });\n  } else {\n    mv.missingMunis=[];\n  }\n\n  // ---- HARD RULE: destino tem de compartilhar a fronteira certa acima do que está fechando ----\n  // setor → mesma GV; GV → mesmo mercado; mercado → nada acima, qualquer vizinho vale.\n  const ownParent=parentOf(level,code);\n  if(level!=='mercado' && !ownParent) throw new Error(`${LEVELS[level].label} ${code} não tem ${level==='setor'?'CD_GV':'MERCADO'} na base de municípios dominantes — sem essa fronteira não há como garantir que os destinos ficam do lado certo da hierarquia`);\n  const sameParent=destCode=>!ownParent||parentOf(level,destCode)===ownParent;\n  mv.gv=ownParent;\n  const alreadyClosed=destCode=>level==='setor'?world.closedSectors.has(destCode):(level==='gv'?(world.closedGvs||new Set()).has(destCode):(world.closedMercados||new Set()).has(destCode));\n\n  mv.allowed={}; mv.allowedRaw={};\n  units.forEach(u=>{ mv.allowed[u]={}; mv.allowedRaw[u]={}; });\n  mv.rejectedCodes=new Set(); mv.rejectedInfo={};\n\n  const extVotes={}; const nbrNames=new Set(); let domSecOf={};\n  if(level==='setor'){\n    // setor: destino descoberto por ADJACÊNCIA GEOGRÁFICA de município — uma query ao vivo por\n    // vizinho, mas o perímetro de UM setor é pequeno o bastante pra valer a pena.\n    closingNames.forEach(n=>{\n      if(!adj.feat[n]) return;\n      adj.nbrs(n).forEach(({o,w})=>{ if(!closingSet.has(o) && !droppedSet.has(o)){ nbrNames.add(o); (extVotes[n]=extVotes[n]||[]).push({o,w}); } });\n    });\n    // Setor dominante de cada vizinho vem da base ESTÁTICA (DOM.setorByMuni), não de uma consulta ao\n    // vivo por contagem de CBs: a consulta antiga só devolvia linha pro município com CB ativo NESTE\n    // snapshot — um vizinho geograficamente real mas sem CB agora simplesmente não aparecia, então\n    // nunca virava rejeitado nem contexto (sumia do mapa em vez de aparecer cinza). A base de\n    // dominantes cobre todo município, com ou sem CB no momento, então nenhum vizinho fica de fora.\n    nbrNames.forEach(n=>{ domSecOf[n]=DOM.setorByMuni[n]; });\n    nbrNames.forEach(n=>{ if(world.muniOverride && world.muniOverride[n]) domSecOf[n]=world.muniOverride[n]; });\n\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    const gvHidden=new Set();\n    nbrNames.forEach(n=>{\n      const destCode=destUnitOfSector(level,domSecOf[n],world);\n      if(destCode && !sameParent(destCode)){\n        gvHidden.add(n);\n        mv.nbrCurrentSector[n]=destCode;\n        mv.rejectedCodes.add(destCode);\n        if(!mv.rejectedInfo[destCode]) mv.rejectedInfo[destCode]={code:destCode,parent:parentOf('setor',destCode)};\n        if(mv.gvRejected.indexOf(destCode)<0) mv.gvRejected.push(destCode);\n        return;\n      }\n      if(domSecOf[n]) mv.nbrCurrentSector[n]=destCode;\n    });\n    // Rejected neighbours KEEP their nbrNames/nbrCurrentSector entry — only their votes are stripped\n    // below, never their geometry — so the map can gray them in with a hover explaining the mismatch\n    // instead of just erasing them from the world.\n    Object.keys(extVotes).forEach(n=>{ extVotes[n]=extVotes[n].filter(v=>!gvHidden.has(v.o)); });\n\n    // Shared-município siblings (mv.sharedMuniSlices, built earlier) get the SAME hard GV rule as any\n    // other destination candidate — a slice of São Paulo owned by a different-GV setor is rejected\n    // context, never a silently-available destination.\n    mv.sharedMuniSlices.forEach(sl=>{\n      if(!sameParent(sl.sector)){\n        mv.rejectedCodes.add(sl.sector);\n        if(!mv.rejectedInfo[sl.sector]) mv.rejectedInfo[sl.sector]={code:sl.sector,parent:parentOf('setor',sl.sector)};\n        if(mv.gvRejected.indexOf(sl.sector)<0) mv.gvRejected.push(sl.sector);\n      }\n    });\n\n    mv.bridgeMunis=closingNames.slice();\n    mv.ignoredBridges=[];\n\n    const wAny={};\n    closingNames.forEach(n=>{\n      const u=n;\n      if(!mv.allowed[u]) return;\n      (extVotes[n]||[]).forEach(({o,w})=>{\n        const destCode=mv.nbrCurrentSector[o];\n        if(!destCode || destCode===code || alreadyClosed(destCode)) return;\n        mv.allowed[u][destCode]=(mv.allowed[u][destCode]||0)+w;\n        mv.allowedRaw[u][destCode]=(mv.allowedRaw[u][destCode]||0)+w;\n        wAny[destCode]=(wAny[destCode]||0)+w;\n      });\n    });\n    mv.dest=Object.keys(wAny).sort((a,b)=>wAny[b]-wAny[a]).slice(0,6);\n    if(!mv.dest.length){\n      mv.dest=[...new Set(Object.values(mv.nbrCurrentSector))].filter(destCode=>destCode&&destCode!==code&&!alreadyClosed(destCode)&&sameParent(destCode)).slice(0,6);\n      units.forEach(u=>{ if(mv.allowed[u]&&!Object.keys(mv.allowed[u]).length){ mv.dest.forEach(destCode=>mv.allowed[u][destCode]=1); } });\n    }\n  } else {\n    // GV/mercado: destino é SEMPRE todo irmão válido na hierarquia — NUNCA descoberto por\n    // adjacência geográfica de município. Uma GV/mercado pode ter território espalhado por\n    // dezenas de UFs sem qualquer conexão física entre as partes (a hierarquia aqui é\n    // organizacional, não geográfica) — \"vizinho por fronteira de município\" não é só caro (a\n    // query ao vivo vira um IN-list de centenas de municípios e estoura timeout: foi exatamente\n    // isso que quebrou o fechamento de um mercado espalhado) — é a pergunta errada. A fronteira que\n    // já importa (mesma GV / mesmo mercado) é imposta pela regra de parentesco logo acima; dentro\n    // dela QUALQUER irmão é candidato, e o otimizador decide pelo equilíbrio de span e continuidade\n    // de líder, nunca por proximidade física — geografia não pesa aqui.\n    mv.nbrCurrentSector={};\n    mv.gvRejected=[];\n    mv.bridgeMunis=closingNames.slice();\n    mv.ignoredBridges=[];\n    const siblings=level==='gv'\n      ? [...(DOM.gvsByMercado[ownParent]||[])]\n      : Object.keys(DOM.sectorsByMercado||{});\n    mv.dest=siblings.filter(destCode=>destCode!==code && !alreadyClosed(destCode) && sameParent(destCode));\n    units.forEach(u=>{ mv.dest.forEach(destCode=>{ mv.allowed[u][destCode]=1; mv.allowedRaw[u][destCode]=1; }); });\n\n    // Geographically-touching GVs that fail the hierarchy rule (different mercado) — grayed on the\n    // map with a hover explaining why, using ONLY the static dominantes CSV (DOM.setorByMuni /\n    // DOM.gvBySetor / DOM.mercadoByGv, already in memory) — no live query, so the timeout that broke\n    // geographic discovery for a spread-out mercado closing (comment above) never re-enters here.\n    if(level==='gv'){\n      closingNames.forEach(n=>{\n        if(!adj.feat[n]) return;\n        adj.nbrs(n).forEach(({o})=>{\n          if(closingSet.has(o) || nbrNames.has(o)) return;\n          const sOf=DOM.setorByMuni[o]; if(!sOf) return;\n          const nGv=DOM.gvBySetor[sOf]; if(!nGv || nGv===code || sameParent(nGv)) return;\n          nbrNames.add(o);\n          mv.nbrCurrentSector[o]=nGv;\n          mv.rejectedCodes.add(nGv);\n          if(!mv.rejectedInfo[nGv]) mv.rejectedInfo[nGv]={code:nGv,parent:DOM.mercadoByGv[nGv]||''};\n          if(mv.gvRejected.indexOf(nGv)<0) mv.gvRejected.push(nGv);\n        });\n      });\n    }\n  }\n  mv.dest=mv.dest.filter(sameParent);\n\n  // ---- O QUE É DESENHADO: território COMPLETO de cada destino (não só a borda), sempre lido da\n  // mesma base de dominantes já usada pro lado que fecha.\n  const territoryOf=destCode=>{\n    const secs=memberSectorsOf(level,destCode,world);\n    const out=new Set();\n    secs.forEach(s=>(domBase[String(s)]||new Set()).forEach(n=>out.add(n)));\n    return out;\n  };\n  mv.muniOwners={};\n  mv.dest.forEach(destCode=>{\n    territoryOf(destCode).forEach(n=>{\n      // A município that's part of the CLOSING sector itself is already drawn (and clicked) via\n      // mv.geoFeat/unitAssign, further down — it isn't up-for-grabs context. Without this guard, a\n      // multi-sector city (São Paulo etc.) that also happens to be one of the closing sector's OWN\n      // municípios got its single \"current assignment\" fill painted right over the sliced context\n      // layer underneath — same IBGE polygon, and the closing layer draws on top.\n      if(closingSet.has(n)) return;\n      (mv.muniOwners[n]=mv.muniOwners[n]||[]).push(destCode);\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  // Draw every município we actually resolved a current sector for — not just the top-voted\n  // mv.dest candidates + rejectedCodes. A real same-GV neighbour sector that lost the vote race\n  // (more than 6 distinct neighbours, or simply outvoted) still gets classified further down as\n  // 'plain' context; filtering it out HERE instead left it entirely undrawn — a blank gap on the\n  // map exactly where a real, valid neighbouring sector is, instead of the grey context it should be.\n  // A neighbour whose current sector never resolved (no CRM row for it in this snapshot, a name\n  // mismatch, whatever) used to be dropped here entirely — invisible, no way to tell it apart from\n  // \"there's genuinely nothing there\". It still keeps its real shape; the render step below tags it\n  // 'unresolved' and shows the actual município name on hover instead, which is what you want when\n  // debugging why something didn't resolve.\n  // Um setor/GV rejeitado só entrava no mapa pelos municípios que sobreviveram à varredura de\n  // adjacência (os poucos que realmente encostam no que está fechando) — o resto do território dele,\n  // segundo a base de dominantes, nunca aparecia. Mesma expansão que mv.dest já recebe logo acima:\n  // território COMPLETO do setor/GV rejeitado, não só a fatia que faz fronteira.\n  mv.rejectedCodes.forEach(destCode=>{\n    territoryOf(destCode).forEach(n=>{\n      if(closingSet.has(n)) return;\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  // Setor fechando: um vizinho da MESMA GV que perdeu a votação (mais de 6 candidatos, ou simplesmente\n  // menos votado) nunca vira mv.dest nem mv.rejectedCodes — fica só como o único município que de fato\n  // encosta no fechamento, sem a expansão de território, e o resto dele sobra como buraco no mapa\n  // (nem colorido, nem cinza, nem contexto — simplesmente ausente). mv.allowedRaw carrega TODO\n  // candidato válido já descoberto, não só o top-6, então dá pra expandir os que sobraram também.\n  const plainCandidates=new Set();\n  units.forEach(u=>Object.keys(mv.allowedRaw[u]||{}).forEach(dc=>plainCandidates.add(dc)));\n  plainCandidates.forEach(destCode=>{\n    if(mv.dest.includes(destCode)) return;\n    territoryOf(destCode).forEach(n=>{\n      if(closingSet.has(n)) return;\n      if(nbrNames.has(n)) return;\n      nbrNames.add(n);\n      mv.nbrCurrentSector[n]=(level==='setor'&&world.muniOverride&&world.muniOverride[n])?world.muniOverride[n]:destCode;\n    });\n  });\n  mv.nbrFeat=[...nbrNames].map(n=>adj.feat[n]&&adj.feat[n].f).filter(Boolean);\n  units.forEach(u=>{ Object.keys(mv.allowed[u]||{}).forEach(destCode=>{ if(!mv.dest.includes(destCode)) delete mv.allowed[u][destCode]; }); });\n  mv.color={}; mv.dest.forEach((destCode,i)=>mv.color[destCode]=PALETTE[i%PALETTE.length]);\n\n  const accC={}; Object.entries(mv.nbrCurrentSector).forEach(([nb,destCode])=>{ if(!mv.dest.includes(destCode))return; const f=(adj.feat[nb]&&adj.feat[nb].f)||world.muniGeo[nb]; if(!f)return; const c=centroidLL(f); accC[destCode]=accC[destCode]||[0,0,0]; accC[destCode][0]+=c[0]; accC[destCode][1]+=c[1]; accC[destCode][2]++; });\n  mv.secCentroid={}; mv.dest.forEach(destCode=>{ if(accC[destCode]) mv.secCentroid[destCode]=[accC[destCode][0]/accC[destCode][2],accC[destCode][1]/accC[destCode][2]]; });\n\n  if(level==='setor'){\n    mv.muniCentroid={}; closingNames.forEach(n=>{ if(mv.geoFeat[n]) mv.muniCentroid[n]=centroidLL(mv.geoFeat[n]); });\n    mv.closeAdj={}; closingNames.forEach(n=>{ mv.closeAdj[n]=adj.feat[n]?adj.nbrs(n).filter(({o})=>closingSet.has(o)).map(({o})=>o):[]; });\n  } else {\n    mv.muniCentroid={};\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const cs=ms.map(n=>mv.geoFeat[n]&&centroidLL(mv.geoFeat[n])).filter(Boolean);\n      if(cs.length) mv.muniCentroid[u]=[cs.reduce((a,c)=>a+c[0],0)/cs.length,cs.reduce((a,c)=>a+c[1],0)/cs.length];\n    });\n    mv.closeAdj={};\n    const unitSet=new Set(units);\n    Object.entries(mv.unitMunis).forEach(([u,ms])=>{\n      const touch=new Set();\n      ms.forEach(n=>{ if(!adj.feat[n]) return; adj.nbrs(n).forEach(({o})=>{ const ou=mv.unitOfMuni[o]; if(ou&&ou!==u&&unitSet.has(ou)) touch.add(ou); }); });\n      mv.closeAdj[u]=[...touch];\n    });\n  }\n  const comp={}; let ci=0; units.forEach(n=>{ if(comp[n]!=null)return; const st=[n]; comp[n]=ci; while(st.length){ const x=st.pop(); (mv.closeAdj[x]||[]).forEach(o=>{ if(comp[o]==null){ comp[o]=ci; st.push(o); } }); } ci++; });\n  const compAllowed={}; units.forEach(n=>{ const cid=comp[n]; (compAllowed[cid]=compAllowed[cid]||new Set()); Object.keys(mv.allowed[n]||{}).forEach(s=>compAllowed[cid].add(s)); });\n  mv.reachable={}; units.forEach(n=>{ mv.reachable[n]=[...(compAllowed[comp[n]]||[])].filter(s=>mv.dest.includes(s)); });\n\n  if(mv.dest.length){\n    const destSectorMap={}; mv.dest.forEach(destCode=>{ destSectorMap[destCode]=memberSectorsOf(level,destCode,world); });\n    const allDestSecs=[...new Set(Object.values(destSectorMap).flat())];\n    const rList=allDestSecs.join(',');\n    const bSQL=`SELECT CD_SETOR setor, COUNT(DISTINCT CD_CONSULTORA) cbs,\n       SUM(CASE WHEN ST_FEZ_PED_CICLO_ATUAL THEN 1 ELSE 0 END) ativas,\n       ROUND(SUM(VL_PEDIDOS_CICLO),0) receita, ROUND(SUM(VL_PEDIDOS_NATURA_CICLO),0) rec_nat,\n       ROUND(SUM(VL_PEDIDOS_AVON_CICLO),0) rec_avon, ROUND(SUM(QT_PONTOS_CICLO),0) pontos, ROUND(SUM(QT_PEDIDOS_CICLO),0) pedidos\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR`;\n    const blSQL=`SELECT CD_SETOR setor, DS_NVL_PLAN_CRESC nivel, COUNT(DISTINCT CD_CONSULTORA) cbs\n      FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR IN (${rList}) AND DS_NVL_PLAN_CRESC IS NOT NULL AND DT_HISTORICO=${SNAP} GROUP BY CD_SETOR, DS_NVL_PLAN_CRESC`;\n    const [bR,blR]=await Promise.all([q(bSQL),q(blSQL)]);\n    const secMetrics={}; rows(bR).forEach(r=>{ secMetrics[String(r.setor)]=r; if(!SESS.sectorBaseCache[String(r.setor)]) SESS.sectorBaseCache[String(r.setor)]=r; });\n    const secLevel={}; rows(blR).forEach(r=>{ const s=String(r.setor); (secLevel[s]=secLevel[s]||{})[r.nivel]=+r.cbs; });\n    mv.baseMetrics={}; mv.baseLevel={};\n    mv.dest.forEach(destCode=>{\n      const secs=destSectorMap[destCode];\n      const acc=blankM(); acc.setor=destCode;\n      secs.forEach(s=>{ const r=secMetrics[String(s)]; if(!r) return; Object.keys(blankM()).forEach(k=>{ acc[k]=(+acc[k]||0)+(+r[k]||0); }); });\n      mv.baseMetrics[destCode]=acc;\n      const lv={}; secs.forEach(s=>{ const l=secLevel[String(s)]; if(!l) return; Object.entries(l).forEach(([nv,c])=>{ lv[nv]=(lv[nv]||0)+c; }); });\n      mv.baseLevel[destCode]=lv;\n    });\n  } else { mv.baseMetrics={}; mv.baseLevel={}; }\n\n  mv.dest.forEach(destCode=>{\n    if(!mv.baseMetrics[destCode]) mv.baseMetrics[destCode]={setor:destCode,cbs:0,ativas:0,receita:0,rec_nat:0,rec_avon:0,pontos:0,pedidos:0};\n    const inflowSrc=level==='setor'?world.sectorInflow:(level==='gv'?world.gvInflow:world.mercadoInflow);\n    const infl=inflowSrc&&inflowSrc[destCode];\n    if(infl) Object.keys(blankM()).forEach(k=>{ mv.baseMetrics[destCode][k]=(+mv.baseMetrics[destCode][k]||0)+(infl[k]||0); });\n    if(level==='setor'){\n      const inflLv=world.sectorInflowLevel&&world.sectorInflowLevel[destCode];\n      if(inflLv){ mv.baseLevel[destCode]=mv.baseLevel[destCode]||{}; Object.entries(inflLv).forEach(([nv,c])=>{ mv.baseLevel[destCode][nv]=(mv.baseLevel[destCode][nv]||0)+c; }); }\n    }\n  });\n\n  aggregateMovement(mv);\n  mv.assign=optimizeFor(mv);\n  return mv;\n}\n\n// ---------- cascade engine ----------\nasync function computeAt(i){\n  await ensurePinned();\n  const world=foldWorldState(SESS.timeline,i);\n  const sec=SESS.timeline[i].sector;\n  const level=SESS.timeline[i].level||'setor';\n  const mv=await computeMovement(sec,level,world);\n  SESS.timeline[i]=mv;\n}\nasync function recomputeFrom(i){\n  for(let k=i;k<SESS.timeline.length;k++){\n    loader(true,`recalculando etapa ${k+1} de ${SESS.timeline.length}…`);\n    try{ await computeAt(k); }\n    catch(e){\n      // A stage that failed to recompute still holds its PREVIOUS result, so the timeline would\n      // silently mix pre- and post-change etapas. Flag this stage and everything after it as stale\n      // (dashed chip + ⟳) so the state is visible and flushDirty retries from here.\n      SESS.dirtyFrom = SESS.dirtyFrom==null ? k : Math.min(SESS.dirtyFrom,k);\n      throw e;\n    }\n  }\n}\nasync function flushDirty(){\n  if(SESS.dirtyFrom==null) return;\n  const from=SESS.dirtyFrom; SESS.dirtyFrom=null;\n  loader(true,'recalculando etapas seguintes…');\n  try{ await recomputeFrom(from); }\n  catch(e){ $('#ds').textContent='erro ao recalcular cadeia: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n}\nasync function addMovement(level,sec){\n  level=level||'setor';\n  sec=(sec==null?'':String(sec)).trim();\n  if(!sec) return;\n  if(!movementAvailable(level)){ $('#ds').textContent=`Fechar ${LEVELS[level].label} ainda não está disponível.`; $('#ds').classList.add('err'); return; }\n  if(!hierarchyAllows(level)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; $('#ds').textContent=`esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`; $('#ds').classList.add('err'); return; }\n  const closedNow=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===level).map(mv=>mv.sector));\n  if(closedNow.has(sec)){ $('#ds').textContent=LEVELS[level].label+' '+sec+' já fechado(a) nesta timeline.'; $('#ds').classList.add('err'); return; }\n  if(level==='setor' && !SESS.sectorsList.some(s=>s.setor===sec)){ $('#ds').textContent='setor \"'+sec+'\" não existe no snapshot congelado.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  loader(true,`fechando ${lc(LEVELS[level].label)} `+sec+' — puxando dado do snapshot congelado…');\n  $('#liveState').classList.remove('err');\n  try{\n    SESS.timeline.push({sector:String(sec),level:level});\n    await computeAt(SESS.timeline.length-1);\n    SESS.selected=SESS.timeline.length-1;\n    $('#ds').classList.remove('err'); $('#ds').textContent='';\n  }catch(e){\n    SESS.timeline.pop();\n    $('#ds').textContent=`erro ao fechar ${lc(LEVELS[level].label)} `+sec+': '+e.message; $('#ds').classList.add('err');\n  } finally { loader(false); renderAll(); }\n}\nfunction removeFrom(i){\n  SESS.timeline.length=i;\n  SESS.selected=Math.min(SESS.selected,Math.max(0,SESS.timeline.length-1));\n  if(SESS.dirtyFrom!=null && SESS.dirtyFrom>=SESS.timeline.length) SESS.dirtyFrom=null;\n  renderAll();\n}\nasync function editSelected(mutator){\n  const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  mutator(mv);\n  if(SESS.selected<SESS.timeline.length-1){\n    SESS.dirtyFrom = SESS.dirtyFrom==null ? SESS.selected+1 : Math.min(SESS.dirtyFrom, SESS.selected+1);\n  }\n  renderAll();\n}\n\n// ---------- sectors list / picker ----------\nasync function loadSectors(){\n  const sql=`SELECT CD_SETOR setor, MAX(DS_ESTADO) uf, COUNT(DISTINCT CD_CONSULTORA) cbs, COUNT(DISTINCT DS_MUNICIPIO) munis\n    FROM ${T} WHERE CD_PAIS=1 AND CD_GRUPO>0 AND CD_SETOR>0 AND DT_HISTORICO=(SELECT MAX(DT_HISTORICO) FROM ${T} WHERE CD_PAIS=1) GROUP BY CD_SETOR ORDER BY CD_SETOR`;\n  SESS.sectorsList=rows(await q(sql)).map(r=>({setor:String(r.setor),uf:r.uf,cbs:+r.cbs,munis:+r.munis}));\n}\nfunction renderSectorPicker(){\n  const ready=SESS.stage==='main';\n  const ob=$('#openMove'); if(ob) ob.disabled=!ready;\n}\n\n// ---------- movement modal (setor / GV / mercado) ----------\n// Hierarquia mercado > GV > setor: a sessão trava no nível da primeira movimentação. Misturar\n// níveis quebraria a leitura de \"antes/depois\" da cadeia — fechar uma GV já redistribui setores\n// inteiros pra outras GVs; fechar um setor isolado depois não bate mais com o mundo que sobrou.\n// de/a/no: contrações de \"de\"/\"a\"/\"em\" com o artigo de cada nível — GV é feminina (a Gerência de\n// Vendas), setor e mercado são masculinos. Errar o gênero aqui é o tipo de coisa que grita \"isto foi\n// feito pra setor e só remendado pros outros níveis\", exatamente o que este objeto existe pra evitar.\nconst LEVELS={\n  setor:{label:'Setor',codeLbl:'Setor a fechar',placeholder:'código do setor',childLabel:'Município',destLabel:'Setor',de:'do',a:'ao',no:'no',parentLabel:'GV',parentPhrase:'da mesma GV',parentWord:'gerência'},\n  gv:{label:'GV',codeLbl:'GV a fechar',placeholder:'código da GV',childLabel:'Setor',destLabel:'GV',de:'da',a:'à',no:'na',parentLabel:'Mercado',parentPhrase:'do mesmo mercado',parentWord:'mercado'},\n  mercado:{label:'Mercado',codeLbl:'Mercado a fechar',placeholder:'nome do mercado',childLabel:'GV',destLabel:'Mercado',de:'do',a:'ao',no:'no',parentLabel:null,parentPhrase:null,parentWord:null},\n};\n// GV é sigla — .toLowerCase() a transformaria em \"gv\", errado em qualquer posição da frase. Palavras\n// normais (Setor/Mercado/Município) minúsculam à vontade no meio de uma frase; GV nunca.\nconst LOWER={Setor:'setor',Mercado:'mercado',Município:'município',GV:'GV'};\nconst lc=w=>LOWER[w]||w.toLowerCase();\n// Hierarquia é uma catraca, não um cadeado: mercado(0) > GV(1) > setor(2). Fechar um nível mais\n// alto (número menor) trava PRA SEMPRE nesta sessão qualquer nível mais baixo (número maior) —\n// mas o próprio nível, ou um ainda mais alto, continua liberado. É assim que \"posso fechar setores\n// até fechar uma GV; posso fechar GVs até fechar um mercado\" funciona: nunca destrava de volta.\nconst LEVEL_RANK={mercado:0,gv:1,setor:2};\nfunction sessionMinRank(){\n  let r=LEVEL_RANK.setor;\n  SESS.timeline.forEach(mv=>{ const rk=LEVEL_RANK[mv.level||'setor']; if(rk<r) r=rk; });\n  return r;\n}\nfunction hierarchyAllows(lvl){ return LEVEL_RANK[lvl]<=sessionMinRank(); }\nfunction movementAvailable(lvl){ return lvl==='setor'||lvl==='gv'||lvl==='mercado'; }\n// ---------- hierarquia: setor ⊂ GV ⊂ mercado — tudo derivado da base de dominantes (DOM) ----------\n// memberSectors: o escopo de CD_SETOR que a query no CRM precisa cobrir pra fechar este código, em\n// qualquer nível — um setor é ele mesmo; uma GV é todo setor que a base lhe atribui; um mercado é\n// todo setor de toda GV que a base atribui a ele.\n// membros ATUAIS de um GV/mercado, um nível abaixo (setores de uma GV; GVs de um mercado) — nunca\n// só a foto estática do CSV: uma etapa anterior NESTA MESMA sessão pode já ter movido um filho pra\n// dentro ou pra fora deste código, e um fechamento seguinte tem de enxergar o mundo como ele está\n// agora, não como o CSV descreveu antes de qualquer movimentação.\nfunction currentMembersOf(level,code,world){\n  code=String(code);\n  // \"gv\": setores da GV (DOM.sectorsByGv). \"mercado\": GVs do mercado (DOM.gvsByMercado) — NUNCA\n  // sectorsByMercado, que pula a camada de GV inteira e faz memberSectorsOf tratar códigos de\n  // setor como se fossem códigos de GV (não batem em DOM.sectorsByGv, e o fechamento morre com\n  // \"não existe na base\" mesmo quando o mercado está lá).\n  const staticMembers=level==='gv'?(DOM.sectorsByGv[code]||new Set()):(DOM.gvsByMercado[code]||new Set());\n  const overrideMap=level==='gv'?(world&&world.secOverride):(world&&world.gvOverride);\n  const out=new Set(staticMembers);\n  if(overrideMap){\n    Object.entries(overrideMap).forEach(([child,dest])=>{\n      if(dest===code) out.add(child);\n      else if(staticMembers.has(child)) out.delete(child);\n    });\n  }\n  return [...out];\n}\n// setores que a query no CRM precisa cobrir pra fechar este código, em qualquer nível — setor é\n// ele mesmo; GV é seus setores ATUAIS; mercado é todo setor de toda GV ATUALMENTE sua (setor não\n// troca de GV sozinho numa sessão de mercado — só GV inteira troca de mercado).\nfunction memberSectorsOf(level,code,world){\n  code=String(code);\n  if(level==='setor') return [code];\n  if(level==='gv') return currentMembersOf('gv',code,world);\n  const gvs=currentMembersOf('mercado',code,world);\n  const out=new Set();\n  gvs.forEach(gv=>(DOM.sectorsByGv[gv]||new Set()).forEach(s=>out.add(s)));\n  return [...out];\n}\n// fronteira que um destino tem de respeitar: GV do setor que fecha, mercado da GV que fecha —\n// mercado não tem nada acima, então não existe regra pra ele. Estática de propósito: um setor não\n// troca de GV nem uma GV troca de mercado só por existirem etapas anteriores nesta sessão — quem\n// muda de dono é o NÍVEL ABAIXO do que está fechando, nunca o próprio código sendo avaliado aqui.\nfunction parentOf(level,code){\n  code=String(code);\n  if(level==='setor') return DOM.gvBySetor[code]||'';\n  if(level==='gv') return DOM.mercadoByGv[code]||'';\n  return '';\n}\n// dado o setor que uma município vizinha atualmente pertence (fato ao vivo do CRM, nunca muda de\n// verdade), a que CANDIDATO A DESTINO isso corresponde neste nível — considerando que uma etapa\n// anterior desta sessão pode já ter movido esse setor (GV fechando) ou essa GV (mercado fechando)\n// pra outro dono, o que a foto estática do CSV sozinha não enxergaria.\nfunction destUnitOfSector(level,setorCode,world){\n  // setorCode is undefined when the live neighbour query found no row for that município (e.g. zero\n  // CBs in this snapshot) — String(undefined) would silently turn that into the literal text\n  // \"undefined\" and the caller would treat it as a real (bogus) sector: shown as a fake rejected\n  // sector on the map, tooltip and all. Bail out to '' — no resolvable sector — instead.\n  if(setorCode==null||setorCode==='') return '';\n  setorCode=String(setorCode);\n  if(level==='setor') return setorCode;\n  if(level==='gv'){ const ov=world&&world.secOverride&&world.secOverride[setorCode]; return ov||DOM.gvBySetor[setorCode]||''; }\n  const gv=DOM.gvBySetor[setorCode]||'';\n  const ov=world&&world.gvOverride&&world.gvOverride[gv];\n  return ov||DOM.mercadoBySetor[setorCode]||'';\n}\nfunction currentMoveLevel(){ const r=root.querySelector('input[name=\"moveLevel\"]:checked'); return (r&&r.value)||'setor'; }\nfunction openMove(){\n  const minRank=sessionMinRank();\n  const lastLevel=SESS.timeline.length?(SESS.timeline[SESS.timeline.length-1].level||'setor'):'setor';\n  root.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>{\n    const lvl=r.value;\n    r.disabled=!movementAvailable(lvl)||!hierarchyAllows(lvl);\n    r.checked=hierarchyAllows(lastLevel)?lvl===lastLevel:lvl===Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank);\n  });\n  const note=$('#moveLockNote');\n  const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===minRank)].label;\n  if(note) note.textContent=minRank<LEVEL_RANK.setor\n    ? `Esta sessão já fechou um nível mais alto na hierarquia — só \"${minLabel}\" (ou acima) continua disponível daqui pra frente.`\n    : 'Escolha o nível a fechar. Você pode fechar setores à vontade até fechar uma GV; aí só GV (ou mercado) daqui pra frente. Fechar um mercado trava a sessão em mercado.';\n  $('#moveCode').value='';\n  const err=$('#moveErr'); if(err){ err.textContent=''; err.classList.add('hidden'); }\n  const st=$('#moveStatus'); if(st) st.textContent='';\n  $('#moveBg').classList.remove('hidden');\n  renderMoveForm();\n  const c=$('#moveCancel'); if(c) c.focus();\n}\nfunction closeMove(){ const b=$('#moveBg'); if(b) b.classList.add('hidden'); }\nfunction renderMoveForm(){\n  root.querySelectorAll('#moveBg .opt-row').forEach(r=>r.classList.toggle('on',r.dataset.lvl===currentMoveLevel()));\n  const lvl=LEVELS[currentMoveLevel()];\n  $('#moveCodeLbl').textContent=lvl.codeLbl;\n  $('#moveCode').placeholder=lvl.placeholder;\n  loadMoveOptions();\n}\nasync function loadMoveOptions(){\n  const lvl=currentMoveLevel();\n  const dl=$('#moveCodeList'); if(!dl) return;\n  const closedSet=new Set(SESS.timeline.filter(mv=>(mv.level||'setor')===lvl).map(mv=>mv.sector));\n  if(lvl==='setor'){\n    dl.innerHTML=SESS.sectorsList.filter(s=>!closedSet.has(s.setor))\n      .map(s=>`<option value=\"${s.setor}\">${s.setor} · ${s.uf||'?'} · ${fmt(s.cbs)} CBs · ${s.munis} munic.</option>`).join('');\n    return;\n  }\n  dl.innerHTML='';\n  try{ await ensureDominantes(); }catch(e){ return; }\n  if(lvl==='gv'){\n    const gvs=Object.keys(DOM.sectorsByGv||{}).filter(g=>!closedSet.has(g)).sort();\n    dl.innerHTML=gvs.map(g=>`<option value=\"${g}\">${g}${DOM.gvLabel[g]?' · '+esc(DOM.gvLabel[g]):''} · ${DOM.sectorsByGv[g].size} setor(es)</option>`).join('');\n  } else if(lvl==='mercado'){\n    const mks=Object.keys(DOM.sectorsByMercado||{}).filter(m=>!closedSet.has(m)).sort();\n    dl.innerHTML=mks.map(m=>`<option value=\"${m}\">${m} · ${(DOM.gvsByMercado[m]||new Set()).size} GV(s) · ${DOM.sectorsByMercado[m].size} setor(es)</option>`).join('');\n  }\n}\nfunction moveRandPick(){\n  const opts=[...($('#moveCodeList').options||[])].map(o=>o.value);\n  if(!opts.length) return;\n  $('#moveCode').value=opts[Math.floor(Math.random()*opts.length)];\n}\nasync function submitMove(){\n  const lvl=currentMoveLevel();\n  const code=($('#moveCode').value||'').trim();\n  const err=$('#moveErr');\n  const fail=msg=>{ if(err){ err.textContent=msg; err.classList.remove('hidden'); } };\n  if(err){ err.textContent=''; err.classList.add('hidden'); }\n  if(!movementAvailable(lvl)) return fail(`Fechar ${LEVELS[lvl].label} ainda não está disponível.`);\n  if(!hierarchyAllows(lvl)){ const minLabel=LEVELS[Object.keys(LEVEL_RANK).find(k=>LEVEL_RANK[k]===sessionMinRank())].label; return fail(`Esta sessão já fechou um nível mais alto — só \"${minLabel}\" (ou acima) fica disponível.`); }\n  if(!code) return fail('Informe um código.');\n  closeMove();\n  await addMovement(lvl,code);\n}\nfunction renderStage(){\n  const isMain=SESS.stage==='main';\n  const cc=$('#confirmCard'), mw=$('#mainWrap');\n  const hdr=$('#topHeader'), rl=$('#timelineRail');\n  if(hdr) hdr.classList.toggle('hidden', !isMain);\n  if(rl) rl.classList.toggle('hidden', !isMain);\n  if(cc) cc.classList.toggle('hidden', isMain);\n  if(mw) mw.classList.toggle('hidden', !isMain || !SESS.timeline.length);\n  const cb=$('#confirmBody');\n  if(cb && !isMain) cb.innerHTML = SESS.snapshotRaw\n    ? `Snapshot congelado · atualizado em <b>${fmtSyncedAt(SESS.snapshotSyncedAt)}</b> · <b>${SESS.sectorsList.length}</b> setor(es) disponível(eis) pra fechar. Nenhuma consulta de detalhe roda até você confirmar e escolher o primeiro setor.`\n    : 'congelando snapshot…';\n}\n\n// ---------- session persistence ----------\n// ONE table (`timeline-shared-sessions`) holds BOTH kinds of row, so SESSIONS_DS and SHARED_DS\n// deliberately resolve to the SAME DataSet — the two names mark intent, not two stores:\n//   · working session  — carries `movements`; saveSession updates it in place\n//   · shared snapshot  — carries `sharedAt` + `stages`; doShare CREATES it and nothing updates it\n// listSessions MUST exclude the shared rows. They are write-once by contract, and if one showed\n// up in the picker, loading it and pressing Salvar would fire ds.update() straight at a frozen\n// snapshot. Filtering here is what keeps that unreachable — it is the guard, not a cosmetic.\n// Both lookups stay because a policy-gated DataSet's Process resolves late; same race the reader\n// component polls for. SESSIONS_DS lives at module scope so no call site can hit it in the TDZ.\nasync function listSessions(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return [];\n  await ds.refresh();\n  const recs=ds.records()||[];\n  return [...recs].filter(r=>!r.sharedAt).sort((a,b)=>new Date(b.updatedAt||0)-new Date(a.updatedAt||0));\n}\nasync function refreshSessionPicker(){\n  const list=await listSessions().catch(()=>[]);\n  SESS.sessionsList=list;\n  const opts=list.map(s=>`<option value=\"${s._id}\">${esc(s.label||'sem nome')} · ${fmt((s.movements||[]).length)} etapa(s)</option>`).join('')+`<option value=\"\">+ Nova sessão</option>`;\n  const sel=$('#sessionPick'); if(sel){ sel.innerHTML=opts; sel.value=SESS.sessionId||''; }\n  const csel=$('#confirmSessionPick'); if(csel){ csel.innerHTML=opts; csel.value=SESS.sessionId||''; }\n}\nasync function saveSession(){\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS));\n  if(!ds){ $('#sessStatus').textContent='dataset de sessões indisponível'; return; }\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra salvar ainda'; return; }\n  await flushDirty();\n  const label=SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR'));\n  const nowIso=new Date().toISOString();\n  const payload={\n    label,\n    settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n    snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt, snapLiteral:SESS.snapLiteral, eloMaxCycle:SESS.eloMaxCycle,\n    movements:SESS.timeline.map(mv=>({\n      sector:mv.sector, level:mv.level||'setor', assign:mv.assign, policy:mv.policy,\n      matrix:mv.matrix, trend:mv.trend, levels:mv.levels, levelByMuni:mv.levelByMuni,\n      pureOwnMetrics:mv.pureOwnMetrics, domUF:mv.domUF, dest:mv.dest, color:mv.color,\n      gv:mv.gv, gvRejected:mv.gvRejected,\n      missingMunis:mv.missingMunis, csvMunisTotal:mv.csvMunisTotal, ufGuessed:mv.ufGuessed,\n      baseMetrics:mv.baseMetrics, baseLevel:mv.baseLevel, geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat,\n      nbrCurrentSector:mv.nbrCurrentSector, allowed:mv.allowed, allowedRaw:mv.allowedRaw, reachable:mv.reachable,\n      optTier:mv.optTier,\n      cbsByMuni:mv.cbsByMuni, shapeMunis:mv.shapeMunis, excludedMunis:mv.excludedMunis, domCsvMunis:mv.domCsvMunis,\n      excludedCbs:mv.excludedCbs, noShapeMunis:mv.noShapeMunis, noShapeCbs:mv.noShapeCbs,\n      secCentroid:mv.secCentroid, muniCentroid:mv.muniCentroid, closeAdj:mv.closeAdj, optBadge:mv.optBadge,\n    })),\n    updatedAt:nowIso,\n  };\n  $('#sessStatus').textContent='salvando…';\n  try{\n    if(SESS.sessionId){\n      await ds.update({_id:SESS.sessionId,...payload});\n    } else {\n      payload.createdAt=nowIso;\n      const created=await ds.create(payload);\n      SESS.sessionId=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    }\n    SESS.sessionLabel=label;\n    await refreshSessionPicker();\n    $('#sessStatus').textContent='salvo ✓';\n  }catch(e){ $('#sessStatus').textContent='erro ao salvar: '+e.message; }\n  setTimeout(()=>{ if($('#sessStatus')) $('#sessStatus').textContent=''; },2200);\n}\n// Duplicate = drop sessionId so saveSession takes its CREATE branch instead of its update branch.\n// The source record is never written to, the copy gets a fresh _id and its own createdAt, and the\n// copy becomes the active session (so the next Salvar edits the copy, not the original).\nasync function renameSession(){\n  const next=prompt('Novo nome da sessão:', SESS.sessionLabel||'');\n  if(next==null) return;\n  const trimmed=next.trim(); if(!trimmed) return;\n  SESS.sessionLabel=trimmed;\n  if(!SESS.sessionId) return;   // ainda não salva — o novo nome só pega valer no próximo \"Salvar sessão\"\n  const ds=(d2.data('sessionsDataset')||d2.data(SESSIONS_DS)); if(!ds) return;\n  if($('#sessStatus')) $('#sessStatus').textContent='renomeando…';\n  try{\n    await ds.update({_id:SESS.sessionId, label:trimmed, updatedAt:new Date().toISOString()});\n    await refreshSessionPicker();\n    if($('#sessStatus')) $('#sessStatus').textContent='renomeado ✓';\n  }catch(e){ if($('#sessStatus')) $('#sessStatus').textContent='erro ao renomear: '+e.message; }\n  setTimeout(()=>{ const s=$('#sessStatus'); if(s&&(s.textContent==='renomeado ✓')) s.textContent=''; }, 2000);\n}\nasync function duplicateSession(){\n  if(!SESS.timeline.length){ $('#sessStatus').textContent='nada pra duplicar ainda'; return; }\n  const prevId=SESS.sessionId, prevLabel=SESS.sessionLabel;\n  SESS.sessionId=null;\n  SESS.sessionLabel=(SESS.sessionLabel||'Sessão')+' (cópia)';\n  await saveSession();\n  if(!SESS.sessionId){\n    // saveSession swallows its own errors into #sessStatus, so a still-null id after the await is\n    // the only failure signal we get. Put the original identity back — otherwise a failed copy\n    // silently leaves the session detached, and the next Salvar would fork a new record instead\n    // of updating the one the user thought they were editing.\n    SESS.sessionId=prevId; SESS.sessionLabel=prevLabel;\n    await refreshSessionPicker().catch(()=>{});\n    return;\n  }\n  if($('#sessStatus')) $('#sessStatus').textContent='duplicado ✓';\n}\nasync function loadSession(id){\n  if(!id) return newSession();\n  const list=(SESS.sessionsList&&SESS.sessionsList.length)?SESS.sessionsList:await listSessions().catch(()=>[]);\n  const rec=list.find(s=>String(s._id)===String(id));\n  if(!rec) return newSession();\n  loader(true,'carregando sessão…');\n  SESS.sessionId=rec._id; SESS.sessionLabel=rec.label;\n  SESS.snapshotRaw=rec.snapshotRaw; SESS.snapshotSyncedAt=rec.snapshotSyncedAt||null; SESS.snapLiteral=rec.snapLiteral; SESS.eloMaxCycle=rec.eloMaxCycle;\n  SESS.settings={...DEFAULT_SETTINGS,...(rec.settings||{}),span:migrateSpanSettings(rec.settings)};\n  SESS.timeline=(rec.movements||[]).map(m=>({sector:String(m.sector),level:m.level||'setor'}));\n  SESS.selected=0; SESS.sectorBaseCache={}; SESS.stage='main'; SESS.dirtyFrom=null;\n  try{\n    for(let i=0;i<SESS.timeline.length;i++){\n      const saved=(rec.movements||[])[i]||{};\n      // Sessions saved before the UF became part of the key hold bare-name keys in matrix rows,\n      // assign, geoFeat and closeAdj alike. Restoring that cache would mix key shapes the moment the\n      // stage is recomputed — and it would preserve the very collision this fixes — so those stages\n      // are replayed against the pinned snapshot and the manual assignment is remapped by name.\n      const legacyKeys=!!(saved.matrix&&saved.matrix.length&&saved.matrix.every(r=>r&&r.uf==null));\n      // Sessões salvas antes da regra de gerência podem carregar um destino de outra GV no cache.\n      // Restaurar esse cache manteria o setor proibido na tela para sempre, então essas etapas são\n      // reprocessadas contra o snapshot fixado — igual ao tratamento das chaves legadas.\n      const crossGv=await savedCrossesGv(saved);\n      if(saved.matrix&&!legacyKeys&&!crossGv){\n        loader(true,`restaurando etapa ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector} (cache, sem consulta)…`);\n        const mv={sector:String(saved.sector), level:saved.level||'setor', policy:saved.policy||'fora', view:'tobe', selMuni:null, optBadge:saved.optBadge||''};\n        mv.matrix=saved.matrix; mv.trend=saved.trend||[]; mv.levels=saved.levels||[]; mv.levelByMuni=saved.levelByMuni||{};\n        mv.pureOwnMetrics=saved.pureOwnMetrics||blankM(); mv.domUF=saved.domUF; mv.dest=saved.dest||[]; mv.color=saved.color||{};\n        mv.baseMetrics=saved.baseMetrics||{}; mv.baseLevel=saved.baseLevel||{}; mv.geoFeat=saved.geoFeat||{}; mv.nbrFeat=saved.nbrFeat||[];\n        mv.nbrCurrentSector=saved.nbrCurrentSector||{}; mv.allowed=saved.allowed||{}; mv.reachable=saved.reachable||{};\n        mv.allowedRaw=saved.allowedRaw||saved.allowed||{};\n        mv.optTier=(saved.optTier==null?0:saved.optTier);\n        mv.gv=saved.gv||''; mv.gvRejected=saved.gvRejected||[];\n        mv.missingMunis=saved.missingMunis||[]; mv.csvMunisTotal=saved.csvMunisTotal||0;\n        mv.ufGuessed=saved.ufGuessed||{};\n        mv.cbsByMuni=saved.cbsByMuni||{}; mv.shapeMunis=saved.shapeMunis||[];\n        mv.excludedMunis=saved.excludedMunis||[]; mv.excludedCbs=saved.excludedCbs||0; mv.domCsvMunis=saved.domCsvMunis||0;\n        mv.noShapeMunis=saved.noShapeMunis||[]; mv.noShapeCbs=saved.noShapeCbs||0;\n        mv.secCentroid=saved.secCentroid||{}; mv.muniCentroid=saved.muniCentroid||{}; mv.closeAdj=saved.closeAdj||{};\n        aggregateMovement(mv);\n        mv.assign=saved.assign||{};\n        if((mv.level||'setor')==='setor') mv.dest.forEach(s=>{ if(!SESS.sectorBaseCache[s]) SESS.sectorBaseCache[s]=mv.baseMetrics[s]; });\n        SESS.timeline[i]=mv;\n      } else {\n        loader(true,`replay ${i+1}/${SESS.timeline.length} — setor ${SESS.timeline[i].sector}…`);\n        await computeAt(i);\n        if(saved.assign) SESS.timeline[i].assign=legacyKeys?remapAssign(SESS.timeline[i],saved.assign):saved.assign;\n        if(saved.policy) SESS.timeline[i].policy=saved.policy;\n      }\n    }\n    await loadSectors();\n  }catch(e){ $('#ds').textContent='erro ao carregar sessão: '+e.message; $('#ds').classList.add('err'); }\n  finally{ loader(false); }\n  renderAll();\n}\n// Re-points a legacy bare-name assignment at the NAME/UF key it can only mean. When a name resolves\n// to more than one município in the recomputed stage the manual choice is genuinely ambiguous —\n// dropping it lets the optimizer allocate instead of guessing on the operator's behalf.\n// True when a cached stage names a destination outside the closing sector's gerência de vendas.\n// Judges the cache by TODAY's rule, reading the (memoised) dominantes base rather than trusting what\n// the session was saved with. Unknown GV on the closing sector answers false: the stage then loads\n// from cache instead of being replayed into the hard throw in computeMovement, which would leave the\n// operator staring at a broken session with no way back.\nasync function savedCrossesGv(saved){\n  if(!saved||!saved.sector||!(saved.dest||[]).length) return false;\n  let gvBy={};\n  try{ await ensureDominantes(); gvBy=DOM.gvBySetor||{}; }catch(e){ return false; }\n  const own=gvBy[String(saved.sector)]||'';\n  if(!own) return false;\n  if(saved.dest.some(s=>(gvBy[String(s)]||'')!==own)) return true;\n  // A stage saved before foreign-GV neighbours stopped being drawn can hold a perfectly clean dest and\n  // STILL carry their shapes in nbrFeat/nbrCurrentSector — it would repaint the old map straight from\n  // cache. An unknown gerência is not evidence of a foreign one, so only a known, differing GV replays.\n  return Object.values(saved.nbrCurrentSector||{}).some(s=>{ const g=gvBy[String(s)]||''; return g&&g!==own; });\n}\nfunction remapAssign(mv,savedAssign){\n  const keys=Object.keys(mv.munis||{}).filter(k=>k!=='__OUTROS__');\n  const out={};\n  Object.entries(savedAssign||{}).forEach(([k,v])=>{\n    if(keys.indexOf(k)>=0){ out[k]=v; return; }\n    const cand=keys.filter(x=>mName(x)===mName(k));\n    if(cand.length===1) out[cand[0]]=v;\n  });\n  return out;\n}\nfunction newSession(){\n  SESS.sessionId=null; SESS.sessionLabel=null; SESS.timeline=[]; SESS.selected=0; SESS.sectorBaseCache={};\n  SESS.settings={...DEFAULT_SETTINGS};\n  SESS.snapshotRaw=null; SESS.snapshotSyncedAt=null; SESS.snapLiteral=null; SESS.eloMaxCycle=null; SESS.stage='loading'; SESS.dirtyFrom=null;\n  (async()=>{\n    loader(true,'iniciando nova sessão…');\n    try{ await Promise.all([ensurePinned(), loadSectors()]); SESS.stage='confirm'; }\n    catch(e){ setError('erro init: '+e.message); }\n    finally{ loader(false); renderAll(); }\n  })();\n}\n\n// ---------- snapshot refresh ----------\n// Deliberately breaks the frozen-snapshot guarantee, on demand: re-resolves DT_HISTORICO and\n// replays the whole timeline against the new one. Skips the replay when the snapshot has not\n// actually moved, so a stray click on a long timeline costs one cheap probe instead of ~8 queries\n// per etapa.\nasync function refreshSnapshot(){\n  if(SESS.stage!=='main' || SESS.snapBusy) return;\n  SESS.snapBusy=true;\n  const prevRaw=SESS.snapshotRaw;\n  const pill=$('#liveState'); if(pill){ pill.classList.add('busy'); pill.textContent='◌ atualizando snapshot…'; }\n  loader(true,'buscando o snapshot mais recente…');\n  let ok=false;\n  try{\n    const p=await resolveSnapshotPin();\n    if(!p.snapshotRaw) throw new Error('não foi possível resolver DT_HISTORICO');\n    const same=String(p.snapshotRaw)===String(prevRaw);\n    SESS.snapshotRaw=p.snapshotRaw; SESS.snapshotSyncedAt=p.syncedAt; SESS.snapLiteral=p.snapLiteral; SESS.eloMaxCycle=p.eloMaxCycle;\n    await loadSectors();   // the sector list follows the live max, so it can have moved too\n    if(same){\n      $('#ds').textContent=`snapshot já era o mais recente · atualizado em ${fmtSyncedAt(p.syncedAt)} — nada recalculado`;\n    } else if(SESS.timeline.length){\n      SESS.dirtyFrom=null;\n      SESS.sectorBaseCache={};   // baselines come from the snapshot, so every one of them changed\n      loader(true,'snapshot novo — recalculando a timeline…');\n      await recomputeFrom(0);\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)} · ${fmt(SESS.timeline.length)} etapa(s) recalculada(s)`;\n    } else {\n      $('#ds').textContent=`snapshot atualizado em ${fmtSyncedAt(p.syncedAt)}`;\n    }\n    $('#ds').classList.remove('err');\n    ok=true;\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?null:SESS.dirtyFrom+1);\n    $('#ds').textContent='erro ao atualizar o snapshot'+(at?` na etapa ${at} — etapa ${at} em diante ficou desatualizada (⟳)`:'')+': '+e.message;\n    $('#ds').classList.add('err');\n  }finally{\n    SESS.snapBusy=false;\n    if(pill) pill.classList.remove('busy');\n    loader(false); renderAll();\n    if(ok) setTimeout(()=>{ const d=$('#ds'); if(d&&!d.classList.contains('err')) d.textContent=''; },7000);\n  }\n}\n\n// ---------- session options ----------\nfunction readOptForm(){\n  const scopeOf=lvl=>({\n    targetSpan:numOrNull($('#optTargetSpan_'+lvl).value),\n    maxSpan:numOrNull($('#optMaxSpan_'+lvl).value),\n    minSpan:numOrNull($('#optMinSpan_'+lvl).value),\n  });\n  // Mercado span has no UI control anymore — carry the session's current value through untouched\n  // instead of reading DOM inputs that no longer exist.\n  const fc=root.querySelector('input[name=\"forceContig\"]:checked');\n  return {span:{setor:scopeOf('setor'), gv:scopeOf('gv'), mercado:mergedSpanAll().mercado},\n    forceContiguity: fc?fc.value==='1':!!(SESS.settings&&SESS.settings.forceContiguity)};\n}\nfunction spanScopeError(sc,label){\n  if(sc.minSpan&&sc.maxSpan&&sc.minSpan>sc.maxSpan) return `${label} — span mínimo (${fmt(sc.minSpan)}) não pode ser maior que o máximo (${fmt(sc.maxSpan)}).`;\n  if(sc.targetSpan&&sc.maxSpan&&sc.targetSpan>sc.maxSpan) return `${label} — span alvo (${fmt(sc.targetSpan)}) não pode passar do máximo (${fmt(sc.maxSpan)}).`;\n  if(sc.targetSpan&&sc.minSpan&&sc.targetSpan<sc.minSpan) return `${label} — span alvo (${fmt(sc.targetSpan)}) não pode ficar abaixo do mínimo (${fmt(sc.minSpan)}).`;\n  return '';\n}\nfunction spanFormError(f){\n  return spanScopeError(f.span.setor,'Setor')||spanScopeError(f.span.gv,'GV')||spanScopeError(f.span.mercado,'Mercado');\n}\nfunction renderOptPreview(){\n  const el=$('#optPreview'); if(!el) return;\n  const f=readOptForm();\n  const err=spanFormError(f), ee=$('#optSpanErr');\n  if(ee){ ee.textContent=err; ee.classList.toggle('hidden',!err); }\n  const ap=$('#optApply'); if(ap) ap.disabled=!!err;\n  const mv=SESS.timeline[SESS.selected];\n  if(!mv||!mv.munis){ el.innerHTML='Nenhuma etapa calculada ainda — as regras passam a valer no próximo fechamento.'; return; }\n  const lvl=mv.level||'setor', sc=f.span[lvl];\n  let h=`<b style=\"color:var(--ink)\">Prévia — etapa ${SESS.selected+1}, ${lc(LEVELS[lvl].label)} ${mv.sector}</b><br>`;\n  if(sc.targetSpan||sc.maxSpan||sc.minSpan){\n    const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,{target:sc.targetSpan,max:sc.maxSpan,min:sc.minSpan});\n    h+=`Span dos destinos como está hoje: ${mv.dest.map(s=>s+' <b>'+fmt(sp[s])+'</b>').join(' · ')||'–'}`;\n    if(v.over.length||v.under.length) h+=`<br><span style=\"color:var(--closing)\">Fora dos limites: ${[...v.over.map(s=>s+' ▲ acima do máx'),...v.under.map(s=>s+' ▼ abaixo do mín')].join(', ')}</span> — o modelo ÓTIMO tenta corrigir no recálculo.`;\n    else h+=` <span style=\"color:var(--ok)\">— já dentro dos limites</span>`;\n  } else {\n    h+=`Sem limite de span pra ${lc(LEVELS[lvl].label)} — ${lc(LEVELS[lvl].destLabel)} destino pode ficar de qualquer tamanho.`;\n  }\n  if(SESS.timeline.length) h+=`<br><span style=\"margin-top:4px;display:inline-block\">Aplicar <b>recalcula as ${fmt(SESS.timeline.length)} etapa(s)</b> contra o snapshot congelado — os destinos ajustados à mão serão refeitos pelo modelo ÓTIMO.</span>`;\n  el.innerHTML=h;\n}\nfunction openOptions(){\n  const sp=mergedSpanAll();\n  ['setor','gv'].forEach(lvl=>{\n    $('#optTargetSpan_'+lvl).value=sp[lvl].targetSpan||'';\n    $('#optMaxSpan_'+lvl).value=sp[lvl].maxSpan||'';\n    $('#optMinSpan_'+lvl).value=sp[lvl].minSpan||'';\n  });\n  const fcVal=(SESS.settings&&SESS.settings.forceContiguity)?'1':'0';\n  const fcEl=root.querySelector('input[name=\"forceContig\"][value=\"'+fcVal+'\"]');\n  if(fcEl) fcEl.checked=true;\n  $('#optApply').disabled=false;\n  $('#optSessName').textContent=SESS.sessionLabel||'sessão não salva';\n  $('#optStatus').textContent='';\n  $('#optionsBg').classList.remove('hidden');\n  renderOptPreview();\n  const c=$('#optCancel'); if(c) c.focus();\n}\nfunction closeOptions(){ const b=$('#optionsBg'); if(b) b.classList.add('hidden'); }\nasync function applyOptions(){\n  const f=readOptForm();\n  if(spanFormError(f)) return;\n  const prevSp=mergedSpanAll();\n  const changed=['setor','gv','mercado'].some(lvl=>\n    numOrNull(prevSp[lvl].targetSpan)!==f.span[lvl].targetSpan\n    || numOrNull(prevSp[lvl].maxSpan)!==f.span[lvl].maxSpan\n    || numOrNull(prevSp[lvl].minSpan)!==f.span[lvl].minSpan\n  );\n  SESS.settings={...SESS.settings,span:f.span,forceContiguity:f.forceContiguity};\n  closeOptions();\n  if(!changed || !SESS.timeline.length){ renderAll(); return; }\n  SESS.dirtyFrom=null;\n  SESS.sectorBaseCache={};   // the recompute re-derives every baseline; keeping the old ones would\n                             // leave \"Impacto acumulado\" comparing against pre-change numbers\n  loader(true,'aplicando novos limites de span — recalculando timeline…');\n  try{\n    await recomputeFrom(0);\n    $('#ds').textContent=''; $('#ds').classList.remove('err');\n  }catch(e){\n    const at=(SESS.dirtyFrom==null?0:SESS.dirtyFrom)+1;\n    $('#ds').textContent=`erro ao aplicar o limite na etapa ${at}: ${e.message} — etapa ${at} em diante ficou desatualizada (⟳)`;\n    $('#ds').classList.add('err');\n  }finally{ loader(false); renderAll(); }\n}\n\nconst SHARED_DS='timeline-shared-sessionsDataSet';\nconst AUTH_REF='timelineSimAuthFinal';   // single source of truth for the auth strategy reference\nlet SHARE={busy:false,url:null,last:null};\n// Serialise a PRESENTATION model, not the raw session, so the reader stays purely presentational\n// and never re-derives spans/desloc/orphans — no second copy of this engine to keep in sync.\n// NOTE: METRICS_DEF[2] is the money marker; read it as !!md[2] (a literal dollar sign in this\n// position trips the upsert's parse check).\nfunction buildShareStages(){\n  return SESS.timeline.map((mv,i)=>{\n    const c=computeDerived(mv);\n    const incBy={};\n    mv.dest.forEach(s=>{ incBy[s]=Object.fromEntries(METRICS_DEF.map(md=>[md[0],0])); });\n    Object.entries(mv.munis).forEach(([m,o])=>{\n      if(m==='__OUTROS__') return;\n      const s=mv.assign[m]; if(!s||!incBy[s]) return;\n      METRICS_DEF.forEach(md=>{ incBy[s][md[0]]+=o[md[0]]||0; });\n    });\n    const saiTot={};\n    METRICS_DEF.forEach(md=>{\n      const k=md[0];\n      saiTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,p)=>a+((p[1][k])||0),0);\n    });\n    // orphans follow their leader's home: they enter that sector AND leave the closing one\n    c.leaders.forEach(L=>{\n      const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n      if(!om||!L.home) return;\n      METRICS_DEF.forEach(md=>{ const k=md[0];\n        if(incBy[L.home]) incBy[L.home][k]+=om[k]||0;\n        saiTot[k]+=om[k]||0; });\n    });\n    const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n    const sv=mv.dest.map(s=>c.span[s]);\n    const baseOf=(s,k)=>{ const b=mv.baseMetrics[s]||{}; return +(b[k]||0); };\n    return {\n      i:i, sector:mv.sector, level:mv.level||'setor', unitOfMuni:mv.unitOfMuni||{}, policy:mv.policy, domUF:mv.domUF,\n      optTier:(mv.optTier==null?0:mv.optTier),\n      dest:mv.dest.slice(), color:Object.assign({},mv.color), assign:Object.assign({},mv.assign),\n      kpis:{ cbsRealocadas:closingCbsOf(mv), desloc:c.desloc,\n             spread:(sv.length?Math.max.apply(null,sv)-Math.min.apply(null,sv):0),\n             municipios:ms.length, lideres:mv.leaders.length },\n      destinos:mv.dest.map(s=>({ s:s, base:baseOf(s,'cbs'), inc:c.inc[s]||0, orph:c.orph[s]||0, span:c.span[s]||0, gained:c.gained[s]||0 })),\n      municipios:ms.map(m=>(mv.level&&mv.level!=='setor')?{ nome:m, uf:'', cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }:{ nome:mName(m), uf:mUf(m), cbs:mv.munis[m].cbs, destino:mv.assign[m]||null }),\n      excluidos:(mv.excludedMunis||[]).map(x=>({ muni:x.muni, cbs:x.cbs })),\n      orphanTotal:c.orphanTotal||0, orphanStranded:c.orphanStranded||0, noShapeCbs:mv.noShapeCbs||0,\n      lideres:c.leaders.map(L=>({ lider:(L.lider||'').split(' ').slice(0,2).join(' '), grupo:L.grupo,\n        total:L.total, home:L.home, desloc:L.desloc, orphan:L.orphan||0 })),\n      metricas:METRICS_DEF.map(md=>{ const k=md[0];\n        return { key:k, label:md[1], money:!!md[2],\n          rows:mv.dest.map(s=>({ s:s, antes:baseOf(s,k), depois:baseOf(s,k)+((incBy[s]&&incBy[s][k])||0) })),\n          sai:saiTot[k]||0 }; }),\n      geoFeat:mv.geoFeat, nbrFeat:mv.nbrFeat, excludedFeat:[]\n    };\n  });\n}\nfunction buildShareSummary(stages){\n  const world=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const receivers=[];\n  SESS.timeline.forEach((mv,i)=>{\n    const own=mv.pureOwnMetrics||{};\n    receivers.push({ s:mv.sector, situacao:'fechado (etapa '+(i+1)+')', antes:+(own.cbs||0), depois:0 });\n  });\n  const seen={};\n  Object.values(world.muniOverride).forEach(s=>{\n    if(seen[s]) return; seen[s]=1;\n    const base=SESS.sectorBaseCache[s]||{}, infl=world.sectorInflow[s]||{};\n    receivers.push({ s:s, situacao:'recebeu território', antes:+(base.cbs||0), depois:(+(base.cbs||0))+(infl.cbs||0) });\n  });\n  return { stages:stages.length, sectors:SESS.timeline.map(mv=>mv.sector),\n    totalCbs:stages.reduce((a,s)=>a+(s.kpis.cbsRealocadas||0),0),\n    totalDesloc:stages.reduce((a,s)=>a+(s.kpis.desloc||0),0), receivers:receivers };\n}\nfunction renderShareBody(){\n  const el=$('#shareBody'); if(!el) return;\n  const go=$('#shareGo'), cp=$('#shareCopy'), op=$('#shareOpen'), cc=$('#shareCancel');\n  if(SHARE.url){\n    el.innerHTML=`<div class=\"opt-note\" style=\"margin-top:0\"><b style=\"color:var(--ink)\">Sessão compartilhada ✓</b><br>\n      Link de leitura desta simulação congelada. Quem abrir precisa ter login no simulador.\n      <div class=\"shareurl\">${esc(SHARE.url)}</div></div>`;\n    if(go) go.classList.add('hidden');\n    if(cp) cp.classList.remove('hidden');\n    if(op) op.classList.remove('hidden');\n    if(cc) cc.textContent='Fechar';\n    return;\n  }\n  if(go) go.classList.remove('hidden');\n  if(cp) cp.classList.add('hidden');\n  if(op) op.classList.add('hidden');\n  if(cc) cc.textContent='Cancelar';\n  const desloc=SESS.timeline.reduce((a,mv)=>a+(mv.munis?computeDerived(mv).desloc:0),0);\n  el.innerHTML=`<div class=\"opt-lbl\">Confirmar compartilhamento</div>\n    <div class=\"opt-sub\">Publica uma <b>cópia congelada</b> desta sessão, exatamente no estado atual.</div>\n    <div class=\"opt-note\" style=\"margin-top:0\">\n      · <b>${fmt(SESS.timeline.length)}</b> etapa(s) — ${esc(SESS.timeline.map(mv=>mv.sector).join(' → '))}<br>\n      · snapshot atualizado em <b>${esc(fmtSyncedAt(SESS.snapshotSyncedAt))}</b><br>\n      · base: <b>municípios dominantes (CSV)</b> · destino: <b>qualquer município</b> · span: <b>${esc(spanShortAll())}</b><br>\n      · <b>${fmt(desloc)}</b> CBs deslocadas somando a cadeia\n    </div>\n    <div class=\"opt-note\">A cópia <b>não</b> acompanha edições posteriores desta sessão — pra atualizar, compartilhe de novo e use o link novo. O link exige login no simulador; não é público.</div>\n    ${SHARE.last?`<div class=\"opt-note\"><b style=\"color:var(--ink)\">Último link gerado nesta sessão</b> — aponta pro estado de <b>antes</b> das edições feitas depois dele.<div class=\"shareurl\">${esc(SHARE.last)}</div></div>`:''}`;\n}\nasync function openShare(){\n  if(!SESS.timeline.length){ $('#ds').textContent='nada pra compartilhar — feche pelo menos um setor primeiro.'; $('#ds').classList.add('err'); return; }\n  await flushDirty();\n  SHARE.url=null;\n  const s=$('#shareStatus'); if(s){ s.textContent=''; s.classList.remove('err'); }\n  renderShareBody();\n  $('#shareBg').classList.remove('hidden');\n  const b=$('#shareCancel'); if(b) b.focus();\n}\nfunction closeShare(){ const b=$('#shareBg'); if(b) b.classList.add('hidden'); }\nasync function doShare(){\n  if(SHARE.busy) return;\n  const go=$('#shareGo'), stx=$('#shareStatus');\n  SHARE.busy=true;\n  if(go){ go.disabled=true; go.textContent='Compartilhando…'; }\n  if(stx){ stx.textContent=''; stx.classList.remove('err'); }\n  try{\n    // Resolved as a GLOBAL PROCESS, not a declared param — which only works because the block\n    // instance on the `main` artboard binds `sharedDataset: @timeline-shared-sessionsDataSet`.\n    // That binding is load-bearing: a Process is only define()d once something on the rendering\n    // tree references it, so without it this lookup returns null and sharing dies on the guard\n    // below. Do NOT remove the instance param because this file never reads d2.params.sharedDataset.\n    const ds=d2.data(SHARED_DS);\n    if(!ds) throw new Error('dataset de sessões compartilhadas indisponível');\n    await flushDirty();\n    const stages=buildShareStages();\n    const summary=buildShareSummary(stages);\n    let who='';\n    try{ const a=d2.auth(AUTH_REF); who=(a&&a.user&&(a.user.email||a.user.nome))||''; }catch(e){}\n    const created=await ds.create({\n      label:SESS.sessionLabel||('Sessão '+new Date().toLocaleString('pt-BR')),\n      snapshotRaw:SESS.snapshotRaw, snapshotSyncedAt:SESS.snapshotSyncedAt,\n      settings:{...DEFAULT_SETTINGS,...(SESS.settings||{})},\n      stages:stages, summary:summary, sharedBy:who, sharedAt:new Date().toISOString()\n    });\n    const id=(created&&created._id)||(created&&created.data&&created.data._id)||(ds.selected&&ds.selected()&&ds.selected()._id)||null;\n    if(!id) throw new Error('registro gravado, mas sem _id na resposta — não foi possível montar o link');\n    SHARE.url=window.location.origin+'/sessions/'+id;\n    SHARE.last=SHARE.url;\n    renderShareBody();\n  }catch(e){\n    if(stx){ stx.textContent='erro ao compartilhar: '+e.message; stx.classList.add('err'); }\n  }finally{\n    SHARE.busy=false;\n    if(go){ go.disabled=false; go.textContent='Compartilhar'; }\n  }\n}\n\n// ---------- rendering ----------\nfunction renderTimeline(){\n  const el=$('#movementChips'); if(!el) return;\n  if(!SESS.timeline.length){ el.innerHTML='<span class=\"empty\">Nenhuma etapa ainda — use \"Adicionar movimentação\" à direita.</span>'; return; }\n  el.innerHTML=SESS.timeline.map((mv,i)=>{\n    const stale=SESS.dirtyFrom!=null && i>=SESS.dirtyFrom;\n    return `<div class=\"tchip ${i===SESS.selected?'on':''} ${stale?'stale':''}\" data-i=\"${i}\">\n      <span class=\"tnum\">${i+1}</span><span class=\"tsec\">${LEVELS[mv.level||'setor'].label} ${mv.sector}</span>\n      ${stale?'<span class=\"tstale\" title=\"recalcula ao selecionar outra etapa\">⟳</span>':''}\n      <span class=\"trm\" data-rm=\"${i}\" title=\"remover esta etapa e as seguintes\">✕</span>\n    </div>`;\n  }).join('');\n  el.querySelectorAll('.tchip').forEach(ch=>{\n    ch.addEventListener('click',async (e)=>{\n      if(e.target.dataset.rm!=null && e.target.dataset.rm!==''){ removeFrom(+e.target.dataset.rm); return; }\n      const i=+ch.dataset.i;\n      if(i!==SESS.selected) await flushDirty();\n      SESS.selected=i; renderAll();\n    });\n  });\n}\nfunction renderRuleNote(){\n  const el=$('#ruleNote'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected];\n  const lvl=LEVELS[(mv&&mv.level)||'setor'];\n  let h='';\n  if(!mv || (mv.level||'setor')==='setor'){\n    h+=`<b style=\"color:var(--ink)\">Base de municípios dominantes:</b> só os municípios que a base (CSV) atribui ao setor entram no mapa e na alocação`;\n    if(mv&&mv.domCsvMunis) h+=` · <b>${fmt(mv.domCsvMunis)}</b> na base do setor ${mv.sector}`;\n    if(mv&&mv.excludedCbs) h+=` · <b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs)} CBs</b> fora da base nesta etapa — não aparecem no mapa e as CBs seguem suas líderes (quebra de vínculo)`;\n  } else {\n    // GV/mercado não têm spillover (todo setor pertence a exatamente uma GV/mercado na base), então\n    // não existe \"excluído\" pra reportar aqui — só o tamanho da base e em quantas unidades ela se agrupa.\n    h+=`<b style=\"color:var(--ink)\">Base de ${lc(lvl.childLabel)}s dominantes:</b> ${mv.domCsvMunis!=null?`<b>${fmt(mv.domCsvMunis)}</b> município(s) `:''}na base ${lvl.de} ${lc(lvl.label)} ${mv.sector}, agrupados em <b>${fmt(Object.keys(mv.unitMunis||{}).length)}</b> ${lc(lvl.childLabel)}(s)`;\n  }\n  if(DOM.error) h+=` · <b style=\"color:var(--closing)\">⚠ base indisponível: ${esc(DOM.error)}</b>`;\n  el.innerHTML=h;\n}\nfunction renderSpanRule(){\n  const el=$('#spanRule'); if(!el) return;\n  const mv=SESS.timeline[SESS.selected], lim=spanLimits(mv);\n  if(!mv||!mv.munis||mv.view==='asis'||!spanLimitsOn(lim)){ el.innerHTML=''; return; }\n  const sp=spanMapFor(mv,mv.assign||{}), v=spanViolationsOf(sp,lim);\n  const bits=[];\n  if(lim.target) bits.push(`alvo <b>${fmt(lim.target)}</b>`);\n  if(lim.max) bits.push(`máx <b>${fmt(lim.max)}</b>`);\n  if(lim.min) bits.push(`mín <b>${fmt(lim.min)}</b>`);\n  let h=`Pré-requisito de span: ${bits.join(' · ')} CBs — `;\n  if(v.over.length||v.under.length){\n    h+=`<b style=\"color:var(--closing)\">⚠ `;\n    if(v.over.length) h+=`acima do máximo: ${v.over.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    if(v.over.length&&v.under.length) h+=' · ';\n    if(v.under.length) h+=`abaixo do mínimo: ${v.under.map(s=>s+' ('+fmt(sp[s])+')').join(', ')}`;\n    h+=`</b>`;\n    if(mv.optTier===1) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita o span mínimo nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===2) h+=`<br><span style=\"opacity:.85\">Nenhuma alocação conectada respeita os limites nesta etapa — o modelo relaxou essa exigência.</span>`;\n    else if(mv.optTier===-1) h+=`<br><span style=\"opacity:.85\">Etapa resolvida por heurística — os limites não são garantidos.</span>`;\n  } else h+=`<b style=\"color:var(--ok)\">✓ todos os setores receptores dentro dos limites</b>`;\n  el.innerHTML=`<div class=\"note\" style=\"margin:-2px 0 6px\">${h}</div>`;\n}\nfunction renderContig(){const mv=SESS.timeline[SESS.selected];const el=$('#contig');if(!el||!mv)return;\n  if(mv.view==='asis'){el.innerHTML='';return;}\n  const lvl=LEVELS[mv.level||'setor'];\n  // Indicador de conectividade geográfica só se aplica a fechamento de setor — ver contigBadFor.\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=''; return; }\n  const bad=contigBadFor(mv,mv.assign);\n  el.innerHTML=bad.length\n    ?`<div class=\"note\" style=\"color:var(--closing);font-weight:600;margin:-2px 0 6px\">⚠ Território desconectado em ${bad.join(', ')} — ${lc(lvl.childLabel)}(s) desconectado(s) do território ${lvl.de} ${lc(lvl.label)}. Use ★ ÓTIMO ou reassocie no mapa.</div>`\n    :`<div class=\"note\" style=\"color:var(--ok);margin:-2px 0 6px\">✓ Sem quebras — cada ${lc(lvl.label)} forma uma região conectada.</div>`;\n}\nfunction closingCbsOf(mv){ return Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+o.cbs,0); }\nfunction sectorTotalCbs(mv){ return Object.values(mv.munis).reduce((a,o)=>a+o.cbs,0); }\nfunction renderKpis(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  const childPlural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  if(mv.view==='asis'){const mc=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').length;\n    $('#kpis').innerHTML=[\n      {n:fmt(sectorTotalCbs(mv)),l:`CBs ${lvl.no} ${lc(lvl.label)} ${mv.sector} (antes desta etapa)`,t:`Total de CBs ${lvl.no} ${lc(lvl.label)} hoje — já considerando o que etapas anteriores da timeline mandaram pra cá.`},\n      {n:mv.csvMunisTotal?`${fmt(mc)}/${fmt(mv.csvMunisTotal)}`:fmt(mc),l:`${childPlural} ${lvl.de} ${lc(lvl.label)}`,t:`${lvl.childLabel}(s) desenhados nesta etapa${mv.csvMunisTotal?` de ${fmt(mv.csvMunisTotal)} que a base de dominantes atribui ${lvl.a} ${lc(lvl.label)}`:''}.${(mv.missingMunis||[]).length?' Fora do mapa: '+mv.missingMunis.map(x=>x.muni+' ('+x.reason+')').join(' · '):''}${Object.keys(mv.ufGuessed||{}).length?' UF inferida da UF dominante do setor (nome repetido em outra UF carregada): '+Object.entries(mv.ufGuessed).map(([nm,u])=>nm+'→'+u).join(', '):''}`},\n      {n:fmt(mv.leaders.length),l:'Líderes (grupos)',t:`Líderes presentes ${lvl.no} ${lc(lvl.label)} nesta etapa.`},\n      {n:fmt(mv.dest.length),l:`${lvl.destLabel}s adjacentes`,t:`${lvl.destLabel}s vizinh${lvl.destLabel==='GV'?'as':'os'} (ainda abertas)${lvl.parentPhrase?` ${lvl.parentPhrase}`:''}${mv.gv?` (${mv.gv})`:''} que poderiam receber os ${lc(lvl.childLabel)}s.${(mv.gvRejected||[]).length&&lvl.parentWord?' Fora do mapa, por serem de outr'+(lvl.parentWord==='gerência'?'a':'o')+' '+lvl.parentWord+': '+mv.gvRejected.join(', ')+'.':''}`},\n    ].map(k=>`<div class=\"kpi\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n    return;}\n  const sv=mv.dest.map(s=>c.span[s]);const spread=sv.length?Math.max(...sv)-Math.min(...sv):0;\n  const zero=mv.dest.filter(s=>c.gained[s]===0);const totCbs=closingCbsOf(mv);\n  $('#kpis').innerHTML=[\n    // \"CBs fora do X\" só faz sentido pra fechamento de setor — a mecânica de vínculo (CBs que\n    // seguem a líder pra fora do território) é uma coisa de setor/orphan, não de GV/mercado.\n    (mv.level||'setor')==='setor'?{n:fmt(c.desloc),l:mv.policy==='fora'?`CBs fora ${lvl.de} ${lc(lvl.destLabel)}`:'CBs c/ quebra de relação',cls:'warn',t:`CBs cuja líder acabou noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} nesta etapa.`}:null,\n    {n:fmt(spread),l:'Desequilíbrio (máx−mín CBs)',cls:spread>800?'warn':'ok',t:`Diferença de tamanho entre ${lvl.destLabel==='GV'?'a maior e a menor':'o maior e o menor'} ${lc(lvl.destLabel)} depois desta etapa.`},\n    {n:fmt(totCbs),l:'CBs realocadas',cls:'',t:`Total de CBs ${lvl.de} ${lc(lvl.label)} ${mv.sector} sendo redistribuídas nesta etapa.`},\n    (mv.level||'setor')==='setor'?{n:zero.length?zero.join(', '):'nenhum',l:`${lvl.destLabel} sem líder herdada`,cls:zero.length?'warn':'ok',t:`${lvl.destLabel} que recebe ${lc(lvl.childLabel)}(s) mas nenhuma líder nesta etapa.`}:null,\n  ].filter(Boolean).concat(mv.excludedCbs?[{n:fmt(mv.excludedCbs),l:`CBs fora do setor · ${fmt(mv.excludedMunis.length)} município(s) fora da base`,cls:'warn',t:'CBs em municípios que a base de dominantes não atribui a este setor: ficam fora do mapa e da alocação geográfica e seguem a líder do seu grupo.'}]:[])\n   .concat((mv.missingMunis||[]).length?[{n:fmt(mv.missingMunis.length),l:`Município(s) da base fora do mapa · ${fmt(mv.csvMunisTotal)} na base`,cls:'warn',t:`A base de dominantes atribui ${fmt(mv.csvMunisTotal)} município(s) ao setor ${mv.sector} e ${fmt(mv.csvMunisTotal-mv.missingMunis.length)} chegaram ao mapa. Fora: ${mv.missingMunis.map(x=>x.muni+' — '+x.reason+(x.cbs?' ('+fmt(x.cbs)+' CBs)':'')).join(' · ')}`}]:[])\n   .map(k=>`<div class=\"kpi ${k.cls}\" title=\"${k.t}\"><div class=\"n\">${k.n}</div><div class=\"l\">${k.l}</div></div>`).join('');\n}\nfunction renderSpans(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  if(mv.view==='asis'){const items=[{s:mv.sector,val:sectorTotalCbs(mv),color:CLOSE_COLOR}].concat(mv.dest.map(s=>({s,val:+(mv.baseMetrics[s]?.cbs||0),color:mv.color[s]})));\n    const max=Math.max(...items.map(x=>x.val),1);\n    $('#spans').innerHTML=items.map(x=>`<div class=\"bar-row\"><div class=\"tag\" style=\"color:${x.color}\">${x.s}</div><div class=\"bar-track\">\n      <div class=\"bar-inc\" style=\"width:${x.val/max*100}%;background:${x.color}\"></div></div>\n      <div class=\"bar-val\">${fmt(x.val)}</div></div>`).join('')\n      +`<div class=\"note\">Estrutura antes desta etapa — ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b> ainda existe. Alterne pra <b>TOBE</b> pra ver a redistribuição.</div>`;\n    return;}\n  const lim=spanLimits(mv),on=spanLimitsOn(lim);\n  const rawMax=Math.max(...mv.dest.map(s=>c.span[s]),lim.target||0,lim.max||0,1);\n  const max=on?rawMax*1.04:rawMax;   // headroom so a marker sitting at the scale top stays visible\n  const marks='';\n  $('#spans').innerHTML=mv.dest.map(s=>{const base=+(mv.baseMetrics[s]?.cbs||0),i=c.inc[s];\n    const over=lim.max&&c.span[s]>lim.max,under=lim.min&&c.span[s]<lim.min;\n    return `<div class=\"bar-row\"><div class=\"tag\" style=\"color:${mv.color[s]}\">${s}</div><div class=\"bar-track\">\n      <div class=\"bar-base\" style=\"width:${base/max*100}%;background:${mv.color[s]}\"></div>\n      <div class=\"bar-inc\" style=\"width:${i/max*100}%;background:${mv.color[s]}\"></div>${marks}</div>\n      <div class=\"bar-val\" style=\"${over?'color:var(--closing);font-weight:700':(under?'color:#ff9500;font-weight:700':'')}\">${fmt(c.span[s])} <span style=\"opacity:.6\">(+${fmt(i)})</span>${over?' ▲':''}${under?' ▼':''}</div></div>`;}).join('')\n    +`<div class=\"note\">Claro = base (já incluindo etapas anteriores) · cheio = CBs entrantes ${lvl.de} ${mv.sector} nesta etapa.${c.orphanTotal?` Inclui <b>${fmt(c.orphanTotal-c.orphanStranded)}</b> CB(s) de municípios fora da base de dominantes que seguiram suas líderes.`:''}</div>`;\n}\n// Two very different reasons land rows in the __OUTROS__ bucket — no IBGE shape, or outside the\n// sector's dominance base. Same bucket computationally, but they must not read as the same thing.\nfunction outsideNote(mv){\n  const bits=[];\n  if(mv.noShapeCbs) bits.push(`<b>${fmt(mv.noShapeCbs)}</b> CBs em ${fmt(mv.noShapeMunis.length)} município(s) sem shape na UF`);\n  if(mv.excludedCbs) bits.push(`<b>${fmt(mv.excludedCbs)}</b> CBs em ${fmt(mv.excludedMunis.length)} município(s) <b>fora da base de dominantes</b> deste setor — <b style=\"color:var(--closing)\">quebra de vínculo</b>`);\n  if(!bits.length) return '';\n  let h=`<div class=\"note\">Fora do mapa e da alocação geográfica: ${bits.join(' · ')}.`;\n  if(mv.excludedCbs){\n    const c=computeDerived(mv);\n    h+=` As <b>${fmt(mv.excludedCbs)}</b> CBs em quebra de vínculo <b>seguem a líder do seu grupo</b>`;\n    const byS=mv.dest.filter(s=>c.orph[s]).map(s=>`<b style=\"color:${mv.color[s]}\">${s}</b> (${fmt(c.orph[s])})`);\n    h+=byS.length?`: ${byS.join(' · ')}.`:'.';\n    if(c.orphanStranded) h+=` <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} CB(s) sem destino</b> — a líder não ficou com nenhum município realocado, então não há setor pra seguir.`;\n  }\n  if(mv.noShapeCbs) h+=` As CBs sem shape na UF continuam fora de qualquer setor destino.`;\n  if(mv.excludedMunis&&mv.excludedMunis.length) h+=`<br><span style=\"opacity:.85\">${mv.excludedMunis.slice(0,8).map(x=>x.muni+' ('+fmt(x.cbs)+')').join(', ')}${mv.excludedMunis.length>8?' +'+(mv.excludedMunis.length-8):''}</span>`;\n  return h+`</div>`;\n}\nfunction renderMuni(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const lvl=LEVELS[mv.level||'setor'];\n  const plural=lvl.childLabel==='Setor'?'Setores':(lvl.childLabel==='GV'?'GVs':'Municípios');\n  const fem=lvl.childLabel==='GV';\n  const t=$('#muniListTitle'); if(t) t.textContent=`${plural} → ${lc(lvl.destLabel)} destino`;\n  const nt=$('#muniListNote'); if(nt) nt.innerHTML=`Clique no <b>código ${lvl.a} ${lc(lvl.destLabel)}</b> pra mandar ${lvl.a} ${lc(lvl.childLabel)} <b>inteir${fem?'a':'o'}</b> pra lá (regra de negócio). A cor acompanha o mapa.`;\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  if(mv.view==='asis'){$('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div><span class=\"badge\" style=\"background:${CLOSE_COLOR};color:#fff;border-color:${CLOSE_COLOR}\">${lc(lvl.label)} ${mv.sector}</span></div>`).join('')\n      +`<div class=\"note\">Hoje tod${fem?'as':'os'} pertencem ${lvl.a} ${lc(lvl.label)} <b>${mv.sector}</b>. No <b>TOBE</b> cada um vira botões de destino.</div>`+outsideNote(mv);return;}\n  $('#muniList').innerHTML=ms.map(m=>`<div class=\"muni-item\"><div class=\"nm\">${m}</div><div class=\"ct\">${fmt(mv.munis[m].cbs)} CBs</div>\n    <div class=\"seg\" data-m=\"${m}\">${mv.dest.map(s=>`<button data-s=\"${s}\" class=\"${mv.assign[m]===s?'on':''}\" style=\"${mv.assign[m]===s?'background:'+mv.color[s]:''}\">${s}</button>`).join('')}</div></div>`).join('')\n    +outsideNote(mv);\n  $('#muniList').querySelectorAll('.seg button').forEach(b=>b.onclick=()=>{const m=b.parentElement.dataset.m,s=b.dataset.s;editSelected(m2=>{m2.assign[m]=s;});});\n}\nfunction renderLeaders(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  if(mv.view==='asis'){$('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>Grupo</th><th>CBs</th><th>${LEVELS[mv.level||'setor'].label}</th></tr></thead><tbody>`\n    +mv.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${L.grupo}</td><td>${fmt(L.total)}</td><td><span class=\"home\" style=\"background:${CLOSE_COLOR}\"></span>${mv.sector}</td></tr>`).join('')\n    +`<tr style=\"font-weight:700\"><td>TOTAL</td><td></td><td>${fmt(mv.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td></tr></tbody>`;return;}\n  const showOrph=c.orphanTotal>0;\n  $('#leaders').innerHTML=`<thead><tr><th>Líder</th><th>CBs</th><th>Home</th>${mv.dest.map(s=>`<th>${s}</th>`).join('')}${showOrph?'<th title=\"CBs em municípios fora da base de dominantes que seguiram esta líder\">Fora do setor</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n   +c.leaders.map(L=>`<tr><td>${(L.lider||'').split(' ').slice(0,2).join(' ')}</td><td>${fmt(L.total)}</td>\n     <td>${L.home?`<span class=\"home\" style=\"background:${mv.color[L.home]}\"></span>${L.home}`:'–'}</td>\n     ${mv.dest.map(s=>`<td class=\"cell\" style=\"${s===L.home?'color:'+mv.color[s]+';font-weight:700':''}\">${L.byS[s]||'·'}</td>`).join('')}\n     ${showOrph?`<td class=\"cell\" style=\"${L.orphan&&!L.orphanHome?'color:var(--closing);font-weight:700':''}\" title=\"${L.orphan?(L.orphanHome?'seguem a líder para o setor '+L.orphanHome:'sem home — CBs sem setor pra seguir'):''}\">${L.orphan?fmt(L.orphan)+(L.orphanHome?'':' ⚠'):'·'}</td>`:''}\n     <td style=\"color:${L.desloc?'var(--closing)':'var(--mut)'};font-weight:${L.desloc?700:400}\">${L.desloc||'0'}</td></tr>`).join('')\n   +`<tr style=\"font-weight:700\"><td>TOTAL</td><td>${fmt(c.leaders.reduce((a,L)=>a+L.total,0))}</td><td></td>${mv.dest.map(s=>`<td>${fmt(c.leaders.reduce((a,L)=>a+L.byS[s],0))}</td>`).join('')}${showOrph?`<td>${fmt(c.orphanTotal)}</td>`:''}<td style=\"color:var(--closing)\">${fmt(c.desloc)}</td></tr></tbody>`;\n}\nfunction renderMatrix(){const mv=SESS.timeline[SESS.selected]; if(!mv) return;\n  // Esta matriz cruza LINHAS DO CRM (município x grupo) — no fechamento de GV/mercado a unidade\n  // que troca de dono é setor/GV, não município, e reconstruir a matriz nesse grão é trabalho ainda\n  // não feito; mostrar a tabela como se fosse por município aqui daria linhas vazias sem avisar.\n  if((mv.level||'setor')!=='setor'){ $('#matrix').innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; return; }\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // `_out` is a flag stamped on the ROW, and rows are shared between etapas (absorbed + inherited\n  // orphans), so the last etapa to aggregate wins and reading it here can describe the wrong etapa.\n  // Derive the bucket from THIS mv instead. Summing rather than .find() because several out-municípios\n  // of the same group all collapse into __OUTROS__, and only the first was being counted.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cell=(m,g)=>mv.matrix.reduce((a,x)=>a+((((outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))===m)&&String(x.grupo)===g)?+x.cbs:0),0);\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  // No destination column here. This is the AS-IS matrix: every município in it still belongs to the\n  // closing sector, so showing where it is HEADED mixed the two states of the world in one table and\n  // made the ASIS view read like a plan. The destination lives on the map and in \"Municípios → setor\n  // destino\", which are about the to-be.\n  $('#matrix').innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${L.lider}\">${L.grupo}</th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')+`</tbody>`;\n}\n// TOBE counterpart of the AS-IS matrix: the same município × grupo grid plus the two columns the AS-IS\n// view deliberately refuses to carry — where the município ends up, and under which líder.\n// \"Líder resultante\" is the líder holding the most CBs in that município. A município is served by several\n// grupos at once and the data names no single owner, so the dominant one is the honest answer and its\n// share is printed beside it instead of being hidden behind a single name.\nfunction renderMatrixTobe(){const mv=SESS.timeline[SESS.selected]; const el=$('#matrixTobe'); const nt=$('#tobeNote');\n  if(!el) return;\n  if(!mv||!mv.munis){ el.innerHTML=''; if(nt) nt.innerHTML=''; return; }\n  if((mv.level||'setor')!=='setor'){ el.innerHTML=`<tr><td class=\"note\" style=\"padding:10px\">Matriz por município × grupo ainda não disponível pra fechamento de ${LEVELS[mv.level].label} — use os KPIs, líderes e plano de migração acima.</td></tr>`; if(nt) nt.innerHTML=''; return; }\n  const c=computeDerived(mv);\n  const homeOf={}; c.leaders.forEach(L=>{ homeOf[L.grupo]=L.home; });\n  const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>mv.munis[b].cbs-mv.munis[a].cbs);\n  // One pass over the matrix instead of a scan per cell: the AS-IS table can afford the naive version\n  // on six municípios, but this one asks for the dominant líder too, which multiplies the lookups.\n  const outOf=x=>!mv.geoFeat[mkey(x.municipio,x.uf)];\n  const cellBy={}; mv.matrix.forEach(x=>{ const k=(outOf(x)?'__OUTROS__':mkey(x.municipio,x.uf))+'|'+String(x.grupo);\n    cellBy[k]=(cellBy[k]||0)+ +x.cbs; });\n  const cell=(m,g)=>cellBy[m+'|'+g]||0;\n  const mx=Math.max(...ms.flatMap(m=>mv.leaders.map(L=>cell(m,L.grupo))),1);\n  const domLeader=m=>{ let best=null,bc=0;\n    mv.leaders.forEach(L=>{ const v=cell(m,L.grupo); if(v>bc){ bc=v; best=L; } });\n    return best?{L:best,cbs:bc}:null; };\n  const short=L=>(L.lider||'').split(' ').slice(0,2).join(' ')||('grupo '+L.grupo);\n  // NOT named `rows` — that is the module-level Databricks response helper, and shadowing it inside a\n  // render function is a trap for whoever adds a query here later.\n  // Off-mismatch ainda calculado pra alimentar a nota abaixo da tabela — só não vira mais coluna: o\n  // destino agora é \"Setor destino\" por GRUPO no rodapé, não mais por município na grade.\n  const recs=ms.map(m=>{ const d=mv.assign[m]||null, dl=domLeader(m);\n    const home=dl?(homeOf[dl.L.grupo]||null):null;\n    return {m:m,off:!!(dl&&home&&d&&home!==d)}; });\n  el.innerHTML=`<thead><tr><th>Município</th>${mv.leaders.map(L=>`<th title=\"${esc(L.lider||'')}\">${L.grupo}<br><span style=\"font-weight:400;font-size:10px;color:var(--mut)\">${esc(short(L))}</span></th>`).join('')}<th>Tot</th></tr></thead><tbody>`\n   +ms.map(m=>`<tr><td>${m}</td>`\n     +mv.leaders.map(L=>{const v=cell(m,L.grupo);return `<td class=\"cell\" style=\"${v?`background:rgba(229,181,63,${.08+v/mx*.5})`:''}\">${v||'·'}</td>`;}).join('')\n     +`<td style=\"font-weight:700\">${fmt(mv.munis[m].cbs)}</td></tr>`).join('')\n   // Um rótulo de destino por GRUPO (coluna), não por município (linha) — cada grupo tem UM home\n   // fixo (computeDerived já calculou), então ele mora uma vez no rodapé, igual o nome já morava.\n   +`<tr class=\"lider-foot\"><td>Setor destino</td>`\n     +mv.leaders.map(L=>{const h=homeOf[L.grupo]; return `<td>${h?`<span class=\"home\" style=\"background:${mv.color[h]}\"></span>${h}`:'–'}</td>`;}).join('')\n     +`<td></td></tr>`\n   +`</tbody>`;\n  if(nt){\n    const offN=recs.filter(r=>r.off).length;\n    nt.innerHTML=(mv.policy!=='fora')\n      ? `Política <b>quebrar relação</b>: as CBs deixam a líder atual e passam à estrutura do setor destino — o rodapé mostra pra onde cada grupo vai.`\n      : `<b>Setor destino</b> = o setor que recebe a maior parte das CBs de cada líder (rodapé, uma vez por grupo). `\n        +(offN?`<span style=\"color:var(--closing)\">${fmt(offN)} município(s) ⚠</span> têm a líder dominante indo para um setor diferente do dela — as CBs seguem a líder (é disso que o KPI “CBs fora do setor” é feito).`\n          :`Em todos os municípios a líder dominante termina no mesmo setor que recebe o território.`);\n  }\n}\nfunction renderPolicy(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor'];\n  // O toggle fora-do-setor/quebra-de-relação é uma decisão de setor fechando — em GV/mercado a\n  // seção some inteira em vez de mostrar um controle que não corresponde a nada calculado.\n  const sec=$('#policySection'); if(sec) sec.classList.toggle('hidden',(mv.level||'setor')!=='setor');\n  if((mv.level||'setor')!=='setor') return;\n  $('#policyNote').innerHTML=mv.policy==='fora'\n    ?`<b style=\"color:var(--acc)\">Mantendo vínculo:</b> ${fmt(c.desloc)} CBs seguem com a líder mas ficam <b>fisicamente noutr${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)}</b>. Preserva relação; suja geografia.`\n    :`<b style=\"color:var(--acc)\">Quebrando vínculo:</b> ${fmt(c.desloc)} CBs <b>re-vinculam a uma líder d${lvl.a==='à'?'a':'o'} ${lc(lvl.destLabel)} destino</b>. Geografia limpa; rompe relação.`;\n}\nfunction renderPlan(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv); const lvl=LEVELS[mv.level||'setor']; const ms=Object.keys(mv.munis).filter(m=>m!=='__OUTROS__').sort((a,b)=>a.localeCompare(b));\n  // GV/mercado: a chave \"m\" aqui é um código de setor/GV, não uma chave município/UF — mName/mUf\n  // parseariam lixo, então o CSV de plano exporta o código cru nesses níveis.\n  mv._plan=ms.map(m=>(mv.level&&mv.level!=='setor')?{municipio:m,uf:'',cbs:mv.munis[m].cbs,destino:mv.assign[m]}:{municipio:mName(m),uf:mUf(m),cbs:mv.munis[m].cbs,destino:mv.assign[m]});\n  $('#plan').innerHTML=`Etapa ${SESS.selected+1} · ${lvl.label} <b>${mv.sector}</b> → ${ms.map(m=>`${m}→<b style=\"color:${mv.color[mv.assign[m]]}\">${mv.assign[m]||'–'}</b>`).join(' · ')}<br>\n   Política: <b>${mv.policy==='fora'?`manter vínculo (CBs fora ${lvl.de} ${lc(lvl.destLabel)})`:'quebrar relação'}</b> · ${fmt(c.desloc)} CBs impactadas nesta etapa.`\n   +(c.orphanTotal?`<br>Fora da base de dominantes: ${fmt(c.orphanTotal)} CB(s) seguem suas líderes — ${mv.dest.filter(s=>c.orph[s]).map(s=>`${s} (${fmt(c.orph[s])})`).join(' · ')||'–'}${c.orphanStranded?` · <b style=\"color:var(--closing)\">${fmt(c.orphanStranded)} sem líder com destino</b>`:''}. Não entram no CSV, que é por município.`:'');\n}\nfunction renderReport(){const mv=SESS.timeline[SESS.selected]; if(!mv) return; const c=computeDerived(mv);\n  mv.metric=mv.metric||'cbs';\n  const KEYS=METRICS_DEF.map(m=>m[0]);\n  const incBy={};mv.dest.forEach(s=>incBy[s]=Object.fromEntries(KEYS.map(k=>[k,0])));\n  Object.entries(mv.munis).forEach(([m,o])=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incBy[s])return;KEYS.forEach(k=>incBy[s][k]+=o[k]||0);});\n  // orphan CBs land on their leader's home sector, not on a geographic destination\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home&&incBy[L.home]) KEYS.forEach(k=>incBy[L.home][k]+=om[k]||0); });\n  const closeTot={};KEYS.forEach(k=>closeTot[k]=Object.entries(mv.munis).filter(([m])=>m!=='__OUTROS__').reduce((a,[,o])=>a+(o[k]||0),0));\n  // they leave the closing sector too, so antes → depois still balances\n  c.leaders.forEach(L=>{ const om=mv.orphanMetricsByGroup&&mv.orphanMetricsByGroup[L.grupo];\n    if(om&&L.home) KEYS.forEach(k=>closeTot[k]+=om[k]||0); });\n  const trend=mv.trend.slice(-6);\n  const chartHTML=mk=>{\n    const isM=['receita','rec_nat','rec_avon'].includes(mk); const fN=isM?money:fmt;\n    const data=mv.dest.map(s=>({label:s,color:mv.color[s],antes:+(mv.baseMetrics[s]?.[mk]||0),depois:+(mv.baseMetrics[s]?.[mk]||0)+(incBy[s]?.[mk]||0)}));\n    data.push({label:mv.sector+'✕',color:'#ff3b30',antes:closeTot[mk]||0,depois:0});\n    const max=Math.max(...data.flatMap(d=>[d.antes,d.depois]),1);\n    const W=720,H=290,base=238,top=42,maxH=base-top,n=data.length,gw=(W-16)/n;\n    let g='';\n    data.forEach((d,i)=>{const cx=8+gw*i+gw/2,bw=Math.min(30,gw*0.30);\n      const ah=d.antes/max*maxH,dh=d.depois/max*maxH,ax=cx-bw-2,dx=cx+2;\n      g+=`<rect x=\"${ax.toFixed(1)}\" y=\"${(base-ah).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${ah.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\" opacity=\"0.35\"/>`;\n      g+=`<rect x=\"${dx.toFixed(1)}\" y=\"${(base-dh).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${dh.toFixed(1)}\" rx=\"2.5\" fill=\"${d.color}\"/>`;\n      g+=`<text x=\"${(ax+bw/2).toFixed(1)}\" y=\"${(base-ah-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#9a9aa8\">${fN(d.antes)}</text>`;\n      g+=`<text x=\"${(dx+bw/2).toFixed(1)}\" y=\"${(base-dh-4).toFixed(1)}\" text-anchor=\"middle\" font-size=\"9.5\" font-weight=\"700\" fill=\"${d.color}\">${fN(d.depois)}</text>`;\n      const dp=d.antes?((d.depois-d.antes)/d.antes*100):(d.depois?100:0);\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"16\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"${dp>=0?'#34c759':'#ff3b30'}\">${dp>0?'+':''}${dp.toFixed(0)}%</text>`;\n      g+=`<text x=\"${cx.toFixed(1)}\" y=\"${base+16}\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" fill=\"#1c1c2b\">${d.label}</text>`;});\n    return `<svg viewBox=\"0 0 ${W} ${H}\" style=\"width:100%\"><line x1=\"8\" y1=\"${base}\" x2=\"${W-8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg>`;\n  };\n  let h=`<div style=\"display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap\">\n      <h2 style=\"margin:0\">Impacto antes → depois desta etapa</h2>\n      <select id=\"metricSel\">${METRICS_DEF.map(m=>`<option value=\"${m[0]}\" ${m[0]===mv.metric?'selected':''}>${m[1]}</option>`).join('')}</select></div>\n    <div class=\"note\" style=\"margin:2px 0 4px\">Barra clara = <b>antes desta etapa</b> (já incluindo etapas anteriores) · barra cheia = <b>depois</b> · <b style=\"color:var(--acc)\">${mv.sector}✕</b> = ${lc(lvl.label)} fechad${lvl.a==='à'?'a':'o'} nesta etapa · % = variação</div>\n    ${chartHTML(mv.metric)}`;\n  h+=`<h2 style=\"margin-top:16px\">Antes → Depois por ${lc(lvl.destLabel)} receptor (snapshot congelado)</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${METRICS_DEF.map(m=>`<th>${m[1]}</th>`).join('')}</tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const b=mv.baseMetrics[s]||{};const inc=incBy[s];\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      METRICS_DEF.map(([k])=>{const bv=+b[k]||0,av=bv+(inc[k]||0);const d=bv?((av-bv)/bv*100):0;const f=METRICS_DEF.find(x=>x[0]===k)[2]==='$'?money:fmt;\n        return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;});\n  h+=`<tr style=\"opacity:.8\"><td>${mv.sector} (sai)</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(closeTot[k]||0)}</td>`).join('')+`</tr>`;\n  h+=`</tbody></table></div>`;\n  const incLvl={};mv.dest.forEach(s=>incLvl[s]={});\n  Object.keys(mv.munis).forEach(m=>{if(m==='__OUTROS__')return;const s=mv.assign[m];if(!s||!incLvl[s])return;const lv=mv.levelByMuni[m]||{};for(const nv in lv)incLvl[s][nv]=(incLvl[s][nv]||0)+lv[nv];});\n  c.leaders.forEach(L=>{ const ol=mv.orphanLevelByGroup&&mv.orphanLevelByGroup[L.grupo];\n    if(ol&&L.home&&incLvl[L.home]) Object.entries(ol).forEach(([nv,n])=>incLvl[L.home][nv]=(incLvl[L.home][nv]||0)+n); });\n  h+=`<h2 style=\"margin-top:18px\">Consultoras por nível — antes → depois por ${lc(lvl.destLabel)} receptor</h2>\n    <div class=\"scroll\"><table><thead><tr><th>${lvl.destLabel}</th>${NIVEIS.map(n=>`<th>${n}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;\n  mv.dest.forEach(s=>{const base=mv.baseLevel[s]||{},inc=incLvl[s]||{};let tb=0,ta=0;\n    h+=`<tr><td><span class=\"home\" style=\"background:${mv.color[s]}\"></span>${s}</td>`+\n      NIVEIS.map(nv=>{const b=base[nv]||0,i=inc[nv]||0,a=b+i;tb+=b;ta+=a;\n        return `<td>${a?fmt(a):'·'}${i?`<div class=\"delta up\">+${fmt(i)}</div>`:''}</td>`;}).join('')+\n      `<td style=\"font-weight:700\">${fmt(ta)}${ta-tb?`<div class=\"delta up\">+${fmt(ta-tb)}</div>`:''}</td></tr>`;});\n  h+=`</tbody></table></div><div class=\"note\">Número = CBs por nível <b>depois</b> desta etapa · <span style=\"color:var(--ok)\">+N</span> = entrantes do ${mv.sector}.</div>`;\n  const spk=(key)=>{const vals=trend.map(t=>+t[key]||0);const mx=Math.max(...vals,1);\n    return `<div class=\"spark\">${vals.map(v=>`<div class=\"b\" style=\"height:${(v/mx*100).toFixed(0)}%\" title=\"${fmt(v)}\"></div>`).join('')}</div>\n      <div class=\"note\">${trend.map(t=>String(t.ciclo).slice(-2)).join(' · ')}</div>`;};\n  h+=`<h2 style=\"margin-top:18px\">Setor ${mv.sector} — últimos ${trend.length} ciclos até o snapshot congelado</h2>\n    <div class=\"note\" style=\"margin:-4px 0 8px\">Tendência histórica <b>do próprio setor</b> — não ajustada pela absorção hipotética de etapas anteriores (limitação conhecida: tendência é por setor, não decomponível por município).</div>\n    <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:10px\">\n    ${[['cbs','CBs'],['ativas','Ativas'],['receita','Receita'],['pontos','Pontos']].map(([k,l])=>`<div><div class=\"note\" style=\"margin:0 0 3px;font-weight:600;color:var(--ink)\">${l}</div>${spk(k)}</div>`).join('')}</div>`;\n  $('#tab-report').innerHTML=h;\n  const sel=$('#metricSel'); if(sel) sel.onchange=()=>{mv.metric=sel.value; renderReport();};\n}\nfunction renderCumulative(){\n  const el=$('#tab-cumulative'); if(!el) return;\n  // Esta view soma a cadeia inteira via foldWorldState().muniOverride/sectorBaseCache — hoje só\n  // populados pelo fechamento de setor. GV/mercado têm o rastro equivalente (secOverride/gvOverride),\n  // mas essa agregação ainda não foi refeita nesse grão; melhor avisar do que mostrar tabela vazia.\n  const lockLvl=SESS.timeline.length?(SESS.timeline[0].level||'setor'):'setor';\n  if(lockLvl!=='setor'){ el.innerHTML=`<div class=\"note\">Impacto acumulado da cadeia ainda não disponível pra sessões de ${lc(LEVELS[lockLvl].label)} — use o relatório da etapa (aba anterior) etapa a etapa.</div>`; return; }\n  if(!SESS.timeline.length){ el.innerHTML='<div class=\"note\">Nenhuma etapa na timeline ainda.</div>'; return; }\n  const ready=SESS.timeline.every(mv=>mv.munis);\n  if(!ready){ el.innerHTML='<div class=\"note\">Aguardando etapas terminarem de calcular…</div>'; return; }\n  const finalWorld=foldWorldState(SESS.timeline,SESS.timeline.length);\n  const openSectors=[...new Set(Object.values(finalWorld.muniOverride))];\n  let h=`<h2 style=\"margin:0 0 8px\">Linha do tempo — ${SESS.timeline.length} fechamento(s)</h2>\n    <div class=\"note\" style=\"margin:-2px 0 10px\">Setores fechados, em ordem: ${SESS.timeline.map(mv=>mv.sector).join(' → ')}. Cada linha abaixo soma o efeito de <b>toda a cadeia</b>, não apenas da última etapa.</div>`;\n  h+=`<div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th>${METRICS_DEF.map(([,l])=>`<th>${l}</th>`).join('')}</tr></thead><tbody>`;\n  SESS.timeline.forEach((mv,i)=>{\n    h+=`<tr style=\"opacity:.75\"><td>${mv.sector}</td><td>fechado (etapa ${i+1})</td>`+METRICS_DEF.map(([k,,t])=>`<td>${(t==='$'?money:fmt)(mv.pureOwnMetrics?.[k]||0)}</td>`).join('')+`</tr>`;\n  });\n  openSectors.forEach(s=>{\n    const base=SESS.sectorBaseCache[s]||{};\n    const infl=finalWorld.sectorInflow[s]||{};\n    h+=`<tr><td>${s}</td><td>recebeu território</td>`+METRICS_DEF.map(([k,,t])=>{\n      const bv=+base[k]||0, av=bv+(infl[k]||0); const d=bv?((av-bv)/bv*100):0; const f=(t==='$'?money:fmt);\n      return `<td>${f(av)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;\n    }).join('')+`</tr>`;\n  });\n  h+=`</tbody></table></div><div class=\"note\">\"Antes\" de um setor receptor é o snapshot congelado (sem nenhum fechamento aplicado); \"depois\" soma tudo que a cadeia inteira jogou pra ele.</div>`;\n  el.innerHTML=h;\n}\nfunction project(feats){let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  const each=cb=>feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(p=>p.forEach(r=>r.forEach(cb)));});\n  each(p=>{mnx=Math.min(mnx,p[0]);mxx=Math.max(mxx,p[0]);mny=Math.min(mny,p[1]);mxy=Math.max(mxy,p[1]);});\n  const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=MAP_W,H=MAP_H,pad=22;\n  const gw=(mxx-mnx)*kx,gh=(mxy-mny),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n  const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n  return p=>[ox+(p[0]-mnx)*kx*sc, oy+(mxy-p[1])*sc];\n}\nfunction pathFor(f,pr){const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  return ps.map(poly=>poly.map(r=>'M'+r.map(p=>{const q=pr(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');}\nfunction cxy(f,pr){let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];ps.forEach(poly=>poly[0].forEach(p=>{const q=pr(p);x+=q[0];y+=q[1];n++;}));return[x/n,y/n];}\n// ---------- shared-município slicing ----------\n// A huge urban município (São Paulo etc.) can be the CSV's dominant município for MANY different\n// setores at once — Natura subdivides one big city into dozens of commercial sectors while IBGE's\n// shapefile only has ONE polygon for the whole city. Drawing that single polygon once per claiming\n// sector stacked identical copies on top of each other: only whichever sector was processed first\n// ever \"won\" the shape, and every other sector's label floated over territory it didn't actually own\n// (mv.muniOwners below records every claimant, not just the first). Slice the polygon into N\n// equal-width strips instead — one per claiming sector, ordered by sector code — along whichever\n// axis the shape is longer on, so each sector genuinely gets its own area instead of losing the race.\nfunction clipRingHalf(ring,axis,keepGe,val){\n  const out=[],n=ring.length; if(!n) return out;\n  const side=p=>keepGe?p[axis]>=val:p[axis]<=val;\n  for(let i=0;i<n;i++){\n    const cur=ring[i],prev=ring[(i-1+n)%n];\n    const curIn=side(cur),prevIn=side(prev);\n    if(curIn!==prevIn){\n      const t=(val-prev[axis])/(cur[axis]-prev[axis]);\n      const other=1-axis,pt=[0,0]; pt[axis]=val; pt[other]=prev[other]+t*(cur[other]-prev[other]);\n      out.push(pt);\n    }\n    if(curIn) out.push(cur);\n  }\n  return out;\n}\nfunction sliceFeatureEqual(feature,n){\n  if(n<=1) return [feature];\n  const g=feature.geometry,parts=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n  const rings=[]; parts.forEach(poly=>poly.forEach(r=>{ if(r&&r.length>=4) rings.push(r); }));\n  let mnx=Infinity,mxx=-Infinity,mny=Infinity,mxy=-Infinity;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  if(!isFinite(mnx)) return Array.from({length:n},()=>({...feature,__empty:true}));\n  const wide=(mxx-mnx)>=(mxy-mny), axis=wide?0:1;\n  const lo=wide?mnx:mny, hi=wide?mxx:mxy, span=(hi-lo)||1;\n  const out=[];\n  for(let i=0;i<n;i++){\n    const a=lo+span*i/n, b=lo+span*(i+1)/n;\n    const polys=[];\n    rings.forEach(r=>{\n      let c=clipRingHalf(r,axis,true,a);\n      c=clipRingHalf(c,axis,false,b);\n      if(c.length>=3) polys.push([[...c,c[0]]]);\n    });\n    out.push(polys.length?{type:'Feature',properties:feature.properties,__uf:feature.__uf,geometry:{type:'MultiPolygon',coordinates:polys}}:{...feature,__empty:true});\n  }\n  return out;\n}\n// ---------- label placement ----------\n// Pole of inaccessibility: the interior point farthest from ANY boundary of the dissolved area. A\n// centroid is only a mean — on a concave sector, one split across a river, or a member whose vertex\n// average falls in a bay, it lands outside the very territory it is supposed to name, which is what\n// pushed the sector numbers off their own shapes. Even-odd across every ring of every member is a\n// correct union test here because municipalities tile rather than overlap.\n// The search box is clipped to the viewBox, so the result is inside the territory AND on screen.\n// Rings are decimated first — this is a label, not a measurement — which keeps a 20-município sector\n// to a few million ops, and the result is cached per stage: geometry never changes between renders,\n// only the fill does.\nfunction labelPointFor(fs,pr){\n  const rings=[];\n  (fs||[]).forEach(f=>{ if(!f||!f.geometry) return;\n    const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>{\n      if(!r||r.length<4) return;\n      const step=Math.max(1,Math.floor(r.length/120)),pts=[];\n      for(let i=0;i<r.length;i+=step) pts.push(pr(r[i]));\n      if(pts.length>3) rings.push(pts);\n    }));});\n  if(!rings.length) return null;\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  rings.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; }));\n  mnx=Math.max(mnx,2); mny=Math.max(mny,2); mxx=Math.min(mxx,698); mxy=Math.min(mxy,498);\n  if(!(mxx>mnx&&mxy>mny)) return null;\n  const inside=(x,y)=>{ let c=false;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      if(((a[1]>y)!==(b[1]>y)) && (x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])) c=!c; }\n    return c; };\n  const edgeDist=(x,y)=>{ let m=Infinity;\n    for(const r of rings) for(let i=0,j=r.length-1;i<r.length;j=i++){ const a=r[i],b=r[j];\n      const dx=b[0]-a[0],dy=b[1]-a[1],L=dx*dx+dy*dy;\n      let t=L?(((x-a[0])*dx+(y-a[1])*dy)/L):0; t=t<0?0:(t>1?1:t);\n      const ex=a[0]+t*dx-x,ey=a[1]+t*dy-y,d=ex*ex+ey*ey; if(d<m)m=d; }\n    return Math.sqrt(m); };\n  const W=mxx-mnx,H=mxy-mny,N=26;\n  // Centroide (média simples dos vértices) do território inteiro — só usado pra desempatar A FAVOR\n  // do meio quando um ponto mais central tem folga quase tão boa quanto o campeão isolado. Um setor\n  // com um lóbulo bem mais gordo de um lado sempre ganhava o rótulo puxado pra esse lado, mesmo\n  // quando um ponto quase tão espaçoso e bem mais central também servia — o peso é pequeno de\n  // propósito, nunca vale mais que uma folga real melhor.\n  let ccx=0,ccy=0,cn=0;\n  rings.forEach(r=>r.forEach(p=>{ ccx+=p[0]; ccy+=p[1]; cn++; }));\n  ccx/=cn||1; ccy/=cn||1;\n  const CENTER_PULL=0.22;\n  const scoreAt=(x,y,d)=>d-CENTER_PULL*Math.hypot(x-ccx,y-ccy);\n  let best=null,bd=-1,bs=-Infinity;\n  for(let i=0;i<=N;i++) for(let j=0;j<=N;j++){\n    const x=mnx+W*i/N,y=mny+H*j/N;\n    if(!inside(x,y)) continue;\n    const d=edgeDist(x,y),s=scoreAt(x,y,d); if(s>bs){ bs=s; bd=d; best=[x,y]; }\n  }\n  if(!best) return null;\n  // Hill-climb off the coarse grid: the grid only has to land somewhere in the right lobe, the\n  // refinement finds the deep point inside it — halving the step whenever no neighbour improves.\n  let step=Math.max(W,H)/N/2;\n  for(let k=0;k<7;k++){\n    let moved=false;\n    for(const o of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]){\n      const x=best[0]+o[0]*step,y=best[1]+o[1]*step;\n      if(x<mnx||x>mxx||y<mny||y>mxy||!inside(x,y)) continue;\n      const d=edgeDist(x,y),s=scoreAt(x,y,d); if(s>bs){ bs=s; bd=d; best=[x,y]; moved=true; }\n    }\n    if(!moved) step/=2;\n  }\n  // The clearance travels with the point: it is the radius of the largest circle that fits inside the\n  // territory there, which is exactly the budget a caller needs to decide how much text will fit.\n  return {p:best,r:bd};\n}\nfunction labelPointOf(mv,key,fs,pr){\n  if(!mv._lbl) mv._lbl={};\n  if(!(key in mv._lbl)) mv._lbl[key]=labelPointFor(fs,pr);\n  return mv._lbl[key];\n}\n// ---------- map zoom/pan ----------\n// Zoom é feito manipulando o viewBox, nunca um transform CSS — assim rótulos e polígonos vivem no\n// MESMO sistema de coordenadas e escalam juntos automaticamente (texto tem font-size em unidades do\n// viewBox, então encolher o viewBox amplia tudo por igual, sem lógica extra pro tamanho da fonte).\nconst MAP_W=700; let MAP_H=500; const MAP_ZOOM_MAX=8;\nlet mapView={x:0,y:0,w:MAP_W,h:MAP_H};\n// MAP_H acompanha a proporção real do card na tela (MAP_W fica fixo) — sem isso o \"mundo\" de\n// desenho tinha sempre proporção 700x500 fixa, e o <svg> (que preserva essa proporção) ficava\n// menor que o card sempre que o card fosse bem mais alto que largo, sobrando um vão em branco\n// embaixo — a sensação de \"mapa preso num quadrado\" dentro de um espaço bem maior.\nfunction syncMapDims(){\n  const wrap=$('.map-wrap'); if(!wrap) return;\n  const cw=wrap.clientWidth, ch=wrap.clientHeight; if(!cw||!ch) return;\n  const newH=Math.round(MAP_W*(ch/cw));\n  if(Math.abs(newH-MAP_H)<2) return;\n  const fracW=mapView.w/MAP_W, cx=(mapView.x+mapView.w/2)/MAP_W, cy=(mapView.y+mapView.h/2)/MAP_H;\n  MAP_H=newH;\n  const w=Math.min(MAP_W,Math.max(MAP_W/MAP_ZOOM_MAX,MAP_W*fracW));\n  const h=Math.min(MAP_H,Math.max(MAP_H/MAP_ZOOM_MAX,MAP_H*fracW));\n  let x=cx*MAP_W-w/2, y=cy*MAP_H-h/2;\n  x=Math.min(MAP_W-w,Math.max(0,x)); y=Math.min(MAP_H-h,Math.max(0,y));\n  mapView={x,y,w,h};\n}\nfunction applyMapView(){\n  const svg=$('#map'); if(!svg) return;\n  svg.setAttribute('viewBox', mapView.x.toFixed(2)+' '+mapView.y.toFixed(2)+' '+mapView.w.toFixed(2)+' '+mapView.h.toFixed(2));\n  // Rótulo tem font-size em unidades do viewBox, então sem isso ele cresceria junto com o zoom até\n  // virar um borrão — encolhendo o font-size na mesma proporção que o viewBox encolhe cancela\n  // exatamente o zoom, e o texto fica do MESMO tamanho na tela em qualquer nível de zoom.\n  const fsScale=mapView.w/MAP_W;\n  svg.querySelectorAll('text[data-fs]').forEach(t=>{\n    const base=+t.getAttribute('data-fs')||8;\n    t.setAttribute('font-size',(base*fsScale).toFixed(2));\n  });\n  // Mesmo raciocínio pro halo do texto e pro fio que liga um rótulo deslocado ao seu município —\n  // os dois têm largura de traço fixa em unidades do viewBox, então sem reescalar iam virar uma\n  // mancha grossa (halo) ou uma barra (fio) no zoom alto em vez de ficarem do mesmo tamanho na tela.\n  svg.querySelectorAll('[data-sw]').forEach(el=>{\n    const base=+el.getAttribute('data-sw')||1;\n    el.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n  });\n  // Fio de rótulo deslocado é calculado UMA vez, no mundo inteiro de 700x500, pra evitar colisão\n  // com TODO outro rótulo — inclusive os que, no zoom atual, nem estão mais na tela. Quanto mais\n  // fundo o zoom, maior a fração da área visível que esse mesmo fio (comprimento fixo em unidades\n  // do mundo) passa a cobrir, virando um risco diagonal cortando o mapa até um rótulo que já saiu\n  // do enquadramento. Esconder o fio quando ele passa de uma fração do que está visível agora\n  // resolve isso sem precisar recalcular posição de rótulo a cada passo de zoom.\n  const viewDiag=Math.hypot(mapView.w,mapView.h);\n  svg.querySelectorAll('line.lead').forEach(ln=>{\n    const dx=(+ln.getAttribute('x2'))-(+ln.getAttribute('x1')), dy=(+ln.getAttribute('y2'))-(+ln.getAttribute('y1'));\n    ln.style.display=(Math.hypot(dx,dy)>viewDiag*0.35)?'none':'';\n  });\n}\nfunction zoomMapBy(factor){\n  const cx=mapView.x+mapView.w/2, cy=mapView.y+mapView.h/2;\n  let w=mapView.w/factor, h=mapView.h/factor;\n  w=Math.min(MAP_W, Math.max(MAP_W/MAP_ZOOM_MAX, w));\n  h=Math.min(MAP_H, Math.max(MAP_H/MAP_ZOOM_MAX, h));\n  let x=cx-w/2, y=cy-h/2;\n  x=Math.min(MAP_W-w, Math.max(0,x));\n  y=Math.min(MAP_H-h, Math.max(0,y));\n  mapView={x,y,w,h};\n  applyMapView();\n}\n// Mesma matemática do zoomMapBy, mas ancorada no cursor em vez do centro da view — o ponto do mapa\n// que está sob o mouse continua sob o mouse depois do zoom, o que faz o scroll parecer\n// contínuo/\"seamless\" em vez de recentralizar o mapa a cada girada da roda.\nfunction zoomMapAt(factor, clientX, clientY){\n  const svg=$('#map'); if(!svg) return;\n  const rect=svg.getBoundingClientRect(); if(!rect.width||!rect.height) return;\n  const fx=(clientX-rect.left)/rect.width, fy=(clientY-rect.top)/rect.height;\n  const wx=mapView.x+fx*mapView.w, wy=mapView.y+fy*mapView.h;\n  let w=mapView.w/factor, h=mapView.h/factor;\n  w=Math.min(MAP_W, Math.max(MAP_W/MAP_ZOOM_MAX, w));\n  h=Math.min(MAP_H, Math.max(MAP_H/MAP_ZOOM_MAX, h));\n  let x=wx-fx*w, y=wy-fy*h;\n  x=Math.min(MAP_W-w, Math.max(0,x));\n  y=Math.min(MAP_H-h, Math.max(0,y));\n  mapView={x,y,w,h};\n  applyMapView();\n}\n// Arrastar sempre fica DENTRO do canvas de 700x500 (clamp em [0, MAP_W-w] / [0, MAP_H-h]) — nunca\n// existe um x/y que deixa o viewBox parcialmente fora da área com conteúdo, então o mapa nunca\n// \"some\" da tela, só pode chegar até a borda do que existe pra ver.\nlet mapDrag=null, mapDidDrag=false;\nfunction mapPointerDown(e){\n  const svg=$('#map'); if(!svg) return;\n  mapDrag={x0:e.clientX,y0:e.clientY,vx0:mapView.x,vy0:mapView.y,rect:svg.getBoundingClientRect()};\n  mapDidDrag=false;\n  svg.classList.add('dragging');\n  // NÃO setPointerCapture aqui: capturar o ponteiro no <svg> reencaminha o click resultante pro\n  // próprio <svg> em vez do <path> do município embaixo do cursor — todo clique parava de chegar\n  // no onclick de reatribuição, não só depois de um arrasto de verdade. move/up escutam em `root`\n  // (nunca substituído por renderAll) em vez de no <svg>, então o arrasto continua funcionando\n  // mesmo se o ponteiro sair da área do mapa no meio do gesto.\n}\nfunction mapPointerMove(e){\n  if(!mapDrag) return;\n  const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n  if(!mapDidDrag && Math.hypot(dxPix,dyPix)>4) mapDidDrag=true;\n  if(!mapDrag.rect.width||!mapDrag.rect.height) return;\n  const sx=mapView.w/mapDrag.rect.width, sy=mapView.h/mapDrag.rect.height;\n  let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n  x=Math.min(MAP_W-mapView.w, Math.max(0,x));\n  y=Math.min(MAP_H-mapView.h, Math.max(0,y));\n  mapView={...mapView,x,y};\n  applyMapView();\n}\nfunction mapPointerUp(){\n  if(!mapDrag) return;\n  mapDrag=null;\n  const svg=$('#map'); if(svg) svg.classList.remove('dragging');\n}\n// ---------- OSM reference layers (rodovias/rios/picos) ----------\n// This map is a hand-projected SVG, not a Leaflet/Mapbox instance — there is no tile layer to drop a\n// basemap into. Vector geometry is the only shape that can ride the SAME pr() projection every\n// município shape already uses and land in the right spot. Scoped to the bbox of what mv.geoFeat +\n// mv.nbrFeat actually put on screen for THIS stage — never the whole country — because Overpass's\n// road/river graph for Brazil is enormous and the ask here is é \"what's around this closure\", not a\n// national basemap. Cached by (layer, bbox) rather than by session/stage index: two stages that\n// happen to look at the same patch of ground share one fetch, and switching sessions can never serve\n// a stale layer under a reused stage index.\n// Rodovias segmentadas por tag \"highway\" (valores documentados na wiki do Overpass API,\n// https://wiki.openstreetmap.org/wiki/Overpass_API — cada nível viário é um valor exato da tag,\n// nunca uma faixa) em vez de um regex casando vários de uma vez: cada nível liga/desliga por conta\n// própria, com espessura decrescente por importância viária (motorway mais grosso, secondary mais\n// fino) — e cada \"_link\" (trevo/alça de acesso) fica em separado do seu tronco, nunca junto.\nconst HIGHWAY_KEYS=['motorway','motorwayLink','trunk','trunkLink','primary','primaryLink','secondary','secondaryLink'];\nconst OSM_LAYERS=[\n  {key:'motorway',label:'Rodovia expressa',tag:'highway=motorway',color:'#8e8e93',sw:1.3,\n   ql:b=>`way[\"highway\"=\"motorway\"](${b});`},\n  {key:'motorwayLink',label:'Acesso de rodovia expressa',tag:'highway=motorway_link',color:'#8e8e93',sw:0.9,\n   ql:b=>`way[\"highway\"=\"motorway_link\"](${b});`},\n  {key:'trunk',label:'Via arterial',tag:'highway=trunk',color:'#8e8e93',sw:1.15,\n   ql:b=>`way[\"highway\"=\"trunk\"](${b});`},\n  {key:'trunkLink',label:'Acesso de via arterial',tag:'highway=trunk_link',color:'#8e8e93',sw:0.85,\n   ql:b=>`way[\"highway\"=\"trunk_link\"](${b});`},\n  {key:'primary',label:'Rodovia principal',tag:'highway=primary',color:'#8e8e93',sw:1.0,\n   ql:b=>`way[\"highway\"=\"primary\"](${b});`},\n  {key:'primaryLink',label:'Acesso de rodovia principal',tag:'highway=primary_link',color:'#8e8e93',sw:0.75,\n   ql:b=>`way[\"highway\"=\"primary_link\"](${b});`},\n  {key:'secondary',label:'Rodovia secundária',tag:'highway=secondary',color:'#8e8e93',sw:0.85,\n   ql:b=>`way[\"highway\"=\"secondary\"](${b});`},\n  {key:'secondaryLink',label:'Acesso de rodovia secundária',tag:'highway=secondary_link',color:'#8e8e93',sw:0.65,\n   ql:b=>`way[\"highway\"=\"secondary_link\"](${b});`},\n  // Só rio PERMANENTE conta como barreira de verdade pra distância real entre municípios — riacho\n  // é atravessado por qualquer bueiro, e um rio \"intermitente\" (comum no semiárido nordestino) fica\n  // seco a maior parte do ano, então nenhum dos dois muda quanto tempo leva pra ir de um lugar ao\n  // outro. \"Picos\" saiu de vez: um ponto de pico não diz nada sobre se existe estrada ali ou quanto\n  // se precisa desviar pra contornar — a camada de rodovias já mostra isso diretamente.\n  {key:'rivers',label:'Rios',color:'#0a84ff',sw:1.3,\n   ql:b=>`way[\"waterway\"=\"river\"][\"intermittent\"!=\"yes\"](${b});`},\n];\nconst OSM_ON={motorway:false,motorwayLink:false,trunk:false,trunkLink:false,primary:false,primaryLink:false,secondary:false,secondaryLink:false,rivers:false};\n// Legendas default ON — é o comportamento de sempre (rótulo em toda unidade/área de contexto);\n// os checkboxes só existem pra permitir DESLIGAR num mapa carregado, não pra ligar algo novo.\nconst LEGEND_ON={muni:true,ctx:false};\nconst CHILD_PLURAL={Município:'municípios',Setor:'setores',GV:'GVs'};\nconst OSM_CACHE={};\nconst OSM_MAX_SPAN=6; // graus — acima disso (fechamento de mercado muito grande) não busca sozinho\nfunction osmBboxOf(feats){\n  let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n  feats.forEach(f=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n    ps.forEach(poly=>poly.forEach(r=>r.forEach(p=>{ if(p[0]<mnx)mnx=p[0]; if(p[0]>mxx)mxx=p[0]; if(p[1]<mny)mny=p[1]; if(p[1]>mxy)mxy=p[1]; })));});\n  if(!isFinite(mnx)) return null;\n  return {south:mny,west:mnx,north:mxy,east:mxx};\n}\n// Calling overpass-api.de straight from the browser died with a bare \"Failed to fetch\" on the\n// operator's own network (proved: the exact same call succeeded from a different network) — a\n// corporate firewall/proxy blocking that third-party domain, most likely. Routing through Data2's own\n// Rush listener (osmOverpassProxy → HttpClient → overpass-api.de) moves the outbound hop to Data2's\n// backend, which never touches the operator's restricted network at all.\nasync function ensureOsmLayer(def,bbox,ck){\n  OSM_CACHE[ck]={status:'loading',data:null,error:null};\n  const b=bbox.south.toFixed(4)+','+bbox.west.toFixed(4)+','+bbox.north.toFixed(4)+','+bbox.east.toFixed(4);\n  const query='[out:json][timeout:25];('+def.ql(b)+');out geom;';\n  const rec=OSM_CACHE[ck];\n  try{\n    const res=await d2.rush('osmOverpassProxy').invoke({data:query});\n    const env=res&&res.data;\n    if(!env) throw new Error('proxy sem resposta');\n    if(!env.ok) throw new Error(env.error||'falha desconhecida no proxy');\n    rec.data=env.elements||[]; rec.status='ok';\n  }catch(e){\n    rec.status='error'; rec.error=(e&&e.message)||'falha no proxy';\n    // Falha (504 do Overpass sobrecarregado, timeout etc.) desmarca o checkbox e esquece este cache\n    // — religar exige um novo clique, e esse clique já cai em ensureOsmLayer de novo (sem registro\n    // em OSM_CACHE pra essa bbox), disparando uma tentativa nova em vez de repetir o erro guardado.\n    delete OSM_CACHE[ck];\n    OSM_ON[def.key]=false;\n    const el=$('#osm'+def.key[0].toUpperCase()+def.key.slice(1)); if(el) el.checked=false;\n    syncRodoviasMaster();\n  }\n  drawMap();\n}\nfunction drawOsmLayer(svg,pr,feats,NS){\n  const st=$('#osmStatus');\n  const active=OSM_LAYERS.filter(l=>OSM_ON[l.key]); if(!active.length){ if(st) st.textContent=''; return; }\n  const bbox=osmBboxOf(feats); if(!bbox){ if(st) st.textContent=''; return; }\n  const tooBig=(bbox.north-bbox.south)>OSM_MAX_SPAN||(bbox.east-bbox.west)>OSM_MAX_SPAN;\n  const fsScale=mapView.w/MAP_W, msgs=[];\n  active.forEach(def=>{\n    if(tooBig){ msgs.push(def.label+': área grande demais p/ carregar automaticamente'); return; }\n    const bk=bbox.south.toFixed(3)+'_'+bbox.west.toFixed(3)+'_'+bbox.north.toFixed(3)+'_'+bbox.east.toFixed(3);\n    const ck=def.key+'|'+bk;\n    let rec=OSM_CACHE[ck];\n    if(!rec){ ensureOsmLayer(def,bbox,ck); rec=OSM_CACHE[ck]; }\n    if(rec.status==='loading') msgs.push(def.label+': carregando…');\n    else if(rec.status==='error') msgs.push(def.label+': falhou ('+rec.error+')');\n    if(rec.status!=='ok') return;\n    const g=document.createElementNS(NS,'g'); g.style.pointerEvents='none';\n    rec.data.forEach(el=>{\n      // Toda camada que sobrou é way (linha) — o único node-layer (picos) saiu do OSM_LAYERS acima.\n      if(!el.geometry||!el.geometry.length) return;\n      const d='M'+el.geometry.filter(Boolean).map(p=>{const q=pr([p.lon,p.lat]);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L');\n      const p=document.createElementNS(NS,'path'); p.setAttribute('d',d); p.setAttribute('fill','none');\n      p.setAttribute('stroke',def.color); p.setAttribute('opacity','0.72');\n      p.setAttribute('data-sw',def.sw); p.style.strokeWidth=(def.sw*fsScale).toFixed(3)+'px';\n      g.appendChild(p);\n    });\n    svg.appendChild(g);\n  });\n  if(st) st.textContent=msgs.join(' · ');\n}\n// Mede o espaço de viewport que sobra abaixo do topo do card do mapa e usa isso como altura —\n// não um percentual fixo — porque o que existe ACIMA do mapa varia (trilho de etapas e toolbar só\n// aparecem quando a sessão já tem movimentações), então só medir de verdade para exatamente na\n// borda da tela em qualquer estado.\nfunction fitMapHeight(){\n  const wrap=$('.map-wrap'); if(!wrap) return;\n  const top=wrap.getBoundingClientRect().top;\n  if(top<=0) return;\n  // Margem generosa de propósito: sobra um pedaço visível do card seguinte (líderes/matriz) na\n  // borda inferior da tela, como sinal de que tem mais conteúdo abaixo pra rolar.\n  const margin=80;\n  wrap.style.height=Math.max(220, window.innerHeight-top-margin)+'px';\n}\nfunction drawMap(){\n  const mv=SESS.timeline[SESS.selected]; const svg=$('#map'); const ignNote=$('#mapIgnoredNote');\n  if(!mv){ svg.innerHTML=''; if(ignNote) ignNote.innerHTML=''; return; }\n  const level=mv.level||'setor';\n  const legendMuniLbl=$('#legendMuniLbl');\n  if(legendMuniLbl) legendMuniLbl.textContent = level==='setor' ? 'Nome de municípios' : `Código de ${CHILD_PLURAL[LEVELS[level].childLabel]||LEVELS[level].childLabel}`;\n  // setor fecha: a unidade É o município. GV/mercado fecham: cada município herda a atribuição do\n  // setor/GV a que pertence — municípios do mesmo setor sempre têm a MESMA cor e giram JUNTOS no\n  // clique, porque é o setor inteiro (não o município) que troca de dono nesses níveis.\n  const unitAssign=m=>level==='setor'?mv.assign[m]:mv.assign[(mv.unitOfMuni||{})[m]];\n  const c=computeDerived(mv);const NS='http://www.w3.org/2000/svg'; svg.innerHTML='';\n  const feats=[...mv.nbrFeat,...Object.values(mv.geoFeat)]; if(!feats.length) return;\n  syncMapDims();\n  const pr=project(feats);\n  const world=foldWorldState(SESS.timeline,SESS.selected);\n  // The context layer is ONE area per SECTOR, not one shape per município. The unit of this decision\n  // is the destination sector; a scatter of individually outlined municipalities carrying their own\n  // names invited exactly the wrong reading of the map. Members of a sector are filled AND stroked in\n  // the same colour, so the shared borders vanish and the group reads as a single dissolved\n  // territory — the IBGE shapes do not share vertices precisely enough to extract a clean union\n  // outline, and carrying a real polygon-clipping union for a context layer is not worth it.\n  // Labels are COLLECTED here and drawn last, after every path. Drawn inline they were buried by the\n  // next sector's fill, and placement cannot avoid a collision it has not seen yet.\n  const labels=[],placed=[];\n  const textW=(s,fs)=>String(s).length*fs*0.56;\n  // Centred on (x,y) in BOTH axes. The old box hung off the first line's baseline, so every two-line\n  // label sat half its own height below the point it was supposed to mark.\n  const rectOf=(x,y,lines)=>{const w=Math.max.apply(null,lines.map(l=>textW(l.t,l.size)));\n    const h=lines.reduce((a,l)=>a+l.size*1.2,0);\n    return {x1:x-w/2-2.5,x2:x+w/2+2.5,y1:y-h/2-1.5,y2:y+h/2+1.5};};\n  const nbrBySec={};\n  mv.nbrFeat.forEach(f=>{\n    const nm=mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf);\n    const owners=mv.muniOwners&&mv.muniOwners[nm]?[...new Set(mv.muniOwners[nm])]:[];\n    if(owners.length>1){\n      owners.sort((a,b)=>(+a-+b)||(a<b?-1:a>b?1:0));\n      sliceFeatureEqual(f,owners.length).forEach((slice,i)=>{\n        if(slice.__empty) return;\n        (nbrBySec[owners[i]]=nbrBySec[owners[i]]||[]).push(slice);\n      });\n      return;\n    }\n    const s=String((mv.nbrCurrentSector&&mv.nbrCurrentSector[nm])||'');\n    (nbrBySec[s]=nbrBySec[s]||[]).push(f);\n  });\n  (mv.sharedMuniSlices||[]).forEach(sl=>{ (nbrBySec[sl.sector]=nbrBySec[sl.sector]||[]).push(sl.feature); });\n  Object.keys(nbrBySec).forEach(s=>{\n    const fs=nbrBySec[s];\n    const isDest=!!s&&mv.dest.includes(s);\n    const isRejected=!!s&&mv.rejectedCodes&&mv.rejectedCodes.has(s);\n    const closedCtx=!!s&&world.closedSectors.has(s);\n    const isUnresolved=!s;\n    const col=isDest?(mv.color[s]||null):null;\n    const rej=isRejected?(mv.rejectedInfo&&mv.rejectedInfo[s]):null;\n    // Sem união de polígono de verdade, a única forma de emular UM contorno em volta do setor\n    // inteiro é desenhar essa borda ANTES do preenchimento: cada aresta INTERNA (entre dois membros\n    // do mesmo setor) fica coberta pelo preenchimento opaco do vizinho por cima, e só a aresta\n    // EXTERNA (sem vizinho do mesmo setor do outro lado) continua visível — e, se dois setores\n    // \"inválidos\" diferentes se tocam, cada um desenha sua própria borda naquele limite, então a\n    // linha aparece do mesmo jeito, sem sumir.\n    if(isRejected) fs.forEach(f=>{\n      const bp=document.createElementNS(NS,'path');\n      bp.setAttribute('d',pathFor(f,pr));\n      bp.setAttribute('class','sect-rejected-border');\n      svg.appendChild(bp);\n    });\n    fs.forEach(f=>{\n      const p=document.createElementNS(NS,'path');\n      p.setAttribute('d',pathFor(f,pr));\n      p.setAttribute('class','sect'+(isDest?'':(isRejected?' rejected':(closedCtx?' closed-ctx':(isUnresolved?' unresolved':' plain')))));\n      // isDest: NO stroke at all, fill only. Matching stroke to fill (even at the same opacity)\n      // still doubles up at every shared município edge — two adjacent members each paint their own\n      // translucent stroke on top of the shared line, so it composites darker than the interior even\n      // when both strokes are the exact fill colour. Only an absent stroke truly stops compositing.\n      if(col){ p.setAttribute('fill',col); }\n      if(rej){\n        const why=level==='setor'\n          ? `${LEVELS.setor.destLabel} ${s} é da GV ${rej.parent||'?'} — a GV do setor fechando é a ${mv.gv||'?'}, então não entra como destino.`\n          : `${LEVELS.gv.destLabel} ${s} é do mercado ${rej.parent||'?'} — o mercado da GV fechando é o ${mv.gv||'?'}, então não entra como destino.`;\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';t.innerHTML=why;};\n        p.onmouseleave=()=>$('#tip').style.opacity=0;\n      }\n      if(isUnresolved){\n        // Setor não encontrado pro município vizinho — mostra o NOME real do município no hover em vez\n        // de deixar em branco ou fingir um destino. Estado de debug de propósito: ajuda a achar rápido\n        // qual município ficou sem sector resolvido na base/consulta ao vivo.\n        const nmDisp=mDisp(mkey(f.properties.name||f.properties.NOME||f.properties.nome||'',f.__uf));\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n          t.innerHTML=`<b>${nmDisp}</b><br><span style=\"color:var(--mut)\">setor não encontrado — sem CBs neste snapshot ou não bateu com a base</span>`;};\n        p.onmouseleave=()=>$('#tip').style.opacity=0;\n      }\n      svg.appendChild(p);\n    });\n    if(!s) return;\n    // One number per sector, at the pole of inaccessibility of its DISSOLVED area — the whole group\n    // of members at once, not a representative member, so the number sits in the visual middle of the\n    // territory the operator actually sees.\n    const lp=labelPointOf(mv,'s:'+s,fs,pr);\n    // Context areas always read LIGHT — .sect paints dest colours at fill-opacity .32 over white, and\n    // plain/closed-ctx are near-white greys — so a sector number never wants white ink, whatever colour\n    // the sector itself was assigned.\n    // A leader line is only worth drawing when there's real room to write into — otherwise a big\n    // multi-sector município (São Paulo etc., dozens of slivers) turns into a canvas-wide tangle of\n    // lines, one per sliver too thin to ever hold readable text. lp.r is the radius of the biggest\n    // circle that fits inside the slice; below this it isn't worth labelling at all.\n    if(LEGEND_ON.ctx && lp&&lp.p&&lp.r>=5) labels.push({anchor:lp.p,big:true,lines:[{t:s,size:11.5,weight:700}]});\n  });\n  const asis=mv.view==='asis';\n  // The UF prefix is information only when the stage actually straddles a border. On a single-UF setor\n  // it is the same three characters repeated on every label — a large share of the clutter and none of\n  // the meaning. The hover tooltip carries the UF either way.\n  const showUf=new Set(Object.keys(mv.geoFeat).map(k=>mUf(k)).filter(Boolean)).size>1;\n  if(level==='setor'){\n    // Setor fechando: a UNIDADE é o próprio município — cada um se comporta, colore e rotula\n    // individualmente, exatamente como sempre foi.\n    Object.entries(mv.geoFeat).forEach(([m,f])=>{const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n      const fillCol=asis?CLOSE_COLOR:(mv.color[unitAssign(m)]||'#444');\n      p.setAttribute('class','muni');p.setAttribute('fill',fillCol);\n      if(!asis) p.onclick=()=>{editSelected(m2=>{if(m2.dest.length)m2.assign[m]=(SESS.settings&&SESS.settings.forceContiguity)?nextContigSafeDest(m2,m):m2.dest[(m2.dest.indexOf(m2.assign[m])+1)%m2.dest.length];m2.selMuni=m;});};\n      p.style.cursor=asis?'default':'pointer';\n      p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n        t.innerHTML=asis?`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs · setor ${mv.sector}`:`<b>${mDisp(m)}</b><br>${fmt(mv.cbsByMuni[m]||0)} CBs → ${unitAssign(m)||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n      p.onmouseleave=()=>$('#tip').style.opacity=0;svg.appendChild(p);\n      const lp=labelPointOf(mv,'m:'+m,[f],pr);\n      const at=(lp&&lp.p)||cxy(f,pr);\n      const nmFull=(showUf&&mUf(m)?mUf(m)+'-':'')+mName(m);\n      if(LEGEND_ON.muni) labels.push({anchor:at,cbs:+(mv.cbsByMuni[m]||0),\n        lines:[{t:nmFull,size:8.2,weight:700}]});\n    });\n  } else {\n    // GV/mercado fechando: a UNIDADE é o setor (ou a GV) inteiro, nunca o município isolado — vários\n    // municípios do MESMO setor têm de ler como UMA área só (stroke = fill inline, opaco, então não há\n    // o artefato de dupla-composição da camada de destino: aqui a cor é sólida, não translúcida).\n    // A fronteira que o operador precisa ver é setor-a-setor, não município-a-município, e isso já\n    // sai de graça da própria cor: setores com destinos diferentes contrastam sem precisar de traço\n    // algum. O único caso que fica sem contorno é dois setores vizinhos caindo no MESMO destino — sem\n    // union real de polígono (ver nota da camada de contexto, acima) não há como bordear só o setor\n    // sem também bordear cada município dentro dele, e a segunda opção é a bagunça pontilhada que essa\n    // troca veio corrigir. UM rótulo só por unidade, no ponto de inacessibilidade da área do grupo,\n    // nunca um rótulo por município.\n    Object.entries(mv.unitMunis||{}).forEach(([u,ms])=>{\n      const fillCol=asis?CLOSE_COLOR:(mv.color[mv.assign[u]]||'#444');\n      const feats=ms.map(m=>mv.geoFeat[m]).filter(Boolean);\n      // Todo município deste u é UMA unidade só na tela — passar o mouse sobre qualquer pedaço tem\n      // que acender o grupo inteiro, não só o pedaço que o cursor está tocando. :hover no CSS só\n      // sabe acender o próprio <path>; o grupo inteiro precisa de JS guardando os irmãos.\n      const els=[];\n      ms.forEach(m=>{\n        const f=mv.geoFeat[m]; if(!f) return;\n        const p=document.createElementNS(NS,'path');p.setAttribute('d',pathFor(f,pr));\n        p.setAttribute('class','muni');p.setAttribute('fill',fillCol);p.style.stroke=fillCol;\n        els.push(p);\n        if(!asis) p.onclick=()=>{editSelected(m2=>{const i=m2.dest.indexOf(m2.assign[u]);if(m2.dest.length)m2.assign[u]=m2.dest[(i+1)%m2.dest.length];m2.selMuni=u;});};\n        p.style.cursor=asis?'default':'pointer';\n        p.onmouseenter=()=>{ els.forEach(e=>e.classList.add('munihl')); };\n        p.onmousemove=e=>{const t=$('#tip');t.style.opacity=1;t.style.left=(e.clientX+12)+'px';t.style.top=(e.clientY+12)+'px';\n          t.innerHTML=asis?`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs · ${LEVELS[level].label} ${mv.sector}`:`<b>${LEVELS[level].childLabel} ${u}</b><br>${fmt(mv.munis[u]?.cbs||0)} CBs → ${mv.assign[u]||'–'}<br><span style=\"color:var(--mut)\">clique p/ trocar</span>`;};\n        p.onmouseleave=()=>{ els.forEach(e=>e.classList.remove('munihl')); $('#tip').style.opacity=0; };svg.appendChild(p);\n      });\n      const lp=labelPointOf(mv,'u:'+u,feats,pr);\n      const at=(lp&&lp.p)||(feats[0]&&cxy(feats[0],pr));\n      if(at && LEGEND_ON.muni){\n        const num=fmt(mv.munis[u]?.cbs||0)+(asis?'':'·'+(mv.assign[u]||'–'));\n        labels.push({anchor:at,big:true,cbs:+(mv.munis[u]?.cbs||0),\n          lines:[{t:`${LEVELS[level].childLabel} ${u}`,size:9,weight:700},{t:num,size:7.5,weight:500,mut:true}]});\n      }\n    });\n  }\n  // Placement, not de-cluttering. Every label IS drawn: it starts at its anchor and, if the text does\n  // not fit the territory or the slot is already taken, walks outward ring by ring until it finds\n  // clear canvas, trailing a hairline leader back to the shape it names. The previous pass dropped the\n  // loser of every collision outright, which is why municípios silently had no name at all.\n  // Order still matters — sector numbers first, then municípios by CB weight — because the earlier a\n  // label is placed the closer to home it lands.\n  const CW=700,CH=500,CPAD=2;\n  const inBounds=r=>r.x1>=CPAD&&r.x2<=CW-CPAD&&r.y1>=CPAD&&r.y2<=CH-CPAD;\n  const hits=r=>placed.reduce((a,q)=>a+((r.x2<q.x1||r.x1>q.x2||r.y2<q.y1||r.y1>q.y2)?0:1),0);\n  // Um rótulo deslocado não pode só EVITAR a caixa de outro — o fio que o liga de volta ao seu\n  // ponto de origem também não pode atravessar por cima do texto de um rótulo já colocado, senão o\n  // fio some por baixo das letras (ou risca por cima delas) do vizinho.\n  const segSeg=(ax,ay,bx,by,cx,cy,dx,dy)=>{\n    const d1=(dx-cx)*(ay-cy)-(dy-cy)*(ax-cx), d2=(dx-cx)*(by-cy)-(dy-cy)*(bx-cx);\n    const d3=(bx-ax)*(cy-ay)-(by-ay)*(cx-ax), d4=(bx-ax)*(dy-ay)-(by-ay)*(dx-ax);\n    return ((d1>0)!==(d2>0))&&((d3>0)!==(d4>0));\n  };\n  const segRect=(x1,y1,x2,y2,r)=>{\n    const inR=(x,y)=>x>=r.x1&&x<=r.x2&&y>=r.y1&&y<=r.y2;\n    if(inR(x1,y1)||inR(x2,y2)) return true;\n    return segSeg(x1,y1,x2,y2,r.x1,r.y1,r.x2,r.y1)||segSeg(x1,y1,x2,y2,r.x2,r.y1,r.x2,r.y2)\n        ||segSeg(x1,y1,x2,y2,r.x2,r.y2,r.x1,r.y2)||segSeg(x1,y1,x2,y2,r.x1,r.y2,r.x1,r.y1);\n  };\n  // Ring 0 is the anchor itself. Rings step 9u out over 16 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs.\n  // Ring 0 is the anchor itself. Rings step 8u out over 20 directions, stretched horizontally because\n  // a label is wider than it is tall and sideways room is the room it actually needs. Pushed to 16\n  // rings (was 7, 16 directions) — \"labels should never overlap\" only holds if the search genuinely\n  // exhausts the nearby space before giving up; a shallower search gave up too soon on crowded\n  // clusters and just stacked labels on top of each other instead of finding a real clear spot.\n  const CAND=[[0,0]];\n  for(let ring=1;ring<=16;ring++){ const d=ring*8;\n    for(let a=0;a<20;a++){ const th=a*Math.PI/10; CAND.push([Math.cos(th)*d*1.35,Math.sin(th)*d]); } }\n  drawOsmLayer(svg,pr,feats,NS);\n  labels.sort((a,b)=>(b.big?1:0)-(a.big?1:0)||((b.cbs||0)-(a.cbs||0)));\n  labels.forEach(L=>{\n    let chosen=null;\n    // Centred on the anchor is ALWAYS the first choice tried (CAND[0]=[0,0]), at full size. If the\n    // whole ringed search comes up empty, shrink the label once (82%) and search again — a smaller\n    // box clears crowded clusters no position at full size ever could.\n    for(const shrink of [1,0.82]){\n      const lines=shrink===1?L.lines:L.lines.map(l=>({...l,size:l.size*shrink}));\n      for(const off of CAND){\n        const x=L.anchor[0]+off[0], y=L.anchor[1]+off[1];\n        const r=rectOf(x,y,lines);\n        if(!inBounds(r)) continue;\n        if(hits(r)) continue;\n        if(Math.abs(off[0])>0.5||Math.abs(off[1])>0.5){\n          const cx=(r.x1+r.x2)/2, cy=(r.y1+r.y2)/2;\n          if(placed.some(q=>segRect(L.anchor[0],L.anchor[1],cx,cy,q))) continue;\n        }\n        chosen={x,y,r,off,lines}; break;\n      }\n      if(chosen) break;\n    }\n    // No slot anywhere — even shrunk — earns dropping the label entirely rather than stamping it on\n    // top of a neighbour. An absent label is legible; an overlapping one is exactly the mess this\n    // pass exists to remove, so it never gets a \"least-bad\" fallback the way it used to.\n    if(!chosen) return;\n    placed.push(chosen.r);\n    const g=document.createElementNS(NS,'g'); g.setAttribute('class','lbl'+(L.big?' big':''));\n    if(Math.abs(chosen.off[0])>0.5||Math.abs(chosen.off[1])>0.5){\n      // The leader stops at the label's edge rather than its centre, so it never runs under the glyphs.\n      const cx=(chosen.r.x1+chosen.r.x2)/2, cy=(chosen.r.y1+chosen.r.y2)/2;\n      const hw=(chosen.r.x2-chosen.r.x1)/2, hh=(chosen.r.y2-chosen.r.y1)/2;\n      const dx=L.anchor[0]-cx, dy=L.anchor[1]-cy;\n      const tt=Math.min(dx?hw/Math.abs(dx):Infinity, dy?hh/Math.abs(dy):Infinity);\n      const ln=document.createElementNS(NS,'line'); ln.setAttribute('class','lead');\n      ln.style.stroke='#1c1c1e'; ln.style.strokeOpacity='0.2';\n      // A largura do fio também é fixa em unidades do viewBox, igual o halo do texto — sem\n      // reescalar junto, no zoom alto ela vira uma barra grossa em vez de um fio fino.\n      ln.setAttribute('data-sw','0.8'); ln.style.strokeWidth='0.8px';\n      ln.setAttribute('x1',L.anchor[0].toFixed(1)); ln.setAttribute('y1',L.anchor[1].toFixed(1));\n      ln.setAttribute('x2',(cx+dx*tt).toFixed(1)); ln.setAttribute('y2',(cy+dy*tt).toFixed(1));\n      g.appendChild(ln);\n    }\n    const hTot=chosen.lines.reduce((a,l)=>a+l.size*1.2,0);\n    let ty=chosen.y-hTot/2+chosen.lines[0].size*0.86;\n    // Inline style rather than a class: a stylesheet rule would outrank a presentation attribute and\n    // undo the per-label contrast decision made above.\n    // One consistent style for every label, regardless of what's underneath — white text, black\n    // outline, always. Mixed ink/halo colors chosen per-fill (dark labels on dark fills, black labels\n    // on light ones) read as inconsistent across a crowded map; a single style reads as one system.\n    const ink='#fff', inkMut='rgba(255,255,255,.85)';\n    const halo='rgba(0,0,0,.72)';\n    // Halo do texto (o \"contorno\" que garante legibilidade sobre qualquer cor de fundo) também é\n    // fixo em unidades do viewBox via .lbl text{stroke-width}/.lbl.big text{stroke-width} — sem\n    // reescalar junto com o font-size, no zoom alto ele vira uma mancha escura cobrindo a letra\n    // inteira em vez de um contorno fino, exatamente o \"Setor 3570\" ilegível reportado.\n    const swBase=L.big?2.4:1.8;\n    chosen.lines.forEach(l=>{ const t=document.createElementNS(NS,'text');\n      t.setAttribute('x',chosen.x.toFixed(1)); t.setAttribute('y',ty.toFixed(1));\n      t.setAttribute('font-size',l.size); t.setAttribute('data-fs',l.size); t.setAttribute('font-weight',l.weight||600);\n      t.setAttribute('data-sw',swBase); t.style.strokeWidth=swBase+'px';\n      t.style.fill=l.mut?inkMut:ink; t.style.stroke=halo;\n      t.textContent=l.t; g.appendChild(t); ty+=l.size*1.2; });\n    svg.appendChild(g);\n  });\n  const rejectedLegend=(!asis && mv.gvRejected && mv.gvRejected.length)\n    ? `<span><b style=\"background:#e4e4ea;border:1px solid #9797a3\"></b>fora da hierarquia (${mv.gvRejected.length}) — passe o mouse no mapa</span>`\n    : '';\n  $('#legend').innerHTML=asis\n    ?`<span><b style=\"background:${CLOSE_COLOR}\"></b>${LEVELS[level].label} ${mv.sector} (estrutura antes desta etapa — ${level==='setor'?Object.keys(mv.geoFeat).length+' municípios':Object.keys(mv.unitMunis||{}).length+' '+LEVELS[level].childLabel.toLowerCase()+'(s)'})</span>`\n    // \"vizinhos (contexto)\"/\"setor já fechado antes\" (.sect.plain/.sect.closed-ctx) still never\n    // legend — those stay dead. \"rejected\" is the one gray key that's real again: it marks a\n    // geographically-touching setor/GV excluded for failing the hierarchy rule, appended below.\n    :mv.dest.map(s=>`<span><b style=\"background:${mv.color[s]}\"></b>${LEVELS[level].destLabel} ${s} — ${c.gained[s]} líder(es)</span>`).join('')+rejectedLegend;\n  // Municipalities outside the dominance base are deliberately NOT drawn — no shape, no hover, no\n  // legend entry. They are reported in the panel instead, so the map only ever shows territory that\n  // is actually being reallocated.\n  if(ignNote) ignNote.innerHTML=(mv.excludedMunis&&mv.excludedMunis.length)\n    ? `<b style=\"color:var(--closing)\">${fmt(mv.excludedMunis.length)} município(s) / ${fmt(mv.excludedCbs||0)} CBs</b> fora da base de dominantes deste setor não são desenhados no mapa — as CBs seguem a líder do seu grupo (detalhe em “Municípios → setor destino”).`\n    : '';\n  applyMapView();\n}\nasync function runSql(){const sql=$('#sqlIn').value.trim();$('#sqlStatus').textContent='rodando…';$('#sqlStatus').classList.remove('err');\n  try{ await ensurePinned(); const resp=await q(sql);const rs=rows(resp);const cols=(resp.manifest?.schema?.columns||[]).map(c=>c.name);\n    $('#sqlOut').innerHTML=`<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`+rs.slice(0,200).map(r=>`<tr>${cols.map(c=>`<td>${r[c]??'·'}</td>`).join('')}</tr>`).join('')+`</tbody>`;\n    $('#sqlStatus').textContent=rs.length+' linha(s)';}\n  catch(e){$('#sqlStatus').textContent='bloqueado/erro: '+e.message;$('#sqlStatus').classList.add('err');}\n}\nfunction renderAll(){\n  renderStage();\n  fitMapHeight();\n  renderTimeline(); renderSectorPicker();\n  const shb=$('#openShare'); if(shb) shb.disabled=!SESS.timeline.length;\n  const dpb=$('#dupSession'); if(dpb) dpb.disabled=!SESS.timeline.length;\n  const rnb=$('#renameSession'); if(rnb) rnb.disabled=!SESS.timeline.length;\n  const mv=SESS.timeline[SESS.selected];\n  $('#liveState').textContent = SESS.snapshotRaw\n    ? 'snapshot atualizado em '+fmtSyncedAt(SESS.snapshotSyncedAt)\n    : '● iniciando';\n  renderRuleNote(); renderSpanRule();\n  if(!mv){ ['#map','#matrix','#matrixTobe','#tobeNote','#leaders'].forEach(s=>{const el=$(s);if(el)el.innerHTML='';}); $('#kpis').innerHTML=''; $('#spans').innerHTML=''; $('#muniList').innerHTML=''; $('#plan').innerHTML=''; return; }\n  $('#viewToggle').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.v===mv.view));\n  $('#policy').querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===mv.policy));\n  $('#optBadge').textContent=mv.optBadge||'';\n  drawMap(); renderContig(); renderSpanRule(); renderKpis(); renderSpans(); renderMuni(); renderLeaders(); renderMatrix(); renderMatrixTobe(); renderPolicy(); renderPlan(); renderReport(); renderCumulative();\n}\n\n// ---------- event wiring ----------\nwindow.addEventListener('resize', ()=>{ fitMapHeight(); drawMap(); });\n$('#confirmSnap').onclick=()=>{ SESS.stage='main'; renderAll(); };\n$('#openMove').onclick=openMove;\n$('#moveClose').onclick=closeMove;\n$('#moveCancel').onclick=closeMove;\n$('#moveBg').addEventListener('click',e=>{ if(e.target.id==='moveBg') closeMove(); });\nroot.querySelectorAll('input[name=\"moveLevel\"]').forEach(r=>r.addEventListener('change',renderMoveForm));\n$('#moveRand').onclick=moveRandPick;\n$('#moveGo').onclick=submitMove;\n$('#moveCode').addEventListener('keydown',e=>{ if(e.key==='Enter') submitMove(); });\n$('#confirmSessionPick').onchange=()=>loadSession($('#confirmSessionPick').value);\n$('#optim').onclick=()=>editSelected(mv=>{ mv.assign=optimizeFor(mv); });\n$('#autoTobe').onclick=()=>editSelected(mv=>{ mv.assign=autoAssignFor(mv); mv.optBadge='TOBE por adjacência'; });\n$('#viewToggle').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.view=b.dataset.v;renderAll();});\n$('#mapZoomIn').onclick=()=>zoomMapBy(1.4);\n$('#mapZoomOut').onclick=()=>zoomMapBy(1/1.4);\n// Rodovias é só um atalho de \"marcar/desmarcar todos os níveis viários de uma vez\" — o estado real\n// de cada nível continua vivendo em OSM_ON por chave individual, então esconder um nível depois de\n// carregado é só desmarcar aquele checkbox, sem afetar os outros nem precisar recarregar nada.\nfunction syncRodoviasMaster(){\n  const master=$('#osmRodovias'); if(!master) return;\n  const on=HIGHWAY_KEYS.map(k=>!!OSM_ON[k]);\n  const allOn=on.every(Boolean), allOff=on.every(v=>!v);\n  master.checked=allOn; master.indeterminate=!allOn&&!allOff;\n}\n['motorway','motorwayLink','trunk','trunkLink','primary','primaryLink','secondary','secondaryLink','rivers'].forEach(k=>{ const el=$('#osm'+k[0].toUpperCase()+k.slice(1)); if(el) el.onchange=()=>{ OSM_ON[k]=el.checked; syncRodoviasMaster(); drawMap(); }; });\nconst osmRodoviasEl=$('#osmRodovias');\nif(osmRodoviasEl) osmRodoviasEl.onchange=()=>{\n  const on=osmRodoviasEl.checked;\n  HIGHWAY_KEYS.forEach(k=>{ OSM_ON[k]=on; const el=$('#osm'+k[0].toUpperCase()+k.slice(1)); if(el) el.checked=on; });\n  osmRodoviasEl.indeterminate=false;\n  drawMap();\n};\n['muni','ctx'].forEach(k=>{ const el=$('#legend'+(k==='muni'?'Muni':'Ctx')); if(el) el.onchange=()=>{ LEGEND_ON[k]=el.checked; drawMap(); }; });\n$('#map').addEventListener('pointerdown',mapPointerDown);\n$('#map').addEventListener('wheel',e=>{ e.preventDefault(); zoomMapAt(e.deltaY<0?1.15:1/1.15, e.clientX, e.clientY); }, {passive:false});\nroot.addEventListener('pointermove',mapPointerMove);\nroot.addEventListener('pointerup',mapPointerUp);\nroot.addEventListener('pointerleave',mapPointerUp);\n// Arrastar não pode virar \"cliquei num município e troquei o destino sem querer\": se o ponteiro se\n// moveu além do limiar de arrasto, este clique (capture, antes de chegar no <path>) é engolido.\n$('#map').addEventListener('click',e=>{ if(mapDidDrag){ e.stopPropagation(); e.preventDefault(); mapDidDrag=false; } },true);\n$('#policy').querySelectorAll('button').forEach(b=>b.onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;mv.policy=b.dataset.p;\n  $('#policy').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));\n  renderKpis();renderPolicy();renderPlan();});\nroot.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{root.querySelectorAll('.tabs button').forEach(x=>x.classList.toggle('on',x===b));\n  ['matrix','tobe','report','cumulative','sql'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\n$('#runSql').onclick=runSql;\n$('#copyPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;navigator.clipboard.writeText(JSON.stringify({setor:mv.sector,politica:mv.policy,plano:mv._plan},null,2));$('#copyPlan').textContent='copiado ✓';setTimeout(()=>$('#copyPlan').textContent='Copiar',1500);};\n$('#csvPlan').onclick=()=>{const mv=SESS.timeline[SESS.selected];if(!mv)return;const csv='municipio,uf,cbs,setor_destino\\n'+mv._plan.map(p=>`${p.municipio},${p.uf||''},${p.cbs},${p.destino}`).join('\\n');\n  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`plano_migracao_setor_${mv.sector}.csv`;a.click();};\n$('#saveSession').onclick=saveSession;\n$('#dupSession').onclick=duplicateSession;\n$('#renameSession').onclick=renameSession;\n$('#sessionPick').onchange=()=>loadSession($('#sessionPick').value);\n$('#liveState').onclick=refreshSnapshot;\n$('#liveState').addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); refreshSnapshot(); } });\n$('#openOptions').onclick=openOptions;\n$('#optClose').onclick=closeOptions;\n$('#optCancel').onclick=closeOptions;\n$('#optApply').onclick=applyOptions;\n['setor','gv'].forEach(lvl=>['#optTargetSpan_','#optMaxSpan_','#optMinSpan_'].forEach(pfx=>$(pfx+lvl).addEventListener('input',renderOptPreview)));\n$('#optionsBg').addEventListener('click',e=>{ if(e.target.id==='optionsBg') closeOptions(); });\nroot.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeOptions(); closeShare(); closeMove(); } });\n\n$('#openShare').onclick=openShare;\n$('#shareX').onclick=closeShare;\n$('#shareCancel').onclick=closeShare;\n$('#shareGo').onclick=doShare;\n$('#shareCopy').onclick=()=>{ if(!SHARE.url) return; navigator.clipboard.writeText(SHARE.url);\n  const b=$('#shareCopy'); if(b){ b.textContent='copiado ✓'; setTimeout(()=>{ const x=$('#shareCopy'); if(x) x.textContent='Copiar link'; },1500); } };\n$('#shareOpen').onclick=()=>{ if(SHARE.url) window.open(SHARE.url,'_blank'); };\n$('#shareBg').addEventListener('click',e=>{ if(e.target.id==='shareBg') closeShare(); });\n\n// ---------- init ----------\n(async()=>{\n  loader(true,'congelando snapshot da sessão…');\n  try{\n    await Promise.all([ensurePinned(), loadSectors(), refreshSessionPicker(), loadMuniAliases()]);\n    SESS.stage='confirm';\n  }catch(e){ setError('erro init: '+e.message); }\n  finally{ loader(false); renderAll(); const rd=$('#ready'); if(rd) rd.setAttribute('data-loaded','1'); }\n})();\n\n  }\n}\n"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}},"dominantesCsvUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dominantesCsvUrl"}}}}}}}}}},{"_id":"6a7f3001bbce4725b9b8a950","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"CNL_4z20SJcX0NMp2I2vz","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\n\nexport default {\n  css: `\n  :host{display:block;height:100%}\n  *{box-sizing:border-box}\n  .rroot{--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--closing:#ff3b30;--ok:#34c759;--warn:#ff9500;\n    height:100%;overflow:auto;background:var(--bg);color:var(--ink);\n    font:13px/1.5 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;-webkit-font-smoothing:antialiased}\n  .shell{max-width:1080px;margin:0 auto;padding:26px 22px 70px}\n  header.top{margin-bottom:20px}\n  .eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);font-weight:600}\n  h1{font-size:25px;font-weight:600;letter-spacing:-.5px;margin:7px 0 10px}\n  .pills{display:flex;gap:7px;flex-wrap:wrap;align-items:center}\n  .pill{font-size:10.5px;padding:3px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel)}\n  .pill b{color:var(--ink);font-weight:600}\n  .pill.go{cursor:pointer;border-color:var(--acc);color:var(--acc);background:#eaf3ff;font-weight:600}\n  .pill.go:hover{background:#dbe9ff}\n  .goact{margin-top:15px;background:var(--acc);color:#fff;border:1px solid transparent;border-radius:8px;\n    padding:9px 16px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer}\n  .goact:hover{background:#0060df}\n  .strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:18px 0 22px}\n  .st{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .st .n{font-size:23px;font-weight:700;letter-spacing:-.5px}\n  .st .l{font-size:10.5px;color:var(--mut);margin-top:3px;line-height:1.4}\n  .st.warn .n{color:var(--closing)}\n  .stepper{position:sticky;top:0;z-index:5;display:flex;gap:7px;overflow-x:auto;padding:11px 0;margin-bottom:6px;\n    background:rgba(245,245,247,.9);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}\n  .sc{flex:0 0 auto;display:flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);\n    border-radius:99px;padding:5px 12px 5px 6px;cursor:pointer;font-size:12px}\n  .sc:hover{border-color:var(--acc)}\n  .sc .k{width:19px;height:19px;border-radius:99px;background:var(--ink);color:#fff;font-size:10px;font-weight:700;\n    display:flex;align-items:center;justify-content:center}\n  .sc .arrow{color:var(--mut)}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:16px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card > h2{font-size:15px;font-weight:600;letter-spacing:-.2px;margin:0 0 3px}\n  .card .sub{font-size:11.5px;color:var(--mut);margin:0 0 15px}\n  h3{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:20px 0 9px;font-weight:600}\n  h3:first-of-type{margin-top:0}\n  .split{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:20px;align-items:start}\n  @media(max-width:820px){.split{grid-template-columns:1fr}}\n  svg.map{width:100%;height:auto;display:block;background:var(--panel2);border-radius:10px;border:1px solid var(--line);cursor:grab;touch-action:none}\n  svg.map.dragging{cursor:grabbing}\n  .map-wrap{position:relative}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 2px 8px rgba(0,0,0,.14);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  .muni{stroke:#fff;stroke-width:1.1}\n  .nbr{fill:#e8e8ec;stroke:#d4d4d9;stroke-width:.7}\n  .exc{fill:#f7e7d0;stroke:var(--warn);stroke-width:.9;stroke-dasharray:2 2}\n  .mlabel{fill:#1c1c1e;font-size:9.5px;font-weight:600;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.7);stroke-width:2.4px}\n  .nlabel{fill:#9a9aa0;font-size:7.5px;text-anchor:middle}\n  .legend{display:flex;gap:11px;flex-wrap:wrap;margin-top:9px;font-size:11px;color:var(--mut)}\n  .legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .bar{display:flex;align-items:center;gap:9px;margin:7px 0}\n  .bar .tag{width:46px;font-weight:600;font-size:12px}\n  .bar .track{flex:1;height:17px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex}\n  .bar .b1{height:100%;opacity:.38}.bar .b2{height:100%}.bar .b3{height:100%;opacity:.7}\n  .bar .v{width:120px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  .mrow{display:flex;align-items:center;gap:9px;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:6px 10px;margin-bottom:5px}\n  .mrow .nm{flex:1;font-weight:600;font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .mrow .cb{color:var(--mut);font-size:11px;font-variant-numeric:tabular-nums}\n  .chip{font-size:11px;font-weight:700;color:#fff;border-radius:6px;padding:2px 8px}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:6px 7px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px}\n  .dot{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .note{font-size:10.5px;color:var(--mut);line-height:1.55;margin-top:9px}\n  .flag{margin-top:11px;padding:9px 11px;border-radius:9px;font-size:11.5px;line-height:1.55;background:#fff8ef;border:1px solid #ffe0bd;color:#8a5200}\n  .scroll{max-height:300px;overflow:auto}\n  .center{display:flex;align-items:center;justify-content:center;height:100%;padding:40px}\n  .msg{max-width:420px;text-align:center}\n  .msg h2{font-size:17px;font-weight:600;margin:0 0 7px}\n  .msg p{font-size:12.5px;color:var(--mut);margin:0;line-height:1.55}\n`,\n  render(d2){\n    // Hierarquia mercado > GV > setor, espelhando o simulador ao vivo. Tudo pré-computado (label,\n    // minúscula, plural, gênero) pra nunca minusculizar \"GV\" (sigla) virando \"gv\", e pra concordar\n    // gênero certo em cada frase — GV é feminina (a Gerência de Vendas), o resto é masculino.\n    const LVL={\n      setor:{label:'Setor',low:'setor',fem:false, child:'Município',childLow:'município',childPlural:'Municípios',childFem:false, dest:'Setor',destLow:'setor'},\n      gv:{label:'GV',low:'GV',fem:true, child:'Setor',childLow:'setor',childPlural:'Setores',childFem:false, dest:'GV',destLow:'GV'},\n      mercado:{label:'Mercado',low:'mercado',fem:false, child:'GV',childLow:'GV',childPlural:'GVs',childFem:true, dest:'Mercado',destLow:'mercado'},\n    };\n    const lv=s=>LVL[(s&&s.level)||'setor'];\n    const esc=s=>(s==null?'':String(s)).replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n    const fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\n    const money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\n    const msg=(t,b,act)=>`<div class=\"rroot\"><div class=\"center\"><div class=\"msg\"><h2>${t}</h2><p>${b}</p>${act?`<button class=\"goact\" id=\"goSim\">Abrir o simulador</button>`:''}</div></div></div>`;\n\n    if(d2.state.error) return msg('Não foi possível abrir a sessão',esc(d2.state.error),true);\n    if(!d2.state.ready) return msg('Carregando sessão…','Buscando o registro compartilhado.');\n    const r=d2.state.rec;\n    if(!r) return msg('Sessão não encontrada','O link pode ter expirado ou o registro foi removido. Peça um novo link a quem compartilhou.',true);\n\n    const stages=Array.isArray(r.stages)?r.stages:[];\n    const sm=r.summary||{};\n    const st=r.settings||{};\n    const ruleDest=st.destRule==='dominant'?`destino: dominantes ≥${fmt(st.destMinCbs)} CBs`:'destino: qualquer município';\n    const ruleReal=st.realocRule==='relevant'?`realocação: ≥${fmt(st.realocMinCbs)} CBs`:'realocação: todos';\n    const spanBits=[];\n    if(st.targetSpan)spanBits.push('alvo '+fmt(st.targetSpan));\n    if(st.maxSpan)spanBits.push('máx '+fmt(st.maxSpan));\n    if(st.minSpan)spanBits.push('mín '+fmt(st.minSpan));\n\n    let h=`<div class=\"rroot\"><div class=\"shell\">\n      <header class=\"top\">\n        <div class=\"eyebrow\">Simulador de timeline de fechamentos · sessão compartilhada</div>\n        <h1>${esc(r.label||'Sessão sem nome')}</h1>\n        <div class=\"pills\">\n          <span class=\"pill\">● snapshot congelado · <b>${esc(r.snapshotRaw||'—')}</b></span>\n          <span class=\"pill\">◇ ${esc(ruleReal)}</span>\n          <span class=\"pill\">◈ ${esc(ruleDest)}</span>\n          <span class=\"pill\">◎ span: <b>${spanBits.length?esc(spanBits.join(' · ')):'livre'}</b></span>\n          ${r.sharedBy?`<span class=\"pill\">por <b>${esc(r.sharedBy)}</b></span>`:''}\n          ${r.sharedAt?`<span class=\"pill\">${esc(new Date(r.sharedAt).toLocaleString('pt-BR'))}</span>`:''}\n          <span class=\"pill go\" id=\"goSim\" role=\"button\" tabindex=\"0\" title=\"Ir para o simulador\">↗ abrir o simulador</span>\n        </div>\n      </header>`;\n\n    const sessLevel=(stages[0]&&stages[0].level)||'setor';\n    h+=`<div class=\"strip\">\n      <div class=\"st\"><div class=\"n\">${fmt(stages.length)}</div><div class=\"l\">Etapa(s) de fechamento</div></div>\n      <div class=\"st\"><div class=\"n\">${fmt(sm.totalCbs)}</div><div class=\"l\">CBs realocadas na cadeia</div></div>\n      ${sessLevel==='setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs deslocadas (quebra de relação)</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs com líder dividida entre destinos</div></div>`}\n      <div class=\"st\"><div class=\"n\" style=\"font-size:15px;line-height:1.5;font-weight:600\">${esc((sm.sectors||[]).join(' → ')||'—')}</div><div class=\"l\">Ordem de fechamento</div></div>\n    </div>`;\n\n    if(stages.length>1){\n      h+=`<div class=\"stepper\">`+stages.map((s,i)=>`<div class=\"sc\" data-goto=\"stage-${i}\"><span class=\"k\">${i+1}</span>${lv(s).label} ${esc(s.sector)}<span class=\"arrow\">→</span>${esc((s.dest||[]).join(', ')||'–')}</div>`).join('')+`</div>`;\n    }\n\n    // ---- projection helpers (pure; mirrors the simulator's) ----\n    const project=feats=>{let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n      feats.forEach(f=>{const g=f&&f.geometry;if(!g)return;const ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n        ps.forEach(p=>p.forEach(rg=>rg.forEach(pt=>{mnx=Math.min(mnx,pt[0]);mxx=Math.max(mxx,pt[0]);mny=Math.min(mny,pt[1]);mxy=Math.max(mxy,pt[1]);})));});\n      if(!isFinite(mnx)) return null;\n      const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=680,H=440,pad=16;\n      const gw=Math.max((mxx-mnx)*kx,1e-9),gh=Math.max(mxy-mny,1e-9),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n      const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n      return {W,H,f:pt=>[ox+(pt[0]-mnx)*kx*sc, oy+(mxy-pt[1])*sc]};};\n    const pathFor=(f,pr)=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      return ps.map(poly=>poly.map(rg=>'M'+rg.map(pt=>{const q=pr(pt);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');};\n    const cxy=(f,pr)=>{let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      ps.forEach(poly=>poly[0].forEach(pt=>{const q=pr(pt);x+=q[0];y+=q[1];n++;}));return n?[x/n,y/n]:[0,0];};\n\n    stages.forEach((s,i)=>{\n      const color=s.color||{};\n      const k=s.kpis||{};\n      const LV=lv(s);\n      h+=`<div class=\"card\" id=\"stage-${i}\" data-key=\"stage-${i}\">\n        <h2>Etapa ${i+1} · ${LV.low} <b>${esc(s.sector)}</b> fechad${LV.fem?'a':'o'}</h2>\n        <p class=\"sub\">Território redistribuído para ${esc((s.dest||[]).join(', ')||'—')}${s.domUF?' · '+esc(s.domUF):''}${s.policy?' · política: '+(s.policy==='fora'?'manter vínculo':'quebrar relação'):''}</p>\n        <div class=\"strip\" style=\"margin:0 0 18px\">\n          <div class=\"st\"><div class=\"n\">${fmt(k.cbsRealocadas)}</div><div class=\"l\">CBs realocadas</div></div>\n          ${LV.label==='Setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs deslocadas</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs com líder dividida</div></div>`}\n          <div class=\"st\"><div class=\"n\">${fmt(k.municipios)}</div><div class=\"l\">${LV.childPlural} movid${LV.childFem?'as':'os'}</div></div>\n          <div class=\"st\"><div class=\"n\">${fmt(k.lideres)}</div><div class=\"l\">Líderes envolvidas</div></div>\n        </div>\n        <div class=\"split\"><div>`;\n\n      // map\n      const gf=s.geoFeat||{}, nb=Array.isArray(s.nbrFeat)?s.nbrFeat:[], ex=Array.isArray(s.excludedFeat)?s.excludedFeat:[];\n      const all=[...nb,...ex,...Object.values(gf)].filter(Boolean);\n      const P=project(all);\n      const unitOf=m=>LV.label==='Setor'?m:((s.unitOfMuni||{})[m]||m);\n      if(P){\n        let g='';\n        nb.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"nbr\" d=\"${pathFor(f,P.f)}\"/>`;\n          const c=cxy(f,P.f);const nm=(f.properties&&f.properties.name)||'';\n          if(nm) g+=`<text class=\"nlabel\" data-fs=\"7.5\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(nm.slice(0,13))}</text>`; });\n        ex.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"exc\" d=\"${pathFor(f,P.f)}\"/>`; });\n        if(LV.label==='Setor'){\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return;\n            const dest=(s.assign||{})[m];\n            g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${esc(color[dest]||'#8e8e93')}\"/>`;\n            const c=cxy(f,P.f);\n            g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(m.slice(0,14))}<tspan data-fs=\"8\" x=\"${c[0].toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`; });\n        } else {\n          // GV/mercado: vários municípios do mesmo setor/GV são UMA unidade só na tela — mesma cor,\n          // mesmo stroke (a costura entre elas some), um rótulo só no centro do grupo em vez de um\n          // por município. Espelha o mapa ao vivo do simulador.\n          const groups={};\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return; const u=unitOf(m); (groups[u]=groups[u]||[]).push(f); });\n          Object.entries(groups).forEach(([u,feats])=>{\n            const dest=(s.assign||{})[u]; const col=esc(color[dest]||'#8e8e93');\n            let cx=0,cy=0,n=0;\n            feats.forEach(f=>{ g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${col}\" stroke=\"${col}\"/>`;\n              const c=cxy(f,P.f); cx+=c[0]; cy+=c[1]; n++; });\n            if(n){ cx/=n; cy/=n;\n              g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${cx.toFixed(1)}\" y=\"${cy.toFixed(1)}\">${esc(u)}<tspan data-fs=\"8\" x=\"${cx.toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`;\n            }\n          });\n        }\n        h+=`<h3>Mapa da redistribuição</h3><div class=\"map-wrap\">\n          <svg class=\"map\" id=\"map-${i}\" data-w=\"${P.W}\" data-h=\"${P.H}\" viewBox=\"0 0 ${P.W} ${P.H}\" preserveAspectRatio=\"xMidYMid meet\">${g}</svg>\n          <div class=\"zoom-pill\"><button type=\"button\" data-zoom=\"out\" data-i=\"${i}\" title=\"Diminuir zoom\">−</button><span class=\"zoom-div\"></span><button type=\"button\" data-zoom=\"in\" data-i=\"${i}\" title=\"Aumentar zoom\">+</button></div>\n          </div>\n          <div class=\"legend\">${(s.dest||[]).map(d=>`<span><i style=\"background:${esc(color[d]||'#8e8e93')}\"></i>${LV.dest} ${esc(d)}</span>`).join('')}\n          <span><i style=\"background:#e8e8ec;border:1px solid #d4d4d9\"></i>vizinhos (contexto)</span>\n          ${ex.length?`<span><i style=\"background:#f7e7d0;border:1px dashed #ff9500\"></i>fora da realocação</span>`:''}</div>`;\n      }\n      h+=`<h3>${LV.childPlural} → ${LV.destLow} destino</h3>`;\n      h+=(s.municipios||[]).map(m=>`<div class=\"mrow\"><span class=\"nm\">${esc(m.nome)}</span><span class=\"cb\">${fmt(m.cbs)} CBs</span>\n        <span class=\"chip\" style=\"background:${esc(color[m.destino]||'#8e8e93')}\">${esc(m.destino||'–')}</span></div>`).join('')||`<div class=\"note\">Nenhum${LV.childFem?'a':''} ${LV.childLow} realocad${LV.childFem?'a':'o'} nesta etapa.</div>`;\n\n      h+=`</div><div>`;\n      // spans\n      const ds=s.destinos||[];\n      const mx=Math.max(...ds.map(d=>+d.span||0),1);\n      h+=`<h3>Tamanho d${LV.fem?'a':'o'} ${LV.destLow} depois da etapa</h3>`;\n      h+=ds.map(d=>{const base=+d.base||0,inc=+d.inc||0,orph=+d.orph||0;\n        return `<div class=\"bar\"><span class=\"tag\" style=\"color:${esc(color[d.s]||'#8e8e93')}\">${esc(d.s)}</span>\n          <span class=\"track\"><span class=\"b1\" style=\"width:${(base/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span><span class=\"b2\" style=\"width:${(inc/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>${orph?`<span class=\"b3\" style=\"width:${(orph/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>`:''}</span>\n          <span class=\"v\">${fmt(d.span)} <span style=\"opacity:.6\">(+${fmt(inc+orph)})</span></span></div>`;}).join('')||`<div class=\"note\">Sem ${LV.destLow} destino nesta etapa.</div>`;\n      h+=`<div class=\"note\">Claro = base antes da etapa · cheio = CBs entrantes${(s.orphanTotal?' · faixa final = CBs que seguiram suas líderes':'')}.</div>`;\n\n      // leaders\n      const L=s.lideres||[];\n      if(L.length){\n        const showOrph=L.some(x=>+x.orphan>0);\n        h+=`<h3>Líderes</h3><div class=\"scroll\"><table><thead><tr><th>Líder</th><th>CBs</th><th>${LV.dest} final</th>${showOrph?'<th>Órfãs</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n          +L.map(x=>`<tr><td>${esc(x.lider||'—')}</td><td>${fmt(x.total)}</td>\n            <td>${x.home?`<span class=\"dot\" style=\"background:${esc(color[x.home]||'#8e8e93')}\"></span>${esc(x.home)}`:'–'}</td>\n            ${showOrph?`<td>${x.orphan?fmt(x.orphan):'·'}</td>`:''}\n            <td style=\"color:${+x.desloc?'var(--closing)':'var(--mut)'};font-weight:${+x.desloc?700:400}\">${fmt(x.desloc||0)}</td></tr>`).join('')\n          +`</tbody></table></div>`;\n      }\n      h+=`</div></div>`;\n\n      // antes -> depois\n      const M=s.metricas||[];\n      if(M.length){\n        h+=`<h3>Impacto antes → depois por ${LV.destLow} ${LV.fem?'receptora':'receptor'}</h3><div class=\"scroll\"><table><thead><tr><th>${LV.dest}</th>${M.map(m=>`<th>${esc(m.label)}</th>`).join('')}</tr></thead><tbody>`;\n        (s.dest||[]).forEach(sec=>{\n          h+=`<tr><td><span class=\"dot\" style=\"background:${esc(color[sec]||'#8e8e93')}\"></span>${esc(sec)}</td>`;\n          h+=M.map(m=>{const row=(m.rows||[]).find(x=>String(x.s)===String(sec))||{};\n            const a=+row.antes||0,b=+row.depois||0,d=a?((b-a)/a*100):0;const f=m.money?money:fmt;\n            return `<td>${f(b)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;\n        });\n        h+=`<tr style=\"opacity:.75\"><td>${esc(s.sector)} (sai)</td>`+M.map(m=>`<td>${(m.money?money:fmt)(m.sai||0)}</td>`).join('')+`</tr>`;\n        h+=`</tbody></table></div>`;\n      }\n\n      // caveats\n      const fl=[];\n      if(s.excluidos&&s.excluidos.length) fl.push(`<b>${fmt(s.excluidos.length)} município(s)</b> com poucas CBs ficaram fora da realocação geográfica (quebra de vínculo): ${esc(s.excluidos.slice(0,6).map(x=>x.muni+' ('+x.cbs+')').join(', '))}${s.excluidos.length>6?' +'+(s.excluidos.length-6):''}. As CBs seguiram a líder do seu grupo.`);\n      if(s.orphanStranded) fl.push(`<b>${fmt(s.orphanStranded)} CB(s)</b> sem setor a seguir — a líder não ficou com nenhum município realocado.`);\n      if(s.noShapeCbs) fl.push(`<b>${fmt(s.noShapeCbs)} CB(s)</b> em municípios sem shape na UF, fora do mapa e de qualquer setor destino.`);\n      if(s.optTier===1) fl.push(`O span mínimo não era atingível nesta etapa e foi relaxado pelo modelo.`);\n      if(s.optTier===2) fl.push(`Os limites de span não eram atingíveis nesta etapa e foram relaxados pelo modelo.`);\n      if(s.optTier===-1) fl.push(`Etapa resolvida por heurística (espaço de busca grande) — os limites de span não são garantidos.`);\n      if(s.destRuleFallback) fl.push(`Nenhum município dominante nesta etapa: a regra de destino foi relaxada para \"qualquer município\".`);\n      if(s.realocFallback) fl.push(`Todos os municípios ficariam abaixo do limite: a regra de realocação foi relaxada para \"realocar todos\".`);\n      if(fl.length) h+=`<div class=\"flag\">⚠ ${fl.join('<br>')}</div>`;\n      h+=`</div>`;\n    });\n\n    // cumulative\n    // Este resumo é calculado no rastro de município (world.muniOverride) — hoje só existe pro\n    // fechamento de setor; GV/mercado têm o rastro equivalente (secOverride/gvOverride) mas essa\n    // agregação ainda não foi refeita nesse grão, então avisa em vez de mostrar tabela vazia.\n    const rc=sm.receivers||[];\n    if(!rc.length && sessLevel!=='setor'){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"note\" style=\"margin-top:0\">Ainda não disponível pra sessões de ${sessLevel==='gv'?'GV':'mercado'} — veja o relatório de cada etapa acima.</p></div>`;\n    }\n    if(rc.length){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"sub\">Efeito de <b>todas</b> as ${fmt(stages.length)} etapa(s) somadas, contra o snapshot congelado sem nenhum fechamento aplicado.</p>\n        <div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th><th>CBs antes</th><th>CBs depois</th><th>Variação</th></tr></thead><tbody>`\n        +rc.map(x=>{const a=+x.antes||0,b=+x.depois||0,d=a?((b-a)/a*100):0;\n          return `<tr><td>${esc(x.s)}</td><td>${esc(x.situacao||'recebeu território')}</td><td>${fmt(a)}</td><td>${fmt(b)}</td>\n            <td class=\"delta ${d>=0?'up':'dn'}\" style=\"font-size:11.5px\">${d>0?'+':''}${d.toFixed(0)}%</td></tr>`;}).join('')\n        +`</tbody></table></div></div>`;\n    }\n\n    h+=`<div class=\"note\" style=\"text-align:center;margin-top:22px\">Visualização somente leitura de uma simulação congelada. Os números refletem o snapshot <b>${esc(r.snapshotRaw||'—')}</b> e as regras vigentes no momento do compartilhamento — não acompanham o dado ao vivo.</div>`;\n    return h+`</div></div>`;\n  },\n  mounted(d2, root){\n    d2.on('#goSim','click',()=>{\n      try{ (d2.router('timelineSimRouter')||d2.router()).goTo('/'); }catch(e){ try{ d2.router().goTo('/'); }catch(e2){} }\n    });\n    d2.on('.sc','click',e=>{\n      const el=e.target.closest('.sc'); if(!el) return;\n      const t=d2.$('#'+el.dataset.goto); if(t) t.scrollIntoView({behavior:'smooth',block:'start'});\n    });\n    // ---- map zoom/pan (viewBox só — texto e polígonos vivem no mesmo sistema de coordenadas, então\n    // escalam juntos automaticamente ao encolher/esticar o viewBox, sem lógica extra pra fonte).\n    // Delegado em `root` (nunca substituído por re-render) em vez de nos <svg> por id, porque cada\n    // atualização de estado reconstrói o HTML das etapas do zero.\n    const mapViews={};\n    const getMapView=(i,W,H)=>mapViews[i]||(mapViews[i]={x:0,y:0,w:W,h:H});\n    const applyMapView=(svg,i)=>{ const v=mapViews[i]; if(!v) return;\n      svg.setAttribute('viewBox', v.x.toFixed(2)+' '+v.y.toFixed(2)+' '+v.w.toFixed(2)+' '+v.h.toFixed(2));\n      // Rótulo tem tamanho em unidades do viewBox — sem isso ele cresceria junto com o zoom até virar\n      // um borrão. Encolher na mesma proporção que o viewBox encolhe cancela o zoom: o texto fica do\n      // mesmo tamanho na tela em qualquer nível. style.fontSize (não o atributo) porque uma regra de\n      // classe como .mlabel{font-size:...} outrankaria um atributo comum.\n      const W=+svg.dataset.w||700;\n      const fsScale=v.w/W;\n      svg.querySelectorAll('[data-fs]').forEach(t=>{\n        const base=+t.getAttribute('data-fs')||8;\n        t.style.fontSize=(base*fsScale).toFixed(2)+'px';\n      });\n      // Halo do texto (contorno branco que garante legibilidade sobre qualquer cor de fundo) tem\n      // largura de traço fixa em unidades do viewBox — sem reescalar junto, no zoom alto ele vira\n      // uma mancha cobrindo a letra inteira em vez de um contorno fino.\n      svg.querySelectorAll('[data-sw]').forEach(t=>{\n        const base=+t.getAttribute('data-sw')||1;\n        t.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n      });\n    };\n    const zoomStageMap=(i,factor)=>{\n      const svg=root.querySelector('#map-'+i); if(!svg) return;\n      const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      const cx=v.x+v.w/2, cy=v.y+v.h/2;\n      let w=v.w/factor, h=v.h/factor;\n      w=Math.min(W, Math.max(W/8, w)); h=Math.min(H, Math.max(H/8, h));\n      let x=cx-w/2, y=cy-h/2;\n      x=Math.min(W-w, Math.max(0,x)); y=Math.min(H-h, Math.max(0,y));\n      mapViews[i]={x,y,w,h};\n      applyMapView(svg,i);\n    };\n    d2.on('.zoom-pill button','click',e=>{\n      const b=e.target.closest('button'); if(!b) return;\n      zoomStageMap(b.dataset.i, b.dataset.zoom==='in'?1.4:1/1.4);\n    });\n    let mapDrag=null;\n    root.addEventListener('pointerdown',e=>{\n      const svg=e.target.closest('svg.map'); if(!svg) return;\n      const i=svg.id.replace('map-',''); const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      mapDrag={svg,i,W,H,x0:e.clientX,y0:e.clientY,vx0:v.x,vy0:v.y,rect:svg.getBoundingClientRect()};\n      svg.classList.add('dragging');\n    });\n    root.addEventListener('pointermove',e=>{\n      if(!mapDrag||!mapDrag.rect.width||!mapDrag.rect.height) return;\n      const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n      const v=getMapView(mapDrag.i,mapDrag.W,mapDrag.H);\n      const sx=v.w/mapDrag.rect.width, sy=v.h/mapDrag.rect.height;\n      let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n      x=Math.min(mapDrag.W-v.w, Math.max(0,x)); y=Math.min(mapDrag.H-v.h, Math.max(0,y));\n      mapViews[mapDrag.i]={...v,x,y};\n      applyMapView(mapDrag.svg,mapDrag.i);\n    });\n    const endMapDrag=()=>{ if(mapDrag){ mapDrag.svg.classList.remove('dragging'); mapDrag=null; } };\n    root.addEventListener('pointerup',endMapDrag);\n    root.addEventListener('pointerleave',endMapDrag);\n    (async()=>{\n      try{\n        // RACE, not a missing dataset. This DataSet is gated by timelineSimAuthFinal, so its\n        // Process is only define()d once the auth session resolves — a Process waits for its\n        // dependencies' setup before its own fires. In the field the auth lookup lands ~200ms\n        // and this DataSet's first fetch ~700-900ms, while mounted() runs immediately. d2.data()\n        // returns null for a module that is not defined YET, so bailing on the first miss threw\n        // \"dataset não disponível\" on every share link even though nothing was wrong.\n        // Poll briefly instead. Two lookups because either may win: the declared `shared` param,\n        // or the global-process path via the reference the instance binds — keep that binding.\n        let dead=false; d2.onUnmount(()=>{ dead=true; });\n        let ds=null;\n        for(let i=0;i<40 && !ds && !dead;i++){\n          ds=d2.data('shared')||d2.data('timeline-shared-sessionsDataSet');\n          if(!ds) await new Promise(r=>setTimeout(r,100));\n        }\n        if(dead) return;\n        if(!ds){ d2.setState({error:'Dataset de sessões compartilhadas não disponível.',ready:true}); return; }\n        // Name the router explicitly. Bare d2.router() looks for the conventional\n        // 'Router'/'MainRouter'; this one is 'timelineSimRouter', so the bare call can hand back a\n        // handle whose .params is empty — which is how a valid /sessions/<id> link ended up\n        // reporting \"o link não traz o identificador\". The goSim handler below already passes the\n        // ref for the same reason.\n        const rt=d2.router('timelineSimRouter')||d2.router();\n        const p=(rt&&rt.params)||{};\n        // ?id= lets the artboard be opened directly, outside the route.\n        let q=null;\n        try{ q=new URLSearchParams(window.location.search).get('id'); }catch(e){}\n        // Last resort, and the only one that cannot fail: the id is sitting in the path. Route\n        // params need the Router handle to resolve AND to have matched by the time mounted() runs\n        // (.params is a reactive getter, read once here); the pathname needs neither. Matches the\n        // current /sessions/:session_id route and the legacy /sessao/:id one.\n        let seg=null;\n        try{ const m=window.location.pathname.match(/\\/sess(?:ions|ao)\\/([^/?#]+)/); seg=m&&m[1]; }catch(e){}\n        const id=p.session_id||p.id||p.sessao||p.session||seg||q;\n        if(!id){ d2.setState({error:'O link não traz o identificador da sessão.',ready:true}); return; }\n        // .query() is a direct read and returns the rows — no refresh/loadAll ordering trap here\n        const rows=await ds.query({_id:id});\n        const rec=Array.isArray(rows)?rows[0]:(rows&&rows.data&&rows.data[0]);\n        d2.setState({rec:rec||null,ready:true,error:''});\n      }catch(err){\n        d2.setState({error:(err&&err.message)||'Falha ao carregar a sessão compartilhada.',ready:true});\n      }\n    })();\n  }\n}"}},"shared":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shared"}}}}}}}}}},{"_id":"6a7f3024f798d7d41c74440f","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"Y7eEvdJeC4azwvfDtwrYP","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const NIVEIS=['Bronze','Cristal','Prata','Ouro','Diamante','Diamante+'];\n\nexport default {\n  css: `\n  :host{display:block;height:100%}\n  *{box-sizing:border-box}\n  .rroot{--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--closing:#ff3b30;--ok:#34c759;--warn:#ff9500;\n    height:100%;overflow:auto;background:var(--bg);color:var(--ink);\n    font:13px/1.5 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;-webkit-font-smoothing:antialiased}\n  .shell{max-width:1080px;margin:0 auto;padding:26px 22px 70px}\n  header.top{margin-bottom:20px}\n  .eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);font-weight:600}\n  h1{font-size:25px;font-weight:600;letter-spacing:-.5px;margin:7px 0 10px}\n  .pills{display:flex;gap:7px;flex-wrap:wrap;align-items:center}\n  .pill{font-size:10.5px;padding:3px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel)}\n  .pill b{color:var(--ink);font-weight:600}\n  .pill.go{cursor:pointer;border-color:var(--acc);color:var(--acc);background:#eaf3ff;font-weight:600}\n  .pill.go:hover{background:#dbe9ff}\n  .goact{margin-top:15px;background:var(--acc);color:#fff;border:1px solid transparent;border-radius:8px;\n    padding:9px 16px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer}\n  .goact:hover{background:#0060df}\n  .strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:18px 0 22px}\n  .st{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .st .n{font-size:23px;font-weight:700;letter-spacing:-.5px}\n  .st .l{font-size:10.5px;color:var(--mut);margin-top:3px;line-height:1.4}\n  .st.warn .n{color:var(--closing)}\n  .stepper{position:sticky;top:0;z-index:5;display:flex;gap:7px;overflow-x:auto;padding:11px 0;margin-bottom:6px;\n    background:rgba(245,245,247,.9);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}\n  .sc{flex:0 0 auto;display:flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);\n    border-radius:99px;padding:5px 12px 5px 6px;cursor:pointer;font-size:12px}\n  .sc:hover{border-color:var(--acc)}\n  .sc .k{width:19px;height:19px;border-radius:99px;background:var(--ink);color:#fff;font-size:10px;font-weight:700;\n    display:flex;align-items:center;justify-content:center}\n  .sc .arrow{color:var(--mut)}\n  .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:16px;box-shadow:0 1px 2px rgba(0,0,0,.04)}\n  .card > h2{font-size:15px;font-weight:600;letter-spacing:-.2px;margin:0 0 3px}\n  .card .sub{font-size:11.5px;color:var(--mut);margin:0 0 15px}\n  h3{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:20px 0 9px;font-weight:600}\n  h3:first-of-type{margin-top:0}\n  .split{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:20px;align-items:start}\n  @media(max-width:820px){.split{grid-template-columns:1fr}}\n  svg.map{width:100%;height:auto;display:block;background:var(--panel2);border-radius:10px;border:1px solid var(--line);cursor:grab;touch-action:none}\n  svg.map.dragging{cursor:grabbing}\n  .map-wrap{position:relative}\n  .zoom-pill{position:absolute;left:10px;bottom:10px;display:flex;align-items:stretch;background:var(--panel);border:1px solid var(--line);border-radius:99px;box-shadow:0 2px 8px rgba(0,0,0,.14);overflow:hidden;z-index:3}\n  .zoom-pill button{width:30px;height:30px;border:0;background:transparent;color:var(--ink);font:16px/1 -apple-system,BlinkMacSystemFont,sans-serif;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center}\n  .zoom-pill button:hover{background:var(--panel2)}\n  .zoom-pill button:active{background:#e5e5ea}\n  .zoom-pill .zoom-div{width:1px;background:var(--line)}\n  .muni{stroke:#fff;stroke-width:1.1}\n  .nbr{fill:#e8e8ec;stroke:#d4d4d9;stroke-width:.7}\n  .exc{fill:#f7e7d0;stroke:var(--warn);stroke-width:.9;stroke-dasharray:2 2}\n  .mlabel{fill:#1c1c1e;font-size:9.5px;font-weight:600;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.7);stroke-width:2.4px}\n  .nlabel{fill:#9a9aa0;font-size:7.5px;text-anchor:middle}\n  .legend{display:flex;gap:11px;flex-wrap:wrap;margin-top:9px;font-size:11px;color:var(--mut)}\n  .legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px}\n  .bar{display:flex;align-items:center;gap:9px;margin:7px 0}\n  .bar .tag{width:46px;font-weight:600;font-size:12px}\n  .bar .track{flex:1;height:17px;background:var(--panel2);border-radius:5px;overflow:hidden;display:flex}\n  .bar .b1{height:100%;opacity:.38}.bar .b2{height:100%}.bar .b3{height:100%;opacity:.7}\n  .bar .v{width:120px;text-align:right;font-size:11px;color:var(--mut);font-variant-numeric:tabular-nums}\n  .mrow{display:flex;align-items:center;gap:9px;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:6px 10px;margin-bottom:5px}\n  .mrow .nm{flex:1;font-weight:600;font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .mrow .cb{color:var(--mut);font-size:11px;font-variant-numeric:tabular-nums}\n  .chip{font-size:11px;font-weight:700;color:#fff;border-radius:6px;padding:2px 8px}\n  table{border-collapse:collapse;width:100%;font-size:11.5px}\n  th,td{padding:6px 7px;text-align:right;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}\n  th:first-child,td:first-child{text-align:left}\n  thead th{color:var(--mut);font-weight:600;font-size:10.5px}\n  .dot{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}\n  .delta{font-size:10px}.delta.up{color:var(--ok)}.delta.dn{color:var(--closing)}\n  .note{font-size:10.5px;color:var(--mut);line-height:1.55;margin-top:9px}\n  .flag{margin-top:11px;padding:9px 11px;border-radius:9px;font-size:11.5px;line-height:1.55;background:#fff8ef;border:1px solid #ffe0bd;color:#8a5200}\n  .scroll{max-height:300px;overflow:auto}\n  .center{display:flex;align-items:center;justify-content:center;height:100%;padding:40px}\n  .msg{max-width:420px;text-align:center}\n  .msg h2{font-size:17px;font-weight:600;margin:0 0 7px}\n  .msg p{font-size:12.5px;color:var(--mut);margin:0;line-height:1.55}\n`,\n  render(d2){\n    // Hierarquia mercado > GV > setor, espelhando o simulador ao vivo. Tudo pré-computado (label,\n    // minúscula, plural, gênero) pra nunca minusculizar \"GV\" (sigla) virando \"gv\", e pra concordar\n    // gênero certo em cada frase — GV é feminina (a Gerência de Vendas), o resto é masculino.\n    const LVL={\n      setor:{label:'Setor',low:'setor',fem:false, child:'Município',childLow:'município',childPlural:'Municípios',childFem:false, dest:'Setor',destLow:'setor'},\n      gv:{label:'GV',low:'GV',fem:true, child:'Setor',childLow:'setor',childPlural:'Setores',childFem:false, dest:'GV',destLow:'GV'},\n      mercado:{label:'Mercado',low:'mercado',fem:false, child:'GV',childLow:'GV',childPlural:'GVs',childFem:true, dest:'Mercado',destLow:'mercado'},\n    };\n    const lv=s=>LVL[(s&&s.level)||'setor'];\n    const esc=s=>(s==null?'':String(s)).replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n    const fmt=n=>n==null?'–':Math.round(+n).toLocaleString('pt-BR');\n    const money=n=>{n=+n||0;return n>=1e6?'R$'+(n/1e6).toFixed(1)+'M':n>=1e3?'R$'+(n/1e3).toFixed(0)+'k':'R$'+Math.round(n)};\n    const msg=(t,b,act)=>`<div class=\"rroot\"><div class=\"center\"><div class=\"msg\"><h2>${t}</h2><p>${b}</p>${act?`<button class=\"goact\" id=\"goSim\">Abrir o simulador</button>`:''}</div></div></div>`;\n\n    if(d2.state.error) return msg('Não foi possível abrir a sessão',esc(d2.state.error),true);\n    if(!d2.state.ready) return msg('Carregando sessão…','Buscando o registro compartilhado.');\n    const r=d2.state.rec;\n    if(!r) return msg('Sessão não encontrada','O link pode ter expirado ou o registro foi removido. Peça um novo link a quem compartilhou.',true);\n\n    const stages=Array.isArray(r.stages)?r.stages:[];\n    const sm=r.summary||{};\n    const st=r.settings||{};\n    const ruleDest=st.destRule==='dominant'?`destino: dominantes ≥${fmt(st.destMinCbs)} CBs`:'destino: qualquer município';\n    const ruleReal=st.realocRule==='relevant'?`realocação: ≥${fmt(st.realocMinCbs)} CBs`:'realocação: todos';\n    const spanBits=[];\n    if(st.targetSpan)spanBits.push('alvo '+fmt(st.targetSpan));\n    if(st.maxSpan)spanBits.push('máx '+fmt(st.maxSpan));\n    if(st.minSpan)spanBits.push('mín '+fmt(st.minSpan));\n\n    let h=`<div class=\"rroot\"><div class=\"shell\">\n      <header class=\"top\">\n        <div class=\"eyebrow\">Simulador de timeline de fechamentos · sessão compartilhada</div>\n        <h1>${esc(r.label||'Sessão sem nome')}</h1>\n        <div class=\"pills\">\n          <span class=\"pill\">● snapshot congelado · <b>${esc(r.snapshotRaw||'—')}</b></span>\n          <span class=\"pill\">◇ ${esc(ruleReal)}</span>\n          <span class=\"pill\">◈ ${esc(ruleDest)}</span>\n          <span class=\"pill\">◎ span: <b>${spanBits.length?esc(spanBits.join(' · ')):'livre'}</b></span>\n          ${r.sharedBy?`<span class=\"pill\">por <b>${esc(r.sharedBy)}</b></span>`:''}\n          ${r.sharedAt?`<span class=\"pill\">${esc(new Date(r.sharedAt).toLocaleString('pt-BR'))}</span>`:''}\n          <span class=\"pill go\" id=\"goSim\" role=\"button\" tabindex=\"0\" title=\"Ir para o simulador\">↗ abrir o simulador</span>\n        </div>\n      </header>`;\n\n    const sessLevel=(stages[0]&&stages[0].level)||'setor';\n    h+=`<div class=\"strip\">\n      <div class=\"st\"><div class=\"n\">${fmt(stages.length)}</div><div class=\"l\">Etapa(s) de fechamento</div></div>\n      <div class=\"st\"><div class=\"n\">${fmt(sm.totalCbs)}</div><div class=\"l\">CBs realocadas na cadeia</div></div>\n      ${sessLevel==='setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs deslocadas (quebra de relação)</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(sm.totalDesloc)}</div><div class=\"l\">CBs com líder dividida entre destinos</div></div>`}\n      <div class=\"st\"><div class=\"n\" style=\"font-size:15px;line-height:1.5;font-weight:600\">${esc((sm.sectors||[]).join(' → ')||'—')}</div><div class=\"l\">Ordem de fechamento</div></div>\n    </div>`;\n\n    if(stages.length>1){\n      h+=`<div class=\"stepper\">`+stages.map((s,i)=>`<div class=\"sc\" data-goto=\"stage-${i}\"><span class=\"k\">${i+1}</span>${lv(s).label} ${esc(s.sector)}<span class=\"arrow\">→</span>${esc((s.dest||[]).join(', ')||'–')}</div>`).join('')+`</div>`;\n    }\n\n    // ---- projection helpers (pure; mirrors the simulator's) ----\n    const project=feats=>{let mnx=1/0,mny=1/0,mxx=-1/0,mxy=-1/0;\n      feats.forEach(f=>{const g=f&&f.geometry;if(!g)return;const ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n        ps.forEach(p=>p.forEach(rg=>rg.forEach(pt=>{mnx=Math.min(mnx,pt[0]);mxx=Math.max(mxx,pt[0]);mny=Math.min(mny,pt[1]);mxy=Math.max(mxy,pt[1]);})));});\n      if(!isFinite(mnx)) return null;\n      const mlat=(mny+mxy)/2,kx=Math.cos(mlat*Math.PI/180),W=680,H=440,pad=16;\n      const gw=Math.max((mxx-mnx)*kx,1e-9),gh=Math.max(mxy-mny,1e-9),sc=Math.min((W-2*pad)/gw,(H-2*pad)/gh);\n      const ox=(W-gw*sc)/2,oy=(H-gh*sc)/2;\n      return {W,H,f:pt=>[ox+(pt[0]-mnx)*kx*sc, oy+(mxy-pt[1])*sc]};};\n    const pathFor=(f,pr)=>{const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      return ps.map(poly=>poly.map(rg=>'M'+rg.map(pt=>{const q=pr(pt);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('')).join('');};\n    const cxy=(f,pr)=>{let x=0,y=0,n=0;const g=f.geometry,ps=g.type==='MultiPolygon'?g.coordinates:[g.coordinates];\n      ps.forEach(poly=>poly[0].forEach(pt=>{const q=pr(pt);x+=q[0];y+=q[1];n++;}));return n?[x/n,y/n]:[0,0];};\n\n    stages.forEach((s,i)=>{\n      const color=s.color||{};\n      const k=s.kpis||{};\n      const LV=lv(s);\n      h+=`<div class=\"card\" id=\"stage-${i}\" data-key=\"stage-${i}\">\n        <h2>Etapa ${i+1} · ${LV.low} <b>${esc(s.sector)}</b> fechad${LV.fem?'a':'o'}</h2>\n        <p class=\"sub\">Território redistribuído para ${esc((s.dest||[]).join(', ')||'—')}${s.domUF?' · '+esc(s.domUF):''}${s.policy?' · política: '+(s.policy==='fora'?'manter vínculo':'quebrar relação'):''}</p>\n        <div class=\"strip\" style=\"margin:0 0 18px\">\n          <div class=\"st\"><div class=\"n\">${fmt(k.cbsRealocadas)}</div><div class=\"l\">CBs realocadas</div></div>\n          ${LV.label==='Setor'?`<div class=\"st warn\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs deslocadas</div></div>`:`<div class=\"st\"><div class=\"n\">${fmt(k.desloc)}</div><div class=\"l\">CBs com líder dividida</div></div>`}\n          <div class=\"st\"><div class=\"n\">${fmt(k.municipios)}</div><div class=\"l\">${LV.childPlural} movid${LV.childFem?'as':'os'}</div></div>\n          <div class=\"st\"><div class=\"n\">${fmt(k.lideres)}</div><div class=\"l\">Líderes envolvidas</div></div>\n        </div>\n        <div class=\"split\"><div>`;\n\n      // map\n      const gf=s.geoFeat||{}, nb=Array.isArray(s.nbrFeat)?s.nbrFeat:[], ex=Array.isArray(s.excludedFeat)?s.excludedFeat:[];\n      const all=[...nb,...ex,...Object.values(gf)].filter(Boolean);\n      const P=project(all);\n      const unitOf=m=>LV.label==='Setor'?m:((s.unitOfMuni||{})[m]||m);\n      if(P){\n        let g='';\n        nb.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"nbr\" d=\"${pathFor(f,P.f)}\"/>`;\n          const c=cxy(f,P.f);const nm=(f.properties&&f.properties.name)||'';\n          if(nm) g+=`<text class=\"nlabel\" data-fs=\"7.5\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(nm.slice(0,13))}</text>`; });\n        ex.forEach(f=>{ if(!f||!f.geometry)return; g+=`<path class=\"exc\" d=\"${pathFor(f,P.f)}\"/>`; });\n        if(LV.label==='Setor'){\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return;\n            const dest=(s.assign||{})[m];\n            g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${esc(color[dest]||'#8e8e93')}\"/>`;\n            const c=cxy(f,P.f);\n            g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${c[0].toFixed(1)}\" y=\"${c[1].toFixed(1)}\">${esc(m.slice(0,14))}<tspan data-fs=\"8\" x=\"${c[0].toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`; });\n        } else {\n          // GV/mercado: vários municípios do mesmo setor/GV são UMA unidade só na tela — mesma cor,\n          // mesmo stroke (a costura entre elas some), um rótulo só no centro do grupo em vez de um\n          // por município. Espelha o mapa ao vivo do simulador.\n          const groups={};\n          Object.entries(gf).forEach(([m,f])=>{ if(!f||!f.geometry)return; const u=unitOf(m); (groups[u]=groups[u]||[]).push(f); });\n          Object.entries(groups).forEach(([u,feats])=>{\n            const dest=(s.assign||{})[u]; const col=esc(color[dest]||'#8e8e93');\n            let cx=0,cy=0,n=0;\n            feats.forEach(f=>{ g+=`<path class=\"muni\" d=\"${pathFor(f,P.f)}\" fill=\"${col}\" stroke=\"${col}\"/>`;\n              const c=cxy(f,P.f); cx+=c[0]; cy+=c[1]; n++; });\n            if(n){ cx/=n; cy/=n;\n              g+=`<text class=\"mlabel\" data-fs=\"9.5\" data-sw=\"2.4\" x=\"${cx.toFixed(1)}\" y=\"${cy.toFixed(1)}\">${esc(u)}<tspan data-fs=\"8\" x=\"${cx.toFixed(1)}\" dy=\"10\" font-size=\"8\">${esc(dest||'–')}</tspan></text>`;\n            }\n          });\n        }\n        h+=`<h3>Mapa da redistribuição</h3><div class=\"map-wrap\">\n          <svg class=\"map\" id=\"map-${i}\" data-w=\"${P.W}\" data-h=\"${P.H}\" viewBox=\"0 0 ${P.W} ${P.H}\" preserveAspectRatio=\"xMidYMid meet\">${g}</svg>\n          <div class=\"zoom-pill\"><button type=\"button\" data-zoom=\"out\" data-i=\"${i}\" title=\"Diminuir zoom\">−</button><span class=\"zoom-div\"></span><button type=\"button\" data-zoom=\"in\" data-i=\"${i}\" title=\"Aumentar zoom\">+</button></div>\n          </div>\n          <div class=\"legend\">${(s.dest||[]).map(d=>`<span><i style=\"background:${esc(color[d]||'#8e8e93')}\"></i>${LV.dest} ${esc(d)}</span>`).join('')}\n          <span><i style=\"background:#e8e8ec;border:1px solid #d4d4d9\"></i>vizinhos (contexto)</span>\n          ${ex.length?`<span><i style=\"background:#f7e7d0;border:1px dashed #ff9500\"></i>fora da realocação</span>`:''}</div>`;\n      }\n      h+=`<h3>${LV.childPlural} → ${LV.destLow} destino</h3>`;\n      h+=(s.municipios||[]).map(m=>`<div class=\"mrow\"><span class=\"nm\">${esc(m.nome)}</span><span class=\"cb\">${fmt(m.cbs)} CBs</span>\n        <span class=\"chip\" style=\"background:${esc(color[m.destino]||'#8e8e93')}\">${esc(m.destino||'–')}</span></div>`).join('')||`<div class=\"note\">Nenhum${LV.childFem?'a':''} ${LV.childLow} realocad${LV.childFem?'a':'o'} nesta etapa.</div>`;\n\n      h+=`</div><div>`;\n      // spans\n      const ds=s.destinos||[];\n      const mx=Math.max(...ds.map(d=>+d.span||0),1);\n      h+=`<h3>Tamanho d${LV.fem?'a':'o'} ${LV.destLow} depois da etapa</h3>`;\n      h+=ds.map(d=>{const base=+d.base||0,inc=+d.inc||0,orph=+d.orph||0;\n        return `<div class=\"bar\"><span class=\"tag\" style=\"color:${esc(color[d.s]||'#8e8e93')}\">${esc(d.s)}</span>\n          <span class=\"track\"><span class=\"b1\" style=\"width:${(base/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span><span class=\"b2\" style=\"width:${(inc/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>${orph?`<span class=\"b3\" style=\"width:${(orph/mx*100).toFixed(1)}%;background:${esc(color[d.s]||'#8e8e93')}\"></span>`:''}</span>\n          <span class=\"v\">${fmt(d.span)} <span style=\"opacity:.6\">(+${fmt(inc+orph)})</span></span></div>`;}).join('')||`<div class=\"note\">Sem ${LV.destLow} destino nesta etapa.</div>`;\n      h+=`<div class=\"note\">Claro = base antes da etapa · cheio = CBs entrantes${(s.orphanTotal?' · faixa final = CBs que seguiram suas líderes':'')}.</div>`;\n\n      // leaders\n      const L=s.lideres||[];\n      if(L.length){\n        const showOrph=L.some(x=>+x.orphan>0);\n        h+=`<h3>Líderes</h3><div class=\"scroll\"><table><thead><tr><th>Líder</th><th>CBs</th><th>${LV.dest} final</th>${showOrph?'<th>Órfãs</th>':''}<th>Desloc.</th></tr></thead><tbody>`\n          +L.map(x=>`<tr><td>${esc(x.lider||'—')}</td><td>${fmt(x.total)}</td>\n            <td>${x.home?`<span class=\"dot\" style=\"background:${esc(color[x.home]||'#8e8e93')}\"></span>${esc(x.home)}`:'–'}</td>\n            ${showOrph?`<td>${x.orphan?fmt(x.orphan):'·'}</td>`:''}\n            <td style=\"color:${+x.desloc?'var(--closing)':'var(--mut)'};font-weight:${+x.desloc?700:400}\">${fmt(x.desloc||0)}</td></tr>`).join('')\n          +`</tbody></table></div>`;\n      }\n      h+=`</div></div>`;\n\n      // antes -> depois\n      const M=s.metricas||[];\n      if(M.length){\n        h+=`<h3>Impacto antes → depois por ${LV.destLow} ${LV.fem?'receptora':'receptor'}</h3><div class=\"scroll\"><table><thead><tr><th>${LV.dest}</th>${M.map(m=>`<th>${esc(m.label)}</th>`).join('')}</tr></thead><tbody>`;\n        (s.dest||[]).forEach(sec=>{\n          h+=`<tr><td><span class=\"dot\" style=\"background:${esc(color[sec]||'#8e8e93')}\"></span>${esc(sec)}</td>`;\n          h+=M.map(m=>{const row=(m.rows||[]).find(x=>String(x.s)===String(sec))||{};\n            const a=+row.antes||0,b=+row.depois||0,d=a?((b-a)/a*100):0;const f=m.money?money:fmt;\n            return `<td>${f(b)}<div class=\"delta ${d>=0?'up':'dn'}\">${d>0?'+':''}${d.toFixed(0)}%</div></td>`;}).join('')+`</tr>`;\n        });\n        h+=`<tr style=\"opacity:.75\"><td>${esc(s.sector)} (sai)</td>`+M.map(m=>`<td>${(m.money?money:fmt)(m.sai||0)}</td>`).join('')+`</tr>`;\n        h+=`</tbody></table></div>`;\n      }\n\n      // caveats\n      const fl=[];\n      if(s.excluidos&&s.excluidos.length) fl.push(`<b>${fmt(s.excluidos.length)} município(s)</b> com poucas CBs ficaram fora da realocação geográfica (quebra de vínculo): ${esc(s.excluidos.slice(0,6).map(x=>x.muni+' ('+x.cbs+')').join(', '))}${s.excluidos.length>6?' +'+(s.excluidos.length-6):''}. As CBs seguiram a líder do seu grupo.`);\n      if(s.orphanStranded) fl.push(`<b>${fmt(s.orphanStranded)} CB(s)</b> sem setor a seguir — a líder não ficou com nenhum município realocado.`);\n      if(s.noShapeCbs) fl.push(`<b>${fmt(s.noShapeCbs)} CB(s)</b> em municípios sem shape na UF, fora do mapa e de qualquer setor destino.`);\n      if(s.optTier===1) fl.push(`O span mínimo não era atingível nesta etapa e foi relaxado pelo modelo.`);\n      if(s.optTier===2) fl.push(`Os limites de span não eram atingíveis nesta etapa e foram relaxados pelo modelo.`);\n      if(s.optTier===-1) fl.push(`Etapa resolvida por heurística (espaço de busca grande) — os limites de span não são garantidos.`);\n      if(s.destRuleFallback) fl.push(`Nenhum município dominante nesta etapa: a regra de destino foi relaxada para \"qualquer município\".`);\n      if(s.realocFallback) fl.push(`Todos os municípios ficariam abaixo do limite: a regra de realocação foi relaxada para \"realocar todos\".`);\n      if(fl.length) h+=`<div class=\"flag\">⚠ ${fl.join('<br>')}</div>`;\n      h+=`</div>`;\n    });\n\n    // cumulative\n    // Este resumo é calculado no rastro de município (world.muniOverride) — hoje só existe pro\n    // fechamento de setor; GV/mercado têm o rastro equivalente (secOverride/gvOverride) mas essa\n    // agregação ainda não foi refeita nesse grão, então avisa em vez de mostrar tabela vazia.\n    const rc=sm.receivers||[];\n    if(!rc.length && sessLevel!=='setor'){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"note\" style=\"margin-top:0\">Ainda não disponível pra sessões de ${sessLevel==='gv'?'GV':'mercado'} — veja o relatório de cada etapa acima.</p></div>`;\n    }\n    if(rc.length){\n      h+=`<div class=\"card\"><h2>Impacto acumulado da cadeia</h2>\n        <p class=\"sub\">Efeito de <b>todas</b> as ${fmt(stages.length)} etapa(s) somadas, contra o snapshot congelado sem nenhum fechamento aplicado.</p>\n        <div class=\"scroll\"><table><thead><tr><th>Setor</th><th>Situação</th><th>CBs antes</th><th>CBs depois</th><th>Variação</th></tr></thead><tbody>`\n        +rc.map(x=>{const a=+x.antes||0,b=+x.depois||0,d=a?((b-a)/a*100):0;\n          return `<tr><td>${esc(x.s)}</td><td>${esc(x.situacao||'recebeu território')}</td><td>${fmt(a)}</td><td>${fmt(b)}</td>\n            <td class=\"delta ${d>=0?'up':'dn'}\" style=\"font-size:11.5px\">${d>0?'+':''}${d.toFixed(0)}%</td></tr>`;}).join('')\n        +`</tbody></table></div></div>`;\n    }\n\n    h+=`<div class=\"note\" style=\"text-align:center;margin-top:22px\">Visualização somente leitura de uma simulação congelada. Os números refletem o snapshot <b>${esc(r.snapshotRaw||'—')}</b> e as regras vigentes no momento do compartilhamento — não acompanham o dado ao vivo.</div>`;\n    return h+`</div></div>`;\n  },\n  mounted(d2, root){\n    d2.on('#goSim','click',()=>{\n      try{ (d2.router('timelineSimRouter')||d2.router()).goTo('/'); }catch(e){ try{ d2.router().goTo('/'); }catch(e2){} }\n    });\n    d2.on('.sc','click',e=>{\n      const el=e.target.closest('.sc'); if(!el) return;\n      const t=d2.$('#'+el.dataset.goto); if(t) t.scrollIntoView({behavior:'smooth',block:'start'});\n    });\n    // ---- map zoom/pan (viewBox só — texto e polígonos vivem no mesmo sistema de coordenadas, então\n    // escalam juntos automaticamente ao encolher/esticar o viewBox, sem lógica extra pra fonte).\n    // Delegado em `root` (nunca substituído por re-render) em vez de nos <svg> por id, porque cada\n    // atualização de estado reconstrói o HTML das etapas do zero.\n    const mapViews={};\n    const getMapView=(i,W,H)=>mapViews[i]||(mapViews[i]={x:0,y:0,w:W,h:H});\n    const applyMapView=(svg,i)=>{ const v=mapViews[i]; if(!v) return;\n      svg.setAttribute('viewBox', v.x.toFixed(2)+' '+v.y.toFixed(2)+' '+v.w.toFixed(2)+' '+v.h.toFixed(2));\n      // Rótulo tem tamanho em unidades do viewBox — sem isso ele cresceria junto com o zoom até virar\n      // um borrão. Encolher na mesma proporção que o viewBox encolhe cancela o zoom: o texto fica do\n      // mesmo tamanho na tela em qualquer nível. style.fontSize (não o atributo) porque uma regra de\n      // classe como .mlabel{font-size:...} outrankaria um atributo comum.\n      const W=+svg.dataset.w||700;\n      const fsScale=v.w/W;\n      svg.querySelectorAll('[data-fs]').forEach(t=>{\n        const base=+t.getAttribute('data-fs')||8;\n        t.style.fontSize=(base*fsScale).toFixed(2)+'px';\n      });\n      // Halo do texto (contorno branco que garante legibilidade sobre qualquer cor de fundo) tem\n      // largura de traço fixa em unidades do viewBox — sem reescalar junto, no zoom alto ele vira\n      // uma mancha cobrindo a letra inteira em vez de um contorno fino.\n      svg.querySelectorAll('[data-sw]').forEach(t=>{\n        const base=+t.getAttribute('data-sw')||1;\n        t.style.strokeWidth=(base*fsScale).toFixed(3)+'px';\n      });\n    };\n    const zoomStageMap=(i,factor)=>{\n      const svg=root.querySelector('#map-'+i); if(!svg) return;\n      const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      const cx=v.x+v.w/2, cy=v.y+v.h/2;\n      let w=v.w/factor, h=v.h/factor;\n      w=Math.min(W, Math.max(W/8, w)); h=Math.min(H, Math.max(H/8, h));\n      let x=cx-w/2, y=cy-h/2;\n      x=Math.min(W-w, Math.max(0,x)); y=Math.min(H-h, Math.max(0,y));\n      mapViews[i]={x,y,w,h};\n      applyMapView(svg,i);\n    };\n    d2.on('.zoom-pill button','click',e=>{\n      const b=e.target.closest('button'); if(!b) return;\n      zoomStageMap(b.dataset.i, b.dataset.zoom==='in'?1.4:1/1.4);\n    });\n    let mapDrag=null;\n    root.addEventListener('pointerdown',e=>{\n      const svg=e.target.closest('svg.map'); if(!svg) return;\n      const i=svg.id.replace('map-',''); const W=+svg.dataset.w||700, H=+svg.dataset.h||500;\n      const v=getMapView(i,W,H);\n      mapDrag={svg,i,W,H,x0:e.clientX,y0:e.clientY,vx0:v.x,vy0:v.y,rect:svg.getBoundingClientRect()};\n      svg.classList.add('dragging');\n    });\n    root.addEventListener('pointermove',e=>{\n      if(!mapDrag||!mapDrag.rect.width||!mapDrag.rect.height) return;\n      const dxPix=e.clientX-mapDrag.x0, dyPix=e.clientY-mapDrag.y0;\n      const v=getMapView(mapDrag.i,mapDrag.W,mapDrag.H);\n      const sx=v.w/mapDrag.rect.width, sy=v.h/mapDrag.rect.height;\n      let x=mapDrag.vx0-dxPix*sx, y=mapDrag.vy0-dyPix*sy;\n      x=Math.min(mapDrag.W-v.w, Math.max(0,x)); y=Math.min(mapDrag.H-v.h, Math.max(0,y));\n      mapViews[mapDrag.i]={...v,x,y};\n      applyMapView(mapDrag.svg,mapDrag.i);\n    });\n    const endMapDrag=()=>{ if(mapDrag){ mapDrag.svg.classList.remove('dragging'); mapDrag=null; } };\n    root.addEventListener('pointerup',endMapDrag);\n    root.addEventListener('pointerleave',endMapDrag);\n    (async()=>{\n      try{\n        // RACE, not a missing dataset. This DataSet is gated by timelineSimAuthFinal, so its\n        // Process is only define()d once the auth session resolves — a Process waits for its\n        // dependencies' setup before its own fires. In the field the auth lookup lands ~200ms\n        // and this DataSet's first fetch ~700-900ms, while mounted() runs immediately. d2.data()\n        // returns null for a module that is not defined YET, so bailing on the first miss threw\n        // \"dataset não disponível\" on every share link even though nothing was wrong.\n        // Poll briefly instead. Two lookups because either may win: the declared `shared` param,\n        // or the global-process path via the reference the instance binds — keep that binding.\n        let dead=false; d2.onUnmount(()=>{ dead=true; });\n        let ds=null;\n        for(let i=0;i<40 && !ds && !dead;i++){\n          ds=d2.data('shared')||d2.data('timeline-shared-sessionsDataSet');\n          if(!ds) await new Promise(r=>setTimeout(r,100));\n        }\n        if(dead) return;\n        if(!ds){ d2.setState({error:'Dataset de sessões compartilhadas não disponível.',ready:true}); return; }\n        // Name the router explicitly. Bare d2.router() looks for the conventional\n        // 'Router'/'MainRouter'; this one is 'timelineSimRouter', so the bare call can hand back a\n        // handle whose .params is empty — which is how a valid /sessions/<id> link ended up\n        // reporting \"o link não traz o identificador\". The goSim handler below already passes the\n        // ref for the same reason.\n        const rt=d2.router('timelineSimRouter')||d2.router();\n        const p=(rt&&rt.params)||{};\n        // ?id= lets the artboard be opened directly, outside the route.\n        let q=null;\n        try{ q=new URLSearchParams(window.location.search).get('id'); }catch(e){}\n        // Last resort, and the only one that cannot fail: the id is sitting in the path. Route\n        // params need the Router handle to resolve AND to have matched by the time mounted() runs\n        // (.params is a reactive getter, read once here); the pathname needs neither. Matches the\n        // current /sessions/:session_id route and the legacy /sessao/:id one.\n        let seg=null;\n        try{ const m=window.location.pathname.match(/\\/sess(?:ions|ao)\\/([^/?#]+)/); seg=m&&m[1]; }catch(e){}\n        const id=p.session_id||p.id||p.sessao||p.session||seg||q;\n        if(!id){ d2.setState({error:'O link não traz o identificador da sessão.',ready:true}); return; }\n        // .query() is a direct read and returns the rows — no refresh/loadAll ordering trap here\n        const rows=await ds.query({_id:id});\n        const rec=Array.isArray(rows)?rows[0]:(rows&&rows.data&&rows.data[0]);\n        d2.setState({rec:rec||null,ready:true,error:''});\n      }catch(err){\n        d2.setState({error:(err&&err.message)||'Falha ao carregar a sessão compartilhada.',ready:true});\n      }\n    })();\n  }\n}"}},"shared":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shared"}}}}}}}}}},{"_id":"6a7f3140b7014a3fb17a76ce","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"ZbSMa0nRld-Ond2eo6tjs","package":"natura-geomarketing","extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const AUTH='timelineSimAuthFinal';\n// SSP origin is derived at runtime from a known listener's resolved URL; this is only the fallback.\nconst SSP_FALLBACK='https://natura-geomarketing.data2apis.com';\nconst RUSH_REF='AwW_11Nkg-dKksP7ic8V8';\nconst ROUTER='timelineSimRouter';\n\nexport default {\n  css: `\n  :host{display:block;height:100%}\n  *{box-sizing:border-box}\n  .lroot{--bg:#f5f5f7;--panel:#ffffff;--panel2:#f2f2f7;--line:#e5e5ea;--ink:#1c1c1e;--mut:#6e6e73;--acc:#007aff;--acc2:#0060df;--closing:#ff3b30;--ok:#34c759;\n    min-height:100%;display:flex;align-items:center;justify-content:center;padding:36px 20px;background:var(--bg);\n    font:13px/1.45 -apple-system,BlinkMacSystemFont,\"SF Pro Text\",\"Segoe UI\",Roboto,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}\n  .card{width:100%;max-width:400px;background:var(--panel);border:1px solid var(--line);border-radius:14px;\n    box-shadow:0 12px 34px rgba(0,0,0,.07),0 1px 2px rgba(0,0,0,.04);padding:26px 26px 22px}\n  .pill{display:inline-block;font-size:10.5px;padding:2.5px 9px;border-radius:99px;border:1px solid var(--line);color:var(--mut);background:var(--panel2)}\n  h1{font-size:17px;font-weight:600;letter-spacing:-.25px;margin:13px 0 5px}\n  .sub{font-size:12px;color:var(--mut);margin:0 0 20px;line-height:1.5}\n  .fl{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);font-weight:600;margin:0 0 5px}\n  .inp{width:100%;background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font:inherit;font-size:13px}\n  .inp:focus{outline:2px solid rgba(0,122,255,.32);outline-offset:0;border-color:var(--acc)}\n  .inp:disabled{opacity:.55}\n  .row{margin-bottom:14px}\n  .act{width:100%;background:var(--acc);color:#fff;border:1px solid transparent;border-radius:8px;padding:10px 14px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 1px rgba(0,0,0,.08);margin-top:4px}\n  .act:hover{background:var(--acc2)}\n  .act:disabled{opacity:.5;cursor:not-allowed}\n  .ghost{width:100%;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 12px;font:inherit;font-size:12.5px;cursor:pointer;margin-top:8px}\n  .ghost:hover{background:var(--panel2)}\n  .okbox{margin:12px 0 0;padding:8px 10px;border-radius:8px;background:#eefaf0;border:1px solid #bdeecb;color:#1c7c33;font-size:11.5px;font-weight:600}\n  .err{margin:12px 0 0;padding:8px 10px;border-radius:8px;background:#fff1f0;border:1px solid #ffd1cc;color:var(--closing);font-size:11.5px;font-weight:600}\n  .who{font-size:12px;color:var(--mut);margin:0 0 16px}\n  .who b{color:var(--ink)}\n  .foot{margin-top:18px;padding-top:14px;border-top:1px solid var(--line);font-size:10.5px;color:var(--mut);line-height:1.55}\n`,\n  render(d2){\n    const esc=s=>(s==null?'':String(s)).replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n    const busy=!!d2.state.busy;\n    const err=d2.state.error||'';\n    const a=d2.auth(AUTH);\n    if(!a){\n      return `<div class=\"lroot\"><div class=\"card\">\n        <span class=\"pill\">configuração</span>\n        <h1>Estratégia de autenticação não encontrada</h1>\n        <p class=\"sub\">Não foi possível resolver <b>${esc(AUTH)}</b>. Confirme a referência da AuthenticationStrategy do sistema.</p>\n      </div></div>`;\n    }\n    // Deliberately NOT branching on a.isAuthed. The Router renders this artboard ONLY from its\n    // `login` slot, i.e. only when the route policy rejected the caller — so anyone looking at it\n    // is unauthenticated by construction. isAuthed also reads TRUE under a builder token, which\n    // would show an \"already signed in\" card that bounces to / , gets rejected, and loops.\n    return `<div class=\"lroot\"><div class=\"card\">\n      <span class=\"pill\">acesso restrito</span>\n      <h1>Simulador de Timeline de Fechamentos</h1>\n      <p class=\"sub\">Entre com suas credenciais pra abrir a timeline de fechamentos de setores.</p>\n      <div class=\"row\">\n        <label class=\"fl\" for=\"em\">E-mail</label>\n        <input class=\"inp\" id=\"em\" type=\"email\" autocomplete=\"username\" autocapitalize=\"off\" autocorrect=\"off\" spellcheck=\"false\" placeholder=\"voce@natura.com.br\"${busy?' disabled':''}>\n      </div>\n      <div class=\"row\">\n        <label class=\"fl\" for=\"pw\">Senha</label>\n        <input class=\"inp\" id=\"pw\" type=\"password\" autocomplete=\"current-password\" placeholder=\"••••••••\"${busy?' disabled':''}>\n      </div>\n      <button class=\"act\" id=\"go\"${busy?' disabled':''}>${busy?'Entrando…':'Entrar'}</button>\n      ${err?`<div class=\"err\">⚠ ${esc(err)}</div>`:''}\n      ${d2.state.ok?`<div class=\"okbox\">✓ Autenticado. No app publicado o redirecionamento é automático.</div>`:''}\n      <div class=\"foot\">Geomarketing · Natura. O simulador é read-only sobre o Databricks — cada sessão salva fica presa ao snapshot congelado da simulação.</div>\n    </div></div>`;\n  },\n  mounted(d2, root){\n    // The router re-runs its policy hook when the session changes, so a successful login normally\n    // swaps the gated content on its own. If it does not (same path), reload as a last resort —\n    // cancelled on unmount, so the reload only ever fires when the swap did NOT happen.\n    // Login now runs through the strategy, so the runtime DOES hold the session in memory and the\n    // route policy would re-evaluate on its own. The hard navigation is kept deliberately: it is\n    // the one path that cannot land on stale client state. In the builder there is no app root to\n    // go to, so we just report success instead of navigating out of /preview/<ref>. [agent:ad78d19d]\n    const enter=()=>{\n      if(d2.context.isBuilder){ d2.setState({busy:false,error:'',ok:true}); return; }\n      try{ window.location.replace('/'); }\n      catch(e){ try{ window.location.reload(); }catch(e2){} }\n    };\n    // pt-BR (ABNT2) keyboards put ^ ~ ´ ¨ on DEAD keys: pressing one before a character that cannot\n    // take the accent (like @) emits the bare mark, or leaves a combining codepoint behind. Either\n    // way it must never reach /id/ln, so strip it plus zero-width paste junk and any whitespace.\n    // The PASSWORD is deliberately left untouched — these characters are legitimate in a password.\n    const cleanEmail=s=>String(s==null?'':s)\n      .normalize('NFKC')\n      .replace(/[̀-ͯ]/g,'')\n      .replace(/[​-‍﻿]/g,'')\n      .replace(/[\\^~´¨`]/g,'')\n      .replace(/\\s+/g,'');\n    // A failed login must NEVER navigate or reload: keep the card, keep what was typed (the morph\n    // patches in place so input values survive), show the error and put the cursor back in the\n    // password so it can be corrected immediately.\n    const failWith=msg=>{\n      d2.setState({busy:false,ok:false,error:msg});\n      setTimeout(()=>{ const p=d2.$('#pw'); if(p){ try{ p.focus(); p.select(); }catch(e){} } },0);\n    };\n    const submit=async()=>{\n      if(d2.state.busy) return;\n      const rawEm=(d2.$('#em')||{}).value||'';\n      const em=cleanEmail(rawEm);\n      if(em!==rawEm && d2.$('#em')) d2.$('#em').value=em;   // show the cleaned address back to the user\n      const pw=((d2.$('#pw')||{}).value||'');\n      if(!em||!pw){ failWith('Preencha e-mail e senha.'); return; }\n      d2.setState({busy:true,error:'',ok:false});\n      try{\n        // Direct interaction with the AuthenticationStrategy — no hand-rolled fetch. The runtime\n        // owns the session envelope, so IT attaches `authorization: Bearer <jwt>` (and the session\n        // cookie) to the login POST. The previous hand-rolled fetch could not: it derived the jwt\n        // from document.cookie on the APP origin while the cookie lives on the SSP origin, so the\n        // header was silently dropped on every attempt — the 401 in devtools. [agent:ad78d19d]\n        // ⚠️ `credentials` MUST stay NESTED — a flat {email,password} authenticates nobody and\n        // fails as an UNCATCHABLE 400/403 (no exception, no rejected promise).\n        // ⚠️ A rejected login is NOT an exception either — after the promise settles, read\n        // `isAuthed`. Do not gate the error UI on catch().\n        const a=d2.auth(AUTH);\n        if(!a){ failWith('Estratégia de autenticação não encontrada ('+AUTH+').'); return; }\n        await a.login({credentials:{email:em,password:pw}});\n        if(a.isAuthed){ enter(); return; }\n        failWith('E-mail ou senha inválidos.');\n      }catch(e){\n        failWith('Falha ao autenticar: '+((e&&e.message)||'erro desconhecido'));\n      }\n    };\n    d2.on('#go','click',submit);\n    d2.on('.inp','keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); submit(); } });\n  }\n}"}}}}}}}},{"_id":"5ed9bed7b9e0891ce913e884","component":"data2-core-types@Component","params":{"package":"data2-core","component":"DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5f472ae463c680de4ff296e9","component":"data2-core-types@Component","params":{"package":"data2-core","component":"Data2TableDataSource","extends":"data2-core@DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"6a643d1370fbd195b8e6a9cd","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"n-E3kblbz0EwqfdH7YVjX","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"n-E3kblbz0EwqfdH7YVjX"}}}}}},{"_id":"6a5fa7e074f960d571569087","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"4hvch1V6MXqBr9rL-nvAu","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"4hvch1V6MXqBr9rL-nvAu"}}}}}},{"_id":"6a6228660ff5a8d431e8cedd","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"O4-zOAoAC9BwIp-XZ0ekO","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"O4-zOAoAC9BwIp-XZ0ekO"}}}}}},{"_id":"6a62286b0ff5a8d431e8cede","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"ONebofT7KQiRJeb0mlziC","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"ONebofT7KQiRJeb0mlziC"}}}}}},{"_id":"6a62286f0ff5a8d431e8cedf","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"gm34nI8iHui1SSuwobYTT","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"gm34nI8iHui1SSuwobYTT"}}}}}},{"_id":"6a6357c99642efac858afb15","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"gfyeGJI77tJzdwSZTKCnV","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"gfyeGJI77tJzdwSZTKCnV"}}}}}},{"_id":"6a5f9f15a5529ef885ca969b","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"0CZ7pC6NULlDQhV3hv-0G","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"0CZ7pC6NULlDQhV3hv-0G"}}}}}},{"_id":"6a62287d0ff5a8d431e8cee2","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"1S_fB3hvzwUro47iuZ55p","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"1S_fB3hvzwUro47iuZ55p"}}}}}},{"_id":"6a626d2d61501108689e9732","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"_3BHHDtVc0ZK0wlfcnLM3","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"_3BHHDtVc0ZK0wlfcnLM3"}}}}}},{"_id":"5ecd05ded68ce41360f74b43","component":"data2-core-types@Component","params":{"extendable":{"placeholder":"children"},"component":"Block","package":"data2-core-primitives"}},{"_id":"612009a592425036c3f3e06e","component":"data2-core-types@Component","params":{"definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"params":{"dangerouslySetInnerHTML":{"__html":{"component":"data2-core-types@Reference","params":{"reference":"text"}}}},"component":"html@div"}}},"component":"Html","package":"data2-blocks","extends":"data2-core-primitives@Block"}},{"_id":"6a666322607e9bc66b1bae82","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"f2_yWxSf4RxX4sceM8HEg","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"f2_yWxSf4RxX4sceM8HEg"}}}}}},{"_id":"6a6c3111b1994f0f4b08f439","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"Y-BartZI5BwXJmLbhFG_3","package":"natura-geomarketing","definition":{"params":{"actions":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}},"value":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"setor"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-types@Setter"},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"7rq4WNBhLtN4x9qy-262L"}},"prop":"filter","args":[{"params":{"fields":{"CD_SETOR":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"setor"}}},"component":"data2-core-types@Reference"}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.UF"}},"value":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"7rq4WNBhLtN4x9qy-262L"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.0.UF"}}}}},"component":"data2-core-types@Setter"},{"params":{"actions":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"7rq4WNBhLtN4x9qy-262L"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"actions":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}},"value":{"params":{"array":{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"},"item":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@ArrayPush"}},"component":"data2-core-types@Setter"},{"params":{"municipio":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"uf":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.UF"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@4vQ1HUOSYMGvZGCOV2GYd"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}},"value":{"params":{"arrays":[{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-functions@ArrayConcat"}},"component":"data2-core-types@Setter"},{"params":{"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@gA5O7o100HVWfKwEEPNyM"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios_normalized"}},"value":{"params":{"array":{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios_normalized"}}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"},"item":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@ArrayPush"}},"component":"data2-core-types@Setter"}]},"component":"data2-core-flow@Series"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Parallel"},{"params":{"resultName":{"component":"data2-core-primitives@String","params":{"content":"municipios"}},"action":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"7rq4WNBhLtN4x9qy-262L"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Step"},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"umJZo_TTyw0E3lOwBNmk2"}},"prop":"filter","args":[{"params":{"fields":{"a":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}},"b":{"params":{"arrays":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"}]},"component":"data2-core-functions@ArrayConcat"}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setores_destino"}},"value":{"params":{"array":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"umJZo_TTyw0E3lOwBNmk2"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}}},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"sector":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.CD_SETOR"}}},"component":"data2-core-types@Reference"},"municipio":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@Setter"}]},"component":"data2-core-flow@Series"}}},{"_id":"6436f04bac93122e72906101","component":"data2-core-types@Component","params":{"extends":"data2-core-primitives@Block","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"html@i","params":{"aria-hidden":"true","style":{"display":"flex","justifyContent":"center","alignItems":"center","width":"100%","height":"100%","fontSize":{"params":{"reference":"size"},"component":"data2-core-types@Reference"},"color":{"params":{"compare":[{"params":{"subject":{"params":{"reference":"color"},"component":"data2-core-types@Reference"},"pattern":{"component":"data2-core-primitives@String","params":{"content":"gradient"}}},"component":"data2-core-functions@Includes"}],"else":{"params":{"components":[{"params":{"reference":"color"},"component":"data2-core-types@Reference"},{"params":{"reference":"super.textColor"},"component":"data2-core-types@Reference"}]},"component":"data2-core-logical-operators@Or"},"then":{"component":"data2-core-primitives@String","params":{"content":"transparent"}}},"component":"data2-core-flow@If"},"backgroundClip":{"component":"data2-core-primitives@String","params":{"content":"text"}},"WebkitBackgroundClip":{"component":"data2-core-primitives@String","params":{"content":"text"}},"backgroundImage":{"params":{"compare":[{"params":{"subject":{"params":{"reference":"color"},"component":"data2-core-types@Reference"},"pattern":{"component":"data2-core-primitives@String","params":{"content":"gradient"}}},"component":"data2-core-functions@Includes"}],"then":{"params":{"portions":[{"params":{"reference":"color"},"component":"data2-core-types@Reference"}]},"component":"data2-core-functions@StringConcat"},"else":{"component":"data2-core-primitives@String","params":{"content":"linear-gradient(transparent,transparent)"}}},"component":"data2-core-flow@If"},"transition":{"component":"data2-core-primitives@String","params":{"content":"inherit"}}},"className":{"component":"data2-core-types@Reference","params":{"reference":"icon"}}}}}},"component":"IconBlock","package":"data2-blocks"}},{"_id":"6a69a173253cab7975060043","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"nGePVjqDWdX2KQL8ahh-w","package":"natura-geomarketing","definition":{"params":{"actions":[{"params":{"actions":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__LoggerService"}},"prop":"log","args":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"message"}}}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"data"}}}}]}}]},"component":"data2-core-flow@Parallel"},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.0"}}}}]},"component":"data2-core-flow@Series"}}},{"_id":"6a69b10fece2da0d644e73a8","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"gA5O7o100HVWfKwEEPNyM","package":"natura-geomarketing","definition":{"component":"data2-core-flow@Series","params":{"actions":[{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"text"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[àáâãäåÀÁÂÃÄÅ]","flags":"g"}},"patch":"a"}},"resultName":"n1"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n1"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[èéêëÈÉÊË]","flags":"g"}},"patch":"e"}},"resultName":"n2"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n2"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ìíîïÌÍÎÏ]","flags":"g"}},"patch":"i"}},"resultName":"n3"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n3"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[òóôõöÒÓÔÕÖ]","flags":"g"}},"patch":"o"}},"resultName":"n4"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n4"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ùúûüÙÚÛÜ]","flags":"g"}},"patch":"u"}},"resultName":"n5"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n5"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[çÇ]","flags":"g"}},"patch":"c"}},"resultName":"n6"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n6"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ñÑ]","flags":"g"}},"patch":"n"}},"resultName":"n7"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@toUpperCase","params":{"string":{"component":"data2-core-types@Reference","params":{"reference":"n7"}}}},"resultName":"n8"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"n8"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[^A-Z0-9]+","flags":"g"}},"patch":" "}},"resultName":"n9"}},{"component":"data2-core-functions@StringTrim","params":{"string":{"component":"data2-core-types@Reference","params":{"reference":"n9"}}}}]}}}},{"_id":"6a6994a1049cd0da321f270c","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"4vQ1HUOSYMGvZGCOV2GYd","package":"natura-geomarketing","definition":{"component":"data2-core-flow@Series","params":{"actions":[{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"N15zEWu4GvXztmPPTjOBt"}},"prop":"sendRequest","args":[{"request":{"component":"data2-core-types@InstanceReference","params":{"component":"data2-core-connect@HttpRequest","reference":"gh_MFH1HrDf2o2JrlseD0"}},"requestParameters":{"uf":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"uf"}}},"component":"data2-core-types@Reference"}}}]}},"resultName":"resp"}},{"params":{"features":{"component":"data2-core-types@Reference","params":{"reference":"resp.data.features"}},"municipio":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipio"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@3EIFSsdsdy7iTIJ5HZ6fY"}]}}}},{"_id":"5f494791a575399a45020f87","component":"data2-core-types@Component","params":{"definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"html@input","params":{"style":{"width":"100%","height":"100%","boxSizing":"border-box","background":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"checkbox"}}],"then":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"comparisonSubject":true}}],"then":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"checkboxFill"}}}},"rgba(0,118,255,1)"]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"content":"none"},"component":"data2-core-primitives@String"}}},"else":{"params":{"content":"none"},"component":"data2-core-primitives@String"}}},"color":{"params":{"reference":"super.textColor"},"component":"data2-core-types@Reference"},"border":"none","outline":"none","appearance":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"orient","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"vertical"}}],"then":"slider-vertical","else":"none"}},"cursor":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"super.cursor"}}}},{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":true}}],"then":{"component":"data2-core-primitives@String","params":{"content":"not-allowed"}},"else":{"component":"data2-core-primitives@String","params":{"content":"auto"}}}}]},"component":"data2-core-logical-operators@Or"},"fontWeight":{"component":"data2-core-primitives@String","params":{"content":"inherit"}},"paddingLeft":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.left"}}}}},"component":"data2-core-comparison-operators@Different"}],"then":{"params":{"portions":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.left"}}}},{"component":"data2-core-primitives@String","params":{"content":"px"}}]},"component":"data2-core-functions@StringConcat"}}},"paddingRight":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.right"}}}}},"component":"data2-core-comparison-operators@Different"}],"then":{"params":{"portions":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.right"}}}},{"component":"data2-core-primitives@String","params":{"content":"px"}}]},"component":"data2-core-functions@StringConcat"}}},"caretColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"caretColor"}}}},"textAlign":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"super.textFormat.textAlign"}}},"component":"data2-core-types@Reference"}},"tabIndex":{"component":"data2-core-types@Reference","params":{"reference":"tabIndex","source":"VIRTUAL_COMPONENT_PARAMS"}},"step":{"component":"data2-core-types@Reference","params":{"reference":"step","source":"VIRTUAL_COMPONENT_PARAMS"}},"min":{"component":"data2-core-types@Reference","params":{"reference":"min","source":"VIRTUAL_COMPONENT_PARAMS"}},"max":{"component":"data2-core-types@Reference","params":{"reference":"max","source":"VIRTUAL_COMPONENT_PARAMS"}},"maxLength":{"params":{"then":{"params":{},"component":"data2-core-primitives@Undefined"},"compare":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"maxLength","source":"VIRTUAL_COMPONENT_PARAMS"}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":""}}},"component":"data2-core-comparison-operators@Equal"}],"else":{"component":"data2-core-types@Reference","params":{"reference":"maxLength","source":"VIRTUAL_COMPONENT_PARAMS"}}},"component":"data2-core-flow@If"},"orient":{"component":"data2-core-types@Reference","params":{"reference":"orient","source":"VIRTUAL_COMPONENT_PARAMS"}},"type":{"component":"data2-core-types@Reference","params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"}},"placeholder":{"component":"data2-core-logical-operators@Or","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":"placeholder","source":"VIRTUAL_COMPONENT_PARAMS"}},{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}}]}},"value":{"params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"defaultCase":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}},"mask":{"component":"data2-core-types@Reference","params":{"reference":"mask","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"date"}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":"YYYY-MM-DD"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"month"}}}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":{"component":"data2-core-primitives@String","params":{"content":"YYYY-MM"}}}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"datetime-local"}}}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":{"component":"data2-core-primitives@String","params":{"content":"YYYY-MM-DDTHH:mm"}}}}}}]},"component":"data2-core-flow@Switch"},"onChange":{"component":"data2-core-types@DataLinkSetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}},"recordUpsert":{"component":"data2-core-types@Reference","params":{"reference":"recordUpsert","source":"VIRTUAL_COMPONENT_PARAMS"}},"onChange":{"component":"data2-core-types@Reference","params":{"reference":"onChange","source":"VIRTUAL_COMPONENT_PARAMS"}},"mask":{"component":"data2-core-types@Reference","params":{"reference":"mask","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"defaultChecked":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"checkbox"}}],"then":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"else":""}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field"}},"opacity":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"}],"then":0.38,"else":1}},"disabled":{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"autoComplete":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Equal","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"builder"}},"comparisonSubject":true}}],"then":"new-password","else":{"params":{"compare":{"params":{"comparisonSubject":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoComplete"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Identical"},"else":{"params":{"components":[{"params":{"reference":"autoComplete","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},{"component":"data2-core-primitives@String","params":{"content":"off"}}]},"component":"data2-core-logical-operators@Or"}},"component":"data2-core-flow@If"}}},"data-lpignore":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"reference":"autoComplete","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"}],"then":"","else":"true"}},"onFocus":{"component":"data2-core-types@Reference","params":{"reference":"onFocus","source":"VIRTUAL_COMPONENT_PARAMS"}},"onBlur":{"component":"data2-core-types@Reference","params":{"reference":"onBlur","source":"VIRTUAL_COMPONENT_PARAMS"}},"onKeyPress":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onKeyPress"}}}},"autoCapitalize":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoCapitalize"}}}},"spellCheck":{"params":{"expression":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"spellCheck"}}},"component":"data2-core-types@Reference"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"value":{"component":"data2-core-primitives@String","params":{"content":"true"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"value":{"component":"data2-core-primitives@String","params":{"content":"false"}}}}]},"component":"data2-core-flow@Switch"}}}}},"component":"DataFieldBlock","package":"data2-blocks","extends":"data2-core-primitives@Block"}},{"_id":"60e6362dda3861ca6516d9b1","component":"data2-core-types@Component","params":{"definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-controls@JsonViewer","params":{"json":{"params":{"reference":"json"},"component":"data2-core-types@Reference"},"theme":{"params":{"reference":"theme"},"component":"data2-core-types@Reference"},"collapsed":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"collapsed"}}},"component":"data2-core-types@Reference"}}}}},"component":"JsonViewer","package":"data2-blocks","extends":"data2-core-primitives@Block"}},{"_id":"6a62715961501108689e973a","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"XaKyb6x3qMpZupICoqsnY","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"XaKyb6x3qMpZupICoqsnY"}}}}}},{"_id":"6a6228810ff5a8d431e8cee3","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"L--dVhugwB17-_W00pA7S","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"L--dVhugwB17-_W00pA7S"}}}}}},{"_id":"6a62728a61501108689e9743","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"iW3NufyoQGjGAwhmVZ3ow","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"iW3NufyoQGjGAwhmVZ3ow"}}}}}},{"_id":"6a6228730ff5a8d431e8cee0","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"extends":"data2-core-primitives@Block","package":"natura-geomarketing","component":"PCOvC0KQdskFojBsWZBfd","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"PCOvC0KQdskFojBsWZBfd"}}}}}},{"_id":"614df5ca33fad3ebebb9879b","component":"data2-core-types@Component","params":{"package":"http","component":"CustomHTTPDataSource","extends":"data2-core@DataSource","definition":{"params":{},"component":"data2-core-types@Definition"},"renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"6a6993dccc0e5e3e8140fd68","component":"data2-core-types@Component","system":"6a57cfe9673eb7e3c5e57784","params":{"component":"3EIFSsdsdy7iTIJ5HZ6fY","package":"natura-geomarketing","definition":{"component":"data2-core-flow@Series","params":{"actions":[{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@ArrayMap","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"features"}},"mapper":{"component":"data2-core-functions@Function","params":{"parameters":["f"],"body":{"name":{"component":"data2-core-types@Reference","params":{"reference":"f.properties.name"}},"normName":{"component":"data2-core-functions@StringTrim","params":{"string":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@toUpperCase","params":{"string":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"f.properties.name"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[àáâãäåÀÁÂÃÄÅ]","flags":"g"}},"patch":"a"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[èéêëÈÉÊË]","flags":"g"}},"patch":"e"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ìíîïÌÍÎÏ]","flags":"g"}},"patch":"i"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[òóôõöÒÓÔÕÖ]","flags":"g"}},"patch":"o"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ùúûüÙÚÛÜ]","flags":"g"}},"patch":"u"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[çÇ]","flags":"g"}},"patch":"c"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ñÑ]","flags":"g"}},"patch":"n"}}}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[^A-Z0-9]+","flags":"g"}},"patch":" "}}}},"keys":{"component":"data2-core-functions@Uniq","params":{"array":{"component":"data2-core-functions@ArrayMap","params":{"array":{"component":"data2-core-functions@Reduce","params":{"array":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparison-operators@Equal","params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@Reference","params":{"reference":"f.geometry"}},"prop":"type"}},"comparisonSubject":"MultiPolygon"}}],"then":{"component":"data2-core-functions@Reduce","params":{"array":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@Reference","params":{"reference":"f.geometry"}},"prop":"coordinates"}},"reducer":{"component":"data2-core-functions@Function","params":{"parameters":["acc","poly"],"body":{"component":"data2-core-functions@ArrayConcat","params":{"arrays":[{"component":"data2-core-types@Reference","params":{"reference":"acc"}},{"component":"data2-core-types@Reference","params":{"reference":"poly"}}]}}}},"initialValue":[]}},"else":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@Reference","params":{"reference":"f.geometry"}},"prop":"coordinates"}}}},"reducer":{"component":"data2-core-functions@Function","params":{"parameters":["acc","ring"],"body":{"component":"data2-core-functions@ArrayConcat","params":{"arrays":[{"component":"data2-core-types@Reference","params":{"reference":"acc"}},{"component":"data2-core-types@Reference","params":{"reference":"ring"}}]}}}},"initialValue":[]}},"mapper":{"component":"data2-core-functions@Function","params":{"parameters":["p"],"body":{"component":"data2-core-functions@StringConcat","params":{"portions":["",{"component":"data2-core-numerical-operators@Divide","params":{"components":[{"component":"data2-core-functions@Round","params":{"value":{"component":"data2-core-numerical-operators@Multiply","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":"p.0"}},1000]}}}},1000]}},"_",{"component":"data2-core-numerical-operators@Divide","params":{"components":[{"component":"data2-core-functions@Round","params":{"value":{"component":"data2-core-numerical-operators@Multiply","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":"p.1"}},1000]}}}},1000]}}]}}}}}}}}}}}}},"resultName":"withKeys"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@ArrayFilter","params":{"array":{"component":"data2-core-functions@ArrayMap","params":{"array":{"component":"data2-core-functions@Split","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"municipio"}},"needle":","}},"mapper":{"component":"data2-core-functions@Function","params":{"parameters":["t"],"body":{"component":"data2-core-functions@StringTrim","params":{"string":{"component":"data2-core-types@Reference","params":{"reference":"t"}}}}}}}},"filter":{"component":"data2-core-functions@Function","params":{"parameters":["t"],"body":{"component":"data2-core-comparison-operators@Different","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"t"}},"comparisonSubject":""}}}}}},"resultName":"targetsRaw"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@ArrayMap","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"targetsRaw"}},"mapper":{"component":"data2-core-functions@Function","params":{"parameters":["t"],"body":{"component":"data2-core-functions@StringTrim","params":{"string":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@toUpperCase","params":{"string":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-functions@Replace","params":{"haystack":{"component":"data2-core-types@Reference","params":{"reference":"t"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[àáâãäåÀÁÂÃÄÅ]","flags":"g"}},"patch":"a"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[èéêëÈÉÊË]","flags":"g"}},"patch":"e"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ìíîïÌÍÎÏ]","flags":"g"}},"patch":"i"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[òóôõöÒÓÔÕÖ]","flags":"g"}},"patch":"o"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ùúûüÙÚÛÜ]","flags":"g"}},"patch":"u"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[çÇ]","flags":"g"}},"patch":"c"}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[ñÑ]","flags":"g"}},"patch":"n"}}}},"needle":{"component":"data2-core-types@Regex","params":{"pattern":"[^A-Z0-9]+","flags":"g"}},"patch":" "}}}}}}}},"resultName":"targetsNorm"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@ArrayFilter","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"withKeys"}},"filter":{"component":"data2-core-functions@Function","params":{"parameters":["f"],"body":{"component":"data2-core-functions@Includes","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"targetsNorm"}},"pattern":{"component":"data2-core-types@Reference","params":{"reference":"f.normName"}}}}}}}},"resultName":"targets"}},{"component":"data2-core-flow@Step","params":{"action":{"component":"data2-core-functions@ArrayFilter","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"withKeys"}},"filter":{"component":"data2-core-functions@Function","params":{"parameters":["f"],"body":{"component":"data2-core-logical-operators@And","params":{"components":[{"component":"data2-core-logical-operators@Not","params":{"subject":{"component":"data2-core-functions@Includes","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"targetsNorm"}},"pattern":{"component":"data2-core-types@Reference","params":{"reference":"f.normName"}}}}}},{"component":"data2-core-comparison-operators@GreaterThan","params":{"subject":{"component":"data2-core-functions@Count","params":{"subject":{"component":"data2-core-functions@ArrayFilter","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"targets"}},"filter":{"component":"data2-core-functions@Function","params":{"parameters":["t"],"body":{"component":"data2-core-comparison-operators@EqualOrGreaterThan","params":{"subject":{"component":"data2-core-functions@Count","params":{"subject":{"component":"data2-core-functions@ArrayFilter","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"f.keys"}},"filter":{"component":"data2-core-functions@Function","params":{"parameters":["k"],"body":{"component":"data2-core-functions@Includes","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"t.keys"}},"pattern":{"component":"data2-core-types@Reference","params":{"reference":"k"}}}}}}}}}},"comparisonSubject":2}}}}}}}},"comparisonSubject":0}}]}}}}}},"resultName":"neighbors"}},{"component":"data2-core-functions@ArrayMap","params":{"array":{"component":"data2-core-types@Reference","params":{"reference":"neighbors"}},"mapper":{"component":"data2-core-functions@Function","params":{"parameters":["f"],"body":{"component":"data2-core-types@Reference","params":{"reference":"f.name"}}}}}}]}}}},{"_id":"60019e844369e2af95919c94","component":"data2-core-types@Component","params":{"definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"html@iframe","params":{"frameBorder":"0","src":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-types@Reference","params":{"reference":"builder"}}],"then":"about:blank","else":{"component":"data2-core-types@Reference","params":{"reference":"url"}}}},"allow":"autoplay","allowFullScreen":{"component":"data2-core-types@Reference","params":{"reference":"allowFullScreen"}},"loading":"lazy","style":{"position":"absolute","width":"100%","height":"100%","border":"none","padding":"none","overflow":"auto"}}}}},"component":"IframeBlock","package":"data2-blocks","extends":"data2-core-primitives@Block"}}]}},{"reference":"ExternalComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"_id":"5f060d9b1ca96def42f3c40a","displayName":"Builder AuthStrategy","component":"data2-core@AuthenticationStrategy","system":"5e3b796d6228227ca637d0f4","params":{"credentials":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"UsersDataSet"}},"sessionShared":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"password":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},"email":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},"resetPassword":{"resetEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"nR6Q9d7qdaPtZhJ28Q8Dy"},"component":"data2-core-types@ArtboardReference"}},"resetSuccessEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"yPCJBTKo8q1Mw0B_3Kpwl"},"component":"data2-core-types@ArtboardReference"}}},"session":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"name"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"sessionMetadata":{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},{"params":{"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"active"}},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.status"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is active user"}]},"component":"data2-core-logical-operators@And","displayName":"is active and has _id"}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $user"},{"params":{"action":{"params":{"pattern":{"component":"data2-core-primitives@String","params":{"content":"@data2.app"}},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.email"}}}}},"component":"data2-core-functions@Includes","displayName":"isData2User"},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}},"component":"data2-core-flow@Step"},{"params":{"action":{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.d2_app"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"management"}}]},"component":"data2-core-functions@Includes","displayName":"isData2App Exception"},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}},"component":"data2-core-flow@Step"},{"params":{"resultName":{"component":"data2-core-primitives@String","params":{"content":"has_access"}},"action":{"params":{"compare":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}}}]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"actions":[{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $metadata.system"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":"filter","args":[{"_id":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}}},"component":"data2-core-logical-operators@Not","displayName":"systems not found"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check system found"},{"params":{"actions":[{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"organization":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"cursor.current.organization._id"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"}]},"component":"data2-core-flow@Parallel"}]},"component":"data2-core-flow@Series","displayName":"common setup"},{"params":{"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"compare":{"params":{"components":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"system"}}],"body":{"params":{"expression":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.visibility"}}}},"defaultCase":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"public"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"billing-owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}},{"component":"data2-core-primitives@String","params":{"content":"member"}}]},"component":"data2-core-functions@Includes","displayName":"has role with access"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"collaborator"}}},"component":"data2-core-comparison-operators@Equal","displayName":"is collaborator"}]},"component":"data2-core-logical-operators@And","displayName":"is org member and collaborator"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is collaborator"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is collaborator and has system access"}]},"component":"data2-core-logical-operators@Or"}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"private"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}}]},"component":"data2-core-functions@Includes","displayName":"is owner/admin"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is owner/admin"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is member and has system access"}]},"component":"data2-core-logical-operators@Or","displayName":"is owner/admin or has system access"}}}]},"component":"data2-core-flow@Switch"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-logical-operators@And","displayName":"has org member for all systems in request"}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@Step"},{"params":{"then":{"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"else":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"},"compare":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"has_access"}}}}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"sessionExpiresIn":{"component":"data2-core-primitives@Number","params":{"content":604800}},"sessionRenewal":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"identityProviders":[{"provider":{"component":"data2-core-primitives@String","params":{"content":"GOOGLE"}},"clientId":{"component":"data2-core-primitives@String","params":{"content":"55470861603-22pvfc1t49lp8cjnbfk71s5ct9hp1q1v.apps.googleusercontent.com"}}}]},"reference":"Data2BuilderAuthentication","updatedAt":"2026-08-06T19:31:23.557Z"},{"_id":"5f060ee51ca96def42f3fbd1","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"UsersTableDataSource"}},"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}}},"reference":"UsersDataSet","updatedAt":"2026-03-04T21:16:40.291Z"},{"_id":"68f938cf5fb002e33752a43d","displayName":"InternalApplicationsAuthenticationStrategy","component":"data2-core@AuthenticationStrategy","system":"5e3b796d6228227ca637d0f4","params":{"resetPassword":{"resetEmail":{"artboard":{"params":{"preventEvaluation":true},"component":"data2-core-types@ArtboardReference"}},"resetSuccessEmail":{"artboard":{"params":{"preventEvaluation":true},"component":"data2-core-types@ArtboardReference"}}},"verifier":{"disabled":false,"component":"data2-core-types@DataSetReference","params":{"reference":"InternalApplicationsVerifierDataSet"}},"password":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"secret"}},"credentials":[{"component":"data2-core-types@FieldReference","params":{"key":"application"}},{"component":"data2-core-types@FieldReference","params":{"key":"secret"}},{"component":"data2-core-types@FieldReference","params":{"key":"status"}}],"session":[{"component":"data2-core-types@FieldReference","params":{"key":"application"}},{"component":"data2-core-types@FieldReference","params":{"key":"secret"}},{"component":"data2-core-types@FieldReference","params":{"key":"status"}}]},"reference":"InternalApplicationsAuthenticationStrategy","updatedAt":"2025-10-22T20:08:54.442Z","createdAt":"2025-10-22T20:04:31.198Z"},{"component":"data2-core@DataSet","reference":"SessionsDataSet","params":{"requiresConfirmation":false,"pk":"key","datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"data2sessionsdatasource"}}}},{"reference":"UsersTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@ServerSessionsDataSource","reference":"data2sessionsdatasource","params":{}},{"_id":"5f1600b70528b92601890f75","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ServiceComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ServiceComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:20.208Z","createdAt":"2020-07-20T20:38:15.436Z"},{"_id":"5f1602190528b92601890f7d","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":40000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentInstancesTableDataSource"}},"sort":{"_id":1},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:07.089Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"5f16017f0528b92601890f7a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"FileComponentInstancesDataSet","updatedAt":"2023-08-03T16:08:18.193Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"5f1601990528b92601890f7b","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ApplicationsTableDataSource"}},"pk":"_id","autoEdit":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"params":{"column":"_id"},"component":"data2-core-types@DataSetColumnReference"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"ApplicationsDataSet","updatedAt":"2022-12-13T21:51:13.995Z","createdAt":"2020-07-20T20:42:01.657Z"},{"_id":"5f16172a0528b92601890f85","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":10000}}},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"DataComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"DataComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:12.831Z","createdAt":"2020-07-20T22:14:02.097Z"},{"_id":"60ddc1bbda3861ca65407c5a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"pagination":{"size":1},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"}},"reference":"OneFileComponentInstancesDataSet","updatedAt":"2022-02-09T19:44:38.036Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"63ee3d38b8c9838c62fb05fb","displayName":"DriveFileComponentInstancesDataSet","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core@Drive"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"DriveFileComponentInstancesDataSet","updatedAt":"2025-12-04T21:58:32.367Z","createdAt":"2023-02-16T14:27:07.797Z"},{"reference":"ServiceComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a7240abab11eee26d640c2e","component":"data2-rush@HTTPListener","reference":"AwW_11Nkg-dKksP7ic8V8","system":"6a57cfe9673eb7e3c5e57784","params":{},"versioning":"enabled","bundleId":null},{"_id":"6a875126f4280e4792668bb3","component":"data2-rush@HTTPListener","reference":"osmOverpassProxy","system":"6a57cfe9673eb7e3c5e57784","params":{},"versioning":"enabled","bundleId":null,"deleteMetadata":{}},{"_id":"6a6992f4e1053d806729e9f5","component":"data2-core-connect@HttpRequest","reference":"gh_MFH1HrDf2o2JrlseD0","system":"6a57cfe9673eb7e3c5e57784","params":{"parent":{"component":"data2-core-types@InstanceReference","params":{"component":"data2-core-connect@HttpClient","reference":"N15zEWu4GvXztmPPTjOBt"}},"method":{"component":"data2-core-primitives@String","params":{"content":"GET"}},"useProxy":true,"rawResponse":true,"requestParameters":[{"component":"data2-core-types@PrimitiveParameter","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"uf"}},"component":"data2-core-primitives@String"}}]}},{"_id":"6a6992f4e1053d806729e9f4","component":"data2-core-connect@HttpClient","reference":"N15zEWu4GvXztmPPTjOBt","system":"6a57cfe9673eb7e3c5e57784","params":{}}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a74acf3dad5912765097ccb","component":"data2-core-primitives@Router","system":"6a57cfe9673eb7e3c5e57784","displayName":"TimelineSimulator-Router","params":{"routes":[{"component":"data2-core-primitives@Route","params":{"path":"/","name":"Timeline de Fechamentos","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"kYmaNnWZ5NlRgXfOSuhwp"}},"policies":[{"component":"data2-core-auth@Policy","params":{"authenticationStrategy":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"timelineSimAuthFinal"}}}}]}},{"component":"data2-core-primitives@Route","params":{"path":"/sessions/:session_id","name":"Sessão compartilhada","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"__C6d2F-yqwEkSNWNK9IH"}},"policies":[{"component":"data2-core-auth@Policy","params":{"authenticationStrategy":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"timelineSimAuthFinal"}}}}]}}],"login":{"component":"data2-core-types@ArtboardReference","params":{"reference":"OjHwaLdtsQ130Tg6z4N_t"}}},"reference":"timelineSimRouter"},{"_id":"6a5e8d605c2b589c492f447a","component":"data2-core-primitives@Router","system":"6a57cfe9673eb7e3c5e57784","displayName":"Hub-Router","params":{"login":{"disabled":true,"component":"data2-core-types@ArtboardReference","params":{}},"initialPath":{"component":"data2-core-primitives@String","params":{"content":"/"}},"routes":[{"component":"data2-core-primitives@Route","params":{"path":{"component":"data2-core-primitives@String","params":{"content":"/"}},"onEnter":{"disabled":true,"component":"data2-core-types@Interaction","params":{}},"content":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"k6yQNLR1Pt1-zCBxtryLD"}}}},{"component":"data2-core-primitives@Route","params":{"path":{"component":"data2-core-primitives@String","params":{"content":"/acessos"}},"content":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"NKi0_IAvHL5fCXkrT5so3"}}}},{"component":"data2-core-primitives@Route","params":{"path":{"component":"data2-core-primitives@String","params":{"content":"/simulador"}},"content":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"5uzSLIEdUa3QR24u4A-pM"}}}}]},"reference":"MNv9vHdKUB_s3iOqiMkiB"},{"_id":"6a74aea863667431f75e46f0","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"login","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"login"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"OjHwaLdtsQ130Tg6z4N_t"},{"_id":"6a74b63815209eb021fcad40","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"sessao compartilhada","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"sessao compartilhada"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"__C6d2F-yqwEkSNWNK9IH"},{"_id":"6a74abe6468fc70c2ef50103","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"main","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768}},"fill":{"params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"}},"reference":"kYmaNnWZ5NlRgXfOSuhwp"},{"_id":"6a5ecd59c28b758182f0d35f","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"Simulador | Fechamento de setores","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768}},"fill":{"params":{"content":"#ffffff"},"component":"data2-core-primitives@String"},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"5uzSLIEdUa3QR24u4A-pM"},{"_id":"6a5f1bc4951c23b0077674d1","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"Gestão de acessos","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768}},"fill":{"params":{"content":"#ffffff"},"component":"data2-core-primitives@String"},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"NKi0_IAvHL5fCXkrT5so3"},{"_id":"6a5face141de3fb32ace7537","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"Início","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768}},"fill":{"params":{"content":"#ffffff"},"component":"data2-core-primitives@String"},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"k6yQNLR1Pt1-zCBxtryLD"},{"_id":"edee90b922e5038ec1d1c76c","parent":"6a74aea863667431f75e46f0","component":"natura-geomarketing@timeline-sim-login","system":"6a57cfe9673eb7e3c5e57784","displayName":"Login form instance","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768,"exactWidth":false,"exactHeight":false}}}},"reference":"Y87XaCN1xFnP4It1hGn6u"},{"_id":"2544724b6e0a08f3b8b81464","parent":"6a74b63815209eb021fcad40","component":"natura-geomarketing@timeline_sim_session_read","system":"6a57cfe9673eb7e3c5e57784","displayName":"Session read instance","params":{"shared":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"timeline-shared-sessionsDataSet"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":900}}}},"reference":"2xQ6i0KYfzxTWFEJPJCv6"},{"_id":"6a7a4288aaf288895983339d","parent":"6a74abe6468fc70c2ef50103","component":"natura-geomarketing@xoLKR_Mb_n2vkwfmdExLH","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":0,"exactY":0,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false}}},"sessionsDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"timeline-shared-sessionsDataSet"}},"sharedDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"timeline-shared-sessionsDataSet"}},"dominantesCsvUrl":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"yw_TQdp0QXEk6JsrliAqP"}}}},{"_id":"6a64499070fbd195b8e6aa5d","parent":"6a5ecd59c28b758182f0d35f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":678,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":90,"exactX":false,"exactY":false,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"childrenMode":"rows"},"index":0},{"_id":"6a6448f470fbd195b8e6aa21","parent":"6a5ecd59c28b758182f0d35f","component":"natura-geomarketing@n-E3kblbz0EwqfdH7YVjX","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":70,"exactWidth":false,"exactHeight":70}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"top":true,"left":true,"right":true,"growH":false}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(241,241,248,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"index":1},{"_id":"6a64494170fbd195b8e6aa2b","parent":"6a5f1bc4951c23b0077674d1","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":675,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":90,"exactX":false,"exactY":false,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":0},{"_id":"6a64487c70fbd195b8e6aa1f","parent":"6a5f1bc4951c23b0077674d1","component":"natura-geomarketing@n-E3kblbz0EwqfdH7YVjX","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":70,"exactWidth":false,"exactHeight":70}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":0,"exactY":0,"top":true,"left":true,"right":true,"growH":false}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(241,241,248,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"index":1},{"_id":"6a67a38e19eeef7ac1c378d5","parent":"6a5face141de3fb32ace7537","component":"natura-geomarketing@n-E3kblbz0EwqfdH7YVjX","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":70,"exactWidth":false,"exactHeight":70}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":0,"exactY":0,"top":true,"left":true,"right":true,"growH":false}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(241,241,248,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"index":1},{"_id":"6a72159a40a65f13df64713c","parent":"6a64499070fbd195b8e6aa5d","component":"natura-geomarketing@4hvch1V6MXqBr9rL-nvAu","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1326,"height":60,"exactWidth":1326,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":0,"exactX":20,"exactY":0,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"width":{"bottom":"1"},"color":"#e7e7e7"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"index":{"component":"data2-core-primitives@Number","params":{"content":1}}},"componentState":{"component":"data2-core-primitives@String","params":{"content":"simulador"}}},"index":0},{"_id":"6a64499070fbd195b8e6aa5f","parent":"6a64499070fbd195b8e6aa5d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#f2f2f2","radius":{"leftTop":"0"},"width":{"left":"0","top":"0","right":"0","bottom":"1"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1304,"height":588,"exactWidth":1304,"exactHeight":false,"growHeightWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":60,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"childrenMode":"cols","textFormat":{"disabled":true,"component":"data2-core-types@TextFormat","params":{}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"scroll":{"disabled":false,"component":"data2-core-types@Scroll","params":{"y":true}},"hide":{"params":{"subject":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"done"}}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-logical-operators@Not"}},"index":1},{"_id":"6a6c5b8c40da54940c0f9c49","parent":"6a64499070fbd195b8e6aa5d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1304,"height":588,"exactWidth":1304,"exactHeight":false,"growHeightWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":60,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"0"}}},"hide":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"done"}}},"component":"data2-core-comparison-operators@Equal"}},"index":2},{"_id":"6a64494170fbd195b8e6aa33","parent":"6a64494170fbd195b8e6aa2b","component":"natura-geomarketing@4hvch1V6MXqBr9rL-nvAu","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1326,"height":60,"exactWidth":1326,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"20","y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"width":{"bottom":"1"},"color":"#e7e7e7"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"text":""},"componentState":{"component":"data2-core-primitives@String","params":{"content":"gestaoDeAcessos"}}},"index":0},{"_id":"6a64494170fbd195b8e6aa2c","parent":"6a64494170fbd195b8e6aa2b","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1306,"height":588,"exactWidth":1306,"exactHeight":false,"growHeightWithContent":true,"minHeight":588}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":30,"y":5,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"text":"","childrenMode":"cols"},"index":1},{"_id":"6a64499070fbd195b8e6aa64","parent":"6a64499070fbd195b8e6aa5f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":540,"growHeightWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"fixed":false,"exactX":0,"exactY":20,"top":true,"left":true,"growH":false,"bottom":true,"growV":false}},"childrenMode":"rows","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1.5"}}}},"index":1},{"_id":"6a64499070fbd195b8e6aa60","parent":"6a64499070fbd195b8e6aa5f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":1007,"exactWidth":false,"exactHeight":1007,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":0,"exactX":false,"exactY":false,"bottom":false,"right":false,"top":true,"growV":false,"fixed":false,"left":true,"growH":false}},"childrenMode":"rows","style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1.33"}}}},"index":2},{"_id":"6a6c5ba940da54940c0f9c4b","parent":"6a6c5b8c40da54940c0f9c49","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":323,"height":18,"exactWidth":323.07,"exactHeight":18.439999999999998}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":491,"y":285,"exactX":false,"exactY":false,"top":false,"left":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"center"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Nenhum setor selecionado"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(97,97,97,1)"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":16}}}},{"_id":"6a643a2a3cf95d21cfe3c83d","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTopBarComponent","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":60,"exactHeight":60}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"n-E3kblbz0EwqfdH7YVjX"},{"_id":"6a64494170fbd195b8e6aa2d","parent":"6a64494170fbd195b8e6aa2c","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":600,"exactWidth":false,"exactHeight":600}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":9,"y":-6,"exactX":8.5,"exactY":-6,"top":true,"left":true,"bottom":true,"growV":false,"fixed":false}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":17}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"text":"","childrenMode":"rows","repeat":{"params":{"end":{"component":"data2-core-primitives@Number","params":{"content":13}},"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@ArraySlice"},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"col1"}}}},{"_id":"6a64494170fbd195b8e6aa2f","parent":"6a64494170fbd195b8e6aa2c","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":600,"exactWidth":false,"exactHeight":600}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":9,"y":-6,"exactX":8.5,"exactY":-6,"top":true,"left":true,"bottom":true,"growV":false,"fixed":false}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":17}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"text":"","childrenMode":"rows","repeat":{"params":{"end":{"component":"data2-core-primitives@Number","params":{"content":26}},"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"start":{"component":"data2-core-primitives@Number","params":{"content":13}}},"component":"data2-core-functions@ArraySlice"},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"col2"}}}},{"_id":"6a64494170fbd195b8e6aa31","parent":"6a64494170fbd195b8e6aa2c","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":600,"exactWidth":false,"exactHeight":600}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":9,"y":-6,"exactX":8.5,"exactY":-6,"top":true,"left":true,"bottom":true,"growV":false,"fixed":false}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":17}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"text":"","childrenMode":"rows","repeat":{"params":{"end":{"component":"data2-core-primitives@Number","params":{"content":39}},"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"start":{"component":"data2-core-primitives@Number","params":{"content":26}}},"component":"data2-core-functions@ArraySlice"},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"col3"}}}},{"_id":"6a5fa7cd74f960d571569086","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubPageHeaderComponent","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1250,"height":60,"exactWidth":false,"exactHeight":false}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"componentStates":[{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"gestaoDeAcessos"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"Jl_gD4rwsECW2tOfUczYm"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"simulador"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"tPvK9qsMTsuUVVoEjEnL1"}}}}],"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"4hvch1V6MXqBr9rL-nvAu"},{"_id":"6a64499070fbd195b8e6aa65","parent":"6a64499070fbd195b8e6aa64","component":"natura-geomarketing@O4-zOAoAC9BwIp-XZ0ekO","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":184,"height":60,"exactWidth":183.77,"exactHeight":60}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":10,"exactX":false,"exactY":false,"top":true,"left":true,"right":false}},"text":"","textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"children":{"locked":true}},"text":""},"index":0},{"_id":"6a64499070fbd195b8e6aa66","parent":"6a64499070fbd195b8e6aa64","component":"natura-geomarketing@yJwIwtjYNeYGoGF68aYQM","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#9a9b9c","radius":{"leftTop":"13"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}},"disabled":false},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":440,"exactWidth":640,"exactHeight":440}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":3,"y":100,"exactX":2.59,"exactY":100,"left":true,"top":true,"bottom":false,"growV":false,"growH":false,"right":true,"fixed":false}},"style":{"transformOrigin":{"component":"data2-core-primitives@String","params":{"content":"top left"}},"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}}},"assignments":{"params":{"fieldsList":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios_normalized"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"key":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m"}}},"component":"data2-core-types@Reference"},"value":{"component":"data2-core-primitives@String","params":{}}}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@FieldSet"},"groups":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setores_destino"}}},"component":"data2-core-types@Getter"},"ufs":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.UF"}}}},"store":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NLO6dHteJxVmbynNHsqPv"}},"municipio":{"params":{"portions":{"params":{"array":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}}},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}},{"component":"data2-core-primitives@String","params":{"content":"i"}},{"component":"data2-core-primitives@String","params":{"content":"l"}}],"body":{"params":{"portions":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m"}}},"component":"data2-core-types@Reference"},{"params":{"compare":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"l.length"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{"components":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"i"}}},"component":"data2-core-types@Reference"},{"component":"data2-core-primitives@Number","params":{"content":1}}]},"component":"data2-core-numerical-operators@Add"}},"component":"data2-core-comparison-operators@Equal"},"else":{"component":"data2-core-primitives@String","params":{"content":","}}},"component":"data2-core-flow@If"}]},"component":"data2-core-functions@StringConcat"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-functions@StringConcat"},"colors":{"params":{"fieldsList":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setores_destino"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}},{"component":"data2-core-primitives@String","params":{"content":"i"}},{"component":"data2-core-primitives@String","params":{"content":"l"}}],"body":{"key":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.sector"}}},"component":"data2-core-types@Reference"},"value":{"params":{"color":{"params":{"color":{"h":{"params":{"components":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"i"}}},"component":"data2-core-types@Reference"},{"params":{"components":[{"params":{"components":[{"component":"data2-core-primitives@Number","params":{"content":10}},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"l.length"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-numerical-operators@Multiply"},{"component":"data2-core-primitives@Number","params":{"content":36}}]},"component":"data2-core-numerical-operators@Divide"}]},"component":"data2-core-numerical-operators@Multiply"},"s":{"component":"data2-core-primitives@String","params":{"content":"1"}},"l":{"component":"data2-core-primitives@String","params":{"content":"0.5"}},"alpha":{"component":"data2-core-primitives@String","params":{"content":"1"}}}},"component":"data2-core-color@ColorFormatHsl"}},"component":"data2-core-color@ColorFormatHex"}}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@FieldSet"}},"index":1},{"_id":"6a64499070fbd195b8e6aa67","parent":"6a64499070fbd195b8e6aa64","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":40,"exactWidth":640,"exactHeight":40,"growHeightWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"text":"","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a64499070fbd195b8e6aa62","parent":"6a64499070fbd195b8e6aa60","component":"natura-geomarketing@ONebofT7KQiRJeb0mlziC","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":100,"exactWidth":546,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":true,"growH":false}},"childrenMode":"grid","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"index":2},{"_id":"6a64499070fbd195b8e6aa61","parent":"6a64499070fbd195b8e6aa60","component":"natura-geomarketing@gm34nI8iHui1SSuwobYTT","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":200,"exactWidth":546,"exactHeight":200,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":true,"growH":false}},"childrenMode":"grid"}},"index":3},{"_id":"6a64499070fbd195b8e6aa63","parent":"6a64499070fbd195b8e6aa60","component":"natura-geomarketing@gfyeGJI77tJzdwSZTKCnV","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"fixed":false,"exactX":0,"exactY":20,"left":true,"top":true,"right":true,"growH":false}},"childrenMode":{"params":{"compare":{"params":{"components":[{"params":{"subject":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__DeviceController"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"width"}}}},"comparisonSubject":{"component":"data2-core-primitives@Number","params":{"content":1600}}},"component":"data2-core-comparison-operators@EqualOrLesserThan"}},"component":"data2-core-logical-operators@Not"},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"builder"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-logical-operators@Or"},"then":"cols","else":{"component":"data2-core-primitives@String","params":{"content":"rows"}}},"component":"data2-core-flow@If"},"text":""},"componentState":{"params":{"compare":{"params":{"components":[{"params":{"subject":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__DeviceController"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"width"}}}},"comparisonSubject":{"component":"data2-core-primitives@Number","params":{"content":1900}}},"component":"data2-core-comparison-operators@EqualOrLesserThan"}},"component":"data2-core-logical-operators@Not"},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"builder"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-logical-operators@Or"},"then":{"component":"data2-core-primitives@String","params":{"content":"cols"}},"else":{"component":"data2-core-primitives@String","params":{"content":"rows"}}},"component":"data2-core-flow@If"}}},{"_id":"6a650c67879c2f24acad741b","parent":"6a643a2a3cf95d21cfe3c83d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"logoContainer","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":163,"height":34,"exactWidth":163.06}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"20","y":"16","exactX":false,"exactY":false,"left":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"onMouseDown":{"disabled":false,"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"MNv9vHdKUB_s3iOqiMkiBInterface"}},"prop":"goTo","args":[{"component":"data2-core-primitives@String","params":{"content":"/"}}]}},"cursor":"pointer"},"index":0},{"_id":"6a6664093733859be08fd69a","parent":"6a643a2a3cf95d21cfe3c83d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"Copy of routesContainer","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1095,"height":47,"exactWidth":1095.13,"exactHeight":47.04}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"225","y":13,"exactX":false,"exactY":false,"top":false,"left":true,"bottom":true,"growV":false,"fixed":false,"right":false,"growH":false}},"childrenMode":"cols","textSize":{"component":"data2-core-primitives@Number","params":{"content":"16"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(27,27,43,1)"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":1,"reference":"routesContainer"},{"_id":"6a665f2d93f1e5c0b6a198b9","parent":"6a643a2a3cf95d21cfe3c83d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"body","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":323,"height":47,"exactWidth":323.07,"exactHeight":47.04}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"1003","y":13,"exactX":false,"exactY":false,"top":false,"left":false,"bottom":true,"growV":false,"fixed":false,"right":true,"growH":false}},"childrenMode":"reversed-cols","font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"16"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(27,27,43,1)"}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":2,"reference":"body"},{"_id":"6a64494170fbd195b8e6aa2e","parent":"6a64494170fbd195b8e6aa2d","component":"natura-geomarketing@0CZ7pC6NULlDQhV3hv-0G","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":10,"y":"10","exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa2e"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa2e"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}},"componentState":{"params":{"content":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa2e"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}}}},"comparisonSubject":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-primitives@String","break":true},"label":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"col1Repeater.email"}}},"component":"data2-core-types@Reference"}}},{"_id":"6a64494170fbd195b8e6aa30","parent":"6a64494170fbd195b8e6aa2f","component":"natura-geomarketing@0CZ7pC6NULlDQhV3hv-0G","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":10,"y":"10","exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa30"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa30"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}},"componentState":{"params":{"content":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa30"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}}}},"comparisonSubject":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-primitives@String","break":true},"label":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"col2Repeater.email"}}},"component":"data2-core-types@Reference"}}},{"_id":"6a64494170fbd195b8e6aa32","parent":"6a64494170fbd195b8e6aa31","component":"natura-geomarketing@0CZ7pC6NULlDQhV3hv-0G","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":10,"y":"10","exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa32"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa32"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}},"componentState":{"params":{"content":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a64494170fbd195b8e6aa32"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hover"}}}}}},"comparisonSubject":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-primitives@String","break":true},"label":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"col3Repeater.email"}}},"component":"data2-core-types@Reference"}}},{"_id":"6a5fcaba3a34c57f2ac6b32d","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubPageHeaderSimulador","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1250,"height":60,"exactWidth":false,"exactHeight":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"tPvK9qsMTsuUVVoEjEnL1"},{"_id":"6a5fa72174f960d57156905d","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubPageHeaderAcessos","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1260,"height":60,"exactWidth":1259.6699999999998,"exactHeight":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"Jl_gD4rwsECW2tOfUczYm"},{"_id":"6a64499070fbd195b8e6aa68","parent":"6a64499070fbd195b8e6aa67","component":"natura-geomarketing@1S_fB3hvzwUro47iuZ55p","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":false,"growH":false}},"childrenMode":"cols","textFormat":{"disabled":true,"component":"data2-core-types@TextFormat","params":{}}}},"index":2},{"_id":"6a64499070fbd195b8e6aa69","parent":"6a64499070fbd195b8e6aa67","component":"natura-geomarketing@_3BHHDtVc0ZK0wlfcnLM3","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":284,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true,"minHeight":500}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"fixed":false,"exactX":0,"exactY":20,"left":true,"top":true,"right":true,"growH":false}}},"componentState":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}}}},{"_id":"6a643ef570fbd195b8e6a9de","parent":"6a650c67879c2f24acad741b","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"logo","params":{"super":{"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(0,120,220,0.3)"},"disabled":true},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":false,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":43,"height":31,"exactWidth":42.74,"exactHeight":31.17}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"y":3,"exactX":false,"exactY":false,"top":false,"left":true,"x":0,"fixed":false}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"200"}}],"shadow":[{"component":"data2-core-types@Shadow","params":{"type":"complex","offsetX":"-1","offsetY":"-1","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(168,157,243,1)"}},"blur":"1"}},{"component":"data2-core-types@Shadow","params":{"type":"complex","offsetX":"0","offsetY":"0","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(168,157,243,1)"}},"blur":"2"}}],"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 0 149.2 118\" fill=\"#fff\" style=\"enable-background:new 0 0 139.2 98;\" xml:space=\"preserve\">\n <path d=\"M65.5,14c-3.1,6-6.8,19-6.8,19s-0.3,1-1-0.1c-0.4-0.7-5.6-7.8-8.2-11c-3-3.7-16.4-17.8-31.7-9.8C4,19.3,9.3,36.9,13.5,43.6\n\tc2.6,4,7.5,10.3,9,12.1c0.2,0.3,0.2,0.4-0.2,0.3c-1.2-0.3-4.3-1.1-9.9-2.6c-5.1-1.3-10-1.1-11.4,1.1c-1,1.6-1.6,4,0.4,7.8\n\tC5.4,69.5,16.6,81.2,35,85c0.4,0,0.3,0.4,0,0.9c-0.9,1.5-0.8,3.4,0.3,4.7c2.7,3.3,5.8,4.5,18,6.6c16.9,2.9,22.9-2.8,23.6-3.6\n\tc0.1,0,0.1,0,0.2,0c8.8,2.1,18,1.2,26.2-2.4c11.8-4.9,16.3-9.6,16.4-12.4s-6.1-2.2-7.9-2.7c-0.6-0.1-0.4-0.4,0-0.6\n\tc0.8-0.5,2.9-1.4,4.6-2.3c1.7-0.9,18.9-9.6,22.4-24.2c3.4-14.7-9.9-14.8-17-11.7s-13.9,6.4-15.4,7.1c-0.2,0.1-0.3,0-0.2-0.2\n\tc1.9-4.6,3.4-9.4,4.6-14.2c1.5-7,0.8-26-18.1-29.6C91.1,0.1,89.8,0,88.4,0C78.7,0.1,69.9,5.4,65.5,14 M66.2,43.9\n\tc1.5-4.2,2-6.3,4.5-14.4c3.1-10.1,9.7-18,20.4-14.8c12.5,3.8,9.6,16.4,7.6,21.7c-1.6,4.1-2.9,8.4-5.6,15.3c-2.1,5.6,2.5,5.7,4.9,5.2\n\tc2.8-0.6,11-4.8,16.1-7.2c3.8-1.7,10.8-6,14.8-1.9s-0.5,11.3-5.3,15.5c-4.8,4.3-17.5,9.6-21.4,11.2c-3.9,1.6-5.4,3.8-3.5,5\n\tc1.9,1.2,7.5,2.2,9.2,2.4c1.4,0.2,2.8,0.5,2.8,1.5c0,1.2-3.3,3.7-11,6.3c-5.2,1.6-10.7,2.2-16.1,1.6c-6.7-1.3-7.8-2.3-11-1.1\n\tc-2.3,0.9-2.5,2.8-5.9,4.2s-8.4,1.1-12.3,0.4s-10.6-1.3-11.3-4.4c-0.3-1.2,1.1-2,2.2-3.3c1.1-1.3,2.1-3.6-2.8-4.6\n\tc-1.3-0.3-14.7-2.4-23-6.6S8.1,65.1,8.9,62.7c0.7-1.9,2.6-2.1,5.2-1.6c2.9,0.6,15.8,4.1,19.7,4.1c1.6,0,2.8-0.3,3.4-1.3\n\tc1.5-2.7-3.2-5.9-7-10.5c-5.6-6.6-16.1-17.6-8.2-27.5c5.3-6.6,15.7-2.3,20,2.7c3.3,3.9,9.1,11.7,13.5,17.1c1.3,1.8,3.3,2.9,5.4,3.1\n\tc2.2,0,4.2-1.6,5.4-4.9\">\n </path>\n</svg>"}}},"index":0,"reference":"logo"},{"_id":"6a72a39a8ad1d21d283b9d05","parent":"6a650c67879c2f24acad741b","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"Copy of logoLabel","params":{"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(0,120,220,0.3)"},"disabled":true},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"params":{"content":"https://natura-hr-demo.data2.media/file/COiL3GBKUjCMIQHLigPb4"},"component":"data2-core-primitives@String"},"positionX":"center","positionY":"center","repeat":"no-repeat","size":"contain"},"component":"data2-core-types@Background","break":true}]},"component":"data2-core-types@Fill","break":true},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":120,"height":28,"exactWidth":120,"exactHeight":27.72}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"50","y":3,"exactX":false,"exactY":false,"left":true,"top":false,"bottom":false}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"220","easing":"cubic-bezier(0.013157894736842105,0.0063750000000000195,0.2894736842105263,0.830375)"}}],"shadow":[{"component":"data2-core-types@Shadow","params":{"type":"complex","offsetX":"-1","offsetY":"-1","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(168,157,243,1)"}},"blur":"1"}},{"component":"data2-core-types@Shadow","params":{"type":"complex","offsetX":"0","offsetY":"0","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(168,157,243,1)"}},"blur":"2"}}],"filter":[{"component":"data2-core-types@BitmapFilter","params":{"type":"invert","unit":"%","value":100}}]},"reference":"logoLabel"},{"_id":"6a6664093733859be08fd69f","parent":"6a6664093733859be08fd69a","component":"natura-geomarketing@f2_yWxSf4RxX4sceM8HEg","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":123,"height":40,"exactWidth":122.8,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"componentState":{"component":"data2-core-primitives@String","params":{"content":"simuladores"}}},"index":1},{"_id":"6a6664093733859be08fd69b","parent":"6a6664093733859be08fd69a","component":"natura-geomarketing@f2_yWxSf4RxX4sceM8HEg","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":116,"height":40,"exactWidth":false,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"30","y":0,"exactX":false,"exactY":false,"fixed":false,"left":true}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"componentState":{"component":"data2-core-primitives@String","params":{"content":"cenarios"}}},"index":3},{"_id":"6a6664093733859be08fd69c","parent":"6a6664093733859be08fd69a","component":"natura-geomarketing@f2_yWxSf4RxX4sceM8HEg","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":50,"height":40,"exactWidth":false,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"50","y":0,"exactX":false,"exactY":false,"fixed":false,"left":true}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"componentState":{"component":"data2-core-primitives@String","params":{"content":"admin"}}},"index":5},{"_id":"6a665f2d93f1e5c0b6a198bb","parent":"6a665f2d93f1e5c0b6a198b9","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":16,"height":21,"exactWidth":16.009999999999998,"exactHeight":20.56}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":10,"exactX":false,"exactY":false,"fixed":false,"left":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100% \" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#1B1B2B\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-chevron-down\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>"}}},"index":0},{"_id":"6a665f2d93f1e5c0b6a198ba","parent":"6a665f2d93f1e5c0b6a198b9","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":159,"height":40,"exactWidth":159.09,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":8,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"lucca@data2.app"}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","letterSpacing":""}}},"index":1},{"_id":"6a665fa593f1e5c0b6a198c1","parent":"6a665f2d93f1e5c0b6a198b9","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":22,"height":37,"exactWidth":22,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"5","y":"8","exactX":false,"exactY":false,"fixed":false,"left":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#1B1B2B\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z\" />\n</svg>"}}},"index":2},{"_id":"6a5fcabb3a34c57f2ac6b340","parent":"6a5fcaba3a34c57f2ac6b32d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1250,"height":60,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"width":{"bottom":"1"},"color":"#e7e7e7"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"},"onMouseDown":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a72149640a65f13df647133"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}}},{"_id":"6a5fa72474f960d571569072","parent":"6a5fa72174f960d57156905d","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"body","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1260,"height":60,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"width":{"bottom":"1"},"color":"#cfcfde"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"body"},{"_id":"6a621885ae79ef246a75777f","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorModeSelect","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":172,"height":60,"exactWidth":172.39,"exactHeight":60}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"O4-zOAoAC9BwIp-XZ0ekO"},{"_id":"6a62184aae79ef246a757737","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorModelButtons","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":100,"exactWidth":false,"exactHeight":false}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"ONebofT7KQiRJeb0mlziC"},{"_id":"6a62184fae79ef246a757744","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorStats","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":200,"exactWidth":false,"exactHeight":200}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"gm34nI8iHui1SSuwobYTT"},{"_id":"6a6357109642efac858afb00","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorSectorsInfo","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"componentStates":[{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"default"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"8ar99L_gcmgTQ43nAS9Jy"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"cols"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"8ar99L_gcmgTQ43nAS9Jy"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"rows"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"lNrJPql94exRI91ppcL7l"}}}}]},"reference":"gfyeGJI77tJzdwSZTKCnV"},{"_id":"6a5f9e6fa5529ef885ca9699","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTextItem","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"componentStates":[{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"default"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"N-d1zIHDj3CBxcV-9h7Uq"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"false"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"N-d1zIHDj3CBxcV-9h7Uq"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"true"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"pBTPkX-Oin21ueqzZ2g1X"}}}}]},"reference":"0CZ7pC6NULlDQhV3hv-0G"},{"_id":"6a72149640a65f13df647133","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":268,"height":40,"exactWidth":267.6,"exactHeight":40,"growHeightWithContent":false,"maintainAspectRatio":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":491,"y":10,"exactX":false,"exactY":false,"bottom":false,"right":false,"top":true,"growV":false,"fixed":false,"left":false,"growH":false}},"childrenMode":"rows","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"border":{"component":"data2-core-types@Border","params":{"color":{"params":{"then":{"params":{"content":"#6C5CE0"},"component":"data2-core-primitives@String"},"compare":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a72149640a65f13df647133"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}}},"component":"data2-core-types@Getter"},"else":"#e6e6e6"},"component":"data2-core-flow@If"},"radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}},"break":true},"shadow":[{"component":"data2-core-types@Shadow","params":{"type":"outset","spread":"-15","blur":"25","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(132,132,132,1)"}}}}],"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}},"component":"data2-core-primitives@Boolean"}},{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}},"component":"data2-core-primitives@String"}}],"onMouseDown":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a72149640a65f13df647133"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onClickOutside":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a72149640a65f13df647133"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}},"index":2},{"_id":"6a5fcabb3a34c57f2ac6b341","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":336,"height":20,"exactWidth":336.25,"exactHeight":20}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"50","y":"13","exactX":false,"exactY":false,"left":true,"top":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"top"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Simulador de movimentações otimizadas"}}}},{"_id":"6a5fcabb3a34c57f2ac6b342","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":490,"height":18,"exactWidth":490.15999999999997,"exactHeight":18.439999999999998}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":50,"y":35,"exactX":50,"exactY":34.66,"top":true,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Novo projeto de planejamento"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(97,97,97,1)"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"14"}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":false}}}},{"_id":"6a5fcabb3a34c57f2ac6b343","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":30,"height":31,"exactWidth":30,"exactHeight":31}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"5","y":14,"exactX":false,"exactY":false,"top":true,"left":true}},"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":51}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.75 3.03v.568c0 .334.148.65.405.864l1.068.89c.442.369.535 1.01.216 1.49l-.51.766a2.25 2.25 0 0 1-1.161.886l-.143.048a1.107 1.107 0 0 0-.57 1.664c.369.555.169 1.307-.427 1.605L9 13.125l.423 1.059a.956.956 0 0 1-1.652.928l-.679-.906a1.125 1.125 0 0 0-1.906.172L4.5 15.75l-.612.153M12.75 3.031a9 9 0 0 0-8.862 12.872M12.75 3.031a9 9 0 0 1 6.69 14.036m0 0-.177-.529A2.25 2.25 0 0 0 17.128 15H16.5l-.324-.324a1.453 1.453 0 0 0-2.328.377l-.036.073a1.586 1.586 0 0 1-.982.816l-.99.282c-.55.157-.894.702-.8 1.267l.073.438c.08.474.49.821.97.821.846 0 1.598.542 1.865 1.345l.215.643m5.276-3.67a9.012 9.012 0 0 1-5.276 3.67m0 0a9 9 0 0 1-10.275-4.835M15.75 9c0 .896-.393 1.7-1.016 2.25\" />\n</svg>\n"}}}},{"_id":"6a5fcabb3a34c57f2ac6b344","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":810,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":440,"y":10,"top":true,"exactX":false,"exactY":false,"right":true,"left":true,"growH":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}},"component":"data2-core-primitives@Boolean"}},{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}},"component":"data2-core-primitives@String"}}]}},{"_id":"6a7216c840a65f13df647148","parent":"6a5fcabb3a34c57f2ac6b340","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":27,"height":22,"exactWidth":27.04,"exactHeight":22}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":247,"y":34,"exactX":247.41,"exactY":34.05,"left":true,"top":false}},"childrenMode":"cols","border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"5"}}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(244,245,245,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}}},{"_id":"6a5fa72474f960d571569073","parent":"6a5fa72474f960d571569072","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"title","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":257,"height":17,"exactWidth":256.97999999999996,"exactHeight":16.89}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"50","y":"13","exactX":false,"exactY":false,"left":true,"top":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"top"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Gerenciamento de acessos"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(27,27,43,1)"}}},"index":0,"reference":"title"},{"_id":"6a5fa72474f960d571569074","parent":"6a5fa72474f960d571569072","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"subtitle","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":328,"height":18,"exactWidth":328.06,"exactHeight":18.439999999999998}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"50","y":"30","exactX":false,"exactY":false,"top":true,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"Mostrando "}},{"params":{"components":[{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}},{"component":"data2-core-primitives@String","params":{"content":"0"}}]},"component":"data2-core-logical-operators@Or"},{"component":"data2-core-primitives@String","params":{"content":" de "}},{"params":{"components":[{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}},{"component":"data2-core-primitives@String","params":{"content":"0"}}]},"component":"data2-core-logical-operators@Or"},{"params":{"then":{"component":"data2-core-primitives@String","params":{"content":" acessos"}},"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"tFznon1CqdR98RwcHH1w_"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}},"comparisonSubject":{"params":{"content":1},"component":"data2-core-primitives@Number"}},"component":"data2-core-comparison-operators@Different"},"else":{"component":"data2-core-primitives@String","params":{"content":" acesso"}}},"component":"data2-core-flow@If"}]},"component":"data2-core-functions@StringConcat"},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(107,107,124,1)"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"12"}}},"index":1,"reference":"subtitle"},{"_id":"6a5fa72474f960d571569075","parent":"6a5fa72474f960d571569072","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"icon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":30,"height":31,"exactWidth":30,"exactHeight":31}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"5","y":14,"exactX":false,"exactY":false,"top":true,"left":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#1B1B2B\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z\" />\n</svg>\n"}}},"index":2,"reference":"icon"},{"_id":"6a5fa72474f960d571569079","parent":"6a5fa72474f960d571569072","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"contentOptions","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":380,"height":40,"exactWidth":380,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"440","y":10,"top":true,"exactX":false,"exactY":false}},"border":{"disabled":true,"component":"data2-core-types@Border","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":3,"reference":"contentOptions"},{"_id":"6a5fa72474f960d571569076","parent":"6a5fa72474f960d571569072","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"newButton","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":160,"height":40,"exactWidth":160}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"1080","y":9,"exactX":false,"exactY":false,"top":true,"right":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#f2eced","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"text":""},"index":4,"reference":"newButton"},{"_id":"6a621887ae79ef246a757787","parent":"6a621885ae79ef246a75777f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":172,"height":60,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"top":true,"left":true,"right":false}},"text":"","textSize":{"component":"data2-core-primitives@Number","params":{"content":"16"}},"children":{"locked":true}},"index":0},{"_id":"6a6223b00ff5a8d431e8cec0","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorInfoModeHeader","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":40,"exactWidth":false,"exactHeight":false}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"1S_fB3hvzwUro47iuZ55p"},{"_id":"6a626d0e61501108689e9731","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorAdditionalInfo","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":284,"exactWidth":false,"exactHeight":false}},"fill":{"params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"componentStates":[{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"default"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"DEUVpakmGeSNEBoFMdXnx"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"matriz"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"DEUVpakmGeSNEBoFMdXnx"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"relatorio"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"8W6k6ufjc-HJOpZ9BZjNE"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"console"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"ymJvTq0nprMkyC2uPfWln"}}}}]},"reference":"_3BHHDtVc0ZK0wlfcnLM3"},{"_id":"6a62184aae79ef246a75773e","parent":"6a62184aae79ef246a757737","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":100,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":false,"growH":false}},"childrenMode":"cols","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":2},{"_id":"6a621854ae79ef246a757758","parent":"6a62184fae79ef246a757744","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":200,"exactWidth":false,"exactHeight":200,"growHeightWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":true,"growH":false}},"childrenMode":"rows"},"index":3},{"_id":"6a6357379642efac858afb02","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorSectorsInfoRows","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550}}},"reference":"lNrJPql94exRI91ppcL7l"},{"_id":"6a63572a9642efac858afb01","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorSectorsInfoCols","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"8ar99L_gcmgTQ43nAS9Jy"},{"_id":"6a666284607e9bc66b1bae7a","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTopBarShortcut","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":284,"height":40,"exactWidth":284.24,"exactHeight":false,"growWidthWithContent":true}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"componentStates":[{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"simuladores"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"c2WTvq5i85sOrTkEXFRti"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"admin"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"w5AERCyEk8sidBQr-3na4"}}}},{"component":"data2-core-types@ComponentState","params":{"state":{"component":"data2-core-primitives@String","params":{"content":"cenarios"}},"artboard":{"disabled":false,"component":"data2-core-types@ArtboardReference","params":{"reference":"12HGth08_9P_9lz97RYRE"}}}}]},"reference":"f2_yWxSf4RxX4sceM8HEg"},{"_id":"6a5f9e6da5529ef885ca9697","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTextItem1","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}}},"reference":"pBTPkX-Oin21ueqzZ2g1X"},{"_id":"6a5f9e50a5529ef885ca9694","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTextItem0","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}}},"reference":"N-d1zIHDj3CBxcV-9h7Uq"},{"_id":"6a72149640a65f13df647134","parent":"6a72149640a65f13df647133","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":222,"height":36,"exactWidth":221.64999999999998,"exactHeight":36}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":8,"y":1,"exactX":7.950000000000001,"exactY":false,"top":true,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Fechamento do setor 3786"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}}},{"_id":"6a72149640a65f13df647135","parent":"6a72149640a65f13df647133","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":21,"height":20,"exactWidth":21,"exactHeight":20}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"234","y":"9","exactX":false,"exactY":false,"right":false,"top":true,"left":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100%\" height=\"100%\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#1a1a1a\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-chevron-down\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>"}}}},{"_id":"6a72149640a65f13df647136","parent":"6a72149640a65f13df647133","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":132,"height":23,"exactWidth":131.78,"exactHeight":22.8}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":288,"y":15,"exactX":287.74,"exactY":14.75,"top":true,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"left","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"2 / 5"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(43,43,43,1)"}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"20"}},"opacity":{"params":{},"component":"data2-core-types@Opacity"}}},{"_id":"6a72182840a65f13df64714c","parent":"6a72149640a65f13df647133","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":208,"height":21,"exactWidth":207.78,"exactHeight":20.53}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":288,"y":-7,"exactX":288.05,"exactY":-6.98,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"left","textOverflow":false,"verticalAlignment":"middle","fontWeight":400,"letterSpacing":1}},"text":{"component":"data2-core-primitives@String","params":{"content":"Movimentações"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(105,105,105,1)"}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"14"}}}},{"_id":"6a722c9448537d17e1a1d3ca","parent":"6a72149640a65f13df647133","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":266,"height":278,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":"40","exactX":false,"exactY":false,"bottom":false,"right":false,"top":true,"growV":false,"fixed":false,"left":false,"growH":false}},"border":{"component":"data2-core-types@Border","params":{"color":"#e6e6e6","radius":{"leftTop":"3"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(241,241,248,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"style":{"transform":{"params":{"compare":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a72149640a65f13df647133"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}}},"component":"data2-core-types@Getter"},"else":{"component":"data2-core-primitives@String","params":{"content":"scale(1,0)"}},"then":{"component":"data2-core-primitives@String","params":{"content":"scale(1,1)"}}},"component":"data2-core-flow@If"},"transformOrigin":{"component":"data2-core-primitives@String","params":{"content":"top center"}},"padding":{"component":"data2-core-primitives@String","params":{"content":"10px 0px 10px 0px"}}},"scroll":{"disabled":false,"component":"data2-core-types@Scroll","params":{"y":true}},"repeat":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@DataSetReference","params":{"reference":"FhlWyAglkQB5EL4hrbzmq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"module":{"params":{"pattern":{"params":{"flags":{"component":"data2-core-primitives@String","params":{"content":"i"}},"pattern":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"("}},{"params":{"array":{"params":{"array":{"params":{"haystack":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}},"component":"data2-core-functions@Split"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"elm"}}],"body":{"params":{"compare":[{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}},"subject":{"component":"data2-core-primitives@String","params":{"content":"$^[]().,?{}|\\/:=*-+"}}},"component":"data2-core-functions@Includes"}],"else":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}},"then":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"\\"}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-functions@ArrayJoin"},{"component":"data2-core-primitives@String","params":{"content":")"}}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-types@Regex","pattern":"[A-Z]","flags":"gm","break":true},"excludesInput":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.setor"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@Match"},"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},"childrenMode":"grid","repeatReference":{"component":"data2-core-primitives@String","params":{"content":"s"}}}},{"_id":"6a5fcabb3a34c57f2ac6b345","parent":"6a5fcabb3a34c57f2ac6b344","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":40,"exactWidth":359.59999999999997}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":215,"y":0,"exactX":false,"exactY":false,"top":true,"right":true}},"border":{"component":"data2-core-types@Border","params":{"color":{"params":{"then":{"params":{"content":"#6C5CE0"},"component":"data2-core-primitives@String"},"compare":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}}},"component":"data2-core-types@Getter"},"else":"#e6e6e6"},"component":"data2-core-flow@If"},"radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}},"break":true},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a5fcabb3a34c57f2ac6b34a","parent":"6a5fcabb3a34c57f2ac6b344","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":293,"height":40,"exactWidth":292.74,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":517,"y":0,"exactX":false,"exactY":false,"top":true,"right":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#f2eced","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"then":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(64,53,157,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"else":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"compare":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b34a"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}},"component":"data2-core-types@Getter"}},"component":"data2-core-flow@If"},"children":{"locked":true},"onClick":{"params":{"actions":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data"}},"value":{}},"component":"data2-core-types@Setter"},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}},"value":{"component":"data2-core-primitives@String","params":{"content":"loading"}}}},{"params":{"setor":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}},"component":"natura-geomarketing@Y-BartZI5BwXJmLbhFG_3"},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}},"value":{"component":"data2-core-primitives@String","params":{"content":"done"}}}}]},"component":"data2-core-flow@Series"},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b34a"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b34a"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":false}}}},{"_id":"6a5fcc983a34c57f2ac6b358","parent":"6a5fcabb3a34c57f2ac6b344","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":40,"height":40,"exactWidth":40,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":-60,"y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a699b27e1053d806729ea00","parent":"6a5fcabb3a34c57f2ac6b344","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":268,"height":278,"exactWidth":268,"exactHeight":277.86}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":271,"y":10,"exactX":false,"exactY":false,"bottom":false,"right":false,"top":true,"growV":false,"fixed":false,"left":false,"growH":false}},"border":{"component":"data2-core-types@Border","params":{"color":"#e6e6e6","radius":{"leftTop":"3"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(241,241,248,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"style":{"transform":{"params":{"compare":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}}},"component":"data2-core-types@Getter"},"else":{"component":"data2-core-primitives@String","params":{"content":"scale(1,0)"}},"then":{"component":"data2-core-primitives@String","params":{"content":"scale(1,1)"}}},"component":"data2-core-flow@If"},"transformOrigin":{"component":"data2-core-primitives@String","params":{"content":"top center"}},"padding":{"component":"data2-core-primitives@String","params":{"content":"10px 0px 10px 0px"}}},"scroll":{"disabled":false,"component":"data2-core-types@Scroll","params":{"y":true}},"repeat":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@DataSetReference","params":{"reference":"FhlWyAglkQB5EL4hrbzmq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"module":{"params":{"pattern":{"params":{"flags":{"component":"data2-core-primitives@String","params":{"content":"i"}},"pattern":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"("}},{"params":{"array":{"params":{"array":{"params":{"haystack":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}},"component":"data2-core-functions@Split"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"elm"}}],"body":{"params":{"compare":[{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}},"subject":{"component":"data2-core-primitives@String","params":{"content":"$^[]().,?{}|\\/:=*-+"}}},"component":"data2-core-functions@Includes"}],"else":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}},"then":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"\\"}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"elm"}}}}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-functions@ArrayJoin"},{"component":"data2-core-primitives@String","params":{"content":")"}}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-types@Regex","pattern":"[A-Z]","flags":"gm","break":true},"excludesInput":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.setor"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@Match"},"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},"childrenMode":"grid","repeatReference":{"component":"data2-core-primitives@String","params":{"content":"s"}}}},{"_id":"6a7216c840a65f13df64714a","parent":"6a7216c840a65f13df647148","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":19,"height":22,"exactWidth":19.47,"exactHeight":21.58}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":5,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":false,"growH":false,"bottom":true,"growV":false}},"text":{"component":"data2-core-primitives@String","params":{"content":"v1"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"16"}},"textFormat":{"params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left","fontStyle":"normal","fontWeight":400},"component":"data2-core-types@TextFormat"},"textColor":{"params":{"rgba":"rgba(134,134,134,1)"},"component":"data2-core-types@Color"},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}}},{"_id":"6a5fa72474f960d57156907a","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"searchContainer","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":40,"exactWidth":359.59999999999997}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":0,"exactY":0,"top":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#e5e5ef","radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"text":""},"reference":"searchContainer"},{"_id":"6a5fa72474f960d57156907d","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"downloadButton","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":40,"height":40,"exactWidth":40,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":-60,"y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true}},"reference":"downloadButton"},{"_id":"6a5fa72474f960d57156907f","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"sortButton","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":40,"height":40,"exactWidth":40,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"380","y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true}},"reference":"sortButton"},{"_id":"6a5fa72474f960d571569081","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"prevPageButton","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":40,"height":40,"exactWidth":40,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"540","y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true}},"reference":"prevPageButton"},{"_id":"6a5fa72474f960d571569083","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"nextPageButton","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":40,"height":40,"exactWidth":40,"exactHeight":40}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"580","y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true}},"reference":"nextPageButton"},{"_id":"6a5fa72474f960d571569085","parent":"6a5fa72474f960d571569079","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"pageIndicator","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":100,"height":41,"exactWidth":99.61,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"440","y":0,"exactX":false,"exactY":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"left","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Página 1 / 1"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(107,107,124,1)"}}},"reference":"pageIndicator"},{"_id":"6a5fa72474f960d571569077","parent":"6a5fa72474f960d571569076","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"newIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":10,"y":"7","exactX":false,"exactY":false}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#fff\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4.5v15m7.5-7.5h-15\" />\n</svg>\n"}}},"reference":"newIcon"},{"_id":"6a5fa72474f960d571569078","parent":"6a5fa72474f960d571569076","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","displayName":"newLabel","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":135,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":25,"y":0,"exactX":false,"exactY":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Criar Acesso"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}}},"reference":"newLabel"},{"_id":"6a621887ae79ef246a757788","parent":"6a621887ae79ef246a757787","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":172,"height":30,"exactHeight":false,"exactWidth":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":30,"exactX":false,"exactY":false}},"text":""}},{"_id":"6a621887ae79ef246a75778b","parent":"6a621887ae79ef246a757787","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":172,"height":30,"exactHeight":false,"exactWidth":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false}}}},{"_id":"6a6223b10ff5a8d431e8cec6","parent":"6a6223b00ff5a8d431e8cec0","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":true,"growH":false}},"childrenMode":"cols","textFormat":{"disabled":true,"component":"data2-core-types@TextFormat","params":{}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"9"}}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(231,232,232,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}},"index":2},{"_id":"6a626d5561501108689e9733","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"console sql","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":293.02000000000004}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"ymJvTq0nprMkyC2uPfWln"},{"_id":"6a626ccf61501108689e9723","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"Relatório de impacto (6 ciclos | antes/depois)","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":526,"exactWidth":false,"exactHeight":525.98}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"8W6k6ufjc-HJOpZ9BZjNE"},{"_id":"6a626ca0bc0ed2896304f0b2","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"MATRIZ ASIS","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":284,"exactWidth":false,"exactHeight":false}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"DEUVpakmGeSNEBoFMdXnx"},{"_id":"6a62184aae79ef246a75773f","parent":"6a62184aae79ef246a75773e","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":40,"exactWidth":359.59999999999997}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":60,"exactX":false,"exactY":false,"top":true,"fixed":false,"left":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#e6e6e6","radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"text":"","shadow":[{"component":"data2-core-types@Shadow","params":{"type":"outset","offsetY":"2","blur":"2","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(202,202,202,1)"}},"spread":"0"}}],"cursor":"pointer"}},{"_id":"6a62184aae79ef246a757742","parent":"6a62184aae79ef246a75773e","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":196,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":60,"exactX":false,"exactY":false,"top":true,"fixed":false,"left":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#e6e6e6","radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"text":"","shadow":[{"component":"data2-core-types@Shadow","params":{"type":"outset","offsetY":"2","blur":"2","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(202,202,202,1)"}},"spread":"0"}}],"cursor":"pointer"}},{"_id":"6a6246ba203e921fd2513b01","parent":"6a621854ae79ef246a757758","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":90,"exactWidth":false,"exactHeight":90}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false}},"childrenMode":"cols"},"index":0},{"_id":"6a621854ae79ef246a757762","parent":"6a621854ae79ef246a757758","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":90,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":true,"right":true,"growH":false}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":1},{"_id":"6a6357949642efac858afb12","parent":"6a6357379642efac858afb02","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"childrenMode":"rows"}},{"_id":"6a6357469642efac858afb06","parent":"6a63572a9642efac858afb01","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":550,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false}},"text":"","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a67a32519eeef7ac1c378d0","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTopBarShortcutCenarios","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":284,"height":40,"exactWidth":284.24,"exactHeight":false,"growWidthWithContent":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"12HGth08_9P_9lz97RYRE"},{"_id":"6a6793087c969e738b8c3057","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTopBarShortcutAdmin","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":284,"height":40,"exactWidth":284.24,"exactHeight":false,"growWidthWithContent":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"w5AERCyEk8sidBQr-3na4"},{"_id":"6a6792457c969e738b8c3044","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubTopBarShortcutSimuladores","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":284,"height":40,"exactWidth":284.24,"exactHeight":false,"growWidthWithContent":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"c2WTvq5i85sOrTkEXFRti"},{"_id":"6a5f9e6da5529ef885ca9698","parent":"6a5f9e6da5529ef885ca9697","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":0,"exactY":0,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"label"}}},"component":"data2-core-types@Reference"},"style":{"padding":{"component":"data2-core-primitives@String","params":{"content":"20px"}}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(0,0,0,0.1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"onClick":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onClick"}}},"component":"data2-core-types@Reference"},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"220","easing":"cubic-bezier(0,-0.0016249999999999876,0.3684210526315789,0.830375)"}}],"cursor":"pointer"},"reference":"label"},{"_id":"6a5f9e51a5529ef885ca9696","parent":"6a5f9e50a5529ef885ca9694","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":180,"height":40,"exactWidth":179.8,"exactHeight":40,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"label"}}},"component":"data2-core-types@Reference"},"style":{"padding":{"component":"data2-core-primitives@String","params":{"content":"20px"}}},"onClick":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onClick"}}},"component":"data2-core-types@Reference"},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"220","easing":"cubic-bezier(0,-0.0016249999999999876,0.3684210526315789,0.830375)"}}],"cursor":"pointer"},"reference":"label"},{"_id":"6a722c9448537d17e1a1d3cb","parent":"6a722c9448537d17e1a1d3ca","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":358,"height":22,"exactWidth":false,"exactHeight":22}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":false,"right":false,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater.setor"}}},"component":"data2-core-types@Reference"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":20}},"onMouseDown":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}},"value":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater.setor"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-types@Setter"},"cursor":"pointer"},"index":0},{"_id":"6a5fcabb3a34c57f2ac6b346","parent":"6a5fcabb3a34c57f2ac6b345","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":22,"height":20,"exactWidth":21.84,"exactHeight":20.27}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":321,"y":9,"exactX":false,"exactY":false}},"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":43}},"style":{"transform":{"params":{"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}}}},"then":{"component":"data2-core-primitives@String","params":{"content":"rotate(180deg)"}}},"component":"data2-core-flow@If"}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m19.5 8.25-7.5 7.5-7.5-7.5\" />\n</svg>\n"}}},"index":0},{"_id":"6a5fcabb3a34c57f2ac6b348","parent":"6a5fcabb3a34c57f2ac6b345","component":"data2-blocks@IconBlock","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":37,"height":38,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"8","y":0,"exactX":false,"exactY":false,"top":true}},"children":{"locked":true},"onClick":{"params":{"actions":[{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}},"value":{"component":"data2-core-primitives@String","params":{"content":"loading"}}}},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}},"value":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}}}}}},{"params":{"actions":[{"params":{"actions":[{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"prop":"filter","args":[{"params":{"fields":{"CD_SETOR":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"resultName":{"component":"data2-core-primitives@String","params":{"content":"municipios"}},"action":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"MUNICIPIO":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"UF":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.UF"}}},"component":"data2-core-types@Reference"}}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Step"},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}},"value":{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipios"}}},"component":"data2-core-types@Reference"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}}}},"component":"data2-core-functions@ArrayMap"}},"displayName":"get municipios"},{"params":{"actions":{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipios"}}},"component":"data2-core-types@Reference"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"actions":[{"params":{"municipio":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"uf":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.UF"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@4vQ1HUOSYMGvZGCOV2GYd"},{"params":{"message":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m"}}},"component":"data2-core-types@Reference"},"data":{"data":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}}},"component":"natura-geomarketing@nGePVjqDWdX2KQL8ahh-w"},{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"subject":{"params":{"pattern":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f"}}},"component":"data2-core-types@Reference"},"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}}}},"component":"data2-core-functions@Includes"}},"component":"data2-core-logical-operators@Not"}}}},"component":"data2-core-functions@ArrayFilter"},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}},"value":{"params":{"array":{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"},"arrays":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-functions@ArrayConcat"}}}]},"component":"data2-core-flow@Series"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Parallel","displayName":"neighbor_municipios"},{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"will filter thingy"}},"data":{"MUNICIPIO":{"in":{"params":{"arrays":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"}]},"component":"data2-core-functions@ArrayConcat"}}}},"component":"natura-geomarketing@nGePVjqDWdX2KQL8ahh-w"},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":"filter","args":[{"params":{"fields":{"MUNICIPIO":{"in":{"params":{"arrays":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"}]},"component":"data2-core-functions@ArrayConcat"}}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"sectors of municipios"}},"data":{"data":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}}}}},"component":"natura-geomarketing@nGePVjqDWdX2KQL8ahh-w"},{"params":{"actions":{"params":{"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"actions":[{"params":{"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@gA5O7o100HVWfKwEEPNyM"},{"component":"data2-core-types@Setter","params":{"value":{"params":{"sources":[{"component":"data2-core-types@FieldSet","params":{"fields":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.assignments"}}},"component":"data2-core-types@Getter"}}},{"component":"data2-core-types@FieldSet","params":{"fieldsList":[{"value":{"params":{"array":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFilter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m2"}}],"body":{"params":{"else":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m2.CD_SETOR"}}},"component":"data2-core-types@Reference"},"then":{"component":"data2-core-primitives@String","params":{"content":""}},"compare":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m2.CD_SETOR"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayMap"},"key":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}}]}}],"object":{}},"component":"data2-core-functions@ObjectAssign"},"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.assignments"}}}}]},"component":"data2-core-flow@Series"}}},"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Series"},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":"filter","args":[{"params":{"fields":{"MUNICIPIO":{"in":{"params":{"arrays":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.municipios"}}},"component":"data2-core-types@Getter"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.neighboring_municipios"}}},"component":"data2-core-types@Getter"}]},"component":"data2-core-functions@ArrayConcat"}}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.groups"}},"value":{"params":{"array":{"params":{"array":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"field":{"component":"data2-core-primitives@String","params":{"content":"CD_SETOR"}}},"component":"data2-core-functions@Uniq"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"subject":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.CD_SETOR"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-logical-operators@Not"}}}},"component":"data2-core-functions@ArrayFilter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.CD_SETOR"}}}}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@Setter"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.colors"}},"value":{"params":{"array":{"params":{"array":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"UI2RIyzH2ZnVnYlZtETzi"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"field":{"component":"data2-core-primitives@String","params":{"content":"CD_SETOR"}}},"component":"data2-core-functions@Uniq"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"subject":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.CD_SETOR"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-logical-operators@Not"}}}},"component":"data2-core-functions@ArrayFilter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}},{"component":"data2-core-primitives@String","params":{"content":"i"}},{"component":"data2-core-primitives@String","params":{"content":"l"}}],"body":{"params":{"fieldsList":[{"key":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.CD_SETOR"}}}},"value":{"params":{"color":{"params":{"color":{"h":{"params":{"components":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"i"}}},"component":"data2-core-types@Reference"},{"params":{"components":[{"params":{"components":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"l.length"}}},"component":"data2-core-types@Reference"},{"component":"data2-core-primitives@Number","params":{"content":10}}]},"component":"data2-core-numerical-operators@Multiply"},{"component":"data2-core-primitives@Number","params":{"content":36}}]},"component":"data2-core-numerical-operators@Divide"}]},"component":"data2-core-numerical-operators@Multiply"},"s":{"component":"data2-core-primitives@String","params":{"content":"1"}},"l":{"component":"data2-core-primitives@String","params":{"content":"1"}},"alpha":{"component":"data2-core-primitives@String","params":{"content":"1"}}}},"component":"data2-core-color@ColorFormatHsl"}},"component":"data2-core-color@ColorFormatHex"}}]},"component":"data2-core-types@FieldSet"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@Setter"},{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"ASIS"}},"data":{"data":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data"}}}}}},"component":"natura-geomarketing@nGePVjqDWdX2KQL8ahh-w"}]},"component":"data2-core-flow@Series"},{"params":{"actions":[{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"prop":"filter","args":[{"params":{"fields":{"CD_SETOR":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}}},"component":"data2-core-types@FieldSet"}]}},{"params":{"actions":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"actions":[{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.ufs"}},"value":{"params":{"item":{"component":"data2-core-types@Getter","params":{"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}},"module":{"params":{"needle":{"component":"data2-core-primitives@String","params":{"content":"-"}},"haystack":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.UF_MUNICIPIO"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@Split"}}},"array":{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.ufs"}}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"}},"component":"data2-core-functions@ArrayPush"}}},{"params":{"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.MUNICIPIO"}}},"component":"data2-core-types@Reference"}},"component":"natura-geomarketing@gA5O7o100HVWfKwEEPNyM"},{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"data.assignments."}},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-functions@StringConcat"},"value":{"params":{"array":{"params":{"components":[{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"data.assignments."}},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-types@Getter"},[]]},"component":"data2-core-logical-operators@Or"},"item":{"component":"data2-core-primitives@String","params":{"content":""}}},"component":"data2-core-functions@ArrayPush"}},"component":"data2-core-types@Setter"}]},"component":"data2-core-flow@Series"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-flow@Series"},{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"ASIS"}},"data":{"data":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data"}}}}}},"component":"natura-geomarketing@nGePVjqDWdX2KQL8ahh-w"}]},"component":"data2-core-flow@Series","disabled":true}]},"component":"data2-core-flow@Series"},{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.state"}},"value":{"component":"data2-core-primitives@String","params":{"content":"done"}}}}]},"component":"data2-core-flow@Series"},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"icon":{"disabled":false,"component":"data2-core-web@Icon","params":{"iconSet":"fontawesome6","icon":"fa6-solid fa6-dice"}},"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(159,159,159,1)"}},"size":{"component":"data2-core-primitives@Number","params":{"content":"22"}},"text":""},"index":1},{"_id":"6a5fcabb3a34c57f2ac6b347","parent":"6a5fcabb3a34c57f2ac6b345","component":"data2-blocks@DataFieldBlock","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":336,"height":38,"exactWidth":336.04,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false,"bottom":true,"growV":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}}},"placeholder":{"component":"data2-core-primitives@String","params":{"content":"Digite aqui um código de setor"}},"dataset":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"field":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}},"type":"text","mask":{"component":"data2-core-primitives@String","params":{"content":"0000"}},"onFocus":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onBlur":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"focused"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}},"index":2},{"_id":"6a5fcabb3a34c57f2ac6b34b","parent":"6a5fcabb3a34c57f2ac6b34a","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":25,"height":27,"exactWidth":24.72,"exactHeight":27.1}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"18","y":"8","exactX":false,"exactY":false}},"filter":[{"component":"data2-core-types@BitmapFilter","params":{"type":"invert","unit":"%","value":100}}]},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100%\" height=\"100%\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-user-plus\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"></path><circle cx=\"8.5\" cy=\"7\" r=\"4\"></circle><line x1=\"20\" y1=\"8\" x2=\"20\" y2=\"14\"></line><line x1=\"23\" y1=\"11\" x2=\"17\" y2=\"11\"></line></svg>"}}}},{"_id":"6a5fcabb3a34c57f2ac6b34c","parent":"6a5fcabb3a34c57f2ac6b34a","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":279,"height":36,"exactWidth":278.74,"exactHeight":36}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":57,"y":2,"exactX":57.26,"exactY":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"left","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Fechar versão e compartilhar"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":false}}}},{"_id":"6a5fcc983a34c57f2ac6b359","parent":"6a5fcc983a34c57f2ac6b358","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":6,"y":6,"exactX":false,"exactY":false}},"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":43}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z\" />\n</svg>\n"}}}},{"_id":"6a699b27e1053d806729ea02","parent":"6a699b27e1053d806729ea00","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":358,"height":22,"exactWidth":false,"exactHeight":22}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":true,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater.setor"}}},"component":"data2-core-types@Reference"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":20}},"onMouseDown":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a5fcabb3a34c57f2ac6b344"}},"key":{"component":"data2-core-primitives@String","params":{"content":"sector"}}}},"value":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater.setor"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-types@Setter"},"cursor":"pointer"},"index":0},{"_id":"6a5fa72474f960d57156907b","parent":"6a5fa72474f960d57156907a","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"searchIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":24,"height":24,"exactWidth":24.32,"exactHeight":24.02}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":8,"y":7,"exactX":false,"exactY":false,"left":true}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#6B6B7C\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n</svg>\n"}}},"reference":"searchIcon"},{"_id":"6a5fa72474f960d57156907c","parent":"6a5fa72474f960d57156907a","component":"data2-blocks@DataFieldBlock","system":"6a57cfe9673eb7e3c5e57784","displayName":"searchInput","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":298,"height":38,"exactWidth":298,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"41","y":0,"exactX":false,"exactY":false,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}}},"placeholder":{"component":"data2-core-primitives@String","params":{"content":"Buscar..."}},"padding":{"left":{"component":"data2-core-primitives@Number","params":{"content":20}},"right":{"component":"data2-core-primitives@Number","params":{"content":40}}}},"reference":"searchInput"},{"_id":"6a5fa72474f960d57156907e","parent":"6a5fa72474f960d57156907d","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"downloadIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":6,"y":6,"exactX":false,"exactY":false}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#6B6B7C\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3\" />\n</svg>\n"}}},"reference":"downloadIcon"},{"_id":"6a5fa72474f960d571569080","parent":"6a5fa72474f960d57156907f","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","displayName":"sortIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":6,"y":6,"exactX":false,"exactY":false}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"#6B6B7C\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9\" />\n</svg>\n"}}},"reference":"sortIcon"},{"_id":"6a5fa72474f960d571569082","parent":"6a5fa72474f960d571569081","component":"data2-blocks@IconBlock","system":"6a57cfe9673eb7e3c5e57784","displayName":"prevPageIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":6,"y":6,"exactX":false,"exactY":false}}},"icon":{"disabled":false,"component":"data2-core-web@Icon","params":{"iconSet":"remix","icon":"ri-arrow-left-wide-fill"}},"size":{"component":"data2-core-primitives@Number","params":{"content":"28"}},"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(107,107,124,1)"}}},"reference":"prevPageIcon"},{"_id":"6a5fa72474f960d571569084","parent":"6a5fa72474f960d571569083","component":"data2-blocks@IconBlock","system":"6a57cfe9673eb7e3c5e57784","displayName":"nextPageIcon","params":{"super":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":28,"height":29,"exactWidth":28,"exactHeight":29}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":6,"y":6,"exactX":false,"exactY":false}}},"icon":{"disabled":false,"component":"data2-core-web@Icon","params":{"iconSet":"remix","icon":"ri-arrow-right-wide-fill"}},"size":{"component":"data2-core-primitives@Number","params":{"content":"28"}},"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(107,107,124,1)"}}},"reference":"nextPageIcon"},{"_id":"6a621887ae79ef246a757789","parent":"6a621887ae79ef246a757788","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"9"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":145,"height":30,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":27,"y":0,"exactX":false,"exactY":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"ASIS (hoje)"}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}}}},{"_id":"6a621887ae79ef246a75778a","parent":"6a621887ae79ef246a757788","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#eeeeee","radius":{"leftTop":"999"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":16,"height":16,"exactWidth":16,"exactHeight":16}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":7,"exactX":false,"exactY":false}},"text":""}},{"_id":"6a621887ae79ef246a75778c","parent":"6a621887ae79ef246a75778b","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":145,"height":30,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":27,"y":0,"exactX":false,"exactY":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"TOBE (simulação)"}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}}}},{"_id":"6a621887ae79ef246a75778d","parent":"6a621887ae79ef246a75778b","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#007aff","radius":{"leftTop":"999"},"width":{"left":"4","top":"4","right":"4","bottom":"4"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(250,250,250,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":16,"height":16,"exactWidth":16,"exactHeight":16}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":7,"exactX":false,"exactY":false}},"text":""}},{"_id":"6a6223b10ff5a8d431e8cec7","parent":"6a6223b10ff5a8d431e8cec6","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"7"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":136,"height":36,"exactWidth":136,"exactHeight":36}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"2","y":2,"exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Matriz ASIS"}},"fill":{"params":{"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}}},"component":"data2-core-comparison-operators@Equal"},{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"matriz"}}},"component":"data2-core-comparison-operators@Equal"}]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"else":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,0)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(0,0,0,0.1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec7"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@If"},"cursor":"pointer","onMouseDown":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}},"value":{"component":"data2-core-primitives@String","params":{"content":"matriz"}}}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260","easing":"cubic-bezier(0.09287141498766448,0.500681640625,0.4941872044613487,0.900681640625)"}}],"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec7"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec7"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}}},{"_id":"6a6223b10ff5a8d431e8cec8","parent":"6a6223b10ff5a8d431e8cec6","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"7"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":357,"height":36,"exactWidth":357,"exactHeight":36}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":3,"y":2,"exactX":3,"exactY":2,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Relatório de impacto (6 ciclos | antes/depois)"}},"fill":{"params":{"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"relatorio"}}},"component":"data2-core-comparison-operators@Equal"},"else":{"params":{"else":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,0)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(0,0,0,0.1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec8"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@If"},"cursor":"pointer","onMouseDown":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}},"value":{"component":"data2-core-primitives@String","params":{"content":"relatorio"}}}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260","easing":"cubic-bezier(0.09287141498766448,0.500681640625,0.4941872044613487,0.900681640625)"}}],"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec8"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec8"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}}},{"_id":"6a6223b10ff5a8d431e8cec9","parent":"6a6223b10ff5a8d431e8cec6","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"7"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":137,"height":36,"exactWidth":137,"exactHeight":36}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":3,"y":"2","exactX":false,"exactY":false,"fixed":false,"left":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Console SQL"}},"fill":{"params":{"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"console"}}},"component":"data2-core-comparison-operators@Equal"},"else":{"params":{"else":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,0)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"then":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(0,0,0,0.1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec9"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@If"},"onClick":{"disabled":true,"component":"data2-core-types@Interaction","params":{}},"onMouseDown":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}},"value":{"component":"data2-core-primitives@String","params":{"content":"console"}}}},"cursor":"pointer","transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260","easing":"cubic-bezier(0.09287141498766448,0.500681640625,0.4941872044613487,0.900681640625)"}}],"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec9"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"},"onMouseLeave":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6223b10ff5a8d431e8cec9"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-types@Setter"}}},{"_id":"6a6223b10ff5a8d431e8ceca","parent":"6a6223b10ff5a8d431e8cec6","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(139,139,139,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1,"height":25,"exactWidth":1}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"499","y":8,"exactX":false,"exactY":false}},"hide":{"params":{"subject":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}}},"component":"data2-core-comparison-operators@Equal"},{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"matriz"}}},"component":"data2-core-comparison-operators@Equal"}]},"component":"data2-core-logical-operators@Or"}},"component":"data2-core-logical-operators@Not"}}},{"_id":"6a626dd861501108689e9736","parent":"6a6223b10ff5a8d431e8cec6","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(139,139,139,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1,"height":25,"exactWidth":1}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"139","y":8,"exactX":false,"exactY":false}},"hide":{"params":{"subject":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"kfqDiL83O6CTlKOfellwq"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.additionalInfo"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"console"}}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-logical-operators@Not"}}},{"_id":"6a6a086d097f6f7e71d00ff3","parent":"6a626d5561501108689e9733","component":"data2-blocks@JsonViewer","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true,"minHeight":1240}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"bottom":false,"left":true,"top":true,"growV":false,"right":true,"growH":false}},"scroll":{"disabled":false,"component":"data2-core-types@Scroll","params":{"y":true}},"fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(20,20,20,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}},"json":{"map":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ziIl3zJpxF2x8qsRQy10a"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data"}}}},"ds":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"umJZo_TTyw0E3lOwBNmk2"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}}},"query":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"umJZo_TTyw0E3lOwBNmk2"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"filterSettings"}}}}}}},{"_id":"6a626ce961501108689e9729","parent":"6a626ccf61501108689e9723","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":320,"height":30,"exactWidth":320.02,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":"5","exactX":false,"exactY":false,"fixed":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"Impacto antes -> depois por setor"}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(26,26,26,1)"}}},"index":1},{"_id":"6a626ce961501108689e972e","parent":"6a626ccf61501108689e9723","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":140,"height":40,"exactWidth":140}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":500,"y":0,"exactX":false,"exactY":false,"top":true}},"border":{"component":"data2-core-types@Border","params":{"color":"#e6e6e6","radius":{"leftTop":"9"},"width":{"left":"1","top":"1","right":"1","bottom":"1"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"}},"index":2},{"_id":"6a62700d61501108689e9737","parent":"6a626ccf61501108689e9723","component":"natura-geomarketing@XaKyb6x3qMpZupICoqsnY","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":468,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":58,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false,"fixed":false}}}}},{"_id":"6a626cb961501108689e9722","parent":"6a626ca0bc0ed2896304f0b2","component":"natura-geomarketing@L--dVhugwB17-_W00pA7S","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":284,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"bottom":false,"left":true,"top":true,"growV":false,"right":true,"growH":false}}}},"index":0},{"_id":"6a62184aae79ef246a757740","parent":"6a62184aae79ef246a75773f","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":24,"height":24,"exactWidth":24.32,"exactHeight":24.02}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":7,"exactX":false,"exactY":false,"left":true}},"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":43}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-star\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"></polygon></svg>"}}}},{"_id":"6a62184aae79ef246a757741","parent":"6a62184aae79ef246a75773f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":326,"height":38,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":32,"y":0,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Modelo ÓTIMO (equilibrio + quebra)"}}}},{"_id":"6a62184aae79ef246a757743","parent":"6a62184aae79ef246a757742","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":194,"height":38,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false,"bottom":true,"growV":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Por adjacência"}}}},{"_id":"6a621854ae79ef246a757759","parent":"6a6246ba203e921fd2513b01","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":160,"height":90,"exactWidth":160,"exactHeight":90}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":true}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}}}},{"_id":"6a621854ae79ef246a75775c","parent":"6a6246ba203e921fd2513b01","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":160,"height":90,"exactWidth":160,"exactHeight":90}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":0,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":true}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a621854ae79ef246a75775f","parent":"6a6246ba203e921fd2513b01","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":160,"height":90,"exactWidth":160,"exactHeight":90}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":0,"exactX":false,"exactY":false,"fixed":false,"top":true,"left":true}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}},"hide":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a621854ae79ef246a757763","parent":"6a621854ae79ef246a757762","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":70,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":19,"y":21,"exactX":19.02,"exactY":20.99,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"3287, 3360, 3258, 3290, 3445"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":22}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"top"}}}},{"_id":"6a621854ae79ef246a757764","parent":"6a621854ae79ef246a757762","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":528,"height":32,"exactWidth":528.0600000000001,"exactHeight":32}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":22,"y":45,"exactX":false,"exactY":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"Setor sem líder herdada"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"bottom"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a6357949642efac858afb14","parent":"6a6357949642efac858afb12","component":"natura-geomarketing@iW3NufyoQGjGAwhmVZ3ow","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":250,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true,"growWidthWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"right":true,"top":true,"left":true,"growH":false}},"text":""}},"index":0},{"_id":"6a6357949642efac858afb13","parent":"6a6357949642efac858afb12","component":"natura-geomarketing@PCOvC0KQdskFojBsWZBfd","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":280,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true,"growWidthWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":20,"exactX":false,"exactY":false,"fixed":false,"right":true,"top":true,"left":true,"growH":false}},"text":""}},"index":1},{"_id":"6a6357469642efac858afb08","parent":"6a6357469642efac858afb06","component":"natura-geomarketing@iW3NufyoQGjGAwhmVZ3ow","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":280,"height":550,"exactWidth":280,"exactHeight":false,"growHeightWithContent":false,"growWidthWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"right":false,"top":true,"left":true,"growH":true}},"text":""}},"index":0},{"_id":"6a6357469642efac858afb07","parent":"6a6357469642efac858afb06","component":"natura-geomarketing@PCOvC0KQdskFojBsWZBfd","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":252,"height":550,"exactWidth":false,"exactHeight":false,"growHeightWithContent":false,"growWidthWithContent":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":294,"y":0,"exactX":false,"exactY":false,"fixed":false,"right":true,"top":true,"left":false,"growH":true}},"text":""}},"index":1},{"_id":"6a67a32519eeef7ac1c378d1","parent":"6a67a32519eeef7ac1c378d0","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"left":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":0},{"_id":"6a67a32519eeef7ac1c378d2","parent":"6a67a32519eeef7ac1c378d0","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":128,"height":40,"growWidthWithContent":true,"exactWidth":127.86}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true}},"childrenMode":"cols","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"style":{"padding":{"component":"data2-core-primitives@String","params":{"content":"0px 10px 0px 10px"}}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"7"}}},"fill":{"params":{"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}},"then":{"params":{"rgba":"rgba(0,0,0,0.1)"},"component":"data2-core-types@Color"},"else":{"params":{"rgba":"rgba(0,0,0,0)"},"component":"data2-core-types@Color"}},"component":"data2-core-flow@If"},"shadow":[{"params":{"then":{"component":"data2-core-types@Shadow","params":{"type":"inset","spread":"1","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}}},"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}}}}}},"else":{"component":"data2-core-types@Shadow","params":{"type":"inset","spread":"0","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}}}},"component":"data2-core-flow@If"}],"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}},{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}}},"onMouseLeave":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"onMouseDown":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}}}},"value":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}}}}}}},"component":"data2-core-logical-operators@Not"}}},"onClickOutside":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a67a32519eeef7ac1c378d2"}},"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"cursor":"pointer"},"index":1},{"_id":"6a6793087c969e738b8c3058","parent":"6a6793087c969e738b8c3057","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1,"height":40,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"left":true}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"index":0},{"_id":"6a6793087c969e738b8c305b","parent":"6a6793087c969e738b8c3057","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":128,"height":40,"growWidthWithContent":true,"exactWidth":127.86}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true}},"childrenMode":"cols","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"style":{"padding":{"component":"data2-core-primitives@String","params":{"content":"0px 10px 0px 10px"}}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"7"}}},"fill":{"params":{"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6793087c969e738b8c305b"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}},"then":{"params":{"rgba":"rgba(0,0,0,0.1)"},"component":"data2-core-types@Color"},"else":{"params":{"rgba":"rgba(0,0,0,0)"},"component":"data2-core-types@Color"}},"component":"data2-core-flow@If"},"shadow":[{"params":{"then":{"component":"data2-core-types@Shadow","params":{"type":"inset","spread":"1","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}}},"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6793087c969e738b8c305b"}},"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}}}}}},"else":{"component":"data2-core-types@Shadow","params":{"type":"inset","spread":"0","color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(108,92,224,1)"}}}}},"component":"data2-core-flow@If"}],"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}},{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"displayList"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6793087c969e738b8c305b"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}}},"onMouseLeave":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6793087c969e738b8c305b"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"cursor":"pointer","onMouseDown":{"disabled":false,"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"MNv9vHdKUB_s3iOqiMkiBInterface"}},"prop":"goTo","args":[{"component":"data2-core-primitives@String","params":{"content":"/acessos"}}]}}},"index":1},{"_id":"6a6792cd7c969e738b8c3054","parent":"6a6792457c969e738b8c3044","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":128,"height":40,"growWidthWithContent":true,"exactWidth":127.86}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"fixed":false,"exactX":false,"exactY":false,"left":true}},"childrenMode":"cols","childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"style":{"padding":{"component":"data2-core-primitives@String","params":{"content":"0px 10px 0px 10px"}}},"border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"7"}}},"fill":{"params":{"compare":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6792cd7c969e738b8c3054"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}}}},"then":{"params":{"rgba":"rgba(0,0,0,0.1)"},"component":"data2-core-types@Color"},"else":{"params":{"rgba":"rgba(0,0,0,0)"},"component":"data2-core-types@Color"}},"component":"data2-core-flow@If"},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}},"component":"data2-core-primitives@Boolean"}}],"onMouseEnter":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6792cd7c969e738b8c3054"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}}},"onMouseLeave":{"component":"data2-core-types@Setter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"__UIState"}},"prop":{"component":"data2-core-user-interface@StateProp","params":{"componentId":{"component":"data2-core-primitives@String","params":{"content":"6a6792cd7c969e738b8c3054"}},"key":{"component":"data2-core-primitives@String","params":{"content":"hovered"}}}},"value":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"cursor":"pointer"},"index":1},{"_id":"6a626ce961501108689e9730","parent":"6a626ce961501108689e972e","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":114,"height":38,"exactWidth":114.15,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Rec. Natura"}}},"index":0},{"_id":"6a626ce961501108689e972f","parent":"6a626ce961501108689e972e","component":"data2-blocks@Html","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":22,"height":20,"exactWidth":21.84,"exactHeight":20.27}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":"111","y":9,"exactX":false,"exactY":false}},"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":43}}},"text":{"component":"data2-core-primitives@String","params":{"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m19.5 8.25-7.5 7.5-7.5-7.5\" />\n</svg>\n"}}},"index":1},{"_id":"6a621854ae79ef246a75775a","parent":"6a621854ae79ef246a757759","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":103,"height":42,"exactWidth":102.80000000000001,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":10,"exactX":false,"exactY":false,"left":true}},"text":{"params":{"components":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"content":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.CBs"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-primitives@Number","break":true}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-numerical-operators@Add"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":22}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}}}},{"_id":"6a621854ae79ef246a75775b","parent":"6a621854ae79ef246a757759","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":140,"height":32,"exactWidth":140,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":"45","exactX":false,"exactY":false,"left":true,"right":true,"growH":false}},"text":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"CBs no setor "}},{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setor"}}}}]},"component":"data2-core-functions@StringConcat"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"bottom"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a621854ae79ef246a75775d","parent":"6a621854ae79ef246a75775c","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":103,"height":42,"exactWidth":102.80000000000001,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":10,"exactX":false,"exactY":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"1.468"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":22}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}}}},{"_id":"6a621854ae79ef246a75775e","parent":"6a621854ae79ef246a75775c","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":140,"height":32,"exactWidth":140,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":45,"exactX":false,"exactY":false,"left":true,"right":true,"growH":false}},"text":{"component":"data2-core-primitives@String","params":{"content":"Desequilíbrio (máx−mín CBs)"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"bottom"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a621854ae79ef246a757760","parent":"6a621854ae79ef246a75775f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":103,"height":42,"exactWidth":102.80000000000001,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":10,"exactX":false,"exactY":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"1.468"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":22}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle"}}}},{"_id":"6a621854ae79ef246a757761","parent":"6a621854ae79ef246a75775f","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":140,"height":32,"exactWidth":140,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":45,"exactX":false,"exactY":false,"left":true,"right":true,"growH":false}},"text":{"component":"data2-core-primitives@String","params":{"content":"CBs realocadas"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"bottom"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a67a32519eeef7ac1c378d3","parent":"6a67a32519eeef7ac1c378d2","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":92,"height":40,"exactWidth":false,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":0,"exactY":0,"fixed":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"Cenários"}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","letterSpacing":1}},"index":{"component":"data2-core-primitives@Number","params":{"content":"1"}},"font":{"params":{"content":"NaturaCo"},"component":"data2-core-primitives@String"},"fill":{"disabled":true,"component":"data2-core-types@Fill","params":{}}},"index":1},{"_id":"6a6793087c969e738b8c305c","parent":"6a6793087c969e738b8c305b","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":92,"height":40,"exactWidth":false,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":0,"exactY":0,"fixed":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"Admin"}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","letterSpacing":1}},"index":{"component":"data2-core-primitives@Number","params":{"content":"1"}},"font":{"params":{"content":"NaturaCo"},"component":"data2-core-primitives@String"},"fill":{"disabled":true,"component":"data2-core-types@Fill","params":{}}},"index":1},{"_id":"6a6792cd7c969e738b8c3055","parent":"6a6792cd7c969e738b8c3054","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":92,"height":40,"exactWidth":false,"exactHeight":false,"growWidthWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":0,"exactY":0,"fixed":false,"left":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"Simulador"}},"textColor":{"disabled":true,"component":"data2-core-types@Color","params":{}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","letterSpacing":1}},"index":{"component":"data2-core-primitives@Number","params":{"content":"1"}},"font":{"params":{"content":"NaturaCo"},"component":"data2-core-primitives@String"},"fill":{"disabled":true,"component":"data2-core-types@Fill","params":{}},"onMouseDown":{"disabled":false,"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"MNv9vHdKUB_s3iOqiMkiBInterface"}},"prop":"goTo","args":[{"component":"data2-core-primitives@String","params":{"content":"/simulador"}}]}}},"index":1},{"_id":"6a6233960ff5a8d431e8cef3","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorGraphBeforeAfter","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":293.02000000000004}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"XaKyb6x3qMpZupICoqsnY"},{"_id":"6a6223b90ff5a8d431e8cecb","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorMatrixAsIs","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":293.02000000000004}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"L--dVhugwB17-_W00pA7S"},{"_id":"6a62728a61501108689e9744","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorSetorDestino","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":275,"exactWidth":false,"exactHeight":275}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"iW3NufyoQGjGAwhmVZ3ow"},{"_id":"6a62185bae79ef246a757765","component":"data2-core-primitives@Artboard","system":"6a57cfe9673eb7e3c5e57784","displayName":"HubSimulatorSpanPerSector","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":275,"exactWidth":false,"exactHeight":275}},"componentClass":{"disabled":false,"component":"data2-core-types@ComponentClass","params":{}},"font":{"params":{"content":"InstrumentSans"},"component":"data2-core-primitives@String"}},"reference":"PCOvC0KQdskFojBsWZBfd"},{"_id":"6a62703e61501108689e9739","parent":"6a6233960ff5a8d431e8cef3","component":"data2-blocks@IframeBlock","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":293.02000000000004}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false}}},"url":{"component":"data2-core-primitives@String","params":{"content":"https://graph.lucca-office.next.data2.app/"}}}},{"_id":"6a6223ba0ff5a8d431e8cecd","parent":"6a6223b90ff5a8d431e8cecb","component":"data2-blocks@IframeBlock","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":640,"height":293,"exactWidth":false,"exactHeight":293.02000000000004}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"bottom":true,"left":true,"top":true,"growV":false,"right":true,"growH":false,"fixed":false}}},"url":{"component":"data2-core-primitives@String","params":{"content":"https://table.lucca-office.next.data2.app/"}}}},{"_id":"6a62728a61501108689e9745","parent":"6a62728a61501108689e9744","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":275,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"right":true,"top":true,"left":true,"growH":false}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}},"index":4},{"_id":"6a62185eae79ef246a757771","parent":"6a62185bae79ef246a757765","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"13"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":275,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"right":true,"top":true,"left":true,"growH":false}},"text":"","fill":{"disabled":false,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(247,247,251,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}},"index":4},{"_id":"6a62728a61501108689e9746","parent":"6a62728a61501108689e9745","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":275,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false,"bottom":false,"growV":false}},"text":""},"index":2},{"_id":"6a62185eae79ef246a757772","parent":"6a62185eae79ef246a757771","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":265,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"text":""},"index":2},{"_id":"6a62728a61501108689e9747","parent":"6a62728a61501108689e9746","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#d6d6d6","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":28,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":20,"exactX":false,"exactY":false,"top":true,"left":true,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Municípios -> Setor destino"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a62728a61501108689e9748","parent":"6a62728a61501108689e9746","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":215,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":60,"left":true,"exactX":false,"exactY":false,"top":true,"right":true,"growH":false,"bottom":false,"growV":false,"fixed":false}},"textFormat":{"disabled":true,"component":"data2-core-types@TextFormat","params":{}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(127,127,127,1)"}},"repeat":{"disabled":false,"component":"data2-core-types@DataSetReference","params":{"reference":"2A0QvRsDzftCYYnjJ_hIe"}},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"m"}}}},{"_id":"6a62185eae79ef246a757773","parent":"6a62185eae79ef246a757772","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#d6d6d6","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":20,"y":20,"exactX":20,"exactY":20,"top":true,"left":true,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"left"}},"text":{"component":"data2-core-primitives@String","params":{"content":"Span por setor (CBS) — base + entrantes"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":14}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":546,"height":28,"exactWidth":false,"exactHeight":false}}}},{"_id":"6a624de8203e921fd2513b0a","parent":"6a62185eae79ef246a757772","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":564,"height":195,"exactWidth":false,"exactHeight":false,"growHeightWithContent":true}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"1","y":"60","left":true,"exactX":false,"exactY":false,"top":true,"right":true,"growH":false,"bottom":false,"growV":false,"fixed":false}},"repeat":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setores_destino"}}},"component":"data2-core-types@Getter"},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"s"}}}},{"_id":"6a62728a61501108689e9749","parent":"6a62728a61501108689e9748","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":526,"height":82,"exactWidth":526,"exactHeight":82}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":20,"y":10,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}}}},{"_id":"6a62185eae79ef246a757774","parent":"6a624de8203e921fd2513b0a","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":566,"height":35,"exactWidth":false,"exactHeight":35}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":10,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"right":true,"growH":false}},"text":""}},{"_id":"6a62732e61501108689e975e","parent":"6a62728a61501108689e9749","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":526,"height":52,"exactWidth":false,"exactHeight":52}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":30,"left":true,"exactX":false,"exactY":false,"top":true,"fixed":false,"right":true,"growH":false}},"childrenMode":"cols","border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"9"},"width":{"top":"1","bottom":"1","left":"1","right":"1"},"color":"#9a9b9c"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"19"}},"repeat":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Y0MVeSB0t_bjBEppVYgQJ"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.setores_destino"}}},"component":"data2-core-types@Getter"},"repeatReference":{"component":"data2-core-primitives@String","params":{"content":"s"}}},"index":2},{"_id":"6a6272db61501108689e9757","parent":"6a62728a61501108689e9749","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":526,"height":20,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":0,"exactX":false,"exactY":false,"top":true,"left":true,"right":true,"growH":false}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"textOverflow":false,"verticalAlignment":"middle","horizontalAlignment":"center"}},"text":{"params":{"portions":[{"params":{"string":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mRepeater.MUNICIPIO"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@toUpperCase"},{"component":"data2-core-primitives@String","params":{"content":" ("}},{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mRepeater.CBs"}}},"component":"data2-core-types@Reference"},{"component":"data2-core-primitives@String","params":{"content":"CBs)"}}]},"component":"data2-core-functions@StringConcat"},"textSize":{"component":"data2-core-primitives@Number","params":{"content":17}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(94,94,94,1)"}}}},{"_id":"6a62185eae79ef246a757775","parent":"6a62185eae79ef246a757774","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":82,"height":29,"exactWidth":82.11,"exactHeight":29.32}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":3,"exactX":false,"exactY":false,"left":true}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater"}}},"component":"data2-core-types@Reference"},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle","fontWeight":700}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(71,85,105,1)"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"19"}}}},{"_id":"6a62185eae79ef246a757776","parent":"6a62185eae79ef246a757774","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"disabled":false,"component":"data2-core-types@Border","params":{"radius":{"leftTop":"9"},"width":{"top":"0","bottom":"0","left":"0","right":"0"},"color":"#9a9b9c"}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(71,85,105,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":400,"height":35,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":83,"y":0,"exactX":false,"exactY":false,"left":true,"top":true,"right":true,"growH":false}},"childrenMode":"reversed-cols"}},{"_id":"6a62185eae79ef246a757777","parent":"6a62185eae79ef246a757774","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":63,"height":17,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":483,"y":0,"exactX":false,"exactY":false,"right":true,"top":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"1.648"}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"right","textOverflow":false,"verticalAlignment":"middle"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"19"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(137,137,137,1)"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a62c2c118443ac5c92bd0da","parent":"6a62185eae79ef246a757774","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":63,"height":14,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":483,"y":21,"exactX":false,"exactY":false,"right":true,"top":true}},"text":{"component":"data2-core-primitives@String","params":{"content":"+745"}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"right","textOverflow":false,"verticalAlignment":"middle"}},"textSize":{"component":"data2-core-primitives@Number","params":{"content":"17"}},"textColor":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(109,222,149,1)"}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},{"_id":"6a62732e61501108689e975f","parent":"6a62732e61501108689e975e","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{"leftTop":"7"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":136,"height":46,"exactWidth":false,"exactHeight":46}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":"2","y":2,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true}},"textFormat":{"disabled":false,"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","textOverflow":false,"verticalAlignment":"middle"}},"text":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sRepeater"}}},"component":"data2-core-types@Reference"},"cursor":"pointer","style":{"flex":{"component":"data2-core-primitives@String","params":{"content":"1"}}}}},{"_id":"6a62531d203e921fd2513b36","parent":"6a62185eae79ef246a757776","component":"data2-core-primitives@Block","system":"6a57cfe9673eb7e3c5e57784","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,255,255,0.5)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":214,"height":35,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"fixed":false,"left":true,"top":true,"growH":true}}}}]}},{"_id":"5fc4faeec8da12be8bf3d08a","component":"data2-core@Data2TableDataSource","system":"5e3b796d6228227ca637d0f4","params":{"paginate":{"default":1000},"super":{},"table":"fileComponentInstances","schema":[{"component":"data2-core-types@Field","params":{"key":"component","title":"Component","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"displayName","title":"Display Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"params","title":"Params","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"reference","title":"Reference","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"fileId"}},"title":"File ID","component":"data2-core-primitives@ObjectId"},"break":true}],"hooks":{"before":{"all":[{"hook":"softDelete"}],"find":[{"hook":"useCache","options":{"additionalGroupKeys":["$and.0.system","$and.1.system"],"skipCache":true}}],"create":[{"hook":"timestamp","options":{"fields":["createdAt","updatedAt"]}}],"update":[{"hook":"timestamp","options":{"fields":["updatedAt"]}}],"patch":[{"hook":"timestamp","options":{"fields":["updatedAt"]}}]},"after":{"all":[{"hook":"fileUrl","options":{"downloadField":"params.fileUrl","uploadField":"params.uploadFileUrl","enhanceSchema":false}}],"find":[{"hook":"useCache","options":{"additionalGroupKeys":["$and.0.system","$and.1.system"],"skipCache":true}}],"create":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"update":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"patch":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"remove":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}]}},"multi":["create"],"useEstimatedDocumentCount":true},"reference":"FileComponentInstancesTableDataSource","updatedAt":"2022-12-19T20:42:51.837Z","createdAt":"2020-11-30T13:59:52.294Z"},{"reference":"ApplicationsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a5e89d15c2b589c492f4469","displayName":"Hub-App","reference":"zcVHYTqjED3Nk5Rj0SWHl","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"hub"}},"entryPoint":{"disabled":false,"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Router","reference":"MNv9vHdKUB_s3iOqiMkiB"}},"font":{"params":{"fonts":[{"params":{"family":{"component":"data2-core-primitives@String","params":{"content":"instrumentSans"}},"faces":[{"src":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"nrYoNtsk0eEXl6idp81hc"}},"format":"truetype"}]},"component":"data2-core-web@Font"},{"component":"data2-core-web@Font","params":{"family":{"component":"data2-core-primitives@String","params":{"content":"NaturaCo"}},"faces":[{"format":"opentype","style":"normal","weight":"normal","src":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"2zxSHUwz93sk1ruAO9TrY"}}},{"format":"opentype","style":"normal","weight":"bold","src":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"O4yB3yzwEUsWuSLUedK8v"}}},{"style":"normal","format":"opentype","weight":"100","src":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"RvhhKo1F_K1wzhwpdh9Iq"}}},{"format":"opentype","style":"italic","weight":"100","src":{"component":"data2-core-types@FileComponentUrlReference","params":{"reference":"6bbmw2LPbYaVCzkZJVaIk"}}}]},"displayName":"NaturaCo"}]},"component":"data2-core-web@Fonts"}},"system":"6a57cfe9673eb7e3c5e57784","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a74ae2663667431f75e45b2","component":"data2-core@AuthenticationStrategy","system":"6a57cfe9673eb7e3c5e57784","params":{"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"AEKK_Z4OsZ2njfQxg7tS6"}},"credentials":[{"component":"data2-core-types@FieldReference","params":{"key":"email"}},{"component":"data2-core-types@FieldReference","params":{"key":"password"}}],"email":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},"password":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}}},"reference":"timelineSimAuthFinal"},{"_id":"6a74b5a363667431f75e53d9","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"timeline-shared-sessions"}}},"reference":"timeline-shared-sessionsDataSet"},{"_id":"6a888cd8696364a2cbd09d2d","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"gb9Mu-5_9AcCDK-myWwSy"}}},"reference":"gb9Mu-5_9AcCDK-myWwSyDataSet","deleteMetadata":{}},{"_id":"6a74bb3c32c4b185b93738ce","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"timeline-sim-users"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"pagination":{"size":20}},"reference":"AEKK_Z4OsZ2njfQxg7tS6"},{"_id":"6a74b5a3a3c269c78fe7bf7c","component":"data2-core@Data2TableDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"table":"timeline-shared-sessions","schema":[{"component":"data2-core-types@Field","params":{"key":"label","title":"Nome da sessao","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"snapshotRaw","title":"Snapshot congelado","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"settings","title":"Regras da sessao","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"stages","title":"Etapas (display model)","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"summary","title":"Resumo","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"sharedBy","title":"Compartilhado por","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"sharedAt","title":"Compartilhado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"snapLiteral","title":"Snapshot (SQL literal)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"eloMaxCycle","title":"Ciclo maximo ELO","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"movements","title":"Movimentos","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"createdAt","title":"Criado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updatedAt","title":"Atualizado em","component":"data2-core-primitives@Date"}}]},"reference":"timeline-shared-sessions"},{"_id":"6a888cd8c0d30aec2da3df33","component":"data2-core@Data2TableDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"table":"gb9Mu-5_9AcCDK-myWwSy","schema":[{"component":"data2-core-types@Field","params":{"key":"de","title":"Grafia encontrada (mapa/IBGE)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"para","title":"Grafia da base (CRM/dominantes)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"observacao","title":"Observação","component":"data2-core-primitives@String"}}]},"reference":"gb9Mu-5_9AcCDK-myWwSy","deleteMetadata":{}},{"_id":"6a74ae1b8bf67595bba6dcfb","component":"data2-core@Data2TableDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"table":"timeline-sim-users","schema":[{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"email","title":"email","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"password","title":"senha","component":"data2-core-types@Password"}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Perfil","component":"data2-core-primitives@String"}}]},"reference":"timeline-sim-users"},{"_id":"6a699f5d97aef987ef45b37b","component":"data2-core@Store","system":"6a57cfe9673eb7e3c5e57784","params":{},"reference":"Y0MVeSB0t_bjBEppVYgQJ"},{"_id":"6a5e8bff5c2b589c492f446a","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"76F6DtuCCd1wGRBMLERZE"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"tFznon1CqdR98RwcHH1w_"},{"_id":"6a6c311217b8ebd6c23e2db0","component":"data2-core@Store","system":"6a57cfe9673eb7e3c5e57784","params":{},"reference":"ziIl3zJpxF2x8qsRQy10a"},{"_id":"6a699f5197aef987ef45b37a","component":"data2-core@Store","system":"6a57cfe9673eb7e3c5e57784","params":{},"reference":"NLO6dHteJxVmbynNHsqPv"},{"_id":"6a5e89c55c2b589c492f4468","component":"data2-core@Data2TableDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"table":"76F6DtuCCd1wGRBMLERZE","multi":["create"],"schema":[{"component":"data2-core-types@Field","params":{"title":"email","key":"email","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"title":"senha","key":"senha","component":"data2-core-types@Password"}},{"component":"data2-core-types@Field","params":{"title":"nome","key":"nome","component":"data2-core-primitives@String"}}]},"reference":"76F6DtuCCd1wGRBMLERZE"},{"_id":"6a626dc261501108689e9734","component":"data2-core@Store","system":"6a57cfe9673eb7e3c5e57784","params":{},"reference":"kfqDiL83O6CTlKOfellwq"},{"_id":"6a692a3a9b51c69afae51b0a","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"40C0Kka9Nw6QCf_-OTbbr"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":10}}}},"reference":"FhlWyAglkQB5EL4hrbzmq"},{"_id":"6a692869a8f059878f14c3eb","component":"http@CustomHTTPDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{},"find":{"params":{"actions":[{"params":{"url":{"component":"data2-core-primitives@String","params":{"content":"https://natura-geomarketing.data2.media/file/KomiqgWenIVasWeRzeSlW"}},"method":"GET"},"component":"http@Request"},{"params":{"csv":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@CSVParse"},{"params":{"column":{"component":"data2-core-primitives@String","params":{"content":"CD_SETOR"}},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.data"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@Group"},{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"setor":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"m.group"}}},"component":"data2-core-types@Reference"}}}}},"component":"data2-core-functions@ArrayMap"},{"records":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},"count":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.length"}}},"component":"data2-core-types@Reference"}}]},"component":"data2-core-flow@Series"},"countKey":{"component":"data2-core-primitives@String","params":{"content":"count"}},"recordsKey":{"component":"data2-core-primitives@String","params":{"content":"records"}},"policies":[{"component":"data2-core-types@PolicyReference","params":{"allow":{"component":"data2-core-auth@InvokedByBuilder","params":{}}}}]},"reference":"40C0Kka9Nw6QCf_-OTbbr"},{"_id":"6a6937679b51c69afae51b0f","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"RCNSJz611z4PJMwRKk0Al"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"filter":{"params":{"CD_SETOR":{"component":"data2-core-primitives@String","params":{"content":"0000000"}}},"component":"data2-core-types@DataSetFilter"}},"reference":"2A0QvRsDzftCYYnjJ_hIe"},{"_id":"6a6b50226dd0259eb0b8fc3f","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"RCNSJz611z4PJMwRKk0Al"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"pagination":{"size":20},"filter":{"params":{"MUNICIPIO":{"in":{"component":"data2-core-primitives@String","params":{"content":"---"}}}},"component":"data2-core-types@DataSetFilter"}},"reference":"UI2RIyzH2ZnVnYlZtETzi"},{"_id":"6a6c306417b8ebd6c23e2daf","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"KugnlUKJX2XGPnuL8YCnj"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"umJZo_TTyw0E3lOwBNmk2"},{"_id":"6a6b4d276dd0259eb0b8fc3e","component":"data2-core@DataSet","system":"6a57cfe9673eb7e3c5e57784","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"KugnlUKJX2XGPnuL8YCnj"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"7rq4WNBhLtN4x9qy-262L"},{"_id":"6a6937679b51c69afae51b0e","component":"http@CustomHTTPDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{},"find":{"params":{"actions":[{"params":{"url":{"component":"data2-core-primitives@String","params":{"content":"https://natura-geomarketing.data2.media/file/KomiqgWenIVasWeRzeSlW"}},"method":"GET"},"component":"http@Request"},{"params":{"csv":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@CSVParse"},{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.data"}}},"component":"data2-core-types@Reference"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.CD_SETOR"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.CD_SETOR"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"},"else":{"params":{"pattern":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.MUNICIPIO.$in"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@Includes"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},{"records":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},"count":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.length"}}},"component":"data2-core-types@Reference"}}]},"component":"data2-core-flow@Series"},"countKey":{"component":"data2-core-primitives@String","params":{"content":"count"}},"recordsKey":{"component":"data2-core-primitives@String","params":{"content":"records"}}},"reference":"RCNSJz611z4PJMwRKk0Al"},{"_id":"6a6b4d276dd0259eb0b8fc3d","component":"http@CustomHTTPDataSource","system":"6a57cfe9673eb7e3c5e57784","params":{"super":{},"countKey":{"component":"data2-core-primitives@String","params":{"content":"count"}},"recordsKey":{"component":"data2-core-primitives@String","params":{"content":"records"}},"find":{"params":{"actions":[{"params":{"url":{"component":"data2-core-primitives@String","params":{"content":"https://natura-geomarketing.data2.media/file/KomiqgWenIVasWeRzeSlW"}},"method":"GET"},"component":"http@Request"},{"params":{"csv":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@CSVParse"},{"params":{"array":{"params":{"array":{"params":{"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.data"}}},"component":"data2-core-types@Reference"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.CD_SETOR"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"comparisonSubject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.CD_SETOR"}}},"component":"data2-core-types@Reference"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.CD_SETOR"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.b"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"ff"}}],"body":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.MUNICIPIO"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ff"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"}}},"array":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.b"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-functions@ArrayFind"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"f"}}],"body":{"params":{"compare":{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.a"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"subject":{"params":{"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"f.CD_SETOR"}}},"component":"data2-core-types@Reference"},"comparisonSubject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"query.a"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Equal"}},"component":"data2-core-logical-operators@Not"}},"component":"data2-core-flow@If"}}}},"component":"data2-core-functions@ArrayFilter"},{"records":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult"}}},"component":"data2-core-types@Reference"},"count":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prevResult.length"}}},"component":"data2-core-types@Reference"}}]},"component":"data2-core-flow@Series"},"policies":[{"component":"data2-core-types@PolicyReference","params":{"allow":{"component":"data2-core-auth@InvokedByBuilder","params":{}}}}]},"reference":"KugnlUKJX2XGPnuL8YCnj"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}