Meta Description" name="description" />
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Sorry β A Cute Card</title>
<style>
:root{
--bg:#fff5f7;
--card:#ffffff;
--accent:#ff6b81;
--muted:#6b6b6b;
--shadow: 0 8px 30px rgba(99, 39, 62, 0.12);
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
*{box-sizing:border-box}
body{
margin:0;
min-height:100vh;
display:grid;
place-items:center;
background:linear-gradient(180deg,var(--bg),#fffaf0);
padding:28px;
}
.card{
width:min(720px,96vw);
background:var(--card);
border-radius:18px;
padding:28px;
box-shadow:var(--shadow);
display:grid;
grid-template-columns:1fr 320px;
gap:18px;
align-items:center;
overflow:hidden;
position:relative;
}
.left{
padding:12px 8px;
}
h1{
margin:0 0 8px 0;
font-size:22px;
color: #401f2b;
letter-spacing:0.2px;
}
p.lead{
margin:0 0 14px 0;
color:var(--muted);
line-height:1.45;
}
label{font-size:13px;color:#4b2a33;display:block;margin-bottom:6px}
input{
width:100%;
padding:10px 12px;
border-radius:10px;
border:1px solid #f0d7db;
background:#fff7f9;
font-size:15px;
outline:none;
transition:transform .12s ease, box-shadow .12s;
}
input:focus{transform:translateY(-2px); box-shadow:0 6px 20px rgba(99,39,62,0.06)}
.btn{
display:inline-flex;
align-items:center;
gap:8px;
background:linear-gradient(90deg,var(--accent),#ff8aa0);
color:white;
border:none;
padding:10px 14px;
border-radius:12px;
cursor:pointer;
font-weight:600;
margin-top:12px;
box-shadow:0 8px 20px rgba(255,107,129,0.18);
}
.btn:active{transform:translateY(1px)}
.small{font-size:13px;color:#7a4a54;margin-top:8px}
.right{
background:linear-gradient(135deg, rgba(255,107,129,0.06), rgba(255,138,160,0.03));
border-radius:12px;
padding:14px;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:10px;
min-height:220px;
text-align:center;
}
.card-visual{
width:120px;
height:120px;
border-radius:16px;
display:grid;
place-items:center;
background:linear-gradient(180deg,#fff0f2,#fff6f7);
border:1px solid rgba(255,107,129,0.08);
}
.heart{
width:72px;height:72px;
display:inline-block;
transform-origin:center;
transition:transform .35s;
}
.heart svg{width:100%;height:100%}
.heart.beat{animation:beat .8s ease 3}
@keyframes beat {
0%{transform:scale(1)}
25%{transform:scale(1.15)}
50%{transform:scale(0.95)}
75%{transform:scale(1.07)}
100%{transform:scale(1)}
}
.message{
font-size:16px;
color:#4b2a33;
min-height:64px;
display:flex;
align-items:center;
justify-content:center;
padding:10px;
}
/* confetti canvas covers card */
canvas.confetti{
position:absolute;
inset:0;
pointer-events:none;
}
@media (max-width:720px){
.card{grid-template-columns:1fr; padding:18px}
.right{order:-1}
}
</style>
</head>
<body>
<div class="card" role="application" aria-label="Cute sorry card">
<canvas class="confetti"></canvas>
<div class="left">
<h1>Make it small. Make it true.</h1>
<p class="lead">Type names and press the button β the card will show a short, heartfelt apology that sounds gentle and genuine.</p>
<label for="yourName">Your name</label>
<input id="yourName" placeholder="e.g. Aashu" autocomplete="off">
<label for="exName" style="margin-top:10px">Their name</label>
<input id="exName" placeholder="e.g. Meera" autocomplete="off">
<div style="display:flex;gap:10px;align-items:center;">
<button class="btn" id="sayBtn" aria-live="polite">π Say Sorry</button>
<button class="btn" id="resetBtn" style="background:#f3f3f4;color:#401f2b;box-shadow:none">Reset</button>
</div>
<div class="small">Tip: keep it short, honest, and avoid blame. This little card nudges the rest.</div>
</div>
<div class="right">
<div class="card-visual" aria-hidden="true">
<div class="heart" id="heart">
<!-- cute heart svg -->
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 21s-7.5-4.7-9.3-7.1C-1 8.6 5.2 4 7.5 6.1 9 7.6 12 10.8 12 10.8s3-3.2 4.5-4.7C18.8 4 25 8.6 21.3 13.9 19.5 16.3 12 21 12 21z" fill="#ff6b81" opacity="0.95"/>
</svg>
</div>
</div>
<div class="message" id="message">A gentle space for a small, honest sorry β ready?</div>
</div>
</div>
<script>
/* Friendly, tiny apology card logic.
No external libs. Works in modern browsers.
*/
const btn = document.getElementById('sayBtn');
const reset = document.getElementById('resetBtn');
const your = document.getElementById('yourName');
const ex = document.getElementById('exName');
const msg = document.getElementById('message');
const heart = document.getElementById('heart');
const canvas = document.querySelector('canvas.confetti');
const ctx = canvas.getContext('2d');
function resizeCanvas(){
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
/* small confetti engine */
let confettiPieces = [];
function spawnConfetti(){
confettiPieces = [];
const w = canvas.width, h = canvas.height;
for(let i=0;i<64;i++){
confettiPieces.push({
x: Math.random()*w,
y: -Math.random()*h,
r: Math.random()*6+4,
vx: (Math.random()-0.5)*2.6,
vy: Math.random()*3+2,
rot: Math.random()*360,
vr: (Math.random()-0.5)*6,
color: `hsl(${Math.random()*30 + 340},80%,60%)`
});
}
stepConfetti();
}
let confAni;
function stepConfetti(){
cancelAnimationFrame(confAni);
ctx.clearRect(0,0,canvas.width, canvas.height);
for(let p of confettiPieces){
p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.vy += 0.03;
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.rot*Math.PI/180);
ctx.fillStyle = p.color;
ctx.fillRect(-p.r/2, -p.r/2, p.r*1.6, p.r);
ctx.restore();
}
confettiPieces = confettiPieces.filter(p => p.y < canvas.height + 50);
if(confettiPieces.length) confAni = requestAnimationFrame(stepConfetti);
}
/* small beep using WebAudio for feedback */
function tinyBeep(){
try{
const a = new (window.AudioContext || window.webkitAudioContext)();
const o = a.createOscillator();
const g = a.createGain();
o.type = 'sine';
o.frequency.value = 880;
g.gain.value = 0.0001;
o.connect(g); g.connect(a.destination);
o.start();
// soft attack
g.gain.exponentialRampToValueAtTime(0.03, a.currentTime + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + 0.35);
o.stop(a.currentTime + 0.36);
setTimeout(()=>a.close(),500);
}catch(e){ /* ignore on unsupported */ }
}
/* apology templates β short & plain, keep respectful */
const templates = [
(s,t)=>`Hey ${t || 'there'}, I'm sorry. I messed up and I wish I had handled things better. β ${s}`,
(s,t)=>`${t || 'Friend'}, I'm sorry. I miss the easy parts of us. I take responsibility and I'm really sorry. β ${s}`,
(s,t)=>`I owe you a sorry, ${t || ''}. Not excuses β just truth: I'm sorry. β ${s}`,
(s,t)=>`Sorry, ${t || ''}. I should've listened better. If you can, I'd like to make things right. β ${s}`,
(s,t)=>`Small sorry, big meaning: I'm sorry. Thank you for the times; I'm sorry I hurt you. β ${s}`
];
function pickTemplate(){
return templates[Math.floor(Math.random()*templates.length)];
}
btn.addEventListener('click', () => {
const s = (your.value || 'Someone').trim();
const t = (ex.value || '').trim();
const apology = pickTemplate()(s,t);
// update visuals
msg.textContent = apology;
heart.classList.remove('beat');
// reflow to restart animation
void heart.offsetWidth;
heart.classList.add('beat');
spawnConfetti();
tinyBeep();
// subtle polite aria notification
btn.setAttribute('aria-label', 'Apology shown');
});
/* reset */
reset.addEventListener('click', () => {
your.value = '';
ex.value = '';
msg.textContent = 'A gentle space for a small, honest sorry β ready?';
cancelAnimationFrame(confAni);
ctx.clearRect(0,0,canvas.width,canvas.height);
heart.classList.remove('beat');
btn.setAttribute('aria-label', 'Say Sorry');
});
/* a small UX nudge: press Enter in exName to activate */
ex.addEventListener('keydown', (e)=>{ if(e.key === 'Enter') btn.click(); });
your.addEventListener('keydown', (e)=>{ if(e.key === 'Enter') ex.focus(); });
/* accessibility: focus first input */
your.focus();
</script>
</body>
</html>1
1
9KB
9KB
285.0ms
400.0ms
287.0ms