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.
1137 lines
21 KiB
1137 lines
21 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">
|
|
<svg
|
|
class={`mailbox ${mailboxReceiving ? 'is-receiving' : ''} ${flagLowered ? 'is-flag-down' : ''}`}
|
|
viewBox="0 0 420 420"
|
|
role="img"
|
|
>
|
|
<defs>
|
|
<linearGradient id="mailboxBody" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#4f8cff" />
|
|
<stop offset="100%" stop-color="#2457d6" />
|
|
</linearGradient>
|
|
|
|
<linearGradient id="mailboxFlag" x1="0" x2="1" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#ff7468" />
|
|
<stop offset="100%" stop-color="#ef3f48" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<ellipse
|
|
class="mailbox-shadow"
|
|
cx="210"
|
|
cy="354"
|
|
rx="132"
|
|
ry="24"
|
|
fill="#dce7ff"
|
|
/>
|
|
|
|
<g class="mailbox-visual">
|
|
<path
|
|
class="mailbox-shell"
|
|
d="M110 194c0-68 55-123 123-123h4c68 0 123 55 123 123v127H110Z"
|
|
fill="url(#mailboxBody)"
|
|
/>
|
|
|
|
<path
|
|
class="mailbox-roof"
|
|
d="M110 194c0-68 55-123 123-123h4c68 0 123 55 123 123H110Z"
|
|
fill="#78a9ff"
|
|
opacity="0.85"
|
|
/>
|
|
|
|
<rect
|
|
class="mailbox-front"
|
|
x="110"
|
|
y="188"
|
|
width="250"
|
|
height="133"
|
|
rx="18"
|
|
fill="#2f6de8"
|
|
/>
|
|
|
|
<rect
|
|
bind:this={mailboxSlotEl}
|
|
class="mailbox-slot"
|
|
x="142"
|
|
y="220"
|
|
width="186"
|
|
height="34"
|
|
rx="17"
|
|
fill="#10255f"
|
|
/>
|
|
|
|
<rect
|
|
class="mailbox-slot-shine"
|
|
x="156"
|
|
y="232"
|
|
width="158"
|
|
height="8"
|
|
rx="4"
|
|
fill="#020b2e"
|
|
opacity="0.5"
|
|
/>
|
|
|
|
<rect
|
|
class="mailbox-slot-lip"
|
|
x="148"
|
|
y="214"
|
|
width="174"
|
|
height="9"
|
|
rx="5"
|
|
fill="#91b7ff"
|
|
opacity="0.65"
|
|
/>
|
|
|
|
<rect
|
|
bind:this={mailboxDropZoneEl}
|
|
x="118"
|
|
y="176"
|
|
width="248"
|
|
height="132"
|
|
rx="28"
|
|
fill="transparent"
|
|
pointer-events="none"
|
|
/>
|
|
|
|
<rect x="184" y="320" width="34" height="72" rx="10" fill="#29447a" />
|
|
<rect x="246" y="320" width="34" height="72" rx="10" fill="#29447a" />
|
|
|
|
<circle cx="186" cy="238" r="6" fill="#91b7ff" />
|
|
<circle cx="302" cy="238" r="6" fill="#91b7ff" />
|
|
</g>
|
|
|
|
<g class="mailbox-flag-pole">
|
|
<rect x="274" y="126" width="16" height="154" rx="8" fill="#133a9c" />
|
|
<circle cx="282" cy="158" r="13" fill="#10255f" />
|
|
<circle cx="282" cy="158" r="7" fill="#91b7ff" />
|
|
</g>
|
|
|
|
<g class="mailbox-flag-arm">
|
|
<rect
|
|
x="282"
|
|
y="132"
|
|
width="92"
|
|
height="52"
|
|
rx="14"
|
|
fill="url(#mailboxFlag)"
|
|
/>
|
|
|
|
<rect
|
|
x="282"
|
|
y="132"
|
|
width="18"
|
|
height="52"
|
|
rx="9"
|
|
fill="#c82f3a"
|
|
opacity="0.38"
|
|
/>
|
|
</g>
|
|
|
|
<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}
|
|
>
|
|
<div class="letter-front">
|
|
<div class="letter-flap"></div>
|
|
|
|
<div class="letter-lines">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
|
|
<div class="stamp"></div>
|
|
</div>
|
|
</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: 282px 158px;
|
|
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;
|
|
filter: drop-shadow(0 22px 30px rgba(16, 37, 95, 0.24));
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.letter-front {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border: 2px solid #d7e2ff;
|
|
border-radius: 16px;
|
|
background: #fffdf8;
|
|
box-shadow: 0 18px 50px rgba(16, 37, 95, 0.18);
|
|
}
|
|
|
|
.letter-front::before,
|
|
.letter-front::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -2px;
|
|
width: 102px;
|
|
height: 102px;
|
|
border-top: 2px solid #d7e2ff;
|
|
background: #fff6e7;
|
|
}
|
|
|
|
.letter-front::before {
|
|
left: -16px;
|
|
transform: rotate(38deg);
|
|
}
|
|
|
|
.letter-front::after {
|
|
right: -16px;
|
|
transform: rotate(-38deg);
|
|
}
|
|
|
|
.letter-flap {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: -62px;
|
|
width: 126px;
|
|
height: 126px;
|
|
border-right: 2px solid #d7e2ff;
|
|
border-bottom: 2px solid #d7e2ff;
|
|
background: #fff8ec;
|
|
transform: translateX(-50%) rotate(45deg);
|
|
}
|
|
|
|
.letter-lines {
|
|
position: absolute;
|
|
left: 22px;
|
|
top: 42px;
|
|
display: grid;
|
|
gap: 8px;
|
|
width: 82px;
|
|
}
|
|
|
|
.letter-lines span {
|
|
display: block;
|
|
height: 7px;
|
|
border-radius: 999px;
|
|
background: #dce7ff;
|
|
}
|
|
|
|
.letter-lines span:nth-child(2) {
|
|
width: 72%;
|
|
}
|
|
|
|
.letter-lines span:nth-child(3) {
|
|
width: 88%;
|
|
}
|
|
|
|
.stamp {
|
|
position: absolute;
|
|
right: 22px;
|
|
top: 24px;
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 10px;
|
|
background:
|
|
linear-gradient(
|
|
45deg,
|
|
transparent 45%,
|
|
rgba(255, 255, 255, 0.7) 45% 55%,
|
|
transparent 55%
|
|
),
|
|
#ff7468;
|
|
box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.55);
|
|
}
|
|
|
|
.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> |