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.
1059 lines
26 KiB
1059 lines
26 KiB
<script lang="ts">
|
|
type ContactPayload = {
|
|
email: string;
|
|
text: string;
|
|
};
|
|
|
|
type Status =
|
|
| 'editing'
|
|
| 'folding'
|
|
| 'ready'
|
|
| 'dragging'
|
|
| 'delivering'
|
|
| 'sending'
|
|
| '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 mailboxSlotEl: SVGRectElement;
|
|
let mailboxDropZoneEl: SVGRectElement;
|
|
|
|
const letterSize = {
|
|
width: 180,
|
|
height: 116
|
|
};
|
|
|
|
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 === 'sending'
|
|
);
|
|
|
|
let showFloatingHint = $derived(
|
|
status === 'ready' ||
|
|
status === 'dragging' ||
|
|
status === 'delivering' ||
|
|
status === 'sending'
|
|
);
|
|
|
|
let mailboxReceiving = $derived(status === 'delivering' || status === 'sending');
|
|
|
|
let flagLowered = $derived(
|
|
status === 'delivering' ||
|
|
status === 'sending' ||
|
|
status === 'sent'
|
|
);
|
|
|
|
let instruction = $derived.by(() => {
|
|
if (status === 'editing') return 'Rellena el formulario.';
|
|
if (status === 'folding') return 'Convirtiendo el formulario en una carta...';
|
|
if (status === 'ready') return 'Arrastra la carta hasta el buzón.';
|
|
if (status === 'dragging') return 'Suelta la carta sobre el buzón.';
|
|
if (status === 'delivering') return 'Metiendo la carta en el buzón...';
|
|
if (status === 'sending') return 'Enviando mensaje...';
|
|
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';
|
|
}, 820);
|
|
}
|
|
|
|
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 returnLetterHome() {
|
|
letter.x = letter.homeX;
|
|
letter.y = letter.homeY;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 (!isLetterOverMailbox()) {
|
|
error = 'Tienes que soltar la carta encima del buzón.';
|
|
status = 'ready';
|
|
returnLetterHome();
|
|
return;
|
|
}
|
|
|
|
await deliverLetterIntoMailbox();
|
|
await sendForm();
|
|
}
|
|
|
|
function cancelDrag(event: PointerEvent) {
|
|
const target = event.currentTarget as HTMLElement;
|
|
|
|
if (target.hasPointerCapture(event.pointerId)) {
|
|
target.releasePointerCapture(event.pointerId);
|
|
}
|
|
|
|
letter.isCaptured = false;
|
|
|
|
if (status === 'dragging') {
|
|
status = 'ready';
|
|
returnLetterHome();
|
|
}
|
|
}
|
|
|
|
function isLetterOverMailbox() {
|
|
const dropRect = mailboxDropZoneEl.getBoundingClientRect();
|
|
const letterRect = letterEl.getBoundingClientRect();
|
|
|
|
const letterCenterX = letterRect.left + letterRect.width / 2;
|
|
const letterCenterY = letterRect.top + letterRect.height / 2;
|
|
|
|
return (
|
|
letterCenterX >= dropRect.left &&
|
|
letterCenterX <= dropRect.right &&
|
|
letterCenterY >= dropRect.top &&
|
|
letterCenterY <= dropRect.bottom
|
|
);
|
|
}
|
|
|
|
function snapLetterToSlot() {
|
|
const stageRect = stageEl.getBoundingClientRect();
|
|
const slotRect = mailboxSlotEl.getBoundingClientRect();
|
|
|
|
letter.x =
|
|
slotRect.left -
|
|
stageRect.left +
|
|
slotRect.width / 2 -
|
|
letterSize.width / 2;
|
|
|
|
letter.y =
|
|
slotRect.top -
|
|
stageRect.top +
|
|
slotRect.height / 2 -
|
|
letterSize.height / 2;
|
|
}
|
|
|
|
async function deliverLetterIntoMailbox() {
|
|
error = '';
|
|
|
|
snapLetterToSlot();
|
|
|
|
status = 'delivering';
|
|
|
|
await sleep(760);
|
|
|
|
status = 'sending';
|
|
|
|
await sleep(280);
|
|
}
|
|
|
|
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(`Error ${response.status}`);
|
|
}
|
|
}
|
|
|
|
status = 'sent';
|
|
} catch {
|
|
returnLetterHome();
|
|
status = 'ready';
|
|
error = 'No se pudo enviar el mensaje. Inténtalo otra vez.';
|
|
}
|
|
}
|
|
|
|
function resetForm() {
|
|
form.email = '';
|
|
form.text = '';
|
|
error = '';
|
|
status = 'editing';
|
|
|
|
letter.x = 0;
|
|
letter.y = 0;
|
|
letter.homeX = 0;
|
|
letter.homeY = 0;
|
|
letter.dragStartX = 0;
|
|
letter.dragStartY = 0;
|
|
letter.pointerStartX = 0;
|
|
letter.pointerStartY = 0;
|
|
letter.isCaptured = false;
|
|
}
|
|
</script>
|
|
|
|
<section
|
|
bind:this={stageEl}
|
|
class={`contact-mailbox ${status === 'folding' ? 'is-folding' : ''}`}
|
|
>
|
|
{#if showFloatingHint}
|
|
<div class="floating-hint">
|
|
<p>{instruction}</p>
|
|
|
|
{#if error}
|
|
<span role="alert">{error}</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="left-panel">
|
|
{#if showForm}
|
|
<div bind:this={formCardEl} class="form-card">
|
|
<form onsubmit={startChallenge}>
|
|
<div class="eyebrow">Contacto</div>
|
|
|
|
<h2>Envíanos un mensaje</h2>
|
|
|
|
<label>
|
|
<span>Email</span>
|
|
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
autocomplete="email"
|
|
placeholder="tu@email.com"
|
|
bind:value={form.email}
|
|
disabled={status !== 'editing'}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Texto</span>
|
|
|
|
<textarea
|
|
name="text"
|
|
rows="6"
|
|
placeholder="Escribe tu mensaje..."
|
|
bind:value={form.text}
|
|
disabled={status !== 'editing'}
|
|
required
|
|
></textarea>
|
|
</label>
|
|
|
|
<button type="submit" disabled={!canSubmit}>
|
|
Preparar carta
|
|
</button>
|
|
|
|
<p class="hint">{instruction}</p>
|
|
|
|
{#if error}
|
|
<p class="error" role="alert">{error}</p>
|
|
{/if}
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if status === 'sent'}
|
|
<div class="success-card">
|
|
<div class="success-icon">✓</div>
|
|
|
|
<h3>Mensaje enviado</h3>
|
|
|
|
<p>La carta ha llegado al buzón.</p>
|
|
|
|
<button type="button" onclick={resetForm}>
|
|
Enviar otro mensaje
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="right-panel" aria-hidden="true">
|
|
<!-- BUZÓN ULTRA MEJORADO CON DETALLES 3D, SOMBRAS FILTRADAS Y RENDERIZADO AVANZADO -->
|
|
<svg
|
|
class={`mailbox ${mailboxReceiving ? 'is-receiving' : ''} ${flagLowered ? 'is-flag-down' : ''}`}
|
|
viewBox="0 0 420 420"
|
|
role="img"
|
|
>
|
|
<defs>
|
|
<!-- Filtro de sombra suave realista para las placas y viseras -->
|
|
<filter id="softShadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
<feDropShadow dx="0" dy="4" stdDeviation="5" flood-color="#0a193d" flood-opacity="0.3" />
|
|
</filter>
|
|
|
|
<filter id="innerShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
<feComponentTransfer in="SourceAlpha">
|
|
<feFuncA type="linear" slope="0.7"/>
|
|
</feComponentTransfer>
|
|
<feGaussianBlur stdDeviation="3" result="blur"/>
|
|
<feOffset dx="0" dy="4"/>
|
|
<feComposite operator="out" in2="SourceGraphic"/>
|
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.8 0"/>
|
|
<feBlend mode="multiply" in2="SourceGraphic"/>
|
|
</filter>
|
|
|
|
<!-- Gradiente cilíndrico realista para el caparazón curvado -->
|
|
<linearGradient id="realMailboxBody" x1="0" x2="1" y1="0" y2="0">
|
|
<stop offset="0%" stop-color="#1e3c72" />
|
|
<stop offset="10%" stop-color="#2a5298" />
|
|
<stop offset="50%" stop-color="#4a76c5" />
|
|
<stop offset="85%" stop-color="#2a5298" />
|
|
<stop offset="100%" stop-color="#11254c" />
|
|
</linearGradient>
|
|
|
|
<!-- Gradiente vertical para la puerta frontal hundida -->
|
|
<linearGradient id="realMailboxFront" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#2b5ec2" />
|
|
<stop offset="100%" stop-color="#173b87" />
|
|
</linearGradient>
|
|
|
|
<!-- Gradiente metálico para la visera texturizada de la ranura -->
|
|
<linearGradient id="viserMetal" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#a4b0be" />
|
|
<stop offset="30%" stop-color="#747d8c" />
|
|
<stop offset="100%" stop-color="#2f3542" />
|
|
</linearGradient>
|
|
|
|
<!-- Gradiente de la bandera roja con relieves -->
|
|
<linearGradient id="realFlag" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#ff4757" />
|
|
<stop offset="70%" stop-color="#ee5253" />
|
|
<stop offset="100%" stop-color="#ff6b81" />
|
|
</linearGradient>
|
|
|
|
<!-- Mástil de metal fundido -->
|
|
<linearGradient id="mastMetal" x1="0" x2="1" y1="0" y2="0">
|
|
<stop offset="0%" stop-color="#57606f" />
|
|
<stop offset="50%" stop-color="#ffffff" />
|
|
<stop offset="100%" stop-color="#2f3542" />
|
|
</linearGradient>
|
|
|
|
<!-- Poste de anclaje de acero realista -->
|
|
<linearGradient id="steelPost" x1="0" x2="1" y1="0" y2="0">
|
|
<stop offset="0%" stop-color="#2f3542" />
|
|
<stop offset="35%" stop-color="#747d8c" />
|
|
<stop offset="65%" stop-color="#ced6e0" />
|
|
<stop offset="100%" stop-color="#2f3542" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Sombra proyectada en el suelo -->
|
|
<ellipse
|
|
class="mailbox-shadow"
|
|
cx="210"
|
|
cy="354"
|
|
rx="132"
|
|
ry="24"
|
|
fill="#dce7ff"
|
|
/>
|
|
|
|
<g class="mailbox-visual">
|
|
<!-- Poste de soporte de acero realista de la imagen -->
|
|
<rect x="198" y="300" width="24" height="95" rx="2" fill="url(#steelPost)" />
|
|
|
|
<!-- Chasis Curvo 3D (Cuerpo Principal) -->
|
|
<path
|
|
class="mailbox-shell"
|
|
d="M110 194c0-68 55-123 123-123h4c68 0 123 55 123 123v127H110Z"
|
|
fill="url(#realMailboxBody)"
|
|
/>
|
|
|
|
<!-- Junta de extrusión trasera / Sombra perimetral interna -->
|
|
<path
|
|
d="M116 194c0-63 50-114 114-114h4c63 0 114 50 114 114v121H116Z"
|
|
fill="#0d1f42"
|
|
opacity="0.4"
|
|
/>
|
|
|
|
<!-- Placa Frontal Hundida con bisel -->
|
|
<rect
|
|
class="mailbox-front"
|
|
x="114"
|
|
y="192"
|
|
width="242"
|
|
height="125"
|
|
rx="12"
|
|
fill="url(#realMailboxFront)"
|
|
/>
|
|
|
|
<!-- Remaches de metal individuales con brillo propio estilo industrial -->
|
|
<g fill="#95afc0" stroke="#2c3e50" stroke-width="0.5">
|
|
<circle cx="124" cy="202" r="3" /> <circle cx="124" cy="202" r="0.8" fill="#fff" />
|
|
<circle cx="346" cy="202" r="3" /> <circle cx="346" cy="202" r="0.8" fill="#fff" />
|
|
<circle cx="124" cy="308" r="3" /> <circle cx="124" cy="308" r="0.8" fill="#fff" />
|
|
<circle cx="346" cy="308" r="3" /> <circle cx="346" cy="308" r="0.8" fill="#fff" />
|
|
<circle cx="235" cy="79" r="2.5" fill="url(#mastMetal)"/>
|
|
</g>
|
|
|
|
<!-- Ranura de entrada de correspondencia (Oscuridad del fondo) -->
|
|
<rect
|
|
bind:this={mailboxSlotEl}
|
|
class="mailbox-slot"
|
|
x="142"
|
|
y="226"
|
|
width="186"
|
|
height="32"
|
|
rx="16"
|
|
fill="#050b1a"
|
|
/>
|
|
|
|
<!-- Sombra interior generada para la profundidad de la ranura -->
|
|
<rect x="142" y="226" width="186" height="32" rx="16" fill="url(#innerShadow)" pointer-events="none" />
|
|
|
|
<!-- Visera/Embellecedor protector basculante metálico de la imagen -->
|
|
<rect
|
|
class="mailbox-slot-lip"
|
|
x="134"
|
|
y="214"
|
|
width="202"
|
|
height="16"
|
|
rx="8"
|
|
fill="url(#viserMetal)"
|
|
filter="url(#softShadow)"
|
|
/>
|
|
<!-- Brillo reflectante horizontal en el embellecedor -->
|
|
<rect x="144" y="217" width="182" height="3" rx="1.5" fill="#ffffff" opacity="0.4" pointer-events="none" />
|
|
|
|
<!-- Zona invisible para arrastrar y soltar con holgura interactiva -->
|
|
<rect
|
|
bind:this={mailboxDropZoneEl}
|
|
x="118"
|
|
y="176"
|
|
width="248"
|
|
height="132"
|
|
rx="28"
|
|
fill="transparent"
|
|
pointer-events="none"
|
|
/>
|
|
</g>
|
|
|
|
<!-- Soporte mecánico de la bandera (Eje rotatorio de metal) -->
|
|
<g class="mailbox-flag-pole">
|
|
<rect x="294" y="116" width="12" height="110" rx="6" fill="url(#mastMetal)" filter="url(#softShadow)" />
|
|
<!-- Perno de rotación centralizado -->
|
|
<circle cx="300" cy="210" r="14" fill="url(#viserMetal)" stroke="#1c2029" stroke-width="1" />
|
|
<circle cx="300" cy="210" r="6" fill="#1e2530" />
|
|
</g>
|
|
|
|
<!-- Conjunto dinámico de la bandera de aviso roja -->
|
|
<g class="mailbox-flag-arm" filter="url(#softShadow)">
|
|
<!-- Cuerpo de la bandera -->
|
|
<path
|
|
d="M 300 110 L 376 110 C 382 110, 386 114, 386 120 L 386 160 C 386 166, 382 170, 376 170 L 300 170 Z"
|
|
fill="url(#realFlag)"
|
|
/>
|
|
<!-- Contrapeso estético estilizado o nervio de torsión -->
|
|
<rect x="306" y="116" width="12" height="48" rx="3" fill="#b31217" opacity="0.4" />
|
|
</g>
|
|
|
|
<!-- Destellos/Sparks cinéticos del sistema original -->
|
|
<g
|
|
class="mailbox-sparks"
|
|
fill="none"
|
|
stroke="#ffd166"
|
|
stroke-width="7"
|
|
stroke-linecap="round"
|
|
>
|
|
<path d="M92 196l-26-13" />
|
|
<path d="M94 240H62" />
|
|
<path d="M103 282l-24 18" />
|
|
<path d="M368 196l26-13" />
|
|
<path d="M370 240h32" />
|
|
<path d="M360 282l24 18" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
{#if showLetter}
|
|
<div
|
|
bind:this={letterEl}
|
|
class={`letter ${status === 'dragging' ? 'is-dragging' : ''} ${status === 'delivering' ? 'is-delivering' : ''} ${status === 'sending' ? 'is-sending' : ''}`}
|
|
style={`--x: ${letter.x}px; --y: ${letter.y}px;`}
|
|
aria-label="Carta del formulario. Arrástrala hasta el buzón."
|
|
onpointerdown={grabLetter}
|
|
onpointermove={moveLetter}
|
|
onpointerup={releaseLetter}
|
|
onpointercancel={cancelDrag}
|
|
>
|
|
<!-- SOBRE VECTORIAL REALISTA DE PAPEL CREMA CON SELLO DENTADO E ILUMINACIÓN -->
|
|
<svg width="180" height="116" viewBox="0 0 180 116" style="overflow: visible;">
|
|
<defs>
|
|
<!-- Filtro de sombra suave exclusivo para el sobre flotante -->
|
|
<filter id="letterShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
<feDropShadow dx="0" dy="12" stdDeviation="10" flood-color="#0a122c" flood-opacity="0.22" />
|
|
</filter>
|
|
|
|
<!-- Textura degradada de papel pergamino sutil/crema de la imagen -->
|
|
<linearGradient id="envelopePaper" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#ffffff" />
|
|
<stop offset="70%" stop-color="#fdfbf7" />
|
|
<stop offset="100%" stop-color="#f5f0e4" />
|
|
</linearGradient>
|
|
|
|
<!-- Sello postal clásico degradado cian/azul real de la imagen -->
|
|
<linearGradient id="stampBlue" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#3a7bd5" />
|
|
<stop offset="100%" stop-color="#3a6073" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Cuerpo base del contenedor del sobre -->
|
|
<rect
|
|
width="180"
|
|
height="116"
|
|
rx="8"
|
|
fill="url(#envelopePaper)"
|
|
stroke="#e4dcce"
|
|
stroke-width="1"
|
|
filter="url(#letterShadow)"
|
|
/>
|
|
|
|
<!-- Líneas simuladas de caligrafía manuscrita fina con tinta oscura -->
|
|
<g opacity="0.65" stroke-linecap="round">
|
|
<line x1="26" y1="64" x2="114" y2="64" stroke="#4a5568" stroke-width="2.5" />
|
|
<line x1="26" y1="78" x2="98" y2="78" stroke="#4a5568" stroke-width="2.5" />
|
|
<line x1="26" y1="92" x2="124" y2="92" stroke="#4a5568" stroke-width="2.5" />
|
|
</g>
|
|
|
|
<!-- Sello de Correos Dentado Detallado (Esquina Superior Derecha) -->
|
|
<g transform="translate(136, 16)">
|
|
<!-- Fondo del Sello -->
|
|
<rect width="24" height="28" fill="url(#stampBlue)" rx="1" />
|
|
|
|
<!-- Patrón Dentado Real mediante círculos de recorte perimetrales -->
|
|
<g fill="#fdfbf7">
|
|
<!-- Superior -->
|
|
<circle cx="4" cy="0" r="1.2" /> <circle cx="9" cy="0" r="1.2" /> <circle cx="14" cy="0" r="1.2" /> <circle cx="19" cy="0" r="1.2" />
|
|
<!-- Inferior -->
|
|
<circle cx="4" cy="28" r="1.2" /> <circle cx="9" cy="28" r="1.2" /> <circle cx="14" cy="28" r="1.2" /> <circle cx="19" cy="28" r="1.2" />
|
|
<!-- Izquierda -->
|
|
<circle cx="0" cy="5" r="1.2" /> <circle cx="0" cy="11" r="1.2" /> <circle cx="0" cy="17" r="1.2" /> <circle cx="0" cy="23" r="1.2" />
|
|
<!-- Derecha -->
|
|
<circle cx="24" cy="5" r="1.2" /> <circle cx="24" cy="11" r="1.2" /> <circle cx="24" cy="17" r="1.2" /> <circle cx="24" cy="23" r="1.2" />
|
|
</g>
|
|
|
|
<!-- Ilustración interna simbólica del sello (Perfil clásico) -->
|
|
<rect x="4" y="4" width="16" height="20" fill="none" stroke="#ffffff" stroke-width="1" opacity="0.4" />
|
|
<circle cx="12" cy="13" r="5" fill="none" stroke="#ffffff" stroke-width="1" opacity="0.6" />
|
|
</g>
|
|
|
|
<!-- Marcas de Matasellos onduladas vintage -->
|
|
<g fill="none" stroke="#2c3e50" stroke-width="0.75" opacity="0.3" stroke-linecap="round">
|
|
<path d="M 124 24 Q 130 20 138 26" />
|
|
<path d="M 121 32 Q 128 28 137 35" />
|
|
<path d="M 123 40 Q 131 35 136 44" />
|
|
</g>
|
|
|
|
<!-- Pliegues Físicos y Solapas de la Estructura del Sobre -->
|
|
<!-- Sombra proyectada por la solapa triangular superior -->
|
|
<path d="M 2 2 L 90 56 L 178 2" fill="none" stroke="#000" stroke-width="3" opacity="0.06" />
|
|
<!-- Líneas de pliegue nítidas -->
|
|
<path d="M 1 1 L 90 55 L 179 1" fill="none" stroke="#dfd5c1" stroke-width="1.5" />
|
|
<path d="M 1 115 L 72 50" fill="none" stroke="#eadabe" stroke-width="1.2" />
|
|
<path d="M 179 115 L 108 50" fill="none" stroke="#eadabe" stroke-width="1.2" />
|
|
</svg>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<style>
|
|
:global(*) {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.contact-mailbox {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: minmax(280px, 1fr) minmax(280px, 0.9fr);
|
|
gap: clamp(1.5rem, 4vw, 4rem);
|
|
align-items: center;
|
|
min-height: 560px;
|
|
padding: clamp(1rem, 3vw, 2.5rem);
|
|
overflow: hidden;
|
|
border-radius: 32px;
|
|
background:
|
|
radial-gradient(circle at 82% 18%, rgba(79, 140, 255, 0.22), transparent 34%),
|
|
linear-gradient(135deg, #f7faff 0%, #eef4ff 100%);
|
|
}
|
|
|
|
.left-panel,
|
|
.right-panel {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.left-panel {
|
|
min-height: 460px;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.form-card,
|
|
.success-card {
|
|
width: min(100%, 440px);
|
|
padding: 2rem;
|
|
border: 1px solid rgba(31, 58, 113, 0.1);
|
|
border-radius: 28px;
|
|
background: rgba(255, 255, 255, 0.92);
|
|
box-shadow: 0 24px 70px rgba(36, 87, 214, 0.14);
|
|
backdrop-filter: blur(14px);
|
|
}
|
|
|
|
.form-card {
|
|
transform-origin: center;
|
|
transition:
|
|
transform 0.8s cubic-bezier(0.65, 0, 0.2, 1),
|
|
opacity 0.45s ease,
|
|
filter 0.45s ease;
|
|
}
|
|
|
|
.is-folding .form-card {
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
filter: blur(4px);
|
|
transform:
|
|
perspective(900px)
|
|
rotateX(72deg)
|
|
scale(0.42)
|
|
translateY(30px);
|
|
}
|
|
|
|
.floating-hint {
|
|
position: absolute;
|
|
left: clamp(1rem, 3vw, 2.5rem);
|
|
top: clamp(1rem, 3vw, 2.5rem);
|
|
z-index: 5;
|
|
max-width: 360px;
|
|
padding: 0.85rem 1rem;
|
|
border: 1px solid rgba(31, 58, 113, 0.1);
|
|
border-radius: 18px;
|
|
background: rgba(255, 255, 255, 0.88);
|
|
box-shadow: 0 14px 40px rgba(16, 37, 95, 0.12);
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.floating-hint p {
|
|
margin: 0;
|
|
color: #10255f;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.floating-hint span {
|
|
display: block;
|
|
margin-top: 0.35rem;
|
|
color: #c1272d;
|
|
font-size: 0.92rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.eyebrow {
|
|
width: fit-content;
|
|
margin-bottom: 0.75rem;
|
|
padding: 0.35rem 0.7rem;
|
|
border-radius: 999px;
|
|
color: #2457d6;
|
|
background: #e7efff;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h2,
|
|
h3,
|
|
p {
|
|
margin: 0;
|
|
}
|
|
|
|
h2 {
|
|
margin-bottom: 1.4rem;
|
|
color: #10255f;
|
|
font-size: clamp(1.7rem, 3vw, 2.35rem);
|
|
line-height: 1.05;
|
|
}
|
|
|
|
label {
|
|
display: grid;
|
|
gap: 0.45rem;
|
|
margin-bottom: 1rem;
|
|
color: #22345f;
|
|
font-weight: 700;
|
|
}
|
|
|
|
input,
|
|
textarea {
|
|
width: 100%;
|
|
border: 1px solid #d7e2ff;
|
|
border-radius: 16px;
|
|
padding: 0.9rem 1rem;
|
|
color: #10255f;
|
|
background: #fbfdff;
|
|
font: inherit;
|
|
outline: none;
|
|
transition:
|
|
border-color 0.2s ease,
|
|
box-shadow 0.2s ease,
|
|
background 0.2s ease;
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 140px;
|
|
}
|
|
|
|
input:focus,
|
|
textarea:focus {
|
|
border-color: #4f8cff;
|
|
box-shadow: 0 0 0 4px rgba(79, 140, 255, 0.16);
|
|
background: #ffffff;
|
|
}
|
|
|
|
input:disabled,
|
|
textarea:disabled {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
button {
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 0.9rem 1.3rem;
|
|
color: #ffffff;
|
|
background: #2457d6;
|
|
font: inherit;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
box-shadow: 0 14px 30px rgba(36, 87, 214, 0.24);
|
|
transition:
|
|
transform 0.2s ease,
|
|
opacity 0.2s ease,
|
|
box-shadow 0.2s ease;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 18px 38px rgba(36, 87, 214, 0.3);
|
|
}
|
|
|
|
button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.5;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.hint {
|
|
margin-top: 1rem;
|
|
color: #607198;
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.error {
|
|
margin-top: 0.75rem;
|
|
color: #c1272d;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.right-panel {
|
|
display: grid;
|
|
place-items: center;
|
|
min-height: 420px;
|
|
}
|
|
|
|
.mailbox {
|
|
width: min(100%, 420px);
|
|
height: auto;
|
|
overflow: visible;
|
|
filter: drop-shadow(0 28px 40px rgba(36, 87, 214, 0.22));
|
|
}
|
|
|
|
.mailbox-visual,
|
|
.mailbox-shadow,
|
|
.mailbox-slot,
|
|
.mailbox-slot-shine,
|
|
.mailbox-slot-lip,
|
|
.mailbox-flag-arm,
|
|
.mailbox-sparks {
|
|
transform-box: fill-box;
|
|
}
|
|
|
|
.mailbox-visual {
|
|
transform-origin: center bottom;
|
|
}
|
|
|
|
.mailbox-shadow {
|
|
transform-origin: center;
|
|
}
|
|
|
|
.mailbox-slot,
|
|
.mailbox-slot-shine,
|
|
.mailbox-slot-lip {
|
|
transform-origin: center;
|
|
}
|
|
|
|
.mailbox-flag-arm {
|
|
transform-box: view-box;
|
|
transform-origin: 300px 210px;
|
|
transition: transform 0.72s cubic-bezier(0.2, 0.75, 0.2, 1.25);
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-flag-arm {
|
|
transition-delay: 0.18s;
|
|
}
|
|
|
|
.mailbox.is-flag-down .mailbox-flag-arm {
|
|
transform: rotate(92deg);
|
|
}
|
|
|
|
.mailbox-sparks {
|
|
opacity: 0;
|
|
transform-origin: center;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-visual {
|
|
animation: mailbox-receive 0.76s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-shadow {
|
|
animation: mailbox-shadow 0.76s ease both;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-slot {
|
|
animation: mailbox-slot-swallow 0.76s ease both;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-slot-shine {
|
|
animation: mailbox-slot-shine 0.76s ease both;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-slot-lip {
|
|
animation: mailbox-slot-lip 0.76s ease both;
|
|
}
|
|
|
|
.mailbox.is-receiving .mailbox-sparks {
|
|
animation: mailbox-sparks 0.76s ease both;
|
|
}
|
|
|
|
.letter {
|
|
position: absolute;
|
|
z-index: 4;
|
|
left: 0;
|
|
top: 0;
|
|
width: 180px;
|
|
height: 116px;
|
|
transform: translate3d(var(--x), var(--y), 0) scale(1);
|
|
touch-action: none;
|
|
cursor: grab;
|
|
transition:
|
|
transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
opacity 0.35s ease;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.letter.is-dragging {
|
|
cursor: grabbing;
|
|
transform: translate3d(var(--x), var(--y), 0) scale(1.04);
|
|
transition: none;
|
|
}
|
|
|
|
.letter.is-delivering {
|
|
pointer-events: none;
|
|
cursor: default;
|
|
transform: translate3d(var(--x), var(--y), 0) scale(0.62);
|
|
transition:
|
|
transform 0.52s cubic-bezier(0.65, 0, 0.2, 1),
|
|
opacity 0.35s ease;
|
|
}
|
|
|
|
.letter.is-sending {
|
|
pointer-events: none;
|
|
cursor: default;
|
|
opacity: 0;
|
|
transform: translate3d(var(--x), var(--y), 0) scale(0.16);
|
|
transition:
|
|
transform 0.28s ease,
|
|
opacity 0.28s ease;
|
|
}
|
|
|
|
.success-card {
|
|
text-align: center;
|
|
}
|
|
|
|
.success-icon {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 64px;
|
|
height: 64px;
|
|
margin: 0 auto 1rem;
|
|
border-radius: 999px;
|
|
color: #ffffff;
|
|
background: #20b26b;
|
|
font-size: 2rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.success-card h3 {
|
|
margin-bottom: 0.5rem;
|
|
color: #10255f;
|
|
font-size: 1.6rem;
|
|
}
|
|
|
|
.success-card p {
|
|
margin-bottom: 1.2rem;
|
|
color: #607198;
|
|
}
|
|
|
|
@keyframes mailbox-receive {
|
|
0% { transform: translateY(0) scale(1) rotate(0); }
|
|
18% { transform: translateY(-8px) scale(1.018, 0.982) rotate(-0.8deg); }
|
|
42% { transform: translateY(5px) scale(0.985, 1.018) rotate(0.8deg); }
|
|
64% { transform: translateY(-3px) scale(1.006, 0.994) rotate(-0.35deg); }
|
|
100% { transform: translateY(0) scale(1) rotate(0); }
|
|
}
|
|
|
|
@keyframes mailbox-shadow {
|
|
0% { transform: scale(1); opacity: 1; }
|
|
20% { transform: scale(0.86); opacity: 0.72; }
|
|
44% { transform: scale(1.08); opacity: 0.9; }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
@keyframes mailbox-slot-swallow {
|
|
0% { transform: scaleX(1) scaleY(1); filter: none; }
|
|
26% { transform: scaleX(1.16) scaleY(1.34); filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.8)); }
|
|
52% { transform: scaleX(0.92) scaleY(0.78); filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.45)); }
|
|
100% { transform: scaleX(1) scaleY(1); filter: none; }
|
|
}
|
|
|
|
@keyframes mailbox-slot-shine {
|
|
0% { opacity: 0.5; transform: scaleX(1); }
|
|
28% { opacity: 0.9; transform: scaleX(1.08); }
|
|
58% { opacity: 0.25; transform: scaleX(0.82); }
|
|
100% { opacity: 0.5; transform: scaleX(1); }
|
|
}
|
|
|
|
@keyframes mailbox-slot-lip {
|
|
0% { transform: translateY(0); opacity: 0.65; }
|
|
30% { transform: translateY(-8px); opacity: 0.95; }
|
|
58% { transform: translateY(2px); opacity: 0.5; }
|
|
100% { transform: translateY(0); opacity: 0.65; }
|
|
}
|
|
|
|
@keyframes mailbox-sparks {
|
|
0% { opacity: 0; transform: scale(0.72); }
|
|
22% { opacity: 1; transform: scale(1); }
|
|
58% { opacity: 0.8; transform: scale(1.05); }
|
|
100% { opacity: 0; transform: scale(1.12); }
|
|
}
|
|
|
|
@media (max-width: 820px) {
|
|
.contact-mailbox { grid-template-columns: 1fr; }
|
|
.right-panel { min-height: 320px; }
|
|
.mailbox { max-width: 340px; }
|
|
.floating-hint { position: relative; left: auto; top: auto; grid-column: 1; margin-bottom: 1rem; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* { transition: none !important; animation: none !important; }
|
|
}
|
|
</style> |