PageSourceSearch

https://astrologicalmagazine.com/preview2/preview2.js?v=26-vastu

js astrologicalmagazine.com collected 2026-10-02 13:18:33 UTC 28,440 bytes, 592 lines download raw bytes

1/* =====================================================================
2 * PREVIEW 2 - news-portal front page behaviour (theage.com.au design
3 * language, TAeM tokens). A renamed adaptation of preview.js: same data
4 * sources and mechanics, [data-p2] selectors, plus a centre medium-card
5 * stack (renderStack) and a "Just in" cadence list (renderJustIn).
6 *
7 * Sources (identical to the live components, so nothing can drift):
8 *   - nav + instruments:   window.TAEM_NAV_ITEMS  (taem-header-footer.js)
9 *   - lead + stack + edition: window.__TAEM_ISSUES__ (issues-data.js)
10 *   - search: /search.js lazy-loaded, window.TAEM_SEARCH.open()
11 * Also: dropdown wiring, '/' shortcut, sticky-compact masthead, skip link.
12 * ===================================================================== */
13(function () {
14    'use strict';
15
16    var VIEWER = 'digitalviewer/index.html';
17    var SEARCH_SRC = '/search.js?v=20260713-w2';
18
19    function el(name) { return document.querySelector('[data-p2="' + name + '"]'); }
20    function esc(s) {
21        return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
22            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
23        });
24    }
25    function setText(name, value) {
26        var node = el(name);
27        if (node && value) node.textContent = value;
28    }
29
30    /* ── Issue data (same gate as the renderer: released once releaseDate
31       has passed; issues-data.js lists newest first) ──────────────────── */
32    function releasedIssues() {
33        var data = window.__TAEM_ISSUES__;
34        if (!data || !Array.isArray(data.issues)) return [];
35        // Local date, not UTC: toISOString() would gate on the UTC day, which
36        // holds a new edition back for a reader east of UTC (and releases it
37        // early to the west). Matches isReleased() in taem-issues-renderer.js.
38        var now = new Date();
39        var today = now.getFullYear() + '-'
40            + String(now.getMonth() + 1).padStart(2, '0') + '-'
41            + String(now.getDate()).padStart(2, '0');
42        return data.issues.filter(function (issue) {
43            if (issue.format && issue.format !== 'digital') return false;
44            var gate = issue.releaseDate || issue.date;
45            return !gate || gate <= today;
46        });
47    }
48
49    function itemHref(item) {
50        if (item.href) return item.href;
51        if (item.slug) return VIEWER + '?article=' + encodeURIComponent(item.slug);
52        if (item.videoId) return 'https://www.youtube.com/watch?v=' + encodeURIComponent(item.videoId);
53        return null;
54    }
55
56    // The Foundations hub-callout card rides in every issue's content list and
57    // has its own promo slot on this page, so it is excluded here.
58    function readable(issue) {
59        return (issue.content || []).filter(function (item) {
60            if (item.type !== 'pdf' && item.type !== 'article') return false;
61            return !/sun-series\/index\.html$/.test(item.href || '');
62        });
63    }
64
65    // What the centre stack may show. Wider than readable(): videos belong in
66    // the stack but must never be picked as an edition's cover story, which is
67    // why lead selection still runs off readable().
68    function stackable(issue) {
69        return (issue.content || []).filter(function (item) {
70            if (item.type !== 'pdf' && item.type !== 'article' && item.type !== 'youtube') return false;
71            return !/sun-series\/index\.html$/.test(item.href || '');
72        });
73    }
74
75    /* ── Lead card (the dominant cover story, content[0]) ────────────── */
76    function renderLead(issue, items) {
77        var lead = items[0];
78        var href = itemHref(lead) || 'previous-issues.html';
79
80        setText('category', lead.category);
81        setText('issue', issue.title);
82        setText('time', lead.estimatedTime);
83
84        // [2026-07-28] audienceLevel no longer prints in the kicker. It gave
85        // every article two captions - a grey level ("Intermediate", "All
86        // levels") followed by the blue category ("Jyotisa Lessons", "Chief
87        // Editor's Column") - which read as one label repeated rather than two
88        // facts. The category is the one that tells a reader what the piece is.
89        // audienceLevel still lives in the issue data and on the article pages.
90
91        var title = el('title');
92        if (title) title.innerHTML = '<a href="' + esc(href) + '">' + esc(lead.title) + '</a>';
93
94        var cover = el('coverimg');
95        var coverLink = el('coverlink');
96        if (cover && issue.cover) {
97            cover.src = issue.cover;
98            cover.alt = issue.title + ' cover of The Astrological eMagazine';
99        }
100        if (coverLink) coverLink.href = VIEWER + '?issue=' + encodeURIComponent(issue.id);
101
102        var orientation = el('orientation');
103        if (orientation) {
104            orientation.textContent =
105                lead.orientationText || lead.description || issue.readerIntro || '';
106        }
107
108        // The gloss prints only when it adds something the paragraph above lacks.
109        var gloss = el('gloss');
110        if (gloss) {
111            var above = orientation ? orientation.textContent : '';
112            var candidate = lead.glossaryHint || lead.description || '';
113            gloss.textContent = candidate && candidate !== above ? candidate : '';
114        }
115
116        // Byline node is omitted entirely when there is no real author.
117        var byline = el('byline');
118        if (byline) {
119            if (lead.author) {
120                setText('author', lead.author);
121                byline.style.display = '';
122            } else {
123                byline.style.display = 'none';
124            }
125        }
126
127        var cta = el('cta');
128        if (cta) cta.href = href;
129
130        // Optional second route: a plain HTML page alongside the viewer. Only
131        // items carrying htmlUrl get it -- the node stays hidden otherwise, the
132        // same way the byline node does above, so an issue without one renders
133        // exactly as it always has.
134        var ctaHtml = el('cta-html');
135        if (ctaHtml) {
136            var alt = typeof lead.htmlUrl === 'string' ? lead.htmlUrl.trim() : '';
137            if (alt) {
138                ctaHtml.href = alt;
139                ctaHtml.style.display = '';
140            } else {
141                ctaHtml.removeAttribute('href');
142                ctaHtml.style.display = 'none';
143            }
144        }
145    }
146
147    /* ── Centre medium-card stack (content[1..], spec §2.2 variant B) ─── */
148
149    // How many earlier pieces an edition may carry into its stack.
150    var STACK_CARRY_COUNT = 5;
151
152    function stackCard(item, i) {
153        var href = itemHref(item);
154        var sizeClass = i === 0 ? ' p2-card--med-l' : '';
155        var standfirst = item.orientationText || item.description || '';
156        var titleInner = href
157            ? '<a href="' + esc(href) + '">' + esc(item.title) + '</a>'
158            : esc(item.title);
159        // Category only - see renderLead() for why audienceLevel was dropped.
160        return '<article class="p2-card p2-card--med' + sizeClass + '" data-s="lead.stack.item.' + (i + 1) + '">' +
161            '<p class="p2-card__kicker">' +
162            '<span class="p2-card__cat">' + esc(item.category || 'Article') + '</span></p>' +
163            '<h3 class="p2-card__title">' + titleInner + '</h3>' +
164            (standfirst ? '<p class="p2-card__standfirst">' + esc(standfirst) + '</p>' : '') +
165            (item.author ? '<p class="p2-card__byline">' + esc(item.author) + '</p>' : '') +
166            '</article>';
167    }
168
169    // Resolve one carryForward reference - a slug or an href - to its item,
170    // wherever in the released editions it lives.
171    function findByRef(issues, ref) {
172        for (var i = 0; i < issues.length; i++) {
173            var items = readable(issues[i]);
174            for (var j = 0; j < items.length; j++) {
175                if (items[j].slug === ref || items[j].href === ref) return items[j];
176            }
177        }
178        return null;
179    }
180
181    // The earlier pieces an edition carries into its stack. An explicit
182    // carryForward list in the issue data wins - that is an editorial choice,
183    // not a guess. Without one, the freshest readable pieces behind it.
184    function carrySelection(issues, idx, limit) {
185        var out = [];
186        var picks = (issues[idx] || {}).carryForward;
187        if (Array.isArray(picks) && picks.length) {
188            for (var p = 0; p < picks.length && out.length < limit; p++) {
189                var hit = findByRef(issues, picks[p]);
190                if (hit) out.push(hit);
191            }
192            return out;
193        }
194        for (var i = idx + 1; i < issues.length && out.length < limit; i++) {
195            var earlier = readable(issues[i]);
196            for (var j = 0; j < earlier.length && out.length < limit; j++) out.push(earlier[j]);
197        }
198        return out;
199    }
200
201    function renderStack(issue, items, issues, idx) {
202        var stack = el('stack');
203        if (!stack) return;
204        // Everything the edition carries except its own cover story (the
205        // duplicate "The edition" band was removed), so the stack is uncapped.
206        var lead = items[0];
207        var rest = stackable(issue).filter(function (item) { return item !== lead; });
208        // Carried pieces sit beneath the edition's own, so an edition that is
209        // still filling out never leaves this column short.
210        var carried = carrySelection(issues || [], idx || 0, STACK_CARRY_COUNT);
211        if (!rest.length && !carried.length) {
212            stack.innerHTML = '<p class="p2-note">The rest of this edition is being released.</p>';
213            return;
214        }
215        var html = rest.map(function (item, i) {
216            return stackCard(item, i);
217        }).join('');
218        if (carried.length) {
219            html += '<p class="p2-note p2-note--divider">From recent editions</p>' +
220                carried.map(function (item, i) {
221                    return stackCard(item, rest.length + i);
222                }).join('');
223        }
224        stack.innerHTML = html;
225    }
226
227    // Kept for parity with preview.js; preview2 has no toc rail, so this is a
228    // no-op unless a [data-p2="toc"] node is present.
229    function renderToc(items) {
230        var toc = el('toc');
231        if (!toc) return;
232        var rest = items.slice(1, 8);
233        if (!rest.length) {
234            toc.innerHTML = '<p class="p2-note">The rest of this edition is being released.</p>';
235            return;
236        }
237        toc.innerHTML = rest.map(function (item) {
238            var href = itemHref(item);
239            var body =
240                '<span class="p2-card__cat">' + esc(item.category || 'Article') + '</span>' +
241                '<span class="p2-card__title">' + esc(item.title) + '</span>' +
242                (item.author ? '<span class="p2-card__byline">' + esc(item.author) + '</span>' : '');
243            return href
244                ? '<a class="p2-card p2-card--rail" href="' + esc(href) + '">' + body + '</a>'
245                : '<div class="p2-card p2-card--rail">' + body + '</div>';
246        }).join('');
247    }
248
249    /* ── "Just in": three freshest real things, edition/label chips only,
250       never a clock time (the brief forbids fabricated timestamps) ──── */
251    function renderJustIn(issue, items) {
252        var box = el('justin');
253        if (!box) return;
254
255        var seq = 0;
256        function row(cat, title, href, chip) {
257            seq += 1;
258            return '<a class="p2-card p2-card--rail" data-s="justin.item.' + seq + '" href="' + esc(href) + '">' +
259                '<p class="p2-card__kicker"><span class="p2-card__cat">' + esc(cat) + '</span></p>' +
260                '<h4 class="p2-card__title">' + esc(title) + '</h4>' +
261                '<span class="p2-card__chip">' + esc(chip) + '</span>' +
262                '</a>';
263        }
264
265        var rows = [];
266        rows.push(row('Transits', 'The Week in Jyotiá¹£a', 'weekly/index.html', 'Friday briefing'));
267
268        var sun = items.filter(function (i) { return /sun-series\/sun-/.test(i.href || ''); })[0];
269        if (sun) {
270            rows.push(row('Foundations of Jyotisha', sun.title, itemHref(sun), 'Foundations'));
271        } else {
272            rows.push(row('Foundations of Jyotisha', 'The Sun Through the Houses',
273                'sun-series/index.html', 'Foundations'));
274        }
275
276        rows.push(row('Digital edition', issue.title,
277            VIEWER + '?issue=' + encodeURIComponent(issue.id), issue.title + ' edition'));
278
279        box.innerHTML = rows.join('');
280    }
281
282    // The edition line derives from the data, never the calendar, so the
283    // masthead can never disagree with the edition rendered below it.
284    // (The topline now carries only the date; these are no-ops unless the
285    // slots return, kept so restoring the markup needs no JS change.)
286    function renderEdition(issue) {
287        var node = el('edition');
288        if (node && issue) node.textContent = issue.title + ' edition';
289        var read = el('read-edition');
290        if (read && issue) read.href = VIEWER + '?issue=' + encodeURIComponent(issue.id);
291    }
292
293    /* ── Vastu: the Chief Editor's standing monthly department. Latest
294       linkable Vastu pieces from released issues, newest edition first. ── */
295    function renderVastu(issues) {
296        var box = el('vastu');
297        if (!box) return;
298        var picks = [];
299        issues.forEach(function (issue) {
300            (issue.content || []).forEach(function (item) {
301                if (picks.length >= 4) return;
302                // A Vastu piece names Vastu in its title, or is the Chief Editor's
303                // column and names it in its description (a column titled for its
304                // subject, e.g. "Choosing Between Two Apartments").
305                var vastuTitle = /v[āa]stu/i.test(item.title || '');
306                var vastuColumn = /chief editor/i.test(item.category || '') && /v[āa]stu/i.test(item.description || '');
307                if (!vastuTitle && !vastuColumn) return;
308                var href = null, video = false;
309                if (item.type === 'pdf' || item.type === 'article') {
310                    href = itemHref(item);
311                } else if (item.type === 'youtube' && item.videoId) {
312                    // The Chief Editor's video classes carry a videoId, not a href.
313                    href = 'https://www.youtube.com/watch?v=' + encodeURIComponent(item.videoId);
314                    video = true;
315                }
316                if (!href) return;
317                picks.push({ href: href, title: item.title, chip: issue.title, video: video });
318            });
319        });
320        if (!picks.length) { box.innerHTML = ''; return; }
321        box.innerHTML = picks.map(function (p, i) {
322            var status = p.video ? '<span class="p2-card__status">Video</span>' : '';
323            var ext = p.video ? ' target="_blank" rel="noopener"' : '';
324            return '<a class="p2-card p2-card--sm p2-guidecard" data-s="vastu.item.' + (i + 1) + '" href="' + esc(p.href) + '"' + ext +
325                ' data-taem-cta="vastu_department" data-taem-cta-type="' + (p.video ? 'video' : 'article') + '">' +
326                '<p class="p2-card__kicker">' + status + '<span class="p2-card__cat">Vastu</span></p>' +
327                '<h3 class="p2-card__title">' + esc(p.title) + '</h3>' +
328                '<span class="p2-card__chip">' + esc(p.chip) + '</span>' +
329                '</a>';
330        }).join('');
331    }
332
333    /* Mirror of slug_for() in generate_issue_pages.py. Every issue in
334       content/issues.json has a static page at /issues/<slug>.html, and the
335       slug comes from the TITLE, not the id: "July 2026" -> july-2026, whereas
336       the id is abbreviated ("jul-2026"). If the Python changes, so must this. */
337    function issuePageHref(issue) {
338        var slug = String((issue && issue.title) || '')
339            .toLowerCase()
340            .replace(/[^a-z0-9]+/g, '-')
341            .replace(/^-+|-+$/g, '');
342        return slug ? '/issues/' + slug + '.html' : '/issues/';
343    }
344
345    /* ── Archive: every released digital edition (including the current one),
346       as a cover strip above the print grid.
347       Covers link to the edition's own page, not straight into the viewer:
348       /issues/<slug>.html is the indexable table of contents and carries the
349       "read this issue" CTA into the viewer, so nothing is lost and the
350       destination is linkable and shareable. ── */
351    function renderEditions(issues) {
352        var box = el('editions');
353        if (!box) return;
354        // Every released digital edition, in a 7-column grid at desktop; editions
355        // beyond the first seven wrap onto the next row.
356        box.innerHTML = issues.map(function (issue, i) {
357            var cover = issue.cover
358                ? '<img class="p2-editions__img" loading="lazy" alt="' + esc(issue.title) + ' cover" src="' + esc(issue.cover) + '">'
359                : '';
360            return '<a class="p2-editions__item" data-s="archive.editions.item.' + (i + 1) + '"' +
361                ' href="' + esc(issuePageHref(issue)) + '"' +
362                ' data-taem-cta="archive_digital_edition" data-taem-cta-type="issue">' +
363                cover +
364                '<span class="p2-editions__label">' + esc(issue.title) + '</span>' +
365                '</a>';
366        }).join('');
367    }
368
369    // The date line: British day-month order (e.g. "Tuesday, 21 July 2026").
370    // en-GB already yields "21 July 2026"; the weekday is prepended by the
371    // options object, giving the full string in one call.
372    function renderDate() {
373        var node = el('date');
374        if (!node) return;
375        try {
376            node.textContent = new Date().toLocaleDateString('en-GB',
377                { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
378        } catch (e) {
379            node.textContent = '';
380        }
381    }
382
383    /* ── Masthead nav from the live nav data ─────────────────────────── */
384    function renderNav() {
385        var nav = el('nav');
386        var items = window.TAEM_NAV_ITEMS;
387        if (!nav) return;
388        if (!Array.isArray(items) || !items.length) { nav.style.display = 'none'; return; }
389
390        nav.innerHTML = items.map(function (item, i) {
391            var sid = 'masthead.nav.item.' + (i + 1);
392            if (item.children && item.children.length) {
393                return '<div class="p2-nav-group" data-nav-group="' + i + '" data-s="' + sid + '">' +
394                    '<button type="button" class="p2-nav-toggle" aria-expanded="false" aria-haspopup="true">' +
395                    esc(item.label) + '<span class="p2-nav-caret" aria-hidden="true"></span></button>' +
396                    '<div class="p2-nav-panel">' +
397                    item.children.map(function (child, ci) {
398                        return '<a data-s="' + sid + '.child.' + (ci + 1) + '" href="' + esc(child.href) + '">' + esc(child.label) + '</a>';
399                    }).join('') +
400                    '</div></div>';
401            }
402            return '<a class="p2-nav-link" data-s="' + sid + '" href="' + esc(item.href) + '">' + esc(item.label) + '</a>';
403        }).join('');
404
405        // Click-to-toggle for touch and keyboard; hover still works via CSS.
406        var groups = nav.querySelectorAll('.p2-nav-group');
407        function closeAll(except) {
408            groups.forEach(function (g) {
409                if (g !== except) {
410                    g.classList.remove('open');
411                    g.querySelector('.p2-nav-toggle').setAttribute('aria-expanded', 'false');
412                }
413            });
414        }
415        groups.forEach(function (group) {
416            var toggle = group.querySelector('.p2-nav-toggle');
417            toggle.addEventListener('click', function (e) {
418                e.stopPropagation();
419                var opening = !group.classList.contains('open');
420                closeAll(opening ? group : null);
421                group.classList.toggle('open', opening);
422                toggle.setAttribute('aria-expanded', String(opening));
423            });
424        });
425        document.addEventListener('click', function () { closeAll(null); });
426        document.addEventListener('keydown', function (e) {
427            if (e.key === 'Escape') closeAll(null);
428        });
429    }
430
431    /* ── Instruments (rail + footer) from the Jyotisa Tools group ────── */
432    var INSTRUMENT_KINDS = {
433        'Monthly Panchanga': 'Almanac',
434        'Daily Panchanga': 'Almanac',
435        'Vedic Ephemeris': 'Tables',
436        'Default Horoscopes': 'Charts',
437        'Atmakaraka Calculator': 'Chara kāraka'
438    };
439    var INSTRUMENT_FALLBACK = [
440        { label: 'Monthly Panchanga', href: '/monthly-panchanga.html' },
441        { label: 'Daily Panchanga', href: '/panchanga.html' },
442        { label: 'Vedic Ephemeris', href: '/ephemeris.html' },
443        { label: 'Default Horoscopes', href: '/horoscope_embed_project/default_charts.html' },
444        { label: 'Atmakaraka Calculator', href: '/horoscope_embed_project/atmakaraka.html' }
445    ];
446
447    function toolItems() {
448        var items = window.TAEM_NAV_ITEMS;
449        if (Array.isArray(items)) {
450            for (var i = 0; i < items.length; i++) {
451                var label = (items[i].label || '').toLowerCase();
452                if (items[i].children && label.indexOf('tools') !== -1) return items[i].children;
453            }
454        }
455        return INSTRUMENT_FALLBACK;
456    }
457
458    function renderInstruments() {
459        var tools = toolItems();
460        var rail = el('instruments');
461        if (rail) {
462            rail.innerHTML = tools.map(function (tool) {
463                // The kind map keys are ASCII; fold diacritics before lookup.
464                var plain = tool.label.normalize('NFD').replace(/[̀-ͯ]/g, '');
465                var kind = INSTRUMENT_KINDS[plain] || 'Instrument';
466                return '<li><a href="' + esc(tool.href) + '" data-taem-cta="preview_instrument" data-taem-cta-type="tool">' +
467                    '<span>' + esc(tool.label) + '</span><em>' + esc(kind) + '</em></a></li>';
468            }).join('');
469        }
470        var footer = el('footer-instruments');
471        if (footer) {
472            footer.innerHTML = tools.map(function (tool) {
473                return '<li><a href="' + esc(tool.href) + '">' + esc(tool.label) + '</a></li>';
474            }).join('');
475        }
476    }
477
478    /* ── Search: lazy-load, then open (same pattern as the live header) ── */
479    function openSearch() {
480        if (window.TAEM_SEARCH && typeof window.TAEM_SEARCH.open === 'function') {
481            window.TAEM_SEARCH.open();
482            return;
483        }
484        var existing = document.getElementById('taem-search-script');
485        if (existing) return; // still loading; the load handler below will open
486        var s = document.createElement('script');
487        s.id = 'taem-search-script';
488        s.src = SEARCH_SRC;
489        s.onload = function () {
490            if (window.TAEM_SEARCH) window.TAEM_SEARCH.open();
491        };
492        document.head.appendChild(s);
493    }
494
495    /* ── Enhance the partial's details-based nav groups: opening one closes
496       its siblings, Escape closes everything including the mobile drawer.
497       (Generated issue pages skip this file and rely on native behavior.) ── */
498    function wireNavGroups() {
499        var groups = [].slice.call(document.querySelectorAll('details.p2-nav-group'));
500        groups.forEach(function (g) {
501            g.addEventListener('toggle', function () {
502                if (g.open) groups.forEach(function (o) { if (o !== g) o.open = false; });
503            });
504        });
505        document.addEventListener('keydown', function (e) {
506            if (e.key !== 'Escape') return;
507            groups.forEach(function (o) { o.open = false; });
508            var drawer = document.getElementById('p2-navdrawer-check');
509            if (drawer) drawer.checked = false;
510        });
511    }
512
513    function wireSearch() {
514        // The masthead search control is a plain <a href="/search.html"> in the
515        // shared partial (works with zero JS on generated pages); here it is
516        // upgraded to open the overlay instead.
517        var btn = document.getElementById('p2-search-btn');
518        if (btn) btn.addEventListener('click', function (e) {
519            e.preventDefault();
520            openSearch();
521        });
522        document.addEventListener('keydown', function (e) {
523            if (e.key !== '/') return;
524            var t = e.target;
525            if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
526                      t.tagName === 'SELECT' || t.isContentEditable)) return;
527            e.preventDefault();
528            openSearch();
529        });
530    }
531
532    /* ── Translate control ──────────────────────────────────────────────
533       [MOVED 2026-07-28] The wiring that lived here (restored 2026-07-26)
534       only ran on the homepage because only the homepage loads preview2.js,
535       which left the control permanently hidden on every other page. It now
536       lives in /taem-translate.js, loaded with defer by the masthead partial
537       itself, so every page carrying the chrome gets a working control and
538       a saved language is re-applied site-wide. Do not re-add wiring here. */
539
540    /* ── Sticky-compact masthead ──────────────────────────────────────
541       [MOVED 2026-07-28] Lived here, so it ran on the homepage only: every
542       other page kept a full-height masthead pinned over the viewport. It
543       now lives in /taem-translate.js, which the masthead partial loads on
544       every page, alongside the --p2-masthead-h publisher that page-level
545       sticky bars offset against. Do not re-add it here - two observers
546       would fight over the same class. */
547
548    /* ── Skip link (bare #fragments break under <base>) ──────────────── */
549    function wireSkipLink() {
550        var skip = document.getElementById('p2-skip');
551        var main = document.getElementById('main-content');
552        if (!skip || !main) return;
553        skip.addEventListener('click', function (e) {
554            e.preventDefault();
555            main.setAttribute('tabindex', '-1');
556            main.focus({ preventScroll: false });
557            main.scrollIntoView();
558        });
559    }
560
561    /* ── Boot ─────────────────────────────────────────────────────────── */
562    function start() {
563        renderDate();
564        renderNav();      // no-op now: the nav is baked into the shared partial
565        wireNavGroups();
566        renderInstruments();
567        wireSearch();
568        // translate is wired by /taem-translate.js (masthead partial), not here
569        // masthead compaction is wired by /taem-translate.js, not here
570        wireSkipLink();
571
572        var issues = releasedIssues();
573        renderVastu(issues);
574        renderEditions(issues);
575        for (var i = 0; i < issues.length; i++) {
576            var items = readable(issues[i]);
577            if (items.length) {
578                renderEdition(issues[i]);
579                renderLead(issues[i], items);
580                renderStack(issues[i], items, issues, i);
581                renderJustIn(issues[i], items);
582                return;
583            }
584        }
585    }
586
587    if (document.readyState === 'loading') {
588        document.addEventListener('DOMContentLoaded', start, { once: true });
589    } else {
590        start();
591    }
592})();

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.