3v324v23 commited on
Commit
696d889
·
1 Parent(s): bb8c49a

Refactor code structure for improved readability and maintainability

Browse files
Files changed (2) hide show
  1. app/workbook.py +1 -1
  2. static/index.html +329 -21
app/workbook.py CHANGED
@@ -3988,7 +3988,7 @@ CATEGORIES = [{"id": f"l{lesson_id(*key)}-{tid}", "name": name, "lesson": lesson
3988
 
3989
  # Games live outside the lesson path. Their phrases use the same scoring bank, but they are not topics,
3990
  # do not affect lesson completion, and cannot be assigned as homework.
3991
- GAMES = [{"id": "game-yalla-verb", "name": "פועל נגד הזמן", "lesson": "1", "bubble": True, "hidden": False,
3992
  "unit": "פועל", "task": "אמרו את הפועל בערבית מדוברת: בעבר ואז בעתיד, לפני שהבועה נעלמת"}]
3993
 
3994
 
 
3988
 
3989
  # Games live outside the lesson path. Their phrases use the same scoring bank, but they are not topics,
3990
  # do not affect lesson completion, and cannot be assigned as homework.
3991
+ GAMES = [{"id": "game-yalla-verb", "name": "פועל זמני", "lesson": "1", "bubble": True, "hidden": False,
3992
  "unit": "פועל", "task": "אמרו את הפועל בערבית מדוברת: בעבר ואז בעתיד, לפני שהבועה נעלמת"}]
3993
 
3994
 
static/index.html CHANGED
@@ -169,6 +169,80 @@ body[data-view=verbs] #vids{max-width:none;gap:6px;margin-top:0}
169
  .vt-say .tr i{font-style:normal;opacity:.5;margin-inline:3px}
170
  @media (max-width:420px){.vt-row{grid-template-columns:minmax(0,7.2em) minmax(0,1fr)}}
171
  .vt-empty{display:none}.vt-none .vt-empty{display:block}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  #vids button.now{padding-block:16px;background:var(--olive);border-color:var(--olive);color:var(--on-olive);box-shadow:0 3px 0 var(--olive-deep);font-size:17px}
173
  #vids button.now i,#vids button.now small{color:inherit}
174
  /* Refer a friend: a compact campaign page inside the home navigation, with the two rewards kept visually distinct. */
@@ -802,7 +876,7 @@ body.driving .card.storymode .prompt{display:block}
802
  /* a story's quiz, once it ended: the grade is back on the card, over the mistakes made most often */
803
  .card.storymode.quizdone .verdict{display:flex}
804
  .tips li .sub{display:block}
805
- /* "פועל נגד הזמן": every prompt is a bead; correct answers join the misbaha above. */
806
  .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}
807
  @property --bead-hole{syntax:"<color>";inherits:true;initial-value:#151a13}
808
  @property --bead-neck{syntax:"<color>";inherits:true;initial-value:#899b79}
@@ -812,12 +886,12 @@ body.driving .card.storymode .prompt{display:block}
812
  @property --bead-c{syntax:"<color>";inherits:true;initial-value:#4a5941}
813
  @property --bead-d{syntax:"<color>";inherits:true;initial-value:#303a2b}
814
  @property --bead-ring{syntax:"<color>";inherits:true;initial-value:#aabd98}
815
- .card.bubblemode .prompt{--bead-hole:#151a13;--bead-neck:#899b79;--bead-light:#fff;--bead-a:#aabd98;--bead-b:#718363;--bead-c:#4a5941;--bead-d:#303a2b;--bead-ring:#aabd98;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%,var(--bead-hole) 0 7px,var(--bead-neck) 8px 11px,transparent 12px),radial-gradient(circle at 31% 25%,color-mix(in srgb,var(--bead-light) 72%,transparent) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,var(--bead-a) 0%,var(--bead-b) 37%,var(--bead-c) 72%,var(--bead-d) 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;transition:--bead-hole .4s ease,--bead-neck .4s ease,--bead-light .4s ease,--bead-a .4s ease,--bead-b .4s ease,--bead-c .4s ease,--bead-d .4s ease,--bead-ring .4s ease,border-color .4s ease,color .4s ease,text-shadow .4s ease}
816
  .card.bubblemode .prompt.bead-yellow{--bead-hole:#704708;--bead-neck:#d9a82c;--bead-light:#fff6ba;--bead-a:#ffe27a;--bead-b:#d7a423;--bead-c:#aa7110;--bead-d:#714507;--bead-ring:#e2b32e;border-color:#72500d;color:#3c2800;text-shadow:0 1px 0 rgb(255 247 194/.6)}
817
  .card.bubblemode .prompt.bead-orange{--bead-hole:#74360b;--bead-neck:#e8943c;--bead-light:#fff0d3;--bead-a:#ffbd72;--bead-b:#e78329;--bead-c:#c75f1d;--bead-d:#913c12;--bead-ring:#e78329;border-color:#924d12;color:#481d05;text-shadow:0 1px 0 rgb(255 224 173/.55)}
818
  .card.bubblemode .prompt.bead-red{--bead-hole:#552622;--bead-neck:#d98980;--bead-light:#fff;--bead-a:#efaaa0;--bead-b:#d77970;--bead-c:#b85d55;--bead-d:#843e38;--bead-ring:#d77970;border-color:#7d3d37;color:#481713;text-shadow:0 1px 0 rgb(255 225 218/.55)}
819
  .card.bubblemode .prompt::before{content:"";position:absolute;inset:-10px;border-radius:50%;border:5px solid var(--bead-ring);clip-path:inset(0);animation:verb-time 9s linear both}
820
- .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%)}
821
  .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)}
822
  .card.bubblemode .bubble-grade::before{content:"★";margin-inline-end:4px;color:#fff;text-shadow:0 1px 1px rgb(92 58 0/.35)}
823
  .card.bubblemode .bubble-grade.good{background:linear-gradient(180deg,#ffe66f,#ffc52f);color:#513600}
@@ -841,24 +915,33 @@ body.driving .card.storymode .prompt{display:block}
841
  .misbaha-cord .imam{fill:url(#imamGold);stroke:#6e4508;stroke-width:1.2;filter:drop-shadow(1px 2px 1px rgb(0 0 0/.45))}
842
  .misbaha-cord .tassel-cap{fill:#191916;stroke:#090a08;stroke-width:1}
843
  .misbaha-cord .tassel-thread{fill:none;stroke:#171713;stroke-width:1.35;stroke-linecap:round;opacity:.95}
844
- .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)}
845
- .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)}
846
  .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}
847
  .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}
848
- .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)}
849
- .mini-bead.good::after{border-color:rgb(40 55 34/.65);box-shadow:0 0 0 1px rgb(226 234 216/.26)}
850
- .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)}
851
- .mini-bead.assisted::after{border-color:rgb(120 55 14/.62);box-shadow:0 0 0 1px rgb(255 224 173/.28)}
852
- .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)}
853
- .mini-bead.failed::after{border-color:rgb(108 43 38/.6);box-shadow:0 0 0 1px rgb(255 225 218/.28)}
854
  .mini-bead.landed{animation:mini-land .3s cubic-bezier(.2,1.7,.4,1)}
855
- .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}
856
- .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)}
857
- .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)}
858
- .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%)}
859
  .card.bubblemode .prompt.collected{visibility:hidden}
860
  .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}
861
  .bubble-hint[hidden]{display:none}
 
 
 
 
 
 
 
 
 
 
 
 
862
  .bubble-hint small{display:block;color:#527254;font-size:11px}
863
  .bubble-hint .binyan{display:block;margin-top:2px}
864
  .bubble-hint .gizra{display:block;margin-top:3px;color:#103f13;font:800 23px/1.35 var(--f-display);text-shadow:0 1px 0 #fff}
@@ -868,7 +951,7 @@ body.driving .card.storymode .prompt{display:block}
868
  .bubble-hint .past em{display:block;margin-top:2px;color:#527254;font:700 17px/1.2 sans-serif;font-style:normal;direction:rtl}
869
  .card.bubblemode .task{font-weight:700;color:var(--olive)}
870
  .card.bubblemode .donehint,.card.bubblemode .progress{position:relative;z-index:2}
871
- .card.bubblemode>:not(.prompt):not(.bubble-score):not(.bubble-beads):not(.bubble-hint){display:none!important}
872
  body.bubble-game #wrap>header,body.bubble-game #practice>.roundbar{display:none}
873
  #gameBack{display:none}
874
  body.bubble-game #gameBack{display:flex;position:fixed;top:max(12px,env(safe-area-inset-top));right:12px;z-index:12;width:40px;height:40px;align-items:center;justify-content:center;border:2px solid #111;border-radius:50%;background:#63bd4d;color:#111;padding:0;cursor:pointer;box-shadow:0 3px 0 #111}
@@ -968,7 +1051,7 @@ body.quiet :is(#mic,#playMine,#driveBtn,.phone-btn),body.driving :is(#quietBtn,#
968
  <main class="card" id="card" aria-live="polite">
969
  <div class="bubble-score" id="bubbleScore" hidden><span>ניקוד<small id="bubbleCount"></small></span><b id="bubblePoints">0</b></div>
970
  <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>
971
- <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>
972
  <div class="meta"><span id="counter"></span></div>
973
  <div class="progress" id="progress" hidden><i id="progressBar"></i></div>
974
  <div class="donehint" id="doneHint" hidden></div>
@@ -1424,7 +1507,10 @@ function drawNav(){ // the tabs, and the screen of videos when one is on show
1424
  switcher.append(mark("","↻","i"),mark("","פועל יוצא","b"),mark("","מהבנה לשליפה מהירה של הטיית הפועל","small"));
1425
  switcher.onclick=openVerbGame; box.append(switcher);
1426
  }
1427
- if(!games.length) box.append(mark("","המשחקים הראשונים יגיעו בקרוב.","p"));
 
 
 
1428
  for(const game of games){
1429
  const b=mark("now","","button"), done=PHRASES.filter(p=>p.cat===game.id&&best[p.id]!=null).length;
1430
  b.append(mark("","●","i"),mark("",game.name,"b"),mark("",done?`תורגלו ${done} פעלים`:"אמרו עבר ועתיד לפני שהבועה נעלמת","small"));
@@ -1490,7 +1576,7 @@ function drawNav(){ // the tabs, and the screen of videos when one is on show
1490
  for(const v of libVideos()) row(v,libWatched,"ihki.libWatched","");
1491
  }
1492
  }
1493
- /* The verb table. Its verbs are the "פועל נגד הזמן" game's: every verb once, in the lesson that first teaches it, with its
1494
  בניין and גזרה (workbook.py, `_game_verbs`). A student sees those of the lessons up to the one they are on, a teacher all. */
1495
  const VT_BINYANIM={ // the past and future pattern of each בניין, in the course's transliteration
1496
  "בניין ראשון":"פַעַל / פִעֵל", "בניין שני":"פַעַّל – יְפַעֵّל", "בניין שלישי":"פַאעַל – יְפַאעֵל", "בניין רביעי":"אַפְעַל",
@@ -1548,7 +1634,9 @@ function drawVerbTable(box){
1548
  const tips=mark("vt-tips","💡 הסברים","button"); tips.type="button"; // what the verbs of each גזרה share: shown, or put away to fit more
1549
  const setTips=(on,keep)=>{ tips.setAttribute("aria-pressed",on); cards.classList.toggle("bare",!on); if(keep) store.set("ihki.vtTips",on); };
1550
  const chosen=store.get("ihki.vtTips",null);
1551
- tips.onclick=()=>setTips(tips.getAttribute("aria-pressed")!=="true",true); setTips(chosen??true); label.append(tips);
 
 
1552
  bar.append(top,slide); box.append(bar,mark("vt-empty","לא נמצא פועל כזה.","p"),cards);
1553
  const filter=()=>{ // rows, then the cards left with none, are hidden
1554
  const q=vtPlain(vtQuery.trim());
@@ -1595,6 +1683,211 @@ function drawVerbTable(box){
1595
  // on a wide screen the table is meant to fit in one: unless the student chose, the explanations give way when it does not
1596
  if(chosen==null&&innerWidth>=900) requestAnimationFrame(()=>{ if(cards.getBoundingClientRect().bottom>innerHeight-$("nav").offsetHeight) setTips(false); });
1597
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1598
  function showView(view){ // "" is the path, which is laid out again: it was not while hidden
1599
  if(view) document.body.dataset.view=view; else delete document.body.dataset.view;
1600
  scrollTo(0,0); drawPath(true);
@@ -2080,7 +2373,7 @@ function armBubble(){
2080
  pauseBubble();
2081
  if(!bubbleCat()||home) return;
2082
  if(bubbleFor!==cur.id){
2083
- bubbleFor=cur.id; bubbleDeadline=Date.now()+BUBBLE_MS; bubbleResolving=false; bubbleHintShown=false; bubblePastShown=false;
2084
  $("bubbleHint").hidden=true; $("bubblePast").hidden=true;
2085
  }
2086
  const left=Math.max(0,bubbleDeadline-Date.now());
@@ -2929,11 +3222,26 @@ function render(r){
2929
  }
2930
  }
2931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2932
  async function bubbleOutcome(r,expired=false){
2933
  const id=cur.id, correct=!r.unrelated&&r.score>=BUBBLE_PASS;
2934
  if(bubbleResolving) return;
2935
  bubbleResolving=true;
2936
  if(!expired){
 
2937
  const old=$("prompt").querySelector(".bubble-grade"); if(old) old.remove();
2938
  const score=Math.round(r.score), band=score>=85?"good":score>=60?"mid":"bad";
2939
  $("prompt").append(mark(`bubble-grade ${band}`,`${score} נק׳`));
 
169
  .vt-say .tr i{font-style:normal;opacity:.5;margin-inline:3px}
170
  @media (max-width:420px){.vt-row{grid-template-columns:minmax(0,7.2em) minmax(0,1fr)}}
171
  .vt-empty{display:none}.vt-none .vt-empty{display:block}
172
+ #vids .vt-play-mg{margin-inline-start:4px;padding:2px 9px;border:1px solid #b67d10;border-radius:999px;background:linear-gradient(180deg,#ffe27a,#d7a423);color:#3c2800;font:800 12px/1.4 var(--f-body);display:inline-block}
173
+ /* "הפועל זכרון": the verbs of the verb table's lesson as a memory game on a misbaha that snakes over the whole screen.
174
+ A bead shows gold, turning on its cord; open, a verb: the Hebrew, or the Arabic (said aloud) in transliteration. A found
175
+ pair turns green and stays open. The cord, the imam and the tassel are those of "פועל זמני", grown to the screen. */
176
+ body.memory-game{overflow:hidden}
177
+ .mg{position:fixed;inset:0;z-index:40;display:flex;flex-direction:column;background:#101510 url("/static/assets/arabic-calligraphy-path.webp?v=7") top center/100% auto repeat-y;color:#f7faef;user-select:none;-webkit-user-select:none;touch-action:manipulation;overflow:hidden}
178
+ .mg-hud{position:relative;z-index:3;display:flex;align-items:center;gap:8px;padding:max(10px,env(safe-area-inset-top)) 12px 4px}
179
+ .mg-hud .mg-title{flex:1;min-width:0}
180
+ .mg-hud h2{margin:0;font:400 clamp(20px,5.4vw,30px)/1.1 var(--f-display);color:#ffe27a;text-shadow:0 2px 0 #714507,0 4px 10px rgb(0 0 0/.5)}
181
+ .mg-hud .mg-title small{display:block;color:#c9d6bb;font:700 12px/1.3 var(--f-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
182
+ .mg-close{flex:none;display:grid;place-items:center;width:40px;height:40px;padding:0;border:2px solid #111;border-radius:50%;background:#63bd4d;color:#111;font:800 18px/1 var(--f-body);box-shadow:0 3px 0 #111;cursor:pointer}
183
+ .mg-close:active{transform:translateY(2px);box-shadow:0 1px 0 #111}
184
+ .mg-stat{flex:none;min-width:58px;padding:5px 9px;border:2px solid #205f20;border-radius:12px;background:rgb(255 255 255/.92);color:#174f18;box-shadow:0 3px 0 #205f20;text-align:center;font:800 11px/1 var(--f-body)}
185
+ .mg-stat b{display:block;font:800 22px/1 var(--f-display);font-variant-numeric:tabular-nums}
186
+ .mg-stat.tick b{animation:score-pop .38s cubic-bezier(.2,1.8,.4,1)}
187
+ .mg-combo{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding:3px 12px;border-radius:999px;background:linear-gradient(180deg,#ffbd72,#e78329);color:#481d05;font:800 14px/1.3 var(--f-body);box-shadow:0 3px 0 #913c12;white-space:nowrap;animation:mg-combo .9s cubic-bezier(.2,1.8,.4,1) both;pointer-events:none}
188
+ .mg-field{position:relative;flex:1;min-height:0}
189
+ .mg-field>svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
190
+ .mg-cord path{fill:none;stroke-linecap:round;stroke-linejoin:round}
191
+ .mg-cord .c-shadow{stroke:#38280a;stroke-width:calc(var(--d) * .1);filter:drop-shadow(2px 5px 3px rgb(0 0 0/.6))}
192
+ .mg-cord .c-base{stroke:#b67d10;stroke-width:calc(var(--d) * .068)}
193
+ .mg-cord .c-a,.mg-cord .c-b{stroke-width:calc(var(--d) * .054);stroke-dasharray:calc(var(--d) * .09) calc(var(--d) * .09)}
194
+ .mg-cord .c-a{stroke:#f3c64c}.mg-cord .c-b{stroke:#8e5b08;stroke-dashoffset:calc(var(--d) * .09)}
195
+ .mg-cord .c-glint{stroke:#fff1a6;stroke-width:calc(var(--d) * .016);stroke-dasharray:calc(var(--d) * .03) calc(var(--d) * .15);opacity:.8}
196
+ .mg-cord .imam{fill:url(#mgGold);stroke:#6e4508;stroke-width:2;filter:drop-shadow(2px 4px 2px rgb(0 0 0/.45))}
197
+ .mg-cord .tassel-cap{fill:#191916;stroke:#090a08;stroke-width:1.5}
198
+ .mg-cord .tassel-thread{fill:none;stroke:#171713;stroke-width:2.2;stroke-linecap:round}
199
+ .mg-bead{position:absolute;left:0;top:0;width:var(--d);height:var(--d);padding:0;border:0;border-radius:50%;background:none;color:inherit;cursor:pointer;will-change:transform;-webkit-tap-highlight-color:transparent}
200
+ .mg-bead:focus-visible{outline:3px solid #ffe27a;outline-offset:3px}
201
+ .mg-rot{position:absolute;inset:0;transform-style:preserve-3d;transform:perspective(600px) rotateY(0);transition:transform .6s cubic-bezier(.3,1.55,.5,1);animation:mg-in .55s cubic-bezier(.2,1.7,.4,1) backwards;animation-delay:calc(var(--i) * 45ms)}
202
+ .mg-bead:not(.open):hover .mg-rot{transform:perspective(600px) rotateY(0) scale(1.08) rotate(-4deg)}
203
+ .mg-bead.open .mg-rot{transform:perspective(600px) rotateY(180deg)}
204
+ .mg-bead.miss .mg-rot{animation:mg-miss .5s ease-in-out}
205
+ .mg-bead.done .mg-rot{animation:mg-pop .6s cubic-bezier(.2,1.8,.4,1)}
206
+ .mg-bead.done{cursor:default}
207
+ .mg-side{position:absolute;inset:0;border-radius:50%;backface-visibility:hidden;-webkit-backface-visibility:hidden;overflow:hidden}
208
+ .mg-back{border:2px solid #72500d;background:radial-gradient(circle at 29% 23%,#fff1a0 0 7%,transparent 10%),radial-gradient(circle at 38% 34%,#ffe27a,#d7a423 44%,#8e5b08 100%);box-shadow:inset calc(var(--d) * -.07) calc(var(--d) * -.09) calc(var(--d) * .13) rgb(91 55 4/.38),0 calc(var(--d) * .08) calc(var(--d) * .12) rgb(0 0 0/.42)}
209
+ .mg-back i{position:absolute;inset:0;border-radius:50%;background:repeating-linear-gradient(180deg,rgb(255 248 200/.3) 0 5%,transparent 5% 14%,rgb(112 71 8/.22) 14% 18%,transparent 18% 25%);background-size:100% 200%;-webkit-mask:radial-gradient(circle,#000 30%,transparent 74%);mask:radial-gradient(circle,#000 30%,transparent 74%);animation:mg-band var(--spin,5s) linear infinite var(--way,normal)} /* the bands run over the top: the bead turning round its horizontal axis */
210
+ .mg-face{transform:rotateY(180deg);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:10%;text-align:center;border:3px solid #c9a24a;background:radial-gradient(circle at 30% 24%,#fff 0 6%,transparent 9%),radial-gradient(circle at 38% 32%,#fffdf3,#f6e7bd 55%,#d9b86a 100%);color:#3c2800;box-shadow:inset calc(var(--d) * -.07) calc(var(--d) * -.09) calc(var(--d) * .13) rgb(120 80 10/.3),0 calc(var(--d) * .08) calc(var(--d) * .12) rgb(0 0 0/.42)}
211
+ .mg-face.he{font:800 calc(var(--d) * .175)/1.15 var(--f-body)}
212
+ .mg-face.ar{border-color:#924d12;background:radial-gradient(circle at 30% 24%,#fff 0 6%,transparent 9%),radial-gradient(circle at 38% 32%,#fff0d3,#ffbd72 55%,#e78329 100%);color:#481d05}
213
+ .mg-face .tr{display:block;font:800 calc(var(--d) * .15)/1.3 var(--f-tr);letter-spacing:.02em;white-space:nowrap}
214
+ .mg-face .ear{font-size:calc(var(--d) * .11);line-height:1;margin-top:2px;opacity:.75}
215
+ .mg-bead.done .mg-face{border-color:#1b2118;background:radial-gradient(circle at 30% 24%,rgb(255 255 255/.7) 0 6%,transparent 9%),radial-gradient(circle at 38% 34%,#c4d6b0,#8ea57b 45%,#4a5941 100%);color:#f7faef;text-shadow:0 1px 2px rgb(0 0 0/.55)}
216
+ .mg-bead.done .mg-face::after{content:"✓";position:absolute;top:6%;left:50%;transform:translateX(-50%);font:800 calc(var(--d) * .13)/1 var(--f-body);color:#e9ffd8}
217
+ .mg-spark{position:absolute;left:0;top:0;z-index:4;font-style:normal;font-size:calc(var(--d) * .22);pointer-events:none;animation:mg-fly .95s cubic-bezier(.15,.7,.3,1) forwards}
218
+ .mg-praise{position:absolute;left:0;top:0;z-index:5;padding:4px 16px;border:3px solid #fff;border-radius:999px;background:linear-gradient(180deg,#ffe66f,#ffc52f);color:#513600;box-shadow:0 4px 0 #a96800,0 10px 20px rgb(0 0 0/.35);font:400 clamp(22px,6vw,34px)/1.2 var(--f-tr);white-space:nowrap;pointer-events:none;animation:mg-praise 1.3s cubic-bezier(.2,1.6,.4,1) forwards}
219
+ .mg-win{position:absolute;inset:0;z-index:6;display:grid;place-items:center;padding:16px;background:rgb(10 14 9/.55);backdrop-filter:blur(2px);animation:mg-fade .3s ease-out both}
220
+ .mg-win>div{width:min(360px,100%);padding:22px 20px 18px;border:3px solid #205f20;border-radius:24px;background:#fffdf3;color:#174f18;text-align:center;box-shadow:0 6px 0 #205f20,0 24px 50px rgb(0 0 0/.45);animation:mg-pop-flat .7s cubic-bezier(.2,1.8,.4,1) both}
221
+ .mg-win .big{font-size:56px;line-height:1;animation:mg-wiggle 1.2s ease-in-out infinite}
222
+ .mg-win h3{margin:8px 0 2px;font:400 32px/1.15 var(--f-tr);color:#103f13}
223
+ .mg-win p{margin:4px 0 0;font:700 16px/1.4 var(--f-body);color:#527254}
224
+ .mg-win .stars{margin:8px 0 4px;font-size:38px;letter-spacing:6px;color:#f2a91d;text-shadow:0 2px 0 #a96800}
225
+ .mg-win .stars span{display:inline-block;animation:mg-pop-flat .5s cubic-bezier(.2,1.8,.4,1) backwards}
226
+ .mg-win .stars span:nth-child(2){animation-delay:.18s}.mg-win .stars span:nth-child(3){animation-delay:.36s}
227
+ .mg-win .stars .off{color:#d5dacb;text-shadow:none}
228
+ .mg-win .acts{display:flex;gap:10px;justify-content:center;margin-top:14px}
229
+ .mg-win button{padding:11px 18px;border:2px solid #111;border-radius:14px;background:#63bd4d;color:#111;font:800 17px/1 var(--f-body);box-shadow:0 4px 0 #111;cursor:pointer}
230
+ .mg-win button+button{background:#fff}
231
+ .mg-win button:active{transform:translateY(2px);box-shadow:0 2px 0 #111}
232
+ .mg-drop{position:absolute;top:-40px;z-index:5;width:var(--s);height:var(--s);border-radius:50%;pointer-events:none;background:radial-gradient(circle at 32% 26%,#fff 0 8%,transparent 11%),radial-gradient(circle at 38% 34%,var(--c1),var(--c2) 50%,var(--c3) 100%);box-shadow:0 3px 5px rgb(0 0 0/.35);animation:mg-drop var(--t) cubic-bezier(.4,.1,.7,1) var(--w) forwards}
233
+ .mg-none{margin:auto;padding:20px;max-width:320px;text-align:center;font:700 18px/1.5 var(--f-body)}
234
+ @keyframes mg-band{to{background-position:0 100%}}
235
+ @keyframes mg-fade{from{opacity:0}}
236
+ @keyframes mg-in{from{transform:perspective(600px) scale(0) rotate(-200deg)}}
237
+ @keyframes mg-miss{0%,100%{transform:perspective(600px) rotateY(180deg)}20%{transform:perspective(600px) rotateY(180deg) translateX(-9px) rotate(-6deg)}45%{transform:perspective(600px) rotateY(180deg) translateX(8px) rotate(5deg)}70%{transform:perspective(600px) rotateY(180deg) translateX(-5px)}}
238
+ @keyframes mg-pop{0%{transform:perspective(600px) rotateY(180deg) scale(1)}40%{transform:perspective(600px) rotateY(180deg) scale(1.3) rotate(8deg)}70%{transform:perspective(600px) rotateY(180deg) scale(.94) rotate(-3deg)}100%{transform:perspective(600px) rotateY(180deg) scale(1)}}
239
+ @keyframes mg-pop-flat{0%{transform:scale(.3) rotate(-6deg);opacity:0}65%{transform:scale(1.08) rotate(2deg);opacity:1}100%{transform:none}}
240
+ @keyframes mg-fly{0%{transform:translate(var(--x),var(--y)) scale(.3);opacity:1}100%{transform:translate(calc(var(--x) + var(--dx)),calc(var(--y) + var(--dy))) scale(1.1) rotate(var(--r));opacity:0}}
241
+ @keyframes mg-praise{0%{transform:translate(var(--x),var(--y)) translate(-50%,-50%) scale(.2) rotate(-12deg);opacity:0}25%{transform:translate(var(--x),var(--y)) translate(-50%,-80%) scale(1.15) rotate(3deg);opacity:1}75%{transform:translate(var(--x),var(--y)) translate(-50%,-110%) scale(1);opacity:1}100%{transform:translate(var(--x),var(--y)) translate(-50%,-150%) scale(.9);opacity:0}}
242
+ @keyframes mg-combo{0%{transform:translateX(-50%) scale(.3);opacity:0}30%{transform:translateX(-50%) scale(1.2);opacity:1}80%{opacity:1}100%{transform:translateX(-50%) translateY(10px) scale(1);opacity:0}}
243
+ @keyframes mg-wiggle{0%,100%{transform:rotate(-8deg)}50%{transform:rotate(8deg) scale(1.1)}}
244
+ @keyframes mg-drop{to{transform:translateY(calc(100vh + 80px)) rotate(var(--r))}}
245
+ @media (prefers-reduced-motion:reduce){.mini-bead::after,.card.bubblemode .prompt::after,.mg-back i,.mg-win .big{animation:none}.mg-rot{transition-duration:.2s}}
246
  #vids button.now{padding-block:16px;background:var(--olive);border-color:var(--olive);color:var(--on-olive);box-shadow:0 3px 0 var(--olive-deep);font-size:17px}
247
  #vids button.now i,#vids button.now small{color:inherit}
248
  /* Refer a friend: a compact campaign page inside the home navigation, with the two rewards kept visually distinct. */
 
876
  /* a story's quiz, once it ended: the grade is back on the card, over the mistakes made most often */
877
  .card.storymode.quizdone .verdict{display:flex}
878
  .tips li .sub{display:block}
879
+ /* "פועל זמני": every prompt is a bead; correct answers join the misbaha above. */
880
  .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}
881
  @property --bead-hole{syntax:"<color>";inherits:true;initial-value:#151a13}
882
  @property --bead-neck{syntax:"<color>";inherits:true;initial-value:#899b79}
 
886
  @property --bead-c{syntax:"<color>";inherits:true;initial-value:#4a5941}
887
  @property --bead-d{syntax:"<color>";inherits:true;initial-value:#303a2b}
888
  @property --bead-ring{syntax:"<color>";inherits:true;initial-value:#aabd98}
889
+ .card.bubblemode .prompt{--bead-hole:#151a13;--bead-neck:#899b79;--bead-light:#fff;--bead-a:#aabd98;--bead-b:#718363;--bead-c:#4a5941;--bead-d:#303a2b;--bead-ring:#aabd98;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(ellipse 7% 11% at 1.5% 50%,var(--bead-hole) 55%,transparent 72%),radial-gradient(ellipse 7% 11% at 98.5% 50%,var(--bead-hole) 55%,transparent 72%),radial-gradient(circle at 31% 25%,color-mix(in srgb,var(--bead-light) 72%,transparent) 0 4%,transparent 5%),radial-gradient(circle at 38% 34%,var(--bead-a) 0%,var(--bead-b) 37%,var(--bead-c) 72%,var(--bead-d) 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;transition:--bead-hole .4s ease,--bead-neck .4s ease,--bead-light .4s ease,--bead-a .4s ease,--bead-b .4s ease,--bead-c .4s ease,--bead-d .4s ease,--bead-ring .4s ease,border-color .4s ease,color .4s ease,text-shadow .4s ease}
890
  .card.bubblemode .prompt.bead-yellow{--bead-hole:#704708;--bead-neck:#d9a82c;--bead-light:#fff6ba;--bead-a:#ffe27a;--bead-b:#d7a423;--bead-c:#aa7110;--bead-d:#714507;--bead-ring:#e2b32e;border-color:#72500d;color:#3c2800;text-shadow:0 1px 0 rgb(255 247 194/.6)}
891
  .card.bubblemode .prompt.bead-orange{--bead-hole:#74360b;--bead-neck:#e8943c;--bead-light:#fff0d3;--bead-a:#ffbd72;--bead-b:#e78329;--bead-c:#c75f1d;--bead-d:#913c12;--bead-ring:#e78329;border-color:#924d12;color:#481d05;text-shadow:0 1px 0 rgb(255 224 173/.55)}
892
  .card.bubblemode .prompt.bead-red{--bead-hole:#552622;--bead-neck:#d98980;--bead-light:#fff;--bead-a:#efaaa0;--bead-b:#d77970;--bead-c:#b85d55;--bead-d:#843e38;--bead-ring:#d77970;border-color:#7d3d37;color:#481713;text-shadow:0 1px 0 rgb(255 225 218/.55)}
893
  .card.bubblemode .prompt::before{content:"";position:absolute;inset:-10px;border-radius:50%;border:5px solid var(--bead-ring);clip-path:inset(0);animation:verb-time 9s linear both}
894
+ .card.bubblemode .prompt::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:50%;background:repeating-linear-gradient(180deg,rgb(255 255 255/.2) 0 5%,transparent 5% 14%,rgb(0 0 0/.14) 14% 18%,transparent 18% 25%) 0 0/100% 200%;-webkit-mask:radial-gradient(circle,#000 30%,transparent 74%);mask:radial-gradient(circle,#000 30%,transparent 74%);animation:mg-band 5.6s linear infinite;pointer-events:none} /* the beads of "הפועל זכרון": they turn round their horizontal axis */
895
  .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)}
896
  .card.bubblemode .bubble-grade::before{content:"★";margin-inline-end:4px;color:#fff;text-shadow:0 1px 1px rgb(92 58 0/.35)}
897
  .card.bubblemode .bubble-grade.good{background:linear-gradient(180deg,#ffe66f,#ffc52f);color:#513600}
 
915
  .misbaha-cord .imam{fill:url(#imamGold);stroke:#6e4508;stroke-width:1.2;filter:drop-shadow(1px 2px 1px rgb(0 0 0/.45))}
916
  .misbaha-cord .tassel-cap{fill:#191916;stroke:#090a08;stroke-width:1}
917
  .misbaha-cord .tassel-thread{fill:none;stroke:#171713;stroke-width:1.35;stroke-linecap:round;opacity:.95}
918
+ .mini-bead{position:relative;z-index:1;width:19px;height:19px;border:1px solid #72500d;border-radius:50%;background:radial-gradient(ellipse 9% 14% at 2% 50%,#704708 55%,transparent 75%),radial-gradient(ellipse 9% 14% at 98% 50%,#704708 55%,transparent 75%),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)}
919
+ .mini-bead::after{content:"";position:absolute;inset:0;border-radius:50%;background:repeating-linear-gradient(180deg,rgb(255 255 255/.24) 0 6%,transparent 6% 15%,rgb(0 0 0/.16) 15% 19%,transparent 19% 25%) 0 0/100% 200%;-webkit-mask:radial-gradient(circle,#000 30%,transparent 74%);mask:radial-gradient(circle,#000 30%,transparent 74%);animation:mg-band 5s linear infinite}
920
  .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}
921
  .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}
922
+ .mini-bead.good{border-color:#1b2118;background:radial-gradient(ellipse 9% 14% at 2% 50%,#151a13 55%,transparent 75%),radial-gradient(ellipse 9% 14% at 98% 50%,#151a13 55%,transparent 75%),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)}
923
+ .mini-bead.assisted{border-color:#924d12;background:radial-gradient(ellipse 9% 14% at 2% 50%,#74360b 55%,transparent 75%),radial-gradient(ellipse 9% 14% at 98% 50%,#74360b 55%,transparent 75%),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)}
924
+ .mini-bead.failed{border-color:#7d3d37;background:radial-gradient(ellipse 9% 14% at 2% 50%,#552622 55%,transparent 75%),radial-gradient(ellipse 9% 14% at 98% 50%,#552622 55%,transparent 75%),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)}
 
 
 
925
  .mini-bead.landed{animation:mini-land .3s cubic-bezier(.2,1.7,.4,1)}
926
+ .bead-flight{position:fixed;z-index:30;margin:0;border:3px solid #72500d;border-radius:50%;background:radial-gradient(ellipse 7% 11% at 1.5% 50%,#704708 55%,transparent 72%),radial-gradient(ellipse 7% 11% at 98.5% 50%,#704708 55%,transparent 72%),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}
927
+ .bead-flight.good{border-color:#1b2118;background:radial-gradient(ellipse 7% 11% at 1.5% 50%,#151a13 55%,transparent 72%),radial-gradient(ellipse 7% 11% at 98.5% 50%,#151a13 55%,transparent 72%),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)}
928
+ .bead-flight.assisted{border-color:#924d12;background:radial-gradient(ellipse 7% 11% at 1.5% 50%,#74360b 55%,transparent 72%),radial-gradient(ellipse 7% 11% at 98.5% 50%,#74360b 55%,transparent 72%),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)}
929
+ .bead-flight.failed{border-color:#7d3d37;background:radial-gradient(ellipse 7% 11% at 1.5% 50%,#552622 55%,transparent 72%),radial-gradient(ellipse 7% 11% at 98.5% 50%,#552622 55%,transparent 72%),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%)}
930
  .card.bubblemode .prompt.collected{visibility:hidden}
931
  .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}
932
  .bubble-hint[hidden]{display:none}
933
+ /* under the bead: the student's try as a WhatsApp text message (one: a new try takes its place), and the hint under it */
934
+ .bubble-foot{display:none}
935
+ .card.bubblemode .bubble-foot{position:absolute;left:50%;bottom:18px;z-index:3;display:flex;flex-direction:column;align-items:center;gap:10px;width:min(300px,calc(100% - 32px));transform:translateX(-50%)}
936
+ .bubble-foot .bubble-hint{position:static;left:auto;bottom:auto;animation:wa-in .25s ease-out both}
937
+ .wa-msg{position:relative;align-self:flex-end;display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:flex-end;gap:0 8px;max-width:min(272px,100%);padding:6px 8px 5px 9px;border-radius:0 8px 8px 8px;background:#d9fdd3;color:#111b21;box-shadow:0 1px .5px rgb(11 20 26/.13),0 6px 16px rgb(0 0 0/.25);animation:wa-in .35s cubic-bezier(.2,1.5,.4,1) both}
938
+ .wa-msg::before{content:"";position:absolute;top:0;right:100%;width:8px;height:13px;background:#d9fdd3;clip-path:polygon(0 0,100% 0,100% 100%)} /* the tail, toward the speaker's side */
939
+ .wa-text{flex:1 1 auto;font:700 19px/1.45 var(--f-tr);letter-spacing:.02em;overflow-wrap:anywhere}
940
+ .wa-msg.empty .wa-text{font:italic 400 14px/1.4 var(--f-body);color:#667781}
941
+ .wa-when{display:inline-flex;align-items:center;gap:3px;margin-bottom:1px;color:#667781;font:400 11px/1 -apple-system,"Segoe UI",Arial,sans-serif;font-variant-numeric:tabular-nums;white-space:nowrap}
942
+ .wa-ticks{display:inline-flex;color:#8696a0}.wa-ticks svg{width:16px;height:11px;fill:currentColor}
943
+ .wa-msg.read .wa-ticks{color:#53bdeb}
944
+ @keyframes wa-in{from{opacity:0;transform:translateY(10px) scale(.96)}}
945
  .bubble-hint small{display:block;color:#527254;font-size:11px}
946
  .bubble-hint .binyan{display:block;margin-top:2px}
947
  .bubble-hint .gizra{display:block;margin-top:3px;color:#103f13;font:800 23px/1.35 var(--f-display);text-shadow:0 1px 0 #fff}
 
951
  .bubble-hint .past em{display:block;margin-top:2px;color:#527254;font:700 17px/1.2 sans-serif;font-style:normal;direction:rtl}
952
  .card.bubblemode .task{font-weight:700;color:var(--olive)}
953
  .card.bubblemode .donehint,.card.bubblemode .progress{position:relative;z-index:2}
954
+ .card.bubblemode>:not(.prompt):not(.bubble-score):not(.bubble-beads):not(.bubble-foot){display:none!important}
955
  body.bubble-game #wrap>header,body.bubble-game #practice>.roundbar{display:none}
956
  #gameBack{display:none}
957
  body.bubble-game #gameBack{display:flex;position:fixed;top:max(12px,env(safe-area-inset-top));right:12px;z-index:12;width:40px;height:40px;align-items:center;justify-content:center;border:2px solid #111;border-radius:50%;background:#63bd4d;color:#111;padding:0;cursor:pointer;box-shadow:0 3px 0 #111}
 
1051
  <main class="card" id="card" aria-live="polite">
1052
  <div class="bubble-score" id="bubbleScore" hidden><span>ניקוד<small id="bubbleCount"></small></span><b id="bubblePoints">0</b></div>
1053
  <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>
1054
+ <div class="bubble-foot" id="bubbleFoot"><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></div>
1055
  <div class="meta"><span id="counter"></span></div>
1056
  <div class="progress" id="progress" hidden><i id="progressBar"></i></div>
1057
  <div class="donehint" id="doneHint" hidden></div>
 
1507
  switcher.append(mark("","↻","i"),mark("","פועל יוצא","b"),mark("","מהבנה לשליפה מהירה של הטיית הפועל","small"));
1508
  switcher.onclick=openVerbGame; box.append(switcher);
1509
  }
1510
+ const memo=mark("now","","button"), upto=vtLessonNow(); // the memory game, on the verb table's lesson
1511
+ memo.append(mark("","📿","i"),mark("","הפועל זכרון","b"),mark("",`הפכו חרוזים ומצאו זוגות של פעלים · עד ${(ALL_LESSONS.find(l=>l.id===upto)||{}).name||""}`,"small"));
1512
+ memo.onclick=openMemoryGame; if(upto) box.append(memo);
1513
+ if(!games.length&&!upto) box.append(mark("","המשחקים הראשונים יגיעו בקרוב.","p"));
1514
  for(const game of games){
1515
  const b=mark("now","","button"), done=PHRASES.filter(p=>p.cat===game.id&&best[p.id]!=null).length;
1516
  b.append(mark("","●","i"),mark("",game.name,"b"),mark("",done?`תורגלו ${done} פעלים`:"אמרו עבר ועתיד לפני שהבועה נעלמת","small"));
 
1576
  for(const v of libVideos()) row(v,libWatched,"ihki.libWatched","");
1577
  }
1578
  }
1579
+ /* The verb table. Its verbs are the "פועל זמני" game's: every verb once, in the lesson that first teaches it, with its
1580
  בניין and גזרה (workbook.py, `_game_verbs`). A student sees those of the lessons up to the one they are on, a teacher all. */
1581
  const VT_BINYANIM={ // the past and future pattern of each בניין, in the course's transliteration
1582
  "בניין ראשון":"פַעַל / פִעֵל", "בניין שני":"פַעַّל – יְפַעֵّל", "בניין שלישי":"פַאעַל – יְפַאעֵל", "בניין רביעי":"אַפְעַל",
 
1634
  const tips=mark("vt-tips","💡 הסברים","button"); tips.type="button"; // what the verbs of each גזרה share: shown, or put away to fit more
1635
  const setTips=(on,keep)=>{ tips.setAttribute("aria-pressed",on); cards.classList.toggle("bare",!on); if(keep) store.set("ihki.vtTips",on); };
1636
  const chosen=store.get("ihki.vtTips",null);
1637
+ tips.onclick=()=>setTips(tips.getAttribute("aria-pressed")!=="true",true); setTips(chosen??true);
1638
+ const play=mark("vt-play-mg","📿 הפועל זכרון","button"); play.type="button"; play.title="הפועל זכרון: משחק זיכרון על הפעלים עד השיעור שבסרגל"; play.onclick=openMemoryGame;
1639
+ label.append(tips,play);
1640
  bar.append(top,slide); box.append(bar,mark("vt-empty","לא נמצא פועל כזה.","p"),cards);
1641
  const filter=()=>{ // rows, then the cards left with none, are hidden
1642
  const q=vtPlain(vtQuery.trim());
 
1683
  // on a wide screen the table is meant to fit in one: unless the student chose, the explanations give way when it does not
1684
  if(chosen==null&&innerWidth>=900) requestAnimationFrame(()=>{ if(cards.getBoundingClientRect().bottom>innerHeight-$("nav").offsetHeight) setTips(false); });
1685
  }
1686
+ /* "הפועל זכרון": the verbs up to the verb table's lesson (the student's own until the slider moves), about half of them
1687
+ that lesson's, each as two beads: its Hebrew and its Arabic. The beads hang on a cord that winds over the screen in rows,
1688
+ like a snake, and a wave runs down it; each bead turns on the cord. A bead opened with the Arabic says it. */
1689
+ const MG_PRAISE=["מֻמְתַאז!","יַא סַלַאם!","שַאטֵר!","בְּרַאבוֹ!","עַאש!","זַי אִלְעַסַל!","תִסְלַם!"];
1690
+ const MG_SPARKS=["✨","⭐","🌟","💛","💚","🎉"];
1691
+ let mg=null; // the game on show: its beads, its track and its counts
1692
+ function vtLessonNow(){ // the verb table's lesson: the slider's, or the student's own
1693
+ const lessons=LESSONS.filter(l=>!l.soon), rank=reachedRank();
1694
+ if(lessons.some(l=>l.id===vtAt)) return vtAt;
1695
+ return (lessons.find(l=>ALL_LESSONS.findIndex(x=>x.id===l.id)===rank)||lessons[0]||{}).id;
1696
+ }
1697
+ function mgPool(upto){ // every verb taught up to the lesson, each Hebrew once (עשה, ענה and נכנס name two verbs)
1698
+ const place=id=>ALL_LESSONS.findIndex(l=>l.id===id), u=place(upto), seen=new Set();
1699
+ return PHRASES.filter(p=>p.binyan&&GAMES.some(g=>g.id===p.cat)&&place(p.unlock)<=u).filter(p=>{
1700
+ const k=vtPlain(p.he).replace(/\s*(ו|,).*$/,""); return !seen.has(k)&&seen.add(k);
1701
+ });
1702
+ }
1703
+ function mgTone(notes,type="triangle",vol=.07){ // the game's small sounds: [frequency, seconds] one after another
1704
+ const ctx=unlockPlayback(); if(!ctx) return;
1705
+ let t=ctx.currentTime;
1706
+ for(const [f,d] of notes){
1707
+ const o=ctx.createOscillator(), g=ctx.createGain(); o.type=type; o.frequency.setValueAtTime(f,t);
1708
+ g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(.0001,t+d); o.connect(g).connect(ctx.destination); o.start(t); o.stop(t+d+.02); t+=d*.75;
1709
+ }
1710
+ }
1711
+ const MG_GAP=1.1; // bead to bead along the cord, in beads: close, as on a misbaha
1712
+ function mgRoom(W,H){ // how many beads fill the field: the snake in every row it has room for, beads still easy to read
1713
+ const d=W<600?78:W<1000?86:96, m=6+d*.2, g=d*1.5, rows=Math.max(1,Math.floor((H-2*m-d)/g)+1), R=g/2, run=Math.max(0,W-2*(m+d/2+R));
1714
+ return Math.max(2,Math.floor((rows*run+(rows-1)*Math.PI*R-d*1.95)/(d*MG_GAP))+1);
1715
+ }
1716
+ function mgTrack(W,H,n){ // the cord: rows across the field joined by half circles; the largest beads it holds, in as few rows as it takes
1717
+ for(let d=Math.min(150,Math.floor(Math.min(W,H)/2.2));d>=34;d-=2){
1718
+ const m=6+d*.2, g=d*1.5, most=Math.max(1,Math.floor((H-2*m-d)/g)+1), R=g/2, a=m+d/2+R, b=W-m-d/2-R; // m: room for the wave
1719
+ if(b<=a) continue;
1720
+ const need=d*1.95+(n-1)*d*MG_GAP;
1721
+ for(let rows=1;rows<=most;rows++){
1722
+ const total=rows*(b-a)+(rows-1)*Math.PI*R;
1723
+ if(total>=need||d===34&&rows===most) return {d,g,rows,R,a,b,total,top:(H-(rows-1)*g)/2};
1724
+ }
1725
+ }
1726
+ }
1727
+ function mgSamples(t){ // points along the cord every 3px, from the top row's right end, with the normal at each
1728
+ const pts=[], step=3, add=(x,y)=>pts.push({x,y});
1729
+ for(let r=0;r<t.rows;r++){
1730
+ const y=t.top+r*t.g, left=r%2===1, from=left?t.a:t.b, to=left?t.b:t.a, len=Math.abs(to-from);
1731
+ for(let s=0;s<len;s+=step) add(from+(to>from?s:-s),y);
1732
+ if(r<t.rows-1){ // the turn down to the next row, round the end it reached
1733
+ const cx=to, cy=y+t.R, side=to===t.a?-1:1, arc=Math.PI*t.R;
1734
+ for(let s=0;s<arc;s+=step){ const th=-Math.PI/2+s/t.R; add(cx+side*Math.cos(th)*t.R,cy+Math.sin(th)*t.R); }
1735
+ }else add(to,y);
1736
+ }
1737
+ for(let j=0;j<pts.length;j++){
1738
+ const p=pts[Math.max(0,j-1)], q=pts[Math.min(pts.length-1,j+1)], dx=q.x-p.x, dy=q.y-p.y, l=Math.hypot(dx,dy)||1;
1739
+ Object.assign(pts[j],{nx:-dy/l,ny:dx/l,s:j*step});
1740
+ }
1741
+ return pts;
1742
+ }
1743
+ function openMemoryGame(){
1744
+ const upto=vtLessonNow(); if(!upto) return;
1745
+ closeMemoryGame();
1746
+ const name=(ALL_LESSONS.find(l=>l.id===upto)||{}).name||"";
1747
+ const root=mark("mg","","section"); root.setAttribute("role","dialog"); root.setAttribute("aria-label","הפועל זכרון");
1748
+ root.innerHTML=`<div class="mg-hud"><button class="mg-close" type="button" aria-label="יציאה מהמשחק">✕</button>
1749
+ <div class="mg-title"><h2>הפועל זכרון</h2><small></small></div>
1750
+ <div class="mg-stat mg-pairs"><b>0</b>זוגות</div><div class="mg-stat mg-moves"><b>0</b>ניסיונות</div></div>
1751
+ <div class="mg-field"><svg class="mg-cord" aria-hidden="true"><defs><linearGradient id="mgGold" 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>
1752
+ <path class="c-shadow"/><path class="c-base"/><path class="c-a"/><path class="c-b"/><path class="c-glint"/><g class="mg-tassel"></g></svg></div>`;
1753
+ document.body.append(root); document.body.classList.add("memory-game");
1754
+ const field=root.querySelector(".mg-field"), hud=root.querySelector(".mg-hud");
1755
+ mg={root,field,hud,upto,name,beads:[],first:null,lock:false,pairs:0,moves:0,streak:0,start:0,raf:0,party:0,audio:null,
1756
+ paths:[...root.querySelectorAll(".mg-cord path")],tassel:root.querySelector(".mg-tassel"),still:matchMedia("(prefers-reduced-motion: reduce)").matches};
1757
+ root.querySelector(".mg-close").onclick=closeMemoryGame;
1758
+ mg.onkey=e=>{ if(e.key==="Escape") closeMemoryGame(); }; addEventListener("keydown",mg.onkey);
1759
+ mg.onresize=()=>mgLayout(); addEventListener("resize",mg.onresize);
1760
+ mgDeal();
1761
+ const frame=now=>{ if(!mg) return; mgFrame(now/1000); mg.raf=requestAnimationFrame(frame); };
1762
+ mg.raf=requestAnimationFrame(frame);
1763
+ }
1764
+ function closeMemoryGame(){
1765
+ if(!mg) return;
1766
+ cancelAnimationFrame(mg.raf); removeEventListener("keydown",mg.onkey); removeEventListener("resize",mg.onresize);
1767
+ if(mg.audio) mg.audio.pause();
1768
+ mg.root.remove(); document.body.classList.remove("memory-game"); mg=null;
1769
+ }
1770
+ function mgDeal(){ // a new round: as many pairs as fill the screen (or as the lessons have verbs), the lesson's own verbs first
1771
+ const pool=mgPool(mg.upto), box=mg.field.getBoundingClientRect(), n=Math.min(Math.floor(mgRoom(box.width,box.height)/2),pool.length);
1772
+ mg.root.querySelector(".mg-win")?.remove(); mg.beads.forEach(b=>b.el.remove());
1773
+ Object.assign(mg,{beads:[],first:null,lock:false,pairs:0,moves:0,streak:0,start:0,party:0,n});
1774
+ mg.root.querySelector(".mg-title small").textContent=`${n} זוגות · הפעלים עד ${mg.name} · פתחו שני חרוזים: עברית וערבית`;
1775
+ mgCount();
1776
+ if(n<2){ mg.field.append(mark("mg-none","עוד אין מספיק פעלים למשחק. הם יגיעו עם השיעורים הבאים 💛","p")); return; }
1777
+ const fresh=shuffle(pool.filter(p=>p.unlock===mg.upto)), rest=shuffle(pool.filter(p=>p.unlock!==mg.upto)), half=Math.ceil(n/2);
1778
+ const verbs=[...fresh.slice(0,half),...rest,...fresh.slice(half)].slice(0,n);
1779
+ for(const p of verbs) answerAudio(p.id); // fetched now, so a bead speaks as it opens
1780
+ const sides=shuffle(verbs.flatMap(p=>[{p,side:"he"},{p,side:"ar"}]));
1781
+ sides.forEach((b,i)=>{
1782
+ const el=mark("mg-bead","","button"), rot=mark("mg-rot","","span"), back=mark("mg-side mg-back","","span"), face=mark(`mg-side mg-face ${b.side}`,"","span");
1783
+ el.type="button"; el.setAttribute("aria-label","חרוז סגור"); el.style.setProperty("--i",i);
1784
+ el.style.setProperty("--spin",`${(3.75+Math.random()*5).toFixed(2)}s`); el.style.setProperty("--way",Math.random()<.5?"normal":"reverse");
1785
+ back.append(document.createElement("i"));
1786
+ if(b.side==="he") face.textContent=b.p.he;
1787
+ else{ const [past,future]=b.p.tr.split(/,\s*/); face.append(mark("tr",past,"bdi"),mark("tr",future||"","bdi"),mark("ear","🔊","span")); }
1788
+ rot.append(back,face); el.append(rot); el.onclick=()=>mgFlip(b);
1789
+ b.el=el; mg.beads.push(b); mg.field.append(el);
1790
+ });
1791
+ mgLayout();
1792
+ }
1793
+ function mgLayout(){ // the track for the field's size, and the beads' places along it
1794
+ if(!mg||!mg.beads.length) return;
1795
+ const r=mg.field.getBoundingClientRect(), t=mgTrack(r.width,r.height,mg.beads.length);
1796
+ mg.track=t; mg.pts=mgSamples(t); mg.root.style.setProperty("--d",`${t.d}px`);
1797
+ const n=mg.beads.length, start=t.d*1.35, gap=t.d*MG_GAP;
1798
+ mg.pts=mg.pts.filter(p=>p.s<=start+(n-1)*gap+t.d*.6); // the cord ends just past its last bead
1799
+ mg.beads.forEach((b,i)=>b.j=Math.min(mg.pts.length-1,Math.round((start+i*gap)/3)));
1800
+ mg.wave=Math.max(gap*2.4,t.d*3);
1801
+ }
1802
+ function mgFrame(time){ // the wave runs down the cord; the beads ride it, each turned along the cord
1803
+ const t=mg.track; if(!t) return;
1804
+ const pts=mg.pts, amp=mg.still?0:t.d*(mg.party?.05:.03), speed=mg.party?2.6:1, k=2*Math.PI/mg.wave, w=2*Math.PI/3.4*speed;
1805
+ const X=new Float32Array(pts.length), Y=new Float32Array(pts.length);
1806
+ let d="";
1807
+ for(let j=0;j<pts.length;j++){
1808
+ const p=pts[j], ease=Math.min(1,p.s/(t.d*2)), off=amp*ease*Math.sin(k*p.s-w*time);
1809
+ X[j]=p.x+p.nx*off; Y[j]=p.y+p.ny*off;
1810
+ if(j%2===0||j===pts.length-1) d+=`${j?"L":"M"}${X[j].toFixed(1)} ${Y[j].toFixed(1)}`;
1811
+ }
1812
+ for(const path of mg.paths) path.setAttribute("d",d);
1813
+ const angle=j=>{ const a=Math.max(0,j-2), b=Math.min(pts.length-1,j+2); return Math.atan2(Y[b]-Y[a],X[b]-X[a])*180/Math.PI; };
1814
+ for(const b of mg.beads){
1815
+ b.el.style.transform=`translate(${(X[b.j]-t.d/2).toFixed(1)}px,${(Y[b.j]-t.d/2).toFixed(1)}px)`;
1816
+ }
1817
+ // the imam and the tassel at the cord's head, turned along it, the tassel's threads swinging
1818
+ const j0=Math.round(t.d*.42/3), s=t.d/40, sway=mg.still?0:Math.sin(time*2.2)*6;
1819
+ mg.tassel.setAttribute("transform",`translate(${X[j0].toFixed(1)} ${Y[j0].toFixed(1)}) rotate(${(angle(j0)+180).toFixed(1)}) scale(${s.toFixed(3)})`);
1820
+ if(!mg.tassel.firstChild){
1821
+ mg.tassel.innerHTML=`<rect class="imam" x="-13" y="-6" width="26" height="12" rx="6"/><rect class="tassel-cap" x="13" y="-7" width="8" height="14" rx="2"/><g class="tassel-threads"></g>`;
1822
+ }
1823
+ const th=mg.tassel.lastChild, q=i=>(i-3.5)*3.2;
1824
+ th.innerHTML=[0,1,2,3,4,5,6,7].map(i=>`<path class="tassel-thread" d="M20 0Q${32+i} ${q(i)+sway*.6} ${44+(i%3)*4} ${q(i)*1.9+sway}"/>`).join("");
1825
+ }
1826
+ function mgCount(){
1827
+ const set=(cls,v)=>{ const box=mg.root.querySelector(cls), b=box.querySelector("b"); if(b.textContent!==String(v)){ b.textContent=v; box.classList.remove("tick"); void box.offsetWidth; box.classList.add("tick"); } };
1828
+ set(".mg-pairs",`${mg.pairs}/${mg.n||0}`); set(".mg-moves",mg.moves);
1829
+ }
1830
+ async function mgSay(p){ // the Arabic, in the lessons' voice, or else the device's
1831
+ if(mg.audio) mg.audio.pause();
1832
+ const a=await answerAudio(p.id); if(!mg) return;
1833
+ if(!a){ speakLocal(p.ar,"ar"); return; }
1834
+ const au=answerPlayer(a.url); mg.audio=au; au.play().catch(()=>speakLocal(p.ar,"ar"));
1835
+ }
1836
+ function mgAt(b){ const r=b.el.getBoundingClientRect(), f=mg.field.getBoundingClientRect(); return {x:r.left-f.left+r.width/2,y:r.top-f.top+r.height/2}; }
1837
+ function mgBurst(x,y,many=12){
1838
+ for(let i=0;i<many;i++){
1839
+ const s=mark("mg-spark",MG_SPARKS[i%MG_SPARKS.length],"i"), th=Math.random()*Math.PI*2, far=mg.track.d*(.7+Math.random()*.9);
1840
+ Object.entries({"--x":`${x-12}px`,"--y":`${y-12}px`,"--dx":`${Math.cos(th)*far}px`,"--dy":`${Math.sin(th)*far}px`,"--r":`${Math.random()*360-180}deg`}).forEach(([k,v])=>s.style.setProperty(k,v));
1841
+ s.onanimationend=()=>s.remove(); mg.field.append(s);
1842
+ }
1843
+ }
1844
+ function mgFlip(b){
1845
+ if(!mg||mg.lock||b.open||b.done) return;
1846
+ if(!mg.start) mg.start=Date.now();
1847
+ b.open=true; b.el.classList.add("open"); b.el.setAttribute("aria-label",b.side==="he"?b.p.he:b.p.tr);
1848
+ mgTone([[620,.06],[930,.07]]);
1849
+ if(b.side==="ar") mgSay(b.p);
1850
+ if(!mg.first){ mg.first=b; return; }
1851
+ const a=mg.first; mg.first=null; mg.moves++;
1852
+ if(a.p===b.p){ // a pair: both stay open, green, and cannot be pressed again
1853
+ mg.pairs++; mg.streak++;
1854
+ for(const x of [a,b]){ x.done=true; x.el.classList.add("done"); x.el.disabled=true; }
1855
+ const pa=mgAt(a), pb=mgAt(b);
1856
+ setTimeout(()=>{ if(!mg) return; mgBurst(pa.x,pa.y); mgBurst(pb.x,pb.y); mgTone([[660,.11],[880,.11],[1320,.22]],"sine",.08); },220);
1857
+ const praise=mark("mg-praise",MG_PRAISE[Math.random()*MG_PRAISE.length|0],"span");
1858
+ praise.style.setProperty("--x",`${(pa.x+pb.x)/2}px`); praise.style.setProperty("--y",`${Math.min(pa.y,pb.y)}px`);
1859
+ praise.onanimationend=()=>praise.remove(); mg.field.append(praise);
1860
+ if(mg.streak>1){ const c=mark("mg-combo",`🔥 ${mg.streak} ברצף!`,"span"); c.onanimationend=()=>c.remove(); mg.hud.append(c); }
1861
+ mgCount();
1862
+ if(mg.pairs===mg.n) setTimeout(mgWin,900);
1863
+ }else{ // not a pair: they shake, and close again
1864
+ mg.streak=0; mg.lock=true; mgCount();
1865
+ setTimeout(()=>{ if(!mg) return; a.el.classList.add("miss"); b.el.classList.add("miss"); mgTone([[240,.14],[180,.2]],"sawtooth",.035); },650);
1866
+ setTimeout(()=>{ if(!mg) return;
1867
+ for(const x of [a,b]){ x.open=false; x.el.classList.remove("open","miss"); x.el.setAttribute("aria-label","חרוז סגור"); }
1868
+ mg.lock=false;
1869
+ },1350);
1870
+ }
1871
+ }
1872
+ function mgWin(){ // every pair found: the misbaha dances, beads rain, and the stars
1873
+ if(!mg) return;
1874
+ mg.party=1; mgTone([[523,.14],[659,.14],[784,.14],[1047,.32],[784,.12],[1047,.4]],"sine",.09);
1875
+ const secs=Math.round((Date.now()-mg.start)/1000), stars=mg.moves<=mg.n*1.4?3:mg.moves<=mg.n*2?2:1;
1876
+ const tones=[["#e2ead8","#aabd98","#4a5941"],["#fff1a0","#ffe27a","#8e5b08"],["#ffe0ad","#ffbd72","#a94716"],["#ffe1da","#efaaa0","#9b4f48"]];
1877
+ for(let i=0;i<46;i++){
1878
+ const drop=mark("mg-drop","","i"), [c1,c2,c3]=tones[i%tones.length];
1879
+ Object.entries({"--c1":c1,"--c2":c2,"--c3":c3,"--s":`${14+Math.random()*22}px`,"--t":`${1.8+Math.random()*1.8}s`,"--w":`${Math.random()*1.4}s`,"--r":`${Math.random()*720-360}deg`}).forEach(([k,v])=>drop.style.setProperty(k,v));
1880
+ drop.style.left=`${Math.random()*100}%`; drop.onanimationend=()=>drop.remove(); mg.root.append(drop);
1881
+ }
1882
+ const win=mark("mg-win","","div"), card=document.createElement("div");
1883
+ card.innerHTML=`<div class="big">📿</div><h3>${MG_PRAISE[Math.random()*3|0]}</h3>
1884
+ <div class="stars">${[0,1,2].map(i=>`<span class="${i<stars?"":"off"}">★</span>`).join("")}</div>
1885
+ <p>${mg.n} זוגות ב־${mg.moves} ניסיונות · ${Math.floor(secs/60)}:${String(secs%60).padStart(2,"0")}</p>
1886
+ <div class="acts"><button type="button" class="again">עוד סיבוב 🔄</button><button type="button" class="out">סיום</button></div>`;
1887
+ win.append(card); mg.root.append(win);
1888
+ card.querySelector(".again").onclick=()=>mgDeal(); card.querySelector(".out").onclick=closeMemoryGame;
1889
+ card.querySelector(".again").focus({preventScroll:true});
1890
+ }
1891
  function showView(view){ // "" is the path, which is laid out again: it was not while hidden
1892
  if(view) document.body.dataset.view=view; else delete document.body.dataset.view;
1893
  scrollTo(0,0); drawPath(true);
 
2373
  pauseBubble();
2374
  if(!bubbleCat()||home) return;
2375
  if(bubbleFor!==cur.id){
2376
+ bubbleFor=cur.id; bubbleDeadline=Date.now()+BUBBLE_MS; bubbleResolving=false; bubbleHintShown=false; bubblePastShown=false; waClear();
2377
  $("bubbleHint").hidden=true; $("bubblePast").hidden=true;
2378
  }
2379
  const left=Math.max(0,bubbleDeadline-Date.now());
 
3222
  }
3223
  }
3224
 
3225
+ /* the student's try in "פועל זמני", as a WhatsApp text message: what was heard, in transliteration, with the time it was
3226
+ sent and its ticks, blue when it passed. One message: a new try takes the last one's place. */
3227
+ let waMsg=null;
3228
+ const WA_TICKS='<svg viewBox="0 0 16 11"><path d="M11.07.65 10.4.1a.5.5 0 0 0-.7.08L4.06 7.16 1.94 5.2a.5.5 0 0 0-.7.02l-.6.64a.5.5 0 0 0 .02.7l3.06 2.86a.5.5 0 0 0 .73-.05l6.7-8.1a.5.5 0 0 0-.08-.6zM15.07.65 14.4.1a.5.5 0 0 0-.7.08L8.06 7.16l-.8-.75-.97 1.17 1.3 1.22a.5.5 0 0 0 .73-.05l6.7-8.1a.5.5 0 0 0-.08-.6z"/></svg>';
3229
+ function waClear(){ if(waMsg){ waMsg.remove(); waMsg=null; } }
3230
+ function waText(r,passed){
3231
+ waClear();
3232
+ const heard=(r.words||[]).map(w=>w.heard_tr).filter(Boolean).join(" "), now=new Date();
3233
+ const el=mark(`wa-msg${passed?" read":""}`,"","div"), text=mark("wa-text","","bdi"), when=mark("wa-when",`${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}`,"span");
3234
+ if(heard) setTr(text,heard); else{ text.textContent="לא זוהה דיבור"; el.classList.add("empty"); }
3235
+ const ticks=mark("wa-ticks","","span"); ticks.innerHTML=WA_TICKS; when.append(ticks);
3236
+ el.append(text,when); el.setAttribute("aria-label",`מה שאמרתם: ${heard||"לא זוהה"}`);
3237
+ waMsg=el; $("bubbleFoot").prepend(el);
3238
+ }
3239
  async function bubbleOutcome(r,expired=false){
3240
  const id=cur.id, correct=!r.unrelated&&r.score>=BUBBLE_PASS;
3241
  if(bubbleResolving) return;
3242
  bubbleResolving=true;
3243
  if(!expired){
3244
+ waText(r,correct);
3245
  const old=$("prompt").querySelector(".bubble-grade"); if(old) old.remove();
3246
  const score=Math.round(r.score), band=score>=85?"good":score>=60?"mid":"bad";
3247
  $("prompt").append(mark(`bubble-grade ${band}`,`${score} נק׳`));