{"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":"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.';\nconst SEM_PREVIEW=Object.create(null);\nfor(let n=2270;n<=2309;n++)if(n%5!==0)SEM_PREVIEW['0103'+n]=1;\n// LUMIERE HERITAGE -- 22 ids com a MESMA arma. Medido antes de cortar, do mesmo jeito que nos\n// brincos Eluna: os 22 recortes de icone tem o mesmo sha1 de pixels, e a geometria de mundo\n// (geo+uv+px) tem uma assinatura so para os 22. Fica o menor id; os outros 21 saem da navegacao.\nfor(let n=3693;n<=3713;n++)SEM_PREVIEW['0170'+n]=1;\n// Retirados a pedido (01/09), por ID e nao por condicao. Uma versao anterior trocou o 01702099\n// por uma regra derivada (sem geoRef E sem icone) e ela varreu um item a MAIS -- o face acc\n// 01012010, que ninguem pediu. Voltou a ser lista: a condicao existe e esta documentada no\n// tools_data2/audita_catalogo.py, que a re-deriva quando o catalogo for atualizado, sem decidir\n// nada sozinha.\n//   01002388, 01004059 -> chapeus sem nome no dump\n//   01702099           -> Transparent Claw: sem geometria e sem icone\nfor(const id of ['01002388','01004059','01702099'])SEM_PREVIEW[id]=1;\n// FAIXAS DE TESTE do jogo (01/09, pedido do usuario): 0160 e 0169 nao sao armas de verdade.\n// Sao 84 ids (8 + 76) e TODOS tem icone, entao nenhuma outra condicao os pegaria -- a faixa de id\n// e a unica coisa que os identifica. So vale em weapon: os prefixos sao daquela categoria.\nconst FAIXA_TESTE={'0160':1,'0169':1};\n// Categorias em que item SEM ICONE nao aparece na navegacao. E regra derivada, nao lista de ids:\n// se a folha de icone ganhar esses recortes um dia, os itens voltam sozinhos. Sao 40 hairs (5\n// blocos de 8, estilo x cor) e 9 faces -- todos na ponta NOVA do catalogo, que e justamente a\n// primeira coisa que se ve desde que a ordem passou a ser decrescente.\n// So hair e face: weapon, cap e accface tambem tem alguns sem icone, mas isso nao foi pedido.\nconst SO_COM_ICONE={hair:1,face:1};\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>';}\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\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  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;\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{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  .pill .lk{width:13px;height:13px;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:9px;padding:16px 22px;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}\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  .side::-webkit-scrollbar{width:var(--gutter)}\n  .side::-webkit-scrollbar-thumb{background:#6d7a82;border-radius:9px;border:2px solid #424c5a}\n  .side::-webkit-scrollbar-thumb:hover{background:#87949c}\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  .abas{display:flex;flex-wrap:wrap;gap:0 3px}\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  .risca{height:1px;background:#6d7883;flex:none;\n    margin:0 calc(-22px - var(--gutter)) 11px -22px}\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  .aba{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  .invcont{font:11px ui-monospace,monospace;color:var(--dim);background:#f4f5f6;\n    border:1px solid #cdd0d4;border-radius:9px;padding:1px 11px}\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  .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  .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  @media (max-width:768px){\n    .wrap{padding:10px}\n    .app{grid-template-columns:1fr;grid-template-rows:minmax(0,auto) minmax(0,1fr);max-width:none}\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];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      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\">−</button><span class=\"badge zind\">${s.zoom}×</span><button class=\"zp pill claro so\">+</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        <div class=\"abas\">${['all'].concat(CATS).map(c=>`<button class=\"aba catsel ${c===s.catAtual?'on':''}\" data-c=\"${c}\">${ROTULO[c]||c}</button>`).join('')}</div>\n        <div class=\"risca\"></div>\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}</button>`;\n            }).join('')}\n          </div>\n          <div class=\"pag\"><button class=\"pp pill claro so\" ${pag<=0?'disabled':''}>‹</button><span class=\"badge\">${pag+1} / ${paginas}</span><button class=\"np pill claro so\" ${pag>=paginas-1?'disabled':''}>›</button></div>\n        </div>\n        <div class=\"nota\">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    // 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();\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      sinaliza();\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        if(!nome){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      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      s.corCat=cat;\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      let nav=linhas.filter(r=>!SEM_PREVIEW[r.id]);\n      if(cat==='weapon')nav=nav.filter(r=>!FAIXA_TESTE[r.id.slice(0,4)]);\n      if(SO_COM_ICONE[cat])nav=nav.filter(r=>r.ic);\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      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='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      await carregaSkin(0);\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      await trocaCategoria(CATS[0]);\n      s.pronto=true;marca();await prefetch();\n     }catch(err){s.erro=(err&&err.message)?err.message:String(err);}\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;}\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      s.corSel=Number(el.getAttribute('data-dg'))||0;\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(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    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 §23 da doc.\\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()=>{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    d2.on('.port','click',()=>{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"}}}}}}}},"__d2PrevCode":{"code":"// `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.';\nconst SEM_PREVIEW=Object.create(null);\nfor(let n=2270;n<=2309;n++)if(n%5!==0)SEM_PREVIEW['0103'+n]=1;\n// LUMIERE HERITAGE -- 22 ids com a MESMA arma. Medido antes de cortar, do mesmo jeito que nos\n// brincos Eluna: os 22 recortes de icone tem o mesmo sha1 de pixels, e a geometria de mundo\n// (geo+uv+px) tem uma assinatura so para os 22. Fica o menor id; os outros 21 saem da navegacao.\nfor(let n=3693;n<=3713;n++)SEM_PREVIEW['0170'+n]=1;\n// Retirados a pedido (01/09), por ID e nao por condicao. Uma versao anterior trocou o 01702099\n// por uma regra derivada (sem geoRef E sem icone) e ela varreu um item a MAIS -- o face acc\n// 01012010, que ninguem pediu. Voltou a ser lista: a condicao existe e esta documentada no\n// tools_data2/audita_catalogo.py, que a re-deriva quando o catalogo for atualizado, sem decidir\n// nada sozinha.\n//   01002388, 01004059 -> chapeus sem nome no dump\n//   01702099           -> Transparent Claw: sem geometria e sem icone\nfor(const id of ['01002388','01004059','01702099'])SEM_PREVIEW[id]=1;\n// FAIXAS DE TESTE do jogo (01/09, pedido do usuario): 0160 e 0169 nao sao armas de verdade.\n// Sao 84 ids (8 + 76) e TODOS tem icone, entao nenhuma outra condicao os pegaria -- a faixa de id\n// e a unica coisa que os identifica. So vale em weapon: os prefixos sao daquela categoria.\nconst FAIXA_TESTE={'0160':1,'0169':1};\n// Categorias em que item SEM ICONE nao aparece na navegacao. E regra derivada, nao lista de ids:\n// se a folha de icone ganhar esses recortes um dia, os itens voltam sozinhos. Sao 40 hairs (5\n// blocos de 8, estilo x cor) e 9 faces -- todos na ponta NOVA do catalogo, que e justamente a\n// primeira coisa que se ve desde que a ordem passou a ser decrescente.\n// So hair e face: weapon, cap e accface tambem tem alguns sem icone, mas isso nao foi pedido.\nconst SO_COM_ICONE={hair:1,face:1};\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>';}\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\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  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;\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{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  .pill .lk{width:13px;height:13px;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:9px;padding:16px 22px;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}\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  .side::-webkit-scrollbar{width:var(--gutter)}\n  .side::-webkit-scrollbar-thumb{background:#6d7a82;border-radius:9px;border:2px solid #424c5a}\n  .side::-webkit-scrollbar-thumb:hover{background:#87949c}\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  .abas{display:flex;flex-wrap:wrap;gap:0 3px}\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  .risca{height:1px;background:#6d7883;flex:none;\n    margin:0 calc(-22px - var(--gutter)) 11px -22px}\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  .aba{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  .invcont{font:11px ui-monospace,monospace;color:var(--dim);background:#f4f5f6;\n    border:1px solid #cdd0d4;border-radius:9px;padding:1px 11px}\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  .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  .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  @media (max-width:768px){\n    .wrap{padding:10px}\n    .app{grid-template-columns:1fr;grid-template-rows:minmax(0,auto) minmax(0,1fr);max-width:none}\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];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      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\">−</button><span class=\"badge zind\">${s.zoom}×</span><button class=\"zp pill claro so\">+</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        <div class=\"abas\">${['all'].concat(CATS).map(c=>`<button class=\"aba catsel ${c===s.catAtual?'on':''}\" data-c=\"${c}\">${ROTULO[c]||c}</button>`).join('')}</div>\n        <div class=\"risca\"></div>\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}</button>`;\n            }).join('')}\n          </div>\n          <div class=\"pag\"><button class=\"pp pill claro so\" ${pag<=0?'disabled':''}>‹</button><span class=\"badge\">${pag+1} / ${paginas}</span><button class=\"np pill claro so\" ${pag>=paginas-1?'disabled':''}>›</button></div>\n        </div>\n        <div class=\"nota\">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    // 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();\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      sinaliza();\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        if(!nome){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      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      s.corCat=cat;\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      let nav=linhas.filter(r=>!SEM_PREVIEW[r.id]);\n      if(cat==='weapon')nav=nav.filter(r=>!FAIXA_TESTE[r.id.slice(0,4)]);\n      if(SO_COM_ICONE[cat])nav=nav.filter(r=>r.ic);\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      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='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      await carregaSkin(0);\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      await trocaCategoria(CATS[0]);\n      s.pronto=true;marca();await prefetch();\n     }catch(err){s.erro=(err&&err.message)?err.message:String(err);}\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    // 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. Sai fora quando o foco esta num campo, senao digitar\n    // \"u\" na busca escondia o anel.\n    d2.on(window,'keydown',e=>{\n      if(e.ctrlKey||e.metaKey||e.altKey)return;\n      const alvo=e.target&&e.target.tagName;\n      if(alvo==='INPUT'||alvo==='TEXTAREA'||alvo==='SELECT')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      const alvo=e.target&&e.target.tagName;\n      if(alvo==='INPUT'||alvo==='TEXTAREA'||alvo==='SELECT')return;   // nao rouba o ctrl+z da busca\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;}\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      s.corSel=Number(el.getAttribute('data-dg'))||0;\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(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    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 §23 da doc.\\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()=>{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    d2.on('.port','click',()=>{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","at":"2026-09-02T04:37:22.967Z"}}},{"_id":"5ed9bed7b9e0891ce913e884","component":"data2-core-types@Component","params":{"package":"data2-core","component":"DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5f472ae463c680de4ff296e9","component":"data2-core-types@Component","params":{"package":"data2-core","component":"Data2TableDataSource","extends":"data2-core@DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5ecd05ded68ce41360f74b43","component":"data2-core-types@Component","params":{"extendable":{"placeholder":"children"},"component":"Block","package":"data2-core-primitives"}},{"_id":"5f494791a575399a45020f87","component":"data2-core-types@Component","params":{"definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"html@input","params":{"style":{"width":"100%","height":"100%","boxSizing":"border-box","background":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"checkbox"}}],"then":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"comparisonSubject":true}}],"then":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"checkboxFill"}}}},"rgba(0,118,255,1)"]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"content":"none"},"component":"data2-core-primitives@String"}}},"else":{"params":{"content":"none"},"component":"data2-core-primitives@String"}}},"color":{"params":{"reference":"super.textColor"},"component":"data2-core-types@Reference"},"border":"none","outline":"none","appearance":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"orient","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"vertical"}}],"then":"slider-vertical","else":"none"}},"cursor":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"super.cursor"}}}},{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":true}}],"then":{"component":"data2-core-primitives@String","params":{"content":"not-allowed"}},"else":{"component":"data2-core-primitives@String","params":{"content":"auto"}}}}]},"component":"data2-core-logical-operators@Or"},"fontWeight":{"component":"data2-core-primitives@String","params":{"content":"inherit"}},"paddingLeft":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.left"}}}}},"component":"data2-core-comparison-operators@Different"}],"then":{"params":{"portions":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.left"}}}},{"component":"data2-core-primitives@String","params":{"content":"px"}}]},"component":"data2-core-functions@StringConcat"}}},"paddingRight":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"comparisonSubject":{"params":{},"component":"data2-core-primitives@Undefined"},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.right"}}}}},"component":"data2-core-comparison-operators@Different"}],"then":{"params":{"portions":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"padding.right"}}}},{"component":"data2-core-primitives@String","params":{"content":"px"}}]},"component":"data2-core-functions@StringConcat"}}},"caretColor":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"caretColor"}}}},"textAlign":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"super.textFormat.textAlign"}}},"component":"data2-core-types@Reference"}},"tabIndex":{"component":"data2-core-types@Reference","params":{"reference":"tabIndex","source":"VIRTUAL_COMPONENT_PARAMS"}},"step":{"component":"data2-core-types@Reference","params":{"reference":"step","source":"VIRTUAL_COMPONENT_PARAMS"}},"min":{"component":"data2-core-types@Reference","params":{"reference":"min","source":"VIRTUAL_COMPONENT_PARAMS"}},"max":{"component":"data2-core-types@Reference","params":{"reference":"max","source":"VIRTUAL_COMPONENT_PARAMS"}},"maxLength":{"params":{"then":{"params":{},"component":"data2-core-primitives@Undefined"},"compare":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"maxLength","source":"VIRTUAL_COMPONENT_PARAMS"}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":""}}},"component":"data2-core-comparison-operators@Equal"}],"else":{"component":"data2-core-types@Reference","params":{"reference":"maxLength","source":"VIRTUAL_COMPONENT_PARAMS"}}},"component":"data2-core-flow@If"},"orient":{"component":"data2-core-types@Reference","params":{"reference":"orient","source":"VIRTUAL_COMPONENT_PARAMS"}},"type":{"component":"data2-core-types@Reference","params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"}},"placeholder":{"component":"data2-core-logical-operators@Or","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":"placeholder","source":"VIRTUAL_COMPONENT_PARAMS"}},{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}}]}},"value":{"params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"defaultCase":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}},"mask":{"component":"data2-core-types@Reference","params":{"reference":"mask","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"date"}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":"YYYY-MM-DD"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"month"}}}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":{"component":"data2-core-primitives@String","params":{"content":"YYYY-MM"}}}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"datetime-local"}}}},"value":{"component":"data2-core-functions@DateFormat","params":{"date":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"format":{"component":"data2-core-primitives@String","params":{"content":"YYYY-MM-DDTHH:mm"}}}}}}]},"component":"data2-core-flow@Switch"},"onChange":{"component":"data2-core-types@DataLinkSetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}},"recordUpsert":{"component":"data2-core-types@Reference","params":{"reference":"recordUpsert","source":"VIRTUAL_COMPONENT_PARAMS"}},"onChange":{"component":"data2-core-types@Reference","params":{"reference":"onChange","source":"VIRTUAL_COMPONENT_PARAMS"}},"mask":{"component":"data2-core-types@Reference","params":{"reference":"mask","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"defaultChecked":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Identical","params":{"subject":{"params":{"reference":"type","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"comparisonSubject":"checkbox"}}],"then":{"component":"data2-core-types@DataLinkGetter","params":{"dataset":{"component":"data2-core-types@Reference","params":{"reference":"dataset","source":"VIRTUAL_COMPONENT_PARAMS"}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field","source":"VIRTUAL_COMPONENT_PARAMS"}},"record":{"component":"data2-core-types@Reference","params":{"reference":"record","source":"VIRTUAL_COMPONENT_PARAMS"}}}},"else":""}},"field":{"component":"data2-core-types@Reference","params":{"reference":"field"}},"opacity":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"}],"then":0.38,"else":1}},"disabled":{"params":{"reference":"disabled","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},"autoComplete":{"component":"data2-core-flow@If","params":{"compare":[{"component":"data2-core-comparision-operators@Equal","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":"builder"}},"comparisonSubject":true}}],"then":"new-password","else":{"params":{"compare":{"params":{"comparisonSubject":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"subject":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoComplete"}}},"component":"data2-core-types@Reference"}},"component":"data2-core-comparison-operators@Identical"},"else":{"params":{"components":[{"params":{"reference":"autoComplete","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"},{"component":"data2-core-primitives@String","params":{"content":"off"}}]},"component":"data2-core-logical-operators@Or"}},"component":"data2-core-flow@If"}}},"data-lpignore":{"component":"data2-core-flow@If","params":{"compare":[{"params":{"reference":"autoComplete","source":"VIRTUAL_COMPONENT_PARAMS"},"component":"data2-core-types@Reference"}],"then":"","else":"true"}},"onFocus":{"component":"data2-core-types@Reference","params":{"reference":"onFocus","source":"VIRTUAL_COMPONENT_PARAMS"}},"onBlur":{"component":"data2-core-types@Reference","params":{"reference":"onBlur","source":"VIRTUAL_COMPONENT_PARAMS"}},"onKeyPress":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onKeyPress"}}}},"autoCapitalize":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"autoCapitalize"}}}},"spellCheck":{"params":{"expression":{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"spellCheck"}}},"component":"data2-core-types@Reference"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"value":{"component":"data2-core-primitives@String","params":{"content":"true"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"value":{"component":"data2-core-primitives@String","params":{"content":"false"}}}}]},"component":"data2-core-flow@Switch"}}}}},"component":"DataFieldBlock","package":"data2-blocks","extends":"data2-core-primitives@Block"}}]}},{"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":[]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a97b27a181d63b9c3c4fff3","component":"data2-core-primitives@Router","system":"69b15e6093419d8151c50e4c","displayName":"MapleCloset Router","params":{"routerType":{"component":"data2-core-primitives@String","params":{"content":"browser"}},"routes":[{"component":"data2-core-primitives@Route","params":{"path":"","name":"MapleCloset","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"ISmxxzJxhsIf25Gs_cYXO"}},"policies":[{"component":"data2-core-auth@Policy","params":{"authenticationStrategy":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcauth"}}}}]}},{"component":"data2-core-primitives@Route","params":{"path":"/login","name":"Sign in","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"nS_4yY8dxJyEWM5eqXJdE"}}}}],"login":{"component":"data2-core-types@ArtboardReference","params":{"reference":"nS_4yY8dxJyEWM5eqXJdE"}},"notFound":{"component":"data2-core-types@ArtboardReference","params":{"reference":"nS_4yY8dxJyEWM5eqXJdE"}}},"reference":"mcRouter","deleteMetadata":{}},{"_id":"6a97b1c9784e6873cded0b1f","component":"data2-core-primitives@Artboard","system":"69b15e6093419d8151c50e4c","displayName":"MapleCloset Login","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"MapleCloset Login"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1440,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":2000,"y":-1254,"exactX":2000,"exactY":-1253.99}}},"reference":"nS_4yY8dxJyEWM5eqXJdE","deleteMetadata":{}},{"_id":"6a8387f65a7c75b8a2c3aeff","component":"data2-core-primitives@Artboard","system":"69b15e6093419d8151c50e4c","displayName":"MapleCloset Base","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"MapleCloset Base"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1000,"height":700}},"position":{"component":"data2-core-types@Position","params":{"x":2852,"y":3534,"exactX":2851.85,"exactY":3534.42}}},"reference":"ISmxxzJxhsIf25Gs_cYXO","deleteMetadata":{}},{"_id":"fc481cb1b14c07e2c0eed35a","parent":"6a97b1c9784e6873cded0b1f","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Login Page","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":1440,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#e6e1ce"}}}}]}}},"reference":"dn-S6t1yWFL4b8t-dWday","deleteMetadata":{}},{"_id":"a086f78485d00aa81cf66df6","parent":"6a8387f65a7c75b8a2c3aeff","component":"bigaton-playground@mcbaseviewer","system":"69b15e6093419d8151c50e4c","displayName":"Base Viewer","params":{"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":1000,"height":700}}}},"reference":"NyWfXj62hXDcdap_tOA5V","deleteMetadata":{}},{"_id":"c586711baf4914dad4b1f0cf","parent":"fc481cb1b14c07e2c0eed35a","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Login Card","params":{"position":{"component":"data2-core-types@Position","params":{"x":530,"y":240,"freeTransform":true,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":380,"height":404}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#eeeeee"}}}}]}},"border":{"component":"data2-core-types@Border","params":{"radius":{"leftTop":"16","rightTop":"16","rightBottom":"16","leftBottom":"16"},"width":{"left":"1","top":"1","right":"1","bottom":"1"},"color":"#d3d3d3"}}},"index":0,"reference":"mr9Df1352PNlF6cs7kPnQ","deleteMetadata":{}},{"_id":"12de874d07ee08fb5190a1d3","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Titulo","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":30,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":30}},"text":"MapleCloset","textColor":{"component":"data2-core-types@Color","params":{"rgba":"#23232a"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":22,"fontWeight":700}}},"index":0,"reference":"t0Vej93UxnLupc6Sp74Fj","deleteMetadata":{}},{"_id":"832293c648dec0e74a9cf7d6","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Subtitulo","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":63,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":18}},"text":"Sign in to open the previewer","textColor":{"component":"data2-core-types@Color","params":{"rgba":"#6a6a76"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":12}}},"index":1,"reference":"7G7TbIdJkys6k9stFfKy3","deleteMetadata":{}},{"_id":"39b8476744a63a6f272b1d59","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Label Email","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":110,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":14}},"text":"E-MAIL","textColor":{"component":"data2-core-types@Color","params":{"rgba":"#6a6a76"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":10,"fontWeight":700,"letterSpacing":0.9}}},"index":2,"reference":"Nr2cVeggkBm2z83x7J2-y","deleteMetadata":{}},{"_id":"62bdff9f9997ebc125f54fb8","parent":"c586711baf4914dad4b1f0cf","component":"data2-blocks@DataFieldBlock","system":"69b15e6093419d8151c50e4c","displayName":"Campo Email","params":{"dataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"field":"data.email","placeholder":"you@example.com","padding":{"left":12,"right":12},"super":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":128,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":40}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#ffffff"}}}}]}},"border":{"component":"data2-core-types@Border","params":{"radius":{"leftTop":"8","rightTop":"8","rightBottom":"8","leftBottom":"8"},"width":{"left":"1","top":"1","right":"1","bottom":"1"},"color":"#cbcbd3"}},"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#23232a"}}}},"index":3,"reference":"xHXmGN6yTxq6MSTFxFLD4","deleteMetadata":{}},{"_id":"c64f2c73eb33422828ea35b9","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Label Senha","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":184,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":14}},"text":"PASSWORD","textColor":{"component":"data2-core-types@Color","params":{"rgba":"#6a6a76"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":10,"fontWeight":700,"letterSpacing":0.9}}},"index":4,"reference":"XV1zizpACFM2b_svduhmO","deleteMetadata":{}},{"_id":"c9ba1f036610cb637e6516d5","parent":"c586711baf4914dad4b1f0cf","component":"data2-blocks@DataFieldBlock","system":"69b15e6093419d8151c50e4c","displayName":"Campo Senha","params":{"dataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"field":"data.password","placeholder":"your password","type":"password","padding":{"left":12,"right":12},"super":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":202,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":40}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#ffffff"}}}}]}},"border":{"component":"data2-core-types@Border","params":{"radius":{"leftTop":"8","rightTop":"8","rightBottom":"8","leftBottom":"8"},"width":{"left":"1","top":"1","right":"1","bottom":"1"},"color":"#cbcbd3"}},"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#23232a"}}},"onEnter":{"component":"data2-core-flow@Series","params":{"actions":[{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"set","args":["erro",""]}},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcauth"}},"prop":"login","args":[{"email":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"data.email"}},"password":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"data.password"}}}]}},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcRouterInterface"}},"prop":"goTo","args":["/"]}}]}}},"index":5,"reference":"Z31mlBc8cKgo7ZofwWyQT","deleteMetadata":{}},{"_id":"737ffa1851e85caa380db8a8","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Botao Entrar","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":264,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":42}},"text":"Sign In","fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#3f4855"}}}}]}},"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#ffffff"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":13,"fontWeight":600,"horizontalAlignment":"center","verticalAlignment":"middle"}},"border":{"component":"data2-core-types@Border","params":{"radius":{"leftTop":"999","rightTop":"999","rightBottom":"999","leftBottom":"999"},"width":{"left":"0","top":"0","right":"0","bottom":"0"}}},"onClick":{"component":"data2-core-flow@Series","params":{"actions":[{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"set","args":["erro",""]}},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcauth"}},"prop":"login","args":[{"email":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"data.email"}},"password":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"data.password"}}}]}},{"component":"data2-core-types@Interaction","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcRouterInterface"}},"prop":"goTo","args":["/"]}}]}}},"index":6,"reference":"3SS_48CnHykb_HJeRJX81","deleteMetadata":{}},{"_id":"9c2762578590929dc20eeafb","parent":"c586711baf4914dad4b1f0cf","component":"data2-core-primitives@Block","system":"69b15e6093419d8151c50e4c","displayName":"Erro","params":{"position":{"component":"data2-core-types@Position","params":{"x":28,"y":322,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":324,"height":34}},"text":{"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"mcLoginForm"}},"prop":"data.erro"}},"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#b04141"}},"textFormat":{"component":"data2-core-types@TextFormat","params":{"size":11,"horizontalAlignment":"center"}}},"index":7,"reference":"GuijmM97llisuJcqFgRIu","deleteMetadata":{}}]}},{"_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":"6a86583605157f68a8ddba69","displayName":"MapleCloset_App","reference":"nOvVqG0KnAOf4iUgzNU2W","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"maplesim"}},"entryPoint":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Router","reference":"mcRouter"}}},"system":"69b15e6093419d8151c50e4c","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a97b19c6aa0c19502f9a95a","component":"data2-core@AuthenticationStrategy","system":"69b15e6093419d8151c50e4c","params":{"credentials":[{"key":"email"},{"key":"password"}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"mc-userDataSet"}},"email":"email","password":"password","role":"role","roleDefault":"user"},"reference":"mcauth","deleteMetadata":{}},{"_id":"6a97b1756d9e6452edabc124","component":"data2-core@DataSet","system":"69b15e6093419d8151c50e4c","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"mc-user"}}},"reference":"mc-userDataSet","deleteMetadata":{}},{"_id":"6a97b175ef8f14e4f9d507a4","component":"data2-core@Data2TableDataSource","system":"69b15e6093419d8151c50e4c","params":{"table":"mc-user","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"password","title":"Senha","component":"data2-core-types@Password","required":true}},{"component":"data2-core-types@Field","params":{"key":"name","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Papel","component":"data2-core-primitives@String"}}]},"reference":"mc-user","deleteMetadata":{}},{"_id":"6a97b1c66d9e6452edabc185","component":"data2-core@Store","system":"69b15e6093419d8151c50e4c","params":{"initialData":{"email":"","password":"","erro":""}},"reference":"mcLoginForm","deleteMetadata":{}}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}