/** Percurso opcional. Usa a sessão, biblioteca e carteira da conta. */
/** Os formatos de painel escolhidos que estão virados para a rua (dito em Lojas e painéis). */
// Só os painéis da loja que não partilham a medida com as redes: esses fazem o mesmo vídeo que elas (revisor, 4 Nov).
// A família da composição, como em lib/video/motion/verificar.js (familiaMotion): medidas da mesma família fazem o
// mesmo desenho do vídeo e não podem ter chamadas diferentes (ronda 12: 1920×1080 e 512×768 são as duas «bloco»).
function familiaCriar(f){const [l,a]=String(f).split('x').map(Number),r=l/a;return r>=2?'faixa':r<=.35?'poster':r<=.65?'9:16':'bloco'}
/** Painéis dos dois lados (uns na rua, outros dentro): aí cada painel diz o seu lado, e o pedido não muda sozinho para «rua» (ronda 14). */
function paineisMistosCriar(config){const r=(config?.formatos||[]).filter(f=>(f.origem==='painel'||f.painel)&&typeof f.rua==='boolean').map(f=>f.rua);return r.includes(true)&&r.includes(false)}
function paineisDentroCriar(brief,config){return (config?.formatos||[]).filter(f=>(f.origem==='painel'||f.painel)&&f.rua!==true&&(brief?.formatos||[]).includes(f.formato)).map(f=>f.formato)}
/** As famílias de desenho que também vão para as redes: aí o painel faz o mesmo vídeo que as redes (ronda 14). */
function familiasSociaisCriar(brief,config){return new Set((brief?.formatos||[]).filter(f=>(brief.usos?.[f]||config?.formatos?.find(m=>m.formato===f)?.canais||[(f==='1080x1920'||f==='1080x1080')?'social':'dooh']).includes('social')).map(familiaCriar))}
function paineisNaRuaCriar(brief,config){const rua=(config?.formatos||[]).filter(f=>f.origem==='painel'&&!f.social&&!f.painel&&f.rua===true&&(brief?.formatos||[]).includes(f.formato)).map(f=>f.formato),dentro=paineisDentroCriar(brief,config),sociais=familiasSociaisCriar(brief,config);return rua.filter(f=>!dentro.some(g=>familiaCriar(g)===familiaCriar(f))&&!sociais.has(familiaCriar(f)))}
/** Os da rua com o desenho das redes sociais: o mesmo vídeo, a mesma chamada (ronda 14, a montra do Rui). */
function paineisRuaComoRedesCriar(brief,config){const sociais=familiasSociaisCriar(brief,config);return (config?.formatos||[]).filter(f=>f.origem==='painel'&&f.rua===true&&(brief?.formatos||[]).includes(f.formato)&&sociais.has(familiaCriar(f.formato))).map(f=>f.formato)}
/** Os da rua que partilham o desenho com um de dentro: para dizer porque não há chamada própria. */
function paineisRuaPartilhadosCriar(brief,config){const dentro=paineisDentroCriar(brief,config);return (config?.formatos||[]).filter(f=>f.origem==='painel'&&f.rua===true&&(brief?.formatos||[]).includes(f.formato)&&dentro.some(g=>familiaCriar(g)===familiaCriar(f.formato))).map(f=>f.formato)}
function briefInicialCriar(){return {oferta:{tipo:'produto',nome:''},informacao:'',objetivo:'Dar a conhecer',modo:'criar',mensagem:'',cta:'',tom:'Sugerido a partir dos materiais',duracao:15,somSocial:true,formatos:['1080x1920']}}
// Frases de loja online (portes, envios…) propostas «só redes». Cópia de SO_LOJA_ONLINE em
// lib/criar/painel.js; tests/criar-painel.test.js confere que são iguais (14 Out).
const SO_LOJA_ONLINE_CRIAR=[/\b(?:portes\s+(?:gr[aá]tis|gratuitos?|de\s+envio|inclu[ií]dos?|oferecidos?|pagos)|envios?|enviamos|entregas?\s+(?:ao\s+domic[ií]lio|em\s+casa|gr[aá]tis|gratuitas?|r[aá]pidas?|expresso|em\s+\d{1,2}\s*h(?:oras)?)|(?:encomendas?|compras?|compre|encomende|encomenda)\s+(?:j[aá]\s+)?online|online\s+com|loja\s+online|checkout|c[oó]digos?\s+(?:promocional|promocionais|de\s+desconto)|cup[aã]o|cup[oõ]es|link\s+na\s+bio|clica|clique|carrinho\s+de\s+compras|[a-z0-9-]+\.(?:pt|com))\b/iu,/\b[cC](?:[oó]digo|[OÓ]DIGO)\s+[A-Z0-9]{3,}\b/u];
const soLojaOnlineCriar=t=>SO_LOJA_ONLINE_CRIAR.some(r=>r.test(t));
// As marcas «só redes» seguem a frase, não a posição (cliente simulado, 14 Out): as linhas iguais
// ligam-se pela maior subsequência comum; uma linha que mudou de sítio leva a sua marca; num troço
// alterado, a linha editada herda a da linha que substituiu; só uma linha nova usa a regra.
function marcasCriar(antes,novas,marcas,rua=false){
 const n=antes.length,m=novas.length,L=Array.from({length:n+1},()=>Array(m+1).fill(0));
 for(let i=n-1;i>=0;i--)for(let j=m-1;j>=0;j--)L[i][j]=antes[i]===novas[j]?L[i+1][j+1]+1:Math.max(L[i+1][j],L[i][j+1]);
 const r=Array(m),trocos=[];let i=0,j=0,t={apagadas:[],novas:[]};
 const fechar=()=>{if(t.apagadas.length||t.novas.length)trocos.push(t);t={apagadas:[],novas:[]}};
 while(i<n||j<m){if(i<n&&j<m&&antes[i]===novas[j]){fechar();r[j]=Boolean(marcas[i]);i++;j++}else if(j<m&&(i>=n||L[i][j+1]>=L[i+1][j]))t.novas.push(j++);else t.apagadas.push(i++)}
 fechar();const livres=new Set(trocos.flatMap(t=>t.apagadas));
 for(const t of trocos)for(const k of t.novas){const a=[...livres].find(x=>antes[x]===novas[k]);if(a!==undefined){r[k]=Boolean(marcas[a]);livres.delete(a)}}
 for(const t of trocos){const sobra=t.apagadas.filter(x=>livres.has(x));for(const k of t.novas.filter(k=>r[k]===undefined)){const a=sobra.shift();if(a!==undefined){r[k]=Boolean(marcas[a]);livres.delete(a)}else r[k]=!rua&&soLojaOnlineCriar(novas[k])}}
 return r;
}
// Os tipos dos dados críticos em português, e sem os identificadores internos das imagens que a
// leitura da IA às vezes cita no resumo, «(id :0)» (cliente simulado, 14 Out).
const TIPOS_DADO_CRIAR={preco:'o preço',desconto:'o desconto',data:'a data',condicao:'a condição',alegacao:'a alegação'};
const semIdsCriar=s=>String(s||'').replace(/\s*\(\s*ids?\s*:\s*[\w-]+(?:\s*,\s*[\w-]+)*\s*\)/gi,'');
// Só mudar formatos ou destinos conserva as edições da aprovação aberta; qualquer outro campo descarta-as.
// Onde estão os painéis também conserva: a escolha está ao lado dos destinos (3 Nov).
function edicoesAoAlterarCriar(campo,guardadas,proposta,estado){return ['formatos','usos','ondePainel'].includes(campo)?(proposta?estado:guardadas):null}
// Algum formato vai para painel? Com o mesmo destino por omissão que a entrada envia ao servidor.
function temPainelCriar(brief,config){return (brief.formatos||[]).some(f=>(brief.usos?.[f]||[config?.formatos?.find(m=>m.formato===f)?.canais?.[0]||(f==='1080x1920'||f==='1080x1080'?'social':'dooh')]).includes('dooh'))}
const linhasCriar=v=>String(v||'').split('\n').map(s=>s.trim()).filter(Boolean);
// Uma promoção com o objectivo por omissão saía «dar a conhecer»; quem não mexeu no objectivo quer vender (agentes, 20 Out).
const objetivoCriar=(tipo,objetivo)=>tipo==='promocao'&&objetivo==='Dar a conhecer'?'Vender uma oferta':objetivo;
const quandoCriar=d=>d?new Date(d).toLocaleString('pt-PT',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'}):'';
function chaveRascunhoCriar(dono,slug){return /^(cliente|netscreen):[a-f0-9-]{36}$/.test(dono||'')&&/^[a-z0-9-]{1,80}$/.test(slug||'')?'netscreen-criar-rascunho:v1:'+dono+':'+slug:null}
function dadosRascunhoCriar({brief,fontes,automaticos,leituraChave},agora=Date.now()){
 const curto=(v,n)=>String(v||'').slice(0,n),b=brief||{},of=b.oferta||{},uuid=v=>/^[a-f0-9-]{36}$/.test(v||'')?v:'';
 const fs=(fontes||[]).slice(0,12).map(f=>({tipo:curto(f.tipo,20),papel:curto(f.papel,20),recibo:uuid(f.recibo),ficheiro:uuid(f.ficheiro),titulo:curto(f.titulo,180)})).filter(f=>f.recibo||f.ficheiro);
 const limpo={...briefInicialCriar(),oferta:{tipo:curto(of.tipo,20),nome:curto(of.nome,180)},informacao:curto(b.informacao,12000),objetivo:curto(b.objetivo,200),modo:curto(b.modo,20),mensagem:curto(b.mensagem,600),cta:curto(b.cta,180),tom:curto(b.tom,100),duracao:[15,20,25].includes(b.duracao)?b.duracao:15,somSocial:b.somSocial!==false,formatos:(b.formatos||[]).filter(f=>/^\d{2,5}x\d{2,5}$/.test(f)).slice(0,8)};
 if(b.ondePainel==='rua')limpo.ondePainel='rua';
 if(b.usos)limpo.usos=Object.fromEntries(limpo.formatos.filter(f=>Array.isArray(b.usos[f])).map(f=>[f,b.usos[f].filter(c=>['social','dooh'].includes(c))]));
 return {versao:1,guardadoEm:agora,brief:limpo,fontes:fs,automaticos:Object.keys(automaticos||{}).filter(k=>['oferta','mensagem','cta'].includes(k)),leituraChave:/^[a-f0-9-]{36,64}$/.test(leituraChave||'')?leituraChave:null};
}
function lerRascunhoCriar(storage,chave,agora=Date.now()){
 if(!chave)return null;const dono=chave.slice(0,chave.lastIndexOf(':')+1);for(let i=storage.length-1;i>=0;i--){const k=storage.key(i);if(k?.startsWith('netscreen-criar-rascunho:')&&!k.startsWith(dono))storage.removeItem(k)}
 try{const raw=storage.getItem(chave);if(!raw)return null;if(raw.length>40000)throw Error();const r=JSON.parse(raw);if(r.versao!==1||!Number.isFinite(r.guardadoEm)||agora-r.guardadoEm>86400000||r.guardadoEm>agora+60000)throw Error();return dadosRascunhoCriar({...r,automaticos:Object.fromEntries((r.automaticos||[]).map(k=>[k,r.brief?.[k]]))},r.guardadoEm)}catch{storage.removeItem(chave);return null}
}
// O exemplo do CTA do painel de rua segue o tratamento da Voz: tratava por você numa marca em «tu»
// (cliente simulado, 3 Nov). Sem Voz confirmada, um exemplo neutro serve a qualquer marca.
function exemploCtaRuaCriar(tratamento){return tratamento==='tu'?'Visita marca.pt':tratamento==='voce'?'Visite marca.pt':'Mais em marca.pt'}
function ScreenCriar({slug,onNavegar,podeMexer,donoRascunho,equipaAdmin}){
 const [textosApoio,setTextosApoio]=React.useState('');
 // O painel da loja: frases que ficam só nas redes e o CTA de quem já está na loja (14 Out).
 const [soRedes,setSoRedes]=React.useState([]),[ctaPainel,setCtaPainel]=React.useState(''),[ctaPainelRua,setCtaPainelRua]=React.useState(''),[tratamento,setTratamento]=React.useState(null); const base=`/api/${slug}/criar`,[config,setConfig]=React.useState(null),[erro,setErro]=React.useState(''),[ocupado,setOcupado]=React.useState(false),[nota,setNota]=React.useState(''),[fontes,setFontes]=React.useState([]),[url,setUrl]=React.useState(''),[orientacao,setOrientacao]=React.useState(null),[confirmados,setConfirmados]=React.useState([]),[historico,setHistorico]=React.useState([]),[biblioteca,setBiblioteca]=React.useState(null),[proposta,setProposta]=React.useState(null),[textos,setTextos]=React.useState(''),[cta,setCta]=React.useState(''),[imagens,setImagens]=React.useState([]),[aprovarTextos,setAprovarTextos]=React.useState(false),[aprovarImagens,setAprovarImagens]=React.useState(false),[videos,setVideos]=React.useState([]),[resultado,setResultado]=React.useState(null),[confirmarVideo,setConfirmarVideo]=React.useState(false),[iaChave,setIaChave]=React.useState(null);
 const [brief,setBrief]=React.useState(briefInicialCriar);
 const [referenciaRecebida]=React.useState(()=>{try{const q=new URLSearchParams(window.location.search),valor=q.get('referencia');if(!valor)return null;const u=new URL(valor);if(u.protocol!=='https:'||u.username||u.password||valor.length>1800)return null;return{url:u.href,tema:(q.get('tema')||'').slice(0,80)}}catch{return null}});
 React.useEffect(()=>{if(referenciaRecebida){setUrl(referenciaRecebida.url);setNota('Esta página entra como referência da campanha. A identidade global da marca mantém-se.')}},[referenciaRecebida]);
 const [rascunho,setRascunho]=React.useState(null),[rascunhoPronto,setRascunhoPronto]=React.useState(null),[rascunhoPausado,setRascunhoPausado]=React.useState(false),[avisoRascunho,setAvisoRascunho]=React.useState('');
 const chaveRascunho=chaveRascunhoCriar(donoRascunho,slug);
 const [sugestao,setSugestao]=React.useState(null);
 const [idSugestao,setIdSugestao]=React.useState(null);
 const [camposAutomaticos,setCamposAutomaticos]=React.useState({});
 React.useEffect(()=>{try{if(podeMexer)setRascunho(lerRascunhoCriar(sessionStorage,chaveRascunho))}catch{setAvisoRascunho('Este browser não permite guardar o rascunho neste separador.')}setRascunhoPronto(chaveRascunho)},[chaveRascunho,podeMexer]);
 React.useEffect(()=>{if(referenciaRecebida?.tema&&rascunhoPronto===chaveRascunho&&!rascunho)setBrief(b=>b.oferta.nome?b:{...b,oferta:{...b.oferta,nome:referenciaRecebida.tema}})},[referenciaRecebida,rascunhoPronto,chaveRascunho,rascunho]);
 React.useEffect(()=>{if(rascunhoPronto!==chaveRascunho||rascunho||rascunhoPausado||!chaveRascunho||!podeMexer)return;try{if(fontes.length||brief.oferta.nome||brief.informacao||brief.mensagem||brief.cta)sessionStorage.setItem(chaveRascunho,JSON.stringify(dadosRascunhoCriar({brief,fontes,automaticos:camposAutomaticos,leituraChave:sugestao?.chave||iaChave})));else sessionStorage.removeItem(chaveRascunho)}catch{setAvisoRascunho('Não foi possível guardar o rascunho neste separador. Conserva a tua informação antes de sair.')}},[brief,fontes,camposAutomaticos,sugestao?.chave,iaChave,rascunhoPronto,rascunho,rascunhoPausado,chaveRascunho,podeMexer]);
 const api=async(caminho,body)=>{let r;try{r=await fetch(base+caminho,{method:body!==undefined?'POST':'GET',headers:{'Content-Type':'application/json','X-Requested-With':'NetScreenCriar'},body:body!==undefined?JSON.stringify(body):undefined})}catch{throw Error('Não foi possível ligar à aplicação. Tenta novamente.')}let j;try{j=await r.json()}catch{throw Error('A resposta da aplicação não chegou completa. Tenta novamente.')}if(!r.ok)throw Error(j.error||'Não foi possível concluir.');return j};
 const carregar=async()=>{const [h,v]=await Promise.all([api('/orientacoes'),api('/videos')]);setHistorico(h);setVideos(v)};
 React.useEffect(()=>{let activo=true;api('/config').then(c=>{if(activo){setConfig(c);/* Os painéis da própria conta vêm marcados, enquanto o cliente não mexeu nos formatos (utilizadora virgem, ronda 6: o painel da montra vinha desmarcado). */{const proprios=(c.formatos||[]).filter(f=>f.origem==='painel').map(f=>f.formato);if(proprios.length)setBrief(b=>JSON.stringify(b.formatos)===JSON.stringify(briefInicialCriar().formatos)?{...b,formatos:[...new Set([...b.formatos,...proprios])]}:b)}/* Para onde estão virados os painéis, dito em Lojas e painéis: todos na rua, «Os painéis estão» vem na rua (rondas 7 a 10). */{const ruas=(c.formatos||[]).filter(f=>(f.origem==='painel'||f.painel)&&typeof f.rua==='boolean').map(f=>f.rua);if(ruas.length&&ruas.every(Boolean))setBrief(b=>b.ondePainel===undefined?{...b,ondePainel:'rua'}:b)}carregar().catch(e=>setErro(e.message));const id=new URLSearchParams(location.search).get('video');if(id&&/^[a-f0-9-]{36}$/.test(id))api('/video/'+id).then(r=>{if(activo)setResultado(r)}).catch(e=>setErro(e.message))}}).catch(e=>{if(activo)setErro(e.message)});return()=>{activo=false}},[slug]);
 // «Ver todas as criações» (Início) chega com ?ver=criacoes: desce-se à lista quando ela existe, sem prazo (Codex X-46).
 const desceuAsCriacoes=React.useRef(false);
 React.useEffect(()=>{if(desceuAsCriacoes.current||!config||!videos.length||new URLSearchParams(location.search).get('ver')!=='criacoes')return;const el=document.getElementById('criacoes');if(el){desceuAsCriacoes.current=true;el.scrollIntoView({behavior:'smooth'});el.querySelector('h2')?.setAttribute('tabindex','-1');el.querySelector('h2')?.focus({preventScroll:true})}},[config,videos]);
 // Uma consulta falhada que a seguinte recupera não deixa o erro à vista; erros de outras acções ficam (Codex X-12, 3 Nov).
 const erroDaConsulta=React.useRef(null);
 // A lista também se relê enquanto houver vídeos na fila ou a criar: abrir outro vídeo parava a consulta do
 // primeiro e a linha ficava «A criar» depois de ele acabar (Codex X-33, 3 Nov).
 React.useEffect(()=>{if(!videos.some(v=>['fila','a_criar'].includes(v.estado)))return;const t=setInterval(()=>{carregar().catch(()=>{})},15000);return()=>clearInterval(t)},[videos]);
 React.useEffect(()=>{if(!resultado||!['fila','a_criar'].includes(resultado.estado))return;let activo=true;const timer=setInterval(()=>api('/video/'+resultado.id).then(r=>{if(activo){setResultado(r);const falhada=erroDaConsulta.current;erroDaConsulta.current=null;if(falhada)setErro(e=>e===falhada?'':e);if(!['fila','a_criar'].includes(r.estado))carregar().catch(()=>{})}}).catch(e=>{if(activo){erroDaConsulta.current=e.message;setErro(e.message)}}),2500);return()=>{activo=false;clearInterval(timer)}},[resultado?.id,resultado?.estado,slug]);
 // Só a Voz confirmada (`voz`, não a `proposta`); se a leitura falhar, fica o exemplo neutro.
 React.useEffect(()=>{let activo=true;setTratamento(null);fetch('/api/'+slug+'/marca/voz').then(r=>r.ok?r.json():null).then(v=>{if(activo)setTratamento(v?.voz?.tratamento||null)}).catch(()=>{});return()=>{activo=false}},[slug]);
 const aoTopo=()=>{const m=document.querySelector('main');if(m)m.scrollTo({top:0,behavior:'smooth'});else window.scrollTo({top:0,behavior:'smooth'})};
 // Uma falha limpa o «A preparar…»/«A ler…», que ficava para sempre ao lado do erro (agentes, 20 Out).
 const fazer=async fn=>{if(ocupado)return;setOcupado(true);setErro('');try{await fn()}catch(e){setNota('');setErro(e.message);aoTopo()}finally{setOcupado(false)}};
 // Mudar só os formatos ou destinos não deita fora as frases e os CTA editados: voltam na próxima
 // aprovação, com os controlos do painel recalculados (cliente simulado, 14 Out).
 const edicoes=React.useRef(null);
 // Abrir uma revisão ou um vídeo a partir de As tuas criações, no fundo da página, deixava-os fora de vista:
 // quem desliza é o <main>, e o scrollIntoView move-o até eles (agentes, 20 Out).
 const refProposta=React.useRef(null),refResultado=React.useRef(null);
 React.useEffect(()=>{if(proposta)refProposta.current?.scrollIntoView({behavior:'smooth',block:'start'})},[proposta]);
 React.useEffect(()=>{if(resultado)refResultado.current?.scrollIntoView({behavior:'smooth',block:'start'})},[resultado?.id]);
 const alterar=(campo,valor)=>{edicoes.current=edicoesAoAlterarCriar(campo,edicoes.current,proposta,{textos,textosApoio,cta,ctaPainel,ctaPainelRua,soRedes});if(proposta&&edicoes.current)setNota('Mantemos as frases e as chamadas para ação que editaste. Usa «Escrever eu os textos» para voltar à aprovação com os novos formatos.');setRascunhoPausado(false);setBrief(b=>campo==='oferta'?{...b,oferta:valor,objetivo:objetivoCriar(valor.tipo,b.objetivo)}:{...b,[campo]:valor});setCamposAutomaticos(a=>{const novos={...a};delete novos[campo];return novos});setOrientacao(null);setConfirmados([]);setIaChave(null);setProposta(null);setIdSugestao(null)};
 // Painéis na rua: sem a regra de loja online e sem «da loja» nos textos (3 Nov).
 const naRua=brief.ondePainel==='rua',oPainel=naRua?'painel':'painel da loja';
 const alterarFontes=fs=>{edicoes.current=null;setRascunhoPausado(false);setFontes(fs);setBrief(b=>({...b,...Object.fromEntries(Object.entries(camposAutomaticos).filter(([campo,valor])=>JSON.stringify(b[campo])===JSON.stringify(valor)).map(([campo])=>[campo,campo==='oferta'?{tipo:'produto',nome:''}:'' ]))}));setCamposAutomaticos({});setOrientacao(null);setIaChave(null);setProposta(null);setSugestao(null);setIdSugestao(null)};
 const ofertas=fontes.flatMap(f=>(f.ofertas||[]).map(o=>({...o,fonteTitulo:f.titulo,fonteUrl:f.url}))).filter((o,i,a)=>a.findIndex(x=>x.url===o.url&&x.tipo===o.tipo)===i);
 const lerReferencia=async(valor,papel)=>{if(fontes.length>=12)throw Error('Usa até 12 referências. Retira uma antes de juntar outra.');if(fontes.some(f=>f.url===valor))throw Error('Esta página já está nas referências.');setNota('A ler a referência…');const r=await api('/fonte',{url:valor});const post=/(^|\.)(instagram\.com|facebook\.com|linkedin\.com|tiktok\.com|x\.com)$/i.test(new URL(r.url).hostname);alterarFontes([...fontes,{tipo:post?'post':'site',papel:papel||(post?'intencao':'identidade'),recibo:r.recibo,url:r.url,titulo:r.titulo,texto:r.texto,avisos:r.avisos||[],ofertas:r.ofertas||[]}]);setNota('Referência lida. Revê o texto e escolhe uma sugestão ou escreve outro tema.')};
 const entrada=()=>({...brief,usos:Object.fromEntries(brief.formatos.map(f=>[f,brief.usos?.[f]||(config.formatos.find(m=>m.formato===f)?.painel?config.formatos.find(m=>m.formato===f).canais:[config.formatos.find(m=>m.formato===f)?.canais?.[0]||(f==='1080x1920'||f==='1080x1080'?'social':'dooh')])])),fontes});
 const material=async m=>{const r=await api('/material/'+m.id+'/ler',{});return {tipo:r.tipo==='text/plain'?'texto':r.tipo==='application/pdf'?'material':r.texto?'flyer':'fotografia',mime:r.tipo,papel:'intencao',ficheiro:r.id,titulo:r.nome,texto:r.texto,url:r.url,avisos:r.avisos||[]}};
 const semFrasesNoPainel=Boolean(proposta?.painel)&&linhasCriar(textos).length>0&&linhasCriar(textos).every((_,i)=>soRedes[i]);
 const limparRascunho=()=>{edicoes.current=null;try{if(chaveRascunho)sessionStorage.removeItem(chaveRascunho)}catch{}setRascunho(null);setBrief(briefInicialCriar());setFontes([]);setUrl('');setCamposAutomaticos({});setSugestao(null);setIaChave(null);setIdSugestao(null);setOrientacao(null);setConfirmados([]);setProposta(null);setRascunhoPausado(false);setNota('Rascunho limpo. As orientações confirmadas continuam guardadas no servidor.')};
 const recuperarRascunho=async()=>{const fs=[];for(const f of rascunho.fontes){if(f.recibo){const r=await api('/fonte/'+f.recibo);fs.push({...f,url:r.url,titulo:r.titulo,texto:r.texto,avisos:r.avisos||[],ofertas:r.ofertas||[]})}else{fs.push({...await material({id:f.ficheiro}),papel:f.papel})}}setBrief(rascunho.brief);setFontes(fs);setCamposAutomaticos(Object.fromEntries(rascunho.automaticos.map(k=>[k,rascunho.brief[k]])));setSugestao(rascunho.leituraChave?{chave:rascunho.leituraChave,rascunho:true}:null);setIaChave(null);setOrientacao(null);setConfirmados([]);setProposta(null);setAprovarTextos(false);setAprovarImagens(false);setRascunho(null);setRascunhoPausado(false);setNota('Rascunho recuperado, por confirmar. Revê a informação e usa «Escrever eu os textos»; preparar uma nova proposta só acontece quando pedires.')};
 const abrirProposta=async id=>{const p=await api('/orientacao/'+id+'/materiais'),sugeridas=id===idSugestao?(sugestao?.imagensSugeridas||[]).filter(m=>p.candidatos.some(c=>c.id===m)):[];const frasesIA=id===idSugestao&&sugestao?.frases?.length>=2?sugestao.frases:null,maxImg=config?.maxImagens||4;setProposta({...p,orientacaoId:id,materiaisSugeridos:sugeridas.length>0});const lista=frasesIA||p.textos,daIA=[...(p.frasesSoRedes||[]),...(id===idSugestao?sugestao?.frasesSoRedes||[]:[])];setTextos(lista.join('\n'));setSoRedes(lista.map(t=>(!naRua&&soLojaOnlineCriar(t))||daIA.includes(t)));{/* Com o campo da rua, o site vai para lá e não para o painel de dentro (ronda 12: a proposta punha-o ao contrário). */const cp=p.ctaPainel||(id===idSugestao?sugestao?.ctaPainel:'')||'',site=/[a-z0-9-]+\.(?:com\.pt|pt|com)(?![a-z0-9-])/i;if(paineisNaRuaCriar(brief,config).length&&site.test(cp)){setCtaPainelRua(cp);setCtaPainel(cp.replace(site,'').replace(/[\s,;:·-]+$/,'').trim())}else setCtaPainel(cp)}setTextosApoio(id===idSugestao?(sugestao?.textosDoSite||[]).join('\n'):'');setCta(p.cta);setImagens([...new Set([...sugeridas,...(maxImg>4?p.candidatos.map(c=>c.id):[])])].slice(0,maxImg));setAprovarTextos(false);setAprovarImagens(false);setResultado(null);setConfirmarVideo(false);const e=edicoes.current;edicoes.current=null;if(e){setTextos(e.textos);setTextosApoio(e.textosApoio);setCta(e.cta);setCtaPainel(e.ctaPainel);if(e.ctaPainelRua!==undefined)setCtaPainelRua(e.ctaPainelRua);setSoRedes(e.soRedes);setNota(p.painel?`Mantivemos as frases e as chamadas para ação que editaste. Revê o que vai para o ${oPainel}.`:'Mantivemos as frases e a chamada para ação que editaste.')}};
 const confirmarERever=async()=>{let r=orientacao;if(r.orientacao.estado!=='orientacao_confirmada'){try{r=await api('/orientacao/'+r.id+'/confirmar',{ids:confirmados})}catch(e){const h=await api('/orientacoes').catch(()=>[]),guardada=h.find(h=>h.id===r.id);if(!guardada)throw e;r=guardada}setOrientacao(r)}setRascunhoPausado(true);try{if(chaveRascunho)sessionStorage.removeItem(chaveRascunho)}catch{}await abrirProposta(r.id);setOrientacao(null);await carregar().catch(e=>setErro(e.message))};
 const prepararProposta=async()=>{let resolvido=entrada(),chave=iaChave;if(!chave){setNota('A preparar uma sugestão a partir das referências…');let r;try{r=await api('/interpretar',{brief:resolvido})}catch{throw Error('Não conseguimos preparar a proposta agora. Tenta outra vez daqui a pouco, ou usa «Escrever eu os textos» e escreve tu o tema e a mensagem.')}const automaticos={...camposAutomaticos};if(!resolvido.oferta.nome)automaticos.oferta=r.oferta;if(!resolvido.mensagem)automaticos.mensagem=r.mensagem;if(!resolvido.cta)automaticos.cta=r.cta;resolvido={...resolvido,oferta:resolvido.oferta.nome?resolvido.oferta:r.oferta,mensagem:resolvido.mensagem||r.mensagem,cta:resolvido.cta||r.cta,...(r.direccaoCriativa&&!resolvido.direccaoCriativa?{direccaoCriativa:r.direccaoCriativa}:{})};resolvido.objetivo=objetivoCriar(resolvido.oferta?.tipo,resolvido.objetivo);setCamposAutomaticos(automaticos);/* «montra», «virado para a rua»: o painel está na rua, se o cliente não escolheu (ronda 6). */if(brief.ondePainel===undefined&&!paineisMistosCriar(config)&&/montra|virad[oa]s? para a rua|na rua|exterior/i.test([resolvido.mensagem,resolvido.informacao,brief.informacao].join(' ')))resolvido={...resolvido,ondePainel:'rua'};setBrief(b=>({...b,oferta:resolvido.oferta,mensagem:resolvido.mensagem,cta:resolvido.cta,objetivo:resolvido.objetivo,...(resolvido.ondePainel==='rua'&&!paineisMistosCriar(config)?{ondePainel:'rua'}:{})}));chave=r.chave;setIaChave(chave);setSugestao(r);setNota('Sugestão por confirmar.')}const o=await api('/orientacao',{brief:resolvido,interpretacaoChave:chave});setOrientacao(o);setIdSugestao(o.id);setConfirmados([]);setProposta(null)};
 const novaOrientacao=h=>{edicoes.current=null;setRascunhoPausado(false);setRascunho(null);const b=h.orientacao.brief;setBrief({...b});setFontes(b.fontes||[]);setCamposAutomaticos({});setOrientacao(null);setConfirmados([]);setIaChave(null);setSugestao(null);setIdSugestao(null);setProposta(null);setResultado(null);setConfirmarVideo(false);setNota('Revê os dados copiados e confirma uma nova orientação. A anterior fica guardada.');aoTopo()};
 const input='w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50';
 const button='border border-border px-4 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50';
 const primary=button+' bg-primary text-primary-foreground hover:bg-primary/90';
 const label='block text-sm mb-2',hint='text-sm text-muted-foreground';
 if(!config)return <div className="space-y-4"><h1 className="font-display text-3xl uppercase">Criar</h1><p role="status">{erro||'A preparar a criação…'}</p>{erro&&<button disabled={ocupado} className={button} onClick={()=>fazer(async()=>{setConfig(await api('/config'));await carregar()})}>Tentar novamente</button>}<button className={button} onClick={()=>onNavegar(`/${slug}/campanhas`)}>Voltar às campanhas</button></div>;
 return <div className="space-y-8 max-w-6xl">
  <div className="flex flex-wrap justify-between items-start gap-4"><div><h1 className="font-display text-3xl uppercase">{resultado?'O teu vídeo':'O que vamos promover?'}</h1><p className={'mt-3 '+hint}>{resultado?'Para um vídeo novo, o pedido está mais abaixo.':'Junta um site, um post ou ficheiros, ou escreve a ideia.'}</p></div></div>{/* Com um vídeo aberto, o vídeo vem primeiro e o pedido novo depois: abria-se com o formulário vazio por cima (parceira virgem, 4 Nov). */}{resultado&&<section ref={refResultado} className="space-y-5 border-t border-border pt-6 scroll-mt-24"><h2 className="text-xl">{resultado.tema}</h2>{/* 15 a 40 minutos com uma frase parada pareciam um ecrã preso (cliente simulado, 3 Nov): a animação, a fase, o tempo desde que começou e o que esperar. Sem percentagens: não há como as medir. */}{['fila','a_criar'].includes(resultado.estado)?<AProcessar variante="bloco" texto={resultado.estado==='fila'?'Vídeo na fila, à espera de vez':resultado.fase||'A criar e rever a composição'} desde={(resultado.estado==='a_criar'&&resultado.iniciadoEm)||resultado.criadoEm||null} detalhe={resultado.estado==='fila'?'Começa quando chegar a vez e costuma levar 15 a 40 minutos. Podes sair e voltar: aparece aqui quando estiver pronto.':'Costuma levar 15 a 40 minutos. Podes sair e voltar: o vídeo continua a ser criado e aparece aqui.'}/>:<p role="status">{({preparado:'Pedido preparado. Revê antes de gerar.',fila:'Vídeo na fila. Podes sair e voltar a esta página.',a_criar:'A criar e rever a composição.',video_por_rever:'Vídeo pronto para revisão.',aprovado:'Vídeo aprovado, sem publicação.',recusado:resultado.semVideo?'O vídeo não foi criado.':'O vídeo foi recusado.',interrompido:'A geração foi interrompida.'})[resultado.estado]||resultado.estado}</p>}{resultado.recurso&&<p role="status" className={hint}>Foi feita uma versão mais simples, porque a animação completa não ficou pronta: {resultado.recurso.motivo}. Revê esta alternativa antes de aprovar.{resultado.recurso.imagensPublicasSemBiblioteca>0?' As imagens públicas que não estão na biblioteca não foram usadas nesta alternativa.':''}</p>}{resultado.error&&<p role="alert">{resultado.error}</p>}{resultado.podeRepetir&&<div className="space-y-2"><p className={hint}>Nada foi entregue nem aprovado. Uma nova tentativa usa o mesmo pedido aprovado e tem orçamento próprio até {Number(config?.limites?.eur??resultado.limites?.eur??4).toFixed(2).replace('.',',')} €.{/* O tecto da nova tentativa é o actual do servidor (limitesVideo), não o do pedido falhado (Codex X-31). */}</p><button className={primary} disabled={ocupado||!podeMexer} onClick={()=>fazer(async()=>{setResultado(await api('/video/'+resultado.id+'/repetir',{confirmar:true}));await carregar()})}>{ocupado?'A pôr na fila…':'Tentar outra vez'}</button></div>}{/* Os formatos lado a lado, à mesma altura, com a legenda por baixo: empilhados, o roll-up ficava longe do vídeo das redes (crítica visual, 3 Nov). */}{(resultado.resultados||[]).length>0&&<div className="flex flex-wrap items-end gap-x-8 gap-y-6">{resultado.resultados.map(r=>{const [lw,la]=String(r.formato).split('x').map(Number),deitado=lw>la;return <figure key={r.formato+(r.ficheiro||'')+(r.rotuloDestino||'')} className={deitado?'basis-full space-y-2':'space-y-2'}><video controls playsInline preload="metadata" className={`${deitado?'w-full max-w-3xl h-auto':'h-[28rem] w-auto max-w-full'} rounded-md border border-border bg-black`} src={`${base}/video/${resultado.id}/ficheiros/${r.ficheiro||r.formato+'.mp4'}`} poster={`${base}/video/${resultado.id}/ficheiros/${r.poster||'final-'+r.formato+'.png'}`} onPlay={e=>document.querySelectorAll('video').forEach(v=>{if(v!==e.target)v.pause()})}/><figcaption className="text-sm"><span className="block text-foreground">{r.rotuloDestino||(r.som?'Com som':'Sem som')}</span><span className="block text-xs text-muted-foreground tabular-nums">{lw} × {la}{' · '}<a className="underline underline-offset-4 hover:text-foreground" href={`${base}/video/${resultado.id}/ficheiros/${r.ficheiro||r.formato+'.mp4'}`} download>Descarregar</a></span></figcaption></figure>})}</div>}{/* O cliente vê primeiro o vídeo; o consumo e os planos ficam num «Detalhes» fechado (demo Eficácia, 3 Nov). */}<details className="text-sm"><summary className="cursor-pointer underline underline-offset-4">Detalhes</summary><div className={`mt-2 space-y-1 ${hint}`}><p>Custo do vídeo: {Number(resultado.custoEurRegistado||resultado.custoEur||0).toFixed(2).replace('.',',')} €.</p>{/* Os planos técnicos (.md) e as chamadas são para a equipa, não para o cliente (parceira virgem, 4 Nov). */}{equipaAdmin&&(resultado.resultados||[]).length>0&&<p className="flex flex-wrap gap-4">{resultado.resultados.map(r=><a key={r.formato+(r.ficheiro||'')} className="underline underline-offset-4" href={`${base}/video/${resultado.id}/ficheiros/${r.formato}-plano.md`}>Plano {r.formato}</a>)}</p>}{resultado.revisaoVisual&&<p><a className="underline underline-offset-4" href={`${base}/video/${resultado.id}/revisao`} target="_blank" rel="noopener">Comparar originais e movimento</a></p>}</div></details>{/* Aberto enquanto se revê; depois de aprovado, fechado (crítica visual, 3 Nov). */}{(resultado.elementosNovos?.length>0||resultado.textosApoioAusentes?.length>0)&&<details open={resultado.estado==='video_por_rever'} className="text-sm"><summary className="cursor-pointer underline underline-offset-4">O que o vídeo acrescenta ou deixa de fora</summary><div role="note" className={`mt-2 space-y-1 ${hint}`}>{resultado.elementosNovos?.length>0&&<p>Elementos novos, que não vêm da vossa marca: {resultado.elementosNovos.map(e=>e.porque?`${e.descricao} (${e.porque})`:e.descricao).join('; ')}.</p>}{resultado.textosApoioAusentes?.length>0&&<p>Ficou fora do vídeo: {resultado.textosApoioAusentes.map(t=>`«${t}»`).join(', ')}.</p>}<p>Para mudar, pede um ajuste.</p></div></details>}{resultado.estado==='video_por_rever'&&<div className="space-y-3"><label className="flex gap-3 text-sm"><input type="checkbox" checked={confirmarVideo} onChange={e=>setConfirmarVideo(e.target.checked)}/>Revi os vídeos de todos os formatos e confirmo os materiais, o texto e a animação.</label><button className={primary} disabled={ocupado||!podeMexer} onClick={()=>fazer(async()=>{setResultado(await api('/video/'+resultado.id+'/aprovar',{confirmar:confirmarVideo}));await carregar()})}>Aprovar vídeo final</button></div>}<AjustesCriacao key={resultado.id} slug={slug} video={resultado} podeMexer={podeMexer} geracaoActiva={config.video} onActualizado={r=>{setResultado(r);setConfirmarVideo(false);carregar().catch(e=>setErro(e.message))}}/>{resultado.estado==='aprovado'&&<p><a className="underline underline-offset-4 text-sm" href={`${base}/video/${resultado.id}/pacote`}>Descarregar pacote aprovado</a></p>}{/* A campanha abre-se desde que o vídeo é pedido: o post escreve-se enquanto ele se faz (rondas 3–5, 4 Nov). */}{['fila','a_criar','video_por_rever','aprovado'].includes(resultado.estado)&&<div className="space-y-1"><button className={resultado.estado==='aprovado'?primary:button} disabled={ocupado||!podeMexer} onClick={()=>fazer(async()=>{const c=await api('/video/'+resultado.id+'/campanha',{});onNavegar(`/${slug}/campanhas/${c.id}`)})}>{resultado.campanhaId?'Abrir a campanha':resultado.estado==='aprovado'?'Ir para a campanha':'Escrever já o post e as datas'}</button>{resultado.estado!=='aprovado'&&!resultado.campanhaId&&<p className={hint}>O vídeo e o post ficam na mesma campanha. Ela aprova-se depois de aprovares o vídeo.</p>}</div>}{/* Só o Admin da NetScreen envia para um painel (3 Nov, demo). */}{resultado.estado==='aprovado'&&equipaAdmin&&<EnviarParaPainelCriar key={resultado.id} video={resultado}/>}</section>}
  {(erro||nota)&&<div className={`${erro?'sticky top-0 z-20 border-b border-border bg-background ':''}-mx-1 space-y-1 px-1 py-2`}>{erro&&<p role="alert" className="text-sm text-destructive">{erro}</p>}{nota&&<p role="status" className={hint}>{nota}</p>}</div>}{config&&!config.video&&<p role="status" className="text-sm border border-border p-3">A criação de vídeo ainda não está ligada nesta conta. Podes preparar e guardar a orientação, os textos e as imagens; para criar o vídeo, fala com o teu gestor NetScreen.</p>}
  {/* A caixa aparecia logo à entrada, sem nada para recuperar nem limpar (agentes, 20 Out). */}
  {podeMexer&&chaveRascunho&&(rascunho||avisoRascunho||fontes.length>0||brief.oferta.nome||brief.informacao||brief.mensagem||brief.cta)&&<div className="space-y-3 border border-border p-4"><p role="status" className={hint}>{rascunho?'Há um rascunho por confirmar neste separador. Recupera-o ou limpa-o antes de começar outro.':'O que preencheste fica guardado neste separador durante 24 horas, para não se perder se recarregares a página.'}</p>{avisoRascunho&&<p role="status" className={hint}>{avisoRascunho}</p>}<div className="flex flex-wrap gap-3">{rascunho&&<button className={primary} disabled={ocupado} onClick={()=>fazer(recuperarRascunho)}>Recuperar rascunho</button>}<button className={button} disabled={ocupado} onClick={limparRascunho}>Limpar rascunho</button></div></div>}
  <fieldset disabled={!podeMexer||ocupado||!!rascunho} className="space-y-8 min-w-0 disabled:opacity-70">
   <section className="space-y-4"><h2 className="text-xl">As tuas referências</h2><div><label htmlFor="criar-url" className={label}>Site ou link de um post, opcional</label><div className="flex flex-wrap gap-2"><input id="criar-url" type="url" className={input+' flex-1 min-w-0'} value={url} onChange={e=>setUrl(e.target.value)} placeholder="https://…"/><button className={button} disabled={fontes.length>=12} onClick={()=>fazer(()=>lerReferencia(url))}>Ler referência</button></div><p className={'mt-2 '+hint}>Se o post não abrir, junta uma captura ou cola o texto em Mais informação.</p></div>
   <div className="flex flex-wrap gap-3 items-center"><label className={button+' cursor-pointer'}>Juntar ficheiros<input className="sr-only" type="file" multiple accept="image/png,image/jpeg,image/webp,application/pdf,text/plain" onChange={e=>{const files=[...e.target.files];e.target.value='';fazer(async()=>{if(fontes.length+files.length>12)throw Error('Usa até 12 referências. Retira uma antes de juntar estes ficheiros.');if(files.length>4||files.some(f=>f.size>12*1024*1024))throw Error('Até quatro ficheiros, 12 MB por ficheiro.');const novos=[];for(const f of files){const r=await fetch(`/api/${slug}/materiais?nome=${encodeURIComponent(f.name)}&ler=0`,{method:'POST',headers:{'Content-Type':f.type||'application/octet-stream'},body:f});const j=await r.json();if(!r.ok)throw Error(j.error);novos.push(await material(j.material));alterarFontes([...fontes,...novos])}setNota('Materiais guardados na biblioteca da conta.')})}}/></label><button className={button} onClick={()=>fazer(async()=>{const r=await fetch(`/api/${slug}/materiais`);if(!r.ok)throw Error('Não foi possível abrir a biblioteca.');const j=await r.json();setBiblioteca(Array.isArray(j)?j:j.materiais)})}>Escolher da biblioteca</button></div>
   {biblioteca&&<div className="space-y-2 border border-border p-4"><h3>Materiais da conta</h3>{biblioteca.length?biblioteca.map(m=><button key={m.id} className={button+' mr-2 mb-2'} onClick={()=>fazer(async()=>{if(fontes.length>=12)throw Error('Usa até 12 referências. Retira uma antes de juntar outra.');if(fontes.some(f=>f.ficheiro===m.id))throw Error('Este material já está nas referências.');alterarFontes([...fontes,await material(m)])})}>{nomeAVista(m.original_name||m.nome)}{fontes.some(f=>f.ficheiro===m.id)?' · junto':''}</button>):<p className={hint}>Ainda não há materiais. Junta um ficheiro para começar.</p>}<button className={button} onClick={()=>setBiblioteca(null)}>Fechar biblioteca</button></div>}
   <p className={hint}>{fontes.length} de 12 referências.</p>
   {fontes.map((f,i)=><div key={f.recibo||f.ficheiro||i} className="space-y-3 border-b border-border pb-4"><div className="flex flex-wrap items-center gap-3"><span className="w-full sm:w-auto sm:flex-1 min-w-0 break-words text-sm">{f.titulo}<span className={'block mt-1 '+hint}>{f.mime==='application/pdf'?'Documento PDF':f.mime==='text/plain'?'Ficheiro de texto':f.mime?.startsWith('image/')?'Imagem':f.tipo==='post'?'Post de rede social':f.tipo==='site'?'Página do site':'Material'}</span></span><select aria-label={'Como usar '+f.titulo} className={input+' w-auto'} value={f.papel} onChange={e=>alterarFontes(fontes.map((x,n)=>n===i?{...x,papel:e.target.value}:x))}>{[['intencao','Conteúdo a promover'],['visual','Referência visual'],['identidade','Identidade da marca'],['factos','Dados da oferta']].map(([v,t])=><option key={v} value={v}>{t}</option>)}</select><button aria-label={'Retirar '+f.titulo} className={button} onClick={()=>{setNota('');alterarFontes(fontes.filter((_,n)=>n!==i))}}>Retirar</button></div>{/* Numa imagem sem texto útil, o OCR dizia «pouca confiança (41%)» e logo a seguir «não foi encontrado texto»:
     fica uma só mensagem; nos documentos o aviso da leitura mantém-se e não é contradito (agentes, 20 Out). */}{!(f.mime?.startsWith('image/')&&!f.texto)&&(f.avisos||[]).map((a,n)=><p key={n} role="status" className={hint}>{a}</p>)}{f.texto?<details className="text-sm"><summary className="cursor-pointer underline underline-offset-4">Ver texto extraído de {f.titulo}</summary><p className={'mt-3 '+hint}>Confere a leitura com o original. Se houver falhas, esclarece em Mais informação.</p><div className="mt-3 max-h-64 overflow-auto whitespace-pre-wrap break-words border border-border p-3">{f.texto}</div></details>:<p className={hint}>{f.mime?.startsWith('image/')?'Não há texto para usar nesta imagem. Serve como referência visual; descreve em Mais informação o que queres promover.':f.avisos?.length?'Podes usar o material como referência visual e descrever o que queres promover.':'Não foi encontrado texto legível nesta referência. Podes usar o material como referência visual e descrever o que queres promover.'}</p>}{(!f.texto||f.avisos?.length>0)&&<button className={button} onClick={()=>{const e=document.getElementById('criar-info');e?.focus();e?.scrollIntoView({behavior:'smooth',block:'center'})}}>Acrescentar contexto</button>}</div>)}
   <div><label htmlFor="criar-info" className={label}>Mais informação</label><textarea id="criar-info" className={input} rows={4} value={brief.informacao} onChange={e=>alterar('informacao',e.target.value)} placeholder="O que queres destacar? Para quem? Há alguma oferta ou indicação a respeitar?"/></div></section>
   <section className="space-y-4 border-t border-border pt-6"><h2 className="text-xl">O que queres promover</h2>{ofertas.length>0&&<div className="space-y-3"><p className={hint}>Sugestões encontradas nas referências. Ler a página de uma oferta acrescenta os seus materiais; o tema escolhido mantém-se.</p>{ofertas.map(o=><div key={o.url+o.tipo} className="flex flex-wrap items-center gap-3"><button className={button} onClick={()=>alterar('oferta',{tipo:o.tipo,nome:o.nome,origem:o.url})}>{o.nome}</button><span className={hint+' min-w-0 break-words'}>Fonte: {o.fonteTitulo}</span>{!fontes.some(f=>f.url===o.url)&&<button disabled={fontes.length>=12} aria-label={'Ler página de '+o.nome} className={button} onClick={()=>fazer(()=>lerReferencia(o.url,'factos'))}>Ler página da oferta</button>}</div>)}</div>}<details open={ofertas.length===0||!!brief.oferta.nome||!!brief.mensagem||!!brief.cta} className="space-y-4"><summary className="cursor-pointer text-sm underline underline-offset-4">Indicar ou corrigir o tema e a mensagem</summary><div className="grid gap-4 md:grid-cols-2"><div><label htmlFor="criar-tipo" className={label}>Tipo</label><select id="criar-tipo" className={input} value={brief.oferta.tipo} onChange={e=>alterar('oferta',{...brief.oferta,tipo:e.target.value})}>{[['produto','Produto'],['servico','Serviço'],['campanha','Campanha'],['promocao','Promoção'],['geral','Negócio em geral']].map(([v,t])=><option key={v} value={v}>{t}</option>)}</select></div><div><label htmlFor="criar-nome" className={label}>Nome ou tema</label><input id="criar-nome" aria-describedby="criar-nome-ajuda" className={input} value={brief.oferta.nome} onChange={e=>alterar('oferta',{...brief.oferta,nome:e.target.value})}/><p id="criar-nome-ajuda" className={'mt-2 '+hint}>Até 80 caracteres e dez palavras.</p></div></div>
   <div><label htmlFor="criar-mensagem" className={label}>Mensagem principal</label><input id="criar-mensagem" className={input} value={brief.mensagem} onChange={e=>alterar('mensagem',e.target.value)}/></div><div><label htmlFor="criar-cta" className={label}>Chamada para ação</label><input id="criar-cta" className={input} value={brief.cta} onChange={e=>alterar('cta',e.target.value)}/></div></details>
   <details className="space-y-4"><summary className="cursor-pointer text-sm underline underline-offset-4">Ajustes opcionais</summary>
   <div className="grid gap-4 md:grid-cols-2"><div><label htmlFor="criar-modo" className={label}>Como usar as referências</label><select id="criar-modo" className={input} value={brief.modo} onChange={e=>alterar('modo',e.target.value)}><option value="criar">Criar uma proposta nova</option><option value="adaptar">Adaptar a peça, preservando a mensagem</option></select></div><div><label htmlFor="criar-tom" className={label}>Tom</label><select id="criar-tom" className={input} value={brief.tom} onChange={e=>alterar('tom',e.target.value)}>{['Sugerido a partir dos materiais','Elegante e contido','Próximo e descontraído','Energético','Direto e informativo'].map(t=><option key={t}>{t}</option>)}</select></div></div>
   <div><label htmlFor="criar-objetivo" className={label}>Objetivo</label><select id="criar-objetivo" className={input} value={brief.objetivo} onChange={e=>alterar('objetivo',e.target.value)}>{['Dar a conhecer','Vender uma oferta','Apresentar um produto ou serviço','Levar ao site ou à loja','Anunciar um evento'].map(t=><option key={t}>{t}</option>)}</select></div></details>
   <p className={hint}>Junta as referências e prepara uma proposta. Podes corrigir o tema e a mensagem antes de confirmar.</p></section>
   <section className="space-y-4 border-t border-border pt-6"><h2 className="text-xl">Onde vai aparecer?</h2>
    <div className="grid gap-3 sm:grid-cols-2">{[...(config.formatos||[]),...brief.formatos.filter(f=>!config.formatos?.some(m=>m.formato===f)).map(formato=>({formato,nome:`Indisponível: ${formato}`}))].map(m=><div key={m.formato} className="space-y-2"><label className="flex items-center gap-3 text-sm"><input type="checkbox" checked={brief.formatos.includes(m.formato)} disabled={!podeMexer||ocupado||(!brief.formatos.includes(m.formato)&&brief.formatos.length>=8)} onChange={e=>alterar('formatos',e.target.checked?[...brief.formatos,m.formato]:brief.formatos.filter(x=>x!==m.formato))}/>{m.nome}</label>{brief.formatos.includes(m.formato)&&m.canais?.length>0&&(m.canais.length===2||brief.usos?.[m.formato]?.some(c=>!m.canais.includes(c)))&&<label className="block text-sm">Destino de {m.formato}<select className={input+' mt-2'} disabled={!podeMexer||ocupado} aria-label={`Destino de ${m.formato}`} value={(brief.usos?.[m.formato]||(m.painel?m.canais:[m.canais[0]])).join(',')} onChange={e=>alterar('usos',{...brief.usos,[m.formato]:e.target.value.split(',')})}>{brief.usos?.[m.formato]?.some(c=>!m.canais.includes(c))&&<option disabled value={brief.usos[m.formato].join(',')}>Destino indisponível: escolhe outro</option>}{m.canais.includes('social')&&<option value="social">Redes sociais</option>}{m.canais.includes('dooh')&&<option value="dooh">Painel, sem som</option>}{m.canais.length===2&&<option value="social,dooh">Redes sociais e painel</option>}</select></label>}</div>)}</div>
    {/* Um outdoor de rua não é o painel da loja: aí um endereço web ou «encomenda já» valem (Eficácia, 3 Nov). */}
    {temPainelCriar(brief,config)&&<label className="block text-sm max-w-sm">Os painéis estão<select className={input+' mt-2'} disabled={!podeMexer||ocupado} aria-label="Onde estão os painéis" value={naRua?'rua':'loja'} onChange={e=>alterar('ondePainel',e.target.value)}><option value="loja">Dentro da loja (vê-os quem já entrou)</option><option value="rua">Virados para a rua (montra ou exterior)</option></select>{(()=>{/* Um na rua e outro dentro: a mesma chamada serve os dois, e diz-se (rondas 9 e 10). */const r=(config?.formatos||[]).filter(f=>(f.origem==='painel'||f.painel)&&typeof f.rua==='boolean');return r.some(f=>f.rua)&&r.some(f=>!f.rua)?<span className={hint+' mt-2 block'}>Tens painéis na rua e dentro: escolhe aqui os de dentro. Na proposta, o painel da rua pode ter a sua chamada (o site, por exemplo).</span>:null})()}</label>}
    <p className={hint+' max-w-prose'}>Nos painéis o vídeo passa sem som.</p>
    {/* Só quando há destino para escolher, e o botão diz o que encontrou (agentes, 20 Out). */}
    {(config.formatos||[]).some(m=>m.canais?.length===2&&brief.formatos.includes(m.formato))&&<p className={hint+' max-w-prose'}>Se a medida serve redes e painel, escolhe onde passa. O som fica só nas redes.</p>}
    <button className={button} disabled={ocupado} onClick={()=>fazer(async()=>{const c=await api('/config');setConfig(c);setNota(`Medidas actualizadas: ${(c.formatos||[]).length} disponíveis${(c.indisponiveis||[]).length?`, ${c.indisponiveis.length} ainda por preparar`:''}.`)})}>Actualizar medidas dos painéis</button>
    {brief.formatos.some(f=>!config.formatos?.some(m=>m.formato===f))&&<p role="alert" className="text-sm">Uma medida escolhida deixou de estar disponível. Desmarca a medida indisponível antes de rever a orientação.</p>}
    {(config.indisponiveis||[]).length>0&&<div className="space-y-2 text-sm"><p>Há painéis que ainda não podem ser escolhidos:</p><ul className="list-disc pl-5 space-y-1">{config.indisponiveis.map((m,i)=><li key={i}>{m.nome}: {m.motivo}</li>)}</ul></div>}
    <div className="flex flex-wrap gap-6 items-center"><label className="flex gap-3 items-center text-sm">Duração<select className={input+' w-auto'} value={brief.duracao} onChange={e=>alterar('duracao',Number(e.target.value))}>{[15,20,25].map(n=><option key={n} value={n}>{n} segundos</option>)}</select></label>{/* Só com algum formato nas redes: num pedido só para painel a escolha não existe (Codex X-39). Mesma regra que lib/criar/formatos.js, usosDoFormato. */}{!config?.semSom&&brief.formatos.some(f=>(brief.usos?.[f]||[f==='1080x1920'||f==='1080x1080'?'social':'dooh']).includes('social'))&&<label className="flex gap-3 items-center text-sm"><input type="checkbox" checked={brief.somSocial} onChange={e=>alterar('somSocial',e.target.checked)}/>Som nas redes sociais</label>}</div>
   </section>
   <div className="space-y-3"><div className="flex flex-wrap gap-3"><button className={primary} onClick={()=>fazer(prepararProposta)}>{ocupado?'A preparar…':'Preparar proposta'}</button><button className={button} onClick={()=>fazer(async()=>{const r=await api('/orientacao',{brief:entrada(),interpretacaoChave:sugestao?.chave||iaChave});setOrientacao(r);setIdSugestao(sugestao?r.id:null);setConfirmados([]);setNota('Revê a orientação e confirma os dados.')})}>Escrever eu os textos</button></div><p className={hint}>Preparar proposta usa IA (até 0,25 €, dos créditos da conta). Nada fica aprovado sem a tua confirmação.</p></div>
  </fieldset>
  {sugestao&&!sugestao.rascunho&&<section className="space-y-3 border-t border-border pt-6"><h2 className="text-xl">A proposta das referências</h2>{sugestao.dono==='fornecedor'&&<p role="alert" className="text-sm border border-border p-3">Esta campanha parece ser de um fornecedor ou de uma marca parceira, e não da tua marca. Confirma que podes usar estas imagens em painéis e redes sociais, ou escolhe uma campanha tua.</p>}<p>{semIdsCriar(sugestao.resumo)}</p>{sugestao.direccaoCriativa&&<div><p className={label}>Direcção criativa para o vídeo</p><p className={hint}>{semIdsCriar(sugestao.direccaoCriativa)}</p></div>}{(sugestao.observacoes||[]).map((s,n)=><p className={hint} key={'observacao-'+n}>{semIdsCriar(s)}</p>)}{(sugestao.incertezas||[]).map((s,n)=><p role="status" className={hint} key={'incerteza-'+n}>{semIdsCriar(s)}</p>)}{(sugestao.avisosImagens||[]).map((s,n)=><p role="status" className={hint} key={'imagem-'+n}>{s}</p>)}{sugestao.imagensVistas?.length>0&&<p className={hint}>A proposta considerou {sugestao.imagensVistas.length} {sugestao.imagensVistas.length===1?'imagem':'imagens'} das referências. Vais rever os materiais sugeridos antes de confirmar.</p>}<p className={hint}>Corrige o tema e a mensagem se for preciso e usa «Escrever eu os textos» para actualizar.</p>{/* O consumo da sugestão sai da nota e fica num «Detalhes» com duas casas, como o do vídeo (cliente simulado, 3 Nov). */}{sugestao.custoEurRegistado!=null&&<details className="text-sm"><summary className="cursor-pointer underline underline-offset-4">Detalhes</summary><p className={`mt-2 ${hint}`}>Consumo registado: {Number(sugestao.custoEurRegistado||0).toFixed(2).replace('.',',')} €.</p></details>}</section>}
  {orientacao&&<section className="space-y-4 border-t border-border pt-6"><h2 className="text-xl">Antes de criar</h2><ResumoDestinosCriar destinos={orientacao.orientacao.destinos}/><p>{orientacao.orientacao.entendimento}</p>{orientacao.orientacao.alertas.map((a,i)=><p key={i} className={hint}>{a}</p>)}{orientacao.orientacao.dados.map(d=><label key={d.id} className="flex gap-3 items-start text-sm"><input type="checkbox" disabled={orientacao.orientacao.estado==='orientacao_confirmada'} checked={orientacao.orientacao.estado==='orientacao_confirmada'||confirmados.includes(d.id)} onChange={e=>setConfirmados(e.target.checked?[...confirmados,d.id]:confirmados.filter(x=>x!==d.id))}/>Confirmo {TIPOS_DADO_CRIAR[d.tipo]||d.tipo}: {d.valor}</label>)}{orientacao.orientacao.estado==='orientacao_confirmada'&&<p role="status" className={hint}>Orientação confirmada e guardada. Podes voltar a abrir os materiais.</p>}<button disabled={ocupado||!podeMexer} className={primary} onClick={()=>fazer(confirmarERever)}>{orientacao.orientacao.estado==='orientacao_confirmada'?'Rever materiais confirmados':'Confirmar orientação e rever os materiais'}</button></section>}
  {proposta&&<section ref={refProposta} className="space-y-5 border-t border-border pt-6 scroll-mt-24"><h2 className="text-xl">O que pode aparecer no vídeo</h2><ResumoDestinosCriar destinos={proposta.destinos}/><div><label htmlFor="criar-frases" className={label}>Frases exactas, uma por linha</label><textarea id="criar-frases" className={input} rows={4} value={textos} onChange={e=>{const antes=linhasCriar(textos),novas=linhasCriar(e.target.value);setSoRedes(m=>marcasCriar(antes,novas,m,naRua));setTextos(e.target.value);setAprovarTextos(false)}}/>{(()=>{/* O preço do pedido que não está em nenhuma frase (utilizadora virgem, 4 Nov: o vídeo ia sair só com «Bolo-Rei»). */const norm=x=>String(x||'').replace(/\s+/g,'').toLowerCase(),precos=[...new Set((String((brief.mensagem||'')+' '+(brief.informacao||'')).match(/\d+(?:,\d+)?\s?€/g)||[]))],frases=norm(textos),falta=precos.filter(x=>!frases.includes(norm(x)));return falta.length?<p role="note" className="mt-2 text-sm text-accent">{`O teu pedido diz ${falta.join(' e ')}, e nenhuma frase o diz. Se for para aparecer no vídeo, junta-o numa linha.`}</p>:null})()}<p className={'mt-2 '+hint}>Até quatro frases curtas. A app escolhe o ritmo e a composição.</p></div>{proposta.painelPartilhado&&<p role="status" className={hint}>Este formato passa nas redes e no {oPainel}: as frases aparecem nos dois.</p>}{proposta.painelPartilhado&&!naRua&&linhasCriar(textos).some(t=>/portes|envio|entrega|encomend|online/i.test(t))&&<p role="status" className="text-sm border border-border p-3">Há uma frase sobre envios ou compras online, e no painel quem a lê já está na loja. Reescreve-a, ou volta à orientação e dá ao painel um formato só dele.</p>}{proposta.painel&&linhasCriar(textos).length>0&&<fieldset className="space-y-2"><legend className={label}>Frases no {oPainel}</legend><p className={hint}>{naRua?'O painel é visto de passagem e de longe. Desliga as frases que só servem nas redes.':'O painel é visto por quem já está na loja. Frases sobre envios, portes ou compras online ficam só nas redes.'}</p>{linhasCriar(textos).map((t,n)=><label key={n} className="flex gap-3 items-start text-sm"><input type="checkbox" checked={!soRedes[n]} onChange={e=>{const m=linhasCriar(textos).map((_,i)=>Boolean(soRedes[i]));m[n]=!e.target.checked;setSoRedes(m);setAprovarTextos(false)}}/><span>{t} <span className={hint}>{soRedes[n]?'· só nas redes':'· também no '+oPainel}</span></span></label>)}</fieldset>}{(config?.maxImagens||4)>4&&<div><label htmlFor="criar-apoio" className={label}>Outros textos que podem aparecer (opcional), um por linha</label><textarea id="criar-apoio" className={input} rows={4} value={textosApoio} onChange={e=>{setTextosApoio(e.target.value);setAprovarTextos(false)}}/><p className={'mt-2 '+hint}>Datas, condições, serviços ou categorias, tal como estão no site. O vídeo usa as que servirem a ideia. Até oito.</p></div>}<div><label htmlFor="criar-final" className={label}>Chamada para ação no fim do vídeo</label><input id="criar-final" className={input} value={cta} onChange={e=>{setCta(e.target.value);setAprovarTextos(false)}}/></div>{proposta.painel&&<div><label htmlFor="criar-cta-painel" className={label}>Chamada para ação no {oPainel}</label><input id="criar-cta-painel" className={input} value={ctaPainel} onChange={e=>{setCtaPainel(e.target.value);setAprovarTextos(false)}}/><p className={'mt-2 '+hint}>{naRua?'No painel ninguém clica, mas um endereço curto lê-se de longe: algo como «'+exemploCtaRuaCriar(tratamento)+'».':'No painel ninguém clica: algo como «Já nesta loja».'} Se ficar vazio, o painel usa a chamada para ação acima.</p></div>}{proposta.painel&&(()=>{/* Painéis na rua e dentro (4 Nov, rondas 9 a 11): os da rua com a sua chamada. */const rua=paineisNaRuaCriar(brief,config);const partilhados=paineisRuaPartilhadosCriar(brief,config);const comoRedes=paineisRuaComoRedesCriar(brief,config);if(!rua.length&&comoRedes.length&&!partilhados.length)return <p className={hint}>{`O painel virado para a rua (${comoRedes.join(', ')}) tem a forma do vídeo das redes sociais (vertical): é o mesmo vídeo nos dois, por isso a chamada também é a mesma. Para ter outra frase na montra, tira as redes sociais deste pedido e faz-lhes um à parte.`}</p>;if(!rua.length&&partilhados.length)return <p className={hint}>{`O painel virado para a rua (${partilhados.join(', ')}) faz o mesmo desenho do vídeo que o de dentro da loja: a chamada é a mesma nos dois, por isso deixa o site de fora, ou escolhe «Virados para a rua» em «Os painéis estão».`}</p>;return rua.length&&rua.length<(config?.formatos||[]).filter(f=>(f.origem==='painel'||f.painel)&&brief.formatos.includes(f.formato)).length?<div><label htmlFor="criar-cta-painel-rua" className={label}>Chamada no painel virado para a rua</label><input id="criar-cta-painel-rua" className={input} value={ctaPainelRua} placeholder={'Ex.: «'+exemploCtaRuaCriar(tratamento)+'»'} onChange={e=>{setCtaPainelRua(e.target.value);setAprovarTextos(false)}}/><p className={'mt-2 '+hint}>{`Só ${rua.length===1?'no painel':'nos painéis'} ${rua.map(f=>config.formatos.find(m=>m.formato===f)?.nome||f).join(', ')}: quem passa vê de longe, e um endereço curto lê-se. Se ficar vazio, usa a chamada do painel acima.`}</p></div>:null})()}{proposta.candidatos.length>0&&<p className={hint}>{(config?.maxImagens||4)>4?`Até ${config.maxImagens} imagens: o motor escolhe as que servem a ideia.`:'Escolhe até quatro imagens.'} Revê também qualquer preço ou mensagem já impresso nelas.</p>}{proposta.materiaisSugeridos&&<p role="status" className={hint}>Materiais sugeridos, revê antes de confirmar.</p>}<div className="grid gap-4 grid-cols-2 md:grid-cols-4">{proposta.candidatos.map(m=><label key={m.id} className="space-y-2 cursor-pointer border border-border p-3"><img className="w-full h-32 object-contain" loading="lazy" src={m.url} alt={m.nome}/><span className="flex gap-2 items-start text-sm"><input type="checkbox" checked={imagens.includes(m.id)} onChange={e=>{setImagens(e.target.checked?[...imagens,m.id]:imagens.filter(x=>x!==m.id));setAprovarImagens(false)}}/>{nomeAVista(m.nome)}</span></label>)}</div>{proposta.candidatos.length===0&&<p role="status" className="text-sm border border-border p-3">As referências não trazem fotografias: o vídeo faz-se só com texto e com as cores da marca. Para usar imagens, volta à orientação e junta uma fotografia ou o endereço de uma página com imagens.</p>}{proposta.duracao>=20&&imagens.length===1&&<p role="status" className={hint}>Com uma só imagem, {proposta.duracao} segundos tendem a repetir-se. Escolhe mais imagens, ou volta à orientação e escolhe 15 segundos.</p>}{/* Uma caixa em vez de duas (florista virgem: seis confirmações em duas rondas); o servidor recebe as duas confirmações. */}<label className="flex gap-3 text-sm"><input type="checkbox" checked={aprovarTextos&&aprovarImagens} onChange={e=>{setAprovarTextos(e.target.checked);setAprovarImagens(e.target.checked)}}/>{proposta.candidatos.length?'Confirmo os textos, preços, datas, condições e alegações, e as imagens escolhidas.':'Confirmo os textos, preços, datas, condições e alegações. O vídeo sai sem fotografias.'}</label><p className={hint}>Custo máximo de {Number(config?.limites?.eur||1.2).toFixed(2).replace('.',',')} € para todos os formatos. O vídeo termina com a chamada para ação{config?.linhaIA?' e a indicação de que foi feito com IA':''}{config?.fechoDoAgente?(config?.linhaIA?'; confirmamos que as duas lá estão':'; confirmamos que lá está'):''}. Um pedido iniciado não é pago novamente de forma automática.</p>{semFrasesNoPainel&&<p id="criar-sem-painel" role="alert" className="text-sm">Para aprovar, liga pelo menos uma frase no painel{naRua?'':' da loja'}.</p>}<button aria-describedby={semFrasesNoPainel?'criar-sem-painel':undefined} disabled={ocupado||!podeMexer||!config.video||semFrasesNoPainel} className={primary} onClick={()=>fazer(async()=>{const p=await api('/orientacao/'+proposta.orientacaoId+'/proposta',{textos:textos.split('\n').map(s=>s.trim()).filter(Boolean),textosApoio:textosApoio.split('\n').map(s=>s.trim()).filter(Boolean),cta,...(proposta.painel?{textosSoRedes:linhasCriar(textos).filter((_,i)=>soRedes[i]),ctaPainel:ctaPainel.trim(),...(ctaPainelRua.trim()&&paineisNaRuaCriar(brief,config).length?{ctaPainelRua:ctaPainelRua.trim(),paineisNaRua:paineisNaRuaCriar(brief,config)}:{})}:{}),imagens,confirmarTextos:aprovarTextos,confirmarMateriais:aprovarImagens});setResultado(await api('/video/'+p.id+'/gerar',{}));setProposta(null);setConfirmarVideo(false);setNota('');await carregar()})}>{config.video?'Aprovar textos e imagens e criar vídeo':'Geração de vídeo ainda não activa'}</button></section>}
  
  <section id="criacoes" className="space-y-4 border-t border-border pt-6 scroll-mt-24"><h2 className="text-xl">As tuas criações</h2>{!videos.length&&!historico.length&&<p className={hint}>Os teus vídeos e pedidos guardados aparecem aqui.</p>}{/* Linhas iguais não se distinguiam: cada uma leva a data, o estado e a marca da que está aberta (agentes, 20 Out). */}{/* As tentativas que não chegaram a vídeo ficam num bloco fechado: eram três linhas iguais «Não foi criado» (parceira virgem, 4 Nov). */}{videos.filter(v=>v.estado!=='recusado'||resultado?.id===v.id).map(v=><div key={v.id} aria-current={resultado?.id===v.id||undefined} className="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-3">{/* A capa do vídeo ao lado do nome (crítica visual, 3 Nov). */}<CapaDeVideo slug={slug} video={v} reservar className="w-10 shrink-0 rounded-sm border border-border"/><p className="text-sm flex-1 min-w-[12rem]">{['fila','a_criar'].includes(resultado?.id===v.id&&resultado.estado?resultado.estado:v.estado)&&<ATrabalhar className="text-primary mr-2"/>}{v.tema} · {({aprovado:'Aprovado',video_por_rever:'Por rever',fila:'Na fila',a_criar:'A criar',preparado:'Pronto para criar',recusado:'Não foi criado',interrompido:'Interrompido'})[resultado?.id===v.id&&resultado.estado?resultado.estado:v.estado]||v.estado}</p><p className={hint}>{[quandoCriar(v.criadoEm),resultado?.id===v.id&&'aberto acima'].filter(Boolean).join(' · ')}</p><div className="flex flex-wrap gap-2"><button className={button} onClick={()=>fazer(async()=>{setResultado(await api('/video/'+v.id));setConfirmarVideo(false)})}>Ver vídeo e estado</button>{(()=>{const h=historico.find(h=>h.id===v.orientacaoId);return h&&<button className={button} disabled={ocupado||!podeMexer} onClick={()=>novaOrientacao(h)}>Novo pedido a partir deste</button>})()}</div></div>)}{(()=>{const falhadas=videos.filter(v=>v.estado==='recusado'&&resultado?.id!==v.id);return falhadas.length>0&&<details className="text-sm"><summary className="cursor-pointer text-muted-foreground underline underline-offset-4">{falhadas.length===1?'1 tentativa que não chegou a vídeo':`${falhadas.length} tentativas que não chegaram a vídeo`}</summary><ul className="mt-2 space-y-2">{falhadas.map(v=><li key={v.id} className="flex flex-wrap items-center justify-between gap-3"><span className={hint}>{v.tema} · {quandoCriar(v.criadoEm)}</span><button className={button} onClick={()=>fazer(async()=>{setResultado(await api('/video/'+v.id));setConfirmarVideo(false)})}>Ver o que aconteceu</button></li>)}</ul></details>})()}{/* Uma linha por criação: a orientação que já tem vídeo não aparece outra vez (NESCAFÉ, 30 Out: tudo em duplicado). */}{historico.filter(h=>!videos.some(v=>v.orientacaoId===h.id)).map(h=><div key={h.id} aria-current={proposta?.orientacaoId===h.id||undefined} className="flex flex-wrap justify-between gap-3"><p className="text-sm">{h.orientacao.brief.oferta.nome} · orientação confirmada</p><p className={hint}>{[quandoCriar(h.orientacao.confirmadoEm||h.criadoEm),h.orientacao.brief.formatos.length===1?'1 formato':h.orientacao.brief.formatos.length+' formatos',proposta?.orientacaoId===h.id&&'aberta acima'].filter(Boolean).join(' · ')}</p><div className="flex flex-wrap gap-2"><button className={button} disabled={ocupado||!podeMexer} onClick={()=>novaOrientacao(h)}>Novo pedido a partir deste</button><button className={button} disabled={ocupado||!podeMexer} onClick={()=>fazer(()=>{edicoes.current=null;return abrirProposta(h.id)})}>Rever textos e imagens</button></div></div>)}</section>
 </div>;
}
