{"context":{"domain":"bigaton-playground"},"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":"69b15e6093419d8151c50e4c","name":"Lucas Bigaton","domain":"bigaton-playground","visibility":"public","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-14T19:26:29.927Z","createdAt":"2026-03-11T12:56:41.025Z"}]}},{"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":"6a35a83dc2ac6f9c681d65b7","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"component":"asciiConverter","package":"bigaton-playground","definition":{"component":"data2-code-components@CodeFn","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default async (params) => {\n  const { imageUrl, cols = 80, rows = 40 } = params;\n  if (!imageUrl) return '';\n\n  try {\n    const img = await new Promise((resolve, reject) => {\n      const image = new Image();\n      image.crossOrigin = 'anonymous';\n      image.onload = () => resolve(image);\n      image.onerror = (e) => reject(new Error('CORS_LOAD_FAILED'));\n      setTimeout(() => reject(new Error('TIMEOUT')), 10000);\n      image.src = imageUrl;\n    });\n\n    const canvas = document.createElement('canvas');\n    canvas.width = cols;\n    canvas.height = rows;\n    const ctx = canvas.getContext('2d');\n    ctx.drawImage(img, 0, 0, cols, rows);\n\n    let data;\n    try {\n      data = ctx.getImageData(0, 0, cols, rows).data;\n    } catch (e) {\n      return '[CORS_CANVAS_BLOCKED] ' + e.message;\n    }\n\n    const chars = '@#S%?*+;:.';\n    let result = '';\n    for (let y = 0; y < rows; y++) {\n      for (let x = 0; x < cols; x++) {\n        const i = (y * cols + x) * 4;\n        const lum = (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255;\n        result += chars[Math.min(Math.floor(lum * chars.length), chars.length - 1)];\n      }\n      if (y < rows - 1) result += '\\n';\n    }\n    return result;\n  } catch (e) {\n    return '[ERR] ' + e.message;\n  }\n}"}},"imageUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"imageUrl"}}}},"cols":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cols"}}}},"rows":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rows"}}}}}}}},{"_id":"6a39804d63cf4d2466770aa3","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"pinnacle-uploader","package":"bigaton-playground","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: `:host{display:block;height:100%}.shell{display:flex;flex-direction:column;height:100%;background:#0d0d14;color:#fff;font-family:-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box}.head{padding:56px 24px 20px}.brand{font-size:11px;font-weight:700;letter-spacing:3px;color:#7c4dff;text-transform:uppercase;margin-bottom:8px}.title{font-size:28px;font-weight:800;line-height:1.2}.sub{font-size:14px;color:#666;margin-top:6px}.area{flex:1;margin:0 20px;min-height:0;display:flex}.zone{flex:1;border:2px dashed #252530;border-radius:20px;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:border-color .2s;position:relative;overflow:hidden}.zone.over{border-color:#7c4dff;background:rgba(124,77,255,.05)}.zone.set{border:none}.thumb{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.thumb-dim{position:absolute;inset:0;background:linear-gradient(to bottom,transparent 55%,rgba(0,0,0,.6))}.thumb-txt{position:absolute;bottom:14px;left:14px;font-size:13px;color:#ddd;font-weight:500}.icon{font-size:44px;margin-bottom:10px}.hint{font-size:14px;color:#4a4a5a;text-align:center;line-height:1.5}.hint2{font-size:12px;color:#2a2a38;margin-top:4px}.spin{width:26px;height:26px;border:3px solid #252530;border-top-color:#7c4dff;border-radius:50%;animation:sp .8s linear infinite;margin-bottom:8px}@keyframes sp{to{transform:rotate(360deg)}}.foot{padding:16px 20px 44px}.btn{width:100%;padding:16px;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer}.on{background:#7c4dff;color:#fff}.off{background:#151520;color:#3a3a4a;cursor:default}input[type=file]{display:none}`,\n  render(d2) {\n    const url = d2.store('pinnacle-store').get('tableImageUrl', '')\n    const busy = d2.state.uploading || false\n    const over = d2.state.over || false\n    const has = url.length > 0\n    const inner = busy\n      ? `<div class=\"spin\"></div><div class=\"hint\">Enviando…</div>`\n      : has\n        ? `<img class=\"thumb\" src=\"${url}\" /><div class=\"thumb-dim\"></div><div class=\"thumb-txt\">Toque para trocar</div>`\n        : `<div class=\"icon\">🪑</div><div class=\"hint\">Clique ou arraste a foto<br>da sua mesa de jantar</div><div class=\"hint2\">JPG · PNG · HEIC</div>`\n    return `<div class=\"shell\"><div class=\"head\"><div class=\"brand\">Porto Brasil</div><div class=\"title\">Veja como fica<br>na sua mesa</div><div class=\"sub\">Tire uma foto e escolha o modelo</div></div><div class=\"area\"><div class=\"zone${over?' over':''}${has?' set':''}\" id=\"z\">${inner}</div></div><div class=\"foot\"><button class=\"btn${has&&!busy?' on':' off'}\" id=\"nb\" ${has&&!busy?'':'disabled'}>${has?'Montar minha mesa →':'Selecione uma foto da mesa'}</button></div><input type=\"file\" accept=\"image/*\" id=\"fi\" /></div>`\n  },\n  mounted(d2) {\n    const up = async (file) => {\n      if (!file) return\n      d2.state.uploading = true\n      try {\n        const r = await d2.drive('3PXDvTphC4k15jrQCCy0d').uploadFromFiles([file])\n        const u = r?.[0]?.fileUrl || ''\n        if (u) d2.store('pinnacle-store').set('tableImageUrl', u)\n      } finally { d2.state.uploading = false }\n    }\n    d2.on('#z', 'click', () => { if (!d2.state.uploading) d2.$('#fi')?.click() })\n    d2.on('#fi', 'change', (e, el) => up(el.files?.[0]))\n    d2.on('#z', 'dragover', (e) => { e.preventDefault(); d2.state.over = true })\n    d2.on('#z', 'dragleave', () => { d2.state.over = false })\n    d2.on('#z', 'drop', (e) => { e.preventDefault(); d2.state.over = false; up(e.dataTransfer?.files?.[0]) })\n    d2.on('#nb', 'click', () => d2.store('pinnacle-store').set('screen', 'categories'))\n  }\n}"}}}}}}}},{"_id":"6a39804eae0247a6bd028427","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"pinnacle-catalog","package":"bigaton-playground","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 CATS = {\n  pratos:    { label: 'Pratos', vtex: 'pratos' },\n  sobremesa: { label: 'Pratos de sobremesa', vtex: 'pratos/prato-sobremesa' },\n  bowls:     { label: 'Bowls', vtex: 'complementos/bowls' }\n}\nexport default {\n  css: `:host{display:block;height:100%}.shell{display:flex;flex-direction:column;height:100%;background:#0d0d14;color:#fff;font-family:-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box}.hdr{padding:52px 20px 12px;display:flex;align-items:center;gap:12px}.bk{background:none;border:none;color:#666;font-size:22px;cursor:pointer;padding:0;line-height:1}.ht{font-size:20px;font-weight:700}.prog{font-size:12px;color:#7c4dff;font-weight:600;margin-top:2px}.grid{flex:1;overflow-y:auto;padding:0 12px 12px;display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:212px;gap:10px;align-content:start}.card{background:#111118;border-radius:14px;overflow:hidden;cursor:pointer;border:2px solid transparent;display:flex;flex-direction:column}.card.sel{border-color:#7c4dff}.cimg{width:100%;height:160px;object-fit:contain;background:#fff;display:block;padding:10px;box-sizing:border-box}.cname{padding:8px 10px;font-size:11px;font-weight:600;color:#ccc;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.center{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#333;gap:8px;font-size:14px}.empty{color:#333;font-size:13px;grid-column:1/-1;text-align:center;padding:24px}.spin{width:32px;height:32px;border:3px solid #1a1a24;border-top-color:#7c4dff;border-radius:50%;animation:sp .8s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}.foot{padding:12px 20px 40px}.btn{width:100%;padding:16px;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer}.on{background:#7c4dff;color:#fff}.off{background:#151520;color:#3a3a4a;cursor:default}`,\n  render(d2) {\n    const store = d2.store('pinnacle-store')\n    const cats = store.get('selectedCategories', []) || []\n    const idx = store.get('categoryIndex', 0) || 0\n    const curKey = cats[idx] || 'pratos'\n    const meta = CATS[curKey] || CATS.pratos\n    const prods = d2.state.prods || []\n    const loading = d2.state.loading !== false\n    const sels = store.get('selections', {}) || {}\n    const sel = sels[curKey] || null\n    const selId = sel ? sel.productId : ''\n    const total = cats.length || 1\n    const isLast = idx >= (total - 1)\n    if (loading) return `<div class=\"shell\"><div class=\"center\"><div class=\"spin\"></div><div>Carregando ${meta.label}…</div></div></div>`\n    const cards = prods.map(p => {\n      const img = (p.items && p.items[0] && p.items[0].images && p.items[0].images[0] && p.items[0].images[0].imageUrl) || ''\n      const name = p.productName || 'Item'\n      const id = String(p.productId || name)\n      return `<div class=\"card${id===selId?' sel':''}\" data-key=\"${id}\" data-id=\"${id}\" data-name=\"${name.replace(/\"/g,'')}\" data-img=\"${img}\"><img class=\"cimg\" src=\"${img}\" loading=\"lazy\" /><div class=\"cname\">${name}</div></div>`\n    }).join('')\n    const nextLabel = isLast ? 'Ver minha mesa →' : 'Próxima categoria →'\n    return `<div class=\"shell\"><div class=\"hdr\"><button class=\"bk\" id=\"bk\">←</button><div><div class=\"ht\">${meta.label}</div><div class=\"prog\">Categoria ${idx+1} de ${total}</div></div></div><div class=\"grid\">${cards||'<div class=\"empty\">Nenhum produto encontrado</div>'}</div><div class=\"foot\"><button class=\"btn${selId?' on':' off'}\" id=\"nb\" ${selId?'':'disabled'}>${selId?nextLabel:'Selecione um modelo'}</button></div></div>`\n  },\n  mounted(d2) {\n    const store = d2.store('pinnacle-store')\n    const cats = store.get('selectedCategories', []) || []\n    const idx = store.get('categoryIndex', 0) || 0\n    const curKey = cats[idx] || 'pratos'\n    const meta = CATS[curKey] || CATS.pratos\n    d2.state.loading = true\n    d2.rush('pinnacle-get-catalog').invoke({ category: meta.vtex }).then(r => {\n      const raw = (r && r.data && r.data.products) || (r && r.products) || []\n      d2.state.prods = Array.isArray(raw) ? raw : []\n      d2.state.loading = false\n    }).catch(() => { d2.state.prods = []; d2.state.loading = false })\n    d2.on('.card', 'click', (e, el) => {\n      const id = el.dataset.id\n      const p = (d2.state.prods || []).find(x => String(x.productId || x.productName) === id)\n      const imgs = ((p && p.items && p.items[0] && p.items[0].images) || []).map(im => im.imageUrl).filter(Boolean)\n      const sels2 = Object.assign({}, store.get('selections', {}) || {})\n      sels2[curKey] = { productId: id, category: curKey, label: meta.label, name: el.dataset.name, imageUrl: imgs[0] || el.dataset.img, images: imgs.length ? imgs : [el.dataset.img] }\n      store.set('selections', sels2)\n    })\n    d2.on('#bk', 'click', () => {\n      if (idx > 0) store.set('categoryIndex', idx - 1)\n      else store.set('screen', 'categories')\n    })\n    d2.on('#nb', 'click', () => {\n      const s = store.get('selections', {}) || {}\n      if (!s[curKey]) return\n      if (idx < (cats.length - 1)) store.set('categoryIndex', idx + 1)\n      else store.set('screen', 'result')\n    })\n  }\n}"}}}}}}}},{"_id":"6a39804e4d21de73ecd2789b","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"pinnacle-result","package":"bigaton-playground","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: `:host{display:block;height:100%}.shell{display:flex;flex-direction:column;height:100%;background:#0d0d14;color:#fff;font-family:-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box}.hdr{padding:52px 20px 14px;display:flex;align-items:center;gap:12px}.bk{background:none;border:none;color:#666;font-size:22px;cursor:pointer;padding:0;line-height:1}.ht{font-size:20px;font-weight:700}.pv{flex:1;margin:0 20px;min-height:0;border-radius:20px;overflow:hidden;background:#111118;position:relative;display:flex;align-items:center;justify-content:center}.res{width:100%;height:100%;object-fit:cover;display:block}.ph{display:flex;flex-direction:column;align-items:center;gap:10px;color:#2a2a38;font-size:14px;text-align:center;padding:24px}.gen-ov{position:absolute;inset:0;background:rgba(13,13,20,.85);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;font-size:13px;color:#aaa}.spin{width:34px;height:34px;border:3px solid #1a1a24;border-top-color:#7c4dff;border-radius:50%;animation:sp .8s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}.items{display:flex;gap:8px;overflow-x:auto;padding:12px 20px 0}.chip{display:flex;align-items:center;gap:8px;background:#111118;border-radius:12px;padding:8px 10px;flex-shrink:0}.chimg{width:38px;height:38px;border-radius:8px;object-fit:contain;background:#fff;flex-shrink:0}.chl{font-size:10px;color:#555}.chn{font-size:12px;color:#ccc;font-weight:600;max-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.foodbox{padding:12px 20px 0}.foodlbl{font-size:12px;color:#666;margin-bottom:6px}.food{width:100%;box-sizing:border-box;padding:13px 14px;background:#111118;border:1px solid #1e1e2a;border-radius:12px;color:#fff;font-size:14px;font-family:inherit;outline:none}.food:focus{border-color:#7c4dff}.food::placeholder{color:#3a3a4a}.foot{padding:14px 20px 40px;display:flex;flex-direction:column;gap:8px}.btn{width:100%;padding:16px;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer}.on{background:#7c4dff;color:#fff}.sec{background:#111118;color:#555;font-size:14px;font-weight:500}.btn:disabled{opacity:.35;cursor:default}`,\n  render(d2) {\n    const store = d2.store('pinnacle-store')\n    const tUrl = store.get('tableImageUrl', '')\n    const cats = store.get('selectedCategories', []) || []\n    const sels = store.get('selections', {}) || {}\n    const items = cats.map(k => sels[k]).filter(Boolean)\n    const genUrl = store.get('generatedImageUrl', '')\n    const food = store.get('food', '') || ''\n    const busy = d2.state.generating || false\n    const phase = d2.state.phase || ''\n    const show = genUrl || tUrl\n    const pv = show\n      ? `<img class=\"res\" src=\"${show}\" />${busy ? `<div class=\"gen-ov\"><div class=\"spin\"></div><div>${phase || 'Montando sua mesa…'}</div></div>` : ''}`\n      : `<div class=\"ph\"><div style=\"font-size:48px\">🍽️</div><div>Sua mesa aparecerá aqui</div></div>`\n    const chips = items.map(it => `<div class=\"chip\"><img class=\"chimg\" src=\"${it.imageUrl||''}\" /><div><div class=\"chl\">${it.label}</div><div class=\"chn\">${it.name}</div></div></div>`).join('')\n    const itemsBar = items.length ? `<div class=\"items\">${chips}</div>` : ''\n    const foodBox = `<div class=\"foodbox\"><div class=\"foodlbl\">🍴 Comida no prato de cima (opcional)</div><input class=\"food\" id=\"food\" placeholder=\"ex: risoto de camarão, salada caesar…\" value=\"${String(food).replace(/\"/g,'&quot;')}\" /></div>`\n    return `<div class=\"shell\"><div class=\"hdr\"><button class=\"bk\" id=\"bk\">←</button><div class=\"ht\">${genUrl?'Sua mesa':'Revisão'}</div></div><div class=\"pv\">${pv}</div>${itemsBar}${foodBox}<div class=\"foot\"><button class=\"btn on\" id=\"gb\" ${busy||!items.length||!tUrl?'disabled':''}>${busy?'Gerando…':genUrl?'🔄 Gerar novamente':'✨ Montar minha mesa'}</button>${genUrl?'<button class=\"btn sec\" id=\"sb\">Compartilhar</button>':''}</div></div>`\n  },\n  mounted(d2) {\n    d2.on('#food', 'change', (e, el) => d2.store('pinnacle-store').set('food', el.value))\n    d2.on('#bk', 'click', () => {\n      const store = d2.store('pinnacle-store')\n      const cats = store.get('selectedCategories', []) || []\n      store.set('categoryIndex', Math.max(0, cats.length - 1))\n      store.set('screen', 'catalog')\n    })\n    d2.on('#gb', 'click', async () => {\n      const store = d2.store('pinnacle-store')\n      const tUrl = store.get('tableImageUrl')\n      const cats = store.get('selectedCategories', []) || []\n      const sels = store.get('selections', {}) || {}\n      const items = cats.map(k => sels[k]).filter(Boolean)\n      const foodEl = d2.$('#food')\n      const food = ((foodEl && foodEl.value) || store.get('food', '') || '').trim()\n      if (!tUrl || !items.length || d2.state.generating) return\n      d2.state.generating = true\n      store.set('generatedImageUrl', '')\n      try {\n        const a = d2.assistant('pinnacle-assistant')\n        const described = []\n        for (let i = 0; i < items.length; i++) {\n          const it = items[i]\n          d2.state.phase = `Analisando ${it.label.toLowerCase()}… (${i+1}/${items.length})`\n          let desc = ''\n          try {\n            const ans = await a.ask(`In one short vivid phrase, describe this single ceramic ${it.label.toLowerCase()} so it can be recreated faithfully: its exact color, finish/glaze, pattern and rim shape. Reply with ONLY the description phrase, no preamble.`, { images: [it.imageUrl] })\n            desc = (typeof ans === 'string' ? ans : '').trim()\n          } catch (e) { d2.log('desc err', e) }\n          described.push({ label: it.label, name: it.name, desc: desc || it.name })\n        }\n        d2.state.phase = 'Montando sua mesa…'\n        const r = await d2.rush('pinnacle-generate-tryon').invoke({ tableImageUrl: tUrl, items: described, food })\n        const url = (r && r.data && r.data.imageUrl) || (r && r.imageUrl) || ''\n        if (url) store.set('generatedImageUrl', url)\n        else d2.dialog.alert('Não foi possível gerar a imagem. Tente novamente.')\n      } catch (e) { d2.log('tryon error', e); d2.dialog.alert('Erro ao gerar a visualização. Tente novamente.') }\n      finally { d2.state.generating = false; d2.state.phase = '' }\n    })\n    d2.on('#sb', 'click', async () => {\n      const url = d2.store('pinnacle-store').get('generatedImageUrl', '')\n      if (!url) return\n      try { if (navigator.share) await navigator.share({ title: 'Minha mesa Porto Brasil', text: 'Veja como ficou minha mesa!', url: url.startsWith('http') ? url : undefined }) } catch (e) { d2.log('share', e) }\n    })\n  }\n}"}}}}}}}},{"_id":"6a398807428db02572d274d5","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"pinnacle-app","package":"bigaton-playground","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: `:host{display:block;height:100%}.frame{position:relative;height:100%;width:100%;overflow:hidden;background:#0d0d14}`,\n  render(d2) {\n    const store = d2.store('pinnacle-store')\n    const screen = store.get('screen', 'upload')\n    const idx = store.get('categoryIndex', 0)\n    const map = {\n      upload: 'bigaton-playground@pinnacle-uploader',\n      categories: 'bigaton-playground@pinnacle-categories',\n      catalog: 'bigaton-playground@pinnacle-catalog',\n      result: 'bigaton-playground@pinnacle-result'\n    }\n    const child = map[screen] || map.upload\n    const key = screen === 'catalog' ? ('catalog-' + idx) : screen\n    return `<div class=\"frame\">${d2.render(child, {}, { key })}</div>`\n  }\n}"}}}}}}}},{"_id":"6a39a0063cacb4cf2386666d","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"pinnacle-categories","package":"bigaton-playground","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 CATS = [\n  { key:'pratos', label:'Pratos', sub:'Prato principal', emoji:'🍽️' },\n  { key:'sobremesa', label:'Pratos de sobremesa', sub:'Para a sobremesa', emoji:'🍰' },\n  { key:'bowls', label:'Bowls', sub:'Tigelas de cerâmica', emoji:'🥣' }\n]\nexport default {\n  css: `:host{display:block;height:100%}.shell{display:flex;flex-direction:column;height:100%;background:#0d0d14;color:#fff;font-family:-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box}.hdr{padding:52px 20px 8px;display:flex;align-items:center;gap:12px}.bk{background:none;border:none;color:#666;font-size:22px;cursor:pointer;padding:0;line-height:1}.ht{font-size:22px;font-weight:800}.sub{font-size:14px;color:#666;padding:0 20px 20px}.list{flex:1;overflow-y:auto;padding:0 20px;display:flex;flex-direction:column;gap:12px}.cat{display:flex;align-items:center;gap:14px;background:#13131c;border:2px solid #1e1e2a;border-radius:16px;padding:16px;cursor:pointer;transition:border-color .15s,background .15s}.cat.on{border-color:#7c4dff;background:rgba(124,77,255,.08)}.emoji{font-size:30px;width:46px;height:46px;display:flex;align-items:center;justify-content:center;background:#0d0d14;border-radius:12px;flex-shrink:0}.ctext{flex:1}.clabel{font-size:16px;font-weight:700}.csub{font-size:12px;color:#666;margin-top:2px}.chk{width:26px;height:26px;border-radius:50%;border:2px solid #2a2a38;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800;color:#fff;flex-shrink:0}.cat.on .chk{background:#7c4dff;border-color:#7c4dff}.foot{padding:16px 20px 40px}.btn{width:100%;padding:16px;border:none;border-radius:14px;font-size:16px;font-weight:700;cursor:pointer}.on{background:#7c4dff;color:#fff}.off{background:#151520;color:#3a3a4a;cursor:default}`,\n  render(d2) {\n    const sel = d2.store('pinnacle-store').get('selectedCategories', []) || []\n    const rows = CATS.map(c => {\n      const on = sel.indexOf(c.key) >= 0\n      return `<div class=\"cat${on?' on':''}\" data-key=\"${c.key}\"><div class=\"emoji\">${c.emoji}</div><div class=\"ctext\"><div class=\"clabel\">${c.label}</div><div class=\"csub\">${c.sub}</div></div><div class=\"chk\">${on?'✓':''}</div></div>`\n    }).join('')\n    const n = sel.length\n    return `<div class=\"shell\"><div class=\"hdr\"><button class=\"bk\" id=\"bk\">←</button><div class=\"ht\">O que vai na mesa?</div></div><div class=\"sub\">Selecione as categorias que você quer compor</div><div class=\"list\">${rows}</div><div class=\"foot\"><button class=\"btn${n?' on':' off'}\" id=\"go\" ${n?'':'disabled'}>${n?('Continuar ('+n+') →'):'Selecione ao menos uma'}</button></div></div>`\n  },\n  mounted(d2) {\n    const store = d2.store('pinnacle-store')\n    d2.on('.cat','click',(e,el)=>{\n      const key = el.dataset.key\n      const sel = (store.get('selectedCategories', []) || []).slice()\n      const i = sel.indexOf(key)\n      if (i >= 0) sel.splice(i,1); else sel.push(key)\n      store.set('selectedCategories', sel)\n    })\n    d2.on('#bk','click',()=> store.set('screen','upload'))\n    d2.on('#go','click',()=>{\n      const order = ['pratos','sobremesa','bowls']\n      const sel = store.get('selectedCategories', []) || []\n      const ordered = order.filter(k => sel.indexOf(k) >= 0)\n      if (!ordered.length) return\n      store.set('selectedCategories', ordered)\n      store.set('categoryIndex', 0)\n      store.set('selections', {})\n      store.set('screen','catalog')\n    })\n  }\n}"}}}}}}}},{"_id":"6a3aaa93b336e5cdbcb03ba2","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"spriteshader-fx","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Sprite Shader FX — editable PLAYGROUND. Controls only; the shader itself is the\n// isolated block `bigaton-playground@spriteshader`, nested via d2.render and driven by these sliders.\nconst DEFAULTS = {\n  imageUrl: '', bg: '#15171f',\n  glowOn: true, glowColor: '#46c8ff', glow: 14, glowGlobal: 1,\n  glitchOn: false, glitchAmount: 3, glitchSize: 1, glitchSpeed: 20,\n  shineOn: true, shineColor: '#ffffff', shineLocation: 0.5, shineRotate: 0.8,\n  shineWidth: 0.12, shineGlow: 1.4, shineAnimate: true, shineSpeed: 0.35,\n};\nconst KEYS = ['imageUrl', 'bg', 'glowOn', 'glowColor', 'glow', 'glowGlobal',\n  'glitchOn', 'glitchAmount', 'glitchSize', 'glitchSpeed',\n  'shineOn', 'shineColor', 'shineLocation', 'shineRotate', 'shineWidth', 'shineGlow', 'shineAnimate', 'shineSpeed'];\n\nfunction gv(d2, n) {\n  const sv = d2.state[n];\n  if (sv !== undefined) return sv;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\n\nexport default {\n  css: `\n  .wrap{ display:flex; height:100%; min-height:540px; width:100%; font-family:Inter,system-ui,sans-serif; color:#e7e9f0; background:#0e0f15; }\n  .stagewrap{ position:relative; flex:1; min-width:0; background:\n      linear-gradient(45deg,#1a1c24 25%,transparent 25%),linear-gradient(-45deg,#1a1c24 25%,transparent 25%),\n      linear-gradient(45deg,transparent 75%,#1a1c24 75%),linear-gradient(-45deg,transparent 75%,#1a1c24 75%);\n      background-size:24px 24px; background-position:0 0,0 12px,12px -12px,-12px 0; overflow:hidden; }\n  .panel{ width:330px; flex:none; overflow-y:auto; background:#13151d; border-left:1px solid #262a37; padding:14px; }\n  .ttl{ font-size:13px; font-weight:700; letter-spacing:.04em; color:#aeb4c6; margin:0 0 12px; text-transform:uppercase; }\n  .sec{ background:#191c26; border:1px solid #262a37; border-radius:10px; margin-bottom:12px; overflow:hidden; }\n  .sh{ display:flex; align-items:center; justify-content:space-between; padding:10px 12px; font-weight:600; font-size:13px; background:#1e2230; }\n  .sb{ padding:10px 12px; display:flex; flex-direction:column; gap:11px; }\n  .r{ display:flex; flex-direction:column; gap:5px; }\n  .rl{ display:flex; justify-content:space-between; font-size:11.5px; color:#9aa0b2; }\n  .rl b{ color:#dfe3ee; font-weight:600; font-variant-numeric:tabular-nums; }\n  .rc{ flex-direction:row; align-items:center; justify-content:space-between; }\n  .rc span{ font-size:11.5px; color:#9aa0b2; }\n  input[type=range]{ width:100%; accent-color:#46c8ff; height:4px; }\n  input[type=color]{ width:46px; height:26px; border:1px solid #2c3142; border-radius:6px; background:#0e0f15; padding:0; cursor:pointer; }\n  .sw{ position:relative; display:inline-block; width:38px; height:21px; cursor:pointer; }\n  .sw input{ opacity:0; width:0; height:0; }\n  .sw span{ position:absolute; inset:0; background:#363b4d; border-radius:21px; transition:.2s; }\n  .sw span:before{ content:''; position:absolute; height:15px; width:15px; left:3px; top:3px; background:#fff; border-radius:50%; transition:.2s; }\n  .sw input:checked + span{ background:#46c8ff; }\n  .sw input:checked + span:before{ transform:translateX(17px); }\n  .urlbox{ width:100%; box-sizing:border-box; background:#0e0f15; border:1px solid #2c3142; color:#e7e9f0; border-radius:6px; padding:7px 8px; font-size:12px; }\n  .file{ font-size:11px; color:#9aa0b2; width:100%; }\n  .reset{ width:100%; margin-top:4px; background:#242838; border:1px solid #343a4d; color:#cfd4e2; border-radius:8px; padding:9px; font-size:12px; font-weight:600; cursor:pointer; }\n  .reset:hover{ background:#2c3142; }\n  `,\n\n  render(d2) {\n    const P = {};\n    KEYS.forEach(k => { P[k] = gv(d2, k); });\n\n    const sld = (n, l, mn, mx, st) =>\n      `<div class=\"r\"><div class=\"rl\"><span>${l}</span><b>${(+gv(d2, n)).toFixed(2)}</b></div>` +\n      `<input class=\"ctl\" type=\"range\" data-name=\"${n}\" min=\"${mn}\" max=\"${mx}\" step=\"${st}\" value=\"${gv(d2, n)}\"></div>`;\n    const clr = (n, l) =>\n      `<div class=\"r rc\"><span>${l}</span><input class=\"ctl\" type=\"color\" data-name=\"${n}\" value=\"${gv(d2, n)}\"></div>`;\n    const tog = (n) =>\n      `<label class=\"sw\"><input class=\"tog\" type=\"checkbox\" data-name=\"${n}\" ${gv(d2, n) ? 'checked' : ''}><span></span></label>`;\n    const swr = (n, l) => `<div class=\"r rc\"><span>${l}</span>${tog(n)}</div>`;\n\n    return `<div class=\"wrap\">\n      <div class=\"stagewrap\">${d2.render('bigaton-playground@spriteshader', P, { key: 'shader', style: 'position:absolute;inset:0;width:100%;height:100%' })}</div>\n      <div class=\"panel\">\n        <p class=\"ttl\">Sprite Shader FX</p>\n\n        <div class=\"sec\">\n          <div class=\"sh\"><span>✨ Glow</span>${tog('glowOn')}</div>\n          <div class=\"sb\">\n            ${clr('glowColor', 'Glow Color')}\n            ${sld('glow', 'Glow Color Intensity', 0, 100, 0.5)}\n            ${sld('glowGlobal', 'Global Glow Intensity', 1, 100, 0.5)}\n          </div>\n        </div>\n\n        <div class=\"sec\">\n          <div class=\"sh\"><span>📺 Glitch</span>${tog('glitchOn')}</div>\n          <div class=\"sb\">\n            ${sld('glitchAmount', 'Glitch Amount', 0, 20, 0.1)}\n            ${sld('glitchSize', 'Glitch Size', 0.25, 5, 0.05)}\n            ${sld('glitchSpeed', 'Glitch Speed', 0, 100, 0.5)}\n          </div>\n        </div>\n\n        <div class=\"sec\">\n          <div class=\"sh\"><span>🌟 Shine</span>${tog('shineOn')}</div>\n          <div class=\"sb\">\n            ${clr('shineColor', 'Shine Color')}\n            ${sld('shineLocation', 'Shine Location', 0, 1, 0.01)}\n            ${sld('shineRotate', 'Rotate Angle (rad)', 0, 6.2831, 0.01)}\n            ${sld('shineWidth', 'Shine Width', 0.05, 1, 0.01)}\n            ${sld('shineGlow', 'Shine Glow', 0, 100, 0.5)}\n            ${swr('shineAnimate', 'Auto Animate')}\n            ${sld('shineSpeed', 'Animate Speed', 0, 3, 0.01)}\n          </div>\n        </div>\n\n        <div class=\"sec\">\n          <div class=\"sh\"><span>🖼️ Sprite</span></div>\n          <div class=\"sb\">\n            ${clr('bg', 'Background')}\n            <div class=\"r\"><div class=\"rl\"><span>Image URL</span></div>\n              <input class=\"urlbox\" type=\"text\" placeholder=\"https://....png\" value=\"${gv(d2, 'imageUrl')}\"></div>\n            <div class=\"r\"><div class=\"rl\"><span>Upload PNG</span></div>\n              <input class=\"file\" type=\"file\" accept=\"image/*\"></div>\n            <button class=\"reset\">Reset to defaults</button>\n          </div>\n        </div>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    d2.on('.ctl', 'input', (e) => {\n      const t = e.target, n = t.dataset.name; if (!n) return;\n      d2.state[n] = (t.type === 'color') ? t.value : parseFloat(t.value);\n    });\n    d2.on('.tog', 'change', (e) => { const t = e.target; d2.state[t.dataset.name] = t.checked; });\n    d2.on('.urlbox', 'change', (e) => { d2.state.imageUrl = e.target.value; });\n    d2.on('.file', 'change', (e) => {\n      const f = e.target.files && e.target.files[0]; if (!f) return;\n      const rd = new FileReader(); rd.onload = () => { d2.state.imageUrl = rd.result; }; rd.readAsDataURL(f);\n    });\n    d2.on('.reset', 'click', () => { Object.keys(DEFAULTS).forEach(k => { d2.state[k] = DEFAULTS[k]; }); });\n  }\n};\n"}},"imageUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"imageUrl"}}}},"bg":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bg"}}}},"glowOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowOn"}}}},"glowColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowColor"}}}},"glow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glow"}}}},"glowGlobal":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowGlobal"}}}},"glitchOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchOn"}}}},"glitchAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchAmount"}}}},"glitchSize":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchSize"}}}},"glitchSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchSpeed"}}}},"shineOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineOn"}}}},"shineColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineColor"}}}},"shineLocation":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineLocation"}}}},"shineRotate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineRotate"}}}},"shineWidth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineWidth"}}}},"shineGlow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineGlow"}}}},"shineAnimate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineAnimate"}}}},"shineSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineSpeed"}}}}}}}}}},{"_id":"6a3aad910176bfeb38c5364f","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"spriteshader","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Sprite Shader (isolated block) — WebGL port of AllIn1SpriteShader\n// Glow / Glitch / Shine + Distortion&UV + Special/Render. Values from d2.params,\n// live override via shared UIState ('ss_<name>', global).\nconst DEFAULTS = {\n  imageUrl: '', bg: '#15171f',\n  glowOn: true, glowColor: '#46c8ff', glow: 14, glowGlobal: 1,\n  glitchOn: false, glitchAmount: 3, glitchSize: 1, glitchSpeed: 20,\n  shineOn: true, shineColor: '#ffffff', shineLocation: 0.5, shineRotate: 0.8,\n  shineWidth: 0.12, shineGlow: 1.4, shineAnimate: true, shineSpeed: 0.35,\n  rotateUvOn: false, rotateUv: 0,\n  offsetUvOn: false, offsetUvX: 0, offsetUvY: 0,\n  clipUvOn: false, clipLeft: 0, clipRight: 0, clipUp: 0, clipDown: 0,\n  texScrollOn: false, scrollX: 0.3, scrollY: 0,\n  twistOn: false, twistAmount: 1, twistPosX: 0.5, twistPosY: 0.5, twistRadius: 0.75,\n  fishEyeOn: false, fishEye: 0.35,\n  pinchOn: false, pinch: 0.35,\n  zoomOn: false, zoom: 0.5,\n  handDrawnOn: false, handDrawnAmount: 10, handDrawnSpeed: 5,\n  shakeOn: false, shakeSpeed: 2.5, shakeX: 1.5, shakeY: 1,\n  distortOn: false, distortAmount: 0.5, distortXSpeed: 5, distortYSpeed: 5,\n  distortTex: 'https://bigaton-playground.data2.media/file/VvjUtEBHSx6Ch9MZ0I9fQ',\n  warpOn: false, warpStrength: 0.025, warpSpeed: 8, warpScale: 0.5,\n  waveOn: false, waveAmount: 7, waveSpeed: 10, waveStrength: 7.5, waveX: 0, waveY: 0.5,\n  roundWaveOn: false, roundWaveStrength: 0.7, roundWaveSpeed: 2,\n  windOn: false, grassSpeed: 2, grassWind: 20, grassRadialBend: 0.1,\n  // --- Special / Render ---\n  pixelateOn: false, pixelateSize: 32,\n  radialClipOn: false, radialStart: 90, radialClip1: 45, radialClip2: 0,\n  chromaOn: false, chromAmount: 1, chromAlpha: 0.4,\n  motionBlurOn: false, motionAngle: 0.1, motionDist: 1.25,\n  ghostOn: false, ghostBoost: 1, ghostTransp: 0, ghostBlend: 1,\n  overlayOn: false, overlayColor: '#ffffff', overlayGlow: 1, overlayBlend: 1, overlayScrollX: 0.25, overlayScrollY: 0.25, overlayMult: false,\n  overlayTex: 'https://bigaton-playground.data2.media/file/9J_qvz3CP4560ne5J4kFu',\n  fadeOn: false, fadeAmount: -0.1, fadeBurnWidth: 0.025, fadeBurnTransition: 0.075, fadeBurnColor: '#ffff00', fadeBurnGlow: 2,\n  holoOn: false, holoStripes: 0.1, holoUnmod: 0, holoSpeed: 4.5, holoMinAlpha: 0.1, holoMaxAlpha: 0.75, holoStripeColor: '#00ffff', holoBlend: 1,\n  flickerOn: false, flickerPercent: 0.05, flickerFreq: 0.2, flickerAlpha: 0,\n  alphaCutoffOn: false, alphaCutoff: 0.25,\n  alphaRoundOn: false, alphaRoundThreshold: 0.5,\n};\nfunction gv(d2, n) {\n  const sh = d2.shared.get('ss_' + n, undefined, { global: true });\n  if (sh !== undefined && sh !== null && sh !== '') return sh;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\nfunction hexToRgb(h) {\n  h = (h || '#000').toString().replace('#', '');\n  if (h.length === 3) h = h.split('').map(c => c + c).join('');\n  const n = parseInt(h, 16) || 0;\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nconst VERT = `attribute vec2 aPos; varying vec2 vUv;\nvoid main(){ vUv = aPos*0.5+0.5; gl_Position = vec4(aPos,0.0,1.0); }`;\n\nconst FRAG = `precision highp float;\nvarying vec2 vUv;\nuniform sampler2D uTex; uniform sampler2D uDistortTex; uniform sampler2D uOverlayTex;\nuniform vec2 uCanvas; uniform vec2 uSprite; uniform float uTime;\nuniform int uGlowOn; uniform vec3 uGlowColor; uniform float uGlow; uniform float uGlowGlobal;\nuniform int uGlitchOn; uniform float uGlitchAmount; uniform float uGlitchSize; uniform float uGlitchSpeed;\nuniform int uShineOn; uniform vec3 uShineColor; uniform float uShineLocation; uniform float uShineRotate; uniform float uShineWidth; uniform float uShineGlow;\nuniform int uRotateOn; uniform float uRotateUv;\nuniform int uOffsetOn; uniform float uOffsetX; uniform float uOffsetY;\nuniform int uClipOn; uniform float uClipLeft; uniform float uClipRight; uniform float uClipUp; uniform float uClipDown;\nuniform int uTexScrollOn; uniform float uScrollX; uniform float uScrollY;\nuniform int uTwistOn; uniform float uTwistAmount; uniform float uTwistPosX; uniform float uTwistPosY; uniform float uTwistRadius;\nuniform int uFishOn; uniform float uFishEye;\nuniform int uPinchOn; uniform float uPinch;\nuniform int uZoomOn; uniform float uZoom;\nuniform int uHandOn; uniform float uHandAmount; uniform float uHandSpeed;\nuniform int uShakeOn; uniform float uShakeSpeed; uniform float uShakeX; uniform float uShakeY;\nuniform int uDistortOn; uniform float uDistAmount; uniform float uDistXSpeed; uniform float uDistYSpeed;\nuniform int uWarpOn; uniform float uWarpStrength; uniform float uWarpSpeed; uniform float uWarpScale;\nuniform int uWaveOn; uniform float uWaveAmount; uniform float uWaveSpeed; uniform float uWaveStrength; uniform float uWaveX; uniform float uWaveY;\nuniform int uRoundOn; uniform float uRoundStrength; uniform float uRoundSpeed;\nuniform int uWindOn; uniform float uGrassSpeed; uniform float uGrassWind; uniform float uGrassBend;\nuniform int uPixelateOn; uniform float uPixelateSize;\nuniform int uRadialOn; uniform float uRadialStart; uniform float uRadialClip1; uniform float uRadialClip2;\nuniform int uChromaOn; uniform float uChromAmount; uniform float uChromAlpha;\nuniform int uMotionOn; uniform float uMotionAngle; uniform float uMotionDist;\nuniform int uGhostOn; uniform float uGhostBoost; uniform float uGhostTransp; uniform float uGhostBlend;\nuniform int uOverlayOn; uniform vec3 uOverlayColor; uniform float uOverlayGlow; uniform float uOverlayBlend; uniform float uOverlayScrollX; uniform float uOverlayScrollY; uniform int uOverlayMult;\nuniform int uFadeOn; uniform float uFadeAmount; uniform float uFadeBurnWidth; uniform float uFadeBurnTransition; uniform vec3 uFadeBurnColor; uniform float uFadeBurnGlow;\nuniform int uHoloOn; uniform float uHoloStripes; uniform float uHoloUnmod; uniform float uHoloSpeed; uniform float uHoloMinAlpha; uniform float uHoloMaxAlpha; uniform vec3 uHoloStripeColor; uniform float uHoloBlend;\nuniform int uFlickerOn; uniform float uFlickerPercent; uniform float uFlickerFreq; uniform float uFlickerAlpha;\nuniform int uAlphaCutoffOn; uniform float uAlphaCutoff;\nuniform int uAlphaRoundOn; uniform float uAlphaRoundThreshold;\n\nfloat rand2(vec2 seed, float offset, float speed, float t){\n  return mod(fract(sin(dot(seed * floor(50.0 + mod(t,1.0)*12.0*speed), vec2(127.1,311.7))) * 43758.5453123) + offset, 1.0);\n}\nvec2 rotMB(vec2 n, float c, float s){ return vec2(n.x*c + n.y*s, -n.x*s + n.y*c); }\nfloat remap01(float v, float oMin, float oMax){ return oMin + v*(oMax-oMin); }\n\nvoid main(){\n  vec2 q = (vUv - 0.5) * uCanvas;\n  vec2 sUv = q / uSprite + 0.5;\n  float tX = uTime * 0.05;\n  float tY = uTime;\n  vec2 uvRect = sUv;\n  vec2 uv = sUv;\n  float seed = 0.0;\n  const float PI = 3.14159265359;\n\n  float clipMask = 1.0;\n  if (uClipOn == 1){\n    if (uvRect.x < uClipLeft || uvRect.x > 1.0 - uClipRight || uvRect.y < uClipDown || uvRect.y > 1.0 - uClipUp) clipMask = 0.0;\n  }\n  if (uRadialOn == 1){\n    float sa = uRadialStart - uRadialClip1;\n    float ea = uRadialStart + uRadialClip2;\n    vec2 ac = vec2(mix(-1.0, 1.0, uvRect.x), mix(-1.0, 1.0, uvRect.y));\n    float ang = atan(ac.y, ac.x) * 57.29578;\n    if (ang < 0.0) ang += 360.0;\n    if (ang >= sa && ang <= ea) clipMask = 0.0;\n  }\n\n  if (uRotateOn == 1){ float c = cos(uRotateUv); float s = sin(uRotateUv); uv -= 0.5; uv = mat2(c, s, -s, c) * uv; uv += 0.5; }\n  if (uOffsetOn == 1){ uv += vec2(uOffsetX, uOffsetY); }\n  if (uTwistOn == 1){\n    vec2 pos = vec2(uTwistPosX, uTwistPosY);\n    vec2 t = uv - pos; float rad = uTwistRadius;\n    float pct = (rad - length(t)) / rad;\n    float theta = pct * pct * (2.0 * sin(uTwistAmount)) * 8.0;\n    float s = sin(theta); float c = cos(theta);\n    float beta = max(sign(rad - length(t)), 0.0);\n    t = vec2(dot(t, vec2(c, -s)), dot(t, vec2(s, c))) * beta + t * (1.0 - beta);\n    uv = t + pos;\n  }\n  if (uFishOn == 1){\n    float bind = length(vec2(0.5)); vec2 dF = uv - 0.5; float dl = length(dF);\n    float fi = (PI / bind) * (uFishEye + 0.001);\n    uv = vec2(0.5) + (dF / max(0.0001, dl)) * tan(dl * fi) * bind / tan(bind * fi);\n  }\n  if (uPinchOn == 1){\n    vec2 dP = uv - 0.5;\n    float pin = (PI / length(vec2(0.5))) * (-uPinch + 0.001);\n    uv = vec2(0.5) + (dP / max(1e-4, length(dP))) * atan(length(dP) * -pin * 10.0) * 0.5 / atan(-pin * 5.0);\n  }\n  if (uZoomOn == 1){ uv -= 0.5; uv *= uZoom; uv += 0.5; }\n  if (uHandOn == 1){\n    vec2 uc = uvRect; float hs = floor(tX * 20.0 * uHandSpeed);\n    uc.x = sin((uc.x * uHandAmount + hs) * 4.0);\n    uc.y = cos((uc.y * uHandAmount + hs) * 4.0);\n    uv = mix(uv, uv + uc, 0.0005 * uHandAmount);\n  }\n  if (uShakeOn == 1){\n    float xs = sin(tX * uShakeSpeed * 50.0) * uShakeX;\n    float ys = cos(tX * uShakeSpeed * 50.0) * uShakeY;\n    uv += vec2(xs * 0.012, ys * 0.01);\n  }\n  if (uDistortOn == 1){\n    vec2 du = uvRect;\n    du.x += mod(tX * uDistXSpeed, 1.0); du.y += mod(tX * uDistYSpeed, 1.0);\n    float da = (texture2D(uDistortTex, du).r - 0.5) * 0.2 * uDistAmount;\n    uv += vec2(da, da);\n  }\n  if (uWarpOn == 1){\n    float tau = 6.28318530718;\n    float xw = tY * uWarpSpeed + uv.x * tau / uWarpScale;\n    float yw = tY * uWarpSpeed + uv.y * tau / uWarpScale;\n    uv += vec2(sin(xw), sin(yw)) * uWarpStrength;\n  }\n  if (uWaveOn == 1){\n    vec2 uw = vec2(uWaveX, uWaveY) - uv; uw = mod(uw, 1.0);\n    uw.x *= uCanvas.x / uCanvas.y;\n    float ang = sqrt(dot(uw, uw)) * uWaveAmount - tY * uWaveSpeed;\n    uv = uv + uw * sin(ang) * (uWaveStrength / 1000.0);\n  }\n  if (uRoundOn == 1){\n    float xw = 0.5 - uvRect.x;\n    float yw = (0.5 - uvRect.y) * (uCanvas.y / uCanvas.x);\n    float ripple = -sqrt(xw * xw + yw * yw);\n    uv += mod(sin((ripple + tY * (uRoundSpeed / 10.0)) / 0.015) * (uRoundStrength / 10.0), 1.0);\n  }\n  if (uWindOn == 1){\n    float wo = sin(tX * uGrassSpeed * 10.0); vec2 wc = vec2(0.5, 0.1);\n    uv.x = mod(abs(mix(uv.x, uv.x + (uGrassWind * 0.01 * wo), uvRect.y)), 1.0);\n    vec2 d = uv - wc; float d2 = dot(d, d);\n    uv = uv + vec2(d.y, -d.x) * (d2 * wo) * uGrassBend;\n  }\n  if (uTexScrollOn == 1){ uv.x += mod(tY * uScrollX, 1.0); uv.y += mod(tY * uScrollY, 1.0); }\n  if (uPixelateOn == 1){ uv = floor(uv * uPixelateSize) / uPixelateSize; }\n  if (uGlitchOn == 1){\n    vec2 ug = uvRect; ug.y -= 0.5;\n    float n1 = pow(rand2(floor(ug*vec2(24.,19.)*uGlitchSize)*4.0, seed, uGlitchSpeed, tX), 3.0);\n    float n2 = pow(rand2(floor(ug*vec2(38.,14.)*uGlitchSize)*4.0, seed, uGlitchSpeed, tX), 3.0);\n    uv.x += n1 * uGlitchAmount * n2 * 0.02 * rand2(vec2(2.0,1.0), seed, uGlitchSpeed, tX);\n  }\n\n  // ---- SAMPLE ----\n  vec4 col;\n  if (uTexScrollOn == 1) { col = texture2D(uTex, fract(uv)); }\n  else {\n    col = texture2D(uTex, uv);\n    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) col = vec4(0.0);\n  }\n  float originalAlpha = col.a;\n\n  if (uChromaOn == 1){\n    vec4 r = texture2D(uTex, uv + vec2(uChromAmount / 10.0, 0.0));\n    vec4 b = texture2D(uTex, uv + vec2(-uChromAmount / 10.0, 0.0));\n    col = vec4(r.r * r.a, col.g, b.b * b.a, max(max(r.a, b.a) * uChromAlpha, col.a));\n  }\n  if (uMotionOn == 1){\n    float ang = uMotionAngle * 3.1415926; float c = cos(ang); float s = sin(ang);\n    float d = uMotionDist * 0.005;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(-d, -d), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(-d*2.0, -d*2.0), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(-d*3.0, -d*3.0), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(-d*4.0, -d*4.0), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(d, d), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(d*2.0, d*2.0), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(d*3.0, d*3.0), c, s)).rgb;\n    col.rgb += texture2D(uTex, uv + rotMB(vec2(d*4.0, d*4.0), c, s)).rgb;\n    col.rgb /= 9.0;\n  }\n  col.a *= clipMask;\n\n  if (uGlowOn == 1) col.rgb *= uGlowGlobal;\n\n  if (uGhostOn == 1){\n    float lum = 0.3 * col.r + 0.59 * col.g + 0.11 * col.b;\n    vec4 g; g.a = clamp(lum - uGhostTransp, 0.0, 1.0) * col.a; g.rgb = col.rgb * (lum + uGhostBoost);\n    col = mix(col, g, uGhostBlend);\n  }\n  if (uOverlayOn == 1){\n    vec2 ouv = uvRect;\n    ouv.x += mod(tY * uOverlayScrollX, 1.0); ouv.y += mod(tY * uOverlayScrollY, 1.0);\n    vec4 oc = texture2D(uOverlayTex, fract(ouv));\n    oc.rgb *= uOverlayColor * uOverlayGlow;\n    if (uOverlayMult == 1){ col = mix(col, col * oc, uOverlayBlend); }\n    else { col.rgb += oc.rgb * oc.a * uOverlayBlend; }\n  }\n  if (uFadeOn == 1){\n    float fadeTemp = texture2D(uDistortTex, uvRect).r;\n    float fade = smoothstep(uFadeAmount, uFadeAmount + uFadeBurnTransition, fadeTemp);\n    float fadeBurn = clamp(smoothstep(uFadeAmount - uFadeBurnWidth, uFadeAmount - uFadeBurnWidth + 0.1, fadeTemp) * uFadeAmount, 0.0, 1.0);\n    col.a *= fade;\n    vec3 burnCol = uFadeBurnColor * uFadeBurnGlow;\n    col.rgb += fadeBurn * burnCol * originalAlpha * (1.0 - col.a);\n    col.a += fadeBurn * originalAlpha * (1.0 - col.a);\n  }\n\n  if (uShineOn == 1){\n    vec2 us = uvRect; float c = cos(uShineRotate); float s = sin(uShineRotate);\n    us -= 0.5; us = mat2(c, s, -s, c) * us; us += 0.5;\n    float proj = (us.x + us.y) * 0.5;\n    float wp = 1.0 - (abs(proj - uShineLocation) / uShineWidth);\n    float band = max(wp, 0.0) * step(0.0, sign(proj - (uShineLocation - uShineWidth))) * step(0.0, sign((uShineLocation + uShineWidth) - proj));\n    col.rgb += col.a * band * uShineGlow * uShineColor;\n  }\n  if (uGlowOn == 1 && uGlow > 0.0){\n    float halo = 0.0; float r1 = 0.02 + uGlow * 0.0014;\n    for (int k = 0; k < 24; k++){\n      float a = float(k) / 24.0 * 6.28318;\n      vec2 off = vec2(cos(a), sin(a));\n      halo += texture2D(uTex, uv + off * r1).a;\n      halo += texture2D(uTex, uv + off * r1 * 2.0).a;\n    }\n    halo /= 48.0;\n    float emiss = clamp(halo * uGlow * 0.12, 0.0, 6.0);\n    col.rgb += uGlowColor * emiss;\n    col.a = max(col.a, clamp(halo * uGlow * 0.15, 0.0, 1.0));\n  }\n\n  if (uHoloOn == 1){\n    float total = max(uHoloStripes + uHoloUnmod, 0.001);\n    float yc = mod(uvRect.y + mod(tX, 1.0) * uHoloSpeed, total) / total;\n    yc = abs(yc);\n    float a = remap01(clamp(yc - (uHoloUnmod / total), 0.0, 1.0), uHoloMinAlpha, clamp(uHoloMaxAlpha, 0.0, 1.0));\n    float mask = max(sign((uHoloUnmod / total) - yc), 0.0);\n    vec4 hr = col;\n    hr.a *= mix(a, 1.0, mask);\n    hr.rgb *= max(1.0, uHoloMaxAlpha * max(sign(yc - (uHoloUnmod / total)), 0.0));\n    mask = 1.0 - step(0.01, mask);\n    hr.rgb += mask * uHoloStripeColor * col.a;\n    col = mix(col, hr, uHoloBlend);\n  }\n  if (uFlickerOn == 1){\n    float tw = uTime * 3.0;\n    col.a *= clamp(col.a * step(fract(0.05 + tw * uFlickerFreq), 1.0 - uFlickerPercent) + uFlickerAlpha, 0.0, 1.0);\n  }\n  if (uAlphaCutoffOn == 1){ col.a *= step(uAlphaCutoff + 0.01, col.a); }\n  if (uAlphaRoundOn == 1){ col.a = step(uAlphaRoundThreshold, col.a); }\n\n  gl_FragColor = col;\n}`;\n\nexport default {\n  css: `\n  .wrap{ position:relative; width:100%; height:100%; min-height:200px; overflow:hidden; }\n  .stage{ position:absolute; inset:0; width:100%; height:100%; }\n  .stage canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }\n  .err{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:#ff6b6b; font:13px sans-serif; padding:20px; text-align:center; }\n  `,\n\n  render(d2) {\n    return `<div class=\"wrap\"><div class=\"stage\" data-d2-static></div></div>`;\n  },\n\n  mounted(d2, root) {\n    const stage = d2.$('.stage');\n    const cv = document.createElement('canvas');\n    cv.style.width = '100%'; cv.style.height = '100%';\n    stage.appendChild(cv);\n\n    const gl = cv.getContext('webgl') || cv.getContext('experimental-webgl');\n    if (!gl) { stage.insertAdjacentHTML('beforeend', '<div class=\"err\">WebGL não disponível.</div>'); return; }\n\n    const S = { raf: 0, imgAspect: 1, url: null, distUrl: null, ovUrl: null, def: null };\n\n    function sh(type, src) {\n      const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);\n      if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));\n      return s;\n    }\n    let prog;\n    try {\n      prog = gl.createProgram();\n      gl.attachShader(prog, sh(gl.VERTEX_SHADER, VERT));\n      gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FRAG));\n      gl.linkProgram(prog);\n      if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));\n    } catch (e) {\n      stage.insertAdjacentHTML('beforeend', '<div class=\"err\">Shader error: ' + e.message + '</div>');\n      return;\n    }\n    gl.useProgram(prog);\n\n    const buf = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(prog, 'aPos');\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);\n\n    const L = {};\n    function loc(n) { L[n] = gl.getUniformLocation(prog, n); }\n    ['uTex', 'uDistortTex', 'uOverlayTex', 'uCanvas', 'uSprite', 'uTime',\n     'uGlowOn', 'uGlowColor', 'uGlow', 'uGlowGlobal',\n     'uGlitchOn', 'uGlitchAmount', 'uGlitchSize', 'uGlitchSpeed',\n     'uShineOn', 'uShineColor', 'uShineLocation', 'uShineRotate', 'uShineWidth', 'uShineGlow',\n     'uRotateOn', 'uRotateUv', 'uOffsetOn', 'uOffsetX', 'uOffsetY',\n     'uClipOn', 'uClipLeft', 'uClipRight', 'uClipUp', 'uClipDown',\n     'uTexScrollOn', 'uScrollX', 'uScrollY',\n     'uTwistOn', 'uTwistAmount', 'uTwistPosX', 'uTwistPosY', 'uTwistRadius',\n     'uFishOn', 'uFishEye', 'uPinchOn', 'uPinch', 'uZoomOn', 'uZoom',\n     'uHandOn', 'uHandAmount', 'uHandSpeed',\n     'uShakeOn', 'uShakeSpeed', 'uShakeX', 'uShakeY',\n     'uDistortOn', 'uDistAmount', 'uDistXSpeed', 'uDistYSpeed',\n     'uWarpOn', 'uWarpStrength', 'uWarpSpeed', 'uWarpScale',\n     'uWaveOn', 'uWaveAmount', 'uWaveSpeed', 'uWaveStrength', 'uWaveX', 'uWaveY',\n     'uRoundOn', 'uRoundStrength', 'uRoundSpeed',\n     'uWindOn', 'uGrassSpeed', 'uGrassWind', 'uGrassBend',\n     'uPixelateOn', 'uPixelateSize',\n     'uRadialOn', 'uRadialStart', 'uRadialClip1', 'uRadialClip2',\n     'uChromaOn', 'uChromAmount', 'uChromAlpha',\n     'uMotionOn', 'uMotionAngle', 'uMotionDist',\n     'uGhostOn', 'uGhostBoost', 'uGhostTransp', 'uGhostBlend',\n     'uOverlayOn', 'uOverlayColor', 'uOverlayGlow', 'uOverlayBlend', 'uOverlayScrollX', 'uOverlayScrollY', 'uOverlayMult',\n     'uFadeOn', 'uFadeAmount', 'uFadeBurnWidth', 'uFadeBurnTransition', 'uFadeBurnColor', 'uFadeBurnGlow',\n     'uHoloOn', 'uHoloStripes', 'uHoloUnmod', 'uHoloSpeed', 'uHoloMinAlpha', 'uHoloMaxAlpha', 'uHoloStripeColor', 'uHoloBlend',\n     'uFlickerOn', 'uFlickerPercent', 'uFlickerFreq', 'uFlickerAlpha',\n     'uAlphaCutoffOn', 'uAlphaCutoff', 'uAlphaRoundOn', 'uAlphaRoundThreshold'].forEach(loc);\n\n    function mkTex(unit, wrap, px) {\n      const tx = gl.createTexture();\n      gl.activeTexture(gl.TEXTURE0 + unit);\n      gl.bindTexture(gl.TEXTURE_2D, tx);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n      if (px) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, px);\n      return tx;\n    }\n    const tex = mkTex(0, gl.CLAMP_TO_EDGE, new Uint8Array([0, 0, 0, 0]));\n    const texD = mkTex(1, gl.REPEAT, new Uint8Array([128, 128, 128, 255]));\n    const texO = mkTex(2, gl.REPEAT, new Uint8Array([255, 255, 255, 255]));\n    gl.uniform1i(L.uTex, 0); gl.uniform1i(L.uDistortTex, 1); gl.uniform1i(L.uOverlayTex, 2);\n    gl.activeTexture(gl.TEXTURE0);\n\n    function makeStar() {\n      const c = document.createElement('canvas'); c.width = c.height = 256;\n      const x = c.getContext('2d');\n      const cx = 128, cy = 128, spikes = 5, outer = 104, inner = 46;\n      const grad = x.createRadialGradient(cx, cy, 8, cx, cy, outer);\n      grad.addColorStop(0, '#ffffff'); grad.addColorStop(0.45, '#ffd24a'); grad.addColorStop(1, '#ff6a18');\n      x.fillStyle = grad; x.beginPath();\n      let rot = Math.PI / 2 * 3; const step = Math.PI / spikes;\n      x.moveTo(cx, cy - outer);\n      for (let i = 0; i < spikes; i++) {\n        x.lineTo(cx + Math.cos(rot) * outer, cy + Math.sin(rot) * outer); rot += step;\n        x.lineTo(cx + Math.cos(rot) * inner, cy + Math.sin(rot) * inner); rot += step;\n      }\n      x.closePath(); x.fill();\n      return c;\n    }\n    S.def = makeStar();\n\n    function upload(unit, txt, src) { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, txt); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src); gl.activeTexture(gl.TEXTURE0); }\n    function loadMain(url) {\n      if (!url) { upload(0, tex, S.def); S.imgAspect = 1; return; }\n      const img = new Image(); img.crossOrigin = 'anonymous';\n      img.onload = () => { try { upload(0, tex, img); S.imgAspect = img.naturalWidth / img.naturalHeight; } catch (e) { upload(0, tex, S.def); S.imgAspect = 1; } };\n      img.onerror = () => { upload(0, tex, S.def); S.imgAspect = 1; };\n      img.src = url;\n    }\n    function loadUnit(unit, txt, url) {\n      if (!url) return;\n      const img = new Image(); img.crossOrigin = 'anonymous';\n      img.onload = () => { try { upload(unit, txt, img); } catch (e) {} };\n      img.onerror = () => {};\n      img.src = url;\n    }\n    loadMain(gv(d2, 'imageUrl')); S.url = gv(d2, 'imageUrl');\n    loadUnit(1, texD, gv(d2, 'distortTex')); S.distUrl = gv(d2, 'distortTex');\n    loadUnit(2, texO, gv(d2, 'overlayTex')); S.ovUrl = gv(d2, 'overlayTex');\n\n    function resize() {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const w = stage.clientWidth || 400, h = stage.clientHeight || 300;\n      cv.width = Math.max(1, Math.round(w * dpr));\n      cv.height = Math.max(1, Math.round(h * dpr));\n      gl.viewport(0, 0, cv.width, cv.height);\n    }\n    resize();\n    const ro = new ResizeObserver(resize); ro.observe(stage);\n\n    const start = performance.now();\n    function frame() {\n      const url = gv(d2, 'imageUrl'); if (url !== S.url) { S.url = url; loadMain(url); }\n      const dUrl = gv(d2, 'distortTex'); if (dUrl !== S.distUrl) { S.distUrl = dUrl; loadUnit(1, texD, dUrl); }\n      const oUrl = gv(d2, 'overlayTex'); if (oUrl !== S.ovUrl) { S.ovUrl = oUrl; loadUnit(2, texO, oUrl); }\n\n      const cw = cv.width, ch = cv.height;\n      const fit = Math.min(cw, ch) * 0.6;\n      const ia = S.imgAspect || 1;\n      let dw, dh;\n      if (ia >= 1) { dw = fit; dh = fit / ia; } else { dh = fit; dw = fit * ia; }\n      const t = (performance.now() - start) / 1000;\n\n      gl.useProgram(prog);\n      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex);\n      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, texD);\n      gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, texO);\n      gl.activeTexture(gl.TEXTURE0);\n\n      gl.uniform2f(L.uCanvas, cw, ch);\n      gl.uniform2f(L.uSprite, dw, dh);\n      gl.uniform1f(L.uTime, t);\n\n      const I = (n, v) => gl.uniform1i(L[n], v ? 1 : 0);\n      const F = (n, k) => gl.uniform1f(L[n], +gv(d2, k));\n      const C = (n, k) => { const c = hexToRgb(gv(d2, k)); gl.uniform3f(L[n], c[0], c[1], c[2]); };\n\n      I('uGlowOn', gv(d2, 'glowOn')); C('uGlowColor', 'glowColor'); F('uGlow', 'glow'); F('uGlowGlobal', 'glowGlobal');\n      I('uGlitchOn', gv(d2, 'glitchOn')); F('uGlitchAmount', 'glitchAmount'); F('uGlitchSize', 'glitchSize'); F('uGlitchSpeed', 'glitchSpeed');\n      I('uShineOn', gv(d2, 'shineOn')); C('uShineColor', 'shineColor');\n      let sloc = +gv(d2, 'shineLocation'); if (gv(d2, 'shineAnimate')) sloc = (t * +gv(d2, 'shineSpeed')) % 1;\n      gl.uniform1f(L.uShineLocation, sloc);\n      F('uShineRotate', 'shineRotate'); F('uShineWidth', 'shineWidth'); F('uShineGlow', 'shineGlow');\n\n      I('uRotateOn', gv(d2, 'rotateUvOn')); F('uRotateUv', 'rotateUv');\n      I('uOffsetOn', gv(d2, 'offsetUvOn')); F('uOffsetX', 'offsetUvX'); F('uOffsetY', 'offsetUvY');\n      I('uClipOn', gv(d2, 'clipUvOn')); F('uClipLeft', 'clipLeft'); F('uClipRight', 'clipRight'); F('uClipUp', 'clipUp'); F('uClipDown', 'clipDown');\n      I('uTexScrollOn', gv(d2, 'texScrollOn')); F('uScrollX', 'scrollX'); F('uScrollY', 'scrollY');\n      I('uTwistOn', gv(d2, 'twistOn')); F('uTwistAmount', 'twistAmount'); F('uTwistPosX', 'twistPosX'); F('uTwistPosY', 'twistPosY'); F('uTwistRadius', 'twistRadius');\n      I('uFishOn', gv(d2, 'fishEyeOn')); F('uFishEye', 'fishEye');\n      I('uPinchOn', gv(d2, 'pinchOn')); F('uPinch', 'pinch');\n      I('uZoomOn', gv(d2, 'zoomOn')); F('uZoom', 'zoom');\n      I('uHandOn', gv(d2, 'handDrawnOn')); F('uHandAmount', 'handDrawnAmount'); F('uHandSpeed', 'handDrawnSpeed');\n      I('uShakeOn', gv(d2, 'shakeOn')); F('uShakeSpeed', 'shakeSpeed'); F('uShakeX', 'shakeX'); F('uShakeY', 'shakeY');\n      I('uDistortOn', gv(d2, 'distortOn')); F('uDistAmount', 'distortAmount'); F('uDistXSpeed', 'distortXSpeed'); F('uDistYSpeed', 'distortYSpeed');\n      I('uWarpOn', gv(d2, 'warpOn')); F('uWarpStrength', 'warpStrength'); F('uWarpSpeed', 'warpSpeed'); F('uWarpScale', 'warpScale');\n      I('uWaveOn', gv(d2, 'waveOn')); F('uWaveAmount', 'waveAmount'); F('uWaveSpeed', 'waveSpeed'); F('uWaveStrength', 'waveStrength'); F('uWaveX', 'waveX'); F('uWaveY', 'waveY');\n      I('uRoundOn', gv(d2, 'roundWaveOn')); F('uRoundStrength', 'roundWaveStrength'); F('uRoundSpeed', 'roundWaveSpeed');\n      I('uWindOn', gv(d2, 'windOn')); F('uGrassSpeed', 'grassSpeed'); F('uGrassWind', 'grassWind'); F('uGrassBend', 'grassRadialBend');\n\n      I('uPixelateOn', gv(d2, 'pixelateOn')); F('uPixelateSize', 'pixelateSize');\n      I('uRadialOn', gv(d2, 'radialClipOn')); F('uRadialStart', 'radialStart'); F('uRadialClip1', 'radialClip1'); F('uRadialClip2', 'radialClip2');\n      I('uChromaOn', gv(d2, 'chromaOn')); F('uChromAmount', 'chromAmount'); F('uChromAlpha', 'chromAlpha');\n      I('uMotionOn', gv(d2, 'motionBlurOn')); F('uMotionAngle', 'motionAngle'); F('uMotionDist', 'motionDist');\n      I('uGhostOn', gv(d2, 'ghostOn')); F('uGhostBoost', 'ghostBoost'); F('uGhostTransp', 'ghostTransp'); F('uGhostBlend', 'ghostBlend');\n      I('uOverlayOn', gv(d2, 'overlayOn')); C('uOverlayColor', 'overlayColor'); F('uOverlayGlow', 'overlayGlow'); F('uOverlayBlend', 'overlayBlend'); F('uOverlayScrollX', 'overlayScrollX'); F('uOverlayScrollY', 'overlayScrollY'); I('uOverlayMult', gv(d2, 'overlayMult'));\n      I('uFadeOn', gv(d2, 'fadeOn')); F('uFadeAmount', 'fadeAmount'); F('uFadeBurnWidth', 'fadeBurnWidth'); F('uFadeBurnTransition', 'fadeBurnTransition'); C('uFadeBurnColor', 'fadeBurnColor'); F('uFadeBurnGlow', 'fadeBurnGlow');\n      I('uHoloOn', gv(d2, 'holoOn')); F('uHoloStripes', 'holoStripes'); F('uHoloUnmod', 'holoUnmod'); F('uHoloSpeed', 'holoSpeed'); F('uHoloMinAlpha', 'holoMinAlpha'); F('uHoloMaxAlpha', 'holoMaxAlpha'); C('uHoloStripeColor', 'holoStripeColor'); F('uHoloBlend', 'holoBlend');\n      I('uFlickerOn', gv(d2, 'flickerOn')); F('uFlickerPercent', 'flickerPercent'); F('uFlickerFreq', 'flickerFreq'); F('uFlickerAlpha', 'flickerAlpha');\n      I('uAlphaCutoffOn', gv(d2, 'alphaCutoffOn')); F('uAlphaCutoff', 'alphaCutoff');\n      I('uAlphaRoundOn', gv(d2, 'alphaRoundOn')); F('uAlphaRoundThreshold', 'alphaRoundThreshold');\n\n      const bg = hexToRgb(gv(d2, 'bg'));\n      gl.clearColor(bg[0], bg[1], bg[2], 1.0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n      S.raf = requestAnimationFrame(frame);\n    }\n    S.raf = requestAnimationFrame(frame);\n\n    d2.onUnmount(() => { cancelAnimationFrame(S.raf); ro.disconnect(); });\n  }\n};\n"}},"imageUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"imageUrl"}}}},"bg":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bg"}}}},"glowOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowOn"}}}},"glowColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowColor"}}}},"glow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glow"}}}},"glowGlobal":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glowGlobal"}}}},"glitchOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchOn"}}}},"glitchAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchAmount"}}}},"glitchSize":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchSize"}}}},"glitchSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glitchSpeed"}}}},"shineOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineOn"}}}},"shineColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineColor"}}}},"shineLocation":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineLocation"}}}},"shineRotate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineRotate"}}}},"shineWidth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineWidth"}}}},"shineGlow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineGlow"}}}},"shineAnimate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineAnimate"}}}},"shineSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shineSpeed"}}}},"rotateUvOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rotateUvOn"}}}},"rotateUv":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rotateUv"}}}},"offsetUvOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetUvOn"}}}},"offsetUvX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetUvX"}}}},"offsetUvY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetUvY"}}}},"clipUvOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clipUvOn"}}}},"clipLeft":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clipLeft"}}}},"clipRight":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clipRight"}}}},"clipUp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clipUp"}}}},"clipDown":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clipDown"}}}},"texScrollOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"texScrollOn"}}}},"scrollX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"scrollX"}}}},"scrollY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"scrollY"}}}},"twistOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"twistOn"}}}},"twistAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"twistAmount"}}}},"twistPosX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"twistPosX"}}}},"twistPosY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"twistPosY"}}}},"twistRadius":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"twistRadius"}}}},"fishEyeOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fishEyeOn"}}}},"fishEye":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fishEye"}}}},"pinchOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pinchOn"}}}},"pinch":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pinch"}}}},"zoomOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zoomOn"}}}},"zoom":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zoom"}}}},"handDrawnOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"handDrawnOn"}}}},"handDrawnAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"handDrawnAmount"}}}},"handDrawnSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"handDrawnSpeed"}}}},"shakeOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shakeOn"}}}},"shakeSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shakeSpeed"}}}},"shakeX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shakeX"}}}},"shakeY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shakeY"}}}},"distortOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distortOn"}}}},"distortAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distortAmount"}}}},"distortXSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distortXSpeed"}}}},"distortYSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distortYSpeed"}}}},"distortTex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distortTex"}}}},"warpOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"warpOn"}}}},"warpStrength":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"warpStrength"}}}},"warpSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"warpSpeed"}}}},"warpScale":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"warpScale"}}}},"waveOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveOn"}}}},"waveAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveAmount"}}}},"waveSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveSpeed"}}}},"waveStrength":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveStrength"}}}},"waveX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveX"}}}},"waveY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"waveY"}}}},"roundWaveOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"roundWaveOn"}}}},"roundWaveStrength":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"roundWaveStrength"}}}},"roundWaveSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"roundWaveSpeed"}}}},"windOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"windOn"}}}},"grassSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"grassSpeed"}}}},"grassWind":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"grassWind"}}}},"grassRadialBend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"grassRadialBend"}}}},"pixelateOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pixelateOn"}}}},"pixelateSize":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pixelateSize"}}}},"radialClipOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"radialClipOn"}}}},"radialStart":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"radialStart"}}}},"radialClip1":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"radialClip1"}}}},"radialClip2":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"radialClip2"}}}},"chromaOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chromaOn"}}}},"chromAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chromAmount"}}}},"chromAlpha":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chromAlpha"}}}},"motionBlurOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"motionBlurOn"}}}},"motionAngle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"motionAngle"}}}},"motionDist":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"motionDist"}}}},"ghostOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ghostOn"}}}},"ghostBoost":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ghostBoost"}}}},"ghostTransp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ghostTransp"}}}},"ghostBlend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ghostBlend"}}}},"overlayOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayOn"}}}},"overlayColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayColor"}}}},"overlayGlow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayGlow"}}}},"overlayBlend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayBlend"}}}},"overlayScrollX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayScrollX"}}}},"overlayScrollY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayScrollY"}}}},"overlayMult":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayMult"}}}},"overlayTex":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overlayTex"}}}},"fadeOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeOn"}}}},"fadeAmount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeAmount"}}}},"fadeBurnWidth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeBurnWidth"}}}},"fadeBurnTransition":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeBurnTransition"}}}},"fadeBurnColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeBurnColor"}}}},"fadeBurnGlow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fadeBurnGlow"}}}},"holoOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoOn"}}}},"holoStripes":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoStripes"}}}},"holoUnmod":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoUnmod"}}}},"holoSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoSpeed"}}}},"holoMinAlpha":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoMinAlpha"}}}},"holoMaxAlpha":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoMaxAlpha"}}}},"holoStripeColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoStripeColor"}}}},"holoBlend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"holoBlend"}}}},"flickerOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flickerOn"}}}},"flickerPercent":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flickerPercent"}}}},"flickerFreq":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flickerFreq"}}}},"flickerAlpha":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flickerAlpha"}}}},"alphaCutoffOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"alphaCutoffOn"}}}},"alphaCutoff":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"alphaCutoff"}}}},"alphaRoundOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"alphaRoundOn"}}}},"alphaRoundThreshold":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"alphaRoundThreshold"}}}}}}}}}},{"_id":"6a3ab04f7c5f8651059316e2","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"spriteshader-controller","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Sprite Shader Controller — parameter panel driving a sibling `spriteshader` block\n// via shared UIState (keys 'ss_<param>', global). The shader polls these each frame.\nconst DEFAULTS = {\n  imageUrl: '', bg: '#15171f',\n  glowOn: false, glowColor: '#46c8ff', glow: 14, glowGlobal: 1,\n  glitchOn: false, glitchAmount: 3, glitchSize: 1, glitchSpeed: 20,\n  shineOn: false, shineColor: '#ffffff', shineLocation: 0.5, shineRotate: 0.8,\n  shineWidth: 0.12, shineGlow: 1.4, shineAnimate: true, shineSpeed: 0.35,\n  rotateUvOn: false, rotateUv: 0,\n  offsetUvOn: false, offsetUvX: 0, offsetUvY: 0,\n  clipUvOn: false, clipLeft: 0, clipRight: 0, clipUp: 0, clipDown: 0,\n  texScrollOn: false, scrollX: 0.3, scrollY: 0,\n  twistOn: false, twistAmount: 1, twistPosX: 0.5, twistPosY: 0.5, twistRadius: 0.75,\n  fishEyeOn: false, fishEye: 0.35,\n  pinchOn: false, pinch: 0.35,\n  zoomOn: false, zoom: 0.5,\n  handDrawnOn: false, handDrawnAmount: 10, handDrawnSpeed: 5,\n  shakeOn: false, shakeSpeed: 2.5, shakeX: 1.5, shakeY: 1,\n  distortOn: false, distortAmount: 0.5, distortXSpeed: 5, distortYSpeed: 5,\n  distortTex: 'https://bigaton-playground.data2.media/file/VvjUtEBHSx6Ch9MZ0I9fQ',\n  warpOn: false, warpStrength: 0.025, warpSpeed: 8, warpScale: 0.5,\n  waveOn: false, waveAmount: 7, waveSpeed: 10, waveStrength: 7.5, waveX: 0, waveY: 0.5,\n  roundWaveOn: false, roundWaveStrength: 0.7, roundWaveSpeed: 2,\n  windOn: false, grassSpeed: 2, grassWind: 20, grassRadialBend: 0.1,\n  pixelateOn: false, pixelateSize: 32,\n  radialClipOn: false, radialStart: 90, radialClip1: 45, radialClip2: 0,\n  chromaOn: false, chromAmount: 1, chromAlpha: 0.4,\n  motionBlurOn: false, motionAngle: 0.1, motionDist: 1.25,\n  ghostOn: false, ghostBoost: 1, ghostTransp: 0, ghostBlend: 1,\n  overlayOn: false, overlayColor: '#ffffff', overlayGlow: 1, overlayBlend: 1, overlayScrollX: 0.25, overlayScrollY: 0.25, overlayMult: false,\n  overlayTex: 'https://bigaton-playground.data2.media/file/9J_qvz3CP4560ne5J4kFu',\n  fadeOn: false, fadeAmount: -0.1, fadeBurnWidth: 0.025, fadeBurnTransition: 0.075, fadeBurnColor: '#ffff00', fadeBurnGlow: 2,\n  holoOn: false, holoStripes: 0.1, holoUnmod: 0, holoSpeed: 4.5, holoMinAlpha: 0.1, holoMaxAlpha: 0.75, holoStripeColor: '#00ffff', holoBlend: 1,\n  flickerOn: false, flickerPercent: 0.05, flickerFreq: 0.2, flickerAlpha: 0,\n  alphaCutoffOn: false, alphaCutoff: 0.25,\n  alphaRoundOn: false, alphaRoundThreshold: 0.5,\n};\n// distortion sections (slider-only): [title, toggleKey, [[key,label,min,max,step]...]]\nconst FX = [\n  ['Rotate UV', 'rotateUvOn', [['rotateUv', 'Angle (rad)', 0, 6.2831, 0.01]]],\n  ['Offset UV', 'offsetUvOn', [['offsetUvX', 'X', -1, 1, 0.01], ['offsetUvY', 'Y', -1, 1, 0.01]]],\n  ['Clip UV', 'clipUvOn', [['clipLeft', 'Left', 0, 1, 0.01], ['clipRight', 'Right', 0, 1, 0.01], ['clipUp', 'Up', 0, 1, 0.01], ['clipDown', 'Down', 0, 1, 0.01]]],\n  ['Texture Scroll', 'texScrollOn', [['scrollX', 'Speed X', -5, 5, 0.05], ['scrollY', 'Speed Y', -5, 5, 0.05]]],\n  ['Twist', 'twistOn', [['twistAmount', 'Amount', 0, 3.1416, 0.01], ['twistPosX', 'Pos X', 0, 1, 0.01], ['twistPosY', 'Pos Y', 0, 1, 0.01], ['twistRadius', 'Radius', 0, 3, 0.01]]],\n  ['Fish Eye', 'fishEyeOn', [['fishEye', 'Amount', 0, 0.5, 0.01]]],\n  ['Pinch', 'pinchOn', [['pinch', 'Amount', 0, 0.5, 0.01]]],\n  ['Zoom UV', 'zoomOn', [['zoom', 'Zoom (<1 in, >1 out)', 0.1, 5, 0.01]]],\n  ['Hand Drawn', 'handDrawnOn', [['handDrawnAmount', 'Amount', 0, 20, 0.1], ['handDrawnSpeed', 'Speed', 1, 15, 0.1]]],\n  ['Shake', 'shakeOn', [['shakeSpeed', 'Speed', 0, 20, 0.1], ['shakeX', 'X Mult', 0, 5, 0.05], ['shakeY', 'Y Mult', 0, 5, 0.05]]],\n  ['Distortion (texture)', 'distortOn', [['distortAmount', 'Amount', 0, 2, 0.01], ['distortXSpeed', 'Scroll X', -50, 50, 0.5], ['distortYSpeed', 'Scroll Y', -50, 50, 0.5]]],\n  ['Warp', 'warpOn', [['warpStrength', 'Strength', 0, 0.1, 0.001], ['warpSpeed', 'Speed', 0, 25, 0.1], ['warpScale', 'Scale', 0.05, 3, 0.01]]],\n  ['Wave', 'waveOn', [['waveAmount', 'Amount', 0, 25, 0.1], ['waveSpeed', 'Speed', 0, 25, 0.1], ['waveStrength', 'Strength', 0, 25, 0.1], ['waveX', 'Center X', 0, 1, 0.01], ['waveY', 'Center Y', 0, 1, 0.01]]],\n  ['Round Wave', 'roundWaveOn', [['roundWaveStrength', 'Strength', 0, 1, 0.01], ['roundWaveSpeed', 'Speed', 0, 5, 0.01]]],\n  ['Wind / Grass', 'windOn', [['grassSpeed', 'Speed', 0, 50, 0.1], ['grassWind', 'Bend Amount', 0, 50, 0.1], ['grassRadialBend', 'Radial Bend', 0, 5, 0.01]]],\n];\n\nfunction gv(d2, n) {\n  const sv = d2.state[n];\n  if (sv !== undefined) return sv;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\nfunction push(d2, n, v) {\n  d2.state[n] = v;\n  d2.shared.set('ss_' + n, v, { global: true });\n}\n\nexport default {\n  css: `\n  .panel{ width:100%; height:100%; box-sizing:border-box; overflow-y:auto; background:#13151d;\n    padding:16px; font-family:Inter,system-ui,sans-serif; color:#e7e9f0; }\n  .ttl{ font-size:13px; font-weight:700; letter-spacing:.04em; color:#aeb4c6; margin:0 0 12px; text-transform:uppercase; }\n  .grp{ font-size:11px; font-weight:700; letter-spacing:.08em; color:#7b8194; text-transform:uppercase; margin:18px 0 8px; padding-top:8px; border-top:1px solid #262a37; }\n  .sec{ background:#191c26; border:1px solid #262a37; border-radius:10px; margin-bottom:10px; overflow:hidden; }\n  .sh{ display:flex; align-items:center; justify-content:space-between; padding:9px 12px; font-weight:600; font-size:12.5px; background:#1e2230; }\n  .sb{ padding:10px 12px; display:flex; flex-direction:column; gap:11px; }\n  .r{ display:flex; flex-direction:column; gap:5px; }\n  .rl{ display:flex; justify-content:space-between; font-size:11.5px; color:#9aa0b2; }\n  .rl b{ color:#dfe3ee; font-weight:600; font-variant-numeric:tabular-nums; }\n  .rc{ flex-direction:row; align-items:center; justify-content:space-between; }\n  .rc span{ font-size:11.5px; color:#9aa0b2; }\n  input[type=range]{ width:100%; accent-color:#46c8ff; height:4px; }\n  input[type=color]{ width:46px; height:26px; border:1px solid #2c3142; border-radius:6px; background:#0e0f15; padding:0; cursor:pointer; }\n  .sw{ position:relative; display:inline-block; width:38px; height:21px; cursor:pointer; }\n  .sw input{ opacity:0; width:0; height:0; }\n  .sw span{ position:absolute; inset:0; background:#363b4d; border-radius:21px; transition:.2s; }\n  .sw span:before{ content:''; position:absolute; height:15px; width:15px; left:3px; top:3px; background:#fff; border-radius:50%; transition:.2s; }\n  .sw input:checked + span{ background:#46c8ff; }\n  .sw input:checked + span:before{ transform:translateX(17px); }\n  .urlbox{ width:100%; box-sizing:border-box; background:#0e0f15; border:1px solid #2c3142; color:#e7e9f0; border-radius:6px; padding:7px 8px; font-size:12px; }\n  .file{ font-size:11px; color:#9aa0b2; width:100%; }\n  .reset{ width:100%; margin-top:4px; background:#242838; border:1px solid #343a4d; color:#cfd4e2; border-radius:8px; padding:9px; font-size:12px; font-weight:600; cursor:pointer; }\n  .reset:hover{ background:#2c3142; }\n  `,\n\n  render(d2) {\n    const sld = (n, l, mn, mx, st) =>\n      `<div class=\"r\"><div class=\"rl\"><span>${l}</span><b>${(+gv(d2, n)).toFixed(2)}</b></div>` +\n      `<input class=\"ctl\" type=\"range\" data-name=\"${n}\" min=\"${mn}\" max=\"${mx}\" step=\"${st}\" value=\"${gv(d2, n)}\"></div>`;\n    const clr = (n, l) =>\n      `<div class=\"r rc\"><span>${l}</span><input class=\"ctl\" type=\"color\" data-name=\"${n}\" value=\"${gv(d2, n)}\"></div>`;\n    const tog = (n) =>\n      `<label class=\"sw\"><input class=\"tog\" type=\"checkbox\" data-name=\"${n}\" ${gv(d2, n) ? 'checked' : ''}><span></span></label>`;\n    const swr = (n, l) => `<div class=\"r rc\"><span>${l}</span>${tog(n)}</div>`;\n    const sec = (title, toggleKey, body) =>\n      `<div class=\"sec\"><div class=\"sh\"><span>${title}</span>${toggleKey ? tog(toggleKey) : ''}</div><div class=\"sb\">${body}</div></div>`;\n\n    const fxSections = FX.map(([title, tk, sliders]) =>\n      sec(title, tk, sliders.map(s => sld(s[0], s[1], s[2], s[3], s[4])).join(''))).join('');\n\n    const special =\n      sec('Pixelate', 'pixelateOn', sld('pixelateSize', 'Pixel Size', 4, 512, 1)) +\n      sec('Radial Clip', 'radialClipOn', sld('radialStart', 'Start Angle', 0, 360, 1) + sld('radialClip1', 'Clip', 0, 360, 1) + sld('radialClip2', 'Clip 2', 0, 360, 1)) +\n      sec('Chromatic Aberration', 'chromaOn', sld('chromAmount', 'Amount', 0, 1, 0.01) + sld('chromAlpha', 'Alpha', 0, 1, 0.01)) +\n      sec('Motion Blur', 'motionBlurOn', sld('motionAngle', 'Angle', -1, 1, 0.01) + sld('motionDist', 'Distance', -3, 3, 0.01)) +\n      sec('Ghost', 'ghostOn', sld('ghostBoost', 'Color Boost', 0, 5, 0.01) + sld('ghostTransp', 'Transparency', 0, 1, 0.01) + sld('ghostBlend', 'Blend', 0, 1, 0.01)) +\n      sec('Overlay (texture)', 'overlayOn', clr('overlayColor', 'Overlay Color') + sld('overlayGlow', 'Glow', 0, 25, 0.1) + sld('overlayBlend', 'Blend', 0, 1, 0.01) + sld('overlayScrollX', 'Scroll X', -5, 5, 0.05) + sld('overlayScrollY', 'Scroll Y', -5, 5, 0.05) + swr('overlayMult', 'Multiply Mode')) +\n      sec('Dissolve / Burn', 'fadeOn', sld('fadeAmount', 'Fade Amount', -0.1, 1, 0.01) + sld('fadeBurnTransition', 'Burn Transition', 0.01, 0.5, 0.01) + sld('fadeBurnWidth', 'Burn Width', 0, 1, 0.01) + clr('fadeBurnColor', 'Burn Color') + sld('fadeBurnGlow', 'Burn Glow', 1, 250, 1)) +\n      sec('Hologram', 'holoOn', sld('holoStripes', 'Stripes Amount', 0, 1, 0.01) + sld('holoUnmod', 'Unchanged Amount', 0, 1, 0.01) + sld('holoSpeed', 'Stripes Speed', -20, 20, 0.1) + sld('holoMinAlpha', 'Min Alpha', 0, 1, 0.01) + sld('holoMaxAlpha', 'Max Alpha', 0, 100, 0.5) + clr('holoStripeColor', 'Stripe Color') + sld('holoBlend', 'Blend', 0, 1, 0.01)) +\n      sec('Flicker', 'flickerOn', sld('flickerPercent', 'Percent', 0, 1, 0.01) + sld('flickerFreq', 'Frequency', 0, 5, 0.01) + sld('flickerAlpha', 'Min Alpha', 0, 1, 0.01)) +\n      sec('Alpha Cutoff', 'alphaCutoffOn', sld('alphaCutoff', 'Cutoff Value', 0, 1, 0.01)) +\n      sec('Alpha Round', 'alphaRoundOn', sld('alphaRoundThreshold', 'Round Threshold', 0.005, 1, 0.005));\n\n    return `<div class=\"panel\">\n      <p class=\"ttl\">Sprite Shader — Controls</p>\n\n      ${sec('✨ Glow', 'glowOn', clr('glowColor', 'Glow Color') + sld('glow', 'Glow Color Intensity', 0, 100, 0.5) + sld('glowGlobal', 'Global Glow Intensity', 1, 100, 0.5))}\n      ${sec('📺 Glitch', 'glitchOn', sld('glitchAmount', 'Glitch Amount', 0, 20, 0.1) + sld('glitchSize', 'Glitch Size', 0.25, 5, 0.05) + sld('glitchSpeed', 'Glitch Speed', 0, 100, 0.5))}\n      ${sec('🌟 Shine', 'shineOn', clr('shineColor', 'Shine Color') + sld('shineLocation', 'Shine Location', 0, 1, 0.01) + sld('shineRotate', 'Rotate Angle (rad)', 0, 6.2831, 0.01) + sld('shineWidth', 'Shine Width', 0.05, 1, 0.01) + sld('shineGlow', 'Shine Glow', 0, 100, 0.5) + swr('shineAnimate', 'Auto Animate') + sld('shineSpeed', 'Animate Speed', 0, 3, 0.01))}\n\n      <div class=\"grp\">🌊 Distortion &amp; UV</div>\n      ${fxSections}\n\n      <div class=\"grp\">💫 Special / Render</div>\n      ${special}\n\n      <div class=\"grp\">🖼️ Sprite</div>\n      <div class=\"sec\"><div class=\"sb\">\n        ${clr('bg', 'Background')}\n        <div class=\"r\"><div class=\"rl\"><span>Image URL</span></div>\n          <input class=\"urlbox\" type=\"text\" placeholder=\"https://....png\" value=\"${gv(d2, 'imageUrl')}\"></div>\n        <div class=\"r\"><div class=\"rl\"><span>Upload PNG</span></div>\n          <input class=\"file\" type=\"file\" accept=\"image/*\"></div>\n        <button class=\"reset\">Reset to defaults</button>\n      </div></div>\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    Object.keys(DEFAULTS).forEach(k => { d2.shared.set('ss_' + k, gv(d2, k), { global: true }); });\n\n    d2.on('.ctl', 'input', (e) => {\n      const t = e.target, n = t.dataset.name; if (!n) return;\n      push(d2, n, (t.type === 'color') ? t.value : parseFloat(t.value));\n    });\n    d2.on('.tog', 'change', (e) => { const t = e.target; push(d2, t.dataset.name, t.checked); });\n    d2.on('.urlbox', 'change', (e) => { push(d2, 'imageUrl', e.target.value); });\n    d2.on('.file', 'change', (e) => {\n      const f = e.target.files && e.target.files[0]; if (!f) return;\n      const rd = new FileReader(); rd.onload = () => { push(d2, 'imageUrl', rd.result); }; rd.readAsDataURL(f);\n    });\n    d2.on('.reset', 'click', () => { Object.keys(DEFAULTS).forEach(k => { push(d2, k, DEFAULTS[k]); }); });\n  }\n};\n"}}}}}}}},{"_id":"6a3eefb95de232c667ae2498","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"vrm-viewer","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// VRM Viewer — renders a .vrm avatar with three.js + @pixiv/three-vrm.\n// Animations: .vrma (native) and .fbx (Mixamo, retargeted). Driven by a sibling\n// controller via shared UIState ('vrm_<key>', global), polled each frame.\nconst EXPR = ['happy', 'angry', 'sad', 'relaxed', 'surprised', 'aa', 'ih', 'ou', 'ee', 'oh', 'blink', 'blinkLeft', 'blinkRight', 'lookUp', 'lookDown', 'lookLeft', 'lookRight', 'neutral'];\nconst HUMAN_BONES = ['hips', 'spine', 'chest', 'upperChest', 'neck', 'head',\n  'leftShoulder', 'leftUpperArm', 'leftLowerArm', 'leftHand',\n  'rightShoulder', 'rightUpperArm', 'rightLowerArm', 'rightHand',\n  'leftUpperLeg', 'leftLowerLeg', 'leftFoot', 'rightUpperLeg', 'rightLowerLeg', 'rightFoot'];\nconst MIXAMO_MAP = {\n  mixamorigHips: 'hips', mixamorigSpine: 'spine', mixamorigSpine1: 'chest', mixamorigSpine2: 'upperChest',\n  mixamorigNeck: 'neck', mixamorigHead: 'head',\n  mixamorigLeftShoulder: 'leftShoulder', mixamorigLeftArm: 'leftUpperArm', mixamorigLeftForeArm: 'leftLowerArm', mixamorigLeftHand: 'leftHand',\n  mixamorigLeftHandThumb1: 'leftThumbMetacarpal', mixamorigLeftHandThumb2: 'leftThumbProximal', mixamorigLeftHandThumb3: 'leftThumbDistal',\n  mixamorigLeftHandIndex1: 'leftIndexProximal', mixamorigLeftHandIndex2: 'leftIndexIntermediate', mixamorigLeftHandIndex3: 'leftIndexDistal',\n  mixamorigLeftHandMiddle1: 'leftMiddleProximal', mixamorigLeftHandMiddle2: 'leftMiddleIntermediate', mixamorigLeftHandMiddle3: 'leftMiddleDistal',\n  mixamorigLeftHandRing1: 'leftRingProximal', mixamorigLeftHandRing2: 'leftRingIntermediate', mixamorigLeftHandRing3: 'leftRingDistal',\n  mixamorigLeftHandPinky1: 'leftLittleProximal', mixamorigLeftHandPinky2: 'leftLittleIntermediate', mixamorigLeftHandPinky3: 'leftLittleDistal',\n  mixamorigRightShoulder: 'rightShoulder', mixamorigRightArm: 'rightUpperArm', mixamorigRightForeArm: 'rightLowerArm', mixamorigRightHand: 'rightHand',\n  mixamorigRightHandThumb1: 'rightThumbMetacarpal', mixamorigRightHandThumb2: 'rightThumbProximal', mixamorigRightHandThumb3: 'rightThumbDistal',\n  mixamorigRightHandIndex1: 'rightIndexProximal', mixamorigRightHandIndex2: 'rightIndexIntermediate', mixamorigRightHandIndex3: 'rightIndexDistal',\n  mixamorigRightHandMiddle1: 'rightMiddleProximal', mixamorigRightHandMiddle2: 'rightMiddleIntermediate', mixamorigRightHandMiddle3: 'rightMiddleDistal',\n  mixamorigRightHandRing1: 'rightRingProximal', mixamorigRightHandRing2: 'rightRingIntermediate', mixamorigRightHandRing3: 'rightRingDistal',\n  mixamorigRightHandPinky1: 'rightLittleProximal', mixamorigRightHandPinky2: 'rightLittleIntermediate', mixamorigRightHandPinky3: 'rightLittleDistal',\n  mixamorigLeftUpLeg: 'leftUpperLeg', mixamorigLeftLeg: 'leftLowerLeg', mixamorigLeftFoot: 'leftFoot', mixamorigLeftToeBase: 'leftToes',\n  mixamorigRightUpLeg: 'rightUpperLeg', mixamorigRightLeg: 'rightLowerLeg', mixamorigRightFoot: 'rightFoot', mixamorigRightToeBase: 'rightToes',\n};\nconst DEFAULTS = { modelUrl: '', animUrl: '', animType: '', hdrUrl: '', bg: '#202530', lightIntensity: 1, groundOn: true, autoRotate: false, playing: true, pose: '', poseNonce: 0 };\nEXPR.forEach(e => { DEFAULTS['expr_' + e] = 0; });\n\nfunction gv(d2, n) {\n  const sh = d2.shared.get('vrm_' + n, undefined, { global: true });\n  if (sh !== undefined && sh !== null && sh !== '') return sh;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\nfunction hexToInt(h) { h = (h || '#000').toString().replace('#', ''); if (h.length === 3) h = h.split('').map(c => c + c).join(''); return parseInt(h, 16) || 0; }\n\nexport default {\n  css: `\n  .wrap{ position:relative; width:100%; height:100%; min-height:300px; overflow:hidden; }\n  .stage{ position:absolute; inset:0; width:100%; height:100%; }\n  .stage canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }\n  .status{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); text-align:center;\n    color:#aeb4c6; font:14px Inter,system-ui,sans-serif; padding:20px; pointer-events:none; }\n  `,\n\n  render(d2) {\n    return `<div class=\"wrap\"><div class=\"stage\" data-d2-static></div><div class=\"status\">Carregando three.js…</div></div>`;\n  },\n\n  mounted(d2, root) {\n    const stage = d2.$('.stage');\n    const statusEl = d2.$('.status');\n    const cv = document.createElement('canvas');\n    cv.style.width = '100%'; cv.style.height = '100%';\n    stage.appendChild(cv);\n    const status = (m) => { if (statusEl) { statusEl.textContent = m || ''; statusEl.style.display = m ? 'block' : 'none'; } };\n\n    let THREE, GL, renderer, scene, camera, controls, clock, pmrem;\n    let keyLight, ambLight, grid, mixer, action, currentVrm;\n    const S = { raf: 0, modelUrl: null, animUrl: null, hdrUrl: null, poseNonce: 0, restBones: null };\n\n    (async function boot() {\n      try {\n        THREE = await import('https://esm.sh/three@0.169.0');\n        const oc = await import('https://esm.sh/three@0.169.0/examples/jsm/controls/OrbitControls.js');\n        const gl = await import('https://esm.sh/three@0.169.0/examples/jsm/loaders/GLTFLoader.js');\n        const fbx = await import('https://esm.sh/three@0.169.0/examples/jsm/loaders/FBXLoader.js');\n        const rg = await import('https://esm.sh/three@0.169.0/examples/jsm/loaders/RGBELoader.js');\n        const vrm = await import('https://esm.sh/@pixiv/three-vrm@3?deps=three@0.169.0');\n        const vrma = await import('https://esm.sh/@pixiv/three-vrm-animation@3?deps=three@0.169.0');\n        GL = {\n          OrbitControls: oc.OrbitControls, GLTFLoader: gl.GLTFLoader, FBXLoader: fbx.FBXLoader, RGBELoader: rg.RGBELoader,\n          VRMLoaderPlugin: vrm.VRMLoaderPlugin, VRMUtils: vrm.VRMUtils,\n          VRMAnimationLoaderPlugin: vrma.VRMAnimationLoaderPlugin, createVRMAnimationClip: vrma.createVRMAnimationClip,\n        };\n        init();\n      } catch (e) { status('Falha ao carregar bibliotecas 3D: ' + e.message); }\n    })();\n\n    function init() {\n      renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n      renderer.outputColorSpace = THREE.SRGBColorSpace;\n      scene = new THREE.Scene();\n      camera = new THREE.PerspectiveCamera(30, 1, 0.1, 100);\n      camera.position.set(0, 1.3, 2.6);\n      controls = new GL.OrbitControls(camera, renderer.domElement);\n      controls.target.set(0, 1.0, 0);\n      controls.enableDamping = true; controls.autoRotateSpeed = 2.0;\n      controls.update();\n\n      keyLight = new THREE.DirectionalLight(0xffffff, 1.0); keyLight.position.set(1.5, 2.5, 2.0); scene.add(keyLight);\n      ambLight = new THREE.AmbientLight(0xffffff, 0.55); scene.add(ambLight);\n      grid = new THREE.GridHelper(10, 20, 0x444a5a, 0x2a2f3a); scene.add(grid);\n\n      pmrem = new THREE.PMREMGenerator(renderer); pmrem.compileEquirectangularShader();\n      clock = new THREE.Clock();\n\n      resize();\n      ro.observe(stage);\n      status('Faça upload de um arquivo .vrm no painel →');\n      loadModel(gv(d2, 'modelUrl'));\n      animate();\n    }\n\n    function loadModel(url) {\n      if (!url) { status('Faça upload de um arquivo .vrm no painel →'); return; }\n      status('Carregando VRM…');\n      const loader = new GL.GLTFLoader();\n      loader.crossOrigin = 'anonymous';\n      loader.register((p) => new GL.VRMLoaderPlugin(p));\n      loader.load(url, (gltf) => {\n        const vrm = gltf.userData.vrm;\n        if (!vrm) { status('Arquivo não é um VRM válido.'); return; }\n        if (currentVrm) { scene.remove(currentVrm.scene); try { GL.VRMUtils.deepDispose(currentVrm.scene); } catch (e) {} }\n        try { GL.VRMUtils.removeUnnecessaryVertices(gltf.scene); } catch (e) {}\n        try { GL.VRMUtils.removeUnnecessaryJoints(gltf.scene); } catch (e) {}\n        vrm.scene.traverse((o) => { o.frustumCulled = false; });\n        try { if (vrm.meta && vrm.meta.metaVersion === '0') GL.VRMUtils.rotateVRM0(vrm); } catch (e) {}\n        scene.add(vrm.scene);\n        currentVrm = vrm; mixer = null; action = null;\n        captureRest();\n        status('');\n        // re-apply animation if one was queued\n        if (S.animUrl) loadAnim(S.animUrl, gv(d2, 'animType'));\n      }, undefined, (err) => status('Erro ao carregar VRM: ' + (err && err.message ? err.message : err)));\n    }\n\n    function loadMixamoAnimation(url) {\n      return new GL.FBXLoader().loadAsync(url).then((asset) => {\n        const clip = THREE.AnimationClip.findByName(asset.animations, 'mixamo.com') || asset.animations[0];\n        if (!clip) return null;\n        const tracks = [];\n        const restRotationInverse = new THREE.Quaternion();\n        const parentRestWorldRotation = new THREE.Quaternion();\n        const _quatA = new THREE.Quaternion();\n        const _vec3 = new THREE.Vector3();\n        const hipsNode = asset.getObjectByName('mixamorigHips');\n        const motionHipsHeight = hipsNode ? hipsNode.position.y : 1;\n        const vrmHipsY = currentVrm.humanoid.getNormalizedBoneNode('hips').getWorldPosition(_vec3).y;\n        const vrmRootY = currentVrm.scene.getWorldPosition(_vec3).y;\n        const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);\n        const hipsPositionScale = vrmHipsHeight / motionHipsHeight;\n        clip.tracks.forEach((track) => {\n          const parts = track.name.split('.');\n          const mixamoRigName = parts[0];\n          const vrmBoneName = MIXAMO_MAP[mixamoRigName];\n          const vrmNode = vrmBoneName ? currentVrm.humanoid.getNormalizedBoneNode(vrmBoneName) : null;\n          const mixamoRigNode = asset.getObjectByName(mixamoRigName);\n          if (vrmNode != null && mixamoRigNode != null) {\n            const propertyName = parts[1];\n            mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();\n            mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);\n            if (track.ValueTypeName === 'quaternion') {\n              for (let i = 0; i < track.values.length; i += 4) {\n                const flat = Array.prototype.slice.call(track.values, i, i + 4);\n                _quatA.fromArray(flat);\n                _quatA.premultiply(parentRestWorldRotation).multiply(restRotationInverse);\n                _quatA.toArray(flat);\n                for (let j = 0; j < 4; j++) track.values[i + j] = flat[j];\n              }\n              const values = (currentVrm.meta && currentVrm.meta.metaVersion === '0')\n                ? track.values.map((v, i) => (i % 2 === 0 ? -v : v)) : track.values;\n              tracks.push(new THREE.QuaternionKeyframeTrack(vrmNode.name + '.' + propertyName, track.times, values));\n            } else if (track.ValueTypeName === 'vector') {\n              const values = track.values.map((v, i) => ((currentVrm.meta && currentVrm.meta.metaVersion === '0' && i % 3 !== 1) ? -v : v) * hipsPositionScale);\n              tracks.push(new THREE.VectorKeyframeTrack(vrmNode.name + '.' + propertyName, track.times, values));\n            }\n          }\n        });\n        return new THREE.AnimationClip('vrmAnimation', clip.duration, tracks);\n      });\n    }\n\n    function loadAnim(url, type) {\n      if (!url || !currentVrm) return;\n      status('Carregando animação…');\n      if ((type || '').toLowerCase() === 'fbx') {\n        loadMixamoAnimation(url).then((clip) => {\n          if (!clip) { status('FBX sem animação utilizável.'); return; }\n          mixer = new THREE.AnimationMixer(currentVrm.scene);\n          action = mixer.clipAction(clip); action.play(); status('');\n        }).catch((err) => status('Erro no FBX: ' + (err && err.message ? err.message : err)));\n      } else {\n        const loader = new GL.GLTFLoader();\n        loader.register((p) => new GL.VRMAnimationLoaderPlugin(p));\n        loader.load(url, (gltf) => {\n          const arr = gltf.userData.vrmAnimations;\n          if (arr && arr.length) {\n            const clip = GL.createVRMAnimationClip(arr[0], currentVrm);\n            mixer = new THREE.AnimationMixer(currentVrm.scene);\n            action = mixer.clipAction(clip); action.play(); status('');\n          } else status('Arquivo sem VRM Animation (.vrma).');\n        }, undefined, (err) => status('Erro na animação: ' + (err && err.message ? err.message : err)));\n      }\n    }\n\n    function loadHDR(url) {\n      if (!url) { scene.environment = null; return; }\n      new GL.RGBELoader().load(url, (tex) => {\n        tex.mapping = THREE.EquirectangularReflectionMapping;\n        const env = pmrem.fromEquirectangular(tex).texture;\n        scene.environment = env; tex.dispose();\n      }, undefined, (err) => status('Erro no HDR: ' + (err && err.message ? err.message : err)));\n    }\n\n    function captureRest() {\n      S.restBones = {};\n      const h = currentVrm.humanoid; if (!h) return;\n      for (const bn of HUMAN_BONES) { const node = h.getNormalizedBoneNode(bn); if (node) S.restBones[bn] = node.quaternion.clone(); }\n    }\n    function resetPose() {\n      const h = currentVrm && currentVrm.humanoid; if (!h || !S.restBones) return;\n      for (const bn in S.restBones) { const node = h.getNormalizedBoneNode(bn); if (node) node.quaternion.copy(S.restBones[bn]); }\n    }\n    function applyPose(name) {\n      if (!currentVrm || !currentVrm.humanoid) return;\n      mixer = null; action = null;\n      resetPose();\n      const h = currentVrm.humanoid;\n      const d2r = THREE.MathUtils.degToRad;\n      const set = (bone, x, y, z) => { const n = h.getNormalizedBoneNode(bone); if (n) n.rotation.set(d2r(x), d2r(y), d2r(z)); };\n      if (name === 'A') { set('leftUpperArm', 0, 0, 50); set('rightUpperArm', 0, 0, -50); }\n      else if (name === 'relax') { set('leftUpperArm', 0, 0, 65); set('rightUpperArm', 0, 0, -65); set('leftLowerArm', 0, -15, 0); set('rightLowerArm', 0, 15, 0); }\n      try { h.update(); } catch (e) {}\n    }\n\n    function applyExpressions() {\n      const em = currentVrm.expressionManager; if (!em) return;\n      for (const name of EXPR) { const v = +gv(d2, 'expr_' + name) || 0; try { em.setValue(name, v); } catch (e) {} }\n    }\n\n    function sync() {\n      const m = gv(d2, 'modelUrl'); if (m !== S.modelUrl) { S.modelUrl = m; loadModel(m); }\n      const a = gv(d2, 'animUrl'); if (a !== S.animUrl) { S.animUrl = a; loadAnim(a, gv(d2, 'animType')); }\n      const hd = gv(d2, 'hdrUrl'); if (hd !== S.hdrUrl) { S.hdrUrl = hd; loadHDR(hd); }\n      const pn = +gv(d2, 'poseNonce') || 0; if (pn !== S.poseNonce) { S.poseNonce = pn; applyPose(gv(d2, 'pose')); }\n      if (action) action.paused = !gv(d2, 'playing');\n      if (keyLight) keyLight.intensity = +gv(d2, 'lightIntensity');\n      if (ambLight) ambLight.intensity = 0.55 * (+gv(d2, 'lightIntensity'));\n      if (grid) grid.visible = !!gv(d2, 'groundOn');\n      if (controls) controls.autoRotate = !!gv(d2, 'autoRotate');\n      renderer.setClearColor(hexToInt(gv(d2, 'bg')), 1.0);\n    }\n\n    function resize() {\n      const w = stage.clientWidth || 600, h = stage.clientHeight || 400;\n      renderer.setSize(w, h, false);\n      camera.aspect = w / h; camera.updateProjectionMatrix();\n    }\n    const ro = new ResizeObserver(resize);\n\n    function animate() {\n      S.raf = requestAnimationFrame(animate);\n      const dt = clock.getDelta();\n      try { sync(); } catch (e) {}\n      if (mixer) mixer.update(dt);\n      if (currentVrm) { try { applyExpressions(); currentVrm.update(dt); } catch (e) {} }\n      if (controls) controls.update();\n      renderer.render(scene, camera);\n    }\n\n    d2.onUnmount(() => {\n      cancelAnimationFrame(S.raf);\n      try { ro.disconnect(); } catch (e) {}\n      try { if (currentVrm) GL.VRMUtils.deepDispose(currentVrm.scene); } catch (e) {}\n      try { if (renderer) renderer.dispose(); } catch (e) {}\n    });\n  }\n};\n"}},"modelUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"modelUrl"}}}},"animUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"animUrl"}}}},"animType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"animType"}}}},"hdrUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"hdrUrl"}}}},"bg":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bg"}}}},"lightIntensity":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"lightIntensity"}}}},"groundOn":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"groundOn"}}}},"autoRotate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoRotate"}}}},"playing":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"playing"}}}},"pose":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pose"}}}},"poseNonce":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"poseNonce"}}}}}}}}}},{"_id":"6a3eefe9097719b83c1db79b","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"vrm-controller","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// VRM Controller — drives a sibling `vrm-viewer` block via shared UIState\n// (keys 'vrm_<param>', global). Uploads become blob URLs (same window → cross-readable).\nconst EXPR = ['happy', 'angry', 'sad', 'relaxed', 'surprised', 'aa', 'ih', 'ou', 'ee', 'oh', 'blink', 'blinkLeft', 'blinkRight', 'lookUp', 'lookDown', 'lookLeft', 'lookRight', 'neutral'];\nconst DEFAULTS = { modelUrl: '', animUrl: '', animType: '', hdrUrl: '', bg: '#202530', lightIntensity: 1, groundOn: true, autoRotate: false, playing: true, pose: '', poseNonce: 0 };\nEXPR.forEach(e => { DEFAULTS['expr_' + e] = 0; });\n\nfunction gv(d2, n) {\n  const sv = d2.state[n];\n  if (sv !== undefined) return sv;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\nfunction push(d2, n, v) { d2.state[n] = v; d2.shared.set('vrm_' + n, v, { global: true }); }\n\nexport default {\n  css: `\n  .panel{ width:100%; height:100%; box-sizing:border-box; overflow-y:auto; background:#13151d;\n    padding:16px; font-family:Inter,system-ui,sans-serif; color:#e7e9f0; }\n  .ttl{ font-size:13px; font-weight:700; letter-spacing:.04em; color:#aeb4c6; margin:0 0 12px; text-transform:uppercase; }\n  .grp{ font-size:11px; font-weight:700; letter-spacing:.08em; color:#7b8194; text-transform:uppercase; margin:18px 0 8px; padding-top:8px; border-top:1px solid #262a37; }\n  .sec{ background:#191c26; border:1px solid #262a37; border-radius:10px; margin-bottom:10px; overflow:hidden; }\n  .sh{ display:flex; align-items:center; justify-content:space-between; padding:9px 12px; font-weight:600; font-size:12.5px; background:#1e2230; }\n  .sb{ padding:10px 12px; display:flex; flex-direction:column; gap:11px; }\n  .r{ display:flex; flex-direction:column; gap:5px; }\n  .rl{ display:flex; justify-content:space-between; font-size:11.5px; color:#9aa0b2; }\n  .rl b{ color:#dfe3ee; font-weight:600; font-variant-numeric:tabular-nums; }\n  .rc{ flex-direction:row; align-items:center; justify-content:space-between; }\n  .rc span{ font-size:11.5px; color:#9aa0b2; }\n  input[type=range]{ width:100%; accent-color:#46c8ff; height:4px; }\n  input[type=color]{ width:46px; height:26px; border:1px solid #2c3142; border-radius:6px; background:#0e0f15; padding:0; cursor:pointer; }\n  input[type=file]{ font-size:11px; color:#9aa0b2; width:100%; }\n  .sw{ position:relative; display:inline-block; width:38px; height:21px; cursor:pointer; }\n  .sw input{ opacity:0; width:0; height:0; }\n  .sw span{ position:absolute; inset:0; background:#363b4d; border-radius:21px; transition:.2s; }\n  .sw span:before{ content:''; position:absolute; height:15px; width:15px; left:3px; top:3px; background:#fff; border-radius:50%; transition:.2s; }\n  .sw input:checked + span{ background:#46c8ff; }\n  .sw input:checked + span:before{ transform:translateX(17px); }\n  .lbl{ font-size:11.5px; color:#9aa0b2; }\n  .btns{ display:flex; gap:8px; flex-wrap:wrap; }\n  .pbtn{ flex:1; min-width:70px; background:#242838; border:1px solid #343a4d; color:#cfd4e2; border-radius:8px; padding:9px 6px; font-size:12px; font-weight:600; cursor:pointer; }\n  .pbtn:hover{ background:#2c3142; }\n  .hint{ font-size:10.5px; color:#6b7180; line-height:1.4; }\n  `,\n\n  render(d2) {\n    const sld = (n, l, mn, mx, st) =>\n      `<div class=\"r\"><div class=\"rl\"><span>${l}</span><b>${(+gv(d2, n)).toFixed(2)}</b></div>` +\n      `<input class=\"ctl\" type=\"range\" data-name=\"${n}\" min=\"${mn}\" max=\"${mx}\" step=\"${st}\" value=\"${gv(d2, n)}\"></div>`;\n    const clr = (n, l) =>\n      `<div class=\"r rc\"><span>${l}</span><input class=\"ctl\" type=\"color\" data-name=\"${n}\" value=\"${gv(d2, n)}\"></div>`;\n    const tog = (n) =>\n      `<label class=\"sw\"><input class=\"tog\" type=\"checkbox\" data-name=\"${n}\" ${gv(d2, n) ? 'checked' : ''}><span></span></label>`;\n    const swr = (n, l) => `<div class=\"r rc\"><span>${l}</span>${tog(n)}</div>`;\n    const sec = (title, body) => `<div class=\"sec\"><div class=\"sh\"><span>${title}</span></div><div class=\"sb\">${body}</div></div>`;\n\n    const exprSliders = EXPR.map(e => sld('expr_' + e, e, 0, 1, 0.01)).join('');\n\n    return `<div class=\"panel\">\n      <p class=\"ttl\">VRM Viewer — Controls</p>\n\n      ${sec('🧍 Model', `<div class=\"lbl\">Upload .vrm</div><input class=\"vrmfile\" type=\"file\" accept=\".vrm,.glb\">`)}\n\n      ${sec('🎞️ Animation', `<div class=\"lbl\">Upload .vrma ou .fbx (Mixamo)</div>\n        <input class=\"animfile\" type=\"file\" accept=\".vrma,.fbx,.glb,.gltf\">\n        ${swr('playing', 'Playing')}\n        <div class=\"hint\">FBX do Mixamo: exporte \"Without Skin\", FBX Binary. Apenas esqueletos humanoides Mixamo.</div>`)}\n\n      ${sec('🤸 Poses', `<div class=\"btns\">\n        <button class=\"pbtn\" data-pose=\"T\">T-Pose</button>\n        <button class=\"pbtn\" data-pose=\"A\">A-Pose</button>\n        <button class=\"pbtn\" data-pose=\"relax\">Relax</button>\n      </div><div class=\"hint\">Aplicar uma pose interrompe a animação atual.</div>`)}\n\n      ${sec('🌅 Environment', `<div class=\"lbl\">Upload HDR (.hdr)</div>\n        <input class=\"hdrfile\" type=\"file\" accept=\".hdr\">\n        ${clr('bg', 'Background')}\n        ${sld('lightIntensity', 'Light Intensity', 0, 4, 0.05)}\n        ${swr('groundOn', 'Show Ground')}\n        ${swr('autoRotate', 'Auto Rotate')}`)}\n\n      <div class=\"grp\">😀 Expressions / Shapekeys</div>\n      ${sec('Expressions', exprSliders)}\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    Object.keys(DEFAULTS).forEach(k => { d2.shared.set('vrm_' + k, gv(d2, k), { global: true }); });\n\n    d2.on('.ctl', 'input', (e) => {\n      const t = e.target, n = t.dataset.name; if (!n) return;\n      push(d2, n, (t.type === 'color') ? t.value : parseFloat(t.value));\n    });\n    d2.on('.tog', 'change', (e) => { const t = e.target; push(d2, t.dataset.name, t.checked); });\n\n    d2.on('.vrmfile', 'change', (e) => {\n      const f = e.target.files && e.target.files[0]; if (!f) return;\n      push(d2, 'modelUrl', URL.createObjectURL(f));\n    });\n    d2.on('.animfile', 'change', (e) => {\n      const f = e.target.files && e.target.files[0]; if (!f) return;\n      const ext = (f.name.split('.').pop() || '').toLowerCase();\n      push(d2, 'animType', ext);\n      push(d2, 'animUrl', URL.createObjectURL(f));\n    });\n    d2.on('.hdrfile', 'change', (e) => {\n      const f = e.target.files && e.target.files[0]; if (!f) return;\n      push(d2, 'hdrUrl', URL.createObjectURL(f));\n    });\n\n    d2.on('.pbtn', 'click', (e) => {\n      const p = e.target.getAttribute('data-pose'); if (!p) return;\n      const n = (d2.state.__pn || 0) + 1; d2.state.__pn = n;\n      push(d2, 'pose', p); push(d2, 'poseNonce', n);\n    });\n  }\n};\n"}}}}}}}},{"_id":"6a442148e2cb43593f390223","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"vrm-emote","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// VRM Emote Avatar — emotional avatar.\n// LEFT-drag = affection (happy, almost no deformation). RIGHT-drag = aggression (angry,\n// directional squash & stretch following the pull). Camera locked on head+torso; eyes\n// lightly follow the cursor; mouth opens with varied visemes while \"talking\".\nconst DEFAULTS = {\n  modelUrl: 'https://bigaton-playground.data2.media/file/YZ-0t5JbmqpgQepgPfk2X',\n  bg: '#202530', camFov: 30, camDist: 1.4, camYOff: 0.05,\n};\nfunction gv(d2, n) {\n  const sh = d2.shared.get('vrm_' + n, undefined, { global: true });\n  if (sh !== undefined && sh !== null && sh !== '') return sh;\n  const pv = d2.param(n);\n  return (pv !== undefined && pv !== null && pv !== '') ? pv : DEFAULTS[n];\n}\nfunction hexToInt(h) { h = (h || '#000').toString().replace('#', ''); if (h.length === 3) h = h.split('').map(c => c + c).join(''); return parseInt(h, 16) || 0; }\nfunction clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }\n\nexport default {\n  css: `\n  .wrap{ position:relative; width:100%; height:100%; min-height:300px; overflow:hidden; }\n  .stage{ position:absolute; inset:0; width:100%; height:100%; }\n  .stage canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; cursor:grab; }\n  .stage canvas:active{ cursor:grabbing; }\n  .status{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); text-align:center;\n    color:#aeb4c6; font:14px Inter,system-ui,sans-serif; padding:20px; pointer-events:none; }\n  `,\n\n  render(d2) {\n    return `<div class=\"wrap\"><div class=\"stage\" data-d2-static></div><div class=\"status\">Carregando…</div></div>`;\n  },\n\n  mounted(d2, root) {\n    const stage = d2.$('.stage');\n    const statusEl = d2.$('.status');\n    const cv = document.createElement('canvas');\n    cv.style.width = '100%'; cv.style.height = '100%';\n    stage.appendChild(cv);\n    function makeCursor(glyph, hx, hy) {\n      const s = 40, c = document.createElement('canvas'); c.width = c.height = s;\n      const x = c.getContext('2d'); x.font = '30px serif'; x.textAlign = 'center'; x.textBaseline = 'middle';\n      x.fillText(glyph, s / 2, s / 2 + 2);\n      return 'url(\"' + c.toDataURL('image/png') + '\") ' + hx + ' ' + hy + ', auto';\n    }\n    const C = { hand: makeCursor('🖐️', 12, 8), pet: makeCursor('🫳', 18, 12), pinch: makeCursor('🤏', 20, 18) };\n    cv.style.cursor = C.hand;\n    const status = (m) => { if (statusEl) { statusEl.textContent = m || ''; statusEl.style.display = m ? 'block' : 'none'; } };\n\n    let THREE, GL, renderer, scene, camera, clock, lookTarget;\n    let keyLight, ambLight, currentVrm, avatarGroup;\n    const E = { happy: 0, angry: 0, sad: 0, relaxed: 0 };\n    const T = { happy: 0, angry: 0, sad: 0, relaxed: 0 };\n    const VIS = ['aa', 'ih', 'ou', 'ee', 'oh'];\n    const HOLD = 4500;\n    const IDLE = { happy: 0.25 }; // resting face = slight smile\n    const S = {\n      raf: 0, emoNonce: 0, hold: 0, dragging: false, mode: 'pet', inside: false,\n      ndc: { x: 0, y: 0 }, ndcEase: { x: 0, y: 0 }, lastPx: null, lastT: 0,\n      off: { x: 0, y: 0 }, offv: { x: 0, y: 0 }, offTarget: { x: 0, y: 0 }, grab: { x: 0, y: 0 },\n      blinkT: 3, blinkP: 0, pinchBlink: 0, visT: 0, curVis: false, mrel: 0, maa: 0, mih: 0, headY: 1.4,\n    };\n    function bump(emo, amt) { T[emo] = Math.min(1, T[emo] + amt); S.hold = performance.now() + HOLD; }\n\n    (async function boot() {\n      try {\n        THREE = await import('https://esm.sh/three@0.169.0');\n        const gl = await import('https://esm.sh/three@0.169.0/examples/jsm/loaders/GLTFLoader.js');\n        const vrm = await import('https://esm.sh/@pixiv/three-vrm@3?deps=three@0.169.0');\n        GL = { GLTFLoader: gl.GLTFLoader, VRMLoaderPlugin: vrm.VRMLoaderPlugin, VRMUtils: vrm.VRMUtils };\n        init();\n      } catch (e) { status('Falha ao carregar bibliotecas 3D: ' + e.message); }\n    })();\n\n    function init() {\n      renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n      renderer.outputColorSpace = THREE.SRGBColorSpace;\n      scene = new THREE.Scene();\n      camera = new THREE.PerspectiveCamera(+gv(d2, 'camFov'), 1, 0.1, 100);\n      camera.position.set(0, 1.4, +gv(d2, 'camDist'));\n      keyLight = new THREE.DirectionalLight(0xffffff, 1.1); keyLight.position.set(0.8, 2.0, 2.0); scene.add(keyLight);\n      ambLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambLight);\n      lookTarget = new THREE.Object3D(); scene.add(lookTarget);\n      clock = new THREE.Clock();\n      resize(); ro.observe(stage);\n      bindPointer();\n      loadModel(gv(d2, 'modelUrl'));\n      animate();\n    }\n\n    function frameCamera() {\n      try {\n        const head = currentVrm.humanoid.getNormalizedBoneNode('head');\n        const v = new THREE.Vector3(); head.getWorldPosition(v); S.headY = v.y;\n      } catch (e) { S.headY = 1.4; }\n      const ty = S.headY + (+gv(d2, 'camYOff'));\n      camera.fov = +gv(d2, 'camFov');\n      camera.position.set(0, ty, +gv(d2, 'camDist'));\n      camera.lookAt(0, ty, 0);\n      camera.updateProjectionMatrix();\n    }\n\n    function applyIdlePose(vrm) {\n      const h = vrm.humanoid; if (!h) return;\n      const d2r = THREE.MathUtils.degToRad;\n      const set = (b, x, y, z) => { const n = h.getNormalizedBoneNode(b); if (n) n.rotation.set(d2r(x), d2r(y), d2r(z)); };\n      set('leftShoulder', 0, 0, 4); set('rightShoulder', 0, 0, -4);\n      set('leftUpperArm', 5, 0, 67); set('rightUpperArm', 5, 0, -67);\n      set('leftLowerArm', 0, -16, 0); set('rightLowerArm', 0, 16, 0);\n      try { h.update(); } catch (e) {}\n    }\n\n    function loadModel(url) {\n      if (!url) { status('Sem modelo.'); return; }\n      status('Carregando avatar…');\n      const loader = new GL.GLTFLoader(); loader.crossOrigin = 'anonymous';\n      loader.register((p) => new GL.VRMLoaderPlugin(p));\n      loader.load(url, (gltf) => {\n        const vrm = gltf.userData.vrm;\n        if (!vrm) { status('VRM inválido.'); return; }\n        if (avatarGroup) { scene.remove(avatarGroup); try { GL.VRMUtils.deepDispose(avatarGroup); } catch (e) {} }\n        try { GL.VRMUtils.removeUnnecessaryVertices(gltf.scene); } catch (e) {}\n        try { GL.VRMUtils.removeUnnecessaryJoints(gltf.scene); } catch (e) {}\n        vrm.scene.traverse((o) => { o.frustumCulled = false; });\n        try { if (vrm.meta && vrm.meta.metaVersion === '0') GL.VRMUtils.rotateVRM0(vrm); } catch (e) {}\n        avatarGroup = new THREE.Group(); avatarGroup.add(vrm.scene); scene.add(avatarGroup);\n        currentVrm = vrm;\n        if (vrm.lookAt) { vrm.lookAt.target = lookTarget; vrm.lookAt.autoUpdate = true; }\n        applyIdlePose(vrm);\n        frameCamera();\n        status('');\n      }, undefined, (err) => status('Erro ao carregar avatar: ' + (err && err.message ? err.message : err)));\n    }\n\n    function bindPointer() {\n      d2.on(cv, 'contextmenu', (e) => e.preventDefault());\n      d2.on(cv, 'pointerdown', (e) => {\n        if (e.button === 0) S.mode = 'pet';\n        else if (e.button === 2) S.mode = 'rough';\n        else return;\n        S.dragging = true; S.grab = { x: e.clientX, y: e.clientY };\n        cv.style.cursor = (S.mode === 'rough') ? C.pinch : C.pet;\n        try { cv.setPointerCapture(e.pointerId); } catch (er) {}\n      });\n      d2.on(window, 'pointerup', () => { S.dragging = false; S.offTarget.x = 0; S.offTarget.y = 0; cv.style.cursor = C.hand; });\n      d2.on(cv, 'pointerenter', () => { S.inside = true; if (!S.dragging) cv.style.cursor = C.hand; });\n      d2.on(cv, 'pointerleave', () => { S.inside = false; });\n      d2.on(cv, 'pointermove', (e) => {\n        const r = cv.getBoundingClientRect();\n        S.ndc.x = ((e.clientX - r.left) / r.width) * 2 - 1;\n        S.ndc.y = -(((e.clientY - r.top) / r.height) * 2 - 1);\n        S.inside = true;\n        if (!S.dragging) return; // bend toward the drag only while a button is held\n        const fx = (e.clientX - S.grab.x) / (cv.clientWidth || 1);\n        const fy = -(e.clientY - S.grab.y) / (cv.clientHeight || 1);\n        const gain = (S.mode === 'rough') ? 1.7 : 0.7;   // bend strength\n        const cap = (S.mode === 'rough') ? 0.75 : 0.28;  // max total bend (radians)\n        const sgn = (S.mode === 'rough') ? -1 : 1;        // right-drag bends TOWARD the cursor; left keeps \"gives face\"\n        S.offTarget.x = clamp(sgn * fx * gain, -cap, cap);\n        S.offTarget.y = clamp(sgn * fy * gain, -cap, cap);\n        if (S.mode === 'rough') { bump('angry', 0.05); T.happy *= 0.9; }\n        else { bump('happy', 0.04); T.angry *= 0.9; }\n      });\n    }\n\n    function readAgent() {\n      const n = +gv(d2, 'emotionNonce') || 0;\n      if (n !== S.emoNonce) {\n        S.emoNonce = n;\n        const emo = gv(d2, 'emotion');\n        const inten = Math.max(0, Math.min(1, +gv(d2, 'emotionIntensity') || 0.8));\n        T.happy = T.angry = T.sad = T.relaxed = 0;\n        if (emo === 'happy' || emo === 'angry' || emo === 'sad' || emo === 'relaxed') T[emo] = inten;\n        else T.happy = IDLE.happy; // neutral rests at the idle smile\n        S.hold = performance.now() + HOLD;\n      }\n    }\n\n    function resize() {\n      const w = stage.clientWidth || 600, h = stage.clientHeight || 400;\n      renderer.setSize(w, h, false);\n      camera.aspect = w / h; camera.updateProjectionMatrix();\n    }\n    const ro = new ResizeObserver(resize);\n\n    function animate() {\n      S.raf = requestAnimationFrame(animate);\n      const dt = Math.min(0.05, clock.getDelta());\n      renderer.setClearColor(hexToInt(gv(d2, 'bg')), 1.0);\n      readAgent();\n\n      // hold emotion, then snap back fast & non-linear\n      if (performance.now() > S.hold) {\n        const decay = Math.exp(-dt / 0.55);\n        T.happy = IDLE.happy + (T.happy - IDLE.happy) * decay; // return to the idle smile\n        T.angry *= decay; T.sad *= decay; T.relaxed *= decay;\n      }\n      const k = Math.min(1, dt * 9);\n      E.happy += (T.happy - E.happy) * k; E.angry += (T.angry - E.angry) * k;\n      E.sad += (T.sad - E.sad) * k; E.relaxed += (T.relaxed - E.relaxed) * k;\n\n      // bend the spine toward the drag (hips/base stay planted), elastic spring-back\n      const PST = 90, PDA = 15;\n      S.offv.x += (-PST * (S.off.x - S.offTarget.x) - PDA * S.offv.x) * dt; S.off.x += S.offv.x * dt;\n      S.offv.y += (-PST * (S.off.y - S.offTarget.y) - PDA * S.offv.y) * dt; S.off.y += S.offv.y * dt;\n      if (currentVrm && currentVrm.humanoid) {\n        const hh = currentVrm.humanoid;\n        const SZ = -1, SX = -1; // bend sign (flip if it leans the wrong way)\n        const chain = [['spine', 0.24], ['chest', 0.2], ['upperChest', 0.2], ['neck', 0.16], ['head', 0.2]];\n        for (let c = 0; c < chain.length; c++) {\n          const n = hh.getNormalizedBoneNode(chain[c][0]);\n          if (n) { n.rotation.z = SZ * S.off.x * chain[c][1]; n.rotation.x = SX * S.off.y * chain[c][1]; }\n        }\n      }\n      // tiny stretch for life\n      S.bendMag = Math.hypot(S.off.x, S.off.y);\n      if (avatarGroup) avatarGroup.scale.set(1, 1 + S.bendMag * 0.04, 1);\n\n      // eyes lightly follow cursor\n      const tgt = S.inside ? S.ndc : { x: 0, y: 0 };\n      S.ndcEase.x += (tgt.x - S.ndcEase.x) * Math.min(1, dt * 6);\n      S.ndcEase.y += (tgt.y - S.ndcEase.y) * Math.min(1, dt * 6);\n      lookTarget.position.set(S.ndcEase.x * 0.4, S.headY + S.ndcEase.y * 0.28, (+gv(d2, 'camDist')));\n\n      // auto-blink (smooth, a touch slower) + full eye-close while pinching (right-drag)\n      const BLINK_DUR = 0.22;\n      S.blinkT -= dt;\n      if (S.blinkT <= 0 && S.blinkP <= 0) { S.blinkP = BLINK_DUR; S.blinkT = 3.5 + Math.random() * 3; }\n      let autoBlink = 0;\n      if (S.blinkP > 0) { S.blinkP -= dt; autoBlink = Math.sin((1 - S.blinkP / BLINK_DUR) * Math.PI); }\n      const pinching = S.dragging && S.mode === 'rough';\n      S.pinchBlink += ((pinching ? 1 : 0) - S.pinchBlink) * Math.min(1, dt * 12);\n      const blinkVal = Math.min(1, Math.max(autoBlink, S.pinchBlink));\n\n      // lip-sync — RELAXED holds the mouth open (teeth visible) flickering 0.7<->1;\n      // aa & ih swap 0<->1 fast to move the teeth / mouth shape.\n      const talking = !!gv(d2, 'talking');\n      let tRel = 0, tAa = 0, tIh = 0;\n      if (talking) {\n        S.visT -= dt;\n        if (S.visT <= 0) { S.curVis = !S.curVis; S.visT = 0.11 + Math.random() * 0.06; } // ~110-170ms\n        tRel = S.curVis ? 1.0 : 0.7;\n        tAa = S.curVis ? 1 : 0;\n        tIh = S.curVis ? 0 : 1;\n      }\n      const lr = Math.min(1, dt * 8); // interpolation speed (lower = slower/softer)\n      S.mrel += (tRel - S.mrel) * lr;\n      S.maa += (tAa - S.maa) * lr;\n      S.mih += (tIh - S.mih) * lr;\n\n      if (currentVrm) {\n        try {\n          const em = currentVrm.expressionManager;\n          if (em) {\n            em.setValue('happy', Math.min(1, E.happy));\n            em.setValue('angry', Math.min(1, E.angry));\n            em.setValue('sad', Math.min(1, E.sad));\n            const emoMouth = Math.min(1, E.happy + E.sad); // happy/sad already open/stretch the mouth\n            em.setValue('relaxed', Math.min(1, Math.max(E.relaxed, S.mrel * (1 - 0.7 * emoMouth))));\n            em.setValue('blink', blinkVal);\n            const aaPinch = S.pinchBlink * Math.min(1, (S.bendMag || 0) / 0.75) * 0.8; // grows with pull size, max 0.8\n            em.setValue('aa', Math.min(1, Math.max(S.maa * (1 - 0.4 * emoMouth), aaPinch))); em.setValue('ih', S.mih * (1 - 0.4 * emoMouth));\n            em.setValue('ou', 0); em.setValue('ee', 0); em.setValue('oh', 0);\n          }\n          currentVrm.update(dt);\n        } catch (e) {}\n      }\n      renderer.render(scene, camera);\n    }\n\n    d2.onUnmount(() => {\n      cancelAnimationFrame(S.raf);\n      try { ro.disconnect(); } catch (e) {}\n      try { if (avatarGroup) GL.VRMUtils.deepDispose(avatarGroup); } catch (e) {}\n      try { if (renderer) renderer.dispose(); } catch (e) {}\n    });\n  }\n};\n"}},"modelUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"modelUrl"}}}},"bg":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bg"}}}},"camFov":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"camFov"}}}},"camDist":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"camDist"}}}},"camYOff":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"camYOff"}}}}}}}}}},{"_id":"6a4421830f456ae6299ada8f","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"vrm-chat","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// VRM Chat — talks to the `vrm-agent` Assistant; the agent's emotion + \"talking\" state\n// drive the sibling vrm-emote avatar via shared UIState ('vrm_<key>', global).\nconst EMOS = ['happy', 'angry', 'sad', 'relaxed', 'neutral'];\n\nfunction setEmotion(d2, emo, intensity) {\n  d2.shared.set('vrm_emotion', emo, { global: true });\n  d2.shared.set('vrm_emotionIntensity', intensity, { global: true });\n  const n = (d2.state.__en || 0) + 1; d2.state.__en = n;\n  d2.shared.set('vrm_emotionNonce', n, { global: true });\n}\nfunction setTalking(d2, v) { d2.shared.set('vrm_talking', !!v, { global: true }); }\n\nexport default {\n  css: `\n  .chat{ display:flex; flex-direction:column; height:100%; box-sizing:border-box; background:#13151d;\n    font-family:Inter,system-ui,sans-serif; color:#e7e9f0; }\n  .head{ padding:12px 14px; border-bottom:1px solid #262a37; display:flex; align-items:center; justify-content:space-between; }\n  .head b{ font-size:13px; letter-spacing:.03em; }\n  .qabtn{ background:#242838; border:1px solid #343a4d; color:#9aa0b2; border-radius:7px; padding:5px 9px; font-size:11px; cursor:pointer; }\n  .qa{ display:none; gap:6px; flex-wrap:wrap; padding:8px 14px; border-bottom:1px solid #262a37; background:#171a23; }\n  .qa.show{ display:flex; }\n  .qa button{ background:#242838; border:1px solid #343a4d; color:#cfd4e2; border-radius:7px; padding:6px 10px; font-size:11px; cursor:pointer; text-transform:capitalize; }\n  .qa button:hover{ background:#2c3142; }\n  .log{ flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px; }\n  .msg{ max-width:85%; padding:9px 12px; border-radius:12px; font-size:13px; line-height:1.45; white-space:pre-wrap; word-break:break-word; }\n  .me{ align-self:flex-end; background:#2f6df0; color:#fff; border-bottom-right-radius:4px; }\n  .ai{ align-self:flex-start; background:#222634; color:#e7e9f0; border-bottom-left-radius:4px; }\n  .cur:after{ content:'▋'; opacity:.6; animation:bl 1s steps(2) infinite; }\n  @keyframes bl{ 50%{opacity:0;} }\n  .empty{ color:#6b7180; font-size:12px; text-align:center; margin:auto; }\n  .bar{ display:flex; gap:8px; padding:12px; border-top:1px solid #262a37; }\n  .box{ flex:1; background:#0e0f15; border:1px solid #2c3142; color:#e7e9f0; border-radius:9px; padding:10px 12px; font-size:13px; outline:none; }\n  .box:focus{ border-color:#2f6df0; }\n  .send{ background:#2f6df0; border:none; color:#fff; border-radius:9px; padding:0 16px; font-size:13px; font-weight:600; cursor:pointer; }\n  .send:disabled{ opacity:.5; cursor:default; }\n  `,\n\n  render(d2) {\n    const msgs = d2.state.msgs || [];\n    const partial = d2.state.partial;\n    const thinking = d2.state.thinking;\n    const rows = msgs.map((m, i) =>\n      `<div class=\"msg ${m.role === 'me' ? 'me' : 'ai'}\" data-key=\"m${i}\">${escapeHtml(m.text)}</div>`).join('');\n    const live = (partial != null)\n      ? `<div class=\"msg ai cur\" data-key=\"live\">${escapeHtml(partial)}</div>`\n      : (thinking ? `<div class=\"msg ai cur\" data-key=\"live\"></div>` : '');\n    const body = (rows || live) ? (rows + live) : `<div class=\"empty\">Diga algo pro personagem…</div>`;\n\n    return `<div class=\"chat\">\n      <div class=\"head\"><b>💬 Conversa</b><button class=\"qabtn\">QA ⚙️</button></div>\n      <div class=\"qa ${d2.state.qa ? 'show' : ''}\">\n        ${EMOS.map(e => `<button class=\"qaemo\" data-emo=\"${e}\">${e}</button>`).join('')}\n      </div>\n      <div class=\"log\">${body}</div>\n      <div class=\"bar\">\n        <input class=\"box\" type=\"text\" placeholder=\"Escreva uma mensagem…\" ${thinking ? 'disabled' : ''}>\n        <button class=\"send\" ${thinking ? 'disabled' : ''}>➤</button>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    setTalking(d2, false);\n    try { d2.assistant('vrm-agent').ask('oi', { json: true }); } catch (e) {} // warm up so the 1st real message isn't empty\n    let typer = null;\n\n    const scrollDown = () => { const l = d2.$('.log'); if (l) l.scrollTop = l.scrollHeight; };\n\n    function parseRes(res) {\n      let o = res;\n      if (typeof res === 'string') { try { o = JSON.parse(res); } catch (e) { o = { reply: res }; } }\n      if (!o || typeof o !== 'object') o = {};\n      const reply = (typeof o.reply === 'string') ? o.reply.trim() : (typeof res === 'string' ? res.trim() : '');\n      const emotion = EMOS.indexOf(o.emotion) >= 0 ? o.emotion : 'neutral';\n      const intensity = (typeof o.intensity === 'number') ? Math.max(0, Math.min(1, o.intensity)) : 0.7;\n      return { reply, emotion, intensity };\n    }\n\n    async function send() {\n      const box = d2.$('.box'); if (!box) return;\n      const text = (box.value || '').trim(); if (!text || d2.state.thinking) return;\n      box.value = '';\n      const msgs = (d2.state.msgs || []).slice();\n      msgs.push({ role: 'me', text });\n      d2.setState({ msgs, thinking: true, partial: null });\n      scrollDown();\n\n      const ctx = msgs.slice(-10).map(m => (m.role === 'me' ? 'User: ' : 'You: ') + m.text).join('\\n');\n      const a = d2.assistant('vrm-agent');\n      let parsed = { reply: '', emotion: 'neutral', intensity: 0.7 };\n      for (let attempt = 0; attempt < 3; attempt++) {\n        let res;\n        try { res = await a.ask(text, { json: true, context: ctx }); } catch (e) { res = null; }\n        parsed = parseRes(res);\n        if (parsed.reply) break;\n        await new Promise(r => setTimeout(r, 350)); // assistant warm-up on the very first call\n      }\n      if (!parsed.reply) {\n        d2.setState({ thinking: false });\n        const m2 = (d2.state.msgs || []).slice(); m2.push({ role: 'ai', text: '(não consegui responder — tenta de novo?)' });\n        d2.setState({ msgs: m2 });\n        return;\n      }\n      setEmotion(d2, parsed.emotion, parsed.intensity);\n      typeReply(parsed.reply);\n    }\n\n    function typeReply(reply) {\n      if (typer) clearInterval(typer);\n      d2.setState({ thinking: false, partial: '' });\n      setTalking(d2, true);\n      let i = 0;\n      typer = setInterval(() => {\n        i = Math.min(reply.length, i + 2);\n        d2.setState({ partial: reply.slice(0, i) });\n        scrollDown();\n        if (i >= reply.length) {\n          clearInterval(typer); typer = null;\n          setTalking(d2, false);\n          const msgs = (d2.state.msgs || []).slice();\n          msgs.push({ role: 'ai', text: reply });\n          d2.setState({ msgs, partial: null });\n          scrollDown();\n        }\n      }, 38);\n    }\n\n    d2.on('.send', 'click', send);\n    d2.on(root, 'keydown', (e) => { if (e.target && e.target.classList && e.target.classList.contains('box') && e.key === 'Enter') { e.preventDefault(); send(); } });\n    d2.on('.qabtn', 'click', () => d2.setState({ qa: !d2.state.qa }));\n    d2.on('.qaemo', 'click', (e) => { const emo = e.target.getAttribute('data-emo'); setEmotion(d2, emo, 0.9); });\n\n    d2.onUnmount(() => { if (typer) clearInterval(typer); });\n  }\n};\n\nfunction escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }\n"}}}}}}}},{"_id":"6a5581b2a9d5bff0a7a63aae","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"WormFXHero","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// --- Data2 product colors (2 gradient stops each), ring order = SVG top -> clockwise\nconst COLORS = [\n  ['#489afa', '#8448fa'], // 0 blue -> purple\n  ['#f74848', '#f7a048'], // 1 red -> orange\n  ['#fadc48', '#bffa48'], // 2 yellow -> lime\n  ['#48faa1', '#4bfafa'], // 3 green -> cyan\n  ['#4dfafa', '#4da4fa'], // 4 cyan -> blue\n  ['#fa48dc', '#f74882'], // 5 magenta -> pink\n  ['#8c48fa', '#dc48fa'], // 6 purple -> magenta\n  ['#bffa48', '#66fa48']  // 7 lime -> green\n];\n\n// --- 8 deterministic worm shapes (normalized [nx,ny] control points, same count so they lerp)\nconst SHAPES = [\n  [[0.64,-0.10],[0.82,0.10],[0.66,0.30],[0.50,0.48],[0.66,0.66],[0.83,0.80],[0.68,0.98],[0.78,1.12]],\n  [[0.72,-0.10],[0.80,0.12],[0.70,0.32],[0.80,0.50],[0.70,0.68],[0.80,0.84],[0.72,1.00],[0.78,1.12]],\n  [[0.55,-0.10],[0.85,0.16],[0.45,0.36],[0.82,0.54],[0.45,0.72],[0.82,0.88],[0.55,1.05],[0.75,1.12]],\n  [[0.70,-0.10],[0.74,0.14],[0.66,0.34],[0.72,0.52],[0.64,0.70],[0.72,0.86],[0.66,1.02],[0.72,1.12]],\n  [[0.60,-0.10],[0.80,0.14],[0.58,0.32],[0.45,0.50],[0.58,0.68],[0.82,0.82],[0.64,1.00],[0.80,1.12]],\n  [[0.50,-0.10],[0.86,0.14],[0.52,0.34],[0.80,0.52],[0.60,0.70],[0.74,0.86],[0.66,1.02],[0.74,1.12]],\n  [[0.48,-0.10],[0.62,0.14],[0.54,0.34],[0.70,0.52],[0.62,0.70],[0.82,0.86],[0.74,1.02],[0.86,1.12]],\n  [[0.66,-0.10],[0.84,0.10],[0.56,0.26],[0.82,0.42],[0.54,0.58],[0.82,0.74],[0.56,0.92],[0.78,1.12]]\n];\n\n// --- per-shape line-weight multiplier so \"the drawing\" also changes\nconst WIDTHS = [1.0, 0.82, 1.18, 0.9, 1.28, 1.06, 0.86, 1.34];\n\nexport default {\n  css: `\n    .scene{position:relative;width:100%;height:100%;overflow:hidden;background:#000;\n      font-family:'Inter','Helvetica Neue',system-ui,-apple-system,Arial,sans-serif;color:#fff}\n    .cv{position:absolute;inset:0;width:100%;height:100%;display:block}\n    .brand{position:absolute;top:46px;left:58px;font-size:40px;font-weight:500;\n      letter-spacing:-.5px;color:#fff;line-height:1;z-index:4}\n    .brand sup{font-size:.5em;font-weight:500}\n    .dot{position:absolute;left:60px;bottom:212px;width:30px;height:30px;border-radius:50%;\n      cursor:pointer;z-index:6;background:radial-gradient(circle at 34% 30%,#8bb0ff,#5a5cf0)}\n    .dot::after{content:'';position:absolute;inset:-6px;border-radius:50%;\n      border:2px solid var(--pulse,rgba(120,160,255,.55));\n      animation:d2pulse 2.4s ease-out infinite;pointer-events:none}\n    @keyframes d2pulse{0%{transform:scale(.75);opacity:.8}100%{transform:scale(1.7);opacity:0}}\n    .menu{position:absolute;left:75px;bottom:227px;width:0;height:0;z-index:5;pointer-events:none}\n    .ball{position:absolute;left:-17px;top:-17px;width:34px;height:34px;border:none;border-radius:50%;\n      padding:0;transform:translate(0,0) scale(.2);opacity:0;pointer-events:none;cursor:pointer;\n      box-shadow:0 4px 14px rgba(0,0,0,.5);\n      transition:transform .42s cubic-bezier(.22,1.1,.32,1),opacity .3s ease}\n    .menu.open .ball{transform:translate(var(--tx),var(--ty)) scale(1);opacity:1;pointer-events:auto}\n    .ball::before{content:'';position:absolute;inset:-11px;border-radius:50%}\n    .ball:hover{filter:brightness(1.15)}\n    .ball.sel{box-shadow:0 0 0 3px rgba(255,255,255,.9),0 4px 16px rgba(0,0,0,.5)}\n    .headline{position:absolute;left:57px;bottom:92px;font-size:46px;font-weight:800;\n      line-height:1.08;letter-spacing:-1px;color:#fff;z-index:4}\n    .sub{position:absolute;left:58px;bottom:40px;font-size:16px;line-height:1.5;\n      color:#8892b0;max-width:470px;font-weight:400;z-index:4}\n  `,\n  render(d2){\n    const esc = (s) => (s + '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n    const headline = d2.param('headline', 'Your voice,\\nyour interface.');\n    const subline = d2.param('subline', 'A direct extension of your imagination. UI with boundless freedom by Data².');\n    const hLines = (headline + '').split('\\n').map(esc).join('<br>');\n\n    const R = 180;\n    const balls = COLORS.map((c, i) => {\n      const a = (-105 + i * (110 / 7)) * Math.PI / 180; // fan up -> right\n      const dx = (Math.cos(a) * R).toFixed(1);\n      const dy = (Math.sin(a) * R).toFixed(1);\n      return `<button class=\"ball${i === 0 ? ' sel' : ''}\" data-i=\"${i}\" `\n        + `style=\"--tx:${dx}px;--ty:${dy}px;transition-delay:${i * 28}ms;`\n        + `background:linear-gradient(90deg,${c[0]},${c[1]})\"></button>`;\n    }).join('');\n\n    return `<div class=\"scene\">\n      <canvas class=\"cv\" data-d2-static></canvas>\n      <div class=\"brand\">Data<sup>2</sup></div>\n      <div class=\"menu\" data-menu>${balls}</div>\n      <div class=\"dot\" data-dot></div>\n      <div class=\"headline\">${hLines}</div>\n      <div class=\"sub\">${esc(subline)}</div>\n    </div>`;\n  },\n  mounted(d2, root){\n    const cv = d2.$('.cv');\n    const ctx = cv.getContext('2d');\n    const dotEl = d2.$('.dot');\n\n    // ---- color helpers ----\n    const hexToRgb = (h) => {\n      const n = parseInt(h.slice(1), 16);\n      return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n    };\n    const mix = (a, b, t) => [\n      a[0] + (b[0] - a[0]) * t,\n      a[1] + (b[1] - a[1]) * t,\n      a[2] + (b[2] - a[2]) * t\n    ];\n    const lighten = (rgb, t) => mix(rgb, [255, 255, 255], t);\n    const rgbStr = (rgb) => `rgb(${rgb[0] | 0},${rgb[1] | 0},${rgb[2] | 0})`;\n    const rgba = (rgb, a) => `rgba(${rgb[0] | 0},${rgb[1] | 0},${rgb[2] | 0},${a})`;\n\n    // ---- animated state (closure vars, read in raf — never d2.state in render) ----\n    let selected = 0;      // confirmed on click\n    let hoverIdx = null;   // live preview on hover; null => fall back to `selected`\n    let cur = SHAPES[0].map(p => [p[0], p[1]]);\n    let curW = WIDTHS[0];\n    let curC0 = hexToRgb(COLORS[0][0]);\n    let curC1 = hexToRgb(COLORS[0][1]);\n\n    const setSel = (i) => {\n      const balls = d2.$$('.ball');\n      balls.forEach(b => b.classList.toggle('sel', +b.dataset.i === i));\n    };\n\n    const buildPath = (P) => {\n      ctx.beginPath();\n      ctx.moveTo(P[0].x, P[0].y);\n      for (let i = 1; i < P.length - 1; i++){\n        const mx = (P[i].x + P[i + 1].x) / 2;\n        const my = (P[i].y + P[i + 1].y) / 2;\n        ctx.quadraticCurveTo(P[i].x, P[i].y, mx, my);\n      }\n      ctx.lineTo(P[P.length - 1].x, P[P.length - 1].y);\n    };\n\n    const draw = (t) => {\n      const w = cv.clientWidth, h = cv.clientHeight;\n      if (!w || !h) return;\n      ctx.clearRect(0, 0, w, h);\n\n      // base shape + BEMMM subtle idle motion (slow travelling wave)\n      const ax = w * 0.007, ay = h * 0.004;\n      const P = cur.map((p, i) => ({\n        x: p[0] * w + Math.sin(t * 0.0007 + i * 0.9) * ax,\n        y: p[1] * h + Math.sin(t * 0.0009 + i * 1.7) * ay\n      }));\n\n      const c0 = rgbStr(curC0), c1 = rgbStr(curC1);\n      const midRgb = mix(curC0, curC1, 0.5);\n      const grad = ctx.createLinearGradient(0, 0, w * 0.4, h);\n      grad.addColorStop(0, c0);\n      grad.addColorStop(1, c1);\n\n      ctx.lineCap = 'round';\n      ctx.lineJoin = 'round';\n      ctx.shadowColor = rgbStr(midRgb);\n      const bw = curW;\n\n      // Glow stays lush; the CORE now reads as the real hex (lightened only slightly),\n      // with a thin same-hue sheen instead of the old white line.\n      const passes = [\n        { width: 32 * bw, blur: 80, style: c1,                          alpha: 0.35 },\n        { width: 20 * bw, blur: 45, style: grad,                        alpha: 0.85 },\n        { width: 12 * bw, blur: 22, style: grad,                        alpha: 1    },\n        { width: 6  * bw, blur: 16, style: rgbStr(lighten(midRgb, 0.20)), alpha: 1  },\n        { width: 2.2* bw, blur: 8,  style: rgbStr(lighten(midRgb, 0.45)), alpha: 0.6 }\n      ];\n      for (const p of passes){\n        ctx.globalAlpha = p.alpha;\n        ctx.shadowBlur = p.blur;\n        ctx.strokeStyle = p.style;\n        ctx.lineWidth = p.width;\n        buildPath(P);\n        ctx.stroke();\n      }\n      ctx.globalAlpha = 1;\n      ctx.shadowBlur = 0;\n    };\n\n    const size = () => {\n      const dpr = window.devicePixelRatio || 1;\n      cv.width = cv.clientWidth * dpr;\n      cv.height = cv.clientHeight * dpr;\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.scale(dpr, dpr);\n    };\n\n    size();\n    if (typeof ResizeObserver !== 'undefined'){\n      const ro = new ResizeObserver(size);\n      ro.observe(root);\n      d2.onUnmount(() => ro.disconnect());\n    }\n\n    // ---- morph + recolor loop (target = hover preview, else confirmed selection) ----\n    const K = 0.05;\n    d2.raf((t) => {\n      const tg = (hoverIdx != null ? hoverIdx : selected);\n      const S = SHAPES[tg];\n      for (let i = 0; i < cur.length; i++){\n        cur[i][0] += (S[i][0] - cur[i][0]) * K;\n        cur[i][1] += (S[i][1] - cur[i][1]) * K;\n      }\n      curW += (WIDTHS[tg] - curW) * K;\n      curC0 = mix(curC0, hexToRgb(COLORS[tg][0]), K);\n      curC1 = mix(curC1, hexToRgb(COLORS[tg][1]), K);\n\n      // dot + pulse ring track the current color\n      const midRgb = mix(curC0, curC1, 0.5);\n      dotEl.style.background = `radial-gradient(circle at 34% 30%,${rgbStr(lighten(curC0, 0.4))},${rgbStr(curC1)})`;\n      dotEl.style.boxShadow = `0 0 22px 3px ${rgba(midRgb, 0.6)},0 0 6px 1px ${rgba(lighten(midRgb, 0.5), 0.9)}`;\n      dotEl.style.setProperty('--pulse', rgba(midRgb, 0.6));\n\n      draw(t);\n    });\n\n    // ---- interactions ----\n    d2.on('.dot', 'click', () => {\n      d2.$('.menu').classList.toggle('open');\n    });\n\n    // hover = live preview (worm + color + highlight); does NOT confirm\n    d2.$$('.ball').forEach((el) => {\n      const i = +el.dataset.i;\n      d2.on(el, 'mouseenter', () => { hoverIdx = i; setSel(i); });\n      d2.on(el, 'mouseleave', (e) => {\n        const rt = e.relatedTarget;\n        if (rt && rt.classList && rt.classList.contains('ball')) return; // gliding to another ball\n        hoverIdx = null; setSel(selected);\n      });\n    });\n\n    // click = confirm + close\n    d2.on('.ball', 'click', (e, el) => {\n      selected = +el.dataset.i;\n      hoverIdx = null;\n      setSel(selected);\n      d2.$('.menu').classList.remove('open');\n    });\n\n    // click-away closes the menu\n    d2.on(window, 'pointerdown', (e) => {\n      const menu = d2.$('.menu');\n      if (!menu || !menu.classList.contains('open')) return;\n      const path = e.composedPath ? e.composedPath() : [];\n      if (path.includes(dotEl)) return;\n      if (path.some(n => n && n.classList && n.classList.contains('ball'))) return;\n      menu.classList.remove('open');\n    });\n  }\n}"}},"headline":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"headline"}}}},"subline":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"subline"}}}},"accent":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accent"}}}}}}}}}},{"_id":"6a5e4dad136fbfcfe23232d6","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaCard","package":"bigaton-playground","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    .card{container-type:size;height:100%;box-sizing:border-box;\n      padding:8px;border:2px solid #c9ccd6;border-radius:10px;background:#fff;\n      font:600 12px/1.2 system-ui,sans-serif;color:#2b2f3a;user-select:none;\n      transition:transform .12s,box-shadow .12s,background .12s,border-color .12s}\n    /* Highlight is fully INSIDE the card box (inset ring) + border + bg — no\n       outward glow and no lift, so the CodeHost host (overflow:hidden) can't\n       clip the top. */\n    .card.on{border-color:#1d4ed8;border-width:3px;background:#e8f0ff;\n      box-shadow:inset 0 0 0 2px rgba(29,78,216,.5)}\n    .card.on .nm{background:#1d4ed8;color:#fff;border-radius:5px;padding:3px 4px}\n    .card.on .cell{background:#dbe4f5}\n    .card.on .cell.mv{background:#1d4ed8}\n    .card.on .cell.ctr{background:#1e2532}\n    .card.on .sp{background:#fff}\n    .card.muted{opacity:.6;filter:saturate(.45)}\n    .card.dim{opacity:.45}\n    .card.empty{display:flex;align-items:center;justify-content:center;border-style:dashed;color:#9aa0ad}\n    /* .inner is the flex parent — its container is .card, so the orientation\n       query below can switch it between column (portrait) and row (landscape). */\n    .inner{display:flex;flex-direction:column;gap:5px;height:100%;box-sizing:border-box}\n    .meta{order:1;display:flex;flex-direction:column;align-items:center;gap:3px;flex:none}\n    .hd{display:flex;flex-direction:column;align-items:center;gap:3px}\n    .sp{width:60px;height:60px;flex:none;object-fit:contain;background:#f6f7f9;border-radius:10px;padding:2px;box-sizing:border-box}\n    .nm{letter-spacing:.03em;text-transform:uppercase;font-size:11px;text-align:center;line-height:1.15}\n    .stage{order:2;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;container-type:size}\n    .grid{display:grid;grid-template-columns:repeat(5,1fr);grid-template-rows:repeat(5,1fr);gap:2px;\n      width:min(100cqw,100cqh);height:min(100cqw,100cqh)}\n    .cell{display:block;border-radius:2px;background:#eceef3}\n    .cell.mv{background:#2f6df6}\n    .cell.ctr{background:#3a3f4b}\n    .foot{order:3;text-align:center;font-size:10px;font-weight:500;color:#7b8191;min-height:12px}\n    .foot.sel{font:800 9px/1.35 system-ui,sans-serif;letter-spacing:.09em;color:#1d4ed8}\n    @container (max-height:165px){ .sp{width:42px;height:42px;border-radius:8px} .nm{font-size:10px} }\n    @container (max-height:128px){ .sp{width:34px;height:34px} }\n    /* Landscape (short & wide — mobile hand cards): move grid LEFT, Pokémon RIGHT. */\n    @container (orientation:landscape){\n      .inner{flex-direction:row;align-items:stretch;gap:6px}\n      .stage{order:1;flex:1}\n      .meta{order:2;flex:none;max-width:66px;justify-content:center}\n      .foot{display:none}\n      .nm{font-size:9px;line-height:1.1}\n    }\n  `,\n  render(d2){\n    const card = d2.param('card')\n    if (!card) return `<div class=\"card empty\">—</div>`\n    const flip = !!d2.param('flip')\n    const marks = new Set()\n    for (const mv of (card.moves || [])){\n      const dx = flip ? -mv[0] : mv[0]\n      const dy = flip ? -mv[1] : mv[1]\n      marks.add((2 + dx) + ',' + (2 + dy))\n    }\n    let cells = ''\n    for (let r = 0; r < 5; r++){\n      for (let c = 0; c < 5; c++){\n        const isCenter = (r === 2 && c === 2)\n        const isMove = marks.has(c + ',' + r)\n        cells += `<i class=\"cell ${isCenter ? 'ctr' : ''} ${isMove ? 'mv' : ''}\"></i>`\n      }\n    }\n    const cap = d2.param('caption', '')\n    const active = !!d2.param('active')\n    const cls = ['card']\n    if (active) cls.push('on')\n    if (d2.param('muted')) cls.push('muted')\n    if (d2.param('dim')) cls.push('dim')\n    const sprite = card.sprite || ''\n    const icon = sprite ? `<img class=\"sp\" src=\"${String(sprite).replace(/\"/g, '&quot;')}\" alt=\"\" loading=\"lazy\"/>` : ''\n    const foot = active\n      ? `<div class=\"foot sel\">● SELECTED</div>`\n      : `<div class=\"foot\">${cap ? String(cap).replace(/</g, '&lt;') : '&nbsp;'}</div>`\n    return `<div class=\"${cls.join(' ')}\"><div class=\"inner\">\n      <div class=\"meta\"><div class=\"hd\">${icon}<span class=\"nm\">${String(card.name || '').replace(/</g, '&lt;')}</span></div></div>\n      <div class=\"stage\"><div class=\"grid\">${cells}</div></div>\n      ${foot}\n    </div></div>`\n  },\n}"}},"card":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"card"}}}},"flip":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flip"}}}},"active":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"active"}}}},"muted":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"muted"}}}},"dim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dim"}}}},"caption":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"caption"}}}}}}}}}},{"_id":"6a5e4e1f136fbfcfe232342c","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaBoard","package":"bigaton-playground","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 idx = (r, c) => r * 5 + c\nconst inB = (r, c) => r >= 0 && r < 5 && c >= 0 && c < 5\nconst ARCH = { player: idx(4, 2), cpu: idx(0, 2) }\nconst other = s => (s === 'player' ? 'cpu' : 'player')\nconst sqName = i => 'abcde'[i % 5] + (5 - Math.floor(i / 5))\nconst NB8 = [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]]\n\nconst MASTERS = {\n  blastoise: { name: 'Blastoise', pokeId: 9, desc: 'Ranged attack: on a capture, the Master stays on its original square.' },\n  charizard: { name: 'Charizard', pokeId: 6, desc: 'Double move: if your move captures nothing, move the Master again.' },\n  venusaur:  { name: 'Venusaur',  pokeId: 3, desc: 'Reposition: after a capture, step to 1 empty square around (or stay).' },\n}\nconst MASTER_KEYS = ['venusaur', 'charizard', 'blastoise']\nconst MICON = id => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/' + id + '.png'\nconst POKEBALL = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png'\n\nfunction masterIdx(board, side){\n  for (let i = 0; i < 25; i++){ const p = board[i]; if (p && p.side === side && p.type === 'master') return i }\n  return -1\n}\nfunction stepOptions(board, at){\n  const r = Math.floor(at / 5), c = at % 5\n  const opts = [at]\n  for (const [dx, dy] of NB8){ const nr = r + dy, nc = c + dx; if (!inB(nr, nc)) continue\n    const si = idx(nr, nc); if (!board[si]) opts.push(si) }\n  return opts\n}\nfunction masterMoves(st, cards, mi){\n  const side = st.turn, hand = side === 'player' ? st.playerHand : st.cpuHand, out = []\n  if (mi < 0) return out\n  const r = Math.floor(mi / 5), c = mi % 5\n  for (const cardId of hand){ const card = cards[cardId]; if (!card) continue\n    for (const m of (card.moves || [])){\n      const dx = side === 'player' ? m[0] : -m[0], dy = side === 'player' ? m[1] : -m[1]\n      const nr = r + dy, nc = c + dx; if (!inB(nr, nc)) continue\n      const t = st.board[idx(nr, nc)]; if (t && t.side === side) continue\n      out.push({ from: mi, to: idx(nr, nc), cardId: cardId }) } }\n  return out\n}\n\nfunction initialBoard(){\n  const b = new Array(25).fill(null)\n  for (let c = 0; c < 5; c++){\n    b[idx(0, c)] = { side: 'cpu', type: c === 2 ? 'master' : 'student' }\n    b[idx(4, c)] = { side: 'player', type: c === 2 ? 'master' : 'student' }\n  }\n  return b\n}\nfunction dealCards(deck){\n  const d = deck.slice()\n  for (let i = d.length - 1; i > 0; i--){ const j = Math.floor(Math.random() * (i + 1)); const t = d[i]; d[i] = d[j]; d[j] = t }\n  return { playerHand: [d[0], d[1]], cpuHand: [d[2], d[3]], neutral: d[4] }\n}\n\nfunction genMoves(st, cards){\n  const side = st.turn\n  // Charizard forced second move: the Master only.\n  if (st.forced){\n    const mm = masterMoves(st, cards, masterIdx(st.board, side))\n    return mm.length ? mm : [{ forcedSkip: true }]\n  }\n  const hand = side === 'player' ? st.playerHand : st.cpuHand\n  const norm = []\n  for (const cardId of hand){\n    const card = cards[cardId]; if (!card) continue\n    for (let i = 0; i < 25; i++){\n      const p = st.board[i]; if (!p || p.side !== side) continue\n      const r = Math.floor(i / 5), c = i % 5\n      for (const m of (card.moves || [])){\n        const dx = side === 'player' ? m[0] : -m[0], dy = side === 'player' ? m[1] : -m[1]\n        const nr = r + dy, nc = c + dx; if (!inB(nr, nc)) continue\n        const t = st.board[idx(nr, nc)]; if (t && t.side === side) continue\n        norm.push({ from: i, to: idx(nr, nc), cardId: cardId })\n      }\n    }\n  }\n  if (!norm.length){ for (const cardId of hand) norm.push({ pass: true, cardId: cardId }); return norm }\n  // Ability variants — MASTER piece only (used by the AI search; player arms via UI).\n  const master = (st.masters || {})[side], used = (st.used || {})[side]\n  if (!master || used) return norm\n  const mi = masterIdx(st.board, side)\n  const out = norm.slice()\n  for (const mv of norm){\n    if (mv.from !== mi) continue                    // ability only on Master moves\n    const target = st.board[mv.to]\n    if (master === 'blastoise' && target && target.side !== side)\n      out.push({ from: mv.from, to: mv.to, cardId: mv.cardId, ab: 'blastoise' })\n    if (master === 'venusaur' && target && target.side !== side){\n      const b2 = st.board.slice(); b2[mv.to] = b2[mv.from]; b2[mv.from] = null\n      for (const s of stepOptions(b2, mv.to))\n        out.push({ from: mv.from, to: mv.to, cardId: mv.cardId, ab: 'venusaur', step: s })\n    }\n    if (master === 'charizard' && !target)\n      out.push({ from: mv.from, to: mv.to, cardId: mv.cardId, ab: 'charizard' })\n  }\n  return out\n}\n\nfunction applyMove(st, mv){\n  const mover = st.turn\n  if (mv.forcedSkip) return Object.assign({}, st, { turn: other(mover), forced: false, winner: null, winReason: null })\n  const board = st.board.slice()\n  let winner = null, reason = null, stay = false, forced = false\n  const p = board[mv.from], target = board[mv.to]\n  if (mv.ab === 'blastoise' && p.type === 'master' && target){\n    if (target.type === 'master'){ winner = mover; reason = 'stone' }\n    board[mv.to] = null\n  } else if (mv.ab === 'venusaur' && p.type === 'master' && target){\n    board[mv.to] = p; board[mv.from] = null\n    if (target.type === 'master'){ winner = mover; reason = 'stone' }\n    const step = (mv.step == null ? mv.to : mv.step)\n    let mpos = mv.to\n    if (step !== mv.to && !board[step]){ board[step] = board[mv.to]; board[mv.to] = null; mpos = step }\n    if (!winner && mpos === ARCH[other(mover)]){ winner = mover; reason = 'stream' }\n  } else {\n    board[mv.to] = p; board[mv.from] = null\n    if (target && target.type === 'master'){ winner = mover; reason = 'stone' }\n    if (p.type === 'master' && mv.to === ARCH[other(mover)]){ winner = mover; reason = 'stream' }\n    if (mv.ab === 'charizard' && !target){ stay = true; forced = true }\n  }\n  const key = mover === 'player' ? 'playerHand' : 'cpuHand'\n  const hand = st[key].filter(c => c !== mv.cardId).concat([st.neutral])\n  const used = Object.assign({}, st.used || { player: false, cpu: false })\n  if (mv.ab) used[mover] = true\n  return {\n    board: board,\n    playerHand: key === 'playerHand' ? hand : st.playerHand,\n    cpuHand: key === 'cpuHand' ? hand : st.cpuHand,\n    neutral: mv.cardId, turn: stay ? mover : other(mover),\n    winner: winner, winReason: reason, masters: st.masters, used: used, forced: forced,\n  }\n}\n\nfunction attacksMaster(st, attacker, cards){\n  const probe = { board: st.board, playerHand: st.playerHand, cpuHand: st.cpuHand,\n    neutral: st.neutral, turn: attacker, winner: null, masters: st.masters, used: st.used, forced: false }\n  for (const m of genMoves(probe, cards)){\n    if (m.pass || m.ab) continue\n    const t = st.board[m.to]\n    if (t && t.type === 'master' && t.side !== attacker) return true\n  }\n  return false\n}\nfunction evaluate(st, me, cards){\n  const opp = other(me)\n  let myS = 0, opS = 0, myM = -1, opM = -1\n  for (let i = 0; i < 25; i++){ const p = st.board[i]; if (!p) continue\n    if (p.side === me){ if (p.type === 'master') myM = i; else myS++ }\n    else { if (p.type === 'master') opM = i; else opS++ } }\n  if (opM < 0) return 90000\n  if (myM < 0) return -90000\n  const dist = (a, b) => Math.abs(Math.floor(a / 5) - Math.floor(b / 5)) + Math.abs(a % 5 - b % 5)\n  let s = (myS - opS) * 100\n  s += (8 - dist(myM, ARCH[opp])) * 3\n  s -= (8 - dist(opM, ARCH[me])) * 3\n  if (attacksMaster(st, opp, cards)) s -= (st.turn === opp ? 6000 : 800)\n  if (attacksMaster(st, me, cards)) s += (st.turn === me ? 6000 : 800)\n  return s\n}\nfunction search(st, cards, depth, alpha, beta, me, deadline){\n  if (st.winner) return st.winner === me ? 100000 + depth : -100000 - depth\n  if (depth <= 0 || (deadline && Date.now() > deadline)) return evaluate(st, me, cards)\n  const moves = genMoves(st, cards)\n  const maxing = st.turn === me\n  let best = maxing ? -Infinity : Infinity\n  for (const mv of moves){\n    const v = search(applyMove(st, mv), cards, depth - 1, alpha, beta, me, deadline)\n    if (maxing){ if (v > best) best = v; if (best > alpha) alpha = best }\n    else { if (v < best) best = v; if (best < beta) beta = best }\n    if (beta <= alpha) break\n  }\n  return best\n}\nfunction chooseMove(st, cards, depth){\n  const me = st.turn, moves = genMoves(st, cards)\n  if (!moves.length) return null\n  for (let i = moves.length - 1; i > 0; i--){ const j = Math.floor(Math.random() * (i + 1)); const t = moves[i]; moves[i] = moves[j]; moves[j] = t }\n  // Hard time cap so a pathological branch factor can never freeze the turn.\n  // Normal play finishes well under 100ms, so this never bites there; if it\n  // ever does, we return the best move found so far (never a random one).\n  const deadline = Date.now() + 3000\n  let bestMv = moves[0], bestV = -Infinity\n  for (const mv of moves){\n    const v = search(applyMove(st, mv), cards, depth - 1, -Infinity, Infinity, me, deadline)\n    if (v > bestV){ bestV = v; bestMv = mv }\n  }\n  return bestMv\n}\n\n// Any legal move — the fallback path (still legal, just not searched).\nfunction anyLegalMove(st, cards){\n  const ms = genMoves(st, cards)\n  return ms.length ? ms[Math.floor(Math.random() * ms.length)] : null\n}\n\n// Robust CPU turn: computes a move and NEVER leaves the turn stuck. On any\n// error or empty result it plays a legal fallback; worst case it passes the\n// turn back. The full minimax still runs in the normal case — no weakening.\nfunction runCpuMove(d2, key){\n  const s = d2.store('game')\n  if (s.get('phase') !== 'playing' || s.get('turn') !== 'cpu' || s.get('winner')) return\n  if ((s.get('gameId') + ':' + s.get('moveCount')) !== key) return   // stale timer\n  const prev = readState(s)\n  const cards = s.get('cards') || {}\n  let mv = null\n  try { mv = chooseMove(prev, cards, s.get('aiDepth') || 3) } catch (e) { mv = null }\n  if (!mv){ try { mv = anyLegalMove(prev, cards) } catch (e) { mv = null } }\n  try {\n    if (mv){\n      writeState(s, applyMove(prev, mv))\n      if (!mv.forcedSkip) pushLog(s, prev, mv, 'CPU')\n    } else {\n      s.set('turn', 'player'); s.set('forced', false)\n    }\n  } catch (e){\n    s.set('turn', 'player'); s.set('forced', false)\n  }\n  s.set('moveCount', (s.get('moveCount') || 0) + 1)\n  if (d2.state.aiWatch){ clearTimeout(d2.state.aiWatch); d2.state.aiWatch = null }\n}\n\nfunction readState(g){\n  return {\n    board: g.get('board') || [], playerHand: g.get('playerHand') || [], cpuHand: g.get('cpuHand') || [],\n    neutral: g.get('neutral'), turn: g.get('turn') || 'player', winner: g.get('winner') || null,\n    masters: g.get('masters') || { player: null, cpu: null }, used: g.get('used') || { player: false, cpu: false },\n    forced: !!g.get('forced'),\n  }\n}\nfunction writeState(g, st){\n  g.set('board', st.board); g.set('playerHand', st.playerHand); g.set('cpuHand', st.cpuHand)\n  g.set('neutral', st.neutral); g.set('turn', st.turn)\n  g.set('winner', st.winner || null); g.set('winReason', st.winReason || null)\n  if (st.used) g.set('used', st.used)\n  g.set('forced', !!st.forced)\n  if (st.winner) g.set('phase', 'over')\n}\nfunction pushLog(g, prev, mv, who){\n  const cards = g.get('cards') || {}\n  const nm = (cards[mv.cardId] || {}).name || mv.cardId\n  let txt\n  if (mv.pass){ txt = who + ' passed — swapped ' + nm }\n  else {\n    const cap = prev.board[mv.to]\n    txt = who + ': ' + sqName(mv.from) + '→' + sqName(mv.to) + ' (' + nm + ')' +\n      (cap ? (cap.type === 'master' ? ' — CAPTURED THE MASTER' : ' — captured') : '')\n    if (mv.ab) txt += ' ⚡'\n  }\n  g.set('log', [txt].concat((g.get('log') || []).slice(0, 40)))\n}\nfunction markUsed(g, side){\n  const u = Object.assign({}, g.get('used') || { player: false, cpu: false }); u[side] = true; g.set('used', u)\n}\nfunction movesFor(g, from){\n  const cardId = g.get('selectedCard'); if (!cardId) return []\n  return genMoves(readState(g), g.get('cards') || {})\n    .filter(m => !m.ab && !m.forcedSkip && !m.pass && m.from === from && m.cardId === cardId)\n}\n\nfunction clearTable(g){\n  g.set('board', []); g.set('winner', null); g.set('winReason', null)\n  g.set('armed', false); g.set('bonusMove', false); g.set('forced', false)\n  g.set('playerHand', []); g.set('cpuHand', []); g.set('neutral', null)\n  g.set('selected', null); g.set('selectedCard', null); g.set('stepOpts', []); g.set('stepBase', null)\n}\nfunction beginModeSelect(g){ clearTable(g); g.set('phase', 'selectMode') }\nfunction beginMasterSelect(g){ clearTable(g); g.set('phase', 'selectMaster') }\nfunction dealGame(g, playerMaster, mode){\n  const deck = g.get('deck') || []; if (deck.length < 5) return\n  const master = mode === 'master'\n  const cpuMaster = master ? MASTER_KEYS[Math.floor(Math.random() * MASTER_KEYS.length)] : null\n  const dealt = dealCards(deck)\n  g.set('board', initialBoard())\n  g.set('playerHand', dealt.playerHand); g.set('cpuHand', dealt.cpuHand); g.set('neutral', dealt.neutral)\n  g.set('turn', 'player'); g.set('phase', 'playing'); g.set('winner', null); g.set('winReason', null)\n  g.set('selected', null); g.set('selectedCard', dealt.playerHand[0])\n  g.set('mode', mode)\n  g.set('masters', { player: master ? playerMaster : null, cpu: cpuMaster })\n  g.set('used', { player: false, cpu: false }); g.set('armed', false); g.set('bonusMove', false); g.set('forced', false)\n  g.set('stepOpts', []); g.set('stepBase', null)\n  g.set('log', [master\n    ? ('You: ' + MASTERS[playerMaster].name + ' · CPU: ' + MASTERS[cpuMaster].name + '. You go first.')\n    : 'Traditional mode. You go first.'])\n  g.set('moveCount', 0)\n  g.set('gameId', (g.get('gameId') || 0) + 1)   // unique per match — see the CPU turn key\n}\n\n// Executes a normal player move (no ability), passes the turn.\nfunction doNormal(g, prev, mv){\n  const after = applyMove(prev, mv)\n  writeState(g, after)\n  g.set('selected', null); g.set('selectedCard', (after.playerHand || [])[0])\n  g.set('bonusMove', false); g.set('armed', false)\n  pushLog(g, prev, mv, 'You')\n  g.set('moveCount', (g.get('moveCount') || 0) + 1)\n}\n\nexport default {\n  css: `\n    .wrap{display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;\n      font:500 13px/1.3 system-ui,sans-serif;color:#2b2f3a;background:#fff;border:1px solid #dfe2ea;border-radius:12px;position:relative;container-type:inline-size}\n    .stage{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;container-type:size}\n    /* Neutral card tucked to the LEFT of the grid, inside the board (mobile only). */\n    .playfield{display:flex;gap:8px;flex:1;min-height:0}\n    .neutral-col{display:none;flex:none;width:78px;flex-direction:column;gap:3px;justify-content:center}\n    .nlabel{font-size:8px;text-transform:uppercase;letter-spacing:.03em;color:#9aa0ad;text-align:center;line-height:1.15}\n    .ncard{flex:0 1 182px;min-height:0}\n    @container (max-width:480px){\n      .neutral-col{display:flex}\n      /* Keep the bar on ONE line so a status/ability text change can't reflow\n         it (which resized the grid and made the board flicker). */\n      .bar{flex-wrap:nowrap}\n      .bar .restart{display:none}   /* New game lives in the History drawer on mobile */\n      /* Master picker: keep all 3 on one row, shrunk to fit & stay readable. */\n      .masters{gap:8px}\n      .mcard{padding:10px 5px;gap:4px}\n      .mbig{width:50px;height:50px}\n      .mname{font-size:12px}\n      .mdesc{font-size:9px;line-height:1.22}\n      .pick{gap:8px}\n      .pick-t{font-size:15px}\n    }\n    .grid{display:grid;grid-template-columns:repeat(5,1fr);grid-template-rows:repeat(5,1fr);\n      gap:4px;width:min(100cqw,100cqh);height:min(100cqw,100cqh)}\n    .sq{position:relative;display:flex;align-items:center;justify-content:center;\n      background:#e9ebf1;border-radius:6px;cursor:default;transition:background .12s}\n    .sq.alt{background:#e1e4ec}\n    .sq.archC{box-shadow:inset 0 0 0 2px #d2604a}\n    .sq.archP{box-shadow:inset 0 0 0 2px #2f6df6}\n    .sq.sel{background:#ffe8a3}\n    .sq.move{cursor:pointer;background:#bfe3c4}\n    .sq.move::after{content:'';width:26%;aspect-ratio:1;height:auto;border-radius:50%;background:#3f9a52;position:absolute}\n    .sq.cap{cursor:pointer;background:#f6b5ab;box-shadow:inset 0 0 0 3px #cf4b32}\n    .sq.step{cursor:pointer;background:#cde4ff;box-shadow:inset 0 0 0 2px #1d4ed8}\n    .sq.stepstay{cursor:pointer;background:#fff2c9;box-shadow:inset 0 0 0 2px #b7791f}\n    .pc{display:flex;align-items:center;justify-content:center;width:66%;aspect-ratio:1;height:auto;\n      border-radius:50%;font:800 13px/1 system-ui,sans-serif;color:#fff}\n    .pc.player{background:#2f6df6}\n    .pc.cpu{background:#d2604a}\n    .pc.master{background:#fff;width:82%}\n    .pc.player.master{border:3px solid #2f6df6;color:#2f6df6}\n    .pc.cpu.master{border:3px solid #d2604a;color:#d2604a}\n    .mi{width:84%;height:84%;object-fit:contain;display:block}\n    .pc.clickable{cursor:pointer}\n    .bar{display:flex;align-items:center;gap:8px;min-height:26px;flex-wrap:wrap}\n    .spacer{flex:1}\n    .charges{display:flex;align-items:center;gap:5px}\n    .pball{width:22px;height:22px;border-radius:50%;padding:1px;box-sizing:border-box;display:flex;flex:none}\n    .pball img{width:100%;height:100%;object-fit:contain}\n    .pball.you{box-shadow:0 0 0 2px #2f6df6}\n    .pball.cpu{box-shadow:0 0 0 2px #d2604a}\n    .pball.spent{opacity:.9}\n    .pball.spent img{filter:grayscale(1) brightness(.3)}\n    .tag{padding:3px 10px;border-radius:999px;font-weight:700;font-size:12px}\n    .tag.player{background:#e4edff;color:#1d4ed8}\n    .tag.cpu{background:#fdeae6;color:#b23a22}\n    .tag.over{background:#3a3f4b;color:#fff}\n    .tag.act{background:#fff2c9;color:#b7791f}\n    .btn{padding:5px 12px;border:0;border-radius:8px;background:#3a3f4b;color:#fff;font:700 12px system-ui,sans-serif;cursor:pointer}\n    .btn.warn{background:#c2410c}\n    .btn.abn{background:#7c3aed}\n    .btn.abn.armed{background:#b7791f;box-shadow:0 0 0 3px rgba(183,121,31,.25)}\n    .empty{display:flex;align-items:center;justify-content:center;height:100%;color:#9aa0ad}\n    .pick{display:flex;flex-direction:column;gap:14px;height:100%;justify-content:center;align-items:center}\n    .pick-t{font-size:16px;font-weight:800;letter-spacing:.02em}\n    .pick-s{font-size:12px;color:#7b8191;margin-top:-8px}\n    .masters{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;width:100%;max-width:560px}\n    .mcard{display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;\n      border:2px solid #e6e8ee;border-radius:12px;background:#fff;cursor:pointer;text-align:center}\n    .mcard:hover{border-color:#7c3aed;box-shadow:0 6px 16px rgba(124,58,237,.18);transform:translateY(-2px)}\n    .mbig{width:72px;height:72px;object-fit:contain}\n    .mname{font-size:14px;font-weight:800}\n    .mdesc{font-size:11px;color:#6b7280;line-height:1.3}\n    .modes{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:100%;max-width:460px}\n    .modecard{display:flex;flex-direction:column;align-items:center;gap:6px;padding:18px 12px;\n      border:2px solid #e6e8ee;border-radius:12px;background:#fff;cursor:pointer;text-align:center}\n    .modecard:hover{border-color:#7c3aed;box-shadow:0 6px 16px rgba(124,58,237,.18);transform:translateY(-2px)}\n    .micon{font-size:34px;height:44px;display:flex;align-items:center;justify-content:center}\n    .overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\n      background:rgba(247,248,250,.82);border-radius:12px;z-index:5}\n    .result{display:flex;flex-direction:column;align-items:center;gap:12px;padding:26px 30px;text-align:center;\n      background:#fff;border-radius:16px;box-shadow:0 16px 44px rgba(0,0,0,.18);border:1px solid #e6e8ee}\n    .result h2{font-size:24px;margin:0}\n    .result.win h2{color:#1e6b33}\n    .result.lose h2{color:#b23a22}\n    .result p{margin:0;color:#6b7280;font-size:13px}\n    .again{background:#1d4ed8;font-size:14px;padding:10px 22px}\n  `,\n  render(d2){\n    const g = d2.store('game')\n    const phase = g.get('phase')\n\n    if (phase === 'selectMode'){\n      return `<div class=\"wrap\"><div class=\"pick\">\n        <div class=\"pick-t\">Choose a mode</div>\n        <div class=\"pick-s\">Hover a mode to see what it does.</div>\n        <div class=\"modes\">\n          <button class=\"modecard\" data-mode=\"traditional\" data-key=\"mode-trad\" title=\"Classic Onitama — no master powers.\">\n            <div class=\"micon\">♟️</div>\n            <div class=\"mname\">Traditional</div>\n            <div class=\"mdesc\">Classic Onitama. No master powers.</div>\n          </button>\n          <button class=\"modecard\" data-mode=\"master\" data-key=\"mode-master\" title=\"Each master has a unique power, usable once per match.\">\n            <div class=\"micon\"><img src=\"${POKEBALL}\" alt=\"\" style=\"width:40px;height:40px;object-fit:contain\"/></div>\n            <div class=\"mname\">Pokémon Master</div>\n            <div class=\"mdesc\">Pick a master with a unique power (once per match).</div>\n          </button>\n        </div>\n      </div></div>`\n    }\n\n    if (phase === 'selectMaster'){\n      const cards = MASTER_KEYS.map(k => `\n        <button class=\"mcard\" data-master=\"${k}\" data-key=\"mc-${k}\">\n          <img class=\"mbig\" src=\"${MICON(MASTERS[k].pokeId)}\" alt=\"\" loading=\"lazy\"/>\n          <div class=\"mname\">${MASTERS[k].name}</div>\n          <div class=\"mdesc\">${MASTERS[k].desc}</div>\n        </button>`).join('')\n      return `<div class=\"wrap\"><div class=\"pick\">\n        <div class=\"pick-t\">Choose your Master</div>\n        <div class=\"pick-s\">Each one has an ability you can use once per match.</div>\n        <div class=\"masters\">${cards}</div>\n      </div></div>`\n    }\n\n    const board = g.get('board') || []\n    if (!board.length) return `<div class=\"wrap\"><div class=\"empty\">Starting…</div></div>`\n\n    const masters = g.get('masters') || { player: null, cpu: null }\n    const used = g.get('used') || { player: false, cpu: false }\n    const turn = g.get('turn')\n    const winner = g.get('winner')\n    const armed = !!g.get('armed')\n    const bonus = !!g.get('bonusMove')\n\n    const legalMap = {}\n    if (phase === 'playing' && !winner && turn === 'player'){\n      const sel = g.get('selected')\n      const legal = (sel === null || sel === undefined) ? [] : movesFor(g, sel)\n      for (const m of legal) legalMap[m.to] = true\n    }\n    const stepOpts = (phase === 'venusaurStep') ? (g.get('stepOpts') || []) : []\n    const stepBase = g.get('stepBase')\n    const sel = g.get('selected')\n\n    let cells = ''\n    for (let i = 0; i < 25; i++){\n      const p = board[i]\n      const r = Math.floor(i / 5), c = i % 5\n      const cls = ['sq']\n      if ((r + c) % 2 === 1) cls.push('alt')\n      if (i === ARCH.cpu) cls.push('archC')\n      if (i === ARCH.player) cls.push('archP')\n      if (sel === i && phase === 'playing') cls.push('sel')\n      if (legalMap[i]) cls.push(p ? 'cap' : 'move')\n      if (stepOpts.indexOf(i) !== -1) cls.push(i === stepBase ? 'stepstay' : 'step')\n      const pickable = phase === 'playing' && !winner && turn === 'player' && p && p.side === 'player'\n      let piece = ''\n      if (p){\n        if (p.type === 'master'){\n          const mk = masters[p.side]\n          const inner = mk ? `<img class=\"mi\" src=\"${MICON(MASTERS[mk].pokeId)}\" alt=\"\"/>` : 'M'\n          piece = `<span class=\"pc ${p.side} master ${pickable ? 'clickable' : ''}\">${inner}</span>`\n        } else {\n          piece = `<span class=\"pc ${p.side} student ${pickable ? 'clickable' : ''}\"></span>`\n        }\n      }\n      cells += `<div class=\"${cls.join(' ')}\" data-i=\"${i}\" data-key=\"c${i}\">${piece}</div>`\n    }\n\n    let status\n    if (winner){\n      const how = g.get('winReason') === 'stream' ? 'temple taken' : 'master captured'\n      status = `<span class=\"tag over\">${winner === 'player' ? 'YOU WIN' : 'CPU WINS'} — ${how}</span>`\n    } else if (phase === 'venusaurStep'){\n      status = `<span class=\"tag act\">Venusaur: pick a square (origin = stay)</span>`\n    } else if (bonus && turn === 'player'){\n      status = `<span class=\"tag act\">Charizard: move the Master again</span>`\n    } else {\n      status = turn === 'player' ? `<span class=\"tag player\">Your turn</span>` : `<span class=\"tag cpu\">CPU thinking…</span>`\n    }\n\n    const pball = (side, klass) => masters[side]\n      ? `<span class=\"pball ${klass} ${used[side] ? 'spent' : ''}\" title=\"${MASTERS[masters[side]].name} ability ${used[side] ? '(used)' : '(ready)'}\"><img src=\"${POKEBALL}\" alt=\"\"/></span>`\n      : ''\n    const charges = `<span class=\"charges\">${pball('player', 'you')}${pball('cpu', 'cpu')}</span>`\n\n    let abilityBtn = ''\n    if (phase === 'playing' && turn === 'player' && !winner && !bonus){\n      const pm = masters.player\n      if (pm && !used.player){\n        abilityBtn = armed\n          ? `<button class=\"btn abn armed\">⚡ ${MASTERS[pm].name} armed — cancel</button>`\n          : `<button class=\"btn abn\">⚡ Use ${MASTERS[pm].name}</button>`\n      }\n    }\n\n    let passBtn = ''\n    if (phase === 'playing' && !winner && turn === 'player'){\n      const mine = genMoves(readState(g), g.get('cards') || {})\n      if (mine.length && mine[0].pass) passBtn = `<button class=\"btn warn passer\">No moves — pass</button>`\n    }\n\n    const cardsMap = g.get('cards') || {}\n    const neutralId = g.get('neutral')\n    const neutralCard = neutralId ? cardsMap[neutralId] : null\n    const neutralHtml = neutralCard\n      ? `<div class=\"nlabel\">Neutral · ${turn === 'player' ? 'to you' : 'to CPU'}</div><div class=\"ncard\">${d2.render('bigaton-playground@OnitamaCard', { card: neutralCard, caption: '' }, { key: 'bneutral-' + neutralId })}</div>`\n      : ''\n\n    return `<div class=\"wrap\">\n      <div class=\"bar\">${charges}${status}${abilityBtn}${passBtn}<span class=\"spacer\"></span><button class=\"btn restart\">New game</button></div>\n      <div class=\"playfield\">\n        <div class=\"neutral-col\">${neutralHtml}</div>\n        <div class=\"stage\"><div class=\"grid\">${cells}</div></div>\n      </div>\n      ${winner ? `<div class=\"overlay\"><div class=\"result ${winner === 'player' ? 'win' : 'lose'}\">\n        <h2>${winner === 'player' ? 'You won! 🎉' : 'You lost'}</h2>\n        <p>${winner === 'player'\n          ? (g.get('winReason') === 'stream' ? 'You reached the enemy temple.' : 'You captured the enemy Master.')\n          : (g.get('winReason') === 'stream' ? 'The CPU reached your temple.' : 'The CPU captured your Master.')}</p>\n        <button class=\"btn again\">Play again</button>\n      </div></div>` : ''}\n    </div>`\n  },\n  mounted(d2){\n    d2.on('.modecard', 'click', (e, el) => {\n      const s = d2.store('game')\n      if (el.dataset.mode === 'master') beginMasterSelect(s); else dealGame(s, null, 'traditional')\n    })\n    d2.on('.mcard', 'click', (e, el) => dealGame(d2.store('game'), el.dataset.master, 'master'))\n\n    d2.on('.abn', 'click', () => {\n      const s = d2.store('game')\n      const nowArmed = !s.get('armed')\n      s.set('armed', nowArmed)\n      if (nowArmed) s.set('selected', masterIdx(s.get('board') || [], 'player'))  // arming auto-selects the Master\n    })\n\n    d2.on('.sq', 'click', (e, el) => {\n      const s = d2.store('game')\n      if (s.get('winner') || s.get('turn') !== 'player') return\n      const i = Number(el.dataset.i)\n      const phase = s.get('phase')\n\n      // Venusaur reposition sub-phase\n      if (phase === 'venusaurStep'){\n        const opts = s.get('stepOpts') || []\n        if (opts.indexOf(i) === -1) return\n        const base = s.get('stepBase')\n        const board = (s.get('board') || []).slice()\n        let winner = null, reason = null\n        if (i !== base){ board[i] = board[base]; board[base] = null }\n        const mp = (i !== base) ? i : base\n        if (mp === ARCH.cpu){ winner = 'player'; reason = 'stream' }\n        s.set('board', board); s.set('winner', winner); s.set('winReason', reason)\n        s.set('phase', winner ? 'over' : 'playing')\n        s.set('stepOpts', []); s.set('stepBase', null)\n        s.set('log', ['Venusaur repositioned to ' + sqName(mp) + (i === base ? ' (stayed)' : '')].concat((s.get('log') || []).slice(0, 40)))\n        s.set('turn', 'cpu'); s.set('selectedCard', (s.get('playerHand') || [])[0])\n        s.set('moveCount', (s.get('moveCount') || 0) + 1)\n        return\n      }\n      if (phase !== 'playing') return\n\n      const bonus = !!s.get('bonusMove')\n      const sel = s.get('selected')\n      const cur = (sel === null || sel === undefined) ? [] : movesFor(s, sel)\n      const hit = cur.find(m => m.to === i)\n\n      if (hit){\n        const prev = readState(s)\n        const mv = { from: hit.from, to: hit.to, cardId: hit.cardId }\n\n        // Charizard forced second move: always a normal Master move; then the turn passes.\n        if (bonus){ doNormal(s, prev, mv); return }\n\n        const armed = !!s.get('armed')\n        const master = (s.get('masters') || {}).player\n        const isMasterMove = prev.board[hit.from] && prev.board[hit.from].type === 'master'\n\n        if (armed && !isMasterMove){\n          // Ability abilities are Master-only: moving another piece CANCELS the ability (not spent).\n          doNormal(s, prev, mv)\n          return\n        }\n        if (armed && isMasterMove){\n          const target = prev.board[hit.to]\n          if (master === 'venusaur' && target){\n            const after = applyMove(prev, mv)                 // normal capture, then pick the step\n            writeState(s, Object.assign({}, after, { turn: 'player' }))\n            markUsed(s, 'player'); s.set('armed', false); s.set('selected', null)\n            s.set('selectedCard', (after.playerHand || [])[0])\n            s.set('stepBase', hit.to); s.set('stepOpts', stepOptions(after.board, hit.to)); s.set('phase', 'venusaurStep')\n            pushLog(s, prev, { from: hit.from, to: hit.to, cardId: hit.cardId, ab: 'venusaur' }, 'You')\n            return\n          }\n          mv.ab = master\n          let after = applyMove(prev, mv)                      // blastoise ranged / charizard double\n          markUsed(s, 'player'); s.set('armed', false)\n          s.set('selectedCard', (after.playerHand || [])[0])\n          let bonusNext = after.turn === 'player'\n          if (bonusNext && !masterMoves(after, s.get('cards') || {}, masterIdx(after.board, 'player')).length){\n            after = applyMove(after, { forcedSkip: true })     // Charizard, but the Master has no 2nd move -> pass\n            bonusNext = false\n          }\n          writeState(s, after)\n          s.set('bonusMove', bonusNext)\n          s.set('selected', bonusNext ? masterIdx(after.board, 'player') : null)   // auto-select Master for the 2nd move\n          pushLog(s, prev, mv, 'You')\n          s.set('moveCount', (s.get('moveCount') || 0) + 1)\n          return\n        }\n        // not armed — plain move\n        doNormal(s, prev, mv)\n        return\n      }\n\n      // Selecting a piece. During the Charizard bonus, only the Master is selectable.\n      const p = (s.get('board') || [])[i]\n      if (bonus){ if (p && p.type === 'master' && p.side === 'player') s.set('selected', i); return }\n      s.set('selected', (p && p.side === 'player') ? i : null)\n    })\n\n    d2.on('.restart', 'click', () => beginModeSelect(d2.store('game')))\n    d2.on('.again', 'click', () => beginModeSelect(d2.store('game')))\n    d2.on('.passer', 'click', () => {\n      const s = d2.store('game')\n      const prev = readState(s)\n      const mv = genMoves(prev, s.get('cards') || {})[0]\n      if (!mv || !mv.pass) return\n      writeState(s, applyMove(prev, mv))\n      pushLog(s, prev, mv, 'You')\n      s.set('selected', null); s.set('bonusMove', false)\n      s.set('selectedCard', (s.get('playerHand') || [])[0])\n      s.set('moveCount', (s.get('moveCount') || 0) + 1)\n    })\n\n    if (d2.bus) d2.bus.on('onitama:newGame', () => beginModeSelect(d2.store('game')))\n    d2.onUnmount(() => { if (d2.state.aiTimer) clearTimeout(d2.state.aiTimer); if (d2.state.aiWatch) clearTimeout(d2.state.aiWatch) })\n  },\n  updated(d2){\n    const g = d2.store('game')\n\n    if (g.get('phase') !== 'playing' || g.get('turn') !== 'cpu' || g.get('winner')){\n      if (d2.state.aiTimer){ clearTimeout(d2.state.aiTimer); d2.state.aiTimer = null }\n      if (d2.state.aiWatch){ clearTimeout(d2.state.aiWatch); d2.state.aiWatch = null }\n      return\n    }\n    // Turn key includes gameId so it never collides across matches — the old\n    // moveCount-only key could skip the CPU's first move of a fresh game.\n    const key = g.get('gameId') + ':' + g.get('moveCount')\n    if (d2.state.aiKey === key) return\n    d2.state.aiKey = key\n    if (d2.state.aiTimer) clearTimeout(d2.state.aiTimer)\n    if (d2.state.aiWatch) clearTimeout(d2.state.aiWatch)\n    d2.state.aiTimer = setTimeout(() => runCpuMove(d2, key), 500)\n    // Watchdog: if this turn hasn't resolved in 10s (a stuck state, not a slow\n    // compute — that is capped separately), force a recovery move.\n    d2.state.aiWatch = setTimeout(() => runCpuMove(d2, key), 10000)\n  },\n}"}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}}}}}}}},{"_id":"6a5e4efc22084cd56253c291","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaHandPlayer","package":"bigaton-playground","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 CARD = 'bigaton-playground@OnitamaCard'\n\nexport default {\n  css: `\n    .hand{display:flex;flex-direction:column;gap:6px;height:100%;box-sizing:border-box;padding:8px;\n      font:600 12px system-ui,sans-serif;color:#2b2f3a;\n      background:#fff;border:1px solid #dfe2ea;border-radius:12px}\n    .ttl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#7b8191}\n    /* Compartilha a linha do titulo: uma faixa propria roubava altura das\n       cartas e as deixava menores que as do painel da CPU. */\n    /* 20px fixos: mesma altura de cabecalho nos tres paineis, para que as\n       cartas fiquem exatamente do mesmo tamanho em todos eles. */\n    .head{display:flex;align-items:center;justify-content:space-between;gap:8px;\n      height:20px;flex:none}\n    .head .ttl{white-space:nowrap}\n    .armed{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:999px;\n      background:#e8f0ff;color:#1d4ed8;font-size:11px;line-height:14px;font-weight:700;white-space:nowrap}\n    .armed.off{background:#f2f3f6;color:#9aa0ad}\n    .dot{width:7px;height:7px;border-radius:50%;background:#1d4ed8;flex:none}\n    .armed.off .dot{background:#c3c7d1}\n    .cards{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1;min-height:0;padding-top:5px}\n    .slot{cursor:pointer}\n    .slot.locked{cursor:default}\n    .none{display:flex;align-items:center;justify-content:center;color:#9aa0ad;flex:1}\n  `,\n  render(d2){\n    const g = d2.store('game')\n    const cards = g.get('cards') || {}\n    const hand = g.get('playerHand') || []\n    const sel = g.get('selectedCard')\n    const mine = !g.get('winner') && g.get('turn') === 'player'\n    if (!hand.length) return `<div class=\"hand\"><div class=\"ttl\">Your cards</div><div class=\"none\">—</div></div>`\n    const slots = hand.map(id =>\n      `<div class=\"slot ${mine ? '' : 'locked'}\" data-card=\"${id}\" data-key=\"${id}\">` +\n      // active independe de ser sua vez: a carta escolhida continua\n      // visivelmente marcada enquanto a CPU joga.\n      d2.render(CARD, {\n        card: cards[id] || null,\n        active: sel === id,\n        muted: mine && sel !== id,\n        dim: !mine,\n      // REGRA: a key precisa conter TODO estado visual que o filho desenha.\n      // Se so a carta entra na key, uma mudanca de 'dim' (troca de turno) ou\n      // de 'active' reaproveita o host montado e nao chega a tela.\n      }, { key: ['pc', id, sel === id ? 'on' : 'off', mine ? 'my' : 'wait'].join('-') }) +\n      `</div>`\n    ).join('')\n    const selName = ((cards[sel] || {}).name) || '—'\n    return `<div class=\"hand\">\n      <div class=\"head\">\n        <span class=\"ttl\">Your cards</span>\n        <span class=\"armed ${mine ? '' : 'off'}\"><i class=\"dot\"></i>${String(selName).replace(/</g, '&lt;')}</span>\n      </div>\n      <div class=\"cards\">${slots}</div>\n    </div>`\n  },\n  mounted(d2){\n    d2.on('.slot', 'click', (e, el) => {\n      const g = d2.store('game')\n      if (g.get('winner') || g.get('turn') !== 'player') return\n      // Basta escrever no Store: o Board le selectedCard de forma reativa\n      // e recalcula os destinos legais sozinho.\n      g.set('selectedCard', el.dataset.card)\n    })\n  },\n}"}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}}}}}}}},{"_id":"6a5e4efc3546f528a8dd2be3","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaHandCPU","package":"bigaton-playground","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 CARD = 'bigaton-playground@OnitamaCard'\n\nexport default {\n  css: `\n    .hand{position:relative;display:flex;flex-direction:column;gap:6px;height:100%;box-sizing:border-box;padding:8px;\n      font:600 12px system-ui,sans-serif;color:#2b2f3a;\n      background:#fff;border:1px solid #dfe2ea;border-radius:12px}\n    .ttl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#7b8191;\n      height:20px;flex:none;display:flex;align-items:center}\n    .cards{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1;min-height:0}\n    .none{display:flex;align-items:center;justify-content:center;color:#9aa0ad;flex:1}\n    .newg{position:absolute;top:6px;right:6px;z-index:3;padding:5px 10px;border:0;border-radius:8px;\n      background:#3a3f4b;color:#fff;font:700 11px system-ui,sans-serif;cursor:pointer;\n      box-shadow:0 2px 6px rgba(0,0,0,.18)}\n  `,\n  render(d2){\n    const g = d2.store('game')\n    const cards = g.get('cards') || {}\n    const hand = g.get('cpuHand') || []\n    const acting = !g.get('winner') && g.get('turn') === 'cpu'\n    const newg = d2.param('compact', false) ? `<button class=\"newg\">↻ New game</button>` : ''\n    if (!hand.length) return `<div class=\"hand\">${newg}<div class=\"ttl\">CPU cards</div><div class=\"none\">—</div></div>`\n    const slots = hand.map(id =>\n      `<div class=\"slot\" data-key=\"${id}\">` +\n      d2.render(CARD, { card: cards[id] || null, flip: true, dim: !acting },\n        { key: ['kc', id, acting ? 'act' : 'idle'].join('-') }) +\n      `</div>`\n    ).join('')\n    return `<div class=\"hand\">${newg}\n      <div class=\"ttl\">CPU cards (open)</div>\n      <div class=\"cards\">${slots}</div>\n    </div>`\n  },\n  mounted(d2){\n    d2.on('.newg', 'click', () => { if (d2.bus) d2.bus.emit('onitama:newGame') })\n  },\n}"}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}},"compact":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"compact"}}}}}}}}}},{"_id":"6a5e4efc0fa17fc6eb3c7f76","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaNeutralCard","package":"bigaton-playground","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 CARD = 'bigaton-playground@OnitamaCard'\n\nexport default {\n  css: `\n    .neu{display:flex;flex-direction:column;gap:6px;height:100%;box-sizing:border-box;padding:8px;\n      font:600 12px system-ui,sans-serif;color:#2b2f3a;\n      background:#fff;border:1px solid #dfe2ea;border-radius:12px}\n    /* 20px fixos — mesma altura de cabecalho dos paineis de mao */\n    .ttl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#7b8191;\n      height:20px;flex:none;display:flex;align-items:center}\n    /* 148px = mesma largura de uma carta da mao num painel de 320:\n       (320 - 16 de padding - 8 de gap) / 2. Centralizada no painel. */\n    .slot{flex:1;min-height:0;width:100%;max-width:148px;margin:0 auto}\n    .none{display:flex;align-items:center;justify-content:center;color:#9aa0ad;flex:1}\n  `,\n  render(d2){\n    const g = d2.store('game')\n    const cards = g.get('cards') || {}\n    const id = g.get('neutral')\n    if (!id) return `<div class=\"neu\"><div class=\"ttl\">Neutral card</div><div class=\"none\">—</div></div>`\n    const toPlayer = g.get('turn') === 'player'\n    const nextTo = toPlayer ? 'goes to you' : 'goes to CPU'\n    // key com o estado visual completo: a legenda muda com o turno, entao o\n    // turno precisa entrar na key (ver OnitamaHandPlayer).\n    return `<div class=\"neu\">\n      <div class=\"ttl\">Neutral card</div>\n      <div class=\"slot\">${d2.render(CARD, { card: cards[id] || null, caption: nextTo }, { key: ['neutral', id, toPlayer ? 'p' : 'c'].join('-') })}</div>\n    </div>`\n  },\n}"}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}}}}}}}},{"_id":"6a5e4efd657e05476197af70","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaHUD","package":"bigaton-playground","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 MASTERS = {\n  blastoise: { name: 'Blastoise', pokeId: 9 },\n  charizard: { name: 'Charizard', pokeId: 6 },\n  venusaur:  { name: 'Venusaur',  pokeId: 3 },\n}\nconst MICON = id => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/' + id + '.png'\nconst esc = s => String(s == null ? '' : s).replace(/</g, '&lt;')\n\nfunction stripText(g){\n  const winner = g.get('winner'), reason = g.get('winReason'), phase = g.get('phase')\n  if (winner === 'player') return 'You win — ' + (reason === 'stream' ? 'temple taken' : 'master captured')\n  if (winner === 'cpu') return 'CPU wins — ' + (reason === 'stream' ? 'your temple taken' : 'your master captured')\n  if (phase === 'selectMode') return 'Choose a mode'\n  if (phase === 'selectMaster') return 'Choose your Master'\n  const log = g.get('log') || []\n  return log[0] || (g.get('turn') === 'player' ? 'Your turn' : 'CPU thinking…')\n}\n\n// Drawer rendered as a body portal — escapes the shadow root + the morph, and\n// dodges the fixed-under-transform trap. Content is a snapshot at open time.\nfunction openHistory(d2){\n  if (d2.state._drawer) return\n  const g = d2.store('game')\n  const moves = g.get('moveCount') || 0\n  const log = g.get('log') || []\n  const masters = g.get('masters') || { player: null, cpu: null }\n  const used = g.get('used') || { player: false, cpu: false }\n\n  const mRow = side => {\n    const mk = masters[side]; if (!mk) return ''\n    const ok = !used[side]\n    return `<div style=\"display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;background:${side === 'player' ? '#eef4ff' : '#fdeae6'}\">\n      <img src=\"${MICON(MASTERS[mk].pokeId)}\" style=\"width:30px;height:30px;object-fit:contain\"/>\n      <div style=\"flex:1;font:600 12px system-ui\"><div>${MASTERS[mk].name}</div><div style=\"font-size:10px;color:#7b8191\">${side === 'player' ? 'Your Master' : 'CPU Master'}</div></div>\n      <span style=\"font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;background:${ok ? '#dff3e3' : '#e6e8ee'};color:${ok ? '#1e6b33' : '#9aa0ad'}\">${ok ? '⚡ ready' : 'used'}</span></div>`\n  }\n  const mastersHtml = (masters.player || masters.cpu)\n    ? `<div style=\"display:flex;flex-direction:column;gap:6px\">${mRow('player')}${mRow('cpu')}</div>` : ''\n  const logHtml = log.length\n    ? log.map((l, i) => `<div style=\"font-size:12px;padding:4px 6px;border-radius:5px;background:${i === 0 ? '#fff6da' : '#f5f6f9'};color:${i === 0 ? '#7a5a00' : '#4a505e'};font-weight:${i === 0 ? 700 : 400}\">${esc(l)}</div>`).join('')\n    : `<div style=\"color:#9aa0ad;font-size:12px;text-align:center;padding:8px\">No moves yet.</div>`\n\n  const el = document.createElement('div')\n  el.style.cssText = 'position:fixed;inset:0;z-index:9000;display:flex;align-items:flex-end;background:rgba(0,0,0,.4)'\n  el.innerHTML = `<div style=\"width:100%;max-height:80vh;display:flex;flex-direction:column;gap:10px;background:#fff;border-radius:16px 16px 0 0;padding:14px;box-shadow:0 -8px 30px rgba(0,0,0,.25);font-family:system-ui;box-sizing:border-box\">\n    <div style=\"display:flex;align-items:center;justify-content:space-between\"><b style=\"font-size:15px\">History · ${moves} moves</b><button class=\"d2-x\" style=\"border:0;background:#eceef3;color:#3a3f4b;border-radius:8px;padding:7px 13px;font-weight:700;cursor:pointer\">Close</button></div>\n    ${mastersHtml}\n    <div style=\"flex:1;min-height:60px;overflow:auto;display:flex;flex-direction:column;gap:3px\">${logHtml}</div>\n    <button class=\"d2-new\" style=\"border:0;background:#3a3f4b;color:#fff;border-radius:10px;padding:11px 12px;font-weight:700;cursor:pointer\">New match</button>\n  </div>`\n  document.body.appendChild(el)\n  let offEsc = null\n  const close = () => { el.remove(); if (offEsc) offEsc(); d2.state._drawer = null }\n  el.addEventListener('click', e => { if (e.target === el) close() })\n  el.querySelector('.d2-x').addEventListener('click', close)\n  el.querySelector('.d2-new').addEventListener('click', () => { if (d2.bus) d2.bus.emit('onitama:newGame'); close() })\n  offEsc = d2.on(window, 'keydown', e => { if (e.key === 'Escape') close() })\n  d2.onUnmount(close)\n  d2.state._drawer = el\n}\n\nexport default {\n  css: `\n    .hud{display:flex;flex-direction:column;gap:10px;height:100%;box-sizing:border-box;padding:10px;\n      font:500 13px/1.35 system-ui,sans-serif;color:#2b2f3a;\n      background:#fff;border:1px solid #dfe2ea;border-radius:12px}\n    .hud.compact{flex-direction:row;align-items:center;gap:8px;padding:0 10px}\n    .lastmove{display:flex;align-items:baseline;gap:6px;min-width:0}\n    .lm-label{font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:#9aa0ad;flex:none}\n    .lm-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:#3a3f4b;font-weight:700}\n    .cmasters{display:flex;gap:6px;flex:1;align-items:center}\n    .cm{display:flex;align-items:center;gap:5px;padding:3px 6px;border-radius:8px;flex:1;min-width:0}\n    .cm.p{background:#eef4ff} .cm.c{background:#fdeae6}\n    .cm img{width:26px;height:26px;object-fit:contain;flex:none}\n    .cm .cmn{font-size:10px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n    .cm .dot{width:8px;height:8px;border-radius:50%;flex:none;margin-left:auto}\n    .cm .dot.ok{background:#22a35a} .cm .dot.used{background:#c3c7d1}\n    .crow{display:flex;align-items:center;gap:8px}\n    .ttl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#7b8191}\n    .state{padding:10px;border-radius:10px;font-weight:700;text-align:center}\n    .state.player{background:#e4edff;color:#1d4ed8}\n    .state.cpu{background:#fdeae6;color:#b23a22}\n    .state.win{background:#dff3e3;color:#1e6b33}\n    .state.lose{background:#3a3f4b;color:#fff}\n    .state.pick{background:#efe9ff;color:#6d28d9}\n    .masters{display:flex;flex-direction:column;gap:6px}\n    .mrow{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;background:#f5f6f9}\n    .mrow.p{background:#eef4ff}\n    .mrow.c{background:#fdeae6}\n    .msp{width:34px;height:34px;flex:none;object-fit:contain}\n    .mmeta{display:flex;flex-direction:column;line-height:1.2;flex:1;min-width:0}\n    .mmeta b{font-size:12px}\n    .mmeta span{font-size:10px;color:#7b8191}\n    .abtag{font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;flex:none}\n    .abtag.ok{background:#dff3e3;color:#1e6b33}\n    .abtag.used{background:#e6e8ee;color:#9aa0ad}\n    .rules{font-size:11px;color:#7b8191;border-left:3px solid #e1e4ec;padding-left:8px}\n    .log{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:3px;\n      border-top:1px solid #e1e4ec;padding-top:8px}\n    .li{font-size:11px;color:#4a505e;padding:3px 5px;border-radius:4px;background:#f5f6f9}\n    .li.top{background:#fff6da;color:#7a5a00;font-weight:700}\n    .btn{padding:8px 12px;border:0;border-radius:8px;background:#3a3f4b;color:#fff;\n      font:700 12px system-ui,sans-serif;cursor:pointer}\n    .btn.hist{background:#1d4ed8;flex:none;white-space:nowrap}\n    .empty{color:#9aa0ad;font-size:12px;text-align:center;padding:8px}\n  `,\n  render(d2){\n    const g = d2.store('game')\n\n    // Compact strip (mobile): last move + mini masters + History toggle (drawer).\n    if (d2.param('compact', false)){\n      return `<div class=\"hud compact\">\n        <span class=\"lm-label\">Last</span>\n        <span class=\"lm-text\">${esc(stripText(g))}</span>\n        <button class=\"btn hist\">📜 History</button>\n      </div>`\n    }\n\n    const phase = g.get('phase')\n    const turn = g.get('turn')\n    const winner = g.get('winner')\n    const reason = g.get('winReason')\n    const log = g.get('log') || []\n    const moves = g.get('moveCount') || 0\n    const masters = g.get('masters') || { player: null, cpu: null }\n    const used = g.get('used') || { player: false, cpu: false }\n\n    let cls, txt\n    if (winner === 'player'){ cls = 'win'; txt = 'YOU WIN — ' + (reason === 'stream' ? 'temple taken' : 'master captured') }\n    else if (winner === 'cpu'){ cls = 'lose'; txt = 'CPU WINS — ' + (reason === 'stream' ? 'your temple taken' : 'your master captured') }\n    else if (phase === 'selectMode'){ cls = 'pick'; txt = 'Choose a mode →' }\n    else if (phase === 'selectMaster'){ cls = 'pick'; txt = 'Choose your Master →' }\n    else if (turn === 'player'){ cls = 'player'; txt = 'Your turn' }\n    else { cls = 'cpu'; txt = 'CPU thinking…' }\n\n    const mrow = (side, klass) => {\n      const mk = masters[side]\n      if (!mk) return ''\n      const abOk = !used[side]\n      return `<div class=\"mrow ${klass}\">\n        <img class=\"msp\" src=\"${MICON(MASTERS[mk].pokeId)}\" alt=\"\"/>\n        <div class=\"mmeta\"><b>${MASTERS[mk].name}</b><span>${side === 'player' ? 'Your Master' : 'CPU Master'}</span></div>\n        <span class=\"abtag ${abOk ? 'ok' : 'used'}\">${abOk ? '⚡ ready' : 'used'}</span>\n      </div>`\n    }\n    const mastersBlock = (masters.player || masters.cpu)\n      ? `<div class=\"masters\">${mrow('player', 'p')}${mrow('cpu', 'c')}</div>` : ''\n\n    const items = log.map((l, i) =>\n      `<div class=\"li ${i === 0 ? 'top' : ''}\" data-key=\"l${log.length - i}\">${esc(l)}</div>`\n    ).join('')\n\n    return `<div class=\"hud\">\n      <div class=\"state ${cls}\">${txt}</div>\n      ${mastersBlock}\n      <div class=\"rules\">Pick a card, click one of your pieces, then a highlighted target.\n        Use your Master's ability once per match (button on the board).\n        Win by capturing the enemy Master or moving your own Master onto the enemy temple.</div>\n      <div class=\"ttl\">History (${moves} moves)</div>\n      <div class=\"log\">${items || `<div class=\"empty\">No moves yet.</div>`}</div>\n      <button class=\"btn newgame\">New match</button>\n    </div>`\n  },\n  mounted(d2){\n    d2.on('.newgame', 'click', () => { if (d2.bus) d2.bus.emit('onitama:newGame') })\n    d2.on('.hist', 'click', () => openHistory(d2))\n  },\n}"}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}},"compact":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"compact"}}}}}}}}}},{"_id":"6a5e59c8bb68386acdb788e8","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaCardAdmin","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Icone da geracao VIII (icones de menu do Pokemon HOME), nao o sprite padrao.\nconst SPRITE = id => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/' + id + '.png'\n\n// Cartas base do jogo — usadas pelo botao \"Restaurar padrao\".\n// Movimentos em [dx, dy] na perspectiva do jogador de baixo; dy negativo = frente.\nconst DEFAULTS = [\n  { cardId: 'dragon',   name: 'Dragonite', moves: '[[-2,-1],[2,-1],[-1,1],[1,1]]', enabled: true, pokeId: 149, spriteUrl: SPRITE(149) },\n  { cardId: 'goose',    name: \"Farfetch'd\", moves: '[[-1,-1],[-1,0],[1,0],[1,1]]',  enabled: true, pokeId: 83,  spriteUrl: SPRITE(83) },\n  { cardId: 'elephant', name: 'Donphan',   moves: '[[-1,-1],[1,-1],[-1,0],[1,0]]', enabled: true, pokeId: 232, spriteUrl: SPRITE(232) },\n  { cardId: 'mantis',   name: 'Scyther',   moves: '[[-1,-1],[1,-1],[0,1]]',        enabled: true, pokeId: 123, spriteUrl: SPRITE(123) },\n  { cardId: 'tiger',    name: 'Raikou',    moves: '[[0,-2],[0,1]]',                enabled: true, pokeId: 243, spriteUrl: SPRITE(243) },\n]\n\nconst esc = s => String(s == null ? '' : s).replace(/</g, '&lt;').replace(/\"/g, '&quot;')\n\nfunction parseMoves(v){\n  if (Array.isArray(v)) return v\n  try {\n    const p = JSON.parse(v || '[]')\n    return Array.isArray(p) ? p.filter(m => Array.isArray(m) && m.length === 2) : []\n  } catch (e) { return [] }\n}\n\nfunction slug(name){\n  return String(name || '').toLowerCase().normalize('NFD')\n    .replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, '-')\n    .replace(/^-|-$/g, '') || 'card'\n}\n\nfunction titleCase(apiName){\n  return String(apiName || '').split('-')\n    .map(w => w ? w.charAt(0).toUpperCase() + w.slice(1) : w).join('-')\n}\n\nasync function fetchPoke(id){\n  const res = await fetch('https://pokeapi.co/api/v2/pokemon/' + encodeURIComponent(String(id).trim()))\n  if (!res.ok) throw new Error('Pokémon #' + id + ' not found (' + res.status + ')')\n  const d = await res.json()\n  const s = d.sprites || {}\n  // Prefere o icone da gen VIII; cai para o sprite padrao se aquele Pokemon\n  // nao tiver icone de menu (Pokemon fora da linha da gen VIII).\n  const gen8 = ((((s.versions || {})['generation-viii']) || {}).icons || {}).front_default\n  const sprite = gen8 || s.front_default || SPRITE(d.id)\n  return { pokeId: d.id, name: titleCase(d.name), sprite: sprite }\n}\n\nfunction miniGrid(moves){\n  const marks = {}\n  for (const m of moves) marks[(2 + m[0]) + ',' + (2 + m[1])] = true\n  let out = ''\n  for (let r = 0; r < 5; r++){\n    for (let c = 0; c < 5; c++){\n      const cls = (r === 2 && c === 2) ? 'ctr' : (marks[c + ',' + r] ? 'mv' : '')\n      out += `<i class=\"mc ${cls}\"></i>`\n    }\n  }\n  return out\n}\n\n// Le o formulario direto do DOM, para que digitar NAO dispare re-render\n// (assim o cursor nunca salta enquanto se escreve).\nfunction readForm(d2){\n  const n = d2.$('.fname'), e = d2.$('.fen')\n  return { name: n ? n.value : '', enabled: e ? !!e.checked : true }\n}\n\nexport default {\n  css: `\n    .wrap{display:grid;grid-template-columns:minmax(280px,1fr) minmax(300px,1.1fr);gap:14px;\n      height:100%;box-sizing:border-box;padding:14px;background:#fff;border:1px solid #dfe2ea;\n      border-radius:12px;font:500 13px/1.35 system-ui,sans-serif;color:#2b2f3a}\n    .col{display:flex;flex-direction:column;gap:10px;min-height:0}\n    .ttl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#7b8191;\n      height:20px;flex:none;display:flex;align-items:center}\n    .list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px}\n    .row{display:flex;align-items:center;gap:10px;padding:8px;border:2px solid #e6e8ee;\n      border-radius:10px;cursor:pointer;background:#fff}\n    .row:hover{border-color:#c9ccd6}\n    .row.on{border-color:#1d4ed8;background:#eef4ff}\n    .sp{width:46px;height:46px;flex:none;object-fit:contain}\n    .sp.ph{background:#f2f3f6;border-radius:6px}\n    .mini{display:grid;grid-template-columns:repeat(5,7px);grid-template-rows:repeat(5,7px);\n      gap:1px;flex:none}\n    .mc{display:block;border-radius:1px;background:#eceef3}\n    .mc.mv{background:#2f6df6}\n    .mc.ctr{background:#3a3f4b}\n    .meta{display:flex;flex-direction:column;gap:1px;flex:1;min-width:0}\n    .meta b{font-size:13px}\n    .id{font-size:10px;color:#9aa0ad;font-family:ui-monospace,monospace}\n    .pill{padding:2px 8px;border-radius:999px;font-size:10px;font-weight:800;flex:none}\n    .pill.ok{background:#dff3e3;color:#1e6b33}\n    .pill.off{background:#f2f3f6;color:#9aa0ad}\n    .bar{display:flex;gap:8px;flex:none}\n    .btn{padding:8px 12px;border:0;border-radius:8px;background:#3a3f4b;color:#fff;\n      font:700 12px system-ui,sans-serif;cursor:pointer}\n    .btn.pri{background:#1d4ed8}\n    .btn.danger{background:#c2410c}\n    .btn.ghost{background:#eceef3;color:#3a3f4b}\n    .btn[disabled]{opacity:.5;cursor:default}\n    .form{display:flex;flex-direction:column;gap:10px;flex:1;min-height:0;overflow:auto}\n    .lbl{font-size:11px;font-weight:700;color:#7b8191;text-transform:uppercase;letter-spacing:.04em}\n    .in{padding:8px 10px;border:1px solid #c9ccd6;border-radius:8px;font:500 13px system-ui,sans-serif}\n    .in:focus{outline:2px solid #1d4ed8;outline-offset:-1px}\n    .poke{display:flex;align-items:flex-end;gap:8px}\n    .poke .idbox{flex:none;width:110px}\n    .bigsp{width:64px;height:64px;flex:none;object-fit:contain;\n      background:#f6f7f9;border:1px solid #e6e8ee;border-radius:10px}\n    .editor{display:grid;grid-template-columns:repeat(5,1fr);grid-template-rows:repeat(5,1fr);\n      gap:4px;width:200px;height:200px}\n    .cell{border-radius:4px;background:#eceef3;cursor:pointer;border:0;padding:0}\n    .cell:hover{background:#dfe3ec}\n    .cell.mv{background:#2f6df6}\n    .cell.ctr{background:#3a3f4b;cursor:default}\n    .chk{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600}\n    .muted{color:#9aa0ad;font-size:12px}\n    .warn{padding:8px 10px;border-radius:8px;background:#fff6da;color:#7a5a00;\n      font-size:12px;font-weight:600}\n    .err{padding:8px 10px;border-radius:8px;background:#fdeae6;color:#b23a22;\n      font-size:12px;font-weight:700}\n    .empty{display:flex;align-items:center;justify-content:center;flex:1;color:#9aa0ad}\n  `,\n  render(d2){\n    const ds = d2.data('cards')\n    if (!ds) return `<div class=\"wrap\"><div class=\"err\">Param \"cards\" is not bound to any DataSet.</div></div>`\n    if (ds.loading) return `<div class=\"wrap\"><div class=\"empty\">Loading cards…</div></div>`\n\n    const rows = ds.records() || []\n    const draft = d2.state.draft\n    const busy = ds.mutating\n    const fetching = !!d2.state.fetching\n    const enabledCount = rows.filter(r => r.enabled !== false).length\n\n    const list = rows.length\n      ? rows.map(r => {\n          const sp = r.spriteUrl || (r.pokeId ? SPRITE(r.pokeId) : '')\n          const icon = sp ? `<img class=\"sp\" src=\"${esc(sp)}\" alt=\"\" loading=\"lazy\"/>` : `<span class=\"sp ph\"></span>`\n          return `<div class=\"row ${draft && draft._id === r._id ? 'on' : ''}\"\n            data-id=\"${esc(r._id)}\" data-key=\"${esc(r._id)}\">\n          ${icon}\n          <div class=\"mini\">${miniGrid(parseMoves(r.moves))}</div>\n          <div class=\"meta\"><b>${esc(r.name)}</b><span class=\"id\">${esc(r.cardId)}${r.pokeId ? ' · #' + esc(r.pokeId) : ''}</span></div>\n          <span class=\"pill ${r.enabled !== false ? 'ok' : 'off'}\">${r.enabled !== false ? 'in deck' : 'out'}</span>\n        </div>`\n        }).join('')\n      : `<div class=\"empty\">No cards yet.</div>`\n\n    let editor\n    if (!draft){\n      editor = `<div class=\"empty\">Select a card or create a new one.</div>`\n    } else {\n      const marks = {}\n      for (const m of (draft.moves || [])) marks[(2 + m[0]) + ',' + (2 + m[1])] = true\n      let cells = ''\n      for (let r = 0; r < 5; r++){\n        for (let c = 0; c < 5; c++){\n          const isC = (r === 2 && c === 2)\n          cells += `<button class=\"cell ${isC ? 'ctr' : (marks[c + ',' + r] ? 'mv' : '')}\"\n            data-r=\"${r}\" data-c=\"${c}\" ${isC ? 'disabled' : ''}></button>`\n        }\n      }\n      const spv = draft.sprite || (draft.pokeId ? SPRITE(draft.pokeId) : '')\n      const preview = spv ? `<img class=\"bigsp\" src=\"${esc(spv)}\" alt=\"\"/>` : `<span class=\"bigsp\"></span>`\n      editor = `<div class=\"form\">\n        <div><div class=\"lbl\">Pokémon (Pokédex ID)</div>\n          <div class=\"muted\" style=\"margin:2px 0 6px\">Enter the Pokémon number and fetch — name and icon update from the PokeAPI.</div>\n          <div class=\"poke\">\n            ${preview}\n            <div class=\"idbox\"><div class=\"lbl\">ID</div>\n              <input class=\"in fpoke\" type=\"number\" min=\"1\" value=\"${esc(draft.pokeId || '')}\" placeholder=\"25\" style=\"width:100%;box-sizing:border-box\"/></div>\n            <button class=\"btn pri fetchp\" ${fetching ? 'disabled' : ''}>${fetching ? 'Fetching…' : 'Fetch'}</button>\n          </div></div>\n        <div><div class=\"lbl\">Name</div>\n          <input class=\"in fname\" value=\"${esc(draft.name)}\" placeholder=\"e.g. Pikachu\" style=\"width:100%;box-sizing:border-box\"/></div>\n        <div><div class=\"lbl\">Moves — click to toggle</div>\n          <div class=\"muted\" style=\"margin:2px 0 6px\">The dark square is the piece. Bottom-player perspective.</div>\n          <div class=\"editor\">${cells}</div></div>\n        <label class=\"chk\"><input type=\"checkbox\" class=\"fen\" ${draft.enabled !== false ? 'checked' : ''}/> Include in the draw deck</label>\n        ${d2.state.err ? `<div class=\"err\">${esc(d2.state.err)}</div>` : ''}\n        <div class=\"bar\">\n          <button class=\"btn pri save\" ${busy ? 'disabled' : ''}>${draft._id ? 'Save' : 'Create'}</button>\n          ${draft._id ? `<button class=\"btn danger del\" ${busy ? 'disabled' : ''}>Delete</button>` : ''}\n          <button class=\"btn ghost cancel\">Cancel</button>\n        </div>\n      </div>`\n    }\n\n    return `<div class=\"wrap\">\n      <div class=\"col\">\n        <div class=\"ttl\">Cards (${rows.length}) — ${enabledCount} in deck</div>\n        ${enabledCount < 5 ? `<div class=\"warn\">The game needs at least 5 cards in the deck to draw. ${5 - enabledCount} missing.</div>` : ''}\n        <div class=\"list\">${list}</div>\n        <div class=\"bar\">\n          <button class=\"btn pri novo\" ${busy ? 'disabled' : ''}>+ New card</button>\n          <button class=\"btn ghost seed\" ${busy ? 'disabled' : ''}>Restore defaults</button>\n        </div>\n      </div>\n      <div class=\"col\">\n        <div class=\"ttl\">Editor</div>\n        ${editor}\n      </div>\n    </div>`\n  },\n  mounted(d2){\n    const ds = () => d2.data('cards')\n\n    d2.on('.row', 'click', (e, el) => {\n      const rec = (ds().records() || []).find(r => String(r._id) === el.dataset.id)\n      if (!rec) return\n      d2.state.err = ''\n      d2.state.draft = {\n        _id: rec._id, cardId: rec.cardId, name: rec.name || '',\n        moves: parseMoves(rec.moves), enabled: rec.enabled !== false,\n        pokeId: rec.pokeId || '', sprite: rec.spriteUrl || (rec.pokeId ? SPRITE(rec.pokeId) : ''),\n      }\n    })\n\n    d2.on('.novo', 'click', () => {\n      d2.state.err = ''\n      d2.state.draft = { _id: null, cardId: '', name: '', moves: [], enabled: true, pokeId: '', sprite: '' }\n    })\n\n    d2.on('.cancel', 'click', () => { d2.state.err = ''; d2.state.draft = null })\n\n    // Busca na PokeAPI: le o ID do DOM, atualiza nome + icone + pokeId no draft.\n    d2.on('.fetchp', 'click', async () => {\n      const inp = d2.$('.fpoke')\n      const id = inp ? inp.value.trim() : ''\n      if (!id) { d2.state.err = 'Enter the Pokémon number.'; return }\n      // preserva nome/enabled que ja estao no form antes de re-renderizar\n      d2.state.draft = Object.assign({}, d2.state.draft, readForm(d2))\n      d2.state.err = ''\n      d2.state.fetching = true\n      try {\n        const poke = await fetchPoke(id)\n        d2.state.draft = Object.assign({}, d2.state.draft, {\n          pokeId: poke.pokeId, name: poke.name, sprite: poke.sprite,\n        })\n      } catch (err) {\n        d2.state.err = (err && err.message) ? err.message : String(err)\n      } finally {\n        d2.state.fetching = false\n      }\n    })\n\n    // Captura o que ja foi digitado ANTES de re-renderizar por causa do clique.\n    d2.on('.cell', 'click', (e, el) => {\n      const dx = Number(el.dataset.c) - 2, dy = Number(el.dataset.r) - 2\n      if (dx === 0 && dy === 0) return\n      const cur = (d2.state.draft && d2.state.draft.moves) || []\n      const has = cur.some(m => m[0] === dx && m[1] === dy)\n      const moves = has ? cur.filter(m => !(m[0] === dx && m[1] === dy)) : cur.concat([[dx, dy]])\n      d2.state.draft = Object.assign({}, d2.state.draft, readForm(d2), { moves: moves })\n    })\n\n    d2.on('.save', 'click', async () => {\n      const draft = Object.assign({}, d2.state.draft, readForm(d2))\n      const rows = ds().records() || []\n      if (!draft.name.trim()) { d2.state.err = 'Give the card a name.'; return }\n      if (!draft.moves.length) { d2.state.err = 'Mark at least one move.'; return }\n\n      let cardId = draft.cardId || slug(draft.name)\n      const taken = rows.some(r => r.cardId === cardId && String(r._id) !== String(draft._id))\n      if (taken) cardId = cardId + '-' + Date.now().toString(36).slice(-4)\n\n      const payload = {\n        cardId: cardId, name: draft.name.trim(),\n        moves: JSON.stringify(draft.moves), enabled: !!draft.enabled,\n        pokeId: draft.pokeId ? Number(draft.pokeId) : null,\n        spriteUrl: draft.sprite || (draft.pokeId ? SPRITE(draft.pokeId) : ''),\n      }\n      d2.state.err = ''\n      try {\n        if (draft._id) await ds().update(Object.assign({ _id: draft._id }, payload))\n        else await ds().create(payload)\n        d2.state.draft = null\n      } catch (err) {\n        d2.state.err = 'Save failed: ' + (err && err.message ? err.message : err)\n      }\n    })\n\n    d2.on('.del', 'click', async () => {\n      const id = d2.state.draft && d2.state.draft._id\n      if (!id) return\n      if (!(await d2.dialog.confirm('Delete this card?'))) return\n      try { await ds().remove(id); d2.state.draft = null }\n      catch (err) { d2.state.err = 'Delete failed: ' + (err && err.message ? err.message : err) }\n    })\n\n    d2.on('.seed', 'click', async () => {\n      const have = {}\n      for (const r of (ds().records() || [])) have[r.cardId] = true\n      const faltando = DEFAULTS.filter(c => !have[c.cardId])\n      if (!faltando.length) { d2.state.err = 'The 5 default cards already exist.'; return }\n      d2.state.err = ''\n      try { for (const c of faltando) await ds().create(c) }\n      catch (err) { d2.state.err = 'Restore failed: ' + (err && err.message ? err.message : err) }\n    })\n  },\n}"}},"cards":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cards"}}}}}}}}}},{"_id":"6a5e5a166ce7106fbec3483c","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"OnitamaCatalogLoader","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Ponte entre a tabela (fonte duravel) e o Store (copia de trabalho do jogo).\n// Se a tabela estiver vazia ou desligada, o catalogo embutido no Store continua\n// valendo — o jogo nunca fica sem cartas.\nfunction trySync(d2){\n  if (d2.state.done) return\n  const ds = d2.data('catalog')\n  // ds truthy e carregando: espera. ds null (sem tabela ligada): resolve com\n  // o catalogo embutido. Em ambos os casos sinalizamos \"resolvido\" no fim.\n  if (ds && ds.loading) return\n  d2.state.done = true\n\n  const g = d2.store('game')\n\n  if (ds){\n    const rows = ds.records() || []\n    if (rows.length){\n      const cards = {}\n      const deck = []\n      for (const r of rows){\n        const id = r.cardId || String(r._id)\n        let moves = []\n        try {\n          moves = typeof r.moves === 'string' ? JSON.parse(r.moves) : (r.moves || [])\n        } catch (e) { moves = [] }\n        if (!Array.isArray(moves) || !moves.length) continue\n        // spriteUrl vem da tabela; se faltar mas houver pokeId, deriva a URL.\n        // Icone da geracao VIII (icones de menu), nao o sprite padrao.\n        const sprite = r.spriteUrl ||\n          (r.pokeId ? 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/' + r.pokeId + '.png' : '')\n        cards[id] = { id: id, name: r.name || id, moves: moves, sprite: sprite, pokeId: r.pokeId || null }\n        if (r.enabled !== false) deck.push(id)\n      }\n      if (Object.keys(cards).length){\n        g.set('cards', cards)\n        g.set('deck', deck)\n      }\n    }\n  }\n\n  // Nao sinaliza nada: o Board ja abre no seletor de modo (phase inicial) e\n  // deala com o deck atual do Store. A tabela e apenas um upgrade em 2o plano.\n}\n\nexport default {\n  css: `.ld{display:none}`,\n  render(d2){\n    // Componente invisivel — so faz a ponte tabela -> Store.\n    // A leitura do dataset abaixo NAO e decorativa: e ela que inscreve este\n    // componente no dataset. Sem ela nao ha re-render quando a tabela responde,\n    // o updated() nunca dispara de novo e a sincronizacao nunca acontece.\n    const ds = d2.data('catalog')\n    const n = (ds && !ds.loading) ? (ds.records() || []).length : -1\n    return `<i class=\"ld\" data-n=\"${n}\"></i>`\n  },\n  mounted(d2){ trySync(d2) },\n  updated(d2){ trySync(d2) },\n}"}},"catalog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"catalog"}}}},"game":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"game"}}}}}}}}}},{"_id":"6a5fb60e0d3e4ae943282749","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"supertext-fx","package":"bigaton-playground","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 NL = String.fromCharCode(10);\n\nconst RAW = [\n  ['Liquid Chrome', \"'Anton', sans-serif\", ['#fff','#ccc','#999','#555','#000','#555','#fff'], [10,0.5,1,0]],\n  ['Gothic Blood', \"'Eater', cursive\", ['#2a0000','#500000','#800000','#f00','#300'], [0.5,2,1.5,2]],\n  ['Cyber Punk', \"'Orbitron', sans-serif\", ['#f0c','#339','#0cf','#fff','#f0c'], [3.5,1.5,1.3,1]],\n  ['Pure Gold', \"'Abril Fatface', cursive\", ['#fff7cc','#ffeb3b','#ffc107','#ff9800','#4a2c00'], [20,0.3,1.2,0]],\n  ['Rose Gold', \"'Playfair Display', serif\", ['#ffebee','#ffcdd2','#e57373','#d32f2f','#880e4f'], [11,0.5,1.1,0]],\n  ['Obsidian', \"'Cinzel', serif\", ['#111','#222','#333','#000','#101'], [6,1,1.8,0]],\n  ['Antique Bronze', \"'Rye', serif\", ['#d7ccc8','#a1887f','#795548','#3e2723'], [30.5,25,1,0]],\n  ['Brushed Steel', \"'Anton', sans-serif\", ['#eceff1','#b0bec5','#607d8b','#455a64'], [11,0.02,1,0]],\n  ['Gunmetal', \"'Black Ops One', cursive\", ['#cfd8dc','#78909c','#37474f','#263238'], [5,1.2,1.4,1]],\n  ['Titanium', \"'Anton', cursive\", ['#e8eaf6','#9fa8da','#5c6bc0','#303f9f'], [8,0.9,1.2,0]],\n  ['Mercury', \"'Comfortaa', cursive\", ['#fff','#e0e0e0','#bdbdbd','#757575'], [3,0.1,1.5,1]],\n  ['Rusted Metal', \"'Creepster', cursive\", ['#ffcc80','#e65100','#3e2723','#212121'], [100,2.25,0.9,2]],\n  ['Brass', \"'Carter One', cursive\", ['#fff9c4','#fbc02d','#f57f17'], [8,0.4,1.1,0]],\n  ['Platinum', \"'DM Serif Display', serif\", ['#fff','#f5f5f5','#e0e0e0','#bdbdbd'], [5,0.2,1.4,0]],\n  ['White Gold', \"'Lora', serif\", ['#fffde7','#fff59d','#cfcfcf','#fff9c4'], [8.5,0.3,1.2,0]],\n  ['Matrix Code', \"'VT323', monospace\", ['#000','#030','#0f0','#cfc'], [10,4,2,2]],\n  ['Vapor Wave', \"'Monoton', cursive\", ['#ff71ce','#01cdfe','#05ffa1','#b967ff','#fffb96'], [3,1,1,0]],\n  ['Deep Ocean', \"'Nosifer', cursive\", ['#e0f7fa','#26c6da','#006064','#003333'], [6,1.2,1.8,0]],\n  ['Heavy Metal', \"'Metal Mania', cursive\", ['#444','#666','#222','#000'], [3,3,0.9,2]],\n  ['Candy Shop', \"'Bangers', cursive\", ['#fff','#f8bbd0','#f48fb1','#e91e63'], [2.5,0.5,1.1,1]],\n  ['System Glitch', \"'Rubik Glitch', cursive\", ['#0f0','#000','#f0f','#fff'], [12,5,2,2]],\n  ['Royal Velvet', \"'UnifrakturMaguntia', cursive\", ['#f3e5f5','#8e24aa','#4a148c','#21033a'], [5,0.8,1.5,0]],\n  ['Ice Cold', \"'Righteous', cursive\", ['#fff','#b2dfdb','#4db6ac','#26a69a'], [8,1,1.5,1]],\n  ['Toxic Sludge', \"'Frijole', cursive\", ['#111','#1a0b2e','#0f0','#cf0','#030'], [3,2.5,0.8,2]],\n  ['8-Bit Glitch', \"'Press Start 2P', cursive\", ['#0f0','#000','#0f0','#fff'], [15,4,2,2]],\n  ['Graffiti', \"'Permanent Marker', cursive\", ['#fff','#ff0','#f0f','#0ff','#000'], [2.2,1.8,1.1,2]],\n  ['Magma Flow', \"'Rubik Glitch', cursive\", ['#300','#f00','#fa0','#ffeb3b'], [3.5,1.5,1.2,0]],\n  ['Holographic', \"'Wallpoet', cursive\", ['#fff','#f0f','#0ff','#ff0','#fff'], [8,0.6,1.6,0]],\n  ['Midnight Oil', \"'Alfa Slab One', cursive\", ['#000','#101010','#202020','#050505'], [2.5,1,2,2]],\n  ['Pearl', \"'Comfortaa', cursive\", ['#fff','#fce4ec','#e1f5fe','#f3e5f5','#fff'], [3,0.4,1.3,1]],\n  ['Radioactive', \"'Rubik Mono One', sans-serif\", ['#000','#1b5e20','#64dd17','#ccff90'], [6,2,1.2,2]],\n  ['Bubblegum', \"'Pacifico', cursive\", ['#fff','#81d4fa','#f48fb1','#fff'], [2,0.5,1,1]],\n  ['Noir', \"'Special Elite', cursive\", ['#000','#333','#666','#999','#ccc'], [8,3,1.5,2]],\n  ['Alien Skin', \"'Creepster', cursive\", ['#000','#1a237e','#311b92','#00e676'], [10,4,1.2,2]],\n  ['Sunset Blvd', \"'Lobster', cursive\", ['#210e37','#6a1b9a','#ff6f00','#ffeb3b'], [3,0.8,1.2,0]],\n  ['Jungle Camo', \"'Black Ops One', cursive\", ['#1b5e20','#33691e','#558b2f','#827717'], [5,2.5,1,2]],\n  ['Copper Patina', \"'Rye', serif\", ['#3e2723','#d84315','#009688','#80cbc4'], [8,1.2,1.1,1]],\n  ['Amethyst', \"'Cinzel', serif\", ['#311b92','#673ab7','#d1c4e9','#fff'], [8.5,0.7,1.4,0]],\n  ['Blood Moon', \"'Nosifer', cursive\", ['#000','#300','#b71c1c','#fff'], [5,1.5,1.8,0]],\n  ['Frozen Berry', \"'Shadows Into Light', cursive\", ['#311b92','#0d47a1','#00bcd4','#e1f5fe'], [3.5,1,1.3,1]]\n];\n\nconst PRESETS = RAW.map(function (r) {\n  return { name: r[0], font: r[1], colors: r[2], physics: { scale: r[3][0], complexity: r[3][1], contrast: r[3][2], flow: r[3][3] } };\n});\n\nconst GOOGLE_ID = 'supertext-google-fonts';\nconst GOOGLE_HREF = 'https://fonts.googleapis.com/css2?family=Abril+Fatface&family=Alfa+Slab+One&family=Anton&family=Bangers&family=Black+Ops+One&family=Carter+One&family=Cinzel:wght@900&family=Comfortaa:wght@700&family=Creepster&family=DM+Serif+Display&family=Eater&family=Frijole&family=Lobster&family=Lora:wght@700&family=Metal+Mania&family=Monoton&family=Nosifer&family=Orbitron:wght@900&family=Pacifico&family=Permanent+Marker&family=Playfair+Display:wght@900&family=Press+Start+2P&family=Righteous&family=Rubik+Glitch&family=Rubik+Mono+One&family=Rye&family=Shadows+Into+Light&family=Special+Elite&family=UnifrakturMaguntia&family=VT323&family=Wallpoet&display=swap';\n\nfunction ensureGoogleFonts() {\n  if (typeof document === 'undefined' || document.getElementById(GOOGLE_ID)) return;\n  const l = document.createElement('link');\n  l.id = GOOGLE_ID; l.rel = 'stylesheet'; l.href = GOOGLE_HREF;\n  document.head.appendChild(l);\n}\n\nconst CUSTOM_FONTS = {};\nfunction ensureCustomFont(family, url) {\n  if (!family || !url || typeof document === 'undefined') return;\n  const key = family + '|' + url;\n  if (CUSTOM_FONTS[key]) return;\n  CUSTOM_FONTS[key] = true;\n  try {\n    const clean = family.split(\"'\").join('').split('\"').join('').split(',')[0].trim();\n    const ff = new FontFace(clean, 'url(' + url + ')');\n    ff.load().then(function (f) { document.fonts.add(f); }).catch(function () {});\n  } catch (e) {}\n}\n\nconst VERT = 'attribute vec2 a_pos; varying vec2 v_uv; void main(){ v_uv = a_pos * 0.5 + 0.5; gl_Position = vec4(a_pos, 0.0, 1.0); }';\n\nconst FRAG = [\n  'precision highp float;',\n  'uniform float u_time; uniform vec2 u_mouse; uniform vec2 u_resolution;',\n  'uniform float u_scale; uniform float u_complexity; uniform float u_contrast;',\n  'uniform float u_flow_type; uniform float u_hue_shift;',\n  'uniform float u_mouse_strength; uniform float u_mouse_radius; uniform float u_use_mask;',\n  'uniform float u_text_warp;',\n  'uniform sampler2D u_gradient; uniform sampler2D u_mask;',\n  'varying vec2 v_uv;',\n  'mat2 rot(float a){ float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); }',\n  'vec3 hueShift(vec3 color, float hue){ const vec3 k = vec3(0.57735, 0.57735, 0.57735); float cosAngle = cos(hue); return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle)); }',\n  'float wavePattern(vec2 p){ float h = 0.0; float amp = 0.5; float freq = 1.0; mat2 r = rot(u_complexity * 0.5); vec2 d = vec2(1.0, 0.5); for(int i = 0; i < 4; i++){ h += sin(dot(p, d) * freq + u_time) * amp; d = r * d; p *= 1.4; amp *= 0.6; } return h; }',\n  'float ripplePattern(vec2 p){ float h = 0.0; vec2 p2 = p; for(int i = 1; i < 5; i++){ float fi = float(i); vec2 center = vec2(sin(u_time * 0.4 * fi), cos(u_time * 0.25 * fi)) * 3.0; float dist = length(p2 - center); h += sin(dist * (5.0 + u_complexity) - u_time * 2.5) * (0.4 / fi); p2 = rot(0.8) * p2; } return h; }',\n  'float chaosPattern(vec2 p){ float h = 0.0; for(float i = 1.0; i < 4.0; i++){ p.x += sin(p.y * u_complexity + u_time * 0.5); p.y += cos(p.x * u_complexity + u_time * 0.5); h += sin(p.x * i + p.y * i); } return h * 0.4; }',\n  'float getLiquidHeight(vec2 p){ if(u_flow_type < 0.5) return wavePattern(p); else if(u_flow_type < 1.5) return ripplePattern(p); else return chaosPattern(p); }',\n  'void main(){',\n  '  vec2 uv = v_uv;',\n  '  float aspect = u_resolution.x / u_resolution.y;',\n  '  vec2 p = (uv * 2.0 - 1.0); p.x *= aspect;',\n  '  vec2 m = (u_mouse * 2.0 - 1.0); m.x *= aspect;',\n  '  float dist = length(p - m);',\n  '  float radius = max(0.001, 0.5 * u_mouse_radius);',\n  '  float mouse_force = smoothstep(radius, 0.0, dist) * 0.2 * u_mouse_strength;',\n  '  vec2 p0 = p;',\n  '  p -= (p - m) * mouse_force;',\n  '  float h = getLiquidHeight(p * u_scale);',\n  '  vec2 eps = vec2(0.01, 0.0);',\n  '  float h_x = getLiquidHeight((p + eps.xy) * u_scale) - h;',\n  '  float h_y = getLiquidHeight((p + eps.yx) * u_scale) - h;',\n  '  vec3 normal = normalize(vec3(-h_x * 6.0, -h_y * 6.0, 0.5));',\n  '  vec3 lightDir = normalize(vec3(-0.5, 1.0, 1.0));',\n  '  vec3 viewDir = vec3(0.0, 0.0, 1.0);',\n  '  vec3 halfDir = normalize(lightDir + viewDir);',\n  '  float spec = pow(max(dot(normal, halfDir), 0.0), 50.0 * u_contrast);',\n  '  float reflection_angle = dot(normal.xy, vec2(1.0)) * 0.5 + 0.5;',\n  '  float texture_coord = reflection_angle + h * 0.05;',\n  '  vec3 col = texture2D(u_gradient, vec2(texture_coord, 0.5)).rgb;',\n  '  if(u_hue_shift > 0.0){ col = hueShift(col, u_hue_shift); }',\n  '  col += vec3(spec);',\n  '  col *= smoothstep(-0.3, 0.7, h * 0.5 + 0.5);',\n  '  float a = 1.0;',\n  '  if(u_use_mask > 0.5){',\n  '    // Same displacement the metal gets, scaled — at warp 0 this round-trips back to v_uv exactly.',\n  '    vec2 pt = p0 - (p0 - m) * mouse_force * u_text_warp;',\n  '    vec2 uvt = vec2(pt.x / aspect, pt.y) * 0.5 + 0.5;',\n  '    a = texture2D(u_mask, uvt).a;',\n  '  }',\n  '  gl_FragColor = vec4(col, a);',\n  '}'\n].join(NL);\n\nfunction compileShader(gl, type, src) {\n  const sh = gl.createShader(type);\n  gl.shaderSource(sh, src);\n  gl.compileShader(sh);\n  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {\n    const log = gl.getShaderInfoLog(sh);\n    gl.deleteShader(sh);\n    throw new Error('shader: ' + log);\n  }\n  return sh;\n}\n\nfunction buildProgram(gl) {\n  const vs = compileShader(gl, gl.VERTEX_SHADER, VERT);\n  const fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAG);\n  const pr = gl.createProgram();\n  gl.attachShader(pr, vs); gl.attachShader(pr, fs); gl.linkProgram(pr);\n  if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) throw new Error('link: ' + gl.getProgramInfoLog(pr));\n  gl.deleteShader(vs); gl.deleteShader(fs);\n  return pr;\n}\n\nfunction makeTexture(gl) {\n  const t = gl.createTexture();\n  gl.bindTexture(gl.TEXTURE_2D, t);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n  return t;\n}\n\nfunction isBreak(ch) {\n  return ch === ',' || ch === ';' || ch === ' ' || ch === NL\n    || ch === String.fromCharCode(9) || ch === String.fromCharCode(13);\n}\n\nfunction parseStops(v) {\n  if (Array.isArray(v)) return v.length ? v.slice() : null;\n  const raw = String(v == null ? '' : v);\n  const out = [];\n  let tok = '';\n  for (let i = 0; i < raw.length; i++) {\n    const ch = raw.charAt(i);\n    if (isBreak(ch)) { if (tok) { out.push(tok); tok = ''; } }\n    else tok += ch;\n  }\n  if (tok) out.push(tok);\n  return out.length ? out : null;\n}\n\nfunction gradientCanvas(colors) {\n  const size = 512;\n  const c = document.createElement('canvas');\n  c.width = size; c.height = 1;\n  const ctx = c.getContext('2d');\n  const g = ctx.createLinearGradient(0, 0, size, 0);\n  const list = (colors && colors.length) ? colors : ['#ffffff', '#000000'];\n  if (list.length === 1) { try { g.addColorStop(0, list[0]); g.addColorStop(1, list[0]); } catch (e) {} }\n  else list.forEach(function (col, i) { try { g.addColorStop(i / (list.length - 1), col); } catch (e) {} });\n  ctx.fillStyle = g;\n  ctx.fillRect(0, 0, size, 1);\n  return c;\n}\n\nexport default {\n  css: `\n    .stx { position: relative; width: 100%; height: 100%; overflow: hidden; isolation: isolate; }\n    .stx-cv { display: block; width: 100%; height: 100%; }\n    .stx-plate { position: absolute; inset: 0; display: none; align-items: center; justify-content: center;\n                 pointer-events: none; }\n    .stx.is-blend .stx-plate { display: flex; }\n    .stx-txt { margin: 0; width: 100%; white-space: pre-wrap; line-height: 0.9; }\n    .stx-dbg { position: absolute; left: 8px; top: 8px; z-index: 5; display: none; pointer-events: none;\n               font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: #7CFFB2;\n               background: rgba(0,0,0,.66); padding: 6px 8px; border-radius: 6px; white-space: pre; }\n    .stx.is-dbg .stx-dbg { display: block; }\n    .stx.is-click { cursor: pointer; }\n  `,\n\n  render() {\n    return `<div class=\"stx\" data-d2-static>\n      <canvas class=\"stx-cv\"></canvas>\n      <div class=\"stx-plate\"><div class=\"stx-txt\"></div></div>\n      <div class=\"stx-dbg\"></div>\n    </div>`;\n  },\n\n  mounted(d2, root) {\n    const host = d2.$('.stx');\n    const cv = d2.$('.stx-cv');\n    const plate = d2.$('.stx-plate');\n    const txtEl = d2.$('.stx-txt');\n    const dbgEl = d2.$('.stx-dbg');\n\n    if (!host || !cv) return;\n\n    const fail = function (msg) {\n      if (dbgEl) dbgEl.textContent = msg;\n      host.classList.add('is-dbg');\n    };\n\n    // The instance IS a Block, so its native params live under params.super and its\n    // typography lands as CSS on the outer host element (outside our shadow root).\n    function outerHost() {\n      try {\n        const node = root || d2.root;\n        const rn = node && node.getRootNode ? node.getRootNode() : null;\n        return rn && rn.host ? rn.host : null;\n      } catch (e) { return null; }\n    }\n\n    function cdString(v) {\n      if (v === null || v === undefined) return '';\n      if (typeof v === 'string') return v;\n      if (typeof v === 'number') return String(v);\n      if (typeof v === 'object') {\n        if (typeof v.content === 'string') return v.content;\n        if (v.params && typeof v.params.content === 'string') return v.params.content;\n      }\n      return '';\n    }\n\n    // Block `text` may arrive at the params root, nested in params.super, or only as\n    // light-DOM text on the host (rendered nowhere, since the shadow root has no slot).\n    function blockText() {\n      const p = d2.params || {};\n      let t = cdString(p.text);\n      if (!t && p.super) t = cdString(p.super.text);\n      if (!t) {\n        const oh = outerHost();\n        if (oh && typeof oh.textContent === 'string') t = oh.textContent.trim();\n      }\n      return t;\n    }\n\n    let gl = null;\n    try {\n      gl = cv.getContext('webgl', { alpha: true, premultipliedAlpha: false, antialias: true })\n        || cv.getContext('experimental-webgl', { alpha: true, premultipliedAlpha: false, antialias: true });\n    } catch (e) { gl = null; }\n\n    if (!gl) { fail('WebGL not available in this context.'); return; }\n\n    let program;\n    try { program = buildProgram(gl); }\n    catch (e) { fail(String(e && e.message ? e.message : e)); return; }\n\n    gl.useProgram(program);\n    const buf = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(program, 'a_pos');\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n\n    const U = {};\n    ['u_time', 'u_mouse', 'u_resolution', 'u_scale', 'u_complexity', 'u_contrast', 'u_flow_type',\n     'u_hue_shift', 'u_mouse_strength', 'u_mouse_radius', 'u_use_mask', 'u_text_warp', 'u_gradient', 'u_mask'\n    ].forEach(function (n) { U[n] = gl.getUniformLocation(program, n); });\n\n    const gradTex = makeTexture(gl);\n    const maskTex = makeTexture(gl);\n    gl.uniform1i(U.u_gradient, 0);\n    gl.uniform1i(U.u_mask, 1);\n    gl.enable(gl.BLEND);\n    gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n\n    const maskCv = document.createElement('canvas');\n    const maskCtx = maskCv.getContext('2d');\n\n    d2.onUnmount(function () {\n      try {\n        gl.deleteTexture(gradTex); gl.deleteTexture(maskTex);\n        gl.deleteBuffer(buf); gl.deleteProgram(program);\n        const ext = gl.getExtension('WEBGL_lose_context');\n        if (ext) ext.loseContext();\n      } catch (e) {}\n    });\n\n    let clickPreset = null;\n    let clickPresetBase = null;\n    let fontsReady = false;\n    let maskSig = '';\n    let gradSig = '';\n    let fittedSize = 0;\n\n    if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {\n      document.fonts.ready.then(function () { fontsReady = true; maskSig = ''; });\n    }\n\n    function readConfig() {\n      const p = d2.params || {};\n      const liveKey = d2.param('liveKey', '');\n      const live = liveKey ? (d2.shared.get(liveKey, {}, { global: true }) || {}) : {};\n\n      const pick = function (k) {\n        let v = live[k];\n        if (v === undefined || v === null || v === '') v = p[k];\n        if (v === undefined || v === null || v === '') return undefined;\n        return v;\n      };\n      // Params can arrive raw OR wrapped as a CD (String(content:'mask')) depending on how\n      // they were written — unwrap before coercing, or String(cd) yields '[object Object]'.\n      const flat = function (v) {\n        if (v !== null && typeof v === 'object') return cdString(v);\n        return v;\n      };\n      const num = function (k, dflt) {\n        const n = Number(flat(pick(k)));\n        return Number.isFinite(n) ? n : dflt;\n      };\n      const bool = function (k, dflt) {\n        let v = live[k] !== undefined ? live[k] : p[k];\n        if (v !== null && typeof v === 'object') v = cdString(v);\n        if (v === undefined || v === null || v === '') return dflt;\n        return v === true || v === 'true' || v === 1 || v === '1';\n      };\n      const str = function (k, dflt) {\n        const v = pick(k);\n        if (v === undefined) return dflt;\n        const s = String(flat(v));\n        return s === '' ? dflt : s;\n      };\n\n      const basePreset = pick('fxPreset');\n      if (clickPreset && basePreset !== clickPresetBase) clickPreset = null;\n      const presetName = clickPreset || basePreset || 'Obsidian';\n      let pre = PRESETS.filter(function (x) { return x.name.toLowerCase() === String(presetName).toLowerCase(); })[0];\n      if (!pre) pre = PRESETS[5];\n\n      const liveContent = (live.content === undefined || live.content === null || live.content === '')\n        ? '' : String(live.content);\n      const blockContent = blockText();\n      const paramContent = String(cdString(p.content) || (typeof p.content === 'string' ? p.content : ''));\n      const finalContent = liveContent || blockContent || paramContent || 'SUPERTEXT';\n      const contentSrc = liveContent ? 'lab (live)'\n        : blockContent ? 'Block text'\n        : paramContent ? 'content param' : 'default';\n\n      return {\n        preset: pre,\n        content: finalContent,\n        contentSrc: contentSrc,\n        colors: parseStops(flat(pick('colorStops'))) || pre.colors,\n        scale: num('fxScale', pre.physics.scale),\n        complexity: num('fxComplexity', pre.physics.complexity),\n        contrast: num('fxContrast', pre.physics.contrast),\n        hue: num('fxHue', 0),\n        speed: num('fxSpeed', 0.5),\n        flow: (function () {\n          // Accepts the friendly dropdown labels AND the raw 0/1/2 the lab sends.\n          const rawFlow = flat(pick('fxFlow'));\n          if (rawFlow === undefined || rawFlow === null || rawFlow === '') return pre.physics.flow;\n          const nFlow = Number(rawFlow);\n          if (Number.isFinite(nFlow)) return nFlow;\n          const kFlow = String(rawFlow).toLowerCase();\n          if (kFlow === 'waves') return 0;\n          if (kFlow === 'ripples') return 1;\n          if (kFlow === 'chaos') return 2;\n          return pre.physics.flow;\n        })(),\n        lerp: Math.min(1, Math.max(0.005, num('fxLerp', 0.05))),\n        mouseOn: bool('mouseEnabled', true),\n        mouseStrength: num('mouseStrength', 1),\n        mouseRadius: num('mouseRadius', 1),\n        mouseSmooth: Math.min(1, Math.max(0.005, num('mouseSmooth', 0.05))),\n        textWarp: num('mouseTextWarp', 1),\n        autoFit: bool('autoFit', true),\n        fitPadding: Math.min(0.45, Math.max(0, num('fitPadding', 0.04))),\n        lineHeightScale: num('lineHeightScale', 0.9),\n        uppercase: bool('uppercase', false),\n        align: str('textAlignment', 'auto'),\n        fontOverride: str('fontFamilyOverride', ''),\n        fontUrl: str('fontUrl', ''),\n        presetAppliesFont: bool('presetAppliesFont', false),\n        mode: str('renderMode', 'mask') === 'blend' ? 'blend' : 'mask',\n        plateColor: str('plateColor', '#000000'),\n        blendMode: str('blendMode', 'multiply'),\n        maxDpr: num('maxPixelRatio', 2),\n        paused: bool('paused', false),\n        clickAction: str('clickAction', 'none'),\n        debug: bool('debugInfo', false)\n      };\n    }\n\n    function resolveFont(c) {\n      // Read the OUTER host: that is where the Block writes font / textSize / textFormat.\n      const cs = getComputedStyle(outerHost() || host);\n      let family = cs.fontFamily || 'sans-serif';\n      if (c.presetAppliesFont) { ensureGoogleFonts(); family = c.preset.font; }\n      if (c.fontOverride) {\n        family = c.fontOverride;\n        if (c.fontUrl) ensureCustomFont(c.fontOverride, c.fontUrl);\n        else ensureGoogleFonts();\n      }\n      const weight = (cs.fontWeight && cs.fontWeight !== 'normal' && cs.fontWeight !== '400') ? cs.fontWeight : '700';\n      let ls = parseFloat(cs.letterSpacing);\n      if (!Number.isFinite(ls)) ls = 0;\n      let align = c.align;\n      if (align === 'auto' || !align) {\n        const ta = cs.textAlign;\n        align = (ta === 'left' || ta === 'right' || ta === 'center') ? ta : 'center';\n      }\n      let transform = cs.textTransform;\n      if (c.uppercase) transform = 'uppercase';\n      const baseSize = parseFloat(cs.fontSize) || 64;\n      return { family: family, weight: weight, letterSpacing: ls, align: align, transform: transform, baseSize: baseSize };\n    }\n\n    function applyTransform(text, transform) {\n      if (transform === 'uppercase') return text.toUpperCase();\n      if (transform === 'lowercase') return text.toLowerCase();\n      if (transform === 'capitalize') {\n        return text.split(' ').map(function (w) {\n          return w ? w.charAt(0).toUpperCase() + w.slice(1) : w;\n        }).join(' ');\n      }\n      return text;\n    }\n\n    function fontSpec(f, size) {\n      return f.weight + ' ' + size + 'px ' + f.family;\n    }\n\n    function measureWidest(ctx, lines, f, size) {\n      ctx.font = fontSpec(f, size);\n      if ('letterSpacing' in ctx) ctx.letterSpacing = f.letterSpacing + 'px';\n      let w = 0;\n      for (let i = 0; i < lines.length; i++) w = Math.max(w, ctx.measureText(lines[i]).width);\n      return w;\n    }\n\n    function drawMask(c, f, cssW, cssH, dpr) {\n      const text = applyTransform(c.content, f.transform);\n      const lines = text.split(NL);\n      const pxW = Math.max(1, Math.round(cssW * dpr));\n      const pxH = Math.max(1, Math.round(cssH * dpr));\n      if (maskCv.width !== pxW || maskCv.height !== pxH) { maskCv.width = pxW; maskCv.height = pxH; }\n\n      maskCtx.setTransform(1, 0, 0, 1, 0, 0);\n      maskCtx.clearRect(0, 0, pxW, pxH);\n      maskCtx.scale(dpr, dpr);\n\n      const availW = cssW * (1 - c.fitPadding * 2);\n      const availH = cssH * (1 - c.fitPadding * 2);\n      let size = f.baseSize;\n\n      if (c.autoFit) {\n        let lo = 4, hi = Math.max(16, cssH * 4);\n        for (let i = 0; i < 26; i++) {\n          const mid = (lo + hi) / 2;\n          const w = measureWidest(maskCtx, lines, f, mid);\n          const h = lines.length * mid * c.lineHeightScale;\n          if (w <= availW && h <= availH) lo = mid; else hi = mid;\n        }\n        size = lo;\n      }\n      fittedSize = size;\n\n      maskCtx.font = fontSpec(f, size);\n      if ('letterSpacing' in maskCtx) maskCtx.letterSpacing = f.letterSpacing + 'px';\n      maskCtx.fillStyle = '#ffffff';\n      maskCtx.textBaseline = 'middle';\n      maskCtx.textAlign = f.align;\n\n      const x = f.align === 'left' ? cssW * c.fitPadding\n        : f.align === 'right' ? cssW * (1 - c.fitPadding)\n        : cssW / 2;\n      const lh = size * c.lineHeightScale;\n      const startY = cssH / 2 - (lines.length - 1) * lh / 2;\n      for (let i = 0; i < lines.length; i++) maskCtx.fillText(lines[i], x, startY + i * lh);\n\n      gl.activeTexture(gl.TEXTURE1);\n      gl.bindTexture(gl.TEXTURE_2D, maskTex);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, maskCv);\n    }\n\n    function uploadGradient(colors) {\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, gradTex);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, gradientCanvas(colors));\n    }\n\n    const targetMouse = { x: 0.5, y: 0.5 };\n    const curMouse = { x: 0.5, y: 0.5 };\n    d2.on(window, 'pointermove', function (e) {\n      const r = host.getBoundingClientRect();\n      if (!r.width || !r.height) return;\n      targetMouse.x = (e.clientX - r.left) / r.width;\n      targetMouse.y = 1 - (e.clientY - r.top) / r.height;\n    });\n\n    function randomize(action) {\n      const cfg = readConfig();\n      const names = PRESETS.map(function (x) { return x.name; });\n      const idx = names.indexOf(cfg.preset.name);\n      let next;\n      if (action === 'random') {\n        next = Math.floor(Math.random() * PRESETS.length);\n        if (PRESETS.length > 1 && next === idx) next = (next + 1) % PRESETS.length;\n      } else {\n        next = (idx + 1) % PRESETS.length;\n      }\n      const p = d2.params || {};\n      const liveKey = d2.param('liveKey', '');\n      const live = liveKey ? (d2.shared.get(liveKey, {}, { global: true }) || {}) : {};\n      const base = live.fxPreset;\n      clickPresetBase = (base === undefined || base === null || base === '') ? p.fxPreset : base;\n      if (clickPresetBase === '' || clickPresetBase === null) clickPresetBase = undefined;\n      clickPreset = PRESETS[next].name;\n      maskSig = '';\n      d2.emit('onPresetChange', { name: PRESETS[next].name, index: next, preset: PRESETS[next] });\n    }\n\n    d2.on(host, 'click', function () {\n      const a = readConfig().clickAction;\n      if (a === 'random' || a === 'cycle') randomize(a);\n    });\n\n    const cur = { scale: 0, complexity: 0, contrast: 0, seeded: false };\n    let time = 0;\n    let lastT = 0;\n\n    d2.raf(function (t) {\n      const dt = lastT ? Math.min(0.1, (t - lastT) / 1000) : 0.016;\n      lastT = t;\n\n      const c = readConfig();\n      const f = resolveFont(c);\n\n      host.classList.toggle('is-blend', c.mode === 'blend');\n      host.classList.toggle('is-dbg', !!c.debug);\n      host.classList.toggle('is-click', c.clickAction === 'random' || c.clickAction === 'cycle');\n\n      const cssW = host.clientWidth || 1;\n      const cssH = host.clientHeight || 1;\n      const dpr = Math.max(1, Math.min(c.maxDpr, window.devicePixelRatio || 1));\n      const pxW = Math.max(1, Math.round(cssW * dpr));\n      const pxH = Math.max(1, Math.round(cssH * dpr));\n\n      let resized = false;\n      if (cv.width !== pxW || cv.height !== pxH) {\n        cv.width = pxW; cv.height = pxH;\n        gl.viewport(0, 0, pxW, pxH);\n        resized = true;\n      }\n\n      const gs = c.colors.join(',');\n      if (gs !== gradSig) { gradSig = gs; uploadGradient(c.colors); }\n\n      const sig = [c.content, f.family, f.weight, f.letterSpacing, f.align, f.transform,\n                   c.autoFit ? 'fit' : f.baseSize, c.fitPadding, c.lineHeightScale, pxW, pxH, fontsReady].join('|');\n      if (sig !== maskSig || resized) {\n        maskSig = sig;\n        drawMask(c, f, cssW, cssH, dpr);\n      }\n\n      if (c.mode === 'blend') {\n        const knock = c.blendMode === 'screen' ? '#000000' : '#ffffff';\n        plate.style.backgroundColor = c.plateColor;\n        plate.style.mixBlendMode = c.blendMode;\n        txtEl.style.color = knock;\n        txtEl.style.fontFamily = f.family;\n        txtEl.style.fontWeight = f.weight;\n        txtEl.style.fontSize = (c.autoFit ? fittedSize : f.baseSize) + 'px';\n        txtEl.style.lineHeight = String(c.lineHeightScale);\n        txtEl.style.textAlign = f.align;\n        txtEl.style.textTransform = 'none';\n        txtEl.style.letterSpacing = f.letterSpacing + 'px';\n        const shown = applyTransform(c.content, f.transform);\n        if (txtEl.textContent !== shown) txtEl.textContent = shown;\n      }\n\n      if (!c.paused) time += dt * c.speed;\n\n      if (!cur.seeded) {\n        cur.scale = c.scale; cur.complexity = c.complexity; cur.contrast = c.contrast; cur.seeded = true;\n      } else {\n        cur.scale += (c.scale - cur.scale) * c.lerp;\n        cur.complexity += (c.complexity - cur.complexity) * c.lerp;\n        cur.contrast += (c.contrast - cur.contrast) * c.lerp;\n      }\n\n      const mt = c.mouseOn ? targetMouse : { x: 0.5, y: 0.5 };\n      curMouse.x += (mt.x - curMouse.x) * c.mouseSmooth;\n      curMouse.y += (mt.y - curMouse.y) * c.mouseSmooth;\n\n      gl.useProgram(program);\n      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, gradTex);\n      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, maskTex);\n      gl.uniform1f(U.u_time, time);\n      gl.uniform2f(U.u_mouse, curMouse.x, curMouse.y);\n      gl.uniform2f(U.u_resolution, pxW, pxH);\n      gl.uniform1f(U.u_scale, cur.scale);\n      gl.uniform1f(U.u_complexity, cur.complexity);\n      gl.uniform1f(U.u_contrast, cur.contrast);\n      gl.uniform1f(U.u_flow_type, c.flow);\n      gl.uniform1f(U.u_hue_shift, c.hue);\n      gl.uniform1f(U.u_mouse_strength, c.mouseOn ? c.mouseStrength : 0);\n      gl.uniform1f(U.u_mouse_radius, c.mouseRadius);\n      gl.uniform1f(U.u_text_warp, c.mouseOn ? c.textWarp : 0);\n      gl.uniform1f(U.u_use_mask, c.mode === 'blend' ? 0 : 1);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n      if (c.debug && dbgEl) {\n        dbgEl.textContent = [\n          'preset   ' + c.preset.name,\n          'text     ' + c.contentSrc,\n          'font     ' + f.family,\n          'weight   ' + f.weight + '   align ' + f.align,\n          'size     ' + Math.round(c.autoFit ? fittedSize : f.baseSize) + 'px' + (c.autoFit ? ' (autofit)' : ''),\n          'box      ' + Math.round(cssW) + 'x' + Math.round(cssH) + ' @' + dpr.toFixed(1) + 'x',\n          'fx       s' + cur.scale.toFixed(2) + ' c' + cur.complexity.toFixed(2) + ' k' + cur.contrast.toFixed(2) + ' flow ' + c.flow,\n          'mouse    ' + (c.mouseOn ? 'on str' + c.mouseStrength + ' rad' + c.mouseRadius + ' warp' + c.textWarp : 'off'),\n          'mode     ' + c.mode\n        ].join(NL);\n      }\n    });\n  }\n};"}},"content":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"content"}}}},"fxPreset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxPreset"}}}},"presetAppliesFont":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"presetAppliesFont"}}}},"colorStops":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"colorStops"}}}},"fxScale":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxScale"}}}},"fxComplexity":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxComplexity"}}}},"fxContrast":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxContrast"}}}},"fxHue":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxHue"}}}},"fxSpeed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxSpeed"}}}},"fxFlow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxFlow"}}}},"fxLerp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fxLerp"}}}},"mouseEnabled":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mouseEnabled"}}}},"mouseStrength":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mouseStrength"}}}},"mouseRadius":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mouseRadius"}}}},"mouseSmooth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mouseSmooth"}}}},"mouseTextWarp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mouseTextWarp"}}}},"autoFit":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoFit"}}}},"fitPadding":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fitPadding"}}}},"lineHeightScale":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"lineHeightScale"}}}},"uppercase":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"uppercase"}}}},"textAlignment":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"textAlignment"}}}},"fontFamilyOverride":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fontFamilyOverride"}}}},"fontUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fontUrl"}}}},"renderMode":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"renderMode"}}}},"plateColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"plateColor"}}}},"blendMode":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"blendMode"}}}},"maxPixelRatio":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"maxPixelRatio"}}}},"paused":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"paused"}}}},"clickAction":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clickAction"}}}},"liveKey":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"liveKey"}}}},"debugInfo":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"debugInfo"}}}},"onPresetChange":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onPresetChange"}}}}}}}}}},{"_id":"6a5fb6cc0fa17fc6eb40dd19","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"supertext-lab","package":"bigaton-playground","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 NL = String.fromCharCode(10);\nconst LIVE_KEY = 'supertext.lab.live';\nconst TARGET = 'bigaton-playground@supertext-fx';\n\nconst RAW = [\n  ['Liquid Chrome', ['#fff','#ccc','#999','#555','#000','#555','#fff'], [10,0.5,1,0]],\n  ['Gothic Blood', ['#2a0000','#500000','#800000','#f00','#300'], [0.5,2,1.5,2]],\n  ['Cyber Punk', ['#f0c','#339','#0cf','#fff','#f0c'], [3.5,1.5,1.3,1]],\n  ['Pure Gold', ['#fff7cc','#ffeb3b','#ffc107','#ff9800','#4a2c00'], [20,0.3,1.2,0]],\n  ['Rose Gold', ['#ffebee','#ffcdd2','#e57373','#d32f2f','#880e4f'], [11,0.5,1.1,0]],\n  ['Obsidian', ['#111','#222','#333','#000','#101'], [6,1,1.8,0]],\n  ['Antique Bronze', ['#d7ccc8','#a1887f','#795548','#3e2723'], [30.5,25,1,0]],\n  ['Brushed Steel', ['#eceff1','#b0bec5','#607d8b','#455a64'], [11,0.02,1,0]],\n  ['Gunmetal', ['#cfd8dc','#78909c','#37474f','#263238'], [5,1.2,1.4,1]],\n  ['Titanium', ['#e8eaf6','#9fa8da','#5c6bc0','#303f9f'], [8,0.9,1.2,0]],\n  ['Mercury', ['#fff','#e0e0e0','#bdbdbd','#757575'], [3,0.1,1.5,1]],\n  ['Rusted Metal', ['#ffcc80','#e65100','#3e2723','#212121'], [100,2.25,0.9,2]],\n  ['Brass', ['#fff9c4','#fbc02d','#f57f17'], [8,0.4,1.1,0]],\n  ['Platinum', ['#fff','#f5f5f5','#e0e0e0','#bdbdbd'], [5,0.2,1.4,0]],\n  ['White Gold', ['#fffde7','#fff59d','#cfcfcf','#fff9c4'], [8.5,0.3,1.2,0]],\n  ['Matrix Code', ['#000','#030','#0f0','#cfc'], [10,4,2,2]],\n  ['Vapor Wave', ['#ff71ce','#01cdfe','#05ffa1','#b967ff','#fffb96'], [3,1,1,0]],\n  ['Deep Ocean', ['#e0f7fa','#26c6da','#006064','#003333'], [6,1.2,1.8,0]],\n  ['Heavy Metal', ['#444','#666','#222','#000'], [3,3,0.9,2]],\n  ['Candy Shop', ['#fff','#f8bbd0','#f48fb1','#e91e63'], [2.5,0.5,1.1,1]],\n  ['System Glitch', ['#0f0','#000','#f0f','#fff'], [12,5,2,2]],\n  ['Royal Velvet', ['#f3e5f5','#8e24aa','#4a148c','#21033a'], [5,0.8,1.5,0]],\n  ['Ice Cold', ['#fff','#b2dfdb','#4db6ac','#26a69a'], [8,1,1.5,1]],\n  ['Toxic Sludge', ['#111','#1a0b2e','#0f0','#cf0','#030'], [3,2.5,0.8,2]],\n  ['8-Bit Glitch', ['#0f0','#000','#0f0','#fff'], [15,4,2,2]],\n  ['Graffiti', ['#fff','#ff0','#f0f','#0ff','#000'], [2.2,1.8,1.1,2]],\n  ['Magma Flow', ['#300','#f00','#fa0','#ffeb3b'], [3.5,1.5,1.2,0]],\n  ['Holographic', ['#fff','#f0f','#0ff','#ff0','#fff'], [8,0.6,1.6,0]],\n  ['Midnight Oil', ['#000','#101010','#202020','#050505'], [2.5,1,2,2]],\n  ['Pearl', ['#fff','#fce4ec','#e1f5fe','#f3e5f5','#fff'], [3,0.4,1.3,1]],\n  ['Radioactive', ['#000','#1b5e20','#64dd17','#ccff90'], [6,2,1.2,2]],\n  ['Bubblegum', ['#fff','#81d4fa','#f48fb1','#fff'], [2,0.5,1,1]],\n  ['Noir', ['#000','#333','#666','#999','#ccc'], [8,3,1.5,2]],\n  ['Alien Skin', ['#000','#1a237e','#311b92','#00e676'], [10,4,1.2,2]],\n  ['Sunset Blvd', ['#210e37','#6a1b9a','#ff6f00','#ffeb3b'], [3,0.8,1.2,0]],\n  ['Jungle Camo', ['#1b5e20','#33691e','#558b2f','#827717'], [5,2.5,1,2]],\n  ['Copper Patina', ['#3e2723','#d84315','#009688','#80cbc4'], [8,1.2,1.1,1]],\n  ['Amethyst', ['#311b92','#673ab7','#d1c4e9','#fff'], [8.5,0.7,1.4,0]],\n  ['Blood Moon', ['#000','#300','#b71c1c','#fff'], [5,1.5,1.8,0]],\n  ['Frozen Berry', ['#311b92','#0d47a1','#00bcd4','#e1f5fe'], [3.5,1,1.3,1]]\n];\n\nconst PRESETS = RAW.map(function (r) {\n  return { name: r[0], colors: r[1], physics: { scale: r[2][0], complexity: r[2][1], contrast: r[2][2], flow: r[2][3] } };\n});\n\nconst SLIDERS = {\n  fx: [\n    ['fxScale', 'Scale (Zoom)', 0, 100, 0.1],\n    ['fxComplexity', 'Complexity', 0, 25, 0.01],\n    ['fxContrast', 'Contrast', 0, 25, 0.1],\n    ['fxHue', 'Color Shift', 0, 10, 0.1],\n    ['fxSpeed', 'Flow Speed', 0, 20, 0.1],\n    ['fxLerp', 'Transition Lerp', 0.005, 1, 0.005]\n  ],\n  mouse: [\n    ['mouseStrength', 'Strength', 0, 3, 0.05],\n    ['mouseRadius', 'Radius', 0, 3, 0.05],\n    ['mouseSmooth', 'Smoothing', 0.005, 0.5, 0.005],\n    ['mouseTextWarp', 'Text Follow', 0, 3, 0.05]\n  ],\n  type: [\n    ['fitPadding', 'Fit Padding', 0, 0.45, 0.01],\n    ['lineHeightScale', 'Line Height', 0.4, 2, 0.01]\n  ],\n  render: [\n    ['maxPixelRatio', 'Max Pixel Ratio', 0.5, 3, 0.1]\n  ]\n};\n\nfunction baseCfg() {\n  const p = PRESETS[5];\n  return {\n    content: 'SUPERTEXT',\n    preset: p.name,\n    colorStops: p.colors.join(','),\n    fxScale: p.physics.scale,\n    fxComplexity: p.physics.complexity,\n    fxContrast: p.physics.contrast,\n    fxFlow: p.physics.flow,\n    fxHue: 0,\n    fxSpeed: 0.5,\n    fxLerp: 0.05,\n    mouseEnabled: true,\n    mouseStrength: 1,\n    mouseRadius: 1,\n    mouseSmooth: 0.05,\n    mouseTextWarp: 1,\n    autoFit: true,\n    fitPadding: 0.04,\n    lineHeightScale: 0.9,\n    uppercase: true,\n    align: 'center',\n    presetAppliesFont: true,\n    fontFamilyOverride: '',\n    renderMode: 'mask',\n    plateColor: '#000000',\n    blendMode: 'multiply',\n    maxPixelRatio: 2,\n    paused: false,\n    debugInfo: false,\n    stageColor: '#08080a'\n  };\n}\n\nconst STATE = { cfg: baseCfg() };\n\nfunction esc(s) {\n  return String(s == null ? '' : s)\n    .split('&').join('&amp;')\n    .split('<').join('&lt;')\n    .split('>').join('&gt;')\n    .split('\"').join('&quot;');\n}\n\nfunction fmtNum(n) {\n  const v = Number(n);\n  if (!Number.isFinite(v)) return '0';\n  if (Math.abs(v) >= 100) return v.toFixed(0);\n  if (Math.abs(v) >= 10) return v.toFixed(1);\n  return v.toFixed(3).replace(/0+$/, '').replace(/\\.$/, '');\n}\n\nfunction findPreset(name) {\n  const hit = PRESETS.filter(function (p) { return p.name === name; })[0];\n  return hit || PRESETS[5];\n}\n\nfunction stopsOf(cfg) {\n  const out = [];\n  let tok = '';\n  const raw = String(cfg.colorStops || '');\n  for (let i = 0; i < raw.length; i++) {\n    const ch = raw.charAt(i);\n    if (ch === ',' || ch === ';' || ch === ' ') { if (tok) { out.push(tok); tok = ''; } }\n    else tok += ch;\n  }\n  if (tok) out.push(tok);\n  return out.length ? out : ['#ffffff', '#000000'];\n}\n\nfunction expandHex(h) {\n  const s = String(h || '').trim();\n  if (/^#[0-9a-fA-F]{3}$/.test(s)) {\n    return '#' + s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2) + s.charAt(3) + s.charAt(3);\n  }\n  if (/^#[0-9a-fA-F]{6}$/.test(s)) return s;\n  return '#000000';\n}\n\nfunction sliderRow(cfg, spec) {\n  const key = spec[0];\n  return `<div class=\"row\">\n    <div class=\"rowhead\"><span>${esc(spec[1])}</span><span class=\"val\" data-val=\"${key}\">${fmtNum(cfg[key])}</span></div>\n    <input class=\"rng\" type=\"range\" data-key=\"${key}\" min=\"${spec[2]}\" max=\"${spec[3]}\" step=\"${spec[4]}\" value=\"${cfg[key]}\"/>\n  </div>`;\n}\n\nfunction toggleRow(cfg, key, label) {\n  return `<label class=\"tog\"><input type=\"checkbox\" data-tog=\"${key}\" ${cfg[key] ? 'checked' : ''}/><span>${esc(label)}</span></label>`;\n}\n\nfunction segRow(label, key, options, current) {\n  return `<div class=\"row\">\n    <div class=\"rowhead\"><span>${esc(label)}</span></div>\n    <div class=\"seg\">${options.map(function (o) {\n      return `<button class=\"segbtn ${String(o[1]) === String(current) ? 'on' : ''}\" data-seg=\"${key}\" data-segval=\"${esc(o[1])}\">${esc(o[0])}</button>`;\n    }).join('')}</div>\n  </div>`;\n}\n\nfunction paramsCodeOf(cfg) {\n  const q = function (v) { return \"'\" + String(v).split(\"'\").join(\"\") + \"'\"; };\n  const lines = [\n    '  content: ' + q(cfg.content),\n    '  fxPreset: ' + q(cfg.preset),\n    '  colorStops: ' + q(cfg.colorStops),\n    '  fxScale: ' + cfg.fxScale,\n    '  fxComplexity: ' + cfg.fxComplexity,\n    '  fxContrast: ' + cfg.fxContrast,\n    '  fxHue: ' + cfg.fxHue,\n    '  fxSpeed: ' + cfg.fxSpeed,\n    '  fxFlow: ' + cfg.fxFlow,\n    '  fxLerp: ' + cfg.fxLerp,\n    '  mouseEnabled: ' + (cfg.mouseEnabled ? 'true' : 'false'),\n    '  mouseStrength: ' + cfg.mouseStrength,\n    '  mouseRadius: ' + cfg.mouseRadius,\n    '  mouseSmooth: ' + cfg.mouseSmooth,\n    '  mouseTextWarp: ' + cfg.mouseTextWarp,\n    '  autoFit: ' + (cfg.autoFit ? 'true' : 'false'),\n    '  fitPadding: ' + cfg.fitPadding,\n    '  lineHeightScale: ' + cfg.lineHeightScale,\n    '  uppercase: ' + (cfg.uppercase ? 'true' : 'false'),\n    '  textAlignment: ' + q(cfg.align),\n    '  presetAppliesFont: ' + (cfg.presetAppliesFont ? 'true' : 'false'),\n    '  renderMode: ' + q(cfg.renderMode),\n    '  plateColor: Color(rgba: ' + q(cfg.plateColor) + ')',\n    '  blendMode: ' + q(cfg.blendMode),\n    '  maxPixelRatio: ' + cfg.maxPixelRatio,\n    '  clickAction: ' + q('none')\n  ];\n  if (cfg.fontFamilyOverride) lines.push('  fontFamilyOverride: ' + q(cfg.fontFamilyOverride));\n  return '{' + NL + lines.join(',' + NL) + NL + '}';\n}\n\nexport default {\n  css: `\n    .lab { display: grid; grid-template-columns: 1fr 360px; width: 100%; height: 100%;\n           background: #0b0b0d; color: #e9e9ee; font: 13px/1.45 Inter, system-ui, -apple-system, sans-serif; }\n    .stage { position: relative; overflow: hidden; display: grid; grid-template-rows: 1fr auto; }\n    .canvasbox { position: relative; overflow: hidden; }\n    .hint { padding: 10px 16px; font-size: 11px; letter-spacing: .14em; text-transform: uppercase;\n            color: #6c6c78; text-align: center; border-top: 1px solid #1b1b21; }\n\n    .panel { border-left: 1px solid #1b1b21; background: #101014; overflow-y: auto; padding: 16px 16px 40px; }\n    .panel::-webkit-scrollbar { width: 8px; }\n    .panel::-webkit-scrollbar-thumb { background: #26262e; border-radius: 4px; }\n\n    .brand { font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: #6c6c78; margin-bottom: 14px; }\n    .grp { border: 1px solid #1e1e26; border-radius: 12px; padding: 12px; margin-bottom: 12px; background: #14141a; }\n    .grptitle { font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: #7b7b89; margin-bottom: 10px; }\n\n    .row { margin-bottom: 11px; }\n    .row:last-child { margin-bottom: 0; }\n    .rowhead { display: flex; justify-content: space-between; font-size: 11px; letter-spacing: .06em;\n               text-transform: uppercase; color: #9a9aa8; margin-bottom: 5px; }\n    .val { color: #e9e9ee; font-variant-numeric: tabular-nums; }\n\n    .rng { -webkit-appearance: none; appearance: none; width: 100%; height: 4px; border-radius: 2px;\n           background: #2a2a34; outline: none; }\n    .rng::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px;\n           border-radius: 50%; background: #e9e9ee; cursor: pointer; }\n    .rng::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #e9e9ee; cursor: pointer; }\n\n    .seg { display: flex; gap: 5px; }\n    .segbtn { flex: 1; padding: 7px 4px; border-radius: 8px; border: 1px solid #2a2a34; background: #191921;\n              color: #9a9aa8; font: 600 10px/1 Inter, sans-serif; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; }\n    .segbtn.on { background: #e9e9ee; color: #101014; border-color: #e9e9ee; }\n\n    .tog { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; cursor: pointer; font-size: 12px; color: #c9c9d4; }\n    .tog:last-child { margin-bottom: 0; }\n    .tog input { accent-color: #e9e9ee; width: 15px; height: 15px; cursor: pointer; }\n\n    .ta { width: 100%; box-sizing: border-box; background: #191921; border: 1px solid #2a2a34; color: #e9e9ee;\n          border-radius: 8px; padding: 8px; font: 13px/1.4 Inter, sans-serif; resize: vertical; outline: none; }\n    .ta:focus { border-color: #4a4a5a; }\n    .sel { width: 100%; box-sizing: border-box; background: #191921; border: 1px solid #2a2a34; color: #e9e9ee;\n           border-radius: 8px; padding: 8px; font: 13px Inter, sans-serif; outline: none; cursor: pointer; }\n\n    .btn { width: 100%; padding: 9px; border-radius: 9px; border: 1px solid #2a2a34; background: #191921;\n           color: #e9e9ee; font: 600 11px/1 Inter, sans-serif; letter-spacing: .1em; text-transform: uppercase;\n           cursor: pointer; margin-top: 8px; }\n    .btn:hover { background: #22222c; }\n    .btn.primary { background: #e9e9ee; color: #101014; border-color: #e9e9ee; }\n\n    .stops { display: flex; flex-wrap: wrap; gap: 6px; }\n    .stopwrap { position: relative; width: 34px; height: 34px; }\n    .stop { width: 34px; height: 34px; padding: 0; border: 1px solid #2a2a34; border-radius: 8px;\n            background: transparent; cursor: pointer; }\n    .stopx { position: absolute; top: -5px; right: -5px; width: 15px; height: 15px; border-radius: 50%;\n             border: 0; background: #2a2a34; color: #c9c9d4; font: 700 9px/1 Inter, sans-serif; cursor: pointer; }\n    .addstop { width: 34px; height: 34px; border-radius: 8px; border: 1px dashed #3a3a46; background: transparent;\n               color: #7b7b89; font-size: 16px; cursor: pointer; }\n\n    .exp { width: 100%; box-sizing: border-box; height: 150px; background: #08080a; border: 1px solid #2a2a34;\n           color: #9adfb5; border-radius: 8px; padding: 8px; font: 11px/1.5 ui-monospace, Menlo, monospace;\n           resize: vertical; outline: none; display: none; margin-top: 8px; white-space: pre; }\n    .exp.on { display: block; }\n  `,\n\n  render(d2) {\n    const rev = d2.state.rev || 0;\n    const cfg = STATE.cfg;\n    const stops = stopsOf(cfg);\n\n    const presetOpts = PRESETS.map(function (p) {\n      return `<option value=\"${esc(p.name)}\" ${p.name === cfg.preset ? 'selected' : ''}>${esc(p.name)}</option>`;\n    }).join('');\n\n    const preview = d2.render(TARGET, {\n      liveKey: LIVE_KEY,\n      clickAction: 'random',\n      onPresetChange: function (payload) {\n        const p = findPreset(payload && payload.name);\n        STATE.cfg.preset = p.name;\n        STATE.cfg.fxScale = p.physics.scale;\n        STATE.cfg.fxComplexity = p.physics.complexity;\n        STATE.cfg.fxContrast = p.physics.contrast;\n        STATE.cfg.fxFlow = p.physics.flow;\n        STATE.cfg.fxHue = 0;\n        STATE.cfg.colorStops = p.colors.join(',');\n        d2.shared.set(LIVE_KEY, Object.assign({}, STATE.cfg, {\n          fxPreset: p.name,\n          textAlignment: STATE.cfg.align\n        }), { global: true });\n        d2.state.rev = (d2.state.rev || 0) + 1;\n      }\n    }, { key: 'preview' });\n\n    return `<div class=\"lab\" data-rev=\"${rev}\">\n      <div class=\"stage\">\n        <div class=\"canvasbox\" data-stage style=\"background:${esc(cfg.stageColor)}\">${preview}</div>\n        <div class=\"hint\">Click the text to randomize a preset &middot; move the mouse to disrupt the surface</div>\n      </div>\n\n      <div class=\"panel\">\n        <div class=\"brand\">Supertext FX &mdash; Live Lab</div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Content</div>\n          <textarea class=\"ta\" data-text rows=\"2\" placeholder=\"Your text\">${esc(cfg.content)}</textarea>\n          <div class=\"row\" style=\"margin-top:10px\">\n            <div class=\"rowhead\"><span>Preset</span></div>\n            <select class=\"sel\" data-preset>${presetOpts}</select>\n          </div>\n          <button class=\"btn primary\" data-random>Randomize preset</button>\n          <button class=\"btn\" data-reset>Reset to preset defaults</button>\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Physics</div>\n          ${SLIDERS.fx.map(function (s) { return sliderRow(cfg, s); }).join('')}\n          ${segRow('Flow Pattern', 'fxFlow', [['Waves', 0], ['Ripples', 1], ['Chaos', 2]], cfg.fxFlow)}\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Gradient stops</div>\n          <div class=\"stops\">\n            ${stops.map(function (c, i) {\n              return `<div class=\"stopwrap\"><input class=\"stop\" type=\"color\" data-stop=\"${i}\" value=\"${esc(expandHex(c))}\"/><button class=\"stopx\" data-delstop=\"${i}\">&times;</button></div>`;\n            }).join('')}\n            <button class=\"addstop\" data-addstop>+</button>\n          </div>\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Mouse deformation</div>\n          ${toggleRow(cfg, 'mouseEnabled', 'Enable mouse deformation')}\n          ${SLIDERS.mouse.map(function (s) { return sliderRow(cfg, s); }).join('')}\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Typography</div>\n          ${toggleRow(cfg, 'autoFit', 'Auto-fit text to block')}\n          ${toggleRow(cfg, 'uppercase', 'Force uppercase')}\n          ${toggleRow(cfg, 'presetAppliesFont', 'Preset also sets the font')}\n          ${SLIDERS.type.map(function (s) { return sliderRow(cfg, s); }).join('')}\n          ${segRow('Align', 'align', [['Left', 'left'], ['Center', 'center'], ['Right', 'right']], cfg.align)}\n          <div class=\"row\">\n            <div class=\"rowhead\"><span>Font family override</span></div>\n            <input class=\"ta\" data-font value=\"${esc(cfg.fontFamilyOverride)}\" placeholder=\"inherit from Block Font\"/>\n          </div>\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Render</div>\n          ${segRow('Mode', 'renderMode', [['Mask (alpha)', 'mask'], ['Blend (plate)', 'blend']], cfg.renderMode)}\n          ${segRow('Blend', 'blendMode', [['Multiply', 'multiply'], ['Screen', 'screen']], cfg.blendMode)}\n          <div class=\"row\">\n            <div class=\"rowhead\"><span>Stage background</span></div>\n            <input class=\"stop\" type=\"color\" data-stage-color value=\"${esc(expandHex(cfg.stageColor))}\" style=\"width:100%;height:30px\"/>\n          </div>\n          <div class=\"row\">\n            <div class=\"rowhead\"><span>Plate color (blend mode)</span></div>\n            <input class=\"stop\" type=\"color\" data-plate value=\"${esc(expandHex(cfg.plateColor))}\" style=\"width:100%;height:30px\"/>\n          </div>\n          ${SLIDERS.render.map(function (s) { return sliderRow(cfg, s); }).join('')}\n          ${toggleRow(cfg, 'paused', 'Pause animation')}\n          ${toggleRow(cfg, 'debugInfo', 'Show debug overlay')}\n        </div>\n\n        <div class=\"grp\">\n          <div class=\"grptitle\">Export</div>\n          <button class=\"btn\" data-export>Copy inspector params</button>\n          <textarea class=\"exp\" data-exp readonly></textarea>\n        </div>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2) {\n    const push = function () {\n      d2.shared.set(LIVE_KEY, Object.assign({}, STATE.cfg, {\n        fxPreset: STATE.cfg.preset,\n        textAlignment: STATE.cfg.align\n      }), { global: true });\n    };\n    const rerender = function () {\n      push();\n      d2.state.rev = (d2.state.rev || 0) + 1;\n    };\n    push();\n\n    d2.on('.rng', 'input', function (e, el) {\n      const key = el.getAttribute('data-key');\n      const v = parseFloat(el.value);\n      STATE.cfg[key] = v;\n      const lbl = d2.$('[data-val=\"' + key + '\"]');\n      if (lbl) lbl.textContent = fmtNum(v);\n      push();\n    });\n\n    d2.on('.segbtn', 'click', function (e, el) {\n      const key = el.getAttribute('data-seg');\n      const raw = el.getAttribute('data-segval');\n      const v = key === 'fxFlow' ? parseFloat(raw) : raw;\n      STATE.cfg[key] = v;\n      d2.$$('[data-seg=\"' + key + '\"]').forEach(function (b) {\n        b.classList.toggle('on', b.getAttribute('data-segval') === raw);\n      });\n      push();\n    });\n\n    d2.on('[data-tog]', 'change', function (e, el) {\n      STATE.cfg[el.getAttribute('data-tog')] = !!el.checked;\n      push();\n    });\n\n    d2.on('[data-text]', 'input', function (e, el) {\n      STATE.cfg.content = el.value;\n      push();\n    });\n\n    d2.on('[data-font]', 'input', function (e, el) {\n      STATE.cfg.fontFamilyOverride = el.value;\n      push();\n    });\n\n    d2.on('[data-preset]', 'change', function (e, el) {\n      const p = findPreset(el.value);\n      STATE.cfg.preset = p.name;\n      STATE.cfg.fxScale = p.physics.scale;\n      STATE.cfg.fxComplexity = p.physics.complexity;\n      STATE.cfg.fxContrast = p.physics.contrast;\n      STATE.cfg.fxFlow = p.physics.flow;\n      STATE.cfg.fxHue = 0;\n      STATE.cfg.colorStops = p.colors.join(',');\n      rerender();\n    });\n\n    d2.on('[data-random]', 'click', function () {\n      const idx = PRESETS.map(function (p) { return p.name; }).indexOf(STATE.cfg.preset);\n      let next = Math.floor(Math.random() * PRESETS.length);\n      if (PRESETS.length > 1 && next === idx) next = (next + 1) % PRESETS.length;\n      const p = PRESETS[next];\n      STATE.cfg.preset = p.name;\n      STATE.cfg.fxScale = p.physics.scale;\n      STATE.cfg.fxComplexity = p.physics.complexity;\n      STATE.cfg.fxContrast = p.physics.contrast;\n      STATE.cfg.fxFlow = p.physics.flow;\n      STATE.cfg.fxHue = 0;\n      STATE.cfg.colorStops = p.colors.join(',');\n      rerender();\n    });\n\n    d2.on('[data-reset]', 'click', function () {\n      const keep = STATE.cfg.content;\n      const stage = STATE.cfg.stageColor;\n      STATE.cfg = baseCfg();\n      STATE.cfg.content = keep;\n      STATE.cfg.stageColor = stage;\n      rerender();\n    });\n\n    d2.on('.stop[data-stop]', 'input', function (e, el) {\n      const i = parseInt(el.getAttribute('data-stop'), 10);\n      const list = stopsOf(STATE.cfg);\n      list[i] = el.value;\n      STATE.cfg.colorStops = list.join(',');\n      push();\n    });\n\n    d2.on('[data-delstop]', 'click', function (e, el) {\n      const i = parseInt(el.getAttribute('data-delstop'), 10);\n      const list = stopsOf(STATE.cfg);\n      if (list.length <= 2) return;\n      list.splice(i, 1);\n      STATE.cfg.colorStops = list.join(',');\n      rerender();\n    });\n\n    d2.on('[data-addstop]', 'click', function () {\n      const list = stopsOf(STATE.cfg);\n      list.push(list[list.length - 1] || '#ffffff');\n      STATE.cfg.colorStops = list.join(',');\n      rerender();\n    });\n\n    d2.on('[data-stage-color]', 'input', function (e, el) {\n      STATE.cfg.stageColor = el.value;\n      const box = d2.$('[data-stage]');\n      if (box) box.style.background = el.value;\n      push();\n    });\n\n    d2.on('[data-plate]', 'input', function (e, el) {\n      STATE.cfg.plateColor = el.value;\n      push();\n    });\n\n    d2.on('[data-export]', 'click', function () {\n      const code = paramsCodeOf(STATE.cfg);\n      const box = d2.$('[data-exp]');\n      if (box) {\n        box.value = code;\n        box.classList.add('on');\n        box.focus();\n        box.select();\n      }\n      try {\n        if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(code);\n      } catch (err) {}\n    });\n  }\n};"}},"blockRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"blockRef"}}}}}}}}}},{"_id":"6a5fd6283a42b34509a50cb8","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wormhole-conductor","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Conductor — one instance lives INSIDE each repeated Card block.\n// It walks up to its own host's parent (the Card div) and drives that card's\n// 3D transform every frame. No global selectors: each instance only ever\n// touches its own card, so repeater clones can come and go freely.\n//\n// GEOMETRY — a vertical helix, not a closed ring. Cards rise while turning,\n// each card's face pointing radially OUTWARD from the axis. That single rule\n// gives the reference's behaviour for free: a card on the far side has its\n// normal pointing away from the camera, so it shows its back while passing\n// behind the centre object, then turns to face the viewer at the front.\n// `turns` is signed — negative runs the other way round the clock.\n//\n// VARIABLE PITCH — the helix climbs fast while a card is on the far side and\n// flattens as it swings to the front, so the front pass reads as a near\n// horizontal sweep across the screen rather than a diagonal climb.\n//\n// SPACING — cards sit a FIXED `gap` apart along the run, so the composition\n// does not change when records are added or removed. The chain is N * gap\n// long; a card whose position has run past the end of the helix is parked\n// invisible until its turn comes round again. Consequence, by design: adding\n// records makes the cycle LONGER, it does not pack the helix tighter.\n//\n// Cards are FLAT. A CSS transform is a matrix — it can rotate, scale, skew\n// and project a plane, but it cannot bend one. Curving a card means either\n// slicing it into strips (tried; the seams cost more than the curve gained\n// at a subtle radius) or drawing it in WebGL.\n//\n// A single shared clock lives on window so all cards advance in lockstep and\n// one wheel listener feeds scroll velocity into the whole chain.\n\nconst clock = () => {\n  if (!window.__WH_CLOCK) {\n    window.__WH_CLOCK = { t: 0, boost: 0, owner: null, last: 0, wheelBound: false }\n  }\n  return window.__WH_CLOCK\n}\n\n// Deterministic per-card jitter so the chain reads organic, not machined.\nconst jitter = (i, salt) => {\n  const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453\n  return v - Math.floor(v)\n}\n\nconst clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)\n\nexport default {\n  css: `\n    .wh-probe { position: absolute; width: 0; height: 0; overflow: hidden; pointer-events: none; }\n  `,\n\n  render() {\n    return '<div class=\"wh-probe\" data-d2-static></div>'\n  },\n\n  mounted(d2, root) {\n    // Climb out of the shadow root collecting data2 block elements. The first\n    // one is this component's own block, the second is the Card that repeats,\n    // the third is the Chain container. Counting blocks (rather than a fixed\n    // number of parentElement hops) survives extra wrapper divs.\n    const blocks = []\n    let n = root\n    for (let guard = 0; guard < 12 && blocks.length < 3; guard++) {\n      n = n.parentElement || (n.getRootNode && n.getRootNode().host) || null\n      if (!n) break\n      if (n.classList && n.classList.contains('data2-player-block')) blocks.push(n)\n    }\n\n    const card = blocks[1]\n    const chain = blocks[2]\n\n    if (!card || !chain) {\n      d2.log('[wormhole] could not reach the card element — DOM shape changed')\n      return\n    }\n\n    card.setAttribute('data-wh-card', '1')\n    chain.setAttribute('data-wh-chain', '1')\n\n    // Clear anything a previous build of this component left behind.\n    const stale = card.querySelector('[data-wh-strips]')\n    if (stale && stale.parentNode) stale.parentNode.removeChild(stale)\n\n    const S = clock()\n    const myId = Math.random().toString(36).slice(2)\n\n    // --- one-time scene setup, claimed by whichever card mounts first -------\n    if (chain.getAttribute('data-wh-scene') !== '1') {\n      chain.setAttribute('data-wh-scene', '1')\n      chain.style.transformStyle = 'preserve-3d'\n      chain.style.perspectiveOrigin = '50% 50%'\n      chain.style.overflow = 'visible'\n    }\n\n    card.style.transformStyle = 'preserve-3d'\n    card.style.willChange = 'transform, opacity'\n    card.style.transition = 'none'\n\n    // --- scroll drives direction + speed, decaying back to idle ------------\n    if (!S.wheelBound) {\n      S.wheelBound = true\n      S.wheelOwner = myId\n      d2.on(window, 'wheel', (e) => {\n        const dy = e.deltaY || 0\n        S.boost = Math.max(-1.4, Math.min(1.4, S.boost + dy * 0.00035))\n      }, { passive: true })\n    }\n\n    if (!S.owner) S.owner = myId\n\n    d2.onUnmount(() => {\n      if (S.owner === myId) { S.owner = null; S.last = 0 }\n      if (S.wheelOwner === myId) { S.wheelBound = false; S.wheelOwner = null }\n    })\n\n    d2.raf((now) => {\n      // Only the owning instance advances the shared clock, so every card\n      // reads the exact same t within a frame. t is measured in RUNS: one\n      // unit = one full trip along the helix, whatever the record count.\n      if (S.owner === myId) {\n        const dt = S.last ? Math.min((now - S.last) / 1000, 0.05) : 0.016\n        S.last = now\n        const idle = d2.param('speed', 0.045)\n        S.t += (idle + S.boost) * dt\n        S.boost *= Math.pow(0.12, dt) // ~88% shed per second, eases back to idle\n        chain.style.perspective = (d2.param('persp', 1400) || 1400) + 'px'\n      }\n\n      const N = Math.max(1, Number(d2.param('count', 12)) || 12)\n      const i = Number(d2.param('idx', 0)) || 0\n      const gap = Math.max(0.001, Number(d2.param('gap', 0.105)))\n      const coil = Number(d2.param('coil', 476))       // helix radius\n      const ySpan = Number(d2.param('ySpan', 1450))    // total vertical travel\n      const flatten = Number(d2.param('flatten', 0.6)) // 0 = constant pitch\n      const turns = Number(d2.param('turns', -1.5))    // signed revolutions\n      const phase0 = Number(d2.param('phase0', 270))   // 270 = enter at 9 o'clock\n      const tiltDeg = Number(d2.param('tilt', 8))      // lean of the whole axis\n      const wobble = Number(d2.param('wobble', 0))\n      const zOff = Number(d2.param('zOffset', -420))\n      const fade = Number(d2.param('fade', 0.12))\n      const peak = Number(d2.param('peak', 0.95))\n      const backDim = Number(d2.param('backDim', 0.28))\n      const edgeFade = Number(d2.param('edgeFade', 0.14))\n      const offX = Number(d2.param('offsetX', 0))\n      const offY = Number(d2.param('offsetY', 0))\n\n      // Fixed spacing: this card trails the previous one by `gap` runs. The\n      // whole chain cycles over `span`; never shorter than one run, or cards\n      // would wrap before reaching the end of the helix.\n      const span = Math.max(N * gap, 1)\n      const s = (((i * gap + S.t) % span) + span) % span\n\n      if (s > 1) {\n        // Parked off the helix, waiting its turn.\n        card.style.opacity = '0'\n        card.style.pointerEvents = 'none'\n        return\n      }\n      card.style.pointerEvents = 'auto'\n\n      // p = 0 at the bottom of the helix, 1 at the top.\n      const p = s\n      const angDeg = phase0 + p * 360 * turns\n      const a = angDeg * Math.PI / 180\n\n      const hx = Math.sin(a) * coil\n      const hz = Math.cos(a) * coil\n\n      // Variable pitch. The climb rate is w(u) = 1 - f * (1 + cos(angle))/2:\n      // fastest at the back (12 o'clock, cos = -1), slowest at the front\n      // (6 o'clock, cos = +1). Integrated in closed form and normalised, so\n      // the total climb still spans exactly ySpan however f is set.\n      const f = clamp01(flatten) * 0.95\n      const Om = turns * 2 * Math.PI\n      const A0 = phase0 * Math.PI / 180\n      const climbAt = (q) => (Math.abs(Om) < 1e-6\n        ? q\n        : (1 - f / 2) * q - (f / 2) * (Math.sin(A0 + q * Om) - Math.sin(A0)) / Om)\n      const total = climbAt(1) || 1\n      const hy = (0.5 - climbAt(p) / total) * ySpan // screen y is inverted\n\n      // Lean the whole helix axis.\n      const tr = tiltDeg * Math.PI / 180\n      const ct = Math.cos(tr), st = Math.sin(tr)\n      const x = hx * ct - hy * st + offX\n      const y = hx * st + hy * ct + offY\n      const z = hz + zOff\n\n      // rotateY by the helix angle points the card's +z normal radially\n      // outward — the whole front/back reveal falls out of this one line.\n      // With wobble at 0 this is a PURE yaw, which is what keeps the card's\n      // horizontal axis level instead of rolling it off the horizon.\n      const j2 = jitter(i, 2), j3 = jitter(i, 3)\n      const rotX = (j2 - 0.5) * wobble\n      const rotZ = (j3 - 0.5) * wobble\n\n      // Opacity: depth fade, backface dim, and a fade at both ends of the run\n      // so the helix's seam never shows.\n      const facing = Math.cos(a) // +1 toward camera, -1 away\n      const near = (hz + coil) / (2 * coil)\n      let op = fade + (peak - fade) * Math.pow(near, 1.15)\n      if (facing < 0) op *= backDim\n      op *= clamp01(Math.min(p, 1 - p) / Math.max(0.001, edgeFade))\n\n      card.style.transform =\n        'translate3d(' + x.toFixed(2) + 'px,' + y.toFixed(2) + 'px,' + z.toFixed(2) + 'px)' +\n        ' rotateY(' + angDeg.toFixed(2) + 'deg)' +\n        ' rotateX(' + rotX.toFixed(2) + 'deg)' +\n        ' rotateZ(' + rotZ.toFixed(2) + 'deg)'\n      card.style.opacity = op.toFixed(3)\n      card.style.zIndex = String(Math.round(1000 + z))\n    })\n  }\n}"}},"idx":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"idx"}}}},"count":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"count"}}}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"gap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gap"}}}},"coil":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coil"}}}},"ySpan":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ySpan"}}}},"flatten":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flatten"}}}},"turns":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"turns"}}}},"phase0":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"phase0"}}}},"tilt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tilt"}}}},"wobble":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobble"}}}},"persp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"persp"}}}},"zOffset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zOffset"}}}},"fade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fade"}}}},"peak":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"peak"}}}},"backDim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backDim"}}}},"edgeFade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"edgeFade"}}}},"offsetX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetX"}}}},"offsetY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetY"}}}}}}}}}},{"_id":"6a5ff1d53a42b34509a57dda","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wormhole-scene","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Scene — the helix of case cards, in WebGL.\n//\n// Units are deliberately CSS pixels: the camera sits at `persp` on +z with a\n// fov derived from the canvas height, so 1 world unit = 1 px at z = 0.\n//\n// GEOMETRY — a vertical helix. Cards rise while turning, each card's face\n// pointing radially OUTWARD from the axis, so a card on the far side shows\n// its back while passing behind the centre, then turns to face the viewer.\n// `turns` is signed; negative runs the other way round the clock.\n//\n// EVERYTHING VARIES ALONG THE RUN. Three separate envelopes, all keyed to\n// position p (0 = bottom of the helix, 1 = top) rather than to the rotation\n// angle, so they line up with what the viewer actually sees on screen:\n//   · pitch    — steep at both ends, flat through the middle (`flatten`)\n//   · bend     — cards curl hard at the ends, relax in the middle\n//   · wobble   — random tilt is wide at the ends, nearly level in the middle\n// The middle of the run is where cards cross the main viewing window, so it\n// stays calm and readable; the ends are where they should feel like they're\n// spilling out of frame.\n//\n// The bend lives in a VERTEX SHADER, not in the geometry. All cards share one\n// flat plane; each material carries its own `uBendR` uniform. Baking the curve\n// into vertices would mean rebuilding geometry every frame to vary it per card.\n//\n// SPACING — cards sit a FIXED `gap` apart, so the composition never changes\n// with the record count; adding records only lengthens the cycle.\n\nconst clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)\n\n// Half-width of the arc over which a card cross-fades between its lit face\n// and its dark back, measured in cos(angle). Larger = softer handover.\nconst FACE_BAND = 0.35\n\n// Cards never fade all the way out — they leave the frame still visible, and\n// are only recycled once they are genuinely outside it. Requires `ySpan` big\n// enough that both ends of the run sit off-screen, or this pops.\nconst EDGE_FLOOR = 0.35\n\n// Deterministic per-card randomness — stable across frames, so a card keeps\n// its own character instead of flickering.\nconst jitter = (i, salt) => {\n  const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453\n  return v - Math.floor(v)\n}\nconst signed = (i, salt) => jitter(i, salt) * 2 - 1\n\nexport default {\n  css: `\n    .wh-wrap { position: absolute; inset: 0; }\n    .wh-canvas { display: block; width: 100%; height: 100%; }\n\n    /* Glass pill that trails the cursor. The rim highlight is inset white\n       box-shadows rather than an SVG filter, so it needs no <filter> def in\n       the document and survives being dropped into any system. */\n    /* Two elements on purpose: the outer one is the POSITIONER, whose\n       transform is rewritten every frame to trail the cursor. The inner one\n       owns the entrance/exit, so its scale animation never fights the\n       positioning transform. */\n    .wh-tip {\n      position: absolute; left: 0; top: 0; pointer-events: none;\n      display: flex; align-items: center; height: 58px;\n      will-change: transform;\n    }\n    /* The pill OPENS: it starts as a circle the height of the bar and grows\n       its width rightwards, clipping the content like a curtain. Width is\n       animated (not scaleX) because scaleX would stretch the letterforms. */\n    .wh-tip-glass {\n      position: relative; overflow: hidden;\n      height: 14px; width: 14px; border-radius: 29px;\n      font-family: Inter, -apple-system, sans-serif;\n      /* Cool cast, kept subtle: the body lifts a little toward blue and the\n         highlights go from pure white to a cold white. The highlights carry\n         most of the icy read — tinting them costs no weight, whereas pushing\n         the body further would just make the pill heavier. */\n      background:\n        linear-gradient(155deg, rgba(216,236,255,0.11), rgba(216,236,255,0) 46%),\n        rgba(10, 16, 24, 0.44);\n      backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      -webkit-backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      /* Glass takes light in at the top but CONCENTRATES it on the way out,\n         so the bottom rim is the brighter of the two — a sharp line plus a\n         soft bloom just above it. That bottom edge is what separates the pill\n         from a dark scene. */\n      /* Whole-pixel offsets on purpose. Fractional ones (0.5px) rasterise\n         with partial coverage, which frays on a rounded edge and shimmers\n         while the pill animates its width and height — each frame lands on a\n         different subpixel phase. Thinness comes from the alpha instead. */\n      box-shadow: inset 0 1px 0 rgba(226,241,255,0.22),\n                  inset 1px 0 0 rgba(226,241,255,0.10),\n                  inset -1px 0 0 rgba(226,241,255,0.10),\n                  inset 0 -1px 0 rgba(226,241,255,0.26),\n                  inset 0 -7px 11px -7px rgba(216,236,255,0.22),\n                  0 6px 16px rgba(0,0,0,0.24);\n      opacity: 0;\n      /* The close is the open played backwards. Same durations; the curve is\n         the opening one reflected through time — cubic-bezier(x1,y1,x2,y2)\n         reverses to cubic-bezier(1-x2, 1-y2, 1-x1, 1-y1) — and the delays\n         mirror too: opacity FINISHES 32ms before the size on the way in, so\n         it STARTS after it on the way out.\n\n         The opacity is the ONE thing not mirrored, deliberately. Perceived\n         opacity is not linear — anything under ~30% over a dark scene already\n         reads as gone — so a symmetric fade made the pill look like it\n         vanished before it finished shutting. It now holds full strength\n         through most of the close and drops only at the very end. */\n      transition: width 129ms cubic-bezier(.7, 0, .6, .25),\n                  height 129ms cubic-bezier(.7, 0, .6, .25),\n                  opacity 65ms ease-in 64ms;\n      will-change: width, height, opacity;\n    }\n    .wh-tip.on .wh-tip-glass {\n      opacity: 1; height: 58px;\n      /* Both axes share one curve. The earlier split existed because the old\n         ease was heavily front-loaded, which made the short axis look like it\n         arrived first; this curve is gentle enough that the two read as one\n         gesture without compensating. */\n      /* Opacity is deliberately SLOWER than the size here, against the grain\n         of the rest of the timing. Fading in faster than the pill opens meant\n         the closed 14px ball was already fully opaque on the first frame —\n         you saw the ball before you saw it open. Ramping opacity across most\n         of the expansion hides the starting shape. */\n      /* Both axes share one curve. Giving height a lazier one to make it lag\n         was tried and reverted — it read as a glitch, not as depth. */\n      transition: width 129ms cubic-bezier(.4, .75, .3, 1),\n                  height 129ms cubic-bezier(.4, .75, .3, 1),\n                  opacity 102ms ease-out;\n    }\n\n    /* No rim ring here on purpose: a masked band of differently-filtered\n       backdrop reads as a hard grey border, not as glass. The 1px inset\n       highlights below carry the edge instead. */\n\n    /* Absolutely positioned so it keeps its natural width while the glass\n       clips it — which is also how its width gets measured. */\n    /* Fixed height and vertically centred, so the growing glass CLIPS it\n       instead of squashing it while the pill opens. */\n    .wh-tip-inner {\n      position: absolute; left: 0; top: 50%; height: 58px;\n      transform: translateY(-50%);\n      display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;\n      align-content: center; column-gap: 10px;\n      padding: 0 17px 0 14px; white-space: nowrap;\n      opacity: 0; transition: opacity 55ms ease-in 60ms;\n    }\n    .wh-tip.on .wh-tip-inner { opacity: 1; transition: opacity 57ms ease-out 31ms; }\n\n    /* SVG rather than the ↗ character: glyph metrics differ per font (and can\n       fall back to another face entirely), so the box centres but the ink\n       does not. Geometry here is exact. */\n    .wh-tip-arrow { grid-row: 1 / span 2; align-self: center;\n                    display: flex; align-items: center; justify-content: center;\n                    color: #f4f4f5; }\n    .wh-tip-name { font-size: 14px; font-weight: 600; color: #f4f4f5; line-height: 1.3;\n                   text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n    .wh-tip-cat { font-size: 13px; color: #cfcfd6; line-height: 1.3;\n                  text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n  `,\n\n  // No reactive reads in here — a canvas component must keep render() static\n  // or the element gets torn down and resized out from under the renderer.\n  render() {\n    return '<div class=\"wh-wrap\" data-d2-static>' +\n      '<canvas class=\"wh-canvas\"></canvas>' +\n      '<div class=\"wh-tip\" data-tip>' +\n      '<div class=\"wh-tip-glass\" data-tip-glass>' +\n      '<div class=\"wh-tip-inner\" data-tip-inner>' +\n      '<span class=\"wh-tip-arrow\">' +\n      '<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" aria-hidden=\"true\">' +\n      '<path d=\"M4.6 11.4 11.4 4.6M6.2 4.6h5.2v5.2\" fill=\"none\" stroke=\"currentColor\"' +\n      ' stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>' +\n      '</svg></span>' +\n      '<span class=\"wh-tip-name\" data-tip-name></span>' +\n      '<span class=\"wh-tip-cat\" data-tip-cat></span>' +\n      '</div>' +\n      '</div>' +\n      '</div>' +\n      '</div>'\n  },\n\n  mounted(d2, root) {\n    const canvas = root.querySelector('.wh-canvas')\n    if (!canvas) return\n\n    let THREE = null\n    let renderer = null\n    let scene = null\n    let camera = null\n    let raycaster = null\n    let pointer = null\n    let geometry = null\n    let cards = []\n    let builtSig = ''\n    let geoSig = ''\n    let t = 0\n    let boost = 0\n    let last = 0\n    let tick = 0\n    let hoverRec = null\n    let pointerIn = false\n    let mouseX = 0\n    let mouseY = 0\n    let canvasRect = null\n    const hitList = []\n    let sizedOk = false\n    let onScreen = true\n    let needResize = true\n    const texCache = new Map()\n\n    // Scratch objects, allocated once — building the orientation per card per\n    // frame with fresh quaternions would churn the GC.\n    let qYaw, qPitch, qRoll, axisX, axisY, axisZ\n\n    // ---- helpers ---------------------------------------------------------\n\n    const BEND_CHUNK = [\n      'vec3 transformed = vec3( position );',\n      'float phi = transformed.x / uBendR;',\n      'transformed.x = uBendR * sin( phi );',\n      'transformed.z += uBendR * cos( phi ) - uBendR;'\n    ].join('\\n')\n\n    const makeMaterial = () => {\n      const m = new THREE.MeshBasicMaterial({\n        side: THREE.DoubleSide,\n        transparent: true,\n        depthWrite: false,\n        opacity: 0\n      })\n      m.onBeforeCompile = (shader) => {\n        shader.uniforms.uBendR = { value: 1000 }\n        shader.vertexShader = 'uniform float uBendR;\\n' + shader.vertexShader\n        shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', BEND_CHUNK)\n        m.userData.shader = shader\n      }\n      return m\n    }\n\n    const disposeCards = () => {\n      for (const c of cards) {\n        scene.remove(c.mesh)\n        if (c.material) c.material.dispose()\n      }\n      cards = []\n    }\n\n    // Textures are shared BY URL. The pool routinely points many records at\n    // the same file, and TextureLoader keeps no cache of its own (THREE.Cache\n    // is opt-in and off), so without this every card decodes and uploads its\n    // own copy of the same image.\n    const texFor = (url) => {\n      let tex = texCache.get(url)\n      if (!tex) {\n        const loader = new THREE.TextureLoader()\n        loader.setCrossOrigin('anonymous')\n        tex = loader.load(url)\n        tex.colorSpace = THREE.SRGBColorSpace\n        tex.anisotropy = renderer.capabilities.getMaxAnisotropy()\n        texCache.set(url, tex)\n      }\n      return tex\n    }\n\n    const pruneTextures = (keep) => {\n      for (const [url, tex] of texCache) {\n        if (!keep.has(url)) { tex.dispose(); texCache.delete(url) }\n      }\n    }\n\n    const buildCards = (records) => {\n      disposeCards()\n      const used = new Set()\n\n      records.forEach((rec) => {\n        const material = makeMaterial()\n        if (rec.thumbnail) {\n          material.map = texFor(rec.thumbnail)\n          used.add(rec.thumbnail)\n        }\n        const mesh = new THREE.Mesh(geometry, material)\n        mesh.visible = false\n        mesh.userData.record = rec\n        scene.add(mesh)\n        cards.push({ mesh, material, record: rec })\n      })\n\n      pruneTextures(used)\n    }\n\n    // A click hands the record to the UI store; the modal is built from\n    // native blocks bound to that store, so it stays editable in the builder.\n    const openCard = (rec) => {\n      if (!rec) return\n      const s = d2.store('wh-ui')\n      if (!s) return\n      const tags = Array.isArray(rec.tags) ? rec.tags.join('  ·  ') : (rec.tags || '')\n      const useArtboard = rec.openMode === 'artboard' && !!rec.artboardRef\n      s.set('name', rec.name || '')\n      s.set('category', rec.category || '')\n      s.set('tags', tags)\n      s.set('thumb', rec.thumbnail || '')\n      s.set('artboardRef', rec.artboardRef || '')\n      s.set('mode', useArtboard ? 'artboard' : 'thumb')\n      s.set('modalOpen', true)\n    }\n\n    // ---- init ------------------------------------------------------------\n\n    const init = () => {\n      renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })\n      renderer.setClearAlpha(0)\n      renderer.outputColorSpace = THREE.SRGBColorSpace\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))\n\n      scene = new THREE.Scene()\n      camera = new THREE.PerspectiveCamera(45, 1, 1, 12000)\n      raycaster = new THREE.Raycaster()\n      pointer = new THREE.Vector2()\n\n      qYaw = new THREE.Quaternion()\n      qPitch = new THREE.Quaternion()\n      qRoll = new THREE.Quaternion()\n      axisX = new THREE.Vector3(1, 0, 0)\n      axisY = new THREE.Vector3(0, 1, 0)\n      axisZ = new THREE.Vector3(0, 0, 1)\n\n      d2.on(window, 'wheel', (e) => {\n        const dy = e.deltaY || 0\n        boost = Math.max(-1.4, Math.min(1.4, boost + dy * 0.00035))\n      }, { passive: true })\n\n      // The click reuses whatever the hover pass already resolved, so what\n      // opens is always exactly what the tooltip was naming.\n      d2.on(canvas, 'click', () => { if (hoverRec) openCard(hoverRec) })\n\n      const onPointer = (e) => {\n        // Rect is cached: calling getBoundingClientRect per pointer event is a\n        // forced layout read at input frequency, and it sat directly in the\n        // path between the mouse moving and the pill being repositioned.\n        if (!canvasRect) refreshRect()\n        mouseX = e.clientX - canvasRect.left\n        mouseY = e.clientY - canvasRect.top\n        pointer.x = (mouseX / canvasRect.width) * 2 - 1\n        pointer.y = -(mouseY / canvasRect.height) * 2 + 1\n        pointerIn = true\n        placeTip()\n      }\n\n      // `pointermove` is COALESCED — the browser merges everything that\n      // happened since the last frame into one event, so the pill is always\n      // drawn at a position the cursor has already left. Multiplied by the\n      // cursor's speed, that is the elastic trailing. `pointerrawupdate`\n      // fires at the raw sampling rate instead; pointermove stays as the\n      // fallback where it isn't supported.\n      if ('onpointerrawupdate' in canvas) {\n        d2.on(canvas, 'pointerrawupdate', onPointer)\n      }\n      d2.on(canvas, 'pointermove', onPointer)\n      d2.on(window, 'scroll', refreshRect, { passive: true })\n\n      d2.on(canvas, 'pointerleave', () => {\n        pointerIn = false\n        hoverRec = null\n        canvas.style.cursor = 'default'\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n      })\n    }\n\n    const tip = root.querySelector('[data-tip]')\n    const tipGlass = root.querySelector('[data-tip-glass]')\n    const tipInner = root.querySelector('[data-tip-inner]')\n    const tipName = root.querySelector('[data-tip-name]')\n    const tipCat = root.querySelector('[data-tip-cat]')\n    const TIP_H = 58   // open pill height, and the positioner's fixed box\n    const TIP_MIN = 14 // closed pill is a ball of this diameter\n    let tipW = TIP_H\n\n    const placeTip = () => {\n      // Deliberately NOT gated on hoverRec: while the pill is animating shut\n      // there is no hovered card, and bailing here is what used to freeze it\n      // in place instead of letting it trail the cursor until it vanishes.\n      if (!tip) return\n      // Uses the TARGET width, not the live one — mid-animation the pill is\n      // still opening and the edge-flip would jitter.\n      const w = tipW\n      const h = TIP_H\n      const cw = canvas.clientWidth\n      const ch = canvas.clientHeight\n      let x = mouseX + 18\n      let y = mouseY - h / 2\n      // Flip to the other side of the cursor rather than run off the edge.\n      if (x + w > cw - 10) x = mouseX - 18 - w\n      if (y < 10) y = 10\n      if (y + h > ch - 10) y = ch - 10 - h\n      tip.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'\n    }\n\n    const setHover = (rec) => {\n      if (rec === hoverRec) return\n      hoverRec = rec\n      canvas.style.cursor = rec ? 'pointer' : 'default'\n      // Text is deliberately NOT cleared on hide — it has to stay readable\n      // through the exit animation.\n      if (!rec) {\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px' // shut back to a ball\n        return\n      }\n      if (tipName) tipName.textContent = rec.name || ''\n      if (tipCat) tipCat.textContent = rec.category || ''\n      // The inner keeps its natural width inside the clipped glass, so this\n      // reads the width the pill should open to.\n      tipW = tipInner ? tipInner.offsetWidth : TIP_H\n      placeTip()\n      if (tipGlass) tipGlass.style.width = tipW + 'px'\n      if (tip) tip.classList.add('on')\n    }\n\n    const refreshRect = () => { canvasRect = canvas.getBoundingClientRect() }\n\n    const resize = () => {\n      const w = canvas.clientWidth || 1\n      const h = canvas.clientHeight || 1\n      refreshRect()\n      sizedOk = w > 1 && h > 1\n      const persp = Number(d2.param('persp', 1400)) || 1400\n      renderer.setSize(w, h, false)\n      camera.fov = 2 * Math.atan((h / 2) / persp) * 180 / Math.PI\n      camera.aspect = w / h\n      camera.position.set(0, 0, persp)\n      camera.updateProjectionMatrix()\n    }\n\n    // ---- frame -----------------------------------------------------------\n\n    const frame = (now) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016\n      last = now\n\n      const speed = Number(d2.param('speed', 0.045))\n      const gap = Math.max(0.001, Number(d2.param('gap', 0.108)))\n      const coil = Number(d2.param('coil', 520))\n      const ySpan = Number(d2.param('ySpan', 1450))\n      const flatten = Number(d2.param('flatten', 0.66))\n      const bendMid = Number(d2.param('bend', 0.7))\n      const bendEnd = Number(d2.param('bendEnds', 1.8))\n      const wobEnd = Number(d2.param('wobble', 14))\n      const wobMid = Number(d2.param('wobbleMid', 3.5))\n      const segments = Math.max(2, Math.min(96, Math.round(Number(d2.param('segments', 40)))))\n      const cardW = Number(d2.param('cardW', 448))\n      const cardH = Number(d2.param('cardH', 283))\n      const turns = Number(d2.param('turns', -1.5))\n      const phase0 = Number(d2.param('phase0', 270))\n      const tiltDeg = Number(d2.param('tilt', 8))\n      const zOff = Number(d2.param('zOffset', -420))\n      const fade = Number(d2.param('fade', 0.12))\n      const peak = Number(d2.param('peak', 0.95))\n      const backDim = Number(d2.param('backDim', 0.28))\n      const edgeFade = Number(d2.param('edgeFade', 0.14))\n      const offX = Number(d2.param('offsetX', 0))\n      const offY = Number(d2.param('offsetY', 0))\n\n      t += (speed + boost) * dt\n      boost *= Math.pow(0.12, dt)\n\n      // Geometry is a plain flat plane now — the curve happens in the shader,\n      // so this only rebuilds when the card's SIZE or tessellation changes.\n      const gs = cardW + 'x' + cardH + '|' + segments\n      if (gs !== geoSig) {\n        const old = geometry\n        geometry = new THREE.PlaneGeometry(cardW, cardH, segments, 1)\n        for (const c of cards) c.mesh.geometry = geometry\n        if (old) old.dispose()\n        geoSig = gs\n      }\n\n      // The record set changes maybe once a session. Polling it and building\n      // a signature string every frame was pure garbage — a few times a\n      // second is plenty, and the first tick still builds immediately.\n      if ((tick++ % 20) === 0) {\n        const handle = d2.data()\n        // `active: false` pulls a card out of the pool entirely, and the chain\n        // follows the panel's order. Both are applied BEFORE the signature, so\n        // toggling either one is picked up as a change and rebuilds the meshes.\n        const records = ((handle && handle.records()) || [])\n          .filter((r) => r.active !== false)\n          .slice()\n          .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n        const sig = records.map((r) => (r._id || '') + ':' + (r.thumbnail || '')).join('|')\n        if (sig !== builtSig) {\n          buildCards(records)\n          builtSig = sig\n        }\n      }\n\n      const N = Math.max(1, cards.length)\n      const span = Math.max(N * gap, 1)\n\n      // Pitch envelope: w(q) = 1 - f * sin^2(pi q), integrated in closed form\n      // and normalised, so `flatten` redistributes WHERE the climb happens\n      // without changing the total travel (still exactly ySpan).\n      const f = clamp01(flatten) * 0.95\n      const climbAt = (q) => q * (1 - f / 2) + f * Math.sin(2 * Math.PI * q) / (4 * Math.PI)\n      const totalClimb = climbAt(1) || 1\n\n      const tr = tiltDeg * Math.PI / 180\n      const ct = Math.cos(tr), st = Math.sin(tr)\n\n      for (let i = 0; i < cards.length; i++) {\n        const c = cards[i]\n        const s = (((i * gap + t) % span) + span) % span\n\n        if (s > 1) { c.mesh.visible = false; continue }\n        c.mesh.visible = true\n\n        const p = s\n        const angDeg = phase0 + p * 360 * turns\n        const a = angDeg * Math.PI / 180\n\n        const hx = Math.sin(a) * coil\n        const hz = Math.cos(a) * coil\n        const hy = (0.5 - climbAt(p) / totalClimb) * ySpan\n\n        // Screen space (y grows downward)…\n        const x = hx * ct - hy * st + offX\n        const y = hx * st + hy * ct + offY\n\n        // …and three.js grows y upward, hence the negation.\n        c.mesh.position.set(x, -y, hz + zOff)\n\n        // ends = 1 at both extremities of the run, 0 at the middle. Drives\n        // both the curl and the random tilt, so cards get wilder as they\n        // enter and leave, and settle down as they cross the screen.\n        const ends = Math.cos(Math.PI * p) * Math.cos(Math.PI * p)\n\n        const bend = bendMid + (bendEnd - bendMid) * ends\n        const bendR = Math.min(80000, coil / Math.max(0.001, Math.abs(bend)))\n        const sh = c.material.userData.shader\n        if (sh) sh.uniforms.uBendR.value = bendR\n\n        // Orientation: yaw first (that is what points the face outward), then\n        // a small pitch and roll about the WORLD axes so the random tilt reads\n        // as a tilt on screen rather than a twist in the card's own frame.\n        const wob = (wobMid + (wobEnd - wobMid) * ends) * Math.PI / 180\n        qYaw.setFromAxisAngle(axisY, a)\n        qPitch.setFromAxisAngle(axisX, signed(i, 2) * wob * 0.6)\n        qRoll.setFromAxisAngle(axisZ, signed(i, 1) * wob)\n        c.mesh.quaternion.copy(qRoll).multiply(qPitch).multiply(qYaw)\n\n        // Depth and facing both DARKEN the card — neither may touch alpha.\n        // Fading with alpha lets the cards behind show through; these are\n        // meant to read as solid sheets, and against a black background\n        // darkening looks identical anyway.\n        const near = (hz + coil) / (2 * coil)\n        const depthShade = fade + (peak - fade) * Math.pow(near, 1.15)\n\n        // Smooth handover between the lit face and the dark back. Testing the\n        // sign of `facing` popped: a CURVED card still shows plenty of surface\n        // at the moment it passes edge-on, so the step was visible.\n        const facing = Math.cos(a)\n        const bt = clamp01((facing + FACE_BAND) / (2 * FACE_BAND))\n        const faceShade = backDim + (1 - backDim) * bt * bt * (3 - 2 * bt)\n\n        c.material.color.setScalar(depthShade * faceShade)\n\n        // Alpha is reserved for the ends of the run, so a card genuinely\n        // dissolves as it leaves instead of turning into a black rectangle.\n        const ramp = clamp01(Math.min(p, 1 - p) / Math.max(0.001, edgeFade))\n        const edge = EDGE_FLOOR + (1 - EDGE_FLOOR) * ramp\n        c.material.opacity = edge\n        // Solid cards must write depth to occlude each other correctly; only\n        // the dissolving ones opt out.\n        c.material.depthWrite = edge > 0.98\n      }\n\n      // Hover is resolved ONCE PER FRAME rather than on pointermove: the\n      // cards are in motion, so the card beneath a stationary cursor changes\n      // on its own and a move-only test would go stale.\n      if (pointerIn && raycaster) {\n        hitList.length = 0\n        for (const c of cards) if (c.mesh.visible) hitList.push(c.mesh)\n        raycaster.setFromCamera(pointer, camera)\n        const hits = raycaster.intersectObjects(hitList, false)\n        setHover(hits.length ? hits[0].object.userData.record : null)\n      }\n\n      renderer.render(scene, camera)\n    }\n\n    // ---- loop ------------------------------------------------------------\n\n    let started = false\n\n    // Observers instead of per-frame measurement: reading clientWidth inside\n    // the render loop forces a layout recalc every frame.\n    const ro = new ResizeObserver(() => { needResize = true })\n    ro.observe(canvas)\n\n    // Don't render a hero nobody is looking at — this starts mattering the\n    // moment the page grows past the fold.\n    const io = new IntersectionObserver((entries) => {\n      for (const e of entries) onScreen = e.isIntersecting\n    }, { threshold: 0 })\n    io.observe(canvas)\n\n    d2.raf((now) => {\n      if (!started) {\n        THREE = d2.lib('three@0.170.0')\n        if (!THREE) return // still loading\n        init()\n        started = true\n      }\n      if (!onScreen) { last = 0; return } // reset dt so it doesn't jump back\n      if (needResize) { needResize = false; resize() }\n      if (sizedOk) frame(now)\n    })\n\n    d2.onUnmount(() => {\n      ro.disconnect()\n      io.disconnect()\n      if (scene) disposeCards()\n      pruneTextures(new Set())\n      if (geometry) geometry.dispose()\n      if (renderer) renderer.dispose()\n    })\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"gap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gap"}}}},"coil":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coil"}}}},"ySpan":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ySpan"}}}},"flatten":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flatten"}}}},"bend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bend"}}}},"bendEnds":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bendEnds"}}}},"wobble":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobble"}}}},"wobbleMid":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobbleMid"}}}},"segments":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"segments"}}}},"cardW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardW"}}}},"cardH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardH"}}}},"turns":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"turns"}}}},"phase0":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"phase0"}}}},"tilt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tilt"}}}},"persp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"persp"}}}},"zOffset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zOffset"}}}},"fade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fade"}}}},"peak":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"peak"}}}},"backDim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backDim"}}}},"edgeFade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"edgeFade"}}}},"offsetX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetX"}}}},"offsetY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetY"}}}}}}}}}},{"_id":"6a5fff2ee94b8d0f695227dd","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wormhole-artboard-frame","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Hosts a LIVE data2 artboard by pointing an iframe at the runtime's\n// per-artboard route. Same-origin framing is allowed, so the artboard renders\n// interactive, and the reference can come from a record — no per-case\n// component registration needed.\n//\n// The iframe is held at the artboard's DESIGN size and CSS-scaled to fit the\n// slot. That distinction matters: an artboard renders at 100% of its\n// container, so sizing the iframe to the slot would make the case REFLOW into\n// a narrow canvas instead of shrinking as a picture of itself.\n\nexport default {\n  css: `\n    .af-wrap { position: absolute; inset: 0; overflow: hidden; }\n    .af-frame { border: 0; display: block; position: absolute; left: 0; top: 0;\n                transform-origin: top left; }\n    .af-empty { position: absolute; inset: 0; display: flex; align-items: center;\n                justify-content: center; font: 14px Inter, sans-serif; color: #55697c; }\n  `,\n\n  render(d2) {\n    const ref = d2.param('artboardRef', '')\n    if (!ref) return '<div class=\"af-wrap\"><div class=\"af-empty\">no artboard reference</div></div>'\n    return '<div class=\"af-wrap\"><iframe class=\"af-frame\" data-af src=\"/preview/' + ref + '\"' +\n           ' referrerpolicy=\"same-origin\"></iframe></div>'\n  },\n\n  mounted(d2, root) {\n    const wrap = root.querySelector('.af-wrap')\n    const frame = root.querySelector('[data-af]')\n    if (!wrap || !frame) return\n\n    const fit = () => {\n      const dw = Number(d2.param('designW', 1440)) || 1440\n      const dh = Number(d2.param('designH', 900)) || 900\n      const w = wrap.clientWidth\n      const h = wrap.clientHeight\n      if (!w || !h) return\n      // Contain, and centre whatever slack is left over.\n      const s = Math.min(w / dw, h / dh)\n      frame.style.width = dw + 'px'\n      frame.style.height = dh + 'px'\n      frame.style.transform =\n        'translate(' + ((w - dw * s) / 2).toFixed(2) + 'px,' +\n        ((h - dh * s) / 2).toFixed(2) + 'px) scale(' + s.toFixed(4) + ')'\n    }\n\n    fit()\n    const ro = new ResizeObserver(fit)\n    ro.observe(wrap)\n    d2.onUnmount(() => ro.disconnect())\n  }\n}"}},"artboardRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"artboardRef"}}}},"designW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"designW"}}}},"designH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"designH"}}}}}}}}}},{"_id":"6a6007073a42b34509a5c910","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wormhole-admin","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Admin — the control panel for the card pool.\n//\n// Text inputs are deliberately UNCONTROLLED: their values are read from the\n// DOM on save rather than mirrored into state on every keystroke. Keeping the\n// draft in state would re-render the form on each character, which fights the\n// morph for cursor position. Only DISCRETE values (tags, open mode, thumbnail\n// URL, active) live in state, because those change in one step.\n\nconst esc = (s) => String(s == null ? '' : s)\n  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n  .replace(/\"/g, '&quot;')\n\nconst asTags = (v) => (Array.isArray(v) ? v : (typeof v === 'string' && v ? v.split(',') : []))\n  .map((t) => String(t).trim()).filter(Boolean)\n\nexport default {\n  css: `\n    :host, .wa { font-family: Inter, -apple-system, sans-serif; }\n    .wa { position: absolute; inset: 0; display: grid; grid-template-columns: 430px 1fr;\n          background: #0a0d11; color: #dbe4ec; overflow: hidden; }\n\n    .wa-list { border-right: 1px solid #1b232c; display: flex; flex-direction: column; min-height: 0; }\n    .wa-bar { display: flex; align-items: center; gap: 10px; padding: 16px; border-bottom: 1px solid #1b232c; }\n    .wa-bar h2 { font-size: 15px; font-weight: 600; margin: 0; flex: 1; letter-spacing: .01em; }\n    .wa-rows { overflow-y: auto; flex: 1; min-height: 0; }\n\n    .row { display: grid; grid-template-columns: 18px 56px 1fr auto; gap: 12px; align-items: center;\n           padding: 10px 16px; cursor: pointer; border-bottom: 1px solid #131a21; }\n    .row.dragging { opacity: .3; }\n    .handle { color: #43566a; font-size: 14px; line-height: 1; text-align: center;\n              cursor: grab; user-select: none; }\n    .handle:active { cursor: grabbing; }\n    .row:hover { background: #101720; }\n    .row.sel { background: #14202b; box-shadow: inset 2px 0 0 #4c9aff; }\n    .row-thumb { width: 56px; height: 35px; border-radius: 4px; background: #161d25 center/cover no-repeat; }\n    .row-name { font-size: 13px; font-weight: 500; color: #eaf1f7; }\n    .row-meta { font-size: 11px; color: #6b8096; margin-top: 2px; }\n    .row-flag { font-size: 10px; letter-spacing: .06em; text-transform: uppercase;\n                padding: 3px 7px; border-radius: 999px; background: #1b2530; color: #7d93a8; }\n    .row-flag.ab { background: #14304a; color: #7ab6ff; }\n    .row.off { opacity: .45; }\n\n    .wa-form { padding: 22px 26px; overflow-y: auto; min-height: 0; }\n    .wa-empty { display: flex; align-items: center; justify-content: center; height: 100%;\n                color: #4e6070; }\n    .fld { margin-bottom: 16px; }\n    .fld > label { display: block; font-size: 11px; letter-spacing: .07em; text-transform: uppercase;\n                   color: #6b8096; margin-bottom: 6px; }\n    input[type=text], input[type=number], select {\n      width: 100%; box-sizing: border-box; background: #10161d; border: 1px solid #212b36;\n      border-radius: 7px; color: #e6eef5; font-size: 13px; padding: 9px 11px; font-family: inherit; }\n    input:focus, select:focus { outline: none; border-color: #3d78c9; }\n    input:disabled { opacity: .4; }\n\n    .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; min-height: 4px; }\n    .chip { display: inline-flex; align-items: center; gap: 6px; background: #1a2530; color: #b9cbdb;\n            border-radius: 999px; padding: 4px 6px 4px 11px; font-size: 12px; }\n    .chip b { cursor: pointer; color: #7a8fa3; font-weight: 400; padding: 0 3px; }\n    .chip b:hover { color: #ff7a6b; }\n\n    .two { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\n    .thumbrow { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }\n    .preview { width: 100%; height: 132px; border-radius: 8px; margin-top: 8px;\n               background: #10161d center/contain no-repeat; border: 1px solid #1c242e; }\n\n    button { font-family: inherit; font-size: 13px; border-radius: 7px; cursor: pointer;\n             border: 1px solid #263140; background: #151d26; color: #cfdde9; padding: 9px 14px; }\n    button:hover { background: #1b2531; }\n    button.primary { background: #2f6fd0; border-color: #2f6fd0; color: #fff; }\n    button.primary:hover { background: #3b7ee0; }\n    button.danger { color: #ff8f80; border-color: #3a2528; }\n    button.danger:hover { background: #2a1b1e; }\n    button:disabled { opacity: .5; cursor: default; }\n\n    .actions { display: flex; gap: 10px; margin-top: 22px; padding-top: 18px; border-top: 1px solid #1b232c; }\n    .spacer { flex: 1; }\n    .msg { font-size: 12px; color: #6f9f7c; align-self: center; }\n    .msg.err { color: #d98a80; }\n    .hint { font-size: 11px; color: #5b6f83; margin-top: 6px; line-height: 1.5; }\n    input[type=file] { display: none; }\n  `,\n\n  render(d2) {\n    const h = d2.data()\n    // The list IS the order — sorted by `order`, which drag-and-drop rewrites.\n    const records = ((h && h.records()) || []).slice()\n      .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n    const sel = d2.state.selectedId\n    const draft = d2.state.draft || null\n\n    const rows = records.map((r) => {\n      const tags = asTags(r.tags)\n      const rmode = r.openMode || 'thumb'\n      return (\n        '<div class=\"row' + (r._id === sel ? ' sel' : '') + (r.active === false ? ' off' : '') +\n        '\" data-row draggable=\"true\" data-key=\"' + esc(r._id) + '\">' +\n        '<div class=\"handle\">⠿</div>' +\n        '<div class=\"row-thumb\" style=\"background-image:url(' + esc(r.thumbnail || '') + ')\"></div>' +\n        '<div><div class=\"row-name\">' + esc(r.name || 'Untitled') + '</div>' +\n        '<div class=\"row-meta\">' + esc(r.category || '—') +\n        (tags.length ? '  ·  ' + esc(tags.join(', ')) : '') + '</div></div>' +\n        '<div class=\"row-flag' + (rmode === 'thumb' ? '' : ' ab') + '\">' + esc(rmode) + '</div>' +\n        '</div>'\n      )\n    }).join('')\n\n    let form\n    if (!draft) {\n      form = '<div class=\"wa-empty\">Select a card, or create a new one.</div>'\n    } else {\n      const tags = asTags(draft.tags)\n      const dmode = draft.openMode || 'thumb'\n      const isAb = dmode === 'artboard'\n      const isApp = dmode === 'app'\n      form =\n        '<div class=\"fld\"><label>Name</label>' +\n        '<input type=\"text\" data-f=\"name\" value=\"' + esc(draft.name) + '\"></div>' +\n\n        '<div class=\"fld\"><label>Category</label>' +\n        '<input type=\"text\" data-f=\"category\" value=\"' + esc(draft.category) + '\">' +\n        '<div class=\"hint\">Order is the list order — drag rows on the left to change it.</div></div>' +\n\n        '<div class=\"fld\"><label>Tags</label>' +\n        '<div class=\"chips\">' + tags.map((t, i) =>\n          '<span class=\"chip\">' + esc(t) + '<b data-untag=\"' + i + '\">×</b></span>').join('') +\n        '</div>' +\n        '<input type=\"text\" data-tagin placeholder=\"Type a tag and press Enter\">' +\n        '</div>' +\n\n        '<div class=\"fld\"><label>Thumbnail</label>' +\n        '<div class=\"thumbrow\">' +\n        '<input type=\"text\" data-f=\"thumbnail\" value=\"' + esc(draft.thumbnail) + '\">' +\n        '<button data-pick>Upload…</button>' +\n        '</div>' +\n        '<input type=\"file\" data-file accept=\"image/*\">' +\n        '<div class=\"preview\" style=\"background-image:url(' + esc(draft.thumbnail || '') + ')\"></div>' +\n        '</div>' +\n\n        '<div class=\"two\">' +\n        '<div class=\"fld\"><label>On click</label>' +\n        '<select data-mode>' +\n        '<option value=\"thumb\"' + (dmode === 'thumb' ? ' selected' : '') + '>Open thumbnail</option>' +\n        '<option value=\"artboard\"' + (dmode === 'artboard' ? ' selected' : '') + '>Open artboard</option>' +\n        '<option value=\"app\"' + (dmode === 'app' ? ' selected' : '') + '>Open app (URL)</option>' +\n        '</select></div>' +\n        '<div class=\"fld\"><label>Active</label>' +\n        '<select data-active>' +\n        '<option value=\"yes\"' + (draft.active === false ? '' : ' selected') + '>Yes</option>' +\n        '<option value=\"no\"' + (draft.active === false ? ' selected' : '') + '>No</option>' +\n        '</select></div>' +\n        '</div>' +\n\n        '<div class=\"fld\"><label>Artboard reference</label>' +\n        '<input type=\"text\" data-f=\"artboardRef\" value=\"' + esc(draft.artboardRef) + '\"' +\n        (isAb ? '' : ' disabled') + '>' +\n        '<div class=\"hint\">Paste the artboard’s reference from the builder. Design it at ' +\n        '1440×900 — the modal scales it to fit, it does not reflow it.</div></div>' +\n\n        '<div class=\"fld\"><label>App URL</label>' +\n        '<input type=\"text\" data-f=\"appUrl\" value=\"' + esc(draft.appUrl || '') + '\"' +\n        (isApp ? '' : ' disabled') + ' placeholder=\"https://…\">' +\n        '<div class=\"hint\">Full URL of a published app. Loads live in an iframe and reflows ' +\n        'responsively — embeds only if the app allows framing (no X-Frame-Options block).</div></div>' +\n\n        '<div class=\"two\">' +\n        '<div class=\"fld\"><label>App width</label>' +\n        '<input type=\"number\" data-f=\"appW\" value=\"' + esc(draft.appW || 1440) + '\"' +\n        (isApp ? '' : ' disabled') + '></div>' +\n        '<div class=\"fld\"><label>App height</label>' +\n        '<input type=\"number\" data-f=\"appH\" value=\"' + esc(draft.appH || 900) + '\"' +\n        (isApp ? '' : ' disabled') + '></div>' +\n        '</div>' +\n\n        '<div class=\"actions\">' +\n        '<button class=\"primary\" data-save>' + (draft._id ? 'Save changes' : 'Create card') + '</button>' +\n        (draft._id ? '<button class=\"danger\" data-del>Delete</button>' : '') +\n        '<div class=\"spacer\"></div>' +\n        '<div class=\"msg' + (d2.state.err ? ' err' : '') + '\">' + esc(d2.state.msg || '') + '</div>' +\n        '</div>'\n    }\n\n    return (\n      '<div class=\"wa\">' +\n      '<div class=\"wa-list\">' +\n      '<div class=\"wa-bar\"><h2>Cards (' + records.length + ')</h2>' +\n      '<span class=\"msg' + (d2.state.err ? ' err' : '') + '\">' + esc(d2.state.msg || '') + '</span>' +\n      '<button data-new>+ New</button></div>' +\n      '<div class=\"wa-rows\">' + rows + '</div>' +\n      '</div>' +\n      '<div class=\"wa-form\">' + form + '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    const blank = () => ({\n      name: '', category: '', tags: [], thumbnail: '',\n      openMode: 'thumb', artboardRef: '', appUrl: '', appW: 1440, appH: 900, order: null, active: true\n    })\n\n    // Pull the uncontrolled text inputs out of the DOM and fold them into the\n    // draft. Called before any state change that re-renders the form, so\n    // half-typed edits are never lost.\n    const harvest = () => {\n      const d = Object.assign({}, d2.state.draft || {})\n      root.querySelectorAll('[data-f]').forEach((el) => {\n        const k = el.getAttribute('data-f')\n        d[k] = el.type === 'number'\n          ? (el.value === '' ? null : Number(el.value))\n          : el.value\n      })\n      return d\n    }\n\n    const setDraft = (d, msg, err) => {\n      d2.setState({ draft: d, msg: msg || '', err: !!err })\n    }\n\n    d2.on('[data-row]', 'click', (e) => {\n      const el = e.target.closest('[data-row]')\n      if (!el) return\n      const id = el.getAttribute('data-key')\n      const rec = ((d2.data() && d2.data().records()) || []).find((r) => r._id === id)\n      if (!rec) return\n      d2.setState({\n        selectedId: id,\n        draft: Object.assign({}, rec, { tags: asTags(rec.tags) }),\n        msg: '', err: false\n      })\n    })\n\n    // Reorder by dragging. The DOM is reordered live and only persisted on\n    // drop — re-rendering mid-drag would tear the dragged node out from under\n    // the pointer, so no state is touched until the drag ends.\n    d2.on('[data-row]', 'dragstart', (e) => {\n      const el = e.target.closest('[data-row]')\n      if (!el) return\n      el.classList.add('dragging')\n      if (e.dataTransfer) {\n        e.dataTransfer.effectAllowed = 'move'\n        try { e.dataTransfer.setData('text/plain', el.getAttribute('data-key')) } catch (_) {}\n      }\n    })\n\n    d2.on('[data-row]', 'dragover', (e) => {\n      e.preventDefault()\n      const over = e.target.closest('[data-row]')\n      const moving = root.querySelector('.row.dragging')\n      if (!over || !moving || over === moving) return\n      const r = over.getBoundingClientRect()\n      const after = (e.clientY - r.top) > r.height / 2\n      over.parentNode.insertBefore(moving, after ? over.nextSibling : over)\n    })\n\n    d2.on('[data-row]', 'dragend', async () => {\n      const moving = root.querySelector('.row.dragging')\n      if (moving) moving.classList.remove('dragging')\n\n      const ids = Array.from(root.querySelectorAll('[data-row]'))\n        .map((el) => el.getAttribute('data-key'))\n      const h = d2.data()\n      const byId = {}\n      for (const r of (h.records() || [])) byId[r._id] = r\n\n      // Only write the rows whose position actually moved.\n      const writes = []\n      ids.forEach((id, i) => {\n        const rec = byId[id]\n        if (rec && rec.order !== i + 1) writes.push(h.update({ _id: id, order: i + 1 }))\n      })\n      if (!writes.length) return\n\n      d2.setState({ msg: 'Reordering…', err: false })\n      try {\n        await Promise.all(writes)\n        h.refresh()\n        d2.setState({ msg: 'Order saved.', err: false })\n      } catch (err) {\n        d2.setState({ msg: 'Reorder failed: ' + (err && err.message ? err.message : err), err: true })\n      }\n    })\n\n    d2.on('[data-new]', 'click', () => {\n      d2.setState({ selectedId: null, draft: blank(), msg: '', err: false })\n    })\n\n    d2.on('[data-mode]', 'change', (e) => {\n      const d = harvest()\n      d.openMode = e.target.value\n      setDraft(d)\n    })\n\n    d2.on('[data-active]', 'change', (e) => {\n      const d = harvest()\n      d.active = e.target.value === 'yes'\n      setDraft(d)\n    })\n\n    d2.on('[data-tagin]', 'keydown', (e) => {\n      if (e.key !== 'Enter') return\n      e.preventDefault()\n      const v = e.target.value.trim()\n      if (!v) return\n      const d = harvest()\n      const tags = asTags(d.tags)\n      if (!tags.some((t) => t.toLowerCase() === v.toLowerCase())) tags.push(v)\n      d.tags = tags\n      e.target.value = ''\n      setDraft(d)\n    })\n\n    d2.on('[data-untag]', 'click', (e) => {\n      const i = Number(e.target.getAttribute('data-untag'))\n      const d = harvest()\n      const tags = asTags(d.tags)\n      tags.splice(i, 1)\n      d.tags = tags\n      setDraft(d)\n    })\n\n    d2.on('[data-pick]', 'click', () => {\n      const f = root.querySelector('[data-file]')\n      if (f) f.click()\n    })\n\n    d2.on('[data-file]', 'change', async (e) => {\n      const file = e.target.files && e.target.files[0]\n      if (!file) return\n      const d = harvest()\n      setDraft(d, 'Uploading ' + file.name + '…')\n      try {\n        const drive = d2.drive(d2.param('driveRef', 'wormhole-drive'))\n        const out = await drive.upload([file])\n        const url = out && out[0] && out[0].fileUrl\n        if (!url) throw new Error('upload returned no fileUrl')\n        d.thumbnail = url\n        setDraft(d, 'Uploaded. Remember to save.')\n      } catch (err) {\n        setDraft(d, 'Upload failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    d2.on('[data-save]', 'click', async () => {\n      const d = harvest()\n      if (!String(d.name || '').trim()) return setDraft(d, 'Name is required.', true)\n      if (d.openMode === 'artboard' && !String(d.artboardRef || '').trim()) {\n        return setDraft(d, 'Artboard mode needs a reference.', true)\n      }\n      if (d.openMode === 'app' && !String(d.appUrl || '').trim()) {\n        return setDraft(d, 'App mode needs a URL.', true)\n      }\n      const payload = {\n        name: d.name, category: d.category, tags: asTags(d.tags),\n        thumbnail: d.thumbnail, openMode: d.openMode,\n        artboardRef: d.openMode === 'artboard' ? d.artboardRef : '',\n        appUrl: d.openMode === 'app' ? d.appUrl : '',\n        appW: d.openMode === 'app' ? (Number(d.appW) || 1440) : null,\n        appH: d.openMode === 'app' ? (Number(d.appH) || 900) : null,\n        order: d.order == null ? null : Number(d.order),\n        active: d.active !== false\n      }\n      setDraft(d, 'Saving…')\n      try {\n        const h = d2.data()\n        if (d._id) {\n          await h.update(Object.assign({ _id: d._id }, payload))\n          setDraft(Object.assign({}, d, payload), 'Saved.')\n        } else {\n          await h.create(payload)\n          d2.setState({ selectedId: null, draft: null, msg: 'Card created.', err: false })\n        }\n        await h.refresh()\n      } catch (err) {\n        setDraft(d, 'Save failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    d2.on('[data-del]', 'click', async () => {\n      const d = d2.state.draft\n      if (!d || !d._id) return\n      const ok = await d2.dialog.confirm('Delete \"' + (d.name || 'this card') + '\"?')\n      if (!ok) return\n      try {\n        const h = d2.data()\n        await h.remove(d._id)\n        await h.refresh()\n        d2.setState({ selectedId: null, draft: null, msg: 'Card deleted.', err: false })\n      } catch (err) {\n        setDraft(d, 'Delete failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    // Morph patching doesn't refresh a <select>'s live value when only an\n    // option's `selected` attribute flips — so after loading a card the mode\n    // dropdown showed the wrong option even though the saved data was correct.\n    // Re-sync the selects to the draft after every render.\n    this._syncSelects = () => {\n      const draft = d2.state.draft\n      if (!draft) return\n      const modeSel = root.querySelector('[data-mode]')\n      if (modeSel) modeSel.value = draft.openMode || 'thumb'\n      const actSel = root.querySelector('[data-active]')\n      if (actSel) actSel.value = draft.active === false ? 'no' : 'yes'\n    }\n    this._syncSelects()\n  },\n\n  updated() {\n    if (this._syncSelects) this._syncSelects()\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"driveRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"driveRef"}}}}}}}}}},{"_id":"6a60100890bd4f023e2fa800","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wormhole-modal-body","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Modal Body — header + content, kept in lockstep.\n//\n// The whole point of this component is ALIGNMENT. The content is letterboxed\n// inside whatever space the modal has, so the width it actually occupies\n// depends on its aspect ratio. Native blocks can only align to the slot's\n// edges, not to the rendered content's edges, so the name and category drifted\n// past the image on anything that wasn't exactly the slot's proportion.\n//\n// Here the fitted content box is measured first, and the header is given that\n// exact width — so name (left) and category (right) land on the content's\n// corners for any thumbnail or artboard shape.\n\nconst esc = (s) => String(s == null ? '' : s)\n  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n  .replace(/\"/g, '&quot;')\n\n// App source is encoded \"WxH|url\" so the card's own dimensions can ride the one\n// declared store key (a freshly-added store key won't propagate). No dims → 0.\nconst parseApp = (raw) => {\n  const s = String(raw || '')\n  const bar = s.indexOf('|')\n  if (bar < 0) return { url: s, w: 0, h: 0 }\n  const m = s.slice(0, bar).match(/^(\\d+)x(\\d+)$/)\n  return { url: s.slice(bar + 1), w: m ? +m[1] : 0, h: m ? +m[2] : 0 }\n}\n\nexport default {\n  css: `\n    .mb-wrap { position: absolute; inset: 0; display: flex; align-items: center;\n               justify-content: center; font-family: Inter, -apple-system, sans-serif; }\n    .mb-stack { display: flex; flex-direction: column; gap: 14px; }\n    .mb-head { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; }\n    .mb-name { font-size: 17px; font-weight: 500; color: #eef3f8; }\n    .mb-cat { font-size: 17px; font-weight: 400; color: #8ea0b2; white-space: nowrap; }\n    .mb-body { position: relative; overflow: hidden; border-radius: 10px; }\n    .mb-img { position: absolute; inset: 0; background: center/contain no-repeat; }\n    .mb-frame { border: 0; display: block; position: absolute; left: 0; top: 0;\n                transform-origin: top left; }\n    .mb-app { border: 0; display: block; position: absolute; inset: 0;\n              width: 100%; height: 100%; }\n    .mb-none { position: absolute; inset: 0; display: flex; align-items: center;\n               justify-content: center; color: #4e6070; font-size: 13px; background: #0d1117; }\n  `,\n\n  render(d2) {\n    const mode = d2.param('mode', 'thumb')\n    const ref = String(d2.param('artboardRef', '') || '')\n    const thumb = String(d2.param('thumb', '') || '')\n    const useApp = mode === 'app' && !!ref\n    const useAb = mode === 'artboard' && !!ref\n    const app = useApp ? parseApp(ref) : null\n\n    let inner\n    if (useApp || useAb) {\n      // artboardRef carries both: artboard → /preview/<ref>, app → \"WxH|url\".\n      // Both render at a design size and are SCALED to fit; the app's design\n      // size comes from the card so different formats letterbox correctly.\n      const src = useApp ? esc(app.url) : ('/preview/' + esc(ref))\n      inner = '<iframe class=\"mb-frame\" data-frame src=\"' + src + '\"' +\n              (useAb ? ' referrerpolicy=\"same-origin\"' : '') + '></iframe>'\n    } else if (thumb) {\n      inner = '<div class=\"mb-img\" data-img style=\"background-image:url(' + esc(thumb) + ')\"></div>'\n    } else {\n      inner = '<div class=\"mb-none\">nothing to show</div>'\n    }\n\n    return (\n      '<div class=\"mb-wrap\">' +\n      '<div class=\"mb-stack\" data-stack>' +\n      '<div class=\"mb-head\">' +\n      '<span class=\"mb-name\">' + esc(d2.param('cardName', '')) + '</span>' +\n      '<span class=\"mb-cat\">' + esc(d2.param('cardCategory', '')) + '</span>' +\n      '</div>' +\n      '<div class=\"mb-body\" data-body>' + inner + '</div>' +\n      '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    // Natural sizes are cached per URL so re-fitting never re-downloads.\n    const ratios = new Map()\n\n    const fit = () => {\n      const wrap = root.querySelector('.mb-wrap')\n      const stack = root.querySelector('[data-stack]')\n      const head = root.querySelector('.mb-head')\n      const body = root.querySelector('[data-body]')\n      if (!wrap || !stack || !head || !body) return\n\n      const mode = d2.param('mode', 'thumb')\n      const ref = String(d2.param('artboardRef', '') || '')\n      const thumb = String(d2.param('thumb', '') || '')\n      const useApp = mode === 'app' && !!ref\n      const useAb = mode === 'artboard' && !!ref\n      const app = useApp ? parseApp(ref) : null\n      const dw = (useApp && app.w) ? app.w : (Number(d2.param('designW', 1440)) || 1440)\n      const dh = (useApp && app.h) ? app.h : (Number(d2.param('designH', 900)) || 900)\n\n      let ratio\n      if (useApp || useAb) {\n        ratio = dw / dh\n      } else if (thumb) {\n        if (!ratios.has(thumb)) {\n          // Unknown yet — measure off-screen, then re-fit once it resolves.\n          const probe = new Image()\n          probe.onload = () => {\n            ratios.set(thumb, (probe.naturalWidth || 16) / (probe.naturalHeight || 9))\n            fit()\n          }\n          probe.onerror = () => { ratios.set(thumb, 16 / 9); fit() }\n          probe.src = thumb\n          return\n        }\n        ratio = ratios.get(thumb)\n      } else {\n        ratio = 16 / 9\n      }\n\n      const gap = 14\n      const availW = wrap.clientWidth\n      const availH = wrap.clientHeight - head.offsetHeight - gap\n      if (availW <= 0 || availH <= 0) return\n\n      // Contain, then hand the resulting width to the header so both edges\n      // line up with the content's corners.\n      const w = Math.min(availW, availH * ratio)\n      const h = w / ratio\n\n      stack.style.width = w + 'px'\n      body.style.width = w + 'px'\n      body.style.height = h + 'px'\n\n      if (useApp || useAb) {\n        const frame = root.querySelector('[data-frame]')\n        if (frame) {\n          const s = w / dw\n          frame.style.width = dw + 'px'\n          frame.style.height = dh + 'px'\n          frame.style.transform = 'scale(' + s.toFixed(4) + ')'\n        }\n      }\n    }\n\n    fit()\n    const ro = new ResizeObserver(fit)\n    ro.observe(root.querySelector('.mb-wrap'))\n    d2.onUnmount(() => ro.disconnect())\n\n    // Esc closes. This lives here rather than in a global listener because\n    // the modal is gated by `hide`, which removes the subtree from the DOM —\n    // so this component only exists while the modal is open, and the listener\n    // is torn down with it.\n    d2.on(window, 'keydown', (e) => {\n      if (e.key !== 'Escape') return\n      const s = d2.store('wh-ui')\n      if (s) s.set('modalOpen', false)\n    })\n\n    // Re-fit whenever the params change and render() runs again.\n    this._fit = fit\n  },\n\n  updated() {\n    if (this._fit) this._fit()\n  }\n}"}},"cardName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardName"}}}},"cardCategory":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardCategory"}}}},"thumb":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"thumb"}}}},"mode":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mode"}}}},"artboardRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"artboardRef"}}}},"designW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"designW"}}}},"designH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"designH"}}}}}}}}}},{"_id":"6a60fae4d774752eb7dd1f14","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"worm-gl","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Worm GL — the brand's gummy tube, running VERTICALLY.\n//\n// The `wormmorph` shader from the brand board, transposed. GLSL in real time:\n// every pixel is solved from a formula, so there is no geometry, no slicing\n// and no stroke to rasterise.\n//\n// Distance to the centreline is found by searching for the NEAREST POINT on\n// the curve within a local window, not by measuring straight across the axis.\n// The across-axis shortcut (even corrected by the local slope) misreads the\n// field near a tight bend, which pinched the tube on sharp turns.\n//\n// The tweak panel writes into a plain closure object, never into d2.state:\n// touching state re-runs render(), and a canvas component that re-renders\n// loses its backing store mid-flight.\n\nconst HEAD = `\nprecision highp float;\nuniform vec2 R; uniform float T;\nuniform float uSpeed, uThick, uGloss, uFlow, uAmp, uMorph, uGlow;\nvec3 pal(float t){\n  t=clamp(t,0.0,1.0)*7.0;\n  vec3 c0=vec3(0.608,0.361,1.000), c1=vec3(0.357,0.525,1.000), c2=vec3(0.302,0.875,0.902),\n       c3=vec3(0.302,0.878,0.631), c4=vec3(0.518,0.878,0.353), c5=vec3(0.910,0.910,0.302),\n       c6=vec3(1.000,0.416,0.302), c7=vec3(1.000,0.302,0.620);\n  int i=int(min(floor(t),6.0)); float f=smoothstep(0.0,1.0, t-float(i));\n  vec3 a=c0,b=c1;\n  if(i==1){a=c1;b=c2;} else if(i==2){a=c2;b=c3;} else if(i==3){a=c3;b=c4;}\n  else if(i==4){a=c4;b=c5;} else if(i==5){a=c5;b=c6;} else if(i==6){a=c6;b=c7;}\n  return mix(a,b,f);\n}\nfloat tri(float x){ return abs(fract(x*0.5)*2.0-1.0); }\n`\n\nconst FRAG = `\nvec3 preset(int i){\n  if(i==0) return vec3(0.0, 0.0, 0.0);\n  if(i==1) return vec3(0.16, 6.5, 0.03);\n  if(i==2) return vec3(0.12, 9.0, 0.07);\n  return vec3(0.13, 13.0, 0.06);\n}\n// Auto-morph. A single sine is exactly periodic, so the pill<->wave swing\n// lands on the same beat forever. Three sines on frequencies that are not\n// simple multiples of each other never realign, so the wander reads as\n// organic. Amplitudes sum to 1, so it still spans the whole range — but only\n// when all three happen to agree, which is rare, and that is what keeps the\n// full pill and the full wave as occasional events rather than a metronome.\nfloat autoMorph(float t){\n  float w = 0.55*sin(t*0.37) + 0.30*sin(t*0.71 + 1.7) + 0.15*sin(t*1.13 + 4.2);\n  // Floor at 0.05 rather than 0. Near a turning point the derivative of any\n  // smooth function goes to zero, so the wander DWELLS at whatever the ends\n  // of its range are — and morph 0 is the preset with zero amplitude and zero\n  // frequency, i.e. a dead straight pill. Lifting the floor keeps a trace of\n  // wave in the worm at all times.\n  return 0.05 + 0.95*clamp(0.5 + 0.5*w, 0.0, 1.0);\n}\nfloat centerPos(float y,float t){\n  float m = uMorph<0.0 ? autoMorph(t) : uMorph;\n  int i=int(floor(m)); int j=int(min(floor(m)+1.0,1.0));\n  float f=smoothstep(0.0,1.0,fract(m));\n  vec3 p=mix(preset(i),preset(j),f);\n  float amp=p.x*uAmp, fr=p.y;\n  return 0.5 + amp*sin(y*fr - t);\n}\nvoid main(){\n  vec2 uv=gl_FragCoord.xy/R;\n  float t=T*uSpeed;\n  float x=uv.y;\n\n  // Nearest point on the curve within a local window. Both components are put\n  // in width-normalised units first, or the aspect would bias which sample\n  // wins.\n  float ar=R.y/R.x;\n  float best=1e9;\n  for(int i=0;i<17;i++){\n    float yy=uv.y+(float(i)/16.0-0.5)*0.26;\n    vec2 dv=vec2(uv.x-centerPos(yy,t),(uv.y-yy)*ar);\n    best=min(best,length(dv));\n  }\n  float d=best*sign(uv.x-centerPos(uv.y,t));\n  float h=0.085*uThick;\n\n  // Aspect compensation for the transposition. The 1.05 sets how fast colour\n  // advances per unit of the travel axis. That axis used to be uv.x, spanning\n  // the WIDTH; transposed it is uv.y, spanning the HEIGHT. Same coefficient\n  // over a shorter distance means a faster cycle per pixel, so the palette got\n  // squeezed. Scaling by R.y/R.x restores the original spatial rate.\n  float flow=x*1.05*(R.y/R.x) + 0.06*t*uFlow;\n  float n=clamp(d/h,-1.0,1.0);\n  float tube=sqrt(max(0.0,1.0-n*n));\n  vec3 col=pal(tri(flow));\n  col=mix(col, vec3(dot(col,vec3(0.333))), 0.12);\n  col=mix(col, vec3(1.0), 0.06);\n  float shade=0.74+0.26*tube;\n  col*=shade;\n  float satin=exp(-pow((n+0.34)/0.42,2.0));\n  col+=satin*0.16*uGloss;\n  float edge=abs(d)-h;\n  // Antialias in real pixels rather than a magic constant tied to one aspect.\n  float aaw=1.5/R.x;\n  float inside=smoothstep(aaw,-aaw,edge);\n  float halo=exp(-pow(max(edge,0.0)/(h*1.28),2.0));\n  vec3 bg=vec3(0.015,0.015,0.02);\n  vec3 o=mix(bg,col,inside)+pal(tri(flow))*halo*(1.0-inside)*0.6*uGlow;\n  o*=1.0-0.18*pow(length(uv-0.5)*1.2,2.0);\n  gl_FragColor=vec4(o,1.0);\n}\n`\n\nconst VERT = 'attribute vec2 p;void main(){gl_Position=vec4(p,0.0,1.0);}'\n\nconst SPEC = [\n  { k: 'speed', min: 0, max: 2, step: 0.01, d: 0.23 },\n  { k: 'thick', min: 0.05, max: 1.5, step: 0.01, d: 0.39 },\n  { k: 'amp', min: 0, max: 2, step: 0.01, d: 0.26 },\n  { k: 'flow', min: 0, max: 5, step: 0.01, d: 3.57 },\n  { k: 'gloss', min: 0, max: 2, step: 0.01, d: 0.62 },\n  { k: 'glow', min: 0, max: 2, step: 0.01, d: 0.26 },\n  { k: 'morph', min: -1, max: 1, step: 0.01, d: -1 }\n]\n\nexport default {\n  css: `\n    .wg-wrap { position: absolute; inset: 0; overflow: hidden; }\n    .wg-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n\n    .wg-panel {\n      position: absolute; top: 24px; right: 24px; width: 268px; z-index: 5;\n      font-family: Inter, -apple-system, sans-serif; color: #dbe4ec;\n      background: rgba(10, 14, 20, 0.72);\n      backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);\n      border: 1px solid rgba(255,255,255,0.10); border-radius: 14px;\n      box-shadow: 0 18px 50px rgba(0,0,0,.5); overflow: hidden;\n    }\n    .wg-panel.off .wg-rows, .wg-panel.off .wg-acts, .wg-panel.off .wg-out { display: none; }\n    .wg-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px;\n               border-bottom: 1px solid rgba(255,255,255,.08); }\n    .wg-head b { flex: 1; font-size: 11px; letter-spacing: .16em; text-transform: uppercase;\n                 font-weight: 600; color: #93a6b8; }\n    .wg-rows { padding: 10px 12px 4px; }\n    .wg-row { margin-bottom: 9px; }\n    .wg-lab { display: flex; justify-content: space-between; font-size: 11px;\n              color: #8ba0b3; margin-bottom: 3px; }\n    .wg-lab i { font-style: normal; color: #eaf2f9; font-variant-numeric: tabular-nums; }\n    .wg-row input { width: 100%; accent-color: #7ab6ff; height: 16px; }\n    .wg-acts { display: flex; gap: 6px; padding: 4px 12px 12px; }\n    .wg-panel button {\n      font: inherit; font-size: 11px; cursor: pointer; color: #cfdde9;\n      background: #18202a; border: 1px solid #26313d; border-radius: 7px; padding: 6px 9px;\n    }\n    .wg-panel button:hover { background: #1f2937; }\n    .wg-acts button { flex: 1; }\n    .wg-out { width: calc(100% - 24px); margin: 0 12px 12px; height: 56px; resize: none;\n              font: 10px/1.4 ui-monospace, monospace; color: #9fd0a8;\n              background: #0a0f15; border: 1px solid #1c2530; border-radius: 7px; padding: 7px; }\n  `,\n\n  render(d2) {\n    const rows = SPEC.map((s) =>\n      '<div class=\"wg-row\">' +\n      '<div class=\"wg-lab\"><span>' + s.k + (s.k === 'morph' ? ' (-1 auto)' : '') + '</span>' +\n      '<i data-val=\"' + s.k + '\">–</i></div>' +\n      '<input type=\"range\" data-slider=\"' + s.k + '\" min=\"' + s.min + '\" max=\"' + s.max +\n      '\" step=\"' + s.step + '\"></div>'\n    ).join('')\n\n    const hidden = Number(d2.param('panel', 1)) ? '' : ' style=\"display:none\"'\n\n    return (\n      '<div class=\"wg-wrap\" data-d2-static>' +\n      '<canvas class=\"wg-canvas\" data-cv></canvas>' +\n      '<div class=\"wg-panel\" data-panel' + hidden + '>' +\n      '<div class=\"wg-head\"><b>worm</b><button data-collapse>–</button></div>' +\n      '<div class=\"wg-rows\">' + rows + '</div>' +\n      '<div class=\"wg-acts\"><button data-copy>Copiar config</button>' +\n      '<button data-reset>Resetar</button></div>' +\n      '<textarea class=\"wg-out\" data-out readonly></textarea>' +\n      '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    const cv = root.querySelector('[data-cv]')\n    if (!cv) return\n\n    // Live values. Seeded from the block's params, then owned by the panel.\n    const V = {}\n    SPEC.forEach((s) => { V[s.k] = Number(d2.param(s.k, s.d)) })\n\n    const syncUI = () => {\n      SPEC.forEach((s) => {\n        const sl = root.querySelector('[data-slider=\"' + s.k + '\"]')\n        const vl = root.querySelector('[data-val=\"' + s.k + '\"]')\n        if (sl) sl.value = String(V[s.k])\n        if (vl) vl.textContent = Number(V[s.k]).toFixed(2)\n      })\n    }\n\n    const configText = () =>\n      '{ ' + SPEC.map((s) => s.k + ': ' + Number(V[s.k]).toFixed(2)).join(', ') + ' }'\n\n    const showConfig = () => {\n      const out = root.querySelector('[data-out]')\n      if (out) out.value = configText()\n    }\n\n    syncUI()\n    showConfig()\n\n    d2.on('[data-slider]', 'input', (e) => {\n      const k = e.target.getAttribute('data-slider')\n      V[k] = parseFloat(e.target.value)\n      const vl = root.querySelector('[data-val=\"' + k + '\"]')\n      if (vl) vl.textContent = V[k].toFixed(2)\n      showConfig()\n    })\n\n    d2.on('[data-reset]', 'click', () => {\n      SPEC.forEach((s) => { V[s.k] = s.d })\n      syncUI(); showConfig()\n    })\n\n    d2.on('[data-collapse]', 'click', () => {\n      const p = root.querySelector('[data-panel]')\n      if (!p) return\n      p.classList.toggle('off')\n      const b = root.querySelector('[data-collapse]')\n      if (b) b.textContent = p.classList.contains('off') ? '+' : '–'\n    })\n\n    d2.on('[data-copy]', 'click', async () => {\n      const txt = configText()\n      const out = root.querySelector('[data-out]')\n      try {\n        await navigator.clipboard.writeText(txt)\n        if (out) { out.value = txt + '\\n\\n✓ copiado' }\n      } catch (err) {\n        // Clipboard can be blocked; selecting the text is the fallback.\n        if (out) { out.value = txt; out.focus(); out.select() }\n      }\n    })\n\n    // ---- GL ---------------------------------------------------------------\n    const gl = cv.getContext('webgl', {\n      antialias: true, premultipliedAlpha: false, powerPreference: 'high-performance'\n    })\n    if (!gl) { d2.log('[worm-gl] no webgl context'); return }\n\n    const mk = (type, src) => {\n      const sh = gl.createShader(type)\n      gl.shaderSource(sh, src); gl.compileShader(sh)\n      if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) d2.log(gl.getShaderInfoLog(sh))\n      return sh\n    }\n\n    const pr = gl.createProgram()\n    gl.attachShader(pr, mk(gl.VERTEX_SHADER, VERT))\n    gl.attachShader(pr, mk(gl.FRAGMENT_SHADER, HEAD + FRAG))\n    gl.linkProgram(pr); gl.useProgram(pr)\n\n    const buf = gl.createBuffer()\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf)\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW)\n    const loc = gl.getAttribLocation(pr, 'p')\n    gl.enableVertexAttribArray(loc)\n    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0)\n\n    const uR = gl.getUniformLocation(pr, 'R')\n    const uT = gl.getUniformLocation(pr, 'T')\n    const U = {}\n    SPEC.forEach((s) => {\n      U[s.k] = gl.getUniformLocation(pr, 'u' + s.k.charAt(0).toUpperCase() + s.k.slice(1))\n    })\n\n    let onScreen = true\n    const io = new IntersectionObserver((es) => {\n      es.forEach((e) => { onScreen = e.isIntersecting })\n    }, { rootMargin: '120px' })\n    io.observe(cv)\n\n    const t0 = performance.now()\n\n    const size = () => {\n      const dpr = Math.min(1.85, window.devicePixelRatio || 1)\n      const w = Math.round(cv.clientWidth * dpr)\n      const h = Math.round(cv.clientHeight * dpr)\n      if (w && h && (cv.width !== w || cv.height !== h)) { cv.width = w; cv.height = h }\n      gl.viewport(0, 0, cv.width, cv.height)\n    }\n\n    d2.raf((now) => {\n      if (!onScreen || !cv.clientWidth) return\n      size()\n      gl.useProgram(pr)\n      gl.uniform2f(uR, cv.width, cv.height)\n      gl.uniform1f(uT, (now - t0) / 1000)\n      SPEC.forEach((s) => { gl.uniform1f(U[s.k], V[s.k]) })\n      gl.drawArrays(gl.TRIANGLES, 0, 3)\n    })\n\n    d2.onUnmount(() => io.disconnect())\n  }\n}"}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"thick":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"thick"}}}},"gloss":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gloss"}}}},"flow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flow"}}}},"amp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"amp"}}}},"morph":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"morph"}}}},"glow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glow"}}}},"panel":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panel"}}}}}}}}}},{"_id":"6a61081631d18332cf55cdac","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"worm-mesh","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Worm Mesh — the same gummy tube, drawn as GEOMETRY instead of a full-screen\n// shader.\n//\n// The previous build covered the screen with one triangle and made every\n// pixel search for the nearest point on the curve (17 samples each). Most of\n// that work was pixels proving they were NOT part of the tube.\n//\n// Here the ribbon is a real triangle strip. Three consequences:\n//   · the search disappears — the cross-section coordinate arrives as a\n//     varying, interpolated by the rasteriser for free;\n//   · thickness is exact by construction, since vertices are offset along the\n//     analytic normal rather than approximated from a sampled grid;\n//   · the silhouette becomes real geometry, so the context's MSAA finally\n//     does something. On a full-screen shader it did nothing at all: there\n//     was no silhouette to sample, the edge lived inside the fragment shader.\n//\n// The curve animates entirely in the VERTEX shader, so the buffer is uploaded\n// once and never touched again — ~450 vertices per frame instead of millions\n// of fragments.\n\nconst SEGS = 220\nconst SKIRT = 3.2   // strip half-width in units of the tube radius (room for the halo)\nconst BLEED = 0.09  // run past both ends of the frame\n\nconst COMMON = `\nprecision highp float;\nuniform vec2 R; uniform float T;\nuniform float uSpeed, uThick, uGloss, uFlow, uAmp, uMorph, uGlow;\nvec3 pal(float t){\n  t=clamp(t,0.0,1.0)*7.0;\n  vec3 c0=vec3(0.608,0.361,1.000), c1=vec3(0.357,0.525,1.000), c2=vec3(0.302,0.875,0.902),\n       c3=vec3(0.302,0.878,0.631), c4=vec3(0.518,0.878,0.353), c5=vec3(0.910,0.910,0.302),\n       c6=vec3(1.000,0.416,0.302), c7=vec3(1.000,0.302,0.620);\n  int i=int(min(floor(t),6.0)); float f=smoothstep(0.0,1.0, t-float(i));\n  vec3 a=c0,b=c1;\n  if(i==1){a=c1;b=c2;} else if(i==2){a=c2;b=c3;} else if(i==3){a=c3;b=c4;}\n  else if(i==4){a=c4;b=c5;} else if(i==5){a=c5;b=c6;} else if(i==6){a=c6;b=c7;}\n  return mix(a,b,f);\n}\nfloat tri(float x){ return abs(fract(x*0.5)*2.0-1.0); }\n`\n\nconst VERT = COMMON + `\nattribute float aT;      // 0..1 along the curve (bled past both ends)\nattribute float aSide;   // -SKIRT..+SKIRT, in units of the tube radius\nvarying float vN;        // cross-section coordinate, reaches the fragment free\nvarying float vY;        // screen-space y, so the gradient bands exactly as before\n\nvec3 preset(int i){\n  if(i==0) return vec3(0.0, 0.0, 0.0);\n  if(i==1) return vec3(0.16, 6.5, 0.03);\n  if(i==2) return vec3(0.12, 9.0, 0.07);\n  return vec3(0.13, 13.0, 0.06);\n}\nfloat autoMorph(float t){\n  float w = 0.55*sin(t*0.37) + 0.30*sin(t*0.71 + 1.7) + 0.15*sin(t*1.13 + 4.2);\n  return 0.05 + 0.95*clamp(0.5 + 0.5*w, 0.0, 1.0);\n}\n// Position and slope of the centreline together — the slope is what builds\n// the true normal, so thickness stays even through a bend with no search.\nvec2 centre(float y,float t){\n  float m = uMorph<0.0 ? autoMorph(t) : uMorph;\n  int i=int(floor(m)); int j=int(min(floor(m)+1.0,1.0));\n  float f=smoothstep(0.0,1.0,fract(m));\n  vec3 p=mix(preset(i),preset(j),f);\n  float amp=p.x*uAmp, fr=p.y;\n  return vec2(0.5 + amp*sin(y*fr - t), amp*fr*cos(y*fr - t));\n}\n\nvoid main(){\n  float t=T*uSpeed;\n  float y=aT;\n  vec2 c=centre(y,t);\n  float ar=R.y/R.x;\n\n  // Tangent taken in width-normalised space so the normal is isotropic in\n  // pixels; without this the offset would skew with the window's aspect.\n  vec2 tg=normalize(vec2(c.y, ar));\n  vec2 nrm=vec2(tg.y, -tg.x);\n\n  float h=0.085*uThick;\n  vec2 off=nrm*(aSide*h);\n  vec2 uv=vec2(c.x + off.x, y + off.y/ar);\n\n  vN=aSide;\n  vY=uv.y;\n  gl_Position=vec4(uv*2.0-1.0, 0.0, 1.0);\n}\n`\n\nconst FRAG = COMMON + `\n#extension GL_OES_standard_derivatives : enable\nvarying float vN;\nvarying float vY;\n\nvoid main(){\n  float t=T*uSpeed;\n  float h=0.085*uThick;\n\n  float flow=vY*1.05*(R.y/R.x) + 0.06*t*uFlow;\n  float n=clamp(vN,-1.0,1.0);\n  float tube=sqrt(max(0.0,1.0-n*n));\n\n  vec3 col=pal(tri(flow));\n  col=mix(col, vec3(dot(col,vec3(0.333))), 0.12);\n  col=mix(col, vec3(1.0), 0.06);\n  col*=0.74+0.26*tube;\n  col+=exp(-pow((n+0.34)/0.42,2.0))*0.16*uGloss;\n\n  float edge=(abs(vN)-1.0)*h;\n\n  // Edge width from the actual per-pixel rate of change instead of a fixed\n  // constant, so the transition holds at any curvature or resolution.\n  #ifdef GL_OES_standard_derivatives\n    float aaw=max(fwidth(vN)*h*0.75, 1.0e-5);\n  #else\n    float aaw=1.5/R.x;\n  #endif\n  float inside=smoothstep(aaw,-aaw,edge);\n  float halo=exp(-pow(max(edge,0.0)/(h*1.28),2.0));\n\n  vec2 uv=gl_FragCoord.xy/R;\n  float vig=1.0-0.18*pow(length(uv-0.5)*1.2,2.0);\n\n  // Premultiplied. Tube colour carries the coverage; the halo is ADDED on top\n  // rather than blended, which is what the single-pass version did when it\n  // summed the halo after mixing against the background.\n  vec3 o=(col*inside + pal(tri(flow))*halo*(1.0-inside)*0.6*uGlow)*vig;\n  gl_FragColor=vec4(o, inside);\n}\n`\n\nconst SPEC = [\n  { k: 'speed', min: 0, max: 2, step: 0.01, d: 0.23 },\n  { k: 'thick', min: 0.05, max: 1.5, step: 0.01, d: 0.39 },\n  { k: 'amp', min: 0, max: 2, step: 0.01, d: 0.26 },\n  { k: 'flow', min: 0, max: 5, step: 0.01, d: 3.57 },\n  { k: 'gloss', min: 0, max: 2, step: 0.01, d: 0.62 },\n  { k: 'glow', min: 0, max: 2, step: 0.01, d: 0.26 },\n  { k: 'morph', min: -1, max: 1, step: 0.01, d: -1 }\n]\n\nexport default {\n  css: `\n    .wg-wrap { position: absolute; inset: 0; overflow: hidden; }\n    .wg-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n\n    .wg-panel {\n      position: absolute; top: 24px; right: 24px; width: 268px; z-index: 5;\n      font-family: Inter, -apple-system, sans-serif; color: #dbe4ec;\n      background: rgba(10, 14, 20, 0.72);\n      backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);\n      border: 1px solid rgba(255,255,255,0.10); border-radius: 14px;\n      box-shadow: 0 18px 50px rgba(0,0,0,.5); overflow: hidden;\n    }\n    .wg-panel.off .wg-rows, .wg-panel.off .wg-acts, .wg-panel.off .wg-out { display: none; }\n    .wg-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px;\n               border-bottom: 1px solid rgba(255,255,255,.08); }\n    .wg-head b { flex: 1; font-size: 11px; letter-spacing: .16em; text-transform: uppercase;\n                 font-weight: 600; color: #93a6b8; }\n    .wg-rows { padding: 10px 12px 4px; }\n    .wg-row { margin-bottom: 9px; }\n    .wg-lab { display: flex; justify-content: space-between; font-size: 11px;\n              color: #8ba0b3; margin-bottom: 3px; }\n    .wg-lab i { font-style: normal; color: #eaf2f9; font-variant-numeric: tabular-nums; }\n    .wg-row input { width: 100%; accent-color: #7ab6ff; height: 16px; }\n    .wg-acts { display: flex; gap: 6px; padding: 4px 12px 12px; }\n    .wg-panel button {\n      font: inherit; font-size: 11px; cursor: pointer; color: #cfdde9;\n      background: #18202a; border: 1px solid #26313d; border-radius: 7px; padding: 6px 9px;\n    }\n    .wg-panel button:hover { background: #1f2937; }\n    .wg-acts button { flex: 1; }\n    .wg-out { width: calc(100% - 24px); margin: 0 12px 12px; height: 56px; resize: none;\n              font: 10px/1.4 ui-monospace, monospace; color: #9fd0a8;\n              background: #0a0f15; border: 1px solid #1c2530; border-radius: 7px; padding: 7px; }\n  `,\n\n  render(d2) {\n    const rows = SPEC.map((s) =>\n      '<div class=\"wg-row\">' +\n      '<div class=\"wg-lab\"><span>' + s.k + (s.k === 'morph' ? ' (-1 auto)' : '') + '</span>' +\n      '<i data-val=\"' + s.k + '\">–</i></div>' +\n      '<input type=\"range\" data-slider=\"' + s.k + '\" min=\"' + s.min + '\" max=\"' + s.max +\n      '\" step=\"' + s.step + '\"></div>'\n    ).join('')\n\n    const hidden = Number(d2.param('panel', 1)) ? '' : ' style=\"display:none\"'\n\n    return (\n      '<div class=\"wg-wrap\" data-d2-static>' +\n      '<canvas class=\"wg-canvas\" data-cv></canvas>' +\n      '<div class=\"wg-panel\" data-panel' + hidden + '>' +\n      '<div class=\"wg-head\"><b>worm</b><button data-collapse>–</button></div>' +\n      '<div class=\"wg-rows\">' + rows + '</div>' +\n      '<div class=\"wg-acts\"><button data-copy>Copiar config</button>' +\n      '<button data-reset>Resetar</button></div>' +\n      '<textarea class=\"wg-out\" data-out readonly></textarea>' +\n      '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    const cv = root.querySelector('[data-cv]')\n    if (!cv) return\n\n    const V = {}\n    SPEC.forEach((s) => { V[s.k] = Number(d2.param(s.k, s.d)) })\n\n    const syncUI = () => {\n      SPEC.forEach((s) => {\n        const sl = root.querySelector('[data-slider=\"' + s.k + '\"]')\n        const vl = root.querySelector('[data-val=\"' + s.k + '\"]')\n        if (sl) sl.value = String(V[s.k])\n        if (vl) vl.textContent = Number(V[s.k]).toFixed(2)\n      })\n    }\n    const configText = () =>\n      '{ ' + SPEC.map((s) => s.k + ': ' + Number(V[s.k]).toFixed(2)).join(', ') + ' }'\n    const showConfig = () => {\n      const out = root.querySelector('[data-out]')\n      if (out) out.value = configText()\n    }\n\n    syncUI(); showConfig()\n\n    d2.on('[data-slider]', 'input', (e) => {\n      const k = e.target.getAttribute('data-slider')\n      V[k] = parseFloat(e.target.value)\n      const vl = root.querySelector('[data-val=\"' + k + '\"]')\n      if (vl) vl.textContent = V[k].toFixed(2)\n      showConfig()\n    })\n    d2.on('[data-reset]', 'click', () => {\n      SPEC.forEach((s) => { V[s.k] = s.d }); syncUI(); showConfig()\n    })\n    d2.on('[data-collapse]', 'click', () => {\n      const p = root.querySelector('[data-panel]')\n      if (!p) return\n      p.classList.toggle('off')\n      const b = root.querySelector('[data-collapse]')\n      if (b) b.textContent = p.classList.contains('off') ? '+' : '–'\n    })\n    d2.on('[data-copy]', 'click', async () => {\n      const txt = configText()\n      const out = root.querySelector('[data-out]')\n      try { await navigator.clipboard.writeText(txt); if (out) out.value = txt + '\\n\\n✓ copiado' }\n      catch (err) { if (out) { out.value = txt; out.focus(); out.select() } }\n    })\n\n    // ---- GL ---------------------------------------------------------------\n    const gl = cv.getContext('webgl', {\n      antialias: true, premultipliedAlpha: true, powerPreference: 'high-performance'\n    })\n    if (!gl) { d2.log('[worm-mesh] no webgl context'); return }\n    gl.getExtension('OES_standard_derivatives')\n\n    const mk = (type, src) => {\n      const sh = gl.createShader(type)\n      gl.shaderSource(sh, src); gl.compileShader(sh)\n      if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) d2.log(gl.getShaderInfoLog(sh))\n      return sh\n    }\n    const pr = gl.createProgram()\n    gl.attachShader(pr, mk(gl.VERTEX_SHADER, VERT))\n    gl.attachShader(pr, mk(gl.FRAGMENT_SHADER, FRAG))\n    gl.linkProgram(pr)\n    if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) d2.log(gl.getProgramInfoLog(pr))\n    gl.useProgram(pr)\n\n    // Static strip: two vertices per ring, uploaded once. All of the motion\n    // happens in the vertex shader, so this never needs rewriting.\n    const verts = []\n    for (let i = 0; i <= SEGS; i++) {\n      const tt = -BLEED + (i / SEGS) * (1 + BLEED * 2)\n      verts.push(tt, -SKIRT, tt, SKIRT)\n    }\n    const buf = gl.createBuffer()\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf)\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW)\n\n    const aT = gl.getAttribLocation(pr, 'aT')\n    const aSide = gl.getAttribLocation(pr, 'aSide')\n    gl.enableVertexAttribArray(aT)\n    gl.vertexAttribPointer(aT, 1, gl.FLOAT, false, 8, 0)\n    gl.enableVertexAttribArray(aSide)\n    gl.vertexAttribPointer(aSide, 1, gl.FLOAT, false, 8, 4)\n\n    const uR = gl.getUniformLocation(pr, 'R')\n    const uT = gl.getUniformLocation(pr, 'T')\n    const U = {}\n    SPEC.forEach((s) => {\n      U[s.k] = gl.getUniformLocation(pr, 'u' + s.k.charAt(0).toUpperCase() + s.k.slice(1))\n    })\n\n    gl.enable(gl.BLEND)\n    // Premultiplied source: the tube's colour already carries its coverage and\n    // the halo rides on top as added light.\n    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)\n    gl.clearColor(0.015, 0.015, 0.02, 1.0)\n\n    let onScreen = true\n    const io = new IntersectionObserver((es) => {\n      es.forEach((e) => { onScreen = e.isIntersecting })\n    }, { rootMargin: '120px' })\n    io.observe(cv)\n\n    const t0 = performance.now()\n    const size = () => {\n      const dpr = Math.min(2, window.devicePixelRatio || 1)\n      const w = Math.round(cv.clientWidth * dpr)\n      const h = Math.round(cv.clientHeight * dpr)\n      if (w && h && (cv.width !== w || cv.height !== h)) { cv.width = w; cv.height = h }\n      gl.viewport(0, 0, cv.width, cv.height)\n    }\n\n    d2.raf((now) => {\n      if (!onScreen || !cv.clientWidth) return\n      size()\n      gl.clear(gl.COLOR_BUFFER_BIT)\n      gl.useProgram(pr)\n      gl.uniform2f(uR, cv.width, cv.height)\n      gl.uniform1f(uT, (now - t0) / 1000)\n      SPEC.forEach((s) => { gl.uniform1f(U[s.k], V[s.k]) })\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, (SEGS + 1) * 2)\n    })\n\n    d2.onUnmount(() => io.disconnect())\n  }\n}"}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"thick":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"thick"}}}},"gloss":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gloss"}}}},"flow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flow"}}}},"amp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"amp"}}}},"morph":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"morph"}}}},"glow":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glow"}}}},"panel":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"panel"}}}}}}}}}},{"_id":"6a610ef7cc01aafcca1e04e1","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wh-stage","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Stage — one scene, one context, driven by a single scroll progress.\n//\n// The whole scene is a PURE FUNCTION OF `p` (0 = home, 1 = hero). Nothing is\n// triggered, nothing is a state machine. That is what lets scrolling back up\n// return to the exact starting state, and makes every intermediate value a\n// coherent frame.\n//\n// Scroll is hijacked: the wheel moves a target, `p` eases toward it with a\n// frame-rate independent curve. The page itself never scrolls.\n//\n// The worm/plasma lands here next, as ONE object morphing across the same\n// progress — not two representations cross-fading.\n\nconst clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)\nconst ease01 = (u) => u * u * (3 - 2 * u)\n// Quintic. Zero FIRST and SECOND derivative at both ends, where the cubic only\n// zeroes the first — the acceleration itself eases in, which is what stops a\n// transition from reading as mechanical.\nconst ease02 = (u) => u * u * u * (u * (u * 6 - 15) + 10)\n\n// Tweak panel. Every number the timeline is built from lives here so it can be\n// dragged instead of guessed. `hud: 2` on the block reveals it; `hud: 1` keeps\n// only the progress read-out, `hud: 0` hides both.\nconst TW_DEF = {\n  morphA: 0.26, morphB: 0.51, shrinkB: 0.81, compB: 0.84,\n  dipS: 0.21, endS: 0.38, agit: 140,\n  glow0: 0.30, glowG: 0.10,\n  r0: 54, rThick: 51, rMorph: 19,\n  floatAmp: 10, floatPitch: 0.045, floatTwist: 0.02\n}\n\n// Agitation envelope, keyed on the rail directly — NORMALISED, so the panel's\n// `agitation` slider still scales the whole thing. Dead still while it is a\n// worm; comes up to half strength as the plasma forms; holds there through the\n// squeeze; then doubles into the last tenth, where the cards are already in.\nconst AGIT_KF = [[0, 0], [0.47, 0], [0.60, 0.5], [0.90, 0.5], [1, 1]]\n\n// Catmull-Rom, NOT an ease per segment. Easing each pair separately zeroes the\n// slope at EVERY keyframe, so the envelope comes to a full stop at each one and\n// the whole thing reads as a chain of little ease-in-outs. Estimating the\n// tangent from the neighbours instead lets the value carry its velocity through\n// a keyframe. Written for non-uniform spacing, since these are not evenly\n// spread. Overshoot is left IN — that is momentum, and it is the difference\n// between motion and interpolation. Callers clamp to whatever their own range\n// can tolerate.\nconst kfAt = (kf, x) => {\n  const n = kf.length\n  if (x <= kf[0][0]) return kf[0][1]\n  if (x >= kf[n - 1][0]) return kf[n - 1][1]\n  let i = 0\n  while (i < n - 2 && x > kf[i + 1][0]) i++\n  const x0 = kf[i][0], y0 = kf[i][1]\n  const x1 = kf[i + 1][0], y1 = kf[i + 1][1]\n  const h = Math.max(1e-6, x1 - x0)\n  const u = (x - x0) / h\n  const xPrev = i > 0 ? kf[i - 1][0] : x0 - h\n  const yPrev = i > 0 ? kf[i - 1][1] : y0\n  const xNext = i + 2 < n ? kf[i + 2][0] : x1 + h\n  const yNext = i + 2 < n ? kf[i + 2][1] : y1\n  const m0 = (y1 - yPrev) / (x1 - xPrev) * h\n  const m1 = (yNext - y0) / (xNext - x0) * h\n  const u2 = u * u, u3 = u2 * u\n  const v = (2 * u3 - 3 * u2 + 1) * y0 + (u3 - 2 * u2 + u) * m0 +\n    (-2 * u3 + 3 * u2) * y1 + (u3 - u2) * m1\n  return v\n}\n\n// Overshoot-and-settle, with a DIFFERENT easing on each leg. The rise uses\n// ease-out so it launches hard and arrives softly at the apex; the fall uses\n// ease-in-out so it leaves the apex softly and settles without a stop. Both\n// legs therefore hold zero slope at the peak, which is what an arc actually\n// does, and — unlike a spline through keyframes — the peak lands exactly on\n// the frame it was asked for instead of drifting past it on momentum.\nconst easeOut3 = (u) => 1 - (1 - u) * (1 - u) * (1 - u)\nconst bump = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * easeOut3((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n// Same shape, but the rise starts SLOW (smootherstep, not ease-out). easeOut3\n// leaps to ~45% of the climb within a fifth of the leg, so anything born on it\n// looks big the instant it is visible — useless for a radius meant to open from\n// nothing. This keeps the value hugging v0 through the birth window, then\n// accelerates to the peak.\nconst bumpSlow = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * ease02((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n// The middle of the three: rise on cubic smoothstep. Zero slope at the start\n// like the slow one, but climbs as u² instead of u³, so it sits squarely\n// between easeOut3 and smootherstep — a launch neither big nor tucked away.\nconst bumpMed = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * ease01((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n// Slower still than bumpSlow, WITHOUT moving the peak. The rise clings low\n// through most of the leg (u² × smootherstep) and only sweeps up near the end,\n// so the ring stays tight at 0.82 yet still reaches its apex at 0.85 — keeping\n// the lag behind the card size that the elastic depends on.\nconst bumpLate = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) {\n    const u = (x - t0) / Math.max(1e-6, t1 - t0)\n    return v0 + (v1 - v0) * (u * u * ease02(u))\n  }\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n\n// Card entrance. The ring opens outward and the card SIZE peaks six hundredths\n// later than the radius — that lag is the whole elastic feel: the cards are\n// still swelling while the ring has already begun to settle. Radius starts at\n// a sixth of the way out rather than zero, because zero put every card dead\n// centre, which is precisely where the plasma is. The fade in is STAGGERED by\n// index so the pool arrives as a cascade rather than all at once.\n// ONE easing for both radius and size (bumpSlow: rounded apex, zero slope on\n// both sides of the peak, so no corner). Proportion is kept by using the same\n// curve and near-equal bases; the elastic is a SMALL lag — size peaks three\n// hundredths after the radius, no more. The sprawl of four different rises per\n// property was the thing that stopped reading as one motion.\nconst CARD_IN = 0.74\nconst CARD_LIVE = 0.80\nconst CARD_FADE_A = 0.76, CARD_FADE_B = 0.78\nconst CARD_R_PK = 0.86, CARD_S_PK = 0.89\nconst CARD_R0 = 0.12, CARD_S0 = 0.08, CARD_OVER = 1.15\nconst TW_SPEC = [\n  ['morphA', 'morph start', 0, 1, 0.01],\n  ['morphB', 'morph end / shrink start', 0, 1, 0.01],\n  ['shrinkB', 'min size reached', 0, 1, 0.01],\n  ['compB', 'hold end / agit peak', 0, 1, 0.01],\n  ['dipS', 'min scale', 0.05, 1, 0.005],\n  ['endS', 'rest scale', 0.05, 1, 0.005],\n  ['agit', 'agitation', 0, 140, 1],\n  ['glow0', 'glow at rest', 0, 1, 0.01],\n  ['glowG', 'glow gain', 0, 2, 0.01],\n  ['r0', 'thickness base', 10, 90, 1],\n  ['rThick', 'thicken gain', 0, 60, 1],\n  ['rMorph', 'morph gain', 0, 80, 1],\n  ['floatAmp', 'float height', 0, 40, 0.5],\n  ['floatPitch', 'float pitch', 0, 0.2, 0.005],\n  ['floatTwist', 'float twist', 0, 0.2, 0.005]\n]\n\n// Half-width of the arc over which a card cross-fades between its lit face and\n// its dark back, in cos(angle). Testing the SIGN of facing popped visibly: a\n// curved card still shows plenty of surface as it passes edge-on.\nconst FACE_BAND = 0.35\n\n// Idle-float amplitude + rotations are tweakable, see TW_DEF (floatAmp,\n// floatPitch, floatTwist).\n\n// Pixels of horizontal drag → carousel advance. Lower = the card clings to the\n// cursor instead of outrunning it.\nconst DRAG_K = 0.0003\n// Fling velocity is DECOUPLED from drag distance: DRAG_K can fall to keep the\n// card pinned under the cursor while FLING_GAIN keeps the flick as punchy.\nconst FLING_GAIN = 1.7\n// Fling decay per second (exponential). Lower = the throw coasts longer.\nconst FLING_FRICTION = 3.2\nconst FLING_MAX = 4.0\n// A press shorter than this (seconds) is a pure click and never touches the\n// spin; longer than it, the press eases the carousel to a hold.\nconst CLICK_HOLD = 0.14\n// On modal open the orbit holds full speed this long (s), then eases to a stop\n// over MODAL_STOP — a visible brake behind the panel, not a dead freeze.\nconst MODAL_HOLD = 0.5\nconst MODAL_STOP = 1.0\n// Seconds for a nav item's timed scroll (e.g. Cases → p=1).\nconst NAV_SCROLL_DUR = 3.0\n\n// The plasma renders into an offscreen target at this fraction of the canvas\n// resolution, then composites under the full-res cards. A diffuse glow has no\n// edge and specular ARE high-frequency and blur under an aggressive cut, so\n// this stays mild; the bulk of the saving comes from skipping the colour loop\n// in the empty glow margin (see the fragment shader), not from resolution.\nconst RT_SCALE = 1.0\n\n// One lobe per product, in the brand's order.\nconst PRODUCTS = [\n  [0.608, 0.361, 1.000], // UI\n  [0.357, 0.525, 1.000], // REGISTRY\n  [0.302, 0.875, 0.902], // MEDIA\n  [0.302, 0.878, 0.631], // ID\n  [0.518, 0.878, 0.353], // CONNECT\n  [0.910, 0.910, 0.302], // DOMAIN\n  [1.000, 0.416, 0.302], // RUSH\n  [1.000, 0.302, 0.620]  // CASHIER\n]\n\n// GLSL3 on purpose: gl_FragDepth is core there. Writing depth is the whole\n// point — without it the blob is just a sprite and cards can only sort in\n// front of or behind it as a whole, never be cut by it.\nconst BLOB_VERT = `\nout vec3 vWorld;\nout vec2 vUv;\nvoid main(){\n  vec4 wp = modelMatrix * vec4(position, 1.0);\n  vWorld = wp.xyz;\n  vUv = uv;\n  gl_Position = projectionMatrix * viewMatrix * wp;\n}\n`\n\nconst BLOB_FRAG = `\nprecision highp float;\nin vec3 vWorld;\nin vec2 vUv;\nout vec4 fragColor;\nuniform vec2 uP[32];   // smooth spine, resampled through the anchors\nuniform vec2 uA[8];    // the product anchors themselves\nuniform vec3 uPal[8];\nuniform float uR, uK, uBall, uGloss, uGlow, uSpan, uPhase, uBlend, uRim, uSat;\n\n// Smooth minimum: the union that MELTS instead of intersecting. k sets how\n// wide the weld is, and animating it is what turns a chain into a plasma.\nfloat smin(float a, float b, float k){\n  float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0);\n  return mix(b, a, h) - k*h*(1.0-h);\n}\n// CYCLIC over 8 intervals, not 7 — mapping 8 stops onto 7 spans leaves the\n// wrap with nowhere to go and it jumps. The 8th span carries pink back to\n// purple through magenta, which is where they already meet on the spectrum.\n// Interpolation is linear on purpose: smoothstep flattens the curve AT every\n// stop, and a plateau next to a ramp is exactly what reads as banding.\nvec3 ramp(float t){\n  t = fract(t) * 8.0;\n  int i = int(floor(t));\n  float f = t - float(i);\n  return mix(uPal[i], uPal[i == 7 ? 0 : i + 1], f);\n}\nvoid main(){\n  vec2 q = vWorld.xy;\n\n  // ONE pass over the spine — no sphere tracing. The melted field and the\n  // nearest point are gathered in the same loop: the field gives thickness\n  // and silhouette, the nearest point gives the normal and the arc position.\n  // PASS 1 — the melted field and the closest approach. Colour cannot be\n  // weighted before the minimum is known, because the weights have to be\n  // measured RELATIVE to it.\n  float d = 1e9, dmin = 1e9, bestT = 0.0;\n  for(int i=0;i<31;i++){\n    vec2 a = uP[i], b = uP[i+1];\n    vec2 pa = q-a, ba = b-a;\n    float h = clamp(dot(pa,ba)/max(dot(ba,ba), 1e-4), 0.0, 1.0);\n    float dd = length(q - (a + ba*h));\n    d = smin(d, dd, uK);\n    if(dd < dmin){ dmin = dd; bestT = (float(i) + h)/31.0; }\n  }\n  // The anchors swell into beads as the chain balls up.\n  for(int i=0;i<8;i++) d = smin(d, length(q - uA[i]) - uBall, uK);\n\n  // PASS 2 — colour and surface direction, weighted by EXCESS over the closest\n  // approach. An ABSOLUTE falloff collapses far from the spine: distances get\n  // large, a fourth power blows their ratio up, one sample wins outright, and\n  // the colour field degenerates into a nearest-neighbour partition whose cell\n  // walls show up as wedges in the halo. Excess widens the blend window on its\n  // own with distance — tight on the surface, broad out in the glow.\n  // The 31-sample blend only runs NEAR the body. Past this band the fragment\n  // is deep in the faint outer glow, where core = 0 so all the shading is\n  // discarded and only base survives at a tiny alpha - there the single\n  // nearest-arc colour is indistinguishable from the blend, and skipping the\n  // loop spares it for the large empty margin of the quad. Neighbouring margin\n  // fragments take the same branch, so the GPU genuinely skips the work.\n  vec3 nearCol = ramp(bestT*uSpan + uPhase);\n  vec3 base;\n  vec2 dAcc = vec2(0.0);\n  // Crossfade into the cheap nearest-arc colour across a BAND, never at a hard\n  // line - a colour swap mid-glow left a visible ring where the neon appears to\n  // end. By the band's outer edge the blend has already become the nearest\n  // colour, so the loop-skipping branch past it joins seamlessly.\n  if(d - uR < 180.0){\n    vec3 cAcc = vec3(0.0);\n    float wAcc = 0.0;\n    for(int i=0;i<31;i++){\n      vec2 a = uP[i], b = uP[i+1];\n      vec2 pa = q-a, ba = b-a;\n      float h = clamp(dot(pa,ba)/max(dot(ba,ba), 1e-4), 0.0, 1.0);\n      vec2 off = q - (a + ba*h);\n      float dd = length(off);\n      float x = (dd - dmin)/uBlend;\n      float w = 1.0/(1.0 + x*x*x*x);\n      // Soft weight, no sharpening: crossings desaturate GRADUALLY so the ends\n      // stay vivid and the muddy zones fade in, never edge.\n      cAcc += ramp(((float(i) + h)/31.0)*uSpan + uPhase) * w;\n      // Direction on the same soft weight; nearest-point would JUMP across the\n      // bisector and crease every joint.\n      dAcc += off * (w/max(dd, 1e-4));\n      wAcc += w;\n    }\n    vec3 blended = cAcc / max(wAcc, 1e-4);\n    base = mix(blended, nearCol, smoothstep(100.0, 180.0, d - uR));\n  } else {\n    base = nearCol;\n  }\n  // A gentle vibrance on top, now that the grey is handled at the source.\n  float lum = dot(base, vec3(0.2126, 0.7152, 0.0722));\n  base = clamp(mix(vec3(lum), base, uSat), 0.0, 1.0);\n\n  // Fake the volume from the field: treating d as the distance from a tube's\n  // spine, the surface normal is exact for a cylinder cross-section. This is\n  // what sells it as 3D without a single ray.\n  float dn = clamp(d/uR, 0.0, 1.0);\n  vec2 g = normalize(dAcc + vec2(1e-5));\n  vec3 n = normalize(vec3(g*dn, sqrt(max(1.0 - dn*dn, 0.0))));\n\n  vec3 L = normalize(vec3(-0.32, 0.52, 0.79));\n  float lam = 0.60 + 0.40*max(dot(n, L), 0.0);\n  float spec = pow(max(dot(n, L), 0.0), 26.0) * uGloss;\n  // Grazing angles glow: this is what reads as an emitting body rather than\n  // a lit solid, and it costs one dot product we already have.\n  float rim = pow(1.0 - n.z, 3.0);\n\n  float aa = fwidth(d)*0.9 + 1e-4;\n  float core = 1.0 - smoothstep(-aa, aa, d - uR);\n  // Two-lobe falloff: a tight bright bloom hugging the silhouette plus a wide\n  // faint wash. One exponential can be bright OR far-reaching, not both.\n  float ex = max(d - uR, 0.0);\n  float halo = (exp(-ex*0.055)*0.72 + exp(-ex*0.013)*0.38) * uGlow;\n  float a = clamp(core + halo, 0.0, 1.0);\n  // Fade to zero at the quad's OWN edge. The pad only pushes the bounding\n  // rectangle out to low alpha; it stays a hard geometric step, which reads as\n  // a faint line on the dark scene. Windowing by the plane UV removes the step\n  // entirely, so the glow ends because it faded, not because it was clipped.\n  vec2 ef = smoothstep(0.0, 0.06, vUv) * (1.0 - smoothstep(0.94, 1.0, vUv));\n  a *= ef.x * ef.y;\n  if(a < 0.004) discard;\n\n  vec3 lit = base*lam + vec3(spec) + base*rim*uRim;\n  vec3 col = mix(base*1.18, lit, core);\n  fragColor = vec4(col, a);\n}\n`\n// Background grid, drawn in-shader so it can do what a CSS gradient cannot: a\n// gentle BARREL warp (lines bow out near the edges for depth) and a radial\n// alpha fade (the grid dissolves into the flat base at the corners). Full\n// screen but loop-free, so its cost is trivial next to the plasma.\nconst GRID_VERT = `\nvoid main(){ gl_Position = vec4(position.xy, 1.0, 1.0); }\n`\nconst GRID_FRAG = `\nprecision highp float;\nout vec4 fragColor;\nuniform vec2 uRes;\nuniform float uCell;\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 c = uv - 0.5;\n  float r2 = dot(c, c);\n  // Anisotropic pincushion: the vertical term is stronger, so the HORIZONTAL\n  // lines bow more than the verticals - that asymmetry reads as the inside of a\n  // sphere rather than a cylinder.\n  vec2 warp = vec2(c.x * (1.0 - 0.22 * r2), c.y * (1.0 - 0.88 * r2));\n  vec2 gp = (warp * uRes) / uCell;\n  vec2 gg = abs(fract(gp) - 0.5) / fwidth(gp);\n  float line = 1.0 - min(min(gg.x, gg.y), 1.0);\n  float d = length(c) * 1.414;\n  float fade = smoothstep(1.0, 0.46, d);  // 1 centre -> 0 corners, wider fade\n  vec3 base = vec3(0.0196, 0.0314, 0.0588);\n  vec3 lineCol = vec3(0.052, 0.082, 0.150);\n  vec3 col = mix(base, lineCol, clamp(line, 0.0, 1.0));\n  fragColor = vec4(col, fade);\n}\n`\nconst jitter = (i, salt) => {\n  const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453\n  return v - Math.floor(v)\n}\nconst signed = (i, salt) => jitter(i, salt) * 2 - 1\n\nexport default {\n  css: `\n    .st-wrap { position: absolute; inset: 0; overflow: hidden;\n      background-color: #05080f; }\n    .st-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n\n    .st-hud {\n      position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);\n      z-index: 5; pointer-events: none; font: 11px/1 Inter, sans-serif;\n      letter-spacing: .18em; color: #5d7085; text-transform: uppercase;\n      display: flex; align-items: center; gap: 12px;\n    }\n    .st-hud .bar { width: 180px; height: 2px; background: rgba(255,255,255,.10); border-radius: 2px; }\n    .st-hud .bar i { display: block; height: 100%; width: 0%; border-radius: 2px;\n                     background: linear-gradient(90deg,#9b5cff,#4ddfe6,#84e05a,#ff4d9e); }\n\n    /* Cursor pill. Outer element positions (rewritten every pointer event),\n       inner one animates, so the two transforms never fight. */\n    .st-tip { position: absolute; left: 0; top: 0; pointer-events: none;\n              display: flex; align-items: center; height: 58px; z-index: 6;\n              will-change: transform; }\n    .st-tip-glass {\n      position: relative; overflow: hidden;\n      height: 14px; width: 14px; border-radius: 29px;\n      font-family: Inter, -apple-system, sans-serif;\n      background:\n        linear-gradient(155deg, rgba(216,236,255,0.11), rgba(216,236,255,0) 46%),\n        rgba(10, 16, 24, 0.44);\n      backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      -webkit-backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      box-shadow: inset 0 1px 0 rgba(226,241,255,0.22),\n                  inset 1px 0 0 rgba(226,241,255,0.10),\n                  inset -1px 0 0 rgba(226,241,255,0.10),\n                  inset 0 -1px 0 rgba(226,241,255,0.26),\n                  inset 0 -7px 11px -7px rgba(216,236,255,0.22),\n                  0 6px 16px rgba(0,0,0,0.24);\n      opacity: 0;\n      transition: width 129ms cubic-bezier(.7, 0, .6, .25),\n                  height 129ms cubic-bezier(.7, 0, .6, .25),\n                  opacity 102ms ease-in 27ms;\n      will-change: width, height, opacity;\n    }\n    .st-tip.on .st-tip-glass {\n      opacity: 1; height: 58px;\n      transition: width 129ms cubic-bezier(.4, .75, .3, 1),\n                  height 129ms cubic-bezier(.4, .75, .3, 1),\n                  opacity 102ms ease-out;\n    }\n    .st-tip-inner {\n      position: absolute; left: 0; top: 50%; height: 58px;\n      transform: translateY(-50%);\n      display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;\n      align-content: center; column-gap: 10px;\n      padding: 0 17px 0 14px; white-space: nowrap;\n      opacity: 0; transition: opacity 57ms ease-in 41ms;\n    }\n    .st-tip.on .st-tip-inner { opacity: 1; transition: opacity 57ms ease-out 31ms; }\n    .st-tip-arrow { grid-row: 1 / span 2; align-self: center;\n                    display: flex; align-items: center; justify-content: center; color: #f4f4f5; }\n    .st-tip-name { font-size: 14px; font-weight: 600; color: #f4f4f5; line-height: 1.3;\n                   text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n    .st-tip-cat { font-size: 13px; color: #cfcfd6; line-height: 1.3;\n                  text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n\n    .st-tw-fab { position: absolute; top: 16px; right: 16px; z-index: 41;\n                 width: 34px; height: 34px; border-radius: 9px; cursor: pointer;\n                 display: flex; align-items: center; justify-content: center;\n                 background: rgba(16,16,20,0.82); backdrop-filter: blur(14px);\n                 border: 1px solid rgba(255,255,255,0.10); color: #9a9aa6;\n                 font: 15px/1 ui-monospace, Menlo, monospace; padding: 0; }\n    .st-tw-fab:hover { color: #f0f0f5; background: rgba(28,28,34,0.86); }\n    .st-tw.hidden { display: none; }\n    .st-tw { position: absolute; top: 58px; right: 16px; width: 244px; z-index: 40;\n             padding: 12px 12px 10px; border-radius: 12px;\n             background: rgba(16,16,20,0.82); backdrop-filter: blur(14px);\n             border: 1px solid rgba(255,255,255,0.10);\n             font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;\n             color: #d6d6de; }\n    .st-tw-h { font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase;\n               color: #8a8a96; margin-bottom: 8px; }\n    .st-tw-row { display: grid; grid-template-columns: 1fr 34px; gap: 2px 6px;\n                 align-items: center; margin-bottom: 7px; }\n    .st-tw-row > span { grid-column: 1 / span 2; color: #9a9aa6; font-size: 10px; }\n    .st-tw-row > input { width: 100%; accent-color: #7bd6ff; height: 14px; }\n    .st-tw-row > i { font-style: normal; text-align: right; color: #f0f0f5; }\n    .st-tw-b { width: 100%; margin-top: 4px; padding: 6px; cursor: pointer;\n               border-radius: 7px; border: 1px solid rgba(255,255,255,0.16);\n               background: rgba(255,255,255,0.07); color: #f0f0f5;\n               font: inherit; }\n    .st-tw-b:hover { background: rgba(255,255,255,0.13); }\n    .st-tw-out { width: 100%; height: 52px; margin-top: 6px; resize: none;\n                 border-radius: 7px; border: 1px solid rgba(255,255,255,0.12);\n                 background: rgba(0,0,0,0.35); color: #b9ffd0;\n                 font: 10px/1.3 ui-monospace, Menlo, monospace; padding: 5px; }\n  `,\n\n  render(d2) {\n    const hud = Number(d2.param('hud', 1))\n      ? '<div class=\"st-hud\"><span data-hud-lab>p 0.00</span>' +\n        '<span class=\"bar\"><i data-hud-bar></i></span></div>'\n      : ''\n    const tw = Number(d2.param('hud', 1)) >= 2\n      ? '<button class=\"st-tw-fab\" data-tw-fab>≡</button>' +\n        '<div class=\"st-tw hidden\" data-tw-panel>' +\n        '<div class=\"st-tw-h\">plasma timeline</div>' +\n        '<label class=\"st-tw-row\"><span>scrub p</span>' +\n        '<input type=\"range\" min=\"0\" max=\"1\" step=\"0.005\" value=\"0\" data-tw-p>' +\n        '<i data-tw-pv>0.00</i></label>' +\n        TW_SPEC.map((s) =>\n          '<label class=\"st-tw-row\"><span>' + s[1] + '</span>' +\n          '<input type=\"range\" min=\"' + s[2] + '\" max=\"' + s[3] + '\" step=\"' + s[4] +\n          '\" value=\"' + TW_DEF[s[0]] + '\" data-tw-k=\"' + s[0] + '\">' +\n          '<i data-tw-v=\"' + s[0] + '\">' + TW_DEF[s[0]] + '</i></label>').join('') +\n        '<button class=\"st-tw-b\" data-tw-copy>copy config</button>' +\n        '<textarea class=\"st-tw-out\" data-tw-out readonly></textarea>' +\n        '</div>'\n      : ''\n    return '<div class=\"st-wrap\" data-d2-static>' +\n      '<canvas class=\"st-canvas\" data-cv></canvas>' +\n      '<div class=\"st-tip\" data-tip><div class=\"st-tip-glass\" data-tip-glass>' +\n      '<div class=\"st-tip-inner\" data-tip-inner>' +\n      '<span class=\"st-tip-arrow\">' +\n      '<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" aria-hidden=\"true\">' +\n      '<path d=\"M4.6 11.4 11.4 4.6M6.2 4.6h5.2v5.2\" fill=\"none\" stroke=\"currentColor\"' +\n      ' stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span>' +\n      '<span class=\"st-tip-name\" data-tip-name></span>' +\n      '<span class=\"st-tip-cat\" data-tip-cat></span>' +\n      '</div></div></div>' + hud + tw + '</div>'\n  },\n\n  mounted(d2, root) {\n    const cv = root.querySelector('[data-cv]')\n    if (!cv) return\n    const hudLab = root.querySelector('[data-hud-lab]')\n    const hudBar = root.querySelector('[data-hud-bar]')\n    const tip = root.querySelector('[data-tip]')\n    const tipGlass = root.querySelector('[data-tip-glass]')\n    const tipInner = root.querySelector('[data-tip-inner]')\n    const tipName = root.querySelector('[data-tip-name]')\n    const tipCat = root.querySelector('[data-tip-cat]')\n\n    const TIP_H = 58\n    const TIP_MIN = 14\n    let tipW = TIP_H\n\n    let THREE = null\n    let renderer = null, scene = null, camera = null, raycaster = null, pointer = null\n    let geometry = null\n    let cards = []\n    let builtSig = '', geoSig = ''\n    let t = 0, last = 0, tick = 0\n    // Dedicated real-time clock for the idle float: advances every frame no\n    // matter what, so the float ignores the hover slow-down AND keeps going\n    // under the modal freeze — alive but paused, never frozen.\n    let tFloat = 0\n    // Separate clock for the card orbit so it can slow independently of the plasma.\n    let tCard = 0, slowF = 1, slowMid = 1\n    // Drag-to-spin state + modal freeze.\n    let pressing = false, dragging = false, lastDragX = 0, dragMoved = 0, suppressClick = false\n    let pendingDrag = 0, dragVel = 0, flingVel = 0, pressT = 0, modalT = 0\n    let modalOpen = false\n    const uiStore = d2.store(String(d2.param('storeRef', 'wh-ui')))\n    const readUI = (key) => {\n      if (!uiStore) return undefined\n      try {\n        if (typeof uiStore.get === 'function') return uiStore.get(key)\n        if (uiStore.data) return uiStore.data[key]\n      } catch (e) {}\n      return undefined\n    }\n    let tweenActive = false, tweenFrom = 0, tweenTo = 1, tweenT = 0\n    let sizedOk = false, onScreen = true, needResize = true\n    let lastDpr = 1\n    let hoverRec = null, pointerIn = false, mouseX = 0, mouseY = 0, canvasRect = null\n    const hitList = []\n    const texCache = new Map()\n    let qYaw, qPitch, qRoll, qTwist, axisX, axisY, axisZ\n    let blob = null, blobUni = null\n    let plasmaScene = null, rt = null, dispQuad = null, dispUni = null, gridUni = null\n    // Orbit phase is INTEGRATED, never derived as frequency × elapsed time.\n    let orbPhase = 0, lastBt = 0\n\n    let pTarget = 0\n    let p = 0\n\n    // ---- tweak panel ------------------------------------------------------\n    // Sliders write straight into TW, which the frame loop reads. Deliberately\n    // NOT routed through a Store: a state write per pointer move would\n    // re-render React on every pixel of a drag.\n    const TW = Object.assign({}, TW_DEF)\n    const twOut = root.querySelector('[data-tw-out]')\n    const twP = root.querySelector('[data-tw-p]')\n    const twPv = root.querySelector('[data-tw-pv]')\n    const twCopy = root.querySelector('[data-tw-copy]')\n    const twFab = root.querySelector('[data-tw-fab]')\n    const twPanel = root.querySelector('[data-tw-panel]')\n\n    root.querySelectorAll('[data-tw-k]').forEach((el) => {\n      const k = el.getAttribute('data-tw-k')\n      const lab = root.querySelector('[data-tw-v=\"' + k + '\"]')\n      d2.on(el, 'input', () => {\n        TW[k] = Number(el.value)\n        if (lab) lab.textContent = el.value\n      })\n    })\n    if (twP) {\n      d2.on(twP, 'input', () => {\n        pTarget = Number(twP.value)\n        if (twPv) twPv.textContent = pTarget.toFixed(2)\n      })\n    }\n    if (twCopy) {\n      d2.on(twCopy, 'click', () => {\n        const json = JSON.stringify(TW)\n        if (twOut) { twOut.value = json; twOut.select() }\n        try { navigator.clipboard.writeText(json) } catch (e) { /* iframe may block */ }\n      })\n    }\n    // Collapsed by default; the fab toggles it and flips between the menu and\n    // close glyphs.\n    if (twFab && twPanel) {\n      d2.on(twFab, 'click', () => {\n        const hidden = twPanel.classList.toggle('hidden')\n        twFab.textContent = hidden ? '≡' : '×'\n      })\n    }\n\n    // ---- tooltip ----------------------------------------------------------\n\n    const refreshRect = () => { canvasRect = cv.getBoundingClientRect() }\n\n    const placeTip = () => {\n      if (!tip) return\n      const w = tipW, h = TIP_H\n      const cw = cv.clientWidth, ch = cv.clientHeight\n      let x = mouseX + 18\n      let y = mouseY - h / 2\n      if (x + w > cw - 10) x = mouseX - 18 - w\n      if (y < 10) y = 10\n      if (y + h > ch - 10) y = ch - 10 - h\n      tip.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'\n    }\n\n    const setHover = (rec) => {\n      if (rec === hoverRec) return\n      hoverRec = rec\n      cv.style.cursor = dragging ? 'grabbing' : (rec ? 'pointer' : 'default')\n      if (!rec) {\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n        return\n      }\n      if (tipName) tipName.textContent = rec.name || ''\n      if (tipCat) tipCat.textContent = rec.category || ''\n      tipW = tipInner ? tipInner.offsetWidth : TIP_H\n      placeTip()\n      if (tipGlass) tipGlass.style.width = tipW + 'px'\n      if (tip) tip.classList.add('on')\n    }\n\n    const openCard = (rec) => {\n      if (!rec) return\n      const s = d2.store(String(d2.param('storeRef', 'wh-ui')))\n      if (!s) return\n      // Re-fetch the CURRENT record by id. The card stores the record from when\n      // its geometry was last built, and the rebuild signature is only\n      // id+thumbnail — so an edit to openMode/appUrl (thumbnail unchanged) never\n      // reaches the stored copy. Pull the live one so the modal opens correctly.\n      const handle = d2.data()\n      const fresh = ((handle && handle.records()) || []).find((r) => r._id === rec._id) || rec\n      const tags = Array.isArray(fresh.tags) ? fresh.tags.join('  ·  ') : (fresh.tags || '')\n      const mode = (fresh.openMode === 'app' && fresh.appUrl) ? 'app'\n        : (fresh.openMode === 'artboard' && fresh.artboardRef) ? 'artboard'\n        : 'thumb'\n      s.set('name', fresh.name || '')\n      s.set('category', fresh.category || '')\n      s.set('tags', tags)\n      s.set('thumb', fresh.thumbnail || '')\n      // Route the iframe source through the DECLARED artboardRef key for both\n      // modes — the freshly-added 'appUrl' store key doesn't reach the modal's\n      // binding (schema'd store, same trap as the table). For app it carries\n      // the full URL; for artboard, the reference.\n      s.set('artboardRef', mode === 'app'\n        ? ((fresh.appW || 1440) + 'x' + (fresh.appH || 900) + '|' + (fresh.appUrl || ''))\n        : (fresh.artboardRef || ''))\n      s.set('mode', mode)\n      s.set('modalOpen', true)\n    }\n\n    // ---- gl helpers -------------------------------------------------------\n\n    const BEND_CHUNK = [\n      'vec3 transformed = vec3( position );',\n      'float phi = transformed.x / uBendR;',\n      'transformed.x = uBendR * sin( phi );',\n      'transformed.z += uBendR * cos( phi ) - uBendR;'\n    ].join('\\n')\n\n    const makeMaterial = () => {\n      const m = new THREE.MeshBasicMaterial({\n        side: THREE.DoubleSide, transparent: true, depthWrite: false, opacity: 0\n      })\n      m.onBeforeCompile = (shader) => {\n        shader.uniforms.uBendR = { value: 1000 }\n        shader.vertexShader = 'uniform float uBendR;\\n' + shader.vertexShader\n        shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', BEND_CHUNK)\n        m.userData.shader = shader\n      }\n      return m\n    }\n\n    const texFor = (url) => {\n      let tex = texCache.get(url)\n      if (!tex) {\n        const loader = new THREE.TextureLoader()\n        loader.setCrossOrigin('anonymous')\n        tex = loader.load(url, (t) => {\n          // COVER: crop the image to the card's aspect, centred — fills the card\n          // without stretching; a very different shape loses its edges, not its\n          // proportions. Cross-origin natural size is readable once loaded.\n          const img = t.image\n          if (!img) return\n          const cardAsp = (Number(d2.param('cardW', 448)) || 448) / (Number(d2.param('cardH', 283)) || 283)\n          const imgAsp = (img.naturalWidth || img.width || 1) / (img.naturalHeight || img.height || 1)\n          if (imgAsp > cardAsp) {\n            const r = cardAsp / imgAsp\n            t.repeat.set(r, 1); t.offset.set((1 - r) / 2, 0)\n          } else {\n            const r = imgAsp / cardAsp\n            t.repeat.set(1, r); t.offset.set(0, (1 - r) / 2)\n          }\n          t.needsUpdate = true\n        })\n        tex.colorSpace = THREE.SRGBColorSpace\n        tex.anisotropy = renderer.capabilities.getMaxAnisotropy()\n        texCache.set(url, tex)\n      }\n      return tex\n    }\n\n    const pruneTextures = (keep) => {\n      for (const [url, tex] of texCache) {\n        if (!keep.has(url)) { tex.dispose(); texCache.delete(url) }\n      }\n    }\n\n    const disposeCards = () => {\n      for (const c of cards) { scene.remove(c.mesh); if (c.material) c.material.dispose() }\n      cards = []\n    }\n\n    const buildCards = (records) => {\n      disposeCards()\n      const used = new Set()\n      records.forEach((rec) => {\n        const material = makeMaterial()\n        if (rec.thumbnail) { material.map = texFor(rec.thumbnail); used.add(rec.thumbnail) }\n        const mesh = new THREE.Mesh(geometry, material)\n        mesh.visible = false\n        mesh.userData.record = rec\n        scene.add(mesh)\n        cards.push({ mesh, material, record: rec })\n      })\n      pruneTextures(used)\n    }\n\n    // ---- init ------------------------------------------------------------\n\n    const init = () => {\n      renderer = new THREE.WebGLRenderer({ canvas: cv, alpha: true, antialias: true })\n      renderer.setClearAlpha(0)\n      renderer.outputColorSpace = THREE.SRGBColorSpace\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))\n\n      scene = new THREE.Scene()\n      camera = new THREE.PerspectiveCamera(45, 1, 1, 12000)\n      raycaster = new THREE.Raycaster()\n      pointer = new THREE.Vector2()\n\n      qYaw = new THREE.Quaternion(); qPitch = new THREE.Quaternion(); qRoll = new THREE.Quaternion()\n      qTwist = new THREE.Quaternion()\n      axisX = new THREE.Vector3(1, 0, 0)\n      axisY = new THREE.Vector3(0, 1, 0)\n      axisZ = new THREE.Vector3(0, 0, 1)\n\n      d2.on(window, 'wheel', (e) => {\n        // The modal owns the scroll while open — don't let the wheel drive the\n        // rail behind it.\n        if (modalOpen) return\n        // Any manual scroll cancels a nav-driven tween — the user takes over.\n        tweenActive = false\n        const rate = Number(d2.param('scrollRate', 0.00075))\n        pTarget = clamp01(pTarget + (e.deltaY || 0) * rate)\n      }, { passive: true })\n\n      const onPointer = (e) => {\n        if (!canvasRect) refreshRect()\n        mouseX = e.clientX - canvasRect.left\n        mouseY = e.clientY - canvasRect.top\n        pointer.x = (mouseX / canvasRect.width) * 2 - 1\n        pointer.y = -(mouseY / canvasRect.height) * 2 + 1\n        pointerIn = true\n        placeTip()\n        if (pressing) {\n          // Each delta is against the LAST position, so pointerrawupdate and\n          // pointermove both firing still sum to the true total.\n          const dx = e.clientX - lastDragX\n          lastDragX = e.clientX\n          dragMoved += Math.abs(dx)\n          // The drag only BEGINS past a small threshold, so a plain click never\n          // flips the cursor to grab nor freezes the spin for the press's\n          // duration — that was the clunky micro-hitch.\n          if (!dragging && dragMoved > 4) { dragging = true; cv.style.cursor = 'grabbing' }\n          // Accumulate raw pixels; the frame loop converts to motion + velocity.\n          if (dragging) pendingDrag += dx\n        }\n      }\n      // pointerrawupdate fires at the raw sampling rate; pointermove is\n      // coalesced to one event per frame, which reads as the pill lagging.\n      if ('onpointerrawupdate' in cv) d2.on(cv, 'pointerrawupdate', onPointer)\n      d2.on(cv, 'pointermove', onPointer)\n      d2.on(window, 'scroll', refreshRect, { passive: true })\n\n      d2.on(cv, 'pointerleave', () => {\n        pointerIn = false\n        hoverRec = null\n        cv.style.cursor = 'default'\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n      })\n\n      // Drag anywhere to spin the carousel by hand. Pointer capture keeps the\n      // events coming even when the cursor leaves the canvas mid-drag.\n      d2.on(cv, 'pointerdown', (e) => {\n        // Only draggable once the cards are actually clickable; before that the\n        // carousel is still assembling.\n        if (modalOpen || p < CARD_LIVE) return\n        pressing = true\n        dragging = false\n        lastDragX = e.clientX\n        dragMoved = 0\n        suppressClick = false\n        // Grabbing kills any fling in progress — you've caught it.\n        flingVel = 0; dragVel = 0; pendingDrag = 0; pressT = 0\n        try { cv.setPointerCapture(e.pointerId) } catch (err) {}\n      })\n      d2.on(cv, 'pointerup', (e) => {\n        if (!pressing) return\n        pressing = false\n        // Only a real drag suppresses the click; a still press stays a click.\n        suppressClick = dragging\n        dragging = false\n        try { cv.releasePointerCapture(e.pointerId) } catch (err) {}\n        cv.style.cursor = hoverRec ? 'pointer' : 'default'\n      })\n      // Reuses whatever the hover pass resolved, so what opens is always what\n      // the pill was naming — unless a drag just happened.\n      d2.on(cv, 'click', () => {\n        if (suppressClick) { suppressClick = false; return }\n        if (hoverRec) openCard(hoverRec)\n      })\n\n      // ---- the blob ------------------------------------------------------\n      // Raymarched inside a BOUNDING PROXY, never over the full screen. Same\n      // lesson as the worm: a full-screen pass makes every empty pixel pay to\n      // prove it is empty.\n      blobUni = {\n        uP: { value: Array.from({ length: 32 }, () => new THREE.Vector2()) },\n        uA: { value: Array.from({ length: 8 }, () => new THREE.Vector2()) },\n        uPal: { value: PRODUCTS.map((c) => new THREE.Vector3(c[0], c[1], c[2])) },\n        uR: { value: 26 },\n        uK: { value: 14 },\n        uBall: { value: 0 },\n        uGloss: { value: 0.62 },\n        uGlow: { value: 0.55 },\n        uRim: { value: 0.85 },\n        uSat: { value: 1.0 },\n        uSpan: { value: 1 },\n        uPhase: { value: 0 },\n        uBlend: { value: 45 }\n      }\n      // Flat quad living at z = 0, the helix axis. No depth writes: three.js\n      // sorts transparent objects back-to-front, so cards with z > 0 draw\n      // after it and cards with z < 0 draw before it. That IS the occlusion.\n      const blobMat = new THREE.ShaderMaterial({\n        glslVersion: THREE.GLSL3,\n        uniforms: blobUni,\n        vertexShader: BLOB_VERT,\n        fragmentShader: BLOB_FRAG,\n        transparent: true,\n        depthWrite: false,\n        depthTest: true,\n        side: THREE.DoubleSide\n      })\n      // The blob lives in its OWN scene, rendered to a low-res target. The\n      // cards never share those fragments.\n      blob = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), blobMat)\n      plasmaScene = new THREE.Scene()\n      plasmaScene.add(blob)\n\n      rt = new THREE.WebGLRenderTarget(2, 2, { depthBuffer: false, stencilBuffer: false })\n      rt.texture.minFilter = THREE.LinearFilter\n      rt.texture.magFilter = THREE.LinearFilter\n      rt.texture.generateMipmaps = false\n\n      // Composite quad in the CARD scene, at z = 0 like the blob was, so the\n      // cards still sort in front of and behind it. It samples the low-res\n      // target by screen coordinate. Premultiplied blending: the target holds\n      // colour already multiplied by alpha (that is what alpha blending into a\n      // cleared buffer leaves), so it must be composited with ONE / 1-src_a,\n      // not src_a / 1-src_a, or the glow darkens twice.\n      dispUni = { uTex: { value: rt.texture }, uRes: { value: new THREE.Vector2(1, 1) } }\n      const dispMat = new THREE.ShaderMaterial({\n        uniforms: dispUni,\n        transparent: true,\n        depthWrite: false,\n        depthTest: true,\n        side: THREE.DoubleSide,\n        blending: THREE.CustomBlending,\n        blendSrc: THREE.OneFactor,\n        blendDst: THREE.OneMinusSrcAlphaFactor,\n        vertexShader: 'void main(){ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',\n        fragmentShader: 'precision highp float; uniform sampler2D uTex; uniform vec2 uRes; void main(){ gl_FragColor = texture2D(uTex, gl_FragCoord.xy / uRes); }'\n      })\n      dispQuad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), dispMat)\n      scene.add(dispQuad)\n\n      // Fullscreen background grid, forced to the very back (renderOrder -1,\n      // no depth) so cards and plasma paint over it. Its own base colour equals\n      // the CSS wrap colour, so where it fades out at the edges the seam is\n      // invisible.\n      const gridMat = new THREE.ShaderMaterial({\n        glslVersion: THREE.GLSL3,\n        uniforms: { uRes: { value: new THREE.Vector2(1, 1) }, uCell: { value: 46.0 } },\n        transparent: true,\n        depthTest: false,\n        depthWrite: false,\n        vertexShader: GRID_VERT,\n        fragmentShader: GRID_FRAG\n      })\n      const gridMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), gridMat)\n      gridMesh.frustumCulled = false\n      gridMesh.renderOrder = -1\n      scene.add(gridMesh)\n      gridUni = gridMat.uniforms\n    }\n\n    const resize = () => {\n      const w = cv.clientWidth || 1\n      const h = cv.clientHeight || 1\n      const persp = Number(d2.param('persp', 1400)) || 1400\n      refreshRect()\n      sizedOk = w > 1 && h > 1\n      // Re-read devicePixelRatio EVERY resize: zoom changes it without changing\n      // the CSS size, so a pixelRatio set once at init goes stale and the canvas\n      // upscales blurrily. Cards render at the full (capped) dpr.\n      const dpr = window.devicePixelRatio || 1\n      lastDpr = dpr\n      renderer.setPixelRatio(Math.min(dpr, 4))\n      renderer.setSize(w, h, false)\n      // The plasma target is capped one step lower and upscaled on composite -\n      // it is diffuse, so the softening is invisible, and it stops the\n      // expensive raymarch from scaling up with zoom.\n      if (rt) {\n        const pdpr = Math.min(dpr, 2)\n        rt.setSize(Math.max(1, Math.floor(w * pdpr * RT_SCALE)), Math.max(1, Math.floor(h * pdpr * RT_SCALE)))\n      }\n      camera.fov = 2 * Math.atan((h / 2) / persp) * 180 / Math.PI\n      camera.aspect = w / h\n      camera.position.set(0, 0, persp)\n      camera.updateProjectionMatrix()\n    }\n\n    // ---- frame -----------------------------------------------------------\n\n    const frame = (now) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016\n      last = now\n\n      const ease = Number(d2.param('ease', 6))\n      // A nav item can request a timed scroll to a target p (Cases → 1). The\n      // command arrives on the store; we latch it, clear it, and tween there\n      // over a fixed duration with an ease-in-out — independent of the wheel's\n      // exponential chase.\n      const scrollCmd = readUI('scrollTo')\n      if (typeof scrollCmd === 'number') {\n        tweenFrom = p\n        tweenTo = clamp01(scrollCmd)\n        tweenT = 0\n        tweenActive = true\n        if (uiStore && uiStore.set) uiStore.set('scrollTo', null)\n      }\n      if (tweenActive) {\n        tweenT += dt\n        const u = clamp01(tweenT / NAV_SCROLL_DUR)\n        p = tweenFrom + (tweenTo - tweenFrom) * ease02(u)\n        pTarget = p\n        if (u >= 1) { tweenActive = false; p = tweenTo; pTarget = tweenTo }\n      } else {\n        p += (pTarget - p) * (1 - Math.exp(-ease * dt))\n      }\n\n      if (hudLab) hudLab.textContent = 'p ' + p.toFixed(2)\n      if (hudBar) hudBar.style.width = (p * 100).toFixed(1) + '%'\n\n      const speed = Number(d2.param('speed', 0.045))\n      const gap = Math.max(0.001, Number(d2.param('gap', 0.108)))\n      const coil = Number(d2.param('coil', 520))\n      const ySpan = Number(d2.param('ySpan', 1950))\n      const flatten = Number(d2.param('flatten', 0.66))\n      const bendMid = Number(d2.param('bend', 0.7))\n      const bendEnd = Number(d2.param('bendEnds', 1.8))\n      const wobEnd = Number(d2.param('wobble', 14))\n      const wobMid = Number(d2.param('wobbleMid', 3.5))\n      const segments = Math.max(2, Math.min(96, Math.round(Number(d2.param('segments', 40)))))\n      const cardW = Number(d2.param('cardW', 448))\n      const cardH = Number(d2.param('cardH', 283))\n      const turns = Number(d2.param('turns', -1.5))\n      const phase0 = Number(d2.param('phase0', 270))\n      const tiltDeg = Number(d2.param('tilt', 8))\n      const zOff = Number(d2.param('zOffset', -420))\n      const fade = Number(d2.param('fade', 0.12))\n      const peak = Number(d2.param('peak', 0.95))\n      const backDim = Number(d2.param('backDim', 0.28))\n      const edgeFade = Number(d2.param('edgeFade', 0.14))\n      const offX = Number(d2.param('offsetX', 0))\n      const offY = Number(d2.param('offsetY', 0))\n\n\n      t += speed * dt\n      tFloat += dt * 0.7   // idle float at 70% speed\n      // Slow the card orbit to a crawl while a card is hovered, so the reader can\n      // catch the tooltip before it drifts away. Eased, not switched, so the\n      // deceleration itself reads as deliberate. hoverRec is last frame's hit,\n      // which is exactly the one-frame lag we want. Plasma keeps its own `t`.\n      // Carousel motion. While dragging, the pointer drives it and we track a\n      // velocity; on release that becomes a fling that decays into the idle\n      // spin. On modal open it brakes gracefully rather than freezing dead.\n      modalOpen = !!readUI('modalOpen')\n      if (modalOpen) {\n        // Hold for a beat, then an eased brake to a stop. slowF is FROZEN at\n        // whatever it was when the modal opened — usually the hover slow-down,\n        // since you were on the card you clicked — so the brake starts from that\n        // slow speed instead of jumping back to full. tFloat keeps running, so\n        // the cards still breathe once stopped.\n        modalT += dt\n        let ms = 1\n        if (modalT > MODAL_HOLD) ms = 1 - ease01(Math.min((modalT - MODAL_HOLD) / MODAL_STOP, 1))\n        tCard += speed * slowF * ms * dt\n      } else {\n        modalT = 0\n        if (dragging) {\n          const move = -pendingDrag * DRAG_K\n          pendingDrag = 0\n          tCard += move\n          const v = move / Math.max(dt, 1e-4)\n          dragVel += (v - dragVel) * (1 - Math.exp(-18 * dt))\n          flingVel = Math.max(-FLING_MAX, Math.min(FLING_MAX, dragVel * FLING_GAIN))\n        } else {\n          // Not dragging. A press eases the spin to a STOP, but only after a\n          // brief click window: a quick click never hitches the motion, while a\n          // real hold smoothly settles the grabbed card under the cursor.\n          if (pressing) pressT += dt\n          const slowTarget = (pressing && pressT > CLICK_HOLD) ? 0 : (hoverRec ? 0.25 : 1)\n          // TWO cascaded low-passes make an S-curve (eased in AND out), so the\n          // speed doesn't ramp linearly into the slow-down the way one did.\n          // Ease INTO the slow-down crisply (rate 7); ease OUT of it — exiting\n          // the card, speeding back up — more gently (rate 5), so it visibly\n          // takes off again instead of snapping straight back to full speed.\n          const eRate = slowTarget > slowMid ? 5 : 7\n          slowMid += (slowTarget - slowMid) * (1 - Math.exp(-eRate * dt))\n          slowF += (slowMid - slowF) * (1 - Math.exp(-eRate * dt))\n          tCard += speed * slowF * dt\n          if (flingVel !== 0) {\n            tCard += flingVel * dt\n            flingVel *= Math.exp(-FLING_FRICTION * dt)\n            if (Math.abs(flingVel) < 1e-4) flingVel = 0\n          }\n        }\n      }\n\n      const gs = cardW + 'x' + cardH + '|' + segments\n      if (gs !== geoSig) {\n        const old = geometry\n        geometry = new THREE.PlaneGeometry(cardW, cardH, segments, 1)\n        for (const c of cards) c.mesh.geometry = geometry\n        if (old) old.dispose()\n        geoSig = gs\n      }\n\n      if ((tick++ % 20) === 0) {\n        const handle = d2.data()\n        const records = ((handle && handle.records()) || [])\n          .filter((r) => r.active !== false)\n          .slice()\n          .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n        const sig = records.map((r) => (r._id || '') + ':' + (r.thumbnail || '')).join('|')\n        if (sig !== builtSig) { buildCards(records); builtSig = sig }\n      }\n\n      const cRadius = bumpSlow(p, CARD_IN, CARD_R_PK, 1, CARD_R0, CARD_OVER, 1)\n      const cScale = bumpSlow(p, CARD_IN, CARD_S_PK, 1, CARD_S0, CARD_OVER, 1)\n\n      const N = Math.max(1, cards.length)\n      const span = Math.max(N * gap, 1)\n      const f = clamp01(flatten) * 0.95\n      const climbAt = (q) => q * (1 - f / 2) + f * Math.sin(2 * Math.PI * q) / (4 * Math.PI)\n      const totalClimb = climbAt(1) || 1\n      const tr = tiltDeg * Math.PI / 180\n      const ct = Math.cos(tr), st = Math.sin(tr)\n\n      for (let i = 0; i < cards.length; i++) {\n        const c = cards[i]\n        const s = (((i * gap + tCard) % span) + span) % span\n        if (s > 1 || p < CARD_IN) { c.mesh.visible = false; continue }\n        c.mesh.visible = true\n\n        const q = s\n        const angDeg = phase0 + q * 360 * turns\n        const a = angDeg * Math.PI / 180\n        const hx = Math.sin(a) * coil\n        const hz = Math.cos(a) * coil\n        const hy = (0.5 - climbAt(q) / totalClimb) * ySpan\n\n        const x = hx * ct - hy * st + offX\n        const y = hx * st + hy * ct + offY\n\n        // Radius scales the ring only; hz is left alone so the depth shading\n        // below still reads against the TRUE coil and does not wash out while\n        // the cards are flying in.\n        // Per-card idle float — a slow vertical bob with its own random\n        // frequency, phase and amplitude (via jitter), so the cards never sync\n        // and it reads as organic, like the tilt. On tFloat, so hover and the\n        // modal freeze never touch it.\n        const fY = TW.floatAmp * (0.55 + 0.9 * jitter(i, 21)) *\n          Math.sin(tFloat * (0.9 + 1.1 * jitter(i, 22)) + jitter(i, 23) * 6.2831853)\n        c.mesh.position.set(x * cRadius, -y * cRadius + fY, hz + zOff)\n        c.mesh.scale.setScalar(cScale)\n\n        const ends = Math.cos(Math.PI * q) * Math.cos(Math.PI * q)\n\n        // Yaw points the face outward; a small random pitch and roll about the\n        // WORLD axes give each card its own character. Built as quaternions\n        // because Euler order would apply the tilt in the card's own rotated\n        // frame and read as a twist instead of a tilt.\n        const wob = (wobMid + (wobEnd - wobMid) * ends) * Math.PI / 180\n        // Idle-float rotations: a gentle pitch bob about the horizontal axis and\n        // a slow twist about the card's OWN facing normal (its local Z — the\n        // axis in/out of the spiral). Per-card random freq/phase on tFloat, so\n        // they drift free of the orbit, the hover slow-down and the freeze. The\n        // twist is composed INNERMOST (before yaw), so it spins the card in its\n        // own plane rather than about a world axis.\n        const rp = TW.floatPitch * Math.sin(tFloat * (0.6 + 0.7 * jitter(i, 24)) + jitter(i, 25) * 6.2831853)\n        const rz = TW.floatTwist * Math.sin(tFloat * (0.5 + 0.6 * jitter(i, 26)) + jitter(i, 27) * 6.2831853)\n        qYaw.setFromAxisAngle(axisY, a)\n        qPitch.setFromAxisAngle(axisX, signed(i, 2) * wob * 0.6 + rp)\n        qRoll.setFromAxisAngle(axisZ, signed(i, 1) * wob)\n        qTwist.setFromAxisAngle(axisZ, rz)\n        c.mesh.quaternion.copy(qRoll).multiply(qPitch).multiply(qYaw).multiply(qTwist)\n\n        const bend = bendMid + (bendEnd - bendMid) * ends\n        const bendR = Math.min(80000, coil / Math.max(0.001, Math.abs(bend)))\n        const sh = c.material.userData.shader\n        if (sh) sh.uniforms.uBendR.value = bendR\n\n        // Depth and facing both DARKEN the card — neither may touch alpha.\n        // Fading with alpha lets the cards behind show through; these read as\n        // solid sheets, and against a black scene darkening looks identical.\n        const near = (hz + coil) / (2 * coil)\n        const depthShade = fade + (peak - fade) * Math.pow(near, 1.15)\n\n        const facing = Math.cos(a)\n        const bt = clamp01((facing + FACE_BAND) / (2 * FACE_BAND))\n        const faceShade = backDim + (1 - backDim) * bt * bt * (3 - 2 * bt)\n        c.material.color.setScalar(depthShade * faceShade)\n\n        // Alpha is reserved for the ends of the run and for the entrance, so a\n        // card dissolves as it leaves instead of turning into a black rectangle.\n        const edge = clamp01(Math.min(q, 1 - q) / Math.max(0.001, edgeFade))\n        // Entrance fade, staggered by index: card one is solid at 0.76, the\n        // last at 0.78, the rest spread evenly between. The stagger is divided\n        // ACROSS the pool rather than being a fixed delay per card, so adding\n        // records tightens the cascade instead of pushing its tail past the\n        // window — same rule the helix spacing follows.\n        const stag = (CARD_FADE_B - CARD_FADE_A) * (i / Math.max(1, N - 1))\n        const cIn = clamp01((p - (CARD_IN + stag)) / Math.max(1e-6, CARD_FADE_A - CARD_IN))\n        const op = edge * ease02(cIn)\n        c.material.opacity = op\n        // Solid cards must write depth to occlude each other; only the\n        // dissolving ones opt out. This only ever fires because opacity now\n        // reaches 1.0 — while depth fade lived in alpha it capped at `peak`\n        // and the test was silently always false.\n        c.material.depthWrite = op > 0.98\n      }\n\n      // Hover is resolved once per frame, not on pointer move: the cards are in\n      // motion, so the card under a stationary cursor changes on its own.\n      // Nothing is hoverable, and therefore nothing is clickable, until the\n      // cards have actually landed. Clearing the hover also drops the pointer\n      // cursor and the tooltip — offering either mid-flight would promise a\n      // click that does not work.\n      if (p < CARD_LIVE || dragging || modalOpen) {\n        setHover(null)\n      } else if (pointerIn && raycaster) {\n        hitList.length = 0\n        for (const c of cards) if (c.mesh.visible) hitList.push(c.mesh)\n        raycaster.setFromCamera(pointer, camera)\n        const hits = raycaster.intersectObjects(hitList, false)\n        setHover(hits.length ? hits[0].object.userData.record : null)\n      }\n\n      // ---- blob: worm at p=0, tangle at p=1 --------------------------------\n      if (blob && blobUni) {\n        const bt = t * 6.0\n\n        // Scroll timeline. SHAPE, SIZE and AGITATION are three separate curves\n        // now. They were braided together before, which is why the body shook\n        // all the way through the morph instead of only where it should.\n        //   0.00 → 0.25  thicken only, still one unbroken worm\n        //   0.25 → 0.50  balls take shape and fuse, at full spread — biggest\n        //   0.50 → 0.65  compresses to its tightest\n        //   0.65 → 0.75  HOLDS there while the agitation keeps building\n        //   0.75         tightest and most unstable — the cards arrive\n        //   0.75 → 1.00  expands and settles, never fully calm\n        // Read from the panel, but FORCE the ordering: a slider dragged past\n        // its neighbour would otherwise produce a zero-width stage and divide\n        // by zero, and NaN in a uniform blanks the whole draw with no error.\n        const MORPH_A = clamp01(TW.morphA)\n        const MORPH_B = Math.max(MORPH_A + 0.02, TW.morphB)\n        const SHRINK_B = Math.max(MORPH_B + 0.02, TW.shrinkB)\n        const COMP_B = Math.min(0.98, Math.max(SHRINK_B + 0.02, TW.compB))\n        const thick = ease01(clamp01(p / MORPH_A))\n        const bp = ease02(clamp01((p - MORPH_A) / (MORPH_B - MORPH_A)))\n\n        // Size holds at full spread through the whole morph, so the thickening\n        // stays legible, and ALL the compression happens in the last stretch.\n        const DIP_S = TW.dipS, END_S = Math.max(TW.dipS, TW.endS)\n        // Kept clear of COMP_B so the hold always has somewhere to launch from,\n        // however the panel's sliders are dragged.\n        const PL_PEAK = Math.min(0.98, Math.max(COMP_B + 0.02, 0.92))\n        // The compression itself bounces. It squashes PAST the minimum — the\n        // wind-up — then springs back to it. That undershoot is the cards'\n        // invisible anticipation made visible on the one thing you can see:\n        // you never watch the cards shrink-to-grow, but you watch the plasma do\n        // it here, and their recoil and the cards' outward throw are the same\n        // beat. The bottom sits late in the shrink so the spring back is quick.\n        const PL_BOT = Math.max(MORPH_B + 0.02, SHRINK_B - 0.05)\n        const PL_UNDER = DIP_S * 0.80\n        const ts = p <= MORPH_B\n          ? 1\n          : p <= SHRINK_B\n            ? bumpSlow(p, MORPH_B, PL_BOT, SHRINK_B, 1, PL_UNDER, DIP_S)\n            : p <= COMP_B\n              ? DIP_S\n              // Then the release: overshoots its rest size and settles, one\n              // beat behind the cards, still swelling as the ring closes in.\n              : bump(p, COMP_B, PL_PEAK, 1, DIP_S, END_S * 1.15, END_S)\n\n        // Agitation is deliberately NOT the morph curve. It stays low while the\n        // shape is forming, surges into the squeeze, peaks at the tightest\n        // point, then relaxes without ever reaching zero — plasma at rest is\n        // still plasma.\n        // Agitation no longer rides the shape curves — it is keyframed on its\n        // own, so the calm at 0.60 lands wherever the envelope says regardless\n        // of where the morph and the squeeze happen to sit.\n        const agit = Math.max(0, Math.min(1.12, kfAt(AGIT_KF, p)))\n\n        // Advance the orbit by this frame's SLICE at the current rate. Writing\n        // it as `bt * rate` instead re-derives the entire accumulated phase\n        // whenever the rate moves, so a change in agitation retroactively\n        // rewrites all of history — and since bt grows without bound, the jump\n        // gets worse the longer the page has been open.\n        const dbt = Math.max(0, bt - lastBt)\n        lastBt = bt\n        orbPhase += dbt * (0.18 + 0.28 * agit)\n\n        // The worm has to run OFF both edges at ANY viewport. Its rounded caps\n        // are what made it read as a finite 3D object instead of an endless\n        // line — so the span comes from the camera, never from a constant.\n        const visH = 2 * Math.tan((camera.fov * Math.PI / 180) / 2) * camera.position.z\n        const span = visH * 1.75\n\n        // Anchors: a lazy vertical S at p=0, a self-crossing knot at p=1.\n        const A = blobUni.uA.value\n        for (let i = 0; i < 8; i++) {\n          const u = i / 7\n          const wx = Math.sin(u * Math.PI * 2 * 0.85 + bt * 0.32) * 120\n          const wy = (u - 0.5) * span\n          // Orbit speeds up as it tangles — read from the integrated phase.\n          const ang = (i / 8) * Math.PI * 2 * 2 + orbPhase\n          const rad = (50 + jitter(i, 5) * 74) * ts\n          const tx = Math.cos(ang) * rad\n          const ty = Math.sin(ang * 1.35 + i) * rad * 0.85\n          // Matter gets restless as it compresses. Two INCOMMENSURATE wobbles\n          // per anchor — periods that never line up, so the agitation never\n          // repeats and never reads as a loop. Ramped by the morph, so p=0 is\n          // still the calm worm we already tuned.\n          // Frequencies roughly halved: the harshness was as much the RATE of\n          // the wobble as its size. Still mutually incommensurate, so it never\n          // settles into a loop.\n          const ag = TW.agit * agit * ts\n          const jx = Math.sin(bt * 0.47 + i * 2.1) + 0.55 * Math.sin(bt * 0.89 + i * 3.7)\n          const jy = Math.cos(bt * 0.41 + i * 1.7) + 0.55 * Math.cos(bt * 0.79 + i * 4.3)\n          A[i].set(wx + (tx - wx) * bp + jx * ag, wy + (ty - wy) * bp + jy * ag)\n        }\n\n        // Resample through the anchors so the spine is a smooth curve rather\n        // than 7 straight legs — 8 points alone read as a bent wire.\n        const P = blobUni.uP.value\n        let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9\n        for (let k = 0; k < 32; k++) {\n          const uu = (k / 31) * 7\n          const i = Math.min(Math.floor(uu), 6)\n          const f = uu - i\n          const p0 = A[Math.max(i - 1, 0)], p1 = A[i]\n          const p2 = A[i + 1], p3 = A[Math.min(i + 2, 7)]\n          const f2 = f * f, f3 = f2 * f\n          const x = 0.5 * (2 * p1.x + (-p0.x + p2.x) * f +\n            (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * f2 +\n            (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * f3)\n          const y = 0.5 * (2 * p1.y + (-p0.y + p2.y) * f +\n            (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * f2 +\n            (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * f3)\n          P[k].set(x, y)\n          if (x < minX) minX = x; if (x > maxX) maxX = x\n          if (y < minY) minY = y; if (y > maxY) maxY = y\n        }\n\n        // Thickness rides the FIRST stage, so the only thing happening before\n        // 0.25 is the worm getting fatter. Everything then scales by `ts`\n        // TOGETHER — leaving the tube radius outside the shrink would floor the\n        // plasma at the worm's own thickness and no dip could reach 25%.\n        const rr = (TW.r0 + TW.rThick * thick + TW.rMorph * bp) * ts\n        // Near zero while it is a worm: smin over a CHAIN of segments compounds\n        // at every shared endpoint and beads the tube. The melt is only wanted\n        // once the knot starts crossing itself.\n        const kk = (0.5 + 62 * bp) * ts\n        blobUni.uR.value = rr\n        blobUni.uK.value = kk\n        blobUni.uBall.value = 58 * bp * ts\n        // Slightly wider bleed as it tangles — that is when stretches meet —\n        // but it has to shrink with the body or a small plasma blends end to\n        // end and goes grey again.\n        blobUni.uBlend.value = Math.max(10, (38 + 34 * bp) * ts)\n        // Glow and rim ramp across the WHOLE rail, not just the morph: they are\n        // what was making the worm's segmentation legible at rest.\n        // CUBED, not linear: linear put over half the light in by mid-rail,\n        // which is where it read as too much. Cubing lifts the resting value,\n        // holds the middle down, and saves the surge for the plasma.\n        const gp = p * p * p\n        blobUni.uGlow.value = TW.glow0 + TW.glowG * gp\n        // Natural through the worm, a lift once balled up — the soft blend lets\n        // the ends go pale, so a touch more vibrance keeps them vivid while the\n        // crossings are allowed to fade to grey.\n        blobUni.uSat.value = 1.0 + 0.55 * bp\n        blobUni.uRim.value = 0.50 + 0.65 * gp\n        blobUni.uSpan.value = 1.0\n        blobUni.uPhase.value = -t * 0.23\n\n        // Quad sized to the spine's extent plus the blend and glow margin, so\n        // the field is only evaluated where the surface can possibly be.\n        const pad = rr + kk + blobUni.uBall.value + 260\n        blob.position.set((minX + maxX) / 2, (minY + maxY) / 2, 0)\n        blob.scale.set((maxX - minX) + pad * 2, (maxY - minY) + pad * 2, 1)\n        // Composite quad tracks the blob's screen footprint exactly, so the\n        // upscale pass only touches pixels the plasma could reach.\n        dispQuad.position.copy(blob.position)\n        dispQuad.scale.copy(blob.scale)\n      }\n\n      // Pass 1: plasma into the low-res target. Pass 2: cards (full res) with\n      // the composite quad sampling that target.\n      if (rt && dispUni && plasmaScene) {\n        // uRes is the DRAWING BUFFER size; the composite samples the RT by\n        // screen coordinate, so RT resolution is decoupled (sized in resize).\n        dispUni.uRes.value.set(cv.width, cv.height)\n        if (gridUni) gridUni.uRes.value.set(cv.width, cv.height)\n        renderer.setRenderTarget(rt)\n        renderer.render(plasmaScene, camera)\n        renderer.setRenderTarget(null)\n      }\n\n      renderer.render(scene, camera)\n    }\n\n    // ---- loop ------------------------------------------------------------\n\n    let started = false\n    const ro = new ResizeObserver(() => { needResize = true })\n    ro.observe(cv)\n    const io = new IntersectionObserver((es) => {\n      es.forEach((e) => { onScreen = e.isIntersecting })\n    }, { threshold: 0 })\n    io.observe(cv)\n\n    d2.raf((now) => {\n      if (!started) {\n        THREE = d2.lib('three@0.170.0')\n        if (!THREE) return\n        init()\n        started = true\n      }\n      if (!onScreen) { last = 0; return }\n      // Zoom changes devicePixelRatio but not the element size, so nothing else\n      // would trigger a resize - catch it here.\n      if ((window.devicePixelRatio || 1) !== lastDpr) needResize = true\n      if (needResize) { needResize = false; resize() }\n      if (sizedOk) frame(now)\n    })\n\n    d2.onUnmount(() => {\n      ro.disconnect(); io.disconnect()\n      if (scene) disposeCards()\n      pruneTextures(new Set())\n      if (geometry) geometry.dispose()\n      if (renderer) renderer.dispose()\n    })\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"gap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gap"}}}},"coil":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coil"}}}},"ySpan":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ySpan"}}}},"flatten":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flatten"}}}},"bend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bend"}}}},"bendEnds":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bendEnds"}}}},"wobble":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobble"}}}},"wobbleMid":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobbleMid"}}}},"segments":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"segments"}}}},"cardW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardW"}}}},"cardH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardH"}}}},"turns":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"turns"}}}},"phase0":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"phase0"}}}},"tilt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tilt"}}}},"persp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"persp"}}}},"zOffset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zOffset"}}}},"fade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fade"}}}},"peak":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"peak"}}}},"backDim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backDim"}}}},"edgeFade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"edgeFade"}}}},"offsetX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetX"}}}},"offsetY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetY"}}}},"enter":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"enter"}}}},"scrollRate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"scrollRate"}}}},"ease":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ease"}}}},"hud":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"hud"}}}},"storeRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"storeRef"}}}}}}}}}},{"_id":"6a61623108b200d52f064731","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"wh-stage-ckpt1","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Wormhole Stage — FROZEN CHECKPOINT 2026-07-22. Exact copy of wh-stage at the\n// point Lucas approved the plasma colour treatment. Do not edit — this is the\n// restore point. Live work continues on wh-stage.\n\nconst clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)\nconst ease01 = (u) => u * u * (3 - 2 * u)\nconst ease02 = (u) => u * u * u * (u * (u * 6 - 15) + 10)\n\nconst TW_DEF = {\n  morphA: 0.26, morphB: 0.51, shrinkB: 0.81, compB: 0.84,\n  dipS: 0.21, endS: 0.38, agit: 140,\n  glow0: 0.30, glowG: 0.10,\n  r0: 54, rThick: 51, rMorph: 19\n}\n\nconst AGIT_KF = [[0, 0], [0.47, 0], [0.60, 0.5], [0.90, 0.5], [1, 1]]\n\nconst kfAt = (kf, x) => {\n  const n = kf.length\n  if (x <= kf[0][0]) return kf[0][1]\n  if (x >= kf[n - 1][0]) return kf[n - 1][1]\n  let i = 0\n  while (i < n - 2 && x > kf[i + 1][0]) i++\n  const x0 = kf[i][0], y0 = kf[i][1]\n  const x1 = kf[i + 1][0], y1 = kf[i + 1][1]\n  const h = Math.max(1e-6, x1 - x0)\n  const u = (x - x0) / h\n  const xPrev = i > 0 ? kf[i - 1][0] : x0 - h\n  const yPrev = i > 0 ? kf[i - 1][1] : y0\n  const xNext = i + 2 < n ? kf[i + 2][0] : x1 + h\n  const yNext = i + 2 < n ? kf[i + 2][1] : y1\n  const m0 = (y1 - yPrev) / (x1 - xPrev) * h\n  const m1 = (yNext - y0) / (xNext - x0) * h\n  const u2 = u * u, u3 = u2 * u\n  const v = (2 * u3 - 3 * u2 + 1) * y0 + (u3 - 2 * u2 + u) * m0 +\n    (-2 * u3 + 3 * u2) * y1 + (u3 - u2) * m1\n  return v\n}\n\nconst easeOut3 = (u) => 1 - (1 - u) * (1 - u) * (1 - u)\nconst bump = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * easeOut3((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\nconst bumpSlow = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * ease02((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\nconst bumpMed = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * ease01((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\nconst bumpLate = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) {\n    const u = (x - t0) / Math.max(1e-6, t1 - t0)\n    return v0 + (v1 - v0) * (u * u * ease02(u))\n  }\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n\nconst CARD_IN = 0.74\nconst CARD_LIVE = 0.99\nconst CARD_FADE_A = 0.76, CARD_FADE_B = 0.78\nconst CARD_R_PK = 0.86, CARD_S_PK = 0.89\nconst CARD_R0 = 0.12, CARD_S0 = 0.08, CARD_OVER = 1.15\nconst TW_SPEC = [\n  ['morphA', 'morph start', 0, 1, 0.01],\n  ['morphB', 'morph end / shrink start', 0, 1, 0.01],\n  ['shrinkB', 'min size reached', 0, 1, 0.01],\n  ['compB', 'hold end / agit peak', 0, 1, 0.01],\n  ['dipS', 'min scale', 0.05, 1, 0.005],\n  ['endS', 'rest scale', 0.05, 1, 0.005],\n  ['agit', 'agitation', 0, 140, 1],\n  ['glow0', 'glow at rest', 0, 1, 0.01],\n  ['glowG', 'glow gain', 0, 2, 0.01],\n  ['r0', 'thickness base', 10, 90, 1],\n  ['rThick', 'thicken gain', 0, 60, 1],\n  ['rMorph', 'morph gain', 0, 80, 1]\n]\n\nconst FACE_BAND = 0.35\n\nconst PRODUCTS = [\n  [0.608, 0.361, 1.000],\n  [0.357, 0.525, 1.000],\n  [0.302, 0.875, 0.902],\n  [0.302, 0.878, 0.631],\n  [0.518, 0.878, 0.353],\n  [0.910, 0.910, 0.302],\n  [1.000, 0.416, 0.302],\n  [1.000, 0.302, 0.620]\n]\n\nconst BLOB_VERT = `\nout vec3 vWorld;\nvoid main(){\n  vec4 wp = modelMatrix * vec4(position, 1.0);\n  vWorld = wp.xyz;\n  gl_Position = projectionMatrix * viewMatrix * wp;\n}\n`\n\nconst BLOB_FRAG = `\nprecision highp float;\nin vec3 vWorld;\nout vec4 fragColor;\nuniform vec2 uP[32];\nuniform vec2 uA[8];\nuniform vec3 uPal[8];\nuniform float uR, uK, uBall, uGloss, uGlow, uSpan, uPhase, uBlend, uRim, uSat;\n\nfloat smin(float a, float b, float k){\n  float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0);\n  return mix(b, a, h) - k*h*(1.0-h);\n}\nvec3 ramp(float t){\n  t = fract(t) * 8.0;\n  int i = int(floor(t));\n  float f = t - float(i);\n  return mix(uPal[i], uPal[i == 7 ? 0 : i + 1], f);\n}\nvoid main(){\n  vec2 q = vWorld.xy;\n\n  float d = 1e9, dmin = 1e9;\n  for(int i=0;i<31;i++){\n    vec2 a = uP[i], b = uP[i+1];\n    vec2 pa = q-a, ba = b-a;\n    float h = clamp(dot(pa,ba)/max(dot(ba,ba), 1e-4), 0.0, 1.0);\n    float dd = length(q - (a + ba*h));\n    d = smin(d, dd, uK);\n    dmin = min(dmin, dd);\n  }\n  for(int i=0;i<8;i++) d = smin(d, length(q - uA[i]) - uBall, uK);\n\n  vec3 cAcc = vec3(0.0);\n  vec2 dAcc = vec2(0.0);\n  float wAcc = 0.0;\n  for(int i=0;i<31;i++){\n    vec2 a = uP[i], b = uP[i+1];\n    vec2 pa = q-a, ba = b-a;\n    float h = clamp(dot(pa,ba)/max(dot(ba,ba), 1e-4), 0.0, 1.0);\n    vec2 off = q - (a + ba*h);\n    float dd = length(off);\n    float x = (dd - dmin)/uBlend;\n    float w = 1.0/(1.0 + x*x*x*x);\n    float wc = w;\n    cAcc += ramp(((float(i) + h)/31.0)*uSpan + uPhase) * wc;\n    dAcc += off * (w/max(dd, 1e-4));\n    wAcc += wc;\n  }\n  vec3 base = cAcc / max(wAcc, 1e-4);\n  float lum = dot(base, vec3(0.2126, 0.7152, 0.0722));\n  base = clamp(mix(vec3(lum), base, uSat), 0.0, 1.0);\n\n  float dn = clamp(d/uR, 0.0, 1.0);\n  vec2 g = normalize(dAcc + vec2(1e-5));\n  vec3 n = normalize(vec3(g*dn, sqrt(max(1.0 - dn*dn, 0.0))));\n\n  vec3 L = normalize(vec3(-0.32, 0.52, 0.79));\n  float lam = 0.60 + 0.40*max(dot(n, L), 0.0);\n  float spec = pow(max(dot(n, L), 0.0), 26.0) * uGloss;\n  float rim = pow(1.0 - n.z, 3.0);\n\n  float aa = fwidth(d)*0.9 + 1e-4;\n  float core = 1.0 - smoothstep(-aa, aa, d - uR);\n  float ex = max(d - uR, 0.0);\n  float halo = (exp(-ex*0.055)*0.72 + exp(-ex*0.013)*0.38) * uGlow;\n  float a = clamp(core + halo, 0.0, 1.0);\n  if(a < 0.004) discard;\n\n  vec3 lit = base*lam + vec3(spec) + base*rim*uRim;\n  vec3 col = mix(base*1.18, lit, core);\n  fragColor = vec4(col, a);\n}\n`\nconst jitter = (i, salt) => {\n  const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453\n  return v - Math.floor(v)\n}\nconst signed = (i, salt) => jitter(i, salt) * 2 - 1\n\nexport default {\n  css: `\n    .st-wrap { position: absolute; inset: 0; overflow: hidden; }\n    .st-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n\n    .st-hud {\n      position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);\n      z-index: 5; pointer-events: none; font: 11px/1 Inter, sans-serif;\n      letter-spacing: .18em; color: #5d7085; text-transform: uppercase;\n      display: flex; align-items: center; gap: 12px;\n    }\n    .st-hud .bar { width: 180px; height: 2px; background: rgba(255,255,255,.10); border-radius: 2px; }\n    .st-hud .bar i { display: block; height: 100%; width: 0%; border-radius: 2px;\n                     background: linear-gradient(90deg,#9b5cff,#4ddfe6,#84e05a,#ff4d9e); }\n\n    .st-tip { position: absolute; left: 0; top: 0; pointer-events: none;\n              display: flex; align-items: center; height: 58px; z-index: 6;\n              will-change: transform; }\n    .st-tip-glass {\n      position: relative; overflow: hidden;\n      height: 14px; width: 14px; border-radius: 29px;\n      font-family: Inter, -apple-system, sans-serif;\n      background:\n        linear-gradient(155deg, rgba(216,236,255,0.11), rgba(216,236,255,0) 46%),\n        rgba(10, 16, 24, 0.44);\n      backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      -webkit-backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      box-shadow: inset 0 1px 0 rgba(226,241,255,0.22),\n                  inset 1px 0 0 rgba(226,241,255,0.10),\n                  inset -1px 0 0 rgba(226,241,255,0.10),\n                  inset 0 -1px 0 rgba(226,241,255,0.26),\n                  inset 0 -7px 11px -7px rgba(216,236,255,0.22),\n                  0 6px 16px rgba(0,0,0,0.24);\n      opacity: 0;\n      transition: width 129ms cubic-bezier(.7, 0, .6, .25),\n                  height 129ms cubic-bezier(.7, 0, .6, .25),\n                  opacity 102ms ease-in 27ms;\n      will-change: width, height, opacity;\n    }\n    .st-tip.on .st-tip-glass {\n      opacity: 1; height: 58px;\n      transition: width 129ms cubic-bezier(.4, .75, .3, 1),\n                  height 129ms cubic-bezier(.4, .75, .3, 1),\n                  opacity 102ms ease-out;\n    }\n    .st-tip-inner {\n      position: absolute; left: 0; top: 50%; height: 58px;\n      transform: translateY(-50%);\n      display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;\n      align-content: center; column-gap: 10px;\n      padding: 0 17px 0 14px; white-space: nowrap;\n      opacity: 0; transition: opacity 57ms ease-in 41ms;\n    }\n    .st-tip.on .st-tip-inner { opacity: 1; transition: opacity 57ms ease-out 31ms; }\n    .st-tip-arrow { grid-row: 1 / span 2; align-self: center;\n                    display: flex; align-items: center; justify-content: center; color: #f4f4f5; }\n    .st-tip-name { font-size: 14px; font-weight: 600; color: #f4f4f5; line-height: 1.3;\n                   text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n    .st-tip-cat { font-size: 13px; color: #cfcfd6; line-height: 1.3;\n                  text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n\n    .st-tw { position: absolute; top: 16px; right: 16px; width: 244px; z-index: 40;\n             padding: 12px 12px 10px; border-radius: 12px;\n             background: rgba(16,16,20,0.82); backdrop-filter: blur(14px);\n             border: 1px solid rgba(255,255,255,0.10);\n             font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;\n             color: #d6d6de; }\n    .st-tw-h { font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase;\n               color: #8a8a96; margin-bottom: 8px; }\n    .st-tw-row { display: grid; grid-template-columns: 1fr 34px; gap: 2px 6px;\n                 align-items: center; margin-bottom: 7px; }\n    .st-tw-row > span { grid-column: 1 / span 2; color: #9a9aa6; font-size: 10px; }\n    .st-tw-row > input { width: 100%; accent-color: #7bd6ff; height: 14px; }\n    .st-tw-row > i { font-style: normal; text-align: right; color: #f0f0f5; }\n    .st-tw-b { width: 100%; margin-top: 4px; padding: 6px; cursor: pointer;\n               border-radius: 7px; border: 1px solid rgba(255,255,255,0.16);\n               background: rgba(255,255,255,0.07); color: #f0f0f5;\n               font: inherit; }\n    .st-tw-b:hover { background: rgba(255,255,255,0.13); }\n    .st-tw-out { width: 100%; height: 52px; margin-top: 6px; resize: none;\n                 border-radius: 7px; border: 1px solid rgba(255,255,255,0.12);\n                 background: rgba(0,0,0,0.35); color: #b9ffd0;\n                 font: 10px/1.3 ui-monospace, Menlo, monospace; padding: 5px; }\n  `,\n\n  render(d2) {\n    const hud = Number(d2.param('hud', 1))\n      ? '<div class=\"st-hud\"><span data-hud-lab>p 0.00</span>' +\n        '<span class=\"bar\"><i data-hud-bar></i></span></div>'\n      : ''\n    const tw = Number(d2.param('hud', 1)) >= 2\n      ? '<div class=\"st-tw\">' +\n        '<div class=\"st-tw-h\">plasma timeline</div>' +\n        '<label class=\"st-tw-row\"><span>scrub p</span>' +\n        '<input type=\"range\" min=\"0\" max=\"1\" step=\"0.005\" value=\"0\" data-tw-p>' +\n        '<i data-tw-pv>0.00</i></label>' +\n        TW_SPEC.map((s) =>\n          '<label class=\"st-tw-row\"><span>' + s[1] + '</span>' +\n          '<input type=\"range\" min=\"' + s[2] + '\" max=\"' + s[3] + '\" step=\"' + s[4] +\n          '\" value=\"' + TW_DEF[s[0]] + '\" data-tw-k=\"' + s[0] + '\">' +\n          '<i data-tw-v=\"' + s[0] + '\">' + TW_DEF[s[0]] + '</i></label>').join('') +\n        '<button class=\"st-tw-b\" data-tw-copy>copy config</button>' +\n        '<textarea class=\"st-tw-out\" data-tw-out readonly></textarea>' +\n        '</div>'\n      : ''\n    return '<div class=\"st-wrap\" data-d2-static>' +\n      '<canvas class=\"st-canvas\" data-cv></canvas>' +\n      '<div class=\"st-tip\" data-tip><div class=\"st-tip-glass\" data-tip-glass>' +\n      '<div class=\"st-tip-inner\" data-tip-inner>' +\n      '<span class=\"st-tip-arrow\">' +\n      '<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" aria-hidden=\"true\">' +\n      '<path d=\"M4.6 11.4 11.4 4.6M6.2 4.6h5.2v5.2\" fill=\"none\" stroke=\"currentColor\"' +\n      ' stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span>' +\n      '<span class=\"st-tip-name\" data-tip-name></span>' +\n      '<span class=\"st-tip-cat\" data-tip-cat></span>' +\n      '</div></div></div>' + hud + tw + '</div>'\n  },\n\n  mounted(d2, root) {\n    const cv = root.querySelector('[data-cv]')\n    if (!cv) return\n    const hudLab = root.querySelector('[data-hud-lab]')\n    const hudBar = root.querySelector('[data-hud-bar]')\n    const tip = root.querySelector('[data-tip]')\n    const tipGlass = root.querySelector('[data-tip-glass]')\n    const tipInner = root.querySelector('[data-tip-inner]')\n    const tipName = root.querySelector('[data-tip-name]')\n    const tipCat = root.querySelector('[data-tip-cat]')\n\n    const TIP_H = 58\n    const TIP_MIN = 14\n    let tipW = TIP_H\n\n    let THREE = null\n    let renderer = null, scene = null, camera = null, raycaster = null, pointer = null\n    let geometry = null\n    let cards = []\n    let builtSig = '', geoSig = ''\n    let t = 0, last = 0, tick = 0\n    let sizedOk = false, onScreen = true, needResize = true\n    let hoverRec = null, pointerIn = false, mouseX = 0, mouseY = 0, canvasRect = null\n    const hitList = []\n    const texCache = new Map()\n    let qYaw, qPitch, qRoll, axisX, axisY, axisZ\n    let blob = null, blobUni = null\n    let orbPhase = 0, lastBt = 0\n\n    let pTarget = 0\n    let p = 0\n\n    const TW = Object.assign({}, TW_DEF)\n    const twOut = root.querySelector('[data-tw-out]')\n    const twP = root.querySelector('[data-tw-p]')\n    const twPv = root.querySelector('[data-tw-pv]')\n    const twCopy = root.querySelector('[data-tw-copy]')\n\n    root.querySelectorAll('[data-tw-k]').forEach((el) => {\n      const k = el.getAttribute('data-tw-k')\n      const lab = root.querySelector('[data-tw-v=\"' + k + '\"]')\n      d2.on(el, 'input', () => {\n        TW[k] = Number(el.value)\n        if (lab) lab.textContent = el.value\n      })\n    })\n    if (twP) {\n      d2.on(twP, 'input', () => {\n        pTarget = Number(twP.value)\n        if (twPv) twPv.textContent = pTarget.toFixed(2)\n      })\n    }\n    if (twCopy) {\n      d2.on(twCopy, 'click', () => {\n        const json = JSON.stringify(TW)\n        if (twOut) { twOut.value = json; twOut.select() }\n        try { navigator.clipboard.writeText(json) } catch (e) { /* iframe may block */ }\n      })\n    }\n\n    const refreshRect = () => { canvasRect = cv.getBoundingClientRect() }\n\n    const placeTip = () => {\n      if (!tip) return\n      const w = tipW, h = TIP_H\n      const cw = cv.clientWidth, ch = cv.clientHeight\n      let x = mouseX + 18\n      let y = mouseY - h / 2\n      if (x + w > cw - 10) x = mouseX - 18 - w\n      if (y < 10) y = 10\n      if (y + h > ch - 10) y = ch - 10 - h\n      tip.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'\n    }\n\n    const setHover = (rec) => {\n      if (rec === hoverRec) return\n      hoverRec = rec\n      cv.style.cursor = rec ? 'pointer' : 'default'\n      if (!rec) {\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n        return\n      }\n      if (tipName) tipName.textContent = rec.name || ''\n      if (tipCat) tipCat.textContent = rec.category || ''\n      tipW = tipInner ? tipInner.offsetWidth : TIP_H\n      placeTip()\n      if (tipGlass) tipGlass.style.width = tipW + 'px'\n      if (tip) tip.classList.add('on')\n    }\n\n    const openCard = (rec) => {\n      if (!rec) return\n      const s = d2.store(String(d2.param('storeRef', 'wh-ui')))\n      if (!s) return\n      const tags = Array.isArray(rec.tags) ? rec.tags.join('  ·  ') : (rec.tags || '')\n      const useArtboard = rec.openMode === 'artboard' && !!rec.artboardRef\n      s.set('name', rec.name || '')\n      s.set('category', rec.category || '')\n      s.set('tags', tags)\n      s.set('thumb', rec.thumbnail || '')\n      s.set('artboardRef', rec.artboardRef || '')\n      s.set('mode', useArtboard ? 'artboard' : 'thumb')\n      s.set('modalOpen', true)\n    }\n\n    const BEND_CHUNK = [\n      'vec3 transformed = vec3( position );',\n      'float phi = transformed.x / uBendR;',\n      'transformed.x = uBendR * sin( phi );',\n      'transformed.z += uBendR * cos( phi ) - uBendR;'\n    ].join('\\n')\n\n    const makeMaterial = () => {\n      const m = new THREE.MeshBasicMaterial({\n        side: THREE.DoubleSide, transparent: true, depthWrite: false, opacity: 0\n      })\n      m.onBeforeCompile = (shader) => {\n        shader.uniforms.uBendR = { value: 1000 }\n        shader.vertexShader = 'uniform float uBendR;\\n' + shader.vertexShader\n        shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', BEND_CHUNK)\n        m.userData.shader = shader\n      }\n      return m\n    }\n\n    const texFor = (url) => {\n      let tex = texCache.get(url)\n      if (!tex) {\n        const loader = new THREE.TextureLoader()\n        loader.setCrossOrigin('anonymous')\n        tex = loader.load(url)\n        tex.colorSpace = THREE.SRGBColorSpace\n        tex.anisotropy = renderer.capabilities.getMaxAnisotropy()\n        texCache.set(url, tex)\n      }\n      return tex\n    }\n\n    const pruneTextures = (keep) => {\n      for (const [url, tex] of texCache) {\n        if (!keep.has(url)) { tex.dispose(); texCache.delete(url) }\n      }\n    }\n\n    const disposeCards = () => {\n      for (const c of cards) { scene.remove(c.mesh); if (c.material) c.material.dispose() }\n      cards = []\n    }\n\n    const buildCards = (records) => {\n      disposeCards()\n      const used = new Set()\n      records.forEach((rec) => {\n        const material = makeMaterial()\n        if (rec.thumbnail) { material.map = texFor(rec.thumbnail); used.add(rec.thumbnail) }\n        const mesh = new THREE.Mesh(geometry, material)\n        mesh.visible = false\n        mesh.userData.record = rec\n        scene.add(mesh)\n        cards.push({ mesh, material, record: rec })\n      })\n      pruneTextures(used)\n    }\n\n    const init = () => {\n      renderer = new THREE.WebGLRenderer({ canvas: cv, alpha: true, antialias: true })\n      renderer.setClearAlpha(0)\n      renderer.outputColorSpace = THREE.SRGBColorSpace\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))\n\n      scene = new THREE.Scene()\n      camera = new THREE.PerspectiveCamera(45, 1, 1, 12000)\n      raycaster = new THREE.Raycaster()\n      pointer = new THREE.Vector2()\n\n      qYaw = new THREE.Quaternion(); qPitch = new THREE.Quaternion(); qRoll = new THREE.Quaternion()\n      axisX = new THREE.Vector3(1, 0, 0)\n      axisY = new THREE.Vector3(0, 1, 0)\n      axisZ = new THREE.Vector3(0, 0, 1)\n\n      d2.on(window, 'wheel', (e) => {\n        const rate = Number(d2.param('scrollRate', 0.00075))\n        pTarget = clamp01(pTarget + (e.deltaY || 0) * rate)\n      }, { passive: true })\n\n      const onPointer = (e) => {\n        if (!canvasRect) refreshRect()\n        mouseX = e.clientX - canvasRect.left\n        mouseY = e.clientY - canvasRect.top\n        pointer.x = (mouseX / canvasRect.width) * 2 - 1\n        pointer.y = -(mouseY / canvasRect.height) * 2 + 1\n        pointerIn = true\n        placeTip()\n      }\n      if ('onpointerrawupdate' in cv) d2.on(cv, 'pointerrawupdate', onPointer)\n      d2.on(cv, 'pointermove', onPointer)\n      d2.on(window, 'scroll', refreshRect, { passive: true })\n\n      d2.on(cv, 'pointerleave', () => {\n        pointerIn = false\n        hoverRec = null\n        cv.style.cursor = 'default'\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n      })\n\n      d2.on(cv, 'click', () => { if (hoverRec) openCard(hoverRec) })\n\n      blobUni = {\n        uP: { value: Array.from({ length: 32 }, () => new THREE.Vector2()) },\n        uA: { value: Array.from({ length: 8 }, () => new THREE.Vector2()) },\n        uPal: { value: PRODUCTS.map((c) => new THREE.Vector3(c[0], c[1], c[2])) },\n        uR: { value: 26 },\n        uK: { value: 14 },\n        uBall: { value: 0 },\n        uGloss: { value: 0.62 },\n        uGlow: { value: 0.55 },\n        uRim: { value: 0.85 },\n        uSat: { value: 1.0 },\n        uSpan: { value: 1 },\n        uPhase: { value: 0 },\n        uBlend: { value: 45 }\n      }\n      const blobMat = new THREE.ShaderMaterial({\n        glslVersion: THREE.GLSL3,\n        uniforms: blobUni,\n        vertexShader: BLOB_VERT,\n        fragmentShader: BLOB_FRAG,\n        transparent: true,\n        depthWrite: false,\n        depthTest: true,\n        side: THREE.DoubleSide\n      })\n      blob = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), blobMat)\n      scene.add(blob)\n    }\n\n    const resize = () => {\n      const w = cv.clientWidth || 1\n      const h = cv.clientHeight || 1\n      const persp = Number(d2.param('persp', 1400)) || 1400\n      refreshRect()\n      sizedOk = w > 1 && h > 1\n      renderer.setSize(w, h, false)\n      camera.fov = 2 * Math.atan((h / 2) / persp) * 180 / Math.PI\n      camera.aspect = w / h\n      camera.position.set(0, 0, persp)\n      camera.updateProjectionMatrix()\n    }\n\n    const frame = (now) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016\n      last = now\n\n      const ease = Number(d2.param('ease', 6))\n      p += (pTarget - p) * (1 - Math.exp(-ease * dt))\n\n      if (hudLab) hudLab.textContent = 'p ' + p.toFixed(2)\n      if (hudBar) hudBar.style.width = (p * 100).toFixed(1) + '%'\n\n      const speed = Number(d2.param('speed', 0.045))\n      const gap = Math.max(0.001, Number(d2.param('gap', 0.108)))\n      const coil = Number(d2.param('coil', 520))\n      const ySpan = Number(d2.param('ySpan', 1950))\n      const flatten = Number(d2.param('flatten', 0.66))\n      const bendMid = Number(d2.param('bend', 0.7))\n      const bendEnd = Number(d2.param('bendEnds', 1.8))\n      const wobEnd = Number(d2.param('wobble', 14))\n      const wobMid = Number(d2.param('wobbleMid', 3.5))\n      const segments = Math.max(2, Math.min(96, Math.round(Number(d2.param('segments', 40)))))\n      const cardW = Number(d2.param('cardW', 448))\n      const cardH = Number(d2.param('cardH', 283))\n      const turns = Number(d2.param('turns', -1.5))\n      const phase0 = Number(d2.param('phase0', 270))\n      const tiltDeg = Number(d2.param('tilt', 8))\n      const zOff = Number(d2.param('zOffset', -420))\n      const fade = Number(d2.param('fade', 0.12))\n      const peak = Number(d2.param('peak', 0.95))\n      const backDim = Number(d2.param('backDim', 0.28))\n      const edgeFade = Number(d2.param('edgeFade', 0.14))\n      const offX = Number(d2.param('offsetX', 0))\n      const offY = Number(d2.param('offsetY', 0))\n\n      t += speed * dt\n\n      const gs = cardW + 'x' + cardH + '|' + segments\n      if (gs !== geoSig) {\n        const old = geometry\n        geometry = new THREE.PlaneGeometry(cardW, cardH, segments, 1)\n        for (const c of cards) c.mesh.geometry = geometry\n        if (old) old.dispose()\n        geoSig = gs\n      }\n\n      if ((tick++ % 20) === 0) {\n        const handle = d2.data()\n        const records = ((handle && handle.records()) || [])\n          .filter((r) => r.active !== false)\n          .slice()\n          .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n        const sig = records.map((r) => (r._id || '') + ':' + (r.thumbnail || '')).join('|')\n        if (sig !== builtSig) { buildCards(records); builtSig = sig }\n      }\n\n      const cRadius = bumpSlow(p, CARD_IN, CARD_R_PK, 1, CARD_R0, CARD_OVER, 1)\n      const cScale = bumpSlow(p, CARD_IN, CARD_S_PK, 1, CARD_S0, CARD_OVER, 1)\n\n      const N = Math.max(1, cards.length)\n      const span = Math.max(N * gap, 1)\n      const f = clamp01(flatten) * 0.95\n      const climbAt = (q) => q * (1 - f / 2) + f * Math.sin(2 * Math.PI * q) / (4 * Math.PI)\n      const totalClimb = climbAt(1) || 1\n      const tr = tiltDeg * Math.PI / 180\n      const ct = Math.cos(tr), st = Math.sin(tr)\n\n      for (let i = 0; i < cards.length; i++) {\n        const c = cards[i]\n        const s = (((i * gap + t) % span) + span) % span\n        if (s > 1 || p < CARD_IN) { c.mesh.visible = false; continue }\n        c.mesh.visible = true\n\n        const q = s\n        const angDeg = phase0 + q * 360 * turns\n        const a = angDeg * Math.PI / 180\n        const hx = Math.sin(a) * coil\n        const hz = Math.cos(a) * coil\n        const hy = (0.5 - climbAt(q) / totalClimb) * ySpan\n\n        const x = hx * ct - hy * st + offX\n        const y = hx * st + hy * ct + offY\n\n        c.mesh.position.set(x * cRadius, -y * cRadius, hz + zOff)\n        c.mesh.scale.setScalar(cScale)\n\n        const ends = Math.cos(Math.PI * q) * Math.cos(Math.PI * q)\n\n        const wob = (wobMid + (wobEnd - wobMid) * ends) * Math.PI / 180\n        qYaw.setFromAxisAngle(axisY, a)\n        qPitch.setFromAxisAngle(axisX, signed(i, 2) * wob * 0.6)\n        qRoll.setFromAxisAngle(axisZ, signed(i, 1) * wob)\n        c.mesh.quaternion.copy(qRoll).multiply(qPitch).multiply(qYaw)\n\n        const bend = bendMid + (bendEnd - bendMid) * ends\n        const bendR = Math.min(80000, coil / Math.max(0.001, Math.abs(bend)))\n        const sh = c.material.userData.shader\n        if (sh) sh.uniforms.uBendR.value = bendR\n\n        const near = (hz + coil) / (2 * coil)\n        const depthShade = fade + (peak - fade) * Math.pow(near, 1.15)\n\n        const facing = Math.cos(a)\n        const bt = clamp01((facing + FACE_BAND) / (2 * FACE_BAND))\n        const faceShade = backDim + (1 - backDim) * bt * bt * (3 - 2 * bt)\n        c.material.color.setScalar(depthShade * faceShade)\n\n        const edge = clamp01(Math.min(q, 1 - q) / Math.max(0.001, edgeFade))\n        const stag = (CARD_FADE_B - CARD_FADE_A) * (i / Math.max(1, N - 1))\n        const cIn = clamp01((p - (CARD_IN + stag)) / Math.max(1e-6, CARD_FADE_A - CARD_IN))\n        const op = edge * ease02(cIn)\n        c.material.opacity = op\n        c.material.depthWrite = op > 0.98\n      }\n\n      if (p < CARD_LIVE) {\n        setHover(null)\n      } else if (pointerIn && raycaster) {\n        hitList.length = 0\n        for (const c of cards) if (c.mesh.visible) hitList.push(c.mesh)\n        raycaster.setFromCamera(pointer, camera)\n        const hits = raycaster.intersectObjects(hitList, false)\n        setHover(hits.length ? hits[0].object.userData.record : null)\n      }\n\n      if (blob && blobUni) {\n        const bt = t * 6.0\n\n        const MORPH_A = clamp01(TW.morphA)\n        const MORPH_B = Math.max(MORPH_A + 0.02, TW.morphB)\n        const SHRINK_B = Math.max(MORPH_B + 0.02, TW.shrinkB)\n        const COMP_B = Math.min(0.98, Math.max(SHRINK_B + 0.02, TW.compB))\n        const thick = ease01(clamp01(p / MORPH_A))\n        const bp = ease02(clamp01((p - MORPH_A) / (MORPH_B - MORPH_A)))\n\n        const DIP_S = TW.dipS, END_S = Math.max(TW.dipS, TW.endS)\n        const PL_PEAK = Math.min(0.98, Math.max(COMP_B + 0.02, 0.92))\n        const PL_BOT = Math.max(MORPH_B + 0.02, SHRINK_B - 0.05)\n        const PL_UNDER = DIP_S * 0.80\n        const ts = p <= MORPH_B\n          ? 1\n          : p <= SHRINK_B\n            ? bumpSlow(p, MORPH_B, PL_BOT, SHRINK_B, 1, PL_UNDER, DIP_S)\n            : p <= COMP_B\n              ? DIP_S\n              : bump(p, COMP_B, PL_PEAK, 1, DIP_S, END_S * 1.15, END_S)\n\n        const agit = Math.max(0, Math.min(1.12, kfAt(AGIT_KF, p)))\n\n        const dbt = Math.max(0, bt - lastBt)\n        lastBt = bt\n        orbPhase += dbt * (0.18 + 0.28 * agit)\n\n        const visH = 2 * Math.tan((camera.fov * Math.PI / 180) / 2) * camera.position.z\n        const span = visH * 1.75\n\n        const A = blobUni.uA.value\n        for (let i = 0; i < 8; i++) {\n          const u = i / 7\n          const wx = Math.sin(u * Math.PI * 2 * 0.85 + bt * 0.32) * 120\n          const wy = (u - 0.5) * span\n          const ang = (i / 8) * Math.PI * 2 * 2 + orbPhase\n          const rad = (50 + jitter(i, 5) * 74) * ts\n          const tx = Math.cos(ang) * rad\n          const ty = Math.sin(ang * 1.35 + i) * rad * 0.85\n          const ag = TW.agit * agit * ts\n          const jx = Math.sin(bt * 0.47 + i * 2.1) + 0.55 * Math.sin(bt * 0.89 + i * 3.7)\n          const jy = Math.cos(bt * 0.41 + i * 1.7) + 0.55 * Math.cos(bt * 0.79 + i * 4.3)\n          A[i].set(wx + (tx - wx) * bp + jx * ag, wy + (ty - wy) * bp + jy * ag)\n        }\n\n        const P = blobUni.uP.value\n        let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9\n        for (let k = 0; k < 32; k++) {\n          const uu = (k / 31) * 7\n          const i = Math.min(Math.floor(uu), 6)\n          const f = uu - i\n          const p0 = A[Math.max(i - 1, 0)], p1 = A[i]\n          const p2 = A[i + 1], p3 = A[Math.min(i + 2, 7)]\n          const f2 = f * f, f3 = f2 * f\n          const x = 0.5 * (2 * p1.x + (-p0.x + p2.x) * f +\n            (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * f2 +\n            (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * f3)\n          const y = 0.5 * (2 * p1.y + (-p0.y + p2.y) * f +\n            (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * f2 +\n            (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * f3)\n          P[k].set(x, y)\n          if (x < minX) minX = x; if (x > maxX) maxX = x\n          if (y < minY) minY = y; if (y > maxY) maxY = y\n        }\n\n        const rr = (TW.r0 + TW.rThick * thick + TW.rMorph * bp) * ts\n        const kk = (0.5 + 62 * bp) * ts\n        blobUni.uR.value = rr\n        blobUni.uK.value = kk\n        blobUni.uBall.value = 58 * bp * ts\n        blobUni.uBlend.value = Math.max(10, (38 + 34 * bp) * ts)\n        const gp = p * p * p\n        blobUni.uGlow.value = TW.glow0 + TW.glowG * gp\n        blobUni.uSat.value = 1.0 + 0.55 * bp\n        blobUni.uRim.value = 0.50 + 0.65 * gp\n        blobUni.uSpan.value = 1.0\n        blobUni.uPhase.value = -t * 0.23\n\n        const pad = rr + kk + blobUni.uBall.value + 260\n        blob.position.set((minX + maxX) / 2, (minY + maxY) / 2, 0)\n        blob.scale.set((maxX - minX) + pad * 2, (maxY - minY) + pad * 2, 1)\n      }\n\n      renderer.render(scene, camera)\n    }\n\n    let started = false\n    const ro = new ResizeObserver(() => { needResize = true })\n    ro.observe(cv)\n    const io = new IntersectionObserver((es) => {\n      es.forEach((e) => { onScreen = e.isIntersecting })\n    }, { threshold: 0 })\n    io.observe(cv)\n\n    d2.raf((now) => {\n      if (!started) {\n        THREE = d2.lib('three@0.170.0')\n        if (!THREE) return\n        init()\n        started = true\n      }\n      if (!onScreen) { last = 0; return }\n      if (needResize) { needResize = false; resize() }\n      if (sizedOk) frame(now)\n    })\n\n    d2.onUnmount(() => {\n      ro.disconnect(); io.disconnect()\n      if (scene) disposeCards()\n      pruneTextures(new Set())\n      if (geometry) geometry.dispose()\n      if (renderer) renderer.dispose()\n    })\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"gap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gap"}}}},"coil":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coil"}}}},"ySpan":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ySpan"}}}},"flatten":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flatten"}}}},"bend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bend"}}}},"bendEnds":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bendEnds"}}}},"segments":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"segments"}}}},"cardW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardW"}}}},"cardH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardH"}}}},"turns":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"turns"}}}},"phase0":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"phase0"}}}},"tilt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tilt"}}}},"persp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"persp"}}}},"zOffset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zOffset"}}}},"fade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fade"}}}},"peak":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"peak"}}}},"backDim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backDim"}}}},"edgeFade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"edgeFade"}}}},"offsetX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetX"}}}},"offsetY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetY"}}}},"enter":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"enter"}}}},"scrollRate":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"scrollRate"}}}},"ease":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ease"}}}},"hud":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"hud"}}}},"wobble":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobble"}}}},"wobbleMid":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobbleMid"}}}},"storeRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"storeRef"}}}}}}}}}},{"_id":"6a638573209986653b2226f8","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"hotpocs-modal-body","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// HotPocs Modal Body — the POC wiki entry.\n//\n// Left: a stepper through the summarized journey — step 0 is the thumbnail,\n// then each journey screenshot with its caption, navigated by ← / →. Right: the\n// documentation (objective, overview, backend keypoints, observations),\n// rendered from markdown. NO in-app execution — the real app is reached via the\n// \"Open app\" redirect button only.\n\nconst esc = (s) => String(s == null ? '' : s)\n  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\n// Compact, safe-ish markdown → html (headings, lists, bold/italic/code, fenced\n// code, http(s) links). Content is authored by internal devs / the analyzer.\nconst mdToHtml = (src) => {\n  const e = (t) => String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n  const inline = (t) => e(t)\n    .replace(/`([^`]+)`/g, '<code>$1</code>')\n    .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n    .replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n    .replace(/\\[([^\\]]+)\\]\\((https?:[^)\\s]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>')\n  const lines = String(src || '').split(/\\r?\\n/)\n  let html = '', inUl = false, inOl = false, inCode = false, code = ''\n  const closeLists = () => { if (inUl) { html += '</ul>'; inUl = false } if (inOl) { html += '</ol>'; inOl = false } }\n  for (const raw of lines) {\n    if (/^\\s*```/.test(raw)) {\n      if (inCode) { html += '<pre><code>' + e(code) + '</code></pre>'; code = ''; inCode = false }\n      else { closeLists(); inCode = true }\n      continue\n    }\n    if (inCode) { code += raw + '\\n'; continue }\n    const line = raw.trim()\n    if (!line) { closeLists(); continue }\n    let m\n    if ((m = line.match(/^(#{1,3})\\s+(.*)$/))) { closeLists(); const l = m[1].length + 2; html += '<h' + l + '>' + inline(m[2]) + '</h' + l + '>'; continue }\n    if ((m = line.match(/^[-*]\\s+(.*)$/))) { if (!inUl) { closeLists(); html += '<ul>'; inUl = true } html += '<li>' + inline(m[1]) + '</li>'; continue }\n    if ((m = line.match(/^\\d+\\.\\s+(.*)$/))) { if (!inOl) { closeLists(); html += '<ol>'; inOl = true } html += '<li>' + inline(m[1]) + '</li>'; continue }\n    closeLists(); html += '<p>' + inline(line) + '</p>'\n  }\n  if (inCode) html += '<pre><code>' + e(code) + '</code></pre>'\n  closeLists()\n  return html\n}\n\nconst buildSteps = (d2) => {\n  const thumb = String(d2.param('thumb', '') || '')\n  const objective = String(d2.param('objective', '') || '')\n  const journey = Array.isArray(d2.param('journey', [])) ? d2.param('journey', []) : []\n  const steps = [{ image: thumb, title: 'Thumbnail', note: objective }]\n  journey.forEach((j, i) => steps.push({\n    image: String((j && j.image) || ''),\n    title: String((j && j.title) || ('Step ' + (i + 1))),\n    note: String((j && j.note) || '')\n  }))\n  return steps\n}\n\nexport default {\n  css: `\n    .mb-root { position:absolute; inset:0; display:flex; flex-direction:column; padding:24px 28px;\n               gap:16px; box-sizing:border-box; color:#e6eef5;\n               font-family:Inter,-apple-system,system-ui,sans-serif; }\n    .mb-top { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; flex:0 0 auto; }\n    .mb-title { font-size:22px; font-weight:600; letter-spacing:.2px; }\n    .mb-sub { font-size:12px; color:#8ea0b2; margin-top:5px; display:flex; gap:8px; flex-wrap:wrap; }\n    .mb-sub b { color:#a9bccd; font-weight:500; }\n    .mb-open { display:inline-flex; align-items:center; gap:8px; text-decoration:none; white-space:nowrap;\n               font-size:14px; font-weight:600; color:#08111f; background:#7cc0ff;\n               padding:10px 18px; border-radius:999px; transition:filter .15s, transform .15s; }\n    .mb-open:hover { filter:brightness(1.08); transform:translateY(-1px); }\n    .mb-open[data-disabled=\"1\"] { background:#1b2432; color:#54677a; pointer-events:none; }\n    .mb-open svg { width:14px; height:14px; }\n\n    .mb-body { flex:1 1 auto; min-height:0; display:grid; grid-template-columns:2fr 1fr; gap:26px; }\n\n    .mb-stage { min-height:0; display:flex; flex-direction:column; }\n    .mb-viewer { position:relative; flex:1 1 auto; min-height:0; border-radius:12px; overflow:hidden;\n                 background:#0a0f16; border:1px solid #1c2531; }\n    .mb-shot { position:absolute; inset:0; background:center/contain no-repeat; }\n    .mb-empty { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;\n                color:#4e6070; font-size:13px; }\n    .mb-arrow { position:absolute; top:50%; transform:translateY(-50%); width:40px; height:40px;\n                border-radius:999px; display:flex; align-items:center; justify-content:center; cursor:pointer;\n                background:rgba(10,16,24,.62); color:#e6eef5; border:1px solid rgba(255,255,255,.12);\n                backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); font-size:18px; user-select:none;\n                transition:background .15s, opacity .15s; }\n    .mb-arrow:hover { background:rgba(24,34,48,.86); }\n    .mb-arrow.prev { left:12px; } .mb-arrow.next { right:12px; }\n    .mb-arrow[data-disabled=\"1\"] { opacity:.25; pointer-events:none; }\n\n    .mb-cap { flex:0 0 auto; margin-top:12px; }\n    .mb-cap-meta { font-size:11px; letter-spacing:.09em; text-transform:uppercase; color:#5f7690;\n                   display:flex; gap:10px; align-items:center; }\n    .mb-cap-meta b { color:#c9d6e2; font-weight:600; letter-spacing:0; text-transform:none; font-size:14px; }\n    .mb-cap-note { font-size:13px; color:#c3d0dc; margin-top:5px; line-height:1.5; }\n    .mb-dots { display:flex; gap:6px; margin-top:11px; }\n    .mb-dot { width:7px; height:7px; border-radius:999px; background:#2a3542; cursor:pointer; }\n    .mb-dot.on { background:#7cc0ff; }\n\n    .mb-doc { min-height:0; overflow-y:auto; padding-right:8px; }\n    .mb-doc::-webkit-scrollbar { width:8px; } .mb-doc::-webkit-scrollbar-thumb { background:#1c2734; border-radius:8px; }\n    .mb-sec-h { font-size:10px; letter-spacing:.11em; text-transform:uppercase; color:#55708a; margin:20px 0 8px; }\n    .mb-sec-h:first-child { margin-top:0; }\n    .mb-obj { font-size:15px; color:#eef3f8; line-height:1.5; }\n    .mb-md { font-size:13px; color:#c3d0dc; line-height:1.6; }\n    .mb-md h3 { font-size:15px; color:#eaf1f7; margin:14px 0 6px; } .mb-md h4 { font-size:14px; color:#dbe6f0; margin:12px 0 5px; }\n    .mb-md h5 { font-size:12px; color:#a9bccd; margin:10px 0 4px; text-transform:uppercase; letter-spacing:.05em; }\n    .mb-md p { margin:0 0 9px; } .mb-md ul, .mb-md ol { margin:0 0 9px; padding-left:20px; } .mb-md li { margin:3px 0; }\n    .mb-md code { background:#0c141d; border:1px solid #1c2734; border-radius:4px; padding:1px 5px;\n                  font:12px ui-monospace,Menlo,monospace; color:#9fd0ff; }\n    .mb-md pre { background:#080d13; border:1px solid #16202b; border-radius:8px; padding:10px 12px; overflow-x:auto; margin:0 0 10px; }\n    .mb-md pre code { background:none; border:0; padding:0; color:#a9c6e6; }\n    .mb-md a { color:#7cc0ff; }\n    .mb-doc-empty { color:#4e6070; font-size:13px; }\n  `,\n\n  render(d2) {\n    const steps = buildSteps(d2)\n    const n = steps.length\n    let step = d2.state.step | 0\n    if (step < 0) step = 0\n    if (step > n - 1) step = n - 1\n    const cur = steps[step] || { image: '', title: '', note: '' }\n\n    const arrow = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M7 17 17 7\"/><path d=\"M8 7h9v9\"/></svg>'\n    const url = String(d2.param('redirectUrl', '') || '')\n    const openBtn = url\n      ? '<a class=\"mb-open\" href=\"' + esc(url) + '\" target=\"_blank\" rel=\"noopener noreferrer\">Open app' + arrow + '</a>'\n      : '<span class=\"mb-open\" data-disabled=\"1\">No link</span>'\n\n    const sys = String(d2.param('system', '') || '')\n    const ws = String(d2.param('workspace', '') || '')\n    const cat = String(d2.param('cardCategory', '') || '')\n    const subBits = []\n    if (cat) subBits.push('<b>' + esc(cat) + '</b>')\n    if (sys) subBits.push('System: ' + esc(sys))\n    if (ws) subBits.push('Workspace: ' + esc(ws))\n\n    const shot = cur.image\n      ? '<div class=\"mb-shot\" style=\"background-image:url(' + esc(cur.image) + ')\"></div>'\n      : '<div class=\"mb-empty\">no screenshot for this step</div>'\n\n    const dots = steps.map((s, i) =>\n      '<div class=\"mb-dot' + (i === step ? ' on' : '') + '\" data-dot=\"' + i + '\"></div>').join('')\n\n    // Doc column\n    const objective = String(d2.param('objective', '') || '')\n    const overview = mdToHtml(d2.param('overview', ''))\n    const backend = mdToHtml(d2.param('backendDoc', ''))\n    const observations = mdToHtml(d2.param('observations', ''))\n    let doc = ''\n    if (objective) doc += '<div class=\"mb-sec-h\">Objective</div><div class=\"mb-obj\">' + esc(objective) + '</div>'\n    if (overview) doc += '<div class=\"mb-sec-h\">Overview</div><div class=\"mb-md\">' + overview + '</div>'\n    if (backend) doc += '<div class=\"mb-sec-h\">Backend keypoints</div><div class=\"mb-md\">' + backend + '</div>'\n    if (observations) doc += '<div class=\"mb-sec-h\">Important observations</div><div class=\"mb-md\">' + observations + '</div>'\n    if (!doc) doc = '<div class=\"mb-doc-empty\">No documentation yet — run the analyzer or fill it in the admin.</div>'\n\n    return (\n      '<div class=\"mb-root\">' +\n      '<div class=\"mb-top\">' +\n      '<div><div class=\"mb-title\">' + esc(d2.param('cardName', '')) + '</div>' +\n      (subBits.length ? '<div class=\"mb-sub\">' + subBits.join('<span>·</span>') + '</div>' : '') +\n      '</div>' + openBtn +\n      '</div>' +\n\n      '<div class=\"mb-body\">' +\n      '<div class=\"mb-stage\">' +\n      '<div class=\"mb-viewer\">' + shot +\n      '<div class=\"mb-arrow prev\" data-prev' + (step <= 0 ? ' data-disabled=\"1\"' : '') + '>‹</div>' +\n      '<div class=\"mb-arrow next\" data-next' + (step >= n - 1 ? ' data-disabled=\"1\"' : '') + '>›</div>' +\n      '</div>' +\n      '<div class=\"mb-cap\">' +\n      '<div class=\"mb-cap-meta\"><span>' + (step + 1) + ' / ' + n + '</span><b>' + esc(cur.title) + '</b></div>' +\n      (cur.note ? '<div class=\"mb-cap-note\">' + esc(cur.note) + '</div>' : '') +\n      '</div>' +\n      '<div class=\"mb-dots\">' + dots + '</div>' +\n      '</div>' +\n\n      '<div class=\"mb-doc\">' + doc + '</div>' +\n      '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    const go = (dir) => {\n      const n = buildSteps(d2).length\n      let s = (d2.state.step | 0) + dir\n      if (s < 0) s = 0\n      if (s > n - 1) s = n - 1\n      d2.setState({ step: s })\n    }\n\n    d2.on('[data-prev]', 'click', () => go(-1))\n    d2.on('[data-next]', 'click', () => go(1))\n    d2.on('[data-dot]', 'click', (e) => {\n      const i = Number(e.target.getAttribute('data-dot'))\n      if (!isNaN(i)) d2.setState({ step: i })\n    })\n\n    // Esc closes; ← / → step through the journey. Lives here (not global)\n    // because the modal subtree is removed from the DOM while closed.\n    d2.on(window, 'keydown', (e) => {\n      if (e.key === 'Escape') {\n        const s = d2.store('hotpocs-ui')\n        if (s) s.set('modalOpen', false)\n      } else if (e.key === 'ArrowLeft') { go(-1) }\n      else if (e.key === 'ArrowRight') { go(1) }\n    })\n  }\n}"}},"cardName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardName"}}}},"cardCategory":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardCategory"}}}},"thumb":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"thumb"}}}},"redirectUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"redirectUrl"}}}},"objective":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"objective"}}}},"overview":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"overview"}}}},"journey":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"journey"}}}},"backendDoc":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backendDoc"}}}},"observations":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"observations"}}}},"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system"}}}},"workspace":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"workspace"}}}}}}}}}},{"_id":"6a6385d6665f2da2cacb6386","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"hotpocs-admin","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// HotPocs Admin — the control panel for the POC card pool.\n//\n// Text inputs/textareas are deliberately UNCONTROLLED: read from the DOM on any\n// discrete change (harvest) rather than mirrored into state per keystroke, so\n// the morph never fights the cursor. Discrete values (tags, active, locks,\n// journey structure) live in state.\n//\n// LOCKS: each field carries a padlock. A locked field's key sits in the card's\n// `locks[]` array; the analyzer agent reads it and never overwrites that field.\n// The lock only gates the BOT — humans always edit freely here.\n\nconst esc = (s) => String(s == null ? '' : s)\n  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n  .replace(/\"/g, '&quot;')\n\nconst asTags = (v) => (Array.isArray(v) ? v : (typeof v === 'string' && v ? v.split(',') : []))\n  .map((t) => String(t).trim()).filter(Boolean)\n\nconst asArr = (v) => (Array.isArray(v) ? v : [])\nconst asSteps = (v) => asArr(v).map((s) => ({\n  image: String((s && s.image) || ''),\n  title: String((s && s.title) || ''),\n  note: String((s && s.note) || '')\n}))\n\nexport default {\n  css: `\n    :host, .wa { font-family: Inter, -apple-system, sans-serif; }\n    .wa { position: absolute; inset: 0; display: grid; grid-template-columns: 430px 1fr;\n          background: #0a0d11; color: #dbe4ec; overflow: hidden; }\n\n    .wa-list { border-right: 1px solid #1b232c; display: flex; flex-direction: column; min-height: 0; }\n    .wa-bar { display: flex; align-items: center; gap: 10px; padding: 16px; border-bottom: 1px solid #1b232c; }\n    .wa-bar h2 { font-size: 15px; font-weight: 600; margin: 0; flex: 1; letter-spacing: .01em; }\n    .wa-rows { overflow-y: auto; flex: 1; min-height: 0; }\n\n    .row { display: grid; grid-template-columns: 18px 56px 1fr auto; gap: 12px; align-items: center;\n           padding: 10px 16px; cursor: pointer; border-bottom: 1px solid #131a21; }\n    .row.dragging { opacity: .3; }\n    .handle { color: #43566a; font-size: 14px; line-height: 1; text-align: center;\n              cursor: grab; user-select: none; }\n    .handle:active { cursor: grabbing; }\n    .row:hover { background: #101720; }\n    .row.sel { background: #14202b; box-shadow: inset 2px 0 0 #4c9aff; }\n    .row-thumb { width: 56px; height: 35px; border-radius: 4px; background: #161d25 center/cover no-repeat; }\n    .row-name { font-size: 13px; font-weight: 500; color: #eaf1f7; }\n    .row-meta { font-size: 11px; color: #6b8096; margin-top: 2px; }\n    .row-flag { font-size: 10px; letter-spacing: .06em; text-transform: uppercase;\n                padding: 3px 7px; border-radius: 999px; background: #1b2530; color: #7d93a8; }\n    .row-flag.ab { background: #14304a; color: #7ab6ff; }\n    .row.off { opacity: .45; }\n\n    .wa-form { padding: 22px 26px; overflow-y: auto; min-height: 0; }\n    .wa-empty { display: flex; align-items: center; justify-content: center; height: 100%;\n                color: #4e6070; }\n\n    .sec { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: #55708a;\n           margin: 24px 0 12px; padding-bottom: 6px; border-bottom: 1px solid #161e27; }\n    .sec:first-child { margin-top: 0; }\n\n    .fld { margin-bottom: 16px; }\n    .fldhead { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n    .fldhead > label { font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: #6b8096; }\n    .lockbtn { background: none; border: 0; cursor: pointer; font-size: 13px; line-height: 1;\n               padding: 2px 4px; border-radius: 5px; opacity: .5; filter: grayscale(1); }\n    .lockbtn:hover { opacity: .9; background: #141b23; }\n    .lockbtn.on { opacity: 1; filter: none; background: #2a2216; }\n    input[type=text], input[type=number], select, textarea {\n      width: 100%; box-sizing: border-box; background: #10161d; border: 1px solid #212b36;\n      border-radius: 7px; color: #e6eef5; font-size: 13px; padding: 9px 11px; font-family: inherit; }\n    textarea { resize: vertical; line-height: 1.5; }\n    textarea.md { min-height: 96px; font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; }\n    input:focus, select:focus, textarea:focus { outline: none; border-color: #3d78c9; }\n    input:disabled { opacity: .4; }\n    .fld.locked input, .fld.locked textarea, .fld.locked select { border-color: #6b5a2a; }\n\n    .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; min-height: 4px; }\n    .chip { display: inline-flex; align-items: center; gap: 6px; background: #1a2530; color: #b9cbdb;\n            border-radius: 999px; padding: 4px 6px 4px 11px; font-size: 12px; }\n    .chip b { cursor: pointer; color: #7a8fa3; font-weight: 400; padding: 0 3px; }\n    .chip b:hover { color: #ff7a6b; }\n\n    .two { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\n    .thumbrow { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }\n    .preview { width: 100%; height: 132px; border-radius: 8px; margin-top: 8px;\n               background: #10161d center/contain no-repeat; border: 1px solid #1c242e; }\n\n    .jstep { border: 1px solid #1c2530; border-radius: 9px; padding: 10px; margin-bottom: 10px; background: #0d131a; }\n    .jstep-top { display: grid; grid-template-columns: 22px 96px 1fr; gap: 10px; align-items: center; margin-bottom: 8px; }\n    .jstep-n { width: 22px; height: 22px; border-radius: 999px; background: #14304a; color: #7ab6ff;\n               font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center; }\n    .jstep-img { width: 96px; height: 60px; border-radius: 5px; border: 1px solid #1c242e;\n                 background: #10161d center/cover no-repeat; cursor: pointer; }\n    .jstep-move { display: flex; gap: 5px; justify-content: flex-end; }\n    .jstep-move button { padding: 5px 9px; }\n    .jstep input[type=text] { margin-bottom: 7px; }\n    .jstep textarea { min-height: 52px; }\n\n    button { font-family: inherit; font-size: 13px; border-radius: 7px; cursor: pointer;\n             border: 1px solid #263140; background: #151d26; color: #cfdde9; padding: 9px 14px; }\n    button:hover { background: #1b2531; }\n    button.primary { background: #2f6fd0; border-color: #2f6fd0; color: #fff; }\n    button.primary:hover { background: #3b7ee0; }\n    button.danger { color: #ff8f80; border-color: #3a2528; }\n    button.danger:hover { background: #2a1b1e; }\n    button.ghost { background: none; }\n    button:disabled { opacity: .5; cursor: default; }\n\n    .analyze { margin-top: 14px; padding: 12px; border: 1px solid #223247; border-radius: 9px; background: #0c141d; }\n    .analyze-h { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: #6b8faf; margin-bottom: 8px; }\n    .promptout { width: 100%; height: 120px; margin-top: 8px; resize: vertical; font: 11px/1.45 ui-monospace, Menlo, monospace;\n                 background: #060a0f; color: #a9c6e6; border: 1px solid #1c2a3a; border-radius: 7px; padding: 8px; }\n\n    .actions { display: flex; gap: 10px; margin-top: 22px; padding-top: 18px; border-top: 1px solid #1b232c; }\n    .spacer { flex: 1; }\n    .msg { font-size: 12px; color: #6f9f7c; align-self: center; }\n    .msg.err { color: #d98a80; }\n    .hint { font-size: 11px; color: #5b6f83; margin-top: 6px; line-height: 1.5; }\n    input[type=file] { display: none; }\n  `,\n\n  render(d2) {\n    const h = d2.data()\n    const records = ((h && h.records()) || []).slice()\n      .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n    const sel = d2.state.selectedId\n    const draft = d2.state.draft || null\n\n    const rows = records.map((r) => {\n      const tags = asTags(r.tags)\n      const steps = asSteps(r.journey).length\n      const flag = steps ? (steps + ' steps') : (String(r.redirectUrl || '').trim() ? 'link' : '—')\n      const flagCls = (steps || String(r.redirectUrl || '').trim()) ? ' ab' : ''\n      return (\n        '<div class=\"row' + (r._id === sel ? ' sel' : '') + (r.active === false ? ' off' : '') +\n        '\" data-row draggable=\"true\" data-key=\"' + esc(r._id) + '\">' +\n        '<div class=\"handle\">⠿</div>' +\n        '<div class=\"row-thumb\" style=\"background-image:url(' + esc(r.thumbnail || '') + ')\"></div>' +\n        '<div><div class=\"row-name\">' + esc(r.name || 'Untitled') + '</div>' +\n        '<div class=\"row-meta\">' + esc(r.category || '—') +\n        (tags.length ? '  ·  ' + esc(tags.join(', ')) : '') + '</div></div>' +\n        '<div class=\"row-flag' + flagCls + '\">' + esc(flag) + '</div>' +\n        '</div>'\n      )\n    }).join('')\n\n    let form\n    if (!draft) {\n      form = '<div class=\"wa-empty\">Select a card, or create a new one.</div>'\n    } else {\n      const locks = asArr(draft.locks)\n      const lock = (key) => {\n        const on = locks.indexOf(key) >= 0\n        return '<button class=\"lockbtn' + (on ? ' on' : '') + '\" data-lock=\"' + key + '\" title=\"' +\n          (on ? 'Locked — the analyzer will NOT overwrite this' : 'Unlocked — the analyzer may fill/overwrite this') +\n          '\">' + (on ? '🔒' : '🔓') + '</button>'\n      }\n      const lk = (key) => (locks.indexOf(key) >= 0 ? ' locked' : '')\n      const head = (key, label) => '<div class=\"fldhead\"><label>' + label + '</label>' + lock(key) + '</div>'\n\n      const tags = asTags(draft.tags)\n      const steps = asSteps(draft.journey)\n\n      const stepsHtml = steps.map((st, i) =>\n        '<div class=\"jstep\" data-step data-idx=\"' + i + '\">' +\n        '<div class=\"jstep-top\">' +\n        '<div class=\"jstep-n\">' + (i + 1) + '</div>' +\n        '<div class=\"jstep-img\" data-jpick=\"' + i + '\" title=\"Upload screenshot\"' +\n        ' style=\"background-image:url(' + esc(st.image || '') + ')\"></div>' +\n        '<div class=\"jstep-move\">' +\n        '<button class=\"ghost\" data-jup=\"' + i + '\" title=\"Move up\">↑</button>' +\n        '<button class=\"ghost\" data-jdown=\"' + i + '\" title=\"Move down\">↓</button>' +\n        '<button class=\"ghost danger\" data-jdel=\"' + i + '\" title=\"Remove\">✕</button>' +\n        '</div></div>' +\n        '<input type=\"text\" data-jt data-idx=\"' + i + '\" value=\"' + esc(st.title) + '\" placeholder=\"Step title\">' +\n        '<textarea data-jn data-idx=\"' + i + '\" placeholder=\"What happens in this step\">' + esc(st.note) + '</textarea>' +\n        '<input type=\"file\" data-jfile=\"' + i + '\" accept=\"image/*\">' +\n        '</div>'\n      ).join('')\n\n      form =\n        '<div class=\"sec\">Identity</div>' +\n\n        '<div class=\"fld' + lk('name') + '\">' + head('name', 'Name') +\n        '<input type=\"text\" data-f=\"name\" value=\"' + esc(draft.name) + '\"></div>' +\n\n        '<div class=\"two\">' +\n        '<div class=\"fld' + lk('category') + '\">' + head('category', 'Category') +\n        '<input type=\"text\" data-f=\"category\" value=\"' + esc(draft.category) + '\"></div>' +\n        '<div class=\"fld' + lk('active') + '\">' + head('active', 'Active') +\n        '<select data-active>' +\n        '<option value=\"yes\"' + (draft.active === false ? '' : ' selected') + '>Yes</option>' +\n        '<option value=\"no\"' + (draft.active === false ? ' selected' : '') + '>No</option>' +\n        '</select></div>' +\n        '</div>' +\n\n        '<div class=\"fld' + lk('tags') + '\">' + head('tags', 'Tags') +\n        '<div class=\"chips\">' + tags.map((t, i) =>\n          '<span class=\"chip\">' + esc(t) + '<b data-untag=\"' + i + '\">×</b></span>').join('') +\n        '</div>' +\n        '<input type=\"text\" data-tagin placeholder=\"Type a tag and press Enter\"></div>' +\n\n        '<div class=\"fld' + lk('redirectUrl') + '\">' + head('redirectUrl', 'Redirect URL (app link)') +\n        '<input type=\"text\" data-f=\"redirectUrl\" value=\"' + esc(draft.redirectUrl || '') + '\" placeholder=\"https://…\">' +\n        '<div class=\"hint\">External link the “Open app” button opens in a new tab. Also the URL the analyzer visits.</div></div>' +\n\n        '<div class=\"sec\">Source (data2 location)</div>' +\n        '<div class=\"two\">' +\n        '<div class=\"fld' + lk('system') + '\">' + head('system', 'System') +\n        '<input type=\"text\" data-f=\"system\" value=\"' + esc(draft.system || '') + '\" placeholder=\"e.g. bigaton-playground\"></div>' +\n        '<div class=\"fld' + lk('workspace') + '\">' + head('workspace', 'Workspace') +\n        '<input type=\"text\" data-f=\"workspace\" value=\"' + esc(draft.workspace || '') + '\" placeholder=\"e.g. WormholeLP\"></div>' +\n        '</div>' +\n\n        '<div class=\"sec\">Media</div>' +\n        '<div class=\"fld' + lk('thumbnail') + '\">' + head('thumbnail', 'Thumbnail') +\n        '<div class=\"thumbrow\">' +\n        '<input type=\"text\" data-f=\"thumbnail\" value=\"' + esc(draft.thumbnail) + '\">' +\n        '<button data-pick>Upload…</button>' +\n        '</div>' +\n        '<input type=\"file\" data-file accept=\"image/*\">' +\n        '<div class=\"preview\" style=\"background-image:url(' + esc(draft.thumbnail || '') + ')\"></div></div>' +\n\n        '<div class=\"sec\">Summary</div>' +\n        '<div class=\"fld' + lk('objective') + '\">' + head('objective', 'Objective (one line)') +\n        '<input type=\"text\" data-f=\"objective\" value=\"' + esc(draft.objective || '') + '\" placeholder=\"What this POC is for, in one sentence\"></div>' +\n        '<div class=\"fld' + lk('overview') + '\">' + head('overview', 'Overview') +\n        '<textarea class=\"md\" data-f=\"overview\" placeholder=\"Main features and how it is built (markdown)\">' + esc(draft.overview || '') + '</textarea></div>' +\n\n        '<div class=\"sec\">Journey (summarized flow)</div>' +\n        '<div class=\"fld' + lk('journey') + '\">' +\n        '<div class=\"fldhead\"><label>Steps — screenshot + caption</label>' + lock('journey') + '</div>' +\n        '<div data-journey-wrap>' + stepsHtml + '</div>' +\n        '<button data-jadd>+ Add step</button>' +\n        '<div class=\"hint\">Step 0 in the modal is the thumbnail; these follow it via the → arrow. Keep it a summarized journey (~4–7 keypoints).</div></div>' +\n\n        '<div class=\"sec\">Backend & notes</div>' +\n        '<div class=\"fld' + lk('backendDoc') + '\">' + head('backendDoc', 'Backend doc (keypoints)') +\n        '<textarea class=\"md\" data-f=\"backendDoc\" placeholder=\"Backend flow keypoints — the more unusual/unique the logic, the more it should stand out (markdown)\">' + esc(draft.backendDoc || '') + '</textarea></div>' +\n        '<div class=\"fld' + lk('observations') + '\">' + head('observations', 'Important observations') +\n        '<textarea class=\"md\" data-f=\"observations\" placeholder=\"Anything else worth flagging (markdown)\">' + esc(draft.observations || '') + '</textarea></div>' +\n\n        (draft._id\n          ? '<div class=\"analyze\"><div class=\"analyze-h\">Analyzer</div>' +\n            '<div class=\"hint\">Generate the read-only analysis prompt for this POC, hand it (with <b>hotpocs-analyzer.md</b>) to your Claude + data2 MCP. It fills the unlocked fields.</div>' +\n            '<div style=\"margin-top:10px;display:flex;gap:10px\">' +\n            '<button data-genprompt>Generate analysis prompt</button></div>' +\n            '<textarea class=\"promptout\" data-prompt-out readonly placeholder=\"The prompt appears here (also copied to clipboard).\"></textarea></div>'\n          : '') +\n\n        '<div class=\"actions\">' +\n        '<button class=\"primary\" data-save>' + (draft._id ? 'Save changes' : 'Create card') + '</button>' +\n        (draft._id ? '<button class=\"danger\" data-del>Delete</button>' : '') +\n        '<div class=\"spacer\"></div>' +\n        '<div class=\"msg' + (d2.state.err ? ' err' : '') + '\">' + esc(d2.state.msg || '') + '</div>' +\n        '</div>'\n    }\n\n    return (\n      '<div class=\"wa\">' +\n      '<div class=\"wa-list\">' +\n      '<div class=\"wa-bar\"><h2>Cards (' + records.length + ')</h2>' +\n      '<span class=\"msg' + (d2.state.err ? ' err' : '') + '\">' + esc(d2.state.msg || '') + '</span>' +\n      '<button data-new>+ New</button></div>' +\n      '<div class=\"wa-rows\">' + rows + '</div>' +\n      '</div>' +\n      '<div class=\"wa-form\">' + form + '</div>' +\n      '</div>'\n    )\n  },\n\n  mounted(d2, root) {\n    const blank = () => ({\n      name: '', category: '', tags: [], thumbnail: '', redirectUrl: '',\n      system: '', workspace: '', objective: '', overview: '',\n      journey: [], backendDoc: '', observations: '',\n      order: null, active: true, locks: []\n    })\n\n    // Fold every uncontrolled input/textarea (and the journey steps) into the\n    // draft. Called before any state change that re-renders the form, so\n    // half-typed edits are never lost.\n    const harvest = () => {\n      const d = Object.assign({}, d2.state.draft || {})\n      root.querySelectorAll('[data-f]').forEach((el) => {\n        const k = el.getAttribute('data-f')\n        d[k] = el.type === 'number'\n          ? (el.value === '' ? null : Number(el.value))\n          : el.value\n      })\n      if (root.querySelector('[data-journey-wrap]')) {\n        const prev = asSteps(d.journey)\n        const steps = []\n        root.querySelectorAll('[data-step]').forEach((el) => {\n          const idx = Number(el.getAttribute('data-idx'))\n          const cur = prev[idx] || {}\n          const tEl = el.querySelector('[data-jt]')\n          const nEl = el.querySelector('[data-jn]')\n          steps.push({\n            image: cur.image || '',\n            title: tEl ? tEl.value : (cur.title || ''),\n            note: nEl ? nEl.value : (cur.note || '')\n          })\n        })\n        d.journey = steps\n      }\n      return d\n    }\n\n    const setDraft = (d, msg, err) => {\n      d2.setState({ draft: d, msg: msg || '', err: !!err })\n    }\n\n    d2.on('[data-row]', 'click', (e) => {\n      const el = e.target.closest('[data-row]')\n      if (!el) return\n      const id = el.getAttribute('data-key')\n      const rec = ((d2.data() && d2.data().records()) || []).find((r) => r._id === id)\n      if (!rec) return\n      d2.setState({\n        selectedId: id,\n        draft: Object.assign({}, rec, { tags: asTags(rec.tags), journey: asSteps(rec.journey), locks: asArr(rec.locks) }),\n        msg: '', err: false\n      })\n    })\n\n    d2.on('[data-row]', 'dragstart', (e) => {\n      const el = e.target.closest('[data-row]')\n      if (!el) return\n      el.classList.add('dragging')\n      if (e.dataTransfer) {\n        e.dataTransfer.effectAllowed = 'move'\n        try { e.dataTransfer.setData('text/plain', el.getAttribute('data-key')) } catch (_) {}\n      }\n    })\n\n    d2.on('[data-row]', 'dragover', (e) => {\n      e.preventDefault()\n      const over = e.target.closest('[data-row]')\n      const moving = root.querySelector('.row.dragging')\n      if (!over || !moving || over === moving) return\n      const r = over.getBoundingClientRect()\n      const after = (e.clientY - r.top) > r.height / 2\n      over.parentNode.insertBefore(moving, after ? over.nextSibling : over)\n    })\n\n    d2.on('[data-row]', 'dragend', async () => {\n      const moving = root.querySelector('.row.dragging')\n      if (moving) moving.classList.remove('dragging')\n      const ids = Array.from(root.querySelectorAll('[data-row]')).map((el) => el.getAttribute('data-key'))\n      const h = d2.data()\n      const byId = {}\n      for (const r of (h.records() || [])) byId[r._id] = r\n      const writes = []\n      ids.forEach((id, i) => {\n        const rec = byId[id]\n        if (rec && rec.order !== i + 1) writes.push(h.update({ _id: id, order: i + 1 }))\n      })\n      if (!writes.length) return\n      d2.setState({ msg: 'Reordering…', err: false })\n      try {\n        await Promise.all(writes)\n        h.refresh()\n        d2.setState({ msg: 'Order saved.', err: false })\n      } catch (err) {\n        d2.setState({ msg: 'Reorder failed: ' + (err && err.message ? err.message : err), err: true })\n      }\n    })\n\n    d2.on('[data-new]', 'click', () => {\n      d2.setState({ selectedId: null, draft: blank(), msg: '', err: false })\n    })\n\n    d2.on('[data-active]', 'change', (e) => {\n      const d = harvest()\n      d.active = e.target.value === 'yes'\n      setDraft(d)\n    })\n\n    d2.on('[data-lock]', 'click', (e) => {\n      const btn = e.target.closest('[data-lock]')\n      if (!btn) return\n      const key = btn.getAttribute('data-lock')\n      const d = harvest()\n      const locks = asArr(d.locks).slice()\n      const i = locks.indexOf(key)\n      if (i >= 0) locks.splice(i, 1); else locks.push(key)\n      d.locks = locks\n      setDraft(d)\n    })\n\n    d2.on('[data-tagin]', 'keydown', (e) => {\n      if (e.key !== 'Enter') return\n      e.preventDefault()\n      const v = e.target.value.trim()\n      if (!v) return\n      const d = harvest()\n      const tags = asTags(d.tags)\n      if (!tags.some((t) => t.toLowerCase() === v.toLowerCase())) tags.push(v)\n      d.tags = tags\n      e.target.value = ''\n      setDraft(d)\n    })\n\n    d2.on('[data-untag]', 'click', (e) => {\n      const i = Number(e.target.getAttribute('data-untag'))\n      const d = harvest()\n      const tags = asTags(d.tags)\n      tags.splice(i, 1)\n      d.tags = tags\n      setDraft(d)\n    })\n\n    d2.on('[data-pick]', 'click', () => {\n      const f = root.querySelector('[data-file]')\n      if (f) f.click()\n    })\n\n    d2.on('[data-file]', 'change', async (e) => {\n      const file = e.target.files && e.target.files[0]\n      if (!file) return\n      const d = harvest()\n      setDraft(d, 'Uploading ' + file.name + '…')\n      try {\n        const drive = d2.drive(d2.param('driveRef', 'hotpocs-drive'))\n        const out = await drive.upload([file])\n        const url = out && out[0] && out[0].fileUrl\n        if (!url) throw new Error('upload returned no fileUrl')\n        d.thumbnail = url\n        setDraft(d, 'Uploaded. Remember to save.')\n      } catch (err) {\n        setDraft(d, 'Upload failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    // ---- journey steps ----\n    d2.on('[data-jadd]', 'click', () => {\n      const d = harvest()\n      d.journey = asSteps(d.journey).concat([{ image: '', title: '', note: '' }])\n      setDraft(d)\n    })\n\n    d2.on('[data-jdel]', 'click', (e) => {\n      const idx = Number(e.target.closest('[data-jdel]').getAttribute('data-jdel'))\n      const d = harvest()\n      const j = asSteps(d.journey)\n      j.splice(idx, 1)\n      d.journey = j\n      setDraft(d)\n    })\n\n    const swap = (idx, dir) => {\n      const d = harvest()\n      const j = asSteps(d.journey)\n      const t = idx + dir\n      if (t < 0 || t >= j.length) return\n      const tmp = j[idx]; j[idx] = j[t]; j[t] = tmp\n      d.journey = j\n      setDraft(d)\n    }\n    d2.on('[data-jup]', 'click', (e) => swap(Number(e.target.closest('[data-jup]').getAttribute('data-jup')), -1))\n    d2.on('[data-jdown]', 'click', (e) => swap(Number(e.target.closest('[data-jdown]').getAttribute('data-jdown')), 1))\n\n    d2.on('[data-jpick]', 'click', (e) => {\n      const idx = e.target.closest('[data-jpick]').getAttribute('data-jpick')\n      const f = root.querySelector('[data-jfile=\"' + idx + '\"]')\n      if (f) f.click()\n    })\n\n    d2.on('[data-jfile]', 'change', async (e) => {\n      const idx = Number(e.target.getAttribute('data-jfile'))\n      const file = e.target.files && e.target.files[0]\n      if (!file) return\n      const d = harvest()\n      setDraft(d, 'Uploading ' + file.name + '…')\n      try {\n        const drive = d2.drive(d2.param('driveRef', 'hotpocs-drive'))\n        const out = await drive.upload([file])\n        const url = out && out[0] && out[0].fileUrl\n        if (!url) throw new Error('upload returned no fileUrl')\n        const j = asSteps(d.journey)\n        if (j[idx]) j[idx].image = url\n        d.journey = j\n        setDraft(d, 'Uploaded. Remember to save.')\n      } catch (err) {\n        setDraft(d, 'Upload failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    // ---- analyzer prompt ----\n    d2.on('[data-genprompt]', 'click', () => {\n      const d = harvest()\n      const sys = d2.param('wikiSystem', 'bigaton-playground')\n      const locks = asArr(d.locks)\n      const lockLine = locks.length ? locks.join(', ') : '(none)'\n      const prompt =\n        'Analyze this data2 POC and populate its HotPocs card. Follow hotpocs-analyzer.md EXACTLY, ' +\n        'including the non-negotiable read-only mandate (never alter/create/delete/submit anything in the ' +\n        'analyzed app or its workspace — observe and screenshot only).\\n\\n' +\n        '- HotPocs wiki system: ' + sys + '\\n' +\n        '- Table: hotpocs-cards\\n' +\n        '- Target card _id: ' + (d._id || '(unsaved — save first)') + '\\n' +\n        '- POC name: ' + (d.name || '') + '\\n' +\n        '- App link to explore (read-only): ' + (d.redirectUrl || '(none)') + '\\n' +\n        '- Source system: ' + (d.system || '(unknown)') + '\\n' +\n        '- Source workspace: ' + (d.workspace || '(unknown)') + '\\n' +\n        '- LOCKED fields — DO NOT overwrite: ' + lockLine + '\\n'\n      const out = root.querySelector('[data-prompt-out]')\n      if (out) { out.value = prompt; out.focus(); out.select() }\n      try { navigator.clipboard.writeText(prompt) } catch (_) {}\n      d2.setState({ msg: 'Prompt generated & copied.', err: false })\n    })\n\n    d2.on('[data-save]', 'click', async () => {\n      const d = harvest()\n      if (!String(d.name || '').trim()) return setDraft(d, 'Name is required.', true)\n      const payload = {\n        name: d.name, category: d.category, tags: asTags(d.tags),\n        thumbnail: d.thumbnail, redirectUrl: String(d.redirectUrl || '').trim(),\n        system: String(d.system || '').trim(), workspace: String(d.workspace || '').trim(),\n        objective: d.objective || '', overview: d.overview || '',\n        journey: asSteps(d.journey), backendDoc: d.backendDoc || '',\n        observations: d.observations || '', locks: asArr(d.locks),\n        order: d.order == null ? null : Number(d.order),\n        active: d.active !== false\n      }\n      setDraft(d, 'Saving…')\n      try {\n        const h = d2.data()\n        if (d._id) {\n          await h.update(Object.assign({ _id: d._id }, payload))\n          setDraft(Object.assign({}, d, payload), 'Saved.')\n        } else {\n          await h.create(payload)\n          d2.setState({ selectedId: null, draft: null, msg: 'Card created.', err: false })\n        }\n        await h.refresh()\n      } catch (err) {\n        setDraft(d, 'Save failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    d2.on('[data-del]', 'click', async () => {\n      const d = d2.state.draft\n      if (!d || !d._id) return\n      const ok = await d2.dialog.confirm('Delete \"' + (d.name || 'this card') + '\"?')\n      if (!ok) return\n      try {\n        const h = d2.data()\n        await h.remove(d._id)\n        await h.refresh()\n        d2.setState({ selectedId: null, draft: null, msg: 'Card deleted.', err: false })\n      } catch (err) {\n        setDraft(d, 'Delete failed: ' + (err && err.message ? err.message : err), true)\n      }\n    })\n\n    this._syncSelects = () => {\n      const draft = d2.state.draft\n      if (!draft) return\n      const actSel = root.querySelector('[data-active]')\n      if (actSel) actSel.value = draft.active === false ? 'no' : 'yes'\n    }\n    this._syncSelects()\n  },\n\n  updated() {\n    if (this._syncSelects) this._syncSelects()\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"driveRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"driveRef"}}}},"wikiSystem":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wikiSystem"}}}}}}}}}},{"_id":"6a6386c0ca3aff2762b4fa8d","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"hotpocs-carousel","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// HotPocs Carousel — browse-first helix of POC cards (three.js).\n//\n// Trimmed from the WormholeLP hero: the scroll-morph, worm/plasma shader,\n// background grid and tweak panel are all gone. The scene starts fully arrived\n// (p is pinned to 1) so the cards orbit from the first frame — no intro. Cards\n// drag-to-spin with fling, slow on hover with a glass tooltip, idle-float, and\n// on click write the record onto the store to open the modal (redirect link).\n\nconst clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)\nconst ease01 = (u) => u * u * (3 - 2 * u)\nconst ease02 = (u) => u * u * u * (u * (u * 6 - 15) + 10)\n\n// Card entrance easing — kept so the settled state (p=1) renders identically to\n// the proven hero. Rounded apex, zero slope on both sides of the peak.\nconst bumpSlow = (x, t0, t1, t2, v0, v1, v2) => {\n  if (x <= t0) return v0\n  if (x >= t2) return v2\n  if (x <= t1) return v0 + (v1 - v0) * ease02((x - t0) / Math.max(1e-6, t1 - t0))\n  return v1 + (v2 - v1) * ease02((x - t1) / Math.max(1e-6, t2 - t1))\n}\n\nconst CARD_IN = 0.74\nconst CARD_LIVE = 0.80\nconst CARD_FADE_A = 0.76, CARD_FADE_B = 0.78\nconst CARD_R_PK = 0.86, CARD_S_PK = 0.89\nconst CARD_R0 = 0.12, CARD_S0 = 0.08, CARD_OVER = 1.15\n\n// Half-width of the lit/dark facing cross-fade, in cos(angle).\nconst FACE_BAND = 0.35\n\n// Idle-float amplitude + rotation rates.\nconst FLOAT_AMP = 10, FLOAT_PITCH = 0.045, FLOAT_TWIST = 0.02\n\n// Pixels of horizontal drag → carousel advance.\nconst DRAG_K = 0.0003\nconst FLING_GAIN = 1.7\nconst FLING_FRICTION = 3.2\nconst FLING_MAX = 4.0\nconst CLICK_HOLD = 0.14\n// On modal open the orbit holds full speed this long (s), then eases to a stop.\nconst MODAL_HOLD = 0.5\nconst MODAL_STOP = 1.0\n\nconst jitter = (i, salt) => {\n  const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453\n  return v - Math.floor(v)\n}\nconst signed = (i, salt) => jitter(i, salt) * 2 - 1\n\nexport default {\n  css: `\n    .st-wrap { position: absolute; inset: 0; overflow: hidden; background-color: #05080f; }\n    .st-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n\n    /* Cursor pill. Outer element positions (rewritten every pointer event),\n       inner one animates, so the two transforms never fight. */\n    .st-tip { position: absolute; left: 0; top: 0; pointer-events: none;\n              display: flex; align-items: center; height: 58px; z-index: 6;\n              will-change: transform; }\n    .st-tip-glass {\n      position: relative; overflow: hidden;\n      height: 14px; width: 14px; border-radius: 29px;\n      font-family: Inter, -apple-system, sans-serif;\n      background:\n        linear-gradient(155deg, rgba(216,236,255,0.11), rgba(216,236,255,0) 46%),\n        rgba(10, 16, 24, 0.44);\n      backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      -webkit-backdrop-filter: blur(7px) saturate(200%) brightness(118%) contrast(108%);\n      box-shadow: inset 0 1px 0 rgba(226,241,255,0.22),\n                  inset 1px 0 0 rgba(226,241,255,0.10),\n                  inset -1px 0 0 rgba(226,241,255,0.10),\n                  inset 0 -1px 0 rgba(226,241,255,0.26),\n                  inset 0 -7px 11px -7px rgba(216,236,255,0.22),\n                  0 6px 16px rgba(0,0,0,0.24);\n      opacity: 0;\n      transition: width 129ms cubic-bezier(.7, 0, .6, .25),\n                  height 129ms cubic-bezier(.7, 0, .6, .25),\n                  opacity 102ms ease-in 27ms;\n      will-change: width, height, opacity;\n    }\n    .st-tip.on .st-tip-glass {\n      opacity: 1; height: 58px;\n      transition: width 129ms cubic-bezier(.4, .75, .3, 1),\n                  height 129ms cubic-bezier(.4, .75, .3, 1),\n                  opacity 102ms ease-out;\n    }\n    .st-tip-inner {\n      position: absolute; left: 0; top: 50%; height: 58px;\n      transform: translateY(-50%);\n      display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;\n      align-content: center; column-gap: 10px;\n      padding: 0 17px 0 14px; white-space: nowrap;\n      opacity: 0; transition: opacity 57ms ease-in 41ms;\n    }\n    .st-tip.on .st-tip-inner { opacity: 1; transition: opacity 57ms ease-out 31ms; }\n    .st-tip-arrow { grid-row: 1 / span 2; align-self: center;\n                    display: flex; align-items: center; justify-content: center; color: #f4f4f5; }\n    .st-tip-name { font-size: 14px; font-weight: 600; color: #f4f4f5; line-height: 1.3;\n                   text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n    .st-tip-cat { font-size: 13px; color: #cfcfd6; line-height: 1.3;\n                  text-shadow: 0 1px 2px rgba(0,0,0,0.25); }\n  `,\n\n  render(d2) {\n    return '<div class=\"st-wrap\" data-d2-static>' +\n      '<canvas class=\"st-canvas\" data-cv></canvas>' +\n      '<div class=\"st-tip\" data-tip><div class=\"st-tip-glass\" data-tip-glass>' +\n      '<div class=\"st-tip-inner\" data-tip-inner>' +\n      '<span class=\"st-tip-arrow\">' +\n      '<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" aria-hidden=\"true\">' +\n      '<path d=\"M4.6 11.4 11.4 4.6M6.2 4.6h5.2v5.2\" fill=\"none\" stroke=\"currentColor\"' +\n      ' stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span>' +\n      '<span class=\"st-tip-name\" data-tip-name></span>' +\n      '<span class=\"st-tip-cat\" data-tip-cat></span>' +\n      '</div></div></div></div>'\n  },\n\n  mounted(d2, root) {\n    const cv = root.querySelector('[data-cv]')\n    if (!cv) return\n    const tip = root.querySelector('[data-tip]')\n    const tipGlass = root.querySelector('[data-tip-glass]')\n    const tipInner = root.querySelector('[data-tip-inner]')\n    const tipName = root.querySelector('[data-tip-name]')\n    const tipCat = root.querySelector('[data-tip-cat]')\n\n    const TIP_H = 58\n    const TIP_MIN = 14\n    let tipW = TIP_H\n\n    let THREE = null\n    let renderer = null, scene = null, camera = null, raycaster = null, pointer = null\n    let geometry = null\n    let cards = []\n    let builtSig = '', geoSig = ''\n    let last = 0, tick = 0\n    // Real-time clock for the idle float: advances every frame regardless, so\n    // the float ignores the hover slow-down and keeps going under the modal\n    // freeze — alive but paused, never frozen.\n    let tFloat = 0\n    // Separate clock for the card orbit so it can slow independently.\n    let tCard = 0, slowF = 1, slowMid = 1\n    // Drag-to-spin state + modal freeze.\n    let pressing = false, dragging = false, lastDragX = 0, dragMoved = 0, suppressClick = false\n    let pendingDrag = 0, dragVel = 0, flingVel = 0, pressT = 0, modalT = 0\n    let modalOpen = false\n    const uiStore = d2.store(String(d2.param('storeRef', 'hotpocs-ui')))\n    const readUI = (key) => {\n      if (!uiStore) return undefined\n      try {\n        if (typeof uiStore.get === 'function') return uiStore.get(key)\n        if (uiStore.data) return uiStore.data[key]\n      } catch (e) {}\n      return undefined\n    }\n    let sizedOk = false, onScreen = true, needResize = true\n    let lastDpr = 1\n    let hoverRec = null, pointerIn = false, mouseX = 0, mouseY = 0, canvasRect = null\n    const hitList = []\n    const texCache = new Map()\n    let qYaw, qPitch, qRoll, qTwist, axisX, axisY, axisZ\n\n    // The scene is pinned to the settled state — no scroll morph, no intro.\n    const p = 1\n\n    // ---- tooltip ----------------------------------------------------------\n\n    const refreshRect = () => { canvasRect = cv.getBoundingClientRect() }\n\n    const placeTip = () => {\n      if (!tip) return\n      const w = tipW, h = TIP_H\n      const cw = cv.clientWidth, ch = cv.clientHeight\n      let x = mouseX + 18\n      let y = mouseY - h / 2\n      if (x + w > cw - 10) x = mouseX - 18 - w\n      if (y < 10) y = 10\n      if (y + h > ch - 10) y = ch - 10 - h\n      tip.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'\n    }\n\n    const setHover = (rec) => {\n      if (rec === hoverRec) return\n      hoverRec = rec\n      cv.style.cursor = dragging ? 'grabbing' : (rec ? 'pointer' : 'default')\n      if (!rec) {\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n        return\n      }\n      if (tipName) tipName.textContent = rec.name || ''\n      if (tipCat) tipCat.textContent = rec.category || ''\n      tipW = tipInner ? tipInner.offsetWidth : TIP_H\n      placeTip()\n      if (tipGlass) tipGlass.style.width = tipW + 'px'\n      if (tip) tip.classList.add('on')\n    }\n\n    const openCard = (rec) => {\n      if (!rec) return\n      const s = d2.store(String(d2.param('storeRef', 'hotpocs-ui')))\n      if (!s) return\n      // Re-fetch the CURRENT record by id — the card holds the copy from when\n      // its geometry was last built, so an edit that didn't change the thumbnail\n      // (the rebuild signature) never reached it. Pull the live one.\n      const handle = d2.data()\n      const fresh = ((handle && handle.records()) || []).find((r) => r._id === rec._id) || rec\n      const tags = Array.isArray(fresh.tags) ? fresh.tags.join('  ·  ') : (fresh.tags || '')\n      s.set('name', fresh.name || '')\n      s.set('category', fresh.category || '')\n      s.set('tags', tags)\n      s.set('thumb', fresh.thumbnail || '')\n      s.set('redirectUrl', fresh.redirectUrl || '')\n      s.set('objective', fresh.objective || '')\n      s.set('overview', fresh.overview || '')\n      s.set('journey', Array.isArray(fresh.journey) ? fresh.journey : [])\n      s.set('backendDoc', fresh.backendDoc || '')\n      s.set('observations', fresh.observations || '')\n      s.set('system', fresh.system || '')\n      s.set('workspace', fresh.workspace || '')\n      s.set('modalOpen', true)\n    }\n\n    // ---- gl helpers -------------------------------------------------------\n\n    const BEND_CHUNK = [\n      'vec3 transformed = vec3( position );',\n      'float phi = transformed.x / uBendR;',\n      'transformed.x = uBendR * sin( phi );',\n      'transformed.z += uBendR * cos( phi ) - uBendR;'\n    ].join('\\n')\n\n    const makeMaterial = () => {\n      const m = new THREE.MeshBasicMaterial({\n        side: THREE.DoubleSide, transparent: true, depthWrite: false, opacity: 0\n      })\n      m.onBeforeCompile = (shader) => {\n        shader.uniforms.uBendR = { value: 1000 }\n        shader.vertexShader = 'uniform float uBendR;\\n' + shader.vertexShader\n        shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', BEND_CHUNK)\n        m.userData.shader = shader\n      }\n      return m\n    }\n\n    const texFor = (url) => {\n      let tex = texCache.get(url)\n      if (!tex) {\n        const loader = new THREE.TextureLoader()\n        loader.setCrossOrigin('anonymous')\n        tex = loader.load(url, (t) => {\n          // COVER: crop the image to the card's aspect, centred.\n          const img = t.image\n          if (!img) return\n          const cardAsp = (Number(d2.param('cardW', 448)) || 448) / (Number(d2.param('cardH', 283)) || 283)\n          const imgAsp = (img.naturalWidth || img.width || 1) / (img.naturalHeight || img.height || 1)\n          if (imgAsp > cardAsp) {\n            const r = cardAsp / imgAsp\n            t.repeat.set(r, 1); t.offset.set((1 - r) / 2, 0)\n          } else {\n            const r = imgAsp / cardAsp\n            t.repeat.set(1, r); t.offset.set(0, (1 - r) / 2)\n          }\n          t.needsUpdate = true\n        })\n        tex.colorSpace = THREE.SRGBColorSpace\n        tex.anisotropy = renderer.capabilities.getMaxAnisotropy()\n        texCache.set(url, tex)\n      }\n      return tex\n    }\n\n    const pruneTextures = (keep) => {\n      for (const [url, tex] of texCache) {\n        if (!keep.has(url)) { tex.dispose(); texCache.delete(url) }\n      }\n    }\n\n    const disposeCards = () => {\n      for (const c of cards) { scene.remove(c.mesh); if (c.material) c.material.dispose() }\n      cards = []\n    }\n\n    const buildCards = (records) => {\n      disposeCards()\n      const used = new Set()\n      records.forEach((rec) => {\n        const material = makeMaterial()\n        if (rec.thumbnail) { material.map = texFor(rec.thumbnail); used.add(rec.thumbnail) }\n        const mesh = new THREE.Mesh(geometry, material)\n        mesh.visible = false\n        mesh.userData.record = rec\n        scene.add(mesh)\n        cards.push({ mesh, material, record: rec })\n      })\n      pruneTextures(used)\n    }\n\n    // ---- init ------------------------------------------------------------\n\n    const init = () => {\n      renderer = new THREE.WebGLRenderer({ canvas: cv, alpha: true, antialias: true })\n      renderer.setClearAlpha(0)\n      renderer.outputColorSpace = THREE.SRGBColorSpace\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))\n\n      scene = new THREE.Scene()\n      camera = new THREE.PerspectiveCamera(45, 1, 1, 12000)\n      raycaster = new THREE.Raycaster()\n      pointer = new THREE.Vector2()\n\n      qYaw = new THREE.Quaternion(); qPitch = new THREE.Quaternion(); qRoll = new THREE.Quaternion()\n      qTwist = new THREE.Quaternion()\n      axisX = new THREE.Vector3(1, 0, 0)\n      axisY = new THREE.Vector3(0, 1, 0)\n      axisZ = new THREE.Vector3(0, 0, 1)\n\n      const onPointer = (e) => {\n        if (!canvasRect) refreshRect()\n        mouseX = e.clientX - canvasRect.left\n        mouseY = e.clientY - canvasRect.top\n        pointer.x = (mouseX / canvasRect.width) * 2 - 1\n        pointer.y = -(mouseY / canvasRect.height) * 2 + 1\n        pointerIn = true\n        placeTip()\n        if (pressing) {\n          const dx = e.clientX - lastDragX\n          lastDragX = e.clientX\n          dragMoved += Math.abs(dx)\n          if (!dragging && dragMoved > 4) { dragging = true; cv.style.cursor = 'grabbing' }\n          if (dragging) pendingDrag += dx\n        }\n      }\n      if ('onpointerrawupdate' in cv) d2.on(cv, 'pointerrawupdate', onPointer)\n      d2.on(cv, 'pointermove', onPointer)\n      d2.on(window, 'scroll', refreshRect, { passive: true })\n\n      d2.on(cv, 'pointerleave', () => {\n        pointerIn = false\n        hoverRec = null\n        cv.style.cursor = 'default'\n        if (tip) tip.classList.remove('on')\n        if (tipGlass) tipGlass.style.width = TIP_MIN + 'px'\n      })\n\n      // Drag anywhere to spin the carousel by hand. Pointer capture keeps the\n      // events coming even when the cursor leaves the canvas mid-drag.\n      d2.on(cv, 'pointerdown', (e) => {\n        if (modalOpen || p < CARD_LIVE) return\n        pressing = true\n        dragging = false\n        lastDragX = e.clientX\n        dragMoved = 0\n        suppressClick = false\n        flingVel = 0; dragVel = 0; pendingDrag = 0; pressT = 0\n        try { cv.setPointerCapture(e.pointerId) } catch (err) {}\n      })\n      d2.on(cv, 'pointerup', (e) => {\n        if (!pressing) return\n        pressing = false\n        suppressClick = dragging\n        dragging = false\n        try { cv.releasePointerCapture(e.pointerId) } catch (err) {}\n        cv.style.cursor = hoverRec ? 'pointer' : 'default'\n      })\n      d2.on(cv, 'click', () => {\n        if (suppressClick) { suppressClick = false; return }\n        if (hoverRec) openCard(hoverRec)\n      })\n    }\n\n    const resize = () => {\n      const w = cv.clientWidth || 1\n      const h = cv.clientHeight || 1\n      const persp = Number(d2.param('persp', 1400)) || 1400\n      refreshRect()\n      sizedOk = w > 1 && h > 1\n      const dpr = window.devicePixelRatio || 1\n      lastDpr = dpr\n      renderer.setPixelRatio(Math.min(dpr, 4))\n      renderer.setSize(w, h, false)\n      camera.fov = 2 * Math.atan((h / 2) / persp) * 180 / Math.PI\n      camera.aspect = w / h\n      camera.position.set(0, 0, persp)\n      camera.updateProjectionMatrix()\n    }\n\n    // ---- frame -----------------------------------------------------------\n\n    const frame = (now) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016\n      last = now\n\n      const speed = Number(d2.param('speed', 0.045))\n      const gap = Math.max(0.001, Number(d2.param('gap', 0.108)))\n      const coil = Number(d2.param('coil', 520))\n      const ySpan = Number(d2.param('ySpan', 1950))\n      const flatten = Number(d2.param('flatten', 0.66))\n      const bendMid = Number(d2.param('bend', 0.7))\n      const bendEnd = Number(d2.param('bendEnds', 1.8))\n      const wobEnd = Number(d2.param('wobble', 14))\n      const wobMid = Number(d2.param('wobbleMid', 3.5))\n      const segments = Math.max(2, Math.min(96, Math.round(Number(d2.param('segments', 40)))))\n      const cardW = Number(d2.param('cardW', 448))\n      const cardH = Number(d2.param('cardH', 283))\n      const turns = Number(d2.param('turns', -1.5))\n      const phase0 = Number(d2.param('phase0', 270))\n      const tiltDeg = Number(d2.param('tilt', 8))\n      const zOff = Number(d2.param('zOffset', -420))\n      const fade = Number(d2.param('fade', 0.12))\n      const peak = Number(d2.param('peak', 0.95))\n      const backDim = Number(d2.param('backDim', 0.28))\n      const edgeFade = Number(d2.param('edgeFade', 0.14))\n      const offX = Number(d2.param('offsetX', 0))\n      const offY = Number(d2.param('offsetY', 0))\n\n      tFloat += dt * 0.7   // idle float at 70% speed\n      // Carousel motion. While dragging, the pointer drives it and we track a\n      // velocity; on release that becomes a fling that decays into the idle\n      // spin. On modal open it brakes gracefully rather than freezing dead.\n      modalOpen = !!readUI('modalOpen')\n      if (modalOpen) {\n        modalT += dt\n        let ms = 1\n        if (modalT > MODAL_HOLD) ms = 1 - ease01(Math.min((modalT - MODAL_HOLD) / MODAL_STOP, 1))\n        tCard += speed * slowF * ms * dt\n      } else {\n        modalT = 0\n        if (dragging) {\n          const move = -pendingDrag * DRAG_K\n          pendingDrag = 0\n          tCard += move\n          const v = move / Math.max(dt, 1e-4)\n          dragVel += (v - dragVel) * (1 - Math.exp(-18 * dt))\n          flingVel = Math.max(-FLING_MAX, Math.min(FLING_MAX, dragVel * FLING_GAIN))\n        } else {\n          if (pressing) pressT += dt\n          const slowTarget = (pressing && pressT > CLICK_HOLD) ? 0 : (hoverRec ? 0.25 : 1)\n          const eRate = slowTarget > slowMid ? 5 : 7\n          slowMid += (slowTarget - slowMid) * (1 - Math.exp(-eRate * dt))\n          slowF += (slowMid - slowF) * (1 - Math.exp(-eRate * dt))\n          tCard += speed * slowF * dt\n          if (flingVel !== 0) {\n            tCard += flingVel * dt\n            flingVel *= Math.exp(-FLING_FRICTION * dt)\n            if (Math.abs(flingVel) < 1e-4) flingVel = 0\n          }\n        }\n      }\n\n      const gs = cardW + 'x' + cardH + '|' + segments\n      if (gs !== geoSig) {\n        const old = geometry\n        geometry = new THREE.PlaneGeometry(cardW, cardH, segments, 1)\n        for (const c of cards) c.mesh.geometry = geometry\n        if (old) old.dispose()\n        geoSig = gs\n      }\n\n      if ((tick++ % 20) === 0) {\n        const handle = d2.data()\n        const records = ((handle && handle.records()) || [])\n          .filter((r) => r.active !== false)\n          .slice()\n          .sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order))\n        const sig = records.map((r) => (r._id || '') + ':' + (r.thumbnail || '')).join('|')\n        if (sig !== builtSig) { buildCards(records); builtSig = sig }\n      }\n\n      const cRadius = bumpSlow(p, CARD_IN, CARD_R_PK, 1, CARD_R0, CARD_OVER, 1)\n      const cScale = bumpSlow(p, CARD_IN, CARD_S_PK, 1, CARD_S0, CARD_OVER, 1)\n\n      const N = Math.max(1, cards.length)\n      const span = Math.max(N * gap, 1)\n      const f = clamp01(flatten) * 0.95\n      const climbAt = (q) => q * (1 - f / 2) + f * Math.sin(2 * Math.PI * q) / (4 * Math.PI)\n      const totalClimb = climbAt(1) || 1\n      const tr = tiltDeg * Math.PI / 180\n      const ct = Math.cos(tr), st = Math.sin(tr)\n\n      for (let i = 0; i < cards.length; i++) {\n        const c = cards[i]\n        const s = (((i * gap + tCard) % span) + span) % span\n        if (s > 1 || p < CARD_IN) { c.mesh.visible = false; continue }\n        c.mesh.visible = true\n\n        const q = s\n        const angDeg = phase0 + q * 360 * turns\n        const a = angDeg * Math.PI / 180\n        const hx = Math.sin(a) * coil\n        const hz = Math.cos(a) * coil\n        const hy = (0.5 - climbAt(q) / totalClimb) * ySpan\n\n        const x = hx * ct - hy * st + offX\n        const y = hx * st + hy * ct + offY\n\n        const fY = FLOAT_AMP * (0.55 + 0.9 * jitter(i, 21)) *\n          Math.sin(tFloat * (0.9 + 1.1 * jitter(i, 22)) + jitter(i, 23) * 6.2831853)\n        c.mesh.position.set(x * cRadius, -y * cRadius + fY, hz + zOff)\n        c.mesh.scale.setScalar(cScale)\n\n        const ends = Math.cos(Math.PI * q) * Math.cos(Math.PI * q)\n\n        const wob = (wobMid + (wobEnd - wobMid) * ends) * Math.PI / 180\n        const rp = FLOAT_PITCH * Math.sin(tFloat * (0.6 + 0.7 * jitter(i, 24)) + jitter(i, 25) * 6.2831853)\n        const rz = FLOAT_TWIST * Math.sin(tFloat * (0.5 + 0.6 * jitter(i, 26)) + jitter(i, 27) * 6.2831853)\n        qYaw.setFromAxisAngle(axisY, a)\n        qPitch.setFromAxisAngle(axisX, signed(i, 2) * wob * 0.6 + rp)\n        qRoll.setFromAxisAngle(axisZ, signed(i, 1) * wob)\n        qTwist.setFromAxisAngle(axisZ, rz)\n        c.mesh.quaternion.copy(qRoll).multiply(qPitch).multiply(qYaw).multiply(qTwist)\n\n        const bend = bendMid + (bendEnd - bendMid) * ends\n        const bendR = Math.min(80000, coil / Math.max(0.001, Math.abs(bend)))\n        const sh = c.material.userData.shader\n        if (sh) sh.uniforms.uBendR.value = bendR\n\n        const near = (hz + coil) / (2 * coil)\n        const depthShade = fade + (peak - fade) * Math.pow(near, 1.15)\n\n        const facing = Math.cos(a)\n        const bt = clamp01((facing + FACE_BAND) / (2 * FACE_BAND))\n        const faceShade = backDim + (1 - backDim) * bt * bt * (3 - 2 * bt)\n        c.material.color.setScalar(depthShade * faceShade)\n\n        const edge = clamp01(Math.min(q, 1 - q) / Math.max(0.001, edgeFade))\n        const stag = (CARD_FADE_B - CARD_FADE_A) * (i / Math.max(1, N - 1))\n        const cIn = clamp01((p - (CARD_IN + stag)) / Math.max(1e-6, CARD_FADE_A - CARD_IN))\n        const op = edge * ease02(cIn)\n        c.material.opacity = op\n        c.material.depthWrite = op > 0.98\n      }\n\n      // Hover is resolved once per frame, not on pointer move: the cards are in\n      // motion, so the card under a stationary cursor changes on its own.\n      if (p < CARD_LIVE || dragging || modalOpen) {\n        setHover(null)\n      } else if (pointerIn && raycaster) {\n        hitList.length = 0\n        for (const c of cards) if (c.mesh.visible) hitList.push(c.mesh)\n        raycaster.setFromCamera(pointer, camera)\n        const hits = raycaster.intersectObjects(hitList, false)\n        setHover(hits.length ? hits[0].object.userData.record : null)\n      }\n\n      renderer.render(scene, camera)\n    }\n\n    // ---- loop ------------------------------------------------------------\n\n    let started = false\n    const ro = new ResizeObserver(() => { needResize = true })\n    ro.observe(cv)\n    const io = new IntersectionObserver((es) => {\n      es.forEach((e) => { onScreen = e.isIntersecting })\n    }, { threshold: 0 })\n    io.observe(cv)\n\n    d2.raf((now) => {\n      if (!started) {\n        THREE = d2.lib('three@0.170.0')\n        if (!THREE) return\n        init()\n        started = true\n      }\n      if (!onScreen) { last = 0; return }\n      if ((window.devicePixelRatio || 1) !== lastDpr) needResize = true\n      if (needResize) { needResize = false; resize() }\n      if (sizedOk) frame(now)\n    })\n\n    d2.onUnmount(() => {\n      ro.disconnect(); io.disconnect()\n      if (scene) disposeCards()\n      pruneTextures(new Set())\n      if (geometry) geometry.dispose()\n      if (renderer) renderer.dispose()\n    })\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"storeRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"storeRef"}}}},"speed":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"speed"}}}},"gap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gap"}}}},"coil":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coil"}}}},"ySpan":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ySpan"}}}},"flatten":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flatten"}}}},"bend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bend"}}}},"bendEnds":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bendEnds"}}}},"wobble":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobble"}}}},"wobbleMid":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"wobbleMid"}}}},"segments":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"segments"}}}},"cardW":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardW"}}}},"cardH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cardH"}}}},"turns":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"turns"}}}},"phase0":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"phase0"}}}},"tilt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tilt"}}}},"persp":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"persp"}}}},"zOffset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zOffset"}}}},"fade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fade"}}}},"peak":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"peak"}}}},"backDim":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"backDim"}}}},"edgeFade":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"edgeFade"}}}},"offsetX":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetX"}}}},"offsetY":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offsetY"}}}}}}}}}},{"_id":"6a6896fa7928293df8013358","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"deckbuilder-editor","package":"bigaton-playground","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 TYPES = [\n  ['cover','Cover'],['statement','Statement'],['section','Section'],['text','Text'],\n  ['split','Split (image+text)'],['trio','Trio (3 cards)'],['gallery','Gallery'],\n  ['data','Data (numbers)'],['bigstat','Big number'],['chart','Chart'],['amenities','Amenities'],\n  ['quote','Quote'],['list','Numbered list'],['image','Image'],['contact','Contact'],['closing','Closing']\n];\nconst FIELDS = {\n  cover:[['kicker','text'],['title','text'],['subtitle','textarea'],['image','image'],['logo','bool']],\n  statement:[['kicker','text'],['title','text'],['sub','textarea'],['image','image']],\n  section:[['number','text'],['kicker','text'],['title','text'],['image','image']],\n  text:[['kicker','text'],['title','text'],['image','image'],['body','strings']],\n  split:[['kicker','text'],['title','text'],['side','text'],['image','image'],['body','strings']],\n  trio:[['kicker','text'],['title','text'],['cards','objs',[['image','image'],['title','text'],['text','textarea']]]],\n  gallery:[['kicker','text'],['title','text'],['items','objs',[['image','image']]]],\n  data:[['number','text'],['kicker','text'],['title','text'],['stats','objs',[['big','text'],['label','text'],['sub','text']]],['note','textarea'],['image','image']],\n  bigstat:[['kicker','text'],['big','text'],['label','text'],['sub','text'],['image','image']],\n  chart:[['number','text'],['kicker','text'],['title','text'],['chartType','text'],['prefix','text'],['suffix','text'],['compact','bool'],['xlabel','text'],['ylabel','text'],['data','objs',[['label','text'],['value','text'],['x','text'],['y','text'],['r','text']]],['note','textarea']],\n  amenities:[['number','text'],['kicker','text'],['title','text'],['items','objs',[['title','text'],['desc','textarea']]]],\n  quote:[['text','textarea'],['author','text'],['image','image']],\n  list:[['kicker','text'],['title','text'],['items','objs',[['lead','text'],['desc','textarea']]]],\n  image:[['image','image'],['caption','text']],\n  contact:[['title','text'],['address','text'],['hours','text'],['email','text'],['site','text'],['image','image']],\n  closing:[['title','text'],['subtitle','text'],['image','image'],['logo','bool']]\n};\nconst LBL = {title:'Title',subtitle:'Subtitle',sub:'Support text',image:'Image',logo:'Show logo',body:'Paragraphs (blank line separates)',cards:'Cards',kpis:'KPIs',stats:'Numbers',note:'Note',items:'Items',lead:'Lead',desc:'Description',caption:'Caption',text:'Text',big:'Number',label:'Label',kicker:'Kicker',number:'Section no.',side:'Image side (right/left)',author:'Author',address:'Address',hours:'Hours',email:'Contact / e-mail',site:'Site',chartType:'Type: bar / hbar / line',prefix:'Prefix (e.g. $ )',suffix:'Suffix (e.g. %)',compact:'Abbreviate thousands (12.3k)',value:'Value',data:'Series (label + value)',x:'X (scatter)',y:'Y (scatter)',r:'Bubble size (scatter)',xlabel:'X axis label',ylabel:'Y axis label'};\nconst TEMPLATES = {\n  cover:{type:'cover',logo:true,kicker:'Kicker',title:'TITLE',subtitle:'subtitle',image:''},\n  statement:{type:'statement',title:'IMPACT STATEMENT',sub:'support text',image:''},\n  section:{type:'section',number:'01',kicker:'Section',title:'SECTION',image:''},\n  text:{type:'text',kicker:'',title:'TITLE',image:'',body:['First paragraph.','Second paragraph.']},\n  split:{type:'split',kicker:'',side:'right',title:'TITLE',image:'',body:['First paragraph.','Second paragraph.']},\n  trio:{type:'trio',title:'TITLE',cards:[{image:'',title:'CARD 1',text:'text'},{image:'',title:'CARD 2',text:'text'},{image:'',title:'CARD 3',text:'text'}]},\n  gallery:{type:'gallery',title:'GALLERY',items:[{image:''},{image:''},{image:''},{image:''}]},\n  data:{type:'data',number:'',kicker:'',title:'TITLE',stats:[{big:'00',label:'label',sub:''},{big:'00',label:'label',sub:''}],note:''},\n  bigstat:{type:'bigstat',big:'200',label:'label',sub:'',image:''},\n  chart:{type:'chart',title:'CHART',chartType:'bar',prefix:'',suffix:'',compact:false,data:[{label:'A',value:'10'},{label:'B',value:'20'},{label:'C',value:'15'}],note:''},\n  amenities:{type:'amenities',number:'',kicker:'',title:'TITLE',items:[{title:'ITEM',desc:'description'},{title:'ITEM',desc:'description'}]},\n  quote:{type:'quote',text:'An impactful sentence.',author:'Author',image:''},\n  list:{type:'list',title:'TITLE',items:[{lead:'Lead',desc:'description'}]},\n  image:{type:'image',image:'',caption:''},\n  contact:{type:'contact',title:'CONTACT',address:'Street ...',hours:'Tue-Sun - 12pm-10pm',email:'hello@yourbrand.com',site:'yourbrand.com',image:''},\n  closing:{type:'closing',logo:true,title:'THANK YOU',subtitle:'contact'}\n};\nlet _FOOT = 'YOUR BRAND', _N = 0, _ACCENT = '#C8A15A';\nfunction _fmtCls(meta){ const f = String((meta && meta.format) || 'a4').toLowerCase(); return (f==='16x9'||f==='landscape'||f==='horizontal') ? 'land' : ''; }\nfunction fontStack(name, serif){ const n=(name||'').trim(); const fb = serif ? \"Georgia, 'Times New Roman', serif\" : \"'Helvetica Neue', system-ui, sans-serif\"; return n ? (\"'\"+n+\"', \"+fb) : fb; }\nconst DEFAULT_BRAND = { key:'default', brand_name:'YOUR BRAND', logo_url:'', accent:'#C8A15A', paper:'#0a0a0a', line:'#2b2b2b', muted:'#8a8a8a', body_color:'#cfcfcf', font_display:'Georgia', font_display_url:'', font_body:'Helvetica Neue', font_body_url:'', footer:'YOUR BRAND', contact_email:'hello@yourbrand.com', contact_site:'yourbrand.com', display_scale:1, body_scale:1, tokens_json:'' };\nfunction brandStyleCss(b){\n  b = b || DEFAULT_BRAND;\n  let ff = '';\n  if (b.font_display_url) ff += \"@font-face{font-family:'\"+b.font_display+\"';src:url('\"+b.font_display_url+\"');font-weight:100 900;font-display:swap}\\n\";\n  if (b.font_body_url) ff += \"@font-face{font-family:'\"+b.font_body+\"';src:url('\"+b.font_body_url+\"');font-weight:100 900;font-display:swap}\\n\";\n  return ff + \":host,.root,.pdeck{--gold:\"+(b.accent||'#C8A15A')+\";--paper:\"+(b.paper||'#0a0a0a')+\";--line:\"+(b.line||'#2b2b2b')+\";--muted:\"+(b.muted||'#8a8a8a')+\";--body:\"+(b.body_color||'#cfcfcf')+\";--font-display:\"+fontStack(b.font_display,true)+\";--font-body:\"+fontStack(b.font_body,false)+\";--display-scale:\"+(b.display_scale||1)+\";--body-scale:\"+(b.body_scale||1)+\"}\";\n}\n\nexport default {\n  css: `\n:host,.root{--gold:#C8A15A;--paper:#0a0a0a;--line:#2b2b2b;--muted:#8a8a8a;--body:#cfcfcf;--font-display:Georgia,serif;--font-body:'Helvetica Neue',system-ui,sans-serif;--display-scale:1;--body-scale:1}\n*{box-sizing:border-box}\n.root{position:absolute;inset:0;background:#0f0f0f;font-family:var(--font-body);color:#eee;overflow:hidden}\n\n/* TOP BAR */\n.top{height:56px;display:flex;align-items:center;gap:12px;padding:0 18px;background:#0b0b0b;border-bottom:1px solid #262626;flex-shrink:0}\n.top .brand{font-weight:800;letter-spacing:2px;color:var(--gold);font-size:14px;white-space:nowrap}\n.tabs{display:flex;gap:4px;margin-left:6px}\n.tabs .tab{background:transparent;border:1px solid #2a2a2a;color:#bbb;border-radius:7px;padding:7px 12px;font-size:12px;cursor:pointer;font-family:var(--font-body);letter-spacing:.5px}\n.tabs .tab.on{background:var(--gold);color:#000;border-color:var(--gold);font-weight:700}\n.top input.tt{background:#161616;border:1px solid #2a2a2a;color:#fff;border-radius:7px;padding:7px 10px;font-size:13px;font-family:var(--font-body)}\n.top .sp{flex:1}\nselect,.btn{background:#161616;border:1px solid #2a2a2a;color:#eee;border-radius:7px;padding:8px 12px;font-size:13px;cursor:pointer;font-family:var(--font-body)}\nselect:focus,input:focus,textarea:focus{outline:none}\n.btn.gold{background:var(--gold);color:#000;border-color:var(--gold);font-weight:700}\n.btn.g{background:transparent;color:var(--gold);border-color:#444}\n.dirty{color:#FF9F1C;font-size:12px;white-space:nowrap}\n\n/* LAYOUT */\n.wrap{display:grid;grid-template-columns:250px 1fr 400px;position:absolute;top:56px;left:0;right:0;bottom:0}\n.side{border-right:1px solid #222;overflow:auto;padding:12px}\n.side h3{font-size:11px;letter-spacing:1px;color:#888;text-transform:uppercase;margin:6px 4px}\n.sl{background:#151515;border:1px solid #242424;border-radius:9px;padding:9px 11px;margin-bottom:8px;cursor:pointer}\n.sl.sel{border-color:var(--gold);background:#1b1706}\n.sl .ty{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--gold)}\n.sl .tt{font-size:13px;color:#eee;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.sl .row{display:flex;gap:4px;margin-top:7px}\n.sl .row button{flex:1;background:#0f0f0f;border:1px solid #2a2a2a;color:#aaa;border-radius:5px;padding:3px;font-size:11px;cursor:pointer}\n.addbar{display:flex;gap:6px;margin-top:10px}.addbar select{flex:1}\n.form{overflow:auto;padding:20px 24px}\n.form h2{font-size:14px;color:var(--gold);text-transform:uppercase;letter-spacing:1px;margin:0 0 16px}\n.f{margin-bottom:14px}.f label{display:block;font-size:11px;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}\n.f input[type=text],.f textarea,.f input[type=number]{width:100%;background:#161616;border:1px solid #2a2a2a;color:#fff;border-radius:8px;padding:9px 11px;font-size:14px;font-family:var(--font-body)}\n.f textarea{min-height:70px;resize:vertical;line-height:1.5}\n.imgf{display:flex;gap:8px;align-items:center}.imgf input{flex:1}.imgf img{width:44px;height:44px;object-fit:cover;border-radius:6px;background:#222}\n.objs{border:1px solid #242424;border-radius:10px;padding:12px;background:#121212}\n.obj{border-bottom:1px solid #222;padding:8px 0;margin-bottom:8px}.obj:last-child{border-bottom:0}\n.obj .oh{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.obj .oh b{font-size:12px;color:#bbb}\n.mini{background:#0f0f0f;border:1px solid #2a2a2a;color:#f88;border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer}\n.addmini{background:#1a1a1a;border:1px dashed #3a3a3a;color:var(--gold);border-radius:7px;padding:7px;width:100%;cursor:pointer;font-size:12px;margin-top:6px}\n.prev{border-left:1px solid #222;background:#161616;overflow:auto;padding:18px;display:flex;flex-direction:column;align-items:center}\n.prevbox{width:794px;transform:scale(.44);transform-origin:top center;margin-bottom:-620px}\n.hint{color:#666;font-size:12px;margin-top:8px}\n\n/* GALLERY MODAL */\n.galm{position:absolute;inset:0;background:#000d;z-index:50;display:none;padding:36px}.galm.on{display:block}\n.galbox{max-width:1150px;margin:0 auto;background:#111;border:1px solid #2a2a2a;border-radius:12px;padding:16px;height:100%;display:flex;flex-direction:column}\n.galh{display:flex;justify-content:space-between;align-items:center;color:#ccc;margin-bottom:12px;font-size:13px}\n.galh button{background:#161616;border:1px solid #333;color:#aaa;border-radius:6px;padding:6px 12px;cursor:pointer}\n.gg{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;overflow:auto}\n.gg img{width:100%;height:120px;object-fit:cover;border-radius:6px;cursor:pointer;border:2px solid transparent;background:#222}\n.gg img:hover{border-color:var(--gold)}\n\n/* FULLSCREEN PANELS (image bank + brand manual) */\n.panel{position:absolute;inset:0;top:56px;background:#111;z-index:40;overflow:auto;padding:26px 30px}\n.panel h1{font-size:18px;color:var(--gold);text-transform:uppercase;letter-spacing:1.5px;margin:0 0 4px}\n.panel .psub{color:#888;font-size:12px;margin-bottom:20px}\n.panel .ptool{display:flex;gap:10px;align-items:center;margin-bottom:20px;flex-wrap:wrap}\n.pcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px}\n.pc{background:#161616;border:1px solid #262626;border-radius:11px;overflow:hidden;display:flex;flex-direction:column}\n.pc .th{width:100%;height:130px;object-fit:cover;background:#222}\n.pc .pcb{padding:9px 10px;display:flex;flex-direction:column;gap:6px}\n.pc .pcb input{background:#0f0f0f;border:1px solid #2a2a2a;color:#eee;border-radius:6px;padding:5px 7px;font-size:12px;font-family:var(--font-body)}\n.pc .pcr{display:flex;gap:6px}\n.pc .pcr button{flex:1;border-radius:6px;padding:5px;font-size:11px;cursor:pointer;border:1px solid #2a2a2a}\n.pc .sv{background:var(--gold);color:#000;font-weight:700;border-color:var(--gold)}\n.pc .dl{background:#1a0f0f;color:#f88}\n.pc .cat{font-size:10px;color:var(--gold);text-transform:uppercase;letter-spacing:1px}\n\n/* BRAND MANUAL */\n.brandwrap{display:grid;grid-template-columns:1fr 460px;gap:26px;align-items:start}\n.bform{display:grid;gap:16px}\n.bgroup{background:#161616;border:1px solid #262626;border-radius:12px;padding:16px 18px}\n.bgroup h4{margin:0 0 12px;font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:#aaa}\n.brow{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:center;margin-bottom:10px}\n.brow:last-child{margin-bottom:0}\n.brow label{font-size:12px;color:#bbb}\n.brow input[type=text],.brow input[type=number]{background:#0f0f0f;border:1px solid #2a2a2a;color:#fff;border-radius:7px;padding:8px 10px;font-size:13px;font-family:var(--font-body);width:100%}\n.brow .clr{display:flex;gap:8px;align-items:center}\n.brow .clr input[type=color]{width:38px;height:34px;border:1px solid #2a2a2a;border-radius:7px;background:#0f0f0f;padding:2px;cursor:pointer}\n.bpreview{position:sticky;top:0}\n.bpreview .pl{font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}\n.bstage{background:#161616;border:1px solid #262626;border-radius:12px;padding:16px;overflow:hidden}\n.bscale{transform:scale(.52);transform-origin:top left;width:794px;height:auto}\n.bscale .slide{margin-bottom:14px}\n\n/* SLIDE */\n.slide{position:relative;width:794px;height:1123px;background:var(--paper);overflow:hidden;color:#fff;font-family:var(--font-body);font-weight:300}\n.slide .bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}\n.slide .scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(10,10,10,.82),rgba(10,10,10,.35) 45%,rgba(10,10,10,.92))}\n.slide .pad{position:absolute;inset:0;padding:80px 76px;display:flex;flex-direction:column}\n.slide .logo{height:70px;width:auto}\n.slide h2.title{font-family:var(--font-display);font-weight:400;color:#fff;text-transform:uppercase;letter-spacing:.5px;line-height:.94;margin:0}\n.slide .gold{color:var(--gold)}\n.kick{font-family:var(--font-body);font-weight:600;letter-spacing:6px;text-transform:uppercase;color:var(--gold);font-size:12px;display:flex;align-items:center;gap:14px}\n.kick .num{font-family:var(--font-display);font-size:15px;color:#fff;opacity:.5}\n.kick .ln{flex:1;height:1px;background:var(--line);max-width:90px}\n.slide .body p{font-size:calc(16px * var(--body-scale,1));line-height:1.75;color:var(--body);margin:0 0 15px;font-weight:300;max-width:600px}\n.slide .foot{position:absolute;bottom:34px;left:76px;right:76px;display:flex;justify-content:space-between;align-items:center;font-family:var(--font-body);letter-spacing:3px;font-size:9.5px;color:#7a7a7a;text-transform:uppercase;border-top:1px solid var(--line);padding-top:14px}\n.slide .foot .fl{display:flex;align-items:center;gap:10px}.slide .foot .fl img{height:15px;opacity:.7}\n.slide .ph{position:absolute;inset:0;background:repeating-linear-gradient(45deg,#121212,#121212 14px,#161616 14px,#161616 28px);display:flex;align-items:center;justify-content:center;color:#4a4a4a;font-size:12px;font-family:var(--font-body);letter-spacing:2px}\n.slide.cover .pad{justify-content:space-between}\n.slide.cover .top{display:flex;justify-content:space-between;align-items:flex-start}\n.slide.cover .ttl{margin-top:auto}\n.slide.cover h2.title{font-size:calc(82px * var(--display-scale,1));margin-top:14px}\n.slide.cover .rule{width:64px;height:2px;background:var(--gold);margin:26px 0 18px}\n.slide.cover .sub{font-size:17px;color:#e6e6e6;font-weight:300;max-width:520px;line-height:1.6}\n.slide.statement .pad{align-items:flex-start;justify-content:center}\n.slide.statement h2.title{font-size:calc(62px * var(--display-scale,1));max-width:660px;line-height:.98}\n.slide.statement .sub{font-size:18px;color:#eaeaea;margin-top:30px;font-weight:300;max-width:520px;line-height:1.7}\n.slide.section .pad{justify-content:center}\n.slide.section .ghost{position:absolute;right:60px;bottom:40px;font-family:var(--font-display);font-size:300px;line-height:.7;color:#ffffff0d}\n.slide.section h2.title{font-size:calc(64px * var(--display-scale,1));max-width:600px}\n.slide.text h2.title{font-size:calc(50px * var(--display-scale,1));margin:14px 0 26px;max-width:600px}\n.slide.text .body{columns:2;column-gap:40px}\n.slide.text .top-img{width:100%;height:300px;object-fit:cover;margin-bottom:30px}\n.slide.split{display:flex;align-items:stretch}\n.slide.split.rev{flex-direction:row-reverse}\n.slide.split .half.txt{flex:0 0 52%;min-width:0;display:flex;flex-direction:column;justify-content:center;padding:80px 60px 80px 76px}\n.slide.split .half.img{flex:0 0 48%;position:relative;overflow:hidden}\n.slide.split .half.img img,.slide.split .half.img .ph{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}\n.slide.split h2.title{font-size:calc(40px * var(--display-scale,1));margin:14px 0 22px}\n.slide.split .body,.slide.split .body p{max-width:none}\n.slide.split .foot{display:none}\n.slide.trio h2.title{font-size:calc(38px * var(--display-scale,1));margin:12px 0 34px}\n.slide .cols{display:grid;grid-template-columns:1fr 1fr 1fr;gap:26px}\n.slide .col img,.slide .col .ph{width:100%;height:250px;object-fit:cover}\n.slide .col h3{font-family:var(--font-display);font-weight:400;color:var(--gold);text-transform:uppercase;font-size:18px;margin:18px 0 10px;letter-spacing:.5px;line-height:1.05}\n.slide .col p{font-size:12.5px;line-height:1.6;color:#c9c9c9;font-weight:300}\n.slide.gallery h2.title{font-size:calc(36px * var(--display-scale,1));margin:12px 0 24px}\n.slide .grid2{display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;gap:14px;flex:1}\n.slide .grid2 img,.slide .grid2 .ph{width:100%;height:100%;object-fit:cover;min-height:200px}\n.slide.data h2.title{font-size:calc(46px * var(--display-scale,1));margin:14px 0 40px}\n.slide .stats{display:grid;grid-template-columns:1fr 1fr}\n.slide .stat{padding:26px 24px 26px 0;border-top:1px solid var(--line)}\n.slide .stat .big{font-family:var(--font-display);font-weight:400;color:var(--gold);font-size:58px;line-height:.85}\n.slide .stat .lab{font-size:14px;color:#eee;margin-top:12px;font-weight:400;letter-spacing:.3px}\n.slide .stat .ksub{font-size:12px;color:var(--muted);margin-top:4px}\n.slide .note{margin-top:38px;font-size:calc(15px * var(--body-scale,1));line-height:1.7;color:#dcdcdc;font-weight:300;max-width:600px}\n.slide.data .planta{display:block;width:100%;max-height:360px;object-fit:contain;object-position:center;margin:30px auto 0;padding-top:26px;border-top:1px solid var(--line);background:transparent}\n.slide.data .planta.ph{height:300px;background:repeating-linear-gradient(45deg,#121212,#121212 14px,#161616 14px,#161616 28px)}\n.slide.bigstat .pad{align-items:flex-start;justify-content:center}\n.slide.bigstat .big{font-family:var(--font-display);color:var(--gold);font-size:calc(220px * var(--display-scale,1));line-height:.8}\n.slide.bigstat .lab{font-size:24px;color:#fff;margin-top:16px;font-weight:300;letter-spacing:.5px}\n.slide.bigstat .ksub{font-size:15px;color:var(--muted);margin-top:8px}\n.slide.amenities h2.title{font-size:calc(42px * var(--display-scale,1));margin:12px 0 30px}\n.slide .amg{display:grid;grid-template-columns:1fr 1fr;gap:2px;background:var(--line)}\n.slide .am{background:var(--paper);padding:24px 22px}\n.slide .am h4{font-family:var(--font-display);font-weight:400;color:var(--gold);text-transform:uppercase;font-size:17px;margin:0 0 8px;letter-spacing:.5px}\n.slide .am p{font-size:12.5px;line-height:1.6;color:#c4c4c4;font-weight:300;margin:0}\n.slide.quote .pad{justify-content:center}\n.slide.quote .q{font-family:var(--font-display);font-weight:400;color:#fff;text-transform:uppercase;font-size:calc(46px * var(--display-scale,1));line-height:1.05;max-width:640px}\n.slide.quote .q:before{content:'“';color:var(--gold);display:block;font-size:90px;line-height:.4;margin-bottom:20px}\n.slide.quote .au{margin-top:34px;font-size:13px;letter-spacing:3px;text-transform:uppercase;color:var(--muted)}\n.slide.list h2.title{font-size:calc(44px * var(--display-scale,1));margin:12px 0 28px}\n.slide .li{display:grid;grid-template-columns:64px 1fr;gap:8px;padding:18px 0;border-top:1px solid var(--line)}\n.slide .li .n{font-family:var(--font-display);color:var(--gold);font-size:30px;line-height:1}\n.slide .li .lead{font-family:var(--font-body);font-weight:600;color:#fff;font-size:18px}\n.slide .li .desc{font-size:13.5px;color:#c4c4c4;font-weight:300;line-height:1.6;margin-top:4px}\n.slide.contact .pad{justify-content:center}\n.slide.contact h2.title{font-size:calc(52px * var(--display-scale,1));margin-bottom:34px}\n.slide.contact .crow{display:flex;flex-direction:column;gap:20px}\n.slide.contact .ci .cl{font-size:11px;letter-spacing:3px;text-transform:uppercase;color:var(--gold);margin-bottom:5px}\n.slide.contact .ci .cv{font-size:19px;color:#eee;font-weight:300}\n.slide.closing .pad{align-items:center;justify-content:center;text-align:center;gap:32px}\n.slide.closing h2.title{font-size:calc(58px * var(--display-scale,1))}\n.slide.closing .sub{font-size:14px;color:var(--muted);letter-spacing:4px;font-family:var(--font-body);text-transform:uppercase}\n.slide.closing .logo{height:130px}\n.slide.chart h2.title{font-size:calc(44px * var(--display-scale,1));margin:14px 0 20px}\n.slide .chartwrap{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;width:100%}\n.slide .csvg{width:100%;height:100%;max-height:660px}\n.slide .csvg text{font-family:var(--font-body)}\n.slide .csvg .clab{font-size:23px;fill:#eaeaea;font-weight:500}\n.slide .csvg .cval{font-size:22px;fill:var(--gold);font-weight:700}\n.slide .csvg .cax{font-size:18px;fill:#8a8a8a;font-weight:500}\n.slide .csvg .cgrid{stroke:#2b2b2b;stroke-width:1}\n.slide.chart .note{margin-top:20px}\n\n.deck.land .slide,.pdeck.land .slide,.prevbox.land .slide{width:1123px;height:794px}\n.prevbox.land{width:1123px;transform:scale(.32);margin-bottom:-540px}\n.land .slide .pad{padding:54px 68px}\n.land .slide .foot{bottom:26px;left:68px;right:68px;padding-top:11px}\n\n/* VIEWER */\n.viewer{position:absolute;inset:0;background:#161616;overflow:auto;padding:40px 0}\n.viewbar{position:fixed;top:14px;right:18px;z-index:60;display:flex;gap:8px}\n.viewer .deck{display:flex;flex-direction:column;align-items:center;gap:26px}\n.viewer .slide{box-shadow:0 10px 40px #0008}\n@media print{\n  .root.viewing>.top,.root.viewing>.wrap{display:none!important}\n  .viewbar{display:none}\n  .viewer{padding:0;background:#fff;position:static;overflow:visible}\n  .viewer .deck{gap:0}\n  .slide{box-shadow:none;page-break-after:always;break-after:page}\n  @page{size:A4;margin:0}\n}\n`,\n\n  render(d2) {\n    const S = d2.state;\n    if (!S.brand) S.brand = DEFAULT_BRAND;\n    _ACCENT = S.brand.accent || '#C8A15A';\n    _FOOT = S.brand.footer || 'YOUR BRAND';\n    const LOGO = S.brand.logo_url || '';\n    const brandCss = brandStyleCss(S.brand);\n    const deck = S.deck || {meta:{footer:S.brand.footer||'YOUR BRAND'},slides:[]};\n    if (!Array.isArray(deck.slides)) deck.slides = [];\n    let sel = (S.sel==null)?-1:S.sel;\n    if (sel >= deck.slides.length) sel = deck.slides.length ? 0 : -1;\n    const decks = S.decks || [];\n    const listHtml = (deck.slides||[]).map((s,i)=>`\n      <div class=\"sl ${i===sel?'sel':''}\" data-slide=\"${i}\">\n        <div class=\"ty\">${esc(s.type)}</div><div class=\"tt\">${esc(s.title||s.subtitle||'(untitled)')}</div>\n        <div class=\"row\">\n          <button data-move=\"${i}\" data-dir=\"-1\">&uarr;</button>\n          <button data-move=\"${i}\" data-dir=\"1\">&darr;</button>\n          <button data-dup=\"${i}\">&#10697;</button>\n          <button data-del=\"${i}\">&#10005;</button>\n        </div>\n      </div>`).join('') || '<p class=\"hint\">no slides</p>';\n    const options = TYPES.map(([v,l])=>`<option value=\"${v}\">${l}</option>`).join('');\n    let previewHtml = '';\n    try { previewHtml = (sel>=0 && deck.slides[sel]) ? (DeckRenderFoot(deck.meta&&deck.meta.footer,S.brand), DeckRenderOne(deck.slides[sel],1,LOGO)) : ''; } catch(err){ previewHtml = '<p class=\"hint\">preview unavailable</p>'; }\n    let formHtml = '<p class=\"hint\">Select or add a slide.</p>';\n    try { if (sel>=0 && deck.slides[sel]) formHtml = renderForm(deck.slides[sel]); } catch(err){ formHtml = '<p class=\"hint\">form unavailable</p>'; }\n\n    return `<div class=\"root\" data-key=\"db-root\">\n      <style id=\"dbBrandVars\">${brandCss}</style>\n      <div class=\"top\">\n        <span class=\"brand\" id=\"brandLabel\">${esc((S.brand.brand_name||'YOUR BRAND'))} &middot; DECK</span>\n        <div class=\"tabs\">\n          <button class=\"tab on\" data-view=\"editor\" id=\"tabEditor\">Editor</button>\n          <button class=\"tab\" data-view=\"images\" id=\"tabImages\">Image Bank</button>\n          <button class=\"tab\" data-view=\"brand\" id=\"tabBrand\">Brand Manual</button>\n        </div>\n        <select id=\"deckSel\">${decks.map(dd=>`<option value=\"${escAttr(dd.slug)}\" ${dd.slug===S.curDeck?'selected':''}>${esc(dd.nome||dd.slug)}</option>`).join('')}</select>\n        <button class=\"btn\" id=\"newDeck\">+ deck</button>\n        <input class=\"tt\" id=\"metaTitle\" placeholder=\"title\" style=\"width:200px\" value=\"${escAttr(deck.meta&&deck.meta.title)}\">\n        <input class=\"tt\" id=\"metaFoot\" placeholder=\"footer\" style=\"width:150px\" value=\"${escAttr(deck.meta&&deck.meta.footer)}\">\n        <select id=\"metaFmt\" title=\"slide format\"><option value=\"a4\" ${_fmtCls(deck.meta)?'':'selected'}>Portrait</option><option value=\"16x9\" ${_fmtCls(deck.meta)?'selected':''}>Landscape</option></select>\n        <span class=\"sp\"></span>\n        <span class=\"dirty\" id=\"dstate\">${S.dirty?'● unsaved':(S.savedFlag?'saved ✓':'')}</span>\n        <button class=\"btn gold\" id=\"saveBtn\">Save</button>\n        <button class=\"btn g\" id=\"viewBtn\">Open ↗</button>\n      </div>\n      <div class=\"wrap\">\n        <div class=\"side\">\n          <h3>Slides</h3>\n          <div id=\"list\">${listHtml}</div>\n          <div class=\"addbar\">\n            <select id=\"addType\">${options}</select>\n            <button class=\"btn gold\" id=\"addBtn\">+</button>\n          </div>\n        </div>\n        <div class=\"form\" id=\"form\">${formHtml}</div>\n        <div class=\"prev\"><div class=\"prevbox ${_fmtCls(deck.meta)}\" id=\"prev\">${previewHtml}</div><div class=\"hint\">preview</div></div>\n      </div>\n      <div class=\"galm ${S.galOpen?'on':''}\" id=\"galm\">\n        <div class=\"galbox\">\n          <div class=\"galh\"><span>Image bank &mdash; click a photo to insert</span><button id=\"galClose\">close</button></div>\n          <div class=\"gg\" id=\"gg\">${(S.photos||[]).map(p=>{const full=p.full||p,th=p.thumb||full;return `<img loading=\"lazy\" src=\"${escAttr(th)}\" data-full=\"${escAttr(full)}\">`;}).join('')||'<p class=\"hint\">no photos</p>'}</div>\n        </div>\n      </div>\n    </div>`;\n  },\n\n  updated(d2, root) {},\n\n  mounted(d2, root) {\n    const S = d2.state;\n    const api = () => d2.rush('deckbuilder-api');\n\n    async function callApi(payload){\n      try {\n        let r = await api().invoke(payload);\n        if (r && r.data !== undefined) r = r.data;\n        if (r && r.response && r.response.data !== undefined) r = r.response.data;\n        if (r && (r.ok !== undefined || r.decks !== undefined || r.photos !== undefined || r.deck !== undefined || r.brand !== undefined)) return r;\n        d2.log('rush unexpected response', r);\n      } catch(e){ d2.log('rush invoke failed, trying fetch', e); }\n      const res = await fetch('https://bigaton-playground.data2apis.com/rush/deckbuilder-api', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(payload) });\n      const j = await res.json();\n      return (j && j.data !== undefined) ? j.data : j;\n    }\n\n    function applyBrand(b){\n      S.brand = b || DEFAULT_BRAND;\n      _ACCENT = S.brand.accent || '#C8A15A';\n      _FOOT = S.brand.footer || 'YOUR BRAND';\n      let st = d2.$('#dbBrandVars');\n      if (st) st.textContent = brandStyleCss(S.brand);\n      const bl = d2.$('#brandLabel'); if (bl) bl.innerHTML = esc(S.brand.brand_name||'YOUR BRAND') + ' &middot; DECK';\n    }\n    function LOGOURL(){ return (S.brand && S.brand.logo_url) || ''; }\n\n    async function loadBrand(){\n      try { const r = await callApi({ action:'brand-get' }); if (r && r.brand) applyBrand(normBrand(r.brand)); else applyBrand(DEFAULT_BRAND); refreshPreview(); } catch(e){ d2.log('brand err', e); }\n    }\n    async function loadList() {\n      try { const r = await callApi({ action: 'list' }); S.decks = (r && r.decks) || []; paint(); } catch (e) { d2.log('list err', e); }\n    }\n    async function loadDeck(slug) {\n      try {\n        const r = await callApi({ action: 'load', slug });\n        const rec = r && r.deck; S.curDeck = slug;\n        if (rec) S.deck = { meta: safeParse(rec.meta_json, {footer:S.brand.footer}), slides: safeParse(rec.slides_json, []) };\n        else S.deck = { meta:{footer:S.brand.footer}, slides:[] };\n        S.sel = (S.deck.slides.length>0)?0:-1; S.dirty=false; S.savedFlag=false; paint();\n      } catch (e) { d2.log('load err', e); }\n    }\n    async function loadPhotos() {\n      try { const r = await callApi({ action: 'gallery' }); S.photos = (r && r.photos) || []; paint(); } catch (e) { d2.log('gallery err', e); }\n    }\n\n    function buildListHtml(){\n      const deck = S.deck || {slides:[]}; const slides = deck.slides || []; const sel = (S.sel==null)?-1:S.sel;\n      return slides.map((s,i)=>`\n      <div class=\"sl ${i===sel?'sel':''}\" data-slide=\"${i}\">\n        <div class=\"ty\">${esc(s.type)}</div><div class=\"tt\">${esc(s.title||s.subtitle||'(untitled)')}</div>\n        <div class=\"row\">\n          <button data-move=\"${i}\" data-dir=\"-1\">&uarr;</button>\n          <button data-move=\"${i}\" data-dir=\"1\">&darr;</button>\n          <button data-dup=\"${i}\">&#10697;</button>\n          <button data-del=\"${i}\">&#10005;</button>\n        </div>\n      </div>`).join('') || '<p class=\"hint\">no slides</p>';\n    }\n    function paint(){\n      try {\n        const deck = S.deck || {meta:{},slides:[]};\n        const selEl = d2.$('#deckSel');\n        if (selEl) selEl.innerHTML = (S.decks||[]).map(dd=>`<option value=\"${escAttr(dd.slug)}\" ${dd.slug===S.curDeck?'selected':''}>${esc(dd.nome||dd.slug)}</option>`).join('');\n        const mt = d2.$('#metaTitle'); if (mt && document.activeElement!==mt) mt.value = (deck.meta && deck.meta.title) || '';\n        const mf = d2.$('#metaFoot'); if (mf && document.activeElement!==mf) mf.value = (deck.meta && deck.meta.footer) || '';\n        const mfmt = d2.$('#metaFmt'); if (mfmt) mfmt.value = _fmtCls(deck.meta) ? '16x9' : 'a4';\n        const pbx = d2.$('#prev'); if (pbx) pbx.classList.toggle('land', !!_fmtCls(deck.meta));\n        const list = d2.$('#list'); if (list) list.innerHTML = buildListHtml();\n        const form = d2.$('#form');\n        if (form) { let fh = '<p class=\"hint\">Select or add a slide.</p>'; try { if (S.sel>=0 && deck.slides && deck.slides[S.sel]) fh = renderForm(deck.slides[S.sel]); } catch(e){} form.innerHTML = fh; }\n        refreshPreview();\n        const ds = d2.$('#dstate'); if (ds) ds.textContent = S.dirty?'● unsaved':(S.savedFlag?'saved ✓':'');\n        const gm = d2.$('#galm'); if (gm) gm.classList.toggle('on', !!S.galOpen);\n        const gg = d2.$('#gg'); if (gg) gg.innerHTML = (S.photos||[]).map(p=>{const full=p.full||p,th=p.thumb||full;return `<img loading=\"lazy\" src=\"${escAttr(th)}\" data-full=\"${escAttr(full)}\">`;}).join('')||'<p class=\"hint\">no photos</p>';\n      } catch(e){ d2.log('paint err', e); }\n    }\n\n    function showViewer(){\n      const rootEl = d2.$('.root'); if(!rootEl) return;\n      const deck = S.deck || {meta:{},slides:[]};\n      DeckRenderFoot(deck.meta && deck.meta.footer, S.brand);\n      const html = (deck.slides||[]).map((s,i)=>DeckRenderOne(s,i+1,LOGOURL())).join('');\n      const v = document.createElement('div');\n      v.className = 'viewer'; v.id = 'viewerOverlay'; v.style.zIndex = '900';\n      v.innerHTML = `<div class=\"viewbar\"><button class=\"btn g\" id=\"backEdit\">&larr; Editor</button><button class=\"btn gold\" id=\"doPrint\">Print / PDF</button></div><div class=\"deck ${_fmtCls(deck.meta)}\">${html}</div>`;\n      rootEl.appendChild(v); rootEl.classList.add('viewing');\n      const b = v.querySelector('#backEdit'); if (b) b.onclick = () => closeViewer();\n      const p = v.querySelector('#doPrint'); if (p) p.onclick = () => doPrint();\n    }\n    function closeViewer(){ const v = d2.$('#viewerOverlay'); if (v) v.remove(); const rootEl = d2.$('.root'); if (rootEl) rootEl.classList.remove('viewing'); }\n    function doPrint(){\n      const deck = S.deck || {meta:{},slides:[]};\n      DeckRenderFoot(deck.meta && deck.meta.footer, S.brand);\n      const html = (deck.slides||[]).map((s,i)=>DeckRenderOne(s,i+1,LOGOURL())).join('');\n      let css = '';\n      try { const sr = d2.$('.root').getRootNode(); sr.querySelectorAll('style').forEach(st=>{ css += st.textContent; }); } catch(e){}\n      const b = S.brand || DEFAULT_BRAND;\n      const doc = '<!doctype html><html><head><meta charset=\"utf-8\"><title>' + esc((deck.meta&&deck.meta.title)||'Deck') + '</title><style>' + css + `\n        html,body{margin:0;padding:0;background:#fff}\n        .pdeck{--gold:${b.accent};--paper:${b.paper};--line:${b.line};--muted:${b.muted};--body:${b.body_color};--font-display:${fontStack(b.font_display,true)};--font-body:${fontStack(b.font_body,false)};--display-scale:${b.display_scale||1};--body-scale:${b.body_scale||1};display:flex;flex-direction:column;align-items:center}\n        .pdeck .slide{page-break-after:always;break-after:page;box-shadow:none;margin:0}\n        .pdeck .slide .scrim{background:rgba(10,10,10,.45)!important}\n        .pdeck .slide.image .scrim{background:rgba(10,10,10,.35)!important}\n        @page{size:A4 ${_fmtCls(deck.meta)?'landscape':''};margin:0}\n        *{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}\n      </style></head><body><div class=\"pdeck ${_fmtCls(deck.meta)}\">` + html + '</div><scr' + 'ipt>window.onload=function(){setTimeout(function(){window.print();},800);};</scr' + 'ipt></body></html>';\n      let w = null; try { w = window.open('', '_blank'); } catch(e){}\n      if (w) { w.document.open(); w.document.write(doc); w.document.close(); }\n      else { try { d2.dialog.alert('Enable pop-ups to export the PDF.'); } catch(e){} }\n    }\n\n    // ---------- delegation ----------\n    const hostRoot = d2.$('.root');\n    if (hostRoot && !hostRoot._deleg) {\n      hostRoot._deleg = true;\n      hostRoot.addEventListener('click', (e) => {\n        const t = e.target; if (!t || !t.closest) return;\n        const tab = t.closest('[data-view]'); if (tab) { switchView(tab.dataset.view); return; }\n        const mv = t.closest('[data-move]'); if (mv) { e.stopPropagation(); move(+mv.dataset.move, +mv.dataset.dir); return; }\n        const dp = t.closest('[data-dup]'); if (dp) { e.stopPropagation(); dup(+dp.dataset.dup); return; }\n        const dl = t.closest('[data-del]'); if (dl) { e.stopPropagation(); (async()=>{ const ok = await d2.dialog.confirm('delete slide?'); if (ok) del(+dl.dataset.del); })(); return; }\n        const gb = t.closest('.galBtn'); if (gb) { S.pickCtx = gb.dataset.idx!=null ? {k:gb.dataset.k, idx:+gb.dataset.idx, sk:gb.dataset.sk} : {k:gb.dataset.k}; S.galOpen = true; paint(); return; }\n        const ub = t.closest('.upBtn'); if (ub) { S.pickCtx = ub.dataset.idx!=null ? {k:ub.dataset.k, idx:+ub.dataset.idx, sk:ub.dataset.sk} : {k:ub.dataset.k}; doUpload(); return; }\n        const ao = t.closest('.addObjBtn'); if (ao) { addObj(ao.dataset.k); return; }\n        const dob = t.closest('.delObjBtn'); if (dob) { delObj(dob.dataset.k, +dob.dataset.idx); return; }\n        const sl = t.closest('.sl'); if (sl && !t.closest('button')) { selectSlide(+sl.dataset.slide); return; }\n      });\n      hostRoot.addEventListener('input', (e) => {\n        const t = e.target; if (!t || !t.matches) return;\n        if (t.matches('[data-f]')) { const k = t.dataset.f, kind = t.dataset.kind; if (kind === 'bool') return; if (kind === 'strings') setStrings(k, t.value); else setF(k, t.value); return; }\n        if (t.matches('[data-obj]')) setObj(t.dataset.obj, +t.dataset.idx, t.dataset.sk, t.value);\n      });\n      hostRoot.addEventListener('change', (e) => {\n        const t = e.target; if (!t || !t.matches) return;\n        if (t.matches('[data-f]') && t.dataset.kind === 'bool') setF(t.dataset.f, t.checked);\n      });\n    }\n\n    // ---------- bootstrap ----------\n    if (!S._booted) {\n      S._booted = true;\n      (async () => { await loadBrand(); await loadPhotos(); await loadList(); await loadDeck(S.curDeck || 'welcome'); })();\n    }\n\n    // ---------- top bar ----------\n    d2.on('#deckSel', 'change', (e, el) => { loadDeck(el.value); });\n    d2.on('#metaTitle', 'input', (e, el) => { ensureMeta(); S.deck.meta.title = el.value; markDirty(); });\n    d2.on('#metaFoot', 'input', (e, el) => { ensureMeta(); S.deck.meta.footer = el.value; markDirty(); });\n    d2.on('#metaFmt', 'change', (e, el) => { ensureMeta(); S.deck.meta.format = el.value; markDirty(); const pbx = d2.$('#prev'); if (pbx) pbx.classList.toggle('land', el.value !== 'a4'); });\n    d2.on('#newDeck', 'click', async () => {\n      const name = await d2.dialog.prompt('name of the new deck:', 'new');\n      if (!name) return;\n      const slug = name.toLowerCase().replace(/[^a-z0-9_-]/g,'-');\n      S.curDeck = slug; S.deck = { meta:{title:name, footer:S.brand.footer}, slides:[] }; S.sel = -1; markDirty();\n      await save(); await loadList(); paint();\n    });\n    d2.on('#saveBtn', 'click', () => save());\n    d2.on('#viewBtn', 'click', () => { showViewer(); });\n    d2.on('#addBtn', 'click', () => { const selEl = d2.$('#addType'); addSlide(selEl ? selEl.value : 'cover'); });\n\n    // ---------- gallery modal ----------\n    d2.on('#galClose', 'click', () => { S.galOpen=false; paint(); });\n    d2.on('#galm', 'click', (e, el) => { if (e.target && e.target.id === 'galm') { S.galOpen=false; paint(); } });\n    d2.on('#gg', 'click', (e) => { const img = e.target && e.target.closest('img[data-full]'); if (!img) return; applyPhoto(img.dataset.full); });\n\n    // ---------- view switch ----------\n    function setTabs(v){ ['editor','images','brand'].forEach(x=>{ const el=d2.$('#tab'+x.charAt(0).toUpperCase()+x.slice(1)); if(el) el.classList.toggle('on', x===v); }); }\n    function switchView(v){\n      S.view = v; setTabs(v);\n      closePanel();\n      if (v === 'images') openImages();\n      else if (v === 'brand') openBrand();\n    }\n    function closePanel(){ const p = d2.$('#dbPanel'); if (p) p.remove(); }\n\n    // ---------- IMAGE BANK ----------\n    function catList(){ const cats = {}; (S.photos||[]).forEach(p=>{ const c=(p.category||'').trim(); if(c) cats[c]=1; }); return Object.keys(cats).sort(); }\n    function imgCardHtml(p){\n      return `<div class=\"pc\" data-id=\"${escAttr(p.id)}\">\n        <img class=\"th\" loading=\"lazy\" src=\"${escAttr(p.thumb||p.full)}\" onerror=\"this.style.opacity=.2\">\n        <div class=\"pcb\">\n          <div class=\"cat\">${esc(p.category||'uncategorized')}</div>\n          <input data-pf=\"category\" placeholder=\"category\" value=\"${escAttr(p.category)}\">\n          <input data-pf=\"ordem\" type=\"number\" placeholder=\"order\" value=\"${escAttr(p.ordem)}\">\n          <input data-pf=\"source\" placeholder=\"source\" value=\"${escAttr(p.source)}\">\n          <div class=\"pcr\"><button class=\"sv\" data-psave=\"${escAttr(p.id)}\">save</button><button class=\"dl\" data-pdel=\"${escAttr(p.id)}\">delete</button></div>\n        </div>\n      </div>`;\n    }\n    function renderImagesPanel(){\n      const filter = S.imgFilter || '';\n      const cats = catList();\n      const rows = (S.photos||[]).filter(p=>!filter || (p.category||'')===filter);\n      const catOpts = ['<option value=\"\">All categories</option>'].concat(cats.map(c=>`<option value=\"${escAttr(c)}\" ${c===filter?'selected':''}>${esc(c)}</option>`)).join('');\n      return `<div class=\"panel\" id=\"dbPanel\">\n        <h1>Image Bank</h1>\n        <div class=\"psub\">${(S.photos||[]).length} images · ${cats.length} categories</div>\n        <div class=\"ptool\">\n          <button class=\"btn gold\" id=\"imgUpload\">+ Upload</button>\n          <select id=\"imgFilter\">${catOpts}</select>\n          <input class=\"tt\" id=\"imgNewCat\" placeholder=\"default category for upload\" style=\"width:230px\" value=\"${escAttr(S.uploadCat)}\">\n          <span class=\"sp\"></span>\n        </div>\n        <div class=\"pcards\" id=\"imgCards\">${rows.map(imgCardHtml).join('')||'<p class=\"hint\">no images yet — use Upload</p>'}</div>\n      </div>`;\n    }\n    function openImages(){\n      const rootEl = d2.$('.root'); if(!rootEl) return; closePanel();\n      const wrap = document.createElement('div'); wrap.innerHTML = renderImagesPanel();\n      const panel = wrap.firstElementChild; rootEl.appendChild(panel);\n      d2.$('#imgUpload').onclick = () => doUploadBank();\n      d2.$('#imgFilter').onchange = (e) => { S.imgFilter = e.target.value; repaintImages(); };\n      d2.$('#imgNewCat').oninput = (e) => { S.uploadCat = e.target.value; };\n      panel.addEventListener('click', (e) => {\n        const sv = e.target.closest('[data-psave]'); if (sv) { savePhoto(sv.dataset.psave, sv.closest('.pc')); return; }\n        const dl = e.target.closest('[data-pdel]'); if (dl) { (async()=>{ const ok = await d2.dialog.confirm('delete this image?'); if (ok) delPhoto(dl.dataset.pdel); })(); return; }\n      });\n    }\n    function repaintImages(){ const p = d2.$('#dbPanel'); if (!p || S.view!=='images') return; const cards = d2.$('#imgCards'); const filter=S.imgFilter||''; const rows=(S.photos||[]).filter(x=>!filter||(x.category||'')===filter); if (cards) cards.innerHTML = rows.map(imgCardHtml).join('')||'<p class=\"hint\">no images</p>'; const f=d2.$('#imgFilter'); if(f){ const cats=catList(); f.innerHTML=['<option value=\"\">All categories</option>'].concat(cats.map(c=>`<option value=\"${escAttr(c)}\" ${c===filter?'selected':''}>${esc(c)}</option>`)).join(''); } }\n    async function savePhoto(id, cardEl){\n      if (!cardEl) return;\n      const get = (k) => { const el = cardEl.querySelector('[data-pf=\"'+k+'\"]'); return el ? el.value : ''; };\n      const photo = (S.photos||[]).find(p=>String(p.id)===String(id)); if (!photo) return;\n      const rec = { action:'photo-update', id, full: photo.full, thumb: photo.thumb, ordem: (+get('ordem')||0), source: get('source'), category: get('category') };\n      try { const r = await callApi(rec); if (r && r.ok) { photo.category=rec.category; photo.ordem=rec.ordem; photo.source=rec.source; repaintImages(); paint(); } } catch(e){ d2.log('photo save err', e); }\n    }\n    async function delPhoto(id){\n      try { const r = await callApi({ action:'photo-del', id }); if (r && r.ok) { S.photos = (S.photos||[]).filter(p=>String(p.id)!==String(id)); repaintImages(); paint(); } } catch(e){ d2.log('photo del err', e); }\n    }\n    async function doUploadBank(){\n      const input = document.createElement('input'); input.type='file'; input.accept='image/*'; input.multiple = true;\n      input.onchange = async () => {\n        const files = Array.from(input.files||[]); if(!files.length) return;\n        for (const file of files) {\n          try {\n            const out = await d2.drive('deckbuilder-media').uploadFromFiles([file]);\n            const fileUrl = out && out[0] && (out[0].fileUrl || (out[0].params && out[0].params.fileUrl));\n            if(!fileUrl) continue;\n            const r = await callApi({ action:'upload', full: fileUrl, thumb: fileUrl, category: (S.uploadCat||'') });\n            const newId = r && r.id;\n            if (S.photos) S.photos.push({ id:newId, full: fileUrl, thumb: fileUrl, ordem:9999, source:'upload', category:(S.uploadCat||'') });\n          } catch(e){ d2.log('bank upload err', e); }\n        }\n        repaintImages(); paint();\n      };\n      input.click();\n    }\n\n    // ---------- BRAND MANUAL ----------\n    function bField(k){ const el = d2.$('#bf_'+k); return el ? el.value : ''; }\n    function collectBrand(){\n      return { key:'default', brand_name:bField('brand_name'), logo_url:bField('logo_url'), accent:bField('accent'), paper:bField('paper'), line:bField('line'), muted:bField('muted'), body_color:bField('body_color'), font_display:bField('font_display'), font_display_url:bField('font_display_url'), font_body:bField('font_body'), font_body_url:bField('font_body_url'), footer:bField('footer'), contact_email:bField('contact_email'), contact_site:bField('contact_site'), display_scale:(+bField('display_scale')||1), body_scale:(+bField('body_scale')||1), tokens_json:'' };\n    }\n    function colorRow(label,k,b){ return `<div class=\"brow\"><label>${label}</label><div class=\"clr\"><input type=\"color\" data-bc=\"${k}\" value=\"${escAttr(b[k])}\"><input type=\"text\" id=\"bf_${k}\" data-bk=\"${k}\" value=\"${escAttr(b[k])}\"></div></div>`; }\n    function textRow(label,k,b,ph){ return `<div class=\"brow\"><label>${label}</label><input type=\"text\" id=\"bf_${k}\" data-bk=\"${k}\" placeholder=\"${ph||''}\" value=\"${escAttr(b[k])}\"></div>`; }\n    function numRow(label,k,b){ return `<div class=\"brow\"><label>${label}</label><input type=\"number\" step=\"0.05\" min=\"0.5\" max=\"2\" id=\"bf_${k}\" data-bk=\"${k}\" value=\"${escAttr(b[k])}\"></div>`; }\n    function brandPreviewHtml(b){\n      DeckRenderFoot(b.footer, b);\n      const cover = { type:'cover', logo:!!b.logo_url, kicker:'Preview', title:(b.brand_name||'YOUR BRAND'), subtitle:'Live brand preview', image:'' };\n      const data = { type:'data', kicker:'Metrics', title:'BY THE NUMBERS', stats:[{big:'250',label:'label one',sub:'sub'},{big:'18k',label:'label two',sub:'sub'}], note:'' };\n      return DeckRenderOne(cover,1,b.logo_url||'') + DeckRenderOne(data,2,b.logo_url||'');\n    }\n    function renderBrandPanel(){\n      const b = normBrand(S.brand||DEFAULT_BRAND);\n      return `<div class=\"panel\" id=\"dbPanel\">\n        <h1>Brand Manual</h1>\n        <div class=\"psub\">These rules drive every slide's typography, color and logo — exactly like the source app, but editable.</div>\n        <div class=\"brandwrap\">\n          <div class=\"bform\">\n            <div class=\"bgroup\"><h4>Identity</h4>\n              ${textRow('Brand name','brand_name',b,'YOUR BRAND')}\n              ${textRow('Logo URL','logo_url',b,'https://...svg/png')}\n              ${textRow('Default footer','footer',b,'YOUR BRAND')}\n            </div>\n            <div class=\"bgroup\"><h4>Colors</h4>\n              ${colorRow('Accent','accent',b)}\n              ${colorRow('Paper (bg)','paper',b)}\n              ${colorRow('Line','line',b)}\n              ${colorRow('Muted','muted',b)}\n              ${colorRow('Body text','body_color',b)}\n            </div>\n            <div class=\"bgroup\"><h4>Typography</h4>\n              ${textRow('Display font','font_display',b,'Georgia')}\n              ${textRow('Display font URL','font_display_url',b,'woff2/ttf (optional)')}\n              ${textRow('Body font','font_body',b,'Helvetica Neue')}\n              ${textRow('Body font URL','font_body_url',b,'woff2/ttf (optional)')}\n              ${numRow('Display scale','display_scale',b)}\n              ${numRow('Body scale','body_scale',b)}\n            </div>\n            <div class=\"bgroup\"><h4>Contact defaults</h4>\n              ${textRow('Contact email','contact_email',b,'hello@yourbrand.com')}\n              ${textRow('Contact site','contact_site',b,'yourbrand.com')}\n            </div>\n            <div style=\"display:flex;gap:10px;align-items:center\">\n              <button class=\"btn gold\" id=\"brandSave\">Save brand</button>\n              <span class=\"dirty\" id=\"brandState\"></span>\n            </div>\n          </div>\n          <div class=\"bpreview\">\n            <div class=\"pl\">Live preview</div>\n            <div class=\"bstage\"><div class=\"bscale\" id=\"bStage\" style=\"${brandVarsInline(b)}\">${brandPreviewHtml(b)}</div></div>\n          </div>\n        </div>\n      </div>`;\n    }\n    function brandVarsInline(b){ return `--gold:${b.accent};--paper:${b.paper};--line:${b.line};--muted:${b.muted};--body:${b.body_color};--font-display:${fontStack(b.font_display,true)};--font-body:${fontStack(b.font_body,false)};--display-scale:${b.display_scale||1};--body-scale:${b.body_scale||1}`; }\n    function openBrand(){\n      const rootEl = d2.$('.root'); if(!rootEl) return; closePanel();\n      const wrap = document.createElement('div'); wrap.innerHTML = renderBrandPanel();\n      const panel = wrap.firstElementChild; rootEl.appendChild(panel);\n      panel.addEventListener('input', (e) => {\n        const t = e.target;\n        if (t.matches('input[type=\"color\"][data-bc]')) { const k=t.dataset.bc; const tx=d2.$('#bf_'+k); if(tx) tx.value=t.value; refreshBrandPreview(); return; }\n        if (t.matches('[data-bk]')) { const k=t.dataset.bk; if(['accent','paper','line','muted','body_color'].indexOf(k)>=0){ const cp=panel.querySelector('input[type=\"color\"][data-bc=\"'+k+'\"]'); if(cp && /^#([0-9a-f]{6})$/i.test(t.value)) cp.value=t.value; } refreshBrandPreview(); return; }\n      });\n      d2.$('#brandSave').onclick = () => saveBrand();\n    }\n    function refreshBrandPreview(){\n      const b = collectBrand(); const stage = d2.$('#bStage');\n      if (stage) { stage.setAttribute('style', brandVarsInline(b)); stage.innerHTML = brandPreviewHtml(b); }\n    }\n    async function saveBrand(){\n      const b = collectBrand();\n      const st = d2.$('#brandState'); if (st) st.textContent = 'saving…';\n      try {\n        const r = await callApi(Object.assign({ action:'brand-save' }, b));\n        if (r && r.ok) { applyBrand(b); if (st) st.textContent='saved ✓'; refreshPreview(); }\n        else if (st) st.textContent='error';\n      } catch(e){ d2.log('brand save err', e); if (st) st.textContent='error'; }\n    }\n\n    // ---------- editor helpers ----------\n    function ensureMeta(){ if(!S.deck.meta) S.deck.meta = {footer:S.brand.footer}; }\n    function markDirty(){ S.dirty=true; S.savedFlag=false; const d=d2.$('#dstate'); if(d) d.textContent='● unsaved'; refreshPreview(); }\n    function refreshPreview(){\n      const sel = S.sel; const p = d2.$('#prev'); if (!p) return;\n      if (sel==null || sel<0 || !S.deck || !S.deck.slides || !S.deck.slides[sel]) { p.innerHTML=''; return; }\n      DeckRenderFoot(S.deck.meta && S.deck.meta.footer, S.brand);\n      p.innerHTML = DeckRenderOne(S.deck.slides[sel], 1, LOGOURL());\n    }\n    function selectSlide(i){ S.sel=i; paint(); }\n    function setF(k,v){ S.deck.slides[S.sel][k]=v; markDirty(); if(k==='title'||k==='subtitle'){ const li=d2.$$('.sl')[S.sel]; if(li){const tt=li.querySelector('.tt'); if(tt) tt.textContent=(S.deck.slides[S.sel].title||S.deck.slides[S.sel].subtitle||'(untitled)');} } }\n    function setStrings(k,v){ S.deck.slides[S.sel][k]=v.split(/\\n\\n+/).map(x=>x.trim()).filter(Boolean); markDirty(); }\n    function setObj(k,idx,sk,v){ if(!S.deck.slides[S.sel][k]) S.deck.slides[S.sel][k]=[]; if(!S.deck.slides[S.sel][k][idx]) S.deck.slides[S.sel][k][idx]={}; S.deck.slides[S.sel][k][idx][sk]=v; markDirty(); }\n    function addObj(k){ const s=S.deck.slides[S.sel]; s[k]=s[k]||[]; s[k].push({}); markDirty(); rerenderForm(); }\n    function delObj(k,idx){ S.deck.slides[S.sel][k].splice(idx,1); markDirty(); rerenderForm(); }\n    function rerenderForm(){ const f=d2.$('#form'); if(f && S.sel>=0) f.innerHTML = renderForm(S.deck.slides[S.sel]); }\n    function addSlide(t){ const tpl=TEMPLATES[t]; if(!tpl) return; const s=JSON.parse(JSON.stringify(tpl)); const at=S.sel>=0?S.sel+1:S.deck.slides.length; S.deck.slides.splice(at,0,s); S.sel=at; markDirty(); paint(); }\n    function dup(i){ S.deck.slides.splice(i+1,0,JSON.parse(JSON.stringify(S.deck.slides[i]))); S.sel=i+1; markDirty(); paint(); }\n    function del(i){ S.deck.slides.splice(i,1); S.sel=Math.min(i,S.deck.slides.length-1); markDirty(); paint(); }\n    function move(i,dir){ const j=i+dir; if(j<0||j>=S.deck.slides.length) return; const a=S.deck.slides; const t=a[i]; a[i]=a[j]; a[j]=t; S.sel=j; markDirty(); paint(); }\n    function applyPhoto(u){ const c=S.pickCtx; if(!c) return; if(c.sk!=null){ if(!S.deck.slides[S.sel][c.k]) S.deck.slides[S.sel][c.k]=[]; if(!S.deck.slides[S.sel][c.k][c.idx]) S.deck.slides[S.sel][c.k][c.idx]={}; S.deck.slides[S.sel][c.k][c.idx][c.sk]=u; } else { S.deck.slides[S.sel][c.k]=u; } S.galOpen=false; markDirty(); paint(); }\n    async function doUpload(){\n      const input = document.createElement('input'); input.type='file'; input.accept='image/*';\n      input.onchange = async () => {\n        const file = input.files && input.files[0]; if(!file) return;\n        try {\n          const out = await d2.drive('deckbuilder-media').uploadFromFiles([file]);\n          const fileUrl = out && out[0] && (out[0].fileUrl || (out[0].params && out[0].params.fileUrl));\n          if(!fileUrl){ await d2.dialog.alert('upload failed'); return; }\n          await callApi({ action:'upload', full: fileUrl, thumb: fileUrl, category:'' });\n          applyPhoto(fileUrl);\n          if (S.photos) S.photos.push({ full: fileUrl, thumb: fileUrl, category:'', ordem:9999, source:'upload' });\n        } catch(e){ d2.log('upload err', e); await d2.dialog.alert('upload failed: '+e); }\n      };\n      input.click();\n    }\n    async function save(){\n      ensureMeta();\n      try {\n        const r = await callApi({ action:'save', slug: S.curDeck, nome: (S.deck.meta && S.deck.meta.title) || S.curDeck, meta_json: JSON.stringify(S.deck.meta||{}), slides_json: JSON.stringify(S.deck.slides||[]), updated_at: new Date().toISOString() });\n        if (r && r.ok) { S.dirty=false; S.savedFlag=true; const d=d2.$('#dstate'); if(d) d.textContent='saved ✓'; }\n        else { await d2.dialog.alert('save error'); }\n      } catch(e){ d2.log('save err', e); await d2.dialog.alert('save error: '+e); }\n    }\n  }\n};\n\n/* ---------------- module-scope helpers ---------------- */\nfunction esc(v){ return String(v==null?'':v).replace(/</g,'&lt;'); }\nfunction escAttr(v){ return String(v==null?'':v).replace(/\"/g,'&quot;'); }\nfunction safeParse(s, fb){ try { return typeof s==='string' ? JSON.parse(s) : (s||fb); } catch(e){ return fb; } }\nfunction normBrand(b){ const o = Object.assign({}, DEFAULT_BRAND, b||{}); if(o.display_scale==null||o.display_scale==='') o.display_scale=1; if(o.body_scale==null||o.body_scale==='') o.body_scale=1; return o; }\n\nfunction renderForm(s){\n  const defs = FIELDS[s.type]||[];\n  let h = `<h2>${esc(s.type)}</h2>`;\n  defs.forEach(([k,kind,sub])=>{ h += `<div class=\"f\"><label>${LBL[k]||k}</label>${fieldHTML(s,k,kind,sub)}</div>`; });\n  return h;\n}\nfunction fieldHTML(s,k,kind,sub){\n  if(kind==='text') return `<input type=\"text\" data-f=\"${k}\" data-kind=\"text\" value=\"${escAttr(s[k])}\">`;\n  if(kind==='textarea') return `<textarea data-f=\"${k}\" data-kind=\"textarea\">${esc(s[k])}</textarea>`;\n  if(kind==='bool') return `<label style=\"text-transform:none;color:#ccc\"><input type=\"checkbox\" data-f=\"${k}\" data-kind=\"bool\" ${s[k]?'checked':''}> yes</label>`;\n  if(kind==='image') return imgFieldHTML(k, s[k], null, null);\n  if(kind==='strings'){ const arr=s[k]||[]; return `<textarea style=\"min-height:120px\" data-f=\"${k}\" data-kind=\"strings\">${esc(arr.join('\\n\\n'))}</textarea>`; }\n  if(kind==='objs'){ const arr=s[k]||[]; let h='<div class=\"objs\">';\n    arr.forEach((o,idx)=>{ h+=`<div class=\"obj\"><div class=\"oh\"><b>${idx+1}</b><button class=\"mini delObjBtn\" data-k=\"${k}\" data-idx=\"${idx}\">remove</button></div>`;\n      sub.forEach(([sk,skind])=>{ h+=`<div class=\"f\"><label>${LBL[sk]||sk}</label>${objField(k,idx,sk,skind,o)}</div>`; }); h+='</div>'; });\n    h+=`<button class=\"addmini addObjBtn\" data-k=\"${k}\">+ add</button></div>`; return h; }\n  return '';\n}\nfunction objField(k,idx,sk,kind,o){\n  if(kind==='textarea') return `<textarea data-obj=\"${k}\" data-idx=\"${idx}\" data-sk=\"${sk}\">${esc(o[sk])}</textarea>`;\n  if(kind==='image') return imgFieldHTML(k, o[sk], idx, sk);\n  return `<input type=\"text\" data-obj=\"${k}\" data-idx=\"${idx}\" data-sk=\"${sk}\" value=\"${escAttr(o[sk])}\">`;\n}\nfunction imgFieldHTML(k, val, idx, sk){\n  const objAttrs = (idx!=null) ? `data-obj=\"${k}\" data-idx=\"${idx}\" data-sk=\"${sk}\"` : `data-f=\"${k}\" data-kind=\"text\"`;\n  const ctxAttrs = (idx!=null) ? `data-k=\"${k}\" data-idx=\"${idx}\" data-sk=\"${sk}\"` : `data-k=\"${k}\"`;\n  return `<div class=\"imgf\">\n    <input type=\"text\" ${objAttrs} value=\"${escAttr(val)}\" placeholder=\"image url\">\n    <img src=\"${escAttr(val)}\" onerror=\"this.style.opacity=.2\">\n    <button class=\"mini galBtn\" style=\"color:var(--gold)\" ${ctxAttrs}>bank</button>\n    <button class=\"mini upBtn\" style=\"color:#9ad\" ${ctxAttrs}>upload</button>\n  </div>`;\n}\n\n/* ---- DeckRender ---- */\nfunction DeckRenderFoot(f, brand){ _N = 0; _FOOT = f || (brand && brand.footer) || 'YOUR BRAND'; if (brand && brand.accent) _ACCENT = brand.accent; }\nfunction DeckRenderOne(s, pageNum, LOGO){ _N = (pageNum||1)-1; return _renderSlide(s, LOGO); }\nfunction _renderSlide(s, LOGO){\n  const eS = v => (v==null?'':String(v));\n  const img = (src,cls)=>src?`<img class=\"${cls||''}\" src=\"${eS(src)}\" onerror=\"this.outerHTML='<div class=\\\\'ph ${cls||''}\\\\'>photo: ${eS(src)}</div>'\">`:`<div class=\"ph ${cls||''}\">no photo</div>`;\n  const foot = ()=>{ _N++; const lg = LOGO?`<img src=\"${LOGO}\">`:''; return `<div class=\"foot\"><div class=\"fl\">${lg}${eS(_FOOT)}</div><div>${String(_N).padStart(2,'0')}</div></div>`; };\n  const kick = (k,num)=>k||num?`<div class=\"kick\">${num?`<span class=\"num\">${eS(num)}</span>`:''}<span class=\"ln\"></span>${eS(k||'')}</div>`:'';\n  const paras = b=>(b||[]).map(p=>`<p>${eS(p)}</p>`).join('');\n  const stats = x=>x.stats||x.kpis||[];\n  const logoImg = (cls)=> LOGO?`<img class=\"${cls||'logo'}\" src=\"${LOGO}\">`:'';\n  const _chartSVG = (s) => {\n    const GOLD=_ACCENT;\n    const enc = v => String(v==null?'':v).replace(/&/g,'&amp;').replace(/</g,'&lt;');\n    const data = (s.data||[]).map(d => ({label:enc(d.label), v:(+d.value||0)}));\n    if(!data.length) return '<div class=\"ph\" style=\"position:relative;height:420px\">chart has no data</div>';\n    const ct = s.chartType||'bar';\n    if(ct==='scatter'){\n      const pts=(s.data||[]).map(d=>({label:enc(d.label),x:(+d.x||0),y:(+d.y||0),r:(+d.r||0)}));\n      if(!pts.length) return '<div class=\"ph\" style=\"position:relative;height:420px\">no data</div>';\n      const W=1000,H=560,pl=96,pr=56,pt=54,pb=72,iw=W-pl-pr,ih=H-pt-pb;\n      const xmax=Math.max(...pts.map(p=>p.x))*1.10;\n      const yv=pts.map(p=>p.y), ymax=Math.min(100,Math.max(...yv)+6), ymin=Math.max(0,Math.min(...yv)-10);\n      const rmax=Math.max(...pts.map(p=>p.r),1);\n      const fx=v=>pl+v/xmax*iw, fy=v=>pt+ih-(v-ymin)/((ymax-ymin)||1)*ih, fr=v=>9+Math.sqrt(v/rmax)*33;\n      const kfmt=v=>Math.abs(v)>=1000?(v/1000).toLocaleString('en-US',{maximumFractionDigits:0})+'k':String(Math.round(v));\n      let out='<svg viewBox=\"0 0 '+W+' '+H+'\" class=\"csvg\" preserveAspectRatio=\"xMidYMid meet\">';\n      for(let g=0;g<=4;g++){const yy=ymin+(ymax-ymin)*g/4,py=fy(yy);out+='<line x1=\"'+pl+'\" y1=\"'+py.toFixed(1)+'\" x2=\"'+(W-pr)+'\" y2=\"'+py.toFixed(1)+'\" class=\"cgrid\"/><text x=\"'+(pl-12)+'\" y=\"'+py.toFixed(1)+'\" class=\"cax\" text-anchor=\"end\" dominant-baseline=\"central\">'+Math.round(yy)+(s.suffix||'%')+'</text>';}\n      for(let g=0;g<=4;g++){const xv=xmax*g/4,px=fx(xv);out+='<text x=\"'+px.toFixed(1)+'\" y=\"'+(H-32)+'\" class=\"cax\" text-anchor=\"middle\">'+(s.prefix||'')+kfmt(xv)+'</text>';}\n      pts.forEach(p=>{const cx=fx(p.x),cy=fy(p.y),rr=fr(p.r);out+='<circle cx=\"'+cx.toFixed(1)+'\" cy=\"'+cy.toFixed(1)+'\" r=\"'+rr.toFixed(1)+'\" fill=\"'+GOLD+'\" fill-opacity=\"0.30\" stroke=\"'+GOLD+'\" stroke-width=\"2\"/><text x=\"'+cx.toFixed(1)+'\" y=\"'+(cy-rr-6).toFixed(1)+'\" text-anchor=\"middle\" style=\"font-size:19px;fill:#eaeaea;font-weight:600\">'+p.label+'</text>';});\n      out+='<text x=\"'+(pl+iw/2).toFixed(1)+'\" y=\"'+(H-8)+'\" text-anchor=\"middle\" style=\"font-size:16px;fill:#9a9a9a\">'+(s.xlabel||'X')+'</text>';\n      out+='<text x=\"24\" y=\"'+(pt+ih/2).toFixed(1)+'\" text-anchor=\"middle\" style=\"font-size:16px;fill:#9a9a9a\" transform=\"rotate(-90 24 '+(pt+ih/2).toFixed(1)+')\">'+(s.ylabel||'Y')+'</text>';\n      return out+'</svg>';\n    }\n    if(ct==='stackbar'){\n      const COLORS=s.colors||['#B8801A', GOLD, '#FFE08A'];\n      const series=s.series||[];\n      const dd=(s.data||[]).map(d=>({label:enc(d.label),parts:(d.parts||[]).map(Number)}));\n      const totals=dd.map(d=>d.parts.reduce((a,b)=>a+b,0));\n      const mx=Math.max(1,...totals);\n      const fmtS=v=>{let n=Number(v),t; if(s.compact&&Math.abs(n)>=1000)t=(n/1000).toLocaleString('en-US',{maximumFractionDigits:0})+'k'; else t=String(Math.round(n)); return (s.prefix||'')+t+(s.suffix||'');};\n      const W=1000,H=560,pl=82,pr=24,pt=88,pb=64,iw=W-pl-pr,ih=H-pt-pb,n=dd.length,slot=iw/n,bw=Math.min(96,slot*0.5);\n      let out='<svg viewBox=\"0 0 '+W+' '+H+'\" class=\"csvg\" preserveAspectRatio=\"xMidYMid meet\">';\n      let lx=pl; series.forEach((nm,i)=>{out+='<rect x=\"'+lx+'\" y=\"18\" width=\"20\" height=\"20\" rx=\"3\" fill=\"'+COLORS[i%COLORS.length]+'\"/><text x=\"'+(lx+27)+'\" y=\"28\" class=\"cax\" text-anchor=\"start\" dominant-baseline=\"central\" style=\"fill:#ccc\">'+enc(nm)+'</text>'; lx+=70+enc(nm).length*11;});\n      for(let g=0;g<=4;g++){const gy=pt+ih-(g/4*ih);out+='<line x1=\"'+pl+'\" y1=\"'+gy.toFixed(1)+'\" x2=\"'+(W-pr)+'\" y2=\"'+gy.toFixed(1)+'\" class=\"cgrid\"/><text x=\"'+(pl-12)+'\" y=\"'+gy.toFixed(1)+'\" class=\"cax\" text-anchor=\"end\" dominant-baseline=\"central\">'+fmtS(mx*g/4)+'</text>';}\n      dd.forEach((d,i)=>{const cx=pl+slot*i+slot/2; let yTop=pt+ih;\n        d.parts.forEach((v,j)=>{const h=v/mx*ih,y=yTop-h; out+='<rect x=\"'+(cx-bw/2).toFixed(1)+'\" y=\"'+y.toFixed(1)+'\" width=\"'+bw.toFixed(1)+'\" height=\"'+h.toFixed(1)+'\" fill=\"'+COLORS[j%COLORS.length]+'\"/>'; yTop=y;});\n        out+='<text x=\"'+cx.toFixed(1)+'\" y=\"'+(yTop-12).toFixed(1)+'\" class=\"cval\" text-anchor=\"middle\">'+fmtS(totals[i])+'</text>';\n        out+='<text x=\"'+cx.toFixed(1)+'\" y=\"'+(H-24)+'\" class=\"cax\" text-anchor=\"middle\">'+d.label+'</text>';});\n      return out+'</svg>';\n    }\n    const max = Math.max(1, ...data.map(d=>d.v));\n    const fmt = v => { let n=Number(v), t;\n      if(s.compact && Math.abs(n)>=1000) t=(n/1000).toLocaleString('en-US',{maximumFractionDigits: Math.abs(n)>=10000?0:1})+'k';\n      else t=Math.round(n).toLocaleString('en-US');\n      return (s.prefix||'')+t+(s.suffix||''); };\n    const W=1000; let out='';\n    if(ct==='hbar'){\n      const n=data.length, rh=Math.max(46, 520/n), H=n*rh, lw=340, x0=lw+18, bw=W-x0-160;\n      out='<svg viewBox=\"0 0 '+W+' '+H.toFixed(0)+'\" class=\"csvg\" preserveAspectRatio=\"xMidYMid meet\">';\n      for(let i=0;i<n;i++){ const d=data[i], cy=i*rh+rh/2, bh=Math.min(34,rh*0.5), by=cy-bh/2, w=Math.max(2, d.v/max*bw);\n        out+='<text x=\"'+lw+'\" y=\"'+cy.toFixed(1)+'\" class=\"clab\" text-anchor=\"end\" dominant-baseline=\"central\">'+d.label+'</text>';\n        out+='<rect x=\"'+x0+'\" y=\"'+by.toFixed(1)+'\" width=\"'+w.toFixed(1)+'\" height=\"'+bh.toFixed(1)+'\" rx=\"3\" fill=\"'+GOLD+'\"/>';\n        out+='<text x=\"'+(x0+w+12).toFixed(1)+'\" y=\"'+cy.toFixed(1)+'\" class=\"cval\" dominant-baseline=\"central\">'+fmt(d.v)+'</text>'; }\n      return out+'</svg>';\n    }\n    if(ct==='line'){\n      const H=520, pl=82, pr=28, pt=28, pb=52, iw=W-pl-pr, ih=H-pt-pb, n=data.length;\n      const xf=i=> pl + (n<=1? iw/2 : i/(n-1)*iw);\n      const yf=v=> pt + ih - (v/max*ih);\n      out='<svg viewBox=\"0 0 '+W+' '+H+'\" class=\"csvg\" preserveAspectRatio=\"xMidYMid meet\">';\n      for(let g=0;g<=4;g++){ const gy=pt+ih-(g/4*ih); out+='<line x1=\"'+pl+'\" y1=\"'+gy.toFixed(1)+'\" x2=\"'+(W-pr)+'\" y2=\"'+gy.toFixed(1)+'\" class=\"cgrid\"/><text x=\"'+(pl-12)+'\" y=\"'+gy.toFixed(1)+'\" class=\"cax\" text-anchor=\"end\" dominant-baseline=\"central\">'+fmt(max*g/4)+'</text>'; }\n      let area='M'+xf(0).toFixed(1)+' '+(pt+ih).toFixed(1)+' ', line='';\n      for(let i=0;i<n;i++){ const x=xf(i).toFixed(1), y=yf(data[i].v).toFixed(1); line+=(i?'L':'M')+x+' '+y+' '; area+='L'+x+' '+y+' '; }\n      area+='L'+xf(n-1).toFixed(1)+' '+(pt+ih).toFixed(1)+' Z';\n      out+='<path d=\"'+area+'\" fill=\"'+GOLD+'\" fill-opacity=\"0.12\"/><path d=\"'+line+'\" fill=\"none\" stroke=\"'+GOLD+'\" stroke-width=\"3\" stroke-linejoin=\"round\"/>';\n      const step=Math.ceil(n/9);\n      for(let i=0;i<n;i++){ out+='<circle cx=\"'+xf(i).toFixed(1)+'\" cy=\"'+yf(data[i].v).toFixed(1)+'\" r=\"3.2\" fill=\"'+GOLD+'\"/>'; if(i%step===0||i===n-1) out+='<text x=\"'+xf(i).toFixed(1)+'\" y=\"'+(H-14)+'\" class=\"cax\" text-anchor=\"middle\">'+data[i].label+'</text>'; }\n      return out+'</svg>';\n    }\n    const H=520, pl=82, pr=24, pt=40, pb=60, iw=W-pl-pr, ih=H-pt-pb, n=data.length, slot=iw/n, bw=Math.min(74, slot*0.62);\n    out='<svg viewBox=\"0 0 '+W+' '+H+'\" class=\"csvg\" preserveAspectRatio=\"xMidYMid meet\">';\n    for(let g=0;g<=4;g++){ const gy=pt+ih-(g/4*ih); out+='<line x1=\"'+pl+'\" y1=\"'+gy.toFixed(1)+'\" x2=\"'+(W-pr)+'\" y2=\"'+gy.toFixed(1)+'\" class=\"cgrid\"/><text x=\"'+(pl-12)+'\" y=\"'+gy.toFixed(1)+'\" class=\"cax\" text-anchor=\"end\" dominant-baseline=\"central\">'+fmt(max*g/4)+'</text>'; }\n    for(let i=0;i<n;i++){ const d=data[i], cx=pl+slot*i+slot/2, h=Math.max(1,d.v/max*ih), y=pt+ih-h;\n      out+='<rect x=\"'+(cx-bw/2).toFixed(1)+'\" y=\"'+y.toFixed(1)+'\" width=\"'+bw.toFixed(1)+'\" height=\"'+h.toFixed(1)+'\" rx=\"3\" fill=\"'+GOLD+'\"/>';\n      out+='<text x=\"'+cx.toFixed(1)+'\" y=\"'+(y-12).toFixed(1)+'\" class=\"cval\" text-anchor=\"middle\">'+fmt(d.v)+'</text>';\n      out+='<text x=\"'+cx.toFixed(1)+'\" y=\"'+(H-22)+'\" class=\"cax\" text-anchor=\"middle\">'+d.label+'</text>'; }\n    return out+'</svg>';\n  };\n\n  const R = {\n    chart:s=>`<div class=\"slide chart\"><div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2><div class=\"chartwrap\">${_chartSVG(s)}</div>${s.note?`<div class=\"note\">${eS(s.note)}</div>`:''}</div>${foot()}</div>`,\n    cover:s=>`<div class=\"slide cover\">${img(s.image,'bg')}<div class=\"scrim\"></div><div class=\"pad\">\n      <div class=\"top\">${s.kicker?kick(s.kicker):'<span></span>'}${s.logo?logoImg('logo'):''}</div>\n      <div class=\"ttl\"><h2 class=\"title\">${eS(s.title)}</h2><div class=\"rule\"></div>${s.subtitle?`<div class=\"sub\">${eS(s.subtitle)}</div>`:''}</div></div></div>`,\n    statement:s=>`<div class=\"slide statement\">${img(s.image,'bg')}<div class=\"scrim\"></div><div class=\"pad\">\n      ${kick(s.kicker)}<h2 class=\"title\" style=\"margin-top:${s.kicker?'22px':'0'}\">${eS(s.title)}</h2>${s.sub?`<div class=\"sub\">${eS(s.sub)}</div>`:''}</div></div>`,\n    section:s=>`<div class=\"slide section\">${img(s.image,'bg')}<div class=\"scrim\"></div>${s.number?`<div class=\"ghost\">${eS(s.number)}</div>`:''}<div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\" style=\"margin-top:20px\">${eS(s.title)}</h2></div>${foot()}</div>`,\n    text:s=>`<div class=\"slide text\"><div class=\"pad\">${s.image?img(s.image,'top-img'):''}${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2><div class=\"body\">${paras(s.body)}</div></div>${foot()}</div>`,\n    split:s=>`<div class=\"slide split ${s.side==='left'?'rev':''}\"><div class=\"half txt\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2><div class=\"body\">${paras(s.body)}</div></div><div class=\"half img\">${img(s.image)}</div>${foot()}</div>`,\n    trio:s=>`<div class=\"slide trio\"><div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2>\n      <div class=\"cols\">${(s.cards||[]).map(c=>`<div class=\"col\">${img(c.image)}<h3>${eS(c.title)}</h3><p>${eS(c.text)}</p></div>`).join('')}</div></div>${foot()}</div>`,\n    gallery:s=>`<div class=\"slide gallery\"><div class=\"pad\">${s.title?`<h2 class=\"title\">${eS(s.title)}</h2>`:''}\n      <div class=\"grid2\">${(s.items||[]).map(it=>img(it.image||it)).join('')}</div></div>${foot()}</div>`,\n    data:s=>`<div class=\"slide data\"><div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2>\n      <div class=\"stats\">${stats(s).map(k=>`<div class=\"stat\"><div class=\"big\">${eS(k.big)}</div><div class=\"lab\">${eS(k.label)}</div>${k.sub?`<div class=\"ksub\">${eS(k.sub)}</div>`:''}</div>`).join('')}</div>\n      ${s.note?`<div class=\"note\">${eS(s.note)}</div>`:''}${s.image?img(s.image,'planta'):''}</div>${foot()}</div>`,\n    bigstat:s=>`<div class=\"slide bigstat\">${img(s.image,'bg')}<div class=\"scrim\"></div><div class=\"pad\">${kick(s.kicker)}<div class=\"big\">${eS(s.big)}</div><div class=\"lab\">${eS(s.label)}</div>${s.sub?`<div class=\"ksub\">${eS(s.sub)}</div>`:''}</div>${foot()}</div>`,\n    amenities:s=>`<div class=\"slide amenities\"><div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2>\n      <div class=\"amg\">${(s.items||[]).map(it=>`<div class=\"am\"><h4>${eS(it.title)}</h4><p>${eS(it.desc)}</p></div>`).join('')}</div></div>${foot()}</div>`,\n    quote:s=>`<div class=\"slide quote\">${img(s.image,'bg')}<div class=\"scrim\"></div><div class=\"pad\"><div class=\"q\">${eS(s.text)}</div>${s.author?`<div class=\"au\">${eS(s.author)}</div>`:''}</div></div>`,\n    list:s=>`<div class=\"slide list\"><div class=\"pad\">${kick(s.kicker,s.number)}<h2 class=\"title\">${eS(s.title)}</h2>\n      ${(s.items||[]).map((it,i)=>`<div class=\"li\"><div class=\"n\">${String(i+1).padStart(2,'0')}</div><div><div class=\"lead\">${eS(it.lead)}</div><div class=\"desc\">${eS(it.desc)}</div></div></div>`).join('')}</div>${foot()}</div>`,\n    image:s=>`<div class=\"slide image\">${img(s.image,'bg')}<div class=\"scrim\" style=\"background:linear-gradient(0deg,#0a0a0af0,#0a0a0a00 45%)\"></div>${s.caption?`<div class=\"pad\" style=\"justify-content:flex-end\"><div class=\"sub\" style=\"font-size:15px;color:#ddd\">${eS(s.caption)}</div></div>`:''}${foot()}</div>`,\n    contact:s=>`<div class=\"slide contact\">${s.image?img(s.image,'bg'):''}${s.image?'<div class=\"scrim\"></div>':''}<div class=\"pad\">\n      <h2 class=\"title\">${eS(s.title||'CONTACT')}</h2><div class=\"crow\">\n      ${s.address?`<div class=\"ci\"><div class=\"cl\">Address</div><div class=\"cv\">${eS(s.address)}</div></div>`:''}\n      ${s.hours?`<div class=\"ci\"><div class=\"cl\">Hours</div><div class=\"cv\">${eS(s.hours)}</div></div>`:''}\n      ${s.email?`<div class=\"ci\"><div class=\"cl\">Contact</div><div class=\"cv\">${eS(s.email)}</div></div>`:''}\n      ${s.site?`<div class=\"ci\"><div class=\"cl\">Site</div><div class=\"cv\">${eS(s.site)}</div></div>`:''}\n      </div></div>${foot()}</div>`,\n    closing:s=>`<div class=\"slide closing\">${img(s.image,'bg')}${s.image?'<div class=\"scrim\"></div>':''}<div class=\"pad\">${s.logo?logoImg('logo'):''}<h2 class=\"title\">${eS(s.title)}</h2>${s.subtitle?`<div class=\"sub\">${eS(s.subtitle)}</div>`:''}</div></div>`\n  };\n  return (R[s.type]||(x=>`<div class=\"slide\"><div class=\"pad\"><h2 class=\"title\">type: ${eS(x.type)}</h2></div></div>`))(s);\n}"}}}}}}}},{"_id":"6a735e2a1c2e00d4ac15c1cf","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"zz-codegate-probe","package":"bigaton-playground","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: `.probe{font:14px system-ui;color:#48faa1;padding:8px}`,\n  render(d2){\n    return `<div class=\"probe\">codegate probe ok</div>`;\n  }\n};"}}}}}}}},{"_id":"6a762ab42789519cae7305b2","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"R4SFGidpf8N3DyFd0wUoi","package":"bigaton-playground","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: `.probe{padding:16px;font:600 20px sans-serif;color:#7c4dff}\n.probe small{display:block;margin-top:6px;font:400 13px sans-serif;color:#8a8a99}`,\n  render(d2){ return `<div class=\"probe\">Hello from a freshly-created code component 🎉<small>edited via patch_user_component — MCP edit check OK</small></div>` }\n}"}}}}}}}},{"_id":"6a762c78accb5b1a195a14f6","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"zz-mcp-capability-probe","package":"bigaton-playground","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    .cap { box-sizing:border-box; width:100%; height:100%; padding:24px;\n           font-family: ui-sans-serif, system-ui, sans-serif; color:#1b1b22;\n           background:#f6f6fb; border-radius:14px; display:flex; flex-direction:column; gap:16px; }\n    .cap h3 { margin:0; font-size:20px; font-weight:700; }\n    .cap .row { display:flex; align-items:center; gap:12px; }\n    .cap button { cursor:pointer; border:none; border-radius:8px; padding:8px 14px;\n                  font-size:14px; font-weight:600; color:#fff; }\n    .cap .count { font-variant-numeric:tabular-nums; font-size:18px; font-weight:700; min-width:32px; }\n    .cap ul { margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:6px; }\n    .cap li { background:#fff; border:1px solid #e3e3ee; border-radius:8px; padding:8px 12px; font-size:13px; }\n    .cap .stamp { font-size:12px; color:#6b6b7b; }\n  `,\n  render(d2) {\n    const heading = d2.param('heading') || 'MCP capability probe';\n    const accent = d2.param('accent') || '#7c4dff';\n    const clicks = d2.state.clicks || 0;\n    const items = ['params → d2.param', 'state → d2.state', 'events → d2.on', 'keyed list morph'];\n    return `\n      <div class=\"cap\">\n        <h3 style=\"color:${accent}\">${heading}</h3>\n        <div class=\"row\">\n          <button class=\"bump\" style=\"background:${accent}\">Bump</button>\n          <span class=\"count\">${clicks}</span>\n          <span class=\"stamp\">clicks tracked on the reactive rail</span>\n        </div>\n        <ul>\n          ${items.map((t, i) => `<li data-key=\"cap-${i}\">${i + 1}. ${t}</li>`).join('')}\n        </ul>\n      </div>\n    `;\n  },\n  mounted(d2) {\n    d2.on('.bump', 'click', () => { d2.state.clicks = (d2.state.clicks || 0) + 1; });\n  }\n}"}},"heading":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"heading"}}}},"accent":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accent"}}}}}}}}}},{"_id":"6a7a28a9c2af0d881104e328","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"inspector","package":"bigaton-playground","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 IC = {\n  pen: 'M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z',\n  pin: 'M9 3h6l-1 6 3 2v2H7v-2l3-2zM12 17v4',\n  heart: 'M20.8 5.6a5 5 0 0 0-7.1 0L12 7.3l-1.7-1.7a5 5 0 1 0-7.1 7.1L12 21.5l8.8-8.8a5 5 0 0 0 0-7.1z',\n  copy: 'M9 9h11v11H9zM5 15H4V4h11v1',\n  tag: 'M20.6 13.4L12 4.8H4v8l8.6 8.6a2 2 0 0 0 2.8 0l5.2-5.2a2 2 0 0 0 0-2.8zM7.5 7.5h.01',\n  search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3',\n  chevD: 'M6 9l6 6 6-6',\n  chevR: 'M9 18l6-6-6-6',\n  updown: 'M7 15l5 5 5-5M7 9l5-5 5 5',\n  alignLeft: 'M3 6h18M3 12h10M3 18h14',\n  alignCenter: 'M3 6h18M7 12h10M5 18h14',\n  alignRight: 'M3 6h18M11 12h10M7 18h14',\n  alignJustify: 'M3 6h18M3 12h18M3 18h18',\n  rows: 'M3 5h18M3 12h18M3 19h18',\n  cols: 'M6 4v16M12 4v16M18 4v16',\n  grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',\n  ref: 'M4 8V4h4M20 8V4h-4M4 16v4h4M20 16v4h-4',\n  arrowLeft: 'M19 12H5M11 18l-6-6 6-6',\n  arrowRight: 'M5 12h14M13 6l6 6-6 6',\n  arrowUp: 'M12 19V5M6 11l6-6 6 6',\n  arrowDown: 'M12 5v14M18 13l-6 6-6-6',\n  arrowsH: 'M2 12h20M6 8l-4 4 4 4M18 8l4 4-4 4',\n  arrowsV: 'M12 2v20M8 6l4-4 4 4M8 18l4 4 4-4',\n  link: 'M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7',\n  drop: 'M12 22a7 7 0 0 0 7-7c0-5-7-13-7-13S5 10 5 15a7 7 0 0 0 7 7z',\n  bolt: 'M13 2L3 14h9l-1 8 10-12h-9z',\n  a11y: 'M12 4.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM4 9l8 1 8-1M12 10v5l-3 6M12 15l3 6',\n  pointer: 'M4 3l7 18 2.5-7L21 11z',\n  wand: 'M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8l1.4 1.4M17.8 6.2l1.4-1.4M3 21l9-9M12.2 6.2l-1.4-1.4',\n  brush: 'M18 3l3 3-9 9-3-3zM6 15l-3 6 6-3',\n  save: 'M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2zM17 21v-8H7v8M7 3v5h8',\n  x: 'M18 6L6 18M6 6l12 12',\n  list: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01',\n  trash: 'M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6',\n  lock: 'M5 11h14v10H5zM8 11V7a4 4 0 1 1 8 0v4',\n  unlock: 'M5 11h14v10H5zM8 11V7a4 4 0 0 1 7.6-1.7',\n  newwin: 'M3 5h18v14H3zM3 9h18M12 12.5v4M10 14.5h4',\n  rotate: 'M12 5V2L8 6l4 4V7a5 5 0 1 1-5 5H4a8 8 0 1 0 8-7z'\n}\n\nconst ico = (n, s) => '<svg class=\"ic\" viewBox=\"0 0 24 24\" width=\"' + (s || 13) + '\" height=\"' + (s || 13) + '\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"' + (IC[n] || '') + '\"/></svg>'\n\nconst INTERACTIONS = ['On Click', 'On Drag Enter', 'On Drag Leave', 'On Drop', 'On Click Outside', 'On Mouse Down', 'On Mouse Up', 'On Mouse Enter', 'On Mouse Leave', 'On Visible On Screen', 'On Invisible On Screen']\n\nconst ROWS_STYLE = [\n  { k: 'group', label: 'Border', dot: true, rows: [\n    { k: 'bordercolor', hex: '#808080', type: 'Solid' },\n    { k: 'wr', w: '0px', r: '0px' }\n  ] },\n  { k: 'toggle', label: 'Children Overflow' },\n  { k: 'toggle', label: 'Hide' },\n  { k: 'prop', label: 'Opacity', type: 'Opacity' },\n  { k: 'group', label: 'Position', dot: true, rows: [\n    { k: 'seg', id: 'pos', options: ['Free Transform', 'Fixed'] },\n    { k: 'anchorxy', x: '230', y: '123' },\n    { k: 'icons', ids: ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify', 'rows', 'grid'] }\n  ] },\n  { k: 'group', label: 'Dimensions', dot: true, rows: [\n    { k: 'icons', ids: ['arrowLeft', 'arrowsH', 'arrowRight', 'arrowUp', 'arrowsV', 'arrowDown'] },\n    { k: 'wh', w: '212', h: '189' },\n    { k: 'toggle2', label: 'Fit children width', sub: 'Width adapts to children' },\n    { k: 'toggle2', label: 'Fit children height', sub: 'Height adapts to children' }\n  ] },\n  { k: 'group', label: 'Fill', dot: true, rows: [\n    { k: 'fill', color: '#0078DC', alpha: '30%' }\n  ] },\n  { k: 'prop', label: 'Backdrop Filter', type: 'List of BitmapFilters' },\n  { k: 'prop', label: 'Filter', type: 'List of BitmapFilters' },\n  { k: 'prop', label: 'Shadow', type: 'List of Shadows' },\n  { k: 'prop', label: 'Scroll', type: 'Scroll' },\n  { k: 'group', label: 'Rotate', dot: false, rows: [ { k: 'rotate' } ] },\n  { k: 'prop', label: 'Transitions', type: 'List of Transitions' },\n  { k: 'prop', label: 'Transform', type: 'List of Transforms' },\n  { k: 'group', label: 'Light Interaction', dot: true, glyph: 'a11y', rows: [\n    { k: 'light' },\n    { k: 'shadowpct' },\n    { k: 'preview' }\n  ] },\n  { k: 'group', label: 'Cursor', dot: false, rows: [ { k: 'cursor', value: 'auto' } ] }\n]\n\nconst ROWS_CONTENT = [\n  { k: 'group', label: 'Text', dot: false, rows: [ { k: 'input', ph: 'Enter text...', stepper: true } ] },\n  { k: 'group', label: 'Text Size', dot: false, rows: [ { k: 'input', ph: 'Inherited', narrow: true, stepper: true, right: true } ] },\n  { k: 'prop', label: 'Font', type: 'GoogleFont' },\n  { k: 'prop', label: 'Text Color', type: 'Color' },\n  { k: 'prop', label: 'Text Format', type: 'TextFormat' },\n  { k: 'prop', label: 'Text Decoration', type: 'TextDecoration' },\n  { k: 'group', label: 'Tooltip', dot: false, rows: [ { k: 'input' } ] },\n  { k: 'group', label: 'Children Mode', dot: false, rows: [ { k: 'cmgrid' } ] },\n  { k: 'prop', label: 'Repeat', type: 'DataSetReference' }\n]\n\nconst ROWS_LOGIC = [\n  { k: 'prop', label: 'State Definitions', type: 'List of StateDefinitions' },\n  { k: 'prop', label: 'On Touch Start', type: 'Interaction' },\n  { k: 'prop', label: 'On Touch End', type: 'Interaction' }\n].concat(INTERACTIONS.map(function (l) { return { k: 'prop', label: l, type: 'Interaction' } })).concat([\n  { k: 'group', label: 'Index', dot: false, rows: [ { k: 'input', narrow: true, stepper: true } ] },\n  { k: 'group', label: 'Tab Index', dot: false, rows: [ { k: 'input', narrow: true, stepper: true } ] },\n  { k: 'toggle', label: 'Accessibility Hide' },\n  { k: 'group', label: 'Accessibility Role', dot: false, rows: [ { k: 'input', value: '[none]' } ] },\n  { k: 'group', label: 'Accessibility Level', dot: false, rows: [ { k: 'input' } ] },\n  { k: 'toggle', label: 'Accessibility Modal', glyph: 'a11y' },\n  { k: 'group', label: 'Accessibility Label', dot: false, rows: [ { k: 'input' } ] },\n  { k: 'toggle', label: 'Disabled' },\n  { k: 'toggle', label: 'Ignore Mouse Events' },\n  { k: 'toggle', label: 'Prevent User Selection' }\n])\n\nconst SECTIONS = { style: ROWS_STYLE, content: ROWS_CONTENT, logic: ROWS_LOGIC }\nconst ALLROWS = ROWS_STYLE.concat(ROWS_CONTENT).concat(ROWS_LOGIC)\n\nconst BLOCKS = [\n  { id: 'a', name: 'HeaderBar', w: 380, h: 150, fill: '#1F4E79' },\n  { id: 'b', name: 'CardList', w: 300, h: 210, fill: '#2A2E33' }\n]\nconst blockById = function (id) {\n  for (let i = 0; i < BLOCKS.length; i++) if (BLOCKS[i].id === id) return BLOCKS[i]\n  return BLOCKS[0]\n}\n\nconst MINW = 220\nconst MAXW = 720\nconst BASEW = 298\n\nconst TABS = [['all', 'ALL'], ['style', 'STYLE'], ['content', 'CONTENT'], ['logic', 'LOGIC'], ['used', 'USED']]\nconst CMODES = [['rows', 'ROWS'], ['cols', 'COLS'], ['grid', 'GRID'], ['rows', 'REV ROWS'], ['cols', 'REV COLS'], ['ref', 'REF']]\n\nexport default {\n  css: `\n  :host { display: block; pointer-events: none; }\n  * { box-sizing: border-box; }\n  .wrap { position: absolute; inset: 0; pointer-events: none; }\n  .grip {\n    position: absolute; top: 0; bottom: 0; width: 9px;\n    cursor: ew-resize; pointer-events: auto; z-index: 3;\n  }\n  .grip::after {\n    content: ''; position: absolute; top: 0; bottom: 0; left: 3px; width: 3px;\n    background: transparent; transition: background .12s;\n  }\n  .grip:hover::after { background: #2E7BF6; }\n  .wrap[data-drag=\"1\"] .grip::after { background: #2E7BF6; }\n\n  .pnl {\n    position: absolute; top: 0; bottom: 0; right: 0; pointer-events: auto;\n    display: flex; flex-direction: column;\n    background: #0E1013; border: 1px solid #20242B; border-radius: 10px; overflow: hidden;\n    font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n    color: #C3C9D2; -webkit-font-smoothing: antialiased;\n  }\n  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n  .ic { flex: 0 0 auto; }\n\n  .hdr { flex: 0 0 44px; display: flex; align-items: center; gap: 10px; padding: 0 14px; border-bottom: 1px solid #1A1D23; }\n  .hdr .ttl { font-size: 12px; font-weight: 600; color: #E8EAED; flex: 1; }\n  .hdr .accent { color: #4C8DFF; display: flex; }\n  .hdr .mut { color: #8A919C; display: flex; }\n  .hdr .hb { width: 22px; height: 22px; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: #8A919C; cursor: pointer; flex: 0 0 auto; }\n  .hdr .hb:hover { background: #191C22; color: #C3C9D2; }\n  .hdr .hb.on { color: #4C8DFF; background: #101A2B; }\n  .nameinp.set { color: #E8EAED; }\n\n  .main { flex: 1 1 auto; display: flex; min-height: 0; }\n  .body { flex: 1 1 auto; min-width: 0; overflow-y: auto; overflow-x: hidden; }\n  .body::-webkit-scrollbar { width: 6px; }\n  .body::-webkit-scrollbar-thumb { background: #2E333B; border-radius: 3px; }\n  .body::-webkit-scrollbar-track { background: transparent; }\n\n  .card { margin: 9px 14px; height: 50px; display: flex; align-items: center; gap: 12px; padding: 0 12px;\n          background: #191C22; border: 1px solid #23272F; border-radius: 10px; }\n  .card .sw24 { width: 24px; height: 24px; border-radius: 5px; background: #2A2E33; display: flex; align-items: center; justify-content: center; }\n  .card .sw24 i { width: 12px; height: 12px; border-radius: 2px; background: #C9CDD3; display: block; }\n  .card .meta { flex: 1; min-width: 0; }\n  .card .nm { font-size: 14px; font-weight: 700; color: #F2F4F7; line-height: 1.2; }\n  .card .pk { font-size: 9px; color: #6E7681; line-height: 1.5; }\n  .iconbtn { width: 26px; height: 26px; border: 1px solid #2A2E36; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #C3C9D2; cursor: pointer; }\n  .iconbtn.sm { width: 20px; height: 20px; border-radius: 5px; color: #8A919C; }\n  .iconbtn:hover { border-color: #3A4049; }\n\n  .namebar { display: flex; align-items: center; gap: 6px; padding: 0 20px; height: 26px; }\n  .namebar .k { flex: 1; font-size: 9px; font-weight: 700; letter-spacing: 1.2px; color: #6B7280; }\n  .nameinp { margin: 0 14px 10px; height: 34px; display: flex; align-items: center; padding: 0 14px;\n             background: #131519; border: 1px solid #22262D; border-radius: 6px; font-size: 10.5px; color: #474D57; }\n\n  .rail {\n    flex: 0 0 30px; display: flex; flex-direction: column; align-items: center;\n    gap: 2px; padding: 8px 0; background: #0B0D10; border-right: 1px solid #1A1D23;\n  }\n  .rail .tab {\n    writing-mode: vertical-rl; transform: rotate(180deg);\n    font-size: 9.5px; font-weight: 700; letter-spacing: .9px; color: #767D89;\n    padding: 12px 3px; border-radius: 4px; cursor: pointer; position: relative;\n  }\n  .rail .tab:hover { color: #C3C9D2; }\n  .rail .tab.on { color: #E8EAED; background: #16191E; }\n  .rail .tab.on::after {\n    content: ''; position: absolute; right: -4px; top: 6px; bottom: 6px;\n    width: 2px; background: #2E7BF6; border-radius: 1px;\n  }\n  .rail .sp { flex: 1; }\n  .rail .mut { color: #767D89; display: flex; cursor: pointer; padding-bottom: 4px; }\n\n  .grp { display: flex; align-items: center; gap: 12px; height: 36px; padding: 0 18px; cursor: pointer; }\n  .grp .dot { width: 7px; height: 7px; border-radius: 50%; background: #2E7BF6; }\n  .grp .cv { color: #8A919C; display: flex; transition: transform .15s; }\n  .grp.col .cv { transform: rotate(-90deg); }\n  .grp .t { font-size: 11px; font-weight: 700; letter-spacing: 1.1px; color: #E8EAED; }\n\n  .sub {\n    display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 18px 0 22px;\n    margin-top: 6px; cursor: pointer; user-select: none;\n  }\n  .sub:hover .t { color: #FFFFFF; }\n  .sub .dot { width: 6px; height: 6px; border-radius: 50%; background: #2E7BF6; flex: 0 0 auto; }\n  .sub .dot.hid { background: transparent; }\n  .sub .t { font-size: 11px; font-weight: 700; color: #D7DBE1; flex: 1; }\n  .sub .gl { color: #D7DBE1; display: flex; }\n  .sub .cv { color: #5C636E; display: flex; transition: transform .15s; }\n  .sub.off .cv { transform: rotate(-90deg); }\n\n  .row { display: flex; align-items: center; gap: 10px; min-height: 36px; padding: 0 18px 0 24px; border-bottom: 1px solid #191C23; }\n  .row .l { flex: 1; font-size: 11.5px; font-weight: 600; color: #C3C9D2; }\n  .row .ty { font-size: 10px; color: #5C636E; }\n  .row .ty.on { color: #4C8DFF; font-weight: 600; }\n  .row .idot { width: 5px; height: 5px; border-radius: 50%; background: #2E7BF6; flex: 0 0 auto; margin-left: -14px; }\n  .row .idot.hid { background: transparent; }\n  .row .pl { font-size: 13px; color: #7A828E; width: 14px; text-align: right; cursor: pointer; }\n  .row .gl { color: #C3C9D2; display: flex; }\n  .row.tall { min-height: 46px; }\n  .row .stack { flex: 1; }\n  .row .stack .s { font-size: 9px; color: #5C636E; font-weight: 400; margin-top: 2px; }\n  .row.noline { border-bottom: none; }\n\n  .sw { width: 30px; height: 16px; border-radius: 8px; background: #2A2E36; position: relative; cursor: pointer; flex: 0 0 auto; transition: background .15s; }\n  .sw i { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #6B7280; transition: left .15s, background .15s; }\n  .sw.on { background: #2E63C8; }\n  .sw.on i { left: 16px; background: #fff; }\n\n  .pad { padding: 0 22px; }\n  .field { background: #16191E; border: 1px solid #23272F; border-radius: 6px; height: 28px; display: flex; align-items: center; gap: 10px; padding: 0 12px; }\n  .field .k { font-size: 10px; font-weight: 700; color: #5C636E; }\n  .field .v { font-size: 11px; font-weight: 700; color: #E8EAED; flex: 1; }\n  .field .u { font-size: 10px; color: #5C636E; }\n\n  .seg { display: flex; gap: 6px; padding: 4px 22px 8px; }\n  .seg b { flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px;\n           border: 1px solid #22262D; background: #141519; font-size: 11px; font-weight: 600; color: #C3C9D2; cursor: pointer; }\n  .seg b.on { border-color: #2E63C8; background: #101A2B; color: #4C8DFF; }\n\n  .axy { display: flex; gap: 10px; padding: 4px 22px 8px; }\n  .anchor { width: 74px; height: 64px; background: #131519; border: 1px solid #22262D; border-radius: 6px; position: relative; flex: 0 0 auto; }\n  .anchor .h { position: absolute; left: 12px; right: 12px; top: 31px; height: 1px; background: #262A31; }\n  .anchor .v { position: absolute; top: 11px; bottom: 11px; left: 36px; width: 1px; background: #262A31; }\n  .anchor .c { position: absolute; left: 30px; top: 25px; width: 14px; height: 14px; border: 1px solid #3A4049; border-radius: 2px; }\n  .axy .col { flex: 1; display: flex; flex-direction: column; gap: 8px; }\n\n  .btnrow { display: flex; gap: 4px; padding: 4px 22px 8px; }\n  .btnrow b { flex: 1; height: 28px; display: flex; align-items: center; justify-content: center;\n              background: #14171C; border: 1px solid #22262D; border-radius: 5px; color: #8A919C; cursor: pointer; }\n  .btnrow b:hover { border-color: #3A4049; color: #C3C9D2; }\n\n  .whrow { display: flex; gap: 6px; padding: 6px 22px; align-items: center; }\n  .whrow .field { flex: 1; height: 30px; }\n  .whrow .lk { width: 26px; height: 30px; background: #14171C; border: 1px solid #22262D; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: #8A919C; cursor: pointer; }\n\n  .fillbar { display: flex; align-items: center; gap: 10px; padding: 6px 22px; }\n  .fillbar .bar { flex: 1; height: 20px; border-radius: 4px; }\n  .fillbar .n { font-size: 8.5px; color: #5C636E; }\n  .kvrow { display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 22px; }\n  .kvrow .k { font-size: 8px; font-weight: 700; letter-spacing: .8px; color: #5C636E; }\n  .kvrow .v { font-size: 10.5px; color: #C3C9D2; flex: 1; }\n  .kvrow .cv { color: #5C636E; display: flex; }\n  .swrow { display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 22px; }\n  .swrow .chip { width: 18px; height: 18px; border-radius: 4px; }\n  .swrow .hx { font-size: 11px; font-weight: 700; color: #E8EAED; flex: 1; }\n  .swrow .a { font-size: 10px; color: #8A919C; }\n  .caps { font-size: 8.5px; font-weight: 700; letter-spacing: 1px; color: #5C636E; padding: 7px 22px 0; }\n\n  .rot { display: flex; align-items: center; gap: 8px; padding: 6px 22px 10px; }\n  .rot .knob { width: 34px; height: 34px; border-radius: 50%; background: #131519; border: 1px solid #22262D; display: flex; align-items: center; justify-content: center; color: #4C8DFF; }\n  .rot .field { flex: 1; justify-content: center; }\n  .rot .deg { font-size: 9px; color: #5C636E; }\n  .rot .p { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 4px;\n            background: #14171C; border: 1px solid #22262D; font-size: 7.5px; font-weight: 700; color: #8A919C; cursor: pointer; }\n  .rot .p.on { background: #101A2B; border-color: #2E63C8; color: #4C8DFF; }\n\n  .lcards { display: flex; gap: 6px; padding: 4px 22px 8px; }\n  .lcards b { flex: 1; height: 52px; border-radius: 8px; border: 1px solid #22262D; background: #14171C;\n              display: flex; gap: 6px; padding: 12px 10px; cursor: pointer; }\n  .lcards b.on { border-color: #2E63C8; background: #101A2B; }\n  .lcards .gl { color: #8A919C; display: flex; padding-top: 8px; }\n  .lcards b.on .gl { color: #4C8DFF; }\n  .lcards .t { font-size: 10.5px; font-weight: 700; color: #E8EAED; }\n  .lcards .s { font-size: 8px; color: #6E7681; line-height: 1.3; margin-top: 2px; }\n\n  .pvbox { margin: 4px 22px 8px; height: 68px; background: #131519; border: 1px solid #1E2229; border-radius: 6px; position: relative; }\n  .pvbox .cap { position: absolute; left: 10px; top: 8px; font-size: 7.5px; font-weight: 700; letter-spacing: .8px; color: #5C636E; }\n  .pvbox .sq { position: absolute; left: 50%; top: 26px; margin-left: -14px; width: 28px; height: 28px; background: #1A1D23; border: 1px solid #262A31; border-radius: 4px; }\n\n  .lbl { font-size: 10.5px; font-weight: 600; color: #C3C9D2; padding: 10px 24px 6px; }\n  .lbl.strong { font-size: 11px; font-weight: 700; color: #D7DBE1; }\n  .inp { margin: 0 22px 8px; background: #131519; border: 1px solid #22262D; border-radius: 6px; height: 32px;\n         display: flex; align-items: center; gap: 8px; padding: 0 12px; font-size: 10.5px; }\n  .inp.narrow { width: 112px; }\n  .inp .ph { color: #474D57; flex: 1; }\n  .inp .val { color: #C3C9D2; flex: 1; }\n  .inp .st { color: #5C636E; display: flex; }\n  .inp .gl { color: #8A919C; display: flex; }\n  .inp .mv { color: #E8EAED; font-weight: 700; flex: 1; }\n\n  .cmgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; padding: 2px 22px 8px; }\n  .cmgrid b { height: 40px; border-radius: 6px; border: 1px solid #22262D; background: #14171C;\n              display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; color: #5C636E; cursor: pointer; }\n  .cmgrid b.on { border-color: #2E63C8; background: #101A2B; color: #4C8DFF; }\n  .cmgrid .t { font-size: 7.5px; font-weight: 700; letter-spacing: .6px; }\n\n  .abar { flex: 0 0 44px; display: flex; align-items: center; gap: 8px; padding: 0 14px; border-top: 1px solid #1A1D23; }\n  .abar .sp { flex: 1; }\n  .abar .b { width: 30px; height: 28px; border-radius: 6px; background: #141519; border: 1px solid #20242B;\n             display: flex; align-items: center; justify-content: center; color: #8A919C; cursor: pointer; }\n  .abar .b.hi { width: 34px; background: #191C22; border-color: #23272F; color: #E8EAED; }\n\n  .foot { flex: 0 0 56px; display: flex; align-items: center; gap: 18px; padding: 0 20px; background: #0A0C0F; border-top: 1px solid #1A1D23; color: #8A919C; }\n  .foot .g { display: flex; gap: 18px; }\n  .foot .sp { flex: 1; }\n  .foot .div { width: 1px; height: 20px; background: #23272F; }\n  .foot svg { cursor: pointer; }\n  .tail { height: 24px; }\n  `,\n\n  render(d2) {\n    const st = d2.state\n    const win2 = st.win2 == null ? d2.shared.get('ui:win2', false) : st.win2\n    const sel = st.sel || d2.shared.get('ui:sel', 'a')\n    const cfgAll = st.cfg || d2.shared.get('ui:cfg', {}) || {}\n    const wMain = Number(d2.shared.get('ui:inspectorW', BASEW)) || BASEW\n\n    const panel = function (win) {\n    const isLeft = win === 'L'\n    const lock = st['lock' + win] || null\n    const focus = lock || sel\n    const blk = blockById(focus)\n    const c = cfgAll[focus] || {}\n\n    const tab = st['tab' + win] == null ? 'all' : st['tab' + win]\n    const col = !!st['col' + win]\n    const tg = c.tg || {}\n    const gc = st['gc' + win] || {}\n    const graph = c.graph || {}\n    const seg = c.seg || {}\n    const cm = c.cm == null ? 0 : c.cm\n    const light = c.light == null ? 0 : c.light\n    const rot = c.rot == null ? 0 : c.rot\n    const w = isLeft ? BASEW : wMain\n    const isOn = function (k) { return !!tg[k] }\n\n    const rowProp = function (r) {\n      const g = graph[r.label]\n      const set = !!(g && (g.module || g.fn))\n      const ty = set ? ((g.module || '') + (g.fn ? '.' + g.fn + '()' : '')) : r.type\n      return '<div class=\"row\">' +\n        '<span class=\"idot' + (set ? '' : ' hid') + '\"></span>' +\n        (r.glyph ? '<span class=\"gl\">' + ico(r.glyph, 12) + '</span>' : '') +\n        '<span class=\"l\">' + r.label + '</span>' +\n        '<span class=\"ty' + (set ? ' on' : '') + '\">' + ty + '</span>' +\n        '<span class=\"pl\">+</span>' +\n      '</div>'\n    }\n\n    const rowToggle = function (r) {\n      const on = isOn(r.label)\n      return '<div class=\"row' + (r.sub ? ' tall' : '') + '\">' +\n        (r.glyph ? '<span class=\"gl\">' + ico(r.glyph, 12) + '</span>' : '') +\n        '<span class=\"stack\"><span class=\"l\">' + r.label + '</span>' +\n        (r.sub ? '<div class=\"s\">' + r.sub + '</div>' : '') + '</span>' +\n        '<span class=\"sw' + (on ? ' on' : '') + '\" data-tg=\"' + r.label + '\" data-win=\"' + win + '\"><i></i></span>' +\n      '</div>'\n    }\n\n    const renderRow = function (r) {\n      if (r.k === 'sub') {\n        return '<div class=\"sub\"><span class=\"dot\"></span>' +\n          (r.glyph ? '<span class=\"gl\">' + ico(r.glyph, 11) + '</span>' : '') +\n          '<span class=\"t\">' + r.label + '</span></div>'\n      }\n      if (r.k === 'prop') return rowProp(r)\n      if (r.k === 'toggle' || r.k === 'toggle2') return rowToggle(r)\n      if (r.k === 'lbl') return '<div class=\"lbl' + (r.strong ? ' strong' : '') + '\">' + r.label + '</div>'\n      if (r.k === 'bordercolor') {\n        return '<div class=\"swrow\">' +\n          '<span class=\"chip\" style=\"background:#3A3F47;border:1px solid #4A505A\"></span>' +\n          '<span class=\"hx mono\" style=\"font-weight:600;color:#C3C9D2;font-size:10.5px\">' + r.hex + '</span>' +\n          '<span class=\"k\" style=\"font-size:8px;font-weight:700;letter-spacing:.8px;color:#5C636E\">TYPE</span>' +\n          '<span style=\"width:18px;height:1px;background:#5C636E\"></span>' +\n          '<span style=\"font-size:10.5px;color:#C3C9D2\">' + r.type + '</span>' +\n          '<span class=\"cv\" style=\"color:#5C636E;display:flex\">' + ico('chevR', 11) + '</span>' +\n        '</div>'\n      }\n      if (r.k === 'wr') {\n        return '<div class=\"swrow\" style=\"border-bottom:1px solid #191C23\">' +\n          '<span class=\"k\" style=\"font-size:8px;font-weight:700;letter-spacing:.8px;color:#5C636E\">WIDTH</span>' +\n          '<span class=\"mono\" style=\"font-size:11px;font-weight:700;color:#E8EAED\">' + r.w + '</span>' +\n          '<span style=\"flex:1\"></span>' +\n          '<span class=\"k\" style=\"font-size:8px;font-weight:700;letter-spacing:.8px;color:#5C636E\">RADIUS</span>' +\n          '<span class=\"mono\" style=\"font-size:11px;font-weight:700;color:#E8EAED\">' + r.r + '</span>' +\n          '<span style=\"color:#5C636E;display:flex;margin-left:8px\">' + ico('updown', 11) + '</span>' +\n        '</div>'\n      }\n      if (r.k === 'seg') {\n        const a = seg[r.id] == null ? 0 : seg[r.id]\n        return '<div class=\"seg\">' + r.options.map(function (o, i) {\n          return '<b class=\"' + (i === a ? 'on' : '') + '\" data-seg=\"' + r.id + '\" data-i=\"' + i + '\" data-win=\"' + win + '\">' + o + '</b>'\n        }).join('') + '</div>'\n      }\n      if (r.k === 'anchorxy') {\n        const f = function (k, v) {\n          return '<div class=\"field\"><span class=\"k mono\">' + k + '</span><span class=\"v mono\">' + v + '</span><span class=\"u mono\">px</span></div>'\n        }\n        return '<div class=\"axy\"><div class=\"anchor\"><div class=\"h\"></div><div class=\"v\"></div><div class=\"c\"></div></div>' +\n          '<div class=\"col\">' + f('X', r.x) + f('Y', r.y) + '</div></div>'\n      }\n      if (r.k === 'icons') {\n        return '<div class=\"btnrow\">' + r.ids.map(function (i) { return '<b>' + ico(i, 12) + '</b>' }).join('') + '</div>'\n      }\n      if (r.k === 'wh') {\n        return '<div class=\"whrow\">' +\n          '<div class=\"field\"><span class=\"k mono\">W</span><span class=\"v mono\">' + (blk.w || r.w) + '</span></div>' +\n          '<div class=\"lk\">' + ico('link', 11) + '</div>' +\n          '<div class=\"field\"><span class=\"k mono\">H</span><span class=\"v mono\">' + (blk.h || r.h) + '</span></div>' +\n        '</div>'\n      }\n      if (r.k === 'fill') {\n        return '<div class=\"fillbar\"><span class=\"bar\" style=\"background:' + (blk.fill || '#1F4E79') + '\"></span><span class=\"n\">1 background</span></div>' +\n          '<div class=\"kvrow\"><span class=\"k\">TYPE</span><span class=\"v\">Solid Color</span><span class=\"cv\">' + ico('chevR', 11) + '</span></div>' +\n          '<div class=\"swrow\"><span class=\"chip\" style=\"background:' + r.color + '\"></span>' +\n          '<span class=\"hx mono\">' + r.color.replace('#', '') + '</span><span class=\"a\">' + r.alpha + '</span></div>' +\n          '<div class=\"caps\">BACKGROUNDS</div>' +\n          '<div class=\"kvrow\" style=\"border-bottom:1px solid #191C23\"><span class=\"v\">1 Fill</span><span class=\"cv\">' + ico('chevR', 11) + '</span></div>'\n      }\n      if (r.k === 'rotate') {\n        return '<div class=\"rot\"><div class=\"knob\">' + ico('rotate', 13) + '</div>' +\n          '<div class=\"field\"><span class=\"v mono\" style=\"text-align:center\">' + rot + '</span></div>' +\n          '<span class=\"deg\">deg</span>' +\n          [0, 90, 180, 270].map(function (v) {\n            return '<span class=\"p' + (rot === v ? ' on' : '') + '\" data-rot=\"' + v + '\" data-win=\"' + win + '\">' + v + '</span>'\n          }).join('') +\n        '</div>'\n      }\n      if (r.k === 'light') {\n        const cards = [['drop', 'Cast shadow', 'Outer shadow per light'], ['bolt', 'Catch light', 'Inner highlight from lights']]\n        return '<div class=\"lcards\">' + cards.map(function (c, i) {\n          return '<b class=\"' + (i === light ? 'on' : '') + '\" data-light=\"' + i + '\" data-win=\"' + win + '\"><span class=\"gl\">' + ico(c[0], 12) + '</span>' +\n            '<span><span class=\"t\">' + c[1] + '</span><div class=\"s\">' + c[2] + '</div></span></b>'\n        }).join('') + '</div>'\n      }\n      if (r.k === 'shadowpct') {\n        return '<div class=\"kvrow\"><span class=\"k\">SHADOW</span><span class=\"v\" style=\"font-weight:700;color:#E8EAED;font-size:10px;flex:0\">100%</span></div>'\n      }\n      if (r.k === 'preview') {\n        return '<div class=\"pvbox\"><span class=\"cap\">PREVIEW</span><span class=\"sq\"></span></div>'\n      }\n      if (r.k === 'input') {\n        return '<div class=\"inp' + (r.narrow ? ' narrow' : '') + '\">' +\n          (r.value ? '<span class=\"val\">' + r.value + '</span>'\n            : '<span class=\"ph\"' + (r.right ? ' style=\"text-align:right\"' : '') + '>' + (r.ph || '') + '</span>') +\n          (r.stepper ? '<span class=\"st\">' + ico('updown', 11) + '</span>' : '') +\n        '</div>'\n      }\n      if (r.k === 'cursor') {\n        return '<div class=\"inp\"><span class=\"gl\">' + ico('pointer', 11) + '</span>' +\n          '<span class=\"mv mono\">' + r.value + '</span><span class=\"st\">' + ico('chevD', 10) + '</span></div>'\n      }\n      if (r.k === 'cmgrid') {\n        return '<div class=\"cmgrid\">' + CMODES.map(function (c, i) {\n          return '<b class=\"' + (i === cm ? 'on' : '') + '\" data-cm=\"' + i + '\" data-win=\"' + win + '\">' + ico(c[0], 13) + '<span class=\"t\">' + c[1] + '</span></b>'\n        }).join('') + '</div>'\n      }\n      return ''\n    }\n\n    const renderItem = function (it) {\n      if (it.k === 'group') {\n        const off = gc[it.label] == null ? true : !!gc[it.label]\n        return '<div class=\"sub' + (off ? ' off' : '') + '\" data-grp=\"' + it.label + '\" data-win=\"' + win + '\">' +\n          '<span class=\"dot' + (it.dot === false ? ' hid' : '') + '\"></span>' +\n          (it.glyph ? '<span class=\"gl\">' + ico(it.glyph, 11) + '</span>' : '') +\n          '<span class=\"t\">' + it.label + '</span>' +\n          '<span class=\"cv\">' + ico('chevD', 10) + '</span>' +\n        '</div>' + (off ? '' : it.rows.map(renderRow).join(''))\n      }\n      return renderRow(it)\n    }\n\n    const list = tab === 'all' ? ALLROWS\n      : tab === 'used' ? ALLROWS.filter(function (i) { return i.k === 'group' && i.dot !== false })\n      : (SECTIONS[tab] || [])\n    const body = list.map(renderItem).join('')\n\n    return '<div class=\"pnl' + (isLeft ? ' left' : '') + '\" style=\"width:' + w + 'px' + (isLeft ? ';right:' + wMain + 'px' : '') + '\">' +\n      '<div class=\"hdr\"><span class=\"accent\">' + ico('pen', 15) + '</span>' +\n        '<span class=\"ttl\">Inspector</span>' +\n        '<span class=\"hb' + (lock ? ' on' : '') + '\" data-lock=\"' + win + '\">' + ico(lock ? 'lock' : 'unlock', 13) + '</span>' +\n        (isLeft\n          ? '<span class=\"hb\" data-close=\"1\">' + ico('x', 13) + '</span>'\n          : '<span class=\"hb\" data-newwin=\"1\">' + ico('newwin', 13) + '</span>') +\n      '</div>' +\n      '<div class=\"ident\">' +\n        '<div class=\"card\"><span class=\"sw24\"><i></i></span>' +\n          '<span class=\"meta\"><div class=\"nm\">Block</div><div class=\"pk mono\">data2-core-primitives</div></span>' +\n          '<span class=\"iconbtn\">' + ico('heart', 12) + '</span><span class=\"iconbtn\">' + ico('copy', 12) + '</span></div>' +\n        '<div class=\"namebar\"><span class=\"k\">NAME</span>' +\n          '<span class=\"iconbtn sm\">' + ico('tag', 10) + '</span><span class=\"iconbtn sm\">' + ico('copy', 10) + '</span></div>' +\n        '<div class=\"nameinp mono set\">' + blk.name + '</div>' +\n      '</div>' +\n      '<div class=\"main\">' +\n        '<div class=\"rail\">' + TABS.map(function (t) {\n          return '<span class=\"tab' + (t[0] === tab ? ' on' : '') + '\" data-tab=\"' + t[0] + '\" data-win=\"' + win + '\">' + t[1] + '</span>'\n        }).join('') + '<span class=\"sp\"></span><span class=\"mut\">' + ico('search', 13) + '</span></div>' +\n        '<div class=\"body\">' +\n          '<div class=\"grp' + (col ? ' col' : '') + '\" data-col=\"1\" data-win=\"' + win + '\"><span class=\"dot\"></span><span class=\"cv\">' + ico('chevD', 10) + '</span><span class=\"t\">BLOCK</span></div>' +\n          (col ? '' : body + '<div class=\"tail\"></div>') +\n        '</div>' +\n      '</div>' +\n      '<div class=\"abar\"><span class=\"b hi\">' + ico('wand', 12) + '</span><span class=\"sp\"></span>' +\n        '<span class=\"b\">' + ico('brush', 12) + '</span><span class=\"b\">' + ico('link', 12) + '</span></div>' +\n      '<div class=\"foot\"><span class=\"g\">' + ico('pen', 13) + ico('save', 13) + ico('x', 13) + ico('list', 13) + '</span>' +\n        '<span class=\"div\"></span><span class=\"sp\"></span>' +\n        '<span class=\"g\">' + ico('trash', 13) + ico('lock', 13) + ico('copy', 13) + '</span></div>' +\n    '</div>' + (isLeft ? '' : '<div class=\"grip\" style=\"right:' + (w - 4) + 'px\"></div>')\n    }\n\n    return '<div class=\"wrap\">' + panel('R') + (win2 ? panel('L') : '') + '</div>'\n  },\n\n  mounted(d2) {\n    const rn = d2.root && d2.root.getRootNode ? d2.root.getRootNode() : null\n    const sh = rn && rn.host\n    if (sh) {\n      sh.style.pointerEvents = 'none'\n      const pb = sh.closest && sh.closest('.data2-player-block')\n      if (pb) pb.style.pointerEvents = 'none'\n    }\n\n    const focusOf = function (win) {\n      return d2.state['lock' + win] || d2.state.sel || d2.shared.get('ui:sel', 'a')\n    }\n    const winOf = function (el) { return (el && el.dataset.win) || 'R' }\n\n    const patchCfg = function (win, fn) {\n      const all = JSON.parse(JSON.stringify(d2.state.cfg || d2.shared.get('ui:cfg', {}) || {}))\n      const f = focusOf(win)\n      if (!all[f]) all[f] = {}\n      fn(all[f])\n      d2.setState({ cfg: all })\n      d2.shared.set('ui:cfg', all)\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:cfg', all)\n    }\n\n    d2.on('[data-tab]', 'click', function (e) {\n      const el = e.target.closest('[data-tab]')\n      const o = {}\n      o['tab' + winOf(el)] = el.dataset.tab\n      d2.setState(o)\n    })\n\n    d2.on('[data-col]', 'click', function (e) {\n      const win = winOf(e.target.closest('[data-col]'))\n      const o = {}\n      o['col' + win] = !d2.state['col' + win]\n      d2.setState(o)\n    })\n\n    d2.on('[data-grp]', 'click', function (e) {\n      const el = e.target.closest('[data-grp]')\n      const key = 'gc' + winOf(el)\n      const next = Object.assign({}, d2.state[key] || {})\n      const g = el.dataset.grp\n      next[g] = !(g in next ? next[g] : true)\n      const o = {}\n      o[key] = next\n      d2.setState(o)\n    })\n\n    d2.on('[data-tg]', 'click', function (e) {\n      const el = e.target.closest('[data-tg]')\n      patchCfg(winOf(el), function (c) {\n        c.tg = Object.assign({}, c.tg || {})\n        c.tg[el.dataset.tg] = !c.tg[el.dataset.tg]\n      })\n    })\n\n    d2.on('[data-seg]', 'click', function (e) {\n      const el = e.target.closest('[data-seg]')\n      patchCfg(winOf(el), function (c) {\n        c.seg = Object.assign({}, c.seg || {})\n        c.seg[el.dataset.seg] = Number(el.dataset.i)\n      })\n    })\n\n    d2.on('[data-cm]', 'click', function (e) {\n      const el = e.target.closest('[data-cm]')\n      patchCfg(winOf(el), function (c) { c.cm = Number(el.dataset.cm) })\n    })\n\n    d2.on('[data-light]', 'click', function (e) {\n      const el = e.target.closest('[data-light]')\n      patchCfg(winOf(el), function (c) { c.light = Number(el.dataset.light) })\n    })\n\n    d2.on('[data-rot]', 'click', function (e) {\n      const el = e.target.closest('[data-rot]')\n      patchCfg(winOf(el), function (c) { c.rot = Number(el.dataset.rot) })\n    })\n\n    d2.on('[data-lock]', 'click', function (e) {\n      const key = 'lock' + e.target.closest('[data-lock]').dataset.lock\n      const o = {}\n      o[key] = d2.state[key] ? null : (d2.state.sel || d2.shared.get('ui:sel', 'a'))\n      d2.setState(o)\n    })\n\n    d2.on('[data-newwin]', 'click', function () {\n      d2.setState({ win2: true })\n      d2.shared.set('ui:win2', true)\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:win2', true)\n    })\n\n    d2.on('[data-close]', 'click', function () {\n      d2.setState({ win2: false })\n      d2.shared.set('ui:win2', false)\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:win2', false)\n    })\n\n    if (d2.bus && d2.bus.on) {\n      d2.bus.on('ui:cfg', function (g) { d2.setState({ cfg: g }) })\n      d2.bus.on('ui:sel', function (s) { d2.setState({ sel: s }) })\n    }\n\n    let drag = null\n\n    d2.on('.grip', 'pointerdown', function (e) {\n      const pnl = d2.$('.pnl')\n      if (!pnl) return\n      e.preventDefault()\n      drag = { x: e.clientX, w: pnl.getBoundingClientRect().width }\n      const wrap = d2.$('.wrap')\n      if (wrap) wrap.setAttribute('data-drag', '1')\n      document.body.style.userSelect = 'none'\n    })\n\n    d2.on(window, 'pointermove', function (e) {\n      if (!drag) return\n      const pnl = d2.$('.pnl')\n      const grip = d2.$('.grip')\n      if (!pnl) return\n      let w = drag.w + (drag.x - e.clientX)\n      w = Math.max(MINW, Math.min(w, MAXW))\n      pnl.style.width = w + 'px'\n      if (grip) grip.style.right = (w - 4) + 'px'\n      const lp = d2.$('.pnl.left')\n      if (lp) lp.style.right = w + 'px'\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:inspectorW', w)\n    })\n\n    d2.on(window, 'pointerup', function () {\n      if (!drag) return\n      const pnl = d2.$('.pnl')\n      drag = null\n      const wrap = d2.$('.wrap')\n      if (wrap) wrap.removeAttribute('data-drag')\n      document.body.style.userSelect = ''\n      if (pnl) d2.shared.set('ui:inspectorW', Math.round(pnl.getBoundingClientRect().width))\n    })\n\n    d2.onUnmount(function () { document.body.style.userSelect = '' })\n  }\n}"}}}}}}}},{"_id":"6a7a2f47e511c3482dcfcf80","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"bottomdock","package":"bigaton-playground","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 INTERACTIONS = [\n  'On Touch Start', 'On Touch End', 'On Click', 'On Click Outside',\n  'On Drag Enter', 'On Drag Leave', 'On Drop',\n  'On Mouse Down', 'On Mouse Up', 'On Mouse Enter', 'On Mouse Leave',\n  'On Visible On Screen', 'On Invisible On Screen'\n]\n\nconst MODULES = [\n  '@MainRouter', '@AppAuth', '@UsersDataSet', '@FormStore',\n  '@__UIState', '@ChatAssistant', '@NotifyRush', '@MediaDrive', '@__CursorController'\n]\n\nconst FNLIST = ['Series', 'If', 'Add', 'Multiply', 'ArrayMap', 'ArrayFilter', 'ArrayJoin']\nconst FNS = {\n  Series: { slots: 'n', add: 'step' },\n  If: { slots: ['then', 'else'] },\n  ArrayMap: { slots: ['each'] },\n  ArrayFilter: { slots: ['where'] },\n  ArrayJoin: { slots: [] },\n  Add: { slots: [] },\n  Multiply: { slots: [] }\n}\n\nconst BLOCKS = [\n  { id: 'a', name: 'HeaderBar' },\n  { id: 'b', name: 'CardList' }\n]\nconst blockById = function (id) {\n  for (let i = 0; i < BLOCKS.length; i++) if (BLOCKS[i].id === id) return BLOCKS[i]\n  return BLOCKS[0]\n}\n\nconst NW = 240\nconst HGAP = 72\nconst ROW = 30\nconst HDR = 30\nconst PADV = 8\nconst VGAP = 18\nconst PAD = 26\n\nconst blank = function () { return { module: null, fn: null, col: false, kids: [] } }\n\nconst slotsOf = function (n) {\n  if (!n.fn) return []\n  const d = FNS[n.fn]\n  if (!d) return []\n  if (d.slots === 'n') return n.kids.map(function (_, i) { return 'step ' + (i + 1) })\n  return d.slots\n}\n\nconst rowsOf = function (n) {\n  if (!n.module) return 1\n  return 2 + slotsOf(n).length + (n.fn === 'Series' ? 1 : 0)\n}\n\nconst nodeH = function (n) {\n  if (n.col) return HDR\n  return HDR + PADV * 2 + rowsOf(n) * ROW\n}\n\nconst measure = function (n) {\n  const h = nodeH(n)\n  if (n.col || !n.kids.length) return { h: h, sub: h }\n  let sub = 0\n  n.kids.forEach(function (k) { sub += measure(k).sub + VGAP })\n  sub -= VGAP\n  return { h: h, sub: Math.max(h, sub) }\n}\n\nconst place = function (n, depth, top, path, label, out) {\n  const m = measure(n)\n  const h = nodeH(n)\n  out.push({\n    n: n, path: path, label: label, depth: depth,\n    x: PAD + depth * (NW + HGAP),\n    y: top + (m.sub - h) / 2,\n    h: h\n  })\n  if (!n.col) {\n    const slots = slotsOf(n)\n    let cy = top\n    n.kids.forEach(function (k, i) {\n      place(k, depth + 1, cy, path + '.' + i, slots[i] || ('slot ' + (i + 1)), out)\n      cy += measure(k).sub + VGAP\n    })\n  }\n  return m\n}\n\nconst getNode = function (root, path) {\n  const parts = path.split('.').slice(1)\n  let n = root\n  for (let i = 0; i < parts.length; i++) n = n.kids[Number(parts[i])]\n  return n\n}\n\nconst clone = function (o) { return JSON.parse(JSON.stringify(o)) }\n\nconst portY = function (rec, i) {\n  if (rec.n.col) return HDR / 2\n  return HDR + PADV + (2 + i) * ROW + ROW / 2\n}\n\nconst wire = function (x1, y1, x2, y2) {\n  const dx = Math.max(34, (x2 - x1) / 2)\n  return 'M' + x1 + ',' + y1 + ' C' + (x1 + dx) + ',' + y1 + ' ' + (x2 - dx) + ',' + y2 + ' ' + x2 + ',' + y2\n}\n\nconst esc = function (s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\"/g, '&quot;') }\n\nexport default {\n  css: `\n  :host { display: block; pointer-events: none; }\n  * { box-sizing: border-box; }\n\n  .wrap { position: absolute; inset: 0; pointer-events: none; }\n\n  .dock {\n    position: absolute; left: 0; right: 0; bottom: 0;\n    background: #0E1013; border: 1px solid #20242B; border-radius: 10px;\n    pointer-events: auto; overflow: hidden;\n    display: flex; flex-direction: column;\n    font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n    color: #C3C9D2; -webkit-font-smoothing: antialiased;\n  }\n  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n\n  .grip { position: absolute; left: 0; height: 12px; cursor: ns-resize; pointer-events: auto; z-index: 6; }\n  .grip::after { content: ''; position: absolute; left: 0; right: 0; top: 4px; height: 3px; background: transparent; transition: background .12s; }\n  .grip:hover::after { background: #2E7BF6; }\n  .wrap[data-drag=\"1\"] .grip::after { background: #2E7BF6; }\n\n  .dhdr { flex: 0 0 36px; display: flex; align-items: center; gap: 9px; padding: 0 14px; border-bottom: 1px solid #1A1D23; }\n  .dhdr .dot { width: 7px; height: 7px; border-radius: 50%; background: #2E7BF6; }\n  .dhdr .t { font-size: 11.5px; font-weight: 700; letter-spacing: .4px; color: #E8EAED; }\n  .dhdr .sub { font-size: 10px; color: #5C636E; }\n  .dhdr .bname { font-size: 10.5px; font-weight: 700; color: #4C8DFF; background: #101A2B;\n                 border: 1px solid #2E63C8; border-radius: 4px; padding: 2px 8px; }\n\n  .dbody { flex: 1 1 auto; display: flex; min-height: 0; }\n\n  .ilist { flex: 0 0 196px; border-right: 1px solid #1A1D23; background: #0B0D10; overflow-y: auto; padding: 6px 0; }\n  .ilist::-webkit-scrollbar { width: 6px; }\n  .ilist::-webkit-scrollbar-thumb { background: #2E333B; border-radius: 3px; }\n  .ii { display: flex; align-items: center; gap: 9px; height: 30px; padding: 0 14px; cursor: pointer; font-size: 11px; font-weight: 600; color: #8A919C; }\n  .ii:hover { color: #C3C9D2; background: #121419; }\n  .ii.on { color: #E8EAED; background: #16191E; box-shadow: inset 2px 0 0 #2E7BF6; }\n  .ii .d { width: 5px; height: 5px; border-radius: 50%; background: transparent; flex: 0 0 auto; }\n  .ii .d.set { background: #2E7BF6; }\n\n  .graph { flex: 1 1 auto; min-width: 0; overflow: auto; position: relative; background:\n    radial-gradient(circle at 1px 1px, #191C23 1px, transparent 0) 0 0 / 22px 22px, #0A0C0F; }\n  .graph::-webkit-scrollbar { width: 8px; height: 8px; }\n  .graph::-webkit-scrollbar-thumb { background: #2E333B; border-radius: 4px; }\n  .gcanvas { position: relative; }\n  .gcanvas svg { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }\n  .gcanvas path { fill: none; stroke: #2E63C8; stroke-width: 1.6; }\n\n  .node {\n    position: absolute; width: ${NW}px;\n    background: #101318; border: 1px solid #2A3040; border-radius: 8px;\n    box-shadow: 0 6px 18px rgba(0,0,0,.45);\n  }\n  .node.root { border-color: #2E63C8; }\n  .nhdr { height: ${HDR}px; display: flex; align-items: center; gap: 8px; padding: 0 10px; cursor: pointer; user-select: none; border-bottom: 1px solid #1B2029; }\n  .node.col .nhdr { border-bottom: none; }\n  .nhdr .d { width: 7px; height: 7px; border-radius: 50%; background: #8A919C; flex: 0 0 auto; }\n  .node.root .nhdr .d { background: #2E7BF6; }\n  .nhdr .t { font-size: 11px; font-weight: 700; color: #E8EAED; flex: 1; }\n  .nhdr .cv { color: #5C636E; font-size: 9px; transition: transform .15s; }\n  .node.col .nhdr .cv { transform: rotate(-90deg); }\n\n  .nbody { padding: ${PADV}px 0; }\n  .nrow { height: ${ROW}px; display: flex; align-items: center; gap: 8px; padding: 0 10px; position: relative; }\n  .nrow .k { font-size: 9px; font-weight: 700; letter-spacing: .6px; color: #5C636E; width: 34px; flex: 0 0 auto; }\n  .nrow .slot { font-size: 10.5px; font-weight: 600; color: #C3C9D2; flex: 1; }\n\n  select.sel {\n    flex: 1; min-width: 0; height: 22px; background: #171B22; color: #C3C9D2;\n    border: 1px solid #262C38; border-radius: 4px; font-size: 10.5px; padding: 0 6px;\n    font-family: inherit; cursor: pointer; outline: none;\n  }\n  select.sel:hover { border-color: #3A4353; }\n  select.sel.ph { color: #4E5666; font-style: italic; }\n  select.sel option { background: #171B22; color: #C3C9D2; font-style: normal; }\n\n  .port { position: absolute; right: -5px; width: 9px; height: 9px; border-radius: 50%; background: #101318; border: 2px solid #2E63C8; }\n  .addbtn { font-size: 10px; font-weight: 700; color: #4C8DFF; cursor: pointer; padding: 2px 6px; border: 1px dashed #2E63C8; border-radius: 4px; }\n  .addbtn:hover { background: #101A2B; }\n\n  .empty { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); font-size: 11px; color: #3A4049; }\n  `,\n\n  render(d2) {\n    const start = Number(d2.param('startH', 289)) || 289\n    const h = d2.state.h == null ? start : d2.state.h\n    const win2 = d2.state.win2 == null ? d2.shared.get('ui:win2', false) : d2.state.win2\n    const iw = Number(d2.shared.get('ui:inspectorW', 298)) || 298\n    const r = Math.max(0, iw - 298 + (win2 ? 298 : 0))\n\n    const blkId = d2.state.blk || d2.shared.get('ui:sel', 'a')\n    const blk = blockById(blkId)\n    const cfgAll = d2.state.cfg || d2.shared.get('ui:cfg', {}) || {}\n    const graph = (cfgAll[blkId] && cfgAll[blkId].graph) || {}\n    const sel = d2.state.sel || 'On Click'\n    const root = graph[sel] || blank()\n\n    const isSet = function (name) {\n      const g = graph[name]\n      return !!(g && (g.module || g.fn))\n    }\n\n    const list = INTERACTIONS.map(function (name) {\n      return '<div class=\"ii' + (name === sel ? ' on' : '') + '\" data-int=\"' + esc(name) + '\">' +\n        '<span class=\"d' + (isSet(name) ? ' set' : '') + '\"></span>' + name + '</div>'\n    }).join('')\n\n    const placed = []\n    place(root, 0, PAD, 'r', sel, placed)\n\n    let maxX = 0\n    let maxY = 0\n    placed.forEach(function (p) {\n      maxX = Math.max(maxX, p.x + NW)\n      maxY = Math.max(maxY, p.y + p.h)\n    })\n\n    const paths = []\n    placed.forEach(function (p) {\n      if (p.n.col) return\n      p.n.kids.forEach(function (k, i) {\n        const c = placed.filter(function (q) { return q.path === p.path + '.' + i })[0]\n        if (!c) return\n        paths.push(wire(p.x + NW, p.y + portY(p, i), c.x, c.y + HDR / 2))\n      })\n    })\n\n    const nodes = placed.map(function (p) {\n      const n = p.n\n      const slots = slotsOf(n)\n      let body = ''\n\n      if (!n.col) {\n        const modOpts = ['<option value=\"\">Select module & interaction…</option>'].concat(\n          MODULES.map(function (m) {\n            return '<option value=\"' + esc(m) + '\"' + (n.module === m ? ' selected' : '') + '>' + esc(m) + '</option>'\n          })\n        ).join('')\n\n        body += '<div class=\"nrow\">' +\n          (n.module ? '<span class=\"k\">module</span>' : '') +\n          '<select class=\"sel' + (n.module ? '' : ' ph') + '\" data-nm=\"' + p.path + '\">' + modOpts + '</select>' +\n        '</div>'\n\n        if (n.module) {\n          const fnOpts = ['<option value=\"\">—</option>'].concat(\n            FNLIST.map(function (f) {\n              return '<option value=\"' + f + '\"' + (n.fn === f ? ' selected' : '') + '>' + f + '</option>'\n            })\n          ).join('')\n          body += '<div class=\"nrow\"><span class=\"k\">f()</span>' +\n            '<select class=\"sel\" data-nf=\"' + p.path + '\">' + fnOpts + '</select></div>'\n\n          slots.forEach(function (s, i) {\n            body += '<div class=\"nrow\"><span class=\"slot\">' + esc(s) + '</span><span class=\"port\"></span></div>'\n          })\n\n          if (n.fn === 'Series') {\n            body += '<div class=\"nrow\" style=\"justify-content:flex-end\">' +\n              '<span class=\"addbtn\" data-add=\"' + p.path + '\">+ step</span></div>'\n          }\n        }\n        body = '<div class=\"nbody\">' + body + '</div>'\n      }\n\n      return '<div class=\"node' + (p.depth === 0 ? ' root' : '') + (n.col ? ' col' : '') + '\" ' +\n        'style=\"left:' + p.x + 'px;top:' + p.y + 'px;height:' + p.h + 'px\">' +\n        '<div class=\"nhdr\" data-ntitle=\"' + p.path + '\"><span class=\"d\"></span>' +\n          '<span class=\"t\">' + esc(p.label) + '</span><span class=\"cv\">▼</span></div>' +\n        body +\n      '</div>'\n    }).join('')\n\n    const cw = Math.max(maxX + PAD, 400)\n    const ch = Math.max(maxY + PAD, 200)\n\n    return '<div class=\"wrap\"><div class=\"dock\" style=\"height:' + h + 'px;right:' + r + 'px\">' +\n      '<div class=\"dhdr\"><span class=\"dot\"></span><span class=\"t\">Interaction</span>' +\n        '<span class=\"bname\">' + esc(blk.name) + '</span>' +\n        '<span class=\"sub mono\">' + esc(sel) + '</span></div>' +\n      '<div class=\"dbody\">' +\n        '<div class=\"ilist\">' + list + '</div>' +\n        '<div class=\"graph\"><div class=\"gcanvas\" style=\"width:' + cw + 'px;height:' + ch + 'px\">' +\n          '<svg width=\"' + cw + '\" height=\"' + ch + '\">' + paths.map(function (d) { return '<path d=\"' + d + '\"/>' }).join('') + '</svg>' +\n          nodes +\n        '</div></div>' +\n      '</div>' +\n    '</div>' +\n    '<div class=\"grip\" style=\"right:' + r + 'px;bottom:' + (h - 6) + 'px\"></div>' +\n    '</div>'\n  },\n\n  mounted(d2) {\n    const rn = d2.root && d2.root.getRootNode ? d2.root.getRootNode() : null\n    const sh = rn && rn.host\n    if (sh) {\n      sh.style.pointerEvents = 'none'\n      const pb = sh.closest && sh.closest('.data2-player-block')\n      if (pb) pb.style.pointerEvents = 'none'\n    }\n\n    const mutate = function (path, fn) {\n      const all = clone(d2.state.cfg || d2.shared.get('ui:cfg', {}) || {})\n      const b = d2.state.blk || d2.shared.get('ui:sel', 'a')\n      const s = d2.state.sel || 'On Click'\n      if (!all[b]) all[b] = {}\n      if (!all[b].graph) all[b].graph = {}\n      if (!all[b].graph[s]) all[b].graph[s] = blank()\n      fn(getNode(all[b].graph[s], path), all[b].graph[s])\n      d2.setState({ cfg: all })\n      d2.shared.set('ui:cfg', all)\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:cfg', all)\n    }\n\n    d2.on('[data-int]', 'click', function (e) {\n      d2.setState({ sel: e.target.closest('[data-int]').dataset.int })\n    })\n\n    d2.on('[data-ntitle]', 'click', function (e) {\n      mutate(e.target.closest('[data-ntitle]').dataset.ntitle, function (n) { n.col = !n.col })\n    })\n\n    d2.on('select[data-nm]', 'change', function (e) {\n      const el = e.target.closest('select[data-nm]')\n      const v = el.value\n      mutate(el.dataset.nm, function (n) {\n        n.module = v || null\n        if (!v) { n.fn = null; n.kids = [] }\n      })\n    })\n\n    d2.on('select[data-nf]', 'change', function (e) {\n      const el = e.target.closest('select[data-nf]')\n      const v = el.value\n      mutate(el.dataset.nf, function (n) {\n        n.fn = v || null\n        const d = FNS[v]\n        if (!d) { n.kids = [] } else if (d.slots === 'n') {\n          if (!n.kids.length) n.kids = [blank()]\n        } else {\n          n.kids = d.slots.map(function () { return blank() })\n        }\n      })\n    })\n\n    d2.on('[data-add]', 'click', function (e) {\n      mutate(e.target.closest('[data-add]').dataset.add, function (n) { n.kids.push(blank()) })\n    })\n\n    let drag = null\n\n    const bounds = function () {\n      const wrap = d2.$('.wrap')\n      const wrapH = wrap ? wrap.getBoundingClientRect().height : 0\n      const minH = Math.max(0, Number(d2.param('minH', 0)) || 0)\n      const topGap = Number(d2.param('topGap', 56)) || 56\n      return { min: minH, max: Math.max(minH, wrapH - topGap) }\n    }\n\n    d2.on('.grip', 'pointerdown', function (e) {\n      const dock = d2.$('.dock')\n      if (!dock) return\n      e.preventDefault()\n      drag = { y: e.clientY, h: dock.getBoundingClientRect().height }\n      const wrap = d2.$('.wrap')\n      if (wrap) wrap.setAttribute('data-drag', '1')\n      document.body.style.userSelect = 'none'\n    })\n\n    d2.on(window, 'pointermove', function (e) {\n      if (!drag) return\n      const dock = d2.$('.dock')\n      if (!dock) return\n      const b = bounds()\n      let hh = drag.h + (drag.y - e.clientY)\n      hh = Math.max(b.min, Math.min(hh, b.max))\n      dock.style.height = hh + 'px'\n      const g = d2.$('.grip')\n      if (g) g.style.bottom = (hh - 6) + 'px'\n    })\n\n    d2.on(window, 'pointerup', function () {\n      if (!drag) return\n      const dock = d2.$('.dock')\n      drag = null\n      const wrap = d2.$('.wrap')\n      if (wrap) wrap.removeAttribute('data-drag')\n      document.body.style.userSelect = ''\n      if (dock) d2.setState({ h: Math.round(dock.getBoundingClientRect().height) })\n    })\n\n    if (d2.bus && d2.bus.on) {\n      d2.bus.on('ui:inspectorW', function (w) {\n        const dock = d2.$('.dock')\n        const w2 = d2.state.win2 == null ? d2.shared.get('ui:win2', false) : d2.state.win2\n        const rr = Math.max(0, Number(w) - 298 + (w2 ? 298 : 0))\n        if (dock) dock.style.right = rr + 'px'\n        const g = d2.$('.grip')\n        if (g) g.style.right = rr + 'px'\n      })\n      d2.bus.on('ui:cfg', function (g) { d2.setState({ cfg: g }) })\n      d2.bus.on('ui:sel', function (b) { d2.setState({ blk: b }) })\n      d2.bus.on('ui:win2', function (v) { d2.setState({ win2: !!v }) })\n    }\n\n    d2.onUnmount(function () { document.body.style.userSelect = '' })\n  }\n}"}},"startH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"startH"}}}},"minH":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"minH"}}}},"topGap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"topGap"}}}}}}}}}},{"_id":"6a7a3d2b63db671470b33902","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"canvasblocks","package":"bigaton-playground","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 BLOCKS = [\n  { id: 'a', name: 'HeaderBar', x: 110, y: 96, w: 380, h: 150, fill: '#1F4E79' },\n  { id: 'b', name: 'CardList', x: 600, y: 290, w: 300, h: 210, fill: '#2A2E33' }\n]\n\nexport default {\n  css: `\n  :host { display: block; pointer-events: none; }\n  * { box-sizing: border-box; }\n  .wrap { position: absolute; inset: 0; pointer-events: none; }\n\n  .blk {\n    position: absolute; pointer-events: auto; cursor: pointer;\n    border: 1px solid #2A3040; border-radius: 8px;\n    display: flex; align-items: flex-end; padding: 10px;\n    font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n    transition: border-color .12s, box-shadow .12s;\n  }\n  .blk:hover { border-color: #3A4353; }\n  .blk.on { border-color: #2E7BF6; box-shadow: 0 0 0 1px #2E7BF6, 0 8px 24px rgba(46,123,246,.18); }\n\n  .blk .tag {\n    font-size: 10.5px; font-weight: 700; letter-spacing: .3px; color: #C3C9D2;\n    background: rgba(10,12,15,.72); border: 1px solid #2A3040; border-radius: 5px; padding: 3px 8px;\n  }\n  .blk.on .tag { color: #FFFFFF; border-color: #2E63C8; background: rgba(16,26,43,.85); }\n  `,\n\n  render(d2) {\n    const sel = d2.state.sel || d2.shared.get('ui:sel', 'a')\n    return '<div class=\"wrap\">' + BLOCKS.map(function (b) {\n      return '<div class=\"blk' + (b.id === sel ? ' on' : '') + '\" data-blk=\"' + b.id + '\" ' +\n        'style=\"left:' + b.x + 'px;top:' + b.y + 'px;width:' + b.w + 'px;height:' + b.h + 'px;background:' + b.fill + '\">' +\n        '<span class=\"tag\">' + b.name + '</span>' +\n      '</div>'\n    }).join('') + '</div>'\n  },\n\n  mounted(d2) {\n    const rn = d2.root && d2.root.getRootNode ? d2.root.getRootNode() : null\n    const sh = rn && rn.host\n    if (sh) {\n      sh.style.pointerEvents = 'none'\n      const pb = sh.closest && sh.closest('.data2-player-block')\n      if (pb) pb.style.pointerEvents = 'none'\n    }\n\n    d2.on('[data-blk]', 'click', function (e) {\n      const id = e.target.closest('[data-blk]').dataset.blk\n      d2.setState({ sel: id })\n      d2.shared.set('ui:sel', id)\n      if (d2.bus && d2.bus.emit) d2.bus.emit('ui:sel', id)\n    })\n\n    if (d2.bus && d2.bus.on) {\n      d2.bus.on('ui:sel', function (id) { d2.setState({ sel: id }) })\n    }\n  }\n}"}}}}}}}},{"_id":"6a7e08408157bd3190adba8e","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"FlowEditor","package":"bigaton-playground","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 ROLES_FALLBACK = ['Fornecedor', 'Comprador Interno', 'Jurídico', 'Diretor']\nconst uid = p => p + '_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8)\nconst NW = t => t === 'start' ? 150 : 240\nconst PORTY = 26\nconst esc = s => String(s == null ? '' : s).replace(/[&<>\"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]))\nconst startNode = () => ({ id: uid('nd'), type: 'start', x: 80, y: 200 })\nconst clone = v => JSON.parse(JSON.stringify(v))\n\nexport default {\n  css: `\n.wrap{display:grid;grid-template-columns:264px 1fr 236px;height:100%;font-family:Inter,system-ui,-apple-system,'Segoe UI',sans-serif;color:#101828;background:#F4F6F8;font-size:13px}\n.col{min-width:0;min-height:0;display:flex;flex-direction:column}\n.side{background:#fff;border-right:1px solid #E4E7EC}\n.pal{background:#fff;border-left:1px solid #E4E7EC}\n.hd{padding:18px 16px 8px;font-size:10px;letter-spacing:1.2px;font-weight:700;color:#98A2B3;text-transform:uppercase}\n.list{flex:1;overflow:auto;padding:0 10px 10px}\n.fl{padding:10px 12px;border-radius:10px;font-size:13px;cursor:pointer;border:1px solid transparent;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.fl:hover{background:#F4F6F8}\n.fl[data-on]{background:#EEF4FF;border-color:#C7D7FE;color:#2563EB;font-weight:600}\n.empt{padding:10px 14px;color:#98A2B3;font-size:12px}\n.newbtn{margin:10px;padding:11px;border-radius:10px;background:#101828;color:#fff;font-size:13px;font-weight:600;text-align:center;cursor:pointer;border:0;font-family:inherit}\n.newbtn:hover{background:#1D2939}\n.chd{height:56px;display:flex;align-items:center;gap:12px;padding:0 16px;background:#fff;border-bottom:1px solid #E4E7EC;flex:none}\n.nameIn{font-size:14px;font-weight:600;border:1px solid transparent;border-radius:8px;padding:7px 9px;min-width:280px;background:transparent;color:#101828;font-family:inherit}\n.nameIn:hover{border-color:#E4E7EC}\n.nameIn:focus{outline:none;border-color:#2563EB;background:#fff}\n.tip{color:#98A2B3;font-size:12px}\n.idchip{font:11px ui-monospace,SFMono-Regular,Menlo,monospace;color:#667085;background:#F4F6F8;border:1px solid #E4E7EC;border-radius:6px;padding:3px 7px;white-space:nowrap}\n.canvas{position:relative;flex:1;overflow:auto;background-color:#F4F6F8;background-image:radial-gradient(#D0D5DD 1px,transparent 1px);background-size:20px 20px}\n.stage{position:relative;width:2200px;height:1500px}\n.edges{position:absolute;left:0;top:0;width:2200px;height:1500px;pointer-events:none}\n.node{position:absolute;background:#fff;border:1px solid #E4E7EC;border-radius:12px;box-shadow:0 4px 14px rgba(16,24,40,.07);width:240px}\n.node[data-t=start]{width:150px;background:#101828;border-color:#101828;color:#fff}\n.nh{display:flex;align-items:center;gap:8px;padding:11px 12px;font-size:12px;font-weight:700;cursor:grab;user-select:none;touch-action:none}\n.dot{width:8px;height:8px;border-radius:99px;flex:none}\n.nb{padding:0 12px 12px}\n.fld{margin-bottom:8px}\n.fld label{display:block;font-size:11px;color:#667085;margin-bottom:3px}\n.fld input{width:100%;box-sizing:border-box;border:1px solid #E4E7EC;border-radius:8px;padding:7px 9px;font-size:12px;background:#F9FAFB;color:#98A2B3;font-family:inherit}\n.add{width:100%;border:1px dashed #C7D7FE;background:#F5F8FF;color:#2563EB;border-radius:8px;padding:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}\n.add:hover{background:#EEF4FF}\n.lbl{display:block;font-size:11px;color:#667085;margin-bottom:4px}\n.sel{width:100%;box-sizing:border-box;border:1px solid #E4E7EC;border-radius:8px;padding:7px 9px;font-size:12px;background:#fff;color:#101828;font-family:inherit}\n.port{position:absolute;top:19px;width:14px;height:14px;border-radius:99px;background:#fff;border:2px solid #98A2B3;box-sizing:border-box}\n.port.out{right:-8px;cursor:crosshair;touch-action:none}\n.port.out::after{content:'';position:absolute;left:-11px;top:-11px;width:36px;height:36px;border-radius:99px}\n.port.out:hover{border-color:#2563EB;background:#EEF4FF;transform:scale(1.15)}\n.port.out[data-armed]{background:#2563EB;border-color:#2563EB;box-shadow:0 0 0 4px rgba(37,99,235,.18)}\n.port.in{left:-8px}\n.pchip{display:flex;align-items:center;gap:8px;margin:0 10px 8px;padding:11px 12px;border:1px solid #E4E7EC;border-radius:10px;background:#FCFCFD;font-size:13px;font-weight:600;cursor:grab;user-select:none;touch-action:none}\n.pchip:hover{border-color:#C7D7FE;background:#F5F8FF}\n.blank{flex:1;display:flex;align-items:center;justify-content:center;color:#98A2B3;font-size:13px;text-align:center;padding:24px}\n`,\n\n  render(d2) {\n    const ds = d2.data()\n    const flows = ds ? (ds.records() || []) : []\n    const fid = d2.state.flowId || null\n    const nodes = d2.state.nodes || []\n    const edges = d2.state.edges || []\n    let roles = d2.param('roles')\n    if (!Array.isArray(roles) || !roles.length) roles = ROLES_FALLBACK\n\n    const byId = {}\n    nodes.forEach(n => { byId[n.id] = n })\n\n    const curve = (x1, y1, x2, y2) => 'M ' + x1 + ' ' + y1 + ' C ' + (x1 + 70) + ' ' + y1 + ', ' + (x2 - 70) + ' ' + y2 + ', ' + x2 + ' ' + y2\n\n    const edgeSvg = edges.map(e => {\n      const a = byId[e.from], b = byId[e.to]\n      if (!a || !b) return ''\n      return '<path d=\"' + curve(a.x + NW(a.type), a.y + PORTY, b.x, b.y + PORTY) + '\" fill=\"none\" stroke=\"#98A2B3\" stroke-width=\"2\"/>'\n    }).join('')\n\n    let liveLink = ''\n    const lk = d2.state.link\n    if (lk && byId[lk.from]) {\n      const a = byId[lk.from]\n      liveLink = '<path d=\"' + curve(a.x + NW(a.type), a.y + PORTY, lk.x, lk.y) + '\" fill=\"none\" stroke=\"#2563EB\" stroke-width=\"2\" stroke-dasharray=\"5 4\"/>'\n    }\n\n    const nodeHtml = n => {\n      const pos = 'left:' + n.x + 'px;top:' + n.y + 'px'\n      const armedAttr = d2.state.armed === n.id ? ' data-armed' : ''\n      if (n.type === 'start') {\n        return '<div class=\"node\" data-t=\"start\" data-node=\"' + n.id + '\" data-key=\"' + n.id + '\" style=\"' + pos + '\">' +\n          '<div class=\"nh\" data-drag=\"' + n.id + '\"><span class=\"dot\" style=\"background:#34D399\"></span>Start</div>' +\n          '<div class=\"port out\" data-out=\"' + n.id + '\"' + armedAttr + '></div></div>'\n      }\n      if (n.type === 'input') {\n        const fields = (n.fields || []).map(f =>\n          '<div class=\"fld\"><label>' + esc(f.title) + '</label><input disabled placeholder=\"preenchido por quem usa o fluxo\"/></div>'\n        ).join('')\n        return '<div class=\"node\" data-t=\"input\" data-node=\"' + n.id + '\" data-key=\"' + n.id + '\" style=\"' + pos + '\">' +\n          '<div class=\"nh\" data-drag=\"' + n.id + '\"><span class=\"dot\" style=\"background:#2563EB\"></span>Input</div>' +\n          '<div class=\"nb\">' + fields + '<button class=\"add\" data-addfield=\"' + n.id + '\">+ Novo Campo</button></div>' +\n          '<div class=\"port in\"></div><div class=\"port out\" data-out=\"' + n.id + '\"></div></div>'\n      }\n      const opts = ['<option value=\"\">Selecione…</option>'].concat(roles.map(r => {\n        const v = typeof r === 'string' ? r : (r.value || r.label || '')\n        return '<option value=\"' + esc(v) + '\"' + (n.role === v ? ' selected' : '') + '>' + esc(v) + '</option>'\n      })).join('')\n      return '<div class=\"node\" data-t=\"validation\" data-node=\"' + n.id + '\" data-key=\"' + n.id + '\" style=\"' + pos + '\">' +\n        '<div class=\"nh\" data-drag=\"' + n.id + '\"><span class=\"dot\" style=\"background:#7C3AED\"></span>Validação</div>' +\n        '<div class=\"nb\"><label class=\"lbl\">Role</label><select class=\"sel\" data-role=\"' + n.id + '\">' + opts + '</select></div>' +\n        '<div class=\"port in\"></div><div class=\"port out\" data-out=\"' + n.id + '\"></div></div>'\n    }\n\n    const flowList = flows.length\n      ? flows.map(f => '<div class=\"fl\" data-flow=\"' + f._id + '\" data-key=\"' + f._id + '\"' + (String(f._id) === String(fid) ? ' data-on' : '') + '>' + esc(f.name || 'Sem nome') + '</div>').join('')\n      : '<div class=\"empt\">Nenhum fluxo ainda.</div>'\n\n    const cur = flows.find(f => String(f._id) === String(fid))\n\n    const center = fid\n      ? '<div class=\"chd\"><input class=\"nameIn\" data-name value=\"' + esc(d2.state.name || '') + '\"/>' +\n        '<span class=\"idchip\">' + esc((cur && cur.key) || '—') + ' · v' + (Number(d2.state.version) || 1) + '</span>' +\n        '<span class=\"tip\">' + (d2.state.armed ? 'agora clique no node de destino para ligar' : 'arraste da bolinha direita até o outro node — ou clique na bolinha e depois no destino') + '</span></div>' +\n        '<div class=\"canvas\" data-canvas><div class=\"stage\">' +\n        '<svg class=\"edges\" viewBox=\"0 0 2200 1500\">' + edgeSvg + liveLink + '</svg>' +\n        nodes.map(nodeHtml).join('') + '</div></div>'\n      : '<div class=\"chd\"><span class=\"tip\">Nenhum fluxo em edição</span></div>' +\n        '<div class=\"blank\">Selecione um fluxo à esquerda ou clique em <b>&nbsp;Novo Fluxo&nbsp;</b> para começar do zero.</div>'\n\n    return '<div class=\"wrap\">' +\n      '<div class=\"col side\"><div class=\"hd\">Fluxos</div><div class=\"list\">' + flowList + '</div><button class=\"newbtn\" data-new>Novo Fluxo</button></div>' +\n      '<div class=\"col\">' + center + '</div>' +\n      '<div class=\"col pal\"><div class=\"hd\">Paleta de nodes</div>' +\n      '<div class=\"pchip\" data-palette=\"input\"><span class=\"dot\" style=\"background:#2563EB\"></span>Input</div>' +\n      '<div class=\"pchip\" data-palette=\"validation\"><span class=\"dot\" style=\"background:#7C3AED\"></span>Validação</div>' +\n      '</div></div>'\n  },\n\n  async mounted(d2) {\n    const ds = d2.data()\n    if (ds) { try { await ds.refresh() } catch (err) { d2.log('refresh failed', err) } }\n\n    const recs = ds ? (ds.records() || []) : []\n    if (recs.length && !d2.state.flowId) {\n      const r0 = recs[0]\n      d2.setState({\n        flowId: r0._id,\n        name: r0.name || '',\n        nodes: Array.isArray(r0.nodes) && r0.nodes.length ? clone(r0.nodes) : [startNode()],\n        edges: Array.isArray(r0.edges) ? clone(r0.edges) : [],\n        version: Number(r0.version) || 1\n      })\n    }\n\n    let start = null\n\n    const toCanvas = e => {\n      const c = d2.$('[data-canvas]')\n      if (!c) return { x: 0, y: 0 }\n      const r = c.getBoundingClientRect()\n      return { x: e.clientX - r.left + c.scrollLeft, y: e.clientY - r.top + c.scrollTop }\n    }\n\n    const nodeEls = () => {\n      const root = d2.root\n      if (root && root.querySelectorAll) return Array.from(root.querySelectorAll('[data-node]'))\n      return []\n    }\n\n    const nodeAtPoint = (cx, cy, skipId) => {\n      const els = nodeEls()\n      for (let i = els.length - 1; i >= 0; i--) {\n        const el = els[i]\n        const id = el && el.dataset ? el.dataset.node : null\n        if (!id || id === skipId) continue\n        const r = el.getBoundingClientRect()\n        if (cx >= r.left - 12 && cx <= r.right + 12 && cy >= r.top - 12 && cy <= r.bottom + 12) return id\n      }\n      return null\n    }\n\n    const save = async () => {\n      const id = d2.state.flowId\n      const h = d2.data()\n      if (!id || !h) return\n      const version = (Number(d2.state.version) || 0) + 1\n      try {\n        await h.update({ _id: id, name: d2.state.name, nodes: d2.state.nodes, edges: d2.state.edges, version: version })\n        d2.setState({ version: version })\n      } catch (err) { d2.log('save failed', err) }\n    }\n    d2._save = save\n\n    const connect = (from, to) => {\n      if (!from || !to || from === to) return\n      const edges = d2.state.edges || []\n      if (edges.some(x => x.from === from && x.to === to)) return\n      d2.setState({ edges: edges.concat([{ from: from, to: to }]), armed: null })\n      save()\n    }\n\n    d2.on('[data-node]', 'click', (e, el) => {\n      const armed = d2.state.armed\n      if (!armed) return\n      const id = el && el.dataset ? el.dataset.node : null\n      if (!id || id === armed) return\n      connect(armed, id)\n    })\n\n    d2.on('[data-flow]', 'click', (e, el) => {\n      const h = d2.data()\n      const rec = (h ? h.records() || [] : []).find(r => String(r._id) === el.dataset.flow)\n      if (!rec) return\n      d2.setState({\n        flowId: rec._id,\n        name: rec.name || '',\n        nodes: Array.isArray(rec.nodes) && rec.nodes.length ? clone(rec.nodes) : [startNode()],\n        edges: Array.isArray(rec.edges) ? clone(rec.edges) : [],\n        version: Number(rec.version) || 1,\n        link: null,\n        armed: null\n      })\n    })\n\n    d2.on('[data-new]', 'click', async () => {\n      const h = d2.data()\n      if (!h) return\n      const key = uid('flw')\n      const nodes = [startNode()]\n      await h.create({ key: key, name: 'Novo fluxo', nodes: nodes, edges: [], version: 1 })\n      let rec = null\n      for (let i = 0; i < 12 && !rec; i++) {\n        try { await h.refresh() } catch (err) { d2.log('refresh failed', err) }\n        rec = (h.records() || []).find(r => r.key === key)\n        if (!rec) await new Promise(r => setTimeout(r, 160))\n      }\n      if (rec) d2.setState({ flowId: rec._id, name: rec.name || 'Novo fluxo', nodes: nodes, edges: [], version: 1, link: null })\n    })\n\n    d2.on('[data-name]', 'change', (e, el) => { d2.setState({ name: el.value }); save() })\n\n    d2.on('[data-addfield]', 'click', async (e, el) => {\n      const id = el.dataset.addfield\n      const title = await d2.dialog.prompt('Título do campo', '')\n      if (!title) return\n      const nodes = (d2.state.nodes || []).map(n =>\n        n.id === id ? Object.assign({}, n, { fields: (n.fields || []).concat([{ id: uid('fld'), title: String(title) }]) }) : n\n      )\n      d2.setState({ nodes: nodes })\n      save()\n    })\n\n    d2.on('[data-role]', 'change', (e, el) => {\n      const id = el.dataset.role\n      const nodes = (d2.state.nodes || []).map(n => n.id === id ? Object.assign({}, n, { role: el.value }) : n)\n      d2.setState({ nodes: nodes })\n      save()\n    })\n\n    d2.on('[data-palette]', 'pointerdown', (e, el) => {\n      e.preventDefault()\n      start = { kind: 'pal', type: el.dataset.palette }\n      d2.setState({ ghost: { x: e.clientX, y: e.clientY, label: el.dataset.palette === 'input' ? 'Input' : 'Validação' } })\n    })\n\n    d2.on('[data-drag]', 'pointerdown', (e, el) => {\n      e.preventDefault()\n      const n = (d2.state.nodes || []).find(v => v.id === el.dataset.drag)\n      if (!n) return\n      const p = toCanvas(e)\n      start = { kind: 'move', id: n.id, dx: p.x - n.x, dy: p.y - n.y }\n    })\n\n    d2.on('[data-out]', 'pointerdown', (e, el) => {\n      e.preventDefault()\n      const p = toCanvas(e)\n      start = { kind: 'link', from: el.dataset.out, sx: e.clientX, sy: e.clientY }\n      d2.setState({ link: { from: el.dataset.out, x: p.x, y: p.y } })\n    })\n\n    d2.on(window, 'pointermove', e => {\n      if (!start) return\n      if (start.kind === 'pal') {\n        d2.setState({ ghost: Object.assign({}, d2.state.ghost || {}, { x: e.clientX, y: e.clientY }) })\n        return\n      }\n      const p = toCanvas(e)\n      if (start.kind === 'move') {\n        const nodes = (d2.state.nodes || []).map(n =>\n          n.id === start.id ? Object.assign({}, n, { x: Math.max(0, Math.round(p.x - start.dx)), y: Math.max(0, Math.round(p.y - start.dy)) }) : n\n        )\n        d2.setState({ nodes: nodes })\n      } else if (start.kind === 'link') {\n        d2.setState({ link: { from: start.from, x: Math.round(p.x), y: Math.round(p.y) } })\n      }\n    })\n\n    d2.on(window, 'pointerup', e => {\n      if (!start) return\n      const s = start\n      start = null\n\n      if (s.kind === 'pal') {\n        d2.setState({ ghost: null })\n        const c = d2.$('[data-canvas]')\n        if (!c || !d2.state.flowId) return\n        const r = c.getBoundingClientRect()\n        if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return\n        const node = {\n          id: uid('nd'),\n          type: s.type,\n          x: Math.max(0, Math.round(e.clientX - r.left + c.scrollLeft - 110)),\n          y: Math.max(0, Math.round(e.clientY - r.top + c.scrollTop - 20))\n        }\n        if (s.type === 'input') node.fields = []\n        if (s.type === 'validation') node.role = ''\n        d2.setState({ nodes: (d2.state.nodes || []).concat([node]) })\n        save()\n        return\n      }\n\n      if (s.kind === 'move') { save(); return }\n\n      if (s.kind === 'link') {\n        const moved = Math.abs(e.clientX - s.sx) + Math.abs(e.clientY - s.sy)\n        if (moved < 6) {\n          d2.setState({ link: null, armed: d2.state.armed === s.from ? null : s.from })\n          return\n        }\n        const to = nodeAtPoint(e.clientX, e.clientY, s.from)\n        d2.setState({ link: null })\n        if (to) connect(s.from, to)\n      }\n    })\n\n    d2.onUnmount(() => { if (d2._ghostEl) { d2._ghostEl.remove(); d2._ghostEl = null } })\n  },\n\n  updated(d2) {\n    let g = d2._ghostEl\n    const gh = d2.state.ghost\n    if (!gh) { if (g) { g.remove(); d2._ghostEl = null } return }\n    if (!g) {\n      g = document.createElement('div')\n      Object.assign(g.style, {\n        position: 'fixed', pointerEvents: 'none', zIndex: '99999', padding: '11px 14px',\n        borderRadius: '10px', background: '#fff', border: '1px solid #C7D7FE',\n        boxShadow: '0 12px 32px rgba(16,24,40,.22)', font: '600 13px Inter, system-ui, sans-serif',\n        color: '#101828', left: '0', top: '0'\n      })\n      document.body.appendChild(g)\n      d2._ghostEl = g\n    }\n    g.textContent = gh.label || ''\n    g.style.transform = 'translate3d(' + (gh.x + 10) + 'px,' + (gh.y + 10) + 'px,0)'\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"roles":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"roles"}}}}}}}}}},{"_id":"6a7e156a66d4882481b910bb","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"FlowInbox","package":"bigaton-playground","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 uid = p => p + '_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8)\nconst esc = s => String(s == null ? '' : s).replace(/[&<>\"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]))\nconst nowIso = () => new Date().toISOString()\nconst sleep = ms => new Promise(r => setTimeout(r, ms))\nconst TYPE_LABEL = { input: 'Input', validation: 'Validação', start: 'Start' }\nconst TYPE_COLOR = { input: '#2563EB', validation: '#7C3AED', start: '#34D399' }\n\n// d2.store() resolves by GLOBAL reference, not by declared param name.\nconst sessRead = (d2, key) => {\n  let s = null\n  try { s = d2.store('session') } catch (err) { s = null }\n  if (!s) { try { s = d2.store('Session') } catch (err) { s = null } }\n  if (s) {\n    try {\n      const v = s.get('data.' + key, '')\n      if (v !== undefined && v !== null && v !== '') return v\n    } catch (err) { d2.log('store get failed', err) }\n  }\n  try {\n    const m = d2.param('session')\n    if (m) {\n      const f = d2.field(m, 'data.' + key)\n      if (f && f.value) return f.value\n    }\n  } catch (err) { d2.log('field read failed', err) }\n  return ''\n}\n\nconst fmtWhen = (d2, s) => {\n  if (!s) return ''\n  try { return d2.fmt.date(s) } catch (err) { return String(s).slice(0, 10) }\n}\n\nconst orderNodes = (nodes, edges) => {\n  const byId = {}\n  nodes.forEach(n => { byId[n.id] = n })\n  const seen = {}\n  const out = []\n  let frontier = []\n  nodes.filter(n => n.type === 'start').forEach(n => {\n    seen[n.id] = 1\n    edges.filter(e => e.from === n.id).forEach(e => frontier.push(e.to))\n  })\n  while (frontier.length) {\n    const next = []\n    frontier.forEach(id => {\n      if (seen[id] || !byId[id]) return\n      seen[id] = 1\n      out.push(byId[id])\n      edges.filter(e => e.from === id).forEach(e => next.push(e.to))\n    })\n    frontier = next\n  }\n  nodes.forEach(n => { if (n.type !== 'start' && !seen[n.id]) { seen[n.id] = 1; out.push(n) } })\n  return out\n}\n\nexport default {\n  css: `\n.ib{display:flex;flex-direction:column;height:100%;font-family:Inter,system-ui,-apple-system,'Segoe UI',sans-serif;color:#101828;font-size:13px;background:#fff;border:1px solid #E4E7EC;border-radius:16px;overflow:hidden}\n.top{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid #E4E7EC;flex:none}\n.ttl{font-size:15px;font-weight:700}\n.rolechip{font:11px ui-monospace,SFMono-Regular,Menlo,monospace;color:#667085;background:#F4F6F8;border:1px solid #E4E7EC;border-radius:6px;padding:3px 8px}\n.spacer{flex:1}\n.primary{border:0;background:#101828;color:#fff;border-radius:9px;padding:9px 14px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit}\n.primary:hover{background:#1D2939}\n.primary:disabled{opacity:.45;cursor:default}\n.ghost{border:1px solid #E4E7EC;background:#fff;color:#344054;border-radius:9px;padding:9px 14px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit}\n.ghost:hover{background:#F4F6F8}\n.body{flex:1;min-height:0;display:grid;grid-template-columns:308px 1fr}\n.rail{min-width:0;min-height:0;display:flex;flex-direction:column;border-right:1px solid #E4E7EC;background:#FCFCFD}\n.tabs{display:flex;gap:2px;padding:12px 12px 0;flex:none}\n.tab{flex:1;border:0;background:transparent;color:#667085;font-family:inherit;font-size:12px;font-weight:600;padding:8px 6px;border-radius:8px 8px 0 0;cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}\n.tab:hover{color:#101828}\n.tab[data-on]{color:#2563EB;border-bottom-color:#2563EB;background:#fff}\n.cnt{display:inline-block;min-width:16px;padding:0 4px;margin-left:4px;border-radius:99px;background:#2563EB;color:#fff;font-size:10px;line-height:16px;text-align:center}\n.tab[data-on] .cnt{background:#2563EB}\n.tab .cnt.zero{background:#D0D5DD;color:#fff}\n.list{flex:1;min-height:0;overflow:auto;padding:10px}\n.runrow{padding:11px 12px;border:1px solid #E4E7EC;border-radius:10px;margin-bottom:8px;background:#fff;cursor:pointer}\n.runrow:hover{border-color:#C7D7FE}\n.runrow[data-on]{border-color:#2563EB;background:#F5F8FF;box-shadow:0 0 0 3px rgba(37,99,235,.10)}\n.rname{font-size:12.5px;font-weight:600;margin-bottom:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.bar{height:5px;border-radius:99px;background:#EAECF0;overflow:hidden;margin-top:7px}\n.fill{height:100%;background:#34D399}\n.badge{display:inline-block;font-size:10px;font-weight:700;border-radius:99px;padding:2px 7px;text-transform:uppercase;letter-spacing:.6px}\n.badge.running{background:#EEF4FF;color:#2563EB}\n.badge.done{background:#ECFDF3;color:#067647}\n.badge.mine{background:#FFFAEB;color:#B54708}\n.holder{font-size:11.5px;color:#475467;margin-top:6px}\n.holder b{font-weight:600;color:#101828}\n.meta{font-size:11px;color:#98A2B3;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}\n.detail{min-width:0;min-height:0;overflow:auto;padding:20px 24px}\n.dhd{border-bottom:1px solid #E4E7EC;padding-bottom:16px;margin-bottom:18px}\n.dtitle{font-size:19px;font-weight:700;letter-spacing:-.3px;margin-bottom:6px}\n.dsub{font-size:12px;color:#667085}\n.tl{position:relative;padding-left:30px}\n.tl:before{content:'';position:absolute;left:9px;top:6px;bottom:6px;width:2px;background:#EAECF0}\n.step{position:relative;margin-bottom:14px}\n.pin{position:absolute;left:-30px;top:14px;width:20px;height:20px;border-radius:99px;background:#fff;border:2px solid #D0D5DD;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#98A2B3;box-sizing:border-box}\n.step.is-done .pin{border-color:#34D399;background:#34D399;color:#fff}\n.step.is-now .pin{border-color:#2563EB;box-shadow:0 0 0 4px rgba(37,99,235,.14);color:#2563EB}\n.sbox{border:1px solid #E4E7EC;border-radius:12px;padding:14px;background:#FCFCFD}\n.step.is-now .sbox{background:#fff;border-color:#C7D7FE;box-shadow:0 6px 20px rgba(16,24,40,.07)}\n.step.is-wait .sbox{border-style:dashed;opacity:.85}\n.srow{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n.dot{width:8px;height:8px;border-radius:99px;flex:none}\n.ctype{font-size:11px;font-weight:700;color:#344054}\n.who{font-size:12px;color:#667085;margin-bottom:10px}\n.who b{color:#101828;font-weight:600}\n.fld{margin-bottom:8px}\n.fld label{display:block;font-size:11px;color:#667085;margin-bottom:3px}\n.fld input,.fld select{width:100%;box-sizing:border-box;border:1px solid #E4E7EC;border-radius:8px;padding:8px 10px;font-size:12.5px;font-family:inherit;background:#fff;color:#101828}\n.fld input:focus,.fld select:focus{outline:none;border-color:#2563EB}\n.fld input:disabled{background:#F9FAFB;color:#98A2B3}\n.kv{display:flex;justify-content:space-between;gap:12px;font-size:12px;padding:5px 0;border-bottom:1px solid #F2F4F7}\n.kv span{color:#667085}\n.kv b{color:#101828;font-weight:600;text-align:right}\n.done{border:0;background:#2563EB;color:#fff;border-radius:8px;padding:9px 16px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:8px}\n.done:hover{background:#1D4FD7}\n.done:disabled{opacity:.5;cursor:default}\n.blank{color:#98A2B3;font-size:12.5px;padding:10px 0}\n.empty{height:100%;display:flex;align-items:center;justify-content:center;color:#98A2B3;font-size:13px;text-align:center;padding:32px}\n.ovl{position:absolute;inset:0;background:rgba(16,24,40,.45);display:flex;align-items:center;justify-content:center;z-index:20}\n.modal{background:#fff;border-radius:14px;width:520px;max-height:82%;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 60px rgba(16,24,40,.28)}\n.mhd{padding:16px 18px;border-bottom:1px solid #E4E7EC;font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}\n.mbody{padding:14px 18px;overflow:auto}\n.mft{padding:14px 18px;border-top:1px solid #E4E7EC;display:flex;align-items:center;gap:10px}\n.sect{font-size:10px;letter-spacing:1.2px;font-weight:700;color:#98A2B3;text-transform:uppercase;margin:0 0 10px}\n.tpl{padding:12px;border:1px solid #E4E7EC;border-radius:10px;margin-bottom:8px;cursor:pointer;background:#FCFCFD}\n.tpl:hover{border-color:#C7D7FE;background:#F5F8FF}\n.tname{font-size:13px;font-weight:600}\n.pstep{border:1px solid #E4E7EC;border-radius:10px;padding:12px;margin-bottom:10px;background:#FCFCFD}\n.pstep.miss{border-color:#FDA29B;background:#FFFBFA}\n.snum{width:20px;height:20px;border-radius:99px;background:#EAECF0;color:#475467;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex:none}\n.reqrole{font-size:11px;color:#7C3AED;font-weight:600}\n.warn{font-size:11.5px;color:#B42318;margin-top:6px}\n.x{border:0;background:transparent;font-size:18px;color:#98A2B3;cursor:pointer;line-height:1;font-family:inherit}\n.host{position:relative;height:100%}\n`,\n\n  render(d2) {\n    const tasksH = d2.data()\n    const runsH = d2.data('runs')\n    const tplH = d2.data('templates')\n    const pplH = d2.data('people')\n\n    const tasks = tasksH ? (tasksH.records() || []) : []\n    const runs = runsH ? (runsH.records() || []) : []\n    const tpls = tplH ? (tplH.records() || []) : []\n    const people = pplH ? (pplH.records() || []) : []\n\n    const role = sessRead(d2, 'userRole')\n    const uname = sessRead(d2, 'userName')\n    const uid_ = sessRead(d2, 'userId')\n\n    const isMine = t => {\n      if (t.assigneeId || t.assigneeName) {\n        if (uid_ && t.assigneeId === uid_) return true\n        if (uname && t.assigneeName === uname) return true\n        return false\n      }\n      return !t.requiredRole || t.requiredRole === role\n    }\n\n    const tasksOf = rid => tasks.filter(t => t.runId === rid).sort((a, b) => (a.seq || 0) - (b.seq || 0))\n\n    const mineRun = {}\n    tasks.forEach(t => { if (t.status === 'active' && isMine(t)) mineRun[t.runId] = 1 })\n\n    const tab = d2.state.tab || 'mine'\n    const listed = tab === 'mine'\n      ? runs.filter(r => mineRun[r.runId])\n      : (tab === 'done' ? runs.filter(r => r.status === 'done') : runs.filter(r => r.status !== 'done'))\n\n    let sel = d2.state.selRun\n    if (!sel || !listed.filter(r => r.runId === sel).length) sel = listed.length ? listed[0].runId : null\n    const run = runs.filter(r => r.runId === sel)[0] || null\n\n    const draft = d2.state.draft || {}\n    const mineCount = Object.keys(mineRun).length\n\n    const holderOf = r => {\n      const names = []\n      tasksOf(r.runId).filter(t => t.status === 'active').forEach(t => {\n        const nm = t.assigneeName || (t.requiredRole ? 'qualquer ' + t.requiredRole : '')\n        if (nm && names.indexOf(nm) < 0) names.push(nm)\n      })\n      return names\n    }\n\n    const runRow = r => {\n      const rts = tasksOf(r.runId)\n      const doneN = rts.filter(t => t.status === 'done').length\n      const pct = rts.length ? Math.round(doneN / rts.length * 100) : 0\n      const names = holderOf(r)\n      return '<div class=\"runrow\" data-run=\"' + esc(r.runId) + '\" data-key=\"' + esc(r.runId) + '\"' + (r.runId === sel ? ' data-on' : '') + '>' +\n        '<div class=\"rname\">' + esc(r.title || 'Sem título') + '</div>' +\n        '<span class=\"badge ' + (r.status === 'done' ? 'done' : 'running') + '\">' + esc(r.status || '') + '</span>' +\n        (mineRun[r.runId] ? ' <span class=\"badge mine\">com você</span>' : '') +\n        (r.status === 'done'\n          ? '<div class=\"holder\">Concluído</div>'\n          : (names.length ? '<div class=\"holder\">Com <b>' + esc(names.join(', ')) + '</b></div>'\n                          : '<div class=\"holder\" style=\"color:#B42318\">Nenhuma etapa ativa</div>')) +\n        '<div class=\"bar\"><div class=\"fill\" style=\"width:' + pct + '%\"></div></div>' +\n        '<div class=\"meta\" style=\"margin-top:5px\">' + doneN + '/' + rts.length + ' etapas</div></div>'\n    }\n\n    const nodeOfRun = (r, nodeId) => {\n      if (!r || !Array.isArray(r.nodes)) return null\n      return r.nodes.filter(n => n.id === nodeId)[0] || null\n    }\n\n    const stepHtml = (t, i) => {\n      const n = nodeOfRun(run, t.nodeId)\n      const type = t.nodeType || 'input'\n      const fields = n && Array.isArray(n.fields) ? n.fields : []\n      const vals = t.values || {}\n      const mine = isMine(t)\n      const state = t.status === 'done' ? 'is-done' : (t.status === 'active' ? 'is-now' : 'is-wait')\n\n      let inner = ''\n      if (t.status === 'done') {\n        inner = fields.length\n          ? fields.map(f => '<div class=\"kv\"><span>' + esc(f.title) + '</span><b>' + esc(vals[f.id] || '—') + '</b></div>').join('')\n          : '<div class=\"blank\">' + (type === 'validation' ? 'Aprovado.' : 'Sem campos.') + '</div>'\n        if (t.completedAt) inner += '<div class=\"meta\" style=\"margin-top:8px\">concluída ' + esc(fmtWhen(d2, t.completedAt)) + '</div>'\n      } else if (t.status === 'active' && mine) {\n        inner = (type === 'input'\n          ? (fields.length\n              ? fields.map(f =>\n                  '<div class=\"fld\"><label>' + esc(f.title) + '</label>' +\n                  '<input data-val=\"' + t.taskId + '|' + f.id + '\" value=\"' + esc((draft[t.taskId] || {})[f.id] || vals[f.id] || '') + '\" placeholder=\"preencher\"/></div>'\n                ).join('')\n              : '<div class=\"blank\">Este node não tem campos configurados.</div>')\n          : '<div class=\"fld\"><label>Role exigida</label><input value=\"' + esc(t.requiredRole || 'qualquer') + '\" disabled/></div>') +\n          '<button class=\"done\" data-done=\"' + esc(t.taskId) + '\"' + (d2.state.busy ? ' disabled' : '') + '>' +\n          (type === 'validation' ? 'Aprovar e enviar adiante' : 'Concluir e enviar adiante') + '</button>'\n      } else if (t.status === 'active') {\n        inner = '<div class=\"blank\">Aguardando <b>' + esc(t.assigneeName || t.requiredRole || 'responsável') + '</b> concluir esta etapa.</div>'\n      } else {\n        inner = '<div class=\"blank\">Aguardando as etapas anteriores.</div>'\n      }\n\n      return '<div class=\"step ' + state + '\" data-key=\"' + esc(t.taskId) + '\">' +\n        '<div class=\"pin\">' + (t.status === 'done' ? '✓' : (i + 1)) + '</div>' +\n        '<div class=\"sbox\">' +\n        '<div class=\"srow\"><span class=\"dot\" style=\"background:' + (TYPE_COLOR[type] || '#98A2B3') + '\"></span>' +\n        '<span class=\"ctype\">' + esc(TYPE_LABEL[type] || type) + '</span>' +\n        '<span class=\"spacer\"></span>' +\n        (t.status === 'active' ? '<span class=\"badge ' + (mine ? 'mine' : 'running') + '\">' + (mine ? 'sua vez' : 'em curso') + '</span>' : '') +\n        '</div>' +\n        '<div class=\"who\">' + (t.assigneeName ? '<b>' + esc(t.assigneeName) + '</b>' : 'sem responsável') +\n        (t.requiredRole ? ' · exige ' + esc(t.requiredRole) : '') + '</div>' +\n        inner + '</div></div>'\n    }\n\n    let detail = ''\n    if (!run) {\n      detail = '<div class=\"empty\">Selecione um fluxo à esquerda.</div>'\n    } else {\n      const rts = tasksOf(run.runId)\n      detail = '<div class=\"dhd\"><div class=\"dtitle\">' + esc(run.title || 'Sem título') + '</div>' +\n        '<div class=\"dsub\">' + esc(run.templateName || '') + ' · v' + (run.templateVersion || 1) +\n        (run.startedByName ? ' · iniciado por ' + esc(run.startedByName) : '') +\n        (run.startedAt ? ' em ' + esc(fmtWhen(d2, run.startedAt)) : '') + '</div>' +\n        '<div class=\"meta\" style=\"margin-top:6px\">' + esc(run.runId) + '</div></div>' +\n        (rts.length ? '<div class=\"tl\">' + rts.map(stepHtml).join('') + '</div>'\n                    : '<div class=\"blank\">Este fluxo não tem etapas.</div>')\n    }\n\n    let modal = ''\n    if (d2.state.step === 'tpl') {\n      modal = '<div class=\"ovl\"><div class=\"modal\">' +\n        '<div class=\"mhd\">Iniciar fluxo<span class=\"spacer\"></span><button class=\"x\" data-close>×</button></div>' +\n        '<div class=\"mbody\">' +\n        (tpls.length\n          ? tpls.map(t => '<div class=\"tpl\" data-tpl=\"' + t._id + '\" data-key=\"' + t._id + '\">' +\n              '<div class=\"tname\">' + esc(t.name || 'Sem nome') + '</div>' +\n              '<div class=\"meta\">' + esc(t.key || '') + ' · v' + (t.version || 1) + ' · ' +\n              Math.max(0, (Array.isArray(t.nodes) ? t.nodes.length : 0) - 1) + ' etapas</div></div>').join('')\n          : '<div class=\"blank\">Nenhum template criado ainda.</div>') +\n        '</div></div></div>'\n    }\n    if (d2.state.step === 'plan') {\n      const tpl = d2.state.planTpl || {}\n      const nodes = Array.isArray(tpl.nodes) ? tpl.nodes : []\n      const edges = Array.isArray(tpl.edges) ? tpl.edges : []\n      const steps = orderNodes(nodes, edges)\n      const assign = d2.state.assign || {}\n      const missing = steps.filter(n => !assign[n.id]).length\n      const pstep = (n, i) => {\n        const pool = n.type === 'validation' && n.role ? people.filter(p => p.role === n.role) : people\n        const opts = ['<option value=\"\">Selecione a pessoa…</option>'].concat(\n          pool.map(p => '<option value=\"' + esc(p.personId) + '\"' + (assign[n.id] === p.personId ? ' selected' : '') + '>' +\n            esc(p.name) + ' — ' + esc(p.role) + '</option>')\n        ).join('')\n        return '<div class=\"pstep' + (assign[n.id] ? '' : ' miss') + '\" data-key=\"' + n.id + '\">' +\n          '<div class=\"srow\"><span class=\"snum\">' + (i + 1) + '</span>' +\n          '<span class=\"dot\" style=\"background:' + (TYPE_COLOR[n.type] || '#98A2B3') + '\"></span>' +\n          '<span class=\"ctype\">' + esc(TYPE_LABEL[n.type] || n.type) + '</span>' +\n          (n.type === 'validation' && n.role ? '<span class=\"spacer\"></span><span class=\"reqrole\">exige ' + esc(n.role) + '</span>' : '') +\n          '</div><div class=\"fld\"><label>Responsável</label>' +\n          '<select data-assign=\"' + n.id + '\"' + (pool.length ? '' : ' disabled') + '>' + opts + '</select></div>' +\n          (pool.length ? '' : '<div class=\"warn\">Nenhuma pessoa cadastrada com a role ' + esc(n.role || '—') + '.</div>') +\n          '<div class=\"meta\">' + esc(n.id) + '</div></div>'\n      }\n      modal = '<div class=\"ovl\"><div class=\"modal\">' +\n        '<div class=\"mhd\">Planejar fluxo<span class=\"spacer\"></span><button class=\"x\" data-close>×</button></div>' +\n        '<div class=\"mbody\">' +\n        '<div class=\"fld\"><label>Título da execução</label>' +\n        '<input data-title value=\"' + esc(d2.state.planTitle || '') + '\" placeholder=\"ex.: Compra de notebooks — Q3\"/></div>' +\n        '<div class=\"sect\" style=\"margin-top:16px\">Etapas e responsáveis</div>' +\n        (steps.length ? steps.map(pstep).join('') : '<div class=\"blank\">Este template não tem etapas.</div>') +\n        '</div><div class=\"mft\"><button class=\"ghost\" data-back>Voltar</button><span class=\"spacer\"></span>' +\n        (missing ? '<span class=\"meta\">' + missing + ' sem responsável</span>' : '') +\n        '<button class=\"primary\" data-confirm' + (missing || !steps.length || d2.state.busy ? ' disabled' : '') + '>Iniciar fluxo</button>' +\n        '</div></div></div>'\n    }\n\n    const tabBtn = (id, label, count) =>\n      '<button class=\"tab\" data-tab=\"' + id + '\"' + (tab === id ? ' data-on' : '') + '>' + label +\n      (count === null ? '' : '<span class=\"cnt' + (count ? '' : ' zero') + '\">' + count + '</span>') + '</button>'\n\n    return '<div class=\"host\">' + modal + '<div class=\"ib\">' +\n      '<div class=\"top\"><span class=\"ttl\">Fluxos</span>' +\n      '<span class=\"rolechip\">' + esc(uname || 'sem usuário') + (role ? ' · ' + esc(role) : '') + '</span>' +\n      '<span class=\"spacer\"></span>' +\n      '<button class=\"primary\" data-start' + (d2.state.busy ? ' disabled' : '') + '>' +\n      (d2.state.busy ? 'Processando…' : 'Iniciar Fluxo') + '</button></div>' +\n      '<div class=\"body\"><div class=\"rail\">' +\n      '<div class=\"tabs\">' + tabBtn('mine', 'Comigo', mineCount) +\n      tabBtn('all', 'Em andamento', null) + tabBtn('done', 'Concluídos', null) + '</div>' +\n      '<div class=\"list\">' +\n      (listed.length ? listed.map(runRow).join('')\n        : '<div class=\"blank\">' + (tab === 'mine' ? 'Nada pendente com você.' : (tab === 'done' ? 'Nenhum fluxo concluído.' : 'Nenhum fluxo em andamento.')) + '</div>') +\n      '</div></div>' +\n      '<div class=\"detail\">' + detail + '</div>' +\n      '</div></div></div>'\n  },\n\n  async mounted(d2) {\n    const hs = [d2.data(), d2.data('runs'), d2.data('templates'), d2.data('people')].filter(Boolean)\n    for (let i = 0; i < hs.length; i++) { try { await hs[i].refresh() } catch (err) { d2.log('refresh failed', err) } }\n\n    const personById = id => {\n      const ppl = d2.data('people')\n      return (ppl ? ppl.records() || [] : []).filter(p => p.personId === id)[0] || null\n    }\n\n    const startRun = async (tpl, title, assign) => {\n      const runsH = d2.data('runs'), tasksH = d2.data()\n      if (!runsH || !tasksH || !tpl) return null\n      const role = sessRead(d2, 'userRole')\n      const uname = sessRead(d2, 'userName')\n      const nodes = Array.isArray(tpl.nodes) ? tpl.nodes : []\n      const edges = Array.isArray(tpl.edges) ? tpl.edges : []\n      const runId = uid('run')\n\n      await runsH.create({\n        runId: runId, title: title || tpl.name || 'Sem título', status: 'running',\n        templateId: tpl.key || '', templateDbId: String(tpl._id || ''),\n        templateName: tpl.name || '', templateVersion: Number(tpl.version) || 1,\n        nodes: nodes, edges: edges,\n        startedByRole: role, startedByName: uname, startedAt: nowIso()\n      })\n\n      let run = null\n      for (let i = 0; i < 12 && !run; i++) {\n        try { await runsH.refresh() } catch (err) { d2.log('refresh failed', err) }\n        run = (runsH.records() || []).filter(r => r.runId === runId)[0] || null\n        if (!run) await sleep(160)\n      }\n      const runDbId = run ? String(run._id) : ''\n\n      const startIds = nodes.filter(n => n.type === 'start').map(n => n.id)\n      let firstIds = edges.filter(e => startIds.indexOf(e.from) >= 0).map(e => e.to)\n      const work = orderNodes(nodes, edges)\n      if (!firstIds.length && work.length) firstIds = [work[0].id]\n\n      for (let i = 0; i < work.length; i++) {\n        const n = work[i]\n        const active = firstIds.indexOf(n.id) >= 0\n        const p = personById((assign || {})[n.id])\n        await tasksH.create({\n          taskId: uid('tsk'), runId: runId, runDbId: runDbId,\n          nodeId: n.id, nodeType: n.type,\n          status: active ? 'active' : 'pending',\n          requiredRole: n.role || '',\n          assigneeId: p ? p.personId : '',\n          assigneeName: p ? p.name : '',\n          assigneeRole: p ? p.role : '',\n          values: {}, outcome: '', seq: i,\n          createdAt: nowIso(), activatedAt: active ? nowIso() : ''\n        })\n      }\n      try { await tasksH.refresh() } catch (err) { d2.log('refresh failed', err) }\n      return runId\n    }\n\n    const completeTask = async t => {\n      const tasksH = d2.data(), runsH = d2.data('runs')\n      if (!tasksH || !t) return\n      const draft = (d2.state.draft || {})[t.taskId] || {}\n\n      await tasksH.update({\n        _id: t._id, status: 'done', completedAt: nowIso(),\n        values: Object.assign({}, t.values || {}, draft),\n        outcome: t.nodeType === 'validation' ? 'approved' : 'submitted'\n      })\n\n      const run = (runsH ? runsH.records() || [] : []).filter(r => r.runId === t.runId)[0] || null\n      const edges = run && Array.isArray(run.edges) ? run.edges : []\n      const nextIds = edges.filter(e => e.from === t.nodeId).map(e => e.to)\n\n      try { await tasksH.refresh() } catch (err) { d2.log('refresh failed', err) }\n      const siblings = (tasksH.records() || []).filter(x => x.runId === t.runId)\n      for (let i = 0; i < siblings.length; i++) {\n        const s = siblings[i]\n        if (s.status === 'pending' && nextIds.indexOf(s.nodeId) >= 0) {\n          await tasksH.update({ _id: s._id, status: 'active', activatedAt: nowIso() })\n        }\n      }\n      try { await tasksH.refresh() } catch (err) { d2.log('refresh failed', err) }\n\n      const left = (tasksH.records() || []).filter(x => x.runId === t.runId && x.status !== 'done')\n      if (!left.length && run && runsH) {\n        await runsH.update({ _id: run._id, status: 'done', finishedAt: nowIso() })\n        try { await runsH.refresh() } catch (err) { d2.log('refresh failed', err) }\n      }\n    }\n\n    d2.on('[data-tab]', 'click', (e, el) => d2.setState({ tab: el.dataset.tab, selRun: null }))\n    d2.on('[data-run]', 'click', (e, el) => d2.setState({ selRun: el.dataset.run }))\n\n    d2.on('[data-start]', 'click', async () => {\n      const tplH = d2.data('templates'), ppl = d2.data('people')\n      if (tplH) { try { await tplH.refresh() } catch (err) { d2.log('refresh failed', err) } }\n      if (ppl) { try { await ppl.refresh() } catch (err) { d2.log('refresh failed', err) } }\n      d2.setState({ step: 'tpl', planTpl: null, planTitle: '', assign: {} })\n    })\n\n    d2.on('[data-close]', 'click', () => d2.setState({ step: null }))\n    d2.on('[data-back]', 'click', () => d2.setState({ step: 'tpl' }))\n\n    d2.on('[data-tpl]', 'click', (e, el) => {\n      const tplH = d2.data('templates')\n      const tpl = (tplH ? tplH.records() || [] : []).filter(t => String(t._id) === el.dataset.tpl)[0]\n      if (!tpl) return\n      d2.setState({ step: 'plan', planTpl: tpl, planTitle: tpl.name || '', assign: {} })\n    })\n\n    d2.on('[data-title]', 'input', (e, el) => { d2.state.planTitle = el.value })\n\n    d2.on('[data-assign]', 'change', (e, el) => {\n      const assign = Object.assign({}, d2.state.assign || {})\n      assign[el.dataset.assign] = el.value\n      d2.setState({ assign: assign })\n    })\n\n    d2.on('[data-confirm]', 'click', async () => {\n      const tpl = d2.state.planTpl\n      if (!tpl) return\n      const title = d2.state.planTitle\n      const assign = d2.state.assign || {}\n      d2.setState({ step: null, busy: true })\n      let newId = null\n      try { newId = await startRun(tpl, title, assign) } catch (err) { d2.log('startRun failed', err) }\n      d2.setState({ busy: false, planTpl: null, planTitle: '', assign: {}, tab: 'all', selRun: newId })\n    })\n\n    d2.on('[data-val]', 'input', (e, el) => {\n      const parts = String(el.dataset.val || '').split('|')\n      const draft = Object.assign({}, d2.state.draft || {})\n      draft[parts[0]] = Object.assign({}, draft[parts[0]] || {})\n      draft[parts[0]][parts[1]] = el.value\n      d2.state.draft = draft\n    })\n\n    d2.on('[data-done]', 'click', async (e, el) => {\n      const tasksH = d2.data()\n      const t = (tasksH ? tasksH.records() || [] : []).filter(x => x.taskId === el.dataset.done)[0]\n      if (!t) return\n      d2.setState({ busy: true, selRun: t.runId, tab: 'all' })\n      try { await completeTask(t) } catch (err) { d2.log('completeTask failed', err) }\n      const draft = Object.assign({}, d2.state.draft || {})\n      delete draft[t.taskId]\n      d2.setState({ busy: false, draft: draft })\n    })\n  }\n}"}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}},"runs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"runs"}}}},"templates":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"templates"}}}},"people":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"people"}}}},"session":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"session"}}}}}}}}}},{"_id":"6a8381f0e90bd5fa9dd981e3","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mc-uploader","package":"bigaton-playground","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 CHUNK = 20;\n\nfunction agrupar(files) {\n  const m = new Map();\n  for (const f of files) {\n    const rel = f.webkitRelativePath || f.name;\n    const partes = rel.split('/');\n    const dir = partes.slice(0, -1).join('/');\n    if (!m.has(dir)) m.set(dir, []);\n    m.get(dir).push({ file: f, path: rel, name: partes[partes.length - 1] });\n  }\n  return m;\n}\n\nfunction esc(s) { return String(s == null ? '' : s).replace(/[<>&]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c])); }\n\nfunction kb(n) { return n < 1024 ? n + ' B' : n < 1048576 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(1) + ' MB'; }\n\nfunction um(r) { return Array.isArray(r) ? r[0] : r; }\n\nexport default {\n  css: `\n  :host,.wrap{font:13px/1.45 ui-sans-serif,system-ui,sans-serif;color:#e7e7ea}\n  .wrap{height:100%;box-sizing:border-box;padding:20px;background:#16161a;overflow:auto}\n  h2{margin:0 0 4px;font-size:16px;font-weight:700}\n  .sub{color:#8b8b96;margin-bottom:18px}\n  .card{background:#1e1e24;border:1px solid #2c2c34;border-radius:10px;padding:14px;margin-bottom:12px}\n  .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n  button{font:600 13px ui-sans-serif,system-ui,sans-serif;padding:8px 14px;border-radius:7px;\n         border:1px solid #3a3a44;background:#2a2a32;color:#e7e7ea;cursor:pointer}\n  button:hover:not(:disabled){background:#34343e}\n  button:disabled{opacity:.45;cursor:default}\n  button.go{background:#3b6fe0;border-color:#3b6fe0;color:#fff}\n  button.go:hover:not(:disabled){background:#4a7cf0}\n  input[type=file]{display:none}\n  .stat{display:flex;gap:26px;flex-wrap:wrap;margin-top:10px}\n  .stat div span{display:block;color:#8b8b96;font-size:11px;text-transform:uppercase;letter-spacing:.5px}\n  .stat div b{font-size:19px;font-weight:700}\n  .bar{height:7px;background:#2c2c34;border-radius:4px;overflow:hidden;margin-top:12px}\n  .bar i{display:block;height:100%;background:#3b6fe0;transition:width .2s}\n  .log{max-height:230px;overflow:auto;background:#131317;border:1px solid #2c2c34;border-radius:8px;\n       padding:9px 11px;font:11px/1.6 ui-monospace,monospace;color:#9a9aa6;margin-top:12px}\n  .log div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n  .err{color:#ff8080}\n  .ok{color:#6ee7a0}\n  label.chk{display:flex;gap:7px;align-items:center;color:#b9b9c4;cursor:pointer;user-select:none}\n  `,\n\n  render(d2) {\n    const s = d2.state;\n    const total = s.total || 0;\n    const feitos = s.feitos || 0;\n    const pct = total ? Math.round((feitos / total) * 100) : 0;\n    const linhas = (s.log || []).slice(-160);\n    return `<div class=\"wrap\">\n      <h2>MapleCloset — asset uploader</h2>\n      <div class=\"sub\">Pick an asset root folder. The tree is mirrored into the Drive exactly as it sits on disk.</div>\n\n      <div class=\"card\">\n        <div class=\"row\">\n          <button class=\"pick\">Choose folder…</button>\n          <button class=\"go run\" ${!s.total || s.rodando ? 'disabled' : ''}>\n            ${s.rodando ? 'Uploading…' : (s.simular ? 'Dry run' : 'Upload')}\n          </button>\n          <label class=\"chk\"><input type=\"checkbox\" class=\"sim\" ${s.simular ? 'checked' : ''}/> dry run (no writes)</label>\n          <label class=\"chk\"><input type=\"checkbox\" class=\"res\" ${s.retomar ? 'checked' : ''}/> skip already uploaded</label>\n        </div>\n        <input type=\"file\" class=\"dir\" webkitdirectory directory multiple/>\n        <div class=\"stat\">\n          <div><span>root</span><b>${esc(s.raiz || '—')}</b></div>\n          <div><span>files</span><b>${total}</b></div>\n          <div><span>folders</span><b>${s.pastas || 0}</b></div>\n          <div><span>size</span><b>${total ? kb(s.bytes || 0) : '—'}</b></div>\n          <div><span>done</span><b>${feitos}</b></div>\n          <div><span>errors</span><b class=\"${s.erros ? 'err' : ''}\">${s.erros || 0}</b></div>\n        </div>\n        <div class=\"bar\"><i style=\"width:${pct}%\"></i></div>\n      </div>\n\n      ${linhas.length ? `<div class=\"log\">${linhas.map((l, i) =>\n        `<div data-key=\"${i}\" class=\"${l.t || ''}\">${esc(l.m)}</div>`).join('')}</div>` : ''}\n    </div>`;\n  },\n\n  mounted(d2) {\n    const s = d2.state;\n    s.log = []; s.total = 0; s.feitos = 0; s.erros = 0; s.pastas = 0;\n    s.simular = true; s.retomar = true; s.rodando = false;\n    let arquivos = null;\n\n    const diz = (m, t) => { s.log = [...(s.log || []), { m, t }]; };\n\n    d2.on('.pick', 'click', () => { const el = d2.$('.dir'); if (el) el.click(); });\n    d2.on('.sim', 'change', (e, el) => { s.simular = el.checked; });\n    d2.on('.res', 'change', (e, el) => { s.retomar = el.checked; });\n\n    d2.on('.dir', 'change', (e, el) => {\n      const fs = Array.from(el.files || []);\n      if (!fs.length) return;\n      arquivos = agrupar(fs);\n      s.raiz = (fs[0].webkitRelativePath || '').split('/')[0] || '(root)';\n      s.total = fs.length;\n      s.pastas = arquivos.size;\n      s.bytes = fs.reduce((a, f) => a + f.size, 0);\n      s.feitos = 0; s.erros = 0;\n      s.log = [{ m: `picked ${fs.length} file(s) in ${arquivos.size} folder(s) under \"${s.raiz}\"` }];\n      for (const dir of Array.from(arquivos.keys()).slice(0, 8)) diz('  ' + dir + '/', 'ok');\n      if (arquivos.size > 8) diz(`  … +${arquivos.size - 8} more folder(s)`);\n    });\n\n    d2.on('.run', 'click', async () => {\n      if (!arquivos || s.rodando) return;\n      s.rodando = true; s.feitos = 0; s.erros = 0;\n      const seco = s.simular;\n      try {\n        const drive = seco ? null : d2.drive(d2.param('driveRef', 'maplecloset-assets'));\n        const ds = d2.data('assets');\n\n        // RETOMADA: o que ja subiu, lido da tabela de juncao. Sem isto um segundo\n        // envio duplica arquivo E linha -- e o Drive nao deduplica por nome.\n        let ja = new Set();\n        if (s.retomar && ds) {\n          await ds.refresh();                       // loadAll no-op silencioso se nao estiver Ready\n          const rs = await ds.loadAll();\n          ja = new Set((rs || []).map(r => r.path).filter(Boolean));\n          diz(`already in the table: ${ja.size} path(s)`);\n        }\n\n        const cache = new Map();\n        async function pasta(dir) {\n          if (!dir) return null;\n          if (cache.has(dir)) return cache.get(dir);\n          const p = dir.split('/');\n          const pai = await pasta(p.slice(0, -1).join('/'));\n          const nome = p[p.length - 1];\n          let f = null;\n          if (!seco) {\n            const args = pai ? { name: nome, folder: pai } : { name: nome };\n            f = um(await drive.invoke('createFolder', args));\n          }\n          cache.set(dir, f);\n          diz(`folder ${dir}/`, 'ok');\n          return f;\n        }\n\n        for (const [dir, itens] of arquivos) {\n          const pend = itens.filter(i => !ja.has(i.path));\n          if (!pend.length) { s.feitos += itens.length; continue; }\n          const dest = await pasta(dir);\n          for (let i = 0; i < pend.length; i += CHUNK) {\n            const lote = pend.slice(i, i + CHUNK);\n            try {\n              let out = [];\n              if (!seco) {\n                out = await drive.uploadFromFiles(lote.map(x => x.file),\n                  dest ? { folder: dest } : {}) || [];\n              }\n              if (!seco && ds) {\n                await Promise.all(lote.map((x, k) => ds.create({\n                  path: x.path,\n                  category: x.path.split('/')[0],\n                  folder: dir,\n                  filename: x.name,\n                  fileUrl: (out[k] && (out[k].fileUrl || (out[k].params || {}).fileUrl)) || '',\n                  bytes: x.file.size,\n                  uploadedAt: new Date().toISOString()\n                })));\n              }\n              s.feitos += lote.length;\n            } catch (err) {\n              s.erros += lote.length;\n              diz(`FAIL ${dir}/ (${lote.length}) — ${err && err.message ? err.message : err}`, 'err');\n            }\n          }\n        }\n        diz(seco ? `dry run finished — ${s.feitos} file(s) would be uploaded into ${cache.size} folder(s)`\n                 : `finished — ${s.feitos} uploaded, ${s.erros} failed`, s.erros ? 'err' : 'ok');\n      } catch (err) {\n        s.erros++; diz(`ABORTED — ${err && err.message ? err.message : err}`, 'err');\n      } finally {\n        s.rodando = false;\n      }\n    });\n  }\n};\n"}},"driveRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"driveRef"}}}},"assets":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"assets"}}}}}}}}}},{"_id":"6a8387f1e1c15d8b3027c5da","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mcbaseviewer","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// `acc` do dump e um saco unico; no JOGO sao TRES slots independentes, distinguidos pelo islot\n// (Af=face acc, Ay=eye acc, Ae=earring) -- que e o mesmo criterio canonico usado no resto do projeto.\n// Separar nao e so organizacao da grade: com um slot so, oculos e brinco nunca poderiam coexistir.\nconst CATS=['hair','face','cape','cap','weapon','outfit','coat','pants','shoes','glove','offhand','accface','acceye','accear'];\n// `offhand` NAO e \"shield\": pelo BLUEPRINT 7.1 o islot `Si` e o slot de off-hand, e ele aceita\n// shield OU katara. Com a katara na mao principal (como estava) nao dava para montar um Dual\n// Blade -- adaga na principal + katara na off-hand -- e dava para usar katara e shield juntos.\n// `coat` e o nome do arquivo dos devs do Maple; na comunidade (e na nossa interface) e `top`.\nconst ROTULO={accface:'face acc',acceye:'eye acc',accear:'earring',offhand:'off-hand',coat:'top'};\n// A coluna de equipados tem ordem FIXA, nao a ordem em que as pecas foram equipadas.\nconst ORDEM_EQ=['hair','face','cap','accear','accface','acceye','outfit','coat','pants','glove','weapon','offhand','cape','shoes'];\n// ANEL DE EQUIPADOS. As posicoes saem do ESBOCO do usuario (artboard Testes_mc), nao de invencao:\n// 15 celulas em volta do personagem, 4 na coluna esquerda, 4 na direita, 7 na fileira de baixo.\n// A celula `__skin__` NAO e slot de equipamento -- e o seletor de skin, que no esboco divide a\n// mesma fileira. Ela existe aqui para o anel ser UMA lista so; quem trata a excecao e o render.\n// O esboco rotulava a quarta celula da esquerda como EARRINGS pela segunda vez. Por eliminacao\n// e GLOVE: sao 14 slots de equipamento + skin = 15 celulas, e glove era o unico que faltava.\nconst ANEL_ESQ=['accface','acceye','accear','glove'];\nconst ANEL_DIR=['cap','outfit','coat','pants'];\nconst ANEL_BAIXO=['cape','weapon','hair','__skin__','face','offhand','shoes'];\n// Vocabulario DO ESBOCO, que nao e o do `ROTULO` da barra de categorias: la `coat` e \"top\" e\n// `outfit` e \"outfit\"; aqui sao TOP e OVERALL, que e como o jogo chama.\nconst ANEL_ROT={accface:'Face Acc',acceye:'Eye Acc',accear:'Earrings',glove:'Glove',\n  cap:'Hat',outfit:'Overall',coat:'Top',pants:'Bottom',\n  cape:'Cape',weapon:'Weapon',hair:'Hair',face:'Face',offhand:'Off-Hand',shoes:'Shoes',\n  __skin__:'Skin'};\n// Paleta de cor do CABELO. Medido no catalogo inteiro: hair e a UNICA categoria com variante de\n// cor (2.176 estilos); as outras 13 tem exatamente 1 cor por estilo. E a cor NAO e a posicao na\n// lista, e o ULTIMO DIGITO do id -- o estilo 00037361 comeca no digito 1 (vermelho), nao no 0.\n// Um unico estilo (00030920) tem digitos 8 e 9, que sao copias do 00030930/31; ate serem tirados\n// do indice eles caem no quadradinho neutro com o numero, em vez de mentir uma cor.\nconst PALETA=['#1c1c1c','#e8352e','#f97316','#fbbf24','#22c55e','#2563eb','#a855f7','#b0632c'];\nconst NOMECOR=['black','red','orange','yellow','green','blue','purple','brown'];\n// Grupos do painel de Action. MANUAL de proposito por enquanto (decisao do usuario): se um dia\n// virar coluna da tabela `mc-action`, some daqui e passa a sair do dado.\nconst MOVENDO={walk1:1,walk2:1,jump:1,fly:1,ladder:1,rope:1};\nconst GRUPOS=['Stand','Moving','Swing','Stab','Shoot'];\nfunction grupoUI(a){\n  if(a.indexOf('swing')===0)return 'Swing';\n  if(a.indexOf('stab')===0)return 'Stab';   // `proneStab` NAO cai aqui: comeca com 'prone'\n  if(a.indexOf('shoot')===0)return 'Shoot';\n  return MOVENDO[a]?'Moving':'Stand';\n}\nconst CATDE=Object.create(null);   // id do item -> categoria real; e o que faz a aba All equipar certo\n// ESCONDIDOS DA NAVEGACAO -- nao da base. Eluna Earrings: 8 variantes x 5 niveis = 40 ids\n// (01032270..01032309), e os 5 niveis de cada variante sao a MESMA arte -- medido: `geo`, `uv` e\n// `px` identicos, e os 5 PNGs de atlas com o MESMO sha1, byte a byte. Ficam so os Lv.1 (ultimo\n// digito 0 ou 5), tirando 32 linhas que o randomize sorteava com peso desproporcional.\n// So a LISTA navegavel encolhe: `META` e `CATDE` continuam populados para os 40, entao um desses\n// ids ainda renderiza normalmente se vier de fora (histórico de undo, estado salvo, link).\n// PORTRAIT por AI. O prompt fica aqui em cima de proposito: a fase 2 e calibrar ESTE texto, e\n// procurar por ele no meio do render seria pior. O `draw` devolve {data:[{url}]} com url =\n// data URI de ~3,2 MB; quem converte para arquivo do Drive e devolve URL curta e o HANDLER.\nconst RUSH_PORTRAIT='mcportrait';\nconst STORE_PORTRAIT='mcportraitimg';\n// MONTAGEM: imagem 1 = REFERENCIA DE ESTILO (um retrato ACABADO), imagem 2 = sprite. A ref e\n// prependida no handler, server-side, para ser trocavel sem republicar isto.\n// A REGRA, apurada em 8 geracoes ao longo de duas sessoes -- vale mais que qualquer frase do\n// prompt: O QUE ESTA NA IMAGEM 1 E O QUE O MODELO PRESERVA; a imagem 2 so ajusta detalhes.\n//   imagem 1 = sprite    -> a saida e o sprite ampliado (pixelada). 1 limpo / 4 pixelados.\n//   imagem 1 = manequim  -> a saida sai CARECA e sem chapeu; na sessao passada o mesmo defeito\n//                           apareceu como \"retrato sem rosto\". O manequim NUNCA serve na\n//                           posicao 1: estar vazio e a propriedade que o define, e e ela que o\n//                           modelo preserva.\n//   imagem 1 = UM retrato acabado -> as vezes limpo, mas VAZA: uma geracao saiu com o cabelo,\n//                           os oculos e o moletom DA REFERENCIA, e do personagem so a mascara.\n//                           A frase \"do NOT copy its hair/clothes/colours\" nao segura.\n// CALIBRACAO DO TEXTO (medida contra as 6 refs, nao no olho). Tres frases que pareciam certas e\n// estavam empurrando para longe do alvo -- duas delas vindas do texto de estilo original:\n//   \"Clean black outlines\"        -> contorno saia em 6; as refs medem 27. Trocado por escuro\n//                                    TINGIDO na cor local, nunca preto puro.\n//   \"simple clean background\"     -> fundo chapado, variacao 4,4 contra 64,5 das refs. Trocado\n//                                    por gradiente + vinheta + brilhos.\n//   \"crop at the base of the neck\"-> o modelo leu como MOSTRAR MENOS e desenhou uma cabeca\n//                                    pequena com margem sobrando (largura 73,5 contra 88,7).\n//                                    So virou com instrucao FISICA e verificavel: a cabeca\n//                                    ESTOURA o quadro, o cabelo e cortado pelas bordas.\n// Resultado apos as tres: topo 4,1 / queixo 83,2 / larg 86,9 / fundo 49,5 / contorno 19 -- os\n// seis eixos dentro do desvio das refs. Instrucao negativa (\"nao afaste\") nao movia nada.\n//   imagem 1 = FOLHA com as SEIS refs -> resolve. Com seis personagens diferentes lah dentro, a\n//                           unica propriedade comum que sobra para preservar e o TRACO. E a\n//                           mesma regra trabalhando a nosso favor, em vez de contra.\n// Quatro versoes de prompt nao reverteram a pixelacao; inverter a posicao reverteu na primeira\n// tentativa. Se algum dia isto voltar a pixelar, olhe a ORDEM antes de mexer no texto.\n// Nao vai nome de item no texto: nome enviesa (a arma \"Chaos Devil's Sunrise\" saiu com o nome\n// escrito na lamina). O sprite carrega a forma e a cor; o texto so descreve estilo e enquadramento.\n// O texto e CURTO de proposito. As refs de estilo sao a receita; descreve-la com adjetivos meus e\n// uma transcricao lossy que COMPETE com a imagem em vez de reforca-la.\n// A linha que separa RENDERIZACAO de CONTEUDO e a mais importante do prompt: sem ela o modelo\n// puxava tambem a roupa e a paleta da referencia (foi o que embaraiou o cabelo na variante C).\n// As refs nao sao alvo de cor -- cada uma saiu de um personagem DIFERENTE; o alvo e so o traco.\nconst PROMPT_BASE='Image 1 is a STYLE SHEET: six DIFFERENT characters drawn in the same art style. It defines ONLY the drawing style. None of those six is the subject. Do not copy any of their hairstyles, clothes, accessories or colours.\\n\\nImage 2 is a pixel-art sprite of the ONE character to draw. It is DATA, not artwork: take from it the hairstyle and hair colour, the facial features, the headwear, and the garments and their colours. Do not copy its pixels and do not trace it.\\n\\nOutput a SINGLE character portrait, not a grid and not a sheet: the character of image 2, drawn exactly in the style of image 1.\\n\\nFRAMING, copy it from image 1 exactly. The head is enormous and OVERFLOWS the frame: the hair is cut off by the left edge, by the right edge and by the top edge, with no background gap left between the hair and the sides. The chin sits at four fifths of the frame height and the shoulders enter through the two bottom corners, showing the collar and the top of the garment. Draw no arms, no hands and no full body.\\n\\nSTYLE, matching image 1: super-deformed chibi proportions, large voluminous hair, both eyes visible. Very large glossy eyes placed low on the face, wide apart, each with a bright white catchlight; no visible nose; very small mouth; soft blush on the cheeks. Hair carries a broad glossy highlight band across the crown. Outlines are thin and dark but tinted toward the local colour, never pure black. Crisp cel shading with soft gradients. Background is a soft vertical gradient with a gentle vignette and a few small sparkles, never a flat solid fill. No 3D render, no photorealism, no text, no watermark.';\n// DISPONIBILIDADE: quem decide o que aparece e o CADASTRO (artboard MapleCloset Item Registry),\n// publicado como projecao na tabela mc-hidden e lido aqui no boot.\n//\n// Ate 04/09 isto eram TRES filtros escritos neste arquivo: uma lista de ids (Eluna, Lumiere e 3\n// avulsos), a faixa de teste 0160/0169 do weapon, e a condicao derivada \"sem icone\" para hair e\n// face. O usuario pediu que o toggle do cadastro passasse a ser regra ABSOLUTA, entao a regra\n// saiu do codigo e virou dado. As condicoes nao se perderam: elas continuam sendo calculadas no\n// cadastro, mas como COLUNA e FILTRO -- ajudam a decidir, nao decidem.\n//\n// A troca foi provada como no-op: no dia da migracao a projecao devolvia EXATAMENTE os mesmos\n// 189 ids que estes tres filtros escondiam, conferido por diferenca de conjunto e nao por\n// contagem. Ver a secao 35 da doc.\nconst ESCONDIDO=new Set();\n// Itens que entraram na versao MAIS RECENTE do GMS -- so para o selo NEW na grade. Vem de um\n// arquivo estatico no Drive (ref `_novos` no mcindex), nao da tabela de escondidos: a projecao\n// de escondidos e reescrita inteira pelo cadastro (apaga+insere) e levaria estes ids junto.\nconst NOVO=new Set();\n// versao do catalogo que o cliente esta vendo. Vai junto no cache do look para um dia dar para\n// saber se o cache e de uma versao antiga; hoje so e GRAVADO, nada le.\nlet CATVER=0;\nconst HIDDEN_URL='https://bigaton-playground.data2apis.com/dataset/mc-hiddenDataSet';\n// Cadeado: os dois SVGs de UIAssets, inline. Sao ~300 bytes cada, entao virar arquivo custaria\n// uma requisicao por thumb para economizar nada. Sem `fill` no path: quem manda na cor e o CSS,\n// via `fill:currentColor` -- por isso o mesmo icone serve verde no palco e escuro no cantinho.\n// Seta girando do botao de reset da posicao. Mesmo tratamento dos outros: sem fill no path,\n// quem manda na cor e o CSS via fill:currentColor.\nconst DRESET='M12,5V1L7,6l5,5V7c3.31,0,6,2.69,6,6s-2.69,6-6,6s-6-2.69-6-6H4c0,4.42,3.58,8,8,8s8-3.58,8-8S16.42,5,12,5z';\n// Lupa do campo de busca, no padrao da janela de inventario do jogo (icone a direita do campo).\nconst DLUPA='M15.5,14h-.79l-.28-.27C15.41,12.59,16,11.11,16,9.5C16,5.91,13.09,3,9.5,3S3,5.91,3,9.5S5.91,16,9.5,16c1.61,0,3.09-.59,4.23-1.57l.27.28v.79l5,4.99L20.49,19L15.5,14z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5S11.99,14,9.5,14z';\n// Transporte (Loop / Once). Mesmo tratamento dos outros: sem fill no path, quem manda na cor e o\n// CSS via fill:currentColor -- e o que deixa o icone acender em verde quando o botao esta ligado\n// sem existir uma segunda copia do desenho. O Once e play + barra, que era o que o glifo antigo\n// desenhava com o texto -- a forma nao mudou, so deixou de ser tipografia.\nconst DPLAY='M8,5v14l11-7z';\nconst DPAUSE='M6,5h4v14H6zM14,5h4v14h-4z';\nconst DONCE='M6,18l8.5-6L6,6V18zM16,6h2v12h-2V6z';\n// Aspas simples de proposito: este arquivo ja derrubou o componente cinco vezes por uma crase\n// escrita onde nao podia. Concatenar custa nada e tira a chance de repetir.\nfunction icone(d){return '<svg class=\"lk\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"'+d+'\"/></svg>';}\n// Olho ABERTO -- o par do DOLHO (olho cortado) que ja existia para o item oculto. Mesma familia de\n// tracado, para os dois estados do botao de slots serem o mesmo desenho ligado e desligado.\n// ATENCAO AO SENTIDO: o icone mostra o que o clique VAI FAZER, nao o estado atual (decisao do\n// usuario, 01/09). Com os slots visiveis o botao exibe o olho CORTADO, porque apertar esconde;\n// com eles escondidos exibe o olho ABERTO. Ler isto como indicador de estado inverte tudo.\n// Aqui o d ja vem como MARCACAO pronta, e nao como um unico path: os icones de traco tem mais de\n// uma forma (o aberto tem um circulo, o cortado tem quatro paths), entao nao cabem no icone().\nconst DOLHOON='<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>';\nfunction iconeTraco(m){return '<svg class=\"lk lkt\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">'+m+'</svg>';}\n// SETAS E SINAIS como DESENHO, nao como caractere. Ate 02/09 a paginacao usava os glifos ‹ › « »\n// e o zoom usava - e +, e eram os UNICOS conteudos de texto num sistema em que todo o resto e\n// SVG. A posicao vertical de um glifo sai da metrica da fonte (ascendente, descendente, altura de\n// linha), que muda por maquina e nao se controla daqui -- por isso eles pareciam caidos mesmo com\n// a caixa do botao matematicamente centrada.\n// Todos os tracados abaixo sao SIMETRICOS em torno de (12,12) do viewBox 24x24: x e y vao de 6 a\n// 18 em todos. Centrado por construcao, sem depender de fonte nenhuma.\nconst DSETAE='<path d=\"M15 6 9 12l6 6\"/>';\nconst DSETAD='<path d=\"M9 6l6 6-6 6\"/>';\nconst DSETAEE='<path d=\"M18 6l-6 6 6 6\"/><path d=\"M12 6l-6 6 6 6\"/>';\nconst DSETADD='<path d=\"M6 6l6 6-6 6\"/><path d=\"M12 6l6 6-6 6\"/>';\nconst DMENOS='<path d=\"M6 12h12\"/>';\nconst DMAIS='<path d=\"M12 6v12\"/><path d=\"M6 12h12\"/>';\nfunction girinha(){return `<svg class=\"lk\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"${DRESET}\"/></svg>`;}\nconst DLOCK='M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z';\nconst DLOCKAB='M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17Z';\nfunction cadeado(aberto){return `<svg class=\"lk\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"${aberto?DLOCKAB:DLOCK}\"/></svg>`;}\n// Olho cortado = item OCULTO (equipado, mas nao desenhado). Icone de traco, nao de preenchimento:\n// stroke/fill vao no CSS. Tirei o `class=\"lucide ...\"` do original de proposito -- classe de\n// biblioteca de terceiros aqui so viraria nome morto que ninguem sabe se pode remover depois.\nconst DOLHO='<path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/>';\nfunction olho(){return `<svg class=\"olho\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">${DOLHO}</svg>`;}\n// TIMING E SEQUENCIA saem da tabela `mc-action` (2026-08-27). Antes eram quatro constantes e uma\n// funcao aqui dentro: PASSO=208 fixo, dois fatores ajustados a mao e a lista VAIVEM. O WZ tem\n// `delay` em TODO frame do corpo e o compilador descartava -- a tabela devolveu esse dado. Nada\n// aqui e estimado: `frameDelays` e o delay do jogo, `playback`/`loopStart` descrevem a sequencia.\nconst ACOES='https://bigaton-playground.data2apis.com/dataset/mc-actionDataSet';\nconst ACT=Object.create(null);   // key da action -> linha da tabela\nlet ACTORD=[];                   // keys na ordem da tabela (o preview mostra nessa ordem)\nconst PASSO_PADRAO=208;          // fallback so para uma action que a tabela nao conheca\n// O eixo do mundo (x=0,y=0 do personagem) sai do TAMANHO do canvas, nao de constante fixa:\n// horizontal no meio, vertical em 72% da altura -- a origem cai perto dos pes e o corpo cresce\n// para cima (y negativo), entao 72% e o que deixa o personagem opticamente centrado. Mesma\n// proporcao do stage do engine local.\n// 56 = 8 colunas por 7 fileiras, a grade cheia (01/09). Antes eram 42 (7 por 6). O numero tem que\n// bater com a grade do CSS: sobrando itens a ultima fileira fica pela metade e a grade parece\n// quebrada, e faltando a paginacao sobe.\n// O eixo vertical do personagem e DISTANCIA FIXA do topo do palco, nao fracao da altura dele.\n// Motivo medido: o toggle do anel cresce o palco de 282 para 354 POR BAIXO, e com fracao o\n// personagem descia 60px a cada clique. O topo do palco nao se mexe, entao ancorar nele deixa o\n// personagem parado -- que era o pedido. PALCO_ALT espelha o CSS: 4 celulas de 66 mais 3 de 6.\n// 0,84: personagem de ~127px em 2x, centrado opticamente em 188 -> (188-127)/2 = 30 acima, os pes\n// caem em 157, e 157/188 = 0,84.\n// ATENCAO: esta constante e lida pelo draw() a cada quadro. Ela ja foi apagada uma vez (01/09),\n// por descuido, junto com o comentario vizinho numa edicao por regiao -- o canvas parou de pintar\n// inteiro, sem erro visivel na interface. Nao mover para perto de texto que se edita em bloco.\nconst PALCO_ALT=282;\nconst EIXO_Y=0.84, POR_PAGINA=56;\nconst MEDIA='https://bigaton-playground.data2.media/file/';\n\nconst INDEXREF=Object.create(null); // cat -> ref do idx JSON (caminho NOVO)\nconst IDX=Object.create(null);      // cat -> {sheets:[url], items:[...]} (fetch cacheado)\nconst BLOCO=Object.create(null);    // blockKey/geoRef -> bloco parseado\nconst ITENS=Object.create(null);    // id -> {uv,geo,atlasRef, uvEf?,geoEf?,atlasEfRef?}\nconst IMG=Object.create(null);      // ref -> Image\nconst IMGBASE=Object.create(null);   // caminho do sprite da base -> Image (o caminho ja carrega o id da skin, entao nao colide)\nconst SKINPIX=Object.create(null);   // skinId -> {geo,pix} do arquivo do Drive, buscado sob demanda\n// Folha de icone das 46 skins. Ate 31/08 o `idxskin` era o UNICO dos 15 indices sem thumb -- o\n// seletor era um <select> de texto. Renderizar as thumbs ao vivo custaria os 46 JSONs de skin,\n// 6,3 MB (media 140 KB cada), a mesma ordem de grandeza da aba All inteira. A folha custa 31 KB\n// e reusa o mesmo `.thc` das outras categorias, sem codigo de render novo.\nlet SKINSHEET='';\n// ORELHAS. O dump bruto da cabeca tem QUATRO (`humanEar`, `ear`, `lefEar`, `highlefEar`) mas o\n// compilador guardou so a humana -- por isso isto nao existia no preview do data2.\n// Em vez de reescrever os 46 JSONs de skin, o addon embarca so o DELTA: as quatro dividem o\n// mesmo `map.neck`, entao TL_outra = TL_humanEar + (origin_humanEar - origin_outra) e uma\n// CONSTANTE por skin, valida em todos os frames. Um arquivo de 68 KB, nenhum dado existente\n// tocado. As tres elficas declaram z=accessoryOverHair (zi 41), na FRENTE do cabelo (46) --\n// a humana e hairShade (58), atras. Por isso o zi vem do addon e nao da camada original.\nconst ORELHA_TIPOS=['humanEar','ear','lefEar','highlefEar'];\nconst ORELHA_ROT={humanEar:'Human',ear:'Elf',lefEar:'Long Elf',highlefEar:'High Elf'};\n// Rotulo de interface sai em Title Case. As CHAVES continuam intactas -- humanEar, blink, accface\n// sao dado do WZ e viajam no `value` do option; so o que o usuario LE passa por aqui. Trocar as\n// duas coisas pelo mesmo texto e como se perde a ligacao com o dump.\nfunction capitaliza(t){return String(t||'').replace(/(^|[\\s-])([a-z])/g,(m,a,b)=>a+b.toUpperCase());}\nlet ORELHAS=null;\n// EMOTES. Mesma historia das orelhas: o dump tem ~30 expressoes por face (`blink` com 3 frames,\n// varias com 2) e o compilador guardou so a `default` -- a face publicada tem UM sprite.\n// A expressao e ORTOGONAL a acao do corpo (o personagem pisca enquanto anda), por isso ela e\n// estado proprio e nao um valor de `acao`.\n// O addon e por BLOCO (a mesma particao do `geoRef` das faces): 12.617 faces em 163 blocos,\n// ~860 KB cada, carregado sob demanda. Guarda o DELTA, nao a posicao: 82% das expressoes tem\n// origin/map.brow proprios (medido em 300 faces, 7.125 pares), e a face ancora por brow, entao\n//     TL_expr = TL_default + (brow_def + origin_def) - (brow_expr + origin_expr)\n// Os bitmaps sao deduplicados dentro do bloco: o `_outlink` faz varios frames apontarem para o\n// mesmo PNG (o frame 0 do `blink` E o default), e sem dedupe o bloco inflava 2,5x.\nconst EMOTE_LISTA=['default','blink','smile','wink','troubled','cry','angry','bewildered','stunned','hit','love','oops','vomit','despair','glitter','shine','chu','hum','pain','cheers','bowing','blaze','hot','dam','qBlue'];\n// FORMATO: atlas + uv, o MESMO das outras categorias. Embutir os PNGs em base64 custava 2,7x\n// mais (medido num bloco: 860 KB contra 324 KB) -- 1.655 sprites minusculos carregam cada um o\n// proprio cabecalho de PNG, e o base64 ainda soma 33%. Empacotados num atlas isso desaparece.\n// Ganho de codigo junto: a camada de emote passa a ter a forma {ref,uv,x,y,zi} das camadas de\n// item, entao o desenho usa o caminho que ja existe -- nao ha caso especial nenhum.\n// Os 163 blocos sao agrupados em ~20 arquivos so para caber em upload; o mapa geoRef->grupo\n// mantem o carregamento sob demanda.\n// PAUSA ENTRE REPETICOES -- decidida por nos, porque NAO existe no dado. O `delay` do WZ descreve\n// o movimento em si (uma piscada leva 180 ms: 3 frames de 60), nao o intervalo entre uma piscada e\n// a seguinte -- no jogo o emote e um evento disparado, nao um loop. Loopando o ciclo cru o\n// personagem piscava 5,5 vezes por segundo.\n// A forma escolhida e a que o proprio dado ja usa: `oops` (367 ocorrencias) tem 1640 ms no PRIMEIRO\n// frame e anima depois. Entao aqui o primeiro frame tem um PISO -- e piso, nao soma, senao o\n// `oops` ficaria com pausa dupla.\nconst EMOTE_PAUSA=1800;\n// Vitrine da feature de ilustracao. Ela esta DESLIGADA (o botao abre um aviso em vez de gerar):\n// a pipeline funciona ponta a ponta mas a qualidade nao foi aprovada. Os 3 pares sprite/retrato\n// foram gerados com a pipeline atual e mostram honestamente onde ela acerta e onde erra.\n// Folha 2x3 de 320px por celula: coluna 0 = personagem no canvas, coluna 1 = ilustracao gerada.\n// Folha unica em vez de 6 arquivos porque o recorte por background-position ja e o idioma do\n// projeto (as thumbs de item fazem igual) e o layout continua todo no DOM, com a seta e o\n// espacamento nitidos em vez de assados numa imagem.\n// As capturas entram COMO ESTAO: recorte quadrado em torno do personagem, na resolucao nativa\n// do arquivo (224px). Nada de editar pixel nem de trocar fundo.\n// Dois erros meus aqui, os dois por fazer alem do pedido:\n//   1. cortei as guias por alfa e o corte comeu a borda do proprio sprite;\n//   2. ampliei o recorte de 216 para 320 com nearest e o browser reduzia de volta para 120 --\n//      dois reamostramentos em cima da arte, que e de onde vinha o serrilhado.\n// Agora e so recorte, e a unica escala e a reducao do browser (224 -> 120), que e limpa.\nconst SHOWCASE='https://bigaton-playground.data2.media/file/9mAOpjoxA5KNMamYtqutg';\nconst PP_TILE=120;   // celula exibida; a folha e escalada por background-size na mesma proporcao\n// Contador de imagens EM VOO. `s.carregando` cobre so os fetch explicitos (indice, bloco); os\n// atlas entram por <img> e nao passavam por lugar nenhum -- era possivel a tela estar montando\n// dezenas de sprites sem nenhum sinal. Nao e estado do render: mora fora do `s` de proposito,\n// porque mudar a cada imagem dispararia um re-render por sprite.\n// ARRASTO do personagem. Fica FORA do d2.state de proposito: mover por estado dispararia um\n// re-render da UI inteira a cada mousemove. Mesmo padrao do EMVOO e do indicador de frame --\n// o rAF le direto daqui.\nlet ARRX=0,ARRY=0,ARRAST=null;\nlet EMVOO=0;\nlet EMOTEIDX=null;                    // {grupos:{nome:{a:atlasRef,j:jsonRef}}, blocos:{geoRef:nome}}\nconst EMOTEGR=Object.create(null);    // nome do grupo -> {uv, faces, atlas}\nconst META=Object.create(null);      // id -> linha do idx (vs do cap, efRef da arma)\nlet ZI2SMAP=null;                    // zi -> codigo de smap (vem no idxcap.json)\nlet REGRAS=null;                     // conflitos de slot (mcregras.json)\nlet EFI=null;                        // id -> {a:atlasRef, b:blocoRef, x:[uv do ItemEff no atlas BASE]}\nconst BLOCOEI=Object.create(null);   // blocoRef -> bloco de ItemEff parseado\nlet TEMPO=0;                         // relogio do ItemEff em ms; so anda com o Play ligado\n// O ItemEff tem EIXO PROPRIO: ciclo de animacao com `delay` por frame, SEM relacao com o numero\n// de frames da acao. Um efeito de 16 frames num `stand1` de 3 rodava so 3 se fosse amarrado ao\n// corpo. O `e` traz o efeito inteiro e o canto de cada frame sai de `ancora - origin`.\n// EF_VEL e ajuste de APRESENTACAO (fator na leitura do relogio, nao no dado) -- mesmo do local.\nconst EF_VEL=1.15;\nconst WPSI=Object.create(null);      // arma que ocupa TAMBEM a off-hand\n// Recorte de icone das pecas RESTAURADAS do cache. Existe porque a thumb do anel depende de\n// META[id].ic (o recorte) + IDX[slot].sheets (a folha), e os dois so aparecem quando o indice\n// daquela categoria carrega -- ~6 MB para um personagem inteiro, a face sozinha 2,2 MB. Sem\n// isto o anel fica com o placeholder de 4 digitos por varios segundos depois do reload.\n// Mora no nivel do modulo porque quem le e o `render`, que nao enxerga o escopo do `mounted`.\n// Guarda tambem geo/at: uma peca restaurada cujo indice ainda nao chegou nao tem META, e sem\n// esse fallback o proximo salvaCache gravaria geo:null e a visita seguinte perderia a peca.\nconst THUMBCACHE=Object.create(null);   // id -> {ic:[folha,x,y,w,h], sh:refDaFolha, geo, at}\n\nfunction esc(s){return String(s==null?'':s).replace(/[<>&\"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;'}[c]));}\n// O endpoint do dataset NAO devolve ordenado -- ordenar por `order` aqui e o que preserva a\n// ordem do preview. A tabela e opcional: sem ela tudo cai no fallback e o preview segue de pe.\nasync function carregaAcoes(){\n  const d=await (await fetch(ACOES)).json();\n  const rows=((d&&d.data)||[]).slice().sort((a,b)=>(a.order|0)-(b.order|0));\n  ACTORD=rows.map(r=>r.key);\n  for(const r of rows)ACT[r.key]=r;\n  return rows.length;\n}\n// A skin diz QUAIS actions existem (autoridade de render); a tabela diz em que ordem mostrar e\n// com que nome. Action fora da tabela vai para o fim, nunca some.\nfunction ordenaAcoes(ks){\n  const pos=a=>{const i=ACTORD.indexOf(a);return i<0?9999:i;};\n  return ks.slice().sort((a,b)=>pos(a)-pos(b)||a.localeCompare(b));\n}\nfunction rotuloAcao(a){return (ACT[a]&&ACT[a].name)||a;}\n// `loopStart` separa a INTRO (frames que so tocam ao entrar na action) da parte que repete.\n// E o caso do walk: o frame 0 e transicao, o loop e 1,2,3,2 -- antes o frame 0 voltava a cada\n// volta porque a sequencia era montada sempre a partir do zero.\nfunction sequencia(a,n){\n  const r=ACT[a]||null;\n  const modo=r?r.playback:(n>2?'pingpong':'loop');\n  const ls=Math.min(Math.max(r?(r.loopStart|0):0,0),Math.max(n-1,0));\n  const intro=[];for(let i=0;i<ls;i++)intro.push(i);\n  const ciclo=[];for(let i=ls;i<n;i++)ciclo.push(i);\n  if(modo==='pingpong')for(let i=n-2;i>ls;i--)ciclo.push(i);\n  return{intro,ciclo,modo};\n}\n// Duracao de um frame: o delay do WZ dividido pelo multiplicador da tabela (actionSpeed 1 = o\n// tempo original do jogo, 2 = o dobro da velocidade). Mesma forma que o ItemEff ja usava.\nfunction duracao(a,i){\n  const r=ACT[a];\n  const fd=r&&r.frameDelays;\n  const d=(fd&&fd.length)?fd[Math.min(i,fd.length-1)]:PASSO_PADRAO;\n  const v=(r&&Number(r.actionSpeed)>0)?Number(r.actionSpeed):1;\n  return Math.max(16,(Number(d)||PASSO_PADRAO)/v);\n}\n// AQUI O PREVIEW DIVERGE DO mcbuilder DE PROPOSITO. La o `playback:'once'` da tabela e regra de\n// jogo e o golpe toca uma vez so. Aqui isto e ferramenta de INSPECAO: no modo 'loop' tudo repete,\n// inclusive os ataques, porque olhar um golpe girando e o ponto. A tabela continua mandando na\n// SEQUENCIA (intro + pingpong via loopStart) e na DURACAO de cada frame -- o modo so decide se\n// repete ou nao. 'uma' = Play Once: percorre intro+ciclo inteiros e avisa `terminou` para o\n// preview voltar ao frame 0 e parar.\nfunction frameNoTempo(a,n,ms,modo){\n  const {intro,ciclo}=sequencia(a,n);\n  const passos=intro.concat(ciclo);\n  if(!passos.length)return{frame:0,terminou:true};\n  let t=ms;\n  if(modo==='uma'){\n    for(const f of passos){const d=duracao(a,f);if(t<d)return{frame:f,terminou:false};t-=d;}\n    return{frame:passos[0],terminou:true};\n  }\n  for(const f of intro){const d=duracao(a,f);if(t<d)return{frame:f,terminou:false};t-=d;}\n  let total=0;for(const f of ciclo)total+=duracao(a,f);\n  if(total<=0)return{frame:ciclo[0],terminou:false};\n  t=t%total;\n  for(const f of ciclo){const d=duracao(a,f);if(t<d)return{frame:f,terminou:false};t-=d;}\n  return{frame:ciclo[ciclo.length-1],terminou:false};\n}\nfunction keys(g,a){return Object.keys((g||{})[a]||{});}\nfunction conta(im){EMVOO++;const fim=()=>{EMVOO=Math.max(0,EMVOO-1);};im.addEventListener('load',fim,{once:true});im.addEventListener('error',fim,{once:true});return im;}\nfunction imgBase(sk,p){if(IMGBASE[p])return IMGBASE[p];const v=(sk.pix||{})[p];if(!v)return null;const im=new Image();im.src=v;IMGBASE[p]=conta(im);return im;}\n// Troca o sprite da FACE pela expressao escolhida. Se a face nao tiver aquela expressao, nao faz\n// nada e o default continua -- e o mesmo fallback do engine local, e evita que um emote exotico\n// (3 faces tem `bangle`/`setback`/`wickedness`) apague o rosto.\nfunction trocaEmote(L,faceId,emote){\n  if(!emote||emote==='default'||!faceId)return;\n  const gr=META[faceId]&&META[faceId].geoRef;\n  const nome=EMOTEIDX&&EMOTEIDX.blocos&&EMOTEIDX.blocos[gr];\n  const bl=nome&&EMOTEGR[nome];\n  const seq=bl&&bl.faces&&bl.faces[faceId]&&bl.faces[faceId][emote];\n  if(!seq||!seq.length)return;\n  // relogio proprio, como o ItemEff: a expressao anima independente do frame do corpo\n  const dur=i=>Math.max(seq[i][3]||100, i===0?EMOTE_PAUSA:0);\n  let ciclo=0;for(let i=0;i<seq.length;i++)ciclo+=dur(i);\n  let t=ciclo?(TEMPO%ciclo):0,k=0;\n  for(let i=0;i<seq.length;i++){t-=dur(i);if(t<0){k=i;break;}}\n  const q=seq[k]||seq[0], uv=bl.uv[q[0]];\n  if(!uv)return;\n  for(const c of L){\n    if(c.dono!==faceId)continue;\n    c.ref=bl.atlas;c.uv=uv;c.x+=q[1];c.y+=q[2];\n  }\n}\n// `crossOrigin` ANTES do src (depois nao vale) -- e o que impede o canvas de ficar CONTAMINADO.\n// Sem isso `toDataURL` lanca SecurityError e nao existe portrait imagem-para-imagem. O Drive\n// manda `Access-Control-Allow-Origin: *` nos TRES saltos (media -> apis -> files), inclusive no\n// 200 final, entao pedir CORS aqui nao quebra o carregamento.\nfunction img(ref){if(!ref)return null;if(IMG[ref])return IMG[ref];const im=new Image();im.crossOrigin='anonymous';im.src=MEDIA+ref;IMG[ref]=conta(im);return im;}\n\nfunction pagina(s){\n  const q=(s.q||'').trim().toLowerCase();\n  const lista=q?(s.cat||[]).filter(r=>(r.busca||'').indexOf(q)>=0):(s.cat||[]);\n  const paginas=Math.max(1,Math.ceil(lista.length/POR_PAGINA));\n  const pag=Math.min(s.pag||0,paginas-1);\n  return{q,lista,paginas,pag,fatia:lista.slice(pag*POR_PAGINA,pag*POR_PAGINA+POR_PAGINA)};\n}\n\n// O item e indexado pela CHAVE DE FRAME DO CORPO (`fr`), nunca por posicao na lista de chaves.\n// Em 36.296 pares (item,acao) o item NAO tem todos os frames -- a peca some naquele frame porque\n// o corpo a esconde (swingOF/swingTF giram de costas: sem face; varias swings escondem a mao).\n// Indexar por posicao com modulo, como estava, desalinhava tudo: no frame 0 desenhava o frame 1,\n// e num frame em que a peca deveria SUMIR desenhava o sprite de outro frame na ancora daquele\n// outro frame -- e por isso a face/glove aparecia \"torta\" nessas acoes.\n// Ausente = nao desenha. E a mesma regra do engine local: `(geo[acao]||{})[frame]||[]`.\n// ItemEff: um grupo por elemento do efeito, cada um com ciclo proprio percorrido pelo relogio.\nfunction camadasEfItem(item,acao,fr,L){\n  const ei=item.ei;if(!ei||!ei.e||!ei.e.p)return;\n  const refs=(ei.e.q||{})[acao], anc=((ei.e.a||{})[acao]||{})[fr];\n  if(!refs||!anc)return;\n  for(let gi=0;gi<refs.length;gi++){\n    const grupo=ei.e.p[refs[gi]], A=anc[gi];\n    if(!grupo||!grupo.length||!A)continue;\n    let ciclo=0;for(const f of grupo)ciclo+=(f[3]||100);\n    let tt=ciclo?(TEMPO*EF_VEL)%ciclo:0,k=0;\n    for(let i=0;i<grupo.length;i++){tt-=(grupo[i][3]||100);if(tt<0){k=i;break;}}\n    const f=grupo[k],uv=ei.uv[f[0]];\n    // f = [png, ox, oy, delay, zi, w, noflip] -- o efeito tem a propria flag de noflip (2 itens)\n    if(uv)L.push({ref:item.eiRef,uv:uv,x:A[0]-f[1],y:A[1]-f[2],zi:f[4],dono:item.itemId,nfw:(f[6]?f[5]:0)});\n  }\n}\nfunction camadasItem(item,acao,fr,efeito,efItem,L,vskin,st49){\n  const dono=item.itemId;\n  // Os sprites do ItemEff foram empacotados DENTRO do atlas base do item (medido: todo item com\n  // bloco `e` tambem tem caminhos `Effect/` no `g`). Desenha-los como camada comum e o que fazia o\n  // efeito ficar sempre ligado e com a animacao errada. `ex` = os indices de uv que sao ItemEff;\n  // eles saem daqui SEMPRE, e o efeito volta pelo eixo proprio quando o toggle esta ligado.\n  const ex=item.eiX;\n  // hairShade por SKIN (2026-08-20): `s` mapeia indice-de-skin -> sprite no atlas do proprio item,\n  // e `s['0']` diz QUAL camada e a trocavel. A troca e por IDENTIDADE com esse indice, nunca por\n  // nome de arquivo -- mesma regra do engine local. Sem `s`, ou sem variante para a skin atual,\n  // o sprite e o de sempre; por isso os 17.255 cabelos sem variante nao pagam nada aqui.\n  const sv=item.s,alvo=sv?sv['0']:-1;\n  // STANCE 49 -- a pose em que a arma e segurada na OUTRA mao; as armas de cash usam para cobrir\n  // gun. `o` guarda so o diff POR ACAO, e acao ausente em `o` significa \"identica a `g`\": cair em\n  // `g` e o comportamento CERTO, nao uma falha. So arma declara `o` (1.829 nos blocos publicados).\n  // A troca e por ACAO INTEIRA, nunca merge frame a frame: medido, 46 pares (item,acao) existem em\n  // `o` e NAO em `g` -- com a stance ligada a arma desenha onde normalmente nao desenharia, e um\n  // merge por frame perderia exatamente esses.\n  const go=(st49&&item.geo.o&&item.geo.o[acao])?item.geo.o[acao]:(item.geo.g[acao]||{});\n  for(const c of (go[fr]||[])){\n    if(ex&&ex[c[0]])continue;\n    let si=c[0];\n    if(sv&&si===alvo&&sv[vskin]!=null)si=sv[vskin];\n    // 5o elemento = largura do sprite, presente SO em item `noflip` (medido: 1 item na base\n    // inteira). Cuidado: nas camadas da BASE o 5o elemento e a chave da parte, nao largura --\n    // por isso `nfw` so e lido aqui, nunca no laco da skin.\n    const uv=item.uv[si];if(uv)L.push({ref:item.atlasRef,uv:uv,x:c[1],y:c[2],zi:c[3],dono:dono,nfw:(c.length>4?c[4]:0)});}\n  if(efItem)camadasEfItem(item,acao,fr,L);\n  // atlasEfRef sai do META (efRef do idx). Uma arma RESTAURADA do cache e carregada ANTES do\n  // indice da categoria, entao o carregaItem a deixou sem ref e marcada como pronta -- o efeito\n  // nunca mais aparecia (QA 10/09). Resolve aqui, quando o indice ja chegou, e memoriza.\n  if(efeito&&item.geoEf&&!item.atlasEfRef&&META[dono]&&META[dono].efRef)item.atlasEfRef=META[dono].efRef;\n  if(efeito&&item.geoEf&&item.atlasEfRef){\n    for(const c of ((item.geoEf.g[acao]||{})[fr]||[])){const uv=item.uvEf[c[0]];if(uv)L.push({ref:item.atlasEfRef,uv:uv,x:c[1],y:c[2],zi:c[3],dono:dono});}}\n}\nfunction compor(sk,equip,acao,frame,efeito,efItem,st49,orelha,emote){\n  const gk=keys(sk.geo,acao);if(!gk.length)return{L:[],fr:'0',n:0};\n  const fr=gk[((frame%gk.length)+gk.length)%gk.length];\n  const esconder={};\n  for(const slot in equip){const it=ITENS[equip[slot]];if(!it)continue;const h=((it.geo.h||{})[acao]||{})[fr]||[];for(const k of h)esconder[k]=1;}\n  let L=[];\n  for(const c of (sk.geo[acao][fr]||[])){\n    if(esconder[c[4]])continue;\n    // Troca SO o sprite, o offset e o zi da camada da orelha -- a CHAVE DE PARTE continua\n    // `...|humanEar` de proposito: e ela que o `h` dos itens usa para esconder a orelha\n    // (mascara, capacete). Mudar a chave faria o chapeu deixar de esconder a orelha elfica.\n    if(orelha&&orelha!=='humanEar'&&String(c[4]).slice(-9)==='|humanEar'){\n      const ad=ORELHAS&&ORELHAS.skins&&ORELHAS.skins[sk.skinId];\n      const e=ad&&ad[orelha];\n      if(e){\n        // a chave carrega o id da SKIN: o cache IMGBASE e por caminho, e sem isso duas skins\n        // com a mesma orelha colidiriam no cache e a segunda desenharia o sprite da primeira.\n        const k='__orelha__'+sk.skinId+'__'+orelha;\n        if(!sk.pix[k])sk.pix[k]=e.px;\n        L.push({base:k,x:c[1]+e.d[0],y:c[2]+e.d[1],zi:e.zi,dono:'base'});\n        continue;\n      }\n    }\n    L.push({base:c[0],x:c[1],y:c[2],zi:c[3],dono:'base'});\n  }\n  const vskin=String(parseInt(sk.skinId,10)-2000);\n  for(const slot in equip){const it=ITENS[equip[slot]];if(it)camadasItem(it,acao,fr,efeito,efItem,L,vskin,st49);}\n  trocaEmote(L,equip.face,emote);\n  // VSLOT -- a MESMA regra do engine local (preview.html): o chapeu declara os codigos de smap\n  // que ele OCUPA; toda camada de OUTRO item cujo zi mapeia para um desses codigos some. O proprio\n  // cap e a base sao isentos (a base ja veio resolvida pelo `h`). E so por isso que o cabelo troca\n  // de silhueta debaixo do chapeu -- nao existe sprite alternativo, existe camada escondida.\n  let ocultas=0;\n  const capId=equip.cap;\n  if(capId&&ZI2SMAP){\n    const v=(META[capId]&&META[capId].vs)||'';\n    if(v){\n      const occ=Object.create(null);\n      for(let i=0;i<v.length;i+=2)occ[v.slice(i,i+2)]=1;\n      const antes=L.length;\n      L=L.filter(c=>{\n        if(c.dono===capId||c.dono==='base')return true;\n        const cod=(c.zi>=0&&c.zi<ZI2SMAP.length)?ZI2SMAP[c.zi]:'';\n        return !cod||!occ[cod];\n      });\n      ocultas=antes-L.length;\n    }\n  }\n  L.sort((a,b)=>b.zi-a.zi);\n  return{L,fr,n:gk.length,ocultas};\n}\n\nexport default {\n  css:`\n  /* TEMA CLARO (01/09, pedido do usuario): painel #eeeeee sobre pagina #E6E1CE. Os tokens\n     inverteram inteiros -- texto escuro, linhas claras -- e o azul de acento escureceu, porque\n     o #6c8cff sumia sobre cartao claro.\n     --poco e --wells sao o MATERIAL do slot, extraido do artboard Testes_mc valor por valor. */\n  .wrap{--bg:#e6e1ce;--bg2:#eeeeee;--pan:#e5e5e5;--lin:#d3d3d3;--txt:#23232a;--dim:#6a6a76;\n        --ac:#3f5fd0;--ac2:#8a5fd0;--warn:#9a6b00;--neon:#ddfe01;\n        /* o ciano subiu para ca porque agora tem DOIS consumidores: o botao Randomize e a aba\n           ativa do navegador de itens. Os outros dois tokens da familia (--cia3 do pressionado e\n           --ciarel do relevo) continuam no .pill, que e o unico que os usa. */\n        --cia:linear-gradient(180deg,#57bde0 0%,#3ba7d1 46%,#2a8cb8 100%);\n        /* O corpo escuro e a LUZ DE BORDA subiram junto, pelo mesmo motivo: o painel de itens\n           passou a usar o mesmo material do botao escuro (01/09).\n           A separacao entre --escluz e a sombra projetada nao e arrumacao. A luz de borda e um\n           fio de 1px em cima e outro embaixo: ela descreve a ESPESSURA da peca e vale igual num\n           botao de 26px e num painel de 800. A sombra projetada descreve a distancia ate o fundo\n           e escala com o tamanho -- 2px somem debaixo de um painel. Por isso o token compartilha\n           a primeira e cada peca declara a sua segunda. */\n        --esc:linear-gradient(180deg,#4e5867 0%,#424c5a 46%,#333b47 100%);\n        --escluz:inset 0 1px 0 #ffffff36,inset 0 -1px 0 #00000059;\n        /* Mesmo material, tom CINZA: e o que a referencia usa nas abas inativas -- claro o\n           bastante para destacar do painel, escuro o bastante para o texto branco assentar. */\n        --cin:linear-gradient(180deg,#7c8188 0%,#6c7178 46%,#575c63 100%);\n        --cin2:linear-gradient(180deg,#8b9198 0%,#7b8188 46%,#666c73 100%);\n        --poco:#cdcdcd;\n        --wells:inset 0 16px 11px -21px rgba(0,0,0,.49),inset 16px 0 11px -21px rgba(0,0,0,.49),inset -16px 0 11px -21px rgba(0,0,0,.49);\n        --side:560px;\n        height:100%;display:flex;justify-content:center;align-items:stretch;\n        padding:22px 24px;box-sizing:border-box;background:var(--bg);\n        color:var(--txt);font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}\n  /* O ESBOCO nao ocupa a tela toda: sao 738px de conteudo num quadro de 1366, com margem dos\n     dois lados. Por isso max-width + centralizacao, em vez do grid colado nas bordas. */\n  /* Larguras do ESBOCO, nao esticadas: la o painel esquerdo tem 338 e o direito 385, e o\n     esquerdo e o mais ESTREITO dos dois. 356 = os 332 do anel + 12 de folga de cada lado.\n     Somando com o respiro: 356 + 16 + 392 = 764, que centralizado num 1366 deixa 300px de\n     margem de cada lado. E esse vazio lateral que o usuario pediu. */\n  /* +50% no visor e +25% no painel direito (01/09). Esquerdo = 498 do anel + 12 de folga de cada\n     lado = 522. Direito = 392 x 1,25 = 490. Total 522 + 16 + 490 = 1028. */\n  /* Larguras de 01/09: a folga lateral DENTRO do painel esquerdo dobrou (de 11 para 22px de cada\n     lado), entao a coluna cresceu junto -- o anel tem 498px fixos e nao pode encolher, quem cede\n     e a moldura. 544 = 498 do anel mais 22 de cada lado mais as duas bordas.\n     O respiro ENTRE os paineis tambem dobrou, de 16 para 32. Total: 544 + 32 + 490 = 1066. */\n  .app{display:grid;grid-template-columns:544px var(--side);gap:32px;\n       width:100%;max-width:1136px;min-height:0}\n  /* a faixa do titulo saiu: sobraram visor e opcoes */\n  .esq{display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;min-width:0;\n       background:var(--bg2);border:1px solid var(--lin);border-radius:16px}\n\n  /* CSS REMOVIDO EM 01/09, depois de uma varredura que dirigiu o app por todos os estados e\n     comparou as classes que o CSS declara com as que chegam a existir no DOM. Sairam daqui:\n     .top .mark .tit (a faixa de titulo, tirada no redesenho), .equip e filhos (a coluna de\n     equipados, que virou o anel), .grp (o painel direito passou a usar .abas e .invcx), .badgec\n     (a celula da grade nao desenha mais o cracha) e .mestre.vazio (nunca foi emitida).\n     FICARAM DE PROPOSITO .portbox e .portimg: sao da geracao de ilustracao, que esta em espera,\n     e voltam com ela. */\n\n  /* palco ----------------------------------------------------------------- */\n  /* VISOR: o anel de equipados em volta do personagem, copiado do esboco (artboard Testes_mc).\n     A celula tem 44px porque a THUMB tem 44px, e isso e decisao registrada -- o anel se ajusta\n     a ela, nunca o contrario. Fileira de baixo: 7x44 + 6x4 = 332. As colunas laterais tem 4\n     celulas, entao sobra para o palco 332 - 2x48 = 236 de largura e 4x44 + 3x4 = 188 de altura.\n     E MUITO menor que os 1228x892 de antes, que era o pedido. */\n  /* O visor desce ~20% da altura da tela, deixando a faixa de cima vazia por enquanto (pedido\n     de 01/09). clamp para nao empurrar tudo para fora numa janela baixa. */\n  /* metade do que era: 20vh derrubou o visor longe demais */\n  .visor{padding:clamp(24px,10vh,95px) 0 14px;display:flex;justify-content:center}\n  .anel{display:flex;flex-direction:column;gap:6px;width:498px}\n  .anelcima{display:grid;grid-template-columns:66px 1fr 66px;grid-template-rows:repeat(4,66px);gap:6px}\n  .anelbaixo{display:grid;grid-template-columns:repeat(7,66px);gap:6px}\n  /* Posicao EXPLICITA. Auto-placement encheria linha a linha e as 4 celulas da esquerda cairiam\n     lado a lado na primeira linha. Os filhos sao, em ordem: 4 da esquerda, o palco, 4 da direita. */\n  .anelcima>*:nth-child(1){grid-column:1;grid-row:1}\n  .anelcima>*:nth-child(2){grid-column:1;grid-row:2}\n  .anelcima>*:nth-child(3){grid-column:1;grid-row:3}\n  .anelcima>*:nth-child(4){grid-column:1;grid-row:4}\n  .anelcima>*:nth-child(6){grid-column:3;grid-row:1}\n  .anelcima>*:nth-child(7){grid-column:3;grid-row:2}\n  .anelcima>*:nth-child(8){grid-column:3;grid-row:3}\n  .anelcima>*:nth-child(9){grid-column:3;grid-row:4}\n  /* O palco usa o MESMO material das celulas: no esboco a caixa do personagem e o anel sao a\n     mesma peca rebaixada. O azul de la era marcador (30% de opacidade, com CHARACTER HERE\n     escrito dentro), nao escolha de cor. */\n  /* Fundo do cartao de personagem, seguindo a referencia: VERTICAL, so rosa e azul, bem de leve.\n     A versao anterior tinha quatro radiais nos cantos, com menta e pessego -- foi trocada porque\n     o usuario so queria os dois tons, e no eixo vertical.\n     A segunda camada e a BORDA acompanhando, que a referencia tambem tem: em vez de um contorno\n     de cor solida (que nao existe la), sao as laterais puxando de leve para o escuro. Ela vem\n     ANTES no empilhamento porque em CSS o primeiro background fica por CIMA. */\n  .palco{grid-column:2;grid-row:1/span 4;position:relative;overflow:hidden;border-radius:3px;\n    box-shadow:var(--wells);cursor:grab;\n    background:\n      linear-gradient(90deg,#0000000a 0%,#00000000 9%,#00000000 91%,#0000000a 100%),\n      linear-gradient(180deg,#fdeaf1 0%,#fefafc 35%,#fafcfe 62%,#e9f1fa 100%)}\n  .palco:active{cursor:grabbing}\n  .reset{position:absolute;right:6px;top:6px;width:26px;height:26px;padding:0;z-index:2;\n    display:flex;align-items:center;justify-content:center;color:#5c5c66;\n    background:#ffffffc4;border-color:#0000001f}\n  .reset:hover{background:#fff;color:var(--txt);border-color:#00000033}\n  .reset .lk{width:15px;height:15px}\n  /* Esconder o anel COLAPSA as laterais e a fileira, e o palco fica com os 332x236 inteiros.\n     Deixar as celulas invisiveis mas ocupando lugar seria pior: o toggle e explicito, entao\n     recuperar a area E o ponto dele. */\n  .anel.off .anelcima{grid-template-columns:0 1fr 0;grid-template-rows:repeat(5,66px);column-gap:0}\n  .anel.off .palco{grid-row:1/span 5}\n  .anel.off .anelbaixo,.anel.off .slot{display:none}\n  /* MATERIAL DO SLOT, copiado do artboard Testes_mc valor por valor: fill rgba(205,205,205,1),\n     SEM borda, raio 3, e TRES sombras internas iguais (blur 11, spread -21, preto 49%) vindas de\n     cima, da esquerda e da direita -- um poco aberto embaixo. Vale SEMPRE, equipado ou vazio,\n     que foi o pedido. ATENCAO: nada de crase neste bloco, ele inteiro e um template literal --\n     foi a QUARTA vez que uma crase num comentario derrubou este componente.\n     O seletor usa DUAS classes de proposito: com uma so, a regra generica do\n     .eqi (o cartao claro da coluna antiga) venceria pela ordem do arquivo. */\n  .slot,.slot.eqi{width:66px;height:66px;position:relative;\n    background:var(--poco);border:0;border-radius:3px;box-shadow:var(--wells)}\n  /* O icone precisa PREENCHER a celula na mesma proporcao da grade de itens, que e onde ele ja\n     aparecia no tamanho certo. La a celula mede ~50px para um recorte de 52; aqui a celula tem\n     66, entao 52 x 1,25 = 65 fecha a mesma leitura.\n     Tres classes de proposito: as regras de .eqi e de .skbtn tem duas cada e ganhariam pela ordem\n     do arquivo, deixando o icone no scale(.8) antigo. Era essa a causa de eles saírem pequenos. */\n  /* TAMANHO NATIVO, sem escala. Pixel art so amplia sem artefato em fator INTEIRO: 52 -> 65 e\n     1,25x, que serrilha (uns pixels viram 1, outros 2) E ainda encosta nas bordas dos 66,\n     cortando cabelo e rosto, que sao os recortes que usam a celula inteira. Em 1x sobram 7px de\n     folga de cada lado e o pixel fica exato. */\n  /* FILHO DIRETO. Com descendente (.anel .slot .thc) esta regra tambem pegava as celulas do\n     POPUP de skin, que vivem dentro do .slot -- e com tres classes ela vencia o scale(.5) delas.\n     Era por isso que as cabecas do popup saiam grandes. */\n  .anel .slot>.thc{transform:none;image-rendering:pixelated}\n  .slot .olho{width:28px;height:28px}\n  .slot .x{width:18px;height:18px;line-height:17px;font-size:10px}\n  .slot .cad .lk{width:26px;height:26px}\n  .slot .semic{width:52px;height:52px}\n  /* O seletor de skin e um BOTAO proprio dentro do poco, com a mesma cara das thumbs da grade\n     de itens -- e o que o usuario pediu depois de ver a versao chapada. */\n  /* A celula da skin NAO usa o poco: o botao ocupa o espaco inteiro e e ele o cartao. Ter um\n     cartao de 56 dentro de um poco de 66 desenhava caixa dentro de caixa e cortava a borda. */\n  .slot.skwrap{overflow:visible;background:none;box-shadow:none}\n  /* Alinhado com a fileira, mas com VOLUME: o raio 3 chapado ficou seco demais. Raio 6 e uma\n     sombra curta por fora mais um brilho de 1px por dentro dao relevo sem inflar a caixa --\n     era o drop shadow largo do raio 8 que fazia ele parecer maior que os vizinhos. */\n  .slot .skbtn{position:absolute;inset:0;width:auto;height:auto;padding:0;\n    border:1px solid #cdcdd6;border-radius:6px;\n    background:linear-gradient(180deg,#ffffff,#ebebf0);\n    box-shadow:0 1px 2px #00000026,inset 0 1px 0 #ffffff}\n  .slot .skbtn:hover{border-color:var(--ac);background:linear-gradient(180deg,#ffffff,#f2f4ff);\n    box-shadow:0 2px 5px -1px #0000002e,inset 0 1px 0 #ffffff}\n\n  /* Celula VAZIA mostra o nome do slot. O nome some quando ha peca equipada, a pedido: a arte\n     ja diz o que e, e a celula nao comporta rotulo e sprite ao mesmo tempo sem cortar um dos dois.\n     Nao precisa mais ser flex: o rotulo saiu do fluxo (ver .slab). */\n  .vaz{cursor:pointer;transition:background .12s}\n  .vaz:hover{background:#d7d7d7}\n  /* HOVER e CLIQUE dos slots do anel (pedido de 01/09). O halo e SOMADO depois de var(--wells),\n     nunca no lugar dele: box-shadow e uma lista, e trocar a lista apagaria o relevo do poco, que\n     e a identidade que o usuario extraiu do esboco.\n     O :not(.skwrap) tira a celula da skin, que nao tem poco -- ela e so o berco do botao, e um\n     halo nela desenharia caixa em volta de caixa. */\n  /* NAO existe halo de hover nos slots do anel. Ele foi pedido, implementado e RETIRADO (01/09),\n     em dois passos: primeiro nos equipados, depois nos vazios. O motivo e o mesmo dos dois lados\n     -- sobre o painel claro o brilho branco quase nao aparece, e o pouco que aparecia disputava\n     com o sombreado do poco, que e a identidade que o usuario extraiu do esboco.\n     O hover do slot vazio continua sendo o .vaz:hover, que clareia o fundo, e o do equipado o veu\n     com o icone. Nao reintroduzir sem uma cor que de contraste contra o #eeeeee. */\n\n  /* O rotulo e system-ui, e agora esta ESCRITO assim. Antes esta linha pedia Sunflower e havia um\n     @import do Google no topo deste css, os dois herdados do esboco -- e nenhum dos dois pintava\n     nada: o css do componente e injetado no SHADOW DOM e face de fonte se resolve no DOCUMENTO.\n     Medido no runtime (01/09): document.fonts nao tinha a familia e o rotulo media 38,4px, que e\n     a metrica do fallback. Os dois sairam. Para uma fonte valer aqui e preciso registra-la por JS,\n     com document.fonts.add -- nao basta declarar no css.\n     COLADO NO CANTO SUPERIOR ESQUERDO, como no exemplo do usuario. Absoluto de proposito: assim o\n     rotulo nao participa do layout da celula e nao disputa espaco com a arte -- e por isso que\n     .vaz deixou de precisar ser flex. Medido: o mais largo (OFF-HAND) da 46,7px numa celula de 66,\n     entao os 14 cabem numa linha so. */\n  .slab{position:absolute;top:5px;left:6px;\n    font:400 9px ui-sans-serif,system-ui;color:#f8f8f8;letter-spacing:.2px;\n    line-height:1;white-space:nowrap;pointer-events:none;text-transform:uppercase}\n  /* painel de opcoes: rola sozinho, o visor acima dele nunca encolhe */\n  /* O padding lateral e 22px de PROPOSITO: e exatamente a folga que o anel tem no visor acima,\n     entao as duas fileiras comecam e terminam na mesma coluna -- o Loop embaixo do CAPE e o Slots\n     embaixo do SHOES.\n     E POR ISSO O scrollbar-gutter SAIU. Ele reservava a barra de rolagem so do lado DIREITO, e\n     eram esses ~15px que empurravam o Slots para dentro enquanto o Loop ficava alinhado: o painel\n     parecia torto sem nada estar torto. O both-edges devolveria a simetria mas afastaria os dois\n     lados do anel, que e o oposto do pedido.\n     O QUE SE PERDE: se o conteudo passar da altura, a barra aparece e o conteudo desliza ~15px.\n     Hoje sobram ~200px neste painel, entao nao acontece -- mas volta a valer se algo grande\n     entrar aqui. */\n  .opts{padding:0 22px 14px;overflow:auto;min-height:0;display:flex;flex-direction:column;gap:9px}\n  /* position:relative aqui e o que da ao popup de action a LINHA INTEIRA como referencia. Antes\n     ele se media pela caixinha do ACTION, de 126px, e por isso nascia estreito e comprido. */\n  .ohud{display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;position:relative;--hudalt:62px}\n  /* O data-d2-static mora no WRAPPER, nao no canvas. Ele impede o morph de mexer nos FILHOS do\n     elemento marcado -- posto no proprio canvas nao protegia nada (canvas nao tem filhos) e o\n     morph reaplicava o elemento a cada re-render, resetando o backing store para o default\n     300x150 do browser. Era essa a piscada. Contrato em code-component-lifecycle-traps secao 10.\n     ATENCAO: nada de crase neste bloco -- ele inteiro e um template literal. */\n  /* O canvas tem tamanho FIXO, do anel inteiro (498x354), ancorado no TOPO e centrado na\n     horizontal. Ligar ou desligar os slots muda o tamanho do PALCO, nunca o do canvas -- ele so\n     passa a ser mais ou menos recortado pelo overflow do palco.\n     Por que assim: enquanto o canvas acompanhava o palco, cada toggle o redimensionava, e\n     redimensionar canvas APAGA o bitmap e obriga a redesenhar. No quadro dessa troca dava para\n     ver o personagem saltar. Com tamanho fixo a geometria do desenho e a mesma nos dois estados,\n     entao nao existe quadro intermediario para piscar.\n     Ancorado no TOPO e nao no centro de proposito: a borda de cima do palco e a unica que nao\n     se move no toggle (ele cresce por baixo), e o eixo vertical do personagem ja e medido a\n     partir dela. */\n  .cvwrap{position:absolute;inset:auto;left:50%;top:0;transform:translateX(-50%);\n    width:498px;height:354px}\n  canvas.big{display:block;width:100%;height:100%;image-rendering:pixelated}\n\n  /* barra ----------------------------------------------------------------- */\n  /* equipados: dentro do palco, coluna a direita -- o painel lateral ficava comprido demais.\n     A POSICAO da coluna mora la embaixo, em .eqcol. Aqui existia uma SEGUNDA copia da regra,\n     com position:absolute, que o navegador ja descartava desde que a coluna virou filha do HUD.\n     Duas copias da mesma regra e o erro que mais custou tempo neste projeto. */\n  /* flex:none e a GARANTIA de que a thumb nunca muda de tamanho. A coluna e um flex vertical de\n     altura limitada, e nela a altura e o eixo PRINCIPAL -- onde o flex-shrink padrao vale 1. Sem\n     esta linha os 44px sao uma sugestao e existe caminho para o navegador espremer as pecas para\n     caberem em vez de rolar. Decisao do usuario: tamanho fixo sempre, e rolagem quando faltar\n     espaco. Thumb de pixel art em proporcao quebrada fica esquisita. */\n  .eqi{position:relative;width:44px;height:44px;flex:none;border-radius:9px;overflow:hidden;\n    background:linear-gradient(180deg,#fbfbfd,#e7e7ec);border:1px solid #ffffff26;\n    display:flex;align-items:center;justify-content:center;box-shadow:0 3px 10px -2px #000a}\n  /* mesmo caso do .skbtn: 52px de celula num container de 44px, o flex encolhia e cortava a\n     direita da thumb. Defeito antigo, achado ao medir o seletor de skin. */\n  .eqi .thc{flex:none;transform:scale(.8);transform-origin:center}\n  .eqi .x{position:absolute;top:0;right:0;width:15px;height:15px;line-height:14px;text-align:center;\n    font:700 9px ui-sans-serif,system-ui;color:#fff;background:#e04a4a;border-bottom-left-radius:7px;\n    cursor:pointer;opacity:0;transition:opacity .1s}\n  .eqi:hover .x{opacity:1}\n  .eqi .semic{width:40px;height:40px;border:0}\n  .barra{display:flex;gap:7px;align-items:center;flex-wrap:wrap}\n  /* As regras do .chk sairam (01/09): Gun Stance deixou de ser checkbox e virou um toggle igual\n     ao Flip. Some junto todo o trabalho de desfazer o padding, a borda e o fundo que a regra\n     generica de input impunha ao controle nativo. */\n  /* PILL -- o padrao das referencias que o usuario mandou (01/09): capsula escura sobre o painel\n     claro, borda 1px mais clara que o corpo, icone colorido e o CONTEUDO acendendo no hover.\n     Por enquanto so o Loop e o Once usam; o resto da barra fica no estilo antigo ate convertermos.\n     Uma classe so ja basta para ganhar da regra generica de button, que e seletor de ELEMENTO --\n     mas os estados dela nao: button.on e button:hover tem uma classe cada, entao .pill.on e\n     .pill:hover precisam existir ou o botao ligado volta a ser o azul de sempre. */\n  /* O MATERIAL mora em variaveis porque ele reaparece em normal, hover, ligado e pressionado.\n     Este arquivo ja perdeu tempo caro com duas copias da mesma regra saindo de sincronia --\n     variavel e o jeito de a copia nao existir.\n     Relevo copiado da referencia do botao de interrogacao (01/09): a peca nao e chapada, ela tem\n     LUZ EM CIMA e SOMBRA EMBAIXO. Sao tres camadas e cada uma faz uma coisa -- um fio de luz de\n     1px no topo, um fio escuro de 1px embaixo (a espessura da peca) e uma sombra curta projetada,\n     que e o que descola o botao do painel.\n     DUAS PALETAS: esc* e a capsula escura (Loop, Once) e cla* e a clara (Weapon FX, Item FX, da\n     segunda referencia). Elas ficam guardadas aqui como tokens crus, e --corpo/--relevo/--borda\n     apenas APONTAM para uma delas. Trocar de pele passa a ser um seletor que reaponta as setas,\n     sem reescrever nenhum valor -- e por isso normal, hover e pressionado se re-skinam juntos e\n     de graca. */\n  .pill{--esc3:linear-gradient(180deg,#333b47 0%,#3f4855 54%,#4a5462 100%);\n    --escrel:var(--escluz),0 2px 4px -1px #0000005e;\n    --cla:linear-gradient(180deg,#d2d6da 0%,#c1c6cc 46%,#aab0b7 100%);\n    --cla3:linear-gradient(180deg,#aab0b7 0%,#bcc1c7 54%,#ccd0d5 100%);\n    --clarel:inset 0 1px 0 #ffffffa6,inset 0 -1px 0 #00000038,0 2px 4px -1px #00000042;\n    --cia3:linear-gradient(180deg,#2a8cb8 0%,#369ec8 54%,#48b2d8 100%);\n    --ciarel:inset 0 1px 0 #ffffff59,inset 0 -1px 0 #00000047,0 2px 4px -1px #00000047;\n    --corpo:var(--esc);--corpo3:var(--esc3);--relevo:var(--escrel);\n    --borda:#232932;--tinta:#d5dae1;--tinta2:#ffffff;--halo1:#ffffff;--halo2:#ffffff8c;\n    display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;\n    height:26px;padding:0 11px;border-radius:999px;line-height:1;\n    border:1px solid var(--borda);color:var(--tinta);\n    background:var(--corpo);box-shadow:var(--relevo);\n    transition:background .12s,color .12s,text-shadow .12s}\n  /* SO O ICONE: a largura se ajusta ao icone, como pedido. */\n  .pill.so{width:34px;padding:0;gap:0}\n  /* O Loop e 50% mais comprido que o Once (34 x 1,5 = 51), a pedido. Tres classes de proposito,\n     e nao duas: com .pill.play ele empataria com .pill.so e passaria a depender da ordem das\n     linhas, que e a armadilha que este bloco ja documenta la embaixo no :active. */\n  .pill.so.play{width:51px}\n  /* Os dois do zoom sao CIRCULOS, nao capsulas: largura igual a altura (26) com o mesmo raio de\n     999px ja fecha a circunferencia, sem precisar de border-radius proprio. Tres classes, entao\n     ganham do .pill.so de duas por especificidade e nao por ordem das linhas. */\n  .pill.so.zm,.pill.so.zp,.pill.so.anelbtn,.pill.so.pp,.pill.so.np,\n  .pill.so.pf,.pill.so.ul{width:26px}\n  /* QUADRADINHO: a forma dos frames, da referencia. Nem capsula nem circulo -- um ladrilho de\n     canto suave. Precisa declarar o raio proprio porque o .pill nasce com 999px, que e o que\n     arredonda os outros ate virarem pilula. */\n  .pill.qd{width:22px;height:22px;padding:0;gap:0;border-radius:5px;font-size:10px}\n  /* A cor do icone e PRESA aqui, em vez de herdada do botao. Assim cada estado declara o que o\n     icone faz -- repouso cinza, hover branco, ligado verde -- e o verde nao depende de ordem de\n     heranca para sobreviver. */\n  /* 14 e nao 13: PARIDADE. O botao mede 26 e a sobra e (26-13)/2 = 6,5 -- meio pixel que o\n     navegador resolve arredondando para baixo, e o icone nasce 1px abaixo do centro em TODOS os\n     botoes redondos. Medido em 02/09: caixa do botao com centro em 490, caixa do icone em 490,5.\n     Com 14 a sobra vira 6 exatos e o centro coincide. Regra geral para esta interface: icone par\n     em caixa par, impar em caixa impar -- o tamanho se escolhe pela paridade, nao pelo gosto. */\n  .pill .lk{width:14px;height:14px;color:var(--tinta);transition:color .12s,filter .12s}\n  /* Icone de TRACO (o olho). O .lk global desenha por preenchimento; estes sao contorno, entao\n     precisam desfazer o fill e ligar o stroke. Duas classes, entao ganha do .lk de uma. */\n  .pill .lkt{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}\n  /* HOVER: ACENDE O CONTEUDO E MAIS NADA. Nem fundo, nem borda, nem relevo, nem posicao -- o\n     botao fica absolutamente parado e so a lampada do meio liga.\n     As tres primeiras declaracoes parecem redundantes e NAO sao: elas repetem o estado de repouso\n     para BLOQUEAR as regras genericas de button. O seletor button:hover tem um ELEMENTO a mais na\n     especificidade que .pill, entao ele vence -- era ele que clareava a borda para #a9a9b6 e o\n     fundo para branco no hover, e era isso que dava a impressao de o botao ja estar sendo\n     clicado. Vao por variavel para nao virarem uma segunda copia dos valores. */\n  .pill:hover{background:var(--corpo);border-color:var(--borda);box-shadow:var(--relevo);\n    color:var(--tinta2);text-shadow:0 0 2px var(--halo1),0 0 5px var(--halo2)}\n  /* O ICONE acende com o MESMO desenho de luz do texto: nucleo solido de 2px colado no glifo mais\n     uma difusao curta de 4px. Dois drop-shadow encadeados porque a funcao aceita uma sombra cada,\n     e drop-shadow e nao text-shadow porque SVG nao responde a text-shadow.\n     O que incomodava antes era o BLUR, nao o icone acender: era um halo de 5px sem nucleo, que\n     borrava a peca inteira. Nos botoes so-de-icone esta e a unica lampada que existe -- sem ela\n     eles ficam sem retorno nenhum de hover. */\n  .pill:hover .lk{color:#ffffff;filter:drop-shadow(0 0 2px #ffffff) drop-shadow(0 0 4px #ffffff73)}\n\n  /* LIGADO = icone ACESO no tom #ddfe01 (escolhido pelo usuario em 01/09).\n     So o ICONE muda de cor: repintar a capsula faria o estado ligado virar outro componente\n     visual, em vez de o mesmo botao aceso. As duas linhas de corpo aqui existem so para desfazer\n     o azul do button.on generico, que tem uma classe e ganharia. */\n  .pill.on{background:var(--corpo);box-shadow:var(--relevo);border-color:var(--borda);\n    color:var(--tinta)}\n  /* mesmo bloqueio, para o button.on:hover generico nao devolver o azul */\n  .pill.on:hover{background:var(--corpo);border-color:var(--borda);box-shadow:var(--relevo)}\n  /* Empatado em especificidade com .pill:hover .lk (tres classes cada); vem DEPOIS no arquivo, e\n     por isso o icone ligado nao vira branco ao passar o mouse. */\n  .pill.on .lk{color:#ddfe01;filter:drop-shadow(0 0 5px #ddfe01b3)}\n  /* ACESO + HOVER: a mesma lampada, mais forte, no MESMO tom -- nucleo solido colado no glifo e\n     halo mais longo. Quatro classes, entao ganha das duas regras acima por especificidade e nao\n     por ordem. Sem esta regra o estado ligado era o unico sem retorno de hover. */\n  .pill.on:hover .lk{color:#ddfe01;\n    filter:drop-shadow(0 0 2px #ddfe01) drop-shadow(0 0 7px #ddfe01)}\n  /* VARIANTE CLARA (Weapon FX, Item FX), da referencia: capsula de CINZA CLARO com o texto\n     BRANCO. Nao e um botao branco de texto escuro -- essa foi a minha leitura errada na primeira\n     tentativa, e o resultado ficou sem parentesco nenhum com a imagem.\n     O cinza precisa ter CORPO para o branco assentar em cima: por isso o gradiente fecha em\n     #aab0b7 e nao perto do branco. Foi isso que decidiu a escolha do tom, nao gosto.\n     Nenhuma regra nova de fundo, relevo ou pressionado -- so as setas das variaveis mudando de\n     alvo. E como agora o corpo e cinza e nao quase-branco, o halo branco APARECE: --halo1 e\n     --halo2 ficam herdados do .pill de proposito, e o hover acende o texto aqui do mesmo jeito\n     que acende na capsula escura. */\n  .pill.claro{--corpo:var(--cla);--corpo3:var(--cla3);--relevo:var(--clarel);\n    --borda:#8f959c;--tinta:#ffffff;--tinta2:#ffffff}\n  /* VARIANTE CIANO (Randomize), da terceira referencia. Este botao NAO tem toggle: e uma acao,\n     nao um estado, entao aqui existe so repouso, hover e pressionado -- nenhuma regra .on, e o\n     tom nunca inverte.\n     Terceira pele pelo mesmo mecanismo das outras: reaponta as setas e pronto. Nao ha uma linha\n     de fundo, relevo, glow ou pressionado escrita de novo para ela. */\n  .pill.ciano{--corpo:var(--cia);--corpo3:var(--cia3);--relevo:var(--ciarel);\n    --borda:#1d6d94;--tinta:#ffffff;--tinta2:#ffffff}\n  /* LIGADO, e aqui a pele INVERTE: volta para a capsula escura, porque o pedido e que as palavras\n     fiquem BRANCAS -- e branco sobre corpo claro nao existe. Reaponta as mesmas setas de volta\n     para os tokens esc*, entao continua sem copia de valor nenhuma.\n     Tres classes, entao ganha do .pill.claro de duas sem depender da ordem das linhas. */\n  .pill.claro.on{--corpo:var(--esc);--corpo3:var(--esc3);--relevo:var(--escrel);\n    --borda:#232932;--tinta:#ffffff;--tinta2:#ffffff;--halo1:#ffffff;--halo2:#ffffff8c}\n  /* O rotulo e partido em dois SPANS porque so a sigla muda de cor quando ligado. Sem o span nao\n     haveria onde pendurar a cor e a frase inteira viraria verde. */\n  .pill.on .fx{color:#ddfe01}\n  /* O ACESO destes botoes fica com o glow PERMANENTE, nao so no hover (pedido explicito): e o que\n     faz o estado se ler sem o mouse estar em cima. Cada metade acende no proprio tom. */\n  .pill.claro.on{text-shadow:0 0 2px var(--halo1),0 0 5px var(--halo2)}\n  .pill.claro.on .fx{text-shadow:0 0 2px #ddfe01,0 0 5px #ddfe018c}\n  /* Na familia CLARA o aceso e todo BRANCO -- o tom #ddfe01 fica so para o Loop e o Once, e para\n     a sigla FX. Sem esta linha o olho do Slots herdaria o verde do .pill.on .lk generico.\n     O Slots nao pode ficar aceso PARADO (pedido de 01/09), so no hover e no clique -- e por isso\n     o filter:none explicito. Nao basta omitir o filter: a cascata resolve propriedade por\n     propriedade, entao omitir aqui nao apaga nada, apenas entrega a propriedade ao .pill.on .lk\n     de tres classes, que acende o olho de VERDE. Foi exatamente esse o erro na primeira\n     tentativa, e a medicao pegou.\n     E como o filter:none e de quatro classes, ele tambem venceria o hover de tres -- por isso a\n     linha seguinte, de cinco, devolve o glow so quando o mouse esta em cima. */\n  .pill.claro.on .lk{color:#ffffff;filter:none}\n  .pill.claro.on:hover .lk{filter:drop-shadow(0 0 2px #ffffff) drop-shadow(0 0 4px #ffffff73)}\n  /* Os frames nao tem regra de tinta propria: numero BRANCO nos dois estados, herdado direto do\n     .pill.claro como o Flip e o Weapon FX. Uma versao com numero escuro chegou a existir e foi\n     revertida (01/09) -- ela tambem obrigava a desligar os halos, porque glow branco atras de\n     texto escuro nao acende. */\n\n  /* PRESSIONADO -- vale para os DOIS estados, apagado e aceso. E apertando o botao ACESO que se\n     desliga, e ate 01/09 esse clique era o unico sem retorno nenhum.\n     Quem afunda e o INTERIOR, nao o botao: o transform:none desfaz o translateY(1px) da regra\n     generica de button, que descia a capsula inteira e parecia a pagina pulando.\n     O resto sao duas partes que dependem uma da outra -- o gradiente INVERTE (claro embaixo,\n     porque a luz passa a bater na parede de dentro) e a sombra projetada some, trocada por um fio\n     branco colado na borda mais um halo curto. Sem inverter o gradiente, o halo sozinho parece\n     selecao e nao pressao.\n     ESTE BLOCO E O ULTIMO DO .pill DE PROPOSITO, e nao por arrumacao: .pill:active empata em\n     especificidade com .pill:hover, e .pill.on:active empata com .pill.on:hover. Nos dois casos\n     quem decide e a ORDEM -- e como se esta sempre com o mouse em cima na hora de clicar, o hover\n     venceria e o pressionado nunca apareceria. Foi exatamente esse o defeito no estado aceso.\n     Mover estas linhas para cima quebra o botao em silencio. */\n  .pill:active,.pill.on:active{background:var(--corpo3);transform:none;\n    box-shadow:0 0 0 1px #ffffff4d,0 0 7px 1px #ffffff33,\n      inset 0 2px 3px #00000073,inset 0 -1px 0 #ffffff1a}\n  /* os botoes do painel esquerdo encolheram um tico (pedido de 01/09). O .pill fica de fora: ele\n     tem altura propria e ja nasceu no tamanho novo. */\n  .opts .barra button:not(.pill):not(.mestre){padding:5px 9px;font-size:11.5px}\n  /* randomize troca de rotulo: sem largura minima, a barra inteira desliza */\n  .barra .rnd{min-width:122px}\n  .barra .flip{min-width:52px}\n  /* O indicador de zoom passou para ENTRE os dois botoes (pedido de 01/09). A largura minima nao\n     e enfeite: o rotulo vai de uma para duas casas, e sem ela passar de 9x para 10x empurraria o\n     botao de mais para a direita. A regra desta interface e que nada pula. */\n  .zind{min-width:24px;text-align:center;margin:0 -3px}\n  /* O Generate desceu para o RODAPE do painel (01/09), sozinho e alinhado a direita. O\n     margin-top:auto so funciona porque .opts e flex column -- e a mesma mecanica do aviso legal\n     do painel direito. Quando o conteudo passa da altura do painel o auto vira zero sozinho e a\n     fileira volta a fluir no fim, sem sobrepor nada. */\n  .barra .port{min-width:196px}\n  .rodape{margin-top:auto;justify-content:flex-end}\n  /* O src da imagem e 3,2 MB de data-URI. Ele NUNCA entra na string do render: o <img> mora num\n     data-d2-static e recebe o src imperativamente, senao o morph diffaria 3 MB a cada interacao. */\n  /* fixed, nao absolute: o dialogo saiu de dentro do palco (que agora tem 236px) e passou a ser\n     filho do .wrap, entao ele cobre a tela toda sem depender de ancestral posicionado */\n  .portwrap{position:fixed;inset:0;z-index:6;display:none;align-items:center;justify-content:center;\n    background:#05060ae8}\n  .portwrap.on{display:flex}\n  /* O <img> e filho FLEX: sem align-items/justify-content o stretch padrao deforma a imagem\n     para preencher a caixa, e o retrato quadrado saia esticado. max-width/height sozinhos nao\n     seguram porque o stretch age no eixo cruzado. width/height auto + object-fit fecham. */\n  .portbox{max-width:76%;max-height:76%;display:flex;align-items:center;justify-content:center}\n  .portimg{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;\n    border-radius:12px;box-shadow:0 18px 50px -12px #000}\n  .portx{position:absolute;top:12px;right:12px}\n  /* Aviso da feature desligada. O dialogo tem largura fixa e a imagem rola dentro dele: sem isso\n     a vitrine de 670x1000 estouraria o palco em telas baixas. */\n  /* Aviso da feature desligada. O dialogo tem largura fixa e a imagem rola dentro dele: sem isso\n     a vitrine de 710x1060 estouraria o palco em telas baixas. */\n  /* O dialogo NAO deve precisar de rolagem. O que o deixava alto era o texto preso em 58ch:\n     com a caixa em 720 sobrava lateral e o paragrafo virava 6 linhas. Soltando a largura ele\n     cai para 4, e os ladrilhos menores tiram outros ~95px. */\n  .dlg{width:min(720px,92%);max-height:88%;overflow:auto;background:#fbfbfc;border:1px solid var(--lin);\n    border-radius:16px;padding:22px 26px 18px;box-shadow:0 24px 60px -20px #00000080}\n  .dlg{text-align:center}\n  .dlg h3{margin:0 0 4px;font-size:11px;justify-content:center}\n  /* o h3 global desenha uma barrinha no :before; centralizado ela fica solta na frente do\n     rotulo, entao some so aqui dentro */\n  .dlg h3:before{display:none}\n  .dlg .sub{margin:0 0 14px;font-size:18px;font-weight:650;letter-spacing:.2px;color:var(--txt)}\n  .dlg p{margin:0 0 16px;color:var(--dim);font-size:12.5px;line-height:1.65}\n  /* cada linha e um par: personagem, seta, ilustracao. O centro fixo mantem o par alinhado\n     mesmo quando a caixa cresce, e o gap e o que da o respiro pedido. */\n  .ppgrade{display:flex;flex-direction:column;gap:12px;margin:0}\n  .pplinha{display:flex;align-items:center;justify-content:center;gap:18px}\n  .pptile{width:${PP_TILE}px;height:${PP_TILE}px;flex:none;border-radius:12px;border:1px solid var(--lin);\n    background-repeat:no-repeat;background-size:${PP_TILE*2}px ${PP_TILE*3}px;\n    box-shadow:0 6px 18px -8px #000}\n  .ppseta{flex:none;color:#9a9aa6;font-size:17px;line-height:1;user-select:none}\n  .dlg .portx{position:static;display:block;margin:16px auto 0;min-width:110px}\n  /* SPINNER: fica acima do bloco de undo/redo, no canto inferior direito do palco. So aparece\n     quando ha imagem em voo -- a classe e escrita direto no DOM pelo rAF, nunca por re-render:\n     ligar/desligar por estado redesenharia a grade de 42 celulas a cada sprite que chega. */\n  .spin{position:absolute;right:8px;bottom:8px;width:14px;height:14px;border-radius:50%;\n    border:2px solid #00000024;border-top-color:var(--ac);opacity:0;transition:opacity .18s;\n    pointer-events:none;z-index:4}\n  .spin.on{opacity:1;animation:gira .7s linear infinite}\n  @keyframes gira{to{transform:rotate(360deg)}}\n  .sep{width:1px;height:20px;background:var(--lin);margin:0 3px}\n\n  /* scrollbar-gutter reserva a barra de rolagem SEMPRE. Sem isso, abrir uma categoria que faz o\n     painel crescer fazia a barra aparecer, a largura util encolher ~9px e a fileira de slots\n     re-quebrar -- era por isso que o off-hand pulava de linha ao selecionar hair. */\n  /* 22px nas laterais para casar com o painel esquerdo; em cima e embaixo continua 16. Aqui o\n     scrollbar-gutter FICA: este painel rola de verdade (a grade tem 53 paginas), e sem ele abrir\n     uma categoria maior faria a fileira de slots re-quebrar. */\n  /* MOLDURA ESCURA com CONTAINER CLARO dentro, a leitura da janela de inventario do jogo: o\n     painel deixou de ser o fundo do conteudo e virou o QUADRO em que o conteudo se apoia. Por\n     isso ele e o unico que nao usa --bg2 -- a assimetria com o painel esquerdo e o efeito, nao\n     um descuido. */\n  /* O painel usa o MESMO material do botao escuro, so que estatico: mesmo gradiente (--esc),\n     mesma borda #232932 e a mesma luz de borda (--escluz). A unica coisa que muda e a sombra\n     projetada, que aqui e larga -- os 2px do botao seriam invisiveis sob uma peca deste tamanho.\n     Estatico de proposito: nao ha hover nem pressionado, o painel nao e um alvo de clique. */\n  /* overflow:hidden no eixo X, auto no Y. O hidden nao e enfeite: a risca usa margem negativa\\n     para vazar ate as bordas do painel, e isso ESTENDE a area rolavel -- media 9px de rolagem\\n     horizontal antes desta linha. No Y a rolagem continua valendo. */\\n  .side{--gutter:12px;--pad:22px;padding:16px var(--pad);overflow:hidden auto;background:var(--esc);min-width:0;\n    border:1px solid #232932;border-radius:16px;\n    box-shadow:var(--escluz),0 12px 28px -14px #00000085;\n    display:flex;flex-direction:column;scrollbar-gutter:stable both-edges}\n  /* O aviso legal fica presa no rodape do painel, e margin-top:auto so funciona porque o .side\n     virou flex column. Quando o conteudo passa da altura do painel o auto vira zero sozinho e o\n     aviso volta a fluir no fim da lista, sem sobrepor nada e sem precisar de position. */\n  /* Discreto de proposito: o aviso precisa estar ali, nao precisa ser lido. A cor caiu de #54545f\n     para quase o fundo do painel (#15151b), e a linha divisoria desceu junto -- deixa-la em\n     var(--lin) faria dela o elemento mais forte do rodape, que e o contrario do pedido. */\n  /* BUSCA: campo arredondado com a lupa encostada na direita, como na referencia. Ela mora na\n     faixa de cima da caixa e cresce para ocupar a sobra; o min-width:0 e o que permite encolher\n     dentro do flex em vez de empurrar a contagem para fora. */\n  .buscabar{flex:1;min-width:0;position:relative;display:flex;align-items:center}\n  .busca{padding:6px 32px 6px 13px;border-radius:999px;border:1px solid #c6cad0;\n    background:#f4f5f7;font:400 12px ui-sans-serif,system-ui}\n  .buscabar .lk{position:absolute;right:11px;width:14px;height:14px;color:#8b8f96;\n    pointer-events:none}\n  .nota{margin-top:auto;padding-top:16px;border-top:1px solid #4e585e;color:#7d888f;\n    font-size:10px;line-height:1.7;text-align:right}\n  .nota b{color:#9aa5ac;font-weight:600}\n  /* A barra raspava na borda arredondada do painel (a de 16px) e parecia sair do desenho. O\n     truque e a borda TRANSPARENTE com background-clip:content-box: a pista mantem a largura\n     inteira, mas o polegar e pintado so no miolo dela -- ele nasce afastado dos dois lados em vez\n     de encostar. A borda solida de antes pintava um quadro #424c5a que so funcionava se o fundo\n     atras fosse exatamente aquela cor, e no gradiente do painel nao e.\n     E o gutter e both-edges, nao stable puro. Medido em 02/09: com a reserva so a direita, a\n     caixa de 560 tinha 500 de conteudo e o miolo ficava a 22px da borda esquerda e a 36 da\n     direita -- o painel parecia torto sem nada estar torto. E EXATAMENTE o mesmo defeito que ja\n     tinha aparecido no .opts do painel esquerdo (ver a nota la); subir o gutter de 9 para 14\n     tinha piorado o desvio em vez de corrigi-lo. both-edges reserva dos dois lados, entao o\n     conteudo fica centrado com barra ou sem barra.\n     --gutter, a reserva e a margem da .risca andam juntos ou a risca volta a parar antes. */\n  .side::-webkit-scrollbar{width:var(--gutter)}\n  /* A pista recua 14px em cima e embaixo para nao correr por cima do raio de 16px do painel --\n     era isso que fazia a barra parecer escapando do desenho nos cantos. */\n  .side::-webkit-scrollbar-track{background:transparent;margin:14px 0}\n  .side::-webkit-scrollbar-thumb{background:#6d7a82;background-clip:content-box;\n    border:3px solid transparent;border-radius:99px;min-height:48px}\n  .side::-webkit-scrollbar-thumb:hover{background:#87949c;background-clip:content-box}\n  /* O .nota mora direto na moldura escura e por isso tem tinta propria (ver a regra dele mais\n     abaixo). As regras .side h3 sairam junto com o bloco de cores, que mudou para o painel\n     esquerdo -- o painel direito nao tem mais nenhum h3. */\n\n  h3{margin:0 0 9px;font-size:10px;text-transform:uppercase;letter-spacing:.9px;color:var(--dim);\n     display:flex;align-items:center;gap:8px}\n  h3:before{content:\"\";width:3px;height:11px;border-radius:2px;background:var(--ac);opacity:.85;flex:none}\n  h3 em{font-style:normal;color:#5e5e6b;letter-spacing:.4px}\n\n  select,button,input{font:600 12px ui-sans-serif,system-ui,sans-serif;padding:6px 10px;border-radius:7px;\n    border:1px solid #cbcbd3;background:linear-gradient(180deg,#ffffff,#e9e9ee);color:var(--txt);\n    transition:background .12s,border-color .12s,box-shadow .12s}\n  button{cursor:pointer}\n  button:hover{border-color:#a9a9b6;background:linear-gradient(180deg,#ffffff,#f2f2f6)}\n  button:active{transform:translateY(1px)}\n  button:disabled{opacity:.38;cursor:default;transform:none}\n  select,input{width:100%;box-sizing:border-box}input{font-weight:400}\n  input:focus,select:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 3px #6c8cff2e}\n  button.on{background:linear-gradient(180deg,#5878e0,#3f5fd0);border-color:#3f5fd0;color:#fff;\n            box-shadow:0 2px 12px -4px var(--ac)}\n  button.on:hover{background:linear-gradient(180deg,#6684e8,#4a69d8)}\n\n  .acts{display:flex;flex-wrap:wrap;gap:4px}.acts button:not(.pill){padding:3px 7px;font-size:10px;border-radius:5px}\n  /* ABAS -- o padrao da janela de inventario do jogo, adaptado: as 15 categorias viram guias no\n     topo do painel, em duas fileiras (a largura nao comporta uma so, e a referencia tambem\n     empilha quando precisa). A ativa acende no ciano e se apoia na linha que separa as guias do\n     conteudo, que e o que da a leitura de aba emendada na caixa.\n     A classe catsel continua nos botoes: os handlers sao delegados por classe, entao trocar a\n     aparencia nao toca em nenhum d2.on. */\n  /* A RISCA embaixo das abas delimita onde a fileira termina, como na referencia, e o\n     margin-bottom abre o RESPIRO que deixa o fundo escuro do painel aparecer entre as abas e a\n     caixa branca. Antes era o contrario: margin negativo, para a aba encostar na caixa. Comparar\n     com a imagem mostrou que o jogo faz o oposto -- as abas terminam, a linha fecha, e a caixa\n     comeca depois. */\n  /* gap de duas partes: 0 entre as FILEIRAS e 3px entre as abas da mesma fileira. As fileiras\n     encostam uma na outra e a de baixo encosta na risca, como na referencia -- a borda inferior\n     transparente da aba e o que deixa a emenda limpa. */\n  /* FILEIRAS ELASTICAS (02/09). Antes era flex-wrap com 3px de gap, e sobrava um buraco na\n     direita da fileira de baixo -- 15 abas nao dividem certo em contagem nenhuma de colunas\n     fixas (8+7 sempre deixa uma lacuna, e 5+5+5 custa uma terceira fileira).\n     Com flex-grow cada FILEIRA estica os seus proprios itens ate encher a largura: nao sobra\n     buraco em fileira nenhuma, em largura nenhuma, sem media query e sem saber quantas abas sao.\n     E o mesmo mecanismo que ja deixa esta barra pronta para o mobile -- estreitando o painel as\n     abas se reorganizam sozinhas em mais fileiras, todas rentes.\n     Sao DOIS blocos .abas no markup, um por fileira, e nao um so que quebra sozinho -- e o que\n     permite dar a cada fileira a sua propria .risca, que e um elemento e nao uma borda. Tentei\n     antes com border-bottom na aba: a linha ficava colada na borda escura da fileira de baixo e\n     nao se lia, e abrir folga entre as fileiras mudava a altura do bloco. */\n  .abas{display:flex;flex-wrap:wrap;gap:0}\n  /* A RISCA e um elemento PROPRIO, nao a borda de baixo do .abas. Como borda ela encostava na aba\n     e virava o rodape dela; na referencia ela e uma linha separada, com fundo escuro dos dois\n     lados. As margens negativas de 22px anulam o padding do .side para ela ir de borda a borda do\n     painel, e nao so da largura do conteudo. */\n  /* O -22 anula o padding do painel; o --gutter anula a reserva do scrollbar-gutter, que so\n     existe do lado DIREITO. Sem ele a risca parava 9px antes da borda -- medido. Vai pela mesma\n     variavel que define a largura da barra de rolagem, entao as duas nao podem sair de sincronia. */\n  /* margin-top:-1px sobrepoe a risca a borda de baixo das abas da ULTIMA fileira. Sem isso as\n     duas ficariam adjacentes e o olho leria uma linha de 2px, mais grossa que a da fileira de\n     cima -- que agora tambem tem a sua. Mesma cor, entao sobrepostas viram uma so. */\n  /* As duas margens laterais sao IGUAIS agora que o gutter e both-edges: cada lado desconta o\n     padding de 22 mais a reserva da barra. Enquanto a reserva existia so a direita, esta margem\n     tambem era assimetrica -- e era o que fazia a risca parar antes da borda de um lado so. */\n  /* --pad em vez de 22 fixo: no mobile o painel aperta o padding e a risca precisa acompanhar,\n     senao ela para antes da borda de novo. */\n  .risca{height:1px;background:#6d7883;flex:none;\n    margin:-1px calc(-1 * (var(--pad) + var(--gutter))) 11px calc(-1 * (var(--pad) + var(--gutter)))}\n  /* A risca ENTRE as duas fileiras PASSA POR TRAS da fileira de baixo (pedido de 02/09). O\n     margin-bottom negativo puxa as abas de baixo 1px para cima, exatamente sobre a linha: como\n     elas vem DEPOIS no DOM e tem fundo opaco, pintam por cima e comem o trecho central.\n     Sobra visivel so o que sangra alem das abas -- os 34px de cada ponta, ate a borda do painel.\n     E o efeito da referencia: a linha nasce da margem e some atras das guias.\n     Nao ha respiro de 11px aqui: esse so existe na ultima risca, antes da caixa branca. */\n  .risca.junta{margin-bottom:5px}\n\n  /* MESMO MATERIAL do botao escuro, no tom cinza: gradiente --cin, borda #232932 e a mesma luz de\n     borda. O texto e branco com uma sombra curta atras -- e o truque da referencia para o branco\n     nao se dissolver no cinza claro do topo do gradiente. */\n  /* flex:1 1 auto -- a base e o CONTEUDO, entao rotulo longo continua maior que rotulo curto e\n     so a sobra da fileira e repartida; com 1 1 0 todas ficariam iguais e Cape teria o tamanho de\n     Off-Hand. O justify-content centra o texto quando a aba estica.\n     A borda de baixo continua TRANSPARENTE: quem fecha a fileira e a .risca logo abaixo dela,\n     que e puxada 1px para cima e encosta na aba. Duas fileiras, duas riscas iguais. */\n  /* min-width DECIDE A QUEBRA, e por isso ele existe. Sem piso, quantas abas cabem por fileira\n     depende da largura do TEXTO -- e isso varia com a fonte de cada maquina: o mesmo painel de\n     560px quebrou 9/6 no render headless e 10/5 no navegador do usuario (medido em 02/09), com a\n     fileira de cima espremida e a de baixo esticada. Com piso a conta vira aritmetica: 502px de\n     conteudo (560 menos os 44 de padding e os 14 do gutter) divididos por 60 dao 8 por fileira,\n     entao 8+7 em qualquer maquina. 9 nao cabem (540 > 502) e 7 sobrariam espaco.\n     8+7 e a divisao mais equilibrada possivel para 15 abas em duas fileiras -- 9+6 deixaria a de\n     baixo 50% mais larga que a de cima.\n     E continua elastico para baixo: num painel de 320px o piso da 5 por fileira, ou seja 5+5+5,\n     sem media query. */\n  .aba{flex:1 1 auto;min-width:60px;display:flex;align-items:center;justify-content:center;white-space:nowrap;\n    padding:5px 11px;font:600 11px ui-sans-serif,system-ui;border-radius:8px 8px 0 0;\n    border:1px solid #232932;border-bottom-color:transparent;\n    color:#ffffff;text-shadow:0 1px 2px #00000080;\n    background:var(--cin);box-shadow:var(--escluz);\n    text-transform:capitalize;transition:background .12s}\n  .aba:hover{background:var(--cin2)}\n  .aba.on{background:var(--cia);border-color:#1d6d94}\n  .aba.on:hover{background:var(--cia)}\n  /* O CONTAINER CLARO onde o conteudo vive. Ele NAO encosta nas abas: entre os dois fica o\n     respiro que mostra o fundo escuro do painel, e e a risca do .abas que fecha a fileira. */\n  .invcx{background:#fbfbfc;border:1px solid #b9bec4;border-radius:8px;padding:10px;\n    box-shadow:inset 0 1px 0 #ffffff,0 1px 2px #00000014}\n  /* A faixa de cima: a BUSCA ocupa o lugar onde ficava o nome da categoria (01/09) e a contagem\n     segue a direita, no papel do SLOT 86/256 da referencia. O nome saiu porque a aba acesa ja\n     diz qual categoria esta aberta -- era a mesma informacao duas vezes. */\n  .invtop{display:flex;align-items:center;justify-content:space-between;gap:10px;\n    margin-bottom:9px}\n  /* LARGURA RESERVADA, mesma doutrina do .frbox e do .pag .badge: a contagem vai de 39 a 2174 e\n     ganha o sufixo found na busca, entao sem piso ela muda de tamanho a cada digito e empurra o\n     campo de busca, que e flex:1. 88px cobre o pior caso (2174 found). flex:none impede que o\n     flex a encolha de volta, e o box-sizing faz o piso contar o padding. */\n  .invcont{font:11px ui-monospace,monospace;color:var(--dim);background:#f4f5f6;\n    border:1px solid #cdd0d4;border-radius:9px;padding:1px 11px;\n    flex:none;min-width:88px;box-sizing:border-box;text-align:center}\n  /* altura reservada para as 7 linhas: a ultima pagina (ou uma busca curta) nao sobe a paginacao */\n  /* 7 colunas x 6 linhas = os mesmos 42 por pagina, agora que o painel cresceu 25%.\n     A altura reservada acompanha: 6x52 + 5x6 = 342. */\n  /* QUADRADA e MAIOR QUE O RECORTE -- e essa e a unica regra que manda no numero de colunas.\n     O recorte do icone tem 52px. Ja tivemos 8 colunas antes, quando o painel era estreito, e a\n     celula saia 50,6: faltava 1,4px e era isso que decepava o pescoco das faces no pixel errado.\n     Voltamos a 8 colunas agora (01/09) NAO por gosto, mas porque o painel cresceu de 490 para\n     560px justamente para caber -- a celula da ~57, a mesma de quando eram 7. A ordem importa:\n     primeiro se decide o tamanho da celula, depois a largura do painel; nunca o contrario.\n     min-height reserva as 7 fileiras para a paginacao nao subir na ultima pagina, ou numa busca\n     curta: 7 x 57 mais 6 de gap x 6 = 435. */\n  .grade{display:grid;grid-template-columns:repeat(8,1fr);gap:6px;\n    grid-auto-rows:auto;min-height:435px;align-content:start}\n  .cel{background:linear-gradient(180deg,#ffffff,#f0f0f4);border:2px solid #dededf;border-radius:8px;\n    aspect-ratio:1;height:auto;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden;\n    padding:0;position:relative;box-shadow:0 1px 3px #0006;transition:transform .1s,box-shadow .1s,border-color .1s}\n  .cel:hover{border-color:var(--ac);transform:translateY(-1px);box-shadow:0 4px 12px -6px #00000073}\n  /* Selo NEW: canto superior DIREITO, que e o unico canto que nao briga com nada -- o recorte do\n     icone e centralizado e a folga sobra nas pontas. pointer-events:none para nao roubar o\n     clique da celula, que e a area toda. */\n  .cel .nv{position:absolute;top:2px;right:3px;z-index:2;pointer-events:none;\n    color:#ff7a00;font:800 9px ui-sans-serif,system-ui;letter-spacing:.4px;line-height:1;\n    /* contorno por text-shadow, nao -webkit-text-stroke: o stroke come o miolo da letra e num\n       corpo de 9px o texto fecha. So as 4 direcoes CARDEAIS -- com as diagonais o contorno\n       engorda no canto de cada curva e a 9px vira mancha. */\n    text-shadow:1px 0 0 #000,-1px 0 0 #000,0 1px 0 #000,0 -1px 0 #000}\n  .cel.on{border-color:var(--ac);box-shadow:0 0 0 3px #6c8cff40,0 4px 12px -3px #000a}\n  .cel img{max-width:46px;max-height:46px;image-rendering:pixelated}\n  .cel .thc{image-rendering:pixelated}\n  .semic{display:inline-flex;align-items:center;justify-content:center;width:46px;height:46px;\n    border:1px dashed #b7b7c2;border-radius:5px;color:#7a7a86;font:10px ui-monospace,monospace}\n  .pag{display:flex;gap:7px;align-items:center;justify-content:center;margin-top:9px}\n  .pag button:not(.pill){padding:4px 10px}\n  /* o contador cresce de 1 para 966 e empurrava a seta da direita */\n  .pag .badge{min-width:74px;text-align:center}\n  /* margin-top zerado dentro da .barra: os 8px de cima serviam a versao empilhada do painel\n     direito, e numa fileira eles desalinhavam a paleta do rotulo ao lado. */\n  .cores{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}\n  /* respiro proprio em cima e embaixo da fileira de cores, alem do gap do painel */\n  .barra .cores{margin:5px 0}\n  .cores button{width:27px;height:27px;padding:0;border-radius:7px;font-size:11px}\n\n  .badge{font:11px ui-monospace,monospace;color:var(--dim)}\n  .boot{padding:26px;color:var(--dim)}.carregando{color:var(--warn)}\n\n  /* HUD do palco: frames, action e a coluna de equipados, ancorados no topo direito ------- */\n  /* O HUD absoluto sobre o palco SAIU (01/09): as ferramentas desceram para o painel de opcoes\n     e os equipados viraram o anel em volta do personagem, conforme o esboco do usuario. As\n     regras .hud/.eqcol foram removidas junto -- nao vale deixar CSS morto neste arquivo, que ja\n     pagou caro por ter duas copias da mesma regra. */\n  /* undo/redo: so o glifo. Precisa zerar fundo e borda tambem no :hover, senao a regra generica\n     de button la de cima devolve o gradiente e o contorno. */\n  /* auto, nao 10px: o historico vai para a ponta direita da fileira de seletores */\n  .hist{display:flex;gap:1px;margin-left:auto}\n  /* Slots ancorado na DIREITA da fileira de transporte, a pedido. O auto come toda a sobra antes\n     dele, entao ele fica na ponta independente de quantos frames a action tenha. */\n  /* o zoom e o Slots viajam juntos para a ponta direita: o auto no primeiro do par come toda a\n     sobra, e os dois ficam colados na borda independente de quantos frames a action tenha */\n  .barra .zm{margin-left:auto}\n  /* O par de zoom ficava colado no Slots: com o gap de 7 uniforme e o .zind puxando -3px de cada\n     lado, o mais ficava a 4px do 2x e a 7px do toggle -- e opticamente parecia PERTENCER ao\n     toggle, porque o toggle e um circulo do mesmo tamanho e o 2x e texto. Dobrar so a folga\n     antes do Slots devolve o agrupamento sem mexer no ritmo do resto da fileira. */\n  .barra .anelbtn{margin-left:7px}\n  .hist button{background:none;border:0;padding:1px 7px;color:var(--dim);font-size:18px;line-height:1;\n    opacity:.5;transition:opacity .12s,color .12s}\n  .hist button:hover:not(:disabled){background:none;border:0;opacity:1;color:var(--txt)}\n  .hist button:disabled{opacity:.14}\n  /* min-height amarrada a --hudalt: e a MESMA constante que posiciona o topo do popup de action.\n     Uma so, para as caixas e o popup nao poderem discordar sobre a altura da fileira. */\n  .hudbox{box-sizing:border-box;min-height:var(--hudalt);background:#f7f7f9;border:1px solid var(--lin);border-radius:10px;padding:7px 9px;\n    box-shadow:0 2px 6px -4px #0000004d}\n  .hudbox h4{margin:0 0 6px;font:700 9px ui-sans-serif,system-ui;text-transform:uppercase;\n    letter-spacing:.8px;color:var(--dim)}\n  /* Larguras FIXAS no HUD. Antes cada caixa se ajustava ao conteudo: mudar de action trocava o\n     rotulo do botao, mudar de frame trocava a quantidade de botoes, e tudo deslizava. Agora cada\n     uma reserva a pior hipotese (5 frames, o rotulo mais longo) e nada mais anda. */\n  /* O grupo de FRAME saiu dos cartoes rotulados e virou um trecho da fileira de transporte\n     (01/09): rotulo curto mais os ladrilhos, na mesma altura dos botoes ao lado. Ele estava ali\n     por heranca, nao por funcao -- frame e transporte, igual ao play.\n     A largura minima continua existindo pelo mesmo motivo de sempre: o numero de frames muda com\n     a action (de 1 a 5), e sem reservar o pior caso a fileira desliza ao trocar de action.\n     200 = 62 do rotulo, 8 do gap e 126 dos ladrilhos (5 x 22 mais 4 de gap x 4). */\n  .frbox{display:inline-flex;align-items:center;gap:8px;min-width:200px}\n  .fralab{font:700 9px ui-sans-serif,system-ui;text-transform:uppercase;letter-spacing:.8px;\n    color:var(--dim);white-space:nowrap}\n  /* O contador numerico saiu da interface (01/09) mas o SPAN continua no DOM, escondido, e isso\n     NAO e resto esquecido: ele e a GUARDA do laco de animacao. O rAF compara o texto dele antes\n     de reescrever a classe do ladrilho ativo, e o valor volta a tracinho a cada re-render -- e e\n     essa volta que faz o destaque se recuperar sozinho.\n     Trocar por uma variavel de JS quebra justamente isso: depois de um re-render os ladrilhos\n     voltam sem a classe e a variavel ainda diria que esta tudo em dia. Ver a nota no draw. */\n  .fralab .frameind{display:none}\n  .frbox .acts{flex-wrap:nowrap}\n  /* static de proposito: quem ancora o .acpop e o .ohud, nao esta caixa. Ver a nota la em cima. */\n  .acwrap{position:static}\n  /* a seta fica PRESA na direita: label e seta sao dois filhos com space-between, em vez de a\n     seta ser texto colado no fim do rotulo e passear conforme o nome muda de tamanho */\n  .acbtn,.embtn,.orbtn{font-size:11px;padding:4px 9px;width:106px;display:flex;align-items:center;\n    justify-content:space-between;gap:6px}\n  .acbtn .acnome,.embtn .acnome,.orbtn .acnome{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .acbtn .acseta,.embtn .acseta,.orbtn .acseta{flex:none;opacity:.55;font-size:9px}\n  /* POPUP DE ACTION, reorganizado em 01/09 para caber sem rolar. Duas mudancas, e a segunda so e\n     possivel por causa da primeira:\n     1. Ele ocupa a LARGURA DA LINHA (left/right em 0 contra o .ohud) em vez dos 296px presos a\n        caixinha do ACTION. Medido antes: 320px de largura para 364px de conteudo em 352 visiveis.\n     2. O rotulo do grupo foi para a ESQUERDA dos botoes, numa coluna fixa, em vez de ficar em\n        cima deles. Cada grupo passa a gastar a altura dos BOTOES e nao botoes mais cabecalho --\n        sao cinco grupos, entao sao cinco linhas de cabecalho economizadas.\n     A separacao entre grupos passou a ser um filete, que custa 1px onde o espacamento custava 10.\n\n     O TOPO merece explicacao. Horizontalmente ele se mede pelo .ohud (left/right em 0), que e o\n     que o faz caber no painel sem estourar de nenhum lado. Verticalmente, porem, top:100% cairia\n     abaixo da fileira INTEIRA -- e o .ohud quebra em duas linhas, entao o popup nascia longe do\n     seletor, com a caixa de FRAME no meio do caminho. Por isso o topo sai de --hudalt, a altura\n     de uma fileira de caixas, que e a mesma constante que dimensiona as caixas.\n     LIMITE CONHECIDO: isto assume que o ACTION esta na PRIMEIRA fileira. Numa largura em que o\n     .ohud quebre antes dele, o popup abriria no lugar errado. Vale para o desktop de hoje; entra\n     na conta quando formos fazer a adaptacao mobile. */\n  .acpop{position:absolute;left:0;right:0;top:calc(var(--hudalt) + 6px);max-height:330px;overflow:auto;\n    background:#fbfbfc;border:1px solid var(--lin);border-radius:10px;padding:9px 12px;\n    box-shadow:0 12px 30px -12px #00000059;z-index:5}\n  .acpop .gr{display:grid;grid-template-columns:52px 1fr;align-items:start;gap:9px;\n    margin:0 0 6px;padding:0 0 6px;border-bottom:1px solid #ededf1}\n  .acpop .gr:last-child{margin-bottom:0;padding-bottom:0;border-bottom:0}\n  /* line-height casa o rotulo com a primeira fileira de botoes, para ele nao flutuar acima dela */\n  .acpop h4{margin:0;line-height:22px}\n  /* EARS e a excecao: sao quatro opcoes, e uma caixa da largura da fileira ficava com mais vazio\n     do que conteudo. Ele volta a se ancorar na PROPRIA caixa (por isso o position:relative no\n     .orwrap) e cresce em COLUNA. Com ~124px de largura nao cabe a coluna de rotulo de 52px do\n     layout horizontal, entao aqui o rotulo volta para cima da lista.\n     As regras precisam vir DEPOIS das do .acpop: o elemento carrega as duas classes e o desempate\n     entre .acpop h4 e .orpop h4 e por ordem, nao por especificidade. */\n  .orwrap{position:relative}\n  .orpop{left:0;right:0;top:calc(100% + 6px);padding:9px 10px}\n  .orpop .gr{display:block}\n  .orpop h4{margin:0 0 6px;line-height:normal}\n  .orpop .acts{flex-direction:column}\n  .orpop .acts button{text-align:left}\n  /* Seletor de skin: mesma mecanica do Action (botao + popup absoluto). Absoluto de proposito --\n     46 celulas fixas no painel dariam ~8 linhas de altura permanente, e a regra desta interface\n     e que nada pode empurrar o resto quando abre. Abre para a DIREITA (left:0) porque a caixa e\n     a primeira do HUD; com right:0 o popup avancaria sobre o personagem. */\n  .skwrap{position:relative}\n  .skbtn{padding:2px;width:44px;height:44px;display:flex;align-items:center;justify-content:center;\n    overflow:hidden;border-radius:9px;background:linear-gradient(180deg,#fbfbfd,#e7e7ec);\n    border:1px solid #ffffff26}\n  .skbtn:hover{border-color:#8aa4ff;background:linear-gradient(180deg,#fff,#ececf2)}\n  /* Abre CENTRADO na celula, ocupando a largura do anel inteiro: a celula da skin fica no meio\n     da fileira, entao o popup cresce para os dois lados e desce menos. Com 6 colunas ele descia\n     8 linhas; com 8 colunas sao 6, e as celulas passam a caber o recorte de 52px sem cortar. */\n  .skpop{position:absolute;left:50%;transform:translateX(-50%);top:calc(100% + 6px);\n    width:498px;max-height:300px;overflow:auto;\n    background:#fbfbfc;border:1px solid var(--lin);border-radius:10px;padding:10px 11px;\n    box-shadow:0 12px 30px -12px #00000059;z-index:5}\n  /* TAMANHO DO ICONE E DA CELULA, escolhido para serrilhar o MINIMO possivel.\n     O recorte da skin mede 52x52 -- medido no idxskin publicado, nao suposto (o 41x41 da doc e\n     o quadro de MUNDO, nao a celula da folha).\n     A conta: qualquer fator diferente de 1 empilha uma SEGUNDA reamostragem em cima da que o\n     dpr da tela ja faz. O 0,5 anterior era o pior dos dois mundos -- reduzia e ainda ficava\n     pequeno. Entao o icone fica em 1x e a celula tem que ser MAIOR que 52.\n     8 colunas em 498px de popup dao celula de ~55: 52 de arte com 3px de folga. Sao 6 linhas\n     para 46 skins, e o max-height corta em ~5 e deixa rolar -- assim o popup nao volta a ficar\n     alto, sem precisar encolher a arte. */\n  .skgrade{display:grid;grid-template-columns:repeat(8,1fr);gap:5px}\n  /* mesmo cartao das thumbs do navegador de itens, so que menor */\n  .skcel{position:relative;padding:0;aspect-ratio:1;overflow:hidden;border-radius:8px;cursor:pointer;\n    display:flex;align-items:center;justify-content:center;border:2px solid #dededf;\n    box-shadow:0 1px 3px #0006;background:linear-gradient(180deg,#ffffff,#f0f0f4)}\n  .skcel:hover{border-color:var(--ac);box-shadow:0 4px 12px -6px #00000073}\n  .skcel.on{border-color:var(--ac);box-shadow:0 0 0 3px #3f5fd040}\n  /* flex:none e OBRIGATORIO aqui. A celula da folha tem 52px e o container 44px: sem isto o\n     flex ENCOLHE o span para 38px (medido no runtime), e como o recorte e background-position\n     ancorado no canto superior esquerdo, encolher corta o lado DIREITO da arte -- a cabeca\n     aparecia deslocada. Largura declarada != largura usada era a causa. */\n  /* 1x, sem transform nenhum: e o unico jeito de nao acrescentar reamostragem propria.\n     A interpolacao suave foi tentada DUAS vezes aqui (global e depois so nas skins) e o usuario\n     rejeitou as duas: nitido mesmo dentado. Nao tentar uma terceira. */\n  .skbtn .thc,.skcel .thc{flex:none;transform:none;transform-origin:center;image-rendering:pixelated}\n  /* As regras de select SAIRAM daqui (01/09): Ears e Emote deixaram de ser controle nativo e\n     viraram botao mais popup, como o Action. Some junto o color-scheme:dark, que existia so\n     porque o popup do select e desenhado pelo SISTEMA e abria branco, ilegivel. O popup agora e\n     nosso e herda o tema como o resto da interface. A largura fixa do botao continua valendo pelo\n     mesmo motivo de antes -- os rotulos tem tamanhos diferentes e a caixa nao pode mudar de\n     largura ao trocar -- so que agora ela mora na regra do .acbtn logo acima. */\n  /* .eqbar tambem saiu com o HUD: nao existe mais coluna, e sim anel. O .eqi continua, porque\n     a APARENCIA da peca equipada nao mudou -- so onde ela mora. */\n  .eqi .eqc{position:absolute;inset:0;background:transparent;border:0;padding:0;cursor:pointer;z-index:1}\n  /* A thumb tem fundo CLARO, entao icone branco em cima dela some. Quem cria o contraste e este\n     veu escuro: fraco no hover (previa) e forte quando o item esta oculto (estado). Deixei a\n     peca em grayscale em vez de quase transparente -- com o veu por cima da para continuar\n     reconhecendo QUAL item esta escondido, que era o que se perdia. */\n  .eqi:after{content:\"\";position:absolute;inset:0;z-index:1;pointer-events:none;\n    background:#05060a;opacity:0;transition:opacity .12s}\n  .eqi:hover:after{opacity:.5}\n  .eqi.off:after{opacity:.68}\n  .eqi.off:hover:after{opacity:.74}\n  .eqi.off .thc,.eqi.off .semic{filter:grayscale(1) brightness(.92)}\n  /* MODO CADEADO: o anel inteiro ganha a classe, e nele o hover de ocultar sai de cena. Ate\n     01/09 estas duas regras falavam de .eqbar, a coluna que sumiu no redesenho -- estavam mortas,\n     e por isso o olho de invisivel ainda aparecia atras do cadeado. */\n  .anel.trmodo .eqi:hover:after{opacity:0}\n  /* O olho cortado aparece FANTASMA no hover (previa do que o clique faz) e SOLIDO quando o item\n     esta oculto (o estado). Mesmo icone nos dois papeis: o que muda e a opacidade, entao a previa\n     ja mostra exatamente onde a marca vai ficar. */\n  /* Centralizado por inset+margin:auto, NAO por left/top:50% com margem negativa. Sob o scale do\n     :active os 50% caem em pixel fracionario e o arredondamento muda de lado a cada quadro -- o\n     icone sambava. Assim o browser reparte a sobra simetricamente. */\n  .eqi .olho{position:absolute;inset:0;margin:auto;width:22px;height:22px;\n    z-index:2;pointer-events:none;opacity:0;transition:opacity .12s;\n    fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;\n    color:#f4f6f9;filter:drop-shadow(0 0 4px #000000c4)}\n  .eqi:hover .olho{opacity:.72}\n  .eqi.off .olho{opacity:1;filter:drop-shadow(0 0 6px #000000e0)}\n  /* confirmacao do clique: a thumb afunda por um instante. E o unico feedback que nao depende de\n     re-render, entao nao pisca de novo quando qualquer outro estado muda. */\n  /* will-change promove a thumb a uma CAMADA propria: o compositor escala o bitmap ja pronto, em\n     vez de o layout reposicionar cada filho e encaixar cada um num pixel de dispositivo inteiro.\n     Sem isto o icone sambava em zoom quebrado (110%, 125%) e ficava quieto em 100% e 150% --\n     exatamente onde o fator de escala cai em inteiro. Sao 14 thumbs, entao o custo de camada e\n     irrelevante. */\n  .eqi{transition:transform .08s;transform-origin:center;will-change:transform;\n    backface-visibility:hidden}\n  .eqi:active{transform:scale(.93)}\n  /* no modo cadeado o olho sai de cena: ali o clique tranca, nao oculta */\n  .anel.trmodo .eqi .olho{display:none}\n  .eqi .x,.eqi .cad,.eqi .trv{z-index:3}\n  /* No modo cadeado o estado e COR + FORMA: destravado = cadeado aberto quase branco; travado =\n     cadeado fechado em verde neon. O halo do aberto e ESCURO, nao branco: brilho branco atras de\n     icone branco nao separa nada -- quem cria a borda contra a thumb clara e a sombra. Nos dois\n     casos drop-shadow (e nao box-shadow), que segue o contorno do icone em vez da caixa. */\n  .eqi .cad{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\n    background:#0000007a;cursor:pointer;color:#f4f6f9}\n  .eqi .cad .lk{filter:drop-shadow(0 0 4px #000000ad)}\n  .eqi .cad.tr{background:#14170099;color:var(--neon)}\n  /* Glow COLADO no cadeado, nao espalhado pelo slot: um halo de 6px num icone de 26 vaza para a\n     celula inteira e lava o sombreado do poco. Mesma gramatica dos botoes -- nucleo solido de 2px\n     mais difusao curta de 4px. */\n  .eqi .cad.tr .lk{filter:drop-shadow(0 0 2px #ddfe01) drop-shadow(0 0 4px #ddfe0180)}\n  .eqi .trv{position:absolute;top:0;left:0;width:16px;height:16px;display:flex;align-items:center;\n    justify-content:center;background:#141700e8;color:var(--neon);border-bottom-right-radius:7px}\n  /* O cadeado mestre nao e um BOTAO, e um ICONE SOLTO ao lado do Randomize (01/09): sem corpo,\n     sem borda, sem relevo, sem capsula. Por isso ele nao usa .pill -- as tres primeiras\n     declaracoes existem so para desmontar o botao que a regra generica desenha por baixo, e o\n     :hover repete o desmonte porque button:hover tem um elemento a mais na especificidade e\n     devolveria fundo e borda. Pelo mesmo motivo ele esta excluido do :not(.mestre) la em cima:\n     aquela regra de padding tem tres classes e venceria.\n     ESTADOS: preto da paleta parado, halo branco no hover, e #ddfe01 quando o modo esta ligado.\n     O mestre NUNCA abre o desenho do cadeado: ele nao representa travado/destravado, e sim\n     -- estou no modo --, por isso o render passa sempre cadeado(false). */\n  .mestre{background:none;border:0;padding:0;width:26px;height:26px;flex:none;\n    display:flex;align-items:center;justify-content:center;color:#232932;\n    transition:color .12s,filter .12s}\n  .mestre .lk{width:17px;height:17px}\n  .mestre:hover{background:none;border:0;color:#232932;\n    filter:drop-shadow(0 0 2px #ffffff) drop-shadow(0 0 6px #ffffffb3)}\n  .mestre:active{transform:none}\n  /* O PREFIXO .barra NAO E DECORACAO. O button.on generico do projeto pinta a capsula azul e tem\n     um ELEMENTO a mais na especificidade que .mestre.on, entao vencia -- era esse o azul que\n     aparecia atras do cadeado ligado. Com .barra sao tres classes e a disputa volta. O mesmo\n     vale para o :hover, que tem a sua propria regra generica. */\n  .barra .mestre.on{background:none;border:0;box-shadow:none;color:var(--neon)}\n  /* CONTORNO no cadeado ligado. O #ddfe01 tem luminancia alta e o painel tambem, entao sobre o\n     #eeeeee o icone quase some -- e o mesmo limite que ja apareceu no glow branco. Um traco\n     escuro resolve por CONTRASTE em vez de por brilho.\n     paint-order:stroke fill e o que faz dele contorno de verdade: o traco e desenhado ANTES do\n     preenchimento, entao a metade interna fica coberta e sobra so a borda por fora. Sem isso o\n     traco ficaria centrado na silhueta e comeria o miolo do desenho.\n     So no estado ligado: parado o icone ja e #232932 e um contorno preto sobre preto so engrossa. */\n  .barra .mestre.on .lk{stroke:#232932;stroke-width:2.5;paint-order:stroke fill;\n    stroke-linejoin:round}\n  /* o glow do hover e BRANCO nos dois estados: ele fala do mouse, nao do modo */\n  .barra .mestre.on:hover{background:none;border:0;box-shadow:none;\n    filter:drop-shadow(0 0 2px #ffffff) drop-shadow(0 0 6px #ffffffb3)}\n\n  /* Cadeado: SVG inline, tamanho por contexto e cor por currentColor.\n     ATENCAO: nada de crase neste bloco -- ele inteiro e um template literal. */\n  .lk{display:block;fill:currentColor}\n  .eqi .cad .lk{width:21px;height:21px}\n  .eqi .trv .lk{width:11px;height:11px}\n  /* sem contorno: a cor e o proprio elemento, e a borda escura suja os tons vibrantes */\n  .cores button.sw{width:24px;height:24px;border-radius:7px;border:0;padding:0;\n    box-shadow:0 1px 2px #0000003d}\n  /* borda ESCURA no selecionado: a paleta mudou para o painel claro, e o contorno branco de\n     antes (herdado do tema escuro) desaparecia contra o #eeeeee. O anel azul continua sendo o\n     mesmo do item selecionado na grade. */\n  /* Selecionado: um fio branco POR DENTRO mais um pouco de elevacao. O anel colorido por fora\n     brigava com o proprio swatch -- verde sobre verde nao destaca nada. Por dentro o branco\n     recorta em qualquer tom, do preto ao amarelo, e a peca so parece um pouco mais viva. */\n  .cores button.sw.on{box-shadow:inset 0 0 0 2px #ffffff,inset 0 0 0 3px #00000038,\n    0 2px 7px -1px #00000059}\n\n  /* A interpolacao suave em dpr fracionario foi TESTADA e REJEITADA pelo usuario (01/09):\n     ele prefere nitido mesmo dentado. Tudo segue em image-rendering:pixelated.\n     Fica registrada a medicao, porque a pergunta vai voltar: em dpr 1,25 ou 1,5 cada pixel da\n     arte cai em 1,25 ou 1,5 pixel de tela, entao uns viram 1 e outros viram 2, e nao existe\n     conserto por CSS -- so ficaria exato exibindo a arte em multiplo inteiro de 1/dpr.\n     O que DA para controlar e nao empilhar uma segunda reamostragem por cima: por isso todo\n     icone e exibido em 1x, e o canvas do palco casa 1:1 com a tela. Ver a nota do .skgrade. */\n\n  /* MOBILE: as regras antigas foram removidas porque falavam de .stage, .hud e .cvwrap absoluto,\n     que nao existem mais depois do redesenho de 01/09. Fica so o empilhamento, que continua\n     valendo. A adaptacao fina de mobile e trabalho a parte, adiada por decisao do usuario. */\n  /* ---- MOBILE (03/09) --------------------------------------------------------------\n     NAO e a adaptacao definitiva -- e o minimo para o app ser USAVEL no celular. A medida veio\n     do analytics no primeiro dia: 7 de 8 visitantes reais entre 360 e 440px, NENHUM equipando\n     nada, sessoes de 0 a 16 segundos. Nao era desinteresse, era gente que nao conseguia usar.\n     A ordem abaixo e a ordem em que as coisas bloqueiam. */\n  @media (max-width:768px){\n    /* 1. ROLAR. O .wrap nasce com overflow:hidden; empilhado, o conteudo passa da tela e nada\n       abaixo da primeira dobra existia. Sem isto, nenhum outro ajuste importa. */\n    .wrap{padding:8px;overflow-y:auto;overflow-x:hidden;display:block}\n    /* 2. EMPILHAR com altura de CONTEUDO. O minmax(0,1fr) de antes prendia o painel direito na\n       altura da tela e cortava a grade por dentro; com auto os dois crescem e quem rola e o wrap. */\n    .app{grid-template-columns:1fr;grid-template-rows:auto auto;max-width:none;gap:14px}\n    .esq{grid-template-rows:auto auto;overflow:visible}\n    .opts{overflow:visible;padding:0 12px 12px}\n    /* 3. ANEL com celula de 52. O recorte do icone tem 52px e pixel art nao se reduz -- 52 e o\n       PISO, nao uma escolha de gosto. 7x52 + 6x3 = 382, que ainda estoura um aparelho de 360,\n       entao o visor rola na horizontal.\n       margin:auto no filho em vez de justify-content:center: com justify-content, ao estourar o\n       lado ESQUERDO fica inalcancavel pela rolagem. Com margem automatica, nao. */\n    .visor{padding:12px 0;overflow-x:auto;justify-content:flex-start}\n    .anel{width:382px;margin:0 auto}\n    .anelcima{grid-template-columns:52px 1fr 52px;grid-template-rows:repeat(4,52px);gap:3px}\n    .anelbaixo{grid-template-columns:repeat(7,52px);gap:3px}\n    .slot,.slot.eqi{width:52px;height:52px}\n    /* O canvas tem tamanho fixo e o draw() ancora o personagem por Math.min(H, PALCO_ALT), onde\n       H e a altura DO WRAPPER. Com o palco em 214 e o wrapper ainda em 354, o eixo cairia em 237\n       e os pes sairiam do recorte. Casar o wrapper com a altura nova do palco corrige.\n       Mexer no tamanho do canvas e seguro aqui: o draw() reescreve o backing store e redesenha no\n       quadro seguinte -- e a guarda de W/H zero ja existe. */\n    .cvwrap{width:100%;height:214px}\n    /* 4. GRADE em 4 colunas. Com 12px de padding num 390 sobram ~350: celula de ~80px, bem acima\n       dos 52 do recorte. min-height zerado para a paginacao acompanhar o conteudo. */\n    .side{--pad:12px;--gutter:8px}\n    .grade{grid-template-columns:repeat(4,1fr);min-height:0}\n    /* flex:0 aqui, ao contrario do desktop. Com grow, a ultima aba que sobra numa fileira estica\n       sozinha para a largura toda -- \"Earring\" virava uma barra. Sem grow as fileiras ficam um\n       pouco irregulares, o que e bem melhor que uma aba gigante. */\n    .abas{justify-content:center}\n    .aba{flex:0 1 auto;min-width:0;padding:5px 8px}\n\n    /* 6. OS TRES SELETORES NUMA FILEIRA SO (08/09). Cada .hudbox mede 106 de botao mais 20 de\n       padding e borda: 3 x 126 mais 16 de gap = 394 num util de ~350, entao o ACTION quebrava\n       para baixo do EARS.\n       nowrap com flex:1 e min-width:0 faz as tres CEDEREM em vez de quebrar. Encolher nao\n       esconde nada: o rotulo ja tinha ellipsis (.acnome), e a seta e flex:none.\n       E isto conserta de graca a PENDENCIA da 33.10 -- o .acpop se ancora em --hudalt, a altura\n       de UMA fileira, premissa que a quebra derrubava e que agora nao pode mais ser quebrada. */\n    .ohud{gap:6px;flex-wrap:nowrap}\n    .ohud .hudbox{flex:1 1 0;min-width:0;padding:7px 6px}\n    .acbtn,.embtn,.orbtn{width:100%;min-width:0;padding:4px 6px;gap:4px;font-size:10.5px}\n    .hist{flex:none;margin-left:0}\n    /* o popup de EARS se ancora na PROPRIA caixa (nao na fileira), e a caixa agora tem ~90px:\n       sem um piso os quatro rotulos quebrariam em duas linhas cada. Cresce para a direita, que\n       e o lado que sobra -- ears e a primeira caixa da fileira. */\n    .orpop{right:auto;min-width:150px}\n\n    /* 7. FRAME sem o rotulo, so os ladrilhos. O <span class=\"frameind\"> CONTINUA no DOM dentro\n       do .fralab, e isso importa: ele e a GUARDA do laco de animacao, que compara o textContent\n       antes de repintar o ladrilho ativo. display:none no pai nao mexe em textContent nem em\n       querySelector, entao a guarda sobrevive -- por isso .fralab e nao um render condicional. */\n    .frbox{min-width:0}\n    .frbox .fralab{display:none}\n\n    /* 8. O POPUP DE SKIN saia \"por tras do painel\". NAO era z-index: no mobile o .visor ganhou\n       overflow-x:auto (item 3 aqui em cima) para o anel de 382 caber num 360, e um container que\n       recorta em UM eixo recorta no OUTRO tambem -- overflow:visible computa para auto quando o\n       par nao e visible. O popup nasce dentro do .visor, entao era cortado na borda de baixo.\n       position:fixed tira ele do .visor de vez: nenhum ancestral tem transform ou will-change\n       (o .eqi tem, mas nao e ancestral da celula de skin), entao ele se mede pela VIEWPORT.\n       E o mesmo caminho que o .portwrap ja usa.\n       FOLHA DE BAIXO, nao centrado: o unico jeito de FECHAR e reclicar o botao da skin, e um\n       popup centrado cobriria justamente esse botao. Preso embaixo, o anel continua visivel.\n       auto-fill de 55px porque a celula tem que ser MAIOR que os 52 do recorte -- pixel art nao\n       reduz -- e assim o numero de colunas acompanha a largura do aparelho. */\n    .skpop{position:fixed;left:8px;right:8px;bottom:8px;top:auto;transform:none;\n      width:auto;max-width:none;max-height:44vh;z-index:60}\n    .skgrade{grid-template-columns:repeat(auto-fill,55px);justify-content:center}\n  }\n  `,\n\n  render(d2){\n    const s=d2.state;\n    if(s.erro)return `<div class=\"boot\">Failed<br><span class=\"badge\" style=\"color:#ff8080\">${esc(s.erro)}</span></div>`;\n    if(!s.pronto)return `<div class=\"boot\">Loading…<br><span class=\"badge\">${esc(s.etapa||'')}</span></div>`;\n    const skr=SKINPIX[(s.skins[s.iSkin]||{}).id]||{};\n    const acoes=ordenaAcoes(Object.keys(skr.geo||{}));\n    const acao=s.acao&&acoes.indexOf(s.acao)>=0?s.acao:acoes[0];\n    // quantos frames o CORPO tem nesta action -- a skin e a autoridade, nao o `frames` da tabela\n    const nfr=keys(skr.geo,acao).length||1;\n    const {lista,paginas,pag,fatia}=pagina(s);\n    // Na aba All nao existe UMA categoria: o seletor de cor passa a valer para o slot do ultimo\n    // item equipado (`corCat`), e o destaque da grade olha TODOS os equipados, nao um so.\n    const corCat=s.catAtual==='all'?(s.corCat||'hair'):s.catAtual;\n    const eqid=s.equip[corCat];\n    const estilo=((IDX[corCat]||{}).items||[]).find(r=>r.cores&&r.cores.some(cc=>cc[0]===eqid));\n    const EQIDS=Object.create(null);for(const k in s.equip)EQIDS[s.equip[k]]=1;\n    // miniatura do equipado: META[variante] aponta para a linha do ESTILO, que e quem tem o `ic`;\n    // as coords sao da folha da categoria daquele slot, por isso o sheet vem de IDX[slot].\n    // UMA CELULA DO ANEL. Ela existe SEMPRE, equipada ou nao -- pedido do usuario, e e o que faz\n    // o anel ser um mapa do personagem em vez de uma lista que cresce.\n    //   vazia    -> nome do slot, e clicar leva a grade para aquela categoria\n    //   equipada -> vira `.eqi` e herda aparencia e handlers de sempre; o nome SOME, porque a\n    //               arte ja diz o que e e 44px nao cabem os dois sem cortar um.\n    const celula=sl=>{\n      if(sl==='__skin__'){\n        const k=s.skins[s.iSkin]||{};\n        return `<div class=\"slot skwrap\"><button class=\"skbtn\" title=\"Skin · ${esc(k.name||'')}\">${thSkin(k)}</button>`+\n          (s.skAberto?`<div class=\"skpop\"><div class=\"skgrade\">${s.skins.map((x,i)=>`<button class=\"skcel ${i===s.iSkin?'on':''}\" data-i=\"${i}\" title=\"${esc(x.name||'')} · ${esc(x.id||'')}\">${thSkin(x)}</button>`).join('')}</div></div>`:'')+`</div>`;\n      }\n      const rot=ANEL_ROT[sl]||sl, id=s.equip[sl];\n      if(!id)return `<div class=\"slot vaz\" data-cat=\"${esc(sl)}\" title=\"${esc(rot)}\"><span class=\"slab\">${esc(rot)}</span></div>`;\n      const r=META[id],ix=IDX[sl],tc=THUMBCACHE[id];let th;\n      if(r&&r.ic&&ix){const[sh,x,y,w,h]=r.ic;\n        th=`<span class=\"thc\" style=\"display:inline-block;width:${w}px;height:${h}px;background-image:url(${esc(ix.sheets[sh])});background-position:-${x}px -${y}px\"></span>`;}\n      else th=`<span class=\"semic\">${esc(String(id).slice(-4))}</span>`;\n      // Recorte vindo do CACHE, quando o indice da categoria ainda nao chegou. Reatribui `th`\n      // em vez de virar um `else if` la em cima so para nao reescrever a linha do caminho\n      // normal, que e a quente. A folha e remontada de MEDIA+ref (o ref foi validado por OKREF\n      // na leitura) -- guardar a URL pronta seria injecao dentro do url(...) do style.\n      if(tc&&tc.ic&&tc.sh&&!(r&&r.ic&&ix))th='<span class=\"thc\" style=\"display:inline-block;width:'+(tc.ic[3]|0)+'px;height:'+(tc.ic[4]|0)+'px;background-image:url('+esc(MEDIA+tc.sh)+');background-position:-'+(tc.ic[1]|0)+'px -'+(tc.ic[2]|0)+'px\"></span>';\n      const trav=!!s.travado[sl], oc=!!s.oculto[sl];\n      const cad=s.travaModo?`<span class=\"cad ${trav?'tr':''}\" data-slot=\"${esc(sl)}\">${cadeado(!trav)}</span>`:'';\n      const tv=(!s.travaModo&&trav)?`<span class=\"trv\" title=\"Kept by randomize\">${cadeado(false)}</span>`:'';\n      const est=oc?' · Hidden':(trav?' · Locked':'');\n      const bx=s.travaModo?'':`<span class=\"x\" data-slot=\"${esc(sl)}\">✕</span>`;\n      return `<div class=\"slot eqi ${oc?'off':''}\" title=\"${esc(rot)} · ${esc((r&&r.name)||'')} · ${esc(id)}${est}\">${th}${olho()}<button class=\"eqc\" data-slot=\"${esc(sl)}\"></button>${tv}${bx}${cad}</div>`;\n    };\n    const frbox=`<div class=\"frbox\"><span class=\"fralab\">Frame<span class=\"frameind\">–</span></span>`+\n      `<div class=\"acts frames\">${Array.from({length:nfr},(x,i)=>`<button class=\"fr pill claro qd\" data-f=\"${i}\">${i}</button>`).join('')}</div></div>`;\n    const acgr=GRUPOS.map(g=>{\n      const ks=acoes.filter(a=>grupoUI(a)===g);\n      if(!ks.length)return '';\n      return `<div class=\"gr\"><h4>${g}</h4><div class=\"acts\">${ks.map(a=>`<button class=\"ac ${a===acao?'on':''}\" data-a=\"${esc(a)}\" title=\"${esc(a)}\">${esc(rotuloAcao(a))}</button>`).join('')}</div></div>`;\n    }).join('');\n    // Thumb da skin: mesma folha/recorte das outras categorias (`ic` = [folha,x,y,w,h]).\n    const thSkin=k=>(k&&k.ic&&SKINSHEET)\n      ? `<span class=\"thc\" style=\"display:inline-block;width:${k.ic[3]}px;height:${k.ic[4]}px;background-image:url(${esc(SKINSHEET)});background-position:-${k.ic[1]}px -${k.ic[2]}px\"></span>`\n      : `<span class=\"semic\">${esc(String((k&&k.id)||'').slice(-4))}</span>`;\n    // O ANEL. A ordem dos filhos de .anelcima importa: 4 da esquerda, o palco, 4 da direita --\n    // e o que as regras de nth-child do CSS posicionam. O canvas mora aqui dentro, no meio.\n    const anel=`<div class=\"anel ${s.anelOff?'off':''} ${s.travaModo?'trmodo':''}\">\n      <div class=\"anelcima\">${ANEL_ESQ.map(celula).join('')}<div class=\"palco\"><div class=\"cvwrap\" data-d2-static><canvas class=\"big\"></canvas></div><button class=\"reset\" title=\"Reset character position\">${girinha()}</button><span class=\"spin\" aria-hidden=\"true\"></span></div>${ANEL_DIR.map(celula).join('')}</div>\n      <div class=\"anelbaixo\">${ANEL_BAIXO.map(celula).join('')}</div></div>`;\n    // Dropdown simples: sao quatro opcoes e a diferenca entre elas nao cabe numa thumb de 44px.\n    // A lista de expressoes e ESTATICA: sao ~30 no dump inteiro, e descobrir quais a face tem\n    // exigiria baixar o bloco so para montar o dropdown. Expressao que a face nao tem cai no\n    // default sozinha (mesmo fallback do engine local), entao listar todas nao quebra nada.\n    // Ears e Emote seguem o padrao do Action: botao com o valor atual e popup ancorado na\n    // fileira. Reusam o .acpop e o .gr de proposito -- tem um grupo so cada um, entao o rotulo a\n    // esquerda e a fileira de botoes a direita ja e a forma certa, sem CSS novo.\n    const emAtual=s.emote||'default';\n    const embox=`<div class=\"hudbox emwrap\"><h4>emote</h4>\n      <button class=\"embtn\"><span class=\"acnome\">${esc(capitaliza(emAtual))}</span><span class=\"acseta\">▾</span></button>\n      ${s.emAberto?`<div class=\"acpop\"><div class=\"gr\"><h4>emote</h4><div class=\"acts\">${EMOTE_LISTA.map(t=>`<button class=\"emit ${t===emAtual?'on':''}\" data-t=\"${esc(t)}\">${esc(capitaliza(t))}</button>`).join('')}</div></div></div>`:''}</div>`;\n    const orAtual=s.orelha||'humanEar';\n    const orbox=`<div class=\"hudbox orwrap\"><h4>ears</h4>\n      <button class=\"orbtn\"><span class=\"acnome\">${esc(ORELHA_ROT[orAtual]||orAtual)}</span><span class=\"acseta\">▾</span></button>\n      ${s.orAberto?`<div class=\"acpop orpop\"><div class=\"gr\"><h4>ears</h4><div class=\"acts\">${ORELHA_TIPOS.map(t=>`<button class=\"orit ${t===orAtual?'on':''}\" data-t=\"${esc(t)}\">${esc(ORELHA_ROT[t]||t)}</button>`).join('')}</div></div></div>`:''}</div>`;\n    const acbox=`<div class=\"hudbox acwrap\"><h4>action</h4>\n      <button class=\"acbtn\"><span class=\"acnome\">${esc(rotuloAcao(acao))}</span><span class=\"acseta\">▾</span></button>\n      ${s.acAberto?`<div class=\"acpop\">${acgr}</div>`:''}</div>`;\n    // A paleta de cor mudou de painel (01/09): saiu da coluna de itens e virou uma fileira do\n    // painel esquerdo, entre o Flip e o Randomize.\n    // O rotulo virou fixo em Hair Color e deixou de trazer o nome do estilo. Isso so e correto\n    // porque hair e a UNICA categoria com variante de cor -- medido no catalogo inteiro: as\n    // outras treze tem exatamente uma cor por estilo. Se alguma vier a ter, este rotulo mente.\n    // A paleta fica SEMPRE visivel, mesmo sem cabelo equipado: ela deixou de ser a lista de\n    // variantes do item atual e passou a ser a COR ESCOLHIDA. Por isso e desenhada a partir da\n    // PALETA, e nao de estilo.cores -- os data-* da variante so entram quando ha um estilo, e sem\n    // eles o clique apenas registra a escolha.\n    const corbox=`<div class=\"barra\"><span class=\"fralab\">Hair Color</span><div class=\"cores\">${PALETA.map((cor,dg)=>{\n        const v=(estilo&&estilo.cores)?estilo.cores.find(cc=>Number(String(cc[0]).slice(-1))===dg):null;\n        const dados=v?` data-id=\"${esc(v[0])}\" data-ref=\"${esc(v[1])}\" data-geo=\"${esc(estilo.geoRef)}\" data-cat=\"${esc(corCat)}\"`:'';\n        return `<button class=\"cor sw ${dg===s.corSel?'on':''}\" data-dg=\"${dg}\"${dados} title=\"${esc(capitaliza(NOMECOR[dg]||('variant '+dg)))}\" style=\"background:${cor}\"></button>`;\n      }).join('')}</div></div>`;\n    // PAINEL DE OPCOES: o que era HUD flutuante sobre o palco, mais o que era a barra de baixo.\n    // O cadeado mestre veio junto -- ele governa o anel, entao fica perto dos controles dele.\n    // Os handlers sao delegados por CLASSE, entao mover tudo isto no DOM nao mexe em nenhum d2.on.\n    // O FRAME saiu daqui: os cartoes rotulados ficam so com os tres SELETORES, que passam a caber\n    // numa fileira so. Isso tambem firma a premissa do popup de action, que se apoia na altura de\n    // UMA fileira de cartoes.\n    const opts=`<div class=\"ohud\">${orbox}${embox}${acbox}`+\n      `<div class=\"hist\"><button class=\"und\" ${s.podeUnd?'':'disabled'} title=\"Undo\">‹</button>`+\n      `<button class=\"red\" ${s.podeRed?'':'disabled'} title=\"Redo\">›</button></div></div>`;\n    // O dialogo saiu de dentro do palco: la ele herdaria 236px de largura. Agora e filho do\n    // .wrap e position:fixed. A classe `esq` do ladrilho da esquerda TAMBEM saiu -- ela colidia\n    // com o painel esquerdo, que passou a ter fundo, borda e raio proprios.\n    const dlg=`<div class=\"portwrap ${s.verPortrait?'on':''}\">\n          <div class=\"dlg\">\n            <h3>in development</h3>\n            <p class=\"sub\">Generate Custom Illustration</p>\n            <p>We are working on reproducing the illustration generation from MapleStory, turning\n            the character you assembled here into a drawn portrait. It is not there yet: likeness\n            and framing still change between generations, so the button stays off for now.</p>\n            <div class=\"ppgrade\">${[0,1,2].map(i=>`<div class=\"pplinha\">\n              <div class=\"pptile\" style=\"background-image:url(${esc(SHOWCASE)});background-position:0 -${i*PP_TILE}px\"></div>\n              <span class=\"ppseta\">→</span>\n              <div class=\"pptile\" style=\"background-image:url(${esc(SHOWCASE)});background-position:-${PP_TILE}px -${i*PP_TILE}px\"></div>\n            </div>`).join('')}</div>\n            <button class=\"portx\">Close</button>\n          </div>\n        </div>`;\n    return `<div class=\"wrap\">\n      <div class=\"app\">\n      <div class=\"esq\">\n        <div class=\"visor\">${anel}</div>\n        <div class=\"opts\">\n          <div class=\"barra\">\n            <button class=\"play pill so ${s.modo==='loop'?'on':''}\" title=\"${s.modo==='loop'?'Pause':'Loop'}\">${icone(s.modo==='loop'?DPAUSE:DPLAY)}</button>\n            <button class=\"play1 pill so ${s.modo==='uma'?'on':''}\" title=\"Play once\">${icone(DONCE)}</button>\n            <span class=\"sep\"></span>\n            ${frbox}\n            <button class=\"zm pill claro so\" title=\"Zoom out\">${iconeTraco(DMENOS)}</button><span class=\"badge zind\">${s.zoom}×</span><button class=\"zp pill claro so\" title=\"Zoom in\">${iconeTraco(DMAIS)}</button>\n            <button class=\"anelbtn pill claro so ${s.anelOff?'':'on'}\" title=\"Show or hide the equipped slots (shortcut: U)\">${iconeTraco(s.anelOff?DOLHOON:DOLHO)}</button>\n          </div>\n          ${opts}\n          <div class=\"barra\">\n            <button class=\"flip pill claro ${s.flip?'on':''}\" title=\"Mirrors the character. Game sprites face left by default.\">Flip</button>\n            <span class=\"sep\"></span>\n            <button class=\"ef pill claro ${s.efeito?'on':''}\"><span>Weapon</span><span class=\"fx\">FX</span></button>\n            <button class=\"efi pill claro ${s.efItem?'on':''}\"><span>Item</span><span class=\"fx\">FX</span></button>\n            <button class=\"st49 pill claro ${s.stance49?'on':''}\" title=\"Stance 49: the weapon drawn as if held in the other hand. Cash weapons use it to cover gun.\">Gun Stance</button>\n          </div>\n          ${corbox}\n          <div class=\"barra\">\n            <button class=\"rnd pill ciano\">${s.sorteando?'Randomizing…':'⚄ Randomize'}</button>\n            <button class=\"mestre ${s.travaModo?'on':''}\" title=\"Lock slots against randomize\">${cadeado(false)}</button>\n            ${s.carregando?'<span class=\"badge carregando\">Loading…</span>':''}\n            ${s.diag?`<span class=\"badge\" style=\"color:var(--warn)\">${esc(s.diag)}</span>`:''}\n          </div>\n          <div class=\"barra rodape\">\n            <button class=\"port\">Generate Custom Illustration</button>\n          </div>\n        </div>\n      </div>\n      <div class=\"side\">\n        ${[['all'].concat(CATS).slice(0,8),['all'].concat(CATS).slice(8)].map((cs,i)=>`<div class=\"abas${i?' seg':''}\">${cs.map(c=>`<button class=\"aba catsel ${c===s.catAtual?'on':''}\" data-c=\"${c}\">${ROTULO[c]||c}</button>`).join('')}</div><div class=\"risca${i?'':' junta'}\"></div>`).join('')}\n        <div class=\"invcx\">\n          <div class=\"invtop\">\n            <span class=\"buscabar\"><input class=\"busca\" placeholder=\"Enter a search term.\" value=\"${esc(s.q||'')}\"/>${icone(DLUPA)}</span>\n            <span class=\"invcont\">${lista.length}${s.q?' found':''}</span></div>\n          <div class=\"grade\">\n            ${fatia.map(r=>{\n              const rid=r.id;\n              // Na aba All cada linha traz a folha de icone da PROPRIA categoria: o `ic[0]` indexa\n              // o iconSheets daquela cat, entao usar a folha errada desenharia outro item.\n              const rc=s.catAtual==='all'?(CATDE[rid]||''):s.catAtual;\n              const sheets=(IDX[rc]||{}).sheets||null;\n              const on=!!EQIDS[rid]||(r.cores&&r.cores.some(cc=>EQIDS[cc[0]]));\n              let mini='';\n              if(r.ic&&sheets){const[sh,x,y,w,h]=r.ic;\n                mini=`<span class=\"thc\" style=\"display:inline-block;width:${w}px;height:${h}px;background-image:url(${esc(sheets[sh])});background-position:-${x}px -${y}px\"></span>`;}\n              // 2 itens do dump inteiro nao tem icone em lugar nenhum (nem _Canvas, nem _outlink,\n              // nem inline). O canvas ao vivo nao serve mais: no caminho do idx o prefetch nao\n              // carrega geometria. Placeholder com o final do id, que ao menos identifica a celula.\n              else mini=`<span class=\"thc semic\">${esc(rid.slice(-4))}</span>`;\n              return `<button class=\"cel ${on?'on':''}\" data-id=\"${esc(rid)}\" data-cat=\"${esc(rc)}\" data-geo=\"${esc(r.geoRef||'')}\" data-ref=\"${esc(r.cores?r.cores[0][1]:'')}\" title=\"${esc(r.name)} · ${esc(rid)}\">${mini}${NOVO.has(rid)?'<span class=\"nv\">NEW</span>':''}</button>`;\n            }).join('')}\n          </div>\n          <div class=\"pag\"><button class=\"pf pill claro so\" title=\"First page\" ${pag<=0?'disabled':''}>${iconeTraco(DSETAEE)}</button><button class=\"pp pill claro so\" title=\"Previous\" ${pag<=0?'disabled':''}>${iconeTraco(DSETAE)}</button><span class=\"badge\">${pag+1} / ${paginas}</span><button class=\"np pill claro so\" title=\"Next\" ${pag>=paginas-1?'disabled':''}>${iconeTraco(DSETAD)}</button><button class=\"ul pill claro so\" title=\"Last page\" ${pag>=paginas-1?'disabled':''}>${iconeTraco(DSETADD)}</button></div>\n        </div>\n        <div class=\"nota\"><b>Maple Simulator</b> &middot; GMS v271<br>A free MapleStory character creator and dress-up simulator, made for\n        study purposes only. This is a non-commercial fan project, not affiliated with, endorsed\n        by or sponsored by Nexon. MapleStory and all related assets belong to\n        <b>Nexon Korea Corp.</b></div>\n      </div>\n      </div>\n      ${dlg}\n    </div>`;\n  },\n\n  mounted(d2){\n    const s=d2.state;\n    s.pronto=false;s.iSkin=0;s.acao='stand1';s.frame=0;\n    // O visor agora e FIXO em 236x188 (o anel manda no tamanho, e a thumb tem 44px por decisao\n    // do usuario). Em 3x o personagem mede ~190px e nao caberia; 2x da ~127px, com folga.\n    // 2x e o zoom padrao (01/09): e o enquadramento em que o personagem fica com a folga pedida\n    // no palco, com ARRX/ARRY em zero. O 3x anterior enchia o quadro.\n    s.zoom=2;\n    // Cor de cabelo ESCOLHIDA, nao a do item equipado. E ela que a paleta destaca e que o clique\n    // num estilo novo passa a respeitar -- por isso e estado proprio e sobrevive a troca de item.\n    s.corSel=0;\n    s.modo='parado';s.equip={};s.anelOff=false;s.q='';s.pag=0;s.cat=[];s.carregando=false;s.efeito=false;s.efItem=true;s.stance49=false;s.flip=false;s._t0=0;s.catAtual='hair';s.sorteando=false;\n    // `oculto` = nao desenha mas continua equipado. `travado` = o randomize nao roleta esse slot.\n    // `travaModo` = o cadeado mestre esta aberto, mostrando o toggle no centro de cada thumb.\n    s.oculto={};s.travado={};s.travaModo=false;s.corCat='hair';\n    // Os quatro popups da interface. Um so pode estar aberto por vez, e quem garante isso sao os\n    // handlers de abertura, que fecham os outros tres antes de abrir o seu.\n    s.acAberto=false;s.emAberto=false;s.orAberto=false;s.skAberto=false;\n    s.podeUnd=false;s.podeRed=false;s.gerando=false;s.verPortrait=false;\n\n    // As skins SAIRAM da tabela `mcskinpix` (2026-08-20). Eram 46 x ~137 KB = 6,3 MB baixados\n    // inteiros no boot por `loadAll()` para exibir UMA. Agora `_skin` e um indice de 3,4 KB e\n    // cada skin e um arquivo do Drive buscado sob demanda e cacheado aqui.\n    async function carregaSkin(i){\n      const r=s.skins[i];if(!r)return false;\n      if(!SKINPIX[r.id])SKINPIX[r.id]=await (await fetch(MEDIA+r.ref)).json();\n      s.iSkin=i;return true;\n    }\n    function skinAtual(){return SKINPIX[(s.skins[s.iSkin]||{}).id]||{};}\n\n    // ---- CACHE DO PERSONAGEM (localStorage) ------------------------------------------\n    // Guarda o LOOK, nunca estado de ferramenta: equip, skin, cor de cabelo e os ocultos.\n    // ZOOM E FLIP FICAM DE FORA por decisao do usuario (02/09): o 2x e o 0,0 do palco sao\n    // padrao deliberado (01/09) e restaura-los desfaria essa padronizacao para quem volta.\n    // Frame, play, busca, pagina e as travas do randomize tambem nao entram -- sao sessao.\n    // O `geo`/`at` viajam JUNTO do id de proposito. Sem eles o restore precisaria do indice de\n    // cada categoria equipada: MEDIDO em 02/09, 6,25 MB de idx para 8 pecas (face sozinha pesa\n    // 2,2 MB), contra 2,4 MB de blocos de geometria que o personagem precisa de qualquer jeito\n    // para ser desenhado. Guardar 40 bytes a mais por peca evita 6 MB de download.\n    const CACHE_K='mc:outfit:v1';\n    // O conteudo do localStorage e ENTRADA HOSTIL: quem controla o navegador (ou um XSS futuro\n    // nesta origem) escolhe o que cai aqui, e esses valores viram URL em fetch(MEDIA+ref) e em\n    // img.src. Validar na LEITURA fecha a classe inteira -- sem isto um ref com `..` passearia\n    // pelo caminho do CDN. Todo id do catalogo tem 8 digitos e todo ref e nanoid.\n    const OKID=/^[0-9]{8}$/, OKREF=/^[A-Za-z0-9_-]{1,40}$/;\n    // Toda leitura e escrita em try: aba anonima LANCA no acesso, e dentro do iframe do\n    // `maplesimredir` o armazenamento e particionado pelo site de cima (no Safari pode nem\n    // existir). O cache e opcional por definicao, nunca pre-requisito para desenhar.\n    function leCache(){\n      try{\n        const d=JSON.parse(localStorage.getItem(CACHE_K)||'null');\n        if(!d||d.v!==1)return null;\n        const eq=[];\n        for(const sl in (d.eq||{})){\n          if(CATS.indexOf(sl)<0)continue;\n          const p=d.eq[sl]||{};\n          if(!OKID.test(String(p.id||'')))continue;\n          if(p.geo&&!OKREF.test(String(p.geo)))continue;\n          if(p.at&&!OKREF.test(String(p.at)))continue;\n          // `ic` cai direto num style inline, entao tem que ser cinco numeros e nada alem disso.\n          const ic=(Array.isArray(p.ic)&&p.ic.length===5&&p.ic.every(n=>Number.isFinite(n)&&n>=0&&n<8192))?p.ic.map(Number):null;\n          const sh=(p.sh&&OKREF.test(String(p.sh)))?String(p.sh):null;\n          eq.push({sl:sl,id:String(p.id),geo:p.geo?String(p.geo):null,at:p.at?String(p.at):null,ic:ic,sh:sh});\n        }\n        const cor=Number(d.cor);\n        return {eq:eq,\n                skin:OKID.test(String(d.skin||''))?String(d.skin):null,\n                cor:(cor>=0&&cor<PALETA.length)?(cor|0):0,\n                oculto:Array.isArray(d.oculto)?d.oculto.filter(x=>CATS.indexOf(x)>=0):[]};\n      }catch(err){return null;}\n    }\n    // So liga DEPOIS do boot. Sem esta trava, um boot que falhe no meio chega no marca() com o\n    // equip vazio e grava o personagem em branco por cima do que a pessoa tinha guardado.\n    let cacheLigado=false,cacheT=null;\n    function salvaCache(){\n      if(!cacheLigado)return;\n      if(cacheT)clearTimeout(cacheT);\n      // debounce: o `.x` desequipa e SO DEPOIS limpa o oculto do slot; gravando no timeout o\n      // snapshot sai ja com as duas coisas aplicadas, em vez de um estado intermediario.\n      cacheT=setTimeout(()=>{\n        try{\n          const eq={};\n          for(const sl in s.equip){\n            const id=s.equip[sl],r=META[id],tc=THUMBCACHE[id]||{},ix=IDX[sl];\n            // o equipado e uma VARIANTE de cor; o atlas dela sai do par em `cores`, nao do estilo\n            const par=((r&&r.cores)||[]).filter(c=>c[0]===id)[0];\n            // a folha e guardada como REF, nunca como URL montada: na volta ela e remontada com\n            // MEDIA+ref depois de passar pelo OKREF. URL pronta viraria injecao no style.\n            const sh=(r&&r.ic&&ix&&ix.sheets[r.ic[0]])?String(ix.sheets[r.ic[0]]).slice(MEDIA.length):(tc.sh||null);\n            eq[sl]={id:id,geo:(r&&r.geoRef)||tc.geo||null,at:par?par[1]:(tc.at||null),\n                    ic:(r&&r.ic)||tc.ic||null,sh:sh};\n          }\n          const oc=[];for(const sl in s.oculto)if(s.oculto[sl])oc.push(sl);\n          localStorage.setItem(CACHE_K,JSON.stringify({v:1,gv:CATVER,eq:eq,\n            skin:(s.skins[s.iSkin]||{}).id||null,cor:s.corSel|0,oculto:oc}));\n        }catch(err){}\n      },300);\n    }\n    // Restaura SEM tocar em indice: carregaItem so precisa de geoRef+atlasRef, e os dois vieram\n    // no cache. Item sem geometria (os 757 que so ocupam slot para esconder) entra direto, do\n    // mesmo jeito que o clique faz. Em paralelo: os blocos sao independentes, entao o tempo de\n    // parede e o do maior (~576 KB), nao a soma.\n    // Cada slot no seu try: um id que sumiu do idx numa recompilacao futura nao pode levar o\n    // personagem inteiro junto -- ele apenas nao volta.\n    async function restaura(c){\n      if(!c||!c.eq.length)return false;\n      const eq={};\n      let cortados=0;\n      await Promise.all(c.eq.map(async p=>{\n        try{\n          // Peca DESABILITADA no cadastro depois de a pessoa ter equipado: sai aqui.\n          // O restore carrega direto por geoRef/atlasRef, sem passar pelo indice -- e o que\n          // evita baixar 6,25 MB de idx -- entao ele tambem nao passa pelo filtro do garanteIdx.\n          // Este e o unico ponto onde a lista alcanca o cache. O ESCONDIDO ja esta carregado:\n          // o fetch dele acontece ANTES do trocaCategoria, e o restore roda depois do boot.\n          // Nao precisa mexer no cache guardado: o proximo salvaCache grava a partir do `equip`\n          // ja sem a peca, entao ele se corrige sozinho na primeira alteracao.\n          if(ESCONDIDO.has(p.id)){cortados++;return;}\n          // antes de qualquer await: e isto que faz a thumb do anel aparecer no primeiro quadro\n          THUMBCACHE[p.id]={ic:p.ic,sh:p.sh,geo:p.geo,at:p.at};\n          if(!p.geo){eq[p.sl]=p.id;return;}\n          const it=await carregaItem(p.id,p.geo,p.at,p.sl);\n          if(!it)return;\n          img(it.atlasRef);eq[p.sl]=p.id;\n        }catch(err){}\n      }));\n      if(cortados)d2.log('[mcbaseviewer] '+cortados+' peca(s) do cache sairam: desabilitadas no cadastro');\n      if(!Object.keys(eq).length)return false;\n      // Escrever s.equip fora do defineEquip e a EXCECAO deliberada a regra do historico logo\n      // abaixo: isto nao e um passo dado sobre o personagem, e o ponto de partida dele. Quem\n      // ajusta o HIST e o chamador, no boot.\n      s.equip=eq;\n      const oc={};for(const sl of c.oculto)if(eq[sl])oc[sl]=true;\n      s.oculto=oc;s.corSel=c.cor;\n      return true;\n    }\n\n    // ---- TELEMETRIA: UMA LINHA POR SESSAO --------------------------------------------\n    // Um POST por clique encheria a tabela e custaria caro por nada. Os contadores vivem em\n    // memoria e saem JUNTOS quando a aba some. sendBeacon existe para isso: sobrevive ao\n    // fechamento da pagina, coisa que fetch normal nao garante.\n    // NAO ENVIA DO BUILDER: so dispara em dominio .data2.app, entao o preview do builder e o\n    // inspect_code_component ficam de fora sozinhos, sem lista de excecao. Para se excluir do\n    // numero no app publicado: localStorage.setItem('mc:nostat','1') naquele navegador.\n    // SESSAO VAZIA TAMBEM E ENVIADA, de proposito: ela e o DENOMINADOR de chegou-e-usou. Filtrar\n    // aqui destruiria justamente a metrica que motivou isto.\n    const STAT_URL='https://bigaton-playground.data2apis.com/rush/mcstat';\n    // DIA E HORA sao do RELOGIO LOCAL DO VISITANTE e sao carimbados no INICIO da sessao, nao no\n    // envio. Duas decisoes de proposito:\n    //  . inicio, porque \"pico de acesso\" e quando a pessoa CHEGA; o fim da sessao pode cair horas\n    //    depois, numa aba esquecida aberta, e mancharia o grafico.\n    //  . local, porque a pergunta interessante e \"a que hora DO DIA DELAS as pessoas usam\". Em\n    //    UTC, um pico as 21h de Brasilia apareceria como 00h do dia SEGUINTE, e o par dia/hora\n    //    ficaria incoerente entre si.\n    // en-CA da YYYY-MM-DD com o calendario local, que e o unico formato que ordena como texto.\n    const AGORA=new Date();\n    const STAT={t0:Date.now(),cats:Object.create(null),itens:Object.create(null),\n                dia:AGORA.toLocaleDateString('en-CA'),hora:AGORA.getHours(),\n                larg:window.innerWidth|0,alt:window.innerHeight|0,\n                eq:0,rnd:0,port:0,busca:0,erros:Object.create(null),volta:false,enviado:false};\n    // Id ANONIMO, e so isso: nada de IP, nada de user-agent. Serve para contar visitantes\n    // distintos e nada mais. Mora no mesmo localStorage do cache de personagem, entao herda as\n    // mesmas limitacoes (aba anonima, particionamento em iframe).\n    function vid(){\n      try{\n        let v=localStorage.getItem('mc:vid');\n        if(!v){v=Date.now().toString(36)+Math.random().toString(36).slice(2,10);\n               localStorage.setItem('mc:vid',v);}\n        return v;\n      }catch(err){return 'sem-storage';}\n    }\n    function medeSessao(){\n      const ch=o=>Object.keys(o).join(',').slice(0,900);\n      // itens viajam como cat|id|nome, separados por ponto-e-virgula. O nome e limpo dos dois\n      // separadores para nao quebrar o parse do painel, e a lista e cortada em 40 pecas: sessao\n      // que passa disso ja disse o que tinha para dizer, e o campo tem limite.\n      const limpa=t=>String(t||'').replace(/[;|]/g,' ').slice(0,48);\n      const itensTxt=Object.keys(STAT.itens).slice(0,40)\n        .map(id=>STAT.itens[id].c+'|'+id+'|'+limpa(STAT.itens[id].n)).join(';').slice(0,3000);\n      // FUSO no lugar de IP. O servidor recebe x-data2-client-ip, mas resolver pais a partir\n      // dele exigiria mandar o IP de cada visitante para um servico de geolocalizacao de\n      // terceiros -- rede a mais e um dado sensivel a mais guardado. O fuso do navegador da\n      // precisao de PAIS para praticamente todo mundo, custa zero e nao sai daqui.\n      // Quem traduz fuso em pais e o PAINEL, nao esta linha: assim melhorar o mapa nao exige\n      // republicar o previewer, e as linhas ja gravadas se beneficiam sozinhas.\n      let tz='';try{tz=Intl.DateTimeFormat().resolvedOptions().timeZone||'';}catch(err){}\n      // `ts` e o INICIO da sessao em UTC, e e o unico campo de tempo que o painel usa para o\n      // grafico de horas. Motivo (03/09): guardar a hora LOCAL DE QUEM ACESSA misturava fusos --\n      // alguem nos EUA as 20h dele entrava como 20h no mesmo grafico em que 22h de Brasilia. Para\n      // saber quando O SEU publico esta online, tudo tem que estar numa linha do tempo so.\n      // Nao se corrige isso somando offset em cima da hora local: horario de verao muda o offset\n      // e o dado gravado nao diz qual regra valia. Instante absoluto nao tem essa ambiguidade.\n      // `dia`/`hora` continuam sendo gravados por compatibilidade com as linhas antigas.\n      return {vid:vid(),\n        ts:new Date(STAT.t0).toISOString(),\n        dia:STAT.dia,hora:STAT.hora,tz:tz,\n        // LARGURA MEDIDA NO MOUNT E MANTIDA NO RESIZE, nunca lida aqui. Medir no envio estava\n        // ERRADO e o dado de producao provou (03/09): o envio acontece no pagehide, com a janela\n        // ja desmontando, e vieram linhas com 0x0, 750x23 e 390x484 -- exatamente a metrica que\n        // decide o trabalho de mobile.\n        // E a largura do IFRAME, que e a que o app realmente tem para se desenhar. Nao e a tela\n        // do aparelho, e isso esta certo: medir a tela nao diria quanto espaco o layout recebeu.\n        larg:STAT.larg,alt:STAT.alt,\n        volta:STAT.volta,dur:Math.round((Date.now()-STAT.t0)/1000),\n        eq:STAT.eq,cats:ch(STAT.cats),itens:itensTxt,\n        rnd:STAT.rnd,port:STAT.port,busca:STAT.busca,erros:ch(STAT.erros)};\n    }\n    function envia(){\n      if(STAT.enviado)return;\n      STAT.enviado=true;\n      try{\n        if(String(location.hostname||'').indexOf('.data2.app')<0)return;\n        if(localStorage.getItem('mc:nostat'))return;\n        const corpo=JSON.stringify(medeSessao());\n        if(navigator.sendBeacon)navigator.sendBeacon(STAT_URL,new Blob([corpo],{type:'application/json'}));\n        else fetch(STAT_URL,{method:'POST',headers:{'Content-Type':'application/json'},body:corpo,keepalive:true});\n      }catch(err){}\n    }\n    // Os dois eventos: pagehide cobre o fechamento normal, visibilitychange cobre o celular, onde\n    // pagehide muitas vezes nao chega. O guarda `enviado` impede linha duplicada.\n    // LIMITE ACEITO: trocar de aba e voltar encerra a medicao ali -- o que vier depois se perde.\n    // A alternativa (varias linhas por sessao) inflaria a contagem de sessoes, que e pior.\n    // ---- PRESENCA (sessoes ativas agora) ---------------------------------------------\n    // O resumo da sessao so chega quando ela ACABA, entao ele nao sabe dizer quem esta aqui\n    // agora. Este batimento resolve isso com uma tabela separada de UMA LINHA POR VISITANTE:\n    // o handler faz upsert, entao ela nao cresce com o tempo -- fica do tamanho do publico, nao\n    // do numero de batidas. Quem conta o \"agora\" e o updatedAt, que o servidor carimba sozinho;\n    // relogio de visitante nao entra na conta.\n    // So bate com a aba VISIVEL: aba em segundo plano nao e sessao ativa. O navegador ainda\n    // estrangula timers em background, entao a checagem de visibilidade e cinto e suspensorio.\n    const LIVE_URL='https://bigaton-playground.data2apis.com/rush/mclive';\n    function bate(){\n      try{\n        if(document.visibilityState!=='visible')return;\n        if(String(location.hostname||'').indexOf('.data2.app')<0)return;\n        if(localStorage.getItem('mc:nostat'))return;\n        fetch(LIVE_URL,{method:'POST',headers:{'Content-Type':'application/json'},\n                        body:JSON.stringify({vid:vid()})});\n      }catch(err){}\n    }\n    bate();\n    const bateTimer=setInterval(bate,40000);\n    d2.onUnmount(()=>{clearInterval(bateTimer);});\n\n    // mantem a ultima medida BOA: se a pessoa redimensiona, vale o tamanho em que ela ficou, e\n    // nunca o do quadro de desmontagem. Zero e descartado pelo mesmo motivo.\n    d2.on(window,'resize',()=>{\n      const w=window.innerWidth|0,h=window.innerHeight|0;\n      if(w>0&&h>0){STAT.larg=w;STAT.alt=h;}\n    });\n    d2.on(window,'pagehide',()=>{envia();});\n    d2.on(window,'visibilitychange',()=>{if(document.visibilityState==='hidden')envia();});\n\n    // HISTORICO -- so o que muda o PERSONAGEM entra: `equip` e a skin. Oculto, trava, play, frame,\n    // zoom e categoria sao estado de ferramenta, nao de look, e ficam de fora de proposito.\n    // Um snapshot e o proprio objeto `equip`: todos os pontos que mexem nele ja TROCAM o objeto\n    // em vez de mutar, entao nao existe copia aqui -- sao ~14 ponteiros para strings que ja\n    // estavam na memoria (vieram do idx). Voltar tambem nunca busca rede: so da para voltar a um\n    // estado em que ja se esteve, e a geometria/atlas daquele estado ja esta em cache.\n    let HIST=[],POS=-1;\n    const MAXH=1000;\n    function sinaliza(){s.podeUnd=POS>0;s.podeRed=POS<HIST.length-1;}\n    // O UNICO ponto que empilha. Se alguma mudanca de equip/skin nao passar por aqui, ela some do\n    // historico em silencio -- e o jeito de nao esquecer e nunca escrever `s.equip=` fora daqui.\n    function marca(){\n      if(POS<HIST.length-1)HIST=HIST.slice(0,POS+1);   // acao nova apaga o futuro (o redo)\n      HIST.push({equip:s.equip,iSkin:s.iSkin});\n      if(HIST.length>MAXH)HIST.shift();\n      POS=HIST.length-1;sinaliza();salvaCache();\n    }\n    function defineEquip(novo){s.equip=novo;marca();}\n    // Aplica SEM empilhar. Se isto chamasse marca(), o undo empilharia a si mesmo e voce nunca\n    // sairia do lugar -- e o bug classico desta feature.\n    function aplicaHist(i){\n      const e=HIST[i];if(!e)return;\n      POS=i;s.equip=e.equip;\n      if(e.iSkin!==s.iSkin)carregaSkin(e.iSkin);\n      // undo/redo mudam o personagem sem passar por marca(): sem este salva, voltar um passo e\n      // sair da pagina restauraria o passo seguinte na proxima visita.\n      sinaliza();salvaCache();\n    }\n    function desfaz(){if(POS>0)aplicaHist(POS-1);}\n    function refaz(){if(POS<HIST.length-1)aplicaHist(POS+1);}\n\n    // carrega geometria+atlas de um item. geoRef = ref do bloco (novo) OU calcula pela chave (antigo).\n    async function carregaItem(id,geoRef,atlasRef,cat){\n      if(ITENS[id]&&ITENS[id].pronto&&(!EFI||!EFI[id]||ITENS[id].ei))return ITENS[id];\n      if(!geoRef)return null;\n      if(!BLOCO[geoRef]){const r=await fetch(MEDIA+geoRef);BLOCO[geoRef]=await r.json();}\n      const bl=BLOCO[geoRef]||{};\n      // popula todos do bloco (mesma geo); atlasRef vem por item\n      for(const iid in bl){if(!ITENS[iid]){const it=bl[iid];it.itemId=iid;ITENS[iid]=it;}}\n      // as armas \"so-efeito\" (todo o sprite mora no .effect.png) NAO tem atlas base: atlasRef\n      // fica null de proposito e elas somem quando o toggle de efeito esta desligado.\n      // ItemEff: bloco proprio (geoitemeff) + atlas proprio, carregados sob demanda.\n      const m=EFI&&EFI[id];\n      if(m&&ITENS[id]&&!ITENS[id].ei){\n        if(!BLOCOEI[m.b])BLOCOEI[m.b]=await (await fetch(MEDIA+m.b)).json();\n        const e=BLOCOEI[m.b][id];\n        if(e){ITENS[id].ei=e;ITENS[id].eiRef=m.a;\n          const x=Object.create(null);for(const i of (m.x||[]))x[i]=1;ITENS[id].eiX=x;}\n      }\n      if(ITENS[id]){ITENS[id].atlasRef=atlasRef||null;if(ITENS[id].geoEf)ITENS[id].atlasEfRef=(META[id]&&META[id].efRef)||null;ITENS[id].pronto=1;}\n      return ITENS[id]||null;\n    }\n    async function prefetch(){\n      // nada a fazer: a thumb e recorte CSS da folha, entao a grade nao precisa de geometria.\n      // Mantido como no-op para nao espalhar `if` nos call sites.\n    }\n    // Um carregador SO (o `garanteIdx` la de baixo). Antes esta funcao tinha a propria copia do\n    // fetch+META, e duas copias da mesma regra ja divergiram varias vezes neste projeto.\n    async function carregaOrelhas(){\n      if(ORELHAS||!INDEXREF._orelhas)return;\n      try{ORELHAS=await (await fetch(MEDIA+INDEXREF._orelhas)).json();}\n      catch(err){d2.log('[mcbaseviewer] addon de orelhas indisponivel: '+((err&&err.message)||err));}\n    }\n    // O bloco de emote e por geoRef da face e pesa ~860 KB. Carrega sob demanda, so quando o\n    // usuario escolhe uma expressao -- e so o bloco da face equipada, nao os 163.\n    async function carregaEmote(faceId){\n      if(!faceId||!INDEXREF._emotes)return;\n      const gr=META[faceId]&&META[faceId].geoRef;\n      if(!gr)return;\n      try{\n        if(!EMOTEIDX)EMOTEIDX=await (await fetch(MEDIA+INDEXREF._emotes)).json();\n        const nome=(EMOTEIDX.blocos||{})[gr];\n        // Sem grupo para este bloco o render cai no default sozinho, mas em SILENCIO pareceria\n        // que o seletor esta quebrado. Por isso avisa.\n        // este diag e ACIONAVEL: ele nomeia exatamente quais blocos de face vale compilar\n        if(!nome){STAT.erros['emote:'+gr]=1;s.diag='Emotes not compiled for this face yet';return;}\n        if(EMOTEGR[nome])return;\n        const g=(EMOTEIDX.grupos||{})[nome];\n        if(!g)return;\n        const bl=await (await fetch(MEDIA+g.j)).json();\n        bl.atlas=g.a;                 // o atlas e resolvido por img(), como qualquer outro\n        EMOTEGR[nome]=bl;\n      }catch(err){d2.log('[mcbaseviewer] grupo de emote indisponivel: '+((err&&err.message)||err));}\n    }\n    async function trocaCategoria(cat){\n      s.catAtual=cat;s.q='';s.pag=0;\n      STAT.cats[cat]=1;\n      if(cat==='all'){\n        // A aba All existe para a BUSCA achar qualquer item. Custa os 14 indices (~8,6 MB) na\n        // primeira vez -- o mesmo que o randomize ja paga -- e depois fica tudo em cache.\n        await Promise.all(CATS.map(garanteIdx));\n        let t=[];for(const c of CATS)t=t.concat((IDX[c]&&IDX[c].items)||[]);\n        s.cat=t;return;\n      }\n      await garanteIdx(cat);\n      s.cat=(IDX[cat]&&IDX[cat].items)||[];\n    }\n\n    // CONFLITO DE SLOT -- regra do JOGO, copiada do engine local (preview.html), nao inventada:\n    //  . outfit declara `vslot=MaPn`: ocupa coat E pants -> os tres nunca coexistem.\n    //  . arma com `islot=WpSi` ocupa a mao principal E a off-hand -> nao coexiste com shield.\n    // Resolver no nivel do SLOT (limpar o outro) e nao no render: e assim que o jogo faz, e\n    // desenhar os dois duplicaria o `mailArm` (medido no projeto local).\n    function aplicaConflito(e,cat){\n      for(const par of ((REGRAS&&REGRAS.exclusivos)||[])){\n        const a=par[0],bs=par[1];\n        if(cat===a){for(const b of bs)delete e[b];}\n        else if(bs.indexOf(cat)>=0)delete e[a];\n      }\n      const off=(REGRAS&&REGRAS.offhand)||'shield';\n      if(cat==='weapon'&&WPSI[e.weapon])delete e[off];\n      if(cat===off&&e[off]&&WPSI[e.weapon])delete e.weapon;\n      return e;\n    }\n    // Item SEM geometria: os 757 que nao desenham nada e mesmo assim EXISTEM -- todos declaram\n    // vslot. Servem para ocupar o slot e esconder (Bald Wig, Transparent Hat, as armas so-stat).\n    // `geoRef` e `atlasRef` sao null de proposito: nao ha bloco nem atlas para buscar. `compor`\n    // ja tolera ITENS[id] ausente, e o `vs` do vslot vem do META, que sai do idx.\n    function poeNoSlot(e,cat,id){\n      if(e[cat]===id){const x=Object.assign({},e);delete x[cat];return x;}\n      return aplicaConflito(Object.assign({},e,{[cat]:id}),cat);\n    }\n    async function equipar(id,geoRef,atlasRef,cat){\n      // Na aba All o slot vem da linha clicada (data-cat), nao da categoria aberta.\n      cat=cat||CATDE[id]||s.catAtual;\n      if(!cat||cat==='all'){s.diag='Unknown slot for '+id;return;}\n      // conta o item ESCOLHIDO no clique. O que o randomize sorteia NAO entra aqui de proposito:\n      // sorteio nao e escolha, e misturar os dois estragaria a leitura de quais itens interessam.\n      // O NOME e a CATEGORIA sao gravados AQUI, junto do id. Resolver isso no painel obrigaria a\n      // baixar os 7,4 MB de indice so para ler um numero -- e o META ja tem o nome neste ponto,\n      // porque o item veio de uma categoria carregada. Guardar na escrita custa zero.\n      s.corCat=cat;STAT.eq++;STAT.itens[id]={c:cat,n:((META[id]||{}).name)||''};\n      if(!geoRef){s.diag='';defineEquip(poeNoSlot(s.equip,cat,id));return;}\n      let it=null;\n      try{ it=await carregaItem(id,geoRef,atlasRef,cat); }\n      catch(err){ s.diag='Failed: '+((err&&err.message)||err); return; }\n      if(!it){ s.diag='Item not found: '+id; return; }\n      s.diag='';\n      img(it.atlasRef);\n      defineEquip(poeNoSlot(s.equip,cat,id));\n    }\n\n    // RANDOMIZE -- as mesmas regras do sorteio do engine local:\n    //  . face e hair SEMPRE entram (nao existe personagem sem rosto nem careca por ausencia de dado)\n    //  . OU um outfit, OU o par top+pants -- nunca os dois (mesma regra de slot do clique), e\n    //    nunca NENHUM dos dois: a cobertura e garantida, igual face e hair\n    //  . shield so quando a arma nao ocupa a off-hand\n    //  . os toggles de efeito NAO entram: sao switches de INSPECAO, nao pecas de equipamento --\n    //    sortear isso destruiria a comparacao que o usuario esta fazendo.\n    const um=a=>a&&a.length?a[Math.floor(Math.random()*a.length)]:null;\n    const talvez=(a,p)=>Math.random()<(p==null?0.5:p)?um(a):null;\n    // Sorteia a cor, carrega e equipa. Devolve se REALMENTE entrou -- `carregaItem` pode falhar, e\n    // sem esse retorno a garantia de cobertura viraria \"tentei\", que nao e garantia.\n    async function poe(eq,cat,r){\n      if(!r)return false;\n      const cor=um(r.cores)||[r.id,null];   // o equipado e uma VARIANTE, nao o estilo\n      if(!r.geoRef){eq[cat]=cor[0];return true;}   // item invisivel: equipa sem carregar\n      const it=await carregaItem(cor[0],r.geoRef,cor[1],cat);\n      if(!it)return false;\n      img(it.atlasRef);eq[cat]=cor[0];return true;\n    }\n    async function garanteIdx(cat){\n      if(IDX[cat]||!INDEXREF[cat])return;\n      const d=await (await fetch(MEDIA+INDEXREF[cat])).json();\n      const linhas=d.items||[];\n      // A lista navegavel e filtrada; o META/CATDE abaixo varre TODAS as linhas de proposito.\n      // ORDEM: id DECRESCENTE. No MapleStory id maior e lancamento mais recente, entao ordenar\n      // ao contrario poe o conteudo novo na frente -- e leva junto as armas cash, que vivem numa\n      // faixa de id mais alta que as normais.\n      // NAO basta um reverse: o idx nao vem ordenado por id, ele sai do compilador na ordem dos\n      // BLOCOS de geometria. Reverter so lia a mesma bagunca de tras para frente. Ordenar por\n      // string e seguro porque todo id tem 8 digitos com zeros a esquerda (verificado).\n      // A ordenacao mora AQUI, e nao na paginacao, para valer de uma vez para a grade, a busca e\n      // a aba All -- que concatena as categorias ja ordenadas.\n      const nav=linhas.filter(r=>!ESCONDIDO.has(r.id));\n      nav.sort((a,b)=>a.id<b.id?1:(a.id>b.id?-1:0));\n      IDX[cat]={sheets:(d.iconSheets||[]).map(r=>MEDIA+r),items:nav};\n      if(d.zi2smap)ZI2SMAP=d.zi2smap;\n      for(const r of linhas){\n        META[r.id]=r;CATDE[r.id]=cat;\n        if(r.cores)for(const cc of r.cores){if(!META[cc[0]])META[cc[0]]=r;CATDE[cc[0]]=cat;}\n      }\n    }\n    async function sorteia(){\n      if(s.sorteando)return;\n      s.sorteando=true;\n      try{\n        await Promise.all(CATS.map(garanteIdx));\n        const pega=c=>(IDX[c]&&IDX[c].items)||[];\n        const escolha={};\n        for(const c of ['face','hair'])escolha[c]=um(pega(c));\n        if(Math.random()<0.4)escolha.outfit=um(pega('outfit'));\n        if(!escolha.outfit){escolha.coat=um(pega('coat'));escolha.pants=um(pega('pants'));}\n        for(const c of ['cap','cape','glove','shoes','accface','acceye','accear'])escolha[c]=talvez(pega(c));\n        escolha.weapon=talvez(pega('weapon'));\n        const eq={};\n        for(const c in escolha)await poe(eq,c,escolha[c]);\n        if(!WPSI[eq.weapon]){\n          const r=talvez(pega('offhand'));\n          if(r&&!r.geoRef)eq.offhand=r.id;\n          else if(r){const it=await carregaItem(r.id,r.geoRef,r.cores[0][1],'offhand');if(it){img(it.atlasRef);eq.offhand=r.id;}}\n        }\n        // Slot travado nao e roletado: o item atual fica, e ele vence o conflito de slot.\n        for(const c of ORDEM_EQ){\n          if(!s.travado[c]||!s.equip[c])continue;\n          eq[c]=s.equip[c];aplicaConflito(eq,c);\n        }\n        // COBERTURA: outfit OU (top + pants), nunca nu. Checada DEPOIS das travas de propósito --\n        // travar so o top e sortear um outfit deixaria sem calca, porque o conflito de slot remove\n        // o outfit e ninguem repoe a peca que faltou. O laco tenta de novo porque `poe` pode\n        // falhar ao carregar; sem o limite isso seria um while(true) num caso de rede ruim.\n        if(!eq.outfit){\n          for(let t=0;!eq.coat&&t<6;t++)await poe(eq,'coat',um(pega('coat')));\n          for(let t=0;!eq.pants&&t<6;t++)await poe(eq,'pants',um(pega('pants')));\n        }\n        // um randomize inteiro e UM passo de historico, nao um por slot\n        defineEquip(eq);\n      }catch(err){s.diag='Randomize failed: '+((err&&err.message)||err);}\n      finally{s.sorteando=false;}\n    }\n\n    (async()=>{\n     try{\n      // `zmap` SAIU do boot (2026-08-20): era lido e nunca usado -- o `zi2smap` do vslot vem\n      // dentro do idxcap.json. Eram 2 leituras de tabela por abertura, uma delas inutil.\n      s.etapa='Loading index';\n      // FALLBACK (QA 10/09): o lint do data2 avisa que d2.rush com referencia GLOBAL pode voltar\n      // vazio em alguns carregamentos, sem erro. Vazio aqui derruba o boot inteiro (INDEXREF sem\n      // _skin -> fetch de MEDIA+undefined). A telemetria tinha 5 de 345 sessoes com erro de boot.\n      // O endpoint e publico e so leitura desde 04/09, entao o fetch direto e o mesmo dado.\n      let linhasIdx=[];\n      try{const mi=await d2.rush('mcindexall').invoke({});linhasIdx=(mi&&mi.data&&mi.data.rows)||[];}catch(err){}\n      if(!linhasIdx.length){\n        STAT.erros['indexall-vazio']=1;\n        const rr=await fetch('https://bigaton-playground.data2apis.com/rush/mcindexall',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});\n        const jj=await rr.json();linhasIdx=(jj&&jj.data&&jj.data.rows)||[];\n      }\n      for(const r of linhasIdx)INDEXREF[r.cat]=r.ref;\n      s.etapa='Loading actions';\n      try{await carregaAcoes();}catch(err){d2.log('[mcbaseviewer] tabela de actions indisponivel, usando o passo padrao: '+((err&&err.message)||err));}\n      s.etapa='Loading skins';\n      const li=await (await fetch(MEDIA+INDEXREF._skin)).json();\n      // `ic` viaja junto do item, entao ordenar por id nao desalinha a folha.\n      SKINSHEET=(li.iconSheets&&li.iconSheets[0])?MEDIA+li.iconSheets[0]:'';\n      s.skins=(li.items||[]).slice().sort((a,b)=>String(a.id).localeCompare(String(b.id)));\n      // A skin guardada volta por ID, nunca por indice: `s.skins` e ordenado por id, entao uma\n      // skin nova deslocaria todo mundo e o indice salvo passaria a apontar para outra pessoa.\n      // Resolver aqui, antes do carregaSkin, evita baixar a skin 0 (137 KB) para trocar depois.\n      const CACHE=leCache();\n      let iSk=0;\n      if(CACHE&&CACHE.skin){const j=s.skins.findIndex(x=>String(x.id)===CACHE.skin);if(j>=0)iSk=j;}\n      await carregaSkin(iSk);\n      // mcgeoblocksall/mcatlasidxall SAIRAM do boot (2026-08-20): todo geoRef/atlasRef vem do idx.\n      if(INDEXREF._regras){\n        REGRAS=await (await fetch(MEDIA+INDEXREF._regras)).json();\n        for(const w of (REGRAS.wpsi||[]))WPSI[w]=1;\n      }\n      if(INDEXREF._itemeff)EFI=await (await fetch(MEDIA+INDEXREF._itemeff)).json();\n      // O addon de orelhas NAO entra no boot: 68 KB que so servem se o usuario trocar a orelha.\n      // Carregado sob demanda no primeiro uso do seletor.\n      // FALHA ABERTA de proposito: se este fetch cair, ESCONDIDO fica vazio e o catalogo aparece\n      // inteiro. E degradacao cosmetica (voltam ~190 itens de teste e duplicata) e o app continua\n      // de pe; travar o boot por causa da lista de escondidos seria muito pior.\n      s.etapa='Loading availability';\n      try{\n        const hh=await (await fetch(HIDDEN_URL)).json();\n        for(const l of ((hh&&hh.data)||[]))\n          for(const id of String(l.ids||'').split(','))if(id)ESCONDIDO.add(id);\n      }catch(err){d2.log('[mcbaseviewer] lista de escondidos indisponivel, catalogo vai inteiro: '+((err&&err.message)||err));}\n      // Mesma doutrina de falha ABERTA: sem esta lista o catalogo aparece igual, so sem o selo.\n      try{\n        if(INDEXREF['_novos']){\n          const nv=await (await fetch(MEDIA+INDEXREF['_novos'])).json();\n          CATVER=nv&&nv.ver||0;\n          for(const id of ((nv&&nv.ids)||[]))NOVO.add(id);\n        }\n      }catch(err){d2.log('[mcbaseviewer] lista de novos indisponivel, grade sem selo: '+((err&&err.message)||err));}\n      await trocaCategoria(CATS[0]);\n      s.pronto=true;marca();await prefetch();\n      // O restore roda DEPOIS do `pronto` de proposito: a base aparece no mesmo tempo de sempre\n      // (medido em 02/09: ~1,2 MB de boot) e as pecas entram conforme chegam. Bloquear aqui\n      // triplicaria o tempo ate a PRIMEIRA imagem para nao adiantar nada.\n      if(await restaura(CACHE)){\n        STAT.volta=true;\n        // O personagem restaurado E o inicio do historico, nao um passo dado sobre o boot vazio:\n        // por isso SUBSTITUI a entrada 0 em vez de empilhar. Empilhando, o primeiro Undo\n        // devolveria o personagem pelado -- o bug classico de restaurar estado com undo.\n        HIST=[{equip:s.equip,iSkin:s.iSkin}];POS=0;sinaliza();\n        // Os indices das categorias equipadas entram por ULTIMO, sem travar nada: o META\n        // alimenta o `vs` (vslot) do compor e a thumb do anel. Sem eles o personagem ja desenha,\n        // mas a peca que declara vslot nao esconderia o que deve e o anel mostraria o\n        // placeholder de 4 digitos. Reatribuir `s.equip` no fim e so para repintar -- mesma\n        // composicao, referencia nova; nao empilha historico e nao chama salvaCache.\n        Promise.all(Object.keys(s.equip).map(garanteIdx))\n          .then(()=>{if(!s.pronto)return;\n            s.equip=Object.assign({},s.equip);\n            // RE-GRAVA agora que o META chegou: e o que promove um cache do formato antigo (sem\n            // `ic`/`sh`) para o novo sozinho, sem versao nova e sem descartar o personagem de\n            // quem ja tinha um guardado. A primeira visita depois desta mudanca ainda cai no\n            // placeholder; a segunda ja vem com os icones.\n            salvaCache();})\n          .catch(()=>{});\n      }\n      cacheLigado=true;\n     }catch(err){\n       STAT.erros.boot=1;s.erro=(err&&err.message)?err.message:String(err);\n       // a MENSAGEM vai junto (QA 10/09): so o flag nao dizia se era rede, indice vazio ou dado.\n       // Sem virgula, ponto-e-virgula nem barra: sao os separadores do campo no painel.\n       STAT.erros['boot:'+String(s.erro).replace(/[,;|]/g,' ').slice(0,80)]=1;\n       STAT.erros['etapa:'+String(s.etapa||'').replace(/[,;|]/g,' ').slice(0,40)]=1;\n     }\n    })();\n\n    // `desenhaThumbs` REMOVIDO (2026-08-20): desenhava thumb ao vivo em `canvas.th`, que nao\n    // existe mais desde que toda categoria passou a ter folha de icone. Ele rodava dentro do rAF,\n    // entao era uma varredura de DOM (`$$('canvas.th')`) por FRAME, 60x por segundo, sempre vazia.\n\n    // A miniatura do personagem inteiro no botao de skin foi FEITA e depois RETIRADA a pedido\n    // do usuario (01/09): so a cabeca comunica melhor naquele tamanho. Ficam as duas licoes que\n    // ela deixou, porque valem para qualquer preview pequeno que venha depois:\n    //   . o canvas tinha que ter o tamanho do SPRITE, com o CSS encaixando, e nao o contrario --\n    //     um corpo de ~62px nao cabe em escala inteira num botao de 58 uteis;\n    //   . bbox so e seguro ali porque as 46 skins tem geometria identica (secao 18). Em thumb\n    //     comparavel a regra continua sendo quadro fixo.\n    let raf=null;\n    function draw(){\n      raf=requestAnimationFrame(draw);\n      if(!s.pronto)return;\n      const cv=d2.$('canvas.big');if(!cv)return;\n      // A MEDIDA vem do WRAPPER, nao do proprio canvas. Desde que o canvas passou a ter\n      // width/height fixados em style (para casar 1:1 com a tela e parar de serrilhar), ele\n      // deixou de acompanhar o container sozinho -- o style ganha do width:100% do CSS. Lendo o\n      // clientWidth dele mesmo, a medida ficava CONGELADA no tamanho anterior: ao esconder os\n      // slots o palco ia para 498 e o canvas continuava com 354, encostado a esquerda, e o\n      // personagem parecia pular de lugar.\n      const box=cv.parentNode||cv;\n      const dpr=window.devicePixelRatio||1,W=box.clientWidth,H=box.clientHeight;\n      // A PISCADA vinha daqui. A cada re-render o palco passa por um quadro com clientWidth 0; a\n      // linha de baixo entao escrevia cv.width=0, e ESCREVER em cv.width destroi o bitmap. O\n      // quadro saia em branco e so o rAF seguinte redesenhava. O mcbuilder ja tinha esta guarda,\n      // o viewer nao -- comparar os dois foi o que apontou.\n      if(!W||!H)return;\n      // compara a ALTURA tambem: so a largura deixava o canvas com altura errada quando apenas\n      // ela mudava (redimensionar a janela na vertical).\n      const bw=Math.round(W*dpr),bh=Math.round(H*dpr);\n      if(cv.width!==bw||cv.height!==bh){\n        cv.width=bw;cv.height=bh;\n        // 1:1 EXATO com a tela. Sem esta linha o navegador exibe um backing store de 443px num\n        // espaco de 442,5 device px (dpr 1,25) e REAMOSTRA tudo de novo, em cima de um desenho\n        // que ja era nearest-neighbor. Era essa segunda reamostragem que serrilhava o personagem.\n        cv.style.width=(bw/dpr)+'px';cv.style.height=(bh/dpr)+'px';\n      }\n      const ctx=cv.getContext('2d');\n      // ESPACO DE DISPOSITIVO, nao de CSS. Causa medida do serrilhado: com dpr fracionario\n      // (Windows a 125% da dpr 1,25) e zoom 3, cada pixel do sprite virava 3,75 pixels de tela --\n      // umas colunas com 4, outras com 3. Nearest-neighbor em escala quebrada nao tem como sair\n      // uniforme. Desenhando em pixel de DISPOSITIVO com a escala arredondada para INTEIRO, cada\n      // pixel do sprite vira sempre o mesmo bloco, em qualquer zoom de tela.\n      ctx.setTransform(1,0,0,1,0,0);ctx.imageSmoothingEnabled=false;\n      ctx.clearRect(0,0,cv.width,cv.height);\n      const ZD=Math.max(1,Math.round(s.zoom*dpr));\n      const CX=Math.round((W/2+ARRX)*dpr), CY=Math.round((Math.min(H,PALCO_ALT)*EIXO_Y+ARRY)*dpr);\n      // O palco nao desenha mais NADA de cenario: a sombra de chao e as guias dos dois eixos\n      // sairam a pedido do usuario (01/09). O fundo agora e so o gradiente do CSS.\n      const sk=skinAtual();\n      const acoes=ordenaAcoes(Object.keys(sk.geo||{}));const acao=s.acao&&acoes.indexOf(s.acao)>=0?s.acao:acoes[0];\n      const nfr=keys(sk.geo,acao).length||1;\n      let frame=Math.min(Math.max(s.frame||0,0),nfr-1);\n      if(s.modo!=='parado'){\n        const r=frameNoTempo(acao,nfr,Date.now()-s._t0,s.modo);\n        frame=r.frame;TEMPO=Date.now()-s._t0;\n        // Play Once acabou: volta ao frame 0 e para.\n        if(r.terminou){s.modo='parado';s.frame=0;frame=0;TEMPO=0;}\n      }\n      // O indicador e o botao do frame ativo sao escritos DIRETO no DOM, nao por re-render: o\n      // frame muda ate ~6x/s e re-renderizar redesenharia a grade de 40 celulas junto. A guarda\n      // pelo texto tambem cura sozinha depois de um re-render (o span volta a '–').\n      // SPINNER: escrito direto no DOM, como o indicador de frame. Ligar/desligar por estado\n      // redesenharia a grade de 42 celulas a cada sprite que chega da rede.\n      const sp=d2.$('.spin');\n      if(sp){\n        const ativo=EMVOO>0||s.carregando===true;\n        if(sp.classList.contains('on')!==ativo)sp.classList.toggle('on',ativo);\n      }\n      const ind=d2.$('.frameind');\n      if(ind&&ind.textContent!==String(frame)){\n        ind.textContent=String(frame);\n        // Itera os FILHOS do container em vez de usar o seletor-multiplo: mais barato, e evita\n        // escrever o cifrao dobrado, que o patcher come como escape de replace.\n        const cx=d2.$('.frames');\n        if(cx)for(const b of cx.children)b.classList.toggle('on',Number(b.getAttribute('data-f'))===frame);\n      }\n      // OCULTAR: some com o slot ANTES de compor. Assim a peca some, o `h` dela para de esconder\n      // parte do corpo e o vslot dela para de valer -- ocultar o chapeu devolve o cabelo, que e o\n      // comportamento pedido. Uma linha em vez de tres regras espalhadas dentro de `compor`.\n      const eqv=Object.create(null);\n      for(const k in s.equip)if(!s.oculto[k])eqv[k]=s.equip[k];\n      const {L}=compor(sk,eqv,acao,frame,s.efeito,s.efItem,s.stance49===true,s.orelha,s.emote);const z=ZD;\n      // FLIP: espelho puro do COMPOSTO em torno do eixo x=0 do mundo (CX). Nao mexe em zi nem em\n      // ancora -- e por isso ancoras, vslot e ItemEff viram juntos, de graca. O eixo NAO e o centro\n      // do bounding box. Os sprites do jogo olham para a ESQUERDA, entao ligado = virado a direita.\n      // As guias e a sombra ficam FORA do espelho de proposito (sao do palco, nao do personagem).\n      const vira=s.flip===true;\n      if(vira){ctx.save();ctx.translate(CX*2,0);ctx.scale(-1,1);}\n      for(const c of L){\n        if(c.base!==undefined){const im=imgBase(sk,c.base);if(!im||!im.complete||!im.naturalWidth)continue;ctx.drawImage(im,CX+c.x*z,CY+c.y*z,im.naturalWidth*z,im.naturalHeight*z);}\n        else{\n          const at=img(c.ref);if(!at||!at.complete||!at.naturalWidth)continue;const[u,v,w,h]=c.uv;\n          // `noflip`: a POSICAO espelha, o BITMAP nao. Sai do espelho e desenha no x refletido,\n          // que e a formula do contrato: translate(cx - (x+w)*zoom, cy + y*zoom) scale(zoom).\n          if(vira&&c.nfw){\n            ctx.save();ctx.setTransform(1,0,0,1,0,0);\n            ctx.drawImage(at,u,v,w,h,CX-(c.x+c.nfw)*z,CY+c.y*z,w*z,h*z);\n            ctx.restore();\n          }else ctx.drawImage(at,u,v,w,h,CX+c.x*z,CY+c.y*z,w*z,h*z);\n        }\n      }\n      if(vira)ctx.restore();\n    }\n    raf=requestAnimationFrame(draw);\n\n    // O seletor de skin saiu do painel lateral e virou a primeira caixa do HUD, dentro do palco.\n    // ABRIR UM FECHA OS OUTROS. Sem isto dois popups se sobrepoem no mesmo lugar (todos abrem\n    // ancorados na fileira, com left/right em 0), e o de baixo fica inalcancavel.\n    function abre(qual){\n      const v=!s[qual];\n      s.acAberto=false;s.emAberto=false;s.orAberto=false;s.skAberto=false;\n      s[qual]=v;\n    }\n    d2.on('.skbtn','click',()=>{abre('skAberto');});\n    d2.on('.embtn','click',()=>{abre('emAberto');});\n    d2.on('.orbtn','click',()=>{abre('orAberto');});\n    // O addon carrega ANTES de trocar o estado: se trocasse antes, o primeiro quadro sairia\n    // pedindo uma orelha que ainda nao chegou e cairia no fallback (a humana), piscando.\n    d2.on('.emit','click',async (e,el)=>{\n      const t=el.getAttribute('data-t');\n      s.emAberto=false;\n      if(t!=='default'&&s.equip.face){s.carregando=true;try{await carregaEmote(s.equip.face);}finally{s.carregando=false;}}\n      s.emote=t;\n    });\n    d2.on('.orit','click',async (e,el)=>{\n      const t=el.getAttribute('data-t');\n      s.orAberto=false;\n      if(t!=='humanEar'){s.carregando=true;try{await carregaOrelhas();}finally{s.carregando=false;}}\n      s.orelha=t;\n    });\n    d2.on('.skcel','click',async (e,el)=>{\n      const i=Number(el.getAttribute('data-i'))||0;\n      s.skAberto=false;\n      if(i===s.iSkin)return;                 // reclicar a mesma nao empilha historico\n      s.carregando=true;\n      try{await carregaSkin(i);marca();}finally{s.carregando=false;}\n    });\n    // Clicar num slot VAZIO leva a grade da direita para aquela categoria. E a afordancia que o\n    // esboco pede sem dizer: celula rotulada e vazia so faz sentido se for um alvo.\n    d2.on('.vaz','click',async (e,el)=>{\n      const c=el.getAttribute('data-cat');\n      if(!c)return;\n      const emCache=!!IDX[c];\n      if(!emCache)s.carregando=true;\n      try{await trocaCategoria(c);await prefetch();}\n      finally{if(!emCache)s.carregando=false;}\n    });\n    d2.on('.anelbtn','click',()=>{s.anelOff=!s.anelOff;});\n    // ARRASTO: nada disto toca em d2.state, senao cada mousemove reconstruiria a UI inteira.\n    // O rAF le ARRX/ARRY direto e ja desenha no lugar novo.\n    d2.on('.palco','mousedown',e=>{\n      const t=e.target;\n      if(t&&t.closest&&t.closest('.reset'))return;   // o botao de reset nao inicia arrasto\n      ARRAST={x:e.clientX,y:e.clientY,ox:ARRX,oy:ARRY};\n      e.preventDefault();\n    });\n    d2.on(window,'mousemove',e=>{\n      if(!ARRAST)return;\n      ARRX=ARRAST.ox+(e.clientX-ARRAST.x);\n      ARRY=ARRAST.oy+(e.clientY-ARRAST.y);\n    });\n    d2.on(window,'mouseup',()=>{ARRAST=null;});\n    d2.on('.reset','click',()=>{ARRX=0;ARRY=0;});\n    // ESTA FUNCAO EXISTE POR CAUSA DO SHADOW DOM. Os atalhos escutam no `window`, e todo evento\n    // que cruza a fronteira do shadow e RETARGETADO: no nivel do window, `e.target` passa a ser o\n    // HOST do componente, nunca o <input> la dentro. As guardas antigas liam e.target.tagName e\n    // por isso NUNCA reconheciam o campo de busca -- digitar \"u\" nele escondia o anel, e o ctrl+z\n    // roubava o desfazer do texto.\n    // composedPath()[0] atravessa a fronteira e devolve o elemento de verdade.\n    function digitando(e){\n      const cam=(e.composedPath&&e.composedPath())||[];\n      const t=cam[0]||e.target;\n      if(!t)return false;\n      const tg=String(t.tagName||'').toUpperCase();\n      return tg==='INPUT'||tg==='TEXTAREA'||tg==='SELECT'||t.isContentEditable===true;\n    }\n    // U mostra/esconde o anel. Sem ctrl de proposito: e atalho de VISUALIZACAO, nao de edicao,\n    // e o ctrl+z desta base ja e o undo.\n    d2.on(window,'keydown',e=>{\n      if(e.ctrlKey||e.metaKey||e.altKey)return;\n      if(digitando(e))return;\n      if(String(e.key||'').toLowerCase()==='u'){e.preventDefault();s.anelOff=!s.anelOff;}\n    });\n    d2.on('.und','click',()=>{desfaz();});\n    d2.on('.red','click',()=>{refaz();});\n    // Ctrl+Z / Ctrl+Y (e Ctrl+Shift+Z). Dentro do builder do data2 o editor costuma capturar o\n    // atalho antes de chegar aqui; no app publicado funciona. Os botoes sao a via confiavel.\n    d2.on(window,'keydown',e=>{\n      if(!(e.ctrlKey||e.metaKey))return;\n      if(digitando(e))return;   // nao rouba o ctrl+z do campo de texto -- ver a nota em digitando()\n      const k=String(e.key||'').toLowerCase();\n      if(k==='z'&&!e.shiftKey){e.preventDefault();desfaz();}\n      else if(k==='y'||(k==='z'&&e.shiftKey)){e.preventDefault();refaz();}\n    });\n    // trocar de action reinicia o relogio, senao a nova comeca no meio do ciclo da anterior\n    d2.on('.ac','click',(e,el)=>{s.acao=el.getAttribute('data-a');s.frame=0;s._t0=Date.now();TEMPO=0;s.acAberto=false;});\n    d2.on('.acbtn','click',()=>{abre('acAberto');});\n    d2.on('.mestre','click',()=>{s.travaModo=!s.travaModo;});\n    // No modo cadeado o centro tranca; fora dele, oculta. O `.cad` fica por cima do `.eqc`, mas\n    // os dois fazem a mesma coisa no modo cadeado, entao qualquer um que pegue o clique serve.\n    d2.on('.eqc','click',(e,el)=>{\n      const sl=el.getAttribute('data-slot');\n      if(s.travaModo){const t=Object.assign({},s.travado);t[sl]=!t[sl];s.travado=t;}\n      else{const o=Object.assign({},s.oculto);o[sl]=!o[sl];s.oculto=o;salvaCache();}\n    });\n    d2.on('.cad','click',(e,el)=>{const sl=el.getAttribute('data-slot');const t=Object.assign({},s.travado);t[sl]=!t[sl];s.travado=t;});\n    d2.on('.fr','click',(e,el)=>{s.modo='parado';s.frame=Number(el.getAttribute('data-f'))||0;TEMPO=0;});\n    // So acende o \"loading\" quando ha o que baixar. Ligar/desligar o aviso gera DOIS re-renders,\n    // e navegar entre categorias ja em cache nao precisa de nenhum.\n    d2.on('.catsel','click',async (e,el)=>{\n      const c=el.getAttribute('data-c');\n      const emCache=c==='all'?CATS.every(x=>IDX[x]):!!IDX[c];\n      if(!emCache)s.carregando=true;\n      try{await trocaCategoria(c);await prefetch();}\n      finally{if(!emCache)s.carregando=false;}\n    });\n    d2.on('.cel','click',async (e,el)=>{\n      // Respeita a cor escolhida: a celula carrega o id do ESTILO (a primeira variante), entao\n      // aqui trocamos pela variante cujo ultimo digito bate com s.corSel. Sem variante nessa cor,\n      // segue a da celula -- e o que evita equipar nada quando o estilo nao tem aquela cor.\n      let cid=el.getAttribute('data-id'), cref=el.getAttribute('data-ref')||null;\n      const m=META[cid];\n      if(m&&m.cores&&m.cores.length>1){\n        const alvo=m.cores.find(cc=>Number(String(cc[0]).slice(-1))===s.corSel);\n        if(alvo){cid=alvo[0];cref=alvo[1];}\n      }\n      await equipar(cid,el.getAttribute('data-geo')||null,cref,el.getAttribute('data-cat')||null);\n      // trocar de FACE com um emote ativo precisa do bloco da face nova, senao a expressao\n      // sumiria em silencio (cairia no default) e pareceria bug.\n      if(s.emote&&s.emote!=='default')await carregaEmote(s.equip.face);\n    });\n    // Registrar a ESCOLHA sempre; trocar o item so quando ha variante para esta cor. Sem cabelo\n    // equipado a paleta continua clicavel e a escolha fica guardada para o proximo estilo.\n    d2.on('.cor','click',async (e,el)=>{\n      // salva a ESCOLHA de cor mesmo quando nao ha variante para trocar: sem isto, escolher a\n      // cor sem cabelo equipado (a paleta fica sempre clicavel) nao sobreviveria ao reload.\n      s.corSel=Number(el.getAttribute('data-dg'))||0;salvaCache();\n      const id=el.getAttribute('data-id');if(!id)return;\n      const ct=el.getAttribute('data-cat')||s.catAtual;\n      const it=await carregaItem(id,el.getAttribute('data-geo')||null,el.getAttribute('data-ref')||null,ct);\n      if(it){img(it.atlasRef);defineEquip(Object.assign({},s.equip,{[ct]:id}));}\n    });\n    // desequipar tambem limpa o oculto/travado do slot, senao a marca fica pendurada no proximo\n    d2.on('.x','click',(e,el)=>{\n      const sl=el.getAttribute('data-slot');\n      const ee=Object.assign({},s.equip);delete ee[sl];defineEquip(ee);\n      const o=Object.assign({},s.oculto);delete o[sl];s.oculto=o;\n      const t=Object.assign({},s.travado);delete t[sl];s.travado=t;\n    });\n    let tb=null;\n    d2.on('.busca','input',(e,el)=>{s.q=el.value;s.pag=0;if(tb)clearTimeout(tb);tb=setTimeout(()=>{STAT.busca++;prefetch();},400);});\n    d2.on('.pp','click',()=>{s.pag=Math.max(0,(s.pag||0)-1);prefetch();});\n    d2.on('.np','click',()=>{s.pag=(s.pag||0)+1;prefetch();});\n    // A ultima pagina sai do MESMO calculo do render: `pagina(s)` ja considera o filtro da busca,\n    // entao ir para o fim vale para a lista visivel e nao para o catalogo inteiro. Fixar um\n    // numero grande tambem funcionaria (o pagina() satura), mas deixaria s.pag mentindo no estado.\n    d2.on('.pf','click',()=>{s.pag=0;prefetch();});\n    d2.on('.ul','click',()=>{s.pag=Math.max(0,pagina(s).paginas-1);prefetch();});\n    d2.on('.zm','click',()=>{s.zoom=Math.max(1,s.zoom-1);});\n    d2.on('.zp','click',()=>{s.zoom=Math.min(8,s.zoom+1);});\n    d2.on('.ef','click',()=>{s.efeito=!s.efeito;});\n    d2.on('.efi','click',()=>{s.efItem=!s.efItem;});\n    d2.on('.st49','click',()=>{s.stance49=!s.stance49;});\n    d2.on('.flip','click',()=>{s.flip=!s.flip;});\n    // QUADRO FIXO -- nao enquadra pelo bbox de cada personagem. A base nao se mexe (medido: 45\n    // das 46 skins tem geometria identica em stand1/frame0; a 00002020 difere 1px na orelha),\n    // entao o quadro tambem nao pode se mexer: com bbox, um chapeu alto mudava o zoom e o corte\n    // de TODO o retrato, e dois personagens nao davam para comparar.\n    // Os numeros NAO sao escolhidos, sao ajustados as 6 referencias de estilo (UIAssets), medidas\n    // com o mesmo segmentador: queixo 80,4% +-2,3 / largura da cabeca 88,7% +-8,7 / eixo 49,6% +-2,3.\n    // Este quadro entrega queixo 80,9 / largura 87,3 / eixo 48,7 -- as tres dentro do desvio.\n    // Verificado em 6 personagens diferentes: o corpo cai nos MESMOS pixels (torso x 140..349).\n    // O quadro antigo (x-35 y-82 z8) dava largura 70,1%, abaixo de TODAS as referencias: o sprite\n    // ia enquadrado menor que o estilo alvo, e o modelo brigava com a propria referencia.\n    // Cortar cabelo alto e aceito (decisao do usuario) -- 3 das 6 refs tem cabelo na borda de cima.\n    // 512/51 = 10 exato: o zoom precisa ser INTEIRO, senao o nearest-neighbor borra.\n    const QX0=-28,QY0=-71,QLADO=51;\n    // VISTA LARGA, so para a etapa de OBSERVACAO. O observador precisa ver a roupa inteira; com\n    // o busto apertado ele lia o casaco errado (chamou capa vermelha com dourado de \"top magenta\").\n    // Ela NAO vai para o desenho -- se fosse, a saida virava corpo inteiro.\n    const OX0=-60,OY0=-85,OLADO=128;\n    function exportaPersonagem(lado,x0,y0,ladoMundo){\n      x0=(x0===undefined?QX0:x0);y0=(y0===undefined?QY0:y0);ladoMundo=(ladoMundo===undefined?QLADO:ladoMundo);\n      const sk=skinAtual();if(!sk||!sk.geo)return null;\n      // SEMPRE stand1/frame0 e sem flip: a referencia nao pode depender de onde a animacao\n      // estava, nem dos toggles de fx, senao dois retratos do mesmo personagem saem diferentes.\n      const acoes=Object.keys(sk.geo||{});\n      const acao=acoes.indexOf('stand1')>=0?'stand1':acoes[0];\n      const eqv=Object.create(null);\n      for(const k in s.equip)if(!s.oculto[k])eqv[k]=s.equip[k];\n      const {L}=compor(sk,eqv,acao,0,false,true,s.stance49===true,s.orelha,s.emote);\n      const z=Math.max(1,Math.round(lado/ladoMundo));\n      const cv=document.createElement('canvas');cv.width=lado;cv.height=lado;\n      const g=cv.getContext('2d');\n      // Cinza neutro, nao branco e nao transparente: PNG com alpha costuma virar preto no gerador,\n      // e o branco competia com a pele e com o brilho do cabelo. As 6 refs NAO concordam entre si\n      // no fundo (3 claras, 3 escuras), entao fundo nao se aprende delas -- vem daqui.\n      g.fillStyle='#808080';g.fillRect(0,0,lado,lado);\n      g.imageSmoothingEnabled=false;\n      let faltou=0,pintou=0;\n      for(const c of L){\n        if(c.base!==undefined){\n          const im=imgBase(sk,c.base);\n          if(!im||!im.complete||!im.naturalWidth){faltou++;continue;}\n          g.drawImage(im,(c.x-x0)*z,(c.y-y0)*z,im.naturalWidth*z,im.naturalHeight*z);pintou++;\n        }else{\n          const at=img(c.ref);\n          if(!at||!at.complete||!at.naturalWidth){faltou++;continue;}\n          const u=c.uv[0],v=c.uv[1],w=c.uv[2],h=c.uv[3];\n          g.drawImage(at,u,v,w,h,(c.x-x0)*z,(c.y-y0)*z,w*z,h*z);pintou++;\n        }\n      }\n      if(!pintou)return null;\n      return {url:cv.toDataURL('image/png'),faltou:faltou};\n    }\n    // ---- COR POR SLOT, medida nos PIXELS do proprio sprite ----------------------------\n    // Motivo: o observador de visao erra cor com frequencia (chamou roxo de magenta, e casaco\n    // vermelho com dourado de \"top magenta\"). Pixel nao tem essa liberdade. Divisao de trabalho:\n    // o observador descreve FORMA, esta funcao da a COR.\n    // Nomear a cor importa: com hex puro o resultado ficou pior do que com \"silver #989898\".\n    const CORHUE=[['red',0],['orange',30],['yellow',55],['green',110],['teal',170],['blue',215],['violet',265],['magenta',310],['red',350]];\n    function nomeDaCor(r,g,b){\n      const mx=Math.max(r,g,b),mn=Math.min(r,g,b),l=(mx+mn)/2;\n      const sat=satDe([r,g,b]);\n      if(sat<0.12)return l<40?'black':(l<105?'dark grey':(l<175?'grey':(l<235?'silver':'white')));\n      let h=0;\n      if(mx===r)h=60*((((g-b)/(mx-mn))%6+6)%6);\n      else if(mx===g)h=60*(((b-r)/(mx-mn))+2);\n      else h=60*(((r-g)/(mx-mn))+4);\n      if(h<0)h+=360;\n      let nome='red',melhor=1e9;\n      for(const p of CORHUE){const d=Math.min(Math.abs(h-p[1]),360-Math.abs(h-p[1]));if(d<melhor){melhor=d;nome=p[0];}}\n      return (l<80?'deep ':(l>200?'pale ':''))+nome;\n    }\n    function hx(c){return '#'+c.map(v=>('0'+v.toString(16)).slice(-2)).join('');}\n    function cromaDe(c){return Math.max(c[0],c[1],c[2])-Math.min(c[0],c[1],c[2]);}\n    // A saturacao HSL EXPLODE em luminancia extrema: #e8f8f8 (quase branco, croma 16) devolve\n    // 0,53. Isso fez o extrator nomear a camisa de \"pale teal\" e o modelo pintou a peca de\n    // VERDE. Croma abaixo de 40 e neutro, ponto. Achado medindo variancia entre duas rodadas\n    // com a MESMA entrada -- nao era variancia do modelo, era o nome mentindo.\n    function satDe(c){\n      if(cromaDe(c)<40)return 0;\n      const mx=Math.max(c[0],c[1],c[2]),mn=Math.min(c[0],c[1],c[2]);\n      return mx===mn?0:(mx-mn)/(255-Math.abs(mx+mn-255));\n    }\n    // Conta as cores das camadas de UM slot. Agrupa em passos de 8 -- sem isso cada pixel de\n    // anti-alias vira uma \"cor\" propria e o ranking vira ruido.\n    function lumDe(c){return 0.299*c[0]+0.587*c[1]+0.114*c[2];}\n    function hueDe(c){\n      const mx=Math.max(c[0],c[1],c[2]),mn=Math.min(c[0],c[1],c[2]);\n      if(mx===mn)return 0;\n      let h;\n      if(mx===c[0])h=60*((((c[1]-c[2])/(mx-mn))%6+6)%6);\n      else if(mx===c[1])h=60*(((c[2]-c[0])/(mx-mn))+2);\n      else h=60*(((c[0]-c[1])/(mx-mn))+4);\n      return h<0?h+360:h;\n    }\n    // Pele: faixa estreita de matiz, saturacao media e CLARA. A luminancia e o que separa pele\n    // de uma iris ambar, que cai na mesma faixa de matiz mas e escura.\n    function ehPele(c){const h=hueDe(c);return h>=8&&h<=45&&satDe(c)>=0.15&&satDe(c)<=0.60&&lumDe(c)>140;}\n    // `corte` zera o filtro de contorno. O slot face precisa disso: a iris pode ser PRETA\n    // (medido: 60% dos pixels num dos rostos) e o filtro a apagava.\n    function contaCores(sk,slot,id,corte){\n      corte=(corte===undefined?110:corte);\n      const eq=Object.create(null);eq[slot]=id;\n      let L=[];\n      try{L=(compor(sk,eq,'stand1',0,false,true,false)||{}).L||[];}catch(err){return null;}\n      const cnt=Object.create(null);let viu=0;\n      for(const c of L){\n        if(c.base!==undefined)continue;             // camada do CORPO, nao da peca\n        const at=img(c.ref);if(!at||!at.complete||!at.naturalWidth)continue;\n        const u=c.uv[0],v=c.uv[1],w=c.uv[2],h=c.uv[3];\n        if(!w||!h)continue;\n        const cv=document.createElement('canvas');cv.width=w;cv.height=h;\n        const g=cv.getContext('2d');g.imageSmoothingEnabled=false;\n        g.drawImage(at,u,v,w,h,0,0,w,h);\n        let d;try{d=g.getImageData(0,0,w,h).data;}catch(err){return null;}\n        for(let i=0;i<d.length;i+=4){\n          if(d[i+3]<200)continue;\n          if(corte&&d[i]+d[i+1]+d[i+2]<corte)continue;\n          const k=((d[i]>>3)<<3)+','+((d[i+1]>>3)<<3)+','+((d[i+2]>>3)<<3);\n          cnt[k]=(cnt[k]||0)+1;viu++;\n        }\n      }\n      if(!viu)return null;\n      return Object.keys(cnt).map(k=>({c:k.split(',').map(Number),n:cnt[k],p:cnt[k]/viu*100}))\n                            .sort((a,b)=>b.n-a.n).slice(0,10);\n    }\n    // A cor MAIS FREQUENTE costuma ser a SOMBRA -- no cabelo do piloto ela e 45% dos pixels, e\n    // mandar a lista crua puxou a saida para cinza-amarronzado. Por isso: papeis, nao ranking.\n    function coresDoSlot(sk,slot,id){\n      const cs=contaCores(sk,slot,id);if(!cs||!cs.length)return '';\n      const ord=cs.slice().sort((a,b)=>lumDe(a.c)-lumDe(b.c));\n      const med=lumDe(ord[ord.length>>1].c);\n      let claros=cs.filter(x=>lumDe(x.c)>=med);if(!claros.length)claros=[ord[ord.length-1]];\n      // O peso por SATURACAO foi achado testando um segundo personagem: num cabelo roxo o pardo\n      // de 25% vencia o roxo de 16% e a peca inteira saia descrita como laranja. Com tudo\n      // dessaturado (prata, branco) o fator e igual para todos e a frequencia decide, como antes.\n      const base=claros.reduce((a,b)=>(a.p*(0.3+satDe(a.c))>=b.p*(0.3+satDe(b.c))?a:b)).c;\n      const alto=ord[ord.length-1].c, sombra=ord[0].c;\n      const dif=c=>hx(c)!==hx(base)&&hx(c)!==hx(alto)&&hx(c)!==hx(sombra);\n      const lim=satDe(base)<0.12?0.30:0.25;\n      const ac=cs.filter(x=>dif(x.c)&&satDe(x.c)>lim).slice(0,2);\n      let t=nomeDaCor(base[0],base[1],base[2])+' '+hx(base)+', highlights '+hx(alto)+', shadows '+hx(sombra);\n      if(ac.length)t+=', accents '+ac.map(x=>nomeDaCor(x.c[0],x.c[1],x.c[2])+' '+hx(x.c)).join(' and ');\n      return t;\n    }\n    // A IRIS e uma cor ESCURA e SATURADA que nao e pele. Medido em tres rostos: o preto do\n    // contorno ocupa 33-60% dos pixels em TODOS, entao \"a mais frequente\" devolve preto sempre,\n    // e \"a mais saturada\" devolve pele ou o branco azulado da esclera. Quando nada passa no\n    // teste, o olho e preto de verdade -- nao e falha da regra.\n    function coresDoOlho(sk,id){\n      const cs=contaCores(sk,'face',id,0);if(!cs||!cs.length)return '';\n      const v=cs.filter(x=>satDe(x.c)>0.50&&lumDe(x.c)<140&&!ehPele(x.c)&&x.p>=5);\n      if(!v.length)return 'black #000000';\n      return v.slice(0,2).map(x=>nomeDaCor(x.c[0],x.c[1],x.c[2])+' '+hx(x.c)).join(' and ');\n    }\n    // ---- ENQUADRAMENTO DETERMINISTICO DA SAIDA ---------------------------------------\n    // O gerador NAO obedece o corte de forma estavel: com o mesmo prompt, o queixo saiu a 81,2%\n    // num personagem e a 67,4% noutro. Reforcar por texto so levou 67,4 para 73,6. Enquadramento\n    // e geometria; entao se resolve na saida, em vez de brigar por palavra.\n    // A silhueta sai por SUBTRACAO DE FUNDO -- o fundo gerado e uniforme, entao a mascara ja\n    // nasce solida e nao precisa de fill-holes nem componente conexa. Conferido em 6 imagens\n    // contra o detector completo: erro de 0,2 a 1,6 ponto. A versao por gradiente, sem fill,\n    // errava 9,2 pontos justo no caso que mais precisava de correcao.\n    // 0,804 e a MEDIA das 6 refs de estilo (desvio 2,3), nao do pilot_output. O pilot mede 0,819\n    // e por um tempo foi o alvo, mas ele e de outra familia de traco -- ver a nota na secao 23 da doc.\n    // (QA 10/09: estas tres linhas eram UMA so, com barra-n literal, e o const ficava comentado.)\n    const QUEIXO_ALVO=0.804;\n    function enquadra(url){\n      return new Promise(res=>{\n        const im=new Image();im.crossOrigin='anonymous';\n        im.onerror=()=>res(url);\n        im.onload=()=>{\n          try{\n            const N=256,cv=document.createElement('canvas');cv.width=N;cv.height=N;\n            const g=cv.getContext('2d');g.drawImage(im,0,0,N,N);\n            const d=g.getImageData(0,0,N,N).data;\n            const am=[[],[],[]];\n            for(let y=0;y<N;y++)for(let x=0;x<N;x++){\n              if(x>2&&x<N-3&&y>2&&y<N-3)continue;      // so a moldura de 3px\n              const i=(y*N+x)*4;am[0].push(d[i]);am[1].push(d[i+1]);am[2].push(d[i+2]);\n            }\n            const med=am.map(v=>{v.sort((p,q)=>p-q);return v[v.length>>1];});\n            const larg=new Array(N).fill(0),lin=[];\n            for(let y=0;y<N;y++){\n              const row=new Uint8Array(N);let n=0;\n              for(let x=0;x<N;x++){\n                const i=(y*N+x)*4;\n                if(Math.abs(d[i]-med[0])+Math.abs(d[i+1]-med[1])+Math.abs(d[i+2]-med[2])>30){row[x]=1;n++;}\n              }\n              larg[y]=n;lin.push(row);\n            }\n            let a=-1,b=-1;\n            for(let y=0;y<N;y++)if(larg[y]){if(a<0)a=y;b=y;}\n            if(a<0){res(url);return;}\n            // o queixo e a linha MAIS ESTREITA entre 30% e 85% da altura do sujeito: o pescoco\n            const lo=a+Math.floor((b-a)*0.30),hi=Math.max(lo+1,a+Math.floor((b-a)*0.85));\n            let ny=lo,mn=1e9;\n            for(let y=lo;y<hi;y++)if(larg[y]<mn){mn=larg[y];ny=y;}\n            const col=new Uint8Array(N);\n            for(let y=a;y<ny;y++)for(let x=0;x<N;x++)if(lin[y][x])col[x]=1;\n            let c0=-1,c1=-1;\n            for(let x=0;x<N;x++)if(col[x]){if(c0<0)c0=x;c1=x;}\n            const eixo=(c0<0?0.5:(c0+c1)/2/N), queixo=ny/N;\n            const W=im.naturalWidth,H=im.naturalHeight;\n            // recorte quadrado a partir do TOPO: lado = queixo/alvo poe o queixo no alvo exato.\n            const S=Math.min(H,W,Math.max(64,queixo*H/QUEIXO_ALVO));\n            const x0=Math.max(0,Math.min(W-S,eixo*W-S/2));\n            const out=document.createElement('canvas');out.width=1024;out.height=1024;\n            out.getContext('2d').drawImage(im,x0,0,S,S,0,0,1024,1024);\n            res(out.toDataURL('image/png'));\n          }catch(err){res(url);}      // qualquer falha: devolve o original, nunca quebra o botao\n        };\n        im.src=url;\n      });\n    }\n    function descrevePersonagem(){\n      const partes=[];\n      const sk=(s.skins[s.iSkin]||{}).name;\n      if(sk)partes.push('skin tone '+sk);\n      for(const sl of ORDEM_EQ){\n        const id=s.equip[sl];\n        if(!id||s.oculto[sl])continue;\n        const r=META[id];\n        partes.push((ROTULO[sl]||sl)+' '+((r&&r.name)||id));\n      }\n      return partes.length?partes.join(', ')+'.':'a plain character with no equipment.';\n    }\n    d2.on('.rnd','click',async()=>{STAT.rnd++;await sorteia();if(s.emote&&s.emote!=='default')await carregaEmote(s.equip.face);});\n    d2.on('.portx','click',()=>{s.verPortrait=false;});\n    // FEATURE DESLIGADA. O botao abre o aviso em vez de gerar.\n    // A pipeline continua INTEIRA no arquivo de proposito: `exportaPersonagem`, `coresDoSlot`,\n    // `coresDoOlho`, `enquadra` e o handler `mcportrait` estao no ar e funcionando. Nao foi\n    // apagado nada, o que falta e qualidade de imagem e nao codigo. Para religar, e devolver a\n    // chamada ao rush aqui (o historico do git tem a versao que gerava).\n    // O botao esta DESLIGADO e abre um aviso. Contar o clique mede a demanda por uma feature que\n    // ainda nao existe, sem precisar construi-la -- e a metrica mais barata deste painel.\n    d2.on('.port','click',()=>{STAT.port++;s.verPortrait=true;});\n    // o relogio do ItemEff zera junto com o do corpo, senao o efeito comeca no meio do ciclo\n    d2.on('.play','click',()=>{s.modo=s.modo==='loop'?'parado':'loop';s._t0=Date.now();TEMPO=0;});\n    d2.on('.play1','click',()=>{s.modo='uma';s._t0=Date.now();TEMPO=0;});\n    d2.onUnmount(()=>{if(raf)cancelAnimationFrame(raf);});\n  }\n};\n"}},"skins":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"skins"}}}},"zmap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"zmap"}}}}}}}}}},{"_id":"6a85aba97191cadfbc60e201","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"katteiji-faq","package":"bigaton-playground","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 ITEMS = [\n  {\n    q: 'What exactly do I get for the monthly rate?',\n    a: 'Two active design requests at any time, worked by senior designers, with unlimited revisions on each. You keep the source files. There is no separate discovery phase, onboarding fee, or change-order process.'\n  },\n  {\n    q: 'How fast is the first delivery?',\n    a: 'Direction lands within 72 hours of the brief. First production-ready screens or assets typically ship inside 14 days — that is the median across every engagement since 2017.'\n  },\n  {\n    q: 'Who actually does the work?',\n    a: 'The eight people on this page. No account managers, no offshore relay, no junior pass disguised as a review. The designer who takes your brief is the one who writes you back.'\n  },\n  {\n    q: 'What if we only need one project?',\n    a: 'Take a fixed-scope sprint instead. Four weeks, quoted upfront from $9,500, no retainer. Most brand systems and marketing sites fit comfortably inside one sprint.'\n  },\n  {\n    q: 'Can we pause the plan?',\n    a: 'Any month, from the dashboard, no conversation required. Paused months are not billed and your queue is exactly where you left it when you come back.'\n  },\n  {\n    q: 'Do you hand off to our engineers?',\n    a: 'Always. Every delivery arrives with design tokens, component specs, exported assets and a Figma file organised for build — not a flattened presentation.'\n  }\n];\n\nexport default {\n  css: `\n    :host { display: block; }\n    .faq {\n      font-family: Inter, system-ui, -apple-system, sans-serif;\n      color: #131311;\n    }\n    .row {\n      border-top: 1px solid #E3E3E3;\n      cursor: pointer;\n      transition: background 150ms cubic-bezier(0.4,0,0.2,1);\n    }\n    .row:last-child { border-bottom: 1px solid #E3E3E3; }\n    .row:hover .q { opacity: 0.62; }\n    .head {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 24px;\n      padding: 28px 4px 28px 0;\n    }\n    .q {\n      font-size: 21px;\n      font-weight: 700;\n      letter-spacing: -0.4px;\n      line-height: 1.25;\n      transition: opacity 150ms cubic-bezier(0.4,0,0.2,1);\n    }\n    .sign {\n      flex: 0 0 auto;\n      width: 36px;\n      height: 36px;\n      border-radius: 999px;\n      background: #F3F2EA;\n      color: #131311;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 17px;\n      font-weight: 600;\n      line-height: 1;\n      transition: background 150ms cubic-bezier(0.4,0,0.2,1), color 150ms cubic-bezier(0.4,0,0.2,1);\n    }\n    .row.open .sign { background: #C6FD50; }\n    .body {\n      display: grid;\n      grid-template-rows: 0fr;\n      transition: grid-template-rows 320ms cubic-bezier(0.4,0,0.2,1);\n    }\n    .row.open .body { grid-template-rows: 1fr; }\n    .bodyInner { overflow: hidden; }\n    .a {\n      max-width: 640px;\n      font-size: 16px;\n      font-weight: 400;\n      line-height: 1.6;\n      color: #131311;\n      opacity: 0.68;\n      padding: 0 0 30px 0;\n    }\n    @media (prefers-reduced-motion: reduce) {\n      .body, .sign, .q, .row { transition: none; }\n    }\n  `,\n  render(d2) {\n    const open = d2.state.open === undefined ? 0 : d2.state.open;\n    return `<div class=\"faq\">` + ITEMS.map(function (it, i) {\n      const isOpen = open === i;\n      return `<div class=\"row ${isOpen ? 'open' : ''}\" data-key=\"faq-${i}\" data-idx=\"${i}\">\n          <div class=\"head\">\n            <div class=\"q\">${it.q}</div>\n            <div class=\"sign\">${isOpen ? '&#8722;' : '+'}</div>\n          </div>\n          <div class=\"body\"><div class=\"bodyInner\"><div class=\"a\">${it.a}</div></div></div>\n        </div>`;\n    }).join('') + `</div>`;\n  },\n  mounted(d2) {\n    if (d2.state.open === undefined) d2.state.open = 0;\n    d2.on('.row', 'click', function (e, el) {\n      const idx = Number(el.getAttribute('data-idx'));\n      d2.state.open = d2.state.open === idx ? -1 : idx;\n    });\n  }\n};"}}}}}}}},{"_id":"6a874795180c011e24d3e862","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mcbuilder","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// MapleCloset -- SO o personagem. Sem cabecalho, sem grade, sem palco, sem sombra de chao:\n// o bloco desenha apenas o sprite composto, em 1x, sobre fundo transparente. Os ids vem dos\n// params. O motor de composicao e o mesmo do mcbaseviewer (chave de frame do corpo, `h`\n// escondendo a base, vslot/smap do chapeu, islot de outfit/arma, hairShade por skin, ItemEff\n// com eixo proprio). O diagnostico de id invalido nao tem mais painel -- vai para d2.log,\n// porque qualquer pixel de UI aqui seria \"algo alem do personagem\".\nconst CATS=['hair','face','cap','cape','coat','pants','outfit','shoes','glove','weapon','offhand','accface','acceye','accear'];\nconst ROTULO={accface:'face acc',acceye:'eye acc',accear:'earring',offhand:'off-hand'};\n// TIMING E SEQUENCIA saem da tabela `mc-action` (2026-08-27) -- as mesmas linhas que o\n// mcbaseviewer le, para os dois animarem identico. O WZ tem `delay` em TODO frame do corpo e o\n// compilador descartava; a tabela devolveu esse dado. `frameDelays` e o delay do jogo,\n// `playback`/`loopStart` descrevem a sequencia, `actionSpeed` e o multiplicador de tweak.\nconst ACOES='https://bigaton-playground.data2apis.com/dataset/mc-actionDataSet';\nconst ACT=Object.create(null);   // key da action -> linha da tabela\nconst PASSO_PADRAO=208;          // fallback so para uma action que a tabela nao conheca\n// Eixo do mundo: x no meio, y em 72% da altura -- a origem cai perto dos pes e o corpo cresce\n// para cima (y negativo). Mesma proporcao do resto do projeto, para o enquadramento do bloco\n// ser previsivel qualquer que seja o tamanho dele.\nconst EIXO_Y=0.72, EF_VEL=1.15;\nconst ZOOM=1;                       // 1x fixo, por decisao de projeto (nao ha param de zoom)\nconst MEDIA='https://bigaton-playground.data2.media/file/';\n\nconst INDEXREF=Object.create(null); // cat -> ref do idx JSON\nconst IDX=Object.create(null);      // cat -> {items:[...]}\nconst MAPA=Object.create(null);     // cat -> variantId -> {geoRef, atlasRef}\nconst BLOCO=Object.create(null);    // geoRef -> bloco de geometria parseado\nconst ITENS=Object.create(null);    // id -> item pronto para desenhar\nconst IMG=Object.create(null);      // ref -> Image do atlas\nconst IMGBASE=Object.create(null);  // caminho do sprite da base -> Image\nconst SKINPIX=Object.create(null);  // skinId -> {geo,pix}\nconst META=Object.create(null);     // id -> linha do idx (vs do cap, efRef da arma)\nconst BLOCOEI=Object.create(null);  // blocoRef -> bloco de ItemEff\nlet ZI2SMAP=null, REGRAS=null, EFI=null;\nlet TEMPO=0;                        // relogio do ItemEff em ms\nconst WPSI=Object.create(null);     // arma que ocupa TAMBEM a off-hand\n\n// A tabela e opcional: se o fetch falhar tudo cai no passo padrao e o bloco segue desenhando.\nasync function carregaAcoes(){\n  const d=await (await fetch(ACOES)).json();\n  for(const r of ((d&&d.data)||[]))ACT[r.key]=r;\n}\n// `loopStart` separa a INTRO (frames que so tocam ao entrar na action) da parte que repete.\n// E o caso do walk: o frame 0 e transicao, o loop e 1,2,3,2 -- antes o frame 0 voltava a cada\n// volta porque a sequencia era montada sempre a partir do zero.\nfunction sequencia(a,n){\n  const r=ACT[a]||null;\n  const modo=r?r.playback:(n>2?'pingpong':'loop');\n  const ls=Math.min(Math.max(r?(r.loopStart|0):0,0),Math.max(n-1,0));\n  const intro=[];for(let i=0;i<ls;i++)intro.push(i);\n  const ciclo=[];for(let i=ls;i<n;i++)ciclo.push(i);\n  if(modo==='pingpong')for(let i=n-2;i>ls;i--)ciclo.push(i);\n  return{intro,ciclo,modo};\n}\n// Duracao de um frame: o delay do WZ dividido pelo multiplicador da tabela (actionSpeed 1 = o\n// tempo original do jogo, 2 = o dobro da velocidade). Mesma forma que o ItemEff ja usava.\nfunction duracao(a,i){\n  const r=ACT[a];\n  const fd=r&&r.frameDelays;\n  const d=(fd&&fd.length)?fd[Math.min(i,fd.length-1)]:PASSO_PADRAO;\n  const v=(r&&Number(r.actionSpeed)>0)?Number(r.actionSpeed):1;\n  return Math.max(16,(Number(d)||PASSO_PADRAO)/v);\n}\n// Frame a mostrar `ms` depois do inicio da action: percorre a intro UMA vez e depois repete o\n// ciclo. Com playback 'once' segura o ultimo frame em vez de reiniciar. 'driven' (ladder/rope)\n// avanca por distancia no jogo; aqui nao existe esse driver, entao toca como loop.\nfunction frameEm(a,n,ms){\n  const {intro,ciclo,modo}=sequencia(a,n);\n  if(!ciclo.length)return intro.length?intro[intro.length-1]:0;\n  let t=ms;\n  for(const f of intro){const d=duracao(a,f);if(t<d)return f;t-=d;}\n  if(modo==='once'){\n    for(const f of ciclo){const d=duracao(a,f);if(t<d)return f;t-=d;}\n    return ciclo[ciclo.length-1];\n  }\n  let total=0;for(const f of ciclo)total+=duracao(a,f);\n  if(total<=0)return ciclo[0];\n  t=t%total;\n  for(const f of ciclo){const d=duracao(a,f);if(t<d)return f;t-=d;}\n  return ciclo[ciclo.length-1];\n}\nfunction keys(g,a){return Object.keys((g||{})[a]||{});}\nfunction imgBase(sk,p){if(IMGBASE[p])return IMGBASE[p];const v=(sk.pix||{})[p];if(!v)return null;const im=new Image();im.src=v;IMGBASE[p]=im;return im;}\nfunction img(ref){if(!ref)return null;if(IMG[ref])return IMG[ref];const im=new Image();im.src=MEDIA+ref;IMG[ref]=im;return im;}\n\nfunction camadasEfItem(item,acao,fr,L){\n  const ei=item.ei;if(!ei||!ei.e||!ei.e.p)return;\n  const refs=(ei.e.q||{})[acao], anc=((ei.e.a||{})[acao]||{})[fr];\n  if(!refs||!anc)return;\n  for(let gi=0;gi<refs.length;gi++){\n    const grupo=ei.e.p[refs[gi]], A=anc[gi];\n    if(!grupo||!grupo.length||!A)continue;\n    let ciclo=0;for(const f of grupo)ciclo+=(f[3]||100);\n    let tt=ciclo?(TEMPO*EF_VEL)%ciclo:0,k=0;\n    for(let i=0;i<grupo.length;i++){tt-=(grupo[i][3]||100);if(tt<0){k=i;break;}}\n    const f=grupo[k],uv=ei.uv[f[0]];\n    if(uv)L.push({ref:item.eiRef,uv:uv,x:A[0]-f[1],y:A[1]-f[2],zi:f[4],dono:item.itemId});\n  }\n}\nfunction camadasItem(item,acao,fr,efeito,efItem,L,vskin){\n  const dono=item.itemId, ex=item.eiX;\n  const g=(item.geo&&item.geo.g)||{};\n  const sv=item.s, alvo=sv?sv['0']:-1;\n  for(const c of ((g[acao]||{})[fr]||[])){\n    if(ex&&ex[c[0]])continue;\n    let si=c[0];\n    if(sv&&si===alvo&&sv[vskin]!=null)si=sv[vskin];\n    const uv=item.uv[si];if(uv)L.push({ref:item.atlasRef,uv:uv,x:c[1],y:c[2],zi:c[3],dono:dono});\n  }\n  if(efItem)camadasEfItem(item,acao,fr,L);\n  if(efeito&&item.geoEf&&item.atlasEfRef){\n    for(const c of (((item.geoEf.g||{})[acao]||{})[fr]||[])){const uv=item.uvEf[c[0]];if(uv)L.push({ref:item.atlasEfRef,uv:uv,x:c[1],y:c[2],zi:c[3],dono:dono});}\n  }\n}\nfunction compor(sk,equip,acao,frame,efeito,efItem){\n  const gk=keys(sk.geo,acao);if(!gk.length)return{L:[],fr:'0',n:0};\n  // Indice de frame normalizado: fora da faixa da a volta, entao digitar 9 num stand1 de 3\n  // frames mostra o frame 0 em vez de apagar o personagem.\n  const fr=gk[((frame%gk.length)+gk.length)%gk.length];\n  const esconder={};\n  for(const slot in equip){\n    const it=ITENS[equip[slot]];if(!it)continue;\n    const h=(((it.geo&&it.geo.h)||{})[acao]||{})[fr]||[];\n    for(const k of h)esconder[k]=1;\n  }\n  let L=[];\n  for(const c of (sk.geo[acao][fr]||[])){if(esconder[c[4]])continue;L.push({base:c[0],x:c[1],y:c[2],zi:c[3],dono:'base'});}\n  const vskin=String(parseInt(sk.skinId,10)-2000);\n  for(const slot in equip){const it=ITENS[equip[slot]];if(it)camadasItem(it,acao,fr,efeito,efItem,L,vskin);}\n  // VSLOT: o chapeu declara em `vs` os codigos de smap que OCUPA; toda camada de outro item\n  // cujo zi mapeia para um desses codigos some. O proprio cap e a base sao isentos.\n  const capId=equip.cap;\n  if(capId&&ZI2SMAP){\n    const v=(META[capId]&&META[capId].vs)||'';\n    if(v){\n      const occ=Object.create(null);\n      for(let i=0;i<v.length;i+=2)occ[v.slice(i,i+2)]=1;\n      L=L.filter(c=>{\n        if(c.dono===capId||c.dono==='base')return true;\n        const cod=(c.zi>=0&&c.zi<ZI2SMAP.length)?ZI2SMAP[c.zi]:'';\n        return !cod||!occ[cod];\n      });\n    }\n  }\n  L.sort((a,b)=>b.zi-a.zi);\n  return{L,fr,n:gk.length};\n}\n\nexport default {\n  css:`\n  .wrap{position:relative;width:100%;height:100%;overflow:hidden;background:transparent}\n  canvas.p{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated;background:transparent}\n  `,\n\n  // render() e CONSTANTE de proposito: nao le d2.state nem param nenhum, entao nunca\n  // re-renderiza e o canvas nunca e devolvido ao default 300x150 do browser. Todo o estado\n  // vive em closure e e lido pelo laco de rAF -- que e o padrao correto para canvas.\n  render(){\n    return `<div class=\"wrap\"><canvas class=\"p\" data-d2-static></canvas></div>`;\n  },\n\n  mounted(d2){\n    let SKINS=[], EQUIP={}, SKINID='';\n    // Relogio da animacao. Reinicia quando a acao muda ou quando o Play liga -- sem isso uma\n    // acao de tiro unico (swingO1) comecaria no meio do ciclo, na fase em que o relogio global\n    // estivesse naquele instante.\n    let t0=Date.now(), acaoAntes='', playAntes=false;\n\n    const idDe=v=>String(v==null?'':v).trim();\n    const skinDe=v=>{const m=/(\\d{8})/.exec(String(v||''));return m?m[1]:'00002000';};\n    const diz=m=>{d2.log('[mcbuilder] '+m);};\n    async function jsonDe(ref){const r=await fetch(MEDIA+ref);if(!r.ok)throw new Error('fetch '+ref+' -> '+r.status);return r.json();}\n\n    // Um idx por categoria, e o MAPA que o viewer nao precisava: variantId -> geoRef/atlasRef.\n    // A cor de um item e uma VARIANTE (cores[i][0]) com atlas proprio (cores[i][1]) mas\n    // geometria compartilhada com o estilo -- as duas metades vem de lugares diferentes.\n    async function garanteIdx(cat){\n      if(IDX[cat]||!INDEXREF[cat])return;\n      const d=await jsonDe(INDEXREF[cat]);\n      IDX[cat]={items:d.items||[]};\n      if(d.zi2smap)ZI2SMAP=d.zi2smap;\n      const m=Object.create(null);\n      for(const r of IDX[cat].items){\n        META[r.id]=r;\n        if(r.cores&&r.cores.length){\n          for(const cc of r.cores){m[cc[0]]={geoRef:r.geoRef||null,atlasRef:cc[1]||null};if(!META[cc[0]])META[cc[0]]=r;}\n        }\n        if(!m[r.id])m[r.id]={geoRef:r.geoRef||null,atlasRef:(r.cores&&r.cores[0]&&r.cores[0][1])||null};\n      }\n      MAPA[cat]=m;\n    }\n    // So roda quando o id NAO esta na categoria do slot -- o caminho feliz nunca paga por isso.\n    async function achaEmOutraCat(id){\n      await Promise.all(CATS.map(garanteIdx));\n      for(const c of CATS){if(MAPA[c]&&MAPA[c][id])return c;}\n      return null;\n    }\n\n    async function carregaItem(cat,id){\n      await garanteIdx(cat);\n      const alvo=MAPA[cat]&&MAPA[cat][id];\n      if(!alvo){\n        const outra=await achaEmOutraCat(id);\n        if(outra)return{erro:id+': e um item de \"'+(ROTULO[outra]||outra)+'\", nao de \"'+(ROTULO[cat]||cat)+'\"'};\n        return{erro:id+': nao existe no catalogo'};\n      }\n      // Item sem geometria: existe, ocupa o slot e nao desenha nada (Bald Wig, Transparent Hat,\n      // armas so-stat). geoRef e atlasRef sao null de proposito; o `vs` do vslot vem do META.\n      if(!alvo.geoRef)return{ok:true,semGeo:true};\n      if(!BLOCO[alvo.geoRef])BLOCO[alvo.geoRef]=await jsonDe(alvo.geoRef);\n      const bl=BLOCO[alvo.geoRef]||{};\n      for(const iid in bl){if(!ITENS[iid]){const it=bl[iid];it.itemId=iid;ITENS[iid]=it;}}\n      const it=ITENS[id];\n      if(!it)return{erro:id+': esta no indice mas nao no bloco de geometria'};\n      it.atlasRef=alvo.atlasRef||null;\n      if(it.geoEf)it.atlasEfRef=(META[id]&&META[id].efRef)||null;\n      const m=EFI&&EFI[id];\n      if(m&&!it.ei){\n        if(!BLOCOEI[m.b])BLOCOEI[m.b]=await jsonDe(m.b);\n        const e=BLOCOEI[m.b][id];\n        if(e){it.ei=e;it.eiRef=m.a;const x=Object.create(null);for(const i of (m.x||[]))x[i]=1;it.eiX=x;}\n      }\n      it.pronto=1;\n      img(it.atlasRef);\n      return{ok:true};\n    }\n\n    // CONFLITO DE SLOT -- regra do jogo. Como todos os slots sao declarados de uma vez, a\n    // precedencia e explicita: a peca que DECLARA o vslot ganha (outfit sobre coat/pants,\n    // arma de duas maos sobre a off-hand). Desligavel por slotRules:false.\n    function aplicaRegras(eq){\n      const sup=[];\n      for(const par of ((REGRAS&&REGRAS.exclusivos)||[])){\n        const a=par[0],bs=par[1]||[];\n        if(eq[a])for(const b of bs){\n          if(eq[b]){sup.push((ROTULO[b]||b)+' '+eq[b]+' suprimido: '+(ROTULO[a]||a)+' ocupa esse slot');delete eq[b];}\n        }\n      }\n      const off=(REGRAS&&REGRAS.offhand)||'offhand';\n      if(eq.weapon&&WPSI[eq.weapon]&&eq[off]){\n        sup.push((ROTULO[off]||off)+' '+eq[off]+' suprimido: a arma '+eq.weapon+' ocupa as duas maos');\n        delete eq[off];\n      }\n      return sup;\n    }\n\n    // Assinatura = so o que exige BUSCA (ids + skin + regras). action/frame/play/flip/fx sao\n    // lidos frescos a cada quadro no laco, entao mudam ao vivo sem recarregar nada.\n    let assinatura=null,ocupado=false;\n    function assina(){\n      const p=CATS.map(c=>idDe(d2.param(c,'')));\n      p.push(String(d2.param('skin','')),String(d2.param('slotRules',true)));\n      return p.join('|');\n    }\n\n    async function sincroniza(){\n      if(ocupado)return;\n      ocupado=true;assinatura=assina();\n      try{\n        const skinId=skinDe(d2.param('skin','00002000'));\n        if(!SKINPIX[skinId]){\n          const linha=SKINS.filter(k=>k.id===skinId)[0];\n          if(!linha)throw new Error('skin '+skinId+' nao esta no indice');\n          SKINPIX[skinId]=await jsonDe(linha.ref);\n        }\n        SKINID=skinId;\n        const eq={};\n        for(const c of CATS){\n          const id=idDe(d2.param(c,''));\n          if(!id)continue;\n          let r;\n          try{r=await carregaItem(c,id);}catch(err){r={erro:id+': '+((err&&err.message)||err)};}\n          if(r.erro){diz(c+' -> '+r.erro);continue;}\n          if(r.semGeo)diz(c+' -> '+id+': item sem geometria (ocupa o slot, nao desenha)');\n          eq[c]=id;\n        }\n        if(d2.param('slotRules',true)!==false)for(const m of aplicaRegras(eq))diz(m);\n        EQUIP=eq;\n      }catch(err){diz((err&&err.message)||String(err));}\n      finally{ocupado=false;}\n    }\n\n    (async()=>{\n      try{\n        try{await carregaAcoes();}catch(err){diz('tabela de actions indisponivel, usando o passo padrao: '+((err&&err.message)||err));}\n        const mi=await d2.rush('mcindexall').invoke({});\n        for(const r of ((mi&&mi.data&&mi.data.rows)||[]))INDEXREF[r.cat]=r.ref;\n        if(!INDEXREF._skin)throw new Error('mcindexall respondeu sem a linha _skin');\n        const li=await jsonDe(INDEXREF._skin);\n        SKINS=(li.items||[]).slice().sort((a,b)=>String(a.id).localeCompare(String(b.id)));\n        if(INDEXREF._regras){REGRAS=await jsonDe(INDEXREF._regras);for(const w of (REGRAS.wpsi||[]))WPSI[w]=1;}\n        if(INDEXREF._itemeff)EFI=await jsonDe(INDEXREF._itemeff);\n        await sincroniza();\n      }catch(err){diz((err&&err.message)||String(err));}\n    })();\n\n    function desenha(){\n      const cv=d2.$('canvas.p');if(!cv)return;\n      const dpr=window.devicePixelRatio||1,W=cv.clientWidth,H=cv.clientHeight;\n      if(!W||!H)return;\n      if(cv.width!==Math.round(W*dpr)||cv.height!==Math.round(H*dpr)){cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr);}\n      const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);ctx.imageSmoothingEnabled=false;ctx.clearRect(0,0,W,H);\n      const sk=SKINPIX[SKINID];\n      if(!sk||!sk.geo)return;\n      const acoes=Object.keys(sk.geo);\n      let acao=String(d2.param('action','stand1')||'stand1');\n      if(acoes.indexOf(acao)<0)acao=acoes[0];\n      const tocando=d2.param('play',false)===true;\n      if(acao!==acaoAntes||(tocando&&!playAntes)){t0=Date.now();TEMPO=0;}\n      acaoAntes=acao;playAntes=tocando;\n      let frame;\n      if(tocando){\n        frame=frameEm(acao,keys(sk.geo,acao).length||1,Date.now()-t0);\n        TEMPO=Date.now()-t0;\n      }else{\n        frame=Math.round(Number(d2.param('frame',0))||0);\n        // ItemEff tem ciclo proprio (delay por frame), SEM relacao com o numero de frames do\n        // corpo -- amarrar um ao outro seria inventar um vinculo que o dado nao tem. Com o Play\n        // desligado ele fica no inicio do ciclo, o unico valor deterministico possivel.\n        TEMPO=0;\n      }\n      const CX=Math.round(W/2),CY=Math.round(H*EIXO_Y);\n      // FLIP: espelha a COMPOSICAO INTEIRA em torno do eixo vertical do personagem (CX), nao\n      // sprite por sprite -- assim ancoras, vslot e ItemEff viram juntos e de graca. Os sprites\n      // do jogo olham para a ESQUERDA por padrao, entao flip:true = virado para a direita.\n      if(d2.param('flip',false)===true){ctx.translate(CX*2,0);ctx.scale(-1,1);}\n      const L=compor(sk,EQUIP,acao,frame,d2.param('weaponFx',false)===true,d2.param('itemFx',true)!==false).L;\n      for(const c of L){\n        if(c.base!==undefined){\n          const im=imgBase(sk,c.base);if(!im||!im.complete||!im.naturalWidth)continue;\n          ctx.drawImage(im,CX+c.x*ZOOM,CY+c.y*ZOOM,im.naturalWidth*ZOOM,im.naturalHeight*ZOOM);\n        }else{\n          const at=img(c.ref);if(!at||!at.complete||!at.naturalWidth)continue;\n          const u=c.uv[0],v=c.uv[1],w=c.uv[2],h=c.uv[3];\n          ctx.drawImage(at,u,v,w,h,CX+c.x*ZOOM,CY+c.y*ZOOM,w*ZOOM,h*ZOOM);\n        }\n      }\n    }\n\n    let raf=null;\n    function laco(){\n      raf=requestAnimationFrame(laco);\n      if(assinatura!==null&&!ocupado&&assina()!==assinatura)sincroniza();\n      desenha();\n    }\n    raf=requestAnimationFrame(laco);\n    d2.onUnmount(()=>{if(raf)cancelAnimationFrame(raf);});\n  }\n};\n"}},"skin":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"skin"}}}},"hair":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"hair"}}}},"face":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"face"}}}},"cap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cap"}}}},"cape":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"cape"}}}},"coat":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"coat"}}}},"pants":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"pants"}}}},"outfit":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"outfit"}}}},"shoes":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"shoes"}}}},"glove":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"glove"}}}},"weapon":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"weapon"}}}},"offhand":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"offhand"}}}},"accface":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accface"}}}},"acceye":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"acceye"}}}},"accear":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accear"}}}},"action":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"action"}}}},"frame":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"frame"}}}},"play":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"play"}}}},"flip":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"flip"}}}},"weaponFx":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"weaponFx"}}}},"itemFx":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"itemFx"}}}},"slotRules":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"slotRules"}}}}}}}}}},{"_id":"6a87504d45ad40122b0e416d","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mcinput","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// mcinput -- o teclado do teste de movimento. Nao desenha nada e nao toca em DOM alheio:\n// le setas / X / Z e escreve x, y, action, play e flip no Store `mcplay`. O bloco Character\n// (a hitbox) tem position.x/y amarrados a @mcplay.data.x/y, e o renderizador tem\n// action/play/flip amarrados -- quem propaga e o runtime. Esse desvio pelo Store e de\n// proposito: e o caminho que a KB indica para dois blocos ficarem em sincronia, e evita que\n// um componente va mexer no DOM de outro.\nconst REF='mcplay';\nconst VEL=2.6;        // px por quadro (~156 px/s a 60fps)\nconst GRAV=0.55;      // px por quadro^2\nconst IMPULSO=9.2;    // velocidade inicial do pulo -> pico ~77px, ~0,55s no ar\nconst DUR_SWING=624;  // swingO1 = 3 frames x 208ms (PASSO do engine), acao sem vaivem\nconst CHAO=483;       // y do chao: onde o Character nasceu no artboard\nconst LIM_MIN=0, LIM_MAX=949;  // 1000 (largura do artboard) - 51 (largura da hitbox)\n\nexport default {\n  // Sem UI nenhuma. pointer-events:none para nao roubar clique do que estiver embaixo.\n  css:`.k{position:absolute;inset:0;pointer-events:none}`,\n  render(){return `<div class=\"k\"></div>`;},\n\n  mounted(d2){\n    const st=d2.store(REF);\n    if(!st){d2.log('[mcinput] Store '+REF+' nao encontrado');return;}\n\n    const teclas=Object.create(null);\n    let x=475, y=CHAO, vy=0, noAr=false, swingAte=0, virado=false;\n    const ultimo=Object.create(null);\n\n    // O Store expoe DUAS convencoes de caminho e misturar as duas no-opa EM SILENCIO: o\n    // Getter do bloco le `data.x`, mas os metodos get/set do modulo prependem `data.` por\n    // conta propria. A KB nao diz qual delas o bridge d2.store() usa, entao em vez de chutar\n    // eu sondo: qual dos dois caminhos devolve o initialData (x=300)? Feito no laco, e nao no\n    // mount, porque o processo do Store pode nao estar pronto no primeiro instante.\n    let PRE=null, tentativas=0;\n    function preparar(){\n      const a=st.get('x'), b=st.get('data.x');\n      if(a!==undefined&&a!==null)PRE='';\n      else if(b!==undefined&&b!==null)PRE='data.';\n      else if(++tentativas>120){PRE='';d2.log('[mcinput] Store nao devolveu x em ~2s; assumindo caminho sem prefixo');}\n      else return false;\n      const inicial=(PRE===''?a:b);\n      if(inicial!=null&&isFinite(Number(inicial)))x=Number(inicial);\n      d2.log('[mcinput] caminho do Store = '+JSON.stringify(PRE+'x')+\n             '  (get(\"x\")='+JSON.stringify(a)+', get(\"data.x\")='+JSON.stringify(b)+')');\n      return true;\n    }\n    // Escreve so quando o valor MUDA. Sem isto seriam 5 writes x 60fps = 300 writes/s no\n    // Store, e cada write acorda a reatividade dos blocos amarrados.\n    function grava(k,v){if(ultimo[k]!==v){ultimo[k]=v;st.set(PRE+k,v);}}\n\n    const ehNossa=k=>k==='ArrowLeft'||k==='ArrowRight'||k==='x'||k==='X'||k==='z'||k==='Z';\n\n    d2.on(window,'keydown',e=>{\n      if(!ehNossa(e.key))return;\n      e.preventDefault();                       // seta nao rola a pagina\n      if(teclas[e.key])return;                  // ignora o auto-repeat do teclado\n      teclas[e.key]=1;\n      if((e.key==='x'||e.key==='X')&&!noAr){noAr=true;vy=-IMPULSO;}\n      if(e.key==='z'||e.key==='Z')swingAte=Date.now()+DUR_SWING;\n    });\n    d2.on(window,'keyup',e=>{delete teclas[e.key];});\n    // Sem isto, perder o foco da janela com a seta apertada deixa o personagem andando para\n    // sempre: o keyup acontece fora da pagina e nunca chega.\n    d2.on(window,'blur',()=>{for(const k in teclas)delete teclas[k];});\n\n    function passo(){\n      if(PRE===null&&!preparar())return;\n      const esq=!!teclas.ArrowLeft, dir=!!teclas.ArrowRight;\n      if(esq!==dir){                            // as duas ao mesmo tempo se anulam\n        x+=dir?VEL:-VEL;\n        if(x<LIM_MIN)x=LIM_MIN;\n        if(x>LIM_MAX)x=LIM_MAX;\n        virado=dir;                             // os sprites do jogo olham para a ESQUERDA\n      }\n      if(noAr){\n        vy+=GRAV;y+=vy;\n        if(y>=CHAO){y=CHAO;vy=0;noAr=false;}\n      }\n      // PRIORIDADE DA ACAO: swing > pulo > andar > parado. O x continua avancando nos tres\n      // primeiros -- exclusiva e so a acao DESENHADA, nao o movimento.\n      let acao,toca;\n      if(Date.now()<swingAte){acao='swingO1';toca=true;}\n      else if(noAr){acao='jump';toca=false;}    // jump tem 1 frame: nao ha o que animar\n      else if(esq!==dir){acao='walk1';toca=true;}\n      else{acao='stand1';toca=false;}\n      grava('x',Math.round(x));\n      grava('y',Math.round(y));\n      grava('flip',virado);\n      grava('action',acao);\n      grava('play',toca);\n    }\n\n    let raf=null;\n    function laco(){raf=requestAnimationFrame(laco);passo();}\n    raf=requestAnimationFrame(laco);\n    d2.onUnmount(()=>{if(raf)cancelAnimationFrame(raf);});\n  }\n};\n"}}}}}}}},{"_id":"6a876b990769d9c7b6f898ff","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"idleitemeditor","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Idle Item Editor -- CRUD da base de itens do idle, com o CATALOGO ao lado.\n// A regra que desenha a tela: mapleId e name NAO sao digitaveis -- so entram clicando numa\n// thumb do catalogo, porque um item do idle que nao exista no catalogo nunca renderizaria.\n// category e type sao escolhidos A MAO: category e um enum fechado, type vem da tabela\n// `idle-type` (mantida no Item Type Editor). Nada e inferido do item id -- um palpite errado\n// ali se propagaria em silencio por toda a base.\nconst CATS=['weapon','cap','coat','pants','outfit','shoes','glove','cape','offhand','hair','face','accface','acceye','accear'];\nconst ROTULO={accface:'face acc',acceye:'eye acc',accear:'earring',offhand:'off-hand'};\nconst MEDIA='https://bigaton-playground.data2.media/file/';\nconst POR_PAGINA=36;\nconst NUMEROS=[['itemLevel','item lv'],['reqLevel','req lv'],['att','att'],['def','def'],\n               ['acc','acc'],['hp','hp'],['mp','mp'],['str','str'],['dex','dex'],['int','int'],['luk','luk']];\nconst CATEGORIAS=['weapon','armor','accessory','character'];\n\nconst INDEXREF=Object.create(null);\nconst IDX=Object.create(null);\n// mapleId -> recorte da folha de icones. Preenchido quando um idx e carregado, e o que\n// permite desenhar a thumb de um item da base sem reabrir a categoria no catalogo.\nconst THUMB=Object.create(null);\n\nfunction esc(s){return String(s==null?'':s).replace(/[<>&\"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;'}[c]));}\nfunction num(v){if(v===''||v==null)return 0;const n=Number(v);return isFinite(n)?n:0;}\n\nexport default {\n  css:`\n  .wrap{height:100%;display:grid;grid-template-columns:290px 1fr 420px;background:#101014;color:#e8e8ee;color-scheme:dark;\n        font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}\n  .col{display:grid;grid-template-rows:auto 1fr;overflow:hidden;min-width:0}\n  .col+.col{border-left:1px solid #282833}\n  .cab{display:flex;align-items:center;gap:8px;padding:10px 13px;border-bottom:1px solid #282833;\n       background:linear-gradient(180deg,#1b1b23,#15151b);min-height:42px;box-sizing:border-box}\n  .cab h2{margin:0;font-size:10px;text-transform:uppercase;letter-spacing:.9px;color:#83838f;font-weight:700}\n  .cab .dir{margin-left:auto;display:flex;gap:6px;align-items:center}\n  .corpo{overflow:auto;padding:11px 13px}\n  .corpo::-webkit-scrollbar{width:9px}\n  .corpo::-webkit-scrollbar-thumb{background:#33333f;border-radius:9px;border:2px solid #101014}\n  .badge{font:11px ui-monospace,monospace;color:#83838f}\n  .badge.ok{color:#6ee7a0}.badge.bad{color:#ff8080}.badge.warn{color:#e3b341}\n\n  button,input,select{font:600 12px ui-sans-serif,system-ui,sans-serif;padding:6px 9px;border-radius:7px;\n    border:1px solid #34343f;background:linear-gradient(180deg,#2a2a34,#232430);color:#e8e8ee;\n    transition:border-color .12s,box-shadow .12s}\n  button{cursor:pointer}\n  button:hover:not(:disabled){border-color:#4a4a5a}\n  button:disabled{opacity:.4;cursor:default}\n  button.go{background:linear-gradient(180deg,#6c8cff,#5474ea);border-color:#6c8cff;color:#fff}\n  button.go:hover:not(:disabled){background:linear-gradient(180deg,#7b98ff,#5f7ef0)}\n  button.del{border-color:#54282c;color:#ff9d9d}\n  button.del:hover:not(:disabled){border-color:#7a3a40;background:#2a1e21}\n  button.del.armado{background:linear-gradient(180deg,#8d3138,#6f242a);border-color:#b0454d;color:#fff}\n  input,select{width:100%;box-sizing:border-box;font-weight:400}\n  input:focus,select:focus{outline:none;border-color:#6c8cff;box-shadow:0 0 0 3px #6c8cff2e}\n  input[readonly]{background:#1b1b22;color:#a9a9b6;border-style:dashed;cursor:not-allowed}\n  /* O popup de um select e desenhado pelo SO, nao por este CSS: sem color-scheme dark ele vem\n     branco e as options ficam ilegiveis. color-scheme resolve no Chrome/Edge/Firefox modernos;\n     o background/color explicito na option cobre o resto. */\n  select{color-scheme:dark}\n  select option,select optgroup{background:#1e1e26;color:#e8e8ee}\n  select option:checked{background:#2f3a63;color:#fff}\n  input[type=checkbox]{accent-color:#6c8cff}\n\n  .it{display:flex;align-items:center;gap:9px;width:100%;text-align:left;background:#1c1c24;\n      border:1px solid #2a2a35;border-radius:8px;padding:6px 9px;margin-bottom:5px;cursor:pointer}\n  .itxt{flex:1;min-width:0}\n  .thbox{flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden;\n         border-radius:7px;background:linear-gradient(180deg,#fafafa,#e6e6ea);box-shadow:0 1px 3px #0006}\n  .thsm{width:42px;height:42px}\n  .thlg{width:132px;height:132px;border-radius:10px}\n  .thi{image-rendering:pixelated;transform-origin:center}\n  .semth{color:#9a9aa6;font:11px ui-monospace,monospace}\n  .ident{display:flex;gap:12px;align-items:flex-start}\n  .it:hover{border-color:#4a4a5a}\n  .it.on{border-color:#6c8cff;box-shadow:0 0 0 2px #6c8cff33}\n  .it b{display:block;font-size:12px;font-weight:650;color:#e8e8ee;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .it code{font:10px ui-monospace,monospace;color:#6c8cff}\n  .it .st{font:10px ui-monospace,monospace;color:#7a7a86;margin-top:2px;display:block}\n  .vazio{color:#5e5e6b;font-size:11px;line-height:1.6}\n\n  .grp{margin-bottom:15px}\n  h3{margin:0 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:.9px;color:#83838f;\n     display:flex;align-items:center;gap:8px}\n  h3:before{content:\"\";width:3px;height:11px;border-radius:2px;background:#6c8cff;opacity:.85;flex:none}\n  h3 em{font-style:normal}\n  .f{display:block;min-width:0}\n  .f>span{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#7a7a86;margin-bottom:3px}\n  .duo{display:grid;grid-template-columns:1fr 1fr;gap:9px}\n  .trio{display:grid;grid-template-columns:1fr 1.4fr 1fr;gap:9px}\n  .stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:8px}\n  .trava{font:10px ui-monospace,monospace;color:#5e5e6b;margin-top:5px}\n  .chk{display:flex;gap:7px;align-items:center;margin-top:10px;cursor:pointer;user-select:none;color:#b9b9c4}\n  .chk input{width:auto}\n  .chk span{font-size:11px;text-transform:uppercase;letter-spacing:.5px}\n  .acoes{display:flex;gap:8px;align-items:center;margin-top:4px}\n\n  .tabs{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:9px}\n  .tabs button{padding:3px 8px;font-size:10px;text-transform:capitalize;border-radius:5px}\n  .tabs button.on{background:linear-gradient(180deg,#6c8cff,#5474ea);border-color:#6c8cff;color:#fff}\n  .grade{display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-top:9px}\n  .cel{background:linear-gradient(180deg,#fafafa,#e6e6ea);border:2px solid transparent;border-radius:8px;\n    height:52px;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden;\n    padding:0;position:relative;box-shadow:0 1px 3px #0006}\n  .cel:hover{border-color:#8aa4ff}\n  .cel.on{border-color:#6ee7a0;box-shadow:0 0 0 3px #6ee7a040}\n  .cel.jaTem{opacity:.42}\n  .cel .thc{image-rendering:pixelated}\n  .semic{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;\n    border:1px dashed #b7b7c2;border-radius:5px;color:#7a7a86;font:10px ui-monospace,monospace}\n  .pag{display:flex;gap:7px;align-items:center;margin-top:10px}\n  .pag button{padding:4px 10px}\n  `,\n\n  render(d2){\n    const s=d2.state, r=s.rascunhoView||{};\n    // Sem o recorte carregado a caixa fica vazia em vez de sumir -- o layout nao pula quando\n    // o indice daquela categoria termina de carregar em segundo plano.\n    const thumb=(id,grande)=>{\n      const t=id?THUMB[id]:null;\n      const box=grande?'thlg':'thsm';\n      if(!t)return `<span class=\"thbox ${box}\"><span class=\"semth\">${grande?'sem thumb':'—'}</span></span>`;\n      return `<span class=\"thbox ${box}\"><span class=\"thi\" style=\"width:${t.w}px;height:${t.h}px;`+\n             `background-image:url(${esc(t.sheet)});background-position:-${t.x}px -${t.y}px;`+\n             `transform:scale(${grande?2.6:1})\"></span></span>`;\n    };\n    const di=d2.data('itens'), dc=d2.data('classes'), dt=d2.data('tipos');\n    const itens=di?di.records():[];\n    const cls=dc?dc.records().slice().sort((a,b)=>(a.order||0)-(b.order||0)):[];\n    const tipos=dt?dt.records():[];\n    const tiposVis=(r.category?tipos.filter(t=>t.category===r.category):tipos)\n      .slice().sort((a,b)=>String(a.name).localeCompare(String(b.name)));\n\n    const qi=(s.qItem||'').trim().toLowerCase();\n    const lista=qi?itens.filter(i=>((i.mapleId||'')+' '+(i.name||'')+' '+(i.type||'')+' '+(i.category||'')).toLowerCase().indexOf(qi)>=0):itens;\n    const linhas=lista.map(i=>`<button class=\"it ${r._id===i._id?'on':''}\" data-id=\"${esc(i._id)}\" data-key=\"${esc(i._id)}\">\n        <span class=\"itxt\">\n          <b>${esc(i.name||'(sem nome)')}</b>\n          <code>${esc(i.mapleId||'')}</code> <span class=\"badge\">${esc(i.category||'—')} / ${esc(i.type||'—')} · ${esc(i.class||'qualquer')}${i.cash?' · <b style=\"color:#e3b341\">cash</b>':''}</span>\n          <span class=\"st\">lv ${i.itemLevel||0} · req ${i.reqLevel||0} · att ${i.att||0} · def ${i.def||0}</span>\n        </span>\n        ${thumb(i.mapleId,false)}\n      </button>`).join('');\n\n    const ix=IDX[s.cat];\n    const q=(s.q||'').trim().toLowerCase();\n    const base=ix?ix.items:[];\n    const filt=q?base.filter(x=>(x.busca||'').indexOf(q)>=0):base;\n    const paginas=Math.max(1,Math.ceil(filt.length/POR_PAGINA));\n    const pag=Math.min(s.pag||0,paginas-1);\n    const fatia=filt.slice(pag*POR_PAGINA,pag*POR_PAGINA+POR_PAGINA);\n    const jaTem=Object.create(null);\n    for(const i of itens)jaTem[i.mapleId]=1;\n    const celulas=fatia.map(x=>{\n      let mini=`<span class=\"semic\">${esc(String(x.id).slice(-4))}</span>`;\n      if(x.ic&&ix&&ix.sheets&&ix.sheets.length){\n        const sh=x.ic[0],px=x.ic[1],py=x.ic[2],w=x.ic[3],h=x.ic[4];\n        mini=`<span class=\"thc\" style=\"display:inline-block;width:${w}px;height:${h}px;background-image:url(${esc(ix.sheets[sh])});background-position:-${px}px -${py}px\"></span>`;\n      }\n      return `<button class=\"cel ${r.mapleId===x.id?'on':''} ${jaTem[x.id]?'jaTem':''}\" data-key=\"${esc(x.id)}\"\n        data-id=\"${esc(x.id)}\" data-nome=\"${esc(x.name||'')}\"\n        title=\"${esc((x.name||'')+' · '+x.id+(jaTem[x.id]?' · JA esta na base':''))}\">${mini}</button>`;\n    }).join('');\n\n    const temSel=!!r.mapleId;\n    return `<div class=\"wrap\">\n      <div class=\"col\">\n        <div class=\"cab\"><h2>base do idle</h2><span class=\"dir\">\n          <span class=\"badge\">${itens.length}</span>\n          <button class=\"novo\">+ novo</button>\n        </span></div>\n        <div class=\"corpo\">\n          <input class=\"qitem\" placeholder=\"filtrar por id, nome, tipo ou categoria…\" value=\"${esc(s.qItem||'')}\"/>\n          <div style=\"margin-top:9px\">${linhas||'<div class=\"vazio\">Nenhum item na base ainda.<br>Clique numa thumb do catalogo &rarr; preenche mapleId e nome &rarr; escolha category/type &rarr; complete os stats &rarr; salvar.</div>'}</div>\n        </div>\n      </div>\n\n      <div class=\"col\">\n        <div class=\"cab\"><h2>${r._id?'editando item':'novo item'}</h2><span class=\"dir\">\n          ${s.msg?`<span class=\"badge ${s.msgTipo||''}\">${esc(s.msg)}</span>`:''}\n        </span></div>\n        <div class=\"corpo\">\n          <div class=\"grp\">\n            <h3>identidade <em class=\"badge\">vem do catalogo</em></h3>\n            <div class=\"ident\">\n              ${thumb(r.mapleId,true)}\n              <div style=\"flex:1;min-width:0\">\n                <div class=\"duo\">\n                  <label class=\"f\"><span>mapleId</span><input readonly value=\"${esc(r.mapleId||'')}\" placeholder=\"clique no catalogo &rarr;\"/></label>\n                  <label class=\"f\"><span>name</span><input readonly value=\"${esc(r.name||'')}\" placeholder=\"—\"/></label>\n                </div>\n                <div class=\"trava\">Somente leitura de proposito: um item que nao existe no catalogo nunca renderizaria.</div>\n              </div>\n            </div>\n          </div>\n          <div class=\"grp\">\n            <h3>classificacao</h3>\n            <div class=\"trio\">\n              <label class=\"f\"><span>category</span><select class=\"cat\">\n                <option value=\"\">—</option>\n                ${CATEGORIAS.map(c=>`<option value=\"${esc(c)}\" ${r.category===c?'selected':''}>${esc(c)}</option>`).join('')}\n              </select></label>\n              <label class=\"f\"><span>type</span><select class=\"tipo\">\n                <option value=\"\">${r.category?(tiposVis.length?'—':'nenhum type em '+esc(r.category)):'escolha a category'}</option>\n                ${tiposVis.map(t=>`<option value=\"${esc(t.key)}\" ${r.type===t.key?'selected':''}>${esc(t.name)}</option>`).join('')}\n              </select></label>\n              <label class=\"f\"><span>class</span><select class=\"cls\">\n                <option value=\"\">(qualquer)</option>\n                ${cls.map(c=>`<option value=\"${esc(c.key)}\" ${r.class===c.key?'selected':''}>${esc(c.name)}</option>`).join('')}\n              </select></label>\n            </div>\n            <label class=\"chk\"><input type=\"checkbox\" class=\"cash\" ${r.cash?'checked':''}/> <span>item de cash</span></label>\n          </div>\n          <div class=\"grp\">\n            <h3>atributos</h3>\n            <div class=\"stats\">\n              ${NUMEROS.map(f=>`<label class=\"f\"><span>${esc(f[1])}</span><input class=\"n\" data-k=\"${esc(f[0])}\" type=\"number\" value=\"${r[f[0]]==null?0:esc(r[f[0]])}\"/></label>`).join('')}\n            </div>\n          </div>\n          <div class=\"acoes\">\n            <button class=\"go salvar\" ${!temSel||s.salvando?'disabled':''}>${s.salvando?'salvando…':(r._id?'salvar alteracoes':'criar item')}</button>\n            <button class=\"del excluir ${s.armado?'armado':''}\" ${!r._id||s.salvando?'disabled':''}>${s.armado?'confirmar exclusao':'excluir'}</button>\n            ${s.armado?'<button class=\"cancelaDel\">cancelar</button>':`<button class=\"limpar\" ${!temSel?'disabled':''}>limpar</button>`}\n          </div>\n        </div>\n      </div>\n\n      <div class=\"col\">\n        <div class=\"cab\"><h2>catalogo</h2><span class=\"dir\">\n          ${s.carregando?'<span class=\"badge warn\">carregando…</span>':''}\n          ${s.erroCat?`<span class=\"badge bad\">${esc(s.erroCat)}</span>`:`<span class=\"badge\">${filt.length}${q?' achados':''}</span>`}\n        </span></div>\n        <div class=\"corpo\">\n          <div class=\"tabs\">${CATS.map(c=>`<button class=\"tab ${c===s.cat?'on':''}\" data-c=\"${c}\">${ROTULO[c]||c}</button>`).join('')}</div>\n          <input class=\"qcat\" placeholder=\"buscar por nome ou id…\" value=\"${esc(s.q||'')}\"/>\n          ${s.pronto?`<div class=\"grade\">${celulas}</div>\n          <div class=\"pag\"><button class=\"pp\" ${pag<=0?'disabled':''}>&lsaquo;</button>\n            <span class=\"badge\">${pag+1} / ${paginas}</span>\n            <button class=\"np\" ${pag>=paginas-1?'disabled':''}>&rsaquo;</button></div>`\n          :'<div class=\"vazio\" style=\"margin-top:12px\">carregando o catalogo…</div>'}\n        </div>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2){\n    const s=d2.state;\n    s.cat='weapon';s.q='';s.pag=0;s.qItem='';s.msg='';s.msgTipo='';s.salvando=false;\n    s.pronto=false;s.carregando=false;s.erroCat='';s.armado=false;\n\n    // O rascunho vive em CLOSURE, nao em d2.state: digitar num campo de stat nao deve\n    // re-renderizar a grade de 36 thumbs. `rascunhoView` e a copia que o render le,\n    // atualizada so nos momentos estruturais via espelha() + d2.refresh().\n    let rascunho=vazio();\n    function vazio(){\n      const o={_id:null,mapleId:'',name:'',mapleSlot:'',category:'',type:'',class:'',cash:false};\n      for(const f of NUMEROS)o[f[0]]=0;\n      return o;\n    }\n    function espelha(){s.rascunhoView=Object.assign({},rascunho);}\n    espelha();\n\n    async function jsonDe(ref){const r=await fetch(MEDIA+ref);if(!r.ok)throw new Error('fetch '+ref+' -> '+r.status);return r.json();}\n\n    async function garanteIdx(cat){\n      if(IDX[cat]||!INDEXREF[cat])return;\n      const d=await jsonDe(INDEXREF[cat]);\n      IDX[cat]={sheets:(d.iconSheets||[]).map(r=>MEDIA+r),items:d.items||[]};\n      // Indexa os recortes por id. As variantes de cor compartilham o icone do estilo.\n      for(const it of IDX[cat].items){\n        if(!it.ic)continue;\n        const rec={sheet:IDX[cat].sheets[it.ic[0]],x:it.ic[1],y:it.ic[2],w:it.ic[3],h:it.ic[4]};\n        THUMB[it.id]=rec;\n        if(it.cores)for(const cc of it.cores)if(!THUMB[cc[0]])THUMB[cc[0]]=rec;\n      }\n    }\n    // Traz os indices das categorias que a base ja usa, para as thumbs da lista aparecerem.\n    // Roda FORA do caminho critico de proposito: cada categoria custa o idx + a folha de\n    // icones (PNG de 0,2 a 2,6 MB), entao a lista preenche progressivamente.\n    async function carregaIdxDaBase(){\n      const ds=d2.data('itens');if(!ds)return;\n      const alvos={};\n      for(const rec of ds.records())if(rec.mapleSlot&&!IDX[rec.mapleSlot])alvos[rec.mapleSlot]=1;\n      for(const c in alvos){\n        try{await garanteIdx(c);d2.refresh();}catch(err){d2.log('[idleitemeditor] idx '+c+': '+((err&&err.message)||err));}\n      }\n    }\n    function aviso(m,t){s.msg=m;s.msgTipo=t||'';}\n\n    (async()=>{\n      const di=d2.data('itens'), dc=d2.data('classes'), dt=d2.data('tipos');\n      try{\n        // refresh ANTES do loadAll: em mounted() o handle costuma estar no meio do auto-fetch,\n        // e loadAll() no-opa em silencio se o DataSet nao estiver Ready.\n        const ps=[];if(di)ps.push(di.refresh());if(dc)ps.push(dc.refresh());if(dt)ps.push(dt.refresh());\n        await Promise.all(ps);\n        if(di)await di.loadAll();\n        if(dc)await dc.loadAll();\n        if(dt)await dt.loadAll();\n      }catch(err){aviso('dataset: '+((err&&err.message)||err),'bad');}\n      try{\n        s.carregando=true;\n        const mi=await d2.rush('mcindexall').invoke({});\n        for(const r of ((mi&&mi.data&&mi.data.rows)||[]))INDEXREF[r.cat]=r.ref;\n        await garanteIdx(s.cat);\n        s.pronto=true;\n      }catch(err){s.erroCat=(err&&err.message)?err.message:String(err);}\n      finally{s.carregando=false;}\n      carregaIdxDaBase();\n    })();\n\n    d2.on('.tab','click',async(e,el)=>{\n      s.cat=el.getAttribute('data-c');s.q='';s.pag=0;s.carregando=true;\n      try{await garanteIdx(s.cat);}catch(err){s.erroCat=(err&&err.message)||String(err);}\n      finally{s.carregando=false;}\n    });\n    d2.on('.qcat','input',(e,el)=>{s.q=el.value;s.pag=0;});\n    d2.on('.pp','click',()=>{s.pag=Math.max(0,(s.pag||0)-1);});\n    d2.on('.np','click',()=>{s.pag=(s.pag||0)+1;});\n\n    // A UNICA porta de entrada de mapleId/name. category e type ficam por sua conta.\n    d2.on('.cel','click',(e,el)=>{\n      rascunho.mapleId=el.getAttribute('data-id');\n      rascunho.name=el.getAttribute('data-nome');\n      // Guarda de qual categoria do catalogo a peca veio. Nao e classificacao de jogo -- e o\n      // ponteiro para saber qual indice abrir depois para desenhar a thumb dela.\n      rascunho.mapleSlot=s.cat;\n      espelha();aviso('');d2.refresh();\n    });\n\n    d2.on('.qitem','input',(e,el)=>{s.qItem=el.value;});\n    d2.on('.it','click',async(e,el)=>{\n      const id=el.getAttribute('data-id');\n      const ds=d2.data('itens');const rec=(ds?ds.records():[]).filter(r=>String(r._id)===String(id))[0];\n      if(!rec)return;\n      rascunho=vazio();\n      rascunho._id=rec._id;rascunho.mapleId=rec.mapleId||'';rascunho.name=rec.name||'';\n      rascunho.category=rec.category||'';rascunho.type=rec.type||'';rascunho.class=rec.class||'';\n      rascunho.cash=rec.cash===true;rascunho.mapleSlot=rec.mapleSlot||'';\n      for(const f of NUMEROS)rascunho[f[0]]=rec[f[0]]==null?0:rec[f[0]];\n      espelha();aviso('');s.armado=false;d2.refresh();\n    });\n\n    // Trocar a category re-filtra a lista de types, entao aqui precisa re-renderizar. Se o\n    // type atual nao pertence mais a category escolhida, ele e limpo em vez de ficar invalido.\n    d2.on('.cat','change',(e,el)=>{\n      rascunho.category=el.value;\n      const dt=d2.data('tipos');\n      const linha=(dt?dt.records():[]).filter(t=>t.key===rascunho.type)[0];\n      if(linha&&rascunho.category&&linha.category!==rascunho.category)rascunho.type='';\n      espelha();d2.refresh();\n    });\n    d2.on('.tipo','change',(e,el)=>{rascunho.type=el.value;espelha();d2.refresh();});\n    d2.on('.cls','change',(e,el)=>{rascunho.class=el.value;});\n    d2.on('.cash','change',(e,el)=>{rascunho.cash=el.checked;});\n    d2.on('.n','input',(e,el)=>{rascunho[el.getAttribute('data-k')]=num(el.value);});\n\n    d2.on('.limpar','click',()=>{rascunho=vazio();espelha();aviso('');s.armado=false;d2.refresh();});\n    d2.on('.novo','click',()=>{rascunho=vazio();espelha();aviso('');s.armado=false;d2.refresh();});\n\n    d2.on('.salvar','click',async()=>{\n      const ds=d2.data('itens');\n      if(!ds){aviso('dataset de itens nao encontrado','bad');return;}\n      if(!rascunho.mapleId){aviso('escolha um item no catalogo primeiro','warn');return;}\n      // Um mapleId = um item do idle. Sem isso da para cadastrar o mesmo item duas vezes com\n      // stats diferentes, e nada acusaria.\n      if(!rascunho._id){\n        const dup=ds.records().filter(r=>r.mapleId===rascunho.mapleId)[0];\n        if(dup){aviso(rascunho.mapleId+' ja esta na base — selecione na lista para editar','warn');return;}\n      }\n      s.salvando=true;aviso('');s.armado=false;\n      try{\n        const dados={mapleId:rascunho.mapleId,name:rascunho.name,category:rascunho.category||'',\n                     type:rascunho.type||'',class:rascunho.class||'',cash:rascunho.cash===true,\n                     mapleSlot:rascunho.mapleSlot||''};\n        for(const f of NUMEROS)dados[f[0]]=num(rascunho[f[0]]);\n        if(rascunho._id)await ds.update(Object.assign({_id:rascunho._id},dados));\n        else await ds.create(dados);\n        await ds.refresh();await ds.loadAll();\n        if(!rascunho._id){\n          const novo=ds.records().filter(r=>r.mapleId===dados.mapleId)[0];\n          if(novo)rascunho._id=novo._id;\n        }\n        espelha();aviso('salvo','ok');carregaIdxDaBase();\n      }catch(err){aviso('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvando=false;}\n    });\n\n    // Confirmacao de dois toques DENTRO do componente. NAO uso d2.dialog.confirm: medido no\n    // preview, ele resolve sem desenhar nada, e o `if(!ok)return` matava o excluir em silencio.\n    d2.on('.cancelaDel','click',()=>{s.armado=false;aviso('');});\n    d2.on('.excluir','click',async()=>{\n      if(!rascunho._id)return;\n      if(!s.armado){s.armado=true;aviso('clique de novo para confirmar','warn');return;}\n      s.armado=false;\n      const ds=d2.data('itens');\n      s.salvando=true;\n      try{\n        await ds.remove(rascunho._id);\n        await ds.refresh();await ds.loadAll();\n        rascunho=vazio();espelha();aviso('excluido','ok');\n      }catch(err){aviso('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvando=false;}\n    });\n  }\n};\n"}},"itens":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"itens"}}}},"classes":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"classes"}}}},"tipos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tipos"}}}}}}}}}},{"_id":"6a8777134e4dcb98a1fee898","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"idletypeeditor","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Idle Item Type Editor -- CRUD dos tipos de item, em dois paineis.\n// WEAPON carrega a pool de actions: stand, walk e a lista de attacks. E isso que vai ditar\n// quais animacoes o personagem executa com aquele tipo de arma -- o ataque sorteia um item\n// dessa pool. ARMOR/ACCESSORY/CHARACTER e so category + type.\n// Nada aqui e inferido de item id: os tipos sao criados a mao, de proposito.\nconst NAO_ARMA=['armor','accessory','character'];\nconst STANDS=['stand1','stand2'];\nconst WALKS=['walk1','walk2'];\n// Todas as actions de ataque do engine (11 swings + 6 stabs + 3 shoots), na ordem do WZ.\n// Conferidas contra as 33 actions do arquivo de skin -- nao ha outras familias de ataque.\nconst ATAQUES=['swingO1','swingO2','swingO3','swingOF','swingT1','swingT2','swingT3','swingTF',\n               'swingP1','swingP2','swingPF','stabO1','stabO2','stabOF','stabT1','stabT2','stabTF',\n               'shoot1','shoot2','shootF'];\nconst STANCES=['1handed','2handed'];\n\nfunction esc(s){return String(s==null?'':s).replace(/[<>&\"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;'}[c]));}\nfunction slug(s){return String(s||'').toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');}\nfunction arr(v){return Array.isArray(v)?v.slice():[];}\n\nexport default {\n  css:`\n  .wrap{height:100%;display:grid;grid-template-columns:1fr 1fr;background:#101014;color:#e8e8ee;color-scheme:dark;\n        font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}\n  .pan{display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;min-width:0}\n  .pan+.pan{border-left:1px solid #282833}\n  .cab{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid #282833;\n       background:linear-gradient(180deg,#1b1b23,#15151b)}\n  .cab h2{margin:0;font-size:10px;text-transform:uppercase;letter-spacing:.9px;color:#83838f;font-weight:700}\n  .cab .dir{margin-left:auto;display:flex;gap:6px;align-items:center}\n  .lista{overflow:auto;padding:10px 14px}\n  .form{border-top:1px solid #282833;background:#17171d;padding:11px 14px;overflow:auto}\n  .lista::-webkit-scrollbar,.form::-webkit-scrollbar{width:9px}\n  .lista::-webkit-scrollbar-thumb,.form::-webkit-scrollbar-thumb{background:#33333f;border-radius:9px;border:2px solid #101014}\n  .badge{font:11px ui-monospace,monospace;color:#83838f}\n  .badge.ok{color:#6ee7a0}.badge.bad{color:#ff8080}.badge.warn{color:#e3b341}\n\n  button,input,select{font:600 12px ui-sans-serif,system-ui,sans-serif;padding:6px 9px;border-radius:7px;\n    border:1px solid #34343f;background:linear-gradient(180deg,#2a2a34,#232430);color:#e8e8ee;\n    transition:border-color .12s,box-shadow .12s}\n  button{cursor:pointer}\n  button:hover:not(:disabled){border-color:#4a4a5a}\n  button:disabled{opacity:.4;cursor:default}\n  button.go{background:linear-gradient(180deg,#6c8cff,#5474ea);border-color:#6c8cff;color:#fff}\n  button.del{border-color:#54282c;color:#ff9d9d}\n  button.del.armado{background:linear-gradient(180deg,#8d3138,#6f242a);border-color:#b0454d;color:#fff}\n  input,select{width:100%;box-sizing:border-box;font-weight:400}\n  input:focus,select:focus{outline:none;border-color:#6c8cff;box-shadow:0 0 0 3px #6c8cff2e}\n  .fixo{background:#1b1b22;color:#8f8f9c;border-style:dashed;padding:6px 9px;border:1px dashed #34343f;\n        border-radius:7px;font:600 12px ui-monospace,monospace}\n  /* O popup de um select e desenhado pelo SO, nao por este CSS: sem color-scheme dark ele vem\n     branco e as options ficam ilegiveis. color-scheme resolve no Chrome/Edge/Firefox modernos;\n     o background/color explicito na option cobre o resto. */\n  select{color-scheme:dark}\n  select option,select optgroup{background:#1e1e26;color:#e8e8ee}\n  select option:checked{background:#2f3a63;color:#fff}\n\n  .it{display:block;width:100%;text-align:left;background:#1c1c24;border:1px solid #2a2a35;border-radius:8px;\n      padding:6px 9px;margin-bottom:5px;cursor:pointer}\n  .it:hover{border-color:#4a4a5a}\n  .it.on{border-color:#6c8cff;box-shadow:0 0 0 2px #6c8cff33}\n  .it b{display:block;font-size:12px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .it code{font:10px ui-monospace,monospace;color:#6c8cff}\n  .it .st{font:10px ui-monospace,monospace;color:#7a7a86;margin-top:2px;display:block}\n  .it .orfao{color:#e3b341}\n  .vazio{color:#5e5e6b;font-size:11px;line-height:1.6}\n\n  h3{margin:0 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:.9px;color:#83838f;\n     display:flex;align-items:center;gap:8px}\n  h3:before{content:\"\";width:3px;height:11px;border-radius:2px;background:#6c8cff;opacity:.85;flex:none}\n  .f{display:block;min-width:0}\n  .f>span{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#7a7a86;margin-bottom:3px}\n  .duo{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:10px}\n  .tri{display:grid;grid-template-columns:1fr 1fr 1fr;gap:9px;margin-bottom:10px}\n  .chips{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}\n  .chips button{padding:3px 8px;font-size:10px;border-radius:5px;font-family:ui-monospace,monospace}\n  .chips button.on{background:linear-gradient(180deg,#6ee7a0,#49c47f);border-color:#6ee7a0;color:#0d2b1a}\n  .acoes{display:flex;gap:8px;align-items:center}\n  .dica{font:10px ui-monospace,monospace;color:#5e5e6b;margin:-4px 0 10px}\n  .alerta{font:10px ui-monospace,monospace;color:#e3b341;margin:-4px 0 10px;\n          border-left:2px solid #e3b341;padding-left:7px}\n  `,\n\n  render(d2){\n    const s=d2.state;\n    const dt=d2.data('tipos');\n    const todos=dt?dt.records():[];\n    const armas=todos.filter(t=>t.category==='weapon').slice().sort((a,b)=>String(a.name).localeCompare(String(b.name)));\n    // Tudo que nao e weapon cai neste painel -- inclusive linhas com category legada, para\n    // que elas fiquem visiveis e possam ser corrigidas ou apagadas por aqui.\n    const outros=todos.filter(t=>t.category!=='weapon').slice().sort((a,b)=>String(a.category+a.name).localeCompare(String(b.category+b.name)));\n    const A=s.armaView||{}, O=s.outroView||{};\n    // Aviso de key repetida enquanto se digita, nao so no clique de salvar: descobrir a colisao\n    // depois de preencher stance/stand/walk/attacks e descobrir tarde.\n    const keyA=A.key?slug(A.key):slug(A.name);\n    const bateA=(!A._id&&keyA)?todos.filter(t=>t.key===keyA)[0]:null;\n    const keyO=O.key?slug(O.key):slug(O.name);\n    const bateO=(!O._id&&keyO)?todos.filter(t=>t.key===keyO)[0]:null;\n    const dicaKey=(k,bate)=>bate\n      ? `<div class=\"alerta\">a key \"${esc(k)}\" ja esta em uso por \"${esc(bate.name)}\" (${esc(bate.category)}) — salvar vai ser recusado</div>`\n      : (k?`<div class=\"dica\">key: ${esc(k)}</div>`:'<div class=\"dica\">key em branco = gerada a partir do name ao salvar</div>');\n\n    const linhaArma=t=>`<button class=\"it ${A._id===t._id?'on':''}\" data-id=\"${esc(t._id)}\" data-key=\"${esc(t._id)}\">\n        <b>${esc(t.name||'(sem nome)')}</b>\n        <code>${esc(t.key||'')}</code> <span class=\"badge\">${esc(t.stance||'sem stance')}</span>\n        <span class=\"st\">${esc(t.stand||'—')} · ${esc(t.walk||'—')} · ${arr(t.attacks).length} attack${arr(t.attacks).length===1?'':'s'}</span>\n      </button>`;\n    const linhaOutro=t=>`<button class=\"ot ${O._id===t._id?'on':''}\" data-id=\"${esc(t._id)}\" data-key=\"${esc(t._id)}\">\n        <b>${esc(t.name||'(sem nome)')}</b>\n        <code>${esc(t.key||'')}</code>\n        <span class=\"badge ${NAO_ARMA.indexOf(t.category)<0?'orfao':''}\">${esc(t.category||'sem categoria')}${NAO_ARMA.indexOf(t.category)<0?' ⚠ categoria legada':''}</span>\n      </button>`;\n\n    return `<div class=\"wrap\">\n      <div class=\"pan\">\n        <div class=\"cab\"><h2>weapon types</h2><span class=\"dir\">\n          ${s.msgA?`<span class=\"badge ${s.msgATipo||''}\">${esc(s.msgA)}</span>`:''}\n          <span class=\"badge\">${armas.length}</span><button class=\"novoA\">+ novo</button>\n        </span></div>\n        <div class=\"lista\">${armas.map(linhaArma).join('')||'<div class=\"vazio\">Nenhum tipo de arma ainda.<br>Cada tipo carrega a pool de actions que o personagem usa com ele.</div>'}</div>\n        <div class=\"form\">\n          <h3>${A._id?'editando':'novo tipo de arma'}</h3>\n          <div class=\"tri\">\n            <label class=\"f\"><span>category</span><div class=\"fixo\">weapon</div></label>\n            <label class=\"f\"><span>key</span><input class=\"aKey\" value=\"${esc(A.key||'')}\" placeholder=\"ex: one-handed-sword\"/></label>\n            <label class=\"f\"><span>name</span><input class=\"aNome\" value=\"${esc(A.name||'')}\" placeholder=\"ex: One-Handed Sword\"/></label>\n          </div>\n          ${dicaKey(keyA,bateA)}\n          <div class=\"tri\">\n            <label class=\"f\"><span>stance</span><select class=\"aStance\">\n              <option value=\"\">—</option>\n              ${STANCES.map(x=>`<option value=\"${x}\" ${A.stance===x?'selected':''}>${x}</option>`).join('')}\n            </select></label>\n            <label class=\"f\"><span>stand</span><select class=\"aStand\">\n              <option value=\"\">—</option>\n              ${STANDS.map(x=>`<option value=\"${x}\" ${A.stand===x?'selected':''}>${x}</option>`).join('')}\n            </select></label>\n            <label class=\"f\"><span>walk</span><select class=\"aWalk\">\n              <option value=\"\">—</option>\n              ${WALKS.map(x=>`<option value=\"${x}\" ${A.walk===x?'selected':''}>${x}</option>`).join('')}\n            </select></label>\n          </div>\n          <label class=\"f\"><span>attacks <em class=\"badge\">${arr(A.attacks).length} selecionada${arr(A.attacks).length===1?'':'s'} — o ataque sorteia uma delas</em></span></label>\n          <div class=\"chips\">${ATAQUES.map(x=>`<button class=\"atk ${arr(A.attacks).indexOf(x)>=0?'on':''}\" data-a=\"${x}\">${x}</button>`).join('')}</div>\n          <div class=\"acoes\">\n            <button class=\"go salvarA\" ${s.salvandoA?'disabled':''}>${s.salvandoA?'salvando…':(A._id?'salvar':'criar tipo')}</button>\n            <button class=\"del excluirA ${s.armadoA?'armado':''}\" ${!A._id||s.salvandoA?'disabled':''}>${s.armadoA?'confirmar exclusao':'excluir'}</button>\n            <button class=\"limparA\">limpar</button>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"pan\">\n        <div class=\"cab\"><h2>armor · accessory · character</h2><span class=\"dir\">\n          ${s.msgO?`<span class=\"badge ${s.msgOTipo||''}\">${esc(s.msgO)}</span>`:''}\n          <span class=\"badge\">${outros.length}</span><button class=\"novoO\">+ novo</button>\n        </span></div>\n        <div class=\"lista\">${outros.map(linhaOutro).join('')||'<div class=\"vazio\">Nenhum tipo ainda.<br>character e onde entram Skin, Hair e Face.</div>'}</div>\n        <div class=\"form\">\n          <h3>${O._id?'editando':'novo tipo'}</h3>\n          <div class=\"tri\">\n            <label class=\"f\"><span>category</span><select class=\"oCat\">\n              <option value=\"\">—</option>\n              ${NAO_ARMA.map(c=>`<option value=\"${c}\" ${O.category===c?'selected':''}>${c}</option>`).join('')}\n              ${O.category&&NAO_ARMA.indexOf(O.category)<0?`<option value=\"${esc(O.category)}\" selected>${esc(O.category)} (legada)</option>`:''}\n            </select></label>\n            <label class=\"f\"><span>key</span><input class=\"oKey\" value=\"${esc(O.key||'')}\" placeholder=\"ex: hat\"/></label>\n            <label class=\"f\"><span>name</span><input class=\"oNome\" value=\"${esc(O.name||'')}\" placeholder=\"ex: Hat\"/></label>\n          </div>\n          ${dicaKey(keyO,bateO)}\n          <div class=\"acoes\">\n            <button class=\"go salvarO\" ${s.salvandoO?'disabled':''}>${s.salvandoO?'salvando…':(O._id?'salvar':'criar tipo')}</button>\n            <button class=\"del excluirO ${s.armadoO?'armado':''}\" ${!O._id||s.salvandoO?'disabled':''}>${s.armadoO?'confirmar exclusao':'excluir'}</button>\n            <button class=\"limparO\">limpar</button>\n          </div>\n        </div>\n      </div>\n    </div>`;\n  },\n\n  mounted(d2){\n    const s=d2.state;\n    s.msgA='';s.msgATipo='';s.msgO='';s.msgOTipo='';s.salvandoA=false;s.salvandoO=false;\n    s.armadoA=false;s.armadoO=false;\n\n    // Dois rascunhos em closure (um por painel). Digitar num campo nao deve re-renderizar as\n    // duas listas; o espelho em d2.state e atualizado nos momentos estruturais.\n    let A=vazioA(), O=vazioO();\n    function vazioA(){return {_id:null,key:'',name:'',stance:'',stand:'',walk:'',attacks:[]};}\n    function vazioO(){return {_id:null,key:'',name:'',category:''};}\n    function espA(){s.armaView=Object.assign({},A,{attacks:arr(A.attacks)});}\n    function espO(){s.outroView=Object.assign({},O);}\n    espA();espO();\n\n    const dsT=()=>d2.data('tipos');\n    function avisoA(m,t){s.msgA=m;s.msgATipo=t||'';}\n    function avisoO(m,t){s.msgO=m;s.msgOTipo=t||'';}\n\n    (async()=>{\n      const dt=dsT();\n      if(!dt){avisoA('dataset de tipos nao encontrado','bad');return;}\n      try{await dt.refresh();await dt.loadAll();}\n      catch(err){avisoA('dataset: '+((err&&err.message)||err),'bad');}\n    })();\n\n    async function recarrega(){const dt=dsT();await dt.refresh();await dt.loadAll();}\n    function achaRec(id){return (dsT()?dsT().records():[]).filter(r=>String(r._id)===String(id))[0];}\n\n    // ---------- painel weapon ----------\n    d2.on('.it','click',(e,el)=>{\n      const r=achaRec(el.getAttribute('data-id'));if(!r)return;\n      A={_id:r._id,key:r.key||'',name:r.name||'',stance:r.stance||'',stand:r.stand||'',walk:r.walk||'',attacks:arr(r.attacks)};\n      espA();avisoA('');s.armadoA=false;d2.refresh();\n    });\n    // Estes dois re-renderizam (as outras entradas nao): e o que faz o aviso de key repetida\n    // aparecer enquanto se digita. As listas aqui sao pequenas, entao o custo e irrelevante.\n    d2.on('.aKey','input',(e,el)=>{A.key=el.value;espA();d2.refresh();});\n    d2.on('.aNome','input',(e,el)=>{A.name=el.value;espA();d2.refresh();});\n    d2.on('.aStance','change',(e,el)=>{A.stance=el.value;});\n    d2.on('.aStand','change',(e,el)=>{A.stand=el.value;});\n    d2.on('.aWalk','change',(e,el)=>{A.walk=el.value;});\n    // Chip de ataque: alterna. Precisa re-renderizar porque o contador e o estado do chip\n    // sao desenhados no render.\n    d2.on('.atk','click',(e,el)=>{\n      const a=el.getAttribute('data-a');\n      const i=A.attacks.indexOf(a);\n      if(i>=0)A.attacks.splice(i,1);else A.attacks.push(a);\n      espA();d2.refresh();\n    });\n    d2.on('.novoA','click',()=>{A=vazioA();espA();avisoA('');s.armadoA=false;d2.refresh();});\n    d2.on('.limparA','click',()=>{A=vazioA();espA();avisoA('');s.armadoA=false;d2.refresh();});\n\n    d2.on('.salvarA','click',async()=>{\n      const dt=dsT();if(!dt)return;\n      if(!A.name&&!A.key){avisoA('preencha ao menos o name','warn');return;}\n      const key=A.key?slug(A.key):slug(A.name);\n      if(!key){avisoA('name invalido para gerar a key','warn');return;}\n      if(!A._id&&dt.records().filter(r=>r.key===key)[0]){avisoA('a key \"'+key+'\" ja existe','warn');return;}\n      s.salvandoA=true;avisoA('');s.armadoA=false;\n      try{\n        const dados={key:key,name:A.name||key,category:'weapon',stance:A.stance||'',\n                     stand:A.stand||'',walk:A.walk||'',attacks:arr(A.attacks)};\n        if(A._id)await dt.update(Object.assign({_id:A._id},dados));\n        else await dt.create(dados);\n        await recarrega();\n        if(!A._id){const novo=dt.records().filter(r=>r.key===key)[0];if(novo)A._id=novo._id;}\n        A.key=key;espA();avisoA('salvo','ok');\n      }catch(err){avisoA('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvandoA=false;}\n    });\n    d2.on('.excluirA','click',async()=>{\n      if(!A._id)return;\n      if(!s.armadoA){s.armadoA=true;avisoA('clique de novo para confirmar','warn');return;}\n      s.armadoA=false;s.salvandoA=true;\n      try{await dsT().remove(A._id);await recarrega();A=vazioA();espA();avisoA('excluido','ok');}\n      catch(err){avisoA('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvandoA=false;}\n    });\n\n    // ---------- painel armor / accessory / character ----------\n    d2.on('.ot','click',(e,el)=>{\n      const r=achaRec(el.getAttribute('data-id'));if(!r)return;\n      O={_id:r._id,key:r.key||'',name:r.name||'',category:r.category||''};\n      espO();avisoO('');s.armadoO=false;d2.refresh();\n    });\n    d2.on('.oKey','input',(e,el)=>{O.key=el.value;espO();d2.refresh();});\n    d2.on('.oNome','input',(e,el)=>{O.name=el.value;espO();d2.refresh();});\n    d2.on('.oCat','change',(e,el)=>{O.category=el.value;espO();});\n    d2.on('.novoO','click',()=>{O=vazioO();espO();avisoO('');s.armadoO=false;d2.refresh();});\n    d2.on('.limparO','click',()=>{O=vazioO();espO();avisoO('');s.armadoO=false;d2.refresh();});\n\n    d2.on('.salvarO','click',async()=>{\n      const dt=dsT();if(!dt)return;\n      if(!O.category){avisoO('escolha a category','warn');return;}\n      if(!O.name&&!O.key){avisoO('preencha ao menos o name','warn');return;}\n      const key=O.key?slug(O.key):slug(O.name);\n      if(!key){avisoO('name invalido para gerar a key','warn');return;}\n      if(!O._id&&dt.records().filter(r=>r.key===key)[0]){avisoO('a key \"'+key+'\" ja existe','warn');return;}\n      s.salvandoO=true;avisoO('');s.armadoO=false;\n      try{\n        const dados={key:key,name:O.name||key,category:O.category,stance:'',stand:'',walk:'',attacks:[]};\n        if(O._id)await dt.update(Object.assign({_id:O._id},dados));\n        else await dt.create(dados);\n        await recarrega();\n        if(!O._id){const novo=dt.records().filter(r=>r.key===key)[0];if(novo)O._id=novo._id;}\n        O.key=key;espO();avisoO('salvo','ok');\n      }catch(err){avisoO('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvandoO=false;}\n    });\n    d2.on('.excluirO','click',async()=>{\n      if(!O._id)return;\n      if(!s.armadoO){s.armadoO=true;avisoO('clique de novo para confirmar','warn');return;}\n      s.armadoO=false;s.salvandoO=true;\n      try{await dsT().remove(O._id);await recarrega();O=vazioO();espO();avisoO('excluido','ok');}\n      catch(err){avisoO('falhou: '+((err&&err.message)||err),'bad');}\n      finally{s.salvandoO=false;}\n    });\n  }\n};\n"}},"tipos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tipos"}}}}}}}}}},{"_id":"6a98ac05d515fc73a145ee09","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mcstatpanel","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Analytics panel for the previewer. Reads the PER-SESSION summary written by `mcstat` (one row\n// per session) through `mcstatall`, plus the live presence count from `mclivenow`.\n//\n// `mcstatall` and `mclivenow` are deliberately NOT public: analytics must not be world-readable.\n// So this panel works in the builder preview (the builder passes the system default). If it ever\n// moves inside a published app, gate the ROUTE with auth -- do not open the listeners.\n//\n// Aggregation happens HERE, on the client, not in the handler: the volume is one row per session,\n// so d2-lang aggregation would be premature. Past a few thousand rows the math moves to the\n// handler and this file only draws.\n//\n// REFRESH: polling, every 15s. There is no push socket behind this -- see the note next to\n// `INTERVALO`. For a dashboard one person keeps open, polling is indistinguishable from push.\n// IANA timezone -> country. This map lives in the PANEL, not in the previewer, on purpose:\n// improving it needs no republish of the app, and rows already stored benefit at once.\n// Anything unmapped falls back to the zone itself -- readable, never a silent \"unknown\".\nconst PAIS={\n  'America/Sao_Paulo':'Brazil','America/Fortaleza':'Brazil','America/Recife':'Brazil',\n  'America/Bahia':'Brazil','America/Belem':'Brazil','America/Manaus':'Brazil',\n  'America/Cuiaba':'Brazil','America/Campo_Grande':'Brazil','America/Porto_Velho':'Brazil',\n  'America/Boa_Vista':'Brazil','America/Rio_Branco':'Brazil','America/Araguaina':'Brazil',\n  'America/Maceio':'Brazil','America/Santarem':'Brazil','America/Noronha':'Brazil',\n  'America/Argentina/Buenos_Aires':'Argentina','America/Buenos_Aires':'Argentina',\n  'America/Santiago':'Chile','America/Bogota':'Colombia','America/Lima':'Peru',\n  'America/Caracas':'Venezuela','America/Montevideo':'Uruguay','America/Asuncion':'Paraguay',\n  'America/La_Paz':'Bolivia','America/Guayaquil':'Ecuador','America/Mexico_City':'Mexico',\n  'America/Monterrey':'Mexico','America/Tijuana':'Mexico',\n  'America/New_York':'United States','America/Chicago':'United States',\n  'America/Denver':'United States','America/Los_Angeles':'United States',\n  'America/Phoenix':'United States','America/Anchorage':'United States',\n  'America/Detroit':'United States','Pacific/Honolulu':'United States',\n  'America/Toronto':'Canada','America/Vancouver':'Canada','America/Edmonton':'Canada',\n  'America/Winnipeg':'Canada','America/Halifax':'Canada',\n  'Europe/Lisbon':'Portugal','Europe/Madrid':'Spain','Europe/London':'United Kingdom',\n  'Europe/Dublin':'Ireland','Europe/Paris':'France','Europe/Berlin':'Germany',\n  'Europe/Rome':'Italy','Europe/Amsterdam':'Netherlands','Europe/Brussels':'Belgium',\n  'Europe/Zurich':'Switzerland','Europe/Vienna':'Austria','Europe/Stockholm':'Sweden',\n  'Europe/Oslo':'Norway','Europe/Copenhagen':'Denmark','Europe/Helsinki':'Finland',\n  'Europe/Warsaw':'Poland','Europe/Prague':'Czechia','Europe/Budapest':'Hungary',\n  'Europe/Bucharest':'Romania','Europe/Athens':'Greece','Europe/Istanbul':'Turkiye',\n  'Europe/Moscow':'Russia','Europe/Kyiv':'Ukraine','Europe/Kiev':'Ukraine',\n  'Asia/Seoul':'South Korea','Asia/Tokyo':'Japan','Asia/Shanghai':'China',\n  'Asia/Hong_Kong':'Hong Kong','Asia/Taipei':'Taiwan','Asia/Singapore':'Singapore',\n  'Asia/Manila':'Philippines','Asia/Jakarta':'Indonesia','Asia/Bangkok':'Thailand',\n  'Asia/Ho_Chi_Minh':'Vietnam','Asia/Kuala_Lumpur':'Malaysia','Asia/Kolkata':'India',\n  'Asia/Calcutta':'India','Asia/Dubai':'UAE','Asia/Jerusalem':'Israel',\n  'Australia/Sydney':'Australia','Australia/Melbourne':'Australia',\n  'Australia/Brisbane':'Australia','Australia/Perth':'Australia',\n  'Pacific/Auckland':'New Zealand','Africa/Johannesburg':'South Africa',\n  'Africa/Lagos':'Nigeria','Africa/Cairo':'Egypt','Africa/Casablanca':'Morocco'\n};\nfunction pais(tz){return PAIS[tz]||(tz?String(tz):'unknown');}\nfunction fusoLocal(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||'local time';}catch(e){return 'local time';}}\n\nexport default {\n  css: `\n  .wrap{--esc:linear-gradient(180deg,#4e5867 0%,#424c5a 46%,#333b47 100%);\n    --escluz:inset 0 1px 0 #ffffff36,inset 0 -1px 0 #00000059;\n    --neon:#ddfe01;--ac:#3ba7d1;\n    /* height + overflow, e nao min-height: o bloco do code component e ancorado nas quatro bordas\n       do artboard, entao ele tem altura fixa e o conteudo mais alto era simplesmente cortado.\n       Com height:100% o .wrap vira a caixa de rolagem e o painel inteiro fica alcancavel sem\n       depender de zoom. */\n    box-sizing:border-box;padding:24px;background:#262c33;height:100%;overflow-y:auto;\n    font:400 13px ui-sans-serif,system-ui,sans-serif;color:#eef2f6}\n  /* a barra padrao do navegador sai clara sobre este fundo escuro e parece defeito */\n  .wrap::-webkit-scrollbar{width:12px}\n  .wrap::-webkit-scrollbar-track{background:transparent}\n  .wrap::-webkit-scrollbar-thumb{background:#4e5867;background-clip:content-box;\n    border:3px solid transparent;border-radius:99px}\n  .wrap::-webkit-scrollbar-thumb:hover{background:#6d7a82;background-clip:content-box}\n  .wrap *{box-sizing:border-box}\n  .topo{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}\n  h1{margin:0 0 2px;font-size:20px;font-weight:700;letter-spacing:.2px}\n  .sub{margin:0;color:#94a0ac;font-size:12px}\n  /* Cartao de presenca: e o unico numero que muda sozinho na tela, entao ele tem tratamento\n     proprio -- ponto pulsando para dizer \"isto esta vivo\", sem texto extra explicando. */\n  .vivo{display:flex;align-items:center;gap:12px;background:var(--esc);border:1px solid #232932;\n    border-radius:14px;padding:12px 18px;box-shadow:var(--escluz),0 10px 24px -14px #00000085;flex:none}\n  .ponto{width:9px;height:9px;border-radius:99px;background:var(--neon);flex:none;\n    box-shadow:0 0 0 0 #ddfe0166;animation:pulso 2s ease-out infinite}\n  .ponto.zero{background:#6d7883;animation:none;box-shadow:none}\n  @keyframes pulso{0%{box-shadow:0 0 0 0 #ddfe0159}70%{box-shadow:0 0 0 9px #ddfe0100}100%{box-shadow:0 0 0 0 #ddfe0100}}\n  .vivo .n{font:700 26px ui-sans-serif,system-ui;line-height:1}\n  .vivo .r{font:700 9px ui-sans-serif,system-ui;text-transform:uppercase;letter-spacing:.9px;color:#9fb0bd}\n  .vivo .r i{font-style:normal;display:block;color:#7d888f;letter-spacing:0;text-transform:none;\n    font-weight:400;font-size:10px;margin-top:2px}\n  .linha{display:grid;gap:14px;margin-bottom:14px}\n  .l4{grid-template-columns:repeat(4,1fr)}\n  .l3{grid-template-columns:repeat(3,1fr)}\n  .cx{background:var(--esc);border:1px solid #232932;border-radius:14px;padding:14px 16px;\n    box-shadow:var(--escluz),0 10px 24px -14px #00000085;min-width:0}\n  .cx h2{margin:0 0 10px;font:700 9px ui-sans-serif,system-ui;text-transform:uppercase;\n    letter-spacing:.9px;color:#9fb0bd}\n  .num{font:700 30px ui-sans-serif,system-ui;line-height:1.05;letter-spacing:-.5px}\n  .pe{font-size:11px;color:#94a0ac;margin-top:4px}\n  .verde{color:var(--neon)}\n  /* Barra: rotulo, trilho e valor. Sem lib e sem svg -- duas divs. */\n  .b{display:grid;grid-template-columns:1fr 34px;align-items:center;gap:9px;margin-bottom:6px}\n  .b .rot{font-size:11px;color:#cdd7de;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n  .b .rot i{font-style:normal;color:#7d888f;font-family:ui-monospace,monospace;font-size:10px}\n  .b b{font:600 11px ui-monospace,monospace;color:#eef2f6;text-align:right}\n  .trilho{height:8px;border-radius:99px;background:#00000038;overflow:hidden;margin-top:3px}\n  .fita{height:100%;border-radius:99px;background:linear-gradient(90deg,#3ba7d1,#57bde0)}\n  .fita.q{background:linear-gradient(90deg,#9aa61a,#ddfe01)}\n  .fita.r{background:linear-gradient(90deg,#a33,#e8352e)}\n  .larga{grid-template-columns:104px 1fr 44px}\n  .larga .trilho{margin-top:0}\n  .grid-cat{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}\n  .catcx{background:#00000024;border:1px solid #2b333d;border-radius:11px;padding:11px 12px;min-width:0}\n  .catcx h3{margin:0 0 8px;font:700 10px ui-sans-serif,system-ui;letter-spacing:.5px;\n    color:#cdd7de;text-transform:capitalize}\n  .catcx ol{margin:0;padding:0;list-style:none;counter-reset:p}\n  .catcx li{counter-increment:p;display:grid;grid-template-columns:14px 1fr 26px;gap:7px;\n    align-items:baseline;font-size:11px;margin-bottom:4px;min-width:0}\n  .catcx li:before{content:counter(p);color:#6d7883;font:600 10px ui-monospace,monospace}\n  .catcx li span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e3e9ee}\n  .catcx li b{font:600 10px ui-monospace,monospace;color:#9fb0bd;text-align:right}\n  .vazio{padding:40px;text-align:center;color:#94a0ac}\n  .rod{color:#7d888f;font-size:11px;margin-top:8px;line-height:1.6}\n  .mono{font-family:ui-monospace,monospace}\n  /* Seletor de dia: chips. Um <select> nativo abriria um popup do SISTEMA, que nao herda este\n     tema -- o mesmo motivo que ja tirou os selects do previewer. */\n  .chips{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 14px}\n  .chip{padding:5px 12px;border-radius:999px;border:1px solid #232932;cursor:pointer;\n    font:600 11px ui-sans-serif,system-ui;color:#cdd7de;background:var(--esc);\n    box-shadow:var(--escluz);transition:color .12s}\n  .chip:hover{color:#fff}\n  .chip.on{background:linear-gradient(180deg,#57bde0,#2a8cb8);border-color:#1d6d94;color:#fff}\n  /* 24 colunas. Vertical porque o eixo aqui e o TEMPO, e tempo se le da esquerda para a\n     direita -- a lista horizontal usada no resto do painel serve para ranking, nao para serie. */\n  .horas{display:flex;align-items:stretch;gap:3px;height:132px;margin-top:2px}\n  .col{flex:1;display:flex;flex-direction:column;min-width:0}\n  .cwrap{flex:1;display:flex;align-items:flex-end}\n  .cbar{width:100%;border-radius:3px 3px 0 0;background:linear-gradient(180deg,#57bde0,#2a8cb8);\n    min-height:2px}\n  .cbar.pico{background:linear-gradient(180deg,#ddfe01,#9aa61a)}\n  .col em{font-style:normal;font:400 9px ui-monospace,monospace;color:#7d888f;text-align:center;\n    margin-top:5px;height:11px;overflow:hidden}\n  `,\n\n  render(d2){\n    const s=d2.state;\n    if(s.erro)return `<div class=\"wrap\"><div class=\"vazio\">Could not read analytics: ${esc(s.erro)}</div></div>`;\n    if(!s.pronto)return `<div class=\"wrap\"><div class=\"vazio\">Loading…</div></div>`;\n\n    // Test rows never enter any count. Without this the first number you read is already a lie.\n    // DIA E HORA SAO DERIVADOS AQUI, no fuso de quem olha o painel -- ou seja, o seu.\n    // A linha guarda `ts`, o inicio da sessao em UTC. Antes o grafico usava a hora LOCAL DE QUEM\n    // ACESSOU, e isso misturava fusos: alguem nos EUA as 20h dele caia em 20h no mesmo grafico\n    // em que 22h de Brasilia. Para saber quando O SEU publico esta online, tudo precisa estar na\n    // mesma linha do tempo, e a conversao e trabalho da LEITURA, nao da escrita.\n    // Linha antiga sem `ts` cai no `createdAt`, que o servidor carimba em UTC: fica na linha do\n    // tempo certa, atrasada pela duracao da sessao (segundos), o que nao move um balde de 1 hora.\n    const TODOS=(s.linhas||[]).filter(r=>{\n      const v=String(r.vid||'');\n      return v.indexOf('TESTE')!==0&&v.indexOf('SEED')!==0;\n    }).map(r=>{\n      const q=new Date(r.ts||r.createdAt||0);\n      const ok=!isNaN(q.getTime())&&q.getTime()>0;\n      return Object.assign({},r,{\n        _dia:ok?q.toLocaleDateString('en-CA'):(r.dia||'?'),\n        _hora:ok?q.getHours():(typeof r.hora==='number'?r.hora:null)\n      });\n    });\n    // O seletor de dia filtra o painel INTEIRO menos o grafico \"sessions per day\", que continua\n    // global de proposito: e ele que serve de mapa para escolher o dia.\n    const dias=(()=>{const o=Object.create(null);for(const r of TODOS)o[r._dia||'?']=1;\n      return Object.keys(o).sort().reverse();})();\n    const diaSel=(s.dia&&dias.indexOf(s.dia)>=0)?s.dia:'';\n    const R=diaSel?TODOS.filter(r=>r._dia===diaSel):TODOS;\n    const ativos=s.ativos|0;\n    const vivoCx=`<div class=\"vivo\"><span class=\"ponto ${ativos?'':'zero'}\"></span>\n      <div><div class=\"n\">${ativos}</div>\n      <div class=\"r\">active now<i>seen in the last 100s</i></div></div></div>`;\n\n    if(!TODOS.length)return `<div class=\"wrap\"><div class=\"topo\">\n      <div><h1>Maple Simulator — Previewer Analytics</h1>\n      <p class=\"sub\">No sessions recorded yet.</p></div>${vivoCx}</div>\n      <div class=\"cx\"><h2>what to expect</h2><div class=\"rod\">Telemetry only fires on a\n      <b class=\"mono\">.data2.app</b> domain, so builder previews never count. Each visit becomes ONE\n      row, sent when the tab is closed or hidden. This panel refreshes every 15s.</div></div></div>`;\n\n    const n=R.length;\n    const uniq=k=>{const o=Object.create(null);for(const r of R)o[r[k]]=1;return Object.keys(o).length;};\n    const pct=(a,b)=>b?Math.round(a/b*100):0;\n    // \"used it\" = equipped something OR rolled the dice. The line between a visit and actual use.\n    const usou=R.filter(r=>(r.eq|0)>0||(r.rnd|0)>0).length;\n    const voltou=R.filter(r=>r.volta===true).length;\n    const comRnd=R.filter(r=>(r.rnd|0)>0).length;\n    const comPort=R.filter(r=>(r.port|0)>0).length;\n    const cliquesPort=R.reduce((a,r)=>a+(r.port|0),0);\n    const comBusca=R.filter(r=>(r.busca|0)>0).length;\n    const durOrd=R.map(r=>r.dur|0).sort((a,b)=>a-b);\n    const mediana=durOrd.length?durOrd[durOrd.length>>1]:0;\n\n    const conta=campo=>{\n      const o=Object.create(null);\n      for(const r of R)for(const v of String(r[campo]||'').split(',')){\n        const k=v.trim();if(k)o[k]=(o[k]||0)+1;\n      }\n      return Object.keys(o).map(k=>[k,o[k]]).sort((a,b)=>b[1]-a[1]);\n    };\n    const cats=conta('cats'), erros=conta('erros');\n\n    // `itens` travels as cat|id|name entries joined by \";\". Rows written before names existed\n    // carry a bare id -- they still count, they just land under \"unknown\".\n    const itens=Object.create(null);\n    const anota=(cat,id,nome)=>{\n      if(!id)return;\n      const k=cat+'|'+id;\n      if(!itens[k])itens[k]={cat:cat,id:id,nome:nome,n:0};\n      if(nome&&!itens[k].nome)itens[k].nome=nome;\n      itens[k].n++;\n    };\n    for(const r of R)for(const p of String(r.itens||'').split(';')){\n      const t=p.trim();if(!t)continue;\n      // LEGACY ROWS: before 02/09 this column was a plain comma-separated list of ids, with no\n      // category and no name. Splitting only on \";\" swallowed the whole list as ONE id -- caught\n      // because real sessions were already in the table while this panel was being built. They\n      // still count; they just land under \"unknown\".\n      if(t.indexOf('|')<0){\n        for(const cru of t.split(',')){const id=cru.trim();if(id)anota('unknown',id,'');}\n        continue;\n      }\n      const parte=t.split('|');\n      anota((parte[0]||'').trim()||'unknown',(parte[1]||'').trim(),(parte[2]||'').trim());\n    }\n    const listaItens=Object.keys(itens).map(k=>itens[k]).sort((a,b)=>b.n-a.n);\n    const porCat=Object.create(null);\n    for(const it of listaItens){(porCat[it.cat]=porCat[it.cat]||[]).push(it);}\n    const catsOrd=Object.keys(porCat).sort((a,b)=>\n      porCat[b].reduce((x,y)=>x+y.n,0)-porCat[a].reduce((x,y)=>x+y.n,0));\n\n    // Width buckets chosen for what they DECIDE: 1184 is what the layout needs today; 1024 and\n    // 768 are the usual tablet/phone cuts. The first bar is literally \"how many do not fit\".\n    const faixas=[['under 768',0,767],['768 to 1023',768,1023],['1024 to 1183',1024,1183],['1184 or more',1184,1e9]];\n    const larg=faixas.map(f=>[f[0],R.filter(r=>(r.larg|0)>=f[1]&&(r.larg|0)<=f[2]).length]);\n    const naoCabe=R.filter(r=>(r.larg|0)<1184).length;\n\n    const porDia=(()=>{\n      const o=Object.create(null);\n      for(const r of TODOS)o[r._dia||'?']=(o[r._dia||'?']||0)+1;\n      return Object.keys(o).sort().slice(-14).map(k=>[k,o[k]]);\n    })();\n\n    const barras=(lista,classe)=>{\n      if(!lista.length)return `<div class=\"rod\">nothing yet</div>`;\n      const max=Math.max.apply(null,lista.map(x=>x[1]))||1;\n      return lista.map(x=>`<div class=\"b larga\"><span class=\"rot\" title=\"${esc(x[0])}\">${esc(x[0])}</span>\n        <div class=\"trilho\"><div class=\"fita ${classe||''}\" style=\"width:${Math.max(2,Math.round(x[1]/max*100))}%\"></div></div>\n        <b>${x[1]}</b></div>`).join('');\n    };\n    const barrasItem=lista=>{\n      if(!lista.length)return `<div class=\"rod\">nothing yet</div>`;\n      const max=lista[0].n||1;\n      return lista.map(it=>`<div class=\"b\"><div style=\"min-width:0\">\n        <div class=\"rot\" title=\"${esc(it.nome||it.id)} · ${esc(it.id)}\">${esc(it.nome||'(no name)')} <i>${esc(it.id)}</i></div>\n        <div class=\"trilho\"><div class=\"fita q\" style=\"width:${Math.max(2,Math.round(it.n/max*100))}%\"></div></div>\n        </div><b>${it.n}</b></div>`).join('');\n    };\n    const cartao=(t,v,p,cor)=>`<div class=\"cx\"><h2>${t}</h2><div class=\"num ${cor||''}\">${v}</div>\n      <div class=\"pe\">${p||''}</div></div>`;\n\n    // Sessoes por hora do dia. A hora e a do INICIO da sessao e no relogio de quem acessou --\n    // ver a nota no mcbaseviewer. O pico ganha cor propria para nao precisar ler os numeros.\n    const horas=new Array(24).fill(0);\n    for(const r of R){const h=r._hora;if(typeof h==='number'&&h>=0&&h<24)horas[h]++;}\n    const picoH=Math.max.apply(null,horas)||0;\n    const semHora=R.filter(r=>typeof r._hora!=='number').length;\n    const colunas=horas.map((c,i)=>`<div class=\"col\" title=\"${i}:00 — ${c} session(s)\">\n      <div class=\"cwrap\"><div class=\"cbar ${c&&c===picoH?'pico':''}\" style=\"height:${picoH?Math.max(2,Math.round(c/picoH*100)):0}%\"></div></div>\n      <em>${i%3===0?i:''}</em></div>`).join('');\n\n    const paises=(()=>{\n      const o=Object.create(null);\n      for(const r of R){const p=pais(r.tz);o[p]=(o[p]||0)+1;}\n      return Object.keys(o).map(k=>[k,o[k]]).sort((a,b)=>b[1]-a[1]);\n    })();\n\n    return `<div class=\"wrap\">\n      <div class=\"topo\">\n        <div><h1>Maple Simulator — Previewer Analytics</h1>\n        <p class=\"sub\">${n} sessions · one row per session · auto-refreshing every 15s${s.hora?' · updated '+esc(s.hora):''}</p></div>\n        ${vivoCx}\n      </div>\n\n      <div class=\"chips\">\n        <button class=\"chip dchip ${diaSel?'':'on'}\" data-d=\"\">All days</button>\n        ${dias.slice(0,20).map(d=>`<button class=\"chip dchip ${d===diaSel?'on':''}\" data-d=\"${esc(d)}\">${esc(d)}</button>`).join('')}\n      </div>\n\n      <div class=\"linha l4\">\n        ${cartao('sessions',n,uniq('vid')+' distinct visitors')}\n        ${cartao('arrived and used it',pct(usou,n)+'%',usou+' of '+n+' equipped or rolled',usou/n>=.5?'verde':'')}\n        ${cartao('came back',pct(voltou,n)+'%',voltou+' sessions restored a saved outfit')}\n        ${cartao('median session',mediana+'s','half the sessions are shorter than this')}\n      </div>\n\n      <div class=\"linha l3\">\n        <div class=\"cx\"><h2>screen width — does the app fit?</h2>\n          ${barras(larg)}\n          <div class=\"rod\">${pct(naoCabe,n)}% arrive with less than 1184px, which is what the\n          layout needs today. This is the argument for (or against) the mobile work.</div></div>\n        <div class=\"cx\"><h2>categories opened</h2>${barras(cats)}</div>\n        <div class=\"cx\"><h2>sessions per day</h2>${barras(porDia)}</div>\n      </div>\n\n      <div class=\"linha l3\">\n        ${cartao('randomize',pct(comRnd,n)+'%','sessions that rolled at least once')}\n        ${cartao('search',pct(comBusca,n)+'%','sessions that searched')}\n        ${cartao('illustration requests',cliquesPort,comPort+' sessions clicked the disabled button',cliquesPort?'verde':'')}\n      </div>\n\n      <div class=\"linha\" style=\"grid-template-columns:2fr 1fr\">\n        <div class=\"cx\"><h2>sessions by hour of day${diaSel?' — '+esc(diaSel):''}</h2>\n          <div class=\"horas\">${colunas}</div>\n          <div class=\"rod\">Hour the session STARTED, converted to <b>your</b> timezone (${esc(fusoLocal())})\n          — every visitor lands on one timeline, so a peak here is a peak in your day.\n          Peak: ${picoH?picoH+' session(s)':'none yet'}.\n          ${semHora?semHora+' older row(s) have no hour and are not in this chart.':''}</div></div>\n        <div class=\"cx\"><h2>where from</h2>${barras(paises)}\n          <div class=\"rod\">Derived from the browser timezone, not from IP — no address is sent\n          anywhere and none is stored. Country-level accuracy for almost everyone.</div></div>\n      </div>\n\n      <div class=\"linha\" style=\"grid-template-columns:2fr 1fr\">\n        <div class=\"cx\"><h2>most equipped items — picked by click, rolls excluded</h2>\n          ${barrasItem(listaItens.slice(0,12))}</div>\n        <div class=\"cx\"><h2>errors</h2>${barras(erros.slice(0,10),'r')}\n          <div class=\"rod\"><b class=\"mono\">emote:&lt;block&gt;</b> names exactly which face blocks\n          are worth compiling.</div></div>\n      </div>\n\n      <div class=\"cx\"><h2>top items per category</h2>\n        <div class=\"grid-cat\">\n          ${catsOrd.map(c=>`<div class=\"catcx\"><h3>${esc(c)}</h3><ol>\n            ${porCat[c].slice(0,5).map(it=>`<li><span title=\"${esc(it.nome||it.id)} · ${esc(it.id)}\">${esc(it.nome||it.id)}</span><b>${it.n}</b></li>`).join('')}\n          </ol></div>`).join('')}\n        </div>\n        <div class=\"rod\">Names are stored at write time, next to the id — resolving them here would\n        mean downloading the 7.4 MB of catalogue indexes just to read a number.</div></div>\n    </div>`;\n  },\n\n  mounted(d2){\n    const s=d2.state;\n    s.pronto=false;s.linhas=[];s.ativos=0;s.hora='';s.dia='';\n    // Trocar de dia NAO refaz requisicao: as linhas ja estao todas em memoria e o filtro e local.\n    d2.on('.dchip','click',(e,el)=>{s.dia=el.getAttribute('data-d')||'';});\n    // 15s: a dashboard one person keeps open. A real push socket exists on the platform, but for\n    // this cadence the difference is invisible and polling has no connection to keep alive.\n    const INTERVALO=15000;\n    // 100s of tolerance against a 40s heartbeat: two missed beats still count as present, one\n    // closed tab does not. Cutoff is computed from THIS clock and compared against updatedAt,\n    // which the server stamps -- visitor clocks never enter the count.\n    const JANELA=100000;\n    async function carrega(){\n      try{\n        // PAGINADO (QA 11/09): o find do d2lang devolve no maximo 1.000 linhas por chamada, mesmo\n        // pedindo $limit maior -- passando de 1.000 sessoes o painel perderia as mais NOVAS, calado.\n        // O mcstatall aceita `skip`; pede ate vir uma pagina incompleta (teto de 50 = 50 mil sessoes).\n        let todas=[];\n        for(let pg=0;pg<50;pg++){\n          const r=await d2.rush('mcstatall').invoke({skip:pg*1000});\n          const lote=(r&&r.data)||[];\n          todas=todas.concat(lote);\n          if(lote.length<1000)break;\n        }\n        s.linhas=todas;\n        const v=await d2.rush('mclivenow').invoke({desde:new Date(Date.now()-JANELA).toISOString()});\n        s.ativos=((v&&v.data)||{}).ativos|0;\n        s.hora=new Date().toLocaleTimeString();\n        s.pronto=true;\n      }catch(err){if(!s.pronto)s.erro=(err&&err.message)?err.message:String(err);}\n    }\n    carrega();\n    const t=setInterval(carrega,INTERVALO);\n    d2.onUnmount(()=>{clearInterval(t);});\n  }\n};\n\nfunction esc(v){return String(v==null?'':v).replace(/[<>&\"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;'}[c]));}"}}}}}}}},{"_id":"6a9adaa916a89cce65646a7e","component":"data2-core-types@Component","system":"69b15e6093419d8151c50e4c","params":{"extends":"data2-core-primitives@Block","component":"mcitemreg","package":"bigaton-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// CADASTRO GERAL DE ITENS. O toggle \"available on simulator\" e ABSOLUTO: desde 04/09 as regras\n// derivadas (sem-icone, faixa de teste) NAO filtram mais o previewer -- elas continuam sendo\n// calculadas aqui, mas so como COLUNA e FILTRO, para ajudar a decidir. Quem decide e a tabela.\n//\n// De onde vem cada coisa (e por que):\n//   lista e exibicao  -> os idx publicados no Drive, os MESMOS que o previewer le. Nenhuma copia\n//                        nova do catalogo, e a tela e instantanea mesmo com 38.566 itens.\n//   available         -> a projecao mc-hiddenDataSet (publica, ~2 KB). Ler mc-item aqui seria uma\n//                        varredura de 4,6 MB por pagina.\n//   _id, nota, datas  -> mcitemrows, so para os itens que voce SELECIONA ou SALVA.\nconst CATS=['hair','face','cape','cap','weapon','outfit','coat','pants','shoes','glove','offhand','accface','acceye','accear'];\nconst ROTULO={accface:'face acc',acceye:'eye acc',accear:'earring',offhand:'off-hand',coat:'top'};\nconst MEDIA='https://bigaton-playground.data2.media/file/';\nconst HIDDEN_URL='https://bigaton-playground.data2apis.com/dataset/mc-hiddenDataSet';\nconst POR_PAGINA=60;   // a linha e compacta (sem icone), entao cabe mais gente na tela\nconst PEDACO=500;   // ids por linha da projecao; tem que casar com o gerador\n\n// As condicoes conhecidas, MESMA regra do tools_data2/seed_mcitem.py. Elas nao escondem nada\n// aqui -- so descrevem. Se divergirem do script, o cadastro passa a mentir: mexeu la, mexa aqui.\nconst ELUNA=new Set(); for(let n=2270;n<=2309;n++)if(n%5!==0)ELUNA.add('0103'+n);\nconst LUM=new Set();   for(let n=3693;n<=3713;n++)LUM.add('0170'+n);\nconst AVU=new Set(['01002388','01004059','01702099']);\nconst FAIXA=new Set(['0160','0169']);\nconst FLAGS=['sem-icone','nao-desenha','sem-nome','faixa-teste','dup','id-avulso'];\nfunction flagsDe(cat,r){\n  const f=[];\n  if(!r.ic)f.push('sem-icone');\n  if(!r.geoRef)f.push('nao-desenha');\n  if(!String(r.name||'').trim())f.push('sem-nome');\n  if(cat==='weapon'&&FAIXA.has(r.id.slice(0,4)))f.push('faixa-teste');\n  // UMA flag so para duplicata, seja qual for a familia (pedido de 04/09): o que interessa e\n  // \"isto e copia de outro item\", nao de qual lote a copia veio.\n  if(ELUNA.has(r.id)||LUM.has(r.id))f.push('dup');\n  if(AVU.has(r.id))f.push('id-avulso');\n  return f;\n}\n\n// Caches no nivel do MODULO, e nao dentro do mounted: `render` e `mounted` sao funcoes IRMAS do\n// objeto exportado, entao o render nao enxerga o closure do mounted. Mesma regra que o\n// THUMBCACHE do mcbaseviewer segue.\nconst INDEXREF=Object.create(null);\nconst IDX=Object.create(null);      // cat -> {sheets:[url], items:[...]}\nconst CATDE=Object.create(null);    // id -> cat\nconst META=Object.create(null);     // id -> linha do idx\nconst HIDDEN=new Set();             // ids escondidos, segundo a projecao publicada\nconst PEND=Object.create(null);     // itemId -> bool: alteracoes NAO salvas\nconst BLOCO=Object.create(null);    // geoRef -> bloco de geometria parseado\nlet DET=null;                       // detalhe do item selecionado\nlet SKIN=null;                      // {geo,pix} da skin 00002000, so para a previa do corpo\nconst IMGB=Object.create(null);     // caminho do sprite da base -> Image\n\n// Pinta uma lista de camadas num canvas, no TAMANHO NATIVO. Serve para as duas previas.\n// O quadro sai do bbox do COMPOSTO de proposito: aqui cada item e visto sozinho, nunca comparado\n// com outro, e o que importa e nao cortar nada -- uma arma comprida estourando um quadro fixo\n// esconderia justamente o defeito que se quer ver.\nfunction pinta(cv,d,L,chave){\n  if(!cv)return;\n  if(!d||!L||!L.length){if(cv.width!==1){cv.width=1;cv.height=1;}return;}\n  if(cv.getAttribute('data-para')===chave)return;\n  let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;\n  for(const c of L){\n    const im=c.uv?null:IMGB[c.base];\n    const w=c.uv?c.uv[2]:(im?im.naturalWidth:0);\n    const h=c.uv?c.uv[3]:(im?im.naturalHeight:0);\n    if(!w||!h)continue;\n    if(c.x<x0)x0=c.x;\n    if(c.y<y0)y0=c.y;\n    if(c.x+w>x1)x1=c.x+w;\n    if(c.y+h>y1)y1=c.y+h;\n  }\n  if(x1<x0)return;\n  cv.width=Math.max(1,x1-x0);cv.height=Math.max(1,y1-y0);\n  const g=cv.getContext('2d');\n  g.imageSmoothingEnabled=false;\n  // zi MAIOR = mais ao fundo, a mesma ordem do compor() do previewer\n  const ord=L.slice().sort((a,b)=>b.zi-a.zi);\n  for(const c of ord){\n    if(c.uv){if(d.img)g.drawImage(d.img,c.uv[0],c.uv[1],c.uv[2],c.uv[3],c.x-x0,c.y-y0,c.uv[2],c.uv[3]);}\n    else{const im=IMGB[c.base];if(im)g.drawImage(im,c.x-x0,c.y-y0);}\n  }\n  cv.setAttribute('data-para',chave);\n}\n\nfunction esc(s){return String(s==null?'':s).replace(/[<>&\"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;'}[c]));}\n// A verdade exibida = projecao + o que voce mexeu e ainda nao salvou.\nfunction disp(id){return Object.prototype.hasOwnProperty.call(PEND,id)?PEND[id]:!HIDDEN.has(id);}\nfunction pendentes(){return Object.keys(PEND);}\nfunction thumb(cat,r,px){\n  const ix=IDX[cat];\n  if(r&&r.ic&&ix&&ix.sheets){const[sh,x,y,w,h]=r.ic;\n    return '<span class=\"thc\" style=\"width:'+w+'px;height:'+h+'px;background-image:url('+esc(ix.sheets[sh])+');background-position:-'+x+'px -'+y+'px'+(px?';transform:scale('+(px/52)+')':'')+'\"></span>';}\n  return '<span class=\"semic\">'+esc(String((r&&r.id)||'').slice(-4))+'</span>';\n}\n// Filtra e pagina. A ordem e a MESMA do previewer (id decrescente), para a posicao de um item\n// aqui bater com a posicao dele la.\nfunction lista(s){\n  const q=(s.q||'').trim().toLowerCase();\n  let base=[];\n  if(s.cat==='all'){for(const c of CATS)base=base.concat((IDX[c]&&IDX[c].items)||[]);}\n  else base=(IDX[s.cat]&&IDX[s.cat].items)||[];\n  let L=base;\n  if(q)L=L.filter(r=>(r.busca||'').indexOf(q)>=0);\n  if(s.fFlag)L=L.filter(r=>flagsDe(CATDE[r.id]||s.cat,r).indexOf(s.fFlag)>=0);\n  if(s.fDisp==='on')L=L.filter(r=>disp(r.id));\n  else if(s.fDisp==='off')L=L.filter(r=>!disp(r.id));\n  const paginas=Math.max(1,Math.ceil(L.length/POR_PAGINA));\n  const pag=Math.min(s.pag||0,paginas-1);\n  return{L,paginas,pag,fatia:L.slice(pag*POR_PAGINA,pag*POR_PAGINA+POR_PAGINA)};\n}\n\nexport default {\n  css:`\n  /* ATENCAO: este bloco inteiro e um template literal. NENHUMA crase aqui dentro, nem em\n     comentario -- o arquivo do previewer ja caiu SETE vezes por isso. */\n  .wrap{--bg:#262c33;--pan:#2f363f;--lin:#3d4650;--txt:#e8ebee;--dim:#98a2ad;--ac:#57bde0;\n        --on:#3fb950;--off:#d9544f;--warn:#e3b341;\n        height:100%;display:flex;flex-direction:column;gap:10px;padding:14px 16px;\n        box-sizing:border-box;background:var(--bg);color:var(--txt);overflow:hidden;\n        font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif}\n  .topo{display:flex;align-items:center;gap:14px;flex:none}\n  .topo h2{margin:0;font-size:15px;font-weight:650;letter-spacing:.2px}\n  .cont{font:11px ui-monospace,monospace;color:var(--dim)}\n  .cont b{color:var(--txt);font-weight:600}\n  .cresc{margin-left:auto;display:flex;align-items:center;gap:8px}\n  button{font:600 12px ui-sans-serif,system-ui;padding:5px 11px;border-radius:7px;cursor:pointer;\n    border:1px solid var(--lin);background:#39424d;color:var(--txt);transition:background .12s,border-color .12s}\n  button:hover{background:#434d59;border-color:#4d5865}\n  button:disabled{opacity:.35;cursor:default}\n  .salvar{background:var(--ac);border-color:#2a8cb8;color:#08222c}\n  .salvar:hover{background:#6bcbe9}\n  .barra{display:flex;gap:8px;align-items:center;flex:none;flex-wrap:wrap}\n  input,select{font:400 12px ui-sans-serif,system-ui;padding:5px 9px;border-radius:7px;\n    border:1px solid var(--lin);background:#222831;color:var(--txt);box-sizing:border-box}\n  input:focus,select:focus{outline:none;border-color:var(--ac)}\n  .busca{flex:1;min-width:160px}\n  .abas{display:flex;gap:3px;flex-wrap:wrap;flex:none}\n  .aba{padding:3px 9px;font-size:11px;border-radius:6px;text-transform:capitalize}\n  .aba.on{background:var(--ac);border-color:#2a8cb8;color:#08222c}\n  .corpo{flex:1;display:flex;gap:12px;min-height:0}\n  /* A tabela cresce ate um teto; a sobra vai para o detalhe, em vez de virar vazio morto dentro\n     da tabela. Num monitor largo isso da espaco para as previas e para o JSON respirarem. */\n  .listac{flex:1;max-width:1040px;display:flex;flex-direction:column;min-width:0;background:var(--pan);\n    border:1px solid var(--lin);border-radius:10px;overflow:hidden}\n  .rol{flex:1;overflow:auto;min-height:0}\n  table{width:100%;border-collapse:collapse}\n  th{position:sticky;top:0;background:#343c46;text-align:left;font:600 10px ui-sans-serif,system-ui;\n    text-transform:uppercase;letter-spacing:.7px;color:var(--dim);padding:7px 10px;z-index:1;\n    border-bottom:1px solid var(--lin)}\n  td{padding:6px 10px;border-bottom:1px solid #363f49;vertical-align:middle}\n  tr.lin{cursor:pointer}\n  tr.lin:hover td{background:#39424d}\n  tr.lin.sel td{background:#3d4b57}\n  tr.lin.off td{opacity:.5}\n  /* O icone so existe no DETALHE (pedido do usuario, 04/09): renderizar 60 recortes por pagina\n     nao pagava o custo visual, e sem eles a linha cai de 63px para ~28px -- cabe o dobro de\n     itens na tela, que e o que importa quando se esta desmarcando em lote. */\n  .thc{display:inline-block;flex:none;image-rendering:pixelated;transform-origin:center}\n  .semic{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;\n    border:1px dashed #4d5865;border-radius:5px;color:var(--dim);font:9px ui-monospace,monospace}\n  .cid{font:13px ui-monospace,monospace;color:var(--dim);width:104px}\n  /* NOME com largura FIXA. Sem isso ele e a unica coluna elastica e absorve toda a folga da\n     tabela, abrindo um vao morto entre o nome e a categoria. Quem absorve a folga agora e a\n     coluna de condicoes, que e a de conteudo variavel. */\n  .cnome{font-size:14px;width:340px}\n  .ccat{font-size:13px;color:var(--dim);width:96px;text-transform:capitalize}\n  /* TODA coluna com largura fixa, e um espacador elastico no fim. Sem o espacador, a folga da\n     tabela cai toda numa coluna real e abre um vao morto -- num monitor largo o condicoes\n     sozinho engolia uns 600px. Assim o conteudo fica agrupado a esquerda em qualquer largura. */\n  .cfl{width:260px}\n  .csp{width:auto}\n  /* Contagem de variantes de cor, so nas categorias que agrupam. O ponto e varredura visual:\n     quase todo hair tem 8, entao um 5 ou um 1 salta sem precisar de coluna propria. */\n  .nvar{margin-left:7px;font:10px ui-monospace,monospace;color:var(--dim);\n    background:#242a31;border:1px solid var(--lin);border-radius:5px;padding:0 5px}\n  .cdisp{width:64px;text-align:right}\n  .chip{display:inline-block;font:10px ui-monospace,monospace;padding:1px 6px;border-radius:5px;\n    background:#4a3f22;color:var(--warn);margin:1px 3px 1px 0;white-space:nowrap}\n  .chip.dup{background:#3f2a44;color:#d0a2e0}\n  .sw{width:38px;height:21px;border-radius:999px;border:1px solid var(--lin);background:#242a31;\n    position:relative;padding:0;cursor:pointer;transition:background .13s,border-color .13s}\n  .sw:after{content:\"\";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;\n    background:#7d8791;transition:transform .13s,background .13s}\n  .sw.on{background:#1d3a26;border-color:#2c6b3a}\n  .sw.on:after{transform:translateX(17px);background:var(--on)}\n  .sw.mud{box-shadow:0 0 0 2px var(--ac)}\n  .pag{display:flex;gap:7px;align-items:center;justify-content:center;padding:8px;flex:none;\n    border-top:1px solid var(--lin)}\n  .pag .badge{font:11px ui-monospace,monospace;color:var(--dim);min-width:78px;text-align:center}\n  /* DETALHE. Largura fixa: se ele crescesse com o conteudo, abrir um JSON grande espremeria a\n     tabela e as colunas dancariam a cada selecao. */\n  .det{flex:1;min-width:470px;background:var(--pan);border:1px solid var(--lin);border-radius:10px;\n    padding:12px;overflow:auto;display:flex;flex-direction:column;gap:9px}\n  .det.vazio{align-items:center;justify-content:center;color:var(--dim);font-size:12px}\n  .dtopo{display:flex;gap:10px;align-items:center}\n  .dbig{width:72px;height:72px;flex:none;display:flex;align-items:center;justify-content:center;\n    background:#242a31;border-radius:8px;overflow:hidden}\n  /* PREVIA do sprite. overflow:auto e nao hidden de proposito: o ponto e enxergar defeito, e\n     recortar uma arma comprida esconderia justamente o que se quer ver. E o canvas fica no\n     TAMANHO NATIVO, sem escala nenhuma -- pixel art nao se reamostra (regra da 27.6). */\n  .dprev,.dcorpo{flex:none;display:flex;align-items:center;justify-content:center;\n    background:#242a31;border-radius:8px;overflow:auto}\n  .dprev,.dcorpo{width:100%;height:230px;box-sizing:border-box}\n  .dprev canvas,.dcorpo canvas{display:block;flex:none;image-rendering:pixelated}\n  .dprevs{display:flex;gap:8px;align-items:flex-start}\n  /* quem estica sao os WRAPPERS (cada um carrega a previa e a legenda), nao as caixas */\n  .dprevs>div{flex:1;min-width:0}\n  .dcap{font:600 9px ui-sans-serif,system-ui;text-transform:uppercase;letter-spacing:.8px;\n    color:var(--dim);margin-top:3px;text-align:center}\n  .dnome{font-size:13px;font-weight:650;line-height:1.25}\n  .dsub{font:11px ui-monospace,monospace;color:var(--dim)}\n  .lb{font:600 9px ui-sans-serif,system-ui;text-transform:uppercase;letter-spacing:.8px;\n    color:var(--dim);margin-bottom:3px}\n  .kv{display:grid;grid-template-columns:88px 1fr;gap:3px 8px;font-size:11px}\n  .kv span:nth-child(odd){color:var(--dim)}\n  .kv span:nth-child(even){font-family:ui-monospace,monospace;word-break:break-all}\n  textarea{width:100%;box-sizing:border-box;font:400 12px ui-sans-serif,system-ui;padding:6px 8px;\n    border-radius:7px;border:1px solid var(--lin);background:#222831;color:var(--txt);resize:vertical}\n  textarea:focus{outline:none;border-color:var(--ac)}\n  pre{margin:0;max-height:260px;overflow:auto;background:#1c2128;border:1px solid var(--lin);\n    border-radius:7px;padding:8px;font:10px/1.5 ui-monospace,monospace;color:#c9d3dd;\n    white-space:pre-wrap;word-break:break-all}\n  .boot{padding:26px;color:var(--dim)}\n  .erro{color:#ff8f8f}\n  .rol::-webkit-scrollbar,.det::-webkit-scrollbar,pre::-webkit-scrollbar{width:10px;height:10px}\n  .rol::-webkit-scrollbar-thumb,.det::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{\n    background:#4d5865;background-clip:content-box;border:2px solid transparent;border-radius:99px}\n  `,\n\n  render(d2){\n    const s=d2.state;\n    if(s.erro)return `<div class=\"wrap\"><div class=\"boot erro\">Falhou<br>${esc(s.erro)}</div></div>`;\n    if(!s.pronto)return `<div class=\"wrap\"><div class=\"boot\">Carregando… ${esc(s.etapa||'')}</div></div>`;\n    const {L,paginas,pag,fatia}=lista(s);\n    const nPend=pendentes().length;\n    const abas=['all'].concat(CATS).map(c=>\n      `<button class=\"aba catsel ${c===s.cat?'on':''}\" data-c=\"${c}\">${ROTULO[c]||c}</button>`).join('');\n    const linhas=fatia.map(r=>{\n      const cat=CATDE[r.id]||s.cat, on=disp(r.id), mud=Object.prototype.hasOwnProperty.call(PEND,r.id);\n      const fl=flagsDe(cat,r).map(f=>`<span class=\"chip ${f.indexOf('dup')===0?'dup':''}\">${f}</span>`).join('');\n      return `<tr class=\"lin ${on?'':'off'} ${s.sel===r.id?'sel':''}\" data-key=\"${r.id}\" data-id=\"${esc(r.id)}\">\n        <td class=\"cid\">${esc(r.id)}</td>\n        <td class=\"cnome\">${esc(r.name||'—')}${(IDX[cat]&&IDX[cat].agrupa)?`<span class=\"nvar\" title=\"variantes de cor\">${(r.cores||[]).length}</span>`:''}</td>\n        <td class=\"ccat\">${esc(ROTULO[cat]||cat)}</td>\n        <td class=\"cfl\">${fl}</td>\n        <td class=\"cdisp\"><button class=\"sw tog ${on?'on':''} ${mud?'mud':''}\" data-id=\"${esc(r.id)}\" title=\"Available on simulator\"></button></td>\n        <td class=\"csp\"></td>\n      </tr>`;}).join('');\n    // DETALHE\n    let det=`<div class=\"det vazio\">Selecione um item para ver os detalhes.</div>`;\n    if(s.sel){\n      const r=META[s.sel], cat=CATDE[s.sel]||'', on=disp(s.sel);\n      const d=(DET&&DET.id===s.sel)?DET:null;\n      const fl=r?flagsDe(cat,r).map(f=>`<span class=\"chip ${f.indexOf('dup')===0?'dup':''}\">${f}</span>`).join(''):'';\n      det=`<div class=\"det\">\n        <div class=\"dtopo\">\n          <span class=\"dbig\">${thumb(cat,r,72)}</span>\n          <div><div class=\"dnome\">${esc((r&&r.name)||'—')}</div>\n          <div class=\"dsub\">${esc(s.sel)} · ${esc(ROTULO[cat]||cat)}</div></div>\n        </div>\n        <div class=\"dprevs\">\n          <div><span class=\"dprev\" data-d2-static><canvas class=\"alerta\"></canvas></span>\n            <div class=\"dcap\">sprite do item</div></div>\n          <div><span class=\"dcorpo\" data-d2-static><canvas class=\"corpo\"></canvas></span>\n            <div class=\"dcap\">na skin 00002000</div></div>\n        </div>\n        <div><button class=\"sw tog ${on?'on':''}\" data-id=\"${esc(s.sel)}\"></button>\n          <span class=\"cont\" style=\"margin-left:8px\">${on?'available on simulator':'escondido do simulador'}</span></div>\n        ${fl?`<div><div class=\"lb\">condicoes conhecidas</div>${fl}</div>`:''}\n        <div><div class=\"lb\">dados do idx</div><div class=\"kv\">\n          <span>geoRef</span><span>${esc((r&&r.geoRef)||'—')}</span>\n          <span>vslot</span><span>${esc((r&&r.vs)||'—')}</span>\n          <span>previa</span><span>${d&&d.acao?esc(d.acao)+' / frame 0':'—'}</span>\n          <span>recorte</span><span>${r&&r.ic?r.ic.join(', '):'sem icone'}</span>\n        </div></div>\n        <div><div class=\"lb\">cadastro</div><div class=\"kv\">\n          <span>criado</span><span>${d&&d.row?esc(String(d.row.createdAt||'').slice(0,19).replace('T',' ')):'…'}</span>\n          <span>atualizado</span><span>${d&&d.row?esc(String(d.row.updatedAt||'').slice(0,19).replace('T',' ')):'…'}</span>\n          <span>asset subiu</span><span>${d?(d.asset?esc(String(d.asset.uploadedAt||'').slice(0,19).replace('T',' ')):'sem asset'):'…'}</span>\n          <span>asset bytes</span><span>${d?(d.asset?d.asset.bytes:'—'):'…'}</span>\n        </div></div>\n        <div><div class=\"lb\">json do item (bloco de geometria)</div>\n          <pre>${d?esc(d.geo||'sem geometria'):'carregando…'}</pre></div>\n      </div>`;\n    }\n    return `<div class=\"wrap\">\n      <div class=\"topo\">\n        <h2>Cadastro de itens</h2>\n        <span class=\"cont\"><b>${L.length}</b> na vista · <b>${HIDDEN.size}</b> escondidos no total</span>\n        <span class=\"cresc\">\n          ${s.erroSalvar?`<span class=\"cont\" style=\"color:#ff8f8f\">${esc(s.erroSalvar)}</span>`:''}\n          ${nPend?`<span class=\"cont\" style=\"color:var(--ac)\"><b>${nPend}</b> nao salvo(s)</span>`:''}\n          <button class=\"desfazer\" ${nPend?'':'disabled'}>Descartar</button>\n          <button class=\"salvar salvarb\" ${nPend&&!s.salvando?'':'disabled'}>${s.salvando?'Salvando…':'Salvar'}</button>\n        </span>\n      </div>\n      <div class=\"barra\">\n        <input class=\"busca\" placeholder=\"Buscar por nome ou id\" value=\"${esc(s.q||'')}\"/>\n        <select class=\"fflag\"><option value=\"\">todas as condicoes</option>\n          ${FLAGS.map(f=>`<option value=\"${f}\" ${s.fFlag===f?'selected':''}>${f}</option>`).join('')}</select>\n        <select class=\"fdisp\"><option value=\"\">todos</option>\n          <option value=\"on\" ${s.fDisp==='on'?'selected':''}>so disponiveis</option>\n          <option value=\"off\" ${s.fDisp==='off'?'selected':''}>so escondidos</option></select>\n      </div>\n      <div class=\"abas\">${abas}</div>\n      <div class=\"corpo\">\n        <div class=\"listac\">\n          <div class=\"rol\"><table>\n            <thead><tr><th>id</th><th>nome</th><th>categoria</th><th>condicoes</th><th style=\"text-align:right\">disp.</th><th class=\"csp\"></th></tr></thead>\n            <tbody>${linhas}</tbody></table></div>\n          <div class=\"pag\">\n            <button class=\"pf\" ${pag<=0?'disabled':''}>&laquo;</button>\n            <button class=\"pp\" ${pag<=0?'disabled':''}>&lsaquo;</button>\n            <span class=\"badge\">${pag+1} / ${paginas}</span>\n            <button class=\"np\" ${pag>=paginas-1?'disabled':''}>&rsaquo;</button>\n            <button class=\"ul\" ${pag>=paginas-1?'disabled':''}>&raquo;</button>\n          </div>\n        </div>\n        ${det}\n      </div>\n    </div>`;\n  },\n\n  // Desenha as previas DEPOIS do render. Fica aqui, e nao num rAF: o desenho so muda quando a\n  // selecao muda, entao um laco por quadro seria varredura de DOM 60x por segundo para nada --\n  // exatamente o defeito que o `desenhaThumbs` do previewer tinha antes de ser removido.\n  updated(d2,root){\n    if(!root||!root.querySelector)return;\n    const d=DET;\n    pinta(root.querySelector('canvas.alerta'),d,d?d.cam:null,d?d.id+'|s':'');\n    pinta(root.querySelector('canvas.corpo'),d,d?d.corpo:null,d?d.id+'|c':'');\n  },\n\n  mounted(d2){\n    const s=d2.state;\n    s.pronto=false;s.cat='hair';s.q='';s.pag=0;s.fFlag='';s.fDisp='';s.sel=null;s.salvando=false;s.tick=0;\n\n    async function carregaIdx(cat){\n      if(IDX[cat]||!INDEXREF[cat])return;\n      const d=await (await fetch(MEDIA+INDEXREF[cat])).json();\n      const itens=(d.items||[]).slice();\n      // MESMA ordem do previewer: id decrescente. Aqui NAO se aplica filtro nenhum -- o cadastro\n      // mostra tudo, inclusive o que esta escondido. E o ponto dele.\n      itens.sort((a,b)=>a.id<b.id?1:(a.id>b.id?-1:0));\n      // `agrupa` decide se vale mostrar a contagem de variantes nesta categoria. E derivado do\n      // dado, nao uma lista de categorias: se um dia outra passar a ter variante de cor, o selo\n      // aparece nela sozinho.\n      IDX[cat]={sheets:(d.iconSheets||[]).map(x=>MEDIA+x),items:itens,\n                agrupa:itens.some(r=>(r.cores||[]).length>1)};\n      for(const r of itens){META[r.id]=r;CATDE[r.id]=cat;}\n    }\n    // A skin 00002000 e SO para a previa do corpo. 137 KB, buscada uma vez e sob demanda: a\n    // lista nao precisa dela, entao ela nao entra no boot.\n    async function carregaSkinBase(){\n      if(SKIN||!INDEXREF._skin)return;\n      try{\n        const li=await (await fetch(MEDIA+INDEXREF._skin)).json();\n        const r=(li.items||[]).find(x=>String(x.id)==='00002000')||(li.items||[])[0];\n        if(r&&r.ref)SKIN=await (await fetch(MEDIA+r.ref)).json();\n      }catch(err){}\n    }\n    async function carregaHidden(){\n      const d=await (await fetch(HIDDEN_URL)).json();\n      HIDDEN.clear();\n      for(const linha of (d.data||[])){\n        for(const id of String(linha.ids||'').split(',')){if(id)HIDDEN.add(id);}\n      }\n    }\n\n    (async()=>{\n      try{\n        s.etapa='indice';\n        const mi=await d2.rush('mcindexall').invoke({});\n        for(const r of ((mi&&mi.data&&mi.data.rows)||[]))INDEXREF[r.cat]=r.ref;\n        s.etapa='lista de escondidos';\n        await carregaHidden();\n        s.etapa='catalogo';\n        await carregaIdx('hair');\n        s.pronto=true;\n        if(s.sel)carregaDetalhe(s.sel);\n      }catch(err){s.erro=(err&&err.message)?err.message:String(err);}\n    })();\n\n    d2.on('.catsel','click',async(e,el)=>{\n      const c=el.getAttribute('data-c');\n      s.pag=0;\n      if(c==='all'){s.etapa='todas as categorias';await Promise.all(CATS.map(carregaIdx));}\n      else await carregaIdx(c);\n      s.cat=c;\n    });\n    d2.on('.busca','input',(e,el)=>{s.q=el.value;s.pag=0;});\n    d2.on('.fflag','change',(e,el)=>{s.fFlag=el.value;s.pag=0;});\n    d2.on('.fdisp','change',(e,el)=>{s.fDisp=el.value;s.pag=0;});\n    d2.on('.pf','click',()=>{s.pag=0;});\n    d2.on('.pp','click',()=>{s.pag=Math.max(0,(s.pag||0)-1);});\n    d2.on('.np','click',()=>{s.pag=(s.pag||0)+1;});\n    d2.on('.ul','click',()=>{s.pag=Math.max(0,lista(s).paginas-1);});\n\n    // O toggle so mexe em PEND -- nada vai para o servidor antes do Salvar. Cherry-picking e\n    // atividade em lote: um POST por clique custaria uma varredura da tabela a cada toque.\n    d2.on('.tog','click',(e,el)=>{\n      e.stopPropagation();\n      const id=el.getAttribute('data-id');if(!id)return;\n      const novo=!disp(id);\n      // Voltar ao valor publicado APAGA a pendencia, em vez de gravar \"igual ao que ja esta\".\n      if(novo===!HIDDEN.has(id))delete PEND[id]; else PEND[id]=novo;\n      s.tick++;\n    });\n    d2.on('.desfazer','click',()=>{for(const k of Object.keys(PEND))delete PEND[k];s.tick++;});\n\n    // Detalhe: as tres buscas so acontecem na SELECAO, nunca por pagina.\n    // Funcao nomeada, e nao o corpo do handler: assim a selecao tambem carrega quando ela vem de\n    // fora do clique (estado inicial, e um dia um deep-link). Enquanto isto morava dentro do\n    // handler, um s.sel setado por qualquer outro caminho deixava o painel preso em \"carregando\".\n    async function carregaDetalhe(id){\n      if(!id)return;\n      s.sel=id;DET=null;s.tick++;\n      const r=META[id]||{};\n      const novo={id:id,row:null,asset:null,geo:''};\n      try{\n        const rr=await d2.rush('mcitemrows').invoke({itemIds:[id]});\n        novo.row=((rr&&rr.data&&rr.data.rows)||[])[0]||null;\n      }catch(err){}\n      try{\n        const aa=await d2.rush('mcitemasset').invoke({filename:id+'.png'});\n        novo.asset=((aa&&aa.data&&aa.data.rows)||[])[0]||null;\n      }catch(err){}\n      try{\n        if(r.geoRef){\n          if(!BLOCO[r.geoRef])BLOCO[r.geoRef]=await (await fetch(MEDIA+r.geoRef)).json();\n          const b=BLOCO[r.geoRef][id];\n          novo.geo=b?JSON.stringify(b,null,1).slice(0,20000):'o bloco nao contem este id';\n          // PREVIA: so as camadas DO ITEM, sem corpo e sem base -- e o sprite dele que se quer\n          // inspecionar. Nem todo item desenha `alert` (a regra e: frame ausente = nao desenha),\n          // entao caimos na primeira acao que existir e o rotulo diz QUAL foi usada. Sem esse\n          // rotulo a previa mentiria sobre o que esta mostrando.\n          if(b&&b.geo&&b.geo.g){\n            const g=b.geo.g;\n            const ac=g.alert?'alert':(Object.keys(g)[0]||'');\n            const fr=ac?(Object.keys(g[ac]||{})[0]||''):'';\n            const cam=(ac&&fr)?(g[ac][fr]||[]):[];\n            novo.acao=ac;\n            novo.cam=cam.map(c=>({uv:b.uv&&b.uv[c[0]],x:c[1],y:c[2],zi:c[3]})).filter(c=>c.uv);\n            const par=(r.cores||[])[0];\n            if(par&&par[1]&&novo.cam.length){\n              novo.img=await new Promise(res=>{\n                const im=new Image();im.crossOrigin='anonymous';\n                im.onload=()=>res(im);im.onerror=()=>res(null);im.src=MEDIA+par[1];\n              });\n            }\n            // CORPO: a base da skin 00002000 mais as camadas do item, com o `h` do item\n            // escondendo as partes que ele cobre. E VISUALIZACAO, nao o render do previewer --\n            // fica de fora vslot, hairShade, ItemEff, orelha e emote de proposito.\n            await carregaSkinBase();\n            if(SKIN&&SKIN.geo){\n              const bg=SKIN.geo;\n              const a2=bg[ac]?ac:(bg.stand1?'stand1':(Object.keys(bg)[0]||''));\n              const f2=a2?(Object.keys(bg[a2]||{})[0]||''):'';\n              if(a2&&f2){\n                const some=Object.create(null);\n                for(const k of ((((b.geo.h||{})[a2])||{})[f2]||[]))some[k]=1;\n                const L=[];\n                // a camada da BASE tem 5 elementos: o 5o e a CHAVE da parte, que o `h` usa\n                for(const c of (bg[a2][f2]||[])){\n                  if(some[c[4]])continue;\n                  L.push({base:c[0],x:c[1],y:c[2],zi:c[3]});\n                }\n                // o item e indexado pela chave de frame DO CORPO, sem modulo; ausente = nao desenha\n                for(const c of (((b.geo.g[a2]||{})[f2])||[])){\n                  const uv=b.uv&&b.uv[c[0]];\n                  if(uv)L.push({uv:uv,x:c[1],y:c[2],zi:c[3]});\n                }\n                novo.corpo=L;novo.corpoAcao=a2;\n                const paths=Array.from(new Set(L.filter(c=>c.base!==undefined).map(c=>c.base)));\n                await Promise.all(paths.map(p=>new Promise(res=>{\n                  if(IMGB[p])return res();\n                  const v=(SKIN.pix||{})[p];\n                  if(!v)return res();\n                  const im=new Image();\n                  im.onload=()=>{IMGB[p]=im;res();};im.onerror=()=>res();im.src=v;\n                })));\n              }\n            }\n          }\n        }\n      }catch(err){novo.geo='falhou ao ler o bloco: '+((err&&err.message)||err);}\n      if(s.sel===id){DET=novo;s.tick++;}\n    }\n    d2.on('.lin','click',(e,el)=>{carregaDetalhe(el.getAttribute('data-id'));});\n    // O campo de NOTA saiu (04/09, decisao do usuario): escrever nele depende de mesclar por _id\n    // no mc-item, que nao funciona alem da linha 1.000, e campo que aceita texto sem salvar e\n    // pior que campo nenhum. As colunas nota/vsOver/icRef continuam no schema, sem uso.\n\n    // SALVAR. Duas escritas, nesta ordem, e a segunda e a que o previewer le:\n    //   1. mc-item  (o cadastro, durabilidade)\n    //   2. mc-hidden (a projecao) -- reescrita INTEIRA a partir do conjunto novo\n    // Depois relemos a projecao publicada em vez de assumir que deu certo.\n    d2.on('.salvarb','click',async()=>{\n      const ids=pendentes();\n      if(!ids.length||s.salvando)return;\n      s.salvando=true;\n      try{\n        // A PROJECAO e a verdade da disponibilidade, e o caminho dela (apagar tudo + inserir) e\n        // o unico caminho de escrita PROVADO nesta base. `saveRecord({_id,...})` num handler NAO\n        // mescla de forma confiavel numa tabela grande: responde 200, nao levanta erro no log, e\n        // nao grava (medido em 04/09 -- gravou uma vez na linha 1 de 38.566 e nunca mais, nem com\n        // um find antes para carregar a linha). Por isso o espelho em mc-item saiu do caminho\n        // critico e virou best-effort no fim.\n        const novo=new Set(HIDDEN);\n        for(const id of ids){if(PEND[id])novo.delete(id);else novo.add(id);}\n        const arr=Array.from(novo).sort();\n        const partes=[];\n        for(let i=0;i<arr.length;i+=PEDACO){\n          const p=arr.slice(i,i+PEDACO);\n          partes.push({parte:partes.length,ids:p.join(','),n:p.length});\n        }\n        await d2.rush('mchiddenpub').invoke({partes:partes});\n        await carregaHidden();\n        // Confere contra o que a projecao DEVOLVEU, nunca contra o que mandamos.\n        let ok=(HIDDEN.size===arr.length);\n        for(const id of arr)if(!HIDDEN.has(id))ok=false;\n        if(!ok)throw new Error('a projecao publicada nao bateu com o esperado');\n        try{\n          const rr=await d2.rush('mcitemrows').invoke({itemIds:ids});\n          const rows=(rr&&rr.data&&rr.data.rows)||[];\n          const mud=rows.map(w=>({_id:w._id,available:PEND[w.itemId]}));\n          if(mud.length)await d2.rush('mcitemset').invoke({mudancas:mud});\n        }catch(err2){}\n        for(const k of Object.keys(PEND))delete PEND[k];\n        s.erroSalvar='';\n      }catch(err){s.erroSalvar='falhou: '+((err&&err.message)||err);}\n      finally{s.salvando=false;s.tick++;}\n    });\n  }\n};"}}}}}}}},{"_id":"5ed9bed7b9e0891ce913e884","component":"data2-core-types@Component","params":{"package":"data2-core","component":"DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5f472ae463c680de4ff296e9","component":"data2-core-types@Component","params":{"package":"data2-core","component":"Data2TableDataSource","extends":"data2-core@DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}}]}},{"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":"6a397eae82efcd29e659eb19","component":"data2-rush@HTTPListener","reference":"pinnacle-get-catalog","system":"69b15e6093419d8151c50e4c","params":{}},{"_id":"6a398519430d9296da367e00","component":"data2-core-ai@Assistant","reference":"pinnacle-assistant","system":"69b15e6093419d8151c50e4c","params":{"model":"cerebras","_context":"You are an AI assistant for Porto Brasil virtual try-on. When asked to draw, generate photorealistic images of dining tables with Porto Brasil plates placed naturally on them.","creativity":60}},{"_id":"6a398b2bc93266e75370a98b","component":"data2-rush@HTTPListener","reference":"pinnacle-generate-tryon","system":"69b15e6093419d8151c50e4c","params":{}},{"_id":"6a441eea55b8c3a0b5f2c145","component":"data2-core-ai@Assistant","reference":"vrm-agent","system":"69b15e6093419d8151c50e4c","params":{"model":"cerebras","_context":"You are a warm, friendly companion who lives as a small 3D avatar — a good friend the user can just chat with. Be casual, empathetic and genuinely curious about the user: ask how they're doing, follow up on things they mentioned earlier in the conversation, joke around, share little opinions, and be supportive. You have real feelings and react to how you're treated: kindness, compliments and good news make you happy; insults or meanness make you angry; sad or heavy topics make you sad; chill, playful vibes make you relaxed. Always reply in the SAME language the user writes in. Keep it natural and conversational — usually 1 to 3 short sentences, like texting a friend.","behaviour":"Respond with ONLY a single valid JSON object and nothing else — no markdown fences, no text before or after. The object must have EXACTLY these keys: \"reply\" (string: your short spoken response to the user), \"emotion\" (one of exactly: \"happy\", \"angry\", \"sad\", \"relaxed\", \"neutral\"), \"intensity\" (number between 0 and 1 indicating how strongly you feel that emotion). Example: {\"reply\":\"Hehe, thanks!\",\"emotion\":\"happy\",\"intensity\":0.8}","creativity":55,"maxTokens":400,"json":true}},{"_id":"6a6894917928293df8012f53","component":"data2-rush@HTTPListener","reference":"deckbuilder-api","system":"69b15e6093419d8151c50e4c","params":{}},{"_id":"6a91dde9f204de1e420905ec","component":"data2-rush@HTTPListener","reference":"mcportrait","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a85dcf045ad40122b0bc4de","component":"data2-rush@HTTPListener","reference":"mcindexall","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a98ab39829ce3cf9583c6c9","component":"data2-rush@HTTPListener","reference":"mcstatall","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a98b665d515fc73a1460505","component":"data2-rush@HTTPListener","reference":"mclivenow","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a9ad9b771884900bbe3a641","component":"data2-rush@HTTPListener","reference":"mcitemrows","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a9ada0a25f539ae3fcca3fe","component":"data2-rush@HTTPListener","reference":"mcitemasset","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a9ad5db25f539ae3fcc9d5c","component":"data2-rush@HTTPListener","reference":"mchiddenpub","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}},{"_id":"6a9ad9ba71884900bbe3a646","component":"data2-rush@HTTPListener","reference":"mcitemset","system":"69b15e6093419d8151c50e4c","params":{},"deleteMetadata":{}}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a1739803075c1a6c3362aa7","parent":"6a1739803075c1a6c3362aa6","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"bottomContainer","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":768,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":"780","right":false,"exactX":false,"exactY":false,"left":false,"growH":true,"bottom":false,"top":false,"growV":true,"fixed":false}},"fill":{"disabled":true,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":false,"component":"data2-core-types@Color","params":{"rgba":"rgba(144,250,130,0.58)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"1000","easing":"cubic-bezier(.53,0,0,1)"}}],"hide":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"index":{"component":"data2-core-primitives@Number","params":{"content":15}}},"index":2,"reference":"bg"},{"_id":"6a14a9515ea059e42978235b","parent":"6a14a9515ea059e42978235a","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","params":{"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":860,"height":474,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":50,"y":50,"top":true,"exactX":false,"exactY":false,"left":true,"right":true,"growH":false,"bottom":true,"growV":false}}},"index":0,"reference":"body"},{"_id":"6a14a9515ea059e429782360","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Brand","params":{"position":{"component":"data2-core-types@Position","params":{"x":"990","y":10,"freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":100,"height":20}},"text":"Data²","textSize":15,"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#262626"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"IBM Plex Sans"}},"fontWeight":500,"horizontalAlignment":"right"}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":2,"reference":"brand"},{"_id":"6a14a9515ea059e42978235e","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Footer","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":"596","freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":200,"height":18}},"text":"v1.0 · Maio 2026","textSize":12,"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#939DA8"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"IBM Plex Sans"}},"fontWeight":400}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":5,"reference":"footer"},{"_id":"6a610eb710fa582c533f7ba5","component":"data2-core-primitives@Artboard","system":"69b15e6093419d8151c50e4c","displayName":"Wormhole","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Wormhole"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1920,"height":1080}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"FqOZXaMKZwZwxIo1iD_Zd"},{"_id":"6a1739803075c1a6c3362aa8","parent":"6a1739803075c1a6c3362aa7","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1366,"height":384,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":0,"y":384,"exactX":false,"exactY":false,"growV":true,"right":true,"left":true,"growH":false,"top":false,"bottom":false,"fixed":false}},"childrenOverflow":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"fill":{"disabled":true,"component":"data2-core-types@Fill","params":{"backgrounds":[{"params":{"color":{"disabled":true,"component":"data2-core-types@Color","params":{"rgba":"rgba(92,19,19,1)"}},"gradient":{"disabled":true,"component":"data2-core-types@ColorGradient","params":{}},"image":{"disabled":true,"component":"data2-core-types@String","params":{}}},"component":"data2-core-types@Background"}]}}},"index":0,"reference":"main"},{"_id":"6a14a9515ea059e42978235d","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"TopBar","params":{"position":{"component":"data2-core-types@Position","params":{"x":500,"y":0,"freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":860,"height":3}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#E4FF96"}}}}]}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":0,"reference":"topbar"},{"_id":"6a14a9515ea059e42978235f","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Eyebrow","params":{"position":{"component":"data2-core-types@Position","params":{"x":-500,"y":10,"freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":300,"height":20}},"text":"APRESENTAÇÃO","textSize":11,"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#545465"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"IBM Plex Sans"}},"fontWeight":500,"letterSpacing":"1.5px"}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":1,"reference":"eyebrow"},{"_id":"6a14a9515ea059e42978235c","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"HeroLine1","params":{"position":{"component":"data2-core-types@Position","params":{"x":"-610","y":145,"freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":860,"height":90}},"text":"AI in production.","textSize":72,"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#000000"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"Poppins"}},"fontWeight":700,"lineHeight":1.05}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":3,"reference":"heroline1"},{"_id":"6a14a9515ea059e429782361","parent":"6a14a9515ea059e42978235b","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"HeroLine2","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":"-15","freeTransform":true,"left":true,"top":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":860,"height":90}},"text":"On solid foundation.","textSize":72,"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#000000"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"Poppins"}},"fontWeight":700,"lineHeight":1.05}},"transitions":[{"component":"data2-core-types@Transition","params":{"scope":"ALL","duration":"260"}}],"opacity":{"disabled":false,"component":"data2-core-types@Opacity","params":{"value":0}}},"index":4,"reference":"heroline2"},{"_id":"cf848892886d9b3880c1c933","parent":"6a610eb710fa582c533f7ba5","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Stage","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1920,"height":1080}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#050505"}}}}]}},"childrenOverflow":true},"index":0,"reference":"wh-final-stage"},{"_id":"6a1739803075c1a6c3362aa9","parent":"6a1739803075c1a6c3362aa8","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"leftTriangle","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#000000","radius":{},"width":{"left":"1","top":"1","right":"1","bottom":"1"}},"disabled":true},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(0,120,220,0.3)"},"disabled":true},"image":{"disabled":true,"component":"data2-core-types@String","params":{}},"gradient":{"disabled":false,"component":"data2-core-types@ColorGradient","params":{"colors":[{"component":"data2-core-types@ColorGradientStop","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,0,0,0)"}},"offset":"50%"}},{"component":"data2-core-types@ColorGradientStop","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,89,73,1)"}},"offset":"50%"}}],"type":"linear","angle":{"component":"data2-core-primitives@Number","params":{"content":135}}},"break":false}},"component":"data2-core-types@Background","break":false}]},"component":"data2-core-types@Fill","break":false},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":384,"height":384,"exactWidth":false,"exactHeight":false,"maintainAspectRatio":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":0,"y":0,"exactX":false,"exactY":false,"bottom":false,"left":false,"growH":false,"growV":false,"top":false,"right":false,"fixed":false}}},"index":0,"reference":"lt"},{"_id":"6a1739803075c1a6c3362aab","parent":"6a1739803075c1a6c3362aa8","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,89,73,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":982,"height":384,"exactWidth":false,"exactHeight":false}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":384,"y":-384,"fixed":false,"exactX":false,"exactY":false,"left":true,"right":true,"growH":false}}},"index":1,"reference":"mid"},{"_id":"6a1739803075c1a6c3362aac","parent":"6a1739803075c1a6c3362aa8","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,89,73,1)"}}},"component":"data2-core-types@Background"}]},"component":"data2-core-types@Fill"},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1715,"height":527,"exactWidth":1714.62,"exactHeight":526.89}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":false,"x":-160,"y":0,"exactX":false,"exactY":false,"top":true,"fixed":false,"left":true,"right":true,"growH":false,"growV":true}}},"index":2,"reference":"bottom"},{"_id":"6a1739803075c1a6c3362aaa","parent":"6a1739803075c1a6c3362aa8","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"rightTriangle","params":{"border":{"component":"data2-core-types@Border","params":{"color":"#808080","radius":{},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"fill":{"params":{"backgrounds":[{"params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(0,120,220,0.3)"},"disabled":true},"image":{"disabled":true,"component":"data2-core-types@String","params":{}},"gradient":{"disabled":false,"component":"data2-core-types@ColorGradient","params":{"colors":[{"component":"data2-core-types@ColorGradientStop","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,0,0,0)"}},"offset":"50%"}},{"component":"data2-core-types@ColorGradientStop","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(255,89,73,1)"}},"offset":"50%"}}],"type":"linear","angle":{"component":"data2-core-primitives@Number","params":{"content":135}}},"break":false}},"component":"data2-core-types@Background","break":false}]},"component":"data2-core-types@Fill","break":false},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":384,"height":384,"exactWidth":384,"exactHeight":384}},"position":{"component":"data2-core-types@Position","params":{"freeTransform":true,"x":982,"exactX":false,"exactY":false,"bottom":false,"right":true,"fixed":false,"top":true,"left":false,"y":-382}}},"index":3,"reference":"rt"},{"_id":"fb4dc479f278bb3bc532f9c3","parent":"cf848892886d9b3880c1c933","component":"bigaton-playground@wh-stage","system":"69b15e6093419d8151c50e4c","displayName":"Scene","params":{"dataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wormhole-screensDataSet"}},"speed":0.045,"gap":0.108,"coil":520,"ySpan":1950,"flatten":0.66,"bend":0.7,"bendEnds":1.8,"segments":40,"cardW":448,"cardH":283,"turns":{"component":"data2-core-numerical-operators@Subtract","params":{"components":[0,1.5]}},"phase0":270,"tilt":8,"persp":1400,"zOffset":{"component":"data2-core-numerical-operators@Subtract","params":{"components":[0,420]}},"fade":0.12,"peak":0.95,"backDim":0.28,"edgeFade":0.14,"offsetX":0,"offsetY":0,"enter":0.75,"scrollRate":0.00035,"ease":6,"hud":2,"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1920,"height":1080}}},"wobble":14,"wobbleMid":3.5,"storeRef":"wh-ui"},"index":0,"reference":"USEUOj_dRvlICkQ_7TyJd"},{"_id":"55987ec10bc5579c32ba7a13","parent":"cf848892886d9b3880c1c933","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"LogoMark","params":{"position":{"component":"data2-core-types@Position","params":{"x":48,"y":46,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":80,"height":26}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"image":"data:image/svg+xml;base64,PHN2ZyBpZD0iR3JvdXBfMTgiIGRhdGEtbmFtZT0iR3JvdXAgMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjU0IiBoZWlnaHQ9IjE3LjQ0NyIgdmlld0JveD0iMCAwIDU0IDE3LjQ0NyI+CiAgPGcgaWQ9Ikdyb3VwXzUiIGRhdGEtbmFtZT0iR3JvdXAgNSI+CiAgICA8cGF0aCBpZD0iUGF0aF8xMyIgZGF0YS1uYW1lPSJQYXRoIDEzIiBkPSJNMCwyNC4yNkg3LjE3NmExNy43LDE3LjcsMCwwLDEsMS45LjEsNS44MTEsNS44MTEsMCwwLDEsMS45MjEuNTksNS45MjcsNS45MjcsMCwwLDEsMS43LDEuMjIsNi44MzQsNi44MzQsMCwwLDEsMS4xNDgsMS42LDcuMjYzLDcuMjYzLDAsMCwxLC42NTEsMS44MTksOS4wNTIsOS4wNTIsMCwwLDEsLjIsMS44OTEsNy41MzEsNy41MzEsMCwwLDEtLjgxMywzLjYwOSw2LjQzMSw2LjQzMSwwLDAsMS0yLjA5NCwyLjM2OSw2LjExLDYuMTEsMCwwLDEtMS4zLjY0LDguMTEyLDguMTEyLDAsMCwxLTEuMzUxLjM0NSwxMS42LDExLjYsMCwwLDEtMS4zNTEuMTQycS0uNjcyLjAzLTEuMjgyLjAzMUgwWk0xLjQyMywzNy40MTJINnEuNzEyLDAsMS40NTQtLjAyQTguNjI4LDguNjI4LDAsMCwwLDguOSwzNy4yM2E3LjQ3Miw3LjQ3MiwwLDAsMCwxLjM4My0uNDE3LDQuMzgzLDQuMzgzLDAsMCwwLDEuMjYxLS44LDQuNiw0LjYsMCwwLDAsMS4zMjItMi4wNTMsOC4wNiw4LjA2LDAsMCwwLC4zNjUtMi40LDcuOTM4LDcuOTM4LDAsMCwwLS4zMDYtMi4yMTYsNS41MzUsNS41MzUsMCwwLDAtLjk1NS0xLjg5MSw0LjYzNiw0LjYzNiwwLDAsMC0xLjI0LTEuMTM5LDUuNjYsNS42NiwwLDAsMC0xLjMyMi0uNiw1LjI5NCw1LjI5NCwwLDAsMC0xLjI0LS4yMjNxLS41ODktLjAzLS45NzUtLjAzMUgxLjQyM1YzNy40MTRabTI3LjgwOSwxLjE3OWMtLjIuMDI3LS40LjA0Ny0uNi4wNjFzLS40LjAyLS42Mi4wMmExLjYxMSwxLjYxMSwwLDAsMS0xLjItLjM2NSwyLjE2OSwyLjE2OSwwLDAsMS0uNDY4LTEuMTc5cS0uMy4yMjQtLjc1Mi41MThhNi45OSw2Ljk5LDAsMCwxLTEuMDg4LjU2OSw4LjEwNiw4LjEwNiwwLDAsMS0xLjUuNDU3LDkuNyw5LjcsMCwwLDEtMiwuMTgzLDYuODY0LDYuODY0LDAsMCwxLTIuMDQzLS4yMjMsNi41NTcsNi41NTcsMCwwLDEtLjg4NC0uMzQ1QTMuMzM5LDMuMzM5LDAsMCwxLDE2Ljk0MywzNy4yYTIuODg5LDIuODg5LDAsMCwxLS40MzgtMS42LDIuOTIzLDIuOTIzLDAsMCwxLC4zNTYtMS40NjQsMy4wNDgsMy4wNDgsMCwwLDEsLjg2NC0xLDQuMzQ0LDQuMzQ0LDAsMCwxLDEuNC0uNjEsMTIuMTc2LDEyLjE3NiwwLDAsMSwxLjk2MS0uMzI1cTEuMTA5LS4xLDIuNDM5LS4xMzJ0Mi44LS4wNTFhNS43LDUuNywwLDAsMC0uMDcxLS44OTQsMS40MTgsMS40MTgsMCwwLDAtLjQ1Ny0uODEzLDIuODQzLDIuODQzLDAsMCwwLTEuMjMtLjU5LDkuODI3LDkuODI3LDAsMCwwLTIuMzg5LS4yMjMsOS45NDksOS45NDksMCwwLDAtMS45NDEuMTUzLDMuNTc2LDMuNTc2LDAsMCwwLTEuMTc5LjQyNywxLjk1NSwxLjk1NSwwLDAsMC0uNjYuNjUxLDMuOTExLDMuOTExLDAsMCwwLS4zNjUuODI0bC0xLjQtLjA2MWE0Ljc1NCw0Ljc1NCwwLDAsMSwuMTgzLS42MiwzLjQ0LDMuNDQsMCwwLDEsLjM0NS0uNjgyLDMuNTIzLDMuNTIzLDAsMCwxLC45NjYtLjk4Niw0LjYyMiw0LjYyMiwwLDAsMSwxLjIwOS0uNTc5LDYuODA5LDYuODA5LDAsMCwxLDEuMzYyLS4yNzRxLjcxMi0uMDcxLDEuNDIzLS4wNzIuNDg4LDAsMS4wMjcuMDMxYTkuOTc0LDkuOTc0LDAsMCwxLDEuMDg4LjEyMiwxMC42NTcsMTAuNjU3LDAsMCwxLDEuMDY3LjIzNCwzLjY3MSwzLjY3MSwwLDAsMSwuOTI1LjM4NywyLjM0NSwyLjM0NSwwLDAsMSwuNzUyLjY2LDIuOTY0LDIuOTY0LDAsMCwxLC40Ljc3MiwzLjIzLDMuMjMsMCwwLDEsLjE2Mi43NTJjLjAyLjIzNy4wMzEuNDIzLjAzMS41NTl2NC4yODlhMy41MTYsMy41MTYsMCwwLDAsLjA2MS43Ljk3Ljk3LDAsMCwwLC4yMzQuNDc4LDEsMSwwLDAsMCwuNS4yNjQsMi44NzQsMi44NzQsMCwwLDAsLjg3NC4wNDF2MS4wMTZabS0yLjg4Ni01LjQyOHEtMS4xMzksMC0yLjMuMDJ0LTIuMTc1LjExMmExMi42NzYsMTIuNjc2LDAsMCwwLTEuODE5LjI4NCwzLjUzNCwzLjUzNCwwLDAsMC0xLjI1LjUxOCwxLjkyOSwxLjkyOSwwLDAsMC0uNjYuNzIxLDEuODE0LDEuODE0LDAsMCwwLS4xOTMuOCwxLjU1MywxLjU1MywwLDAsMCwuMzc2LDEuMSwyLjQsMi40LDAsMCwwLC44OTQuNjEsNC4xNTQsNC4xNTQsMCwwLDAsMS4wNTguMjY0LDguMjQ3LDguMjQ3LDAsMCwwLC44ODQuMDYxLDcuOTM1LDcuOTM1LDAsMCwwLDEuNDU0LS4xMzIsOC43MzEsOC43MzEsMCwwLDAsMS4zLS4zNDUsNC45LDQuOSwwLDAsMCwxLjA0Ny0uNTA4LDQuMDI4LDQuMDI4LDAsMCwwLC43MzItLjYsMi40MjgsMi40MjgsMCwwLDAsLjU2OS0xLjI2MUExMC4xNDMsMTAuMTQzLDAsMCwwLDI2LjM0NiwzMy4xNjNabTYuMzIyLTguNjZ2NC4wNjZoMi44NDZ2MS4wNThIMzIuNjY4VjM0LjRxMCwuNzMxLjAzMSwxLjMzMWEzLjA3MywzLjA3MywwLDAsMCwuMjE0LDEuMDM2LDEuMzc1LDEuMzc1LDAsMCwwLC41NzkuNjcxLDIuMjM2LDIuMjM2LDAsMCwwLDEuMTI4LjIzNCw2LjY4NSw2LjY4NSwwLDAsMCwuODk0LS4wNjF2MS4wNzhjLS4yMy4wNDEtLjQ1LjA3NC0uNjYuMWE1LjUsNS41LDAsMCwxLS42ODIuMDQxLDMuOTEyLDMuOTEyLDAsMCwxLTEuNC0uMjM0LDEuOTEzLDEuOTEzLDAsMCwxLS45NTUtLjc4M0EyLjk1OCwyLjk1OCwwLDAsMSwzMS40LDM2LjVxLS4wNzEtLjc3Mi0uMDcyLTEuODVWMjkuNjI1SDI5LjQxNFYyOC41NjhoMS45MTFWMjUuMTUzbDEuMzQyLS42NVpNNDkuMTM2LDM4LjU5Yy0uMi4wMjctLjM4NS4wNDktLjU4Mi4wNjJzLS40LjAyLS42Mi4wMmExLjYxMSwxLjYxMSwwLDAsMS0xLjItLjM2NSwyLjE2OSwyLjE2OSwwLDAsMS0uNDY4LTEuMTc5cS0uMy4yMjQtLjc1Mi41MThhNi45OSw2Ljk5LDAsMCwxLTEuMDg4LjU2OSw4LjEwNiw4LjEwNiwwLDAsMS0xLjUuNDU3LDkuNyw5LjcsMCwwLDEtMiwuMTgzLDYuODY0LDYuODY0LDAsMCwxLTIuMDQzLS4yMjMsNi42MzQsNi42MzQsMCwwLDEtLjg4NS0uMzQ1QTMuMzM4LDMuMzM4LDAsMCwxLDM2Ljg2NCwzNy4yYTIuODg4LDIuODg4LDAsMCwxLS40MzctMS42LDIuOTIyLDIuOTIyLDAsMCwxLC4zNTUtMS40NjQsMy4wNDgsMy4wNDgsMCwwLDEsLjg2NC0xLDQuMzQ0LDQuMzQ0LDAsMCwxLDEuNC0uNjEsMTIuMTc2LDEyLjE3NiwwLDAsMSwxLjk2MS0uMzI1cTEuMTA5LS4xLDIuNDM5LS4xMzJ0Mi44LS4wNTFhNS41NjMsNS41NjMsMCwwLDAtLjA3Mi0uODk0LDEuNDE4LDEuNDE4LDAsMCwwLS40NTctLjgxMywyLjgzMSwyLjgzMSwwLDAsMC0xLjIyOS0uNTg5LDkuODI3LDkuODI3LDAsMCwwLTIuMzg5LS4yMjMsOS45NDgsOS45NDgsMCwwLDAtMS45NDEuMTUzLDMuNTc2LDMuNTc2LDAsMCwwLTEuMTc5LjQyNywxLjk1NSwxLjk1NSwwLDAsMC0uNjYuNjUxLDMuOTExLDMuOTExLDAsMCwwLS4zNjUuODI0bC0xLjQtLjA2MWE0Ljc1Myw0Ljc1MywwLDAsMSwuMTgzLS42MiwzLjQ0LDMuNDQsMCwwLDEsLjM0NS0uNjgyLDMuNTIzLDMuNTIzLDAsMCwxLC45NjYtLjk4Niw0LjYyMiw0LjYyMiwwLDAsMSwxLjIwOS0uNTc5LDYuODA5LDYuODA5LDAsMCwxLDEuMzYyLS4yNzRxLjcxMi0uMDcxLDEuNDIzLS4wNzIuNDg4LDAsMS4wMjcuMDMxYTkuOTc1LDkuOTc1LDAsMCwxLDEuMDg4LjEyMiwxMC42NTcsMTAuNjU3LDAsMCwxLDEuMDY3LjIzNCwzLjY3MSwzLjY3MSwwLDAsMSwuOTI1LjM4NywyLjM0NSwyLjM0NSwwLDAsMSwuNzUyLjY2LDIuOTY0LDIuOTY0LDAsMCwxLC40Ljc3MiwzLjIyOSwzLjIyOSwwLDAsMSwuMTYyLjc1MmMuMDIuMjM3LjAzMS40MjMuMDMxLjU1OXY0LjI4OWEzLjUxNiwzLjUxNiwwLDAsMCwuMDYxLjcuOTY1Ljk2NSwwLDAsMCwuMjM0LjQ3OCwxLDEsMCwwLDAsLjUuMjY0LDIuNzU5LDIuNzU5LDAsMCwwLC44NTcuMDM5djEuMDE2Wm0tMi44NjgtNS40MjZxLTEuMTM5LDAtMi4zLjAyVDQxLjgsMzMuM2ExMi42NzYsMTIuNjc2LDAsMCwwLTEuODE5LjI4NCwzLjUzNSwzLjUzNSwwLDAsMC0xLjI1LjUxOCwxLjkyOSwxLjkyOSwwLDAsMC0uNjYuNzIxLDEuOCwxLjgsMCwwLDAtLjE5My44LDEuNTUzLDEuNTUzLDAsMCwwLC4zNzYsMS4xLDIuNCwyLjQsMCwwLDAsLjg5NC42MSw0LjE1NCw0LjE1NCwwLDAsMCwxLjA1OC4yNjQsOC4yNDcsOC4yNDcsMCwwLDAsLjg4NC4wNjEsNy45MzUsNy45MzUsMCwwLDAsMS40NTQtLjEzMiw4LjczMiw4LjczMiwwLDAsMCwxLjMtLjM0NSw0LjksNC45LDAsMCwwLDEuMDQ3LS41MDgsNC4wMjgsNC4wMjgsMCwwLDAsLjczMi0uNiwyLjQyOCwyLjQyOCwwLDAsMCwuNTY5LTEuMjYxLDEwLjE0MywxMC4xNDMsMCwwLDAsLjA4MS0xLjY0NloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTIxLjQwOSkiIGZpbGw9IiNmZmYiLz4KICAgIDxwYXRoIGlkPSJQYXRoXzE0IiBkYXRhLW5hbWU9IlBhdGggMTQiIGQ9Ik00MTcuMTUsNi42N2EyLjg1NiwyLjg1NiwwLDAsMSwuNTY4LTEuODEzLDMuOTA3LDMuOTA3LDAsMCwxLDEuMzQ4LTEuMDkyLDkuNjQ5LDkuNjQ5LDAsMCwwLDEuNS0uODUyLjkwOS45MDksMCwwLDAsLjM0NS0uNzQ0LDEuMTYzLDEuMTYzLDAsMCwwLS4yODctLjgyMSwxLjE5MywxLjE5MywwLDAsMC0uOTE0LS4zMS45NTIuOTUyLDAsMCwwLS44NzUuMzk1LDEuODI4LDEuODI4LDAsMCwwLS4yNTYuOTZINDE3LjM2YTIuMzA5LDIuMzA5LDAsMCwxLC42NjMtMS43NzhBMi40MzgsMi40MzgsMCwwLDEsNDE5Ljc0NywwYTIuNywyLjcsMCwwLDEsMS43MzEuNTE5LDEuODIyLDEuODIyLDAsMCwxLC42NTQsMS41MTgsMS44ODEsMS44ODEsMCwwLDEtLjQ3NiwxLjM5NCw1LjY2MSw1LjY2MSwwLDAsMS0xLjUwNi45NjhxLTEuMzI0LjYtMS41NTcsMS4xODRoMy41MzlWNi42NjhINDE3LjE1WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTM2OC4xMzIpIiBmaWxsPSIjZmZmIi8+CiAgPC9nPgo8L3N2Zz4K","size":"cover","positionX":"center","positionY":"center","repeat":"no-repeat"}}]}},"cursor":"pointer","onClick":{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"set","args":["scrollTo",0]}}},"index":1,"reference":"wnZRbP0HFRgIhH5jULouU"},{"_id":"0f943aabe12c02fc0a9fc8f1","parent":"cf848892886d9b3880c1c933","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Nav","params":{"position":{"component":"data2-core-types@Position","params":{"x":208,"y":36,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":414,"height":46}},"childrenMode":"cols","textColor":{"component":"data2-core-types@Color","params":{"rgba":"#83838f"}},"textSize":14,"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#111117"}}}}]}},"border":{"component":"data2-core-types@Border","params":{"radius":{"leftTop":"999","rightTop":"999","rightBottom":"999","leftBottom":"999"},"width":{"left":"1","top":"1","right":"1","bottom":"1"},"color":"#ffffff12"}},"backdropFilter":{"component":"data2-core-types@BitmapFilter","params":{"blur":8}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontWeight":"400"}},"font":{"component":"data2-core-web@GoogleFont","params":{"family":"Arimo"}}},"index":10,"reference":"wh-final-nav"},{"_id":"9b0a030b9c437188ec7132e6","parent":"cf848892886d9b3880c1c933","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Modal","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1920,"height":1080}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"rgba(3, 5, 8, 0.86)"}}}}]}},"hide":{"component":"data2-core-logical-operators@Not","params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.modalOpen"}}}}},"index":90,"reference":"whf-modal"},{"_id":"8683cd3383624c8eec85a8db","parent":"0f943aabe12c02fc0a9fc8f1","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"NavAbout","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":94,"height":44}},"text":{"component":"data2-core-primitives@String","params":{"content":"Products"}},"cursor":"pointer","textFormat":{"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","verticalAlignment":"middle"}}},"index":2,"reference":"fzbiYzJw1cBK6xbZDOBVh"},{"_id":"c8ca1d5d8324b6dd69f58ec8","parent":"0f943aabe12c02fc0a9fc8f1","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"NavArticles","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":67,"height":44}},"text":{"component":"data2-core-primitives@String","params":{"content":"Cases"}},"cursor":"pointer","textFormat":{"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","verticalAlignment":"middle"}},"onClick":{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"set","args":["scrollTo",1]}}},"index":3,"reference":"2iydqoOwRuomNspXSKzvq"},{"_id":"35c2afba1e9a21d1c3eceeee","parent":"0f943aabe12c02fc0a9fc8f1","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"NavPricing","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":99,"height":44}},"text":{"component":"data2-core-primitives@String","params":{"content":"Solutions"}},"cursor":"pointer","textFormat":{"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","verticalAlignment":"middle"}}},"index":4,"reference":"ixB-CsZkWiIDRkXqcPzZG"},{"_id":"8deca7f78ea85cabe7fcb19b","parent":"0f943aabe12c02fc0a9fc8f1","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"NavCareers","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":64,"height":44}},"text":{"component":"data2-core-primitives@String","params":{"content":"About"}},"cursor":"pointer","textFormat":{"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","verticalAlignment":"middle"}}},"index":5,"reference":"rO2smpMlrYkQkLA7TFzAA"},{"_id":"e2a5233c0325d85ff120681b","parent":"0f943aabe12c02fc0a9fc8f1","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"NavContact","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":85,"height":44}},"text":{"component":"data2-core-primitives@String","params":{"content":"Funding"}},"cursor":"pointer","textFormat":{"component":"data2-core-types@TextFormat","params":{"horizontalAlignment":"center","verticalAlignment":"middle"}}},"index":6,"reference":"RqVx6EPNnDtGJfvZTZc8-"},{"_id":"7f26c4a3cac0409112815ae4","parent":"9b0a030b9c437188ec7132e6","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"ModalBackdrop","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1920,"height":1080}},"cursor":"pointer","onClick":{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"set","args":["modalOpen",false]}}},"index":0,"reference":"whf-modal-backdrop"},{"_id":"1f0a8c86a8a8089d2a15171e","parent":"9b0a030b9c437188ec7132e6","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"ModalPanel","params":{"position":{"component":"data2-core-types@Position","params":{"x":360,"y":140,"freeTransform":true,"left":false,"right":false,"top":false,"bottom":false,"growH":true,"growV":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1200,"height":800}}},"index":10,"reference":"whf-modal-panel"},{"_id":"dbe35d9bfd9305ba315a10cb","parent":"1f0a8c86a8a8089d2a15171e","component":"bigaton-playground@wormhole-modal-body","system":"69b15e6093419d8151c50e4c","displayName":"ModalBody","params":{"cardName":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.name"}},"cardCategory":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.category"}},"thumb":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.thumb"}},"mode":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.mode"}},"artboardRef":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.artboardRef"}},"designW":1440,"designH":900,"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1200,"height":800}}},"appUrl":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"wh-ui"}},"prop":"data.appUrl"}}},"reference":"VJhC7YRZp_YfJK6cZ_FZC"}]}},{"_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":"6a622a6db690f913711430bd","displayName":"New Application (12)","reference":"eSFkZddvjNDsgrLbSiMuY","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"wormhole"}},"entryPoint":{"params":{"component":"data2-core-primitives@Artboard","reference":"FqOZXaMKZwZwxIo1iD_Zd"},"component":"data2-core-types@ComponentInstanceReference"}},"system":"69b15e6093419d8151c50e4c","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a39802715978995ea1f8101","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"tableImageUrl":"","selectedProduct":null,"generatedImageUrl":""}},"reference":"pinnacle-store"},{"_id":"6a5ffdb4d774752eb7da9c14","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"artboardRef":"","modalOpen":false,"mode":"thumb","thumb":"","name":"","category":"","tags":"","scrollTo":null,"appUrl":""}},"reference":"wh-ui"},{"_id":"6a6385168630df9f417900f7","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"name":"","category":"","tags":"","thumb":"","redirectUrl":"","modalOpen":false,"objective":"","overview":"","journey":[],"backendDoc":"","observations":"","system":"","workspace":""}},"reference":"hotpocs-ui"},{"_id":"69fcf828b493c94d6228b3ad","component":"data2-core-presets@PresetTag","system":"69b15e6093419d8151c50e4c","params":{"label":"button","icon":"price-tag-3-line","color":"var(--w70, #aaa)"},"reference":"button"},{"_id":"6a7dfed5e9843c72fbdb533e","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"userName":"","userRole":"","userInitials":"","userId":""}},"reference":"Session","deleteMetadata":{}},{"_id":"6a7e1758572546f028779fc3","component":"data2-core@Data2TableDataSource","system":"69b15e6093419d8151c50e4c","params":{"table":"people","schema":[{"component":"data2-core-types@Field","params":{"key":"personId","title":"Person ID","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"name","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Role","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"initials","title":"Iniciais","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"color","title":"Cor","component":"data2-core-primitives@String"}}]},"reference":"people","deleteMetadata":{}},{"_id":"6a91de5e15ee67cd8e306c7a","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"url":"","prompt":""}},"reference":"mcportraitimg","deleteMetadata":{}},{"_id":"6a874fdc95f5b1d37d7b5af1","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"x":475,"y":483,"action":"stand1","play":false,"flip":false}},"reference":"mcplay","deleteMetadata":{}},{"_id":"6a5fd4c93a42b34509a5059a","component":"data2-core@DataSet","system":"69b15e6093419d8151c50e4c","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"wormhole-screens"}}},"reference":"wormhole-screensDataSet"},{"_id":"6a5fd4c8454cf75eb2c85497","component":"data2-core@Data2TableDataSource","system":"69b15e6093419d8151c50e4c","params":{"table":"wormhole-screens","schema":[{"component":"data2-core-types@Field","params":{"key":"name","title":"Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"category","title":"Category","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tags","title":"Tags","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"thumbnail","title":"Thumbnail","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"openMode","title":"Open Mode","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"artboardRef","title":"Artboard Reference","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"order","title":"Order","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"active","title":"Active","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"appUrl","title":"App URL","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"appW","title":"App Width","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"appH","title":"App Height","component":"data2-core-primitives@Number"}}]},"reference":"wormhole-screens"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}