diff --git a/system/public/kidlisp.com/index.html b/system/public/kidlisp.com/index.html
--- a/system/public/kidlisp.com/index.html
+++ b/system/public/kidlisp.com/index.html
@@ -321,20 +321,7 @@ body.stage-mode:not(.device-mode) .monaco-editor .margin {
background: transparent !important;
}
- /* Cycling color drop shadow on editor text for readability over animation */
- body.stage-mode:not(.device-mode) .monaco-editor .view-lines span {
- text-shadow: 1px 1px 2px var(--stage-shadow-color, rgba(0,0,0,0.8));
- animation: stage-shadow-cycle 6s linear infinite;
- }
- @keyframes stage-shadow-cycle {
- 0% { --stage-shadow-color: rgba(0, 0, 0, 0.8); }
- 16% { --stage-shadow-color: rgba(255, 255, 255, 0.9); }
- 33% { --stage-shadow-color: rgba(0, 200, 255, 0.8); }
- 50% { --stage-shadow-color: rgba(255, 0, 180, 0.8); }
- 66% { --stage-shadow-color: rgba(255, 220, 0, 0.8); }
- 83% { --stage-shadow-color: rgba(0, 255, 120, 0.8); }
- 100% { --stage-shadow-color: rgba(0, 0, 0, 0.8); }
- }
+ /* Stage mode text shadows are applied per-token via JS (dynamic contrast shadows) */
/* Hide line numbers in stage mode */
body.stage-mode:not(.device-mode) .monaco-editor .margin {
diff --git a/system/public/kidlisp.com/js/monaco-kidlisp-highlighting.mjs b/system/public/kidlisp.com/js/monaco-kidlisp-highlighting.mjs
--- a/system/public/kidlisp.com/js/monaco-kidlisp-highlighting.mjs
+++ b/system/public/kidlisp.com/js/monaco-kidlisp-highlighting.mjs
@@ -313,7 +313,13 @@ // Add text-shadow for dark colors in dark mode
const shadow = this._getShadowForColor(cssColor);
const shadowStyle = shadow ? ` text-shadow: ${shadow};` : '';
- style.textContent = `.monaco-editor .view-line > span > span.${cssClass} { color: ${cssColor}; font-weight: bold;${shadowStyle} }`;
+ // Stage mode always gets a dynamic contrast shadow for readability over animation
+ const stageShadow = this._getStageShadowForColor(cssColor);
+ const stageRule = stageShadow
+ ? ` body.stage-mode:not(.device-mode) .monaco-editor .view-line > span > span.${cssClass} { text-shadow: ${stageShadow}; }`
+ : '';
+
+ style.textContent = `.monaco-editor .view-line > span > span.${cssClass} { color: ${cssColor}; font-weight: bold;${shadowStyle} }${stageRule}`;
document.head.appendChild(style);
}
@@ -361,6 +367,98 @@ if (luminance < 50) {
return '1px 1px 0px rgba(220, 220, 220, 0.8)';
} else if (luminance < 100) {
return '1px 1px 0px rgba(200, 200, 200, 0.6)';
+ }
+
+ return null;
+ }
+
+ /**
+ * Get dynamic contrast shadow for stage mode (algorithm from disk.mjs)
+ * Always returns a shadow — ensures readability over any background animation.
+ * @private
+ */
+ _getStageShadowForColor(colorStr) {
+ let rgb = this._parseColorToRGB(colorStr);
+ if (!rgb) return '1px 1px 2px rgba(0,0,0,0.8)'; // fallback dark shadow
+
+ const [r, g, b] = rgb;
+
+ // Very dark colors (near black) get bright shadow
+ if (r <= 24 && g <= 24 && b <= 24) {
+ return '1px 1px 2px rgba(220,220,220,0.9)';
+ }
+
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
+
+ // Bright colors get dark shadow
+ if (luminance > 140) {
+ return '1px 1px 2px rgba(30,20,50,0.9)';
+ }
+
+ // Red channel special case
+ if (r >= 0 && g === 0 && b === 0) {
+ const sr = Math.max(32, Math.round(r * 0.4));
+ return `1px 1px 2px rgb(${sr},0,0)`;
+ }
+
+ // Green channel special case
+ if (r === 0 && g >= 0 && b === 0) {
+ const sg = Math.max(32, Math.round(g * 0.4));
+ return `1px 1px 2px rgb(0,${sg},0)`;
+ }
+
+ // Blue/cyan channel special case
+ if (r === 0 && b >= 0 && g >= 0) {
+ const expectedG = Math.round(b * 0.75);
+ if (Math.abs(g - expectedG) <= 1) {
+ const sg = Math.max(24, Math.round(g * 0.4));
+ const sb = Math.max(32, Math.round(b * 0.4));
+ return `1px 1px 2px rgb(0,${sg},${sb})`;
+ }
+ }
+
+ // Dark colors — lighten shadow (mix 85% with white)
+ if (luminance < 128) {
+ const factor = 0.85;
+ const sr = Math.round(r + (255 - r) * factor);
+ const sg = Math.round(g + (255 - g) * factor);
+ const sb = Math.round(b + (255 - b) * factor);
+ return `1px 1px 2px rgb(${sr},${sg},${sb})`;
+ }
+
+ // Light colors — darken shadow (60% darker)
+ const factor = 0.6;
+ const sr = Math.round(r * (1 - factor));
+ const sg = Math.round(g * (1 - factor));
+ const sb = Math.round(b * (1 - factor));
+ return `1px 1px 2px rgb(${sr},${sg},${sb})`;
+ }
+
+ /**
+ * Parse a CSS color string to [r, g, b]
+ * @private
+ */
+ _parseColorToRGB(colorStr) {
+ if (!colorStr) return null;
+
+ // rgb(...) format
+ if (colorStr.startsWith('rgb')) {
+ const parts = colorStr.replace(/rgb\(|\)/g, '').split(',').map(v => parseInt(v.trim()));
+ return parts.length >= 3 ? parts : null;
+ }
+
+ // Named color — look up in cssColorMap
+ const lower = colorStr.toLowerCase();
+ if (this.cssColorMap[lower]) return this.cssColorMap[lower];
+
+ // Hex format
+ if (colorStr.startsWith('#')) {
+ const hex = colorStr.slice(1);
+ if (hex.length === 3) {
+ return [parseInt(hex[0]+hex[0],16), parseInt(hex[1]+hex[1],16), parseInt(hex[2]+hex[2],16)];
+ } else if (hex.length >= 6) {
+ return [parseInt(hex.slice(0,2),16), parseInt(hex.slice(2,4),16), parseInt(hex.slice(4,6),16)];
+ }
}
return null;