eklavya 1.85.2 → 1.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/dashboard.html +56 -8
  3. package/dist/assets/mascot/expressions/celebrate-body.svg +24 -8
  4. package/dist/assets/mascot/expressions/celebrate-face.svg +24 -8
  5. package/dist/assets/mascot/expressions/confused-body.svg +24 -8
  6. package/dist/assets/mascot/expressions/confused-face.svg +24 -8
  7. package/dist/assets/mascot/expressions/cool-body.svg +85 -0
  8. package/dist/assets/mascot/expressions/cool-face.svg +59 -0
  9. package/dist/assets/mascot/expressions/curious-body.svg +24 -8
  10. package/dist/assets/mascot/expressions/curious-face.svg +24 -8
  11. package/dist/assets/mascot/expressions/determined-body.svg +85 -0
  12. package/dist/assets/mascot/expressions/determined-face.svg +59 -0
  13. package/dist/assets/mascot/expressions/excited-body.svg +24 -8
  14. package/dist/assets/mascot/expressions/excited-face.svg +24 -8
  15. package/dist/assets/mascot/expressions/focused-body.svg +24 -8
  16. package/dist/assets/mascot/expressions/focused-face.svg +24 -8
  17. package/dist/assets/mascot/expressions/happy-body.svg +24 -8
  18. package/dist/assets/mascot/expressions/happy-face.svg +24 -8
  19. package/dist/assets/mascot/expressions/hopeful-body.svg +85 -0
  20. package/dist/assets/mascot/expressions/hopeful-face.svg +59 -0
  21. package/dist/assets/mascot/expressions/idea-body.svg +85 -0
  22. package/dist/assets/mascot/expressions/idea-face.svg +59 -0
  23. package/dist/assets/mascot/expressions/laugh-body.svg +85 -0
  24. package/dist/assets/mascot/expressions/laugh-face.svg +59 -0
  25. package/dist/assets/mascot/expressions/love-body.svg +85 -0
  26. package/dist/assets/mascot/expressions/love-face.svg +59 -0
  27. package/dist/assets/mascot/expressions/nervous-body.svg +85 -0
  28. package/dist/assets/mascot/expressions/nervous-face.svg +59 -0
  29. package/dist/assets/mascot/expressions/neutral-body.svg +24 -8
  30. package/dist/assets/mascot/expressions/neutral-face.svg +24 -8
  31. package/dist/assets/mascot/expressions/proud-body.svg +85 -0
  32. package/dist/assets/mascot/expressions/proud-face.svg +59 -0
  33. package/dist/assets/mascot/expressions/relieved-body.svg +85 -0
  34. package/dist/assets/mascot/expressions/relieved-face.svg +59 -0
  35. package/dist/assets/mascot/expressions/sad-body.svg +24 -8
  36. package/dist/assets/mascot/expressions/sad-face.svg +24 -8
  37. package/dist/assets/mascot/expressions/shy-body.svg +85 -0
  38. package/dist/assets/mascot/expressions/shy-face.svg +59 -0
  39. package/dist/assets/mascot/expressions/sleepy-body.svg +24 -8
  40. package/dist/assets/mascot/expressions/sleepy-face.svg +24 -8
  41. package/dist/assets/mascot/expressions/starstruck-body.svg +85 -0
  42. package/dist/assets/mascot/expressions/starstruck-face.svg +59 -0
  43. package/dist/assets/mascot/expressions/surprised-body.svg +24 -8
  44. package/dist/assets/mascot/expressions/surprised-face.svg +24 -8
  45. package/dist/assets/mascot/expressions/thinking-body.svg +24 -8
  46. package/dist/assets/mascot/expressions/thinking-face.svg +24 -8
  47. package/dist/assets/mascot/expressions/wink-body.svg +24 -8
  48. package/dist/assets/mascot/expressions/wink-face.svg +24 -8
  49. package/dist/assets/mascot/expressions/worried-body.svg +24 -8
  50. package/dist/assets/mascot/expressions/worried-face.svg +24 -8
  51. package/dist/assets/mascot/loading.svg +59 -36
  52. package/dist/assets/mascot/mascot.css +24 -8
  53. package/dist/assets/mascot/mascot.js +64 -7
  54. package/dist/assets/mascot.html +15 -11
  55. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  56. package/dist/plugin/hooks/CLAUDE.md +1 -0
  57. package/dist/plugin/hooks/panel/card.tsx +47 -0
  58. package/dist/plugin/hooks/panel/model.ts +43 -1
  59. package/dist/plugin/hooks/panel/register.tsx +101 -3
  60. package/dist/plugin/hooks/panel/types/index.d.ts +5 -0
  61. package/package.json +1 -1
@@ -18,19 +18,35 @@ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line
18
18
  .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
19
  .ek-mascot[data-variant="face"] { overflow:hidden; }
20
20
  .ek-mascot__face path:not([fill]) { fill:none; }
21
- .ek-mascot--loading .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
22
- .ek-mascot--loading .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
23
- .ek-mascot--loading .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
24
- .ek-mascot--loading .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
25
- @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
26
- @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
27
- @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
28
- @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
29
21
  .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
22
  .ek-companion--hero { margin-bottom:16px; }
31
23
  .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
24
  .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
25
  .ek-mascot-message p {margin:0}
26
+ /* Secondary loader: draws and releases an arrow over 2.8 s. */
27
+ .ek-mascot--draw .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
28
+ .ek-mascot--draw .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
29
+ .ek-mascot--draw .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
30
+ .ek-mascot--draw .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
31
+ @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
32
+ @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
33
+ @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
34
+ @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
35
+ /* Loader: walking toward the viewer, a 1.2 s stride (two steps). Ground rungs recede on an ease-out, as perspective slows them near the horizon. */
36
+ .ek-mascot--loading .ek-mascot__walker { animation:ek-sway 1.2s ease-in-out infinite; transform-origin:95px 185px; }
37
+ .ek-mascot--loading .ek-mascot__torso { animation:ek-bob .6s ease-in-out infinite; }
38
+ .ek-mascot--loading .ek-mascot__leg, .ek-mascot--loading .ek-mascot__hand { animation:ek-step 1.2s ease-in-out infinite; }
39
+ .ek-mascot--loading .ek-mascot__leg--r, .ek-mascot--loading .ek-mascot__hand--arrow { animation-delay:-.6s; }
40
+ .ek-mascot--loading .ek-mascot__shadow { animation:ek-shadow .6s ease-in-out infinite; transform-origin:95px 186px; }
41
+ .ek-mascot--loading .ek-mascot__rung { animation:ek-recede 1.2s cubic-bezier(.15,.6,.4,1) infinite; transform-origin:95px 162px; }
42
+ .ek-mascot--loading .ek-mascot__rung:nth-child(3) { animation-delay:-.3s; }
43
+ .ek-mascot--loading .ek-mascot__rung:nth-child(4) { animation-delay:-.6s; }
44
+ .ek-mascot--loading .ek-mascot__rung:nth-child(5) { animation-delay:-.9s; }
45
+ @keyframes ek-sway { 0%,100% {transform:rotate(-1.5deg)} 50% {transform:rotate(1.5deg)} }
46
+ @keyframes ek-bob { 0%,100% {transform:translateY(1px)} 50% {transform:translateY(-3px)} }
47
+ @keyframes ek-step { 0%,50%,100% {transform:translateY(0)} 25% {transform:translateY(-6px)} }
48
+ @keyframes ek-shadow { 0%,100% {transform:scaleX(1)} 50% {transform:scaleX(.92)} }
49
+ @keyframes ek-recede { 0% {transform:translateY(56px) scaleX(1);opacity:0} 10% {opacity:1} 85% {opacity:1} 100% {transform:translateY(0) scaleX(.4);opacity:0} }
34
50
  @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
51
 
36
52
  .ek-companion--rail { padding:12px 20px; text-decoration:none; }
@@ -18,19 +18,35 @@ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line
18
18
  .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
19
  .ek-mascot[data-variant="face"] { overflow:hidden; }
20
20
  .ek-mascot__face path:not([fill]) { fill:none; }
21
- .ek-mascot--loading .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
22
- .ek-mascot--loading .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
23
- .ek-mascot--loading .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
24
- .ek-mascot--loading .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
25
- @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
26
- @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
27
- @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
28
- @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
29
21
  .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
22
  .ek-companion--hero { margin-bottom:16px; }
31
23
  .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
24
  .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
25
  .ek-mascot-message p {margin:0}
26
+ /* Secondary loader: draws and releases an arrow over 2.8 s. */
27
+ .ek-mascot--draw .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
28
+ .ek-mascot--draw .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
29
+ .ek-mascot--draw .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
30
+ .ek-mascot--draw .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
31
+ @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
32
+ @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
33
+ @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
34
+ @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
35
+ /* Loader: walking toward the viewer, a 1.2 s stride (two steps). Ground rungs recede on an ease-out, as perspective slows them near the horizon. */
36
+ .ek-mascot--loading .ek-mascot__walker { animation:ek-sway 1.2s ease-in-out infinite; transform-origin:95px 185px; }
37
+ .ek-mascot--loading .ek-mascot__torso { animation:ek-bob .6s ease-in-out infinite; }
38
+ .ek-mascot--loading .ek-mascot__leg, .ek-mascot--loading .ek-mascot__hand { animation:ek-step 1.2s ease-in-out infinite; }
39
+ .ek-mascot--loading .ek-mascot__leg--r, .ek-mascot--loading .ek-mascot__hand--arrow { animation-delay:-.6s; }
40
+ .ek-mascot--loading .ek-mascot__shadow { animation:ek-shadow .6s ease-in-out infinite; transform-origin:95px 186px; }
41
+ .ek-mascot--loading .ek-mascot__rung { animation:ek-recede 1.2s cubic-bezier(.15,.6,.4,1) infinite; transform-origin:95px 162px; }
42
+ .ek-mascot--loading .ek-mascot__rung:nth-child(3) { animation-delay:-.3s; }
43
+ .ek-mascot--loading .ek-mascot__rung:nth-child(4) { animation-delay:-.6s; }
44
+ .ek-mascot--loading .ek-mascot__rung:nth-child(5) { animation-delay:-.9s; }
45
+ @keyframes ek-sway { 0%,100% {transform:rotate(-1.5deg)} 50% {transform:rotate(1.5deg)} }
46
+ @keyframes ek-bob { 0%,100% {transform:translateY(1px)} 50% {transform:translateY(-3px)} }
47
+ @keyframes ek-step { 0%,50%,100% {transform:translateY(0)} 25% {transform:translateY(-6px)} }
48
+ @keyframes ek-shadow { 0%,100% {transform:scaleX(1)} 50% {transform:scaleX(.92)} }
49
+ @keyframes ek-recede { 0% {transform:translateY(56px) scaleX(1);opacity:0} 10% {opacity:1} 85% {opacity:1} 100% {transform:translateY(0) scaleX(.4);opacity:0} }
34
50
  @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
51
 
36
52
  .ek-companion--rail { padding:12px 20px; text-decoration:none; }
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 200" width="200" height="200" class="ek-mascot ek-mascot--loading" data-expression="excited" data-variant="loading" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 4 214 214" width="200" height="200" class="ek-mascot ek-mascot--loading" data-expression="cool" data-variant="loading" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
2
  Archer Cloak artwork keeps its approved identity across both grounds.
3
3
  UI chrome continues to use the surface, text and accent roles below. */
4
4
  --mascot-coral: #ef9875;
@@ -18,52 +18,75 @@ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line
18
18
  .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
19
  .ek-mascot[data-variant="face"] { overflow:hidden; }
20
20
  .ek-mascot__face path:not([fill]) { fill:none; }
21
- .ek-mascot--loading .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
22
- .ek-mascot--loading .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
23
- .ek-mascot--loading .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
24
- .ek-mascot--loading .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
25
- @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
26
- @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
27
- @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
28
- @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
29
21
  .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
22
  .ek-companion--hero { margin-bottom:16px; }
31
23
  .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
24
  .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
25
  .ek-mascot-message p {margin:0}
26
+ /* Secondary loader: draws and releases an arrow over 2.8 s. */
27
+ .ek-mascot--draw .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
28
+ .ek-mascot--draw .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
29
+ .ek-mascot--draw .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
30
+ .ek-mascot--draw .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
31
+ @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
32
+ @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
33
+ @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
34
+ @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
35
+ /* Loader: walking toward the viewer, a 1.2 s stride (two steps). Ground rungs recede on an ease-out, as perspective slows them near the horizon. */
36
+ .ek-mascot--loading .ek-mascot__walker { animation:ek-sway 1.2s ease-in-out infinite; transform-origin:95px 185px; }
37
+ .ek-mascot--loading .ek-mascot__torso { animation:ek-bob .6s ease-in-out infinite; }
38
+ .ek-mascot--loading .ek-mascot__leg, .ek-mascot--loading .ek-mascot__hand { animation:ek-step 1.2s ease-in-out infinite; }
39
+ .ek-mascot--loading .ek-mascot__leg--r, .ek-mascot--loading .ek-mascot__hand--arrow { animation-delay:-.6s; }
40
+ .ek-mascot--loading .ek-mascot__shadow { animation:ek-shadow .6s ease-in-out infinite; transform-origin:95px 186px; }
41
+ .ek-mascot--loading .ek-mascot__rung { animation:ek-recede 1.2s cubic-bezier(.15,.6,.4,1) infinite; transform-origin:95px 162px; }
42
+ .ek-mascot--loading .ek-mascot__rung:nth-child(3) { animation-delay:-.3s; }
43
+ .ek-mascot--loading .ek-mascot__rung:nth-child(4) { animation-delay:-.6s; }
44
+ .ek-mascot--loading .ek-mascot__rung:nth-child(5) { animation-delay:-.9s; }
45
+ @keyframes ek-sway { 0%,100% {transform:rotate(-1.5deg)} 50% {transform:rotate(1.5deg)} }
46
+ @keyframes ek-bob { 0%,100% {transform:translateY(1px)} 50% {transform:translateY(-3px)} }
47
+ @keyframes ek-step { 0%,50%,100% {transform:translateY(0)} 25% {transform:translateY(-6px)} }
48
+ @keyframes ek-shadow { 0%,100% {transform:scaleX(1)} 50% {transform:scaleX(.92)} }
49
+ @keyframes ek-recede { 0% {transform:translateY(56px) scaleX(1);opacity:0} 10% {opacity:1} 85% {opacity:1} 100% {transform:translateY(0) scaleX(.4);opacity:0} }
34
50
  @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
51
 
36
52
  .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
53
  .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
54
  .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
- </style><title>Eklavya drawing a bow</title><g class="ek-mascot__character">
40
- <ellipse cx="94" cy="185" rx="49" ry="5" fill="var(--mascot-shadow)"/>
41
- <path d="M51 124Q29 137 34 168l23-8 17-27m59-9q26 8 21 39l-24-7-17-22" fill="var(--mascot-cloak-dark)"/>
42
- <path d="M65 157v23q0 8 16 6l4-28m21 0 1 23q2 8 17 4v-29" fill="var(--mascot-coral)"/>
43
- <path d="M58 122q35-16 72 0l8 46q-19 9-42 1-23 7-43-1Z" fill="var(--mascot-cloak)"/>
44
- <path d="M96 127v40m-31-22q29 9 64 0" fill="none" stroke="var(--mascot-cloak-dark)" stroke-width="3"/>
45
- <path d="M52 109q42-22 86 0l-9 29-32-14-32 14Z" fill="var(--mascot-cloak)" stroke="var(--mascot-cloak-dark)" stroke-width="3" stroke-linejoin="round"/>
46
- <path d="M57 114l37 10m42-10-39 10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/>
47
- <path d="M54 148h80v11H54Z" fill="var(--mascot-ink)"/>
48
- <rect x="87" y="146" width="17" height="15" rx="3" fill="var(--mascot-metal)"/><rect x="92" y="150" width="7" height="7" rx="1" fill="var(--mascot-ink)"/>
49
- <g class="ek-mascot__head">
55
+ </style><title>Eklavya walking toward you</title><g class="ek-mascot__ground" fill="none" stroke="var(--mascot-metal)" stroke-width="2" stroke-linecap="round" opacity=".5">
56
+ <path d="M-20 218 63 162m147 56-83-56"/>
57
+ <path class="ek-mascot__rung" d="M-10 162h210" transform="matrix(1 0 0 1 0 56)"/><path class="ek-mascot__rung" d="M-10 162h210" transform="matrix(0.75 0 0 1 23.75 34)"/><path class="ek-mascot__rung" d="M-10 162h210" transform="matrix(0.55 0 0 1 42.75 17)"/><path class="ek-mascot__rung" d="M-10 162h210" transform="matrix(0.42 0 0 1 55.1 6)"/>
58
+ </g>
59
+ <ellipse class="ek-mascot__shadow" cx="95" cy="186" rx="40" ry="5" fill="var(--mascot-shadow)"/>
60
+ <g class="ek-mascot__walker">
61
+ <path class="ek-mascot__leg ek-mascot__leg--l" d="M65 157v23q0 8 16 6l4-28Z" fill="var(--mascot-coral)"/>
62
+ <path class="ek-mascot__leg ek-mascot__leg--r" d="M106 158l1 23q2 8 17 4v-29Z" fill="var(--mascot-coral)"/>
63
+ <g class="ek-mascot__torso">
64
+ <path d="M51 124Q29 137 34 168l23-8 17-27m59-9q26 8 21 39l-24-7-17-22" fill="var(--mascot-cloak-dark)"/>
65
+ <path d="M58 122q35-16 72 0l8 46q-19 9-42 1-23 7-43-1Z" fill="var(--mascot-cloak)"/>
66
+ <path d="M96 127v40m-31-22q29 9 64 0" fill="none" stroke="var(--mascot-cloak-dark)" stroke-width="3"/>
67
+ <g class="ek-mascot__hand ek-mascot__hand--arrow" stroke-linecap="round" stroke-linejoin="round">
68
+ <path d="M60 118 47 141" stroke="var(--mascot-cloak)" stroke-width="13"/>
69
+ <path d="M46 104v72" stroke="var(--mascot-wood-dark)" stroke-width="3"/>
70
+ <path d="M41 174l5 11 5-11Z" fill="var(--mascot-thread)" stroke="var(--mascot-wood)" stroke-width="1.5"/>
71
+ <path d="M46 106l-6-9 1 11 5 5 5-5 1-11Z" fill="var(--mascot-thread)"/>
72
+ <rect x="39" y="136" width="15" height="15" rx="7" fill="var(--mascot-coral)"/>
73
+ </g>
74
+ <g class="ek-mascot__hand ek-mascot__hand--bow" fill="none" stroke-linecap="round">
75
+ <path d="M130 118 151 141" stroke="var(--mascot-cloak)" stroke-width="13"/>
76
+ <path d="M152 90v104" stroke="var(--mascot-string)" stroke-width="1.5"/>
77
+ <path d="M152 90q16 52 0 104" stroke="var(--mascot-wood-dark)" stroke-width="7"/>
78
+ <path d="M153 90q13 52 0 104" stroke="var(--mascot-wood)" stroke-width="3.5"/>
79
+ <rect x="151" y="135" width="15" height="15" rx="7" fill="var(--mascot-coral)"/>
80
+ </g>
81
+ <path d="M52 109q42-22 86 0l-9 29-32-14-32 14Z" fill="var(--mascot-cloak)" stroke="var(--mascot-cloak-dark)" stroke-width="3" stroke-linejoin="round"/>
82
+ <path d="M57 114l37 10m42-10-39 10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/>
83
+ <path d="M54 148h80v11H54Z" fill="var(--mascot-ink)"/>
84
+ <rect x="87" y="146" width="17" height="15" rx="3" fill="var(--mascot-metal)"/><rect x="92" y="150" width="7" height="7" rx="1" fill="var(--mascot-ink)"/>
85
+ <g class="ek-mascot__head">
50
86
  <path d="M46 43h18V29h19V18h23v12h20v14h17v19h8v29h-8v16q0 12-16 15H62q-18-3-18-17V91h-9V63h11Z" fill="var(--mascot-coral)" stroke="var(--mascot-coral-dark)" stroke-width="2" stroke-linejoin="round"/>
51
87
  <path d="M48 45h18V32h18V21h19" fill="none" stroke="var(--mascot-highlight)" stroke-width="3" stroke-linejoin="round"/>
52
- <g class="ek-mascot__face" fill="var(--mascot-ink)" stroke="var(--mascot-ink)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><rect x="64" y="64" width="10" height="19" rx="5"/><rect x="110" y="64" width="10" height="19" rx="5"/><path d="M81 87q11 22 22 0Z" fill="var(--mascot-ink)"/><path d="M87 97q5-3 10 0" stroke="var(--mascot-coral)"/></g>
53
- </g>
54
- <circle cx="96" cy="125" r="7" fill="var(--mascot-metal)"/><circle cx="94" cy="123" r="2" fill="var(--mascot-thread)"/>
55
- <g class="ek-mascot__bow" fill="none" stroke-linecap="round" stroke-linejoin="round">
56
- <path d="M157 78q36 47 0 94" stroke="var(--mascot-wood-dark)" stroke-width="8"/>
57
- <path d="M158 78q32 47 0 94" stroke="var(--mascot-wood)" stroke-width="4"/>
58
- <path class="ek-mascot__string" d="M157 78l-88 50 88 44" stroke="var(--mascot-string)" stroke-width="1.8"/>
88
+ <g class="ek-mascot__face" fill="var(--mascot-ink)" stroke="var(--mascot-ink)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M60 65h19v7q0 9-9 9h-1q-9 0-9-9Zm46 0h19v7q0 9-9 9h-1q-9 0-9-9Z" fill="var(--mascot-ink)"/><path d="M56 66h72M86 92q9 3 15-4"/></g>
59
89
  </g>
60
- <path d="M130 127l16-8q10-1 13 5 2 8-6 11l-15 4" fill="var(--mascot-cloak)"/>
61
- <rect x="147" y="117" width="15" height="18" rx="7" fill="var(--mascot-coral)"/>
62
- <g class="ek-mascot__arrow" stroke-linecap="round" stroke-linejoin="round">
63
- <path d="M69 128h114" stroke="var(--mascot-wood-dark)" stroke-width="3"/>
64
- <path d="M179 120l13 8-13 8Z" fill="var(--mascot-thread)" stroke="var(--mascot-wood)" stroke-width="1.5"/>
65
- <path d="M71 128l-9-9 10 1 8 8-8 8-10 1Z" fill="var(--mascot-thread)"/>
90
+ <circle cx="96" cy="125" r="7" fill="var(--mascot-metal)"/><circle cx="94" cy="123" r="2" fill="var(--mascot-thread)"/>
66
91
  </g>
67
- <path d="M58 128q-11-6-14 5-2 9 8 13l17-8" fill="var(--mascot-cloak)"/>
68
- <rect x="62" y="121" width="18" height="16" rx="7" fill="var(--mascot-coral)"/>
69
- </g><circle class="ek-mascot__ripple" cx="196" cy="128" r="10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/></svg>
92
+ </g></svg>
@@ -3,19 +3,35 @@ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line
3
3
  .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
4
4
  .ek-mascot[data-variant="face"] { overflow:hidden; }
5
5
  .ek-mascot__face path:not([fill]) { fill:none; }
6
- .ek-mascot--loading .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
7
- .ek-mascot--loading .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
8
- .ek-mascot--loading .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
9
- .ek-mascot--loading .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
10
- @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
11
- @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
12
- @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
13
- @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
14
6
  .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
15
7
  .ek-companion--hero { margin-bottom:16px; }
16
8
  .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
17
9
  .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
18
10
  .ek-mascot-message p {margin:0}
11
+ /* Secondary loader: draws and releases an arrow over 2.8 s. */
12
+ .ek-mascot--draw .ek-mascot__character { animation:ek-breathe 2.8s ease-in-out infinite; transform-origin:95px 185px; }
13
+ .ek-mascot--draw .ek-mascot__arrow { animation:ek-release 2.8s cubic-bezier(.3,0,.3,1) infinite; }
14
+ .ek-mascot--draw .ek-mascot__string { animation:ek-draw 2.8s ease-in-out infinite; transform-origin:157px 125px; }
15
+ .ek-mascot--draw .ek-mascot__ripple { animation:ek-hit 2.8s ease-out infinite; transform-origin:196px 128px; opacity:0; }
16
+ @keyframes ek-breathe { 0%,100% {transform:translateY(0)} 35%,55% {transform:translateY(-3px) rotate(-1deg)} }
17
+ @keyframes ek-release { 0%,12% {transform:translateX(0);opacity:1} 40%,52% {transform:translateX(-10px);opacity:1} 68% {transform:translateX(22px);opacity:0} 69%,85% {transform:translateX(0);opacity:0} 100% {transform:translateX(0);opacity:1} }
18
+ @keyframes ek-draw { 0%,100% {transform:scaleX(1)} 40%,52% {transform:scaleX(1.11)} 60% {transform:scaleX(.08)} 68% {transform:scaleX(1)} }
19
+ @keyframes ek-hit { 0%,60%,100% {opacity:0;transform:scale(.4)} 68% {opacity:.8;transform:scale(.7)} 88% {opacity:0;transform:scale(1.7)} }
20
+ /* Loader: walking toward the viewer, a 1.2 s stride (two steps). Ground rungs recede on an ease-out, as perspective slows them near the horizon. */
21
+ .ek-mascot--loading .ek-mascot__walker { animation:ek-sway 1.2s ease-in-out infinite; transform-origin:95px 185px; }
22
+ .ek-mascot--loading .ek-mascot__torso { animation:ek-bob .6s ease-in-out infinite; }
23
+ .ek-mascot--loading .ek-mascot__leg, .ek-mascot--loading .ek-mascot__hand { animation:ek-step 1.2s ease-in-out infinite; }
24
+ .ek-mascot--loading .ek-mascot__leg--r, .ek-mascot--loading .ek-mascot__hand--arrow { animation-delay:-.6s; }
25
+ .ek-mascot--loading .ek-mascot__shadow { animation:ek-shadow .6s ease-in-out infinite; transform-origin:95px 186px; }
26
+ .ek-mascot--loading .ek-mascot__rung { animation:ek-recede 1.2s cubic-bezier(.15,.6,.4,1) infinite; transform-origin:95px 162px; }
27
+ .ek-mascot--loading .ek-mascot__rung:nth-child(3) { animation-delay:-.3s; }
28
+ .ek-mascot--loading .ek-mascot__rung:nth-child(4) { animation-delay:-.6s; }
29
+ .ek-mascot--loading .ek-mascot__rung:nth-child(5) { animation-delay:-.9s; }
30
+ @keyframes ek-sway { 0%,100% {transform:rotate(-1.5deg)} 50% {transform:rotate(1.5deg)} }
31
+ @keyframes ek-bob { 0%,100% {transform:translateY(1px)} 50% {transform:translateY(-3px)} }
32
+ @keyframes ek-step { 0%,50%,100% {transform:translateY(0)} 25% {transform:translateY(-6px)} }
33
+ @keyframes ek-shadow { 0%,100% {transform:scaleX(1)} 50% {transform:scaleX(.92)} }
34
+ @keyframes ek-recede { 0% {transform:translateY(56px) scaleX(1);opacity:0} 10% {opacity:1} 85% {opacity:1} 100% {transform:translateY(0) scaleX(.4);opacity:0} }
19
35
  @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
20
36
 
21
37
  .ek-companion--rail { padding:12px 20px; text-decoration:none; }
@@ -4,10 +4,13 @@
4
4
  const expressions = Object.freeze({
5
5
  neutral: 'Calm and attentive', happy: 'Happy', excited: 'Excited', celebrate: 'Celebrating',
6
6
  curious: 'Curious', thinking: 'Thinking', focused: 'Focused', wink: 'Offering a tip',
7
- surprised: 'Surprised', confused: 'Seeking clarity', worried: 'Concerned', sad: 'Sad', sleepy: 'Resting'
7
+ surprised: 'Surprised', confused: 'Seeking clarity', worried: 'Concerned', sad: 'Sad', sleepy: 'Resting',
8
+ proud: 'Proud', determined: 'Determined', idea: 'Having an idea', laugh: 'Laughing', love: 'Loving it',
9
+ shy: 'Shy', relieved: 'Relieved', starstruck: 'Starstruck', cool: 'Cool and confident', hopeful: 'Hopeful',
10
+ nervous: 'Nervous'
8
11
  });
9
12
  const states = Object.freeze({ idle: 'sleepy', info: 'neutral', success: 'happy', complete: 'celebrate',
10
- milestone: 'celebrate', empty: 'curious', notFound: 'curious', loading: 'excited',
13
+ milestone: 'celebrate', empty: 'curious', notFound: 'curious', loading: 'cool',
11
14
  review: 'focused', tip: 'wink', new: 'surprised', validation: 'confused', warning: 'worried', error: 'sad' });
12
15
  const own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
13
16
  const escape = (text) => String(text).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
@@ -17,9 +20,14 @@
17
20
  const eyes = '<rect x="64" y="64" width="10" height="19" rx="5"/><rect x="110" y="64" width="10" height="19" rx="5"/>';
18
21
  const smile = '<path d="M83 88q9 10 18 0"/>';
19
22
  const closed = '<path d="M64 74q5-7 10 0m36 0q5-7 10 0"/>';
23
+ const shine = '<circle cx="71" cy="69" r="2" fill="var(--mascot-highlight)" stroke="none"/><circle cx="117" cy="69" r="2" fill="var(--mascot-highlight)" stroke="none"/>';
24
+ const blush = '<g fill="var(--mascot-coral-dark)" stroke="none" opacity=".55"><ellipse cx="60" cy="91" rx="7" ry="4"/><ellipse cx="124" cy="91" rx="7" ry="4"/></g>';
25
+ const sweat = '<path d="M134 50q-5 7-5 10a5 5 0 0 0 10 0q0-3-5-10Z" fill="var(--mascot-thread)" stroke-width="1.5"/>';
26
+ const open = '<path d="M80 86q12 24 24 0Z" fill="var(--mascot-ink)"/><path d="M86 97q6-4 12 0" stroke="var(--mascot-coral)"/>';
20
27
  const brows = { focused: 'M62 57l14 4m32 0 14-4', thinking: 'M62 58h14m33-2 13 3',
21
28
  worried:'M62 59l14-6m32 0 14 6', sad:'M62 60l14-5m32 0 14 5',
22
- confused:'M62 58l14-5m32 4h14', curious:'M62 59h14m32-6 14 5' };
29
+ confused:'M62 58l14-5m32 4h14', curious:'M62 59h14m32-6 14 5', determined:'M61 56l15 7m32 0 15-7',
30
+ proud:'M62 56q7-4 14 0m32 0q7-4 14 0', hopeful:'M62 54l14-3m32 0 14 3', nervous:'M62 58l14-4m32 0 14 4' };
23
31
  const faces = {
24
32
  neutral: eyes + '<path d="M84 89q8 6 16 0"/>', happy: closed + smile,
25
33
  celebrate: closed + '<path d="M82 87q10 22 20 0Z" fill="var(--mascot-ink)"/><path d="M87 98h10" stroke="var(--mascot-coral)"/>',
@@ -28,13 +36,23 @@
28
36
  focused: eyes + '<path d="M85 91h14"/>', wink: '<rect x="64" y="64" width="10" height="19" rx="5"/><path d="M110 74q5-7 10 0"/>' + smile,
29
37
  surprised: eyes + '<ellipse cx="92" cy="92" rx="5" ry="7" fill="var(--mascot-ink)"/>',
30
38
  confused: eyes + '<path d="M83 93q5-7 10-2t9-2"/>', worried: eyes + '<path d="M83 95q9-9 18 0"/>',
31
- sad: eyes + '<path d="M83 96q9-9 18 0"/>', sleepy: '<path d="M64 75q5 5 10 0m36 0q5 5 10 0m-30 16h10"/>'
39
+ sad: eyes + '<path d="M83 96q9-9 18 0"/>', sleepy: '<path d="M64 75q5 5 10 0m36 0q5 5 10 0m-30 16h10"/>',
40
+ proud: closed + '<path d="M80 87q12 11 24 0"/>', determined: eyes + '<path d="M84 92q8-3 16 0"/>',
41
+ idea: eyes + shine + '<path d="M81 87q11 18 22 0Z" fill="var(--mascot-ink)"/>',
42
+ laugh: '<path d="M64 67l10 7-10 7m56-14-10 7 10 7"/>' + open + blush,
43
+ love: '<path d="M69 82l-7-7a4 4 0 0 1 7-5 4 4 0 0 1 7 5Z m46 0l-7-7a4 4 0 0 1 7-5 4 4 0 0 1 7 5Z" fill="var(--mascot-ink)"/><path d="M84 89q4 4 8 0q4 4 8 0"/>' + blush,
44
+ shy: '<rect x="62" y="68" width="10" height="17" rx="5"/><rect x="108" y="68" width="10" height="17" rx="5"/><path d="M86 93q3-2 6 0t6 0"/>' + blush,
45
+ relieved: closed + smile + sweat,
46
+ starstruck: '<path d="M69 63l3 8 8 3-8 3-3 8-3-8-8-3 8-3Z m46 0l3 8 8 3-8 3-3 8-3-8-8-3 8-3Z" fill="var(--mascot-ink)"/>' + open,
47
+ cool: '<path d="M60 65h19v7q0 9-9 9h-1q-9 0-9-9Zm46 0h19v7q0 9-9 9h-1q-9 0-9-9Z" fill="var(--mascot-ink)"/><path d="M56 66h72M86 92q9 3 15-4"/>',
48
+ hopeful: '<rect x="64" y="60" width="10" height="19" rx="5"/><rect x="110" y="60" width="10" height="19" rx="5"/>' + shine.replaceAll('cy="69"', 'cy="65"') + '<path d="M86 89q6 5 12 0"/>',
49
+ nervous: '<rect x="68" y="64" width="10" height="19" rx="5"/><rect x="114" y="64" width="10" height="19" rx="5"/><path d="M82 93q3-3 6 0t6 0t6 0t6 0"/>' + sweat
32
50
  };
33
51
  return `<g class="ek-mascot__face" fill="var(--mascot-ink)" stroke="var(--mascot-ink)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round">${faces[expression]}${brows[expression] ? `<path d="${brows[expression]}" fill="none"/>` : ''}</g>`;
34
52
  }
35
53
  function svg({ expression = 'neutral', state, variant = 'body', size = 64, label = null } = {}) {
36
54
  expression = state ? expressionForState(state) : own(expressions, expression) ? expression : 'neutral';
37
- variant = ['body','face','loading'].includes(variant) ? variant : 'body';
55
+ variant = ['body','face','loading','draw'].includes(variant) ? variant : 'body';
38
56
  const loading = variant === 'loading';
39
57
  const px = dimension(size);
40
58
  const head = `<g class="ek-mascot__head">
@@ -69,8 +87,47 @@
69
87
  <path d="M58 128q-11-6-14 5-2 9 8 13l17-8" fill="var(--mascot-cloak)"/>
70
88
  <rect x="62" y="121" width="18" height="16" rx="7" fill="var(--mascot-coral)"/>
71
89
  </g>`;
72
- const accents = expression === 'celebrate' ? '<g fill="var(--mascot-thread)"><path d="m20 47 3-7 3 7 7 3-7 3-3 7-3-7-7-3Z"/><path d="m166 31 2-5 2 5 5 2-5 2-2 5-2-5-5-2Z"/></g>' : '';
73
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${variant === 'face' ? '30 12 126 128' : '0 0 210 200'}" width="${px}" height="${px}" class="ek-mascot${loading ? ' ek-mascot--loading' : ''}" data-expression="${expression}" data-variant="${variant}" ${label == null ? 'aria-hidden="true"' : 'role="img"'}>${label == null ? '' : `<title>${escape(label)}</title>`}${accents}${variant === 'face' ? `<path d="M48 109q44-19 88 0l-8 30-33-15-31 15Z" fill="var(--mascot-cloak)"/>${head}<circle cx="95" cy="126" r="6" fill="var(--mascot-metal)"/>` : body}${loading ? '<circle class="ek-mascot__ripple" cx="196" cy="128" r="10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/>' : ''}</svg>`;
90
+ // The loader faces the viewer mid-stride: arrow held point-down in one hand, bow in the other, ground sliding away.
91
+ const walk = `<g class="ek-mascot__ground" fill="none" stroke="var(--mascot-metal)" stroke-width="2" stroke-linecap="round" opacity=".5">
92
+ <path d="M-20 218 63 162m147 56-83-56"/>
93
+ ${[[56,1],[34,.75],[17,.55],[6,.42]].map(([dy, sx]) => `<path class="ek-mascot__rung" d="M-10 162h210" transform="matrix(${sx} 0 0 1 ${+(95 * (1 - sx)).toFixed(2)} ${dy})"/>`).join('')}
94
+ </g>
95
+ <ellipse class="ek-mascot__shadow" cx="95" cy="186" rx="40" ry="5" fill="var(--mascot-shadow)"/>
96
+ <g class="ek-mascot__walker">
97
+ <path class="ek-mascot__leg ek-mascot__leg--l" d="M65 157v23q0 8 16 6l4-28Z" fill="var(--mascot-coral)"/>
98
+ <path class="ek-mascot__leg ek-mascot__leg--r" d="M106 158l1 23q2 8 17 4v-29Z" fill="var(--mascot-coral)"/>
99
+ <g class="ek-mascot__torso">
100
+ <path d="M51 124Q29 137 34 168l23-8 17-27m59-9q26 8 21 39l-24-7-17-22" fill="var(--mascot-cloak-dark)"/>
101
+ <path d="M58 122q35-16 72 0l8 46q-19 9-42 1-23 7-43-1Z" fill="var(--mascot-cloak)"/>
102
+ <path d="M96 127v40m-31-22q29 9 64 0" fill="none" stroke="var(--mascot-cloak-dark)" stroke-width="3"/>
103
+ <g class="ek-mascot__hand ek-mascot__hand--arrow" stroke-linecap="round" stroke-linejoin="round">
104
+ <path d="M60 118 47 141" stroke="var(--mascot-cloak)" stroke-width="13"/>
105
+ <path d="M46 104v72" stroke="var(--mascot-wood-dark)" stroke-width="3"/>
106
+ <path d="M41 174l5 11 5-11Z" fill="var(--mascot-thread)" stroke="var(--mascot-wood)" stroke-width="1.5"/>
107
+ <path d="M46 106l-6-9 1 11 5 5 5-5 1-11Z" fill="var(--mascot-thread)"/>
108
+ <rect x="39" y="136" width="15" height="15" rx="7" fill="var(--mascot-coral)"/>
109
+ </g>
110
+ <g class="ek-mascot__hand ek-mascot__hand--bow" fill="none" stroke-linecap="round">
111
+ <path d="M130 118 151 141" stroke="var(--mascot-cloak)" stroke-width="13"/>
112
+ <path d="M152 90v104" stroke="var(--mascot-string)" stroke-width="1.5"/>
113
+ <path d="M152 90q16 52 0 104" stroke="var(--mascot-wood-dark)" stroke-width="7"/>
114
+ <path d="M153 90q13 52 0 104" stroke="var(--mascot-wood)" stroke-width="3.5"/>
115
+ <rect x="151" y="135" width="15" height="15" rx="7" fill="var(--mascot-coral)"/>
116
+ </g>
117
+ <path d="M52 109q42-22 86 0l-9 29-32-14-32 14Z" fill="var(--mascot-cloak)" stroke="var(--mascot-cloak-dark)" stroke-width="3" stroke-linejoin="round"/>
118
+ <path d="M57 114l37 10m42-10-39 10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/>
119
+ <path d="M54 148h80v11H54Z" fill="var(--mascot-ink)"/>
120
+ <rect x="87" y="146" width="17" height="15" rx="3" fill="var(--mascot-metal)"/><rect x="92" y="150" width="7" height="7" rx="1" fill="var(--mascot-ink)"/>
121
+ ${head}
122
+ <circle cx="96" cy="125" r="7" fill="var(--mascot-metal)"/><circle cx="94" cy="123" r="2" fill="var(--mascot-thread)"/>
123
+ </g>
124
+ </g>`;
125
+ const accents = {
126
+ celebrate: '<g fill="var(--mascot-thread)"><path d="m20 47 3-7 3 7 7 3-7 3-3 7-3-7-7-3Z"/><path d="m166 31 2-5 2 5 5 2-5 2-2 5-2-5-5-2Z"/></g>',
127
+ proud: '<path d="m144 26 2-5 2 5 5 2-5 2-2 5-2-5-5-2Z" fill="var(--mascot-thread)"/>',
128
+ idea: '<g stroke-linecap="round"><path d="M142 18v-3m11 7 2-2m-24 2-2-2" stroke="var(--mascot-thread)" stroke-width="2"/><circle cx="142" cy="31" r="8" fill="var(--mascot-thread)" stroke="var(--mascot-ink)" stroke-width="2"/><path d="M139 40h6v4h-6Z" fill="var(--mascot-metal)"/></g>'
129
+ }[expression] || '';
130
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${variant === 'face' ? '30 12 126 128' : loading ? '-2 4 214 214' : '0 0 210 200'}" width="${px}" height="${px}" class="ek-mascot${loading ? ' ek-mascot--loading' : ''}${variant === 'draw' ? ' ek-mascot--draw' : ''}" data-expression="${expression}" data-variant="${variant}" ${label == null ? 'aria-hidden="true"' : 'role="img"'}>${label == null ? '' : `<title>${escape(label)}</title>`}${accents}${variant === 'face' ? `<path d="M48 109q44-19 88 0l-8 30-33-15-31 15Z" fill="var(--mascot-cloak)"/>${head}<circle cx="95" cy="126" r="6" fill="var(--mascot-metal)"/>` : loading ? walk : body}${variant === 'draw' ? '<circle class="ek-mascot__ripple" cx="196" cy="128" r="10" fill="none" stroke="var(--mascot-thread)" stroke-width="2"/>' : ''}</svg>`;
74
131
  }
75
132
  root.EklavyaMascot = Object.freeze({ expressions, states, expressionForState, svg });
76
133
  if (root.customElements && !root.customElements.get('eklavya-mascot')) {
@@ -3,7 +3,7 @@
3
3
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
4
  <title>Eklavya — Mascot studio</title><meta name="robots" content="noindex, follow"><meta name="description" content="The Eklavya archer companion: expressions, motion, and reusable design-system components.">
5
5
  <script>try{document.documentElement.dataset.mode=localStorage.getItem('eklavya-ground')==='paper'?'paper':'ink'}catch(e){}</script>
6
- <link rel="stylesheet" href="/tokens.css?v=3"><link rel="stylesheet" href="/brand/mascot/mascot.css?v=1"><script src="/brand/mascot/mascot.js?v=1" defer></script>
6
+ <link rel="stylesheet" href="/tokens.css?v=3"><link rel="stylesheet" href="/brand/mascot/mascot.css?v=4"><script src="/brand/mascot/mascot.js?v=4" defer></script>
7
7
  <style>
8
8
  *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 var(--font-body)}
9
9
  a{color:var(--spot)}button{font:inherit;color:var(--ink);cursor:pointer;background:var(--panel);border:1px solid var(--line-2);border-radius:0;padding:9px 15px}button:hover,button[aria-pressed=true]{border-color:var(--spot);background:var(--spot-soft)}:focus-visible{outline:2px solid var(--spot);outline-offset:4px}
@@ -11,30 +11,34 @@ a{color:var(--spot)}button{font:inherit;color:var(--ink);cursor:pointer;backgrou
11
11
  @media(max-width:760px){.hero{grid-template-columns:1fr;margin:40px 0;gap:0}.stage{min-height:280px}.grid{grid-template-columns:repeat(3,minmax(0,1fr))}.panels{grid-template-columns:1fr}.section-head{display:block}}
12
12
  </style></head><body><main class="studio">
13
13
  <nav class="top" aria-label="Studio navigation"><a class="wordmark" href="/">Eklavya <span class="eyebrow">/ mascot studio</span></a><button id="ground" aria-label="Switch to paper theme">Paper ground</button></nav>
14
- <section class="hero"><div class="intro"><span class="eyebrow">Meet your learning companion</span><h1>A steady aim.<br>A curious mind.</h1><p>The Archer Cloak companion. A warm coral character, a verdigris cloak, and a little encouragement as you learn.</p><p class="notice" id="selected" aria-live="polite">Calm and attentive</p><div class="controls"><button id="pose" aria-pressed="false">Show face only</button><button id="motion" aria-pressed="false">Play loading loop</button></div></div><div class="stage"><eklavya-mascot id="preview" expression="neutral" size="280" label="Eklavya, calm and attentive"></eklavya-mascot></div></section>
14
+ <section class="hero"><div class="intro"><span class="eyebrow">Meet your learning companion</span><h1>A steady aim.<br>A curious mind.</h1><p>The Archer Cloak companion. A warm coral character, a verdigris cloak, and a little encouragement as you learn.</p><p class="notice" id="selected" aria-live="polite">Calm and attentive</p><div class="controls"><button id="pose" aria-pressed="false">Show face only</button><button id="motion" aria-pressed="false">Play loading loop</button><button id="draw" aria-pressed="false">Play bow-draw loop</button></div></div><div class="stage"><eklavya-mascot id="preview" expression="neutral" size="280" label="Eklavya, calm and attentive"></eklavya-mascot></div></section>
15
15
  <section class="section"><div class="section-head"><h2>A face for the moment</h2><p>Choose an expression to preview it.</p></div><div class="grid" id="expressions" role="group" aria-label="Mascot expressions"></div></section>
16
- <section class="section"><h2>Practice in motion</h2><p>Draw, release, reset. A quiet, repeating rhythm for real loading states.</p><div class="panels"><div class="panel"><div class="motion" role="status"><eklavya-mascot state="loading" variant="loading" size="112"></eklavya-mascot><div><strong>Loading your history…</strong><p class="caption">Page loading · 112 px</p></div></div></div><div class="panel"><div class="motion" role="status"><eklavya-mascot state="loading" variant="loading" size="56"></eklavya-mascot><div><strong>Finding the next step…</strong><p class="caption">Inline loading · 56 px</p></div></div></div></div><button id="pause" aria-pressed="false">Pause animations</button><p class="caption">Reduced-motion preferences are respected automatically. Status text remains readable when motion is off.</p></section>
16
+ <section class="section"><h2>Practice in motion</h2><p>Bow in one hand, arrow in the other, walking toward you: the default loader. The bow-draw loop (<code>variant="draw"</code>) is a secondary loader for places that suit it.</p><div class="panels"><div class="panel"><div class="motion" role="status"><eklavya-mascot state="loading" variant="loading" size="112"></eklavya-mascot><div><strong data-verb>Pondering…</strong><p class="caption">Page loading · 112 px</p></div></div></div><div class="panel"><div class="motion" role="status"><eklavya-mascot state="loading" variant="loading" size="56"></eklavya-mascot><div><strong data-verb>Moseying…</strong><p class="caption">Inline loading · 56 px</p></div></div></div></div><button id="pause" aria-pressed="false">Pause animations</button><p class="caption">Reduced-motion preferences are respected automatically. Status text remains readable when motion is off.</p></section>
17
17
  <section class="section"><h2>Small enough to belong</h2><p>Use the face at compact sizes and the full archer when there is room to breathe.</p><div class="sizes" id="sizes"></div></section>
18
18
  <section class="section"><h2>React to the state</h2><p>Choose a UI state. One shared mapping keeps the companion consistent everywhere.</p><div class="state-list" id="states"></div><div class="ek-mascot-message" style="margin-top:16px"><eklavya-mascot id="state-preview" state="empty" size="80"></eklavya-mascot><div><strong id="state-name">empty → curious</strong><p id="state-copy">Nothing here yet. Your next session is a good place to start.</p></div></div></section>
19
19
  <section class="section"><h2>Made to be reused</h2><p>Same artwork on the website and the local dashboard. Use a state for product moments, or an expression for editorial illustrations.</p><pre>&lt;link rel="stylesheet" href="/tokens.css?v=3"&gt;
20
- &lt;link rel="stylesheet" href="/brand/mascot/mascot.css?v=1"&gt;
21
- &lt;script src="/brand/mascot/mascot.js?v=1" defer&gt;&lt;/script&gt;
20
+ &lt;link rel="stylesheet" href="/brand/mascot/mascot.css?v=4"&gt;
21
+ &lt;script src="/brand/mascot/mascot.js?v=4" defer&gt;&lt;/script&gt;
22
22
 
23
23
  &lt;eklavya-mascot state="empty" size="80"&gt;&lt;/eklavya-mascot&gt;
24
24
  &lt;eklavya-mascot expression="wink" variant="face" size="40"&gt;&lt;/eklavya-mascot&gt;
25
25
 
26
26
  &lt;div role="status"&gt;
27
27
  &lt;eklavya-mascot state="loading" variant="loading" size="56"&gt;&lt;/eklavya-mascot&gt;
28
- Loading your history…
28
+ &lt;!-- a random verb for each wait, such as Pondering… --&gt;
29
+ &lt;span&gt;Pondering…&lt;/span&gt;
29
30
  &lt;/div&gt;</pre><p class="caption">Decorative by default. Add a label when the artwork communicates something on its own. Keep errors and guidance in visible text.</p></section>
30
31
  </main><script>
31
32
  window.addEventListener('DOMContentLoaded',()=>{
32
- const M=EklavyaMascot, $=id=>document.getElementById(id), preview=$('preview');let expression='neutral',face=false,loading=false;
33
- function update(){preview.removeAttribute('state');preview.setAttribute('expression',expression);preview.setAttribute('variant',loading?'loading':face?'face':'body');preview.setAttribute('label','Eklavya, '+M.expressions[expression].toLowerCase());$('selected').textContent=M.expressions[expression];$('pose').setAttribute('aria-pressed',String(face));$('motion').setAttribute('aria-pressed',String(loading));document.querySelectorAll('.expression').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.expression===expression)))}
34
- for(const [key,label] of Object.entries(M.expressions)){const b=document.createElement('button');b.className='expression';b.dataset.expression=key;b.setAttribute('aria-pressed',String(key==='neutral'));b.innerHTML=M.svg({expression:key,variant:'face',size:72})+'<span>'+label+'</span><small>'+key+'</small>';b.addEventListener('click',()=>{expression=key;loading=false;update()});$('expressions').append(b)}
35
- $('pose').onclick=()=>{face=!face;loading=false;update()};$('motion').onclick=()=>{loading=!loading;if(loading)expression='excited';update()};
33
+ const M=EklavyaMascot, $=id=>document.getElementById(id), preview=$('preview');let expression='neutral',face=false,loading=false,drawing=false;
34
+ function update(){preview.removeAttribute('state');preview.setAttribute('expression',expression);preview.setAttribute('variant',loading?'loading':drawing?'draw':face?'face':'body');preview.setAttribute('label','Eklavya, '+M.expressions[expression].toLowerCase());$('selected').textContent=M.expressions[expression];$('pose').setAttribute('aria-pressed',String(face));$('motion').setAttribute('aria-pressed',String(loading));$('draw').setAttribute('aria-pressed',String(drawing));document.querySelectorAll('.expression').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.expression===expression)))}
35
+ for(const [key,label] of Object.entries(M.expressions)){const b=document.createElement('button');b.className='expression';b.dataset.expression=key;b.setAttribute('aria-pressed',String(key==='neutral'));b.innerHTML=M.svg({expression:key,variant:'face',size:72})+'<span>'+label+'</span><small>'+key+'</small>';b.addEventListener('click',()=>{expression=key;update()});$('expressions').append(b)}
36
+ $('pose').onclick=()=>{face=!face;loading=drawing=false;update()};$('motion').onclick=()=>{loading=!loading;drawing=false;if(loading)expression='cool';update()};$('draw').onclick=()=>{drawing=!drawing;loading=false;if(drawing)expression='excited';update()};
36
37
  $('pause').onclick=()=>{const paused=document.body.classList.toggle('paused');$('pause').setAttribute('aria-pressed',String(paused));$('pause').textContent=paused?'Resume animations':'Pause animations'};
37
- const copy={empty:'Nothing here yet. Your next session is a good place to start.',loading:'Loading your history…',success:'Saved. Keep going.',complete:'Practice complete. Nice work.',error:'This could not load. Try again.',warning:'Review this step before continuing.',validation:'Check the highlighted field.',review:'Take a moment to review.',tip:'A useful detail to consider.',idle:'Ready when you are.'};
38
+ // A sample of the dashboard's LOADER_VERBS: every wait shows a random one, never a fixed word.
39
+ const verbs=['Pondering','Moseying','Brewing','Noodling','Percolating','Wandering'],verb=()=>verbs[Math.floor(Math.random()*verbs.length)]+'…';
40
+ document.querySelectorAll('[data-verb]').forEach(e=>e.textContent=verb());
41
+ const copy={empty:'Nothing here yet. Your next session is a good place to start.',get loading(){return verb()},success:'Saved. Keep going.',complete:'Practice complete. Nice work.',error:'This could not load. Try again.',warning:'Review this step before continuing.',validation:'Check the highlighted field.',review:'Take a moment to review.',tip:'A useful detail to consider.',idle:'Ready when you are.'};
38
42
  for(const [state,mood] of Object.entries(M.states)){const b=document.createElement('button');b.textContent=state;b.setAttribute('aria-pressed',String(state==='empty'));b.onclick=()=>{$('state-preview').setAttribute('state',state);$('state-preview').setAttribute('variant',state==='loading'?'loading':'body');$('state-name').textContent=state+' → '+mood;$('state-copy').textContent=copy[state]||M.expressions[mood];$('states').querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',String(x===b)))};$('states').append(b)}
39
43
  for(const size of [24,32,40,64,96,128]){const node=document.createElement('div');node.className='size';node.innerHTML=M.svg({expression:'neutral',variant:size<64?'face':'body',size})+'<span>'+size+' px</span>';$('sizes').append(node)}
40
44
  function ground(){const paper=document.documentElement.dataset.mode==='paper';$('ground').textContent=paper?'Ink ground':'Paper ground';$('ground').setAttribute('aria-label',paper?'Switch to ink theme':'Switch to paper theme')}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eklavya",
3
3
  "displayName": "Eklavya",
4
- "version": "1.85.2",
4
+ "version": "1.87.0",
5
5
  "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
6
  "author": {
7
7
  "name": "Ajay Kumar"
@@ -56,6 +56,7 @@ returns `{disabled: true}` and the mod opens and draws nothing. `presentation` i
56
56
  the Stop sweep and the delegation lines (`delegation-lib.ts`) read it, and an
57
57
  open panel question silences the checkpoint and the Stop sweep. Read
58
58
  `docs/verified-schemas.md` (mods section) before changing anything it records.
59
+ `/eklavya-panel-demo` (`DEMO_COMMAND`) is the maintainer's shortcut for trying the pane: it seeds `PanelState.demo` with a mock question and never calls the server, so it works with `quiz.panel` off. While `demo` is set the heartbeat (`beat`) does not sync, `submit` grades locally with `demoResult` (no `panel_answer`, grader or explainer) and Next restarts the mock; `/eklavya-panel` and a real `present_question` sync replace it. On a non-terminal surface it also offers the Answer cards switch: `button` is the production layout, `client` draws each option as a `Client` region from `card.tsx` that posts `{pick}` to the `ui.message` hook, which accepts only an id of the open question. The switch is demo-only until the whole-card press is observed on Desktop; then make it the default and say so in `docs/verified-schemas.md`.
59
60
  Run `scripts/test-panel-mod.sh`: it stages the mod as its own plugin, validates
60
61
  it, runs `claude plugin test`, then validates the distributed plugin.
61
62
 
@@ -0,0 +1,47 @@
1
+ import type { ClientModule } from 'claude-code'
2
+
3
+ /**
4
+ * One answer card drawn as a `Client` region, so a press anywhere on it (border,
5
+ * padding, title or note) picks it. A `Button` is only as large as its label, and a
6
+ * `Box` takes no press, so this is the one way to make the whole card the target.
7
+ *
8
+ * It gets no mods API: it holds only its hover and posts `{ pick: id }` to the hooks
9
+ * module, whose `ui.message` hook validates the id and records the pick. The colours
10
+ * come in as props, so the module imports nothing.
11
+ */
12
+ export type CardProps = {
13
+ id: string
14
+ label: string
15
+ note: string
16
+ chosen: boolean
17
+ locked: boolean
18
+ brand: string
19
+ fill: string
20
+ text: string
21
+ noteText: string
22
+ }
23
+
24
+ const Card: ClientModule<CardProps, { hover: boolean }> = (p, s) => {
25
+ const { Box, Text } = s.elements
26
+ const hover = s.state?.hover ?? false
27
+ // One listener, replaced on each call, so it always reads this render's props.
28
+ s.onPointer(e => {
29
+ if (e.type === 'enter' && !hover) s.setState({ hover: true })
30
+ else if (e.type === 'leave' && hover) s.setState({ hover: false })
31
+ else if (e.type === 'up' && !p.locked && (e.button === undefined || e.button === 'left')) s.post({ pick: p.id })
32
+ })
33
+ return Box({
34
+ flexDirection: 'column',
35
+ paddingX: 2,
36
+ paddingY: 1,
37
+ borderStyle: p.chosen ? 'bold' : 'single',
38
+ borderColor: p.chosen || (hover && !p.locked) ? p.brand : 'inactive',
39
+ backgroundColor: p.chosen ? p.fill : undefined,
40
+ children: [
41
+ Text({ wrap: 'wrap', bold: p.chosen, color: p.chosen ? p.text : undefined, children: [p.label] }),
42
+ Text({ wrap: 'wrap', dimColor: !p.chosen, color: p.chosen ? p.noteText : undefined, children: [p.note] }),
43
+ ],
44
+ })
45
+ }
46
+
47
+ export default Card
@@ -1,4 +1,4 @@
1
- import type { PanelQuestion, PanelState } from './types'
1
+ import type { PanelQuestion, PanelResult, PanelState } from './types'
2
2
 
3
3
  /** The pane's id, as `$.ui.open` and the `ui.render` hook both name it. */
4
4
  export const PANE = 'eklavya-quiz'
@@ -162,6 +162,15 @@ export const STR = {
162
162
  focus: 'Ctrl+X then Tab moves the keys here.',
163
163
  keys: '1–4 pick · o other · s submit · k skip · t teach me · Esc back to prompt',
164
164
  off: 'The quiz panel is off. Turn it on with: eklavya config set quiz.panel true',
165
+ demoBanner: 'DEMO · a mock question. Nothing here is graded by the server or recorded.',
166
+ demoReady: 'Last click: none yet',
167
+ demoOpened: 'Eklavya quiz panel opened with a mock question. Nothing you answer is recorded.',
168
+ demoFailed: 'The demo panel could not open. Try /eklavya-panel-demo again.',
169
+ demoStyleLabel: 'Answer cards:',
170
+ demoStyleButton: 'Native button',
171
+ demoStyleClient: 'Whole card',
172
+ demoExplanation: 'A rename inside one folder is atomic: the poller sees the finished file or none, never half of it. (This is a mock, so it was not recorded.)',
173
+ demoTyped: 'Typed answers are not graded in the demo. (Nothing was sent or recorded.)',
165
174
  } as const
166
175
 
167
176
  /** The explainer agent the panel starts itself, so no prompt lands in the transcript. */
@@ -202,6 +211,39 @@ export function explainerBrief(x: ExplainBlock): string {
202
211
 
203
212
  export const answerWas = (label: string): string => `The answer was: ${label}`
204
213
 
214
+ /** The command that opens the mock question; it never reaches the server, so it needs no question to be waiting. */
215
+ export const DEMO_COMMAND = 'eklavya-panel-demo'
216
+
217
+ /** The mock question: long answers, each with a note, as real questions have. `o3` is the right one. */
218
+ export const DEMO_QUESTION: PanelQuestion = {
219
+ question_id: 'demo',
220
+ repo: '*',
221
+ stem: 'A job writes a text file into a shared folder that another system polls every minute. Why is it safer to write the file under a temporary name and then rename it into place, rather than writing it directly under its final name?',
222
+ options: [
223
+ { id: 'o1', label: 'Renaming makes the write faster', note: 'A rename skips the disk write, so the final file appears sooner than a direct write would.' },
224
+ { id: 'o2', label: 'Renaming encrypts the file in transit', note: 'The temporary name keeps the contents protected until the rename publishes the finished file.' },
225
+ { id: 'o3', label: 'The poller never sees a half-written file', note: 'A rename within one folder is atomic, so the file appears complete or not at all to the reader.' },
226
+ { id: 'o4', label: 'Renaming lets two jobs write at once', note: 'Each job writes its own temporary name, so both can write the same final file without any conflict.' },
227
+ ],
228
+ concept_name: 'Atomic file replace',
229
+ tier: 2,
230
+ more: true,
231
+ }
232
+
233
+ export const DEMO_RIGHT = 'o3'
234
+
235
+ /** What the server would reply, made here: a demo answer is graded on this machine and recorded nowhere. */
236
+ export function demoResult(kind: 'choice' | 'text' | 'skip' | 'dont_know', optionId: string | null): PanelResult {
237
+ const right = DEMO_QUESTION.options.find(o => o.id === DEMO_RIGHT)!.label
238
+ if (kind === 'skip') return { phase: 'skipped', attempt_id: 0 }
239
+ if (kind === 'dont_know') return { phase: 'answered', attempt_id: 0, taught: true, correct: false, correct_label: right, explanation: STR.demoExplanation }
240
+ const correct = kind === 'choice' && optionId === DEMO_RIGHT
241
+ return { phase: 'answered', attempt_id: 0, correct, correct_label: right, explanation: kind === 'text' ? STR.demoTyped : STR.demoExplanation }
242
+ }
243
+
244
+ /** The tester's line of what was just clicked, and how the click arrived. */
245
+ export const demoLast = (what: string): string => `Last click: ${what}`
246
+
205
247
  /**
206
248
  * The key hints. Only the terminal gets them: there Ctrl+X then Tab moves the keys into the pane. The
207
249
  * desktop app keeps typing in its prompt even after a click on the pane (seen 2026-10-16), so a mod's