3v324v23 commited on
Commit
ffa4566
·
1 Parent(s): 684c930

Add classroom demo HTML for live lesson interaction with students

Browse files
Files changed (2) hide show
  1. static/classroom-demo.html +117 -0
  2. static/index.html +33 -13
static/classroom-demo.html ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="he" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>احكي · שיעור חי — הדגמת תפעול</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Assistant:wght@400;600;700&family=Frank+Ruhl+Libre:wght@500;700&display=swap');
9
+ :root{--ink:#182018;--muted:#687065;--paper:#f4f1e9;--surface:#fffdf7;--olive:#596b3d;--olive-dark:#354329;--soft:#e7ecdc;--line:#d9d8cb;--sumac:#9f3f32;--amber:#d68b22;--ok:#287451;--shadow:0 14px 34px rgba(40,48,35,.1)}
10
+ *{box-sizing:border-box} body{margin:0;background:var(--paper);color:var(--ink);font-family:Assistant,Arial,sans-serif;min-height:100vh}
11
+ button,input,select{font:inherit} button{cursor:pointer}.shell{max-width:1280px;margin:auto;padding:20px}
12
+ header{display:flex;align-items:center;gap:14px;margin-bottom:18px}.brand{font:700 27px/1 'Frank Ruhl Libre',serif}.brand small{display:block;color:var(--muted);font:600 12px/1.4 Assistant,sans-serif}.live{margin-inline-start:auto;display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:99px;background:#e3f1e8;color:var(--ok);font-weight:700}.live i{width:9px;height:9px;border-radius:50%;background:currentColor;box-shadow:0 0 0 5px rgba(40,116,81,.12)}.view-toggle{display:flex;gap:3px;padding:3px;border-radius:11px;background:#e5e5dc}.view-toggle button{border:0;border-radius:8px;padding:7px 11px;background:transparent;color:var(--muted);font-weight:700}.view-toggle button.active{background:#fff;color:var(--ink);box-shadow:0 1px 4px rgba(0,0,0,.12)}
13
+ .top{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;background:var(--olive-dark);color:#fff;padding:18px 20px;border-radius:18px;box-shadow:var(--shadow)}
14
+ .top h1{font:500 30px/1.1 'Frank Ruhl Libre',serif;margin:0}.top p{margin:6px 0 0;color:#d9e2cf}.session-access{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;align-items:center;padding:10px 13px;border-radius:12px;background:rgba(255,255,255,.1)}.session-access .check{grid-row:1/3;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:#dfead5;color:var(--olive-dark);font-weight:700}.session-access small{color:#d9e2cf}.session-access b{font-size:16px}
15
+ .layout{display:grid;grid-template-columns:330px minmax(0,1fr);gap:16px;margin-top:16px}.card{background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:0 6px 20px rgba(40,48,35,.05)}
16
+ .roster{padding:18px;align-self:start;position:sticky;top:16px}.section-head{display:flex;align-items:start;gap:10px;margin-bottom:14px}.section-head h2{margin:0;font:500 22px/1.2 'Frank Ruhl Libre',serif}.section-head p{margin:3px 0 0;color:var(--muted);font-size:13px}.count{margin-inline-start:auto;font-weight:700;background:var(--soft);color:var(--olive-dark);padding:5px 9px;border-radius:99px}
17
+ .students{display:flex;flex-direction:column;gap:7px}.student{display:grid;grid-template-columns:38px 1fr auto;gap:9px;align-items:center;padding:8px;border-radius:12px;transition:.25s}.student:hover{background:#f2f3ec}.avatar{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;background:#e8e4d9;font-weight:700;color:var(--muted)}.student.on .avatar{background:var(--soft);color:var(--olive-dark)}.student b{display:block}.student small{color:var(--muted)}.status{width:9px;height:9px;border-radius:50%;background:#c5c4bc}.student.on .status{background:var(--ok);box-shadow:0 0 0 4px #e3f1e8}.student.answering .status{background:var(--amber);box-shadow:0 0 0 4px #f9ecd7}
18
+ .main{display:flex;flex-direction:column;gap:16px}.composer{padding:20px}.steps{display:flex;gap:8px;margin-bottom:18px}.step{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:13px}.step b{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:#ecebe4}.step.active{color:var(--olive-dark);font-weight:700}.step.active b{background:var(--olive);color:#fff}.step+.step:before{content:"";width:24px;border-top:1px solid var(--line);margin-inline-end:2px}
19
+ .question-modes{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mode{border:1px solid var(--line);border-radius:14px;padding:13px;text-align:right;background:#fff;color:var(--ink)}.mode strong{display:block}.mode small{color:var(--muted)}.mode.active{border-color:var(--olive);background:var(--soft);box-shadow:inset 0 0 0 1px var(--olive)}
20
+ .field{margin-top:13px}.field label{display:block;margin-bottom:6px;font-weight:700;font-size:14px}.select,input{width:100%;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:11px;padding:11px 12px}.speech{display:none}.speech.visible{display:block}.speechbox{display:flex;gap:8px}.speechbox input{flex:1}.mic{border:0;border-radius:11px;background:var(--sumac);color:#fff;min-width:48px;font-size:20px}.mic.listening{animation:pulse 1s infinite}@keyframes pulse{50%{transform:scale(.94);box-shadow:0 0 0 7px rgba(159,63,50,.13)}}
21
+ .preview{margin-top:14px;padding:14px;border:1px dashed #b5b8a9;border-radius:13px;background:#faf9f3}.preview small{color:var(--muted)}.preview h3{margin:5px 0 1px;font-size:21px}.preview .arabic{font-size:19px;direction:rtl;color:var(--olive-dark)}.preview .pending{color:var(--amber);font-size:13px}.actions{display:flex;gap:8px;margin-top:16px}.btn{border:1px solid var(--line);border-radius:11px;padding:10px 15px;background:#fff;color:var(--ink);font-weight:700}.btn.primary{background:var(--olive);border-color:var(--olive);color:#fff}.btn:disabled{opacity:.45;cursor:not-allowed}
22
+ .results{padding:20px}.empty{padding:34px;text-align:center;color:var(--muted)}.empty span{display:block;font-size:38px;margin-bottom:7px}.question-live{display:flex;gap:12px;align-items:center;padding:14px;border-radius:14px;background:var(--olive-dark);color:#fff;margin-bottom:14px}.question-live .q{flex:1}.question-live small{display:block;color:#d9e2cf}.question-live strong{font-size:19px}.timer{font-weight:700;font-variant-numeric:tabular-nums;background:rgba(255,255,255,.12);padding:8px 11px;border-radius:10px}
23
+ table{width:100%;border-collapse:collapse}th{text-align:right;color:var(--muted);font-size:12px;padding:7px 9px}td{padding:10px 9px;border-top:1px solid var(--line);vertical-align:top}.answer{min-width:260px}.said-line{display:grid;grid-template-columns:3.7em 1fr;gap:8px;align-items:baseline;line-height:1.65}.said-line+.said-line{margin-top:2px}.said-line small{color:var(--muted);font-size:12px;font-weight:600}.translit{font-size:17px;font-weight:700;letter-spacing:.02em}.translit.expected{color:var(--olive-dark)}.translit.heard{color:var(--ink)}.expected-diff,.letter-diff{padding:1px 2px;border-radius:4px;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px}.expected-diff{background:#e4f1e8;color:var(--ok)}.letter-diff{background:#f8dcd5;color:var(--sumac)}.missing{color:var(--sumac);font-weight:700}.score{display:inline-grid;place-items:center;min-width:42px;height:30px;border-radius:9px;font-weight:700;background:#e4f1e8;color:var(--ok)}.score.mid{background:#faecd7;color:#955a0b}.score.low{background:#f6e4df;color:var(--sumac)}.wait{color:var(--muted)}.note{font-size:12px;color:var(--muted);margin:12px 0 0}.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%,20px);opacity:0;background:var(--ink);color:#fff;padding:10px 16px;border-radius:99px;transition:.25s;pointer-events:none}.toast.show{opacity:1;transform:translate(-50%,0)}
24
+ #studentView{display:none;max-width:620px;margin:42px auto}.student-view #studentView{display:block}.student-view .teacher-only{display:none}.student-shell{padding:28px;text-align:center}.student-shell .class-name{color:var(--muted);font-weight:700}.student-shell h1{font:500 31px/1.25 'Frank Ruhl Libre',serif;margin:22px 0 8px}.student-shell .instruction{color:var(--muted);margin:0 0 24px}.student-mic{display:grid;place-items:center;width:92px;height:92px;margin:20px auto;border:0;border-radius:50%;background:var(--sumac);color:#fff;font-size:35px;box-shadow:0 8px 0 #713128}.student-mic:active{transform:translateY(4px);box-shadow:0 4px 0 #713128}.waiting-ring{display:grid;place-items:center;width:76px;height:76px;margin:26px auto;border:7px solid var(--soft);border-top-color:var(--olive);border-radius:50%;animation:turn 1.5s linear infinite}@keyframes turn{to{transform:rotate(360deg)}}.student-score{font:700 48px/1 'Frank Ruhl Libre',serif;color:var(--ok);margin:12px 0 20px}.student-compare{max-width:460px;margin:0 auto;text-align:right;padding:14px;border-radius:13px;background:#f6f5ef}.student-actions{display:flex;justify-content:center;gap:8px;margin-top:18px}
25
+ @media(max-width:800px){.layout{grid-template-columns:1fr}.roster{position:static}.students{display:grid;grid-template-columns:repeat(2,1fr)}.question-modes{grid-template-columns:1fr}.top{grid-template-columns:1fr}.session-access{justify-self:start}}
26
+ @media(max-width:520px){.shell{padding:10px}header{flex-wrap:wrap}.live{margin-inline-start:0}.view-toggle{order:3;width:100%}.view-toggle button{flex:1}.students{grid-template-columns:1fr}.steps{overflow:auto}.step+.step:before{width:8px}.question-live{align-items:start}th:nth-child(3),td:nth-child(3){display:none}}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div class="shell">
31
+ <header><div class="brand">احكي · ihki<small id="viewName">הדגמת שיעור חי · מסך המדריך</small></div><div class="view-toggle"><button class="active" data-view="teacher">מסך המדריך</button><button data-view="student">מסך התלמיד</button></div><div class="live"><i></i> המפגש פתוח</div></header>
32
+ <section class="top teacher-only"><div><h1>שיעור 6 · קבוצת יום רביעי</h1><p>המדריך מכין שאלה, שולח אותה לתלמידים ורואה את התשובות בזמן אמת.</p></div><div class="session-access"><span class="check">✓</span><b>המפגש פתוח לתלמידי הקבוצה</b><small>5 מתוך 8 הצטרפו אוטומטית</small></div></section>
33
+ <div class="layout teacher-only">
34
+ <aside class="card roster">
35
+ <div class="section-head"><div><h2>התלמידים</h2><p>מי כבר נמצא בשיעור</p></div><span class="count" id="onlineCount">5/8</span></div>
36
+ <div class="students" id="students"></div>
37
+ </aside>
38
+ <main class="main">
39
+ <section class="card composer">
40
+ <div class="steps"><span class="step active"><b>1</b>מכינים שאלה</span><span class="step"><b>2</b>שולחים לכיתה</span><span class="step"><b>3</b>רואים תשובות</span></div>
41
+ <div class="section-head"><div><h2>מה אומרים עכשיו?</h2><p>אפשר לבחור מהשיעור או לומר משפט בעברית.</p></div></div>
42
+ <div class="question-modes">
43
+ <button class="mode active" data-mode="bank"><strong>בחירה מהשיעור</strong><small>משפט מוכן עם תשובה בערבית</small></button>
44
+ <button class="mode" data-mode="speech"><strong>אומרים משפט בעברית</strong><small>המערכת מחפשת משפט מתאים במאגר</small></button>
45
+ </div>
46
+ <div class="field bank" id="bankField"><label for="sentence">משפט מתוך השיעור</label><select class="select" id="sentence"><option value="איפה היית אתמול?|وين كنت امبارح؟|וֵין כֻּנְת אִמְבַּארֶח?">איפה היית אתמול?</option><option value="מתי הגעתם לבית הספר?|ايمتى وصلتوا عالمدرسة؟|אֵימְתַא וִצֶלְתוּ עַלְמַדְרַסֶה?">מתי הגעתם לבית הספר?</option><option value="מה אתם רוצים לעשות מחר?|شو بدّكم تعملوا بكرا؟|שוּ בִּדְّכֹּם תִעְמַלוּ בֻּכְּרַא?">מה אתם רוצים לעשות מחר?</option></select></div>
47
+ <div class="field speech" id="speechField"><label for="spoken">המשפט שאמרת</label><div class="speechbox"><input id="spoken" value="מה עשיתם בסוף השבוע?" aria-label="המשפט בעברית"><button class="mic" id="micBtn" title="הדמיית הקלטה">●</button></div></div>
48
+ <div class="preview"><small id="previewLabel">התלמידים יראו</small><h3 id="questionText">איפה היית אתמול?</h3><div class="arabic" id="arabicText">وين كنت امبارح؟</div><div class="pending" id="pending" hidden></div></div>
49
+ <div class="actions"><button class="btn primary" id="sendBtn">שליחה לכולם</button></div>
50
+ </section>
51
+ <section class="card results" id="results"><div class="empty"><span>◌</span><b>עוד לא נשלחה שאלה</b><div>התשובות והציונים יופיעו כאן.</div></div></section>
52
+ </main>
53
+ </div>
54
+ <section id="studentView" class="card" aria-live="polite"></section>
55
+ </div>
56
+ <script>
57
+ const $=id=>document.getElementById(id);
58
+ const pupils=[['נועה','נכנסה עכשיו',1],['אמיר','מחובר',1],['מאיה','מחוברת',1],['יוסף','מחובר',1],['ליאן','מחוברת',1],['תום','טרם נכנס',0],['רים','טרם נכנסה',0],['אדם','טרם נכנס',0]];
59
+ const speechBank=[
60
+ {he:'מה עשיתם בסוף השבוע?',ar:'شو عملتوا بنهاية الأسبوع؟',tr:'שוּ עְמֵלְתוּ בִּנְהַאיֵת אִלְאֻסְבּוּע?'},
61
+ {he:'איפה היית אתמול?',ar:'وين كنت امبارح؟',tr:'וֵין כֻּנְת אִמְבַּארֶח?'},
62
+ {he:'מתי הגעתם לבית הספר?',ar:'ايمتى وصلتوا عالمدرسة؟',tr:'אֵימְתַא וִצֶלְתוּ עַלְמַדְרַסֶה?'}
63
+ ];
64
+ let mode='bank', sent=false, currentMatch=null;
65
+ function drawStudents(answering=false){$('students').innerHTML=pupils.map((p,i)=>`<div class="student ${p[2]?'on':''} ${answering&&i===3?'answering':''}"><span class="avatar">${p[0][0]}</span><span><b>${p[0]}</b><small>${answering&&i===3?'עונה עכשיו':p[1]}</small></span><i class="status"></i></div>`).join('')}
66
+ function studentWaiting(){
67
+ $('studentView').innerHTML='<div class="student-shell"><div class="class-name">שיעור 6 · קבוצת יום רביעי</div><div class="waiting-ring"></div><h1>הצטרפת לשיעור החי</h1><p class="instruction">זיהינו את הכיתה שלך וחיברנו אותך למפגש. השאלה תופיע כאן כשהמדריך ישלח אותה.</p></div>';
68
+ }
69
+ function studentQuestion(){
70
+ if(!sent||!currentMatch)return studentWaiting();
71
+ $('studentView').innerHTML=`<div class="student-shell"><div class="class-name">מחוברים לשיעור 6 · קבוצת יום רביעי</div><h1>${currentMatch.he}</h1><p class="instruction">ענו בערבית. התשובה והתעתיק אינם מוצגים לפני ההקלטה.</p><button class="student-mic" id="studentMic" aria-label="הקלטת תשובה">●</button><b>לחצו והתחילו לדבר</b></div>`;
72
+ }
73
+ function studentResult(){
74
+ const rows=answerRows(currentMatch.tr), mine=rows[2];
75
+ $('studentView').innerHTML=`<div class="student-shell"><div class="class-name">התשובה נבדקה</div><h1>המשוב האישי שלך</h1><div class="student-score">72</div><div class="student-compare"><div class="said-line"><small>צריך</small><span class="translit expected">${mine[1]}</span></div><div class="said-line"><small>נאמר</small><span class="translit heard">${mine[2]}</span></div></div><p class="instruction">האותיות המסומנות נשמעו שונות. רק את והמדריך רואים את הציון הזה.</p><div class="student-actions"><button class="btn primary" data-student-action="retry">ניסיון נוסף</button><button class="btn" data-student-action="done">סיום והמתנה</button></div></div>`;
76
+ }
77
+ const clean=s=>s.replace(/[?!.,]/g,'').replace(/\s+/g,' ').trim();
78
+ function sync(){
79
+ let he,ar,tr;
80
+ if(mode==='bank'){
81
+ [he,ar,tr]=$('sentence').value.split('|'); currentMatch={he,ar,tr};
82
+ $('previewLabel').textContent='התלמידים יראו'; $('pending').hidden=true;
83
+ }else{
84
+ he=$('spoken').value.trim(); currentMatch=speechBank.find(x=>clean(x.he)===clean(he))||null;
85
+ $('previewLabel').textContent=currentMatch?'נמצאה התאמה במאגר':'תוצאת החיפוש במאגר';
86
+ $('pending').hidden=false;
87
+ $('pending').textContent=currentMatch?'זהו משפט קיים ומוכן לבדיקה פונמית. לא נוצר תרגום חדש.':'לא נמצא משפט מוכן. אפשר לבחור משפט מהשיעור או לנסות ניסוח אחר.';
88
+ ar=currentMatch?currentMatch.ar:'אין משפט מתאים במאגר';
89
+ }
90
+ $('questionText').textContent=he||'אמרו משפט בעברית'; $('arabicText').textContent=ar; $('sendBtn').disabled=!currentMatch;
91
+ }
92
+ function expectedTr(){return currentMatch.tr}
93
+ function compared(expected,from,to){return [expected.replace(from,`<span class="expected-diff">${from}</span>`),expected.replace(from,`<span class="letter-diff">${to}</span>`)]}
94
+ function answerRows(expected){
95
+ let change=['כֻּנְת','כּוּנְת'];
96
+ if(expected.includes('וִצֶלְתוּ'))change=['וִצֶלְתוּ','וִסֶלְתוּ'];
97
+ else if(expected.includes('תִעְמַלוּ'))change=['תִעְמַלוּ','תַעְמַלוּ'];
98
+ else if(expected.includes('עְמֵלְתוּ'))change=['עְמֵלְתוּ','עַמַלְתוּ'];
99
+ const different=compared(expected,change[0],change[1]);
100
+ return [['נועה',expected,expected,94,'הגייה ברורה'],['אמיר',expected,expected,87,'המשפט זוהה במלואו'],['מאיה',different[0],different[1],72,'האותיות המסומנות נשמעו שונות'],['יוסף','','',null,'מקליט עכשיו…'],['ליאן',expected,expected,91,'הגייה ברורה']]
101
+ }
102
+ document.querySelectorAll('.mode').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;document.querySelectorAll('.mode').forEach(x=>x.classList.toggle('active',x===b));$('bankField').style.display=mode==='bank'?'block':'none';$('speechField').classList.toggle('visible',mode==='speech');sync()});
103
+ document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>{const student=b.dataset.view==='student';document.body.classList.toggle('student-view',student);document.querySelectorAll('[data-view]').forEach(x=>x.classList.toggle('active',x===b));$('viewName').textContent=`הדגמת שיעור חי · מסך ${student?'התלמיד':'המדריך'}`;if(student)studentQuestion()});
104
+ $('studentView').onclick=e=>{
105
+ if(e.target.id==='studentMic'){
106
+ e.target.disabled=true;e.target.classList.add('listening');e.target.closest('.student-shell').querySelector('b').textContent='מקליט…';
107
+ setTimeout(()=>{if($('studentView'))$('studentView').innerHTML='<div class="student-shell"><div class="waiting-ring"></div><h1>בודקים את ההגייה…</h1><p class="instruction">התשובה נשלחה למדריך.</p></div>';setTimeout(studentResult,900)},900);
108
+ }else if(e.target.dataset.studentAction==='retry')studentQuestion();
109
+ else if(e.target.dataset.studentAction==='done')studentWaiting();
110
+ };
111
+ $('sentence').onchange=sync;$('spoken').oninput=sync;
112
+ $('micBtn').onclick=()=>{const b=$('micBtn');b.classList.add('listening');$('spoken').value='מקשיב…';sync();setTimeout(()=>{b.classList.remove('listening');$('spoken').value='מה עשיתם בסוף השבוע?';sync()},1400)};
113
+ $('sendBtn').onclick=()=>{sent=true;drawStudents(true);studentQuestion();document.querySelectorAll('.step').forEach((s,i)=>s.classList.toggle('active',i<3));const he=$('questionText').textContent,expected=expectedTr();$('results').innerHTML=`<div class="question-live"><div class="q"><small>השאלה פתוחה עכשיו</small><strong>${he}</strong></div><span class="timer" id="timer">00:30</span><button class="btn" id="closeQ">סיום</button></div><table><thead><tr><th>תלמיד/ה</th><th>מה היה צריך לומר / מה נאמר</th><th>משוב</th><th>ציון</th></tr></thead><tbody id="answerRows"><tr><td colspan="4" class="wait">ממתינים לתשובות…</td></tr></tbody></table><p class="note">התעתיק העברי מציג את פענוח ההקלטה. ירוק מסמן את הרצף הצפוי ואדום את מה שנשמע במקומו. זהו אב־טיפוס והנתונים בו מדומים.</p>`;let n=30;const timer=setInterval(()=>{if(!$('timer'))return clearInterval(timer);$('timer').textContent=`00:${String(--n).padStart(2,'0')}`;if(n<=0)clearInterval(timer)},1000);setTimeout(()=>{if(!sent)return;$('answerRows').innerHTML=answerRows(expected).map(a=>`<tr><td><b>${a[0]}</b></td><td class="answer">${a[3]==null?'<span class="wait">ממתינים לסיום ההקלטה…</span>':`<div class="said-line"><small>צריך</small><span class="translit expected">${a[1]}</span></div><div class="said-line"><small>נאמר</small><span class="translit heard">${a[2]}</span></div>`}</td><td>${a[4]}</td><td>${a[3]==null?'<span class="wait">…</span>':`<span class="score ${a[3]<80?'mid':''}">${a[3]}</span>`}</td></tr>`).join('');$('closeQ').onclick=()=>{sent=false;drawStudents();studentWaiting();$('closeQ').disabled=true;$('closeQ').textContent='השאלה הסתיימה';clearInterval(timer)}},850)};
114
+ drawStudents();sync();studentWaiting();
115
+ </script>
116
+ </body>
117
+ </html>
static/index.html CHANGED
@@ -607,8 +607,8 @@ body.driving .card.storymode .prompt{display:block}
607
  .tips li .sub{display:block}
608
  /* "פועל נגד הזמן": every prompt is a bead; correct answers join the misbaha above. */
609
  .card.bubblemode{min-height:min(610px,72vh);overflow:hidden;position:relative;background:#101510 url("/static/assets/arabic-calligraphy-path.webp?v=7") top center/100% auto repeat-y}
610
- .card.bubblemode .prompt{position:relative;z-index:1;display:grid;place-items:center;width:clamp(160px,44vw,220px);aspect-ratio:1;margin:clamp(72px,13vh,105px) auto 18px;padding:40px 28px 28px;box-sizing:border-box;border:3px solid #1b2118;border-radius:50%;background:radial-gradient(circle at 50% 15%,#151a13 0 7px,#899b79 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.72) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#aabd98 0%,#718363 37%,#4a5941 72%,#303a2b 100%);color:#f7faef;text-shadow:0 2px 1px rgb(0 0 0/.45);box-shadow:inset 13px 16px 24px rgb(255 255 255/.14),inset -18px -22px 30px rgb(10 17 9/.38),0 18px 28px rgb(0 0 0/.38);animation:bead-rise 12s linear both}
611
- .card.bubblemode .prompt::before{content:"";position:absolute;inset:-10px;border-radius:50%;border:5px solid #aabd98;clip-path:inset(0);animation:verb-time 12s linear both}
612
  .card.bubblemode .prompt::after{content:"";position:absolute;left:50%;top:13px;width:15px;height:15px;border-radius:50%;background:#111710;box-shadow:inset 0 3px 4px #050705,0 1px 0 rgb(255 255 255/.24);transform:translateX(-50%)}
613
  .card.bubblemode .bubble-grade{position:absolute;left:50%;bottom:28px;min-width:64px;padding:6px 11px;border:2px solid #fff;border-radius:999px;background:linear-gradient(180deg,#ffd95e,#f2a91d);color:#4c3100;box-shadow:0 3px 0 #a96800,0 6px 12px rgb(66 50 0/.22);font:800 20px/1 var(--f-display);font-variant-numeric:tabular-nums;white-space:nowrap;transform:translateX(-50%);animation:grade-pop .38s cubic-bezier(.2,1.8,.4,1)}
614
  .card.bubblemode .bubble-grade::before{content:"★";margin-inline-end:4px;color:#fff;text-shadow:0 1px 1px rgb(92 58 0/.35)}
@@ -635,15 +635,18 @@ body.driving .card.storymode .prompt{display:block}
635
  .misbaha-cord .tassel-thread{fill:none;stroke:#171713;stroke-width:1.35;stroke-linecap:round;opacity:.95}
636
  .mini-bead{position:relative;z-index:1;width:19px;height:19px;border:1px solid #72500d;border-radius:50%;background:radial-gradient(circle at 50% 50%,#704708 0 2px,transparent 3px),radial-gradient(circle at 29% 23%,#fff1a0 0 9%,transparent 11%),radial-gradient(circle at 38% 34%,#ffe27a,#d7a423 44%,#8e5b08 100%);box-shadow:inset -3px -4px 5px rgb(91 55 4/.32),0 2px 3px rgb(0 0 0/.38)}
637
  .mini-bead::after{content:"";position:absolute;inset:3px;border:1px dotted rgb(112 71 8/.65);border-radius:50%;box-shadow:0 0 0 1px rgb(255 231 135/.28)}
638
- .mini-bead:nth-of-type(1){grid-area:1/1}.mini-bead:nth-of-type(2){grid-area:1/2}.mini-bead:nth-of-type(3){grid-area:1/3}.mini-bead:nth-of-type(4){grid-area:1/4}.mini-bead:nth-of-type(5){grid-area:1/5}
639
- .mini-bead:nth-of-type(6){grid-area:2/5}.mini-bead:nth-of-type(7){grid-area:2/4}.mini-bead:nth-of-type(8){grid-area:2/3}.mini-bead:nth-of-type(9){grid-area:2/2}.mini-bead:nth-of-type(10){grid-area:2/1}
640
  .mini-bead.good{border-color:#1b2118;background:radial-gradient(circle at 50% 50%,#151a13 0 2px,transparent 3px),radial-gradient(circle at 31% 25%,#e2ead8 0 8%,transparent 10%),radial-gradient(circle at 38% 34%,#aabd98,#718363 44%,#35402f 100%);box-shadow:inset -3px -4px 5px rgb(10 17 9/.3),0 2px 3px rgb(0 0 0/.35)}
641
  .mini-bead.good::after{border-color:rgb(40 55 34/.65);box-shadow:0 0 0 1px rgb(226 234 216/.26)}
 
 
642
  .mini-bead.failed{border-color:#7d3d37;background:radial-gradient(circle at 50% 50%,#552622 0 2px,transparent 3px),radial-gradient(circle at 31% 25%,#ffe1da 0 8%,transparent 10%),radial-gradient(circle at 38% 34%,#efaaa0,#d77970 44%,#9b4f48 100%);box-shadow:inset -3px -4px 5px rgb(91 35 30/.25),0 2px 3px rgb(0 0 0/.35)}
643
  .mini-bead.failed::after{border-color:rgb(108 43 38/.6);box-shadow:0 0 0 1px rgb(255 225 218/.28)}
644
  .mini-bead.landed{animation:mini-land .3s cubic-bezier(.2,1.7,.4,1)}
645
  .bead-flight{position:fixed;z-index:30;margin:0;border:3px solid #72500d;border-radius:50%;background:radial-gradient(circle at 50% 15%,#704708 0 7px,#d9a82c 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 246 186/.82) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#ffe27a,#d7a423 42%,#8e5b08 100%);box-shadow:inset -14px -18px 30px rgb(91 55 4/.32),0 14px 24px rgb(0 0 0/.3);pointer-events:none}
646
  .bead-flight.good{border-color:#1b2118;background:radial-gradient(circle at 50% 15%,#151a13 0 7px,#899b79 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.72) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#aabd98,#718363 42%,#303a2b 100%);box-shadow:inset -14px -18px 30px rgb(10 17 9/.35),0 14px 24px rgb(0 0 0/.3)}
 
647
  .bead-flight.failed{border-color:#7d3d37;background:radial-gradient(circle at 50% 15%,#552622 0 7px,#d98980 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.7) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#efaaa0,#d77970 42%,#9b4f48 100%)}
648
  .card.bubblemode .prompt.collected{visibility:hidden}
649
  .bubble-hint{position:absolute;left:50%;bottom:18px;z-index:3;width:max-content;min-width:170px;max-width:min(230px,calc(100% - 32px));box-sizing:border-box;padding:10px 13px;border-radius:12px;background:rgb(255 255 255/.94);color:#174f18;box-shadow:0 4px 14px rgb(30 91 36/.18);font:700 14px/1.35 var(--f-body);animation:hint-in .25s ease-out both}
@@ -664,12 +667,20 @@ body.bubble-game #gameBack{display:flex;position:fixed;top:max(12px,env(safe-are
664
  body.bubble-game #gameBack .ic{width:20px;height:20px}
665
  body.bubble-game #gameBack:active{transform:translateY(2px);box-shadow:0 1px 0 #111}
666
  @keyframes bead-rise{0%{transform:translateY(54px) rotate(-3deg) scale(.9)}50%{transform:translateY(2px) rotate(2deg) scale(.97)}100%{transform:translateY(-58px) rotate(-2deg) scale(1.02)}}
 
 
 
 
 
 
 
 
667
  @keyframes verb-time{from{clip-path:inset(0)}to{clip-path:inset(100% 0 0 0)}}
668
  @keyframes score-pop{0%{transform:scale(1)}45%{transform:scale(1.55);color:#2d9b31}100%{transform:scale(1)}}
669
  @keyframes grade-pop{0%{transform:translateX(-50%) scale(.25) rotate(-8deg);opacity:0}65%{transform:translateX(-50%) scale(1.18) rotate(3deg);opacity:1}100%{transform:translateX(-50%) scale(1) rotate(0)}}
670
  @keyframes hint-in{from{transform:translate(-50%,6px);opacity:0}to{transform:translate(-50%,0);opacity:1}}
671
  @keyframes mini-land{0%{transform:scale(.4)}65%{transform:scale(1.35)}100%{transform:scale(1)}}
672
- @media (prefers-reduced-motion:reduce){.card.bubblemode .prompt{animation:none}}
673
  /* Without a microphone ("תרגול שקט ללא ציון"): a Hebrew sentence is answered by hand instead of said, built from its
674
  Arabic words, tapped in order out of a bank that holds a few words too many. */
675
  body.quiet :is(#mic,#playMine,#driveBtn,.phone-btn),body.driving :is(#quietBtn,#heBtn,.miclab,.phone-btn){display:none}
@@ -756,7 +767,7 @@ body.quiet :is(#mic,#playMine,#driveBtn,.phone-btn),body.driving :is(#quietBtn,#
756
 
757
  <main class="card" id="card" aria-live="polite">
758
  <div class="bubble-score" id="bubbleScore" hidden><span>ניקוד<small id="bubbleCount"></small></span><b id="bubblePoints">0</b></div>
759
- <div class="bubble-beads" id="bubbleBeads" hidden aria-label="תוצאות במסבחה: ירוק בלי רמז, צהוב עם רמז ואדום בכישלון"><svg class="misbaha-cord" viewBox="0 0 150 88" aria-hidden="true"><defs><linearGradient id="imamGold" x1="0" x2="1"><stop stop-color="#845308"/><stop offset=".28" stop-color="#f5ce57"/><stop offset=".58" stop-color="#b8790d"/><stop offset=".82" stop-color="#ffe68a"/><stop offset="1" stop-color="#744607"/></linearGradient></defs><path class="cord-shadow" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-base" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-weave-a" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-weave-b" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-glint" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-base" d="M36 36H27"/><rect class="imam" x="9" y="31" width="21" height="10" rx="5"/><rect class="tassel-cap" x="4" y="30" width="7" height="12" rx="2"/><path class="tassel-thread" d="M7 42Q8 55 2 75M7 42Q12 57 8 80M7 42Q16 58 15 82M7 42Q20 56 22 78M7 42Q24 53 29 73M7 42Q18 48 35 67M7 42Q13 62 3 84M7 42Q21 61 18 85"/></svg></div>
760
  <div class="bubble-hint" id="bubbleHint" hidden><small>רמז</small><span class="binyan" id="bubbleBinyan"></span><strong class="gizra" id="bubbleGizra"></strong><span class="past" id="bubblePast" hidden><small>עבר · הוא</small><b id="bubblePastText"></b><em id="bubblePastAr" lang="ar"></em></span></div>
761
  <div class="meta"><span id="counter"></span></div>
762
  <div class="progress" id="progress" hidden><i id="progressBar"></i></div>
@@ -1519,7 +1530,7 @@ const EXCELLENT=85; // the score the verdict calls "מצוין"
1519
  // left, it is the whole topic, and a practised sentence shows its best score (see show).
1520
  // `here` is the topic's sentence on the card: not always the round's, because going back passes through the practised ones.
1521
  let ordered=false, topic=[], here=null, showDone=store.get("ihki.showDone",false);
1522
- const BUBBLE_MS=12000, BUBBLE_HINT_MS=3000, BUBBLE_PAST_MS=6000, BUBBLE_ROUND=10;
1523
  let bubbleTimer=null, bubbleHintTimer=null, bubblePastTimer=null, bubbleDeadline=0, bubbleFor=null, bubbleResolving=false, bubbleHintShown=false, bubblePastShown=false;
1524
  let bubbleGame={answered:0,correct:0,total:0};
1525
  const bubbleCat=()=>!daily&&(categoryOf(cat)||{}).bubble;
@@ -1534,12 +1545,21 @@ function countBubble(correct){
1534
  drawBubbleScore(correct);
1535
  }
1536
  async function collectBead(result){
1537
- const source=$("prompt"), shelf=$("bubbleBeads"), from=source.getBoundingClientRect(), tone=result==="clean"?" good":result==="failed"?" failed":"", mini=mark(`mini-bead${tone}`,"");
1538
  shelf.append(mini); const to=mini.getBoundingClientRect(); mini.style.visibility="hidden"; source.classList.add("collected");
1539
  const ghost=mark(`bead-flight${tone}`,""); Object.assign(ghost.style,{left:from.left+"px",top:from.top+"px",width:from.width+"px",height:from.height+"px"}); document.body.append(ghost);
1540
  if(!matchMedia("(prefers-reduced-motion:reduce)").matches){
1541
- const dx=to.left+to.width/2-(from.left+from.width/2), dy=to.top+to.height/2-(from.top+from.height/2), scale=to.width/from.width;
1542
- try{ await ghost.animate([{transform:"translate(0,0) scale(1)"},{transform:`translate(${dx}px,${dy}px) scale(${scale})`}],{duration:360,easing:"cubic-bezier(.4,0,.2,1)",fill:"forwards"}).finished; }catch{}
 
 
 
 
 
 
 
 
 
1543
  }
1544
  ghost.remove(); mini.style.visibility=""; mini.classList.add("landed");
1545
  }
@@ -2282,7 +2302,7 @@ function render(r){
2282
  }
2283
 
2284
  async function bubbleOutcome(r,expired=false){
2285
- const id=cur.id, correct=!r.unrelated&&r.score>=70;
2286
  if(bubbleResolving) return;
2287
  bubbleResolving=true;
2288
  if(!expired){
@@ -2291,10 +2311,10 @@ async function bubbleOutcome(r,expired=false){
2291
  $("prompt").append(mark(`bubble-grade ${band}`,`${score} נק׳`));
2292
  }
2293
  if(correct){
2294
- const hinted=bubbleHintShown||bubblePastShown;
2295
  countBubble(true);
2296
  stopBubble();
2297
- await collectBead(hinted?"hinted":"clean");
2298
  }else{
2299
  if(!expired&&Date.now()<bubbleDeadline){
2300
  bubbleResolving=false;
 
607
  .tips li .sub{display:block}
608
  /* "פועל נגד הזמן": every prompt is a bead; correct answers join the misbaha above. */
609
  .card.bubblemode{min-height:min(610px,72vh);overflow:hidden;position:relative;background:#101510 url("/static/assets/arabic-calligraphy-path.webp?v=7") top center/100% auto repeat-y}
610
+ .card.bubblemode .prompt{position:relative;z-index:1;display:grid;place-items:center;width:clamp(160px,44vw,220px);aspect-ratio:1;margin:clamp(72px,13vh,105px) auto 18px;padding:40px 28px 28px;box-sizing:border-box;border:3px solid #1b2118;border-radius:50%;background:radial-gradient(circle at 50% 15%,#151a13 0 7px,#899b79 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.72) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#aabd98 0%,#718363 37%,#4a5941 72%,#303a2b 100%);color:#f7faef;text-shadow:0 2px 1px rgb(0 0 0/.45);box-shadow:inset 13px 16px 24px rgb(255 255 255/.14),inset -18px -22px 30px rgb(10 17 9/.38),0 18px 28px rgb(0 0 0/.38);animation:bead-rise 9s linear both,bead-color 9s linear both}
611
+ .card.bubblemode .prompt::before{content:"";position:absolute;inset:-10px;border-radius:50%;border:5px solid #aabd98;clip-path:inset(0);animation:verb-time 9s linear both,bead-ring 9s linear both}
612
  .card.bubblemode .prompt::after{content:"";position:absolute;left:50%;top:13px;width:15px;height:15px;border-radius:50%;background:#111710;box-shadow:inset 0 3px 4px #050705,0 1px 0 rgb(255 255 255/.24);transform:translateX(-50%)}
613
  .card.bubblemode .bubble-grade{position:absolute;left:50%;bottom:28px;min-width:64px;padding:6px 11px;border:2px solid #fff;border-radius:999px;background:linear-gradient(180deg,#ffd95e,#f2a91d);color:#4c3100;box-shadow:0 3px 0 #a96800,0 6px 12px rgb(66 50 0/.22);font:800 20px/1 var(--f-display);font-variant-numeric:tabular-nums;white-space:nowrap;transform:translateX(-50%);animation:grade-pop .38s cubic-bezier(.2,1.8,.4,1)}
614
  .card.bubblemode .bubble-grade::before{content:"★";margin-inline-end:4px;color:#fff;text-shadow:0 1px 1px rgb(92 58 0/.35)}
 
635
  .misbaha-cord .tassel-thread{fill:none;stroke:#171713;stroke-width:1.35;stroke-linecap:round;opacity:.95}
636
  .mini-bead{position:relative;z-index:1;width:19px;height:19px;border:1px solid #72500d;border-radius:50%;background:radial-gradient(circle at 50% 50%,#704708 0 2px,transparent 3px),radial-gradient(circle at 29% 23%,#fff1a0 0 9%,transparent 11%),radial-gradient(circle at 38% 34%,#ffe27a,#d7a423 44%,#8e5b08 100%);box-shadow:inset -3px -4px 5px rgb(91 55 4/.32),0 2px 3px rgb(0 0 0/.38)}
637
  .mini-bead::after{content:"";position:absolute;inset:3px;border:1px dotted rgb(112 71 8/.65);border-radius:50%;box-shadow:0 0 0 1px rgb(255 231 135/.28)}
638
+ .mini-bead:nth-of-type(1){grid-area:2/1}.mini-bead:nth-of-type(2){grid-area:2/2}.mini-bead:nth-of-type(3){grid-area:2/3}.mini-bead:nth-of-type(4){grid-area:2/4}.mini-bead:nth-of-type(5){grid-area:2/5}
639
+ .mini-bead:nth-of-type(6){grid-area:1/5}.mini-bead:nth-of-type(7){grid-area:1/4}.mini-bead:nth-of-type(8){grid-area:1/3}.mini-bead:nth-of-type(9){grid-area:1/2}.mini-bead:nth-of-type(10){grid-area:1/1}
640
  .mini-bead.good{border-color:#1b2118;background:radial-gradient(circle at 50% 50%,#151a13 0 2px,transparent 3px),radial-gradient(circle at 31% 25%,#e2ead8 0 8%,transparent 10%),radial-gradient(circle at 38% 34%,#aabd98,#718363 44%,#35402f 100%);box-shadow:inset -3px -4px 5px rgb(10 17 9/.3),0 2px 3px rgb(0 0 0/.35)}
641
  .mini-bead.good::after{border-color:rgb(40 55 34/.65);box-shadow:0 0 0 1px rgb(226 234 216/.26)}
642
+ .mini-bead.assisted{border-color:#924d12;background:radial-gradient(circle at 50% 50%,#74360b 0 2px,transparent 3px),radial-gradient(circle at 31% 25%,#ffe0ad 0 8%,transparent 10%),radial-gradient(circle at 38% 34%,#ffbd72,#e78329 44%,#a94716 100%);box-shadow:inset -3px -4px 5px rgb(104 45 9/.28),0 2px 3px rgb(0 0 0/.35)}
643
+ .mini-bead.assisted::after{border-color:rgb(120 55 14/.62);box-shadow:0 0 0 1px rgb(255 224 173/.28)}
644
  .mini-bead.failed{border-color:#7d3d37;background:radial-gradient(circle at 50% 50%,#552622 0 2px,transparent 3px),radial-gradient(circle at 31% 25%,#ffe1da 0 8%,transparent 10%),radial-gradient(circle at 38% 34%,#efaaa0,#d77970 44%,#9b4f48 100%);box-shadow:inset -3px -4px 5px rgb(91 35 30/.25),0 2px 3px rgb(0 0 0/.35)}
645
  .mini-bead.failed::after{border-color:rgb(108 43 38/.6);box-shadow:0 0 0 1px rgb(255 225 218/.28)}
646
  .mini-bead.landed{animation:mini-land .3s cubic-bezier(.2,1.7,.4,1)}
647
  .bead-flight{position:fixed;z-index:30;margin:0;border:3px solid #72500d;border-radius:50%;background:radial-gradient(circle at 50% 15%,#704708 0 7px,#d9a82c 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 246 186/.82) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#ffe27a,#d7a423 42%,#8e5b08 100%);box-shadow:inset -14px -18px 30px rgb(91 55 4/.32),0 14px 24px rgb(0 0 0/.3);pointer-events:none}
648
  .bead-flight.good{border-color:#1b2118;background:radial-gradient(circle at 50% 15%,#151a13 0 7px,#899b79 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.72) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#aabd98,#718363 42%,#303a2b 100%);box-shadow:inset -14px -18px 30px rgb(10 17 9/.35),0 14px 24px rgb(0 0 0/.3)}
649
+ .bead-flight.assisted{border-color:#924d12;background:radial-gradient(circle at 50% 15%,#74360b 0 7px,#e8943c 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 240 211/.78) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#ffbd72,#e78329 42%,#a94716 100%);box-shadow:inset -14px -18px 30px rgb(104 45 9/.3),0 14px 24px rgb(0 0 0/.3)}
650
  .bead-flight.failed{border-color:#7d3d37;background:radial-gradient(circle at 50% 15%,#552622 0 7px,#d98980 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.7) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#efaaa0,#d77970 42%,#9b4f48 100%)}
651
  .card.bubblemode .prompt.collected{visibility:hidden}
652
  .bubble-hint{position:absolute;left:50%;bottom:18px;z-index:3;width:max-content;min-width:170px;max-width:min(230px,calc(100% - 32px));box-sizing:border-box;padding:10px 13px;border-radius:12px;background:rgb(255 255 255/.94);color:#174f18;box-shadow:0 4px 14px rgb(30 91 36/.18);font:700 14px/1.35 var(--f-body);animation:hint-in .25s ease-out both}
 
667
  body.bubble-game #gameBack .ic{width:20px;height:20px}
668
  body.bubble-game #gameBack:active{transform:translateY(2px);box-shadow:0 1px 0 #111}
669
  @keyframes bead-rise{0%{transform:translateY(54px) rotate(-3deg) scale(.9)}50%{transform:translateY(2px) rotate(2deg) scale(.97)}100%{transform:translateY(-58px) rotate(-2deg) scale(1.02)}}
670
+ /* Matches the hint clock: green 0–3s, yellow 3–6s, orange 6–9s; only expiry is red. */
671
+ @keyframes bead-color{
672
+ 0%,24.99%{border-color:#1b2118;background:radial-gradient(circle at 50% 15%,#151a13 0 7px,#899b79 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.72) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#aabd98 0%,#718363 37%,#4a5941 72%,#303a2b 100%);color:#f7faef;text-shadow:0 2px 1px rgb(0 0 0/.45)}
673
+ 25%,49.99%{border-color:#72500d;background:radial-gradient(circle at 50% 15%,#704708 0 7px,#d9a82c 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 246 186/.82) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#ffe27a 0%,#d7a423 40%,#aa7110 73%,#714507 100%);color:#3c2800;text-shadow:0 1px 0 rgb(255 247 194/.6)}
674
+ 50%,99.99%{border-color:#924d12;background:radial-gradient(circle at 50% 15%,#74360b 0 7px,#e8943c 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 240 211/.78) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#ffbd72 0%,#e78329 40%,#c75f1d 73%,#913c12 100%);color:#481d05;text-shadow:0 1px 0 rgb(255 224 173/.55)}
675
+ 100%{border-color:#7d3d37;background:radial-gradient(circle at 50% 15%,#552622 0 7px,#d98980 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,rgb(255 255 255/.7) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,#efaaa0 0%,#d77970 40%,#b85d55 73%,#843e38 100%);color:#481713;text-shadow:0 1px 0 rgb(255 225 218/.55)}
676
+ }
677
+ @keyframes bead-ring{0%,24.99%{border-color:#aabd98}25%,49.99%{border-color:#e2b32e}50%,99.99%{border-color:#e78329}100%{border-color:#d77970}}
678
  @keyframes verb-time{from{clip-path:inset(0)}to{clip-path:inset(100% 0 0 0)}}
679
  @keyframes score-pop{0%{transform:scale(1)}45%{transform:scale(1.55);color:#2d9b31}100%{transform:scale(1)}}
680
  @keyframes grade-pop{0%{transform:translateX(-50%) scale(.25) rotate(-8deg);opacity:0}65%{transform:translateX(-50%) scale(1.18) rotate(3deg);opacity:1}100%{transform:translateX(-50%) scale(1) rotate(0)}}
681
  @keyframes hint-in{from{transform:translate(-50%,6px);opacity:0}to{transform:translate(-50%,0);opacity:1}}
682
  @keyframes mini-land{0%{transform:scale(.4)}65%{transform:scale(1.35)}100%{transform:scale(1)}}
683
+ @media (prefers-reduced-motion:reduce){.card.bubblemode .prompt{animation:bead-color 9s linear both}}
684
  /* Without a microphone ("תרגול שקט ללא ציון"): a Hebrew sentence is answered by hand instead of said, built from its
685
  Arabic words, tapped in order out of a bank that holds a few words too many. */
686
  body.quiet :is(#mic,#playMine,#driveBtn,.phone-btn),body.driving :is(#quietBtn,#heBtn,.miclab,.phone-btn){display:none}
 
767
 
768
  <main class="card" id="card" aria-live="polite">
769
  <div class="bubble-score" id="bubbleScore" hidden><span>ניקוד<small id="bubbleCount"></small></span><b id="bubblePoints">0</b></div>
770
+ <div class="bubble-beads" id="bubbleBeads" hidden aria-label="תוצאות במסבחה: ירוק ללא רמז, צהוב אחרי רמז ראשון, כתום אחרי רמז שני ואדום בכישלון"><svg class="misbaha-cord" viewBox="0 0 150 88" aria-hidden="true"><defs><linearGradient id="imamGold" x1="0" x2="1"><stop stop-color="#845308"/><stop offset=".28" stop-color="#f5ce57"/><stop offset=".58" stop-color="#b8790d"/><stop offset=".82" stop-color="#ffe68a"/><stop offset="1" stop-color="#744607"/></linearGradient></defs><path class="cord-shadow" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-base" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-weave-a" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-weave-b" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-glint" d="M36 14H120Q134 14 134 25T120 36H36"/><path class="cord-base" d="M36 36H27"/><rect class="imam" x="9" y="31" width="21" height="10" rx="5"/><rect class="tassel-cap" x="4" y="30" width="7" height="12" rx="2"/><path class="tassel-thread" d="M7 42Q8 55 2 75M7 42Q12 57 8 80M7 42Q16 58 15 82M7 42Q20 56 22 78M7 42Q24 53 29 73M7 42Q18 48 35 67M7 42Q13 62 3 84M7 42Q21 61 18 85"/></svg></div>
771
  <div class="bubble-hint" id="bubbleHint" hidden><small>רמז</small><span class="binyan" id="bubbleBinyan"></span><strong class="gizra" id="bubbleGizra"></strong><span class="past" id="bubblePast" hidden><small>עבר · הוא</small><b id="bubblePastText"></b><em id="bubblePastAr" lang="ar"></em></span></div>
772
  <div class="meta"><span id="counter"></span></div>
773
  <div class="progress" id="progress" hidden><i id="progressBar"></i></div>
 
1530
  // left, it is the whole topic, and a practised sentence shows its best score (see show).
1531
  // `here` is the topic's sentence on the card: not always the round's, because going back passes through the practised ones.
1532
  let ordered=false, topic=[], here=null, showDone=store.get("ihki.showDone",false);
1533
+ const BUBBLE_MS=9000, BUBBLE_HINT_MS=3000, BUBBLE_PAST_MS=6000, BUBBLE_PASS=70, BUBBLE_ROUND=10;
1534
  let bubbleTimer=null, bubbleHintTimer=null, bubblePastTimer=null, bubbleDeadline=0, bubbleFor=null, bubbleResolving=false, bubbleHintShown=false, bubblePastShown=false;
1535
  let bubbleGame={answered:0,correct:0,total:0};
1536
  const bubbleCat=()=>!daily&&(categoryOf(cat)||{}).bubble;
 
1545
  drawBubbleScore(correct);
1546
  }
1547
  async function collectBead(result){
1548
+ const source=$("prompt"), shelf=$("bubbleBeads"), from=source.getBoundingClientRect(), tone=result==="clean"?" good":result==="assisted"?" assisted":result==="failed"?" failed":"", mini=mark(`mini-bead${tone}`,"");
1549
  shelf.append(mini); const to=mini.getBoundingClientRect(); mini.style.visibility="hidden"; source.classList.add("collected");
1550
  const ghost=mark(`bead-flight${tone}`,""); Object.assign(ghost.style,{left:from.left+"px",top:from.top+"px",width:from.width+"px",height:from.height+"px"}); document.body.append(ghost);
1551
  if(!matchMedia("(prefers-reduced-motion:reduce)").matches){
1552
+ const chain=shelf.getBoundingClientRect(), origin={x:from.left+from.width/2,y:from.top+from.height/2}, scale=to.width/from.width;
1553
+ const point=(x,y,s=scale)=>({transform:`translate(${x-origin.x}px,${y-origin.y}px) scale(${s})`});
1554
+ const entry={x:chain.left+chain.width*36/150,y:chain.top+chain.height*14/88};
1555
+ const bendTop={x:chain.left+chain.width*120/150,y:entry.y};
1556
+ const bendMid={x:chain.left+chain.width*134/150,y:chain.top+chain.height*25/88};
1557
+ const bendBottom={x:bendTop.x,y:chain.top+chain.height*36/88};
1558
+ const target={x:to.left+to.width/2,y:to.top+to.height/2}, n=shelf.querySelectorAll(".mini-bead").length;
1559
+ const frames=[{transform:"translate(0,0) scale(1)"},point(entry.x,entry.y)];
1560
+ if(n<=5) frames.push(point(bendTop.x,bendTop.y),point(bendMid.x,bendMid.y),point(bendBottom.x,bendBottom.y));
1561
+ frames.push(point(target.x,target.y));
1562
+ try{ await ghost.animate(frames,{duration:n<=5?900:520,easing:"ease-in-out",fill:"forwards"}).finished; }catch{}
1563
  }
1564
  ghost.remove(); mini.style.visibility=""; mini.classList.add("landed");
1565
  }
 
2302
  }
2303
 
2304
  async function bubbleOutcome(r,expired=false){
2305
+ const id=cur.id, correct=!r.unrelated&&r.score>=BUBBLE_PASS;
2306
  if(bubbleResolving) return;
2307
  bubbleResolving=true;
2308
  if(!expired){
 
2311
  $("prompt").append(mark(`bubble-grade ${band}`,`${score} נק׳`));
2312
  }
2313
  if(correct){
2314
+ const result=bubblePastShown?"assisted":bubbleHintShown?"hinted":"clean";
2315
  countBubble(true);
2316
  stopBubble();
2317
+ await collectBead(result);
2318
  }else{
2319
  if(!expired&&Date.now()<bubbleDeadline){
2320
  bubbleResolving=false;