/* Tamanho de letra base do canvas = o do HTML exportado (16px, o default do
   browser). O body do editor usa 13px, e medidas em em/rem dos templates
   (ex: padding:1em, gap:1em nos templates especiais) davam 13px no editor
   e 16px no export — conteúdo com margens diferentes. */
#previewFrame {
    font-size: 16px;
    font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
}

/* blocks.css — toolbar do preview, canvas (3 níveis) e árvore de blocos.
   Valores base copiados diretamente do Nova (css/editor.css). */

.preview-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 14px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
    gap: 12px;
}

.device-buttons {
    display: flex;
    gap: 12px;
}

.device-group {
    display: flex;
    align-items: center;
    gap: 4px;
}

.device-btn {
    padding: 6px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 11px;
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    gap: 4px;
}

.device-btn:hover {
    background: var(--bg-hover);
}

.device-btn.active {
    background: var(--accent);
    border-color: var(--accent);
    color: white;
}

.device-dropdown {
    padding: 6px 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-size: 10px;
    cursor: pointer;
    max-width: 140px;
}

.device-dropdown:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.device-dropdown option,
.device-dropdown optgroup {
    background: #ffffff;
    color: #111111;
}

.device-dropdown:focus {
    outline: none;
    border-color: var(--accent);
}

.zoom-control {
    display: flex;
    align-items: center;
    gap: 6px;
}

.zoom-control button {
    width: 26px;
    height: 26px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}

.zoom-control button:hover {
    background: var(--bg-hover);
}

#zoomLevel {
    font-size: 11px;
    color: var(--text-secondary);
    min-width: 36px;
    text-align: center;
}

.preview-container {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    overflow: auto;
    background:
        linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
        linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
        linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
    background-size: 16px 16px;
    background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
    background-color: #141414;
}

/* Stage: recebe width/height JÁ escalados via JS (applyCanvasSize), para o
   transform:scale() do .preview-frame (transform-origin:top left) não deixar
   espaço "fantasma" a fazer scroll. */
.preview-stage {
    position: relative;
}

.preview-frame {
    position: relative;
    width: 1920px;
    height: 1080px;
    background: white;
    box-shadow: var(--shadow);
    transform-origin: top left;
    transition: transform 0.3s;
    overflow: hidden;
    --preview-ui-scale: 1;
}

.preview-frame.mobile {
    width: 1080px;
    height: 1920px;
}

.main-container {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ffffff;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

.preview-canvas {
    position: absolute;
    inset: 0;
    overflow: hidden;
}

/* ============================================
   3 NÍVEIS — Nível 2 (livre, sobreponível) e
   Nível 3 (célula de grelha, sem sobrepor).
   ============================================

   Cada bloco (qualquer nível) é TRÊS elementos, não um:
     .nv-block   — posição/tamanho/z-index/label/seleção (nunca tem padding
                   nem background)
     .nv-bg      — fundo (cor/imagem/border-radius/border), SEMPRE 100% do
                   bloco (position:absolute;inset:0) — o padding do Layout
                   Interno NUNCA encolhe o fundo
     .nv-inner   — padding + flex + overflow:hidden (recebe os filhos:
                   .nv-grid ou .block-content-scale), irmão de .nv-bg (não
                   filho), também position:absolute;inset:0, por cima e
                   transparente — só posiciona o CONTEÚDO dentro do espaço
                   já descontado do padding, nunca o fundo

   Bug real confirmado e corrigido (versão anterior, .nv-bg era filho de
   .nv-inner): o padding do Layout Interno esticava/encolhia o retângulo do
   FUNDO junto com o conteúdo — pedido explícito do utilizador era o
   inverso: cor/imagem de fundo ocupam sempre o bloco todo (seja Nível 2
   seja Nível 3), o padding cria só "respiro" para os media items/grelha,
   nunca para o fundo. Com .nv-bg e .nv-inner como IRMÃOS (ambos inset:0
   direto em .nv-block), o padding de .nv-inner nunca chega a tocar em
   .nv-bg — geometricamente impossível de voltar a acontecer por engano.

   IMPORTANTE (2º bug real, corrigido a seguir ao 1º): o CONTEÚDO
   (.nv-grid, .block-content-scale) tem de ser SEMPRE filho de .nv-inner,
   NUNCA de .nv-bg — chegou a ser anexado a .nv-bg por engano ao corrigir
   o bug do fundo acima, e isso fazia o padding perder qualquer efeito
   visual: wireBlockContentScale/measureItemOverflow (preview-panel.js)
   medem clientWidth/clientHeight e getBoundingClientRect() do elemento
   que RECEBE o conteúdo para saber o espaço disponível — medir a partir
   de .nv-bg (sempre 100% do bloco, sem padding) deixava os media items
   espalharem-se livremente até às bordas do bloco. .nv-bg é só um
   retângulo de fundo, nunca um container de conteúdo.

   .nv-block não tem padding (só .nv-inner tem), por isso o seu único
   filho absoluto (.nv-inner, inset:0) não sofre o bug abaixo — só
   acontece quando o PRÓPRIO elemento com padding é também o pai
   flex/grid de um filho position:absolute.

   Bug real confirmado e corrigido: um filho position:absolute+inset:0
   dentro de um pai display:flex QUE TAMBÉM TEM PADDING ignorava esse
   padding na prática (o width computado do filho batia com a largura
   TOTAL do pai, não com o espaço já descontado do padding) — por isso
   .nv-grid e .block-content-scale (os filhos reais de .nv-inner, que
   TEM padding) são sempre FLEX NORMAIS (flex:1 1 auto, sem position/
   inset), nunca position:absolute — um filho flex normal respeita
   sempre o padding do pai, sem essa ambiguidade. */

.nv-block {
    position: absolute;
    /* Mesma força visual do estado .selected (2px sólido) — pedido
       explícito do utilizador: no template "Livre" o bloco único fica
       selecionado por defeito (linha forte), mas templates com vários
       blocos (1x2, 2x1, etc.) selecionam o Canvas ao aplicar (ver
       templates.js: applyTemplate), deixando os blocos SEM seleção —
       a linha ténue de 1px tracejado ficava quase invisível nesses
       casos, dando a impressão de que só o "Livre" tinha contorno. */
    /* OUTLINE desenhado para dentro (offset -2px), NUNCA border: uma
       border ocupa espaço e encolhia a área de conteúdo do bloco em 2px
       de cada lado no editor, o que o HTML exportado (sem esta moldura de
       edição) não tem — preview e export davam tamanhos ligeiramente
       diferentes. Visualmente igual, mas sem afetar o layout. */
    border: 0;
    outline: 2px solid rgba(34, 197, 94, 0.5);
    outline-offset: -2px;
    cursor: pointer;
    transition: all 0.15s;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
}

.nv-block:hover {
    outline-color: rgba(34, 197, 94, 0.5);
    background: rgba(34, 197, 94, 0.03);
}

.nv-block.selected {
    outline-color: var(--accent);
    outline-style: solid;
    outline-width: 2px;
    box-shadow: inset 0 0 0 2px rgba(34, 197, 94, 0.2);
}

/* Drag do CORPO do bloco (mover X/Y) — só Nível 2 (livre), nunca Nível 3
   (preso à célula da grelha do pai, sem X/Y próprios). Cursor "move" já
   avisa a intenção antes de o utilizador começar a arrastar. */
.nv-block.nv-draggable.selected {
    cursor: move;
}

/* Pegas de resize nos 4 cantos — só aparecem no bloco selecionado, Nível
   2. overflow:visible aqui (só quando selecionado) para as pegas ficarem
   meio-dentro/meio-fora da borda sem serem cortadas pelo overflow:hidden
   normal do bloco (necessário para o conteúdo/media nunca "sair"). */
.nv-block.nv-draggable.selected {
    overflow: visible;
}

.nv-resize-handle {
    display: none; /* só aparecem no bloco SELECIONADO, ver .selected abaixo */
    position: absolute;
    width: 10px;
    height: 10px;
    background: var(--accent);
    border: 1.5px solid white;
    border-radius: 2px;
    z-index: 20;
    /* pointer-events:auto explícito: o bloco tem cursor:move no corpo
       inteiro, as pegas precisam do PRÓPRIO cursor de resize por cima. */
    pointer-events: auto;
}

.nv-block.selected .nv-resize-handle {
    display: block;
}

.nv-resize-handle[data-corner="nw"] { top: -3px; left: -3px; cursor: nwse-resize; }
.nv-resize-handle[data-corner="ne"] { top: -3px; right: -3px; cursor: nesw-resize; }
.nv-resize-handle[data-corner="sw"] { bottom: -3px; left: -3px; cursor: nesw-resize; }
.nv-resize-handle[data-corner="se"] { bottom: -3px; right: -3px; cursor: nwse-resize; }

/* Nível 3: ao contrário do Nível 2 (position:absolute + left/top/width/
   height livres), uma célula é posicionada pelo próprio CSS Grid do pai —
   por isso não herda position:absolute nem left/top/width/height. NUNCA
   fixar width/height:auto aqui — "auto" (diferente de "não definido") faz
   um item de grid resolver ao tamanho do CONTEÚDO em vez de esticar
   (stretch, o default do CSS Grid quando width/height simplesmente não
   são declarados) — bug real: a célula colapsava para uma linha fininha,
   porque o seu conteúdo (.nv-inner, position:absolute;inset:0) não tem
   tamanho intrínseco próprio para o "auto" se basear. Deixar a UA
   stylesheet do <div> tratar disto (sem width/height nenhum aqui) faz o
   grid esticar a célula à célula toda, como esperado. */
/* position:relative (não static): a célula continua posicionada pelo
   CSS Grid do pai normalmente (relative não tira o item do fluxo do
   grid) — mas TEM de criar o seu próprio contexto de posicionamento para
   os filhos .nv-bg/.nv-inner (position:absolute;inset:0, ver abaixo).
   Bug real confirmado: com position:static aqui, um .nv-bg/.nv-inner
   "absolute" sobe na árvore até ao primeiro ancestral POSICIONADO, que
   era o .nv-block Nível 2 (pai, position:absolute) — o fundo/conteúdo de
   uma célula acabava a cobrir o BLOCO INTEIRO em vez de só a fatia da
   célula (ex: imagem de fundo na Célula 1 de uma subdivisão em 2 linhas
   ocupava as duas linhas). */
.nv-block.nv-cell {
    position: relative;
    left: auto;
    top: auto;
    z-index: auto;
}

/* Fundo do bloco: cor e/ou imagem (ver block-inner-style-panel.js), raio
   (border-radius), border sólido. IRMÃO de .nv-inner (nunca filho —
   ver comentário grande acima), position:absolute;inset:0 direto em
   .nv-block — ocupa SEMPRE o retângulo do bloco inteiro, com ou sem
   padding configurado no Layout Interno. É o PRIMEIRO filho de .nv-block
   (pintado antes de .nv-inner, que fica visualmente por cima — z-index
   natural da ordem no DOM chega, não precisa de z-index explícito).

   Retângulo PURO de fundo — sem display:flex, sem conteúdo nenhum lá
   dentro (nunca media items, nunca a grelha). O CONTEÚDO REAL é sempre
   filho de .nv-inner (ver abaixo), que TEM o padding — nunca de .nv-bg,
   que não tem. pointer-events:none aqui: como .nv-bg nunca tem conteúdo
   interativo, herdar "none" nunca desativa nada de útil, e evita que um
   fundo por baixo intercete cliques por engano em cenários futuros. */
.nv-bg {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
}

/* Invólucro interno: recebe padding/flex/overflow E É QUEM RECEBE O
   CONTEÚDO REAL (.nv-grid ou .block-content-scale, ver preview-panel.js:
   buildBlockElement) — nunca o background (esse é sempre .nv-bg, irmão,
   nunca pai/filho deste elemento). flex-direction/justify-content/
   align-items/gap/padding são definidos inline pelo JS (Layout Interno,
   ver block-inner-style-panel.js) — sem defaults fixos aqui para não
   competir/mascarar os valores reais aplicados a cada bloco.

   position:absolute;inset:0, IRMÃO de .nv-bg (não seu pai) — por isso o
   bug antigo ("um filho position:absolute+inset:0 dentro de um pai
   display:flex COM padding ignora esse padding") não se aplica aqui:
   quem tem padding (.nv-inner) e quem é display:flex (.nv-inner) são o
   MESMO elemento, e os filhos reais do padding (.nv-grid,
   .block-content-scale) continuam FLEX NORMAIS dentro dele — só o próprio
   .nv-inner é que passou a absolute+inset:0, para ocupar exatamente
   .nv-block (sem depender de width/height:100% em fluxo normal, que
   colapsava em alguns casos de pai flex — Nível 3/.nv-cell). Fundo
   (.nv-bg) nunca vê este padding porque não é filho de .nv-inner.

   Bug real corrigido (2ª iteração desta separação em irmãos): uma versão
   anterior tinha o CONTEÚDO anexado a .nv-bg (por engano, na altura em
   que se corrigiu o fundo para nunca encolher com o padding) — o padding
   deixava de ter QUALQUER efeito visual, porque o mecanismo de medição/
   encolhimento (wireBlockContentScale, preview-panel.js) usa
   clientWidth/clientHeight e getBoundingClientRect() do elemento que
   RECEBE o conteúdo para saber o espaço disponível — medir a partir de
   .nv-bg (sempre 100% do bloco, sem padding) fazia os media items
   espalharem-se livremente até às bordas do bloco, ignorando por
   completo Top/Right/Bottom/Left configurados. Conteúdo TEM de ser
   filho de .nv-inner, nunca de .nv-bg, para o padding ser respeitado
   tanto pelo CSS puro como por qualquer medição feita em JS. */
.nv-inner {
    position: absolute;
    inset: 0;
    display: flex;
    overflow: hidden;
    background: transparent;
}

/* Área livre do CANVAS (Nível 1) — filho FLEX NORMAL de .nv-canvas-inner
   (nunca position:absolute), mesmo raciocínio de .nv-grid logo abaixo:
   respeita sempre o padding do pai, sem a ambiguidade de position:
   absolute+inset:0 dentro de um pai flex com padding. Os blocos Nível 2
   (sempre .nv-block, position:absolute) são filhos DESTA área, nunca
   diretos de .nv-canvas-inner — position:relative aqui é o que os torna
   containing block corretos (X/Y/Largura/Altura em % resolvem-se contra
   este espaço já descontado do padding do Canvas, não contra o Canvas
   inteiro). Ver preview-panel.js: renderPreview / export-html.js:
   buildMainContainerElement. */
.nv-canvas-free-area {
    position: relative;
    flex: 1 1 auto;
    align-self: stretch;
    min-width: 0;
    min-height: 0;
}

/* Grelha do Nível 3: CSS Grid real dentro do espaço (já sem padding) do
   Nível 2 pai. Substitui os media items diretos quando o bloco tem
   filhos — ver preview-panel.js: buildBlockElement (isLeaf decide).
   FILHO FLEX NORMAL do .nv-inner (nunca position:absolute) — flex:1 1
   auto respeita sempre o padding do pai, sem a ambiguidade de position:
   absolute+inset:0 num pai flex (ver .nv-inner, mesmo raciocínio).
   align-self:stretch é OBRIGATÓRIO aqui pela mesma razão que já existia
   em .block-content-scale — sem isto, align-items:center/flex-start/
   flex-end herdado do Layout Interno fazia .nv-grid encolher à largura
   NATURAL do seu conteúdo (a(s) célula(s)) em vez de esticar à largura
   toda do bloco (bug real: a célula única de uma grelha 1×1 aparecia
   mais estreita que o bloco, com o fundo do bloco visível dos lados,
   como se a grelha tivesse encolhido a meio do espaço). */
.nv-grid {
    flex: 1 1 auto;
    display: grid;
    overflow: hidden;
    align-self: stretch;
    min-width: 0;
    min-height: 0;
}

/* Wrapper que recebe os media items de um bloco folha (ver preview-panel.js:
   buildLeafContent/wireBlockContentScale) — existe só para poder aplicar
   transform:scale() ao CONJUNTO dos media items sem escalar o próprio
   .nv-inner (que tem padding/background geridos à parte).
   flex-direction/gap herdados do bloco pai via "inherit" — o Layout Interno
   continua a organizar os media items exatamente como organizava quando
   eram filhos diretos do bloco, só que agora dentro deste wrapper.

   flex:1 1 auto (ocupa o espaço TODO do bloco, nunca só o do conteúdo)
   é o que faz justify-content Space Between/Around/Evenly terem espaço
   real para distribuir os media items entre si — um wrapper que encolhe
   ao tamanho do próprio conteúdo (flex:0 0 auto) nunca sobra espaço
   nenhum lá dentro para distribuir, e todos os valores de justify-content
   pareciam produzir o mesmo resultado (bug real, corrigido no testeNova).
   O encolhimento por transform:scale (wireBlockContentScale) mede a soma
   dos .media-item filhos diretamente (offsetWidth/Height + gap). */
.nv-inner .block-content-scale {
    position: relative;
    display: flex;
    flex-direction: inherit;
    justify-content: inherit;
    align-items: inherit;
    gap: inherit;
    flex: 1 1 auto;
    /* Sempre stretch no EIXO TRANSVERSAL de .nv-bg, independentemente
       do align-items herdado (esse continua a controlar só o alinhamento
       dos .media-item DENTRO deste wrapper). Sem isto, align-items:
       center/flex-start/flex-end no Layout Interno fazia este wrapper
       encolher à largura NATURAL do conteúdo em vez de ocupar a largura
       toda do bloco — bug real, corrigido no testeNova. */
    align-self: stretch;
    min-width: 0;
    min-height: 0;
    transform-origin: center center;
}

.block-label {
    position: absolute;
    top: 2px;
    left: 2px;
    background: var(--accent);
    color: white;
    font-size: calc(9px * var(--preview-ui-scale));
    padding: calc(2px * var(--preview-ui-scale)) calc(5px * var(--preview-ui-scale));
    border-radius: calc(3px * var(--preview-ui-scale));
    z-index: 10;
    pointer-events: none;
    font-weight: 600;
}

.nv-inner .media-item {
    display: flex;
    flex-direction: inherit;
    align-items: inherit;
    justify-content: inherit;
    flex: 0 1 auto;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    cursor: pointer;
}

/* Clicar num media item no preview seleciona-o no painel "Conteúdos do
   Bloco" (ver preview-panel.js: buildLeafContent) — mesmo destaque visual
   que .nv-block.selected, mas com outline em vez de border (o media-item
   não tem border própria a perder). */
.nv-inner .media-item.selected {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* Countdown/Color Picker: o WRAPPER (.media-item) é sempre flex:1 1 auto
   (ocupa o espaço TODO do bloco, ver preview-panel.js:
   applyMediaStylesToElement — necessário para o countdown/color picker
   crescerem/encolherem corretamente), mas o CONTEÚDO visível
   (.countdown-widget/.colorpicker-widget) fica centrado dentro desse
   wrapper com tamanho bem menor — a outline de seleção acima desenhava à
   volta do wrapper esticado (ex: um retângulo vertical a toda a altura
   do bloco só com Padding 1x1), não à volta do widget em si, dando a
   impressão de "espaço gigante em cima/baixo" (bug real confirmado).
   Move-se a outline para o filho direto nesses dois casos — o resto
   (img/video normais) continua com a outline no próprio .media-item,
   que já tem o tamanho visual certo. */
.nv-inner .media-item.selected:has(> .countdown-widget),
.nv-inner .media-item.selected:has(> .colorpicker-widget) {
    outline: none;
}

.nv-inner .media-item.selected > .countdown-widget,
.nv-inner .media-item.selected > .colorpicker-widget {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.nv-inner .media-item img,
.nv-inner .media-item video {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.nv-inner .placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: calc(10px * var(--preview-ui-scale));
    gap: calc(4px * var(--preview-ui-scale));
    line-height: 1.15;
    text-align: center;
    max-width: 92%;
    padding: calc(6px * var(--preview-ui-scale));
    pointer-events: none;
    margin: auto;
}

.nv-inner .placeholder i {
    font-size: calc(18px * var(--preview-ui-scale));
    opacity: 0.5;
}

/* Vídeo: container próprio (mantém o botão de som alinhado ao vídeo, não ao
   bloco inteiro) e o botão de som sobreposto no canto inferior direito.
   aspect-ratio fixa o tamanho REAL do vídeo (não o do bloco) — o botão de
   som (position:absolute dentro deste container) fica sempre colado ao
   vídeo, não ao bloco inteiro. --video-aspect é definida por vídeo (ver
   preview-panel.js: buildVideoElement, a partir de videoWidth/videoHeight
   reais, com 16/9 como fallback antes de a metadata carregar). */
.nv-inner .preview-vctn {
    position: relative;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    display: flex;
    border-radius: 15px;
    overflow: hidden;
    isolation: isolate;
    aspect-ratio: var(--video-aspect, 16 / 9);
}

.nv-inner .preview-vctn video {
    display: block;
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 15px;
}

.nv-inner .preview-sound-btn {
    position: absolute;
    bottom: calc(10px * var(--preview-ui-scale));
    right: calc(10px * var(--preview-ui-scale));
    width: calc(34px * var(--preview-ui-scale));
    height: calc(34px * var(--preview-ui-scale));
    padding: 0;
    background: transparent;
    border: none;
    color: #ffffff;
    font-size: calc(24px * var(--preview-ui-scale));
    cursor: pointer;
    z-index: 2;
    pointer-events: auto;
    touch-action: manipulation;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: calc(6px * var(--preview-ui-scale));
}

.nv-inner .preview-sound-btn:hover {
    opacity: 0.85;
}

/* Zoom Video — no editor, o widget mostra sempre a posição final "docked"
   (sem animar, ver zoomvideo-widget.js: buildZoomVideoPreviewElement); o
   badge é só um indicador visual de que este media item tem esse
   comportamento configurado — o efeito real só se vê no Preview/Export. */
.zoomvideo-badge {
    position: absolute;
    top: calc(8px * var(--preview-ui-scale, 1));
    left: calc(8px * var(--preview-ui-scale, 1));
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: calc(3px * var(--preview-ui-scale, 1)) calc(7px * var(--preview-ui-scale, 1));
    font-size: calc(11px * var(--preview-ui-scale, 1));
    font-weight: 600;
    color: #fff;
    background: rgba(0, 0, 0, .55);
    border-radius: calc(5px * var(--preview-ui-scale, 1));
    pointer-events: none;
    white-space: nowrap;
}

/* Keyframes de animação de entrada/saída (preview do editor — toca 1 vez
   quando o utilizador altera a animação; ver applyAnimationPreview em
   preview-panel.js). */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeInDown { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeInLeft { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } }
@keyframes fadeInRight { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
@keyframes fadeOutUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-30px); } }
@keyframes fadeOutDown { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(30px); } }
@keyframes fadeOutLeft { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-30px); } }
@keyframes fadeOutRight { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(30px); } }
@keyframes slideInUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes slideInDown { from { transform: translateY(-100%); } to { transform: translateY(0); } }
@keyframes slideInLeft { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes slideOutUp { from { transform: translateY(0); } to { transform: translateY(-100%); } }
@keyframes slideOutDown { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes slideOutLeft { from { transform: translateX(0); } to { transform: translateX(-100%); } }
@keyframes slideOutRight { from { transform: translateX(0); } to { transform: translateX(100%); } }
@keyframes zoomIn { from { opacity: 0; transform: scale(0.3); } to { opacity: 1; transform: scale(1); } }
@keyframes zoomInUp { from { opacity: 0; transform: scale(0.3) translateY(30px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@keyframes zoomInDown { from { opacity: 0; transform: scale(0.3) translateY(-30px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@keyframes zoomOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.3); } }
@keyframes bounceIn { 0% { opacity: 0; transform: scale(0.3); } 50% { transform: scale(1.05); } 70% { transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } }
@keyframes bounceOut { 0% { transform: scale(1); } 20% { transform: scale(0.9); } 50%, 55% { opacity: 1; transform: scale(1.1); } 100% { opacity: 0; transform: scale(0.3); } }
@keyframes flipInX { from { transform: perspective(400px) rotateX(90deg); opacity: 0; } to { transform: perspective(400px) rotateX(0); opacity: 1; } }
@keyframes flipInY { from { transform: perspective(400px) rotateY(90deg); opacity: 0; } to { transform: perspective(400px) rotateY(0); opacity: 1; } }
