PageSourceSearch

https://newsco.in/assets/js/main.js?v=48f2812a0c

js newsco.in collected 2026-10-04 08:09:46 UTC 67,902 bytes, 1,544 lines download raw bytes

1// The Masthead — main.js
2
3// Date display in top bar
4(function() {
5    const el = document.getElementById('current-date');
6    if (el) {
7        const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
8        const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
9        const now = new Date();
10        el.textContent = days[now.getDay()] + ', ' + now.getDate() + ' ' + months[now.getMonth()] + ' ' + now.getFullYear();
11    }
12})();
13
14// Mobile menu toggle
15(function() {
16    const btn = document.querySelector('.mobile-menu-btn');
17    const nav = document.querySelector('.nav-list');
18    if (btn && nav) {
19        btn.addEventListener('click', function() {
20            nav.classList.toggle('open');
21            btn.textContent = nav.classList.contains('open') ? '✕' : '☰';
22        });
23    }
24})();
25
26// Business Voices avatar initials — when a Q&A post's author has no profile
27// photo, post.hbs renders their full name into the small circular avatar as
28// a fallback. A full name doesn't fit a 42px circle, so this shrinks it down
29// to two letters (first + last initial) once the page loads.
30(function() {
31    document.querySelectorAll('.avatar-initials').forEach(function(el) {
32        const name = (el.getAttribute('data-fullname') || el.textContent || '').trim();
33        if (!name) return;
34        const parts = name.split(/\s+/);
35        const initials = parts[0].charAt(0) + (parts.length > 1 ? parts[parts.length - 1].charAt(0) : '');
36        el.textContent = initials.toUpperCase();
37    });
38})();
39
40// Ticker duplicate for seamless loop
41(function() {
42    const ticker = document.getElementById('ticker');
43    if (ticker) {
44        const clone = ticker.innerHTML;
45        ticker.innerHTML = clone + clone;
46    }
47})();
48
49// Newsco signup form — collects email, name, organisation, location, phone
50// (all but email + name are optional, by design) and posts to the Make.com
51// webhook, which tags the signup Business/Personal by email domain for
52// audience segmentation only (no domain is ever blocked), logs the row to
53// the audience sheet, creates the Ghost member, and triggers Ghost's native
54// magic-link sign-in email. See SIGNUP_AUTOMATION.md for the full pipeline.
55(function() {
56    const form = document.getElementById('newsco-signup-form');
57    if (!form) return;
58
59    const WEBHOOK_URL = 'https://hook.eu1.make.com/i9alnipcf2x7jl5p8qq73oj15q37wmzm';
60
61    const successEl = document.getElementById('signup-success');
62    const errorEl = document.getElementById('signup-error');
63    const submitBtn = form.querySelector('.signup-submit');
64
65    form.addEventListener('submit', function(e) {
66        e.preventDefault();
67        errorEl.style.display = 'none';
68        successEl.style.display = 'none';
69
70        const email = form.email.value.trim();
71        const name = form.name.value.trim();
72        if (!email || !name) {
73            errorEl.textContent = 'Please fill in your email and name.';
74            errorEl.style.display = 'block';
75            return;
76        }
77
78        const payload = {
79            email: email,
80            name: name,
81            organisation: form.organisation.value.trim(),
82            location: form.location.value.trim(),
83            phone: form.phone.value.trim(),
84            whatsapp_consent: form.whatsapp_consent.checked
85        };
86
87        submitBtn.disabled = true;
88        submitBtn.textContent = 'Submitting…';
89
90        fetch(WEBHOOK_URL, {
91            method: 'POST',
92            headers: { 'Content-Type': 'application/json' },
93            body: JSON.stringify(payload)
94        }).then(function(res) {
95            if (!res.ok) throw new Error('Request failed');
96            successEl.style.display = 'block';
97            form.reset();
98        }).catch(function() {
99            errorEl.textContent = 'Something went wrong. Please try again in a moment.';
100            errorEl.style.display = 'block';
101        }).finally(function() {
102            submitBtn.disabled = false;
103            submitBtn.textContent = "Subscribe — it's free";
104        });
105    });
106})();
107
108// Press release submission form — collects registrant + release details,
109// validates official-email-only and phone format client-side, enforces a
110// 150-word cap per paragraph (4 paragraph boxes max), and posts to the
111// Make.com webhook which
112// handles email verification, the human review queue, and fake-release
113// flagging. See PR_SUBMISSION.md for the full pipeline.
114(function() {
115    const form = document.getElementById('pr-submit-form');
116    if (!form) return;
117
118    const WEBHOOK_URL = 'https://hook.eu1.make.com/uz1lpr1lgy34sp3mu6hv2ibq19k1lnka';
119    const PERSONAL_EMAIL_DOMAINS = ['gmail.com', 'yahoo.com', 'yahoo.co.in', 'hotmail.com', 'outlook.com', 'live.com', 'aol.com', 'icloud.com', 'protonmail.com', 'rediffmail.com'];
120    const MAX_WORDS_PER_PARA = 150;
121    const PARA_COUNT = 4;
122    const MAX_WORDS = MAX_WORDS_PER_PARA * PARA_COUNT;
123    const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
124    // Not every contact submits a professionally-shot headshot, so this is
125    // a floor, not a quality guarantee: it catches the worst case (a tiny
126    // thumbnail or screenshot-of-a-screenshot that would look blurry or
127    // pixelated once placed in a 280px-wide article slot or homepage card),
128    // without rejecting normal phone-camera photos.
129    const MIN_IMAGE_WIDTH = 500;
130    const MIN_IMAGE_HEIGHT = 400;
131
132    const emailInput = document.getElementById('pr-email');
133    const emailError = document.getElementById('pr-email-error');
134    const phoneInput = document.getElementById('pr-phone');
135    const phoneError = document.getElementById('pr-phone-error');
136    const paraInputs = [];
137    const paraCounters = [];
138    const paraErrors = [];
139    for (let i = 1; i <= PARA_COUNT; i++) {
140        paraInputs.push(document.getElementById('pr-body-' + i));
141        paraCounters.push(document.getElementById('pr-body-' + i + '-counter'));
142        paraErrors.push(document.getElementById('pr-body-' + i + '-error'));
143    }
144    const bodyError = document.getElementById('pr-body-error');
145    const wordCounter = document.getElementById('pr-word-counter');
146    const sectorSelect = document.getElementById('pr-sector');
147    const sectorError = document.getElementById('pr-sector-error');
148    const sectorOtherRow = document.getElementById('pr-sector-other-row');
149    const sectorOtherInput = document.getElementById('pr-sector-other');
150    const sectorOtherError = document.getElementById('pr-sector-other-error');
151    const imageInput = document.getElementById('pr-image');
152    const imageError = document.getElementById('pr-image-error');
153    const imagePreview = document.getElementById('pr-image-preview');
154    const imagePreviewImg = document.getElementById('pr-image-preview-img');
155    const successEl = document.getElementById('pr-success');
156    const errorEl = document.getElementById('pr-error');
157    const submitBtn = document.getElementById('pr-submit-btn');
158
159    function countWords(text) {
160        return text.trim().length ? text.trim().split(/\s+/).length : 0;
161    }
162
163    // Converts the plain-text textarea value into structured HTML so the
164    // editorial draft created in Ghost has real paragraphs and bullet
165    // lists instead of one run-on block. Blank lines separate paragraphs;
166    // a block where every line starts with •, - or * becomes a <ul>.
167    function escapeHtml(s) {
168        return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
169    }
170
171    function formatBodyToHtml(text) {
172        const normalized = text.replace(/\r\n/g, '\n').trim();
173        if (!normalized) return '';
174        const bulletRe = /^[•\-\*]\s+/;
175        return normalized.split(/\n{2,}/).map(function(block) {
176            const lines = block.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
177            if (lines.length && lines.every(function(l) { return bulletRe.test(l); })) {
178                const items = lines.map(function(l) {
179                    return '<li>' + escapeHtml(l.replace(bulletRe, '')) + '</li>';
180                }).join('');
181                return '<ul>' + items + '</ul>';
182            }
183            return '<p>' + lines.map(escapeHtml).join('<br>') + '</p>';
184        }).join('');
185    }
186
187    function isOfficialEmail(email) {
188        const domain = email.split('@')[1];
189        if (!domain) return false;
190        return !PERSONAL_EMAIL_DOMAINS.includes(domain.toLowerCase());
191    }
192
193    function isValidIndianMobile(phone) {
194        const digits = phone.replace(/\D/g, '');
195        return /^[6-9]\d{9}$/.test(digits.slice(-10)) && digits.length >= 10;
196    }
197
198    let imageBase64 = '';
199
200    // Builds the combined plain-text and HTML body from the 4 paragraph
201    // boxes, skipping any left empty, so downstream payload shape
202    // (body_copy / body_copy_html / word_count) stays unchanged.
203    function getParaTexts() {
204        return paraInputs.map(function(input) { return input.value.trim(); }).filter(Boolean);
205    }
206
207    function updateParaCounter(index) {
208        const words = countWords(paraInputs[index].value);
209        paraCounters[index].textContent = words + ' / ' + MAX_WORDS_PER_PARA + ' words';
210        paraCounters[index].classList.toggle('pr-word-over', words > MAX_WORDS_PER_PARA);
211        paraErrors[index].style.display = words > MAX_WORDS_PER_PARA ? 'block' : 'none';
212    }
213
214    function updateTotalCounter() {
215        const totalWords = paraInputs.reduce(function(sum, input) { return sum + countWords(input.value); }, 0);
216        wordCounter.textContent = totalWords + ' / ' + MAX_WORDS + ' words total';
217        wordCounter.classList.toggle('pr-word-over', totalWords > MAX_WORDS);
218    }
219
220    paraInputs.forEach(function(input, index) {
221        input.addEventListener('input', function() {
222            updateParaCounter(index);
223            updateTotalCounter();
224        });
225    });
226
227    sectorSelect.addEventListener('change', function() {
228        const isOther = sectorSelect.value === 'other';
229        sectorOtherRow.style.display = isOther ? 'flex' : 'none';
230        sectorOtherInput.required = isOther;
231        if (!isOther) {
232            sectorOtherInput.value = '';
233            sectorOtherError.style.display = 'none';
234        }
235    });
236
237    imageInput.addEventListener('change', function() {
238        imageError.style.display = 'none';
239        imageBase64 = '';
240        imagePreview.style.display = 'none';
241        const file = imageInput.files[0];
242        if (!file) return;
243
244        if (file.size > MAX_IMAGE_BYTES) {
245            imageError.textContent = 'Image must be under 2MB.';
246            imageError.style.display = 'block';
247            imageInput.value = '';
248            return;
249        }
250
251        const reader = new FileReader();
252        reader.onload = function() {
253            const dataUrl = reader.result;
254            // Check actual pixel dimensions before accepting — file size alone
255            // doesn't catch a small/blurry image (a heavily-compressed JPEG
256            // can be tiny in bytes but still low resolution).
257            const probe = new Image();
258            probe.onload = function() {
259                if (probe.naturalWidth < MIN_IMAGE_WIDTH || probe.naturalHeight < MIN_IMAGE_HEIGHT) {
260                    imageError.textContent = 'This image is too low-resolution (' + probe.naturalWidth + '×' + probe.naturalHeight + 'px). Please upload a sharper photo, at least ' + MIN_IMAGE_WIDTH + '×' + MIN_IMAGE_HEIGHT + 'px.';
261                    imageError.style.display = 'block';
262                    imageInput.value = '';
263                    imageBase64 = '';
264                    imagePreview.style.display = 'none';
265                    return;
266                }
267                imageBase64 = dataUrl;
268                imagePreviewImg.src = imageBase64;
269                imagePreview.style.display = 'block';
270            };
271            probe.onerror = function() {
272                imageError.textContent = 'Could not read this image. Please try a different file.';
273                imageError.style.display = 'block';
274                imageInput.value = '';
275            };
276            probe.src = dataUrl;
277        };
278        reader.readAsDataURL(file);
279    });
280
281    form.addEventListener('submit', function(e) {
282        e.preventDefault();
283        errorEl.style.display = 'none';
284        successEl.style.display = 'none';
285        emailError.style.display = 'none';
286        phoneError.style.display = 'none';
287        bodyError.style.display = 'none';
288        sectorError.style.display = 'none';
289        sectorOtherError.style.display = 'none';
290
291        const email = form.official_email.value.trim();
292        const phone = form.phone.value.trim();
293        const paraTexts = getParaTexts();
294        const bodyCopy = paraTexts.join('\n\n');
295        const bodyCopyHtml = paraTexts.map(formatBodyToHtml).join('');
296        // Split into "intro" (first 2 paragraphs) and "rest" (remaining), so the
297        // spokesperson quote + photo can be inserted as the 3rd block in the
298        // published article — right after the intro and before the rest of the
299        // body — regardless of how many paragraphs (1-4) were submitted.
300        const bodyIntro = paraTexts.slice(0, 2).join('\n\n');
301        const bodyRest = paraTexts.slice(2).join('\n\n');
302        const words = paraInputs.reduce(function(sum, input) { return sum + countWords(input.value); }, 0);
303        const anyParaOverLimit = paraInputs.some(function(input) { return countWords(input.value) > MAX_WORDS_PER_PARA; });
304        const sector = sectorSelect.value;
305        const sectorOther = form.sector_other.value.trim();
306
307        let valid = true;
308
309        if (!isOfficialEmail(email)) {
310            emailError.style.display = 'block';
311            valid = false;
312        }
313        if (!isValidIndianMobile(phone)) {
314            phoneError.style.display = 'block';
315            valid = false;
316        }
317        if (!paraInputs[0].value.trim() || anyParaOverLimit) {
318            bodyError.style.display = 'block';
319            valid = false;
320        }
321        if (!sector) {
322            sectorError.style.display = 'block';
323            valid = false;
324        }
325        if (sector === 'other' && !sectorOther) {
326            sectorOtherError.style.display = 'block';
327            valid = false;
328        }
329        if (!valid) return;
330
331        const payload = {
332            contact_name: form.contact_name.value.trim(),
333            organisation: form.organisation.value.trim(),
334            official_email: email,
335            contact_location: form.contact_location.value.trim(),
336            phone: phone,
337            release_date: form.release_date.value,
338            dateline: form.dateline.value.trim(),
339            headline: form.headline.value.trim(),
340            subheadline: form.subheadline.value.trim(),
341            body_copy: bodyCopy,
342            body_copy_html: bodyCopyHtml,
343            body_intro: bodyIntro,
344            body_rest: bodyRest,
345            word_count: words,
346            sector: sector,
347            sector_other: sector === 'other' ? sectorOther : '',
348            spokesperson_image: imageBase64,
349            spokesperson_name_title: form.spokesperson_name_title.value.trim(),
350            spokesperson_quote: form.spokesperson_quote.value.trim()
351        };
352
353        submitBtn.disabled = true;
354        submitBtn.textContent = 'Submitting…';
355
356        fetch(WEBHOOK_URL, {
357            method: 'POST',
358            headers: { 'Content-Type': 'application/json' },
359            body: JSON.stringify(payload)
360        }).then(function(res) {
361            if (!res.ok) throw new Error('Request failed');
362            successEl.style.display = 'block';
363            form.reset();
364            paraCounters.forEach(function(counter, index) {
365                counter.textContent = '0 / ' + MAX_WORDS_PER_PARA + ' words';
366                counter.classList.remove('pr-word-over');
367                paraErrors[index].style.display = 'none';
368            });
369            wordCounter.textContent = '0 / ' + MAX_WORDS + ' words total';
370            wordCounter.classList.remove('pr-word-over');
371            sectorOtherRow.style.display = 'none';
372            sectorOtherInput.required = false;
373            imagePreview.style.display = 'none';
374            imageBase64 = '';
375            // Redirect to the relevant sector tag page so the submitter
376            // lands on content that matches their release. Fall back to
377            // /tag/press-releases/ if they chose "Other".
378            var prRedirect = (sector && sector !== 'other') ? '/tag/' + sector + '/' : '/tag/press-releases/';
379            setTimeout(function() {
380                window.location.href = prRedirect;
381            }, 5000);
382        }).catch(function() {
383            errorEl.textContent = 'Something went wrong. Please try again in a moment.';
384            errorEl.style.display = 'block';
385        }).finally(function() {
386            submitBtn.disabled = false;
387            submitBtn.textContent = 'Submit for Review';
388        });
389    });
390})();
391
392// OP-In article submission form — collects author + article content, validates
393// official email, phone, word limits (300 per part, 1200 total), and the
394// originality declaration. Posts to the Make.com OP-In webhook which runs
395// Copyscape plagiarism screening and AI grammar analysis before editorial review.
396(function() {
397    const form = document.getElementById('op-in-submit-form');
398    if (!form) return;
399
400    const WEBHOOK_URL = 'https://hook.eu1.make.com/gngzm2ehd2y3v94xh6rfpvk5ctzl2j1u';
401    const PERSONAL_EMAIL_DOMAINS = ['gmail.com','yahoo.com','yahoo.co.in','hotmail.com','outlook.com','live.com','aol.com','icloud.com','protonmail.com','rediffmail.com'];
402    const MAX_WORDS_PER_PARA = 200;
403    const MAX_WORDS_TOTAL    = 1200;
404    const MAX_IMAGE_BYTES    = 2 * 1024 * 1024;
405    const MIN_IMAGE_WIDTH    = 800;
406    const MIN_IMAGE_HEIGHT   = 800;
407
408    let paraCount = 0;
409    const paraContainer = document.getElementById('op-para-container');
410    const totalCounter  = document.getElementById('op-word-counter');
411    const progressFill  = document.getElementById('op-word-progress');
412    const sectorSelect      = document.getElementById('op-sector');
413    const sectorOtherRow    = document.getElementById('op-sector-other-row');
414    const sectorOtherInput  = document.getElementById('op-sector-other');
415    const successEl  = document.getElementById('op-success');
416    const errorEl    = document.getElementById('op-error');
417    const submitBtn  = document.getElementById('op-submit-btn');
418
419    function countWords(text) {
420        return text.trim().length ? text.trim().split(/\s+/).length : 0;
421    }
422
423    function getTotalWords() {
424        var total = 0;
425        paraContainer.querySelectorAll('textarea').forEach(function(ta) { total += countWords(ta.value); });
426        return total;
427    }
428
429    function updateTotalBar() {
430        var total = getTotalWords();
431        var pct   = Math.min(100, Math.round(total / MAX_WORDS_TOTAL * 100));
432        totalCounter.textContent = total + ' / 1,200 words total';
433        totalCounter.classList.toggle('pr-word-over', total > MAX_WORDS_TOTAL);
434        if (progressFill) {
435            progressFill.style.width = pct + '%';
436            progressFill.classList.toggle('op-progress-warn', total > 1000 && total <= MAX_WORDS_TOTAL);
437            progressFill.classList.toggle('op-progress-over', total > MAX_WORDS_TOTAL);
438        }
439    }
440
441    function renumberParagraphs() {
442        paraContainer.querySelectorAll('.pr-body-para-group').forEach(function(g, i) {
443            var n   = i + 1;
444            var lbl = g.querySelector('.op-para-label');
445            var shInput = g.querySelector('.op-subheading-input');
446            var shLabel = g.querySelector('.op-subheading-label');
447            var ta  = g.querySelector('textarea');
448            var ctr = g.querySelector('.pr-word-counter');
449            var err = g.querySelector('.pr-field-error');
450            if (lbl) lbl.textContent = 'Paragraph ' + n + (i === 0 ? ' *' : '');
451            if (shInput) { shInput.id = 'op-subheading-' + n; shInput.name = 'subheading_' + n; }
452            if (shLabel) shLabel.htmlFor = 'op-subheading-' + n;
453            if (ta)  ta.id = 'op-body-' + n;
454            if (ctr) ctr.id = 'op-body-' + n + '-counter';
455            if (err) { err.id = 'op-body-' + n + '-error'; err.textContent = 'Paragraph ' + n + ' exceeds 200 words.'; }
456        });
457        paraCount = paraContainer.querySelectorAll('.pr-body-para-group').length;
458    }
459
460    function addParagraph(isFirst) {
461        paraCount++;
462        var n     = paraCount;
463        var group = document.createElement('div');
464        group.className = 'pr-body-para-group';
465
466        var header = document.createElement('div');
467        header.className = 'op-para-header';
468
469        var lbl = document.createElement('span');
470        lbl.className = 'op-para-label';
471        lbl.textContent = 'Paragraph ' + n + (isFirst ? ' *' : '');
472        header.appendChild(lbl);
473
474        if (!isFirst) {
475            var rmBtn = document.createElement('button');
476            rmBtn.type = 'button';
477            rmBtn.className = 'op-remove-para-btn';
478            rmBtn.textContent = 'Remove';
479            rmBtn.addEventListener('click', function() {
480                group.remove();
481                renumberParagraphs();
482                updateTotalBar();
483            });
484            header.appendChild(rmBtn);
485        }
486
487        /* Sub-heading field — optional, becomes <h3> in published article */
488        var shLabel = document.createElement('label');
489        shLabel.className = 'op-subheading-label';
490        shLabel.htmlFor   = 'op-subheading-' + n;
491        shLabel.textContent = 'Sub-heading (optional)';
492
493        var shInput = document.createElement('input');
494        shInput.type        = 'text';
495        shInput.id          = 'op-subheading-' + n;
496        shInput.name        = 'subheading_' + n;
497        shInput.className   = 'pr-input op-subheading-input';
498        shInput.maxLength   = 120;
499        shInput.placeholder = 'Section heading for this paragraph — leave blank if none';
500
501        var shHint = document.createElement('span');
502        shHint.className   = 'pr-hint op-subheading-hint';
503        shHint.textContent = 'Will appear as a red sub-heading above this paragraph on the site.';
504
505        /* Body textarea */
506        var ta = document.createElement('textarea');
507        ta.id          = 'op-body-' + n;
508        ta.className   = 'pr-input pr-textarea';
509        ta.rows        = 5;
510        ta.placeholder = 'Write this paragraph here…';
511        if (isFirst) ta.required = true;
512
513        var ctr = document.createElement('span');
514        ctr.className = 'pr-word-counter';
515        ctr.id        = 'op-body-' + n + '-counter';
516        ctr.textContent = '0 / 200 words';
517
518        var errSpan = document.createElement('span');
519        errSpan.className    = 'pr-field-error';
520        errSpan.id           = 'op-body-' + n + '-error';
521        errSpan.style.display = 'none';
522        errSpan.textContent  = 'Paragraph ' + n + ' exceeds 200 words.';
523
524        ta.addEventListener('input', function() {
525            var w = countWords(ta.value);
526            ctr.textContent = w + ' / 200 words';
527            ctr.classList.toggle('pr-word-over', w > MAX_WORDS_PER_PARA);
528            errSpan.style.display = w > MAX_WORDS_PER_PARA ? 'block' : 'none';
529            updateTotalBar();
530        });
531
532        group.appendChild(header);
533        group.appendChild(shLabel);
534        group.appendChild(shInput);
535        group.appendChild(shHint);
536        group.appendChild(ta);
537        group.appendChild(ctr);
538        group.appendChild(errSpan);
539        paraContainer.appendChild(group);
540        updateTotalBar();
541    }
542
543    // Initialise with 4 paragraphs
544    addParagraph(true);
545    addParagraph(false);
546    addParagraph(false);
547    addParagraph(false);
548
549    var addBtn = document.getElementById('op-add-para-btn');
550    if (addBtn) { addBtn.addEventListener('click', function() { addParagraph(false); }); }
551
552    function isOfficialEmail(email) {
553        var domain = (email.split('@')[1] || '').toLowerCase();
554        return domain && !PERSONAL_EMAIL_DOMAINS.includes(domain);
555    }
556
557    function isValidIndianMobile(phone) {
558        var digits = phone.replace(/\D/g, '');
559        return /^[6-9]\d{9}$/.test(digits.slice(-10)) && digits.length >= 10;
560    }
561
562    if (sectorSelect) {
563        sectorSelect.addEventListener('change', function() {
564            var isOther = sectorSelect.value === 'other';
565            sectorOtherRow.style.display = isOther ? 'flex' : 'none';
566            if (!isOther) { sectorOtherInput.value = ''; }
567        });
568    }
569
570    var imageBase64 = '';
571    var imageInput  = document.getElementById('op-image');
572    if (imageInput) {
573        imageInput.addEventListener('change', function() {
574            var errEl    = document.getElementById('op-image-error');
575            var preview  = document.getElementById('op-image-preview');
576            var previewImg = document.getElementById('op-image-preview-img');
577            errEl.style.display = 'none'; imageBase64 = ''; preview.style.display = 'none';
578            var file = imageInput.files[0];
579            if (!file) return;
580            if (file.size > MAX_IMAGE_BYTES) {
581                errEl.textContent = 'Image must be under 2MB.';
582                errEl.style.display = 'block'; imageInput.value = ''; return;
583            }
584            var reader = new FileReader();
585            reader.onload = function() {
586                var dataUrl = reader.result;
587                var probe   = new Image();
588                probe.onload = function() {
589                    if (probe.naturalWidth < MIN_IMAGE_WIDTH || probe.naturalHeight < MIN_IMAGE_HEIGHT) {
590                        errEl.textContent = 'Image too small (' + probe.naturalWidth + '\xd7' + probe.naturalHeight + 'px). Minimum 800\xd7800px.';
591                        errEl.style.display = 'block'; imageInput.value = ''; imageBase64 = ''; return;
592                    }
593                    imageBase64 = dataUrl; previewImg.src = dataUrl; preview.style.display = 'block';
594                };
595                probe.onerror = function() {
596                    errEl.textContent = 'Could not read image. Please try a different file.';
597                    errEl.style.display = 'block'; imageInput.value = '';
598                };
599                probe.src = dataUrl;
600            };
601            reader.readAsDataURL(file);
602        });
603    }
604
605    form.addEventListener('submit', function(e) {
606        e.preventDefault();
607        if (errorEl)   errorEl.style.display = 'none';
608        if (successEl) successEl.style.display = 'none';
609        var valid = true;
610
611        var emailInput = document.getElementById('op-email');
612        var emailError = document.getElementById('op-email-error');
613        if (emailInput && !isOfficialEmail(emailInput.value.trim())) {
614            emailError.style.display = 'block'; valid = false;
615        } else if (emailError) { emailError.style.display = 'none'; }
616
617        var phoneInput = document.getElementById('op-phone');
618        var phoneError = document.getElementById('op-phone-error');
619        if (phoneInput && !isValidIndianMobile(phoneInput.value.trim())) {
620            phoneError.style.display = 'block'; valid = false;
621        } else if (phoneError) { phoneError.style.display = 'none'; }
622
623        var bodyError  = document.getElementById('op-body-error');
624        var textareas  = paraContainer.querySelectorAll('textarea');
625        var partTexts  = [];
626        var partSubheadings = [];
627        var bodyValid  = true;
628        textareas.forEach(function(ta) {
629            var w = countWords(ta.value);
630            partTexts.push(ta.value.trim());
631            var shInput = ta.parentNode.querySelector('.op-subheading-input');
632            partSubheadings.push(shInput ? shInput.value.trim() : '');
633            var errEl = ta.parentNode.querySelector('.pr-field-error');
634            if (w > MAX_WORDS_PER_PARA) { if (errEl) errEl.style.display = 'block'; bodyValid = false; }
635            else { if (errEl) errEl.style.display = 'none'; }
636        });
637        if (!partTexts.length || countWords(partTexts[0]) === 0) { bodyValid = false; }
638        if (getTotalWords() > MAX_WORDS_TOTAL) { bodyValid = false; }
639        if (!bodyValid) { if (bodyError) bodyError.style.display = 'block'; valid = false; }
640        else { if (bodyError) bodyError.style.display = 'none'; }
641
642        var sectorError = document.getElementById('op-sector-error');
643        if (sectorSelect && !sectorSelect.value) {
644            sectorError.style.display = 'block'; valid = false;
645        } else if (sectorError) { sectorError.style.display = 'none'; }
646        if (sectorSelect && sectorSelect.value === 'other' && sectorOtherInput && !sectorOtherInput.value.trim()) {
647            document.getElementById('op-sector-other-error').style.display = 'block'; valid = false;
648        } else { var otherErr = document.getElementById('op-sector-other-error'); if (otherErr) otherErr.style.display = 'none'; }
649
650        var decl    = document.getElementById('op-declaration');
651        var declErr = document.getElementById('op-declaration-error');
652        if (decl && !decl.checked) { declErr.style.display = 'block'; valid = false; }
653        else if (declErr) { declErr.style.display = 'none'; }
654
655        if (!valid) return;
656
657        var wordCount = getTotalWords();
658
659        // Build the article HTML automatically:
660        // subheading present → <h3>heading</h3><p>body</p>
661        // no subheading      → <p>body</p>
662        var bodyHtml = partTexts.filter(Boolean).map(function(text, i) {
663            var sh = partSubheadings[i] || '';
664            var escaped = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
665            var shHtml  = sh ? '<h3>' + sh.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</h3>' : '';
666            return shHtml + '<p>' + escaped + '</p>';
667        }).join('\n');
668
669        var payload = {
670            author_name:    document.getElementById('op-name').value.trim(),
671            designation:    document.getElementById('op-designation').value.trim(),
672            organisation:   document.getElementById('op-org').value.trim(),
673            official_email: emailInput.value.trim(),
674            phone:          phoneInput.value.trim(),
675            linkedin_url:   (document.getElementById('op-linkedin').value || '').trim(),
676            headline:       document.getElementById('op-headline').value.trim(),
677            subheadline:    document.getElementById('op-subheadline').value.trim(),
678            sector:         sectorSelect.value,
679            sector_other:   (sectorOtherInput ? sectorOtherInput.value.trim() : ''),
680            body_html:      bodyHtml,
681            body_combined:  partTexts.filter(Boolean).join('\n\n'),
682            word_count:     wordCount,
683            author_image:   imageBase64,
684            declaration:    true,
685            submitted_at:   new Date().toISOString()
686        };
687        partTexts.forEach(function(t, i) {
688            payload['body_part_' + (i + 1)] = t;
689            payload['subheading_' + (i + 1)] = partSubheadings[i] || '';
690        });
691
692        submitBtn.disabled = true;
693        submitBtn.textContent = 'Submitting…';
694
695        fetch(WEBHOOK_URL, {
696            method: 'POST',
697            headers: { 'Content-Type': 'application/json' },
698            body: JSON.stringify(payload)
699        }).then(function(res) {
700            if (!res.ok) throw new Error('failed');
701            successEl.style.display = 'block';
702            form.style.display = 'none';
703            // Redirect to the relevant sector tag page. Fall back to /tag/op-in/
704            // if they chose "Other" or no sector was selected.
705            var opSector = sectorSelect ? sectorSelect.value : '';
706            var opRedirect = (opSector && opSector !== 'other') ? '/tag/' + opSector + '/' : '/tag/op-in/';
707            setTimeout(function() { window.location.href = opRedirect; }, 5000);
708        }).catch(function() {
709            errorEl.textContent = 'Something went wrong. Please try again in a moment.';
710            errorEl.style.display = 'block';
711            submitBtn.disabled = false;
712            submitBtn.textContent = 'Submit for Review';
713        });
714    });
715})();
716
717// Fixed masthead — logo + nav are position:fixed so they never move.
718// We dynamically measure the masthead height and apply it as body
719// padding-top so content starts below the header on all screen sizes.
720(function() {
721    const masthead = document.querySelector('.sticky-masthead');
722    if (!masthead) return;
723
724    function applyPadding() {
725        document.body.style.paddingTop = masthead.offsetHeight + 'px';
726    }
727    applyPadding();
728    window.addEventListener('resize', applyPadding, { passive: true });
729
730    window.addEventListener('scroll', function() {
731        const scrolled = window.scrollY > 80;
732        const wasScrolled = masthead.classList.contains('is-scrolled');
733        masthead.classList.toggle('is-scrolled', scrolled);
734        // Recalculate padding when masthead height changes (scroll state change)
735        if (scrolled !== wasScrolled) applyPadding();
736    }, { passive: true });
737})();
738
739// Segment-aware feature image handler.
740// Reads data-segment from the article element and applies the correct layout:
741//   press-releases → Spokesperson Callout Block (portrait + blockquote side by side)
742//   qa             → NC5 Author Card at top (circular portrait) + Q&A label colouring
743//   op-in          → OP-In Author Byline at top (circular portrait) + bio at bottom
744//   all others     → float right from paragraph 1
745(function() {
746    var figure     = document.getElementById('post-feature-image-inline');
747    var body       = document.querySelector('.post-body');
748    var segmentEl  = document.getElementById('article-segment');
749    if (!body) return;
750
751    var segment = (segmentEl && segmentEl.dataset.segment) ? segmentEl.dataset.segment : '';
752
753    // ── PRESS RELEASE ────────────────────────────────────────────────────────
754    if (segment === 'press-releases') {
755        var bq = body.querySelector('blockquote');
756        if (figure && bq) {
757            var callout = document.createElement('div');
758            callout.className = 'spokesperson-callout';
759
760            // Image column
761            var imgCol = document.createElement('div');
762            imgCol.className = 'sc-image';
763            var img = figure.querySelector('img');
764            if (img) imgCol.appendChild(img.cloneNode(true));
765            var figcap = figure.querySelector('figcaption');
766            if (figcap) {
767                var cap = document.createElement('p');
768                cap.className = 'sc-caption';
769                cap.innerHTML = figcap.innerHTML;
770                imgCol.appendChild(cap);
771            }
772
773            // Quote column
774            var quoteCol = document.createElement('div');
775            quoteCol.className = 'sc-quote';
776            quoteCol.appendChild(bq.cloneNode(true));
777
778            callout.appendChild(imgCol);
779            callout.appendChild(quoteCol);
780
781            bq.parentNode.replaceChild(callout, bq);
782            figure.remove();
783        }
784        return;
785    }
786
787    // ── NC5 (QA) ─────────────────────────────────────────────────────────────
788    if (segment === 'nc5') {
789        if (figure) {
790            var card = document.createElement('div');
791            card.className = 'nc5-author-card';
792
793            var ncImg = figure.querySelector('img');
794            if (ncImg) {
795                var ncImgClone = ncImg.cloneNode(true);
796                ncImgClone.className = 'nc5-portrait';
797                card.appendChild(ncImgClone);
798            }
799
800            var info = document.createElement('div');
801            info.className = 'nc5-author-info';
802            // Name and title come from the feature image caption.
803            // Editors should set it as "Interviewee Name | Title, Organisation"
804            // e.g. "Neeraj Saxena | CEO & M.D., Auxilo Finserve"
805            var figcapNC = figure.querySelector('figcaption');
806            var capText  = figcapNC ? figcapNC.textContent.trim() : '';
807            var capParts = capText.split('|');
808            var name = capParts[0] ? capParts[0].trim() : '';
809            var bio  = capParts[1] ? capParts[1].trim() : '';
810            info.innerHTML = (name ? '<h3>' + name + '</h3>' : '') +
811                             (bio  ? '<p>'  + bio  + '</p>' : '');
812            card.appendChild(info);
813
814            body.insertAdjacentElement('beforebegin', card);
815            figure.remove();
816        }
817
818        // Colour NC: questions and speaker answers
819        body.querySelectorAll('p').forEach(function(p) {
820            var fec = p.firstElementChild;
821            if (fec && fec.tagName === 'STRONG') {
822                var label = fec.textContent.trim();
823                if (label === 'NC:') {
824                    p.classList.add('nc5-q');
825                } else if (label.slice(-1) === ':') {
826                    p.classList.add('nc5-a');
827                }
828            }
829        });
830        return;
831    }
832
833    // ── OP-IN ─────────────────────────────────────────────────────────────────
834    if (segment === 'op-in') {
835        // Primary: read author data from hidden #opin-meta div injected by Make.com
836        var metaDiv  = document.getElementById('opin-meta');
837        var opAuthor = metaDiv ? (metaDiv.getAttribute('data-name') || '').trim() : '';
838        var opDesig  = metaDiv ? (metaDiv.getAttribute('data-designation') || '').trim() : '';
839        var opOrg    = metaDiv ? (metaDiv.getAttribute('data-org') || '').trim() : '';
840        var opDesigOrg = [opDesig, opOrg].filter(Boolean).join(', ');
841        var opTitle  = opDesigOrg ? opDesigOrg + ' | Views are personal.' : 'Views are personal.';
842
843        // Fallback: if no meta div, try figcaption pipe-format
844        if (!opAuthor && figure) {
845            var figcapOp = figure.querySelector('figcaption');
846            var capRaw   = figcapOp ? figcapOp.textContent.trim() : '';
847            var capParts = capRaw.split('|');
848            opAuthor = capParts[0] ? capParts[0].trim() : '';
849            opTitle  = capParts[1] ? capParts[1].trim() : opTitle;
850        }
851
852        // Last resort: Ghost author name (but never "newsdesk")
853        if (!opAuthor) {
854            var metaName = document.querySelector('.author-name');
855            var fbName   = metaName ? metaName.textContent.trim() : '';
856            opAuthor = (fbName && fbName.toLowerCase() !== 'newsdesk') ? fbName : '';
857        }
858
859        var byline = document.createElement('div');
860        byline.className = 'opin-byline';
861
862        // Photo: from feature image figure if present
863        if (figure) {
864            var opImg = figure.querySelector('img');
865            if (opImg) byline.appendChild(opImg.cloneNode(true));
866            figure.remove();
867        }
868
869        var opText = document.createElement('div');
870        opText.className = 'opin-byline-text';
871        opText.innerHTML = (opAuthor ? '<strong>' + opAuthor + '</strong>' : '') +
872                           '<span>' + opTitle + '</span>';
873        byline.appendChild(opText);
874        body.insertAdjacentElement('beforebegin', byline);
875        if (metaDiv) metaDiv.remove();
876
877        // Hide the Ghost "newsdesk" meta author row — byline card replaces it
878        var authorInfo = document.querySelector('.post-author-info');
879        if (authorInfo) authorInfo.style.display = 'none';
880
881        // Style the existing author box as a bottom bio card
882        var authorBox = document.querySelector('.author-box');
883        if (authorBox) authorBox.classList.add('opin-author-bio');
884        return;
885    }
886
887    // ── ALL OTHER SEGMENTS — float right from paragraph 1 ────────────────────
888    if (!figure) return;
889    // If the article uses an nc5-leader-card (spokesperson bio block), remove
890    // the feature image entirely — the headshot is the visual anchor.
891    if (body.querySelector('.nc5-leader-card')) {
892        figure.remove();
893        return;
894    }
895    var paras = body.querySelectorAll('p');
896    if (paras.length >= 1) {
897        paras[0].insertAdjacentElement('beforebegin', figure);
898    }
899})();
900
901// Quick Read card — removed per editorial decision (all segments).
902(function() {
903    var content = document.querySelector('.gh-content');
904    if (!content) return;
905    content.querySelectorAll('p').forEach(function(p) {
906        var strong = p.querySelector('strong');
907        if (!strong || strong.textContent.trim() !== 'Quick Read:') return;
908        var nextEl = p.nextElementSibling;
909        p.style.display = 'none';
910        if (nextEl && nextEl.tagName === 'UL') nextEl.style.display = 'none';
911    });
912})();
913
914// NC5 article submission form — reporters fill in leader profile, 5 Q&A pairs
915// and a Newsco Takeaway. Submits directly to Ghost Admin API (no Make.com).
916// To set up: Ghost Admin → Settings → Integrations → Add custom integration
917// → name "NC5 Submit" → copy the Admin API Key → paste below.
918(function() {
919    var form = document.getElementById('nc5-submit-form');
920    if (!form) return;
921
922    // Ghost Admin API key (id:secret) — from Ghost Admin → Settings → Integrations
923    var GHOST_ADMIN_KEY = '6a355ce2ad17f25da61c88ac:32b8d5bb808fad825eb90e592e6d2da8a04d07d49ec6e5ba3f547b0a7b323591';
924    var GHOST_API_URL   = '/ghost/api/admin/posts/?source=html';
925    var MAX_IMG_BYTES   = 2 * 1024 * 1024;
926
927    var successEl      = document.getElementById('nc5-success');
928    var errorEl        = document.getElementById('nc5-error');
929    var submitBtn      = document.getElementById('nc5-submit-btn');
930    var photoInput     = document.getElementById('nc5-photo');
931    var photoError     = document.getElementById('nc5-photo-error');
932    var photoPreview   = document.getElementById('nc5-photo-preview');
933    var photoPreviewImg= document.getElementById('nc5-photo-preview-img');
934    var photoBase64    = '';
935    var photoFile      = null; // actual File object — uploaded to Ghost media library on submit
936
937    // Photo upload — preview only; actual upload happens at submit time
938    photoInput.addEventListener('change', function() {
939        var file = photoInput.files[0];
940        photoError.style.display = 'none';
941        photoPreview.style.display = 'none';
942        photoBase64 = '';
943        photoFile   = null;
944        if (!file) return;
945        if (file.size > MAX_IMG_BYTES) {
946            photoError.textContent = 'Image must be under 2MB.';
947            photoError.style.display = 'block';
948            photoInput.value = '';
949            return;
950        }
951        photoFile = file;
952        var reader = new FileReader();
953        reader.onload = function(ev) {
954            photoBase64 = ev.target.result;
955            photoPreviewImg.src = photoBase64;
956            photoPreview.style.display = 'block';
957        };
958        reader.readAsDataURL(file);
959    });
960
961    // Upload a File to Ghost media library; returns Promise<url string>
962    function uploadImageToGhost(file, token) {
963        var fd = new FormData();
964        fd.append('file', file, file.name);
965        fd.append('purpose', 'image');
966        return fetch('/ghost/api/admin/images/upload/', {
967            method: 'POST',
968            headers: { 'Authorization': 'Ghost ' + token },
969            body: fd
970        }).then(function(res) {
971            if (!res.ok) throw new Error('Image upload failed');
972            return res.json();
973        }).then(function(data) {
974            return data.images[0].url;
975        });
976    }
977
978    function esc(str) {
979        return String(str)
980            .replace(/&/g, '&amp;')
981            .replace(/</g, '&lt;')
982            .replace(/>/g, '&gt;')
983            .replace(/"/g, '&quot;');
984    }
985
986    function toParas(str) {
987        return esc(str).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>');
988    }
989
990    function autoInitials(name) {
991        var parts = name.trim().split(/\s+/).filter(Boolean);
992        if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
993        if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
994        return 'NC';
995    }
996
997    function buildNc5Html(d) {
998        var firstName = d.leader_name.trim().split(/\s+/)[0];
999
1000        var photoHtml = d.photo
1001            ? '<img src="' + d.photo + '" class="nc5-leader-photo" alt="' + esc(d.leader_name) + '">'
1002            : '<div class="nc5-leader-photo-placeholder">' + autoInitials(d.leader_name) + '</div>';
1003
1004        var takeawayHtml = '<div class="nc5-takeaway">\n' +
1005                          '    <p>' + esc(d.takeaway) + '</p>\n' +
1006                          '</div>';
1007
1008        var qaBlocks = [1,2,3,4,5].map(function(i) {
1009            var block = '<div class="nc5-qa">\n' +
1010                        '    <div class="nc5-question">\n' +
1011                        '        <span class="nc5-q-num">' + i + '</span>\n' +
1012                        '        <span class="nc5-q-sep"></span>\n' +
1013                        '        <p class="nc5-q-text"><strong>' + esc(d['q' + i]) + '</strong></p>\n' +
1014                        '    </div>\n' +
1015                        '    <div class="nc5-answer">\n' +
1016                        '        <p><strong>' + esc(firstName) + ':</strong> ' + toParas(d['a' + i]) + '</p>\n' +
1017                        '    </div>\n' +
1018                        '</div>';
1019            return i === 2 ? block + '\n' + takeawayHtml : block;
1020        }).join('\n');
1021
1022        // Wrap in Ghost HTML card markers so the API stores the HTML exactly
1023        // as-is (raw HTML card), preserving all nc5-* classes without Ghost's
1024        // Lexical parser converting or stripping them.
1025        return '<!--kg-card-begin: html-->\n' +
1026               '<div class="nc5-leader-card">\n' +
1027               '    ' + photoHtml + '\n' +
1028               '    <div class="nc5-leader-info">\n' +
1029               '        <p class="nc5-leader-name">' + esc(d.leader_name) + '</p>\n' +
1030               '        <p class="nc5-leader-title">' + esc(d.leader_title_org) + '</p>\n' +
1031               '        <p class="nc5-leader-bio">' + esc(d.leader_bio) + '</p>\n' +
1032               '    </div>\n' +
1033               '</div>\n' +
1034               '<div class="nc5-divider">\n' +
1035               '    <span class="nc5-divider-line"></span>\n' +
1036               '    <span class="nc5-divider-line"></span>\n' +
1037               '</div>\n' +
1038               qaBlocks + '\n' +
1039               '<!--kg-card-end: html-->';
1040    }
1041
1042    form.addEventListener('submit', function(e) {
1043        e.preventDefault();
1044        errorEl.style.display = 'none';
1045        successEl.style.display = 'none';
1046
1047        var d = {
1048            title:            document.getElementById('nc5-title').value.trim() + ' — ' + form.leader_name.value.trim(),
1049            reporter_name:    'newsco',
1050            leader_name:      form.leader_name.value.trim(),
1051            leader_title_org: form.leader_title_org.value.trim(),
1052            leader_bio:       form.leader_bio.value.trim(),
1053            photo:            photoBase64,
1054            q1: form.q1.value.trim(), a1: form.a1.value.trim(),
1055            q2: form.q2.value.trim(), a2: form.a2.value.trim(),
1056            q3: form.q3.value.trim(), a3: form.a3.value.trim(),
1057            q4: form.q4.value.trim(), a4: form.a4.value.trim(),
1058            q5: form.q5.value.trim(), a5: form.a5.value.trim(),
1059            takeaway: form.takeaway.value.trim()
1060        };
1061
1062        var required = ['title','leader_name','leader_title_org','leader_bio',
1063                        'q1','a1','q2','a2','q3','a3','q4','a4','q5','a5','takeaway'];
1064        for (var i = 0; i < required.length; i++) {
1065            if (!d[required[i]]) {
1066                errorEl.textContent = 'Please fill in all required fields.';
1067                errorEl.style.display = 'block';
1068                return;
1069            }
1070        }
1071
1072        submitBtn.disabled = true;
1073        submitBtn.textContent = 'Creating draft…';
1074
1075        // 1. Get JWT, 2. Upload photo (if any) to Ghost media, 3. Create draft post
1076        ghostJWT(GHOST_ADMIN_KEY).then(function(token) {
1077            var imgPromise = photoFile
1078                ? uploadImageToGhost(photoFile, token).then(function(url) { d.photo = url; })
1079                : Promise.resolve();
1080            return imgPromise.then(function() {
1081                d.html = buildNc5Html(d);
1082                return fetch(GHOST_API_URL, {
1083                    method: 'POST',
1084                    headers: {
1085                        'Content-Type': 'application/json',
1086                        'Authorization': 'Ghost ' + token
1087                    },
1088                    body: JSON.stringify({
1089                        posts: [{
1090                            title: d.title,
1091                            html: d.html,
1092                            status: 'draft',
1093                            tags: [{ slug: 'nc5' }]
1094                        }]
1095                    })
1096                });
1097            });
1098        }).then(function(res) {
1099            if (!res.ok) return res.json().then(function(e) { throw new Error(JSON.stringify(e)); });
1100            return res.json();
1101        }).then(function() {
1102            successEl.style.display = 'block';
1103            form.reset();
1104            photoBase64 = '';
1105            photoFile   = null;
1106            photoPreview.style.display = 'none';
1107            window.scrollTo({ top: 0, behavior: 'smooth' });
1108        }).catch(function(err) {
1109            console.error('NC5 submit error:', err);
1110            errorEl.textContent = 'Something went wrong. Please try again in a moment.';
1111            errorEl.style.display = 'block';
1112        }).finally(function() {
1113            submitBtn.disabled = false;
1114            submitBtn.textContent = 'Create Draft →';
1115        });
1116    });
1117
1118    // Generate a Ghost Admin API JWT (HS256) from an "id:hexsecret" key.
1119    function ghostJWT(apiKey) {
1120        var parts  = apiKey.split(':');
1121        var id     = parts[0];
1122        var secret = parts[1];
1123        var iat    = Math.floor(Date.now() / 1000);
1124
1125        function b64url(str) {
1126            return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
1127        }
1128        var header  = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: id }));
1129        var payload = b64url(JSON.stringify({ iat: iat, exp: iat + 300, aud: '/admin/' }));
1130        var unsigned = header + '.' + payload;
1131
1132        // Decode hex secret → Uint8Array
1133        var secretBytes = new Uint8Array(secret.match(/.{2}/g).map(function(b) { return parseInt(b, 16); }));
1134
1135        return crypto.subtle.importKey(
1136            'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
1137        ).then(function(key) {
1138            return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(unsigned));
1139        }).then(function(sig) {
1140            var sigB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sig)))
1141                .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
1142            return unsigned + '.' + sigB64;
1143        });
1144    }
1145})();
1146
1147// IN CONVERSATION article submission form — reporters fill in guest profile,
1148// intro, unlimited Q&A pairs, and a pull quote. Creates Ghost draft automatically.
1149(function() {
1150    var form = document.getElementById('or-submit-form');
1151    if (!form) return;
1152
1153    var GHOST_ADMIN_KEY = '6a355ce2ad17f25da61c88ac:32b8d5bb808fad825eb90e592e6d2da8a04d07d49ec6e5ba3f547b0a7b323591';
1154    var GHOST_API_URL   = '/ghost/api/admin/posts/?source=html';
1155    var MAX_IMG_BYTES   = 2 * 1024 * 1024;
1156
1157    var successEl   = document.getElementById('or-success');
1158    var errorEl     = document.getElementById('or-error');
1159    var submitBtn   = document.getElementById('or-submit-btn');
1160    var photoInput  = document.getElementById('or-photo');
1161    var photoError  = document.getElementById('or-photo-error');
1162    var photoPreview    = document.getElementById('or-photo-preview');
1163    var photoPreviewImg = document.getElementById('or-photo-preview-img');
1164    var photoFile   = null;
1165    var photoBase64 = '';
1166    var qaList      = document.getElementById('or-qa-list');
1167    var addQaBtn    = document.getElementById('or-add-qa');
1168    var qaPairCount = 7;
1169
1170    // Photo preview
1171    photoInput.addEventListener('change', function() {
1172        photoError.style.display = 'none';
1173        photoPreview.style.display = 'none';
1174        photoBase64 = '';
1175        photoFile = null;
1176        var file = photoInput.files[0];
1177        if (!file) return;
1178        if (file.size > MAX_IMG_BYTES) {
1179            photoError.textContent = 'Image must be under 2MB.';
1180            photoError.style.display = 'block';
1181            photoInput.value = '';
1182            return;
1183        }
1184        photoFile = file;
1185        var reader = new FileReader();
1186        reader.onload = function(ev) {
1187            photoBase64 = ev.target.result;
1188            photoPreviewImg.src = photoBase64;
1189            photoPreview.style.display = 'block';
1190        };
1191        reader.readAsDataURL(file);
1192    });
1193
1194    // Add another Q&A pair dynamically
1195    addQaBtn.addEventListener('click', function() {
1196        qaPairCount++;
1197        var n = qaPairCount;
1198        var div = document.createElement('div');
1199        div.className = 'pr-body-para-group or-qa-pair';
1200        div.dataset.num = n;
1201        div.innerHTML =
1202            '<label>Question ' + n + '</label>' +
1203            '<textarea name="q' + n + '" class="pr-input pr-textarea or-q" rows="2"></textarea>' +
1204            '<label class="nc5-answer-label">Answer ' + n + '</label>' +
1205            '<textarea name="a' + n + '" class="pr-input pr-textarea or-a" rows="4"></textarea>';
1206        qaList.appendChild(div);
1207        div.querySelector('textarea').focus();
1208    });
1209
1210    function esc(str) {
1211        return String(str)
1212            .replace(/&/g, '&amp;').replace(/</g, '&lt;')
1213            .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1214    }
1215
1216    function toParas(str) {
1217        return esc(str).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>');
1218    }
1219
1220    function ghostJWT(apiKey) {
1221        var parts  = apiKey.split(':');
1222        var id     = parts[0];
1223        var secret = parts[1];
1224        var iat    = Math.floor(Date.now() / 1000);
1225        function b64url(str) {
1226            return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
1227        }
1228        var header   = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: id }));
1229        var payload  = b64url(JSON.stringify({ iat: iat, exp: iat + 300, aud: '/admin/' }));
1230        var unsigned = header + '.' + payload;
1231        var secretBytes = new Uint8Array(secret.match(/.{2}/g).map(function(b) { return parseInt(b, 16); }));
1232        return crypto.subtle.importKey(
1233            'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
1234        ).then(function(key) {
1235            return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(unsigned));
1236        }).then(function(sig) {
1237            var sigB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sig)))
1238                .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
1239            return unsigned + '.' + sigB64;
1240        });
1241    }
1242
1243    function uploadImageToGhost(file, token) {
1244        var fd = new FormData();
1245        fd.append('file', file, file.name);
1246        fd.append('purpose', 'image');
1247        return fetch('/ghost/api/admin/images/upload/', {
1248            method: 'POST',
1249            headers: { 'Authorization': 'Ghost ' + token },
1250            body: fd
1251        }).then(function(res) {
1252            if (!res.ok) throw new Error('Image upload failed');
1253            return res.json();
1254        }).then(function(data) { return data.images[0].url; });
1255    }
1256
1257    function buildOrHtml(d) {
1258        var firstName = d.guest_name.trim().split(/\s+/)[0];
1259
1260        var photoHtml = d.photo
1261            ? '<img src="' + d.photo + '" class="nc5-leader-photo" alt="' + esc(d.guest_name) + '">'
1262            : '<div class="nc5-leader-photo-placeholder">' +
1263              (function(name) {
1264                  var p = name.trim().split(/\s+/).filter(Boolean);
1265                  if (p.length >= 2) return (p[0][0] + p[p.length-1][0]).toUpperCase();
1266                  return p[0] ? p[0].slice(0,2).toUpperCase() : 'IC';
1267              })(d.guest_name) + '</div>';
1268
1269        var takeawayHtml =
1270               '<div class="nc5-takeaway ic-pullquote">\n' +
1271               '    <p class="nc5-takeaway-label">In Their Words</p>\n' +
1272               '    <p>&ldquo;' + esc(d.quote) + '&rdquo;</p>\n' +
1273               '    <span class="nc5-takeaway-attr">&mdash; ' + esc(d.guest_name) + ', ' + esc(d.guest_title) + ', ' + esc(d.guest_company) + '</span>\n' +
1274               '</div>';
1275
1276        // Insert pull quote after Q3 (or after last Q if fewer than 3)
1277        var insertAfter = Math.min(2, d.pairs.length - 1);
1278        var qaBlocks = d.pairs.map(function(pair, idx) {
1279            var num = String(idx + 1).padStart(2, '0');
1280            var block = '<div class="nc5-qa">\n' +
1281                   '    <div class="nc5-question">\n' +
1282                   '        <span class="nc5-q-num">' + num + '</span>\n' +
1283                   '        <span class="nc5-q-sep"></span>\n' +
1284                   '        <p class="nc5-q-text"><strong>' + esc(pair.q) + '</strong></p>\n' +
1285                   '    </div>\n' +
1286                   '    <div class="nc5-answer">\n' +
1287                   '        <p><strong>' + esc(firstName) + ':</strong> ' + toParas(pair.a) + '</p>\n' +
1288                   '    </div>\n' +
1289                   '</div>';
1290            if (idx === insertAfter) { block += '\n' + takeawayHtml; }
1291            return block;
1292        }).join('\n');
1293
1294        return '<!--kg-card-begin: html-->\n' +
1295               '<div class="ic-article">\n' +
1296               '<div class="nc5-divider ic-section-header">\n' +
1297               '    <span class="nc5-divider-line"></span>\n' +
1298               '    <span class="nc5-divider-label ic-divider-label">Inside Business</span>\n' +
1299               '    <span class="nc5-divider-line"></span>\n' +
1300               '</div>\n' +
1301               '<div class="ic-leader-wrap">\n' +
1302               '<div class="nc5-leader-card">\n' +
1303               '    ' + photoHtml + '\n' +
1304               '    <div class="nc5-leader-info">\n' +
1305               '        <p class="nc5-leader-name">' + esc(d.guest_name) + '</p>\n' +
1306               '        <p class="nc5-leader-title">' + esc(d.guest_title) + ' · ' + esc(d.guest_company) + '</p>\n' +
1307               '    </div>\n' +
1308               '</div>\n' +
1309               '<p class="ic-intro">' + esc(d.intro) + '</p>\n' +
1310               '</div>\n' +
1311               qaBlocks + '\n' +
1312               '</div>\n' +
1313               '<!--kg-card-end: html-->';
1314    }
1315
1316    form.addEventListener('submit', function(e) {
1317        e.preventDefault();
1318        errorEl.style.display = 'none';
1319        successEl.style.display = 'none';
1320
1321        // Collect all Q&A pairs
1322        var pairs = [];
1323        var qaPairs = qaList.querySelectorAll('.or-qa-pair');
1324        var allFilled = true;
1325        qaPairs.forEach(function(pair) {
1326            var q = pair.querySelector('.or-q').value.trim();
1327            var a = pair.querySelector('.or-a').value.trim();
1328            if (q || a) {
1329                if (!q || !a) { allFilled = false; }
1330                else { pairs.push({ q: q, a: a }); }
1331            }
1332        });
1333
1334        var d = {
1335            title:        form.title.value.trim(),
1336            guest_name:   form.guest_name.value.trim(),
1337            guest_title:  form.guest_title.value.trim(),
1338            guest_company:form.guest_company.value.trim(),
1339
1340            intro:        form.intro.value.trim(),
1341            quote:        form.quote.value.trim(),
1342            photo:        photoBase64,
1343            pairs:        pairs
1344        };
1345
1346        var missing = !d.title || !d.guest_name || !d.guest_title || !d.guest_company ||
1347                      !d.intro || !d.quote || pairs.length === 0 || !allFilled;
1348        if (missing) {
1349            errorEl.textContent = 'Please fill in all required fields. Every question must have an answer.';
1350            errorEl.style.display = 'block';
1351            return;
1352        }
1353
1354        submitBtn.disabled = true;
1355        submitBtn.textContent = 'Creating draft…';
1356
1357        ghostJWT(GHOST_ADMIN_KEY).then(function(token) {
1358            var imgPromise = photoFile
1359                ? uploadImageToGhost(photoFile, token).then(function(url) { d.photo = url; })
1360                : Promise.resolve();
1361            return imgPromise.then(function() {
1362                d.html = buildOrHtml(d);
1363                return fetch(GHOST_API_URL, {
1364                    method: 'POST',
1365                    headers: { 'Content-Type': 'application/json', 'Authorization': 'Ghost ' + token },
1366                    body: JSON.stringify({
1367                        posts: [{
1368                            title: d.title,
1369                            html: d.html,
1370                            status: 'draft',
1371                            tags: [{ slug: 'inside-business' }]
1372                        }]
1373                    })
1374                });
1375            });
1376        }).then(function(res) {
1377            if (!res.ok) return res.json().then(function(e) { throw new Error(JSON.stringify(e)); });
1378            return res.json();
1379        }).then(function() {
1380            successEl.style.display = 'block';
1381            form.reset();
1382            photoBase64 = ''; photoFile = null;
1383            photoPreview.style.display = 'none';
1384            // Reset dynamic Q&A pairs back to 7
1385            var extra = qaList.querySelectorAll('.or-qa-pair');
1386            extra.forEach(function(p, i) { if (i >= 7) p.remove(); });
1387            qaPairCount = 7;
1388            window.scrollTo({ top: 0, behavior: 'smooth' });
1389        }).catch(function(err) {
1390            console.error('Inside Business submit error:', err);
1391            errorEl.textContent = 'Something went wrong. Please try again in a moment.';
1392            errorEl.style.display = 'block';
1393        }).finally(function() {
1394            submitBtn.disabled = false;
1395            submitBtn.textContent = 'Create Draft →';
1396        });
1397    });
1398})();
1399
1400// HTML card swipe hint.
1401// CSS (main.css) makes .kg-html-card the scroll container (overflow-x:auto).
1402// body has no overflow-x so iOS Safari routes horizontal touch events here.
1403// This JS adds a "swipe →" visual hint on cards that actually overflow.
1404// Double-rAF waits for layout to fully settle before measuring.
1405(function() {
1406    function initCard(card) {
1407        requestAnimationFrame(function() {
1408            requestAnimationFrame(function() {
1409                if (card.scrollWidth <= card.clientWidth + 2) return;
1410                card.style.position = 'relative';
1411                var hint = document.createElement('div');
1412                hint.className = 'kg-html-card-hint';
1413                hint.textContent = 'swipe →';
1414                card.appendChild(hint);
1415                card.addEventListener('scroll', function() {
1416                    hint.style.display = 'none';
1417                }, { passive: true, once: true });
1418            });
1419        });
1420    }
1421    document.querySelectorAll('.kg-html-card').forEach(initCard);
1422})();
1423
1424// Tap-to-enlarge lightbox for article images.
1425// Any image inside .gh-content can be tapped/clicked to view full size.
1426// Excludes author avatars and emoji/icon images.
1427(function() {
1428    var content = document.querySelector('.gh-content');
1429    if (!content) return;
1430
1431    // Build overlay
1432    var overlay = document.createElement('div');
1433    overlay.id = 'img-lightbox';
1434    var closeBtn = document.createElement('span');
1435    closeBtn.id = 'img-lightbox-close';
1436    closeBtn.textContent = '✕';
1437    var bigImg = document.createElement('img');
1438    overlay.appendChild(closeBtn);
1439    overlay.appendChild(bigImg);
1440    document.body.appendChild(overlay);
1441
1442    function open(src) {
1443        bigImg.src = src;
1444        overlay.classList.add('open');
1445        document.body.style.overflow = 'hidden';
1446    }
1447    function close() {
1448        overlay.classList.remove('open');
1449        document.body.style.overflow = '';
1450    }
1451
1452    overlay.addEventListener('click', close);
1453    bigImg.addEventListener('click', function(e) { e.stopPropagation(); });
1454    closeBtn.addEventListener('click', close);
1455    document.addEventListener('keydown', function(e) { if (e.key === 'Escape') close(); });
1456
1457    // Wire up all content images (exclude small avatars / icons)
1458    content.querySelectorAll('img').forEach(function(el) {
1459        if (el.naturalWidth && el.naturalWidth < 80) return; // skip tiny icons
1460        el.style.cursor = 'zoom-in';
1461        el.addEventListener('click', function() { open(el.src); });
1462    });
1463})();
1464
1465// Copy-intercept — appends source URL when a reader copies text from any
1466// article page. Shows a brief toast notice. Applies site-wide on post pages.
1467(function() {
1468    var body = document.querySelector('.post-body, .gh-content');
1469    if (!body) return;
1470
1471    // Build a small toast notice element
1472    var notice = document.createElement('div');
1473    notice.className = 'copy-notice';
1474    notice.textContent = '© newsco.in — source link added to your clipboard';
1475    document.body.appendChild(notice);
1476
1477    var hideTimer;
1478    function showNotice() {
1479        notice.classList.add('show');
1480        clearTimeout(hideTimer);
1481        hideTimer = setTimeout(function() { notice.classList.remove('show'); }, 2800);
1482    }
1483
1484    document.addEventListener('copy', function(e) {
1485        var selection = window.getSelection();
1486        if (!selection || selection.isCollapsed) return;
1487        var text = selection.toString();
1488        if (!text || text.trim().length < 10) return;  // ignore tiny selections
1489
1490        var url = window.location.href;
1491        var sourceNote = '\n\nSource: ' + url + '\n© newsco.in — All rights reserved.';
1492
1493        try {
1494            e.clipboardData.setData('text/plain', text + sourceNote);
1495            e.clipboardData.setData('text/html',
1496                text +
1497                '<br><br><small>Source: <a href="' + url + '">' + url + '</a><br>' +
1498                '&copy; newsco.in &mdash; All rights reserved.</small>'
1499            );
1500            e.preventDefault();
1501            showNotice();
1502        } catch (err) {
1503            // If clipboard API fails, let the copy proceed without interception
1504        }
1505    });
1506})();
1507
1508// In-article ad slot — inserted ~60% through the article body.
1509// Runs on every post page, regardless of segment/tag (incl. Press Releases),
1510// since all posts share post.hbs.
1511(function() {
1512    const body = document.querySelector('.post-body');
1513    if (!body) return;
1514    const children = Array.prototype.slice.call(body.children);
1515    if (children.length < 2) return;
1516
1517    const totalHeight = body.scrollHeight;
1518    const targetHeight = totalHeight * 0.6;
1519    let cumulative = 0;
1520    let insertBeforeEl = null;
1521
1522    for (let i = 0; i < children.length; i++) {
1523        cumulative += children[i].offsetHeight;
1524        if (cumulative >= targetHeight) {
1525            insertBeforeEl = children[i + 1] || null;
1526            break;
1527        }
1528    }
1529
1530    const ad = document.createElement('div');
1531    ad.className = 'ad-zone ad-in-article';
1532    ad.innerHTML =
1533        '<div class="ad-placeholder">' +
1534            '<span>ADVERTISEMENT</span>' +
1535            '<span>In-article Banner 728 x 90</span>' +
1536            '<span>60% scroll-depth slot</span>' +
1537        '</div>';
1538
1539    if (insertBeforeEl) {
1540        body.insertBefore(ad, insertBeforeEl);
1541    } else {
1542        body.appendChild(ad);
1543    }
1544})();

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.