{"context":{"domain":"aramis"},"layers":{"ApplicationLayer":{"reference":"ApplicationLayer","availableInstances":[{"reference":"ComponentsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"SystemsDataSet","params":{"filter":{"domain":"data2"},"pagination":{"size":1},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSource"}}}},{"reference":"ExternalComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"SystemsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"5e3b796d6228227ca637d0f4","name":"data2","domain":"data2","organization":"62292184611f480b3191c37c","visibility":"public","updatedAt":"2026-07-27T14:19:36.810Z"}]}},{"reference":"OrganizationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"DataComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"OneFileComponentInstancesDataSet","params":{"pagination":{"size":1},"master":{"dataset":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSet"}},"localField":"system","masterField":"_id"},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"FileComponentInstancesDataSource"}}}},{"reference":"ApplicationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ServiceComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@Data2TableDataSource","reference":"FileComponentInstancesDataSource","params":{"table":"fileComponentInstances","schema":[{"component":"data2-core-types@Field","params":{"key":"component","title":"Component","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"displayName","title":"Display Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"params","title":"Params","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"reference","title":"Reference","component":"data2-core-primitives@String"}}]}},{"component":"data2-core@MemoryDataSource","reference":"EmptyDataSource","params":{"initialData":[]}},{"reference":"ComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"UserComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"DataComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ServiceComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ExternalComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ApplicationsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}}],"processesParams":{}},"HostApplicationLayer":{"reference":"HostApplicationLayer","availableInstances":[{"_id":"5f15ff190528b92601890f71","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"SystemsTableDataSource"}},"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"domain"}}},"pk":"_id","fields":[{"component":"data2-core-types@FieldReference","params":{"key":"_id"}},{"component":"data2-core-types@FieldReference","params":{"key":"name"}},{"component":"data2-core-types@FieldReference","params":{"key":"domain"}},{"component":"data2-core-types@FieldReference","params":{"key":"visibility"}},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization._id"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.name"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.billingStatus"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.publicTrial.active"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"settings.defaultAccessPolicy"}}},"break":true}],"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"staticPagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"policies":[{"component":"data2-core-auth@Policy","params":{"allow":{"params":{"actions":[{"params":{"components":[{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"_id"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"domain"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"}]},"component":"data2-core-logical-operators@Or"}]},"component":"data2-core-flow@Series"}}}]},"reference":"SystemsDataSet","updatedAt":"2026-08-07T21:51:24.037Z","createdAt":"2020-07-20T20:31:21.838Z"},{"_id":"5f1601ce0528b92601890f7c","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":1000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"sort":{"params.package":1,"params.component":1},"pk":"_id","autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"staticFilter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"filter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}}},"reference":"ComponentsDataSet","updatedAt":"2025-11-05T19:37:01.935Z","createdAt":"2020-07-20T20:42:54.898Z"},{"_id":"5f9041dbc8da12be8b94aea4","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":2000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"UserComponentsDataSet","updatedAt":"2025-11-06T21:06:58.095Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"65d8c4fabc9dbb8f94de8d53","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ExternalComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ExternalComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:23.742Z","createdAt":"2024-02-23T16:17:01.816Z"},{"reference":"SystemsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"69133afb854168f247b6f9af","name":"aramis","domain":"aramis","visibility":"private","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-17T18:19:09.051Z","createdAt":"2025-11-11T13:32:43.074Z"}]}},{"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":"6a3edbe664cf98a42c0c8075","component":"data2-core-types@Component","system":"69133afb854168f247b6f9af","params":{"extends":"data2-core-primitives@Block","component":"markdownViewer","package":"aramis","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 DEFAULT_MD = [\n  '# Virtual Try-On — Fluxo e Contrato do Entry Point',\n  '',\n  '> Sistema **aramis** (69133afb854168f247b6f9af) · Entry point: `get-generated-image-aramis` · doc completo no repo: `docs/TRY-ON-FLOW.md`',\n  '',\n  '## 0. TL;DR',\n  '',\n  'O try-on é um pipeline assíncrono com streaming. O cliente renderiza o componente **Clothing-Swap-Aramis** passando a foto do usuário + imagens do produto. Esse componente faz POST no listener rush **get-generated-image-aramis**, que:',\n  '',\n  '1. **Cacheia** por (product_id, user_image) — se já existe, devolve na hora.',\n  '2. Em paralelo roda **3 analisadores**: garment_judge, user_photo_judge e product_kb (perfil do garment).',\n  '3. **Gera** a imagem com gemini-3.1-flash-image-preview.',\n  '4. **Julga** o resultado (final_result_judge) — hoje desativado por um `|| true` (ver Gotchas).',\n  '5. **Persiste** em try-on-cache e dispara o overlay de PDP.',\n  '6. **Stream** de status (finished_input → first_draw → final_draw) e Response 200 com a URL da imagem.',\n  '',\n  'Tem um **watchdog de 90s** que aborta com 408.',\n  '',\n  '## 1. Arquitetura (camadas)',\n  '',\n  '| Camada | Componente | Referência |',\n  '|---|---|---|',\n  '| UI (wrapper) | Clothing-Swap-Aramis (UC) | `PHD5ZyD4_DR1_RqnMRGly` |',\n  '| UI (engine) | smart-image-aramis (code) | `psQ71aKVkDktwIr6-8WAk` |',\n  '| **Entry point** | get-generated-image-aramis (rush) | `QGJ0EoHKHWKtKuhbFcqe9` |',\n  '| Análise | garment_judge | `83yNK5odcDVMrtOJhki1A` |',\n  '| Análise | user_photo_judge | `qNgTWSGArg0vJ8rtnqknF` |',\n  '| Análise | product_kb (perfil) | `zUjwck80yboh9WFYpiw9t` |',\n  '| Avaliação | final_result_judge | `k59aK7cPZCofvi_V0-1Qd` |',\n  '| Geração | image .draw() (gemini) | `u_om3jtIGBMqXL3IP4Khu` |',\n  '| Storage | uploadFromFiles | `hCO-D0reJs35pp5pSD00J` |',\n  '| Overlay/PDP | generate-image-with-overlay | `MaCzpAm5QCknf5xducTZ6` |',\n  '',\n  '**Fluxo:** smart-image → POST → listener → [cache? | 3 analisadores em paralelo] → draw → upload → final_result_judge → saveRecord(cache) → overlay → stream + Response 200.',\n  '',\n  '## 2. Entry Point — Contrato',\n  '',\n  'Entry point = rush HTTPListener **get-generated-image-aramis** (`QGJ0EoHKHWKtKuhbFcqe9`). Config: enableExecutionLogs, shouldLogRequestBody, omitHeaders authorization.',\n  '',\n  '### 2.1 Request — body',\n  '',\n  '| Campo | Tipo | Obrig. | Uso |',\n  '|---|---|---|---|',\n  '| `product_id` | string | sim | chave de cache + filtro + persistido. Ausente → 504. |',\n  '| `user_image` | string url | sim | foto do usuário (Image 1). Sempre sufixada com `?w=600`. |',\n  '| `garment_images` | string[] | sim | imagens do produto → garment_judge e product_kb. |',\n  '| `product_metadata` | object | sim | `.productName` é URI-encoded na URL do overlay. |',\n  '| `user_id` | string | não | persistido; fallback de device_id na análise da foto. |',\n  '| `device_id` | string | não | persistido em try-on-cache. |',\n  '| `ignore_cache` | boolean | não | se truthy, força regeneração mesmo com cache hit. |',\n  '',\n  '```http',\n  'POST https://<rush-host>/get-generated-image-aramis',\n  'Content-Type: application/json',\n  '',\n  '{',\n  '  \"product_id\": \"12345\",',\n  '  \"user_image\": \"https://aramis.data2.media/file/abc123\",',\n  '  \"garment_images\": [\"https://.../garment-front\", \"https://.../on-model\"],',\n  '  \"product_metadata\": { \"productName\": \"Camisa Linho Slim\", \"category\": \"shirts\" },',\n  '  \"user_id\": \"user-789\",',\n  '  \"device_id\": \"device-abc\",',\n  '  \"ignore_cache\": false',\n  '}',\n  '```',\n  '',\n  '### 2.2 Response final',\n  '',\n  '| Cenário | Código | data |',\n  '|---|---|---|',\n  '| Sucesso (cache hit ou geração) | 200 | URL da imagem |',\n  '| product_id ausente | 504 | \"No product_id\" |',\n  '| Timeout (> 90s) | 408 | \"TimeOut\" |',\n  '| Falha na geração | 500 | \"Image not geberated\" (typo no código) |',\n  '',\n  '> Convenção interna: o sucesso viaja como `Error(code: 999, message: \"sucesso\")`. Não é erro — o dispatcher mapeia code==999 → 200, qualquer outro → 500.',\n  '',\n  '### 2.3 Streaming (Emit)',\n  '',\n  'Eventos antes do Response: `finished_input` → `first_draw` → `final_draw`. O cliente usa para mostrar progresso.',\n  '',\n  '### 2.4 Contrato do cliente (props do UC)',\n  '',\n  '| Prop | Mapeia para |',\n  '|---|---|',\n  '| `image` | body.user_image (Image 1) |',\n  '| `clothing[]` | body.garment_images (images = [image, ...clothing]) |',\n  '| `product_id` | body.product_id |',\n  '| `product_metadata` | body.product_metadata |',\n  '',\n  '## 3. Pipeline passo-a-passo',\n  '',\n  'Todo o corpo roda num race paralelo `||{ mainFlow, watchdog(90s → 408) }`.',\n  '',\n  '1. Valida input — sem product_id → 504.',\n  '2. Cache lookup — `try-on-cache.filter({ product_id, user_image: <user_image>?w=600 })`.',\n  '3. Cache hit && !ignore_cache → devolve result_file (sentinela 999) → 200. Senão segue.',\n  '4. Normaliza user_image = `<user_image>?w=600`.',\n  '5. Fan-out paralelo (3 sub-serviços): garment_judge (melhor imagem, body_area, color_profile), user_photo_judge (subject_description, pose_notes), product_kb (judge_result, archetype_id, fabric_id).',\n  '6. merge() + Emit(finished_input). variables.garment_image vira Image 2.',\n  '7. 1ª geração — `.draw({ images:[user_image, garment_image], model: gemini-3.1-flash-image-preview, size 256x256 })`.',\n  '8. Decode base64 → uploadFromFiles → fileUrl. Emit(first_draw).',\n  '9. final_result_judge.invoke({ result_image, original_image, garment_image }).',\n  '10. Retry condicional — `if ((approved || true))` aceita o 1º draw. O `|| true` torna DEAD CODE o 2º draw (nano-banana).',\n  '11. saveRecord em try-on-cache { user_image, product_id, result_file, user_id, device_id }.',\n  '12. Pós-processo: Emit(final_draw); dispara generate-image-with-overlay (URL pdp image-overlay + result_id); delay(3000).',\n  '13. Sinaliza sucesso `Error(data: result_image, code: 999)`.',\n  '14. Pós-race: code==999 → Response 200, senão Error 500.',\n  '',\n  '## 4. Os 3 Analisadores (judges)',\n  '',\n  '**garment_judge** (`83yNK5odcDVMrtOJhki1A`) — analista de produto. Cacheia em try-on-products por product_id. Seleciona a melhor imagem standalone (isolation 40% / completeness 30% / front_facing 20% / clarity 10%). Recusa categorias proibidas (underwear, swimwear). Extrai body_area, garment_type, color_profile (hex amostrado), pattern, fabric, coverage zones.',\n  '',\n  '**user_photo_judge** (`qNgTWSGArg0vJ8rtnqknF`) — juiz da foto do usuário. Cacheia em try-on-users por (device_id, user_image). Step 1 content safety (nudez, violência, ódio, **menores** — na dúvida rejeita). Step 2 person detection (hard gate: humano real com torso visível). Step 3 scoring (body_visibility 30 / pose 25 / lighting 20 / background 15 / sharpness 10; threshold 6.5). Devolve subject_description + pose_notes.',\n  '',\n  '**final_result_judge** (`k59aK7cPZCofvi_V0-1Qd`) — juiz do resultado. Step 1 no-op detection. Step 2 anatomy audit (hard gate: conta cabeças/mãos/dedos; reprova membros duplicados, dedos != 5). Step 3 scoring (garment_accuracy 35 / coverage 25 / subject_preservation 20 / realism 15 / lighting 5). Aprova só com anatomy ok + final_score >= 7.5. **Hoje o output é ignorado** pelo orquestrador (|| true).',\n  '',\n  '## 5. Modelo de dados',\n  '',\n  '```',\n  'try-on-cache  E9B1LJLQBUBfxAITGUj5u  (dataset HG2Psc2dpnir_5Ut6RXyq)',\n  '  product_id · user_image(+?w=600) · result_file · user_id · device_id',\n  '  user_shared_image · status(pending->complete) · source_kind · model_used · shadow',\n  '',\n  'try-on-users  d8yh75od5C-FDSPzmEByz  (dataset ZOcppMDBBFzD5ZsGLCspX)',\n  '  device_id · user_image · judge_result · accepted_terms · saw_analysis · user_file',\n  '',\n  'try-on-products  ERpZLMARMx7z3OvYoyIjd  (dataset kynPVsue3YGG-U8-Fm0Py)  [view joined]',\n  '  product_id · product_name · display_name · archetype_id · fabric_id · colorway_id',\n  '  fit_intent · judge_result · pattern · garment_image · normalized_product',\n  '```',\n  '',\n  '## 6. Cache & Geração',\n  '',\n  '- **Chave** = (product_id, user_image + \"?w=600\"). O sufixo de largura faz parte da chave.',\n  '- **Hit** → devolve result_file na hora (pula geração e LLMs). Bypass via ignore_cache.',\n  '- **Caches secundários**: garment_judge por product_id; user_photo_judge por (device_id, user_image).',\n  '- **Geração**: gemini-3.1-flash-image-preview, size 256x256; correção (dead code) usaria replicate.google/nano-banana. Inputs images = [user_image, garment_image].',\n  '',\n  '## 7. Gotchas & dívida técnica',\n  '',\n  '1. **Retry de correção é DEAD CODE.** `if ((approved || true))` é sempre verdadeiro → o 1º draw é sempre aceito e o final_result_judge não altera nada. Reativar = trocar por `if (approved)`.',\n  '2. **Sucesso viaja como `Error(code: 999)`** — convenção d2-flow, não é erro.',\n  '3. **Typo** na mensagem de erro 500: \"Image not geberated\".',\n  '4. **Watchdog fixo de 90s** — gerações lentas viram 408 silenciosamente.',\n  '5. **Saída pequena (256x256)** no 1º draw — possível gargalo de qualidade.',\n  '6. **Chave de cache amarrada a `?w=600`** — mudar o resize quebra hits existentes.',\n  '7. **Sobreposição garment_judge × product_kb** — ambos produzem judge_result; mapear a fonte canônica do garment_image.',\n  ''\n].join('\\n')\n\nfunction esc(s){\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\nfunction inline(s){\n  let out = esc(s)\n  out = out.replace(/`([^`]+)`/g, (m, c) => '<code>' + c + '</code>')\n  out = out.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n  out = out.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener\">$1</a>')\n  return out\n}\n\nfunction mdToHtml(md){\n  const lines = String(md || '').split('\\n')\n  let html = '', i = 0, inUl = false, inOl = false\n  const closeLists = () => {\n    if (inUl) { html += '</ul>'; inUl = false }\n    if (inOl) { html += '</ol>'; inOl = false }\n  }\n  while (i < lines.length) {\n    const ln = lines[i]\n    if (/^```/.test(ln)) {\n      closeLists()\n      const lang = ln.slice(3).trim()\n      i++\n      const buf = []\n      while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++ }\n      i++\n      const label = lang ? '<div class=\"cb-lang\">' + esc(lang) + '</div>' : ''\n      html += '<div class=\"cb\">' + label + '<pre>' + esc(buf.join('\\n')) + '</pre></div>'\n      continue\n    }\n    if (/^\\s*\\|.*\\|/.test(ln) && i + 1 < lines.length && /-/.test(lines[i + 1]) && /^\\s*\\|?[\\s:|-]+\\|?\\s*$/.test(lines[i + 1])) {\n      closeLists()\n      const header = ln.split('|').slice(1, -1).map(c => c.trim())\n      i += 2\n      const rows = []\n      while (i < lines.length && /^\\s*\\|.*\\|/.test(lines[i])) {\n        rows.push(lines[i].split('|').slice(1, -1).map(c => c.trim()))\n        i++\n      }\n      html += '<table><thead><tr>' + header.map(h => '<th>' + inline(h) + '</th>').join('') + '</tr></thead><tbody>'\n      html += rows.map(r => '<tr>' + r.map(c => '<td>' + inline(c) + '</td>').join('') + '</tr>').join('')\n      html += '</tbody></table>'\n      continue\n    }\n    const h = ln.match(/^(#{1,6})\\s+(.*)$/)\n    if (h) { closeLists(); const lv = h[1].length; html += '<h' + lv + '>' + inline(h[2]) + '</h' + lv + '>'; i++; continue }\n    if (/^\\s*(---|\\*\\*\\*|___)\\s*$/.test(ln)) { closeLists(); html += '<hr>'; i++; continue }\n    if (/^\\s*>\\s?/.test(ln)) { closeLists(); html += '<blockquote>' + inline(ln.replace(/^\\s*>\\s?/, '')) + '</blockquote>'; i++; continue }\n    if (/^\\s*[-*+]\\s+/.test(ln)) {\n      if (!inUl) { closeLists(); html += '<ul>'; inUl = true }\n      html += '<li>' + inline(ln.replace(/^\\s*[-*+]\\s+/, '')) + '</li>'; i++; continue\n    }\n    if (/^\\s*\\d+\\.\\s+/.test(ln)) {\n      if (!inOl) { closeLists(); html += '<ol>'; inOl = true }\n      html += '<li>' + inline(ln.replace(/^\\s*\\d+\\.\\s+/, '')) + '</li>'; i++; continue\n    }\n    if (/^\\s*$/.test(ln)) { closeLists(); i++; continue }\n    closeLists()\n    html += '<p>' + inline(ln) + '</p>'\n    i++\n  }\n  closeLists()\n  return html\n}\n\nexport default {\n  css: `\n    :host { display:block; height:100%; }\n    .doc { height:100%; overflow:auto; background:#f4f4f5; box-sizing:border-box;\n      font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; }\n    .page { max-width:920px; margin:0 auto; background:#fff; min-height:100%;\n      padding:56px 64px 96px; box-sizing:border-box; color:#3f3f46; line-height:1.65; }\n    .page h1 { font-size:32px; font-weight:800; color:#18181b; line-height:1.2; margin:0 0 8px; }\n    .page h2 { font-size:23px; font-weight:700; color:#18181b; margin:40px 0 14px;\n      padding-top:18px; border-top:1px solid #e4e4e7; }\n    .page h3 { font-size:17px; font-weight:700; color:#27272a; margin:24px 0 10px; }\n    .page h4,.page h5,.page h6 { font-size:14px; font-weight:700; color:#3f3f46; margin:18px 0 8px; }\n    .page p { font-size:14.5px; margin:10px 0; }\n    .page ul,.page ol { font-size:14.5px; margin:10px 0; padding-left:24px; }\n    .page li { margin:5px 0; }\n    .page a { color:#2563eb; text-decoration:none; }\n    .page a:hover { text-decoration:underline; }\n    .page strong { color:#18181b; font-weight:700; }\n    .page code { font-family: \"Roboto Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;\n      font-size:12.5px; background:#f4f4f5; color:#9333ea; padding:1px 5px; border-radius:4px; }\n    .page hr { border:0; border-top:1px solid #e4e4e7; margin:28px 0; }\n    .page blockquote { margin:14px 0; padding:10px 16px; background:#fffbeb;\n      border-left:3px solid #f59e0b; color:#78350f; border-radius:0 6px 6px 0; font-size:14px; }\n    .page blockquote code { background:#fef3c7; color:#92400e; }\n    .cb { margin:14px 0; border-radius:8px; overflow:hidden; background:#18181b; }\n    .cb-lang { font:11px/1 \"Roboto Mono\", monospace; color:#a1a1aa; padding:8px 14px;\n      background:#27272a; text-transform:uppercase; letter-spacing:.06em; }\n    .cb pre { margin:0; padding:16px; overflow-x:auto;\n      font-family:\"Roboto Mono\", ui-monospace, monospace; font-size:12.5px; line-height:1.55; color:#e4e4e7; }\n    .page table { border-collapse:collapse; width:100%; margin:14px 0; font-size:13px; }\n    .page th { text-align:left; background:#fafafa; color:#18181b; font-weight:700;\n      padding:8px 12px; border:1px solid #e4e4e7; }\n    .page td { padding:8px 12px; border:1px solid #e4e4e7; vertical-align:top; }\n    .page tr:nth-child(even) td { background:#fafafa; }\n  `,\n  render(d2){\n    const md = (d2.param && d2.param('markdown')) || DEFAULT_MD\n    return '<div class=\"doc\"><div class=\"page\">' + mdToHtml(md) + '</div></div>'\n  }\n}"}},"markdown":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"markdown"}}}}}}}}}},{"_id":"6acf83bd8eeac4a4b818b1b3","component":"data2-core-types@Component","system":"69133afb854168f247b6f9af","params":{"extends":"data2-core-primitives@Block","component":"fpsPrototype","package":"aramis","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Fashion Personal Shopper — protótipo (fase 0.5, descartável). Mock = catálogo Wake real de 2026-10-14.\nconst THEME = 'https://aramis.data2.media/file/FoPSiAH1DGrpNy6iK1Enh'\nconst UI = 'https://aramis.data2.media/file/Dl7LSkkqcZi-rDhz8sLdb'\nconst LUCIDE = 'lucide@0.468.0'\nconst DATA = 'https://aramis.data2.media/file/H68P8sGI-m4BPFHvoudiN'\nlet MOCK = null\n\nconst SWATCH = { BRANCO: '#f4f2ec', ALUMINIO: '#b9bcbf', CAQUI: '#b49b74', CONHAQUE: '#8a4b2a', VERDE: '#5c6b4a', PRETO: '#151515' }\nconst START_IMG = 'https://aramis.data2.media/file/ox2GPzmQADZ_4Fov70xWP'\nconst SUGGESTIONS = ['Um casamento na praia', 'Uma reunião que importa', 'Uma viagem a trabalho', 'Um jantar especial', 'Renovar o guarda-roupa', 'Um presente']\nconst PROTO_MSG = 'Protótipo: aqui o personal shopper refaria os looks.'\n\nconst total = (look) => look.pieces.reduce((s, p) => s + p.price, 0)\nconst heroOf = (look) => look.pieces[look.heroFrom].hero\nconst findLook = (d2, id) => d2.state.looks.find((l) => l.id === id)\n\nfunction ic(d2, name, size) {\n  const ui = d2.lib(UI)\n  const lucide = d2.lib(LUCIDE)\n  return ui && lucide ? ui.icon(lucide.icons || lucide.default?.icons || lucide, name, size) : ''\n}\n\nfunction price(d2, p) {\n  const ui = d2.lib(UI)\n  const fmt = ui ? ui.brl : (n) => n\n  return p.list > p.price ? `<s>${fmt(p.list)}</s>${fmt(p.price)}` : fmt(p.price)\n}\n\nfunction renderStart(d2) {\n  return `\n    <section class=\"start\">\n      <img class=\"start-img\" src=\"${START_IMG}\" alt=\"\">\n      <div class=\"start-shade\"></div>\n      <header class=\"start-top\"><span class=\"eyebrow light\">${d2.param('productName', 'Fashion Personal Shopper')}</span></header>\n      <div class=\"start-body\">\n        <h1 class=\"display start-title\">Qual presença o momento pede?</h1>\n        <p class=\"start-sub\">Um compromisso, uma viagem, uma virada. Conte o momento e a gente edita a sua versão.</p>\n        <div class=\"suggestions\">${SUGGESTIONS.map((o) => `<button type=\"button\" class=\"chip glass\" data-act=\"suggest\" data-v=\"${o}\">${o}</button>`).join('')}</div>\n        <form class=\"start-form\">\n          <input class=\"input glass\" name=\"q\" placeholder=\"Conte o que vem por aí…\" autocomplete=\"off\">\n          <button class=\"btn btn-icon round send-light\" aria-label=\"Enviar\">${ic(d2, 'ArrowUp', 20)}</button>\n        </form>\n      </div>\n    </section>`\n}\n\nfunction renderLoading(d2) {\n  return `\n    <section class=\"loading\">\n      <p class=\"eyebrow\">Editando a sua versão</p>\n      <p class=\"caption\">“${d2.state.answers.occasion || ''}”</p>\n      <div class=\"skeleton sk-hero\"></div>\n      <div class=\"skeleton sk-line\"></div>\n      <div class=\"skeleton sk-line short\"></div>\n    </section>`\n}\n\nfunction renderIntro(d2) {\n  const o = MOCK.occasion\n  return `\n    <article class=\"page cover\" data-key=\"intro\">\n      <img class=\"cover-img\" src=\"${o.cover}\" alt=\"\">\n      <div class=\"cover-text\">\n        <p class=\"eyebrow light\">Seu personal shopper · ${d2.fmt.truncate(d2.state.answers.occasion || 'Casamento', 40)}</p>\n        <h1 class=\"display intro-title\">${o.title}</h1>\n        <ul class=\"meta\">\n          <li>${ic(d2, 'MapPin')}${o.where}</li>\n          <li>${ic(d2, 'CalendarDays')}${o.when}</li>\n          <li>${ic(d2, 'Sun')}${o.weather}</li>\n        </ul>\n        <p class=\"lede\">${MOCK.intro}</p>\n        <ol class=\"index\">${d2.state.looks.map((l, i) => `\n          <li><button data-act=\"goto\" data-i=\"${i + 1}\"><span>0${i + 1}</span><span>${l.title}</span>${ic(d2, 'ArrowRight', 14)}</button></li>`).join('')}\n        </ol>\n      </div>\n    </article>`\n}\n\nfunction renderLook(d2, look, i) {\n  const n = d2.state.looks.length\n  const pieces = look.pieces.map((p) => `\n    <div class=\"piece\" data-key=\"${look.id}-${p.id}\">\n      <img src=\"${p.img}\" alt=\"${p.name}\" loading=\"lazy\">\n      <div class=\"piece-info\">\n        <span class=\"eyebrow\">${p.role}</span>\n        <span class=\"piece-name\">${p.name}</span>\n        <span class=\"price\">${price(d2, p)}</span>\n      </div>\n      ${MOCK.alternatives[p.role] ? `<button class=\"btn btn-ghost btn-icon swap\" data-act=\"swap\" data-look=\"${look.id}\" data-role=\"${p.role}\" aria-label=\"Trocar ${p.role}\">${ic(d2, 'Shuffle')}</button>` : ''}\n    </div>`).join('')\n  return `\n    <article class=\"page look\" data-key=\"${look.id}\">\n      <figure class=\"hero\">\n        <img src=\"${heroOf(look)}\" alt=\"${look.title}\">\n        <figcaption>\n          <span class=\"eyebrow light\">Look ${i + 1} de ${n}</span>\n          <h2 class=\"display look-title\">${look.title}</h2>\n        </figcaption>\n      </figure>\n      <div class=\"look-body\">\n        <p class=\"caption\">${look.caption}</p>\n        <div class=\"pieces\">${pieces}</div>\n        <div class=\"look-actions\">\n          <div class=\"look-total\"><span class=\"eyebrow\">Look completo</span><span class=\"price total\">${price(d2, { price: total(look), list: 0 })}</span></div>\n          <button class=\"btn btn-outline btn-lg\" data-act=\"tryon\" data-look=\"${look.id}\">${ic(d2, 'ScanFace')}Ver em mim</button>\n          <button class=\"btn btn-primary btn-lg\" data-act=\"cart\" data-look=\"${look.id}\">${ic(d2, 'ShoppingBag')}Comprar o look</button>\n        </div>\n      </div>\n    </article>`\n}\n\nfunction renderMore(d2) {\n  const opts = ['Mais formal', 'Sem linho', 'Até R$ 2.000', 'Só os sapatos', 'Com a minha foto']\n  return `\n    <article class=\"page more\" data-key=\"more\">\n      <p class=\"eyebrow\">Edite sua versão</p>\n      <h2 class=\"display more-title\">Quer seguir por outro caminho?</h2>\n      <div class=\"opts\">${opts.map((o) => `<button class=\"chip chip-lg\" data-act=\"refine\" data-v=\"${o}\">${o}</button>`).join('')}</div>\n    </article>`\n}\n\nfunction renderTryOn(d2) {\n  const look = findLook(d2, d2.state.tryOn)\n  const shirt = look.pieces.find((p) => p.role === 'Camisa')\n  const img = shirt && d2.state.tryColor != null ? MOCK.colorways[d2.state.tryColor].hero : heroOf(look)\n  const swatches = shirt ? MOCK.colorways.map((c, i) => `\n    <button class=\"swatch\" data-act=\"color\" data-i=\"${i}\" aria-pressed=\"${d2.state.tryColor === i}\" aria-label=\"${c.color}\">\n      <i style=\"background:${SWATCH[c.color] || '#ccc'}\"></i><span>${c.color.toLowerCase()}</span>\n    </button>`).join('') : ''\n  return `\n    <section class=\"tryon\" role=\"dialog\" aria-label=\"Ver em mim\">\n      <header class=\"tryon-top\">\n        <button class=\"btn btn-icon round\" data-act=\"close\" aria-label=\"Fechar\">${ic(d2, 'X', 20)}</button>\n        <span class=\"badge\">${ic(d2, 'Sparkles', 12)}Prévia · no app, com a sua foto</span>\n      </header>\n      <img class=\"tryon-img\" src=\"${img}\" alt=\"\">\n      <div class=\"tryon-panel\">\n        <p class=\"eyebrow\">${look.title}</p>\n        ${shirt ? `<p class=\"tryon-label\">Cor da camisa</p><div class=\"swatches\">${swatches}</div>` : '<p class=\"caption\">Troque peças direto no look.</p>'}\n        <div class=\"row\">\n          <button class=\"btn btn-outline btn-lg\" data-act=\"close\">Voltar ao look</button>\n          <button class=\"btn btn-primary btn-lg grow\" data-act=\"cart\" data-look=\"${look.id}\">${ic(d2, 'ShoppingBag')}Comprar</button>\n        </div>\n      </div>\n    </section>`\n}\n\nfunction renderSheet(d2) {\n  const sh = d2.state.sheet\n  const look = findLook(d2, sh.look)\n  let inner\n  if (sh.type === 'swap') {\n    const cur = look.pieces.find((p) => p.role === sh.role)\n    inner = `\n      <p class=\"eyebrow\">Trocar ${sh.role.toLowerCase()}</p>\n      <h3 class=\"display sheet-title\">Outras opções pra esse look</h3>\n      <div class=\"alts\">${MOCK.alternatives[sh.role].map((a) => `\n        <button class=\"alt\" data-act=\"pick-alt\" data-id=\"${a.id}\" aria-pressed=\"${a.id === cur.id}\">\n          <img src=\"${a.img}\" alt=\"\"><span class=\"eyebrow\">${a.color}</span><span class=\"piece-name\">${a.name}</span><span class=\"price\">${price(d2, a)}</span>\n        </button>`).join('')}\n      </div>`\n  } else {\n    const chosen = d2.state.sizes[look.id] || {}\n    const ready = look.pieces.every((p) => chosen[p.id])\n    inner = `\n      <p class=\"eyebrow\">Sacola</p>\n      <h3 class=\"display sheet-title\">${look.title}</h3>\n      <div class=\"cart\">${look.pieces.map((p) => `\n        <div class=\"cart-row\" data-key=\"c-${p.id}\">\n          <img src=\"${p.img}\" alt=\"\">\n          <div class=\"grow\">\n            <span class=\"piece-name\">${p.name}</span>\n            <span class=\"price\">${price(d2, p)}</span>\n            <div class=\"sizes\">${p.sizes.map((z) => `<button class=\"size\" data-act=\"size\" data-id=\"${p.id}\" data-v=\"${z}\" aria-pressed=\"${chosen[p.id] === z}\">${z}</button>`).join('')}</div>\n          </div>\n        </div>`).join('')}\n      </div>\n      <div class=\"sheet-foot\">\n        <div class=\"cart-total\"><span>Total</span><span class=\"price total\">${price(d2, { price: total(look), list: 0 })}</span></div>\n        <button class=\"btn btn-primary btn-lg btn-block\" data-act=\"checkout\" data-look=\"${look.id}\" ${ready ? '' : 'disabled'}>\n          ${ready ? 'Adicionar o look à sacola' : 'Escolha os tamanhos'}\n        </button>\n      </div>`\n  }\n  return `\n    <div class=\"scrim\" data-act=\"close-sheet\"></div>\n    <section class=\"sheet\" role=\"dialog\"><span class=\"grabber\"></span>${inner}</section>`\n}\n\nfunction renderFeed(d2) {\n  const s = d2.state\n  return `\n    <section class=\"feed\">\n      <div class=\"pages\">\n        ${renderIntro(d2)}\n        ${s.looks.map((l, i) => renderLook(d2, l, i)).join('')}\n        ${renderMore(d2)}\n      </div>\n      <form class=\"ask\">\n        <input class=\"input\" name=\"q\" placeholder=\"Quer ajustar algo? É só dizer.\" autocomplete=\"off\">\n        <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-act=\"mic\" aria-label=\"Falar\">${ic(d2, 'Mic', 18)}</button>\n        <button class=\"btn btn-primary btn-icon round\" aria-label=\"Enviar\">${ic(d2, 'ArrowUp', 18)}</button>\n      </form>\n      ${s.tryOn ? renderTryOn(d2) : ''}\n      ${s.sheet ? renderSheet(d2) : ''}\n    </section>`\n}\n\nfunction toast(d2, msg) {\n  d2.state.toast = msg\n  clearTimeout(d2.state._t)\n  d2.state._t = setTimeout(() => { d2.state.toast = null }, 2600)\n}\n\nfunction go(d2, act, el) {\n  const s = d2.state\n  const v = el.dataset.v\n  if (act === 'suggest') {\n    const input = d2.$('.start-form input')\n    input.value = v\n    input.focus()\n  }\n  else if (act === 'goto') d2.$$('.page')[+el.dataset.i]?.scrollIntoView({ behavior: 'smooth' })\n  else if (act === 'tryon') d2.setState({ tryOn: el.dataset.look, tryColor: null })\n  else if (act === 'close') s.tryOn = null\n  else if (act === 'color') {\n    const i = +el.dataset.i\n    const look = findLook(d2, s.tryOn)\n    s.looks = s.looks.map((l) => (l === look ? { ...l, pieces: l.pieces.map((p) => (p.role === 'Camisa' ? { ...p, ...MOCK.colorways[i] } : p)) } : l))\n    s.tryColor = i\n  } else if (act === 'swap') s.sheet = { type: 'swap', look: el.dataset.look, role: el.dataset.role }\n  else if (act === 'pick-alt') {\n    const alt = MOCK.alternatives[s.sheet.role].find((a) => a.id === el.dataset.id)\n    const { look, role } = s.sheet\n    s.looks = s.looks.map((l) => (l.id === look ? { ...l, pieces: l.pieces.map((p) => (p.role === role ? { ...p, ...alt } : p)) } : l))\n    s.sheet = null\n    toast(d2, 'Peça trocada')\n  } else if (act === 'cart') d2.setState({ tryOn: null, sheet: { type: 'cart', look: el.dataset.look } })\n  else if (act === 'size') s.sizes = { ...s.sizes, [s.sheet.look]: { ...(s.sizes[s.sheet.look] || {}), [el.dataset.id]: v } }\n  else if (act === 'checkout') {\n    const look = findLook(d2, el.dataset.look)\n    const items = look.pieces.map((p) => ({ productId: p.id, size: s.sizes[look.id][p.id], link: p.link }))\n    // ponytail: postMessage cru pro host; a versão real usa o contrato do PostEventsWebView.\n    window.parent?.postMessage({ type: 'fps:add-to-cart', items }, '*')\n    s.sheet = null\n    toast(d2, `Look adicionado à sacola · ${items.length} peças`)\n  } else if (act === 'close-sheet') s.sheet = null\n  else if (act === 'refine' || act === 'mic') toast(d2, PROTO_MSG)\n}\n\nfunction finishStories(d2) {\n  d2.state.screen = 'loading'\n  setTimeout(() => { d2.state.screen = 'feed' }, 1400)\n}\n\nexport default {\n  render(d2) {\n    d2.lib({ css: THEME })\n    const ui = d2.lib(UI)\n    const data = d2.lib(DATA)\n    MOCK = data?.default || data || null\n    if (!MOCK) return `<div class=\"app\"><p class=\"caption\" style=\"padding:20px\">${d2.lib.state(DATA) === 'error' ? `erro: ${d2.lib.error(DATA)}` : ''}</p></div>`\n    const s = d2.state\n    if (!s.screen) Object.assign(s, { screen: d2.param('startAt', 'stories'), step: 0, answers: {}, looks: MOCK.looks, sizes: {}, sheet: null, tryOn: null, tryColor: null, toast: null })\n    if (ui && !s._init) {\n      s._init = true\n      ui.ensureFonts()\n      const tenant = d2.param('tenantTheme')\n      if (tenant && typeof tenant === 'object') ui.applyTenant(tenant)\n    }\n    const screen = s.screen === 'loading' ? renderLoading(d2) : s.screen === 'feed' ? renderFeed(d2) : renderStart(d2)\n    return `<div class=\"app\">${screen}${s.toast ? `<div class=\"toast\" role=\"status\">${s.toast}</div>` : ''}</div>`\n  },\n  mounted(d2) {\n    d2.on('[data-act]', 'click', (e) => {\n      const el = e.target.closest('[data-act]')\n      if (el && !el.disabled) go(d2, el.dataset.act, el)\n    })\n    d2.on('form', 'submit', (e) => {\n      e.preventDefault()\n      const form = e.target.closest('form')\n      const input = form?.querySelector('input')\n      const v = input?.value.trim()\n      if (!v) return\n      if (form.classList.contains('start-form')) {\n        d2.state.answers = { occasion: v }\n        finishStories(d2)\n        return\n      }\n      input.value = ''\n      toast(d2, PROTO_MSG)\n    })\n  },\n  css: `\n:host { display: block; height: 100%; container-type: size; }\n.app { position: relative; height: 100%; overflow: hidden; background: var(--background); }\n\n.start { position: relative; height: 100%; color: #fff; background: #1c1b19; }\n.start-img { position: absolute; top: 0; left: 0; width: 100%; height: 64%; object-fit: cover; object-position: center bottom; }\n.start-shade { position: absolute; top: 0; left: 0; width: 100%; height: 64%; background: linear-gradient(rgba(0,0,0,.35), transparent 18%, transparent 86%, #1c1b19); }\n.start-top { position: absolute; top: 0; left: 0; right: 0; padding: 20px; }\n.start-body { position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; gap: 14px; padding: 20px 20px 28px; max-width: 560px; }\n.start-title { font-size: clamp(28px, 8cqi, 40px); }\n.start-sub { font-size: 15px; line-height: 1.45; color: rgba(255,255,255,.8); margin-top: -6px; }\n.chip.glass, .input.glass { background: rgba(255,255,255,.14); color: #fff; border: 1px solid rgba(255,255,255,.28); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }\n.suggestions { display: flex; gap: 8px; overflow-x: auto; margin: 0 -20px; padding: 0 20px; scrollbar-width: none; }\n.suggestions::-webkit-scrollbar { display: none; }\n.suggestions .chip { flex: 0 0 auto; }\n.suggestions .chip { height: 34px; font-size: 13px; }\n.suggestions .chip.glass:hover { background: rgba(255,255,255,.26); }\n.start-form { display: flex; gap: 8px; align-items: center; }\n.start-form .input { flex: 1; height: 52px; }\n.start-form .input::placeholder { color: rgba(255,255,255,.7); }\n.btn.send-light { width: 52px; height: 52px; background: #fff; color: #0a0a0a; }\n.chip-lg { height: 44px; padding: 0 18px; font-size: 15px; }\n.chip-lg:hover { border-color: var(--foreground); }\n\n.loading { height: 100%; display: flex; flex-direction: column; gap: 12px; padding: 24px 20px; max-width: 560px; margin: 0 auto; }\n.sk-hero { flex: 1; max-height: 70%; } .sk-line { height: 20px; } .sk-line.short { width: 60%; }\n\n.feed { position: relative; height: 100%; }\n.pages { height: 100%; overflow-y: auto; scroll-snap-type: y mandatory; scrollbar-width: none; }\n.pages::-webkit-scrollbar { display: none; }\n.page { scroll-snap-align: start; min-height: 100%; padding-bottom: 80px; }\n\n.cover { position: relative; height: 100cqh; padding: 0; overflow: hidden; background: #1a1a1a; }\n.cover-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }\n.cover-text { position: absolute; inset: auto 0 0 0; display: flex; flex-direction: column; gap: 12px; padding: 160px 20px 92px; color: #fff;\n  background: linear-gradient(transparent, rgba(0,0,0,.25) 25%, rgba(0,0,0,.6)); }\n.intro-title { font-size: clamp(36px, 10cqi, 56px); }\n.meta { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; color: rgba(255,255,255,.85); font-size: 13px; }\n.meta li { display: inline-flex; align-items: center; gap: 6px; }\n.lede { font-size: 15px; line-height: 1.5; max-width: 36ch; }\n.index { list-style: none; padding: 0; margin-top: 4px; border-top: 1px solid rgba(255,255,255,.3); }\n.index button { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.3); text-align: left; font-size: 14px; color: #fff; }\n.index button span:first-child { font-family: var(--font-display); font-size: 11px; letter-spacing: .12em; opacity: .7; }\n.index button span:nth-child(2) { flex: 1; }\n\n.look { display: flex; flex-direction: column; height: 100cqh; min-height: 0; }\n.hero { position: relative; margin: 0; flex: 1; min-height: 0; background: var(--muted); }\n.hero img { width: 100%; height: 100%; object-fit: cover; object-position: center 20%; }\n.hero figcaption { position: absolute; inset: auto 0 0 0; padding: 64px 20px 20px; background: linear-gradient(transparent, rgba(0,0,0,.5)); }\n.eyebrow.light { color: rgba(255,255,255,.85); }\n.look-title { color: #fff; font-size: clamp(24px, 7cqi, 36px); margin-top: 6px; }\n.look-body { display: flex; flex-direction: column; gap: 12px; padding: 16px 20px; }\n.look-body .caption { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }\n.pieces { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 20px; margin: 0 -20px; padding: 0 20px 4px; scrollbar-width: none; }\n.piece { position: relative; flex: 0 0 104px; width: 104px; min-width: 0; scroll-snap-align: start; display: flex; flex-direction: column; gap: 8px; }\n.piece img { aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); background: var(--muted); }\n.piece-info { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }\n.piece-info .price s { display: block; margin: 0; font-size: 11px; }\n.piece-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.35; }\n.swap { position: absolute; top: 6px; right: 6px; width: 32px; height: 32px; border-radius: 999px; background: rgba(255,255,255,.9); }\n.look-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.look-total { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; }\n.total { font-size: 20px; }\n\n.more { display: flex; flex-direction: column; justify-content: center; gap: 16px; padding: 48px 20px 120px; max-width: 560px; margin: 0 auto; }\n.more-title { font-size: clamp(26px, 7cqi, 36px); }\n\n.ask { position: absolute; left: 0; right: 0; bottom: 0; display: flex; gap: 6px; align-items: center; padding: 12px 16px 16px;\n  background: none; }\n.ask .input { flex: 1; box-shadow: 0 1px 2px rgba(0,0,0,.04); }\n.btn.round { border-radius: 999px; }\n\n.tryon { position: absolute; inset: 0; z-index: 10; background: #0a0a0a; display: flex; flex-direction: column; animation: fps-in .25s ease-out; }\n.tryon-top { position: absolute; top: 0; left: 0; right: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; }\n.tryon-top .round { background: rgba(255,255,255,.9); }\n.tryon-top .badge { gap: 4px; background: rgba(255,255,255,.9); }\n.tryon-img { flex: 1; min-height: 0; width: 100%; object-fit: cover; object-position: center 20%; }\n.tryon-panel { background: var(--background); border-radius: 16px 16px 0 0; margin-top: -16px; padding: 20px; display: flex; flex-direction: column; gap: 12px; }\n.tryon-label { font-size: 14px; font-weight: 500; }\n.swatches { display: flex; gap: 10px; overflow-x: auto; }\n.swatch { display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 11px; color: var(--muted-foreground); text-transform: capitalize; }\n.swatch i { width: 36px; height: 36px; border-radius: 999px; border: 1px solid var(--border); box-shadow: inset 0 0 0 3px var(--background); }\n.swatch[aria-pressed=\"true\"] i { border: 2px solid var(--foreground); }\n.row { display: flex; gap: 8px; } .grow { flex: 1; min-width: 0; }\n\n.scrim { position: absolute; inset: 0; z-index: 20; background: rgba(0,0,0,.4); animation: fps-fade .2s ease-out; }\n.sheet { position: absolute; left: 0; right: 0; bottom: 0; z-index: 21; max-height: 85%; overflow-y: auto; background: var(--background);\n  border-radius: 16px 16px 0 0; padding: 8px 20px 24px; display: flex; flex-direction: column; gap: 12px; animation: fps-up .25s cubic-bezier(.4,0,.2,1); }\n.grabber { align-self: center; width: 36px; height: 4px; border-radius: 4px; background: var(--border); margin-bottom: 4px; }\n.sheet-title { font-size: 20px; }\n.alts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n.alt { display: flex; flex-direction: column; gap: 6px; text-align: left; font-size: 13px; }\n.alt img { aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); border: 2px solid transparent; }\n.alt[aria-pressed=\"true\"] img { border-color: var(--foreground); }\n.cart { display: flex; flex-direction: column; gap: 14px; }\n.cart-row { display: flex; gap: 12px; font-size: 14px; }\n.cart-row img { width: 72px; aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); }\n.cart-row .grow { display: flex; flex-direction: column; gap: 4px; }\n.sizes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }\n.size { min-width: 40px; height: 32px; padding: 0 8px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 13px; }\n.size[aria-pressed=\"true\"] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }\n.sheet-foot { position: sticky; bottom: -24px; margin: 0 -20px -24px; padding: 12px 20px 24px; background: var(--background); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }\n.cart-total { display: flex; justify-content: space-between; align-items: baseline; }\n.btn[disabled] { opacity: .5; cursor: not-allowed; }\n\n.toast { position: absolute; left: 50%; bottom: 84px; transform: translateX(-50%); z-index: 30; max-width: calc(100% - 32px); padding: 10px 16px;\n  border-radius: var(--radius); background: var(--foreground); color: var(--background); font-size: 13px; animation: fps-fade .2s ease-out; }\n\n@keyframes fps-in { from { opacity: 0; transform: scale(.98); } }\n@keyframes fps-up { from { transform: translateY(100%); } }\n@keyframes fps-fade { from { opacity: 0; } }\n\n@container (min-width: 900px) {\n  .look { display: grid; grid-template-columns: 55% 1fr; height: 100cqh; padding-bottom: 0; }\n  .hero { height: 100%; min-height: 0; }\n  .look-body { justify-content: center; padding: 48px 48px 112px; overflow-y: auto; }\n  .pieces { flex-wrap: wrap; margin: 0; padding: 0; overflow: visible; }\n  .piece { flex-basis: calc(25% - 8px); width: auto; }\n  .ask { left: 55%; padding: 16px 48px 24px; }\n  .start-img { left: auto; right: 0; width: auto; height: 100%; max-width: 60%; aspect-ratio: 1600 / 2240; object-position: center; }\n  .start-shade { left: auto; right: 0; width: 60%; height: 100%; background: linear-gradient(90deg, #1c1b19, transparent 30%); }\n  .start-top { padding: 32px 56px; }\n  .start-body { top: 0; justify-content: center; padding: 56px; max-width: 42%; }\n  .start-title { font-size: 48px; }\n  .suggestions { flex-wrap: wrap; overflow: visible; margin: 0; padding: 0; }\n  .cover-text { inset: 0 auto 0 0; width: 560px; justify-content: flex-end; padding: 48px 56px 64px; background: linear-gradient(90deg, rgba(0,0,0,.6), rgba(0,0,0,.25) 70%, transparent); }\n  .sheet { left: auto; width: 440px; top: 0; max-height: none; border-radius: 0; animation-name: fps-left; }\n  .tryon { flex-direction: row; background: #efefef; }\n  .tryon-img { object-fit: contain; }\n  .tryon-panel { width: 400px; margin: 0; border-radius: 0; justify-content: center; }\n}\n@keyframes fps-left { from { transform: translateX(100%); } }\n`,\n}\n"}},"productName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"productName"}}}},"startAt":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"startAt"}}}},"tenantTheme":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"tenantTheme"}}}}}}}}}},{"_id":"6acfea4316c0cadc570708c5","component":"data2-core-types@Component","system":"69133afb854168f247b6f9af","params":{"extends":"data2-core-primitives@Block","component":"fpsShell","package":"aramis","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Fashion Personal Shopper — shell de produção. Renderiza os blocos do turno (fps-conversa); textos, imagens e tema vêm do fps-public-config.\nconst THEME = 'https://aramis.data2.media/file/FoPSiAH1DGrpNy6iK1Enh'\nconst UI = 'https://aramis.data2.media/file/Dl7LSkkqcZi-rDhz8sLdb'\nconst LUCIDE = 'lucide@0.468.0'\n\nconst keep = (k, v) => { try { v == null ? localStorage.removeItem(k) : localStorage.setItem(k, v) } catch (e) {} }\nconst read = (k) => { try { return localStorage.getItem(k) } catch (e) { return null } }\nconst esc = (s) => String(s ?? '').replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]))\n\nfunction t(d2, key, vars) {\n  const copy = d2.state.cfg?.copy || {}\n  let v = key.split('.').reduce((o, k) => (o == null ? o : o[k]), copy)\n  if (typeof v !== 'string') v = copy[key]\n  if (typeof v !== 'string') return ''\n  return vars ? v.replace(/\\{(\\w+)\\}/g, (_, k) => vars[k] ?? '') : v\n}\n\nfunction ic(d2, name, size) {\n  const ui = d2.lib(UI)\n  const lucide = d2.lib(LUCIDE)\n  return ui && lucide ? ui.icon(lucide.icons || lucide.default?.icons || lucide, name, size) : ''\n}\n\nconst brl = (d2, n) => (d2.lib(UI)?.brl || ((x) => x))(n)\n// Texto da cópia do tenant com padrão local (blocos novos antes de a cópia ganhar a chave).\nconst tt = (d2, key, fb, vars) => t(d2, key, vars) || (vars ? fb.replace(/\\{(\\w+)\\}/g, (_, k) => vars[k] ?? '') : fb)\nconst EXTRAS = ['sizeAdvisor', 'styleQuiz', 'compareTable', 'capsuleGrid', 'stockAlert', 'closet']\nconst price = (d2, p) => (p.listPrice > p.price ? `<s>${brl(d2, p.listPrice)}</s>${brl(d2, p.price)}` : brl(d2, p.price))\nconst totalOf = (look) => look.pieces.reduce((s, p) => s + (p.price || 0), 0)\nconst findLook = (d2, id) => [...d2.state.looks].reverse().find((l) => l.lookId === id)\n\n// Polaroids arrastados: posição e ordem por cartão, memória só da visita (zera quando o look some).\nconst POS = new Map()\nlet ZTOP = 20\nconst posDe = (key) => POS.get(key) || POS.set(key, { dx: 0, dy: 0 }).get(key)\nconst posStyle = (key) => { const p = POS.get(key) || { dx: 0, dy: 0 }; return `style=\"--dx:${p.dx}px;--dy:${p.dy}px;${p.z != null ? `--z:${p.z}` : ''}\"` }\nfunction alternar(el, key) {\n  const p = posDe(key)\n  const zs = [...el.parentElement.querySelectorAll('.pola')].map((x) => Number(getComputedStyle(x).zIndex) || 0)\n  const mine = Number(getComputedStyle(el).zIndex) || 0\n  const naFrente = mine >= Math.max(...zs) && zs.filter((z) => z === mine).length === 1\n  p.z = naFrente ? 0 : ++ZTOP\n  el.style.setProperty('--z', p.z)\n}\n\n// Resolve na primeira imagem emitida pelo shadow (first_draw); overlay e recolors continuam no servidor sem segurar a tela.\nfunction primeiraImagem(d2, body) {\n  const h = d2.rush(d2.params.provador)\n  const urlDe = (r) => (typeof r?.data === 'string' ? r.data : typeof r === 'string' ? r : null)\n  if (typeof h.stream !== 'function') return h.invoke(body).then((r) => ({ url: urlDe(r), cedo: false }))\n  return new Promise((resolve, reject) => {\n    let pronto = false\n    const fim = (url, cedo) => { if (pronto) return; pronto = true; resolve(url ? { url, cedo } : null) }\n    h.stream(body, (u) => {\n      const p = u?.payload || u\n      if ((p?.status === 'first_draw' || p?.status === 'final_draw') && typeof p.image_result === 'string') fim(p.image_result, true)\n    }).then((r) => fim(urlDe(r), false)).catch((e) => { if (!pronto) reject(e) })\n  })\n}\n\nasync function reduzir(file, max = 1280) {\n  try {\n    const bmp = await createImageBitmap(file)\n    const k = Math.min(1, max / Math.max(bmp.width, bmp.height))\n    if (k === 1) return file\n    const c = document.createElement('canvas')\n    c.width = Math.round(bmp.width * k)\n    c.height = Math.round(bmp.height * k)\n    c.getContext('2d').drawImage(bmp, 0, 0, c.width, c.height)\n    const blob = await new Promise((r) => c.toBlob(r, 'image/jpeg', 0.9))\n    return blob ? new File([blob], 'foto.jpg', { type: 'image/jpeg' }) : file\n  } catch (e) {\n    return file\n  }\n}\n\nfunction ratioDe(w, h) {\n  const r = w / h\n  const opcoes = { '1:1': 1, '2:3': 2 / 3, '3:2': 1.5, '3:4': 0.75, '4:3': 4 / 3, '4:5': 0.8, '5:4': 1.25, '9:16': 9 / 16, '16:9': 16 / 9 }\n  return Object.entries(opcoes).sort((a, b) => Math.abs(a[1] - r) - Math.abs(b[1] - r))[0][0]\n}\n\nconst salvoDe = (d2, lookId) => (d2.state.salvos || []).find((x) => x.lookId === lookId)\n\nasync function carregarSalvos(d2) {\n  const s = d2.state\n  try {\n    const r = await d2.rush(d2.params.salvos || 'fps-looks-salvos-api').invoke({ acao: 'listar', deviceId: s.deviceId })\n    s.salvos = (r?.data || r)?.itens || []\n  } catch (e) {}\n}\n\nasync function salvarLook(d2, lookId) {\n  const s = d2.state\n  const ja = salvoDe(d2, lookId)\n  const look = findLook(d2, lookId) || (ja ? { ...ja.look, lookId, tryonImage: ja.tryonImage } : null)\n  if (!look) return\n  const snap = { title: look.title, caption: look.caption, pieces: look.pieces, total: totalOf(look), heroImage: look.heroImage, origin: look.origin }\n  s.salvos = ja ? s.salvos.filter((x) => x.lookId !== lookId) : [{ lookId, look: snap, tryonImage: look.tryonImage || s.tryOn?.img || null, salvoEm: new Date().toISOString() }, ...(s.salvos || [])]\n  toast(d2, t(d2, ja ? 'salvos.removido' : 'salvos.salvo'))\n  track(d2, ja ? 'look_removido' : 'look_salvo', { lookId, productIds: look.pieces.map((p) => p.productId), valor: totalOf(look) })\n  try {\n    const r = await d2.rush(d2.params.salvos || 'fps-looks-salvos-api').invoke({ acao: ja ? 'remover' : 'salvar', deviceId: s.deviceId, sessionId: s.sessionId, tenant: s.cfg?.slug, lookId, look: snap, tryonImage: look.tryonImage || s.tryOn?.img || null })\n    const itens = (r?.data || r)?.itens\n    if (Array.isArray(itens)) s.salvos = itens\n  } catch (e) {}\n}\n\n// Look salvo acompanha o look vivo: peça trocada ou prévia nova atualizam o snapshot e a imagem no device e no servidor.\nasync function sincronizarSalvo(d2, lookId) {\n  const s = d2.state\n  const look = findLook(d2, lookId)\n  if (!look || !salvoDe(d2, lookId)) return\n  const snap = { title: look.title, caption: look.caption, pieces: look.pieces, total: totalOf(look), heroImage: look.heroImage, origin: look.origin }\n  const tryonImage = look.tryonImage || null\n  s.salvos = s.salvos.map((x) => (x.lookId === lookId ? { ...x, look: snap, tryonImage } : x))\n  try { await d2.rush(d2.params.salvos || 'fps-looks-salvos-api').invoke({ acao: 'atualizar', deviceId: s.deviceId, lookId, look: snap, tryonImage }) } catch (e) {}\n}\n\n// Ajuste da prévia no modo look: regenera com a instrução da pessoa (acumulada), sem passar pelo stylist.\nasync function ajustarPrevia(d2, texto) {\n  const s = d2.state\n  const tr = s.tryOn\n  if (!tr) return\n  const look = findLook(d2, tr.lookId)\n  const instrucao = [tr.instrucao, texto].filter(Boolean).join('. ')\n  const rotulo = t(d2, 'tryon.ajustando') || 'ajuste'\n  const t0 = Date.now()\n  s.tryOn = { ...tr, status: 'gerando', etapa: null, roles: [], idx: 0, instrucao, msgProgresso: t(d2, 'tryon.ajustandoMsg') }\n  track(d2, 'tryon_ajuste', { lookId: tr.lookId, payload: { pedido: texto, instrucao } })\n  try {\n    const r = await d2.rush(d2.params.tryonLook || 'fps-tryon-look').invoke({ userImage: s.foto, deviceId: s.deviceId, sessionId: s.sessionId, lookId: tr.lookId, pieces: tr.pecasLook || [], aspectRatio: s.fotoRatio || '3:4', instrucao, editarDe: tr.img || undefined, ajuste: texto })\n    const out = r?.data || r\n    if (!out?.imageUrl) throw new Error(out?.erro || 'sem imagem')\n    s.tryOn = { ...s.tryOn, img: out.imageUrl, status: 'pronto', etapa: null, msgProgresso: null, recolorBase: null, baseUltima: s.foto }\n    if (look) s.looks = s.looks.map((l) => (l.lookId === look.lookId ? { ...l, tryonImage: out.imageUrl, tryonFoto: s.foto, tryonPecas: tr.pecasLook || [], tryonInstrucao: instrucao } : l))\n    if (look) sincronizarSalvo(d2, look.lookId)\n    toast(d2, t(d2, 'tryon.ajusteFeito'))\n    track(d2, 'tryon_gerado', { lookId: tr.lookId, productIds: (tr.pecasLook || []).map((p) => p.productId), payload: { ms: Date.now() - t0, modo: 'look', ajuste: true, cache: !!out.cache } })\n  } catch (e) {\n    const erroMsg = String(e?.message || e || '').slice(0, 200)\n    s.tryOn = { ...s.tryOn, status: 'erro', etapa: null, msgProgresso: null, erroEm: rotulo, erroMsg }\n    track(d2, 'tryon_erro', { lookId: tr.lookId, payload: { role: 'ajuste', msg: erroMsg, ms: Date.now() - t0 } })\n  }\n}\n\nfunction garantirLook(d2, item) {\n  const s = d2.state\n  if (!findLook(d2, item.lookId)) s.looks = [...s.looks, { ...item.look, lookId: item.lookId, tryonImage: item.tryonImage || null }]\n  else if (item.tryonImage) s.looks = s.looks.map((l) => (l.lookId === item.lookId && !l.tryonImage ? { ...l, tryonImage: item.tryonImage } : l))\n}\n\nfunction track(d2, evento, extra) {\n  const s = d2.state\n  if (!s.sessionId) return\n  d2.rush(d2.params.evento || 'fps-evento').invoke({ sessionId: s.sessionId, deviceId: s.deviceId, canal: 'embed', evento, ...extra }).catch((e) => console.log('[fps] evento', evento, e?.message || e))\n}\n\nasync function turno(d2, body, quieto) {\n  const s = d2.state\n  if (!quieto) {\n    s.busy = true\n    s.passo = 0\n    clearInterval(s._pt)\n    s._pt = setInterval(() => { s.passo = (s.passo || 0) + 1 }, 2800)\n  }\n  try {\n    const r = await d2.rush(d2.params.conversa).invoke({ canal: 'embed', sessionId: s.sessionId, deviceId: s.deviceId, ...body })\n    const out = r?.data || r\n    if (out?.sessionId) s.sessionId = out.sessionId\n    s.latTurno = out?.latenciaMs || null\n    return out?.blocks || []\n  } catch (e) {\n    if (!quieto) toast(d2, t(d2, 'erro.turno'))\n    return null\n  } finally {\n    if (!quieto) {\n      s.busy = false\n      clearInterval(s._pt)\n    }\n  }\n}\n\nfunction aplicar(d2, blocks) {\n  const s = d2.state\n  // Resposta ao pedido de ajuste da prévia: fica na prévia (alternativas abrem por cima, estilo regenera, fora de escopo só avisa).\n  if (s.tryOn?.ajustando) {\n    const note = blocks.find((b) => b.type === 'message') || null\n    const alt = blocks.find((b) => b.type === 'alternatives')\n    s.tryOn = { ...s.tryOn, ajustando: false, msg: note?.text || null, min: false }\n    if (alt) s.sheet = { type: 'swap', look: alt.lookId, role: alt.role, options: alt.options }\n    if (note?.ajuste?.tipo === 'estilo' && note.ajuste.instrucao) ajustarPrevia(d2, note.ajuste.instrucao)\n    if (!note && !alt) toast(d2, t(d2, 'erro.turno'))\n    return\n  }\n  const looks = blocks.filter((b) => b.type === 'look')\n  const note = blocks.find((b) => b.type === 'message') || null\n  const prods = blocks.find((b) => b.type === 'products')?.items || []\n  const quest = blocks.find((b) => b.type === 'questions') || null\n  const story = blocks.find((b) => b.type === 'styleQuiz' && b.modo === 'stories')\n  if (story) s.story = { b: story }\n  const extras = blocks.filter((b) => EXTRAS.includes(b.type) && b !== story)\n  const pend = s.chapters.find((c) => c.pending)\n  if (looks.length || note || prods.length || quest || extras.length) {\n    const id = pend?.id || `c${Date.now()}`\n    const pecasExtras = extras.flatMap((b) => b.products || b.pieces || [])\n    const avulsos = [...prods, ...pecasExtras].filter((p) => p?.productId).map((p) => ({ lookId: `p-${p.productId}`, title: p.name, pieces: [{ ...p, role: (p.name || '').split(' ')[0] }] }))\n    s.looks = [...s.looks, ...looks.map((l) => ({ ...l, pieces: [...l.pieces] })), ...avulsos]\n    const ch = { id, pedido: pend?.pedido || s.pedido, occasion: blocks.find((b) => b.type === 'occasion') || null, lookIds: looks.map((l) => l.lookId), note, prods, quest, extras }\n    s.chapters = pend ? s.chapters.map((c) => (c.id === id ? ch : c)) : [...s.chapters, ch]\n    s.screen = 'feed'\n    requestAnimationFrame(() => d2.$(ch.occasion ? `[data-key=\"cover-${id}\"]` : `[data-key=\"ch-${id}\"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }))\n  } else {\n    if (pend) s.chapters = s.chapters.filter((c) => !c.pending)\n    if (!blocks.length) toast(d2, t(d2, 'erro.turno'))\n  }\n  const qr = blocks.find((b) => b.type === 'quickReplies')\n  if (qr || looks.length || note || prods.length) s.replies = qr?.options || []\n  const alt = blocks.find((b) => b.type === 'alternatives')\n  if (alt) s.sheet = { type: 'swap', look: alt.lookId, role: alt.role, options: alt.options }\n  const tr = blocks.find((b) => b.type === 'tryon')\n  if (tr) provar(d2, tr)\n  if (!s.chapters.length && s.screen === 'loading') s.screen = 'start'\n}\n\n// Fluxo em stories (modo presente): cada resposta roda o turno em silêncio e troca o story; o resultado final entra no feed.\nasync function storyTurno(d2, event, payload) {\n  const s = d2.state\n  s.story = { ...(s.story || {}), busy: true }\n  const blocks = await turno(d2, { evento: { event, blockId: null, payload } }, true)\n  if (!blocks) { s.story = s.story?.b ? { ...s.story, busy: false } : null; toast(d2, t(d2, 'erro.turno')); return }\n  aplicar(d2, blocks)\n  if (s.story?.busy) s.story = null\n}\n\nfunction renderStory(d2) {\n  const st = d2.state.story\n  const b = st.b\n  const chave = b?.chave || 'presente'\n  return `\n    <section class=\"story\" role=\"dialog\">\n      <div class=\"story-bars\">${b ? Array.from({ length: b.total }, (_, i) => `<i class=\"${i < b.step - 1 ? 'on' : i === b.step - 1 ? 'now' : ''}\"></i>`).join('') : ''}</div>\n      <header class=\"story-top\"><span class=\"eyebrow light\">${esc(tt(d2, `extras.${chave}.eyebrow`, chave === 'presente' ? 'Modo presente' : 'Quiz'))}</span><button type=\"button\" class=\"btn btn-icon round\" data-act=\"story-x\" aria-label=\"${esc(t(d2, 'tryon.voltar'))}\">${ic(d2, 'X', 20)}</button></header>\n      ${b ? `<h2 class=\"display story-q\">${esc(b.pergunta)}</h2>\n      <div class=\"story-cards\">${(b.cards || []).map((c) => `<button type=\"button\" class=\"story-card ${c.image ? 'img' : ''}\" data-act=\"x-story\" data-id=\"${esc(c.id)}\" data-v=\"${esc(c.label)}\" data-step=\"${b.step}\" data-chave=\"${esc(chave)}\" ${st.busy ? 'disabled' : ''}>${c.image ? `<img src=\"${c.image}\" alt=\"\">` : ''}<span>${esc(c.label)}</span></button>`).join('')}</div>` : ''}\n      ${st.busy ? '<div class=\"story-busy\"><span class=\"spinner light\"></span></div>' : ''}\n    </section>`\n}\n\nasync function enviar(d2, texto, ev) {\n  const s = d2.state\n  s.pedido = texto\n  if (s.screen !== 'feed') s.screen = 'loading'\n  else {\n    const id = `c${Date.now()}`\n    s.chapters = [...s.chapters, { id, pedido: texto, pending: true }]\n    requestAnimationFrame(() => d2.$(`[data-key=\"ch-${id}\"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }))\n  }\n  const blocks = await turno(d2, ev ? { evento: ev } : { mensagem: texto })\n  if (blocks) aplicar(d2, blocks)\n  else {\n    s.chapters = s.chapters.filter((c) => !c.pending)\n    if (s.screen === 'loading') s.screen = 'start'\n  }\n}\n\nasync function evento(d2, event, blockId, payload, quieto) {\n  const blocks = await turno(d2, { evento: { event, blockId, payload }, userImage: d2.state.foto }, quieto)\n  if (blocks) aplicar(d2, blocks)\n  else if (d2.state.tryOn?.ajustando) { d2.state.tryOn = { ...d2.state.tryOn, ajustando: false }; toast(d2, t(d2, 'erro.turno')) }\n}\n\nasync function provar(d2, tr) {\n  const s = d2.state\n  const look = findLook(d2, tr.lookId)\n  const prev = s.tryOn || {}\n  // Cores chegam num turno à parte, depois da imagem: não mexem no estado da prévia.\n  if (tr.status === 'cores') {\n    if (prev.lookId === tr.lookId) s.tryOn = { ...prev, colorways: tr.colorways || [] }\n    return\n  }\n  const recolor = prev.recolorBase\n  s.tryOn = { lookId: tr.lookId, status: tr.status, img: tr.imageUrl || prev.img || null, etapa: null, consentimento: tr.consentimento,\n    colorways: recolor ? prev.colorways : (tr.colorways || []), baseUltima: prev.baseUltima, cortadas: tr.cortadas || [], regioes: tr.regioes, tempos: tr.tempos || prev.tempos || null,\n    modo: tr.modo || prev.modo, pecasLook: tr.pecasLook || prev.pecasLook || [], instrucao: prev.instrucao || '' }\n  if (tr.status === 'precisaFoto') s.sheet = { type: 'foto', look: tr.lookId }\n  if (tr.status === 'fotoRecusada') {\n    s.foto = null\n    keep('fps.foto', null)\n    s.sheet = { type: 'foto', look: tr.lookId }\n    toast(d2, t(d2, `foto.recusada.${tr.motivoFoto}`) || t(d2, 'foto.recusada.padrao'))\n  }\n  if (tr.status !== 'gerando') return\n  const t0 = Date.now()\n  const msTool = prev.tPedido ? t0 - prev.tPedido : null\n  const tempos = []\n  // Modo look: uma geração só com todas as peças (fps-tryon-look); o recolor segue em camadas sobre a imagem pronta.\n  if (tr.modo === 'look' && !recolor) {\n    const rotulo = t(d2, 'tryon.lookInteiro') || 'Look'\n    s.tryOn = { ...s.tryOn, etapa: rotulo, roles: [rotulo], idx: 0 }\n    try {\n      const r = await d2.rush(d2.params.tryonLook || 'fps-tryon-look').invoke({ userImage: s.foto, deviceId: s.deviceId, sessionId: s.sessionId, lookId: tr.lookId, pieces: tr.pecasLook || [], aspectRatio: s.fotoRatio || '3:4', instrucao: s.tryOn.instrucao || undefined })\n      const out = r?.data || r\n      if (!out?.imageUrl) throw new Error(out?.erro || 'sem imagem')\n      s.tryOn = { ...s.tryOn, img: out.imageUrl, status: 'pronto', etapa: null, recolorBase: null, baseUltima: s.foto }\n      if (look) s.looks = s.looks.map((l) => (l.lookId === look.lookId ? { ...l, tryonImage: out.imageUrl, tryonFoto: s.foto, tryonPecas: tr.pecasLook || [], tryonCortadas: tr.cortadas || [], tryonInstrucao: s.tryOn.instrucao || '' } : l))\n      if (look) sincronizarSalvo(d2, look.lookId)\n      track(d2, 'tryon_gerado', { lookId: tr.lookId, productIds: (tr.pecasLook || []).map((p) => p.productId), payload: { ms: Date.now() - t0, msTool, modo: 'look', cache: !!out.cache, cortadas: tr.cortadas || [] } })\n      if (look) evento(d2, 'tryon', look.id, { lookId: look.lookId, pieces: look.pieces, soCores: true }, true)\n    } catch (e) {\n      const erroMsg = String(e?.message || e || '').slice(0, 200)\n      s.tryOn = { ...s.tryOn, status: 'erro', etapa: null, erroEm: rotulo, erroMsg }\n      track(d2, 'tryon_erro', { lookId: tr.lookId, payload: { role: 'look', msg: erroMsg, ms: Date.now() - t0 } })\n    }\n    return\n  }\n  let img = recolor || s.foto\n  const roles = (tr.etapas || []).map((e) => e.role)\n  for (const [idx, et] of (tr.etapas || []).entries()) {\n    if (!recolor) s.tryOn = { ...s.tryOn, baseUltima: img }\n    s.tryOn = { ...s.tryOn, etapa: et.role, roles, idx }\n    const tE = Date.now()\n    try {\n      const r = await primeiraImagem(d2, { ...et.body, user_image: img })\n      if (!r?.url) throw new Error('sem imagem')\n      img = r.url\n      tempos.push({ role: et.role, ms: Date.now() - tE, cedo: r.cedo })\n      s.tryOn = { ...s.tryOn, img }\n    } catch (e) {\n      const erroMsg = String(e?.message || e || '').slice(0, 200)\n      s.tryOn = { ...s.tryOn, status: 'erro', etapa: null, erroEm: et.role, erroMsg }\n      track(d2, 'tryon_erro', { lookId: tr.lookId, productIds: [et.productId], payload: { role: et.role, msg: erroMsg, ms: Date.now() - tE } })\n      return\n    }\n  }\n  s.tryOn = { ...s.tryOn, status: 'pronto', etapa: null, recolorBase: null }\n  if (look) s.looks = s.looks.map((l) => (l.lookId === look.lookId ? { ...l, tryonImage: img, tryonFoto: s.foto } : l))\n  if (look) sincronizarSalvo(d2, look.lookId)\n  if (!recolor && look) evento(d2, 'tryon', look.id, { lookId: look.lookId, pieces: look.pieces, soCores: true }, true)\n  track(d2, recolor ? 'tryon_cor' : 'tryon_gerado', { lookId: tr.lookId, productIds: (tr.etapas || []).map((e) => e.productId), payload: { ms: Date.now() - t0, msTool, msTurno: s.latTurno || null, tool: tr.tempos || null, etapas: tempos, cortadas: tr.cortadas || [] } })\n}\n\nasync function compartilhar(d2, tipo, productId) {\n  const s = d2.state\n  const look = findLook(d2, s.tryOn?.lookId)\n  const peca = productId ? look?.pieces.find((p) => p.productId === productId) : null\n  const linha = (p) => `${p.name} · ${brl(d2, p.price)}${p.link ? ` · ${p.link}` : ''}`\n  const dados = tipo === 'look' && look ? { title: look.title, text: [look.title, ...look.pieces.map(linha)].join('\\n'), url: s.tryOn?.img || look.heroImage }\n    : tipo === 'peca' && peca ? { title: peca.name, text: linha(peca), url: peca.link || peca.image }\n    : { title: s.cfg.productName, url: s.tryOn?.img }\n  if (!dados.url && !dados.text) return\n  track(d2, 'compartilhado', { lookId: look?.lookId, productIds: peca ? [peca.productId] : undefined, payload: { tipo: tipo || 'previa' } })\n  s.sheet = null\n  try {\n    if (navigator.share) await navigator.share(dados)\n    else { await navigator.clipboard.writeText([dados.text, dados.url].filter(Boolean).join('\\n')); toast(d2, t(d2, 'tryon.copiado')) }\n  } catch (e) {}\n}\n\nasync function enviarFoto(d2, file) {\n  const s = d2.state\n  const lookId = (s.sheet?.type === 'foto' ? s.sheet.look : null) || s.tryOn?.lookId\n  s.fotoBusy = true\n  try {\n    const menor = await reduzir(file)\n    try { const bmp = await createImageBitmap(menor); s.fotoRatio = ratioDe(bmp.width, bmp.height); keep('fps.fotoRatio', s.fotoRatio) } catch (e) {}\n    const out = await d2.drive(d2.params.fotos).uploadFromFiles([menor])\n    if (!out?.[0]?.fileUrl) throw new Error('sem url')\n    const url = out[0].fileUrl\n    // Moderação antes de aceitar: a mesma consulta que decide as camadas, fica em cache pro try-on.\n    let mod = null\n    try { const r = await d2.rush(d2.params.fotoRegioes || 'fps-foto-regioes').invoke({ userImage: url, deviceId: s.deviceId }); mod = r?.data || r } catch (e) {}\n    if (mod?.recusada) {\n      track(d2, 'foto_recusada', { payload: { motivo: mod.motivo } })\n      toast(d2, t(d2, `foto.recusada.${mod.motivo}`) || t(d2, 'foto.recusada.padrao'))\n      return\n    }\n    s.foto = url\n    keep('fps.foto', s.foto)\n    track(d2, 'foto_enviada')\n  } catch (e) {\n    toast(d2, t(d2, 'tryon.erro'))\n    return\n  } finally {\n    s.fotoBusy = false\n  }\n  if (s.sheet?.type === 'foto') s.sheet = null\n  const look = lookId && findLook(d2, lookId)\n  if (!look) return\n  s.tryOn = { lookId: look.lookId, status: 'pedindo', img: null }\n  evento(d2, 'tryon', look.id, { lookId: look.lookId, pieces: look.pieces })\n}\n\nfunction renderStart(d2) {\n  const c = d2.state.cfg\n  return `\n    <section class=\"start\">\n      <img class=\"start-img\" src=\"${c.imagens?.abertura || ''}\" alt=\"\">\n      <div class=\"start-shade\"></div>\n      <header class=\"start-top\"><span class=\"eyebrow light\">${esc(c.productName)}</span>${d2.state.salvos?.length ? `<button type=\"button\" class=\"chip glass\" data-act=\"salvos\">${ic(d2, 'Heart', 14)}${t(d2, 'salvos.meus', { n: d2.state.salvos.length })}</button>` : ''}</header>\n      <div class=\"start-body\">\n        <h1 class=\"display start-title\">${t(d2, 'abertura.titulo')}</h1>\n        <p class=\"start-sub\">${t(d2, 'abertura.sub')}</p>\n        <div class=\"suggestions\">${(c.sugestoesAbertura || []).map((o) => `<button type=\"button\" class=\"chip glass\" data-act=\"suggest\" data-v=\"${esc(o)}\">${esc(o)}</button>`).join('')}<button type=\"button\" class=\"chip glass\" data-act=\"quiz-start\">${ic(d2, 'Sparkles', 14)}${tt(d2, 'extras.quiz.comecar', 'Descobrir meu estilo')}</button><button type=\"button\" class=\"chip glass\" data-act=\"quiz-start\" data-chave=\"presente\" data-modo=\"stories\">${ic(d2, 'Gift', 14)}${tt(d2, 'extras.presente.comecar', 'Presente')}</button></div>\n        <form class=\"start-form\">\n          <input class=\"input glass\" name=\"q\" placeholder=\"${esc(t(d2, 'abertura.placeholder'))}\" autocomplete=\"off\">\n          <button class=\"btn btn-icon round send-light\" aria-label=\"${esc(t(d2, 'abertura.placeholder'))}\">${ic(d2, 'ArrowUp', 20)}</button>\n        </form>\n      </div>\n    </section>`\n}\n\nfunction passos(d2) {\n  const lista = t(d2, 'loadingPassos').split('|').map((x) => x.trim()).filter(Boolean)\n  const n = lista.length || 1\n  const i = Math.min(d2.state.passo || 0, n - 1)\n  return { texto: lista[i] || t(d2, 'loading'), i, pct: Math.min(94, Math.round(((d2.state.passo || 0) + 1) / (n + 1) * 100)) }\n}\n\nfunction frames(k) {\n  return `<div class=\"frames\">${Array.from({ length: k }, (_, i) => `<div class=\"frame f${i}\"><div class=\"shine\"></div></div>`).join('')}</div>`\n}\n\nfunction renderLoading(d2) {\n  const p = passos(d2)\n  return `\n    <section class=\"loading2\">\n      <div class=\"bar\"><i style=\"width:${p.pct}%\"></i></div>\n      <p class=\"eyebrow\">${t(d2, 'loading')}</p>\n      <p class=\"display quote\">“${esc(d2.state.pedido)}”</p>\n      ${frames(3)}\n      <p class=\"status\" data-key=\"st-${p.i}\">${esc(p.texto)}</p>\n    </section>`\n}\n\n// Opção \"usar onde estou\": pede a posição ao browser e guarda em _geo (3 casas, ~100 m); sem permissão, a pessoa escreve a cidade.\nfunction pegarLocal(d2, ch) {\n  const s = d2.state\n  const falhou = () => toast(d2, t(d2, 'clima.semLocal'))\n  if (!navigator.geolocation) { falhou(); return }\n  toast(d2, t(d2, 'clima.pegando'))\n  navigator.geolocation.getCurrentPosition(\n    (p) => { s.resp = { ...s.resp, [ch]: { ...(s.resp[ch] || {}), _geo: { lat: +p.coords.latitude.toFixed(3), lon: +p.coords.longitude.toFixed(3) } } } },\n    falhou,\n    { timeout: 8000, maximumAge: 600000 })\n}\n\nfunction renderCover(d2, o, ch) {\n  if (!o) return ''\n  const meta = [o.where && `<li>${ic(d2, 'MapPin')}${esc(o.where)}</li>`, o.when && `<li>${ic(d2, 'CalendarDays')}${esc(o.when)}</li>`, o.weather?.summary && `<li>${ic(d2, 'Sun')}${esc(o.weather.summary)}</li>`].filter(Boolean).join('')\n  return `\n    <article class=\"page cover\" data-key=\"cover-${ch.id}\">\n      <img class=\"cover-img\" src=\"${o.coverUrl}\" alt=\"\">\n      <div class=\"cover-text\">\n        <p class=\"eyebrow light\">${o.eyebrow ? esc(o.eyebrow) : t(d2, 'capa.eyebrow')}</p>\n        <h1 class=\"display intro-title\">${esc(o.title)}</h1>\n        ${meta ? `<ul class=\"meta\">${meta}</ul>` : ''}\n        <p class=\"lede\">${esc(o.intro)}</p>\n        ${o.mala?.itens?.length ? `<div class=\"mala\"><p class=\"eyebrow light\">${t(d2, 'capa.mala')}</p><ul>${o.mala.itens.map((i) => `<li><b>${esc(i.qtd)}</b>${esc(i.papel)}${i.nota ? `<span>${esc(i.nota)}</span>` : ''}</li>`).join('')}</ul>${o.mala.dica ? `<p class=\"fine light\">${esc(o.mala.dica)}</p>` : ''}</div>` : ''}\n        <ol class=\"index\">${o.index.map((l, i) => `\n          <li><button data-act=\"goto\" data-look=\"${l.lookId}\"><span>0${i + 1}</span><span>${esc(l.title)}</span>${ic(d2, 'ArrowRight', 14)}</button></li>`).join('')}\n        </ol>\n      </div>\n    </article>`\n}\n\nfunction renderPieces(d2, look) {\n  return look.pieces.map((p) => `\n    <div class=\"piece\" data-key=\"${look.lookId}-${p.productId}\">\n      <a href=\"${p.link || '#'}\" target=\"_blank\" rel=\"noopener\"><img src=\"${p.packshot || p.image}\" alt=\"${esc(p.name)}\" loading=\"lazy\"></a>\n      <div class=\"piece-info\">\n        <span class=\"eyebrow\">${esc(p.role)}</span>\n        <span class=\"piece-name\">${esc(p.name)}</span>\n        <span class=\"price\">${price(d2, p)}</span>\n      </div>\n      <button class=\"btn btn-ghost btn-icon swap\" data-act=\"swap\" data-look=\"${look.lookId}\" data-role=\"${esc(p.role)}\" data-id=\"${p.productId}\" aria-label=\"${esc(t(d2, 'trocar.eyebrow', { papel: p.role }))}\">${ic(d2, 'Shuffle')}</button>\n    </div>`).join('')\n}\n\nfunction renderLook(d2, look, i, n) {\n  const pola = (key, cls, img, legenda, alt) => `\n          <figure class=\"pola ${cls}\" data-key=\"${key}\" data-pola=\"${key}\" ${posStyle(key)} tabindex=\"0\" role=\"button\" aria-label=\"${esc(alt)}\"><img src=\"${img}\" alt=\"\" draggable=\"false\"><figcaption>${legenda}</figcaption></figure>`\n  const polas = look.pieces.slice(0, 5).map((p, k) => pola(`pola-${look.lookId}-${p.productId}`, `p${k}`, p.packshot || p.image, esc(p.role), p.name)).join('')\n  const foto = d2.state.foto\n  return `\n    <article class=\"page look\" data-key=\"${look.lookId}\">\n      <div class=\"hero board\">\n        ${polas}\n        ${look.tryonImage ? pola(`pola-${look.lookId}-voce`, 'voce', look.tryonImage, t(d2, 'tryon.pronto'), t(d2, 'tryon.pronto')) : ''}\n        <button class=\"btn btn-icon round arrumar\" data-act=\"arrumar\" data-look=\"${look.lookId}\" aria-label=\"${esc(t(d2, 'foto.arrumar'))}\" title=\"${esc(t(d2, 'foto.arrumar'))}\">${ic(d2, 'Layers', 16)}</button>\n      </div>\n      <div class=\"look-body\">\n        <div class=\"look-head\">\n          <div class=\"look-head-row\">\n            <span class=\"eyebrow\">${t(d2, 'look.de', { i: i + 1, n })}${look.origin === 'curado' ? ` · ${t(d2, 'look.curado')}` : ''}</span>\n            <div class=\"row\">\n              <button class=\"foto-chip ${salvoDe(d2, look.lookId) ? 'on' : ''}\" data-act=\"salvar\" data-look=\"${look.lookId}\">${ic(d2, 'Heart', 14)}<span>${t(d2, salvoDe(d2, look.lookId) ? 'salvos.salvo' : 'salvos.salvar')}</span></button>\n              <button class=\"foto-chip\" data-act=\"foto\" data-look=\"${look.lookId}\">${foto ? `<img src=\"${foto}\" alt=\"\">` : ic(d2, 'Camera', 14)}<span>${t(d2, 'foto.sua')}</span></button>\n            </div>\n          </div>\n          <h2 class=\"display look-title\">${esc(look.title)}</h2>\n        </div>\n        <p class=\"caption\">${esc(look.caption)}</p>\n        <div class=\"pieces\">${renderPieces(d2, look)}</div>\n        <div class=\"look-actions\">\n          <div class=\"look-total\"><span class=\"eyebrow\">${t(d2, 'look.total')}</span><span class=\"price total\">${brl(d2, totalOf(look))}</span></div>\n          <button class=\"btn btn-outline btn-lg\" data-act=\"tryon\" data-look=\"${look.lookId}\">${ic(d2, 'ScanFace')}${t(d2, 'look.verEmMim')}</button>\n          <button class=\"btn btn-primary btn-lg\" data-act=\"cart\" data-look=\"${look.lookId}\">${ic(d2, 'ShoppingBag')}${t(d2, 'look.comprar')}</button>\n        </div>\n      </div>\n    </article>`\n}\n\nfunction renderMore(d2) {\n  const opts = d2.state.replies || []\n  if (!opts.length) return ''\n  return `\n    <article class=\"page more\" data-key=\"more\">\n      <p class=\"eyebrow\">${t(d2, 'mais.eyebrow')}</p>\n      <h2 class=\"display more-title\">${t(d2, 'mais.titulo')}</h2>\n      <div class=\"opts\">${opts.map((o) => `<button class=\"chip chip-lg\" data-act=\"reply\" data-v=\"${esc(o.value)}\" data-kind=\"${o.kind}\" data-chave=\"${esc(o.chave || '')}\" data-modo=\"${esc(o.modo || '')}\">${esc(o.label)}</button>`).join('')}</div>\n    </article>`\n}\n\nfunction renderQuote(d2, ch, k) {\n  return `\n    <section class=\"chapter-quote\" data-key=\"ch-${ch.id}\">\n      <span class=\"eyebrow\">${t(d2, 'conversa.voce')} · ${String(k + 1).padStart(2, '0')}</span>\n      <p class=\"display quote\">“${esc(ch.pedido)}”</p>\n    </section>`\n}\n\nfunction renderNote(d2, ch) {\n  const n = ch.note\n  if (!n) return ''\n  const src = (n.sources || []).map((x) => esc(x.title)).join(', ')\n  return `\n    <section class=\"chapter-text\" data-key=\"note-${ch.id}\">\n      <span class=\"eyebrow\">${t(d2, 'capa.eyebrow')}</span>\n      <p class=\"lede-text\">${esc(n.text)}</p>\n      ${src ? `<span class=\"eyebrow muted\">${t(d2, 'fonte')}: ${src}</span>` : ''}\n    </section>`\n}\n\nfunction renderQuestions(d2, ch) {\n  const q = ch.quest\n  const r = d2.state.resp[ch.id] || {}\n  const pronto = q.perguntas.every((p) => !p.obrigatoria || r[p.chave])\n  return `\n    <section class=\"questions ${ch.respondido ? 'done' : ''}\" data-key=\"q-${ch.id}\">\n      ${q.perguntas.map((p) => `\n        <div class=\"q\">\n          <p class=\"q-label\">${esc(p.pergunta)}</p>\n          ${p.livre ? `<input class=\"input q-livre\" data-k=\"${p.chave}\" placeholder=\"${esc(p.placeholder || '')}\" ${ch.respondido ? 'disabled' : ''}>`\n            : `<div class=\"opts\">${(p.opcoes || []).map((o) => `<button class=\"chip\" data-act=\"q-opt\" data-ch=\"${ch.id}\" data-k=\"${p.chave}\" data-v=\"${esc(o.label)}\" data-geo=\"${o.geo ? '1' : ''}\" aria-pressed=\"${r[p.chave] === o.label}\" ${ch.respondido ? 'disabled' : ''}>${esc(o.label)}</button>`).join('')}</div>`}\n        </div>`).join('')}\n      ${ch.respondido ? '' : `<button class=\"btn btn-primary btn-lg\" data-act=\"q-send\" data-ch=\"${ch.id}\" ${pronto ? '' : 'disabled'}>${t(d2, 'perguntas.cta')}</button>`}\n    </section>`\n}\n\nfunction renderProducts(d2, ch) {\n  return `\n    <section class=\"products\" data-key=\"prod-${ch.id}\">\n      <div class=\"pcards\">${ch.prods.map((p) => {\n        const tams = (p.sizes || []).filter((z) => z.size && z.available !== false).map((z) => esc(z.size)).join(' · ')\n        return `\n        <article class=\"pcard\" data-key=\"pc-${ch.id}-${p.productId}\">\n          <a href=\"${p.link || '#'}\" target=\"_blank\" rel=\"noopener\" class=\"pcard-img\"><img src=\"${p.packshot || p.image}\" alt=\"${esc(p.name)}\" loading=\"lazy\"></a>\n          ${p.color ? `<span class=\"eyebrow\">${esc(p.color)}</span>` : ''}\n          <span class=\"piece-name\">${esc(p.name)}</span>\n          <span class=\"price\">${price(d2, p)}</span>\n          ${tams ? `<span class=\"tams\">${tams}</span>` : ''}\n          <div class=\"pcard-actions\">\n            <button class=\"btn btn-outline btn-sm\" data-act=\"look-com\" data-v=\"${esc(p.name)}\" data-id=\"${p.productId}\">${t(d2, 'produto.montarLook')}</button>\n            <button class=\"btn btn-primary btn-sm\" data-act=\"cart\" data-look=\"p-${p.productId}\">${ic(d2, 'ShoppingBag', 14)}${t(d2, 'produto.comprar')}</button>\n          </div>\n        </article>`\n      }).join('')}</div>\n    </section>`\n}\n\n// Blocos extras (modos avançados): tamanho, quiz, comparação, cápsula, aviso de estoque e closet. A escolha da pessoa vira evento pro stylist.\nfunction renderExtra(d2, b, ch, i) {\n  const s = d2.state\n  const key = `${ch.id}-${i}`\n  const res = (s.xres || {})[key]\n  const head = (eyebrow, titulo) => `<span class=\"eyebrow\">${esc(eyebrow)}</span>${titulo ? `<h3 class=\"display xw-title\">${esc(titulo)}</h3>` : ''}`\n  const mini = (p) => `<div class=\"xmini\"><img src=\"${p.packshot || p.image || ''}\" alt=\"\" loading=\"lazy\"><span class=\"piece-name\">${esc(p.name)}</span>${p.price != null ? `<span class=\"price\">${price(d2, p)}</span>` : ''}</div>`\n  let inner = ''\n  if (b.type === 'sizeAdvisor') {\n    const r = b.result\n    inner = `${head(tt(d2, 'extras.tamanho.eyebrow', 'Seu tamanho'), b.name)}\n      ${r?.size ? `<div class=\"xs-res\"><span class=\"display xs-size\">${esc(r.size)}</span><span class=\"muted-txt\">${tt(d2, 'extras.tamanho.confianca', '{pct}% de confiança · {fonte}', { pct: Math.round((r.confidence || 0) * 100), fonte: r.source || '' })}</span>${(r.alternatives || []).filter((a) => a.size).map((a) => `<span class=\"muted-txt\">${esc(a.size)}: ${esc(a.note || '')}</span>`).join('')}</div>\n        <button class=\"btn btn-outline btn-sm\" data-act=\"x-size-usar\" data-id=\"${esc(b.productId)}\" data-v=\"${esc(r.size)}\" ${res ? 'disabled' : ''}>${ic(d2, 'Check', 14)}${res ? tt(d2, 'extras.tamanho.guardado', 'Guardado pra sacola') : tt(d2, 'extras.tamanho.usar', 'Usar na sacola')}</button>`\n      : `<form class=\"xs-form\" data-form=\"x-size\" data-id=\"${esc(b.productId)}\" data-cat=\"${esc(b.category)}\">\n          <input class=\"input\" type=\"number\" name=\"alturaCm\" placeholder=\"${esc(tt(d2, 'extras.tamanho.altura', 'altura (cm)'))}\">\n          <input class=\"input\" type=\"number\" name=\"pesoKg\" placeholder=\"${esc(tt(d2, 'extras.tamanho.peso', 'peso (kg)'))}\">\n          <select class=\"input\" name=\"caimento\"><option value=\"regular\">${tt(d2, 'extras.tamanho.regular', 'caimento regular')}</option><option value=\"justo\">${tt(d2, 'extras.tamanho.justo', 'mais justo')}</option><option value=\"folgado\">${tt(d2, 'extras.tamanho.folgado', 'mais folgado')}</option></select>\n          <button class=\"btn btn-primary btn-sm\">${tt(d2, 'extras.tamanho.cta', 'Ver meu tamanho')}</button>\n        </form>`}`\n  } else if (b.type === 'styleQuiz') {\n    inner = `${head(tt(d2, 'extras.quiz.eyebrow', 'Pergunta {i} de {n}', { i: b.step, n: b.total }), b.pergunta)}\n      <div class=\"xq-cards\">${(b.cards || []).map((c) => `<button class=\"xq-card\" data-act=\"x-quiz\" data-k=\"${key}\" data-chave=\"${esc(b.chave || '')}\" data-id=\"${esc(c.id)}\" data-v=\"${esc(c.label)}\" data-step=\"${b.step}\" aria-pressed=\"${res === c.id}\" ${res ? 'disabled' : ''}>${c.image ? `<img src=\"${c.image}\" alt=\"\" loading=\"lazy\">` : ''}<span>${esc(c.label)}</span></button>`).join('')}</div>`\n  } else if (b.type === 'compareTable') {\n    const ps = b.products || []\n    const val = (p, a) => (a.key === 'price' ? price(d2, p) : esc(Array.isArray(p[a.key]) ? p[a.key].join(', ') : p[a.key] ?? '–'))\n    inner = `${head(tt(d2, 'extras.comparar.eyebrow', 'Lado a lado'))}\n      <div class=\"xc\" style=\"--n:${ps.length}\">\n        <div class=\"xc-row xc-head\"><span></span>${ps.map((p) => `<div class=\"xc-p\"><img src=\"${p.packshot || p.image || ''}\" alt=\"\"><span class=\"piece-name\">${esc(p.name)}</span></div>`).join('')}</div>\n        ${(b.attributes || []).map((a) => `<div class=\"xc-row\"><span class=\"eyebrow\">${esc(a.label)}</span>${ps.map((p) => `<span>${val(p, a)}</span>`).join('')}</div>`).join('')}\n        <div class=\"xc-row\"><span></span>${ps.map((p) => `<button class=\"btn btn-primary btn-sm\" data-act=\"cart\" data-look=\"p-${p.productId}\">${ic(d2, 'ShoppingBag', 14)}${t(d2, 'produto.comprar')}</button>`).join('')}</div>\n      </div>`\n  } else if (b.type === 'capsuleGrid') {\n    const ps = b.pieces || []\n    const tot = ps.reduce((a, p) => a + (p.price || 0), 0)\n    inner = `${head(tt(d2, 'extras.capsula.eyebrow', 'Cápsula até {orc}', { orc: brl(d2, b.budget) }), tt(d2, 'extras.capsula.titulo', '{n} peças, {c} combinações', { n: ps.length, c: (b.combinations || []).length }))}\n      <div class=\"xmini-grid\">${ps.map(mini).join('')}</div>\n      <div class=\"row xw-foot\"><span class=\"price total\">${brl(d2, tot)}</span><button class=\"btn btn-primary btn-sm\" data-act=\"x-capsula\" data-k=\"${key}\" data-ch=\"${ch.id}\" data-i=\"${i}\">${ic(d2, 'Shirt', 14)}${tt(d2, 'extras.capsula.cta', 'Ver os looks da cápsula')}</button></div>`\n  } else if (b.type === 'stockAlert') {\n    const pedido = res === 'pedido' || b.status === 'avisado'\n    inner = `${head(tt(d2, 'extras.estoque.eyebrow', 'Aviso de estoque'))}\n      <div class=\"xs-alert\">${b.image ? `<img src=\"${b.image}\" alt=\"\">` : ''}<div><span class=\"piece-name\">${esc(b.name || '')}</span><span class=\"muted-txt\">${tt(d2, 'extras.estoque.tamanho', 'Tamanho {t}', { t: b.size })}</span></div></div>\n      ${pedido ? `<p class=\"caption\">${ic(d2, 'BellRing', 14)} ${tt(d2, 'extras.estoque.pedido', 'Aviso pedido. Quando o tamanho voltar, você recebe pelo {canal}.', { canal: b.channel })}</p>`\n        : `<button class=\"btn btn-primary btn-sm\" data-act=\"x-estoque\" data-k=\"${key}\" data-id=\"${esc(b.productId)}\" data-v=\"${esc(b.size)}\" data-canal=\"${esc(b.channel)}\">${ic(d2, 'Bell', 14)}${tt(d2, 'extras.estoque.cta', 'Me avise quando voltar')}</button>`}`\n  } else if (b.type === 'closet') {\n    const its = b.items || []\n    inner = `${head(tt(d2, 'extras.closet.eyebrow', 'Seu closet'), tt(d2, 'extras.closet.titulo', '{n} peça(s) que você já tem', { n: its.length }))}\n      <div class=\"xmini-grid\">${its.map((p) => `<div class=\"xmini\"><img src=\"${p.image || ''}\" alt=\"\" loading=\"lazy\"><span class=\"piece-name\">${esc(p.name)}</span>${p.purchasedAt ? `<span class=\"muted-txt\">${esc(p.purchasedAt)}</span>` : ''}</div>`).join('')}</div>\n      <button class=\"btn btn-outline btn-sm\" data-act=\"x-closet\" data-ch=\"${ch.id}\" data-i=\"${i}\">${ic(d2, 'Sparkles', 14)}${tt(d2, 'extras.closet.cta', 'Montar look com o que eu já tenho')}</button>`\n  }\n  return `<section class=\"xw\" data-key=\"x-${key}\">${inner}</section>`\n}\n\nfunction renderChapter(d2, ch, k, last) {\n  if (ch.pending) return `${renderQuote(d2, ch, k)}\n    <section class=\"chapter-text pending\" data-key=\"pend-${ch.id}\">\n      ${frames(3)}\n      <p class=\"status\" data-key=\"stp-${passos(d2).i}\">${esc(passos(d2).texto)}</p>\n    </section>`\n  const looks = ch.lookIds.map((id) => findLook(d2, id)).filter(Boolean)\n  return `${renderQuote(d2, ch, k)}\n    ${renderCover(d2, ch.occasion, ch)}\n    ${!ch.occasion || !looks.length ? renderNote(d2, ch) : ''}\n    ${ch.prods?.length ? renderProducts(d2, ch) : ''}\n    ${ch.quest ? renderQuestions(d2, ch) : ''}\n    ${(ch.extras || []).map((b, i) => renderExtra(d2, b, ch, i)).join('')}\n    ${looks.map((l, i) => renderLook(d2, l, i, looks.length)).join('')}\n    ${last ? renderMore(d2) : ''}`\n}\n\nfunction renderTryOn(d2) {\n  const tr = d2.state.tryOn\n  const look = findLook(d2, tr.lookId)\n  const msg = { precisaFoto: t(d2, 'tryon.foto'), limite: t(d2, 'tryon.limite'), semPecaElegivel: t(d2, 'tryon.semPeca'), fotoRecusada: t(d2, 'foto.recusada.padrao') }[tr.status]\n  const working = tr.status === 'gerando' || tr.status === 'enviando' || tr.status === 'pedindo'\n  const pronto = tr.status === 'pronto' && !!look\n  const salvo = look ? salvoDe(d2, look.lookId) : null\n  const min = pronto && tr.min !== false\n  return `\n    <section class=\"tryon ${min ? 'min' : ''}\" role=\"dialog\" aria-label=\"${esc(t(d2, 'tryon.titulo'))}\" data-regioes=\"${esc(JSON.stringify(tr.regioes || null))}\" data-tempos=\"${esc(JSON.stringify({ tool: tr.tempos || null, turno: d2.state.latTurno || null }))}\">\n      <header class=\"tryon-top\">\n        <button class=\"btn btn-icon round\" data-act=\"close\" aria-label=\"${esc(t(d2, 'tryon.voltar'))}\">${ic(d2, 'X', 20)}</button>\n        <div class=\"row\">\n          <button class=\"foto-chip light\" data-act=\"foto\" data-look=\"${tr.lookId}\">${d2.state.foto ? `<img src=\"${d2.state.foto}\" alt=\"\">` : ic(d2, 'Camera', 14)}<span>${t(d2, 'foto.sua')}</span></button>\n          <span class=\"badge\">${ic(d2, 'Sparkles', 12)}${tr.status === 'pronto' ? t(d2, 'tryon.pronto') : t(d2, 'tryon.titulo')}</span>\n        </div>\n      </header>\n      <div class=\"tryon-stage\">\n        <img class=\"tryon-img ${working ? 'dim' : ''}\" src=\"${tr.img || d2.state.foto || look?.heroImage || ''}\" alt=\"\">\n        ${working ? `<div class=\"scan\"></div><div class=\"tryon-progress\">\n          ${tr.roles?.length ? `<div class=\"trilha\">${tr.roles.map((r, i) => `<span class=\"${i < tr.idx ? 'ok' : i === tr.idx ? 'now' : ''}\">${i < tr.idx ? '✓ ' : ''}${esc(r)}</span>`).join('<i>·</i>')}</div>` : ''}\n          <span>${tr.msgProgresso || (tr.etapa ? t(d2, 'tryon.gerando', { papel: tr.etapa.toLowerCase() }) : t(d2, 'loading'))}</span>\n          ${tr.etapa ? `<span class=\"etapa-n\">${t(d2, 'tryon.etapa', { i: tr.idx + 1, n: tr.roles.length })}</span><div class=\"ebar\" data-key=\"eb-${tr.idx}\"><i></i></div>` : '<span class=\"spinner\"></span>'}\n        </div>` : ''}\n        ${pronto ? `<div class=\"rail\">\n          <button class=\"rail-btn\" data-act=\"voto\" data-v=\"up\" aria-pressed=\"${tr.voto === 'up'}\" aria-label=\"${esc(t(d2, 'tryon.gostei'))}\">${ic(d2, 'ThumbsUp', 22)}</button>\n          <button class=\"rail-btn\" data-act=\"voto\" data-v=\"down\" aria-pressed=\"${tr.voto === 'down'}\" aria-label=\"${esc(t(d2, 'tryon.naoGostei'))}\">${ic(d2, 'ThumbsDown', 22)}</button>\n          <button class=\"rail-btn ${salvo ? 'on' : ''}\" data-act=\"salvar\" data-look=\"${look.lookId}\" aria-label=\"${esc(t(d2, salvo ? 'salvos.salvo' : 'salvos.salvar'))}\">${ic(d2, 'Heart', 22)}</button>\n          <button class=\"rail-btn\" data-act=\"share-menu\" aria-label=\"${esc(t(d2, 'tryon.compartilhar'))}\">${ic(d2, 'Share2', 22)}</button>\n        </div>` : ''}\n      </div>\n      <div class=\"tryon-bar\">\n        <button type=\"button\" class=\"bar-toggle\" data-act=\"painel\">${ic(d2, 'ChevronUp', 18)}<span>${t(d2, 'tryon.ajustar')}</span></button>\n        ${look ? `<button class=\"btn btn-primary btn-lg grow\" data-act=\"cart\" data-look=\"${look.lookId}\">${ic(d2, 'ShoppingBag')}${t(d2, 'look.comprar')}</button>` : ''}\n      </div>\n      <div class=\"tryon-panel\">\n        <div class=\"panel-head\"><p class=\"eyebrow\">${esc(look?.title)}</p>${pronto ? `<button type=\"button\" class=\"btn btn-icon round btn-sm\" data-act=\"painel\" aria-label=\"${esc(t(d2, 'tryon.recolher'))}\">${ic(d2, 'ChevronDown', 18)}</button>` : ''}</div>\n        ${msg ? `<p class=\"caption\">${msg}</p>` : ''}\n        ${tr.ajustando ? `<p class=\"caption ajustando\"><span class=\"spinner\"></span>${t(d2, 'tryon.ajustando')}</p>` : tr.msg ? `<p class=\"caption\">${esc(tr.msg)}</p>` : ''}\n        ${tr.status === 'erro' ? `<div class=\"erro-box\" role=\"alert\">${ic(d2, 'TriangleAlert', 18)}<div><b>${t(d2, 'tryon.erro')}</b>${tr.erroEm ? `<span>${t(d2, 'tryon.erroEtapa', { papel: tr.erroEm.toLowerCase() })}</span>` : ''}${tr.erroMsg ? `<code>${esc(tr.erroMsg)}</code>` : ''}</div></div>\n          <button class=\"btn btn-primary btn-lg btn-block\" data-act=\"tryon\" data-look=\"${tr.lookId}\">${ic(d2, 'RefreshCw')}${t(d2, 'tryon.tentarDeNovo')}</button>` : ''}\n        ${tr.status === 'precisaFoto' || tr.status === 'fotoRecusada' ? `<button class=\"btn btn-primary btn-lg btn-block\" data-act=\"foto\" data-look=\"${tr.lookId}\">${ic(d2, 'Camera')}${t(d2, 'tryon.fotoCta')}</button>` : ''}\n        ${pronto && tr.colorways?.length ? (() => {\n          const role = tr.colorways[0].role\n          const cur = look?.pieces.find((p) => p.role === role)\n          return `<p class=\"tryon-label\">${t(d2, 'tryon.cor', { papel: (role || '').toLowerCase() })}</p>\n          <div class=\"swatches\">${tr.colorways.map((c) => `<button class=\"swatch\" data-act=\"color\" data-id=\"${c.productId}\" aria-pressed=\"${c.productId === cur?.productId}\"><img src=\"${c.packshot || c.image}\" alt=\"\"><span>${esc((c.color || '').toLowerCase())}</span></button>`).join('')}</div>`\n        })() : ''}\n        ${tr.cortadas?.length ? `<p class=\"fine\">${t(d2, 'tryon.cortadas', { pecas: tr.cortadas.map((x) => x.toLowerCase()).join(', ') })}</p>` : ''}\n        ${pronto ? `\n          <p class=\"tryon-label\">${t(d2, 'tryon.trocarPeca')}</p>\n          <div class=\"tr-pecas\">${look.pieces.map((p) => `<button class=\"tr-peca\" data-act=\"swap\" data-look=\"${look.lookId}\" data-role=\"${esc(p.role)}\" data-id=\"${p.productId}\"><img src=\"${p.packshot || p.image}\" alt=\"\"><span>${esc(p.role)}</span></button>`).join('')}</div>\n          <form class=\"ajuste\"><input class=\"input\" name=\"q\" placeholder=\"${esc(t(d2, 'tryon.ajustePlaceholder'))}\" autocomplete=\"off\" ${tr.ajustando ? 'disabled' : ''}><button class=\"btn btn-outline btn-icon round\" aria-label=\"${esc(t(d2, 'tryon.ajusteCta'))}\" ${tr.ajustando ? 'disabled' : ''}>${ic(d2, 'ArrowUp', 16)}</button></form>` : ''}\n        ${look ? `<button class=\"btn btn-primary btn-lg btn-block\" data-act=\"cart\" data-look=\"${look.lookId}\">${ic(d2, 'ShoppingBag')}${t(d2, 'look.comprar')}</button>` : ''}\n      </div>\n    </section>`\n}\n\nfunction renderSheet(d2) {\n  const sh = d2.state.sheet\n  const look = findLook(d2, sh.look)\n  let inner\n  if (sh.type === 'swap') {\n    const cur = look?.pieces.find((p) => p.role === sh.role)\n    inner = `\n      <p class=\"eyebrow\">${t(d2, 'trocar.eyebrow', { papel: (sh.role || '').toLowerCase() })}</p>\n      <h3 class=\"display sheet-title\">${t(d2, 'trocar.titulo')}</h3>\n      ${sh.options ? `<div class=\"alts\">${sh.options.map((a) => `\n        <button class=\"alt\" data-act=\"pick-alt\" data-id=\"${a.productId}\" aria-pressed=\"${a.productId === cur?.productId}\">\n          <img src=\"${a.packshot || a.image}\" alt=\"\"><span class=\"eyebrow\">${esc(a.color || '')}</span><span class=\"piece-name\">${esc(a.name)}</span><span class=\"price\">${price(d2, a)}</span>\n        </button>`).join('')}</div>` : `<div class=\"alts-loading\">${frames(3)}<p class=\"status\">${t(d2, 'trocar.buscando')}</p></div>`}`\n  } else if (sh.type === 'salvos') {\n    const itens = d2.state.salvos || []\n    inner = `\n      <p class=\"eyebrow\">${t(d2, 'salvos.eyebrow')}</p>\n      <h3 class=\"display sheet-title\">${t(d2, 'salvos.titulo')}</h3>\n      ${itens.length ? `<div class=\"salvos\">${itens.map((it) => `\n        <article class=\"salvo\" data-key=\"sv-${it.lookId}\">\n          <img src=\"${it.tryonImage || it.look?.heroImage || it.look?.pieces?.[0]?.image || ''}\" alt=\"\">\n          <div class=\"salvo-info\">\n            <span class=\"piece-name\">${esc(it.look?.title)}</span>\n            <span class=\"muted-txt\">${(it.look?.pieces || []).map((p) => esc(p.role)).join(' · ')}</span>\n            <span class=\"price\">${brl(d2, it.look?.total)}</span>\n            <div class=\"salvo-acoes\">\n              <button class=\"btn btn-outline btn-sm\" data-act=\"salvo-ver\" data-look=\"${it.lookId}\">${t(d2, 'salvos.ver')}</button>\n              <button class=\"btn btn-primary btn-sm\" data-act=\"salvo-comprar\" data-look=\"${it.lookId}\">${ic(d2, 'ShoppingBag', 14)}${t(d2, 'look.comprar')}</button>\n              <button class=\"btn btn-ghost btn-icon btn-sm\" data-act=\"salvo-remover\" data-look=\"${it.lookId}\" aria-label=\"${esc(t(d2, 'salvos.remover'))}\">${ic(d2, 'Trash2', 14)}</button>\n            </div>\n          </div>\n        </article>`).join('')}</div>` : `<p class=\"caption\">${t(d2, 'salvos.vazio')}</p>`}`\n  } else if (sh.type === 'share') {\n    inner = `\n      <p class=\"eyebrow\">${t(d2, 'tryon.compartilhar')}</p>\n      <h3 class=\"display sheet-title\">${t(d2, 'tryon.compartilharTitulo')}</h3>\n      <div class=\"share-opts\">\n        <button class=\"btn btn-outline btn-lg btn-block\" data-act=\"share-do\" data-v=\"previa\">${ic(d2, 'Image')}${t(d2, 'tryon.compartilharPrevia')}</button>\n        <button class=\"btn btn-outline btn-lg btn-block\" data-act=\"share-do\" data-v=\"look\">${ic(d2, 'Shirt')}${t(d2, 'tryon.compartilharLook')}</button>\n        ${(look?.pieces || []).map((p) => `<button class=\"btn btn-ghost btn-block share-peca\" data-act=\"share-do\" data-v=\"peca\" data-id=\"${p.productId}\"><img src=\"${p.packshot || p.image}\" alt=\"\"><span>${esc(p.name)}</span>${ic(d2, 'Link', 14)}</button>`).join('')}\n      </div>`\n  } else if (sh.type === 'foto') {\n    const foto = d2.state.foto\n    const busy = d2.state.fotoBusy\n    const dicas = t(d2, 'foto.dicas').split('|').map((x) => x.trim()).filter(Boolean)\n    inner = `\n      <p class=\"eyebrow\">${t(d2, 'tryon.titulo')}</p>\n      <h3 class=\"display sheet-title\">${t(d2, 'foto.titulo')}</h3>\n      <p class=\"caption\">${t(d2, 'foto.sub')}</p>\n      ${foto ? `<div class=\"foto-atual\"><img src=\"${foto}\" alt=\"\"><div><span class=\"eyebrow\">${t(d2, 'foto.atual')}</span><button class=\"btn btn-ghost btn-sm\" data-act=\"foto-remover\">${ic(d2, 'Trash2', 14)}${t(d2, 'foto.remover')}</button></div></div>`\n        : `<ul class=\"dicas\">${dicas.map((x) => `<li>${ic(d2, 'Check', 14)}${esc(x)}</li>`).join('')}</ul>`}\n      <p class=\"fine\">${esc(d2.state.cfg?.consentimentoTryon || '')}</p>\n      <label class=\"btn btn-primary btn-lg btn-block\">${busy ? `<span class=\"spinner light\"></span>${t(d2, 'foto.enviando')}` : `${ic(d2, 'Camera')}${foto ? t(d2, 'foto.trocar') : t(d2, 'foto.cta')}`}<input type=\"file\" accept=\"image/*\" class=\"file\" hidden ${busy ? 'disabled' : ''}></label>`\n  } else {\n    const chosen = d2.state.sizes[look.lookId] || {}\n    const ready = look.pieces.every((p) => chosen[p.productId])\n    inner = `\n      <p class=\"eyebrow\">${t(d2, 'sacola.eyebrow')}</p>\n      <h3 class=\"display sheet-title\">${esc(look.title)}</h3>\n      <div class=\"cart\">${look.pieces.map((p) => `\n        <div class=\"cart-row\" data-key=\"c-${p.productId}\">\n          <img src=\"${p.packshot || p.image}\" alt=\"\">\n          <div class=\"grow\">\n            <span class=\"piece-name\">${esc(p.name)}</span>\n            <span class=\"price\">${price(d2, p)}</span>\n            <div class=\"sizes\">${(p.sizes || []).filter((z) => z.size).map((z) => `<button class=\"size\" data-act=\"size\" data-id=\"${p.productId}\" data-v=\"${esc(z.size)}\" aria-pressed=\"${chosen[p.productId] === z.size}\" ${z.available === false ? `disabled title=\"${esc(t(d2, 'sacola.esgotado'))}\"` : ''}>${esc(z.size)}</button>`).join('')}</div>\n          </div>\n        </div>`).join('')}\n      </div>\n      <div class=\"sheet-foot\">\n        <div class=\"cart-total\"><span>${t(d2, 'sacola.total')}</span><span class=\"price total\">${brl(d2, totalOf(look))}</span></div>\n        <button class=\"btn btn-primary btn-lg btn-block\" data-act=\"checkout\" data-look=\"${look.lookId}\" ${ready ? '' : 'disabled'}>${ready ? t(d2, 'sacola.cta') : t(d2, 'sacola.escolha')}</button>\n      </div>`\n  }\n  return `\n    <div class=\"scrim\" data-act=\"close-sheet\"></div>\n    <section class=\"sheet\" role=\"dialog\"><span class=\"grabber\"></span><button type=\"button\" class=\"btn btn-icon round sheet-x\" data-act=\"close-sheet\" aria-label=\"${esc(t(d2, 'tryon.voltar'))}\">${ic(d2, 'X', 18)}</button>${inner}</section>`\n}\n\nfunction renderFeed(d2) {\n  const s = d2.state\n  return `\n    <section class=\"feed\">\n      <div class=\"pages\">\n        ${s.chapters.map((ch, k) => renderChapter(d2, ch, k, k === s.chapters.length - 1)).join('')}\n      </div>\n      ${renderAsk(d2)}\n      ${s.tryOn ? renderTryOn(d2) : ''}\n      ${s.sheet ? renderSheet(d2) : ''}\n    </section>`\n}\n\nfunction renderAsk(d2) {\n  return `\n    <div class=\"ask-wrap\">\n      <form class=\"ask\">\n        <button type=\"button\" class=\"btn btn-icon round salvos-btn ${d2.state.salvos?.length ? 'on' : ''}\" data-act=\"salvos\" aria-label=\"${esc(t(d2, 'salvos.titulo'))}\">${ic(d2, 'Heart', 18)}${d2.state.salvos?.length ? `<i>${d2.state.salvos.length}</i>` : ''}</button>\n        <input class=\"input\" name=\"q\" placeholder=\"${esc(t(d2, 'ask.placeholder'))}\" autocomplete=\"off\" ${d2.state.busy ? 'disabled' : ''}>\n        <button class=\"btn btn-primary btn-icon round\" aria-label=\"${esc(t(d2, 'ask.placeholder'))}\" ${d2.state.busy ? 'disabled' : ''}>${d2.state.busy ? '<span class=\"spinner light\"></span>' : ic(d2, 'ArrowUp', 18)}</button>\n      </form>\n    </div>`\n}\n\nfunction renderNoteScreen(d2) {\n  return `<section class=\"note-screen\">${renderAsk(d2)}</section>`\n}\n\nfunction toast(d2, msg) {\n  d2.state.toast = msg\n  clearTimeout(d2.state._t)\n  d2.state._t = setTimeout(() => { d2.state.toast = null }, 2600)\n}\n\nfunction go(d2, act, el) {\n  const s = d2.state\n  const v = el.dataset.v\n  if (act === 'suggest') {\n    const input = d2.$('.start-form input')\n    input.value = v\n    input.focus()\n  } else if (act === 'goto') d2.$(`.page.look[data-key=\"${el.dataset.look}\"]`)?.scrollIntoView({ behavior: 'smooth' })\n  else if (act === 'q-opt') {\n    const ch = el.dataset.ch\n    s.resp = { ...s.resp, [ch]: { ...(s.resp[ch] || {}), [el.dataset.k]: v } }\n    if (el.dataset.geo) pegarLocal(d2, ch)\n  } else if (act === 'q-send') {\n    const id = el.dataset.ch\n    const ch = s.chapters.find((c) => c.id === id)\n    const respostas = { ...(s.resp[id] || {}) }\n    el.closest('.questions').querySelectorAll('.q-livre').forEach((i) => { if (i.value.trim()) respostas[i.dataset.k] = i.value.trim() })\n    s.chapters = s.chapters.map((c) => (c.id === id ? { ...c, respondido: true } : c))\n    const resumo = ch.quest.perguntas.map((p) => respostas[p.chave]).filter(Boolean).join(' · ')\n    enviar(d2, resumo, { event: 'answers', blockId: null, payload: { ocasiao: ch.quest.ocasiao, respostas } })\n  }\n  else if (act === 'look-com') enviar(d2, t(d2, 'produto.montarLookMsg', { nome: v }), { event: 'anchor', blockId: null, payload: { productId: el.dataset.id, name: v } })\n  else if (act === 'reply') {\n    if (el.dataset.kind === 'preencher') { const i = d2.$('.ask input'); i.value = v; i.focus() }\n    else if (el.dataset.kind === 'evento') {\n      if (v === 'quiz_start') s.quizResp = []\n      const payload = el.dataset.chave ? { chave: el.dataset.chave } : {}\n      if (el.dataset.modo === 'stories') { s.pedido = el.textContent.trim(); storyTurno(d2, v, payload) }\n      else enviar(d2, el.textContent.trim(), { event: v, blockId: null, payload })\n    }\n    else enviar(d2, v)\n  } else if (act === 'quiz-start') {\n    s.quizResp = []\n    const chave = el.dataset.chave\n    if (el.dataset.modo === 'stories') { s.pedido = el.textContent.trim(); storyTurno(d2, 'quiz_start', { chave }) }\n    else enviar(d2, tt(d2, 'extras.quiz.comecar', 'Descobrir meu estilo'), { event: 'quiz_start', blockId: null, payload: chave ? { chave } : {} })\n  } else if (act === 'x-story') {\n    const step = Number(el.dataset.step)\n    s.quizResp = [...(s.quizResp || []).filter((r) => r.step < step), { step, cardId: el.dataset.id }]\n    storyTurno(d2, 'quiz', { chave: el.dataset.chave, step, cardId: el.dataset.id, label: v, respostas: s.quizResp })\n  } else if (act === 'story-x') {\n    s.story = null\n  } else if (act === 'tryon') {\n    const look = findLook(d2, el.dataset.look)\n    if (!s.foto) { s.sheet = { type: 'foto', look: look.lookId }; return }\n    // Já tem prévia desta foto: abre na hora e só busca as cores em segundo plano.\n    if (look.tryonImage && (!look.tryonFoto || look.tryonFoto === s.foto)) {\n      s.tryOn = { lookId: look.lookId, status: 'pronto', img: look.tryonImage, modo: 'look', pecasLook: look.tryonPecas || look.pieces, cortadas: look.tryonCortadas || [], instrucao: look.tryonInstrucao || '', baseUltima: s.foto, colorways: [], tempos: null }\n      evento(d2, 'tryon', look.id, { lookId: look.lookId, pieces: look.pieces, soCores: true }, true)\n      return\n    }\n    s.tryOn = { lookId: look.lookId, status: 'pedindo', img: null, tPedido: Date.now() }\n    evento(d2, 'tryon', look.id, { lookId: look.lookId, pieces: look.pieces })\n  } else if (act === 'salvar') salvarLook(d2, el.dataset.look || s.tryOn?.lookId)\n  else if (act === 'salvos') s.sheet = { type: 'salvos' }\n  else if (act === 'salvo-remover') salvarLook(d2, el.dataset.look)\n  else if (act === 'salvo-ver' || act === 'salvo-comprar') {\n    const item = salvoDe(d2, el.dataset.look)\n    if (!item) return\n    garantirLook(d2, item)\n    s.tryOn = null\n    if (act === 'salvo-comprar') { s.sheet = { type: 'cart', look: item.lookId }; return }\n    s.sheet = null\n    if (!s.chapters.some((c) => (c.lookIds || []).includes(item.lookId))) s.chapters = [...s.chapters, { id: `c${Date.now()}`, pedido: t(d2, 'salvos.pedido'), occasion: null, lookIds: [item.lookId], note: null, prods: [], quest: null }]\n    s.screen = 'feed'\n    requestAnimationFrame(() => d2.$(`.page.look[data-key=\"${item.lookId}\"]`)?.scrollIntoView({ behavior: 'smooth' }))\n  } else if (act === 'foto') s.sheet = { type: 'foto', look: s.tryOn ? null : el.dataset.look || null }\n  else if (act === 'foto-remover') {\n    s.foto = null\n    keep('fps.foto', null)\n    track(d2, 'foto_removida')\n    toast(d2, t(d2, 'foto.removida'))\n  } else if (act === 'arrumar') {\n    for (const k of [...POS.keys()]) if (k.startsWith(`pola-${el.dataset.look}-`)) POS.delete(k)\n    s.tick = Date.now()\n  } else if (act === 'close') s.tryOn = null\n  else if (act === 'painel') s.tryOn = { ...s.tryOn, min: s.tryOn?.min === false }\n  else if (act === 'share-menu') s.sheet = { type: 'share', look: s.tryOn?.lookId }\n  else if (act === 'share-do') compartilhar(d2, v, el.dataset.id)\n  else if (act === 'voto') {\n    const tr = s.tryOn\n    s.tryOn = { ...tr, voto: v }\n    track(d2, 'tryon_feedback', { lookId: tr.lookId, payload: { voto: v, imagem: tr.img } })\n    toast(d2, t(d2, 'tryon.feedbackObrigado'))\n  }\n  else if (act === 'color') {\n    const tr = s.tryOn\n    const cw = tr.colorways.find((c) => c.productId === el.dataset.id)\n    const role = cw.role\n    s.looks = s.looks.map((l) => (l.lookId === tr.lookId ? { ...l, pieces: l.pieces.map((p) => (p.role === role ? { ...cw } : p)) } : l))\n    s.tryOn = { ...tr, status: 'pedindo', recolorBase: tr.baseUltima, tPedido: Date.now() }\n    evento(d2, 'tryon', findLook(d2, tr.lookId)?.id, { lookId: tr.lookId, pieces: [cw], semCores: true })\n  }\n  else if (act === 'swap') {\n    s.sheet = { type: 'swap', look: el.dataset.look, role: el.dataset.role, options: null }\n    evento(d2, 'swap', findLook(d2, el.dataset.look)?.id, { lookId: el.dataset.look, role: el.dataset.role, productId: el.dataset.id })\n  } else if (act === 'pick-alt') {\n    const { look, role, options } = s.sheet\n    const alt = options.find((a) => a.productId === el.dataset.id)\n    const antes = findLook(d2, look)?.pieces.find((p) => p.role === role)?.productId\n    s.looks = s.looks.map((l) => (l.lookId === look ? { ...l, tryonImage: null, pieces: l.pieces.map((p) => (p.role === role ? { ...alt, role } : p)) } : l))\n    s.sheet = null\n    toast(d2, t(d2, 'trocar.toast'))\n    track(d2, 'peca_trocada', { lookId: look, productIds: [alt.productId], payload: { role, de: antes } })\n    sincronizarSalvo(d2, look)\n    // Troca feita com a prévia aberta: veste a peça nova na foto na hora.\n    if (s.tryOn?.lookId === look && s.tryOn.status === 'pronto') {\n      const novo = findLook(d2, look)\n      s.tryOn = { ...s.tryOn, status: 'pedindo', msg: null, recolorBase: null, colorways: [], tPedido: Date.now() }\n      evento(d2, 'tryon', novo.id, { lookId: look, pieces: novo.pieces })\n    }\n  } else if (act === 'x-quiz') {\n    const step = Number(el.dataset.step)\n    s.xres = { ...(s.xres || {}), [el.dataset.k]: el.dataset.id }\n    s.quizResp = [...(s.quizResp || []).filter((r) => r.step < step), { step, cardId: el.dataset.id }]\n    enviar(d2, v, { event: 'quiz', blockId: null, payload: { chave: el.dataset.chave || undefined, step, cardId: el.dataset.id, label: v, respostas: s.quizResp } })\n  } else if (act === 'x-size-usar') {\n    s.tamPref = { ...(s.tamPref || {}), [el.dataset.id]: v }\n    s.xres = { ...(s.xres || {}), [el.closest('.xw')?.dataset.key?.slice(2)]: 'usado' }\n    toast(d2, tt(d2, 'extras.tamanho.guardado', 'Guardado pra sacola'))\n  } else if (act === 'x-estoque') {\n    s.xres = { ...(s.xres || {}), [el.dataset.k]: 'pedido' }\n    track(d2, 'estoque_aviso', { productIds: [el.dataset.id], payload: { size: v, canal: el.dataset.canal } })\n    evento(d2, 'stock_alert', null, { productId: el.dataset.id, size: v, channel: el.dataset.canal }, true)\n  } else if (act === 'x-capsula' || act === 'x-closet') {\n    const ch = s.chapters.find((c) => c.id === el.dataset.ch)\n    const b = ch?.extras?.[Number(el.dataset.i)]\n    if (!b) return\n    const ids = (b.pieces || b.items || []).map((p) => p.productId)\n    if (act === 'x-capsula') enviar(d2, tt(d2, 'extras.capsula.msg', 'Monte os looks com a cápsula'), { event: 'capsule_looks', blockId: null, payload: { productIds: ids, pieces: b.pieces || [], combinations: b.combinations || [] } })\n    else enviar(d2, tt(d2, 'extras.closet.msg', 'Monte um look com o que eu já tenho'), { event: 'closet_look', blockId: null, payload: { productIds: ids } })\n  } else if (act === 'cart') {\n    d2.setState({ tryOn: null, sheet: { type: 'cart', look: el.dataset.look } })\n    const look = findLook(d2, el.dataset.look)\n    // Tamanho guardado pelo bloco de tamanho entra pré-selecionado na sacola.\n    const pref = s.tamPref || {}\n    if (look) s.sizes = { ...s.sizes, [look.lookId]: { ...Object.fromEntries(look.pieces.filter((p) => pref[p.productId]).map((p) => [p.productId, pref[p.productId]])), ...(s.sizes[look.lookId] || {}) } }\n    if (look) track(d2, 'sacola_aberta', { lookId: look.lookId, valor: totalOf(look) })\n  }\n  else if (act === 'size') s.sizes = { ...s.sizes, [s.sheet.look]: { ...(s.sizes[s.sheet.look] || {}), [el.dataset.id]: v } }\n  else if (act === 'checkout') {\n    const look = findLook(d2, el.dataset.look)\n    const items = look.pieces.map((p) => {\n      const size = s.sizes[look.lookId][p.productId]\n      const z = (p.sizes || []).find((x) => x.size === size) || {}\n      return { productId: p.productId, hostProductId: p.hostProductId || p.productId, itemId: z.itemId, sku: z.sku, size, link: p.link }\n    })\n    // Contrato vivo do host (PostEventsWebView da v1): um TRYON_ADD_TO_CART por peça.\n    for (const it of items) {\n      try {\n        Promise.resolve(d2.exec({ component: 'aramis@kM4ZWouoVflmF6qUPpDrH', params: { type: 'TRYON_ADD_TO_CART', payload: { product_id: String(it.hostProductId), sku_id: String(it.itemId), quantity: '1' } } }))\n          .then((r) => (typeof r === 'function' ? r() : r))\n          .catch((e) => console.log('[fps] TRYON_ADD_TO_CART falhou', e?.message || e))\n      } catch (e) { console.log('[fps] TRYON_ADD_TO_CART erro', e?.message || e) }\n    }\n    window.parent?.postMessage({ type: 'fps:add-to-cart', items }, '*')\n    track(d2, 'add_to_cart', { lookId: look.lookId, productIds: items.map((it) => it.productId), valor: totalOf(look), payload: { items } })\n    s.sheet = null\n    toast(d2, t(d2, 'sacola.toast'))\n  } else if (act === 'close-sheet') s.sheet = null\n  else if (act === 'close-note') s.note = null\n}\n\nexport default {\n  render(d2) {\n    d2.lib({ css: THEME })\n    const ui = d2.lib(UI)\n    const s = d2.state\n    if (!s.screen) {\n      Object.assign(s, { screen: 'boot', looks: [], chapters: [], resp: {}, sizes: {}, replies: [], sheet: null, tryOn: null, note: null, toast: null, busy: false,\n        sessionId: null, deviceId: read('fps.deviceId') || `d_${Math.random().toString(36).slice(2)}`, foto: read('fps.foto'), fotoRatio: read('fps.fotoRatio') })\n      keep('fps.deviceId', s.deviceId)\n      let tenant = d2.param('tenant')\n      try { tenant = tenant || new URLSearchParams(location.search).get('tenant') } catch (e) {}\n      d2.rush(d2.params.publicConfig).invoke(tenant ? { slug: tenant } : {}).then((r) => {\n        s.cfg = { ...(r?.data || r), ...(s._prev || {}) }\n        if (ui) ui.applyTenant(s.cfg.theme || {})\n        s.screen = 'start'\n        carregarSalvos(d2)\n      }).catch(() => { s.screen = 'erro' })\n    }\n    if (ui && !s._fonts) { s._fonts = true; ui.ensureFonts() }\n    if (s.cfg && ui && !s._theme) { s._theme = true; ui.applyTenant(s.cfg.theme || {}) }\n    if (s.screen === 'boot' || !s.cfg) return `<div class=\"app\"><div class=\"loading\"><div class=\"skeleton sk-hero\"></div></div></div>`\n    const screen = s.screen === 'loading' ? renderLoading(d2) : s.screen === 'feed' ? renderFeed(d2) : s.screen === 'note' ? renderNoteScreen(d2) : renderStart(d2)\n    return `<div class=\"app\">${screen}${s.story ? renderStory(d2) : ''}${s.screen !== 'feed' && s.sheet ? renderSheet(d2) : ''}${s.toast ? `<div class=\"toast\" role=\"status\">${esc(s.toast)}</div>` : ''}</div>`\n  },\n  mounted(d2) {\n    // Preview do admin: o app de gestão manda o rascunho do tenant e o shell renderiza com ele.\n    window.addEventListener('message', (e) => {\n      // Prévia de blocos (admin, testes): só da mesma origem; renderiza como um turno sem chamar o stylist.\n      if (e.data?.type === 'fps:preview-blocos' && e.origin === location.origin && Array.isArray(e.data.blocks)) {\n        const s = d2.state\n        if (!s.cfg) return\n        s.pedido = e.data.pedido || 'prévia'\n        aplicar(d2, e.data.blocks)\n        return\n      }\n      if (e.data?.type !== 'fps:preview-config' || !e.data.cfg) return\n      const s = d2.state\n      s._prev = e.data.cfg\n      s.cfg = { ...(s.cfg || {}), ...e.data.cfg }\n      d2.lib(UI)?.applyTenant(s.cfg.theme || {})\n      if (s.screen === 'boot' || s.screen === 'erro') s.screen = 'start'\n    })\n    window.parent?.postMessage({ type: 'fps:preview-ready' }, '*')\n    d2.on('[data-act]', 'click', (e) => {\n      const el = e.target.closest('[data-act]')\n      if (el && !el.disabled) go(d2, el.dataset.act, el)\n    })\n    d2.on('form[data-form=\"x-size\"]', 'submit', (e) => {\n      e.preventDefault()\n      const f = e.target\n      const alt = f.alturaCm.value\n      const peso = f.pesoKg.value\n      if (!alt) return toast(d2, tt(d2, 'extras.tamanho.faltaAltura', 'Diga sua altura'))\n      enviar(d2, tt(d2, 'extras.tamanho.msg', 'Tenho {a} cm e {p} kg, caimento {c}', { a: alt, p: peso || '?', c: f.caimento.value }), { event: 'size', blockId: null, payload: { productId: f.dataset.id, category: f.dataset.cat, profile: { alturaCm: Number(alt), pesoKg: peso ? Number(peso) : undefined, caimento: f.caimento.value } } })\n    })\n    d2.on('input.file', 'change', (e) => {\n      const f = e.target.files?.[0]\n      if (f) enviarFoto(d2, f)\n    })\n    // Polaroids: arrastar com o ponteiro; toque sem arrastar traz pra frente (ou manda pra trás se já está na frente).\n    let drag = null\n    d2.on('.pola', 'pointerdown', (e) => {\n      const el = e.target.closest('.pola')\n      if (!el || e.button) return\n      const p = posDe(el.dataset.pola)\n      drag = { el, key: el.dataset.pola, p, x0: e.clientX, y0: e.clientY, dx0: p.dx, dy0: p.dy, id: e.pointerId, moved: false }\n      try { el.setPointerCapture(e.pointerId) } catch (err) {}\n    })\n    d2.on('.pola', 'pointermove', (e) => {\n      if (!drag) return\n      const dx = e.clientX - drag.x0\n      const dy = e.clientY - drag.y0\n      if (!drag.moved) {\n        if (Math.hypot(dx, dy) < 8) return\n        drag.moved = true\n        drag.el.classList.add('drag')\n        drag.p.z = ++ZTOP\n        drag.el.style.setProperty('--z', drag.p.z)\n      }\n      drag.p.dx = drag.dx0 + dx\n      drag.p.dy = drag.dy0 + dy\n      drag.el.style.setProperty('--dx', `${drag.p.dx}px`)\n      drag.el.style.setProperty('--dy', `${drag.p.dy}px`)\n      drag.el.style.setProperty('--tilt', `${Math.max(-6, Math.min(6, (e.movementX || 0) * 0.6))}deg`)\n    })\n    const soltar = (e) => {\n      if (!drag) return\n      const { el, key, moved } = drag\n      drag = null\n      el.classList.remove('drag')\n      el.style.setProperty('--tilt', '0deg')\n      if (!moved && e.type === 'pointerup') alternar(el, key)\n    }\n    d2.on('.pola', 'pointerup', soltar)\n    d2.on('.pola', 'pointercancel', soltar)\n    d2.on('.pola', 'keydown', (e) => {\n      if (e.key !== 'Enter' && e.key !== ' ') return\n      e.preventDefault()\n      const el = e.target.closest('.pola')\n      alternar(el, el.dataset.pola)\n    })\n    d2.on('form', 'submit', (e) => {\n      e.preventDefault()\n      const form = e.target.closest('form')\n      const input = form?.querySelector('input')\n      const v = input?.value.trim()\n      if (!v || d2.state.busy) return\n      input.value = ''\n      if (form.classList.contains('ajuste')) {\n        const tr = d2.state.tryOn\n        if (!tr || tr.ajustando) return\n        const look = findLook(d2, tr.lookId)\n        d2.state.tryOn = { ...tr, ajustando: true, msg: null }\n        evento(d2, 'tryon_ajuste', look?.id, { lookId: tr.lookId, imagem: tr.img, pedido: v, pieces: look?.pieces }, true)\n        return\n      }\n      enviar(d2, v)\n    })\n  },\n  css: `\n:host { display: block; height: 100%; container-type: size; }\n.app { position: relative; height: 100%; overflow: hidden; background: var(--background); }\n\n.start { position: relative; height: 100%; color: #fff; background: #1c1b19; }\n.start-img { position: absolute; top: 0; left: 0; width: 100%; height: 64%; object-fit: cover; object-position: center bottom; }\n.start-shade { position: absolute; top: 0; left: 0; width: 100%; height: 64%; background: linear-gradient(rgba(0,0,0,.35), transparent 18%, transparent 86%, #1c1b19); }\n.start-top { position: absolute; top: 0; left: 0; right: 0; padding: 20px; }\n.start-body { position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; gap: 14px; padding: 20px 20px 28px; max-width: 560px; }\n.start-title { font-size: clamp(28px, 8cqi, 40px); }\n.start-sub { font-size: 15px; line-height: 1.45; color: rgba(255,255,255,.8); margin-top: -6px; }\n.chip.glass, .input.glass { background: rgba(255,255,255,.14); color: #fff; border: 1px solid rgba(255,255,255,.28); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }\n.suggestions { display: flex; gap: 8px; overflow-x: auto; margin: 0 -20px; padding: 0 20px; scrollbar-width: none; }\n.suggestions::-webkit-scrollbar { display: none; }\n.suggestions .chip { flex: 0 0 auto; height: 34px; font-size: 13px; }\n.suggestions .chip.glass:hover { background: rgba(255,255,255,.26); }\n.start-form { display: flex; gap: 8px; align-items: center; }\n.start-form .input { flex: 1; height: 52px; }\n.start-form .input::placeholder { color: rgba(255,255,255,.7); }\n.btn.send-light { width: 52px; height: 52px; background: #fff; color: #0a0a0a; }\n.chip-lg { height: 44px; padding: 0 18px; font-size: 15px; }\n.chip-lg:hover { border-color: var(--foreground); }\n\n.loading { height: 100%; display: flex; flex-direction: column; gap: 12px; padding: 24px 20px; max-width: 560px; margin: 0 auto; }\n.sk-hero { flex: 1; max-height: 70%; } .sk-line { height: 20px; } .sk-line.short { width: 60%; } .sk-alt { aspect-ratio: 3/4; }\n\n.feed, .note-screen { position: relative; height: 100%; }\n.pages { height: 100%; overflow-y: auto; scroll-snap-type: y proximity; scrollbar-width: none; }\n.pages::-webkit-scrollbar { display: none; }\n.page { scroll-snap-align: start; min-height: 100%; padding-bottom: 80px; }\n\n.cover { position: relative; height: 100cqh; padding: 0; overflow: hidden; background: #1a1a1a; }\n.cover-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }\n.cover-text { position: absolute; inset: auto 0 0 0; display: flex; flex-direction: column; gap: 12px; padding: 160px 20px 92px; color: #fff;\n  background: linear-gradient(transparent, rgba(0,0,0,.25) 25%, rgba(0,0,0,.6)); }\n.intro-title { font-size: clamp(26px, 7cqi, 40px); line-height: 1.1; }\n.meta { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; color: rgba(255,255,255,.85); font-size: 13px; }\n.meta li { display: inline-flex; align-items: center; gap: 6px; }\n.lede { font-size: 15px; line-height: 1.5; max-width: 36ch; }\n.index { list-style: none; padding: 0; margin-top: 4px; border-top: 1px solid rgba(255,255,255,.3); }\n.index button { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.3); text-align: left; font-size: 14px; color: #fff; }\n.index button span:first-child { font-family: var(--font-display); font-size: 11px; letter-spacing: .12em; opacity: .7; }\n.index button span:nth-child(2) { flex: 1; }\n.mala { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border: 1px solid rgba(255,255,255,.3); border-radius: var(--radius); font-size: 13px; }\n.mala ul { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; }\n.mala li b { font-family: var(--font-display); margin-right: 4px; }\n.mala li span { opacity: .75; margin-left: 4px; }\n.fine.light { color: rgba(255,255,255,.75); }\n\n.look { display: flex; flex-direction: column; height: 100cqh; min-height: 0; }\n.hero { position: relative; margin: 0; flex: 1; min-height: 0; background: var(--muted); }\n.hero img { width: 100%; height: 100%; object-fit: cover; object-position: center 20%; }\n.hero figcaption { position: absolute; inset: auto 0 0 0; padding: 64px 20px 20px; background: linear-gradient(transparent, rgba(0,0,0,.5)); }\n.eyebrow.light { color: rgba(255,255,255,.85); }\n.look-title { font-size: clamp(24px, 7cqi, 36px); margin-top: 4px; }\n.look-head { display: flex; flex-direction: column; }\n.board { position: relative; overflow: hidden; background: var(--muted); container-type: size; isolation: isolate; }\n.pola { position: absolute; margin: 0; background: #fff; padding: 6px 6px 22px; box-shadow: 0 10px 28px rgba(0,0,0,.14), 0 1px 3px rgba(0,0,0,.08);\n  transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(calc(var(--r, 0deg) + var(--tilt, 0deg))); z-index: var(--z, var(--z0, 1));\n  transition: transform .25s cubic-bezier(.2,.8,.2,1), box-shadow .25s; touch-action: pan-y; user-select: none; -webkit-user-select: none; cursor: grab; }\n.pola.drag { transition: none; cursor: grabbing; box-shadow: 0 24px 48px rgba(0,0,0,.26), 0 2px 6px rgba(0,0,0,.1); }\n.pola:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }\n.pola img { pointer-events: none; -webkit-user-drag: none; }\n.arrumar { position: absolute; top: 10px; right: 10px; z-index: 60; width: 32px; height: 32px; background: rgba(255,255,255,.85); color: #333; }\n.look-head-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.foto-chip { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px 0 4px; border-radius: 999px; border: 1px solid var(--border); background: var(--background); font-size: 12px; color: var(--muted-foreground); }\n.foto-chip img, .foto-chip svg { width: 20px; height: 20px; border-radius: 999px; object-fit: cover; object-position: center top; flex: 0 0 auto; }\n.foto-chip svg { padding: 3px; }\n.foto-chip:hover { border-color: var(--foreground); color: var(--foreground); }\n.foto-chip.light { background: rgba(255,255,255,.9); }\n.foto-chip.on { color: var(--primary); border-color: var(--primary); }\n.foto-chip.on svg { fill: currentColor; }\n.start-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.start-top .chip { height: 30px; font-size: 12px; gap: 6px; }\n.salvos-btn { position: relative; flex: 0 0 auto; background: var(--background); border: 1px solid var(--border); color: var(--muted-foreground); }\n.salvos-btn.on { color: var(--primary); border-color: var(--primary); }\n.salvos-btn.on svg { fill: currentColor; }\n.salvos-btn i { position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--primary); color: var(--primary-foreground); font-size: 10px; font-style: normal; display: flex; align-items: center; justify-content: center; }\n.salvos { display: flex; flex-direction: column; gap: 12px; }\n.salvo { display: flex; gap: 12px; }\n.salvo img { width: 84px; aspect-ratio: 3/4; object-fit: cover; object-position: center top; border-radius: var(--radius); background: var(--muted); flex: 0 0 auto; }\n.salvo-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; font-size: 13px; }\n.muted-txt { color: var(--muted-foreground); font-size: 12px; }\n.salvo-acoes { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }\n.dicas { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 14px; }\n.dicas li { display: flex; align-items: center; gap: 8px; }\n.foto-atual { display: flex; gap: 12px; align-items: center; }\n.foto-atual img { width: 72px; aspect-ratio: 3/4; object-fit: cover; object-position: center top; border-radius: var(--radius); }\n.foto-atual div { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }\n.pola img { display: block; width: 100%; height: auto; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; }\n.pola figcaption { position: absolute; inset: auto 8px 4px 8px; padding: 0; background: none; font-family: var(--font-display); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: #555; }\n.pola.p0 { top: 6%; left: 7%; width: min(44cqw, 50cqh); --r: -5deg; --z0: 2; }\n.pola.p1 { top: 14%; right: 7%; width: min(42cqw, 48cqh); --r: 4deg; --z0: 3; }\n.pola.p2 { bottom: 5%; left: 30%; width: min(32cqw, 38cqh); --r: 3deg; --z0: 4; }\n.pola.p3 { bottom: 9%; right: 12%; width: min(28cqw, 34cqh); --r: -7deg; --z0: 5; }\n.pola.p4 { top: 3%; left: 40%; width: min(24cqw, 30cqh); --r: 8deg; --z0: 1; }\n.pola.voce { top: 6%; left: 50%; margin-left: calc(min(56cqw, 64cqh) / -2); width: min(56cqw, 64cqh); --r: -2deg; --z0: 9; }\n.pola.voce img { object-fit: cover; }\n\n.look-body { display: flex; flex-direction: column; gap: 12px; padding: 16px 20px; }\n.look-body .caption { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }\n.pieces { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 20px; margin: 0 -20px; padding: 0 20px 4px; scrollbar-width: none; }\n.piece { position: relative; flex: 0 0 104px; width: 104px; min-width: 0; scroll-snap-align: start; display: flex; flex-direction: column; gap: 8px; }\n.piece img { width: 100%; aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); background: var(--muted); }\n.piece-info { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }\n.piece-info .price s, .alt .price s, .cart-row .price s { display: block; margin: 0; font-size: 11px; }\n.piece-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.35; }\n.swap { position: absolute; top: 6px; right: 6px; width: 32px; height: 32px; border-radius: 999px; background: rgba(255,255,255,.9); }\n.look-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.look-total { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; }\n.total { font-size: 20px; }\n\n.more { display: flex; flex-direction: column; justify-content: center; gap: 16px; padding: 48px 20px 120px; max-width: 560px; margin: 0 auto; }\n.more-title { font-size: clamp(26px, 7cqi, 36px); }\n.questions { max-width: 720px; margin: 0 auto; padding: 4px 20px 120px; display: flex; flex-direction: column; gap: 22px; }\n.questions.done { opacity: .55; padding-bottom: 24px; }\n.q { display: flex; flex-direction: column; gap: 10px; }\n.q-label { font-family: var(--font-display); font-size: 17px; }\n.q .opts { display: flex; flex-wrap: wrap; gap: 8px; }\n.q .chip[aria-pressed=\"true\"] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }\n.chapter-text:has(+ .questions) { padding-bottom: 16px; }\n.products { padding: 0 0 120px; }\n.chapter-text:has(+ .products) { padding-bottom: 20px; }\n.pcards { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 20px; padding: 0 20px 8px; scrollbar-width: none; }\n.pcards::-webkit-scrollbar { display: none; }\n.pcard { flex: 0 0 200px; min-width: 0; scroll-snap-align: start; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }\n.pcard-img img { width: 100%; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; border-radius: var(--radius); }\n.pcard .tams { font-size: 11px; color: var(--muted-foreground); letter-spacing: .04em; }\n.pcard-actions { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }\n.btn-sm { height: 34px; font-size: 13px; padding: 0 12px; }\n@container (min-width: 900px) { .pcards { flex-wrap: wrap; max-width: 960px; margin: 0 auto; overflow: visible; } .pcard { flex-basis: 210px; } }\n.loading2 { height: 100%; display: flex; flex-direction: column; justify-content: center; gap: 14px; padding: 24px 20px 48px; max-width: 560px; margin: 0 auto; position: relative; }\n.loading2 .bar { position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--border); }\n.loading2 .bar i { display: block; height: 100%; background: var(--primary); transition: width 1.2s ease; }\n.frames { position: relative; height: 210px; margin: 8px 0; }\n.frame { position: absolute; top: 10px; width: 120px; aspect-ratio: 3/4.4; background: #fff; padding: 6px 6px 22px; box-shadow: 0 10px 28px rgba(0,0,0,.12); animation: fps-drop .7s cubic-bezier(.2,.8,.3,1.2) both; }\n.frame::before { content: ''; display: block; width: 100%; aspect-ratio: 3/4; background: var(--muted); }\n.frame .shine { position: absolute; inset: 6px 6px 22px; background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.75) 50%, transparent 65%); background-size: 250% 100%; animation: fps-shine 1.6s ease-in-out infinite; }\n.frame.f0 { left: 8%; --r: -6deg; transform: rotate(-6deg); animation-delay: .1s; }\n.frame.f1 { left: 36%; top: 26px; --r: 4deg; transform: rotate(4deg); animation-delay: .5s; z-index: 2; }\n.frame.f2 { left: 62%; --r: -2deg; transform: rotate(-2deg); animation-delay: .9s; }\n.status { font-family: var(--font-display); font-size: 13px; letter-spacing: .08em; color: var(--muted-foreground); animation: fps-status .5s ease both; }\n.chapter-text.pending .frames { height: 170px; }\n.chapter-text.pending .frame { width: 96px; }\n.alts-loading .frames { height: 180px; }\n.scan { position: absolute; left: 0; right: 0; height: 30%; background: linear-gradient(transparent, rgba(255,255,255,.18) 80%, rgba(255,255,255,.55)); border-bottom: 1px solid rgba(255,255,255,.8); animation: fps-scan 2.4s ease-in-out infinite; pointer-events: none; z-index: 1; }\n.tryon-progress { z-index: 2; }\n.trilha { display: flex; gap: 8px; align-items: center; font-family: var(--font-display); font-size: 12px; letter-spacing: .1em; text-transform: uppercase; }\n.trilha span { opacity: .55; } .trilha span.ok { opacity: .9; } .trilha span.now { opacity: 1; font-weight: 600; } .trilha i { opacity: .5; font-style: normal; }\n.etapa-n { font-size: 12px; opacity: .8; }\n.ebar { width: 160px; height: 2px; background: rgba(255,255,255,.3); }\n.ebar i { display: block; height: 100%; width: 0; background: #fff; animation: fps-ebar 22s cubic-bezier(.1,.6,.3,1) forwards; }\n@keyframes fps-drop { from { opacity: 0; transform: translateY(-40px) rotate(0deg); } to { opacity: 1; transform: rotate(var(--r)); } }\n@keyframes fps-shine { from { background-position: 150% 0; } to { background-position: -50% 0; } }\n@keyframes fps-status { from { opacity: 0; transform: translateY(6px); } }\n@keyframes fps-scan { 0% { top: -30%; } 100% { top: 100%; } }\n@keyframes fps-ebar { to { width: 92%; } }\n@media (prefers-reduced-motion: reduce) { .frame, .frame .shine, .scan, .status, .ebar i { animation: none !important; } .ebar i { width: 50%; } }\n.chapter-quote { scroll-snap-align: start; padding: 56px 20px 16px; max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 10px; border-top: 1px solid var(--border); }\n.chapter-quote:first-child { border-top: 0; }\n.quote { font-size: clamp(19px, 5cqi, 26px); line-height: 1.25; }\n.chapter-text { padding: 4px 20px 120px; max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }\n.lede-text { font-size: 17px; line-height: 1.6; }\n.eyebrow.muted { color: var(--muted-foreground); }\n.opts { display: flex; flex-wrap: wrap; gap: 8px; }\n\n.ask-wrap { position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px 16px; }\n.ask { display: flex; gap: 6px; align-items: center; }\n.ask .input { flex: 1; box-shadow: 0 1px 2px rgba(0,0,0,.04); }\n.note { position: relative; padding: 14px 40px 14px 16px; border-radius: var(--radius); background: var(--background); border: 1px solid var(--border); box-shadow: 0 6px 24px rgba(0,0,0,.08); font-size: 14px; line-height: 1.5; display: flex; flex-direction: column; gap: 6px; }\n.note-x { position: absolute; top: 6px; right: 6px; width: 28px; height: 28px; }\n.note-screen .ask-wrap { top: 0; justify-content: flex-end; max-width: 640px; margin: 0 auto; }\n.btn.round { border-radius: 999px; }\n.spinner { width: 18px; height: 18px; border-radius: 999px; border: 2px solid currentColor; border-right-color: transparent; animation: fps-spin .8s linear infinite; }\n.spinner.light { color: var(--primary-foreground); }\n\n.tryon { position: absolute; inset: 0; z-index: 10; background: #0a0a0a; display: flex; flex-direction: column; animation: fps-in .25s ease-out; }\n.tryon-top { position: absolute; top: 0; left: 0; right: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; }\n.tryon-top .round { background: rgba(255,255,255,.9); }\n.tryon-top .badge { gap: 4px; background: rgba(255,255,255,.9); }\n.tryon-stage { position: relative; flex: 1; min-height: 0; }\n.tryon-img { width: 100%; height: 100%; object-fit: cover; object-position: center 20%; transition: filter .3s; }\n.tryon-img.dim { filter: brightness(.55) blur(2px); }\n.tryon-progress { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: #fff; font-size: 14px; }\n.tryon-panel { background: var(--background); border-radius: 16px 16px 0 0; margin-top: -16px; padding: 20px; display: flex; flex-direction: column; gap: 12px; position: relative; z-index: 1; max-height: 62%; overflow-y: auto; }\n.rail { position: absolute; right: 12px; bottom: 18px; z-index: 2; display: flex; flex-direction: column; gap: 12px; }\n.rail-btn { width: 44px; height: 44px; border-radius: 999px; display: flex; align-items: center; justify-content: center; color: #fff; background: rgba(0,0,0,.28); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }\n.rail-btn[aria-pressed=\"true\"], .rail-btn.on { background: rgba(255,255,255,.92); color: #0a0a0a; }\n.rail-btn.on svg { fill: currentColor; }\n.tryon-bar { position: relative; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 10px 16px 14px; background: var(--background); }\n.bar-toggle { display: inline-flex; align-items: center; gap: 6px; height: 44px; padding: 0 10px; font-size: 13px; color: var(--muted-foreground); white-space: nowrap; }\n.tryon.min .tryon-panel { display: none; }\n.tryon:not(.min) .tryon-bar { display: none; }\n.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 32px; }\n.caption.ajustando { display: flex; align-items: center; gap: 8px; }\n.caption.ajustando .spinner { width: 14px; height: 14px; }\n.sheet-x { position: absolute; top: 10px; right: 12px; width: 32px; height: 32px; }\n.tr-pecas { display: flex; gap: 10px; overflow-x: auto; scrollbar-width: none; }\n.tr-peca { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 11px; color: var(--muted-foreground); }\n.tr-peca img { width: 44px; aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--border); }\n.ajuste { display: flex; gap: 6px; align-items: center; }\n.ajuste .input { flex: 1; height: 42px; }\n.ajuste .btn { width: 42px; height: 42px; }\n.share-opts { display: flex; flex-direction: column; gap: 8px; }\n.share-peca { justify-content: flex-start; gap: 10px; height: auto; padding: 8px 12px; text-align: left; font-size: 13px; }\n.share-peca img { width: 32px; aspect-ratio: 3/4; object-fit: cover; border-radius: 4px; }\n.share-peca span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.fine { font-size: 12px; color: var(--muted-foreground); }\n.erro-box { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; border-radius: var(--radius); background: #fdecec; border: 1px solid #f0b3b3; color: #8a1c1c; font-size: 13px; line-height: 1.4; }\n.erro-box svg { flex: 0 0 auto; margin-top: 1px; }\n.erro-box > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.erro-box b { font-weight: 600; }\n.erro-box code { font-size: 11px; opacity: .8; word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n.tryon-img.dim.erro { filter: grayscale(.6) brightness(.7); }\n.row { display: flex; gap: 8px; } .grow { flex: 1; min-width: 0; }\n.tryon-label { font-size: 14px; font-weight: 500; }\n.swatches { display: flex; gap: 10px; overflow-x: auto; scrollbar-width: none; }\n.swatch { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 11px; color: var(--muted-foreground); text-transform: capitalize; }\n.swatch img { width: 44px; height: 44px; border-radius: 999px; object-fit: cover; object-position: center 30%; border: 1px solid var(--border); }\n.swatch[aria-pressed=\"true\"] img { border: 2px solid var(--foreground); }\n\n.scrim { position: absolute; inset: 0; z-index: 20; background: rgba(0,0,0,.4); animation: fps-fade .2s ease-out; }\n.sheet { position: absolute; left: 0; right: 0; bottom: 0; z-index: 21; max-height: 85%; overflow-y: auto; background: var(--background);\n  border-radius: 16px 16px 0 0; padding: 8px 20px 24px; display: flex; flex-direction: column; gap: 12px; animation: fps-up .25s cubic-bezier(.4,0,.2,1); }\n.grabber { align-self: center; width: 36px; height: 4px; border-radius: 4px; background: var(--border); margin-bottom: 4px; }\n.sheet-title { font-size: 20px; }\n.alts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n.alt { display: flex; flex-direction: column; gap: 6px; text-align: left; font-size: 13px; min-width: 0; }\n.alt img { width: 100%; aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); border: 2px solid transparent; }\n.alt[aria-pressed=\"true\"] img { border-color: var(--foreground); }\n.cart { display: flex; flex-direction: column; gap: 14px; }\n.cart-row { display: flex; gap: 12px; font-size: 14px; }\n.cart-row img { width: 72px; aspect-ratio: 3/4; object-fit: cover; border-radius: var(--radius); }\n.cart-row .grow { display: flex; flex-direction: column; gap: 4px; }\n.sizes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }\n.size { min-width: 40px; height: 32px; padding: 0 8px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 13px; }\n.size[aria-pressed=\"true\"] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }\n.size[disabled] { text-decoration: line-through; opacity: .4; }\n.sheet-foot { position: sticky; bottom: -24px; margin: 0 -20px -24px; padding: 12px 20px 24px; background: var(--background); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }\n.cart-total { display: flex; justify-content: space-between; align-items: baseline; }\n.btn[disabled] { opacity: .5; cursor: not-allowed; }\n\n.toast { position: absolute; left: 50%; bottom: 84px; transform: translateX(-50%); z-index: 30; max-width: calc(100% - 32px); padding: 10px 16px;\n  border-radius: var(--radius); background: var(--foreground); color: var(--background); font-size: 13px; animation: fps-fade .2s ease-out; }\n\n@keyframes fps-in { from { opacity: 0; transform: scale(.98); } }\n@keyframes fps-up { from { transform: translateY(100%); } }\n@keyframes fps-fade { from { opacity: 0; } }\n@keyframes fps-spin { to { transform: rotate(360deg); } }\n\n@container (min-width: 900px) {\n  .look { display: grid; grid-template-columns: 55% 1fr; height: 100cqh; padding-bottom: 0; }\n  .hero { height: 100%; min-height: 0; }\n  .look-body { justify-content: center; padding: 48px 48px 112px; overflow-y: auto; }\n  .pieces { flex-wrap: wrap; margin: 0; padding: 0; overflow: visible; }\n  .piece { flex-basis: calc(25% - 8px); width: auto; }\n  .ask-wrap { left: 55%; padding: 16px 48px 24px; }\n  .note-screen .ask-wrap { left: 0; }\n  .start-img { left: auto; right: 0; width: auto; height: 100%; max-width: 60%; aspect-ratio: 1600 / 2240; object-position: center; }\n  .start-shade { left: auto; right: 0; width: 60%; height: 100%; background: linear-gradient(90deg, #1c1b19, transparent 30%); }\n  .start-top { padding: 32px 56px; }\n  .start-body { top: 0; justify-content: center; padding: 56px; max-width: 42%; }\n  .start-title { font-size: 48px; }\n  .suggestions { flex-wrap: wrap; overflow: visible; margin: 0; padding: 0; }\n  .cover-text { inset: 0 auto 0 0; width: 560px; justify-content: flex-end; padding: 48px 56px 64px; background: linear-gradient(90deg, rgba(0,0,0,.6), rgba(0,0,0,.25) 70%, transparent); }\n  .sheet { left: auto; width: 440px; top: 0; max-height: none; border-radius: 0; animation-name: fps-left; }\n  .tryon { flex-direction: row; background: #efefef; }\n  .tryon-img { object-fit: contain; }\n  .tryon-panel { width: 400px; margin: 0; border-radius: 0; justify-content: center; }\n  .tryon.min .tryon-panel { display: flex; }\n  .tryon-bar { display: none; }\n}\n@keyframes fps-left { from { transform: translateX(100%); } }\n.xw { display: flex; flex-direction: column; gap: 12px; padding: 28px 20px; border-top: 1px solid var(--border); background: var(--background); }\n.xw-title { font-size: 24px; line-height: 1.15; }\n.xw-foot { justify-content: space-between; }\n.xs-res { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }\n.xs-size { font-size: 44px; line-height: 1; }\n.xs-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.xs-form select, .xs-form button { grid-column: 1 / -1; }\n.story { position: fixed; inset: 0; z-index: 60; background: #121212; color: #fff; display: flex; flex-direction: column; gap: 18px; padding: 14px 16px 28px; overflow-y: auto; }\n.story > * { width: 100%; max-width: 560px; margin-inline: auto; }\n.story-bars { display: flex; gap: 4px; }\n.story-bars i { flex: 1; height: 3px; border-radius: 2px; background: rgba(255,255,255,.25); }\n.story-bars i.on { background: #fff; }\n.story-bars i.now { background: linear-gradient(90deg, #fff 50%, rgba(255,255,255,.25) 50%); }\n.story-top { display: flex; justify-content: space-between; align-items: center; }\n.story-top .btn { color: #fff; background: rgba(255,255,255,.12); }\n.story-q { font-size: clamp(28px, 7vw, 44px); line-height: 1.1; margin: 12px auto 4px; color: #fff; }\n.story-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }\n.story-card { min-height: 96px; border-radius: var(--radius); border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06); color: #fff; font-size: 17px; text-align: left; padding: 14px; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; cursor: pointer; }\n.story-card.img { padding: 0 0 12px; overflow: hidden; }\n.story-card.img span { padding: 0 12px; }\n.story-card img { width: 100%; aspect-ratio: 3/4; object-fit: cover; }\n.story-card:hover:not([disabled]) { border-color: #fff; }\n.story-card[disabled] { opacity: .5; }\n.story-busy { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,.35); }\n.xq-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }\n.xq-card { display: flex; flex-direction: column; gap: 6px; padding: 0; border: 2px solid transparent; border-radius: var(--radius); background: none; text-align: left; font-size: 14px; }\n.xq-card img { width: 100%; aspect-ratio: 3/4; object-fit: cover; border-radius: calc(var(--radius) - 2px); background: var(--muted); }\n.xq-card[aria-pressed=\"true\"] { border-color: var(--primary); }\n.xq-card[disabled]:not([aria-pressed=\"true\"]) { opacity: .45; }\n.xc { display: flex; flex-direction: column; overflow-x: auto; }\n.xc-row { display: grid; grid-template-columns: 96px repeat(var(--n), minmax(130px, 1fr)); gap: 10px; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }\n.xc-head { align-items: end; border-bottom: 0; }\n.xc-p { display: flex; flex-direction: column; gap: 4px; }\n.xc-p img { width: 100%; max-width: 180px; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; border-radius: 6px; }\n.xmini-grid { display: flex; gap: 10px; overflow-x: auto; }\n.xmini { flex: 0 0 120px; display: flex; flex-direction: column; gap: 4px; font-size: 13px; }\n.xmini img { width: 120px; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; border-radius: 6px; }\n.xs-alert { display: flex; gap: 12px; align-items: center; }\n.xs-alert img { width: 64px; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; border-radius: 6px; }\n.xs-alert > div { display: flex; flex-direction: column; gap: 2px; }\n@container (min-width: 900px) { .xw { padding: 40px 48px; } }\n`,\n}"}},"conversa":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"conversa"}}}},"publicConfig":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"publicConfig"}}}},"provador":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"provador"}}}},"fotos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"fotos"}}}}}}}}}},{"_id":"6ad112102e5d35cd271504de","component":"data2-core-types@Component","system":"69133afb854168f247b6f9af","params":{"extends":"data2-core-primitives@Block","component":"fpsAdmin","package":"aramis","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Fashion Personal Shopper — app de gestão. Lê e escreve só pelo fps-admin-api (versão + auditoria); o preview é o fpsShell recebendo o rascunho.\nconst THEME = 'https://aramis.data2.media/file/FoPSiAH1DGrpNy6iK1Enh'\nconst UI = 'https://aramis.data2.media/file/Dl7LSkkqcZi-rDhz8sLdb'\nconst LUCIDE = 'lucide@0.468.0'\nconst PREVIEW = { mobile: { ref: 'IiAQaSG6IlFKETXBGGxpw', w: 390, h: 844 }, desktop: { ref: 'VjF08qLzRZr_eYehfqaIh', w: 1440, h: 900 } }\nconst SISTEMA = ['_id', 'createdAt', 'updatedAt', '__v', 'embedding', '_embedding']\n\nconst TELAS = [\n  { g: 'Visão geral', k: 'painel', t: 'Painel', i: 'LayoutDashboard' },\n  { g: 'Visão geral', k: 'conversas', t: 'Conversas', i: 'MessagesSquare', tabela: 'fps-sessions', ro: true },\n  { g: 'Visão geral', k: 'historico', t: 'Histórico', i: 'History', tabela: 'fps-audit', ro: true },\n  { g: 'Visão geral', k: 'insights', t: 'Insights', i: 'Lightbulb' },\n  { g: 'Visão geral', k: 'alertas', t: 'Alertas', i: 'BellRing', tabela: 'fps-alertas', semBarra: true },\n  { g: 'Experiência', k: 'marca', t: 'Marca e experiência', i: 'Palette', tabela: 'fps-tenant', preview: true },\n  { g: 'Experiência', k: 'persona', t: 'Persona e tom', i: 'Feather', tabela: 'fps-persona', versionada: true },\n  { g: 'Experiência', k: 'exemplos', t: 'Exemplos de tom', i: 'Quote', tabela: 'fps-persona-exemplos' },\n  { g: 'Experiência', k: 'ocasioes', t: 'Ocasiões', i: 'CalendarHeart', tabela: 'fps-occasions' },\n  { g: 'Experiência', k: 'looksCfg', t: 'Looks', i: 'LayoutGrid', tabela: 'fps-config', chaveada: true, chave: 'looks' },\n  { g: 'Experiência', k: 'blocos', t: 'Blocos', i: 'Blocks', tabela: 'fps-blocks' },\n  { g: 'Experiência', k: 'config', t: 'Regras e limites', i: 'SlidersHorizontal', tabela: 'fps-config', chaveada: true },\n  { g: 'Curadoria', k: 'looks', t: 'Looks curados', i: 'Shirt', tabela: 'fps-curated-looks' },\n  { g: 'Curadoria', k: 'campanhas', t: 'Campanhas', i: 'Megaphone', tabela: 'fps-campaigns' },\n  { g: 'Curadoria', k: 'merch', t: 'Merchandising', i: 'Tags', tabela: 'fps-merch-rules' },\n  { g: 'Curadoria', k: 'imagens', t: 'Banco de imagens', i: 'Images', tabela: 'fps-images' },\n  { g: 'Curadoria', k: 'guia', t: 'Guia de estilo', i: 'Library', tabela: 'fps-guia' },\n  { g: 'Conteúdo', k: 'kb', t: 'Base de conhecimento', i: 'BookOpen', tabela: 'fps-kb-articles' },\n  { g: 'Conteúdo', k: 'gaps', t: 'Sem resposta', i: 'CircleHelp', tabela: 'fps-kb-gaps', semBarra: true },\n  { g: 'Qualidade', k: 'console', t: 'Console do stylist', i: 'FlaskConical' },\n  { g: 'Qualidade', k: 'replay', t: 'Replay', i: 'Repeat', tabela: 'fps-sessions', ro: true },\n  { g: 'Qualidade', k: 'simulados', t: 'Clientes simulados', i: 'UserCog', tabela: 'fps-sim-clientes' },\n  { g: 'Qualidade', k: 'experimentos', t: 'Experimentos', i: 'Split', tabela: 'fps-experimentos' },\n  { g: 'Clientes', k: 'clientes', t: 'Clientes', i: 'Users', tabela: 'fps-customers' },\n  { g: 'Clientes', k: 'tamanho', t: 'Tamanho', i: 'Ruler', tabela: 'fps-size-charts' },\n  { g: 'Clientes', k: 'integracoes', t: 'Integrações', i: 'Plug' },\n  { g: 'Operação', k: 'servicos', t: 'Serviços', i: 'Wrench', tabela: 'fps-services' },\n  { g: 'Operação', k: 'execucoes', t: 'Execuções', i: 'Activity', tabela: 'fps-service-runs', ro: true },\n  { g: 'Operação', k: 'tryonCfg', t: 'Try-on', i: 'ScanFace', tabela: 'fps-config', chaveada: true, chave: 'tryon' },\n  { g: 'Operação', k: 'modelCfg', t: 'Modelo e custo', i: 'Cpu', tabela: 'fps-config', chaveada: true, chave: 'model' },\n  { g: 'Operação', k: 'limitesCfg', t: 'Limites', i: 'ShieldAlert', tabela: 'fps-config', chaveada: true, chave: 'guardrails' },\n  { g: 'Operação', k: 'canaisCfg', t: 'Canais', i: 'Radio', tabela: 'fps-config', chaveada: true, chave: 'channels' },\n  { g: 'Sistema', k: 'gestores', t: 'Gestores', i: 'ShieldCheck', tabela: 'fps-managers' },\n]\n\nconst esc = (s) => String(s ?? '').replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]))\nconst tela = (d2) => TELAS.find((x) => x.k === d2.state.nav)\n// Opção de pergunta como linha de texto: \"Label | geo | precoMax=300\".\nconst opcaoTxt = (o) => [o.label, o.geo ? 'geo' : '', o.precoMax != null ? `precoMax=${o.precoMax}` : ''].filter(Boolean).join(' | ')\nconst opcaoDe = (line) => {\n  const [label, ...fl] = line.split('|').map((x) => x.trim())\n  const o = { label }\n  for (const f of fl) { if (f === 'geo') o.geo = true; else if (f.startsWith('precoMax=')) o.precoMax = Number(f.slice(9)) }\n  return o\n}\nconst ic = (d2, name, size) => {\n  const ui = d2.lib(UI)\n  const lucide = d2.lib(LUCIDE)\n  return ui && lucide ? ui.icon(lucide.icons || lucide.default?.icons || lucide, name, size) : ''\n}\nconst quando = (v) => (v ? new Date(v).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' }) : '')\nconst ehISO = (v) => typeof v === 'string' && /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}/.test(v)\n// ISO (UTC) → valor de <input type=datetime-local> no fuso do browser.\nconst localDT = (iso) => { const d = new Date(iso); if (isNaN(d)) return ''; const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}` }\nconst ONDE = [['capa', 'Capa'], ['primeira-pagina', 'Primeira página'], ['sugestao', 'Sugestão']]\nconst rotulo = (r) => (r.tabela && r.acao ? `${r.tabela.replace('fps-', '')} · ${r.acao}${r.depois?.nome || r.depois?.titulo || r.depois?.chave || r.depois?.key ? ` · ${r.depois.nome || r.depois.titulo || r.depois.chave || r.depois.key}` : ''}` : null) || r.titulo || r.productName || r.nome || r.title || r.pergunta || r.mensagem || r.email || r.chave || r.slug || (r.serviceKey ? `${r.serviceKey} · ${r.status || ''}${r.duracaoMs != null ? ` · ${(r.duracaoMs / 1000).toFixed(1)}s` : ''}` : null) || r.key || (r.categoria && r.tamanho ? `${r.categoria} · ${r.tamanho}` : null) || (r.sessionId && r.turnos != null ? `${r.ocasiao || 'conversa'} · ${r.turnos} turno(s) · ${r.canal || ''}` : null) || r.sessionId || r.acao || r._id\n// Objeto \"plano\": só primitivos ou objetos planos dentro, até dois níveis; vira formulário em vez de JSON (copy, theme, imagens, valor).\nconst ehPlano = (v, nivel = 0) => v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0 && Object.values(v).every((x) => x == null || typeof x !== 'object' || (Array.isArray(x) && x.every((y) => typeof y === 'string')) || (nivel < 1 && ehPlano(x, nivel + 1)))\nconst setPath = (obj, path, v) => {\n  const ks = path.split('.')\n  const out = { ...obj }\n  let cur = out\n  for (let i = 0; i < ks.length - 1; i++) { cur[ks[i]] = { ...(cur[ks[i]] || {}) }; cur = cur[ks[i]] }\n  cur[ks[ks.length - 1]] = v\n  return out\n}\nconst tipoDe = (v) => (typeof v === 'boolean' ? 'bool' : typeof v === 'number' ? 'num' : Array.isArray(v) && v.every((x) => typeof x === 'string') ? 'lista'\n  : v && typeof v === 'object' ? 'json' : typeof v === 'string' && v.length > 80 ? 'texto' : 'str')\n\nasync function api(d2, body) {\n  // O guia tem porta própria: salva com auditoria e reindexa os trechos da busca.\n  const porta = body.tabela === 'fps-guia' ? d2.params.guiaApi || 'fps-guia-admin' : d2.params.api\n  const r = await d2.rush(porta).invoke(body)\n  const out = r?.data ?? r\n  if (out?.erro) throw new Error(out.erro)\n  return out\n}\n\nfunction toast(d2, msg) {\n  d2.state.toast = msg\n  clearTimeout(d2.state._t)\n  d2.state._t = setTimeout(() => { d2.state.toast = null }, 2600)\n}\n\n// fps-config guarda uma linha por versão; a lista mostra a última de cada chave.\nfunction ultimas(rows) {\n  const m = {}\n  for (const r of rows) if (!m[r.chave] || (r.versao || 0) > (m[r.chave].versao || 0)) m[r.chave] = r\n  return Object.values(m)\n}\n\nasync function abrir(d2, k) {\n  const s = d2.state\n  Object.assign(s, { nav: k, rows: [], sel: null, draft: null, dirty: false, erroCampo: null, msgs: null, loading: true })\n  const tl = tela(d2)\n  try {\n    if (k === 'painel') s.stats = await painel(d2)\n    else if (k === 'integracoes') { await carregarIntegracoes(d2) }\n    else if (k === 'insights') { s.ins = await insights(d2) }\n    else if (k === 'console') { s.cons = s.cons || { pub: [], rasc: [] }; s.sims = (await api(d2, { acao: 'listar', tabela: 'fps-sim-clientes', limite: 50 })).data || [] }\n    else {\n      const out = await api(d2, { acao: 'listar', tabela: tl.tabela, limite: 300, filtro: { ...(k === 'execucoes' && s.exFiltro ? { serviceKey: s.exFiltro } : {}), $sort: { createdAt: -1 } } })\n      if (k === 'modelCfg') d2.rush(d2.params.alertas || 'fps-alertas-rodar').invoke({ dryRun: true }).then((r) => { s.custoMed = (r?.data || r)?.medidas }).catch(() => { s.custoMed = null })\n      s.rows = tl.chaveada ? ultimas(out.data || []) : out.data || []\n      if (tl.chave) s.rows = s.rows.filter((r) => r.chave === tl.chave)\n      if ((tl.preview || tl.chave) && s.rows[0]) selecionar(d2, s.rows[0]._id)\n      if (k === 'ocasioes') api(d2, { acao: 'listar', tabela: 'fps-images', limite: 200, filtro: { tipo: 'capa', ativo: true } }).then((o) => { s.capas = o.data || [] }).catch(() => { s.capas = [] })\n      if (k === 'gaps') s.rows = s.rows.slice().sort((a, b) => (a.status === 'aberta' ? 0 : 1) - (b.status === 'aberta' ? 0 : 1) || (b.ocorrencias || 0) - (a.ocorrencias || 0))\n      if (k === 'gaps') api(d2, { acao: 'listar', tabela: 'fps-kb-articles', limite: 300 }).then((o) => { s.kbArts = o.data || [] }).catch(() => { s.kbArts = [] })\n      if (k === 'imagens') { s.capaTeste = null; api(d2, { acao: 'listar', tabela: 'fps-occasions', limite: 100, filtro: { status: 'publicado' } }).then((o) => { s.occs = o.data || []; capasPorOcasiao(d2) }).catch(() => { s.occs = [] }) }\n      if (k === 'alertas') s.rows = s.rows.slice().sort((a, b) => (a.status === 'aberto' ? 0 : 1) - (b.status === 'aberto' ? 0 : 1) || new Date(b.ultimaVez || b.createdAt) - new Date(a.ultimaVez || a.createdAt))\n      if (k === 'tamanho') s.rows = s.rows.slice().sort((a, b) => String(a.categoria).localeCompare(String(b.categoria)) || (a.medidas?.ordem || 0) - (b.medidas?.ordem || 0))\n      if (k === 'campanhas') api(d2, { acao: 'listar', tabela: 'fps-curated-looks', limite: 300, filtro: { $sort: { createdAt: -1 } } }).then((o) => { s.looksCurados = o.data || [] }).catch(() => { s.looksCurados = [] })\n    }\n  } catch (e) {\n    toast(d2, `Não carregou: ${e.message}`)\n  } finally {\n    s.loading = false\n  }\n}\n\nasync function painel(d2) {\n  // ponytail: conta no cliente sobre até 2000 linhas por tabela; passa pra aggregate no fps-admin-api quando o volume estourar isso.\n  const [ss, ms, ev, gp] = await Promise.all([['fps-sessions', {}], ['fps-messages', { papel: 'cliente' }], ['fps-events', {}], ['fps-kb-gaps', {}]]\n    .map(([t, filtro]) => api(d2, { acao: 'listar', tabela: t, limite: 2000, filtro }).then((o) => o.data || []).catch(() => [])))\n  const alertas = await api(d2, { acao: 'listar', tabela: 'fps-alertas', limite: 200, filtro: { status: 'aberto' } }).then((o) => o.data || []).catch(() => [])\n  const teste = (id) => /^suite-/.test(id || '')\n  const dias = d2.state.periodo ?? 7\n  const desde = dias ? Date.now() - dias * 864e5 : 0\n  const sP = ss.filter((x) => !teste(x.sessionId) && new Date(x.iniciadaEm || x.createdAt).getTime() >= desde)\n  const ids = new Set(sP.map((x) => x.sessionId))\n  const eP = ev.filter((e) => ids.has(e.sessionId))\n  const por = (tipo) => eP.filter((e) => e.tipo === tipo)\n  const com = (tipo) => new Set(por(tipo).map((e) => e.sessionId))\n  const comLook = com('look_mostrado'), comTry = com('tryon_gerado'), comSacola = com('sacola_aberta'), comCart = com('add_to_cart')\n  const ABANDONO_MIN = 30 // ponytail: limiar fixo; vira chave painel.abandonoMin em fps-config se alguem quiser ajustar\n  const ultima = (x) => new Date(x.ultimaEm || x.updatedAt || x.createdAt).getTime()\n  const abandonadas = sP.filter((x) => comLook.has(x.sessionId) && !comCart.has(x.sessionId) && Date.now() - ultima(x) > ABANDONO_MIN * 60e3)\n  const primeira = {}\n  for (const m of ms) if (m.papel === 'cliente' && (!primeira[m.sessionId] || new Date(m.createdAt) < new Date(primeira[m.sessionId].at))) primeira[m.sessionId] = { texto: m.texto, at: m.createdAt }\n  const porTipo = {}\n  for (const e of eP) { const k = e.tipo || 'outro'; porTipo[k] = (porTipo[k] || 0) + 1 }\n  const pct = (n) => (sP.length ? `${Math.round((n / sP.length) * 100)}% das sessões` : '')\n  return {\n    sessoes: sP.length, looks: por('look_mostrado').length, tryons: por('tryon_gerado').length, fotos: por('foto_enviada').length,\n    comCart: comCart.size, pctCart: pct(comCart.size), valor: por('add_to_cart').reduce((s, e) => s + (Number(e.valor) || 0), 0),\n    abandonadas: abandonadas.length, pctAband: pct(abandonadas.length), gaps: gp.length,\n    alertas: alertas.length, alertasCrit: alertas.filter((a) => a.nivel === 'critico').length,\n    funil: [['Sessões', sP.length], ['Viu look', comLook.size], ['Try-on', comTry.size], ['Abriu sacola', comSacola.size], ['Adicionou', comCart.size]],\n    porTipo: Object.entries(porTipo).sort((a, b) => b[1] - a[1]).slice(0, 10),\n    recentes: sP.sort((a, b) => ultima(b) - ultima(a)).slice(0, 8).map((x) => ({ ...x, frase: primeira[x.sessionId]?.texto, tryon: comTry.has(x.sessionId), cart: comCart.has(x.sessionId) })),\n  }\n}\n\nfunction selecionar(d2, id) {\n  const s = d2.state\n  const tl = tela(d2)\n  if (id === 'novo' && tl.k === 'imagens') {\n    s.sel = 'novo'\n    s.draft = { titulo: '', descricao: '', url: '', tipo: 'capa', ativo: true, ocasioes: [], fonte: '' }\n  } else if (id === 'novo' && tl.k === 'experimentos') {\n    s.sel = 'novo'\n    s.draft = { nome: '', alvo: 'persona', versaoA: null, versaoB: null, pctB: 50, status: 'rascunho', nota: '' }\n  } else if (id === 'novo' && tl.k === 'gestores') {\n    s.sel = 'novo'\n    s.draft = { email: '', nome: '', ativo: true }\n  } else if (id === 'novo' && tl.k === 'merch') {\n    s.sel = 'novo'\n    s.draft = { nome: '', tipo: 'destaque', alvo: { campo: 'categoria', valor: '' }, peso: 1, nota: '', status: 'rascunho' }\n    s.mRes = null\n  } else if (id === 'novo') {\n    const base = s.rows[0] || {}\n    const vazio = {}\n    for (const [k, v] of Object.entries(base)) if (!SISTEMA.includes(k)) vazio[k] = tipoDe(v) === 'bool' ? false : tipoDe(v) === 'num' ? 0 : tipoDe(v) === 'lista' ? [] : tipoDe(v) === 'json' ? (Array.isArray(v) ? [] : {}) : ''\n    if ('status' in vazio) vazio.status = 'rascunho'\n    s.sel = 'novo'\n    s.draft = vazio\n  } else {\n    const r = s.rows.find((x) => x._id === id)\n    s.sel = id\n    s.draft = JSON.parse(JSON.stringify(r))\n  }\n  s.dirty = false\n  s.erroCampo = null\n  s.mRes = null\n  s.cliVisto = null\n  if (tl.k === 'experimentos') { s.expMet = null; if (!s.personas) api(d2, { acao: 'listar', tabela: 'fps-persona', limite: 50, filtro: { $sort: { versao: -1 } } }).then((o) => { s.personas = o.data || [] }).catch(() => { s.personas = [] }); if (s.draft?._id) metricasExperimento(d2, s.draft._id) }\n  if (tl.k === 'historico') s.rbConfirma = false\n  if (tl.k === 'servicos' && s.draft?.key) { s.svcStats = null; const desde = new Date(Date.now() - 7 * 864e5).toISOString(); api(d2, { acao: 'listar', tabela: 'fps-service-runs', limite: 1000, filtro: { serviceKey: s.draft.key, createdAt: { $gte: desde }, $sort: { createdAt: -1 } } }).then((o) => { const rs = o.data || []; const ok = rs.filter((x) => x.status === 'sucesso'); s.svcStats = { total: rs.length, erros: rs.filter((x) => !['sucesso', 'desligado'].includes(x.status)).length, desligado: rs.filter((x) => x.status === 'desligado').length, mediaMs: ok.length ? Math.round(ok.reduce((a, x) => a + (x.duracaoMs || 0), 0) / ok.length) : null, ultima: rs[0]?.createdAt } }).catch(() => { s.svcStats = { total: 0 } }) }\n  if (tl.k === 'clientes' && s.draft?.customerId) { s.cliEv = null; api(d2, { acao: 'listar', tabela: 'fps-events', limite: 50, filtro: { customerId: s.draft.customerId, $sort: { createdAt: -1 } } }).then((o) => { s.cliEv = o.data || [] }).catch(() => { s.cliEv = [] }) }\n  if (tl.k === 'conversas' || tl.k === 'replay') { s.replay = null; mensagens(d2, s.draft.sessionId) }\n  if (tl.preview) requestAnimationFrame(() => mandarPreview(d2))\n}\n\nasync function mensagens(d2, sessionId) {\n  d2.state.msgs = null\n  const out = await api(d2, { acao: 'listar', tabela: 'fps-messages', limite: 200, filtro: { sessionId, $sort: { createdAt: 1 } } }).catch(() => ({ data: [] }))\n  d2.state.msgs = out.data || []\n}\n\nfunction mandarPreview(d2) {\n  const t = d2.state.draft\n  if (!t) return\n  const cfg = { productName: t.productName, theme: t.theme, logoUrl: t.logoUrl, imagens: t.imagens, sugestoesAbertura: t.sugestoesAbertura, copy: t.copy }\n  d2.$('iframe.pv')?.contentWindow?.postMessage({ type: 'fps:preview-config', cfg }, '*')\n}\n\nfunction lerCampo(d2, el) {\n  const s = d2.state\n  const k = el.dataset.f\n  const t = el.dataset.t\n  let v = el.value\n  if (t === 'bool') v = el.checked\n  else if (t === 'num') v = v === '' ? null : Number(v)\n  else if (t === 'date') v = v ? new Date(v).toISOString() : null\n  else if (t === 'lista') v = v.split('\\n').map((x) => x.trim()).filter(Boolean)\n  else if (t === 'json') {\n    try { v = JSON.parse(v || 'null') } catch (e) { s.erroCampo = k; return }\n  }\n  if (s.erroCampo === k) s.erroCampo = null\n  // Chaves com ponto editam um campo dentro de um objeto (copy.tryon.ajustar, theme.primary, valor.porResposta).\n  s.draft = k.includes('.') ? setPath(s.draft, k, v) : { ...s.draft, [k]: v }\n  s.dirty = true\n  if (tela(d2).preview) mandarPreview(d2)\n}\n\nasync function salvar(d2, modo) {\n  const s = d2.state\n  const tl = tela(d2)\n  if (s.erroCampo) return toast(d2, `JSON inválido em ${s.erroCampo}`)\n  let reg = { ...s.draft }\n  for (const k of SISTEMA) if (k !== '_id') delete reg[k]\n  if (modo === 'agendar') {\n    if (!s.agendaEm) return toast(d2, 'Escolha data e hora')\n    reg.status = 'agendado'\n    reg.publicarEm = new Date(s.agendaEm).toISOString()\n  } else if (tl.k === 'experimentos') {\n    if (reg.status === 'ativo' && s.rows.some((r) => r.status === 'ativo' && r._id !== reg._id)) return toast(d2, 'Já existe um experimento ativo. Encerre o outro antes.')\n  } else if (tl.k === 'servicos') {\n    // status de serviço é ativo | instavel | desligado, não rascunho/publicado: salvar grava o que está na tela.\n  } else if ('status' in reg || tl.chaveada || tl.versionada) reg.status = modo === 'publicar' ? 'publicado' : 'rascunho'\n  // Versionadas (persona, config): toda gravação vira versão nova; a publicada continua até a nova ser publicada.\n  if (tl.versionada) {\n    delete reg._id\n    reg.versao = Math.max(0, ...s.rows.map((r) => r.versao || 0)) + 1\n  }\n  if (tl.chaveada) reg = { chave: reg.chave, valor: reg.valor, status: reg.status, publicarEm: reg.publicarEm, nota: reg.nota }\n  s.saving = true\n  try {\n    const out = await api(d2, { acao: 'salvar', tabela: tl.tabela, registro: reg })\n    toast(d2, modo === 'publicar' ? 'Publicado' : modo === 'agendar' ? 'Agendado' : 'Rascunho salvo')\n    const novoId = out.registro?._id\n    await abrir(d2, tl.k)\n    if (novoId && s.rows.find((r) => r._id === novoId)) selecionar(d2, novoId)\n    else if (tl.chaveada) { const r = s.rows.find((x) => x.chave === reg.chave); if (r) selecionar(d2, r._id) }\n  } catch (e) {\n    toast(d2, `Não salvou: ${e.message}`)\n  } finally {\n    s.saving = false\n  }\n}\n\nasync function subir(d2, campo, file) {\n  try {\n    const out = await d2.drive(d2.params.assets).uploadFromFiles([file])\n    const url = out?.[0]?.fileUrl\n    if (!url) throw new Error('sem url')\n    d2.state.draft = { ...d2.state.draft, [campo]: url }\n    d2.state.dirty = true\n    if (tela(d2).preview) mandarPreview(d2)\n  } catch (e) {\n    toast(d2, 'Upload falhou')\n  }\n}\n\nfunction renderSide(d2) {\n  const grupos = [...new Set(TELAS.map((x) => x.g))]\n  return `\n    <aside class=\"side\">\n      <div class=\"brand\"><span class=\"eyebrow\">Fashion Personal Shopper</span><span class=\"display brand-t\">Gestão</span></div>\n      <nav>${grupos.map((g) => `\n        <p class=\"nav-g\">${g}</p>\n        ${TELAS.filter((x) => x.g === g).map((x) => `<button class=\"nav-i\" data-act=\"nav\" data-k=\"${x.k}\" aria-current=\"${d2.state.nav === x.k}\">${ic(d2, x.i, 16)}<span>${x.t}</span></button>`).join('')}`).join('')}\n      </nav>\n    </aside>`\n}\n\nfunction badge(st) {\n  if (!st) return ''\n  return `<span class=\"st st-${esc(st)}\">${esc(st)}</span>`\n}\n\nfunction renderList(d2) {\n  const s = d2.state\n  const tl = tela(d2)\n  const q = (s.q || '').toLowerCase()\n  const rows = s.rows.filter((r) => (s.verArquivados || r.status !== 'arquivado') && (!q || JSON.stringify(rotulo(r)).toLowerCase().includes(q)))\n  const temArq = s.rows.some((r) => r.status === 'arquivado')\n  return `\n    <div class=\"list\">\n      <div class=\"list-top\">\n        <input class=\"input input-sm\" data-act-input=\"busca\" placeholder=\"Buscar\" value=\"${esc(s.q || '')}\">\n        ${tl.ro || tl.chave ? '' : `<button class=\"btn btn-outline btn-sm\" data-act=\"novo\">${ic(d2, 'Plus', 14)}Novo</button>`}\n        ${temArq ? `<label class=\"chk\"><input type=\"checkbox\" data-act-input=\"arq\" ${s.verArquivados ? 'checked' : ''}>Mostrar arquivados</label>` : ''}\n        ${tl.k === 'execucoes' && s.exFiltro ? `<button class=\"btn btn-ghost btn-sm\" data-act=\"ex-limpar\">${ic(d2, 'X', 14)}${esc(s.exFiltro)}</button>` : ''}\n        ${tl.k === 'alertas' ? `<button class=\"btn btn-outline btn-sm\" data-act=\"al-verificar\" ${s.alBusy ? 'disabled' : ''}>${ic(d2, 'RefreshCw', 14)}${s.alBusy ? 'Verificando…' : 'Verificar agora'}</button>` : ''}\n        ${tl.k === 'kb' || tl.k === 'guia' ? `<button class=\"btn btn-outline btn-sm\" data-act=\"kb-imp\" title=\"Importar PDF, Word, Markdown ou Google Docs\">${ic(d2, 'FileUp', 14)}Importar</button>` : ''}\n      </div>\n      <div class=\"rows\">${s.loading ? '<div class=\"skeleton sk-row\"></div><div class=\"skeleton sk-row\"></div><div class=\"skeleton sk-row\"></div>' : rows.map((r) => `\n        <button class=\"row-i\" data-act=\"sel\" data-id=\"${r._id}\" aria-current=\"${s.sel === r._id}\">\n          ${r.url || r.capaUrl ? `<img src=\"${esc(r.url || r.capaUrl)}\" alt=\"\">` : ''}\n          <span class=\"row-t\">${esc(rotulo(r))}</span>\n          <span class=\"row-m\">${badge(r.status)}${r.versao ? `<span class=\"muted\">v${r.versao}</span>` : ''}${r.ativo === false ? '<span class=\"st\">inativo</span>' : ''}<span class=\"muted\">${quando(r.updatedAt || r.createdAt)}</span></span>\n        </button>`).join('') || '<p class=\"vazio\">Nada aqui ainda.</p>'}\n      </div>\n    </div>`\n}\n\nfunction renderCampo(d2, k, v, nivel = 0) {\n  if (nivel < 2 && ehPlano(v, nivel)) {\n    return `<fieldset class=\"grupo wide\"><legend class=\"lbl\">${esc(k.split('.').pop())}</legend><div class=\"campos\">${Object.entries(v).map(([sk, sv]) => renderCampo(d2, `${k}.${sk}`, sv, nivel + 1)).join('')}</div></fieldset>`\n  }\n  const t = tipoDe(v)\n  const inv = d2.state.erroCampo === k ? ' invalid' : ''\n  const ro = tela(d2).ro ? 'disabled' : ''\n  const img = /url$/i.test(k) && typeof v === 'string'\n  let input\n  if (t === 'bool') input = `<label class=\"sw\"><input type=\"checkbox\" data-f=\"${k}\" data-t=\"bool\" ${v ? 'checked' : ''} ${ro}><span></span></label>`\n  else if (t === 'num') input = `<input class=\"input\" type=\"number\" data-f=\"${k}\" data-t=\"num\" value=\"${v ?? ''}\" ${ro}>`\n  else if (t === 'lista') input = `<textarea class=\"input ta\" rows=\"${Math.min(8, Math.max(2, v.length + 1))}\" data-f=\"${k}\" data-t=\"lista\" ${ro}>${esc(v.join('\\n'))}</textarea><span class=\"hint\">uma por linha</span>`\n  else if (t === 'json') input = `<textarea class=\"input ta mono${inv}\" rows=\"${Math.min(16, JSON.stringify(v, null, 2).split('\\n').length + 1)}\" data-f=\"${k}\" data-t=\"json\" ${ro}>${esc(JSON.stringify(v, null, 2))}</textarea>`\n  else if (t === 'texto') input = `<textarea class=\"input ta\" rows=\"5\" data-f=\"${k}\" data-t=\"str\" ${ro}>${esc(v)}</textarea>`\n  else if (k === 'status') input = `<span>${badge(v)}</span>`\n  else if (ehISO(v) || (v === '' && /^(inicio|fim|publicarEm|ate|desde)$/i.test(k.split('.').pop()))) input = `<input class=\"input\" type=\"datetime-local\" data-f=\"${k}\" data-t=\"date\" value=\"${v ? localDT(v) : ''}\" ${ro}>`\n  else if (/^#[0-9a-f]{3,8}$/i.test(v)) input = `<span class=\"cor\"><input type=\"color\" data-f=\"${k}\" data-t=\"str\" value=\"${esc(v.length === 4 ? v.replace(/#(.)(.)(.)/, '#$1$1$2$2$3$3') : v.slice(0, 7))}\" ${ro}><input class=\"input\" data-f=\"${k}\" data-t=\"str\" value=\"${esc(v)}\" ${ro}></span>`\n  else input = `<input class=\"input\" data-f=\"${k}\" data-t=\"str\" value=\"${esc(v)}\" ${ro}>`\n  return `\n    <div class=\"campo${t === 'json' || t === 'texto' || t === 'lista' ? ' wide' : ''}\">\n      <label class=\"lbl\">${esc(k.split('.').pop())}</label>\n      ${input}\n      ${img ? `<div class=\"img-row\">${v ? `<img class=\"thumb\" src=\"${esc(v)}\" alt=\"\">` : ''}${ro ? '' : `<label class=\"btn btn-ghost btn-sm\">${ic(d2, 'Upload', 14)}Enviar imagem<input type=\"file\" accept=\"image/*\" class=\"up\" data-f=\"${k}\" hidden></label>`}</div>` : ''}\n    </div>`\n}\n\nfunction renderBar(d2) {\n  const s = d2.state\n  const tl = tela(d2)\n  if (tl.ro || tl.semBarra) return ''\n  const temStatus = s.draft && ('status' in s.draft || tl.chaveada || tl.versionada)\n  return `\n    <div class=\"bar\">\n      <div class=\"bar-l\">${s.dirty ? `<span class=\"dot\"></span><span>Rascunho não salvo</span>` : `<span class=\"muted\">${s.sel === 'novo' ? 'Novo registro' : 'Sem alterações'}</span>`}${s.draft?.status ? badge(s.draft.status) : ''}</div>\n      <div class=\"bar-r\">\n        ${s.dirty ? `<button class=\"btn btn-ghost btn-sm\" data-act=\"descartar\">Descartar</button>` : ''}\n        ${temStatus ? `<button class=\"btn btn-outline btn-sm\" data-act=\"rascunho\" ${s.saving ? 'disabled' : ''}>Salvar rascunho</button>\n        <span class=\"agenda\"><input class=\"input input-sm\" type=\"datetime-local\" data-act-input=\"agenda\" value=\"${esc(s.agendaEm || '')}\"><button class=\"btn btn-outline btn-sm\" data-act=\"agendar\" ${s.saving ? 'disabled' : ''}>${ic(d2, 'CalendarClock', 14)}Agendar</button></span>` : ''}\n        <button class=\"btn btn-primary btn-sm\" data-act=\"publicar\" ${s.saving ? 'disabled' : ''}>${ic(d2, 'Send', 14)}Publicar</button>\n      </div>\n    </div>`\n}\n\nconst brl = (n) => `R$ ${Number(n || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`\n\nfunction miniBloco(b) {\n  if (!b || typeof b !== 'object') return `<span class=\"st\">${esc(b)}</span>`\n  const tag = (x) => `<span class=\"st\">${esc(x)}</span>`\n  const img = (src) => (src ? `<img src=\"${esc(src)}\" alt=\"\">` : '')\n  if (b.type === 'occasion') return `<div class=\"blk\">${img(b.coverUrl)}<div>${tag('capa')}<b>${esc(b.title)}</b><span class=\"muted\">${esc((b.index || []).map((i) => i.title).join(' · '))}</span></div></div>`\n  if (b.type === 'look') return `<div class=\"blk\">${img(b.heroImage || b.pieces?.[0]?.image)}<div>${tag(b.origin || 'look')}<b>${esc(b.title)}</b><span class=\"muted\">${(b.pieces || []).length} peças · ${brl(b.total)}</span></div></div>`\n  if (b.type === 'tryon') return `<div class=\"blk\">${img(b.imageUrl)}<div>${tag('try-on')}<b>${esc(b.status)}</b><span class=\"muted\">${(b.etapas || []).map((e) => e.role).join(' → ')}</span></div></div>`\n  if (b.type === 'message') return `<div class=\"blk\"><div>${tag('texto')}<span>${esc(b.text)}</span></div></div>`\n  if (b.type === 'quickReplies') return `<div class=\"blk\"><div>${tag('sugestões')}<span class=\"muted\">${esc((b.options || []).map((o) => o.label).join(' · '))}</span></div></div>`\n  if (b.type === 'products') return `<div class=\"blk\"><div>${tag('produtos')}<span class=\"muted\">${(b.items || []).map((p) => esc(p.name)).join(' · ')}</span></div></div>`\n  if (b.type === 'alternatives') return `<div class=\"blk\"><div>${tag('alternativas')}<span class=\"muted\">${esc(b.role)} · ${(b.options || []).length} opções</span></div></div>`\n  if (b.type === 'sizeAdvisor') return `<div class=\"blk\"><div>${tag('tamanho')}<b>${esc(b.result?.size || 'pergunta medidas')}</b><span class=\"muted\">${esc(b.name || b.productId)}</span></div></div>`\n  if (b.type === 'styleQuiz') return `<div class=\"blk\">${img(b.cards?.[0]?.image)}<div>${tag(`quiz ${b.step}/${b.total}`)}<span class=\"muted\">${esc(b.pergunta || '')} · ${(b.cards || []).length} cartões</span></div></div>`\n  if (b.type === 'compareTable') return `<div class=\"blk\"><div>${tag('comparar')}<span class=\"muted\">${(b.products || []).map((p) => esc(p.name)).join(' × ')}</span></div></div>`\n  if (b.type === 'capsuleGrid') return `<div class=\"blk\">${img(b.pieces?.[0]?.packshot)}<div>${tag('cápsula')}<b>${brl(b.budget)}</b><span class=\"muted\">${(b.pieces || []).length} peças · ${(b.combinations || []).length} combinações</span></div></div>`\n  if (b.type === 'stockAlert') return `<div class=\"blk\">${img(b.image)}<div>${tag('estoque')}<b>${esc(b.size)} · ${esc(b.status)}</b><span class=\"muted\">${esc(b.name || b.productId)}</span></div></div>`\n  if (b.type === 'closet') return `<div class=\"blk\">${img(b.items?.[0]?.image)}<div>${tag('closet')}<span class=\"muted\">${(b.items || []).length} peça(s)</span></div></div>`\n  if (b.type === 'questions') return `<div class=\"blk\"><div>${tag('perguntas')}<span class=\"muted\">${esc((b.perguntas || []).map((p) => p.pergunta).join(' · '))}</span></div></div>`\n  return tag(b.type)\n}\n\n// Turno disparado por toque na tela: grava \"[evento x no bloco y] {json}\"; mostra o nome da ação e o essencial do payload.\nconst EVENTOS = { tryon: 'Ver em mim', swap: 'Trocar peça', answers: 'Respondeu as perguntas', anchor: 'Montar look com a peça', quiz: 'Resposta do quiz', quiz_start: 'Começou o quiz', capsule_looks: 'Ver os looks da cápsula', closet_look: 'Look com o closet', tryon_ajuste: 'Ajuste da prévia', size: 'Medidas', stock_alert: 'Aviso de estoque' }\nfunction eventoDe(m) {\n  if (m.evento?.event) return m.evento\n  const t = m.texto || ''\n  const mm = t.match(/^\\[evento (\\S+) no bloco [^\\]]*\\]\\s*([\\s\\S]*)$/)\n  if (!mm) return null\n  let payload = null\n  try { payload = JSON.parse(mm[2]) } catch (e) {}\n  return { event: mm[1], payload }\n}\nfunction textoMsg(d2, m) {\n  const ev = eventoDe(m)\n  if (!ev) return `<p>${esc(m.texto || m.text || m.mensagem || m.legenda || '')}</p>`\n  const p = ev.payload || {}\n  const pecas = (p.pieces || []).map((x) => x.role || x.name).filter(Boolean)\n  const resp = p.respostas && !Array.isArray(p.respostas) ? Object.values(p.respostas).join(' · ') : ''\n  const detalhe = [p.label, p.pedido, p.name, p.role && !pecas.length ? p.role : '', pecas.join(', '), resp, p.soCores ? 'só as cores' : '', p.size ? `tamanho ${p.size}` : ''].filter(Boolean).join(' · ')\n  return `<p class=\"ev-l\">${ic(d2, 'MousePointerClick', 14)}<b>${esc(EVENTOS[ev.event] || ev.event)}</b>${detalhe ? ` · ${esc(detalhe)}` : ''}</p>\n    <details class=\"ev-d\"><summary class=\"hint\">dados</summary><pre class=\"mono\">${esc(JSON.stringify(p, null, 2))}</pre></details>`\n}\n\nfunction renderMsgs(d2) {\n  const ms = d2.state.msgs\n  if (!ms) return '<div class=\"skeleton sk-row\"></div>'\n  if (!ms.length) return '<p class=\"vazio\">Sem mensagens.</p>'\n  return `<div class=\"msgs\">${ms.map((m) => {\n    const eu = m.papel === 'cliente' || m.papel === 'usuario' || m.role === 'user'\n    const blocos = m.blocos || m.blocks || []\n    return `<div class=\"msg ${eu ? 'eu' : ''}\">\n      <span class=\"eyebrow\">${eu ? 'Cliente' : 'Stylist'} · ${quando(m.createdAt)}${m.latenciaMs ? ` · ${(m.latenciaMs / 1000).toFixed(1)}s` : ''}</span>\n      ${textoMsg(d2, m)}\n      ${blocos.length ? `<div class=\"blks\">${blocos.map(miniBloco).join('')}</div>` : ''}\n    </div>`\n  }).join('')}</div>`\n}\n\n// Ocasiões: campos simples pelo editor genérico; perguntas como lista estruturada e capa escolhida do banco de imagens.\nfunction renderPerguntas(d2) {\n  const qs = d2.state.draft.perguntas || []\n  return `\n    <div class=\"campo wide\">\n      <label class=\"lbl\">perguntas <span class=\"hint\">feitas antes de montar os looks. Opções uma por linha; depois de | entram flags: geo (usar a localização do browser), precoMax=300</span></label>\n      <div class=\"qs\">${qs.map((q, i) => `\n        <div class=\"qcard\">\n          <div class=\"qrow\">\n            <input class=\"input input-sm qk\" data-q=\"${i}\" data-qf=\"chave\" placeholder=\"chave\" value=\"${esc(q.chave || '')}\">\n            <input class=\"input input-sm grow\" data-q=\"${i}\" data-qf=\"pergunta\" placeholder=\"Pergunta\" value=\"${esc(q.pergunta || '')}\">\n            <label class=\"sw\" title=\"obrigatória\"><input type=\"checkbox\" data-q=\"${i}\" data-qf=\"obrigatoria\" ${q.obrigatoria ? 'checked' : ''}><span></span></label>\n            <div class=\"seg\"><button type=\"button\" data-act=\"q-tipo\" data-i=\"${i}\" data-v=\"opcoes\" aria-pressed=\"${!q.livre}\">Opções</button><button type=\"button\" data-act=\"q-tipo\" data-i=\"${i}\" data-v=\"livre\" aria-pressed=\"${!!q.livre}\">Livre</button></div>\n            <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"q-up\" data-i=\"${i}\" ${i ? '' : 'disabled'}>${ic(d2, 'ArrowUp', 14)}</button>\n            <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"q-del\" data-i=\"${i}\">${ic(d2, 'Trash2', 14)}</button>\n          </div>\n          ${q.livre ? `<input class=\"input input-sm\" data-q=\"${i}\" data-qf=\"placeholder\" placeholder=\"Placeholder do campo livre\" value=\"${esc(q.placeholder || '')}\">`\n            : `<textarea class=\"input ta\" rows=\"${Math.min(8, Math.max(2, (q.opcoes || []).length + 1))}\" data-q=\"${i}\" data-qf=\"opcoes\" placeholder=\"Uma opção por linha\">${esc((q.opcoes || []).map(opcaoTxt).join('\\n'))}</textarea>`}\n        </div>`).join('') || '<p class=\"hint\">Sem perguntas: os looks saem direto.</p>'}</div>\n      <div><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"q-add\">${ic(d2, 'Plus', 14)}Pergunta</button></div>\n    </div>`\n}\n\nfunction renderOcasiao(d2) {\n  const s = d2.state\n  const campos = Object.entries(s.draft).filter(([k]) => !SISTEMA.includes(k) && k !== 'perguntas')\n  const capas = s.capas || []\n  return `<div class=\"campos\">\n    ${campos.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n    ${capas.length ? `<div class=\"campo wide\"><label class=\"lbl\">capa do banco de imagens <span class=\"hint\">clique pra usar como capaUrl</span></label><div class=\"pick\">${capas.map((i) => `<img src=\"${esc(i.url)}\" alt=\"\" title=\"${esc(i.titulo || i.nome || '')}\" data-act=\"capa\" data-url=\"${esc(i.url)}\" aria-pressed=\"${s.draft.capaUrl === i.url}\">`).join('')}</div></div>` : ''}\n    ${renderPerguntas(d2)}\n  </div>`\n}\n\nfunction mudarPergunta(d2, i, f, v) {\n  const s = d2.state\n  const qs = [...(s.draft.perguntas || [])]\n  qs[i] = { ...qs[i], [f]: v }\n  s.draft = { ...s.draft, perguntas: qs }\n  s.dirty = true\n}\n\n// Tela de chave única da fps-config (Looks): cada campo do valor vira um controle; objetos aninhados seguem como JSON.\nfunction renderConfigChave(d2) {\n  const v = d2.state.draft.valor || {}\n  return `${tela(d2).chave === 'model' ? renderCustoModelo(d2) : ''}<div class=\"campos\">\n    ${Object.entries(v).map(([k, x]) => renderCampo(d2, `valor.${k}`, x)).join('')}\n    ${renderCampo(d2, 'nota', d2.state.draft.nota || '')}\n  </div>`\n}\n\n// Blocos: o exemplo tipado é conferido contra os obrigatórios e desenhado como o admin desenha nas Conversas.\nfunction renderBlocoExtra(d2) {\n  const b = d2.state.draft\n  const ex = b.exemplo && typeof b.exemplo === 'object' ? b.exemplo : null\n  const faltam = (b.obrigatorios || []).filter((k) => !ex || ex[k] == null || ex[k] === '')\n  return `<div class=\"campo wide\"><label class=\"lbl\">prévia do exemplo</label>\n    ${ex ? `<div class=\"blks\">${miniBloco(ex)}</div>` : '<p class=\"hint\">Exemplo inválido ou vazio.</p>'}\n    ${faltam.length ? `<p class=\"hint invalid-txt\">Faltam no exemplo os obrigatórios: ${esc(faltam.join(', '))}</p>` : '<p class=\"hint ok-txt\">Exemplo cobre todos os obrigatórios.</p>'}\n  </div>`\n}\n\n// Looks curados: editor próprio. Peças como lista editável (adicionar da busca, papel, ordem, herói, remover), busca com filtros e a IA como auxílio: texto livre pra montar do zero ou completar o que já está na lista.\nconst PAPEIS = ['Camisa', 'Camiseta', 'Polo', 'Calça', 'Bermuda', 'Paletó', 'Blazer', 'Malha', 'Jaqueta', 'Mocassim', 'Tênis', 'Sapato', 'Bota', 'Cinto', 'Acessório']\nconst pecaDe = (p, papel) => ({ productId: p.productId, papel: papel || p.role || PAPEIS.find((x) => (p.name || '').toLowerCase().startsWith(x.toLowerCase())) || (p.name || '').split(' ')[0], name: p.name, price: p.price, image: p.packshot || p.image, heroImage: p.heroImage || p.image })\n\nasync function buscarPecas(d2) {\n  const s = d2.state\n  const q = (s.bq || '').trim() || s.bCat || ''\n  if (!q) return toast(d2, 'Digite um termo ou escolha um tipo')\n  s.bBusca = null\n  s.bBuscando = true\n  try {\n    const r = await d2.rush(d2.params.busca || 'fps-tool-search').invoke({ q, categoria: s.bCat || undefined, precoMax: s.bPreco || undefined, limite: 12, dryRun: true })\n    s.bBusca = (r?.data || r)?.pieces || []\n  } catch (e) {\n    toast(d2, `Busca falhou: ${e.message}`)\n    s.bBusca = []\n  } finally {\n    s.bBuscando = false\n  }\n}\n\n// modo: 'montar' (do zero, a partir do texto livre), 'completar' (mantém a lista e pede o que falta) ou 'ancora' (a partir de uma peça da busca).\nasync function montarComIA(d2, modo, productId) {\n  const s = d2.state\n  const pecas = s.draft.pecas || []\n  const texto = (s.bTexto || '').trim()\n  const anc = modo === 'ancora' ? (s.bBusca || []).find((p) => p.productId === productId) : null\n  const ancoraId = modo === 'ancora' ? productId : modo === 'completar' ? pecas[0]?.productId : undefined\n  if (modo === 'completar' && !pecas.length) return toast(d2, 'Adicione ao menos uma peça pra completar')\n  const jaTem = pecas.length ? ` Já escolhidas e que devem ficar: ${pecas.map((p) => `${p.papel} ${p.name || p.productId}`).join(', ')}. Complete só o que falta.` : ''\n  const pedido = modo === 'montar' ? (texto || s.draft.titulo || 'um look completo') : modo === 'completar' ? `${texto || s.draft.titulo || 'um look completo'}.${jaTem}` : `um look completo com a peça ${anc?.name || ''}${texto ? `. ${texto}` : ''}`\n  s.bMontando = true\n  try {\n    const r = await d2.rush(d2.params.compose || 'fps-tool-compose').invoke({ pedido, pecaAncora: ancoraId, ocasiao: (s.draft.ocasioes || [])[0], looks: 1, dryRun: true })\n    const look = ((r?.data || r)?.looks || [])[0]\n    if (!look) throw new Error('o motor não devolveu look')\n    const novas = look.pieces.map((p) => pecaDe(p))\n    const lista = modo === 'completar' ? [...pecas, ...novas.filter((n) => !pecas.some((p) => p.productId === n.productId || p.papel === n.papel))] : novas\n    const manter = modo === 'completar'\n    s.draft = { ...s.draft, pecas: lista,\n      titulo: manter && s.draft.titulo ? s.draft.titulo : look.title || s.draft.titulo,\n      legenda: manter && s.draft.legenda ? s.draft.legenda : look.caption || s.draft.legenda,\n      heroImage: s.draft.heroImage || look.heroImage || lista[0]?.heroImage || '' }\n    s.dirty = true\n    toast(d2, modo === 'completar' ? `Completado: ${lista.length} peças` : `Look montado com ${lista.length} peças`)\n  } catch (e) {\n    toast(d2, `Não montou: ${e.message}`)\n  } finally {\n    s.bMontando = false\n  }\n}\n\nfunction mudarPeca(d2, i, f, v) {\n  const s = d2.state\n  const ps = [...(s.draft.pecas || [])]\n  ps[i] = { ...ps[i], [f]: v }\n  s.draft = { ...s.draft, pecas: ps }\n  s.dirty = true\n}\n\nfunction renderLookCurado(d2) {\n  const s = d2.state\n  const d = s.draft\n  const pecas = d.pecas || []\n  const total = pecas.reduce((a, p) => a + (Number(p.price) || 0), 0)\n  const outros = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['titulo', 'legenda', 'heroImage', 'pecas', 'ocasioes', 'status'].includes(k))\n  const busy = s.bMontando ? 'disabled' : ''\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\">\n        <div class=\"campos\">\n          ${renderCampo(d2, 'titulo', d.titulo || '')}\n          ${renderCampo(d2, 'ocasioes', d.ocasioes || [])}\n          <div class=\"campo wide\"><label class=\"lbl\">legenda</label><textarea class=\"input ta\" rows=\"3\" data-f=\"legenda\" data-t=\"str\">${esc(d.legenda || '')}</textarea></div>\n          ${renderCampo(d2, 'heroImage', d.heroImage || '')}\n          ${outros.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n        </div>\n      </section>\n\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Peças do look</h3><span class=\"muted\">${pecas.length} peças · ${brl(total)}</span></div>\n        ${pecas.length ? `<div class=\"ptable\">${pecas.map((p, i) => `\n          <div class=\"prow\" data-key=\"pc-${p.productId}-${i}\">\n            ${p.image ? `<img src=\"${esc(p.image)}\" alt=\"\">` : '<span class=\"pimg\"></span>'}\n            <input class=\"input input-sm ppapel\" list=\"papeis\" data-p=\"${i}\" data-pf=\"papel\" value=\"${esc(p.papel || '')}\" placeholder=\"papel\">\n            <div class=\"pinfo\"><span>${esc(p.name || p.productId)}</span><span class=\"muted\">${p.productId}${p.price != null ? ` · ${brl(p.price)}` : ''}</span></div>\n            <div class=\"pacts\">\n              <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"peca-hero\" data-i=\"${i}\" title=\"Usar como foto herói\" aria-pressed=\"${d.heroImage && d.heroImage === (p.heroImage || p.image)}\">${ic(d2, 'Star', 14)}</button>\n              <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"peca-up\" data-i=\"${i}\" ${i ? '' : 'disabled'}>${ic(d2, 'ArrowUp', 14)}</button>\n              <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"peca-del\" data-i=\"${i}\">${ic(d2, 'Trash2', 14)}</button>\n            </div>\n          </div>`).join('')}</div>` : '<p class=\"vazio\">Nenhuma peça ainda. Busque abaixo e adicione, ou peça pra IA montar.</p>'}\n        <datalist id=\"papeis\">${PAPEIS.map((x) => `<option value=\"${x}\">`).join('')}</datalist>\n      </section>\n\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Adicionar peças do catálogo</h3></div>\n        <form class=\"filtros\" data-form=\"busca\">\n          <input class=\"input input-sm grow\" data-bf=\"q\" placeholder=\"Nome, tecido, cor… ex.: camisa linho branca\" value=\"${esc(s.bq || '')}\">\n          <select class=\"input input-sm\" data-bf=\"cat\"><option value=\"\">Todos os tipos</option>${PAPEIS.map((x) => `<option value=\"${x}\" ${s.bCat === x ? 'selected' : ''}>${x}</option>`).join('')}</select>\n          <input class=\"input input-sm preco\" type=\"number\" data-bf=\"preco\" placeholder=\"Até R$\" value=\"${s.bPreco || ''}\">\n          <button class=\"btn btn-outline btn-sm\" ${s.bBuscando ? 'disabled' : ''}>${ic(d2, 'Search', 14)}Buscar</button>\n        </form>\n        ${s.bBuscando ? '<div class=\"skeleton sk-row\"></div>' : s.bBusca ? `<div class=\"bgrid\">${s.bBusca.map((p) => {\n          const ja = pecas.some((x) => x.productId === p.productId)\n          return `<div class=\"bitem\"><img src=\"${esc(p.packshot || p.image)}\" alt=\"\"><span class=\"bnome\">${esc(p.name)}</span><span class=\"muted\">${brl(p.price)}</span>\n            <div class=\"bacts\"><button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"peca-add\" data-id=\"${p.productId}\" ${ja ? 'disabled' : ''}>${ic(d2, 'Plus', 14)}${ja ? 'No look' : 'Adicionar'}</button><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"ancora\" data-id=\"${p.productId}\" title=\"A IA monta o look em volta desta peça\" ${busy}>${ic(d2, 'Sparkles', 14)}</button></div></div>`\n        }).join('') || '<p class=\"vazio\">Nada encontrado com esses filtros.</p>'}</div>` : '<p class=\"hint\">Busque por nome, tipo ou preço e adicione ao look. O ícone de brilho pede pra IA montar o look em volta daquela peça.</p>'}\n      </section>\n\n      <section class=\"sec ia\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Sparkles', 16)} Ajuda da IA</h3><span class=\"muted\">mesmo motor do stylist: respeita merchandising e o tom da marca</span></div>\n        <textarea class=\"input ta\" rows=\"2\" data-bt=\"1\" placeholder=\"Descreva o look em texto livre. Ex.: casamento na praia de dia, linho, tons claros, até R$ 2.500\">${esc(s.bTexto || '')}</textarea>\n        <div class=\"row\">\n          <button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"ia-montar\" ${busy}>${ic(d2, 'Wand2', 14)}${s.bMontando ? 'Montando…' : 'Montar do zero'}</button>\n          <button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"ia-completar\" ${busy || (pecas.length ? '' : 'disabled')}>${ic(d2, 'PlusCircle', 14)}Completar o que falta</button>\n          <span class=\"hint\">Montar do zero substitui as peças, título e legenda. Completar mantém o que está na lista e só preenche o que faltar; título e legenda só se estiverem vazios.</span>\n        </div>\n      </section>\n    </div>`\n}\n\n// Campanhas: período com data e hora, onde aparece como chips e os looks escolhidos entre os curados (ordem = ordem de clique).\nfunction renderCampanha(d2) {\n  const s = d2.state\n  const d = s.draft\n  const ids = d.lookIds || []\n  const onde = d.onde || []\n  const todos = s.looksCurados || []\n  const campos = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['lookIds', 'onde'].includes(k))\n  const card = (l, i) => `<button type=\"button\" class=\"lkc\" data-act=\"camp-look\" data-id=\"${l._id}\" aria-pressed=\"${i >= 0}\">${l.heroImage ? `<img src=\"${esc(l.heroImage)}\" alt=\"\">` : '<span class=\"pimg\"></span>'}<span class=\"lkc-t\">${esc(l.titulo || l._id)}</span><span class=\"row-m\">${badge(l.status)}<span class=\"muted\">${(l.pecas || []).length} peças</span></span>${i >= 0 ? `<i class=\"ord\">${i + 1}</i>` : ''}</button>`\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">${campos.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n        <div class=\"campo\"><label class=\"lbl\">onde aparece</label><div class=\"opts\">${ONDE.map(([v, t]) => `<button type=\"button\" class=\"chip\" data-act=\"camp-onde\" data-v=\"${v}\" aria-pressed=\"${onde.includes(v)}\">${t}</button>`).join('')}</div></div>\n      </div></section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Looks da campanha</h3><span class=\"muted\">${ids.length} escolhidos · clique pra incluir ou tirar; a ordem é a de clique</span></div>\n        ${!s.looksCurados ? '<div class=\"skeleton sk-row\"></div>' : todos.length ? `<div class=\"lkgrid\">${todos.map((l) => card(l, ids.indexOf(l._id))).join('')}</div>` : '<p class=\"vazio\">Nenhum look curado ainda. Crie em Looks curados.</p>'}\n        ${ids.filter((id) => !todos.some((l) => l._id === id)).length ? `<p class=\"hint invalid-txt\">Ids sem look correspondente: ${esc(ids.filter((id) => !todos.some((l) => l._id === id)).join(', '))}</p>` : ''}\n      </section>\n    </div>`\n}\n\n// Base de conhecimento: artigo com apoio da IA (variantes; resposta só a partir de texto-fonte colado) e teste da pergunta contra a base.\nasync function kbIA(d2, acao) {\n  const s = d2.state\n  const d = s.draft\n  if (!d.pergunta) return toast(d2, 'Escreva a pergunta principal antes')\n  if (acao === 'resposta' && (s.kbFonte || '').trim().length < 20) return toast(d2, 'Cole o texto-fonte (política, página do site) pra IA usar')\n  s.kbBusy = acao\n  try {\n    const r = await d2.rush(d2.params.kbIa || 'fps-kb-ia').invoke({ acao, pergunta: d.pergunta, fonte: s.kbFonte })\n    const o = r?.data || r\n    if (o?.erro) throw new Error(o.erro)\n    if (acao === 'variantes') {\n      const atuais = d.variantes || []\n      const novas = (o.variantes || []).filter((v) => !atuais.some((x) => x.toLowerCase() === v.toLowerCase()))\n      s.draft = { ...d, variantes: [...atuais, ...novas] }\n      toast(d2, `${novas.length} variantes sugeridas; revise e tire o que não servir`)\n    } else {\n      s.draft = { ...d, resposta: o.resposta || d.resposta, titulo: d.titulo || o.titulo }\n      s.kbFalta = o.falta || null\n      toast(d2, 'Resposta escrita a partir do texto-fonte; confira antes de publicar')\n    }\n    s.dirty = true\n  } catch (e) {\n    toast(d2, `IA falhou: ${e.message}`)\n  } finally {\n    s.kbBusy = null\n  }\n}\n\nasync function kbTestar(d2) {\n  const s = d2.state\n  const q = (s.kbTeste || '').trim()\n  if (!q) return\n  s.kbRes = null\n  try {\n    const r = await d2.rush(d2.params.faq || 'fps-tool-faq').invoke({ pergunta: q, dryRun: true })\n    s.kbRes = r?.data || r\n  } catch (e) {\n    toast(d2, `Teste falhou: ${e.message}`)\n  }\n}\n\nfunction renderArtigo(d2) {\n  const s = d2.state\n  const d = s.draft\n  const outros = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['titulo', 'categoria', 'fonte', 'pergunta', 'variantes', 'resposta', 'status', 'versao'].includes(k))\n  const res = s.kbRes\n  const pct = (x) => (x == null ? '–' : `${Math.round(x * 100)}%`)\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">\n        ${renderCampo(d2, 'titulo', d.titulo || '')}\n        ${renderCampo(d2, 'categoria', d.categoria || '')}\n        <div class=\"campo wide\"><label class=\"lbl\">pergunta principal</label><input class=\"input\" data-f=\"pergunta\" data-t=\"str\" value=\"${esc(d.pergunta || '')}\"></div>\n        <div class=\"campo wide\"><label class=\"lbl\">outras formas de perguntar <span class=\"hint\">uma por linha; quanto mais jeitos, mais a busca acerta</span></label>\n          <textarea class=\"input ta\" rows=\"${Math.min(10, Math.max(3, (d.variantes || []).length + 1))}\" data-f=\"variantes\" data-t=\"lista\">${esc((d.variantes || []).join('\\n'))}</textarea>\n          <div><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"kb-var\" ${s.kbBusy ? 'disabled' : ''}>${ic(d2, 'Sparkles', 14)}${s.kbBusy === 'variantes' ? 'Sugerindo…' : 'Sugerir variantes com IA'}</button></div></div>\n        <div class=\"campo wide\"><label class=\"lbl\">resposta <span class=\"hint\">o que o stylist responde, com o artigo como fonte</span></label><textarea class=\"input ta\" rows=\"5\" data-f=\"resposta\" data-t=\"str\">${esc(d.resposta || '')}</textarea></div>\n        ${renderCampo(d2, 'fonte', d.fonte || '')}\n        ${outros.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n      </div></section>\n\n      <section class=\"sec ia\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Sparkles', 16)} Escrever a resposta com IA</h3><span class=\"muted\">só usa o texto colado; não inventa prazo nem condição</span></div>\n        <textarea class=\"input ta\" rows=\"4\" data-kb=\"fonte\" placeholder=\"Cole aqui a política ou o trecho da página do site que responde a pergunta\">${esc(s.kbFonte || '')}</textarea>\n        <div class=\"row\"><button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"kb-resp\" ${s.kbBusy ? 'disabled' : ''}>${ic(d2, 'Wand2', 14)}${s.kbBusy === 'resposta' ? 'Escrevendo…' : 'Escrever resposta'}</button>${s.kbFalta ? `<span class=\"hint invalid-txt\">O texto não cobre: ${esc(s.kbFalta)}</span>` : ''}</div>\n      </section>\n\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Testar uma pergunta</h3><span class=\"muted\">mostra qual artigo o stylist usaria hoje (só publicados respondem; rascunhos aparecem como candidatos)</span></div>\n        <form class=\"filtros\" data-form=\"kbteste\"><input class=\"input input-sm grow\" data-kb=\"teste\" placeholder=\"Ex.: dá pra trocar na loja?\" value=\"${esc(s.kbTeste || '')}\"><button class=\"btn btn-outline btn-sm\">${ic(d2, 'Search', 14)}Testar</button></form>\n        ${res ? `<div class=\"kbres\">\n          ${res.semResposta ? `<p class=\"invalid-txt\">Sem resposta: o mais próximo publicado ficou em ${pct(res.melhorScore)}, abaixo do limiar ${pct(res.limiar)}.</p>` : `<p class=\"ok-txt\">Responde com <b>${esc(res.artigo?.titulo)}</b> (${pct(res.score)}, limiar ${pct(res.limiar)}).</p><p class=\"caption\">${esc(res.resposta)}</p>`}\n          ${(res.candidatos || []).length ? `<div class=\"ev-l\">${res.candidatos.map((c) => `<div class=\"ev\"><span>${esc(c.titulo)} ${badge(c.status)}${c.id === d._id ? ' <span class=\"st\">este artigo</span>' : ''}</span><span>${pct(c.score)}</span></div>`).join('')}</div>` : ''}\n        </div>` : ''}\n      </section>\n    </div>`\n}\n\n// Importar documentos na base: o texto sai no browser (md/txt direto, PDF com pdf.js, Word com mammoth), link passa pelo fps-kb-fonte-url (só domínios da config), a IA quebra em artigos rascunho.\nconst PDFJS = { url: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js', global: 'pdfjsLib' }\nconst PDFJS_WORKER = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js'\nconst MAMMOTH = { url: 'https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js', global: 'mammoth' }\n\nasync function libPronta(d2, spec) {\n  for (let i = 0; i < 100; i++) {\n    const m = d2.lib(spec)\n    if (m) return m\n    if (d2.lib.state(spec) === 'error') throw new Error(`não carregou a biblioteca: ${d2.lib.error(spec)}`)\n    await new Promise((r) => setTimeout(r, 200))\n  }\n  throw new Error('biblioteca demorou demais pra carregar')\n}\n\nasync function textoDoArquivo(d2, file) {\n  const ext = (file.name.split('.').pop() || '').toLowerCase()\n  if (['md', 'markdown', 'txt', 'text'].includes(ext)) return { tipo: ext === 'txt' || ext === 'text' ? 'txt' : 'md', texto: await file.text() }\n  if (ext === 'pdf') {\n    const pdfjs = await libPronta(d2, PDFJS)\n    pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER\n    const doc = await pdfjs.getDocument({ data: new Uint8Array(await file.arrayBuffer()) }).promise\n    const paginas = []\n    for (let p = 1; p <= doc.numPages; p++) {\n      const c = await (await doc.getPage(p)).getTextContent()\n      paginas.push(c.items.map((it) => it.str + (it.hasEOL ? '\\n' : ' ')).join(''))\n    }\n    return { tipo: 'pdf', texto: paginas.join('\\n\\n') }\n  }\n  if (ext === 'docx') {\n    const mammoth = await libPronta(d2, MAMMOTH)\n    const r = await mammoth.extractRawText({ arrayBuffer: await file.arrayBuffer() })\n    return { tipo: 'docx', texto: r.value }\n  }\n  throw new Error(`formato .${ext} não suportado: use PDF, Word (.docx), Markdown ou texto; Google Docs pelo link`)\n}\n\nfunction textoDoHtml(html) {\n  const doc = new DOMParser().parseFromString(html, 'text/html')\n  doc.querySelectorAll('script,style,noscript,svg,nav,header,footer,iframe,form').forEach((n) => n.remove())\n  const main = doc.querySelector('main,article,[role=main]') || doc.body\n  return (main?.innerText || main?.textContent || '').replace(/[ \\t]+/g, ' ').replace(/\\n\\s*\\n\\s*\\n+/g, '\\n\\n').trim()\n}\n\n// Trechos por parágrafo, até max caracteres cada, pra caber num consult.\nfunction trechos(texto, max) {\n  const ps = texto.split(/\\n\\s*\\n/).map((x) => x.trim()).filter(Boolean)\n  const out = []\n  let cur = ''\n  for (const p of ps) {\n    if (cur && cur.length + p.length + 2 > max) { out.push(cur); cur = '' }\n    if (p.length > max) { for (let i = 0; i < p.length; i += max) out.push(p.slice(i, i + max)); continue }\n    cur = cur ? `${cur}\\n\\n${p}` : p\n  }\n  if (cur) out.push(cur)\n  return out\n}\n\nasync function importar(d2, origem) {\n  const s = d2.state\n  const imp = (patch) => { s.kbImp = { ...s.kbImp, ...patch } }\n  imp({ rodando: true, erro: null, etapa: 'Lendo o documento…', criados: [], trecho: 0, total: 0 })\n  let fonteId = null\n  try {\n    let nome, tipo, texto, url = null, arquivoUrl = null\n    if (origem.file) {\n      nome = origem.file.name\n      ;({ tipo, texto } = await textoDoArquivo(d2, origem.file))\n      try { const up = await d2.drive(d2.params.assets).uploadFromFiles([origem.file]); arquivoUrl = up?.[0]?.fileUrl || null } catch (e) {}\n    } else {\n      url = origem.url.trim()\n      const r = await d2.rush(d2.params.kbFonte || 'fps-kb-fonte-url').invoke({ url })\n      const o = r?.data || r\n      if (o?.erro) throw new Error(o.erro)\n      tipo = o.tipo\n      texto = o.html ? textoDoHtml(o.texto) : o.texto\n      nome = url\n    }\n    texto = (texto || '').trim()\n    if (texto.length < 40) throw new Error('não achei texto no documento (PDF escaneado como imagem não tem texto pra ler)')\n    const cfg = await api(d2, { acao: 'listar', tabela: 'fps-config', limite: 20, filtro: { chave: 'kb', status: 'publicado', $sort: { versao: -1 } } }).then((o) => o.data?.[0]?.valor || {}).catch(() => ({}))\n    const partes = trechos(texto, cfg.maxCharsPorTrecho || 12000)\n    const fonte = await api(d2, { acao: 'salvar', tabela: 'fps-kb-sources', registro: { nome, tipo, url, arquivoUrl, chars: texto.length, artigos: 0, artigoIds: [], status: 'processando' } })\n    fonteId = fonte.registro?._id\n    const existentes = await api(d2, { acao: 'listar', tabela: 'fps-kb-articles', limite: 300 }).then((o) => (o.data || []).map((a) => a.titulo).filter(Boolean)).catch(() => [])\n    imp({ total: partes.length, chars: texto.length, nome })\n    const criados = []\n    for (let i = 0; i < partes.length; i++) {\n      imp({ trecho: i + 1, etapa: `IA lendo o trecho ${i + 1} de ${partes.length}…` })\n      const r = await d2.rush(d2.params.kbIa || 'fps-kb-ia').invoke({ acao: 'importar', nome, fonte: partes[i], existentes: [...existentes, ...criados.map((c) => c.titulo)] })\n      const o = r?.data || r\n      for (const a of o?.artigos || []) {\n        if (!a?.pergunta || !a?.resposta) continue\n        const sv = await api(d2, { acao: 'salvar', tabela: 'fps-kb-articles', registro: { titulo: a.titulo || '', categoria: a.categoria || '', pergunta: a.pergunta, variantes: a.variantes || [], resposta: a.resposta, fonte: url || arquivoUrl || nome, status: 'rascunho', versao: 1 } })\n        if (sv.registro?._id) criados.push({ id: sv.registro._id, titulo: a.titulo || a.pergunta, categoria: a.categoria })\n      }\n      imp({ criados: [...criados] })\n    }\n    await api(d2, { acao: 'salvar', tabela: 'fps-kb-sources', registro: { _id: fonteId, status: 'ok', artigos: criados.length, artigoIds: criados.map((c) => c.id) } })\n    imp({ rodando: false, etapa: criados.length ? `${criados.length} artigos criados como rascunho a partir de “${nome}”` : 'A IA não achou nada que vire artigo pra cliente nesse documento' })\n    const rows = await api(d2, { acao: 'listar', tabela: 'fps-kb-articles', limite: 300, filtro: { $sort: { createdAt: -1 } } }).catch(() => null)\n    if (rows && tela(d2).k === 'kb') s.rows = rows.data || s.rows\n    carregarFontes(d2)\n  } catch (e) {\n    if (fonteId) api(d2, { acao: 'salvar', tabela: 'fps-kb-sources', registro: { _id: fonteId, status: 'erro', erro: String(e.message || e).slice(0, 300) } }).catch(() => {})\n    imp({ rodando: false, erro: e.message || String(e), etapa: null })\n  }\n}\n\nfunction carregarFontes(d2) {\n  api(d2, { acao: 'listar', tabela: 'fps-kb-sources', limite: 30, filtro: { $sort: { createdAt: -1 } } }).then((o) => { d2.state.kbFontes = o.data || [] }).catch(() => { d2.state.kbFontes = [] })\n}\n\nfunction renderImportar(d2) {\n  const s = d2.state\n  const imp = s.kbImp || {}\n  const fontes = s.kbFontes\n  const tipoIc = { pdf: 'FileText', docx: 'FileType', md: 'FileCode', txt: 'File', gdoc: 'FileText', url: 'Globe' }\n  return `\n    <div class=\"editor\"><div class=\"ed-body\">\n      <div class=\"sec-h\"><h2 class=\"display ed-t\">Importar documentos</h2><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"kb-imp-fechar\">${ic(d2, 'X', 14)}Fechar</button></div>\n      <p class=\"hint\">${tela(d2).k === 'guia' ? 'O documento entra inteiro como um artigo rascunho do guia. Ao publicar, ele é quebrado em trechos pra busca do stylist.' : 'A IA lê o documento, separa as dúvidas que um cliente teria e cria um artigo rascunho pra cada uma, usando só o que está escrito. Nada vai pro stylist antes de alguém revisar e publicar.'}</p>\n      <div class=\"lk\">\n        <section class=\"sec\">\n          <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Upload', 16)} Arquivo</h3><span class=\"muted\">PDF, Word (.docx), Markdown (.md) ou texto (.txt)</span></div>\n          <label class=\"drop ${imp.rodando ? 'off' : ''}\">${ic(d2, 'FileUp', 22)}<span>Escolher arquivo</span><input type=\"file\" class=\"kbfile\" accept=\".pdf,.docx,.md,.markdown,.txt\" hidden ${imp.rodando ? 'disabled' : ''}></label>\n        </section>\n        <section class=\"sec\">\n          <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Link', 16)} Link</h3><span class=\"muted\">Google Docs (compartilhado como “qualquer pessoa com o link”) ou página dos domínios liberados na config kb</span></div>\n          <form class=\"filtros\" data-form=\"kburl\"><input class=\"input input-sm grow\" placeholder=\"https://docs.google.com/document/d/…\" ${imp.rodando ? 'disabled' : ''}><button class=\"btn btn-outline btn-sm\" ${imp.rodando ? 'disabled' : ''}>${ic(d2, 'Download', 14)}Importar link</button></form>\n        </section>\n        ${imp.etapa || imp.erro ? `<section class=\"sec ${imp.erro ? 'err' : ''}\">\n          ${imp.erro ? `<p class=\"invalid-txt\">${esc(imp.erro)}</p>` : `<p>${imp.rodando ? '<span class=\"spinner\"></span> ' : ''}${esc(imp.etapa)}</p>`}\n          ${imp.total ? `<div class=\"ebar2\"><i style=\"width:${Math.round(((imp.trecho || 0) / imp.total) * 100)}%\"></i></div><span class=\"muted\">${imp.chars?.toLocaleString('pt-BR')} caracteres · ${imp.total} trecho(s)</span>` : ''}\n          ${(imp.criados || []).length ? `<div class=\"ev-l\">${imp.criados.map((c) => `<div class=\"ev\"><span>${esc(c.titulo)} ${c.categoria ? `<span class=\"st\">${esc(c.categoria)}</span>` : ''}</span><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"kb-abrir\" data-id=\"${c.id}\">Revisar</button></div>`).join('')}</div>` : ''}\n        </section>` : ''}\n        <section class=\"sec\">\n          <div class=\"sec-h\"><h3 class=\"display sec-t\">Documentos importados</h3></div>\n          ${tela(d2).k === 'guia' ? '<p class=\"hint\">Os documentos importados aparecem na lista à esquerda como rascunho.</p>' : !fontes ? '<div class=\"skeleton sk-row\"></div>' : fontes.length ? `<div class=\"ev-l\">${fontes.map((f) => `<div class=\"ev\"><span>${ic(d2, tipoIc[f.tipo] || 'File', 14)} ${f.url ? `<a href=\"${esc(f.url)}\" target=\"_blank\" rel=\"noopener\">${esc(f.nome)}</a>` : f.arquivoUrl ? `<a href=\"${esc(f.arquivoUrl)}\" target=\"_blank\" rel=\"noopener\">${esc(f.nome)}</a>` : esc(f.nome)}</span><span class=\"row-m\">${badge(f.status)}<span class=\"muted\">${f.artigos || 0} artigos · ${quando(f.createdAt)}</span></span></div>${f.erro ? `<p class=\"hint invalid-txt\">${esc(f.erro)}</p>` : ''}`).join('')}</div>` : '<p class=\"vazio\">Nenhum documento importado ainda.</p>'}\n        </section>\n      </div>\n    </div></div>`\n}\n\n// Guia de estilo: artigo em Markdown, IA escreve a partir de notas, teste da busca mostra os trechos que o stylist receberia.\nconst TIPOS_GUIA = ['tecido', 'estilo', 'ocasiao', 'cuidado', 'colecao', 'outro']\n\nasync function guiaIA(d2) {\n  const s = d2.state\n  const notas = (s.guiaNotas || '').trim()\n  if (notas.length < 20) return toast(d2, 'Escreva ou cole as notas (dados, briefing, texto do stylist) pra IA usar')\n  s.guiaBusy = true\n  try {\n    const r = await d2.rush(d2.params.kbIa || 'fps-kb-ia').invoke({ acao: 'artigo', tema: s.draft.titulo || 'o assunto das notas', fonte: notas })\n    const o = r?.data || r\n    if (o?.erro || !o?.texto) throw new Error(o?.erro || 'a IA não devolveu texto')\n    s.draft = { ...s.draft, titulo: s.draft.titulo || o.titulo, tipo: s.draft.tipo || o.tipo, tags: (s.draft.tags || []).length ? s.draft.tags : o.tags || [], texto: o.texto }\n    s.dirty = true\n    toast(d2, 'Rascunho escrito a partir das notas; revise antes de publicar')\n  } catch (e) {\n    toast(d2, `IA falhou: ${e.message}`)\n  } finally {\n    s.guiaBusy = false\n  }\n}\n\nasync function guiaTestar(d2) {\n  const s = d2.state\n  const q = (s.guiaTeste || '').trim()\n  if (!q) return\n  s.guiaRes = null\n  try {\n    const r = await d2.rush(d2.params.guiaBusca || 'fps-guia-buscar').invoke({ q })\n    s.guiaRes = r?.data || r\n  } catch (e) {\n    toast(d2, `Teste falhou: ${e.message}`)\n  }\n}\n\nfunction renderGuia(d2) {\n  const s = d2.state\n  const d = s.draft\n  const res = s.guiaRes\n  const pct = (x) => (x == null ? '–' : `${Math.round(x * 100)}%`)\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">\n        ${renderCampo(d2, 'titulo', d.titulo || '')}\n        <div class=\"campo\"><label class=\"lbl\">tipo</label><select class=\"input\" data-f=\"tipo\" data-t=\"str\">${TIPOS_GUIA.map((t) => `<option value=\"${t}\" ${d.tipo === t ? 'selected' : ''}>${t}</option>`).join('')}</select></div>\n        <div class=\"campo wide\"><label class=\"lbl\">tags <span class=\"hint\">uma por linha: tecidos, ocasiões, peças; ajudam a busca a achar o artigo</span></label><textarea class=\"input ta\" rows=\"3\" data-f=\"tags\" data-t=\"lista\">${esc((d.tags || []).join('\\n'))}</textarea></div>\n        <div class=\"campo wide\"><label class=\"lbl\">texto <span class=\"hint\">Markdown; só o stylist lê, o cliente não vê este texto</span></label><textarea class=\"input ta md\" rows=\"16\" data-f=\"texto\" data-t=\"str\">${esc(d.texto || '')}</textarea></div>\n        ${renderCampo(d2, 'fonte', d.fonte || '')}\n        <div class=\"campo\"><label class=\"lbl\">busca</label><span class=\"muted\">${d.status === 'publicado' ? `${d.trechos || 0} trecho(s) indexado(s)${d.indexadoEm ? ` em ${quando(d.indexadoEm)}` : ''}` : 'fora da busca até publicar'}</span></div>\n      </div></section>\n      <section class=\"sec ia\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Sparkles', 16)} Escrever com IA a partir de notas</h3><span class=\"muted\">usa só o que você colar; organiza em toque, quando usar, como combinar e cuidados</span></div>\n        <textarea class=\"input ta\" rows=\"4\" data-g=\"notas\" placeholder=\"Cole notas do stylist, briefing da coleção, ficha técnica do tecido…\">${esc(s.guiaNotas || '')}</textarea>\n        <div class=\"row\"><button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"guia-ia\" ${s.guiaBusy ? 'disabled' : ''}>${ic(d2, 'Wand2', 14)}${s.guiaBusy ? 'Escrevendo…' : 'Escrever artigo'}</button><span class=\"hint\">Substitui o texto atual.</span></div>\n      </section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Testar a busca</h3><span class=\"muted\">o que o stylist receberia pra esta frase (só artigos publicados)</span></div>\n        <form class=\"filtros\" data-form=\"guiateste\"><input class=\"input input-sm grow\" placeholder=\"Ex.: casamento na praia de dia, 30 graus\" value=\"${esc(s.guiaTeste || '')}\"><button class=\"btn btn-outline btn-sm\">${ic(d2, 'Search', 14)}Testar</button></form>\n        ${res ? `<div class=\"kbres\">${(res.trechos || []).length ? res.trechos.map((t) => `<div class=\"trecho\"><div class=\"ev\"><b>${esc(t.titulo)}${t.artigoId === d._id ? ' <span class=\"st\">este artigo</span>' : ''}</b><span>${pct(t.score)}</span></div><p class=\"caption\">${esc((t.texto || '').slice(0, 280))}${(t.texto || '').length > 280 ? '…' : ''}</p></div>`).join('') : `<p class=\"invalid-txt\">Nenhum trecho acima do limiar ${pct(res.limiar)} (melhor: ${pct(res.melhorScore)}).</p>`}</div>` : ''}\n      </section>\n    </div>`\n}\n\nasync function importarGuia(d2, origem) {\n  const s = d2.state\n  const imp = (patch) => { s.kbImp = { ...s.kbImp, ...patch } }\n  imp({ rodando: true, erro: null, etapa: 'Lendo o documento…', criados: [], total: 0 })\n  try {\n    let nome, texto, fonte, arquivoUrl = null\n    if (origem.file) {\n      nome = origem.file.name.replace(/\\.[^.]+$/, '')\n      ;({ texto } = await textoDoArquivo(d2, origem.file))\n      try { const up = await d2.drive(d2.params.assets).uploadFromFiles([origem.file]); arquivoUrl = up?.[0]?.fileUrl || null } catch (e) {}\n      fonte = arquivoUrl || origem.file.name\n    } else {\n      const r = await d2.rush(d2.params.kbFonte || 'fps-kb-fonte-url').invoke({ url: origem.url.trim() })\n      const o = r?.data || r\n      if (o?.erro) throw new Error(o.erro)\n      texto = o.html ? textoDoHtml(o.texto) : o.texto\n      nome = origem.url.trim()\n      fonte = nome\n    }\n    texto = (texto || '').trim()\n    if (texto.length < 40) throw new Error('não achei texto no documento (PDF escaneado como imagem não tem texto pra ler)')\n    imp({ etapa: 'Salvando como rascunho…', chars: texto.length })\n    const out = await api(d2, { acao: 'salvar', tabela: 'fps-guia', registro: { titulo: nome, tipo: 'outro', tags: [], texto, fonte, status: 'rascunho' } })\n    const id = out.registro?._id\n    imp({ rodando: false, etapa: `Artigo “${nome}” criado como rascunho (${texto.length.toLocaleString('pt-BR')} caracteres). Revise tipo e tags e publique pra entrar na busca.`, criados: id ? [{ id, titulo: nome, categoria: 'rascunho' }] : [] })\n    const rows = await api(d2, { acao: 'listar', tabela: 'fps-guia', limite: 300, filtro: { $sort: { createdAt: -1 } } }).catch(() => null)\n    if (rows && tela(d2).k === 'guia') s.rows = rows.data || s.rows\n  } catch (e) {\n    imp({ rodando: false, erro: e.message || String(e), etapa: null })\n  }\n}\n\n// Merchandising: regra com tipo, alvo (campo + valor, vazio = catálogo todo), parâmetro do tipo e vigência. O teste roda a busca do stylist com o rascunho aplicado por cima das regras publicadas.\nconst TIPOS_MERCH = [\n  ['destaque', 'Destaque', 'Sobe as peças do alvo na busca. Peso maior sobe mais; pesos de regras diferentes somam.'],\n  ['exclusao', 'Exclusão', 'Tira as peças do alvo da busca e dos looks.'],\n  ['estoqueMinimo', 'Estoque mínimo', 'Só mostra peças do alvo com pelo menos N tamanhos disponíveis.'],\n  ['faixaPreco', 'Faixa de preço', 'Só mostra peças do alvo dentro da faixa.'],\n  ['novidade', 'Novidade', 'Sobe peças do alvo lançadas nos últimos N dias.'],\n]\nconst CAMPOS_MERCH = [['categoria', 'Categoria'], ['colecao', 'Coleção'], ['linha', 'Linha'], ['ref', 'Referência (SKU)'], ['nome', 'Nome da peça']]\n\nasync function merchTestar(d2) {\n  const s = d2.state\n  const q = (s.mq || '').trim()\n  if (!q) return toast(d2, 'Digite uma busca pra testar')\n  s.mRes = null\n  s.mBusy = true\n  try {\n    const regra = { ...s.draft, status: 'publicado' }\n    const busca = (extra) => d2.rush(d2.params.busca || 'fps-tool-search').invoke({ q, limite: 12, dryRun: true, ...extra }).then((r) => r?.data || r)\n    const [com, sem] = await Promise.all([busca({ regrasPreview: [regra] }), busca({ regrasPreview: s.draft._id ? [{ _id: s.draft._id, status: 'arquivado' }] : [] })])\n    s.mRes = { com, sem }\n  } catch (e) {\n    toast(d2, `Teste falhou: ${e.message}`)\n  } finally {\n    s.mBusy = false\n  }\n}\n\nfunction renderMerch(d2) {\n  const s = d2.state\n  const d = s.draft\n  const tipo = TIPOS_MERCH.find((t) => t[0] === d.tipo) || TIPOS_MERCH[0]\n  const alvo = d.alvo || {}\n  const num = (k, lbl, hint) => `<div class=\"campo\"><label class=\"lbl\">${lbl}${hint ? ` <span class=\"hint\">${hint}</span>` : ''}</label><input class=\"input\" type=\"number\" data-f=\"${k}\" data-t=\"num\" value=\"${d[k] ?? ''}\"></div>`\n  const param = d.tipo === 'estoqueMinimo' ? num('tamanhos', 'tamanhos disponíveis no mínimo', 'ex.: 3')\n    : d.tipo === 'faixaPreco' ? num('precoMin', 'preço mínimo (R$)') + num('precoMax', 'preço máximo (R$)')\n    : d.tipo === 'novidade' ? num('dias', 'lançadas nos últimos (dias)', 'padrão 30') + num('peso', 'peso', 'padrão 1')\n    : d.tipo === 'destaque' ? num('peso', 'peso', 'padrão 1') : ''\n  const r = s.mRes\n  const ids = (x) => (x?.pieces || []).map((p) => p.productId)\n  const antes = ids(r?.sem)\n  const pecaCard = (p) => `<div class=\"bitem\"><img src=\"${esc(p.packshot || p.image)}\" alt=\"\"><span class=\"bnome\">${esc(p.name)}</span><span class=\"muted\">${brl(p.price)} · ${p.tamanhosDisponiveis ?? '–'} tam.</span><span class=\"row-m\">${p.peso ? `<span class=\"st st-publicado\">peso ${p.peso}</span>` : ''}${antes.length && !antes.includes(p.productId) ? '<span class=\"st st-agendado\">entrou</span>' : ''}</span></div>`\n  const m = r?.com?.merch || {}\n  const saiu = r ? (r.sem?.pieces || []).filter((p) => !ids(r.com).includes(p.productId)) : []\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">\n        <div class=\"campo wide\"><label class=\"lbl\">nome</label><input class=\"input\" data-f=\"nome\" data-t=\"str\" value=\"${esc(d.nome || '')}\" placeholder=\"Ex.: Outlet fora do stylist\"></div>\n        <div class=\"campo wide\"><label class=\"lbl\">tipo</label><div class=\"opts\">${TIPOS_MERCH.map(([v, t]) => `<button type=\"button\" class=\"chip\" data-act=\"merch-tipo\" data-v=\"${v}\" aria-pressed=\"${d.tipo === v}\">${t}</button>`).join('')}</div><span class=\"hint\">${tipo[2]}</span></div>\n        <div class=\"campo\"><label class=\"lbl\">aplica em</label><select class=\"input\" data-f=\"alvo.campo\" data-t=\"str\">${CAMPOS_MERCH.map(([v, t]) => `<option value=\"${v}\" ${(alvo.campo || 'categoria') === v ? 'selected' : ''}>${t}</option>`).join('')}</select></div>\n        <div class=\"campo\"><label class=\"lbl\">que contém <span class=\"hint\">vazio = catálogo todo</span></label><input class=\"input\" data-f=\"alvo.valor\" data-t=\"str\" value=\"${esc(alvo.valor || '')}\" placeholder=\"Ex.: Outlet, RELANÇAMENTO 118, ML.05.0398\"></div>\n        ${param}\n        ${renderCampo(d2, 'inicio', d.inicio || '')}\n        ${renderCampo(d2, 'fim', d.fim || '')}\n        <div class=\"campo wide\"><label class=\"lbl\">nota</label><input class=\"input\" data-f=\"nota\" data-t=\"str\" value=\"${esc(d.nota || '')}\" placeholder=\"Por que a regra existe\"></div>\n      </div><p class=\"hint\">Vale pra busca do stylist, pros looks gerados e pelas peças dos looks curados. Só regras publicadas e dentro da vigência valem.</p></section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Testar a regra</h3><span class=\"muted\">mesma busca do stylist, com este rascunho aplicado; compara com as regras publicadas sem ele</span></div>\n        <form class=\"filtros\" data-form=\"merchteste\"><input class=\"input input-sm grow\" placeholder=\"Ex.: camisa linho\" value=\"${esc(s.mq || '')}\"><button class=\"btn btn-outline btn-sm\" ${s.mBusy ? 'disabled' : ''}>${ic(d2, 'Search', 14)}${s.mBusy ? 'Buscando…' : 'Testar'}</button></form>\n        ${r ? `<div class=\"row\"><span class=\"st\">${m.excluidos || 0} excluídas</span><span class=\"st\">${m.semEstoque || 0} sem estoque</span><span class=\"st\">${m.foraDaFaixa || 0} fora da faixa</span><span class=\"st\">${m.comPeso || 0} com peso</span><span class=\"muted\">${r.com?.brutos || 0} peças na busca crua</span></div>\n          <div class=\"bgrid\">${(r.com?.pieces || []).map(pecaCard).join('') || '<p class=\"vazio\">Nenhuma peça sobrou com essa regra.</p>'}</div>\n          ${saiu.length ? `<p class=\"hint\">Saíram com a regra: ${esc(saiu.map((p) => p.name).join(' · '))}</p>` : ''}` : ''}\n      </section>\n    </div>`\n}\n\n// Sem resposta: cada pergunta vira artigo novo, entra como variante de um artigo existente ou é ignorada.\nasync function gapAcao(d2, acao, artigoId) {\n  const s = d2.state\n  const g = s.draft\n  try {\n    if (acao === 'novo') {\n      const out = await api(d2, { acao: 'salvar', tabela: 'fps-kb-articles', registro: { pergunta: g.pergunta, titulo: '', categoria: '', resposta: '', variantes: [], fonte: '', status: 'rascunho', versao: 1 } })\n      const id = out.registro?._id\n      await api(d2, { acao: 'salvar', tabela: 'fps-kb-gaps', registro: { _id: g._id, status: 'virou-artigo', artigoId: id } })\n      s.dirty = false\n      await abrir(d2, 'kb')\n      if (id) selecionar(d2, id)\n      toast(d2, 'Artigo criado como rascunho; escreva a resposta e publique')\n      return\n    }\n    if (acao === 'juntar') {\n      const a = (s.kbArts || []).find((x) => x._id === artigoId)\n      if (!a) return\n      const vs = a.variantes || []\n      if (!vs.some((v) => v.toLowerCase() === g.pergunta.toLowerCase())) await api(d2, { acao: 'salvar', tabela: 'fps-kb-articles', registro: { _id: a._id, variantes: [...vs, g.pergunta] } })\n      await api(d2, { acao: 'salvar', tabela: 'fps-kb-gaps', registro: { _id: g._id, status: 'virou-artigo', artigoId: a._id } })\n      toast(d2, `Pergunta entrou como variante de “${a.titulo || a.pergunta}”`)\n    }\n    if (acao === 'ignorar' || acao === 'reabrir') {\n      await api(d2, { acao: 'salvar', tabela: 'fps-kb-gaps', registro: { _id: g._id, status: acao === 'ignorar' ? 'ignorada' : 'aberta' } })\n      toast(d2, acao === 'ignorar' ? 'Pergunta ignorada' : 'Pergunta reaberta')\n    }\n    const sel = g._id\n    await abrir(d2, 'gaps')\n    selecionar(d2, sel)\n  } catch (e) {\n    toast(d2, `Não gravou: ${e.message}`)\n  }\n}\n\nfunction renderGap(d2) {\n  const s = d2.state\n  const g = s.draft\n  const arts = s.kbArts || []\n  const q = (s.gapQ || '').toLowerCase()\n  const lista = arts.filter((a) => a.status !== 'arquivado' && (!q || `${a.titulo} ${a.pergunta} ${(a.variantes || []).join(' ')}`.toLowerCase().includes(q))).slice(0, 12)\n  const fechada = g.status && g.status !== 'aberta'\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\">\n        <p class=\"gap-q\">“${esc(g.pergunta)}”</p>\n        <div class=\"row\">${badge(g.status || 'aberta')}<span class=\"muted\">${g.ocorrencias || 1} vez(es) · última em ${quando(g.updatedAt)}</span>${g.melhorArtigo ? `<span class=\"muted\">mais próximo: ${esc(g.melhorArtigo)}${g.melhorScore != null ? ` (${Math.round(g.melhorScore * 100)}%)` : ''}</span>` : ''}</div>\n        <div class=\"row\">\n          <button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"gap-novo\">${ic(d2, 'FilePlus', 14)}Virar artigo novo</button>\n          ${fechada ? `<button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"gap-reabrir\">${ic(d2, 'RotateCcw', 14)}Reabrir</button>` : `<button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"gap-ignorar\">${ic(d2, 'EyeOff', 14)}Ignorar</button>`}\n        </div>\n      </section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Ou juntar a um artigo existente</h3><span class=\"muted\">a pergunta entra como mais uma forma de perguntar</span></div>\n        <input class=\"input input-sm\" data-kb=\"gapq\" placeholder=\"Filtrar artigos\" value=\"${esc(s.gapQ || '')}\">\n        ${!s.kbArts ? '<div class=\"skeleton sk-row\"></div>' : lista.length ? `<div class=\"ev-l\">${lista.map((a) => `<div class=\"ev\"><span>${esc(a.titulo || a.pergunta)} ${badge(a.status)}${g.artigoId === a._id ? ' <span class=\"st\">já ligado</span>' : ''}</span><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"gap-juntar\" data-id=\"${a._id}\">${ic(d2, 'Link', 14)}Juntar</button></div>`).join('')}</div>` : '<p class=\"vazio\">Nenhum artigo com esse filtro.</p>'}\n      </section>\n    </div>`\n}\n\n// Console do stylist: a mesma mensagem roda na config publicada e no rascunho (persona, guardrails e ocasiões em rascunho), lado a lado, sem gravar nada. Cada lado guarda o próprio histórico.\nconst LADOS = [['pub', 'Publicada', false], ['rasc', 'Rascunho', true]]\n\nasync function turnoTeste(d2, { mensagem, historico, rascunho }) {\n  const r = await d2.rush(d2.params.conversa || 'fps-conversa').invoke({ mensagem, historico, rascunho, dryRun: true, canal: d2.state.consCanal || 'embed', sessionId: `console-${Date.now()}` })\n  return r?.data || r\n}\n\nasync function consoleEnviar(d2, texto) {\n  const s = d2.state\n  texto = (texto || '').trim()\n  if (!texto) return\n  const lados = s.consSo ? LADOS.filter((l) => l[0] === 'rasc') : LADOS\n  s.consBusy = true\n  await Promise.all(lados.map(async ([k, , rascunho]) => {\n    const hist = s.cons[k].map((m) => ({ papel: m.papel, texto: m.texto }))\n    s.cons = { ...s.cons, [k]: [...s.cons[k], { papel: 'cliente', texto }] }\n    try {\n      const o = await turnoTeste(d2, { mensagem: texto, historico: hist, rascunho })\n      s.cons = { ...s.cons, [k]: [...s.cons[k], { papel: 'stylist', texto: o.legenda || '', blocos: o.blocks || [], meta: { tools: o.tools || [], passos: o.passos, ms: o.latenciaMs, versoes: o.versoes, invalidos: o.invalidos || [], correcoes: o.correcoes || [] } }] }\n    } catch (e) {\n      s.cons = { ...s.cons, [k]: [...s.cons[k], { papel: 'stylist', texto: '', erro: e.message }] }\n    }\n  }))\n  s.consBusy = false\n}\n\nasync function rodarRoteiro(d2, sim) {\n  const s = d2.state\n  s.cons = { pub: [], rasc: [] }\n  s.consSim = sim.nome\n  for (const m of sim.roteiro || []) await consoleEnviar(d2, m)\n  toast(d2, `Roteiro de “${sim.nome}” concluído`)\n}\n\nfunction renderTurnos(d2, itens) {\n  if (!itens.length) return '<p class=\"vazio\">Sem mensagens ainda.</p>'\n  return `<div class=\"msgs\">${itens.map((m) => m.papel === 'cliente' ? `<div class=\"msg eu\"><span class=\"eyebrow\">Cliente</span><p>${esc(m.texto)}</p></div>`\n    : `<div class=\"msg\"><span class=\"eyebrow\">Stylist${m.meta ? ` · ${(m.meta.ms / 1000).toFixed(1)}s · ${m.meta.passos} passo(s)` : ''}</span>\n      ${m.erro ? `<p class=\"invalid-txt\">${esc(m.erro)}</p>` : `<p>${esc(m.texto)}</p>`}\n      ${(m.blocos || []).length ? `<div class=\"blks\">${m.blocos.map(miniBloco).join('')}</div>` : ''}\n      ${m.meta ? `<span class=\"row-m\">${m.meta.tools.length ? m.meta.tools.map((t) => `<span class=\"st\">${esc(t)}</span>`).join('') : '<span class=\"st st-rascunho\">sem ferramenta</span>'}${m.meta.invalidos.length ? `<span class=\"st st-rascunho\">${m.meta.invalidos.length} bloco(s) inválido(s)</span>` : ''}${(m.meta.correcoes || []).map((c) => `<span class=\"st st-agendado\" title=\"correção automática do turno\">${c === 'toolRepetidaBloqueada' ? 'ferramenta repetida bloqueada' : c === 'exigiuFerramenta' ? 'peça sem ferramenta refeita' : esc(c)}</span>`).join('')}${m.meta.versoes ? `<span class=\"muted\">persona v${m.meta.versoes.persona} (${esc(m.meta.versoes.personaStatus)})</span>` : ''}</span>` : ''}\n    </div>`).join('')}</div>`\n}\n\nfunction renderConsole(d2) {\n  const s = d2.state\n  const c = s.cons || { pub: [], rasc: [] }\n  const lados = s.consSo ? LADOS.filter((l) => l[0] === 'rasc') : LADOS\n  return `\n    <div class=\"painel\">\n      <section class=\"sec\">\n        <form class=\"filtros\" data-form=\"console\"><input class=\"input input-sm grow\" placeholder=\"Escreva como o cliente escreveria. Ex.: jantar a dois sexta, algo casual\" ${s.consBusy ? 'disabled' : ''}><button class=\"btn btn-primary btn-sm\" ${s.consBusy ? 'disabled' : ''}>${ic(d2, 'Send', 14)}${s.consBusy ? 'Rodando…' : 'Enviar'}</button></form>\n        <div class=\"row\">\n          <select class=\"input input-sm\" data-cons=\"sim\" ${s.consBusy ? 'disabled' : ''}><option value=\"\">Cliente simulado…</option>${(s.sims || []).filter((x) => x.ativo !== false).map((x) => `<option value=\"${x._id}\">${esc(x.nome)}</option>`).join('')}</select>\n          <button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"cons-roteiro\" ${s.consBusy ? 'disabled' : ''}>${ic(d2, 'Play', 14)}Rodar roteiro</button>\n          <select class=\"input input-sm\" data-cons=\"canal\"><option value=\"embed\" ${s.consCanal !== 'whatsapp' ? 'selected' : ''}>canal embed</option><option value=\"whatsapp\" ${s.consCanal === 'whatsapp' ? 'selected' : ''}>canal whatsapp</option></select>\n          <label class=\"chk\"><input type=\"checkbox\" data-cons=\"so\" ${s.consSo ? 'checked' : ''}>só o rascunho</label>\n          <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"cons-limpar\" ${s.consBusy ? 'disabled' : ''}>${ic(d2, 'RotateCcw', 14)}Limpar</button>\n          <span class=\"hint\">Nada é gravado. O rascunho usa a última versão de persona, guardrails e ocasiões, publicada ou não; looks, merchandising e base usam o que está publicado.${s.consSim ? ` Roteiro: ${esc(s.consSim)}.` : ''}</span>\n        </div>\n      </section>\n      <div class=\"cols\">${lados.map(([k, t]) => `<section class=\"box\"><p class=\"eyebrow\">${t}</p>${renderTurnos(d2, c[k])}</section>`).join('')}</div>\n    </div>`\n}\n\n// Replay: cada mensagem de texto do cliente roda de novo contra o rascunho com o histórico original até ali; eventos (try-on, troca) ficam de fora.\nasync function rodarReplay(d2) {\n  const s = d2.state\n  const ms = (s.msgs || []).filter((m) => !m.evento)\n  s.replay = {}\n  s.replayBusy = true\n  try {\n    for (let i = 0; i < ms.length; i++) {\n      if (ms[i].papel !== 'cliente') continue\n      const historico = ms.slice(0, i).map((m) => ({ papel: m.papel, texto: m.texto }))\n      const o = await turnoTeste(d2, { mensagem: ms[i].texto, historico, rascunho: true }).catch((e) => ({ erro: e.message }))\n      s.replay = { ...s.replay, [ms[i]._id]: o }\n    }\n  } finally {\n    s.replayBusy = false\n  }\n}\n\nfunction renderReplay(d2) {\n  const s = d2.state\n  const ms = (s.msgs || []).filter((m) => !m.evento)\n  if (!s.msgs) return '<div class=\"skeleton sk-row\"></div>'\n  const pares = ms.map((m, i) => m.papel === 'cliente' ? { cli: m, orig: ms[i + 1]?.papel === 'stylist' ? ms[i + 1] : null } : null).filter(Boolean)\n  if (!pares.length) return '<p class=\"vazio\">Essa conversa não tem mensagem de texto do cliente pra repetir.</p>'\n  const nova = (o) => !o ? '<p class=\"muted\">–</p>' : o.erro ? `<p class=\"invalid-txt\">${esc(o.erro)}</p>` : `<p>${esc(o.legenda || '')}</p>${(o.blocks || []).length ? `<div class=\"blks\">${o.blocks.map(miniBloco).join('')}</div>` : ''}<span class=\"row-m\">${(o.tools || []).map((t) => `<span class=\"st\">${esc(t)}</span>`).join('') || '<span class=\"st st-rascunho\">sem ferramenta</span>'}<span class=\"muted\">${((o.latenciaMs || 0) / 1000).toFixed(1)}s</span></span>`\n  return `\n    <div class=\"lk\">\n      <div class=\"row\"><button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"replay\" ${s.replayBusy ? 'disabled' : ''}>${ic(d2, 'Repeat', 14)}${s.replayBusy ? 'Rodando…' : 'Rodar contra o rascunho'}</button><span class=\"hint\">${pares.length} mensagem(ns) do cliente. Cada uma roda com o histórico original até ali; nada é gravado; try-on e trocas de peça ficam de fora.</span></div>\n      ${pares.map((p) => `<section class=\"sec\">${eventoDe(p.cli) ? textoMsg(d2, p.cli) : `<p class=\"gap-q\">“${esc(p.cli.texto)}”</p>`}<div class=\"cols\">\n        <div><p class=\"eyebrow\">Original · ${quando(p.orig?.createdAt)}</p>${p.orig ? `<p>${esc(p.orig.texto || '')}</p>${(p.orig.blocos || []).length ? `<div class=\"blks\">${p.orig.blocos.map(miniBloco).join('')}</div>` : ''}<span class=\"row-m\">${(p.orig.tools || []).map((t) => `<span class=\"st\">${esc(t)}</span>`).join('')}</span>` : '<p class=\"muted\">sem resposta gravada</p>'}</div>\n        <div><p class=\"eyebrow\">Rascunho</p>${nova(s.replay?.[p.cli._id])}</div>\n      </div></section>`).join('')}\n    </div>`\n}\n\n// Clientes: espelho do CRM. Perfil editável, closet das compras, atividade (eventos com customerId, só com consentimento) e o que o stylist recebe do fps-crm.\nasync function verComoStylist(d2) {\n  const s = d2.state\n  try {\n    const r = await d2.rush(d2.params.crm || 'fps-crm').invoke({ acao: 'profile', customerToken: `tok_${s.draft.customerId}`, consent: true })\n    s.cliVisto = r?.data || r\n  } catch (e) {\n    toast(d2, `CRM falhou: ${e.message}`)\n  }\n}\n\nfunction renderCliente(d2) {\n  const s = d2.state\n  const d = s.draft\n  const p = d.perfil || {}\n  const compras = p.purchases || []\n  const v = s.cliVisto\n  const ev = s.cliEv\n  const outros = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['nome', 'segmento', 'consentimento', 'customerId', 'fonte', 'perfil'].includes(k))\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">\n        ${renderCampo(d2, 'nome', d.nome || '')}\n        ${renderCampo(d2, 'segmento', d.segmento || '')}\n        <div class=\"campo\"><label class=\"lbl\">customerId</label><span class=\"mono\">${esc(d.customerId || '')}</span></div>\n        ${renderCampo(d2, 'consentimento', !!d.consentimento)}\n        ${renderCampo(d2, 'perfil.sizes', p.sizes || {})}\n        ${renderCampo(d2, 'perfil.medidas', p.medidas || {})}\n        ${renderCampo(d2, 'perfil.preferences', p.preferences || {})}\n        ${renderCampo(d2, 'perfil.lastVisit', p.lastVisit || '')}\n        ${outros.map(([k, x]) => renderCampo(d2, k, x)).join('')}\n      </div><p class=\"hint\">Fonte: ${esc(d.fonte || 'manual')}. Sem consentimento o stylist trata a pessoa como anônima e os eventos não levam o customerId.</p></section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Closet</h3><span class=\"muted\">${compras.length} compra(s); o stylist não recomenda o que a pessoa já tem e combina com essas peças</span></div>\n        ${compras.length ? `<div class=\"ev-l\">${compras.map((c) => `<div class=\"ev\"><span>${esc(c.nome || c.ref)} <span class=\"muted\">${esc(c.ref || '')}</span></span><span class=\"row-m\"><span class=\"st\">${esc(c.size || '–')}</span><span class=\"muted\">${esc(c.date || '')} · ${brl(c.price)}</span></span></div>`).join('')}</div>` : '<p class=\"vazio\">Sem compras registradas.</p>'}\n        <details><summary class=\"hint\">Editar compras (JSON)</summary>${renderCampo(d2, 'perfil.purchases', compras)}</details>\n      </section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Atividade</h3><span class=\"muted\">eventos do stylist com este customerId</span></div>\n        ${ev && ev.length ? `<div class=\"row\">${Object.entries(ev.reduce((a, e) => ({ ...a, [e.tipo]: (a[e.tipo] || 0) + 1 }), {})).map(([t, n]) => `<span class=\"st\">${esc(t)} · ${n}</span>`).join('')}<span class=\"muted\">${new Set(ev.map((e) => e.sessionId)).size} sessão(ões)${ev.length >= 50 ? ', últimos 50 eventos' : ''}</span></div>` : ''}\n        ${!ev ? '<div class=\"skeleton sk-row\"></div>' : ev.length ? `<div class=\"ev-l\">${ev.slice(0, 10).map((e) => `<div class=\"ev\"><span>${esc(e.tipo)} <span class=\"muted mono\">${esc(e.sessionId || '')}</span></span><span class=\"row-m\">${e.valor ? `<span class=\"muted\">${brl(e.valor)}</span>` : ''}<span class=\"muted\">${quando(e.createdAt)}</span></span></div>`).join('')}</div>` : '<p class=\"vazio\">Nenhum evento identificado ainda.</p>'}\n      </section>\n      <section class=\"sec ia\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Eye', 16)} O que o stylist recebe</h3><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"cli-ver\">Consultar o CRM</button></div>\n        ${v ? (v.anonimo ? `<p class=\"invalid-txt\">Anônimo: ${esc(v.motivo)}</p>` : `<pre class=\"mono pre\">${esc(JSON.stringify(v, null, 2))}</pre>`) : '<p class=\"hint\">Chama o fps-crm com o token deste cliente e consentimento do host, como o stylist faz no turno. Mostra o que está publicado, não o rascunho desta tela.</p>'}\n      </section>\n    </div>`\n}\n\n// Tamanho: grade por categoria e simulador da recomendação (CRM, Sizebay, tabela, perguntar), pelo mesmo fps-size do stylist.\nasync function simularTamanho(d2) {\n  const s = d2.state\n  const t = s.simTam || {}\n  if (!t.categoria) return toast(d2, 'Escolha a categoria')\n  s.simTamRes = null\n  try {\n    const r = await d2.rush(d2.params.size || 'fps-size').invoke({ category: t.categoria, profile: { alturaCm: t.altura ? Number(t.altura) : undefined, pesoKg: t.peso ? Number(t.peso) : undefined }, customerToken: t.cliente ? `tok_${t.cliente}` : undefined, consent: !!t.cliente })\n    s.simTamRes = r?.data || r\n  } catch (e) {\n    toast(d2, `Simulação falhou: ${e.message}`)\n  }\n}\n\nfunction renderSimTamanho(d2) {\n  const s = d2.state\n  const t = s.simTam || {}\n  const cats = [...new Set((s.rows || []).map((r) => r.categoria).filter(Boolean))]\n  const r = s.simTamRes\n  const pct = (x) => (x == null ? '–' : `${Math.round(x * 100)}%`)\n  return `<section class=\"sec ia\">\n    <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Ruler', 16)} Simular recomendação</h3><span class=\"muted\">ordem: histórico do CRM, Sizebay, tabela, perguntar</span></div>\n    <form class=\"filtros\" data-form=\"simtam\">\n      <select class=\"input input-sm\" data-st=\"categoria\"><option value=\"\">categoria</option>${cats.map((c) => `<option value=\"${esc(c)}\" ${t.categoria === c ? 'selected' : ''}>${esc(c)}</option>`).join('')}</select>\n      <input class=\"input input-sm preco\" type=\"number\" data-st=\"altura\" placeholder=\"altura cm\" value=\"${esc(t.altura || '')}\">\n      <input class=\"input input-sm preco\" type=\"number\" data-st=\"peso\" placeholder=\"peso kg\" value=\"${esc(t.peso || '')}\">\n      <input class=\"input input-sm\" data-st=\"cliente\" placeholder=\"customerId (opcional)\" value=\"${esc(t.cliente || '')}\">\n      <button class=\"btn btn-outline btn-sm\">${ic(d2, 'Play', 14)}Simular</button>\n    </form>\n    ${r ? (r.perguntar ? `<p class=\"invalid-txt\">Sem dado suficiente: o stylist pergunta ${esc((r.ask || []).join(', '))}.</p>` : `<div class=\"row\"><span class=\"display kpi-v\">${esc(r.size)}</span><span class=\"st\">fonte ${esc(r.source)}</span><span class=\"st\">confiança ${pct(r.confidence)}</span>${r.preSelecionar ? '<span class=\"st st-publicado\">pré-seleciona na sacola</span>' : '<span class=\"st\">só sugere</span>'}${(r.alternatives || []).filter((a) => a.size).map((a) => `<span class=\"muted\">${esc(a.size)}: ${esc(a.note)}</span>`).join('')}</div>`) : ''}\n  </section>`\n}\n\nfunction renderTamanhoHome(d2) {\n  const s = d2.state\n  const por = {}\n  for (const r of s.rows || []) (por[r.categoria] = por[r.categoria] || []).push(r)\n  return `<div class=\"editor\"><div class=\"ed-body\">\n    <h2 class=\"display ed-t\">Tabelas de medida</h2>\n    <p class=\"hint\">Cada linha é um tamanho: até que altura e peso ele serve. Confiança mínima e fonte Sizebay ficam em Integrações.</p>\n    ${renderSimTamanho(d2)}\n    ${Object.entries(por).map(([cat, rs]) => `<section class=\"sec\"><div class=\"sec-h\"><h3 class=\"display sec-t\">${esc(cat)}</h3><span class=\"muted\">${esc(rs[0]?.fonte || '')}</span></div><div class=\"ev-l\">${rs.map((r) => `<div class=\"ev\"><span><b>${esc(r.tamanho)}</b></span><span class=\"row-m\"><span class=\"muted\">até ${r.medidas?.alturaMax ?? '–'} cm · ${r.medidas?.pesoMax ?? '–'} kg</span><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"sel\" data-id=\"${r._id}\">Editar</button></span></div>`).join('')}</div></section>`).join('')}\n  </div></div>`\n}\n\n// Integrações: CRM e Sizebay com modo, campos do contrato, consentimento e teste com cliente de fixture. A config vive em fps-config (chaves crm e size); editar abre a chave em Regras e limites.\nconst CAMPOS_CRM = [['customerId', 'id do cliente no CRM'], ['firstName', 'primeiro nome'], ['segment', 'segmento'], ['sizes', 'tamanhos por categoria'], ['preferences', 'cores, caimento'], ['purchases', 'compras: ref, tamanho, data, preço'], ['lastVisit', 'última visita']]\n\nasync function carregarIntegracoes(d2) {\n  const s = d2.state\n  const [cfg, cli] = await Promise.all([\n    api(d2, { acao: 'listar', tabela: 'fps-config', limite: 100, filtro: { chave: { $in: ['crm', 'size', 'channels'] }, status: 'publicado', $sort: { versao: -1 } } }).then((o) => ultimas(o.data || [])),\n    api(d2, { acao: 'listar', tabela: 'fps-customers', limite: 50 }).then((o) => o.data || []),\n  ])\n  s.integ = Object.fromEntries(cfg.map((r) => [r.chave, r]))\n  s.integCli = cli\n}\n\nasync function testarIntegracao(d2) {\n  const s = d2.state\n  const cid = s.integTeste || s.integCli?.[0]?.customerId\n  if (!cid) return toast(d2, 'Sem cliente de fixture')\n  s.integRes = null\n  const tok = `tok_${cid}`\n  const [crm, size] = await Promise.all([\n    d2.rush(d2.params.crm || 'fps-crm').invoke({ acao: 'profile', customerToken: tok, consent: true }).then((r) => r?.data || r).catch((e) => ({ erro: e.message })),\n    d2.rush(d2.params.size || 'fps-size').invoke({ category: 'camisa', customerToken: tok, consent: true, profile: {} }).then((r) => r?.data || r).catch((e) => ({ erro: e.message })),\n  ])\n  s.integRes = { cid, crm, size }\n}\n\nfunction irParaChave(d2, chave) {\n  abrir(d2, 'config').then(() => { const r = d2.state.rows.find((x) => x.chave === chave); if (r) selecionar(d2, r._id) })\n}\n\nfunction renderIntegracoes(d2) {\n  const s = d2.state\n  if (!s.integ) return '<div class=\"painel\"><div class=\"skeleton sk-row\"></div></div>'\n  const crm = s.integ.crm?.valor || {}\n  const size = s.integ.size?.valor || {}\n  const modoSt = (m) => `<span class=\"st ${m === 'live' ? 'st-publicado' : m === 'desligado' ? '' : 'st-rascunho'}\">${esc(m || '–')}</span>`\n  const r = s.integRes\n  return `\n    <div class=\"painel\">\n      <div class=\"cols\">\n        <section class=\"box\">\n          <div class=\"sec-h\"><p class=\"eyebrow\">CRM</p><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"integ-editar\" data-v=\"crm\">${ic(d2, 'Pencil', 14)}Editar</button></div>\n          <div class=\"ev\"><span>provedor</span><span>${esc(crm.provedor || '–')}</span></div>\n          <div class=\"ev\"><span>modo</span><span>${modoSt(crm.modo)}</span></div>\n          <div class=\"ev\"><span>consentimento obrigatório</span><span>${crm.consentimentoObrigatorio ? 'sim' : 'não'}</span></div>\n          <div class=\"ev\"><span>texto de consentimento</span><span class=\"muted\">${esc(crm.textoConsentimento || 'não definido')}</span></div>\n          <div class=\"ev\"><span>eventos enviados</span><span class=\"muted\">${esc((crm.eventos || []).join(', '))}</span></div>\n          <p class=\"eyebrow\">campos que o stylist lê</p>\n          ${CAMPOS_CRM.map(([k, t]) => `<div class=\"ev\"><span class=\"mono\">${k}</span><span class=\"muted\">${t}</span></div>`).join('')}\n          <p class=\"hint\">Modo fixture lê a tabela de Clientes. Live precisa do provedor definido e da credencial no KeyVault; o adapter troca o token do host por customerId.</p>\n        </section>\n        <section class=\"box\">\n          <div class=\"sec-h\"><p class=\"eyebrow\">Tamanho · Sizebay</p><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"integ-editar\" data-v=\"size\">${ic(d2, 'Pencil', 14)}Editar</button></div>\n          <div class=\"ev\"><span>modo Sizebay</span><span>${modoSt(size.sizebayModo)}</span></div>\n          <div class=\"ev\"><span>ordem das fontes</span><span class=\"muted\">${esc((size.fontes || []).join(' → '))}</span></div>\n          <div class=\"ev\"><span>confiança mínima pra mostrar</span><span>${size.confiancaMinMostrar ?? '–'}</span></div>\n          <div class=\"ev\"><span>confiança pra pré-selecionar na sacola</span><span>${size.confiancaMinPreSelecionar ?? '–'}</span></div>\n          <p class=\"hint\">Sem credencial, fixture devolve a tabela de medida como se fosse a Sizebay. Desligado pula direto pra tabela.</p>\n        </section>\n      </div>\n      <section class=\"box\">\n        <p class=\"eyebrow\">Testar com cliente de fixture</p>\n        <div class=\"row\"><select class=\"input input-sm\" data-integ=\"cli\">${(s.integCli || []).map((c) => `<option value=\"${esc(c.customerId)}\" ${s.integTeste === c.customerId ? 'selected' : ''}>${esc(c.nome)} · ${esc(c.segmento || '')}${c.consentimento ? '' : ' · sem consentimento'}</option>`).join('')}</select><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"integ-testar\">${ic(d2, 'Play', 14)}Testar CRM e tamanho</button></div>\n        ${r ? `<div class=\"cols\"><div><p class=\"eyebrow\">fps-crm</p>${r.crm?.anonimo ? `<p class=\"invalid-txt\">Anônimo: ${esc(r.crm.motivo)}</p>` : `<pre class=\"mono pre\">${esc(JSON.stringify(r.crm, null, 2))}</pre>`}</div><div><p class=\"eyebrow\">fps-size · camisa</p><pre class=\"mono pre\">${esc(JSON.stringify(r.size, null, 2))}</pre></div></div>` : ''}\n      </section>\n    </div>`\n}\n\n// Serviços: cada tool que o stylist pode chamar. Desligado sai do prompt no turno seguinte (fps-stylist-prompt filtra) e o executor responde desligado.\nconst STATUS_SVC = [['ativo', 'Ativo'], ['instavel', 'Instável'], ['desligado', 'Desligado']]\nconst CANAIS = ['embed', 'launcher', 'app', 'whatsapp', 'voz']\n\nfunction renderServico(d2) {\n  const s = d2.state\n  const d = s.draft\n  const st = s.svcStats\n  const canais = d.canais || []\n  const outros = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['key', 'nome', 'descricao', 'exemplos', 'status', 'executor', 'efeito', 'timeoutMs', 'canais', 'parametros'].includes(k))\n  return `\n    <div class=\"lk\">\n      <section class=\"sec ${d.status === 'desligado' ? 'err' : ''}\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">${esc(d.nome || d.key || 'Serviço')} <span class=\"muted mono\">${esc(d.key || '')}</span></h3>${d.status !== 'desligado' && d.key ? `<button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"svc-desligar\">${ic(d2, 'Power', 14)}Desligar agora</button>` : ''}</div>\n        <div class=\"opts\">${STATUS_SVC.map(([v, t]) => `<button type=\"button\" class=\"chip\" data-act=\"svc-status\" data-v=\"${v}\" aria-pressed=\"${(d.status || 'ativo') === v}\">${t}</button>`).join('')}</div>\n        <p class=\"hint\">Desligado: a tool sai do prompt do stylist no turno seguinte e o executor responde \"desligado\". Instável continua no ar e serve de aviso. \"Desligar agora\" grava e publica na hora.</p>\n        ${st ? `<div class=\"row\"><span class=\"st\">${st.total} chamada(s) em 7 dias</span>${st.erros ? `<span class=\"st st-rascunho\">${st.erros} erro(s)</span>` : ''}${st.desligado ? `<span class=\"st\">${st.desligado} recusada(s) desligado</span>` : ''}${st.mediaMs != null ? `<span class=\"st\">média ${(st.mediaMs / 1000).toFixed(1)} s</span>` : ''}${st.ultima ? `<span class=\"muted\">última ${quando(st.ultima)}</span>` : ''}<button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"svc-execucoes\">${ic(d2, 'Activity', 14)}Ver execuções</button></div>` : d.key ? '<div class=\"skeleton sk-row\"></div>' : ''}\n      </section>\n      <section class=\"sec\"><div class=\"campos\">\n        ${renderCampo(d2, 'nome', d.nome || '')}\n        ${renderCampo(d2, 'key', d.key || '')}\n        <div class=\"campo wide\"><label class=\"lbl\">descrição <span class=\"hint\">é o que o modelo lê pra decidir quando chamar</span></label><textarea class=\"input ta\" rows=\"4\" data-f=\"descricao\" data-t=\"str\">${esc(d.descricao || '')}</textarea></div>\n        ${renderCampo(d2, 'exemplos', d.exemplos || [])}\n        ${renderCampo(d2, 'executor', d.executor || '')}\n        ${renderCampo(d2, 'efeito', d.efeito || '')}\n        ${renderCampo(d2, 'timeoutMs', d.timeoutMs ?? 0)}\n        <div class=\"campo\"><label class=\"lbl\">canais</label><div class=\"opts\">${CANAIS.map((c) => `<button type=\"button\" class=\"chip\" data-act=\"svc-canal\" data-v=\"${c}\" aria-pressed=\"${canais.includes(c)}\">${c}</button>`).join('')}</div></div>\n        ${renderCampo(d2, 'parametros', d.parametros || {})}\n        ${outros.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n      </div></section>\n    </div>`\n}\n\n// Modelo e custo: a chave model e, ao lado, o custo estimado que os alertas usam (turnos × custo médio da chave alertas).\nfunction renderCustoModelo(d2) {\n  const m = d2.state.custoMed\n  const v = d2.state.draft?.valor || {}\n  if (m === undefined) return '<div class=\"skeleton sk-row\"></div>'\n  if (!m) return ''\n  const orc = v.orcamentoDiarioUsd || 0\n  return `<section class=\"sec ia\"><div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Gauge', 16)} Custo estimado nas últimas 24 h</h3><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"nav\" data-k=\"alertas\">Ver alertas</button></div>\n    <div class=\"row\"><span class=\"display kpi-v\">US$ ${Number(m.custoEstimadoUsd || 0).toFixed(2)}</span><span class=\"st\">${m.turnos24h} turnos</span><span class=\"st\">${m.pctOrcamento}% de US$ ${orc}</span></div>\n    <p class=\"hint\">Estimativa: turnos gravados × custo médio por turno (chave alertas). O modelo não devolve tokens, então o número real fica na fatura do provedor. O alerta dispara em ${v.alertaEmPct ?? 80}% do orçamento.</p></section>`\n}\n\n// Banco de imagens de capa: a capa da página de looks sai do fps-capa-buscar (capa fixa da ocasião, imagem ativa marcada pra ocasião, imagem ativa mais próxima do pedido, capa do tenant). A busca lê título e descrição.\nconst FONTE_CAPA = { ocasiao: 'capa fixa da ocasião', 'banco-ocasiao': 'banco, marcada pra ocasião', 'banco-pedido': 'banco, mais próxima do pedido', tenant: 'padrão da marca' }\n\nasync function buscarCapa(d2, q, ocasiao) {\n  const r = await d2.rush(d2.params.capa || 'fps-capa-buscar').invoke({ q, ocasiao: ocasiao || undefined })\n  return r?.data || r\n}\n\nasync function capasPorOcasiao(d2) {\n  const s = d2.state\n  const occs = s.occs || []\n  s.capasOcc = null\n  const res = await Promise.all(occs.map((o) => buscarCapa(d2, o.titulo, o.slug).then((c) => ({ o, c })).catch(() => ({ o, c: null }))))\n  s.capasOcc = res\n}\n\nasync function testarCapa(d2) {\n  const s = d2.state\n  const q = (s.capaQ || '').trim()\n  if (!q) return toast(d2, 'Escreva um pedido pra testar')\n  s.capaTeste = 'buscando'\n  try { s.capaTeste = await buscarCapa(d2, q, s.capaOcc) } catch (e) { s.capaTeste = null; toast(d2, `Busca falhou: ${e.message}`) }\n}\n\nfunction renderTesteCapa(d2) {\n  const s = d2.state\n  const t = s.capaTeste\n  const pct = (x) => (x == null ? '–' : `${Math.round(x * 100)}%`)\n  return `<section class=\"sec ia\">\n    <div class=\"sec-h\"><h3 class=\"display sec-t\">${ic(d2, 'Search', 16)} Testar a busca</h3><span class=\"muted\">o mesmo fps-capa-buscar que a conversa usa</span></div>\n    <form class=\"filtros\" data-form=\"capateste\"><input class=\"input input-sm grow\" data-capa=\"q\" placeholder=\"Ex.: casamento no campo, às 17h\" value=\"${esc(s.capaQ || '')}\"><select class=\"input input-sm\" data-capa=\"occ\"><option value=\"\">sem ocasião</option>${(s.occs || []).map((o) => `<option value=\"${esc(o.slug)}\" ${s.capaOcc === o.slug ? 'selected' : ''}>${esc(o.titulo)}</option>`).join('')}</select><button class=\"btn btn-outline btn-sm\">${ic(d2, 'Search', 14)}Testar</button></form>\n    ${t === 'buscando' ? '<div class=\"skeleton sk-row\"></div>' : t ? `<div class=\"row\">${t.capa ? `<img class=\"capa-g\" src=\"${esc(t.capa)}\" alt=\"\">` : ''}<div><b>Capa escolhida</b><p class=\"muted\">${esc(FONTE_CAPA[t.fonte] || t.fonte)}</p></div></div>\n      <div class=\"bgrid\">${(t.candidatos || []).map((c) => `<div class=\"bitem\"><img src=\"${esc(c.url)}\" alt=\"\"><span class=\"bnome\">${esc(c.titulo)}</span><span class=\"muted\">${pct(c.score)} · ${esc((c.ocasioes || []).join(', ') || 'sem ocasião')}</span></div>`).join('')}</div>` : ''}\n  </section>`\n}\n\nfunction renderImagensHome(d2) {\n  const s = d2.state\n  const co = s.capasOcc\n  return `<div class=\"editor\"><div class=\"ed-body\">\n    <h2 class=\"display ed-t\">Capas</h2>\n    <p class=\"hint\">A busca compara o pedido com título e descrição de cada imagem ativa. Descreva o lugar, a hora e o clima da foto. Imagem marcada com a ocasião ganha preferência; capa fixa na ocasião ganha de todas.</p>\n    ${renderTesteCapa(d2)}\n    <section class=\"sec\"><div class=\"sec-h\"><h3 class=\"display sec-t\">Capa que cada ocasião recebe hoje</h3><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"capa-recalc\">${ic(d2, 'RefreshCw', 14)}Recalcular</button></div>\n      ${!co ? '<div class=\"skeleton sk-row\"></div>' : `<div class=\"lkgrid\">${co.map(({ o, c }) => `<div class=\"lkc\">${c?.capa ? `<img src=\"${esc(c.capa)}\" alt=\"\">` : '<span class=\"pimg\"></span>'}<span class=\"lkc-t\">${esc(o.titulo)}</span><span class=\"muted\">${esc(FONTE_CAPA[c?.fonte] || 'sem capa')}</span></div>`).join('')}</div>`}\n    </section>\n  </div></div>`\n}\n\nfunction renderImagem(d2) {\n  const s = d2.state\n  const d = s.draft\n  const occ = d.ocasioes || []\n  const outros = Object.entries(d).filter(([k]) => !SISTEMA.includes(k) && !['titulo', 'descricao', 'url', 'tipo', 'ativo', 'ocasioes', 'fonte', 'nome'].includes(k))\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\">${d.url ? `<img class=\"capa-g\" src=\"${esc(d.url)}\" alt=\"\">` : '<p class=\"hint\">Sem imagem ainda: envie abaixo.</p>'}</section>\n      <section class=\"sec\"><div class=\"campos\">\n        ${renderCampo(d2, 'titulo', d.titulo || '')}\n        ${renderCampo(d2, 'ativo', d.ativo !== false)}\n        <div class=\"campo wide\"><label class=\"lbl\">descrição <span class=\"hint\">é o que a busca lê: lugar, hora, clima, roupa, clima da cena</span></label><textarea class=\"input ta\" rows=\"3\" data-f=\"descricao\" data-t=\"str\">${esc(d.descricao || '')}</textarea></div>\n        <div class=\"campo wide\"><label class=\"lbl\">ocasiões</label><div class=\"opts\">${(s.occs || []).map((o) => `<button type=\"button\" class=\"chip\" data-act=\"img-occ\" data-v=\"${esc(o.slug)}\" aria-pressed=\"${occ.includes(o.slug)}\">${esc(o.titulo)}</button>`).join('') || '<span class=\"hint\">carregando ocasiões…</span>'}</div></div>\n        ${renderCampo(d2, 'url', d.url || '')}\n        ${renderCampo(d2, 'tipo', d.tipo || 'capa')}\n        ${renderCampo(d2, 'fonte', d.fonte || '')}\n        ${outros.map(([k, v]) => renderCampo(d2, k, v)).join('')}\n      </div></section>\n      ${renderTesteCapa(d2)}\n    </div>`\n}\n\n// Experimentos: duas versões de persona, sorteio por sessão nova no fps-conversa (sessions.experimento), um ativo por vez. Métricas por braço a partir de sessões e eventos.\nasync function metricasExperimento(d2, id) {\n  const s = d2.state\n  try {\n    const ss = (await api(d2, { acao: 'listar', tabela: 'fps-sessions', limite: 2000, filtro: { 'experimento.id': id } })).data || []\n    const ids = ss.map((x) => x.sessionId)\n    const ev = ids.length ? (await api(d2, { acao: 'listar', tabela: 'fps-events', limite: 2000, filtro: { sessionId: { $in: ids } } })).data || [] : []\n    const por = {}\n    for (const b of ['A', 'B']) {\n      const sb = new Set(ss.filter((x) => x.experimento?.braco === b).map((x) => x.sessionId))\n      const eb = ev.filter((e) => sb.has(e.sessionId))\n      const com = (t) => new Set(eb.filter((e) => e.tipo === t).map((e) => e.sessionId)).size\n      const cart = eb.filter((e) => e.tipo === 'add_to_cart')\n      const nCart = com('add_to_cart')\n      por[b] = { sessoes: sb.size, viuLook: com('look_mostrado'), tryon: com('tryon_gerado'), sacola: nCart, valor: cart.reduce((a, e) => a + (Number(e.valor) || 0), 0), ticket: nCart ? cart.reduce((a, e) => a + (Number(e.valor) || 0), 0) / nCart : 0 }\n    }\n    s.expMet = por\n  } catch (e) {\n    s.expMet = { erro: e.message }\n  }\n}\n\nfunction renderExperimento(d2) {\n  const s = d2.state\n  const d = s.draft\n  const ps = s.personas || []\n  const m = s.expMet\n  const opt = (v) => ps.map((p) => `<option value=\"${p.versao}\" ${Number(v) === p.versao ? 'selected' : ''}>v${p.versao} · ${esc(p.status)}${p.versao === 1 ? '' : ''}</option>`).join('')\n  const pct = (n, t) => (t ? `${Math.round((n / t) * 100)}%` : '–')\n  const col = (b) => { const x = m?.[b] || {}; return `<div class=\"box\"><p class=\"eyebrow\">Braço ${b} · persona v${b === 'A' ? d.versaoA ?? '–' : d.versaoB ?? '–'}</p><div class=\"ev\"><span>sessões</span><span>${x.sessoes ?? 0}</span></div><div class=\"ev\"><span>viram look</span><span>${pct(x.viuLook, x.sessoes)}</span></div><div class=\"ev\"><span>fizeram try-on</span><span>${pct(x.tryon, x.sessoes)}</span></div><div class=\"ev\"><span>adicionaram à sacola</span><span>${pct(x.sacola, x.sessoes)}</span></div><div class=\"ev\"><span>ticket médio na sacola</span><span>${brl(x.ticket)}</span></div></div>` }\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\"><div class=\"campos\">\n        <div class=\"campo wide\"><label class=\"lbl\">nome</label><input class=\"input\" data-f=\"nome\" data-t=\"str\" value=\"${esc(d.nome || '')}\"></div>\n        <div class=\"campo wide\"><label class=\"lbl\">hipótese</label><input class=\"input\" data-f=\"nota\" data-t=\"str\" value=\"${esc(d.nota || '')}\" placeholder=\"Ex.: legenda que fecha com pergunta aumenta a sacola\"></div>\n        <div class=\"campo\"><label class=\"lbl\">versão A (controle)</label><select class=\"input\" data-f=\"versaoA\" data-t=\"num\"><option value=\"\">–</option>${opt(d.versaoA)}</select></div>\n        <div class=\"campo\"><label class=\"lbl\">versão B</label><select class=\"input\" data-f=\"versaoB\" data-t=\"num\"><option value=\"\">–</option>${opt(d.versaoB)}</select></div>\n        <div class=\"campo\"><label class=\"lbl\">% das sessões novas no B</label><input class=\"input\" type=\"number\" min=\"0\" max=\"100\" data-f=\"pctB\" data-t=\"num\" value=\"${d.pctB ?? 50}\"></div>\n        <div class=\"campo\"><label class=\"lbl\">status</label><div class=\"opts\">${[['rascunho', 'Rascunho'], ['ativo', 'Ativo'], ['encerrado', 'Encerrado']].map(([v, t]) => `<button type=\"button\" class=\"chip\" data-act=\"exp-status\" data-v=\"${v}\" aria-pressed=\"${(d.status || 'rascunho') === v}\">${t}</button>`).join('')}</div></div>\n      </div><p class=\"hint\">O que muda entre os braços é a persona do stylist (tom, faça/não faça). Cada sessão nova é sorteada uma vez e fica no braço até o fim. Só um experimento ativo por vez. Ativar vale na hora, sem publish; o Console mostra cada versão antes.</p></section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">Resultado</h3>${d._id ? `<button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"exp-recalc\">${ic(d2, 'RefreshCw', 14)}Recalcular</button>` : ''}</div>\n        ${!d._id ? '<p class=\"hint\">Salve o experimento pra acompanhar o resultado.</p>' : !m ? '<div class=\"skeleton sk-row\"></div>' : m.erro ? `<p class=\"invalid-txt\">${esc(m.erro)}</p>` : `<div class=\"cols\">${col('A')}${col('B')}</div><p class=\"hint\">Sessões de teste (suite-*) não gravam. Com poucas sessões a diferença é ruído; espere pelo menos algumas dezenas por braço.</p>`}\n      </section>\n    </div>`\n}\n\n// Histórico: cada gravação do admin (fps-audit) com o que mudou e volta pra versão anterior. Config volta republicando a versão publicada anterior da chave; o resto regrava o \"antes\".\nconst VOLATEIS = ['createdAt', 'updatedAt', '__v', 'embedding', '_embedding', 'd2_embedding']\n\nfunction renderAuditoria(d2) {\n  const s = d2.state\n  const a = s.draft\n  const antes = a.antes || {}\n  const depois = a.depois || {}\n  const chaves = [...new Set([...Object.keys(antes), ...Object.keys(depois)])].filter((k) => !VOLATEIS.includes(k) && k !== '_id')\n  const mudou = chaves.filter((k) => JSON.stringify(antes[k]) !== JSON.stringify(depois[k]))\n  const curto = (v) => { const t = v === undefined ? '–' : typeof v === 'string' ? v : JSON.stringify(v, null, 1); return esc(t.length > 600 ? `${t.slice(0, 600)}…` : t) }\n  const ehConfig = a.tabela === 'fps-config'\n  const podeVoltar = ehConfig ? (depois.versao || 0) > 1 : !!a.antes\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\">\n        <div class=\"row\"><span class=\"st\">${esc(a.tabela)}</span><span class=\"st\">${esc(a.acao)}</span><span class=\"muted\">por ${esc(a.por || '–')} · ${esc(a.origem || '')} · ${quando(a.createdAt)}</span></div>\n        ${ehConfig ? `<p class=\"hint\">Config: chave ${esc(depois.chave)}, versão ${depois.versao} (${esc(depois.status)}).</p>` : ''}\n      </section>\n      <section class=\"sec\">\n        <div class=\"sec-h\"><h3 class=\"display sec-t\">O que mudou</h3><span class=\"muted\">${a.antes ? `${mudou.length} campo(s)` : 'criação'}</span></div>\n        ${a.antes ? (mudou.length ? `<div class=\"diff\">${mudou.map((k) => `<div class=\"dk\">${esc(k)}</div><pre class=\"mono pre d-antes\">${curto(antes[k])}</pre><pre class=\"mono pre d-depois\">${curto(depois[k])}</pre>`).join('')}</div>` : '<p class=\"vazio\">Gravou sem mudar nenhum campo.</p>') : `<pre class=\"mono pre\">${curto(Object.fromEntries(Object.entries(depois).filter(([k]) => !VOLATEIS.includes(k))))}</pre>`}\n      </section>\n      <section class=\"sec\">\n        <div class=\"row\"><button type=\"button\" class=\"btn ${s.rbConfirma ? 'btn-primary' : 'btn-outline'} btn-sm\" data-act=\"rollback\" ${podeVoltar && !s.rbBusy ? '' : 'disabled'}>${ic(d2, 'Undo2', 14)}${s.rbConfirma ? 'Confirmar: voltar agora' : 'Voltar pra versão anterior'}</button>\n        <span class=\"hint\">${!podeVoltar ? 'Criação: não há versão anterior.' : ehConfig ? 'Republica a versão publicada anterior desta chave como versão nova (fica no histórico).' : 'Regrava o registro como estava antes desta mudança. Vale na hora.'}</span></div>\n      </section>\n    </div>`\n}\n\nasync function rollback(d2) {\n  const s = d2.state\n  const a = s.draft\n  if (!s.rbConfirma) { s.rbConfirma = true; return toast(d2, 'Clique de novo para confirmar a volta') }\n  s.rbConfirma = false\n  s.rbBusy = true\n  try {\n    if (a.tabela === 'fps-config') {\n      const v = a.depois?.versao || 0\n      const ant = ((await api(d2, { acao: 'listar', tabela: 'fps-config', limite: 50, filtro: { chave: a.depois.chave, status: 'publicado', versao: { $lt: v }, $sort: { versao: -1 } } })).data || [])[0]\n      if (!ant) throw new Error('não achei versão publicada anterior')\n      await api(d2, { acao: 'salvar', tabela: 'fps-config', registro: { chave: ant.chave, valor: ant.valor, status: 'publicado', nota: `volta pra v${ant.versao} (histórico)` } })\n      toast(d2, `Chave ${ant.chave} voltou pro conteúdo da v${ant.versao}`)\n    } else {\n      const reg = Object.fromEntries(Object.entries(a.antes).filter(([k]) => !VOLATEIS.includes(k)))\n      await api(d2, { acao: 'salvar', tabela: a.tabela, registro: reg })\n      toast(d2, 'Registro voltou pro estado anterior')\n    }\n    await abrir(d2, 'historico')\n  } catch (e) {\n    toast(d2, `Não voltou: ${e.message}`)\n  } finally {\n    s.rbBusy = false\n  }\n}\n\n// Alertas: as regras rodam no fps-alertas-rodar a cada 15 min (config chave alertas); aqui a pessoa lê, resolve e roda a verificação na hora.\nasync function alertaStatus(d2, status) {\n  const s = d2.state\n  try {\n    await api(d2, { acao: 'salvar', tabela: 'fps-alertas', registro: { _id: s.draft._id, status, resolvidoEm: status === 'resolvido' ? new Date().toISOString() : null } })\n    const id = s.draft._id\n    await abrir(d2, 'alertas')\n    selecionar(d2, id)\n    toast(d2, status === 'resolvido' ? 'Alerta resolvido' : 'Alerta reaberto')\n  } catch (e) {\n    toast(d2, `Não gravou: ${e.message}`)\n  }\n}\n\nasync function verificarAlertas(d2) {\n  const s = d2.state\n  s.alBusy = true\n  try {\n    const r = await d2.rush(d2.params.alertas || 'fps-alertas-rodar').invoke({})\n    const o = r?.data || r\n    await abrir(d2, 'alertas')\n    toast(d2, `Verificado: ${(o?.alertas || []).length} alerta(s) ativo(s)`)\n  } catch (e) {\n    toast(d2, `Verificação falhou: ${e.message}`)\n  } finally {\n    s.alBusy = false\n  }\n}\n\nconst TIPO_ALERTA = { orcamento: 'Orçamento', toolFalhando: 'Ferramenta falhando', semResposta: 'Sem resposta', campanhaSemEstoque: 'Campanha sem estoque' }\n\nfunction renderAlerta(d2) {\n  const a = d2.state.draft\n  const aberto = a.status === 'aberto'\n  return `\n    <div class=\"lk\">\n      <section class=\"sec\">\n        <div class=\"row\">${badge(a.status)}<span class=\"st ${a.nivel === 'critico' ? 'st-rascunho' : ''}\">${esc(a.nivel)}</span><span class=\"st\">${esc(TIPO_ALERTA[a.tipo] || a.tipo)}</span></div>\n        <p class=\"gap-q\">${esc(a.mensagem)}</p>\n        <div class=\"ev-l\">\n          <div class=\"ev\"><span>medido</span><span>${a.valor ?? '–'}</span></div>\n          <div class=\"ev\"><span>limite</span><span>${a.limite ?? '–'}</span></div>\n          <div class=\"ev\"><span>primeira vez</span><span>${quando(a.createdAt)}</span></div>\n          <div class=\"ev\"><span>última verificação que viu</span><span>${quando(a.ultimaVez)}</span></div>\n          ${a.resolvidoEm ? `<div class=\"ev\"><span>resolvido em</span><span>${quando(a.resolvidoEm)}</span></div>` : ''}\n        </div>\n        ${a.detalhe ? `<pre class=\"mono pre\">${esc(JSON.stringify(a.detalhe, null, 2))}</pre>` : ''}\n        <div class=\"row\">${aberto ? `<button type=\"button\" class=\"btn btn-primary btn-sm\" data-act=\"al-resolver\">${ic(d2, 'Check', 14)}Marcar resolvido</button>` : `<button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"al-reabrir\">${ic(d2, 'RotateCcw', 14)}Reabrir</button>`}<span class=\"hint\">Resolvido some do contador do Painel. Se a condição continuar no dia, a próxima verificação abre um alerta novo.</span></div>\n      </section>\n    </div>`\n}\n\n// Insights: o que as pessoas pediram no período (ocasião, faixa de valor dos looks), pedidos que não bateram com ocasião cadastrada e perguntas sem resposta, com atalho pra criar ocasião ou artigo.\nasync function insights(d2) {\n  // ponytail: conta no cliente sobre até 2000 linhas por tabela, como o Painel; passa pra aggregate quando o volume crescer.\n  const [ss, ms, ev, gp] = await Promise.all([\n    api(d2, { acao: 'listar', tabela: 'fps-sessions', limite: 2000 }),\n    api(d2, { acao: 'listar', tabela: 'fps-messages', limite: 2000, filtro: { papel: 'cliente' } }),\n    api(d2, { acao: 'listar', tabela: 'fps-events', limite: 2000, filtro: { tipo: 'look_mostrado' } }),\n    api(d2, { acao: 'listar', tabela: 'fps-kb-gaps', limite: 300, filtro: { status: 'aberta' } }),\n  ].map((p) => p.then((o) => o.data || []).catch(() => [])))\n  const dias = d2.state.periodo ?? 30\n  const desde = dias ? Date.now() - dias * 864e5 : 0\n  const sP = ss.filter((x) => !/^suite-/.test(x.sessionId || '') && new Date(x.iniciadaEm || x.createdAt).getTime() >= desde)\n  const ids = new Set(sP.map((x) => x.sessionId))\n  const primeira = {}\n  for (const m of ms) if (ids.has(m.sessionId) && !m.evento && m.texto && (!primeira[m.sessionId] || new Date(m.createdAt) < new Date(primeira[m.sessionId].createdAt))) primeira[m.sessionId] = m\n  const porOcc = {}\n  for (const x of sP) { const k = x.ocasiao || ''; porOcc[k] = (porOcc[k] || 0) + 1 }\n  const semOcc = sP.filter((x) => !x.ocasiao && primeira[x.sessionId]).map((x) => primeira[x.sessionId].texto).filter((t) => !/^\\[evento/.test(t))\n  const faixas = [[0, 1000, 'até R$ 1.000'], [1000, 2000, 'R$ 1.000 a 2.000'], [2000, 3000, 'R$ 2.000 a 3.000'], [3000, Infinity, 'acima de R$ 3.000']]\n  const evP = ev.filter((e) => ids.has(e.sessionId) && e.valor)\n  return {\n    sessoes: sP.length,\n    porOcasiao: Object.entries(porOcc).sort((a, b) => b[1] - a[1]),\n    semOcasiao: [...new Set(semOcc)].slice(0, 15),\n    faixas: faixas.map(([a, b, l]) => [l, evP.filter((e) => e.valor >= a && e.valor < b).length]),\n    gaps: gp.slice().sort((a, b) => (b.ocorrencias || 0) - (a.ocorrencias || 0)).slice(0, 10),\n  }\n}\n\nfunction renderInsights(d2) {\n  const st = d2.state.ins\n  const per = d2.state.periodo ?? 30\n  const seg = `<div class=\"seg\">${[[7, '7 dias'], [30, '30 dias'], [0, 'Tudo']].map(([v, l]) => `<button data-act=\"ins-periodo\" data-v=\"${v}\" aria-pressed=\"${per === v}\">${l}</button>`).join('')}</div>`\n  if (!st) return `<div class=\"painel\">${seg}<div class=\"skeleton sk-row\"></div></div>`\n  const max = Math.max(1, ...st.porOcasiao.map(([, n]) => n))\n  const maxF = Math.max(1, ...st.faixas.map(([, n]) => n))\n  const barra = (l, n, m) => `<div class=\"fx\"><div class=\"fbar\" style=\"width:${Math.round((n / m) * 100)}%\"></div><span class=\"fl\">${esc(l)}</span><span class=\"fn\">${n}</span></div>`\n  return `\n    <div class=\"painel\">\n      <div class=\"painel-top\">${seg}<span class=\"muted\">${st.sessoes} sessões reais no período; a suíte de testes fica fora.</span></div>\n      <div class=\"cols\">\n        <section class=\"box\"><p class=\"eyebrow\">O que pediram, por ocasião</p><div class=\"funil\">${st.porOcasiao.map(([k, n]) => barra(k || 'sem ocasião cadastrada', n, max)).join('') || '<p class=\"vazio\">Sem sessões.</p>'}</div></section>\n        <section class=\"box\"><p class=\"eyebrow\">Valor dos looks mostrados</p><div class=\"funil\">${st.faixas.map(([l, n]) => barra(l, n, maxF)).join('')}</div></section>\n      </div>\n      <section class=\"box\"><p class=\"eyebrow\">Pedidos que não bateram com ocasião cadastrada</p>\n        ${st.semOcasiao.map((t, i) => `<div class=\"ev\"><span class=\"ev-t\">“${esc(t)}”</span><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"ins-ocasiao\" data-i=\"${i}\">${ic(d2, 'Plus', 14)}Criar ocasião</button></div>`).join('') || '<p class=\"vazio\">Todos os pedidos do período bateram com uma ocasião.</p>'}\n      </section>\n      <section class=\"box\"><p class=\"eyebrow\">Perguntas sem resposta mais repetidas</p>\n        ${st.gaps.map((g) => `<div class=\"ev\"><span class=\"ev-t\">“${esc(g.pergunta)}” <span class=\"muted\">${g.ocorrencias || 1}×</span></span><button type=\"button\" class=\"btn btn-ghost btn-sm\" data-act=\"ins-gap\" data-id=\"${g._id}\">${ic(d2, 'FilePlus', 14)}Resolver</button></div>`).join('') || '<p class=\"vazio\">Nenhuma pergunta aberta.</p>'}\n      </section>\n    </div>`\n}\n\nfunction renderEditor(d2) {\n  const s = d2.state\n  const tl = tela(d2)\n  if ((tl.k === 'kb' || tl.k === 'guia') && s.kbImp) return renderImportar(d2)\n  if (!s.draft && tl.k === 'tamanho') return renderTamanhoHome(d2)\n  if (!s.draft && tl.k === 'imagens') return renderImagensHome(d2)\n  if (!s.draft) return `<div class=\"editor vazio-ed\"><p class=\"muted\">Escolha um item${tl.ro ? '' : ' ou crie um novo'}.</p></div>`\n  const campos = Object.entries(s.draft).filter(([k]) => !SISTEMA.includes(k))\n  return `\n    <div class=\"editor\">\n      ${renderBar(d2)}\n      <div class=\"ed-body\">\n        <h2 class=\"display ed-t\">${esc(tl.chave ? `${tl.t} · v${s.draft.versao || 0}` : rotulo(s.draft) || 'Novo')}</h2>\n        ${tl.chave ? '<p class=\"hint\">Publicar grava uma versão nova da chave; o stylist usa a última publicada.</p>' : ''}\n        ${tl.versionada ? '<p class=\"hint\">Cada gravação vira uma versão nova. O stylist usa a última publicada.</p>' : ''}\n        ${tl.k === 'simulados' && s.draft._id ? `<div class=\"row\"><button type=\"button\" class=\"btn btn-outline btn-sm\" data-act=\"sim-rodar\">${ic(d2, 'Play', 14)}Rodar no console</button><span class=\"hint\">toca o roteiro na publicada e no rascunho</span></div>` : ''}\n        ${tl.k === 'replay' ? renderReplay(d2) : tl.k === 'conversas' ? renderMsgs(d2) : tl.k === 'ocasioes' ? renderOcasiao(d2)          : tl.chave ? renderConfigChave(d2) : tl.k === 'looks' ? renderLookCurado(d2) : tl.k === 'campanhas' ? renderCampanha(d2) : tl.k === 'kb' ? renderArtigo(d2) : tl.k === 'gaps' ? renderGap(d2) : tl.k === 'guia' ? renderGuia(d2) : tl.k === 'merch' ? renderMerch(d2) : tl.k === 'clientes' ? renderCliente(d2) : tl.k === 'alertas' ? renderAlerta(d2) : tl.k === 'servicos' ? renderServico(d2) : tl.k === 'experimentos' ? renderExperimento(d2) : tl.k === 'imagens' ? renderImagem(d2) : tl.k === 'historico' ? renderAuditoria(d2) : `<div class=\"campos\">${campos.map(([k, v]) => renderCampo(d2, k, v)).join('')}${tl.k === 'blocos' ? renderBlocoExtra(d2) : ''}</div>${tl.k === 'tamanho' ? renderSimTamanho(d2) : ''}${tl.k === 'gestores' ? '<p class=\"hint\">Desativar bloqueia o acesso ao admin na hora: a porta do admin recusa o e-mail desativado. O login continua pelo Aramis Admin Login; último acesso é gravado a cada uso, no máximo a cada 10 min.</p>' : ''}`}\n      </div>\n    </div>`\n}\n\nfunction renderPreview(d2) {\n  const s = d2.state\n  const p = PREVIEW[s.pv || 'mobile']\n  const caixa = s.pv === 'desktop' ? 520 : 300\n  const esc_ = caixa / p.w\n  return `\n    <div class=\"preview\">\n      <div class=\"pv-top\">\n        <span class=\"eyebrow\">Preview com o rascunho</span>\n        <div class=\"seg\"><button data-act=\"pv\" data-v=\"mobile\" aria-pressed=\"${s.pv !== 'desktop'}\">${ic(d2, 'Smartphone', 14)}</button><button data-act=\"pv\" data-v=\"desktop\" aria-pressed=\"${s.pv === 'desktop'}\">${ic(d2, 'Monitor', 14)}</button></div>\n      </div>\n      <div class=\"pv-box\" style=\"width:${caixa}px;height:${Math.round(p.h * esc_)}px\">\n        <iframe class=\"pv\" data-key=\"pv-${s.pv || 'mobile'}\" src=\"/preview/${p.ref}\" style=\"width:${p.w}px;height:${p.h}px;transform:scale(${esc_})\"></iframe>\n      </div>\n    </div>`\n}\n\nfunction renderPainel(d2) {\n  const st = d2.state.stats\n  const per = d2.state.periodo ?? 7\n  const seg = `<div class=\"seg\">${[[7, '7 dias'], [30, '30 dias'], [0, 'Tudo']].map(([v, l]) => `<button data-act=\"periodo\" data-v=\"${v}\" aria-pressed=\"${per === v}\">${l}</button>`).join('')}</div>`\n  if (!st) return `<div class=\"painel\">${seg}<div class=\"skeleton sk-row\"></div></div>`\n  const card = (t, v, i, sub) => `<div class=\"kpi\">${ic(d2, i, 16)}<span class=\"eyebrow\">${t}</span><span class=\"display kpi-v\">${v}</span>${sub ? `<span class=\"muted\">${sub}</span>` : ''}</div>`\n  const max = Math.max(1, ...st.funil.map(([, n]) => n))\n  const funil = st.funil.map(([l, n], i) => {\n    const ant = st.funil[i - 1]?.[1]\n    return `<div class=\"fx\"><div class=\"fbar\" style=\"width:${Math.round((n / max) * 100)}%\"></div><span class=\"fl\">${l}</span><span class=\"fn\">${n}${ant ? `<i>${Math.round((n / Math.max(1, ant)) * 100)}%</i>` : ''}</span></div>`\n  }).join('')\n  return `\n    <div class=\"painel\">\n      <div class=\"painel-top\">${seg}<span class=\"muted\">Sessões da suíte de testes (suite-*) ficam fora da conta.</span></div>\n      <div class=\"kpis\">\n        ${card('Sessões', st.sessoes, 'Users')}${card('Looks gerados', st.looks, 'Shirt')}${card('Try-ons gerados', st.tryons, 'ScanFace')}${card('Fotos enviadas', st.fotos, 'Camera')}\n        ${card('Sessões com sacola', st.comCart, 'ShoppingBag', st.pctCart)}${card('Valor adicionado', brl(st.valor), 'Receipt')}${card('Abandonadas', st.abandonadas, 'DoorOpen', st.pctAband)}${card('Sem resposta', st.gaps, 'CircleHelp')}${card('Alertas abertos', st.alertas, 'BellRing', st.alertasCrit ? `${st.alertasCrit} crítico(s)` : '')}\n      </div>\n      <section class=\"box\"><p class=\"eyebrow\">Funil</p><div class=\"funil\">${funil}</div></section>\n      <div class=\"cols\">\n        <section class=\"box\"><p class=\"eyebrow\">Eventos</p>${st.porTipo.map(([k, n]) => `<div class=\"ev\"><span>${esc(k)}</span><span>${n}</span></div>`).join('') || '<p class=\"vazio\">Sem eventos.</p>'}</section>\n        <section class=\"box\"><p class=\"eyebrow\">Sessões recentes</p>${st.recentes.map((x) => `<div class=\"ev\"><span class=\"ev-t\" title=\"${esc(x.sessionId)}\">${esc(x.frase || x.sessionId)}</span><span class=\"row-m\">${x.tryon ? '<span class=\"st\">try-on</span>' : ''}${x.cart ? '<span class=\"st st-publicado\">sacola</span>' : ''}<span class=\"muted\">${quando(x.ultimaEm || x.createdAt)}</span></span></div>`).join('') || '<p class=\"vazio\">Nenhuma.</p>'}</section>\n      </div>\n    </div>`\n}\n\nexport default {\n  render(d2) {\n    d2.lib({ css: THEME })\n    const ui = d2.lib(UI)\n    const s = d2.state\n    if (ui && !s._fonts) { s._fonts = true; ui.ensureFonts() }\n    if (!s.nav) s.nav = 'painel'\n    const tl = tela(d2)\n    const main = tl.k === 'painel' ? renderPainel(d2) : tl.k === 'console' ? renderConsole(d2) : tl.k === 'integracoes' ? renderIntegracoes(d2) : tl.k === 'insights' ? renderInsights(d2) : `<div class=\"work ${tl.preview ? 'com-pv' : ''}\">${renderList(d2)}${renderEditor(d2)}${tl.preview ? renderPreview(d2) : ''}</div>`\n    return `\n      <div class=\"adm\">\n        ${renderSide(d2)}\n        <main class=\"main\">\n          <header class=\"head\"><div><span class=\"eyebrow\">${tl.g}</span><h1 class=\"display head-t\">${tl.t}</h1></div>${tl.tabela ? `<span class=\"muted mono\">${tl.tabela}</span>` : ''}</header>\n          ${main}\n        </main>\n        ${s.toast ? `<div class=\"toast\" role=\"status\">${esc(s.toast)}</div>` : ''}\n      </div>`\n  },\n  mounted(d2) {\n    const s = d2.state\n    // A carga inicial sai daqui: o d2 do primeiro render não é o vivo e o estado gravado por ele não chega na tela.\n    setTimeout(() => abrir(d2, s.nav || 'painel'), 0)\n    window.addEventListener('message', (e) => { if (e.data?.type === 'fps:preview-ready') mandarPreview(d2) })\n    d2.on('[data-act]', 'click', (e) => {\n      const el = e.target.closest('[data-act]')\n      if (!el || el.disabled) return\n      const a = el.dataset.act\n      if (s.dirty && (a === 'nav' || a === 'sel' || a === 'novo') && !s._confirmou) { s._confirmou = true; return toast(d2, 'Há alterações não salvas. Clique de novo para sair sem salvar.') }\n      s._confirmou = false\n      if (a === 'nav') abrir(d2, el.dataset.k)\n      else if (a === 'sel') { if (!s.kbImp?.rodando) s.kbImp = null; selecionar(d2, el.dataset.id) }\n      else if (a === 'novo') selecionar(d2, 'novo')\n      else if (a === 'descartar') selecionar(d2, s.sel)\n      else if (a === 'rascunho') salvar(d2, 'rascunho')\n      else if (a === 'publicar') salvar(d2, 'publicar')\n      else if (a === 'agendar') salvar(d2, 'agendar')\n      else if (a === 'pv') s.pv = el.dataset.v\n      else if (a === 'periodo') { s.periodo = Number(el.dataset.v); abrir(d2, 'painel') }\n      else if (a === 'capa') { s.draft = { ...s.draft, capaUrl: el.dataset.url }; s.dirty = true }\n      else if (a === 'ancora') montarComIA(d2, 'ancora', el.dataset.id)\n      else if (a === 'camp-look') { const ids = s.draft.lookIds || []; const id = el.dataset.id; s.draft = { ...s.draft, lookIds: ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id] }; s.dirty = true }\n      else if (a === 'camp-onde') { const o = s.draft.onde || []; const v = el.dataset.v; s.draft = { ...s.draft, onde: o.includes(v) ? o.filter((x) => x !== v) : [...o, v] }; s.dirty = true }\n      else if (a === 'kb-imp') { s.kbImp = {}; s.draft = null; s.sel = null; carregarFontes(d2) }\n      else if (a === 'kb-imp-fechar') { if (!s.kbImp?.rodando) s.kbImp = null }\n      else if (a === 'kb-abrir') { s.kbImp = null; selecionar(d2, el.dataset.id) }\n      else if (a === 'guia-ia') guiaIA(d2)\n      else if (a === 'cli-ver') verComoStylist(d2)\n      else if (a === 'img-occ') { const o = s.draft.ocasioes || []; const v = el.dataset.v; s.draft = { ...s.draft, ocasioes: o.includes(v) ? o.filter((x) => x !== v) : [...o, v] }; s.dirty = true }\n      else if (a === 'capa-recalc') capasPorOcasiao(d2)\n      else if (a === 'exp-status') { s.draft = { ...s.draft, status: el.dataset.v }; s.dirty = true }\n      else if (a === 'exp-recalc') { s.expMet = null; metricasExperimento(d2, s.draft._id) }\n      else if (a === 'rollback') rollback(d2)\n      else if (a === 'svc-status') { s.draft = { ...s.draft, status: el.dataset.v }; s.dirty = true }\n      else if (a === 'svc-canal') { const c = s.draft.canais || []; const v = el.dataset.v; s.draft = { ...s.draft, canais: c.includes(v) ? c.filter((x) => x !== v) : [...c, v] }; s.dirty = true }\n      else if (a === 'svc-desligar') { s.draft = { ...s.draft, status: 'desligado' }; salvar(d2, 'publicar') }\n      else if (a === 'svc-execucoes') { s.exFiltro = s.draft.key; abrir(d2, 'execucoes') }\n      else if (a === 'ex-limpar') { s.exFiltro = null; abrir(d2, 'execucoes') }\n      else if (a === 'al-resolver') alertaStatus(d2, 'resolvido')\n      else if (a === 'al-reabrir') alertaStatus(d2, 'aberto')\n      else if (a === 'al-verificar') verificarAlertas(d2)\n      else if (a === 'ins-periodo') { s.periodo = Number(el.dataset.v); s.ins = null; insights(d2).then((o) => { s.ins = o }) }\n      else if (a === 'ins-gap') { const id = el.dataset.id; abrir(d2, 'gaps').then(() => selecionar(d2, id)) }\n      else if (a === 'ins-ocasiao') { const t = s.ins?.semOcasiao?.[Number(el.dataset.i)] || ''; abrir(d2, 'ocasioes').then(() => { selecionar(d2, 'novo'); s.draft = { ...s.draft, titulo: t.slice(0, 60), sinonimos: [t] }; s.dirty = true; toast(d2, 'Rascunho de ocasião a partir do pedido; ajuste e publique') }) }\n      else if (a === 'integ-testar') testarIntegracao(d2)\n      else if (a === 'integ-editar') irParaChave(d2, el.dataset.v)\n      else if (a === 'cons-limpar') { s.cons = { pub: [], rasc: [] }; s.consSim = null }\n      else if (a === 'cons-roteiro') { const sim = (s.sims || []).find((x) => x._id === s.consSimId); if (!sim) toast(d2, 'Escolha um cliente simulado'); else rodarRoteiro(d2, sim) }\n      else if (a === 'replay') rodarReplay(d2)\n      else if (a === 'sim-rodar') { const sim = { ...s.draft }; abrir(d2, 'console').then(() => rodarRoteiro(d2, sim)) }\n      else if (a === 'merch-tipo') { s.draft = { ...s.draft, tipo: el.dataset.v }; s.dirty = true }\n      else if (a === 'kb-var') kbIA(d2, 'variantes')\n      else if (a === 'kb-resp') kbIA(d2, 'resposta')\n      else if (a === 'gap-novo') gapAcao(d2, 'novo')\n      else if (a === 'gap-juntar') gapAcao(d2, 'juntar', el.dataset.id)\n      else if (a === 'gap-ignorar') gapAcao(d2, 'ignorar')\n      else if (a === 'gap-reabrir') gapAcao(d2, 'reabrir')\n      else if (a === 'ia-montar') montarComIA(d2, 'montar')\n      else if (a === 'ia-completar') montarComIA(d2, 'completar')\n      else if (a === 'peca-add') { const p = (s.bBusca || []).find((x) => x.productId === el.dataset.id); if (p) { s.draft = { ...s.draft, pecas: [...(s.draft.pecas || []), pecaDe(p, s.bCat || undefined)], heroImage: s.draft.heroImage || p.heroImage || p.image || '' }; s.dirty = true } }\n      else if (a === 'peca-del') { s.draft = { ...s.draft, pecas: (s.draft.pecas || []).filter((_, j) => j !== Number(el.dataset.i)) }; s.dirty = true }\n      else if (a === 'peca-up') { const i = Number(el.dataset.i); const ps = [...(s.draft.pecas || [])]; [ps[i - 1], ps[i]] = [ps[i], ps[i - 1]]; s.draft = { ...s.draft, pecas: ps }; s.dirty = true }\n      else if (a === 'peca-hero') { const p = (s.draft.pecas || [])[Number(el.dataset.i)]; if (p) { s.draft = { ...s.draft, heroImage: p.heroImage || p.image }; s.dirty = true } }\n      else if (a === 'q-add') { s.draft = { ...s.draft, perguntas: [...(s.draft.perguntas || []), { chave: '', pergunta: '', opcoes: [], obrigatoria: false }] }; s.dirty = true }\n      else if (a === 'q-del') { s.draft = { ...s.draft, perguntas: (s.draft.perguntas || []).filter((_, j) => j !== Number(el.dataset.i)) }; s.dirty = true }\n      else if (a === 'q-up') { const i = Number(el.dataset.i); const qs = [...(s.draft.perguntas || [])]; [qs[i - 1], qs[i]] = [qs[i], qs[i - 1]]; s.draft = { ...s.draft, perguntas: qs }; s.dirty = true }\n      else if (a === 'q-tipo') { const i = Number(el.dataset.i); const livre = el.dataset.v === 'livre'; const q = (s.draft.perguntas || [])[i] || {}; mudarPergunta(d2, i, 'livre', livre); if (livre) mudarPergunta(d2, i, 'opcoes', undefined); else mudarPergunta(d2, i, 'opcoes', q.opcoes || []) }\n    })\n    d2.on('form[data-form=\"busca\"]', 'submit', (e) => { e.preventDefault(); buscarPecas(d2) })\n    d2.on('[data-bf]', 'change', (e) => { const el = e.target; const f = el.dataset.bf; if (f === 'q') s.bq = el.value; else if (f === 'cat') s.bCat = el.value; else s.bPreco = el.value ? Number(el.value) : null })\n    d2.on('[data-bt]', 'change', (e) => { s.bTexto = e.target.value })\n    const imp = (o) => (tela(d2).k === 'guia' ? importarGuia(d2, o) : importar(d2, o))\n    d2.on('input.kbfile', 'change', (e) => { const f = e.target.files?.[0]; e.target.value = ''; if (f) imp({ file: f }) })\n    d2.on('form[data-form=\"kburl\"]', 'submit', (e) => { e.preventDefault(); const v = e.target.querySelector('input').value.trim(); if (v) imp({ url: v }) })\n    d2.on('[data-g]', 'change', (e) => { s.guiaNotas = e.target.value })\n    d2.on('form[data-form=\"console\"]', 'submit', (e) => { e.preventDefault(); const inp = e.target.querySelector('input'); const v = inp.value; inp.value = ''; consoleEnviar(d2, v) })\n    d2.on('form[data-form=\"capateste\"]', 'submit', (e) => { e.preventDefault(); s.capaQ = e.target.querySelector('input').value; testarCapa(d2) })\n    d2.on('[data-capa]', 'change', (e) => { if (e.target.dataset.capa === 'occ') s.capaOcc = e.target.value; else s.capaQ = e.target.value })\n    d2.on('form[data-form=\"simtam\"]', 'submit', (e) => { e.preventDefault(); simularTamanho(d2) })\n    d2.on('[data-st]', 'change', (e) => { s.simTam = { ...(s.simTam || {}), [e.target.dataset.st]: e.target.value } })\n    d2.on('[data-integ]', 'change', (e) => { s.integTeste = e.target.value })\n    d2.on('[data-cons]', 'change', (e) => { const k = e.target.dataset.cons; if (k === 'sim') s.consSimId = e.target.value; else if (k === 'canal') s.consCanal = e.target.value; else s.consSo = e.target.checked })\n    d2.on('form[data-form=\"merchteste\"]', 'submit', (e) => { e.preventDefault(); s.mq = e.target.querySelector('input').value; merchTestar(d2) })\n    d2.on('form[data-form=\"guiateste\"]', 'submit', (e) => { e.preventDefault(); s.guiaTeste = e.target.querySelector('input').value; guiaTestar(d2) })\n    d2.on('[data-kb]', 'change', (e) => { const k = e.target.dataset.kb; if (k === 'fonte') s.kbFonte = e.target.value; else if (k === 'teste') s.kbTeste = e.target.value; else s.gapQ = e.target.value })\n    d2.on('form[data-form=\"kbteste\"]', 'submit', (e) => { e.preventDefault(); s.kbTeste = e.target.querySelector('input').value; kbTestar(d2) })\n    d2.on('[data-p]', 'change', (e) => mudarPeca(d2, Number(e.target.dataset.p), e.target.dataset.pf, e.target.value))\n    d2.on('[data-q]', 'change', (e) => {\n      const el = e.target\n      const f = el.dataset.qf\n      const v = f === 'obrigatoria' ? el.checked : f === 'opcoes' ? el.value.split('\\n').map((x) => x.trim()).filter(Boolean).map(opcaoDe) : el.value\n      mudarPergunta(d2, Number(el.dataset.q), f, v)\n    })\n    d2.on('[data-f]', 'change', (e) => { if (!e.target.classList.contains('up')) lerCampo(d2, e.target) })\n    d2.on('input.up', 'change', (e) => { const f = e.target.files?.[0]; if (f) subir(d2, e.target.dataset.f, f) })\n    d2.on('[data-act-input]', 'change', (e) => {\n      const el = e.target\n      if (el.dataset.actInput === 'busca') s.q = el.value\n      else if (el.dataset.actInput === 'arq') s.verArquivados = el.checked\n      else s.agendaEm = el.value\n    })\n  },\n  css: `\n:host { display: block; height: 100%; }\n.adm { display: grid; grid-template-columns: 232px 1fr; height: 100%; background: var(--background); color: var(--foreground); font-size: 14px; }\n.side { border-right: 1px solid var(--border); padding: 20px 12px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; background: var(--muted); }\n.brand { display: flex; flex-direction: column; gap: 2px; padding: 0 8px; }\n.brand-t { font-size: 22px; }\n.nav-g { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted-foreground); padding: 12px 8px 4px; }\n.nav-i { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 8px; border-radius: var(--radius); text-align: left; color: var(--foreground); }\n.nav-i:hover { background: var(--background); }\n.nav-i[aria-current=\"true\"] { background: var(--background); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.06); }\n.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }\n.head { display: flex; align-items: flex-end; justify-content: space-between; padding: 20px 28px 16px; border-bottom: 1px solid var(--border); }\n.head-t { font-size: 26px; }\n.muted { color: var(--muted-foreground); font-size: 12px; }\n.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }\n.work { flex: 1; min-height: 0; display: grid; grid-template-columns: 300px 1fr; }\n.work.com-pv { grid-template-columns: 260px 1fr auto; }\n.list { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }\n.list-top { display: flex; gap: 8px; padding: 12px; border-bottom: 1px solid var(--border); }\n.input-sm { height: 34px; font-size: 13px; }\n.list-top { flex-wrap: wrap; }\n.list-top .input { flex: 1 1 100%; min-width: 0; }\n.list-top .btn { flex: 1; }\n.list-top .chk { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); flex: 1 1 100%; }\n.btn-sm { height: 34px; font-size: 13px; padding: 0 12px; gap: 6px; }\n.rows { overflow-y: auto; flex: 1; }\n.row-i { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; width: 100%; padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); }\n.row-i img { grid-row: 1 / 3; width: 40px; height: 40px; object-fit: cover; border-radius: 6px; }\n.row-i:hover { background: var(--muted); }\n.row-i[aria-current=\"true\"] { background: var(--muted); box-shadow: inset 3px 0 0 var(--primary); }\n.row-t { grid-column: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }\n.row-m { grid-column: 2; display: flex; gap: 6px; align-items: center; margin-top: 2px; }\n.st { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border-radius: 999px; font-size: 11px; background: var(--muted); border: 1px solid var(--border); }\n.st-publicado { background: #e8f3ec; border-color: #b9dcc5; color: #1d5b33; }\n.st-rascunho { background: #fdf3e2; border-color: #f0d6a8; color: #7a4e09; }\n.st-agendado { background: #e9eefb; border-color: #bccaf0; color: #23408e; }\n.editor { display: flex; flex-direction: column; min-width: 0; min-height: 0; }\n.vazio-ed { align-items: center; justify-content: center; }\n.bar { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 20px; border-bottom: 1px solid var(--border); background: var(--background); flex-wrap: wrap; }\n.bar-l, .bar-r, .agenda { display: flex; align-items: center; gap: 8px; }\n.agenda .input { width: 190px; }\n.dot { width: 8px; height: 8px; border-radius: 999px; background: #d4a017; }\n.ed-body { overflow-y: auto; padding: 20px 24px 48px; display: flex; flex-direction: column; gap: 14px; }\n.ed-t { font-size: 22px; }\n.campos { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; }\n.campo { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n.campo.wide { grid-column: 1 / -1; }\n.lbl { font-size: 12px; font-weight: 600; color: var(--muted-foreground); }\n.adm .input { border-radius: 8px; }\n.campo .ta { height: auto !important; min-height: 64px; padding: 8px 10px; line-height: 1.45; resize: vertical; }\n.ta.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }\n.invalid { border-color: #c0392b !important; box-shadow: 0 0 0 2px rgba(192,57,43,.15); }\n.hint { font-size: 12px; color: var(--muted-foreground); }\n.lk { display: flex; flex-direction: column; gap: 16px; }\n.sec { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--radius); }\n.sec.ia { background: var(--muted); }\n.sec-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }\n.sec-t { font-size: 16px; display: inline-flex; align-items: center; gap: 6px; }\n.ptable { display: flex; flex-direction: column; gap: 6px; }\n.prow { display: grid; grid-template-columns: 40px 130px 1fr auto; align-items: center; gap: 10px; padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 13px; }\n.prow img, .pimg { width: 40px; aspect-ratio: 3/4; object-fit: cover; border-radius: 4px; background: var(--muted); }\n.pinfo { display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n.pinfo span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pacts { display: flex; gap: 2px; }\n.pacts .btn[aria-pressed=\"true\"] { color: #b8860b; }\n.filtros { display: flex; gap: 8px; flex-wrap: wrap; }\n.filtros .grow { flex: 1; min-width: 220px; }\n.filtros select { width: 170px; }\n.filtros .preco { width: 110px; }\n.bgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }\n.bitem { display: flex; flex-direction: column; gap: 4px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); font-size: 12px; }\n.bitem img { width: 100%; aspect-ratio: 3/4; object-fit: contain; background: #f6f6f4; border-radius: 6px; }\n.bnome { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.6em; }\n.bacts { display: flex; gap: 4px; margin-top: 4px; }\n.bacts .btn-primary { flex: 1; }\n.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.row > select.input { width: auto; min-width: 200px; flex: 0 0 auto; }\n.capa-g { width: 220px; aspect-ratio: 3/4; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); }\n.diff { display: grid; grid-template-columns: 160px 1fr 1fr; gap: 6px 10px; align-items: start; }\n.diff .dk { font-size: 12px; font-weight: 600; padding-top: 8px; }\n.d-antes { background: #fdecec !important; }\n.d-depois { background: #e8f3ec !important; }\n.pre { white-space: pre-wrap; background: var(--background); border: 1px solid var(--border); border-radius: 8px; padding: 10px; max-height: 320px; overflow: auto; }\n.filtros select { width: auto; min-width: 150px; }\n.row .chk { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }\n.opts { display: flex; flex-wrap: wrap; gap: 6px; }\n.drop { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; padding: 28px; border: 2px dashed var(--border); border-radius: var(--radius); cursor: pointer; color: var(--muted-foreground); font-size: 13px; }\n.drop:hover { border-color: var(--foreground); color: var(--foreground); }\n.drop.off { opacity: .5; pointer-events: none; }\n.sec.err { border-color: #f0b3b3; background: #fdecec; }\n.ebar2 { height: 4px; background: var(--muted); border-radius: 4px; overflow: hidden; }\n.ebar2 i { display: block; height: 100%; background: var(--primary); transition: width .4s; }\n.spinner { display: inline-block; width: 14px; height: 14px; border-radius: 999px; border: 2px solid currentColor; border-right-color: transparent; animation: fps-spin .8s linear infinite; vertical-align: -2px; }\n@keyframes fps-spin { to { transform: rotate(360deg); } }\n.ev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }\n.ta.md { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.55; }\n.trecho { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--border); }\n.kbres { display: flex; flex-direction: column; gap: 6px; font-size: 13px; }\n.ev-l { display: flex; flex-direction: column; }\n.ev-l .ev { align-items: center; }\n.gap-q { font-family: var(--font-display); font-size: 20px; line-height: 1.3; }\n.chip { height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 999px; font-size: 13px; background: var(--background); }\n.chip[aria-pressed=\"true\"] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }\n.lkgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }\n.lkc { position: relative; display: flex; flex-direction: column; gap: 4px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); text-align: left; font-size: 12px; }\n.lkc img, .lkc .pimg { width: 100%; aspect-ratio: 3/4; object-fit: cover; border-radius: 6px; background: var(--muted); }\n.lkc[aria-pressed=\"true\"] { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(0,0,0,.06); }\n.lkc-t { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.lkc .ord { position: absolute; top: 6px; left: 6px; width: 22px; height: 22px; border-radius: 999px; background: var(--primary); color: var(--primary-foreground); font-style: normal; font-size: 11px; display: flex; align-items: center; justify-content: center; }\n.grupo { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px 14px; margin: 0; min-width: 0; }\n.grupo > legend { padding: 0 6px; }\n.grupo .campos { gap: 10px 16px; }\n.cor { display: flex; gap: 8px; align-items: center; }\n.cor input[type=\"color\"] { width: 40px; height: 40px; padding: 2px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }\n.cor .input { flex: 1; }\n.invalid-txt { color: #8a1c1c; }\n.ok-txt { color: #1d5b33; }\n.img-row { display: flex; align-items: center; gap: 10px; }\n.thumb { width: 96px; height: 64px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); }\n.qs { display: flex; flex-direction: column; gap: 10px; }\n.qcard { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--muted); }\n.qrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n.qrow .qk { width: 120px; }\n.qrow .grow { flex: 1; min-width: 200px; }\n.qcard .ta { background: var(--background); }\n.pick { display: flex; gap: 8px; flex-wrap: wrap; }\n.pick img { width: 72px; height: 96px; object-fit: cover; border-radius: 6px; border: 2px solid transparent; cursor: pointer; }\n.pick img[aria-pressed=\"true\"] { border-color: var(--primary); }\n.sw { position: relative; display: inline-block; width: 40px; height: 22px; }\n.sw input { opacity: 0; width: 0; height: 0; }\n.sw span { position: absolute; inset: 0; border-radius: 999px; background: var(--border); transition: background .15s; cursor: pointer; }\n.sw span::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 999px; background: #fff; transition: transform .15s; }\n.sw input:checked + span { background: var(--primary); }\n.sw input:checked + span::after { transform: translateX(18px); }\n.preview { border-left: 1px solid var(--border); padding: 14px 18px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; background: var(--muted); }\n.pv-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }\n.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--background); }\n.seg button { padding: 6px 10px; }\n.seg button[aria-pressed=\"true\"] { background: var(--primary); color: var(--primary-foreground); }\n.pv-box { position: relative; overflow: hidden; border-radius: 14px; box-shadow: 0 12px 32px rgba(0,0,0,.15); background: #fff; }\n.pv { position: absolute; top: 0; left: 0; border: 0; transform-origin: 0 0; }\n.msgs { display: flex; flex-direction: column; gap: 10px; max-width: 720px; }\n.msg { padding: 10px 14px; border-radius: var(--radius); border: 1px solid var(--border); display: flex; flex-direction: column; gap: 4px; }\n.msg.eu { background: var(--muted); }\n.msg p { overflow-wrap: anywhere; }\n.ev-l { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.ev-d pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 240px; overflow: auto; font-size: 11px; margin: 4px 0 0; }\n.blks { display: flex; gap: 6px; flex-wrap: wrap; }\n.blk { display: flex; gap: 10px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius); font-size: 12px; max-width: 360px; background: var(--background); }\n.blk img { width: 40px; aspect-ratio: 3/4; object-fit: cover; border-radius: 4px; flex: 0 0 auto; }\n.blk > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; }\n.blk .st { align-self: flex-start; }\n.painel-top { display: flex; align-items: center; gap: 14px; }\n.funil { display: flex; flex-direction: column; gap: 6px; }\n.fx { position: relative; height: 30px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; font-size: 13px; }\n.fbar { position: absolute; left: 0; top: 0; bottom: 0; background: var(--muted); border-radius: 6px; }\n.fl, .fn { position: relative; }\n.fn i { font-style: normal; margin-left: 8px; color: var(--muted-foreground); font-size: 11px; }\n.ev-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }\n.painel { padding: 24px 28px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; }\n.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }\n.kpi { display: flex; flex-direction: column; gap: 6px; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); }\n.kpi-v { font-size: 30px; }\n.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\n.box { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; gap: 8px; }\n.ev { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 13px; }\n.vazio { padding: 16px; color: var(--muted-foreground); font-size: 13px; }\n.sk-row { height: 44px; margin: 10px 12px; }\n.btn[disabled] { opacity: .5; cursor: not-allowed; }\n.toast { position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 30; padding: 10px 16px; border-radius: var(--radius); background: var(--foreground); color: var(--background); font-size: 13px; }\n`,\n}\n"}},"api":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"api"}}}},"assets":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"assets"}}}}}}}}}},{"_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":{"component":"data2-core-flow@If","displayName":"management exception never picks the system","params":{"compare":{"component":"data2-core-logical-operators@Or","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}}},{"component":"data2-core-logical-operators@Not","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}}}}}]}},"then":{"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"else":{"d2_app":"management"}}},"else":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"},"compare":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"has_access"}}}}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"sessionExpiresIn":{"component":"data2-core-primitives@Number","params":{"content":604800}},"sessionRenewal":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"identityProviders":[{"provider":{"component":"data2-core-primitives@String","params":{"content":"GOOGLE"}},"clientId":{"component":"data2-core-primitives@String","params":{"content":"55470861603-22pvfc1t49lp8cjnbfk71s5ct9hp1q1v.apps.googleusercontent.com"}}}]},"reference":"Data2BuilderAuthentication","updatedAt":"2026-10-19T16:23:02.013Z","updateMetadata":{"updatedBy":{"user":"5f1609be1ca96def427e1559"}}},{"_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"},{"_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"},{"component":"data2-core@DataSet","reference":"SessionsDataSet","params":{"requiresConfirmation":false,"pk":"key","datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"data2sessionsdatasource"}}}},{"component":"data2-core@ServerSessionsDataSource","reference":"data2sessionsdatasource","params":{}},{"reference":"UsersTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"_id":"5f1600b70528b92601890f75","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ServiceComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ServiceComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:20.208Z","createdAt":"2020-07-20T20:38:15.436Z"},{"_id":"5f1602190528b92601890f7d","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":40000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentInstancesTableDataSource"}},"sort":{"_id":1},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:07.089Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"5f16017f0528b92601890f7a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"FileComponentInstancesDataSet","updatedAt":"2023-08-03T16:08:18.193Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"5f1601990528b92601890f7b","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ApplicationsTableDataSource"}},"pk":"_id","autoEdit":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"params":{"column":"_id"},"component":"data2-core-types@DataSetColumnReference"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"ApplicationsDataSet","updatedAt":"2022-12-13T21:51:13.995Z","createdAt":"2020-07-20T20:42:01.657Z"},{"_id":"5f16172a0528b92601890f85","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":10000}}},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"DataComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"DataComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:12.831Z","createdAt":"2020-07-20T22:14:02.097Z"},{"_id":"60ddc1bbda3861ca65407c5a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"pagination":{"size":1},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"}},"reference":"OneFileComponentInstancesDataSet","updatedAt":"2022-02-09T19:44:38.036Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"63ee3d38b8c9838c62fb05fb","displayName":"DriveFileComponentInstancesDataSet","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core@Drive"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"DriveFileComponentInstancesDataSet","updatedAt":"2025-12-04T21:58:32.367Z","createdAt":"2023-02-16T14:27:07.797Z"},{"reference":"ServiceComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6ad51734079b8ca6f5a2ae88","component":"data2-rush@HTTPListener","reference":"fps-looks-salvos-api","system":"69133afb854168f247b6f9af","params":{}},{"_id":"6ad3da29354e1a868a578924","component":"data2-rush@HTTPListener","reference":"fps-evento","system":"69133afb854168f247b6f9af","params":{}},{"_id":"6a1794b7185f1a3077726ca5","component":"data2-rush@HTTPListener","reference":"get-generated-image-aramis-shadow","system":"69133afb854168f247b6f9af","params":{}},{"_id":"6acfd61e37f71ecd10ae8cf0","component":"data2-rush@HTTPListener","reference":"fps-public-config","system":"69133afb854168f247b6f9af","params":{}},{"_id":"6acfe4de6bccff2e22d1eeec","component":"data2-rush@HTTPListener","reference":"fps-conversa","system":"69133afb854168f247b6f9af","params":{}}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6acfea5a364521922265f9ce","component":"data2-core-primitives@Artboard","system":"69133afb854168f247b6f9af","displayName":"FPS · Shell (mobile)","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"FPS · Shell (mobile)"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":390,"height":844}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"IiAQaSG6IlFKETXBGGxpw"},{"_id":"6acfea5937f71ecd10aecdf4","component":"data2-core-primitives@Artboard","system":"69133afb854168f247b6f9af","displayName":"FPS · Shell (desktop)","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"FPS · Shell (desktop)"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1440,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"VjF08qLzRZr_eYehfqaIh"},{"_id":"540efb72aa50acff612cc84a","parent":"6acfea5a364521922265f9ce","component":"aramis@fpsShell","system":"69133afb854168f247b6f9af","displayName":"fps-shell","params":{"conversa":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-conversa"}},"publicConfig":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-public-config"}},"provador":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"get-generated-image-aramis-shadow"}},"fotos":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"30D9qsESixfKBtWQCXFyn"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"growH":true,"growV":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":390,"height":844}}},"evento":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-evento"}},"salvos":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-looks-salvos-api"}}},"reference":"ZlcvJ1VPVt3qUDeDwLKpO"},{"_id":"c7dde8c37429c2a5c80b1538","parent":"6acfea5937f71ecd10aecdf4","component":"aramis@fpsShell","system":"69133afb854168f247b6f9af","displayName":"fps-shell","params":{"conversa":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-conversa"}},"publicConfig":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-public-config"}},"provador":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"get-generated-image-aramis-shadow"}},"fotos":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"30D9qsESixfKBtWQCXFyn"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"growH":true,"growV":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1440,"height":900}}},"evento":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-evento"}},"salvos":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"fps-looks-salvos-api"}}},"reference":"ZI_UDo7_fG2bRHcNQYQ4J"}]}},{"_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":"6ad7dac9c719c72557a0cfc7","displayName":"FPS · Personal Shopper (v2)","reference":"VzdGyK13AmMB4czWI0mXH","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"fps"}},"entryPoint":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"IiAQaSG6IlFKETXBGGxpw"}}},"system":"69133afb854168f247b6f9af","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6a231ca461af3725c6de1556","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"look","schema":[{"component":"data2-core-types@Field","params":{"key":"name","title":"Name","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"owner","title":"Owner","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"title","title":"Title","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"subtitle","title":"Subtitle","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"rationale","title":"Rationale","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"hero_image","title":"Hero Image","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"total_price","title":"Total Price","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"pieces","title":"Pieces","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"source_query","title":"Source Query","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"occasion","title":"Occasion","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"formality","title":"Formality","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"intent","title":"Intent","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Created At","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"Updated At","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"inspiration_image","title":"Inspiration Image","component":"data2-core-primitives@String"}}]},"reference":"look"},{"_id":"6acfd87b3268db3abaf3e2d6","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-sessions","schema":[{"component":"data2-core-types@Field","params":{"key":"sessionId","title":"Sessão","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tenant","title":"Tenant","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"canal","title":"Canal","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"customerId","title":"Cliente","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ocasiao","title":"Ocasião declarada","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"iniciadaEm","title":"Iniciada em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"ultimaEm","title":"Última interação","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"turnos","title":"Turnos","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"tokens","title":"Tokens","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"configVersao","title":"Versão da config usada","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ultimosLooks","title":"Últimos looks mostrados","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"deviceId","title":"Device","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"experimento","title":"Experimento {id, braco, versao}","component":"data2-core-primitives@Object"}}]},"reference":"fps-sessions"},{"_id":"6acfd82ac3e8fc00c0916620","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-audit","schema":[{"component":"data2-core-types@Field","params":{"key":"tabela","title":"Tabela","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"registroId","title":"Registro","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"acao","title":"Ação (criar | editar | publicar | arquivar)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"antes","title":"Antes","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"depois","title":"Depois","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"por","title":"Por","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"origem","title":"Origem (admin | agente | migracao)","component":"data2-core-primitives@String"}}]},"reference":"fps-audit"},{"_id":"6ad7ac81cd0f237d894c6dca","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-alertas","schema":[{"component":"data2-core-types@Field","params":{"key":"chave","title":"Chave (tipo:alvo:dia, evita duplicar)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (orcamento | toolFalhando | semResposta | campanhaSemEstoque)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"nivel","title":"Nível (aviso | critico)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"mensagem","title":"Mensagem","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"valor","title":"Valor medido","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"limite","title":"Limite","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"detalhe","title":"Detalhe","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (aberto | resolvido)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"resolvidoEm","title":"Resolvido em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"ultimaVez","title":"Última verificação que viu","component":"data2-core-primitives@Date"}}]},"reference":"fps-alertas"},{"_id":"6acfd5bc6834035a9ac99d4c","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-tenant","schema":[{"component":"data2-core-types@Field","params":{"key":"slug","title":"Slug","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"productName","title":"Nome do produto","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"theme","title":"Tema (variáveis --fps-*)","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"logoUrl","title":"Logo","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"imagens","title":"Imagens (abertura, capa padrão, capas por ocasião)","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"sugestoesAbertura","title":"Sugestões da abertura","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"copy","title":"Textos de interface","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"catalogSource","title":"Fonte de catálogo","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}}]},"reference":"fps-tenant"},{"_id":"6acfd82b86ba87b276bcae9b","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-persona","schema":[{"component":"data2-core-types@Field","params":{"key":"tenant","title":"Tenant","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome do stylist","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"guiaTom","title":"Guia de tom","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"faca","title":"Faça","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"naoFaca","title":"Não faça","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"maxLegenda","title":"Tamanho máximo da legenda (caracteres)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"idioma","title":"Idioma","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":"versao","title":"Versão","component":"data2-core-primitives@Number"}}]},"reference":"fps-persona"},{"_id":"6acfd82d16c0cadc5706cf5e","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-persona-exemplos","schema":[{"component":"data2-core-types@Field","params":{"key":"situacao","title":"Situação","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"respostaBoa","title":"Resposta boa","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"respostaRuim","title":"Resposta ruim","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"origem","title":"Origem","component":"data2-core-primitives@String"}}]},"reference":"fps-persona-exemplos"},{"_id":"6acfd83286ba87b276bcaea4","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-occasions","schema":[{"component":"data2-core-types@Field","params":{"key":"slug","title":"Slug","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"titulo","title":"Título","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"sinonimos","title":"Sinônimos","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"dressCode","title":"Dress code","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"papeisObrigatorios","title":"Papéis obrigatórios no look","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"capaUrl","title":"Capa","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"climaImporta","title":"Clima importa","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"sugestoesRefino","title":"Sugestões de refino","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"perguntas","title":"Perguntas antes de sugerir","component":"data2-core-primitives@Array"}}]},"reference":"fps-occasions"},{"_id":"6acfd82816c0cadc5706cf4e","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-config","schema":[{"component":"data2-core-types@Field","params":{"key":"chave","title":"Chave","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"valor","title":"Valor","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"versao","title":"Versão","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (rascunho | publicado | agendado | arquivado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"publicarEm","title":"Publicar em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"nota","title":"Nota da mudança","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"updatedBy","title":"Atualizado por","component":"data2-core-primitives@String"}}]},"reference":"fps-config"},{"_id":"6acfd5baa435d1e33d00926b","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-blocks","schema":[{"component":"data2-core-types@Field","params":{"key":"type","title":"Tipo","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"versao","title":"Versão do contrato","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"descricao","title":"Descrição (o que o modelo lê)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"exemplo","title":"Exemplo tipado","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"obrigatorios","title":"Campos obrigatórios","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"grupo","title":"Grupo (base | extra)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"canais","title":"Canais","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"ordem","title":"Ordem","component":"data2-core-primitives@Number"}}]},"reference":"fps-blocks"},{"_id":"6acfd8386bccff2e22d1c5ee","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-curated-looks","schema":[{"component":"data2-core-types@Field","params":{"key":"titulo","title":"Título","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"legenda","title":"Legenda","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ocasioes","title":"Ocasiões (slugs)","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"publico","title":"Público","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"pecas","title":"Peças ({productId, ref, role})","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"heroImage","title":"Foto herói","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":"criadoPor","title":"Criado por","component":"data2-core-primitives@String"}}]},"reference":"fps-curated-looks"},{"_id":"6acfd83a3268db3abaf3e1f6","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-campaigns","schema":[{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"lookIds","title":"Looks","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"inicio","title":"Início","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"fim","title":"Fim","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"prioridade","title":"Prioridade","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"onde","title":"Onde aparece (capa | primeira-pagina | sugestao)","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}}]},"reference":"fps-campaigns"},{"_id":"6acfd83616c0cadc5706cf6e","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-merch-rules","schema":[{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (destaque | exclusao | estoqueMinimo | faixaPreco | margem | novidade)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"alvo","title":"Alvo ({colecao | categoria | sku | ref})","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"valor","title":"Valor","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"prioridade","title":"Prioridade","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"inicio","title":"Início","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"fim","title":"Fim","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"peso","title":"Peso (destaque/novidade)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"tamanhos","title":"Tamanhos disponíveis mínimos","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"precoMin","title":"Preço mínimo","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"precoMax","title":"Preço máximo","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"dias","title":"Dias de novidade","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"nota","title":"Nota","component":"data2-core-primitives@String"}}]},"reference":"fps-merch-rules"},{"_id":"6ad10d48f8eb9f06e956a8ae","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-images","schema":[{"component":"data2-core-types@Field","params":{"key":"url","title":"URL","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"titulo","title":"Título","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"descricao","title":"Descrição (o que a busca lê)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (capa | abertura)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ocasioes","title":"Ocasiões (slugs)","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"estilo","title":"Estilo","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"clima","title":"Clima / luz","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"fonte","title":"Fonte","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"licenca","title":"Licença","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tenant","title":"Tenant","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}}]},"reference":"fps-images"},{"_id":"6ad6597e33ace932bc1588ae","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-guia","schema":[{"component":"data2-core-types@Field","params":{"key":"titulo","title":"Título","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (tecido | estilo | ocasiao | cuidado | colecao | outro)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tags","title":"Tags (tecidos, ocasiões, categorias de peça)","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"texto","title":"Texto (Markdown)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"fonte","title":"Fonte (arquivo, link ou origem)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (rascunho | publicado | agendado | arquivado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"publicarEm","title":"Publicar em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"trechos","title":"Trechos indexados","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"indexadoEm","title":"Indexado em","component":"data2-core-primitives@Date"}}]},"reference":"fps-guia"},{"_id":"6acfd85a5d637f38c7d89643","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-kb-articles","schema":[{"component":"data2-core-types@Field","params":{"key":"pergunta","title":"Pergunta","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"variantes","title":"Outras formas de perguntar","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"resposta","title":"Resposta","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"titulo","title":"Título","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"categoria","title":"Categoria","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"fonte","title":"Fonte","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (rascunho | publicado | arquivado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"versao","title":"Versão","component":"data2-core-primitives@Number"}}]},"reference":"fps-kb-articles"},{"_id":"6acfd8608eeac4a4b819c3b2","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-kb-gaps","schema":[{"component":"data2-core-types@Field","params":{"key":"pergunta","title":"Pergunta","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"ocorrencias","title":"Ocorrências","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (aberta | virou-artigo | ignorada)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ultimaSessao","title":"Última sessão","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"melhorScore","title":"Score do artigo mais próximo","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"melhorArtigo","title":"Artigo mais próximo","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"artigoId","title":"Artigo criado ou que absorveu a pergunta","component":"data2-core-primitives@String"}}]},"reference":"fps-kb-gaps"},{"_id":"6ad75d7acd0f237d894b97ad","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-sim-clientes","schema":[{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"descricao","title":"Quem é","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"perfil","title":"Perfil de fixture (tamanhos, medidas, preferências)","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"roteiro","title":"Mensagens em ordem","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"esperado","title":"O que uma boa conversa entrega","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}}]},"reference":"fps-sim-clientes"},{"_id":"6ad7b49941232854da23fd59","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-experimentos","schema":[{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"alvo","title":"Alvo (persona)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"versaoA","title":"Versão A (controle)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"versaoB","title":"Versão B","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"pctB","title":"% das sessões novas no B","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (rascunho | ativo | encerrado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"inicio","title":"Início","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"fim","title":"Fim","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"nota","title":"Hipótese","component":"data2-core-primitives@String"}}]},"reference":"fps-experimentos"},{"_id":"6acfd86386ba87b276bcaf67","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-customers","schema":[{"component":"data2-core-types@Field","params":{"key":"customerId","title":"Cliente","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Primeiro nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"segmento","title":"Segmento","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"consentimento","title":"Consentimento","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"perfil","title":"Perfil (tamanhos, preferências, medidas)","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"fonte","title":"Fonte (fixture | crm)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"atualizadoEm","title":"Atualizado em","component":"data2-core-primitives@Date"}}]},"reference":"fps-customers"},{"_id":"6acfd8623268db3abaf3e294","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-size-charts","schema":[{"component":"data2-core-types@Field","params":{"key":"categoria","title":"Categoria","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tamanho","title":"Tamanho","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"medidas","title":"Medidas (cm)","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"fonte","title":"Fonte","component":"data2-core-primitives@String"}}]},"reference":"fps-size-charts"},{"_id":"6acfd8563268db3abaf3e25f","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-services","schema":[{"component":"data2-core-types@Field","params":{"key":"key","title":"Chave (como o motor chama)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"descricao","title":"Descrição (o modelo decide chamar lendo isto)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"exemplos","title":"Exemplos de frase","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (ativo | instavel | desligado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"executor","title":"Listener executor","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"efeito","title":"Efeito (leitura | escrita)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"timeoutMs","title":"Timeout (ms)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"canais","title":"Canais","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"parametros","title":"Parâmetros (exemplo tipado)","component":"data2-core-primitives@Object"}}]},"reference":"fps-services"},{"_id":"6acfd858364521922265c250","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-service-runs","schema":[{"component":"data2-core-types@Field","params":{"key":"serviceKey","title":"Serviço","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (sucesso | erro | desligado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"canal","title":"Canal","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"sessionId","title":"Sessão","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"duracaoMs","title":"Duração (ms)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"erro","title":"Erro","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"entrada","title":"Entrada (recorte)","component":"data2-core-primitives@Object"}}]},"reference":"fps-service-runs"},{"_id":"6acfd8656834035a9ac9a66b","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-managers","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ativo","title":"Ativo","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"ultimoAcesso","title":"Último acesso","component":"data2-core-primitives@Date"}}]},"reference":"fps-managers"},{"_id":"6acfd87c6bccff2e22d1c6df","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-messages","schema":[{"component":"data2-core-types@Field","params":{"key":"sessionId","title":"Sessão","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"turnId","title":"Turno","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"papel","title":"Papel (cliente | stylist | evento)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"texto","title":"Texto","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"blocos","title":"Blocos","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"evento","title":"Evento de widget","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"tools","title":"Tools chamadas","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"tokens","title":"Tokens","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"latenciaMs","title":"Latência (ms)","component":"data2-core-primitives@Number"}}]},"reference":"fps-messages"},{"_id":"6acfd87e37f71ecd10ae951f","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-events","schema":[{"component":"data2-core-types@Field","params":{"key":"sessionId","title":"Sessão","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (look_viewed | tryon_done | look_added_to_cart | size_advised | occasion_declared)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"lookId","title":"Look","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"origemLook","title":"Origem do look (curado | gerado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"productIds","title":"Produtos","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"valor","title":"Valor (R$)","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"canal","title":"Canal","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"customerId","title":"Cliente","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"deviceId","title":"Device","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tenant","title":"Tenant","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"payload","title":"Payload do evento","component":"data2-core-primitives@Object"}}]},"reference":"fps-events"},{"_id":"6ad654fb2ac18dc3fcbcdecf","component":"data2-core@Data2TableDataSource","system":"69133afb854168f247b6f9af","params":{"table":"fps-kb-sources","schema":[{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nome do arquivo ou título","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (pdf | docx | md | txt | gdoc | url)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"url","title":"Link de origem (Google Docs, página)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"arquivoUrl","title":"Arquivo original no drive","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"chars","title":"Caracteres extraídos","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"artigos","title":"Artigos criados","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"artigoIds","title":"Ids dos artigos criados","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status (processando | ok | erro)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"erro","title":"Erro","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"por","title":"Importado por","component":"data2-core-primitives@String"}}]},"reference":"fps-kb-sources"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}