@litfamily/litclaude 1.0.30 → 1.0.32

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.32 - 2026-10-17
4
+
5
+ - The desktop mod card has its look and motion back. 1.0.31 drew it with the app's plain boxes and text to stop it blinking, but that also took away its design and all of its animation. This release restores the animated card from 1.0.30. Its animations now start already visible, so when the app reloads the card after a real change, the card no longer fades in from blank or restarts its border light from the corner. The clock stays off the desktop card, so a running tool alone does not reload it. The terminal and phone cards are unchanged.
6
+
7
+ ## 1.0.31 - 2026-10-17
8
+
9
+ - In the Claude Code desktop app, the mod card used to blink: every real change, such as a new step, tool or skill, swapped in a whole new page, so the card went blank for a moment and its animations started over. The desktop card is now built from the app's own boxes and text, and a change updates only what moved. It looks the same but no longer animates. The terminal and phone cards are unchanged.
10
+
3
11
  ## 1.0.30 - 2026-10-16
4
12
 
5
13
  - The pre-write guard now refuses more of the lines that tell a reader what a document is not for, such as `This table is not a ranking.` or `이 문서는 제품을 비교하려는 것이 아닙니다.` These forms came from real sessions. Bold-label callouts such as `> **주의:** ...` are now read too, while ordinary quoted text is still skipped. Lines from manuals and lab reports that only look similar stay allowed, and a large flood of matches can no longer push the scan past its limit into an allowed write.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitClaude robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitClaude robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  <h1 align="center">LitClaude</h1>
4
4
  <p align="center"><strong>Keep the work lit.</strong></p>
5
5
  <p align="center">A Claude Code plugin that helps every session plan the work, build it, check it, and leave a note the next session can pick up.</p>
6
6
  <p align="center">
7
- <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" />
8
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
7
+ <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/readme/badge-version.svg" alt="1.0.32" />
8
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
9
9
  </p>
10
10
  <p align="center">
11
- <a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/README_ko-KR.md">한국어</a>
11
+ <a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/README_ko-KR.md">한국어</a>
12
12
  </p>
13
13
 
14
14
  Add `lit` to a Claude Code prompt and Claude pins the goal, writes a failing test first,
@@ -73,7 +73,7 @@ the `/plugin` menu.
73
73
  ## More
74
74
 
75
75
  - [Full guide and skills gallery on GitHub](https://github.com/wjgoarxiv/litclaude#readme)
76
- - [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/privacy.md)
77
- - [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/migration.md)
78
- - [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/CHANGELOG.md)
79
- - [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE)
76
+ - [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/privacy.md)
77
+ - [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/migration.md)
78
+ - [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/CHANGELOG.md)
79
+ - [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/LICENSE)
package/README_ko-KR.md CHANGED
@@ -1,14 +1,14 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
2
2
 
3
3
  <h1 align="center">LitClaude</h1>
4
4
  <p align="center"><strong>Keep the work lit.</strong></p>
5
5
  <p align="center">Claude Code에서 세션마다 계획하고, 만들고, 확인한 뒤 다음 세션이 이어받을 기록을 남기게 돕는 플러그인입니다.</p>
6
6
  <p align="center">
7
- <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" />
8
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
7
+ <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/readme/badge-version.svg" alt="1.0.32" />
8
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
9
9
  </p>
10
10
  <p align="center">
11
- <a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/README.md">English</a>
11
+ <a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/README.md">English</a>
12
12
  </p>
13
13
 
14
14
  Claude Code 프롬프트에 `lit`을 붙이면 Claude가 목표를 고정하고, 실패하는 테스트를 먼저 쓰고,
@@ -71,7 +71,7 @@ LitClaude는 프로젝트 상태를 `.litclaude/`에 두고, 그 안의 `.gitign
71
71
  ## 더 보기
72
72
 
73
73
  - [전체 안내와 스킬 갤러리 (GitHub)](https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md)
74
- - [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/privacy.md)
75
- - [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/migration.md)
76
- - [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/CHANGELOG.md)
77
- - [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE)
74
+ - [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/privacy.md)
75
+ - [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/docs/migration.md)
76
+ - [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/CHANGELOG.md)
77
+ - [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.32/LICENSE)
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.30"><title>release: 1.0.30</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.30</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.32"><title>release: 1.0.32</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.32</text></g></svg>
package/docs/hooks.md CHANGED
@@ -470,7 +470,11 @@ pill and the border breathe and the step's spinner turns, and on `full` a light
470
470
  current stage and the current chips, a head runs the rail leaving it, the count bar's head
471
471
  shimmers and a comet runs around the border. An idle card is a still image. The app loads each new
472
472
  drawing from scratch and restarts its animation, so this card names the running tool without its
473
- clock and shows the loop round without its countdown. It is redrawn only when something you would
473
+ clock and shows the loop round without its countdown. Each animation starts part way through its
474
+ cycle, so a freshly loaded card already looks like the running one: the border and the pill are
475
+ lit, the light sits on its pill and the comet is part way round, not parked at the top-left
476
+ corner. The app still blanks the card for a moment while it loads a new drawing; the mod cannot
477
+ prevent that. It is redrawn only when something you would
474
478
  notice changes, such as the stage, the tool, a skill, a usage figure or the goal. While the card
475
479
  shows in a terminal and the app at once, the terminal's loop countdown and lane times wait for such
476
480
  a change too; its step clock keeps counting on its own.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litclaude",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Claude Code plugin: add lit to a prompt and Claude plans, tests, checks and reviews the work, then leaves a handoff for the next session.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "litclaude",
3
3
  "description": "Claude Code-native workflow plugin with a bounded-authority start-work lifecycle.",
4
- "version": "1.0.30",
4
+ "version": "1.0.32",
5
5
  "author": {
6
6
  "name": "LitClaude contributors"
7
7
  },
@@ -9,7 +9,7 @@ import {
9
9
  } from "../lib/wikify-knowledge.mjs";
10
10
 
11
11
  const protocolVersion = "2024-11-05";
12
- const serverVersion = "1.0.30";
12
+ const serverVersion = "1.0.32";
13
13
 
14
14
  const publicSourceReadTool = {
15
15
  name: "public_source_read",
@@ -13,7 +13,8 @@
13
13
  // head shimmers, the step spinner turns and a comet runs around the border (full only). Idle, it is
14
14
  // still. The drawing carries no step clock and no loop countdown: the surface loads each new
15
15
  // drawing afresh, restarting every animation, so a figure that moves with time alone would make the
16
- // card blink on every poll.
16
+ // card blink on every poll. Each loop opens mid-cycle (see `loop`), so a fresh load starts on a
17
+ // frame of the running card rather than on a dim border and a comet at the corner.
17
18
  //
18
19
  // Text is set in a monospace face and placed by counted cells (Hangul and emoji as two), so the
19
20
  // layout does not depend on measuring a font the drawing cannot see.
@@ -50,6 +51,14 @@ const ROW_Y = Object.freeze([28, 58, 86, 114, 142]);
50
51
  const PILL_H = 20;
51
52
  const FONT = "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, 'JetBrains Mono', Consolas, monospace";
52
53
 
54
+ // Timing of one looping animation, started `phase` of the way into its cycle. The surface loads
55
+ // each new drawing afresh, so every loop would otherwise open on its first frame: the breathing at
56
+ // its dimmest, the light off the pill's left edge, the comet parked at the top-left corner. A
57
+ // negative begin opens each loop mid-cycle, in a frame that looks like the card already running.
58
+ const loop = (ms, phase) => `dur="${ms}ms" begin="-${Math.round(ms * phase)}ms" repeatCount="indefinite"`;
59
+ // The breathing pieces share one phase so the pill, the border and the count bar's head stay in step.
60
+ const BREATH_PHASE = 0.25;
61
+
53
62
  const esc = (text) => String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
54
63
  const widthOf = (text) => textWidth(text) * SVG_CARD.cell;
55
64
  const levelColor = (percent) => (percent >= 90 ? C.hudRed : percent >= 70 ? C.hudYellow : C.hudGreen);
@@ -79,7 +88,7 @@ const pill = (x, y, label, { fill, ink, bold = true, sweep = false, breath = nul
79
88
  const w = widthOf(label) + 16;
80
89
  const top = y - PILL_H + 6;
81
90
  const parts = [`<rect x="${x.toFixed(1)}" y="${top}" width="${w.toFixed(1)}" height="${PILL_H}" rx="${PILL_H / 2}" fill="${fill}"/>`];
82
- if (breath) parts.push(`<rect x="${x.toFixed(1)}" y="${top}" width="${w.toFixed(1)}" height="${PILL_H}" rx="${PILL_H / 2}" fill="${breath}" opacity="0"><animate attributeName="opacity" values="0;${breathPeak};0" dur="${TIMINGS.breath}ms" repeatCount="indefinite"/></rect>`);
91
+ if (breath) parts.push(`<rect x="${x.toFixed(1)}" y="${top}" width="${w.toFixed(1)}" height="${PILL_H}" rx="${PILL_H / 2}" fill="${breath}" opacity="0"><animate attributeName="opacity" values="0;${breathPeak};0" ${loop(TIMINGS.breath, BREATH_PHASE)}/></rect>`);
83
92
  if (sweep) parts.push(`<rect x="${x.toFixed(1)}" y="${top}" width="${w.toFixed(1)}" height="${PILL_H}" rx="${PILL_H / 2}" fill="url(#lit-sweep)"/>`);
84
93
  parts.push(text(x + 8, y, label, { fill: ink, bold }));
85
94
  return { markup: parts.join(""), end: x + w };
@@ -197,7 +206,7 @@ const mapRowSvg = (model, accent, animate) => {
197
206
  out.push(`<line x1="${x1.toFixed(1)}" y1="${y - 4}" x2="${x2.toFixed(1)}" y2="${y - 4}" stroke="${C.rail}" stroke-width="2.5" stroke-linecap="round"/>`);
198
207
  const filled = count && count.total > 0 ? Math.min(1, count.done / count.total) : 0;
199
208
  if (filled > 0) out.push(`<line x1="${x1.toFixed(1)}" y1="${y - 4}" x2="${(x1 + filled * layout.rail).toFixed(1)}" y2="${y - 4}" stroke="${mapColor(accent, mid)}" stroke-width="2.5" stroke-linecap="round"/>`);
200
- if (animate.sweep) out.push(`<circle cx="${x1.toFixed(1)}" cy="${y - 4}" r="2.5" fill="#ffffff"><animate attributeName="cx" values="${x1.toFixed(1)};${x2.toFixed(1)}" dur="${TIMINGS.flow}ms" repeatCount="indefinite"/></circle>`);
209
+ if (animate.sweep) out.push(`<circle cx="${x1.toFixed(1)}" cy="${y - 4}" r="2.5" fill="#ffffff"><animate attributeName="cx" values="${x1.toFixed(1)};${x2.toFixed(1)}" ${loop(TIMINGS.flow, 0.5)}/></circle>`);
201
210
  } else {
202
211
  out.push(`<line x1="${x1.toFixed(1)}" y1="${y - 4}" x2="${x2.toFixed(1)}" y2="${y - 4}" stroke="${C.rail}" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.7"/>`);
203
212
  }
@@ -232,7 +241,7 @@ const detailRowSvg = (model, accent, animate) => {
232
241
  out.push(text(nameEnd, y, model.step.name, { fill: C.white, anchor: "end" }));
233
242
  const cx = nameEnd - widthOf(model.step.name) - 10;
234
243
  out.push(`<circle cx="${cx.toFixed(1)}" cy="${y - 4}" r="4.5" fill="none" stroke="${accent}" stroke-width="1.8" stroke-dasharray="18 10.3">`
235
- + (animate.breath ? `<animateTransform attributeName="transform" type="rotate" values="0 ${cx.toFixed(1)} ${y - 4};360 ${cx.toFixed(1)} ${y - 4}" dur="${TIMINGS.spin * 8}ms" repeatCount="indefinite"/>` : "")
244
+ + (animate.breath ? `<animateTransform attributeName="transform" type="rotate" values="0 ${cx.toFixed(1)} ${y - 4};360 ${cx.toFixed(1)} ${y - 4}" ${loop(TIMINGS.spin * 8, 0.5)}/>` : "")
236
245
  + "</circle>");
237
246
  right = width - cx + 10;
238
247
  }
@@ -252,7 +261,7 @@ const detailRowSvg = (model, accent, animate) => {
252
261
  const cells = barCells(total > 0 ? done / total : 0, 8);
253
262
  cells.forEach(({ glyph, color }, index) => out.push(`<rect x="${(x + index * 9).toFixed(1)}" y="${y - 10}" width="7" height="10" rx="1.5" fill="${glyph === "█" ? color : C.empty}"/>`));
254
263
  const head = cells.findLastIndex(({ glyph }) => glyph === "█");
255
- if (head >= 0 && animate.sweep) out.push(`<rect x="${(x + head * 9).toFixed(1)}" y="${y - 10}" width="7" height="10" rx="1.5" fill="#ffffff" opacity="0"><animate attributeName="opacity" values="0;0.6;0" dur="${TIMINGS.breath}ms" repeatCount="indefinite"/></rect>`);
264
+ if (head >= 0 && animate.sweep) out.push(`<rect x="${(x + head * 9).toFixed(1)}" y="${y - 10}" width="7" height="10" rx="1.5" fill="#ffffff" opacity="0"><animate attributeName="opacity" values="0;0.6;0" ${loop(TIMINGS.breath, BREATH_PHASE)}/></rect>`);
256
265
  x += 8 * 9 + 4;
257
266
  }
258
267
  const pieces = [];
@@ -389,13 +398,13 @@ export const composeSvgCard = (model, { motion = "full", accent = "#5fd7ff" } =
389
398
  const edge = borderColor(model, accent);
390
399
  const perimeter = 1000;
391
400
  const defs = animate.sweep
392
- ? `<defs><linearGradient id="lit-sweep" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="0.5" stop-color="#fff" stop-opacity="0.55"/><stop offset="1" stop-color="#fff" stop-opacity="0"/><animateTransform attributeName="gradientTransform" type="translate" values="-1.2 0;1.2 0" dur="${TIMINGS.sweep}ms" repeatCount="indefinite"/></linearGradient></defs>`
401
+ ? `<defs><linearGradient id="lit-sweep" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="0.5" stop-color="#fff" stop-opacity="0.55"/><stop offset="1" stop-color="#fff" stop-opacity="0"/><animateTransform attributeName="gradientTransform" type="translate" values="-1.2 0;1.2 0" ${loop(TIMINGS.sweep, 0.5)}/></linearGradient></defs>`
393
402
  : "";
394
403
  const frame = `<rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="14" fill="${C.bg}" stroke="${edge}" stroke-width="1.5"${model.idle ? ' stroke-opacity="0.8"' : ""}>`
395
- + (animate.breath && !model.warnings.length ? `<animate attributeName="stroke-opacity" values="0.45;1;0.45" dur="${TIMINGS.breath}ms" repeatCount="indefinite"/>` : "")
404
+ + (animate.breath && !model.warnings.length ? `<animate attributeName="stroke-opacity" values="0.45;1;0.45" ${loop(TIMINGS.breath, BREATH_PHASE)}/>` : "")
396
405
  + "</rect>";
397
406
  const comet = animate.comet
398
- ? `<rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="14" fill="none" stroke="url(#lit-comet)" stroke-width="2.5" stroke-linecap="round" pathLength="${perimeter}" stroke-dasharray="70 ${perimeter - 70}"><animate attributeName="stroke-dashoffset" values="0;-${perimeter}" dur="${TIMINGS.comet * 2}ms" repeatCount="indefinite"/></rect>`
407
+ ? `<rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="14" fill="none" stroke="url(#lit-comet)" stroke-width="2.5" stroke-linecap="round" pathLength="${perimeter}" stroke-dasharray="70 ${perimeter - 70}"><animate attributeName="stroke-dashoffset" values="0;-${perimeter}" ${loop(TIMINGS.comet * 2, 0.5)}/></rect>`
399
408
  + `<defs><linearGradient id="lit-comet" x1="0" x2="1" y1="0" y2="1"><stop offset="0" stop-color="${C.orange}"/><stop offset="0.6" stop-color="${C.pink}"/><stop offset="1" stop-color="#ffffff"/></linearGradient></defs>`
400
409
  : "";
401
410
  const body = [