{"context":{"domain":"cronica-app"},"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":"2022-03-09T02:01:26.213Z"}]}},{"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}],"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":"2025-11-05T19:23:47.135Z","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":"6a32a5e751daba649fb7e28b","name":"Cronica","domain":"cronica-app","visibility":"private","updatedAt":"2026-06-17T13:49:27.180Z","createdAt":"2026-06-17T13:49:27.180Z"}]}},{"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":"6a342f78568f29374ea467c4","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"PosSessaoBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', muted:'#a08a5e', myst:'#5b3f7a', mystLight:'#9a78c4', mystDeep:'#3a2752', blood:'#9c3320', bloodLight:'#c4543a', green:'#5f8b46', greenLight:'#8fc46a' };\nconst GB = 'cronica-app@GoldenButton';\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nfunction escA(s){ return esc(s).replace(/\"/g,'&quot;'); }\n\nconst GROUPS = [\n  { key:'npcs',    title:'NPCs',     glyph:'☻', nameKey:'name'  },\n  { key:'locais',  title:'Locais',   glyph:'⌂', nameKey:'name'  },\n  { key:'quests',  title:'Quests',   glyph:'✦', nameKey:'title' },\n  { key:'faccoes', title:'Facções',  glyph:'⚑', nameKey:'name'  }\n];\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:16px; padding:8px 8px 40px; font-family:'Alegreya',sans-serif; color:${PAL.cream}; background:#1c140d; }\n  .topbar{ display:flex; align-items:center; gap:10px; }\n  .back-w{ width:120px; height:40px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .hero{ text-align:center; padding:4px 4px 0; }\n  .title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:40px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}, 0 0 18px rgba(154,120,196,0.45); line-height:1; }\n  .subtitle{ color:${PAL.mystLight}; font-size:13px; letter-spacing:1px; margin-top:8px; opacity:0.9; }\n  .panel{ box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 60%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 24px rgba(0,0,0,0.5), 0 8px 18px rgba(0,0,0,0.55); }\n  .sec-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }\n  .sec-ttl{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:26px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .sec-act{ width:200px; max-width:100%; height:48px; flex:0 0 auto; }\n  .sec-act > *{ width:100% !important; }\n  .label{ color:${PAL.gold}; font-size:12px; letter-spacing:1.5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-bottom:6px; }\n  .root textarea.gf-input{\n    box-sizing:border-box; width:100%; background:#241a10; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:15px; border:3px solid ${PAL.edgeDark}; border-radius:4px;\n    padding:10px 12px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -2px -2px 0 rgba(106,84,54,0.4); outline:none;\n    min-height:150px; resize:vertical; line-height:1.5; }\n  .root textarea.gf-input:focus{ border-color:${PAL.goldDeep}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), 0 0 8px rgba(216,178,76,0.55); }\n  .save-row{ display:flex; justify-content:flex-end; gap:10px; margin-top:12px; align-items:center; }\n  .save-w{ width:160px; height:46px; }\n  .err{ color:${PAL.bloodLight}; font-size:12px; letter-spacing:0.5px; }\n  .ok-msg{ color:${PAL.greenLight}; font-size:12px; letter-spacing:0.5px; }\n  .thinking{ color:${PAL.mystLight}; font-size:14px; font-style:italic; padding:6px 0; }\n  .group{ margin-top:14px; }\n  .group-ttl{ color:${PAL.gold}; font-size:15px; letter-spacing:1px; margin-bottom:8px; display:flex; align-items:center; gap:8px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .cards{ display:flex; flex-direction:column; gap:10px; }\n  .card{ box-sizing:border-box; padding:12px 14px; border-radius:6px; border:3px solid ${PAL.edgeDark};\n    background:linear-gradient(150deg, #2e2418 0%, ${PAL.stone1} 100%); box-shadow: inset 1px 1px 0 rgba(255,255,255,0.05); }\n  .card.exists{ border-color:${PAL.green}; }\n  .card.added{ border-color:${PAL.goldDeep}; opacity:0.85; }\n  .card-top{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }\n  .card-name{ color:${PAL.cream}; font-size:17px; }\n  .card-type{ color:${PAL.muted}; font-size:12px; letter-spacing:0.5px; }\n  .card-desc{ color:${PAL.cream}; font-size:13px; line-height:1.4; margin-top:6px; opacity:0.92; }\n  .badge{ flex:0 0 auto; font-size:11px; letter-spacing:0.5px; padding:3px 9px; border-radius:12px; border:2px solid ${PAL.edgeDark}; }\n  .badge.new{ background:rgba(91,63,122,0.3); color:${PAL.mystLight}; border-color:${PAL.mystDeep}; }\n  .badge.exists{ background:rgba(95,139,70,0.25); color:${PAL.greenLight}; border-color:${PAL.green}; }\n  .badge.added{ background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); color:${PAL.edgeDark}; }\n  .rels{ display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }\n  .rel{ display:inline-flex; align-items:center; gap:4px; font-size:11px; letter-spacing:0.3px; padding:2px 8px; border-radius:10px;\n    color:${PAL.cream}; background:rgba(58,46,32,0.6); border:1px solid ${PAL.edgeLight};\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.05); }\n  .rel .rel-g{ color:${PAL.gold}; font-size:11px; }\n  .rel.tag{ color:${PAL.muted}; background:rgba(28,20,13,0.55); border-color:${PAL.edgeDark}; }\n  .rel.status{ color:${PAL.greenLight}; border-color:${PAL.green}; background:rgba(95,139,70,0.18); }\n  .card-actions{ display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }\n  .ca-slot{ height:38px; }\n  .ca-slot.add{ width:200px; }\n  .ca-slot.link{ width:130px; }\n  .ca-slot.ignore{ width:100px; }\n  .link-note{ color:${PAL.greenLight}; font-size:12px; margin-top:6px; }\n  .empty{ color:${PAL.muted}; font-size:13px; font-style:italic; }\n  .hint{ color:${PAL.muted}; font-size:13px; margin-bottom:6px; }\n  /* image generation */\n  .gen-row{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:12px; }\n  .gen-w{ width:160px; height:46px; }\n  .gen-status{ color:${PAL.mystLight}; font-size:13px; font-style:italic; }\n  .gen-row.is-generating .gen-w{ opacity:0.6; pointer-events:none; filter:grayscale(0.3); }\n  .gen-banner{ display:flex; align-items:center; gap:10px; margin-top:12px; padding:10px 14px; border-radius:6px;\n    color:${PAL.mystLight}; font-size:14px; letter-spacing:0.3px;\n    background:linear-gradient(180deg, rgba(91,63,122,0.35), rgba(58,39,82,0.35));\n    border:2px solid ${PAL.mystDeep}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.06), 0 4px 10px rgba(0,0,0,0.4); }\n  .gen-spin{ flex:0 0 auto; width:18px; height:18px; border-radius:50%;\n    border:3px solid rgba(154,120,196,0.3); border-top-color:${PAL.mystLight};\n    animation:cron-spin 0.8s linear infinite; }\n  @keyframes cron-spin{ to{ transform:rotate(360deg); } }\n  .gen-ctx{ display:inline-flex; align-items:center; gap:6px; cursor:pointer; color:${PAL.gold}; font-size:12px;\n    letter-spacing:0.5px; padding:5px 10px; border-radius:4px; border:2px solid ${PAL.goldDeep};\n    background:rgba(216,178,76,0.08); box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08); }\n  .gen-ctx:hover{ filter:brightness(1.1); }\n  .gal-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:10px; margin-top:14px; }\n  .gal-cell{ position:relative; border:3px solid ${PAL.edgeDark}; border-radius:8px; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone1},${PAL.edgeDark}); aspect-ratio:1/1;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08), 0 4px 8px rgba(0,0,0,0.5); cursor:pointer; }\n  .gal-cell.sel{ border-color:${PAL.gold}; box-shadow:0 0 12px rgba(216,178,76,0.6), inset 0 0 6px rgba(216,178,76,0.25); }\n  .gal-cell img{ width:100%; height:100%; object-fit:cover; display:block; }\n  .gal-badge{ position:absolute; top:5px; left:5px; font-size:10px; letter-spacing:0.5px; padding:2px 7px; border-radius:10px;\n    border:2px solid ${PAL.edgeDark}; color:${PAL.edgeDark}; background:${PAL.goldLight}; }\n  .gal-ref{ position:absolute; top:4px; right:4px; font-size:10px; letter-spacing:0.3px; padding:2px 7px; border-radius:10px;\n    border:2px solid ${PAL.edgeDark}; color:${PAL.cream}; background:rgba(28,20,13,0.78); }\n  .gal-ref.on{ background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); color:${PAL.edgeDark}; }\n  .gal-foot{ position:absolute; left:0; right:0; bottom:0; padding:4px 6px; font-size:11px; line-height:1.2;\n    color:${PAL.cream}; background:rgba(18,12,7,0.82); max-height:40%; overflow:hidden; }\n  /* hero reference chips */\n  .hero-refs{ display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }\n  .hero-chip{ display:inline-flex; align-items:center; gap:8px; cursor:pointer; padding:5px 10px 5px 6px;\n    border-radius:22px; border:2px solid ${PAL.edgeDark}; background:linear-gradient(150deg, #2e2418 0%, ${PAL.stone1} 100%);\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.06); transition:border-color 140ms ease, box-shadow 140ms ease; }\n  .hero-chip:hover{ filter:brightness(1.08); }\n  .hero-chip.on{ border-color:${PAL.gold}; box-shadow:0 0 10px rgba(216,178,76,0.5), inset 0 0 5px rgba(216,178,76,0.2); }\n  .hero-chip .hc-ava{ width:32px; height:32px; border-radius:50%; overflow:hidden; flex:0 0 auto;\n    border:2px solid ${PAL.goldDeep}; background:linear-gradient(180deg,${PAL.stone1},${PAL.edgeDark});\n    display:flex; align-items:center; justify-content:center; }\n  .hero-chip .hc-ava img{ width:100%; height:100%; object-fit:cover; }\n  .hero-chip .hc-ini{ font-family:'Jacquard 24','Alegreya',serif; font-size:14px; color:${PAL.goldLight}; }\n  .hero-chip .hc-txt{ display:flex; flex-direction:column; line-height:1.1; }\n  .hero-chip .hc-name{ font-size:13px; color:${PAL.cream}; }\n  .hero-chip .hc-meta{ font-size:10px; color:${PAL.muted}; }\n  .hero-chip .hc-mark{ font-size:14px; width:16px; text-align:center; color:${PAL.gold}; }\n  .hero-chip.on .hc-mark{ color:${PAL.goldLight}; }\n  /* lightbox */\n  .lightbox-scrim{ position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center;\n    background:rgba(8,5,2,0.86); padding:5vh 5vw; cursor:zoom-out; }\n  .lightbox-frame{ position:relative; max-width:90vw; max-height:90vh; display:flex; cursor:default;\n    border:4px solid ${PAL.edgeDark}; border-radius:6px; background:#1c140d;\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, 0 10px 30px rgba(0,0,0,0.7); padding:8px; }\n  .lightbox-img{ max-width:calc(90vw - 24px); max-height:calc(90vh - 24px); object-fit:contain; display:block; border-radius:3px; }\n  .lightbox-close{ position:absolute; top:-16px; right:-16px; width:38px; height:38px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:22px; line-height:1; color:${PAL.edgeDark};\n    background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:3px solid ${PAL.edgeDark}; border-radius:50%;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 3px 8px rgba(0,0,0,0.5); }\n  .lightbox-close:hover{ filter:brightness(1.1); }\n  `,\n\n  sid(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/possessao\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if (d2.state.summary == null) d2.state.summary = '';\n    if (d2.state.genPrompt == null) d2.state.genPrompt = '';\n    if (d2.state.refSel == null) d2.state.refSel = {};\n    if (d2.state.heroSel == null) d2.state.heroSel = {};\n    if (d2.state.ignored == null) d2.state.ignored = {};\n    if (d2.state.added == null) d2.state.added = {};\n    if (d2.state.linked == null) d2.state.linked = {};\n    if (!d2.state.loadedSession){ this.loadSession(d2); }\n    try { d2.on(window, 'popstate', ()=>{ try { this.loadSession(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back', 'click', ()=>{ const sid = this.sid(d2); this.goBack(d2, sid ? ('/session/'+sid) : '/'); });\n    d2.on('#summaryTA', 'input', (e)=>{ d2.state.summary = e.target.value; });\n    d2.on('#genPromptTA', 'input', (e)=>{ d2.state.genPrompt = e.target.value; });\n    d2.on('#genCtxBtn', 'click', ()=>{ this.appendContext(d2); });\n    d2.on('.gal-cell', 'click', (e)=>{\n      const cell = e.target.closest('.gal-cell');\n      if (!cell) return;\n      // The .gal-ref badge toggles the image as a generation reference; clicking\n      // anywhere else on the cell opens the lightbox (view larger).\n      if (e.target.closest('.gal-ref')){\n        if (cell.dataset.img){ this.toggleRef(d2, cell.dataset.img); }\n        return;\n      }\n      if (cell.dataset.url){ this.openLightbox(d2, cell.dataset.url); }\n    });\n    // Hero reference chips: toggle selection (portrait + description context).\n    d2.on('.hero-chip', 'click', (e)=>{\n      const chip = e.target.closest('.hero-chip');\n      if (chip && chip.dataset.hero){ this.toggleHero(d2, chip.dataset.hero); }\n    });\n    // Lightbox: clicking the scrim or the close button dismisses it.\n    d2.on('.lightbox-scrim', 'click', (e)=>{\n      if (e.target.closest('.lightbox-close') || e.target.classList.contains('lightbox-scrim')){\n        this.closeLightbox(d2);\n      }\n    });\n  },\n\n  loadSession(d2){\n    const sid = this.sid(d2);\n    if (!sid){ return; }\n    d2.state.loadedSession = sid;\n    d2.rush('sessionDetail').invoke({ sessionId: sid }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      const s = (data && data.session) ? data.session : {};\n      d2.state.session = { id: s.id || sid, title: s.title, number: s.number, campaignId: s.campaignId };\n      d2.state.heroes = Array.isArray(data.heroes) ? data.heroes : [];\n      if (d2.state.summary === '' && s.summary){ d2.state.summary = s.summary; const ta = d2.$('#summaryTA'); if(ta){ ta.value = s.summary; } }\n      this.loadGallery(d2, sid);\n      d2.refresh();\n    }).catch((e)=>{ d2.log('loadSession', e); });\n  },\n\n  cid(d2){ return (d2.state.session && d2.state.session.campaignId) || ''; },\n\n  genSummary(d2){\n    if (d2.state.summarizing) return;\n    const sid = this.sid(d2);\n    d2.state.summarizing = true; d2.state.summaryErr = null; d2.refresh();\n    d2.rush('summarizeSession').invoke({ sessionId: sid }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      if (data && data.recap){\n        d2.state.summary = data.recap;\n        const ta = d2.$('#summaryTA'); if(ta){ ta.value = data.recap; }\n      } else { d2.state.summaryErr = (data && data.error) || 'Não foi possível gerar o resumo.'; }\n      d2.state.summarizing = false; d2.refresh();\n    }).catch((e)=>{ d2.log('genSummary', e); d2.state.summaryErr = 'Falha ao falar com o Cronista.'; d2.state.summarizing = false; d2.refresh(); });\n  },\n\n  saveSummary(d2){\n    if (d2.state.savingSummary) return;\n    const sid = this.sid(d2);\n    d2.state.savingSummary = true; d2.state.summarySaved = false; d2.refresh();\n    d2.rush('updateSession').invoke({ sessionId: sid, summary: d2.state.summary || '' }).then(()=>{\n      d2.state.savingSummary = false; d2.state.summarySaved = true; d2.refresh();\n    }).catch((e)=>{ d2.log('saveSummary', e); d2.state.savingSummary = false; d2.state.summaryErr = 'Falha ao salvar.'; d2.refresh(); });\n  },\n\n  // Salva o resumo editado E encerra a sessão (status:'encerrada') num único write,\n  // depois navega de volta para a sessão (que agora mostra a crônica somente-leitura).\n  saveAndClose(d2){\n    if (d2.state.closingSession) return;\n    const sid = this.sid(d2);\n    if (!sid){ return; }\n    d2.state.closingSession = true; d2.state.summaryErr = null; d2.refresh();\n    d2.rush('updateSession').invoke({ sessionId: sid, summary: d2.state.summary || '', status: 'encerrada' }).then(()=>{\n      d2.state.closingSession = false; d2.state.summarySaved = true; d2.refresh();\n      this.go(d2, '/session/' + sid);\n    }).catch((e)=>{ d2.log('saveAndClose', e); d2.state.closingSession = false; d2.state.summaryErr = 'Falha ao encerrar a sessão.'; d2.refresh(); });\n  },\n\n  extract(d2){\n    if (d2.state.extracting) return;\n    const sid = this.sid(d2);\n    d2.state.extracting = true; d2.state.extractErr = null; d2.refresh();\n    d2.rush('extractFromSession').invoke({ sessionId: sid }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      const unwrapped = (data && data.data) ? data.data : data;\n      if (unwrapped && unwrapped.suggestions){\n        d2.state.suggestions = unwrapped.suggestions;\n      } else if (unwrapped && unwrapped.error){\n        d2.state.extractErr = unwrapped.error;\n      } else { d2.state.extractErr = 'Nenhuma sugestão retornada.'; }\n      d2.state.extracting = false; d2.refresh();\n    }).catch((e)=>{ d2.log('extract', e); d2.state.extractErr = 'Falha ao extrair entidades.'; d2.state.extracting = false; d2.refresh(); });\n  },\n\n  // ---- session image gallery + AI generation ----\n  loadGallery(d2, sessionId){\n    const sid = sessionId || this.sid(d2);\n    if (!sid) return;\n    if (d2.state.galleryLoadingId === sid) return;\n    d2.state.galleryLoadingId = sid;\n    d2.rush('imagesBySession').invoke({ sessionId: sid }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      d2.state.gallery = (res && res.results) || [];\n      d2.state.galleryId = sid;\n      d2.state.galleryLoadingId = null;\n      d2.refresh();\n    }).catch((e)=>{ d2.log('loadGallery', e); d2.state.galleryLoadingId = null; });\n  },\n  toggleRef(d2, imageId){\n    if (!imageId) return;\n    const sel = Object.assign({}, d2.state.refSel||{});\n    if (sel[imageId]) { delete sel[imageId]; } else { sel[imageId] = true; }\n    d2.state.refSel = sel;\n    d2.refresh();\n  },\n  refUrls(d2){\n    const sel = d2.state.refSel || {};\n    const galUrls = (d2.state.gallery||[]).filter(im=>sel[im._id]).map(im=>im.url).filter(u=>u);\n    // Add the portrait URLs of the selected heroes so the generator references\n    // the real party visually. Dedupe against the gallery selection.\n    const hsel = d2.state.heroSel || {};\n    const heroUrls = (d2.state.heroes||[]).filter(h=>hsel[h.id]).map(h=>h.portrait).filter(u=>u);\n    const out = galUrls.slice();\n    heroUrls.forEach(u=>{ if(out.indexOf(u) === -1) out.push(u); });\n    return out;\n  },\n  toggleHero(d2, heroId){\n    if (!heroId) return;\n    const sel = Object.assign({}, d2.state.heroSel||{});\n    if (sel[heroId]) { delete sel[heroId]; } else { sel[heroId] = true; }\n    d2.state.heroSel = sel;\n    d2.refresh();\n  },\n  // Build the textual \"Personagens na cena:\" suffix for the selected heroes,\n  // including race, class and a trimmed historia when present.\n  heroPromptSuffix(d2){\n    const hsel = d2.state.heroSel || {};\n    const chosen = (d2.state.heroes||[]).filter(h=>hsel[h.id]);\n    if (!chosen.length) return '';\n    const bits = chosen.map(h=>{\n      const rc = [h.race, h.klass].map(x=>(x||'').trim()).filter(x=>x).join(' ');\n      let s = (h.name||'') + (rc ? (' (' + rc) : ' (');\n      const hist = (h.historia||'').trim();\n      if (hist){\n        const t = hist.length > 200 ? (hist.slice(0,200) + '…') : hist;\n        s += (rc ? ', ' : '') + 'descrição: ' + t;\n      }\n      s += ')';\n      return s;\n    });\n    return '\\n\\nPersonagens na cena: ' + bits.join('; ');\n  },\n  openLightbox(d2, url){\n    if (!url) return;\n    d2.state.lightboxUrl = url;\n    d2.refresh();\n  },\n  closeLightbox(d2){\n    d2.state.lightboxUrl = null;\n    d2.refresh();\n  },\n  appendContext(d2){\n    const sum = (d2.state.summary||'').trim();\n    if (!sum){ d2.dialog.alert('Não há resumo da sessão para usar como contexto.'); return; }\n    const cur = (d2.state.genPrompt||'').trim();\n    const next = cur ? (cur + '\\n\\n' + sum) : sum;\n    d2.state.genPrompt = next;\n    const ta = d2.$('#genPromptTA'); if (ta){ ta.value = next; }\n    d2.refresh();\n  },\n  genImage(d2){\n    if (d2.state.generating) return;\n    const sid = this.sid(d2);\n    const cid = this.cid(d2);\n    if (!sid){ d2.dialog.alert('Sessão não carregada.'); return; }\n    const prompt = (d2.state.genPrompt||'').trim();\n    if (!prompt){ d2.dialog.alert('Descreva a imagem que deseja gerar.'); return; }\n    const refUrls = this.refUrls(d2);\n    const finalPrompt = prompt + this.heroPromptSuffix(d2);\n    d2.state.generating = true;\n    d2.state.genErr = null;\n    d2.refresh();\n    const payload = { sessionId: sid, campaignId: cid, prompt: finalPrompt };\n    if (refUrls.length) payload.refUrls = refUrls;\n    d2.rush('genSessionImage').invoke(payload).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      d2.state.generating = false;\n      if (res && res.ok && res.image){\n        // Optimistically show the new image immediately, then refetch to reconcile.\n        const img = res.image;\n        const cur = (d2.state.gallery || []).filter(im => im && im._id !== img._id);\n        d2.state.gallery = [img].concat(cur);\n        d2.state.genPrompt = '';\n        const ta = d2.$('#genPromptTA'); if (ta){ ta.value = ''; }\n        d2.state.galleryLoadingId = null;\n        this.loadGallery(d2, sid);\n      } else {\n        d2.state.genErr = (res && res.error) || 'Falha ao gerar a imagem.';\n      }\n      d2.refresh();\n    }).catch((e)=>{ d2.log('genImage', e); d2.state.generating = false; d2.state.genErr = 'Falha ao falar com o gerador de imagens.'; d2.refresh(); });\n  },\n  galleryPanel(d2){\n    const imgs = d2.state.gallery || [];\n    const sel = d2.state.refSel || {};\n    const generating = !!d2.state.generating;\n    const prompt = d2.state.genPrompt || '';\n    const refCount = this.refUrls(d2).length;\n    const cells = imgs.length ? imgs.map(im=>{\n      const on = !!sel[im._id];\n      const isGen = im.source === 'gen';\n      const badge = `<span class=\"gal-badge\">${isGen?'gerada':'enviada'}</span>`;\n      const foot = im.prompt ? `<div class=\"gal-foot\">${esc(im.prompt)}</div>` : '';\n      return `<div class=\"gal-cell ${on?'sel':''}\" data-img=\"${escA(im._id)}\" data-url=\"${escA(im.url||'')}\">\n        <img src=\"${escA(im.url||'')}\" alt=\"\"/>\n        ${badge}\n        <span class=\"gal-ref ${on?'on':''}\">${on?'✓ ref':'usar como referência'}</span>\n        ${foot}\n      </div>`;\n    }).join('') : `<div class=\"empty\">Nenhuma imagem ainda. Gere ou envie imagens na sessão.</div>`;\n    const ctxBtn = (d2.state.summary||'').trim()\n      ? `<span class=\"gen-ctx\" id=\"genCtxBtn\">✦ usar contexto da sessão</span>` : '';\n    let status = '';\n    if (d2.state.genErr) status = `<span class=\"err\">${esc(d2.state.genErr)}</span>`;\n    else if (refCount) status = `<span class=\"gen-status\">${refCount} referência(s) selecionada(s)</span>`;\n    // Prominent, impossible-to-miss loading banner while the ~20s call runs.\n    const genBanner = generating\n      ? `<div class=\"gen-banner\"><span class=\"gen-spin\"></span><span>Gerando imagem… isto pode levar ~20s. Aguarde nesta tela.</span></div>`\n      : '';\n    const genBtn = d2.render(GB, { label: generating ? 'Gerando…' : 'Gerar', kind:'gold', size:'sm', glyph:'✨' }, { key:'pb-genimg', onPress:()=>{ if (d2.state.generating) return; this.genImage(d2); } });\n    // Hero reference chips — each selectable hero adds its portrait (refUrl) +\n    // textual description to the next generation.\n    const heroes = d2.state.heroes || [];\n    const hsel = d2.state.heroSel || {};\n    const heroChips = heroes.length ? heroes.map(h=>{\n      const on = !!hsel[h.id];\n      const ini = (h.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n      const thumb = h.portrait ? `<img src=\"${escA(h.portrait)}\" alt=\"\"/>` : `<span class=\"hc-ini\">${esc(ini)}</span>`;\n      const meta = [h.race, h.klass].map(x=>(x||'').trim()).filter(x=>x).join(' · ');\n      return `<div class=\"hero-chip ${on?'on':''}\" data-hero=\"${escA(h.id)}\">\n        <span class=\"hc-ava\">${thumb}</span>\n        <span class=\"hc-txt\"><span class=\"hc-name\">${esc(h.name||'')}</span>${meta?`<span class=\"hc-meta\">${esc(meta)}</span>`:''}</span>\n        <span class=\"hc-mark\">${on?'✓':'+'}</span>\n      </div>`;\n    }).join('') : '';\n    const heroRefHtml = heroes.length ? `\n      <div class=\"label\" style=\"margin-top:14px\">Heróis</div>\n      <div class=\"hint\">Selecione heróis para incluir o retrato e a descrição na geração.</div>\n      <div class=\"hero-refs\">${heroChips}</div>` : '';\n    return `<div class=\"panel\">\n      <div class=\"sec-head\">\n        <div class=\"sec-ttl\">Gerar imagem</div>\n      </div>\n      <div class=\"label\">Descreva a cena</div>\n      <textarea class=\"gf-input\" id=\"genPromptTA\" placeholder=\"descreva a imagem…\" ${generating?'disabled':''}>${esc(prompt)}</textarea>\n      <div class=\"gen-row ${generating?'is-generating':''}\">\n        ${ctxBtn}\n        <div class=\"gen-w\">${genBtn}</div>\n        ${status}\n      </div>\n      ${genBanner}\n      ${heroRefHtml}\n      <div class=\"label\" style=\"margin-top:14px\">Galeria da sessão</div>\n      <div class=\"hint\">Toque no selo \"referência\" para usar a imagem na geração; toque na imagem para ampliá-la.</div>\n      <div class=\"gal-grid\">${cells}</div>\n    </div>`;\n  },\n  lightboxHtml(d2){\n    const url = d2.state.lightboxUrl;\n    if (!url) return '';\n    return `<div class=\"lightbox-scrim\">\n      <div class=\"lightbox-frame\">\n        <button class=\"lightbox-close\" title=\"Fechar\">×</button>\n        <img class=\"lightbox-img\" src=\"${escA(url)}\" alt=\"\"/>\n      </div>\n    </div>`;\n  },\n\n  ckey(group, item){\n    const nm = (group==='quests') ? item.title : item.name;\n    return group + '::' + (nm||'').toLowerCase();\n  },\n\n  ignore(d2, group, item){\n    const ig = Object.assign({}, d2.state.ignored||{});\n    ig[this.ckey(group, item)] = true;\n    d2.state.ignored = ig; d2.refresh();\n  },\n\n  addEntity(d2, group, item){\n    const k = this.ckey(group, item);\n    if ((d2.state.added||{})[k] === 'pending' || (d2.state.added||{})[k] === 'done') return;\n    const cid = this.cid(d2);\n    if (!cid){ d2.dialog.alert('Campanha não carregada ainda.'); return; }\n    const setStatus = (st)=>{ const a = Object.assign({}, d2.state.added||{}); a[k] = st; d2.state.added = a; d2.refresh(); };\n    setStatus('pending');\n    let handler, payload;\n    if (group === 'npcs'){\n      handler = 'saveNpc';\n      payload = { campaignId: cid, name: item.name, type: item.type||'neutro', description: item.description||'', faction: item.faction||'', locationName: item.locationName||'', status: item.status||'vivo', tags: item.tags||'', secret:'' };\n    } else if (group === 'locais'){\n      handler = 'saveLocation';\n      payload = { campaignId: cid, name: item.name, type: item.type||'', description: item.description||'', parentName: item.parentName||'', tags: item.tags||'' };\n    } else if (group === 'quests'){\n      handler = 'saveQuest';\n      payload = { campaignId: cid, title: item.title, type: item.type||'secundária', status: item.status||'ativa', giverName: item.giverName||item.giver||'', reward: item.reward||'', description: item.description||'', tags: item.tags||'', secret:'' };\n    } else if (group === 'faccoes'){\n      handler = 'saveFaction';\n      payload = { campaignId: cid, name: item.name, type: item.type||'neutro', description: item.description||'', tags: item.tags||'' };\n    }\n    d2.rush(handler).invoke(payload).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      if (data && data.ok){ setStatus('done'); } else { setStatus(null); d2.dialog.alert('Falha ao adicionar.'); }\n    }).catch((e)=>{ d2.log('addEntity', e); setStatus(null); d2.dialog.alert('Falha ao adicionar ao mundo.'); });\n  },\n\n  linkEntity(d2, group, item){\n    const k = this.ckey(group, item);\n    const ln = Object.assign({}, d2.state.linked||{});\n    ln[k] = 'pending'; d2.state.linked = ln; d2.refresh();\n    const cid = this.cid(d2);\n    // Link a suggested quest to an existing giver NPC by updating the existing quest's giverName.\n    if (group === 'quests' && item.existingId){\n      d2.rush('saveQuest').invoke({ _id: item.existingId, campaignId: cid, title: item.title, type: item.type||'secundária', status:'ativa', giverName: item.giverName||item.giver||'', reward:'', description: item.description||'', tags:'', secret:'' }).then((raw)=>{\n        const data = (raw && raw.data) ? raw.data : raw;\n        const ln2 = Object.assign({}, d2.state.linked||{}); ln2[k] = (data && data.ok) ? 'done' : null; d2.state.linked = ln2; d2.refresh();\n      }).catch((e)=>{ d2.log('linkQuest', e); const ln2 = Object.assign({}, d2.state.linked||{}); ln2[k]=null; d2.state.linked=ln2; d2.refresh(); });\n    } else {\n      const ln2 = Object.assign({}, d2.state.linked||{}); ln2[k]='done'; d2.state.linked=ln2; d2.refresh();\n    }\n  },\n\n  card(d2, group, item){\n    const nm = (group==='quests') ? item.title : item.name;\n    const k = this.ckey(group, item);\n    if ((d2.state.ignored||{})[k]) return '';\n    const addState = (d2.state.added||{})[k];\n    const linkState = (d2.state.linked||{})[k];\n    const exists = !!item.exists;\n    const cls = exists ? 'exists' : (addState==='done' ? 'added' : '');\n    let badge;\n    if (addState==='done') badge = `<span class=\"badge added\">✓ adicionado</span>`;\n    else if (exists) badge = `<span class=\"badge exists\">já cadastrado</span>`;\n    else badge = `<span class=\"badge new\">novo</span>`;\n\n    const typeTxt = (group==='quests') ? (item.type||'') : (item.type||'');\n    const descBits = [];\n    if (item.description) descBits.push(esc(item.description));\n    if (group==='quests' && (item.giverName||item.giver)) descBits.push('oferecido por: '+esc(item.giverName||item.giver));\n    const descHtml = descBits.length ? `<div class=\"card-desc\">${descBits.join(' · ')}</div>` : '';\n\n    // Relations as Golden Axe pills\n    const rels = [];\n    const pill = (txt, glyph, cls)=> `<span class=\"rel${cls?(' '+cls):''}\">${glyph?`<span class=\"rel-g\">${glyph}</span>`:''}${esc(txt)}</span>`;\n    const tagPills = (raw)=>{ String(raw||'').split(',').map(t=>t.trim()).filter(t=>t).forEach(t=>rels.push(pill(t,'','tag'))); };\n    if (group==='npcs'){\n      if (item.locationName) rels.push(pill(item.locationName,'⌂'));\n      if (item.faction) rels.push(pill(item.faction,'⚑'));\n      if (item.status) rels.push(pill(item.status,'','status'));\n      tagPills(item.tags);\n    } else if (group==='locais'){\n      if (item.parentName) rels.push(pill(item.parentName,'⌂'));\n      tagPills(item.tags);\n    } else if (group==='quests'){\n      if (item.reward) rels.push(pill(item.reward,'🎁'));\n      if (item.status) rels.push(pill(item.status,'','status'));\n      tagPills(item.tags);\n    } else if (group==='faccoes'){\n      tagPills(item.tags);\n    }\n    const relsHtml = rels.length ? `<div class=\"rels\">${rels.join('')}</div>` : '';\n\n    // Actions\n    let actions = '';\n    if (!exists && addState!=='done'){\n      const lbl = addState==='pending' ? 'Adicionando…' : 'Adicionar ao Mundo';\n      actions += `<div class=\"ca-slot add\">${d2.render(GB, { label: lbl, kind:'gold', size:'sm', glyph:'✚' }, { key:'add-'+k, onPress:()=>this.addEntity(d2, group, item) })}</div>`;\n    }\n    // Linking: a suggested quest whose giver resolves to an existing NPC, and the quest already exists -> offer link.\n    const canLink = (group==='quests' && exists && item.giverNpcId && linkState!=='done');\n    if (canLink){\n      const llbl = linkState==='pending' ? 'Vinculando…' : ('Vincular a '+esc(item.giverName||''));\n      actions += `<div class=\"ca-slot link\">${d2.render(GB, { label: llbl, kind:'gold', size:'sm', ghost:true, glyph:'⚭' }, { key:'link-'+k, onPress:()=>this.linkEntity(d2, group, item) })}</div>`;\n    }\n    if (addState!=='done'){\n      actions += `<div class=\"ca-slot ignore\">${d2.render(GB, { label:'Ignorar', kind:'danger', size:'sm', ghost:true }, { key:'ign-'+k, onPress:()=>this.ignore(d2, group, item) })}</div>`;\n    }\n    const linkNote = (linkState==='done') ? `<div class=\"link-note\">✓ vínculo aplicado</div>` : '';\n\n    return `<div class=\"card ${cls}\" data-key=\"${escA(k)}\">\n      <div class=\"card-top\">\n        <div>\n          <div class=\"card-name\">${esc(nm)}</div>\n          ${typeTxt ? `<div class=\"card-type\">${esc(typeTxt)}</div>` : ''}\n        </div>\n        ${badge}\n      </div>\n      ${descHtml}\n      ${relsHtml}\n      <div class=\"card-actions\">${actions}</div>\n      ${linkNote}\n    </div>`;\n  },\n\n  render(d2){\n    const self = this;\n    const sess = d2.state.session || {};\n    const sumLoading = !!d2.state.summarizing;\n    const extracting = !!d2.state.extracting;\n    const sug = d2.state.suggestions;\n\n    const backBtn = `<button class=\"back\">‹ Voltar</button>`;\n    const genBtn = d2.render(GB, { label: sumLoading ? 'Gerando…' : 'Gerar resumo ✨', kind:'gold', size:'sm' }, { key:'pb-gen', onPress:()=>self.genSummary(d2) });\n    const saveBtn = d2.render(GB, { label: d2.state.savingSummary ? 'Salvando…' : 'Salvar resumo', kind:'gold', size:'sm', glyph:'◆' }, { key:'pb-save', onPress:()=>self.saveSummary(d2) });\n    const closeBtn = d2.render(GB, { label: d2.state.closingSession ? 'Encerrando…' : 'Salvar e encerrar sessão', kind:'gold', size:'sm' }, { key:'pb-close', onPress:()=>self.saveAndClose(d2) });\n    const extractBtn = d2.render(GB, { label: extracting ? 'Extraindo…' : 'Extrair entidades ✨', kind:'gold', size:'sm' }, { key:'pb-extract', onPress:()=>self.extract(d2) });\n\n    const sumStatus = d2.state.summaryErr ? `<span class=\"err\">${esc(d2.state.summaryErr)}</span>` : (d2.state.summarySaved ? `<span class=\"ok-msg\">✓ resumo salvo</span>` : '');\n\n    // Extraction body\n    let extractBody = '';\n    if (extracting){\n      extractBody = `<div class=\"thinking\">As névoas revelam as entidades da sessão…</div>`;\n    } else if (d2.state.extractErr){\n      extractBody = `<div class=\"err\">${esc(d2.state.extractErr)}</div>`;\n    } else if (sug){\n      const groupsHtml = GROUPS.map(g=>{\n        const items = (sug[g.key] || []);\n        const visible = items.map(it=>self.card(d2, g.key, it)).filter(h=>h);\n        if (!visible.length) return '';\n        return `<div class=\"group\">\n          <div class=\"group-ttl\"><span>${g.glyph}</span> ${esc(g.title)} <span class=\"card-type\">(${visible.length})</span></div>\n          <div class=\"cards\">${visible.join('')}</div>\n        </div>`;\n      }).filter(h=>h).join('');\n      extractBody = groupsHtml || `<div class=\"empty\">Nenhuma entidade nova encontrada nas anotações.</div>`;\n    } else {\n      extractBody = `<div class=\"hint\">Clique em \"Extrair entidades\" para detectar NPCs, locais, quests e facções mencionados nas anotações desta sessão.</div>`;\n    }\n\n    return `<div class=\"root\">\n      ${backBtn}\n      <div class=\"hero\">\n        <div class=\"title\">Pós-sessão</div>\n        <div class=\"subtitle\">${sess.number!=null ? ('Sessão '+esc(sess.number)+' — '+esc(sess.title||'')) : 'Resumo e extração para o Mundo'}</div>\n      </div>\n\n      <div class=\"panel\">\n        <div class=\"sec-head\">\n          <div class=\"sec-ttl\">Resumo</div>\n          <div class=\"sec-act\">${genBtn}</div>\n        </div>\n        <div class=\"label\">Recap narrativo (editável)</div>\n        <textarea class=\"gf-input\" id=\"summaryTA\" placeholder=\"Gere ou escreva o resumo da sessão…\">${esc(d2.state.summary||'')}</textarea>\n        <div class=\"save-row\">${sumStatus}<div class=\"save-w\">${saveBtn}</div><div class=\"save-w\" style=\"width:auto; min-width:200px\">${closeBtn}</div></div>\n      </div>\n\n      <div class=\"panel\">\n        <div class=\"sec-head\">\n          <div class=\"sec-ttl\">Extração para o Mundo</div>\n          <div class=\"sec-act\">${extractBtn}</div>\n        </div>\n        ${extractBody}\n      </div>\n\n      ${self.galleryPanel(d2)}\n      ${self.lightboxHtml(d2)}\n    </div>`;\n  }\n};"}}}}}}}},{"_id":"6a342beee064f2dc1696bba4","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"OraculoBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', myst:'#5b3f7a', mystLight:'#9a78c4', mystDeep:'#3a2752' };\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nconst EXAMPLES = [\n  'O que sabemos sobre Strahd?',\n  'Quais quests estão ativas?',\n  'Quem está em Barovia?',\n  'Quem é Madame Eva?'\n];\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:14px; padding:8px; font-family:'Alegreya',sans-serif; color:${PAL.cream}; background:#1c140d; }\n  .topbar{ display:flex; align-items:center; gap:10px; }\n  .back-w{ width:110px; height:40px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .hero{ text-align:center; padding:6px 4px 2px; }\n  .title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:42px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}, 0 0 18px rgba(154,120,196,0.5); line-height:1; }\n  .subtitle{ color:${PAL.mystLight}; font-size:13px; letter-spacing:1px; margin-top:8px; opacity:0.9; }\n  .panel{ box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.mystDeep} 0%, ${PAL.stone1} 60%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .label{ color:${PAL.gold}; font-size:12px; letter-spacing:1.5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-bottom:6px; }\n  .root textarea.gf-input{\n    box-sizing:border-box; width:100%; background:#1f1630; color:#ece0c4;\n    font-family:'Alegreya',sans-serif; font-size:15px; border:3px solid #1c140d; border-radius:4px;\n    padding:10px 12px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -2px -2px 0 rgba(106,84,54,0.4); outline:none;\n    min-height:64px; resize:vertical; line-height:1.4; }\n  .root textarea.gf-input:focus{ border-color:${PAL.mystLight}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), 0 0 8px rgba(154,120,196,0.6); }\n  .ask-row{ display:flex; justify-content:flex-end; margin-top:12px; }\n  .ask-w{ width:200px; height:52px; }\n  .examples{ display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; }\n  .ex{ cursor:pointer; font-size:12px; letter-spacing:0.5px; padding:7px 12px; border-radius:14px;\n    border:2px solid ${PAL.mystDeep}; background:rgba(91,63,122,0.22); color:${PAL.mystLight}; transition:filter 140ms ease; }\n  .ex:hover{ filter:brightness(1.25); }\n  .answer-panel{ box-sizing:border-box; padding:18px 20px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.05) 0px, rgba(0,0,0,0.05) 2px, rgba(255,255,255,0.012) 2px, rgba(255,255,255,0.012) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2a1f3a 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 30px rgba(0,0,0,0.55); }\n  .answer-head{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; margin-bottom:10px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .answer-body{ color:${PAL.cream}; font-size:15px; line-height:1.6; white-space:pre-wrap; }\n  .answer-body.err{ color:${PAL.mystLight}; }\n  .thinking{ color:${PAL.mystLight}; font-size:15px; font-style:italic; }\n  `,\n  cid(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/oraculo\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if (d2.state.question == null) d2.state.question = '';\n    try { d2.on(window, 'popstate', ()=>{ try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back', 'click', ()=>{ const id = this.cid(d2); this.goBack(d2, '/campaign/'+id); });\n    d2.on('.gf-input', 'input', (e)=>{ d2.state.question = e.target.value; });\n    d2.on('.ex', 'click', (e)=>{\n      const el = e.target.closest('.ex');\n      if (!el) return;\n      d2.state.question = el.getAttribute('data-q') || '';\n      const ta = d2.$('.gf-input');\n      if (ta) ta.value = d2.state.question;\n    });\n  },\n  async consult(d2){\n    const q = (d2.state.question || '').trim();\n    if (!q){ d2.state.answer = 'As névoas aguardam uma pergunta...'; d2.state.isErr = true; d2.refresh(); return; }\n    if (d2.state.loading) return;\n    d2.state.loading = true;\n    d2.state.answer = '';\n    d2.state.isErr = false;\n    d2.refresh();\n    const id = this.cid(d2);\n    if (!id){ d2.state.answer = 'Campanha não encontrada.'; d2.state.isErr = true; d2.state.loading = false; d2.refresh(); return; }\n    try {\n      const res = await d2.rush('askOraculo').invoke({ campaignId: id, question: q });\n      const data = (res && res.results) ? res.results : (res && res.data ? res.data : res);\n      const unwrapped = (data && data.data) ? data.data : data;\n      if (unwrapped && unwrapped.answer){\n        d2.state.answer = unwrapped.answer;\n        d2.state.isErr = false;\n      } else if (unwrapped && unwrapped.error){\n        d2.state.answer = unwrapped.error;\n        d2.state.isErr = true;\n      } else {\n        d2.state.answer = 'As névoas se fecharam em silêncio.';\n        d2.state.isErr = true;\n      }\n    } catch(e){\n      d2.state.answer = 'As névoas se agitaram e a consulta se perdeu: ' + String(e && e.message ? e.message : e);\n      d2.state.isErr = true;\n    }\n    d2.state.loading = false;\n    d2.refresh();\n  },\n  render(d2){\n    const self = this;\n    const q = d2.state.question || '';\n    const loading = !!d2.state.loading;\n    const backBtn = `<button class=\"back\">‹ Voltar</button>`;\n    const askBtn = d2.render('cronica-app@GoldenButton', { label: loading ? 'Consultando…' : 'Consultar 🔮', kind:'gold' }, { key:'or-ask', onPress:()=>{ self.consult(d2); } });\n\n    const examples = EXAMPLES.map(e => `<div class=\"ex\" data-q=\"${esc(e).replace(/\"/g,'&quot;')}\">${esc(e)}</div>`).join('');\n\n    let answerBlock = '';\n    if (loading){\n      answerBlock = `<div class=\"answer-panel\"><div class=\"answer-head\">O Oráculo medita…</div><div class=\"thinking\">As névoas se agitam, formando uma resposta…</div></div>`;\n    } else if (d2.state.answer){\n      answerBlock = `<div class=\"answer-panel\"><div class=\"answer-head\">As névoas revelam</div><div class=\"answer-body ${d2.state.isErr?'err':''}\">${esc(d2.state.answer)}</div></div>`;\n    }\n\n    return `<div class=\"root\">\n      ${backBtn}\n      <div class=\"hero\">\n        <div class=\"title\">O Oráculo</div>\n        <div class=\"subtitle\">Faça uma pergunta às névoas sobre a sua crônica</div>\n      </div>\n      <div class=\"panel\">\n        <div class=\"label\">Sua pergunta</div>\n        <textarea class=\"gf-input\" placeholder=\"ex: o que sabemos sobre Strahd?\">${esc(q)}</textarea>\n        <div class=\"ask-row\"><div class=\"ask-w\">${askBtn}</div></div>\n      </div>\n      <div class=\"examples\">${examples}</div>\n      ${answerBlock}\n    </div>`;\n  }\n};"}}}}}}}},{"_id":"6a341d0527423d3c53700bad","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"CritTablesBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = {\n  bg:'#1c140d', panel:'#241a10', panel2:'#2c2013', stone:'#3a2c1a',\n  gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a',\n  blood:'#9c3320', bloodLight:'#c4543a', cream:'#ece0c4', edgeDark:'#120c07',\n  muted:'#a08a5e', edgeLight:'#6a5436'\n};\nconst GB = 'cronica-app@GoldenButton';\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; overflow-y:auto; box-sizing:border-box;\n    font-family:'Alegreya',sans-serif; color:${PAL.cream}; background:#1c140d;\n    padding:16px 16px 48px; display:flex; flex-direction:column; gap:16px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .hdr{ display:flex; align-items:flex-start; gap:12px; justify-content:space-between; }\n  .hdr-l{ display:flex; flex-direction:column; gap:4px; }\n  .ttl{ font-family:'Jacquard 24',serif; color:${PAL.goldLight}; font-size:38px; line-height:1.02;\n    text-shadow:2px 2px 0 ${PAL.edgeDark}; }\n  .sub{ font-size:13px; color:${PAL.muted}; }\n  .back-slot{ width:96px; height:40px; flex:0 0 auto; }\n  .tabs{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }\n  .tab{ cursor:pointer; text-align:center; padding:12px 8px; border-radius:8px; border:3px solid ${PAL.edgeDark};\n    background:${PAL.panel2}; color:${PAL.muted}; font-family:'Jacquard 24',serif; font-size:24px;\n    text-shadow:2px 2px 0 ${PAL.edgeDark}; transition:all 140ms ease;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); }\n  .tab.sel{ color:${PAL.edgeDark}; border-color:${PAL.edgeDark};\n    background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); text-shadow:1px 1px 0 rgba(255,255,255,0.3);\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.3); }\n  .tab .cnt{ display:block; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:1px; margin-top:2px; }\n  .panel{ background:${PAL.panel}; border:3px solid ${PAL.edgeDark}; border-radius:8px; padding:12px;\n    box-shadow: inset 2px 2px 0 rgba(255,255,255,0.04), inset -2px -2px 0 rgba(0,0,0,0.5); }\n  .panel.nat20 .sec-ttl{ color:${PAL.goldLight}; }\n  .panel.nat1 .sec-ttl{ color:${PAL.bloodLight}; }\n  .sec-ttl{ font-family:'Jacquard 24',serif; font-size:26px; text-shadow:2px 2px 0 ${PAL.edgeDark}; margin:0 0 10px; }\n  .list{ display:flex; flex-direction:column; gap:6px; }\n  .row{ display:flex; align-items:flex-start; gap:10px; background:${PAL.panel2};\n    border:2px solid ${PAL.stone}; border-radius:6px; padding:7px 9px; }\n  .row .rn{ flex:0 0 auto; width:44px; text-align:center; font-family:'Jacquard 24',serif; font-size:22px;\n    line-height:1.05; color:${PAL.goldLight}; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .panel.nat1 .row .rn{ color:${PAL.bloodLight}; }\n  .row .ef{ flex:1 1 auto; font-size:13px; line-height:1.3; color:${PAL.cream}; }\n  .loading{ padding:50px 20px; text-align:center; color:${PAL.gold}; font-size:18px; }\n  .empty{ padding:30px 20px; text-align:center; color:${PAL.muted}; font-size:14px; }\n  `,\n  unwrap(res){\n    if(!res) return [];\n    return res.results ?? res.data?.results ?? [];\n  },\n  loadKind(d2, kind){\n    if(d2.state['loaded_'+kind] || d2.state['loading_'+kind]) return;\n    d2.state['loading_'+kind] = true;\n    d2.rush('critByKind').invoke({ kind }).then((res)=>{\n      const rows = this.unwrap(res).slice().sort((a,b)=> (Number(a.roll)||0) - (Number(b.roll)||0));\n      d2.state['rows_'+kind] = rows;\n      d2.state['loaded_'+kind] = true;\n      d2.state['loading_'+kind] = false;\n    }).catch((e)=>{\n      d2.log('critByKind error', e);\n      d2.state['rows_'+kind] = [];\n      d2.state['loaded_'+kind] = true;\n      d2.state['loading_'+kind] = false;\n    });\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2){\n    let backTo = '/';\n    try {\n      const q = (typeof window!=='undefined' && window.location) ? (window.location.search || '') : '';\n      const m = /[?&]from=([^&]+)/.exec(q);\n      if(m){ backTo = decodeURIComponent(m[1]); }\n    } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, backTo || '/'); } catch(_){}\n  },\n  render(d2){\n    const tab = d2.state.tab || 'nat20';\n    this.loadKind(d2, tab);\n\n    const loading = !!d2.state['loading_'+tab] && !d2.state['loaded_'+tab];\n    const rows = d2.state['rows_'+tab] || [];\n    const n20 = (d2.state['rows_nat20']||[]).length;\n    const n1 = (d2.state['rows_nat1']||[]).length;\n\n    const header = `\n      <button class=\"back\">‹ Voltar</button>\n      <div class=\"hdr\">\n        <div class=\"hdr-l\">\n          <div class=\"ttl\">Tabelas de Crítico</div>\n          <div class=\"sub\">Role 1d100 ao tirar Nat 20 ou Nat 1 em combate</div>\n        </div>\n      </div>`;\n\n    const tabs = `\n      <div class=\"tabs\">\n        <div class=\"tab ${tab==='nat20'?'sel':''}\" data-tab=\"nat20\">Nat 20<span class=\"cnt\">críticos${n20?(' · '+n20):''}</span></div>\n        <div class=\"tab ${tab==='nat1'?'sel':''}\" data-tab=\"nat1\">Nat 1<span class=\"cnt\">falhas${n1?(' · '+n1):''}</span></div>\n      </div>`;\n\n    let body;\n    if(loading){\n      body = `<div class=\"loading\">Consultando os destinos…</div>`;\n    } else if(!rows.length){\n      body = `<div class=\"empty\">Nenhuma entrada encontrada.</div>`;\n    } else {\n      const secTtl = tab==='nat20' ? 'Nat 20 — Críticos Espetaculares' : 'Nat 1 — Falhas e Trapalhadas';\n      const items = rows.map(r=>`\n        <div class=\"row\" data-key=\"${r.id||r.roll}\">\n          <div class=\"rn\">${r.roll}</div>\n          <div class=\"ef\">${this.esc(r.effect)}</div>\n        </div>`).join('');\n      body = `<div class=\"panel ${tab}\"><div class=\"sec-ttl\">${secTtl}</div><div class=\"list\">${items}</div></div>`;\n    }\n\n    return `<div class=\"root\">${header}${tabs}${body}</div>`;\n  },\n  esc(s){\n    return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    d2.on('.back', 'click', ()=>{ this.goBack(d2); });\n    d2.on('.tab', 'click', (e)=>{\n      const el = e.target && e.target.closest ? e.target.closest('.tab') : null;\n      const t = el && el.getAttribute('data-tab');\n      if(t){ d2.state.tab = t; }\n    });\n  }\n};"}}}}}}}},{"_id":"6a341578c03e59866674f290","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"GearBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320' };\nconst TYPES = ['arma','armadura','item maravilhoso','consumível','poção','outro'];\nconst RARITIES = ['comum','incomum','raro','muito raro','lendário','artefato'];\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:14px; padding:4px; font-family:'Alegreya',sans-serif; background:#1c140d; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:32px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1.05; }\n  .toolbar{ display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }\n  .count{ color:${PAL.cream}; opacity:0.75; font-size:12px; letter-spacing:1px; }\n  .card{ box-sizing:border-box; padding:14px; border-radius:6px; cursor:pointer;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); transition:filter 140ms ease; }\n  .card:hover{ filter:brightness(1.08); }\n  .card-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; letter-spacing:1px; text-shadow:1px 1px 0 ${PAL.edgeDark}; line-height:1.1; margin-bottom:8px; }\n  .card-meta{ display:flex; gap:6px; flex-wrap:wrap; align-items:center; margin-bottom:8px; }\n  .pill{ font-size:11px; letter-spacing:1px; padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .pill.type{ background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .pill.rarity{ background:${PAL.gold}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .pill.attune{ background:${PAL.blood}; color:${PAL.goldLight}; }\n  .pill.owner{ background:${PAL.stone1}; color:${PAL.cream}; }\n  .pill.qty{ background:${PAL.edgeLight}; color:${PAL.edgeDark}; }\n  .card-desc{ color:${PAL.cream}; font-size:13px; opacity:0.85; line-height:1.35; }\n  .empty{ color:${PAL.cream}; opacity:0.7; font-size:14px; text-align:center; padding:18px 12px; }\n  .loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  .panel{ box-sizing:border-box; padding:18px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .panel-title{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:2px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-bottom:12px; }\n  .fld{ margin-bottom:12px; }\n  .fld-label{ display:block; color:${PAL.gold}; font-size:11px; letter-spacing:2px; margin-bottom:5px; font-family:'Alegreya',sans-serif; }\n  .root input.gf-input, .root textarea.gf-input, .root select.gf-input{\n    width:100%; box-sizing:border-box; background:#241a10; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid ${PAL.edgeDark}; border-radius:4px;\n    padding:9px 10px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -2px -2px 0 rgba(255,255,255,0.04); outline:none; }\n  .root input.gf-input:focus, .root textarea.gf-input:focus, .root select.gf-input:focus{ border-color:${PAL.goldDeep}; }\n  .root textarea.gf-input{ resize:vertical; min-height:62px; line-height:1.35; }\n  .root select.gf-input{ appearance:none; -webkit-appearance:none; cursor:pointer; }\n  .toggle{ display:flex; align-items:center; gap:10px; cursor:pointer; user-select:none; color:${PAL.cream}; font-size:14px; }\n  .toggle-box{ width:26px; height:26px; flex:none; border:3px solid ${PAL.edgeDark}; border-radius:4px; background:#241a10; display:flex; align-items:center; justify-content:center; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); }\n  .toggle-box.on{ background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); color:${PAL.edgeDark}; font-weight:700; }\n  .err{ color:${PAL.blood}; font-size:13px; margin-bottom:8px; min-height:16px; }\n  .form-actions{ display:flex; gap:10px; flex-wrap:wrap; margin-top:6px; }\n  .danger{ cursor:pointer; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:1px; color:${PAL.goldLight};\n    background:linear-gradient(180deg,#b9472f,${PAL.blood}); border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:0 16px; height:50px;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.25), 0 2px 0 #5a1c12; }\n  .danger:hover{ filter:brightness(1.1); }\n  `,\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if(d2.state.view == null){ d2.state.view = 'list'; d2.state.items = null; d2.state.loading = true; d2.state.form = {}; d2.state.err = ''; }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.reload(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n\n    d2.on('.back','click',()=>{\n      if(d2.state.view === 'form'){ d2.state.view='list'; d2.state.err=''; return; }\n      const id = this.campaignId(d2);\n      this.goBack(d2, '/campaign/' + id);\n    });\n    d2.on('.card','click',(e)=>{\n      const el = e.target.closest('.card'); if(!el) return;\n      const gid = el.getAttribute('data-id');\n      const it = (d2.state.items||[]).find(x => String(x._id) === String(gid));\n      this.openForm(d2, it || null);\n    });\n    d2.on('.toggle','click',()=>{ const f = this.current(d2); f.attunement = !f.attunement; this._buf = f; d2.state.form = Object.assign({}, f); });\n    // input wiring (delegated; survives re-render)\n    d2.on('.gf-input','input',(e)=>{ this.field(d2, e.target.getAttribute('data-k'), e.target.value); });\n    d2.on('.gf-input','change',(e)=>{ this.field(d2, e.target.getAttribute('data-k'), e.target.value); });\n    d2.on('.danger','click',()=>{ this.del(d2); });\n  },\n  campaignId(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/gear\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  field(d2, k, v){\n    if(!this._buf) this._buf = Object.assign({}, d2.state.form || {});\n    if(k === 'quantity'){ const n = parseInt(v,10); this._buf[k] = isNaN(n) ? '' : n; }\n    else this._buf[k] = v;\n  },\n  current(d2){\n    return Object.assign({}, d2.state.form || {}, this._buf || {});\n  },\n  async load(d2){\n    if (this._loading) return;\n    const id = this.campaignId(d2);\n    if (!id){ d2.state.loading = false; return; }\n    this._loading = true;\n    try {\n      const nonce = Date.now() + '-' + Math.random().toString(36).slice(2);\n      const res = await d2.rush('gearByCampaign').invoke({ campaignId: id, _t: nonce }, { headers: { 'Cache-Control': 'no-cache' } });\n      const rr = (res && res.results) ? res.results : ((res && res.data && res.data.results) ? res.data.results : []);\n      d2.state.items = rr || [];\n    } catch(e){\n      d2.state.items = [];\n      d2.state.err = String(e && e.message ? e.message : e);\n    }\n    d2.state.loading = false;\n    this._loading = false;\n    d2.refresh();\n  },\n  openForm(d2, item){\n    d2.state.err = '';\n    this._buf = null;\n    if(item){\n      d2.state.form = {\n        _id: item._id, name: item.name||'', type: item.type||'outro', rarity: item.rarity||'comum',\n        attunement: !!item.attunement, quantity: (item.quantity!=null?item.quantity:1),\n        owner: item.owner||'', description: item.description||'', properties: item.properties||''\n      };\n    } else {\n      d2.state.form = { name:'', type:'outro', rarity:'comum', attunement:false, quantity:1, owner:'', description:'', properties:'' };\n    }\n    d2.state.view = 'form';\n    d2.refresh();\n  },\n  async save(d2){\n    const f = this.current(d2);\n    if(!f.name || !String(f.name).trim()){ d2.state.err = 'Informe o nome do item'; d2.refresh(); return; }\n    d2.state.err = '';\n    const id = this.campaignId(d2);\n    const payload = {\n      name: f.name, type: f.type||'outro', rarity: f.rarity||'comum',\n      attunement: !!f.attunement, quantity: (f.quantity!=null && f.quantity!==''? f.quantity : 1),\n      owner: f.owner||'', description: f.description||'', properties: f.properties||''\n    };\n    if(f._id) payload._id = f._id; else payload.campaignId = id;\n    try {\n      await d2.rush('saveGear').invoke(payload);\n      this._buf = null;\n      d2.state.view = 'list';\n      await this.reload(d2);\n    } catch(e){ d2.state.err = 'Falha ao salvar'; d2.refresh(); }\n  },\n  async del(d2){\n    const f = this.current(d2);\n    if(!f._id){ d2.state.view='list'; d2.refresh(); return; }\n    const ok = await d2.dialog.confirm('Excluir \"' + (f.name||'este item') + '\"?');\n    if(!ok) return;\n    const gid = f._id;\n    // optimistic: drop from list immediately\n    d2.state.items = (d2.state.items||[]).filter(x => String(x._id) !== String(gid));\n    d2.state.view = 'list';\n    d2.refresh();\n    try {\n      await d2.rush('deleteGear').invoke({ gearId: gid });\n    } catch(e){ /* keep optimistic */ }\n    // reconcile against the source of truth (SSP read lag tolerated)\n    await this.reload(d2);\n  },\n  async reload(d2){\n    this._loading = false;\n    await this.load(d2);\n  },\n  render(d2){\n    const self = this;\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    function attr(s){ return esc(s).replace(/\"/g,'&quot;'); }\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n\n    if(d2.state.view === 'form'){\n      const f = d2.state.form || {};\n      const isEdit = !!f._id;\n      const opt = (arr, cur) => arr.map(o => `<option value=\"${attr(o)}\" ${o===cur?'selected':''}>${esc(o)}</option>`).join('');\n      const saveBtn = d2.render('cronica-app@GoldenButton', { label: isEdit?'Salvar':'Criar', kind:'gold' }, { key:'saveBtn', style:'height:50px;flex:1', onPress:()=>{ self.save(d2); } });\n      const delBtn = isEdit ? `<button class=\"danger\">Excluir</button>` : '';\n      return `<div class=\"root\">\n        ${back}\n        <div class=\"title\">${isEdit?'Editar item':'Novo item'}</div>\n        <div class=\"panel\">\n          <div class=\"err\">${esc(d2.state.err)}</div>\n          <div class=\"fld\"><label class=\"fld-label\">NOME</label>\n            <input class=\"gf-input\" data-k=\"name\" value=\"${attr(f.name)}\" placeholder=\"nome do item\" /></div>\n          <div class=\"fld\"><label class=\"fld-label\">TIPO</label>\n            <select class=\"gf-input\" data-k=\"type\">${opt(TYPES, f.type)}</select></div>\n          <div class=\"fld\"><label class=\"fld-label\">RARIDADE</label>\n            <select class=\"gf-input\" data-k=\"rarity\">${opt(RARITIES, f.rarity)}</select></div>\n          <div class=\"fld\">\n            <div class=\"toggle\"><div class=\"toggle-box ${f.attunement?'on':''}\">${f.attunement?'✦':''}</div><span>Requer sintonia</span></div>\n          </div>\n          <div class=\"fld\"><label class=\"fld-label\">QUANTIDADE</label>\n            <input class=\"gf-input\" data-k=\"quantity\" type=\"number\" min=\"1\" value=\"${attr(f.quantity)}\" /></div>\n          <div class=\"fld\"><label class=\"fld-label\">DONO (opcional)</label>\n            <input class=\"gf-input\" data-k=\"owner\" value=\"${attr(f.owner)}\" placeholder=\"herói / dono\" /></div>\n          <div class=\"fld\"><label class=\"fld-label\">DESCRIÇÃO</label>\n            <textarea class=\"gf-input\" data-k=\"description\" placeholder=\"descrição\">${esc(f.description)}</textarea></div>\n          <div class=\"fld\"><label class=\"fld-label\">PROPRIEDADES (efeitos / regras homebrew)</label>\n            <textarea class=\"gf-input\" data-k=\"properties\" placeholder=\"efeitos, regras homebrew\">${esc(f.properties)}</textarea></div>\n          <div class=\"form-actions\">${saveBtn}${delBtn}</div>\n        </div>\n      </div>`;\n    }\n\n    // LIST VIEW\n    const items = d2.state.items;\n    const newBtn = d2.render('cronica-app@GoldenButton', { label:'Novo item', kind:'gold', glyph:'➕' }, { key:'newBtn', style:'height:48px', onPress:()=>{ self.openForm(d2, null); } });\n    const header = `<div class=\"title\">Equipamentos</div>\n      <div class=\"toolbar\"><span class=\"count\">${items?items.length:0} ${items&&items.length===1?'item':'itens'}</span><div style=\"min-width:150px\">${newBtn}</div></div>`;\n\n    if(d2.state.loading && items == null){\n      return `<div class=\"root\">${back}${header}<div class=\"loading\">Carregando…</div></div>`;\n    }\n    const list = (items && items.length) ? items.map((it)=>{\n      const attune = it.attunement ? `<span class=\"pill attune\">✦ sintonia</span>` : '';\n      const owner = (it.owner && String(it.owner).trim()) ? `<span class=\"pill owner\">${esc(it.owner)}</span>` : '';\n      const qty = (it.quantity && it.quantity > 1) ? `<span class=\"pill qty\">×${esc(it.quantity)}</span>` : '';\n      const desc = it.description ? `<div class=\"card-desc\">${esc(d2.fmt.truncate(it.description, 110))}</div>` : '';\n      return `<div class=\"card\" data-id=\"${attr(it._id)}\" data-key=\"${attr(it._id)}\">\n        <div class=\"card-name\">${esc(it.name||'Item')}</div>\n        <div class=\"card-meta\">\n          <span class=\"pill type\">${esc(it.type||'outro')}</span>\n          <span class=\"pill rarity\">${esc(it.rarity||'comum')}</span>\n          ${attune}${owner}${qty}\n        </div>\n        ${desc}\n      </div>`;\n    }).join('') : `<div class=\"empty\">Nenhum equipamento ainda. Toque em “Novo item”.</div>`;\n\n    return `<div class=\"root\">${back}${header}${list}</div>`;\n  }\n}"}},"gearDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"gearDataset"}}}}}}}}}},{"_id":"6a34101b39b42da2d44a8b33","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"WorldBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nfunction attr(s){ return esc(s).replace(/\"/g,'&quot;'); }\nfunction tagArr(t){ return Array.isArray(t) ? t : (t ? String(t).split(',').map(s=>s.trim()).filter(Boolean) : []); }\nfunction tagStr(t){ return tagArr(t).join(', '); }\nconst NPC_TYPES = ['aliado','vilão','neutro','comerciante','monstro'];\nconst NPC_STATUS = ['vivo','morto','desaparecido'];\nconst LOC_TYPES = ['região','cidade','masmorra','taverna','marco'];\nconst QUEST_TYPES = ['principal','secundária'];\nconst QUEST_STATUS = ['ativa','concluída','falhada'];\nconst FACTION_TYPES = ['aliado','neutro','vilão'];\n\n// View mode shared across clients via localStorage ('mestre' | 'jogador', default 'mestre').\nconst VIEW_KEY = 'cronica-view-mode';\nfunction getViewMode(){\n  try {\n    if(typeof localStorage !== 'undefined'){\n      const v = localStorage.getItem(VIEW_KEY);\n      if(v === 'jogador' || v === 'mestre') return v;\n    }\n  } catch(_){}\n  return 'mestre';\n}\nfunction setViewMode(v){\n  try {\n    if(typeof localStorage !== 'undefined'){ localStorage.setItem(VIEW_KEY, v); }\n  } catch(_){}\n}\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:14px; padding:6px; font-family:'Alegreya',sans-serif; color:${PAL.cream}; background:#1c140d; }\n  .topbar{ display:flex; align-items:center; gap:10px; }\n  .back-w{ width:96px; height:40px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:30px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1; flex:1; }\n  .tabs{ display:flex; gap:6px; }\n  .tab{ flex:1; cursor:pointer; text-align:center; padding:9px 6px; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:1px;\n    border:3px solid ${PAL.edgeDark}; border-radius:4px; background:${PAL.stone1}; color:${PAL.cream};\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; transition:filter 140ms ease; }\n  .tab:hover{ filter:brightness(1.12); }\n  .tab.on{ background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 2px 0 #7a5a18; }\n  .tab.disabled{ opacity:0.4; cursor:not-allowed; }\n  .toolbar{ display:flex; justify-content:flex-end; }\n  .new-w{ width:150px; height:50px; }\n  .list{ display:flex; flex-direction:column; gap:10px; }\n  .card{ box-sizing:border-box; padding:12px 14px; border-radius:6px; cursor:pointer;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55); transition:filter 140ms ease; }\n  .card:hover{ filter:brightness(1.08); }\n  .card-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:21px; line-height:1.05; margin-bottom:7px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .meta{ display:flex; gap:6px; flex-wrap:wrap; align-items:center; }\n  .pill{ font-size:11px; letter-spacing:1px; padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .pill.type{ background:${PAL.gold}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .pill.type.vilão{ background:${PAL.blood}; color:${PAL.cream}; }\n  .pill.status{ background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .pill.status.morto{ color:${PAL.bloodLight}; }\n  .pill.status.concluída{ color:#9fd47a; }\n  .pill.status.falhada{ color:${PAL.bloodLight}; }\n  .pill.loc{ background:#2e2418; color:${PAL.cream}; }\n  .pill.tag{ background:rgba(216,178,76,0.14); color:${PAL.goldLight}; border-color:${PAL.goldDeep}; }\n  .qtag-row{ display:flex; justify-content:flex-end; margin-top:9px; }\n  .qtag-w{ width:128px; height:34px; }\n  .loc-sub{ color:${PAL.cream}; opacity:0.75; font-size:12px; margin-top:5px; }\n  .desc-sub{ color:${PAL.cream}; opacity:0.82; font-size:13px; margin-top:6px; line-height:1.35; }\n  .empty{ color:${PAL.cream}; opacity:0.7; font-size:14px; text-align:center; padding:22px 12px; }\n  .loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  /* form */\n  .panel{ box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .field{ margin-bottom:12px; display:flex; flex-direction:column; gap:5px; }\n  .label{ color:${PAL.gold}; font-size:12px; letter-spacing:1.5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .label .lock{ color:${PAL.bloodLight}; }\n  .view-toggle{ display:inline-flex; align-items:center; justify-content:center; gap:6px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:0.5px; box-sizing:border-box;\n    color:${PAL.gold}; background:rgba(216,178,76,0.08); border:2px solid ${PAL.goldDeep}; border-radius:4px;\n    padding:6px 12px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .view-toggle:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .view-toggle:active{ transform:translateY(1px); }\n  .view-toggle.jogador{ color:${PAL.cream}; border-color:${PAL.edgeLight}; background:rgba(58,44,26,0.4); }\n  .secret-block{ margin-top:8px; padding:8px 10px; border-radius:4px;\n    background:rgba(156,51,32,0.10); border:2px solid ${PAL.goldDeep};\n    box-shadow: inset 1px 1px 0 rgba(0,0,0,0.4); }\n  .secret-block .secret-lbl{ color:${PAL.bloodLight}; font-size:11px; letter-spacing:1px; margin-bottom:3px; }\n  .secret-block .secret-txt{ color:${PAL.cream}; opacity:0.9; font-size:13px; line-height:1.35; white-space:pre-wrap; }\n  .root input.gf-input, .root textarea.gf-input{\n    box-sizing:border-box; width:100%; background:#241a10; color:#ece0c4;\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid #1c140d; border-radius:4px;\n    padding:8px 10px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -2px -2px 0 rgba(106,84,54,0.4); outline:none; }\n  .root textarea.gf-input{ min-height:70px; resize:vertical; line-height:1.35; }\n  .root input.gf-input:focus, .root textarea.gf-input:focus{ border-color:${PAL.goldDeep}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), 0 0 6px rgba(216,178,76,0.5); }\n  .form-actions{ display:flex; gap:10px; margin-top:6px; }\n  .act-w{ flex:1; height:48px; }\n  .err{ color:${PAL.bloodLight}; font-size:13px; min-height:16px; margin-bottom:8px; }\n  `,\n  cid(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/world\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if (d2.state.tab == null) d2.state.tab = 'npcs';\n    // Force a fresh load on every (re)mount — clears stale throttle/guard flags from a\n    // prior mount of this reused instance so returning from PosSessão (after accepting\n    // NPCs) always re-fetches instead of being suppressed by _lastRenderLoad/_loading.\n    this._loading = false;\n    this._lastRenderLoad = 0;\n    this.loadAll(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.loadAll(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back', 'click', ()=>{ const id = this.cid(d2); this.goBack(d2, '/campaign/'+id); });\n    // Master/Player view toggle — flips shared localStorage mode + re-renders.\n    d2.on('.view-toggle', 'click', ()=>{\n      const next = getViewMode() === 'mestre' ? 'jogador' : 'mestre';\n      setViewMode(next);\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.tab', 'click', (e)=>{\n      const el = e.target.closest('.tab');\n      if (!el || el.classList.contains('disabled')) return;\n      const t = el.getAttribute('data-tab');\n      if (t){ d2.state.tab = t; d2.state.editing = null; d2.state.err=''; }\n    });\n    d2.on('.card', 'click', (e)=>{\n      if (e.target.closest('.qtag-w')) return;\n      const el = e.target.closest('.card');\n      if (!el) return;\n      const id = el.getAttribute('data-id');\n      const kind = el.getAttribute('data-kind');\n      this.openEdit(d2, kind, id);\n    });\n    d2.on('.gf-input', 'input', (e)=>{\n      const k = e.target.getAttribute('data-k');\n      if (!k) return;\n      const f = d2.state.form || {};\n      f[k] = e.target.value;\n      d2.state.form = Object.assign({}, f);\n    });\n    d2.on('.gf-input', 'change', (e)=>{\n      const k = e.target.getAttribute('data-k');\n      if (!k) return;\n      const f = d2.state.form || {};\n      f[k] = e.target.value;\n      d2.state.form = Object.assign({}, f);\n    });\n  },\n  async loadAll(d2){\n    if (this._loading) return;\n    const id = this.cid(d2);\n    if (!id){ return; }\n    this._loading = true;\n    d2.state.loading = true;\n    const fetchRows = async (ref) => {\n      const nonce = Date.now() + '-' + Math.random().toString(36).slice(2);\n      const r = await d2.rush(ref).invoke({ campaignId: id, _t: nonce }, { headers: { 'Cache-Control': 'no-cache' } });\n      return (r && r.results) ? r.results : ((r && r.data && r.data.results) ? r.data.results : []);\n    };\n    try {\n      d2.state.npcs = (await fetchRows('npcsByCampaign')) || [];\n      d2.state.locations = (await fetchRows('locationsByCampaign')) || [];\n      d2.state.quests = (await fetchRows('questsByCampaign')) || [];\n      d2.state.factions = (await fetchRows('factionsByCampaign')) || [];\n    } catch(e){\n      d2.state.err = String(e && e.message ? e.message : e);\n    }\n    d2.state.loading = false;\n    this._loading = false;\n    d2.refresh();\n  },\n  async autoTag(d2, id){\n    if (!id) return;\n    const tagging = Object.assign({}, d2.state.tagging || {});\n    tagging[id] = true;\n    d2.state.tagging = tagging;\n    d2.refresh();\n    try {\n      await d2.rush('autoTagQuest').invoke({ questId: id });\n    } catch(e){ /* the write lands server-side even if the post-call surface errors; reload reflects truth */ }\n    const done = Object.assign({}, d2.state.tagging || {});\n    delete done[id];\n    d2.state.tagging = done;\n    await this.loadAll(d2);\n  },\n  openNew(d2, kind){\n    d2.state.editing = { kind: kind };\n    if (kind === 'npc'){\n      d2.state.form = { _id:'', name:'', type:'aliado', faction:'', locationId:'', status:'vivo', description:'', tags:'', secret:'' };\n    } else if (kind === 'loc'){\n      d2.state.form = { _id:'', name:'', type:'região', parentId:'', description:'', tags:'', secret:'' };\n    } else if (kind === 'quest'){\n      d2.state.form = { _id:'', title:'', type:'principal', status:'ativa', giverName:'', reward:'', description:'', tags:'', secret:'' };\n    } else if (kind === 'faction'){\n      d2.state.form = { _id:'', name:'', type:'aliado', description:'', tags:'', secret:'' };\n    }\n    d2.state.err = '';\n    d2.refresh();\n  },\n  openEdit(d2, kind, id){\n    const listFor = { npc:'npcs', loc:'locations', quest:'quests', faction:'factions' };\n    const list = d2.state[listFor[kind]] || [];\n    const rec = list.find(r => String(r._id) === String(id));\n    if (!rec) return;\n    d2.state.editing = { kind: kind, id: id };\n    if (kind === 'npc'){\n      d2.state.form = {\n        _id: rec._id, name: rec.name||'', type: rec.type||'aliado', faction: rec.faction||'',\n        locationId: rec.locationId||'', status: rec.status||'vivo', description: rec.description||'',\n        tags: tagStr(rec.tags), secret: rec.secret||''\n      };\n    } else if (kind === 'loc'){\n      d2.state.form = {\n        _id: rec._id, name: rec.name||'', type: rec.type||'região', parentId: rec.parentId||'',\n        description: rec.description||'', tags: tagStr(rec.tags), secret: rec.secret||''\n      };\n    } else if (kind === 'quest'){\n      d2.state.form = {\n        _id: rec._id, title: rec.title||'', type: rec.type||'principal', status: rec.status||'ativa',\n        giverName: rec.giverName||'', reward: rec.reward||'', description: rec.description||'',\n        tags: tagStr(rec.tags), secret: rec.secret||''\n      };\n    } else if (kind === 'faction'){\n      d2.state.form = {\n        _id: rec._id, name: rec.name||'', type: rec.type||'aliado',\n        description: rec.description||'', tags: tagStr(rec.tags), secret: rec.secret||''\n      };\n    }\n    d2.state.err = '';\n    d2.refresh();\n  },\n  cancel(d2){ d2.state.editing = null; d2.state.err=''; d2.refresh(); },\n  locName(d2, id){\n    const l = (d2.state.locations||[]).find(x => String(x._id) === String(id));\n    return l ? l.name : '';\n  },\n  secretDisplay(secret){\n    // Master-only locked block; players never see it (caller gates by view mode).\n    const s = secret == null ? '' : String(secret);\n    if (!s.trim()) return '';\n    return `<div class=\"secret-block\"><div class=\"secret-lbl\">🔒 Segredo</div><div class=\"secret-txt\">${esc(s)}</div></div>`;\n  },\n  async save(d2){\n    const f = d2.state.form || {};\n    const kind = d2.state.editing.kind;\n    const isQuest = kind === 'quest';\n    const nameVal = isQuest ? f.title : f.name;\n    if (!nameVal || !nameVal.trim()){ d2.state.err = isQuest ? 'Informe o título' : 'Informe o nome'; d2.refresh(); return; }\n    const id = this.cid(d2);\n    const tags = (f.tags||'').split(',').map(s=>s.trim()).filter(Boolean);\n    d2.state.err = '';\n    d2.state.saving = true; d2.refresh();\n    try {\n      if (kind === 'npc'){\n        await d2.rush('saveNpc').invoke({\n          _id: f._id||'', campaignId: id, name: f.name.trim(), type: f.type||'aliado',\n          faction: f.faction||'', locationId: f.locationId||'', locationName: this.locName(d2, f.locationId),\n          status: f.status||'vivo', description: f.description||'', tags: tags, secret: f.secret||''\n        });\n      } else if (kind === 'loc'){\n        await d2.rush('saveLocation').invoke({\n          _id: f._id||'', campaignId: id, name: f.name.trim(), type: f.type||'região',\n          parentId: f.parentId||'', parentName: this.locName(d2, f.parentId),\n          description: f.description||'', tags: tags, secret: f.secret||''\n        });\n      } else if (kind === 'quest'){\n        await d2.rush('saveQuest').invoke({\n          _id: f._id||'', campaignId: id, title: f.title.trim(), type: f.type||'principal',\n          status: f.status||'ativa', giverName: f.giverName||'', reward: f.reward||'',\n          description: f.description||'', tags: tags, secret: f.secret||''\n        });\n      } else if (kind === 'faction'){\n        await d2.rush('saveFaction').invoke({\n          _id: f._id||'', campaignId: id, name: f.name.trim(), type: f.type||'aliado',\n          description: f.description||'', tags: tags, secret: f.secret||''\n        });\n      }\n    } catch(e){ /* post-write fetch error can surface even when the write landed; reload to reflect truth */ }\n    d2.state.editing = null;\n    d2.state.saving = false;\n    await this.loadAll(d2);\n  },\n  async del(d2){\n    const f = d2.state.form || {};\n    if (!f._id){ d2.state.editing = null; d2.refresh(); return; }\n    const kind = d2.state.editing.kind;\n    const delId = f._id;\n    d2.state.saving = true; d2.refresh();\n    try {\n      if (kind === 'npc') await d2.rush('deleteNpc').invoke({ npcId: delId });\n      else if (kind === 'loc') await d2.rush('deleteLocation').invoke({ locationId: delId });\n      else if (kind === 'quest') await d2.rush('deleteQuest').invoke({ questId: delId });\n      else if (kind === 'faction') await d2.rush('deleteFaction').invoke({ factionId: delId });\n    } catch(e){ /* post-delete fetch error can surface even when the row was removed; the delete itself is reliable */ }\n    const listFor = { npc:'npcs', loc:'locations', quest:'quests', faction:'factions' };\n    const key = listFor[kind];\n    d2.state[key] = (d2.state[key]||[]).filter(r => String(r._id) !== String(delId));\n    d2.state.editing = null;\n    d2.state.err = '';\n    d2.state.saving = false;\n    d2.refresh();\n    this.loadAll(d2);\n  },\n  gsField(d2, k, opts, sel, ph){\n    // Renders a GoldenSelect (custom dropdown) for form field k. opts is an\n    // array of {value,label}. onChange writes d2.state.form[k] + refresh.\n    // The native store is broken; the callback is the channel (igual o autocomplete).\n    return d2.render('cronica-app@GoldenSelect',\n      { options: JSON.stringify(opts), value: (sel==null?'':sel), placeholder: ph||'— selecionar —' },\n      { key:'gs-'+k, style:'height:42px', onChange:(v)=>{\n        const ff = Object.assign({}, d2.state.form||{});\n        ff[k] = v;\n        d2.state.form = ff;\n        d2.refresh();\n      } });\n  },\n  secretField(f){\n    return `\\n        <div class=\"field\"><div class=\"label\"><span class=\"lock\">🔒</span> Segredo (só mestre)</div><textarea class=\"gf-input\" data-k=\"secret\" placeholder=\"só o mestre vê\">${esc(f.secret)}</textarea></div>\\n      `;\n  },\n  renderForm(d2){\n    const self = this;\n    const f = d2.state.form || {};\n    const kind = d2.state.editing.kind;\n    const isNew = !f._id;\n    const isMestre = getViewMode() === 'mestre';\n    const secretFld = isMestre ? this.secretField(f) : '';\n    let heading = '';\n    let body = '';\n    if (kind === 'npc'){\n      heading = isNew?'Novo NPC':'Editar NPC';\n      const npcTypeOpts = NPC_TYPES.map(t=>({ value:t, label:t }));\n      const npcStatusOpts = NPC_STATUS.map(t=>({ value:t, label:t }));\n      const locOpts = [{ value:'', label:'— sem local —' }].concat((d2.state.locations||[]).map(l=>({ value:l._id, label:l.name })));\n      body = `\n        <div class=\"field\"><div class=\"label\">Nome</div><input class=\"gf-input\" data-k=\"name\" value=\"${attr(f.name)}\" placeholder=\"nome do NPC\"/></div>\n        <div class=\"field\"><div class=\"label\">Tipo</div>${self.gsField(d2, 'type', npcTypeOpts, f.type, '— tipo —')}</div>\n        <div class=\"field\"><div class=\"label\">Facção</div><input class=\"gf-input\" data-k=\"faction\" value=\"${attr(f.faction)}\" placeholder=\"facção (opcional)\"/></div>\n        <div class=\"field\"><div class=\"label\">Local</div>${self.gsField(d2, 'locationId', locOpts, f.locationId, '— sem local —')}</div>\n        <div class=\"field\"><div class=\"label\">Status</div>${self.gsField(d2, 'status', npcStatusOpts, f.status, '— status —')}</div>\n        <div class=\"field\"><div class=\"label\">Descrição</div><textarea class=\"gf-input\" data-k=\"description\" placeholder=\"descrição\">${esc(f.description)}</textarea></div>\n        <div class=\"field\"><div class=\"label\">Tags <span style=\"opacity:0.6\">(vírgula)</span></div><input class=\"gf-input\" data-k=\"tags\" value=\"${attr(f.tags)}\" placeholder=\"ex: vampiro, chefe\"/></div>\n        ${secretFld}\n      `;\n    } else if (kind === 'loc'){\n      heading = isNew?'Novo Local':'Editar Local';\n      const locTypeOpts = LOC_TYPES.map(t=>({ value:t, label:t }));\n      const parOpts = [{ value:'', label:'— sem pai —' }].concat((d2.state.locations||[]).filter(l=>String(l._id)!==String(f._id)).map(l=>({ value:l._id, label:l.name })));\n      body = `\n        <div class=\"field\"><div class=\"label\">Nome</div><input class=\"gf-input\" data-k=\"name\" value=\"${attr(f.name)}\" placeholder=\"nome do local\"/></div>\n        <div class=\"field\"><div class=\"label\">Tipo</div>${self.gsField(d2, 'type', locTypeOpts, f.type, '— tipo —')}</div>\n        <div class=\"field\"><div class=\"label\">Pertence a</div>${self.gsField(d2, 'parentId', parOpts, f.parentId, '— sem pai —')}</div>\n        <div class=\"field\"><div class=\"label\">Descrição</div><textarea class=\"gf-input\" data-k=\"description\" placeholder=\"descrição\">${esc(f.description)}</textarea></div>\n        <div class=\"field\"><div class=\"label\">Tags <span style=\"opacity:0.6\">(vírgula)</span></div><input class=\"gf-input\" data-k=\"tags\" value=\"${attr(f.tags)}\" placeholder=\"ex: importante\"/></div>\n        ${secretFld}\n      `;\n    } else if (kind === 'quest'){\n      heading = isNew?'Nova quest':'Editar quest';\n      const questTypeOpts = QUEST_TYPES.map(t=>({ value:t, label:t }));\n      const questStatusOpts = QUEST_STATUS.map(t=>({ value:t, label:t }));\n      body = `\n        <div class=\"field\"><div class=\"label\">Título</div><input class=\"gf-input\" data-k=\"title\" value=\"${attr(f.title)}\" placeholder=\"título da quest\"/></div>\n        <div class=\"field\"><div class=\"label\">Tipo</div>${self.gsField(d2, 'type', questTypeOpts, f.type, '— tipo —')}</div>\n        <div class=\"field\"><div class=\"label\">Status</div>${self.gsField(d2, 'status', questStatusOpts, f.status, '— status —')}</div>\n        <div class=\"field\"><div class=\"label\">Quem deu</div><input class=\"gf-input\" data-k=\"giverName\" value=\"${attr(f.giverName)}\" placeholder=\"quem ofereceu (opcional)\"/></div>\n        <div class=\"field\"><div class=\"label\">Recompensa</div><input class=\"gf-input\" data-k=\"reward\" value=\"${attr(f.reward)}\" placeholder=\"recompensa (opcional)\"/></div>\n        <div class=\"field\"><div class=\"label\">Descrição</div><textarea class=\"gf-input\" data-k=\"description\" placeholder=\"descrição\">${esc(f.description)}</textarea></div>\n        <div class=\"field\"><div class=\"label\">Tags <span style=\"opacity:0.6\">(vírgula)</span></div><input class=\"gf-input\" data-k=\"tags\" value=\"${attr(f.tags)}\" placeholder=\"ex: urgente\"/></div>\n        ${secretFld}\n      `;\n    } else if (kind === 'faction'){\n      heading = isNew?'Nova facção':'Editar facção';\n      const facTypeOpts = FACTION_TYPES.map(t=>({ value:t, label:t }));\n      body = `\n        <div class=\"field\"><div class=\"label\">Nome</div><input class=\"gf-input\" data-k=\"name\" value=\"${attr(f.name)}\" placeholder=\"nome da facção\"/></div>\n        <div class=\"field\"><div class=\"label\">Tipo</div>${self.gsField(d2, 'type', facTypeOpts, f.type, '— tipo —')}</div>\n        <div class=\"field\"><div class=\"label\">Descrição</div><textarea class=\"gf-input\" data-k=\"description\" placeholder=\"descrição\">${esc(f.description)}</textarea></div>\n        <div class=\"field\"><div class=\"label\">Tags <span style=\"opacity:0.6\">(vírgula)</span></div><input class=\"gf-input\" data-k=\"tags\" value=\"${attr(f.tags)}\" placeholder=\"ex: religiosa\"/></div>\n        ${secretFld}\n      `;\n    }\n    const saveBtn = d2.render('cronica-app@GoldenButton', { label: d2.state.saving?'…':'Salvar', kind:'gold' }, { key:'wb-save', onPress:()=>{ self.save(d2); } });\n    const cancelBtn = d2.render('cronica-app@GoldenButton', { label:'Cancelar', kind:'gold', ghost:true }, { key:'wb-cancel', onPress:()=>{ self.cancel(d2); } });\n    const delBtn = isNew ? '' : `<div class=\"act-w\">${d2.render('cronica-app@GoldenButton', { label:'Excluir', kind:'danger' }, { key:'wb-del', onPress:()=>{ self.del(d2); } })}</div>`;\n    return `<div class=\"panel\">\n      <div class=\"card-name\" style=\"font-size:22px;margin-bottom:12px\">${esc(heading)}</div>\n      <div class=\"err\">${esc(d2.state.err||'')}</div>\n      ${body}\n      <div class=\"form-actions\">\n        <div class=\"act-w\">${cancelBtn}</div>\n        ${delBtn}\n        <div class=\"act-w\">${saveBtn}</div>\n      </div>\n    </div>`;\n  },\n  renderNpcList(d2){\n    const npcs = d2.state.npcs || [];\n    if (!npcs.length) return `<div class=\"empty\">Nenhum NPC ainda. Crie o primeiro!</div>`;\n    const showSecret = getViewMode() === 'mestre';\n    return `<div class=\"list\">` + npcs.map(n=>{\n      const secretHtml = showSecret ? this.secretDisplay(n.secret) : '';\n      const tags = tagArr(n.tags).slice(0,4).map(t=>`<span class=\"pill tag\">${esc(t)}</span>`).join('');\n      const loc = n.locationName ? `<span class=\"pill loc\">📍 ${esc(n.locationName)}</span>` : '';\n      const stCls = n.status==='morto' ? 'status morto' : 'status';\n      return `<div class=\"card\" data-kind=\"npc\" data-id=\"${attr(n._id)}\" data-key=\"${attr(n._id)}\">\n        <div class=\"card-name\">${esc(n.name)}</div>\n        <div class=\"meta\">\n          <span class=\"pill type\">${esc(n.type||'')}</span>\n          <span class=\"pill ${stCls}\">${esc(n.status||'')}</span>\n          ${loc}\n          ${tags}\n        </div>\n        ${secretHtml}\n      </div>`;\n    }).join('') + `</div>`;\n  },\n  renderLocList(d2){\n    const locs = d2.state.locations || [];\n    if (!locs.length) return `<div class=\"empty\">Nenhum local ainda. Crie o primeiro!</div>`;\n    const showSecret = getViewMode() === 'mestre';\n    return `<div class=\"list\">` + locs.map(l=>{\n      const par = l.parentName ? `<div class=\"loc-sub\">↳ em ${esc(l.parentName)}</div>` : '';\n      const secretHtml = showSecret ? this.secretDisplay(l.secret) : '';\n      return `<div class=\"card\" data-kind=\"loc\" data-id=\"${attr(l._id)}\" data-key=\"${attr(l._id)}\">\n        <div class=\"card-name\">${esc(l.name)}</div>\n        <div class=\"meta\"><span class=\"pill type\">${esc(l.type||'')}</span></div>\n        ${par}\n        ${secretHtml}\n      </div>`;\n    }).join('') + `</div>`;\n  },\n  renderQuestList(d2){\n    const quests = d2.state.quests || [];\n    if (!quests.length) return `<div class=\"empty\">Nenhuma quest ainda. Crie a primeira!</div>`;\n    const self = this;\n    const busy = d2.state.tagging || {};\n    const showSecret = getViewMode() === 'mestre';\n    return `<div class=\"list\">` + quests.map(q=>{\n      const secretHtml = showSecret ? this.secretDisplay(q.secret) : '';\n      const tags = tagArr(q.tags).slice(0,6).map(t=>`<span class=\"pill tag\">${esc(t)}</span>`).join('');\n      const giver = q.giverName ? `<span class=\"pill loc\">🗣 ${esc(q.giverName)}</span>` : '';\n      const reward = q.reward ? `<span class=\"pill loc\">🎁 ${esc(q.reward)}</span>` : '';\n      const stCls = 'status ' + (q.status||'');\n      const isBusy = !!busy[q._id];\n      const tagBtn = d2.render('cronica-app@GoldenButton',\n        { label: isBusy ? 'Taggeando…' : 'Tags IA ✨', kind:'gold', ghost:true, size:'sm' },\n        { key:'wb-tag-'+q._id, onPress:()=>{ if (!isBusy) self.autoTag(d2, q._id); } });\n      return `<div class=\"card\" data-kind=\"quest\" data-id=\"${attr(q._id)}\" data-key=\"${attr(q._id)}\">\n        <div class=\"card-name\">${esc(q.title)}</div>\n        <div class=\"meta\">\n          <span class=\"pill type\">${esc(q.type||'')}</span>\n          <span class=\"pill ${stCls}\">${esc(q.status||'')}</span>\n          ${giver}\n          ${reward}\n          ${tags}\n        </div>\n        ${secretHtml}\n        <div class=\"qtag-row\"><div class=\"qtag-w\">${tagBtn}</div></div>\n      </div>`;\n    }).join('') + `</div>`;\n  },\n  renderFactionList(d2){\n    const facs = d2.state.factions || [];\n    if (!facs.length) return `<div class=\"empty\">Nenhuma facção ainda. Crie a primeira!</div>`;\n    const showSecret = getViewMode() === 'mestre';\n    return `<div class=\"list\">` + facs.map(fc=>{\n      const secretHtml = showSecret ? this.secretDisplay(fc.secret) : '';\n      const tags = tagArr(fc.tags).slice(0,4).map(t=>`<span class=\"pill tag\">${esc(t)}</span>`).join('');\n      const typeCls = 'type ' + (fc.type||'');\n      const desc = fc.description ? `<div class=\"desc-sub\">${esc(fc.description)}</div>` : '';\n      return `<div class=\"card\" data-kind=\"faction\" data-id=\"${attr(fc._id)}\" data-key=\"${attr(fc._id)}\">\n        <div class=\"card-name\">${esc(fc.name)}</div>\n        <div class=\"meta\"><span class=\"pill ${typeCls}\">${esc(fc.type||'')}</span>${tags}</div>\n        ${desc}\n        ${secretHtml}\n      </div>`;\n    }).join('') + `</div>`;\n  },\n  render(d2){\n    const self = this;\n    try {\n      const _now = Date.now();\n      if (!d2.state.editing && !this._loading && (!this._lastRenderLoad || (_now - this._lastRenderLoad) > 1200)){\n        this._lastRenderLoad = _now;\n        this.loadAll(d2);\n      }\n    } catch(_){}\n    const tab = d2.state.tab || 'npcs';\n    const backBtn = `<button class=\"back\">‹ Voltar</button>`;\n\n    const tabs = `<div class=\"tabs\">\n      <div class=\"tab ${tab==='npcs'?'on':''}\" data-tab=\"npcs\">NPCs</div>\n      <div class=\"tab ${tab==='locais'?'on':''}\" data-tab=\"locais\">Locais</div>\n      <div class=\"tab ${tab==='quests'?'on':''}\" data-tab=\"quests\">Quests</div>\n      <div class=\"tab ${tab==='faccoes'?'on':''}\" data-tab=\"faccoes\">Facções</div>\n    </div>`;\n\n    const isPlayer = getViewMode() === 'jogador';\n    const viewToggle = `<button class=\"view-toggle ${isPlayer?'jogador':''}\" title=\"Alternar visão Mestre/Jogador\">${isPlayer?'Jogador':'Mestre'} — trocar visão</button>`;\n    const header = `${backBtn}<div class=\"topbar\"><div class=\"title\">Mundo</div>${viewToggle}</div>`;\n\n    if (d2.state.editing){\n      return `<div class=\"root\">${header}${tabs}${this.renderForm(d2)}</div>`;\n    }\n\n    const anyData = (d2.state.npcs||[]).length || (d2.state.locations||[]).length || (d2.state.quests||[]).length || (d2.state.factions||[]).length;\n    if (d2.state.loading && !anyData){\n      return `<div class=\"root\">${header}${tabs}<div class=\"loading\">Carregando…</div></div>`;\n    }\n\n    let toolbar = '';\n    let listHtml = '';\n    if (tab === 'npcs'){\n      const newBtn = d2.render('cronica-app@GoldenButton', { label:'Novo NPC', kind:'gold', size:'sm', glyph:'➕' }, { key:'wb-new-npc', onPress:()=>{ self.openNew(d2,'npc'); } });\n      toolbar = `<div class=\"toolbar\"><div class=\"new-w\">${newBtn}</div></div>`;\n      listHtml = this.renderNpcList(d2);\n    } else if (tab === 'locais'){\n      const newBtn = d2.render('cronica-app@GoldenButton', { label:'Novo Local', kind:'gold', size:'sm', glyph:'➕' }, { key:'wb-new-loc', onPress:()=>{ self.openNew(d2,'loc'); } });\n      toolbar = `<div class=\"toolbar\"><div class=\"new-w\">${newBtn}</div></div>`;\n      listHtml = this.renderLocList(d2);\n    } else if (tab === 'quests'){\n      const newBtn = d2.render('cronica-app@GoldenButton', { label:'Nova quest', kind:'gold', size:'sm', glyph:'➕' }, { key:'wb-new-quest', onPress:()=>{ self.openNew(d2,'quest'); } });\n      toolbar = `<div class=\"toolbar\"><div class=\"new-w\">${newBtn}</div></div>`;\n      listHtml = this.renderQuestList(d2);\n    } else if (tab === 'faccoes'){\n      const newBtn = d2.render('cronica-app@GoldenButton', { label:'Nova facção', kind:'gold', size:'sm', glyph:'➕' }, { key:'wb-new-fac', onPress:()=>{ self.openNew(d2,'faction'); } });\n      toolbar = `<div class=\"toolbar\"><div class=\"new-w\">${newBtn}</div></div>`;\n      listHtml = this.renderFactionList(d2);\n    }\n\n    return `<div class=\"root\">${header}${tabs}${toolbar}${listHtml}</div>`;\n  }\n}"}}}}}}}},{"_id":"6a353e7b9227be1644c1e61d","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"LoginBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a', inputBg:'#241a10', inputBorder:'#1c140d' };\n\nexport default {\n  css: `\n  :host{ display:block; width:100%; height:100%; background:#1c140d; }\n  .lf-root{ width:100%; min-height:100%; box-sizing:border-box; display:flex; align-items:center; justify-content:center;\n    padding:32px 16px; background:#1c140d; font-family:'Alegreya',sans-serif; }\n  .lf-panel{ position:relative; box-sizing:border-box; width:100%; max-width:360px; padding:26px 24px 24px; border-radius:8px;\n    display:flex; flex-direction:column; gap:14px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 12px 26px rgba(0,0,0,0.6);\n    image-rendering:pixelated; }\n  .lf-title{ color:${PAL.gold}; font-family:'Jacquard 24','Alegreya',serif; font-size:52px; line-height:0.9;\n    letter-spacing:1px; text-align:center; text-shadow:2px 2px 0 ${PAL.edgeDark}, 0 0 10px rgba(216,178,76,0.3); margin:0; }\n  .lf-sub{ color:${PAL.cream}; font-size:13px; letter-spacing:1px; text-align:center; opacity:0.82; margin:-4px 0 6px; }\n  .lf-field{ display:flex; flex-direction:column; gap:4px; }\n  .lf-label{ color:${PAL.goldLight}; font-size:12px; letter-spacing:1px; opacity:0.9; }\n  .lf-input{ box-sizing:border-box; width:100%; height:46px; padding:8px 12px;\n    font-family:'Alegreya',sans-serif; font-size:15px; color:${PAL.cream};\n    background:${PAL.inputBg}; border:3px solid ${PAL.inputBorder}; border-radius:5px; outline:none;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -1px -1px 0 rgba(255,255,255,0.05); }\n  .lf-input::placeholder{ color:${PAL.cream}; opacity:0.4; }\n  .lf-input:focus{ border-color:${PAL.goldDeep}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), 0 0 0 2px rgba(216,178,76,0.25); }\n  .lf-submit{ width:100%; height:56px; margin-top:4px; }\n  .lf-submit > *{ width:100% !important; height:100% !important; }\n  .lf-err{ min-height:18px; color:${PAL.bloodLight}; font-size:12.5px; letter-spacing:0.5px; text-align:center;\n    word-break:break-word; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .lf-toggle{ text-align:center; color:${PAL.goldLight}; font-size:13px; letter-spacing:1px; cursor:pointer;\n    padding:4px; opacity:0.9; }\n  .lf-toggle:hover{ color:${PAL.gold}; filter:brightness(1.1); text-decoration:underline; }\n  `,\n\n  mounted(d2){\n    try {\n      if (typeof document !== 'undefined' && !document.getElementById('cronica-gfont')) {\n        const l = document.createElement('link');\n        l.id = 'cronica-gfont';\n        l.rel = 'stylesheet';\n        l.href = 'https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n        document.head.appendChild(l);\n      }\n    } catch(_){}\n    d2.on('.lf-input', 'input', (e) => {\n      const k = e.target.getAttribute('data-k');\n      if (k) d2.state[k] = e.target.value;\n    });\n    d2.on('.lf-toggle', 'click', () => {\n      d2.state.mode = (d2.state.mode === 'login' ? 'signup' : 'login');\n      d2.state.error = '';\n      d2.refresh();\n    });\n    // Direct click handler on the submit wrapper — robust even if the nested\n    // GoldenButton's onPress emit doesn't fire in this context.\n    d2.on('.lf-submit', 'click', () => { this.submit(d2); });\n  },\n\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n\n  async submit(d2){\n    if (d2.state.busy) return;\n    const email = (d2.state.email || '').trim();\n    const password = (d2.state.password || '');\n    const name = (d2.state.name || '').trim();\n    const mode = d2.state.mode || 'login';\n    if (!email || !password || (mode === 'signup' && !name)) {\n      d2.state.error = 'Preencha todos os campos'; d2.refresh(); return;\n    }\n    d2.state.busy = true; d2.state.error = ''; d2.refresh();\n    try {\n      if (mode === 'signup') {\n        let su = null, rushErr = null;\n        try {\n          su = await d2.rush('signup').invoke({ name, email, password });\n        } catch(e){ rushErr = String(e && e.message || e); }\n        const okRush = su && (su.ok || (su.data && su.data.ok));\n        const errRush = (su && su.error) || (su && su.data && su.data.error);\n        if (!okRush) {\n          // Fallback: register directly through the auth strategy (Rush path may be unavailable).\n          let regErr = null;\n          try {\n            const auth = d2.auth('cronicaAuth');\n            if (auth && auth.register) { await auth.register({ authenticationStrategyReference: 'cronicaAuth', credentials: { email, password }, name: name, role: 'player' }); }\n            else { regErr = 'register indisponível'; }\n          } catch(e){ regErr = String(e && e.message || e); }\n          if (regErr) {\n            d2.state.error = errRush || rushErr || regErr || 'Não foi possível criar a conta';\n            d2.state.busy = false; d2.refresh(); return;\n          }\n        }\n      }\n      let res;\n      try { res = await d2.auth('cronicaAuth').login({ authenticationStrategyReference: 'cronicaAuth', credentials: { email, password } }); }\n      catch(e){ res = { code: 'EXC', message: String(e && e.message || e) }; }\n      if (res && res.code) {\n        d2.state.error = 'Credenciais inválidas (' + res.code + ')';\n        d2.state.busy = false; d2.refresh(); return;\n      }\n      try { await d2.rush('acceptInvites').invoke({}); } catch(_){}\n      d2.state.busy = false;\n      this.go(d2, '/');\n    } catch(e){\n      d2.state.error = 'Erro: ' + String(e && e.message || e);\n      d2.state.busy = false; d2.refresh();\n    }\n  },\n\n  render(d2){\n    const self = this;\n    if (d2.state.mode === undefined) d2.state.mode = 'login';\n    const mode = d2.state.mode;\n    const isSignup = mode === 'signup';\n    const busy = !!d2.state.busy;\n    const err = d2.state.error || '';\n    const email = d2.state.email || '';\n    const password = d2.state.password || '';\n    const name = d2.state.name || '';\n\n    let label = isSignup ? 'Forjar herói' : 'Entrar';\n    if (busy) label = '…';\n\n    const btn = d2.render('cronica-app@GoldenButton',\n      { label: label, kind: 'gold' },\n      { key: 'lf-submit', onPress: () => self.submit(d2) }\n    );\n\n    const nameField = isSignup ? `\n      <div class=\"lf-field\">\n        <span class=\"lf-label\">nome do herói</span>\n        <input class=\"lf-input\" data-k=\"name\" type=\"text\" placeholder=\"seu nome\" value=\"${name.replace(/\"/g,'&quot;')}\" autocomplete=\"name\" />\n      </div>` : '';\n\n    const sub = isSignup ? 'forje uma nova lenda' : 'retome sua jornada';\n\n    return `\n    <div class=\"lf-root\">\n      <div class=\"lf-panel\">\n        <div class=\"lf-title\">crônica</div>\n        <div class=\"lf-sub\">${sub}</div>\n        ${nameField}\n        <div class=\"lf-field\">\n          <span class=\"lf-label\">email</span>\n          <input class=\"lf-input\" data-k=\"email\" type=\"email\" placeholder=\"voce@exemplo.com\" value=\"${email.replace(/\"/g,'&quot;')}\" autocomplete=\"email\" />\n        </div>\n        <div class=\"lf-field\">\n          <span class=\"lf-label\">senha</span>\n          <input class=\"lf-input\" data-k=\"password\" type=\"password\" placeholder=\"••••••••\" value=\"${password.replace(/\"/g,'&quot;')}\" autocomplete=\"current-password\" />\n        </div>\n        <div class=\"lf-err\">${err.replace(/</g,'&lt;')}</div>\n        <div class=\"lf-submit\">${btn}</div>\n        <div class=\"lf-toggle\">${isSignup ? 'já tenho conta · entrar' : 'criar conta'}</div>\n      </div>\n    </div>`;\n  }\n};\n"}}}}}}}},{"_id":"6a3a8592803be63949daab2b","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"GoldBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nconst KIND_OPTS = [ { value:'ganho', label:'Ganho' }, { value:'gasto', label:'Gasto' } ];\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:16px; padding:4px; font-family:'Alegreya',sans-serif; background:#1c140d; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .title{ color:${PAL.gold}; font-family:'Jacquard 24','Alegreya',serif; font-size:34px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1; }\n  .sec-title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; margin:2px 2px 0; line-height:1; }\n  .panel{ position:relative; box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6);\n    image-rendering:pixelated; }\n  .bal-panel{ display:flex; flex-direction:column; align-items:center; gap:6px; padding:22px 16px; }\n  .bal-cap{ color:${PAL.cream}; font-size:12px; letter-spacing:2px; opacity:0.8; }\n  .bal-n{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:58px; line-height:1; text-shadow:2px 2px 0 ${PAL.edgeDark}, 0 0 14px rgba(216,178,76,0.45); }\n  .bal-unit{ color:${PAL.gold}; font-size:16px; letter-spacing:2px; opacity:0.9; }\n  .bal-break{ color:${PAL.cream}; font-size:12px; letter-spacing:1px; opacity:0.85; margin-top:4px; text-align:center; }\n  .bal-break b{ color:${PAL.gold}; }\n  .form-grid{ display:flex; flex-direction:column; gap:12px; }\n  .fld-label{ display:block; color:${PAL.gold}; font-size:11px; letter-spacing:2px; margin-bottom:5px; font-family:'Alegreya',sans-serif; }\n  .root input.gf-input{\n    width:100%; box-sizing:border-box; background:#241a10; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid ${PAL.edgeDark}; border-radius:4px;\n    padding:9px 10px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5), inset -2px -2px 0 rgba(255,255,255,0.04); outline:none; }\n  .root input.gf-input:focus{ border-color:${PAL.goldDeep}; }\n  .kind-row{ display:flex; gap:12px; align-items:flex-end; }\n  .kind-row .kind-sel{ flex:0 0 130px; }\n  .kind-row .amt{ flex:1; }\n  .form-actions{ margin-top:4px; }\n  .err{ color:${PAL.bloodLight}; font-size:13px; min-height:16px; }\n  .entry{ display:flex; align-items:center; gap:10px; padding:11px 12px; border-bottom:2px solid rgba(0,0,0,0.35); }\n  .entry:last-child{ border-bottom:none; }\n  .entry-main{ flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }\n  .entry-note{ color:${PAL.cream}; font-size:14px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; }\n  .entry-meta{ display:flex; gap:8px; align-items:center; }\n  .entry-kind{ font-size:10px; letter-spacing:1px; padding:2px 8px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .entry-kind.ganho{ background:${PAL.edgeDark}; color:${PAL.goldLight}; }\n  .entry-kind.gasto{ background:${PAL.blood}; color:${PAL.goldLight}; }\n  .entry-date{ color:${PAL.cream}; font-size:10px; opacity:0.55; letter-spacing:1px; }\n  .entry-amt{ flex:0 0 auto; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; line-height:1; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .entry-amt.pos{ color:${PAL.goldLight}; }\n  .entry-amt.neg{ color:${PAL.bloodLight}; }\n  .entry-del{ flex:0 0 auto; width:30px; height:30px; cursor:pointer; display:flex; align-items:center; justify-content:center;\n    color:${PAL.bloodLight}; background:rgba(156,51,32,0.18); border:2px solid ${PAL.blood}; border-radius:4px;\n    font-size:16px; line-height:1; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08); }\n  .entry-del:hover{ filter:brightness(1.15); color:${PAL.cream}; }\n  .entry-del:active{ transform:translateY(1px); }\n  .empty{ color:${PAL.cream}; opacity:0.7; font-size:14px; text-align:center; padding:16px 12px; }\n  .loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  `,\n  campaignId(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/gold\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2){\n    const cid = this.campaignId(d2);\n    const fallback = cid ? ('/campaign/' + cid) : '/';\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if(d2.state.form == null){ d2.state.form = { kind:'ganho', amount:'', note:'' }; }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.reload(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ this.goBack(d2); });\n    // text inputs: keep in buffer WITHOUT re-render to avoid cursor jump\n    d2.on('.gf-input','input',(e)=>{ this.field(d2, e.target.getAttribute('data-k'), e.target.value); });\n    d2.on('.entry-del','click',(e)=>{\n      const el = e.target.closest('.entry-del'); if(!el) return;\n      this.del(d2, el.getAttribute('data-id'));\n    });\n  },\n  field(d2, k, v){\n    if(!this._buf) this._buf = Object.assign({}, d2.state.form || {});\n    this._buf[k] = v;\n  },\n  current(d2){\n    return Object.assign({}, d2.state.form || {}, this._buf || {});\n  },\n  async load(d2){\n    if (this._loading) return;\n    const cid = this.campaignId(d2);\n    if (!cid){ d2.state.loading = false; return; }\n    this._cid = cid;\n    this._loading = true;\n    d2.state.loading = true;\n    try {\n      const res = await d2.rush('goldByCampaign').invoke({ campaignId: cid });\n      const data = res && res.data ? res.data : res;\n      d2.state.gold = data || {};\n      d2.state.error = '';\n    } catch(e){\n      d2.state.error = String(e && e.message ? e.message : e);\n      d2.state.gold = null;\n    }\n    d2.state.loading = false;\n    this._loading = false;\n    d2.refresh();\n  },\n  async reload(d2){\n    this._loading = false;\n    await this.load(d2);\n  },\n  async add(d2){\n    const f = this.current(d2);\n    const n = Math.abs(parseFloat(f.amount));\n    if (!n || isNaN(n)){ d2.state.error = 'Informe um valor'; d2.refresh(); return; }\n    const cid = this.campaignId(d2);\n    const kind = (f.kind === 'gasto') ? 'gasto' : 'ganho';\n    const amount = kind === 'gasto' ? -n : n;\n    const note = f.note || '';\n    d2.state.error = '';\n    // optimistic: push entry + adjust balance, re-render BEFORE awaiting\n    const g = d2.state.gold || {};\n    const optimistic = { _id: '__tmp_' + Date.now(), amount: amount, note: note, kind: kind, createdAt: new Date().toISOString() };\n    g.entries = [optimistic].concat(g.entries || []);\n    g.entriesTotal = (g.entriesTotal || 0) + amount;\n    g.balance = (g.balance || 0) + amount;\n    d2.state.gold = g;\n    // clear form\n    this._buf = null;\n    d2.state.form = { kind:'ganho', amount:'', note:'' };\n    d2.refresh();\n    try {\n      await d2.rush('addGoldEntry').invoke({ campaignId: cid, amount: amount, note: note, kind: kind });\n    } catch(e){ d2.state.error = 'Falha ao adicionar'; }\n    await this.reload(d2);\n  },\n  async del(d2, entryId){\n    if(!entryId) return;\n    const g = d2.state.gold || {};\n    const eidOf = (x)=> String(x.id != null ? x.id : x._id);\n    const ent = (g.entries || []).find(x => eidOf(x) === String(entryId));\n    // optimistic: remove + adjust balance, re-render BEFORE awaiting\n    g.entries = (g.entries || []).filter(x => eidOf(x) !== String(entryId));\n    if (ent){ g.entriesTotal = (g.entriesTotal || 0) - (ent.amount || 0); g.balance = (g.balance || 0) - (ent.amount || 0); }\n    d2.state.gold = g;\n    d2.refresh();\n    try {\n      await d2.rush('deleteGoldEntry').invoke({ entryId: entryId });\n    } catch(e){ /* keep optimistic */ }\n    await this.reload(d2);\n  },\n  render(d2){\n    const self = this;\n    const cid = this.campaignId(d2);\n    if (cid && cid !== this._cid){ this.load(d2); }\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    function attr(s){ return esc(s).replace(/\"/g,'&quot;'); }\n    function fmtN(n){ try { return Number(n||0).toLocaleString('pt-BR'); } catch(_){ return String(n||0); } }\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n\n    if (!cid || (d2.state.loading && !d2.state.gold)){\n      return `<div class=\"root\">${back}<div class=\"title\">Tesouro do Grupo</div><div class=\"loading\">Carregando…</div></div>`;\n    }\n    const g = d2.state.gold || {};\n    const err = d2.state.error ? `<div class=\"err\">${esc(d2.state.error)}</div>` : '';\n    const f = d2.state.form || {};\n\n    const balSec = `<div class=\"panel bal-panel\">\n      <div class=\"bal-cap\">Ouro do grupo</div>\n      <div class=\"bal-n\">${fmtN(g.balance)}</div>\n      <div class=\"bal-unit\">peças de ouro</div>\n      <div class=\"bal-break\">Sessões: <b>${fmtN(g.sessionGold)}</b>  ·  Ajustes: <b>${fmtN(g.entriesTotal)}</b></div>\n    </div>`;\n\n    const kindSel = d2.render('cronica-app@GoldenSelect',\n      { options: KIND_OPTS, value: f.kind || 'ganho', label:'TIPO' },\n      { key:'kindSel', style:'height:71px', onChange:(v)=>{ self.field(d2, 'kind', v); d2.state.form = Object.assign({}, self.current(d2)); self._buf = null; d2.refresh(); } });\n    const addBtn = d2.render('cronica-app@GoldenButton',\n      { label:'Adicionar', kind:'gold' },\n      { key:'addBtn', style:'height:50px', onPress:()=>{ self.add(d2); } });\n\n    const formSec = `<div class=\"sec-title\">Adicionar ajuste</div>\n      <div class=\"panel form-grid\">\n        ${err}\n        <div class=\"kind-row\">\n          <div class=\"kind-sel\">${kindSel}</div>\n          <div class=\"amt\"><label class=\"fld-label\">VALOR</label>\n            <input class=\"gf-input\" data-k=\"amount\" type=\"number\" min=\"0\" step=\"1\" value=\"${attr(f.amount)}\" placeholder=\"0\" /></div>\n        </div>\n        <div><label class=\"fld-label\">NOTA</label>\n          <input class=\"gf-input\" data-k=\"note\" value=\"${attr(f.note)}\" placeholder=\"ex: venda de tesouro, compra de cavalos\" /></div>\n        <div class=\"form-actions\">${addBtn}</div>\n      </div>`;\n\n    const entries = (g.entries || []).slice().sort((a,b)=>{\n      const ta = a.createdAt ? Date.parse(a.createdAt) : 0;\n      const tb = b.createdAt ? Date.parse(b.createdAt) : 0;\n      return tb - ta;\n    });\n    const rows = entries.length ? entries.map(en => {\n      const eid = en.id != null ? en.id : en._id;\n      const amt = en.amount || 0;\n      const pos = amt >= 0;\n      const sign = pos ? '+' : '−';\n      const kc = en.kind === 'gasto' ? 'gasto' : 'ganho';\n      let dt = '';\n      try { if(en.createdAt){ dt = new Date(en.createdAt).toLocaleDateString('pt-BR'); } } catch(_){}\n      const note = (en.note && String(en.note).trim()) ? esc(en.note) : '(sem nota)';\n      return `<div class=\"entry\" data-key=\"${attr(eid)}\">\n        <div class=\"entry-main\">\n          <div class=\"entry-note\">${note}</div>\n          <div class=\"entry-meta\"><span class=\"entry-kind ${kc}\">${kc}</span>${dt?`<span class=\"entry-date\">${esc(dt)}</span>`:''}</div>\n        </div>\n        <div class=\"entry-amt ${pos?'pos':'neg'}\">${sign}${fmtN(Math.abs(amt))}</div>\n        <div class=\"entry-del\" data-id=\"${attr(eid)}\">×</div>\n      </div>`;\n    }).join('') : `<div class=\"empty\">Nenhum ajuste manual ainda.</div>`;\n    const listSec = `<div class=\"sec-title\">Ajustes manuais</div><div class=\"panel\" style=\"padding:6px 12px\">${rows}</div>`;\n\n    return `<div class=\"root\">${back}<div class=\"title\">Tesouro do Grupo</div>${balSec}${formSec}${listSec}</div>`;\n  }\n}"}}}}}}}},{"_id":"6a3963a58509ad09a462e30e","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"CombatBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = {\n  bg:'#1c140d', panel:'#241a10', panel2:'#2c2013', stone:'#3a2c1a',\n  gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a',\n  blood:'#9c3320', bloodLight:'#c4543a', cream:'#ece0c4', edgeDark:'#120c07',\n  muted:'#a08a5e', edgeLight:'#6a5436', inputBg:'#241a10',\n  hpGood:'#5a8c3a', hpMid:'#c8932f', hpLow:'#9c3320'\n};\nconst GB = 'cronica-app@GoldenButton';\nconst GF = 'cronica-app@GoldenField';\nconst GA = 'cronica-app@GoldenAutocomplete';\nconst GS = 'cronica-app@GoldenSelect';\n\nconst CONDITIONS = ['Envenenado','Caído','Atordoado','Agarrado','Amedrontado','Cego','Surdo','Enfeitiçado','Inconsciente','Paralisado','Petrificado','Exausto','Invisível','Impedido','Incapacitado'];\n// Condições que tiram ações no D&D 5e — esmaecem o combatente (o mestre ainda decide pular o turno).\nconst INCAP = ['Inconsciente','Paralisado','Petrificado','Atordoado','Incapacitado'];\n\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;'); }\nfunction clamp(n, lo, hi){ n = Number(n)||0; if(n<lo) return lo; if(n>hi) return hi; return n; }\nfunction genId(){ return 'c' + Date.now() + Math.floor(Math.random()*1000); }\nfunction ordered(combatants){ return (combatants||[]).slice().sort((a,b)=>{ const d=(Number(b.initiative)||0)-(Number(a.initiative)||0); return d!==0 ? d : String(a.id).localeCompare(String(b.id)); }); }\nfunction combatIdFromPath(){\n  try {\n    const pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n    const m = pn.match(/combat\\/([^\\/?#]+)/);\n    if(m) return decodeURIComponent(m[1]);\n  } catch(_){}\n  return '';\n}\n// View mode shared across clients via localStorage ('mestre' | 'jogador', default 'mestre').\nconst VIEW_KEY = 'cronica-view-mode';\nfunction getViewMode(){\n  try {\n    if(typeof localStorage !== 'undefined'){\n      const v = localStorage.getItem(VIEW_KEY);\n      if(v === 'jogador' || v === 'mestre') return v;\n    }\n  } catch(_){}\n  return 'mestre';\n}\nfunction setViewMode(v){\n  try {\n    if(typeof localStorage !== 'undefined'){ localStorage.setItem(VIEW_KEY, v); }\n  } catch(_){}\n}\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; overflow-y:auto; box-sizing:border-box; font-family:'Alegreya',sans-serif;\n    color:${PAL.cream}; background:${PAL.bg}; padding:2px 0 40px; display:flex; flex-direction:column; gap:18px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:${PAL.gold}; background:rgba(216,178,76,0.08); border:2px solid ${PAL.goldDeep}; border-radius:4px;\n    padding:6px 12px; margin-bottom:6px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .ttl{ font-family:'Jacquard 24',serif; color:${PAL.goldLight}; line-height:1.05; text-shadow:2px 2px 0 ${PAL.edgeDark}; }\n  .sec-ttl{ font-family:'Jacquard 24',serif; font-size:24px; color:${PAL.gold};\n    text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1.35; padding-bottom:6px; margin:0 0 10px; display:block; }\n  .panel{ background:${PAL.panel}; border:3px solid ${PAL.edgeDark}; border-radius:8px;\n    padding:14px; box-shadow: inset 2px 2px 0 rgba(255,255,255,0.04), inset -2px -2px 0 rgba(0,0,0,0.5); }\n  .hdr{ display:flex; align-items:center; gap:12px; justify-content:space-between; flex-wrap:wrap; }\n  .hdr-l{ display:flex; flex-direction:column; gap:6px; }\n  .pill{ align-self:flex-start; font-size:12px; letter-spacing:1px; padding:3px 10px; border-radius:20px;\n    border:2px solid ${PAL.edgeDark}; color:${PAL.edgeDark}; }\n  .pill.open{ background:${PAL.goldLight}; }\n  .pill.closed{ background:${PAL.muted}; }\n  .round-box{ display:flex; align-items:center; gap:14px; flex:0 0 auto; }\n  .round-num{ font-family:'Jacquard 24',serif; font-size:34px; color:${PAL.goldLight}; line-height:1; }\n  .round-lbl{ font-size:12px; color:${PAL.muted}; letter-spacing:1px; }\n  .turn-slot{ width:100%; height:42px; }\n  .end-slot{ width:100%; height:42px; }\n  .hdr-actions{ display:flex; flex-direction:column; gap:8px; align-items:stretch; width:168px; max-width:100%; }\n  .list{ display:flex; flex-direction:column; gap:10px; }\n  .cmb{ background:${PAL.panel2}; border:2px solid ${PAL.stone}; border-radius:8px; padding:10px 12px;\n    display:flex; flex-direction:column; gap:8px; transition:border-color 140ms ease, box-shadow 140ms ease, opacity 140ms ease; }\n  .cmb.active{ border-color:${PAL.gold}; box-shadow:0 0 12px rgba(216,178,76,0.5), inset 0 0 6px rgba(216,178,76,0.18); }\n  .cmb.down{ opacity:0.45; filter:grayscale(0.5); }\n  .cmb-top{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }\n  .side{ width:10px; height:10px; border-radius:50%; flex:0 0 auto; }\n  .side.hero{ background:${PAL.hpGood}; box-shadow:0 0 6px rgba(90,140,58,0.6); }\n  .side.enemy{ background:${PAL.blood}; box-shadow:0 0 6px rgba(156,51,32,0.6); }\n  .cmb-name{ font-size:17px; color:${PAL.goldLight}; flex:1 1 auto; min-width:120px; }\n  .cmb-name .turn-mark{ color:${PAL.gold}; margin-right:5px; }\n  .ca{ font-size:13px; color:${PAL.muted}; letter-spacing:0.5px; flex:0 0 auto; }\n  .ini-wrap{ display:flex; align-items:center; gap:5px; flex:0 0 auto; }\n  .ini-wrap label{ font-size:12px; color:${PAL.muted}; }\n  .ini-input{ width:54px; box-sizing:border-box; background:${PAL.inputBg}; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:14px; text-align:center; border:2px solid ${PAL.edgeDark};\n    border-radius:4px; padding:5px 4px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .ini-input:focus{ border-color:${PAL.goldDeep}; }\n  .hp-row{ display:flex; align-items:center; gap:8px; }\n  .hp-bar{ flex:1 1 auto; height:16px; border-radius:8px; background:${PAL.edgeDark};\n    border:2px solid ${PAL.edgeDark}; overflow:hidden; box-shadow: inset 1px 1px 0 rgba(0,0,0,0.5); }\n  .hp-fill{ height:100%; border-radius:6px 0 0 6px; transition:width 200ms ease, background 200ms ease; }\n  .hp-txt{ font-size:13px; color:${PAL.cream}; flex:0 0 auto; min-width:64px; text-align:right; letter-spacing:0.5px; }\n  .hp-ctrl{ display:flex; align-items:center; gap:6px; flex:0 0 auto; }\n  .hp-amt{ width:46px; box-sizing:border-box; background:${PAL.inputBg}; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:13px; text-align:center; border:2px solid ${PAL.edgeDark};\n    border-radius:4px; padding:5px 3px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .hp-amt:focus{ border-color:${PAL.goldDeep}; }\n  .hp-btn-slot{ width:36px; height:32px; flex:0 0 auto; }\n  .conds{ display:flex; flex-wrap:wrap; gap:6px; align-items:center; }\n  .chip{ display:inline-flex; align-items:center; gap:5px; font-size:12px; letter-spacing:0.3px;\n    background:linear-gradient(180deg,${PAL.stone},${PAL.edgeDark}); color:${PAL.cream};\n    border:1px solid ${PAL.goldDeep}; border-radius:14px; padding:3px 6px 3px 9px; }\n  .chip .x{ cursor:pointer; color:${PAL.bloodLight}; font-weight:bold; padding:0 2px; }\n  .chip .x:hover{ color:${PAL.blood}; }\n  .cond-add{ display:flex; gap:8px; align-items:flex-end; flex-wrap:wrap; margin-top:2px; }\n  .cond-sel-slot{ width:180px; }\n  .cond-free-wrap{ flex:1 1 auto; min-width:140px; display:flex; gap:6px; align-items:flex-end; }\n  .cond-free-input{ flex:1 1 auto; box-sizing:border-box; background:${PAL.inputBg}; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:13px; border:2px solid ${PAL.edgeDark}; border-radius:4px;\n    padding:7px 9px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .cond-free-input:focus{ border-color:${PAL.goldDeep}; }\n  .cond-free-btn{ width:80px; height:34px; flex:0 0 auto; }\n  .add-enemy{ display:flex; flex-direction:column; gap:10px; }\n  .ae-row{ display:flex; gap:8px; align-items:flex-end; flex-wrap:wrap; }\n  .ae-ac{ flex:1 1 220px; min-height:42px; }\n  .ae-fld{ flex:1 1 90px; }\n  .ae-fld.sm{ flex:0 0 90px; }\n  .ae-label{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px;\n    letter-spacing:1px; margin-bottom:5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .ae-input{ width:100%; box-sizing:border-box; background:${PAL.inputBg}; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid ${PAL.edgeDark}; border-radius:4px;\n    padding:9px 11px; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .ae-input:focus{ border-color:${PAL.goldDeep}; }\n  .ae-input::placeholder{ color:rgba(236,224,196,0.4); }\n  .ae-add-slot{ width:100%; height:42px; }\n  .kill-banner{ background:rgba(156,51,32,0.12); border:2px solid ${PAL.blood}; border-radius:8px; padding:8px 10px; margin:-4px 0 2px; display:flex; flex-direction:column; gap:6px; }\n  .kill-q{ font-size:13px; color:${PAL.bloodLight}; letter-spacing:0.5px; }\n  .kill-chips{ display:flex; flex-wrap:wrap; gap:6px; }\n  .kill-chip{ cursor:pointer; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:0.3px; color:${PAL.cream}; background:${PAL.panel2}; border:2px solid ${PAL.stone}; border-radius:14px; padding:5px 11px; }\n  .kill-chip:hover{ filter:brightness(1.12); border-color:${PAL.goldDeep}; }\n  .kill-chip.on{ background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); color:${PAL.edgeDark}; border-color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .kill-chip.none.on{ background:linear-gradient(180deg,${PAL.bloodLight},${PAL.blood}); color:${PAL.cream}; }\n  .view-toggle{ display:inline-flex; align-items:center; justify-content:center; gap:6px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:0.5px; width:100%; box-sizing:border-box;\n    color:${PAL.gold}; background:rgba(216,178,76,0.08); border:2px solid ${PAL.goldDeep}; border-radius:4px;\n    padding:7px 10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .view-toggle:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .view-toggle:active{ transform:translateY(1px); }\n  .view-toggle.jogador{ color:${PAL.cream}; border-color:${PAL.stone}; background:rgba(58,44,26,0.4); }\n  .hint{ font-size:12px; color:${PAL.muted}; margin-top:8px; }\n  .loading{ padding:60px 20px; text-align:center; color:${PAL.gold}; font-size:18px; }\n  .err{ padding:40px 20px; text-align:center; color:${PAL.bloodLight}; font-size:16px; }\n  `,\n\n  render(d2){\n    const combatId = combatIdFromPath();\n    if(combatId && d2.state.loadedId !== combatId && d2.state.loadingId !== combatId && d2.state.errorId !== combatId){\n      this.load(d2, combatId);\n    }\n\n    if(!combatId){\n      return `<div class=\"root\"><div class=\"err\">Combate não informado.</div></div>`;\n    }\n    if(d2.state.errorId === combatId){\n      return `<div class=\"root\">\n        <button class=\"back\">‹ Voltar</button>\n        <div class=\"panel\"><div class=\"err\">Combate não encontrado.</div></div>\n      </div>`;\n    }\n    const combat = d2.state.combat;\n    if(!combat || d2.state.loadedId !== combatId){\n      return `<div class=\"root\"><div class=\"loading\">Carregando combate…</div></div>`;\n    }\n\n    const combatants = combat.combatants || [];\n    const list = ordered(combatants);\n    const turnIndex = Number(combat.turnIndex)||0;\n    const activeId = list.length ? (list[turnIndex % list.length] || {}).id : null;\n    const isEnded = combat.status === 'ended';\n    const statusTxt = isEnded ? 'encerrado' : (combat.status || 'ativo');\n    const viewMode = getViewMode();\n    const isPlayer = viewMode === 'jogador';\n\n    // ---- Header (round + turn controls) ----\n    const headerHtml = `\n      <button class=\"back\">‹ Voltar</button>\n      <div class=\"panel hdr\">\n        <div class=\"hdr-l\">\n          <div class=\"ttl\" style=\"font-size:28px\">Combate</div>\n          <span class=\"pill ${isEnded?'closed':'open'}\">${esc(statusTxt)}</span>\n        </div>\n        <div class=\"round-box\">\n          <div style=\"text-align:center\">\n            <div class=\"round-num\">${esc(combat.round||1)}</div>\n            <div class=\"round-lbl\">rodada</div>\n          </div>\n          <div class=\"hdr-actions\">\n            <button class=\"view-toggle ${isPlayer?'jogador':''}\" title=\"Alternar visão Mestre/Jogador\">${isPlayer?'Jogador':'Mestre'} — trocar visão</button>\n            <div class=\"turn-slot\">${d2.render(GB, { label:'Próximo turno', kind:'gold', size:'sm', glyph:'➤' }, { key:'nextTurn', onPress:()=>this.nextTurn(d2) })}</div>\n            <div class=\"end-slot\">${d2.render(GB, { label:'Encerrar combate', kind:'danger', size:'sm', ghost:true, glyph:'☠' }, { key:'endCombat', onPress:()=>this.endCombat(d2) })}</div>\n          </div>\n        </div>\n      </div>`;\n\n    // ---- Combatant list (initiative order) ----\n    const heroList = list.filter(c=>c.side==='hero');\n    const rowsHtml = list.map(c=>{\n      // hpMax is often 0/unset on hero sheets. Don't clamp the displayed current\n      // HP down to a bogus max (that made edited hero HP render as \"1/1\"); use the\n      // larger of hpMax and the current value as the denominator so the real HP\n      // shows and the bar fills proportionally.\n      const rawCur = Number(c.hpAtual)||0;\n      const max = Math.max(1, Number(c.hpMax)||0, rawCur);\n      const cur = clamp(rawCur, 0, max);\n      const pct = Math.round((cur/max)*100);\n      const hpColor = pct>50 ? PAL.hpGood : (pct>25 ? PAL.hpMid : PAL.hpLow);\n      const active = c.id === activeId;\n      // Player mode masks enemy HP/CA values (hero rows always show real numbers).\n      const maskStats = isPlayer && c.side==='enemy';\n      const hasIncap = (c.conditions||[]).some(x=>INCAP.indexOf(x)!==-1);\n      const isDown = c.down || cur<=0 || hasIncap;\n      const amt = (d2.state.amt && d2.state.amt[c.id] != null) ? d2.state.amt[c.id] : '';\n      const condChips = (c.conditions||[]).map((cond,ci)=>\n        `<span class=\"chip\">${esc(cond)}<span class=\"x\" data-rmcond=\"${esc(c.id)}\" data-condidx=\"${ci}\">×</span></span>`\n      ).join('');\n      const condSel = d2.render(GS,\n        { options:JSON.stringify([{ value:'', label:'+ condição' }].concat(CONDITIONS.map(x=>({ value:x, label:x })))), value:'', placeholder:'+ condição' },\n        { key:'cond-'+c.id+'-'+(c.conditions||[]).length, style:'height:34px', onChange:(v)=>this.addCondition(d2, c.id, v) });\n      // Kill attribution: shown for any enemy at 0 HP, rendered OUTSIDE the dimmed\n      // row (sibling) so it stays interactive. Editable anytime; abate events are\n      // written only at endCombat (enemy curado/ressuscitado não conta).\n      let killBanner = '';\n      if(c.side==='enemy' && cur<=0){\n        const chips = heroList.map(h=>\n          `<button class=\"kill-chip ${c.killerHeroId===h.id?'on':''}\" data-kill=\"${esc(c.id)}\" data-killer=\"${esc(h.id)}\">${esc(h.name)}</button>`\n        ).join('');\n        killBanner = `<div class=\"kill-banner\">\n          <span class=\"kill-q\">☠ ${esc(c.name)} caiu — quem abateu?</span>\n          <div class=\"kill-chips\">\n            ${chips}\n            <button class=\"kill-chip none ${!c.killerHeroId?'on':''}\" data-kill=\"${esc(c.id)}\" data-killer=\"\">— ninguém —</button>\n          </div>\n        </div>`;\n      }\n      return `<div class=\"cmb ${active?'active':''} ${isDown?'down':''}\" data-key=\"${esc(c.id)}\">\n        <div class=\"cmb-top\">\n          <span class=\"side ${c.side==='hero'?'hero':'enemy'}\"></span>\n          <span class=\"cmb-name\">${active?'<span class=\"turn-mark\">▶</span>':''}${esc(c.name)}</span>\n          <span class=\"ini-wrap\"><label>CA</label>${maskStats ? `<span class=\"ini-input\" style=\"display:inline-flex;align-items:center;justify-content:center\">?</span>` : `<input class=\"ini-input\" type=\"number\" data-ca=\"${esc(c.id)}\" value=\"${esc(c.ca!=null?c.ca:10)}\" />`}</span>\n          <span class=\"ini-wrap\"><label>init</label><input class=\"ini-input\" type=\"number\" data-ini=\"${esc(c.id)}\" value=\"${esc(c.initiative!=null?c.initiative:0)}\" /></span>\n          <span class=\"ini-wrap\"><label>HP máx</label>${maskStats ? `<span class=\"ini-input\" style=\"display:inline-flex;align-items:center;justify-content:center\">?</span>` : `<input class=\"ini-input\" type=\"number\" data-hpmax=\"${esc(c.id)}\" value=\"${esc(c.hpMax!=null?c.hpMax:0)}\" />`}</span>\n        </div>\n        <div class=\"hp-row\">\n          <div class=\"hp-ctrl\">\n            <div class=\"hp-btn-slot\">${d2.render(GB, { label:'−', kind:'danger', size:'sm' }, { key:'dmg-'+c.id, onPress:()=>this.applyHp(d2, c.id, -1) })}</div>\n            <input class=\"hp-amt\" type=\"number\" data-amt=\"${esc(c.id)}\" placeholder=\"1\" value=\"${esc(amt)}\" />\n            <div class=\"hp-btn-slot\">${d2.render(GB, { label:'+', kind:'gold', size:'sm' }, { key:'heal-'+c.id, onPress:()=>this.applyHp(d2, c.id, 1) })}</div>\n          </div>\n          <div class=\"hp-bar\">${maskStats ? '' : `<div class=\"hp-fill\" style=\"width:${pct}%; background:${hpColor}\"></div>`}</div>\n          <span class=\"hp-txt\">${maskStats ? 'HP ???' : `${cur}/${max}`}</span>\n        </div>\n        <div class=\"conds\">\n          ${condChips || '<span class=\"hint\" style=\"margin:0\">sem condições</span>'}\n        </div>\n        <div class=\"cond-add\">\n          <div class=\"cond-sel-slot\">${condSel}</div>\n          <div class=\"cond-free-wrap\">\n            <input class=\"cond-free-input\" type=\"text\" data-condfree=\"${esc(c.id)}\" placeholder=\"condição livre…\" value=\"${esc((d2.state.condFree&&d2.state.condFree[c.id])||'')}\" />\n            <div class=\"cond-free-btn\">${d2.render(GB, { label:'+ tag', kind:'gold', size:'sm', ghost:true }, { key:'condfreebtn-'+c.id, onPress:()=>this.addFreeCondition(d2, c.id) })}</div>\n          </div>\n        </div>\n      </div>${killBanner}`;\n    }).join('');\n\n    const listHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Ordem de iniciativa</div>\n        <div class=\"list\">\n          ${list.length ? rowsHtml : '<div class=\"hint\">Nenhum combatente.</div>'}\n        </div>\n      </div>`;\n\n    // ---- Add enemy ----\n    const f = d2.state.enemyForm || {};\n    const addEnemyHtml = `\n      <div class=\"panel add-enemy\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Adicionar inimigo</div>\n        <div class=\"ae-ac\">${d2.render(GA,\n          { searchHandler:'searchMonsters', placeholder:'Buscar no catálogo…', label:'Monstro (catálogo)', value:(f.name||'') },\n          { key:'enemyAC-'+(d2.state.enemyACKey||0), onPick:(name)=>this.pickMonster(d2, name) })}</div>\n        <div class=\"ae-row\">\n          <div class=\"ae-fld\">\n            <div class=\"ae-label\">Nome</div>\n            <input class=\"ae-input\" type=\"text\" data-ef=\"name\" placeholder=\"Nome do inimigo\" value=\"${esc(f.name||'')}\" />\n          </div>\n          <div class=\"ae-fld sm\">\n            <div class=\"ae-label\">HP</div>\n            <input class=\"ae-input\" type=\"number\" data-ef=\"hp\" placeholder=\"0\" value=\"${esc(f.hp!=null?f.hp:'')}\" />\n          </div>\n          <div class=\"ae-fld sm\">\n            <div class=\"ae-label\">CA</div>\n            <input class=\"ae-input\" type=\"number\" data-ef=\"ca\" placeholder=\"10\" value=\"${esc(f.ca!=null?f.ca:'')}\" />\n          </div>\n        </div>\n        <div class=\"ae-add-slot\">${d2.render(GB, { label:'+ Adicionar inimigo', kind:'gold', size:'sm', glyph:'✚' }, { key:'addEnemyBtn', onPress:()=>this.addEnemy(d2) })}</div>\n        <div class=\"hint\">Escolha do catálogo para preencher HP/CA, ou digite tudo manualmente (inimigo caseiro).</div>\n      </div>`;\n\n    return `<div class=\"root\">\n      ${headerHtml}\n      ${listHtml}\n      ${addEnemyHtml}\n    </div>`;\n  },\n\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if(!d2.state.amt) d2.state.amt = {};\n    if(!d2.state.condFree) d2.state.condFree = {};\n    if(!d2.state.enemyForm) d2.state.enemyForm = {};\n    if(d2.state.dirtyUntil == null) d2.state.dirtyUntil = 0;\n\n    // ---- Live refresh: polling (~3s), guarded against clobbering local edits ----\n    // setInterval is guarded so it never throws in non-browser/dry-render envs.\n    try {\n      if(typeof setInterval === 'function' && !d2.state.pollId){\n        d2.state.pollId = setInterval(()=>{ try { this.refreshCombat(d2); } catch(_){} }, 3000);\n        if(typeof d2.onUnmount === 'function'){\n          d2.onUnmount(()=>{ try { clearInterval(d2.state.pollId); } catch(_){} d2.state.pollId = null; });\n        }\n      }\n    } catch(_){}\n\n    // ---- WebSocket client (immediate refresh; polling is the fallback) ----\n    // Fully optional: if d2.socket is absent or throws, swallow it — polling covers.\n    try {\n      const wsCombatId = combatIdFromPath();\n      const sock = d2.socket && d2.socket('combatLive');\n      if(sock && wsCombatId){\n        sock.subscribe(wsCombatId);\n        sock.onMessage(()=>{ try { this.refreshCombat(d2); } catch(_){} });\n      }\n    } catch(_){}\n\n    try { d2.on(window, 'popstate', ()=>{ try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ this.goBack(d2); });\n    // Master/Player view toggle — flips shared localStorage mode + re-renders.\n    d2.on('.view-toggle','click',()=>{\n      const next = getViewMode() === 'mestre' ? 'jogador' : 'mestre';\n      setViewMode(next);\n      try { d2.refresh(); } catch(_){}\n    });\n    // initiative input -> set + persist (on change, not every keystroke).\n    d2.on('.ini-input','change',(e)=>{\n      const t = e.target; if(!t) return;\n      const iniId = t.getAttribute('data-ini');\n      if(iniId){ this.setInitiative(d2, iniId, t.value); return; }\n      const caId = t.getAttribute('data-ca');\n      if(caId){ this.setCa(d2, caId, t.value); return; }\n      const hpMaxId = t.getAttribute('data-hpmax');\n      if(hpMaxId){ this.setHpMax(d2, hpMaxId, t.value); }\n    });\n    // per-combatant custom amount field (kept in d2.state.amt; no re-render on input).\n    d2.on('.hp-amt','input',(e)=>{\n      const id = e.target && e.target.getAttribute('data-amt');\n      if(id){ d2.state.amt[id] = e.target.value; }\n    });\n    // free condition text field (kept in d2.state.condFree).\n    d2.on('.cond-free-input','input',(e)=>{\n      const id = e.target && e.target.getAttribute('data-condfree');\n      if(id){ d2.state.condFree[id] = e.target.value; }\n    });\n    // remove condition chip\n    d2.on('.chip .x','click',(e)=>{\n      const x = e.target.closest('.x');\n      if(!x) return;\n      const id = x.getAttribute('data-rmcond');\n      const idx = parseInt(x.getAttribute('data-condidx'),10);\n      if(id != null && !isNaN(idx)){ this.removeCondition(d2, id, idx); }\n    });\n    // kill attribution chips (sibling banner, not dimmed) -> set killer on combatant\n    d2.on('.kill-chip','click',(e)=>{\n      const b = e.target.closest('.kill-chip');\n      if(!b) return;\n      const enemyId = b.getAttribute('data-kill');\n      const heroId = b.getAttribute('data-killer') || '';\n      if(enemyId){ this.setKiller(d2, enemyId, heroId); }\n    });\n    // add-enemy form fields -> d2.state.enemyForm\n    d2.on('.ae-input','input',(e)=>{\n      const field = e.target && e.target.getAttribute('data-ef');\n      if(field){ const f = Object.assign({}, d2.state.enemyForm||{}); f[field] = e.target.value; d2.state.enemyForm = f; }\n    });\n  },\n\n  // ---------- data loading ----------\n  load(d2, combatId){\n    d2.state.loadingId = combatId;\n    d2.rush('combatDetail').invoke({ combatId }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      if(data && data.combat){\n        d2.state.combat = data.combat;\n        d2.state.loadedId = combatId;\n        d2.state.errorId = null;\n      } else {\n        d2.state.errorId = combatId;\n      }\n      d2.state.loadingId = null;\n    }).catch((err)=>{\n      d2.log('combatDetail error', err);\n      d2.state.errorId = combatId;\n      d2.state.loadingId = null;\n    });\n  },\n  // Guarded background re-fetch shared by polling + websocket. Replaces ONLY\n  // d2.state.combat (transient input state in amt/condFree/enemyForm untouched),\n  // and SKIPS while the user has a recent local edit in flight (dirtyUntil).\n  refreshCombat(d2){\n    const combatId = d2.state.loadedId || combatIdFromPath();\n    if(!combatId) return;\n    const cur = d2.state.combat;\n    if(cur && cur.status === 'ended') return;\n    if(Date.now() < (d2.state.dirtyUntil||0)) return;\n    d2.rush('combatDetail').invoke({ combatId }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      // Re-check the guard after the await — a local edit may have started mid-fetch.\n      if(Date.now() < (d2.state.dirtyUntil||0)) return;\n      if(data && data.combat && d2.state.loadedId === combatId){\n        d2.state.combat = data.combat;\n        try { d2.refresh(); } catch(_){}\n      }\n    }).catch((e)=>d2.log('refreshCombat', e));\n  },\n\n  // ---------- helpers ----------\n  cmbById(d2, id){\n    const c = d2.state.combat;\n    return (c && (c.combatants||[]).find(x=>x.id===id)) || null;\n  },\n  // Replace the combatants array on the cached combat (new object refs so render reacts).\n  setCombatants(d2, combatants){\n    d2.state.combat = Object.assign({}, d2.state.combat, { combatants });\n  },\n  // Single write of the whole combatants array.\n  persistCombatants(d2){\n    const c = d2.state.combat;\n    if(!c) return;\n    d2.state.dirtyUntil = Date.now() + 3500;\n    d2.rush('updateCombat').invoke({ combatId: c._id, combatants: c.combatants })\n      .catch((e)=>d2.log('updateCombat combatants', e));\n  },\n\n  // ---------- initiative ----------\n  setInitiative(d2, id, value){\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    const next = (d2.state.combat.combatants||[]).map(x=> x.id===id ? Object.assign({}, x, { initiative: Number(value)||0 }) : x);\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n  setCa(d2, id, value){\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    const next = (d2.state.combat.combatants||[]).map(x=> x.id===id ? Object.assign({}, x, { ca: Number(value)||0 }) : x);\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n  // Edit a combatant's MAX HP inline (master action; for enemies it fixes the\n  // total, for heroes it's a combat-local override — NOT written back to the\n  // hero sheet). Clamps hpAtual down when the new max is lower (and >0).\n  setHpMax(d2, id, value){\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    const newMax = Number(value)||0;\n    const next = (d2.state.combat.combatants||[]).map(x=>{\n      if(x.id!==id) return x;\n      const cur = Number(x.hpAtual)||0;\n      const hpAtual = (newMax > 0 && cur > newMax) ? newMax : cur;\n      return Object.assign({}, x, { hpMax: newMax, hpAtual });\n    });\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n\n  // ---------- HP damage/heal ----------\n  applyHp(d2, id, sign){\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    // Upper bound = the combatant's hpMax, but ONLY when it's a real (>0) value.\n    // Many hero sheets carry hpMax:0 (never filled in); clamping to that traps the\n    // hero at 0/1 HP and makes \"+ / −\" look broken (\"can't edit hero HP\"). When\n    // hpMax is unset/0 we impose no ceiling so the master can still set real HP.\n    const hpMax = Number(c.hpMax)||0;\n    const cur0 = Number(c.hpAtual)||0;\n    let amt = parseInt((d2.state.amt && d2.state.amt[id])||'',10);\n    if(!amt || isNaN(amt) || amt<=0) amt = 1;\n    const delta = sign * amt;\n    const ceil = hpMax > 0 ? hpMax : Infinity;\n    const newHp = clamp(cur0 + delta, 0, ceil);\n    const down = newHp <= 0;\n    const next = (d2.state.combat.combatants||[]).map(x=> x.id===id ? Object.assign({}, x, { hpAtual:newHp, down }) : x);\n    this.setCombatants(d2, next);\n    // Single combat write (whole combatants array).\n    this.persistCombatants(d2);\n    // Sync hero HP to their sheet (separate, allowed second write — different doc).\n    if(c.side==='hero' && c.refId){\n      d2.rush('updateHero').invoke({ heroId: c.refId, hpAtual: newHp }).catch((e)=>d2.log('updateHero hp', e));\n    }\n  },\n\n  // ---------- conditions ----------\n  addCondition(d2, id, cond){\n    if(!cond) return;\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    if((c.conditions||[]).indexOf(cond) !== -1) return;\n    const next = (d2.state.combat.combatants||[]).map(x=> x.id===id ? Object.assign({}, x, { conditions:(x.conditions||[]).concat([cond]) }) : x);\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n  addFreeCondition(d2, id){\n    const raw = (d2.state.condFree && d2.state.condFree[id]) || '';\n    const cond = String(raw).trim();\n    if(!cond) return;\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    if((c.conditions||[]).indexOf(cond) === -1){\n      const next = (d2.state.combat.combatants||[]).map(x=> x.id===id ? Object.assign({}, x, { conditions:(x.conditions||[]).concat([cond]) }) : x);\n      this.setCombatants(d2, next);\n      this.persistCombatants(d2);\n    }\n    d2.state.condFree[id] = '';\n  },\n  removeCondition(d2, id, idx){\n    const c = this.cmbById(d2, id);\n    if(!c) return;\n    const next = (d2.state.combat.combatants||[]).map(x=>{\n      if(x.id!==id) return x;\n      const conds = (x.conditions||[]).slice();\n      conds.splice(idx,1);\n      return Object.assign({}, x, { conditions:conds });\n    });\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n\n  // ---------- turn / round ----------\n  nextTurn(d2){\n    const c = d2.state.combat;\n    if(!c) return;\n    const n = (c.combatants||[]).length;\n    if(!n) return;\n    let turnIndex = (Number(c.turnIndex)||0) + 1;\n    let round = Number(c.round)||1;\n    if(turnIndex >= n){ turnIndex = 0; round = round + 1; }\n    d2.state.combat = Object.assign({}, c, { turnIndex, round });\n    d2.state.dirtyUntil = Date.now() + 3500;\n    d2.rush('updateCombat').invoke({ combatId: c._id, round, turnIndex }).catch((e)=>d2.log('updateCombat turn', e));\n  },\n  async endCombat(d2){\n    const c = d2.state.combat;\n    if(!c || d2.state.ending) return;\n    d2.state.ending = true;\n    // Abate events are written ONLY now, and only for enemies STILL down at 0 HP\n    // (resurrected/healed enemies don't count). Sequential writes (never parallel).\n    const downEnemies = (c.combatants||[]).filter(x=> x.side==='enemy' && (Number(x.hpAtual)||0) <= 0);\n    for(const e of downEnemies){\n      try {\n        await d2.rush('addEvent').invoke({\n          sessionId: c.sessionId,\n          campaignId: c.campaignId,\n          heroId: e.killerRefId || null,\n          heroName: e.killerHeroName || null,\n          type: 'abate',\n          note: e.name\n        });\n      } catch(err){ d2.log('endCombat abate', err); }\n    }\n    d2.state.combat = Object.assign({}, c, { status:'ended' });\n    d2.state.dirtyUntil = Date.now() + 3500;\n    try { await d2.rush('endCombat').invoke({ combatId: c._id }); }\n    catch(e){ d2.log('endCombat', e); }\n    d2.state.ending = false;\n    try { if(typeof window!=='undefined' && window.history){ window.history.back(); } } catch(_){}\n  },\n\n  // ---------- add enemy ----------\n  pickMonster(d2, name){\n    // The autocomplete fires onPick with the picked name; resolve full row via searchMonsters\n    // to prefill HP/CA. Catalog monsters expose name/type/cr/hp (no CA), so prefill HP and name,\n    // leave CA blank for the master to fill. refId stays null here (no _id exposed); set on match below.\n    const f = Object.assign({}, d2.state.enemyForm||{}, { name });\n    d2.state.enemyForm = f;\n    if(!name){ return; }\n    d2.rush('searchMonsters').invoke({ q: name }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      const results = (data && data.results) || [];\n      const exact = results.find(r=> String(r.name).toLowerCase() === String(name).toLowerCase()) || results[0];\n      if(exact){\n        const nf = Object.assign({}, d2.state.enemyForm||{}, {\n          name: exact.name,\n          hp: exact.hp!=null ? exact.hp : (d2.state.enemyForm.hp||'')\n        });\n        d2.state.enemyForm = nf;\n      }\n    }).catch((e)=>d2.log('pickMonster lookup', e));\n  },\n  addEnemy(d2){\n    const c = d2.state.combat;\n    if(!c) return;\n    const f = d2.state.enemyForm || {};\n    const name = String(f.name||'').trim();\n    if(!name){ try { if(d2.dialog && d2.dialog.alert){ d2.dialog.alert('Informe o nome do inimigo.'); } else { d2.log('addEnemy: nome vazio'); } } catch(_){ } return; }\n    const hp = parseInt(f.hp||'0',10) || 0;\n    const ca = parseInt(f.ca||'0',10) || 0;\n    const enemy = {\n      id: genId(), side:'enemy', refId: f.refId || null, name,\n      initiative: 0, hpMax: hp, hpAtual: hp, ca, conditions:[], down:false\n    };\n    const next = (c.combatants||[]).concat([enemy]);\n    this.setCombatants(d2, next);\n    d2.state.enemyForm = {};\n    d2.state.enemyACKey = (d2.state.enemyACKey||0) + 1;\n    this.persistCombatants(d2);\n  },\n\n  // ---------- abate / kill attribution ----------\n  // Records WHO killed an enemy on the combatant itself (combat doc) — editable\n  // anytime while the enemy is down. NO event is fired here; abate events are\n  // written only at endCombat, so a healed/resurrected enemy never counts.\n  setKiller(d2, enemyId, heroId){\n    const c = d2.state.combat;\n    if(!c) return;\n    const killer = heroId ? this.cmbById(d2, heroId) : null;\n    const next = (c.combatants||[]).map(x=> x.id===enemyId ? Object.assign({}, x, {\n      killerHeroId: heroId || null,\n      killerHeroName: killer ? killer.name : null,\n      killerRefId: killer ? killer.refId : null\n    }) : x);\n    this.setCombatants(d2, next);\n    this.persistCombatants(d2);\n  },\n\n  // ---------- nav ----------\n  goBack(d2){\n    try {\n      if(typeof window!=='undefined' && window.history && window.history.length > 1){ window.history.back(); return; }\n    } catch(_){}\n    try {\n      if(typeof window!=='undefined'){ window.history.pushState({}, '', '/'); window.dispatchEvent(new PopStateEvent('popstate', { state:{} })); }\n    } catch(_){}\n  }\n}\n"}}}}}}}},{"_id":"6a3406dac03e59866674c0ff","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"StatsBoard","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nconst LEADERS = [\n  { key:'maisNat20', glyph:'✨', label:'Mais sortudo', sub:'Nat 20' },\n  { key:'maisAzarado', glyph:'☠', label:'Mais azarado', sub:'Nat 1' },\n  { key:'maisKOs', glyph:'💫', label:'Mais nocauteado', sub:'KOs' },\n  { key:'maisLevelUps', glyph:'⬆', label:'Mais evoluído', sub:'Level-ups' },\n  { key:'maisMortes', glyph:'💀', label:'Mais fatídico', sub:'Mortes' },\n  { key:'maisAbates', glyph:'⚔', label:'Maior caçador', sub:'Abates' }\n];\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .sb{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:16px; padding:4px; font-family:'Alegreya',sans-serif; background:#1c140d; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .sb-head{ display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%; max-width:100%; box-sizing:border-box; }\n  .sb-title{ color:${PAL.gold}; font-family:'Jacquard 24','Alegreya',serif; font-size:34px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1; flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n  .sb-back{ flex:0 0 auto; width:120px; height:42px; }\n  .sb-back > *{ width:100% !important; height:100% !important; }\n  .sec-title{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; margin:2px 2px 0; line-height:1; }\n  .panel{ position:relative; box-sizing:border-box; padding:14px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6);\n    image-rendering:pixelated; }\n  .totals{ display:grid; grid-template-columns:repeat(auto-fit, minmax(92px,1fr)); gap:10px; }\n  .stat{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;\n    border:3px solid ${PAL.edgeDark}; border-radius:5px; padding:10px 6px;\n    background:linear-gradient(180deg, rgba(0,0,0,0.25), rgba(0,0,0,0.45));\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08); }\n  .stat-n{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:30px; line-height:1; text-shadow:2px 2px 0 ${PAL.edgeDark}, 0 0 8px rgba(216,178,76,0.35); }\n  .stat-l{ color:${PAL.cream}; font-size:11px; letter-spacing:1px; opacity:0.85; text-align:center; }\n  .lead-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(150px,1fr)); gap:12px; }\n  .trophy{ position:relative; box-sizing:border-box; padding:12px; border-radius:6px;\n    border:3px solid ${PAL.goldDeep};\n    background:linear-gradient(160deg, rgba(216,178,76,0.16) 0%, rgba(28,20,13,0.7) 70%);\n    box-shadow: inset 2px 2px 0 rgba(255,255,255,0.12), inset -2px -2px 0 rgba(0,0,0,0.5), 0 6px 14px rgba(0,0,0,0.5);\n    display:flex; flex-direction:column; gap:4px; }\n  .trophy-top{ display:flex; align-items:center; gap:8px; }\n  .trophy-gly{ font-size:24px; line-height:1; filter:drop-shadow(0 0 4px rgba(216,178,76,0.5)); }\n  .trophy-cat{ color:${PAL.gold}; font-size:14px; letter-spacing:1px; line-height:1.1; }\n  .trophy-sub{ color:${PAL.cream}; font-size:10px; letter-spacing:1px; opacity:0.7; }\n  .trophy-hero{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:20px; line-height:1.1; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-top:2px; }\n  .trophy-n{ color:${PAL.cream}; font-size:13px; }\n  .trophy-n b{ color:${PAL.gold}; font-size:16px; }\n  .htable{ display:flex; flex-direction:column; gap:0; }\n  .hrow{ display:grid; grid-template-columns:1.6fr repeat(7, 0.7fr); align-items:center; gap:4px; padding:8px 4px; border-bottom:2px solid rgba(0,0,0,0.35); }\n  .hrow.head{ border-bottom:3px solid ${PAL.goldDeep}; }\n  .hrow.head .hc{ color:${PAL.gold}; font-size:11px; letter-spacing:1px; opacity:0.9; }\n  .hc{ color:${PAL.cream}; font-size:13px; text-align:center; }\n  .hc.name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:16px; text-align:left; text-shadow:1px 1px 0 ${PAL.edgeDark}; line-height:1.1; }\n  .hc.zero{ opacity:0.35; }\n  .sb-loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  .sb-err{ color:${PAL.bloodLight}; font-size:12px; text-align:center; padding:6px; }\n  `,\n  campaignId(d2){\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/stats\\/([^\\/?#]+)/);\n      if (_m) return decodeURIComponent(_m[1]);\n    } catch(_){}\n    try { const p = d2.router().params; if (p && p.id) return p.id; } catch(_){}\n    return null;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.load(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ this.goBack(d2); });\n  },\n  goBack(d2){\n    const cid = this.campaignId(d2);\n    const fallback = cid ? ('/campaign/' + cid) : '/';\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  async load(d2){\n    if (this._loading) return;\n    const cid = this.campaignId(d2);\n    if (!cid){ return; }\n    this._cid = cid;\n    this._loading = true;\n    d2.state.loading = true;\n    try {\n      const res = await d2.rush('campaignStats').invoke({ campaignId: cid });\n      const data = (res && res.perHero) ? res : (res && res.data ? res.data : res);\n      d2.state.stats = data || {};\n      d2.state.error = '';\n    } catch(e){\n      d2.state.error = String(e && e.message ? e.message : e);\n      d2.state.stats = null;\n    }\n    d2.state.loading = false;\n    this._loading = false;\n  },\n  render(d2){\n    const cid = this.campaignId(d2);\n    if (cid && cid !== this._cid){ this.load(d2); }\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n    const head = `${back}<div class=\"sb-head\"><div class=\"sb-title\">Estatísticas</div></div>`;\n    if (!cid || (d2.state.loading && !d2.state.stats)){\n      return `<div class=\"sb\">${head}<div class=\"sb-loading\">Carregando…</div></div>`;\n    }\n    const st = d2.state.stats || {};\n    const err = d2.state.error ? `<div class=\"sb-err\">${esc(d2.state.error)}</div>` : '';\n    const t = st.totals || {};\n    const totalsCards = [\n      { n: t.sessions != null ? t.sessions : 0, l:'Sessões' },\n      { n: t.xp != null ? t.xp : 0, l:'XP total' },\n      { n: t.gold != null ? t.gold : 0, l:'Ouro' },\n      { n: t.tesouros != null ? t.tesouros : 0, l:'Tesouros' },\n      { n: t.eventos != null ? t.eventos : 0, l:'Eventos' },\n      { n: t.abates != null ? t.abates : 0, l:'Abates' }\n    ].map(c => `<div class=\"stat\"><div class=\"stat-n\">${esc(c.n)}</div><div class=\"stat-l\">${esc(c.l)}</div></div>`).join('');\n    const totalsSec = `<div class=\"sec-title\">Totais da campanha</div><div class=\"panel totals\">${totalsCards}</div>`;\n    const ld = st.leaders || {};\n    const trophies = LEADERS.map(cfg => {\n      const w = ld[cfg.key] || { heroName:'—', n:0 };\n      return `<div class=\"trophy\">\n        <div class=\"trophy-top\"><span class=\"trophy-gly\">${cfg.glyph}</span>\n          <div><div class=\"trophy-cat\">${esc(cfg.label)}</div><div class=\"trophy-sub\">${esc(cfg.sub)}</div></div></div>\n        <div class=\"trophy-hero\">${esc(w.heroName || '—')}</div>\n        <div class=\"trophy-n\"><b>${esc(w.n || 0)}</b> ${esc(cfg.sub.toLowerCase())}</div>\n      </div>`;\n    }).join('');\n    const leadSec = `<div class=\"sec-title\">Destaques</div><div class=\"lead-grid\">${trophies}</div>`;\n    const heroes = st.perHero || [];\n    const headRow = `<div class=\"hrow head\"><div class=\"hc name\">Herói</div><div class=\"hc\">N20</div><div class=\"hc\">N1</div><div class=\"hc\">KO</div><div class=\"hc\">Morte</div><div class=\"hc\">Lvl+</div><div class=\"hc\">Abates</div><div class=\"hc\">Ses.</div></div>`;\n    const cell = (v) => `<div class=\"hc${(!v) ? ' zero' : ''}\">${esc(v || 0)}</div>`;\n    const heroRows = heroes.length\n      ? heroes.map(h => `<div class=\"hrow\"><div class=\"hc name\">${esc(h.heroName || '—')}</div>${cell(h.nat20)}${cell(h.nat1)}${cell(h.ko)}${cell(h.morte)}${cell(h.levelup)}${cell(h.frags)}${cell(h.sessions)}</div>`).join('')\n      : `<div class=\"hc\" style=\"padding:14px;opacity:0.6\">Sem eventos registrados</div>`;\n    const tableSec = `<div class=\"sec-title\">Por herói</div><div class=\"panel htable\">${headRow}${heroRows}</div>`;\n    return `<div class=\"sb\">${head}${err}${totalsSec}${leadSec}${tableSec}</div>`;\n  }\n}"}}}}}}}},{"_id":"6a34040d23b312eaa4d382a1","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"HeroSheet","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nconst FALLBACK_CAMPAIGN = '6a32e35ace56858f8793fe26';\nconst FALLBACK_HERO = '6a32e361ad12b9d7913cc070';\nconst DRIVE_REF = 'cronicaDrive';\nconst ATTRS = [\n  ['forca','Força'],['destreza','Destreza'],['constituicao','Constituição'],\n  ['inteligencia','Inteligência'],['sabedoria','Sabedoria'],['carisma','Carisma']\n];\n// 18 D&D 5e skills (PT) -> attribute key\nconst SKILLS = [\n  ['Atletismo','forca'],\n  ['Acrobacia','destreza'],['Furtividade','destreza'],['Prestidigitação','destreza'],\n  ['Arcanismo','inteligencia'],['História','inteligencia'],['Investigação','inteligencia'],['Natureza','inteligencia'],['Religião','inteligencia'],\n  ['Adestrar Animais','sabedoria'],['Intuição','sabedoria'],['Medicina','sabedoria'],['Percepção','sabedoria'],['Sobrevivência','sabedoria'],\n  ['Atuação','carisma'],['Enganação','carisma'],['Intimidação','carisma'],['Persuasão','carisma']\n];\nconst ATTR_ABBR = { forca:'FOR', destreza:'DES', constituicao:'CON', inteligencia:'INT', sabedoria:'SAB', carisma:'CAR' };\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;'); }\nfunction modNum(v){ return Math.floor(((Number(v)||0)-10)/2); }\nfunction mod(v){ const m = modNum(v); return (m>=0?'+':'')+m; }\nfunction sign(n){ return (n>=0?'+':'')+n; }\n// normalize hero.pericias to array of {name, level}. Tolerates legacy array of strings.\nfunction normPericias(arr){\n  if (!Array.isArray(arr)) return [];\n  return arr.map(p => {\n    if (p && typeof p === 'object') return { name:String(p.name||''), level: Number(p.level)||0 };\n    return { name:String(p||''), level:1 };\n  }).filter(p=>p.name);\n}\nfunction periciaLevel(list, name){ const f = (list||[]).find(p=>p.name===name); return f ? (Number(f.level)||0) : 0; }\n// ---- XP / level (D&D 5e) ----\nconst XP_LEVELS = [0,300,900,2700,6500,14000,23000,34000,48000,64000,85000,100000,120000,140000,165000,195000,225000,265000,305000,355000];\nfunction levelFromXp(xp){ const v = Number(xp)||0; let L = 1; for(let i=1;i<=20;i++){ if(XP_LEVELS[i-1] <= v) L = i; } return L; }\nfunction xpProgress(xp){\n  const v = Number(xp)||0;\n  const L = levelFromXp(v);\n  if(L >= 20) return { level:L, cur:v, base:XP_LEVELS[19], next:null, pct:1, into:0, span:0 };\n  const base = XP_LEVELS[L-1], next = XP_LEVELS[L];\n  const into = v - base, span = next - base;\n  const pct = span > 0 ? Math.max(0, Math.min(1, into/span)) : 0;\n  return { level:L, cur:v, base, next, pct, into, span };\n}\nfunction fmtXp(n){ return String(Number(n)||0); }\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:14px; padding:6px; font-family:'Alegreya',sans-serif; color:${PAL.cream}; background:#1c140d; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .panel{ box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .sec-ttl{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:2px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-bottom:10px; }\n  /* header */\n  .hdr{ display:flex; gap:14px; align-items:center; }\n  .ava{ flex:0 0 auto; width:84px; height:84px; border-radius:8px; border:3px solid ${PAL.goldDeep};\n    background:linear-gradient(180deg,${PAL.stone2},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center;\n    overflow:hidden; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.12), 0 4px 8px rgba(0,0,0,0.5); }\n  .ava img{ width:100%; height:100%; object-fit:cover; }\n  .ava .ini{ font-family:'Jacquard 24',serif; font-size:36px; color:${PAL.goldLight}; }\n  .hcol{ display:flex; flex-direction:column; gap:5px; min-width:0; }\n  .hname{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:30px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1.05; }\n  .hmeta{ color:${PAL.cream}; font-size:14px; opacity:0.9; }\n  .pill{ align-self:flex-start; font-size:11px; letter-spacing:1px; padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .pill.morto{ color:${PAL.bloodLight}; }\n  /* xp / level */\n  .xpwrap{ display:flex; flex-direction:column; gap:8px; }\n  .xphead{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap; }\n  .xplvl{ font-family:'Jacquard 24','Alegreya',serif; font-size:24px; color:${PAL.goldLight}; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1; }\n  .xpnums{ font-size:13px; color:${PAL.cream}; opacity:0.92; letter-spacing:0.5px; }\n  .xpnums b{ color:${PAL.goldLight}; font-weight:600; }\n  .xpbar{ position:relative; height:18px; border-radius:4px; overflow:hidden;\n    background:linear-gradient(180deg,#1a120a,#0f0a06); border:3px solid ${PAL.edgeDark};\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.6); }\n  .xpfill{ position:absolute; left:0; top:0; bottom:0;\n    background:linear-gradient(180deg,${PAL.goldLight} 0%,${PAL.gold} 45%,${PAL.goldDeep} 100%);\n    box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), 0 0 8px rgba(216,178,76,0.55);\n    transition:width 280ms ease; }\n  .xppct{ position:absolute; right:6px; top:50%; transform:translateY(-50%); font-size:11px; letter-spacing:1px;\n    color:${PAL.edgeDark}; text-shadow:0 1px 0 rgba(255,255,255,0.35); font-weight:600; }\n  /* attributes */\n  .attrs{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }\n  .attr{ box-sizing:border-box; padding:10px 6px; border-radius:6px; text-align:center;\n    background:linear-gradient(180deg,${PAL.stone2},#2e2418); border:3px solid ${PAL.edgeDark};\n    box-shadow: inset 2px 2px 0 ${PAL.edgeLight}, inset -2px -2px 0 ${PAL.edgeDark}; }\n  .attr .lbl{ font-size:10px; letter-spacing:1px; color:${PAL.gold}; }\n  .attr .val{ font-size:26px; color:${PAL.cream}; line-height:1.1; font-weight:600; }\n  .attr .mod{ font-size:13px; color:${PAL.goldLight}; }\n  /* combat */\n  .combat{ display:flex; gap:12px; }\n  .stat{ flex:1; text-align:center; box-sizing:border-box; padding:12px 6px; border-radius:6px;\n    background:linear-gradient(180deg,${PAL.stone2},#2e2418); border:3px solid ${PAL.edgeDark};\n    box-shadow: inset 2px 2px 0 ${PAL.edgeLight}, inset -2px -2px 0 ${PAL.edgeDark}; }\n  .stat .lbl{ font-size:11px; letter-spacing:1px; color:${PAL.gold}; }\n  .stat .big{ font-size:26px; color:${PAL.goldLight}; line-height:1.2; font-weight:600; }\n  .hpbox{ display:flex; align-items:center; justify-content:center; gap:10px; }\n  .hpbtn{ flex:0 0 auto; width:40px; height:40px; }\n  .hpval{ min-width:74px; }\n  .hpamt{ display:flex; align-items:center; justify-content:center; gap:6px; margin-top:10px; }\n  .hpamt label{ font-size:10px; letter-spacing:1px; color:${PAL.gold}; }\n  .root input.hpamtin{ box-sizing:border-box; width:64px; text-align:center; background:#241a10; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:15px; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:5px 6px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .root input.hpamtin:focus{ border-color:${PAL.goldDeep}; }\n  /* death saves */\n  .death{ display:flex; flex-direction:column; gap:12px; }\n  .drow{ display:flex; align-items:center; gap:10px; }\n  .drow .dlbl{ flex:0 0 96px; font-size:12px; letter-spacing:1px; color:${PAL.gold}; }\n  .dcircs{ display:flex; gap:8px; }\n  .dcirc{ width:26px; height:26px; border-radius:50%; cursor:pointer; border:2px solid ${PAL.edgeDark};\n    display:flex; align-items:center; justify-content:center; font-size:14px; line-height:1; font-weight:600;\n    background:rgba(0,0,0,0.35); color:transparent; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08); transition:all 120ms ease; }\n  .dcirc.succ.on{ background:${PAL.gold}; border-color:${PAL.goldLight}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 0 6px rgba(216,178,76,0.6); }\n  .dcirc.fail.on{ background:${PAL.blood}; border-color:${PAL.bloodLight}; color:${PAL.cream}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.2), 0 0 6px rgba(156,51,32,0.7); }\n  .dbadge{ align-self:flex-start; font-size:12px; letter-spacing:1px; padding:4px 12px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .dbadge.estab{ background:${PAL.edgeDark}; color:${PAL.goldLight}; }\n  .dbadge.morto{ background:${PAL.blood}; color:${PAL.cream}; border-color:${PAL.bloodLight}; }\n  /* skills (view) */\n  .sklist{ display:flex; flex-direction:column; gap:6px; }\n  .skrow{ display:flex; align-items:center; gap:10px; padding:6px 10px; border-radius:4px; background:rgba(0,0,0,0.22); border-left:3px solid ${PAL.goldDeep}; }\n  .skrow .dots{ flex:0 0 auto; color:${PAL.goldLight}; font-size:14px; letter-spacing:1px; width:26px; }\n  .skrow .skn{ flex:1; font-size:13px; color:${PAL.cream}; }\n  .skrow .ska{ font-size:10px; letter-spacing:1px; color:${PAL.gold}; opacity:0.85; }\n  .skrow .skm{ flex:0 0 auto; font-size:15px; color:${PAL.goldLight}; font-weight:600; min-width:30px; text-align:right; }\n  /* skills (edit) */\n  .skedit{ display:flex; flex-direction:column; gap:5px; }\n  .sker{ display:flex; align-items:center; gap:10px; padding:5px 8px; border-radius:4px; background:rgba(0,0,0,0.18); }\n  .sker .dotg{ flex:0 0 auto; display:flex; gap:7px; }\n  .dot{ width:18px; height:18px; border-radius:50%; cursor:pointer; border:2px solid ${PAL.goldDeep};\n    background:rgba(0,0,0,0.35); box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08); transition:background 120ms ease; }\n  .dot.on{ background:${PAL.gold}; border-color:${PAL.goldLight}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 0 5px rgba(216,178,76,0.6); }\n  .dot.off{ opacity:0.5; }\n  .sker .skn{ flex:1; font-size:13px; color:${PAL.cream}; }\n  .sker .ska{ flex:0 0 auto; font-size:10px; letter-spacing:1px; color:${PAL.gold}; width:34px; text-align:right; }\n  .skleg{ font-size:11px; color:${PAL.cream}; opacity:0.7; margin-bottom:8px; }\n  /* chips + lists */\n  .eqlist{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }\n  .eqlist li{ font-size:13px; color:${PAL.cream}; padding:6px 10px; border-radius:4px; background:rgba(0,0,0,0.25); border-left:3px solid ${PAL.goldDeep}; }\n  .eqedit{ display:flex; flex-direction:column; gap:6px; }\n  .eqitem{ display:flex; align-items:center; gap:8px; padding:5px 8px; border-radius:4px; background:rgba(0,0,0,0.22); border-left:3px solid ${PAL.goldDeep}; }\n  .eqitem .eqn{ flex:1; font-size:13px; color:${PAL.cream}; }\n  .eqdel{ flex:0 0 auto; cursor:pointer; color:${PAL.bloodLight}; background:none; border:2px solid ${PAL.blood}; border-radius:4px; width:26px; height:26px; font-size:14px; line-height:1; font-family:'Alegreya',sans-serif; }\n  .eqdel:hover{ background:rgba(156,51,32,0.25); }\n  .eqadd{ display:flex; gap:8px; align-items:stretch; margin-top:4px; }\n  .eqadd input{ flex:1; }\n  .eqaddbtn{ flex:0 0 auto; width:110px; }\n  .photorow{ display:flex; gap:10px; align-items:center; margin-bottom:10px; }\n  .photoprev{ width:64px; height:64px; flex:0 0 auto; border-radius:6px; border:3px solid ${PAL.goldDeep}; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone2},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center; }\n  .photoprev img{ width:100%; height:100%; object-fit:cover; }\n  .photoprev .ini{ font-family:'Jacquard 24',serif; font-size:24px; color:${PAL.goldLight}; }\n  .photoctl{ flex:1; display:flex; flex-direction:column; gap:6px; }\n  .uploadbtn{ height:38px; }\n  .story{ font-size:14px; line-height:1.5; color:${PAL.cream}; white-space:pre-wrap; }\n  .empty{ color:${PAL.cream}; opacity:0.6; font-size:13px; }\n  .loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  .btnrow{ display:flex; gap:10px; }\n  .btnrow > *{ flex:1; }\n  /* edit inputs */\n  .root input.gf-input, .root textarea.gf-input{ box-sizing:border-box; width:100%; background:#241a10; color:#ece0c4;\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid #1c140d; border-radius:4px; padding:8px 10px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; }\n  .root input.gf-input:focus, .root textarea.gf-input:focus{ border-color:#c8932f; }\n  .root input.gf-input::placeholder, .root textarea.gf-input::placeholder{ color:#7a6a4a; }\n  .root textarea.gf-input{ min-height:90px; resize:vertical; }\n  .frow{ display:flex; flex-direction:column; gap:4px; margin-bottom:10px; }\n  .frow > label{ font-size:11px; letter-spacing:1px; color:${PAL.gold}; }\n  .fgrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }\n  .fgrid3{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px; }\n  .fgrid .frow, .fgrid3 .frow{ margin-bottom:0; }\n  .fileinput{ display:none; }\n  `,\n  mounted(d2){\n    const self = this;\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.load(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ self.goBack(d2, d2.state.hero); });\n    // styled text inputs -> edit draft (sessionLogForm store broken; use d2.state)\n    d2.on('.gf-input','input',(e)=>{\n      const f = e.target && e.target.getAttribute('data-f');\n      if(!f) return;\n      const next = Object.assign({}, d2.state.draft||{});\n      next[f] = e.target.value;\n      d2.state.draft = next;\n    });\n    // HP amount input -> d2.state WITHOUT re-render (avoid cursor jump); blank = default 1\n    d2.on('.hpamtin','input',(e)=>{ d2.state.hpAmt = e.target.value; });\n    // death save circle clicks\n    d2.on('.dcirc','click',(e)=>{\n      const kind = e.target.getAttribute('data-dk');\n      const n = Number(e.target.getAttribute('data-dn'));\n      if(!kind || !n) return;\n      self.setDeathSave(d2, d2.state.hero, kind, n);\n    });\n    // skill dot toggles (edit)\n    d2.on('.dot','click',(e)=>{\n      const name = e.target.getAttribute('data-sk');\n      const slot = Number(e.target.getAttribute('data-slot')); // 1=prof, 2=spec\n      if(!name) return;\n      self.toggleSkill(d2, name, slot);\n    });\n    // equipment delete\n    d2.on('.eqdel','click',(e)=>{\n      const i = Number(e.target.getAttribute('data-i'));\n      const d = Object.assign({}, d2.state.draft||{});\n      const list = (d.equipamento||[]).slice();\n      list.splice(i,1);\n      d.equipamento = list;\n      d2.state.draft = d;\n    });\n    // equipment add\n    d2.on('.eqaddbtn','click',()=>{ self.addEquip(d2); });\n    d2.on('.eqaddinput','keydown',(e)=>{ if(e.key==='Enter'){ e.preventDefault(); self.addEquip(d2); } });\n    // photo upload via hidden file input\n    d2.on('.fileinput','change',(e)=>{ self.onFilePicked(d2, e.target); });\n  },\n  heroId(d2){\n    let id = null;\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/hero\\/([^\\/?#]+)/);\n      if (_m) id = decodeURIComponent(_m[1]);\n    } catch(_){}\n    if (!id) { try { const p = d2.router().params; if (p && p.id) id = p.id; } catch(_){} }\n    return id;\n  },\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  fallbackHero(d2){\n    try {\n      const h = d2.data('heroesDataset');\n      if (!h) return null;\n      const recs = h.records() || [];\n      if (!recs.length) return null;\n      const inCamp = recs.filter(r => String(r.campaignId) === String(FALLBACK_CAMPAIGN));\n      const pick = (inCamp.length ? inCamp : recs).find(r => String(r._id) === String(FALLBACK_HERO)) || (inCamp[0] || recs[0]);\n      return pick || null;\n    } catch(_){ return null; }\n  },\n  async load(d2){\n    if (this._loading) return;\n    const id = this.heroId(d2);\n    if (id){\n      this._loading = true;\n      d2.state.loading = true;\n      try {\n        const res = await d2.rush('heroDetail').invoke({ heroId: id });\n        const data = (res && res.data) ? res.data : res;\n        d2.state.hero = (data && data.hero) ? data.hero : null;\n      } catch(e){\n        d2.state.error = String(e && e.message ? e.message : e);\n      }\n      d2.state.loading = false;\n      this._loading = false;\n      d2.refresh();\n    } else {\n      const fb = this.fallbackHero(d2);\n      if (fb) d2.state.hero = fb;\n      d2.state.usingFallback = true;\n    }\n  },\n  startEdit(d2, hero){\n    const d = {};\n    ['name','race','klass','level','xp','status','portrait','forca','destreza','constituicao','inteligencia','sabedoria','carisma','hpMax','hpAtual','ca','historia'].forEach(k=>{ d[k] = hero[k]==null ? '' : hero[k]; });\n    d.pericias = normPericias(hero.pericias);\n    d.equipamento = (hero.equipamento||[]).slice();\n    d2.state.draft = d;\n    d2.state.editing = true;\n    d2.refresh();\n  },\n  toggleSkill(d2, name, slot){\n    const d = Object.assign({}, d2.state.draft||{});\n    const list = (d.pericias||[]).map(p=>Object.assign({},p));\n    let cur = periciaLevel(list, name);\n    let next;\n    if (slot===1){ // proficiency dot: 0<->1; if currently 2 -> drop to 0\n      next = (cur>=1) ? 0 : 1;\n    } else { // specialization dot: toggle 2; spec implies prof\n      next = (cur===2) ? 1 : 2;\n    }\n    const idx = list.findIndex(p=>p.name===name);\n    if (next===0){ if(idx>=0) list.splice(idx,1); }\n    else if (idx>=0){ list[idx].level = next; }\n    else { list.push({ name, level: next }); }\n    d.pericias = list;\n    d2.state.draft = d;\n    d2.refresh();\n  },\n  addEquip(d2){\n    const inp = d2.$('.eqaddinput');\n    const val = inp && inp.value ? inp.value.trim() : '';\n    if (!val) return;\n    const d = Object.assign({}, d2.state.draft||{});\n    const list = (d.equipamento||[]).slice();\n    list.push(val);\n    d.equipamento = list;\n    d2.state.draft = d;\n    if (inp) inp.value = '';\n    d2.refresh();\n  },\n  async onFilePicked(d2, input){\n    const files = input && input.files;\n    if (!files || !files.length) return;\n    d2.state.uploading = true;\n    d2.refresh();\n    try {\n      const out = await d2.drive(DRIVE_REF).uploadFromFiles([files[0]]);\n      const url = out && out[0] && (out[0].fileUrl || (out[0].params && out[0].params.fileUrl));\n      if (url){\n        const d = Object.assign({}, d2.state.draft||{});\n        d.portrait = url;\n        d2.state.draft = d;\n      } else {\n        d2.state.error = 'Upload sem URL';\n      }\n    } catch(e){\n      d2.state.error = 'Falha no upload da imagem';\n    }\n    d2.state.uploading = false;\n    if (input) input.value = '';\n    d2.refresh();\n  },\n  hpAmount(d2){\n    // amount input value lives in d2.state (not re-rendered on input). Default 1 when empty/invalid.\n    const raw = d2.state.hpAmt;\n    let n = Math.floor(Math.abs(Number(raw)));\n    if (!n || isNaN(n)) n = 1;\n    return n;\n  },\n  async adjustHp(d2, hero, delta){\n    // read the live base from state (not the stale render closure) so rapid clicks compound correctly\n    const base = (d2.state.hero && d2.state.hero._id === hero._id) ? d2.state.hero : hero;\n    const max = Number(base.hpMax);\n    let cur = Number(base.hpAtual);\n    if (isNaN(cur)) cur = 0;\n    let next = cur + delta;\n    if (next < 0) next = 0;\n    if (!isNaN(max) && next > max) next = max;\n    if (next === cur) return;\n    // healing from 0 HP clears death saves\n    const revived = (cur <= 0 && next > 0);\n    const payload = { heroId: base._id, hpAtual: next };\n    let optimistic = Object.assign({}, base, { hpAtual: next });\n    if (revived){\n      payload.deathSucc = 0; payload.deathFail = 0;\n      optimistic = Object.assign(optimistic, { deathSucc: 0, deathFail: 0 });\n    }\n    // optimistic\n    d2.state.hero = optimistic;\n    d2.refresh();\n    try {\n      const res = await d2.rush('updateHero').invoke(payload);\n      const data = (res && res.data) ? res.data : res;\n      // only adopt server hero if user hasn't clicked again in the meantime\n      if (data && data.hero && d2.state.hero && d2.state.hero.hpAtual === next) d2.state.hero = data.hero;\n    } catch(e){\n      d2.state.hero = base;\n      d2.state.error = 'Falha ao atualizar PV';\n      d2.refresh();\n    }\n  },\n  async setDeathSave(d2, hero, kind, n){\n    // kind: 'succ' | 'fail'; n: clicked circle index (1..3). Toggle: clicking the current value drops by 1.\n    const base = (d2.state.hero && d2.state.hero._id === hero._id) ? d2.state.hero : hero;\n    const field = (kind==='succ') ? 'deathSucc' : 'deathFail';\n    let cur = Number(base[field]); if (isNaN(cur)) cur = 0;\n    let next = (n === cur) ? (n - 1) : n;\n    if (next < 0) next = 0; if (next > 3) next = 3;\n    if (next === cur) return;\n    const optimistic = Object.assign({}, base, { [field]: next });\n    d2.state.hero = optimistic;\n    d2.refresh();\n    try {\n      const res = await d2.rush('updateHero').invoke({ heroId: base._id, [field]: next });\n      const data = (res && res.data) ? res.data : res;\n      if (data && data.hero && d2.state.hero && d2.state.hero[field] === next) d2.state.hero = data.hero;\n    } catch(e){\n      d2.state.hero = base;\n      d2.state.error = 'Falha ao atualizar testes de morte';\n      d2.refresh();\n    }\n  },\n  async resetDeath(d2, hero){\n    const base = (d2.state.hero && d2.state.hero._id === hero._id) ? d2.state.hero : hero;\n    if ((Number(base.deathSucc)||0) === 0 && (Number(base.deathFail)||0) === 0) return;\n    const optimistic = Object.assign({}, base, { deathSucc: 0, deathFail: 0 });\n    d2.state.hero = optimistic;\n    d2.refresh();\n    try {\n      const res = await d2.rush('updateHero').invoke({ heroId: base._id, deathSucc: 0, deathFail: 0 });\n      const data = (res && res.data) ? res.data : res;\n      if (data && data.hero && d2.state.hero && (Number(d2.state.hero.deathSucc)||0) === 0 && (Number(d2.state.hero.deathFail)||0) === 0) d2.state.hero = data.hero;\n    } catch(e){\n      d2.state.hero = base;\n      d2.state.error = 'Falha ao resetar testes de morte';\n      d2.refresh();\n    }\n  },\n  async save(d2, hero){\n    const d = d2.state.draft || {};\n    const num = (v) => { const n = Number(v); return isNaN(n) ? undefined : n; };\n    const pericias = (d.pericias||[]).filter(p=>p && p.name && (Number(p.level)||0)>0).map(p=>({ name:p.name, level:Number(p.level) }));\n    const xpVal = num(d.xp);\n    const lvlVal = (xpVal != null) ? levelFromXp(xpVal) : num(d.level);\n    const payload = {\n      heroId: hero._id,\n      name: d.name, race: d.race, klass: d.klass, status: d.status, portrait: d.portrait,\n      historia: d.historia,\n      xp: xpVal, level: lvlVal, forca: num(d.forca), destreza: num(d.destreza), constituicao: num(d.constituicao),\n      inteligencia: num(d.inteligencia), sabedoria: num(d.sabedoria), carisma: num(d.carisma),\n      hpMax: num(d.hpMax), hpAtual: num(d.hpAtual), ca: num(d.ca),\n      pericias: pericias, equipamento: (d.equipamento||[]).slice()\n    };\n    d2.state.saving = true;\n    d2.refresh();\n    try {\n      const res = await d2.rush('updateHero').invoke(payload);\n      const data = (res && res.data) ? res.data : res;\n      if (data && data.hero) d2.state.hero = data.hero;\n      d2.state.editing = false;\n      d2.state.draft = null;\n    } catch(e){\n      d2.state.error = 'Falha ao salvar';\n    }\n    d2.state.saving = false;\n    d2.refresh();\n  },\n  goBack(d2, hero){\n    const cid = hero && hero.campaignId;\n    const fallback = cid ? ('/sessions/' + cid) : '/';\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  render(d2){\n    const self = this;\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n    if (d2.state.loading && !d2.state.hero) return `<div class=\"root\">${back}<div class=\"loading\">Carregando…</div></div>`;\n    let hero = d2.state.hero;\n    if (!hero){\n      const fb = this.fallbackHero(d2);\n      if (fb) hero = fb;\n    }\n    if (!hero) return `<div class=\"root\">${back}<div class=\"empty\">Herói não encontrado</div></div>`;\n\n    if (d2.state.editing) return this.renderEdit(d2, hero);\n\n    // ---- VIEW ----\n    const ini = (hero.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n    const ava = hero.portrait ? `<img src=\"${esc(hero.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(ini)}</span>`;\n    const statusCls = (hero.status==='morto') ? 'pill morto' : 'pill';\n    const prog = xpProgress(hero.xp);\n    const header = `<div class=\"panel hdr\">\n      <div class=\"ava\">${ava}</div>\n      <div class=\"hcol\">\n        <div class=\"hname\">${esc(hero.name||'Herói')}</div>\n        <div class=\"hmeta\">${esc(hero.race||'')} · ${esc(hero.klass||'')} · nível ${prog.level}</div>\n        <span class=\"${statusCls}\">${esc(hero.status||'vivo')}</span>\n      </div>\n    </div>`;\n\n    const pctTxt = Math.round(prog.pct*100) + '%';\n    const xpNums = prog.next==null\n      ? `<span class=\"xpnums\"><b>${fmtXp(prog.cur)}</b> xp · nível máximo</span>`\n      : `<span class=\"xpnums\"><b>${fmtXp(prog.cur)}</b> / ${fmtXp(prog.next)} xp <span style=\"opacity:0.7\">(faltam ${fmtXp(prog.span-prog.into)})</span></span>`;\n    const xpPanel = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Experiência</div>\n      <div class=\"xpwrap\">\n        <div class=\"xphead\">\n          <span class=\"xplvl\">Nível ${prog.level}</span>\n          ${xpNums}\n        </div>\n        <div class=\"xpbar\">\n          <div class=\"xpfill\" style=\"width:${(prog.pct*100).toFixed(1)}%\"></div>\n          <span class=\"xppct\">${pctTxt}</span>\n        </div>\n      </div>\n    </div>`;\n\n    const attrs = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Atributos</div>\n      <div class=\"attrs\">\n        ${ATTRS.map(([k,lbl])=>`<div class=\"attr\"><div class=\"lbl\">${esc(lbl)}</div><div class=\"val\">${esc(hero[k]!=null?hero[k]:'—')}</div><div class=\"mod\">${esc(mod(hero[k]))}</div></div>`).join('')}\n      </div>\n    </div>`;\n\n    const minusBtn = d2.render('cronica-app@GoldenButton', { label:'−', kind:'danger', size:'sm' }, { key:'hpMinus', width:40, height:40, onPress:()=>{ self.adjustHp(d2, hero, -self.hpAmount(d2)); } });\n    const plusBtn = d2.render('cronica-app@GoldenButton', { label:'+', kind:'gold', size:'sm' }, { key:'hpPlus', width:40, height:40, onPress:()=>{ self.adjustHp(d2, hero, +self.hpAmount(d2)); } });\n    const hpAmtVal = (d2.state.hpAmt==null || d2.state.hpAmt==='') ? '' : esc(d2.state.hpAmt);\n    const combat = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Combate</div>\n      <div class=\"combat\">\n        <div class=\"stat\">\n          <div class=\"lbl\">Pontos de Vida</div>\n          <div class=\"hpbox\">\n            ${minusBtn}\n            <div class=\"big hpval\">${esc(hero.hpAtual!=null?hero.hpAtual:'—')} / ${esc(hero.hpMax!=null?hero.hpMax:'—')}</div>\n            ${plusBtn}\n          </div>\n          <div class=\"hpamt\">\n            <label>Quantidade</label>\n            <input class=\"hpamtin\" type=\"number\" min=\"1\" inputmode=\"numeric\" value=\"${hpAmtVal}\" placeholder=\"1\"/>\n          </div>\n        </div>\n        <div class=\"stat\"><div class=\"lbl\">Classe de Armadura</div><div class=\"big\">${esc(hero.ca!=null?hero.ca:'—')}</div></div>\n      </div>\n    </div>`;\n\n    const dSucc = Math.max(0, Math.min(3, Number(hero.deathSucc)||0));\n    const dFail = Math.max(0, Math.min(3, Number(hero.deathFail)||0));\n    const circ = (kind, n, filled) => `<span class=\"dcirc ${kind==='succ'?'succ':'fail'} ${filled?'on':''}\" data-dk=\"${kind}\" data-dn=\"${n}\">${kind==='succ'?'✓':'☠'}</span>`;\n    const succCircs = [1,2,3].map(n=>circ('succ', n, n<=dSucc)).join('');\n    const failCircs = [1,2,3].map(n=>circ('fail', n, n<=dFail)).join('');\n    let deathBadge = '';\n    if (dSucc >= 3) deathBadge = `<span class=\"dbadge estab\">Estabilizado</span>`;\n    else if (dFail >= 3) deathBadge = `<span class=\"dbadge morto\">Morto</span>`;\n    const resetDeathBtn = d2.render('cronica-app@GoldenButton', { label:'Resetar', kind:'danger', ghost:true, size:'sm' }, { key:'deathReset', width:110, onPress:()=>{ self.resetDeath(d2, hero); } });\n    const deathPanel = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Testes de Morte</div>\n      <div class=\"death\">\n        <div class=\"drow\"><span class=\"dlbl\">Sucessos</span><div class=\"dcircs\">${succCircs}</div></div>\n        <div class=\"drow\"><span class=\"dlbl\">Falhas</span><div class=\"dcircs\">${failCircs}</div></div>\n        ${deathBadge ? `<div class=\"drow\">${deathBadge}</div>` : ''}\n        <div class=\"drow\">${resetDeathBtn}</div>\n      </div>\n    </div>`;\n\n    const PROF = 2;\n    const plist = normPericias(hero.pericias);\n    const trained = SKILLS.filter(([name])=>periciaLevel(plist,name)>0);\n    const skillRow = ([name, attrKey]) => {\n      const lvl = periciaLevel(plist, name);\n      const bonus = (lvl===2) ? PROF*2 : PROF;\n      const total = modNum(hero[attrKey]) + bonus;\n      const dots = (lvl===2) ? '●●' : '●';\n      return `<div class=\"skrow\"><span class=\"dots\">${dots}</span><span class=\"skn\">${esc(name)}</span><span class=\"ska\">${ATTR_ABBR[attrKey]}</span><span class=\"skm\">${sign(total)}</span></div>`;\n    };\n    const periciasHtml = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Perícias</div>\n      ${trained.length ? `<div class=\"sklist\">${trained.map(skillRow).join('')}</div>` : `<div class=\"empty\">Nenhuma perícia treinada</div>`}\n    </div>`;\n\n    const equip = (hero.equipamento||[]);\n    const equipHtml = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Equipamento</div>\n      ${equip.length ? `<ul class=\"eqlist\">${equip.map(e=>`<li>${esc(e)}</li>`).join('')}</ul>` : `<div class=\"empty\">Sem equipamento</div>`}\n    </div>`;\n\n    const story = `<div class=\"panel\">\n      <div class=\"sec-ttl\">História</div>\n      ${hero.historia ? `<div class=\"story\">${esc(hero.historia)}</div>` : `<div class=\"empty\">Sem história registrada</div>`}\n    </div>`;\n\n    const editBtn = d2.render('cronica-app@GoldenButton', { label:'Editar', kind:'gold', glyph:'✎' }, { key:'editBtn', style:'height:52px', onPress:()=>{ self.startEdit(d2, hero); } });\n\n    return `<div class=\"root\">${back}${header}${xpPanel}${attrs}${combat}${deathPanel}${periciasHtml}${equipHtml}${story}${editBtn}</div>`;\n  },\n  renderEdit(d2, hero){\n    const self = this;\n    const d = d2.state.draft || {};\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n    const txt = (f, ph) => `<input class=\"gf-input\" data-f=\"${f}\" type=\"text\" value=\"${esc(d[f]!=null?d[f]:'')}\" placeholder=\"${esc(ph||'')}\"/>`;\n    const numf = (f) => `<input class=\"gf-input\" data-f=\"${f}\" type=\"number\" value=\"${esc(d[f]!=null?d[f]:'')}\"/>`;\n    const ta = (f, ph) => `<textarea class=\"gf-input\" data-f=\"${f}\" placeholder=\"${esc(ph||'')}\">${esc(d[f]!=null?d[f]:'')}</textarea>`;\n    const row = (lbl, inner) => `<div class=\"frow\"><label>${esc(lbl)}</label>${inner}</div>`;\n\n    // photo control: preview + upload button + URL field\n    const pini = (d.name||hero.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n    const prev = d.portrait ? `<img src=\"${esc(d.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(pini)}</span>`;\n    const uploadBtn = d2.render('cronica-app@GoldenButton', { label: d2.state.uploading ? 'Enviando…' : 'Enviar foto', kind:'gold', size:'sm', glyph:'⬆' }, { key:'upBtn', class:'uploadbtn', onPress:()=>{ if(!d2.state.uploading){ const fi=d2.$('.fileinput'); if(fi) fi.click(); } } });\n    const photo = `<div class=\"photorow\">\n        <div class=\"photoprev\">${prev}</div>\n        <div class=\"photoctl\">\n          ${uploadBtn}\n          <input class=\"fileinput\" type=\"file\" accept=\"image/*\"/>\n          ${txt('portrait','ou cole a URL da imagem')}\n        </div>\n      </div>`;\n\n    const identity = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Identidade</div>\n      ${row('Nome', txt('name','nome do herói'))}\n      ${photo}\n      <div class=\"fgrid\">\n        ${row('Raça', txt('race'))}\n        ${row('Classe', txt('klass'))}\n      </div>\n      <div class=\"fgrid\">\n        ${row('XP', numf('xp'))}\n        ${row('Status', txt('status','vivo / morto'))}\n      </div>\n      <div class=\"frow\">\n        <label>Nível (derivado do XP)</label>\n        <div class=\"xpnums\" style=\"padding:6px 2px\">Nível <b>${levelFromXp(d.xp)}</b> — calculado a partir do XP ao salvar</div>\n      </div>\n    </div>`;\n\n    const attrs = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Atributos</div>\n      <div class=\"fgrid3\">\n        ${ATTRS.map(([k,lbl])=>row(lbl, numf(k))).join('')}\n      </div>\n    </div>`;\n\n    const combat = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Combate</div>\n      <div class=\"fgrid3\">\n        ${row('HP Atual', numf('hpAtual'))}\n        ${row('HP Máx', numf('hpMax'))}\n        ${row('CA', numf('ca'))}\n      </div>\n    </div>`;\n\n    const plist = d.pericias || [];\n    const skEditRow = ([name, attrKey]) => {\n      const lvl = periciaLevel(plist, name);\n      const d1cls = lvl>=1 ? 'dot on' : 'dot off';\n      const d2cls = lvl>=2 ? 'dot on' : 'dot off';\n      return `<div class=\"sker\">\n        <div class=\"dotg\">\n          <span class=\"${d1cls}\" data-sk=\"${esc(name)}\" data-slot=\"1\" title=\"Proficiente\"></span>\n          <span class=\"${d2cls}\" data-sk=\"${esc(name)}\" data-slot=\"2\" title=\"Especialista\"></span>\n        </div>\n        <span class=\"skn\">${esc(name)}</span>\n        <span class=\"ska\">${ATTR_ABBR[attrKey]}</span>\n      </div>`;\n    };\n    const skills = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Perícias</div>\n      <div class=\"skleg\">● proficiente · ●● especialista — clique nas bolinhas</div>\n      <div class=\"skedit\">${SKILLS.map(skEditRow).join('')}</div>\n    </div>`;\n\n    const eqlist = (d.equipamento||[]);\n    const addBtn = d2.render('cronica-app@GoldenButton', { label:'Adicionar', kind:'gold', size:'sm', glyph:'+' }, { key:'eqAdd', width:120, onPress:()=>{ self.addEquip(d2); } });\n    const equip = `<div class=\"panel\">\n      <div class=\"sec-ttl\">Equipamento</div>\n      <div class=\"eqedit\">\n        ${eqlist.length ? eqlist.map((it,i)=>`<div class=\"eqitem\"><span class=\"eqn\">${esc(it)}</span><button class=\"eqdel\" data-i=\"${i}\" title=\"Remover\">×</button></div>`).join('') : `<div class=\"empty\">Sem itens</div>`}\n      </div>\n      <div class=\"eqadd\">\n        <input class=\"gf-input eqaddinput\" type=\"text\" placeholder=\"novo item\"/>\n        ${addBtn}\n      </div>\n    </div>`;\n\n    const story = `<div class=\"panel\">\n      <div class=\"sec-ttl\">História</div>\n      ${ta('historia','história do herói')}\n    </div>`;\n\n    const saveBtn = d2.render('cronica-app@GoldenButton', { label: d2.state.saving ? 'Salvando…' : 'Salvar', kind:'gold', glyph:'✔' }, { key:'saveBtn', style:'height:52px', onPress:()=>{ if(!d2.state.saving) self.save(d2, hero); } });\n    const cancelBtn = d2.render('cronica-app@GoldenButton', { label:'Cancelar', kind:'danger', ghost:true }, { key:'cancelBtn', style:'height:52px', onPress:()=>{ d2.state.editing=false; d2.state.draft=null; d2.refresh(); } });\n\n    return `<div class=\"root\">${back}${identity}${attrs}${combat}${skills}${equip}${story}<div class=\"btnrow\">${cancelBtn}${saveBtn}</div></div>`;\n  }\n}"}},"heroesDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"heroesDataset"}}}}}}}}}},{"_id":"6a32f35329de3b76dd7e8dac","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"SessionLog","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = {\n  bg:'#1c140d', panel:'#241a10', panel2:'#2c2013', stone:'#3a2c1a',\n  gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a',\n  blood:'#9c3320', bloodLight:'#c4543a', cream:'#ece0c4', edgeDark:'#120c07',\n  muted:'#a08a5e', edgeLight:'#6a5436', inputBg:'#241a10'\n};\nconst GB = 'cronica-app@GoldenButton';\nconst GF = 'cronica-app@GoldenField';\nconst GA = 'cronica-app@GoldenAutocomplete';\nconst GS = 'cronica-app@GoldenSelect';\n\nconst ACTIONS = [\n  { type:'KO',       label:'KO',       glyph:'✖', kind:'danger' },\n  { type:'morte',    label:'Morte',    glyph:'☠', kind:'danger' },\n  { type:'level-up', label:'Level up', glyph:'★', kind:'gold'   }\n];\nconst ROLL_TYPES = [\n  { v:'ataque',  l:'Ataque'  },\n  { v:'perícia', l:'Perícia' },\n  { v:'social',  l:'Social'  },\n  { v:'outro',   l:'Outro'   }\n];\nconst SCORE = [\n  { type:'nat20',    label:'Nat 20'   },\n  { type:'nat1',     label:'Nat 1'    },\n  { type:'KO',       label:'KO'       },\n  { type:'morte',    label:'Morte'    },\n  { type:'level-up', label:'Level up' }\n];\nconst KINDS = [\n  { v:'item',  l:'Item'  },\n  { v:'ouro',  l:'Ouro'  },\n  { v:'gema',  l:'Gema'  },\n  { v:'outro', l:'Outro' }\n];\n\nfunction esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;'); }\nfunction hhmm(iso){\n  if(!iso) return '--:--';\n  const d = new Date(iso);\n  if(isNaN(d.getTime())) return '--:--';\n  const h = String(d.getHours()).padStart(2,'0');\n  const m = String(d.getMinutes()).padStart(2,'0');\n  return h+':'+m;\n}\nfunction typeLabel(t){\n  const m = SCORE.find(s=>s.type===t);\n  return m ? m.label : (t||'evento');\n}\n// ---- XP / level (D&D 5e) — cumulative thresholds to REACH each level ----\nconst XP_LEVELS = [0,300,900,2700,6500,14000,23000,34000,48000,64000,85000,100000,120000,140000,165000,195000,225000,265000,305000,355000];\nfunction levelFromXp(xp){ const v = Number(xp)||0; let L = 1; for(let i=1;i<=20;i++){ if(XP_LEVELS[i-1] <= v) L = i; } return L; }\n\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .root{ width:100%; height:100%; min-height:100%; overflow-y:auto; box-sizing:border-box; font-family:'Alegreya',sans-serif;\n    color:${PAL.cream}; background:#1c140d; padding:2px 0 40px; display:flex; flex-direction:column; gap:18px; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .ttl{ font-family:'Jacquard 24',serif; color:${PAL.goldLight}; line-height:1.05;\n    text-shadow:2px 2px 0 ${PAL.edgeDark}; }\n  .sec-ttl{ font-family:'Jacquard 24',serif; font-size:26px; color:${PAL.gold};\n    text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1.35; padding-bottom:6px; margin:0 0 10px; display:block; }\n  .panel{ background:${PAL.panel}; border:3px solid ${PAL.edgeDark}; border-radius:8px;\n    padding:14px; box-shadow: inset 2px 2px 0 rgba(255,255,255,0.04), inset -2px -2px 0 rgba(0,0,0,0.5); }\n  .hdr{ display:flex; align-items:flex-start; gap:12px; justify-content:space-between; }\n  .hdr-l{ display:flex; flex-direction:column; gap:6px; }\n  .pill{ align-self:flex-start; font-size:12px; letter-spacing:1px; padding:3px 10px; border-radius:20px;\n    border:2px solid ${PAL.edgeDark}; color:${PAL.edgeDark}; }\n  .pill.open{ background:${PAL.goldLight}; }\n  .pill.closed{ background:${PAL.muted}; }\n  .ig-line{ font-size:13px; color:${PAL.gold}; letter-spacing:0.5px; }\n  .ro-prose{ font-size:15px; line-height:1.5; color:${PAL.cream}; white-space:pre-wrap; }\n  .ro-empty{ font-size:13px; color:${PAL.muted}; font-style:italic; }\n  .stat-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }\n  .stat-cell{ background:${PAL.panel2}; border:2px solid ${PAL.stone}; border-radius:8px; padding:10px 8px;\n    display:flex; flex-direction:column; align-items:center; gap:4px; }\n  .stat-cell .n{ font-family:'Jacquard 24',serif; font-size:28px; color:${PAL.goldLight}; line-height:1; }\n  .stat-cell .l{ font-size:11px; color:${PAL.muted}; text-align:center; }\n  .htable{ display:flex; flex-direction:column; gap:0; overflow-x:auto; }\n  .hrow{ display:grid; grid-template-columns:1.6fr repeat(6, 0.7fr); align-items:center; gap:4px; padding:8px 4px; border-bottom:2px solid rgba(0,0,0,0.35); min-width:420px; }\n  .hrow.head{ border-bottom:3px solid ${PAL.goldDeep}; }\n  .hrow.head .hc{ color:${PAL.gold}; font-size:11px; letter-spacing:1px; opacity:0.9; }\n  .hc{ color:${PAL.cream}; font-size:13px; text-align:center; }\n  .hc.name{ color:${PAL.goldLight}; font-family:'Jacquard 24',serif; font-size:16px; text-align:left; text-shadow:1px 1px 0 ${PAL.edgeDark}; line-height:1.1; }\n  .hc.zero{ opacity:0.35; }\n  .ro-feed .ev{ cursor:default; }\n  .ro-list{ display:flex; flex-direction:column; gap:8px; }\n  .back-slot{ width:96px; height:40px; flex:0 0 auto; }\n  .tray{ display:flex; gap:10px; overflow-x:auto; padding:4px 2px 10px; }\n  .hero{ flex:0 0 auto; width:108px; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:6px;\n    padding:10px 8px; border-radius:8px; border:2px solid ${PAL.stone}; background:${PAL.panel2};\n    transition:border-color 140ms ease, box-shadow 140ms ease, transform 120ms ease; }\n  .hero.sel{ border-color:${PAL.gold}; box-shadow:0 0 12px rgba(216,178,76,0.6), inset 0 0 6px rgba(216,178,76,0.25); transform:translateY(-2px); }\n  .ava{ width:62px; height:62px; border-radius:8px; border:3px solid ${PAL.goldDeep};\n    background:linear-gradient(180deg,${PAL.stone},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center;\n    overflow:hidden; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.12); }\n  .ava img{ width:100%; height:100%; object-fit:cover; }\n  .ava .ini{ font-family:'Jacquard 24',serif; font-size:26px; color:${PAL.goldLight}; }\n  .hero .nm{ font-size:13px; color:${PAL.cream}; text-align:center; line-height:1.1; }\n  .hero .meta{ font-size:10px; color:${PAL.muted}; text-align:center; line-height:1.1; }\n  .hero-ficha{ margin-top:2px; cursor:pointer; font-family:'Alegreya',sans-serif; font-size:10px; letter-spacing:0.5px;\n    color:${PAL.edgeDark}; background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:2px solid ${PAL.edgeDark}; border-radius:3px; padding:3px 8px;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .hero-ficha:hover{ filter:brightness(1.12); }\n  .row-actions{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }\n  .act-slot{ height:40px; }\n  .hint{ font-size:12px; color:${PAL.muted}; margin-top:8px; }\n  .free{ display:flex; flex-direction:column; gap:10px; }\n  .free-row{ display:flex; gap:10px; align-items:flex-end; }\n  .fld-slot{ flex:1 1 auto; }\n  .ac-slot{ flex:1 1 auto; min-height:42px; }\n  .selct{ flex:1 1 auto; }\n  .selct label{ display:block; font-size:13px; color:${PAL.gold}; letter-spacing:1px; margin-bottom:5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .root input.gf-input{ width:100%; box-sizing:border-box; background:${PAL.inputBg}; color:${PAL.cream};\n    font-family:'Alegreya',sans-serif; font-size:14px; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.5); outline:none; transition:border-color 160ms ease; }\n  .root input.gf-input:focus{ border-color:${PAL.goldDeep}; }\n  .root input.gf-input::placeholder{ color:rgba(236,224,196,0.4); }\n  .btn-slot{ height:42px; }\n  .btn-slot.add{ width:100%; }\n  .score{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }\n  .score-cell{ background:${PAL.panel2}; border:2px solid ${PAL.stone}; border-radius:8px; padding:10px 8px;\n    display:flex; flex-direction:column; align-items:center; gap:4px; }\n  .score-cell .n{ font-family:'Jacquard 24',serif; font-size:30px; color:${PAL.goldLight}; line-height:1; }\n  .score-cell .l{ font-size:12px; color:${PAL.muted}; }\n  .feed{ display:flex; flex-direction:column; gap:8px; }\n  .ev{ display:flex; align-items:center; gap:10px; background:${PAL.panel2}; border:2px solid ${PAL.stone};\n    border-radius:6px; padding:8px 10px; }\n  .ev .txt{ flex:1 1 auto; font-size:13px; line-height:1.25; }\n  .ev .txt .t{ color:${PAL.muted}; }\n  .ev .txt .ty{ color:${PAL.goldLight}; }\n  .ev .x-slot{ width:42px; height:34px; flex:0 0 auto; }\n  .enc{ background:${PAL.panel2}; border:2px solid ${PAL.stone}; border-radius:8px; padding:12px; display:flex; flex-direction:column; gap:8px; }\n  .enc .en-name{ font-size:17px; color:${PAL.goldLight}; }\n  .enc .en-meta{ font-size:12px; color:${PAL.muted}; }\n  .enemies{ display:flex; flex-direction:column; gap:3px; font-size:13px; }\n  .pres{ display:flex; flex-wrap:wrap; gap:8px; padding:4px 2px 2px; }\n  .pres-pill{ flex:0 0 auto; cursor:pointer; display:flex; align-items:center; gap:7px;\n    padding:6px 12px 6px 8px; border-radius:22px; border:2px solid ${PAL.stone}; background:${PAL.panel2};\n    transition:border-color 140ms ease, box-shadow 140ms ease, opacity 140ms ease, filter 140ms ease; }\n  .pres-pill .pava{ width:30px; height:30px; border-radius:50%; border:2px solid ${PAL.goldDeep}; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center; flex:0 0 auto; }\n  .pres-pill .pava img{ width:100%; height:100%; object-fit:cover; }\n  .pres-pill .pava .ini{ font-family:'Jacquard 24',serif; font-size:14px; color:${PAL.goldLight}; }\n  .pres-pill .pnm{ font-size:13px; color:${PAL.cream}; }\n  .pres-pill .pmark{ font-size:14px; width:16px; text-align:center; }\n  .pres-pill.on{ border-color:${PAL.gold}; box-shadow:0 0 10px rgba(216,178,76,0.45), inset 0 0 5px rgba(216,178,76,0.2); }\n  .pres-pill.on .pmark{ color:${PAL.goldLight}; }\n  .pres-pill.off{ opacity:0.5; filter:grayscale(0.6); }\n  .pres-pill.off .pmark{ color:${PAL.muted}; }\n  .enemy-kill{ display:flex; align-items:center; gap:8px; font-size:13px; flex-wrap:wrap; }\n  .enemy-kill .ek-name{ flex:1 1 auto; min-width:90px; }\n  .enemy-kill .ek-by{ color:${PAL.muted}; font-size:11px; letter-spacing:0.5px; }\n  .enc-actions{ display:flex; gap:8px; }\n  .enc-actions .es{ flex:1 1 0; height:38px; }\n  .enemy-builder{ display:flex; flex-direction:column; gap:8px; border-top:2px dashed ${PAL.stone}; padding-top:10px; }\n  .eb-row{ display:flex; gap:8px; align-items:flex-end; }\n  .eb-list{ display:flex; flex-direction:column; gap:4px; }\n  .eb-item{ display:flex; align-items:center; gap:8px; font-size:13px; background:${PAL.panel2}; border:1px solid ${PAL.stone}; border-radius:5px; padding:5px 8px; }\n  .eb-item .rm-slot{ width:34px; height:28px; flex:0 0 auto; margin-left:auto; }\n  .trs{ display:flex; flex-direction:column; gap:8px; }\n  .trs-item{ display:flex; align-items:center; gap:10px; background:${PAL.panel2}; border:2px solid ${PAL.stone};\n    border-radius:6px; padding:8px 10px; }\n  .trs-item .ti{ flex:1 1 auto; font-size:13px; line-height:1.25; }\n  .trs-item .ti .nm{ color:${PAL.goldLight}; font-size:15px; }\n  .trs-item .ti .meta{ color:${PAL.muted}; font-size:12px; }\n  .trs-item .x-slot{ width:84px; height:34px; flex:0 0 auto; }\n  .trs-form{ display:flex; flex-direction:column; gap:8px; border-top:2px dashed ${PAL.stone}; padding-top:10px; margin-top:6px; }\n  .notes-list{ display:flex; flex-direction:column; gap:8px; }\n  .note-item{ display:flex; align-items:flex-start; gap:10px; background:${PAL.panel2}; border:2px solid ${PAL.stone};\n    border-radius:6px; padding:8px 10px; }\n  .note-item .ni{ flex:1 1 auto; font-size:13px; line-height:1.3; }\n  .note-item .ni .nt{ color:${PAL.cream}; font-size:15px; white-space:pre-wrap; }\n  .note-item .ni .nmeta{ color:${PAL.muted}; font-size:12px; margin-top:3px; }\n  .note-item .x-slot{ width:84px; height:34px; flex:0 0 auto; }\n  .gf-label{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px;\n    letter-spacing:1px; margin-bottom:5px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .recap-bar{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }\n  .recap-bar > *:first-child{ width:172px; height:34px; flex:0 0 auto; }\n  .recap-err{ color:${PAL.bloodLight}; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:0.5px; }\n  .xp-dist{ display:block; margin:6px 0 4px; }\n  .xp-dist > .gf-label{ margin-bottom:8px; }\n  .xp-row{ display:block; }\n  .xp-row > *:first-child{ display:block; width:200px; min-height:40px; margin-bottom:10px; }\n  .xp-info{ display:block; font-size:12px; color:${PAL.muted}; letter-spacing:0.5px; line-height:1.35; }\n  .xp-info b{ color:${PAL.goldLight}; }\n  .xp-done{ color:${PAL.goldLight}; font-size:13px; letter-spacing:0.5px; }\n  .xp-hist{ display:flex; flex-direction:column; gap:4px; margin-top:4px;\n    border-top:2px dashed ${PAL.stone}; padding-top:8px; }\n  .xp-hist-row{ display:flex; flex-direction:column; gap:1px; }\n  .xp-hist-main{ font-size:11px; color:${PAL.muted}; letter-spacing:0.5px; }\n  .xp-hist-up{ font-size:10px; color:${PAL.goldDeep}; letter-spacing:0.3px; }\n  textarea.gta{ width:100%; box-sizing:border-box; min-height:120px; resize:vertical; padding:10px 12px;\n    background:${PAL.inputBg}; color:${PAL.cream}; font-family:'Alegreya',sans-serif; font-size:16px; letter-spacing:0.5px;\n    line-height:1.4; border:3px solid ${PAL.edgeDark}; border-radius:4px; outline:none;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.6), inset -2px -2px 0 ${PAL.edgeLight};\n    transition:box-shadow 160ms ease, border-color 160ms ease; }\n  textarea.gta::placeholder{ color:rgba(236,224,196,0.4); }\n  textarea.gta:focus{ border-color:${PAL.goldDeep};\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.6), inset -2px -2px 0 ${PAL.edgeLight}, 0 0 8px rgba(216,178,76,0.5); }\n  .loading{ padding:60px 20px; text-align:center; color:${PAL.gold}; font-size:18px; }\n  .err{ padding:40px 20px; text-align:center; color:${PAL.bloodLight}; font-size:16px; }\n  /* gallery */\n  .gal-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:10px; }\n  .gal-cell{ position:relative; border:3px solid ${PAL.goldDeep}; border-radius:8px; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone},${PAL.edgeDark}); aspect-ratio:1/1;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.12), 0 4px 8px rgba(0,0,0,0.5); }\n  .gal-cell img{ width:100%; height:100%; object-fit:cover; display:block; }\n  .gal-badge{ position:absolute; top:5px; left:5px; font-size:10px; letter-spacing:0.5px; padding:2px 7px; border-radius:10px;\n    border:2px solid ${PAL.edgeDark}; color:${PAL.edgeDark}; background:${PAL.goldLight}; }\n  .gal-badge.gen{ background:${PAL.goldLight}; }\n  .gal-badge.up{ background:${PAL.muted}; color:${PAL.edgeDark}; }\n  .gal-x{ position:absolute; top:4px; right:4px; width:26px; height:26px; flex:0 0 auto; }\n  .gal-foot{ position:absolute; left:0; right:0; bottom:0; padding:4px 6px; font-size:11px; line-height:1.2;\n    color:${PAL.cream}; background:rgba(18,12,7,0.82); max-height:40%; overflow:hidden; }\n  .gal-up{ width:170px; height:40px; }\n  .gal-cell{ cursor:zoom-in; }\n  /* lightbox */\n  .lightbox-scrim{ position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center;\n    background:rgba(8,5,2,0.86); padding:5vh 5vw; cursor:zoom-out; }\n  .lightbox-frame{ position:relative; max-width:90vw; max-height:90vh; display:flex; cursor:default;\n    border:4px solid ${PAL.edgeDark}; border-radius:6px; background:${PAL.bg};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, 0 10px 30px rgba(0,0,0,0.7); padding:8px; }\n  .lightbox-img{ max-width:calc(90vw - 24px); max-height:calc(90vh - 24px); object-fit:contain; display:block; border-radius:3px; }\n  .lightbox-close{ position:absolute; top:-16px; right:-16px; width:38px; height:38px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:22px; line-height:1; color:${PAL.edgeDark};\n    background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:3px solid ${PAL.edgeDark}; border-radius:50%;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 3px 8px rgba(0,0,0,0.5); }\n  .lightbox-close:hover{ filter:brightness(1.1); }\n  `,\n\n  render(d2){\n    // --- reactive read of sessions dataset (subscribes) ---\n    const ds = d2.data('sessionsDataset');\n    let rows = [];\n    let dsLoading = false;\n    if(ds){\n      try { rows = ds.records() || []; } catch(e){ rows = []; }\n      dsLoading = !!ds.loading;\n    }\n\n    // --- resolve sessionId: router param first, else pathname, else lowest-number session ---\n    let routeId = '';\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/session\\/([^\\/?#]+)/);\n      if(_m) routeId = decodeURIComponent(_m[1]);\n    } catch(_){}\n    if(!routeId){ try { routeId = (d2.router().params || {}).id || ''; } catch(e){ routeId = ''; } }\n    let resolvedId = routeId;\n    if(!resolvedId && rows.length){\n      const sorted = rows.slice().sort((a,b)=> (Number(a.number)||0) - (Number(b.number)||0));\n      resolvedId = sorted[0] && sorted[0]._id;\n    }\n\n    if(resolvedId && d2.state.wantId !== resolvedId){\n      d2.state.wantId = resolvedId;\n    }\n\n    if(resolvedId && d2.state.loadedId !== resolvedId && !d2.state.loadingId){\n      this.load(d2, resolvedId);\n    }\n\n    const detail = d2.state.detail;\n    if(!resolvedId){\n      return `<div class=\"root\"><div class=\"loading\">Carregando…</div></div>`;\n    }\n    if(d2.state.errorId === resolvedId){\n      return `<div class=\"root\"><div class=\"err\">Sessão não encontrada.</div></div>`;\n    }\n    if(!detail || d2.state.loadedId !== resolvedId){\n      return `<div class=\"root\"><div class=\"loading\">Carregando…</div></div>`;\n    }\n\n    const session = detail.session || {};\n    const events = (detail.events || []).slice();\n    const encounters = detail.encounters || [];\n    // Prefer the full campaign roster (campHeroes) so the tray + selectors + presence\n    // show everyone; sessionDetail.heroes narrows to attendees once presence is set.\n    const campHeroes = d2.state.campHeroes || [];\n    const heroes = campHeroes.length ? campHeroes : (detail.heroes || []);\n    const treasures = detail.treasures || [];\n    const notes = (detail.notes || []).slice().sort((a,b)=> new Date(a.createdAt||0) - new Date(b.createdAt||0));\n\n    if(!d2.state.selectedHeroId && heroes.length){\n      d2.state.selectedHeroId = heroes[0].id;\n    }\n    const selId = d2.state.selectedHeroId || '';\n\n    const isOpen = session.status !== 'encerrada';\n    const statusTxt = isOpen ? (session.status || 'em andamento') : 'encerrada';\n\n    // In-game date/time line (shown in both views' header when present).\n    const igDate = isOpen ? (this.fval(d2,'inGameDate') || session.inGameDate || '') : (session.inGameDate || '');\n    const igTime = isOpen ? (this.fval(d2,'inGameTime') || session.inGameTime || '') : (session.inGameTime || '');\n    const igBits = [];\n    if(igDate) igBits.push(esc(igDate));\n    if(igTime) igBits.push(esc(igTime));\n    const igLine = igBits.length ? `<div class=\"ig-line\">In-game: ${igBits.join(' · ')}</div>` : '';\n\n    // ---- Header ----\n    const headerHtml = `\n      <button class=\"back\">‹ Voltar</button>\n      <div class=\"panel hdr\">\n        <div class=\"hdr-l\">\n          <div class=\"ttl\" style=\"font-size:30px\">Sessão ${esc(session.number)} — ${esc(session.title)}</div>\n          ${igLine}\n          <span class=\"pill ${isOpen?'open':'closed'}\">${esc(statusTxt)}</span>\n        </div>\n        <div style=\"display:flex; gap:8px; align-items:flex-start; flex:0 0 auto;\">\n          <div class=\"back-slot\" style=\"width:150px\">${d2.render(GB, { label:'Pós-sessão ✨', kind:'gold', size:'sm' }, { key:'posSessao', onPress:()=>this.goPosSessao(d2) })}</div>\n        </div>\n      </div>`;\n\n    // ---- Hero tray ----\n    const trayHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Heróis</div>\n        <div class=\"tray\">\n          ${heroes.map(h=>{\n            const sel = h.id===selId;\n            const ini = (h.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n            const ava = h.portrait ? `<img src=\"${esc(h.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(ini)}</span>`;\n            return `<div class=\"hero ${sel?'sel':''}\" data-hero=\"${esc(h.id)}\" data-key=\"${esc(h.id)}\">\n              <div class=\"ava\">${ava}</div>\n              <div class=\"nm\">${esc(h.name)}</div>\n              <div class=\"meta\">${esc(h.race||'')}·${esc(h.klass||'')}·nível ${esc(h.level||'')}</div>\n              <button class=\"hero-ficha\" data-hero=\"${esc(h.id)}\">ficha ›</button>\n            </div>`;\n          }).join('')}\n          ${(()=>{\n            // Mestre pill — lives in the SAME tray and uses the SAME selection\n            // (d2.state.selectedHeroId) as the hero pills. Sentinel id 'mestre';\n            // submitCrit maps it to heroId:null / heroName:'Mestre'.\n            const sel = selId==='mestre';\n            const mAva = session.mestrePortrait ? `<img src=\"${esc(session.mestrePortrait)}\" alt=\"\"/>` : `<span class=\"ini\">M</span>`;\n            const mName = session.mestreName ? esc(session.mestreName) : 'Mestre';\n            return `<div class=\"hero ${sel?'sel':''}\" data-hero=\"mestre\" data-key=\"mestre\">\n              <div class=\"ava\">${mAva}</div>\n              <div class=\"nm\">${mName}</div>\n              <div class=\"meta\">narrador</div>\n            </div>`;\n          })()}\n        </div>\n        <div class=\"sec-ttl\" style=\"font-size:18px; margin-top:12px\">Presença</div>\n        <div class=\"pres\">\n          ${heroes.length ? heroes.map(h=>{\n            const on = this.isPresent(d2, h.id);\n            const ini = (h.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n            const ava = h.portrait ? `<img src=\"${esc(h.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(ini)}</span>`;\n            return `<div class=\"pres-pill ${on?'on':'off'}\" data-pres=\"${esc(h.id)}\" data-key=\"pres-${esc(h.id)}\">\n              <span class=\"pava\">${ava}</span>\n              <span class=\"pnm\">${esc(h.name)}</span>\n              <span class=\"pmark\">${on?'✓':'—'}</span>\n            </div>`;\n          }).join('') : '<div class=\"hint\">Nenhum herói na campanha.</div>'}\n        </div>\n        <div class=\"hint\">Toque para marcar presente (dourado) ou ausente.</div>\n      </div>`;\n\n    // ---- Action buttons ----\n    const actionsHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Ações do herói</div>\n        <div class=\"row-actions\">\n          ${ACTIONS.map(a=>`<div class=\"act-slot\">${d2.render(GB,\n            { label:a.label, kind:a.kind, size:'sm', glyph:a.glyph },\n            { key:'act-'+a.type, onPress:()=>this.quickEvent(d2, a.type) })}</div>`).join('')}\n        </div>\n        <div class=\"hint\">${(selId && selId!=='mestre') ? 'Agindo sobre o herói selecionado.' : 'Selecione um herói acima (o Mestre vale só para a rolagem crítica).'}</div>\n      </div>`;\n\n    // ---- Crit logger (Nat 20 / Nat 1) mini-form ----\n    // \"Quem rolou\" reuses the hero tray selection (d2.state.selectedHeroId) — the\n    // same selection the quick-event buttons use — instead of a separate select.\n    // The tray now includes a \"Mestre\" pill (selId === 'mestre') which submitCrit\n    // maps to heroId:null / heroName:'Mestre'.\n    const critWhoName = selId === 'mestre'\n      ? 'Mestre'\n      : (this.heroById(d2, selId) ? this.heroById(d2, selId).name : '');\n    const critWhoHtml = `<div class=\"hint\" style=\"margin-top:0\">Quem rolou: <b style=\"color:${PAL.goldLight}\">${critWhoName ? esc(critWhoName) : 'selecione um herói ou o Mestre no tray acima'}</b></div>`;\n    const critRollT = d2.state.critRollType || 'ataque';\n    const critRollSelect = d2.render(GS,\n      { options:JSON.stringify(ROLL_TYPES.map(r=>({ value:r.v, label:r.l }))), value:critRollT, placeholder:'Tipo', label:'Tipo de rolagem' },\n      { key:'critRollType', style:'height:38px', onChange:(v)=>{ d2.state.critRollType = v; d2.refresh(); } });\n    const critFormHtml = `\n      <div class=\"panel free\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Rolagem crítica</div>\n        ${critWhoHtml}\n        <div class=\"selct\">${critRollSelect}</div>\n        <div class=\"fld-slot\">${this.gfield(d2,'critContext','Contexto (opcional)','ex: ataque no goblin, teste de Persuasão…')}</div>\n        <div class=\"free-row\">\n          <div class=\"fld-slot\" style=\"max-width:140px\">${this.gfield(d2,'critRoll','Resultado d100 (1–100)','opcional')}</div>\n        </div>\n        <div class=\"enc-actions\">\n          <div class=\"es\">${d2.render(GB, { label:'Nat 20', kind:'gold', size:'sm', glyph:'⚅' }, { key:'critN20', onPress:()=>this.submitCrit(d2, 'nat20') })}</div>\n          <div class=\"es\">${d2.render(GB, { label:'Nat 1', kind:'gold', size:'sm', glyph:'⚀' }, { key:'critN1', onPress:()=>this.submitCrit(d2, 'nat1') })}</div>\n        </div>\n        <div class=\"hint\">Preencha o d100 para buscar o efeito na tabela de críticos. Deixe em branco para registrar só o acerto/erro com o contexto.</div>\n      </div>`;\n\n    // ---- Free event ----\n    // Fall back to the tray hero, but never the 'mestre' sentinel (not a hero).\n    const freeHeroSel = d2.state.freeHeroSel || (selId === 'mestre' ? '' : selId);\n    const freeHeroSelect = d2.render(GS,\n      { options:JSON.stringify([{ value:'', label:'— grupo —' }].concat(heroes.map(h=>({ value:h.id, label:h.name })))), value:(freeHeroSel||''), placeholder:'— grupo —', label:'Herói' },\n      { key:'freeHeroSel', style:'height:38px', onChange:(v)=>{ d2.state.freeHeroSel = v; d2.refresh(); } });\n    const freeHtml = `\n      <div class=\"panel free\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Evento livre</div>\n        <div class=\"fld-slot\">${this.gfield(d2,'freeText','Nota','Descreva o evento…')}</div>\n        <div class=\"selct\">\n          ${freeHeroSelect}\n        </div>\n        <div class=\"free-row\">\n          <div class=\"btn-slot\" style=\"width:150px\">${d2.render(GB, { label:'Adicionar evento', kind:'gold', size:'sm', glyph:'✚' }, { key:'addFree', onPress:()=>this.addFree(d2) })}</div>\n        </div>\n      </div>`;\n\n    // ---- Score (fresh each render) — Tesouro reflects treasures.length ----\n    const scoreHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\">Placar da sessão</div>\n        <div class=\"score\">\n          ${SCORE.map(s=>{\n            const n = events.filter(e=>e.type===s.type).length;\n            return `<div class=\"score-cell\"><div class=\"n\">${n}</div><div class=\"l\">${esc(s.label)}</div></div>`;\n          }).join('')}\n          <div class=\"score-cell\"><div class=\"n\">${treasures.length}</div><div class=\"l\">Tesouro</div></div>\n        </div>\n      </div>`;\n\n    // ---- Feed (recent -> old) ----\n    const sortedEvents = events.slice().sort((a,b)=> new Date(b.createdAt||0) - new Date(a.createdAt||0));\n    const feedHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\" style=\"font-size:22px\">Feed de eventos</div>\n        <div class=\"feed\">\n          ${sortedEvents.length ? sortedEvents.map(e=>`\n            <div class=\"ev\" data-key=\"${esc(e.id)}\">\n              <div class=\"txt\"><span class=\"t\">[${hhmm(e.createdAt)}]</span> ${esc(e.heroName||'—')} — <span class=\"ty\">${esc(typeLabel(e.type))}</span>${e.note?(' — '+esc(e.note)):''}</div>\n              <div class=\"x-slot\">${d2.render(GB, { label:'×', kind:'danger', size:'sm', ghost:true }, { key:'del-'+e.id, onPress:()=>this.delEvent(d2, e.id) })}</div>\n            </div>`).join('') : `<div class=\"hint\">Nenhum evento ainda.</div>`}\n        </div>\n      </div>`;\n\n    // ---- Encounters ----\n    // Abates agora são geridos na tela de Combate — aqui os inimigos são apenas\n    // texto somente-leitura. Se um abate estiver registrado em enemies[].kills[],\n    // mostramos o nome de quem matou (texto puro), senão o inimigo sozinho.\n    const killerName = (heroId)=>{\n      if(!heroId) return '';\n      const h = this.heroById(d2, heroId);\n      return (h && h.name) ? h.name : '';\n    };\n    const encListHtml = encounters.map(c=>{\n      const enemies = (c.enemies||[]).map((en)=>{\n        const qty = Math.max(1, parseInt(en.qty,10) || 1);\n        const kills = this.enemyKills(en); // per-unit heroId array, length = qty\n        if(qty > 1){\n          const units = [];\n          for(let u=0; u<qty; u++){\n            const by = killerName(kills[u]);\n            units.push(`<div class=\"enemy-kill\"><span class=\"ek-name\">• ${esc(en.name)} #${u+1}${en.type?(' ('+esc(en.type)+')'):''}</span>${by?`<span class=\"ek-by\">abatido por ${esc(by)}</span>`:''}</div>`);\n          }\n          return units.join('');\n        }\n        const by = killerName(kills[0]);\n        return `<div class=\"enemy-kill\"><span class=\"ek-name\">• ${esc(en.name)}${en.type?(' ('+esc(en.type)+')'):''}</span>${by?`<span class=\"ek-by\">abatido por ${esc(by)}</span>`:''}</div>`;\n      }).join('');\n      return `<div class=\"enc\" data-key=\"${esc(c.id)}\">\n        <div class=\"en-name\">${esc(c.name)}</div>\n        <div class=\"en-meta\">${esc(c.kind||'')} · dificuldade ${esc(c.difficulty||'—')} · ${esc(c.result||'—')} · xp ${esc(c.xp||0)}</div>\n        <div class=\"enemies\">${enemies||'<div class=\"en-meta\">Sem inimigos.</div>'}</div>\n        <div class=\"hint\" style=\"margin-top:2px\">Edite os abates na tela de Combate.</div>\n        <div class=\"enc-actions\">\n          ${(()=>{\n            const combatId = (d2.state.combatByEnc||{})[c.id];\n            const lbl = d2.state.startingCombatId===c.id\n              ? 'Iniciando…'\n              : (combatId ? '⚔ Editar combate' : '⚔ Iniciar combate');\n            const onPress = combatId\n              ? (()=>this.go(d2, '/combat/' + combatId))\n              : (()=>this.startCombatFromEnc(d2, c));\n            return `<div class=\"es\">${d2.render(GB, { label:lbl, kind:'gold', size:'sm' }, { key:'startcombat-'+c.id, onPress })}</div>`;\n          })()}\n          <div class=\"es\">${d2.render(GB, { label:'Editar', kind:'gold', size:'sm', glyph:'✎' }, { key:'edit-'+c.id, onPress:()=>this.editEnc(d2, c) })}</div>\n          <div class=\"es\">${d2.render(GB, { label:'Excluir', kind:'danger', size:'sm', ghost:true }, { key:'delenc-'+c.id, onPress:()=>this.delEnc(d2, c.id) })}</div>\n        </div>\n      </div>`;\n    }).join('');\n\n    const localEnemies = d2.state.enemies || [];\n    const enemyListHtml = localEnemies.length ? `<div class=\"eb-list\">${localEnemies.map((en,i)=>`\n      <div class=\"eb-item\" data-key=\"en-${i}\"><span>${esc(en.name)} ×${esc(en.qty)}${en.type?(' ('+esc(en.type)+')'):''}</span>\n        <div class=\"rm-slot\">${d2.render(GB, { label:'×', kind:'danger', size:'sm', ghost:true }, { key:'rmen-'+i, onPress:()=>this.rmEnemy(d2, i) })}</div>\n      </div>`).join('')}</div>` : `<div class=\"hint\">Nenhum inimigo adicionado.</div>`;\n\n    const editing = !!d2.state.editingEncId;\n    const encFormTtl = editing ? 'Salvar encontro' : 'Adicionar encontro';\n    const encHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\">Encontros</div>\n        ${encListHtml || '<div class=\"hint\">Nenhum encontro ainda.</div>'}\n        <div class=\"enemy-builder\" style=\"margin-top:14px\">\n          <div class=\"en-name\" style=\"font-size:15px\">${esc(encFormTtl)}</div>\n          <div class=\"fld-slot\">${this.gfield(d2,'encName','Nome','Nome do encontro')}</div>\n          <div class=\"free-row\">\n            <div class=\"fld-slot\">${this.gfield(d2,'encKind','Tipo','combate, social…')}</div>\n            <div class=\"fld-slot\">${this.gfield(d2,'encDiff','Dificuldade','fácil/médio/difícil')}</div>\n          </div>\n          <div class=\"free-row\">\n            <div class=\"fld-slot\">${this.gfield(d2,'encResult','Resultado','vitória…')}</div>\n            <div class=\"fld-slot\">${this.gfield(d2,'encXp','XP','0')}</div>\n          </div>\n          <div class=\"enemy-builder\">\n            <div class=\"en-name\" style=\"font-size:14px;color:${PAL.gold}\">Inimigos</div>\n            ${enemyListHtml}\n            <div class=\"eb-row\">\n              <div class=\"ac-slot\">${d2.render(GA, { searchHandler:'searchMonsters', placeholder:'Nome do inimigo', label:'Inimigo', storeField:'enemyName', storeRef:'sessionLogForm', value:this.fval(d2,'enemyName') }, { key:'enemyAC-'+(d2.state.enemyACKey||0), onPick:(name)=>{ d2.state.enemyName = name; this.setForm(d2,'enemyName',name); } })}</div>\n              <div class=\"fld-slot\" style=\"max-width:80px\">${this.gfield(d2,'enemyQty','Qtd','qtd')}</div>\n              <div class=\"fld-slot\">${this.gfield(d2,'enemyType','Tipo','tipo')}</div>\n            </div>\n            <div class=\"btn-slot add\">${d2.render(GB, { label:'+ inimigo', kind:'gold', size:'sm', ghost:true }, { key:'addEnemy', onPress:()=>this.addEnemy(d2) })}</div>\n          </div>\n          <div class=\"enc-actions\">\n            <div class=\"es\">${d2.render(GB, { label:encFormTtl, kind:'gold', size:'sm', glyph:'✚' }, { key:'saveEnc', onPress:()=>this.saveEnc(d2) })}</div>\n            ${editing ? `<div class=\"es\">${d2.render(GB, { label:'Cancelar', kind:'gold', size:'sm', ghost:true }, { key:'cancelEnc', onPress:()=>this.cancelEnc(d2) })}</div>` : ''}\n          </div>\n        </div>\n      </div>`;\n\n    // ---- Treasures section ----\n    const trsListHtml = treasures.length ? `<div class=\"trs\">${treasures.map(t=>{\n      const bits = [];\n      if(t.kind) bits.push(esc(t.kind));\n      if(t.value!=null && t.value!=='' && Number(t.value)) bits.push(esc(t.value)+' ouro');\n      if(t.rarity) bits.push(esc(t.rarity));\n      if(t.heroName) bits.push('→ '+esc(t.heroName));\n      return `<div class=\"trs-item\" data-key=\"${esc(t.id||t._id)}\">\n        <div class=\"ti\"><div class=\"nm\">${esc(t.name)}</div><div class=\"meta\">${bits.join(' · ')||'—'}</div></div>\n        <div class=\"x-slot\">${d2.render(GB, { label:'Excluir', kind:'danger', size:'sm', ghost:true }, { key:'deltrs-'+(t.id||t._id), onPress:()=>this.delTreasure(d2, t.id||t._id) })}</div>\n      </div>`;\n    }).join('')}</div>` : `<div class=\"hint\">Nenhum tesouro ainda.</div>`;\n\n    const kindSel = d2.state.trsKind || 'item';\n    const kindSelect = d2.render(GS,\n      { options:JSON.stringify(KINDS.map(k=>({ value:k.v, label:k.l }))), value:kindSel, placeholder:'Tipo', label:'Tipo' },\n      { key:'trsKind', style:'height:38px', onChange:(v)=>{ d2.state.trsKind = v; d2.refresh(); } });\n    const trsHeroSelect = d2.render(GS,\n      { options:JSON.stringify([{ value:'', label:'— grupo —' }].concat(heroes.map(h=>({ value:h.id, label:h.name })))), value:(d2.state.trsHeroSel || ''), placeholder:'— grupo —', label:'Herói' },\n      { key:'trsHeroSel', style:'height:38px', onChange:(v)=>{ d2.state.trsHeroSel = v; d2.refresh(); } });\n    const treasureHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\">Tesouros</div>\n        ${trsListHtml}\n        <div class=\"trs-form\">\n          <div class=\"en-name\" style=\"font-size:15px;color:${PAL.goldLight}\">Adicionar tesouro</div>\n          <div class=\"ac-slot\">${d2.render(GA, { searchHandler:'searchItems', placeholder:'Nome do item', label:'Nome', storeField:'treasureName', storeRef:'sessionLogForm', value:this.fval(d2,'treasureName') }, { key:'trsAC-'+(d2.state.trsACKey||0), onPick:(name)=>{ d2.state.treasureName = name; this.setForm(d2,'treasureName',name); } })}</div>\n          <div class=\"free-row\">\n            <div class=\"fld-slot\" style=\"max-width:120px\">${this.gfield(d2,'trsValue','Ouro','0')}</div>\n          </div>\n          <div class=\"selct\">\n            ${kindSelect}\n          </div>\n          <div class=\"selct\">\n            ${trsHeroSelect}\n          </div>\n          <div class=\"btn-slot add\">${d2.render(GB, { label:'Adicionar', kind:'gold', size:'sm' }, { key:'addTrs', onPress:()=>this.addTreasure(d2) })}</div>\n        </div>\n      </div>`;\n\n    // ---- Resumo (maior) + Anotações gerais ----\n    const closeLabel = isOpen ? 'Encerrar sessão' : 'Reabrir sessão';\n    // XP distribution: rateio igual entre presentes (d2.state.attendees).\n    const xpSessao = parseInt(this.fval(d2,'xp')||'0',10) || 0;\n    const presentCount = (d2.state.attendees || []).length;\n    const awarded = !!session.xpAwarded;\n    const share = presentCount > 0 ? Math.floor(xpSessao / presentCount) : 0;\n    const xpBtn = awarded\n      ? d2.render(GB, { label:'✓ XP distribuído', kind:'gold', size:'sm', ghost:true }, { key:'distribXp', onPress:()=>this.distribuirXp(d2) })\n      : d2.render(GB, { label:(d2.state.distributingXp ? 'Distribuindo…' : 'Distribuir XP'), kind:'gold', size:'sm', glyph:'✦' }, { key:'distribXp', onPress:()=>this.distribuirXp(d2) });\n    const xpInfo = awarded\n      ? `<span class=\"xp-done\">XP já distribuído nesta sessão. Toque para redistribuir.</span>`\n      : `<span class=\"xp-info\">${presentCount} presente(s) · <b>${share}</b> xp para cada (de ${xpSessao} total).</span>`;\n    // ---- Histórico de XP (mais recente primeiro) ----\n    const xpLog = Array.isArray(session.xpLog) ? session.xpLog : [];\n    const fmtXpAt = (iso)=>{\n      const d = new Date(iso);\n      if(isNaN(d.getTime())) return '';\n      const dd = String(d.getDate()).padStart(2,'0');\n      const mm = String(d.getMonth()+1).padStart(2,'0');\n      return dd+'/'+mm+' '+hhmm(iso);\n    };\n    const xpHistHtml = xpLog.length ? `\n        <div class=\"xp-hist\">\n          <div class=\"gf-label\" style=\"margin-bottom:2px\">Histórico de XP</div>\n          ${xpLog.slice().reverse().map(en=>{\n            const ups = Array.isArray(en.ups) ? en.ups : [];\n            const upLine = ups.length ? `<div class=\"xp-hist-up\">↑ ${esc(ups.join(', '))}</div>` : '';\n            return `<div class=\"xp-hist-row\"><span class=\"xp-hist-main\">${esc(fmtXpAt(en.at))} · ${esc(en.total||0)} xp ×${esc(en.n||0)} presente(s)</span>${upLine}</div>`;\n          }).join('')}\n        </div>` : '';\n    const xpDistHtml = `\n        <div class=\"xp-dist\">\n          <div class=\"gf-label\">Distribuir XP da sessão</div>\n          <div class=\"xp-row\">\n            ${xpBtn}\n            ${xpInfo}\n          </div>\n          ${xpHistHtml}\n        </div>`;\n    const tailHtml = `\n      <div class=\"panel free\">\n        <div class=\"sec-ttl\">Resumo da sessão</div>\n        <div class=\"free-row\">\n          <div class=\"fld-slot\">${this.gfield(d2,'xp','XP','0')}</div>\n          <div class=\"fld-slot\">${this.gfield(d2,'gold','Ouro','0')}</div>\n        </div>\n        <div class=\"free-row\">\n          <div class=\"fld-slot\">${this.gfield(d2,'inGameDate','Dia (in-game)','ex: 15 de Ches')}</div>\n          <div class=\"fld-slot\">${this.gfield(d2,'inGameTime','Horário (in-game)','ex: anoitecer')}</div>\n        </div>\n        ${xpDistHtml}\n        <div class=\"hint\" style=\"margin-top:6px\">O resumo narrativo da sessão é escrito na tela de <b style=\"color:${PAL.goldLight}\">Pós-sessão ✨</b>.</div>\n        <div class=\"enc-actions\">\n          <div class=\"es\">${d2.render(GB, { label:'Salvar', kind:'gold', size:'sm' }, { key:'saveSess', onPress:()=>this.saveSession(d2) })}</div>\n          <div class=\"es\">${d2.render(GB, { label:closeLabel, kind:(isOpen?'danger':'gold'), size:'sm', ghost:true }, { key:'toggleStatus', onPress:()=>this.toggleStatus(d2) })}</div>\n        </div>\n      </div>`;\n\n    // ---- Anotações gerais (LISTA) ----\n    const notesListHtml = notes.length ? `<div class=\"notes-list\">${notes.map(n=>{\n      const meta = [];\n      if(n.createdAt) meta.push(hhmm(n.createdAt));\n      if(n.author) meta.push(esc(n.author));\n      return `<div class=\"note-item\" data-key=\"${esc(n.id||n._id)}\">\n        <div class=\"ni\">\n          <div class=\"nt\">${esc(n.text)}</div>\n          <div class=\"nmeta\">${meta.join(' · ')||'—'}</div>\n        </div>\n        <div class=\"x-slot\">${d2.render(GB, { label:'Excluir', kind:'danger', size:'sm', ghost:true }, { key:'delnote-'+(n.id||n._id), onPress:()=>this.delNote(d2, n.id||n._id) })}</div>\n      </div>`;\n    }).join('')}</div>` : `<div class=\"hint\">Nenhuma anotação ainda.</div>`;\n\n    const notesHtml = `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\">Anotações gerais</div>\n        ${notesListHtml}\n        <div class=\"trs-form\">\n          <div class=\"fld-slot\">\n            <textarea class=\"gta\" id=\"noteTA\" placeholder=\"Nova anotação…\">${esc(d2.state.noteDraft||'')}</textarea>\n          </div>\n          <div class=\"btn-slot add\">${d2.render(GB, { label:'Adicionar anotação', kind:'gold', size:'sm', glyph:'✎' }, { key:'addNote', onPress:()=>this.addNote(d2) })}</div>\n        </div>\n      </div>`;\n\n    // ============ READ-ONLY \"chronicle\" view for an ENCERRADA session ============\n    if(!isOpen){\n      // ---- Per-session stats (derived client-side from session.events) ----\n      const evAll = (detail.events || []);\n      const cnt = (t)=> evAll.filter(e=>e.type===t).length;\n      const STAT_TYPES = [\n        { t:'nat20', l:'Nat 20' }, { t:'nat1', l:'Nat 1' }, { t:'KO', l:'KO' },\n        { t:'morte', l:'Morte' }, { t:'level-up', l:'Level up' }, { t:'abate', l:'Abate' }\n      ];\n      const statCells = STAT_TYPES.map(s=>`<div class=\"stat-cell\"><div class=\"n\">${cnt(s.t)}</div><div class=\"l\">${esc(s.l)}</div></div>`).join('')\n        + `<div class=\"stat-cell\"><div class=\"n\">${esc(session.xp||0)}</div><div class=\"l\">XP</div></div>`\n        + `<div class=\"stat-cell\"><div class=\"n\">${esc(session.gold||0)}</div><div class=\"l\">Ouro</div></div>`\n        + `<div class=\"stat-cell\"><div class=\"n\">${treasures.length}</div><div class=\"l\">Tesouros</div></div>`\n        + `<div class=\"stat-cell\"><div class=\"n\">${encounters.length}</div><div class=\"l\">Encontros</div></div>`;\n      const statsHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Placar da sessão</div>\n          <div class=\"stat-grid\">${statCells}</div>\n        </div>`;\n\n      // ---- Per-participant table (heroes present + Mestre) from sessionStats ----\n      this.loadSessionStats(d2, session.id);\n      const sStats = (d2.state.sStatsId === session.id) ? (d2.state.sStats || {}) : null;\n      let perPartHtml = '';\n      if(sStats){\n        const rows = sStats.perHero || [];\n        const headRow = `<div class=\"hrow head\"><div class=\"hc name\">Herói</div><div class=\"hc\">N20</div><div class=\"hc\">N1</div><div class=\"hc\">KO</div><div class=\"hc\">Mortes</div><div class=\"hc\">Abates</div><div class=\"hc\">Eventos</div></div>`;\n        const cell = (v)=>`<div class=\"hc${(!v)?' zero':''}\">${esc(v||0)}</div>`;\n        const bodyRows = rows.length\n          ? rows.map(h=>`<div class=\"hrow\"><div class=\"hc name\">${esc(h.heroName||'—')}</div>${cell(h.nat20)}${cell(h.nat1)}${cell(h.ko)}${cell(h.morte)}${cell(h.abate)}${cell(h.eventos)}</div>`).join('')\n          : `<div class=\"hint\" style=\"padding:10px\">Sem participantes registrados.</div>`;\n        perPartHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Por participante</div>\n          <div class=\"htable\">${headRow}${bodyRows}</div>\n        </div>`;\n      } else {\n        perPartHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Por participante</div>\n          <div class=\"hint\" style=\"padding:10px\">Carregando…</div>\n        </div>`;\n      }\n\n      // ---- Recap (read-only prose) ----\n      const recap = session.summary || '';\n      const recapHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Resumo</div>\n          ${recap ? `<div class=\"ro-prose\">${esc(recap)}</div>` : '<div class=\"ro-empty\">Nenhum resumo registrado. Escreva-o na tela de Pós-sessão.</div>'}\n        </div>`;\n\n      // ---- Heróis participantes (read-only) ----\n      const roHeroes = (detail.heroes && detail.heroes.length) ? detail.heroes : heroes;\n      const roHeroesHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\" style=\"font-size:22px\">Heróis participantes</div>\n          <div class=\"tray\">\n            ${roHeroes.length ? roHeroes.map(h=>{\n              const ini = (h.name||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase();\n              const ava = h.portrait ? `<img src=\"${esc(h.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(ini)}</span>`;\n              return `<div class=\"hero\" style=\"cursor:default\">\n                <div class=\"ava\">${ava}</div>\n                <div class=\"nm\">${esc(h.name)}</div>\n                <div class=\"meta\">${esc(h.race||'')}·${esc(h.klass||'')}·nível ${esc(h.level||'')}</div>\n              </div>`;\n            }).join('') : '<div class=\"ro-empty\">Nenhum herói registrado.</div>'}\n          </div>\n        </div>`;\n\n      // ---- Events feed (read-only) ----\n      const roFeedHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\" style=\"font-size:22px\">Feed de eventos</div>\n          <div class=\"feed ro-feed\">\n            ${sortedEvents.length ? sortedEvents.map(e=>`\n              <div class=\"ev\">\n                <div class=\"txt\"><span class=\"t\">[${hhmm(e.createdAt)}]</span> ${esc(e.heroName||'—')} — <span class=\"ty\">${esc(typeLabel(e.type))}</span>${e.note?(' — '+esc(e.note)):''}</div>\n              </div>`).join('') : `<div class=\"ro-empty\">Nenhum evento registrado.</div>`}\n          </div>\n        </div>`;\n\n      // ---- Encounters (read-only — no action buttons, just enemies + kills) ----\n      const roKillerName = (heroId)=>{ if(!heroId) return ''; const h = this.heroById(d2, heroId); return (h && h.name) ? h.name : ''; };\n      const roEncList = encounters.map(c=>{\n        const enemies = (c.enemies||[]).map((en)=>{\n          const qty = Math.max(1, parseInt(en.qty,10) || 1);\n          const kills = this.enemyKills(en);\n          if(qty > 1){\n            const units = [];\n            for(let u=0; u<qty; u++){\n              const by = roKillerName(kills[u]);\n              units.push(`<div class=\"enemy-kill\"><span class=\"ek-name\">• ${esc(en.name)} #${u+1}${en.type?(' ('+esc(en.type)+')'):''}</span>${by?`<span class=\"ek-by\">abatido por ${esc(by)}</span>`:''}</div>`);\n            }\n            return units.join('');\n          }\n          const by = roKillerName(kills[0]);\n          return `<div class=\"enemy-kill\"><span class=\"ek-name\">• ${esc(en.name)}${en.type?(' ('+esc(en.type)+')'):''}</span>${by?`<span class=\"ek-by\">abatido por ${esc(by)}</span>`:''}</div>`;\n        }).join('');\n        return `<div class=\"enc\">\n          <div class=\"en-name\">${esc(c.name)}</div>\n          <div class=\"en-meta\">${esc(c.kind||'')} · dificuldade ${esc(c.difficulty||'—')} · ${esc(c.result||'—')} · xp ${esc(c.xp||0)}</div>\n          <div class=\"enemies\">${enemies||'<div class=\"en-meta\">Sem inimigos.</div>'}</div>\n        </div>`;\n      }).join('');\n      const roEncHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Encontros</div>\n          ${roEncList || '<div class=\"ro-empty\">Nenhum encontro registrado.</div>'}\n        </div>`;\n\n      // ---- Treasures (read-only) ----\n      const roTrsHtml = `\n        <div class=\"panel\">\n          <div class=\"sec-ttl\">Tesouros</div>\n          ${treasures.length ? `<div class=\"trs\">${treasures.map(t=>{\n            const bits = [];\n            if(t.kind) bits.push(esc(t.kind));\n            if(t.value!=null && t.value!=='' && Number(t.value)) bits.push(esc(t.value)+' ouro');\n            if(t.rarity) bits.push(esc(t.rarity));\n            if(t.heroName) bits.push('→ '+esc(t.heroName));\n            return `<div class=\"trs-item\">\n              <div class=\"ti\"><div class=\"nm\">${esc(t.name)}</div><div class=\"meta\">${bits.join(' · ')||'—'}</div></div>\n            </div>`;\n          }).join('')}</div>` : '<div class=\"ro-empty\">Nenhum tesouro registrado.</div>'}\n        </div>`;\n\n      // ---- Reabrir sessão ----\n      const reopenHtml = `\n        <div class=\"panel free\">\n          <div class=\"enc-actions\">\n            <div class=\"es\">${d2.render(GB, { label:'Reabrir sessão', kind:'gold', size:'sm', ghost:true }, { key:'reopenSess', onPress:()=>this.toggleStatus(d2) })}</div>\n            <div class=\"es\">${d2.render(GB, { label:'Pós-sessão', kind:'gold', size:'sm' }, { key:'roPos', onPress:()=>this.goPosSessao(d2) })}</div>\n          </div>\n        </div>`;\n\n      return `<div class=\"root\">\n        ${headerHtml}\n        ${recapHtml}\n        ${roHeroesHtml}\n        ${statsHtml}\n        ${perPartHtml}\n        ${roFeedHtml}\n        ${roEncHtml}\n        ${roTrsHtml}\n        ${this.galleryHtml(d2, true)}\n        ${reopenHtml}\n        ${this.lightboxHtml(d2)}\n      </div>`;\n    }\n\n    return `<div class=\"root\">\n      ${headerHtml}\n      ${trayHtml}\n      ${actionsHtml}\n      ${critFormHtml}\n      ${freeHtml}\n      ${scoreHtml}\n      ${feedHtml}\n      ${encHtml}\n      ${treasureHtml}\n      ${this.galleryHtml(d2, false)}\n      ${tailHtml}\n      ${notesHtml}\n      ${this.lightboxHtml(d2)}\n    </div>`;\n  },\n\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    if(!d2.state.enemies) d2.state.enemies = [];\n    if(!d2.state.form) d2.state.form = {};\n    try { d2.on(window, 'popstate', ()=>{ try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ this.goBack(d2); });\n    // hero selection\n    d2.on('.hero','click',(e)=>{\n      if(e.target.closest('.hero-ficha')) return;\n      const card = e.target.closest('.hero');\n      if(card && card.dataset.hero){\n        d2.state.selectedHeroId = card.dataset.hero;\n        // Mirror to the free-event hero select only for real heroes — the\n        // 'mestre' sentinel isn't a valid heroId for the free-event payload.\n        if(card.dataset.hero !== 'mestre'){ d2.state.freeHeroSel = card.dataset.hero; }\n      }\n    });\n    // presence toggle\n    d2.on('.pres-pill','click',(e)=>{\n      const pill = e.target.closest('.pres-pill');\n      if(pill && pill.dataset.pres){ this.toggleAttendance(d2, pill.dataset.pres); }\n    });\n    d2.on('.hero-ficha','click',(e)=>{\n      e.stopPropagation();\n      const b = e.target.closest('.hero-ficha');\n      const hid = b && b.getAttribute('data-hero');\n      if(hid){ this.go(d2, '/hero/' + hid); }\n    });\n    // Free-event hero, treasure kind, treasure hero and kill attribution now\n    // use GoldenSelect (custom dropdown), wired via its onChange callback at\n    // render time — no delegated pill click handlers needed here.\n    // parent-owned form inputs -> d2.state.form (the reliable channel).\n    // Delegated by id prefix 'f-<field>' on the shared .gf-input class.\n    d2.on('.gf-input','input',(e)=>{\n      const id = e.target && e.target.id;\n      if(id && id.indexOf('f-')===0){ this.setForm(d2, id.slice(2), e.target.value); }\n    });\n    // In-game date/time persist on blur/change (kept in form on input above to\n    // avoid cursor jump; one updateSession write here on change, no re-render).\n    d2.on('.gf-input','change',(e)=>{\n      const id = e.target && e.target.id;\n      if(id==='f-inGameDate' || id==='f-inGameTime'){ this.saveInGame(d2); }\n    });\n    // new general-note draft -> local state\n    d2.on('#noteTA','input',(e)=>{ d2.state.noteDraft = e.target.value; });\n    // gallery image upload (hidden file input)\n    d2.on('.gal-fileinput','change',(e)=>{ this.onGalleryFilePicked(d2, e.target); });\n    // gallery lightbox: clicking the image (NOT the × delete button) enlarges it.\n    d2.on('.gal-cell','click',(e)=>{\n      if(e.target.closest('.gal-x')) return; // delete button wins\n      const cell = e.target.closest('.gal-cell');\n      if(cell && cell.dataset.url){ this.openLightbox(d2, cell.dataset.url); }\n    });\n    // lightbox dismiss: click the scrim or the close button.\n    d2.on('.lightbox-scrim','click',(e)=>{\n      if(e.target.closest('.lightbox-close') || e.target.classList.contains('lightbox-scrim')){\n        this.closeLightbox(d2);\n      }\n    });\n  },\n  openLightbox(d2, url){\n    if(!url) return;\n    d2.state.lightboxUrl = url;\n    d2.refresh();\n  },\n  closeLightbox(d2){\n    d2.state.lightboxUrl = null;\n    d2.refresh();\n  },\n  lightboxHtml(d2){\n    const url = d2.state.lightboxUrl;\n    if(!url) return '';\n    return `<div class=\"lightbox-scrim\">\n      <div class=\"lightbox-frame\">\n        <button class=\"lightbox-close\" title=\"Fechar\">×</button>\n        <img class=\"lightbox-img\" src=\"${esc(url)}\" alt=\"\"/>\n      </div>\n    </div>`;\n  },\n\n  // ---------- data loading ----------\n  load(d2, sessionId){\n    d2.state.loadingId = sessionId;\n    d2.rush('sessionDetail').invoke({ sessionId }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      if(data && data.session){\n        d2.state.detail = data;\n        d2.state.loadedId = sessionId;\n        d2.state.errorId = null;\n        const s = data.session;\n        d2.state.attendees = Array.isArray(s.attendeeHeroIds) ? s.attendeeHeroIds.slice() : [];\n        this.loadCampHeroes(d2, s.campaignId);\n        this.loadCombats(d2, sessionId);\n        this.loadGallery(d2, sessionId);\n        const seed = Object.assign({}, d2.state.form||{}, {\n          xp: s.xp!=null?String(s.xp):'',\n          gold: s.gold!=null?String(s.gold):'',\n          summary: s.summary||'',\n          inGameDate: s.inGameDate||'',\n          inGameTime: s.inGameTime||''\n        });\n        d2.state.form = seed;\n      } else {\n        d2.state.errorId = sessionId;\n      }\n      d2.state.loadingId = null;\n    }).catch((err)=>{\n      d2.log('sessionDetail error', err);\n      d2.state.errorId = sessionId;\n      d2.state.loadingId = null;\n    });\n  },\n\n  reload(d2){\n    const id = d2.state.loadedId || d2.state.wantId;\n    if(!id) return;\n    d2.rush('sessionDetail').invoke({ sessionId:id }).then((raw)=>{\n      const data = (raw && raw.data) ? raw.data : raw;\n      if(data && data.session){\n        d2.state.detail = data;\n        const s = data.session;\n        d2.state.attendees = Array.isArray(s.attendeeHeroIds) ? s.attendeeHeroIds.slice() : [];\n        this.loadCampHeroes(d2, s.campaignId);\n        this.loadCombats(d2, id);\n        this.loadGallery(d2, id);\n        d2.state.form = Object.assign({}, d2.state.form||{}, {\n          xp: s.xp!=null?String(s.xp):'',\n          gold: s.gold!=null?String(s.gold):'',\n          summary: s.summary||'',\n          inGameDate: s.inGameDate||'',\n          inGameTime: s.inGameTime||''\n        });\n      }\n    }).catch((e)=>d2.log('reload error', e));\n  },\n\n  curIds(d2){\n    const s = (d2.state.detail && d2.state.detail.session) || {};\n    return { sessionId: s.id, campaignId: s.campaignId };\n  },\n  heroById(d2, id){\n    const hs = (d2.state.detail && d2.state.detail.heroes) || [];\n    let found = hs.find(h=>h.id===id);\n    if(found) return found;\n    const ch = d2.state.campHeroes || [];\n    return ch.find(h=>h.id===id);\n  },\n  // All heroes of the campaign (for the Presença faixa + kill attribution),\n  // loaded once per campaignId via heroesByCampaign. sessionDetail.heroes is\n  // filtered to attendees when attendeeHeroIds is set, so it can't drive the\n  // full presence roster — this can.\n  loadCampHeroes(d2, campaignId){\n    if(!campaignId) return;\n    if(d2.state.campHeroesCid === campaignId && Array.isArray(d2.state.campHeroes)) return;\n    if(d2.state.campHeroesLoading === campaignId) return;\n    d2.state.campHeroesLoading = campaignId;\n    d2.rush('heroesByCampaign').invoke({ campaignId }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      const list = (res && res.results) || [];\n      d2.state.campHeroes = list;\n      d2.state.campHeroesCid = campaignId;\n      d2.state.campHeroesLoading = null;\n    }).catch((e)=>{ d2.log('loadCampHeroes', e); d2.state.campHeroesLoading = null; });\n  },\n  // Load the combats for this session (single read) and build a map\n  // encounterId -> combatId so each encounter can show \"Editar combate\" and\n  // navigate to its existing /combat/<id> instead of always creating a new one.\n  // Prefer a non-ended (active) combat when an encounter has several.\n  loadCombats(d2, sessionId){\n    if(!sessionId) return;\n    d2.rush('combatsBySession').invoke({ sessionId }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      const list = (res && res.results) || [];\n      const map = {};\n      // Deterministic resume: per encounter, prefer a NON-ENDED combat, and among\n      // combats of the same ended/active status pick the MOST RECENTLY EDITED one\n      // (updatedAt, then createdAt, then _id as a monotonic tiebreaker). Duplicate\n      // combats per encounter exist in real data; this guarantees \"Editar combate\"\n      // opens the live one the master last touched instead of a stale/zeroed dup.\n      const tnum = (v)=>{ const n = v ? new Date(v).getTime() : 0; return isNaN(n) ? 0 : n; };\n      const ts = (c)=> tnum(c && (c.updatedAt || c.createdAt));\n      // A combat that was created but never touched has updatedAt == createdAt;\n      // it's a fresh/zeroed duplicate, not the one the master worked in. Treat an\n      // EDITED combat (updatedAt noticeably after createdAt) as the real resume\n      // target so we don't open a stale empty dup over the edited live combat.\n      const edited = (c)=> (tnum(c.updatedAt) - tnum(c.createdAt)) > 1000;\n      const better = (a, b)=>{\n        // true if b should replace a\n        const aEnded = a.status === 'ended', bEnded = b.status === 'ended';\n        if(aEnded !== bEnded){ return aEnded && !bEnded; } // non-ended wins\n        const aEd = edited(a), bEd = edited(b);\n        if(aEd !== bEd){ return bEd && !aEd; }             // an edited combat wins over a pristine dup\n        const ta = ts(a), tb = ts(b);\n        if(tb !== ta){ return tb > ta; }                   // most-recently active wins\n        return String(b.id) > String(a.id);                // monotonic _id tiebreak\n      };\n      list.forEach(c=>{\n        if(!c || !c.encounterId) return;\n        const prev = map[c.encounterId];\n        if(!prev || better(prev, c)){ map[c.encounterId] = c; }\n      });\n      const byEnc = {};\n      Object.keys(map).forEach(k=>{ byEnc[k] = map[k].id; });\n      d2.state.combatByEnc = byEnc;\n    }).catch((e)=>{ d2.log('loadCombats', e); });\n  },\n  isPresent(d2, heroId){\n    const a = d2.state.attendees || [];\n    return a.indexOf(heroId) !== -1;\n  },\n  // Load the per-participant stats (incl. the Mestre bucket) for the encerrada\n  // view. One read of the sessionStats handler, cached per sessionId in state.\n  loadSessionStats(d2, sessionId){\n    if(!sessionId) return;\n    if(d2.state.sStatsId === sessionId && d2.state.sStats) return;\n    if(d2.state.sStatsLoading === sessionId) return;\n    d2.state.sStatsLoading = sessionId;\n    d2.rush('sessionStats').invoke({ sessionId }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      d2.state.sStats = res || {};\n      d2.state.sStatsId = sessionId;\n      d2.state.sStatsLoading = null;\n      d2.refresh();\n    }).catch((e)=>{ d2.log('loadSessionStats', e); d2.state.sStatsLoading = null; });\n  },\n  // ---- per-session image gallery ----\n  // Load images via imagesBySession, cached per sessionId in d2.state.gallery.\n  loadGallery(d2, sessionId){\n    if(!sessionId) return;\n    if(d2.state.galleryLoadingId === sessionId) return;\n    d2.state.galleryLoadingId = sessionId;\n    d2.rush('imagesBySession').invoke({ sessionId }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      d2.state.gallery = (res && res.results) || [];\n      d2.state.galleryId = sessionId;\n      d2.state.galleryLoadingId = null;\n      d2.refresh();\n    }).catch((e)=>{ d2.log('loadGallery', e); d2.state.galleryLoadingId = null; });\n  },\n  galleryImages(d2){\n    const ids = this.curIds(d2);\n    if(d2.state.galleryId && ids.sessionId && d2.state.galleryId === ids.sessionId){\n      return d2.state.gallery || [];\n    }\n    return d2.state.gallery || [];\n  },\n  // Build the Galeria panel. readOnly=true omits the upload + delete controls\n  // (the encerrada chronicle view).\n  galleryHtml(d2, readOnly){\n    const imgs = this.galleryImages(d2);\n    const cells = imgs.length ? imgs.map(im=>{\n      const src = esc(im.url||'');\n      const isGen = im.source === 'gen';\n      const badge = `<span class=\"gal-badge ${isGen?'gen':'up'}\">${isGen?'gerada':'enviada'}</span>`;\n      const foot = im.prompt ? `<div class=\"gal-foot\">${esc(im.prompt)}</div>` : '';\n      const x = readOnly ? '' : `<div class=\"gal-x\">${d2.render(GB, { label:'×', kind:'danger', size:'sm', ghost:true }, { key:'galx-'+(im._id), onPress:()=>this.delImage(d2, im._id) })}</div>`;\n      return `<div class=\"gal-cell\" data-key=\"gal-${esc(im._id)}\" data-url=\"${src}\">\n        <img src=\"${src}\" alt=\"\"/>\n        ${badge}\n        ${x}\n        ${foot}\n      </div>`;\n    }).join('') : `<div class=\"${readOnly?'ro-empty':'hint'}\">Nenhuma imagem ainda.</div>`;\n    const uploadCtl = readOnly ? '' : `\n      <div style=\"margin-top:12px; display:flex; align-items:center; gap:10px;\">\n        <div class=\"gal-up\">${d2.render(GB, { label:(d2.state.galUploading?'Enviando…':'Enviar imagem'), kind:'gold', size:'sm', glyph:'⬆' }, { key:'galUpload', onPress:()=>this.triggerGalleryUpload(d2) })}</div>\n        ${d2.state.galUploadErr ? `<span class=\"recap-err\">${esc(d2.state.galUploadErr)}</span>` : ''}\n        <input class=\"gal-fileinput\" type=\"file\" accept=\"image/*\" style=\"display:none\" />\n      </div>`;\n    return `\n      <div class=\"panel\">\n        <div class=\"sec-ttl\">Galeria</div>\n        <div class=\"gal-grid\">${cells}</div>\n        ${uploadCtl}\n      </div>`;\n  },\n  triggerGalleryUpload(d2){\n    const el = d2.$('.gal-fileinput');\n    if(el && el.click){ el.click(); }\n  },\n  // Upload via cronicaDrive (same flow as HeroSheet portrait), then addSessionImage.\n  onGalleryFilePicked(d2, input){\n    const files = input && input.files;\n    if(!files || !files.length) return;\n    const ids = this.curIds(d2);\n    if(!ids.sessionId){ d2.dialog.alert('Sessão não carregada.'); return; }\n    d2.state.galUploading = true;\n    d2.state.galUploadErr = null;\n    d2.refresh();\n    d2.drive('cronicaDrive').uploadFromFiles([files[0]]).then((out)=>{\n      const url = out && out[0] && (out[0].fileUrl || (out[0].params && out[0].params.fileUrl));\n      if(!url){ d2.state.galUploadErr = 'Upload sem URL.'; d2.state.galUploading = false; d2.refresh(); return; }\n      // Optimistic: show the new image immediately, then persist + refetch.\n      const optimistic = (d2.state.gallery||[]).slice();\n      optimistic.unshift({ _id:'tmp-'+Date.now(), url, prompt:'', source:'upload', _optimistic:true });\n      d2.state.gallery = optimistic;\n      d2.refresh();\n      d2.rush('addSessionImage').invoke({ sessionId: ids.sessionId, campaignId: ids.campaignId, url, prompt:'', source:'upload' }).then(()=>{\n        d2.state.galUploading = false;\n        d2.state.galleryLoadingId = null;\n        this.loadGallery(d2, ids.sessionId);\n      }).catch((e)=>{ d2.log('addSessionImage', e); d2.state.galUploading = false; d2.state.galUploadErr = 'Falha ao salvar a imagem.'; this.loadGallery(d2, ids.sessionId); });\n    }).catch((e)=>{ d2.log('galUpload', e); d2.state.galUploading = false; d2.state.galUploadErr = 'Falha no upload da imagem.'; d2.refresh(); });\n    if(input){ input.value = ''; }\n  },\n  delImage(d2, imageId){\n    if(!imageId) return;\n    // Optimistic remove, then delete + refetch.\n    const ids = this.curIds(d2);\n    d2.state.gallery = (d2.state.gallery||[]).filter(im=>im._id !== imageId);\n    d2.refresh();\n    d2.rush('deleteSessionImage').invoke({ imageId }).then(()=>{\n      d2.state.galleryLoadingId = null;\n      this.loadGallery(d2, ids.sessionId);\n    }).catch((e)=>{ d2.log('delImage', e); this.loadGallery(d2, ids.sessionId); });\n  },\n  // Toggle a hero's presence: optimistic flip on d2.state.attendees, then ONE\n  // updateSession write carrying just attendeeHeroIds (the handler merges it\n  // conditionally — title/summary/notes/xp/gold/status are untouched).\n  toggleAttendance(d2, heroId){\n    if(!heroId) return;\n    const ids = this.curIds(d2);\n    if(!ids.sessionId) return;\n    const cur = (d2.state.attendees || []).slice();\n    const i = cur.indexOf(heroId);\n    if(i === -1){ cur.push(heroId); } else { cur.splice(i,1); }\n    d2.state.attendees = cur;\n    d2.rush('updateSession').invoke({\n      sessionId: ids.sessionId,\n      attendeeHeroIds: cur\n    }).then(()=>this.reload(d2)).catch((e)=>{ d2.log('toggleAttendance', e); this.reload(d2); });\n  },\n  // Normalize an enemy into a per-unit kills array (length = qty). Reads the\n  // new `kills` array if present; else migrates a legacy single `killedBy`\n  // (unit 0) into a fresh array. Returns a NEW array (caller-safe to mutate).\n  enemyKills(en){\n    const qty = Math.max(1, parseInt(en && en.qty,10) || 1);\n    const out = new Array(qty).fill('');\n    if(en && Array.isArray(en.kills)){\n      for(let i=0;i<qty;i++){ out[i] = en.kills[i] || ''; }\n    } else if(en && en.killedBy){\n      out[0] = en.killedBy; // legacy: one unit killed by this hero\n    }\n    return out;\n  },\n  // Canonical form-value access. The named store ('sessionLogForm') is NOT\n  // shared across component instances in this runtime, so child fields cannot\n  // talk to the parent through it. We mirror every field into d2.state.form via\n  // each child's onChange opt and read from there (falling back to the store\n  // for safety).\n  fval(d2, field){\n    const f = (d2.state.form||{});\n    return f[field] != null ? f[field] : '';\n  },\n  setForm(d2, field, value){\n    const next = Object.assign({}, d2.state.form||{});\n    next[field] = value;\n    d2.state.form = next;\n  },\n  // Render a parent-OWNED plain input (lives in SessionLog's own shadow root)\n  // so its value is captured via d2.on('#id','input') in mounted — the only\n  // channel that reliably reaches the parent in this runtime. id is the field\n  // name prefixed with 'f-'.\n  gfield(d2, field, label, ph, extra){\n    const v = esc(this.fval(d2, field));\n    const lab = label ? `<div class=\"gf-label\">${esc(label)}</div>` : '';\n    return `<div class=\"gf\">${lab}<input class=\"gf-input\" id=\"f-${esc(field)}\" type=\"text\" placeholder=\"${esc(ph||'')}\" value=\"${v}\" autocomplete=\"off\" ${extra||''} /></div>`;\n  },\n  // ---------- handlers ----------\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2){\n    const ids = this.curIds(d2);\n    const fallback = ids.campaignId ? ('/sessions/'+ids.campaignId) : '/';\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  goPosSessao(d2){\n    const ids = this.curIds(d2);\n    if(ids.sessionId){ this.go(d2, '/possessao/'+ids.sessionId); }\n  },\n\n  // Ensure the d100 crit table for a kind is loaded + cached in d2.state.\n  ensureCrit(d2, kind){\n    if(d2.state['crit_'+kind]) return Promise.resolve(d2.state['crit_'+kind]);\n    if(d2.state['critP_'+kind]) return d2.state['critP_'+kind];\n    const p = d2.rush('critByKind').invoke({ kind }).then((res)=>{\n      const rows = (res && (res.results ?? (res.data && res.data.results))) || [];\n      const byRoll = {};\n      rows.forEach(r=>{ byRoll[Number(r.roll)] = r.effect; });\n      d2.state['crit_'+kind] = byRoll;\n      d2.state['critP_'+kind] = null;\n      return byRoll;\n    }).catch((e)=>{ d2.log('critByKind', e); d2.state['critP_'+kind] = null; return {}; });\n    d2.state['critP_'+kind] = p;\n    return p;\n  },\n  // Roll 1d100 client-side and resolve the matching effect (nearest lower if a gap).\n  critNote(byRoll, d100){\n    let eff = byRoll && byRoll[d100];\n    if(!eff && byRoll){\n      let best = null;\n      Object.keys(byRoll).forEach(k=>{ const n = Number(k); if(n <= d100 && (best===null || n > best)) best = n; });\n      if(best !== null) eff = byRoll[best];\n    }\n    return '(d100=' + d100 + ') ' + (eff || 'efeito desconhecido');\n  },\n  quickEvent(d2, type){\n    const sel = d2.state.selectedHeroId;\n    if(!sel){ d2.dialog.alert('Selecione um herói primeiro.'); return; }\n    // The \"Mestre\" tray pill is only meaningful for nat20/nat1 (submitCrit).\n    // KO/morte/level-up act on a real hero, so ignore a Mestre selection here.\n    if(sel === 'mestre'){ d2.dialog.alert('Selecione um herói (não o Mestre) para esta ação.'); return; }\n    const ids = this.curIds(d2);\n    const hero = this.heroById(d2, sel);\n    const isCrit = (type === 'nat20' || type === 'nat1');\n    const fire = (note)=>{\n      // Optimistic: push the event locally so the placar + feed react to the tap\n      // immediately; reload() reconciles with the server right after.\n      if(d2.state.detail){\n        const evs = (d2.state.detail.events||[]).slice();\n        evs.push({ id:'tmp-'+Date.now(), type, heroName: hero?hero.name:'', note:note||'', createdAt:new Date().toISOString(), _optimistic:true });\n        d2.state.detail = Object.assign({}, d2.state.detail, { events: evs });\n      }\n      d2.rush('addEvent').invoke({\n        sessionId: ids.sessionId, campaignId: ids.campaignId,\n        heroId: sel, heroName: hero?hero.name:'', type, note: note||''\n      }).then(()=>this.reload(d2)).catch((e)=>{ d2.log('addEvent', e); this.reload(d2); });\n    };\n    if(isCrit){\n      const d100 = Math.floor(Math.random()*100)+1;\n      this.ensureCrit(d2, type).then((byRoll)=>{ fire(this.critNote(byRoll, d100)); });\n    } else {\n      fire('');\n    }\n  },\n\n  // Mini-form submit for Nat 20 / Nat 1. Attributes to a present hero OR Mestre.\n  // Mestre -> heroId null, heroName 'Mestre' (heroId is an objectid field, so we\n  // never put the literal 'mestre' string in it). If a d100 (1-100) is filled we\n  // look up the crit effect (nat20 -> acerto crítico, nat1 -> falha crítica) and\n  // store both the effect text in note + the number in rollValue. Otherwise we\n  // just register the crit with the optional context note (skill/social rolls).\n  submitCrit(d2, type){\n    // Roller comes from the hero tray selection (the SAME one the quick-event\n    // buttons use). The tray's \"Mestre\" pill selects the sentinel id 'mestre'.\n    const who = d2.state.selectedHeroId || '';\n    const isMestre = (who === 'mestre');\n    if(!who){ d2.dialog.alert('Selecione quem rolou (um herói ou o Mestre) no tray acima.'); return; }\n    const heroId = isMestre ? null : who;\n    let heroName = 'Mestre';\n    if(!isMestre){\n      const hero = this.heroById(d2, heroId);\n      if(!hero){ d2.dialog.alert('Selecione quem rolou (um herói ou o Mestre) no tray acima.'); return; }\n      heroName = hero.name;\n    }\n    const ids = this.curIds(d2);\n    const rollType = d2.state.critRollType || 'ataque';\n    const context = (this.fval(d2,'critContext')||'').trim();\n    const rollRaw = (this.fval(d2,'critRoll')||'').trim();\n    let d100 = parseInt(rollRaw, 10);\n    if(isNaN(d100)) d100 = null;\n    if(d100 != null){ d100 = Math.max(1, Math.min(100, d100)); }\n\n    const fire = (note, rollValue)=>{\n      // Optimistic: reflect in placar + feed right away, then reconcile.\n      if(d2.state.detail){\n        const evs = (d2.state.detail.events||[]).slice();\n        evs.push({ id:'tmp-'+Date.now(), type, heroName, note:note||'', createdAt:new Date().toISOString(), _optimistic:true });\n        d2.state.detail = Object.assign({}, d2.state.detail, { events: evs });\n      }\n      // Clear the d100 + context fields for the next roll (keep who/rollType).\n      this.setForm(d2,'critRoll','');\n      this.setForm(d2,'critContext','');\n      d2.state.critKey = (d2.state.critKey||0)+1;\n      const payload = { sessionId: ids.sessionId, campaignId: ids.campaignId, heroName, type, rollType, note: note||'' };\n      if(heroId){ payload.heroId = heroId; } // omit for Mestre / grupo (objectid field)\n      if(rollValue != null){ payload.rollValue = rollValue; }\n      d2.rush('addEvent').invoke(payload).then(()=>this.reload(d2)).catch((e)=>{ d2.log('submitCrit', e); this.reload(d2); });\n    };\n\n    if(d100 != null){\n      this.ensureCrit(d2, type).then((byRoll)=>{\n        const eff = this.critNote(byRoll, d100);\n        const note = context ? (context + ' — ' + eff) : eff;\n        fire(note, d100);\n      });\n    } else {\n      fire(context, null);\n    }\n  },\n\n  addFree(d2){\n    const note = (this.fval(d2,'freeText')||'').trim();\n    if(!note){ d2.dialog.alert('Escreva uma nota para o evento.'); return; }\n    let heroId = d2.state.freeHeroSel || d2.state.selectedHeroId || '';\n    if(heroId === 'mestre') heroId = ''; // Mestre isn't a hero — log as grupo\n    const hero = this.heroById(d2, heroId);\n    const ids = this.curIds(d2);\n    if(d2.state.detail){\n      const evs = (d2.state.detail.events||[]).slice();\n      evs.push({ id:'tmp-'+Date.now(), type:'livre', heroName: hero?hero.name:'', note, createdAt:new Date().toISOString(), _optimistic:true });\n      d2.state.detail = Object.assign({}, d2.state.detail, { events: evs });\n    }\n    this.setForm(d2,'freeText','');\n    d2.state.freeKey = (d2.state.freeKey||0)+1;\n    const frPayload = { sessionId: ids.sessionId, campaignId: ids.campaignId, heroName: hero?hero.name:'', type:'livre', note };\n    if(heroId){ frPayload.heroId = heroId; }\n    d2.rush('addEvent').invoke(frPayload).then(()=>{\n      this.reload(d2);\n    }).catch((e)=>{ d2.log('addFree', e); this.reload(d2); });\n  },\n\n  delEvent(d2, eventId){\n    // Optimistic: drop the event from the cached detail and re-render right away\n    // (same pattern as addTreasure), THEN fire deleteEvent in the background —\n    // no full reload/refetch, so the \"×\" feels instant.\n    if(d2.state.detail){\n      const evs = (d2.state.detail.events||[]).filter(e=>(e.id||e._id)!==eventId);\n      d2.state.detail = Object.assign({}, d2.state.detail, { events: evs });\n    }\n    d2.refresh();\n    d2.rush('deleteEvent').invoke({ eventId }).catch((e)=>d2.log('delEvent', e));\n  },\n\n  // ---- enemies builder ----\n  addEnemy(d2){\n    let name = (this.fval(d2,'enemyName')||'').trim();\n    if(!name) name = (d2.state.enemyName||'').trim();\n    if(!name){ d2.dialog.alert('Informe o nome do inimigo.'); return; }\n    const qty = parseInt(this.fval(d2,'enemyQty')||'1',10) || 1;\n    const type = (this.fval(d2,'enemyType')||'').trim();\n    const list = (d2.state.enemies||[]).slice();\n    list.push({ name, qty, type });\n    d2.state.enemies = list;\n    d2.state.enemyName = '';\n    this.setForm(d2,'enemyName','');\n    this.setForm(d2,'enemyQty','');\n    this.setForm(d2,'enemyType','');\n    d2.state.enemyACKey = (d2.state.enemyACKey||0) + 1;\n  },\n  rmEnemy(d2, i){\n    const list = (d2.state.enemies||[]).slice();\n    list.splice(i,1);\n    d2.state.enemies = list;\n  },\n\n  fillEncForm(d2, name, kind, diff, result, xp){\n    this.setForm(d2,'encName',name||'');\n    this.setForm(d2,'encKind',kind||'');\n    this.setForm(d2,'encDiff',diff||'');\n    this.setForm(d2,'encResult',result||'');\n    this.setForm(d2,'encXp',xp!=null?String(xp):'');\n  },\n  editEnc(d2, c){\n    d2.state.editingEncId = c.id;\n    d2.state.enemies = (c.enemies||[]).map(e=>{\n      const o = { name:e.name, qty:e.qty, type:e.type };\n      if(Array.isArray(e.kills)){ o.kills = e.kills.slice(); }\n      else if(e.killedBy){ o.killedBy = e.killedBy; o.killedByName = e.killedByName || ''; }\n      return o;\n    });\n    this.fillEncForm(d2, c.name, c.kind, c.difficulty, c.result, c.xp);\n  },\n  cancelEnc(d2){\n    d2.state.editingEncId = '';\n    d2.state.enemies = [];\n    d2.state.enemyName = '';\n    this.setForm(d2,'enemyName','');\n    d2.state.enemyACKey = (d2.state.enemyACKey||0) + 1;\n    this.fillEncForm(d2,'','','','','');\n  },\n  saveEnc(d2){\n    const name = (this.fval(d2,'encName')||'').trim();\n    if(!name){ d2.dialog.alert('Informe o nome do encontro.'); return; }\n    const ids = this.curIds(d2);\n    const payload = {\n      name,\n      kind: (this.fval(d2,'encKind')||'').trim(),\n      difficulty: (this.fval(d2,'encDiff')||'').trim(),\n      result: (this.fval(d2,'encResult')||'').trim(),\n      xp: parseInt(this.fval(d2,'encXp')||'0',10) || 0,\n      notes: '',\n      enemies: (d2.state.enemies||[]).slice()\n    };\n    const editingId = d2.state.editingEncId;\n    const done = ()=>{ this.cancelEnc(d2); this.reload(d2); };\n    if(editingId){\n      d2.rush('updateEncounter').invoke(Object.assign({ encounterId: editingId }, payload)).then(done).catch((e)=>d2.log('updateEnc', e));\n    } else {\n      d2.rush('addEncounter').invoke(Object.assign({ sessionId: ids.sessionId, campaignId: ids.campaignId }, payload)).then(done).catch((e)=>d2.log('addEnc', e));\n    }\n  },\n  delEnc(d2, encounterId){\n    d2.rush('deleteEncounter').invoke({ encounterId }).then(()=>this.reload(d2)).catch((e)=>d2.log('delEnc', e));\n  },\n\n  // ---- live combat from an encounter ----\n  // Look up a monster's catalog HP by name (searchMonsters returns namePt||name).\n  // Cached per distinct name in d2.state so a group of N units triggers ONE read.\n  // Returns a non-zero hp (catalog value, else the 10 fallback) — a 0-HP enemy\n  // renders as already \"down\" on the CombatBoard with no way to raise its max.\n  monsterHp(d2, name){\n    const key = String(name||'').trim().toLowerCase();\n    if(!d2.state.monsterHpCache) d2.state.monsterHpCache = {};\n    if(d2.state.monsterHpCache[key] != null) return Promise.resolve(d2.state.monsterHpCache[key]);\n    return d2.rush('searchMonsters').invoke({ q: name }).then((raw)=>{\n      const res = (raw && raw.data) ? raw.data : raw;\n      const list = (res && res.results) || [];\n      // Prefer an exact (case-insensitive) name match; else first result.\n      let m = list.find(r=>String(r.name||'').trim().toLowerCase() === key) || list[0];\n      const hp = (m && Number(m.hp)) ? Number(m.hp) : 10;\n      d2.state.monsterHpCache[key] = hp;\n      return hp;\n    }).catch((e)=>{ d2.log('monsterHp', e); d2.state.monsterHpCache[key] = 10; return 10; });\n  },\n  // Build the combatant list (present heroes + enemy groups expanded to units),\n  // create the combat via startCombat (ONE write), then navigate to /combat/<id>.\n  async startCombatFromEnc(d2, c){\n    if(d2.state.startingCombatId) return;\n    const ids = this.curIds(d2);\n    if(!ids.sessionId){ d2.dialog.alert('Sessão não carregada.'); return; }\n\n    // Anti-duplicate: if loadCombats already mapped a (non-ended, most-recent)\n    // combat for this encounter, RESUME it instead of inserting a new zeroed doc.\n    // startCombat always creates a fresh record, so without this guard a second\n    // tap (or a tap before the button relabels to \"Editar\") spawns a duplicate.\n    const existing = (d2.state.combatByEnc || {})[c.id];\n    if(existing){ this.go(d2, '/combat/' + existing); return; }\n\n    // Present heroes: campHeroes filtered by attendees. These carry hpMax/hpAtual.\n    const present = (d2.state.attendees || []).slice();\n    const byId = {};\n    (d2.state.campHeroes || []).forEach(h=>{ byId[h.id] = h; });\n    const heroCombatants = present.map(hid=>{\n      const h = byId[hid];\n      if(!h) return null;\n      const hpMax = Number(h.hpMax) || 0;\n      const hpAtual = (h.hpAtual!=null && h.hpAtual!=='') ? Number(h.hpAtual) : hpMax;\n      return { id:'h_'+h.id, side:'hero', refId:h.id, name:h.name, initiative:0,\n        hpMax, hpAtual, ca:(Number(h.ca)||10), conditions:[], down:false };\n    }).filter(Boolean);\n\n    if(heroCombatants.length === 0){\n      d2.dialog.alert('Marque ao menos um herói como presente para iniciar o combate.');\n      return;\n    }\n\n    d2.state.startingCombatId = c.id;\n    d2.refresh();\n\n    try {\n      // Expand enemy groups into individual units, sourcing HP from the catalog\n      // (cached per distinct name so it's one read per group, not per unit).\n      const enemyCombatants = [];\n      let ei = 0;\n      for(const grp of (c.enemies || [])){\n        const base = (grp && grp.name) ? String(grp.name) : 'Inimigo';\n        const qty = Math.max(1, parseInt(grp && grp.qty, 10) || 1);\n        const hp = await this.monsterHp(d2, base);\n        for(let n=1; n<=qty; n++){\n          enemyCombatants.push({ id:'e_'+ei, side:'enemy', refId:null,\n            name: base + ' #' + n, initiative:0, hpMax:hp, hpAtual:hp,\n            ca:10, conditions:[], down:false });\n          ei++;\n        }\n      }\n\n      const combatants = heroCombatants.concat(enemyCombatants);\n      const raw = await d2.rush('startCombat').invoke({\n        sessionId: ids.sessionId,\n        campaignId: ids.campaignId,\n        encounterId: c.id,\n        combatants\n      });\n      const res = (raw && raw.data) ? raw.data : raw;\n      const combatId = res && res.combat && res.combat._id;\n      d2.state.startingCombatId = null;\n      if(combatId){\n        this.go(d2, '/combat/' + combatId);\n      } else {\n        d2.refresh();\n        d2.dialog.alert('Não foi possível iniciar o combate.');\n      }\n    } catch(e){\n      d2.log('startCombatFromEnc', e);\n      d2.state.startingCombatId = null;\n      d2.refresh();\n      d2.dialog.alert('Falha ao iniciar o combate.');\n    }\n  },\n\n  // ---- treasures ----\n  addTreasure(d2){\n    let name = (this.fval(d2,'treasureName')||'').trim();\n    if(!name) name = (d2.state.treasureName||'').trim();\n    if(!name){ d2.dialog.alert('Informe o nome do tesouro.'); return; }\n    const ids = this.curIds(d2);\n    const value = parseInt(this.fval(d2,'trsValue')||'0',10) || 0;\n    const kind = d2.state.trsKind || 'item';\n    const heroId = d2.state.trsHeroSel || '';\n    const hero = this.heroById(d2, heroId);\n    // Optimistic: reflect the new treasure immediately (placar \"Tesouro\" + list).\n    if(d2.state.detail){\n      const trs = (d2.state.detail.treasures||[]).slice();\n      trs.push({ id:'tmp-'+Date.now(), name, kind, value, heroName: hero?hero.name:'', note:'', _optimistic:true });\n      d2.state.detail = Object.assign({}, d2.state.detail, { treasures: trs });\n    }\n    d2.state.treasureName = '';\n    this.setForm(d2,'treasureName','');\n    this.setForm(d2,'trsValue','');\n    d2.state.trsACKey = (d2.state.trsACKey||0) + 1;\n    // Only send heroId when a hero is actually selected — the server casts it to\n    // ObjectId and rejects an empty string (\"— grupo —\" loot has no hero).\n    const trsPayload = { sessionId: ids.sessionId, campaignId: ids.campaignId, name, kind, value, heroName: hero?hero.name:'' };\n    if(heroId){ trsPayload.heroId = heroId; }\n    d2.rush('addTreasure').invoke(trsPayload).then(()=>{\n      this.reload(d2);\n    }).catch((e)=>{ d2.log('addTreasure', e); this.reload(d2); });\n  },\n  delTreasure(d2, treasureId){\n    d2.rush('deleteTreasure').invoke({ treasureId }).then(()=>this.reload(d2)).catch((e)=>d2.log('delTreasure', e));\n  },\n\n  // Distribui o XP da sessão igualmente entre os PRESENTES (d2.state.attendees).\n  // SEQUENCIAL (for...of + await) — Write em tabela registry NUNCA em paralelo/loop\n  // concorrente (deadlock). Cada herói: novoXp = xpAtual + share; novoLvl recalculado.\n  async distribuirXp(d2){\n    if(d2.state.distributingXp) return;\n    const ids = this.curIds(d2);\n    if(!ids.sessionId){ d2.dialog.alert('Sessão não carregada.'); return; }\n    const session = (d2.state.detail && d2.state.detail.session) || {};\n    const xpSessao = parseInt(this.fval(d2,'xp')||'0',10) || 0;\n    const present = (d2.state.attendees || []).slice();\n    if(xpSessao <= 0){ d2.dialog.alert('Defina o XP da sessão (maior que zero) antes de distribuir.'); return; }\n    if(present.length === 0){ d2.dialog.alert('Marque ao menos um herói como presente para distribuir o XP.'); return; }\n    // Histórico atual — usado para detectar uma distribuição anterior e desfazê-la.\n    const xpLogAtual = (d2.state.detail && d2.state.detail.session && Array.isArray(d2.state.detail.session.xpLog))\n      ? d2.state.detail.session.xpLog : [];\n    // Já distribuído = flag xpAwarded ligada E existe ao menos uma entrada no log.\n    // Nesse caso CORRIGIMOS (substituímos) em vez de empilhar.\n    const jaDistribuido = !!session.xpAwarded && xpLogAtual.length > 0;\n    if(jaDistribuido){\n      const ok = await d2.dialog.confirm('Esta sessão já teve XP distribuído. Vou CORRIGIR substituindo a distribuição anterior pelos valores atuais. Continuar?');\n      if(!ok) return;\n    }\n    const share = Math.floor(xpSessao / present.length);\n    if(share <= 0){ d2.dialog.alert('XP insuficiente para ratear entre os presentes.'); return; }\n\n    // Share da última distribuição (para desfazer antes de aplicar a nova).\n    // Best-effort: subtrai do conjunto de presentes ATUAL (d2.state.attendees).\n    const ultimaEntrada = jaDistribuido ? xpLogAtual[xpLogAtual.length - 1] : null;\n    const shareAnterior = (ultimaEntrada && Number(ultimaEntrada.share)) || 0;\n\n    const roster = d2.state.campHeroes || [];\n    const byId = {};\n    roster.forEach(h=>{ byId[h.id] = h; });\n\n    d2.state.distributingXp = true;\n    d2.refresh();\n\n    const levelUps = [];\n    const failed = [];\n    // SEQUENCIAL — um por vez.\n    for(const heroId of present){\n      const h = byId[heroId];\n      if(!h){ failed.push(heroId); continue; }\n      const xpAtual = Number(h.xp) || 0;\n      const lvlAntes = levelFromXp(xpAtual);\n      // Substituição: desfaz o share anterior, depois aplica o novo. Clamp em 0.\n      const novoXp = Math.max(0, xpAtual - shareAnterior + share);\n      const novoLvl = levelFromXp(novoXp);\n      try {\n        await d2.rush('updateHero').invoke({ heroId, xp: novoXp, level: novoLvl });\n        if(novoLvl > lvlAntes){ levelUps.push((h.name||'Herói') + ' → nível ' + novoLvl); }\n      } catch(e){\n        d2.log('distribuirXp hero', heroId, e);\n        failed.push(h.name || heroId);\n      }\n    }\n\n    // UMA entrada corrigida no histórico (substitui a última se já distribuído;\n    // senão acrescenta). Nunca duplica.\n    const novaEntrada = {\n      at: new Date().toISOString(),\n      total: xpSessao,\n      share,\n      n: present.length,\n      ups: levelUps.slice()\n    };\n    const novoXpLog = jaDistribuido\n      ? xpLogAtual.slice(0, -1).concat([novaEntrada])\n      : xpLogAtual.concat([novaEntrada]);\n    // Otimista: reflete o histórico imediatamente.\n    if(d2.state.detail && d2.state.detail.session){\n      d2.state.detail = Object.assign({}, d2.state.detail, {\n        session: Object.assign({}, d2.state.detail.session, { xpAwarded: true, xpLog: novoXpLog })\n      });\n    }\n    // Marca a sessão como XP distribuído + histórico (sequencial, depois dos heróis).\n    try {\n      await d2.rush('updateSession').invoke({ sessionId: ids.sessionId, xpAwarded: true, xpLog: novoXpLog });\n    } catch(e){\n      d2.log('distribuirXp updateSession', e);\n    }\n\n    d2.state.distributingXp = false;\n    // Recarrega roster + detalhe (força refetch do campHeroes).\n    d2.state.campHeroesCid = null;\n    this.reload(d2);\n    d2.refresh();\n\n    let msg = jaDistribuido\n      ? ('Distribuição corrigida: cada herói presente ficou com ' + share + ' XP desta sessão.')\n      : ('Cada herói presente recebeu ' + share + ' XP.');\n    if(levelUps.length){ msg += '\\n\\nSubiram de nível:\\n• ' + levelUps.join('\\n• '); }\n    else { msg += '\\n\\nNinguém subiu de nível.'; }\n    if(failed.length){ msg += '\\n\\nFalha em: ' + failed.join(', '); }\n    d2.dialog.alert(msg);\n  },\n\n  // Persist apenas os campos in-game (Dia/Horário) — UM write, sem reload (o\n  // valor já está no form; reload causaria cursor jump no campo seguinte).\n  saveInGame(d2){\n    const ids = this.curIds(d2);\n    if(!ids.sessionId) return;\n    if(d2.state.detail && d2.state.detail.session){\n      d2.state.detail = Object.assign({}, d2.state.detail, {\n        session: Object.assign({}, d2.state.detail.session, {\n          inGameDate: this.fval(d2,'inGameDate')||'',\n          inGameTime: this.fval(d2,'inGameTime')||''\n        })\n      });\n    }\n    d2.rush('updateSession').invoke({\n      sessionId: ids.sessionId,\n      inGameDate: this.fval(d2,'inGameDate')||'',\n      inGameTime: this.fval(d2,'inGameTime')||''\n    }).catch((e)=>d2.log('saveInGame', e));\n  },\n\n  saveSession(d2){\n    const ids = this.curIds(d2);\n    const cur = (d2.state.detail && d2.state.detail.session) || {};\n    d2.rush('updateSession').invoke({\n      sessionId: ids.sessionId,\n      xp: parseInt(this.fval(d2,'xp')||'0',10) || 0,\n      gold: parseInt(this.fval(d2,'gold')||'0',10) || 0,\n      inGameDate: this.fval(d2,'inGameDate')||'',\n      inGameTime: this.fval(d2,'inGameTime')||'',\n      status: cur.status || 'em andamento'\n    }).then(()=>this.reload(d2)).catch((e)=>d2.log('saveSession', e));\n  },\n\n  // ---- general notes (list) ----\n  addNote(d2){\n    const text = (d2.state.noteDraft||'').trim();\n    if(!text){ d2.dialog.alert('Escreva uma anotação.'); return; }\n    const ids = this.curIds(d2);\n    const author = (this.curUser && this.curUser(d2)) || 'Mestre';\n    d2.rush('addNote').invoke({\n      sessionId: ids.sessionId, campaignId: ids.campaignId, text, author\n    }).then(()=>{\n      d2.state.noteDraft = '';\n      const el = d2.$('#noteTA'); if(el){ el.value = ''; }\n      this.reload(d2);\n    }).catch((e)=>d2.log('addNote', e));\n  },\n  delNote(d2, noteId){\n    d2.rush('deleteNote').invoke({ noteId }).then(()=>this.reload(d2)).catch((e)=>d2.log('delNote', e));\n  },\n\n  toggleStatus(d2){\n    const ids = this.curIds(d2);\n    const cur = (d2.state.detail && d2.state.detail.session) || {};\n    const next = (cur.status === 'encerrada') ? 'em andamento' : 'encerrada';\n    d2.rush('updateSession').invoke({\n      sessionId: ids.sessionId,\n      xp: parseInt(this.fval(d2,'xp')||'0',10) || (cur.xp||0),\n      gold: parseInt(this.fval(d2,'gold')||'0',10) || (cur.gold||0),\n      status: next\n    }).then(()=>this.reload(d2)).catch((e)=>d2.log('toggleStatus', e));\n  }\n}"}},"sessionsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionsDataset"}}}}}}}}}},{"_id":"6a32e1ce491645141ab5aa71","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"CampaignDetailView","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320' };\nconst XP_LEVELS = [0,300,900,2700,6500,14000,23000,34000,48000,64000,85000,100000,120000,140000,165000,195000,225000,265000,305000,355000];\nfunction levelFromXp(xp){ const v = Number(xp)||0; let L = 1; for(let i=1;i<=20;i++){ if(XP_LEVELS[i-1] <= v) L = i; } return L; }\nconst DRIVE_REF = 'cronicaDrive';\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .wrap{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:14px; padding:4px; font-family:'Alegreya',sans-serif; background:#1c140d; }\n  .back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .back:active{ transform:translateY(1px); }\n  .hdr{ box-sizing:border-box; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .hdr-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:30px; letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1.1; margin-bottom:6px; }\n  .hdr-name-row{ display:flex; align-items:flex-start; gap:10px; }\n  .hdr-name-row .hdr-name{ margin-bottom:6px; }\n  .hdr-pencil{ cursor:pointer; flex:0 0 auto; color:${PAL.gold}; font-size:18px; line-height:1; padding:4px 7px; border:2px solid ${PAL.goldDeep}; border-radius:4px; background:rgba(216,178,76,0.08); }\n  .hdr-pencil:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .hdr-input{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:18px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px; margin-bottom:8px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .hdr-input:focus{ border-color:${PAL.goldDeep}; }\n  .hdr-input::placeholder{ color:${PAL.cream}; opacity:0.45; }\n  .hdr-err{ color:${PAL.blood}; font-family:'Alegreya',sans-serif; font-size:13px; margin-bottom:6px; min-height:14px; }\n  .hdr-edit-actions{ display:flex; gap:10px; }\n  .hdr-btn{ flex:1; text-align:center; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:15px; letter-spacing:1px;\n    padding:9px 12px; border:3px solid ${PAL.edgeDark}; border-radius:4px; transition:filter 140ms ease, transform 100ms ease; }\n  .hdr-btn:active{ transform:translateY(2px); }\n  .hdr-save{ color:${PAL.edgeDark}; background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%); box-shadow: inset 2px 2px 0 rgba(255,255,255,0.4), 0 3px 0 #7a5a18; }\n  .hdr-save:hover{ filter:brightness(1.1); }\n  .hdr-cancel{ color:${PAL.cream}; background:${PAL.stone1}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; }\n  .hdr-cancel:hover{ filter:brightness(1.12); }\n  .hdr-disabled{ opacity:0.6; pointer-events:none; }\n  .hdr-setting{ color:${PAL.cream}; font-size:14px; opacity:0.85; margin-bottom:8px; }\n  .hdr-meta{ display:flex; gap:8px; flex-wrap:wrap; }\n  .pill{ font-size:11px; letter-spacing:1px; padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .pill.status{ background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .pill.role{ background:${PAL.gold}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .sec-title{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:2px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin:2px 0; }\n  .member{ box-sizing:border-box; padding:12px 14px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark}; box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55); margin-bottom:10px; }\n  .member-name{ color:${PAL.goldLight}; font-size:17px; margin-bottom:6px; }\n  .member-meta{ display:flex; gap:6px; flex-wrap:wrap; }\n  .ver-ficha{ margin-top:10px; cursor:pointer; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:1px;\n    color:${PAL.edgeDark}; background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:2px solid ${PAL.edgeDark}; border-radius:4px; padding:6px 12px;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 2px 0 #7a5a18; }\n  .ver-ficha:hover{ filter:brightness(1.1); }\n  .hero-card-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:24px; letter-spacing:1px; text-shadow:1px 1px 0 ${PAL.edgeDark}; line-height:1.1; margin-bottom:8px; }\n  .hero-ava{ width:46px; height:46px; flex:0 0 auto; border-radius:6px; border:3px solid ${PAL.goldDeep}; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone2},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center; }\n  .hero-ava img{ width:100%; height:100%; object-fit:cover; }\n  .hero-ava .ini{ font-family:'Jacquard 24',serif; font-size:20px; color:${PAL.goldLight}; }\n  .hero-top{ display:flex; align-items:center; gap:12px; }\n  .hero-sec-head{ display:flex; align-items:center; justify-content:space-between; gap:10px; }\n  .cd-hero-create{ cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:1px;\n    color:${PAL.edgeDark}; background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:2px solid ${PAL.edgeDark}; border-radius:4px; padding:7px 12px;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 2px 0 #7a5a18; }\n  .cd-hero-create:hover{ filter:brightness(1.1); }\n  .cd-hinput{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:15px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px; margin-bottom:8px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .cd-hinput:focus{ border-color:${PAL.goldDeep}; }\n  .cd-hinput::placeholder{ color:${PAL.cream}; opacity:0.45; }\n  .cd-hxp{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:15px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px; margin-bottom:8px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .cd-hxp:focus{ border-color:${PAL.goldDeep}; }\n  .cd-hxp::placeholder{ color:${PAL.cream}; opacity:0.45; }\n  .cd-hxplbl{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:11px; letter-spacing:1px; margin-bottom:4px; }\n  .cd-hactions{ display:flex; gap:10px; }\n  .cd-hbtn{ flex:1; text-align:center; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:15px; letter-spacing:1px;\n    padding:9px 12px; border:3px solid ${PAL.edgeDark}; border-radius:4px; transition:filter 140ms ease, transform 100ms ease; }\n  .cd-hbtn:active{ transform:translateY(2px); }\n  .cd-hsave{ color:${PAL.edgeDark}; background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%); box-shadow: inset 2px 2px 0 rgba(255,255,255,0.4), 0 3px 0 #7a5a18; }\n  .cd-hsave:hover{ filter:brightness(1.1); }\n  .cd-hcancel{ color:${PAL.cream}; background:${PAL.stone1}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; }\n  .cd-hcancel:hover{ filter:brightness(1.12); }\n  .cd-hdisabled{ opacity:0.6; pointer-events:none; }\n  .empty{ color:${PAL.cream}; opacity:0.7; font-size:14px; text-align:center; padding:18px 12px; }\n  /* === CANONICAL: GoldenPanel stone === */\n  .panel{ box-sizing:border-box; padding:18px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  /* === CANONICAL: GoldenField label === */\n  .panel-title{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:2px; text-shadow:1px 1px 0 ${PAL.edgeDark}; margin-bottom:10px; }\n  .roles{ display:flex; gap:8px; margin:10px 0; }\n  .role-btn{ flex:1; cursor:pointer; padding:8px; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:1px;\n    border:3px solid ${PAL.edgeDark}; border-radius:4px; background:${PAL.stone1}; color:${PAL.cream};\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; transition:filter 140ms ease; }\n  .role-btn:hover{ filter:brightness(1.12); }\n  .role-btn.on{ background:${PAL.gold}; color:${PAL.edgeDark}; font-weight:600;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .err{ color:${PAL.blood}; font-size:13px; margin-bottom:8px; min-height:16px; }\n  .loading{ color:${PAL.gold}; font-size:14px; text-align:center; padding:24px; }\n  /* === Mestre section === */\n  .mestre-row{ display:flex; gap:14px; align-items:center; }\n  .mestre-ava{ width:64px; height:64px; flex:0 0 auto; border-radius:6px; border:3px solid ${PAL.goldDeep}; overflow:hidden;\n    background:linear-gradient(180deg,${PAL.stone2},${PAL.edgeDark}); display:flex; align-items:center; justify-content:center; }\n  .mestre-ava img{ width:100%; height:100%; object-fit:cover; }\n  .mestre-ava .ini{ font-family:'Jacquard 24',serif; font-size:26px; color:${PAL.goldLight}; }\n  .mestre-ctl{ flex:1; min-width:0; display:flex; flex-direction:column; gap:8px; }\n  .mestre-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:22px; letter-spacing:1px; text-shadow:1px 1px 0 ${PAL.edgeDark}; line-height:1.1; }\n  .mestre-name-row{ display:flex; align-items:center; gap:10px; }\n  .mestre-pencil{ cursor:pointer; flex:0 0 auto; color:${PAL.gold}; font-size:15px; line-height:1; padding:3px 6px; border:2px solid ${PAL.goldDeep}; border-radius:4px; background:rgba(216,178,76,0.08); }\n  .mestre-pencil:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .mestre-ninput{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:15px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:8px 10px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .mestre-ninput:focus{ border-color:${PAL.goldDeep}; }\n  .mestre-nactions{ display:flex; gap:10px; }\n  .mestre-nbtn{ flex:1; text-align:center; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    padding:8px 10px; border:3px solid ${PAL.edgeDark}; border-radius:4px; transition:filter 140ms ease, transform 100ms ease; }\n  .mestre-nbtn:active{ transform:translateY(2px); }\n  .mestre-nsave{ color:${PAL.edgeDark}; background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%); box-shadow: inset 2px 2px 0 rgba(255,255,255,0.4), 0 3px 0 #7a5a18; }\n  .mestre-nsave:hover{ filter:brightness(1.1); }\n  .mestre-ncancel{ color:${PAL.cream}; background:${PAL.stone1}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; }\n  .mestre-ncancel:hover{ filter:brightness(1.12); }\n  .mestre-disabled{ opacity:0.6; pointer-events:none; }\n  .mestre-upload{ align-self:flex-start; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:12px; letter-spacing:1px;\n    color:${PAL.edgeDark}; background:linear-gradient(180deg,${PAL.goldLight},${PAL.goldDeep}); border:2px solid ${PAL.edgeDark}; border-radius:4px; padding:7px 12px;\n    box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4), 0 2px 0 #7a5a18; }\n  .mestre-upload:hover{ filter:brightness(1.1); }\n  .mestre-fileinput{ display:none; }\n  `,\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.load(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.back','click',()=>{ try{ d2.emit('onBack'); }catch(_){} this.goBack(d2, '/'); });\n    d2.on('.ver-ficha','click',(e)=>{ e.stopPropagation(); const hid = e.target.closest('.ver-ficha').getAttribute('data-hero'); if(hid){ this.go(d2, '/hero/' + hid); } });\n    // === Heróis (party por campanha) ===\n    d2.on('.cd-hero-create','click',()=>{ d2.state.creatingHero = true; d2.state.cdHeroErr = ''; d2.state.cdHeroName = ''; d2.state.cdHeroXp = ''; try{ d2.refresh(); }catch(_){} });\n    d2.on('.cd-hcancel','click',()=>{ d2.state.creatingHero = false; d2.state.cdHeroErr = ''; d2.state.cdHeroXp = ''; try{ d2.refresh(); }catch(_){} });\n    d2.on('.cd-hinput','input',(e)=>{ d2.state.cdHeroName = e.target.value; });\n    d2.on('.cd-hxp','input',(e)=>{ d2.state.cdHeroXp = e.target.value; });\n    d2.on('.cd-hxp','keydown',(e)=>{ if(e.key==='Enter'){ e.preventDefault(); this.createHero(d2); } });\n    d2.on('.cd-hinput','keydown',(e)=>{ if(e.key==='Enter'){ e.preventDefault(); this.createHero(d2); } });\n    d2.on('.cd-hsave','click',()=>{ this.createHero(d2); });\n    // rename campaign (JS, no Store)\n    d2.on('.hdr-pencil','click',()=>{\n      const c = d2.state.campaign || {};\n      d2.state.eform = { name: c.name || '', setting: c.setting || '' };\n      d2.state.eerror = '';\n      d2.state.editingName = true;\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.hdr-input','input',(e)=>{\n      const k = e.target.getAttribute('data-k');\n      if (!k) return;\n      if (!d2.state.eform) d2.state.eform = {};\n      d2.state.eform[k] = e.target.value;\n    });\n    d2.on('.hdr-cancel','click',()=>{\n      d2.state.editingName = false;\n      d2.state.eerror = '';\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.hdr-save','click',()=>{ this.saveName(d2); });\n    // === Mestre ===\n    d2.on('.mestre-upload','click',()=>{ if(!d2.state.mestreUploading){ const fi=d2.$('.mestre-fileinput'); if(fi) fi.click(); } });\n    d2.on('.mestre-fileinput','change',(e)=>{ this.onMestreFilePicked(d2, e.target); });\n    d2.on('.mestre-pencil','click',()=>{\n      const c = d2.state.campaign || {};\n      d2.state.mestreNameDraft = (c.mestreName != null ? c.mestreName : 'Mestre');\n      d2.state.mestreErr = '';\n      d2.state.editingMestreName = true;\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.mestre-ninput','input',(e)=>{ d2.state.mestreNameDraft = e.target.value; });\n    d2.on('.mestre-ncancel','click',()=>{ d2.state.editingMestreName = false; d2.state.mestreErr = ''; try{ d2.refresh(); }catch(_){} });\n    d2.on('.mestre-ninput','keydown',(e)=>{ if(e.key==='Enter'){ e.preventDefault(); this.saveMestreName(d2); } });\n    d2.on('.mestre-nsave','click',()=>{ this.saveMestreName(d2); });\n  },\n  async onMestreFilePicked(d2, input){\n    const files = input && input.files;\n    if (!files || !files.length) return;\n    const id = this.campaignId(d2);\n    if (!id){ d2.state.mestreErr = 'Sem campanha na rota'; try{ d2.refresh(); }catch(_){}; return; }\n    d2.state.mestreUploading = true;\n    try { d2.refresh(); } catch(_){}\n    try {\n      const out = await d2.drive(DRIVE_REF).uploadFromFiles([files[0]]);\n      const url = out && out[0] && (out[0].fileUrl || (out[0].params && out[0].params.fileUrl));\n      if (url){\n        // optimistic\n        d2.state.campaign = Object.assign({}, d2.state.campaign||{}, { mestrePortrait: url });\n        d2.state.mestreUploading = false;\n        if (input) input.value = '';\n        try { d2.refresh(); } catch(_){}\n        try {\n          const res = await d2.rush('updateCampaign').invoke({ campaignId: id, mestrePortrait: url });\n          const ok = res && (res.ok || (res.data && res.data.ok));\n          if (!ok) d2.state.mestreErr = 'Falha ao salvar imagem';\n        } catch(e){\n          d2.state.mestreErr = 'Falha ao salvar imagem';\n        }\n        try { d2.refresh(); } catch(_){}\n        return;\n      }\n      d2.state.mestreErr = 'Upload sem URL';\n    } catch(e){\n      d2.state.mestreErr = 'Falha no upload da imagem';\n    }\n    d2.state.mestreUploading = false;\n    if (input) input.value = '';\n    try { d2.refresh(); } catch(_){}\n  },\n  async saveMestreName(d2){\n    if (this._savingMestre) return;\n    const id = this.campaignId(d2);\n    const name = (d2.state.mestreNameDraft || '').trim() || 'Mestre';\n    if (!id){ d2.state.mestreErr = 'Sem campanha na rota'; try{ d2.refresh(); }catch(_){}; return; }\n    this._savingMestre = true;\n    d2.state.mestreSaving = true;\n    d2.state.mestreErr = '';\n    // optimistic\n    d2.state.campaign = Object.assign({}, d2.state.campaign||{}, { mestreName: name });\n    try { d2.refresh(); } catch(_){}\n    try {\n      const res = await d2.rush('updateCampaign').invoke({ campaignId: id, mestreName: name });\n      const ok = res && (res.ok || (res.data && res.data.ok));\n      if (!ok){\n        d2.state.mestreErr = 'Falha ao salvar';\n      } else {\n        d2.state.editingMestreName = false;\n      }\n    } catch(e){\n      d2.state.mestreErr = String(e && e.message ? e.message : e);\n    }\n    d2.state.mestreSaving = false;\n    this._savingMestre = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  async saveName(d2){\n    if (this._savingName) return;\n    const id = this.campaignId(d2);\n    const ef = d2.state.eform || {};\n    const name = (ef.name || '').trim();\n    if (!name){ d2.state.eerror = 'Informe um nome'; try{ d2.refresh(); }catch(_){}; return; }\n    if (!id){ d2.state.eerror = 'Sem campanha na rota'; try{ d2.refresh(); }catch(_){}; return; }\n    this._savingName = true;\n    d2.state.esaving = true;\n    d2.state.eerror = '';\n    try { d2.refresh(); } catch(_){}\n    try {\n      const res = await d2.rush('updateCampaign').invoke({ campaignId: id, name: name, setting: (ef.setting||'').trim() });\n      const ok = res && (res.ok || (res.data && res.data.ok));\n      if (!ok){\n        d2.state.eerror = 'Falha ao salvar';\n      } else {\n        d2.state.editingName = false;\n        await this.load(d2);\n      }\n    } catch(e){\n      d2.state.eerror = String(e && e.message ? e.message : e);\n    }\n    d2.state.esaving = false;\n    this._savingName = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  campaignId(d2){\n    let id = null;\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/campaign\\/([^\\/?#]+)/);\n      if (_m) id = decodeURIComponent(_m[1]);\n    } catch(_){}\n    if (!id) { try { const p = d2.router().params; if (p && p.id) id = p.id; } catch(_){} }\n    return id;\n  },\n  // first campaign from the bound dataset (preview fallback when no route :id)\n  fallbackCampaign(d2){\n    try {\n      const h = d2.data('campaignsDataset');\n      if (!h) return null;\n      const recs = h.records() || [];\n      const rid = this.campaignId(d2);\n      if (rid){ const m = recs.find(r => String(r._id||r.id) === String(rid)); if (m) return m; }\n      return recs.length ? recs[0] : null;\n    } catch(_){ return null; }\n  },\n  async load(d2){\n    if (this._loading) return;\n    const routeId = this.campaignId(d2);\n    if (routeId){\n      d2.state.noId = false;\n      this._loading = true;\n      d2.state.loading = true;\n      try {\n        const det = await d2.rush('campaignDetail').invoke({ campaignId: routeId });\n        const detData = (det && det.data) ? det.data : det;\n        d2.state.campaign = (detData && detData.campaign) ? detData.campaign : null;\n        d2.state.myRole = detData ? detData.myRole : null;\n        const her = await d2.rush('heroesByCampaign').invoke({ campaignId: routeId });\n        const herData = (her && her.data) ? her.data : her;\n        d2.state.heroes = (herData && herData.results) ? herData.results : [];\n      } catch(e){\n        d2.state.error = String(e && e.message ? e.message : e);\n      }\n      d2.state.loading = false;\n      this._loading = false;\n    } else {\n      // no route :id (preview) → dataset fallback for visualization\n      d2.state.noId = false;\n      d2.state.usingFallback = true;\n    }\n  },\n  async createHero(d2){\n    if (this._creatingHero) return;\n    const id = this.campaignId(d2);\n    const name = (d2.state.cdHeroName || '').trim();\n    if (!name){ d2.state.cdHeroErr = 'Informe o nome'; try{ d2.refresh(); }catch(_){}; return; }\n    if (!id){ d2.state.cdHeroErr = 'Sem campanha na rota'; try{ d2.refresh(); }catch(_){}; return; }\n    this._creatingHero = true;\n    d2.state.cdHeroSaving = true;\n    d2.state.cdHeroErr = '';\n    try { d2.refresh(); } catch(_){}\n    const xp = parseInt(d2.state.cdHeroXp||'0',10)||0;\n    const level = levelFromXp(xp);\n    try {\n      const res = await d2.rush('createHero').invoke({ campaignId: id, name: name, xp: xp, level: level });\n      const data = (res && res.data) ? res.data : res;\n      const ok = data && data.ok;\n      const newId = data && data.id;\n      if (ok && newId){\n        d2.state.creatingHero = false;\n        d2.state.cdHeroName = '';\n        d2.state.cdHeroXp = '';\n        d2.state.cdHeroSaving = false;\n        this._creatingHero = false;\n        this.go(d2, '/hero/' + newId);\n        return;\n      }\n      d2.state.cdHeroErr = 'Falha ao criar herói';\n    } catch(e){\n      d2.state.cdHeroErr = String(e && e.message ? e.message : e);\n    }\n    d2.state.cdHeroSaving = false;\n    this._creatingHero = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  render(d2){\n    const self = this;\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    const back = `<button class=\"back\">‹ Voltar</button>`;\n    if (d2.state.noId) return `<div class=\"wrap\">${back}<div class=\"empty\">Campanha não encontrada</div></div>`;\n\n    // resolve campaign from session state only\n    let c = d2.state.campaign;\n    let myRole = d2.state.myRole;\n\n    if (d2.state.loading && !c) return `<div class=\"wrap\">${back}<div class=\"loading\">Carregando…</div></div>`;\n    if (!c) return `<div class=\"wrap\">${back}<div class=\"empty\">Campanha não encontrada</div></div>`;\n    c = c || {};\n\n    const settingLine = c.setting ? `<div class=\"hdr-setting\">${esc(c.setting)}</div>` : '';\n    let header;\n    if (d2.state.editingName){\n      const ef = d2.state.eform || {};\n      const eErr = d2.state.eerror ? `<div class=\"hdr-err\">${esc(d2.state.eerror)}</div>` : '';\n      const esaving = !!d2.state.esaving;\n      header = `<div class=\"hdr\">\n        ${eErr}\n        <input class=\"hdr-input\" data-k=\"name\" type=\"text\" placeholder=\"nome da campanha\" value=\"${esc(ef.name != null ? ef.name : (c.name||''))}\" />\n        <input class=\"hdr-input\" data-k=\"setting\" type=\"text\" placeholder=\"cenário\" value=\"${esc(ef.setting != null ? ef.setting : (c.setting||''))}\" />\n        <div class=\"hdr-edit-actions\">\n          <div class=\"hdr-btn hdr-save ${esaving?'hdr-disabled':''}\">${esaving?'Salvando…':'Salvar'}</div>\n          <div class=\"hdr-btn hdr-cancel\">Cancelar</div>\n        </div>\n      </div>`;\n    } else {\n      header = `<div class=\"hdr\">\n        <div class=\"hdr-name-row\">\n          <div class=\"hdr-name\">${esc(c.name || 'Campanha')}</div>\n          <div class=\"hdr-pencil\" title=\"Renomear\">✎</div>\n        </div>\n        ${settingLine}\n        <div class=\"hdr-meta\">\n          <span class=\"pill status\">${esc(c.status || 'ativa')}</span>\n          ${myRole ? `<span class=\"pill role\">${myRole === 'mestre' ? 'Mestre' : 'Jogador'}</span>` : ''}\n        </div>\n      </div>`;\n    }\n\n    // === Mestre (per-campaign DM) ===\n    function mestreIni(nm){ return String(nm||'M').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase() || 'M'; }\n    const mName = c.mestreName != null && c.mestreName !== '' ? c.mestreName : 'Mestre';\n    const mAva = c.mestrePortrait\n      ? `<img src=\"${esc(c.mestrePortrait)}\" alt=\"\"/>`\n      : `<span class=\"ini\">${esc(mestreIni(mName))}</span>`;\n    let mestreNameBlock;\n    if (d2.state.editingMestreName){\n      const msaving = !!d2.state.mestreSaving;\n      const mErr = d2.state.mestreErr ? `<div class=\"err\">${esc(d2.state.mestreErr)}</div>` : '';\n      mestreNameBlock = `${mErr}\n        <input class=\"mestre-ninput\" type=\"text\" placeholder=\"nome do Mestre\" value=\"${esc(d2.state.mestreNameDraft != null ? d2.state.mestreNameDraft : mName)}\" />\n        <div class=\"mestre-nactions\">\n          <div class=\"mestre-nbtn mestre-nsave ${msaving?'mestre-disabled':''}\">${msaving?'Salvando…':'Salvar'}</div>\n          <div class=\"mestre-nbtn mestre-ncancel\">Cancelar</div>\n        </div>`;\n    } else {\n      const mErr = (!d2.state.editingMestreName && d2.state.mestreErr) ? `<div class=\"err\">${esc(d2.state.mestreErr)}</div>` : '';\n      mestreNameBlock = `<div class=\"mestre-name-row\">\n          <div class=\"mestre-name\">${esc(mName)}</div>\n          <div class=\"mestre-pencil\" title=\"Editar nome\">✎</div>\n        </div>\n        ${mErr}\n        <div class=\"mestre-upload\">${d2.state.mestreUploading ? 'Enviando…' : '⬆ Alterar imagem'}</div>\n        <input class=\"mestre-fileinput\" type=\"file\" accept=\"image/*\"/>`;\n    }\n    const mestreSec = `<div class=\"panel\">\n      <div class=\"panel-title\">Mestre</div>\n      <div class=\"mestre-row\">\n        <div class=\"mestre-ava\">${mAva}</div>\n        <div class=\"mestre-ctl\">${mestreNameBlock}</div>\n      </div>\n    </div>`;\n\n    // === Heróis (party da campanha) ===\n    const heroes = d2.state.heroes || [];\n    function heroIni(nm){ return String(nm||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase(); }\n    const heroCards = heroes.length ? heroes.map((h,i)=>{\n      const ava = h.portrait ? `<img src=\"${esc(h.portrait)}\" alt=\"\"/>` : `<span class=\"ini\">${esc(heroIni(h.name))}</span>`;\n      const metaBits = [];\n      if (h.race) metaBits.push(esc(h.race));\n      if (h.klass) metaBits.push(esc(h.klass));\n      if (h.level) metaBits.push('nível ' + esc(h.level));\n      const metaPill = metaBits.length ? `<span class=\"pill status\">${metaBits.join(' · ')}</span>` : '';\n      return `<div class=\"member\" data-key=\"hero-${esc(h.id || i)}\">\n        <div class=\"hero-top\">\n          <div class=\"hero-ava\">${ava}</div>\n          <div style=\"min-width:0;\">\n            <div class=\"hero-card-name\">${esc(h.name || 'Herói')}</div>\n            <div class=\"member-meta\">${metaPill}</div>\n          </div>\n        </div>\n        <button class=\"ver-ficha\" data-hero=\"${esc(h.id)}\">Ver ficha ›</button>\n      </div>`;\n    }).join('') : `<div class=\"empty\">Nenhum herói ainda</div>`;\n\n    let heroCreateBlock;\n    if (d2.state.creatingHero){\n      const cErr = d2.state.cdHeroErr ? `<div class=\"err\">${esc(d2.state.cdHeroErr)}</div>` : '';\n      const csaving = !!d2.state.cdHeroSaving;\n      heroCreateBlock = `<div class=\"panel\">\n        ${cErr}\n        <input class=\"cd-hinput\" type=\"text\" placeholder=\"nome do herói\" value=\"${esc(d2.state.cdHeroName != null ? d2.state.cdHeroName : '')}\" />\n        <div class=\"cd-hxplbl\">XP (opcional)</div>\n        <input class=\"cd-hxp\" type=\"number\" min=\"0\" placeholder=\"0\" value=\"${esc(d2.state.cdHeroXp != null ? d2.state.cdHeroXp : '')}\" />\n        <div class=\"cd-hactions\">\n          <div class=\"cd-hbtn cd-hsave ${csaving?'cd-hdisabled':''}\">${csaving?'Criando…':'Criar'}</div>\n          <div class=\"cd-hbtn cd-hcancel\">Cancelar</div>\n        </div>\n      </div>`;\n    } else {\n      heroCreateBlock = '';\n    }\n    const heroHead = `<div class=\"hero-sec-head\"><div class=\"sec-title\">Heróis</div>${d2.state.creatingHero ? '' : '<div class=\"cd-hero-create\">✚ Criar herói</div>'}</div>`;\n    const heroesSec = `${heroHead}${heroCreateBlock}${heroCards}`;\n\n    const sessionsBtn = d2.render('cronica-app@GoldenButton', { label:'Sessões', kind:'gold' }, { key:'sessionsBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/sessions/' + id); } } });\n    const statsBtn = d2.render('cronica-app@GoldenButton', { label:'Estatísticas', kind:'gold', glyph:'🏆' }, { key:'statsBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/stats/' + id); } } });\n    const worldBtn = d2.render('cronica-app@GoldenButton', { label:'Mundo', kind:'gold', glyph:'🗺' }, { key:'worldBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/world/' + id); } } });\n    const oraculoBtn = d2.render('cronica-app@GoldenButton', { label:'Oráculo', kind:'gold', glyph:'🔮' }, { key:'oraculoBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/oraculo/' + id); } } });\n    const gearBtn = d2.render('cronica-app@GoldenButton', { label:'Equipamentos', kind:'gold', glyph:'🎒' }, { key:'gearBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/gear/' + id); } } });\n    const critsBtn = d2.render('cronica-app@GoldenButton', { label:'Tabelas', kind:'gold', glyph:'🎲' }, { key:'critsBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); const from = id ? ('?from=' + encodeURIComponent('/campaign/' + id)) : ''; self.go(d2, '/crits' + from); } });\n    const goldBtn = d2.render('cronica-app@GoldenButton', { label:'Ouro', kind:'gold', glyph:'💰' }, { key:'goldBtn', style:'height:50px;flex:1', onPress:()=>{ const id = self.campaignId(d2); if (id){ self.go(d2, '/gold/' + id); } } });\n    const navRow = `<div style=\"display:flex;gap:10px;flex-wrap:wrap;\">${sessionsBtn}${statsBtn}${worldBtn}${oraculoBtn}${gearBtn}${critsBtn}${goldBtn}</div>`;\n    return `<div class=\"wrap\">${back}${header}${mestreSec}${navRow}${heroesSec}</div>`;\n  }\n}"}},"onBack":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onBack"}}}},"membersDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"membersDataset"}}}},"campaignsDataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"campaignsDataset"}}}}}}}}}},{"_id":"6a32f1b89f6e1c72479e5149","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"SessionList","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .sl{ width:100%; height:100%; min-height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:12px; padding:2px; background:#1c140d; }\n  .sl-back{ align-self:flex-start; display:inline-flex; align-items:center; gap:4px; cursor:pointer;\n    font-family:'Alegreya',sans-serif; font-size:14px; letter-spacing:1px;\n    color:#d8b24c; background:rgba(216,178,76,0.08); border:2px solid #c8932f; border-radius:4px;\n    padding:6px 12px; margin-bottom:10px; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.10), inset -1px -1px 0 rgba(0,0,0,0.4); }\n  .sl-back:hover{ color:#f0d27a; filter:brightness(1.1); }\n  .sl-back:active{ transform:translateY(1px); }\n  .sl-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; }\n  .sl-title{ color:${PAL.gold}; font-family:'Jacquard 24','Alegreya',serif; font-size:34px;\n    letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; line-height:1; }\n  .sl-new{ cursor:pointer; user-select:none; white-space:nowrap;\n    font-family:'Alegreya',sans-serif; font-size:18px; letter-spacing:1px;\n    color:${PAL.edgeDark}; border:3px solid ${PAL.edgeDark}; border-radius:5px; padding:8px 14px;\n    text-shadow:1px 1px 0 rgba(255,255,255,0.25);\n    background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%);\n    box-shadow: inset 2px 2px 0 rgba(255,255,255,0.45), inset -3px -3px 0 rgba(0,0,0,0.45), 0 4px 0 #7a5a18, 0 6px 12px rgba(0,0,0,0.5);\n    transition:filter 140ms ease, transform 100ms ease; image-rendering:pixelated; }\n  .sl-new:hover{ filter:brightness(1.12) drop-shadow(0 0 6px rgba(216,178,76,0.55)); }\n  .sl-new:active{ transform:translateY(3px); }\n  .sl-empty{ color:${PAL.cream}; opacity:0.7; font-family:'Alegreya',sans-serif; font-size:15px;\n    text-align:center; padding:28px 12px; letter-spacing:0.5px; }\n  .sl-loading{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:14px; text-align:center; padding:20px; }\n  .sl-err{ color:${PAL.bloodLight}; font-family:'Alegreya',sans-serif; font-size:12px; text-align:center; padding:4px; }\n  .card{ position:relative; box-sizing:border-box; cursor:pointer; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6);\n    transition:filter 140ms ease, transform 100ms ease; image-rendering:pixelated; }\n  .card:hover{ filter:brightness(1.1) drop-shadow(0 0 6px rgba(216,178,76,0.4)); }\n  .card:active{ transform:translateY(2px); }\n  .card-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:24px;\n    letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; margin-bottom:4px; line-height:1.1; }\n  .card-date{ color:${PAL.cream}; font-family:'Alegreya',sans-serif; font-size:13px; opacity:0.8; margin-bottom:8px; }\n  .card-meta{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }\n  .pill{ font-family:'Alegreya',sans-serif; font-size:11px; letter-spacing:1px;\n    padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .pill.status{ background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .pill.xp{ background:${PAL.gold}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .pill.gold{ background:${PAL.goldDeep}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .card-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }\n  .se-pencil{ cursor:pointer; flex:0 0 auto; color:${PAL.gold}; font-size:16px; line-height:1; padding:4px 7px; border:2px solid ${PAL.goldDeep}; border-radius:4px; background:rgba(216,178,76,0.08); }\n  .se-pencil:hover{ color:${PAL.goldLight}; filter:brightness(1.1); }\n  .card-editing{ cursor:default; display:flex; flex-direction:column; gap:10px; }\n  .card-err{ color:${PAL.bloodLight}; font-family:'Alegreya',sans-serif; font-size:13px; min-height:14px; }\n  .se-input{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:16px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .se-input:focus{ border-color:${PAL.goldDeep}; }\n  .se-input::placeholder{ color:${PAL.cream}; opacity:0.45; }\n  .se-actions{ display:flex; gap:10px; }\n  .se-btn{ flex:1; text-align:center; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:15px; letter-spacing:1px;\n    padding:9px 12px; border:3px solid ${PAL.edgeDark}; border-radius:4px; transition:filter 140ms ease, transform 100ms ease; }\n  .se-btn:active{ transform:translateY(2px); }\n  .se-save{ color:${PAL.edgeDark}; background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%); box-shadow: inset 2px 2px 0 rgba(255,255,255,0.4), 0 3px 0 #7a5a18; }\n  .se-save:hover{ filter:brightness(1.1); }\n  .se-cancel{ color:${PAL.cream}; background:${PAL.stone1}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; }\n  .se-cancel:hover{ filter:brightness(1.12); }\n  .se-disabled{ opacity:0.6; pointer-events:none; }\n  .se-del{ cursor:pointer; user-select:none; flex:0 0 auto;\n    font-family:'Alegreya',sans-serif; font-size:14px; line-height:1; padding:4px 7px;\n    color:${PAL.bloodLight}; background:rgba(28,20,13,0.65); border:2px solid ${PAL.blood}; border-radius:4px; }\n  .se-del:hover{ color:#e8a08c; filter:brightness(1.12); background:rgba(156,51,32,0.25); }\n  .se-del.se-confirm{ color:#ece0c4; background:${PAL.blood}; border-color:${PAL.bloodLight}; }\n  `,\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  goBack(d2, fallback){\n    try {\n      if (typeof window !== 'undefined' && window.history && window.history.length > 1) {\n        window.history.back();\n        return;\n      }\n    } catch(_){}\n    try { this.go(d2, fallback || '/'); } catch(_){}\n  },\n  mounted(d2){\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    this.load(d2);\n    try { d2.on(window, 'popstate', ()=>{ try { this.load(d2); } catch(_){}; try { d2.refresh(); } catch(_){} }); } catch(_){}\n    d2.on('.card','click',(e)=>{\n      if (e.target.closest('.se-pencil') || e.target.closest('.se-del') || e.target.closest('.card-editing')) return;\n      const el = e.target.closest('.card');\n      if (!el) return;\n      const id = el.getAttribute('data-id');\n      if (!id) return;\n      try { d2.emit('onOpen', id); } catch(_){}\n      this.go(d2, '/session/' + id);\n    });\n    d2.on('.sl-new','click',()=>{ this.createSession(d2); });\n    d2.on('.sl-back','click',()=>{\n      const cid = this.campaignId(d2);\n      this.goBack(d2, cid ? ('/campaign/' + cid) : '/');\n    });\n    // rename session (JS, no Store)\n    d2.on('.se-pencil','click',(e)=>{\n      e.stopPropagation();\n      const id = e.target.closest('.se-pencil').getAttribute('data-id');\n      if (!id) return;\n      const rows = d2.state.rows || [];\n      const row = rows.find(r => String(r.id) === String(id));\n      d2.state.editingId = id;\n      d2.state.etitle = row ? (row.title || '') : '';\n      d2.state.eerror = '';\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.se-input','input',(e)=>{ e.stopPropagation(); d2.state.etitle = e.target.value; });\n    d2.on('.se-input','click',(e)=>{ e.stopPropagation(); });\n    d2.on('.se-cancel','click',(e)=>{\n      e.stopPropagation();\n      d2.state.editingId = null;\n      d2.state.etitle = null;\n      d2.state.eerror = '';\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.se-save','click',(e)=>{\n      e.stopPropagation();\n      const id = e.target.closest('.se-save').getAttribute('data-id');\n      this.saveTitle(d2, id);\n    });\n    // 🗑 excluir sessão — d2.dialog.confirm se existir, senão confirmação inline em 2 toques\n    d2.on('.se-del','click',(e)=>{\n      e.stopPropagation();\n      const el = e.target.closest('.se-del');\n      if (!el) return;\n      const id = el.getAttribute('data-id');\n      const num = el.getAttribute('data-num') || '';\n      const title = el.getAttribute('data-title') || '';\n      if (!id) return;\n      const label = 'Sessão ' + num + ' — ' + title;\n      const msg = 'Excluir a sessão \"' + label + '\"? Isso não pode ser desfeito.';\n      let dlg = null;\n      try { dlg = (d2.dialog && typeof d2.dialog.confirm === 'function') ? d2.dialog.confirm(msg) : null; } catch(_){ dlg = null; }\n      if (dlg && typeof dlg.then === 'function'){\n        dlg.then((ok)=>{ if (ok) this.deleteSession(d2, id); });\n        return;\n      }\n      if (dlg === true){ this.deleteSession(d2, id); return; }\n      if (String(d2.state.delConfirmId) === String(id)){\n        d2.state.delConfirmId = null;\n        this.deleteSession(d2, id);\n      } else {\n        d2.state.delConfirmId = id;\n        try { d2.refresh(); } catch(_){}\n      }\n    });\n  },\n  async deleteSession(d2, id){\n    if (this._deleting || !id) return;\n    this._deleting = true;\n    try {\n      await d2.rush('deleteSession').invoke({ sessionId: id });\n      d2.state.delConfirmId = null;\n      await this.load(d2);\n    } catch(e){\n      d2.state.error = String(e && e.message ? e.message : e);\n    }\n    this._deleting = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  async saveTitle(d2, id){\n    if (this._savingTitle) return;\n    if (!id){ return; }\n    const title = (d2.state.etitle || '').trim();\n    if (!title){ d2.state.eerror = 'Informe um título'; try{ d2.refresh(); }catch(_){}; return; }\n    this._savingTitle = true;\n    d2.state.esaving = true;\n    d2.state.eerror = '';\n    try { d2.refresh(); } catch(_){}\n    try {\n      const res = await d2.rush('updateSession').invoke({ sessionId: id, title: title });\n      const ok = res && (res.ok || (res.data && res.data.ok));\n      if (!ok){\n        d2.state.eerror = 'Falha ao salvar';\n      } else {\n        d2.state.editingId = null;\n        d2.state.etitle = null;\n        await this.load(d2);\n      }\n    } catch(err){\n      d2.state.eerror = String(err && err.message ? err.message : err);\n    }\n    d2.state.esaving = false;\n    this._savingTitle = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  campaignId(d2){\n    let id = null;\n    try {\n      const _pn = (typeof window!=='undefined' && window.location) ? window.location.pathname : '';\n      const _m = _pn.match(/sessions\\/([^\\/?#]+)/);\n      if (_m) id = decodeURIComponent(_m[1]);\n    } catch(_){}\n    if (!id) { try { const p = d2.router().params; if (p && p.id) id = p.id; } catch(_){} }\n    return id;\n  },\n  // fallback rows from the bound dataset param (preview / no-session); filter by route campaignId when present\n  datasetRows(d2){\n    try {\n      const h = d2.data('dataset');\n      if (!h) return [];\n      let recs = h.records() || [];\n      const cid = this.campaignId(d2);\n      if (cid) recs = recs.filter(r => String(r.campaignId) === String(cid));\n      return recs.map(r => ({\n        id: r._id || r.id,\n        number: r.number,\n        title: r.title,\n        date: r.date,\n        status: r.status || 'em andamento',\n        xp: r.xp || 0,\n        gold: r.gold || 0\n      }));\n    } catch(_){ return []; }\n  },\n  fmtDate(v){\n    if (!v) return '';\n    try {\n      const d = new Date(v);\n      if (isNaN(d.getTime())) return '';\n      return d.toLocaleDateString('pt-BR', { day:'2-digit', month:'2-digit', year:'numeric' });\n    } catch(_){ return ''; }\n  },\n  render(d2){\n    try {\n      const _now = Date.now();\n      if (!this._loading && (!this._lastRenderLoad || (_now - this._lastRenderLoad) > 1200)){\n        this._lastRenderLoad = _now;\n        this.load(d2);\n      }\n    } catch(_){}\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    const self = this;\n    // session-backed rows only\n    let rows = d2.state.rows || [];\n    const back = `<button class=\"sl-back\">‹ Voltar</button>`;\n    const head = `${back}<div class=\"sl-head\"><div class=\"sl-title\">Sessões</div><div class=\"sl-new\">Nova sessão</div></div>`;\n    let body;\n    if (d2.state.loading && !rows.length){\n      body = `<div class=\"sl-loading\">Carregando…</div>`;\n    } else if (!rows.length){\n      body = `<div class=\"sl-empty\">Nenhuma sessão ainda</div>`;\n    } else {\n      const editId = d2.state.editingId;\n      const etitle = d2.state.etitle;\n      const esaving = !!d2.state.esaving;\n      const eerr = d2.state.eerror;\n      body = rows.map((r,i) => {\n        const num = (r.number != null) ? r.number : (i+1);\n        const title = r.title || 'Sem título';\n        const dateStr = self.fmtDate(r.date);\n        const dateHtml = dateStr ? `<div class=\"card-date\">${esc(dateStr)}</div>` : '';\n        const status = r.status || 'em andamento';\n        const xpPill = (r.xp) ? `<span class=\"pill xp\">${esc(r.xp)} XP</span>` : '';\n        const goldPill = (r.gold) ? `<span class=\"pill gold\">${esc(r.gold)} Ouro</span>` : '';\n        const rid = r.id || '';\n        if (String(editId) === String(rid) && rid){\n          const val = (etitle != null) ? etitle : title;\n          const errHtml = eerr ? `<div class=\"card-err\">${esc(eerr)}</div>` : '';\n          return `<div class=\"card card-editing\" data-key=\"${esc(rid || i)}\" data-id=\"${esc(rid)}\">\n            ${errHtml}\n            <input class=\"se-input\" data-id=\"${esc(rid)}\" type=\"text\" placeholder=\"título da sessão\" value=\"${esc(val)}\" />\n            <div class=\"se-actions\">\n              <div class=\"se-btn se-save ${esaving?'se-disabled':''}\" data-id=\"${esc(rid)}\">${esaving?'Salvando…':'Salvar'}</div>\n              <div class=\"se-btn se-cancel\">Cancelar</div>\n            </div>\n          </div>`;\n        }\n        return `<div class=\"card\" data-key=\"${esc(rid || i)}\" data-id=\"${esc(rid)}\">\n          <div class=\"card-head\">\n            <div class=\"card-name\">Sessão ${esc(num)} — ${esc(title)}</div>\n            <div style=\"display:flex; gap:6px; flex:0 0 auto;\">\n              <div class=\"se-pencil\" data-id=\"${esc(rid)}\" title=\"Renomear\">✎</div>\n              ${rid ? `<div class=\"se-del ${String(d2.state.delConfirmId) === String(rid) ? 'se-confirm' : ''}\" data-id=\"${esc(rid)}\" data-num=\"${esc(num)}\" data-title=\"${esc(title)}\" title=\"Excluir sessão\">${String(d2.state.delConfirmId) === String(rid) ? 'Confirmar?' : '🗑'}</div>` : ''}\n            </div>\n          </div>\n          ${dateHtml}\n          <div class=\"card-meta\">\n            <span class=\"pill status\">${esc(status)}</span>\n            ${xpPill}\n            ${goldPill}\n          </div>\n        </div>`;\n      }).join('');\n    }\n    const err = d2.state.error ? `<div class=\"sl-err\">${esc(d2.state.error)}</div>` : '';\n    return `<div class=\"sl\">${head}${err}${body}</div>`;\n  },\n  async load(d2){\n    if (this._loading) return;\n    const cid = this.campaignId(d2);\n    if (!cid){ return; }\n    this._loading = true;\n    d2.state.loading = true;\n    try {\n      const res = await d2.rush('sessionsByCampaign').invoke({ campaignId: cid });\n      const rows = (res && res.results) ? res.results : ((res && res.data && res.data.results) ? res.data.results : []);\n      d2.state.rows = rows;\n      d2.state.error = '';\n    } catch(e){\n      d2.state.error = String(e && e.message ? e.message : e);\n      d2.state.rows = [];\n    }\n    d2.state.loading = false;\n    this._loading = false;\n  },\n  async createSession(d2){\n    const cid = this.campaignId(d2);\n    if (!cid){ d2.state.error = 'Sem campanha na rota'; d2.refresh(); return; }\n    if (this._creating) return;\n    this._creating = true;\n    try {\n      await d2.rush('createSession').invoke({ campaignId: cid, title: 'Sessão nova' });\n      const list = await d2.rush('sessionsByCampaign').invoke({ campaignId: cid });\n      const rows = (list && list.results) ? list.results : ((list && list.data && list.data.results) ? list.data.results : []);\n      d2.state.rows = rows; d2.state.error = '';\n    } catch(e){ d2.state.error = String(e && e.message ? e.message : e); }\n    this._creating = false;\n    d2.refresh();\n  }\n}"}},"onOpen":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onOpen"}}}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}}}}}}}},{"_id":"6a32de1f14d462b0c3d8996f","component":"data2-core-types@Component","system":"6a32a5e751daba649fb7e28b","params":{"extends":"data2-core-primitives@Block","component":"CampaignList","package":"cronica-app","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const PAL = { stone1:'#3a2e20', stone2:'#4a3a28', edgeDark:'#1c140d', edgeLight:'#6a5436', gold:'#d8b24c', goldDeep:'#c8932f', goldLight:'#f0d27a', cream:'#ece0c4', blood:'#9c3320', bloodLight:'#c4543a' };\nexport default {\n  css: `\n  @import url('https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap');\n  :host{ display:block; width:100%; height:100%; }\n  .cl{ width:100%; height:100%; box-sizing:border-box; overflow-y:auto; display:flex; flex-direction:column; gap:12px; padding:2px; }\n  .cl-empty{ color:${PAL.cream}; opacity:0.7; font-family:'Alegreya',sans-serif; font-size:15px;\n    text-align:center; padding:28px 12px; letter-spacing:0.5px; }\n  .cl-loading{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:14px; text-align:center; padding:20px; }\n  .card{ position:relative; box-sizing:border-box; cursor:pointer; padding:16px; border-radius:6px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6);\n    transition:filter 140ms ease, transform 100ms ease; image-rendering:pixelated; }\n  .card:hover{ filter:brightness(1.1) drop-shadow(0 0 6px rgba(216,178,76,0.4)); }\n  .card:active{ transform:translateY(2px); }\n  .card-name{ color:${PAL.goldLight}; font-family:'Jacquard 24','Alegreya',serif; font-size:24px;\n    letter-spacing:1px; text-shadow:2px 2px 0 ${PAL.edgeDark}; margin-bottom:4px; line-height:1.1; }\n  .card-setting{ color:${PAL.cream}; font-family:'Alegreya',sans-serif; font-size:14px; opacity:0.85; margin-bottom:8px; }\n  .card-meta{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }\n  .pill{ font-family:'Alegreya',sans-serif; font-size:11px; letter-spacing:1px;\n    padding:3px 9px; border-radius:3px; border:2px solid ${PAL.edgeDark}; }\n  .pill.role{ background:${PAL.gold}; color:${PAL.edgeDark}; box-shadow: inset 1px 1px 0 rgba(255,255,255,0.4); }\n  .pill.status{ background:${PAL.edgeDark}; color:${PAL.gold}; }\n  .cl-newrow{ display:flex; }\n  .cl-new{ cursor:pointer; user-select:none; white-space:nowrap;\n    font-family:'Alegreya',sans-serif; font-size:16px; letter-spacing:1px;\n    color:${PAL.edgeDark}; border:3px solid ${PAL.edgeDark}; border-radius:5px; padding:8px 14px;\n    text-shadow:1px 1px 0 rgba(255,255,255,0.25);\n    background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%);\n    box-shadow: inset 2px 2px 0 rgba(255,255,255,0.45), inset -3px -3px 0 rgba(0,0,0,0.45), 0 4px 0 #7a5a18, 0 6px 12px rgba(0,0,0,0.5);\n    transition:filter 140ms ease, transform 100ms ease; image-rendering:pixelated; }\n  .cl-new:hover{ filter:brightness(1.12) drop-shadow(0 0 6px rgba(216,178,76,0.55)); }\n  .cl-new:active{ transform:translateY(3px); }\n  .cl-form{ box-sizing:border-box; padding:16px; border-radius:6px; display:flex; flex-direction:column; gap:10px;\n    background:\n      repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0px, rgba(0,0,0,0.06) 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px),\n      linear-gradient(150deg, ${PAL.stone2} 0%, ${PAL.stone1} 55%, #2e2418 100%);\n    border:4px solid ${PAL.edgeDark};\n    box-shadow: inset 3px 3px 0 ${PAL.edgeLight}, inset -4px -4px 0 ${PAL.edgeDark}, inset 0 0 28px rgba(0,0,0,0.55), 0 10px 22px rgba(0,0,0,0.6); }\n  .cl-form-title{ color:${PAL.gold}; font-family:'Alegreya',sans-serif; font-size:13px; letter-spacing:2px; text-shadow:1px 1px 0 ${PAL.edgeDark}; }\n  .cl-cerr{ color:${PAL.blood}; font-family:'Alegreya',sans-serif; font-size:13px; min-height:14px; }\n  .cl-cinput{ box-sizing:border-box; width:100%; font-family:'Alegreya',sans-serif; font-size:15px;\n    color:${PAL.cream}; background:#241a10; border:3px solid ${PAL.edgeDark}; border-radius:4px; padding:9px 11px;\n    box-shadow: inset 2px 2px 0 rgba(0,0,0,0.45); outline:none; }\n  .cl-cinput:focus{ border-color:${PAL.goldDeep}; }\n  .cl-cinput::placeholder{ color:${PAL.cream}; opacity:0.45; }\n  .cl-cta{ resize:vertical; min-height:60px; }\n  .cl-form-actions{ display:flex; gap:10px; }\n  .cl-btn{ flex:1; text-align:center; cursor:pointer; user-select:none; font-family:'Alegreya',sans-serif; font-size:15px; letter-spacing:1px;\n    padding:9px 12px; border:3px solid ${PAL.edgeDark}; border-radius:4px; transition:filter 140ms ease, transform 100ms ease; }\n  .cl-btn:active{ transform:translateY(2px); }\n  .cl-create{ color:${PAL.edgeDark}; background:linear-gradient(180deg, ${PAL.goldLight} 0%, ${PAL.gold} 45%, ${PAL.goldDeep} 100%);\n    box-shadow: inset 2px 2px 0 rgba(255,255,255,0.4), 0 3px 0 #7a5a18; }\n  .cl-create:hover{ filter:brightness(1.1); }\n  .cl-cancel{ color:${PAL.cream}; background:${PAL.stone1}; box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4), inset -2px -2px 0 ${PAL.edgeLight}; }\n  .cl-cancel:hover{ filter:brightness(1.12); }\n  .cl-disabled{ opacity:0.6; pointer-events:none; }\n  .cl-del{ position:absolute; top:10px; right:10px; cursor:pointer; user-select:none; z-index:2;\n    font-family:'Alegreya',sans-serif; font-size:14px; line-height:1; padding:4px 7px;\n    color:${PAL.bloodLight}; background:rgba(28,20,13,0.65); border:2px solid ${PAL.blood}; border-radius:4px; }\n  .cl-del:hover{ color:#e8a08c; filter:brightness(1.12); background:rgba(156,51,32,0.25); }\n  .cl-del.cl-confirm{ color:#ece0c4; background:${PAL.blood}; border-color:${PAL.bloodLight}; }\n  `,\n  go(d2, path){\n    try { d2.router().goTo(path); } catch(_){}\n    try {\n      if (typeof window !== 'undefined' && window.location && (window.location.pathname + window.location.search) !== path) {\n        window.history.pushState({}, '', path);\n        window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n      }\n    } catch(_){}\n  },\n  mounted(d2){\n    const self = this;\n    const FID='cronica-gfont';\n    if(typeof document!=='undefined' && document.head && !document.getElementById(FID)){\n      const l=document.createElement('link');\n      l.id=FID; l.rel='stylesheet';\n      l.href='https://fonts.googleapis.com/css2?family=Jacquard+24&family=Alegreya:wght@400;500;700;800&display=swap';\n      document.head.appendChild(l);\n    }\n    this.load(d2);\n    d2.on('.card','click',(e)=>{\n      if (e.target.closest('.cl-del')) return;\n      const el = e.target.closest('.card');\n      if (!el) return;\n      const id = el.getAttribute('data-id');\n      if (!id) return;\n      const path = '/campaign/' + id;\n      try { d2.emit('onOpen', id); } catch(_){}\n      try { d2.router().goTo(path); } catch(_){}\n      // fallback for runtimes where the router handle is detached (goTo no-op):\n      // drive the browser-history router directly.\n      try {\n        if (typeof window !== 'undefined' && window.location && window.location.pathname !== path) {\n          window.history.pushState({}, '', path);\n          window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n        }\n      } catch(_){}\n    });\n    // ✚ Nova campanha — toggle inline form\n    d2.on('.cl-new','click',()=>{\n      d2.state.creating = !d2.state.creating;\n      if (d2.state.creating && !d2.state.cform) d2.state.cform = { name:'', setting:'', description:'' };\n      d2.state.cerror = '';\n      try { d2.refresh(); } catch(_){}\n    });\n    // capture inputs into own state (no Store)\n    d2.on('.cl-cinput','input',(e)=>{\n      const k = e.target.getAttribute('data-k');\n      if (!k) return;\n      if (!d2.state.cform) d2.state.cform = { name:'', setting:'', description:'' };\n      d2.state.cform[k] = e.target.value;\n    });\n    d2.on('.cl-cancel','click',()=>{\n      d2.state.creating = false;\n      d2.state.cform = { name:'', setting:'', description:'' };\n      d2.state.cerror = '';\n      try { d2.refresh(); } catch(_){}\n    });\n    d2.on('.cl-create','click',()=>{ this.createCampaign(d2); });\n    // 🗑 excluir campanha — d2.dialog.confirm se existir, senão confirmação inline em 2 toques\n    d2.on('.cl-del','click',(e)=>{\n      e.stopPropagation();\n      const el = e.target.closest('.cl-del');\n      if (!el) return;\n      const id = el.getAttribute('data-id');\n      const name = el.getAttribute('data-name') || '';\n      if (!id) return;\n      const msg = 'Excluir a campanha \"' + name + '\"? Isso não pode ser desfeito.';\n      let dlg = null;\n      try { dlg = (d2.dialog && typeof d2.dialog.confirm === 'function') ? d2.dialog.confirm(msg) : null; } catch(_){ dlg = null; }\n      if (dlg && typeof dlg.then === 'function'){\n        dlg.then((ok)=>{ if (ok) this.deleteCampaign(d2, id); });\n        return;\n      }\n      if (dlg === true){ this.deleteCampaign(d2, id); return; }\n      // fallback: 2 toques (primeiro arma \"Confirmar?\", segundo executa)\n      if (String(d2.state.delConfirmId) === String(id)){\n        d2.state.delConfirmId = null;\n        this.deleteCampaign(d2, id);\n      } else {\n        d2.state.delConfirmId = id;\n        try { d2.refresh(); } catch(_){}\n      }\n    });\n  },\n  async deleteCampaign(d2, id){\n    if (this._deleting || !id) return;\n    this._deleting = true;\n    try {\n      await d2.rush('deleteCampaign').invoke({ campaignId: id });\n      d2.state.delConfirmId = null;\n      await this.load(d2);\n    } catch(e){\n      d2.state.cerror = String(e && e.message ? e.message : e);\n    }\n    this._deleting = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  async createCampaign(d2){\n    if (this._creating) return;\n    const cf = d2.state.cform || {};\n    const name = (cf.name || '').trim();\n    if (!name){ d2.state.cerror = 'Informe um nome'; try{ d2.refresh(); }catch(_){}; return; }\n    this._creating = true;\n    d2.state.csaving = true;\n    d2.state.cerror = '';\n    try { d2.refresh(); } catch(_){}\n    try {\n      const res = await d2.rush('createCampaign').invoke({ name: name, setting: (cf.setting||'').trim(), description: (cf.description||'').trim() });\n      const ok = res && (res.ok || (res.data && res.data.ok));\n      if (!ok){\n        d2.state.cerror = 'Falha ao criar campanha';\n      } else {\n        d2.state.creating = false;\n        d2.state.cform = { name:'', setting:'', description:'' };\n        d2.state.cerror = '';\n        await this.load(d2);\n      }\n    } catch(e){\n      d2.state.cerror = String(e && e.message ? e.message : e);\n    }\n    d2.state.csaving = false;\n    this._creating = false;\n    try { d2.refresh(); } catch(_){}\n  },\n  render(d2){\n    function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n    const rows = d2.state.rows || [];\n    const cf = d2.state.cform || {};\n    const creating = !!d2.state.creating;\n    const cerr = d2.state.cerror ? `<div class=\"cl-cerr\">${esc(d2.state.cerror)}</div>` : '';\n    const saving = !!d2.state.csaving;\n    const topBtn = `<div class=\"cl-newrow\"><div class=\"cl-new\">✚ Nova campanha</div></div>`;\n    const form = creating ? `<div class=\"cl-form\">\n        <div class=\"cl-form-title\">Nova campanha</div>\n        ${cerr}\n        <input class=\"cl-cinput\" data-k=\"name\" type=\"text\" placeholder=\"nome da campanha\" value=\"${esc(cf.name||'')}\" />\n        <input class=\"cl-cinput\" data-k=\"setting\" type=\"text\" placeholder=\"cenário (ex.: Faerûn)\" value=\"${esc(cf.setting||'')}\" />\n        <textarea class=\"cl-cinput cl-cta\" data-k=\"description\" placeholder=\"descrição (opcional)\">${esc(cf.description||'')}</textarea>\n        <div class=\"cl-form-actions\">\n          <div class=\"cl-btn cl-create ${saving?'cl-disabled':''}\">${saving?'Criando…':'Criar'}</div>\n          <div class=\"cl-btn cl-cancel\">Cancelar</div>\n        </div>\n      </div>` : '';\n    const head = `${topBtn}${form}`;\n    if (d2.state.loading && !rows.length) return `<div class=\"cl\">${head}<div class=\"cl-loading\">Carregando…</div></div>`;\n    if (!rows.length) return `<div class=\"cl\">${head}<div class=\"cl-empty\">Nenhuma campanha ainda</div></div>`;\n    const cards = rows.map((r,i) => {\n      const name = r.name || 'Sem nome';\n      const setting = r.setting ? `<div class=\"card-setting\">${esc(r.setting)}</div>` : '';\n      const role = (r.role === 'mestre') ? 'Mestre' : 'Jogador';\n      const status = r.status || 'ativa';\n      const rid = r.id || '';\n      const isConfirm = String(d2.state.delConfirmId) === String(rid) && rid;\n      const delBtn = rid ? `<div class=\"cl-del ${isConfirm?'cl-confirm':''}\" data-id=\"${esc(rid)}\" data-name=\"${esc(name)}\" title=\"Excluir campanha\">${isConfirm?'Confirmar?':'🗑'}</div>` : '';\n      return `<div class=\"card\" data-key=\"${esc(rid || i)}\" data-id=\"${esc(rid)}\">\n        ${delBtn}\n        <div class=\"card-name\">${esc(name)}</div>\n        ${setting}\n        <div class=\"card-meta\">\n          <span class=\"pill role\">${role}</span>\n          <span class=\"pill status\">${esc(status)}</span>\n        </div>\n      </div>`;\n    }).join('');\n    return `<div class=\"cl\">${head}${cards}</div>`;\n  },\n  async load(d2){\n    if (this._loading) return;\n    this._loading = true;\n    d2.state.loading = true;\n    try {\n      const res = await d2.rush('myCampaigns').invoke({});\n      const rows = (res && res.results) ? res.results : ((res && res.data && res.data.results) ? res.data.results : []);\n      d2.state.rows = rows;\n      d2.state.error = '';\n    } catch(e){\n      d2.state.error = String(e && e.message ? e.message : e);\n      d2.state.rows = [];\n    }\n    d2.state.loading = false;\n    this._loading = false;\n  }\n}"}},"onOpen":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onOpen"}}}},"dataset":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dataset"}}}}}}}}}},{"_id":"5ed9bed7b9e0891ce913e884","component":"data2-core-types@Component","params":{"package":"data2-core","component":"DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5f472ae463c680de4ff296e9","component":"data2-core-types@Component","params":{"package":"data2-core","component":"Data2TableDataSource","extends":"data2-core@DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}}]}},{"reference":"ExternalComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"_id":"5f060d9b1ca96def42f3c40a","displayName":"Builder AuthStrategy","component":"data2-core@AuthenticationStrategy","system":"5e3b796d6228227ca637d0f4","params":{"credentials":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"UsersDataSet"}},"sessionShared":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"password":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},"email":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},"resetPassword":{"resetEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"nR6Q9d7qdaPtZhJ28Q8Dy"},"component":"data2-core-types@ArtboardReference"}},"resetSuccessEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"yPCJBTKo8q1Mw0B_3Kpwl"},"component":"data2-core-types@ArtboardReference"}}},"session":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"name"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"sessionMetadata":{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},{"params":{"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"active"}},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.status"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is active user"}]},"component":"data2-core-logical-operators@And","displayName":"is active and has _id"}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $user"},{"params":{"action":{"params":{"pattern":{"component":"data2-core-primitives@String","params":{"content":"@data2.app"}},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.email"}}}}},"component":"data2-core-functions@Includes","displayName":"isData2User"},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}},"component":"data2-core-flow@Step"},{"params":{"action":{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.d2_app"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"management"}}]},"component":"data2-core-functions@Includes","displayName":"isData2App Exception"},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}},"component":"data2-core-flow@Step"},{"params":{"resultName":{"component":"data2-core-primitives@String","params":{"content":"has_access"}},"action":{"params":{"compare":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}}}]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"actions":[{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $metadata.system"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":"filter","args":[{"_id":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}}},"component":"data2-core-logical-operators@Not","displayName":"systems not found"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check system found"},{"params":{"actions":[{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"organization":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"cursor.current.organization._id"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"}]},"component":"data2-core-flow@Parallel"}]},"component":"data2-core-flow@Series","displayName":"common setup"},{"params":{"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"compare":{"params":{"components":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"system"}}],"body":{"params":{"expression":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.visibility"}}}},"defaultCase":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"public"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"billing-owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}},{"component":"data2-core-primitives@String","params":{"content":"member"}}]},"component":"data2-core-functions@Includes","displayName":"has role with access"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"collaborator"}}},"component":"data2-core-comparison-operators@Equal","displayName":"is collaborator"}]},"component":"data2-core-logical-operators@And","displayName":"is org member and collaborator"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is collaborator"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is collaborator and has system access"}]},"component":"data2-core-logical-operators@Or"}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"private"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}}]},"component":"data2-core-functions@Includes","displayName":"is owner/admin"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is owner/admin"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is member and has system access"}]},"component":"data2-core-logical-operators@Or","displayName":"is owner/admin or has system access"}}}]},"component":"data2-core-flow@Switch"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-logical-operators@And","displayName":"has org member for all systems in request"}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@Step"},{"params":{"then":{"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"else":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"},"compare":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"has_access"}}}}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"sessionExpiresIn":{"component":"data2-core-primitives@Number","params":{"content":604800}},"sessionRenewal":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"reference":"Data2BuilderAuthentication","updatedAt":"2025-10-28T02:48:49.233Z"},{"_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":"6a32b4a8ec12023d1caec374","component":"data2-core-primitives@Router","system":"6a32a5e751daba649fb7e28b","displayName":"Main Router","params":{"routerType":{"component":"data2-core-primitives@String","params":{"content":"browser"}},"routes":[{"component":"data2-core-primitives@Route","params":{"path":"/","name":"Home","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"iH4bfUNkjV3joQutjGUCZ"}},"policies":[]}},{"component":"data2-core-primitives@Route","params":{"path":"/login","name":"Login","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"8JWiyc9Qx7pAQQQ4pkJk4"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/campaign/:id","name":"Campanha","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"V2ph3j_woYgr73g8zwgL4"}},"policies":[]}},{"component":"data2-core-primitives@Route","params":{"path":"/sessions/:id","name":"Sessões","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"VsbWQ9GE9sgG9Jaml8d5Z"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/session/:id","name":"Log da Sessão","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"42ssD2QC7h_Au0EY148xr"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/hero/:id","name":"Ficha do Herói","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"UcLYTBuLABw04box_y8_z"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/stats/:id","name":"Estatísticas","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"az0xsvtpMJcVHliD58JKG"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/world/:id","name":"Mundo","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"UjyN4nlBF2eviQIi1TTGW"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/gear/:id","name":"Equipamentos","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"BLAFBC3iuPXKGtokpPZKO"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/crits","name":"Tabelas de Crítico","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"cPSpyLHODppe-CtUv_oMc"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/oraculo/:id","name":"O Oráculo","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"xhWHOTdC3GyM_WE1ewojw"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/possessao/:id","name":"Pós-sessão","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"JklxuhRTzbpyUG7QfCmaU"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/combat/:id","name":"Combate","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"taCkUnOINmm3bnTcPD81B"}}}},{"component":"data2-core-primitives@Route","params":{"path":"/gold/:id","name":"Ouro","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"SRmAzBy6NizqSfMqLWDSw"}}}}],"login":{"component":"data2-core-types@ArtboardReference","params":{"reference":"8JWiyc9Qx7pAQQQ4pkJk4"}},"initialPath":{"component":"data2-core-primitives@String","params":{"content":"/"}}},"reference":"mainRouter"},{"_id":"6a32d5cc491645141ab57e83","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"Login CC","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Login CC"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}},"position":{"component":"data2-core-types@Position","params":{"x":-33,"y":1537,"exactX":-33.43,"exactY":1536.68}}},"reference":"8JWiyc9Qx7pAQQQ4pkJk4"},{"_id":"6a3a859921b520e3be9faea5","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"GoldBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"GoldBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-1255,"y":-1306,"exactX":-1255.05,"exactY":-1305.68}}},"reference":"SRmAzBy6NizqSfMqLWDSw"},{"_id":"6a3965ba6185700287482147","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"CombatBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"CombatBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-714,"y":-1371,"exactX":-713.56,"exactY":-1371.44}}},"reference":"taCkUnOINmm3bnTcPD81B"},{"_id":"6a342f7f324a369e319f5697","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"PosSessaoBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"PosSessaoBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1443,"height":900,"exactWidth":1443.39,"exactHeight":900}},"position":{"component":"data2-core-types@Position","params":{"x":-2459,"y":-2653,"exactX":-2459.27,"exactY":-2653.04}}},"reference":"JklxuhRTzbpyUG7QfCmaU"},{"_id":"6a342bf3f7c7a164bedf8f71","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"Oraculo Board","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Oraculo Board"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":440,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-824,"y":-2651,"exactX":-824.2,"exactY":-2650.62}}},"reference":"xhWHOTdC3GyM_WE1ewojw"},{"_id":"6a341d1cec6c11c941441cec","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"CritTablesBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"CritTablesBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":414,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-543,"y":-96,"exactX":-542.75,"exactY":-95.6}}},"reference":"cPSpyLHODppe-CtUv_oMc"},{"_id":"6a3415b483c2399a357c4a1f","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"Gear Board","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Gear Board"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":430,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-2379,"y":-1130,"exactX":-2378.8399999999997,"exactY":-1130.2}}},"reference":"BLAFBC3iuPXKGtokpPZKO"},{"_id":"6a3410247d7ea7360a3a8179","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"WorldBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"WorldBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":430,"height":920}},"position":{"component":"data2-core-types@Position","params":{"x":-1801,"y":-1198,"exactX":-1801.31,"exactY":-1197.56}}},"reference":"UjyN4nlBF2eviQIi1TTGW"},{"_id":"6a3406e123b312eaa4d38e1f","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"StatsBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"StatsBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":420,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-1242,"y":-21,"exactX":-1242.24,"exactY":-21.48}}},"reference":"az0xsvtpMJcVHliD58JKG"},{"_id":"6a34042601215fafacaa6d61","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"HeroSheetBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"HeroSheetBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":414,"height":896}},"position":{"component":"data2-core-types@Position","params":{"x":-1809,"y":-33,"exactX":-1808.62,"exactY":-32.58}}},"reference":"UcLYTBuLABw04box_y8_z"},{"_id":"6a32f35c0c713cafa7d2f4dd","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"SessionLogBoard","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"SessionLogBoard"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":414,"height":1406,"exactWidth":414,"exactHeight":1405.55}},"position":{"component":"data2-core-types@Position","params":{"x":-2377,"y":-32,"exactX":-2376.88,"exactY":-31.65}}},"reference":"42ssD2QC7h_Au0EY148xr"},{"_id":"6a32f1c69fe2a989dbb78f9a","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"Sessions","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Sessions"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-43,"y":-1460,"exactX":-42.71,"exactY":-1459.59}}},"reference":"VsbWQ9GE9sgG9Jaml8d5Z"},{"_id":"6a32e202fd2b35b0fa7a7b49","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"CampaignDetail","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"CampaignDetail"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}},"position":{"component":"data2-core-types@Position","params":{"x":-18,"y":521,"exactX":-18.21,"exactY":520.81}}},"reference":"V2ph3j_woYgr73g8zwgL4"},{"_id":"6a32de2abaa61811109932f8","component":"data2-core-primitives@Artboard","system":"6a32a5e751daba649fb7e28b","displayName":"Campaigns","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Campaigns"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":-1645,"y":1404,"exactX":-1644.75,"exactY":1404.32}}},"reference":"iH4bfUNkjV3joQutjGUCZ"},{"_id":"aeecc0c99a7833903489a21b","parent":"6a32d5cc491645141ab57e83","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":1280,"height":800}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}}},"reference":"0_VqE6S3iTHf7vbb-Fzpw"},{"_id":"606e0257fcd5bc591c605d04","parent":"6a3a859921b520e3be9faea5","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":420,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"yYPVvRioO6oApwnTvKGlG"},{"_id":"de619318bc457a1661229f04","parent":"6a3965ba6185700287482147","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":420,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"LynZ_IgrB1hl1JXk3D3rx"},{"_id":"51b3d28ac7ed8672a3fa87fb","parent":"6a342f7f324a369e319f5697","component":"cronica-app@PosSessaoBoard","system":"6a32a5e751daba649fb7e28b","displayName":"PosSessaoBoard","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":1440,"height":900}}}},"reference":"9Y2mE6ohNn9elqXx90QJi"},{"_id":"303f942ccff95d82dbbe1708","parent":"6a342bf3f7c7a164bedf8f71","component":"cronica-app@OraculoBoard","system":"6a32a5e751daba649fb7e28b","displayName":"OraculoBoard","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":false,"top":true,"bottom":false,"growH":true,"growV":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":440,"height":900}}}},"reference":"l6q5BhcsSNVa9U62RdcqJ"},{"_id":"af2a5b960cdddc382b16409d","parent":"6a341d1cec6c11c941441cec","component":"cronica-app@CritTablesBoard","system":"6a32a5e751daba649fb7e28b","displayName":"CritTablesInstance","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":414,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}}}},"reference":"6MEtqOVNUdivztfghbWNl"},{"_id":"ea05613a71a012519edf1916","parent":"6a3415b483c2399a357c4a1f","component":"cronica-app@GearBoard","system":"6a32a5e751daba649fb7e28b","displayName":"GearBoard Host","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":430,"height":900}}}},"reference":"lLqKdT32kn63fgavpXFRB"},{"_id":"582d55b214c70d958d1e9825","parent":"6a3410247d7ea7360a3a8179","component":"cronica-app@WorldBoard","system":"6a32a5e751daba649fb7e28b","displayName":"WorldBoard Host","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":430,"height":920}}}},"reference":"HGYh1vtTCYZOfS2KH5nzU"},{"_id":"82a179532b84150fcc457c3e","parent":"6a3406e123b312eaa4d38e1f","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":420,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"dTuPOB_rPNACUAZzJo-hc"},{"_id":"04a06e488389ee3b93b71718","parent":"6a34042601215fafacaa6d61","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":414,"height":896}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}}},"reference":"ppEUas7wBbV4CM_3H88bl"},{"_id":"71011ae51a4829d3d9f4af99","parent":"6a32f35c0c713cafa7d2f4dd","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Page","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true,"exactX":false,"exactY":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":414,"height":1403,"exactWidth":414,"exactHeight":1402.61}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"CvfUQUpBFKOZ6VHXlUptE"},{"_id":"7a53621bd28797279838bdd3","parent":"6a32f1c69fe2a989dbb78f9a","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":1280,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"-iaoYdbSqvDhojMzQl2-a"},{"_id":"313a1d72756459cd080d5958","parent":"6a32e202fd2b35b0fa7a7b49","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":1280,"height":800}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}}},"reference":"ztlCEkUsoF_Frj3MoCvnZ"},{"_id":"e8c53aabdf99011e7d804c20","parent":"6a32de2abaa61811109932f8","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"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":1280,"height":900}},"fill":{"component":"data2-core-types@Fill","params":{"backgrounds":[{"component":"data2-core-types@Background","params":{"color":{"component":"data2-core-types@Color","params":{"rgba":"#1c140d"}}}}]}},"scroll":{"component":"data2-core-types@Scroll","params":{}}},"reference":"IHkTcFyv1I08BbjZ2uRtf"},{"_id":"2d46eb8d3398bc5eebdf446e","parent":"aeecc0c99a7833903489a21b","component":"cronica-app@LoginBoard","system":"6a32a5e751daba649fb7e28b","displayName":"LoginBoardInstance","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":1280,"height":800}}}},"reference":"k_veT1PYIfkEiLUdMIdCX"},{"_id":"5704a84f8b5ef643df050283","parent":"606e0257fcd5bc591c605d04","component":"cronica-app@GoldBoard","system":"6a32a5e751daba649fb7e28b","displayName":"GoldBoardInstance","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":16,"y":16,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":388,"height":868}}}},"reference":"iJS__CDPOnQrlyn5qGk4E"},{"_id":"d0f28fda56e0be5c38a6d474","parent":"de619318bc457a1661229f04","component":"cronica-app@CombatBoard","system":"6a32a5e751daba649fb7e28b","displayName":"CombatBoardInstance","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":16,"y":16,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":388,"height":868}}}},"reference":"MExi5XFndiKscZWxnDOpb"},{"_id":"1d380b241fddd8fe8d3012f8","parent":"82a179532b84150fcc457c3e","component":"cronica-app@StatsBoard","system":"6a32a5e751daba649fb7e28b","displayName":"StatsBoardInstance","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":16,"y":16,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":388,"height":868}}}},"reference":"TuVYpccct-b-po0rBogGR"},{"_id":"f4bd0603d02a60ae15bd390c","parent":"04a06e488389ee3b93b71718","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Column","params":{"position":{"component":"data2-core-types@Position","params":{"x":16,"y":16,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":382,"height":864}}},"reference":"nXyx8DI0wj0SttCrOhAR5"},{"_id":"4ab30dc8461b2a14c2ee715d","parent":"71011ae51a4829d3d9f4af99","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Column","params":{"position":{"component":"data2-core-types@Position","params":{"x":16,"y":16,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":382,"height":1368}}},"reference":"n8faYTe9rdPhFgDNbJIm_"},{"_id":"0f67f3e8066d531cbd8d2499","parent":"7a53621bd28797279838bdd3","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Column","params":{"position":{"component":"data2-core-types@Position","params":{"x":460,"y":24,"freeTransform":true,"left":false,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":860,"growHeightWithContent":false}}},"reference":"gH6z33Z88LP876NRJgG90"},{"_id":"38ba6924d44f31f00f4db8d8","parent":"313a1d72756459cd080d5958","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Column","params":{"position":{"component":"data2-core-types@Position","params":{"x":460,"y":20,"freeTransform":true,"left":false,"right":false,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":760}}},"reference":"MttKkoCMdoKPm0bDzFIiR"},{"_id":"5b482f1598144082b8e639b1","parent":"e8c53aabdf99011e7d804c20","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Column","params":{"position":{"component":"data2-core-types@Position","params":{"x":460,"y":24,"freeTransform":true,"left":false,"right":false,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":860,"growHeightWithContent":false}}},"reference":"No9EB1yL_wiNzWgigeIPQ"},{"_id":"5b17e6a957ff7d7133428a84","parent":"f4bd0603d02a60ae15bd390c","component":"cronica-app@HeroSheet","system":"6a32a5e751daba649fb7e28b","displayName":"HeroSheetInstance","params":{"heroesDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"heroesDataSet"}},"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":382,"height":864}}}},"reference":"4XFNl-jV2irTan5BcaNib"},{"_id":"5250e150022b6eeb4bcbcc33","parent":"4ab30dc8461b2a14c2ee715d","component":"cronica-app@SessionLog","system":"6a32a5e751daba649fb7e28b","displayName":"SessionLogInstance","params":{"sessionsDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"sessionsDataSet"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":382,"height":1373,"exactWidth":382,"exactHeight":1372.9}}}},"reference":"qRXP41_21-LAhNtwFLt2E"},{"_id":"e5a20aa1a4be11f39d8f4b8c","parent":"0f67f3e8066d531cbd8d2499","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"ListContainer","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"top":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":860}}},"reference":"wgKTefZoX2yVHlCU_gRXj"},{"_id":"4693cdce0621622636ff35e4","parent":"38ba6924d44f31f00f4db8d8","component":"cronica-app@CampaignDetailView","system":"6a32a5e751daba649fb7e28b","displayName":"DetailView","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":360,"height":760}}},"membersDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"membershipsDataSet"}},"campaignsDataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"campaignsDataSet"}}},"reference":"dDqnwfP98JoOMHkKzn05X"},{"_id":"a4d2e48471a8b4a0ab5dbc10","parent":"5b482f1598144082b8e639b1","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"Header","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":56}},"text":{"component":"data2-core-primitives@String","params":{"content":"Minhas campanhas"}},"textColor":{"component":"data2-core-types@Color","params":{"rgba":"#d8b24c"}},"textSize":34,"textFormat":{"component":"data2-core-types@TextFormat","params":{"fontFamily":{"component":"data2-core-web@GoogleFont","params":{"fontFamily":"Jacquard 24"}},"horizontalAlignment":"center","verticalAlignment":"middle","fontWeight":400}}},"reference":"rfbyU1vB7eiTmbv5hkSxp"},{"_id":"ff82aa4b072a747f343c9ca2","parent":"5b482f1598144082b8e639b1","component":"data2-core-primitives@Block","system":"6a32a5e751daba649fb7e28b","displayName":"ListContainer","params":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":68,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":false}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":360}}},"reference":"XHS3XJOny8AvTN2VPdrPk"},{"_id":"2fac85e8941288c2005b4f9a","parent":"e5a20aa1a4be11f39d8f4b8c","component":"cronica-app@SessionList","system":"6a32a5e751daba649fb7e28b","displayName":"SessionListInstance","params":{"dataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"sessionsDataSet"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"top":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":780}}}},"reference":"1VPCMew5ptfM8fAPuzhP8"},{"_id":"40643967decf9b31518072ac","parent":"ff82aa4b072a747f343c9ca2","component":"cronica-app@CampaignList","system":"6a32a5e751daba649fb7e28b","displayName":"CampaignListInstance","params":{"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"top":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":360,"height":360}}},"dataset":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"campaignsDataSet"}}},"reference":"uuqfW77-WTpbZJa6hHIig"}]}},{"_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":"6a32b4acbaa618111098abfc","displayName":"Crônica App","reference":"LQkR5XgIE-JAb1jn6s3hj","params":{"name":{"component":"data2-core-primitives@String","params":{"content":"Crônica App"}},"shortName":{"component":"data2-core-primitives@String","params":{"content":"Crônica App"}},"domain":{"component":"data2-core-primitives@String","params":{"content":"cronica"}},"serverSideRendering":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"entryPoint":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Router","reference":"mainRouter"}},"responseHeaders":{"X-Frame-Options":{"component":"data2-core-primitives@String","params":{"content":""}},"Content-Security-Policy":{"component":"data2-core-primitives@String","params":{"content":""}},"X-XSS-Protection":{"component":"data2-core-primitives@String","params":{"content":""}}},"scriptTags":[{"component":"data2-core-web@Script","params":{"reference":"cronica-fonts","script":"(function(){function link(rel,href,cross){var l=document.createElement('link');l.rel=rel;l.href=href;if(cross)l.crossOrigin='anonymous';document.head.appendChild(l);return l;}if(!document.querySelector('link[data-cronica-font]')){link('preconnect','https://fonts.googleapis.com',false).setAttribute('data-cronica-font','1');link('preconnect','https://fonts.gstatic.com',true).setAttribute('data-cronica-font','1');link('stylesheet','https://fonts.googleapis.com/css2?family=Jacquard+24&family=Pixelify+Sans:wght@400;500;600;700&display=swap',false).setAttribute('data-cronica-font','1');}function load(){try{document.fonts.load(\"400 64px 'Jacquard 24'\");document.fonts.load(\"400 16px 'Pixelify Sans'\");document.fonts.load(\"700 16px 'Pixelify Sans'\");}catch(e){}}load();var DISPLAY=['CRÔNICA','ENTRAR NA SAGA','FORJAR HERÓI','ENTRAR'];function norm(t){return (t||'').replace(/\\s+/g,' ').trim();}function apply(){var els=document.querySelectorAll('.d2__block__content, .data2-player-block');for(var i=0;i<els.length;i++){var el=els[i];var txt=norm(el.textContent);var base=txt;var fam=(DISPLAY.indexOf(base)>=0)?\"'Jacquard 24', serif\":\"'Pixelify Sans', sans-serif\";if(el.style.fontFamily!==fam){el.style.fontFamily=fam;}if(el.classList.contains('data2-player-block')){var h=el.offsetHeight;if(/ENTRAR$/.test(base)&&base.indexOf('SAGA')<0&&h>=50&&h<=70){el.setAttribute('data-tid','btn-login');}else if(/FORJAR HERÓI$/.test(base)&&h>=50&&h<=70){el.setAttribute('data-tid','btn-signup');}else if(/conta\\?/.test(base)){el.setAttribute('data-tid','toggle');}}}}apply();var mo=new MutationObserver(function(){apply();});mo.observe(document.documentElement,{childList:true,subtree:true});document.fonts.ready.then(apply);setInterval(apply,600);})();"}}]},"system":"6a32a5e751daba649fb7e28b","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a32aeb551daba649fb7fddc","component":"data2-core@DataSet","system":"6a32a5e751daba649fb7e28b","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"heroes"}}},"reference":"heroesDataSet"},{"_id":"6a32eec6948195d3c9f4a902","component":"data2-core@DataSet","system":"6a32a5e751daba649fb7e28b","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"sessions"}}},"reference":"sessionsDataSet"},{"_id":"6a32aeb14eb1b962ce1087c9","component":"data2-core@DataSet","system":"6a32a5e751daba649fb7e28b","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"campaigns"}}},"reference":"campaignsDataSet"},{"_id":"6a32aeb3794780d41265d776","component":"data2-core@DataSet","system":"6a32a5e751daba649fb7e28b","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"memberships"}}},"reference":"membershipsDataSet"},{"_id":"6a32aeb5774630f283dd46b5","component":"data2-core@Data2TableDataSource","system":"6a32a5e751daba649fb7e28b","params":{"table":"heroes","schema":[{"component":"data2-core-types@Field","params":{"key":"campaignId","title":"Campaign","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"controllerUserId","title":"Controller","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"name","title":"Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"portrait","title":"Portrait","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"race","title":"Race","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"klass","title":"Class","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"level","title":"Level","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"forca","title":"Força","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"destreza","title":"Destreza","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"constituicao","title":"Constituição","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"inteligencia","title":"Inteligência","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"sabedoria","title":"Sabedoria","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"carisma","title":"Carisma","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"hpMax","title":"HP Máx","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"hpAtual","title":"HP Atual","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"ca","title":"CA","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"pericias","title":"Perícias","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"equipamento","title":"Equipamento","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"historia","title":"História","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"xp","title":"XP","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"deathSucc","title":"Death Successes","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"deathFail","title":"Death Failures","component":"data2-core-primitives@Number"}}]},"reference":"heroes"},{"_id":"6a32eec69fe2a989dbb782d1","component":"data2-core@Data2TableDataSource","system":"6a32a5e751daba649fb7e28b","params":{"table":"sessions","schema":[{"component":"data2-core-types@Field","params":{"key":"campaignId","title":"Campaign","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"number","title":"Number","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"date","title":"Date","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"title","title":"Title","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"summary","title":"Summary","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"notes","title":"Notes","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"xp","title":"XP","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"gold","title":"Gold","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"attendeeHeroIds","title":"Attendees","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"xpAwarded","title":"XP Awarded","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"xpLog","title":"XP Log","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"inGameDate","title":"In-Game Date","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"inGameTime","title":"In-Game Time","component":"data2-core-primitives@String"}}]},"reference":"sessions"},{"_id":"6a32aeb1774630f283dd46ae","component":"data2-core@Data2TableDataSource","system":"6a32a5e751daba649fb7e28b","params":{"table":"campaigns","schema":[{"component":"data2-core-types@Field","params":{"key":"name","title":"Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"setting","title":"Setting","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"description","title":"Description","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ownerUserId","title":"Owner","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"createdAt","title":"Created At","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"mestreName","title":"Mestre Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"mestrePortrait","title":"Mestre Portrait","component":"data2-core-primitives@String"}}]},"reference":"campaigns"},{"_id":"6a32aeb37fc78eca6d2fb732","component":"data2-core@Data2TableDataSource","system":"6a32a5e751daba649fb7e28b","params":{"table":"memberships","schema":[{"component":"data2-core-types@Field","params":{"key":"campaignId","title":"Campaign","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"userId","title":"User","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Role","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"invitedEmail","title":"Invited Email","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"heroId","title":"Hero","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"memberName","title":"Member Name","component":"data2-core-primitives@String"}}]},"reference":"memberships"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}