You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
567 lines
17 KiB
567 lines
17 KiB
<script lang="ts">
|
|
type ContactPayload = {
|
|
email: string;
|
|
text: string;
|
|
};
|
|
|
|
type Status =
|
|
| 'editing'
|
|
| 'folding'
|
|
| 'ready'
|
|
| 'dragging'
|
|
| 'delivering' // La paloma atrapa la carta
|
|
| 'flying' // La paloma se va volando
|
|
| 'sent';
|
|
|
|
type Props = {
|
|
endpoint?: string;
|
|
onSent?: (payload: ContactPayload) => void | Promise<void>;
|
|
};
|
|
|
|
let { endpoint = '/api/contact', onSent }: Props = $props();
|
|
|
|
let form = $state<ContactPayload>({
|
|
email: '',
|
|
text: ''
|
|
});
|
|
|
|
let status = $state<Status>('editing');
|
|
let error = $state('');
|
|
|
|
let stageEl: HTMLElement;
|
|
let formCardEl: HTMLElement;
|
|
let letterEl: HTMLElement;
|
|
let pigeonBeakEl: SVGElement;
|
|
let pigeonDropZoneEl: SVGRectElement;
|
|
|
|
const letterSize = {
|
|
width: 140,
|
|
height: 90
|
|
};
|
|
|
|
let letter = $state({
|
|
x: 0,
|
|
y: 0,
|
|
homeX: 0,
|
|
homeY: 0,
|
|
dragStartX: 0,
|
|
dragStartY: 0,
|
|
pointerStartX: 0,
|
|
pointerStartY: 0,
|
|
isCaptured: false
|
|
});
|
|
|
|
let canSubmit = $derived(
|
|
status === 'editing' &&
|
|
form.email.trim().length > 0 &&
|
|
form.text.trim().length > 0
|
|
);
|
|
|
|
let showForm = $derived(status === 'editing' || status === 'folding');
|
|
let showLetter = $derived(
|
|
status === 'ready' ||
|
|
status === 'dragging' ||
|
|
status === 'delivering' ||
|
|
status === 'flying'
|
|
);
|
|
|
|
let isNearPigeon = $state(false);
|
|
|
|
let instruction = $derived.by(() => {
|
|
if (status === 'editing') return 'Rellena el formulario.';
|
|
if (status === 'folding') return 'Preparando el mensaje...';
|
|
if (status === 'ready') return 'Arrastra la carta hacia el pico de la paloma.';
|
|
if (status === 'dragging') return isNearPigeon ? '¡Suéltala ahí! La paloma la recogerá.' : 'Acerca la carta a la paloma.';
|
|
if (status === 'delivering') return '¡La tiene!';
|
|
if (status === 'flying') return 'La paloma vuela hacia el horizonte...';
|
|
return 'Mensaje enviado correctamente.';
|
|
});
|
|
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
}
|
|
|
|
function startChallenge(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
if (!canSubmit) {
|
|
error = 'Introduce un email y un mensaje antes de enviar.';
|
|
return;
|
|
}
|
|
|
|
error = '';
|
|
status = 'folding';
|
|
|
|
window.setTimeout(() => {
|
|
placeLetterOverForm();
|
|
status = 'ready';
|
|
}, 800);
|
|
}
|
|
|
|
function placeLetterOverForm() {
|
|
const stageRect = stageEl.getBoundingClientRect();
|
|
const formRect = formCardEl.getBoundingClientRect();
|
|
|
|
letter.x = formRect.left - stageRect.left + formRect.width / 2 - letterSize.width / 2;
|
|
letter.y = formRect.top - stageRect.top + formRect.height / 2 - letterSize.height / 2;
|
|
letter.homeX = letter.x;
|
|
letter.homeY = letter.y;
|
|
}
|
|
|
|
function grabLetter(event: PointerEvent) {
|
|
if (status !== 'ready') return;
|
|
const target = event.currentTarget as HTMLElement;
|
|
target.setPointerCapture(event.pointerId);
|
|
|
|
letter.isCaptured = true;
|
|
letter.dragStartX = letter.x;
|
|
letter.dragStartY = letter.y;
|
|
letter.pointerStartX = event.clientX;
|
|
letter.pointerStartY = event.clientY;
|
|
error = '';
|
|
status = 'dragging';
|
|
}
|
|
|
|
function moveLetter(event: PointerEvent) {
|
|
if (status !== 'dragging' || !letter.isCaptured) return;
|
|
|
|
letter.x = letter.dragStartX + event.clientX - letter.pointerStartX;
|
|
letter.y = letter.dragStartY + event.clientY - letter.pointerStartY;
|
|
|
|
// Comprobar si está cerca de la zona del pico para activar el estado visual "boca abierta"
|
|
isNearPigeon = checkProximity();
|
|
}
|
|
|
|
function checkProximity() {
|
|
if (!pigeonDropZoneEl || !letterEl) return false;
|
|
const dropRect = pigeonDropZoneEl.getBoundingClientRect();
|
|
const letterRect = letterEl.getBoundingClientRect();
|
|
|
|
const letterCenterX = letterRect.left + letterRect.width / 2;
|
|
const letterCenterY = letterRect.top + letterRect.height / 2;
|
|
|
|
return (
|
|
letterCenterX >= dropRect.left - 40 &&
|
|
letterCenterX <= dropRect.right + 40 &&
|
|
letterCenterY >= dropRect.top - 40 &&
|
|
letterCenterY <= dropRect.bottom + 40
|
|
);
|
|
}
|
|
|
|
async function releaseLetter(event: PointerEvent) {
|
|
if (status !== 'dragging') return;
|
|
const target = event.currentTarget as HTMLElement;
|
|
|
|
if (target.hasPointerCapture(event.pointerId)) {
|
|
target.releasePointerCapture(event.pointerId);
|
|
}
|
|
letter.isCaptured = false;
|
|
|
|
if (!checkProximity()) {
|
|
error = 'Lleva la carta hasta el pico de la paloma.';
|
|
status = 'ready';
|
|
isNearPigeon = false;
|
|
letter.x = letter.homeX;
|
|
letter.y = letter.homeY;
|
|
return;
|
|
}
|
|
|
|
await deliverToPigeon();
|
|
}
|
|
|
|
async function deliverToPigeon() {
|
|
error = '';
|
|
status = 'delivering';
|
|
|
|
// Bloquear la carta exactamente en la posición del pico de la paloma
|
|
const stageRect = stageEl.getBoundingClientRect();
|
|
const beakRect = pigeonBeakEl.getBoundingClientRect();
|
|
|
|
letter.x = beakRect.left - stageRect.left - 20;
|
|
letter.y = beakRect.top - stageRect.top - 20;
|
|
|
|
await sleep(800);
|
|
|
|
// Cambiar estado a volando e iniciar animación de salida
|
|
status = 'flying';
|
|
|
|
// Mandar los datos mientras vuela de fondo
|
|
sendForm();
|
|
}
|
|
|
|
async function sendForm() {
|
|
const payload: ContactPayload = {
|
|
email: form.email.trim(),
|
|
text: form.text.trim()
|
|
};
|
|
|
|
try {
|
|
if (onSent) {
|
|
await onSent(payload);
|
|
} else {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) throw new Error();
|
|
}
|
|
|
|
// Esperar a que la paloma complete su animación de vuelo antes de mostrar el éxito
|
|
await sleep(1500);
|
|
status = 'sent';
|
|
} catch {
|
|
// Si falla, la paloma regresa indignada
|
|
status = 'ready';
|
|
isNearPigeon = false;
|
|
letter.x = letter.homeX;
|
|
letter.y = letter.homeY;
|
|
error = 'La paloma se perdió en la tormenta. Inténtalo de nuevo.';
|
|
}
|
|
}
|
|
|
|
function resetForm() {
|
|
form.email = '';
|
|
form.text = '';
|
|
error = '';
|
|
status = 'editing';
|
|
isNearPigeon = false;
|
|
}
|
|
</script>
|
|
|
|
<section bind:this={stageEl} class="contact-stage">
|
|
<div class="floating-hint">
|
|
<p>{instruction}</p>
|
|
{#if error} <span role="alert" class="err-text">{error}</span> {/if}
|
|
</div>
|
|
|
|
<div class="left-panel">
|
|
{#if showForm}
|
|
<div bind:this={formCardEl} class={`form-card ${status === 'folding' ? 'is-folding' : ''}`}>
|
|
<form onsubmit={startChallenge}>
|
|
<div class="eyebrow">Mensajería Alada</div>
|
|
<h2>Enviar Correo</h2>
|
|
|
|
<label>
|
|
<span>Tu Email</span>
|
|
<input type="email" bind:value={form.email} disabled={status !== 'editing'} required placeholder="tu@email.com" />
|
|
</label>
|
|
|
|
<label>
|
|
<span>Mensaje</span>
|
|
<textarea bind:value={form.text} disabled={status !== 'editing'} required placeholder="Escribe algo bonito..." rows="4"></textarea>
|
|
</label>
|
|
|
|
<button type="submit" disabled={!canSubmit}>Sellar carta</button>
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if status === 'sent'}
|
|
<div class="success-card">
|
|
<div class="success-icon">🕊️</div>
|
|
<h3>¡Buen viaje!</h3>
|
|
<p>La paloma se ha llevado tu mensaje más allá del horizonte.</p>
|
|
<button type="button" onclick={resetForm}>Enviar otra paloma</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="right-panel">
|
|
<!-- ESCENARIO DE LA PALOMA -->
|
|
<div class={`pigeon-container state-${status} ${isNearPigeon ? 'is-hungry' : ''}`}>
|
|
<svg class="pigeon-svg" viewBox="0 0 400 400" width="100%" height="100%">
|
|
<defs>
|
|
<linearGradient id="skyGrad" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#e0efff" />
|
|
<stop offset="100%" stop-color="#ffffff" opacity="0"/>
|
|
</linearGradient>
|
|
<linearGradient id="pigeonBody" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#f3f4f6" />
|
|
<stop offset="60%" stop-color="#d1d5db" />
|
|
<stop offset="100%" stop-color="#9ca3af" />
|
|
</linearGradient>
|
|
<linearGradient id="pigeonWing" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#e5e7eb" />
|
|
<stop offset="100%" stop-color="#4b5563" />
|
|
</linearGradient>
|
|
<filter id="softShadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#1e293b" flood-opacity="0.15" />
|
|
</filter>
|
|
</defs>
|
|
|
|
<!-- Aura o fondo sutil de cielo -->
|
|
<circle cx="200" cy="200" r="150" fill="url(#skyGrad)" />
|
|
|
|
<!-- Rama donde se posa la paloma (Se queda quieta cuando vuela) -->
|
|
<g class="branch" filter="url(#softShadow)">
|
|
<path d="M 50 320 C 120 310, 280 320, 350 290" fill="none" stroke="#5c4033" stroke-width="12" stroke-linecap="round" />
|
|
<path d="M 280 300 C 300 290, 320 270, 330 250" fill="none" stroke="#5c4033" stroke-width="6" stroke-linecap="round" />
|
|
<!-- Pequeñas hojas verdes -->
|
|
<path d="M 325 255 Q 335 245, 340 255 Q 330 265, 325 255 Z" fill="#2e7d32" />
|
|
<path d="M 120 315 Q 110 300, 115 295 Q 125 305, 120 315 Z" fill="#2e7d32" />
|
|
</g>
|
|
|
|
<!-- GRUPO MÓVIL DE LA PALOMA (Hará la animación de salir volando) -->
|
|
<g class="pigeon-actor" filter="url(#softShadow)">
|
|
|
|
<!-- Patas -->
|
|
<g class="pigeon-legs" stroke="#f59e0b" stroke-width="4" stroke-linecap="round" fill="none">
|
|
<path d="M 185 275 L 180 315 M 180 315 L 170 318 M 180 315 L 185 319" />
|
|
<path d="M 205 275 L 210 312 M 210 312 L 202 316 M 210 312 L 218 315" />
|
|
</g>
|
|
|
|
<!-- Cola -->
|
|
<path class="pigeon-tail" d="M 150 250 L 110 280 L 130 290 Z" fill="#9ca3af" />
|
|
|
|
<!-- Cuerpo Principal -->
|
|
<path d="M 160 200 C 150 250, 220 280, 230 240 C 240 200, 210 170, 180 170 C 165 170, 160 185, 160 200 Z" fill="url(#pigeonBody)" />
|
|
|
|
<!-- Pecho brillante (Matiz verde/púrpura típico de palomas) -->
|
|
<path d="M 195 180 C 180 190, 185 220, 220 225 C 222 210, 215 190, 195 180 Z" fill="#14b8a6" opacity="0.25" />
|
|
|
|
<!-- Cabeza -->
|
|
<circle cx="210" cy="150" r="24" fill="url(#pigeonBody)" />
|
|
|
|
<!-- Ojo -->
|
|
<circle cx="218" cy="146" r="4" fill="#000000" />
|
|
<circle cx="219" cy="144" r="1.2" fill="#ffffff" /> <!-- Brillo del ojo -->
|
|
|
|
<!-- Pico Dinámico (Cambia con CSS si la carta está cerca o atrapada) -->
|
|
<g class="pigeon-beak" bind:this={pigeonBeakEl}>
|
|
<!-- Parte superior del pico -->
|
|
<path class="beak-top" d="M 232 146 L 252 150 L 233 156 Z" fill="#fbbf24" stroke="#d97706" stroke-width="1" />
|
|
<!-- Parte inferior del pico -->
|
|
<path class="beak-bottom" d="M 232 154 L 250 156 L 231 159 Z" fill="#f59e0b" stroke="#d97706" stroke-width="1" />
|
|
</g>
|
|
|
|
<!-- Ala Trasera (Solo visible en vuelo) -->
|
|
<path class="pigeon-wing-back" d="M 180 190 C 160 120, 130 110, 150 170 Z" fill="#6b7280" opacity="0.6"/>
|
|
|
|
<!-- Ala Principal (Tiene animación de aleteo en CSS) -->
|
|
<path class="pigeon-wing" d="M 185 200 C 160 170, 145 250, 195 245 C 210 240, 200 210, 185 200 Z" fill="url(#pigeonWing)" />
|
|
|
|
<!-- Zona de Drop interactiva invisible posicionada sobre el pico -->
|
|
<rect
|
|
bind:this={pigeonDropZoneEl}
|
|
x="210" y="120" width="70" height="70"
|
|
fill="transparent"
|
|
pointer-events="none"
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CARTA VOLADORA / ARRASTRABLE -->
|
|
{#if showLetter}
|
|
<div
|
|
bind:this={letterEl}
|
|
class={`letter-envelope status-${status}`}
|
|
style={`--x: ${letter.x}px; --y: ${letter.y}px;`}
|
|
onpointerdown={grabLetter}
|
|
onpointermove={moveLetter}
|
|
onpointerup={releaseLetter}
|
|
>
|
|
<svg width="140" height="90" viewBox="0 0 140 90" filter="url(#softShadow)">
|
|
<!-- Papel Crema -->
|
|
<rect width="140" height="90" rx="6" fill="#fefcf8" stroke="#e6dfd1" stroke-width="1.5" />
|
|
<!-- Sello postal azul -->
|
|
<rect x="110" y="8" width="18" height="22" fill="#3b82f6" rx="1" />
|
|
<circle cx="119" cy="19" r="4" fill="#ffffff" opacity="0.3" />
|
|
<!-- Líneas de dirección ficticias -->
|
|
<line x1="16" y1="50" x2="90" y2="50" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
|
|
<line x1="16" y1="62" x2="80" y2="62" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
|
|
<line x1="16" y1="74" x2="96" y2="74" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
|
|
<!-- Pliegue trasero del sobre -->
|
|
<path d="M 2 2 L 70 42 L 138 2" fill="none" stroke="#e2d8c5" stroke-width="1.5" />
|
|
</svg>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<style>
|
|
:global(*) { box-sizing: border-box; }
|
|
|
|
.contact-stage {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2rem;
|
|
min-height: 550px;
|
|
padding: 2rem;
|
|
background: linear-gradient(135deg, #f0f7ff 0%, #e2eeff 100%);
|
|
border-radius: 24px;
|
|
overflow: hidden;
|
|
align-items: center;
|
|
}
|
|
|
|
.floating-hint {
|
|
position: absolute;
|
|
top: 1.5rem;
|
|
left: 1.5rem;
|
|
z-index: 10;
|
|
background: rgba(255, 255, 255, 0.85);
|
|
padding: 0.75rem 1.2rem;
|
|
border-radius: 16px;
|
|
backdrop-filter: blur(8px);
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.05);
|
|
max-width: 340px;
|
|
}
|
|
.floating-hint p { margin: 0; font-weight: 700; color: #1e3a8a; }
|
|
.err-text { display: block; margin-top: 0.25rem; color: #ef4444; font-size: 0.88rem; }
|
|
|
|
.left-panel, .right-panel { position: relative; width: 100%; }
|
|
|
|
.form-card, .success-card {
|
|
background: #ffffff;
|
|
padding: 2rem;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 40px rgba(0, 50, 150, 0.06);
|
|
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
|
|
}
|
|
|
|
.form-card.is-folding {
|
|
transform: scale(0.3) rotateX(45deg) translateY(100px);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.eyebrow { font-size: 0.8rem; font-weight: 700; color: #3b82f6; text-transform: uppercase; margin-bottom: 0.5rem; }
|
|
h2 { margin: 0 0 1.5rem 0; color: #1e293b; font-size: 1.8rem; }
|
|
label { display: grid; gap: 0.4rem; margin-bottom: 1rem; font-weight: 600; color: #475569; }
|
|
|
|
input, textarea {
|
|
padding: 0.75rem 1rem;
|
|
border: 1.5px solid #cbd5e1;
|
|
border-radius: 12px;
|
|
outline: none;
|
|
font: inherit;
|
|
}
|
|
input:focus, textarea:focus { border-color: #3b82f6; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); }
|
|
|
|
button {
|
|
width: 100%;
|
|
padding: 0.85rem;
|
|
background: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.2);
|
|
transition: transform 0.2s, background 0.2s;
|
|
}
|
|
button:hover:not(:disabled) { background: #2563eb; transform: translateY(-2px); }
|
|
button:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
|
|
|
|
/* --- PANEL DE LA PALOMA --- */
|
|
.pigeon-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.pigeon-actor {
|
|
transform-origin: 200px 220px;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
/* Comportamiento del Pico */
|
|
.beak-top, .beak-bottom {
|
|
transform-origin: 232px 150px;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
/* Cuando la carta se acerca: Abre el pico */
|
|
.is-hungry .beak-top { transform: rotate(-15deg); }
|
|
.is-hungry .beak-bottom { transform: rotate(15deg); }
|
|
|
|
/* Cuando muerde la carta: Pico cerrado/apretado */
|
|
.state-delivering .beak-top, .state-flying .beak-top { transform: rotate(-2deg); }
|
|
.state-delivering .beak-bottom .state-flying .beak-bottom { transform: rotate(2deg); }
|
|
|
|
/* ANIMACIONES DE VUELO DE LA PALOMA */
|
|
.state-flying .pigeon-actor {
|
|
/* Animación combinada: Se echa a volar hacia arriba y hacia la derecha saliéndose del mapa */
|
|
animation: pigeonFlyAway 1.8s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
|
|
}
|
|
|
|
.state-flying .pigeon-wing {
|
|
/* Aleteo rápido */
|
|
animation: wingFlap 0.15s ease-in-out infinite alternate;
|
|
transform-origin: 185px 200px;
|
|
}
|
|
.state-flying .pigeon-wing-back {
|
|
animation: wingFlapBack 0.15s ease-in-out infinite alternate;
|
|
transform-origin: 180px 190px;
|
|
}
|
|
.state-flying .pigeon-legs {
|
|
/* Esconde las patas al volar */
|
|
transform: scaleY(0.2) translateY(-30px);
|
|
opacity: 0.5;
|
|
transition: transform 0.4s ease;
|
|
}
|
|
|
|
@keyframes pigeonFlyAway {
|
|
0% { transform: translate(0, 0) scale(1); }
|
|
20% { transform: translate(-10px, 15px) scale(0.98); } /* Pequeño impulso inicial abajo */
|
|
100% { transform: translate(320px, -450px) scale(0.25); } /* Se pierde en el horizonte superior */
|
|
}
|
|
|
|
@keyframes wingFlap {
|
|
0% { transform: rotate(0deg) scaleY(1); }
|
|
100% { transform: rotate(-65deg) scaleY(0.4); }
|
|
}
|
|
@keyframes wingFlapBack {
|
|
0% { transform: rotate(0deg) scaleY(0.4); }
|
|
100% { transform: rotate(50deg) scaleY(1.2); }
|
|
}
|
|
|
|
/* --- MANIPULACIÓN DE LA CARTA --- */
|
|
.letter-envelope {
|
|
position: absolute;
|
|
z-index: 50;
|
|
left: 0;
|
|
top: 0;
|
|
width: 140px;
|
|
height: 90px;
|
|
transform: translate3d(var(--x), var(--y), 0);
|
|
touch-action: none;
|
|
cursor: grab;
|
|
will-change: transform;
|
|
}
|
|
.letter-envelope:active { cursor: grabbing; }
|
|
|
|
/* Cuando la paloma la atrapa en el pico */
|
|
.status-delivering {
|
|
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Cuando vuela con ella */
|
|
.status-flying {
|
|
pointer-events: none;
|
|
animation: letterFlyAway 1.8s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
|
|
transform-origin: left center;
|
|
}
|
|
|
|
@keyframes letterFlyAway {
|
|
0% { transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
|
|
20% { transform: translate3d(calc(var(--x) - 10px), calc(var(--y) + 15px), 0) scale(0.98); }
|
|
100% {
|
|
/* Sigue exactamente la misma trayectoria del pico de la paloma */
|
|
transform: translate3d(calc(var(--x) + 320px), calc(var(--y) - -30px - 450px), 0) scale(0.25) rotate(-15deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.success-card { text-align: center; }
|
|
.success-icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
|
|
@media (max-width: 768px) {
|
|
.contact-stage { grid-template-columns: 1fr; }
|
|
.pigeon-container { height: 300px; }
|
|
}
|
|
</style> |