eklavya 1.62.0 → 1.63.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 (38) hide show
  1. package/dist/assets/dashboard.html +42 -38
  2. package/dist/assets/mascot/expressions/celebrate-body.svg +69 -0
  3. package/dist/assets/mascot/expressions/celebrate-face.svg +43 -0
  4. package/dist/assets/mascot/expressions/confused-body.svg +69 -0
  5. package/dist/assets/mascot/expressions/confused-face.svg +43 -0
  6. package/dist/assets/mascot/expressions/curious-body.svg +69 -0
  7. package/dist/assets/mascot/expressions/curious-face.svg +43 -0
  8. package/dist/assets/mascot/expressions/excited-body.svg +69 -0
  9. package/dist/assets/mascot/expressions/excited-face.svg +43 -0
  10. package/dist/assets/mascot/expressions/focused-body.svg +69 -0
  11. package/dist/assets/mascot/expressions/focused-face.svg +43 -0
  12. package/dist/assets/mascot/expressions/happy-body.svg +69 -0
  13. package/dist/assets/mascot/expressions/happy-face.svg +43 -0
  14. package/dist/assets/mascot/expressions/neutral-body.svg +69 -0
  15. package/dist/assets/mascot/expressions/neutral-face.svg +43 -0
  16. package/dist/assets/mascot/expressions/sad-body.svg +69 -0
  17. package/dist/assets/mascot/expressions/sad-face.svg +43 -0
  18. package/dist/assets/mascot/expressions/sleepy-body.svg +69 -0
  19. package/dist/assets/mascot/expressions/sleepy-face.svg +43 -0
  20. package/dist/assets/mascot/expressions/surprised-body.svg +69 -0
  21. package/dist/assets/mascot/expressions/surprised-face.svg +43 -0
  22. package/dist/assets/mascot/expressions/thinking-body.svg +69 -0
  23. package/dist/assets/mascot/expressions/thinking-face.svg +43 -0
  24. package/dist/assets/mascot/expressions/wink-body.svg +69 -0
  25. package/dist/assets/mascot/expressions/wink-face.svg +43 -0
  26. package/dist/assets/mascot/expressions/worried-body.svg +69 -0
  27. package/dist/assets/mascot/expressions/worried-face.svg +43 -0
  28. package/dist/assets/mascot/loading.svg +69 -0
  29. package/dist/assets/mascot/mascot.css +23 -0
  30. package/dist/assets/mascot/mascot.js +89 -0
  31. package/dist/assets/mascot.html +43 -0
  32. package/dist/assets/tokens.css +17 -0
  33. package/dist/dashboard.js +10 -0
  34. package/dist/dashboard.js.map +1 -1
  35. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  36. package/dist/plugin/hooks/panel/model.ts +4 -0
  37. package/dist/plugin/hooks/panel/register.tsx +11 -0
  38. package/package.json +1 -1
@@ -44,6 +44,8 @@
44
44
  this page is fetched from anywhere but this loopback server. The font
45
45
  stacks fall through to the system faces they already name. -->
46
46
  <link rel="stylesheet" href="/tokens.css">
47
+ <link rel="stylesheet" href="/brand/mascot/mascot.css">
48
+ <script src="/brand/mascot/mascot.js" defer></script>
47
49
  <!-- Feature tips: Driver.js's hints module, pinned and served by this server (see TIP). -->
48
50
  <link rel="stylesheet" href="/vendor/driver-hints.css">
49
51
  <script src="/vendor/driver-hints.js" defer></script>
@@ -154,23 +156,13 @@
154
156
  animation:skel 1.4s var(--ease) infinite alternate; }
155
157
  @keyframes skel { from { opacity:.55; } to { opacity:1; } }
156
158
 
157
- /* ---------- the loader: the mark, held still, with its tile traced ---------- */
158
- /* One loader for every wait on the page: `loader(label)`. The mark is the
159
- brand mark and never moves; a short --spot segment travels round a square
160
- outline just outside its tile. It fades in after 150ms so a fast loopback
161
- answer never flashes it; reduced motion stops the trace and keeps the label. */
159
+ /* One companion loader for every wait. A delayed reveal avoids flicker;
160
+ the shared mascot stylesheet respects reduced motion. */
162
161
  .loader { display:flex; align-items:center; gap:var(--space-4); padding:var(--space-6) 0; color:var(--dim);
163
162
  font-family:var(--font-mono); font-size:12px; letter-spacing:.04em;
164
163
  animation:loader-in var(--duration-base) var(--ease) 150ms both; }
165
164
  .loader--page { justify-content:center; flex-direction:column; min-height:40vh; gap:var(--space-5); }
166
- .loader .mark { position:relative; overflow:visible; }
167
- .loader__trace { position:absolute; inset:-5px; width:calc(100% + 10px); height:calc(100% + 10px); overflow:visible; }
168
- .loader__trace rect { fill:none; stroke:var(--spot); stroke-width:1.5; stroke-dasharray:32 128;
169
- animation:trace 1.6s linear infinite; }
170
- .loader--page .mark { width:40px; height:40px; }
171
- .loader--page .mark svg:first-child { width:24px; height:24px; }
172
165
  @keyframes loader-in { from { opacity:0; } to { opacity:1; } }
173
- @keyframes trace { to { stroke-dashoffset:-160; } }
174
166
 
175
167
  /* ---------- the sidebar navigation ---------- */
176
168
  nav.side__nav { display:flex; flex-direction:column; gap:var(--space-4); }
@@ -940,6 +932,7 @@
940
932
  <span>dashboard</span>
941
933
  </div>
942
934
  </a>
935
+ <a href="/mascot.html" class="ek-companion ek-companion--rail"><eklavya-mascot state="info" variant="face" size="40"></eklavya-mascot><span>Meet your companion ↗</span></a>
943
936
 
944
937
  <div class="wf" id="wf">
945
938
  <a class="wf__main" id="wf-main" href="#/learning/dashboard"></a>
@@ -982,7 +975,7 @@
982
975
  <p class="page__eyebrow crumb inner" id="crumb" aria-label="Workflow and scope"></p>
983
976
  <div class="inner stack" id="view" aria-busy="true">
984
977
  <!-- The same markup `loader()` writes; the script has not run yet. -->
985
- <div class="loader loader--page" role="status" aria-live="polite"><div class="mark" aria-hidden="true"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4c5 2.5 5 13.5 0 16"/><line x1="6" y1="4" x2="6" y2="20"/><line x1="4" y1="12" x2="18" y2="12"/><polyline points="14.5 8.5 18 12 14.5 15.5"/></svg><svg class="loader__trace" viewBox="0 0 40 40" aria-hidden="true"><rect x="0.75" y="0.75" width="38.5" height="38.5" pathLength="160"/></svg></div><span>Loading your history…</span></div>
978
+ <div class="loader loader--page" role="status" aria-live="polite"><eklavya-mascot state="loading" variant="loading" size="96"></eklavya-mascot><span>Loading your history…</span></div>
986
979
  </div>
987
980
  <span class="sr" id="fb-live" role="status" aria-live="polite"></span>
988
981
  <footer class="foot inner">
@@ -1362,8 +1355,16 @@ function copyCommand(btn) {
1362
1355
  * One line per item: `{ t, s, v, go, cls }` — a title, a quiet mono second
1363
1356
  * line, one value on the right, and where it opens. All HTML the caller escaped.
1364
1357
  */
1358
+ /** State art is decorative; the caller owns escaped content and visible copy. */
1359
+ function mascotMessage(content, state = 'empty') {
1360
+ return `<div class="ek-mascot-message"><eklavya-mascot state="${state}" size="56"></eklavya-mascot><div>${content}</div></div>`;
1361
+ }
1362
+ function mascotStatus(text, state) {
1363
+ return `<span class="ek-mascot-status"><eklavya-mascot state="${state}" variant="face" size="24"></eklavya-mascot>${esc(text)}</span>`;
1364
+ }
1365
+
1365
1366
  function lines(items, empty = 'Nothing here yet.') {
1366
- if (!items.length) return `<p class="empty">${empty}</p>`;
1367
+ if (!items.length) return mascotMessage(`<p class="empty">${empty}</p>`);
1367
1368
  return `<ul class="lines">` + items.map((x) =>
1368
1369
  `<li${x.go ? ` data-go="${esc(x.go)}" role="link"` : ''}${x.cls ? ` class="${x.cls}"` : ''}>`
1369
1370
  + `<div class="ln__main"><span class="ln__t">${x.t}</span>${x.s ? `<span class="ln__s">${x.s}</span>` : ''}</div>`
@@ -1392,7 +1393,7 @@ function qaLine(a, lead, linkConcept = false) {
1392
1393
  }
1393
1394
 
1394
1395
  function table(cols, rows) {
1395
- if (!rows.length) return '<p class="empty">Nothing here yet.</p>';
1396
+ if (!rows.length) return mascotMessage('<p class="empty">Nothing here yet.</p>');
1396
1397
  return `<div class="scroll"><table><thead><tr>`
1397
1398
  + cols.map((c) => `<th${c.n ? ' class="num"' : ''}${c.w ? ` style="width:${c.w}"` : ''}>${esc(c.t)}</th>`).join('')
1398
1399
  + `</tr></thead><tbody>` + rows.join('') + `</tbody></table></div>`;
@@ -1696,13 +1697,13 @@ let GEN = 0;
1696
1697
  * not exist until `render()` has written the view's `innerHTML`.
1697
1698
  */
1698
1699
  /**
1699
- * The one loading state: the mark loosing its arrow, and what is being waited
1700
+ * The one loading state: the companion drawing and releasing its arrow, and what is being waited
1700
1701
  * for. `page: true` is the full-height version the first paint uses. Every
1701
1702
  * placeholder a `fill()` target starts with is one of these, so a wait looks
1702
1703
  * the same wherever it happens. `label` is plain text; it is escaped here.
1703
1704
  */
1704
1705
  function loader(label, { page = false } = {}) {
1705
- return `<div class="loader${page ? ' loader--page' : ''}" role="status" aria-live="polite"><div class="mark" aria-hidden="true"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4c5 2.5 5 13.5 0 16"/><line x1="6" y1="4" x2="6" y2="20"/><line x1="4" y1="12" x2="18" y2="12"/><polyline points="14.5 8.5 18 12 14.5 15.5"/></svg><svg class="loader__trace" viewBox="0 0 40 40" aria-hidden="true"><rect x="0.75" y="0.75" width="38.5" height="38.5" pathLength="160"/></svg></div><span>${esc(label)}</span></div>`;
1706
+ return `<div class="loader${page ? ' loader--page' : ''}" role="status" aria-live="polite"><eklavya-mascot state="loading" variant="loading" size="${page ? 96 : 56}"></eklavya-mascot><span>${esc(label)}</span></div>`;
1706
1707
  }
1707
1708
 
1708
1709
  function fill(id, url, draw) {
@@ -1725,7 +1726,7 @@ function fill(id, url, draw) {
1725
1726
  TIP.sync();
1726
1727
  }).catch((e) => {
1727
1728
  const el = $(id);
1728
- if (el && gen === GEN) { el.innerHTML = `<p class="empty">Could not load: ${esc(e.message)}</p>`; el.removeAttribute('aria-busy'); }
1729
+ if (el && gen === GEN) { el.innerHTML = `<div class="ek-mascot-message"><eklavya-mascot state="error" size="56"></eklavya-mascot><p class="empty">Could not load: ${esc(e.message)}</p></div>`; el.removeAttribute('aria-busy'); }
1729
1730
  });
1730
1731
  });
1731
1732
  }
@@ -1968,7 +1969,7 @@ function viewConcepts(param) {
1968
1969
 
1969
1970
  function viewConcept(slug) {
1970
1971
  const c = S.concepts.find((x) => x.slug === slug);
1971
- if (!c) return `<p class="empty">No concept called <code>${esc(slug)}</code>. <a class="link" data-go="${esc(LH('concepts'))}">Back to concepts</a>.</p>`;
1972
+ if (!c) return mascotMessage(`<p class="empty">No concept called <code>${esc(slug)}</code>. <a class="link" data-go="${esc(LH('concepts'))}">Back to concepts</a>.</p>`, 'notFound');
1972
1973
 
1973
1974
  const atts = S.attempts.filter((a) => a.slug === slug);
1974
1975
  // A logged row with no context line is a bookkeeping row, not a memory: it
@@ -2004,7 +2005,7 @@ function viewConcept(slug) {
2004
2005
 
2005
2006
  card('Every question you were asked', atts.length ? `${plural(atts.length, 'attempt')}, newest first. Open one for the options and the feedback.` : '',
2006
2007
  atts.length ? history + pager('concept', atts.length, p, PER_Q)
2007
- : '<p class="empty">Not asked yet. It will come up when your work touches it.</p>'),
2008
+ : mascotMessage('<p class="empty">Not asked yet. It will come up when your work touches it.</p>')),
2008
2009
 
2009
2010
  atts.length
2010
2011
  ? fold('concept:grades', 'How the grades went', plural(atts.length, 'grade'),
@@ -2019,7 +2020,7 @@ function viewConcept(slug) {
2019
2020
  s: `${esc(ago(l.ts))} · ${esc(pname(l.repo, l.session_id))} · ${esc(l.origin ?? 'work')}`,
2020
2021
  go: sessHref(l.session_id),
2021
2022
  })))
2022
- : '<p class="empty">No context recorded — this one came from the seed graph.</p>',
2023
+ : mascotMessage('<p class="empty">No context recorded — this one came from the seed graph.</p>'),
2023
2024
  { hint: 'What the agent said the concept was for, in the code it wrote. Open a line for that session.' }),
2024
2025
 
2025
2026
  fold('concept:graph', 'Connected concepts',
@@ -2131,8 +2132,8 @@ function viewSessions() {
2131
2132
  ].join('');
2132
2133
  }
2133
2134
 
2134
- const noSession = (id) => `<p class="empty">No session <code>${esc(id)}</code> in this scope.
2135
- <a class="link" data-go="${esc(href(CUR.wf, 'sessions'))}">Back to sessions</a>.</p>`;
2135
+ const noSession = (id) => mascotMessage(`<p class="empty">No session <code>${esc(id)}</code> in this scope.
2136
+ <a class="link" data-go="${esc(href(CUR.wf, 'sessions'))}">Back to sessions</a>.</p>`, 'notFound');
2136
2137
 
2137
2138
  /**
2138
2139
  * One session, from either workflow. Shared rendering: the learning half and
@@ -2298,7 +2299,7 @@ function viewProjects() {
2298
2299
  fourth Node service and their first Rust one. Everyone starts at easy (tiers 1–2) and climbs on
2299
2300
  evidence — a promotion needs passing answers <em>and</em> accuracy <em>and</em> distinct concepts. A
2300
2301
  project appears here as soon as its work logs a concept, before a single question is asked.`),
2301
- folds.join('') || '<p class="empty">No project has logged or answered anything yet.</p>',
2302
+ folds.join('') || mascotMessage('<p class="empty">No project has logged or answered anything yet.</p>'),
2302
2303
  other.length
2303
2304
  ? fold('proj:memory-only', 'Memory only', plural(other.length, 'project'),
2304
2305
  lines(other.map((p) => ({
@@ -2348,8 +2349,8 @@ function viewDomains() {
2348
2349
 
2349
2350
  function viewDomain(name) {
2350
2351
  const all = conceptsInScope().filter((c) => c.domain === name);
2351
- if (!all.length) return `<p class="empty">No concepts in <code>${esc(name)}</code> for this scope.
2352
- <a class="link" data-go="${esc(LH('domains'))}">Back to domains</a>.</p>`;
2352
+ if (!all.length) return mascotMessage(`<p class="empty">No concepts in <code>${esc(name)}</code> for this scope.
2353
+ <a class="link" data-go="${esc(LH('domains'))}">Back to domains</a>.</p>`);
2353
2354
  const seen = all.filter((c) => c.seen);
2354
2355
  const list = [...all].sort((a, b) => (a.seen === b.seen ? a.score - b.score : a.seen ? -1 : 1));
2355
2356
  const p = page('domain', list.length);
@@ -2417,7 +2418,7 @@ function tlEntry(e) {
2417
2418
  */
2418
2419
  function drawMemory(d) {
2419
2420
  T.memory.page = d.page; // the server clamps a page number a filter shrank
2420
- if (!d.rows.length) return '<p class="empty">Nothing in memory matches these filters.</p>';
2421
+ if (!d.rows.length) return mascotMessage('<p class="empty">Nothing in memory matches these filters.</p>');
2421
2422
  const summaries = new Map(d.rows.filter((e) => e.kind === 'session_summary' && e.session_id).map((e) => [e.session_id, e]));
2422
2423
  const kids = new Map();
2423
2424
  for (const e of d.rows) {
@@ -2697,7 +2698,7 @@ function drawEntry(d) {
2697
2698
  `<div class="list">${d.replaces.map((x) => `<div>${link(x)}</div>`).join('')}</div>`)
2698
2699
  : '',
2699
2700
  fold('entry:evidence', 'The raw evidence', plural(d.events.length, 'event'),
2700
- evidence || '<p class="empty">No evidence is linked — this entry was written by hand or imported.</p>',
2701
+ evidence || mascotMessage('<p class="empty">No evidence is linked — this entry was written by hand or imported.</p>'),
2701
2702
  { hint: 'Every event this was distilled from, exactly as captured. Open one to read it.' }),
2702
2703
  d.candidates.length
2703
2704
  ? fold('entry:cands', 'Concepts it proposed', plural(d.candidates.length, 'candidate'),
@@ -2723,7 +2724,7 @@ function drawEntry(d) {
2723
2724
 
2724
2725
  function viewEntry(id) {
2725
2726
  fill('entry-body', '/api/memory/entry?id=' + encodeURIComponent(id), (d) =>
2726
- d && d.entry ? drawEntry(d) : `<p class="empty">No observation <code>${esc(id)}</code>.</p>`);
2727
+ d && d.entry ? drawEntry(d) : mascotMessage(`<p class="empty">No observation <code>${esc(id)}</code>.</p>`, 'notFound'));
2727
2728
  return `<div><a class="page__back" data-go="${esc(MH('timeline'))}">← Timeline</a></div>
2728
2729
  <div class="stack" id="entry-body">${loader('Loading the observation…')}</div>`;
2729
2730
  }
@@ -2926,7 +2927,7 @@ function viewArtifacts(param) {
2926
2927
  const projects = new Set(all.map((a) => pid(a.project))).size;
2927
2928
  const chip = (k, label) => `<button class="chip" data-go="${esc(AH('dashboard', k === 'all' ? null : k))}" aria-pressed="${kind === k}">${label} ${counts[k]}</button>`;
2928
2929
  const body = !list.length
2929
- ? `<p class="empty">${all.length ? 'Nothing matches that search.' : 'No artifacts yet. Ask for an Eklavya artifact, or miss a question with explain_on_wrong on, and they land here.'}</p>`
2930
+ ? mascotMessage(`<p class="empty">${all.length ? 'Nothing matches that search.' : 'No artifacts yet. Ask for an Eklavya artifact, or miss a question with explain_on_wrong on, and they land here.'}</p>`)
2930
2931
  : [...groups].map(([id, rows]) =>
2931
2932
  `<h3 class="slug" style="margin:var(--space-5) 0 var(--space-2)">${esc(projectById(id)?.name ?? (id === '~' ? 'no project' : base(id)))}</h3>`
2932
2933
  + `<div class="gallery">${rows.map(artifactCard).join('')}</div>`).join('') + pager('artifacts', list.length, p);
@@ -3033,7 +3034,7 @@ function shuffled(n) {
3033
3034
 
3034
3035
  function fixSay(text, kind) {
3035
3036
  const m = $('fix-msg');
3036
- m.textContent = text;
3037
+ m.innerHTML = text ? mascotStatus(text, kind === 'ok' ? 'success' : kind === 'wrong' ? 'tip' : 'error') : '';
3037
3038
  m.className = 'fix__msg' + (kind ? ' ' + kind : '');
3038
3039
  }
3039
3040
 
@@ -3520,7 +3521,7 @@ function fieldError(key, text) {
3520
3521
  const el = document.querySelector(`[data-set="${CSS.escape(key)}"]`);
3521
3522
  const box = el && $(el.id + '-e');
3522
3523
  if (!box) return;
3523
- box.textContent = text || '';
3524
+ box.innerHTML = text ? mascotStatus(text, 'validation') : '';
3524
3525
  box.hidden = !text;
3525
3526
  if (text) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
3526
3527
  }
@@ -3666,6 +3667,7 @@ const fbCmd = (text) => `<div class="cmd"><code>${esc(text)}</code><button type=
3666
3667
  /** Why there is no item: the one reason, in plain words. */
3667
3668
  function fbEmpty(F) {
3668
3669
  let body;
3670
+ let state = 'empty';
3669
3671
  if (!F.enabled) {
3670
3672
  body = `<p>Prompt feedback is off. Turn on <code>feedback.enabled</code> in Settings to start.</p>
3671
3673
  <a class="btn" data-go="${esc(href('settings', 'user'))}">Open Settings</a>`;
@@ -3675,11 +3677,12 @@ function fbEmpty(F) {
3675
3677
  body = `<p>Prompt feedback needs an observer model. Run <code>eklavya config set providers.observer &lt;model&gt;</code>.</p>${fbCmd(FB.observerCmd)}
3676
3678
  <p class="cmd__note">Put the model's name in place of &lt;model&gt;, for example claude-haiku-4-5. Setting it lets Eklavya send your prompts to that model, through your Claude Code login.</p>`;
3677
3679
  } else if (F.failed) {
3680
+ state = 'error';
3678
3681
  body = `<p>Couldn't review the last session. It will try again at a later start.</p>`;
3679
3682
  } else {
3680
3683
  body = `<p>Nothing to review yet. When a session ends, the next one you start reviews one of its prompts.</p>`;
3681
3684
  }
3682
- return `<div class="card fb__why" id="fb-empty">${body}</div>`;
3685
+ return `<div class="card fb__why" id="fb-empty"><div class="ek-mascot-message"><eklavya-mascot state="${state}" size="72"></eklavya-mascot><div>${body}</div></div></div>`;
3683
3686
  }
3684
3687
 
3685
3688
  function viewFeedbackDashboard() {
@@ -3807,7 +3810,7 @@ function fbDone(text) {
3807
3810
 
3808
3811
  function fbFail(e) {
3809
3812
  const m = $('fb-msg');
3810
- if (m) m.textContent = `Couldn't do that: ${e.message}`;
3813
+ if (m) m.innerHTML = mascotStatus(`Couldn't do that: ${e.message}`, 'error');
3811
3814
  }
3812
3815
 
3813
3816
  function fbAcknowledge(btn) {
@@ -3820,7 +3823,7 @@ function fbAcknowledge(btn) {
3820
3823
 
3821
3824
  function fbAskDelete(btn) {
3822
3825
  const id = +btn.dataset.fbDel;
3823
- $('fb-actions').innerHTML = `<span class="fb__ask" role="group" aria-label="Confirm delete"><span>Delete this feedback for good?</span>
3826
+ $('fb-actions').innerHTML = `<span class="fb__ask" role="group" aria-label="Confirm delete"><span class="ek-mascot-status"><eklavya-mascot state="warning" variant="face" size="24"></eklavya-mascot>Delete this feedback for good?</span>
3824
3827
  <button type="button" class="btn" data-fb-yes="${id}">Delete</button>
3825
3828
  <button type="button" class="set__btn" data-fb-no="${id}">Cancel</button></span><p class="fb__msg" id="fb-msg" role="alert"></p>`;
3826
3829
  document.querySelector('[data-fb-no]').focus();
@@ -3869,7 +3872,7 @@ function postJson(path, body) {
3869
3872
  function saveSetting(key, change) {
3870
3873
  const scope = CUR.page === 'project' ? 'project' : 'user';
3871
3874
  const msg = document.querySelector(`[data-msg="${CSS.escape(key)}"]`);
3872
- const say = (text, warn) => { if (msg) { msg.textContent = text; msg.className = warn ? 'warn' : 'here'; } };
3875
+ const say = (text, warn) => { if (msg) { msg.innerHTML = text ? mascotStatus(text, warn ? 'error' : 'loading') : ''; msg.className = warn ? 'warn' : 'here'; } };
3873
3876
  fieldError(key, null);
3874
3877
  say('saving…');
3875
3878
  const gen = GEN;
@@ -3884,7 +3887,7 @@ function saveSetting(key, change) {
3884
3887
  const back = (focusId && $(focusId)) || document.querySelector(`[data-set="${CSS.escape(key)}"]`);
3885
3888
  back?.focus({ preventScroll: true });
3886
3889
  const m = document.querySelector(`[data-msg="${CSS.escape(key)}"]`);
3887
- if (m) { m.textContent = change.unset ? 'inherited' : 'saved'; m.className = 'here'; }
3890
+ if (m) { m.innerHTML = mascotStatus(change.unset ? 'inherited' : 'saved', 'success'); m.className = 'here'; }
3888
3891
  })
3889
3892
  .catch((e) => {
3890
3893
  // A refused choice or switch is put back to what is on disk, so the
@@ -4586,6 +4589,7 @@ syncDrawer();
4586
4589
 
4587
4590
  function viewNotFound(what, bad) {
4588
4591
  return `<div>
4592
+ <eklavya-mascot state="${bad ? 'validation' : 'notFound'}" size="80"></eklavya-mascot>
4589
4593
  <h1 class="page__title">${bad ? 'This link is malformed' : 'No page here'}</h1>
4590
4594
  <p class="page__sub">${bad ? 'Part of' : 'Nothing answers to'} <code class="slug">${esc(what)}</code>${bad ? ' is not valid URL encoding, so it cannot be opened.' : '.'}
4591
4595
  Open the <a class="link" data-go="${esc(LH('dashboard'))}">Learning dashboard</a> or the
@@ -4892,7 +4896,7 @@ Promise.all([json('/api/state'), json('/api/projects')]).then(([s, inv]) => {
4892
4896
  }).catch((e) => {
4893
4897
  $('app').classList.remove('booting');
4894
4898
  $('view').removeAttribute('aria-busy');
4895
- $('view').innerHTML = `<p class="empty">Could not read the database: ${esc(e.message)}</p>`;
4899
+ $('view').innerHTML = `<div class="ek-mascot-message"><eklavya-mascot state="error" size="72"></eklavya-mascot><p class="empty">Could not read the database: ${esc(e.message)}</p></div>`;
4896
4900
  });
4897
4901
  </script>
4898
4902
  </body>
@@ -0,0 +1,69 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 200" width="200" height="200" class="ek-mascot" data-expression="celebrate" data-variant="body" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
+ Archer Cloak artwork keeps its approved identity across both grounds.
3
+ UI chrome continues to use the surface, text and accent roles below. */
4
+ --mascot-coral: #ef9875;
5
+ --mascot-coral-dark: #c96d51;
6
+ --mascot-highlight: #ffc8a6;
7
+ --mascot-cloak: #0E6E66;
8
+ --mascot-cloak-dark: #07403C;
9
+ --mascot-thread: #C4EDE4;
10
+ --mascot-ink: #302c2a;
11
+ --mascot-metal: #5b7c73;
12
+ --mascot-wood: #bc9569;
13
+ --mascot-wood-dark: #74583f;
14
+ --mascot-string: #EAE7E1;
15
+ --mascot-shadow: #26262a;
16
+ /* ---------- END MASCOT IDENTITY ---------- */}/* Identity colours and UI surfaces use Eklavya's semantic tokens. */
17
+ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line-height:0; }
18
+ .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
+ .ek-mascot[data-variant="face"] { overflow:hidden; }
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
+ .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
+ .ek-companion--hero { margin-bottom:16px; }
31
+ .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
+ .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
+ .ek-mascot-message p {margin:0}
34
+ @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
+
36
+ .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
+ .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
+ .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
+ </style><title>Eklavya, celebrating</title><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><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">
50
+ <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
+ <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"><path d="M64 74q5-7 10 0m36 0q5-7 10 0"/><path d="M82 87q10 22 20 0Z" fill="var(--mascot-ink)"/><path d="M87 98h10" 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"/>
59
+ </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)"/>
66
+ </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></svg>
@@ -0,0 +1,43 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="30 12 126 128" width="200" height="200" class="ek-mascot" data-expression="celebrate" data-variant="face" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
+ Archer Cloak artwork keeps its approved identity across both grounds.
3
+ UI chrome continues to use the surface, text and accent roles below. */
4
+ --mascot-coral: #ef9875;
5
+ --mascot-coral-dark: #c96d51;
6
+ --mascot-highlight: #ffc8a6;
7
+ --mascot-cloak: #0E6E66;
8
+ --mascot-cloak-dark: #07403C;
9
+ --mascot-thread: #C4EDE4;
10
+ --mascot-ink: #302c2a;
11
+ --mascot-metal: #5b7c73;
12
+ --mascot-wood: #bc9569;
13
+ --mascot-wood-dark: #74583f;
14
+ --mascot-string: #EAE7E1;
15
+ --mascot-shadow: #26262a;
16
+ /* ---------- END MASCOT IDENTITY ---------- */}/* Identity colours and UI surfaces use Eklavya's semantic tokens. */
17
+ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line-height:0; }
18
+ .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
+ .ek-mascot[data-variant="face"] { overflow:hidden; }
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
+ .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
+ .ek-companion--hero { margin-bottom:16px; }
31
+ .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
+ .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
+ .ek-mascot-message p {margin:0}
34
+ @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
+
36
+ .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
+ .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
+ .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
+ </style><title>Eklavya, celebrating</title><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><path d="M48 109q44-19 88 0l-8 30-33-15-31 15Z" fill="var(--mascot-cloak)"/><g class="ek-mascot__head">
40
+ <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"/>
41
+ <path d="M48 45h18V32h18V21h19" fill="none" stroke="var(--mascot-highlight)" stroke-width="3" stroke-linejoin="round"/>
42
+ <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="M64 74q5-7 10 0m36 0q5-7 10 0"/><path d="M82 87q10 22 20 0Z" fill="var(--mascot-ink)"/><path d="M87 98h10" stroke="var(--mascot-coral)"/></g>
43
+ </g><circle cx="95" cy="126" r="6" fill="var(--mascot-metal)"/></svg>
@@ -0,0 +1,69 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 200" width="200" height="200" class="ek-mascot" data-expression="confused" data-variant="body" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
+ Archer Cloak artwork keeps its approved identity across both grounds.
3
+ UI chrome continues to use the surface, text and accent roles below. */
4
+ --mascot-coral: #ef9875;
5
+ --mascot-coral-dark: #c96d51;
6
+ --mascot-highlight: #ffc8a6;
7
+ --mascot-cloak: #0E6E66;
8
+ --mascot-cloak-dark: #07403C;
9
+ --mascot-thread: #C4EDE4;
10
+ --mascot-ink: #302c2a;
11
+ --mascot-metal: #5b7c73;
12
+ --mascot-wood: #bc9569;
13
+ --mascot-wood-dark: #74583f;
14
+ --mascot-string: #EAE7E1;
15
+ --mascot-shadow: #26262a;
16
+ /* ---------- END MASCOT IDENTITY ---------- */}/* Identity colours and UI surfaces use Eklavya's semantic tokens. */
17
+ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line-height:0; }
18
+ .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
+ .ek-mascot[data-variant="face"] { overflow:hidden; }
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
+ .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
+ .ek-companion--hero { margin-bottom:16px; }
31
+ .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
+ .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
+ .ek-mascot-message p {margin:0}
34
+ @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
+
36
+ .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
+ .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
+ .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
+ </style><title>Eklavya, seeking clarity</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">
50
+ <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
+ <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="M83 93q5-7 10-2t9-2"/><path d="M62 58l14-5m32 4h14" fill="none"/></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"/>
59
+ </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)"/>
66
+ </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></svg>
@@ -0,0 +1,43 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="30 12 126 128" width="200" height="200" class="ek-mascot" data-expression="confused" data-variant="face" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
+ Archer Cloak artwork keeps its approved identity across both grounds.
3
+ UI chrome continues to use the surface, text and accent roles below. */
4
+ --mascot-coral: #ef9875;
5
+ --mascot-coral-dark: #c96d51;
6
+ --mascot-highlight: #ffc8a6;
7
+ --mascot-cloak: #0E6E66;
8
+ --mascot-cloak-dark: #07403C;
9
+ --mascot-thread: #C4EDE4;
10
+ --mascot-ink: #302c2a;
11
+ --mascot-metal: #5b7c73;
12
+ --mascot-wood: #bc9569;
13
+ --mascot-wood-dark: #74583f;
14
+ --mascot-string: #EAE7E1;
15
+ --mascot-shadow: #26262a;
16
+ /* ---------- END MASCOT IDENTITY ---------- */}/* Identity colours and UI surfaces use Eklavya's semantic tokens. */
17
+ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line-height:0; }
18
+ .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
+ .ek-mascot[data-variant="face"] { overflow:hidden; }
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
+ .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
+ .ek-companion--hero { margin-bottom:16px; }
31
+ .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
+ .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
+ .ek-mascot-message p {margin:0}
34
+ @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
+
36
+ .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
+ .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
+ .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
+ </style><title>Eklavya, seeking clarity</title><path d="M48 109q44-19 88 0l-8 30-33-15-31 15Z" fill="var(--mascot-cloak)"/><g class="ek-mascot__head">
40
+ <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"/>
41
+ <path d="M48 45h18V32h18V21h19" fill="none" stroke="var(--mascot-highlight)" stroke-width="3" stroke-linejoin="round"/>
42
+ <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="M83 93q5-7 10-2t9-2"/><path d="M62 58l14-5m32 4h14" fill="none"/></g>
43
+ </g><circle cx="95" cy="126" r="6" fill="var(--mascot-metal)"/></svg>
@@ -0,0 +1,69 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 200" width="200" height="200" class="ek-mascot" data-expression="curious" data-variant="body" role="img"><style>:root {/* ---------- MASCOT IDENTITY ----------
2
+ Archer Cloak artwork keeps its approved identity across both grounds.
3
+ UI chrome continues to use the surface, text and accent roles below. */
4
+ --mascot-coral: #ef9875;
5
+ --mascot-coral-dark: #c96d51;
6
+ --mascot-highlight: #ffc8a6;
7
+ --mascot-cloak: #0E6E66;
8
+ --mascot-cloak-dark: #07403C;
9
+ --mascot-thread: #C4EDE4;
10
+ --mascot-ink: #302c2a;
11
+ --mascot-metal: #5b7c73;
12
+ --mascot-wood: #bc9569;
13
+ --mascot-wood-dark: #74583f;
14
+ --mascot-string: #EAE7E1;
15
+ --mascot-shadow: #26262a;
16
+ /* ---------- END MASCOT IDENTITY ---------- */}/* Identity colours and UI surfaces use Eklavya's semantic tokens. */
17
+ eklavya-mascot { display:inline-flex; flex-shrink:0; vertical-align:middle; line-height:0; }
18
+ .ek-mascot { display:block; overflow:visible; flex-shrink:0; }
19
+ .ek-mascot[data-variant="face"] { overflow:hidden; }
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
+ .ek-companion { display:flex;align-items:center;gap:12px;color:var(--dim);font:13px/1.5 var(--font-body); }
30
+ .ek-companion--hero { margin-bottom:16px; }
31
+ .ek-companion--hero b { display:block;color:var(--ink);font-weight:600; }
32
+ .ek-mascot-message { display:flex;align-items:center;gap:16px;padding:16px 0; }
33
+ .ek-mascot-message p {margin:0}
34
+ @media (prefers-reduced-motion:reduce) { .ek-mascot * {animation:none !important;transition:none !important} }
35
+
36
+ .ek-companion--rail { padding:12px 20px; text-decoration:none; }
37
+ .ek-mascot-message > :last-child { min-width:0; overflow-wrap:break-word; }
38
+ .ek-mascot-status { display:inline-flex; align-items:center; gap:6px; }
39
+ </style><title>Eklavya, curious</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">
50
+ <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
+ <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="M86 91q6-4 12 0"/><path d="M62 59h14m32-6 14 5" fill="none"/></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"/>
59
+ </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)"/>
66
+ </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></svg>