PageSourceSearch

https://cjtype.com/dunbar-design-repository/assets/js/bundle.js

js cjtype.com collected 2026-10-02 03:01:21 UTC 66,734 bytes, 1,719 lines download raw bytes

vendor: 14 bytes, lines 1-2
1(() => {
2  // 
2dunbar-design-repository/assets/js/tester.js
3  document.addEventListener("DOMContentLoaded", () => {
4    console.log("tester.js loaded");
5    const tester = document.getElementById("tester");
6    const timelineSection = document.getElementById("timeline");
7    const timelineEntryMarkers = [...document.querySelectorAll(".timeline-entry-marker")];
8    const timelineEntrySections = [...document.querySelectorAll("#timeline .timeline-section[data-slug]")];
9    const fontWeight = document.getElementById("fontWeight");
10    const fontWidth = document.getElementById("fontWidth");
11    const xHeight = document.getElementById("xHeight");
12    const styleSelect = document.getElementById("style");
13    const resetBtn = document.getElementById("resetBtn");
14    const fontWeightValue = document.getElementById("fontWeightValue");
15    const fontWidthValue = document.getElementById("fontWidthValue");
16    const xHeightValue = document.getElementById("xHeightValue");
17    if (!tester || !fontWeight || !fontWidth || !xHeight || !resetBtn) {
18      console.warn("Tester elements missing");
19      return;
20    }
21    const testerWordListPaths = [
22      "/words/dunbar.txt"
23    ];
24    const fallbackPresetTexts = [
25      "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
26      "abcdefghijklmnopqrstuvwxyz",
27      "0123456789",
28      ".,:;!?&@#%*()[]{}+-=/",
29      "The quick brown fox jumps over the lazy dog.",
30      "Various alternate characters."
31    ];
32    let presetTexts = fallbackPresetTexts;
33    let testerHasUserText = false;
34    let scrollTextStep = 0;
35    let scrollTextFrameRequested = false;
36    const scrollTextStepCount = 4;
37    const weightStops = [100, 300, 500, 700, 800, 900];
38    tester.addEventListener("keydown", (e) => {
39      if (e.key === "Enter") {
40        e.preventDefault();
41      }
42    });
43    tester.addEventListener("paste", (e) => {
44      e.preventDefault();
45      testerHasUserText = true;
46      const text = (e.clipboardData || window.clipboardData).getData("text").replace(/[\r\n]+/g, " ");
47      document.execCommand("insertText", false, text);
48    });
49    function parseWordList(data) {
50      return data.replace(/^\/\/.+$/mg, "").replace(/(^|\n)\/\*[^]*?\*\/($|\n)/g, "\n").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).flatMap((line) => line.split("|").map((word) => word.trim()).filter(Boolean));
51    }
52    async function loadPresetTexts() {
53      try {
54        const lists = await Promise.all(
55          testerWordListPaths.map(async (path) => {
56            const response = await fetch(path);
57            if (!response.ok) {
58              throw new Error(`Unable to load ${path}`);
59            }
60            return response.text();
61          })
62        );
63        const nextPresetTexts = lists.flatMap(parseWordList);
64        if (nextPresetTexts.length) {
65          presetTexts = nextPresetTexts;
66        }
67      } catch (error) {
68        console.warn("Using fallback tester presets:", error);
69      }
70    }
71    function getRandomPreset(currentText = "") {
72      if (presetTexts.length < 2) return presetTexts[0] || "";
73      let nextText = currentText;
74      let attempts = 0;
75      while (nextText === currentText && attempts < 8) {
76        nextText = presetTexts[Math.floor(Math.random() * presetTexts.length)];
77        attempts += 1;
78      }
79      return nextText;
80    }
81    function syncStyleDropdownToWeight() {
82      if (!styleSelect) return;
83      const weight = Number(fontWeight.value);
84      const nearestWeight = weightStops.reduce((nearest, current) => {
85        return Math.abs(current - weight) < Math.abs(nearest - weight) ? current : nearest;
86      }, weightStops[0]);
87      styleSelect.value = String(nearestWeight);
88    }
89    function syncWeightToStyleDropdown() {
90      if (!styleSelect) return;
91      fontWeight.value = styleSelect.value;
92    }
93    function updateValueLabels() {
94      if (fontWeightValue) fontWeightValue.textContent = fontWeight.value;
95      if (fontWidthValue) fontWidthValue.textContent = fontWidth.value;
96      if (xHeightValue) xHeightValue.textContent = xHeight.value;
97    }
98    function updateTesterStyles() {
99      tester.style.fontWeight = fontWeight.value;
100      tester.style.fontVariationSettings = `"wght" ${fontWeight.value}, "wdth" ${fontWidth.value}, "XHGT" ${xHeight.value}`;
101      updateValueLabels();
102      console.log("fontVariationSettings:", tester.style.fontVariationSettings);
103    }
104    function autoResizeText() {
105      const text = tester.textContent.trim();
106      const isMobile = window.matchMedia("(max-width: 640px)").matches;
107      const baseSize = isMobile ? 44 : 90;
108      const minSize = isMobile ? 14 : 18;
109      const maxSize = isMobile ? 72 : 320;
110      const availableWidth = tester.offsetWidth * 0.95;
111      if (!text.length) {
112        tester.style.fontSize = `${baseSize}px`;
113        return;
114      }
115      tester.style.fontSize = `${baseSize}px`;
116      const range = document.createRange();
117      range.selectNodeContents(tester);
118      const rect = range.getBoundingClientRect();
119      if (!rect.width) return;
120      let scale = availableWidth / rect.width;
121      if (text.length <= 2) {
122        scale *= 1.1;
123      } else if (text.length <= 6) {
124        scale *= 1.05;
125      }
126      const newSize = Math.max(minSize, Math.min(baseSize * scale, maxSize));
127      tester.style.fontSize = `${newSize}px`;
128    }
129    function updateBaseline() {
130      const marker = document.createElement("span");
131      marker.textContent = "x";
132      marker.style.display = "inline-block";
133      marker.style.padding = "0";
134      marker.style.margin = "0";
135      marker.style.lineHeight = "1";
136      marker.style.visibility = "hidden";
137      tester.appendChild(marker);
138      const markerRect = marker.getBoundingClientRect();
139      const testerRect = tester.getBoundingClientRect();
140      const baseline = markerRect.bottom - testerRect.top;
141      tester.style.setProperty("--baseline", `${baseline}px`);
142      marker.remove();
143    }
144    function refreshTesterLayout() {
145      updateTesterStyles();
146      autoResizeText();
147      updateBaseline();
148    }
149    function setRandomTesterText() {
150      tester.textContent = getRandomPreset(tester.textContent.trim());
151      refreshTesterLayout();
152    }
153    function updateTesterTextOnScroll() {
154      scrollTextFrameRequested = false;
155      if (!timelineSection || testerHasUserText || !timelineEntryMarkers.length) return;
156      const timelineRect = timelineSection.getBoundingClientRect();
157      const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
158      if (timelineRect.bottom <= 0 || timelineRect.top >= viewportHeight) {
159        return;
160      }
161      const passedEntryCount = timelineEntrySections.length ? timelineEntrySections.filter((section) => {
162        return section.getBoundingClientRect().top <= viewportHeight * 0.5;
163      }).length : Math.round(
164        Math.min(
165          1,
166          Math.max(0, (viewportHeight - timelineRect.top) / (viewportHeight + timelineRect.height))
167        ) * timelineEntryMarkers.length
168      );
169      const nextStep = Math.min(
170        scrollTextStepCount,
171        Math.floor(passedEntryCount * scrollTextStepCount / timelineEntryMarkers.length)
172      );
173      if (nextStep !== scrollTextStep) {
174        scrollTextStep = nextStep;
175        setRandomTesterText();
176      }
177    }
178    function requestTesterTextScrollUpdate() {
179      if (scrollTextFrameRequested) return;
180      scrollTextFrameRequested = true;
181      window.requestAnimationFrame(updateTesterTextOnScroll);
182    }
183    tester.textContent = getRandomPreset();
184    loadPresetTexts().then(() => {
185      if (!testerHasUserText) {
186        setRandomTesterText();
187      }
188    });
189    tester.addEventListener("beforeinput", () => {
190      testerHasUserText = true;
191    });
192    tester.addEventListener("input", () => {
193      testerHasUserText = true;
194      refreshTesterLayout();
195    });
196    fontWeight.addEventListener("input", () => {
197      syncStyleDropdownToWeight();
198      refreshTesterLayout();
199    });
200    fontWidth.addEventListener("input", refreshTesterLayout);
201    xHeight.addEventListener("input", refreshTesterLayout);
202    fontWeight.addEventListener("change", refreshTesterLayout);
203    fontWidth.addEventListener("change", refreshTesterLayout);
204    xHeight.addEventListener("change", refreshTesterLayout);
205    if (styleSelect) {
206      styleSelect.addEventListener("change", () => {
207        syncWeightToStyleDropdown();
208        refreshTesterLayout();
209      });
210    }
211    resetBtn.addEventListener("click", () => {
212      testerHasUserText = false;
213      scrollTextStep = 0;
214      fontWeight.value = 500;
215      fontWidth.value = 100;
216      xHeight.value = 460;
217      if (styleSelect) styleSelect.value = "500";
218      setRandomTesterText();
219    });
220    window.addEventListener("scroll", requestTesterTextScrollUpdate, { passive: true });
221    window.addEventListener("resize", () => {
222      refreshTesterLayout();
223      requestTesterTextScrollUpdate();
224    });
225    refreshTesterLayout();
226  });
227
228  
228// dunbar-design-repository/assets/js/feature.js
229  document.addEventListener("DOMContentLoaded", () => {
230    console.log("feature.js loaded");
231    function initIntroVariableDemo(demo) {
232      const svg = demo.querySelector(".intro-variable-svg");
233      const plot = demo.querySelector(".intro-variable-plot");
234      const letters = [...demo.querySelectorAll(".intro-variable-letter")];
235      const word = demo.querySelector(".intro-variable-word");
236      const guideLayer = demo.querySelector(".intro-variable-guides");
237      const sequenceLetters = [...demo.dataset.introSequence || ""];
238      const isSequentialLetterDemo = sequenceLetters.length > 0 && letters.length === 1;
239      if (!svg || !plot || !letters.length || !word || !guideLayer) return null;
240      const holdDuration = 1400;
241      const transitionDuration = 900;
242      const letterSwitchPause = 2e3;
243      const ranges = {
244        wght: [100, 900],
245        wdth: [75, 100],
246        XHGT: [353, 574]
247      };
248      const capHeightValue = 746;
249      const widthStops = [
250        { value: 75, label: "Compressed" },
251        { value: 82, label: "Condensed" },
252        { value: 90, label: "Narrow" },
253        { value: 100, label: "Regular" }
254      ];
255      const xheightStops = [
256        { value: 353, label: "Low" },
257        { value: 525, label: "Text" },
258        { value: 574, label: "Tall" }
259      ];
260      const weightStops = [
261        { value: 100, label: "Hairline" },
262        { value: 300, label: "Light" },
263        { value: 350, label: "Book" },
264        { value: 400, label: "Regular" },
265        { value: 500, label: "Medium" },
266        { value: 700, label: "Bold" },
267        { value: 800, label: "Extra Bold" },
268        { value: 900, label: "Ultra" }
269      ];
270      const layout = {
271        plotY: 455,
272        baselineY: 0,
273        capHeightY: -395,
274        weightValueY: 22,
275        xheightValueOffset: -8,
276        capHeightValueOffset: 16,
277        capHeightLineOffset: 23,
278        xheightScale: 0.72,
279        widthValueY: -410,
280        widthGuideTopY: -405,
281        labelY: 58,
282        labelLineHeight: 15,
283        contentLeft: isSequentialLetterDemo ? -110 : -260,
284        contentRightPadding: isSequentialLetterDemo ? 110 : 260
285      };
286      const initialStates = makeStateSet();
287      let sequenceIndex = 0;
288      let sequenceChanges = 0;
289      if (isSequentialLetterDemo) {
290        letters[0].textContent = sequenceLetters[sequenceIndex];
291      }
292      const states = letters.map((el, i) => {
293        const current = initialStates[i];
294        return {
295          el,
296          current,
297          from: { ...current },
298          target: { ...current }
299        };
300      });
301      const axisX = createSvgEl("line", ["intro-axis-line"]);
302      const axisY = createSvgEl("line", ["intro-axis-line"]);
303      const capLine = createSvgEl("line", ["intro-axis-line"]);
304      const baselineLine = createSvgEl("line", ["intro-baseline-line"]);
305      const originLine = createSvgEl("line", ["intro-origin-line"]);
306      const widthAxisLabel = createSvgEl("text", ["intro-axis-label"]);
307      const capHeightAxisLabel = createSvgEl("text", ["intro-axis-label"]);
308      const xheightAxisLabel = createSvgEl("text", ["intro-axis-label", "intro-xheight-axis-label"]);
309      const weightAxisLabel = createSvgEl("text", ["intro-axis-label"]);
310      guideLayer.appendChild(axisX);
311      guideLayer.appendChild(axisY);
312      guideLayer.appendChild(capLine);
313      guideLayer.appendChild(baselineLine);
314      guideLayer.appendChild(originLine);
315      guideLayer.appendChild(widthAxisLabel);
316      guideLayer.appendChild(capHeightAxisLabel);
317      guideLayer.appendChild(xheightAxisLabel);
318      guideLayer.appendChild(weightAxisLabel);
319      const guides = letters.map((letter, i) => {
320        const group = createSvgEl("g");
321        const isCapHeightGuide = !isSequentialLetterDemo && i === 0;
322        const widthLine = createSvgEl("line", ["intro-guide-line"]);
323        const xheightLine = createSvgEl("line", [
324          "intro-guide-line",
325          isCapHeightGuide ? "intro-capheight-guide" : "intro-xheight-guide"
326        ]);
327        const widthValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]);
328        const xheightValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]);
329        const weightValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]);
330        const label = createSvgEl("text", ["intro-letter-label"]);
331        const labelBox = createSvgEl("rect", ["intro-letter-label-box"]);
332        const widthLabel = createSvgEl("tspan", ["intro-label-row"]);
333        const xheightLabel = createSvgEl("tspan", ["intro-label-row"]);
334        const weightLabel = createSvgEl("tspan", ["intro-label-row"]);
335        group.appendChild(widthLine);
336        group.appendChild(xheightLine);
337        group.appendChild(widthValue);
338        group.appendChild(xheightValue);
339        group.appendChild(weightValue);
340        group.appendChild(labelBox);
341        label.appendChild(widthLabel);
342        label.appendChild(xheightLabel);
343        label.appendChild(weightLabel);
344        group.appendChild(label);
345        guideLayer.appendChild(group);
346        return {
347          widthLine,
348          xheightLine,
349          widthValue,
350          xheightValue,
351          weightValue,
352          labelBox,
353          label,
354          widthLabel,
355          xheightLabel,
356          weightLabel,
357          isCapHeightGuide
358        };
359      });
360      let phase = "hold";
361      let phaseStart = performance.now();
362      let isRunning = false;
363      let animationFrameId = null;
364      function makeStateSet(previousStates = []) {
365        const stateProfiles = [
366          { wght: [100, 170], wdth: [92, 100], XHGT: [353, 390] },
367          { wght: [850, 900], wdth: [82, 90], XHGT: [540, 574] },
368          { wght: [420, 680], wdth: [75, 77], XHGT: [420, 500] },
369          { wght: [680, 800], wdth: [94, 100], XHGT: [353, 410] },
370          { wght: [260, 340], wdth: [81, 83], XHGT: [430, 500] },
371          { wght: [430, 560], wdth: [94, 100], XHGT: [520, 574] }
372        ];
373        const nextStates = [];
374        const profiles = stateProfiles.slice(0, letters.length);
375        profiles.forEach((profile) => {
376          nextStates.push(makeDistinctState(nextStates, profile));
377        });
378        while (nextStates.length < letters.length) {
379          nextStates.push(makeDistinctState(nextStates));
380        }
381        return orderStatesForContrast(nextStates, previousStates);
382      }
383      function makeDistinctState(existingStates, profile = ranges) {
384        let candidate = makeRandomState(profile);
385        let attempts = 0;
386        while (!isDistinctState(candidate, existingStates) && attempts < 80) {
387          candidate = makeRandomState(profile);
388          attempts += 1;
389        }
390        return candidate;
391      }
392      function makeRandomState(profile = ranges) {
393        return {
394          wght: random(...profile.wght),
395          wdth: random(...profile.wdth),
396          XHGT: random(...profile.XHGT)
397        };
398      }
399      function isDistinctState(candidate, existingStates) {
400        return existingStates.every((state) => {
401          const wghtDistance = Math.abs(candidate.wght - state.wght);
402          const wdthDistance = Math.abs(candidate.wdth - state.wdth);
403          const xhgtDistance = Math.abs(candidate.XHGT - state.XHGT);
404          if (wghtDistance < 140 && wdthDistance < 7 && xhgtDistance < 60) {
405            return false;
406          }
407          return getStateDistance(candidate, state) > 0.34;
408        });
409      }
410      function getStateDistance(a, b) {
411        const wght = (a.wght - b.wght) / (ranges.wght[1] - ranges.wght[0]);
412        const wdth = (a.wdth - b.wdth) / (ranges.wdth[1] - ranges.wdth[0]);
413        const XHGT = (a.XHGT - b.XHGT) / (ranges.XHGT[1] - ranges.XHGT[0]);
414        return Math.sqrt(wght * wght + wdth * wdth + XHGT * XHGT);
415      }
416      function orderStatesForContrast(nextStates, previousStates = []) {
417        let bestOrder = shuffle(nextStates);
418        let bestScore = getSequenceContrastScore(bestOrder, previousStates);
419        for (let i = 0; i < 220; i += 1) {
420          const candidate = shuffle(nextStates);
421          const score = getSequenceContrastScore(candidate, previousStates);
422          if (score > bestScore) {
423            bestOrder = candidate;
424            bestScore = score;
425          }
426        }
427        return bestOrder;
428      }
429      function getSequenceContrastScore(sequence, previousStates = []) {
430        const neighborDistances = sequence.slice(1).map((state, i) => {
431          return getStateDistance(sequence[i], state);
432        });
433        const transitionDistances = sequence.map((state, i) => {
434          if (!previousStates[i]) return 1;
435          return getStateDistance(previousStates[i], state);
436        });
437        const minNeighborDistance = Math.min(...neighborDistances, 1);
438        const minTransitionDistance = Math.min(...transitionDistances);
439        const averageNeighborDistance = getAverage(neighborDistances);
440        const averageTransitionDistance = getAverage(transitionDistances);
441        const weakNeighborCount = neighborDistances.filter((distance) => distance < 0.42).length;
442        const weakTransitionCount = transitionDistances.filter((distance) => distance < 0.48).length;
443        return minNeighborDistance * 6 + minTransitionDistance * 7 + averageNeighborDistance + averageTransitionDistance - weakNeighborCount * 3 - weakTransitionCount * 4;
444      }
445      function getAverage(values) {
446        if (!values.length) return 0;
447        return values.reduce((sum, value) => sum + value, 0) / values.length;
448      }
449      function applyState(el, state) {
450        el.style.fontVariationSettings = getFontVariationSettings(state);
451      }
452      function getFontVariationSettings(state) {
453        return `
454      "wght" ${Math.round(state.wght)},
455      "wdth" ${Math.round(state.wdth)},
456      "XHGT" ${Math.round(state.XHGT)}
457    `;
458      }
459      function getXHeightY(state) {
460        return layout.baselineY - state.XHGT / ranges.XHGT[1] * Math.abs(layout.capHeightY) * layout.xheightScale;
461      }
462      function syncGuideLabelFontSizes() {
463        const labelRowGap = Number.parseFloat(
464          getComputedStyle(demo).getPropertyValue("--intro-label-row-gap")
465        );
466        if (Number.isFinite(labelRowGap) && labelRowGap > 0) {
467          layout.labelLineHeight = labelRowGap;
468        }
469        const viewBoxWidth = svg.viewBox.baseVal.width;
470        const renderedWidth = svg.getBoundingClientRect().width;
471        if (!viewBoxWidth || !renderedWidth) return;
472        const svgScale = renderedWidth / viewBoxWidth;
473        const labels = guideLayer.querySelectorAll(
474          ".intro-origin-text, .intro-axis-label, .intro-guide-text, .intro-letter-label"
475        );
476        labels.forEach((label) => {
477          label.style.removeProperty("font-size");
478          const cssFontSize = Number.parseFloat(getComputedStyle(label).fontSize);
479          if (Number.isFinite(cssFontSize) && cssFontSize > 0) {
480            label.style.fontSize = `${cssFontSize / svgScale}px`;
481          }
482        });
483      }
484      function updateGuides() {
485        let boxes = [];
486        try {
487          boxes = states.map((state, i) => {
488            const start2 = word.getStartPositionOfChar(i);
489            const end = word.getEndPositionOfChar(i);
490            const width = end.x - start2.x;
491            if (Number.isFinite(width) && width > 0) {
492              return { x: start2.x, width };
493            }
494            const box = state.el.getBBox();
495            if (box.width > 0) return box;
496            return word.getExtentOfChar(i);
497          });
498        } catch {
499          return;
500        }
501        if (!boxes.length) return;
502        const lastBox = boxes[boxes.length - 1];
503        const wordRight = lastBox.x + lastBox.width;
504        const viewBoxWidth = svg.viewBox.baseVal.width || svg.clientWidth;
505        const contentLeft = layout.contentLeft;
506        const contentRight = wordRight + layout.contentRightPadding;
507        const plotX = (viewBoxWidth - (contentRight - contentLeft)) / 2 - contentLeft;
508        const svgScale = svg.getBoundingClientRect().width / viewBoxWidth || 1;
509        const rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
510        const letterLabelPaddingX = rootFontSize * 0.8 / svgScale;
511        const letterLabelPaddingY = rootFontSize * 0.5 / svgScale;
512        plot.setAttribute("transform", `translate(${plotX} ${layout.plotY})`);
513        widthAxisLabel.setAttribute("x", -6);
514        widthAxisLabel.setAttribute("y", layout.capHeightY - 10);
515        widthAxisLabel.setAttribute("text-anchor", "end");
516        widthAxisLabel.textContent = "Width";
517        let widthLabelCapHeightY = layout.widthGuideTopY;
518        try {
519          widthLabelCapHeightY = widthAxisLabel.getBBox().y;
520        } catch {
521        }
522        axisX.setAttribute("x1", layout.contentLeft);
523        axisX.setAttribute("x2", wordRight + layout.contentRightPadding);
524        axisX.setAttribute("y1", layout.baselineY);
525        axisX.setAttribute("y2", layout.baselineY);
526        axisY.setAttribute("x1", 0);
527        axisY.setAttribute("x2", 0);
528        axisY.setAttribute("y1", widthLabelCapHeightY);
529        axisY.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2);
530        originLine.setAttribute("x1", 0);
531        originLine.setAttribute("x2", 0);
532        originLine.setAttribute("y1", widthLabelCapHeightY);
533        originLine.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2);
534        capLine.setAttribute("x1", 0);
535        capLine.setAttribute("x2", wordRight);
536        capLine.setAttribute("y1", layout.capHeightY + layout.capHeightLineOffset);
537        capLine.setAttribute("y2", layout.capHeightY + layout.capHeightLineOffset);
538        baselineLine.setAttribute("x1", 0);
539        baselineLine.setAttribute("x2", wordRight);
540        baselineLine.setAttribute("y1", layout.baselineY);
541        baselineLine.setAttribute("y2", layout.baselineY);
542        capHeightAxisLabel.setAttribute("x", -6);
543        capHeightAxisLabel.setAttribute("y", layout.capHeightY + layout.capHeightValueOffset);
544        capHeightAxisLabel.setAttribute("text-anchor", "end");
545        capHeightAxisLabel.textContent = "Cap height";
546        xheightAxisLabel.setAttribute("x", -6);
547        xheightAxisLabel.setAttribute(
548          "y",
549          (layout.capHeightY + layout.capHeightValueOffset + layout.weightValueY) / 2
550        );
551        xheightAxisLabel.setAttribute("text-anchor", "end");
552        xheightAxisLabel.textContent = isSequentialLetterDemo && sequenceIndex === 0 ? "" : "x-HEIGHT";
553        weightAxisLabel.setAttribute("x", -6);
554        weightAxisLabel.setAttribute("y", layout.weightValueY);
555        weightAxisLabel.setAttribute("text-anchor", "end");
556        weightAxisLabel.textContent = "Weight";
557        states.forEach((state, i) => {
558          const guide = guides[i];
559          const box = boxes[i];
560          if (!guide || !box) return;
561          const isCapHeightGuide = isSequentialLetterDemo ? sequenceIndex === 0 : guide.isCapHeightGuide;
562          guide.xheightLine.classList.toggle("intro-capheight-guide", isCapHeightGuide);
563          guide.xheightLine.classList.toggle("intro-xheight-guide", !isCapHeightGuide);
564          const left = box.x;
565          const right = box.x + box.width;
566          const center = box.x + box.width / 2;
567          const xheightY = isCapHeightGuide ? layout.capHeightY + layout.capHeightLineOffset : getXHeightY(state.current);
568          const xheightGuideY = isCapHeightGuide ? xheightY : xheightY - 2;
569          guide.widthLine.setAttribute("x1", right);
570          guide.widthLine.setAttribute("x2", right);
571          guide.widthLine.setAttribute("y1", widthLabelCapHeightY);
572          guide.widthLine.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2);
573          guide.xheightLine.setAttribute("x1", left);
574          guide.xheightLine.setAttribute("x2", right);
575          guide.xheightLine.setAttribute("y1", xheightGuideY);
576          guide.xheightLine.setAttribute("y2", xheightGuideY);
577          guide.widthValue.setAttribute("x", center);
578          guide.widthValue.setAttribute("y", layout.widthValueY);
579          guide.widthValue.setAttribute("text-anchor", "middle");
580          guide.widthValue.textContent = Math.round(box.width);
581          const xheightValueY = isCapHeightGuide ? layout.capHeightY + layout.capHeightValueOffset : xheightY + layout.xheightValueOffset;
582          guide.xheightValue.setAttribute("x", center);
583          guide.xheightValue.setAttribute("y", xheightValueY);
584          guide.xheightValue.setAttribute("text-anchor", "middle");
585          guide.xheightValue.textContent = isCapHeightGuide ? capHeightValue : Math.round(state.current.XHGT);
586          guide.weightValue.setAttribute("x", center);
587          guide.weightValue.setAttribute("y", layout.weightValueY);
588          guide.weightValue.setAttribute("text-anchor", "middle");
589          guide.weightValue.textContent = Math.round(state.current.wght);
590          guide.label.setAttribute("x", center);
591          guide.label.setAttribute("y", layout.labelY);
592          guide.label.setAttribute("text-anchor", "middle");
593          guide.widthLabel.setAttribute("x", center);
594          guide.widthLabel.setAttribute("dy", 0);
595          guide.widthLabel.textContent = getNearestLabel(state.current.wdth, widthStops);
596          guide.xheightLabel.setAttribute("x", center);
597          guide.xheightLabel.setAttribute("dy", layout.labelLineHeight);
598          guide.xheightLabel.textContent = getNearestLabel(state.current.XHGT, xheightStops);
599          guide.weightLabel.setAttribute("x", center);
600          guide.weightLabel.setAttribute("dy", layout.labelLineHeight);
601          guide.weightLabel.textContent = getNearestLabel(state.current.wght, weightStops);
602          const labelBounds = guide.label.getBBox();
603          guide.labelBox.setAttribute("x", labelBounds.x - letterLabelPaddingX);
604          guide.labelBox.setAttribute("y", labelBounds.y - letterLabelPaddingY);
605          guide.labelBox.setAttribute("width", labelBounds.width + letterLabelPaddingX * 2);
606          guide.labelBox.setAttribute("height", labelBounds.height + letterLabelPaddingY * 2);
607        });
608      }
609      function animate(now) {
610        if (!isRunning) return;
611        const elapsed = now - phaseStart;
612        if (phase === "letter-switch-pause") {
613          states.forEach((state) => applyState(state.el, state.current));
614          updateGuides();
615          if (elapsed >= letterSwitchPause) {
616            sequenceIndex = (sequenceIndex + 1) % sequenceLetters.length;
617            letters[0].textContent = sequenceLetters[sequenceIndex];
618            sequenceChanges = 0;
619            phase = "hold";
620            phaseStart = now;
621          }
622          animationFrameId = requestAnimationFrame(animate);
623          return;
624        }
625        if (phase === "hold") {
626          states.forEach((state) => applyState(state.el, state.current));
627          updateGuides();
628          if (elapsed >= holdDuration) {
629            const nextStates = isSequentialLetterDemo ? [makeDistinctState([states[0].current])] : makeStateSet(states.map((state) => state.current));
630            states.forEach((state) => {
631              state.from = { ...state.current };
632              state.target = nextStates.shift();
633            });
634            phase = "transition";
635            phaseStart = now;
636          }
637          animationFrameId = requestAnimationFrame(animate);
638          return;
639        }
640        if (phase === "transition") {
641          const progress = Math.min(elapsed / transitionDuration, 1);
642          const eased = easeInOutCubic(progress);
643          states.forEach((state) => {
644            state.current = {
645              wght: mix(state.from.wght, state.target.wght, eased),
646              wdth: mix(state.from.wdth, state.target.wdth, eased),
647              XHGT: mix(state.from.XHGT, state.target.XHGT, eased)
648            };
649            applyState(state.el, state.current);
650          });
651          updateGuides();
652          if (progress >= 1) {
653            if (isSequentialLetterDemo) {
654              sequenceChanges += 1;
655              if (sequenceChanges === 2) {
656                phase = "letter-switch-pause";
657                phaseStart = now;
658                animationFrameId = requestAnimationFrame(animate);
659                return;
660              }
661            }
662            phase = "hold";
663            phaseStart = now;
664          }
665        }
666        animationFrameId = requestAnimationFrame(animate);
667      }
668      function start() {
669        if (isRunning) return;
670        isRunning = true;
671        phaseStart = performance.now();
672        animationFrameId = requestAnimationFrame(animate);
673      }
674      function stop() {
675        isRunning = false;
676        if (animationFrameId) {
677          cancelAnimationFrame(animationFrameId);
678          animationFrameId = null;
679        }
680      }
681      function refreshGuides() {
682        syncGuideLabelFontSizes();
683        states.forEach((state) => applyState(state.el, state.current));
684        updateGuides();
685      }
686      function scheduleGuidesRefresh() {
687        requestAnimationFrame(() => {
688          requestAnimationFrame(refreshGuides);
689        });
690      }
691      function initialize() {
692        refreshGuides();
693        const introSection = demo.closest("#intro") || demo;
694        const introObserver = new IntersectionObserver(
695          (entries) => {
696            entries.forEach((entry) => {
697              if (entry.isIntersecting) {
698                start();
699              } else {
700                stop();
701              }
702            });
703          },
704          {
705            threshold: 0.1
706          }
707        );
708        introObserver.observe(introSection);
709      }
710      if (document.fonts?.ready) {
711        document.fonts.ready.then(initialize);
712        document.fonts.addEventListener?.("loadingdone", scheduleGuidesRefresh);
713      } else {
714        initialize();
715      }
716      window.addEventListener("load", scheduleGuidesRefresh, { once: true });
717      if (window.ResizeObserver) {
718        const resizeObserver = new ResizeObserver(scheduleGuidesRefresh);
719        resizeObserver.observe(svg);
720      }
721      return { element: demo, start, stop };
722    }
723    function createSvgEl(tag, classNames = []) {
724      const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
725      classNames.forEach((name) => el.classList.add(name));
726      return el;
727    }
728    function random(min, max) {
729      return min + Math.random() * (max - min);
730    }
731    function shuffle(items) {
732      const shuffled = [...items];
733      for (let i = shuffled.length - 1; i > 0; i -= 1) {
734        const j = Math.floor(Math.random() * (i + 1));
735        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
736      }
737      return shuffled;
738    }
739    function mix(a, b, t) {
740      return a + (b - a) * t;
741    }
742    function easeInOutCubic(t) {
743      return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
744    }
745    function getNearestLabel(value, stops) {
746      let closest = stops[0];
747      stops.forEach((stop) => {
748        if (Math.abs(stop.value - value) < Math.abs(closest.value - value)) {
749          closest = stop;
750        }
751      });
752      return closest.label;
753    }
754    document.querySelectorAll("[data-intro-variable-demo]").forEach((demo) => {
755      initIntroVariableDemo(demo);
756    });
757    const featureDemos = [];
758    function initTypeDemo(demo, settings) {
759      const svg = demo.querySelector(".type-svg");
760      const textArea = demo.querySelector(".type-text");
761      const word = demo.querySelector(".word");
762      const wordText = demo.querySelector(".word-text");
763      const guides = demo.querySelector(".guides");
764      const axisValue = demo.querySelector(".axis-value");
765      const buttons = demo.querySelectorAll(".controls button");
766      const xheightGuide = demo.querySelector(".xheight-guide");
767      const baselineGuide = demo.querySelector(".baseline-guide");
768      if (!axisValue || buttons.length === 0) return null;
769      if (!word && !wordText) return null;
770      const activeWord = wordText || word;
771      const stops = settings.stops;
772      let stopIndex = 0;
773      let pauseUntil = 0;
774      const pauseDuration = 700;
775      let currentValue = stops[0];
776      let isPausedByHover = false;
777      let isRunning = false;
778      let animationFrameId = null;
779      const speed = settings.speed || 1;
780      let guideLines = [];
781      const yTop = 70;
782      const yBottom = 340;
783      function createWidthGuides() {
784        if (!guides || !word) return;
785        guides.innerHTML = "";
786        guideLines = [];
787        const text = word.textContent.trim();
788        for (let i = 0; i < text.length; i++) {
789          const line = document.createElementNS(
790            "http://www.w3.org/2000/svg",
791            "line"
792          );
793          line.setAttribute("y1", yTop);
794          line.setAttribute("y2", yBottom);
795          line.setAttribute("class", "guide-line");
796          guides.appendChild(line);
797          guideLines.push(line);
798        }
799      }
800      function updateWidthGuides() {
801        if (settings.axis !== "wdth") return;
802        if (!word || !guides || guideLines.length === 0) return;
803        const text = word.textContent.trim();
804        for (let i = 0; i < text.length; i++) {
805          if (!guideLines[i]) continue;
806          const endPosition = word.getEndPositionOfChar(i);
807          guideLines[i].setAttribute("x1", endPosition.x);
808          guideLines[i].setAttribute("x2", endPosition.x);
809        }
810      }
811      function updateXHeightGuides() {
812        if (settings.axis !== "XHGT") return;
813        if (!xheightGuide || !baselineGuide) return;
814        const baselineY = settings.baselineY || 205;
815        const minXHeight = Math.min(...settings.stops);
816        const maxXHeight = Math.max(...settings.stops);
817        const lowGuideY = settings.lowGuideY || 145;
818        const tallGuideY = settings.tallGuideY || 85;
819        const progress = (currentValue - minXHeight) / (maxXHeight - minXHeight);
820        const xheightY = lowGuideY + (tallGuideY - lowGuideY) * progress;
821        baselineGuide.setAttribute("y1", baselineY);
822        baselineGuide.setAttribute("y2", baselineY);
823        xheightGuide.setAttribute("y1", xheightY);
824        xheightGuide.setAttribute("y2", xheightY);
825      }
826      function setAxisValue(value) {
827        currentValue = value;
828        const xhgt = settings.axis === "XHGT" ? currentValue : settings.xhgt;
829        const wdth = settings.axis === "wdth" ? currentValue : settings.wdth;
830        activeWord.style.fontVariationSettings = `
831          "XHGT" ${xhgt},
832          "wght" ${settings.wght},
833          "wdth" ${wdth}
834        `;
835        axisValue.textContent = Math.round(currentValue);
836        updateActiveButton();
837        requestAnimationFrame(() => {
838          updateWidthGuides();
839          updateXHeightGuides();
840        });
841      }
842      function updateActiveButton() {
843        buttons.forEach((button) => {
844          button.classList.remove("active");
845          const buttonValue = Number(button.dataset.value);
846          if (Math.round(currentValue) === buttonValue) {
847            button.classList.add("active");
848          }
849        });
850      }
851      function animate(timestamp) {
852        if (!isRunning) return;
853        if (!isPausedByHover) {
854          if (timestamp < pauseUntil) {
855            animationFrameId = requestAnimationFrame(animate);
856            return;
857          }
858          const targetValue = stops[stopIndex];
859          if (Math.abs(currentValue - targetValue) <= speed) {
860            currentValue = targetValue;
861            setAxisValue(currentValue);
862            pauseUntil = timestamp + pauseDuration;
863            stopIndex++;
864            if (stopIndex >= stops.length) {
865              stopIndex = 0;
866            }
867          } else {
868            const moveDirection = currentValue < targetValue ? 1 : -1;
869            currentValue += moveDirection * speed;
870            setAxisValue(currentValue);
871          }
872        }
873        animationFrameId = requestAnimationFrame(animate);
874      }
875      function start() {
876        if (isRunning) return;
877        isRunning = true;
878        demo.classList.add("is-active-demo");
879        animationFrameId = requestAnimationFrame(animate);
880      }
881      function stop() {
882        isRunning = false;
883        demo.classList.remove("is-active-demo");
884        if (animationFrameId) {
885          cancelAnimationFrame(animationFrameId);
886          animationFrameId = null;
887        }
888      }
889      buttons.forEach((button) => {
890        button.addEventListener("click", () => {
891          const value = Number(button.dataset.value);
892          const currentStopIndex = stops.indexOf(value);
893          stopIndex = currentStopIndex + 1;
894          if (stopIndex >= stops.length || currentStopIndex === -1) {
895            stopIndex = 0;
896          }
897          pauseUntil = performance.now() + pauseDuration;
898          setAxisValue(value);
899        });
900      });
901      [svg, textArea, ...buttons].filter(Boolean).forEach((el) => {
902        el.addEventListener("mouseenter", () => {
903          isPausedByHover = true;
904        });
905        el.addEventListener("mouseleave", () => {
906          isPausedByHover = false;
907        });
908      });
909      document.fonts.ready.then(() => {
910        createWidthGuides();
911        setAxisValue(currentValue);
912      });
913      return {
914        element: demo,
915        start,
916        stop
917      };
918    }
919    function initParagraphDemo(demo, settings) {
920      const layers = [...demo.querySelectorAll(".paragraph-demo-layer")];
921      const buttons = demo.querySelectorAll(".paragraph-controls button");
922      if (layers.length === 0 || buttons.length === 0) return null;
923      function wrapText(el) {
924        if (!el.dataset.originalText) {
925          el.dataset.originalText = el.textContent;
926        }
927        const rawText = el.dataset.originalText.replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
928        el.innerHTML = "";
929        const words = rawText.split(" ");
930        words.forEach((word, wordIndex) => {
931          const wordUnit = document.createElement("span");
932          wordUnit.className = "word-unit";
933          [...word].forEach((char, charIndex) => {
934            const letter = document.createElement("span");
935            letter.className = "char-letter";
936            letter.textContent = char;
937            wordUnit.appendChild(letter);
938            if (charIndex < word.length - 1) {
939              const letterSpace = document.createElement("span");
940              letterSpace.className = "letter-space";
941              wordUnit.appendChild(letterSpace);
942            }
943          });
944          el.appendChild(wordUnit);
945          if (wordIndex < words.length - 1) {
946            const wordSpace = document.createElement("span");
947            wordSpace.className = "word-space";
948            wordSpace.textContent = "\xA0";
949            wordSpace.classList.add(`word-space-${wordIndex + 1}`);
950            el.appendChild(wordSpace);
951          }
952        });
953      }
954      layers.forEach((layer, index) => {
955        const heading = layer.querySelector(".heading-demo-text");
956        const paragraph = layer.querySelector(".paragraph-demo-text");
957        const state = settings.states[index];
958        if (!heading || !paragraph || !state) return;
959        wrapText(heading);
960        wrapText(paragraph);
961        [heading, paragraph].forEach((el) => {
962          if (state.fontFamily) {
963            el.style.fontFamily = state.fontFamily;
964          }
965          if (state.fontVariationSettings) {
966            el.style.fontVariationSettings = state.fontVariationSettings;
967          }
968        });
969      });
970      const holdDuration = settings.holdDuration || 2400;
971      let stateIndex = 0;
972      let timeoutId = null;
973      let isPausedByHover = false;
974      let isRunning = false;
975      function updateActiveButton() {
976        buttons.forEach((button) => {
977          button.classList.toggle(
978            "active",
979            Number(button.dataset.state) === stateIndex
980          );
981        });
982      }
983      function showState(index) {
984        if (!layers[index]) return;
985        stateIndex = index;
986        layers.forEach((layer, layerIndex) => {
987          layer.classList.toggle("active", layerIndex === stateIndex);
988        });
989        updateActiveButton();
990      }
991      function clearAutoTimer() {
992        if (timeoutId) {
993          window.clearTimeout(timeoutId);
994          timeoutId = null;
995        }
996      }
997      function scheduleNext() {
998        clearAutoTimer();
999        if (!isRunning || isPausedByHover) return;
1000        timeoutId = window.setTimeout(() => {
1001          showState((stateIndex + 1) % layers.length);
1002          scheduleNext();
1003        }, holdDuration);
1004      }
1005      buttons.forEach((button) => {
1006        button.addEventListener("click", () => {
1007          const nextIndex = Number(button.dataset.state);
1008          if (Number.isNaN(nextIndex) || !layers[nextIndex]) return;
1009          showState(nextIndex);
1010          scheduleNext();
1011        });
1012      });
1013      demo.addEventListener("mouseenter", () => {
1014        isPausedByHover = true;
1015        clearAutoTimer();
1016      });
1017      demo.addEventListener("mouseleave", () => {
1018        isPausedByHover = false;
1019        scheduleNext();
1020      });
1021      function start() {
1022        if (isRunning) return;
1023        isRunning = true;
1024        demo.classList.add("is-active-demo");
1025        showState(stateIndex);
1026        scheduleNext();
1027      }
1028      function stop() {
1029        isRunning = false;
1030        demo.classList.remove("is-active-demo");
1031        clearAutoTimer();
1032      }
1033      document.fonts.ready.then(() => {
1034        showState(0);
1035      });
1036      return {
1037        element: demo,
1038        start,
1039        stop
1040      };
1041    }
1042    function initSvgSlideshowDemo(demo, settings = {}) {
1043      const desktopSlides = [...demo.querySelectorAll(".svg-slideshow > .svg-slide")];
1044      const mobileSlides = [...demo.querySelectorAll(".svg-slideshow-mobile > .svg-slide")];
1045      const buttons = [...demo.querySelectorAll(".controls button")];
1046      const mobileQuery = window.matchMedia("(max-width: 640px)");
1047      if (!desktopSlides.length && !mobileSlides.length) return null;
1048      let currentIndex = 0;
1049      let intervalId = null;
1050      let isPausedByHover = false;
1051      const interval = settings.interval || 1400;
1052      function getSlides() {
1053        return mobileQuery.matches && mobileSlides.length ? mobileSlides : desktopSlides;
1054      }
1055      function showSlide(index) {
1056        const slides = getSlides();
1057        currentIndex = index % slides.length;
1058        [...desktopSlides, ...mobileSlides].forEach((slide) => {
1059          slide.classList.remove("active");
1060        });
1061        slides.forEach((slide, i) => {
1062          slide.classList.toggle("active", i === currentIndex);
1063        });
1064        buttons.forEach((button, i) => {
1065          button.classList.toggle("active", i === currentIndex);
1066        });
1067      }
1068      function nextSlide() {
1069        const nextIndex = (currentIndex + 1) % getSlides().length;
1070        showSlide(nextIndex);
1071      }
1072      function start() {
1073        if (intervalId) return;
1074        demo.classList.add("is-active-demo");
1075        intervalId = window.setInterval(() => {
1076          if (!isPausedByHover) {
1077            nextSlide();
1078          }
1079        }, interval);
1080      }
1081      function stop() {
1082        demo.classList.remove("is-active-demo");
1083        if (intervalId) {
1084          window.clearInterval(intervalId);
1085          intervalId = null;
1086        }
1087      }
1088      buttons.forEach((button, index) => {
1089        button.addEventListener("click", () => {
1090          showSlide(index);
1091        });
1092      });
1093      demo.addEventListener("mouseenter", () => {
1094        isPausedByHover = true;
1095      });
1096      demo.addEventListener("mouseleave", () => {
1097        isPausedByHover = false;
1098      });
1099      demo.addEventListener("focusin", () => {
1100        isPausedByHover = true;
1101      });
1102      demo.addEventListener("focusout", () => {
1103        isPausedByHover = false;
1104      });
1105      mobileQuery.addEventListener("change", () => showSlide(currentIndex));
1106      showSlide(0);
1107      return {
1108        element: demo,
1109        start,
1110        stop
1111      };
1112    }
1113    function initGlyphSlideshowDemo(demo, settings = {}) {
1114      const slides = [...demo.querySelectorAll(".glyph-slide")];
1115      const slideButtons = [...demo.querySelectorAll(".glyph-slideshow-controls > button")];
1116      if (!slides.length) return null;
1117      let currentIndex = 0;
1118      let intervalId = null;
1119      let toggleTimeoutId = null;
1120      let isPausedByHover = false;
1121      const interval = settings.interval || 2400;
1122      const toggleDelay = settings.toggleDelay || interval / 2;
1123      function setGlyphFeature(slide, state) {
1124        const display = slide.querySelector(".glyph-display");
1125        const toggleButtons = [...slide.querySelectorAll(".glyph-toggle button")];
1126        if (!display) return;
1127        const feature = slide.dataset.feature;
1128        display.classList.toggle("feature-off", state === "off");
1129        display.classList.toggle("feature-on", state === "on");
1130        display.style.fontFeatureSettings = state === "on" && feature ? `"${feature}" 1` : "normal";
1131        toggleButtons.forEach((button) => {
1132          button.classList.toggle("active", button.dataset.state === state);
1133        });
1134      }
1135      function clearAutoToggle() {
1136        if (toggleTimeoutId) {
1137          window.clearTimeout(toggleTimeoutId);
1138          toggleTimeoutId = null;
1139        }
1140      }
1141      function scheduleAutoToggle() {
1142        clearAutoToggle();
1143        const activeSlide = slides[currentIndex];
1144        setGlyphFeature(activeSlide, "off");
1145        toggleTimeoutId = window.setTimeout(() => {
1146          if (!isPausedByHover) {
1147            setGlyphFeature(activeSlide, "on");
1148          }
1149        }, toggleDelay);
1150      }
1151      function showSlide(index) {
1152        currentIndex = index;
1153        slides.forEach((slide, i) => {
1154          slide.classList.toggle("active", i === currentIndex);
1155          if (i !== currentIndex) {
1156            setGlyphFeature(slide, "off");
1157          }
1158        });
1159        slideButtons.forEach((button, i) => {
1160          button.classList.toggle("active", i === currentIndex);
1161        });
1162        scheduleAutoToggle();
1163      }
1164      function nextSlide() {
1165        const nextIndex = (currentIndex + 1) % slides.length;
1166        showSlide(nextIndex);
1167      }
1168      slides.forEach((slide) => {
1169        const toggleButtons = [...slide.querySelectorAll(".glyph-toggle button")];
1170        toggleButtons.forEach((button) => {
1171          button.addEventListener("click", () => {
1172            clearAutoToggle();
1173            setGlyphFeature(slide, button.dataset.state);
1174          });
1175        });
1176        setGlyphFeature(slide, "off");
1177      });
1178      slideButtons.forEach((button, index) => {
1179        button.addEventListener("click", () => {
1180          showSlide(index);
1181        });
1182      });
1183      function start() {
1184        if (intervalId) return;
1185        demo.classList.add("is-active-demo");
1186        scheduleAutoToggle();
1187        intervalId = window.setInterval(() => {
1188          if (!isPausedByHover) {
1189            nextSlide();
1190          }
1191        }, interval);
1192      }
1193      function stop() {
1194        demo.classList.remove("is-active-demo");
1195        clearAutoToggle();
1196        if (intervalId) {
1197          window.clearInterval(intervalId);
1198          intervalId = null;
1199        }
1200      }
1201      demo.addEventListener("mouseenter", () => {
1202        isPausedByHover = true;
1203        clearAutoToggle();
1204      });
1205      demo.addEventListener("mouseleave", () => {
1206        isPausedByHover = false;
1207        scheduleAutoToggle();
1208      });
1209      demo.addEventListener("focusin", () => {
1210        isPausedByHover = true;
1211        clearAutoToggle();
1212      });
1213      demo.addEventListener("focusout", () => {
1214        isPausedByHover = false;
1215        scheduleAutoToggle();
1216      });
1217      showSlide(0);
1218      return {
1219        element: demo,
1220        start,
1221        stop
1222      };
1223    }
1224    const widthDemo = document.querySelector(".width-demo");
1225    const xheightDemo = document.querySelector(".xheight-demo");
1226    if (widthDemo) {
1227      const demo = initTypeDemo(widthDemo, {
1228        axis: "wdth",
1229        stops: [75, 82, 100],
1230        speed: 0.4,
1231        xhgt: 353,
1232        wght: 400,
1233        wdth: 75
1234      });
1235      if (demo) featureDemos.push(demo);
1236    }
1237    if (xheightDemo) {
1238      const demo = initTypeDemo(xheightDemo, {
1239        axis: "XHGT",
1240        stops: [353, 574],
1241        speed: 4,
1242        xhgt: 353,
1243        wght: 400,
1244        wdth: 100,
1245        // adjust these if the guide line feels visually off
1246        baselineY: 205,
1247        lowGuideY: 100,
1248        tallGuideY: 35
1249      });
1250      if (demo) featureDemos.push(demo);
1251    }
1252    const demoObserver = new IntersectionObserver(
1253      (entries) => {
1254        entries.forEach((entry) => {
1255          const matchingDemo = featureDemos.find(
1256            (demo) => demo.element === entry.target
1257          );
1258          if (!matchingDemo) return;
1259          if (entry.isIntersecting) {
1260            matchingDemo.start();
1261          } else {
1262            matchingDemo.stop();
1263          }
1264        });
1265      },
1266      {
1267        threshold: 0.35,
1268        rootMargin: "-10% 0px -10% 0px"
1269      }
1270    );
1271    const paragraphDemo = document.querySelector(".paragraph-demo");
1272    if (paragraphDemo) {
1273      const demo = initParagraphDemo(paragraphDemo, {
1274        fadeDuration: 600,
1275        holdDuration: 2e3,
1276        states: [
1277          {
1278            label: "Text",
1279            fontFamily: "'DunbarSeriesVF-mini'",
1280            fontVariationSettings: "var(--DunbarTextRegular_mini)"
1281          },
1282          {
1283            label: "Low",
1284            fontFamily: "'DunbarSeriesVF-mini'",
1285            fontVariationSettings: "var(--DunbarTextRegularUnadjusted_mini)"
1286          }
1287        ]
1288      });
1289      if (demo) featureDemos.push(demo);
1290    }
1291    const svgSlideshowDemo = document.querySelector(".svg-slideshow-demo");
1292    if (svgSlideshowDemo) {
1293      const demo = initSvgSlideshowDemo(svgSlideshowDemo, {
1294        interval: 1400
1295      });
1296      if (demo) featureDemos.push(demo);
1297    }
1298    const xheightCapsDemo = document.querySelector(".xheight-caps-demo");
1299    if (xheightCapsDemo) {
1300      const demo = initTypeDemo(xheightCapsDemo, {
1301        axis: "XHGT",
1302        stops: [574, 353],
1303        speed: 4,
1304        xhgt: 574,
1305        wght: 400,
1306        wdth: 100,
1307        baselineY: 205,
1308        lowGuideY: 145,
1309        tallGuideY: 85
1310      });
1311      if (demo) featureDemos.push(demo);
1312    }
1313    const glyphSlideshowDemo = document.querySelector(".glyph-slideshow-demo");
1314    if (glyphSlideshowDemo) {
1315      const demo = initGlyphSlideshowDemo(glyphSlideshowDemo, {
1316        interval: 2600,
1317        toggleDelay: 1300
1318      });
1319      if (demo) featureDemos.push(demo);
1320    }
1321    featureDemos.forEach((demo) => {
1322      demoObserver.observe(demo.element);
1323    });
1324    window.featureAnimations = {
1325      stopAll() {
1326        featureDemos.forEach((demo) => demo.stop());
1327      }
1328    };
1329  });
1330
1331  
1331// dunbar-design-repository/assets/js/timeline.js
1332  document.addEventListener("DOMContentLoaded", () => {
1333    const markers = [...document.querySelectorAll(".timeline-entry-marker[data-target]")];
1334    const sectionMarkers = [...document.querySelectorAll(".section-marker[data-section-target]")];
1335    const panels = [...document.querySelectorAll(".timeline-content > .timeline-panel[data-slug]")];
1336    const sections = [...document.querySelectorAll(".timeline-section[data-slug]")];
1337    const timelineTester = document.querySelector("#timeline .tester");
1338    const timelineContent = document.querySelector("#timeline .timeline-content");
1339    const commitMarkers = [...document.querySelectorAll(".timeline-marker--commit")];
1340    const mobileTimelineQuery = window.matchMedia("(max-width: 850px)");
1341    const body = document.body;
1342    const intro = document.getElementById("intro");
1343    const feature = document.getElementById("feature");
1344    const article = document.getElementById("article");
1345    const timeline = document.getElementById("timeline");
1346    const footer = document.getElementById("footer");
1347    if (!window.location.hash) {
1348      if ("scrollRestoration" in history) history.scrollRestoration = "manual";
1349      window.scrollTo({ top: 0, left: 0, behavior: "auto" });
1350    }
1351    const scrollTopTriggers = [
1352      ...document.querySelectorAll(".btn-top, #site-title")
1353    ];
1354    const sectionHashIds = /* @__PURE__ */ new Set(["intro", "feature", "article", "timeline", "colophon"]);
1355    let activeSectionId = "";
1356    scrollTopTriggers.forEach((trigger) => {
1357      trigger.addEventListener("click", (event) => {
1358        event.preventDefault();
1359        window.scrollTo({
1360          top: 0,
1361          behavior: "smooth"
1362        });
1363      });
1364    });
1365    function getActiveSectionHash() {
1366      const hashSectionId = activeSectionId === "footer" ? "colophon" : activeSectionId;
1367      const sectionId = sectionHashIds.has(hashSectionId) ? hashSectionId : "article";
1368      return sectionHashIds.has(sectionId) ? `#${sectionId}` : "#intro";
1369    }
1370    function updateSectionHash(sectionId, method = "replaceState") {
1371      const hashSectionId = sectionId === "footer" ? "colophon" : sectionId;
1372      if (!sectionHashIds.has(hashSectionId)) return;
1373      if (document.body.classList.contains("lightbox-open") || document.body.classList.contains("lightbox-open")) return;
1374      const hash = `#${hashSectionId}`;
1375      if (window.location.hash === hash) return;
1376      history[method]("", document.title, hash);
1377    }
1378    function syncSectionHashFromBody() {
1379      const sectionId = [...sectionHashIds].find((id) => body.classList.contains(`section-${id}`));
1380      if (sectionId) updateSectionHash(sectionId);
1381    }
1382    function setActiveSectionHash(method = "pushState") {
1383      const hash = getActiveSectionHash();
1384      if (window.location.hash === hash) return;
1385      history[method]("", document.title, hash);
1386    }
1387    if (!markers.length || !panels.length || !sections.length) return;
1388    let introInView = false;
1389    let featureInView = false;
1390    let articleInView = false;
1391    let timelineInView = false;
1392    let footerInView = false;
1393    let sectionStateFrame = null;
1394    let commitScrollFrame = null;
1395    function updateScrollActiveCommit() {
1396      commitScrollFrame = null;
1397      if (!mobileTimelineQuery.matches || !body.classList.contains("timeline-list-in-view")) return;
1398      if (!timelineContent || !commitMarkers.length) return;
1399      const rect = timelineContent.getBoundingClientRect();
1400      const viewportAnchor = window.innerHeight * 0.45;
1401      const progress = Math.min(1, Math.max(0, (viewportAnchor - rect.top) / Math.max(rect.height, 1)));
1402      const activeIndex = Math.round(progress * (commitMarkers.length - 1));
1403      commitMarkers.forEach((marker, index) => {
1404        marker.classList.toggle("scroll-active", index === activeIndex);
1405      });
1406    }
1407    function scheduleScrollActiveCommit() {
1408      if (commitScrollFrame) return;
1409      commitScrollFrame = window.requestAnimationFrame(updateScrollActiveCommit);
1410    }
1411    function setActive(slug) {
1412      markers.forEach((marker) => {
1413        marker.classList.toggle("active", marker.dataset.target === slug);
1414        marker.classList.remove("show-all");
1415      });
1416      panels.forEach((panel) => {
1417        panel.classList.toggle("active", panel.dataset.slug === slug);
1418        panel.classList.remove("show-all");
1419      });
1420    }
1421    function clearActive() {
1422      markers.forEach((marker) => marker.classList.remove("active"));
1423      panels.forEach((panel) => panel.classList.remove("active"));
1424    }
1425    function showAllTimeline() {
1426      markers.forEach((marker) => {
1427        marker.classList.remove("active");
1428        marker.classList.add("show-all");
1429      });
1430      panels.forEach((panel) => {
1431        panel.classList.remove("active");
1432        panel.classList.add("show-all");
1433      });
1434    }
1435    function clearShowAllTimeline() {
1436      markers.forEach((marker) => marker.classList.remove("show-all"));
1437      panels.forEach((panel) => panel.classList.remove("show-all"));
1438    }
1439    function setSectionNavActive(sectionId) {
1440      sectionMarkers.forEach((marker) => {
1441        marker.classList.toggle(
1442          "active",
1443          marker.dataset.sectionTarget === sectionId
1444        );
1445      });
1446      body.classList.toggle("timeline-expanded", sectionId === "timeline");
1447      body.classList.toggle("timeline-folded", sectionId !== "timeline");
1448      if (sectionId !== "timeline") {
1449        clearActive();
1450      }
1451    }
1452    function setMode(mode) {
1453      const sectionId = mode.replace("section-", "");
1454      if (body.classList.contains(mode)) {
1455        activeSectionId = sectionId;
1456        updateSectionHash(sectionId);
1457        return;
1458      }
1459      body.classList.remove("section-intro", "section-feature", "section-article", "section-timeline", "section-footer");
1460      if (mode) body.classList.add(mode);
1461      activeSectionId = sectionId;
1462      setSectionNavActive(sectionId);
1463      updateSectionHash(sectionId);
1464    }
1465    function isSectionCurrent(element) {
1466      if (!element) return false;
1467      const rect = element.getBoundingClientRect();
1468      const targetY = window.innerHeight * 0.45;
1469      return rect.top <= targetY && rect.bottom > targetY;
1470    }
1471    function updateFooterInView() {
1472      if (!footer) {
1473        footerInView = false;
1474        return false;
1475      }
1476      const rect = footer.getBoundingClientRect();
1477      const enterFooter = rect.top < window.innerHeight && rect.bottom > 0;
1478      const stayInFooter = rect.top < window.innerHeight + 96 && rect.bottom > -96;
1479      footerInView = footerInView ? stayInFooter : enterFooter;
1480      return footerInView;
1481    }
1482    function resolveArticleTimelineMode() {
1483      sectionStateFrame = null;
1484      if (introInView || featureInView) return;
1485      if (updateFooterInView()) {
1486        setMode("section-footer");
1487        return;
1488      }
1489      timelineInView = isSectionCurrent(timeline);
1490      if (isSectionCurrent(article)) {
1491        setMode("section-article");
1492        showAllTimeline();
1493        clearActive();
1494        return;
1495      }
1496      if (timelineInView) {
1497        setMode("section-timeline");
1498      }
1499    }
1500    function scheduleArticleTimelineMode() {
1501      if (sectionStateFrame) return;
1502      sectionStateFrame = window.requestAnimationFrame(() => {
1503        resolveArticleTimelineMode();
1504        syncSectionHashFromBody();
1505      });
1506    }
1507    function getFirstTimelineSlug() {
1508      return sections[0]?.dataset.slug;
1509    }
1510    sectionMarkers.forEach((marker) => {
1511      marker.addEventListener("click", (event) => {
1512        event.preventDefault();
1513        const id = marker.dataset.sectionTarget;
1514        const target = document.getElementById(id);
1515        if (!target) return;
1516        if (id === "timeline") {
1517          setMode("section-timeline");
1518          const firstSlug = getFirstTimelineSlug();
1519          if (firstSlug) {
1520            setActive(firstSlug);
1521            scrollToSlug(firstSlug);
1522          }
1523          return;
1524        }
1525        window.scrollTo({
1526          top: target.offsetTop - 80,
1527          behavior: "smooth"
1528        });
1529      });
1530    });
1531    function scrollToSlug(slug, behavior = "smooth") {
1532      const section = document.querySelector(`.timeline-section[data-slug="${slug}"]`);
1533      if (!section) return;
1534      const top = section.getBoundingClientRect().top + window.scrollY - window.innerHeight * 0.35;
1535      window.scrollTo({
1536        top,
1537        behavior
1538      });
1539    }
1540    markers.forEach((marker) => {
1541      marker.addEventListener("click", (event) => {
1542        event.preventDefault();
1543        event.stopPropagation();
1544        setMode("section-timeline");
1545        setActive(marker.dataset.target);
1546        scrollToSlug(marker.dataset.target);
1547      });
1548      marker.addEventListener("keydown", (event) => {
1549        if (event.key === "Enter" || event.key === " ") {
1550          event.preventDefault();
1551          setMode("section-timeline");
1552          setActive(marker.dataset.target);
1553          scrollToSlug(marker.dataset.target);
1554        }
1555      });
1556    });
1557    const sectionObserver = new IntersectionObserver(
1558      (entries) => {
1559        if (introInView || featureInView || updateFooterInView() || isSectionCurrent(article)) return;
1560        setMode("section-timeline");
1561        const visible = entries.filter((entry) => entry.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio);
1562        if (!visible.length) return;
1563        clearShowAllTimeline();
1564        setActive(visible[0].target.dataset.slug);
1565      },
1566      {
1567        rootMargin: "-35% 0px -35% 0px",
1568        threshold: [0.25, 0.5, 0.75]
1569      }
1570    );
1571    sections.forEach((section) => sectionObserver.observe(section));
1572    if (timeline) {
1573      const timelineObserver = new IntersectionObserver(
1574        (entries) => {
1575          if (updateFooterInView()) return;
1576          timelineInView = entries[0].isIntersecting && isSectionCurrent(timeline);
1577          if (timelineInView && !introInView && !featureInView && !footerInView) {
1578            setMode("section-timeline");
1579          }
1580        },
1581        { threshold: 0.01 }
1582      );
1583      timelineObserver.observe(timeline);
1584    }
1585    if (timelineContent) {
1586      const timelineListObserver = new IntersectionObserver(
1587        (entries) => {
1588          const isVisible = entries[0].isIntersecting;
1589          body.classList.toggle("timeline-list-in-view", isVisible);
1590          if (isVisible) {
1591            scheduleScrollActiveCommit();
1592          } else {
1593            commitMarkers.forEach((marker) => marker.classList.remove("scroll-active"));
1594          }
1595        },
1596        { threshold: 0.01 }
1597      );
1598      timelineListObserver.observe(timelineContent);
1599    }
1600    if (timelineTester) {
1601      const timelineTesterObserver = new IntersectionObserver(
1602        (entries) => {
1603          body.classList.toggle("timeline-tester-in-view", entries[0].isIntersecting);
1604        },
1605        { threshold: 0.01 }
1606      );
1607      timelineTesterObserver.observe(timelineTester);
1608    }
1609    window.addEventListener("scroll", scheduleArticleTimelineMode, { passive: true });
1610    window.addEventListener("scroll", scheduleScrollActiveCommit, { passive: true });
1611    window.addEventListener("resize", scheduleArticleTimelineMode);
1612    window.addEventListener("resize", scheduleScrollActiveCommit);
1613    mobileTimelineQuery.addEventListener("change", () => {
1614      if (!mobileTimelineQuery.matches) {
1615        body.classList.remove("timeline-list-in-view");
1616        commitMarkers.forEach((marker) => marker.classList.remove("scroll-active"));
1617      }
1618    });
1619    if (intro) {
1620      const introObserver = new IntersectionObserver(
1621        (entries) => {
1622          introInView = entries[0].isIntersecting;
1623          body.classList.toggle("section-intro", introInView);
1624          if (introInView) {
1625            setMode("section-intro");
1626            clearShowAllTimeline();
1627            clearActive();
1628          } else {
1629            body.classList.remove("section-intro");
1630            scheduleArticleTimelineMode();
1631          }
1632        },
1633        { threshold: 0.01 }
1634      );
1635      introObserver.observe(intro);
1636    }
1637    if (feature) {
1638      const featureObserver = new IntersectionObserver(
1639        (entries) => {
1640          featureInView = entries[0].isIntersecting;
1641          if (featureInView) {
1642            setMode("section-feature");
1643            showAllTimeline();
1644            clearActive();
1645          } else {
1646            clearShowAllTimeline();
1647            if (window.featureAnimations) {
1648              window.featureAnimations.stopAll();
1649            }
1650            scheduleArticleTimelineMode();
1651          }
1652        },
1653        { threshold: 0.01 }
1654      );
1655      featureObserver.observe(feature);
1656    }
1657    if (article) {
1658      const articleObserver = new IntersectionObserver(
1659        (entries) => {
1660          articleInView = entries[0].isIntersecting;
1661          if (articleInView && isSectionCurrent(article)) {
1662            setMode("section-article");
1663            showAllTimeline();
1664            clearActive();
1665          } else {
1666            clearShowAllTimeline();
1667          }
1668        },
1669        { threshold: 0.01 }
1670      );
1671      articleObserver.observe(article);
1672    }
1673    if (footer) {
1674      const footerObserver = new IntersectionObserver(
1675        () => {
1676          if (updateFooterInView()) {
1677            setMode("section-footer");
1678            return;
1679          }
1680          scheduleArticleTimelineMode();
1681        },
1682        { threshold: 0.01 }
1683      );
1684      footerObserver.observe(footer);
1685    }
1686    function openSectionFromHash() {
1687      const hashId = window.location.hash.replace("#", "");
1688      const timelineMarker = markers.find((marker) => marker.dataset.target === hashId);
1689      if (timelineMarker) {
1690        setMode("section-timeline");
1691        setActive(hashId);
1692        requestAnimationFrame(() => scrollToSlug(hashId, "auto"));
1693        return;
1694      }
1695      const sectionId = hashId;
1696      if (!sectionHashIds.has(sectionId)) return;
1697      const targetId = sectionId === "colophon" ? "footer" : sectionId;
1698      const target = document.getElementById(targetId);
1699      if (!target) return;
1700      setMode(`section-${targetId}`);
1701      window.scrollTo({
1702        top: targetId === "timeline" ? target.offsetTop : target.offsetTop - 80,
1703        behavior: "auto"
1704      });
1705    }
1706    function handleHashState() {
1707      openSectionFromHash();
1708    }
1709    window.addEventListener("hashchange", handleHashState);
1710    window.addEventListener("popstate", handleHashState);
1711    handleHashState();
1712  });
1713
1714  
1714// dunbar-design-repository/assets/js/main.js
1715  console.log("main.js loaded");
1716  window.addEventListener("load", () => {
1717  });
1718})();
1719//# sourceMappingURL=bundle.js.map

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.