<!DOCTYPE html>
<html lang="bn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#07060f">
<title>রাগের ঘর – নিরাপদ ভার্চুয়াল পরিবেশে রাগ প্রকাশ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Bengali:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root{
--bg:#07060f; --ink:#f5f2ff; --mut:#bdb7dc; --line:rgba(255,255,255,.15);
--glass:rgba(255,255,255,.07); --pink:#ff4d8d; --vio:#8b5cff; --cyan:#35e0ff;
--lime:#9dff6b; --gold:#ffd23f; --r:22px;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
[hidden]{display:none!important}
html,body{margin:0;min-height:100%;color:var(--ink);overflow-x:hidden;
font-family:'Noto Sans Bengali','Hind Siliguri','Kalpurush','Siyam Rupali',system-ui,-apple-system,'Segoe UI',sans-serif;line-height:1.6}
body{background:
radial-gradient(1100px 700px at 12% -10%,#2f1566 0,transparent 60%),
radial-gradient(900px 600px at 100% 0,#5e1140 0,transparent 55%),
radial-gradient(900px 700px at 50% 120%,#0b3d55 0,transparent 60%),#07060f;
background-attachment:fixed}
h1,h2,h3,p{margin:0}
button{font:inherit;color:inherit}
:focus-visible{outline:3px solid var(--cyan);outline-offset:3px;border-radius:12px}
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
/* ---------- glass / buttons ---------- */
.glass{background:linear-gradient(145deg,rgba(255,255,255,.11),rgba(255,255,255,.04));border:1px solid var(--line);
-webkit-backdrop-filter:blur(16px) saturate(140%);backdrop-filter:blur(16px) saturate(140%);border-radius:var(--r);
box-shadow:0 20px 50px rgba(0,0,0,.45),inset 0 1px 0 rgba(255,255,255,.16)}
.btn{font-weight:700;border:0;border-radius:999px;padding:.8em 1.6em;min-height:48px;min-width:48px;cursor:pointer;color:#fff;
display:inline-flex;align-items:center;justify-content:center;gap:.5em;text-align:center;line-height:1.4;
transition:transform .15s,box-shadow .2s,filter .2s,background .2s}
.btn-primary{background:linear-gradient(135deg,#ff4d8d,#8b5cff);
box-shadow:0 6px 0 #4a1f9e,0 14px 34px rgba(255,77,141,.45),inset 0 1px 0 rgba(255,255,255,.4)}
.btn-primary:hover{transform:translateY(-2px);filter:brightness(1.1)}
.btn-primary:active{transform:translateY(4px);box-shadow:0 2px 0 #4a1f9e,0 6px 16px rgba(255,77,141,.4)}
.btn-ghost{background:var(--glass);border:1px solid var(--line);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}
.btn-ghost:hover{background:rgba(255,255,255,.16);transform:translateY(-1px)}
.btn-ghost:active{transform:scale(.97)}
.btn-danger{border-color:rgba(255,92,122,.6);color:#ffc2cf}
.btn-xl{font-size:1.15rem;padding:.9em 2em;min-height:58px}
.btn-sm{min-height:44px;padding:.4em 1em;font-size:.92rem}
.muted{color:var(--mut)}
/* ---------- screens ---------- */
.screen{display:none;position:relative;min-height:calc(100dvh - 44px)}
.screen.active{display:flex;animation:screenIn .5s ease both}
@keyframes screenIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.center{align-items:center;justify-content:center;flex-direction:column;padding:20px;text-align:center}
/* ---------- top bar / language ---------- */
#topbar{display:flex;justify-content:flex-end;align-items:center;padding:4px 10px;min-height:44px;position:relative;z-index:50}
.lang{display:inline-flex;padding:3px;border-radius:999px;background:rgba(255,255,255,.08);border:1px solid var(--line);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}
.lang-btn{border:0;background:transparent;color:var(--mut);font-weight:700;border-radius:999px;padding:0 14px;min-height:36px;min-width:48px;cursor:pointer;transition:background .2s,color .2s,box-shadow .2s}
.lang-btn:hover{color:#fff}
.lang-btn[aria-pressed="true"]{background:linear-gradient(135deg,#ff4d8d,#8b5cff);color:#fff;box-shadow:0 0 16px rgba(139,92,255,.6)}
.credit-mini{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:6px 12px;padding:6px 8px 6px 16px;border-radius:999px;
background:rgba(255,255,255,.06);border:1px solid var(--line);font-size:.82rem;color:var(--mut);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}
.credit-mini strong{font-weight:800;background:linear-gradient(120deg,#ffb4d4,#a98bff,#7fe9ff);-webkit-background-clip:text;background-clip:text;color:transparent}
.fb-mini{display:inline-flex;align-items:center;gap:6px;min-height:40px;padding:0 14px;border-radius:999px;background:#1877f2;color:#fff;font-weight:700;font-size:.82rem;text-decoration:none;
box-shadow:0 0 14px rgba(24,119,242,.55);transition:transform .15s,filter .2s}
.fb-mini:hover{transform:translateY(-1px);filter:brightness(1.1)}
/* ---------- landing ---------- */
.landing{flex-direction:column;align-items:center;justify-content:space-between;gap:18px;padding:8px 16px 14px;overflow:hidden}
.orb{position:absolute;border-radius:50%;filter:blur(70px);opacity:.55;pointer-events:none;animation:orb 14s ease-in-out infinite alternate}
.orb.o1{width:340px;height:340px;background:#8b5cff;left:-80px;top:-60px}
.orb.o2{width:300px;height:300px;background:#ff4d8d;right:-70px;top:30%;animation-delay:-5s}
.orb.o3{width:360px;height:360px;background:#17b6d6;left:20%;bottom:-140px;animation-delay:-9s}
@keyframes orb{to{transform:translate(40px,-30px) scale(1.15)}}
.floaters{position:absolute;inset:0;pointer-events:none;z-index:0}
.floater{position:absolute;width:var(--s);height:var(--s);animation:bob 6s ease-in-out infinite;animation-delay:var(--d);
filter:drop-shadow(0 18px 14px rgba(0,0,0,.5)) drop-shadow(0 0 18px rgba(139,92,255,.5));opacity:.9}
@keyframes bob{0%,100%{transform:translateY(0) rotate(var(--r0))}50%{transform:translateY(-22px) rotate(calc(var(--r0) + 12deg))}}
.landing-inner{position:relative;z-index:2;width:100%;max-width:640px;display:flex;flex-direction:column;gap:16px;align-items:center;flex:1 1 auto;justify-content:center}
.hero{padding:34px 22px 26px;text-align:center;width:100%;transition:transform .2s ease-out;
box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 80px rgba(139,92,255,.25),inset 0 1px 0 rgba(255,255,255,.2)}
.hero h1{font-size:clamp(2.1rem,8.5vw,3.7rem);font-weight:900;line-height:1.45;
background:linear-gradient(120deg,#fff 0%,#ffb4d4 35%,#a98bff 70%,#7fe9ff 100%);-webkit-background-clip:text;background-clip:text;color:transparent;
filter:drop-shadow(0 0 22px rgba(255,77,141,.35))}
.hero .sub{margin:10px 0 26px;font-size:clamp(1rem,3.6vw,1.2rem);color:var(--mut)}
.disclaimer{margin-top:20px;font-size:.88rem;color:#ffd9a8;background:rgba(255,210,63,.08);border:1px solid rgba(255,210,63,.25);
border-radius:14px;padding:8px 14px}
.privacy-strip{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;width:100%;flex-wrap:wrap}
.privacy-strip p{font-size:.9rem;color:var(--mut);flex:1 1 220px}
/* ---------- upload ---------- */
.upload{align-items:center;justify-content:center;padding:18px 14px}
.panel{width:100%;max-width:580px;padding:22px 18px;display:flex;flex-direction:column;gap:14px;text-align:center;align-items:center}
.panel h2{font-size:clamp(1.4rem,5.5vw,1.9rem);font-weight:800;line-height:1.5}
.back{align-self:flex-start}
.dropzone{width:100%;border:2px dashed rgba(255,255,255,.35);border-radius:20px;padding:26px 14px;display:flex;flex-direction:column;align-items:center;gap:10px;
background:rgba(255,255,255,.04);transition:border-color .2s,background .2s,box-shadow .2s;cursor:pointer}
.dropzone:hover,.dropzone.dragover{border-color:var(--cyan);background:rgba(53,224,255,.08);box-shadow:0 0 34px rgba(53,224,255,.25)}
.dropzone.shake{animation:dzShake .45s}
@keyframes dzShake{20%,60%{transform:translateX(-8px)}40%,80%{transform:translateX(8px)}}
.dz-ico{width:64px;height:64px}
.dz-title{font-weight:700;font-size:1.1rem}
.dz-note{font-size:.82rem;color:var(--mut)}
.preview{width:100%;display:flex;flex-direction:column;align-items:center;gap:12px}
.preview-frame{padding:8px;border-radius:18px;background:linear-gradient(145deg,#6d4cff,#ff4d8d 55%,#ffd23f);box-shadow:0 0 36px rgba(139,92,255,.5)}
.preview-frame img{display:block;max-width:100%;max-height:300px;border-radius:10px;background:#111}
.row{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;width:100%}
/* ---------- room ---------- */
.room{flex-direction:column;height:calc(100dvh - 44px);min-height:0;padding:8px;gap:8px;overflow:hidden}
.room-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 10px;flex:0 0 auto}
.room-title{font-size:1.05rem;font-weight:800}
.head-actions{display:flex;gap:6px}
.stage{position:relative;flex:1 1 auto;min-height:0;border-radius:var(--r);overflow:hidden;display:flex;align-items:center;justify-content:center;
border:1px solid var(--line);box-shadow:inset 0 0 90px rgba(0,0,0,.65),0 20px 50px rgba(0,0,0,.5);perspective:1100px;background:#120b27}
.wall{position:absolute;inset:0 0 34% 0;
background:repeating-linear-gradient(90deg,rgba(255,255,255,.035) 0 2px,transparent 2px 48px),linear-gradient(180deg,#150e2e,#2a1a52 75%,#35216a)}
.wall::after{content:"";position:absolute;left:0;right:0;bottom:0;height:10px;background:linear-gradient(180deg,#3d2a78,#20153f);box-shadow:0 -2px 14px rgba(139,92,255,.5)}
.neon{position:absolute;top:10px;left:10%;right:10%;height:4px;border-radius:4px;background:linear-gradient(90deg,transparent,var(--pink),var(--cyan),transparent);
box-shadow:0 0 18px var(--pink),0 0 30px var(--cyan);opacity:.8}
.floor{position:absolute;left:0;right:0;bottom:0;height:34%;overflow:hidden;background:linear-gradient(180deg,#1b1233,#0a0716)}
.floor::before{content:"";position:absolute;left:-60%;right:-60%;top:0;height:320%;transform-origin:50% 0;transform:perspective(240px) rotateX(58deg);
background:repeating-linear-gradient(90deg,rgba(139,92,255,.4) 0 1px,transparent 1px 64px),repeating-linear-gradient(0deg,rgba(53,224,255,.25) 0 1px,transparent 1px 44px)}
.light{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse 52% 70% at 50% 0,rgba(255,236,200,.26),transparent 72%)}
.motes{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.mote{position:absolute;bottom:-10px;border-radius:50%;background:radial-gradient(circle,#fff,rgba(139,92,255,.0) 70%);opacity:.0;animation:mote linear infinite}
@keyframes mote{0%{transform:translateY(0);opacity:0}15%{opacity:.7}100%{transform:translateY(-110vh) translateX(30px);opacity:0}}
.stage-obj{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;max-width:94%;transform:rotateX(2deg)}
.frame{position:relative;padding:10px;border-radius:18px;cursor:pointer;touch-action:manipulation;
background:linear-gradient(145deg,#6d4cff,#ff4d8d 55%,#ffd23f);
box-shadow:0 0 0 3px rgba(255,255,255,.15),0 0 40px rgba(139,92,255,.55),0 26px 40px rgba(0,0,0,.6);transition:box-shadow .2s}
.frame.armed{box-shadow:0 0 0 3px var(--cyan),0 0 46px rgba(53,224,255,.7),0 26px 40px rgba(0,0,0,.6)}
.frame.drop-ready{box-shadow:0 0 0 4px var(--lime),0 0 70px rgba(157,255,107,.85),0 26px 40px rgba(0,0,0,.6)}
.tag{position:absolute;top:-14px;left:50%;transform:translateX(-50%);z-index:3;white-space:nowrap;font-size:.78rem;font-weight:700;
background:rgba(10,6,24,.9);border:1px solid var(--cyan);color:var(--cyan);padding:1px 14px;border-radius:999px;box-shadow:0 0 14px rgba(53,224,255,.5)}
.photo-clip{position:relative;border-radius:10px;overflow:hidden;line-height:0;background:#111}
.photo-clip img{display:block;width:auto;height:auto;max-width:min(calc(100vw - 60px),560px);max-height:clamp(150px,calc(100dvh - 475px),560px);
object-fit:contain;user-select:none;-webkit-user-drag:none;pointer-events:none}
.photo-fx{position:absolute;inset:0;overflow:hidden;pointer-events:none}
.ov{position:absolute;transform:translate(-50%,-50%);pointer-events:none}
.ov-in{width:100%;height:100%}
.ov-in svg{width:100%;height:100%;overflow:visible}
.ov-in .stk{filter:drop-shadow(2px 0 0 #fff) drop-shadow(-2px 0 0 #fff) drop-shadow(0 2px 0 #fff) drop-shadow(0 -2px 0 #fff) drop-shadow(0 6px 8px rgba(0,0,0,.45))}
.wash{position:absolute;inset:0;pointer-events:none}
.wdrop{position:absolute;width:9px;height:13px;border-radius:50% 50% 55% 55%;background:radial-gradient(circle at 35% 30%,#fff,#6ccbff 55%,#2b86e6);opacity:0}
.wring{position:absolute;width:40px;height:40px;border-radius:50%;border:3px solid rgba(190,235,255,.9);transform:translate(-50%,-50%)}
.floor-shadow{width:76%;height:18px;margin-top:12px;border-radius:50%;background:radial-gradient(ellipse,rgba(0,0,0,.7),transparent 70%)}
.hint{position:absolute;left:8px;right:8px;bottom:8px;z-index:4;text-align:center;font-size:.85rem;font-weight:500;
background:rgba(10,6,24,.7);border:1px solid var(--line);border-radius:14px;padding:5px 10px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.meter-panel{flex:0 0 auto;padding:8px 14px 10px}
.panel-title{font-size:.9rem;font-weight:700;color:var(--mut)}
.meter-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
.meter-num{font-size:1.9rem;font-weight:900;line-height:1.35;text-shadow:0 0 20px currentColor}
.status{font-weight:700;font-size:.95rem;text-align:right}
.bar{height:14px;border-radius:99px;background:rgba(255,255,255,.1);overflow:hidden;border:1px solid var(--line);margin-top:2px}
.bar-fill{height:100%;width:100%;border-radius:99px;transition:width .6s cubic-bezier(.3,.8,.3,1),background .6s;box-shadow:0 0 14px rgba(255,255,255,.4)}
.combo-badge{margin-top:8px;display:inline-block;padding:2px 14px;border-radius:99px;font-weight:800;font-size:.92rem;
background:linear-gradient(135deg,#ffd23f,#ff7a45);color:#2a1200;box-shadow:0 0 20px rgba(255,210,63,.7)}
.only-desk{display:none}
.items-panel{flex:0 0 auto;padding:8px}
.items-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.item{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:66px;padding:4px 2px;
border-radius:16px;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;
background:linear-gradient(160deg,rgba(255,255,255,.14),rgba(255,255,255,.04));border:1px solid var(--line);
box-shadow:0 4px 0 rgba(0,0,0,.4),0 10px 18px rgba(0,0,0,.3);transition:transform .15s,box-shadow .2s,border-color .2s,background .2s}
.item:hover{transform:translateY(-3px);border-color:rgba(255,255,255,.4);box-shadow:0 7px 0 rgba(0,0,0,.4),0 14px 24px rgba(139,92,255,.35)}
.item:active{transform:scale(.93)}
.item.selected{border-color:var(--cyan);background:linear-gradient(160deg,rgba(53,224,255,.28),rgba(139,92,255,.2));
box-shadow:0 0 0 2px var(--cyan),0 0 26px rgba(53,224,255,.6),0 6px 0 rgba(0,0,0,.4)}
.item.dragging{opacity:.45}
.item-ico{width:38px;height:38px;display:block;pointer-events:none}
.item-ico svg,.floater svg,.fly svg,.ghost svg{width:100%;height:100%;display:block;overflow:visible}
.item-name{font-size:.68rem;font-weight:700;line-height:1.25;text-align:center;pointer-events:none}
@media (min-width:960px){
.room{padding:14px;gap:14px}
.room.active{display:grid;grid-template-columns:300px 1fr 320px;grid-template-rows:auto minmax(0,1fr)}
.room-head{grid-column:1/-1;padding:8px 16px}
.room-title{font-size:1.25rem}
.stage{grid-column:2;grid-row:2}
.items-panel{grid-column:1;grid-row:2;padding:14px;overflow:auto}
.meter-panel{grid-column:3;grid-row:2;padding:20px;align-self:stretch;display:flex;flex-direction:column;gap:10px}
.items-panel .panel-title{display:block;margin-bottom:10px}
.items-grid{grid-template-columns:repeat(2,1fr);gap:12px}
.item{min-height:104px;gap:6px}
.item-ico{width:60px;height:60px}
.item-name{font-size:.9rem}
.meter-row{flex-direction:column;align-items:flex-start;gap:0}
.meter-num{font-size:4.6rem;line-height:1.3}
.status{text-align:left;font-size:1.15rem}
.bar{height:22px}
.only-desk{display:inline-flex}
.photo-clip img{max-width:min(calc(100vw - 800px),560px);max-height:calc(100dvh - 255px)}
}
@media (max-width:959px){.items-panel .panel-title{display:none}}
@media (max-height:620px) and (max-width:959px){.photo-clip img{max-height:clamp(110px,calc(100dvh - 425px),400px)}.item{min-height:58px}.item-ico{width:30px;height:30px}}
/* ---------- usage tally ---------- */
.cnt{position:absolute;top:-6px;right:-4px;min-width:22px;height:22px;padding:0 6px;border-radius:99px;background:linear-gradient(135deg,#ffd23f,#ff7a45);color:#2a1200;
font-weight:800;font-size:.72rem;line-height:22px;text-align:center;box-shadow:0 0 12px rgba(255,170,60,.7);pointer-events:none}
.cnt:empty{display:none}
.desk-block{display:none}
.tally-list{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
.tally-list li{display:grid;grid-template-columns:1fr auto;gap:2px 8px;font-size:.92rem;font-weight:600}
.tally-list .mini{grid-column:1/-1;height:6px;border-radius:99px;background:rgba(255,255,255,.1);overflow:hidden}
.tally-list .mini i{display:block;height:100%;border-radius:99px;transition:width .4s}
.tally-total{margin-top:10px;font-weight:800}
.tally-empty{color:var(--mut);font-size:.85rem}
@media (min-width:960px){.desk-block{display:block}.tally{flex:1 1 auto;min-height:0;overflow:auto}}
/* ---------- breathing / reflection / final ---------- */
.breath-wrap{position:relative;width:min(78vw,380px);aspect-ratio:1;display:grid;place-items:center;margin:18px 0}
.breath-circle{position:absolute;width:74%;height:74%;border-radius:50%;transform:scale(.62);will-change:transform;
background:radial-gradient(circle at 35% 30%,#bff6ff,#35a8ff 45%,#6a3cff);
box-shadow:0 0 70px rgba(53,224,255,.6),inset 0 -22px 44px rgba(0,0,0,.28)}
.ring{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
.ring circle{fill:none;stroke-width:4}
.ring .bgc{stroke:rgba(255,255,255,.14)}
.ring .pg{stroke:var(--cyan);stroke-linecap:round;stroke-dasharray:590.6;stroke-dashoffset:590.6;filter:drop-shadow(0 0 6px var(--cyan))}
.breath-text{position:relative;z-index:2;text-shadow:0 2px 12px rgba(0,0,0,.6)}
.breath-text .ph{font-size:clamp(1.5rem,6vw,2.1rem);font-weight:800;line-height:1.5}
.breath-text .left{font-size:.95rem;color:#e2f6ff}
.done-msg{font-size:1.3rem;font-weight:800;color:#c9ffe0;margin:6px 0 14px}
.reflect{align-items:center;justify-content:center;padding:20px 14px}
.reflect .panel{max-width:640px;text-align:left;align-items:stretch}
.reflect h2{text-align:center}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-weight:700}
textarea{width:100%;min-height:96px;resize:vertical;font:inherit;color:var(--ink);padding:12px 14px;border-radius:16px;line-height:1.7;
background:rgba(8,5,20,.6);border:1px solid var(--line)}
textarea::placeholder{color:#8f89b4}
textarea:focus{border-color:var(--cyan);outline:none;box-shadow:0 0 0 3px rgba(53,224,255,.3)}
.final h1{font-size:clamp(2rem,8vw,3.4rem);font-weight:900;line-height:1.5;
background:linear-gradient(120deg,#c9ffe0,#7fe9ff,#a98bff);-webkit-background-clip:text;background-clip:text;color:transparent}
.final .sub{font-size:1.1rem;color:var(--mut);max-width:30ch;margin:6px auto 20px}
.final .btns{display:flex;flex-direction:column;gap:12px;width:min(100%,360px)}
.final .note{margin-top:22px;font-size:.9rem;color:#ffd9a8}
.calm-orb{width:96px;height:96px;margin:0 auto 6px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#d8fff0,#4fe3b0 50%,#2a8fd8);box-shadow:0 0 50px rgba(79,227,176,.6)}
.credit{margin-top:26px;padding:14px 22px;display:flex;flex-direction:column;align-items:center;gap:10px;border-radius:20px;
box-shadow:0 0 40px rgba(139,92,255,.25),inset 0 1px 0 rgba(255,255,255,.16)}
.credit p{font-size:1rem;color:var(--mut)}
.credit strong{color:#fff;background:linear-gradient(120deg,#ffb4d4,#a98bff,#7fe9ff);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:800}
.fb-btn{background:linear-gradient(135deg,#1877f2,#0b4fc4);text-decoration:none;box-shadow:0 5px 0 #08327a,0 12px 28px rgba(24,119,242,.45)}
.fb-btn:hover{transform:translateY(-2px);filter:brightness(1.1)}
.fb-btn:active{transform:translateY(3px);box-shadow:0 2px 0 #08327a}
/* ---------- modal / toast ---------- */
.modal-back{position:fixed;inset:0;z-index:600;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(5,3,14,.72);
-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.modal-back.open{display:flex;animation:fade .25s}
@keyframes fade{from{opacity:0}to{opacity:1}}
.modal{width:100%;max-width:480px;max-height:92dvh;overflow:auto;padding:24px 20px;text-align:center;display:flex;flex-direction:column;gap:14px;align-items:stretch;
background:linear-gradient(145deg,rgba(40,26,80,.95),rgba(18,11,40,.95));animation:pop .35s cubic-bezier(.3,1.4,.5,1)}
@keyframes pop{from{transform:scale(.88);opacity:0}to{transform:none;opacity:1}}
.modal h2{font-size:1.45rem;font-weight:800;line-height:1.5}
.modal ul{list-style:none;margin:0;padding:0;text-align:left;display:flex;flex-direction:column;gap:10px}
.modal li{padding:10px 14px;border-radius:14px;background:rgba(255,255,255,.06);border:1px solid var(--line)}
.switch-row{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:52px;padding:6px 14px;border-radius:14px;background:rgba(255,255,255,.06);
border:1px solid var(--line);text-align:left;font-weight:600;cursor:pointer}
.switch{appearance:none;-webkit-appearance:none;width:52px;height:30px;border-radius:99px;background:rgba(255,255,255,.22);position:relative;cursor:pointer;flex:0 0 auto;transition:background .2s;margin:0}
.switch::after{content:"";position:absolute;top:3px;left:3px;width:24px;height:24px;border-radius:50%;background:#fff;transition:transform .2s;box-shadow:0 2px 6px rgba(0,0,0,.4)}
.switch:checked{background:linear-gradient(135deg,#35e0ff,#8b5cff)}
.switch:checked::after{transform:translateX(22px)}
#toasts{position:fixed;left:0;right:0;top:calc(10px + env(safe-area-inset-top));z-index:900;display:flex;flex-direction:column;align-items:center;gap:8px;padding:0 12px;pointer-events:none}
.toast{pointer-events:auto;max-width:520px;padding:12px 18px;border-radius:16px;font-weight:600;text-align:center;
-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:rgba(22,13,46,.94);border:1px solid var(--line);
box-shadow:0 14px 40px rgba(0,0,0,.5);animation:toastIn .3s cubic-bezier(.3,1.4,.5,1)}
.toast.error{border-color:#ff5c7a;box-shadow:0 0 28px rgba(255,92,122,.4)}
.toast.success{border-color:#5dffb0;box-shadow:0 0 28px rgba(93,255,176,.35)}
.toast.info{border-color:var(--cyan)}
.toast.out{opacity:0;transform:translateY(-10px);transition:all .3s}
@keyframes toastIn{from{opacity:0;transform:translateY(-16px) scale(.94)}to{opacity:1;transform:none}}
/* ---------- fx layer ---------- */
#fxLayer{position:fixed;inset:0;z-index:300;pointer-events:none;overflow:hidden}
.fly{position:fixed;left:0;top:0;width:88px;height:88px;pointer-events:none;will-change:transform;filter:drop-shadow(0 14px 8px rgba(0,0,0,.45))}
.ghost{position:fixed;left:0;top:0;width:84px;height:84px;z-index:450;pointer-events:none;filter:drop-shadow(0 16px 10px rgba(0,0,0,.5))}
.pt{position:fixed;left:0;top:0;pointer-events:none;border-radius:50%;will-change:transform,opacity}
.pt-dust{background:radial-gradient(circle,#efe3cd,#9c8b73 75%);opacity:.9}
.pt-star{border-radius:0;background:linear-gradient(135deg,#fff7b0,#ffd23f);clip-path:polygon(50% 0,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%)}
.pt-line{border-radius:3px;background:#fff;box-shadow:0 0 8px #ffd23f}
.pt-paint{background:#ff4d8d}
.pt-water{background:radial-gradient(circle at 30% 30%,#fff,#68cbff 55%,#1e7fe0)}
.pt-slime{background:radial-gradient(circle at 30% 30%,#e3ffb0,#6be04a 55%,#2fa832)}
.pt-smoke{background:radial-gradient(circle,rgba(235,235,245,.85),rgba(160,160,180,0) 70%);filter:blur(2px)}
.pt-fluff{background:radial-gradient(circle,#fff,#ffd6e6 70%)}
.pt-brown{background:radial-gradient(circle at 30% 30%,#b98650,#5a3416)}
.mline{position:fixed;left:0;top:0;height:4px;border-radius:4px;background:linear-gradient(90deg,transparent,#fff);pointer-events:none}
.pop-burst{position:fixed;left:0;top:0;width:180px;height:180px;pointer-events:none;filter:drop-shadow(0 6px 0 rgba(0,0,0,.35))}
.pop-burst svg{width:100%;height:100%;display:block}
.pop-word{position:fixed;left:0;top:0;pointer-events:none;white-space:nowrap;font-weight:900;line-height:1.4;font-size:clamp(2rem,9vw,3.3rem);
color:var(--c);-webkit-text-stroke:7px #1a0f2e;paint-order:stroke fill;text-shadow:0 6px 0 #1a0f2e,0 0 26px var(--c)}
.pop-cut{position:fixed;left:0;top:0;pointer-events:none;font-weight:900;font-size:1.3rem;color:#9dff6b;text-shadow:0 2px 0 #0b2a05,0 0 14px #9dff6b;white-space:nowrap}
.ring-fx{position:fixed;left:0;top:0;width:24px;height:24px;border-radius:50%;pointer-events:none;border:5px solid var(--c);box-shadow:0 0 22px var(--c)}
.combo-pop{position:fixed;left:50%;top:0;pointer-events:none;white-space:nowrap;font-weight:900;font-size:clamp(1.6rem,7vw,2.6rem);color:#fff4b0;
-webkit-text-stroke:6px #3a1200;paint-order:stroke fill;text-shadow:0 5px 0 #3a1200,0 0 36px #ffb02e,0 0 70px #ff6a2e}
/* ---------- reduced motion ---------- */
html.reduce *,html.reduce *::before,html.reduce *::after{animation:none!important;transition-duration:.01ms!important}
html.reduce .floater,html.reduce .orb,html.reduce .mote{animation:none!important}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation:none!important;transition-duration:.01ms!important}
}
</style>
</head>
<body>
<!-- shared SVG gradients -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="gWood" x1="0" x2="1"><stop offset="0" stop-color="#eebb82"/><stop offset="1" stop-color="#8a5a2b"/></linearGradient>
<linearGradient id="gStraw" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9de80"/><stop offset="1" stop-color="#c58a22"/></linearGradient>
<linearGradient id="gShoe" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff8585"/><stop offset="1" stop-color="#c0212f"/></linearGradient>
<linearGradient id="gSole" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#cfd4e0"/></linearGradient>
<radialGradient id="gEgg" cx=".35" cy=".3" r=".85"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#f0dcba"/></radialGradient>
<radialGradient id="gYolk" cx=".35" cy=".3" r=".8"><stop offset="0" stop-color="#ffe874"/><stop offset="1" stop-color="#ff9d00"/></radialGradient>
<linearGradient id="gPaint" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#78bcff"/><stop offset="1" stop-color="#2b5fd0"/></linearGradient>
<linearGradient id="gWater" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#b3e9ff"/><stop offset="1" stop-color="#2f8fe0"/></linearGradient>
<linearGradient id="gMetal" x1="0" x2="1"><stop offset="0" stop-color="#f5f8fc"/><stop offset=".45" stop-color="#aab5c3"/><stop offset=".72" stop-color="#eef2f7"/><stop offset="1" stop-color="#7f8b9a"/></linearGradient>
<radialGradient id="gPillow" cx=".35" cy=".3" r=".9"><stop offset="0" stop-color="#fff6fa"/><stop offset="1" stop-color="#ffaccb"/></radialGradient>
<radialGradient id="gSlime" cx=".35" cy=".3" r=".9"><stop offset="0" stop-color="#d2ff85"/><stop offset="1" stop-color="#36b83a"/></radialGradient>
<radialGradient id="gPoop" cx=".35" cy=".3" r=".9"><stop offset="0" stop-color="#c08a52"/><stop offset="1" stop-color="#5a3416"/></radialGradient>
<radialGradient id="gDent" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#14142c" stop-opacity=".6"/><stop offset=".7" stop-color="#14142c" stop-opacity=".25"/><stop offset="1" stop-color="#14142c" stop-opacity="0"/></radialGradient>
</defs>
</svg>
<div id="topbar"><div class="lang" role="group" aria-label="ভাষা">
<button type="button" class="lang-btn" data-lang="bn" aria-pressed="true">বাংলা</button>
<button type="button" class="lang-btn" data-lang="en" aria-pressed="false">English</button>
</div></div>
<main id="app">
<!-- ===== ১. ল্যান্ডিং পেজ ===== -->
<section id="landingScreen" class="screen landing active" aria-labelledby="landingTitle">
<div class="orb o1"></div><div class="orb o2"></div><div class="orb o3"></div>
<div class="floaters" id="floaters" aria-hidden="true"></div>
<div class="landing-inner">
<div class="hero glass" id="hero">
<h1 id="landingTitle" tabindex="-1">রাগটা একটু বের করে দিন</h1>
<p class="sub">নিরাপদ ভার্চুয়াল পরিবেশে আপনার রাগ প্রকাশ করুন</p>
<button type="button" class="btn btn-primary btn-xl" id="enterBtn">রাগের ঘরে প্রবেশ করুন</button>
<p class="disclaimer">এখানে যা কিছু ঘটবে সবই ভার্চুয়াল। বাস্তবে কেউ ক্ষতিগ্রস্ত হবে না।</p>
</div>
<div class="privacy-strip glass">
<p>আপনার গোপনীয়তা আমাদের কাছে গুরুত্বপূর্ণ। ছবিটি কোনো সার্ভারে পাঠানো হবে না।</p>
<button type="button" class="btn btn-ghost btn-sm" data-open="privacyModal">গোপনীয়তা</button>
</div>
</div>
<footer class="credit-mini">
<span>ডেভেলপ করেছেন <strong>সালাম ভুঁইয়া আকাশ</strong></span>
<a class="fb-mini" href="https://www.facebook.com/salambhuiyan.akash" target="_blank" rel="noopener noreferrer"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="#fff" d="M13.5 22v-8.2h2.8l.5-3.3h-3.3V8.4c0-.9.4-1.7 1.8-1.7h1.6V3.8c-.3 0-1.3-.2-2.5-.2-2.6 0-4.3 1.6-4.3 4.4v2.5H7.3v3.3h2.8V22h3.4z"/></svg>ফেসবুক প্রোফাইল</a>
</footer>
</section>
<!-- ===== ২. ছবি আপলোড ===== -->
<section id="uploadScreen" class="screen upload" aria-labelledby="uploadTitle">
<div class="panel glass">
<button type="button" class="btn btn-ghost btn-sm back" id="uploadBack">← ফিরে যান</button>
<h2 id="uploadTitle" tabindex="-1">আপনি যার ওপর রাগ করেছেন, তার ছবি দিন</h2>
<p class="muted">ছবিটি শুধু এই ডিভাইসেই ব্যবহার হবে।</p>
<div class="dropzone" id="dropzone">
<svg class="dz-ico" viewBox="0 0 64 64" aria-hidden="true"><defs><linearGradient id="dzg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#35e0ff"/><stop offset="1" stop-color="#8b5cff"/></linearGradient></defs><rect x="6" y="10" width="52" height="44" rx="9" fill="none" stroke="url(#dzg)" stroke-width="3.5"/><circle cx="22" cy="26" r="5" fill="url(#dzg)"/><path d="M8 48 L24 33 L36 44 L44 37 L56 48" fill="none" stroke="url(#dzg)" stroke-width="3.5" stroke-linejoin="round" stroke-linecap="round"/></svg>
<p class="dz-title">ছবি এখানে টেনে আনুন</p>
<button type="button" class="btn btn-primary" id="pickBtn">ছবি নির্বাচন করুন</button>
<p class="dz-note">সমর্থিত: JPG, JPEG, PNG, WEBP</p>
</div>
<div class="preview" id="previewWrap" hidden>
<div class="preview-frame"><img id="previewImg" alt="আপনার নির্বাচিত ছবির প্রিভিউ"></div>
<div class="row">
<button type="button" class="btn btn-ghost" id="changeBtn">ছবি পরিবর্তন করুন</button>
<button type="button" class="btn btn-ghost btn-danger" id="removeBtn">ছবি মুছে দিন</button>
</div>
</div>
<button type="button" class="btn btn-primary btn-xl" id="startRoomBtn">রাগের ঘর শুরু করুন</button>
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp" hidden>
</div>
</section>
<!-- ===== ৩. মূল রাগের ঘর ===== -->
<section id="roomScreen" class="screen room" aria-labelledby="roomTitle">
<header class="room-head glass">
<h2 class="room-title" id="roomTitle" tabindex="-1">রাগের ঘর</h2>
<div class="head-actions">
<button type="button" class="btn btn-ghost btn-sm" id="soundBtn" aria-pressed="false"><span id="soundIco" aria-hidden="true">🔇</span><span id="soundLabel">শব্দ চালু</span></button>
<button type="button" class="btn btn-ghost btn-sm" id="settingsBtn"><span aria-hidden="true">⚙️</span>সেটিংস</button>
</div>
</header>
<div class="stage" id="stage">
<div class="wall"></div><div class="neon"></div><div class="floor"></div><div class="light"></div>
<div class="motes" id="motes" aria-hidden="true"></div>
<div class="stage-obj">
<div class="frame" id="frame" tabindex="0" role="button" aria-label="নির্বাচিত জিনিসটি ছবির ওপর ব্যবহার করুন">
<span class="tag">ভার্চুয়াল লক্ষ্য</span>
<div class="photo-clip" id="photoClip">
<img id="targetImg" alt="আপনার নির্বাচিত ছবি – ভার্চুয়াল লক্ষ্য" draggable="false">
<div class="photo-fx" id="photoFx"></div>
</div>
</div>
<div class="floor-shadow" aria-hidden="true"></div>
</div>
<p class="hint" id="hint"></p>
</div>
<section class="meter-panel glass" aria-label="রাগের মাত্রা">
<h3 class="panel-title">রাগের মাত্রা</h3>
<div class="meter-row">
<div class="meter-num" id="meterNum">১০০%</div>
<div class="status" id="meterStatus" aria-live="polite">রাগ অনেক বেশি</div>
</div>
<div class="bar" id="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-label="রাগের মাত্রা"><div class="bar-fill" id="barFill"></div></div>
<div><span class="combo-badge" id="comboBadge" hidden aria-live="polite"></span></div>
<div class="tally desk-block">
<h3 class="panel-title">জিনিস ব্যবহারের হিসাব</h3>
<ul id="tallyList" class="tally-list"></ul>
<p class="tally-total" id="tallyTotal"></p>
</div>
<button type="button" class="btn btn-ghost btn-sm only-desk" id="dlDeskBtn">সারাংশ ডাউনলোড করুন</button>
<button type="button" class="btn btn-ghost btn-sm only-desk" id="resetDeskBtn">সেশন নতুন করে শুরু করুন</button>
</section>
<nav class="items-panel glass" aria-label="জিনিসপত্রের তালিকা">
<h3 class="panel-title">জিনিসপত্র</h3>
<div class="items-grid" id="itemsGrid"></div>
</nav>
</section>
<!-- ===== ৪. শ্বাসের অনুশীলন ===== -->
<section id="breathScreen" class="screen center" aria-labelledby="breathTitle">
<h2 id="breathTitle" class="sr-only" tabindex="-1">শান্ত হওয়ার অনুশীলন</h2>
<div class="breath-wrap">
<svg class="ring" viewBox="0 0 200 200" aria-hidden="true"><circle class="bgc" cx="100" cy="100" r="94"/><circle class="pg" id="ringProg" cx="100" cy="100" r="94"/></svg>
<div class="breath-circle" id="breathCircle" aria-hidden="true"></div>
<div class="breath-text"><div class="ph" id="breathPhase" aria-live="polite">শ্বাস নিন</div><div class="left" id="breathLeft"></div></div>
</div>
<p class="done-msg" id="breathDone" hidden>খুব ভালো। আপনি এখন আগের চেয়ে শান্ত।</p>
<div class="row" style="justify-content:center">
<button type="button" class="btn btn-primary" id="breathNext" hidden>চালিয়ে যান</button>
<button type="button" class="btn btn-ghost btn-sm" id="breathSkip">এড়িয়ে যান</button>
</div>
</section>
<!-- ===== ৫. প্রশ্ন ===== -->
<section id="checkScreen" class="screen center" aria-labelledby="checkTitle">
<div class="panel glass">
<div class="calm-orb" aria-hidden="true"></div>
<h2 id="checkTitle" tabindex="-1">আপনার রাগ কি কমেছে?</h2>
<div class="row">
<button type="button" class="btn btn-primary" id="rageYes">হ্যাঁ, কমেছে</button>
<button type="button" class="btn btn-ghost" id="rageNo">না, এখনও আছে</button>
</div>
</div>
</section>
<section id="playScreen" class="screen center" aria-labelledby="playTitle">
<div class="panel glass">
<h2 id="playTitle" tabindex="-1">আপনি কি আরও খেলতে চান?</h2>
<div class="row">
<button type="button" class="btn btn-primary" id="playYes">হ্যাঁ, আবার খেলব</button>
<button type="button" class="btn btn-ghost" id="playNo">না, ধন্যবাদ</button>
</div>
</div>
</section>
<!-- ===== ৬. শেষ পর্দা ===== -->
<section id="finalScreen" class="screen center final" aria-labelledby="finalTitle">
<div class="calm-orb" aria-hidden="true"></div>
<h1 id="finalTitle" tabindex="-1">আপনাকে ধন্যবাদ</h1>
<p class="sub">রাগ সাময়িক। কিন্তু আপনার পরবর্তী সিদ্ধান্ত গুরুত্বপূর্ণ।</p>
<div class="btns">
<button type="button" class="btn btn-primary" id="dlFinalBtn">সারাংশ ডাউনলোড করুন</button>
<button type="button" class="btn btn-ghost" id="againBtn">আবার শুরু করুন</button>
<button type="button" class="btn btn-ghost" id="newPhotoBtn">নতুন ছবি দিন</button>
<button type="button" class="btn btn-ghost" id="endBtn">শেষ করুন</button>
</div>
<p class="note">মনে রাখুন: এটি ছিল একটি সম্পূর্ণ ভার্চুয়াল অভিজ্ঞতা।</p>
<footer class="credit glass">
<p>ডেভেলপ করেছেন <strong>সালাম ভুঁইয়া আকাশ</strong></p>
<a id="fbBtn" class="btn fb-btn" href="https://www.facebook.com/salambhuiyan.akash" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="#fff" d="M13.5 22v-8.2h2.8l.5-3.3h-3.3V8.4c0-.9.4-1.7 1.8-1.7h1.6V3.8c-.3 0-1.3-.2-2.5-.2-2.6 0-4.3 1.6-4.3 4.4v2.5H7.3v3.3h2.8V22h3.4z"/></svg>
ফেসবুকে যোগাযোগ করুন
</a>
</footer>
</section>
</main>
<!-- ===== মোডাল ===== -->
<div class="modal-back" id="calmModal" role="dialog" aria-modal="true" aria-labelledby="calmTitle" aria-hidden="true" data-persistent="1">
<div class="modal glass">
<div class="calm-orb" aria-hidden="true"></div>
<h2 id="calmTitle">আপনি এখন অনেকটাই শান্ত</h2>
<p class="muted">রাগটা বের হয়েছে। এবার মনটাকে একটু শান্ত করি।</p>
<button type="button" class="btn btn-primary" id="calmStartBtn" data-autofocus>৩০ সেকেন্ডের শান্ত হওয়ার অনুশীলন শুরু করুন</button>
</div>
</div>
<div class="modal-back" id="privacyModal" role="dialog" aria-modal="true" aria-labelledby="privacyTitle" aria-hidden="true">
<div class="modal glass">
<h2 id="privacyTitle">গোপনীয়তা</h2>
<ul>
<li>আপনার গোপনীয়তা আমাদের কাছে গুরুত্বপূর্ণ।</li>
<li>আপনার নির্বাচিত ছবি আপনার ডিভাইসের ব্রাউজারেই প্রক্রিয়া করা হবে।</li>
<li>ছবিটি কোনো সার্ভারে পাঠানো হবে না।</li>
<li>আপনার ব্যক্তিগত তথ্য সংগ্রহ করা হবে না।</li>
</ul>
<button type="button" class="btn btn-primary" data-close="privacyModal" data-autofocus>বুঝেছি</button>
</div>
</div>
<div class="modal-back" id="settingsModal" role="dialog" aria-modal="true" aria-labelledby="settingsTitle" aria-hidden="true">
<div class="modal glass">
<h2 id="settingsTitle">সেটিংস</h2>
<label class="switch-row"><span>শব্দ চালু/বন্ধ</span><input type="checkbox" class="switch" id="setSound" role="switch"></label>
<label class="switch-row"><span>অ্যানিমেশন কমান</span><input type="checkbox" class="switch" id="setReduce" role="switch"></label>
<button type="button" class="btn btn-ghost" id="setChangePhoto">ছবি পরিবর্তন করুন</button>
<button type="button" class="btn btn-ghost" id="setDownload">সারাংশ ডাউনলোড করুন</button>
<button type="button" class="btn btn-ghost" id="setReset">সেশন নতুন করে শুরু করুন</button>
<button type="button" class="btn btn-ghost" id="setPrivacy">গোপনীয়তা</button>
<button type="button" class="btn btn-primary" data-close="settingsModal" data-autofocus>বন্ধ করুন</button>
</div>
</div>
<div id="toasts" aria-live="polite"></div>
<div id="fxLayer" aria-hidden="true"></div>
<script>
(function () {
'use strict';
/* ================= helpers ================= */
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
const EN = {
'রাগটা একটু বের করে দিন':'Let it out, just a little',
'নিরাপদ ভার্চুয়াল পরিবেশে আপনার রাগ প্রকাশ করুন':'Release your anger in a safe virtual space',
'রাগের ঘরে প্রবেশ করুন':'Enter the Anger Room',
'এখানে যা কিছু ঘটবে সবই ভার্চুয়াল। বাস্তবে কেউ ক্ষতিগ্রস্ত হবে না।':'Everything here is virtual. No one will be harmed in real life.',
'আপনার গোপনীয়তা আমাদের কাছে গুরুত্বপূর্ণ। ছবিটি কোনো সার্ভারে পাঠানো হবে না।':'Your privacy matters to us. Your photo is never sent to any server.',
'গোপনীয়তা':'Privacy',
'ডেভেলপ করেছেন':'Developed by',
'সালাম ভুঁইয়া আকাশ':'Salam Bhuiyan Akash',
'ফেসবুক প্রোফাইল':'Facebook profile',
'ফেসবুকে যোগাযোগ করুন':'Connect on Facebook',
'ভাষা':'Language',
'← ফিরে যান':'← Go back',
'আপনি যার ওপর রাগ করেছেন, তার ছবি দিন':'Add a photo of the person you are angry at',
'ছবিটি শুধু এই ডিভাইসেই ব্যবহার হবে।':'The photo is used on this device only.',
'ছবি এখানে টেনে আনুন':'Drag your photo here',
'ছবি নির্বাচন করুন':'Choose a photo',
'সমর্থিত: JPG, JPEG, PNG, WEBP':'Supported: JPG, JPEG, PNG, WEBP',
'ছবি পরিবর্তন করুন':'Change photo',
'ছবি মুছে দিন':'Remove photo',
'রাগের ঘর শুরু করুন':'Start the Anger Room',
'আপনার নির্বাচিত ছবির প্রিভিউ':'Preview of your selected photo',
'রাগের ঘর':'Anger Room',
'শব্দ চালু':'Sound on',
'শব্দ বন্ধ':'Sound off',
'সেটিংস':'Settings',
'নির্বাচিত জিনিসটি ছবির ওপর ব্যবহার করুন':'Use the selected item on the photo',
'ভার্চুয়াল লক্ষ্য':'Virtual target',
'আপনার নির্বাচিত ছবি – ভার্চুয়াল লক্ষ্য':'Your selected photo – virtual target',
'রাগের মাত্রা':'Anger level',
'সেশন নতুন করে শুরু করুন':'Restart session',
'জিনিসপত্র':'Items',
'জিনিসপত্রের তালিকা':'Items list',
'রাগ অনেক বেশি':'Very angry',
'রাগ এখনও আছে':'Still angry',
'আপনি ধীরে ধীরে শান্ত হচ্ছেন':'You are slowly calming down',
'প্রায় শান্ত':'Almost calm',
'আপনি শান্ত':'You are calm',
'লাঠি':'Stick','ঝাড়ু':'Broom','জুতা':'Shoe','ডিম':'Egg','রং':'Paint','পানি':'Water','ক্যান':'Can','বালিশ':'Pillow','স্লাইম':'Slime','কার্টুন পায়খানা':'Cartoon poop',
'ধাম!':'Thud!','শোঁ!':'Swish!','ধুপ!':'Thump!','ছপ!':'Splat!','ছিটকে গেল!':'Splash!','ছপছপ!':'Splish-splash!','ঠাস!':'Whack!','ফুস!':'Poof!','চপচপ!':'Squelch!','ওহ নো!':'Oh no!',
'একটি জিনিস ধরে ছবির ওপর নিয়ে যান':'Grab an item and drag it onto the photo',
'একটি জিনিস নির্বাচন করুন, তারপর ছবিতে স্পর্শ করুন':'Select an item, then tap the photo',
'শান্ত হওয়ার অনুশীলন':'Calming exercise',
'শ্বাস নিন':'Breathe in','ধরে রাখুন':'Hold','শ্বাস ছাড়ুন':'Breathe out','শান্ত থাকুন':'Stay calm',
'খুব ভালো। আপনি এখন আগের চেয়ে শান্ত।':'Very good. You are calmer than before.',
'চালিয়ে যান':'Continue','এড়িয়ে যান':'Skip',
'আপনার রাগ কি কমেছে?':'Has your anger gone down?',
'হ্যাঁ, কমেছে':'Yes, it has','না, এখনও আছে':'No, it is still there',
'আপনি কি আরও খেলতে চান?':'Do you want to play more?',
'হ্যাঁ, আবার খেলব':'Yes, play again','না, ধন্যবাদ':'No, thanks',
'আপনাকে ধন্যবাদ':'Thank you',
'রাগ সাময়িক। কিন্তু আপনার পরবর্তী সিদ্ধান্ত গুরুত্বপূর্ণ।':'Anger is temporary. But your next decision matters.',
'আবার শুরু করুন':'Start again','নতুন ছবি দিন':'Add a new photo','শেষ করুন':'Finish',
'মনে রাখুন: এটি ছিল একটি সম্পূর্ণ ভার্চুয়াল অভিজ্ঞতা।':'Remember: this was a completely virtual experience.',
'আপনি এখন অনেকটাই শান্ত':'You are much calmer now',
'রাগটা বের হয়েছে। এবার মনটাকে একটু শান্ত করি।':'The anger is out. Now let us calm the mind a little.',
'৩০ সেকেন্ডের শান্ত হওয়ার অনুশীলন শুরু করুন':'Start the 30-second calming exercise',
'আপনার গোপনীয়তা আমাদের কাছে গুরুত্বপূর্ণ।':'Your privacy is important to us.',
'আপনার নির্বাচিত ছবি আপনার ডিভাইসের ব্রাউজারেই প্রক্রিয়া করা হবে।':"Your selected photo is processed only in your device's browser.",
'ছবিটি কোনো সার্ভারে পাঠানো হবে না।':'The photo will not be sent to any server.',
'আপনার ব্যক্তিগত তথ্য সংগ্রহ করা হবে না।':'Your personal information will not be collected.',
'বুঝেছি':'Got it',
'শব্দ চালু/বন্ধ':'Sound on/off',
'অ্যানিমেশন কমান':'Reduce animation',
'বন্ধ করুন':'Close',
'দয়া করে JPG, PNG অথবা WEBP ছবি ব্যবহার করুন।':'Please use a JPG, PNG or WEBP image.',
'ছবিটি অনেক বড়। অনুগ্রহ করে ছোট একটি ছবি নির্বাচন করুন।':'The image is too large. Please choose a smaller image.',
'দয়া করে প্রথমে একটি ছবি নির্বাচন করুন।':'Please choose a photo first.',
'প্রথমে একটি জিনিস নির্বাচন করুন।':'Select an item first.',
'ছবি মুছে ফেলা হয়েছে।':'Photo removed.',
'সেশন নতুন করে শুরু হয়েছে।':'Session restarted.',
'ধন্যবাদ। নিজের যত্ন নিন।':'Thank you. Take care of yourself.',
'রাগের ঘর – নিরাপদ ভার্চুয়াল পরিবেশে রাগ প্রকাশ':'Anger Room – Release anger in a safe virtual space',
'জিনিস ব্যবহারের হিসাব':'Item usage count',
'সারাংশ ডাউনলোড করুন':'Download summary',
'রাগের ঘর – সারাংশ':'Anger Room – Summary',
'মোট':'Total',
'এখনও কোনো জিনিস ব্যবহার করা হয়নি।':'No item has been used yet.',
'সারাংশ ডাউনলোড হয়েছে।':'Summary downloaded.',
'সারাংশ তৈরি করা যায়নি।':'Could not create the summary.',
'এটি ছিল একটি ভার্চুয়াল অভিজ্ঞতা।':'This was a virtual experience.'
};
const tr = s => (state.lang === 'en' && EN[s] !== undefined) ? EN[s] : s;
const bn = n => state.lang === 'en' ? String(n) : String(n).replace(/\d/g, d => '০১২৩৪৫৬৭৮৯'[d]);
const comboText = n => state.lang === 'en' ? n + ' actions in a row!' : bn(n) + 'টি ধারাবাহিক কাজ!';
const countText = n => state.lang === 'en' ? n + (n === 1 ? ' time' : ' times') : bn(n) + ' বার';
const leftText = n => state.lang === 'en' ? n + ' seconds left' : 'বাকি ' + bn(n) + ' সেকেন্ড';
const rand = (a, b) => a + Math.random() * (b - a);
const pick = a => a[Math.floor(Math.random() * a.length)];
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
const ease = t => t < .5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
const MAX_FILE = 12 * 1024 * 1024;
const state = {
imageURL: null, anger: 100, shown: 100, combo: 0, selected: null,
sound: false, reduce: false, locked: false, calmShown: false, lang: 'bn', hits: {}, total: 0,
audio: null, noise: null, pc: 0
};
const fine = window.matchMedia && matchMedia('(hover:hover) and (pointer:fine)').matches;
const fxLayer = $('#fxLayer');
const timers = new Set();
let comboTimer = 0, meterTok = 0, toastCount = 0;
const B = { raf: 0, start: 0, phase: '' };
function later(fn, ms) {
const id = setTimeout(() => { timers.delete(id); fn(); }, ms);
timers.add(id); return id;
}
function clearTimers() { timers.forEach(clearTimeout); timers.clear(); }
/* ================= item graphics ================= */
const I = {
stick: '<ellipse cx="50" cy="95" rx="30" ry="4" fill="rgba(0,0,0,.3)"/><g transform="rotate(38 50 50)"><rect x="41" y="4" width="18" height="90" rx="9" fill="url(#gWood)" stroke="#5a3515" stroke-width="2.5"/><rect x="45" y="10" width="4" height="78" rx="2" fill="#fff" opacity=".35"/><ellipse cx="54" cy="34" rx="3" ry="5" fill="#7a4a1f" opacity=".6"/><ellipse cx="47" cy="66" rx="3" ry="4" fill="#7a4a1f" opacity=".6"/></g>',
broom: '<g transform="rotate(30 50 50)"><rect x="47" y="2" width="7" height="58" rx="3.5" fill="url(#gWood)" stroke="#5a3515" stroke-width="2"/><rect x="36" y="54" width="29" height="9" rx="3" fill="#e0405a" stroke="#7b1630" stroke-width="2"/><path d="M33 63 H68 L76 96 H25 Z" fill="url(#gStraw)" stroke="#8a5d12" stroke-width="2.5" stroke-linejoin="round"/><path d="M40 68 L36 94 M50 68 V94 M60 68 L64 94" stroke="#b07a1a" stroke-width="2" fill="none"/></g>',
shoe: '<ellipse cx="50" cy="92" rx="38" ry="4" fill="rgba(0,0,0,.3)"/><path d="M10 58 Q10 36 30 34 L44 34 Q50 50 68 54 Q92 58 92 72 L92 80 L10 80 Z" fill="url(#gShoe)" stroke="#6b0f1b" stroke-width="2.5" stroke-linejoin="round"/><rect x="8" y="74" width="86" height="14" rx="7" fill="url(#gSole)" stroke="#8d93a3" stroke-width="2.5"/><path d="M36 40 L48 46 M42 36 L54 44 M48 33 L60 42" stroke="#fff" stroke-width="3" stroke-linecap="round"/><path d="M64 58 Q82 60 89 68" stroke="#fff" stroke-width="3" opacity=".45" fill="none" stroke-linecap="round"/>',
egg: '<ellipse cx="50" cy="95" rx="24" ry="3.5" fill="rgba(0,0,0,.3)"/><path d="M50 6 C72 6 86 42 86 62 C86 82 70 94 50 94 C30 94 14 82 14 62 C14 42 28 6 50 6Z" fill="url(#gEgg)" stroke="#b79a6a" stroke-width="2.5"/><ellipse cx="36" cy="38" rx="7" ry="12" fill="#fff" opacity=".75" transform="rotate(20 36 38)"/><circle cx="60" cy="62" r="2" fill="#d6bd8e"/><circle cx="48" cy="76" r="2.4" fill="#d6bd8e"/><circle cx="68" cy="76" r="1.8" fill="#d6bd8e"/>',
paint: '<ellipse cx="50" cy="95" rx="30" ry="3.5" fill="rgba(0,0,0,.3)"/><path d="M18 34 L82 34 L74 90 Q50 98 26 90 Z" fill="url(#gPaint)" stroke="#173a85" stroke-width="2.5" stroke-linejoin="round"/><path d="M21 60 H79 L77.6 72 H22.4Z" fill="#ffd93b" opacity=".92"/><ellipse cx="50" cy="34" rx="32" ry="9" fill="#ff4fa3" stroke="#173a85" stroke-width="2.5"/><path d="M30 38 Q30 54 36 54 Q41 54 41 38Z M58 38 Q58 62 64 62 Q70 62 70 38Z" fill="#ff4fa3"/><path d="M20 34 Q50 -6 80 34" fill="none" stroke="#555" stroke-width="3"/>',
water: '<ellipse cx="50" cy="96" rx="28" ry="3" fill="rgba(0,0,0,.3)"/><rect x="39" y="4" width="22" height="9" rx="3" fill="#1fb6a6" stroke="#0b6d63" stroke-width="2"/><path d="M40 13 H60 V24 Q76 32 76 52 V86 Q76 95 67 95 H33 Q24 95 24 86 V52 Q24 32 40 24Z" fill="url(#gWater)" stroke="#1b5fa3" stroke-width="2.5" stroke-linejoin="round"/><path d="M26 56 Q38 50 50 56 T74 56 V86 Q74 93 67 93 H33 Q26 93 26 86Z" fill="#fff" opacity=".3"/><path d="M32 40 Q30 56 32 76" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity=".65" fill="none"/>',
can: '<ellipse cx="50" cy="94" rx="30" ry="3.5" fill="rgba(0,0,0,.3)"/><rect x="22" y="16" width="56" height="72" rx="7" fill="url(#gMetal)" stroke="#59636f" stroke-width="2.5"/><ellipse cx="50" cy="16" rx="28" ry="7" fill="#d9e0ea" stroke="#59636f" stroke-width="2.5"/><rect x="44" y="12" width="14" height="6" rx="3" fill="#aab4c1" stroke="#59636f" stroke-width="1.5"/><rect x="22.5" y="38" width="55" height="28" fill="#ff7a45"/><path d="M50 42 l3.5 7 7.5 1 -5.5 5 1.5 7.5 -7 -3.8 -7 3.8 1.5 -7.5 -5.5 -5 7.5 -1z" fill="#fff3b0"/><path d="M28 20 V84" stroke="#fff" stroke-width="3" opacity=".55" stroke-linecap="round"/>',
pillow: '<ellipse cx="50" cy="90" rx="38" ry="4" fill="rgba(0,0,0,.28)"/><path d="M16 30 Q50 16 84 30 Q94 50 84 70 Q50 84 16 70 Q6 50 16 30Z" fill="url(#gPillow)" stroke="#c0527f" stroke-width="2.5" stroke-linejoin="round"/><circle cx="14" cy="30" r="5" fill="#ff9cc2" stroke="#c0527f" stroke-width="2"/><circle cx="86" cy="30" r="5" fill="#ff9cc2" stroke="#c0527f" stroke-width="2"/><circle cx="14" cy="70" r="5" fill="#ff9cc2" stroke="#c0527f" stroke-width="2"/><circle cx="86" cy="70" r="5" fill="#ff9cc2" stroke="#c0527f" stroke-width="2"/><path d="M28 52 H72" stroke="#e57ba6" stroke-width="2" stroke-dasharray="5 5" stroke-linecap="round"/><ellipse cx="34" cy="36" rx="14" ry="5" fill="#fff" opacity=".65"/>',
slime: '<ellipse cx="50" cy="96" rx="26" ry="3" fill="rgba(0,0,0,.28)"/><path d="M16 62 Q10 34 38 28 Q48 6 68 22 Q94 26 86 58 Q92 76 72 80 L74 94 Q72 99 68 94 L66 82 Q44 90 28 80 L26 90 Q24 96 20 90 L22 76 Q12 74 16 62Z" fill="url(#gSlime)" stroke="#1c7a22" stroke-width="2.5" stroke-linejoin="round"/><ellipse cx="36" cy="38" rx="9" ry="5" fill="#fff" opacity=".6"/><circle cx="38" cy="56" r="7" fill="#fff" stroke="#1c7a22" stroke-width="2"/><circle cx="60" cy="56" r="7" fill="#fff" stroke="#1c7a22" stroke-width="2"/><circle cx="39" cy="57" r="3" fill="#123"/><circle cx="61" cy="57" r="3" fill="#123"/><path d="M42 68 Q50 75 58 68" stroke="#1c7a22" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
poop: '<ellipse cx="50" cy="95" rx="34" ry="3.5" fill="rgba(0,0,0,.3)"/><path d="M14 80 Q14 64 34 62 H66 Q86 64 86 80 Q86 92 66 92 H34 Q14 92 14 80Z" fill="url(#gPoop)" stroke="#3b200b" stroke-width="2.5"/><path d="M22 60 Q22 46 38 44 H62 Q78 46 78 60 Q78 66 62 66 H38 Q22 66 22 60Z" fill="url(#gPoop)" stroke="#3b200b" stroke-width="2.5"/><path d="M32 44 Q32 30 44 28 Q40 14 54 6 Q52 18 62 24 Q68 28 68 44 Q60 48 50 48 Q40 48 32 44Z" fill="url(#gPoop)" stroke="#3b200b" stroke-width="2.5" stroke-linejoin="round"/><circle cx="40" cy="72" r="7" fill="#fff" stroke="#3b200b" stroke-width="2"/><circle cx="60" cy="72" r="7" fill="#fff" stroke="#3b200b" stroke-width="2"/><circle cx="41" cy="73" r="3.2" fill="#1a0f05"/><circle cx="61" cy="73" r="3.2" fill="#1a0f05"/><path d="M40 82 Q50 92 60 82Z" fill="#3b200b"/><path d="M45 85 Q50 89 55 85Z" fill="#ff7a8a"/><path d="M22 76 Q22 69 29 67" stroke="#fff" stroke-width="3" opacity=".4" fill="none" stroke-linecap="round"/>'
};
const svg = inner => '<svg viewBox="0 0 100 100" aria-hidden="true" focusable="false">' + inner + '</svg>';
/* ================= items config ================= */
const ITEMS = [
{ id: 'stick', name: 'লাঠি', word: 'ধাম!', cut: 7, color: '#ffd23f', shake: 'hit', screen: 1, burst: 1, fly: { r0: 85, r1: -40, s0: .9, s1: 1.35, arc: 30, dur: 340 } },
{ id: 'broom', name: 'ঝাড়ু', word: 'শোঁ!', cut: 5, color: '#9de8ff', shake: 'sweep', screen: 0, burst: 0, fly: { r0: -75, r1: 40, s0: .9, s1: 1.3, arc: 20, dur: 380 } },
{ id: 'shoe', name: 'জুতা', word: 'ধুপ!', cut: 6, color: '#ff8a8a', shake: 'hit', screen: 1, burst: 1, fly: { r0: -20, r1: 340, s0: .8, s1: 1.25, arc: 120, dur: 480 } },
{ id: 'egg', name: 'ডিম', word: 'ছপ!', cut: 8, color: '#fff3a8', shake: 'jiggle', screen: 0, burst: 0, fly: { r0: 0, r1: 360, s0: .7, s1: 1.2, arc: 150, dur: 520 } },
{ id: 'paint', name: 'রং', word: 'ছিটকে গেল!', cut: 5, color: '#ff6fb5', shake: 'jiggle', screen: 0, burst: 0, fly: { r0: 0, r1: -130, s0: .8, s1: 1.25, arc: 130, dur: 520 } },
{ id: 'water', name: 'পানি', word: 'ছপছপ!', cut: 4, color: '#7fd4ff', shake: 'jiggle', screen: 0, burst: 0, fly: { r0: 0, r1: 135, s0: .8, s1: 1.25, arc: 110, dur: 480 } },
{ id: 'can', name: 'ক্যান', word: 'ঠাস!', cut: 7, color: '#ffb36b', shake: 'hit', screen: 1, burst: 1, fly: { r0: 0, r1: 720, s0: .75, s1: 1.25, arc: 100, dur: 450 } },
{ id: 'pillow', name: 'বালিশ', word: 'ফুস!', cut: 3, color: '#ffc4dd', shake: 'soft', screen: 0, burst: 0, fly: { r0: -30, r1: 25, s0: .85, s1: 1.4, arc: 80, dur: 520 } },
{ id: 'slime', name: 'স্লাইম', word: 'চপচপ!', cut: 6, color: '#a6ff6b', shake: 'jiggle', screen: 0, burst: 0, fly: { r0: -10, r1: 15, s0: .8, s1: 1.3, arc: 140, dur: 560 } },
{ id: 'poop', name: 'কার্টুন পায়খানা', word: 'ওহ নো!', cut: 8, color: '#d9a066', shake: 'jiggle', screen: 0, burst: 0, fly: { r0: 0, r1: 720, s0: .75, s1: 1.25, arc: 170, dur: 600 } }
];
ITEMS.forEach(it => { it.svg = svg(I[it.id]); });
const byId = id => ITEMS.find(i => i.id === id);
/* ================= screens / modals / toast ================= */
function showScreen(id) {
$$('.screen').forEach(s => s.classList.remove('active'));
const s = $('#' + id); s.classList.add('active');
window.scrollTo(0, 0);
const h = s.querySelector('h1,h2');
if (h) { try { h.focus({ preventScroll: true }); } catch (e) {} }
}
const modalStack = [];
function openModal(id) {
const m = $('#' + id); if (!m || m.classList.contains('open')) return;
m._prev = document.activeElement;
m.classList.add('open'); m.setAttribute('aria-hidden', 'false'); modalStack.push(id);
const f = m.querySelector('[data-autofocus]') || m.querySelector('button');
setTimeout(() => f && f.focus(), 40);
}
function closeModal(id) {
const m = $('#' + id); if (!m) return;
m.classList.remove('open'); m.setAttribute('aria-hidden', 'true');
const i = modalStack.indexOf(id); if (i > -1) modalStack.splice(i, 1);
if (m._prev && document.contains(m._prev)) { try { m._prev.focus(); } catch (e) {} }
}
function closeAllModals() { modalStack.slice().forEach(closeModal); }
function toast(msg, kind) {
const box = $('#toasts');
while (box.children.length >= 3) box.firstChild.remove();
const el = document.createElement('div');
el.className = 'toast ' + (kind || 'info');
el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
el.textContent = tr(msg); box.appendChild(el);
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 320); }, 3600);
}
/* ================= image upload ================= */
function isAllowed(file) {
const okType = ['image/jpeg', 'image/png', 'image/webp'].includes(file.type);
const okExt = /\.(jpe?g|png|webp)$/i.test(file.name || '');
return file.type ? okType : okExt;
}
function handleImageUpload(file) {
if (!file) return;
if (!isAllowed(file)) { toast('দয়া করে JPG, PNG অথবা WEBP ছবি ব্যবহার করুন।', 'error'); return; }
if (file.size > MAX_FILE) { toast('ছবিটি অনেক বড়। অনুগ্রহ করে ছোট একটি ছবি নির্বাচন করুন।', 'error'); return; }
const url = URL.createObjectURL(file);
const probe = new Image();
probe.onload = () => {
if (state.imageURL) URL.revokeObjectURL(state.imageURL);
state.imageURL = url; previewImage();
};
probe.onerror = () => { URL.revokeObjectURL(url); toast('দয়া করে JPG, PNG অথবা WEBP ছবি ব্যবহার করুন।', 'error'); };
probe.src = url;
}
function previewImage() {
const has = !!state.imageURL;
$('#previewWrap').hidden = !has;
$('#dropzone').hidden = has;
if (has) $('#previewImg').src = state.imageURL; else $('#previewImg').removeAttribute('src');
}
function clearImage() {
if (state.imageURL) { URL.revokeObjectURL(state.imageURL); state.imageURL = null; }
$('#targetImg').removeAttribute('src');
previewImage();
}
/* ================= room ================= */
function startAngerRoom(keep) {
if (!state.imageURL) {
toast('দয়া করে প্রথমে একটি ছবি নির্বাচন করুন।', 'error');
const dz = $('#dropzone'); dz.classList.remove('shake'); void dz.offsetWidth; dz.classList.add('shake');
return;
}
resetSession({ keepImage: true, keepStats: keep === true });
$('#targetImg').src = state.imageURL;
showScreen('roomScreen');
updateAngerMeter(100, true);
updateHint();
}
function buildItems() {
const grid = $('#itemsGrid');
ITEMS.forEach(it => {
const b = document.createElement('button');
b.type = 'button'; b.className = 'item'; b.dataset.id = it.id;
b.setAttribute('aria-pressed', 'false');
b.setAttribute('aria-label', it.name + ' নির্বাচন করুন');
b.innerHTML = '<span class="item-ico">' + it.svg + '</span><span class="item-name">' + it.name + '</span><span class="cnt" aria-hidden="true"></span>';
let drag = null;
b.addEventListener('pointerdown', e => {
if (e.button !== undefined && e.button !== 0) return;
if (state.locked) return;
drag = { sx: e.clientX, sy: e.clientY, active: false, ghost: null };
try { b.setPointerCapture(e.pointerId); } catch (er) {}
});
b.addEventListener('pointermove', e => {
if (!drag) return;
if (!drag.active && Math.hypot(e.clientX - drag.sx, e.clientY - drag.sy) > 8) {
drag.active = true; selectItem(it, true);
const g = document.createElement('div'); g.className = 'ghost'; g.innerHTML = it.svg;
document.body.appendChild(g); drag.ghost = g; b.classList.add('dragging');
}
if (drag.active) {
drag.ghost.style.transform = 'translate(' + (e.clientX - 42) + 'px,' + (e.clientY - 62) + 'px) scale(1.15) rotate(-8deg)';
$('#frame').classList.toggle('drop-ready', overFrame(e.clientX, e.clientY));
e.preventDefault();
}
});
const end = e => {
if (!drag) return;
const d = drag; drag = null;
try { b.releasePointerCapture(e.pointerId); } catch (er) {}
if (d.active) {
d.ghost.remove(); b.classList.remove('dragging'); $('#frame').classList.remove('drop-ready');
if (e.type === 'pointerup' && overFrame(e.clientX, e.clientY)) handleTargetInteraction(e.clientX, e.clientY, true);
} else if (e.type === 'pointerup') {
selectItem(it, false);
}
};
b.addEventListener('pointerup', end);
b.addEventListener('pointercancel', end);
b.addEventListener('click', e => { if (e.detail === 0) selectItem(it, false); });
b.addEventListener('contextmenu', e => e.preventDefault());
grid.appendChild(b);
});
}
function overFrame(x, y) {
const r = $('#frame').getBoundingClientRect();
return x >= r.left - 20 && x <= r.right + 20 && y >= r.top - 20 && y <= r.bottom + 20;
}
function selectItem(it, forceOn) {
state.selected = (!forceOn && state.selected && state.selected.id === it.id) ? null : it;
$$('.item').forEach(b => {
const on = !!state.selected && b.dataset.id === state.selected.id;
b.classList.toggle('selected', on); b.setAttribute('aria-pressed', on ? 'true' : 'false');
});
$('#frame').classList.toggle('armed', !!state.selected);
updateHint();
}
function updateHint() {
const h = $('#hint'), s = state.selected, en = state.lang === 'en';
if (s) {
h.textContent = en
? 'Selected: ' + tr(s.name) + '. Now ' + (fine ? 'click the photo or drop the item on it' : 'tap the photo') + '.'
: 'নির্বাচিত: ' + s.name + '। এখন ছবিতে ' + (fine ? 'ক্লিক করুন অথবা ছবির ওপর ছেড়ে দিন' : 'স্পর্শ করুন') + '।';
} else h.textContent = tr(fine ? 'একটি জিনিস ধরে ছবির ওপর নিয়ে যান' : 'একটি জিনিস নির্বাচন করুন, তারপর ছবিতে স্পর্শ করুন');
}
function handleTargetInteraction(x, y, fromDrag) {
if (state.locked || state.anger <= 0) return;
const it = state.selected;
if (!it) { toast('প্রথমে একটি জিনিস নির্বাচন করুন।', 'info'); return; }
const pr = $('#photoClip').getBoundingClientRect();
const tx = clamp(x, pr.left + pr.width * .12, pr.right - pr.width * .12);
const ty = clamp(y, pr.top + pr.height * .12, pr.bottom - pr.height * .12);
let from;
if (fromDrag) {
from = { x: clamp(tx + (Math.random() < .5 ? -1 : 1) * rand(90, 150), 40, innerWidth - 40), y: Math.max(40, ty - rand(130, 190)) };
} else {
const br = $('.item[data-id="' + it.id + '"]').getBoundingClientRect();
from = { x: br.left + br.width / 2, y: br.top + br.height / 2 };
}
state.anger = Math.max(0, state.anger - it.cut);
createEffect(it, from, { x: tx, y: ty }, state.anger);
updateCombo();
}
function flightKF(f, a, b) {
const arc = state.reduce ? 0 : f.arc;
const mx = (a.x + b.x) / 2, my = Math.min(a.y, b.y) - arc;
const T = (x, y, r, s) => 'translate(' + (x - 44) + 'px,' + (y - 44) + 'px) rotate(' + r + 'deg) scale(' + s + ')';
return [
{ transform: T(a.x, a.y, f.r0, f.s0), opacity: .85, easing: 'ease-out' },
{ offset: .5, transform: T(mx, my, (f.r0 + f.r1) / 2, (f.s0 + f.s1) / 2), opacity: 1, easing: 'ease-in' },
{ transform: T(b.x, b.y, f.r1, f.s1), opacity: 1 }
];
}
function createEffect(it, from, to, after) {
const dur = state.reduce ? 150 : it.fly.dur;
const fly = document.createElement('div');
fly.className = 'fly'; fly.innerHTML = it.svg; fxLayer.appendChild(fly);
fly.animate(flightKF(it.fly, from, to), { duration: dur, easing: 'linear', fill: 'forwards' });
later(() => {
fly.remove();
impact(it, to.x, to.y);
updateAngerMeter(after);
if (after === 0 && !state.calmShown) {
state.calmShown = true; state.locked = true;
later(showCalmMode, 1500);
}
}, dur);
}
/* ================= impact & effects ================= */
function impact(it, x, y) {
registerHit(it);
shakePhoto(it.shake);
if (it.screen && !state.reduce) shakeScreen();
ringFx(x, y, it.color);
popWord(it, x, y);
cutLabel(it, x, y);
FX[it.id](x, y);
playSound(it.id);
}
function shakePhoto(kind) {
const el = $('#frame');
if (state.reduce) { el.animate([{ opacity: 1 }, { opacity: .8 }, { opacity: 1 }], { duration: 180 }); return; }
const K = {
hit: [{ transform: 'none' }, { transform: 'translate(-12px,4px) rotate(-4deg)' }, { transform: 'translate(10px,-3px) rotate(3deg)' }, { transform: 'translate(-7px,2px) rotate(-2deg)' }, { transform: 'translate(3px,0) rotate(.8deg)' }, { transform: 'none' }],
sweep: [{ transform: 'none' }, { transform: 'translateX(-14px) skewX(-5deg)' }, { transform: 'translateX(10px) skewX(3deg)' }, { transform: 'translateX(-4px) skewX(-1deg)' }, { transform: 'none' }],
soft: [{ transform: 'none' }, { transform: 'scale(.95) translateY(5px)' }, { transform: 'scale(1.025)' }, { transform: 'none' }],
jiggle: [{ transform: 'none' }, { transform: 'translate(-5px,2px) rotate(-1.8deg)' }, { transform: 'translate(5px,-2px) rotate(1.6deg)' }, { transform: 'translate(-3px,1px) rotate(-.8deg)' }, { transform: 'none' }]
};
el.animate(K[kind] || K.jiggle, { duration: kind === 'soft' ? 420 : 460, easing: 'ease-out' });
}
function shakeScreen() {
$('#roomScreen').animate([{ transform: 'none' }, { transform: 'translate(-5px,3px)' }, { transform: 'translate(5px,-3px)' }, { transform: 'translate(-3px,2px)' }, { transform: 'translate(2px,-1px)' }, { transform: 'none' }], { duration: 300 });
}
function ringFx(x, y, color) {
const r = document.createElement('div'); r.className = 'ring-fx'; r.style.setProperty('--c', color);
fxLayer.appendChild(r);
const T = s => 'translate(' + x + 'px,' + y + 'px) translate(-50%,-50%) scale(' + s + ')';
const a = r.animate([{ transform: T(.3), opacity: 1 }, { transform: T(state.reduce ? 3 : 7), opacity: 0 }], { duration: state.reduce ? 250 : 520, easing: 'ease-out' });
a.onfinish = () => r.remove();
}
function burstSVG() {
const pts = [], n = 14;
for (let i = 0; i < n * 2; i++) {
const rr = i % 2 ? 40 : 64, a = i / (n * 2) * Math.PI * 2, j = 1 + rand(-.08, .08);
pts.push((75 + Math.cos(a) * rr * j).toFixed(1) + ',' + (75 + Math.sin(a) * rr * j).toFixed(1));
}
return '<svg viewBox="0 0 150 150"><polygon points="' + pts.join(' ') + '" fill="#ffe14a" stroke="#1a0f2e" stroke-width="4" stroke-linejoin="round"/></svg>';
}
function popWord(it, x, y) {
const rot = rand(-10, 10);
const T = (dy, s, r) => 'translate(' + x + 'px,' + (y + dy) + 'px) translate(-50%,-50%) rotate(' + r + 'deg) scale(' + s + ')';
if (it.burst) {
const bu = document.createElement('div'); bu.className = 'pop-burst'; bu.innerHTML = burstSVG(); fxLayer.appendChild(bu);
const a = bu.animate([{ transform: T(0, .2, rot), opacity: 0 }, { offset: .2, transform: T(0, 1.15, rot), opacity: 1 }, { offset: .7, transform: T(0, 1, rot), opacity: 1 }, { transform: T(-20, 1, rot), opacity: 0 }], { duration: state.reduce ? 500 : 900 });
a.onfinish = () => bu.remove();
}
const w = document.createElement('div'); w.className = 'pop-word'; w.textContent = tr(it.word); w.style.setProperty('--c', it.color);
if (it.burst) w.style.color = '#fff';
fxLayer.appendChild(w);
const a2 = w.animate([{ transform: T(0, .2, rot - 8), opacity: 0 }, { offset: .15, transform: T(0, 1.25, rot - 3), opacity: 1 }, { offset: .3, transform: T(0, 1, rot), opacity: 1 }, { offset: .72, transform: T(-8, 1, rot), opacity: 1 }, { transform: T(-60, 1, rot), opacity: 0 }], { duration: state.reduce ? 600 : 1100 });
a2.onfinish = () => w.remove();
}
function cutLabel(it, x, y) {
const c = document.createElement('div'); c.className = 'pop-cut'; c.textContent = '−' + bn(it.cut) + '%'; fxLayer.appendChild(c);
const a = c.animate([{ transform: 'translate(' + (x + 40) + 'px,' + (y + 50) + 'px) translate(-50%,-50%)', opacity: 0 }, { offset: .2, opacity: 1 }, { transform: 'translate(' + (x + 40) + 'px,' + (y - 30) + 'px) translate(-50%,-50%)', opacity: 0 }], { duration: state.reduce ? 500 : 1000 });
a.onfinish = () => c.remove();
}
function burst(x, y, type, n, o) {
o = o || {};
if (state.reduce) n = Math.max(2, Math.round(n * .35));
for (let i = 0; i < n; i++) {
if (state.pc >= 170) return;
const p = document.createElement('i'); p.className = 'pt pt-' + type;
let w = rand(o.min || 6, o.max || 14), h = w;
const ang = o.angle != null ? o.angle + rand(-(o.spread || .6), (o.spread || .6)) : rand(0, Math.PI * 2);
if (type === 'line') { w = rand(16, 34); h = 3; }
p.style.width = w + 'px'; p.style.height = h + 'px';
if (o.colors) p.style.background = pick(o.colors);
fxLayer.appendChild(p); state.pc++;
const dist = rand(o.dmin || 40, o.dmax || 130), dx = Math.cos(ang) * dist, dy = Math.sin(ang) * dist, g = o.g != null ? o.g : 50;
const rot = type === 'line' ? ang * 180 / Math.PI : rand(0, 360);
const dur = (state.reduce ? .4 : 1) * rand(500, 950);
const T = (px, py, r, s) => 'translate(' + px + 'px,' + py + 'px) translate(-50%,-50%) rotate(' + r + 'deg) scale(' + s + ')';
const a = p.animate([
{ transform: T(x, y, rot, .3), opacity: 1 },
{ offset: .55, transform: T(x + dx, y + dy, type === 'line' ? rot : rot + rand(-120, 120), 1), opacity: 1 },
{ transform: T(x + dx * 1.2, y + dy * 1.2 + g, rot, .2), opacity: 0 }
], { duration: dur, easing: 'cubic-bezier(.15,.7,.3,1)' });
a.onfinish = () => { p.remove(); state.pc = Math.max(0, state.pc - 1); };
}
}
function motionLines(x, y) {
if (state.reduce) return;
for (let i = 0; i < 6; i++) {
const l = document.createElement('i'); l.className = 'mline'; const w = rand(60, 130); l.style.width = w + 'px'; fxLayer.appendChild(l);
const oy = y + rand(-55, 55);
const a = l.animate([{ transform: 'translate(' + (x - w - 40) + 'px,' + oy + 'px)', opacity: 0 }, { offset: .3, opacity: .95 }, { transform: 'translate(' + (x + 70) + 'px,' + oy + 'px)', opacity: 0 }], { duration: rand(280, 420), delay: i * 25, easing: 'ease-out', fill: 'backwards' });
a.onfinish = () => l.remove();
}
}
/* ----- overlays on the photo ----- */
function blobPath(cx, cy, r, n, j) {
const pts = [];
for (let i = 0; i < n; i++) { const a = i / n * Math.PI * 2, rr = r * (1 - j / 2 + Math.random() * j); pts.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]); }
const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2], f = v => v.toFixed(1);
const m0 = mid(pts[n - 1], pts[0]); let d = 'M' + f(m0[0]) + ' ' + f(m0[1]);
for (let i = 0; i < n; i++) { const p = pts[i], q = pts[(i + 1) % n], m = mid(p, q); d += ' Q' + f(p[0]) + ' ' + f(p[1]) + ' ' + f(m[0]) + ' ' + f(m[1]); }
return d + 'Z';
}
function addOverlay(html, x, y, o) {
o = o || {};
const fx = $('#photoFx'), r = fx.getBoundingClientRect(); if (!r.width) return;
const lx = clamp(x - r.left, r.width * .12, r.width * .88), ly = clamp(y - r.top, r.height * .12, r.height * .88);
const size = o.size || Math.min(r.width, r.height) * .55;
const ov = document.createElement('div'); ov.className = 'ov';
ov.style.cssText = 'left:' + lx + 'px;top:' + ly + 'px;width:' + size + 'px;height:' + size + 'px';
const inn = document.createElement('div'); inn.className = 'ov-in'; inn.innerHTML = html; ov.appendChild(inn); fx.appendChild(ov);
const life = state.reduce ? 1500 : (o.life || 3800), rot = o.rot != null ? o.rot : rand(-25, 25), dr = o.drip || 14;
const R = 'rotate(' + rot + 'deg)';
const kf = o.squash ? [
{ offset: 0, transform: R + ' scale(.3,.3)' }, { offset: .04, transform: R + ' scale(1.3,.7)' }, { offset: .08, transform: R + ' scale(.88,1.15)' },
{ offset: .12, transform: R + ' scale(1.06,.94)' }, { offset: .16, transform: R + ' scale(1)' }, { offset: 1, transform: R + ' translateY(' + dr + 'px) scaleY(1.08)' }
] : [
{ offset: 0, transform: R + ' scale(.2)' }, { offset: .05, transform: R + ' scale(1.18)' }, { offset: .09, transform: R + ' scale(1)' },
{ offset: 1, transform: R + ' translateY(' + dr + 'px) scale(1)' }
];
inn.animate(kf, { duration: life + 700, easing: 'ease-out', fill: 'forwards' });
later(() => { ov.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 700, fill: 'forwards' }); }, life);
later(() => ov.remove(), life + 720);
}
function washFx(color, peak) {
const w = document.createElement('div'); w.className = 'wash'; w.style.background = color; $('#photoFx').appendChild(w);
const life = state.reduce ? 900 : 3000;
w.animate([{ opacity: 0 }, { opacity: peak, offset: .12 }, { opacity: peak * .6, offset: .6 }, { opacity: 0 }], { duration: life });
later(() => w.remove(), life + 50);
}
function eggSVG() {
return '<svg viewBox="0 0 160 160"><path d="' + blobPath(80, 80, 52, 10, .35) + '" fill="#fffdf4" stroke="#d9c9a0" stroke-width="3"/><path d="' + blobPath(32, 40, 16, 7, .3) + '" fill="#fffdf4" stroke="#d9c9a0" stroke-width="2.5"/><path d="' + blobPath(130, 114, 13, 7, .3) + '" fill="#fffdf4" stroke="#d9c9a0" stroke-width="2.5"/><path d="M62 118 Q60 150 70 150 Q80 150 78 118Z" fill="#fffdf4" stroke="#d9c9a0" stroke-width="2.5"/><path d="M102 116 Q100 138 107 138 Q114 138 112 116Z" fill="#fffdf4" stroke="#d9c9a0" stroke-width="2.5"/><circle cx="82" cy="78" r="22" fill="url(#gYolk)" stroke="#d98a00" stroke-width="3"/><ellipse cx="74" cy="70" rx="6" ry="4" fill="#fff" opacity=".75"/><path d="M18 112 l12 -7 4 12z M132 38 l9 8 -11 5z" fill="#f1dfc0" stroke="#b79a6a" stroke-width="2"/></svg>';
}
function paintSVG() {
const cols = ['#ff3b8d', '#35c7ff', '#ffd93b', '#7cff4f', '#a24bff', '#ff7a2e'].sort(() => Math.random() - .5);
let s = '<svg viewBox="0 0 200 200">';
for (let i = 0; i < 3; i++) s += '<path d="' + blobPath(rand(60, 140), rand(60, 140), rand(32, 50), 9, .4) + '" fill="' + cols[i] + '" stroke="rgba(0,0,0,.25)" stroke-width="2.5"/>';
for (let i = 0; i < 9; i++) s += '<circle cx="' + rand(10, 190).toFixed(0) + '" cy="' + rand(10, 190).toFixed(0) + '" r="' + rand(3, 9).toFixed(1) + '" fill="' + pick(cols) + '"/>';
return s + '</svg>';
}
function slimeSVG() {
let s = '<svg viewBox="0 0 180 200"><path d="' + blobPath(90, 70, 58, 10, .3) + '" fill="url(#gSlime)" stroke="#1c7a22" stroke-width="3"/>';
[48, 76, 104, 130].forEach(x => {
const len = rand(40, 100);
s += '<rect x="' + (x - 7) + '" y="80" width="14" height="' + len.toFixed(0) + '" rx="7" fill="url(#gSlime)" stroke="#1c7a22" stroke-width="2.5"/>';
});
s += '<ellipse cx="68" cy="48" rx="16" ry="8" fill="#fff" opacity=".6"/><circle cx="116" cy="60" r="5" fill="#fff" opacity=".6"/><circle cx="52" cy="90" r="4" fill="#fff" opacity=".5"/></svg>';
return s;
}
function poopStickerSVG() {
return '<svg viewBox="0 0 160 160" class="stk"><path d="' + blobPath(80, 86, 66, 12, .3) + '" fill="#8a5527" stroke="#3b200b" stroke-width="3"/><path d="M50 128 Q48 152 57 152 Q66 152 64 128Z M100 126 Q98 146 105 146 Q112 146 110 126Z" fill="#8a5527" stroke="#3b200b" stroke-width="3"/><svg x="28" y="24" width="104" height="104" viewBox="0 0 100 100">' + I.poop + '</svg><path d="M40 20 q-6 -8 0 -14 q6 -6 0 -12 M80 16 q-6 -8 0 -14" stroke="#7cd13a" stroke-width="4" fill="none" stroke-linecap="round" transform="translate(0,12)"/></svg>';
}
function dentSVG() {
return '<svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="46" fill="url(#gDent)"/><path d="M60 8 V22 M60 98 V112 M8 60 H22 M98 60 H112 M24 24 L33 33 M96 96 L87 87 M96 24 L87 33 M24 96 L33 87" stroke="#fff" stroke-width="5" stroke-linecap="round"/><path d="M30 74 A32 32 0 0 0 80 90" stroke="#fff" stroke-width="4" opacity=".55" fill="none" stroke-linecap="round"/></svg>';
}
function waterFx(x, y) {
washFx('linear-gradient(180deg,rgba(80,170,255,.55),rgba(40,120,230,.3))', .75);
const fx = $('#photoFx'), r = fx.getBoundingClientRect(), lx = clamp(x - r.left, 0, r.width), ly = clamp(y - r.top, 0, r.height);
const n = state.reduce ? 4 : 14;
for (let i = 0; i < n; i++) {
const d = document.createElement('i'); d.className = 'wdrop';
d.style.left = rand(4, 94).toFixed(0) + '%'; d.style.top = rand(0, 55).toFixed(0) + '%'; fx.appendChild(d);
const dur = rand(1400, 2600);
d.animate([{ opacity: 0, transform: 'translateY(0)' }, { opacity: 1, offset: .15 }, { opacity: 0, transform: 'translateY(' + rand(40, 110).toFixed(0) + 'px)' }], { duration: dur, delay: rand(0, 300), fill: 'backwards' });
later(() => d.remove(), dur + 400);
}
for (let i = 0; i < (state.reduce ? 1 : 3); i++) {
const rg = document.createElement('i'); rg.className = 'wring'; rg.style.left = lx + 'px'; rg.style.top = ly + 'px'; fx.appendChild(rg);
rg.animate([{ transform: 'translate(-50%,-50%) scale(.2)', opacity: .95 }, { transform: 'translate(-50%,-50%) scale(5)', opacity: 0 }], { duration: 1000, delay: i * 220, fill: 'backwards', easing: 'ease-out' });
later(() => rg.remove(), 1400 + i * 220);
}
burst(x, y, 'water', 16, { dmin: 50, dmax: 150, g: 90 });
}
const FX = {
stick(x, y) { burst(x, y, 'star', 8, { dmin: 50, dmax: 130, min: 12, max: 20 }); burst(x, y, 'line', 12, { dmin: 50, dmax: 100 }); burst(x, y, 'dust', 8, { dmax: 90 }); },
broom(x, y) { motionLines(x, y); burst(x, y, 'dust', 14, { dmin: 40, dmax: 120, min: 8, max: 18 }); burst(x, y, 'smoke', 3, { min: 30, max: 50, dmax: 60 }); },
shoe(x, y) { burst(x, y, 'dust', 12, { dmin: 40, dmax: 120, min: 8, max: 18 }); burst(x, y, 'star', 5, { min: 12, max: 18 }); burst(x, y, 'line', 8, { dmin: 40, dmax: 90 }); },
egg(x, y) { addOverlay(eggSVG(), x, y, { drip: 26 }); burst(x, y, 'paint', 10, { colors: ['#fffdf4', '#ffe36a', '#fff3b0'], dmin: 40, dmax: 120, g: 80 }); },
paint(x, y) { washFx('rgba(255,90,170,.28)', .8); addOverlay(paintSVG(), x, y, { size: Math.min($('#photoFx').getBoundingClientRect().width, 300) * .75, drip: 18 }); burst(x, y, 'paint', 18, { colors: ['#ff3b8d', '#35c7ff', '#ffd93b', '#7cff4f', '#a24bff'], dmin: 50, dmax: 150, g: 80 }); },
water(x, y) { waterFx(x, y); },
can(x, y) { addOverlay(dentSVG(), x, y, { size: 150, rot: 0, drip: 0, life: 3000 }); burst(x, y, 'star', 7, { min: 12, max: 20 }); burst(x, y, 'line', 8, { dmin: 40, dmax: 90 }); burst(x, y, 'dust', 6); },
pillow(x, y) { burst(x, y, 'fluff', 18, { dmin: 40, dmax: 140, min: 8, max: 20, g: 30 }); burst(x, y, 'smoke', 4, { min: 30, max: 54, dmax: 60 }); },
slime(x, y) { addOverlay(slimeSVG(), x, y, { squash: 1, drip: 30, size: Math.min($('#photoFx').getBoundingClientRect().width, 300) * .6, rot: rand(-10, 10) }); burst(x, y, 'slime', 14, { dmin: 40, dmax: 130, g: 90 }); },
poop(x, y) { addOverlay(poopStickerSVG(), x, y, { size: Math.min($('#photoFx').getBoundingClientRect().width, 300) * .55, drip: 10, rot: rand(-18, 18) }); burst(x, y, 'brown', 14, { dmin: 50, dmax: 140, g: 90 }); burst(x, y, 'star', 4, { min: 12, max: 18 }); }
};
/* ================= meter & combo ================= */
function statusText(v) {
if (v >= 75) return 'রাগ অনেক বেশি';
if (v >= 50) return 'রাগ এখনও আছে';
if (v >= 25) return 'আপনি ধীরে ধীরে শান্ত হচ্ছেন';
if (v > 0) return 'প্রায় শান্ত';
return 'আপনি শান্ত';
}
function updateAngerMeter(v, instant) {
const from = state.shown; state.shown = v;
const hue = Math.round((1 - v / 100) * 130);
const fill = $('#barFill');
fill.style.width = v + '%';
fill.style.background = 'linear-gradient(90deg,hsl(' + hue + ',90%,52%),hsl(' + (hue + 25) + ',95%,62%))';
$('#meterNum').style.color = 'hsl(' + hue + ',90%,68%)';
$('#bar').setAttribute('aria-valuenow', v);
$('#meterStatus').textContent = tr(statusText(v));
const tok = ++meterTok, t0 = performance.now(), dur = (instant || state.reduce) ? 0 : 600;
const set = n => { $('#meterNum').textContent = bn(n) + '%'; };
if (!dur) { set(v); return; }
(function step(now) {
if (tok !== meterTok) return;
const k = Math.min(1, (now - t0) / dur);
set(Math.round(from + (v - from) * (1 - Math.pow(1 - k, 3))));
if (k < 1) requestAnimationFrame(step);
})(t0);
}
function updateCombo() {
state.combo++;
clearTimeout(comboTimer);
comboTimer = setTimeout(() => { state.combo = 0; $('#comboBadge').hidden = true; }, 4500);
const n = state.combo, badge = $('#comboBadge');
if (n >= 2) {
badge.hidden = false; badge.textContent = comboText(n);
if (!state.reduce) badge.animate([{ transform: 'scale(.7)' }, { transform: 'scale(1.18)' }, { transform: 'scale(1)' }], { duration: 300 });
}
if (n === 3 || n === 5 || n === 10 || (n > 10 && n % 5 === 0)) comboPopup(n);
}
function comboPopup(n) {
const sr = $('#stage').getBoundingClientRect();
const x = sr.left + sr.width / 2, y = sr.top + Math.min(90, sr.height * .22);
const el = document.createElement('div'); el.className = 'combo-pop'; el.textContent = comboText(n); fxLayer.appendChild(el);
const T = (dy, s) => 'translate(' + x + 'px,' + (y + dy) + 'px) translate(-50%,-50%) scale(' + s + ')';
const a = el.animate([{ transform: T(20, .3), opacity: 0 }, { offset: .2, transform: T(0, 1.3), opacity: 1 }, { offset: .35, transform: T(0, 1), opacity: 1 }, { offset: .8, transform: T(-6, 1), opacity: 1 }, { transform: T(-50, 1), opacity: 0 }], { duration: state.reduce ? 800 : 1500 });
a.onfinish = () => el.remove();
burst(x, y, 'star', 14, { dmin: 70, dmax: 180, min: 12, max: 22, g: 70 });
if (!state.reduce) $('#stage').animate([{ boxShadow: 'inset 0 0 90px rgba(0,0,0,.65)' }, { boxShadow: 'inset 0 0 120px rgba(255,190,60,.6)' }, { boxShadow: 'inset 0 0 90px rgba(0,0,0,.65)' }], { duration: 700 });
tone({ type: 'sine', f0: 520, f1: 1040, dur: .22, gain: .22 }, true);
}
/* ================= sound ================= */
function audioCtx() {
if (!state.audio) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; try { state.audio = new AC(); } catch (e) { return null; } }
if (state.audio.state === 'suspended') state.audio.resume();
return state.audio;
}
function noiseBuf(c) {
if (!state.noise) { const b = c.createBuffer(1, c.sampleRate, c.sampleRate), d = b.getChannelData(0); for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; state.noise = b; }
return state.noise;
}
function env(g, t, peak, dur) { g.gain.setValueAtTime(.0001, t); g.gain.exponentialRampToValueAtTime(peak, t + .012); g.gain.exponentialRampToValueAtTime(.0001, t + dur); }
function tone(o, force) {
if (!state.sound && !force) return; if (!state.sound) return;
const c = audioCtx(); if (!c) return; const t = c.currentTime + (o.delay || 0);
const osc = c.createOscillator(), g = c.createGain();
osc.type = o.type || 'sine'; osc.frequency.setValueAtTime(o.f0, t); osc.frequency.exponentialRampToValueAtTime(Math.max(20, o.f1), t + o.dur);
env(g, t, o.gain || .3, o.dur); osc.connect(g); g.connect(c.destination); osc.start(t); osc.stop(t + o.dur + .05);
}
function noise(o) {
if (!state.sound) return;
const c = audioCtx(); if (!c) return; const t = c.currentTime + (o.delay || 0);
const s = c.createBufferSource(); s.buffer = noiseBuf(c);
const f = c.createBiquadFilter(); f.type = o.ftype || 'lowpass'; f.Q.value = o.q || 1;
f.frequency.setValueAtTime(o.f0, t); f.frequency.exponentialRampToValueAtTime(Math.max(40, o.f1), t + o.dur);
const g = c.createGain(); env(g, t, o.gain || .3, o.dur);
s.connect(f); f.connect(g); g.connect(c.destination); s.start(t); s.stop(t + o.dur + .05);
}
const SND = {
stick() { tone({ f0: 180, f1: 55, dur: .26, gain: .5 }); noise({ f0: 700, f1: 200, dur: .12, gain: .2 }); },
broom() { noise({ ftype: 'bandpass', f0: 400, f1: 2600, dur: .36, gain: .3, q: 2 }); },
shoe() { tone({ f0: 150, f1: 65, dur: .2, gain: .45 }); noise({ f0: 500, f1: 150, dur: .1, gain: .15 }); },
egg() { noise({ ftype: 'bandpass', f0: 1000, f1: 300, dur: .2, gain: .3, q: 1.5 }); tone({ f0: 230, f1: 90, dur: .14, gain: .25 }); },
paint() { noise({ ftype: 'highpass', f0: 1800, f1: 3400, dur: .24, gain: .22 }); tone({ f0: 520, f1: 300, dur: .1, gain: .15 }); },
water() { noise({ f0: 1800, f1: 500, dur: .35, gain: .25 }); [0, .08, .16, .26].forEach((d, i) => tone({ f0: 380 + i * 120, f1: 800 + i * 140, dur: .1, gain: .15, delay: d })); },
can() { tone({ type: 'triangle', f0: 540, f1: 280, dur: .32, gain: .3 }); tone({ type: 'triangle', f0: 820, f1: 420, dur: .26, gain: .18 }); noise({ f0: 3000, f1: 800, dur: .1, gain: .12 }); },
pillow() { noise({ f0: 600, f1: 200, dur: .22, gain: .3 }); },
slime() { tone({ f0: 320, f1: 140, dur: .18, gain: .3 }); tone({ f0: 200, f1: 360, dur: .2, gain: .28, delay: .12 }); noise({ f0: 900, f1: 300, dur: .2, gain: .1, delay: .05 }); },
poop() { tone({ type: 'sawtooth', f0: 760, f1: 170, dur: .45, gain: .16 }); tone({ type: 'square', f0: 130, f1: 80, dur: .16, gain: .22, delay: .42 }); noise({ f0: 900, f1: 250, dur: .18, gain: .22, delay: .42 }); }
};
function playSound(id) { if (!state.sound) return; try { SND[id](); } catch (e) {} }
function setSound(on) {
state.sound = on;
if (on) { audioCtx(); tone({ f0: 440, f1: 660, dur: .15, gain: .2 }); }
$('#soundLabel').textContent = tr(on ? 'শব্দ বন্ধ' : 'শব্দ চালু');
$('#soundIco').textContent = on ? '🔊' : '🔇';
$('#soundBtn').setAttribute('aria-pressed', on ? 'true' : 'false');
$('#setSound').checked = on;
}
function setReduce(on) {
state.reduce = on; document.documentElement.classList.toggle('reduce', on); $('#setReduce').checked = on;
}
/* ================= calm / breathing / reflection ================= */
function showCalmMode() {
if (state.audio && state.sound) { [523, 659, 784].forEach((f, i) => tone({ f0: f, f1: f * 1.01, dur: .6, gain: .18, delay: i * .18 })); }
openModal('calmModal');
}
function startBreathingExercise() {
closeModal('calmModal');
showScreen('breathScreen');
$('#breathDone').hidden = true; $('#breathNext').hidden = true; $('#breathSkip').hidden = false;
const circle = $('#breathCircle'), prog = $('#ringProg'), phaseEl = $('#breathPhase'), leftEl = $('#breathLeft');
circle.style.transition = 'none'; circle.style.transform = 'scale(.62)'; prog.style.strokeDashoffset = '590.6';
cancelAnimationFrame(B.raf); B.phase = ''; B.start = performance.now();
const TOTAL = 30000, CYC = 12000;
const step = now => {
const t = Math.min(now - B.start, TOTAL), c = t % CYC;
let ph, txt, sc;
if (c < 4000) { ph = 'in'; txt = 'শ্বাস নিন'; sc = lerp(.62, 1, ease(c / 4000)); }
else if (c < 6000) { ph = 'hold'; txt = 'ধরে রাখুন'; sc = 1; }
else { ph = 'out'; txt = 'শ্বাস ছাড়ুন'; sc = lerp(1, .62, ease((c - 6000) / 6000)); }
if (t >= TOTAL) {
phaseEl.textContent = tr('শান্ত থাকুন'); leftEl.textContent = '';
circle.style.transition = 'transform 1.2s ease'; circle.style.transform = 'scale(.85)';
prog.style.strokeDashoffset = '0';
$('#breathDone').hidden = false; $('#breathNext').hidden = false; $('#breathSkip').hidden = true;
setTimeout(() => { try { $('#breathNext').focus(); } catch (e) {} }, 60);
return;
}
if (ph !== B.phase) { B.phase = ph; phaseEl.textContent = tr(txt); }
circle.style.transform = 'scale(' + sc.toFixed(3) + ')';
prog.style.strokeDashoffset = (590.6 * (1 - t / TOTAL)).toFixed(1);
leftEl.textContent = leftText(Math.ceil((TOTAL - t) / 1000));
B.raf = requestAnimationFrame(step);
};
B.raf = requestAnimationFrame(step);
}
function showCheck() {
cancelAnimationFrame(B.raf);
showScreen('checkScreen');
}
/* ================= reset ================= */
function resetSession(o) {
o = o || {};
clearTimers(); clearTimeout(comboTimer); cancelAnimationFrame(B.raf); meterTok++;
fxLayer.innerHTML = ''; $('#photoFx').innerHTML = ''; state.pc = 0;
$$('.ghost').forEach(g => g.remove()); $$('.item.dragging').forEach(b => b.classList.remove('dragging'));
state.anger = 100; state.shown = 100; state.combo = 0; state.selected = null; state.locked = false; state.calmShown = false;
if (!o.keepStats) { state.hits = {}; state.total = 0; }
renderStats();
$('#comboBadge').hidden = true;
$$('.item').forEach(b => { b.classList.remove('selected'); b.setAttribute('aria-pressed', 'false'); });
$('#frame').classList.remove('armed', 'drop-ready');
updateAngerMeter(100, true); updateHint();
closeAllModals();
if (!o.keepImage) clearImage();
}
/* ================= usage tally & summary ================= */
function registerHit(it) {
state.hits[it.id] = (state.hits[it.id] || 0) + 1; state.total++;
renderStats();
}
function renderStats() {
$$('.item').forEach(b => {
const n = state.hits[b.dataset.id] || 0, c = b.querySelector('.cnt');
if (c) c.textContent = n ? bn(n) : '';
});
const list = $('#tallyList'), tot = $('#tallyTotal'); if (!list) return;
list.innerHTML = '';
const rows = ITEMS.filter(i => state.hits[i.id]).sort((a, b) => state.hits[b.id] - state.hits[a.id]);
const max = rows.length ? state.hits[rows[0].id] : 1;
if (!rows.length) { const li = document.createElement('li'); li.className = 'tally-empty'; li.textContent = tr('এখনও কোনো জিনিস ব্যবহার করা হয়নি।'); list.appendChild(li); }
rows.forEach(i => {
const li = document.createElement('li'), n = state.hits[i.id];
const a = document.createElement('span'); a.textContent = tr(i.name);
const b = document.createElement('span'); b.textContent = countText(n);
const m = document.createElement('div'); m.className = 'mini';
const f = document.createElement('i'); f.style.width = (n / max * 100) + '%'; f.style.background = i.color; m.appendChild(f);
li.append(a, b, m); list.appendChild(li);
});
tot.textContent = state.total ? tr('মোট') + ': ' + countText(state.total) : '';
}
function rrect(x, px, py, w, h, r) {
x.beginPath(); x.moveTo(px + r, py); x.arcTo(px + w, py, px + w, py + h, r); x.arcTo(px + w, py + h, px, py + h, r);
x.arcTo(px, py + h, px, py, r); x.arcTo(px, py, px + w, py, r); x.closePath();
}
async function downloadSummary() {
const img = $('#targetImg');
if (!state.total || !state.imageURL || !img.naturalWidth) { toast('এখনও কোনো জিনিস ব্যবহার করা হয়নি।', 'info'); return; }
try { await document.fonts.load('700 30px "Noto Sans Bengali"'); } catch (e) {}
try {
const FONT = '"Noto Sans Bengali","Hind Siliguri","Kalpurush",system-ui,sans-serif';
const W = 1080, P = 52, nw = img.naturalWidth, nh = img.naturalHeight;
const sc = Math.min((W - 2 * P - 24) / nw, 560 / nh, 1.5), pw = Math.round(nw * sc), ph = Math.round(nh * sc);
const rows = ITEMS.filter(i => state.hits[i.id]).sort((a, b) => state.hits[b.id] - state.hits[a.id]);
const photoY = 150, rowH = 66;
const listY = photoY + ph + 24 + 70;
const H = listY + 30 + rows.length * rowH + 40 + 56 + 130;
const c = document.createElement('canvas'); c.width = W; c.height = H; const x = c.getContext('2d');
const bg = x.createLinearGradient(0, 0, W, H); bg.addColorStop(0, '#2a1260'); bg.addColorStop(.55, '#120a2a'); bg.addColorStop(1, '#07060f');
x.fillStyle = bg; x.fillRect(0, 0, W, H);
const glow = x.createRadialGradient(W * .8, 0, 10, W * .8, 0, 520); glow.addColorStop(0, 'rgba(255,77,141,.35)'); glow.addColorStop(1, 'rgba(255,77,141,0)');
x.fillStyle = glow; x.fillRect(0, 0, W, H);
x.textBaseline = 'alphabetic'; x.textAlign = 'left';
x.fillStyle = '#fff'; x.font = '800 44px ' + FONT; x.fillText(tr('রাগের ঘর – সারাংশ'), P, 84);
x.fillStyle = '#bdb7dc'; x.font = '500 24px ' + FONT;
x.fillText(new Date().toLocaleDateString(state.lang === 'en' ? 'en-GB' : 'bn-BD', { year: 'numeric', month: 'long', day: 'numeric' }), P, 122);
// photo frame
const fx = Math.round((W - pw) / 2) - 12;
const fg = x.createLinearGradient(fx, photoY, fx + pw, photoY + ph); fg.addColorStop(0, '#6d4cff'); fg.addColorStop(.55, '#ff4d8d'); fg.addColorStop(1, '#ffd23f');
x.save(); x.shadowColor = 'rgba(139,92,255,.6)'; x.shadowBlur = 40; x.fillStyle = fg; rrect(x, fx, photoY, pw + 24, ph + 24, 22); x.fill(); x.restore();
x.save(); rrect(x, fx + 12, photoY + 12, pw, ph, 12); x.clip(); x.drawImage(img, fx + 12, photoY + 12, pw, ph); x.restore();
// list
x.fillStyle = '#fff'; x.font = '800 36px ' + FONT; x.fillText(tr('জিনিস ব্যবহারের হিসাব'), P, listY);
const max = state.hits[rows[0].id], barX = P + 270, barW = W - P - barX - 190;
rows.forEach((it, i) => {
const cy = listY + 30 + i * rowH + rowH / 2, n = state.hits[it.id];
x.textBaseline = 'middle'; x.textAlign = 'left'; x.fillStyle = '#f5f2ff'; x.font = '700 30px ' + FONT; x.fillText(tr(it.name), P, cy);
x.fillStyle = 'rgba(255,255,255,.12)'; rrect(x, barX, cy - 11, barW, 22, 11); x.fill();
x.fillStyle = it.color; rrect(x, barX, cy - 11, Math.max(22, barW * n / max), 22, 11); x.fill();
x.textAlign = 'right'; x.fillStyle = '#fff'; x.font = '800 30px ' + FONT; x.fillText(countText(n), W - P, cy);
});
const ty = listY + 30 + rows.length * rowH + 40;
x.strokeStyle = 'rgba(255,255,255,.25)'; x.lineWidth = 2; x.beginPath(); x.moveTo(P, ty - 28); x.lineTo(W - P, ty - 28); x.stroke();
x.textBaseline = 'middle'; x.textAlign = 'left'; x.fillStyle = '#ffd23f'; x.font = '800 38px ' + FONT; x.fillText(tr('মোট'), P, ty + 8);
x.textAlign = 'right'; x.fillText(countText(state.total), W - P, ty + 8);
// footer
x.textAlign = 'center'; x.fillStyle = '#bdb7dc'; x.font = '500 24px ' + FONT;
x.fillText(tr('এটি ছিল একটি ভার্চুয়াল অভিজ্ঞতা।'), W / 2, H - 78);
x.fillStyle = '#fff'; x.font = '700 26px ' + FONT;
x.fillText(tr('ডেভেলপ করেছেন') + ' ' + tr('সালাম ভুঁইয়া আকাশ'), W / 2, H - 38);
c.toBlob(b => {
if (!b) { toast('সারাংশ তৈরি করা যায়নি।', 'error'); return; }
const url = URL.createObjectURL(b), a = document.createElement('a');
a.href = url; a.download = 'anger-room-summary.png'; document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 4000);
toast('সারাংশ ডাউনলোড হয়েছে।', 'success');
}, 'image/png');
} catch (e) { toast('সারাংশ তৈরি করা যায়নি।', 'error'); }
}
/* ================= language ================= */
const LANG_ATTRS = ['aria-label', 'placeholder', 'alt', 'title'];
function translateTree() {
const en = state.lang === 'en';
const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: n => (n.parentNode && /^(SCRIPT|STYLE)$/.test(n.parentNode.nodeName)) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
});
const nodes = []; while (w.nextNode()) nodes.push(w.currentNode);
nodes.forEach(n => {
if (!en) { if (n._bn !== undefined) { n.nodeValue = n._bn; delete n._bn; } return; }
const raw = n.nodeValue, key = raw.trim(); if (!key) return;
const e = EN[key]; if (e === undefined) return;
n._bn = raw; n.nodeValue = raw.replace(key, () => e);
});
$$('[aria-label],[placeholder],[alt],[title]').forEach(el => LANG_ATTRS.forEach(a => {
if (!el.hasAttribute(a)) return;
const k = '_bn_' + a;
if (!en) { if (el[k] !== undefined) { el.setAttribute(a, el[k]); delete el[k]; } return; }
const v = el.getAttribute(a), e = EN[v];
if (e !== undefined) { el[k] = v; el.setAttribute(a, e); }
}));
}
function applyLang() {
const en = state.lang === 'en';
translateTree();
document.documentElement.lang = en ? 'en' : 'bn';
document.title = tr('রাগের ঘর – নিরাপদ ভার্চুয়াল পরিবেশে রাগ প্রকাশ');
$$('.item').forEach(b => {
const it = byId(b.dataset.id);
b.setAttribute('aria-label', en ? 'Select ' + tr(it.name) : it.name + ' নির্বাচন করুন');
});
updateAngerMeter(state.shown, true); updateHint(); setSound(state.sound); renderStats();
const cb = $('#comboBadge'); if (!cb.hidden) cb.textContent = comboText(state.combo);
B.phase = '';
if (!$('#breathDone').hidden) $('#breathPhase').textContent = tr('শান্ত থাকুন');
}
function setLang(l) {
state.lang = l === 'en' ? 'en' : 'bn';
try { localStorage.setItem('ragerGhorLang', state.lang); } catch (e) {}
$$('.lang-btn').forEach(b => b.setAttribute('aria-pressed', b.dataset.lang === state.lang ? 'true' : 'false'));
applyLang();
}
/* ================= init ================= */
function initApp() {
buildItems();
// floating items on the landing page
const fl = $('#floaters');
[['egg', '5%', '12%', 70, 0, -10], ['shoe', '80%', '8%', 84, -2, 14], ['broom', '8%', '66%', 84, -4, -20], ['slime', '82%', '62%', 76, -1, 8], ['pillow', '44%', '90%', 76, -3, -6], ['stick', '88%', '34%', 70, -5, 20], ['can', '2%', '38%', 62, -2.5, 10]].forEach(a => {
const d = document.createElement('div'); d.className = 'floater';
d.style.cssText = 'left:' + a[1] + ';top:' + a[2] + ';--s:' + a[3] + 'px;--d:' + a[4] + 's;--r0:' + a[5] + 'deg';
d.innerHTML = svg(I[a[0]]); fl.appendChild(d);
});
// floating motes in the room
const mo = $('#motes');
for (let i = 0; i < 16; i++) {
const m = document.createElement('i'); m.className = 'mote'; const s = rand(4, 12);
m.style.cssText = 'left:' + rand(2, 98).toFixed(0) + '%;width:' + s + 'px;height:' + s + 'px;animation-duration:' + rand(9, 18).toFixed(1) + 's;animation-delay:' + (-rand(0, 14)).toFixed(1) + 's';
mo.appendChild(m);
}
// reduced motion
const mq = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)');
setReduce(!!(mq && mq.matches)); setSound(false);
// landing
$('#enterBtn').addEventListener('click', () => showScreen('uploadScreen'));
$('#landingScreen').addEventListener('pointermove', e => {
if (state.reduce || e.pointerType === 'touch') return;
const x = e.clientX / innerWidth - .5, y = e.clientY / innerHeight - .5;
$('#hero').style.transform = 'perspective(1000px) rotateY(' + (x * 6).toFixed(2) + 'deg) rotateX(' + (-y * 5).toFixed(2) + 'deg)';
});
$$('[data-open]').forEach(b => b.addEventListener('click', () => openModal(b.dataset.open)));
$$('[data-close]').forEach(b => b.addEventListener('click', () => closeModal(b.dataset.close)));
$$('.modal-back').forEach(m => m.addEventListener('click', e => { if (e.target === m && !m.dataset.persistent) closeModal(m.id); }));
// upload
const fi = $('#fileInput'), dz = $('#dropzone');
$('#uploadBack').addEventListener('click', () => showScreen('landingScreen'));
dz.addEventListener('click', () => fi.click());
fi.addEventListener('change', () => { handleImageUpload(fi.files && fi.files[0]); fi.value = ''; });
['dragenter', 'dragover'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.add('dragover'); }));
['dragleave', 'drop'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.remove('dragover'); }));
dz.addEventListener('drop', e => { const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; handleImageUpload(f); });
window.addEventListener('dragover', e => e.preventDefault());
window.addEventListener('drop', e => e.preventDefault());
$('#changeBtn').addEventListener('click', () => fi.click());
$('#removeBtn').addEventListener('click', () => { clearImage(); toast('ছবি মুছে ফেলা হয়েছে।', 'info'); });
$('#startRoomBtn').addEventListener('click', startAngerRoom);
// room
const frame = $('#frame');
frame.addEventListener('click', e => {
if (e.detail === 0) { const r = frame.getBoundingClientRect(); handleTargetInteraction(r.left + r.width / 2, r.top + r.height / 2, false); }
else handleTargetInteraction(e.clientX, e.clientY, false);
});
frame.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const r = frame.getBoundingClientRect(); handleTargetInteraction(r.left + r.width / 2, r.top + r.height / 2, false); }
});
$('#soundBtn').addEventListener('click', () => setSound(!state.sound));
$('#settingsBtn').addEventListener('click', () => openModal('settingsModal'));
$('#resetDeskBtn').addEventListener('click', () => { resetSession(); showScreen('uploadScreen'); toast('সেশন নতুন করে শুরু হয়েছে।', 'info'); });
// settings
$('#setSound').addEventListener('change', e => setSound(e.target.checked));
$('#setReduce').addEventListener('change', e => setReduce(e.target.checked));
$('#setChangePhoto').addEventListener('click', () => { closeModal('settingsModal'); showScreen('uploadScreen'); });
$('#setReset').addEventListener('click', () => { resetSession(); showScreen('uploadScreen'); toast('সেশন নতুন করে শুরু হয়েছে।', 'info'); });
$('#setPrivacy').addEventListener('click', () => openModal('privacyModal'));
// calm / breathing / reflection / final
$('#calmStartBtn').addEventListener('click', startBreathingExercise);
$('#breathNext').addEventListener('click', showCheck);
$('#breathSkip').addEventListener('click', showCheck);
$('#rageYes').addEventListener('click', () => showScreen('playScreen'));
$('#rageNo').addEventListener('click', () => startAngerRoom(true));
$('#playYes').addEventListener('click', () => startAngerRoom(true));
$('#playNo').addEventListener('click', () => showScreen('finalScreen'));
['#dlDeskBtn', '#setDownload', '#dlFinalBtn'].forEach(id => $(id).addEventListener('click', downloadSummary));
$('#againBtn').addEventListener('click', startAngerRoom);
$('#newPhotoBtn').addEventListener('click', () => { resetSession(); showScreen('uploadScreen'); });
$('#endBtn').addEventListener('click', () => { resetSession(); showScreen('landingScreen'); toast('ধন্যবাদ। নিজের যত্ন নিন।', 'success'); });
// modal keyboard handling (Escape + focus trap)
document.addEventListener('keydown', e => {
const top = modalStack[modalStack.length - 1]; if (!top) return;
const m = $('#' + top);
if (e.key === 'Escape' && !m.dataset.persistent) { closeModal(top); return; }
if (e.key === 'Tab') {
const f = $$('button,input,[tabindex]:not([tabindex="-1"])', m).filter(n => !n.disabled && n.offsetParent !== null);
if (!f.length) return;
const first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
else if (!m.contains(document.activeElement)) { e.preventDefault(); first.focus(); }
}
});
window.addEventListener('beforeunload', () => { if (state.imageURL) URL.revokeObjectURL(state.imageURL); });
previewImage(); updateHint(); updateAngerMeter(100, true); renderStats();
// language switch
$$('.lang-btn').forEach(b => b.addEventListener('click', () => setLang(b.dataset.lang)));
let saved = 'bn'; try { saved = localStorage.getItem('ragerGhorLang') || 'bn'; } catch (e) {}
if (saved === 'en') setLang('en');
}
initApp();
})();
</script>
</body>
</html>