/*
 * O desenho dos banners que o assistente gera (gerar_banner).
 *
 * Só regra e HTML, sem banco e sem importação com "@/": o teste roda no
 * `node --test` puro (ver ../gerador-de-banner.test.ts) e o roteiro de amostras
 * monta as imagens sem subir o painel.
 *
 * O HTML entra no Chrome do servidor com o tamanho exato do formato e vira uma
 * imagem. Tudo o que ele usa vem dentro dele (fotos como data:), menos as
 * fontes, que vêm do Google. Todo texto vem da pessoa e passa por
 * `escaparHtml`. O tamanho das letras sai de uma estimativa feita aqui e um
 * pequeno script na página só diminui (nunca aumenta) até caber: título em no
 * máximo duas linhas, e reticências se nem assim couber.
 */

/* ================================================================ formatos */

export type ChaveDoFormato = "carrossel" | "carrossel vendor" | "banner duplo" | "faixa de seção" | "story" | "quadrado";

/* largo = carrossel; medio = banner duplo; faixa = 8:1; quadrado; alto = story. */
export type Disposicao = "largo" | "medio" | "faixa" | "quadrado" | "alto";

export type Formato = { chave: ChaveDoFormato; largura: number; altura: number; disposicao: Disposicao; uso: string };

export const FORMATOS: Record<ChaveDoFormato, Formato> = {
  carrossel: { chave: "carrossel", largura: 1920, altura: 540, disposicao: "largo", uso: "carrossel do topo da loja (32:9)" },
  "carrossel vendor": { chave: "carrossel vendor", largura: 1920, altura: 600, disposicao: "largo", uso: "primeiro banner do modelo Vendor (16:5)" },
  "banner duplo": { chave: "banner duplo", largura: 1600, altura: 1000, disposicao: "medio", uso: "2º e 3º banners do modelo Vendor (8:5)" },
  "faixa de seção": { chave: "faixa de seção", largura: 1920, altura: 240, disposicao: "faixa", uso: "seção de banner de imagem da página inicial (8:1)" },
  story: { chave: "story", largura: 1080, altura: 1920, disposicao: "alto", uso: "story (9:16)" },
  quadrado: { chave: "quadrado", largura: 1080, altura: 1080, disposicao: "quadrado", uso: "post quadrado (1:1)" },
};

export const NOMES_DOS_FORMATOS = Object.keys(FORMATOS) as ChaveDoFormato[];

/**
 * O desenho que serve a um tamanho qualquer (largura ÷ altura): bem
 * comprido é faixa, largo é largo, um pouco mais largo que alto é médio,
 * perto do quadrado é quadrado e em pé é alto. O formato da lista mais
 * parecido vem junto (os modelos leem o resto dele).
 */
export function disposicaoPelaProporcao(largura: number, altura: number): { disposicao: Disposicao; formato: ChaveDoFormato } {
  const razao = largura / Math.max(1, altura);
  if (razao >= 5) return { disposicao: "faixa", formato: "faixa de seção" };
  if (razao >= 2.2) return { disposicao: "largo", formato: razao >= 3.4 ? "carrossel" : "carrossel vendor" };
  if (razao >= 1.25) return { disposicao: "medio", formato: "banner duplo" };
  if (razao >= 0.75) return { disposicao: "quadrado", formato: "quadrado" };
  return { disposicao: "alto", formato: "story" };
}

function semAcento(valor: unknown) {
  return String(valor ?? "")
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9:]+/g, " ")
    .trim();
}

const APELIDOS_DO_FORMATO: Array<[RegExp, ChaveDoFormato]> = [
  [/vendor|16 ?:? ?5|1920 ?x ?600/, "carrossel vendor"],
  [/duplo|8 ?:? ?5|1600 ?x ?1000/, "banner duplo"],
  [/faixa|secao|8 ?:? ?1|1920 ?x ?240/, "faixa de seção"],
  [/story|stories|9 ?:? ?16|1080 ?x ?1920|vertical/, "story"],
  [/quadrad|post|feed|1 ?:? ?1|1080 ?x ?1080/, "quadrado"],
  [/carrossel|carousel|hero|topo|principal|32 ?:? ?9|1920 ?x ?540/, "carrossel"],
];

/** O formato pelo que o assistente mandou ("faixa de secao", "Story", "16:5"...). Vazio = carrossel. */
export function lerFormato(valor: unknown): ChaveDoFormato | null {
  const texto = semAcento(valor);
  if (!texto) return "carrossel";
  const exato = NOMES_DOS_FORMATOS.find((nome) => semAcento(nome) === texto);
  if (exato) return exato;
  for (const [padrao, chave] of APELIDOS_DO_FORMATO) if (padrao.test(texto)) return chave;
  return null;
}

/* ================================================================ modelos */

export const MODELOS = ["imagem à direita", "imagem de fundo com degradê", "centralizado", "oferta com selo de desconto", "minimalista"] as const;
export type Modelo = (typeof MODELOS)[number];

const APELIDOS_DO_MODELO: Array<[RegExp, Modelo]> = [
  [/degrade|gradiente|fundo/, "imagem de fundo com degradê"],
  [/centr/, "centralizado"],
  [/oferta|selo|desconto|promo/, "oferta com selo de desconto"],
  [/minimal|limpo|simples/, "minimalista"],
  [/direita|lado|padrao/, "imagem à direita"],
];

/** O modelo pelo que o assistente mandou. Vazio = "imagem à direita". */
export function lerModelo(valor: unknown): Modelo | null {
  const texto = semAcento(valor);
  if (!texto) return "imagem à direita";
  const exato = MODELOS.find((nome) => semAcento(nome) === texto);
  if (exato) return exato;
  for (const [padrao, modelo] of APELIDOS_DO_MODELO) if (padrao.test(texto)) return modelo;
  return null;
}

/* ================================================================== cores */

export function hexValido(valor: unknown): string | null {
  const limpo = String(valor ?? "").trim().replace(/^#/, "").toUpperCase();
  if (/^[0-9A-F]{6}$/.test(limpo)) return `#${limpo}`;
  if (/^[0-9A-F]{3}$/.test(limpo)) return `#${limpo.split("").map((letra) => letra + letra).join("")}`;
  return null;
}

function rgb(hex: string): [number, number, number] {
  const limpo = (hexValido(hex) ?? "#000000").slice(1);
  return [0, 2, 4].map((i) => parseInt(limpo.slice(i, i + 2), 16)) as [number, number, number];
}

function paraHex([r, g, b]: number[]) {
  return `#${[r, g, b].map((canal) => Math.max(0, Math.min(255, Math.round(canal))).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
}

/** Luminância relativa (WCAG): 0 = preto, 1 = branco. */
export function luminancia(hex: string) {
  const [r, g, b] = rgb(hex).map((canal) => {
    const c = canal / 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

/** Contraste WCAG entre duas cores (1 a 21). Texto grande pede 3; texto comum, 4,5. */
export function contraste(a: string, b: string) {
  const [claro, escuro] = [luminancia(a), luminancia(b)].sort((x, y) => y - x);
  return (claro + 0.05) / (escuro + 0.05);
}

export const BRANCO = "#FFFFFF";
export const QUASE_PRETO = "#141414";

/**
 * Branco ou quase preto sobre o fundo. O branco ganha sempre que passa do
 * contraste de texto grande (3): letra branca em fundo vermelho, rosa ou azul
 * é o que se espera de um banner de loja; o preto fica para os fundos claros.
 */
export function textoSobre(fundo: string) {
  if (contraste(BRANCO, fundo) >= 3) return BRANCO;
  return contraste(BRANCO, fundo) >= contraste(QUASE_PRETO, fundo) ? BRANCO : QUASE_PRETO;
}

/** Mistura `a` com `b` (t = 0 dá `a`, t = 1 dá `b`). */
export function misturar(a: string, b: string, t: number) {
  const x = rgb(a);
  const y = rgb(b);
  return paraHex(x.map((canal, i) => canal + (y[i] - canal) * t));
}

/** Escurece (ou clareia) a cor até ter o contraste mínimo com o fundo. */
export function comContraste(cor: string, fundo: string, minimo: number) {
  if (contraste(cor, fundo) >= minimo) return cor;
  const alvo = luminancia(fundo) > 0.4 ? "#000000" : BRANCO;
  for (let t = 0.1; t <= 1; t += 0.1) {
    const nova = misturar(cor, alvo, t);
    if (contraste(nova, fundo) >= minimo) return nova;
  }
  return alvo === BRANCO ? BRANCO : QUASE_PRETO;
}

/* Roxo da Ktalogu: a cor de quem ainda não escolheu a cor da loja. */
export const COR_PADRAO = "#6D28D9";
const AMARELO = "#FFD400";

export type CoresDoBanner = {
  fundo: string;
  texto: string;
  /* Botão, selo e detalhes. */
  destaque: string;
  textoDoDestaque: string;
  /* Um tom do próprio fundo, para formas decorativas. */
  tom: string;
  /* Texto secundário (subtítulo), um pouco mais suave que o título. */
  textoSuave: string;
};

/**
 * As cores do banner. O que a pessoa escolheu vale; o resto sai da cor da
 * loja. O texto (quando não foi escolhido) é sempre o de maior contraste com o
 * fundo. O destaque padrão (botão, selo):
 *  - na oferta, o amarelo de cartaz sobre fundo escuro ou saturado;
 *  - nos outros, branco sobre fundo escuro ou saturado, com o texto do botão
 *    na própria cor da loja (é o que as marcas fazem; o amarelo em todo banner
 *    dava cara de panfleto);
 *  - sobre fundo claro, a cor da loja (ou quase preto, se ela não aparecer).
 */
export function coresDoBanner(pedido: { modelo: Modelo; corDaLoja?: string | null; fundo?: string | null; texto?: string | null; destaque?: string | null }): CoresDoBanner {
  const daLoja = hexValido(pedido.corDaLoja) ?? COR_PADRAO;
  const minimalista = pedido.modelo === "minimalista";
  const oferta = pedido.modelo === "oferta com selo de desconto";
  const fundo = hexValido(pedido.fundo) ?? (minimalista ? BRANCO : daLoja);
  const claro = luminancia(fundo) > 0.45;
  const texto = hexValido(pedido.texto) ?? textoSobre(fundo);
  let destaque = hexValido(pedido.destaque);
  const escolhido = Boolean(destaque);
  if (!destaque) {
    if (minimalista) destaque = comContraste(daLoja, fundo, 3);
    else if (claro) destaque = contraste(daLoja, fundo) >= 3 ? daLoja : QUASE_PRETO;
    else destaque = oferta ? AMARELO : BRANCO;
    /* O destaque padrão nunca some no fundo (ex.: loja amarela com banner amarelo). */
    if (contraste(destaque, fundo) < 1.8) destaque = claro ? QUASE_PRETO : BRANCO;
  }
  const tom = claro ? misturar(fundo, "#000000", 0.07) : misturar(fundo, BRANCO, 0.14);
  const textoSuave = misturar(texto, fundo, 0.14);
  /* Botão branco: a letra na cor do fundo, quando ela dá leitura; senão, o de sempre. */
  const textoDoDestaque =
    !escolhido && destaque === BRANCO && contraste(fundo, BRANCO) >= 4.5 ? fundo : textoSobre(destaque);
  return { fundo, texto, destaque, textoDoDestaque, tom, textoSuave };
}

/* ================================================================= fontes */

/* As fontes de um peso só (pedir 700 ao Google derruba a folha inteira). */
const UM_PESO = new Set([
  "instrument-serif",
  "dm-serif-display",
  "marcellus",
  "anton",
  "bebas-neue",
  "archivo-black",
  "abril-fatface",
  "alfa-slab-one",
  "righteous",
  "pacifico",
  "sacramento",
  "great-vibes",
]);
const CONDENSADAS = new Set(["anton", "bebas-neue", "darker-grotesque", "instrument-serif", "cormorant-garamond"]);
const LARGAS = new Set(["unbounded", "archivo-black", "syne", "alfa-slab-one", "righteous", "space-mono", "jetbrains-mono", "ibm-plex-mono", "roboto-mono", "abril-fatface", "lexend"]);
const MANUSCRITAS = new Set(["caveat", "dancing-script", "pacifico", "sacramento", "great-vibes"]);
const SERIFADAS = new Set([
  "fraunces",
  "playfair-display",
  "instrument-serif",
  "lora",
  "eb-garamond",
  "cormorant-garamond",
  "libre-baskerville",
  "crimson-pro",
  "merriweather",
  "source-serif-4",
  "spectral",
  "newsreader",
  "literata",
  "dm-serif-display",
  "bodoni-moda",
  "marcellus",
  "abril-fatface",
  "alfa-slab-one",
]);
const NOME_ESPECIAL: Record<string, string> = {
  "eb-garamond": "EB Garamond",
  "dm-sans": "DM Sans",
  "dm-serif-display": "DM Serif Display",
  "ibm-plex-mono": "IBM Plex Mono",
  "jetbrains-mono": "JetBrains Mono",
};

export const FONTE_PADRAO_DO_TITULO = "montserrat";
export const FONTE_PADRAO_DO_TEXTO = "inter";

function chaveSegura(chave: unknown, padrao: string) {
  const limpa = String(chave ?? "").trim().toLowerCase();
  return /^[a-z0-9-]{2,40}$/.test(limpa) ? limpa : padrao;
}

/** O nome da família no Google Fonts pela chave gravada no banco ("source-serif-4" → "Source Serif 4"). */
export function familiaDaFonte(chave: string) {
  if (NOME_ESPECIAL[chave]) return NOME_ESPECIAL[chave];
  return chave
    .split("-")
    .map((parte) => parte.charAt(0).toUpperCase() + parte.slice(1))
    .join(" ");
}

/** O peso do título: 700, ou 400 nas fontes que só têm esse (sem negrito falso). */
export function pesoDoTitulo(chave: string) {
  return UM_PESO.has(chave) ? 400 : 700;
}

/** Largura média de uma letra, em frações do tamanho da fonte (negrito). */
export function fatorDaFonte(chave: string, maiusculas = false) {
  let fator = 0.56;
  if (CONDENSADAS.has(chave)) fator = 0.44;
  else if (LARGAS.has(chave)) fator = 0.68;
  else if (MANUSCRITAS.has(chave)) fator = 0.5;
  else if (SERIFADAS.has(chave)) fator = 0.54;
  /* Bebas já é toda em maiúsculas. */
  if (maiusculas && chave !== "bebas-neue") fator *= 1.16;
  return fator;
}

function pilhaDaFonte(chave: string) {
  const reserva = SERIFADAS.has(chave) ? "Georgia, 'Times New Roman', serif" : MANUSCRITAS.has(chave) ? "cursive" : "Arial, Helvetica, sans-serif";
  return `'${familiaDaFonte(chave)}', ${reserva}`;
}

/** A folha do Google Fonts com as duas famílias (sem repetir). */
export function linkDasFontes(chaves: string[]) {
  const unicas = [...new Set(chaves)];
  const familias = unicas.map((chave) => {
    const nome = familiaDaFonte(chave).replace(/ /g, "+");
    return UM_PESO.has(chave) ? `family=${nome}` : `family=${nome}:wght@400;700`;
  });
  return `https://fonts.googleapis.com/css2?${familias.join("&")}&display=block`;
}

/* ================================================================== texto */

export function escaparHtml(valor: unknown) {
  return String(valor ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

/**
 * O maior tamanho de letra (px) em que o texto cabe na caixa, em até `linhas`
 * linhas. Estimativa pela largura média das letras; a página ainda confere
 * medindo de verdade e só diminui.
 */
export function tamanhoQueCabe(opcoes: { texto: string; largura: number; maximo: number; minimo: number; linhas: number; fator: number }) {
  const texto = opcoes.texto.trim();
  if (!texto) return Math.round(opcoes.maximo);
  const palavras = texto.split(/\s+/);
  const maiorPalavra = Math.max(...palavras.map((palavra) => palavra.length));
  /* A maior palavra precisa caber numa linha só. */
  const pelaPalavra = opcoes.largura / (maiorPalavra * opcoes.fator);
  /* O texto todo nas linhas permitidas, com folga para a quebra entre palavras. */
  const folga = opcoes.linhas > 1 ? 0.86 : 0.98;
  const peloTexto = (opcoes.largura * opcoes.linhas * folga) / (texto.length * opcoes.fator);
  const tamanho = Math.min(opcoes.maximo, pelaPalavra, peloTexto);
  return Math.floor(Math.max(opcoes.minimo, tamanho));
}

/* ================================================================= imagens */

/* recorte = produto em fundo branco; transparente = PNG sem fundo; foto = cena. */
export type TipoDaImagem = "recorte" | "transparente" | "foto";
/* `fundo`: a cor da borda da foto (#RRGGBB), para o cartão ou círculo do recorte. */
export type ImagemDoBanner = { src: string; tipo: TipoDaImagem; largura: number; altura: number; fundo?: string | null };
export type LogoDoBanner = { src: string; largura: number; altura: number };

const DATA_URI = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/;

/** Só imagem embutida (data:) entra no HTML: a página não busca nada de fora. */
export function srcSeguro(src: unknown): string | null {
  const texto = String(src ?? "");
  return DATA_URI.test(texto) ? texto : null;
}

/* =================================================================== plano */

type Caixa = { x: number; y: number; w: number; h: number };

export type PlanoDoBanner = {
  texto: Caixa;
  /* painel = a foto ocupa um lado inteiro do banner, até a borda. */
  imagem: (Caixa & { forma: "circulo" | "cartao" | "livre" | "cobrir" | "painel" }) | null;
  /* Tamanhos máximos (px), antes de caber. */
  titulo: number;
  subtitulo: number;
  botao: number;
  logo: number;
  linhasDoTitulo: number;
  linhasDoSubtitulo: number;
  alinhamento: "esquerda" | "centro";
  /* faixa = tudo numa linha só (logo, textos, botão). */
  emLinha: boolean;
};

/** Onde fica cada coisa, pelo formato, pelo modelo e pelo tipo de imagem. */
export function planoDoBanner(formato: Formato, modelo: Modelo, imagem: TipoDaImagem | null): PlanoDoBanner {
  const { largura: w, altura: h, disposicao } = formato;
  const centro = modelo === "centralizado" || disposicao === "alto";
  /* Foto de cena na vitrine e na oferta: um lado inteiro do banner é dela. */
  const painel = imagem === "foto" && (modelo === "imagem à direita" || modelo === "oferta com selo de desconto");

  if (disposicao === "quadrado" || disposicao === "alto") {
    const pad = Math.round(w * 0.08);
    const altoDaImagem = imagem ? Math.round(h * (disposicao === "alto" ? 0.5 : 0.46)) : 0;
    let caixaDaImagem: PlanoDoBanner["imagem"] = null;
    if (painel) {
      const alto = Math.round(h * (disposicao === "alto" ? 0.54 : 0.5));
      const topo = alto + Math.round(pad * 0.7);
      return {
        texto: { x: pad, y: topo, w: w - pad * 2, h: h - topo - pad },
        imagem: { x: 0, y: 0, w, h: alto, forma: "painel" },
        titulo: Math.round(w * (disposicao === "alto" ? 0.12 : 0.092)),
        subtitulo: Math.round(w * (disposicao === "alto" ? 0.044 : 0.036)),
        botao: Math.round(w * (disposicao === "alto" ? 0.04 : 0.033)),
        logo: Math.round(w * (disposicao === "alto" ? 0.07 : 0.056)),
        linhasDoTitulo: 2,
        linhasDoSubtitulo: 2,
        alinhamento: centro ? "centro" : "esquerda",
        emLinha: false,
      };
    }
    if (imagem) {
      const cobre = modelo === "imagem de fundo com degradê" && imagem === "foto";
      /* A foto vai até o pé: o véu escuro faz a cama do texto (cortada no meio, ficava uma linha reta). */
      if (cobre) caixaDaImagem = { x: 0, y: 0, w, h, forma: "cobrir" };
      else {
        const lado = Math.min(w - pad * 2, altoDaImagem) - Math.round(pad * 0.4);
        const forma = imagem === "transparente" ? "livre" : imagem === "foto" ? (modelo === "minimalista" ? "cartao" : "circulo") : modelo === "minimalista" ? "livre" : "circulo";
        caixaDaImagem = { x: Math.round((w - lado) / 2), y: pad + Math.round((altoDaImagem - lado) / 2), w: lado, h: lado, forma };
      }
    }
    /* Oferta sem imagem: a estrela do selo ocupa o alto, e o texto vem abaixo dela. */
    const reservaDaEstrela = !imagem && modelo === "oferta com selo de desconto" ? Math.round(h * (disposicao === "alto" ? 0.37 : 0.38)) : 0;
    const topoDoTexto = imagem ? pad + altoDaImagem + Math.round(pad * 0.5) : pad + reservaDaEstrela;
    return {
      texto: { x: pad, y: topoDoTexto, w: w - pad * 2, h: h - topoDoTexto - pad },
      imagem: caixaDaImagem,
      titulo: Math.round(w * (disposicao === "alto" ? 0.125 : 0.096)),
      subtitulo: Math.round(w * (disposicao === "alto" ? 0.044 : 0.036)),
      botao: Math.round(w * (disposicao === "alto" ? 0.04 : 0.033)),
      logo: Math.round(w * (disposicao === "alto" ? 0.075 : 0.06)),
      linhasDoTitulo: 2,
      linhasDoSubtitulo: disposicao === "alto" ? 3 : 2,
      alinhamento: centro ? "centro" : "esquerda",
      emLinha: false,
    };
  }

  if (disposicao === "faixa") {
    const padX = Math.round(h * 0.3);
    const padY = Math.round(h * 0.14);
    let caixaDaImagem: PlanoDoBanner["imagem"] = null;
    let direita = padX;
    if (imagem) {
      if (modelo === "imagem de fundo com degradê" && imagem === "foto") {
        caixaDaImagem = { x: Math.round(w * 0.45), y: 0, w: Math.round(w * 0.55), h, forma: "cobrir" };
        direita = padX;
      } else {
        const lado = Math.round(h * 0.86);
        const forma = imagem === "foto" ? "cartao" : imagem === "transparente" ? "livre" : modelo === "minimalista" ? "livre" : "circulo";
        caixaDaImagem = { x: w - padX - lado, y: Math.round((h - lado) / 2), w: lado, h: lado, forma };
        direita = padX + lado + Math.round(h * 0.25);
      }
    }
    return {
      texto: { x: padX, y: padY, w: w - padX - direita, h: h - padY * 2 },
      imagem: caixaDaImagem,
      titulo: Math.round(h * 0.25),
      subtitulo: Math.round(h * 0.105),
      botao: Math.round(h * 0.095),
      logo: Math.round(h * 0.4),
      linhasDoTitulo: 2,
      linhasDoSubtitulo: 1,
      alinhamento: modelo === "centralizado" ? "centro" : "esquerda",
      emLinha: true,
    };
  }

  /* largo (carrossel) e medio (banner duplo): texto de um lado, imagem do outro. */
  const largo = disposicao === "largo";
  const pad = Math.round(largo ? h * 0.12 : h * 0.085);
  const vao = Math.round(largo ? h * 0.1 : w * 0.04);
  let caixaDaImagem: PlanoDoBanner["imagem"] = null;
  let caixaDoTexto: Caixa;
  if (imagem && modelo === "imagem de fundo com degradê") {
    if (imagem === "foto") caixaDaImagem = { x: 0, y: 0, w, h, forma: "cobrir" };
    else {
      const painel = Math.round(w * (largo ? 0.5 : 0.5));
      const lado = Math.min(painel - pad * 2, h - pad * 2);
      caixaDaImagem = { x: w - painel + Math.round((painel - lado) / 2), y: Math.round((h - lado) / 2), w: lado, h: lado, forma: "livre" };
    }
    caixaDoTexto = { x: pad, y: pad, w: Math.round(w * (largo ? 0.46 : 0.46)) - pad, h: h - pad * 2 };
  } else if (painel) {
    const x = Math.round(w * (largo ? 0.5 : 0.48));
    caixaDaImagem = { x, y: 0, w: w - x, h, forma: "painel" };
    caixaDoTexto = { x: pad, y: pad, w: x - vao - pad, h: h - pad * 2 };
  } else if (imagem && modelo === "centralizado" && imagem === "foto") {
    caixaDaImagem = { x: 0, y: 0, w, h, forma: "cobrir" };
    const larguraDoTexto = Math.round(w * (largo ? 0.62 : 0.8));
    caixaDoTexto = { x: Math.round((w - larguraDoTexto) / 2), y: pad, w: larguraDoTexto, h: h - pad * 2 };
  } else if (imagem) {
    const lado = Math.round(largo ? h * 0.84 : Math.min(h * 0.74, w * 0.42));
    const forma = imagem === "transparente" ? "livre" : imagem === "foto" ? (modelo === "minimalista" ? "cartao" : "circulo") : modelo === "minimalista" ? "livre" : "circulo";
    /* No centralizado, a imagem vai à esquerda e o texto se centra no resto. */
    const aEsquerda = modelo === "centralizado";
    const centroDaImagem = largo ? w - pad - lado / 2 - h * 0.08 : w * 0.73;
    const x = Math.round(aEsquerda ? w - centroDaImagem - lado / 2 : centroDaImagem - lado / 2);
    caixaDaImagem = { x, y: Math.round((h - lado) / 2), w: lado, h: lado, forma };
    caixaDoTexto = aEsquerda
      ? { x: x + lado + vao, y: pad, w: w - (x + lado + vao) - pad, h: h - pad * 2 }
      : { x: pad, y: pad, w: x - vao - pad, h: h - pad * 2 };
  } else {
    const larguraDoTexto = modelo === "centralizado" ? Math.round(w * (largo ? 0.66 : 0.82)) : Math.round(w * (largo ? 0.6 : 0.72));
    caixaDoTexto = { x: modelo === "centralizado" ? Math.round((w - larguraDoTexto) / 2) : pad, y: pad, w: larguraDoTexto, h: h - pad * 2 };
  }
  return {
    texto: caixaDoTexto,
    imagem: caixaDaImagem,
    titulo: Math.round(largo ? h * 0.19 : h * 0.115),
    subtitulo: Math.round(largo ? h * 0.054 : h * 0.036),
    botao: Math.round(largo ? h * 0.044 : h * 0.03),
    logo: Math.round(largo ? h * 0.12 : h * 0.08),
    linhasDoTitulo: 2,
    linhasDoSubtitulo: 2,
    alinhamento: modelo === "centralizado" ? "centro" : "esquerda",
    emLinha: false,
  };
}

/* ==================================================================== html */

export type PedidoDoBanner = {
  formato: ChaveDoFormato;
  /* Um tamanho fora da lista (o editor da página inicial pede o banner na
     proporção exata da seção). Sem isto, o tamanho do formato. */
  medida?: { largura: number; altura: number; disposicao: Disposicao } | null;
  modelo: Modelo;
  titulo: string;
  subtitulo?: string | null;
  botao?: string | null;
  selo?: string | null;
  imagem?: ImagemDoBanner | null;
  logo?: LogoDoBanner | null;
  /* Quando a loja não tem logo (ou ela não abriu), o nome entra no lugar. */
  nomeDaLoja?: string | null;
  usarLogo?: boolean;
  cores: CoresDoBanner;
  fonteDoTitulo?: string | null;
  fonteDoTexto?: string | null;
};

const px = (valor: number) => `${Math.round(valor)}px`;

function caixaCss(caixa: Caixa) {
  return `left:${px(caixa.x)};top:${px(caixa.y)};width:${px(caixa.w)};height:${px(caixa.h)}`;
}

/* A estrela do selo de oferta: 18 pontas, como nos cartazes de loja. */
function estrela(cor: string) {
  const pontos: string[] = [];
  const n = 18;
  for (let i = 0; i < n * 2; i += 1) {
    const raio = i % 2 === 0 ? 50 : 42;
    const angulo = (Math.PI * i) / n - Math.PI / 2;
    pontos.push(`${(50 + raio * Math.cos(angulo)).toFixed(2)},${(50 + raio * Math.sin(angulo)).toFixed(2)}`);
  }
  return `<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"><polygon points="${pontos.join(" ")}" fill="${cor}"/></svg>`;
}

/**
 * O texto do selo ("-30%", "10% OFF", "NOVO"): numa linha se for uma palavra
 * só, em duas se tiver espaço; a letra é a maior que cabe no miolo do selo.
 */
export function tamanhoDoSelo(selo: string, diametro: number, fonte: string) {
  const palavras = selo.trim().split(/\s+/);
  const fator = fatorDaFonte(fonte, true);
  const miolo = diametro * 0.62;
  const maiorLinha = palavras.length > 1 ? Math.max(...palavras.map((palavra) => palavra.length), Math.ceil(selo.length / 2)) : selo.length;
  const linhas = palavras.length > 1 ? 2 : 1;
  const pelaLargura = miolo / (Math.max(2, maiorLinha) * fator);
  const pelaAltura = miolo / (linhas * 1.05);
  return Math.floor(Math.min(pelaLargura, pelaAltura, diametro * 0.34));
}

function conteudoDoSelo(selo: string, diametro: number, fonte: string) {
  const umaPalavra = !/\s/.test(selo.trim());
  return `<span style="font-size:${px(tamanhoDoSelo(selo, diametro, fonte))};${umaPalavra ? "white-space:nowrap" : ""}">${escaparHtml(selo)}</span>`;
}

/** O HTML completo do banner, no tamanho exato do formato. */
export function htmlDoBanner(pedido: PedidoDoBanner): string {
  const formato: Formato = pedido.medida ? { ...FORMATOS[pedido.formato], ...pedido.medida } : FORMATOS[pedido.formato];
  const { largura: W, altura: H, disposicao } = formato;
  const modelo = pedido.modelo;
  const cores = pedido.cores;
  const fonteDoTitulo = chaveSegura(pedido.fonteDoTitulo, FONTE_PADRAO_DO_TITULO);
  const fonteDoTexto = chaveSegura(pedido.fonteDoTexto, FONTE_PADRAO_DO_TEXTO);
  const imagemSrc = pedido.imagem ? srcSeguro(pedido.imagem.src) : null;
  const imagem = imagemSrc && pedido.imagem ? { ...pedido.imagem, src: imagemSrc } : null;
  const logoSrc = pedido.logo ? srcSeguro(pedido.logo.src) : null;
  const plano = planoDoBanner(formato, modelo, imagem?.tipo ?? null);
  const oferta = modelo === "oferta com selo de desconto";
  const maiusculas = oferta;
  const fator = fatorDaFonte(fonteDoTitulo, maiusculas);
  const titulo = String(pedido.titulo ?? "").trim();
  const subtitulo = String(pedido.subtitulo ?? "").trim();
  const botao = String(pedido.botao ?? "").trim();
  const selo = String(pedido.selo ?? "").trim() || (oferta ? "OFERTA" : "");
  const usarLogo = pedido.usarLogo !== false;
  const nomeDaLoja = String(pedido.nomeDaLoja ?? "").trim().slice(0, 40);

  /* Na faixa, o logo e o botão dividem a linha com o texto. */
  let larguraDoTexto = plano.texto.w;
  if (plano.emLinha) {
    if (usarLogo && (logoSrc || nomeDaLoja)) larguraDoTexto -= plano.logo * 2.4;
    if (botao) larguraDoTexto -= plano.botao * (botao.length * 0.62 + 3.4) + plano.botao;
    if (selo && !plano.imagem) larguraDoTexto -= plano.titulo * 2.4;
  }
  const ehManuscrita = MANUSCRITAS.has(fonteDoTitulo);
  const alturaDaLinha = ehManuscrita ? 1.22 : maiusculas || CONDENSADAS.has(fonteDoTitulo) ? 1.0 : 1.06;
  const tituloPx = tamanhoQueCabe({
    texto: titulo,
    largura: Math.max(80, larguraDoTexto),
    maximo: plano.titulo,
    minimo: Math.round(plano.titulo * 0.42),
    linhas: plano.linhasDoTitulo,
    fator,
  });
  const subtituloPx = tamanhoQueCabe({
    texto: subtitulo,
    largura: Math.max(80, larguraDoTexto),
    maximo: plano.subtitulo,
    minimo: Math.round(plano.subtitulo * 0.7),
    linhas: plano.linhasDoSubtitulo,
    fator: 0.5,
  });
  const botaoPx = Math.min(plano.botao, Math.round((plano.texto.w * 0.9) / Math.max(6, botao.length * 0.62 + 3.2)));

  const claro = luminancia(cores.fundo) > 0.45;
  const centro = plano.alinhamento === "centro";

  /* ---------------------------------------------------------- fundo */
  /*
    O desenho de cada modelo (out/2026, "aprimorar sem IA"): nada de foto em
    círculo com anel, de caixa branca no logo nem de faixa por cima do título.
    Profundidade com luz e sombra da própria cor, textura de impressão bem leve,
    foto de cena até a borda e produto em cartão com sombra de verdade.
  */
  const camadas: string[] = [];
  const u = Math.min(W, H);
  const vertical = disposicao === "alto" || disposicao === "quadrado";
  const cobre = plano.imagem?.forma === "cobrir";
  const luz = misturar(cores.fundo, BRANCO, claro ? 0.55 : 0.16);
  const profundo = misturar(cores.fundo, "#000000", claro ? 0.06 : 0.34);
  const linhaFina = claro ? "rgba(0,0,0,.08)" : "rgba(255,255,255,.13)";
  /* Na foto de fundo o texto é sempre branco: o véu é escuro, não da cor da loja. */
  const textoDoBanner = cobre ? BRANCO : cores.texto;
  const textoSuaveDoBanner = cobre ? "rgba(255,255,255,.84)" : cores.textoSuave;

  if (modelo !== "minimalista" && !cobre) {
    /* A cor da loja acende num canto e afunda no outro: volume sem virar degradê de cartaz. */
    const origem = vertical ? "50% 0%" : centro ? "50% 38%" : "0% 0%";
    camadas.push(`<div class="cheio" style="background:radial-gradient(130% 150% at ${origem}, ${luz} 0%, ${cores.fundo} 46%, ${profundo} 100%)"></div>`);
  }

  if (modelo === "imagem à direita" && !plano.imagem && disposicao !== "faixa") {
    /* Sem imagem: dois arcos finos do lado vazio. */
    const r1 = u * 1.25;
    camadas.push(`<div class="forma" style="right:${px(-r1 * 0.42)};top:${px((H - r1) / 2)};width:${px(r1)};height:${px(r1)};border-radius:50%;border:${px(Math.max(2, u * 0.004))} solid ${linhaFina}"></div>`);
    camadas.push(`<div class="forma" style="right:${px(-r1 * 0.22)};top:${px((H - r1 * 0.7) / 2)};width:${px(r1 * 0.7)};height:${px(r1 * 0.7)};border-radius:50%;border:${px(Math.max(2, u * 0.004))} solid ${linhaFina}"></div>`);
  } else if (modelo === "centralizado" && !cobre) {
    /* Palco: anéis bem finos atrás do texto. */
    const cx = plano.texto.x + plano.texto.w / 2;
    const cy = plano.texto.y + plano.texto.h / 2;
    for (const k of [0.9, 1.45, 2.1]) {
      const d = u * k;
      camadas.push(`<div class="forma" style="left:${px(cx - d / 2)};top:${px(cy - d / 2)};width:${px(d)};height:${px(d)};border-radius:50%;border:${px(Math.max(2, u * 0.0035))} solid ${linhaFina}"></div>`);
    }
  } else if (oferta) {
    /* Liquidação: o lado da imagem (ou do selo) em tom mais fundo, com borda inclinada que nunca encosta no título. */
    const corte = plano.imagem && plano.imagem.forma !== "cobrir" ? plano.imagem : null;
    if (vertical) {
      const baixo = corte ? (corte.y + corte.h) / H : 0.36;
      camadas.push(`<div class="cheio" style="background:${profundo};clip-path:polygon(0 0,100% 0,100% ${(baixo * 100 + 4).toFixed(1)}%,0 ${(baixo * 100 - 4).toFixed(1)}%)"></div>`);
      camadas.push(`<div class="cheio" style="background:${cores.destaque};clip-path:polygon(0 ${(baixo * 100 - 4).toFixed(1)}%,100% ${(baixo * 100 + 4).toFixed(1)}%,100% ${(baixo * 100 + 4.6).toFixed(1)}%,0 ${(baixo * 100 - 3.4).toFixed(1)}%)"></div>`);
    } else if (disposicao !== "faixa") {
      const inicio = corte ? Math.max(plano.texto.x + plano.texto.w + u * 0.04, corte.x - u * 0.16) : W * 0.6;
      const a = ((inicio + u * 0.22) / W) * 100;
      const b = (inicio / W) * 100;
      camadas.push(`<div class="cheio" style="background:${profundo};clip-path:polygon(${a.toFixed(2)}% 0,100% 0,100% 100%,${b.toFixed(2)}% 100%)"></div>`);
      const fio = (Math.max(4, u * 0.012) / W) * 100;
      camadas.push(`<div class="cheio" style="background:${cores.destaque};clip-path:polygon(${(a - fio).toFixed(2)}% 0,${a.toFixed(2)}% 0,${b.toFixed(2)}% 100%,${(b - fio).toFixed(2)}% 100%)"></div>`);
    }
    camadas.push(`<div class="pontos" style="right:${px(W * 0.025)};${vertical ? `top:${px(H * 0.03)}` : `bottom:${px(H * 0.06)}`};width:${px(u * 0.26)};height:${px(u * 0.2)};--ponto:${claro ? "rgba(0,0,0,.16)" : "rgba(255,255,255,.2)"}"></div>`);
  } else if (modelo === "minimalista") {
    /* Editorial: um filete de cor no pé e uma régua fina no alto. */
    camadas.push(`<div class="forma" style="left:0;bottom:0;width:100%;height:${px(Math.max(6, u * 0.014))};background:${cores.destaque}"></div>`);
  }

  /* ---------------------------------------------------------- imagem */
  let htmlDaImagem = "";
  if (imagem && plano.imagem) {
    const c = plano.imagem;
    const raio = px(u * (modelo === "minimalista" ? 0.03 : 0.055));
    if (c.forma === "cobrir") {
      /* Véu escuro e neutro: a foto mantém as cores dela; a da loja entra só num toque. */
      let veu: string;
      if (modelo === "centralizado") veu = `linear-gradient(0deg, rgba(10,8,20,.58), rgba(10,8,20,.58)), linear-gradient(0deg, ${cores.fundo}33, ${cores.fundo}33)`;
      else if (vertical) veu = `linear-gradient(0deg, rgba(10,8,20,.9) 0%, rgba(10,8,20,.62) 34%, rgba(10,8,20,0) 72%)`;
      else if (disposicao === "faixa") veu = `linear-gradient(90deg, rgba(10,8,20,.85) 0%, rgba(10,8,20,.35) 45%, rgba(10,8,20,0) 70%)`;
      else veu = `linear-gradient(90deg, rgba(10,8,20,.86) 0%, rgba(10,8,20,.66) 32%, rgba(10,8,20,.18) 60%, rgba(10,8,20,0) 78%)`;
      const fio = !vertical && disposicao !== "faixa" ? `<div class="fio" style="background:${hexValido(cores.destaque) && cores.destaque !== BRANCO ? cores.destaque : cores.fundo}"></div>` : "";
      htmlDaImagem = `<div class="img cobrir" style="${caixaCss(c)}"><img src="${imagem.src}" alt=""><div class="veu" style="background:${veu}"></div>${fio}</div>`;
    } else if (c.forma === "painel") {
      /* A foto ocupa um lado inteiro e se desfaz na cor do banner. */
      htmlDaImagem = `<div class="img painel ${vertical ? "em-pe" : ""}" style="${caixaCss(c)}"><img src="${imagem.src}" alt=""></div>`;
    } else if (imagem.tipo === "transparente") {
      /* PNG sem fundo: solto, com sombra no chão e uma luz atrás. */
      const brilho = c.w * 1.3;
      htmlDaImagem = `<div class="forma" style="left:${px(c.x + c.w / 2 - brilho / 2)};top:${px(c.y + c.h / 2 - brilho / 2)};width:${px(brilho)};height:${px(brilho)};border-radius:50%;background:radial-gradient(circle, ${misturar(cores.fundo, BRANCO, claro ? 0.6 : 0.34)} 0%, transparent 62%)"></div>
<div class="chao" style="left:${px(c.x + c.w * 0.14)};top:${px(c.y + c.h * 0.9)};width:${px(c.w * 0.72)};height:${px(c.h * 0.08)}"></div>
<div class="img livre" style="${caixaCss(c)}"><img src="${imagem.src}" alt=""></div>`;
    } else if (imagem.tipo === "recorte") {
      /* Produto em fundo claro: cartão da cor do fundo da foto, cantos grandes e sombra longa. */
      const corDoCartao = hexValido(imagem.fundo) ?? BRANCO;
      const some = contraste(corDoCartao, cores.fundo) < 1.08;
      htmlDaImagem = `<div class="img cartao recorte ${some ? "sem-sombra" : ""}" style="${caixaCss(c)};border-radius:${raio};background:${corDoCartao}"><img src="${imagem.src}" alt=""></div>`;
    } else {
      /* Foto de cena num cartão: retângulo de cantos grandes, nunca círculo. */
      htmlDaImagem = `<div class="img cartao foto" style="${caixaCss(c)};border-radius:${raio}"><img src="${imagem.src}" alt=""></div>`;
    }
  }

  /* ---------------------------------------------------------- selo */
  let seloSolto = "";
  let seloNaColuna = "";
  if (selo) {
    const tamanho = Math.round(u * (disposicao === "faixa" ? 0.62 : disposicao === "alto" ? 0.3 : disposicao === "quadrado" ? 0.26 : oferta ? 0.38 : 0.3));
    const conteudo = conteudoDoSelo(selo, oferta ? tamanho : tamanho * 0.86, fonteDoTitulo);
    if (plano.imagem && plano.imagem.forma !== "cobrir") {
      const c = plano.imagem;
      /* No painel (foto até a borda) o selo entra na foto, sem passar do topo. */
      const noPainel = c.forma === "painel";
      const x = noPainel
        ? vertical
          ? W - tamanho - u * 0.06
          : c.x + c.w * 0.2
        : c.x + (disposicao === "faixa" ? -tamanho * 0.55 : c.w * 0.02 - tamanho * 0.25);
      const y = noPainel ? u * 0.07 : c.y + (disposicao === "faixa" ? (c.h - tamanho) / 2 : -tamanho * 0.12);
      seloSolto = oferta
        ? `<div class="selo estrela" style="left:${px(x)};top:${px(y)};width:${px(tamanho)};height:${px(tamanho)};color:${cores.textoDoDestaque}">${estrela(cores.destaque)}${conteudo}</div>`
        : `<div class="selo redondo" style="left:${px(x)};top:${px(y)};width:${px(tamanho * 0.86)};height:${px(tamanho * 0.86)};background:${cores.destaque};color:${cores.textoDoDestaque}">${conteudo}</div>`;
    } else if (oferta && !plano.emLinha) {
      /* Oferta sem imagem (ou com foto de fundo): a estrela grande do lado vazio. */
      const grande = Math.round(u * (disposicao === "alto" ? 0.62 : disposicao === "quadrado" ? 0.36 : 0.66));
      const conteudoGrande = conteudoDoSelo(selo, grande, fonteDoTitulo);
      const pos =
        disposicao === "alto" || disposicao === "quadrado"
          ? `left:${px((W - grande) / 2)};top:${px(H * 0.06)}`
          : `left:${px(W * 0.78 - grande / 2)};top:${px((H - grande) / 2)}`;
      seloSolto = `<div class="selo estrela" style="${pos};width:${px(grande)};height:${px(grande)};color:${cores.textoDoDestaque}">${estrela(cores.destaque)}${conteudoGrande}</div>`;
    } else {
      seloNaColuna = `<div class="etiqueta" style="background:${cores.destaque};color:${cores.textoDoDestaque};font-size:${px(Math.max(plano.subtitulo * 0.9, tituloPx * 0.32))}">${escaparHtml(selo)}</div>`;
    }
  }

  /* ---------------------------------------------------------- logo */
  let htmlDoLogo = "";
  if (usarLogo && logoSrc && pedido.logo) {
    const proporcao = pedido.logo.largura / Math.max(1, pedido.logo.altura);
    let altura = plano.logo;
    /* Logo muito comprido não toma a coluna toda. */
    const larguraMaxima = Math.min(plano.texto.w * 0.6, u * 1.1);
    if (altura * proporcao > larguraMaxima) altura = larguraMaxima / proporcao;
    /* Em fundo de cor, o logo vai numa pílula branca discreta (logo colorido some na cor da loja). */
    const chip = luminancia(cores.fundo) < 0.92 || cobre;
    if (chip) altura *= 0.82;
    htmlDoLogo = `<div class="logo ${chip ? "chip" : ""}" style="height:${px(altura + (chip ? altura * 0.5 : 0))};padding:${chip ? `${px(altura * 0.25)} ${px(altura * 0.46)}` : "0"}"><img src="${logoSrc}" alt="" style="height:${px(altura)}"></div>`;
  } else if (usarLogo && nomeDaLoja) {
    htmlDoLogo = `<div class="marca" style="font-size:${px(plano.logo * 0.34)};color:${textoDoBanner}"><span class="marca-ponto" style="background:${cores.destaque}"></span>${escaparHtml(nomeDaLoja)}</div>`;
  }

  /* ---------------------------------------------------------- textos */
  const tracoDoMinimalista =
    modelo === "minimalista" && !plano.emLinha ? `<div class="traco" style="background:${cores.destaque};width:${px(u * 0.12)};height:${px(Math.max(5, u * 0.012))}"></div>` : "";
  const htmlDoBotao = botao
    ? `<div class="botao" style="font-size:${px(botaoPx)};background:${cores.destaque};color:${cores.textoDoDestaque}"><span>${escaparHtml(botao)}</span><svg class="seta" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h9.5M8.5 3.5 13 8l-4.5 4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>`
    : "";
  const blocoDeTexto = `<div class="textos" id="textos">
      ${seloNaColuna}
      ${tracoDoMinimalista}
      <div class="titulo" id="titulo" data-tam="${tituloPx}" data-lh="${alturaDaLinha}" data-linhas="${plano.linhasDoTitulo}" style="font-size:${px(tituloPx)};line-height:${alturaDaLinha}">${escaparHtml(titulo)}</div>
      ${subtitulo ? `<div class="subtitulo" id="subtitulo" data-tam="${subtituloPx}" data-lh="1.3" data-linhas="${plano.linhasDoSubtitulo}" style="font-size:${px(subtituloPx)}">${escaparHtml(subtitulo)}</div>` : ""}
    </div>`;
  const coluna = plano.emLinha
    ? `<div class="coluna em-linha" id="coluna" style="${caixaCss(plano.texto)}">${htmlDoLogo}${blocoDeTexto}${htmlDoBotao}</div>`
    : `<div class="coluna" id="coluna" style="${caixaCss(plano.texto)}">${htmlDoLogo}${blocoDeTexto}${htmlDoBotao}</div>`;

  const espaco = plano.emLinha ? H * 0.06 : Math.max(10, tituloPx * 0.28);
  const fonteT = pilhaDaFonte(fonteDoTitulo);
  const fonteC = pilhaDaFonte(fonteDoTexto);
  const sombraDoTexto = plano.imagem?.forma === "cobrir" ? "0 2px 18px rgba(0,0,0,.18)" : "none";

  return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">
<link rel="stylesheet" href="${linkDasFontes([fonteDoTitulo, fonteDoTexto])}">
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:${W}px;height:${H}px;overflow:hidden}
body{position:relative;background:${cores.fundo};color:${textoDoBanner};font-family:${fonteC};-webkit-font-smoothing:antialiased}
.cheio{position:absolute;inset:0}
.forma,.pontos{position:absolute}
.pontos{background-image:radial-gradient(var(--ponto) 22%,transparent 24%);background-size:${px(u * 0.045)} ${px(u * 0.045)};opacity:.55}
.listras{background:repeating-linear-gradient(135deg,var(--listra) 0 var(--l),transparent var(--l) calc(var(--l)*2))}
.img{position:absolute;overflow:hidden}
.img img{width:100%;height:100%;display:block}
.img.cobrir img{object-fit:cover}
.img .veu{position:absolute;inset:0}
.img.painel img{object-fit:cover}
.img.painel{-webkit-mask-image:linear-gradient(90deg,transparent 0,rgba(0,0,0,.55) 9%,#000 26%);mask-image:linear-gradient(90deg,transparent 0,rgba(0,0,0,.55) 9%,#000 26%)}
.img.painel.em-pe{-webkit-mask-image:linear-gradient(0deg,transparent 0,rgba(0,0,0,.55) 10%,#000 30%);mask-image:linear-gradient(0deg,transparent 0,rgba(0,0,0,.55) 10%,#000 30%)}
.img .fio{position:absolute;left:0;top:0;bottom:0;width:${px(Math.max(6, u * 0.014))}}
.chao{position:absolute;border-radius:50%;background:radial-gradient(closest-side,rgba(0,0,0,.34),transparent);filter:blur(${px(u * 0.012)})}
.grao{position:absolute;inset:0;opacity:.09;mix-blend-mode:overlay;pointer-events:none;z-index:1}
.img.recorte img,.img.com-cartao img{-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 7%,#000 93%,transparent 100%),linear-gradient(180deg,transparent 0,#000 7%,#000 93%,transparent 100%);-webkit-mask-composite:source-in;mask-composite:intersect;-webkit-mask-origin:content-box;-webkit-mask-clip:content-box}
.img.cartao{z-index:2;box-shadow:0 ${px(u * 0.012)} ${px(u * 0.03)} rgba(0,0,0,.14),0 ${px(u * 0.05)} ${px(u * 0.12)} rgba(0,0,0,.26)}
.img.cartao.sem-sombra{box-shadow:none}
.img.cartao img{object-fit:cover}
.img.cartao.recorte img{object-fit:contain;padding:9%}
.img.livre{overflow:visible}
.img.livre img{object-fit:contain;filter:drop-shadow(0 ${px(u * 0.025)} ${px(u * 0.035)} rgba(0,0,0,.28))}
.img.livre.com-cartao{overflow:hidden;box-shadow:0 ${px(u * 0.03)} ${px(u * 0.08)} rgba(0,0,0,.22)}
.img.livre.com-cartao.sem-sombra{box-shadow:none}
.img.livre.com-cartao img{padding:8%;filter:none}
.selo{position:absolute;display:flex;align-items:center;justify-content:center;text-align:center;font-family:${fonteT};font-weight:${pesoDoTitulo(fonteDoTitulo)};line-height:.95;z-index:3}
.selo svg{position:absolute;inset:0;width:100%;height:100%;filter:drop-shadow(0 ${px(u * 0.012)} ${px(u * 0.02)} rgba(0,0,0,.25))}
.selo span{position:relative;display:inline-block;max-width:80%;overflow-wrap:anywhere;text-transform:uppercase;letter-spacing:-.01em}
.selo.redondo{border-radius:50%;box-shadow:0 ${px(u * 0.012)} ${px(u * 0.03)} rgba(0,0,0,.25);transform:rotate(-8deg);z-index:4}
.selo.estrela{z-index:4}
.selo.estrela{transform:rotate(-10deg)}
.coluna{position:absolute;display:flex;flex-direction:column;justify-content:center;gap:${px(espaco)};z-index:2;${centro ? "align-items:center;text-align:center" : "align-items:flex-start;text-align:left"}}
.coluna.em-linha{flex-direction:row;align-items:center;justify-content:${centro ? "center" : "flex-start"};gap:${px(H * 0.14)}}
.coluna.em-linha .textos{flex:1 1 auto;min-width:0}
.textos{display:flex;flex-direction:column;gap:${px(plano.emLinha ? H * 0.03 : Math.max(8, tituloPx * 0.22))};max-width:100%;${centro ? "align-items:center" : "align-items:flex-start"}}
.titulo{font-family:${fonteT};font-weight:${pesoDoTitulo(fonteDoTitulo)};letter-spacing:${ehManuscrita ? "0" : maiusculas ? "-.005em" : "-.03em"};max-width:100%;text-shadow:${sombraDoTexto};${maiusculas ? "text-transform:uppercase;" : ""}}
.subtitulo{line-height:1.3;color:${textoSuaveDoBanner};max-width:${plano.emLinha ? "100%" : "92%"};font-weight:400;text-shadow:${sombraDoTexto}}
.limitado .titulo{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:${plano.linhasDoTitulo};overflow:hidden;overflow-wrap:anywhere}
.limitado .subtitulo{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:${plano.linhasDoSubtitulo};overflow:hidden}
.botao{flex:none;display:inline-flex;align-items:center;gap:.6em;white-space:nowrap;font-weight:700;padding:.85em 1.45em .85em 1.7em;border-radius:999px;letter-spacing:.005em;box-shadow:0 .5em 1.4em rgba(0,0,0,.2)}
.botao .seta{width:1.05em;height:1.05em;flex:none}
.logo{flex:none;display:inline-flex;align-items:center}
.logo.chip{background:rgba(255,255,255,.97);border-radius:999px;box-shadow:0 .25em .9em rgba(0,0,0,.14)}
.logo img{display:block;width:auto}
.marca{flex:none;display:inline-flex;align-items:center;gap:.55em;font-family:${fonteT};font-weight:${pesoDoTitulo(fonteDoTitulo)};letter-spacing:.16em;text-transform:uppercase;opacity:.92}
.marca-ponto{width:.55em;height:.55em;border-radius:50%;flex:none}
.etiqueta{display:inline-block;font-family:${fonteT};font-weight:${pesoDoTitulo(fonteDoTitulo)};padding:.22em .7em;border-radius:.4em;text-transform:uppercase;letter-spacing:.02em;flex:none}
.traco{border-radius:99px}
</style></head><body>
${camadas.join("\n")}
${modelo === "minimalista" ? "" : `<svg class="grao" width="${W}" height="${H}" aria-hidden="true"><filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="100%" height="100%" filter="url(#grao)"/></svg>`}
${htmlDaImagem}
${seloSolto}
${coluna}
<script>
(async function(){
  try{await Promise.race([Promise.all([document.fonts.load("${pesoDoTitulo(fonteDoTitulo)} 40px '${familiaDaFonte(fonteDoTitulo)}'"),document.fonts.load("400 20px '${familiaDaFonte(fonteDoTexto)}'"),document.fonts.load("700 20px '${familiaDaFonte(fonteDoTexto)}'")]),new Promise(function(r){setTimeout(r,8000)})]);await document.fonts.ready}catch(e){}
  var C=document.getElementById("coluna"),T=document.getElementById("titulo"),S=document.getElementById("subtitulo");
  function linhas(el){var t=parseFloat(el.style.fontSize),lh=parseFloat(el.dataset.lh);return Math.round(el.getBoundingClientRect().height/(t*lh))}
  function passou(){
    if(linhas(T)>+T.dataset.linhas||T.scrollWidth>T.clientWidth+1)return true;
    if(S&&linhas(S)>+S.dataset.linhas)return true;
    return C.scrollHeight>C.clientHeight+1||C.scrollWidth>C.clientWidth+1;
  }
  var t=+T.dataset.tam,minT=t*0.4,s=S?+S.dataset.tam:0,minS=s*0.72,n=0;
  while(passou()&&n<80){
    var mexeu=false;
    if(t>minT){t*=0.95;T.style.fontSize=t+"px";mexeu=true}
    if(S&&s>minS&&(n%2===1||t<=minT)){s*=0.96;S.style.fontSize=s+"px";mexeu=true}
    if(!mexeu)break;n++;
  }
  /* O selo: a letra diminui até o texto ficar dentro do miolo. */
  Array.prototype.forEach.call(document.querySelectorAll(".selo span"),function(el){
    var caixa=el.parentElement.getBoundingClientRect(),f=parseFloat(el.style.fontSize),k=0;
    while(k<40&&(el.getBoundingClientRect().width>caixa.width*0.62||el.getBoundingClientRect().height>caixa.height*0.6)){f*=0.94;el.style.fontSize=f+"px";k++}
  });
  document.body.classList.add("limitado");
  document.body.setAttribute("data-pronto","1");
})();
</script>
</body></html>`;
}
