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.
svelte-kit-vice/web/routes/demos/mail2.svelte

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>

Powered by TurnKey Linux.