#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gen-zero-formatos.py — a cena Z3 APROVADA promovida aos 7 formatos reais.

════════════════════════════════════════════════════════════════════════════════
0. QUEM LÊ ISTO DAQUI A TRÊS MESES PRECISA SABER
════════════════════════════════════════════════════════════════════════════════
· O QUE ESTE ARQUIVO GERA: o grafismo de rodapé do DS v2 da SEED — a "serra em
  arcos" — aplicado em 7 formatos de peça, nos dois temas. É a PROMOÇÃO da cena
  Z3 do estudo 5 (`gen-estudo-zero.py`), aprovada pelo Rafael em 2026-08-20:
      "a Z3, podemos aprovar, a aplicacao ficou aceitavel. mais ondulacoes,
       ficou boa porque nao pode perder o sentido de movimento apesar de serem
       montanhas"
  ⚠ CRITÉRIO NOVO REGISTRADO nesse veredito: o SENTIDO DE MOVIMENTO. As
  ondulações não são só montanhas paradas — o movimento das ondas é parte do
  significado (a onda originou o desenho; a montanha é a leitura). Composições
  futuras não podem "congelar" a serra a ponto de matar o movimento.

· O MODELO (história completa em gen-estudo-crista/vale/seed/frente/zero.py):
  FUNDO = única linha contínua, a mais alta e mais clara, atravessando a peça.
  MEIO = morros PARCIAIS em duas profundidades (arcos que nascem e morrem
  abaixo da moldura ou atrás de outro morro; cume fora de centro, flancos
  assimétricos, ápice arredondado — nunca ponta).
  FRENTE = baixa e suave, atravessa mergulhando abaixo do limite inferior.
  OCLUSÃO = recorte na própria crista dentro da <mask> (toque exato; nada de
  massa pintada — "linha, nunca massa").
  REGRA DO FRAGMENTO = trecho visível <80px ou com folga <9px é suprimido:
  ou o morro aparece de verdade, ou não aparece.

· A CENA Z3 (frações congeladas — mexer nelas é reabrir o aprovado):
  fundo: 7 cumes (em 1080px), base 0,90F, amp 0,612F, seq 8, deriva 0,19F
         ciclos 0,49 fase 2,1+π   (F = altura da faixa)
  longe: morro cx 0,20w · topo 0,36F · larg 0,30w · assim −0,35
  perto: MASSIVO cx 0,70w · topo 0,27F · larg 0,40w · assim +0,45 ·
         ombro (0,50, 0,58)   e   APOIO cx 0,02w · topo 0,50F · larg 0,24w
  frente: cumes (0,12 · 0,68F · 0,30) e (0,48 · 0,63F · 0,34)

· ADAPTAÇÃO POR FORMATO (regras declaradas, não improviso):
  1. FAIXA = floor(altura × 0,0795) — o teto de ~8% do canon com margem de
     arredondamento (lição do gate v14.5: "por volta de 8%" reprovou 5 de 7).
  2. DENSIDADE DO FUNDO: n = max(3, round(7·w/1080)) — cumes por pixel
     constantes; peça larga ganha cumes, não cumes esticados (lição da LEI DA
     SERRA, hoje superseded como lei mas viva como observação de densidade).
  3. PEÇA ESTREITA (w < 700px) recebe SUBCONJUNTO da cena: massivo + apoio +
     frente de 1 cume. Mesma cena, menos elementos — densidade de arcos por
     pixel é o que preserva a leitura; espremer a cena inteira viraria ruído.
  4. PESO DO TRAÇO escala com a FAIXA (não com a largura): peso_frente =
     3,5 × F/86, grampeado em [1,5 · 4,5]px; demais na proporção da paleta
     (3,0/2,5/2,2 × o mesmo fator).
     ⚠⚠ ATUALIZADO em 2026-08-22 (pendência MR-P6, FECHADA): o traço em
     MILÍMETRO deixou de ser pendência e virou canon — `marca-seed.md` v5.6
     §7.4-c, veredito verbatim do Rafael: "saída B — piso de 0,25 mm por
     ESCALA, e só para peça IMPRESSA". Neste arquivo isso vale para UMA peça,
     a F6-pasta-A4, que é a única com ALVO FÍSICO declarado aqui (210×297mm).
     As outras seis são peça de TELA e seguem sem piso — peça de tela não tem
     milímetro. ⚠ F2/F3 (o cartão desta família) NÃO recebe o piso: foi
     SUPERSEDIDO como cartão impresso pelas peças L2/L3 do gate
     (gen-z3-largos.py), hoje em 90×50mm; decisão declarada no MANIFESTO
     §108.6 e marcada lá para veto dele.
  5. A REGRA DO FRAGMENTO roda POR FORMATO: o que vira fragmento numa
     proporção não vira em outra.

· CORES (hierarquia de 4 profundidades):
  TEMA CLARO (C1 do estudo 3, a única que ele viu e aprovou em peça):
    frente #098475 · perto #11B0A0 · longe #66D1C2 · fundo #9CE4D8
  TEMA BRAND-DEEP (fundo #005048 = turquesa-800): frente #66D1C2 (tq300) ·
    perto #11B0A0 (tq400) · longe #098475 (tq600, o verde institucional) ·
    fundo #006C62 (tq700). É a "E1-névoa", APROVADA por Rafael em 2026-08-21
    ("pergunta 2, E1 nevoa fica melhor") contra a alternativa "presença".
    Os quatro são stops da rampa canônica turquesa (seed-tokens.md §3).
    ⚠ SUPERSEDE FORMAL: até 2026-08-21 esta escada era ['#9CE4D8','#66D1C2',
    '#11B0A0','#0A6B60'] — a "E2 estendida" — e o 4º plano, #0A6B60, NÃO É
    TOKEN: varredura de alvo de pasta nos 412 arquivos de texto da pasta deu
    ZERO ocorrências dele nos gêmeos de token, e o "precedente" que o
    justificava (gen-variantes-montanha.py) NÃO EXISTE na pasta. Cor criada
    sem autorização do decisor e retirada por ordem dele. A troca não custa
    aparência: ΔE76(#0A6B60,#006C62) = 1,03, abaixo do limiar de percepção
    (~2,3). Regra que fica: hex que não é stop de rampa canônica não entra em
    artefato desta casa, nem "por precedente".

· PENDÊNCIA TÉCNICA DECLARADA: a gramática (cordilheira/filetes/morro/
  frente_ondulada/compoe) está DUPLICADA em gen-estudo-zero.py e aqui (e
  parcialmente em 3 estudos anteriores). Consolidar numa lib quando o modelo
  estabilizar; enquanto os estudos estiverem vivos, mudança lá = mudança aqui.

· RESOLUÇÃO DA PEÇA IMPRESSA — corrigida em 2026-08-22 (pendência MR-P8):
  a F6-pasta-A4 era um MOCK de 874×1240px, isto é 105,7 DPI para 210mm — um
  terço da resolução de impressão. Pior: a razão daquele mock (0,7048) NÃO era
  a do A4 (0,70707), errava por 0,3%. Agora ela sai em 2480×3508px = 300 DPI
  EXATOS e razão exata de A4. Todas as medidas tipográficas e de padding da
  peça são multiplicadas pelo mesmo fator (2480/874 = 2,8375), o que mantém
  cada elemento no MESMO tamanho físico em milímetro — inclusive o risco
  amarelo. ⚠ Por que isso importa e não é cosmético: com o piso da §7.4-c
  escrito em MILÍMETRO, a conversão px↔mm depende do DPI. Peça exportada em
  resolução de mock recebe o piso na resolução errada — passa no papel e
  imprime fino.

Uso:   python3 validacao/gen-zero-formatos.py
Saída: render-audit/gate-z3/formatos/*.html  (7 peças)
Depois: node validacao/guarda-grafismo.mjs render-audit/gate-z3/formatos/*.html
        node validacao/contraste-composicao.mjs render-audit/gate-z3/formatos/*.html
"""
import base64
import math
import pathlib
import re
import shutil

DEST = pathlib.Path('render-audit/gate-z3/formatos')
shutil.rmtree(DEST, ignore_errors=True)
DEST.mkdir(parents=True)

FS = pathlib.Path('node_modules/@fontsource')
ASSETS = pathlib.Path.home() / '.claude/skills/synced/seed-ds-post/assets'

LONGE_ = 1e9
VAR = [1.00, 0.45, 0.78, 0.58, 0.92, 0.38, 0.70, 0.52, 0.85, 0.63]
LARG = [1.00, 0.62, 1.30, 0.80, 1.15, 0.70, 1.42, 0.88, 1.08, 0.75]
SELA = [0.22, 0.42, 0.12, 0.34, 0.26, 0.48, 0.18, 0.38, 0.28, 0.15]
ASS = [0.14, -0.19, 0.26, -0.10, 0.21, -0.24, 0.09, -0.16, 0.18, -0.12]


def _d(a, b):
    return math.hypot(b[0] - a[0], b[1] - a[1])


def _lerp(a, b, t):
    return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)


def cordilheira(largura, base, amp, n, seq=0, deriva_amp=0.0, deriva_ciclos=0.7,
                deriva_fase=0.0, sela=0.34, assim=0.18, teto=3.0):
    pesos = [LARG[(i + seq) % len(LARG)] for i in range(n)]
    tot = sum(pesos)
    bordas = [0.0]
    for p in pesos:
        bordas.append(bordas[-1] + p / tot * largura)

    def deriva(x):
        if not deriva_amp:
            return 0.0
        return deriva_amp * math.sin(2 * math.pi * deriva_ciclos * x / largura
                                     + deriva_fase)

    def gy(y):
        return max(teto, min(base + abs(deriva_amp), y))

    v = [(-largura * 0.06,
          gy(base - amp * sela * SELA[seq % len(SELA)] - deriva(-largura * 0.06)))]
    for i in range(n):
        x0, x1 = bordas[i], bordas[i + 1]
        w = x1 - x0
        cx = x0 + w * (0.5 + assim * ASS[(i + seq) % len(ASS)])
        v.append((cx, gy(base - amp * VAR[(i + seq) % len(VAR)] - deriva(cx))))
        vx = x1 if i < n - 1 else largura + largura * 0.06
        v.append((vx, gy(base - amp * sela * SELA[(i + 1 + seq) % len(SELA)] - deriva(vx))))
    return v


def filetes(v, k_cume=0.20, k_vale=0.26):
    segs, cur = [], v[0]
    for i in range(1, len(v) - 1):
        P, V, N = v[i - 1], v[i], v[i + 1]
        k = k_cume if V[1] < (P[1] + N[1]) / 2 else k_vale
        lp, ln = _d(V, P), _d(V, N)
        if min(lp, ln) < 20:
            k = max(k, 0.34)
        r = k * min(lp, ln)
        Aa = _lerp(V, P, r / lp) if lp else V
        B = _lerp(V, N, r / ln) if ln else V
        if _d(cur, Aa) > 0.01:
            segs.append(('L', cur, Aa))
        segs.append(('Q', Aa, V, B))
        cur = B
    if _d(cur, v[-1]) > 0.01:
        segs.append(('L', cur, v[-1]))
    return segs


def segs_para_d(segs):
    d = [f'M{segs[0][1][0]:.2f},{segs[0][1][1]:.2f}']
    for s in segs:
        if s[0] == 'L':
            d.append(f'L{s[2][0]:.2f},{s[2][1]:.2f}')
        else:
            d.append(f'Q{s[2][0]:.2f},{s[2][1]:.2f} {s[3][0]:.2f},{s[3][1]:.2f}')
    return ' '.join(d)


def cena_z3(w, F):
    """A cena Z3 nas frações congeladas, instanciada para (largura, faixa).
    Devolve (fundo_v, morros[(segs, perto)], frente_segs)."""
    fundo_y = F + 12

    def morro(cx, topo_y, mw, assim=0.0, ombro=None, k=0.42):
        x0, x1 = cx - mw / 2, cx + mw / 2
        px = cx + assim * mw * 0.22
        v = [(x0, fundo_y)]
        if ombro is not None and assim >= 0:
            fx, fy = ombro
            v.append((x0 + (px - x0) * (fx - 0.12), fundo_y - (fundo_y - topo_y) * (fy - 0.14)))
            v.append((x0 + (px - x0) * fx, fundo_y - (fundo_y - topo_y) * fy))
        v.append((px, topo_y))
        if ombro is not None and assim < 0:
            fx, fy = ombro
            v.append((x1 - (x1 - px) * fx, fundo_y - (fundo_y - topo_y) * fy))
            v.append((x1 - (x1 - px) * (fx - 0.12), fundo_y - (fundo_y - topo_y) * (fy - 0.14)))
        v.append((x1, fundo_y))
        return filetes(v, k_cume=k, k_vale=0.30)

    def frente(cumes):
        v = [(-w * 0.06, fundo_y + 2)]
        for cx, topo, cw in cumes:
            x, t = cx * w, topo * F
            v.append((x - cw * w * 0.34, F + 6))
            v.append((x, t))
            v.append((x + cw * w * 0.30, F + 6))
        v.append((w * 1.06, fundo_y + 2))
        limpo = [v[0]]
        for pt in v[1:]:
            if pt[1] > F and limpo[-1][1] > F:
                limpo[-1] = ((limpo[-1][0] + pt[0]) / 2, max(pt[1], limpo[-1][1]))
            else:
                limpo.append(pt)
        return filetes(limpo, k_cume=0.44, k_vale=0.42)

    n_fundo = max(3, round(7 * w / 1080))
    fundo_v = cordilheira(w, F * 0.90, F * 0.612, n_fundo, seq=8,
                          deriva_amp=F * 0.19, deriva_ciclos=0.49,
                          deriva_fase=2.1 + math.pi)
    if w >= 700:
        morros = [(morro(w * 0.20, F * 0.36, w * 0.30, assim=-0.35), False),
                  (morro(w * 0.70, F * 0.27, w * 0.40, assim=+0.45, ombro=(0.50, 0.58)), True),
                  (morro(w * 0.02, F * 0.50, w * 0.24, assim=+0.30), True)]
        fr = frente([(0.12, 0.68, 0.30), (0.48, 0.63, 0.34)])
    else:
        morros = [(morro(w * 0.62, F * 0.28, w * 0.46, assim=+0.45, ombro=(0.50, 0.58)), True),
                  (morro(w * 0.04, F * 0.48, w * 0.30, assim=+0.30), True)]
        fr = frente([(0.34, 0.65, 0.44)])
    return fundo_v, morros, fr


# ═══ compositor com REGRA DO FRAGMENTO (cópia de gen-estudo-zero.py) ═════════
M_AM = 540
FRAG_LEN = 80.0
FRAG_FOLGA = 9.0


def amostra_segs(segs, por=32):
    pts = []
    for s in segs:
        for j in range(por + 1):
            t = j / por
            mt = 1 - t
            if s[0] == 'L':
                p = _lerp(s[1], s[2], t)
            else:
                a, c, b = s[1], s[2], s[3]
                p = (mt * mt * a[0] + 2 * mt * t * c[0] + t * t * b[0],
                     mt * mt * a[1] + 2 * mt * t * c[1] + t * t * b[1])
            if not pts or _d(pts[-1], p) > 1e-9:
                pts.append(p)
    return pts


def perfil(segs, w):
    pts = amostra_segs(segs)
    xs = [pt[0] for pt in pts]
    x_min, x_max = min(xs), max(xs)
    ys, j = [], 0
    for i in range(M_AM):
        x = w * i / (M_AM - 1)
        if x < x_min or x > x_max:
            ys.append(LONGE_)
            continue
        while j < len(pts) - 2 and pts[j + 1][0] < x:
            j += 1
        a, b = pts[j], pts[j + 1]
        t = 0 if b[0] == a[0] else max(0.0, min(1.0, (x - a[0]) / (b[0] - a[0])))
        ys.append(a[1] + (b[1] - a[1]) * t)
    return ys


def compoe(w, F, fundo_v, morros, frente_segs):
    """FRAG_LEN escala com a largura da peça (80px foi calibrado em 1080)."""
    frag_len = FRAG_LEN * w / 1080.0
    ordenados = ([(m, p) for m, p in morros if not p]
                 + [(m, p) for m, p in morros if p])
    capas = [dict(nome='fundo', segs=filetes(fundo_v), tom=3)]
    for m, perto in ordenados:
        capas.append(dict(nome='morro', segs=m, tom=1 if perto else 2))
    capas.append(dict(nome='frente', segs=frente_segs, tom=0))
    perfis = [perfil(c['segs'], w) for c in capas]
    passo = w / (M_AM - 1)
    supress, frag_n = [], 0
    for i in range(len(capas) - 1):
        oc = [min(perfis[j][k] for j in range(i + 1, len(capas))) for k in range(M_AM)]
        vis = [(perfis[i][k] < LONGE_ / 2 and 1 <= perfis[i][k] <= F
                and perfis[i][k] < oc[k] - 2) for k in range(M_AM)]
        k = 0
        while k < M_AM:
            if not vis[k]:
                k += 1
                continue
            k0 = k
            while k < M_AM and vis[k]:
                k += 1
            k1 = k - 1
            comp = (k1 - k0) * passo
            folga = max(oc[j] - perfis[i][j] for j in range(k0, k1 + 1))
            if comp < frag_len or folga < FRAG_FOLGA:
                ys = [perfis[i][j] for j in range(k0, k1 + 1)]
                supress.append((i, k0 * passo - 6, k1 * passo + 6,
                                min(ys) - 8, max(ys) + 8))
                frag_n += 1
    return capas, supress, frag_n


def svg_cena(w, F, capas, supress, cores, pesos, uid):
    o = [f'<svg class="serra" viewBox="0 0 {w:.0f} {F:.0f}" preserveAspectRatio="none" '
         f'aria-hidden="true" focusable="false" data-cena="Z3" '
         f'data-fragmentos-suprimidos="{len(supress)}"><defs>']
    ds = []
    for c in capas:
        vv = [c['segs'][0][1]] + [s[3] if s[0] == 'Q' else s[2] for s in c['segs']]
        ds.append((segs_para_d(c['segs']), vv))
    for i in range(len(capas) - 1):
        oculta = []
        for j in range(i + 1, len(capas)):
            dj, vj = ds[j]
            oculta.append(f'<path d="{dj} L{vj[-1][0]:.1f},{F+40:.1f} '
                          f'L{vj[0][0]:.1f},{F+40:.1f} Z" fill="#000"/>')
        for si, x0, x1, y0, y1 in supress:
            if si == i:
                oculta.append(f'<rect x="{x0:.1f}" y="{y0:.1f}" width="{x1-x0:.1f}" '
                              f'height="{y1-y0:.1f}" fill="#000"/>')
        o.append(f'<mask id="q{uid}{i}" maskUnits="userSpaceOnUse" x="0" y="0" '
                 f'width="{w:.0f}" height="{F:.0f}">'
                 f'<rect width="{w:.0f}" height="{F:.0f}" fill="#fff"/>{"".join(oculta)}</mask>')
    o.append('</defs>')
    for i, c in enumerate(capas):
        m = f' mask="url(#q{uid}{i})"' if i < len(capas) - 1 else ''
        o.append(f'<path d="{ds[i][0]}" fill="none" stroke="{cores[c["tom"]]}" '
                 f'stroke-width="{pesos[c["tom"]]:.2f}" stroke-linecap="round"{m}/>')
    o.append('</svg>')
    return ''.join(o)


# ═══ peças ═══════════════════════════════════════════════════════════════════
def duri(p):
    return 'data:font/woff2;base64,' + base64.b64encode(pathlib.Path(p).read_bytes()).decode()


FACES = ''.join(f"@font-face{{font-family:'Montserrat';font-weight:{w};font-display:block;"
                f"src:url({duri(FS/'montserrat/files'/f'montserrat-latin-{w}-normal.woff2')}) format('woff2')}}"
                for w in (400, 500, 700))
FACES += ("@font-face{font-family:'JetBrains Mono';font-weight:500;font-display:block;"
          f"src:url({duri(FS/'jetbrains-mono/files/jetbrains-mono-latin-500-normal.woff2')}) format('woff2')}}")


def simbolo(arq, px):
    return re.sub(r'width="\d+(\.\d+)?" height="\d+(\.\d+)?"',
                  f'width="{px}" height="{px*935/942:.1f}"',
                  (ASSETS / arq).read_text(encoding='utf-8'), count=1)


CLARO = dict(cores=['#098475', '#11B0A0', '#66D1C2', '#9CE4D8'],
             fundo='#FFFFFF', borda='border:1px solid #E3EAEE;',
             sim='simbolos/simbolo-principal.svg',
             ceye='#006C62', ctit='#242E34', cass='#4D606C')
# E1-névoa, aprovada 2026-08-21 — quatro stops da rampa canônica, zero hex
# fora do sistema. Supersede do #0A6B60 explicado no docstring (seção CORES).
ESCURO = dict(cores=['#66D1C2', '#11B0A0', '#098475', '#006C62'],
              fundo='#005048', borda='',
              sim='simbolos/simbolo-mono-negativo.svg',
              ceye='#66D1C2', ctit='#FFFFFF', cass='#CDF3EC')

RISCO = ('<svg class="risco" viewBox="0 0 56 8" aria-hidden="true">'
         '<path d="M1,5.5 C14,1.5 30,7 55,2.5"/></svg>')

TIT = 'Onde quer que esteja, conte com a experi&ecirc;ncia da SEED.'
SUB = 'Especialistas em energia h&aacute; mais de uma d&eacute;cada'
EYE = '#PROVEMOS EFICI&Ecirc;NCIA'
CORPO_POST = (RISCO + f'<div class="eyebrow">{EYE}</div><div class="titulo">{TIT}</div>'
              f'<div class="assin">{SUB}</div>')
CORPO_CART = (RISCO + '<div class="eyebrow">RAFAEL SANT&#39;ANA &middot; CEO</div>'
              '<div class="titulo">SEED engenharia</div>'
              '<div class="assin">seed.eng.br &middot; contato@seed.eng.br</div>')
CORPO_BAN = RISCO + f'<div class="eyebrow">{EYE}</div><div class="titulo">Onde quer que esteja.</div>'

BASE = """<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">%(meta)s<title>%(t)s</title><style>%(faces)s
html,body{margin:0;background:#EDF2F4;font-family:'Montserrat',sans-serif;-webkit-font-smoothing:antialiased}
.card{position:relative;width:%(w)spx;height:%(h)spx;box-sizing:border-box;background:%(bg)s;%(borda)s
 padding:%(pad)spx;padding-bottom:calc(%(pad)spx + %(faixa)spx);display:flex;flex-direction:column;overflow:hidden}
.serra{position:absolute;left:0;right:0;bottom:0;width:100%%;height:%(faixa)spx;display:block}
.marca{line-height:0;margin-bottom:auto}
.eyebrow{font-family:'JetBrains Mono',monospace;font-size:%(fe)spx;font-weight:500;
 letter-spacing:1.6px;text-transform:uppercase;color:%(ceye)s}
.titulo{font-weight:700;font-size:%(ft)spx;line-height:1.12;letter-spacing:-.7px;margin:8px 0 0;color:%(ctit)s}
.assin{font-size:%(fa)spx;margin-top:10px;line-height:1.45;color:%(cass)s}
.risco{display:block;width:56px;height:8px;margin:0 0 10px}
.risco path{fill:none;stroke:#FAD61D;stroke-width:2;stroke-linecap:round}
</style></head><body><div class="card">
<div class="marca">%(sim)s</div><div>%(corpo)s</div>%(serra)s</div></body></html>"""


def faixa_de(h):
    return int(math.floor(h * 0.0795))


# ══════════════════════════════════════════════════════════════════════════════
# PISO DE TRAÇO EM MILÍMETROS — canon v5.6 §7.4-c
# ══════════════════════════════════════════════════════════════════════════════
# "saída B — piso de 0,25 mm por ESCALA, e só para peça IMPRESSA" (Rafael, 2026-08-22).
# Em peça impressa nenhum traço do grafismo fica abaixo de 0,25mm; quando o mais
# fino ficaria abaixo, TODOS os planos sobem pelo MESMO fator. ESCALA e não GRAMPO
# porque grampear achata planos no mesmo peso, e a hierarquia de peso é uma das três
# condições que fazem a linha ler como PLANO (§7.1 ③) — medido: 4 pesos caem para 2.
PISO_MM_CANON = 0.25


def _quantiza(pesos, k):
    """Aplica o fator k e arredonda em 2 casas — o MENOR para CIMA.

    ⚠ ARREDONDAMENTO: o menor peso é arredondado para CIMA (ceil em 2 casas), os
    outros para o mais próximo. Motivo MEDIDO em 2026-08-22: na pasta A4
    (11,80952 px/mm) o piso vale 2,95238 px e `round(...,2)` devolvia **2,95 px =
    0,24980 mm** — 0,0002 mm ABAIXO do canon. A guarda MM1 pegou. *Piso violado por
    arredondamento é piso que não existe; e arredondar o mínimo para cima só
    ENGROSSA, nunca afina, logo não pode criar um defeito novo.*
    """
    esc = [round(p * k, 2) for p in pesos]
    if k > 1.0:
        i = min(range(len(pesos)), key=lambda j: pesos[j])
        esc[i] = math.ceil(pesos[i] * k * 100) / 100.0
    return esc


def aplica_piso_mm(pesos, ppmm):
    """ppmm=None → peça de TELA, devolve intacto. ppmm dado → peça IMPRESSA."""
    if not ppmm:
        return pesos
    piso_px = PISO_MM_CANON * ppmm
    menor = min(pesos)
    k = (piso_px / menor) if menor < piso_px else 1.0
    return _quantiza(pesos, k)


# nome, w, h, pad, fe, ft, fa, tema, corpo, simbolo_px
FORMATOS = [
    ('F1-post-1080', 1080, 1080, 76, 26, 76, 36, CLARO, CORPO_POST, 110),
    ('F2-cartao', 1050, 600, 50, 17, 42, 23, CLARO, CORPO_CART, 84),
    ('F3-cartao-fundo', 1050, 600, 50, 17, 42, 23, ESCURO, CORPO_CART, 84),
    ('F4-banner-3x1', 1500, 500, 46, 16, 46, 23, CLARO, CORPO_BAN, 78),
    ('F5-capa-4x1', 1584, 396, 42, 15, 42, 21, ESCURO, CORPO_BAN, 70),
    # ⚠ F6 em 2480×3508px = A4 a 300 DPI EXATOS (MR-P8, 2026-08-22). As medidas
    # tipográficas vêm das do mock multiplicadas por 2480/874 = 2,8375, o que
    # preserva o tamanho FÍSICO de cada elemento em milímetro.
    ('F6-pasta-A4', 2480, 3508, 182, 57, 159, 79, CLARO, CORPO_POST, 272),
    ('F7-story-9x16', 1080, 1920, 76, 26, 76, 36, ESCURO, CORPO_POST, 110),
]

print('Z3 promovida aos 7 formatos · faixa = floor(h*0,0795) · fragmento por formato\n')
print(f"{'formato':18s} {'peça':>11s} {'faixa':>7s} {'%alt':>6s} {'traço':>6s} {'FR':>4s}")
# ALVO FÍSICO por peça — px por mm. Só peça IMPRESSA entra aqui; o resto é TELA.
# A pasta A4 tem 2480px para 210mm = 11,81 px/mm = 300 DPI.
PPMM = {'F6-pasta-A4': 2480 / 210.0}
ALVO_MM = {'F6-pasta-A4': (210.0, 297.0)}

for nome, w, h, pad, fe, ft, fa, tema, corpo, spx in FORMATOS:
    F = faixa_de(h)
    fator = max(1.5 / 3.5, min(4.5 / 3.5, F / 86.0))
    pesos = [round(p * fator, 2) for p in (3.5, 3.0, 2.5, 2.2)]
    pesos = aplica_piso_mm(pesos, PPMM.get(nome))
    fundo_v, morros, fr = cena_z3(float(w), float(F))
    capas, supress, frag_n = compoe(float(w), float(F), fundo_v, morros, fr)
    serra = svg_cena(float(w), float(F), capas, supress, tema['cores'], pesos, nome[:2])
    # ⚠ ALVO FÍSICO DECLARADO NO ARTEFATO (2026-08-22): a guarda do piso em mm
    # (contrato MM1) lê isto do arquivo, em vez de consultar lista de nomes.
    # Peça sem a meta sai [n/a] NOMEADO — peça de tela não tem milímetro.
    # ⚠ `papel=gate`: estes 7 arquivos são de GATE (do tamanho do corte, é o que a
    # GR1 mede). O arquivo de PRODUÇÃO da pasta A4 — com sangria, aba e vinco — NÃO
    # EXISTE e espera decisão dele (item E da §10 do inventario-pecas-ds.md).
    meta = (('<meta name="alvo-fisico" content="%gx%gmm">' % ALVO_MM[nome]) +
            '<meta name="papel" content="gate">') if nome in ALVO_MM else ''
    html = BASE % dict(t=nome, faces=FACES, w=w, h=h, pad=pad, faixa=F, meta=meta,
                       bg=tema['fundo'], borda=tema['borda'], fe=fe, ft=ft, fa=fa,
                       ceye=tema['ceye'], ctit=tema['ctit'], cass=tema['cass'],
                       sim=simbolo(tema['sim'], spx), corpo=corpo, serra=serra)
    (DEST / f'{nome}.html').write_text(html, encoding='utf-8')
    pp = PPMM.get(nome)
    mm = (' · impressa %.1f DPI · traço mais fino %.3fmm' % (pp * 25.4, min(pesos) / pp)) if pp else ''
    print(f'{nome:18s} {f"{w}x{h}":>11s} {F:5d}px {100*F/h:5.1f}% {pesos[0]:5.2f}px {frag_n:4d}{mm}')

print(f'\n7 peças em {DEST} · agora: guarda-grafismo + contraste-composicao sobre elas')
