PageSourceSearch

https://ahuchala.com/hodge/scripts.js

js ahuchala.com collected 2026-10-04 21:52:02 UTC 44,802 bytes, 922 lines download raw bytes

1// hodge/scripts.js
2//
3// Cross-cutting init logic shared by all 6 Hodge calculators. Called once from
4// pages/hodge.js after every individual calculator's init() has run.
5//
6// Responsibilities:
7//   1. Calculator toggle buttons — show/hide container divs, mark active button
8//   2. Label alignment — equalize .input-row label widths within each container
9//   3. Preset button highlighting — keep preset buttons highlighted when active
10//   4. Dynamic diamond description — update the "Hodge diamond for a smooth ..."
11//      caption above each diamond in real time as inputs change
12//   5. Coordinate annotation — stamp data-i / data-j attributes and tooltip titles
13//      onto every diamond cell (used by the Twisted calculator's click handler)
14//   6. Zero-entry coloring — add .is-zero class to cells whose value is "0"
15//      when the "Alternate color for zero entries" toggle is on
16//   7. Hide-top-half toggle — add .is-top-half to rows above the middle row
17//   8. Scroll wheel guard — prevent number inputs from changing on wheel scroll
18
19export function init() {
20    // --- Toggle Between Calculators ---
21    const toggleCompleteIntersection = document.getElementById("toggle-complete-intersection");
22    const toggleAbelianVariety = document.getElementById("toggle-abelian-variety");
23    const toggleGrassmannian = document.getElementById("toggle-grassmannian");
24    const toggleFlag = document.getElementById("toggle-flag");
25    const toggleTwisted = document.getElementById("toggle-twisted");
26    const toggleProductGrassmannian = document.getElementById("toggle-product-grassmannian");
27
28    const completeIntersectionContainer = document.getElementById("complete-intersection-container");
29    const abelianVarietyContainer = document.getElementById("abelian-variety-container");
30    const grassmannianContainer = document.getElementById("grassmannian-container");
31    const flagContainer = document.getElementById("flag-container");
32    const twistedContainer = document.getElementById("twisted-container");
33    const productGrassmannianContainer = document.getElementById("product-grassmannian-container");
34
35    const setActiveToggle = (active, ...inactives) => {
36      active.classList.add("pressed");
37      inactives.forEach(el => el.classList.remove("pressed"));
38    };
39
40    // Aligns all .input-row labels within a container to the same width so
41    // sliders line up. Must be called while the container is visible.
42    function alignLabelsInContainer(container) {
43      if (!container) return;
44      const labels = Array.from(container.querySelectorAll(".input-row label"));
45      labels.forEach(l => { l.style.minWidth = ""; });
46      const maxW = labels.reduce((m, l) => Math.max(m, l.scrollWidth), 0);
47      labels.forEach(l => { l.style.minWidth = maxW + "px"; });
48    }
49
50    const showContainer = (container) => {
51      completeIntersectionContainer.style.display = "none";
52      abelianVarietyContainer.style.display = "none";
53      grassmannianContainer.style.display = "none";
54      flagContainer.style.display = "none";
55      twistedContainer.style.display = "none";
56      if (productGrassmannianContainer) productGrassmannianContainer.style.display = "none";
57      container.style.display = "block";
58      // Align labels now that the container is visible (scrollWidth is valid).
59      requestAnimationFrame(() => alignLabelsInContainer(container));
60      // If the container has a registered update function, call it.
61      if (typeof container.updateCalculator === "function") {
62          container.updateCalculator();
63      }
64      requestAnimationFrame(updateHodgeDiamondDescription);
65    };
66
67    toggleCompleteIntersection.addEventListener("click", () => {
68      showContainer(completeIntersectionContainer);
69      setActiveToggle(toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleFlag, toggleTwisted, toggleProductGrassmannian);
70    });
71    toggleAbelianVariety.addEventListener("click", () => {
72      showContainer(abelianVarietyContainer);
73      setActiveToggle(toggleAbelianVariety, toggleCompleteIntersection, toggleGrassmannian, toggleFlag, toggleTwisted, toggleProductGrassmannian);
74    });
75    toggleGrassmannian.addEventListener("click", () => {
76      showContainer(grassmannianContainer);
77      setActiveToggle(toggleGrassmannian, toggleCompleteIntersection, toggleAbelianVariety, toggleFlag, toggleTwisted, toggleProductGrassmannian);
78    });
79    toggleFlag.addEventListener("click", () => {
80      showContainer(flagContainer);
81      setActiveToggle(toggleFlag, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleTwisted, toggleProductGrassmannian);
82    });
83    toggleTwisted.addEventListener("click", () => {
84      showContainer(twistedContainer);
85      setActiveToggle(toggleTwisted, toggleFlag, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleProductGrassmannian);
86    });
87    if (toggleProductGrassmannian) {
88      toggleProductGrassmannian.addEventListener("click", () => {
89        showContainer(productGrassmannianContainer);
90        setActiveToggle(toggleProductGrassmannian, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleFlag, toggleTwisted);
91      });
92    }
93
94    // Show more / show less toggle for advanced calculators
95    const toggleMenu = document.getElementById("toggle-menu");
96    const toggleAdvancedBtn = document.getElementById("toggle-advanced");
97    const advancedContainers = [abelianVarietyContainer, flagContainer, productGrassmannianContainer];
98    const advancedToggles = [toggleAbelianVariety, toggleFlag, toggleProductGrassmannian];
99    if (toggleAdvancedBtn && toggleMenu) {
100      toggleAdvancedBtn.addEventListener("click", () => {
101        const expanding = !toggleMenu.classList.contains("show-advanced");
102        toggleMenu.classList.toggle("show-advanced", expanding);
103        toggleAdvancedBtn.setAttribute("aria-expanded", expanding);
104        toggleAdvancedBtn.textContent = expanding ? "▲ Show less" : "▼ Show more";
105        // If collapsing while an advanced calculator is visible, revert to CI
106        if (!expanding) {
107          const advancedVisible = advancedContainers.some(c => c && c.style.display !== "none");
108          if (advancedVisible) {
109            showContainer(completeIntersectionContainer);
110            setActiveToggle(toggleCompleteIntersection, ...advancedToggles, toggleGrassmannian, toggleTwisted);
111          }
112        }
113      });
114    }
115
116    // Align the default-visible container on first paint.
117    requestAnimationFrame(() => alignLabelsInContainer(completeIntersectionContainer));
118
119    // --- Preset Button State Updates ---
120    const getDegrees = (container) =>
121      [...(container?.children || [])].map(row => parseInt(row.querySelector("input")?.value || 0, 10));
122
123    const updateAllPressedButtons = () => {
124      // Complete Intersection (P^n)
125      const nValue = parseInt(document.getElementById("n-value")?.value || 0, 10);
126      const degreeToggles = document.getElementById("degree-toggles");
127      const ciDegrees = getDegrees(degreeToggles);
128      document.querySelectorAll("#complete-intersection-container .preset-button").forEach(btn => {
129        const n = parseInt(btn.dataset.n, 10);
130        const degrees = btn.dataset.degrees?.split(",").map(Number) ?? [];
131        const match = n === nValue && degrees.length === ciDegrees.length &&
132          degrees.every((d, i) => d === ciDegrees[i]);
133        btn.classList.toggle("pressed", match);
134      });
135
136      // Abelian Variety
137      const gValue = parseInt(document.getElementById("g-value")?.value || 0, 10);
138      document.querySelectorAll("#abelian-variety-container .preset-button").forEach(btn => {
139        btn.classList.toggle("pressed", parseInt(btn.dataset.g, 10) === gValue);
140      });
141
142      // Grassmannian — apply Gr(k,n)≅Gr(n-k,n) symmetry reduction before comparing
143      const grN = parseInt(document.getElementById("n-value-grassmannian")?.value || 0, 10);
144      const grKRaw = parseInt(document.getElementById("k-value-grassmannian")?.value || 0, 10);
145      const grK = Math.min(grKRaw, grN - grKRaw);  // canonical form
146      const grR = parseInt(document.getElementById("r-value-grassmannian")?.value || 0, 10);
147      const grDegToggles = document.getElementById("degree-toggles-grassmannian");
148      const grDegrees = getDegrees(grDegToggles);
149      document.querySelectorAll("#grassmannian-container .preset-button").forEach(btn => {
150        const n = parseInt(btn.dataset.n, 10);
151        const kRaw = parseInt(btn.dataset.k, 10);
152        const k = Math.min(kRaw, n - kRaw);  // canonical form
153        const r = parseInt(btn.dataset.r, 10);
154        const degrees = btn.dataset.degrees?.split(",").map(Number) ?? [];
155        const match = n === grN && k === grK && r === grR &&
156          degrees.length === grDegrees.length && degrees.every((d, i) => d === grDegrees[i]);
157        btn.classList.toggle("pressed", match);
158      });
159
160      // Product Grassmannian
161      const prodR = parseInt(document.getElementById("r-value-product")?.value || 0, 10);
162      const prodDegToggles = document.getElementById("degree-toggles-product");
163      // Each degree row has multiple inputs (one per factor) — read them all as a flat array
164      const prodDegrees = [...(prodDegToggles?.querySelectorAll("input") || [])].map(inp => parseInt(inp.value || 0, 10));
165      const factorRows = [...(document.getElementById("factor-inputs-product")?.children || [])];
166      const currentFactors = factorRows.flatMap(row => [
167        parseInt(row.querySelector(".factor-k-value")?.value || 0, 10),
168        parseInt(row.querySelector(".factor-n-value")?.value || 0, 10),
169      ]);
170      document.querySelectorAll("#product-grassmannian-container .preset-button[data-factors]").forEach(btn => {
171        const btnFactors = btn.dataset.factors?.split(",").map(Number) ?? [];
172        const btnR = parseInt(btn.dataset.r, 10);
173        const btnDegrees = btn.dataset.degrees ? btn.dataset.degrees.split(",").map(Number) : [];
174        const match = btnR === prodR &&
175          btnFactors.length === currentFactors.length &&
176          btnFactors.every((f, i) => f === currentFactors[i]) &&
177          btnDegrees.length === prodDegrees.length &&
178          btnDegrees.every((d, i) => d === prodDegrees[i]);
179        btn.classList.toggle("pressed", match);
180      });
181    };
182
183    const delayedUpdate = () => setTimeout(updateAllPressedButtons, 0);
184    const attachListeners = (selector, eventType) => {
185      document.querySelectorAll(selector).forEach(el => el.addEventListener(eventType, delayedUpdate));
186    };
187
188    attachListeners("#n-slider, #k-slider, #g-slider", "input");
189    attachListeners("#n-value, #k-value, #g-value", "input");
190    attachListeners(".preset-button", "click");
191    document.getElementById("degree-toggles")?.addEventListener("input", delayedUpdate);
192    attachListeners("#n-value-grassmannian, #k-value-grassmannian, #r-value-grassmannian, #n-slider-grassmannian, #k-slider-grassmannian, #r-slider-grassmannian", "input");
193    document.getElementById("degree-toggles-grassmannian")?.addEventListener("input", delayedUpdate);
194    document.getElementById("factor-inputs-product")?.addEventListener("input", delayedUpdate);
195    document.getElementById("r-value-product")?.addEventListener("input", delayedUpdate);
196    document.getElementById("r-slider-product")?.addEventListener("input", delayedUpdate);
197    document.getElementById("degree-toggles-product")?.addEventListener("input", delayedUpdate);
198
199    updateAllPressedButtons();
200  
201    // --- Unified Dynamic Hodge Diamond Description ---
202
203    // Returns "\mathbb{P}^{n-1}" when Gr(k,n) is projective space (k=1 or k=n-1),
204    // otherwise returns "\mathrm{Gr}(k,n)".
205    function grOrPn(k, n) {
206      const ki = parseInt(k, 10), ni = parseInt(n, 10);
207      if (ki === 1 || ki === ni - 1) return `\\mathbb{P}^{${ni - 1}}`;
208      return `\\mathrm{Gr}(${k},${n})`;
209    }
210
211    function updateHodgeDiamondDescription() {
212      let descriptionText = "";
213
214      // ----- Complete intersections in CP^n -----
215      if (completeIntersectionContainer && completeIntersectionContainer.style.display !== "none") {
216        const nValueEl = document.getElementById("n-value");
217        const degreeToggles = document.getElementById("degree-toggles");
218        if (!nValueEl || !degreeToggles) return;
219
220        const nRaw = nValueEl.value.trim();
221        const degreeInputs = Array.from(degreeToggles.querySelectorAll("input.hodge-input"));
222
223        if (nRaw === "" || isNaN(parseInt(nRaw, 10))) {
224          descriptionText = "Hodge diamond for a complete intersection in projective space";
225        } else {
226          const n = parseInt(nRaw, 10);
227          const degreesRaw = degreeInputs.map(inp => (inp.value ?? "").trim());
228          const anyBadDegree = degreesRaw.some(d => d === "" || isNaN(parseInt(d, 10)));
229
230          if (degreeInputs.length === 0 || anyBadDegree) {
231            descriptionText = `Hodge diamond for a smooth complete intersection in \\(\\mathbb{P}^{${n}}\\)`;
232          } else if (degreeInputs.length === 1) {
233            const degree = parseInt(degreesRaw[0], 10);
234            descriptionText =
235              `Hodge diamond for a smooth hypersurface of degree ${degree} in ` +
236              `\\(\\mathbb{P}^{${n}}\\)`;
237          } else {
238            const degrees = degreesRaw.map(d => parseInt(d, 10));
239            const multidegreeStr = "(" + degrees.join(", ") + ")";
240            descriptionText =
241              `Hodge diamond for a smooth complete intersection of multidegree ${multidegreeStr} ` +
242              `in \\(\\mathbb{P}^{${n}}\\)`;
243          }
244          const sElCI = document.getElementById("s-value");
245          const sCI = sElCI ? (parseInt(sElCI.value, 10) || 0) : 0;
246          if (sCI > 0) {
247            descriptionText += `, blown up at \\(${sCI}\\) point${sCI === 1 ? "" : "s"} in general position`;
248          }
249        }
250      }
251
252      // For Grassmannians:
253      else if (grassmannianContainer && grassmannianContainer.style.display !== "none") {
254        const nValueEl = document.getElementById("n-value-grassmannian");
255        const kValueEl = document.getElementById("k-value-grassmannian");
256        const rValueEl = document.getElementById("r-value-grassmannian");
257        const degreeToggles = document.getElementById("degree-toggles-grassmannian");
258        if (!nValueEl || !kValueEl || !rValueEl || !degreeToggles) return;
259        const n = nValueEl.value;
260        const k = kValueEl.value;
261        const r = parseInt(rValueEl.value, 10);
262        const degreeInputs = Array.from(degreeToggles.querySelectorAll("input.hodge-input"));
263        const ambientGr = grOrPn(k, n);
264        if (degreeInputs.length === 0 || r === 0) {
265          descriptionText = `Hodge diamond for \\(${ambientGr}\\)`;
266        } else if (degreeInputs.length === 1) {
267          const degree = degreeInputs[0].value;
268          descriptionText = `Hodge diamond for a smooth hypersurface of degree ${degree} in \\(${ambientGr}\\)`;
269        } else {
270          const degrees = degreeInputs.map(input => input.value);
271          const multidegreeStr = "(" + degrees.join(", ") + ")";
272          descriptionText = `Hodge diamond for a smooth complete intersection of multidegree ${multidegreeStr} in \\(${ambientGr}\\)`;
273        }
274        const sElGr = document.getElementById("s-value-grassmannian");
275        const sGr = sElGr ? (parseInt(sElGr.value, 10) || 0) : 0;
276        if (sGr > 0) {
277          descriptionText += `, blown up at \\(${sGr}\\) point${sGr === 1 ? "" : "s"} in general position`;
278        }
279      }
280      // For Abelian Varieties:
281      else if (abelianVarietyContainer && abelianVarietyContainer.style.display !== "none") {
282        const gValueEl = document.getElementById("g-value");
283        if (!gValueEl) return;
284        const g = gValueEl.value;
285        descriptionText = `Hodge diamond for an abelian variety of genus \\(g=${g}\\)`;
286      }
287      // For Flag Varieties:
288      else if (flagContainer && flagContainer.style.display !== "none") {
289        const dimsInput = document.getElementById("dims-input");
290        const rValueFlag = document.getElementById("r-value-flag");
291        const degreeToggles = document.getElementById("degree-toggles-flag");
292        if (!dimsInput || !rValueFlag || !degreeToggles) return;
293
294        const dimsList = dimsInput.value
295          .split(",")
296          .map(s => s.trim())
297          .filter(s => s !== "");
298        const dimsStr = dimsList.join(", ");
299
300        const r = parseInt(rValueFlag.value, 10);
301        const degreeInputs = Array.from(
302          degreeToggles.querySelectorAll("input.hodge-input")
303        );
304
305        if (!Number.isFinite(r) || r === 0 || degreeInputs.length === 0) {
306          descriptionText = `Hodge diamond for a partial flag of dimensions [${dimsStr}]`;
307        } else if (degreeInputs.length === 1) {
308          const block = (degreeInputs[0].value || "").trim();
309          descriptionText =
310            `Hodge diamond for a hypersurface of multidegree [${block}] ` +
311            `in a partial flag of dimensions [${dimsStr}]`;
312        } else {
313          const blocks = degreeInputs.map(inp => {
314            const txt = (inp.value || "").trim();
315            return `[${txt}]`;
316          });
317          descriptionText =
318            `Hodge diamond for a complete intersection (r=${r}) ` +
319            `in a partial flag of dimensions [${dimsStr}] ` +
320            `with multidegrees ${blocks.join(", ")}`;
321        }
322      }
323
324      // For Product of Grassmannians:
325      else if (productGrassmannianContainer && productGrassmannianContainer.style.display !== "none") {
326        const r = parseInt(document.getElementById("r-value-product")?.value, 10);
327        const factorRows = Array.from(
328          document.querySelectorAll("#factor-inputs-product .factor-row-product")
329        );
330        const factorStrs = factorRows.map(row => {
331          const k = row.querySelector(".factor-k-value")?.value;
332          const n = row.querySelector(".factor-n-value")?.value;
333          return grOrPn(k, n);
334        });
335        const productStr = factorStrs.join("\\times ");
336        const degInputs = Array.from(
337          document.getElementById("degree-toggles-product")
338            ?.querySelectorAll(".degree-toggle") ?? []
339        );
340        if (!r || degInputs.length === 0) {
341          descriptionText = `Hodge diamond for \\(${productStr}\\)`;
342        } else {
343          const degs = degInputs.map(row => {
344            const vals = Array.from(row.querySelectorAll(".hodge-input")).map(i => i.value ?? "?");
345            return `(${vals.join(",")})`;
346          });
347          descriptionText =
348            `Hodge diamond for a smooth complete intersection of multidegree ${degs.join(", ")} ` +
349            `in \\(${productStr}\\)`;
350        }
351      }
352      // For Twisted Hodge Numbers:
353      else if (twistedContainer && twistedContainer.style.display !== "none") {
354        const nValueEl = document.getElementById("n-value-twisted");
355        const kValueEl = document.getElementById("k-value-twisted");
356        const tValueEl = document.getElementById("t-value-twisted");
357        if (!nValueEl || !kValueEl || !tValueEl) return;
358        const n = nValueEl.value;
359        const k = kValueEl.value;
360        const t = parseInt(tValueEl.value, 10);
361        if (t === 0) {
362          descriptionText = `Hodge diamond for \\(\\text{Gr}(${k},${n})\\)`;
363        } else {
364          descriptionText = `Hodge diamond for \\(\\Omega_{\\text{Gr}(${k},${n})}\\otimes\\mathcal O_{\\text{Gr}(${k},${n})}(${t})\\)`;
365        }
366      }
367      else {
368        descriptionText = "";
369      }
370  
371      document.querySelectorAll(".hodge-diamond-description").forEach(el => {
372        el.innerHTML = descriptionText;
373        if (window.MathJax && MathJax.typesetPromise) {
374          MathJax.typesetPromise([el]).catch(err => console.error("MathJax typeset failed: " + err.message));
375        }
376      });
377    }
378  
379    // --- Attach Event Listeners for Description Updates ---
380    // (unchanged listeners omitted for brevity – keep your existing ones)
381    const nSlider = document.getElementById("n-slider");
382    const nValueEl = document.getElementById("n-value");
383    const kSlider = document.getElementById("k-slider");
384    const kValueEl = document.getElementById("k-value");
385    const degreeTogglesEl = document.getElementById("degree-toggles");
386    if (nSlider) {
387      nSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
388      nSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
389    }
390    if (nValueEl) nValueEl.addEventListener("input", updateHodgeDiamondDescription);
391    if (kSlider) {
392      kSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
393      kSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
394    }
395    if (kValueEl) kValueEl.addEventListener("input", updateHodgeDiamondDescription);
396    if (degreeTogglesEl) degreeTogglesEl.addEventListener("input", updateHodgeDiamondDescription);
397  
398    const nSliderG = document.getElementById("n-slider-grassmannian");
399    const nValueG = document.getElementById("n-value-grassmannian");
400    const kSliderG = document.getElementById("k-slider-grassmannian");
401    const kValueG = document.getElementById("k-value-grassmannian");
402    const rSliderG = document.getElementById("r-slider-grassmannian");
403    const rValueG = document.getElementById("r-value-grassmannian");
404    const degreeTogglesG = document.getElementById("degree-toggles-grassmannian");
405    if (nSliderG) {
406      nSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
407      nSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
408    }
409    if (nValueG) nValueG.addEventListener("input", updateHodgeDiamondDescription);
410    if (kSliderG) {
411      kSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
412      kSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
413    }
414    if (kValueG) kValueG.addEventListener("input", updateHodgeDiamondDescription);
415    if (rSliderG) {
416      rSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
417      rSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
418    }
419    if (rValueG) rValueG.addEventListener("input", updateHodgeDiamondDescription);
420    if (degreeTogglesG) degreeTogglesG.addEventListener("input", updateHodgeDiamondDescription);
421
422    const sSliderCI = document.getElementById("s-slider");
423    const sValueCI = document.getElementById("s-value");
424    if (sSliderCI) {
425      sSliderCI.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
426      sSliderCI.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
427    }
428    if (sValueCI) sValueCI.addEventListener("input", updateHodgeDiamondDescription);
429
430    const sSliderGr = document.getElementById("s-slider-grassmannian");
431    const sValueGr = document.getElementById("s-value-grassmannian");
432    if (sSliderGr) {
433      sSliderGr.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
434      sSliderGr.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
435    }
436    if (sValueGr) sValueGr.addEventListener("input", updateHodgeDiamondDescription);
437
438    const gSlider = document.getElementById("g-slider");
439    const gValueEl = document.getElementById("g-value");
440    if (gSlider) {
441      gSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
442      gSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
443    }
444    if (gValueEl) gValueEl.addEventListener("input", updateHodgeDiamondDescription);
445  
446
447    const nSliderT = document.getElementById("n-slider-twisted");
448    const nValueT = document.getElementById("n-value-twisted");
449    const kSliderT = document.getElementById("k-slider-twisted");
450    const kValueT = document.getElementById("k-value-twisted");
451    const tSliderT = document.getElementById("t-slider-twisted");
452    const tValueT = document.getElementById("t-value-twisted");
453    if (nSliderT) {
454      nSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
455      nSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
456    }
457    if (nValueT) nValueT.addEventListener("input", updateHodgeDiamondDescription);
458    if (kSliderT) {
459      kSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
460      kSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
461    }
462    if (kValueT) kValueT.addEventListener("input", updateHodgeDiamondDescription);
463    if (tSliderT) {
464      tSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
465      tSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
466    }
467    if (tValueT) tValueT.addEventListener("input", updateHodgeDiamondDescription);
468
469    // Product Grassmannian inputs — r slider/textbox
470    const rSliderProduct = document.getElementById("r-slider-product");
471    const rValueProduct  = document.getElementById("r-value-product");
472    if (rSliderProduct) {
473      rSliderProduct.addEventListener("input",  () => requestAnimationFrame(updateHodgeDiamondDescription));
474      rSliderProduct.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription));
475    }
476    if (rValueProduct) rValueProduct.addEventListener("input", updateHodgeDiamondDescription);
477    // Factor rows and degree toggles are dynamically created; use event delegation
478    const factorInputsProduct = document.getElementById("factor-inputs-product");
479    if (factorInputsProduct) {
480      factorInputsProduct.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription));
481    }
482    const degTogglesProduct = document.getElementById("degree-toggles-product");
483    if (degTogglesProduct) degTogglesProduct.addEventListener("input", updateHodgeDiamondDescription);
484    // Re-describe after Add/Remove Factor buttons
485    ["add-factor-product","remove-factor-product"].forEach(id => {
486      const el = document.getElementById(id);
487      if (el) el.addEventListener("click", () => requestAnimationFrame(updateHodgeDiamondDescription));
488    });
489
490    const presetButtons = document.querySelectorAll(".preset-button");
491    presetButtons.forEach(button => {
492      button.addEventListener("click", () => {
493        requestAnimationFrame(updateHodgeDiamondDescription);
494      });
495    });
496  
497    // ----- Zero-color toggle  -----
498    const zeroToggle = document.getElementById("zero-color-toggle");
499
500    // All diamond roots we render into
501    const diamondRoots = [
502      document.getElementById("diamond-container"),
503      document.getElementById("diamond-container-abelian"),
504      document.getElementById("diamond-container-grassmannian"),
505      document.getElementById("diamond-container-flag"),
506      document.getElementById("diamond-container-twisted"),
507      document.getElementById("diamond-container-product"),
508    ].filter(Boolean);
509
510    // Stamp data-i, data-j, and a hover tooltip onto every cell of the given diamond root.
511    //
512    // Diamond convention: the diamond has 2N+1 rows. Row 0 (top) is h^{N,N}; row 2N
513    // (bottom) is h^{0,0}. In each row, the leftmost cell has the largest i value.
514    //
515    // For a row at DOM index idxTop (0 = top of diamond):
516    //   - "antidiagonal index" r = i + j = (2N) - idxTop  (bottom row → r=0, top row → r=2N)
517    //   - i ranges from iMax = min(r, N) down to iMin = max(0, r-N), left to right
518    //   - j = r - i
519    //
520    // These coordinates are consumed by the Twisted calculator's click handler to show
521    // which Schubert partitions contribute to the clicked h^{i,j} value.
522    function annotateDiamondCoords(root) {
523      if (!root) return;
524
525      const rows = Array.from(
526        root.querySelectorAll(".hodge-row, .diamond-row")
527      );
528      if (!rows.length) return;
529
530      const L = rows.length;
531      if (L % 2 === 0) return; // expect 2N+1 rows; bail if something is wrong
532      const N = (L - 1) / 2;  // half-dimension: diamond spans h^{p,q} with p,q in [0,N]
533
534      rows.forEach((row, idxTop) => {
535        const r = (L - 1) - idxTop;  // antidiagonal: bottom row → r=0, top row → r=2N
536
537        const spans = Array.from(
538          row.querySelectorAll(".diamond-value, .hodge-cell, span")
539        ).filter(el => el.textContent && el.textContent.trim() !== "");
540        if (!spans.length) return;
541
542        const iMin = Math.max(0, r - N);
543        const iMax = Math.min(r, N);
544
545        // Leftmost span has i = iMax; i decreases by 1 per step going right
546        spans.forEach((span, s) => {
547          const i = iMax - s;
548          const j = r - i;
549
550          if (i < 0 || j < 0 || i > N || j > N) return;
551
552          span.dataset.i = String(i);
553          span.dataset.j = String(j);
554          span.title = `(${i}, ${j})`;  // native hover tooltip
555        });
556      });
557    }
558
559
560    function annotateAllDiamondCoords() {
561      diamondRoots.forEach(annotateDiamondCoords);
562    }
563
564    // Mark any element whose visible text is exactly "0"
565    function labelZeros(root) {
566      if (!root) return;
567      const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
568      const toMark = new Set();
569      while (true) {
570        const n = walker.nextNode();
571        if (!n) break;
572        const txt = n.nodeValue && n.nodeValue.trim();
573        if (txt === "0") {
574          const host = n.parentElement;
575          if (host) toMark.add(host);
576        }
577      }
578      toMark.forEach(el => el.classList.add("is-zero"));
579    }
580
581    // Mark the top half (excluding the middle row) of each diamond
582    function labelTopHalf(root) {
583      if (!root) return;
584
585      const rows = Array.from(
586        root.querySelectorAll(".hodge-row, .diamond-row")
587      );
588      if (!rows.length) return;
589
590      // querySelectorAll returns DOM order (top→bottom), no position sort needed
591      const mid = Math.floor(rows.length / 2);
592
593      rows.forEach((row, idx) => {
594        if (idx < mid) {
595          row.classList.add("is-top-half");
596        } else {
597          row.classList.remove("is-top-half");
598        }
599      });
600    }
601
602    function relabelAllZeros() {
603      diamondRoots.forEach(labelZeros);
604    }
605
606    function relabelAllTopHalf() {
607      diamondRoots.forEach(labelTopHalf);
608    }
609
610    // Scroll #diamond-box so the diamond peak is centred in the visible area.
611    // Only activates when the diamond is wider than the box.
612    function centreDiamondScroll(root) {
613      const wrapper = root.closest('.diamond-scroll-wrapper');
614      if (!wrapper) return;
615      requestAnimationFrame(() => {
616        const overflow = wrapper.scrollWidth - wrapper.clientWidth;
617        wrapper.scrollLeft = overflow > 0 ? overflow / 2 : 0;
618      });
619    }
620
621    // Mutation observer: now also annotates coordinates
622    const observers = diamondRoots.map(root => {
623      const obs = new MutationObserver(() => {
624        annotateDiamondCoords(root);  // NEW
625        if (document.body.classList.contains("zero-alt-on")) labelZeros(root);
626        labelTopHalf(root);
627        centreDiamondScroll(root);
628      });
629      obs.observe(root, { childList: true, subtree: true });
630      return obs;
631    });
632
633    window.addEventListener('beforeunload', () =>
633 observers.forEach(o => o.disconnect()));
634
635    if (zeroToggle) {
636      zeroToggle.addEventListener("change", () => {
637        const on = zeroToggle.checked;
638        document.body.classList.toggle("zero-alt-on", on);
639        if (on) relabelAllZeros();
640      });
641    }
642
643    const hideTopHalfToggle = document.getElementById("hide-top-half-toggle");
644
645    if (hideTopHalfToggle) {
646      hideTopHalfToggle.addEventListener("change", () => {
647        const on = hideTopHalfToggle.checked;
648        document.body.classList.toggle("hide-top-half", on);
649        if (on) {
650          relabelAllTopHalf();
651        }
652      });
653    }
654
655    const showBlowupsToggle = document.getElementById("show-blowups-toggle");
656    if (showBlowupsToggle) {
657      showBlowupsToggle.addEventListener("change", () => {
658        document.body.classList.toggle("show-blowups", showBlowupsToggle.checked);
659      });
660    }
661
662    // Initial pass
663    annotateAllDiamondCoords();   // NEW
664
665    if (zeroToggle?.checked) {
666      document.body.classList.add("zero-alt-on");
667      relabelAllZeros();
668    }
669
670    if (hideTopHalfToggle?.checked) {
671      document.body.classList.add("hide-top-half");
672    }
673    relabelAllTopHalf();
674
675    if (showBlowupsToggle?.checked) {
676      document.body.classList.add("show-blowups");
677    }
678
679    updateHodgeDiamondDescription();
680
681    // --- URL State: encode current calculator state into query params ---
682    function encodeState() {
683        const params = new URLSearchParams();
684
685        let calc = 'ci';
686        if (abelianVarietyContainer.style.display !== 'none')          calc = 'abelian';
687        else if (grassmannianContainer.style.display !== 'none')       calc = 'grassmannian';
688        else if (flagContainer.style.display !== 'none')               calc = 'flag';
689        else if (twistedContainer.style.display !== 'none')            calc = 'twisted';
690        else if (productGrassmannianContainer?.style.display !== 'none') calc = 'product';
691        params.set('calc', calc);
692
693        if (calc === 'ci') {
694            params.set('n', document.getElementById('n-value').value);
695            const r = document.getElementById('r-value').value;
696            params.set('r', r);
697            if (parseInt(r) > 0) {
698                const degs = Array.from(document.querySelectorAll('#degree-toggles .hodge-input')).map(i => i.value);
699                if (degs.length) params.set('deg', degs.join(','));
700            }
701            const s = document.getElementById('s-value')?.value;
702            if (s && s !== '0') params.set('s', s);
703
704        } else if (calc === 'abelian') {
705            params.set('g', document.getElementById('g-value').value);
706
707        } else if (calc === 'grassmannian') {
708            params.set('n', document.getElementById('n-value-grassmannian').value);
709            params.set('k', document.getElementById('k-value-grassmannian').value);
710            const r = document.getElementById('r-value-grassmannian').value;
711            params.set('r', r);
712            if (parseInt(r) > 0) {
713                const degs = Array.from(document.querySelectorAll('#degree-toggles-grassmannian .hodge-input')).map(i => i.value);
714                if (degs.length) params.set('deg', degs.join(','));
715            }
716            const s = document.getElementById('s-value-grassmannian')?.value;
717            if (s && s !== '0') params.set('s', s);
718
719        } else if (calc === 'flag') {
720            params.set('dims', (document.getElementById('dims-input')?.value ?? '').replace(/\s/g, ''));
721            params.set('r', document.getElementById('r-value-flag')?.value ?? '0');
722            const flagInputs = document.querySelectorAll('#degree-toggles-flag .hodge-input');
723            if (flagInputs.length) {
724                params.set('md', Array.from(flagInputs).map(i => i.value.replace(/\s/g, '')).join('|'));
725            }
726
727        } else if (calc === 'twisted') {
728            params.set('n', document.getElementById('n-value-twisted').value);
729            params.set('k', document.getElementById('k-value-twisted').value);
730            params.set('t', document.getElementById('t-value-twisted').value);
731
732        } else if (calc === 'product') {
733            const factorRows = document.querySelectorAll('#factor-inputs-product .factor-row-product');
734            const pairs = Array.from(factorRows).map(row =>
735                row.querySelector('.factor-k-value').value + ',' + row.querySelector('.factor-n-value').value
736            );
737            params.set('factors', pairs.join(','));
738            const r = document.getElementById('r-value-product')?.value ?? '0';
739            params.set('r', r);
740            if (parseInt(r) > 0) {
741                const degRows = document.querySelectorAll('#degree-toggles-product .degree-toggle');
742                const tuples = Array.from(degRows).map(row =>
743                    Array.from(row.querySelectorAll('.hodge-input')).map(i => i.value).join(',')
744                );
745                if (tuples.length) params.set('md', tuples.join('|'));
746            }
747        }
748
749        if (document.getElementById('zero-color-toggle')?.checked)  params.set('color', '1');
750        if (document.getElementById('hide-top-half-toggle')?.checked) params.set('half', '1');
751        if (document.getElementById('show-blowups-toggle')?.checked) params.set('blowups', '1');
752
753        return params;
754    }
755
756    // --- URL State: restore calculator state from query params on page load ---
757    function decodeState() {
758        const params = new URLSearchParams(window.location.search);
759        const calc = params.get('calc');
760        if (!calc) return;
761
762        function fire(el) { el?.dispatchEvent(new Event('input', { bubbles: true })); }
763        function setVal(id, val) {
764            if (val === null) return;
765            const el = document.getElementById(id);
766            if (el) { el.value = val; fire(el); }
767        }
768
769        // Switch to the requested tab (CI is already active by default so skip its click)
770        const tabBtns = { abelian: toggleAbelianVariety, grassmannian: toggleGrassmannian,
771                          flag: toggleFlag, twisted: toggleTwisted, product: toggleProductGrassmannian };
772        if (tabBtns[calc]) tabBtns[calc].click();
773
774        if (calc === 'ci') {
775            setVal('n-value', params.get('n'));
776            setVal('r-value', params.get('r'));
777            setVal('s-value', params.get('s') ?? '0');
778            const deg = params.get('deg');
779            if (deg) {
780                requestAnimationFrame(() => {
781                    const inputs = document.querySelectorAll('#degree-toggles .hodge-input');
782                    deg.split(',').forEach((d, i) => {
783                        if (inputs[i]) { inputs[i].value = d; fire(inputs[i]); }
784                    });
785                });
786            }
787
788        } else if (calc === 'abelian') {
789            setVal('g-value', params.get('g'));
790
791        } else if (calc === 'grassmannian') {
792            setVal('n-value-grassmannian', params.get('n'));
793            setVal('k-value-grassmannian', params.get('k'));
794            setVal('r-value-grassmannian', params.get('r'));
795            setVal('s-value-grassmannian', params.get('s') ?? '0');
796            const deg = params.get('deg');
797            if (deg) {
798                requestAnimationFrame(() => {
799                    const inputs = document.querySelectorAll('#degree-toggles-grassmannian .hodge-input');
800                    deg.split(',').forEach((d, i) => {
801                        if (inputs[i]) { inputs[i].value = d; fire(inputs[i]); }
802                    });
803                });
804            }
805
806        } else if (calc === 'flag') {
807            // Flag is lazily loaded — click the toggle to trigger the dynamic import,
808            // then wait for _flagInitPromise (set up in pages/hodge.js) to resolve
809            // before restoring state. This replaces the old fragile setTimeout(500/200).
810            toggleFlag?.click();
811            (window._flagInitPromise || Promise.resolve())
812            .catch(e => console.error('Flag init failed, skipping state restore:', e))
813            .then(() => {
814                setVal('dims-input', (params.get('dims') ?? '').replace(/,/g, ', '));
815                setVal('r-value-flag', params.get('r'));
816                const md = params.get('md');
817                if (md) {
818                    const inputs = document.querySelectorAll('#degree-toggles-flag .hodge-input');
819                    md.split('|').forEach((val, i) => {
820                        if (inputs[i]) { inputs[i].value = val; fire(inputs[i]); }
821                    });
822                }
823            });
824
825        } else if (calc === 'twisted') {
826            setVal('n-value-twisted', params.get('n'));
827            setVal('k-value-twisted', params.get('k'));
828            setVal('t-value-twisted', params.get('t'));
829
830        } else if (calc === 'product') {
831            const factorsStr = params.get('factors');
832            if (factorsStr && productGrassmannianContainer?._loadPreset) {
833                const flat = factorsStr.split(',').map(Number);
834                const factors = [];
835                for (let i = 0; i + 1 < flat.length; i += 2) factors.push({ k: flat[i], n: flat[i + 1] });
836                const rNum = parseInt(params.get('r')) || 0;
837                const md = params.get('md');
838                const degrees = md ? md.split('|').map(row => row.split(',').map(Number)) : [];
839                productGrassmannianContainer._loadPreset(factors, rNum, degrees);
840            }
841        }
842
843        if (params.get('color') === '1') {
844            const el = document.getElementById('zero-color-toggle');
845            if (el) { el.checked = true; el.dispatchEvent(new Event('change')); }
846        }
847        if (params.get('half') === '1') {
848            const el = document.getElementById('hide-top-half-toggle');
849            if (el) { el.checked = true; el.dispatchEvent(new Event('change')); }
850        }
851        if (params.get('blowups') === '1') {
852            const el = document.getElementById('show-blowups-toggle');
853            if (el) { el.checked = true; el.dispatchEvent(new Event('change')); }
854        }
855    }
856
857    // Keep the URL in sync with the current calculator state as inputs change
858    let _urlUpdateTimer = null;
859    function scheduleUrlUpdate() {
860        clearTimeout(_urlUpdateTimer);
861        _urlUpdateTimer = setTimeout(() => {
862            const params = encodeState();
863            history.replaceState(history.state, '', '/hodge?' + params.toString());
864        }, 300);
865    }
866    document.getElementById('app')?.addEventListener('input', scheduleUrlUpdate);
867    document.getElementById('app')?.addEventListener('change', scheduleUrlUpdate);
868    document.getElementById('app')?.addEventListener('click', (e) => {
869        if (e.target.closest('.preset-button')) scheduleUrlUpdate();
870    });
871    // Also update when switching tabs (tab buttons fire click, not input)
872    [toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian,
873     toggleFlag, toggleTwisted, toggleProductGrassmannian].forEach(btn => {
874        btn?.addEventListener('click', () => setTimeout(scheduleUrlUpdate, 0));
875    });
876    ['add-factor-product', 'remove-factor-product'].forEach(id => {
877        document.getElementById(id)?.addEventListener('click', scheduleUrlUpdate);
878    });
879
880    // Extend _pageCleanup to cancel any pending URL update timer on navigation
881    const prevCleanupUrl = window._pageCleanup;
882    window._pageCleanup = () => {
883        prevCleanupUrl?.();
884        clearTimeout(_urlUpdateTimer);
885    };
886
887    // Copy Link button: encode state to URL, update address bar, copy to clipboard
888    const copyLinkBtn = document.getElementById('copy-link-btn');
889    if (copyLinkBtn) {
890        copyLinkBtn.addEventListener('click', () => {
891            clearTimeout(_urlUpdateTimer);
892            const params = encodeState();
893            const search = '?' + params.toString();
894            const fullUrl = window.location.origin + '/hodge' + search;
895            history.replaceState(history.state, '', '/hodge' + search);
896
897            const flash = (ok) => {
898                const orig = copyLinkBtn.textContent;
899                copyLinkBtn.textContent = ok ? 'Copied!' : 'Copy failed';
900                copyLinkBtn.disabled = true;
901                setTimeout(() => { copyLinkBtn.textContent = orig; copyLinkBtn.disabled = false; }, 1500);
902            };
903
904            if (navigator.clipboard?.writeText) {
905                navigator.clipboard.writeText(fullUrl).then(() => flash(true)).catch(() => flash(false));
906            } else {
907                // Fallback for browsers without clipboard API
908                const ta = document.createElement('textarea');
909                ta.value = fullUrl;
910                ta.style.cssText = 'position:fixed;opacity:0';
911                document.body.appendChild(ta);
912                ta.select();
913                try { document.execCommand('copy'); flash(true); } catch { flash(false); }
914                document.body.removeChild(ta);
915            }
916        });
917    }
918
919    // Restore state from URL query params (shared links, back-button navigation)
920    decodeState();
921
922}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.