{"context":{"domain":"lucca-office"},"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":"6a11143bae205cd35b1d5de0","name":"Lucca Office","domain":"lucca-office","visibility":"public","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-14T20:00:20.536Z","createdAt":"2026-05-23T02:43:07.343Z"}]}},{"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":"6a602c613a42b34509a62927","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"interactive-muni-map","package":"lucca-office","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    .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    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 } };\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      return storeRef ? d2.store(storeRef).get(key, S.assign[key]) : S.assign[key];\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        if (lastAssignSnapshot[key] !== v) { changed = true; lastAssignSnapshot[key] = v; }\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 groups = d2.param('groups', []);\n      const colors = d2.param('colors', {});\n      const colorFor = g => colors[g] || PALETTE[groups.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 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        const currentSector = getAssign(key);\n        p.setAttribute('fill', currentSector ? colorFor(currentSector) : (clickable ? '#94a3b8' : '#dcdfe8'));\n        if (clickable) {\n          p.onclick = () => {\n            if (dragMoved) { dragMoved = false; return; }\n            const gs = d2.param('groups', []);\n            if (!gs.length) return;\n            const next = gs[(gs.indexOf(getAssign(key)) + 1) % gs.length];\n            S.assign[key] = next;\n            const storeRef = d2.param('store');\n            if (storeRef) d2.store(storeRef).set(key, next);\n            drawMap();\n            d2.emit('onChange', { 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          tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b><br>${esc(getAssign(key) || (clickable ? 'sem destino' : 'apenas contexto'))}`;\n        };\n        p.onmouseleave = () => { $('#tip').style.opacity = 0; };\n        viewport.appendChild(p);\n\n        const [cx, cy] = cxy(f, S.pr);\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 = groups.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        // 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 closingNames = [...new Set([...neighborhoodNames, ...assignedLoaded])];\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\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"}}}}}}}}}},{"_id":"6a622469425e2477b774cbed","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"matriz-asis-table","package":"lucca-office","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 fmt = n => Math.round(n).toLocaleString('pt-BR');\nconst esc = s => (s == null ? '' : String(s)).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));\nconst isNum = v => typeof v === 'number' && Number.isFinite(v);\nconst cellText = v => {\n  if (v == null) return '';\n  if (typeof v === 'object') return esc(JSON.stringify(v));\n  return isNum(v) ? fmt(v) : esc(v);\n};\n\n// Accepts '#rgb', '#rrggbb', 'rgb(...)' or 'rgba(...)' — returns \"r,g,b\" or null.\nfunction parseRgb(input) {\n  const s = (input == null ? '' : String(input)).trim();\n  let m = s.match(/^#([0-9a-f]{3})$/i);\n  if (m) return m[1].split('').map(c => parseInt(c + c, 16)).join(',');\n  m = s.match(/^#([0-9a-f]{6})$/i);\n  if (m) return [m[1].slice(0, 2), m[1].slice(2, 4), m[1].slice(4, 6)].map(h => parseInt(h, 16)).join(',');\n  m = s.match(/^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);\n  if (m) return `${m[1]},${m[2]},${m[3]}`;\n  return null;\n}\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%;--panel:#ffffff;--line:#e7e7ef;--ink:#1c1c2b;--mut:#71717f}\n    *{box-sizing:border-box}\n    .wrap{width:100%;height:100%;display:flex;flex-direction:column;background:var(--panel);font:12px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;color:var(--ink)}\n    .scroll{flex:1;min-height:0;overflow:auto}\n    .empty{padding:16px;color:var(--mut)}\n    table{border-collapse:collapse;width:100%;font-size:11.5px}\n    th,td{padding:5px 8px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}\n    th.num,td.num{text-align:right;font-variant-numeric:tabular-nums}\n    th.bold,td.bold{font-weight:700}\n    thead th{color:var(--mut);font-weight:600;font-size:10.5px;position:sticky;top:0;background:var(--panel)}\n  `,\n  render(d2) {\n    const rows = d2.param('rows', []) || [];\n    const heatmapCols = new Set(d2.param('heatmap', []) || []);\n    const heatmapRgb = parseRgb(d2.param('heatmapColor')) || '229,181,63';\n    const firstCol = d2.param('first');\n    const lastCol = d2.param('last');\n\n    if (!rows.length) {\n      return `<div class=\"wrap\"><div class=\"empty\">Sem dados — passe uma lista de objetos em <code>rows</code>; cada propriedade vira uma coluna.</div></div>`;\n    }\n\n    // Column order = key order as properties first appear across the rows.\n    const cols = [];\n    const seen = new Set();\n    rows.forEach(r => {\n      Object.keys(r || {}).forEach(k => {\n        if (!seen.has(k)) { seen.add(k); cols.push(k); }\n      });\n    });\n\n    // `first`/`last` pin a column to either end of the table.\n    if (firstCol && cols.includes(firstCol)) {\n      cols.splice(cols.indexOf(firstCol), 1);\n      cols.unshift(firstCol);\n    }\n    if (lastCol && cols.includes(lastCol)) {\n      cols.splice(cols.indexOf(lastCol), 1);\n      cols.push(lastCol);\n    }\n\n    // A column is numeric only if every value present in it is a number.\n    const numericCol = {};\n    cols.forEach(c => {\n      numericCol[c] = rows.every(r => r[c] == null || isNum(r[c]));\n    });\n\n    // Heatmap intensity is scaled GLOBALLY: one shared max across every value in\n    // every column named in `heatmap`, so intensity is comparable column-to-column\n    // (a 200 in one heatmap column looks exactly as intense as a 200 in another).\n    let heatMax = 1;\n    if (heatmapCols.size) {\n      rows.forEach(r => {\n        heatmapCols.forEach(c => {\n          if (isNum(r[c]) && r[c] > heatMax) heatMax = r[c];\n        });\n      });\n    }\n\n    const head = `<tr>${cols.map(c => `<th class=\"${numericCol[c] ? 'num' : ''}${c === lastCol ? ' bold' : ''}\">${esc(c)}</th>`).join('')}</tr>`;\n\n    const body = rows\n      .map((r, i) => {\n        const key = r[cols[0]] != null ? esc(r[cols[0]]) : i;\n        const cells = cols\n          .map(c => {\n            const v = r[c];\n            let bg = '';\n            if (heatmapCols.has(c) && isNum(v) && v > 0) {\n              const ratio = v / heatMax;\n              bg = ` style=\"background:rgba(${heatmapRgb},${(0.08 + ratio * 0.5).toFixed(2)})\"`;\n            }\n            return `<td class=\"${numericCol[c] ? 'num' : ''}${c === lastCol ? ' bold' : ''}\"${bg}>${cellText(v)}</td>`;\n          })\n          .join('');\n        return `<tr data-key=\"${key}\">${cells}</tr>`;\n      })\n      .join('');\n\n    return `<div class=\"wrap\">\n      <div class=\"scroll\">\n        <table>\n          <thead>${head}</thead>\n          <tbody>${body}</tbody>\n        </table>\n      </div>\n    </div>`;\n  },\n};\n"}},"rows":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rows"}}}},"heatmap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"heatmap"}}}},"heatmapColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"heatmapColor"}}}},"first":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"first"}}}},"last":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"last"}}}}}}}}}},{"_id":"6a623156e0a2e35d92bba09a","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"before-after-chart","package":"lucca-office","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 PALETTE = ['#8b7fd6', '#d1b048', '#67bd5b', '#5aa9d6', '#d67fa8', '#c98a4b'];\nconst fmt = n => Math.round(n).toLocaleString('pt-BR');\nconst esc = s => (s == null ? '' : String(s)).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));\n\nfunction validateRow(r, i) {\n  const where = `linha ${i + 1}`;\n  if (r == null || typeof r !== 'object' || Array.isArray(r)) {\n    return { ok: false, error: `${where}: esperado um objeto { label, before, after, color? }, recebido ${JSON.stringify(r)}` };\n  }\n  if (r.label == null || (typeof r.label !== 'string' && typeof r.label !== 'number')) {\n    return { ok: false, error: `${where}: \"label\" ausente ou inválido (esperado string/number)` };\n  }\n  const before = r.before == null ? 0 : Number(r.before);\n  if (!Number.isFinite(before)) {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"before\" não é um número válido (${JSON.stringify(r.before)})` };\n  }\n  const after = r.after == null ? 0 : Number(r.after);\n  if (!Number.isFinite(after)) {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"after\" não é um número válido (${JSON.stringify(r.after)})` };\n  }\n  if (r.color != null && typeof r.color !== 'string') {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"color\" deveria ser uma string (ex: \"#8b7fd6\")` };\n  }\n  return { ok: true, row: { label: String(r.label), before, after, color: r.color || null } };\n}\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%}\n    *{box-sizing:border-box}\n    .wrap{width:100%;height:100%;display:flex;flex-direction:column;background:#ffffff;font:12px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;padding:8px}\n    .empty{padding:16px;color:#71717f}\n    .empty.error{color:#c8104e}\n    .errbar{flex:none;background:#fdecef;color:#c8104e;border:1px solid #f0c2cb;border-radius:6px;padding:6px 9px;font-size:11px;margin-bottom:6px;line-height:1.5}\n    svg{width:100%;flex:1;min-height:0;display:block}\n  `,\n  render(d2) {\n    const rowsParam = d2.param('rows', []);\n\n    if (!Array.isArray(rowsParam)) {\n      return `<div class=\"wrap\"><div class=\"empty error\">\"rows\" deveria ser uma lista de objetos { label, before, after, color? } — recebido tipo \"${esc(typeof rowsParam)}\".</div></div>`;\n    }\n    if (!rowsParam.length) {\n      return `<div class=\"wrap\"><div class=\"empty\">Sem dados — passe uma lista de objetos em <code>rows</code> (label, before, after, color?).</div></div>`;\n    }\n\n    const data = [];\n    const errors = [];\n    rowsParam.forEach((r, i) => {\n      const v = validateRow(r, i);\n      if (v.ok) data.push({ ...v.row, color: v.row.color || PALETTE[data.length % PALETTE.length] });\n      else errors.push(v.error);\n    });\n\n    if (!data.length) {\n      return `<div class=\"wrap\"><div class=\"empty error\">Nenhuma linha válida em \"rows\":<br>${errors.map(esc).join('<br>')}</div></div>`;\n    }\n\n    const max = Math.max(...data.flatMap(d => [d.before, d.after]), 1);\n    const W = 720, H = 290, base = 238, top = 42, maxH = base - top;\n    const n = data.length, gw = (W - 16) / n;\n\n    let g = '';\n    data.forEach((d, i) => {\n      const cx = 8 + gw * i + gw / 2;\n      const bw = Math.min(30, gw * 0.3);\n      const ah = (d.before / max) * maxH;\n      const dh = (d.after / max) * maxH;\n      const ax = cx - bw - 2;\n      const dx = cx + 2;\n      const dp = d.before ? ((d.after - d.before) / d.before) * 100 : (d.after ? 100 : 0);\n\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\">${fmt(d.before)}</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}\">${fmt(d.after)}</text>`;\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\">${esc(d.label)}</text>`;\n    });\n\n    const errorBanner = errors.length\n      ? `<div class=\"errbar\">${errors.length} linha(s) de \"rows\" ignorada(s): ${errors.map(esc).join(' · ')}</div>`\n      : '';\n\n    return `<div class=\"wrap\">${errorBanner}<svg viewBox=\"0 0 ${W} ${H}\" preserveAspectRatio=\"xMidYMid meet\"><line x1=\"8\" y1=\"${base}\" x2=\"${W - 8}\" y2=\"${base}\" stroke=\"#e7e7ef\"/>${g}</svg></div>`;\n  },\n};\n"}},"rows":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rows"}}}}}}}}}},{"_id":"6a63f0db4eab3e4ed9fd4da7","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"leaders-table","package":"lucca-office","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 PALETTE = ['#8b7fd6', '#d1b048', '#67bd5b', '#5aa9d6', '#d67fa8', '#c98a4b'];\nconst fmt = n => Math.round(n).toLocaleString('pt-BR');\nconst esc = s => (s == null ? '' : String(s)).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));\n\nfunction validateRow(r, i) {\n  const where = `linha ${i + 1}`;\n  if (r == null || typeof r !== 'object' || Array.isArray(r)) {\n    return { ok: false, error: `${where}: esperado um objeto { label, total, home, bySetor, desloc? }, recebido ${JSON.stringify(r)}` };\n  }\n  if (r.label == null || (typeof r.label !== 'string' && typeof r.label !== 'number')) {\n    return { ok: false, error: `${where}: \"label\" ausente ou inválido (esperado string/number)` };\n  }\n  const total = r.total == null ? 0 : Number(r.total);\n  if (!Number.isFinite(total)) {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"total\" não é um número válido (${JSON.stringify(r.total)})` };\n  }\n  if (r.bySetor != null && (typeof r.bySetor !== 'object' || Array.isArray(r.bySetor))) {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"bySetor\" deveria ser um objeto { setor: cbs }` };\n  }\n  const desloc = r.desloc == null ? 0 : Number(r.desloc);\n  if (!Number.isFinite(desloc)) {\n    return { ok: false, error: `${where} (\"${r.label}\"): \"desloc\" não é um número válido (${JSON.stringify(r.desloc)})` };\n  }\n  return { ok: true, row: { label: String(r.label), total, home: r.home != null ? String(r.home) : null, bySetor: r.bySetor || {}, desloc } };\n}\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%;--panel:#ffffff;--line:#e7e7ef;--ink:#1c1c2b;--mut:#71717f;--closing:#c8104e}\n    *{box-sizing:border-box}\n    .wrap{width:100%;height:100%;display:flex;flex-direction:column;background:var(--panel);font:12px -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;color:var(--ink)}\n    .scroll{flex:1;min-height:0;overflow:auto}\n    .empty{padding:16px;color:var(--mut)}\n    .empty.error{color:var(--closing)}\n    .errbar{flex:none;background:#fdecef;color:var(--closing);border:1px solid #f0c2cb;border-radius:6px;padding:6px 9px;font-size:11px;margin:8px;line-height:1.5}\n    table{border-collapse:collapse;width:100%;font-size:11.5px}\n    th,td{padding:5px 8px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap;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    tr.total td{font-weight:700}\n  `,\n  render(d2) {\n    const rowsParam = d2.param('rows', []);\n    const sectors = d2.param('sectors', []) || [];\n    const colors = d2.param('colors', {}) || {};\n    const showTotal = d2.param('showTotal', true);\n    const truncateWords = d2.param('truncateNameWords', 2);\n    const colorFor = s => colors[s] || PALETTE[sectors.indexOf(s) % PALETTE.length] || '#94a3b8';\n    const nameOf = label => (truncateWords > 0 ? label.split(' ').slice(0, truncateWords).join(' ') : label);\n\n    if (!Array.isArray(rowsParam)) {\n      return `<div class=\"wrap\"><div class=\"empty error\">\"rows\" deveria ser uma lista de objetos { label, total, home, bySetor, desloc? } — recebido tipo \"${esc(typeof rowsParam)}\".</div></div>`;\n    }\n    if (!rowsParam.length) {\n      return `<div class=\"wrap\"><div class=\"empty\">Sem dados — passe uma lista de líderes em <code>rows</code> e a ordem dos setores em <code>sectors</code>.</div></div>`;\n    }\n\n    const data = [];\n    const errors = [];\n    rowsParam.forEach((r, i) => {\n      const v = validateRow(r, i);\n      if (v.ok) data.push(v.row);\n      else errors.push(v.error);\n    });\n\n    if (!data.length) {\n      return `<div class=\"wrap\"><div class=\"empty error\">Nenhuma linha válida em \"rows\":<br>${errors.map(esc).join('<br>')}</div></div>`;\n    }\n\n    const head = `<tr><th>Líder</th><th>CBs</th><th>Home</th>${sectors.map(s => `<th>${esc(s)}</th>`).join('')}<th>Desloc.</th></tr>`;\n\n    const body = data.map((r, i) => {\n      const homeColor = r.home ? colorFor(r.home) : '#94a3b8';\n      const sectorCells = sectors.map(s => {\n        const v = r.bySetor[s];\n        const isHome = s === r.home;\n        const style = isHome ? ` style=\"color:${colorFor(s)};font-weight:700\"` : '';\n        return `<td${style}>${v != null ? fmt(v) : '·'}</td>`;\n      }).join('');\n      const deslocStyle = r.desloc ? ` style=\"color:var(--closing);font-weight:700\"` : ` style=\"color:var(--mut)\"`;\n      return `<tr data-key=\"${esc(r.label)}-${i}\">\n        <td>${esc(nameOf(r.label))}</td>\n        <td>${fmt(r.total)}</td>\n        <td>${r.home ? `<span class=\"home\" style=\"background:${homeColor}\"></span>${esc(r.home)}` : '–'}</td>\n        ${sectorCells}\n        <td${deslocStyle}>${fmt(r.desloc)}</td>\n      </tr>`;\n    }).join('');\n\n    let footer = '';\n    if (showTotal) {\n      const totalCbs = data.reduce((a, r) => a + r.total, 0);\n      const totalDesloc = data.reduce((a, r) => a + r.desloc, 0);\n      const sectorTotals = sectors.map(s => data.reduce((a, r) => a + (r.bySetor[s] || 0), 0));\n      footer = `<tr class=\"total\"><td>TOTAL</td><td>${fmt(totalCbs)}</td><td></td>${sectorTotals.map(v => `<td>${fmt(v)}</td>`).join('')}<td style=\"color:var(--closing)\">${fmt(totalDesloc)}</td></tr>`;\n    }\n\n    const errorBanner = errors.length\n      ? `<div class=\"errbar\">${errors.length} linha(s) de \"rows\" ignorada(s): ${errors.map(esc).join(' · ')}</div>`\n      : '';\n\n    return `<div class=\"wrap\">${errorBanner}<div class=\"scroll\"><table><thead>${head}</thead><tbody>${body}${footer}</tbody></table></div></div>`;\n  },\n};\n"}},"rows":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rows"}}}},"sectors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sectors"}}}},"colors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"colors"}}}},"showTotal":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"showTotal"}}}},"truncateNameWords":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"truncateNameWords"}}}}}}}}}},{"_id":"6a696da4dc1fded10b04d48f","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"NoQh9Rv3EeK6K8WBWkpPO","package":"lucca-office","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":"6a698066dc1fded10b04f6ba","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"get_neighboring_municipios","package":"lucca-office","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']};\n\nconst norm = s => (s||'').normalize('NFKD').replace(/[̀-ͯ]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,' ').trim();\nconst splitUfs = s => (s||'').split(',').map(x=>x.trim().toUpperCase()).filter(Boolean);\n\nasync 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\n// Same trick the map uses: a município's boundary points, rounded to ~110m, are\n// (near-)identical to its neighbor's along a shared edge. 2+ shared points = a real\n// edge (not just a corner-touch), which is what \"neighboring\" means here.\nfunction 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\nexport default {\n  css: `:host{display:none}`,\n  render(){\n    return '';\n  },\n  mounted(d2){\n    const writeResult = (neighbors) => {\n      const storeRef = d2.param('store');\n      if (storeRef) d2.store(storeRef).set('neighbors', neighbors);\n      d2.emit('onChange', { neighbors });\n    };\n    (async () => {\n      const targetsRaw = (d2.param('municipio', '') || '').split(',').map(x => x.trim()).filter(Boolean);\n      const ufs = splitUfs(d2.param('ufs', ''));\n      if (!targetsRaw.length || !ufs.length) { writeResult([]); return; }\n\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\n      const geos = await Promise.all(allUfs.map(u => geoUF(u).catch(() => null)));\n      const allFeats = [];\n      geos.forEach(g => { if (g) allFeats.push(...g.features); });\n      if (!allFeats.length) { writeResult([]); return; }\n\n      const adj = buildAdj(allFeats);\n      const targets = targetsRaw.map(norm).filter(t => adj.feat[t]);\n      if (!targets.length) { writeResult([]); return; }\n\n      const targetSet = new Set(targets);\n      const result = new Set();\n      targets.forEach(t => adj.nbrs(t).forEach(n => { if (!targetSet.has(n)) result.add(n); }));\n\n      const neighbors = [...result].map(n => adj.feat[n].f.properties.name || adj.feat[n].f.properties.NOME || adj.feat[n].f.properties.nome || n);\n      writeResult(neighbors);\n    })();\n  }\n};"}},"municipio":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipio"}}}},"ufs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ufs"}}}},"neighborUfs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"neighborUfs"}}}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"onChange":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onChange"}}}}}}}}}},{"_id":"6a6c50bb88f670e6c118e310","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"sector-closing-map","package":"lucca-office","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 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    .legend .closing{font-style:italic;opacity:.75}\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 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 against a single straight half-plane (side(x,y) >= 0 is kept)\n    // via Sutherland-Hodgman — exact for ANY simple polygon, convex or concave, so stacking\n    // these to slice a município into N vertical bands 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 (all MultiPolygon parts included), used to\n    // lay bands across its ENTIRE geometry 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    // Bootstrap `sectors` snapshot — used only to know which municípios to load geometry\n    // for. Live membership (incl. which municípios are still under a closing sector, which\n    // is permanent — see cycleMuni) is always read fresh from the Store, never from here.\n    const bootstrapSectors = JSON.parse(JSON.stringify(d2.param('sectors', []) || []));\n\n    let S = { sectorsLocal: JSON.parse(JSON.stringify(bootstrapSectors)), mapFeats: {}, bgFeats: [], labels: {}, pr: null, view: { scale: 1, tx: 0, ty: 0 } };\n    let dragStart = null, dragMoved = false;\n    let lastSig = null;\n\n    // Live sectors: the Store (if `store`+`prop` are bound) is the source of truth —\n    // reads are tracked, so an external change to the Store re-renders this map.\n    // `S.sectorsLocal` is only the bootstrap default before the Store has a value, or the\n    // fallback when no `store` param is bound at all.\n    function getLiveSectors(){\n      const storeRef = d2.param('store');\n      const prop = d2.param('prop');\n      if (!storeRef || !prop) return S.sectorsLocal;\n      const val = d2.store(storeRef).get(prop, null);\n      return Array.isArray(val) ? val : S.sectorsLocal;\n    }\n    function setLiveSectors(next){\n      S.sectorsLocal = next;\n      const storeRef = d2.param('store');\n      const prop = d2.param('prop');\n      if (!storeRef || !prop) return;\n      d2.store(storeRef).set(prop, next);\n    }\n\n    // Every sector's own `municipios` list is FIXED forever — a closing sector never loses\n    // or gains territory, and a non-closing sector's data is never touched by a click. What\n    // a click changes is metadata living ONLY on the closing sector's own entry: a `targets`\n    // map from município → the non-closing sector code it's currently pointed at. Rendering\n    // then paints that município using the TARGET's color/code instead of the closing\n    // sector's own — the closing sector still \"owns\" the município (it's still in its\n    // `municipios` list, still permanently clickable), it just visually partakes in\n    // whichever destination sector's identity is currently assigned.\n    function cycleMuni(muniKey, closingCode){\n      const live = getLiveSectors();\n      const sectorByCode = {};\n      live.forEach(s => { sectorByCode[s.code] = s; });\n      const closingSector = sectorByCode[closingCode];\n      if (!closingSector) return null;\n      const nonClosingCycle = live.filter(s => !s.closing_sector).map(s => s.code);\n      if (!nonClosingCycle.length) return null;\n\n      // Every code CURRENTLY shown on this município via a DIFFERENT sector's slot — a\n      // static non-closing membership, or another closing sector's own current pick — must\n      // never be picked again, so this slot's new color is always visibly distinct.\n      const otherDisplayed = new Set();\n      live.forEach(s => {\n        if (s.code === closingCode) return;\n        if (!(s.municipios||[]).some(m => norm(m) === muniKey)) return;\n        if (s.closing_sector) {\n          const t = s.targets && s.targets[muniKey];\n          otherDisplayed.add((t && sectorByCode[t]) ? t : s.code);\n        } else {\n          otherDisplayed.add(s.code);\n        }\n      });\n\n      const currentTarget = closingSector.targets && closingSector.targets[muniKey];\n      const startIdx = nonClosingCycle.indexOf(currentTarget);\n      let nextTarget = null;\n      for (let step = 1; step <= nonClosingCycle.length; step++) {\n        const candidate = nonClosingCycle[(startIdx + step) % nonClosingCycle.length];\n        if (!otherDisplayed.has(candidate)) { nextTarget = candidate; break; }\n      }\n      if (nextTarget == null) return null;\n\n      const updated = live.map(s => s.code === closingCode\n        ? { ...s, targets: { ...(s.targets||{}), [muniKey]: nextTarget } }\n        : s);\n      setLiveSectors(updated);\n      return { fromSector: currentTarget || null, toSector: nextTarget };\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 render()\n    // string), so a Store read here isn't auto-tracked the way it would be inside render().\n    // Poll the bound Store's sectors value every frame and redraw when it changed — this is\n    // what makes an EXTERNAL write to the Store (not just this map's own clicks) show up.\n    d2.raf(() => {\n      const storeRef = d2.param('store');\n      if (!storeRef) return;\n      const sig = JSON.stringify(getLiveSectors());\n      if (sig !== lastSig) { lastSig = sig; if (Object.keys(S.mapFeats).length) 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\n      const live = getLiveSectors();\n      const sectorByCode = {};\n      live.forEach(s => { sectorByCode[s.code] = s; });\n      const codeList = live.map(s => s.code);\n      const colorFor = code => (sectorByCode[code] && sectorByCode[code].color) || PALETTE[codeList.indexOf(code) % PALETTE.length] || '#94a3b8';\n\n      const partsByMuni = {};\n      Object.keys(S.mapFeats).forEach(key => {\n        const parts = [];\n        live.forEach(s => {\n          if (!(s.municipios||[]).some(m => norm(m) === key)) return;\n          if (s.closing_sector) {\n            const t = s.targets && s.targets[key];\n            const displayCode = (t && sectorByCode[t]) ? t : s.code;\n            parts.push({ displayCode, closingAnchor: s.code });\n          } else {\n            parts.push({ displayCode: s.code, closingAnchor: null });\n          }\n        });\n        partsByMuni[key] = parts;\n      });\n\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 (few municípios) or wide (many, spread out).\n      const labelFontSize = Math.max(4, Math.min(14, 9 * ((S.pr.scale || 1000) / 1000)));\n\n      S.bgFeats.forEach(f => {\n        const p = document.createElementNS(NS, 'path');\n        p.setAttribute('d', pathFor(f, S.pr));\n        p.setAttribute('class', 'bg');\n        viewport.appendChild(p);\n      });\n\n      const handleClick = (key, part) => {\n        const result = cycleMuni(key, part.closingAnchor);\n        if (!result) return;\n        drawMap();\n        d2.emit('onChange', { municipio: key, closingSector: part.closingAnchor, fromSector: result.fromSector, toSector: result.toSector });\n      };\n\n      Object.entries(S.mapFeats).forEach(([key, f]) => {\n        const parts = partsByMuni[key] || [];\n        const isPie = parts.length >= 2;\n        const p = document.createElementNS(NS, 'path');\n        p.setAttribute('d', pathFor(f, S.pr));\n        p.setAttribute('data-key', key);\n\n        if (isPie) {\n          // Multiple sectors claim this município at once — the shape itself is a neutral\n          // safety-net backdrop; the wedges below (clipped to the REAL border, one per\n          // sector) carry the actual colors and fully cover the município's area.\n          p.setAttribute('class', 'muni ctx');\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            tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b><br>${parts.length} setores: ${esc(parts.map(pt => pt.displayCode).join(', '))}`;\n          };\n          p.onmouseleave = () => { $('#tip').style.opacity = 0; };\n          viewport.appendChild(p);\n\n          const [bx0, , bx1] = bboxOf(f, S.pr);\n          const n = parts.length;\n          const stripeW = (bx1 - bx0) / n;\n          parts.forEach((part, 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', colorFor(part.displayCode));\n            // A wedge is interactive iff it's the closing sector's OWN slot — regardless of\n            // which sector's color it's currently borrowing for display.\n            const clickable = part.closingAnchor != null;\n            wedge.style.cursor = clickable ? 'pointer' : 'default';\n            if (clickable) wedge.onclick = () => { if (dragMoved) { dragMoved = false; return; } handleClick(key, part); };\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(part.displayCode)}${clickable ? ' (clique p/ reatribuir)' : ''}`;\n            };\n            wedge.onmouseleave = () => { $('#tip').style.opacity = 0; };\n            viewport.appendChild(wedge);\n          });\n        } else {\n          const part = parts[0] || null;\n          // A solo tile is interactive iff it's the closing sector's own slot — whether it's\n          // still showing the closing sector's own color (undecided) or already borrowing a\n          // target's color/code, it always stays clickable.\n          const clickable = !!(part && part.closingAnchor != null);\n          p.setAttribute('class', ['muni', clickable ? '' : 'ctx'].filter(Boolean).join(' '));\n          p.setAttribute('fill', part ? colorFor(part.displayCode) : '#dcdfe8');\n          if (clickable) p.onclick = () => { if (dragMoved) { dragMoved = false; return; } handleClick(key, part); };\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 = part ? part.displayCode : 'sem setor';\n            tip.innerHTML = `<b>${esc(S.labels[key] || key)}</b><br>${esc(label)}${clickable ? ' (clique p/ reatribuir)' : ''}`;\n          };\n          p.onmouseleave = () => { $('#tip').style.opacity = 0; };\n          viewport.appendChild(p);\n        }\n\n        const [cx, cy] = cxy(f, S.pr);\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 = live.map(s => `<span><b style=\"background:${colorFor(s.code)}\"></b>${esc(s.code)}${s.closing_sector ? '<i class=\"closing\"> (fechando)</i>' : ''}</span>`).join('');\n    }\n\n    async function load(){\n      loader(true, 'carregando malhas…', false);\n      try {\n        const ufs = splitUfs(d2.param('ufs', ''));\n        if (!ufs.length) throw new Error('informe ao menos uma UF em \"ufs\"');\n        const nbrUfs = splitUfs(d2.param('neighborUfs', '')).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 allFeats = [];\n        allUfs.forEach((u, i) => { const g = geos[i]; if (g) allFeats.push(...g.features); });\n        if (!allFeats.length) throw new Error('nenhuma malha carregada para essas UFs');\n\n        const idx = {};\n        allFeats.forEach(f => { const nm = norm(f.properties.name || f.properties.NOME || f.properties.nome); idx[nm] = f; });\n\n        const declaredNames = new Set();\n        bootstrapSectors.forEach(s => (s.municipios||[]).forEach(m => declaredNames.add(norm(m))));\n        if (!declaredNames.size) throw new Error('nenhum município declarado em \"sectors\"');\n\n        S.mapFeats = {}; S.labels = {};\n        declaredNames.forEach(n => {\n          if (idx[n]) { S.mapFeats[n] = idx[n]; S.labels[n] = idx[n].properties.name || idx[n].properties.NOME || idx[n].properties.nome || n; }\n        });\n        const missing = [...declaredNames].filter(n => !idx[n]);\n        if (!Object.keys(S.mapFeats).length) throw new Error('município(s) não encontrado(s) na(s) UF(s) informada(s): ' + missing.join(', '));\n\n        // Everything else loaded (rest of the state(s) / neighborUfs) is faint background context.\n        S.bgFeats = allFeats.filter(f => {\n          const n = norm(f.properties.name || f.properties.NOME || f.properties.nome || '');\n          return n && !S.mapFeats[n];\n        });\n\n        const zoomFeats = Object.values(S.mapFeats);\n        S.pr = project(zoomFeats.length ? zoomFeats : allFeats);\n        S.view = { scale: 1, tx: 0, ty: 0 };\n        drawMap();\n        lastSig = JSON.stringify(getLiveSectors());\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.mapFeats = {}; S.bgFeats = []; S.labels = {};\n      load();\n    };\n\n    load();\n  },\n};\n"}},"sectors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sectors"}}}},"ufs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ufs"}}}},"neighborUfs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"neighborUfs"}}}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"prop":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"prop"}}}},"onChange":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onChange"}}}}}}}}}},{"_id":"6a6c67f495b4a2c9973aacd2","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"UYcDX4v5ky74c0Eqq_U2l","package":"lucca-office","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  .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  .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\">carregando…</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 cruza o snapshot local (arquivos carregados, sem Databricks), 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 (antes/depois)</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>\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 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 DATA=null;\nasync function loadData(){\n  const res=await d2.rush('secDataFilesProxy').invoke({});\n  DATA=res && res.data ? res.data : res;\n}\n\nlet S={sector:null, matrix:[], munis:{}, leaders:[], color:{}, dest:[], assign:{}, allowed:{},\n       geoFeat:{}, nbrFeat:[], baseMetrics:{}, baseLevel:{}, levelByMuni:{},\n       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 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+' — cruzando snapshot local…');\n  $('#liveState').textContent='● processando';$('#ds').classList.remove('err');\n  try{\n    S.sector=sec;\n    const summary=DATA.summary[sec];\n    const muniGrupoRows=DATA.muniGrupo[sec]||[];\n    const muniNivelRows=DATA.muniNivel[sec]||[];\n\n    S.matrix=muniGrupoRows.map(r=>({municipio:r.municipio, grupo:r.grupo, lider:r.lider, cbs:r.cbs, receita:r.receita, pontos:r.pontos, ativas:r.ativas}));\n    S.levelByMuni={};\n    muniNivelRows.forEach(r=>{ (S.levelByMuni[r.municipio]=S.levelByMuni[r.municipio]||{})[r.nivel]=r.cbs; });\n\n    const domUF=summary?.uf||'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\n    const domSecOf={};\n    nbrNames.forEach(nb=>{\n      const cands=(DATA.muniSetor[nb]||[]).filter(r=>String(r.setor)!==String(sec));\n      if(cands.length) domSecOf[nb]=String(cands[0].setor);\n    });\n\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    S.baseMetrics={}; S.baseLevel={};\n    S.dest.forEach(s=>{\n      const b=DATA.summary[s];\n      S.baseMetrics[s]=b?{cbs:b.cbs,ativas:b.ativas,receita:b.receita,pontos:b.pontos}:{cbs:0,ativas:0,receita:0,pontos:0};\n      const lv={};(DATA.nivel[s]||[]).forEach(r=>{lv[r.nivel]=r.cbs;});\n      S.baseLevel[s]=lv;\n    });\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. · snapshot local · ${dt}`;\n    $('#liveState').textContent='● pronto (offline)';\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.pontos+=+r.pontos||0;o.ativas+=+r.ativas||0;};\n  const blank=()=>({cbs:0,receita:0,pontos: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'],['pontos','Pontos'],['receita','Receita','$']];\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 chartHTML=mk=>{\n    const isM=mk==='receita'; 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  $('#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}\nfunction loadSectors(){\n  S.sectorsList=Object.values(DATA.summary).map(s=>({setor:String(s.setor),uf:s.uf,cbs:+s.cbs,munis:+s.munis})).sort((a,b)=>+a.setor-+b.setor);\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='3487';\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'].forEach(t=>$('#tab-'+t).classList.toggle('hidden',t!==b.dataset.tab));});\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 snapshot local (dados dos arquivos, sem Databricks)…');try{await loadData();loadSectors();await closeSector('3487');}catch(e){$('#ds').textContent='erro init: '+e.message;}loader(false);})();\n\n  }\n}\n"}}}}}}}},{"_id":"6a713b47f8f3c5135cec6fd6","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"mj32g2gcm5nQXhRqJIhw6","package":"lucca-office","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:14px;border:1px 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(0,0,0,.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: 0, 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"}}}}}}}}}},{"_id":"6a7170c3a9d7cf0ea991a718","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"pq5nxhb0pbusiAlnbFhhO","package":"lucca-office","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:14px;border:1px 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    .stage.globe::after{content:'';position:absolute;inset:0;pointer-events:none;z-index:4;border-radius:inherit;background:radial-gradient(circle at 50% 50%, rgba(255,255,255,0) 52%, rgba(0,0,0,.12) 100%);box-shadow:inset 0 0 40px rgba(0,0,0,.08)}\n    svg{width:100%;height:100%;display:block}\n    .area{stroke:rgba(0,0,0,.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    function projRingsWorld(rings, pr){\n      return rings.map(ring => ring.map(p => pr(p)));\n    }\n    function stripeClipRingsPts(rings, pr, x0, x1){\n      const out = [];\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) out.push(pts);\n      });\n      return out;\n    }\n    function stringifyPtRings(ptRings, warp){\n      return ptRings.map(ring => 'M'+ring.map(p=>{const q=warp(p);return q[0].toFixed(1)+','+q[1].toFixed(1);}).join('L')+'Z').join('');\n    }\n    // Bends the already-projected flat map toward an orthographic-globe look: each point is\n    // pulled toward the camera-center radius via sin(theta) instead of growing linearly, so it\n    // compresses near the simulated limb — the classic \"looking at a sphere\" cue. `bend` softens\n    // as view.scale grows (zoomed in reads as close to the surface, ~flat), and grows as it\n    // shrinks (zoomed out reads as more of the globe's curvature). The camera center (cx,cy) is\n    // derived from the current pan/zoom, so the bend's origin follows wherever the user is looking.\n    function makeWarp(view, W, H){\n      const cx = (W / 2 - view.tx) / view.scale;\n      const cy = (H / 2 - view.ty) / view.scale;\n      const bend = Math.max(0.08, Math.min(0.92, 0.55 / Math.sqrt(view.scale)));\n      const maxR = Math.max(W, H) * 0.62;\n      return function(p){\n        const dx = p[0] - cx, dy = p[1] - cy;\n        const r = Math.hypot(dx, dy);\n        if (r < 1e-6) return [W / 2, H / 2];\n        const rn = Math.min(r / maxR, 1);\n        const theta = rn * (Math.PI / 2) * bend;\n        const k = Math.sin(theta) / rn;\n        const sx = W / 2 + dx * k * view.scale;\n        const sy = H / 2 + dy * k * view.scale;\n        const droop = (dx * dx) / (maxR * maxR) * bend * 14;\n        return [sx, sy + droop];\n      };\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: [], paths: [], labels: [], globeOn: null };\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    const VB_W = 700, VB_H = 500;\n    let globeRaf = null;\n    // Non-globe mode keeps the cheap path: a single <g transform> on the viewport, path `d`s\n    // untouched. Globe mode can't be expressed as one affine transform (the bend is nonlinear\n    // per-point), so it re-stringifies every path/label against the current camera each redraw —\n    // opt-in cost, gated entirely behind experimental_globe.\n    function redraw(){\n      const globeOn = !!d2.param('experimental_globe', false);\n      const modeChanged = globeOn !== S.globeOn;\n      S.globeOn = globeOn;\n      const stage = $('.stage');\n      if (stage) stage.classList.toggle('globe', globeOn);\n      if (globeOn) {\n        const g = $('#viewport');\n        if (g) g.removeAttribute('transform');\n        const warp = makeWarp(S.view, VB_W, VB_H);\n        S.paths.forEach(({ el, ptRings }) => el.setAttribute('d', stringifyPtRings(ptRings, warp)));\n        S.labels.forEach(({ el, pt }) => { const q = warp(pt); el.setAttribute('x', q[0]); el.setAttribute('y', q[1]); });\n      } else {\n        if (modeChanged) {\n          const flat = p => p;\n          S.paths.forEach(({ el, ptRings }) => el.setAttribute('d', stringifyPtRings(ptRings, flat)));\n          S.labels.forEach(({ el, pt }) => { el.setAttribute('x', pt[0]); el.setAttribute('y', pt[1]); });\n        }\n        applyTransform();\n      }\n    }\n    function scheduleRedraw(){\n      if (globeRaf) return;\n      globeRaf = requestAnimationFrame(() => { globeRaf = null; redraw(); });\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      scheduleRedraw();\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        scheduleRedraw();\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: 0, 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      S.paths = []; S.labels = []; S.globeOn = null;\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        const ptRings = projRingsWorld(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        S.paths.push({ el: p, ptRings });\n      });\n\n      const legendGroups = [];\n      S.focus.forEach(item => {\n        const rings = dissolveRings(item.idxs, IDX.recs);\n        const basePtRings = projRingsWorld(rings, S.pr);\n        const base = document.createElementNS(NS, 'path');\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        S.paths.push({ el: base, ptRings: basePtRings });\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 wedgePtRings = stripeClipRingsPts(rings, S.pr, bx0 + ci * stripeW, bx0 + (ci + 1) * stripeW);\n            if (!wedgePtRings.length) return;\n            const wedge = document.createElementNS(NS, 'path');\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            S.paths.push({ el: wedge, ptRings: wedgePtRings });\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');\n        t.style.fontSize = labelFontSize + 'px';\n        t.textContent = (item.label || '').slice(0, 16);\n        viewport.appendChild(t);\n        S.labels.push({ el: t, pt: [cx, cy] });\n\n        if (item.colors.length) legendGroups.push({ entry: item.entry, items: item.colors });\n      });\n\n      renderLegend(legendGroups);\n      redraw();\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};"}},"city_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"city_list"}}}},"country_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"country_list"}}}},"municipality_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"municipality_list"}}}},"region_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"region_list"}}}},"should_render_neighbors":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"should_render_neighbors"}}}},"state_list":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"state_list"}}}},"touch_controls":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"touch_controls"}}}},"experimental_globe":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"experimental_globe"}}}}}}}}}},{"_id":"6a73f636d5a555faccfd905c","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"NQBdVnJLb4YsikMQETP8X","package":"lucca-office","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function roundRect(ctx, x, y, w, h, r) {\n  const rr = Math.min(r, w / 2, h / 2)\n  ctx.beginPath()\n  ctx.moveTo(x + rr, y)\n  ctx.arcTo(x + w, y, x + w, y + h, rr)\n  ctx.arcTo(x + w, y + h, x, y + h, rr)\n  ctx.arcTo(x, y + h, x, y, rr)\n  ctx.arcTo(x, y, x + w, y, rr)\n  ctx.closePath()\n}\n\nfunction makeCanvasTexture(THREE, draw, w, h, scale) {\n  const s = scale || 2\n  const cnv = document.createElement('canvas')\n  cnv.width = w * s\n  cnv.height = h * s\n  const ctx = cnv.getContext('2d')\n  ctx.scale(s, s)\n  draw(ctx, w, h)\n  const tex = new THREE.CanvasTexture(cnv)\n  tex.needsUpdate = true\n  if (THREE.SRGBColorSpace) tex.colorSpace = THREE.SRGBColorSpace\n  return tex\n}\n\nfunction makeLabelSprite(THREE, text) {\n  const tex = makeCanvasTexture(THREE, (ctx, w, h) => {\n    ctx.clearRect(0, 0, w, h)\n    ctx.font = '600 40px -apple-system, \"Segoe UI\", Arial, sans-serif'\n    ctx.fillStyle = 'rgba(38,38,42,0.88)'\n    ctx.textAlign = 'center'\n    ctx.textBaseline = 'middle'\n    ctx.fillText(text, w / 2, h / 2)\n  }, 512, 128)\n  const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false })\n  const spr = new THREE.Sprite(mat)\n  spr.scale.set(1.8, 0.45, 1)\n  return spr\n}\n\nfunction makeGlowSprite(THREE, colorInner, colorOuter, size) {\n  const tex = makeCanvasTexture(THREE, (ctx, w, h) => {\n    const g = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, w / 2)\n    g.addColorStop(0, colorInner)\n    g.addColorStop(0.35, colorOuter)\n    g.addColorStop(1, 'rgba(255,255,255,0)')\n    ctx.fillStyle = g\n    ctx.fillRect(0, 0, w, h)\n  }, 512, 512, 1)\n  const mat = new THREE.SpriteMaterial({\n    map: tex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\n  })\n  const spr = new THREE.Sprite(mat)\n  spr.scale.set(size, size, 1)\n  return spr\n}\n\n// ---------- simple 2D value noise (no external lib) ----------\nfunction hash2(x, y) {\n  const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123\n  return s - Math.floor(s)\n}\nfunction noise2D(x, y) {\n  const xi = Math.floor(x), yi = Math.floor(y)\n  const xf = x - xi, yf = y - yi\n  const u = xf * xf * (3 - 2 * xf)\n  const v = yf * yf * (3 - 2 * yf)\n  const a = hash2(xi, yi)\n  const b = hash2(xi + 1, yi)\n  const c = hash2(xi, yi + 1)\n  const d = hash2(xi + 1, yi + 1)\n  return (a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v) * 2 - 1\n}\nfunction terrainHeight(x, y, peak, radius) {\n  const dist = Math.sqrt(x * x + y * y)\n  const falloff = Math.max(0, 1 - dist / radius)\n  const dome = Math.pow(falloff, 1.6)\n  const n1 = noise2D(x * 1.8 + 4.1, y * 1.8 + 1.7)\n  const n2 = noise2D(x * 5.5 + 9.3, y * 5.5 + 2.2)\n  const rough = (n1 * 0.12 + n2 * 0.045) * falloff\n  return Math.max(0, dome * peak + rough * peak)\n}\nconst TERRAIN_STOPS = [\n  { t: 0.00, c: [0.80, 0.72, 0.55] },\n  { t: 0.20, c: [0.36, 0.55, 0.27] },\n  { t: 0.52, c: [0.53, 0.50, 0.46] },\n  { t: 0.80, c: [0.84, 0.83, 0.80] },\n  { t: 1.00, c: [1.00, 1.00, 1.00] }\n]\nfunction heightColor(t) {\n  for (let i = 0; i < TERRAIN_STOPS.length - 1; i++) {\n    const a = TERRAIN_STOPS[i], b = TERRAIN_STOPS[i + 1]\n    if (t >= a.t && t <= b.t) {\n      const f = (t - a.t) / (b.t - a.t || 1)\n      return [\n        a.c[0] + (b.c[0] - a.c[0]) * f,\n        a.c[1] + (b.c[1] - a.c[1]) * f,\n        a.c[2] + (b.c[2] - a.c[2]) * f\n      ]\n    }\n  }\n  return TERRAIN_STOPS[TERRAIN_STOPS.length - 1].c\n}\n\n// ---------- app-card canvas drawings (logical 480x600 units) ----------\nfunction drawCalendarCard(ctx, w, h) {\n  ctx.clearRect(0, 0, w, h)\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.fillStyle = '#ffffff'\n  ctx.fill()\n  ctx.save()\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.clip()\n\n  ctx.fillStyle = '#1a1a1e'\n  ctx.font = '700 30px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.textAlign = 'left'\n  ctx.fillText('Calendar', 34, 62)\n  ctx.font = '500 18px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillStyle = '#8a8a90'\n  ctx.fillText('April 2016', 34, 90)\n\n  ctx.strokeStyle = '#eceded'\n  ctx.lineWidth = 2\n  ctx.beginPath()\n  ctx.moveTo(24, 110)\n  ctx.lineTo(w - 24, 110)\n  ctx.stroke()\n\n  const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S']\n  const gridX = 30, gridY = 140, cellW = (w - 60) / 7, cellH = 46\n  ctx.font = '600 15px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillStyle = '#b3b3b8'\n  ctx.textAlign = 'center'\n  days.forEach((d, i) => ctx.fillText(d, gridX + cellW * i + cellW / 2, gridY))\n\n  const firstDayCol = 5\n  const daysInMonth = 30\n  let col = firstDayCol, row = 0\n  ctx.font = '500 17px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  for (let day = 1; day <= daysInMonth; day++) {\n    const cx = gridX + cellW * col + cellW / 2\n    const cy = gridY + 40 + row * cellH\n    if (day === 6) {\n      ctx.beginPath()\n      ctx.arc(cx, cy, 17, 0, Math.PI * 2)\n      ctx.fillStyle = '#e0384a'\n      ctx.fill()\n      ctx.fillStyle = '#ffffff'\n    } else {\n      ctx.fillStyle = '#3a3a3e'\n    }\n    ctx.fillText(String(day), cx, cy + 5)\n    col++\n    if (col > 6) { col = 0; row++ }\n  }\n\n  const evY = gridY + 40 + (row + 1) * cellH + 20\n  ctx.textAlign = 'left'\n  ctx.font = '600 15px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillStyle = '#6a6a70'\n  ctx.fillText('TODAY', 34, evY)\n  const events = ['Team meeting', 'Lunch with team', 'Design review', 'Get groceries']\n  ctx.font = '500 16px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  events.forEach((e, i) => {\n    const y = evY + 34 + i * 34\n    ctx.beginPath()\n    ctx.arc(40, y - 5, 4, 0, Math.PI * 2)\n    ctx.fillStyle = '#e0384a'\n    ctx.fill()\n    ctx.fillStyle = '#2c2c30'\n    ctx.fillText(e, 56, y)\n  })\n\n  ctx.restore()\n}\n\nfunction drawWeatherCard(ctx, w, h) {\n  ctx.clearRect(0, 0, w, h)\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.fillStyle = '#1f3fe0'\n  ctx.fill()\n  ctx.save()\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.clip()\n  ctx.fillStyle = '#ffffff'\n  ctx.textAlign = 'center'\n  ctx.font = '300 150px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillText('46°', w / 2, h / 2 + 10)\n  ctx.font = '600 24px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.globalAlpha = 0.85\n  ctx.fillText('WEATHER', w / 2, h / 2 + 90)\n  ctx.globalAlpha = 1\n  ctx.restore()\n}\n\nfunction drawCalculatorCard(ctx, w, h) {\n  ctx.clearRect(0, 0, w, h)\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.fillStyle = '#e0384a'\n  ctx.fill()\n  ctx.save()\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.clip()\n\n  ctx.strokeStyle = 'rgba(255,255,255,0.85)'\n  ctx.lineWidth = 2\n  ctx.beginPath()\n  ctx.moveTo(30, 40)\n  ctx.lineTo(50, 40)\n  ctx.stroke()\n  ctx.strokeRect(70, 32, 16, 16)\n  ctx.beginPath()\n  ctx.moveTo(100, 32)\n  ctx.lineTo(116, 48)\n  ctx.moveTo(116, 32)\n  ctx.lineTo(100, 48)\n  ctx.stroke()\n\n  const dispX = 30, dispY = 64, dispW = w - 72, dispH = 90\n  roundRect(ctx, dispX, dispY, dispW, dispH, 14)\n  ctx.fillStyle = '#ffffff'\n  ctx.fill()\n  ctx.fillStyle = '#1a1a1e'\n  ctx.font = '600 52px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.textAlign = 'right'\n  ctx.fillText('0', dispX + dispW - 20, dispY + dispH - 26)\n\n  const keys = [\n    ['CE', 'C', '⌫', '÷'],\n    ['7', '8', '9', '×'],\n    ['4', '5', '6', '−'],\n    ['1', '2', '3', '+'],\n    ['0', '.', '=', '=']\n  ]\n  const padX = 30, padY = 180, padW = w - 60, gap = 10\n  const keyW = (padW - gap * 3) / 4\n  const keyH = 62\n  keys.forEach((rowArr, r) => {\n    rowArr.forEach((k, c) => {\n      if (r === 4 && c === 3) return\n      const kx = padX + c * (keyW + gap)\n      const ky = padY + r * (keyH + gap)\n      let kw = keyW\n      if (r === 4 && c === 2) kw = keyW * 2 + gap\n      roundRect(ctx, kx, ky, kw, keyH, 12)\n      const isOp = ['÷', '×', '−', '+', '='].includes(k)\n      ctx.fillStyle = isOp ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.18)'\n      ctx.fill()\n      ctx.fillStyle = isOp ? '#e0384a' : '#ffffff'\n      ctx.font = '600 24px -apple-system, \"Segoe UI\", Arial, sans-serif'\n      ctx.textAlign = 'center'\n      ctx.fillText(k, kx + kw / 2, ky + keyH / 2 + 8)\n    })\n  })\n\n  ctx.restore()\n}\n\nfunction drawWorldClockCard(ctx, w, h) {\n  ctx.clearRect(0, 0, w, h)\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.fillStyle = '#0d1420'\n  ctx.fill()\n  ctx.save()\n  roundRect(ctx, 6, 6, w - 12, h - 12, 28)\n  ctx.clip()\n\n  ctx.fillStyle = '#ffffff'\n  ctx.textAlign = 'left'\n  ctx.font = '600 22px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillText('London', 40, 90)\n  ctx.font = '300 64px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillText('14:32', 40, 170)\n\n  ctx.strokeStyle = 'rgba(255,255,255,0.12)'\n  ctx.lineWidth = 2\n  ctx.beginPath()\n  ctx.moveTo(40, 220)\n  ctx.lineTo(w - 40, 220)\n  ctx.stroke()\n\n  ctx.font = '600 22px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillStyle = '#ffffff'\n  ctx.fillText('Berlin', 40, 270)\n  ctx.font = '300 64px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillText('15:32', 40, 350)\n\n  ctx.font = '500 16px -apple-system, \"Segoe UI\", Arial, sans-serif'\n  ctx.fillStyle = 'rgba(255,255,255,0.55)'\n  ctx.fillText('1 hour ahead', 40, 390)\n\n  const cxp = w - 90, cyp = h - 90, r = 52\n  ctx.strokeStyle = 'rgba(255,255,255,0.4)'\n  ctx.lineWidth = 2\n  ctx.beginPath()\n  ctx.arc(cxp, cyp, r, 0, Math.PI * 2)\n  ctx.stroke()\n  for (let i = 0; i < 12; i++) {\n    const a = (i / 12) * Math.PI * 2\n    const x1 = cxp + Math.cos(a) * (r - 6), y1 = cyp + Math.sin(a) * (r - 6)\n    const x2 = cxp + Math.cos(a) * r, y2 = cyp + Math.sin(a) * r\n    ctx.beginPath()\n    ctx.moveTo(x1, y1)\n    ctx.lineTo(x2, y2)\n    ctx.stroke()\n  }\n  ctx.beginPath()\n  ctx.moveTo(cxp, cyp)\n  ctx.lineTo(cxp + Math.cos(-1.2) * r * 0.55, cyp + Math.sin(-1.2) * r * 0.55)\n  ctx.stroke()\n  ctx.beginPath()\n  ctx.moveTo(cxp, cyp)\n  ctx.lineTo(cxp + Math.cos(0.4) * r * 0.8, cyp + Math.sin(0.4) * r * 0.8)\n  ctx.stroke()\n\n  ctx.restore()\n}\n\n// ---------- floor widgets ----------\nfunction buildIslandWidget(THREE, x, z) {\n  const group = new THREE.Group()\n  group.position.set(x, 0, z)\n\n  const water = new THREE.Mesh(\n    new THREE.PlaneGeometry(3.3, 3.3),\n    new THREE.MeshPhysicalMaterial({ color: 0x2f8a95, roughness: 0.15, metalness: 0, transparent: true, opacity: 0.92 })\n  )\n  water.rotation.x = -Math.PI / 2\n  water.position.y = 0.02\n  water.receiveShadow = true\n  group.add(water)\n\n  const peak = 1.15\n  const radius = 1.05\n  const size = 2.1\n  const segs = 64\n  const geo = new THREE.PlaneGeometry(size, size, segs, segs)\n  const pos = geo.attributes.position\n  const colors = new Float32Array(pos.count * 3)\n  for (let i = 0; i < pos.count; i++) {\n    const vx = pos.getX(i)\n    const vy = pos.getY(i)\n    const hgt = terrainHeight(vx, vy, peak, radius)\n    pos.setZ(i, hgt)\n    const t = Math.max(0, Math.min(1, hgt / peak))\n    const c = heightColor(t)\n    colors[i * 3] = c[0]; colors[i * 3 + 1] = c[1]; colors[i * 3 + 2] = c[2]\n  }\n  geo.computeVertexNormals()\n  geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))\n  const terrain = new THREE.Mesh(\n    geo,\n    new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.88, metalness: 0 })\n  )\n  terrain.rotation.x = -Math.PI / 2\n  terrain.position.y = 0.05\n  terrain.castShadow = true\n  terrain.receiveShadow = true\n  group.add(terrain)\n\n  const treeSpots = [[0.38, -0.22], [-0.32, 0.18], [0.06, 0.44]]\n  treeSpots.forEach(([tx, tz]) => {\n    const th = terrainHeight(tx, tz, peak, radius) + 0.05\n    const trunk = new THREE.Mesh(\n      new THREE.CylinderGeometry(0.015, 0.02, 0.09, 5),\n      new THREE.MeshStandardMaterial({ color: 0x5a4028, roughness: 0.9 })\n    )\n    trunk.position.set(tx, th + 0.045, tz)\n    trunk.castShadow = true\n    trunk.receiveShadow = true\n    const foliage = new THREE.Mesh(\n      new THREE.ConeGeometry(0.075, 0.16, 6),\n      new THREE.MeshStandardMaterial({ color: 0x3f6b3a, roughness: 0.85 })\n    )\n    foliage.position.set(tx, th + 0.09 + 0.08, tz)\n    foliage.castShadow = true\n    foliage.receiveShadow = true\n    group.add(trunk, foliage)\n  })\n\n  const label = makeLabelSprite(THREE, 'Research Project')\n  label.position.set(0, 0.1, 1.45)\n  group.add(label)\n\n  return group\n}\n\nfunction buildGemWidget(THREE, x, z) {\n  const group = new THREE.Group()\n  group.position.set(x, 0, z)\n\n  const pedestal = new THREE.Mesh(\n    new THREE.CylinderGeometry(0.88, 0.98, 0.16, 28),\n    new THREE.MeshStandardMaterial({ color: 0xeceae4, roughness: 0.72, metalness: 0 })\n  )\n  pedestal.position.y = 0.08\n  pedestal.castShadow = true\n  pedestal.receiveShadow = true\n  group.add(pedestal)\n\n  const radius = 0.62\n  let geo = new THREE.IcosahedronGeometry(radius, 0)\n  geo = geo.toNonIndexed()\n  const posAttr = geo.attributes.position\n  const faceCount = posAttr.count / 3\n  const palette = [\n    [0.60, 0.34, 0.86], [0.42, 0.30, 0.80], [0.72, 0.46, 0.92],\n    [0.48, 0.55, 0.93], [0.78, 0.55, 0.95], [0.55, 0.40, 0.88]\n  ]\n  const colors = new Float32Array(posAttr.count * 3)\n  for (let f = 0; f < faceCount; f++) {\n    const c = palette[f % palette.length]\n    for (let v = 0; v < 3; v++) {\n      const idx = (f * 3 + v) * 3\n      colors[idx] = c[0]; colors[idx + 1] = c[1]; colors[idx + 2] = c[2]\n    }\n  }\n  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3))\n  geo.computeVertexNormals()\n  const gem = new THREE.Mesh(geo, new THREE.MeshPhysicalMaterial({\n    vertexColors: true, flatShading: true, roughness: 0.2,\n    clearcoat: 0.6, clearcoatRoughness: 0.15, metalness: 0.05\n  }))\n  gem.position.y = 0.16 + radius * 0.82\n  gem.rotation.y = 0.4\n  gem.castShadow = true\n  gem.receiveShadow = true\n  group.add(gem)\n  group.userData.gem = gem\n\n  const label = makeLabelSprite(THREE, 'Rewards')\n  label.position.set(0, 0.1, 1.15)\n  group.add(label)\n\n  return group\n}\n\nfunction buildPieWidget(THREE, x, z) {\n  const group = new THREE.Group()\n  group.position.set(x, 0, z)\n\n  const radius = 1.05\n  const slices = [\n    { deg: 120, height: 0.85, color: 0xf5f0e6 },\n    { deg: 90, height: 0.55, color: 0x6b7280 },\n    { deg: 90, height: 1.25, color: 0xe0384a },\n    { deg: 60, height: 0.35, color: 0x2b2b30 }\n  ]\n  let startAngle = -Math.PI / 2\n  slices.forEach(s => {\n    const angleRad = (s.deg * Math.PI) / 180\n    const endAngle = startAngle + angleRad\n    const shape = new THREE.Shape()\n    shape.moveTo(0, 0)\n    shape.absarc(0, 0, radius, startAngle, endAngle, false)\n    shape.lineTo(0, 0)\n    const geo = new THREE.ExtrudeGeometry(shape, { depth: s.height, bevelEnabled: false, curveSegments: 24 })\n    geo.rotateX(-Math.PI / 2)\n    geo.computeBoundingBox()\n    const minY = geo.boundingBox.min.y\n    geo.translate(0, -minY, 0)\n    geo.computeVertexNormals()\n    const mat = new THREE.MeshStandardMaterial({ color: s.color, roughness: 0.6, metalness: 0, flatShading: true })\n    const mesh = new THREE.Mesh(geo, mat)\n    mesh.castShadow = true\n    mesh.receiveShadow = true\n    group.add(mesh)\n    const edgeGeo = new THREE.EdgesGeometry(geo, 20)\n    const edgeMat = new THREE.LineBasicMaterial({ color: 0x1c1c1e, transparent: true, opacity: 0.35 })\n    group.add(new THREE.LineSegments(edgeGeo, edgeMat))\n    startAngle = endAngle\n  })\n\n  const label = makeLabelSprite(THREE, 'March 2016 Sales')\n  label.position.set(0, 0.12, 1.55)\n  group.add(label)\n\n  return group\n}\n\nfunction buildTaskCubesWidget(THREE, x, z) {\n  const group = new THREE.Group()\n  group.position.set(x, 0, z)\n\n  function cube(cx, cz, bodyColor, text) {\n    const size = 0.62\n    const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.68, metalness: 0 })\n    const tex = makeCanvasTexture(THREE, (ctx, w, h) => {\n      ctx.clearRect(0, 0, w, h)\n      ctx.fillStyle = '#ffffff'\n      ctx.textAlign = 'center'\n      ctx.textBaseline = 'middle'\n      ctx.font = '700 64px -apple-system, \"Segoe UI\", Arial, sans-serif'\n      ctx.fillText(text, w / 2, h / 2)\n    }, 256, 256)\n    const faceMat = new THREE.MeshStandardMaterial({ map: tex, transparent: true, roughness: 0.6, metalness: 0 })\n    const materials = [bodyMat, bodyMat, bodyMat, bodyMat, faceMat, bodyMat]\n    const mesh = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), materials)\n    mesh.position.set(cx, size / 2, cz)\n    mesh.castShadow = true\n    mesh.receiveShadow = true\n    mesh.userData.faceTexture = tex\n    return mesh\n  }\n\n  const c1 = cube(-0.42, 0, 0x17181c, '2/24')\n  const c2 = cube(0.32, 0.06, 0xe0384a, '4/19')\n  const c3 = cube(1.15, -0.25, 0x17181c, '')\n  group.add(c1, c2, c3)\n\n  const homeLabel = makeLabelSprite(THREE, 'Home remodel')\n  homeLabel.scale.set(1.3, 0.32, 1)\n  homeLabel.position.set(1.15, 0.85, -0.25)\n  group.add(homeLabel)\n\n  const label = makeLabelSprite(THREE, 'Tasks')\n  label.position.set(0.3, 0.12, 1.1)\n  group.add(label)\n\n  return { group, textures: [c1.userData.faceTexture, c2.userData.faceTexture, c3.userData.faceTexture] }\n}\n\nfunction initScene(THREE, d2, root, canvas) {\n  const scene = new THREE.Scene()\n\n  const camera = new THREE.PerspectiveCamera(36, 1, 0.1, 100)\n  const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })\n  renderer.setClearColor(0x000000, 0)\n  renderer.shadowMap.enabled = true\n  renderer.shadowMap.type = THREE.PCFSoftShadowMap\n  renderer.outputColorSpace = THREE.SRGBColorSpace\n  renderer.toneMapping = THREE.ACESFilmicToneMapping\n  renderer.toneMappingExposure = 1.1\n\n  scene.fog = new THREE.Fog(0xdfe1e3, 12, 30)\n\n  const hemi = new THREE.HemisphereLight(0xeef1f3, 0xb6b2a6, 0.55)\n  const key = new THREE.DirectionalLight(0xfff0d6, 2.4)\n  key.position.set(6.5, 12, 6)\n  key.castShadow = true\n  key.shadow.mapSize.set(2048, 2048)\n  key.shadow.bias = -0.0015\n  key.shadow.normalBias = 0.02\n  const rim = new THREE.DirectionalLight(0xdfe8ff, 0.35)\n  rim.position.set(-8, 6, -5)\n  scene.add(hemi, key, rim)\n\n  const floor = new THREE.Mesh(\n    new THREE.PlaneGeometry(48, 48),\n    new THREE.MeshStandardMaterial({ color: 0xd7d9dc, roughness: 0.92, metalness: 0 })\n  )\n  floor.rotation.x = -Math.PI / 2\n  floor.receiveShadow = true\n  scene.add(floor)\n\n  // sun + glow (self-lit, excepted from PBR shading per spec)\n  const sunGroup = new THREE.Group()\n  const sun = new THREE.Mesh(\n    new THREE.SphereGeometry(0.45, 24, 24),\n    new THREE.MeshBasicMaterial({ color: 0xfff3c4, toneMapped: false })\n  )\n  const glow = makeGlowSprite(THREE, 'rgba(255,247,214,0.9)', 'rgba(255,236,170,0.35)', 5.2)\n  sunGroup.add(glow, sun)\n  sunGroup.position.set(7, 10, -7)\n  scene.add(sunGroup)\n\n  const contentGroup = new THREE.Group()\n  scene.add(contentGroup)\n\n  const windowsGroup = new THREE.Group()\n  contentGroup.add(windowsGroup)\n  const windowDefs = [\n    { draw: drawCalendarCard, x: -3.3, y: 4.1, z: 1.0, rotY: 0.16 },\n    { draw: drawWeatherCard, x: -1.05, y: 4.7, z: -0.6, rotY: -0.06 },\n    { draw: drawCalculatorCard, x: 1.35, y: 4.35, z: 0.9, rotY: -0.2 },\n    { draw: drawWorldClockCard, x: 3.55, y: 3.75, z: -1.1, rotY: 0.11 }\n  ]\n  const windowTextures = []\n  const bevelMat = new THREE.MeshStandardMaterial({ color: 0xe4e3e6, roughness: 0.68, metalness: 0 })\n  windowDefs.forEach(def => {\n    const tex = makeCanvasTexture(THREE, def.draw, 480, 600, 2)\n    windowTextures.push(tex)\n    const frontMat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.55, metalness: 0 })\n    const depth = 0.22\n    const geo = new THREE.BoxGeometry(2.15, 2.6875, depth)\n    const mesh = new THREE.Mesh(geo, [bevelMat, bevelMat, bevelMat, bevelMat, frontMat, bevelMat])\n    mesh.position.set(def.x, def.y, def.z)\n    mesh.rotation.y = def.rotY\n    mesh.castShadow = true\n    mesh.receiveShadow = true\n    windowsGroup.add(mesh)\n  })\n\n  const widgetsGroup = new THREE.Group()\n  contentGroup.add(widgetsGroup)\n  const island = buildIslandWidget(THREE, -4.4, 4.0)\n  const gemWidget = buildGemWidget(THREE, -1.4, 4.7)\n  const pie = buildPieWidget(THREE, 1.5, 4.9)\n  const cubesResult = buildTaskCubesWidget(THREE, 4.4, 4.1)\n  widgetsGroup.add(island, gemWidget, pie, cubesResult.group)\n\n  // fit the key-light shadow frustum to the actual scene bounds\n  const bbox = new THREE.Box3().setFromObject(contentGroup)\n  const bcenter = bbox.getCenter(new THREE.Vector3())\n  const bsize = bbox.getSize(new THREE.Vector3())\n  const halfSpan = Math.max(bsize.x, bsize.z) / 2 + 1.5\n  key.target.position.copy(bcenter)\n  scene.add(key.target)\n  key.shadow.camera.near = 1\n  key.shadow.camera.far = halfSpan * 3 + 20\n  key.shadow.camera.left = -halfSpan\n  key.shadow.camera.right = halfSpan\n  key.shadow.camera.top = halfSpan\n  key.shadow.camera.bottom = -halfSpan\n  key.shadow.camera.updateProjectionMatrix()\n\n  function setAnisotropy() {\n    const maxAniso = renderer.capabilities.getMaxAnisotropy()\n    windowTextures.forEach(t => { t.anisotropy = maxAniso; t.needsUpdate = true })\n    cubesResult.textures.forEach(t => { if (t) { t.anisotropy = maxAniso; t.needsUpdate = true } })\n  }\n  setAnisotropy()\n\n  function resize() {\n    const w = canvas.clientWidth || 1\n    const h = canvas.clientHeight || 1\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))\n    renderer.setSize(w, h, false)\n    camera.aspect = w / h\n    camera.updateProjectionMatrix()\n  }\n  resize()\n  const ro = new ResizeObserver(resize)\n  ro.observe(canvas)\n\n  const mouse = { x: 0, y: 0 }\n  const mouseEased = { x: 0, y: 0 }\n  d2.on(window, 'mousemove', (e) => {\n    const rect = canvas.getBoundingClientRect()\n    mouse.x = Math.max(-1, Math.min(1, ((e.clientX - rect.left) / rect.width) * 2 - 1))\n    mouse.y = Math.max(-1, Math.min(1, ((e.clientY - rect.top) / rect.height) * 2 - 1))\n  })\n\n  const center = new THREE.Vector3(0, 1.75, 1.1)\n  const R = 12.5\n  const H = 6.4\n  let theta = 0\n\n  const clockEls = {\n    time: root.querySelector('.overlay .time'),\n    day: root.querySelector('.overlay .day'),\n    date: root.querySelector('.overlay .date')\n  }\n  let lastMinute = -1\n  function tickClock() {\n    const now = new Date()\n    if (now.getMinutes() === lastMinute) return\n    lastMinute = now.getMinutes()\n    const h = now.getHours()\n    const hh = ((h + 11) % 12) + 1\n    const mm = String(now.getMinutes()).padStart(2, '0')\n    const ampm = h >= 12 ? 'PM' : 'AM'\n    if (clockEls.time) clockEls.time.textContent = hh + ':' + mm + ' ' + ampm\n    const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']\n    if (clockEls.day) clockEls.day.textContent = dayNames[now.getDay()]\n    if (clockEls.date) clockEls.date.textContent = monthNames[now.getMonth()] + ' ' + now.getDate()\n  }\n  tickClock()\n\n  d2.raf(() => {\n    theta += 0.00075\n    mouseEased.x += (mouse.x - mouseEased.x) * 0.04\n    mouseEased.y += (mouse.y - mouseEased.y) * 0.04\n\n    camera.position.x = center.x + Math.sin(theta) * R + mouseEased.x * 1.1\n    camera.position.z = center.z + Math.cos(theta) * R\n    camera.position.y = H + mouseEased.y * 0.6\n    camera.lookAt(center.x + mouseEased.x * 0.3, center.y, center.z)\n\n    if (gemWidget.userData.gem) gemWidget.userData.gem.rotation.y += 0.004\n\n    renderer.render(scene, camera)\n    tickClock()\n  })\n\n  d2.onUnmount(() => {\n    ro.disconnect()\n    renderer.dispose()\n  })\n}\n\nexport default {\n  css: `\n    .stage { position:absolute; inset:0; width:100%; height:100%; overflow:hidden;\n      background: linear-gradient(180deg, #eef0f2 0%, #e6e8eb 50%, #d7d9dc 100%); }\n    canvas.scene-canvas { position:absolute; inset:0; width:100%; height:100%; display:block; }\n    .overlay { position:absolute; top:28px; left:36px; pointer-events:none;\n      font-family: -apple-system, \"Segoe UI\", Roboto, Arial, sans-serif; color:#141414; }\n    .overlay .time { font-size:56px; font-weight:800; line-height:1; letter-spacing:-1px; }\n    .overlay .day { font-size:20px; font-weight:700; opacity:.72; margin-top:8px; }\n    .overlay .date { font-size:20px; font-weight:700; opacity:.72; }\n    .loading { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;\n      font-family: -apple-system, sans-serif; color:#9a9a9e;\n      background: linear-gradient(180deg, #eef0f2 0%, #d7d9dc 100%); }\n  `,\n  render(d2) {\n    const THREE = d2.lib('three@0.160.0')\n    if (!THREE || d2.lib.state('three@0.160.0') !== 'ready') {\n      return `<div class=\"loading\">Loading scene…</div>`\n    }\n    return `<div class=\"stage\">\n      <canvas class=\"scene-canvas\" data-d2-static></canvas>\n      <div class=\"overlay\" data-d2-static>\n        <div class=\"time\">--:--</div>\n        <div class=\"day\">-</div>\n        <div class=\"date\">-</div>\n      </div>\n    </div>`\n  },\n  updated(d2, root) {\n    const THREE = d2.lib('three@0.160.0')\n    if (!THREE) return\n    const canvas = root.querySelector('canvas.scene-canvas')\n    if (!canvas || canvas.__inited) return\n    canvas.__inited = true\n    initScene(THREE, d2, root, canvas)\n  }\n}\n"}}}}}}}},{"_id":"6a73fbd2f08aa56d3e04cc5b","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"Mm2odErnOlW8b_N0I0TTd","package":"lucca-office","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    .hic-frame  { position:relative; width:100%; height:100%; box-sizing:border-box;\n                  border-radius:16px; overflow:hidden; }\n    .hic-canvas { display:block; width:100%; height:100%; }\n    /* natural-size so it can float + reflect; not stretched to fill */\n    .hic-content{ display:inline-block; box-sizing:border-box; }\n    /* fallback path (API off) — just show the content */\n    .hic-fallback{ display:block; width:100%; height:100%; box-sizing:border-box; }\n  `,\n  render(d2) {\n    const html = d2.param('html', '<p>No content</p>')\n    const supported = typeof CanvasRenderingContext2D !== 'undefined'\n      && typeof CanvasRenderingContext2D.prototype.drawElementImage === 'function'\n    if (!supported) {\n      return `<div class=\"hic-fallback\">${html}</div>`\n    }\n    return `<div class=\"hic-frame\"><canvas layoutsubtree class=\"hic-canvas\"><div class=\"hic-content\">${html}</div></canvas></div>`\n  },\n  mounted(d2) { wireCanvas(d2) },\n  updated(d2) {\n    wireCanvas(d2)\n    const canvas = d2.$('.hic-canvas')\n    if (canvas && canvas.requestPaint) canvas.requestPaint()\n  },\n}\n\nfunction wireCanvas(d2) {\n  const canvas = d2.$('.hic-canvas')\n  if (!canvas || canvas._hicWired) return\n  canvas._hicWired = true\n\n  const ctx = canvas.getContext('2d')\n  const content = d2.$('.hic-content')\n  const hue = Number(d2.param('accent', 265))\n\n  let raf = 0, W = 1, H = 1, dpr = 1\n  const parts = Array.from({ length: 40 }, (_, i) => ({\n    x: (i * 97) % 920 / 920, y: ((i * 173) % 520) / 520,\n    r: 0.6 + (i % 5) * 0.7, s: 0.15 + (i % 7) * 0.05, ph: i * 0.6,\n  }))\n\n  const size = () => {\n    const r = canvas.getBoundingClientRect()\n    W = Math.max(1, r.width); H = Math.max(1, r.height)\n    dpr = Math.max(1, window.devicePixelRatio || 1)\n    canvas.width = Math.round(W * dpr)      // DPR-scaled backing store = crisp on retina\n    canvas.height = Math.round(H * dpr)\n  }\n\n  const aurora = (t) => {\n    const g = ctx.createLinearGradient(0, 0, W, H)\n    const s = Math.sin(t / 4000) * 40\n    g.addColorStop(0, `hsl(${hue} 70% 12%)`)\n    g.addColorStop(0.5, `hsl(${hue + s + 40} 60% 9%)`)\n    g.addColorStop(1, `hsl(${hue + 80} 65% 8%)`)\n    ctx.fillStyle = g; ctx.fillRect(0, 0, W, H)\n    for (let k = 0; k < 2; k++) {\n      const cx = W * (0.3 + 0.4 * k) + Math.sin(t / 2600 + k * 2) * 90\n      const cy = H * (0.35 + 0.3 * k) + Math.cos(t / 3100 + k) * 70\n      const rad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 240)\n      rad.addColorStop(0, `hsl(${hue + k * 90} 85% 60% / .28)`)\n      rad.addColorStop(1, 'transparent')\n      ctx.fillStyle = rad; ctx.fillRect(0, 0, W, H)\n    }\n  }\n\n  const particles = (t) => {\n    ctx.fillStyle = `hsl(${hue} 90% 85% / .5)`\n    for (const p of parts) {\n      let py = (p.y - (t / 1000) * p.s * 0.05) % 1; if (py < 0) py += 1\n      const xx = p.x * W + Math.sin(t / 1500 + p.ph) * 10\n      ctx.globalAlpha = (0.4 + 0.6 * Math.abs(Math.sin(t / 900 + p.ph))) * 0.6\n      ctx.beginPath(); ctx.arc(xx, py * H, p.r, 0, Math.PI * 2); ctx.fill()\n    }\n    ctx.globalAlpha = 1\n  }\n\n  const frame = (t) => {\n    ctx.reset(); ctx.scale(dpr, dpr)\n    aurora(t); particles(t)\n\n    const cw = content.offsetWidth || W, ch = content.offsetHeight || H\n    const cx = (W - cw) / 2\n    const cy = (H - ch) / 2 + Math.sin(t / 1000) * 6\n\n    // reflection first (non-interactive copy)\n    ctx.save()\n    ctx.globalAlpha = 0.16\n    ctx.translate(0, cy * 2 + ch + 22); ctx.scale(1, -1)\n    try { ctx.drawElementImage(content, cx, -cy - ch) } catch (e) {}\n    ctx.restore()\n\n    // primary draw — feed the returned matrix back so clicks/focus line up\n    try {\n      const m = ctx.drawElementImage(content, cx, cy)\n      if (m && m.toString) content.style.transform = m.toString()\n    } catch (e) {}\n\n    raf = requestAnimationFrame(frame)\n  }\n\n  size()\n  const ro = new ResizeObserver(() => size())\n  ro.observe(canvas)\n  raf = requestAnimationFrame(frame)\n\n  d2.onUnmount(() => { cancelAnimationFrame(raf); ro.disconnect() })\n}"}},"html":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"html"}}}}}}}}}},{"_id":"6a73feb5d5a555faccfd9eb6","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"TeXEHCJDi5uGSHcKKr56N","package":"lucca-office","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: `.plain-html-content { display:block; width:100%; height:100%; box-sizing:border-box; }`,\n  render(d2) {\n    const html = d2.param('html', '<p>No content</p>')\n    return `<div class=\"plain-html-content\">${html}</div>`\n  },\n}\n"}},"html":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"html"}}}}}}}}}},{"_id":"6a76491f505962a50a29efb1","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"_EH47qLRPDw222FnDDcNM","package":"lucca-office","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 MAPLIBRE_JS = { url: 'https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js', global: 'maplibregl' };\nconst MAPLIBRE_CSS = { css: 'https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css' };\nconst WORLD_SPAN_DEG = 4;\n\nfunction esc(s) {\n  return (s == null ? '' : String(s)).replace(/[&<>\"']/g, function (c) {\n    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n  });\n}\n\nexport default {\n  css: `\n    :host{display:block;width:100%;height:100%;box-sizing:border-box}\n    *{box-sizing:border-box}\n    .app{position:relative;width:100%;height:100%;display:flex;flex-direction:column;\n      font:13px -apple-system,BlinkMacSystemFont,\"SF Pro Text\",Inter,sans-serif;\n      background:#111214;border-radius:14px;overflow:hidden;\n      box-shadow:0 10px 30px rgba(0,0,0,.28);color:#1d1d1f}\n\n    .topbar{display:flex;align-items:center;gap:12px;padding:10px 14px;\n      background:rgba(28,28,30,.92);-webkit-backdrop-filter:blur(20px) saturate(160%);\n      backdrop-filter:blur(20px) saturate(160%);border-bottom:1px solid rgba(255,255,255,.08);\n      z-index:20;position:relative}\n    .tabs{display:flex;gap:6px;flex-wrap:wrap}\n    .tab{appearance:none;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);\n      color:rgba(255,255,255,.75);padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;\n      cursor:pointer;transition:background .12s ease,color .12s ease}\n    .tab:hover{background:rgba(255,255,255,.12)}\n    .tab.active{background:#f5b642;color:#1d1d1f;border-color:transparent}\n\n    .search{position:relative;flex:1;max-width:280px}\n    .search input{width:100%;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);\n      color:#fff;border-radius:8px;padding:7px 10px;font-size:12px;outline:none}\n    .search input::placeholder{color:rgba(255,255,255,.4)}\n    .search input:focus{border-color:#f5b642}\n    .searchResults{position:absolute;top:calc(100% + 6px);left:0;right:0;background:rgba(28,28,30,.98);\n      border:1px solid rgba(255,255,255,.1);border-radius:10px;overflow:hidden;display:none;\n      box-shadow:0 12px 28px rgba(0,0,0,.4);max-height:260px;overflow-y:auto;z-index:30}\n    .searchResults.show{display:block}\n    .resultRow{display:flex;align-items:center;gap:8px;padding:8px 10px;color:rgba(255,255,255,.85);\n      cursor:pointer;font-size:12px;border-bottom:1px solid rgba(255,255,255,.06)}\n    .resultRow:last-child{border-bottom:none}\n    .resultRow:hover{background:rgba(255,255,255,.08)}\n    .resultRegion{margin-left:auto;color:rgba(255,255,255,.4);font-size:11px}\n\n    .fsBtn{appearance:none;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);\n      color:rgba(255,255,255,.8);width:32px;height:32px;border-radius:8px;cursor:pointer;font-size:15px;\n      display:flex;align-items:center;justify-content:center;flex:0 0 auto}\n    .fsBtn:hover{background:rgba(255,255,255,.14)}\n\n    .stage{position:relative;flex:1;min-height:0}\n    #mapEl{position:absolute;inset:0;background:#000;z-index:1;font-family:inherit}\n    #mapEl .maplibregl-ctrl-attrib,#mapEl .maplibregl-ctrl-logo{display:none}\n    #mapEl .maplibregl-popup-content{background:#fff;padding:10px 12px;border-radius:10px;\n      box-shadow:0 8px 20px rgba(0,0,0,.35)}\n    #mapEl .maplibregl-popup-tip{border-top-color:#fff}\n    #mapEl .maplibregl-popup-anchor-bottom .maplibregl-popup-tip{border-top-color:#fff}\n    #mapEl .maplibregl-popup-close-button{font-size:16px;color:#999;padding:2px 6px}\n\n    .sidebar{position:absolute;top:14px;right:14px;width:200px;max-height:calc(100% - 28px);\n      overflow-y:auto;background:#1c1c1e;border:1px solid rgba(255,255,255,.12);border-radius:12px;\n      padding:10px;z-index:15;box-shadow:0 8px 24px rgba(0,0,0,.45)}\n    .sidebarHeader{display:flex;align-items:center;justify-content:space-between;color:rgba(255,255,255,.55);\n      font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}\n    .reset{appearance:none;border:none;background:none;color:#f5b642;font-size:11px;cursor:pointer;\n      font-weight:600}\n    .catList{display:flex;flex-direction:column;gap:6px}\n    .catRow{display:flex;align-items:center;gap:7px;color:rgba(255,255,255,.85);font-size:12px;\n      cursor:pointer;padding:3px 2px;border-radius:6px}\n    .catRow:hover{background:rgba(255,255,255,.06)}\n    .catRow input{accent-color:#f5b642}\n    .dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;box-shadow:0 0 0 1px rgba(0,0,0,.3)}\n    .catName{flex:1}\n    .catCount{color:rgba(255,255,255,.35);font-size:11px}\n\n    .coords{position:absolute;left:12px;bottom:10px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.75);\n      font-size:11px;padding:4px 8px;border-radius:6px;z-index:15;pointer-events:none;\n      font-family:ui-monospace,Menlo,monospace}\n\n    .boot{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\n      background:#1c1c1e;color:rgba(255,255,255,.6);font:13px -apple-system,sans-serif;z-index:25;\n      text-align:center;padding:24px;line-height:1.5}\n    .boot.err{color:#ff8a80}\n    .boot code{background:rgba(255,255,255,.1);padding:2px 5px;border-radius:4px}\n\n    .pin{background:transparent;border:none}\n    .pinBadge{width:26px;height:26px;border-radius:50% 50% 50% 4px;transform:rotate(-45deg);\n      display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.5),\n      inset 0 0 0 2px rgba(255,255,255,.5);font-size:12px;font-weight:800;color:#111;\n      font-family:-apple-system,sans-serif;overflow:hidden}\n    .pinBadge span{transform:rotate(45deg);display:block}\n    .pinBadge img{width:16px;height:16px;object-fit:contain;transform:rotate(45deg)}\n\n    .pop{min-width:200px;max-width:240px;font:12px -apple-system,sans-serif;color:#1d1d1f}\n    .popHead{display:flex;align-items:center;gap:8px;border-bottom:2px solid #ccc;padding-bottom:6px;margin-bottom:6px}\n    .popGlyph{width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;\n      font-size:12px;font-weight:800;color:#111;flex:0 0 auto;overflow:hidden}\n    .popGlyph img{width:16px;height:16px;object-fit:contain}\n    .popTitle{font-weight:700;font-size:13px;line-height:1.2}\n    .popCat{color:#888;font-size:10px;text-transform:uppercase;letter-spacing:.04em}\n    .popDesc{color:#333;line-height:1.4}\n    .popCoords{margin-top:6px;color:#999;font-size:10px;font-family:ui-monospace,Menlo,monospace}\n  `,\n  render(d2) {\n    const regions = d2.param('regions', []) || [];\n    const gl = d2.lib(MAPLIBRE_JS);\n    d2.lib(MAPLIBRE_CSS);\n\n    let bootHtml = '';\n    if (!gl) {\n      const st = d2.lib.state(MAPLIBRE_JS);\n      bootHtml = st === 'error'\n        ? '<div class=\"boot err\">Failed to load MapLibre GL: ' + esc(d2.lib.error(MAPLIBRE_JS) || 'unknown error') + '</div>'\n        : '<div class=\"boot\">Loading map…</div>';\n    } else if (!regions.length) {\n      bootHtml = '<div class=\"boot\">No map data supplied.<br/>Pass <code>regions</code>, <code>categories</code> and <code>markers</code> params to this component instance.</div>';\n    }\n\n    const tabs = regions.map(function (r) {\n      return '<button class=\"tab\" data-region=\"' + esc(r.id) + '\">' + esc(r.name || r.id) + '</button>';\n    }).join('');\n\n    return (\n      '<div class=\"app\">' +\n      '<div class=\"topbar\">' +\n      '<div class=\"tabs\">' + tabs + '</div>' +\n      '<div class=\"search\"><input id=\"search\" type=\"text\" placeholder=\"Search the map…\" autocomplete=\"off\"/>' +\n      '<div id=\"searchResults\" class=\"searchResults\"></div></div>' +\n      '<button id=\"fsBtn\" class=\"fsBtn\" title=\"Toggle fullscreen\">&#10021;</button>' +\n      '</div>' +\n      '<div class=\"stage\">' +\n      '<div id=\"mapEl\" class=\"mapEl\" data-d2-static></div>' +\n      '<div class=\"sidebar\">' +\n      '<div class=\"sidebarHeader\">Categories<button id=\"resetFilters\" class=\"reset\">Reset</button></div>' +\n      '<div id=\"catList\" class=\"catList\"></div>' +\n      '</div>' +\n      '<div id=\"coords\" class=\"coords\">—, —</div>' +\n      bootHtml +\n      '</div>' +\n      '</div>'\n    );\n  },\n  mounted(d2, root) {\n    tryInit(d2, root);\n  },\n  updated(d2, root) {\n    tryInit(d2, root);\n  }\n};\n\n// A game map is flat pixel space, not real geography. MapLibre only projects real\n// mercator lng/lat, so every region is squeezed into a tiny patch of degrees centered\n// on the equator/prime meridian — small enough (WORLD_SPAN_DEG) that mercator distortion\n// is imperceptible, which makes the projection behave like a plain Cartesian CRS.\nfunction regionMetrics(region) {\n  const w = region.width || 1000;\n  const h = region.height || 700;\n  const scale = WORLD_SPAN_DEG / Math.max(w, h);\n  return { w: w, h: h, scale: scale };\n}\n\nfunction pxToLngLat(region, px, py) {\n  const m = regionMetrics(region);\n  return [(px - m.w / 2) * m.scale, (m.h / 2 - py) * m.scale];\n}\n\nfunction regionCorners(region) {\n  const m = regionMetrics(region);\n  return {\n    tl: pxToLngLat(region, 0, 0),\n    tr: pxToLngLat(region, m.w, 0),\n    br: pxToLngLat(region, m.w, m.h),\n    bl: pxToLngLat(region, 0, m.h)\n  };\n}\n\nfunction regionBoundsLngLat(region, pad) {\n  const m = regionMetrics(region);\n  const p = pad || 0;\n  const nw = pxToLngLat(region, -m.w * p, -m.h * p);\n  const se = pxToLngLat(region, m.w * (1 + p), m.h * (1 + p));\n  return { sw: [nw[0], se[1]], ne: [se[0], nw[1]] };\n}\n\nfunction tryInit(d2, root) {\n  const mapEl = root.querySelector('#mapEl');\n  if (!mapEl) return;\n  const gl = d2.lib(MAPLIBRE_JS);\n  if (!gl) return;\n\n  const regions = d2.param('regions', []) || [];\n  const categories = d2.param('categories', []) || [];\n  const markers = d2.param('markers', []) || [];\n  if (!regions.length) return;\n\n  const sig = JSON.stringify([regions, categories, markers]);\n  if (mapEl.getAttribute('data-sig') === sig) return;\n  mapEl.setAttribute('data-sig', sig);\n  if (mapEl._d2map) {\n    mapEl._d2map.remove();\n    mapEl._d2map = null;\n  }\n\n  const state = {\n    ready: false,\n    regionId: regions[0].id,\n    activeCats: new Set(categories.map(function (c) { return c.id; })),\n    catMarkers: {},\n    markerRefs: {},\n    regionBaseZoom: {}\n  };\n\n  const map = new gl.Map({\n    container: mapEl,\n    style: { version: 8, sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#000' } }] },\n    center: [0, 0],\n    zoom: 0,\n    attributionControl: false,\n    dragRotate: false,\n    pitchWithRotate: false,\n    touchPitch: false,\n    maxPitch: 0,\n    minZoom: 0,\n    maxZoom: 22\n  });\n  mapEl._d2map = map;\n\n  const catList = root.querySelector('#catList');\n  catList.innerHTML = categories.map(function (c) {\n    return (\n      '<label class=\"catRow\" data-cat=\"' + esc(c.id) + '\">' +\n      '<input type=\"checkbox\" checked/>' +\n      '<span class=\"dot\" style=\"background:' + esc(c.color || '#888') + '\"></span>' +\n      '<span class=\"catName\">' + esc(c.name || c.id) + '</span>' +\n      '<span class=\"catCount\" id=\"cnt-' + esc(c.id) + '\"></span>' +\n      '</label>'\n    );\n  }).join('');\n\n  function catFor(id) {\n    for (let i = 0; i < categories.length; i++) if (categories[i].id === id) return categories[i];\n    return { id: id, name: id, color: '#888' };\n  }\n\n  function badgeInner(cat) {\n    return cat.iconUrl\n      ? '<img src=\"' + esc(cat.iconUrl) + '\" alt=\"\"/>'\n      : '<span>' + esc((cat.glyph || (cat.name || cat.id || '?').charAt(0).toUpperCase())) + '</span>';\n  }\n\n  function buildPinEl(cat) {\n    const el = document.createElement('div');\n    el.className = 'pin';\n    el.innerHTML = '<div class=\"pinBadge\" style=\"background:' + esc(cat.color || '#888') + '\">' + badgeInner(cat) + '</div>';\n    return el;\n  }\n\n  function popupHtml(m, cat) {\n    return (\n      '<div class=\"pop\">' +\n      '<div class=\"popHead\">' +\n      '<span class=\"popGlyph\" style=\"background:' + esc(cat.color || '#888') + '\">' + badgeInner(cat) + '</span>' +\n      '<div><div class=\"popTitle\">' + esc(m.name) + '</div><div class=\"popCat\">' + esc(cat.name || cat.id) + '</div></div>' +\n      '</div>' +\n      (m.description ? '<div class=\"popDesc\">' + esc(m.description) + '</div>' : '') +\n      '<div class=\"popCoords\">x ' + Number(m.x).toFixed(1) + ' · y ' + Number(m.y).toFixed(1) + '</div>' +\n      '</div>'\n    );\n  }\n\n  function clearMarkers() {\n    Object.keys(state.markerRefs).forEach(function (id) { state.markerRefs[id].remove(); });\n    state.markerRefs = {};\n    state.catMarkers = {};\n  }\n\n  function addMarker(m, region) {\n    const cat = catFor(m.category);\n    const px = (Number(m.x) / 100) * (region.width || 1000);\n    const py = (Number(m.y) / 100) * (region.height || 700);\n    const ll = pxToLngLat(region, px, py);\n    const marker = new gl.Marker({ element: buildPinEl(cat), anchor: 'bottom' })\n      .setLngLat(ll)\n      .setPopup(new gl.Popup({ offset: 20, closeButton: true }).setHTML(popupHtml(m, cat)));\n    marker._added = false;\n    state.markerRefs[m.id] = marker;\n    if (!state.catMarkers[cat.id]) state.catMarkers[cat.id] = [];\n    state.catMarkers[cat.id].push(marker);\n    if (state.activeCats.has(cat.id)) {\n      marker.addTo(map);\n      marker._added = true;\n    }\n  }\n\n  function setCatVisible(catId, visible) {\n    const arr = state.catMarkers[catId] || [];\n    arr.forEach(function (mk) {\n      if (visible && !mk._added) { mk.addTo(map); mk._added = true; }\n      else if (!visible && mk._added) { mk.remove(); mk._added = false; }\n    });\n  }\n\n  function loadRegion(regionId) {\n    if (!state.ready) return;\n    const region = regions.filter(function (r) { return r.id === regionId; })[0] || regions[0];\n    state.regionId = region.id;\n\n    clearMarkers();\n    if (map.getLayer('region-img')) map.removeLayer('region-img');\n    if (map.getSource('region-img')) map.removeSource('region-img');\n\n    if (region.imageUrl) {\n      const corners = regionCorners(region);\n      map.addSource('region-img', {\n        type: 'image',\n        url: region.imageUrl,\n        coordinates: [corners.tl, corners.tr, corners.br, corners.bl]\n      });\n      map.addLayer({ id: 'region-img', type: 'raster', source: 'region-img', paint: { 'raster-resampling': 'nearest' } });\n    }\n\n    const padded = regionBoundsLngLat(region, 0.25);\n    map.setMaxBounds([padded.sw, padded.ne]);\n    const fit = regionBoundsLngLat(region, 0);\n    map.fitBounds([fit.sw, fit.ne], { animate: false, padding: 20 });\n    state.regionBaseZoom[region.id] = map.getZoom();\n\n    categories.forEach(function (cat) { state.catMarkers[cat.id] = []; });\n\n    const regionMarkers = markers.filter(function (m) { return m.region === region.id; });\n    regionMarkers.forEach(function (m) { addMarker(m, region); });\n\n    categories.forEach(function (cat) {\n      const cntEl = root.querySelector('#cnt-' + cat.id);\n      if (cntEl) cntEl.textContent = String(regionMarkers.filter(function (m) { return m.category === cat.id; }).length);\n    });\n\n    Array.prototype.forEach.call(root.querySelectorAll('.tab'), function (btn) {\n      btn.classList.toggle('active', btn.getAttribute('data-region') === region.id);\n    });\n  }\n\n  function renderSearchResults(q) {\n    const searchResultsEl = root.querySelector('#searchResults');\n    q = q.trim().toLowerCase();\n    if (!q) {\n      searchResultsEl.innerHTML = '';\n      searchResultsEl.classList.remove('show');\n      return;\n    }\n    const matches = markers.filter(function (m) {\n      return (m.name || '').toLowerCase().indexOf(q) !== -1;\n    }).slice(0, 8);\n    searchResultsEl.innerHTML = matches.map(function (m) {\n      const cat = catFor(m.category);\n      const region = regions.filter(function (r) { return r.id === m.region; })[0] || { name: m.region };\n      return (\n        '<div class=\"resultRow\" data-id=\"' + esc(m.id) + '\">' +\n        '<span class=\"dot\" style=\"background:' + esc(cat.color || '#888') + '\"></span>' +\n        esc(m.name) +\n        '<span class=\"resultRegion\">' + esc(region.name || region.id) + '</span>' +\n        '</div>'\n      );\n    }).join('');\n    searchResultsEl.classList.toggle('show', matches.length > 0);\n  }\n\n  function flyToMarker(id) {\n    const m = markers.filter(function (x) { return x.id === id; })[0];\n    if (!m) return;\n    if (m.region !== state.regionId) loadRegion(m.region);\n    const region = regions.filter(function (r) { return r.id === m.region; })[0];\n    if (!region) return;\n    const px = (Number(m.x) / 100) * (region.width || 1000);\n    const py = (Number(m.y) / 100) * (region.height || 700);\n    const ll = pxToLngLat(region, px, py);\n\n    if (!state.activeCats.has(m.category)) {\n      state.activeCats.add(m.category);\n      const cb = catList.querySelector('.catRow[data-cat=\"' + m.category + '\"] input');\n      if (cb) cb.checked = true;\n      setCatVisible(m.category, true);\n    }\n\n    const baseZoom = state.regionBaseZoom[state.regionId] || map.getZoom();\n    map.flyTo({ center: ll, zoom: Math.max(map.getZoom(), baseZoom + 2), essential: true });\n    const marker = state.markerRefs[m.id];\n    if (marker) {\n      const popup = marker.getPopup();\n      if (popup && !popup.isOpen()) marker.togglePopup();\n    }\n\n    const searchResultsEl = root.querySelector('#searchResults');\n    const searchInput = root.querySelector('#search');\n    searchResultsEl.classList.remove('show');\n    searchInput.value = '';\n  }\n\n  function toggleFullscreen() {\n    const appEl = root.querySelector('.app');\n    if (!document.fullscreenElement) {\n      const req = appEl.requestFullscreen && appEl.requestFullscreen();\n      if (req && req.catch) req.catch(function () { appEl.classList.add('fsFallback'); });\n    } else if (document.exitFullscreen) {\n      document.exitFullscreen();\n    }\n  }\n\n  d2.on(catList, 'change', function (e) {\n    const row = e.target.closest('.catRow');\n    if (!row) return;\n    const catId = row.getAttribute('data-cat');\n    if (e.target.checked) {\n      state.activeCats.add(catId);\n      setCatVisible(catId, true);\n    } else {\n      state.activeCats.delete(catId);\n      setCatVisible(catId, false);\n    }\n  });\n\n  d2.on(root, 'click', function (e) {\n    const tab = e.target.closest('.tab');\n    if (tab) {\n      loadRegion(tab.getAttribute('data-region'));\n      renderSearchResults('');\n      root.querySelector('#search').value = '';\n      return;\n    }\n    if (e.target.id === 'resetFilters') {\n      state.activeCats = new Set(categories.map(function (c) { return c.id; }));\n      Array.prototype.forEach.call(catList.querySelectorAll('input[type=checkbox]'), function (cb) { cb.checked = true; });\n      categories.forEach(function (cat) { setCatVisible(cat.id, true); });\n      return;\n    }\n    if (e.target.id === 'fsBtn') {\n      toggleFullscreen();\n      return;\n    }\n    const resultRow = e.target.closest('.resultRow');\n    if (resultRow) {\n      flyToMarker(resultRow.getAttribute('data-id'));\n      return;\n    }\n  });\n\n  d2.on(root.querySelector('#search'), 'input', function (e) {\n    renderSearchResults(e.target.value);\n  });\n\n  map.on('mousemove', function (e) {\n    const c = root.querySelector('#coords');\n    if (!c) return;\n    const region = regions.filter(function (r) { return r.id === state.regionId; })[0];\n    if (!region) return;\n    const m = regionMetrics(region);\n    const px = e.lngLat.lng / m.scale + m.w / 2;\n    const py = m.h / 2 - e.lngLat.lat / m.scale;\n    c.textContent = px.toFixed(0) + ', ' + py.toFixed(0);\n  });\n\n  map.on('load', function () {\n    state.ready = true;\n    loadRegion(state.regionId);\n    setTimeout(function () { map.resize(); }, 60);\n  });\n}\n"}},"regions":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"regions"}}}},"categories":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"categories"}}}},"markers":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"markers"}}}}}}}}}},{"_id":"6a7a7d1868641501c2dbc6b7","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"InspectorMacOS","package":"lucca-office","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Inspector · macOS — schema-driven inspector tree (Data2 code component)\nconst ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }\nconst esc = v => String(v == null ? '' : v).replace(/[&<>\"']/g, c => ESC[c])\n\nconst I = {\n  chev: '<svg viewBox=\"0 0 8 12\" class=\"ic\"><path d=\"M2.2 1.4 6.4 6l-4.2 4.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  search: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"6\" cy=\"6\" r=\"4.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M9.3 9.3 12.4 12.4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  pin: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M8.6 1.6 12.4 5.4 10.6 6.2 9.6 9.4 4.6 4.4 7.8 3.4z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M4.6 9.4 1.8 12.2\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  copy: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"2.2\" y=\"2.2\" width=\"7\" height=\"7\" rx=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M4.8 11.8h5a2 2 0 0 0 2-2v-5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  undo: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3 6.2a4.4 4.4 0 1 1 .3 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M2.4 2.6v3.6H6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  link: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M5.8 8.2a2.6 2.6 0 0 0 3.7 0l1.7-1.7a2.6 2.6 0 0 0-3.7-3.7l-.9.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><path d=\"M8.2 5.8a2.6 2.6 0 0 0-3.7 0L2.8 7.5a2.6 2.6 0 0 0 3.7 3.7l.9-.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  plus: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M7 3v8M3 7h8\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  lock: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"3\" y=\"6.2\" width=\"8\" height=\"5.6\" rx=\"1.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M4.9 6.2V4.8a2.1 2.1 0 0 1 4.2 0v1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  trash: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3.2 4.2h7.6M5.6 4.2V3a.8.8 0 0 1 .8-.8h1.2a.8.8 0 0 1 .8.8v1.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><path d=\"M4.3 4.2l.5 6.6a1 1 0 0 0 1 .9h2.4a1 1 0 0 0 1-.9l.5-6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  dup: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"1.8\" y=\"1.8\" width=\"7\" height=\"7\" rx=\"1.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"5.2\" y=\"5.2\" width=\"7\" height=\"7\" rx=\"1.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  code: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M5 3.4 1.8 7 5 10.6M9 3.4 12.2 7 9 10.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  save: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M2.4 3.6a1.2 1.2 0 0 1 1.2-1.2h5.6l2.4 2.4v5.6a1.2 1.2 0 0 1-1.2 1.2H3.6a1.2 1.2 0 0 1-1.2-1.2z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M4.8 2.4v3h4.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  cube: '<svg viewBox=\"0 0 16 16\" class=\"ic\"><path d=\"M8 1.8 14 5v6l-6 3.2L2 11V5z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M2 5l6 3.2L14 5M8 8.2v6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.1\" opacity=\".55\"/></svg>'\n}\n\nconst TABS = [\n  { id: 'all', label: 'All' },\n  { id: 'style', label: 'Style' },\n  { id: 'content', label: 'Content' },\n  { id: 'logic', label: 'Logic' },\n  { id: 'used', label: 'Used' }\n]\n\nconst DEFAULT_SCHEMA = [\n  {\n    id: 'block', title: 'Block', tab: 'style', open: true, rows: [\n      {\n        kind: 'group', id: 'border', label: 'Border', open: true, rows: [\n          { kind: 'color', key: 'border.color', label: 'Color', value: '#0A84FF' },\n          { kind: 'number', key: 'border.width', label: 'Width', value: 1, unit: 'px' },\n          { kind: 'select', key: 'border.style', label: 'Style', value: 'Solid', options: ['None', 'Solid', 'Dashed', 'Dotted'] },\n          { kind: 'corners', key: 'border.radius', label: 'Radius', value: [10, 10, 10, 10] }\n        ]\n      },\n      { kind: 'toggle', key: 'childrenOverflow', label: 'Children overflow', value: false },\n      { kind: 'toggle', key: 'hide', label: 'Hide', value: false },\n      { kind: 'slider', key: 'opacity', label: 'Opacity', value: 100, min: 0, max: 100, unit: '%' }\n    ]\n  },\n  {\n    id: 'position', title: 'Position', tab: 'style', open: true, rows: [\n      { kind: 'segmented', key: 'position.mode', label: 'Mode', value: 'Free transform', options: ['Free transform', 'Fixed'] },\n      { kind: 'anchor', key: 'position.anchor', label: 'Anchors', value: { top: true, left: true, right: false, bottom: false } },\n      { kind: 'pair', label: 'Offset', items: [{ key: 'position.x', label: 'X', value: 1135, unit: 'px' }, { key: 'position.y', label: 'Y', value: 51, unit: 'px' }] }\n    ]\n  },\n  {\n    id: 'dimensions', title: 'Dimensions', tab: 'style', open: true, rows: [\n      { kind: 'pair', label: 'Size', items: [{ key: 'width', label: 'W', value: 160, unit: 'px' }, { key: 'height', label: 'H', value: 540, unit: 'px' }] },\n      { kind: 'toggle', key: 'growW', label: 'Fit children width', hint: 'Width adapts to children', value: false },\n      { kind: 'toggle', key: 'growH', label: 'Fit children height', hint: 'Height adapts to children', value: false }\n    ]\n  },\n  {\n    id: 'fill', title: 'Fill', tab: 'style', open: false, rows: [\n      { kind: 'color', key: 'fill.color', label: 'Color', value: '#1D1D1F' },\n      { kind: 'select', key: 'fill.blend', label: 'Blend', value: 'Normal', options: ['Normal', 'Multiply', 'Screen', 'Overlay'] }\n    ]\n  },\n  {\n    id: 'text', title: 'Text', tab: 'content', open: true, rows: [\n      { kind: 'text', key: 'text.value', label: 'Content', value: '', placeholder: 'Type text…' },\n      { kind: 'select', key: 'text.font', label: 'Font', value: 'SF Pro Text', options: ['SF Pro Text', 'Inter', 'Helvetica Neue', 'Menlo'] },\n      { kind: 'pair', label: 'Metrics', items: [{ key: 'text.size', label: 'S', value: 13, unit: 'px' }, { key: 'text.lh', label: 'LH', value: 18, unit: 'px' }] }\n    ]\n  },\n  {\n    id: 'logic', title: 'Interactions', tab: 'logic', open: true, rows: [\n      { kind: 'select', key: 'onClick', label: 'On click', value: 'None', options: ['None', 'Set value', 'Go to route', 'Run service'] },\n      { kind: 'toggle', key: 'disabled', label: 'Disabled', value: false },\n      { kind: 'note', text: 'Handlers run in the order they are listed.' }\n    ]\n  },\n  {\n    id: 'used', title: 'Used by', tab: 'used', open: true, rows: [\n      { kind: 'note', text: 'Referenced by 3 artboards and 1 user component.' }\n    ]\n  }\n]\n\n/* ---------- schema helpers ---------- */\nfunction leaves(rows, out) {\n  out = out || []\n  ;(rows || []).forEach(r => {\n    if (r.kind === 'group') leaves(r.rows, out)\n    else if (r.kind === 'pair') (r.items || []).forEach(it => out.push(it))\n    else if (r.key != null) out.push(r)\n  })\n  return out\n}\n\nfunction defaults(schema) {\n  const d = {}\n  ;(schema || []).forEach(s => leaves(s.rows).forEach(f => { d[f.key] = f.value }))\n  return d\n}\n\nfunction sameValue(a, b) {\n  if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => String(v) === String(b[i]))\n  if (a && b && typeof a === 'object' && typeof b === 'object') {\n    const ka = Object.keys(a), kb = Object.keys(b)\n    return ka.length === kb.length && ka.every(k => String(a[k]) === String(b[k]))\n  }\n  return String(a) === String(b)\n}\n\nfunction openMap(schema) {\n  const o = {}\n  ;(schema || []).forEach(s => {\n    o['s:' + s.id] = s.open !== false\n    ;(s.rows || []).forEach(r => { if (r.kind === 'group') o['g:' + s.id + ':' + r.id] = r.open !== false })\n  })\n  return o\n}\n\nfunction matches(row, q) {\n  if (!q) return true\n  const hay = [row.label, row.text, row.value, (row.items || []).map(i => i.label).join(' ')].join(' ').toLowerCase()\n  return hay.indexOf(q) > -1\n}\n\n/* ---------- field renderers ---------- */\nfunction dot(modified) {\n  return '<span class=\"dot' + (modified ? ' on' : '') + '\"></span>'\n}\n\nfunction numField(f, v, extra) {\n  return '<span class=\"num' + (extra || '') + '\">' +\n    '<input class=\"in num-in\" type=\"text\" inputmode=\"decimal\" spellcheck=\"false\" data-key=\"' + esc(f.key) + '\" data-kind=\"num\" value=\"' + esc(v) + '\">' +\n    (f.unit ? '<i class=\"unit\">' + esc(f.unit) + '</i>' : '') +\n    '</span>'\n}\n\nfunction renderRow(row, ctx) {\n  if (row.kind === 'pair') return renderPair(row, ctx)\n  const vals = ctx.values, defs = ctx.defs\n  const k = row.key\n  const v = k != null ? vals[k] : null\n  const mod = k != null && !sameValue(v, defs[k])\n\n  if (row.kind === 'note') return '<div class=\"row note\">' + esc(row.text) + '</div>'\n\n  if (row.kind === 'group') {\n    const id = 'g:' + ctx.sectionId + ':' + row.id\n    const isOpen = ctx.open[id] !== false\n    const kids = (row.rows || []).filter(r => matches(r, ctx.q))\n    if (!kids.length) return ''\n    const anyMod = leaves(kids).some(f => !sameValue(vals[f.key], defs[f.key]))\n    return '<div class=\"grp' + (isOpen ? ' open' : '') + '\">' +\n      '<button class=\"row grp-h\" data-toggle=\"' + esc(id) + '\">' +\n      dot(anyMod) + '<span class=\"chev\">' + I.chev + '</span>' +\n      '<span class=\"lbl\">' + esc(row.label) + '</span>' +\n      '<span class=\"grp-n\">' + kids.length + '</span>' +\n      '</button>' +\n      (isOpen ? '<div class=\"grp-b\">' + kids.map(r => renderRow(r, ctx)).join('') + '</div>' : '') +\n      '</div>'\n  }\n\n  let ctrl = ''\n  if (row.kind === 'toggle') {\n    ctrl = '<button class=\"sw' + (v ? ' on' : '') + '\" role=\"switch\" aria-checked=\"' + (v ? 'true' : 'false') + '\" data-key=\"' + esc(k) + '\" data-kind=\"toggle\"><i></i></button>'\n  } else if (row.kind === 'text') {\n    ctrl = '<input class=\"in txt-in\" type=\"text\" spellcheck=\"false\" data-key=\"' + esc(k) + '\" data-kind=\"text\" value=\"' + esc(v) + '\" placeholder=\"' + esc(row.placeholder || '') + '\">'\n  } else if (row.kind === 'number') {\n    ctrl = numField(row, v)\n  } else if (row.kind === 'select') {\n    ctrl = '<span class=\"sel\"><select class=\"in sel-in\" data-key=\"' + esc(k) + '\" data-kind=\"select\">' +\n      (row.options || []).map(o => '<option' + (String(o) === String(v) ? ' selected' : '') + '>' + esc(o) + '</option>').join('') +\n      '</select><span class=\"sel-c\">' + I.chev + '</span></span>'\n  } else if (row.kind === 'color') {\n    ctrl = '<span class=\"col\"><span class=\"sw-c\" style=\"background:' + esc(v) + '\"></span>' +\n      '<input class=\"in col-in\" type=\"text\" spellcheck=\"false\" data-key=\"' + esc(k) + '\" data-kind=\"text\" value=\"' + esc(v) + '\"></span>'\n  } else if (row.kind === 'slider') {\n    const pct = Math.max(0, Math.min(100, ((Number(v) - (row.min || 0)) / ((row.max || 100) - (row.min || 0))) * 100))\n    ctrl = '<span class=\"sld-wrap\">' +\n      '<span class=\"sld\" data-key=\"' + esc(k) + '\" data-kind=\"slider\" data-min=\"' + (row.min || 0) + '\" data-max=\"' + (row.max || 100) + '\">' +\n      '<i class=\"sld-t\" style=\"width:' + pct + '%\"></i><i class=\"sld-k\" style=\"left:' + pct + '%\"></i></span>' +\n      numField(row, v, ' sm') + '</span>'\n  } else if (row.kind === 'segmented') {\n    ctrl = '<span class=\"seg' + (row.wide ? ' wide' : '') + '\">' +\n      (row.options || []).map(o => '<button class=\"seg-o' + (String(o) === String(v) ? ' on' : '') + '\" data-key=\"' + esc(k) + '\" data-kind=\"seg\" data-val=\"' + esc(o) + '\">' + esc(o) + '</button>').join('') +\n      '</span>'\n  } else if (row.kind === 'corners') {\n    const arr = Array.isArray(v) ? v : [v, v, v, v]\n    const linked = arr.every(x => String(x) === String(arr[0]))\n    ctrl = '<span class=\"quad\">' +\n      arr.map((x, i) => '<span class=\"num qn\"><input class=\"in num-in\" type=\"text\" inputmode=\"decimal\" data-key=\"' + esc(k) + '\" data-kind=\"quad\" data-idx=\"' + i + '\" value=\"' + esc(x) + '\"></span>').join('') +\n      '<button class=\"mini' + (linked ? ' on' : '') + '\" data-key=\"' + esc(k) + '\" data-kind=\"link\" title=\"Link corners\">' + I.link + '</button>' +\n      '</span>'\n  } else if (row.kind === 'anchor') {\n    const a = v || {}\n    const cell = side => '<button class=\"a-' + side + (a[side] ? ' on' : '') + '\" data-key=\"' + esc(k) + '\" data-kind=\"anchor\" data-side=\"' + side + '\"></button>'\n    ctrl = '<span class=\"anc\">' +\n      '<span class=\"anc-g\">' +\n      '<i></i>' + cell('top') + '<i></i>' +\n      cell('left') + '<span class=\"a-c\"></span>' + cell('right') +\n      '<i></i>' + cell('bottom') + '<i></i>' +\n      '</span></span>'\n  }\n\n  const stacked = row.kind === 'anchor' || row.kind === 'corners'\n  return '<div class=\"row f' + (stacked ? ' stack' : '') + '\" data-row=\"' + esc(k || '') + '\">' +\n    dot(mod) +\n    (row.label ? '<span class=\"lbl\">' + esc(row.label) + (row.hint ? '<i class=\"hint\">' + esc(row.hint) + '</i>' : '') + '</span>' : '') +\n    '<span class=\"ctl\">' + ctrl + '</span>' +\n    (mod ? '<button class=\"mini rst\" data-kind=\"reset\" data-key=\"' + esc(k) + '\" title=\"Reset\">' + I.undo + '</button>' : '') +\n    '</div>'\n}\n\nfunction renderPair(row, ctx) {\n  const mod = (row.items || []).some(it => !sameValue(ctx.values[it.key], ctx.defs[it.key]))\n  return '<div class=\"row f pair\">' + dot(mod) +\n    '<span class=\"lbl\">' + esc(row.label) + '</span>' +\n    '<span class=\"ctl pair-c\">' +\n    (row.items || []).map(it => '<span class=\"num\"><i class=\"pfx\">' + esc(it.label) + '</i>' +\n      '<input class=\"in num-in\" type=\"text\" inputmode=\"decimal\" data-key=\"' + esc(it.key) + '\" data-kind=\"num\" value=\"' + esc(ctx.values[it.key]) + '\">' +\n      (it.unit ? '<i class=\"unit\">' + esc(it.unit) + '</i>' : '') + '</span>').join('') +\n    '</span></div>'\n}\n\nexport default {\n  css: `\n:host { display: block; width: 100%; height: 100% }\n:host, .ins { box-sizing: border-box }\n.ins *, .ins *::before, .ins *::after { box-sizing: border-box }\n.ins {\n  --accent: #0A84FF;\n  --bg: #2c2c2e; --bg2: #333336; --card: rgba(255,255,255,.055); --card-b: rgba(255,255,255,.09);\n  --sep: rgba(255,255,255,.075); --txt: rgba(255,255,255,.93); --txt2: rgba(235,235,245,.62);\n  --txt3: rgba(235,235,245,.35); --ctl: rgba(255,255,255,.10); --ctl2: rgba(255,255,255,.16);\n  --ctl-b: rgba(0,0,0,.32); --track: rgba(0,0,0,.28); --sh: 0 1px 2px rgba(0,0,0,.4);\n  --scroll: rgba(255,255,255,.22); --dot: rgba(235,235,245,.22);\n  width: 100%; height: 100%; display: flex; flex-direction: column; overflow: hidden;\n  border-radius: 11px; background: linear-gradient(180deg, var(--bg2), var(--bg) 120px);\n  color: var(--txt); font-family: -apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Helvetica Neue\", \"Segoe UI\", system-ui, sans-serif;\n  font-size: 12px; line-height: 1.35; -webkit-font-smoothing: antialiased;\n  box-shadow: 0 0 0 .5px rgba(255,255,255,.09), 0 12px 34px rgba(0,0,0,.34);\n}\n.ins.light {\n  --accent: #007AFF;\n  --bg: #ECECEE; --bg2: #F7F7F9; --card: #FFFFFF; --card-b: rgba(0,0,0,.075);\n  --sep: rgba(0,0,0,.07); --txt: #1D1D1F; --txt2: rgba(60,60,67,.62); --txt3: rgba(60,60,67,.34);\n  --ctl: #FFFFFF; --ctl2: rgba(0,0,0,.06); --ctl-b: rgba(0,0,0,.18); --track: rgba(120,120,128,.22);\n  --sh: 0 1px 1.5px rgba(0,0,0,.14); --scroll: rgba(0,0,0,.24); --dot: rgba(60,60,67,.2);\n  box-shadow: 0 0 0 .5px rgba(0,0,0,.1), 0 12px 34px rgba(0,0,0,.16);\n}\n.ic { width: 100%; height: 100%; display: block }\nbutton { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer }\n\n/* ---- title bar ---- */\n.tbar { display: flex; align-items: center; gap: 6px; height: 34px; padding: 0 10px 0 12px; flex: none;\n  border-bottom: .5px solid var(--sep) }\n.tbar .t { font-size: 12.5px; font-weight: 590; letter-spacing: -.01em; flex: 1 }\n.mini { width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; color: var(--txt2);\n  transition: background .12s, color .12s }\n.mini > svg { width: 13px; height: 13px }\n.mini:hover { background: var(--ctl2); color: var(--txt) }\n.mini.on { color: var(--accent) }\n\n/* ---- subject chip ---- */\n.subj { display: flex; align-items: center; gap: 9px; margin: 10px 10px 8px; padding: 8px 9px;\n  background: var(--card); border: .5px solid var(--card-b); border-radius: 9px }\n.subj .tile { width: 26px; height: 26px; border-radius: 7px; flex: none; display: grid; place-items: center;\n  color: #fff; background: linear-gradient(160deg, #5aa9ff, var(--accent)); box-shadow: var(--sh) }\n.subj .tile > svg { width: 15px; height: 15px }\n.subj .m { min-width: 0; flex: 1; display: block }\n.subj .n { display: block; font-size: 12.5px; font-weight: 590; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.subj .p { display: block; font-size: 10.5px; color: var(--txt2); font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n\n/* ---- identity ---- */\n.ident { margin: 0 10px 10px }\n.ident .ir { display: flex; align-items: center; gap: 8px; min-height: 26px }\n.ident .ik { width: 78px; flex: none; font-size: 11px; color: var(--txt2) }\n.ident .iv { flex: 1; min-width: 0; font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 10.5px;\n  color: var(--txt2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n\n/* ---- tabs ---- */\n.tabs { display: flex; align-items: center; gap: 6px; margin: 0 10px 10px }\n.segbar { display: flex; flex: 1; gap: 2px; padding: 2px; background: var(--track); border-radius: 8px }\n.segbar button { flex: 1; height: 20px; border-radius: 6px; font-size: 11px; color: var(--txt2);\n  transition: background .14s, color .14s, box-shadow .14s }\n.segbar button:hover { color: var(--txt) }\n.segbar button.on { background: var(--ctl2); color: var(--txt); font-weight: 590; box-shadow: var(--sh) }\n.ins.light .segbar button.on { background: #fff }\n.srch { display: flex; align-items: center; gap: 5px; height: 24px; padding: 0 7px; border-radius: 7px;\n  background: var(--ctl); border: .5px solid var(--ctl-b); width: 0; opacity: 0; overflow: hidden;\n  transition: width .18s ease, opacity .14s }\n.srch.on { width: 108px; opacity: 1 }\n.srch > svg { width: 11px; height: 11px; color: var(--txt3); flex: none }\n.srch input { flex: 1; min-width: 0; background: none; border: 0; outline: none; color: var(--txt); font: inherit; font-size: 11px }\n\n/* ---- body ---- */\n.body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 0 10px 14px; scrollbar-width: thin }\n.body::-webkit-scrollbar { width: 9px }\n.body::-webkit-scrollbar-thumb { background: transparent; border-radius: 5px; border: 3px solid transparent; background-clip: padding-box }\n.body:hover::-webkit-scrollbar-thumb { background: var(--scroll); background-clip: padding-box }\n\n.sec { margin-bottom: 12px }\n.sec-h { display: flex; align-items: center; gap: 6px; width: 100%; height: 22px; padding: 0 2px;\n  color: var(--txt3); font-size: 10.5px; font-weight: 590; text-transform: uppercase; letter-spacing: .055em }\n.sec-h:hover { color: var(--txt2) }\n.sec-h .chev { width: 8px; height: 8px; color: var(--txt3); transition: transform .16s ease }\n.sec.open .sec-h .chev { transform: rotate(90deg) }\n.sec-h .t { flex: 1; text-align: left }\n.sec-h .n { font-variant-numeric: tabular-nums; opacity: .8; text-transform: none; letter-spacing: 0 }\n\n.card { background: var(--card); border: .5px solid var(--card-b); border-radius: 10px; overflow: hidden }\n.ins.light .card { box-shadow: 0 1px 2px rgba(0,0,0,.05) }\n.row { display: flex; align-items: center; gap: 7px; min-height: 30px; padding: 4px 9px 4px 6px; width: 100% }\n.row + .row, .grp + .row, .row + .grp, .grp + .grp { border-top: .5px solid var(--sep) }\n.row.stack { flex-wrap: wrap }\n.row.note { color: var(--txt2); font-size: 11px; line-height: 1.45; padding: 8px 10px }\n.dot { width: 5px; height: 5px; border-radius: 50%; flex: none; background: transparent;\n  box-shadow: inset 0 0 0 1px var(--dot) }\n.dot.on { background: var(--accent); box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--accent) 22%, transparent) }\n.lbl { flex: 1; min-width: 0; font-size: 11.5px; color: var(--txt); display: flex; flex-direction: column; text-align: left }\n.lbl .hint { font-size: 10px; color: var(--txt3); font-style: normal; margin-top: 1px }\n.ctl { display: flex; align-items: center; justify-content: flex-end; gap: 5px; flex: none }\n.row.stack .ctl { flex: 1 0 100%; justify-content: stretch; margin-top: 4px }\n.rst { opacity: .75 }\n\n/* nested group */\n.grp-h { text-align: left; color: var(--txt) }\n.grp-h .chev { width: 8px; height: 8px; color: var(--txt3); transition: transform .16s ease; flex: none }\n.grp.open .grp-h .chev { transform: rotate(90deg) }\n.grp-h .lbl { font-weight: 590; font-size: 11.5px }\n.grp-n { font-size: 10px; color: var(--txt3); font-variant-numeric: tabular-nums }\n.grp-b { padding-left: 13px; background: rgba(255,255,255,.02) }\n.ins.light .grp-b { background: rgba(0,0,0,.018) }\n.grp-b > .row { border-top: .5px solid var(--sep) }\n\n/* controls */\n.in { background: var(--ctl); border: .5px solid var(--ctl-b); border-radius: 6px; color: var(--txt);\n  font: inherit; font-size: 11.5px; height: 22px; padding: 0 6px; outline: none; min-width: 0;\n  box-shadow: inset 0 .5px 0 rgba(255,255,255,.07); transition: border-color .12s, box-shadow .12s }\n.ins.light .in { box-shadow: 0 .5px 1px rgba(0,0,0,.06) }\n.in:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--ctl-b)) }\n.in:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 26%, transparent) }\n.txt-in { width: 128px }\n.num { position: relative; display: inline-flex; align-items: center }\n.num .pfx { position: absolute; left: 6px; font-size: 10px; color: var(--txt3); font-style: normal; pointer-events: none }\n.num .unit { position: absolute; right: 6px; font-size: 10px; color: var(--txt3); font-style: normal; pointer-events: none }\n.num-in { width: 68px; text-align: right; padding-right: 18px; font-variant-numeric: tabular-nums }\n.num .pfx ~ .num-in { padding-left: 15px }\n.num.sm .num-in { width: 52px; padding-right: 18px; text-align: right }\n.num.qn { flex: 1 }\n.num.qn .num-in { width: 100%; padding: 0 4px; text-align: center }\n.pair-c { gap: 6px }\n.sel { position: relative; display: inline-flex; align-items: center }\n.sel-in { appearance: none; -webkit-appearance: none; width: 128px; padding-right: 20px; cursor: pointer }\n.sel-c { position: absolute; right: 6px; width: 7px; height: 7px; color: var(--txt3); transform: rotate(90deg); pointer-events: none }\n.col { display: inline-flex; align-items: center; gap: 6px }\n.sw-c { width: 20px; height: 20px; border-radius: 5px; box-shadow: inset 0 0 0 .5px rgba(0,0,0,.35), 0 0 0 .5px rgba(255,255,255,.14) }\n.col-in { width: 82px; font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 10.5px; text-transform: uppercase }\n.sw { width: 30px; height: 18px; border-radius: 9px; background: var(--track); position: relative; flex: none;\n  transition: background .18s ease; box-shadow: inset 0 0 0 .5px rgba(0,0,0,.14) }\n.sw i { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff;\n  box-shadow: 0 1px 2px rgba(0,0,0,.35); transition: transform .18s cubic-bezier(.32,.72,0,1) }\n.ins.light .sw { box-shadow: inset 0 0 0 .5px rgba(0,0,0,.07) }\n.ins.light .sw i { box-shadow: 0 1px 2px rgba(0,0,0,.24), 0 0 0 .5px rgba(0,0,0,.07) }\n.sw.on { background: var(--accent) }\n.sw.on i { transform: translateX(12px) }\n.seg { display: inline-flex; gap: 2px; padding: 2px; background: var(--track); border-radius: 7px }\n.seg.wide { width: 100% }\n.seg-o { flex: 1; height: 20px; padding: 0 10px; border-radius: 5px; font-size: 11px; color: var(--txt2); white-space: nowrap }\n.seg-o.on { background: var(--ctl2); color: var(--txt); font-weight: 590; box-shadow: var(--sh) }\n.ins.light .seg-o.on { background: #fff }\n.quad { display: inline-flex; align-items: center; gap: 3px }\n.row.stack .quad { width: 100%; gap: 5px }\n.row.stack .ctl { gap: 6px }\n.sld-wrap { display: inline-flex; align-items: center; gap: 8px }\n.sld { position: relative; width: 84px; height: 4px; border-radius: 2px; background: var(--track); cursor: pointer }\n.sld-t { position: absolute; left: 0; top: 0; height: 100%; border-radius: 2px; background: var(--accent) }\n.sld-k { position: absolute; top: 50%; width: 13px; height: 13px; margin-left: -6.5px; border-radius: 50%;\n  background: #fff; transform: translateY(-50%); box-shadow: 0 1px 2.5px rgba(0,0,0,.4), 0 0 0 .5px rgba(0,0,0,.12) }\n.anc { display: flex; align-items: center; justify-content: center; width: 100% }\n.anc-g { display: grid; grid-template-columns: 22px 30px 22px; grid-template-rows: 18px 24px 18px; gap: 3px;\n  padding: 8px; background: var(--track); border-radius: 8px }\n.anc-g > i { display: block }\n.anc-g button { border-radius: 3px; background: var(--dot); transition: background .14s, box-shadow .14s }\n.anc-g button:hover { background: var(--txt3) }\n.anc-g button.on { background: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent) }\n.a-top, .a-bottom { justify-self: center; align-self: center; width: 22px; height: 3px }\n.a-left, .a-right { align-self: center; justify-self: center; width: 3px; height: 18px }\n.a-c { border-radius: 5px; background: var(--ctl2); box-shadow: inset 0 0 0 .5px var(--card-b) }\n\n/* ---- dock ---- */\n.dock { flex: none; display: flex; align-items: center; gap: 2px; height: 36px; padding: 0 8px;\n  border-top: .5px solid var(--sep); background: var(--bg2) }\n.dock .sp { flex: 1 }\n.dock .mini { width: 24px; height: 24px }\n.dock .mini > svg { width: 14px; height: 14px }\n.dock .mini.danger:hover { background: rgba(255,69,58,.16); color: #ff453a }\n.empty { padding: 26px 12px; text-align: center; color: var(--txt3); font-size: 11.5px }\n`,\n\n  render(d2) {\n    const schema = d2.param('schema') || DEFAULT_SCHEMA\n    const defs = defaults(schema)\n    const values = d2.state.values || defs\n    const open = d2.state.open || openMap(schema)\n    const tab = d2.state.tab || 'all'\n    const q = (d2.state.query || '').trim().toLowerCase()\n    const light = String(d2.param('appearance', 'dark')).toLowerCase() === 'light'\n    const accent = d2.param('accentHex', '')\n\n    const ctx = { values: values, defs: defs, open: open, q: q, sectionId: '' }\n\n    const secs = schema.filter(s => tab === 'all' || (s.tab || 'style') === tab).map(s => {\n      ctx.sectionId = s.id\n      const rows = (s.rows || []).filter(r => r.kind === 'group' || matches(r, q))\n      const html = rows.map(r => renderRow(r, ctx)).join('')\n      if (!html.trim()) return ''\n      const isOpen = open['s:' + s.id] !== false\n      return '<section class=\"sec' + (isOpen ? ' open' : '') + '\">' +\n        '<button class=\"sec-h\" data-toggle=\"s:' + esc(s.id) + '\"><span class=\"chev\">' + I.chev + '</span>' +\n        '<span class=\"t\">' + esc(s.title) + '</span><span class=\"n\">' + leaves(rows).length + '</span></button>' +\n        (isOpen ? '<div class=\"card\">' + html + '</div>' : '') +\n        '</section>'\n    }).join('')\n\n    return '<div class=\"ins' + (light ? ' light' : '') + '\"' + (accent ? ' style=\"--accent:' + esc(accent) + '\"' : '') + '>' +\n      '<div class=\"tbar\"><span class=\"t\">' + esc(d2.param('panelTitle', 'Inspector')) + '</span>' +\n      '<button class=\"mini' + (d2.state.search ? ' on' : '') + '\" data-kind=\"search-toggle\" title=\"Filter\">' + I.search + '</button>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Keep open\">' + I.pin + '</button></div>' +\n\n      '<div class=\"subj\"><span class=\"tile\">' + I.cube + '</span>' +\n      '<span class=\"m\"><span class=\"n\">' + esc(d2.param('subjectTitle', 'Block')) + '</span>' +\n      '<span class=\"p\">' + esc(d2.param('subjectType', 'data2-core-primitives@Block')) + '</span></span>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Copy type id\">' + I.copy + '</button></div>' +\n\n      '<div class=\"ident\">' +\n      '<div class=\"ir\"><span class=\"ik\">Component ID</span><span class=\"iv\">' + esc(d2.param('subjectId', '6a7a79932add8bd2c1a66f18')) + '</span>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Copy id\">' + I.copy + '</button></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Reference</span><input class=\"in txt-in\" style=\"flex:1;width:auto\" data-key=\"__reference\" data-kind=\"text\" spellcheck=\"false\" value=\"' + esc(values.__reference == null ? d2.param('subjectReference', '') : values.__reference) + '\" placeholder=\"auto\"></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Display name</span><input class=\"in txt-in\" style=\"flex:1;width:auto\" data-key=\"__displayName\" data-kind=\"text\" spellcheck=\"false\" value=\"' + esc(values.__displayName == null ? d2.param('subjectName', '') : values.__displayName) + '\" placeholder=\"Untitled\"></div>' +\n      '</div>' +\n\n      '<div class=\"tabs\"><span class=\"segbar\">' +\n      TABS.map(t => '<button class=\"' + (t.id === tab ? 'on' : '') + '\" data-kind=\"tab\" data-val=\"' + t.id + '\">' + t.label + '</button>').join('') +\n      '</span>' +\n      '<span class=\"srch' + (d2.state.search ? ' on' : '') + '\">' + I.search +\n      '<input type=\"text\" spellcheck=\"false\" placeholder=\"Filter\" data-kind=\"query\" value=\"' + esc(d2.state.query || '') + '\"></span>' +\n      '</div>' +\n\n      '<div class=\"body\">' + (secs.trim() ? secs : '<div class=\"empty\">No properties in this tab.</div>') + '</div>' +\n\n      '<div class=\"dock\">' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Edit code\">' + I.code + '</button>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Save as preset\">' + I.save + '</button>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Duplicate\">' + I.dup + '</button>' +\n      '<span class=\"sp\"></span>' +\n      '<button class=\"mini\" data-kind=\"noop\" title=\"Lock\">' + I.lock + '</button>' +\n      '<button class=\"mini\" data-kind=\"reset-all\" title=\"Reset all\">' + I.undo + '</button>' +\n      '<button class=\"mini danger\" data-kind=\"noop\" title=\"Delete\">' + I.trash + '</button>' +\n      '</div></div>'\n  },\n\n  mounted(d2) {\n    const schemaOf = () => d2.param('schema') || DEFAULT_SCHEMA\n    const valsOf = () => d2.state.values || defaults(schemaOf())\n    const openOf = () => d2.state.open || openMap(schemaOf())\n    const setVal = (k, v) => {\n      const next = Object.assign({}, valsOf())\n      next[k] = v\n      d2.setState({ values: next })\n    }\n\n    d2.on('[data-toggle]', 'click', e => {\n      const el = e.target.closest('[data-toggle]')\n      if (!el) return\n      const id = el.getAttribute('data-toggle')\n      const next = Object.assign({}, openOf())\n      next[id] = next[id] === false\n      d2.setState({ open: next })\n    })\n\n    d2.on('[data-kind]', 'click', e => {\n      const el = e.target.closest('[data-kind]')\n      if (!el) return\n      const kind = el.getAttribute('data-kind')\n      const key = el.getAttribute('data-key')\n      if (kind === 'toggle') setVal(key, !valsOf()[key])\n      else if (kind === 'seg') setVal(key, el.getAttribute('data-val'))\n      else if (kind === 'tab') d2.setState({ tab: el.getAttribute('data-val') })\n      else if (kind === 'search-toggle') d2.setState({ search: !d2.state.search, query: '' })\n      else if (kind === 'anchor') {\n        const side = el.getAttribute('data-side')\n        const cur = Object.assign({}, valsOf()[key] || {})\n        cur[side] = !cur[side]\n        setVal(key, cur)\n      } else if (kind === 'link') {\n        const arr = valsOf()[key]\n        if (Array.isArray(arr)) setVal(key, [arr[0], arr[0], arr[0], arr[0]])\n      } else if (kind === 'reset') {\n        setVal(key, defaults(schemaOf())[key])\n      } else if (kind === 'reset-all') {\n        d2.setState({ values: defaults(schemaOf()) })\n      }\n    })\n\n    d2.on('[data-kind=\"slider\"]', 'pointerdown', e => {\n      const el = e.target.closest('[data-kind=\"slider\"]')\n      if (!el) return\n      const key = el.getAttribute('data-key')\n      const min = Number(el.getAttribute('data-min')) || 0\n      const max = Number(el.getAttribute('data-max')) || 100\n      const move = ev => {\n        const r = el.getBoundingClientRect()\n        const p = Math.max(0, Math.min(1, (ev.clientX - r.left) / (r.width || 1)))\n        setVal(key, Math.round(min + p * (max - min)))\n      }\n      move(e)\n      const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }\n      window.addEventListener('pointermove', move)\n      window.addEventListener('pointerup', up)\n    })\n\n    d2.on('input', 'input', e => {\n      const el = e.target\n      const kind = el.getAttribute && el.getAttribute('data-kind')\n      const key = el.getAttribute && el.getAttribute('data-key')\n      if (kind === 'query') { d2.setState({ query: el.value }); return }\n      if (!key) return\n      if (kind === 'num') {\n        const n = parseFloat(el.value)\n        setVal(key, isNaN(n) ? el.value : n)\n      } else if (kind === 'quad') {\n        const idx = Number(el.getAttribute('data-idx')) || 0\n        const arr = (valsOf()[key] || []).slice()\n        const n = parseFloat(el.value)\n        arr[idx] = isNaN(n) ? 0 : n\n        setVal(key, arr)\n      } else if (kind === 'text') {\n        setVal(key, el.value)\n      }\n    })\n\n    d2.on('select', 'change', e => {\n      const el = e.target\n      const key = el.getAttribute && el.getAttribute('data-key')\n      if (key) setVal(key, el.value)\n    })\n  }\n}\n"}},"panelTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panelTitle"}}}},"subjectTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectTitle"}}}},"subjectType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectType"}}}},"subjectId":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectId"}}}},"subjectReference":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectReference"}}}},"subjectName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectName"}}}},"schema":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"schema"}}}},"appearance":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"appearance"}}}},"accentHex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accentHex"}}}}}}}}}},{"_id":"6a7a81283d96feddf93e4b27","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"InspectorPiecesMac","package":"lucca-office","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Inspector Pieces · macOS — props as modular, copy/cut/paste-able lego pieces\nconst ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }\nconst esc = v => String(v == null ? '' : v).replace(/[&<>\"']/g, c => ESC[c])\nconst clone = o => JSON.parse(JSON.stringify(o))\nconst FOCUSED = new WeakSet()\n\nconst I = {\n  chev: '<svg viewBox=\"0 0 8 12\" class=\"ic\"><path d=\"M2.2 1.4 6.4 6l-4.2 4.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  copy: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"2.2\" y=\"2.2\" width=\"7\" height=\"7\" rx=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M4.8 11.8h5a2 2 0 0 0 2-2v-5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  cut: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"3.6\" cy=\"10.4\" r=\"1.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><circle cx=\"10.4\" cy=\"10.4\" r=\"1.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><path d=\"M4.6 8.8 10.2 1.8M9.4 8.8 3.8 1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  paste: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M4.6 2.6H3.4a1.2 1.2 0 0 0-1.2 1.2v7.4a1.2 1.2 0 0 0 1.2 1.2h7.2a1.2 1.2 0 0 0 1.2-1.2V3.8a1.2 1.2 0 0 0-1.2-1.2H9.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"4.6\" y=\"1.4\" width=\"4.8\" height=\"2.6\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  dup: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"1.8\" y=\"1.8\" width=\"7\" height=\"7\" rx=\"1.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"5.2\" y=\"5.2\" width=\"7\" height=\"7\" rx=\"1.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  trash: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3.2 4.2h7.6M5.6 4.2V3a.8.8 0 0 1 .8-.8h1.2a.8.8 0 0 1 .8.8v1.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><path d=\"M4.3 4.2l.5 6.6a1 1 0 0 0 1 .9h2.4a1 1 0 0 0 1-.9l.5-6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  undo: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3 6.2a4.4 4.4 0 1 1 .3 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M2.4 2.6v3.6H6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  link: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M5.8 8.2a2.6 2.6 0 0 0 3.7 0l1.7-1.7a2.6 2.6 0 0 0-3.7-3.7l-.9.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><path d=\"M8.2 5.8a2.6 2.6 0 0 0-3.7 0L2.8 7.5a2.6 2.6 0 0 0 3.7 3.7l.9-.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  plus: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M7 3v8M3 7h8\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  search: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"6\" cy=\"6\" r=\"4.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M9.3 9.3 12.4 12.4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  cube: '<svg viewBox=\"0 0 16 16\" class=\"ic\"><path d=\"M8 1.8 14 5v6l-6 3.2L2 11V5z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M2 5l6 3.2L14 5M8 8.2v6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.1\" opacity=\".55\"/></svg>',\n  arw: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.8v8M2.8 6.6 6 9.8l3.2-3.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  arw2: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.6v8.8M3.4 4 6 1.4 8.6 4M3.4 8 6 10.6 8.6 8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  algn: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M1.4 1.2v9.6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><rect x=\"3.6\" y=\"3.4\" width=\"7\" height=\"5.2\" rx=\"1.3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  check: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M2.4 6.4 4.8 8.8 9.6 3.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  x: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M3.2 3.2 8.8 8.8M8.8 3.2 3.2 8.8\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  dots: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><circle cx=\"6\" cy=\"2.5\" r=\"1.05\" fill=\"currentColor\"/><circle cx=\"6\" cy=\"6\" r=\"1.05\" fill=\"currentColor\"/><circle cx=\"6\" cy=\"9.5\" r=\"1.05\" fill=\"currentColor\"/></svg>'\n}\n\nconst TABS = [\n  { id: 'all', label: 'All' },\n  { id: 'style', label: 'Style' },\n  { id: 'layout', label: 'Layout' },\n  { id: 'content', label: 'Content' },\n  { id: 'logic', label: 'Logic' }\n]\n\nconst KIND_LABEL = { border: 'border', fill: 'fill', shadow: 'shadow', geometry: 'position', size: 'size', spacing: 'spacing', flags: 'flags', text: 'text' }\n\nconst DEFAULT_MODEL = [\n  {\n    id: 'root', title: 'Block', pieces: [\n      /* ---- style ---- */\n      { id: 'p1', kind: 'border', title: 'Border', cat: 'style',\n        v: { r: [10, 10, 10, 10], w: [1, 1, 1, 1], color: '#0078DC', style: 'Solid' },\n        def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#000000', style: 'None' } },\n      { id: 'p2', kind: 'fill', title: 'Fill', cat: 'style',\n        v: { color: '#1D1D1F', opacity: 100, blend: 'Normal' },\n        def: { color: '#000000', opacity: 100, blend: 'Normal' } },\n      { id: 'p3', kind: 'fields', title: 'Opacity', cat: 'style',\n        fields: [{ path: 'value', label: '%', type: 'num' }],\n        v: { value: 100 }, def: { value: 100 } },\n      { id: 'p4', kind: 'shadow', title: 'Shadow', cat: 'style', sub: 'List of shadows',\n        v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' },\n        def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n      { id: 'p5', kind: 'fields', title: 'Backdrop filter', cat: 'style', sub: 'List of bitmap filters',\n        fields: [{ path: 'blur', label: 'blur', type: 'num' }, { path: 'saturate', label: 'sat', type: 'num' }],\n        v: { blur: 0, saturate: 100 }, def: { blur: 0, saturate: 100 } },\n      { id: 'p21', kind: 'fields', title: 'Filter', cat: 'style', sub: 'List of bitmap filters',\n        fields: [{ path: 'blur', label: 'blur', type: 'num' }, { path: 'grayscale', label: 'gray', type: 'num' }],\n        v: { blur: 0, grayscale: 0 }, def: { blur: 0, grayscale: 0 } },\n      { id: 'p6', kind: 'fields', title: 'Transition', cat: 'style',\n        fields: [{ path: 'ms', label: 'ms', type: 'num' }, { path: 'ease', label: 'easing', type: 'enum', options: ['Linear', 'Ease', 'Ease in', 'Ease out', 'Spring'] }],\n        v: { ms: 0, ease: 'Ease' }, def: { ms: 0, ease: 'Ease' } },\n      /* ---- layout ---- */\n      { id: 'p7', kind: 'geometry', title: 'Position', cat: 'layout',\n        v: { anchor: { top: true, left: true, right: false, bottom: false }, x: 1135, y: 51, growH: false, growV: false },\n        def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n      { id: 'p8', kind: 'size', title: 'Dimensions', cat: 'layout',\n        v: { w: 160, h: 540, link: false, growW: false, growH: false },\n        def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n      { id: 'p9', kind: 'spacing', title: 'Padding', cat: 'layout',\n        v: { t: 12, r: 14, b: 12, l: 14 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n      { id: 'p10', kind: 'fields', title: 'Constraints', cat: 'layout',\n        fields: [{ path: 'minW', label: 'min W', type: 'num' }, { path: 'maxW', label: 'max W', type: 'num' }, { path: 'minH', label: 'min H', type: 'num' }, { path: 'maxH', label: 'max H', type: 'num' }],\n        v: { minW: 0, maxW: 0, minH: 0, maxH: 0 }, def: { minW: 0, maxW: 0, minH: 0, maxH: 0 } },\n      { id: 'p11', kind: 'fields', title: 'Children mode', cat: 'layout',\n        fields: [{ path: 'mode', label: 'flow', type: 'enum', options: ['Absolute', 'Rows', 'Columns', 'Reversed rows', 'Reversed columns'] }],\n        v: { mode: 'Absolute' }, def: { mode: 'Absolute' } },\n      { id: 'p12', kind: 'fields', title: 'Scroll', cat: 'layout',\n        fields: [{ path: 'on', label: 'Scrollable', type: 'bool' }],\n        v: { on: false }, def: { on: false } },\n      /* ---- content ---- */\n      { id: 'p13', kind: 'fields', title: 'Text', cat: 'content',\n        fields: [{ path: 'value', label: 'content', type: 'str', ph: 'Type text…' }],\n        v: { value: '' }, def: { value: '' } },\n      { id: 'p14', kind: 'fields', title: 'Typography', cat: 'content',\n        fields: [{ path: 'font', label: 'font', type: 'enum', options: ['SF Pro Text', 'Inter', 'Helvetica Neue', 'Menlo'] }, { path: 'weight', label: 'weight', type: 'enum', options: ['Regular', 'Medium', 'Semibold', 'Bold'] }, { path: 'size', label: 'size', type: 'num' }, { path: 'lh', label: 'line', type: 'num' }],\n        v: { font: 'SF Pro Text', weight: 'Regular', size: 13, lh: 18 },\n        def: { font: 'SF Pro Text', weight: 'Regular', size: 13, lh: 18 } },\n      { id: 'p15', kind: 'fields', title: 'Text color', cat: 'content',\n        fields: [{ path: 'color', label: '', type: 'color' }],\n        v: { color: '#FFFFFF' }, def: { color: '#FFFFFF' } },\n      { id: 'p16', kind: 'fields', title: 'Icon', cat: 'content',\n        fields: [{ path: 'name', label: 'name', type: 'str', ph: 'fa fa-star' }],\n        v: { name: '' }, def: { name: '' } },\n      /* ---- logic ---- */\n      { id: 'p17', kind: 'fields', title: 'Hide', cat: 'logic',\n        fields: [{ path: 'on', label: 'Hidden', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p18', kind: 'fields', title: 'Disabled', cat: 'logic',\n        fields: [{ path: 'on', label: 'Disabled', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p19', kind: 'fields', title: 'Children overflow', cat: 'logic',\n        fields: [{ path: 'on', label: 'Visible outside bounds', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p20', kind: 'fields', title: 'On click', cat: 'logic',\n        fields: [{ path: 'action', label: 'action', type: 'enum', options: ['None', 'Set value', 'Go to route', 'Run service', 'Open link'] }],\n        v: { action: 'None' }, def: { action: 'None' } }\n    ]\n  }\n]\n\n/* addable layers — the `+` on a node appends another instance of one of these */\nconst TEMPLATES = {\n  border: { kind: 'border', title: 'Border', cat: 'style', v: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#0078DC', style: 'None' }, def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#0078DC', style: 'None' } },\n  fill: { kind: 'fill', title: 'Fill', cat: 'style', v: { color: '#0078DC', opacity: 100, blend: 'Normal' }, def: { color: '#0078DC', opacity: 100, blend: 'Normal' } },\n  shadow: { kind: 'shadow', title: 'Shadow', cat: 'style', v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' }, def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n  padding: { kind: 'spacing', title: 'Padding', cat: 'layout', v: { t: 0, r: 0, b: 0, l: 0 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n  position: { kind: 'geometry', title: 'Position', cat: 'layout', v: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false }, def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n  dimensions: { kind: 'size', title: 'Dimensions', cat: 'layout', v: { w: 0, h: 0, link: false, growW: false, growH: false }, def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n  transition: { kind: 'fields', title: 'Transition', cat: 'style', fields: [{ path: 'ms', label: 'ms', type: 'num' }, { path: 'ease', label: 'easing', type: 'enum', options: ['Linear', 'Ease', 'Ease in', 'Ease out', 'Spring'] }], v: { ms: 0, ease: 'Ease' }, def: { ms: 0, ease: 'Ease' } },\n  text: { kind: 'fields', title: 'Text', cat: 'content', fields: [{ path: 'value', label: 'content', type: 'str', ph: 'Type text…' }], v: { value: '' }, def: { value: '' } }\n}\n\n/* ---------- value path helpers ---------- */\nfunction getAt(v, path) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length; i++) {\n    if (cur == null) return null\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n  }\n  return cur\n}\nfunction setAt(v, path, val) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length - 1; i++) {\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n    if (cur == null) return\n  }\n  const last = parts[parts.length - 1]\n  if (Array.isArray(cur)) cur[Number(last)] = val\n  else cur[last] = val\n}\nfunction findPiece(model, id) {\n  for (let i = 0; i < model.length; i++) {\n    const ps = model[i].pieces || []\n    for (let j = 0; j < ps.length; j++) if (ps[j].id === id) return { tray: model[i], piece: ps[j], ti: i, pi: j }\n  }\n  return null\n}\nfunction nextId(model) {\n  let max = 0\n  model.forEach(t => (t.pieces || []).forEach(p => {\n    const n = parseInt(String(p.id).replace(/[^0-9]/g, ''), 10)\n    if (n > max) max = n\n  }))\n  return 'p' + (max + 1)\n}\n\n/* ---------- slot renderers ---------- */\nfunction slot(pid, path, type, value, opts) {\n  opts = opts || {}\n  const sel = opts.sel && opts.sel.type === 'slot' && opts.sel.piece === pid && opts.sel.path === path\n  const cls = 'slot' + (sel ? ' sel' : '') + (opts.cls ? ' ' + opts.cls : '')\n  let inner = ''\n  if (type === 'color') {\n    inner = '<span class=\"chip-c\" style=\"background:' + esc(value) + '\"></span>' +\n      '<input class=\"sin hex\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + esc(value) + '\">'\n  } else if (type === 'enum') {\n    inner = '<select class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\">' +\n      (opts.options || []).map(o => '<option' + (String(o) === String(value) ? ' selected' : '') + '>' + esc(o) + '</option>').join('') + '</select>'\n  } else if (type === 'text') {\n    inner = '<input class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + esc(value) + '\" placeholder=\"' + esc(opts.ph || '') + '\">'\n  } else {\n    inner = '<input class=\"sin num\" data-in=\"' + esc(pid) + '|' + esc(path) + '|num\" inputmode=\"decimal\" spellcheck=\"false\" value=\"' + esc(value) + '\">'\n  }\n  return '<span class=\"' + cls + '\" data-slot=\"' + esc(pid) + '|' + esc(path) + '|' + type + '\"' +\n    (opts.cap ? ' data-cap=\"' + esc(opts.cap) + '\"' : '') + '>' +\n    (opts.cap ? '<i class=\"cap\">' + esc(opts.cap) + '</i>' : '') + inner + '</span>'\n}\n\nfunction chip(pid, path, on, label) {\n  return '<button class=\"tchip' + (on ? ' on' : '') + '\" data-act=\"flag\" data-piece=\"' + esc(pid) + '\" data-path=\"' + esc(path) + '\">' +\n    '<i class=\"tbox\"></i>' + esc(label) + '</button>'\n}\n\n/* ---------- piece bodies ---------- */\nfunction bodyBorder(p, s) {\n  const v = p.v\n  const c = i => slot(p.id, 'r.' + i, 'num', v.r[i], { sel: s, cls: 'corner c' + i, cap: 'radius' })\n  const e = i => slot(p.id, 'w.' + i, 'num', v.w[i], { sel: s, cls: 'edge', cap: 'width' })\n  const selC = s && s.type === 'slot' && s.piece === p.id && s.path === 'color'\n  return '<div class=\"mtx brd\">' +\n    c(0) + e(0) + c(1) +\n    e(3) + '<span class=\"cempty\"></span>' + e(1) +\n    c(3) + e(2) + c(2) +\n    '<span class=\"cbig slot' + (selC ? ' sel' : '') + '\" data-slot=\"' + esc(p.id) + '|color|color\" style=\"background:' + esc(v.color) + '\">' +\n    '<input class=\"sin hex big\" data-in=\"' + esc(p.id) + '|color|text\" spellcheck=\"false\" value=\"' + esc(v.color) + '\"></span>' +\n    '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'style', 'enum', v.style, { sel: s, cls: 'wide', options: ['None', 'Solid', 'Dashed', 'Dotted'], cap: 'type' }) + '</div>'\n}\n\nfunction bodySpacing(p, s) {\n  const v = p.v\n  return '<div class=\"mtx pad\">' +\n    '<i></i>' + slot(p.id, 't', 'num', v.t, { sel: s, cap: 'top' }) + '<i></i>' +\n    slot(p.id, 'l', 'num', v.l, { sel: s, cap: 'left' }) + '<span class=\"padc\">content</span>' + slot(p.id, 'r', 'num', v.r, { sel: s, cap: 'right' }) +\n    '<i></i>' + slot(p.id, 'b', 'num', v.b, { sel: s, cap: 'bottom' }) + '<i></i>' +\n    '</div>'\n}\n\nfunction bodyGeometry(p, s) {\n  const v = p.v, a = v.anchor || {}\n  const cell = side => '<button class=\"a-' + side + (a[side] ? ' on' : '') + '\" data-act=\"anchor\" data-piece=\"' + esc(p.id) + '\" data-side=\"' + side + '\"></button>'\n  const grp = ks => '<span class=\"clus\">' + ks.map(k => '<button class=\"ib al-' + k + '\" data-act=\"noop\" title=\"Align\">' + I.algn + '</button>').join('') + '</span>'\n  return '<div class=\"geo\">' +\n    '<span class=\"anc-g\">' + '<i></i>' + cell('top') + '<i></i>' + cell('left') + '<span class=\"a-c\"></span>' + cell('right') + '<i></i>' + cell('bottom') + '<i></i>' + '</span>' +\n    '<span class=\"geo-r\">' +\n    chip(p.id, 'growH', v.growH, 'Grow H') + chip(p.id, 'growV', v.growV, 'Grow V') +\n    '</span></div>' +\n    '<div class=\"clusrow\">' + grp(['l', 'c', 'r']) + grp(['t', 'm', 'b']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y' }) + '</div>'\n}\n\nfunction bodySize(p, s) {\n  const v = p.v\n  const grp = ks => '<span class=\"clus\">' + ks.map(k => '<button class=\"ib d-' + k + '\" data-act=\"noop\" title=\"Distribute\">' + (k === 'w1' || k === 'h1' ? I.arw2 : I.arw) + '</button>').join('') + '</span>'\n  return '<div class=\"clusrow\">' + grp(['w0', 'w1', 'w2']) + grp(['h0', 'h1', 'h2']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'w', 'num', v.w, { sel: s, cap: 'W' }) +\n    '<button class=\"ib lk' + (v.link ? ' on' : '') + '\" data-act=\"flag\" data-piece=\"' + esc(p.id) + '\" data-path=\"link\">' + I.link + '</button>' +\n    slot(p.id, 'h', 'num', v.h, { sel: s, cap: 'H' }) + '</div>' +\n    '<div class=\"chips\">' + chip(p.id, 'growW', v.growW, 'Fit width') + chip(p.id, 'growH', v.growH, 'Fit height') + '</div>'\n}\n\nfunction bodyFill(p, s) {\n  const v = p.v\n  return '<div class=\"prow2\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'grow' }) +\n    slot(p.id, 'opacity', 'num', v.opacity, { sel: s, cap: '%' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'blend', 'enum', v.blend, { sel: s, cls: 'wide', options: ['Normal', 'Multiply', 'Screen', 'Overlay'], cap: 'blend' }) + '</div>'\n}\n\nfunction bodyShadow(p, s) {\n  const v = p.v\n  return '<div class=\"shw\"><span class=\"shw-p\" style=\"box-shadow:' + Number(v.x) + 'px ' + Number(v.y) + 'px ' + Number(v.blur) + 'px ' + Number(v.spread) + 'px ' + esc(v.color) + '\"></span></div>' +\n    '<div class=\"prow4\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y' }) +\n    slot(p.id, 'blur', 'num', v.blur, { sel: s, cap: 'blur' }) + slot(p.id, 'spread', 'num', v.spread, { sel: s, cap: 'sprd' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'wide' }) + '</div>'\n}\n\nfunction bodyFlags(p, s) {\n  return '<div class=\"chips\">' + (p.v.items || []).map((it, i) => chip(p.id, 'items.' + i + '.on', it.on, it.label)).join('') + '</div>'\n}\n\nfunction bodyText(p, s) {\n  const v = p.v\n  return '<div class=\"prow\">' + slot(p.id, 'value', 'text', v.value, { sel: s, cls: 'wide', ph: 'Type text…' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'font', 'enum', v.font, { sel: s, cls: 'wide', options: ['SF Pro Text', 'Inter', 'Helvetica Neue', 'Menlo'], cap: 'font' }) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'size', 'num', v.size, { sel: s, cap: 'size' }) + slot(p.id, 'lh', 'num', v.lh, { sel: s, cap: 'line' }) + '</div>'\n}\n\nfunction bodyFields(p, s) {\n  const fs = p.fields || []\n  const bools = fs.filter(f => f.type === 'bool')\n  const rest = fs.filter(f => f.type !== 'bool')\n  let out = ''\n  if (rest.length) {\n    const one = rest.length === 1\n    out += '<div class=\"' + (one ? 'prow' : 'fgrid') + '\">' + rest.map(f => {\n      const type = f.type === 'enum' ? 'enum' : f.type === 'color' ? 'color' : f.type === 'str' ? 'text' : 'num'\n      return slot(p.id, f.path, type, getAt(p.v, f.path), { sel: s, cls: one ? 'wide' : '', options: f.options, cap: f.label, ph: f.ph })\n    }).join('') + '</div>'\n  }\n  if (bools.length) out += '<div class=\"chips\">' + bools.map(f => chip(p.id, f.path, !!getAt(p.v, f.path), f.label)).join('') + '</div>'\n  return out || '<div class=\"empty\">No editable values.</div>'\n}\n\nconst BODIES = { fields: bodyFields, border: bodyBorder, spacing: bodySpacing, geometry: bodyGeometry, size: bodySize, fill: bodyFill, shadow: bodyShadow, flags: bodyFlags, text: bodyText }\n\n/* ---------- category selector (stepper pill) ---------- */\nfunction catCount(model, id) {\n  let n = 0\n  model.forEach(t => (t.pieces || []).forEach(p => { if (id === 'all' || (p.cat || 'style') === id) n += 1 }))\n  return n\n}\n\nfunction renderAddMenu(t) {\n  return '<div class=\"menu add-m\">' + Object.keys(TEMPLATES).map(k =>\n    '<button class=\"cat\" data-act=\"add-pick\" data-tray=\"' + esc(t.id) + '\" data-kind=\"' + esc(k) + '\">' +\n    '<span class=\"ck\"></span><span class=\"cl2\">' + esc(TEMPLATES[k].title) + '</span>' +\n    '<span class=\"cc\">' + esc(k) + '</span></button>').join('') + '</div>'\n}\n\nfunction pieceMatches(p, q) {\n  return (p.title + ' ' + p.kind + ' ' + JSON.stringify(p.v)).toLowerCase().indexOf(q) > -1\n}\n\nfunction renderPill(model, tab, mode, q) {\n  if (mode === 'search') {\n    return '<div class=\"tabs\"><div class=\"pwrap\"><div class=\"pill act\">' +\n      '<span class=\"pglyph\">' + I.search + '</span>' +\n      '<input class=\"pq\" spellcheck=\"false\" placeholder=\"Search props…\" value=\"' + esc(q) + '\">' +\n      '<button class=\"parr nx\" data-act=\"cat-close\" title=\"Close search\">' + I.x + '</button>' +\n      '</div></div></div>'\n  }\n  let idx = TABS.map(t => t.id).indexOf(tab)\n  if (idx < 0) idx = 0\n  const open = mode === 'list'\n  return '<div class=\"tabs\"><div class=\"pwrap\">' +\n    '<div class=\"pill' + (open ? ' act' : '') + '\">' +\n    '<button class=\"parr\" data-act=\"cat-prev\" title=\"Previous category\">' + I.chev + '</button>' +\n    '<button class=\"plabel\" data-act=\"cat-open\" title=\"Click to pick · double-click to search\">' + esc(TABS[idx].label) + '</button>' +\n    '<button class=\"parr nx\" data-act=\"cat-next\" title=\"Next category\">' + I.chev + '</button>' +\n    '</div>' +\n    (open ? '<div class=\"cats\">' + TABS.map(t => '<button class=\"cat' + (t.id === tab ? ' on' : '') + '\" data-act=\"cat-pick\" data-val=\"' + t.id + '\">' +\n      '<span class=\"ck\">' + (t.id === tab ? I.check : '') + '</span>' +\n      '<span class=\"cl2\">' + esc(t.label) + '</span>' +\n      '<span class=\"cc\">' + catCount(model, t.id) + '</span></button>').join('') + '</div>' : '') +\n    '</div></div>'\n}\n\nfunction renderPieceMenu(p) {\n  const items = [['copy', 'Copy'], ['cut', 'Cut'], ['dup', 'Duplicate'], ['paste-into', 'Paste into'], ['reset-piece', 'Reset'], ['del', 'Remove']]\n  return '<div class=\"menu p-m\">' + items.map(it =>\n    '<button class=\"cat' + (it[0] === 'del' ? ' dngr' : '') + '\" data-act=\"' + it[0] + '\" data-piece=\"' + esc(p.id) + '\">' +\n    '<span class=\"cl2\">' + it[1] + '</span></button>').join('') + '</div>'\n}\n\nfunction fxBody(p) {\n  return '<div class=\"fxrow\"><span class=\"fxk\">f()</span>' +\n    '<input class=\"sin fxin\" data-in=\"' + esc(p.id) + '|__fx|text\" spellcheck=\"false\" value=\"' + esc(p.v.__fx) + '\" placeholder=\"Reference(&#39;brandColor&#39;)\"></div>'\n}\n\nfunction renderPiece(p, tray, st) {\n  const ref = p.def !== undefined ? p.def : (TEMPLATES[p.kind] ? TEMPLATES[p.kind].v : null)\n  const pop = !ref || JSON.stringify(ref) !== JSON.stringify(p.v)\n  const exp = st.pOpen && st.pOpen[p.id] !== undefined ? st.pOpen[p.id] : pop\n  const sel = st.sel && st.sel.type === 'piece' && st.sel.piece === p.id\n  const cut = st.cut === p.id\n  const fx = p.v && p.v.__fx != null\n  const fn = BODIES[p.kind] || bodyFlags\n  return '<div class=\"piece' + (pop ? ' pop' : '') + (exp ? ' exp' : '') + (p.sub ? ' has-sub' : '') + (sel ? ' sel' : '') + (cut ? ' cut' : '') + (st.drag === p.id ? ' drag' : '') + '\" data-piece=\"' + esc(p.id) + '\" data-tray=\"' + esc(tray.id) + '\">' +\n    '<div class=\"ph\" data-act=\"select\" data-piece=\"' + esc(p.id) + '\" data-grip=\"' + esc(p.id) + '\">' +\n    '<button class=\"pdot' + (exp ? ' on' : '') + '\" data-act=\"pexp\" data-piece=\"' + esc(p.id) + '\" title=\"Expand / collapse\"></button>' +\n    '<span class=\"ptxt\"><span class=\"pt\">' + esc(p.title) + '</span>' +\n    (p.sub ? '<span class=\"psub\">' + esc(p.sub) + '</span>' : '') + '</span>' +\n    (fx ? '<span class=\"fxbadge\">f()</span>' : '') +\n    '<span class=\"pspace\"></span>' +\n    '<span class=\"ptools\">' +\n    '<button class=\"ib tb\" data-act=\"noop\" title=\"About this property\">?</button>' +\n    '<button class=\"ib tb' + (fx ? ' on' : '') + '\" data-act=\"fx\" data-piece=\"' + esc(p.id) + '\" title=\"Bind to an expression\">f()</button>' +\n    '<button class=\"ib tb\" data-act=\"menu\" data-piece=\"' + esc(p.id) + '\" title=\"More\">' + I.dots + '</button>' +\n    '</span>' +\n    (st.menuFor === p.id ? renderPieceMenu(p) : '') +\n    '</div>' +\n    (exp ? '<div class=\"pb\">' + (fx ? fxBody(p) : fn(p, st.sel)) + '</div>' : '') +\n    '</div>'\n}\n\nexport default {\n  css: `\n:host { display: block; width: 100%; height: 100% }\n:host, .ins { box-sizing: border-box }\n.ins *, .ins *::before, .ins *::after { box-sizing: border-box }\n.ins {\n  --accent: #0A84FF;\n  --bg: #2c2c2e; --bg2: #333336; --card: rgba(255,255,255,.052); --card-b: rgba(255,255,255,.09);\n  --sep: rgba(255,255,255,.075); --txt: rgba(255,255,255,.93); --txt2: rgba(235,235,245,.62);\n  --txt3: rgba(235,235,245,.34); --ctl: rgba(255,255,255,.10); --ctl2: rgba(255,255,255,.16);\n  --ctl-b: rgba(0,0,0,.32); --track: rgba(0,0,0,.28); --sh: 0 1px 2px rgba(0,0,0,.4);\n  --scroll: rgba(255,255,255,.22); --dot: rgba(235,235,245,.22); --stud: rgba(255,255,255,.13);\n  width: 300px; max-width: 100%; height: 100%; margin-left: auto; display: flex; flex-direction: column; overflow: hidden;\n  transition: width .22s cubic-bezier(.32,.72,0,1); border-radius: 11px; background: linear-gradient(180deg, var(--bg2), var(--bg) 120px);\n  color: var(--txt); font-family: -apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Helvetica Neue\", \"Segoe UI\", system-ui, sans-serif;\n  font-size: 12px; line-height: 1.35; -webkit-font-smoothing: antialiased; user-select: none;\n  box-shadow: 0 0 0 .5px rgba(255,255,255,.09), 0 12px 34px rgba(0,0,0,.34);\n}\n.ins.light {\n  --accent: #007AFF;\n  --bg: #ECECEE; --bg2: #F7F7F9; --card: #FFFFFF; --card-b: rgba(0,0,0,.08);\n  --sep: rgba(0,0,0,.07); --txt: #1D1D1F; --txt2: rgba(60,60,67,.62); --txt3: rgba(60,60,67,.34);\n  --ctl: #FFFFFF; --ctl2: rgba(0,0,0,.06); --ctl-b: rgba(0,0,0,.18); --track: rgba(120,120,128,.20);\n  --sh: 0 1px 1.5px rgba(0,0,0,.14); --scroll: rgba(0,0,0,.24); --dot: rgba(60,60,67,.2); --stud: rgba(0,0,0,.09);\n  box-shadow: 0 0 0 .5px rgba(0,0,0,.1), 0 12px 34px rgba(0,0,0,.16);\n}\n.ic { width: 100%; height: 100%; display: block }\nbutton { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer }\n.ib { width: 19px; height: 19px; border-radius: 5px; display: grid; place-items: center; color: var(--txt2); flex: none;\n  transition: background .12s, color .12s }\n.ib > svg { width: 12px; height: 12px }\n.ib:hover { background: var(--ctl2); color: var(--txt) }\n.ib.on { color: #fff; background: var(--accent) }\n.ib.danger:hover { background: rgba(255,69,58,.18); color: #ff453a }\n\n/* header */\n.tbar { display: flex; align-items: center; gap: 6px; height: 34px; padding: 0 8px 0 12px; flex: none; border-bottom: .5px solid var(--sep) }\n.tbar .t { font-size: 12.5px; font-weight: 590; letter-spacing: -.01em; flex: 1 }\n.subj { display: flex; align-items: center; gap: 9px; margin: 10px 10px 8px; padding: 8px 9px;\n  background: var(--card); border: .5px solid var(--card-b); border-radius: 9px }\n.subj .tile { width: 26px; height: 26px; border-radius: 7px; flex: none; display: grid; place-items: center;\n  color: #fff; background: linear-gradient(160deg, #5aa9ff, var(--accent)); box-shadow: var(--sh) }\n.subj .tile > svg { width: 15px; height: 15px }\n.subj .m { min-width: 0; flex: 1; display: block }\n.subj .n { display: block; font-size: 12.5px; font-weight: 590; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.subj .p { display: block; font-size: 10.5px; color: var(--txt2); font-family: ui-monospace, \"SF Mono\", Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.ident { margin: 0 10px 10px }\n.ident .ir { display: flex; align-items: center; gap: 8px; min-height: 25px }\n.ident .ik { width: 76px; flex: none; font-size: 11px; color: var(--txt2) }\n.ident .iv { flex: 1; min-width: 0; font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 10.5px; color: var(--txt2);\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: text }\n.ident input { flex: 1; width: auto; background: var(--ctl); border: .5px solid var(--ctl-b); border-radius: 6px;\n  color: var(--txt); font: inherit; font-size: 11.5px; height: 22px; padding: 0 6px; outline: none; user-select: text }\n.ident input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 26%, transparent) }\n.tabs { display: flex; align-items: center; gap: 6px; margin: 0 10px 10px }\n.segbar { display: flex; flex: 1; gap: 2px; padding: 2px; background: var(--track); border-radius: 8px }\n.segbar button { flex: 1; height: 20px; border-radius: 6px; font-size: 10.5px; color: var(--txt2); transition: background .14s, color .14s }\n.segbar button.on { background: var(--ctl2); color: var(--txt); font-weight: 590; box-shadow: var(--sh) }\n.ins.light .segbar button.on { background: #fff }\n\n/* category stepper pill */\n.tabs { position: relative; z-index: 20 }\n.pwrap { position: relative; flex: 1 }\n.pill { display: flex; align-items: center; height: 30px; padding: 0 3px; border-radius: 999px;\n  background: rgba(0,0,0,.16); border: 1px solid var(--card-b); transition: border-color .14s, box-shadow .14s }\n.ins.light .pill { background: rgba(0,0,0,.028) }\n.pill:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--card-b)) }\n.pill.act { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent) }\n.parr { width: 26px; height: 24px; border-radius: 999px; display: grid; place-items: center; color: var(--txt3); flex: none;\n  transition: color .12s, background .12s }\n.parr > svg { width: 7px; height: 11px }\n.parr:hover { color: var(--txt); background: var(--ctl2) }\n.parr:not(.nx) > svg { transform: rotate(180deg) }\n.plabel { flex: 1; min-width: 0; height: 24px; border-radius: 999px; font-size: 12.5px; color: var(--txt2);\n  text-transform: lowercase; letter-spacing: .01em; transition: color .12s, background .12s }\n.plabel:hover { color: var(--txt); background: var(--ctl2) }\n.pglyph { width: 24px; display: grid; place-items: center; color: var(--txt3); flex: none }\n.pglyph > svg { width: 12px; height: 12px }\n.pq { flex: 1; min-width: 0; height: 24px; background: none; border: 0; outline: none; color: var(--txt);\n  font: inherit; font-size: 12px; user-select: text }\n.cats, .menu { position: absolute; left: 0; right: 0; top: 35px; z-index: 30; padding: 4px; border-radius: 10px;\n  background: var(--bg2); border: .5px solid var(--card-b); box-shadow: 0 12px 28px rgba(0,0,0,.45) }\n.ins.light .cats, .ins.light .menu { background: #fff; box-shadow: 0 12px 28px rgba(0,0,0,.16) }\n.cat { display: flex; align-items: center; gap: 7px; width: 100%; height: 26px; padding: 0 7px; border-radius: 6px;\n  font-size: 11.5px; color: var(--txt); text-transform: lowercase }\n.cat:hover { background: var(--accent); color: #fff }\n.cat .ck { width: 11px; height: 11px; flex: none; color: var(--accent) }\n.cat:hover .ck { color: #fff }\n.cat .cl2 { flex: 1; text-align: left }\n.cat .cc { font-size: 10px; color: var(--txt3); font-variant-numeric: tabular-nums }\n.cat:hover .cc { color: rgba(255,255,255,.8) }\n\n/* body / trays */\n.ins.wide { width: 100% }\n.ins.wide .tray-b { display: block; columns: 2; column-gap: 10px }\n.ins.wide .tray-b .piece { break-inside: avoid; margin-bottom: 10px }\n.ins.wide .body { padding: 0 12px 14px }\n.body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 0 10px 14px; scrollbar-width: thin }\n.body::-webkit-scrollbar { width: 9px }\n.body::-webkit-scrollbar-thumb { background: transparent; border-radius: 5px; border: 3px solid transparent; background-clip: padding-box }\n.body:hover::-webkit-scrollbar-thumb { background: var(--scroll); background-clip: padding-box }\n.tray { margin-bottom: 14px }\n.node { position: relative; display: flex; align-items: center; gap: 3px; height: 40px; padding: 0 4px 0 0; margin-bottom: 2px }\n.node-h { flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px; height: 32px; padding: 0 8px;\n  border-radius: 8px; text-align: left; transition: background .12s }\n.node-h:hover { background: var(--ctl2) }\n.node-h .chev { width: 9px; height: 12px; flex: none; color: var(--txt3); transition: transform .16s ease }\n.tray.open .node-h .chev { transform: rotate(90deg) }\n.nt { font-size: 16px; font-weight: 590; letter-spacing: -.02em; color: var(--txt) }\n.ntools { display: flex; gap: 2px; opacity: 0; transition: opacity .12s }\n.node:hover .ntools, .node:focus-within .ntools { opacity: 1 }\n.nn { font-size: 10px; color: var(--txt3); font-variant-numeric: tabular-nums; min-width: 12px; text-align: right }\n.add-m { top: 36px; left: auto; right: 2px; width: 170px }\n.tray-h { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 2px; color: var(--txt3);\n  font-size: 10px; font-weight: 590; text-transform: uppercase; letter-spacing: .06em }\n.tray-h .t { flex: 1; text-align: left }\n.tray-h .n { font-variant-numeric: tabular-nums }\n.tray-b { display: flex; flex-direction: column; gap: 8px; padding: 2px; border-radius: 12px; border: 1px dashed transparent }\n.tray.over .tray-b { border-color: color-mix(in srgb, var(--accent) 55%, transparent); background: color-mix(in srgb, var(--accent) 7%, transparent) }\n.drop { height: 3px; border-radius: 2px; background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent) }\n\n/* prop piece */\n.piece { position: relative; background: transparent; border: .5px solid transparent; border-radius: 10px;\n  transition: background .14s, border-color .14s, box-shadow .14s, opacity .14s }\n.piece.pop { background: var(--card); border-color: var(--card-b) }\n.ins.light .piece.pop { box-shadow: 0 1px 2px rgba(0,0,0,.06) }\n.piece.sel { box-shadow: 0 0 0 2px var(--accent) }\n.piece.cut { opacity: .42; outline: 1.5px dashed var(--accent); outline-offset: 2px }\n.piece.drag { opacity: .55 }\n.ph { position: relative; display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 5px 0 9px }\n.piece.pop.exp .ph { border-bottom: .5px solid var(--sep) }\n.pdot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: transparent;\n  box-shadow: inset 0 0 0 1.2px var(--dot); transition: background .14s, box-shadow .14s }\n.pdot:hover { box-shadow: inset 0 0 0 1.2px var(--txt3) }\n.pdot.on { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent) }\n.ptxt { display: flex; flex-direction: column; gap: 1px; min-width: 0 }\n.pt { font-size: 13px; font-weight: 640; letter-spacing: -.01em; color: var(--txt) }\n.psub { font-size: 10px; color: var(--txt3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.piece.has-sub .ph { height: 44px }\n.pspace { flex: 1 }\n.fxbadge { font-size: 8.5px; font-weight: 700; font-family: ui-monospace, \"SF Mono\", Menlo, monospace; color: var(--accent);\n  border: .5px solid color-mix(in srgb, var(--accent) 50%, transparent); border-radius: 4px; padding: 1px 3px }\n.ptools { display: flex; gap: 3px; opacity: 0; transition: opacity .12s }\n.piece:hover .ptools, .piece.sel .ptools { opacity: 1 }\n.tb { width: 21px; height: 18px; border: .5px solid var(--card-b); border-radius: 5px; font-size: 9px; font-weight: 700;\n  font-family: ui-monospace, \"SF Mono\", Menlo, monospace; color: var(--txt3) }\n.tb > svg { width: 10px; height: 10px }\n.tb:hover { color: var(--txt); background: var(--ctl2); border-color: var(--ctl-b) }\n.tb.on { color: #fff; background: var(--accent); border-color: transparent }\n.p-m { top: 30px; left: auto; right: 4px; width: 150px }\n.cat.dngr { color: #ff453a }\n.cat.dngr:hover { background: #ff453a; color: #fff }\n.fxrow { display: flex; align-items: center; gap: 7px; height: 28px; padding: 0 7px; border-radius: 7px;\n  background: color-mix(in srgb, var(--accent) 12%, transparent);\n  border: .5px solid color-mix(in srgb, var(--accent) 40%, transparent) }\n.fxk { font-size: 9.5px; font-weight: 700; font-family: ui-monospace, \"SF Mono\", Menlo, monospace; color: var(--accent); flex: none }\n.fxin { font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 10.5px }\n.pb { padding: 11px 9px 10px; display: flex; flex-direction: column; gap: 12px }\n\n/* slots */\n.slot { position: relative; display: flex; align-items: center; min-width: 0; height: 26px; padding: 0 5px;\n  background: var(--ctl); border: .5px solid var(--ctl-b); border-radius: 6px; cursor: pointer;\n  transition: box-shadow .12s, border-color .12s }\n.slot:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--ctl-b)) }\n.slot.sel { box-shadow: 0 0 0 2px var(--accent); border-color: transparent }\n.slot .cap { position: absolute; top: -9px; left: 3px; font-size: 8px; line-height: 9px; font-style: normal;\n  letter-spacing: .03em; color: var(--txt3); pointer-events: none; white-space: nowrap }\n.sin { flex: 1; min-width: 0; width: 100%; background: none; border: 0; outline: none; color: var(--txt);\n  font: inherit; font-size: 11px; user-select: text }\n.sin.num { text-align: center; font-variant-numeric: tabular-nums }\n.sin.hex { font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 10px; text-transform: uppercase }\nselect.sin { appearance: none; -webkit-appearance: none; cursor: pointer; text-align: left }\n/* the native popup inherits the select's colour — without this it is white-on-white */\nselect.sin option { background-color: #2F2F31; color: #FFFFFF }\n.ins.light select.sin option { background-color: #FFFFFF; color: #1D1D1F }\n.chip-c { width: 15px; height: 15px; border-radius: 4px; flex: none; margin-right: 5px;\n  box-shadow: inset 0 0 0 .5px rgba(0,0,0,.4), 0 0 0 .5px rgba(255,255,255,.16) }\n\n/* geometric matrices */\n.mtx { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px 6px }\n.mtx > i { display: block }\n.mtx .mid { padding: 0 4px }\n.mtx.brd { grid-template-columns: 1fr 1fr 1fr 1.3fr; grid-template-rows: repeat(3, 26px) }\n.cempty { border-radius: 6px; background: var(--track) }\n.cbig { grid-column: 4; grid-row: 1 / span 3; height: auto; padding: 0; align-items: flex-end; justify-content: center;\n  border-radius: 8px; overflow: hidden; border: .5px solid rgba(0,0,0,.32);\n  box-shadow: inset 0 0 0 .5px rgba(255,255,255,.16) }\n.hex.big { flex: none; width: calc(100% - 8px); margin: 0 4px 4px; height: 19px; text-align: center; color: #fff;\n  background: rgba(0,0,0,.34); border-radius: 5px; font-size: 9.5px; letter-spacing: .02em }\n.mtx.pad .padc { display: grid; place-items: center; font-size: 9px; color: var(--txt3); background: var(--track);\n  border-radius: 6px; letter-spacing: .04em }\n.corner.c0 { border-top-left-radius: 10px } .corner.c1 { border-top-right-radius: 10px }\n.corner.c2 { border-bottom-right-radius: 10px } .corner.c3 { border-bottom-left-radius: 10px }\n.prow { display: flex }\n.prow2 { display: flex; align-items: center; gap: 6px }\n.prow4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px }\n.fgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 6px }\n.prow .wide, .slot.wide { flex: 1; width: 100% }\n.prow2 .slot { flex: 1 } .prow2 .grow { flex: 2 }\n.chips { display: flex; flex-wrap: wrap; gap: 6px }\n.tchip { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px 0 6px; border-radius: 6px;\n  background: var(--ctl); border: .5px solid var(--ctl-b); font-size: 10.5px; color: var(--txt2); transition: all .12s }\n.tchip .tbox { width: 9px; height: 9px; border-radius: 3px; border: 1px solid var(--txt3); flex: none }\n.tchip.on { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 60%, transparent); color: var(--txt) }\n.tchip.on .tbox { background: var(--accent); border-color: var(--accent) }\n.clusrow { display: flex; gap: 8px }\n.clus { flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; padding: 3px;\n  background: var(--track); border-radius: 7px }\n.clus .ib { width: 100%; height: 21px; border-radius: 5px }\n.d-h0 svg, .al-t svg { transform: rotate(180deg) } .d-h1 svg { transform: none } .d-h2 svg { transform: none }\n.d-w0 svg { transform: rotate(90deg) } .d-w1 svg { transform: rotate(90deg) } .d-w2 svg { transform: rotate(-90deg) }\n.al-c svg { transform: rotate(90deg) } .al-r svg { transform: scaleX(-1) } .al-m svg { transform: rotate(90deg) scaleX(-1) }\n.al-b svg { transform: rotate(90deg) scaleY(-1) }\n.geo { display: flex; align-items: center; gap: 10px }\n.geo-r { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0 }\n.anc-g { display: grid; grid-template-columns: 20px 26px 20px; grid-template-rows: 16px 22px 16px; gap: 3px;\n  padding: 7px; background: var(--track); border-radius: 8px; flex: none }\n.anc-g > i { display: block }\n.anc-g button { border-radius: 3px; background: var(--dot); transition: background .14s, box-shadow .14s }\n.anc-g button:hover { background: var(--txt3) }\n.anc-g button.on { background: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent) }\n.a-top, .a-bottom { justify-self: center; align-self: center; width: 20px; height: 3px }\n.a-left, .a-right { align-self: center; justify-self: center; width: 3px; height: 16px }\n.a-c { border-radius: 5px; background: var(--ctl2); box-shadow: inset 0 0 0 .5px var(--card-b) }\n.shw { display: grid; place-items: center; height: 56px; background: var(--track); border-radius: 8px }\n.shw-p { width: 46px; height: 26px; border-radius: 6px; background: var(--ctl2) }\n\n/* clipboard bar + dock */\n.clip { flex: none; display: flex; align-items: center; gap: 7px; margin: 0 10px 8px; padding: 5px 6px 5px 8px;\n  border-radius: 9px; background: color-mix(in srgb, var(--accent) 15%, transparent);\n  border: .5px solid color-mix(in srgb, var(--accent) 45%, transparent) }\n.clip > svg { width: 13px; height: 13px; flex: none; color: var(--accent) }\n.clip .cl { font-size: 10.5px; color: var(--txt2); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }\n.clip .cn { color: var(--txt); font-weight: 590 }\n.clip .pb2 { display: inline-flex; align-items: center; gap: 4px; height: 21px; padding: 0 8px; border-radius: 6px;\n  background: var(--accent); color: #fff; font-size: 10.5px; font-weight: 590 }\n.clip .pb2 > svg { width: 11px; height: 11px }\n.dock { flex: none; display: flex; align-items: center; gap: 2px; height: 36px; padding: 0 8px;\n  border-top: .5px solid var(--sep); background: var(--bg2) }\n.dock .sp { flex: 1 }\n.dock .ib { width: 23px; height: 23px }\n.dock .ib > svg { width: 13px; height: 13px }\n.hintbar { font-size: 9.5px; color: var(--txt3); padding: 0 4px }\n.empty { padding: 26px 12px; text-align: center; color: var(--txt3); font-size: 11.5px }\n`,\n\n  render(d2) {\n    const model = d2.state.model || (d2.param('model') || DEFAULT_MODEL)\n    const tab = d2.state.tab || 'all'\n    const light = String(d2.param('appearance', 'dark')).toLowerCase() === 'light'\n    const accent = d2.param('accentHex', '')\n    const baseMap = {}\n    ;(d2.param('model') || DEFAULT_MODEL).forEach(t => (t.pieces || []).forEach(p => { baseMap[p.id] = p.v }))\n    const st = { sel: d2.state.sel, cut: d2.state.cut, drag: d2.state.drag, base: baseMap,\n      pOpen: d2.state.pOpen, menuFor: d2.state.menuFor }\n    const drop = d2.state.drop\n\n    const q = (d2.state.q || '').trim().toLowerCase()\n    const nodes = d2.state.nodes || {}\n    const trays = model.map(t => {\n      const nodeOpen = nodes[t.id] !== false\n      const pieces = (t.pieces || []).filter(p => q ? pieceMatches(p, q) : (tab === 'all' || (p.cat || 'style') === tab))\n      let inner = ''\n      pieces.forEach((p, i) => {\n        if (drop && drop.tray === t.id && drop.index === i) inner += '<div class=\"drop\"></div>'\n        inner += renderPiece(p, t, st)\n      })\n      if (drop && drop.tray === t.id && drop.index >= pieces.length) inner += '<div class=\"drop\"></div>'\n      return '<div class=\"tray' + (nodeOpen ? ' open' : '') + (drop && drop.tray === t.id ? ' over' : '') + '\" data-tray=\"' + esc(t.id) + '\">' +\n        '<div class=\"node\">' +\n        '<button class=\"node-h\" data-act=\"node\" data-tray=\"' + esc(t.id) + '\">' +\n        '<span class=\"chev\">' + I.chev + '</span><span class=\"nt\">' + esc(t.title) + '</span></button>' +\n        '<span class=\"ntools\">' +\n        '<button class=\"ib add\" data-act=\"add-open\" data-tray=\"' + esc(t.id) + '\" title=\"Add property\">' + I.plus + '</button>' +\n        '<button class=\"ib\" data-act=\"paste-tray\" data-tray=\"' + esc(t.id) + '\" title=\"Paste piece here\">' + I.paste + '</button>' +\n        '</span><span class=\"nn\">' + pieces.length + '</span>' +\n        (d2.state.addFor === t.id ? renderAddMenu(t) : '') +\n        '</div>' +\n        (nodeOpen ? '<div class=\"tray-b\">' + (inner || '<div class=\"empty\">' + (q ? 'No matches.' : 'No properties in this category.') + '</div>') + '</div>' : '') +\n        '</div>'\n    }).join('')\n\n    const clip = d2.state.clip\n    const clipBar = clip ? '<div class=\"clip\">' + I.paste +\n      '<span class=\"cl\">' + (clip.mode === 'piece'\n        ? '<span class=\"cn\">' + esc(clip.piece.title) + '</span> piece' + (d2.state.cut ? ' · cut' : ' · copied')\n        : '<span class=\"cn\">' + esc(clip.value) + '</span> ' + esc(clip.type) + ' value') + '</span>' +\n      '<button class=\"pb2\" data-act=\"paste\">' + I.paste + 'Paste</button>' +\n      '<button class=\"ib\" data-act=\"clip-clear\" title=\"Clear clipboard\">' + I.trash + '</button></div>' : ''\n\n    return '<div class=\"ins' + (light ? ' light' : '') + (d2.state.wide ? ' wide' : '') + '\"' + (accent ? ' style=\"--accent:' + esc(accent) + '\"' : '') + '>' +\n      '<div class=\"tbar\"><span class=\"t\">' + esc(d2.param('panelTitle', 'Inspector')) + '</span>' +\n      '<button class=\"ib\" data-act=\"noop\" title=\"Filter\">' + I.search + '</button>' +\n      '<button class=\"ib' + (d2.state.wide ? ' on' : '') + '\" data-act=\"expand\" title=\"' + (d2.state.wide ? 'Collapse inspector' : 'Expand inspector — second column') + '\">' + I.plus + '</button></div>' +\n\n      '<div class=\"subj\"><span class=\"tile\">' + I.cube + '</span>' +\n      '<span class=\"m\"><span class=\"n\">' + esc(d2.param('subjectTitle', 'Block')) + '</span>' +\n      '<span class=\"p\">' + esc(d2.param('subjectType', 'data2-core-primitives@Block')) + '</span></span>' +\n      '<button class=\"ib\" data-act=\"noop\" title=\"Copy all pieces\">' + I.copy + '</button></div>' +\n\n      '<div class=\"ident\">' +\n      '<div class=\"ir\"><span class=\"ik\">Component ID</span><span class=\"iv\">' + esc(d2.param('subjectId', '6a7a79932add8bd2c1a66f18')) + '</span></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Reference</span><input spellcheck=\"false\" placeholder=\"auto\"></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Display name</span><input spellcheck=\"false\" value=\"' + esc(d2.param('subjectName', '')) + '\" placeholder=\"Untitled\"></div>' +\n      '</div>' +\n\n      renderPill(model, tab, d2.state.catMode, d2.state.q || '') +\n\n      '<div class=\"body\">' + (trays.trim() ? trays : '<div class=\"empty\">' + (q ? 'No pieces match “' + esc(q) + '”.' : 'No pieces in this category.') + '</div>') + '</div>' +\n      clipBar +\n      '<div class=\"dock\">' +\n      '<button class=\"ib\" data-act=\"copy-sel\" title=\"Copy (⌘C)\">' + I.copy + '</button>' +\n      '<button class=\"ib\" data-act=\"cut-sel\" title=\"Cut (⌘X)\">' + I.cut + '</button>' +\n      '<button class=\"ib\" data-act=\"paste\" title=\"Paste (⌘V)\">' + I.paste + '</button>' +\n      '<span class=\"sp\"></span><span class=\"hintbar\">⌘C · ⌘X · ⌘V · ⌘D</span><span class=\"sp\"></span>' +\n      '<button class=\"ib\" data-act=\"reset-all\" title=\"Reset all\">' + I.undo + '</button>' +\n      '<button class=\"ib danger\" data-act=\"del-sel\" title=\"Remove selected\">' + I.trash + '</button>' +\n      '</div></div>'\n  },\n\n  mounted(d2) {\n    let clickTimer = null\n    d2.onUnmount(() => { if (clickTimer) clearTimeout(clickTimer) })\n    const base = () => d2.param('model') || DEFAULT_MODEL\n    const M = () => d2.state.model || base()\n    const commit = m => d2.setState({ model: m })\n\n    function copyFrom(sel, cut) {\n      if (!sel) return\n      const m = M()\n      if (sel.type === 'piece') {\n        const hit = findPiece(m, sel.piece)\n        if (!hit) return\n        d2.setState({ clip: { mode: 'piece', kind: hit.piece.kind, piece: clone(hit.piece) }, cut: cut ? sel.piece : null })\n      } else {\n        const hit = findPiece(m, sel.piece)\n        if (!hit) return\n        d2.setState({ clip: { mode: 'slot', type: sel.stype, value: getAt(hit.piece.v, sel.path) }, cut: null })\n      }\n    }\n\n    function paste(trayId) {\n      const clip = d2.state.clip\n      if (!clip) return\n      const m = clone(M())\n      const sel = d2.state.sel\n\n      if (clip.mode === 'slot') {\n        if (!sel || sel.type !== 'slot' || sel.stype !== clip.type) return\n        const hit = findPiece(m, sel.piece)\n        if (!hit) return\n        setAt(hit.piece.v, sel.path, clip.value)\n        commit(m)\n        return\n      }\n\n      // piece paste — onto a same-kind selection it overwrites values, otherwise it inserts a clone\n      if (!trayId && sel && sel.type === 'piece') {\n        const hit = findPiece(m, sel.piece)\n        if (hit && hit.piece.kind === clip.kind && hit.piece.id !== clip.piece.id) {\n          hit.piece.v = clone(clip.piece.v)\n          if (d2.state.cut) removeById(m, d2.state.cut)\n          commit(m)\n          d2.setState({ cut: null })\n          return\n        }\n      }\n      let target = trayId\n      if (!target && sel) { const h = findPiece(m, sel.piece); if (h) target = h.tray.id }\n      if (!target) target = m[0] && m[0].id\n      const tray = m.filter(t => t.id === target)[0]\n      if (!tray) return\n      const fresh = clone(clip.piece)\n      if (d2.state.cut) {\n        removeById(m, d2.state.cut)\n        fresh.id = clip.piece.id\n      } else {\n        fresh.id = nextId(m)\n        fresh.title = clip.piece.title\n      }\n      tray.pieces = tray.pieces || []\n      tray.pieces.push(fresh)\n      commit(m)\n      d2.setState({ cut: null, sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    function removeById(m, id) {\n      m.forEach(t => { t.pieces = (t.pieces || []).filter(p => p.id !== id) })\n    }\n\n    function duplicate(id) {\n      const m = clone(M())\n      const hit = findPiece(m, id)\n      if (!hit) return\n      const fresh = clone(hit.piece)\n      fresh.id = nextId(m)\n      hit.tray.pieces.splice(hit.pi + 1, 0, fresh)\n      commit(m)\n      d2.setState({ sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    d2.on('[data-slot]', 'pointerdown', e => {\n      const el = e.target.closest('[data-slot]')\n      if (!el) return\n      const parts = el.getAttribute('data-slot').split('|')\n      d2.setState({ sel: { type: 'slot', piece: parts[0], path: parts[1], stype: parts[2] } })\n    })\n\n    d2.on('[data-act]', 'click', e => {\n      const el = e.target.closest('[data-act]')\n      if (!el) return\n      const act = el.getAttribute('data-act')\n      const pid = el.getAttribute('data-piece')\n      if (act === 'node') {\n        const id = el.getAttribute('data-tray')\n        const nx = Object.assign({}, d2.state.nodes || {})\n        nx[id] = nx[id] === false\n        d2.setState({ nodes: nx })\n        return\n      }\n      if (act === 'add-open') {\n        const id = el.getAttribute('data-tray')\n        d2.setState({ addFor: d2.state.addFor === id ? null : id })\n        return\n      }\n      if (act === 'add-pick') {\n        const tpl = TEMPLATES[el.getAttribute('data-kind')]\n        if (!tpl) return\n        const m = clone(M())\n        const tray = m.filter(t => t.id === el.getAttribute('data-tray'))[0]\n        if (!tray) return\n        const fresh = clone(tpl)\n        fresh.id = nextId(m)\n        tray.pieces = tray.pieces || []\n        tray.pieces.push(fresh)\n        commit(m)\n        d2.setState({ addFor: null, sel: { type: 'piece', piece: fresh.id }, tab: 'all' })\n        return\n      }\n      if (act === 'cat-prev' || act === 'cat-next') {\n        const ids = TABS.map(t => t.id)\n        let i = ids.indexOf(d2.state.tab || 'all')\n        if (i < 0) i = 0\n        i = (i + (act === 'cat-next' ? 1 : -1) + ids.length) % ids.length\n        d2.setState({ tab: ids[i], catMode: null, q: '' })\n        return\n      }\n      if (act === 'cat-open') {\n        if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; return }\n        clickTimer = setTimeout(() => {\n          clickTimer = null\n          d2.setState({ catMode: d2.state.catMode === 'list' ? null : 'list' })\n        }, 210)\n        return\n      }\n      if (act === 'cat-pick') { d2.setState({ tab: el.getAttribute('data-val'), catMode: null, q: '' }); return }\n      if (act === 'cat-close') { d2.setState({ catMode: null, q: '' }); return }\n      if (act === 'expand') { d2.setState({ wide: !d2.state.wide }); return }\n      if (d2.state.menuFor && act !== 'menu') d2.setState({ menuFor: null })\n      if (act === 'select') {\n        const op = Object.assign({}, d2.state.pOpen || {})\n        op[pid] = true\n        d2.setState({ sel: { type: 'piece', piece: pid }, pOpen: op })\n        return\n      }\n      if (act === 'pexp') {\n        const hit0 = findPiece(M(), pid)\n        if (!hit0) return\n        const ref0 = hit0.piece.def !== undefined ? hit0.piece.def : (TEMPLATES[hit0.piece.kind] ? TEMPLATES[hit0.piece.kind].v : null)\n        const pop0 = !ref0 || JSON.stringify(ref0) !== JSON.stringify(hit0.piece.v)\n        const cur = d2.state.pOpen || {}\n        const isOpen = cur[pid] !== undefined ? cur[pid] : pop0\n        const nx = Object.assign({}, cur)\n        nx[pid] = !isOpen\n        d2.setState({ pOpen: nx })\n        return\n      }\n      if (act === 'menu') { d2.setState({ menuFor: d2.state.menuFor === pid ? null : pid }); return }\n      if (act === 'fx') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.v.__fx != null) delete hit.piece.v.__fx\n        else hit.piece.v.__fx = ''\n        commit(m)\n        return\n      }\n      if (act === 'paste-into') {\n        const cb = d2.state.clip\n        if (!cb || cb.mode !== 'piece') return\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit || hit.piece.kind !== cb.kind) return\n        hit.piece.v = clone(cb.piece.v)\n        commit(m)\n        return\n      }\n      if (act === 'reset-piece') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.def !== undefined) hit.piece.v = clone(hit.piece.def)\n        else {\n          let src = null\n          ;(d2.param('model') || DEFAULT_MODEL).forEach(t => (t.pieces || []).forEach(x => { if (x.id === pid) src = x }))\n          hit.piece.v = clone(src ? src.v : (TEMPLATES[hit.piece.kind] || { v: {} }).v)\n        }\n        commit(m)\n        return\n      }\n      if (act === 'copy') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, false); return }\n      if (act === 'cut') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, true); return }\n      if (act === 'dup') { duplicate(pid); return }\n      if (act === 'del') { const m = clone(M()); removeById(m, pid); commit(m); d2.setState({ sel: null }); return }\n      if (act === 'copy-sel') { copyFrom(d2.state.sel, false); return }\n      if (act === 'cut-sel') { copyFrom(d2.state.sel, true); return }\n      if (act === 'paste') { paste(null); return }\n      if (act === 'paste-tray') { paste(el.getAttribute('data-tray')); return }\n      if (act === 'clip-clear') { d2.setState({ clip: null, cut: null }); return }\n      if (act === 'del-sel') {\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') { const m = clone(M()); removeById(m, sel.piece); commit(m); d2.setState({ sel: null }) }\n        return\n      }\n      if (act === 'reset-all') { d2.setState({ model: clone(base()), sel: null, clip: null, cut: null }); return }\n      if (act === 'flag') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const path = el.getAttribute('data-path')\n        setAt(hit.piece.v, path, !getAt(hit.piece.v, path))\n        commit(m)\n        return\n      }\n      if (act === 'anchor') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const side = el.getAttribute('data-side')\n        hit.piece.v.anchor[side] = !hit.piece.v.anchor[side]\n        commit(m)\n      }\n    })\n\n    d2.on('input', 'input', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) return\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      let val = el.value\n      if (parts[2] === 'num') { const n = parseFloat(val); val = isNaN(n) ? 0 : n }\n      setAt(hit.piece.v, parts[1], val)\n      commit(m)\n    })\n\n    d2.on('select', 'change', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) return\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      setAt(hit.piece.v, parts[1], el.value)\n      commit(m)\n    })\n\n    /* drag a piece by its grip — reorder within or across trays */\n    d2.on('[data-grip]', 'pointerdown', e => {\n      const g = e.target.closest('[data-grip]')\n      if (!g) return\n      if (e.target.closest('.ptools') || e.target.closest('.pdot') || e.target.closest('.menu')) return\n      const id = g.getAttribute('data-grip')\n      let started = false\n      const startY = e.clientY\n\n      const move = ev => {\n        if (!started && Math.abs(ev.clientY - startY) < 4) return\n        if (!started) { started = true; d2.setState({ drag: id, sel: { type: 'piece', piece: id } }) }\n        const rows = d2.$$('.piece').map(el => ({\n          tray: el.getAttribute('data-tray'),\n          id: el.getAttribute('data-piece'),\n          r: el.getBoundingClientRect()\n        }))\n        let best = null\n        const wide = !!d2.state.wide\n        rows.forEach(row => {\n          if (row.id === id) return\n          const mid = row.r.top + row.r.height / 2\n          const cx = row.r.left + row.r.width / 2\n          const dy = ev.clientY - mid\n          const d = wide ? Math.sqrt((ev.clientX - cx) * (ev.clientX - cx) + dy * dy) : Math.abs(dy)\n          if (!best || d < best.d) best = { d: d, row: row, after: dy > 0 }\n        })\n        if (!best) return\n        const m = M()\n        const tray = m.filter(t => t.id === best.row.tray)[0]\n        if (!tray) return\n        let idx = (tray.pieces || []).findIndex(p => p.id === best.row.id)\n        if (best.after) idx += 1\n        const cur = d2.state.drop\n        if (!cur || cur.tray !== tray.id || cur.index !== idx) d2.setState({ drop: { tray: tray.id, index: idx } })\n      }\n\n      const up = () => {\n        window.removeEventListener('pointermove', move)\n        window.removeEventListener('pointerup', up)\n        const drop = d2.state.drop\n        if (started && drop) {\n          const m = clone(M())\n          const hit = findPiece(m, id)\n          if (hit) {\n            const moved = clone(hit.piece)\n            hit.tray.pieces.splice(hit.pi, 1)\n            const tray = m.filter(t => t.id === drop.tray)[0]\n            if (tray) {\n              let idx = drop.index\n              if (hit.tray.id === tray.id && hit.pi < idx) idx -= 1\n              tray.pieces.splice(Math.max(0, Math.min(idx, tray.pieces.length)), 0, moved)\n              commit(m)\n            }\n          }\n        }\n        d2.setState({ drag: null, drop: null })\n      }\n      window.addEventListener('pointermove', move)\n      window.addEventListener('pointerup', up)\n    })\n\n    d2.on('[data-act=\"cat-open\"]', 'dblclick', () => {\n      if (clickTimer) { clearTimeout(clickTimer); clickTimer = null }\n      d2.setState({ catMode: 'search', q: '' })\n    })\n\n    d2.on('.pq', 'input', e => { d2.setState({ q: e.target.value }) })\n\n    d2.on('.pq', 'keydown', e => {\n      if (e.key === 'Escape') { e.stopPropagation(); d2.setState({ catMode: null, q: '' }) }\n    })\n\n    d2.on(window, 'pointerdown', e => {\n      if (!d2.state.catMode && !d2.state.addFor && !d2.state.menuFor) return\n      const path = e.composedPath ? e.composedPath() : []\n      let inPill = false, inAdd = false, inMenu = false\n      for (let i = 0; i < path.length; i++) {\n        const el = path[i]\n        if (!el || !el.classList) continue\n        if (el.classList.contains('p-m') || el.classList.contains('tb')) inMenu = true\n        else if (el.classList.contains('add') || el.classList.contains('add-m')) inAdd = true\n        else if (el.classList.contains('pill') || el.classList.contains('cats')) inPill = true\n      }\n      if (d2.state.menuFor && !inMenu) d2.setState({ menuFor: null })\n      if (d2.state.addFor && !inAdd) d2.setState({ addFor: null })\n      if (!d2.state.catMode || inPill) return\n      if (d2.state.catMode === 'search' && (d2.state.q || '').trim()) return\n      d2.setState({ catMode: null })\n    })\n\n    d2.on(window, 'keydown', e => {\n      const path = e.composedPath ? e.composedPath() : []\n      const t = path[0] || e.target\n      const tag = t && t.tagName\n      if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return\n      if (!(e.metaKey || e.ctrlKey)) {\n        if (e.key === 'Escape') d2.setState({ sel: null, cut: null, catMode: null, addFor: null })\n        return\n      }\n      const k = String(e.key).toLowerCase()\n      if (k === 'c') { e.preventDefault(); copyFrom(d2.state.sel, false) }\n      else if (k === 'x') { e.preventDefault(); copyFrom(d2.state.sel, true) }\n      else if (k === 'v') { e.preventDefault(); paste(null) }\n      else if (k === 'd') {\n        e.preventDefault()\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') duplicate(sel.piece)\n      }\n    })\n  },\n\n  updated(d2) {\n    if (d2.state.catMode === 'search') {\n      if (FOCUSED.has(d2)) return\n      const el = d2.$('.pq')\n      if (el) { FOCUSED.add(d2); el.focus() }\n    } else FOCUSED.delete(d2)\n  }\n}\n"}},"panelTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panelTitle"}}}},"subjectTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectTitle"}}}},"subjectType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectType"}}}},"subjectId":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectId"}}}},"subjectName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectName"}}}},"model":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"model"}}}},"appearance":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"appearance"}}}},"accentHex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accentHex"}}}}}}}}}},{"_id":"6a7a8e4e68641501c2dbde58","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"InspectorPiecesRams","package":"lucca-office","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Inspector Pieces · Rams — same inspector recipe, Braun/Vitsoe design language\nconst ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }\nconst esc = v => String(v == null ? '' : v).replace(/[&<>\"']/g, c => ESC[c])\nconst clone = o => JSON.parse(JSON.stringify(o))\nconst FOCUSED = new WeakSet()\n\nconst I = {\n  chev: '<svg viewBox=\"0 0 8 12\" class=\"ic\"><path d=\"M2.4 1.6 6.2 6l-3.8 4.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\"/></svg>',\n  search: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"6\" cy=\"6\" r=\"4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M9.2 9.2 12.6 12.6\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  copy: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"2\" y=\"2\" width=\"7\" height=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M5 12h7V5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  cut: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"3.6\" cy=\"10.4\" r=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><circle cx=\"10.4\" cy=\"10.4\" r=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><path d=\"M4.6 8.9 10.2 1.8M9.4 8.9 3.8 1.8\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>',\n  paste: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"2.4\" y=\"2.6\" width=\"9.2\" height=\"9.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"4.8\" y=\"1.2\" width=\"4.4\" height=\"2.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  dup: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"1.6\" y=\"1.6\" width=\"7\" height=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"5.4\" y=\"5.4\" width=\"7\" height=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  trash: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3 4h8M5.6 4V2.2h2.8V4M4.2 4l.5 7.8h4.6L9.8 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  undo: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3 6.2a4.4 4.4 0 1 1 .3 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M2.4 2.6v3.6H6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  link: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M5.6 8.4h2.8M4.6 4.4H2.8v5.2h1.8M9.4 4.4h1.8v5.2H9.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  plus: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M7 2.6v8.8M2.6 7h8.8\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  x: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  dots: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><rect x=\"5.1\" y=\"1.6\" width=\"1.8\" height=\"1.8\" fill=\"currentColor\"/><rect x=\"5.1\" y=\"5.1\" width=\"1.8\" height=\"1.8\" fill=\"currentColor\"/><rect x=\"5.1\" y=\"8.6\" width=\"1.8\" height=\"1.8\" fill=\"currentColor\"/></svg>',\n  cube: '<svg viewBox=\"0 0 16 16\" class=\"ic\"><path d=\"M8 1.6 14.2 5v6L8 14.4 1.8 11V5z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><path d=\"M1.8 5 8 8.4 14.2 5M8 8.4v6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\"/></svg>',\n  arw: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.8v8.4M2.8 7 6 10.2 9.2 7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  arw2: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.4v9.2M3.4 4 6 1.4 8.6 4M3.4 8 6 10.6 8.6 8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>',\n  algn: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M1.4 1.4v9.2\" stroke=\"currentColor\" stroke-width=\"1.4\"/><rect x=\"3.8\" y=\"3.6\" width=\"6.8\" height=\"4.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>'\n}\n\nconst TABS = [\n  { id: 'all', label: 'All' },\n  { id: 'style', label: 'Style' },\n  { id: 'layout', label: 'Layout' },\n  { id: 'content', label: 'Content' },\n  { id: 'logic', label: 'Logic' }\n]\n\nconst DEFAULT_MODEL = [\n  {\n    id: 'root', title: 'Block', pieces: [\n      { id: 'p1', kind: 'border', title: 'Border', cat: 'style',\n        v: { r: [10, 10, 10, 10], w: [1, 1, 1, 1], color: '#F25C19', style: 'Solid' },\n        def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#000000', style: 'None' } },\n      { id: 'p2', kind: 'fill', title: 'Fill', cat: 'style',\n        v: { color: '#1C1C1A', opacity: 100, blend: 'Normal' },\n        def: { color: '#000000', opacity: 100, blend: 'Normal' } },\n      { id: 'p3', kind: 'fields', title: 'Opacity', cat: 'style',\n        fields: [{ path: 'value', label: 'percent', type: 'num' }],\n        v: { value: 100 }, def: { value: 100 } },\n      { id: 'p4', kind: 'shadow', title: 'Shadow', cat: 'style', sub: 'List of shadows',\n        v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' },\n        def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n      { id: 'p5', kind: 'fields', title: 'Backdrop filter', cat: 'style', sub: 'List of bitmap filters',\n        fields: [{ path: 'blur', label: 'blur', type: 'num' }, { path: 'saturate', label: 'sat', type: 'num' }],\n        v: { blur: 0, saturate: 100 }, def: { blur: 0, saturate: 100 } },\n      { id: 'p21', kind: 'fields', title: 'Filter', cat: 'style', sub: 'List of bitmap filters',\n        fields: [{ path: 'blur', label: 'blur', type: 'num' }, { path: 'grayscale', label: 'gray', type: 'num' }],\n        v: { blur: 0, grayscale: 0 }, def: { blur: 0, grayscale: 0 } },\n      { id: 'p6', kind: 'fields', title: 'Transition', cat: 'style',\n        fields: [{ path: 'ms', label: 'ms', type: 'num' }, { path: 'ease', label: 'easing', type: 'enum', options: ['Linear', 'Ease', 'Ease in', 'Ease out', 'Spring'] }],\n        v: { ms: 0, ease: 'Ease' }, def: { ms: 0, ease: 'Ease' } },\n      { id: 'p7', kind: 'geometry', title: 'Position', cat: 'layout',\n        v: { anchor: { top: true, left: true, right: false, bottom: false }, x: 1135, y: 51, growH: false, growV: false },\n        def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n      { id: 'p8', kind: 'size', title: 'Dimensions', cat: 'layout',\n        v: { w: 160, h: 540, link: false, growW: false, growH: false },\n        def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n      { id: 'p9', kind: 'spacing', title: 'Padding', cat: 'layout',\n        v: { t: 12, r: 14, b: 12, l: 14 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n      { id: 'p10', kind: 'fields', title: 'Constraints', cat: 'layout',\n        fields: [{ path: 'minW', label: 'min w', type: 'num' }, { path: 'maxW', label: 'max w', type: 'num' }, { path: 'minH', label: 'min h', type: 'num' }, { path: 'maxH', label: 'max h', type: 'num' }],\n        v: { minW: 0, maxW: 0, minH: 0, maxH: 0 }, def: { minW: 0, maxW: 0, minH: 0, maxH: 0 } },\n      { id: 'p11', kind: 'fields', title: 'Children mode', cat: 'layout',\n        fields: [{ path: 'mode', label: 'flow', type: 'enum', options: ['Absolute', 'Rows', 'Columns', 'Reversed rows', 'Reversed columns'] }],\n        v: { mode: 'Absolute' }, def: { mode: 'Absolute' } },\n      { id: 'p12', kind: 'fields', title: 'Scroll', cat: 'layout',\n        fields: [{ path: 'on', label: 'Scrollable', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p13', kind: 'fields', title: 'Text', cat: 'content',\n        fields: [{ path: 'value', label: 'content', type: 'str', ph: 'Type text' }],\n        v: { value: '' }, def: { value: '' } },\n      { id: 'p14', kind: 'fields', title: 'Typography', cat: 'content',\n        fields: [{ path: 'font', label: 'font', type: 'enum', options: ['Helvetica Neue', 'Akzidenz', 'Inter', 'Menlo'] }, { path: 'weight', label: 'weight', type: 'enum', options: ['Regular', 'Medium', 'Bold'] }, { path: 'size', label: 'size', type: 'num' }, { path: 'lh', label: 'line', type: 'num' }],\n        v: { font: 'Helvetica Neue', weight: 'Regular', size: 13, lh: 18 },\n        def: { font: 'Helvetica Neue', weight: 'Regular', size: 13, lh: 18 } },\n      { id: 'p15', kind: 'fields', title: 'Text color', cat: 'content',\n        fields: [{ path: 'color', label: '', type: 'color' }], v: { color: '#1C1C1A' }, def: { color: '#1C1C1A' } },\n      { id: 'p16', kind: 'fields', title: 'Icon', cat: 'content',\n        fields: [{ path: 'name', label: 'name', type: 'str', ph: 'fa fa-star' }], v: { name: '' }, def: { name: '' } },\n      { id: 'p17', kind: 'fields', title: 'Hide', cat: 'logic',\n        fields: [{ path: 'on', label: 'Hidden', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p18', kind: 'fields', title: 'Disabled', cat: 'logic',\n        fields: [{ path: 'on', label: 'Disabled', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p19', kind: 'fields', title: 'Children overflow', cat: 'logic',\n        fields: [{ path: 'on', label: 'Visible outside bounds', type: 'bool' }], v: { on: false }, def: { on: false } },\n      { id: 'p20', kind: 'fields', title: 'On click', cat: 'logic',\n        fields: [{ path: 'action', label: 'action', type: 'enum', options: ['None', 'Set value', 'Go to route', 'Run service', 'Open link'] }],\n        v: { action: 'None' }, def: { action: 'None' } }\n    ]\n  }\n]\n\nconst TEMPLATES = {\n  border: { kind: 'border', title: 'Border', cat: 'style', v: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#F25C19', style: 'None' }, def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#F25C19', style: 'None' } },\n  fill: { kind: 'fill', title: 'Fill', cat: 'style', v: { color: '#F25C19', opacity: 100, blend: 'Normal' }, def: { color: '#F25C19', opacity: 100, blend: 'Normal' } },\n  shadow: { kind: 'shadow', title: 'Shadow', cat: 'style', v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' }, def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n  padding: { kind: 'spacing', title: 'Padding', cat: 'layout', v: { t: 0, r: 0, b: 0, l: 0 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n  position: { kind: 'geometry', title: 'Position', cat: 'layout', v: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false }, def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n  dimensions: { kind: 'size', title: 'Dimensions', cat: 'layout', v: { w: 0, h: 0, link: false, growW: false, growH: false }, def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n  transition: { kind: 'fields', title: 'Transition', cat: 'style', fields: [{ path: 'ms', label: 'ms', type: 'num' }, { path: 'ease', label: 'easing', type: 'enum', options: ['Linear', 'Ease', 'Ease in', 'Ease out', 'Spring'] }], v: { ms: 0, ease: 'Ease' }, def: { ms: 0, ease: 'Ease' } },\n  text: { kind: 'fields', title: 'Text', cat: 'content', fields: [{ path: 'value', label: 'content', type: 'str', ph: 'Type text' }], v: { value: '' }, def: { value: '' } }\n}\n\n/* ---------- value path helpers ---------- */\nfunction getAt(v, path) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length; i++) {\n    if (cur == null) return null\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n  }\n  return cur\n}\nfunction setAt(v, path, val) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length - 1; i++) {\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n    if (cur == null) return\n  }\n  const last = parts[parts.length - 1]\n  if (Array.isArray(cur)) cur[Number(last)] = val\n  else cur[last] = val\n}\nfunction findPiece(model, id) {\n  for (let i = 0; i < model.length; i++) {\n    const ps = model[i].pieces || []\n    for (let j = 0; j < ps.length; j++) if (ps[j].id === id) return { tray: model[i], piece: ps[j], ti: i, pi: j }\n  }\n  return null\n}\nfunction nextId(model) {\n  let max = 0\n  model.forEach(t => (t.pieces || []).forEach(p => {\n    const n = parseInt(String(p.id).replace(/[^0-9]/g, ''), 10)\n    if (n > max) max = n\n  }))\n  return 'p' + (max + 1)\n}\n\n/* ---------- controls ---------- */\nfunction slot(pid, path, type, value, opts) {\n  opts = opts || {}\n  const sel = opts.sel && opts.sel.type === 'slot' && opts.sel.piece === pid && opts.sel.path === path\n  const cls = 'slot' + (sel ? ' sel' : '') + (opts.cls ? ' ' + opts.cls : '')\n  let inner = ''\n  if (type === 'color') {\n    inner = '<span class=\"chip-c\" style=\"background:' + esc(value) + '\"></span>' +\n      '<input class=\"sin hex\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + esc(value) + '\">'\n  } else if (type === 'enum') {\n    inner = '<select class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\">' +\n      (opts.options || []).map(o => '<option' + (String(o) === String(value) ? ' selected' : '') + '>' + esc(o) + '</option>').join('') + '</select>'\n  } else if (type === 'text') {\n    inner = '<input class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + esc(value) + '\" placeholder=\"' + esc(opts.ph || '') + '\">'\n  } else {\n    inner = '<input class=\"sin num\" data-in=\"' + esc(pid) + '|' + esc(path) + '|num\" inputmode=\"decimal\" spellcheck=\"false\" value=\"' + esc(value) + '\">'\n  }\n  return '<span class=\"' + cls + '\" data-slot=\"' + esc(pid) + '|' + esc(path) + '|' + type + '\">' +\n    (opts.cap ? '<i class=\"cap\">' + esc(opts.cap) + '</i>' : '') + inner + '</span>'\n}\n\nfunction rocker(pid, path, on, label) {\n  return '<span class=\"rockrow\"><button class=\"rock' + (on ? ' on' : '') + '\" data-act=\"flag\" data-piece=\"' + esc(pid) + '\" data-path=\"' + esc(path) + '\"><i></i><i></i></button>' +\n    (label ? '<span class=\"rocklbl\">' + esc(label) + '</span>' : '') + '</span>'\n}\n\n/* ---------- piece bodies ---------- */\nfunction bodyBorder(p, s) {\n  const v = p.v\n  const c = i => slot(p.id, 'r.' + i, 'num', v.r[i], { sel: s, cls: 'corner', cap: 'radius' })\n  const e = i => slot(p.id, 'w.' + i, 'num', v.w[i], { sel: s, cap: 'width' })\n  const selC = s && s.type === 'slot' && s.piece === p.id && s.path === 'color'\n  return '<div class=\"mtx brd\">' +\n    c(0) + e(0) + c(1) +\n    e(3) + '<span class=\"cempty\"></span>' + e(1) +\n    c(3) + e(2) + c(2) +\n    '<span class=\"cbig slot' + (selC ? ' sel' : '') + '\" data-slot=\"' + esc(p.id) + '|color|color\" style=\"background:' + esc(v.color) + '\">' +\n    '<input class=\"sin hex big\" data-in=\"' + esc(p.id) + '|color|text\" spellcheck=\"false\" value=\"' + esc(v.color) + '\"></span>' +\n    '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'style', 'enum', v.style, { sel: s, cls: 'wide', options: ['None', 'Solid', 'Dashed', 'Dotted'], cap: 'type' }) + '</div>'\n}\n\nfunction bodySpacing(p, s) {\n  const v = p.v\n  return '<div class=\"mtx pad\">' +\n    '<i></i>' + slot(p.id, 't', 'num', v.t, { sel: s, cap: 'top' }) + '<i></i>' +\n    slot(p.id, 'l', 'num', v.l, { sel: s, cap: 'left' }) + '<span class=\"padc\">content</span>' + slot(p.id, 'r', 'num', v.r, { sel: s, cap: 'right' }) +\n    '<i></i>' + slot(p.id, 'b', 'num', v.b, { sel: s, cap: 'bottom' }) + '<i></i>' +\n    '</div>'\n}\n\nfunction bodyGeometry(p, s) {\n  const v = p.v, a = v.anchor || {}\n  const cell = side => '<button class=\"a-' + side + (a[side] ? ' on' : '') + '\" data-act=\"anchor\" data-piece=\"' + esc(p.id) + '\" data-side=\"' + side + '\"></button>'\n  const grp = ks => '<span class=\"grp\">' + ks.map(k => '<button class=\"ib al-' + k + '\" data-act=\"noop\" title=\"Align\">' + I.algn + '</button>').join('') + '</span>'\n  return '<div class=\"geo\">' +\n    '<span class=\"anc-g\">' + '<i></i>' + cell('top') + '<i></i>' + cell('left') + '<span class=\"a-c\"></span>' + cell('right') + '<i></i>' + cell('bottom') + '<i></i>' + '</span>' +\n    '<span class=\"geo-r\">' + rocker(p.id, 'growH', v.growH, 'Grow H') + rocker(p.id, 'growV', v.growV, 'Grow V') + '</span></div>' +\n    '<div class=\"clusrow\">' + grp(['l', 'c', 'r']) + grp(['t', 'm', 'b']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y' }) + '</div>'\n}\n\nfunction bodySize(p, s) {\n  const v = p.v\n  const grp = ks => '<span class=\"grp\">' + ks.map(k => '<button class=\"ib d-' + k + '\" data-act=\"noop\" title=\"Distribute\">' + (k === 'w1' || k === 'h1' ? I.arw2 : I.arw) + '</button>').join('') + '</span>'\n  return '<div class=\"clusrow\">' + grp(['w0', 'w1', 'w2']) + grp(['h0', 'h1', 'h2']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'w', 'num', v.w, { sel: s, cap: 'W' }) +\n    '<button class=\"ib lk' + (v.link ? ' on' : '') + '\" data-act=\"flag\" data-piece=\"' + esc(p.id) + '\" data-path=\"link\">' + I.link + '</button>' +\n    slot(p.id, 'h', 'num', v.h, { sel: s, cap: 'H' }) + '</div>' +\n    '<div class=\"chips\">' + rocker(p.id, 'growW', v.growW, 'Fit width') + rocker(p.id, 'growH', v.growH, 'Fit height') + '</div>'\n}\n\nfunction bodyFill(p, s) {\n  const v = p.v\n  return '<div class=\"prow2\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'grow' }) +\n    slot(p.id, 'opacity', 'num', v.opacity, { sel: s, cap: 'percent' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'blend', 'enum', v.blend, { sel: s, cls: 'wide', options: ['Normal', 'Multiply', 'Screen', 'Overlay'], cap: 'blend' }) + '</div>'\n}\n\nfunction bodyShadow(p, s) {\n  const v = p.v\n  return '<div class=\"shw\"><span class=\"shw-p\" style=\"box-shadow:' + Number(v.x) + 'px ' + Number(v.y) + 'px ' + Number(v.blur) + 'px ' + Number(v.spread) + 'px ' + esc(v.color) + '\"></span></div>' +\n    '<div class=\"prow4\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y' }) +\n    slot(p.id, 'blur', 'num', v.blur, { sel: s, cap: 'blur' }) + slot(p.id, 'spread', 'num', v.spread, { sel: s, cap: 'sprd' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'wide' }) + '</div>'\n}\n\nfunction bodyFields(p, s) {\n  const fs = p.fields || []\n  const bools = fs.filter(f => f.type === 'bool')\n  const rest = fs.filter(f => f.type !== 'bool')\n  let out = ''\n  if (rest.length) {\n    const one = rest.length === 1\n    out += '<div class=\"' + (one ? 'prow' : 'fgrid') + '\">' + rest.map(f => {\n      const type = f.type === 'enum' ? 'enum' : f.type === 'color' ? 'color' : f.type === 'str' ? 'text' : 'num'\n      return slot(p.id, f.path, type, getAt(p.v, f.path), { sel: s, cls: one ? 'wide' : '', options: f.options, cap: f.label, ph: f.ph })\n    }).join('') + '</div>'\n  }\n  if (bools.length) out += '<div class=\"chips\">' + bools.map(f => rocker(p.id, f.path, !!getAt(p.v, f.path), f.label)).join('') + '</div>'\n  return out || '<div class=\"empty\">No editable values.</div>'\n}\n\nconst BODIES = { fields: bodyFields, border: bodyBorder, spacing: bodySpacing, geometry: bodyGeometry, size: bodySize, fill: bodyFill, shadow: bodyShadow }\n\n/* ---------- category stepper ---------- */\nfunction catCount(model, id) {\n  let n = 0\n  model.forEach(t => (t.pieces || []).forEach(p => { if (id === 'all' || (p.cat || 'style') === id) n += 1 }))\n  return n\n}\n\nfunction pieceMatches(p, q) {\n  return (p.title + ' ' + p.kind + ' ' + JSON.stringify(p.v)).toLowerCase().indexOf(q) > -1\n}\n\nfunction renderAddMenu(t) {\n  return '<div class=\"menu add-m\">' + Object.keys(TEMPLATES).map(k =>\n    '<button class=\"cat\" data-act=\"add-pick\" data-tray=\"' + esc(t.id) + '\" data-kind=\"' + esc(k) + '\">' +\n    '<span class=\"cl2\">' + esc(TEMPLATES[k].title) + '</span><span class=\"cc\">' + esc(k) + '</span></button>').join('') + '</div>'\n}\n\nfunction renderPill(model, tab, mode, q) {\n  if (mode === 'search') {\n    return '<div class=\"tabs\"><div class=\"pwrap\"><div class=\"pill act\">' +\n      '<span class=\"pglyph\">' + I.search + '</span>' +\n      '<input class=\"pq\" spellcheck=\"false\" placeholder=\"Search\" value=\"' + esc(q) + '\">' +\n      '<button class=\"parr\" data-act=\"cat-close\" title=\"Close search\">' + I.x + '</button>' +\n      '</div></div></div>'\n  }\n  let idx = TABS.map(t => t.id).indexOf(tab)\n  if (idx < 0) idx = 0\n  const open = mode === 'list'\n  return '<div class=\"tabs\"><div class=\"pwrap\">' +\n    '<div class=\"pill' + (open ? ' act' : '') + '\">' +\n    '<button class=\"parr pv\" data-act=\"cat-prev\" title=\"Previous\">' + I.chev + '</button>' +\n    '<button class=\"plabel\" data-act=\"cat-open\" title=\"Click to pick, double-click to search\">' + esc(TABS[idx].label) + '</button>' +\n    '<button class=\"parr\" data-act=\"cat-next\" title=\"Next\">' + I.chev + '</button>' +\n    '</div>' +\n    (open ? '<div class=\"menu cats\">' + TABS.map(t => '<button class=\"cat' + (t.id === tab ? ' on' : '') + '\" data-act=\"cat-pick\" data-val=\"' + t.id + '\">' +\n      '<span class=\"cl2\">' + esc(t.label) + '</span><span class=\"cc\">' + catCount(model, t.id) + '</span></button>').join('') + '</div>' : '') +\n    '</div></div>'\n}\n\n/* ---------- piece ---------- */\nfunction renderPieceMenu(p) {\n  const items = [['copy', 'Copy'], ['cut', 'Cut'], ['dup', 'Duplicate'], ['paste-into', 'Paste into'], ['reset-piece', 'Reset'], ['del', 'Remove']]\n  return '<div class=\"menu p-m\">' + items.map(it =>\n    '<button class=\"cat' + (it[0] === 'del' ? ' dngr' : '') + '\" data-act=\"' + it[0] + '\" data-piece=\"' + esc(p.id) + '\">' +\n    '<span class=\"cl2\">' + it[1] + '</span></button>').join('') + '</div>'\n}\n\nfunction fxBody(p) {\n  return '<div class=\"fxrow\"><span class=\"fxk\">f()</span>' +\n    '<input class=\"sin fxin\" data-in=\"' + esc(p.id) + '|__fx|text\" spellcheck=\"false\" value=\"' + esc(p.v.__fx) + '\" placeholder=\"Reference(&#39;brandColor&#39;)\"></div>'\n}\n\nfunction renderPiece(p, tray, st) {\n  const ref = p.def !== undefined ? p.def : (TEMPLATES[p.kind] ? TEMPLATES[p.kind].v : null)\n  const pop = !ref || JSON.stringify(ref) !== JSON.stringify(p.v)\n  const exp = st.pOpen && st.pOpen[p.id] !== undefined ? st.pOpen[p.id] : pop\n  const sel = st.sel && st.sel.type === 'piece' && st.sel.piece === p.id\n  const cut = st.cut === p.id\n  const fx = p.v && p.v.__fx != null\n  const fn = BODIES[p.kind] || bodyFields\n  return '<div class=\"piece' + (pop ? ' pop' : '') + (exp ? ' exp' : '') + (p.sub ? ' has-sub' : '') + (sel ? ' sel' : '') + (cut ? ' cut' : '') + (st.drag === p.id ? ' drag' : '') + '\" data-piece=\"' + esc(p.id) + '\" data-tray=\"' + esc(tray.id) + '\">' +\n    '<div class=\"ph\" data-act=\"select\" data-piece=\"' + esc(p.id) + '\" data-grip=\"' + esc(p.id) + '\">' +\n    '<button class=\"pdot' + (exp ? ' on' : '') + '\" data-act=\"pexp\" data-piece=\"' + esc(p.id) + '\" title=\"Expand / collapse\"></button>' +\n    '<span class=\"ptxt\"><span class=\"pt\">' + esc(p.title) + '</span>' +\n    (p.sub ? '<span class=\"psub\">' + esc(p.sub) + '</span>' : '') + '</span>' +\n    (fx ? '<span class=\"fxbadge\">f()</span>' : '') +\n    '<span class=\"pspace\"></span>' +\n    '<span class=\"ptools\">' +\n    '<button class=\"ib tb\" data-act=\"noop\" title=\"About this property\">?</button>' +\n    '<button class=\"ib tb' + (fx ? ' on' : '') + '\" data-act=\"fx\" data-piece=\"' + esc(p.id) + '\" title=\"Bind to an expression\">f()</button>' +\n    '<button class=\"ib tb\" data-act=\"menu\" data-piece=\"' + esc(p.id) + '\" title=\"More\">' + I.dots + '</button>' +\n    '</span>' +\n    (st.menuFor === p.id ? renderPieceMenu(p) : '') +\n    '</div>' +\n    (exp ? '<div class=\"pb\">' + (fx ? fxBody(p) : fn(p, st.sel)) + '</div>' : '') +\n    '</div>'\n}\n\nexport default {\n  css: `\n:host { display: block; width: 100%; height: 100% }\n:host, .ins { box-sizing: border-box }\n.ins *, .ins *::before, .ins *::after { box-sizing: border-box }\n.ins {\n  --acc: #F25C19;\n  --paper: #E5E3DE; --surf: #F1F0EC; --ink: #1C1C1A; --ink2: #6B6A66; --ink3: #9C9A94;\n  --rule: #C6C3BC; --ctl: #FCFBF9; --track: #D6D3CC; --grid: rgba(0,0,0,.10);\n  width: 300px; max-width: 100%; height: 100%; margin-left: auto; display: flex; flex-direction: column;\n  overflow: hidden; background: var(--paper); color: var(--ink); border: 1px solid var(--rule);\n  font-family: \"Helvetica Neue\", Helvetica, Arial, Inter, system-ui, sans-serif;\n  font-size: 11px; line-height: 1.35; -webkit-font-smoothing: antialiased; user-select: none;\n  transition: width .22s cubic-bezier(.4,0,.2,1);\n}\n.ins.dark {\n  --paper: #262523; --surf: #2F2E2B; --ink: #EDEBE6; --ink2: #A29F98; --ink3: #75726C;\n  --rule: #403E3B; --ctl: #383734; --track: #1D1C1B; --grid: rgba(255,255,255,.11);\n}\n.ic { width: 100%; height: 100%; display: block }\nbutton { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; border-radius: 0 }\ninput, select { border-radius: 0 }\n\n/* title bar */\n.tbar { display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 6px 0 12px; flex: none;\n  border-bottom: 1px solid var(--rule) }\n.tbar .t { font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .18em; flex: none }\n.grille { flex: 1; height: 14px; background-image: radial-gradient(circle, var(--grid) 1.1px, transparent 1.2px);\n  background-size: 5px 5px; background-position: 0 1px }\n.ib { width: 20px; height: 20px; display: grid; place-items: center; color: var(--ink2); flex: none;\n  transition: background .1s, color .1s }\n.ib > svg { width: 12px; height: 12px }\n.ib:hover { background: var(--ink); color: var(--paper) }\n.ib.on { background: var(--acc); color: #fff }\n\n/* subject */\n.subj { display: flex; align-items: center; gap: 10px; padding: 12px; border-bottom: 1px solid var(--rule) }\n.subj .tile { width: 26px; height: 26px; flex: none; display: grid; place-items: center; background: var(--ink); color: var(--paper) }\n.subj .tile > svg { width: 15px; height: 15px }\n.subj .m { min-width: 0; flex: 1; display: block }\n.subj .n { display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em;\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.subj .p { display: block; font-size: 9px; color: var(--ink2); font-family: ui-monospace, Menlo, monospace;\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px }\n\n/* identity */\n.ident { padding: 10px 12px; border-bottom: 1px solid var(--rule); display: flex; flex-direction: column; gap: 6px }\n.ir { display: flex; align-items: center; gap: 10px; min-height: 22px }\n.ik { width: 92px; flex: none; font-size: 8px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink2);\n  white-space: nowrap }\n.iv { flex: 1; min-width: 0; font-family: ui-monospace, Menlo, monospace; font-size: 10px; color: var(--ink2);\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: text }\n.ident input { flex: 1; min-width: 0; height: 22px; background: var(--ctl); border: 1px solid var(--rule);\n  color: var(--ink); font: inherit; font-size: 11px; padding: 0 6px; outline: none; user-select: text }\n.ident input:focus { border-color: var(--acc) }\n\n/* category stepper */\n.tabs { padding: 10px 12px; border-bottom: 1px solid var(--rule); position: relative; z-index: 20 }\n.pwrap { position: relative }\n.pill { display: flex; align-items: center; height: 26px; background: var(--ctl); border: 1px solid var(--rule) }\n.pill.act { border-color: var(--acc) }\n.parr { width: 26px; height: 24px; display: grid; place-items: center; color: var(--ink2); flex: none }\n.parr > svg { width: 7px; height: 10px }\n.parr:hover { background: var(--ink); color: var(--paper) }\n.parr.pv > svg { transform: rotate(180deg) }\n.plabel { flex: 1; min-width: 0; height: 24px; font-size: 9.5px; font-weight: 700; text-transform: uppercase;\n  letter-spacing: .18em; color: var(--ink) }\n.plabel:hover { background: var(--track) }\n.pglyph { width: 24px; display: grid; place-items: center; color: var(--ink2); flex: none }\n.pglyph > svg { width: 11px; height: 11px }\n.pq { flex: 1; min-width: 0; height: 24px; background: none; border: 0; outline: none; color: var(--ink);\n  font: inherit; font-size: 11px; user-select: text }\n.menu { position: absolute; left: 0; right: 0; top: 27px; z-index: 30; background: var(--surf); border: 1px solid var(--ink) }\n.cat { display: flex; align-items: center; gap: 8px; width: 100%; height: 26px; padding: 0 8px;\n  font-size: 9.5px; text-transform: uppercase; letter-spacing: .1em; color: var(--ink); border-bottom: 1px solid var(--rule) }\n.cat:last-child { border-bottom: 0 }\n.cat:hover { background: var(--acc); color: #fff }\n.cat.on { font-weight: 700 }\n.cat.on .cl2::before { content: \"\\\\2022  \" }\n.cat .cl2 { flex: 1; text-align: left }\n.cat .cc { font-size: 8.5px; color: var(--ink3); letter-spacing: .06em }\n.cat:hover .cc { color: rgba(255,255,255,.8) }\n.cat.dngr { color: #C0341A }\n.cat.dngr:hover { background: #C0341A; color: #fff }\n\n/* body */\n.body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin }\n.body::-webkit-scrollbar { width: 10px }\n.body::-webkit-scrollbar-thumb { background: var(--track); border: 4px solid var(--paper) }\n.node { position: relative; display: flex; align-items: center; gap: 4px; height: 38px; padding: 0 8px 0 12px;\n  border-bottom: 1px solid var(--rule) }\n.node-h { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; height: 100%; text-align: left }\n.node-h .chev { width: 8px; height: 11px; flex: none; color: var(--ink2); transition: transform .15s }\n.tray.open .node-h .chev { transform: rotate(90deg) }\n.nt { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .16em }\n.ntools { display: flex; gap: 2px; opacity: 0; transition: opacity .1s }\n.node:hover .ntools { opacity: 1 }\n.nn { font-size: 9px; color: var(--ink3); font-variant-numeric: tabular-nums; min-width: 14px; text-align: right }\n.add-m { top: 34px; left: auto; right: 8px; width: 168px }\n.tray-b { padding: 12px; display: flex; flex-direction: column; gap: 10px }\n.tray.over .tray-b { background: color-mix(in srgb, var(--acc) 8%, transparent) }\n.drop { height: 2px; background: var(--acc) }\n\n/* piece */\n.piece { position: relative; background: transparent; border: 1px solid transparent }\n.piece.pop { background: var(--surf); border-color: var(--rule) }\n.piece.sel { border-color: var(--acc); box-shadow: inset 0 0 0 1px var(--acc) }\n.piece.cut { opacity: .4; border-style: dashed; border-color: var(--acc) }\n.piece.drag { opacity: .5 }\n.ph { position: relative; display: flex; align-items: center; gap: 9px; height: 30px; padding: 0 6px 0 9px }\n.piece.pop.exp .ph { border-bottom: 1px solid var(--rule) }\n.piece.has-sub .ph { height: 42px }\n.pdot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: transparent; border: 1.5px solid var(--ink3) }\n.pdot:hover { border-color: var(--ink) }\n.pdot.on { background: var(--acc); border-color: var(--acc) }\n.ptxt { display: flex; flex-direction: column; gap: 2px; min-width: 0 }\n.pt { font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .12em; color: var(--ink) }\n.psub { font-size: 8px; text-transform: uppercase; letter-spacing: .1em; color: var(--ink3);\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.pspace { flex: 1 }\n.fxbadge { font-size: 8px; font-weight: 700; font-family: ui-monospace, Menlo, monospace; color: #fff;\n  background: var(--acc); padding: 2px 3px }\n.ptools { display: flex; gap: 3px; opacity: 0; transition: opacity .1s }\n.piece:hover .ptools, .piece.sel .ptools { opacity: 1 }\n.tb { width: 20px; height: 17px; border: 1px solid var(--rule); font-size: 8.5px; font-weight: 700;\n  font-family: ui-monospace, Menlo, monospace; color: var(--ink2); background: var(--ctl) }\n.tb > svg { width: 9px; height: 9px }\n.tb:hover { background: var(--ink); color: var(--paper); border-color: var(--ink) }\n.tb.on { background: var(--acc); color: #fff; border-color: var(--acc) }\n.p-m { top: 28px; left: auto; right: 6px; width: 148px }\n.pb { padding: 15px 9px 11px; display: flex; flex-direction: column; gap: 16px }\n\n/* slots */\n.slot { position: relative; display: flex; align-items: center; min-width: 0; height: 24px; padding: 0 5px;\n  background: var(--ctl); border: 1px solid var(--rule); cursor: pointer }\n.slot:hover { border-color: var(--ink2) }\n.slot.sel { border-color: var(--acc); box-shadow: inset 0 0 0 1px var(--acc) }\n.slot .cap { position: absolute; top: -12px; left: 0; font-size: 7.5px; font-style: normal; text-transform: uppercase;\n  letter-spacing: .14em; color: var(--ink3); pointer-events: none; white-space: nowrap }\n.sin { flex: 1; min-width: 0; width: 100%; background: none; border: 0; outline: none; color: var(--ink);\n  font: inherit; font-size: 10.5px; user-select: text }\n.sin.num { text-align: right; font-variant-numeric: tabular-nums; letter-spacing: .02em }\n.sin.hex { font-family: ui-monospace, Menlo, monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: .04em }\nselect.sin { appearance: none; -webkit-appearance: none; cursor: pointer; text-transform: uppercase;\n  font-size: 9px; letter-spacing: .1em }\n/* the native popup inherits the select's colour — without this it is white-on-white */\nselect.sin option { background-color: #F2F1ED; color: #1C1C1A; text-transform: none; letter-spacing: 0 }\n.ins.dark select.sin option { background-color: #2F2E2B; color: #EDEBE6 }\n.chip-c { width: 14px; height: 14px; flex: none; margin-right: 6px; border: 1px solid var(--rule) }\n\n/* matrices */\n.mtx { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px 6px }\n.mtx > i { display: block }\n.mtx.brd { grid-template-columns: 1fr 1fr 1fr 1.25fr; grid-template-rows: repeat(3, 24px) }\n.cempty { border: 1px solid var(--rule); background: var(--track) }\n.cbig { grid-column: 4; grid-row: 1 / span 3; height: auto; padding: 0; align-items: flex-end }\n.hex.big { width: 100%; height: 18px; text-align: center; color: var(--paper); background: var(--ink); padding: 0 2px }\n.mtx.pad .padc { display: grid; place-items: center; font-size: 7.5px; text-transform: uppercase; letter-spacing: .14em;\n  color: var(--ink3); border: 1px solid var(--rule); background: var(--track) }\n.prow { display: flex }\n.prow2 { display: flex; align-items: center; gap: 6px }\n.prow4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px }\n.fgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 6px }\n.prow .wide, .slot.wide { flex: 1; width: 100% }\n.prow2 .slot { flex: 1 } .prow2 .grow { flex: 2 }\n.chips { display: flex; flex-direction: column; gap: 8px }\n\n/* rocker switch */\n.rockrow { display: flex; align-items: center; gap: 8px }\n.rock { display: inline-flex; width: 32px; height: 16px; flex: none; border: 1px solid var(--rule); background: var(--ctl) }\n.rock i { flex: 1; transition: background .12s }\n.rock i:first-child { background: var(--ink3) }\n.rock.on { border-color: var(--acc) }\n.rock.on i:first-child { background: transparent }\n.rock.on i:last-child { background: var(--acc) }\n.rocklbl { font-size: 8.5px; text-transform: uppercase; letter-spacing: .12em; color: var(--ink2) }\n.rock.on ~ .rocklbl { color: var(--ink) }\n\n/* clusters */\n.d-w0 svg, .d-w1 svg { transform: rotate(90deg) }\n.d-w2 svg { transform: rotate(-90deg) }\n.d-h0 svg { transform: rotate(180deg) }\n.al-c svg { transform: rotate(90deg) }\n.al-r svg { transform: scaleX(-1) }\n.al-t svg { transform: rotate(180deg) }\n.al-m svg { transform: rotate(90deg) scaleX(-1) }\n.al-b svg { transform: rotate(90deg) scaleY(-1) }\n.clusrow { display: flex; gap: 10px }\n.grp { flex: 1; display: flex; border: 1px solid var(--rule); background: var(--ctl) }\n.grp .ib { flex: 1; width: auto; height: 21px; border-right: 1px solid var(--rule) }\n.grp .ib:last-child { border-right: 0 }\n.lk { width: 22px; height: 22px; border: 1px solid var(--rule); background: var(--ctl) }\n\n/* anchor pad */\n.geo { display: flex; align-items: center; gap: 12px }\n.geo-r { display: flex; flex-direction: column; gap: 9px; flex: 1; min-width: 0 }\n.anc-g { display: grid; grid-template-columns: 18px 24px 18px; grid-template-rows: 14px 20px 14px; gap: 4px;\n  padding: 7px; border: 1px solid var(--rule); background: var(--ctl); flex: none }\n.anc-g > i { display: block }\n.anc-g button { background: var(--track) }\n.anc-g button:hover { background: var(--ink2) }\n.anc-g button.on { background: var(--acc) }\n.a-top, .a-bottom { justify-self: center; align-self: center; width: 18px; height: 2px }\n.a-left, .a-right { align-self: center; justify-self: center; width: 2px; height: 14px }\n.a-c { border: 1px solid var(--ink3); background: transparent }\n.shw { display: grid; place-items: center; height: 54px; border: 1px solid var(--rule); background: var(--track) }\n.shw-p { width: 44px; height: 24px; background: var(--ctl); border: 1px solid var(--rule) }\n.fxrow { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 7px; border: 1px solid var(--acc);\n  background: color-mix(in srgb, var(--acc) 10%, transparent) }\n.fxk { font-size: 9px; font-weight: 700; font-family: ui-monospace, Menlo, monospace; color: var(--acc); flex: none }\n.fxin { font-family: ui-monospace, Menlo, monospace; font-size: 10px }\n.empty { padding: 18px 10px; text-align: center; color: var(--ink3); font-size: 9px; text-transform: uppercase; letter-spacing: .14em }\n\n/* wide */\n.ins.wide { width: 100% }\n.ins.wide .tray-b { display: block; columns: 2; column-gap: 12px }\n.ins.wide .tray-b .piece { break-inside: avoid; margin-bottom: 12px }\n\n/* clipboard + dock */\n.clip { flex: none; display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 10px;\n  background: var(--acc); color: #fff; border-top: 1px solid var(--rule) }\n.clip > svg { width: 12px; height: 12px; flex: none }\n.clip .cl { flex: 1; min-width: 0; font-size: 8.5px; text-transform: uppercase; letter-spacing: .12em;\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.clip .cn { font-weight: 700 }\n.clip .pb2 { height: 20px; padding: 0 9px; background: #fff; color: var(--acc); font-size: 8.5px; font-weight: 700;\n  text-transform: uppercase; letter-spacing: .12em }\n.clip .ib:hover { background: #fff; color: var(--acc) }\n.dock { flex: none; display: flex; align-items: center; height: 34px; padding: 0 6px;\n  border-top: 1px solid var(--rule); background: var(--paper) }\n.dock .sp { flex: 1 }\n.dock .ib { width: 24px; height: 22px }\n.hintbar { font-size: 8px; color: var(--ink3); text-transform: uppercase; letter-spacing: .14em; padding: 0 6px }\n`,\n\n  render(d2) {\n    const model = d2.state.model || (d2.param('model') || DEFAULT_MODEL)\n    const tab = d2.state.tab || 'all'\n    const dark = String(d2.param('appearance', 'light')).toLowerCase() === 'dark'\n    const accent = d2.param('accentHex', '')\n    const st = { sel: d2.state.sel, cut: d2.state.cut, drag: d2.state.drag, pOpen: d2.state.pOpen, menuFor: d2.state.menuFor }\n    const drop = d2.state.drop\n    const q = (d2.state.q || '').trim().toLowerCase()\n    const nodes = d2.state.nodes || {}\n\n    const trays = model.map(t => {\n      const nodeOpen = nodes[t.id] !== false\n      const pieces = (t.pieces || []).filter(p => q ? pieceMatches(p, q) : (tab === 'all' || (p.cat || 'style') === tab))\n      let inner = ''\n      pieces.forEach((p, i) => {\n        if (drop && drop.tray === t.id && drop.index === i) inner += '<div class=\"drop\"></div>'\n        inner += renderPiece(p, t, st)\n      })\n      if (drop && drop.tray === t.id && drop.index >= pieces.length) inner += '<div class=\"drop\"></div>'\n      return '<div class=\"tray' + (nodeOpen ? ' open' : '') + (drop && drop.tray === t.id ? ' over' : '') + '\" data-tray=\"' + esc(t.id) + '\">' +\n        '<div class=\"node\">' +\n        '<button class=\"node-h\" data-act=\"node\" data-tray=\"' + esc(t.id) + '\">' +\n        '<span class=\"chev\">' + I.chev + '</span><span class=\"nt\">' + esc(t.title) + '</span></button>' +\n        '<span class=\"ntools\">' +\n        '<button class=\"ib add\" data-act=\"add-open\" data-tray=\"' + esc(t.id) + '\" title=\"Add property\">' + I.plus + '</button>' +\n        '<button class=\"ib\" data-act=\"paste-tray\" data-tray=\"' + esc(t.id) + '\" title=\"Paste here\">' + I.paste + '</button>' +\n        '</span><span class=\"nn\">' + pieces.length + '</span>' +\n        (d2.state.addFor === t.id ? renderAddMenu(t) : '') +\n        '</div>' +\n        (nodeOpen ? '<div class=\"tray-b\">' + (inner || '<div class=\"empty\">' + (q ? 'No matches' : 'No properties') + '</div>') + '</div>' : '') +\n        '</div>'\n    }).join('')\n\n    const clip = d2.state.clip\n    const clipBar = clip ? '<div class=\"clip\">' + I.paste +\n      '<span class=\"cl\">' + (clip.mode === 'piece'\n        ? '<span class=\"cn\">' + esc(clip.piece.title) + '</span> ' + (d2.state.cut ? 'cut' : 'copied')\n        : '<span class=\"cn\">' + esc(clip.value) + '</span> ' + esc(clip.type)) + '</span>' +\n      '<button class=\"pb2\" data-act=\"paste\">Paste</button>' +\n      '<button class=\"ib\" data-act=\"clip-clear\" title=\"Clear\">' + I.trash + '</button></div>' : ''\n\n    return '<div class=\"ins' + (dark ? ' dark' : '') + (d2.state.wide ? ' wide' : '') + '\"' + (accent ? ' style=\"--acc:' + esc(accent) + '\"' : '') + '>' +\n      '<div class=\"tbar\"><span class=\"t\">' + esc(d2.param('panelTitle', 'Inspector')) + '</span><span class=\"grille\"></span>' +\n      '<button class=\"ib\" data-act=\"noop\" title=\"Filter\">' + I.search + '</button>' +\n      '<button class=\"ib' + (d2.state.wide ? ' on' : '') + '\" data-act=\"expand\" title=\"Expand\">' + I.plus + '</button></div>' +\n\n      '<div class=\"subj\"><span class=\"tile\">' + I.cube + '</span>' +\n      '<span class=\"m\"><span class=\"n\">' + esc(d2.param('subjectTitle', 'Block')) + '</span>' +\n      '<span class=\"p\">' + esc(d2.param('subjectType', 'data2-core-primitives@Block')) + '</span></span>' +\n      '<button class=\"ib\" data-act=\"noop\" title=\"Copy type\">' + I.copy + '</button></div>' +\n\n      '<div class=\"ident\">' +\n      '<div class=\"ir\"><span class=\"ik\">Component ID</span><span class=\"iv\">' + esc(d2.param('subjectId', '6a7a79932add8bd2c1a66f18')) + '</span></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Reference</span><input spellcheck=\"false\" placeholder=\"auto\"></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Display name</span><input spellcheck=\"false\" value=\"' + esc(d2.param('subjectName', '')) + '\" placeholder=\"Untitled\"></div>' +\n      '</div>' +\n\n      renderPill(model, tab, d2.state.catMode, d2.state.q || '') +\n      '<div class=\"body\">' + (trays.trim() ? trays : '<div class=\"empty\">No properties</div>') + '</div>' +\n      clipBar +\n      '<div class=\"dock\">' +\n      '<button class=\"ib\" data-act=\"copy-sel\" title=\"Copy\">' + I.copy + '</button>' +\n      '<button class=\"ib\" data-act=\"cut-sel\" title=\"Cut\">' + I.cut + '</button>' +\n      '<button class=\"ib\" data-act=\"paste\" title=\"Paste\">' + I.paste + '</button>' +\n      '<span class=\"sp\"></span><span class=\"hintbar\">C X V D</span><span class=\"sp\"></span>' +\n      '<button class=\"ib\" data-act=\"reset-all\" title=\"Reset all\">' + I.undo + '</button>' +\n      '<button class=\"ib\" data-act=\"del-sel\" title=\"Remove\">' + I.trash + '</button>' +\n      '</div></div>'\n  },\n\n  mounted(d2) {\n    let clickTimer = null\n    d2.onUnmount(() => { if (clickTimer) clearTimeout(clickTimer) })\n    const base = () => d2.param('model') || DEFAULT_MODEL\n    const M = () => d2.state.model || base()\n    const commit = m => d2.setState({ model: m })\n\n    function removeById(m, id) { m.forEach(t => { t.pieces = (t.pieces || []).filter(p => p.id !== id) }) }\n\n    function copyFrom(sel, cut) {\n      if (!sel) return\n      const m = M()\n      const hit = findPiece(m, sel.piece)\n      if (!hit) return\n      if (sel.type === 'piece') d2.setState({ clip: { mode: 'piece', kind: hit.piece.kind, piece: clone(hit.piece) }, cut: cut ? sel.piece : null })\n      else d2.setState({ clip: { mode: 'slot', type: sel.stype, value: getAt(hit.piece.v, sel.path) }, cut: null })\n    }\n\n    function paste(trayId) {\n      const clip = d2.state.clip\n      if (!clip) return\n      const m = clone(M())\n      const sel = d2.state.sel\n      if (clip.mode === 'slot') {\n        if (!sel || sel.type !== 'slot' || sel.stype !== clip.type) return\n        const hit = findPiece(m, sel.piece)\n        if (!hit) return\n        setAt(hit.piece.v, sel.path, clip.value)\n        commit(m)\n        return\n      }\n      if (!trayId && sel && sel.type === 'piece') {\n        const hit = findPiece(m, sel.piece)\n        if (hit && hit.piece.kind === clip.kind && hit.piece.id !== clip.piece.id) {\n          hit.piece.v = clone(clip.piece.v)\n          if (d2.state.cut) removeById(m, d2.state.cut)\n          commit(m)\n          d2.setState({ cut: null })\n          return\n        }\n      }\n      let target = trayId\n      if (!target && sel) { const h = findPiece(m, sel.piece); if (h) target = h.tray.id }\n      if (!target) target = m[0] && m[0].id\n      const tray = m.filter(t => t.id === target)[0]\n      if (!tray) return\n      const fresh = clone(clip.piece)\n      if (d2.state.cut) { removeById(m, d2.state.cut); fresh.id = clip.piece.id }\n      else fresh.id = nextId(m)\n      tray.pieces = tray.pieces || []\n      tray.pieces.push(fresh)\n      commit(m)\n      d2.setState({ cut: null, sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    function duplicate(id) {\n      const m = clone(M())\n      const hit = findPiece(m, id)\n      if (!hit) return\n      const fresh = clone(hit.piece)\n      fresh.id = nextId(m)\n      hit.tray.pieces.splice(hit.pi + 1, 0, fresh)\n      commit(m)\n      d2.setState({ sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    d2.on('[data-slot]', 'pointerdown', e => {\n      const el = e.target.closest('[data-slot]')\n      if (!el) return\n      const parts = el.getAttribute('data-slot').split('|')\n      d2.setState({ sel: { type: 'slot', piece: parts[0], path: parts[1], stype: parts[2] } })\n    })\n\n    d2.on('[data-act]', 'click', e => {\n      const el = e.target.closest('[data-act]')\n      if (!el) return\n      const act = el.getAttribute('data-act')\n      const pid = el.getAttribute('data-piece')\n      if (act === 'expand') { d2.setState({ wide: !d2.state.wide }); return }\n      if (d2.state.menuFor && act !== 'menu') d2.setState({ menuFor: null })\n      if (act === 'select') {\n        const op = Object.assign({}, d2.state.pOpen || {})\n        op[pid] = true\n        d2.setState({ sel: { type: 'piece', piece: pid }, pOpen: op })\n        return\n      }\n      if (act === 'pexp') {\n        const hit0 = findPiece(M(), pid)\n        if (!hit0) return\n        const ref0 = hit0.piece.def !== undefined ? hit0.piece.def : (TEMPLATES[hit0.piece.kind] ? TEMPLATES[hit0.piece.kind].v : null)\n        const pop0 = !ref0 || JSON.stringify(ref0) !== JSON.stringify(hit0.piece.v)\n        const cur = d2.state.pOpen || {}\n        const isOpen = cur[pid] !== undefined ? cur[pid] : pop0\n        const nx = Object.assign({}, cur)\n        nx[pid] = !isOpen\n        d2.setState({ pOpen: nx })\n        return\n      }\n      if (act === 'menu') { d2.setState({ menuFor: d2.state.menuFor === pid ? null : pid }); return }\n      if (act === 'fx') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.v.__fx != null) delete hit.piece.v.__fx\n        else hit.piece.v.__fx = ''\n        commit(m)\n        return\n      }\n      if (act === 'paste-into') {\n        const cb = d2.state.clip\n        if (!cb || cb.mode !== 'piece') return\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit || hit.piece.kind !== cb.kind) return\n        hit.piece.v = clone(cb.piece.v)\n        commit(m)\n        return\n      }\n      if (act === 'reset-piece') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.def !== undefined) hit.piece.v = clone(hit.piece.def)\n        commit(m)\n        return\n      }\n      if (act === 'node') {\n        const id = el.getAttribute('data-tray')\n        const nx = Object.assign({}, d2.state.nodes || {})\n        nx[id] = nx[id] === false\n        d2.setState({ nodes: nx })\n        return\n      }\n      if (act === 'add-open') {\n        const id = el.getAttribute('data-tray')\n        d2.setState({ addFor: d2.state.addFor === id ? null : id })\n        return\n      }\n      if (act === 'add-pick') {\n        const tpl = TEMPLATES[el.getAttribute('data-kind')]\n        if (!tpl) return\n        const m = clone(M())\n        const tray = m.filter(t => t.id === el.getAttribute('data-tray'))[0]\n        if (!tray) return\n        const fresh = clone(tpl)\n        fresh.id = nextId(m)\n        tray.pieces = tray.pieces || []\n        tray.pieces.push(fresh)\n        commit(m)\n        d2.setState({ addFor: null, sel: { type: 'piece', piece: fresh.id }, tab: 'all' })\n        return\n      }\n      if (act === 'cat-prev' || act === 'cat-next') {\n        const ids = TABS.map(t => t.id)\n        let i = ids.indexOf(d2.state.tab || 'all')\n        if (i < 0) i = 0\n        i = (i + (act === 'cat-next' ? 1 : -1) + ids.length) % ids.length\n        d2.setState({ tab: ids[i], catMode: null, q: '' })\n        return\n      }\n      if (act === 'cat-open') {\n        if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; return }\n        clickTimer = setTimeout(() => {\n          clickTimer = null\n          d2.setState({ catMode: d2.state.catMode === 'list' ? null : 'list' })\n        }, 210)\n        return\n      }\n      if (act === 'cat-pick') { d2.setState({ tab: el.getAttribute('data-val'), catMode: null, q: '' }); return }\n      if (act === 'cat-close') { d2.setState({ catMode: null, q: '' }); return }\n      if (act === 'copy') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, false); return }\n      if (act === 'cut') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, true); return }\n      if (act === 'dup') { duplicate(pid); return }\n      if (act === 'del') { const m = clone(M()); removeById(m, pid); commit(m); d2.setState({ sel: null }); return }\n      if (act === 'copy-sel') { copyFrom(d2.state.sel, false); return }\n      if (act === 'cut-sel') { copyFrom(d2.state.sel, true); return }\n      if (act === 'paste') { paste(null); return }\n      if (act === 'paste-tray') { paste(el.getAttribute('data-tray')); return }\n      if (act === 'clip-clear') { d2.setState({ clip: null, cut: null }); return }\n      if (act === 'del-sel') {\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') { const m = clone(M()); removeById(m, sel.piece); commit(m); d2.setState({ sel: null }) }\n        return\n      }\n      if (act === 'reset-all') { d2.setState({ model: clone(base()), sel: null, clip: null, cut: null }); return }\n      if (act === 'flag') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const path = el.getAttribute('data-path')\n        setAt(hit.piece.v, path, !getAt(hit.piece.v, path))\n        commit(m)\n        return\n      }\n      if (act === 'anchor') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const side = el.getAttribute('data-side')\n        hit.piece.v.anchor[side] = !hit.piece.v.anchor[side]\n        commit(m)\n      }\n    })\n\n    d2.on('input', 'input', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) {\n        if (el.classList && el.classList.contains('pq')) d2.setState({ q: el.value })\n        return\n      }\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      let val = el.value\n      if (parts[2] === 'num') { const n = parseFloat(val); val = isNaN(n) ? 0 : n }\n      setAt(hit.piece.v, parts[1], val)\n      commit(m)\n    })\n\n    d2.on('select', 'change', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) return\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      setAt(hit.piece.v, parts[1], el.value)\n      commit(m)\n    })\n\n    d2.on('[data-act=\"cat-open\"]', 'dblclick', () => {\n      if (clickTimer) { clearTimeout(clickTimer); clickTimer = null }\n      d2.setState({ catMode: 'search', q: '' })\n    })\n\n    d2.on('.pq', 'keydown', e => {\n      if (e.key === 'Escape') { e.stopPropagation(); d2.setState({ catMode: null, q: '' }) }\n    })\n\n    d2.on('[data-grip]', 'pointerdown', e => {\n      const g = e.target.closest('[data-grip]')\n      if (!g) return\n      if (e.target.closest('.ptools') || e.target.closest('.pdot') || e.target.closest('.menu')) return\n      const id = g.getAttribute('data-grip')\n      let started = false\n      const startY = e.clientY\n\n      const move = ev => {\n        if (!started && Math.abs(ev.clientY - startY) < 4) return\n        if (!started) { started = true; d2.setState({ drag: id, sel: { type: 'piece', piece: id } }) }\n        const rows = d2.$$('.piece').map(el => ({\n          tray: el.getAttribute('data-tray'),\n          id: el.getAttribute('data-piece'),\n          r: el.getBoundingClientRect()\n        }))\n        let best = null\n        const wide = !!d2.state.wide\n        rows.forEach(row => {\n          if (row.id === id) return\n          const mid = row.r.top + row.r.height / 2\n          const cx = row.r.left + row.r.width / 2\n          const dy = ev.clientY - mid\n          const d = wide ? Math.sqrt((ev.clientX - cx) * (ev.clientX - cx) + dy * dy) : Math.abs(dy)\n          if (!best || d < best.d) best = { d: d, row: row, after: dy > 0 }\n        })\n        if (!best) return\n        const m = M()\n        const tray = m.filter(t => t.id === best.row.tray)[0]\n        if (!tray) return\n        let idx = (tray.pieces || []).findIndex(p => p.id === best.row.id)\n        if (best.after) idx += 1\n        const cur = d2.state.drop\n        if (!cur || cur.tray !== tray.id || cur.index !== idx) d2.setState({ drop: { tray: tray.id, index: idx } })\n      }\n\n      const up = () => {\n        window.removeEventListener('pointermove', move)\n        window.removeEventListener('pointerup', up)\n        const drop = d2.state.drop\n        if (started && drop) {\n          const m = clone(M())\n          const hit = findPiece(m, id)\n          if (hit) {\n            const moved = clone(hit.piece)\n            hit.tray.pieces.splice(hit.pi, 1)\n            const tray = m.filter(t => t.id === drop.tray)[0]\n            if (tray) {\n              let idx = drop.index\n              if (hit.tray.id === tray.id && hit.pi < idx) idx -= 1\n              tray.pieces.splice(Math.max(0, Math.min(idx, tray.pieces.length)), 0, moved)\n              commit(m)\n            }\n          }\n        }\n        d2.setState({ drag: null, drop: null })\n      }\n      window.addEventListener('pointermove', move)\n      window.addEventListener('pointerup', up)\n    })\n\n    d2.on(window, 'pointerdown', e => {\n      if (!d2.state.catMode && !d2.state.addFor && !d2.state.menuFor) return\n      const path = e.composedPath ? e.composedPath() : []\n      let inPill = false, inAdd = false, inMenu = false\n      for (let i = 0; i < path.length; i++) {\n        const el = path[i]\n        if (!el || !el.classList) continue\n        if (el.classList.contains('p-m') || el.classList.contains('tb')) inMenu = true\n        else if (el.classList.contains('add') || el.classList.contains('add-m')) inAdd = true\n        else if (el.classList.contains('pill') || el.classList.contains('cats')) inPill = true\n      }\n      if (d2.state.menuFor && !inMenu) d2.setState({ menuFor: null })\n      if (d2.state.addFor && !inAdd) d2.setState({ addFor: null })\n      if (!d2.state.catMode || inPill) return\n      if (d2.state.catMode === 'search' && (d2.state.q || '').trim()) return\n      d2.setState({ catMode: null })\n    })\n\n    d2.on(window, 'keydown', e => {\n      const path = e.composedPath ? e.composedPath() : []\n      const t = path[0] || e.target\n      const tag = t && t.tagName\n      if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return\n      if (!(e.metaKey || e.ctrlKey)) {\n        if (e.key === 'Escape') d2.setState({ sel: null, cut: null, catMode: null, addFor: null, menuFor: null })\n        return\n      }\n      const k = String(e.key).toLowerCase()\n      if (k === 'c') { e.preventDefault(); copyFrom(d2.state.sel, false) }\n      else if (k === 'x') { e.preventDefault(); copyFrom(d2.state.sel, true) }\n      else if (k === 'v') { e.preventDefault(); paste(null) }\n      else if (k === 'd') {\n        e.preventDefault()\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') duplicate(sel.piece)\n      }\n    })\n  },\n\n  updated(d2) {\n    if (d2.state.catMode === 'search') {\n      if (FOCUSED.has(d2)) return\n      const el = d2.$('.pq')\n      if (el) { FOCUSED.add(d2); el.focus() }\n    } else FOCUSED.delete(d2)\n  }\n}\n"}},"panelTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panelTitle"}}}},"subjectTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectTitle"}}}},"subjectType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectType"}}}},"subjectId":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectId"}}}},"subjectName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectName"}}}},"model":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"model"}}}},"appearance":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"appearance"}}}},"accentHex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accentHex"}}}}}}}}}},{"_id":"6a7a9bb2a0a162269e7fe809","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"InspectorPiecesGT7","package":"lucca-office","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Inspector Pieces · GT7 — same inspector recipe, Gran Turismo 7 UI scheme\nconst ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }\nconst esc = v => String(v == null ? '' : v).replace(/[&<>\"']/g, c => ESC[c])\nconst clone = o => JSON.parse(JSON.stringify(o))\nconst FOCUSED = new WeakSet()\n\nconst I = {\n  chev: '<svg viewBox=\"0 0 8 12\" class=\"ic\"><path d=\"M2.4 1.6 6.2 6l-3.8 4.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  chev2: '<svg viewBox=\"0 0 14 12\" class=\"ic\"><path d=\"M2 1.8 6 6l-4 4.2M7.4 1.8 11.4 6l-4 4.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  caret: '<svg viewBox=\"0 0 12 8\" class=\"ic\"><path d=\"M1.6 2.2 6 6.4l4.4-4.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  tri: '<svg viewBox=\"0 0 10 7\" class=\"ic\"><path d=\"M0.6 1.2h8.8L5 6.2z\" fill=\"currentColor\"/></svg>',\n  shapes: '<svg viewBox=\"0 0 20 20\" class=\"ic\"><path d=\"M10 1.6 14.6 8.6H5.4z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/><rect x=\"1.4\" y=\"10.8\" width=\"7.6\" height=\"7.6\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><circle cx=\"14.8\" cy=\"14.6\" r=\"3.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  search: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"6.2\" cy=\"6.2\" r=\"4.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M9.4 9.4 12.4 12.4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>',\n  copy: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"2.2\" y=\"2.2\" width=\"7\" height=\"7\" rx=\"1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M4.8 11.8h5a2 2 0 0 0 2-2v-5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  cut: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><circle cx=\"3.6\" cy=\"10.4\" r=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><circle cx=\"10.4\" cy=\"10.4\" r=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/><path d=\"M4.6 8.9 10.2 1.8M9.4 8.9 3.8 1.8\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  paste: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M4.6 2.6H3.4a1.2 1.2 0 0 0-1.2 1.2v7.4a1.2 1.2 0 0 0 1.2 1.2h7.2a1.2 1.2 0 0 0 1.2-1.2V3.8a1.2 1.2 0 0 0-1.2-1.2H9.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"4.6\" y=\"1.4\" width=\"4.8\" height=\"2.6\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  dup: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"1.8\" y=\"1.8\" width=\"7\" height=\"7\" rx=\"1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"5.2\" y=\"5.2\" width=\"7\" height=\"7\" rx=\"1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  trash: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3.2 4.2h7.6M5.6 4.2V3a.8.8 0 0 1 .8-.8h1.2a.8.8 0 0 1 .8.8v1.2M4.3 4.2l.5 6.6a1 1 0 0 0 1 .9h2.4a1 1 0 0 0 1-.9l.5-6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  undo: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M3 6.2a4.4 4.4 0 1 1 .3 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M2.4 2.6v3.6H6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  link: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M5.8 8.2a2.6 2.6 0 0 0 3.7 0l1.7-1.7a2.6 2.6 0 0 0-3.7-3.7l-.9.9M8.2 5.8a2.6 2.6 0 0 0-3.7 0L2.8 7.5a2.6 2.6 0 0 0 3.7 3.7l.9-.9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  plus: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M7 2.8v8.4M2.8 7h8.4\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  x: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M3.2 3.2 8.8 8.8M8.8 3.2 3.2 8.8\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  dots: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><circle cx=\"6\" cy=\"2.5\" r=\"1.05\" fill=\"currentColor\"/><circle cx=\"6\" cy=\"6\" r=\"1.05\" fill=\"currentColor\"/><circle cx=\"6\" cy=\"9.5\" r=\"1.05\" fill=\"currentColor\"/></svg>',\n  cube: '<svg viewBox=\"0 0 16 16\" class=\"ic\"><path d=\"M8 1.8 14 5v6l-6 3.2L2 11V5z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M2 5l6 3.2L14 5M8 8.2v6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.1\" opacity=\".5\"/></svg>',\n  arw: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.8v8.4M2.8 7 6 10.2 9.2 7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  arw2: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M6 1.4v9.2M3.4 4 6 1.4 8.6 4M3.4 8 6 10.6 8.6 8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  algn: '<svg viewBox=\"0 0 12 12\" class=\"ic\"><path d=\"M1.4 1.4v9.2\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><rect x=\"3.8\" y=\"3.6\" width=\"6.8\" height=\"4.8\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>'\n}\n\n/* row icons — one per property, GT7 puts a glyph on every settings row */\nconst PI = {\n  border: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"2.6\" y=\"2.6\" width=\"12.8\" height=\"12.8\" rx=\"2.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  fill: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"2.6\" y=\"2.6\" width=\"12.8\" height=\"12.8\" rx=\"2.4\" fill=\"currentColor\" opacity=\".85\"/></svg>',\n  shadow: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"2.2\" y=\"2.2\" width=\"10\" height=\"10\" rx=\"2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M6.4 15.6h7.2a2 2 0 0 0 2-2V6.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" opacity=\".55\"/></svg>',\n  opacity: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><circle cx=\"9\" cy=\"9\" r=\"6.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M9 2.6a6.4 6.4 0 0 1 0 12.8z\" fill=\"currentColor\" opacity=\".8\"/></svg>',\n  filter: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2.4 5h13.2M2.4 9h13.2M2.4 13h13.2\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/><circle cx=\"6\" cy=\"5\" r=\"1.7\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"9\" r=\"1.7\" fill=\"currentColor\"/><circle cx=\"7.6\" cy=\"13\" r=\"1.7\" fill=\"currentColor\"/></svg>',\n  transition: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2 13.4C5.4 13.4 6.6 4.6 10 4.6c2.4 0 3.4 4.4 6 4.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>',\n  position: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M9 1.6v14.8M1.6 9h14.8\" stroke=\"currentColor\" stroke-width=\"1.2\" opacity=\".55\"/><rect x=\"6\" y=\"6\" width=\"6\" height=\"6\" rx=\"1.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  size: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2.4 2.4h5M2.4 2.4v5M15.6 15.6h-5M15.6 15.6v-5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M2.4 2.4 15.6 15.6\" stroke=\"currentColor\" stroke-width=\"1.2\" opacity=\".5\"/></svg>',\n  spacing: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"1.8\" y=\"1.8\" width=\"14.4\" height=\"14.4\" rx=\"2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" opacity=\".55\"/><rect x=\"5.4\" y=\"5.4\" width=\"7.2\" height=\"7.2\" rx=\"1.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>',\n  constraints: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M5.4 2.6H2.6v12.8h2.8M12.6 2.6h2.8v12.8h-2.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>',\n  flow: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"2.4\" y=\"2.6\" width=\"13.2\" height=\"3.4\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"2.4\" y=\"7.4\" width=\"13.2\" height=\"3.4\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"2.4\" y=\"12.2\" width=\"13.2\" height=\"3.2\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/></svg>',\n  scroll: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"4.4\" y=\"1.8\" width=\"9.2\" height=\"14.4\" rx=\"2.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M9 5.4v7.2M6.8 7.6 9 5.4l2.2 2.2M6.8 10.4 9 12.6l2.2-2.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  text: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M3.4 4.2h11.2M9 4.2v10.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>',\n  typo: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2.2 14 6.4 3.6 10.6 14M3.6 10.8h5.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M13 14V8.6a2 2 0 1 1 3 1.7V14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  tcolor: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M4 11.4 8 3.4l4 8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M5.4 8.8h5.2\" stroke=\"currentColor\" stroke-width=\"1.3\"/><rect x=\"3\" y=\"13.6\" width=\"12\" height=\"2.4\" rx=\"1.2\" fill=\"currentColor\"/></svg>',\n  icon: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M9 2.2l2 4.4 4.8.5-3.6 3.2 1 4.7L9 12.6l-4.2 2.4 1-4.7L2.2 7.1 7 6.6z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>',\n  hide: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M1.8 9S4.6 4 9 4s7.2 5 7.2 5-2.8 5-7.2 5S1.8 9 1.8 9z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><circle cx=\"9\" cy=\"9\" r=\"2.1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M3 15 15 3\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  disabled: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><circle cx=\"9\" cy=\"9\" r=\"6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M4.4 13.6 13.6 4.4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/></svg>',\n  overflow: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"2.4\" y=\"6\" width=\"9.6\" height=\"9.6\" rx=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M8.4 2.6h7v7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-dasharray=\"2 2\"/></svg>',\n  click: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M4.6 2.4 7 14.2l2.4-3.6 4.4-.6z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/></svg>',\n  rotate: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M14.8 7A6.2 6.2 0 1 0 15.2 10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M11.2 2.8h4.2V7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  transform: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M6.4 3h8.4l-3.2 12H3.2z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>',\n  light: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><circle cx=\"9\" cy=\"9\" r=\"3.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/><path d=\"M9 1.6v2M9 14.4v2M1.6 9h2M14.4 9h2M3.9 3.9l1.4 1.4M12.7 12.7l1.4 1.4M14.1 3.9l-1.4 1.4M5.3 12.7l-1.4 1.4\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  textsize: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M1.8 13.4 5.4 4.6 9 13.4M3 10.8h4.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M14 4.6v9M11.9 6.7 14 4.6l2.1 2.1M11.9 11.5 14 13.6l2.1-2.1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  format: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2.6 4.2h12.8M2.6 7.8h8.8M2.6 11.4h12.8M2.6 15h6.6\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  decoration: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M4.6 2.8v6.2a4.4 4.4 0 0 0 8.8 0V2.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\"/><path d=\"M3.6 15.2h10.8\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"/></svg>',\n  tooltip: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M2.6 4.4a1.8 1.8 0 0 1 1.8-1.8h9.2a1.8 1.8 0 0 1 1.8 1.8v5.8a1.8 1.8 0 0 1-1.8 1.8H9.6l-3.2 3.2v-3.2H4.4a1.8 1.8 0 0 1-1.8-1.8z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>',\n  states: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M9 1.8 16 5.4 9 9 2 5.4z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M2 9.4 9 13l7-3.6M2 12.8 9 16.4l7-3.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" opacity=\".55\"/></svg>',\n  event: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M10.4 1.8 4.2 10.4h4l-.6 5.8 6.2-8.6h-4z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>',\n  index: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M6.4 2.6 4.8 15.4M11.6 2.6 10 15.4M2.6 6.6h12.8M2 11.4h12.8\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  a11y: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><circle cx=\"9\" cy=\"9\" r=\"7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><circle cx=\"9\" cy=\"5.4\" r=\"1.1\" fill=\"currentColor\"/><path d=\"M5.6 7.8h6.8M9 7.8v3.2M9 11l-1.7 3.2M9 11l1.7 3.2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/></svg>',\n  mouse: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><path d=\"M5 2.6 7.4 13.6l2.2-3.4 4-.6z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/><path d=\"M2.8 15.2 15.2 2.8\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\"/></svg>',\n  modal: '<svg viewBox=\"0 0 18 18\" class=\"ic\"><rect x=\"1.8\" y=\"3.6\" width=\"14.4\" height=\"10.8\" rx=\"1.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" opacity=\".5\"/><rect x=\"5\" y=\"6.4\" width=\"8\" height=\"6.4\" rx=\"1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"/></svg>'\n}\n\n/* category glyphs — grid for All, then one per facet */\nconst CI = {\n  all: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><g fill=\"currentColor\"><rect x=\"1.5\" y=\"1.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"5.5\" y=\"1.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"9.5\" y=\"1.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"1.5\" y=\"5.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"5.5\" y=\"5.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"9.5\" y=\"5.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"1.5\" y=\"9.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"5.5\" y=\"9.5\" width=\"3\" height=\"3\" rx=\".6\"/><rect x=\"9.5\" y=\"9.5\" width=\"3\" height=\"3\" rx=\".6\"/></g></svg>',\n  style: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M7 1.5s4.3 4.5 4.3 7.1A4.3 4.3 0 0 1 2.7 8.6C2.7 6 7 1.5 7 1.5z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>',\n  layout: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><rect x=\"1.6\" y=\"2.2\" width=\"10.8\" height=\"9.6\" rx=\"1.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"/><path d=\"M5.5 2.2v9.6\" stroke=\"currentColor\" stroke-width=\"1.2\"/></svg>',\n  content: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M2.4 11.8 7 2.5l4.6 9.3M4.2 8.9h5.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>',\n  logic: '<svg viewBox=\"0 0 14 14\" class=\"ic\"><path d=\"M8.3 1.4 3.3 8.1h3.3l-.5 4.5 5-6.7H7.9z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linejoin=\"round\"/></svg>'\n}\n\nconst TABS = [\n  { id: 'all', label: 'All' },\n  { id: 'style', label: 'Style' },\n  { id: 'layout', label: 'Layout' },\n  { id: 'content', label: 'Content' },\n  { id: 'logic', label: 'Logic' }\n]\n\nconst ACTIONS = ['None', 'Set value', 'Go to route', 'Run service', 'Open link', 'Emit event']\nconst CURSORS = ['[none]', 'Default', 'Pointer', 'Text', 'Move', 'Grab', 'Not allowed', 'Crosshair']\nconst ROLES = ['[none]', 'Button', 'Link', 'Heading', 'Image', 'List', 'Navigation', 'Dialog']\nconst FLOWS = ['[none]', 'Rows', 'Columns', 'Reversed rows', 'Reversed columns']\n\nfunction ev(id, title) {\n  return { id: id, kind: 'fields', title: title, cat: 'logic', ic: 'event',\n    fields: [{ path: 'action', label: 'Action', type: 'enum', options: ACTIONS }],\n    v: { action: 'None' }, def: { action: 'None' } }\n}\nfunction lst(id, title, sub, ic, cat) {\n  return { id: id, kind: 'list', title: title, sub: sub, cat: cat, ic: ic, v: { items: [] }, def: { items: [] } }\n}\n/* simple types render their control in the row itself — no bullet, nothing to open */\nfunction bl(id, title, cat, ic, label) {\n  return { id: id, kind: 'fields', title: title, cat: cat, ic: ic, inline: true,\n    fields: [{ path: 'on', label: label || title, type: 'bool' }], v: { on: false }, def: { on: false } }\n}\nfunction nm(id, title, cat, ic, label, unit, dflt) {\n  const d = dflt == null ? 0 : dflt\n  return { id: id, kind: 'fields', title: title, cat: cat, ic: ic, inline: true,\n    fields: [{ path: 'value', label: label, type: 'num', unit: unit }], v: { value: d }, def: { value: d } }\n}\nfunction sr(id, title, cat, ic, ph) {\n  return { id: id, kind: 'fields', title: title, cat: cat, ic: ic, inline: true,\n    fields: [{ path: 'value', label: 'Value', type: 'str', ph: ph }], v: { value: '' }, def: { value: '' } }\n}\nfunction en(id, title, cat, ic, label, options) {\n  return { id: id, kind: 'fields', title: title, cat: cat, ic: ic, inline: true,\n    fields: [{ path: 'value', label: label, type: 'enum', options: options }],\n    v: { value: options[0] }, def: { value: options[0] } }\n}\n/* opt out — this prop opens a body instead */\nfunction bul(p) { delete p.inline; return p }\n\n/* the full declared surface of data2-core-primitives@Block, in inspector order */\nconst DEFAULT_MODEL = [\n  {\n    id: 'root', title: 'Block', pieces: [\n      { id: 'p1', kind: 'border', title: 'Border', cat: 'style', ic: 'border',\n        v: { r: [14, 14, 14, 14], w: [1, 1, 1, 1], color: '#E8502A', style: 'Solid' },\n        def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#000000', style: 'None' } },\n      bl('p19', 'Children Overflow', 'logic', 'overflow', 'Visible outside bounds'),\n      bl('p17', 'Hide', 'logic', 'hide', 'Hidden'),\n      { id: 'p3', kind: 'fields', title: 'Opacity', cat: 'style', ic: 'opacity',\n        fields: [{ path: 'value', label: 'Degree', type: 'slider', min: 0, max: 100, unit: '%' }],\n        v: { value: 100 }, def: { value: 100 } },\n      { id: 'p7', kind: 'geometry', title: 'Position', cat: 'layout', ic: 'position',\n        v: { anchor: { top: true, left: true, right: false, bottom: false }, x: 1135, y: 51, growH: false, growV: false },\n        def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n      { id: 'p8', kind: 'size', title: 'Dimensions', cat: 'layout', ic: 'size',\n        v: { w: 160, h: 540, link: false, growW: false, growH: false },\n        def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n      { id: 'p9', kind: 'spacing', title: 'Padding', cat: 'layout', ic: 'spacing',\n        v: { t: 12, r: 14, b: 12, l: 14 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n      { id: 'p10', kind: 'fields', title: 'Constraints', cat: 'layout', ic: 'constraints',\n        fields: [{ path: 'minW', label: 'Min width', type: 'num', unit: 'px' }, { path: 'maxW', label: 'Max width', type: 'num', unit: 'px' }, { path: 'minH', label: 'Min height', type: 'num', unit: 'px' }, { path: 'maxH', label: 'Max height', type: 'num', unit: 'px' }],\n        v: { minW: 0, maxW: 0, minH: 0, maxH: 0 }, def: { minW: 0, maxW: 0, minH: 0, maxH: 0 } },\n      { id: 'p2', kind: 'fill', title: 'Fill', cat: 'style', ic: 'fill',\n        v: { color: '#1A1A1C', opacity: 100, blend: 'Normal' },\n        def: { color: '#000000', opacity: 100, blend: 'Normal' } },\n      lst('p5', 'Backdrop Filter', 'List of BitmapFilters', 'filter', 'style'),\n      lst('p21', 'Filter', 'List of BitmapFilters', 'filter', 'style'),\n      { id: 'p4', kind: 'shadow', title: 'Shadow', cat: 'style', ic: 'shadow',\n        v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' },\n        def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n      bul(bl('p12', 'Scroll', 'layout', 'scroll', 'Scrollable')),\n      nm('p22', 'Rotate', 'style', 'rotate', 'Degrees', 'deg', 0),\n      lst('p23', 'Transitions', 'List of Transitions', 'transition', 'style'),\n      lst('p24', 'Transform', 'List of Transforms', 'transform', 'style'),\n      lst('p25', 'Light Interaction', 'Light Interaction', 'light', 'style'),\n      sr('p13', 'Text', 'content', 'text', 'Type text'),\n      nm('p26', 'Text Size', 'content', 'textsize', 'Size', 'px', 13),\n      bul(en('p14', 'Font', 'content', 'typo', 'Family', ['[none]', 'Inter', 'Helvetica Neue', 'Roboto', 'Menlo'])),\n      { id: 'p15', kind: 'fields', title: 'Text Color', cat: 'content', ic: 'tcolor',\n        fields: [{ path: 'color', label: 'Color', type: 'color' }], v: { color: '#FFFFFF' }, def: { color: '#FFFFFF' } },\n      { id: 'p27', kind: 'fields', title: 'Text Format', cat: 'content', ic: 'format',\n        fields: [{ path: 'weight', label: 'Weight', type: 'enum', options: ['Regular', 'Medium', 'Bold'] }, { path: 'align', label: 'Align', type: 'enum', options: ['Left', 'Center', 'Right', 'Justify'] }],\n        v: { weight: 'Regular', align: 'Left' }, def: { weight: 'Regular', align: 'Left' } },\n      lst('p28', 'Text Decoration', 'TextDecoration', 'decoration', 'content'),\n      sr('p29', 'Tooltip', 'content', 'tooltip', 'Tooltip text'),\n      en('p11', 'Children Mode', 'layout', 'flow', 'Flow', FLOWS),\n      lst('p30', 'State Definitions', 'List of StateDefinitions', 'states', 'logic'),\n      ev('p31', 'On Touch Start'),\n      ev('p32', 'On Touch End'),\n      en('p33', 'Cursor', 'logic', 'click', 'Cursor', CURSORS),\n      ev('p20', 'On Click'),\n      ev('p34', 'On Drag Enter'),\n      ev('p35', 'On Drag Leave'),\n      ev('p36', 'On Drop'),\n      ev('p37', 'On Click Outside'),\n      ev('p38', 'On Mouse Down'),\n      ev('p39', 'On Mouse Up'),\n      ev('p40', 'On Mouse Enter'),\n      ev('p41', 'On Mouse Leave'),\n      ev('p42', 'On Visible On Screen'),\n      ev('p43', 'On Invisible On Screen'),\n      nm('p44', 'Index', 'layout', 'index', 'Order', '', 0),\n      nm('p45', 'Tab Index', 'logic', 'index', 'Tab order', '', 0),\n      bl('p46', 'Accessibility Hide', 'logic', 'a11y', 'Hidden from AT'),\n      en('p47', 'Accessibility Role', 'logic', 'a11y', 'Role', ROLES),\n      nm('p48', 'Accessibility Level', 'logic', 'a11y', 'Level', '', 0),\n      bl('p51', 'Accessibility Modal', 'logic', 'modal', 'Modal'),\n      sr('p52', 'Accessibility Label', 'logic', 'a11y', 'Label for assistive tech'),\n      bl('p18', 'Disabled', 'logic', 'disabled', 'Disabled'),\n      bl('p49', 'Ignore Mouse Events', 'logic', 'mouse', 'Pointer events off'),\n      bl('p50', 'Prevent User Selection', 'logic', 'mouse', 'Not selectable')\n    ]\n  }\n]\n\nconst TEMPLATES = {\n  border: { kind: 'border', title: 'Border', cat: 'style', ic: 'border', v: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#E8502A', style: 'None' }, def: { r: [0, 0, 0, 0], w: [0, 0, 0, 0], color: '#E8502A', style: 'None' } },\n  fill: { kind: 'fill', title: 'Fill', cat: 'style', ic: 'fill', v: { color: '#E8502A', opacity: 100, blend: 'Normal' }, def: { color: '#E8502A', opacity: 100, blend: 'Normal' } },\n  shadow: { kind: 'shadow', title: 'Shadow', cat: 'style', ic: 'shadow', v: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' }, def: { x: 0, y: 0, blur: 0, spread: 0, color: '#000000' } },\n  padding: { kind: 'spacing', title: 'Padding', cat: 'layout', ic: 'spacing', v: { t: 0, r: 0, b: 0, l: 0 }, def: { t: 0, r: 0, b: 0, l: 0 } },\n  position: { kind: 'geometry', title: 'Position', cat: 'layout', ic: 'position', v: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false }, def: { anchor: { top: false, left: false, right: false, bottom: false }, x: 0, y: 0, growH: false, growV: false } },\n  dimensions: { kind: 'size', title: 'Dimensions', cat: 'layout', ic: 'size', v: { w: 0, h: 0, link: false, growW: false, growH: false }, def: { w: 0, h: 0, link: false, growW: false, growH: false } },\n  transitions: lst('tpl-tr', 'Transitions', 'List of Transitions', 'transition', 'style'),\n  transform: lst('tpl-tf', 'Transform', 'List of Transforms', 'transform', 'style'),\n  text: sr('tpl-tx', 'Text', 'content', 'text', 'Type text'),\n  handler: ev('tpl-ev', 'On Click')\n}\n\n/* ---------- value path helpers ---------- */\nfunction getAt(v, path) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length; i++) {\n    if (cur == null) return null\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n  }\n  return cur\n}\nfunction setAt(v, path, val) {\n  const parts = String(path).split('.')\n  let cur = v\n  for (let i = 0; i < parts.length - 1; i++) {\n    const k = parts[i]\n    cur = Array.isArray(cur) ? cur[Number(k)] : cur[k]\n    if (cur == null) return\n  }\n  const last = parts[parts.length - 1]\n  if (Array.isArray(cur)) cur[Number(last)] = val\n  else cur[last] = val\n}\nfunction findPiece(model, id) {\n  for (let i = 0; i < model.length; i++) {\n    const ps = model[i].pieces || []\n    for (let j = 0; j < ps.length; j++) if (ps[j].id === id) return { tray: model[i], piece: ps[j], ti: i, pi: j }\n  }\n  return null\n}\nfunction nextId(model) {\n  let max = 0\n  model.forEach(t => (t.pieces || []).forEach(p => {\n    const n = parseInt(String(p.id).replace(/[^0-9]/g, ''), 10)\n    if (n > max) max = n\n  }))\n  return 'p' + (max + 1)\n}\n\n/* ---------- controls ---------- */\nfunction slot(pid, path, type, value, opts) {\n  opts = opts || {}\n  const sel = opts.sel && opts.sel.type === 'slot' && opts.sel.piece === pid && opts.sel.path === path\n  const cls = 'slot' + (sel ? ' sel' : '') + (opts.cls ? ' ' + opts.cls : '')\n  let inner = ''\n  if (type === 'color') {\n    inner = '<span class=\"chip-c\" style=\"background:' + esc(value) + '\"></span>' +\n      '<input class=\"sin hex\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + esc(value) + '\">'\n  } else if (type === 'enum') {\n    inner = '<select class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\">' +\n      (opts.options || []).map(o => '<option' + (String(o) === String(value) ? ' selected' : '') + '>' + esc(o) + '</option>').join('') + '</select>' +\n      '<span class=\"selc\">' + I.tri + '</span>'\n  } else if (type === 'text') {\n    inner = '<input class=\"sin\" data-in=\"' + esc(pid) + '|' + esc(path) + '|text\" spellcheck=\"false\" value=\"' + (opts.blank ? '' : esc(value)) + '\" placeholder=\"' + esc(opts.blank ? '·' : (opts.ph || '')) + '\">'\n  } else {\n    inner = '<input class=\"sin num\" data-in=\"' + esc(pid) + '|' + esc(path) + '|num\" inputmode=\"decimal\" spellcheck=\"false\" value=\"' + (opts.blank ? '' : esc(value)) + '\" placeholder=\"' + (opts.blank ? '·' : '') + '\">' +\n      (opts.unit ? '<i class=\"unit\">' + esc(opts.unit) + '</i>' : '')\n  }\n  return '<span class=\"' + cls + '\" data-slot=\"' + esc(pid) + '|' + esc(path) + '|' + type + '\">' +\n    (opts.cap ? '<i class=\"cap\">' + esc(opts.cap) + '</i>' : '') + inner + '</span>'\n}\n\n/* radius templates — quads are [TL, TR, BR, BL] */\nconst RTPL = [\n  { n: 'Square', r: [0, 0, 0, 0] },\n  { n: 'Soft', r: [4, 4, 4, 4] },\n  { n: 'Round', r: [14, 14, 14, 14] },\n  { n: 'Pill', r: [99, 99, 99, 99] },\n  { n: 'Arch', r: [16, 16, 0, 0] },\n  { n: 'Base', r: [0, 0, 16, 16] },\n  { n: 'Leaf', r: [16, 0, 16, 0] },\n  { n: 'Wing', r: [0, 16, 0, 16] },\n  { n: 'Drop', r: [16, 16, 16, 0] },\n  { n: 'Blade', r: [16, 0, 0, 0] }\n]\nfunction radCss(q, cap) {\n  return q.map(x => Math.min(Math.max(Number(x) || 0, 0), cap) + 'px').join(' ')\n}\nfunction tplName(q) {\n  const k = (q || []).join(',')\n  for (let i = 0; i < RTPL.length; i++) if (RTPL[i].r.join(',') === k) return RTPL[i].n\n  return 'Custom'\n}\n\nfunction optsOnly(pid, path, on) {\n  return '<button class=\"tgl' + (on ? ' on' : '') + '\" role=\"switch\" aria-checked=\"' + (on ? 'true' : 'false') + '\"' +\n    ' data-act=\"flag\" data-piece=\"' + esc(pid) + '\" data-path=\"' + esc(path) + '\"><i></i></button>'\n}\n\nfunction inlineCtl(p, f, s, blank) {\n  const v = getAt(p.v, f.path)\n  if (f.type === 'bool') return optsOnly(p.id, f.path, !!v)\n  if (f.type === 'color') return slot(p.id, f.path, 'color', v, { sel: s })\n  if (f.type === 'slider') return sldRow(p, f, v)\n  const t = f.type === 'enum' ? 'enum' : f.type === 'str' ? 'text' : 'num'\n  return slot(p.id, f.path, t, v, { sel: s, cls: 'wide', options: f.options, ph: f.ph, unit: f.unit, blank: blank })\n}\n\n/* GT7 slider: 1px rail, small rectangular handle, value right-aligned */\nfunction sldRow(p, f, v) {\n  const min = f.min == null ? 0 : f.min\n  const max = f.max == null ? 100 : f.max\n  const pct = Math.max(0, Math.min(100, ((Number(v) - min) / ((max - min) || 1)) * 100))\n  return '<span class=\"sldrow\">' +\n    '<span class=\"sld\" data-sld=\"' + esc(p.id) + '|' + esc(f.path) + '|' + min + '|' + max + '\">' +\n    '<i class=\"sld-r\"></i><i class=\"sld-f\" style=\"width:' + pct + '%\"></i><i class=\"sld-h\" style=\"left:' + pct + '%\"></i></span>' +\n    '<span class=\"sld-v\">' + esc(v) + (f.unit ? '<i>' + esc(f.unit) + '</i>' : '') + '</span></span>'\n}\n\n\nfunction optbool(pid, path, on, label) {\n  return '<div class=\"brow\">' + (label ? '<span class=\"blbl\">' + esc(label) + '</span>' : '') +\n    optsOnly(pid, path, on) + '</div>'\n}\n\nfunction divider(label) {\n  return '<div class=\"dvd\"><i></i><span>' + esc(label) + '</span><i></i></div>'\n}\n\n/* ---------- piece bodies ---------- */\nfunction bodyBorder(p, s, blank, st) {\n  const v = p.v\n  const c = i => slot(p.id, 'r.' + i, 'num', v.r[i], { sel: s, cap: 'Radius' })\n  const e = i => slot(p.id, 'w.' + i, 'num', v.w[i], { sel: s, cap: 'Width' })\n  const selC = s && s.type === 'slot' && s.piece === p.id && s.path === 'color'\n  const open = st && st.tplFor === p.id\n  const cur = (v.r || []).join(',')\n  return '<div class=\"mtx brd\">' +\n    c(0) + e(0) + c(1) +\n    e(3) +\n    '<button class=\"ctpl' + (open ? ' on' : '') + '\" data-act=\"tpl-open\" data-piece=\"' + esc(p.id) + '\" title=\"Radius template\">' +\n    '<span class=\"ctpl-i\">' + I.shapes + '</span></button>' +\n    e(1) +\n    c(3) + e(2) + c(2) +\n    '<span class=\"cbig slot' + (selC ? ' sel' : '') + '\" data-slot=\"' + esc(p.id) + '|color|color\" style=\"background:' + esc(v.color) + '\">' +\n    '<input class=\"sin hex big\" data-in=\"' + esc(p.id) + '|color|text\" spellcheck=\"false\" value=\"' + esc(v.color) + '\"></span>' +\n    (open ? '<div class=\"tplm\">' +\n      '<div class=\"tplh\"><span>Radius</span><em>' + esc(tplName(v.r)) + '</em></div>' +\n      '<div class=\"tplg\">' + RTPL.map(t =>\n        '<button class=\"tplb' + (t.r.join(',') === cur ? ' on' : '') + '\" data-act=\"tpl-pick\" data-piece=\"' + esc(p.id) + '\" data-r=\"' + t.r.join(',') + '\" title=\"' + esc(t.n) + '\">' +\n        '<i style=\"border-radius:' + radCss(t.r, 10) + '\"></i><em>' + esc(t.n) + '</em></button>').join('') +\n      '</div></div>' : '') +\n    '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'style', 'enum', v.style, { sel: s, cls: 'wide', options: ['None', 'Solid', 'Dashed', 'Dotted'], cap: 'Type' }) + '</div>'\n}\n\nfunction bodySpacing(p, s) {\n  const v = p.v\n  return '<div class=\"mtx pad\">' +\n    '<i></i>' + slot(p.id, 't', 'num', v.t, { sel: s, cap: 'Top' }) + '<i></i>' +\n    slot(p.id, 'l', 'num', v.l, { sel: s, cap: 'Left' }) + '<span class=\"padc\">Content</span>' + slot(p.id, 'r', 'num', v.r, { sel: s, cap: 'Right' }) +\n    '<i></i>' + slot(p.id, 'b', 'num', v.b, { sel: s, cap: 'Bottom' }) + '<i></i>' +\n    '</div>'\n}\n\nfunction bodyGeometry(p, s) {\n  const v = p.v, a = v.anchor || {}\n  const cell = side => '<button class=\"a-' + side + (a[side] ? ' on' : '') + '\" data-act=\"anchor\" data-piece=\"' + esc(p.id) + '\" data-side=\"' + side + '\"></button>'\n  const grp = ks => '<span class=\"grp\">' + ks.map(k => '<button class=\"ib al-' + k + '\" data-act=\"noop\" title=\"Align\">' + I.algn + '</button>').join('') + '</span>'\n  return '<div class=\"geo\">' +\n    '<span class=\"anc-g\">' + '<i></i>' + cell('top') + '<i></i>' + cell('left') + '<span class=\"a-c\"></span>' + cell('right') + '<i></i>' + cell('bottom') + '<i></i>' + '</span>' +\n    '<span class=\"geo-r\">' + optbool(p.id, 'growH', v.growH, 'Grow H') + optbool(p.id, 'growV', v.growV, 'Grow V') + '</span></div>' +\n    '<div class=\"clusrow\">' + grp(['l', 'c', 'r']) + grp(['t', 'm', 'b']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X', unit: 'px' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y', unit: 'px' }) + '</div>'\n}\n\nfunction bodySize(p, s) {\n  const v = p.v\n  const grp = ks => '<span class=\"grp\">' + ks.map(k => '<button class=\"ib d-' + k + '\" data-act=\"noop\" title=\"Distribute\">' + (k === 'w1' || k === 'h1' ? I.arw2 : I.arw) + '</button>').join('') + '</span>'\n  return '<div class=\"clusrow\">' + grp(['w0', 'w1', 'w2']) + grp(['h0', 'h1', 'h2']) + '</div>' +\n    '<div class=\"prow2\">' + slot(p.id, 'w', 'num', v.w, { sel: s, cap: 'Width', unit: 'px' }) +\n    '<button class=\"ib lk' + (v.link ? ' on' : '') + '\" data-act=\"flag\" data-piece=\"' + esc(p.id) + '\" data-path=\"link\">' + I.link + '</button>' +\n    slot(p.id, 'h', 'num', v.h, { sel: s, cap: 'Height', unit: 'px' }) + '</div>' +\n    optbool(p.id, 'growW', v.growW, 'Fit width') + optbool(p.id, 'growH', v.growH, 'Fit height')\n}\n\nfunction bodyFill(p, s) {\n  const v = p.v\n  return '<div class=\"prow2\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'grow', cap: 'Color' }) +\n    slot(p.id, 'opacity', 'num', v.opacity, { sel: s, cap: 'Opacity', unit: '%' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'blend', 'enum', v.blend, { sel: s, cls: 'wide', options: ['Normal', 'Multiply', 'Screen', 'Overlay'], cap: 'Blend' }) + '</div>'\n}\n\nfunction bodyShadow(p, s) {\n  const v = p.v\n  return '<div class=\"shw\"><span class=\"shw-p\" style=\"box-shadow:' + Number(v.x) + 'px ' + Number(v.y) + 'px ' + Number(v.blur) + 'px ' + Number(v.spread) + 'px ' + esc(v.color) + '\"></span></div>' +\n    '<div class=\"prow4\">' + slot(p.id, 'x', 'num', v.x, { sel: s, cap: 'X' }) + slot(p.id, 'y', 'num', v.y, { sel: s, cap: 'Y' }) +\n    slot(p.id, 'blur', 'num', v.blur, { sel: s, cap: 'Blur' }) + slot(p.id, 'spread', 'num', v.spread, { sel: s, cap: 'Spread' }) + '</div>' +\n    '<div class=\"prow\">' + slot(p.id, 'color', 'color', v.color, { sel: s, cls: 'wide', cap: 'Color' }) + '</div>'\n}\n\nfunction bodyFields(p, s, blank) {\n  const fs = p.fields || []\n  const bools = fs.filter(f => f.type === 'bool')\n  const slides = fs.filter(f => f.type === 'slider')\n  const rest = fs.filter(f => f.type !== 'bool' && f.type !== 'slider')\n  let out = ''\n  slides.forEach(f => {\n    out += '<div class=\"brow\"><span class=\"blbl\">' + esc(f.label) + '</span>' + sldRow(p, f, getAt(p.v, f.path)) + '</div>'\n  })\n  if (rest.length) {\n    const one = rest.length === 1\n    out += '<div class=\"' + (one ? 'prow' : 'fgrid') + '\">' + rest.map(f => {\n      const type = f.type === 'enum' ? 'enum' : f.type === 'color' ? 'color' : f.type === 'str' ? 'text' : 'num'\n      return slot(p.id, f.path, type, getAt(p.v, f.path), { sel: s, cls: one ? 'wide' : '', options: f.options, cap: f.label, ph: f.ph, unit: f.unit, blank: blank })\n    }).join('') + '</div>'\n  }\n  bools.forEach(f => { out += optbool(p.id, f.path, !!getAt(p.v, f.path), f.label) })\n  return out || '<div class=\"empty\">No editable values</div>'\n}\n\nfunction bodyList(p, s) {\n  const items = p.v.items || []\n  const rows = items.map((it, i) =>\n    '<div class=\"lrow\"><span class=\"ln\">' + (i + 1) + '</span>' +\n    '<span class=\"lt\">' + esc(it.name || (p.title + ' ' + (i + 1))) + '</span>' +\n    '<button class=\"ib tb\" data-act=\"list-del\" data-piece=\"' + esc(p.id) + '\" data-idx=\"' + i + '\" title=\"Remove\">' + I.x + '</button></div>').join('')\n  return (rows || '<div class=\"empty\">Empty list</div>') +\n    '<button class=\"addrow\" data-act=\"list-add\" data-piece=\"' + esc(p.id) + '\">' + I.plus + '<span>Add</span></button>'\n}\n\nconst BODIES = { list: bodyList, fields: bodyFields, border: bodyBorder, spacing: bodySpacing, geometry: bodyGeometry, size: bodySize, fill: bodyFill, shadow: bodyShadow }\n\n/* ---------- category stepper ---------- */\nfunction catCount(model, id) {\n  let n = 0\n  model.forEach(t => (t.pieces || []).forEach(p => { if (id === 'all' || (p.cat || 'style') === id) n += 1 }))\n  return n\n}\nfunction pieceMatches(p, q) {\n  return (p.title + ' ' + p.kind + ' ' + JSON.stringify(p.v)).toLowerCase().indexOf(q) > -1\n}\nfunction renderAddMenu(t) {\n  return '<div class=\"menu add-m\">' + Object.keys(TEMPLATES).map(k =>\n    '<button class=\"cat\" data-act=\"add-pick\" data-tray=\"' + esc(t.id) + '\" data-kind=\"' + esc(k) + '\">' +\n    '<span class=\"ci\">' + (PI[TEMPLATES[k].ic] || PI.filter) + '</span>' +\n    '<span class=\"cl2\">' + esc(TEMPLATES[k].title) + '</span></button>').join('') + '</div>'\n}\nfunction renderPill(model, tab, mode, q, anim) {\n  if (mode === 'search') {\n    return '<div class=\"tabs\"><div class=\"pwrap\"><div class=\"pill act\">' +\n      '<span class=\"pglyph\">' + I.search + '</span>' +\n      '<input class=\"pq\" spellcheck=\"false\" placeholder=\"Search properties\" value=\"' + esc(q) + '\">' +\n      '<button class=\"parr\" data-act=\"cat-close\" title=\"Close search\">' + I.x + '</button>' +\n      '</div></div></div>'\n  }\n  let idx = TABS.map(t => t.id).indexOf(tab)\n  if (idx < 0) idx = 0\n  const open = mode === 'list'\n  return '<div class=\"tabs\"><div class=\"pwrap\">' +\n    '<div class=\"pill' + (open ? ' act' : '') + '\">' +\n    '<button class=\"parr pv\" data-act=\"cat-prev\" title=\"Previous\">' + I.chev + '</button>' +\n    '<button class=\"plabel ' + ((anim || 0) % 2 ? 'la' : 'lb') + '\" data-act=\"cat-open\" title=\"Click to pick, double-click to search\">' +\n    '<span class=\"pli\">' + (CI[TABS[idx].id] || '') + '</span>' + esc(TABS[idx].label) + '</button>' +\n    '<button class=\"parr\" data-act=\"cat-next\" title=\"Next\">' + I.chev + '</button>' +\n    '</div>' +\n    (open ? '<div class=\"menu cats\">' + TABS.map(t => '<button class=\"cat' + (t.id === tab ? ' on' : '') + '\" data-act=\"cat-pick\" data-val=\"' + t.id + '\">' +\n      '<span class=\"ci\">' + (CI[t.id] || '') + '</span>' +\n      '<span class=\"cl2\">' + esc(t.label) + '</span><span class=\"cc\">' + catCount(model, t.id) + '</span></button>').join('') + '</div>' : '') +\n    '</div></div>'\n}\n\n/* ---------- piece ---------- */\nfunction renderPieceMenu(p) {\n  const items = [['copy', 'Copy'], ['cut', 'Cut'], ['dup', 'Duplicate'], ['paste-into', 'Paste into'], ['reset-piece', 'Reset'], ['del', 'Remove']]\n  return '<div class=\"menu p-m\">' + items.map(it =>\n    '<button class=\"cat' + (it[0] === 'del' ? ' dngr' : '') + '\" data-act=\"' + it[0] + '\" data-piece=\"' + esc(p.id) + '\">' +\n    '<span class=\"cl2\">' + it[1] + '</span></button>').join('') + '</div>'\n}\nfunction fxBody(p) {\n  return '<div class=\"fxrow\"><span class=\"fxk\">f()</span>' +\n    '<input class=\"sin fxin\" data-in=\"' + esc(p.id) + '|__fx|text\" spellcheck=\"false\" value=\"' + esc(p.v.__fx) + '\" placeholder=\"Reference(&#39;brandColor&#39;)\"></div>'\n}\n\nfunction renderPiece(p, tray, st, i) {\n  const ref = p.def !== undefined ? p.def : (TEMPLATES[p.kind] ? TEMPLATES[p.kind].v : null)\n  const pop = !ref || JSON.stringify(ref) !== JSON.stringify(p.v)\n  const exp = st.pOpen && st.pOpen[p.id] !== undefined ? st.pOpen[p.id] : pop\n  const sel = st.sel && st.sel.type === 'piece' && st.sel.piece === p.id\n  const cut = st.cut === p.id\n  const fx = p.v && p.v.__fx != null\n  const fn = BODIES[p.kind] || bodyFields\n  const inl = p.inline && p.fields && p.fields.length === 1 ? p.fields[0] : null\n\n  if (inl) {\n    const onRight = inl.type === 'bool' || inl.type === 'color'\n    return '<div class=\"piece inl' + (pop ? ' pop' : '') + (sel ? ' sel' : '') + (cut ? ' cut' : '') + (st.drag === p.id ? ' drag' : '') + '\" data-piece=\"' + esc(p.id) + '\" data-tray=\"' + esc(tray.id) + '\" style=\"--i:' + (i || 0) + '\">' +\n      '<div class=\"ph\" data-act=\"select\" data-piece=\"' + esc(p.id) + '\" data-grip=\"' + esc(p.id) + '\">' +\n      '<span class=\"pic\">' + (PI[p.ic] || PI.filter) + '</span>' +\n      '<span class=\"ptxt\"><span class=\"pt\">' + esc(p.title) + '</span></span>' +\n      (fx ? '<span class=\"fxbadge\">f()</span>' : '') +\n      (onRight && !fx ? '<span class=\"pinl\">' + inlineCtl(p, inl, st.sel, !pop) + '</span>' : '') +\n      '<span class=\"ptools\">' +\n      '<button class=\"ib tb\" data-act=\"noop\" title=\"About this property\">?</button>' +\n      '<button class=\"ib tb' + (fx ? ' on' : '') + '\" data-act=\"fx\" data-piece=\"' + esc(p.id) + '\" title=\"Bind to an expression\">f()</button>' +\n      '<button class=\"ib tb\" data-act=\"menu\" data-piece=\"' + esc(p.id) + '\" title=\"More\">' + I.dots + '</button>' +\n      '</span>' +\n      (st.menuFor === p.id ? renderPieceMenu(p) : '') +\n      '</div>' +\n      (fx ? '<div class=\"pinrow\">' + fxBody(p) + '</div>'\n        : (onRight ? '' : '<div class=\"pinrow\">' + inlineCtl(p, inl, st.sel, !pop) + '</div>')) +\n      '</div>'\n  }\n\n  return '<div class=\"piece' + (pop ? ' pop' : '') + (exp ? ' exp' : '') + (sel ? ' sel' : '') + (cut ? ' cut' : '') + (st.drag === p.id ? ' drag' : '') + '\" data-piece=\"' + esc(p.id) + '\" data-tray=\"' + esc(tray.id) + '\" style=\"--i:' + (i || 0) + '\">' +\n    '<div class=\"ph\" data-act=\"select\" data-piece=\"' + esc(p.id) + '\" data-grip=\"' + esc(p.id) + '\">' +\n    '<button class=\"pdot' + (exp ? ' on' : '') + '\" data-act=\"pexp\" data-piece=\"' + esc(p.id) + '\" title=\"Expand / collapse\"></button>' +\n    '<span class=\"pic\">' + (PI[p.ic] || PI.filter) + '</span>' +\n    '<span class=\"ptxt\"><span class=\"pt\">' + esc(p.title) + '</span>' +\n    (p.sub ? '<span class=\"psub\">' + esc(p.sub) + '</span>' : '') + '</span>' +\n    (fx ? '<span class=\"fxbadge\">f()</span>' : '') +\n    '<span class=\"ptools\">' +\n    '<button class=\"ib tb\" data-act=\"noop\" title=\"About this property\">?</button>' +\n    '<button class=\"ib tb' + (fx ? ' on' : '') + '\" data-act=\"fx\" data-piece=\"' + esc(p.id) + '\" title=\"Bind to an expression\">f()</button>' +\n    '<button class=\"ib tb\" data-act=\"menu\" data-piece=\"' + esc(p.id) + '\" title=\"More\">' + I.dots + '</button>' +\n    '</span>' +\n    (st.menuFor === p.id ? renderPieceMenu(p) : '') +\n    '</div>' +\n    '<div class=\"pbwrap\"' + (exp ? '' : ' data-collapsed=\"1\"') + '><div class=\"pbi\"><div class=\"pb\">' +\n    (fx ? fxBody(p) : fn(p, st.sel, !pop, st)) + '</div></div></div>' +\n    '</div>'\n}\n\nexport default {\n  css: `\n:host { display: block; width: 100%; height: 100% }\n:host, .ins { box-sizing: border-box }\n.ins *, .ins *::before, .ins *::after { box-sizing: border-box }\n.ins {\n  --acc: #E8502A;\n  --bg: #0C0C0D; --bg2: #141416; --card: rgba(255,255,255,.045); --edge: rgba(255,255,255,.09);\n  --rule: rgba(255,255,255,.085); --txt: #FFFFFF; --txt2: rgba(255,255,255,.58); --txt3: rgba(255,255,255,.34);\n  --ctl: rgba(255,255,255,.06); --ctl2: rgba(255,255,255,.12); --focus: rgba(255,255,255,.6);\n  --well: rgba(0,0,0,.45); --ok: #3ED77A;\n  --strack: rgba(0,0,0,.55); --sbar: #6B6B70; --sbar2: #87878D; --tglo: rgba(255,255,255,.17);\n  width: 300px; max-width: 100%; height: 100%; margin-left: auto; display: flex; flex-direction: column;\n  overflow: hidden; border-radius: 12px; background: var(--bg); color: var(--txt);\n  border: 1px solid var(--edge);\n  font-family: Inter, \"Helvetica Neue\", Helvetica, Arial, system-ui, sans-serif;\n  font-size: 12px; line-height: 1.35; -webkit-font-smoothing: antialiased; user-select: none;\n  transition: width .24s cubic-bezier(.4,0,.2,1);\n}\n.ins.light {\n  --acc: #D8431F;\n  --bg: #DCDCDE; --bg2: #EDEDEF; --card: rgba(255,255,255,.72); --edge: rgba(0,0,0,.13);\n  --rule: rgba(0,0,0,.10); --txt: #17171A; --txt2: rgba(0,0,0,.56); --txt3: rgba(0,0,0,.34);\n  --ctl: rgba(255,255,255,.8); --ctl2: rgba(0,0,0,.07); --focus: rgba(0,0,0,.5);\n  --well: rgba(255,255,255,.92); --ok: #1E9E52;\n  --strack: rgba(0,0,0,.13); --sbar: #9A9AA0; --sbar2: #7E7E85; --tglo: rgba(0,0,0,.19);\n}\n.ic { width: 100%; height: 100%; display: block }\nbutton { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer }\n.ib { width: 24px; height: 24px; border-radius: 999px; display: grid; place-items: center; color: var(--txt2);\n  flex: none; border: 1px solid transparent; transition: background .12s, color .12s, border-color .12s }\n.ib > svg { width: 13px; height: 13px }\n.ib:hover { background: var(--ctl2); color: var(--txt); border-color: var(--edge) }\n.ib.on { background: var(--acc); color: #fff; border-color: transparent }\n\n/* title bar */\n.tbar { display: flex; align-items: center; gap: 4px; height: 40px; padding: 0 8px 0 14px; flex: none;\n  border-bottom: 1px solid var(--rule) }\n.tbar .t { font-size: 13px; font-weight: 600; letter-spacing: .01em; flex: 1 }\n\n/* subject — GT7 car card */\n.subj { display: flex; align-items: center; gap: 10px; margin: 10px; padding: 10px; border-radius: 10px;\n  background: var(--bg2); border: 1px solid var(--rule) }\n.subj .tile { width: 30px; height: 30px; border-radius: 8px; flex: none; display: grid; place-items: center;\n  background: var(--ctl); color: var(--txt) }\n.subj .tile > svg { width: 17px; height: 17px }\n.subj .m { min-width: 0; flex: 1; display: block }\n.subj .n { display: block; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.subj .p { display: block; font-size: 10px; color: var(--txt2); font-family: ui-monospace, Menlo, monospace;\n  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px }\n.pp { flex: none; height: 22px; padding: 0 9px; border-radius: 999px; background: var(--txt); color: var(--bg);\n  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; display: grid; place-items: center;\n  font-variant-numeric: tabular-nums; white-space: nowrap }\n\n/* identity — GT7 spec rows */\n.ident { padding: 0 14px 4px }\n.ir { display: flex; align-items: center; gap: 10px; min-height: 30px; border-bottom: 1px solid var(--rule) }\n.ir:last-child { border-bottom: 0 }\n.ik { flex: 1; font-size: 11.5px; color: var(--txt2) }\n.iv { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; color: var(--txt);\n  max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; user-select: text }\n.ident input { width: 148px; height: 24px; background: var(--ctl); border: 1px solid var(--rule); border-radius: 6px;\n  color: var(--txt); font: inherit; font-size: 11.5px; padding: 0 7px; outline: none; text-align: right; user-select: text }\n.ident input:focus { border-color: var(--focus) }\n\n/* category stepper */\n.tabs { padding: 8px 10px 10px; position: relative; z-index: 20 }\n.pwrap { position: relative }\n.pill { display: flex; align-items: center; height: 32px; border-radius: 999px; background: var(--ctl);\n  border: 1px solid var(--rule); transition: border-color .14s, background .14s }\n.pill:hover { border-color: var(--edge) }\n.pill.act { border-color: var(--focus); background: var(--ctl2) }\n.parr { width: 30px; height: 30px; border-radius: 999px; display: grid; place-items: center; color: var(--txt2); flex: none }\n.parr > svg { width: 7px; height: 11px }\n.parr:hover { color: var(--txt); background: var(--ctl2) }\n.parr.pv > svg { transform: rotate(180deg) }\n.plabel { flex: 1; min-width: 0; height: 30px; border-radius: 999px; font-size: 12.5px; font-weight: 500;\n  color: var(--txt); text-transform: lowercase; display: flex; align-items: center; justify-content: center; gap: 8px }\n.pli { width: 13px; height: 13px; flex: none; color: var(--txt2); transition: color .14s var(--ez) }\n.plabel:hover .pli { color: var(--txt) }\n.cat.on .ci { color: var(--acc) }\n.cat:hover .ci { color: #fff }\n.plabel:hover { background: var(--ctl2) }\n.pglyph { width: 28px; display: grid; place-items: center; color: var(--txt3); flex: none }\n.pglyph > svg { width: 12px; height: 12px }\n.pq { flex: 1; min-width: 0; height: 30px; background: none; border: 0; outline: none; color: var(--txt);\n  font: inherit; font-size: 12px; user-select: text }\n.menu { position: absolute; left: 0; right: 0; top: 36px; z-index: 30; padding: 4px; border-radius: 10px;\n  background: var(--bg2); border: 1px solid var(--edge); box-shadow: 0 14px 34px rgba(0,0,0,.5) }\n.ins.light .menu { box-shadow: 0 14px 34px rgba(0,0,0,.18) }\n.cat { display: flex; align-items: center; gap: 8px; width: 100%; height: 30px; padding: 0 8px; border-radius: 7px;\n  font-size: 12px; color: var(--txt); border: 1px solid transparent }\n.cat:hover { background: var(--ctl2); border-color: var(--focus) }\n.cat.on { color: var(--acc); font-weight: 600 }\n.cat .ci { width: 15px; height: 15px; flex: none; color: var(--txt2) }\n.cat .cl2 { flex: 1; text-align: left }\n.cat .cc { font-size: 10.5px; color: var(--txt3); font-variant-numeric: tabular-nums }\n.cat.dngr { color: #FF5A4E }\n\n/* body */\n.body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 0 0 14px }\n.body::-webkit-scrollbar { width: 9px; background: var(--strack) }\n.body::-webkit-scrollbar-track { background: var(--strack) }\n.body::-webkit-scrollbar-thumb { background: var(--sbar); border-radius: 0; border: 0 }\n.body::-webkit-scrollbar-thumb:hover { background: var(--sbar2) }\n.body::-webkit-scrollbar-corner { background: var(--strack) }\n\n/* node — GT7 centred vermilion section header */\n.node { position: relative; display: flex; align-items: center; justify-content: center; height: 38px; padding: 0 12px }\n.node-h { display: flex; align-items: center; gap: 8px; height: 100%; padding: 0 6px }\n.node-h .chev { width: 11px; height: 8px; color: var(--acc); transition: transform .16s }\n.tray:not(.open) .node-h .chev { transform: rotate(-90deg) }\n.nt { font-size: 11px; font-weight: 700; color: var(--acc); letter-spacing: .06em }\n.nrule { position: absolute; top: 50%; height: 1px; background: var(--rule); width: 26% }\n.nrule.l { left: 12px } .nrule.r { right: 12px }\n.ntools { position: absolute; right: 10px; display: flex; gap: 2px; opacity: 0; transition: opacity .12s }\n.node:hover .ntools { opacity: 1 }\n.nn { position: absolute; right: 12px; font-size: 10px; color: var(--txt3); font-variant-numeric: tabular-nums }\n.node:hover .nn { opacity: 0 }\n.add-m { top: 34px; left: auto; right: 8px; width: 176px }\n.tray-b { padding: 0 10px }\n.tray.over { background: rgba(232,80,42,.07) }\n.drop { height: 2px; background: var(--acc); border-radius: 1px; margin: 3px 0 }\n\n/* piece — GT7 settings row */\n.piece { position: relative; border-radius: 9px; border: 1px solid transparent; background: transparent;\n  transition: background .14s, border-color .14s }\n.piece + .piece { margin-top: 4px }\n.piece.pop { background: var(--card) }\n.piece.sel { border-color: var(--focus); background: var(--ctl2) }\n.piece.cut { opacity: .4; border-style: dashed; border-color: var(--acc) }\n.piece.drag { opacity: .5 }\n.ph { position: relative; display: flex; align-items: center; gap: 9px; height: 40px; padding: 0 10px 0 9px }\n.piece.has-sub .ph { height: 48px }\n.piece.pop.exp .ph { border-bottom: 1px solid var(--rule) }\n.pdot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: transparent;\n  box-shadow: inset 0 0 0 1.4px var(--txt3); transition: background .14s, box-shadow .14s }\n.pdot:hover { box-shadow: inset 0 0 0 1.4px var(--txt2) }\n.pdot.on { background: var(--acc); box-shadow: 0 0 0 3px rgba(232,80,42,.24) }\n.pic { width: 17px; height: 17px; flex: none; color: var(--txt2) }\n.piece.exp .pic, .piece.sel .pic { color: var(--txt) }\n.ptxt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px }\n.pt { font-size: 12.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.psub { font-size: 10px; color: var(--txt3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.fxbadge { flex: none; font-size: 8.5px; font-weight: 700; font-family: ui-monospace, Menlo, monospace;\n  color: #fff; background: var(--acc); border-radius: 4px; padding: 2px 4px }\n.ptools { position: absolute; right: 8px; display: flex; gap: 3px; opacity: 0; transition: opacity .12s }\n.piece:hover .ptools, .piece.sel .ptools { opacity: 1 }\n.tb { width: 24px; height: 21px; border-radius: 6px; border: 1px solid var(--rule); font-size: 9px; font-weight: 700;\n  font-family: ui-monospace, Menlo, monospace; color: var(--txt2); background: var(--bg2) }\n.tb > svg { width: 11px; height: 11px }\n.tb:hover { color: var(--txt); border-color: var(--focus) }\n.tb.on { background: var(--acc); color: #fff; border-color: transparent }\n.p-m { top: 36px; left: auto; right: 8px; width: 154px }\n.pb { padding: 18px 11px 16px; display: flex; flex-direction: column; gap: 20px }\n\n/* inline props — control lives in the row, no bullet */\n.piece.inl .ph { height: 34px; padding-left: 11px }\n.pinl { display: flex; align-items: center; gap: 6px; flex: none }\n/* on an inline prop the control stays live — the tools open up beside it, never over it */\n.piece.inl .ptools { position: static; max-width: 0; overflow: hidden; opacity: 0;\n  transition: max-width .22s var(--ez), opacity .16s var(--ez) }\n.piece.inl:hover .ptools, .piece.inl.sel .ptools { max-width: 92px; opacity: 1 }\n.pinl .slot { height: 26px }\n.pinl .col-in, .pinl .hex { width: 74px }\n.pinrow { padding: 0 11px 12px; display: flex }\n.pinrow > .slot { flex: 1; width: 100% }\n.pinrow > .fxrow { flex: 1 }\n\n/* slots */\n.slot { position: relative; display: flex; align-items: center; min-width: 0; height: 32px; padding: 0 8px;\n  background: var(--well); border: 1px solid transparent; border-radius: 6px; cursor: pointer;\n  transition: border-color .12s, background .12s }\n.slot:hover { border-color: var(--edge) }\n.slot:focus-within { border-color: var(--focus) }\n.slot.sel { border-color: var(--focus) }\n.sin::placeholder { color: var(--txt3); opacity: 1 }\n.sin.num::placeholder { font-size: 15px; line-height: 1 }\n.slot .cap { position: absolute; top: -13px; left: 1px; font-size: 9.5px; font-style: normal; color: var(--txt3);\n  pointer-events: none; white-space: nowrap }\n.sin { flex: 1; min-width: 0; width: 100%; background: none; border: 0; outline: none; color: var(--txt);\n  font: inherit; font-size: 12px; user-select: text }\n.sin.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600 }\n.sin.hex { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; text-transform: uppercase }\n.unit { font-style: normal; font-size: 9.5px; color: var(--txt3); margin-left: 4px; flex: none }\nselect.sin { appearance: none; -webkit-appearance: none; cursor: pointer; text-align: right; padding-right: 2px }\n/* the native popup inherits the select's colour — without this it is white-on-white */\nselect.sin option { background-color: #17171A; color: #F2F2F2 }\n.ins.light select.sin option { background-color: #FFFFFF; color: #17171A }\n.selc { width: 9px; height: 6px; color: var(--txt2); flex: none; margin-left: 6px }\n.sldrow { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0 }\n.sld { position: relative; flex: 1; min-width: 0; height: 22px; cursor: pointer; display: flex; align-items: center }\n.sld-r { position: absolute; left: 0; right: 0; height: 1px; background: var(--txt3) }\n.sld-f { position: absolute; left: 0; height: 1px; background: var(--txt) }\n.sld-h { position: absolute; top: 50%; width: 5px; height: 15px; margin-left: -2.5px; border-radius: 1px;\n  background: var(--txt); transform: translateY(-50%); transition: background .12s }\n.sld:hover .sld-h { background: var(--acc) }\n.sld-v { flex: none; min-width: 42px; text-align: right; font-size: 12px; font-weight: 600;\n  font-variant-numeric: tabular-nums }\n.sld-v > i { font-style: normal; font-size: 9.5px; color: var(--txt3); margin-left: 2px }\n\n.chip-c { width: 15px; height: 15px; border-radius: 4px; flex: none; margin-right: 7px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.35) }\n\n/* matrices */\n.mtx { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 21px 6px }\n.mtx > i { display: block }\n.mtx.brd { grid-template-columns: 1fr 1fr 1fr 1.25fr; grid-template-rows: repeat(3, 32px) }\n.mtx.brd { position: relative }\n.ctpl { display: grid; place-items: center; border-radius: 6px; background: var(--well);\n  border: 1px solid transparent; transition: border-color .14s var(--ez) }\n.ctpl:hover { border-color: var(--edge) }\n.ctpl.on { border-color: var(--acc) }\n.ctpl-i { display: block; width: 18px; height: 18px; color: var(--txt2); transition: color .14s var(--ez) }\n.ctpl:hover .ctpl-i { color: var(--txt) }\n.ctpl.on .ctpl-i { color: var(--acc) }\n.tplm { position: absolute; left: 0; right: 0; top: 0; z-index: 40; padding: 8px;\n  display: flex; flex-direction: column; gap: 7px; border-radius: 9px;\n  background: var(--bg2); border: 1px solid var(--edge); box-shadow: 0 16px 36px rgba(0,0,0,.6);\n  animation: menuIn .18s var(--ez) both }\n.ins.light .tplm { box-shadow: 0 16px 36px rgba(0,0,0,.2) }\n.tplh { display: flex; align-items: center; justify-content: space-between; padding: 0 2px;\n  font-size: 8.5px; letter-spacing: .15em; text-transform: uppercase; color: var(--txt3) }\n.tplh em { font-style: normal; font-weight: 700; letter-spacing: .1em; color: var(--acc) }\n.tplg { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px 5px }\n.tplb { display: grid; place-items: center; gap: 5px; padding: 4px 0 3px; border-radius: 6px;\n  border: 1px solid transparent; transition: background .12s var(--ez), border-color .12s var(--ez) }\n.tplb > i { display: block; width: 30px; height: 21px; border: 1.4px solid var(--txt2);\n  transition: border-color .14s var(--ez), border-radius .2s var(--ez) }\n.tplb > em { font-style: normal; font-size: 7.5px; letter-spacing: .08em; text-transform: uppercase;\n  color: var(--txt3); transition: color .12s var(--ez) }\n.tplb:hover { background: var(--ctl2) }\n.tplb:hover > i { border-color: var(--txt) }\n.tplb:hover > em { color: var(--txt) }\n.tplb.on > i { border-color: var(--acc) }\n.tplb.on > em { color: var(--acc); font-weight: 700 }\n.cbig { grid-column: 4; grid-row: 1 / span 3; height: auto; padding: 0; align-items: flex-end; border-radius: 8px; overflow: hidden }\n.hex.big { width: 100%; height: 21px; text-align: center; color: #fff; background: rgba(0,0,0,.42); font-size: 10px }\n.mtx.pad .padc { display: grid; place-items: center; font-size: 10px; color: var(--txt3); border-radius: 6px;\n  background: rgba(255,255,255,.04); border: 1px solid var(--rule) }\n.ins.light .mtx.pad .padc { background: rgba(0,0,0,.05) }\n.prow { display: flex }\n.prow2 { display: flex; align-items: center; gap: 6px }\n.prow4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px }\n.fgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 21px 6px }\n.prow .wide, .slot.wide { flex: 1; width: 100% }\n.prow2 .slot { flex: 1 } .prow2 .grow { flex: 2 }\n\n/* toggle */\n.brow { display: flex; align-items: center; gap: 12px; min-height: 34px }\n.blbl { flex: 1; min-width: 0; font-size: 12px; color: var(--txt2); white-space: nowrap;\n  overflow: hidden; text-overflow: ellipsis }\n.tgl { position: relative; width: 36px; height: 20px; border-radius: 999px; flex: none;\n  background: var(--tglo); transition: background .2s var(--ez) }\n.tgl > i { position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%;\n  background: #E7E7EA; box-shadow: 0 1px 2px rgba(0,0,0,.45);\n  transition: transform .22s var(--ez), background .2s var(--ez) }\n.tgl:hover { background: color-mix(in srgb, var(--txt) 28%, var(--tglo)) }\n.tgl.on { background: var(--acc) }\n.tgl.on > i { transform: translateX(16px); background: #fff }\n\n/* clusters + anchors */\n.d-w0 svg, .d-w1 svg { transform: rotate(90deg) }\n.d-w2 svg { transform: rotate(-90deg) }\n.d-h0 svg { transform: rotate(180deg) }\n.al-c svg { transform: rotate(90deg) }\n.al-r svg { transform: scaleX(-1) }\n.al-t svg { transform: rotate(180deg) }\n.al-m svg { transform: rotate(90deg) scaleX(-1) }\n.al-b svg { transform: rotate(90deg) scaleY(-1) }\n.clusrow { display: flex; gap: 8px }\n.grp { flex: 1; display: flex; padding: 2px; gap: 2px; background: var(--ctl); border: 1px solid var(--rule); border-radius: 8px }\n.grp .ib { flex: 1; width: auto; height: 26px; border-radius: 6px }\n.lk { width: 26px; height: 26px; border: 1px solid var(--rule); background: var(--ctl); border-radius: 7px }\n.geo { display: flex; align-items: center; gap: 12px }\n.geo-r { display: flex; flex-direction: column; gap: 9px; flex: 1; min-width: 0 }\n.anc-g { display: grid; grid-template-columns: 20px 26px 20px; grid-template-rows: 16px 26px 16px; gap: 5px;\n  padding: 8px; background: var(--ctl); border: 1px solid var(--rule); border-radius: 9px; flex: none }\n.anc-g > i { display: block }\n.anc-g button { border-radius: 2px; background: var(--txt3) }\n.anc-g button:hover { background: var(--txt2) }\n.anc-g button.on { background: var(--acc) }\n.a-top, .a-bottom { justify-self: center; align-self: center; width: 20px; height: 3px }\n.a-left, .a-right { align-self: center; justify-self: center; width: 3px; height: 15px }\n.a-c { border-radius: 4px; border: 1px solid var(--txt3) }\n.shw { display: grid; place-items: center; height: 66px; background: var(--ctl); border: 1px solid var(--rule); border-radius: 9px }\n.shw-p { width: 46px; height: 26px; border-radius: 6px; background: var(--ctl2) }\n.fxrow { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 9px; border-radius: 8px;\n  background: rgba(232,80,42,.13); border: 1px solid rgba(232,80,42,.5) }\n.fxk { font-size: 10px; font-weight: 700; font-family: ui-monospace, Menlo, monospace; color: var(--acc); flex: none }\n.fxin { font-family: ui-monospace, Menlo, monospace; font-size: 11px }\n.dvd { display: flex; align-items: center; gap: 9px; color: var(--acc); font-size: 10.5px; font-weight: 700 }\n.dvd i { flex: 1; height: 1px; background: var(--rule) }\n.lrow { display: flex; align-items: center; gap: 9px; height: 32px; padding: 0 6px 0 9px; border-radius: 7px;\n  background: var(--ctl); border: 1px solid var(--rule) }\n.ln { width: 12px; font-size: 10px; color: var(--txt3); font-variant-numeric: tabular-nums }\n.lt { flex: 1; min-width: 0; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.addrow { display: flex; align-items: center; justify-content: center; gap: 6px; height: 32px; border-radius: 7px;\n  border: 1px dashed var(--rule); color: var(--txt2); font-size: 11.5px; font-weight: 500;\n  transition: border-color .16s, color .16s, background .16s }\n.addrow > svg { width: 11px; height: 11px }\n.addrow:hover { border-color: var(--focus); color: var(--txt); background: var(--ctl) }\n.empty { padding: 16px 12px; text-align: center; color: var(--txt3); font-size: 11px }\n\n/* wide */\n.ins.wide { width: 100% }\n.ins.wide .tray-b { columns: 2; column-gap: 12px; padding: 0 12px }\n.ins.wide .tray-b .piece { break-inside: avoid; margin: 0 0 4px }\n\n/* clipboard + dock */\n.clip { flex: none; display: flex; align-items: center; gap: 9px; margin: 0 10px 8px; padding: 8px 8px 8px 11px;\n  border-radius: 9px; background: rgba(232,80,42,.15); border: 1px solid rgba(232,80,42,.45) }\n.clip > svg { width: 13px; height: 13px; flex: none; color: var(--acc) }\n.clip .cl { flex: 1; min-width: 0; font-size: 11px; color: var(--txt2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }\n.clip .cn { color: var(--txt); font-weight: 600 }\n.clip .pb2 { height: 24px; padding: 0 11px; border-radius: 7px; background: var(--acc); color: #fff;\n  font-size: 11px; font-weight: 600 }\n.dock { flex: none; display: flex; align-items: center; gap: 2px; height: 42px; padding: 0 8px;\n  border-top: 1px solid var(--rule) }\n.dock .sp { flex: 1 }\n.hintbar { font-size: 9.5px; color: var(--txt3); letter-spacing: .1em }\n\n/* ---------------- motion ---------------- */\n:host { --ez: cubic-bezier(.22,.86,.16,1) }\n\n/* prop open/close: real height animation, both directions */\n.pbwrap { display: grid; grid-template-rows: 1fr; opacity: 1; visibility: visible;\n  transition: grid-template-rows .32s var(--ez), opacity .24s var(--ez), visibility 0s }\n.pbwrap > .pbi { overflow: hidden; min-height: 0; padding: 0 }\n.pbwrap[data-collapsed] { grid-template-rows: 0fr; opacity: 0; pointer-events: none; visibility: hidden;\n  transition: grid-template-rows .28s var(--ez), opacity .18s var(--ez), visibility 0s .28s }\n\n/* direction-aware category change + staggered rows */\n@keyframes slideA { from { opacity: 0; transform: translateX(var(--dx, 22px)) } to { opacity: 1; transform: none } }\n@keyframes slideB { from { opacity: 0; transform: translateX(var(--dx, 22px)) } to { opacity: 1; transform: none } }\n.tray-b.sa { animation: slideA .34s var(--ez) both }\n.tray-b.sb { animation: slideB .34s var(--ez) both }\n@keyframes popRow { from { opacity: 0; transform: translateY(7px) } to { opacity: 1; transform: none } }\n.tray-b.sa .piece, .tray-b.sb .piece { animation: popRow .4s var(--ez) both;\n  animation-delay: calc(var(--i, 0) * 26ms) }\n@keyframes labA { from { opacity: 0; transform: translateX(calc(var(--dx, 22px) * .5)) } to { opacity: 1; transform: none } }\n@keyframes labB { from { opacity: 0; transform: translateX(calc(var(--dx, 22px) * .5)) } to { opacity: 1; transform: none } }\n.plabel.la { animation: labA .3s var(--ez) both }\n.plabel.lb { animation: labB .3s var(--ez) both }\n\n/* menus + clipboard */\n@keyframes menuIn { from { opacity: 0; transform: translateY(-7px) scale(.97) } to { opacity: 1; transform: none } }\n.menu { transform-origin: top center; animation: menuIn .2s var(--ez) both }\n@keyframes rise { from { opacity: 0; transform: translateY(12px) } to { opacity: 1; transform: none } }\n.clip { animation: rise .3s var(--ez) both }\n\n/* expandable dock buttons — icon grows into a label on hover */\n.dock .ib.xp { display: flex; align-items: center; width: auto; min-width: 26px; padding: 0 5px; height: 26px }\n.ib.xp .g { width: 15px; height: 15px; display: grid; place-items: center; flex: none }\n.ib.xp .g > svg { width: 13px; height: 13px }\n.ib.xp .lb { max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap; font-size: 11px; font-weight: 600;\n  transition: max-width .32s var(--ez), opacity .2s var(--ez), margin .32s var(--ez) }\n.ib.xp:hover .lb { max-width: 68px; opacity: 1; margin-left: 6px; margin-right: 2px }\n.ib.xp.dg:hover { background: rgba(255,90,78,.16); color: #FF5A4E; border-color: rgba(255,90,78,.4) }\n\n/* micro-interactions */\n.pdot { transition: background .2s var(--ez), box-shadow .24s var(--ez), transform .24s var(--ez) }\n.pdot.on { transform: scale(1.12) }\n.piece:not(.pop):hover { background: rgba(255,255,255,.03) }\n.ins.light .piece:not(.pop):hover { background: rgba(255,255,255,.45) }\n.pic { transition: color .18s var(--ez), transform .28s var(--ez) }\n.piece.exp .pic { transform: scale(1.06) }\n.opt, .slot, .ib, .tb, .anc-g button { transition-timing-function: var(--ez) }\n.nt, .node-h .chev { transition: opacity .2s var(--ez) }\n\n@media (prefers-reduced-motion: reduce) {\n  .ins *, .ins *::before, .ins *::after {\n    animation-duration: .01ms !important; animation-delay: 0s !important; transition-duration: .01ms !important }\n}\n`,\n\n  render(d2) {\n    const model = d2.state.model || (d2.param('model') || DEFAULT_MODEL)\n    const tab = d2.state.tab || 'all'\n    const light = String(d2.param('appearance', 'dark')).toLowerCase() === 'light'\n    const accent = d2.param('accentHex', '')\n    const st = { sel: d2.state.sel, cut: d2.state.cut, drag: d2.state.drag, pOpen: d2.state.pOpen,\n      menuFor: d2.state.menuFor, tplFor: d2.state.tplFor }\n    const drop = d2.state.drop\n    const q = (d2.state.q || '').trim().toLowerCase()\n    const nodes = d2.state.nodes || {}\n    const anim = d2.state.anim || 0\n    let total = 0\n    model.forEach(t => { total += (t.pieces || []).length })\n\n    const trays = model.map(t => {\n      const nodeOpen = nodes[t.id] !== false\n      const pieces = (t.pieces || []).filter(p => q ? pieceMatches(p, q) : (tab === 'all' || (p.cat || 'style') === tab))\n      let inner = ''\n      pieces.forEach((p, i) => {\n        if (drop && drop.tray === t.id && drop.index === i) inner += '<div class=\"drop\"></div>'\n        inner += renderPiece(p, t, st, i)\n      })\n      if (drop && drop.tray === t.id && drop.index >= pieces.length) inner += '<div class=\"drop\"></div>'\n      return '<div class=\"tray' + (nodeOpen ? ' open' : '') + (drop && drop.tray === t.id ? ' over' : '') + '\" data-tray=\"' + esc(t.id) + '\">' +\n        '<div class=\"node\"><span class=\"nrule l\"></span>' +\n        '<button class=\"node-h\" data-act=\"node\" data-tray=\"' + esc(t.id) + '\">' +\n        '<span class=\"nt\">' + esc(t.title) + '</span><span class=\"chev\">' + I.tri + '</span></button>' +\n        '<span class=\"nrule r\"></span>' +\n        '<span class=\"nn\">' + pieces.length + '</span>' +\n        '<span class=\"ntools\">' +\n        '<button class=\"ib add\" data-act=\"add-open\" data-tray=\"' + esc(t.id) + '\" title=\"Add property\">' + I.plus + '</button>' +\n        '<button class=\"ib\" data-act=\"paste-tray\" data-tray=\"' + esc(t.id) + '\" title=\"Paste here\">' + I.paste + '</button>' +\n        '</span>' +\n        (d2.state.addFor === t.id ? renderAddMenu(t) : '') +\n        '</div>' +\n        (nodeOpen ? '<div class=\"tray-b ' + (anim % 2 ? 'sa' : 'sb') + '\">' + (inner || '<div class=\"empty\">' + (q ? 'No matches' : 'No properties in this category') + '</div>') + '</div>' : '') +\n        '</div>'\n    }).join('')\n\n    const clip = d2.state.clip\n    const clipBar = clip ? '<div class=\"clip\">' + I.paste +\n      '<span class=\"cl\">' + (clip.mode === 'piece'\n        ? '<span class=\"cn\">' + esc(clip.piece.title) + '</span> ' + (d2.state.cut ? 'cut' : 'copied')\n        : '<span class=\"cn\">' + esc(clip.value) + '</span> ' + esc(clip.type)) + '</span>' +\n      '<button class=\"pb2\" data-act=\"paste\">Paste</button>' +\n      '<button class=\"ib\" data-act=\"clip-clear\" title=\"Clear\">' + I.trash + '</button></div>' : ''\n\n    return '<div class=\"ins' + (light ? ' light' : '') + (d2.state.wide ? ' wide' : '') + '\" style=\"--dx:' + ((d2.state.dir || 1) > 0 ? 22 : -22) + 'px' + (accent ? ';--acc:' + esc(accent) : '') + '\">' +\n      '<div class=\"tbar\"><span class=\"t\">' + esc(d2.param('panelTitle', 'Inspector')) + '</span>' +\n      '<button class=\"ib\" data-act=\"noop\" title=\"Filter\">' + I.search + '</button>' +\n      '<button class=\"ib' + (d2.state.wide ? ' on' : '') + '\" data-act=\"expand\" title=\"Expand\">' + I.plus + '</button></div>' +\n\n      '<div class=\"body\">' +\n\n      '<div class=\"subj\"><span class=\"tile\">' + I.cube + '</span>' +\n      '<span class=\"m\"><span class=\"n\">' + esc(d2.param('subjectTitle', 'Block')) + '</span>' +\n      '<span class=\"p\">' + esc(d2.param('subjectType', 'data2-core-primitives@Block')) + '</span></span>' +\n      '<span class=\"pp\">' + total + ' PROPS</span></div>' +\n\n      '<div class=\"ident\">' +\n      '<div class=\"ir\"><span class=\"ik\">Component ID</span><span class=\"iv\">' + esc(d2.param('subjectId', '6a7a79932add8bd2c1a66f18')) + '</span></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Reference</span><input spellcheck=\"false\" placeholder=\"auto\"></div>' +\n      '<div class=\"ir\"><span class=\"ik\">Display name</span><input spellcheck=\"false\" value=\"' + esc(d2.param('subjectName', '')) + '\" placeholder=\"Untitled\"></div>' +\n      '</div>' +\n\n      renderPill(model, tab, d2.state.catMode, d2.state.q || '', anim) +\n      (trays.trim() ? trays : '<div class=\"empty\">No properties</div>') +\n      '</div>' +\n      clipBar +\n      '<div class=\"dock\">' +\n      '<button class=\"ib xp\" data-act=\"copy-sel\" title=\"Copy\"><span class=\"g\">' + I.copy + '</span><span class=\"lb\">Copy</span></button>' +\n      '<button class=\"ib xp\" data-act=\"cut-sel\" title=\"Cut\"><span class=\"g\">' + I.cut + '</span><span class=\"lb\">Cut</span></button>' +\n      '<button class=\"ib xp\" data-act=\"paste\" title=\"Paste\"><span class=\"g\">' + I.paste + '</span><span class=\"lb\">Paste</span></button>' +\n      '<span class=\"sp\"></span>' +\n      '<button class=\"ib xp\" data-act=\"reset-all\" title=\"Reset all\"><span class=\"g\">' + I.undo + '</span><span class=\"lb\">Reset</span></button>' +\n      '<button class=\"ib xp dg\" data-act=\"del-sel\" title=\"Remove\"><span class=\"g\">' + I.trash + '</span><span class=\"lb\">Remove</span></button>' +\n      '</div></div>'\n  },\n\n  mounted(d2) {\n    let clickTimer = null\n    d2.onUnmount(() => { if (clickTimer) clearTimeout(clickTimer) })\n    const base = () => d2.param('model') || DEFAULT_MODEL\n    const M = () => d2.state.model || base()\n    const commit = m => d2.setState({ model: m })\n    function removeById(m, id) { m.forEach(t => { t.pieces = (t.pieces || []).filter(p => p.id !== id) }) }\n\n    function copyFrom(sel, cut) {\n      if (!sel) return\n      const hit = findPiece(M(), sel.piece)\n      if (!hit) return\n      if (sel.type === 'piece') d2.setState({ clip: { mode: 'piece', kind: hit.piece.kind, piece: clone(hit.piece) }, cut: cut ? sel.piece : null })\n      else d2.setState({ clip: { mode: 'slot', type: sel.stype, value: getAt(hit.piece.v, sel.path) }, cut: null })\n    }\n\n    function paste(trayId) {\n      const clip = d2.state.clip\n      if (!clip) return\n      const m = clone(M())\n      const sel = d2.state.sel\n      if (clip.mode === 'slot') {\n        if (!sel || sel.type !== 'slot' || sel.stype !== clip.type) return\n        const hit = findPiece(m, sel.piece)\n        if (!hit) return\n        setAt(hit.piece.v, sel.path, clip.value)\n        commit(m)\n        return\n      }\n      if (!trayId && sel && sel.type === 'piece') {\n        const hit = findPiece(m, sel.piece)\n        if (hit && hit.piece.kind === clip.kind && hit.piece.id !== clip.piece.id) {\n          hit.piece.v = clone(clip.piece.v)\n          if (d2.state.cut) removeById(m, d2.state.cut)\n          commit(m)\n          d2.setState({ cut: null })\n          return\n        }\n      }\n      let target = trayId\n      if (!target && sel) { const h = findPiece(m, sel.piece); if (h) target = h.tray.id }\n      if (!target) target = m[0] && m[0].id\n      const tray = m.filter(t => t.id === target)[0]\n      if (!tray) return\n      const fresh = clone(clip.piece)\n      if (d2.state.cut) { removeById(m, d2.state.cut); fresh.id = clip.piece.id }\n      else fresh.id = nextId(m)\n      tray.pieces = tray.pieces || []\n      tray.pieces.push(fresh)\n      commit(m)\n      d2.setState({ cut: null, sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    function duplicate(id) {\n      const m = clone(M())\n      const hit = findPiece(m, id)\n      if (!hit) return\n      const fresh = clone(hit.piece)\n      fresh.id = nextId(m)\n      hit.tray.pieces.splice(hit.pi + 1, 0, fresh)\n      commit(m)\n      d2.setState({ sel: { type: 'piece', piece: fresh.id } })\n    }\n\n    d2.on('[data-sld]', 'pointerdown', e => {\n      const el = e.target.closest('[data-sld]')\n      if (!el) return\n      const parts = el.getAttribute('data-sld').split('|')\n      const min = Number(parts[2]) || 0\n      const max = Number(parts[3]) || 100\n      const move = ev => {\n        const r = el.getBoundingClientRect()\n        const t = Math.max(0, Math.min(1, (ev.clientX - r.left) / (r.width || 1)))\n        const m = clone(M())\n        const hit = findPiece(m, parts[0])\n        if (!hit) return\n        setAt(hit.piece.v, parts[1], Math.round(min + t * (max - min)))\n        commit(m)\n      }\n      move(e)\n      const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }\n      window.addEventListener('pointermove', move)\n      window.addEventListener('pointerup', up)\n    })\n\n    d2.on('[data-slot]', 'pointerdown', e => {\n      const el = e.target.closest('[data-slot]')\n      if (!el) return\n      const parts = el.getAttribute('data-slot').split('|')\n      d2.setState({ sel: { type: 'slot', piece: parts[0], path: parts[1], stype: parts[2] } })\n    })\n\n    d2.on('[data-act]', 'click', e => {\n      const el = e.target.closest('[data-act]')\n      if (!el) return\n      const act = el.getAttribute('data-act')\n      const pid = el.getAttribute('data-piece')\n      if (act === 'expand') { d2.setState({ wide: !d2.state.wide }); return }\n      if (d2.state.menuFor && act !== 'menu') d2.setState({ menuFor: null })\n      if (act === 'select') {\n        const op = Object.assign({}, d2.state.pOpen || {})\n        op[pid] = true\n        d2.setState({ sel: { type: 'piece', piece: pid }, pOpen: op })\n        return\n      }\n      if (act === 'pexp') {\n        const hit0 = findPiece(M(), pid)\n        if (!hit0) return\n        const ref0 = hit0.piece.def !== undefined ? hit0.piece.def : (TEMPLATES[hit0.piece.kind] ? TEMPLATES[hit0.piece.kind].v : null)\n        const pop0 = !ref0 || JSON.stringify(ref0) !== JSON.stringify(hit0.piece.v)\n        const cur = d2.state.pOpen || {}\n        const isOpen = cur[pid] !== undefined ? cur[pid] : pop0\n        const nx = Object.assign({}, cur)\n        nx[pid] = !isOpen\n        d2.setState({ pOpen: nx })\n        return\n      }\n      if (act === 'menu') { d2.setState({ menuFor: d2.state.menuFor === pid ? null : pid }); return }\n      if (act === 'fx') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.v.__fx != null) delete hit.piece.v.__fx\n        else hit.piece.v.__fx = ''\n        commit(m)\n        return\n      }\n      if (act === 'tpl-open') { d2.setState({ tplFor: d2.state.tplFor === pid ? null : pid }); return }\n      if (act === 'tpl-pick') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        hit.piece.v.r = String(el.getAttribute('data-r')).split(',').map(Number)\n        commit(m)\n        d2.setState({ tplFor: null })\n        return\n      }\n      if (act === 'list-add') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const cur = hit.piece.v.items || []\n        hit.piece.v.items = cur.concat([{ name: hit.piece.title + ' ' + (cur.length + 1) }])\n        commit(m)\n        return\n      }\n      if (act === 'list-del') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const ix = Number(el.getAttribute('data-idx'))\n        hit.piece.v.items = (hit.piece.v.items || []).filter((x, i) => i !== ix)\n        commit(m)\n        return\n      }\n      if (act === 'setbool') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        setAt(hit.piece.v, el.getAttribute('data-path'), el.getAttribute('data-val') === '1')\n        commit(m)\n        return\n      }\n      if (act === 'paste-into') {\n        const cb = d2.state.clip\n        if (!cb || cb.mode !== 'piece') return\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit || hit.piece.kind !== cb.kind) return\n        hit.piece.v = clone(cb.piece.v)\n        commit(m)\n        return\n      }\n      if (act === 'reset-piece') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        if (hit.piece.def !== undefined) hit.piece.v = clone(hit.piece.def)\n        commit(m)\n        return\n      }\n      if (act === 'node') {\n        const id = el.getAttribute('data-tray')\n        const nx = Object.assign({}, d2.state.nodes || {})\n        nx[id] = nx[id] === false\n        d2.setState({ nodes: nx })\n        return\n      }\n      if (act === 'add-open') {\n        const id = el.getAttribute('data-tray')\n        d2.setState({ addFor: d2.state.addFor === id ? null : id })\n        return\n      }\n      if (act === 'add-pick') {\n        const tpl = TEMPLATES[el.getAttribute('data-kind')]\n        if (!tpl) return\n        const m = clone(M())\n        const tray = m.filter(t => t.id === el.getAttribute('data-tray'))[0]\n        if (!tray) return\n        const fresh = clone(tpl)\n        fresh.id = nextId(m)\n        tray.pieces = tray.pieces || []\n        tray.pieces.push(fresh)\n        commit(m)\n        d2.setState({ addFor: null, sel: { type: 'piece', piece: fresh.id }, tab: 'all' })\n        return\n      }\n      if (act === 'cat-prev' || act === 'cat-next') {\n        const ids = TABS.map(t => t.id)\n        let i = ids.indexOf(d2.state.tab || 'all')\n        if (i < 0) i = 0\n        i = (i + (act === 'cat-next' ? 1 : -1) + ids.length) % ids.length\n        d2.setState({ tab: ids[i], catMode: null, q: '', dir: act === 'cat-next' ? 1 : -1, anim: (d2.state.anim || 0) + 1 })\n        return\n      }\n      if (act === 'cat-open') {\n        if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; return }\n        clickTimer = setTimeout(() => {\n          clickTimer = null\n          d2.setState({ catMode: d2.state.catMode === 'list' ? null : 'list' })\n        }, 210)\n        return\n      }\n      if (act === 'cat-pick') {\n        const ids = TABS.map(t => t.id)\n        const nv = el.getAttribute('data-val')\n        const fwd = ids.indexOf(nv) >= ids.indexOf(d2.state.tab || 'all')\n        d2.setState({ tab: nv, catMode: null, q: '', dir: fwd ? 1 : -1, anim: (d2.state.anim || 0) + 1 })\n        return\n      }\n      if (act === 'cat-close') { d2.setState({ catMode: null, q: '' }); return }\n      if (act === 'copy') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, false); return }\n      if (act === 'cut') { d2.setState({ sel: { type: 'piece', piece: pid } }); copyFrom({ type: 'piece', piece: pid }, true); return }\n      if (act === 'dup') { duplicate(pid); return }\n      if (act === 'del') { const m = clone(M()); removeById(m, pid); commit(m); d2.setState({ sel: null }); return }\n      if (act === 'copy-sel') { copyFrom(d2.state.sel, false); return }\n      if (act === 'cut-sel') { copyFrom(d2.state.sel, true); return }\n      if (act === 'paste') { paste(null); return }\n      if (act === 'paste-tray') { paste(el.getAttribute('data-tray')); return }\n      if (act === 'clip-clear') { d2.setState({ clip: null, cut: null }); return }\n      if (act === 'del-sel') {\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') { const m = clone(M()); removeById(m, sel.piece); commit(m); d2.setState({ sel: null }) }\n        return\n      }\n      if (act === 'reset-all') { d2.setState({ model: clone(base()), sel: null, clip: null, cut: null }); return }\n      if (act === 'flag') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        const path = el.getAttribute('data-path')\n        setAt(hit.piece.v, path, !getAt(hit.piece.v, path))\n        commit(m)\n        return\n      }\n      if (act === 'anchor') {\n        const m = clone(M())\n        const hit = findPiece(m, pid)\n        if (!hit) return\n        hit.piece.v.anchor[el.getAttribute('data-side')] = !hit.piece.v.anchor[el.getAttribute('data-side')]\n        commit(m)\n      }\n    })\n\n    d2.on('input', 'input', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) {\n        if (el.classList && el.classList.contains('pq')) d2.setState({ q: el.value })\n        return\n      }\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      let val = el.value\n      if (parts[2] === 'num') { const n = parseFloat(val); val = isNaN(n) ? 0 : n }\n      setAt(hit.piece.v, parts[1], val)\n      commit(m)\n    })\n\n    d2.on('select', 'change', e => {\n      const el = e.target\n      const spec = el.getAttribute && el.getAttribute('data-in')\n      if (!spec) return\n      const parts = spec.split('|')\n      const m = clone(M())\n      const hit = findPiece(m, parts[0])\n      if (!hit) return\n      setAt(hit.piece.v, parts[1], el.value)\n      commit(m)\n    })\n\n    d2.on('[data-act=\"cat-open\"]', 'dblclick', () => {\n      if (clickTimer) { clearTimeout(clickTimer); clickTimer = null }\n      d2.setState({ catMode: 'search', q: '' })\n    })\n\n    d2.on('.pq', 'keydown', e => {\n      if (e.key === 'Escape') { e.stopPropagation(); d2.setState({ catMode: null, q: '' }) }\n    })\n\n    d2.on('[data-grip]', 'pointerdown', e => {\n      const g = e.target.closest('[data-grip]')\n      if (!g) return\n      if (e.target.closest('.ptools') || e.target.closest('.pdot') || e.target.closest('.menu')) return\n      const id = g.getAttribute('data-grip')\n      let started = false\n      const startY = e.clientY\n      const move = ev => {\n        if (!started && Math.abs(ev.clientY - startY) < 4) return\n        if (!started) { started = true; d2.setState({ drag: id, sel: { type: 'piece', piece: id } }) }\n        const rows = d2.$$('.piece').map(el => ({\n          tray: el.getAttribute('data-tray'), id: el.getAttribute('data-piece'), r: el.getBoundingClientRect()\n        }))\n        let best = null\n        const wide = !!d2.state.wide\n        rows.forEach(row => {\n          if (row.id === id) return\n          const mid = row.r.top + row.r.height / 2\n          const cx = row.r.left + row.r.width / 2\n          const dy = ev.clientY - mid\n          const d = wide ? Math.sqrt((ev.clientX - cx) * (ev.clientX - cx) + dy * dy) : Math.abs(dy)\n          if (!best || d < best.d) best = { d: d, row: row, after: dy > 0 }\n        })\n        if (!best) return\n        const m = M()\n        const tray = m.filter(t => t.id === best.row.tray)[0]\n        if (!tray) return\n        let idx = (tray.pieces || []).findIndex(p => p.id === best.row.id)\n        if (best.after) idx += 1\n        const cur = d2.state.drop\n        if (!cur || cur.tray !== tray.id || cur.index !== idx) d2.setState({ drop: { tray: tray.id, index: idx } })\n      }\n      const up = () => {\n        window.removeEventListener('pointermove', move)\n        window.removeEventListener('pointerup', up)\n        const drop = d2.state.drop\n        if (started && drop) {\n          const m = clone(M())\n          const hit = findPiece(m, id)\n          if (hit) {\n            const moved = clone(hit.piece)\n            hit.tray.pieces.splice(hit.pi, 1)\n            const tray = m.filter(t => t.id === drop.tray)[0]\n            if (tray) {\n              let idx = drop.index\n              if (hit.tray.id === tray.id && hit.pi < idx) idx -= 1\n              tray.pieces.splice(Math.max(0, Math.min(idx, tray.pieces.length)), 0, moved)\n              commit(m)\n            }\n          }\n        }\n        d2.setState({ drag: null, drop: null })\n      }\n      window.addEventListener('pointermove', move)\n      window.addEventListener('pointerup', up)\n    })\n\n    d2.on(window, 'pointerdown', e => {\n      if (!d2.state.catMode && !d2.state.addFor && !d2.state.menuFor && !d2.state.tplFor) return\n      if (d2.state.tplFor) d2.setState({ tplFor: null })\n      const path = e.composedPath ? e.composedPath() : []\n      let inPill = false, inAdd = false, inMenu = false\n      for (let i = 0; i < path.length; i++) {\n        const el = path[i]\n        if (!el || !el.classList) continue\n        if (el.classList.contains('tplm') || el.classList.contains('ctpl')) return\n        if (el.classList.contains('p-m') || el.classList.contains('tb')) inMenu = true\n        else if (el.classList.contains('add') || el.classList.contains('add-m')) inAdd = true\n        else if (el.classList.contains('pill') || el.classList.contains('cats')) inPill = true\n      }\n      if (d2.state.menuFor && !inMenu) d2.setState({ menuFor: null })\n      if (d2.state.addFor && !inAdd) d2.setState({ addFor: null })\n      if (!d2.state.catMode || inPill) return\n      if (d2.state.catMode === 'search' && (d2.state.q || '').trim()) return\n      d2.setState({ catMode: null })\n    })\n\n    d2.on(window, 'keydown', e => {\n      const path = e.composedPath ? e.composedPath() : []\n      const t = path[0] || e.target\n      const tag = t && t.tagName\n      if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return\n      if (!(e.metaKey || e.ctrlKey)) {\n        if (e.key === 'Escape') d2.setState({ sel: null, cut: null, catMode: null, addFor: null, menuFor: null, tplFor: null })\n        return\n      }\n      const k = String(e.key).toLowerCase()\n      if (k === 'c') { e.preventDefault(); copyFrom(d2.state.sel, false) }\n      else if (k === 'x') { e.preventDefault(); copyFrom(d2.state.sel, true) }\n      else if (k === 'v') { e.preventDefault(); paste(null) }\n      else if (k === 'd') {\n        e.preventDefault()\n        const sel = d2.state.sel\n        if (sel && sel.type === 'piece') duplicate(sel.piece)\n      }\n    })\n  },\n\n  updated(d2) {\n    if (d2.state.catMode === 'search') {\n      if (FOCUSED.has(d2)) return\n      const el = d2.$('.pq')\n      if (el) { FOCUSED.add(d2); el.focus() }\n    } else FOCUSED.delete(d2)\n  }\n}\n"}},"panelTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panelTitle"}}}},"subjectTitle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectTitle"}}}},"subjectType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectType"}}}},"subjectId":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectId"}}}},"subjectName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subjectName"}}}},"model":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"model"}}}},"appearance":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"appearance"}}}},"accentHex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accentHex"}}}}}}}}}},{"_id":"6a876fee180c011e24d4159b","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"component":"smoothScrollIntoView","package":"lucca-office","definition":{"component":"data2-code-components@CodeFn","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default (params, d2) => {\n  if (typeof document === 'undefined') return false\n  document.title = 'INVOKED ' + (params && params.id)\n\n  const {\n    id,\n    alignment = 'start',\n    behavior = 'smooth',\n    duration = 450,\n    offset = 0,\n  } = params || {}\n\n  const el = document.getElementById(id)\n  if (!el) return false\n\n  if (behavior !== 'smooth') {\n    el.scrollIntoView({ block: alignment, behavior: 'auto' })\n    return true\n  }\n\n  const findScrollParent = (node) => {\n    let p = node.parentElement\n    while (p) {\n      const style = getComputedStyle(p)\n      const canScrollY = /(auto|scroll)/.test(style.overflowY) && p.scrollHeight > p.clientHeight\n      if (canScrollY) return p\n      p = p.parentElement\n    }\n    return document.scrollingElement || document.documentElement\n  }\n\n  const container = findScrollParent(el)\n  const usesWindow = container === document.scrollingElement || container === document.documentElement\n  const containerRect = usesWindow\n    ? { top: 0, bottom: window.innerHeight, height: window.innerHeight }\n    : container.getBoundingClientRect()\n  const elRect = el.getBoundingClientRect()\n  const currentScroll = usesWindow ? window.scrollY : container.scrollTop\n\n  const topWithin = elRect.top - containerRect.top + currentScroll\n  const bottomWithin = elRect.bottom - containerRect.bottom + currentScroll\n\n  let targetTop\n  if (alignment === 'end') {\n    targetTop = bottomWithin\n  } else if (alignment === 'center') {\n    targetTop = topWithin - (containerRect.height - elRect.height) / 2\n  } else if (alignment === 'nearest') {\n    const alreadyVisible = elRect.top >= containerRect.top && elRect.bottom <= containerRect.bottom\n    targetTop = alreadyVisible ? currentScroll : (Math.abs(topWithin - currentScroll) < Math.abs(bottomWithin - currentScroll) ? topWithin : bottomWithin)\n  } else {\n    targetTop = topWithin\n  }\n  targetTop += offset\n\n  const maxScroll = usesWindow\n    ? document.documentElement.scrollHeight - window.innerHeight\n    : container.scrollHeight - container.clientHeight\n  targetTop = Math.max(0, Math.min(targetTop, Math.max(0, maxScroll)))\n\n  const startTop = currentScroll\n  const delta = targetTop - startTop\n  if (Math.abs(delta) < 1) return Promise.resolve(true)\n\n  const easeInOutCubic = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)\n\n  const setScroll = (v) => {\n    if (usesWindow) window.scrollTo(0, v)\n    else container.scrollTop = v\n  }\n\n  return new Promise((resolve) => {\n    let startTime = null\n    const step = (now) => {\n      if (startTime === null) startTime = now\n      const elapsed = now - startTime\n      const p = Math.min(elapsed / duration, 1)\n      setScroll(startTop + delta * easeInOutCubic(p))\n      if (p < 1) requestAnimationFrame(step)\n      else resolve(true)\n    }\n    requestAnimationFrame(step)\n  })\n}"}},"id":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"id"}}}},"alignment":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"alignment"}}}},"behavior":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"behavior"}}}},"duration":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"duration"}}}},"offset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offset"}}}}}}}},{"_id":"6a8775e4ef426ca40f64192b","component":"data2-core-types@Component","system":"6a11143bae205cd35b1d5de0","params":{"extends":"data2-core-primitives@Block","component":"smoothScrollDemo","package":"lucca-office","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    .demo { height: 100%; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }\n    .scroller { height: 100%; overflow: auto; background: #f5f5f7; }\n    .header { position: sticky; top: 0; z-index: 2; height: 80px; background: #1e1b2e; color: #fff;\n      display: flex; align-items: center; justify-content: center; font-size: 26px; }\n    .nav { display: flex; gap: 10px; padding: 16px; }\n    .nav button { flex: 1; height: 50px; border: 0; border-radius: 10px; color: #fff; font-size: 15px;\n      cursor: pointer; font-family: inherit; }\n    .section { margin: 0 16px 16px 16px; height: 500px; border-radius: 16px; display: flex;\n      align-items: center; justify-content: center; color: #fff; font-size: 32px; font-weight: 600; }\n    .back { display: block; margin: 0 16px 16px 16px; height: 40px; width: 160px; border: 0;\n      border-radius: 10px; background: #1e1b2e; color: #fff; cursor: pointer; font-size: 13px; font-family: inherit; }\n  `,\n  render() {\n    return `<div class=\"demo\"><div class=\"scroller\" id=\"scroller\">\n      <div class=\"header\" id=\"top-anchor\">Smooth Scroll Demo</div>\n      <div class=\"nav\">\n        <button class=\"go\" data-target=\"section-1\" style=\"background:#7c4dff\">Go to Section 1</button>\n        <button class=\"go\" data-target=\"section-2\" style=\"background:#16a34a\">Go to Section 2</button>\n        <button class=\"go\" data-target=\"section-3\" style=\"background:#f97316\">Go to Section 3</button>\n      </div>\n      <div class=\"section\" id=\"section-1\" style=\"background:#7c4dff\">Section 1</div>\n      <button class=\"back\" data-target=\"top-anchor\">&uarr; Back to top</button>\n      <div class=\"section\" id=\"section-2\" style=\"background:#16a34a\">Section 2</div>\n      <button class=\"back\" data-target=\"top-anchor\">&uarr; Back to top</button>\n      <div class=\"section\" id=\"section-3\" style=\"background:#f97316\">Section 3</div>\n      <button class=\"back\" data-target=\"top-anchor\">&uarr; Back to top</button>\n    </div></div>`\n  },\n  mounted(d2) {\n    const smoothScrollIntoView = ({ id, alignment = 'start', duration = 600, offset = 0 }) => {\n      const container = d2.$('#scroller')\n      const el = d2.$('#' + id)\n      if (!container || !el) return\n      const containerRect = container.getBoundingClientRect()\n      const elRect = el.getBoundingClientRect()\n      const currentScroll = container.scrollTop\n      const topWithin = elRect.top - containerRect.top + currentScroll\n      const bottomWithin = elRect.bottom - containerRect.bottom + currentScroll\n      let targetTop\n      if (alignment === 'end') targetTop = bottomWithin\n      else if (alignment === 'center') targetTop = topWithin - (containerRect.height - elRect.height) / 2\n      else targetTop = topWithin\n      targetTop += offset\n      const maxScroll = container.scrollHeight - container.clientHeight\n      targetTop = Math.max(0, Math.min(targetTop, Math.max(0, maxScroll)))\n      const startTop = currentScroll\n      const delta = targetTop - startTop\n      if (Math.abs(delta) < 1) return\n      const easeInOutCubic = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)\n      let startTime = null\n      const step = (now) => {\n        if (startTime === null) startTime = now\n        const p = Math.min((now - startTime) / duration, 1)\n        container.scrollTop = startTop + delta * easeInOutCubic(p)\n        if (p < 1) requestAnimationFrame(step)\n      }\n      requestAnimationFrame(step)\n    }\n    d2.on('.go', 'click', (e, el) => smoothScrollIntoView({ id: el.dataset.target, alignment: 'start' }))\n    d2.on('.back', 'click', (e, el) => smoothScrollIntoView({ id: el.dataset.target, alignment: 'start' }))\n  },\n}"}}}}}}}}]}},{"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":"6a6ccff72e237fe6dd6fdff3","component":"data2-rush@HTTPListener","reference":"secDataFilesProxy","system":"6a11143bae205cd35b1d5de0","params":{}}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a625d29c3c616984c8ba58e","component":"data2-core-primitives@Router","system":"6a11143bae205cd35b1d5de0","displayName":"Map Router","params":{"initialPath":{"component":"data2-core-primitives@String","params":{"content":"/"}},"routes":[{"component":"data2-core-primitives@Route","params":{"path":{"component":"data2-core-primitives@String","params":{"content":"/"}},"content":{"params":{"reference":"okNy2g39hjrBlPv3-OAMH"},"component":"data2-core-types@ArtboardReference"}}}]},"reference":"T-_LsQa_bspHKwzS7nFoS"},{"_id":"6a60fc41127ce811434cf0cf","component":"data2-core-primitives@Artboard","system":"6a11143bae205cd35b1d5de0","displayName":"map ab","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":460,"height":460,"exactWidth":460,"exactHeight":460}}},"reference":"okNy2g39hjrBlPv3-OAMH"},{"_id":"6a60fc42127ce811434cf0d1","parent":"6a60fc41127ce811434cf0cf","component":"lucca-office@interactive-muni-map","system":"6a11143bae205cd35b1d5de0","displayName":"","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,"growH":false,"growV":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":460,"height":460,"exactWidth":false,"exactHeight":false}},"stateDefinitions":[{"component":"data2-core-user-interface@StateDefinition","params":{"key":"selectedMunicipio","initialValue":""}}]},"municipio":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.municipio"}}},"component":"data2-core-types@Getter"},"groups":{"params":{"array":{"params":{"haystack":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.groups"}}},"component":"data2-core-types@Getter"},"needle":{"component":"data2-core-primitives@String","params":{"content":"´"}}},"component":"data2-core-functions@Split"},"mapper":{"params":{"body":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"SETOR "}},{"params":{"string":{"params":{"haystack":{"params":{"reference":"m"},"component":"data2-core-types@Reference"},"needle":{"component":"data2-core-primitives@String","params":{"content":"setor"}}},"component":"data2-core-functions@Replace"}},"component":"data2-core-functions@toUpperCase"}]},"component":"data2-core-functions@StringConcat"},"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}]},"component":"data2-core-functions@Function"}},"component":"data2-core-functions@ArrayMap"},"colors":{"params":{"array":{"params":{"needle":{"component":"data2-core-primitives@String","params":{"content":"´"}},"haystack":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.colors"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@Split"},"mapper":{"params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"params":{"fieldsList":[{"key":{"params":{"string":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"SETOR "}},{"params":{"haystack":{"params":{"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}},"module":{"params":{"haystack":{"params":{"reference":"m"},"component":"data2-core-types@Reference"},"needle":{"component":"data2-core-primitives@String","params":{"content":":"}}},"component":"data2-core-functions@Split"}},"component":"data2-core-types@Getter"},"needle":{"component":"data2-core-primitives@String","params":{"content":"setor"}},"patch":{"component":"data2-core-primitives@String","params":{"content":""}}},"component":"data2-core-functions@Replace"}]},"component":"data2-core-functions@StringConcat"}},"component":"data2-core-functions@toUpperCase"},"value":{"params":{"portions":[{"component":"data2-core-primitives@String","params":{"content":"#"}},{"params":{"prop":{"component":"data2-core-primitives@String","params":{"content":"1"}},"module":{"params":{"haystack":{"params":{"reference":"m"},"component":"data2-core-types@Reference"},"needle":{"component":"data2-core-primitives@String","params":{"content":":"}}},"component":"data2-core-functions@Split"}},"component":"data2-core-types@Getter"}]},"component":"data2-core-functions@StringConcat"}}]},"component":"data2-core-types@FieldSet"}},"component":"data2-core-functions@Function"}},"component":"data2-core-functions@ArrayMap"},"assignments":{"params":{"fieldsList":{"params":{"array":{"params":{"needle":{"component":"data2-core-primitives@String","params":{"content":"´"}},"haystack":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.assignments"}}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@Split"},"mapper":{"params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"m"}}],"body":{"key":{"params":{"string":{"params":{"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}},"module":{"params":{"haystack":{"params":{"reference":"m"},"component":"data2-core-types@Reference"},"needle":{"component":"data2-core-primitives@String","params":{"content":":"}}},"component":"data2-core-functions@Split"}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@toUpperCase"},"value":{"params":{"string":{"params":{"prop":{"component":"data2-core-primitives@String","params":{"content":"1"}},"module":{"params":{"haystack":{"params":{"reference":"m"},"component":"data2-core-types@Reference"},"needle":{"component":"data2-core-primitives@String","params":{"content":":"}}},"component":"data2-core-functions@Split"}},"component":"data2-core-types@Getter"}},"component":"data2-core-functions@toUpperCase"}}},"component":"data2-core-functions@Function"}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-types@FieldSet"},"store":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"SectorSimStore"}},"ufs":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.ufs"}}},"component":"data2-core-types@Getter"},"neighborUfs":{"params":{"module":{"params":{"reference":"T-_LsQa_bspHKwzS7nFoSInterface"},"component":"data2-core-types@InteractiveModuleReference"},"prop":{"component":"data2-core-primitives@String","params":{"content":"routeParams.query.neighborUfs"}}},"component":"data2-core-types@Getter"}},"index":0,"reference":"sKDIksRhx7i-9Xu7qTuGm"}]}},{"_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":"6a60fc58127ce811434cf0d2","displayName":"map","reference":"K8gTiZQhK2oqRdCuXlgQL","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"map"}},"entryPoint":{"params":{"component":"data2-core-primitives@Router","reference":"T-_LsQa_bspHKwzS7nFoS"},"component":"data2-core-types@ComponentInstanceReference"}},"system":"6a11143bae205cd35b1d5de0","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a60db5a61d9611c1e0fcd28","component":"data2-core@Store","system":"6a11143bae205cd35b1d5de0","params":{"initialData":{}},"reference":"SectorSimStore"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}