PageSourceSearch

https://netsecurity.com/shared/pages/home-1.js?v=20260928-tour

js netsecurity.com collected 2026-10-02 05:28:09 UTC 172,946 bytes, 2,024 lines download raw bytes

1(function () {
2  var still = document.documentElement.classList.contains('poster') || window.PUI.REDUCED;
3  /* Hero (mockup): threats arrive from the left and meet the agent's shield, a curved glowing membrane with
4     the ThreatResponder logo bent along it; what the agent does fans out on the right. The lines sway,
5     particles stream along them all the time, and every second or two one threat is carried in, flares on
6     the shield, makes the logo pulse where it hit, and flows on to the capabilities that answer it.
7     Membrane keeps a standing list of threats with swaying lines; Waves leaves the membrane alone and
8     sends threats in as blocks (campaigns). */
9  (function shieldHero() {
10    const mode = window.__heroMode, host = document.getElementById('tsh');
11    if (!host || ['layers', 'console'].indexOf(mode) < 0) return;
12    /* Quiet = Waves, one notch fainter everywhere, with the capabilities a little larger and further apart */
13    /* Layers / Console / Console · colour: Waves at the Quiet level on the left, a different scene on the right */
14    const right = ['layers', 'console', 'console2'].indexOf(mode) >= 0 ? mode : 'caps';
15    const quiet = mode === 'quiet' || right !== 'caps', strike = mode === 'waves' || quiet, capGap = quiet ? 97 : 88, capTop = quiet ? 80 : 102, capR = quiet ? 23 : 21, capTx = quiet ? 38 : 34;
16    host.classList.add('tsv', 'v-membrane', 'v-' + mode);
17    if (quiet) host.classList.add('v-quiet');
18    let upd = null;
19    const hero = document.getElementById('hero'), text = hero.querySelector('.hgrid > div'), shot = document.getElementById('heroShot');
20    const NS = 'http://www.w3.org/2000/svg';
21    const IC = {
22      mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 7l8.5 6 8.5-6"/>',
23      lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
24      code: '<path d="M8 8l-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/>',
25      bug: '<path d="M8 8h8v6a4 4 0 0 1-8 0z"/><path d="M12 4v4M4 12h4M16 12h4M5 18l3-2M19 18l-3-2M5 6l3 2M19 6l-3 2"/>',
26      user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"/>',
27      lateral: '<circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="18" r="2.5"/><circle cx="12" cy="6" r="2.5"/><path d="M10.8 8.2L7.2 15.8M13.2 8.2l3.6 7.6M8.5 18h7"/>',
28      beacon: '<circle cx="12" cy="12" r="2"/><path d="M8.5 15.5a5 5 0 0 1 0-7M15.5 8.5a5 5 0 0 1 0 7M5.6 18.4a9 9 0 0 1 0-12.8M18.4 5.6a9 9 0 0 1 0 12.8"/>',
29      search: '<circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/>',
30      mitre: '<circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="5" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><path d="M6.5 6.5l3.7 3.7M17.5 6.5l-3.7 3.7M6.5 17.5l3.7-3.7M17.5 17.5l-3.7-3.7"/>',
31      doc: '<path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5M10 13h6M10 17h6"/>',
32      shield: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/>',
33      cross: '<circle cx="12" cy="12" r="7"/><path d="M12 2v5M12 17v5M2 12h5M17 12h5"/><circle cx="12" cy="12" r="1.2"/>',
34      bars: '<path d="M4 20V11M10 20V5M16 20v-6M2.5 20.5h19"/>'
35    };
36    const THREATS = [
37      { ic: 'mail', n: 'Phishing', s: 'MALICIOUS EMAIL', caps: ['det', 'alert', 'contain'] },
38      { ic: 'lock', n: 'Ransomware', s: 'ENCRYPTION ATTEMPT', caps: ['det', 'mitre', 'contain'] },
39      { ic: 'code', n: 'Malicious script', s: 'POWERSHELL · BASH', caps: ['det', 'mitre', 'hunt'] },
40      { ic: 'bug', n: 'Exploit', s: 'ZERO-DAY · REMOTE', caps: ['det', 'hunt', 'fx'] },
41      { ic: 'user', n: 'Credential theft', s: 'STOLEN CREDENTIALS', caps: ['det', 'mitre', 'alert'] },
42      { ic: 'lateral', n: 'Lateral movement', s: 'REMOTE LOGONS', caps: ['det', 'hunt', 'fx'] },
43      { ic: 'beacon', n: 'C2 beacon', s: 'OUTBOUND CONNECTION', caps: ['det', 'contain', 'hunt'] }
44    ];
45    const CAPS = [
46      { id: 'det', ic: 'search', n: 'Threat detection', s: 'FOUR ENGINES · ONE VERDICT' },
47      { id: 'mitre', ic: 'mitre', n: 'MITRE ATT&CK mapping', s: 'EVERY STEP, A TECHNIQUE' },
48      { id: 'alert', ic: 'doc', n: 'Custom alert rules', s: 'ALERT · NOTIFY · INTEGRATE' },
49      { id: 'contain', ic: 'shield', n: 'Containment', s: 'CONTAIN · KILL · QUARANTINE' },
50      { id: 'hunt', ic: 'cross', n: 'Threat hunting', s: 'ONE QUERY, EVERY ENDPOINT' },
51      { id: 'fx', ic: 'bars', n: 'Forensics', s: 'TIMELINE · EVIDENCE · REPORT' }
52    ];
53    const OX = 900, OY = 322, RIM = 432, capX = 852;
54    function ax(y, r) { return OX - Math.sqrt(Math.max(0, r * r - (y - OY) * (y - OY))); }
55    function pts(r, y0, y1) { const a = []; for (let y = y0; y <= y1; y += 8) a.push(ax(y, r).toFixed(1) + ',' + y); return a; }
56    function arc(r, y0, y1) { return 'M' + pts(r, y0, y1).join(' L'); }
57    function esc(v) { return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }
58    function icon(n, x, y, s) { return '<svg class="ic" x="' + x + '" y="' + y + '" width="' + s + '" height="' + s + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + IC[n] + '</svg>'; }
59    function badge(cx, cy, r, ic) { return '<circle class="ring" cx="' + cx + '" cy="' + cy + '" r="' + r + '"/>' + icon(ic, cx - 10, cy - 10, 20); }
60    /* a line = four control points; it sways by moving the two inner ones */
61    function line(p0, p1, p2, p3, amp) { return { b: [p0, p1, p2, p3], ph: Math.random() * 6.3, amp: amp, sp: .8 + Math.random() * .5 }; }
62    function cur(L, t) { const a = Math.sin(t * .00055 * L.sp + L.ph) * L.amp, b = Math.cos(t * .00047 * L.sp + L.ph * 1.7) * L.amp; return [L.b[0], [L.b[1][0], L.b[1][1] + a], [L.b[2][0], L.b[2][1] + b], L.b[3]]; }
63    function bez(P, u) { const v = 1 - u, A = v * v * v, B = 3 * v * v * u, C = 3 * v * u * u, D = u * u * u; return [A * P[0][0] + B * P[1][0] + C * P[2][0] + D * P[3][0], A * P[0][1] + B * P[1][1] + C * P[2][1] + D * P[3][1]]; }
64    function dOf(P) { return 'M' + P[0][0].toFixed(1) + ',' + P[0][1].toFixed(1) + ' C' + P[1][0].toFixed(1) + ',' + P[1][1].toFixed(1) + ' ' + P[2][0].toFixed(1) + ',' + P[2][1].toFixed(1) + ' ' + P[3][0].toFixed(1) + ',' + P[3][1].toFixed(1); }
65
66    const INNER = 384, LOGO_R = 408, LOGO_K = .92;
67    let s = '<svg viewBox="0 0 1100 640" preserveAspectRatio="xMinYMid meet" xmlns="http://www.w3.org/2000/svg"><defs>' +
68      '<pattern id="tsvMesh" width="16" height="27.7" patternUnits="userSpaceOnUse"><path d="M8,0 L16,4.62 L16,13.86 L8,18.48 L0,13.86 L0,4.62 Z M8,18.48 L8,27.7" fill="none" class="mesh-p"/></pattern>' +
69      '<clipPath id="tsvBand"><path d="M' + pts(RIM, 24, 620).concat(pts(INNER, 24, 620).reverse()).join(' L') + ' Z"/></clipPath>' +
70      '<linearGradient id="tsvFadeG" gradientUnits="userSpaceOnUse" x1="0" y1="20" x2="0" y2="624"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".24" stop-color="#fff"/><stop offset=".76" stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
71      '<mask id="tsvFade" maskUnits="userSpaceOnUse" x="300" y="0" width="360" height="640"><rect x="300" y="0" width="360" height="640" fill="url(#tsvFadeG)"/></mask>' +
72      '<linearGradient id="tsvBandG" gradientUnits="userSpaceOnUse" x1="' + (OX - RIM) + '" y1="0" x2="' + (OX - INNER + 30) + '" y2="0"><stop offset="0" stop-color="#9dbcff" stop-opacity=".22"/><stop offset=".45" stop-color="#6f95ff" stop-opacity=".08"/><stop offset="1" stop-color="#6f95ff" stop-opacity="0"/></linearGradient>' +
73      '<radialGradient id="tsvHot"><stop offset="0" stop-color="#d6e4ff" stop-opacity=".45"/><stop offset="1" stop-color="#d6e4ff" stop-opacity="0"/></radialGradient>' +
74      '<filter id="tsvBlur" x="-60%" y="-10%" width="220%" height="120%"><feGaussianBlur stdDeviation="5"/></filter>' +
75      '<radialGradient id="tsvFlare"><stop offset="0" stop-color="#f4f7ff" stop-opacity=".9"/><stop offset=".35" stop-color="#b9ccff" stop-opacity=".35"/><stop offset="1" stop-color="#b9ccff" stop-opacity="0"/></radialGradient><linearGradient id="tsvLayer" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#9fc4ff" stop-opacity=".085"/><stop offset="1" stop-color="#6597ff" stop-opacity=".035"/></linearGradient></defs>';
76
77    /* the membrane, layered like the reference: halo rings toward the threats, a glowing rim, a translucent
78       band with a hex mesh and the logo; it fades out at the top and bottom */
79    s += '<g mask="url(#tsvFade)">' +
80      '<path class="sh-halo" d="' + arc(446, 24, 620) + '"/>' +
81      [[520, .03], [494, .05], [470, .08], [452, .14]].map(function (a) { return '<path class="sh-ring" style="stroke-opacity:' + a[1] + '" d="' + arc(a[0], 24, 620) + '"/>'; }).join('') +
82      '<path d="M' + pts(RIM, 24, 620).concat(pts(INNER, 24, 620).reverse()).join(' L') + ' Z" fill="url(#tsvBandG)"/>' +
83      '<rect x="360" y="20" width="200" height="604" fill="url(#tsvMesh)" clip-path="url(#tsvBand)" class="mesh"/>' +
84      '<ellipse cx="' + (OX - RIM + 4) + '" cy="' + OY + '" rx="34" ry="170" fill="url(#tsvHot)" class="hot"/>' +
85      '<path class="sh-rimglow" filter="url(#tsvBlur)" d="' + arc(RIM, 24, 620) + '"/><path class="sh-main" d="' + arc(RIM, 24, 620) + '"/><path class="sh-in" d="' + arc(INNER, 24, 620) + '"/>' +
86      '<g class="specks"></g></g><g class="mlogo"></g>';
87
88    /* threats on the left; each has a swaying dashed line to the rim (Membrane only; Waves sends them in blocks) */
89    if (!strike) THREATS.forEach(function (t, i) {
90      const y = 92 + i * 76, bx = 252, iy = OY + (y - OY) * .5, ix = ax(iy, RIM + 2) - 2, x0 = bx + 24;
91      t.ix = ix; t.iy = iy;
92      t.L = line([x0, y], [x0 + 95, y + (i % 2 ? 10 : -10)], [ix - 95, iy], [ix, iy], 7 + Math.random() * 8);
93      s += '<path class="tl" data-l="t' + i + '"/><circle class="tend" cx="' + ix + '" cy="' + iy + '" r="2"/>' +
94        '<g class="th" data-t="' + i + '">' + badge(bx, y, 21, t.ic) + '<text class="ttl" x="' + (bx - 34) + '" y="' + (y - 2) + '" text-anchor="end">' + esc(t.n) + '</text><text class="tsb" x="' + (bx - 34) + '" y="' + (y + 13) + '" text-anchor="end">' + t.s + '</text></g>';
95    });
96
97
98    /* Right-hand scenes for Layers and Console (Waves on the left, Quiet level). A scene gives the fibre
99       targets, which of them a threat reaches, and what happens when a signal arrives */
100    const TX = [
101      { t: 'T1566', act: 'attachment quarantined' }, { t: 'T1486', act: 'process killed' }, { t: 'T1059', act: 'process tree killed' },
102      { t: 'T1203', act: 'operation blocked' }, { t: 'T1003', act: 'endpoint contained' }, { t: 'T1021', act: 'endpoint contained' }, { t: 'T1071', act: 'connection closed' }
103    ];
104    /* the product's own response actions (Resolve / Validate / detection-rule actions), rotated per threat and
105       weighted so Quarantine and Kill process come up most often, Contain endpoint less */
106    const RESP = [['Quarantine', 'Quarantine', 'Clean'], ['Kill process', 'Revert', 'Quarantine'], ['Kill process', 'Quarantine', 'Kill process'], ['Block', 'Kill process', 'Kill process'],
107      ['Kill process', 'Contain endpoint', 'Quarantine'], ['Contain endpoint', 'Kill process', 'Quarantine'], ['Close connection', 'Kill process', 'Quarantine']];
108    const respN = RESP.map(function () { return 0; });
109    function respFor(i) { return RESP[i][respN[i]++ % RESP[i].length]; }
110    function buildRight(kind) {
111      const R = { targets: [], svg: '' }, lx = function (y) { return ax(OY + (y - OY) * .3, INNER) + 6; }, ly = function (y) { return OY + (y - OY) * .3; };
112      function fibres(y, to, amp) { return [-5, 0, 5].map(function (o) { return line([lx(y), ly(y) + o], [lx(y) + 70, ly(y) + o * 1.5], [to[0] - 80, to[1] + o * .5], to, amp || 5 + Math.random() * 5); }); }
113      if (kind === 'layers') {
114        /* three glass slabs in depth, after the capability study: a skewed plane with a thickness edge, a faint
115           grid, corner dots, the layer's icon and name, two small record slabs, and a card in front. Detect sits
116           on top, investigate at the bottom. No pulses travel to them: after a hit the layers simply light up a
117           little, one threat at a time, paced, so each detection can be read */
118        const K = 1.1, X0 = 598, LV = [
119          { id: 'investigate', y: 448, name: 'INVESTIGATE', ic: 'bars', title: 'Hunting + forensics', sub: 'Timeline · searchable evidence' },
120          { id: 'respond', y: 324, name: 'RESPOND', ic: 'shield', title: '', sub: '' },
121          { id: 'detect', y: 200, name: 'DETECT', ic: 'search', title: 'Threat detection', sub: 'Custom rules · ATT&CK mapping' }];
122        const top = LV[2].y, bot = LV[0].y;
123        R.svg += '<path class="ly-v" d="M' + X0 + ',' + top + 'V' + (bot + 10 * K) + 'M' + (X0 + 424 * K) + ',' + (top + 3 * K) + 'V' + (bot + 13 * K) + 'M' + (X0 + 214 * K) + ',' + (top + 78 * K) + 'V' + (bot + 88 * K) + '"/>';
124        LV.forEach(function (l) {
125          const plane = 'M0 0 208-63 424 3 214 78Z';
126          let body = '<text class="tsb ly-d" x="44" y="37">' + esc(l.sub) + '</text>';
127          if (l.id === 'detect') body += '<g class="dx-pos"><g class="dx-pill"><rect height="14" rx="7"/><text x="7" y="10"></text></g></g>';
128          if (l.id === 'respond') body = '<text class="ttl ra" x="44" y="30">Quarantine</text>';
129          R.svg += '<g class="ly" data-c="' + l.id + '" transform="translate(' + X0 + ' ' + l.y + ')"><g transform="scale(' + K + ')">' +
130            '<path class="ly-p" d="' + plane + '"/><path class="ly-e" d="M0 0v10l214 78 210-76V3L214 78Z"/><path class="ly-hi" d="' + plane + '"/>' +
131            [.25, .5, .75].map(function (f) { return '<path class="ly-g" d="M' + (208 * f) + ' ' + (-63 * f) + 'l216 66M' + (216 * f) + ' ' + (66 * f) + 'l208-63"/>'; }).join('') +
132            '<circle class="ly-dot" r="3"/><circle class="ly-dot" cx="424" cy="3" r="3"/><circle class="ly-dot" cx="214" cy="78" r="3"/>' +
133            '<g class="ly-rec"><path d="m125-3 39-12 46 14-39 13Z m54 17 39-12 46 14-39 13Z"/><path class="ln" d="m142-1 20-6m30 21 20-6"/></g>' +
134            icon(l.ic, 306, -6, 22) + '<text class="ly-n" x="317" y="36" text-anchor="middle">' + l.name + '</text></g>' +
135            '<g class="ly-c" transform="translate(' + (28 * K) + ' ' + (37 * K) + ')"><rect width="214" height="50" rx="7"/>' + icon(l.ic, 11, 14, 21) +
136            (l.title ? '<text class="ttl" x="44" y="22">' + esc(l.title) + '</text>' : '') + body + '</g></g>';
137          R.targets.unshift({ id: l.id, sub: l.sub, F: fibres(l.y, [X0 - 5, l.y]) });
138        });
139        R.svg += '<text class="zn" x="' + (X0 + 214 * K) + '" y="590">ALWAYS-ON VISIBILITY · COORDINATED RESPONSE</text>';
140        R.noPulse = true;
141        R.pick = function () { return []; };
142        const queue = [], lit = {};
143        let nextOut = 0, arrivals = 0;
144        R.init = function () {
145          R.targets.forEach(function (tg) { tg.el = host.querySelector('.ly[data-c="' + tg.id + '"]'); tg.d = tg.el.querySelector('.ly-d'); lit[tg.id] = 0; });
146          R.pill = host.querySelector('.dx-pill'); R.ra = host.querySelector('.ra');
147        };
148        /* every arriving threat queues one detection; every third also queues a custom-alert entry */
149        R.arrive = function (id, th) {
150          if (queue.length < 4) queue.push({ th: th, i: THREATS.indexOf(th) });
151          if (++arrivals % 3 === 0 && queue.length < 5) queue.push({ alert: true, th: th });
152        };
153        const tgt = function (id) { return R.targets.find(function (x) { return x.id === id; }); };
154        function show(item, t) {
155          const det = tgt('detect'), pill = R.pill, pt = pill.querySelector('text'), pr = pill.querySelector('rect');
156          det.d.textContent = item.alert ? 'Custom alert rule fired' : item.th.n;
157          pt.textContent = item.alert ? 'SLACK' : TX[item.i].t;
158          const x = 44 + det.d.getComputedTextLength() + 7, w = pt.getComputedTextLength() + 14;
159          pill.parentNode.setAttribute('transform', 'translate(' + x.toFixed(1) + ' 27)'); pr.setAttribute('width', w.toFixed(1));
160          pill.classList.remove('pop'); void pill.getBBox(); pill.classList.add('pop');
161          pill.classList.toggle('alert', !!item.alert);
162          lit.detect = t + 1500; det.until = t + 1700;
163          if (!item.alert) {
164            R.pending = [[t + 550, 'respond', respFor(item.i)], [t + 1100, 'investigate']];
165          }
166        }
167        R.step = function (t) {
168          if (queue.length && t >= nextOut) { show(queue.shift(), t); nextOut = t + 1800; }
169          (R.pending || []).slice().forEach(function (p) {
170            if (t < p[0]) return;
171            R.pending.splice(R.pending.indexOf(p), 1);
172            lit[p[1]] = t + 1400;
173            /* the response action is the card's title, swapped in turn (Quarantine, Kill process, Contain endpoint…) */
174            if (p[1] === 'respond') { R.ra.textContent = p[2]; R.ra.classList.remove('swap'); void R.ra.getBBox(); R.ra.classList.add('swap'); }
175            if (p[1] === 'investigate') { const inv = tgt('investigate'); inv.d.textContent = 'Timeline · evidence preserved'; inv.until = t + 1600; }
176          });
177          R.targets.forEach(function (tg) {
178            tg.el.classList.toggle('on', t < lit[tg.id]);
179            if (tg.until && t > tg.until) { tg.until = 0; if (tg.id === 'detect') { tg.d.textContent = tg.sub; R.pill.classList.remove('pop'); } else if (tg.d) tg.d.textContent = tg.sub; }
180          });
181        };
182        return R;
183      }
184      /* the console: a wireframe of the Overview, notifications arrive as each threat is handled */
185      const CX = 590, CY = 96, CW = 480, CH = 470, color = false;
186      const KPI = [['Threats', 41, 'crit'], ['Vulnerabilities', 61, 'warn'], ['Identity', 27, 'info'], ['Endpoints', '42/51', 'ok']];
187      R.svg += '<g class="cs' + (color ? ' col' : '') + '" data-c="console"><rect class="cs-w" x="' + CX + '" y="' + CY + '" width="' + CW + '" height="' + CH + '" rx="14"/>' +
188        '<path class="cs-l" d="M' + CX + ',' + (CY + 36) + 'H' + (CX + CW) + 'M' + (CX + 44) + ',' + (CY + 36) + 'V' + (CY + CH) + '"/>' +
189        [0, 1, 2].map(function (j) { return '<circle class="cs-dot" cx="' + (CX + 18 + j * 12) + '" cy="' + (CY + 18) + '" r="3.2"/>'; }).join('') +
190        '<text class="cs-t" x="' + (CX + 60) + '" y="' + (CY + 22) + '">ThreatResponder Management Console</text><text class="cs-t dim" x="' + (CX + CW - 16) + '" y="' + (CY + 22) + '" text-anchor="end">All companies · last 30 days</text>' +
191        ['search', 'shield', 'mitre', 'bars', 'doc'].map(function (ic, j) { return icon(ic, CX + 13, CY + 54 + j * 42, 18); }).join('') +
192        KPI.map(function (k, j) { const x = CX + 60 + j * 102; return '<g class="kpi ' + k[2] + '"><rect x="' + x + '" y="' + (CY + 52) + '" width="94" height="58" rx="7"/><rect class="kb" x="' + x + '" y="' + (CY + 52) + '" width="3" height="58" rx="1.5"/><text class="kl" x="' + (x + 12) + '" y="' + (CY + 70) + '">' + k[0] + '</text><text class="kv" x="' + (x + 12) + '" y="' + (CY + 97) + '"' + (j === 0 ? ' data-kpi="threats"' : '') + '>
192' + k[1] + '</text></g>'; }).join('') +
193        '<rect class="cs-b" x="' + (CX + 60) + '" y="' + (CY + 124) + '" width="' + (CW - 76) + '" height="130" rx="8"/><text class="cs-h" x="' + (CX + 74) + '" y="' + (CY + 144) + '">WHAT ARRIVED · LAST 14 DAYS</text>' +
194        Array.from({ length: 14 }, function (_, j) { const x = CX + 76 + j * 27, h = 20 + ((j * 37) % 55); return '<rect class="bar' + (j % 3 === 0 ? ' b2' : j % 4 === 1 ? ' b3' : '') + '" data-bar="' + j + '" x="' + x + '" y="' + (CY + 240 - h) + '" width="16" height="' + h + '" rx="2"/>'; }).join('') +
195        '<rect class="cs-b" x="' + (CX + 60) + '" y="' + (CY + 268) + '" width="' + (CW - 76) + '" height="186" rx="8"/><text class="cs-h" x="' + (CX + 74) + '" y="' + (CY + 288) + '">NEEDS YOUR ATTENTION</text><g class="rows"></g>' +
196        '<g class="toasts"></g></g>';
197      /* no connectors: the hit is handed straight to the console, which answers with a calm notification */
198      R.targets.push({ id: 'console', F: [] });
199      R.noPulse = true;
200      R.pick = function () { return []; };
201      const rows = [], toasts = [];
202      let threats = 41;
203      R.init = function () {
204        R.rowsG = host.querySelector('.cs .rows'); R.toastG = host.querySelector('.cs .toasts'); R.kpi = host.querySelector('[data-kpi="threats"]'); R.bars = host.querySelectorAll('.cs [data-bar]');
205        [['Ransomware behaviour stopped', 'FIN-SRV-02 · contained', 'crit'], ['Credential dumping via LSASS', 'LAPTOP-EAF574 · contained', 'crit'], ['Unusual service-ticket burst', 'DEV-WKS-07 · reviewing', 'warn']].forEach(function (r) { addRow(r[0], r[1], r[2], -1e9); });
206      };
207      function el(tag, a, parent) { const e = document.createElementNS(NS, tag); Object.keys(a).forEach(function (k) { e.setAttribute(k, a[k]); }); parent.appendChild(e); return e; }
208      function addRow(title, sub, sev, t) {
209        const g = el('g', { class: 'row ' + sev }, R.rowsG);
210        g.innerHTML = '<rect width="' + (CW - 104) + '" height="42" rx="6"/><rect class="rs" width="3" height="42" rx="1.5"/><text class="rt" x="14" y="17">' + esc(title) + '</text><text class="rd" x="14" y="32">' + esc(sub) + '</text>';
211        rows.unshift({ g: g, t0: t, y: CY + 292 });
212        while (rows.length > 4) rows.pop().g.remove();
213      }
214      function toast(title, sub, sev, ic, t) {
215        const g = el('g', { class: 'toast ' + sev }, R.toastG);
216        g.innerHTML = '<rect width="224" height="46" rx="8"/><rect class="rs" width="3" height="46" rx="1.5"/>' + icon(ic, 12, 14, 18) + '<text class="rt" x="40" y="19">' + esc(title) + '</text><text class="rd" x="40" y="34">' + esc(sub) + '</text>';
217        toasts.unshift({ g: g, t0: t, y: CY + 46, x: CX + CW - 208 });
218      }
219      /* each arriving threat queues one notification; they go out as a calm pulse, one every ~1.6 s,
220         rotating threat detected → custom alert → contained, so a block reads as one short sequence */
221      const queue = [];
222      let kindN = 0, nextOut = 0;
223      R.arrive = function (id, th) {
224        const i = THREATS.indexOf(th), k = kindN++ % 3;
225        if (queue.length < 3) queue.push(k === 0 ? ['Threat detected', th.n + ' · ' + TX[i].t, 'crit', 'search', th, i] : k === 1 ? ['Custom alert rule', th.n + ' · alert sent to Slack', 'warn', 'doc', th, i] : [respFor(i), th.n + ' · response applied', 'ok', 'shield', th, i]);
226      };
227      R.step = function (t, dt) {
228        if (queue.length && t >= nextOut) {
229          const n = queue.shift();
230          toast(n[0], n[1], n[2], n[3], t);
231          if (n[2] === 'ok' || n[2] === 'crit') { addRow(n[4].n + ' stopped', 'story ' + TX[n[5]].t + ' · ' + TX[n[5]].act, 'crit', t); }
232          if (n[2] === 'crit') { threats++; if (R.kpi) R.kpi.textContent = threats; }
233          nextOut = t + 1600;
234        }
235        toasts.forEach(function (o, j) {
236          const age = t - o.t0, ty = CY + 46 + j * 54, tx = CX + CW - 238;
237          o.x += (tx - o.x) * Math.min(1, dt / 120); o.y += (ty - o.y) * Math.min(1, dt / 160);
238          o.g.setAttribute('transform', 'translate(' + o.x.toFixed(1) + ' ' + o.y.toFixed(1) + ')');
239          o.g.style.opacity = Math.max(0, Math.min(1, age / 450, (3400 - age) / 800, j < 3 ? 1 : 0)).toFixed(2);
240          o.g.classList.toggle('fresh', age < 900);
241        });
242        for (let j = toasts.length - 1; j >= 0; j--) if (t - toasts[j].t0 > 3400 || j > 5) { toasts[j].g.remove(); toasts.splice(j, 1); }
243        rows.forEach(function (o, j) {
244          const ty = CY + 300 + j * 48;
245          o.y += (ty - o.y) * Math.min(1, dt / 200);
246          o.g.setAttribute('transform', 'translate(' + (CX + 74) + ' ' + o.y.toFixed(1) + ')');
247          o.g.style.opacity = j < 3 ? Math.max(0, Math.min(1, (t - o.t0) / 500)).toFixed(2) : '0';
248        });
249        if (R.bars) R.bars.forEach(function (b, j) { const base = 20 + ((j * 37) % 55), h = base + Math.sin(t * .0007 + j) * 5; b.setAttribute('height', h.toFixed(1)); b.setAttribute('y', (CY + 240 - h).toFixed(1)); });
250      };
251      return R;
252    }
253    const RIGHT = right === 'caps' ? null : buildRight(right), TG = RIGHT ? RIGHT.targets : CAPS;
254
255    /* capabilities on the right; swaying fibres leave the inside of the membrane and fan out to them */
256    const offs = [-7, 0, 7];
257    if (RIGHT) s += TG.map(function (c, i) { return '<g class="fb" data-c="' + c.id + '">' + c.F.map(function (x, k) { return '<path data-l="c' + i + '-' + k + '"/>'; }).join('') + '</g>'; }).join('') + RIGHT.svg;
258    else CAPS.forEach(function (c, i) {
259      const y = capTop + i * capGap, ys = OY + (y - OY) * .3, xs = ax(ys, INNER) + 6;
260      c.xs = xs; c.ys = ys;
261      c.F = offs.map(function (o) { return line([xs, ys + o], [xs + 110, ys + o * 1.8], [capX - 130, y + o * .6], [capX - capR - 5, y], 5 + Math.random() * 7); });
262      s += '<g class="fb" data-c="' + c.id + '">' + c.F.map(function (f, k) { return '<path data-l="c' + i + '-' + k + '"/>'; }).join('') + '</g>' +
263        '<g class="cp" data-c="' + c.id + '">' + badge(capX, y, capR, c.ic) + '<text class="ttl" x="' + (capX + capTx) + '" y="' + (y - 2) + '">' + esc(c.n) + '</text><text class="tsb" x="' + (capX + capTx) + '" y="' + (y + 13) + '">' + c.s + '</text></g>';
264    });
265    s += '<g class="nt"><line x1="836" y1="612" x2="866" y2="612"/><text x="878" y="608">ATTACKS BLOCKED.</text><text x="878" y="622">ENDPOINTS PROTECTED.</text></g><g class="amb"></g><g class="fx"></g></svg>';
266    host.innerHTML = s;
267    if (RIGHT) RIGHT.init();
268    const svg = host.querySelector('svg');
269
270    /* the logo, bent along the membrane: every glyph of the real mark (cross, T-h-r-e-a-t, R-E-S-P-O-N-D-E-R)
271       is placed on the arc at its own position and turned to the arc's tangent, so it reads top to bottom */
272    const glyphs = [];
273    (function bendLogo() {
274      const wrap = document.createElement('div');
275      wrap.innerHTML = '<span data-logo="threatresponder"></span>';
276      if (window.Logos) window.Logos.paint(wrap);
277      const src = wrap.querySelector('svg');
278      if (!src) return;
279      const tmp = document.createElementNS(NS, 'g');
280      tmp.style.visibility = 'hidden';
281      Array.prototype.forEach.call(src.querySelectorAll('path, rect, polygon'), function (n) { tmp.appendChild(document.importNode(n, true)); });
282      svg.appendChild(tmp);
283      const W = 363.409, CY = 13.75, out = host.querySelector('.mlogo');
284      Array.prototype.slice.call(tmp.childNodes).forEach(function (n) {
285        const b = n.getBBox(), gx = b.x + b.width / 2, th = (gx - W / 2) * LOGO_K / LOGO_R;
286        const tx = Math.sin(th), ty = Math.cos(th), nx = -Math.cos(th), ny = Math.sin(th), px = OX - LOGO_R * Math.cos(th), py = OY + LOGO_R * Math.sin(th);
287        const a = LOGO_K * tx, bb = LOGO_K * ty, c = LOGO_K * nx, d = LOGO_K * ny;
288        n.setAttribute('transform', 'matrix(' + [a, bb, c, d, px - a * gx - c * CY, py - bb * gx - d * CY].map(function (v) { return v.toFixed(4); }).join(' ') + ')');
289        out.appendChild(n);
290        glyphs.push({ el: n, y: py });
291      });
292      tmp.remove();
293    })();
294
295    const fx = host.querySelector('.fx'), amb = host.querySelector('.amb'), thEl = host.querySelectorAll('.th'), logoG = host.querySelector('.mlogo');
296    const capEl = {}, fbEl = {};
297    TG.forEach(function (c, i) { capEl[c.id] = host.querySelector('[data-c="' + c.id + '"]:not(.fb)'); fbEl[c.id] = host.querySelector('.fb[data-c="' + c.id + '"]'); c.F.forEach(function (f, k) { f.el = host.querySelector('[data-l="c' + i + '-' + k + '"]'); }); });
298    if (!strike) THREATS.forEach(function (t, i) { t.L.el = host.querySelector('[data-l="t' + i + '"]'); });
299    const ALL = [].concat.apply(strike ? [] : THREATS.map(function (t) { return t.L; }), TG.map(function (c) { return c.F; }));
300    function mk(tag, a, parent) { const e = document.createElementNS(NS, tag); Object.keys(a).forEach(function (k) { e.setAttribute(k, a[k]); }); (parent || fx).appendChild(e); return e; }
301    function ease(k) { return k < .5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; }
302
303    /* ambient flow: particles always travelling in along the threat lines and out along the fibres */
304    const drift = [], specks = [];
305    if (!strike) THREATS.forEach(function (t) { for (let j = 0; j < 4; j++) drift.push({ L: t.L, u0: Math.random(), dur: 2600 + Math.random() * 2800, jit: 1 + Math.random() * 3, op: .25 + Math.random() * .3, el: mk('circle', { class: 'amb-t', r: (.8 + Math.random() * 1).toFixed(2) }, amb) }); });
306    if (!(RIGHT && RIGHT.noPulse)) TG.forEach(function (c) { for (let j = 0; j < 3; j++) drift.push({ L: c.F[Math.floor(Math.random() * c.F.length)], u0: Math.random(), dur: 2200 + Math.random() * 2600, jit: 1 + Math.random() * 2, op: .2 + Math.random() * .3, el: mk('circle', { class: 'amb-c', r: (.7 + Math.random() * .9).toFixed(2) }, amb) }); });
307    const sg = host.querySelector('.specks');
308    for (let i = 0; i < 54; i++) specks.push({ a: (Math.random() - .5) * 1.5, r: 380 + Math.random() * 120, sp: (Math.random() - .5) * .00005, tw: .001 + Math.random() * .003, ph: Math.random() * 6, el: mk('circle', { class: 'speck', r: (.5 + Math.random() * 1.2).toFixed(2) }, sg) });
309
310
311    /* Waves: no standing threat list, the membrane stands alone. A faint drift of particles always flows toward
312       it, and every few seconds a block of related threats (a campaign) flies in together and strikes it */
313    const BLOCKS = [
314      { n: 'PHISHING CAMPAIGN', t: [0, 2, 4] },
315      { n: 'RANSOMWARE OPERATION', t: [3, 5, 1] },
316      { n: 'COMMAND & CONTROL', t: [6, 2] },
317      { n: 'INTRUSION', t: [3, 4, 5] },
318      { n: 'CREDENTIAL ATTACK', t: [0, 4] }
319    ];
320    const waves = [], loose = [];
321    let nextWave = 300, bi = Math.floor(Math.random() * BLOCKS.length);
322    function resetLoose(p, fresh) { p.x = fresh ? Math.random() * 420 : -10 - Math.random() * 40; p.y = 70 + Math.random() * 520; p.vx = 30 + Math.random() * 55; p.op = .15 + Math.random() * .3; }
323    if (strike) for (let i = 0; i < 26; i++) { const p = { el: mk('circle', { class: 'amb-t', r: (.7 + Math.random() * 1).toFixed(2) }, amb) }; resetLoose(p, true); loose.push(p); }
324    function chip(th) {
325      const g = mk('g', { class: 'chip th on' });
326      g.innerHTML = '<circle class="ring" r="19"/>' + icon(th.ic, -9, -9, 18) + '<text class="ttl" x="-30" y="-2" text-anchor="end">' + esc(th.n) + '</text><text class="tsb" x="-30" y="12" text-anchor="end">' + th.s + '</text>';
327      return g;
328    }
329    function strikeStep(t, dt, P, hits, onC, onF) {
330      loose.forEach(function (p) {
331        p.x += p.vx * dt / 1000; p.y += (OY - p.y) * .00012 * dt;
332        const edge = ax(p.y, RIM) - 4;
333        if (p.x > edge) resetLoose(p, false);
334        p.el.setAttribute('cx', p.x.toFixed(1)); p.el.setAttribute('cy', p.y.toFixed(1));
335        p.el.style.opacity = (p.op * Math.min(1, Math.max(0, p.x + 10) / 80, Math.max(0, edge - p.x) / 60)).toFixed(2);
336      });
337      if (t >= nextWave) {
338        const B = BLOCKS[bi++ % BLOCKS.length], n = B.t.length, cy = 210 + Math.random() * 220;
339        const w = { t0: t, label: mk('text', { class: 'wlbl' }), items: [] };
340        w.label.textContent = B.n;
341        B.t.forEach(function (ti, j) {
342          const th = THREATS[ti], y0 = cy + (j - (n - 1) / 2) * 72 + (Math.random() - .5) * 12, iy = OY + (y0 - OY) * .8;
343          w.items.push({ th: th, x0: 70 + (Math.random() - .5) * 40 + (j % 2) * 34, y0: y0, ix: ax(iy, RIM + 2) - 2, iy: iy, delay: j * 170 + Math.random() * 150, dur: 2600 + Math.random() * 600,
344            sway: (Math.random() - .5) * 14, el: chip(th), trail: [1.8, 1.3, .9].map(function (r) { return mk('circle', { class: 'pa2', r: r }); }), flare: mk('circle', { class: 'flare', r: 0 }),
345            pb: (RIGHT ? RIGHT.pick(th) : th.caps).map(function () { return { d: mk('circle', { class: 'pb', r: 2.6 }), f: Math.floor(Math.random() * 3) }; }) });
346        });
347        waves.push(w);
348        nextWave = t + 4400 + Math.random() * 2200;
349      }
350      for (let wi = waves.length - 1; wi >= 0; wi--) {
351        const w = waves[wi];
352        let done = true, lx = 1e9, ly = 1e9, lead = 0;
353        w.items.forEach(function (it) {
354          const k = t - w.t0 - it.delay, D = it.dur, q = Math.max(0, Math.min(1, k / D));
355          if (k < D + 3200) done = false;
356          const pos = function (u) { const ee = .45 * u + .55 * u * u, s2 = u * u * (3 - 2 * u); return [it.x0 + (it.ix - it.x0) * ee, it.y0 + (it.iy - it.y0) * s2 + Math.sin(u * Math.PI) * it.sway]; };
357          if (k >= 0 && k < D) {
358            const p = pos(q), sc = q > .86 ? 1 - (q - .86) / .14 * .45 : 1;
359            it.el.setAttribute('transform', 'translate(' + p[0].toFixed(1) + ' ' + p[1].toFixed(1) + ') scale(' + sc.toFixed(3) + ')');
360            it.el.style.opacity = Math.min(1, k / 450, (1 - q) * 8 + .2).toFixed(2);
361            it.trail.forEach(function (d, j) { const r = pos(Math.max(0, q - (j + 1) * .03)); d.setAttribute('cx', r[0] - 22); d.setAttribute('cy', r[1]); d.style.opacity = String((.45 - j * .12) * Math.min(1, k / 450)); });
362            lx = Math.min(lx, p[0]); ly = Math.min(ly, p[1]); lead = Math.max(lead, q);
363          } else {
364            it.el.style.opacity = '0'; it.trail.forEach(function (d) { d.style.opacity = '0'; });
365          }
366          const kf = (k - D) / 1100;
367          if (kf >= 0 && kf <= 1) { it.flare.setAttribute('cx', it.ix); it.flare.setAttribute('cy', it.iy); it.flare.setAttribute('r', (6 + 36 * kf).toFixed(1)); it.flare.style.opacity = (1 - kf).toFixed(2); } else it.flare.style.opacity = '0';
368          if (k >= D && k < D + 1400) hits.push({ y: it.iy, p: Math.exp(-(k - D) / 420) });
369          /* Layers takes no pulses: the hit itself is handed over, and the layers light up on their own pace */
370          if (RIGHT && RIGHT.noPulse && k >= D + 200 && !it.arrived) { it.arrived = true; RIGHT.arrive(null, it.th, t); }
371          (RIGHT ? RIGHT.pick(it.th) : it.th.caps).forEach(function (cid, j) {
372            const c = TG.find(function (x) { return x.id === cid; }), kb = (k - D - 260 - j * (RIGHT ? 380 : 140)) / 900, b = it.pb[j];
373            if (RIGHT && kb >= 1 && !b.arr) { b.arr = true; RIGHT.arrive(cid, it.th, t); }
374            if (kb >= 0 && kb <= 1) { const p = bez(P.get(c.F[b.f]), ease(kb)); b.d.setAttribute('cx', p[0]); b.d.setAttribute('cy', p[1]); b.d.style.opacity = ''; onF[cid] = true; } else b.d.style.opacity = '0';
375            if (kb > .85 && kb < 3) onC[cid] = true;
376          });
377        });
378        /* the campaign's name rides above the block and fades as it closes in */
379        if (lx < 1e9) { w.label.setAttribute('x', (lx - 150).toFixed(1)); w.label.setAttribute('y', (ly - 36).toFixed(1)); w.label.style.opacity = Math.max(0, Math.min(1, (t - w.t0) / 500, (.7 - lead) * 3)).toFixed(2); } else w.label.style.opacity = '0';
380        if (done) { w.label.remove(); w.items.forEach(function (it) { [it.el, it.flare].concat(it.trail, it.pb.map(function (b) { return b.d; })).forEach(function (n) { n.remove(); }); }); waves.splice(wi, 1); }
381      }
382    }
383
384    const events = [];
385    let next = 200, last = -1, prevT = 0;
386    upd = function (t) {
387      const dt = Math.max(0, Math.min(50, t - prevT));
388      prevT = t;
389      const P = new Map();
390      ALL.forEach(function (L) { const c = cur(L, t); P.set(L, c); L.el.setAttribute('d', dOf(c)); });
391      drift.forEach(function (d) {
392        const u = ((t / d.dur) + d.u0) % 1, p = bez(P.get(d.L), u), w = Math.sin(t * .002 + d.u0 * 20) * d.jit;
393        d.el.setAttribute('cx', p[0].toFixed(1)); d.el.setAttribute('cy', (p[1] + w).toFixed(1));
394        d.el.style.opacity = (d.op * Math.min(1, u * 6, (1 - u) * 6)).toFixed(2);
395      });
396      specks.forEach(function (k) {
397        k.a += k.sp * dt; if (k.a > .78) k.a = -.78; if (k.a < -.78) k.a = .78;
398        k.el.setAttribute('cx', (OX - k.r * Math.cos(k.a)).toFixed(1)); k.el.setAttribute('cy', (OY + k.r * Math.sin(k.a)).toFixed(1));
399        k.el.style.opacity = (.12 + .4 * Math.abs(Math.sin(t * k.tw + k.ph))).toFixed(2);
400      });
401      const onT = {}, onC = {}, onF = {}, hits = [];
402      if (RIGHT) RIGHT.step(t, dt);
403      if (strike) strikeStep(t, dt, P, hits, onC, onF);
404      else {
405      if (t >= next) {
406        let i = Math.floor(Math.random() * THREATS.length);
407        if (i === last) i = (i + 1 + Math.floor(Math.random() * (THREATS.length - 1))) % THREATS.length;
408        last = i;
409        const th = THREATS[i];
410        events.push({ i: i, t0: t, pa: mk('circle', { class: 'pa', r: 3 }), trail: [mk('circle', { class: 'pa2', r: 2 }), mk('circle', { class: 'pa2', r: 1.4 })],
411          flare: mk('circle', { class: 'flare', cx: th.ix, cy: th.iy, r: 0 }), dur: 1000 + Math.random() * 500,
412          pb: th.caps.map(function () { return { d: mk('circle', { class: 'pb', r: 2.6 }), f: Math.floor(Math.random() * offs.length) }; }) });
413        next = t + 1300 + Math.random() * 1300;
414      }
415      for (let e = events.length - 1; e >= 0; e--) {
416        const ev = events[e], th = THREATS[ev.i], k = t - ev.t0, D = ev.dur;
417        if (k < 0) continue;
418        if (k > D + 3000) { [ev.pa, ev.flare].concat(ev.trail, ev.pb.map(function (b) { return b.d; })).forEach(function (n) { n.remove(); }); events.splice(e, 1); continue; }
419        if (k < D + 600) onT[ev.i] = true;
420        if (k < D) {
421          const q = ease(k / D), p = bez(P.get(th.L), q);
422          ev.pa.setAttribute('cx', p[0]); ev.pa.setAttribute('cy', p[1]); ev.pa.style.opacity = '';
423          ev.trail.forEach(function (d, j) { const r = bez(P.get(th.L), Math.max(0, q - (j + 1) * .035)); d.setAttribute('cx', r[0]); d.setAttribute('cy', r[1]); d.style.opacity = ''; });
424        } else { ev.pa.style.opacity = '0'; ev.trail.forEach(function (d) { d.style.opacity = '0'; }); }
425        const kf = (k - D) / 1100;
426        if (kf >= 0 && kf <= 1) { ev.flare.setAttribute('r', (6 + 34 * kf).toFixed(1)); ev.flare.style.opacity = (1 - kf).toFixed(2); } else ev.flare.style.opacity = '0';
427        /* the logo pulses around where the threat hit, strongest there and fading along the curve */
428        if (k >= D && k < D + 1400) hits.push({ y: th.iy, p: Math.exp(-(k - D) / 420) });
429        th.caps.forEach(function (cid, j) {
430          const c = CAPS.find(function (x) { return x.id === cid; }), kb = (k - D - 260 - j * 140) / 900, b = ev.pb[j];
431          if (kb >= 0 && kb <= 1) { const p = bez(P.get(c.F[b.f]), ease(kb)); b.d.setAttribute('cx', p[0]); b.d.setAttribute('cy', p[1]); b.d.style.opacity = ''; onF[cid] = true; } else b.d.style.opacity = '0';
432          if (kb > .85 && kb < 3) onC[cid] = true;
433        });
434      }
435      }
436      let peak = 0;
437      glyphs.forEach(function (g) {
438        let I = 0;
439        hits.forEach(function (h) { I += h.p * Math.exp(-Math.pow((g.y - h.y) / 120, 2)); });
440        I = Math.min(1, I); peak = Math.max(peak, I);
441        g.el.style.opacity = (.42 + .58 * I).toFixed(3);
442      });
443      logoG.style.filter = peak > .02 ? 'drop-shadow(0 0 ' + (3 + 9 * peak).toFixed(1) + 'px rgba(170, 200, 255, ' + (.15 + .55 * peak).toFixed(2) + '))' : '';
444      thEl.forEach(function (el, i) { el.classList.toggle('on', !!onT[i]); });
445      if (!(RIGHT && RIGHT.noPulse)) TG.forEach(function (c) { capEl[c.id].classList.toggle('on', !!onC[c.id]); fbEl[c.id].classList.toggle('on', !!onF[c.id] || !!onC[c.id]); });
446    };
447
448    function layout() {
449      const hb = hero.getBoundingClientRect(), tb = text.getBoundingClientRect(), sb = (function () { const band = hero.querySelector('.tb'); return band && band.offsetParent ? band : shot; })().getBoundingClientRect();
450      const left = tb.right - hb.left + 24;
451      host.style.left = left + 'px'; host.style.top = '16px';
452      host.style.width = Math.max(200, hero.clientWidth - left - 12) + 'px'; host.style.height = Math.max(200, sb.top - hb.top - 24) + 'px';
453    }
454    layout();
455    document.addEventListener('site:ready', layout);
456    if (document.fonts) document.fonts.ready.then(layout);
457    let rz = 0;
458    window.addEventListener('resize', function () { clearTimeout(rz); rz = setTimeout(layout, 120); });
459
460    let running = true, t0 = 0, lastNow = 0;
461    window.__story = { frozen: false, seek: function (t) { window.__story.frozen = true; running = false; for (let k = Math.max(0, t - 4000); k <= t; k += 16) upd(k); } };
462    if (still) { for (let k = 0; k <= 1900; k += 30) upd(k); return; }
463    (function loop(now) {
464      if (running) { if (!t0) t0 = now; upd(now - t0); } else if (lastNow) t0 += now - lastNow;
465      lastNow = now;
466      requestAnimationFrame(loop);
467    })(0);
468    new IntersectionObserver(function (es) { if (window.__story.frozen) return; running = es[0].isIntersecting; }).observe(hero);
469  })();
470
471  /* Globe hero (?hero=globe): the supplied globe artwork as the backdrop, with the product composed over it
472     after the user's mock: Detection, Response, Investigation and Evidence cards wired to a shield at the
473     centre. Everything sits on a 1916×821 stage in the artwork's own pixels (globe centre 1235,545,
474     radius 443), scaled with it, so it stays locked to the globe at any width. The cards are still (their
475     content is a placeholder); the only motion is the connectors flowing into the shield. */
476  (function () {
477    if (window.__heroMode !== 'globe') return;
478    const hero = document.getElementById('hero');
479    if (!hero) return;
480        const ic = function (n) { return window.PUI ? window.PUI.icon(n) : ''; };
481    const bars = '<svg class="pu-i" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="12" width="3.2" height="8" rx="1"/><rect x="10.4" y="6" width="3.2" height="14" rx="1"/><rect x="16.8" y="9" width="3.2" height="11" rx="1"/></svg>';
482    const card = function (cls, box, icon, title, body) {
483      return '<div class="gc ' + cls + '" style="left:' + box[0] + 'px;top:' + box[1] + 'px;width:' + box[2] + 'px;height:' + box[3] + 'px"><h5>' + icon + '<span>' + title + '</span></h5>' + body + '</div>';
484    };
485    const RESP = [['lock', 'Contain endpoint'], ['bolt', 'Kill process'], ['archive', 'Quarantine'], ['clock', 'Revert changes'], ['search', 'Hunt']];
486    const INV = [['file', 'Files'], ['cog', 'Processes'], ['net', 'Network'], ['reg', 'Registry'], ['user', 'Users'], ['clock', 'Timeline']];
487    const EVI = [['10:24', 'Malicious file blocked', 'r'], ['10:25', 'Process terminated', ''], ['10:27', 'Artifacts collected', ''], ['10:28', 'Incident resolved', 'g']];
488
489    const glb = document.createElement('div');
490    glb.className = 'glb'; glb.setAttribute('aria-hidden', 'true');
491    glb.innerHTML = '<img class="glb-bg dk" src="/shared/hero-globe.webp" alt=""><img class="glb-bg lt" src="/shared/hero-globe-light.webp" alt="">' +
492      '<div class="glb-st">' +
493      '<svg class="glb-svg" viewBox="0 0 1916 821" width="1916" height="821">' +
494      '<defs><radialGradient id="gcore" cx="50%" cy="45%" r="60%"><stop offset="0" stop-color="#1d4fb8"/><stop offset="1" stop-color="#0a1f5e"/></radialGradient>' +
495      '<linearGradient id="gshield" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bfe0ff"/><stop offset="1" stop-color="#3f8cff"/></linearGradient>' +
496      '<filter id="gglow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>' +
497      '<g class="gl-links" filter="url(#gglow)">' +
498      ['M1163,413 V433 Q1163,447 1177,447 H1232 Q1242,447 1250,455',
499        'M1355,365 H1345 Q1331,365 1331,379 V441',
500        'M1213,517 H1236 Q1248,517 1253,507',
501        'M1339,508 Q1345,517 1357,517 H1419 Q1433,517 1433,531 V532'].map(function (d) { return '<path class="lk" d="' + d + '"/><path class="lk-f" d="' + d + '"/>'; }).join('') + '</g>' +
502      '<g class="gl-core" transform="translate(1295 480)"><circle class="gr-o" r="52"/><circle class="gr-i" r="40" fill="url(#gcore)"/>' +
503      '<path class="gr-s" d="M0,-24 L19,-16 V0 C19,13 10,21 0,26 C-10,21 -19,13 -19,0 V-16 Z" fill="url(#gshield)"/><path class="gr-h" d="M0,-24 V26 C-10,21 -19,13 -19,0 V-16 Z"/></g>' +
504      '</svg>' +
505      card('det', [1060, 253, 235, 160], bars, 'Detection', '<div class="gd"><div><b class="gn">1,248</b><small>Threats blocked</small></div><div class="gt"><em>▼ 87%</em><small>vs. last 30 days</small></div></div>') +
506      card('rsp', [1355, 263, 193, 213], ic('cog'), 'Response', '<ul>' + RESP.map(function (r) { return '<li>' + ic(r[0]) + r[1] + '</li>'; }).join('') + '</ul>') +
507      card('inv', [999, 472, 214, 249], ic('search'), 'Investigation', '<ul>' + INV.map(function (r) { return '<li>' + ic(r[0]) + '<span>' + r[1] + '</span><i class="cv"></i></li>'; }).join('') + '</ul>') +
508      card('evi', [1345, 532, 250, 181], ic('file'), 'Evidence', '<ol>' + EVI.map(function (r) { return '<li class="' + r[2] + '"><i></i><time>' + r[0] + '</time>' + r[1] + '</li>'; }).join('') + '</ol>') +
509      '<div class="gl-tag">DETECT <i>·</i> PREVENT <i>·</i> <b>INVESTIGATE</b> <i>·</i> RESPOND</div>' +
510      '</div>';
511    hero.insertBefore(glb, hero.firstChild);
512    const st = glb.querySelector('.glb-st');
513
514    /* the cards must clear the text: measure where the words actually end, then scale the globe down (to
515       72% at most, kept centred in the band) until the Investigation card (stage x 999) clears it; only
516       what is still missing after that shifts the globe right */
517    const textCol = hero.querySelector('.hgrid > div');
518    function textRight() {
519      let r = 0;
520      const w = document.createTreeWalker(textCol, NodeFilter.SHOW_TEXT), rg = document.createRange();
521      while (w.nextNode()) {
522        if (!w.currentNode.textContent.trim()) continue;
523        rg.selectNodeContents(w.currentNode);
524        rg.getClientRects && Array.prototype.forEach.call(rg.getClientRects(), function (b) { r = Math.max(r, b.right); });
525      }
526      textCol.querySelectorAll('img, .btn').forEach(function (e) { r = Math.max(r, e.getBoundingClientRect().right); });
527      return r;
528    }
529    function fit() {
530      glb.style.height = glb.style.top = glb.style.right = '';
531      if (window.innerWidth <= 1100) { st.style.transform = ''; return; }
532      const band = glb.clientHeight, hb = hero.getBoundingClientRect(), vw = hero.clientWidth;
533      const need = textRight() - hb.left + 28, left = function (sc) { return vw - (1916 - 999) * sc; };
534      let s = band / 821;
535      if (left(s) < need) s = Math.max(band / 821 * .72, (vw - need) / (1916 - 999));
536      const shift = Math.max(0, need - left(s)), h = 821 * s;
537      glb.classList.toggle('sc', h < band - 1);
538      glb.style.height = h.toFixed(1) + 'px'; glb.style.top = ((band - h) / 2).toFixed(1) + 'px'; glb.style.right = (-shift).toFixed(1) + 'px';
539      st.style.transform = 'scale(' + s.toFixed(4) + ')';
540    }
541    fit();
542    window.addEventListener('resize', fit);
543    document.addEventListener('site:ready', fit);
544    if (document.fonts) document.fonts.ready.then(fit);
545
546  })();
547
548  /* After the hero (?next=pillars|pipeline|layers|bento|screen): the live site's "Predict, Stop, &
549     Investigate Attacks" section, with its exact heading and body, and the platform drawn four ways. Nothing
550     pins: the section scrolls like any other.
551       pillars  (after the user's picture 1): Mission Control wired down to five capability cards standing
552                on the red lightweight-agent slab; assembles once in view, then data rises up the wires
553       pipeline (after the user's picture 2): endpoints, identities and activity → the agent → five
554                integrated capabilities → Mission Control with a real panel; Collect / Detect / Investigate /
555                Respond take turns, lighting what does the work
556       layers   (static): console / capabilities / agent as three labelled bands
557       bento    (static): the slogan as columns, a small product-style picture in each big tile
558     The section takes the opposite tone of the hero. screen keeps the product screenshot instead. */
559  (function () {
560    const root = document.documentElement, mode = root.getAttribute('data-next');
561    const hero = document.getElementById('hero');
562    if (!hero || !mode || mode === 'screen') return;
563    const still = root.classList.contains('poster') || root.classList.contains('reduced');
564    const I = function (n) { return '<i data-ic="' + n + '"></i>'; };
565
566    /* the capabilities, each named once, with what it actually does */
567    const CAP = [
568      { k: 'edr', ic: 'shield', t: 'Detection &amp; response', d: 'Stops the attack on the endpoint and tells it as one story.', pui: 'triage', url: 'threats',
569        f: ['Ransomware, exploit and script blocking', 'Threat stories mapped to MITRE ATT&amp;CK', 'Kill, quarantine, contain, revert', 'Custom detection rules'] },
570      { k: 'itdr', ic: 'user', t: 'Identity threat protection', d: 'Sees the accounts an attacker would use, from the same agent.', pui: 'terrain', url: 'identity',
571        f: ['Risky and exposed accounts', 'Identity-based attack detection', 'Lateral movement and paths to Tier 0'] },
572      { k: 'exp', ic: 'bug', t: 'Exposure', d: 'Finds what an attacker could use, before they do.', pui: 'exposure', url: 'exposure',
573        f: ['CVEs in real time, without scanning', 'Known-exploited (CISA KEV) first', 'BOD 22-01 prioritised', 'Security posture score'] },
574      { k: 'for', ic: 'search', t: 'Forensics &amp; IR', d: 'Investigates remotely, even after a breach.', pui: 'forensics', url: 'forensics',
575        f: ['Remote forensic collection', 'Super-timeline and threat hunting', 'Live response on any endpoint', 'Offline collection for isolated hosts'] }];
576    const CONSOLE = ['Multi-tenant, built for MSSPs', 'Threat stories and triage', 'Dashboards across every company', 'SIEM integration'];
577    const AGENT = ['Windows', 'macOS', 'Linux', 'Deploys in minutes', 'Works after a breach', 'Offline evidence collection'];
578    const logo = '<img class="plx-logo" src="/shared/threatresponder-logo.svg" alt="ThreatResponder®">';
579    const list = function (a) { return '<ul class="plx-f">' + a.map(function (x) { return '<li>' + I('check') + '<span>' + x + '</span></li>'; }).join('') + '</ul>'; };
580
581    /* two-tone icons for the two picture variants: fill is the icon colour, --i2 the accent */
582    const SV = function (b) { return '<svg viewBox="0 0 24 24" aria-hidden="true">' + b + '</svg>'; };
583    const G = {
584      edr: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><circle cx="12" cy="11.5" r="3.6" fill="none" stroke="#fff" stroke-width="1.6"/><path d="M12 6.6v2.6M12 13.8v2.6M7.1 11.5h2.6M14.3 11.5h2.6" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/>'),
585      itdr: SV('<circle cx="10" cy="7" r="4" fill="currentColor"/><path d="M2 21.5c0-4.6 3.6-8 8-8 1.4 0 2.7.3 3.8.9V21.5z" fill="currentColor"/><path d="M18.5 11.5l4 1.5v3c0 2.7-1.7 4.7-4 5.6-2.3-.9-4-2.9-4-5.6v-3z" fill="var(--i2)"/>'),
586      threat: SV('<circle cx="10.5" cy="10.5" r="7.2" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M15.8 15.8l5.4 5.4" stroke="currentColor" stroke-width="2.8" stroke-linecap="round"/><path d="M10.5 6.4l4 7h-8z" fill="var(--i2)"/><path d="M10.5 9.2v2.2" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/>'),
587      ir: SV('<path d="M4.5 2h9l5 5v8.5a6 6 0 0 0-4 6.5H4.5z" fill="currentColor"/><path d="M7.5 9h7M7.5 12.5h5" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/><circle cx="18" cy="18" r="4.2" fill="var(--i2)"/><circle cx="18" cy="18" r="1.5" fill="#fff"/>'),
588      exp: SV('<rect x="3" y="14" width="4" height="7.5" rx="1" fill="currentColor"/><rect x="10" y="10.5" width="4" height="11" rx="1" fill="currentColor"/><rect x="17" y="7.5" width="4" height="14" rx="1" fill="currentColor"/><path d="M3 10.5l6-4 4 2 7-6.2" fill="none" stroke="var(--i2)" stroke-width="2" stroke-linecap="round"/><path d="M16.5 2.2h3.8V6" fill="none" stroke="var(--i2)" stroke-width="2" stroke-linecap="round"/>'),
589      monitor: SV('<rect x="2" y="3" width="20" height="14" rx="2.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8.5 21h7M12 17v4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
589<path d="M7.5 13.5v-3M11 13.5V7.5M14.5 13.5v-4.5M18 13.5V9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'),
590      bolt: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><path d="M13.2 5.8L8.8 13h3.1l-1.1 5.2 4.4-7.3h-3.1z" fill="var(--i2)"/>'),
591      plus: SV('<path d="M12 2l8 3v6c0 5-3.5 9-8 11-4.5-2-8-6-8-11V5z" fill="currentColor"/><path d="M12 7.2v8.6M7.7 11.5h8.6" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>'),
592      laptop: SV('<rect x="4" y="4" width="16" height="11" rx="1.6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M2 18.5h20" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>'),
593      users: SV('<circle cx="8.5" cy="8" r="3.4" fill="currentColor"/><circle cx="16.5" cy="8.5" r="2.8" fill="currentColor" opacity=".7"/><path d="M2 19.5c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6z" fill="currentColor"/><path d="M15 13.8c3.4-.4 7 1.6 7 5.7h-5" fill="currentColor" opacity=".7"/>'),
594      activity: SV('<path d="M2 12.5h4l2.5-6 4.5 12 3-8h6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>'),
595      db: SV('<ellipse cx="12" cy="5.5" rx="7.5" ry="3" fill="none" stroke="currentColor" stroke-width="2"/><path d="M4.5 5.5v13c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-13M4.5 12c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3" fill="none" stroke="currentColor" stroke-width="2"/>'),
596      search: SV('<circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M15.5 15.5l5 5" stroke="currentColor" stroke-width="2.8" stroke-linecap="round"/>'),
597      doc: SV('<path d="M5 2h9.5l4.5 4.5V22H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M8.5 11h7M8.5 14.5h7M8.5 18h4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>'),
598      cog: SV('<circle cx="12" cy="12" r="3.3" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>')
599    };
600    /* the five capabilities as both pictures name them; red and blue alternate as in the images */
601    /* the logo's own parts, cropped from the official file: the grey square with the red plus, and the wordmark */
602    const MARK = '<span class="tr-mark"><img src="/shared/threatresponder-logo.svg" alt=""></span>';
603    const WORD = '<span class="tr-word"><img src="/shared/threatresponder-logo.svg" alt="ThreatResponder®"></span>';
604    const icon = function (k) { return k === 'edr' ? MARK : G[k]; };
605    const FIVE = [['edr', 'b', 'Endpoint Detection &amp; Response', 'Detect and stop threats on endpoints, mapped to MITRE ATT&amp;CK.', 'Stopped as they run, mapped to MITRE ATT&amp;CK'],
606      ['itdr', 'r', 'Identity Threat Protection', 'Find and prevent identity-based attacks.', 'Risky accounts and paths to Tier 0'],
607      ['threat', 'b', 'Threat Hunting', 'Hunt for threats across every endpoint.', 'One query across every endpoint'],
608      ['ir', 'r', 'Incident Response &amp; Forensics', 'Investigate, contain, and respond faster.', 'Remote and offline evidence collection'],
609      ['exp', 'b', 'Vulnerability &amp; Exposure Detection', 'Find and prioritize risk across your environment.', 'CVEs in real time, without scanning']];
610
611    let body = '';
612    if (mode === 'pillars') {
613      body = '<div class="pi"><svg class="pi-wires" aria-hidden="true"></svg>' +
614        '<div class="pi-mc"><span class="pi-mi">' + G.monitor + '</span><div><b>Mission Control</b><small>Unified console</small></div></div>' +
615        '<div class="pi-cards">' + FIVE.map(function (c, i) {
616          return '<div class="pi-card" style="--i:' + i + '"><span class="pi-ic ic-' + c[1] + '">' + icon(c[0]) + '</span><i class="pi-dash"></i><b>' + c[2] + '</b><span class="pi-d">' + c[4] + '</span></div>';
617        }).join('') + '</div>' +
618        '<div class="pi-agent"><span class="pi-ai">' + MARK + '</span><i class="pi-sep"></i><div><b>Lightweight Endpoint Agent</b><small>One agent. Many capabilities.</small></div></div>' +
619        '<div class="pi-foot pi-proof-footer"><aside class="pi-performance" aria-label="Independent performance test results">' +
620          '<div class="pi-performance-brand"><img src="/shared/av-comparatives-logo.png" width="162" height="152" alt="AV-Comparatives" loading="lazy"></div>' +
621          '<div class="pi-performance-copy"><h3>Lowest measured system impact among tested business security products — AV-Comparatives, 2024 &amp; 2025.</h3>' +
622          '<p>Ranked <strong>#1</strong> in Business Performance in <a href="https://av-comparatives.org/tests/business-security-test-2024-august-november/#performance-test-result" target="_blank" rel="noopener">November 2024</a> (Impact Score <strong>3.8</strong>) and <strong>#1</strong> in November 2025 (Impact Score <strong>2.8</strong>).</p>' +
623          '<p>Rated “Very Fast” across all performance subtests in both Windows-based tests.<a class="pi-test-link" href="https://av-comparatives.org/tests/business-security-test-2025-august-november/#performance-test-result" target="_blank" rel="noopener">Test results <span aria-hidden="true">↗</span></a></p></div></aside>' +
624          '<ul class="pi-os-support" aria-label="ThreatResponder supported operating systems">' +
625            '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M4 6l8-1v8H4zm10-1 10-2v10H14zM4 15h8v8l-8-1zm10 0h10v11l-10-2z"/></svg>Windows</li>' +
626            '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M18 6c-2 1-4 1-5 0-3-1-6 1-7 4-2 5 1 13 5 16 2 1 3-1 5-1s3 2 5 0c2-2 4-5 4-7-5-2-6-7-2-10-1-2-3-3-5-2ZM15 5c0-3 2-5 5-5 0 3-2 5-5 5z"/></svg>macOS</li>' +
627            '<li><svg viewBox="0 0 28 28" aria-hidden="true"><path d="M14 2c-5 0-6 5-5 9-1 3-5 5-5 11 0 3 4 3 7 2 2 2 5 2 7 0 4 2 8 0 7-3-1-5-5-8-5-11 0-4-1-8-6-8z"/><ellipse class="os-cutout" cx="14" cy="17" rx="5" ry="7"/><ellipse class="os-cutout" cx="12" cy="8" rx="1.2" ry="1.8"/><ellipse class="os-cutout" cx="17" cy="8" rx="1.2" ry="1.8"/><path class="os-cutout" d="m12 11 3-2 3 2-3 2z"/></svg>Linux</li></ul></div></div>';
628    }
629    if (mode === 'pipeline') {
630      const SRC = [['laptop', 'Endpoints', 'Laptops, desktops, servers, cloud VMs', 'Collect'], ['users', 'Users &amp; identity', 'Accounts, logons, privileges', 'Monitor'], ['activity', 'Activity', 'Processes, files, network connections', 'Record']];
631      const STEP = [['db', 'b', 'Collect', 'One agent gathers what every endpoint and identity does.'], ['search', 'r', 'Detect', 'Known and unknown threats, in real time.'],
632        ['doc', 'b', 'Investigate', 'The full context to understand and contain.'], ['cog', 'r', 'Respond', 'Kill, quarantine, contain, revert.']];
633      body = '<div class="pp"><svg class="pp-wires" aria-hidden="true"></svg>
633' +
634        '<div class="pp-in">' + SRC.map(function (s) { return '<div class="pp-src"><span class="pp-sic">' + G[s[0]] + '</span><div><b>' + s[1] + '</b><span>' + s[2] + '</span></div></div>'; }).join('') + '</div>' +
635        SRC.map(function (s) { return '<span class="pp-pill">' + s[3] + '</span>'; }).join('') +
636        '<div class="pp-agent"><div class="pp-ring">' + MARK + WORD + '<b>Agent</b><small>Lightweight. Always on.</small></div></div>' +
637        '<div class="pp-caps"><small class="pp-lab">Integrated capabilities</small><div class="pp-row">' + FIVE.map(function (c) {
638          return '<div class="pp-cap t-' + c[1] + '"><span class="pp-cic">' + icon(c[0]) + '</span><b>' + c[2] + '</b><i></i><span>' + c[3] + '</span></div>';
639        }).join('') + '</div></div>' +
640        '<div class="pp-mc"><b>Mission Control</b><small>Unified console</small><div class="pp-screen"><div data-pui="overview" data-skin="navy" data-loop></div></div><b class="pp-tag">See. Investigate. Respond.</b><small>All from one place.</small></div>' +
641        '<div class="pp-steps">' + STEP.map(function (s, i) { return '<div class="pp-step" data-i="' + i + '"><span class="pp-stic ic-' + s[1] + '">' + G[s[0]] + '</span><div><b>' + s[2] + '</b><span>' + s[3] + '</span></div></div>'; }).join('') + '</div></div>';
642    }
643    if (mode === 'layers') {
644      body = '<div class="la">' +
645        '<div class="la-band la-console"><div class="la-lab"><b>Console</b><span>Mission Control</span></div><div class="la-body"><div class="la-mc"><b>Mission Control</b><span>One console for every endpoint and every company.</span></div>' +
646        '<div class="la-chips">' + CONSOLE.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div></div>' +
647        '<div class="la-band la-caps"><div class="la-lab"><b>Capabilities</b><span>on the same agent</span></div><div class="la-body la-grid">' + CAP.map(function (c) {
648          return '<div class="la-cap"><div class="la-h">' + I(c.ic) + '<b>' + c.t + '</b></div><p>' + c.d + '</p>' + list(c.f) + '</div>';
649        }).join('') + '</div></div>' +
650        '<div class="la-band la-agent"><div class="la-lab"><b>Agent</b><span>one per endpoint</span></div><div class="la-body"><div class="la-ag">' + logo + '<b>One lightweight agent</b></div>' +
651        '<div class="la-chips">' + AGENT.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div></div></div>';
652    }
653    if (mode === 'bento') {
654      const cve = '<div class="bx-cve">' + [['CVE-2024-3400', 'PAN-OS', '10.0'], ['CVE-2023-4966', 'Citrix NetScaler', '9.4'], ['CVE-2024-21412', 'Windows SmartScreen', '8.1']].map(function (c) {
655        return '<div><code>' + c[0] + '</code><span>' + c[1] + '</span><em>KEV</em><b>' + c[2] + '</b></div>'; }).join('') + '</div>';
656      const tree = '<div class="bx-tree"><div><code>outlook.exe</code></div><div class="d1"><code>invoice.js</code></div><div class="d2"><code>powershell.exe -enc …</code><em>KILLED</em></div><div class="d2"><code>stage.dll</code><em class="q">QUARANTINED</em></div></div>';
657      const chain = '<div class="bx-chain">' + [['T1566', 'Phishing'], ['T1059', 'Script'], ['T1053', 'Scheduled task'], ['T1003', 'Credential dump']].map(function (c) {
658        return '<div><code>' + c[0] + '</code><span>' + c[1] + '</span></div>'; }).join('') + '</div>';
659      const col = function (w, big, small) { return '<div class="bx-col"><h3>' + w + '</h3>' + big + small.map(function (s) { return '<div class="bx-t">' + I(s[0]) + '<b>' + s[1] + '</b><span>' + s[2] + '</span></div>'; }).join('') + '</div>'; };
660      body = '<div class="bx">' +
661        '<div class="bx-wide bx-mc"><b>Mission Control</b><span>One console for every endpoint and every company.</span><div class="la-chips">' + CONSOLE.map(function (x) { return '<span>' + x + '</span>'; }).join('') + '</div></div>' +
662        col('Predict.', '<div class="bx-big"><b>CVEs in real time, without scanning</b><span>Known-exploited first, from CISA KEV.</span>' + cve + '</div>',
663          [['user', 'Risky identities', 'Exposed accounts and paths to Tier 0.'], ['pulse', 'Security posture', 'A score from live criteria, per endpoint.']]) +
664        col('Stop.', '<div class="bx-big"><b>Ransomware, exploits and scripts blocked</b><span>On the endpoint, as they run.</span>' + tree + '</div>',
665          [['shield', 'Kill, quarantine, contain, revert', 'The response the attack needs, per threat.'], ['user', 'Identity attacks', 'Detected from the same agent.']]) +
666        col('Investigate.', '<div class="bx-big"><b>Every attack as one threat story</b><span>Mapped to MITRE ATT&amp;CK.</span>' + chain + '</div>',
667          [['search', 'Remote forensic collection', 'Even from hosts that are offline.'], ['term', 'Live response and hunting', 'Any endpoint, any question.']]) +
668        '<div class="bx-wide bx-ag">' + logo + '<div><b>One lightweight agent</b><span>Windows, macOS and Linux. It deploys in minutes, even after a breach.</span></div></div></div>';
669    }
670
671    const sec = document.createElement('section');
672    sec.className = 'plx plx-' + mode + (still ? ' still' : ''); sec.id = 'pf';
673    sec.innerHTML = '<div class="wrap plx-in"><div class="plx-head"><h2>Predict, Stop, &amp; Investigate Attacks</h2><p class="plx-sub">One lightweight agent. <span>One single pane of glass.</span> Unlimited capabilities.</p></div>' + body + '</div>';
674    hero.parentNode.insertBefore(sec, hero.nextSibling);
675
676    /* ---- motion for the two picture variants, only while on screen, none under reduced motion ---- */
677    let inView = false, shown = false;
678    new IntersectionObserver(function (es) {
679      inView = es[0].isIntersecting;
680      if (inView && es[0].intersectionRatio > .2 && !shown) { shown = true; sec.classList.add('go'); }
681    }, { threshold: [0, .2, .4] }).observe(sec);
682    if (!still && (mode === 'pillars' || mode === 'pipeline')) sec.classList.add('pre');
683
684    const NS = 'http://www.w3.org/2000/svg';
685    const curve = function (a, b) { const m = (a[0] + b[0]) / 2; return 'M' + a[0].toFixed(1) + ',' + a[1].toFixed(1) + ' C' + m.toFixed(1) + ',' + a[1].toFixed(1) + ' ' + m.toFixed(1) + ',' + b[1].toFixed(1) + ' ' + b[0].toFixed(1) + ',' + b[1].toFixed(1); };
686    const vcurve = function (a, b) { const m = (a[1] + b[1]) / 2; return 'M' + a[0].toFixed(1) + ',' + a[1].toFixed(1) + ' C' + a[0].toFixed(1) + ',' + m.toFixed(1) + ' ' + b[0].toFixed(1) + ',' + m.toFixed(1) + ' ' + b[0].toFixed(1) + ',' + b[1].toFixed(1); };
687    function wired(box, svg, build) {
688      let dots = [];
689      function draw() {
690        if (window.innerWidth <= 1100) { svg.innerHTML = ''; dots = []; return; }
691        const r = box.getBoundingClientRect();
692        const at = function (el, x, y) { const b = el.getBoundingClientRect(); return [b.left - r.left + b.width * x, b.top - r.top + b.height * y]; };
693        svg.setAttribute('viewBox', '0 0 ' + r.width.toFixed(0) + ' ' + r.height.toFixed(0));
694        const W = build(at);
695        svg.innerHTML = '<defs><marker id="plxArr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,1 L9,5 L0,9 Z"/></marker></defs>' +
696          W.map(function (w) { return '<path class="w ' + (w.cls || '') + '" pathLength="1" d="' + w.d + '"' + (w.arrow ? ' marker-end="url(#plxArr)"' : '') + '/>'; }).join('') +
697          W.filter(function (w) { return w.node; }).map(function (w) { return '<circle class="nd" cx="' + w.node[0].toFixed(1) + '" cy="' + w.node[1].toFixed(1) + '" r="5"/>'; }).join('') + '<g class="dots"></g>';
698        const g = svg.querySelector('.dots'); dots = [];
699        [].slice.call(svg.querySelectorAll('path.w')).forEach(function (p, j) {
700          const w = W[j]; if (!w.dots) return;
701          const len = p.getTotalLength();
702          for (let k = 0; k < w.dots; k++) { const c = document.createElementNS(NS, 'circle'); c.setAttribute('r', 2.6); c.setAttribute('class', 'dot'); g.appendChild(c); dots.push({ el: c, p: p, len: len, u: k / w.dots + Math.random() * .15, sp: .16 + Math.random() * .08, rev: !!w.rev }); }
703        });
704      }
705      draw();
706      window.addEventListener('resize', draw);
707      document.addEventListener('site:ready', draw);
708      if (document.fonts) document.fonts.ready.then(draw);
709      if (still) return;
710      let last = 0;
711      (function loop(now) {
712        const dt = Math.min(64, now - (last || now)) / 1000; last = now;
713        if (inView && shown) dots.forEach(function (d) {
714          d.u = (d.u + dt * d.sp) % 1;
715          const pt = d.p.getPointAtLength((d.rev ? 1 - d.u : d.u) * d.len);
716          d.el.setAttribute('cx', pt.x.toFixed(1)); d.el.setAttribute('cy', pt.y.toFixed(1));
717          d.el.style.opacity = Math.min(1, d.u * 5, (1 - d.u) * 5).toFixed(2);
718        });
719        requestAnimationFrame(loop);
720      })(0);
721    }
722
723    if (mode === 'pillars') {
724      const box = sec.querySelector('.pi'), mc = box.querySelector('.pi-mc'), cards = box.querySelectorAll('.pi-card');
725      /* Mission Control reaches down to every card; data rises back up the wires */
726      wired(box, box.querySelector('.pi-wires'), function (at) {
727        return [].map.call(cards, function (c, i) {
728          const a = at(mc, .08 + i * .21, 1), b = at(c, .5, 0);
729          return { d: vcurve(a, b), node: a, dots: 1, rev: true };
730        });
731      });
732    }
733
734    if (mode === 'pipeline') {
735      const box = sec.querySelector('.pp'), srcs = box.querySelectorAll('.pp-src'), pills = box.querySelectorAll('.pp-pill');
736      const ring = box.querySelector('.pp-ring'), caps = box.querySelector('.pp-row'), mcx = box.querySelector('.pp-mc');
737      wired(box, box.querySelector('.pp-wires'), function (at) {
738        const W = [], ag = at(ring, 0, .5);
739        [].forEach.call(srcs, function (s, i) {
740          const a = at(s, 1, .5), pill = pills[i], mid = [(a[0] + ag[0]) / 2 - 14, a[1] * .55 + ag[1] * .45];
741          pill.style.left = mid[0].toFixed(1) + 'px'; pill.style.top = mid[1].toFixed(1) + 'px';
742          W.push({ d: curve(a, mid) + ' ' + curve(mid, [ag[0] - 4, ag[1] + (i - 1) * 26]).replace(/^M[^C]+/, ''), node: a, dots: 2, arrow: true });
743        });
744        W.push({ d: 'M' + at(ring, 1, .5).map(function (v, k) { return (v + (k ? 0 : 6)).toFixed(1); }).join(',') + ' L' + at(caps, 0, .5).map(function (v, k) { return (v - (k ? 0 : 8)).toFixed(1); }).join(','), arrow: true, dots: 1, cls: 'hop' });
745        W.push({ d: 'M' + at(caps, 1, .5).map(function (v, k) { return (v + (k ? 0 : 8)).toFixed(1); }).join(',') + ' L' + at(mcx, 0, .5).map(function (v, k) { return (v - (k ? 0 : 8)).toFixed(1); }).join(','), arrow: true, dots: 1, cls: 'hop' });
746        return W;
747      });
748      /* one part at a time, left to right: the sources, each capability in turn, then the console; the step
749         below follows what the lit part does (collect, detect, investigate, respond) */
750      const steps = box.querySelectorAll('.pp-step'), capEls = box.querySelectorAll('.pp-cap');
751      const SEQ = [['src', 0], [0, 1], [1, 1], [2, 1], [3, 2], [4, 1], ['mc', 3]];
752      let k = -1;
753      function stepTo(i) {
754        k = i;
755        const s = SEQ[i];
756        srcs.forEach(function (e) { e.classList.toggle('on', s[0] === 'src'); });
757        capEls.forEach(function (c, j) { c.classList.toggle('on', s[0] === j); });
758        mcx.classList.toggle('on', s[0] === 'mc');
759        steps.forEach(function (e, j) { e.classList.toggle('on', j === s[1]); });
760      }
761      /* paused while the Screens section is carrying these cards away (it adds .carrying) */
762      if (!still) setInterval(function () { if (inView && shown && !sec.classList.contains('carrying')) stepTo((k + 1) % SEQ.length); }, 1800);
763    }
764
765    /* ---- Screens (?shots=carry|orbit|off): the console itself, as the third part of the page.
766       Only built when neither the hero nor the after-hero already shows it. Takes the hero's tone, so the
767       three parts alternate. Nothing pins; carry's hand-over plays by itself once started (never rests half-way); orbit follows the scroll.
768         carry  one big console window. As you scroll, the after-hero's own capability cards (Pillars or
769                Pipeline) come loose from their section and drift down into the window's tab strip; then each
770                tab lights in turn and its screen shows (click to choose). With Layers or Bento there are no
771                cards to carry, so the tabs themselves gather into the strip instead.
772         orbit  every screen on a 3D carousel: the chosen one turns to the front at full size, the rest
773                wait behind it, turned away and dimmed; the list beside it moves on by itself */
774    (function () {
775      const sm = root.getAttribute('data-shots');
776      if (!sm || sm === 'off' || root.getAttribute('data-hero') === 'screen' || !window.PUI) return;
777      /* the screens are captures of the product's own pages (trmc-mockup, app menus hidden), in both themes;
778         the light hero tone gets the light ones. Capabilities with a workflow show it as steps. */
779      const CAPS = [
780        { k: 'edr', t: 'Endpoint Detection &amp; Response', shots: [['edr-dashboard', 'dashboard', 'The dashboard: open threats, what needs a decision, and adversary behaviour mapped to MITRE ATT&amp;CK.']] },
781        { k: 'itdr', t: 'Identity Threat Protection', shots: [['itdr-overview', 'itdr / overview', 'Identity detections, lockout sources and the riskiest identities across the domain.']] },
782        { k: 'threat', t: 'Threat Hunting', shots: [['hunt-results', 'hunt / processes', 'A hunt query across every endpoint: hidden-window PowerShell, with every match.']] },
783        { k: 'ir', t: 'Incident Response &amp; Forensics', shots: [['ir-liveview-cmd', 'live-view / WS-AISHA204', 'Live View: run a command on the endpoint, answered in seconds.'],
784          ['ir-liveview-proc', 'live-view / WS-AISHA204 / processes', 'Live View: every process on the endpoint, live.'],
785          ['ir-forensics', 'forensics / investigation', 'A forensic investigation across endpoints, mapped to MITRE ATT&amp;CK.']] },
786        { k: 'exp', t: 'Vulnerability &amp; Exposure Detection', shots: [['exp-summary', 'exposure / summary', 'The exposure dashboard: what is actively exploited, and where.'],
787          ['exp-detail', 'exposure / details', 'One vulnerability, the endpoints it affects, and why.']] }];
788      const ORDER = CAPS.map(function (c) { return c.k; });
789      const SHOTS = [];
790      CAPS.forEach(function (c, i) { c.first = SHOTS.length; c.shots.forEach(function (s, j) { SHOTS.push({ c: i, j: j, n: c.shots.length, key: s[0], url: s[1], d: s[2] }); }); });
791      const img = function (key) {
792        return '<img class="sx-img sx-d" src="/shared/screens/' + key + '-dark.webp" alt="" loading="lazy" decoding="async" width="1600" height="960">' +
793          '<img class="sx-img sx-l" src="/shared/screens/' + key + '-light.webp" alt="" loading="lazy" decoding="async" width="1600" height="960">';
794      };
795      const stepDots = function (s) { return s.n < 2 ? '' : '<span class="sx-steps">' + SHOTS.filter(function (x) { return x.c === s.c; }).map(function (x) { return '<i' + (x.j === s.j ? ' class="on"' : '') + '></i>'; }).join('') + '</span>'; };
796      const tint = function (k) { const f = FIVE.find(function (c) { return c[0] === k; }); return f ? f[1] : 'b'; };
797      const bar = function (u) { return '<div class="shx-bar"><i></i><i></i><i></i><span>console.threatresponder.com / <em>' + (u || '') + '</em></span></div>'; };
798      const win = function (s) { return '<div class="shx-win">' + bar(s.url) + '<div class="shx-scr">' + img(s.key) + '</div></div>'; };
799      const clamp = function (v) { return v < 0 ? 0 : v > 1 ? 1 : v; };
800      const ease = function (k) { return 1 - Math.pow(1 - k, 3); };
801      const io = function (k) { return k < .5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; };
802      const narrow = function () { return window.innerWidth <= 1100; };
803      const HEAD = '<div class="shx-head"><h2>Inside Mission Control</h2><p>Every capability of the agent, in one console, for one company or all of them.</p></div>';
804
805      /* ---- live player: the product's own pages, not pictures. The app mockups are served at /mockup/ next to
806         /website/ (same origin), so two iframes can load them with the app's header and menu hidden, scaled into
807         the window, and a script can drive them: the next page loads hidden and cross-fades in as soon as it has
808         drawn (its own loading effects play), then the step's actions run (typing a query, clicking Run, opening
809         a row) and it stays a moment. Three steps per capability. If /mockup/ can't be reached (e.g. a plain
810         local server), the screenshots above stay and cycle instead. */
811      const APP = '/mockup/';
812      /* Live View's process feed adds or ends a random process every 4.2 s; for the tour it ticks every 1.2 s and
813         its first two ticks start a process (conhost.exe, then cmd.exe: both sort near the top of the list) */
814      const liveSpawns = function (d, w) {
815        const si = w.setInterval, R = w.Math.random, plan = [[0, 0], [0, 6.5 / 14]];
816        w.setInterval = function (fn, ms) {
817          if (ms !== 4200) return si.apply(w, arguments);
818          return si.call(w, function () {
819            const q = plan.shift();
820            if (q) { w.Math.random = function () { return q.length ? q.shift() : R(); }; }
821            try { fn(); } finally { w.Math.random = R; }
822          }, 1200);
823        };
824      };
825      const TOUR = [
826        [{ page: 'dashboard', url: 'dashboard', d: 'The dashboard: open threats, what needs a decision, and MITRE ATT&amp;CK at a glance.', stay: 2600 },
827          { page: 'alerts-threats', url: 'alerts / threats', d: 'The threat queue: every threat, its kill chain, and what the agent already did.', stay: 2600 },
828          { page: 'threat-story-full', bare: true, url: 'threats / story', d: 'Threat Story: the attack replayed step by step, mapped to MITRE ATT&amp;CK.', run: [['wait', 700], ['click', '#playButton']], stay: 4600 }],
829        [{ page: 'itdr-overview', url: 'itdr / overview', d: 'Identity posture, detections and authentication across the domain.', stay: 2600 },
830          { page: 'itdr-detections', url: 'itdr / detections', d: 'Identity detections, correlated and ranked by risk.', stay: 2800 },
831          { page: 'lateral-movement', url: 'itdr / lateral-movement', d: 'Lateral movement: how an attacker reached Tier 0, replayed.', run: [['wait', 600], ['click', '#lmReplayAttack']], stay: 4200 }],
832        [{ page: 'hunt-process', q: '&dense=1', url: 'hunt / processes', d: 'Write a hunt query and run it across every endpoint.', run: [['wait', 250], ['type', '#hxInput', 'process.name(powershell.exe)', 22], ['wait', 150], ['run', '#hxRun']], stay: 2000 },
833          { page: 'hunt-process', q: '&dense=1', same: true, url: 'hunt / processes', d: 'Open any match: the full process record, and where to pivot next.', run: [['click', '[data-exp]']], stay: 2200 },
834          { page: 'hunt-network', url: 'hunt / network', d: 'Network hunts from the library: connections on a rare port.', run: [['wait', 350], ['click', '[data-runlib]']], stay: 2400 }],
835        [{ page: 'liveview-details', url: 'live-view / WS-AISHA204', d: 'Live View: run a command on the endpoint, answered in seconds.', run: [['wait', 250], ['type', '#lvxTermInput', 'netstat', 45], ['wait', 120], ['click', '#lvxTermRun']], stay: 2700 },
836          { page: 'liveview-details', same: true, url: 'live-view / WS-AISHA204 / script', d: 'Write a script and run it on the endpoint.', run: [['click', '#lvxTermMode button:nth-child(2)'], ['wait', 350],
837            ['scroll', 200], ['cm', '.CodeMirror', "Get-Process | Where-Object { $_.Path -like '*\\Users\\Public\\*' } |\n  Select-Object Name, Id, Path", 9], ['wait', 200], ['click', '#lvxScriptRun']], stay: 2400 },
838          { page: 'liveview-details', same: true, url: 'live-view / WS-AISHA204 / processes', d: 'Every process on the endpoint, live: new ones pop up as they start.', run: [['scroll', 0], ['fn', liveSpawns], ['click', 'text:Processes']], stay: 4000 }],
839        [{ page: 'exposure-summary', url: 'exposure / summary', d: 'Exposure: what is actively exploited, where, and how fast it gets patched.', stay: 2600 },
840          { page: 'exposure-details', url: 'exposure / details', d: 'Each vulnerability, the endpoints it affects, and why.', run: [['wait', 700], ['click', '[data-expand="0"]']], stay: 3200 },
841          { page: 'exposure-misconfigs', url: 'exposure / misconfigurations', d: 'Misconfigurations that weaken each endpoint, ranked by impact.', stay: 2800 }]];
842      const CHROME = '#meniuHeader,#nvSide,.page-footer-wrapper,#ovxDock,.ovx-dock,.nv-side{display:none!important}' +
843        'html body .page-container,html body .page-container .page-content-wrapper{padding-top:0!important;margin-top:0!important}' +
844        'html body .page-container .page-content-wrapper .page-content{margin-left:0!important;margin-top:0!important;min-height:0!important;border:0!important;border-radius:0!important}' +
845        'body{padding-top:0!important;overflow-x:hidden!important}::-webkit-scrollbar{display:none}html{scrollbar-width:none}';
846      const tourDots = function (mi, si) { return '<span class="sx-steps">' + TOUR[mi].map(function (x, j) { return '<i' + (j === si ? ' class="on"' : '') + '></i>'; }).join('') + '</span>'; };
847      /* vw: the width the app lays out at (the window scales it); narrower = larger text */
848      function Player(layer, on, vw) {
849        vw = vw || 1600;
850        layer.innerHTML = '<iframe tabindex="-1" aria-hidden="true" title="ThreatResponder console"></iframe><iframe tabindex="-1" aria-hidden="true" title="ThreatResponder console"></iframe>';
851        const F = [].slice.call(layer.querySelectorAll('iframe')), CANCEL = { c: 1 }, DEAD = { d: 1 };
852        let front = 0, gen = 0, mi = 0, si = 0, dead = false, loaded = '';
853        F.forEach(function (f) { f.style.width = vw + 'px'; f.style.height = Math.round(vw * .6) + 'px'; });
854        const fit = function () { const k = layer.clientWidth / vw; if (k) F.forEach(function (f) { f.style.transform = 'scale(' + k.toFixed(4) + ')'; }); };
855        if (window.ResizeObserver) new ResizeObserver(fit).observe(layer);
856        window.addEventListener('resize', fit); fit();
857        const theme = function () { return root.getAttribute('data-tone') === 'light' ? 'light' : 'dark'; };
858        /* the hero tone switch flips the app's own theme in place */
859        new MutationObserver(function () {
860          F.forEach(function (f) {
861            try {
862              const d = f.contentDocument, tb = d.getElementById('themeButton');
863              /* the Threat Story page draws its graph for its own theme: flip it with its own button so it redraws */
864              if (tb && d.documentElement.getAttribute('data-theme') !== theme()) tb.click();
865              else d.documentElement.setAttribute('data-theme', theme());
866            } catch (e) { /* not loaded */ }
867          });
868        })
869          .observe(root, { attributes: true, attributeFilter: ['data-tone'] });
870        const sleep = function (ms, my) { return new Promise(function (res, rej) { setTimeout(function () { if (my === gen) res(); else rej(CANCEL); }, ms); }); };
871        const wake = async function (my) { while (!on.visible() || document.hidden) await sleep(300, my); };
872        /* the embedded app must never move the website: no scrolling into view, focus without scrolling */
873        function prep(d) {
874          const s = d.createElement('style'); s.textContent = CHROME; d.head.appendChild(s);
875          const w = d.defaultView, P = w.HTMLElement.prototype, focus = P.focus;
876          P.scrollIntoView = function () {};
877          P.focus = function () { focus.call(this, { preventScroll: true }); };
878          d.documentElement.setAttribute('data-theme', theme());
879        }
880        async function load(page, my, bare, q) {
881          const f = F[1 - front], want = page + '.html', t0 = Date.now();
882          /* Static hosts may redirect name.html to name. Never accept a stale frame. */
883          const canonical = function (url) {
884              const u = new URL(url, location.href);
885
886              return u.origin + u.pathname.replace(/\.html\/?$/, '').replace(/\/$/, '') + u.search;
887          };
888          f.classList.remove('on');
889          /* the Threat Story page takes its theme from its own saved setting (same origin), read before it draws */
890          try { localStorage.setItem('tr-threat-story-theme', theme()); } catch (e) { /* storage blocked */ }
891          f.src = APP + want + '?theme=' + theme() + (q || '');
892          for (;;) {
893            await sleep(40, my);
894            let d = null;
895            try { d = f.contentDocument; } catch (e) { throw DEAD; }
896            if (d && canonical(d.URL) === canonical(f.src) && d.body && d.readyState !== 'loading') {
897              if (d.querySelector('.page-content') || (bare && d.body.children.length > 1)) { prep(d); break; }
898              if (d.readyState === 'complete') throw DEAD;
899            }
900            if (Date.now() - t0 > 9000) throw DEAD;
901          }
902          await sleep(60, my);
903          f.classList.add('on'); F[front].classList.remove('on'); front = 1 - front; loaded = page;
904          on.live();
905        }
906        function find(q) {
907          const d = F[front].contentDocument;
908          if (q === 'lib-play') return [].slice.call(d.querySelectorAll('.fa-play')).filter(function (e) { return e.offsetParent && !/Run on/.test(e.parentNode.textContent); })[0];
909          if (q.indexOf('text:') === 0) {
910            const t = q.slice(5);
911            return [].slice.call(d.querySelectorAll('.page-content a, .page-content button, .page-content li, .page-content span, .page-content div'))
912              .filter(function (e) { return e.offsetParent && e.textContent.trim().indexOf(t) === 0; })
913              .sort(function (a, b) { return a.textContent.length - b.textContent.length; })[0];
914          }
915          return d.querySelector(q);
916        }
917        async function act(a, my) {
918          if (a[0] === 'wait') return sleep(a[1], my);
919          if (a[0] === 'fn') { a[1](F[front].contentDocument, F[front].contentWindow); return; }
920          if (a[0] === 'scroll') { F[front].contentWindow.scrollTo({ top: a[1], behavior: 'smooth' }); return sleep(450, my); }
921          /* pages draw in stages (Live View connects before its terminal exists): wait for the target */
922          let el = find(a[1]);
923          for (let k = 0; !el && k < 50; k++) { await sleep(60, my); el = find(a[1]); }
924          const w = F[front].contentWindow;
925          if (!el) return;
926          if (a[0] === 'click') { el.click(); return sleep(150, my); }
927          if (a[0] === 'run') { el.disabled = false; el.click(); return sleep(150, my); }
928          /* a CodeMirror editor: type into its document */
929          if (a[0] === 'cm' && el.CodeMirror) {
930            const cm = el.CodeMirror;
931            cm.setValue('');
932            for (const ch of a[2]) { cm.replaceRange(ch, cm.posFromIndex(cm.getValue().length)); await sleep((a[3] || 30) * (.6 + Math.random() * .8), my); }
933            return;
934          }
935          if (a[0] === 'type') {
936            el.value = '';
937            for (const ch of a[2]) { el.value += ch; el.dispatchEvent(new w.Event('input', { bubbles: true })); await sleep((a[3] || 30) * (.6 + Math.random() * .8), my); }
938          }
939        }
940        async function loop(my) {
941          try {
942            for (;;) {
943              await wake(my);
944              if (si === 0 && on.enter && await on.enter(mi, my, sleep)) loaded = '';
945              const st = TOUR[mi][si];
946              on.step(mi, si, st);
947              if (!st.same || loaded !== st.page) await load(st.page, my, st.bare, st.q);
948              for (const a of st.run || []) await act(a, my);
949              await sleep(st.stay, my);
950              if (++si >= TOUR[mi].length) { si = 0; mi = (mi + 1) % TOUR.length; }
951            }
952          } catch (e) {
953            if (e !== CANCEL) {
954                /* A failed embedded page must not leave an apparently running tour frozen. */
955                console.warn('Mission Control live preview unavailable; continuing with screenshots.', e === DEAD ? 'Frame failed to load.' : e);
956                dead = true; layer.remove(); on.dead();
957            }
958          }
959        }
960        return {
961          go: function (i) { if (dead) return; gen++; mi = i; si = 0; loop(gen); },
962          get dead() { return dead; },
963          get on() { return !dead && gen > 0; }
964        };
965      }
966
967      const box = document.createElement('section');
968      box.className = 'shx shx-' + sm + (still ? ' still' : ''); box.id = 'shx';
969      if (sm === 'carry') {
970        box.innerHTML = '<div class="wrap shx-in">' + HEAD + '<div class="sxmg"><div class="sxmg-tabs" role="tablist" aria-label="Capabilities">' + CAPS.map(function (c, i) {
971          return '<button type="button" class="sxmg-tab ic-' + tint(c.k) + '" role="tab" data-i="' + i + '"><span class="sxmg-ic">
971' + icon(c.k) + '</span><b>' + c.t + '</b></button>';
972        }).join('') + '</div>' +
973          '<div class="sxmg-stage"><div class="shx-win sxmg-win">' + bar('').replace('<em></em>', '<em class="sxmg-url"></em>') +
974          '<div class="sxmg-scr">' + SHOTS.map(function (s) { return '<div class="sxmg-s">' + img(s.key) + '</div>'; }).join('') + (still ? '' : '<div class="sx-live"></div>') + '</div></div></div>' +
975          '<p class="sxmg-cap" aria-live="polite"></p></div></div>';
976      }
977      if (sm === 'orbit') {
978        box.innerHTML = '<div class="wide shx-in sxob-grid"><div class="sxob-side">' + HEAD +
979          '<ul class="sxob-list">' + CAPS.map(function (c, i) {
980            return '<li><button type="button" data-i="' + i + '"><span class="sxob-ic ic-' + tint(c.k) + '">' + icon(c.k) + '</span><b>' + c.t + '</b><i class="sxob-run"></i></button></li>';
981          }).join('') + '</ul></div>' +
982          '<div class="sxob"><div class="sxob-stage">' + CAPS.map(function (c, i) { return '<div class="sxob-t" data-i="' + i + '">' + win(SHOTS[c.first]) + '</div>'; }).join('') + '</div>' +
983          '<div class="sxob-cap"><button type="button" class="sxob-nav" data-d="-1" aria-label="Previous screen">' + I('chevronL') + '</button>' +
984          '<div class="sxob-txt" aria-live="polite"></div><button type="button" class="sxob-nav" data-d="1" aria-label="Next screen">' + I('chevronR') + '</button></div></div></div>';
985      }
986      sec.parentNode.insertBefore(box, sec.nextSibling);
987      box.querySelectorAll('.sxob-nav i').forEach(function (el) {
988        el.outerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="' + (el.getAttribute('data-ic') === 'chevronL' ? 'M14.5 6l-6 6 6 6' : 'M9.5 6l6 6-6 6') + '" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
989      });
990
991      let seen = false;
992      new IntersectionObserver(function (es) { seen = es[0].isIntersecting; }, { threshold: 0 }).observe(box);
993
994      /* one scroll-linked update per frame */
995      function onScroll(cb) {
996        if (still) { cb(1); return; }
997        let queued = false;
998        const run = function () { queued = false; cb(null); };
999        const req = function () { if (!queued) { queued = true; requestAnimationFrame(run); } };
1000        window.addEventListener('scroll', req, { passive: true });
1001        window.addEventListener('resize', req);
1002        document.addEventListener('site:ready', req);
1003        if (document.fonts) document.fonts.ready.then(req);
1004        req();
1005      }
1006      /* page position from the layout, not the painted box: ignores the lift of a highlighted card and any
1007         transition still running, so a card never jumps when it comes loose */
1008      function page(el) {
1009        let x = 0, y = 0, e = el;
1010        while (e) { x += e.offsetLeft; y += e.offsetTop; e = e.offsetParent; }
1011        return { x: x, y: y, w: el.offsetWidth, h: el.offsetHeight };
1012      }
1013      /* played, not scrubbed (user, 24 Sep: the page must not rest with the cards half-way): once the page passes the
1014         start line the move runs to its end by itself in PLAY_MS, and back when you scroll above the line again; a
1015         40 px band keeps it from flickering on the line. The first frame jumps, so a reload mid-page does not replay. */
1016      const PLAY_MS = 1300;
1017      function played(line, draw) {
1018        let p = -1, want = 0, from = 0, t0 = 0, dur = 0, raf = 0;
1019        const tick = function (now) { const k = dur ? clamp((now - t0) / dur) : 1; p = from + (want - from) * k; draw(p); raf = k < 1 ? requestAnimationFrame(tick) : 0; };
1020        return function () {
1021          const l = line(), sy = window.scrollY, w = sy > l + 40 ? 1 : sy < l - 40 ? 0 : want;
1022          if (p < 0) { want = w; p = w; draw(p); return; }
1023          if (w !== want) { want = w; from = p; t0 = performance.now(); dur = PLAY_MS * Math.abs(want - from); if (!raf) raf = requestAnimationFrame(tick); }
1024          else if (!raf) draw(p);
1025        };
1026      }
1027
1028      if (sm === 'carry') {
1029        const tabs = [].slice.call(box.querySelectorAll('.sxmg-tab')), wn = box.querySelector('.sxmg-win'), scr = box.querySelectorAll('.sxmg-s');
1030        const url = box.querySelector('.sxmg-url'), cap = box.querySelector('.sxmg-cap');
1031        let docked = false, cur = -1, hold = 0, last = Date.now();
1032        /* show screen n; its capability's tab lights, the caption says what the screen shows */
1033        function show(n, user) {
1034          if (user) hold = Date.now() + 15000;
1035          last = Date.now();
1036          if (n === cur) return;
1037          cur = n;
1038          const s = SHOTS[n];
1039          tabs.forEach(function (t, j) { t.classList.toggle('on', j === s.c); t.setAttribute('aria-selected', j === s.c); });
1040          scr.forEach(function (e, j) { e.classList.toggle('on', j === n); });
1041          url.textContent = s.url;
1042          cap.innerHTML = '<b>' + CAPS[s.c].t + '.</b> ' + s.d + stepDots(s);
1043        }
1044        const layer = box.querySelector('.sx-live');
1045        const player = layer && Player(layer, {
1046          visible: function () { return seen; },
1047          live: function () { box.classList.add('live'); },
1048          dead: function () { box.classList.remove('live'); last = Date.now(); },
1049          step: function (mi, si, st) {
1050            tabs.forEach(function (t, j) { t.classList.toggle('on', j === mi); t.setAttribute('aria-selected', j === mi); });
1051            url.textContent = st.url;
1052            cap.innerHTML = '<b>' + CAPS[mi].t + '.</b> ' + st.d + tourDots(mi, si);
1053          }
1054        });
1055        tabs.forEach(function (t, i) { t.addEventListener('click', function () { if (player && player.on) player.go(i); else show(CAPS[i].first, true); }); });
1056        show(0);
1057        /* the tour starts once the cards have landed */
1058        const setDocked = function (d) {
1059          if (d === docked) return;
1060          docked = d; box.classList.toggle('docked', d);
1061          if (d && player && !player.on && !player.dead) player.go(0);
1062        };
1063        const srcs = [].slice.call(document.querySelectorAll('#pf .pi-card, #pf .pp-cap'));
1064
1065        if (srcs.length === ORDER.length && !still) {
1066          /* copies of the after-hero's own cards live in a layer in page coordinates. While a card travels, the
1067             original is hidden with visibility (instant: the cards' own opacity transition made them blink) and
1068             the after-hero stops lighting its cards, so the copy always matches what it replaces. Each copy lifts
1069             (shadow grows, then settles), shrinks to its tab's width and, over the last stretch, is clipped to the
1070             tab's height as it hands over to the real tab underneath. Played, not scrubbed: it starts when the tab
1071             strip is a quarter of the way up the screen and runs to the end (or back) by itself. */
1072          const layer = document.createElement('div');
1073          layer.className = sec.className.replace(/\b(pre|go|still|carrying)\b/g, '') + ' sxc-layer';
1074          layer.setAttribute('aria-hidden', 'true');
1075          const ghosts = srcs.map(function (s) { const g = s.cloneNode(true); g.classList.remove('on'); g.removeAttribute('style'); layer.appendChild(g); return g; });
1076          document.body.appendChild(layer);
1077          box.classList.add('carry-live');
1078          let carrying = false;
1079          const play = played(function () { return page(tabs[0]).y - window.innerHeight * .75; }, function (p) { draw(p); });
1080          onScroll(function () {
1081            if (narrow()) {
1082              ghosts.forEach(function (g) { g.style.visibility = 'hidden'; });
1083              srcs.forEach(function (s) { s.style.visibility = ''; });
1084              tabs.forEach(function (t) { t.style.opacity = ''; });
1085              box.classList.remove('carry-live'); sec.classList.remove('carrying'); carrying = false;
1086              setDocked(true); return;
1087            }
1088            box.classList.add('carry-live');
1089            play();
1090          });
1091          function draw(p) {
1092            const A = srcs.map(page), B = tabs.map(page);
1093            const now = p > 0;
1094            if (now !== carrying) {
1095              carrying = now; sec.classList.toggle('carrying', now);
1096              if (now) srcs.forEach(function (s) { s.classList.remove('on'); });
1097            }
1098            let all = true;
1099            srcs.forEach(function (s, i) {
1100              const e = io(clamp(p * 1.18 - i * .045)), g = ghosts[i], a = A[i], b = B[i], t = tabs[i];
1101              if (e <= 0) { g.style.visibility = 'hidden'; s.style.visibility = ''; t.style.opacity = 0; all = false; return; }
1102              /* size changes only a little (Pipeline's cards are half a tab wide; scaling them to fit made them huge):
1103                 each card flies to its tab's centre and the tab, wider or narrower, takes over around it */
1104              const hand = clamp((e - .88) / .12), k = Math.min(1.08, Math.max(.9, b.w / a.w)), sc = 1 + (k - 1) * e, lift = Math.sin(Math.PI * Math.min(1, e / .9));
1105              const cut = Math.max(0, a.h - (b.h / sc)) * hand;
1106              s.style.visibility = 'hidden';
1107              g.style.width = a.w + 'px'; g.style.height = a.h + 'px';
1108              const cx = a.x + a.w / 2 + (b.x + b.w / 2 - a.x - a.w / 2) * e;
1109              g.style.transform = 'translate(' + (cx - a.w * sc / 2).toFixed(1) + 'px,' + (a.y + (b.y - a.y) * e).toFixed(1) + 'px) scale(' + sc.toFixed(4) + ')';
1110              g.style.clipPath = cut > .5 ? 'inset(0 0 ' + cut.toFixed(1) + 'px 0 round 14px)' : '';
1111              g.style.filter = lift > .02 ? 'drop-shadow(0 ' + (18 * lift).toFixed(1) + 'px ' + (30 * lift).toFixed(1) + 'px rgba(5, 12, 45, ' + (.3 * lift).toFixed(3) + '))' : '';
1112              /* the copy clears quickly while the tab comes up beneath it, so the two labels overlap only briefly */
1113              const out = clamp(hand * 1.7);
1114              g.style.opacity = (1 - out).toFixed(3); g.style.visibility = out >= 1 ? 'hidden' : 'visible';
1115              t.style.opacity = clamp(hand * 1.35).toFixed(3);
1116              if (e < 1) all = false;
1117            });
1118            setDocked(all);
1119          }
1120        } else {
1121          /* no cards to carry (Layers or Bento after the hero): the tabs gather from a spread row into the strip */
1122          const gather = function (p) {
1123            const mid = (tabs.length - 1) / 2;
1124            tabs.forEach(function (t, i) {
1125              const e = ease(clamp(p * 1.3 - i * .06)), u = 1 - e;
1126              t.style.transform = u < .002 ? '' : 'translate(' + ((i - mid) * 70 * u).toFixed(1) + 'px,' + (-34 * u).toFixed(1) + 'px) scale(' + (1 + .05 * u).toFixed(3) + ')';
1127            });
1128            const w = 1 - ease(p); wn.style.transform = p >= 1 ? '' : 'translateY(' + (90 * w).toFixed(1) + 'px) rotateX(' + (10 * w).toFixed(2) + 'deg)';
1129            setDocked(p >= .999);
1130          };
1131          const play = played(function () { return page(wn).y - window.innerHeight * .8; }, gather);
1132          onScroll(function (fixed) { if (fixed !== null) gather(fixed); else if (narrow()) gather(1); else play(); });
1133        }
1134        if (!still) setInterval(function () {
1135          const now = Date.now();
1136          if (player && !player.dead) return;
1137          if (!seen || !docked || now < hold) { last = Math.max(last, now - 1500); return; }
1138          if (now - last >= 4600) show((cur + 1) % SHOTS.length);
1139        }, 200);
1140      }
1141
1142      if (sm === 'orbit') {
1143        const wrap = box.querySelector('.sxob'), stage = box.querySelector('.sxob-stage'), tiles = [].slice.call(box.querySelectorAll('.sxob-t'));
1144        const items = [].slice.call(box.querySelectorAll('.sxob-list button')), txt = box.querySelector('.sxob-txt'), list = box.querySelector('.sxob-list'), n = tiles.length;
1145        const STEP = 4600, pos = tiles.map(function () { return null; });
1146        let cur = -1, hold = 0, over = false, hoverT = 0;
1147        const X = function (d) { return 'translate3d(' + (d * 44) + '%, 0, ' + (-Math.abs(d) * 380) + 'px) rotateY(' + (-d * 36) + 'deg)' + (d ? ' scale(.84)' : ''); };
1148        /* offset from the chosen screen: 0 in front, ±1 and ±2 turned away behind it, 3 out of sight. A screen
1149           that wraps round the back first appears, unseen, at the far edge it comes from, then turns in. */
1150        function place(i, user) {
1151          if (user) hold = Date.now() + 12000;
1152          if (i === cur) return;
1153          const dir = cur < 0 ? 1 : (((i - cur) % n + n) % n <= n / 2 ? 1 : -1);
1154          cur = i;
1155          tiles.forEach(function (t, j) {
1156            let d = ((j - i) % n + n) % n; if (d > n / 2) d -= n;
1157            if (d === n / 2 && dir > 0) d = -n / 2;
1158            const was = pos[j];
1159            if (was !== null && Math.abs(d - was) > 1) {
1160              const from = d > 0 ? 3 : -3;
1161              t.style.transition = 'none'; t.style.transform = X(from); t.setAttribute('data-a', 3);
1162              void t.offsetWidth; t.style.transition = '';
1163            }
1164            pos[j] = d;
1165            t.style.transform = X(d);
1166            t.setAttribute('data-a', Math.abs(d));
1167            t.style.zIndex = 10 - Math.abs(d);
1168            t.classList.toggle('on', d === 0);
1169            t.setAttribute('aria-hidden', d !== 0);
1170          });
1171          const s = SHOTS[CAPS[i].first];
1172          items.forEach(function (b, j) { b.classList.toggle('on', j === i); b.setAttribute('aria-pressed', j === i); });
1173          if (!(player && player.on)) txt.innerHTML = '<b>' + CAPS[i].t + '</b><span>' + s.d + '</span>';
1174          /* the running line under the chosen item shows when the carousel moves on */
1175          list.classList.remove('run'); void list.offsetWidth; if (!still && !user && !(player && player.on)) list.classList.add('run');
1176        }
1177        /* live: the front screen plays its capability's three steps, then the carousel turns to the next one */
1178        const layer = still ? null : document.createElement('div');
1179        let player = null;
1180        if (layer) {
1181          layer.className = 'sx-live';
1182          player = Player(layer, {
1183            visible: function () { return seen; },
1184            live: function () { box.classList.add('live'); layer.classList.add('show'); },
1185            dead: function () { box.classList.remove('live'); },
1186            enter: async function (i, my, sleep) {
1187              let moved = false;
1188              if (cur !== i) { layer.classList.remove('show'); place(i); await sleep(1150, my); }
1189              const host = tiles[i].querySelector('.shx-scr');
1190              if (layer.parentNode !== host) { layer.classList.remove('show'); host.appendChild(layer); moved = true; }
1191              return moved;
1192            },
1193            step: function (mi, si, st) {
1194              items.forEach(function (b, j) { b.classList.toggle('on', j === mi); b.setAttribute('aria-pressed', j === mi); });
1195              txt.innerHTML = '<b>' + CAPS[mi].t + '</b><span>' + st.d + '</span>' + tourDots(mi, si);
1196              tiles[mi].querySelector('.shx-bar em').textContent = st.url;
1197            }
1198          }, 1280);
1199        }
1200        const pick = function (i) { if (player && player.on) player.go(i); else place(i, true); };
1201        items.forEach(function (b, i) {
1202          /* a short settle before hover picks, so sweeping the mouse down the list doesn't spin the carousel */
1203          const go = function () { if (cur !== i) pick(i); else hold = Date.now() + 12000; };
1204          /* hover picks only in the picture mode; live pages load on click */
1205          b.addEventListener('mouseenter', function () { clearTimeout(hoverT); if (!(player && player.on)) hoverT = setTimeout(go, 140); });
1206          b.addEventListener('mouseleave', function () { clearTimeout(hoverT); });
1207          b.addEventListener('focus', go);
1208          b.addEventListener('click', function () { clearTimeout(hoverT); go(); });
1209        });
1210        tiles.forEach(function (t, i) { t.addEventListener('click', function () { if (cur !== i) pick(i); }); });
1211        box.querySelectorAll('.sxob-nav').forEach(function (b) { b.addEventListener('click', function () { pick((cur + (+b.getAttribute('data-d')) + n) % n); }); });
1212        /* the carousel waits while the pointer is on it or on the list */
1213        [wrap, list].forEach(function (el) {
1214          el.addEventListener('mouseenter', function () { over = true; list.classList.add('held'); });
1215          el.addEventListener('mouseleave', function () { over = false; hold = Math.max(hold, Date.now() + 2500); list.classList.remove('held'); });
1216        });
1217        place(0);
1218        /* leans back while it comes up the page, level once in view */
1219        onScroll(function (fixed) {
1220          let p = fixed;
1221          if (p === null) { const r = stage.getBoundingClientRect(), vh = window.innerHeight; p = narrow() ? 1 : clamp((vh - r.top) / (vh * .7)); }
1222          stage.style.setProperty('--p', ease(p).toFixed(3));
1223        });
1224        if (!still) {
1225          let last = Date.now();
1226          setInterval(function () {
1227            const now = Date.now();
1228            if (player && !player.dead) {
1229              if (!player.on && seen) player.go(0);
1230              return;
1231            }
1232            if (!seen || over || now < hold) { last = now; list.classList.remove('run'); return; }
1233            if (!list.classList.contains('run')) { list.classList.add('run'); last = now; }
1234            if (now - last >= STEP) { last = now; place((cur + 1) % n); }
1235          }, 200);
1236        }
1237      }
1238    })();
1239    window.__pf = { mode: mode };
1240  })();
1241
1242
1243  /* ---- Predict Attacks Before they Occur (?predict=dash|window|off): the first of the headline's three verbs, after
1244     the Screens and before Neutralize (Stop). window (my first variant) has two halves, both drawn as the product sees them:
1245       exposure  one week of one endpoint: every vulnerable application opens a CVE the moment it is installed
1246                 and closes it the moment it is updated, each window kept on record; under it, what a weekly
1247                 scan would have seen (one of three, days late). A cursor sweeps the week while in view.
1248       identity  what the agent sees about accounts on every endpoint: new accounts, privilege, where
1249                 privileged accounts log on, unusual logons, weak authentication; one event at a time.
1250     Takes the opposite tone of the Screens (a new chapter). Replaces the old Predict section. */
1251  (function () {
1252    const root = document.documentElement;
1253    const hero = document.getElementById('hero');
1254    if (!hero) return;
1255    const still = root.classList.contains('poster') || root.classList.contains('reduced');
1256    const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; };
1257    const IC = {
1258      bug: '<rect x="7" y="8" width="10" height="12" rx="5"/><path d="M12 8V5M9 5l1.5 2M15 5l-1.5 2M4 12h3M17 12h3M4 17h3M17 17h3"/>',
1259      user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"/>',
1260      check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>'
1261    };
1262    /* the week runs Mon 00:00 → next Mon 06:00 (174 h); the weekly scan runs Mon 02:00 */
1263    const H = 174, at = function (d, h, m) { return ((d * 24 + h + (m || 0) / 60) / H * 100).toFixed(2); };
1264    const EXP = [
1265      { app: 'Google Chrome 124.0.6367.60', cve: 'CVE-2024-4671', kev: true, a: [1, 9, 14], b: [1, 15, 40], dur: '6 h 26 m', how: 'auto-updated' },
1266      { app: '7-Zip 24.07', cve: 'CVE-2025-0411', kev: true, a: [2, 11, 2], b: [4, 10, 15], dur: '1 d 23 h', how: 'updated to 24.09' },
1267      { app: 'Zoom 5.16.2', cve: 'CVE-2024-24691', kev: false, a: [3, 14, 30], b: null, dur: 'open', how: '' }];
1268    const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon'];
1269    const IDN = [
1270      { k: 'new', t: 'New local administrator', w: '<code>helpdesk2</code> created on <code>SRV-APP-02</code>' },
1271      { k: 'priv', t: 'Privileged logon on a workstation', w: '<code>da-lopez</code> (Domain Admins) on <code>WS-FIN-07</code>' },
1272      { k: 'beh', t: 'First logon at this hour', w: '<code>rharper</code> at 03:12 on <code>WS-RHARPER</code>' },
1273      { k: 'weak', t: 'Weak authentication', w: '<code>hd-admin</code> used NTLMv1 on <code>WS-KOVAC112</code>' },
1274      { k: 'svc', t: 'Service account used interactively', w: '<code>svc-backup</code> signed in on <code>WS-AISHA204</code>' },
1275      { k: 'priv', t: 'Added to a privileged group', w: '<code>jmiller</code> joined Backup Operators' }];
1276    const KIND = { new: 'Identity creation', priv: 'Privilege', beh: 'Behavior', weak: 'Authentication', svc: 'Usage' };
1277    const TIMES = ['09:41', '10:03', '10:17', '10:52', '11:26', '11:40'];
1278
1279    /* ---- dash (the previous default, before the brief; after the user's reference picture): four numbers, a 30-day exposure heatmap
1280       (applications × days, colour = how many endpoints have the vulnerable version, a t
1280ick on the day it is
1281       fixed everywhere, status at the end of each row), a live identity table, and four capability cards.
1282       The numbers are an example organisation, said so; no "vs last week" arrows (rising risk read as bad news). */
1283    const place = function (el) {
1284      const after = document.getElementById('shx') || document.getElementById('pf') || hero;
1285      after.parentNode.insertBefore(el, after.nextSibling);
1286      [].forEach.call(document.querySelectorAll('section.sec h2'), function (h) { if (/^Predict Attacks Before/.test(h.textContent)) h.closest('section').classList.add('pdx-old'); });
1287      const tn = function () {
1288        const heroDark = root.getAttribute('data-tone') !== 'light';
1289        el.classList.toggle('lt', document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark);
1290      };
1291      tn();
1292      new MutationObserver(tn).observe(root, { attributes: true, attributeFilter: ['data-tone'] });
1293    };
1294    /* ---- brief (default, 25 Sep 2026, after the user's picture): the homepage keeps the gist and the full dashboard
1295       (heatmap, identity table) lives on the platform page, where "See it in ThreatResponder" leads. Copy and a CTA on
1296       the left, a tilted summary card on the right (the official logo, four numbers of an example organisation,
1297       labelled sample data, and how many are already fixed), four capability cards below. ?predict=dash brings back
1298       the previous dashboard (only while the page is not SETTLED). */
1299    if (!/^(dash|window)$/.test(root.getAttribute('data-predict') || '')) {
1300      const BI = {
1301        warn: '<path d="M12 4l9 16H3z"/><path d="M12 10v4M12 17h.01"/>',
1302        layers: '<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 13l9 5 9-5"/>',
1303        user: IC.user,
1304        shield: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/>',
1305        bolt: '<path d="M13 3L5 13h6l-1 8 8-10h-6z"/>',
1306        aim: '<circle cx="12" cy="12" r="7.5"/><circle cx="12" cy="12" r="3.5"/><path d="M12 2.5v3M21.5 12h-3"/>',
1307        team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>',
1308        lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
1309        chev: '<path d="M9 6l6 6-6 6"/>',
1310        arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>'
1311      };
1312      const BK = [['warn', 'red', 127, 'Exposures detected'], ['layers', 'navy', 34, 'Known exploited (KEV)'],
1313        ['user', 'peri', 289, 'Identities watched'], ['shield', 'blue', 12, 'Critical risks']];
1314      const BC = [['bolt', 'red', 'Real-time exposures', 'Open on install, closed on update. No scans.'],
1315        ['aim', 'navy', 'Known exploited first', 'CISA KEV and BOD 22-01 at the top.'],
1316        ['team', 'peri', 'Identity behavior', 'How each account is used, and where.'],
1317        ['lock', 'blue', 'Weak authentication', 'Legacy protocols and brute force, flagged.']];
1318      const s3 = document.createElement('section');
1319      s3.className = 'pdx pdb' + (still ? ' still' : ''); s3.id = 'pdx';
1320      s3.innerHTML = '<div class="pdb-deco" aria-hidden="true"><svg class="pdb-sh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width=".9" stroke-linejoin="round">' + BI.shield + '</svg><i class="pdb-dots a"></i><i class="pdb-dots b"></i></div>' +
1321        '<div class="pdb-in"><div class="pdb-top">' +
1322        '<div class="pdb-copy"><p class="pdx-kick">Predict</p>' +
1323        '<h2>Predict Attacks <span>Before they Occur</span></h2>' +
1324        '<p class="pdb-lede">Spot vulnerable software and risky identities across your environment, and act before attackers can use them against you.</p>' +
1325        '<a class="pdb-cta" href="/threatresponder-platform/#predict">See it in ThreatResponder' + svg(BI.arrow) + '</a>' +
1326        '<p class="pdb-tags"><span>Real time</span><span>No scans</span><span>One lightweight agent</span></p></div>' +
1327        '<div class="pdb-vis" role="img" aria-label="Example summary, sample data: 127 exposures detected, 34 known exploited, 289 identities watched, 12 critical risks; 94 of 127 exposures already fixed.">' +
1328        '<div class="pdb-card" aria-hidden="true"><div class="pdb-ch"><span class="pdb-logo" data-logo="threatresponder"></span><span class="pdb-sample">Sample data</span></div>' +
1329        '<ul class="pdb-kp">' + BK.map(function (k, i) {
1330          return '<li style="--r:' + i + '"><span class="pdb-ki t-' + k[1] + '">' + svg(BI[k[0]]) + '</span><span class="pdb-kt"><b data-n="' + k[2] + '">' + (still ? k[2] : 0) + '</b><span>' + k[3] + '</span></span>' + svg(BI.chev) + '</li>';
1331        }).join('') + '</ul></div>' +
1332        '<div class="pdb-float" aria-hidden="true"><span class="pdb-fi">' + svg(IC.check) + '</span><span class="pdb-ft"><b>94 of 127 fixed</b><span class="pdb-bar"><i></i></span></span></div></div>' +
1333        '</div><ul class="pdb-cards">' + BC.map(function (c) {
1334          return '<li><span class="pdb-ci t-' + c[1] + '">' + svg(BI[c[0]]) + '</span><span><b>' + c[2] + '</b><small>' + c[3] + '</small></span></li>';
1335        }).join('') + '</ul></div>';
1336      place(s3);
1337      if (window.Logos) window.Logos.paint(s3);
1338      /* once in view: the card settles in, its rows follow, the numbers count up, then the fixed badge */
1339      if (still) { s3.classList.add('go'); return; }
1340      const io3 = new IntersectionObserver(function (en) {
1341        if (!en[0].isIntersecting) return;
1342        io3.disconnect(); s3.classList.add('go');
1343        s3.querySelectorAll('.pdb-kp b').forEach(function (b, i) {
1344          const n = +b.dataset.n;
1345          setTimeout(function () {
1346            const t0 = performance.now();
1347            (function f(now) { const u = Math.min(1, (now - t0) / 1200); b.textContent = Math.round(n * (1 - Math.pow(1 - u, 3))); if (u < 1) requestAnimationFrame(f); })(t0);
1348          }, 350 + i * 120);
1349        });
1350      }, { threshold: .3 });
1351      io3.observe(s3.querySelector('.pdb-vis'));
1352      return;
1353    }
1354    if (root.getAttribute('data-predict') !== 'window') {
1355      const DI = {
1356        warn: '<path d="M12 4l9 16H3z"/>
1356<path d="M12 10v4M12 17h.01"/>',
1357        layers: '<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 13l9 5 9-5"/>',
1358        user: IC.user,
1359        shield: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/>',
1360        bolt: '<path d="M13 3L5 13h6l-1 8 8-10h-6z"/>',
1361        aim: '<circle cx="12" cy="12" r="7.5"/><circle cx="12" cy="12" r="3.5"/><path d="M12 2.5v3M21.5 12h-3"/>',
1362        team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>',
1363        lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>'
1364      };
1365      const KPI = [['warn', 127, 'Exposures detected', '94 already fixed'], ['layers', 34, 'Known exploited (KEV)', 'fixed first'],
1366        ['user', 289, 'Identities watched', 'on 1,240 endpoints'], ['shield', 12, 'Critical risks', '3 need a decision']];
1367      /* 30 days, today last; per application: endpoints with the vulnerable version by day (0 = none) */
1368      const D0 = new Date(2026, 7, 26), DAYN = 30;
1369      const day = function (i) { const d = new Date(D0); d.setDate(d.getDate() + i); return d; };
1370      const fmt = function (d) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); };
1371      const HM = [
1372        { app: 'Google Chrome', cve: 'CVE-2024-4671', kev: true, m: 'G', d: { 3: 3, 4: 9, 5: 24, 6: 31, 7: 18, 8: 6, 9: 2 }, fix: 10 },
1373        { app: '7-Zip', cve: 'CVE-2025-0411', kev: true, m: '7z', d: { 8: 2, 9: 4, 10: 7, 11: 12, 12: 16, 13: 22, 14: 26, 15: 25, 16: 19, 17: 11, 18: 6, 19: 3, 20: 1 }, fix: 21 },
1374        { app: 'OpenSSL', cve: 'CVE-2024-5535', kev: false, m: 'SSL', d: { 12: 3, 13: 3, 14: 4, 15: 4, 16: 5, 17: 5, 18: 4, 19: 3, 20: 2, 21: 2, 22: 1 }, fix: 23 },
1375        { app: 'Apache Tomcat', cve: 'CVE-2024-50379', kev: false, m: 'Tc', d: { 15: 4, 16: 8, 17: 9, 18: 9, 19: 5, 20: 2 }, fix: 21 },
1376        { app: 'Microsoft Windows', cve: 'CVE-2024-21338', kev: true, m: 'W', d: { 21: 6, 22: 14, 23: 27, 24: 38, 25: 33, 26: 21, 27: 12, 28: 8, 29: 5 }, fix: null }];
1377      const lvl = function (n) { return !n ? 0 : n <= 5 ? 1 : n <= 20 ? 2 : 3; };
1378      const IDT = [
1379        ['09:41', 'New local administrator', '<code>jdoe</code> on <code>WKSTN-004</code>', 'priv', 'Privilege'],
1380        ['11:26', 'Service account used interactively', '<code>svc-backup</code> on <code>FILE-02</code>', 'svc', 'Unusual use'],
1381        ['13:03', 'Multiple failed logons', '<code>m.smith</code> from <code>203.0.113.24</code>', 'weak', 'Brute force'],
1382        ['15:17', 'Sign-in from a new country', '<code>c.wilson</code> from Germany', 'beh', 'Unusual location'],
1383        ['17:02', 'Weak authentication method', '<code>legacy-app</code> using NTLMv1', 'new', 'Weak auth'],
1384        ['17:40', 'Added to a privileged group', '<code>jmiller</code> joined Backup Operators', 'priv', 'Privilege'],
1385        ['18:05', 'Domain admin on a workstation', '<code>da-lopez</code> on <code>WS-FIN-07</code>', 'priv', 'Exposure']];
1386      const CARDS = [['bolt', 'Real-time exposure visibility', 'A vulnerability appears when the software is installed and closes when it is fixed. No scans.'],
1387        ['aim', 'Known exploited, prioritized', 'CISA KEV and BOD 22-01, straight to the top.'],
1388        ['team', 'Identity behavior context', 'How each account is used, where it signs in, and when that changes.'],
1389        ['lock', 'Weak authentication detection', 'Legacy protocols, brute force, and service accounts used by people.']];
1390      const irow = function (r) {
1391        return '<div class="pdd-rw"><div class="pdd-ir"><span class="pdd-t">' + r[0] + '</span><span class="pdd-e"><b>' + r[1] + '</b><span>' + r[2] + '</span></span><span class="pdd-k k-' + r[3] + '">' + r[4] + '</span></div></div>';
1392      };
1393      const s2 = document.createElement('section');
1394      s2.className = 'pdx pdd' + (still ? ' still' : ''); s2.id = 'pdx';
1395      s2.innerHTML = '<div class="pdd-in">' +
1396        '<div class="pdx-head"><p class="pdx-kick">Predict</p><h2>Predict Attacks Before they Occur</h2>' +
1397        '<p>Spot vulnerable software and risky identities across your environment before attackers use them against you.</p></div>' +
1398        '<div class="pdd-kpis">' + KPI.map(function (k, i) {
1399          return '<div class="pdd-kpi"><span class="pdd-ki' + (i ? '' : ' r') + '">' + svg(DI[k[0]]) + '</span><div><b data-n="' + k[1] + '">' + (still ? k[1] : 0) + '</b><span>' + k[2] + '</span><small>' + k[3] + '</small></div></div>';
1400        }).join('') + '</div>' +
1401        '<div class="pdd-grid">' +
1402        '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.bug) + '</span><div><h3>Vulnerabilities, the moment they appear</h3><p>When vulnerable software is introduced, how far it spreads, and when it is fixed.</p></div><span class="pdd-when">Last 30 days</span></div>' +
1403        '<div class="pdd-hm" aria-hidden="true" style="--n:' + DAYN + '"><div class="pdd-hh"><span>Application / CVE</span><span class="pdd-ax">' +
1404        [0, 4, 8, 12, 16, 20].map(function (i) { return '<i' + (i % 8 ? ' class="o"' : '') + ' style="--i:' + i + '">' + fmt(day(i)) + '</i>'; }).join('') + '<i class="pdd-today" style="--i:' + (DAYN - 1) + '">Today</i></span><span>Status</span></div>' +
1405        HM.map(function (h) {
1406          let cells = '';
1407          for (let i = 0; i < DAYN; i++) {
1408            const n = h.d[i] || 0, fx = h.fix === i;
1409            cells += '<i class="c' + lvl(n) + (fx ? ' fx' : '') + '" style="--i:' + i + '"' + (n ? ' title="' + n + ' endpoints"' : '') + '>
1409' + (fx ? svg(IC.check) : '') + '</i>';
1410          }
1411          const peak = Math.max.apply(null, Object.keys(h.d).map(function (k) { return h.d[k]; }));
1412          const first = Math.min.apply(null, Object.keys(h.d).map(Number));
1413          const st = h.fix !== null ? '<span class="pdd-st ok">Fixed · ' + (h.fix - first) + ' d</span>' : '<span class="pdd-st open">Open · ' + h.d[DAYN - 1] + ' endpoints</span>';
1414          return '<div class="pdd-r"><span class="pdd-app"><span class="pdd-m">' + h.m + '</span><span><b>' + h.app + (h.kev ? '<em>KEV</em>' : '') + '</b><small>' + h.cve + ' · peak ' + peak + '</small></span></span>' +
1415            '<span class="pdd-cells">' + cells + '</span>' + st + '</div>';
1416        }).join('') +
1417        '<div class="pdd-lg"><span><i class="c0"></i>Not present</span><span><i class="c1"></i>1–5 endpoints</span><span><i class="c2"></i>6–20</span><span><i class="c3"></i>21+</span><span><i class="fx">' + svg(IC.check) + '</i>Fixed everywhere</span></div></div></article>' +
1418        '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.user) + '</span><div><h3>Identities, seen from every endpoint</h3><p>How accounts are used, where they sign in, and when risky behavior appears.</p></div></div>' +
1419        '<div class="pdd-it" aria-hidden="true"><div class="pdd-ih"><span>Time</span><span>Identity / event</span><span class="pdx-live"><i></i>Live</span></div><div class="pdd-rows">' +
1420        IDT.slice(0, 5).reverse().map(irow).join('') + '</div></div></article></div>' +
1421        '<ul class="pdd-cards">' + CARDS.map(function (c) { return '<li><span>' + svg(DI[c[0]]) + '</span><div><b>' + c[1] + '</b><small>' + c[2] + '</small></div></li>'; }).join('') + '</ul></div>';
1422      place(s2);
1423
1424      /* reveal once in view: numbers count up, the heatmap fills day by day up to Today, then the status;
1425         the identity table takes a new event every 3 s (five rows, fixed height) */
1426      const rows2 = s2.querySelector('.pdd-rows');
1427      const fix2 = function () { if (rows2.children.length !== 5 || rows2.querySelector('.shut')) return; rows2.style.height = ''; rows2.style.height = rows2.offsetHeight + 'px'; };
1428      fix2();
1429      window.addEventListener('resize', function () { rows2.style.height = ''; setTimeout(fix2, 700); });
1430      document.addEventListener('site:ready', fix2);
1431      if (document.fonts) document.fonts.ready.then(fix2);
1432      if (still) { s2.classList.add('go'); return; }
1433      let k2 = 5, t2 = 0, seen2 = false, in2 = false;
1434      function next2() {
1435        clearTimeout(t2); if (!in2) return;
1436        rows2.insertAdjacentHTML('afterbegin', irow(IDT[k2 % IDT.length]).replace('pdd-rw', 'pdd-rw shut fresh')); k2++;
1437        const r = rows2.firstElementChild; void r.offsetHeight; r.classList.remove('shut');
1438        setTimeout(function () { r.classList.remove('fresh'); }, 1800);
1439        const old = rows2.children[rows2.children.length - 1]; old.classList.add('shut'); setTimeout(function () { old.remove(); }, 650);
1440        t2 = setTimeout(next2, 3000);
1441      }
1442      new IntersectionObserver(function (en) {
1443        const was = in2; in2 = en[0].isIntersecting && en[0].intersectionRatio > .25;
1444        if (in2 && !seen2) {
1445          seen2 = true; s2.classList.add('go');
1446          s2.querySelectorAll('.pdd-kpi b').forEach(function (b) {
1447            const n = +b.dataset.n, t0 = performance.now();
1448            (function f(now) { const u = Math.min(1, (now - t0) / 1400); b.textContent = Math.round(n * (1 - Math.pow(1 - u, 3))); if (u < 1) requestAnimationFrame(f); })(t0);
1449          });
1450        }
1451        if (in2 && !was) t2 = setTimeout(next2, 2600);
1452        if (!in2) clearTimeout(t2);
1453      }, { threshold: [0, .25, .5] }).observe(s2.querySelector('.pdd-grid'));
1454      return;
1455    }
1456
1457    const sec = document.createElement('section');
1458    sec.className = 'pdx' + (still ? ' still' : ''); sec.id = 'pdx';
1459    sec.innerHTML = '<div class="pdx-in">' +
1460      '<div class="pdx-head"><p class="pdx-kick">Predict</p><h2>Predict Attacks Before they Occur</h2>' +
1461      '<p>See what an attacker could use before they use it: every vulnerable application and every risky account, on every endpoint, as it changes.</p></div>' +
1462      '<div class="pdx-grid">' +
1463      /* exposure */
1464      '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.bug) + '</span><div><h3>Vulnerabilities, the moment they appear</h3><p>No scans. A CVE opens when vulnerable software is installed and closes when it is updated.</p></div></div>' +
1465      '<div class="pdx-wk" aria-hidden="true"><div class="pdx-wk-top"><span class="pdx-host"><code>WS-AISHA204</code></span><span class="pdx-days">' +
1466      DAYS.map(function (d, i) { return '<i style="left:' + at(i, 0) + '%">' + d + '</i>'; }).join('') + '</span></div>' +
1467      '<div class="pdx-lane pdx-tr"><span class="pdx-ln">ThreatResponder</span><div class="pdx-track">' +
1468      EXP.map(function (e, i) {
1469        const a = +at(e.a[0], e.a[1], e.a[2]), b = e.b ? +at(e.b[0], e.b[1], e.b[2]) : 100;
1470        return '<div class="pdx-x' + (e.b ? '' : ' open') + '" data-a="' + a + '" data-b="' + b + '" style="--a:' + a + '%;--b:' + b + '%;--r:' + i + '">' +
1471          '<span class="pdx-bar"></span><span class="pdx-tag"><b>' + e.cve + '</b>' + (e.kev ? '<em>KEV</em>' : '') + '<small>' + e.app + '</small></span>' +
1472          '<span class="pdx-end">' + (e.b ? svg(IC.check) + 'Closed · ' + e.dur : 'Still open') + '</span></div>';
1473      }).join('') + '</div></div>' +
1474      '<div class="pdx-lane pdx-sc"><span class="pdx-ln">Weekly scan</span><div class="pdx-track">' +
1475      '<span class="pdx-scan" style="left:' + at(0, 2) + '%" data-a="' + at(0, 2) + '"><i></i>0 found</span>' +
1476      '<span class="pdx-scan" style="left:' + at(7, 2) + '%" data-a="' + at(7, 2) + '"><i></i>1 found</span></div></div>' +
1477      '<div class="pdx-cur"></div></div>' +
1478      '<p class="pdx-note" aria-hidden="true"><span class="pdx-note-a">Every window on record: opened, closed, and how.</span><span class="pdx-note-b">A weekly scan saw 1 of 3, four days late.</span></p>' +
1479      '<ul class="pdx-f"><li><b>Real-time CVE detection</b><span>Opened and closed the moment software changes.</span></li>' +
1480      '<li><b>Known-exploited first</b><span>CISA KEV and BOD 22-01, straight to the top.</span></li>' +
1481      '<li><b>Remediation tracking</b><span>
1481Every exposure window kept as evidence.</span></li></ul></article>' +
1482      /* identity */
1483      '<article class="pdx-p"><div class="pdx-ph"><span class="pdx-ic">' + svg(IC.user) + '</span><div><h3>Identities, seen from every endpoint</h3><p>Not only at the domain controller: the agent sees every account where it is used.</p></div></div>' +
1484      '<div class="pdx-id" aria-hidden="true"><div class="pdx-id-h"><span>Identity activity</span><span class="pdx-live"><i></i>Live</span></div><div class="pdx-rows"></div></div>' +
1485      '<ul class="pdx-f"><li><b>Identity creation &amp; usage</b><span>New accounts, privilege changes and where each account logs on.</span></li>' +
1486      '<li><b>User behavior</b><span>Unusual logons, hours and sessions, per user.</span></li>' +
1487      '<li><b>Risky authentication</b><span>Weak protocols and service accounts used by people.</span></li></ul></article>' +
1488      '</div></div>';
1489    const after = document.getElementById('shx') || document.getElementById('pf') || hero;
1490    after.parentNode.insertBefore(sec, after.nextSibling);
1491    /* the live site's own Predict section is replaced by this one */
1492    [].forEach.call(document.querySelectorAll('section.sec h2'), function (h) { if (/^Predict Attacks Before/.test(h.textContent)) h.closest('section').classList.add('pdx-old'); });
1493
1494    /* ---- tone: a new chapter, so the opposite of the Screens (or of whatever comes before) ---- */
1495    function tone() {
1496      const heroDark = root.getAttribute('data-tone') !== 'light';
1497      const prevDark = document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark;
1498      sec.classList.toggle('lt', prevDark);
1499    }
1500    tone();
1501    new MutationObserver(tone).observe(root, { attributes: true, attributeFilter: ['data-tone'] });
1502
1503    /* ---- identity feed: newest on top, four rows, fixed height ---- */
1504    const rows = sec.querySelector('.pdx-rows');
1505    let n = 0;
1506    function add(fresh) {
1507      const e = IDN[n % IDN.length], tm = TIMES[n % TIMES.length]; n++;
1508      rows.insertAdjacentHTML('afterbegin', '<div class="pdx-rw' + (fresh ? ' shut fresh' : '') + '"><div class="pdx-r k-' + e.k + '"><span class="pdx-t">' + tm + '</span>' +
1509        '<span class="pdx-w"><b>' + e.t + '</b><span>' + e.w + '</span></span><span class="pdx-k">' + KIND[e.k] + '</span></div></div>');
1510      const r = rows.firstElementChild;
1511      if (fresh) { void r.offsetHeight; r.classList.remove('shut'); setTimeout(function () { r.classList.remove('fresh'); }, 1800); }
1512      const all = rows.children;
1513      if (all.length > 4) { const old = all[all.length - 1]; if (fresh) { old.classList.add('shut'); setTimeout(function () { old.remove(); }, 650); } else old.remove(); }
1514    }
1515    for (let i = 0; i < 4; i++) add(false);
1516    function fixH() { if (rows.children.length !== 4 || rows.querySelector('.shut')) return; rows.style.height = ''; rows.style.height = rows.offsetHeight + 'px'; }
1517    fixH();
1518    window.addEventListener('resize', function () { rows.style.height = ''; setTimeout(fixH, 700); });
1519    document.addEventListener('site:ready', fixH);
1520    if (document.fonts) document.fonts.ready.then(fixH);
1521
1522    /* ---- the week: a cursor sweeps it; windows open and close as it passes; the scans fire at their hour ---- */
1523    const wk = sec.querySelector('.pdx-wk'), xs = [].slice.call(sec.querySelectorAll('.pdx-x')), scans = [].slice.call(sec.querySelectorAll('.pdx-scan'));
1524    const note = sec.querySelector('.pdx-note');
1525    function paint(t) {
1526      wk.style.setProperty('--tn', (t / 100).toFixed(4));
1527      xs.forEach(function (x) {
1528        const a = +x.dataset.a, b = +x.dataset.b;
1529        x.style.setProperty('--w', Math.max(0, Math.min(t, b) - a) + '%');
1530        x.classList.toggle('on', t >= a); x.classList.toggle('done', t >= b && !x.classList.contains('open'));
1531      });
1532      scans.forEach(function (s) { s.classList.toggle('on', t >= +s.dataset.a); });
1533      note.classList.toggle('on', t >= 99.5);
1534    }
1535    if (still) { paint(100); return; }
1536    paint(0);
1537    let inView = false, t0 = 0, raf = 0, idTimer = 0;
1538    const SWEEP = 9000, HOLD = 4200;
1539    function frame(now) {
1540      if (!inView) { raf = 0; return; }
1541      if (!t0) t0 = now;
1542      const e = now - t0;
1543      if (e < SWEEP) paint(e / SWEEP * 100);
1544      else if (e < SWEEP + HOLD) paint(100);
1545      else { wk.classList.add('reset'); paint(0); t0 = now; setTimeout(function () { wk.classList.remove('reset'); }, 60); }
1546      raf = requestAnimationFrame(frame);
1547    }
1548    function idLoop() { clearTimeout(idTimer); if (!inView) return; add(true); idTimer = setTimeout(idLoop, 2600); }
1549    new IntersectionObserver(function (en) {
1550      const was = inView; inView = en[0].isIntersecting && en[0].intersectionRatio > .3;
1551      if (inView && !was) { t0 = 0; if (!raf) raf = requestAnimationFrame(frame); idTimer = setTimeout(idLoop, 1400); }
1552      if (!inView) clearTimeout(idTimer);
1553    }, { threshold: [0, .3, .5] }
1553).observe(sec.querySelector('.pdx-grid'));
1554  })();
1555
1556  /* ---- Neutralize Threats in Real Time (?engines=pair|flip|off): the live site's detection-engines section,
1557     straight after the Screens: the six engines of the agent (names and items as on netsecurity.com), then
1558     the four outcomes. Static (the user dropped the wired alert queue below the cards).
1559     Tone: pair = the same tone as the section before it (Screens and Engines read as one product chapter,
1560     on a different surface: the corridor picture), flip = strict alternation. The section after it always
1561     takes the other tone. Replaces the Yardstick section, the proof strip and the old Neutralize section. */
1562  (function () {
1563    const root = document.documentElement;
1564    const hero = document.getElementById('hero');
1565    if (!hero) return;
1566    const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; };
1567    const IC = {
1568      intel: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.8 3.3 2.8 13.7 0 17M12 3.5c-2.8 3.3-2.8 13.7 0 17"/>',
1569      files: '<path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5"/><path d="M10.5 12.5l-2 2 2 2M14 12.5l2 2-2 2"/>',
1570      behav: '<rect x="4" y="5" width="16" height="11" rx="1.5"/><path d="M2 19.5h20"/><path d="M7 11h2.5l1.5-3 2 5 1.5-2H17"/>',
1571      net: '<circle cx="12" cy="5" r="2.2"/><circle cx="5" cy="18.5" r="2.2"/><circle cx="19" cy="18.5" r="2.2"/><circle cx="12" cy="12.5" r="2.2"/><path d="M12 7.2v3.1M10.2 13.9l-3.4 2.8M13.8 13.9l3.4 2.8"/>',
1572      exploit: '<path d="M12 3l7 3v6c0 4.2-3 7.7-7 9-4-1.3-7-4.8-7-9V6z"/><path d="M12 8v5M12 16.2h.01"/>',
1573      rules: '<path d="M4 7h9M17 7h3M4 12h5M13 12h7M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="11" cy="12" r="2"/><circle cx="9" cy="17" r="2"/>',
1574      clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7v5l3 2"/>',
1575      aim: '<circle cx="12" cy="12" r="6.5"/><path d="M12 2.5v4M12 17.5v4M2.5 12h4M17.5 12h4"/><circle cx="12" cy="12" r="1.4"/>',
1576      team: '<circle cx="9" cy="8" r="3.3"/><path d="M2.8 20c1-3.5 3.5-5.3 6.2-5.3s5.2 1.8 6.2 5.3"/><circle cx="17" cy="9" r="2.6"/><path d="M16.2 14.5c2.5-.2 4.4 1.3 5.1 4.3"/>',
1577      rise: '<path d="M4 20h16M7 20v-4M11.5 20v-7M16 20V10"/><path d="M5 11.5l5-4 3 2 5.5-5M15 4.5h3.5V8"/>',
1578      arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>'
1579    };
1580    /* the six engines, from the live site (TRMC, the internal name, reads "the agent" here). Compact (25 Sep 2026): the homepage
1581       shows each engine's name and one line; the lists (f) are shown on the platform page (#engines), linked below the cards.
1582       The sixth is "Custom Detection Rules" (user): Sigma, YARA and regex rules are added or uploaded in the console. */
1583    const ENG = [
1584      { ic: 'intel', t: 'Threat Intelligence &amp; Reputation', d: 'Known indicators, reputation and cloud intelligence.',
1585        f: [['File Reputation &amp; Cloud Lookup', 'Known malicious file intelligence.'], ['Malicious IP Indicators', 'Curated IPv4 and IPv6 intelligence.'],
1586          ['Malicious Domain Indicators', 'Malicious and phishing-domain intelligence.'], ['Newly Registered Domains', 'Adds risk context for newly registered domains.']] },
1587      { ic: 'files', t: 'File, Script &amp; Document Analysis', d: 'Inspect file content, scripts and documents.',
1588        f: [['Static File Machine Learning', 'Unknown and zero-day file analysis.'], ['PowerShell Command &amp; Context Analysis', 'Command analysis, context, relationships, and patterns.'],
1589          ['Malicious Script Analysis', 'Malicious VBScript, JScript, WSH and in-memory script content.'], ['Malicious Document Analysis', 'Office, PDF, macro, and embedded content analysis.'],
1590          ['Script Content Inspection', 'Script content via Windows AMSI or the agent’s own scanning.']] },
1591      { ic: 'behav', t: 'Endpoint Behavior &amp; System Integrity', d: 'Monitor behavior, persistence and privilege activity.',
1592        f: [['Persistence &amp; Autorun Monitoring', 'Services, tasks, run keys, startup items, and WMI subscriptions.'], ['Credential &amp; Privilege Activity', 'Sensitive process, token, account, and privilege activity.'],
1593          ['Security Controls &amp; System Integrity', 'Attempts to weaken security controls, logging, telemetry, updates, or boot security.'],
1594          ['Agent Tamper Detection', 'Attempts to stop, modify, remove, or disable the agent.'], ['Enhanced Process Visibility', 'Detailed process activity for rules and specialized detections.']] },
1595      { ic: 'net', t: 'Network &amp; Remote Activity', d: 'C2, remote access, lateral movement and exfiltration.',
1596        f: [['Network Behavior &amp; C2 Analytics', 'Beaconing, retries, reverse shells, and rare destinations.'], ['Remote Access &amp; Lateral Movement', 'RDP, SSH, SMB, WinRM, sessions, and remote services.'],
1597          ['Collection &amp; Exfiltration Signals', 'Transfer, staging, USB, and destination correlation.'], ['QUIC &amp; HTTP/3 Traffic Inspection', 'Domains in QUIC and HTTP/3 connections, checked against malicious destinations.']] },
1598      { ic: 'exploit', t: 'Exploit &amp; Impact Protection', d: 'Block exploits, ransomware and memory threats.',
1599        f: [['Exploit Prevention', 'Application exploitation and exploit-chain interruption.'], ['Process Injection &amp; Memory Threats', 'Injection, hollowing, shellcode, and executable memory.'],
1600          ['Kernel &amp; Driver Abuse', 'Vulnerable driver and kernel-abuse signals.'], ['Anti-Ransomware', 'Canary files, rapid I/O behavior, and immediate containment.'],
1601          ['Destructive System Activity', 'Wipers, backup deletion, and destructive configuration changes.']] },
1602      { ic: 'rules', t: 'Custom Detection Rules', d: 'Add your own Sigma, YARA and regex rules beside the managed detections.',
1603        f: [['Managed Endpoint &amp; Cloud Detections', 'Assigned through endpoint group policy.'], ['Enable Detection Rules', 'Turn assigned rules on or off for each group.']] }];
1604    const BEN = [['clock', 'Real-time visibility', 'See threats as they happen.', 'red'], ['aim', 'High-fidelity alerts', 'Less noise. More clarity.', 'navy'],
1605      ['team', 'Faster response', 'Take action with confidence.', 'peri'], ['rise', 'A more secure tomorrow', 'Turn insight into impact.', 'blue']];
1606
1607    const sec = document.createElement('section');
1608    sec.className = 'ntx'; sec.id = 'ntx';
1609    sec.innerHTML = '<div class="ntx-in">' +
1610      '<div class="ntx-head"><p class="ntx-kick">
1610Detect<i></i>Prevent<i></i>Respond</p><span class="ntx-rule"></span><h2>Neutralize Threats <span>in Real Time</span></h2>' +
1611      '<p>Six detection engines on the agent inspect every file, script, process and connection as it happens. What reaches your team is one prioritized alert per attack.</p></div>' +
1612      '<div class="ntx-eng">' + ENG.map(function (e) {
1613        return '<article class="ntx-c"><span class="ntx-hex">' + svg(IC[e.ic]) + '</span><h3>' + e.t + '</h3><p class="ntx-cd">' + e.d + '</p></article>';
1614      }).join('') + '</div>' +
1615      '<p class="ntx-more"><a href="/threatresponder-platform/#engines">See every engine feature' + svg(IC.arrow) + '</a></p>' +
1616      '<ul class="ntx-ben">' + BEN.map(function (b) { return '<li><span class="t-' + b[3] + '">' + svg(IC[b[0]]) + '</span><b>' + b[1] + '</b><small>' + b[2] + '</small></li>'; }).join('') + '</ul></div>';
1617    const after = document.getElementById('pdx') || document.getElementById('shx') || document.getElementById('pf') || hero;
1618    after.parentNode.insertBefore(sec, after.nextSibling);
1619
1620    const nextSec = function (from) { let n = from.nextElementSibling; while (n && (n.tagName !== 'SECTION' || getComputedStyle(n).display === 'none')) n = n.nextElementSibling; return n; };
1621
1622    /* ---- tone: pair with the section before, or alternate; the next section takes the other tone ---- */
1623    function tone() {
1624      const heroDark = root.getAttribute('data-tone') !== 'light';
1625      const pd = document.getElementById('pdx');
1626      /* the section before is Predict when it is on (it takes the opposite tone of the Screens) */
1627      const prevDark = pd && getComputedStyle(pd).display !== 'none' ? !pd.classList.contains('lt') : document.getElementById('shx') ? heroDark : document.getElementById('pf') ? !heroDark : heroDark;
1628      const dark = root.getAttribute('data-engines') === 'flip' ? !prevDark : prevDark;
1629      sec.classList.toggle('lt', !dark);
1630      [].forEach.call(document.querySelectorAll('.ntx-dk'), function (e) { e.classList.remove('ntx-dk', 'on-dark'); });
1631      if (root.getAttribute('data-engines') === 'off') return;
1632      /* the two (still light) sections that follow keep alternating: with the Yardstick and the old Neutralize
1633         gone, two lavender sections would otherwise meet */
1634      const n1 = nextSec(sec), n2 = n1 && nextSec(n1), navy = dark ? n2 : n1;
1635      if (navy && navy.classList.contains('lav') && !navy.classList.contains('on-dark')) navy.classList.add('ntx-dk', 'on-dark');
1636    }
1637    tone();
1638    new MutationObserver(tone).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] });
1639  })();
1640
1641  /* One combined forensics chapter, following the supplied visual reference. */
1642  window.renderHomepageForensics();
1643
1644  /* Reference-led MSSP integrations workspace. */
1645  window.renderHomepageMssp();
1646
1647  /* ---- Why Organizations Choose ThreatResponder®, after MSSPs + Integrations. The live site's seven reasons and their
1648     text (netsecurity.com, 24 Sep 2026), but all visible: production hides each one behind a + and leaves the seventh
1649     alone on its row. Save Money leads with its four points (no proof strip: the user noted it is not about saving money
1650     and the hero already says since 2004); the other six sit beside it, 2 × 3.
1651     Replaces the legacy Pinpoint, Extend and Why sections. Opposite tone of the section before. */
1652  (function () {
1653    const root = document.documentElement;
1654    const hero = document.getElementById('hero');
1655    if (!hero) return;
1656    const svg = function (p) { return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>'; };
1657    const IC = {
1658      coin: '<circle cx="12" cy="12" r="9"/><path d="M15 9.5c-.6-1-1.7-1.5-3-1.5-1.7 0-3 .9-3 2.2 0 3 6 1.6 6 4.4 0 1.3-1.3 2.4-3 2.4-1.4 0-2.6-.6-3.2-1.6M12 6v2m0 8v2"/>',
1659      eye: '<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>',
1660      lock: '<rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
1661      bolt: '<path d="M13 2 4 14h7l-1 8 9-12h-7z"/>',
1662      doc: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M9 14l2 2 4-4"/>',
1663      chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
1664      star: '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1 6.2L12 17.3 6.5 20.2l1-6.2L3 9.6l6.2-.9z"/>',
1665      check: '<path d="m5 12 5 5 9-10"/>'
1666    };
1667    const SAVE = ['Prevent costly cyber attacks and data breaches', 'Eliminate ineffective technologies and gain high ROI', 'Reduce or eliminate the cost of investigations', 'Significantly reduce the cost of security operations'];
1668    const WHY = [['eye', 'Gain Situational Awareness and Quickly Make Informed Decisions', 'You gain threat visibility and pinpoint adversarial activities, so you can quickly make informed decisions to prioritize and mitigate enterprise risks.'],
1669      ['lock', 'Protect Your Intellectual Property', 'Avoid business and operational disruption, protect crucial assets and maintain a competitive and strategic advantage.'],
1670      ['bolt', 'Boost Efficiency and Productivity', 'Stop users from violating policy and enforce strict security policies at the endpoint, with granular controls that keep your security team and staff productive.'],
1671      ['doc', 'Stay Compliant and Avoid Fines', 'Not complying could mean steep fines and penalties. Protect your crucial assets and maintain regulatory compliance.', ['FISMA', 'PCI', 'GDPR', 'SOX', 'HIPAA']],
1672      ['chart', 'Improve Shareholders’ Value', 'ThreatResponder as your security arsenal eliminates unnecessary security operations costs, increasing your shareholders’ value.'],
1673      ['star', 'Preserve Your Reputation and Image', 'Stop a successful compromise before it becomes unwanted media coverage, and maintain your business reputation.']];
1674    const sec = document.createElement('section');
1675    sec.className = 'wyx'; sec.id = 'wyx';
1676    sec.innerHTML = '<div class="wyx-in"><div class="wyx-head"><p class="wyx-kick">Business outcomes</p><h2>Why Organizations Choose ThreatResponder®</h2></div><div class="wyx-grid">' +
1677      '<article class="wyx-save"><span class="wyx-ic">' + svg(IC.coin) + '</span><h3>Save Money</h3><ul>' + SAVE.map(function (s) { return '<li><i>' + svg(IC.check) + '</i>' + s + '</li>'; }).join('') + '</ul>' + '</article>' +
1678      WHY.map(function (w, i) {
1679        return '<article class="wyx-c" style="--i:' + i + '"><span class="wyx-ic">' + svg(IC[w[0]]) + '</span><div><h3>' + w[1] + '</h3><p>' + w[2] + '</p>' + (w[3] ? '<p class="wyx-reg">' + w[3].map(function (r) { return '<i>' + r + '</i>'; }).join('') + '</p>' : '') + '</div></article>';
1680      }).join('') + '</div></div>';
1681    const after = document.getElementById('igx') || document.getElementById('tbx') || document.getElementById('fsx') || hero;
1682    after.parentNode.insertBefore(sec, after.nextSibling);
1683
1684    /* ---- tone: the opposite of the section before ---- */
1685    const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; };
1686    const darkOf = function (p) {
1687      if (!p) return true;
1688      if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx)\b/.test(p.className)) return !p.classList.contains('lt');
1689      if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light';
1690      return p.classList.contains('on-dark');
1691    };
1692    function tone() {
1693      let p = sec.previousElementSibling;
1694      while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling;
1695      sec.classList.toggle('lt', darkOf(p));
1696    }
1697    tone();
1698    /* after the integrations section (20 ms) has re-toned for the same change */
1699    new MutationObserver(function () { setTimeout(tone, 30); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] });
1700  })();
1701  /* ---- Testimonials, right after Why choose. The live site's seven unique quotes, word for word (netsecurity.com,
1702     24 Sep 2026; production loops them in a one-at-a-time carousel). Only two are about ThreatResponder: they are the
1703     two large cards. The five from security services work (audits, penetration tests, an engagement) drift past below
1704     in a slow ribbon (in view only, pauses on hover; still under reduced motion), each labelled with its service.
1705     Replaces the legacy Trusted-by and Testimonials sections (the hero already carries the client logos). */
1706  (function () {
1707    const root = document.documentElement;
1708    const hero = document.getElementById('hero');
1709    if (!hero) return;
1710    const still = root.classList.contains('poster') || root.classList.contains('reduced');
1711    const PRODUCT = [['ThreatResponder is a very innovative and essential cybersecurity platform. By deploying the ThreatResponder Platform, our team can detect and prevent advanced cyber attacks that existing security products were not able to detect. As a result, we have eliminated ineffective technologies and now gain situational awareness of all our technology assets. With ThreatResponder, we are better positioned to prioritize and mitigate enterprise risks timely and cost-effectively.', 'Jim Brundage', 'VP Operations, CKA', 'JB'],
1712      ['NetSecurity’s ThreatResponder Platform provided comprehensive services in detecting, responding, and preventing serious and prevalent cyber attacks. It allows us to focus on our core business in delivering infrastru
1712cture and digital transformation services and building confidence with our clients.', 'Thuan Nguyen', 'CEO, Whonome, LTD', 'TN']];
1713    const SERVICES = [['Security audit', 'The security audit we received from NetSecurity was integral to the planning and design of our suite of web based service products… This is sensitive data and protected by law. NetSecurity helped us make our web products both secure and compliant with FERPA requirements.', 'Director of Services, Ellucian'],
1714      ['Penetration testing', 'Thank you for an outstanding job on the penetration testing! I am very impressed by your knowledge and professionalism. I look forward to working with you on future engagements.', 'Blake Turrentine, Founder & Chief CTO, HotWan'],
1715      ['Security audit', 'Excellent job on the security audit performed by NetSecurity. It provided an in-depth view of our network that offers search and registration services. NetSecurity was able to demonstrate their knowledge on latest network security issues.', 'Thuan Nguyen, Partner, Whonome, Limited'],
1716      ['Application security testing', 'Thank you very much for all the hard work, focus, expertise, and time you have put into testing the security of our web-based payroll application… All that we have learned so far will become part of our standard development procedures.', 'Lead Software Developer, a Major International Payroll Company'],
1717      ['Engagement support', 'NetSecurity’s ability to devote an immense effort, with little to no advanced notice, and deliver a work product that really shines is something that we have come to rely on here at our firm.', 'President & CEO, System Integrator']];
1718    const q = '<svg class="tmx-q" viewBox="0 0 40 32" aria-hidden="true"><path d="M0 32V19C0 8 6 1.5 16 0l1.5 4.5C11.5 6.5 9 10.5 9 15h7v17zm23 0V19c0-11 6-17.5 16-19l1.5 4.5C34.5 6.5 32 10.5 32 15h7v17z"/></svg>';
1719    const svc = function (s) { return '<figure class="tmx-s"><small>' + s[0] + '</small><blockquote>“' + s[1] + '”</blockquote><figcaption>' + s[2] + '</figcaption></figure>'; };
1720    const sec = document.createElement('section');
1721    sec.className = 'tmx' + (still ? ' still' : ''); sec.id = 'tmx';
1722    sec.innerHTML = '<div class="tmx-in"><div class="tmx-head"><p class="tmx-kick">Testimonials</p><h2>In Their Words</h2></div>' +
1723      '<div class="tmx-prod">' + PRODUCT.map(function (p) {
1724        return '<figure class="tmx-p">' + q + '<span class="tmx-tag">ThreatResponder® Platform</span><blockquote>' + p[0] + '</blockquote><figcaption><i>' + p[3] + '</i><span><b>' + p[1] + '</b><small>' + p[2] + '</small></span></figcaption></figure>';
1725      }).join('') + '</div>' +
1726      '<p class="tmx-sub">From our security services work</p><div class="tmx-rib"><div class="tmx-track">' + SERVICES.map(svc).join('') + SERVICES.map(svc).join('').replace(/<figure class="tmx-s">/g, '<figure class="tmx-s" aria-hidden="true">') + '</div></div></div>';
1727    const after = document.getElementById('wyx') || document.getElementById('igx') || document.getElementById('tbx') || hero;
1728    after.parentNode.insertBefore(sec, after.nextSibling);
1729
1730    /* ---- tone: the opposite of Why choose. The earlier sections' scripts run before these two sections exist and may
1731       have drawn Get started navy as their next section; that is undone here (Get started keeps its own lavender) ---- */
1732    const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; };
1733    const darkOf = function (p) {
1734      if (!p) return true;
1735      if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx)\b/.test(p.className)) return !p.classList.contains('lt');
1736      if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light';
1737      return p.classList.contains('on-dark');
1738    };
1739    function tone() {
1740      let p = sec.previousElementSibling;
1741      while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling;
1742      sec.classList.toggle('lt', darkOf(p));
1743      [].forEach.call(document.querySelectorAll('.wyx ~ .sec[class*="-dk"]'), function (e) { e.classList.remove('ntx-dk', 'fsx-dk', 'tbx-dk', 'igx-dk', 'on-dark'); });
1744    }
1745    tone();
1746    new MutationObserver(function () { setTimeout(tone, 40); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] });
1747
1748    /* the ribbon moves only while on screen */
1749    if (still) return;
1750    new IntersectionObserver(function (en) { sec.classList.toggle('run', en[0].isIntersecting); }, { threshold: 0 }).observe(sec.querySelector('.tmx-rib'));
1751  })();
1752  /* ---- Get started, About and the footer (static markup from outro.html.txt, same content as before). Get started takes
1753     the opposite tone of Testimonials and About the opposite of Get started; the footer stays the deepest navy. The
1754     Live demo / 15-day trial switch relabels the submit button. */
1755  (function () {
1756    const root = document.documentElement;
1757    const gs = document.getElementById('started'), ab = document.querySelector('.abx');
1758    if (!gs || !ab) return;
1759    const vis = function (el) { return el && getComputedStyle(el).display !== 'none'; };
1760    const darkOf = function (p) {
1761      if (!p) return true;
1762      if (/\b(ntx|pdx|fsx|tbx|igx|wyx|tmx|gsx|abx)\b/.test(p.className)) return !p.classList.contains('lt');
1763      if (p.id === 'shx') return root.getAttribute('data-tone') !== 'light';
1764      return p.classList.contains('on-dark');
1765    };
1766    const prev = function (el) { let p = el.previousElementSibling; while (p && (p.tagName !== 'SECTION' || !vis(p))) p = p.previousElementSibling; return p; };
1767    function tone() {
1768      gs.classList.toggle('lt', darkOf(prev(gs)));
1769      ab.classList.toggle('lt', darkOf(gs));
1770    }
1771    tone();
1772    /* after Testimonials (40 ms) has re-toned for the same change */
1773    new MutationObserver(function () { setTimeout(tone, 50); }).observe(root, { attributes: true, attributeFilter: ['data-tone', 'data-engines', 'data-predict'] });
1774
1775    const seg = [].slice.call(gs.querySelectorAll('.gsx-seg button')), go = gs.querySelector('.gsx-btn');
1776    seg.forEach(function (b) {
1777      b.addEventListener('click', function () {
1778        seg.forEach(function (x) { x.classList.toggle('on', x === b); x.setAttribute('aria-selected', x === b ? 'true' : 'false'); });
1779        go.textContent = b.getAttribute('data-cta');
1780      });
1781    });
1782  })();
1783  /* ---- Product tour: "Take the product tour" (hero) and "Product tour" (menus, footer) open the console itself in a
1784     window over the page: the console mockup (/mockup/) with its full header and side menu, on sample data. The
1785     tooltips are the product's own tour engine (driver.js, the ThreatResponder build in helpers/driver) configured as
1786     ui/src/services/featureTour.service.js does: one step at a time, Next / Back / Skip Tour, "Step n of N", arrow keys.
1787     Phase 1 covers the Dashboard only: every other menu item, header control and module is shown disabled (greyed out,
1788     a not-allowed cursor, no response), and anything else that would leave the Dashboard is announced as coming to the
1789     tour. #tour in the URL opens it on load. */
1790  (function () {
1791    const root = document.documentElement;
1792    const APP = '/mockup/', PAGE = 'dashboard.html';
1793    const theme = function () { return root.getAttribute('data-tone') === 'light' ? 'light' : 'dark'; };
1794    const x = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>';
1795    const re = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>';
1796    const box = document.createElement('div');
1797    box.className = 'ptx'; box.hidden = true;
1798    box.setAttribute('role', 'dialog'); box.setAttribute('aria-modal', 'true'); box.setAttribute('aria-label', 'ThreatResponder product tour');
1799    box.innerHTML = '<div class="ptx-win"><div class="ptx-bar"><i></i><i></i><i></i><span class="ptx-url">console.threatresponder.com / <em>dashboard</em></span>' +
1800      '<nav class="ptx-mods" aria-label="Tour modules"></nav><span class="ptx-sp"></span><span class="ptx-tag">Sample data</span>' +
1801      '<button type="button" class="ptx-re">' + re + 'Restart tour</button><a class="ptx-demo cta" href="#started">Book a free demo</a>' +
1802      '<button type="button" class="ptx-x" aria-label="Close the product tour">' + x + '</button></div>' +
1803      '<div class="ptx-stage"><iframe title="ThreatResponder console, product tour"></iframe><p class="ptx-load"><i></i>
1803Opening the console…</p></div>' +
1804      '<p class="ptx-toast" role="status" aria-live="polite"></p></div>';
1805    document.body.appendChild(box);
1806    const mods = box.querySelector('.ptx-mods');
1807    const stage = box.querySelector('.ptx-stage'), frame = box.querySelector('iframe'), toastEl = box.querySelector('.ptx-toast');
1808    let win = null, doc = null, drv = null, cur = 0, ready = false, wantTour = false, back = null, toastT = 0;
1809
1810    /* the console lays out at 1280 px at least; a narrower window scales it down */
1811    const MIN_W = 1280;
1812    function fit() {
1813      const w = stage.clientWidth, h = stage.clientHeight;
1814      if (!w) return;
1815      const k = Math.min(1, w / MIN_W);
1816      frame.style.width = (w / k) + 'px'; frame.style.height = (h / k) + 'px'; frame.style.transform = k < 1 ? 'scale(' + k.toFixed(4) + ')' : '';
1817    }
1818    if (window.ResizeObserver) new ResizeObserver(fit).observe(stage);
1819
1820    function toast(msg) {
1821      toastEl.innerHTML = msg; toastEl.classList.add('on');
1822      clearTimeout(toastT); toastT = setTimeout(function () { toastEl.classList.remove('on'); }, 3600);
1823    }
1824
1825    /* ---- the steps: what is on screen, in the product's words ---- */
1826    const panel = function (pane, title) {
1827      return function () {
1828        const h = [].slice.call(doc.querySelectorAll('#dbxPane-' + pane + ' .dbx-panel-header')).find(function (e) { return e.textContent.trim().indexOf(title) === 0; });
1829        return h ? h.closest('.dbx-panel') : null;
1830      };
1831    };
1832    const tab = function (name) {
1833      return function () {
1834        const b = [].slice.call(doc.querySelectorAll('.dbx-tab-btn')).find(function (e) { return e.textContent.trim() === name; });
1835        win.scrollTo(0, 0);
1836        if (b && !b.classList.contains('active')) { b.click(); return 700; }
1837        return 0;
1838      };
1839    };
1840    /* module by module, not a user guide (user, 24 Sep): each module is a chapter shown on its own page; its steps
1841       say what the module is for. Phase 1 has the Dashboard; Alerts, Live View, Forensics and Hunt follow as chapters. */
1842    const MODULES = [
1843      { name: 'Dashboard', page: 'dashboard.html', steps: [
1844        { before: tab('Overview'), page: true, title: 'Dashboard · Overview', text: 'The whole estate at a glance, for one company or all of them: security posture, threats, vulnerabilities, identity, endpoints and network, each with its trend and what needs action.' },
1845        { before: tab('Threats'), page: true, title: 'Dashboard · Threats', text: 'Detections over time and by engine, how fast they are resolved, and how many were auto-resolved with no analyst action needed.' },
1846        { before: tab('Endpoints'), page: true, title: 'Dashboard · Endpoints', text: 'Vital signs for every endpoint: the failing checks ranked by how many endpoints they hit, and the endpoints that need attention first.' },
1847        { before: tab('Network'), page: true, title: 'Dashboard · Network', text: 'Where data leaves the fleet: egress over time and by destination country, then risk signals such as single uploads large enough to look like exfiltration.' },
1848        { before: tab('ITDR'), page: true, title: 'Dashboard · Identity (ITDR)', text: 'Failed logons and lockouts per day, a risk score for every identity, and identity detections such as repeated failed logons across many accounts.' },
1849        { before: tab('Exposure'), page: true, title: 'Dashboard · Exposure', text: 'Vulnerabilities ranked by real risk: actively exploited first, then critical and high, with patching progress over the last 7 days.' }
1850      ] },
1851      { name: 'Alerts', soon: true }, { name: 'Live View', soon: true }, { name: 'Forensics', soon: true }, { name: 'Hunt', soon: true }
1852    ];
1853    const STEPS = [];
1854    mods.innerHTML = MODULES.map(function (m, k) { return '<button type="button"' + (m.soon ? ' class="soon" disabled title="Coming to the tour soon"' : '') + ' data-m="' + k + '">' + m.name + (m.soon ? '<small>soon</small>' : '') + '</button>'; }).join('');
1855    MODULES.forEach(function (m) { if (m.steps) m.steps.forEach(function (st, j) { st.mod = m; st.n = j; STEPS.push(st); }); });
1856
1857    /* ---- the runner (featureTour.service.js: a fresh driver per step) ---- */
1858    function center() {
1859      let v = doc.querySelector('.feature-tour-virtual-element');
1860      if (!v) { v = doc.createElement('div'); v.className = 'feature-tour-virtual-element'; doc.body.appendChild(v); }
1861      return v;
1862    }
1863    /* driver.js puts position: relative on the highlighted element, which unpins a fixed menu or header part: the
1864       element keeps its own position while it is highlighted */
1865    let pinned = null;
1866    function keep(el) {
1867      const p = win.getComputedStyle(el).position;
1868      if (p === 'fixed' || p === 'absolute' || p === 'sticky') { pinned = [el, el.style.position]; el.style.position = p; }
1869    }
1870    function stop() {
1871      if (drv) { try { drv.destroy(); } catch (e) { /* already gone */ } drv = null; }
1872      if (pinned) { pinned[0].style.position = pinned[1]; pinned = null; }
1873    }
1874    /* the welcome and closing cards sit in the middle of the console, without an arrow */
1875    function middle() {
1876      const p = doc.querySelector('.driver-popover');
1877      if (!p) return;
1878      p.style.left = Math.round((win.innerWidth - p.offsetWidth) / 2) + 'px';
1879      p.style.top = Math.round((win.innerHeight - p.offsetHeight) * .42) + 'px';
1880    }
1881    function show(i) {
1882      stop();
1883      if (!ready || i < 0 || i >= STEPS.length) return;
1884      cur = i;
1885      const s = STEPS[i], wait = s.before ? s.before() : 0;
1886      markMod(s.mod);
1887      setTimeout(function () {
1888        const el = s.center || s.page ? center() : typeof s.el === 'function' ? s.el() : doc.querySelector(s.el);
1889        const last = i === STEPS.length - 1;
1890        if (el) keep(el);
1891        drv = win.driver({
1892          animate: true, overlayColor: s.page ? 'rgba(0, 0, 0, 0)' : 'rgba(0, 0, 0, 0.7)', stagePadding: s.center || s.page ? 0 : 10, stageRadius: 8, allowClose: true,
1893          doneBtnText: last ? 'Finish Tour' : 'Next →', nextBtnText: 'Next →', prevBtnText: '← Back', skipBtnText: 'Skip Tour',
1894          showProgress: true, progressText: s.mod.name + ' · ' + (s.n + 1) + ' of ' + s.mod.steps.length, popoverClass: 'feature-tour-popover ptx-pop' + (s.center ? ' ptx-
1894pop--center' : '') + (s.page ? ' ptx-pop--dock' : ''),
1895          steps: [{ element: el || 'body', popover: { title: s.title, description: s.text, side: s.center ? 'bottom' : s.side, align: s.center ? 'center' : s.align || 'center' } }],
1896          onNextClick: function () { if (last) end(); else show(i + 1); },
1897          onPrevClick: function () { if (i > 0) show(i - 1); },
1898          onCloseClick: end, onSkipClick: end, onDestroyed: function () { drv = null; }
1899        });
1900        drv.drive ? drv.drive() : drv.highlight && drv.highlight();
1901        setTimeout(function () { const p = doc.querySelector('.driver-popover-prev-btn'); if (p) p.style.display = i === 0 ? 'none' : ''; if (s.center) middle(); }, 0);
1902      }, wait);
1903    }
1904    function markMod(m) { [].forEach.call(mods.children, function (b) { b.classList.toggle('on', b.textContent.indexOf(m.name) === 0); }); }
1905    function end() {
1906      stop();
1907      if (cur === STEPS.length - 1) toast('Next in the tour: <b>Alerts</b>, <b>Live View</b>, <b>Forensics</b> and <b>Hunt</b>. For now, explore the Dashboard.');
1908      const b = [].slice.call(doc.querySelectorAll('.dbx-tab-btn')).find(function (e) { return e.textContent.trim() === 'Overview'; });
1909      if (b && !b.classList.contains('active')) b.click();
1910    }
1911
1912    /* ---- the console: loaded once, on first open; only the Dashboard is part of the tour so far ---- */
1913    function label(a) { return (a.getAttribute('title') || a.textContent).trim().replace(/\s+/g, ' ').replace(/\s*\d+$/, '').slice(0, 40); }
1914    /* everything in the console's menus except the Dashboard; CSS selectors, so rows the side menu rebuilds stay disabled */
1915    const OFF = '#nvSide .nv-row:not([href="dashboard.html"]),#nvSide .nv-pin,#nvSide .nv-search,.page-header .top-menu .dropdown-toggle,.page-header .unified-logout-btn,.global-company-context,.unified-user-section,.ovc-btn';
1916    function soon(what) { toast('<b>' + (what || 'That page') + '</b> is coming to the tour soon. For now, explore the Dashboard.'); }
1917    function prep() {
1918      doc = frame.contentDocument; win = frame.contentWindow;
1919      doc.documentElement.setAttribute('data-theme', theme());
1920      const st = doc.createElement('style');
1921      /* the mockup's Network tab lets its uploads table spill past the page (2009 px wide at 1920): the table scrolls in its
1922         panel instead, and the page never scrolls sideways (driver.js centres targets horizontally too) */
1923      st.textContent = '#ovxDock,.ovx-dock,.nv-sec--lab{display:none!important}html{overflow-x:clip}#dbxPane-network .dbx-panel-body--flush{overflow-x:auto}' +
1924        '.feature-tour-virtual-element{position:fixed;top:36%;left:50%;width:1px;height:1px;opacity:0;pointer-events:none}' +
1925        '.driver-popover.feature-tour-popover{max-width:360px}.feature-tour-popover .driver-popover-title{font-size:17px;margin-bottom:10px}' +
1926        OFF.split(',').map(function (q) { return q + '{opacity:.3!important;filter:grayscale(1);cursor:not-allowed!important}'; }).join('') +
1927        OFF.split(',').map(function (q) { return q + ':hover{background:none!important}'; }).join('') +
1928        '.ptx-side-note{margin:10px 12px 6px;padding:9px 11px;border-radius:8px;background:rgba(47,111,237,.14);color:#9fb6ec;font-size:11.5px;line-height:1.45}.ptx-side-note b{color:#dbe6ff}[data-theme=light] .ptx-side-note{background:#e8efff;color:#3d5a99}[data-theme=light] .ptx-side-note b{color:#1c3f8f}' +
1929        '.feature-tour-popover .driver-popover-description{font-size:14px;line-height:1.7}.ptx-pop .driver-popover-arrow{display:none}.driver-popover.ptx-pop--dock{left:auto!important;top:auto!important;right:28px!important;bottom:28px!important}.ptx-pop--dock{box-shadow:0 0 0 1px rgba(110,160,255,.45),0 24px 60px rgba(0,0,0,.45)!important}';
1930      doc.head.appendChild(st);
1931      const side = doc.getElementById('nvSide'), note = doc.createElement('p');
1932      note.className = 'ptx-side-note'; note.innerHTML = '<b>Product tour:</b> the Dashboard. More modules are coming soon.';
1933      if (side) side.insertBefore(note, side.firstChild);
1934      const css = doc.createElement('link'); css.rel = 'stylesheet'; css.href = 'helpers/driver/driver.min.css'; doc.head.appendChild(css);
1935      const js = doc.createElement('script'); js.src = 'helpers/driver/driver.min.js';
1936      js.onload = function () { ready = true; box.classList.add('ok'); if (wantTour) { wantTour = false; show(0); } };
1937      doc.head.appendChild(js);
1938      /* links to other pages stay on the Dashboard and say what is coming */
1939      doc.addEventListener('click', function (e) {
1940        if (e.target.closest && e.target.closest(OFF)) { e.preventDefault(); e.stopPropagation(); return; }
1941        const a = e.target.closest && e.target.closest('a[href]');
1942        if (a) {
1943          const h = a.getAttribute('href');
1944          if (/\.html(\?|#|$)/.test(h) && h.indexOf(PAGE) !== 0) { e.preventDefault(); e.stopPropagation(); soon(label(a)); }
1945          return;
1946        }
1947        if (e.target.closest && e.target.closest('.unified-logout-btn')) { e.preventDefault(); e.stopPropagation(); toast('This is a demo console: there is nothing to sign out of.'); }
1948      }, true);
1949      /* keys: this driver build ends a one-step tour on ArrowRight, so the tour's own keys are taken first */
1950      doc.addEventListener('keydown', keys, true);
1951      doc.addEventListener('focusin', function (e) { if (e.target.closest && e.target.closest(OFF)) e.target.blur(); });
1952      [].forEach.call(doc.querySelectorAll(OFF), function (el) { el.setAttribute('tabindex', '-1'); el.setAttribute('aria-disabled', 'true'); });
1953    }
1954    frame.addEventListener('load', function () {
1955      let d = null;
1956      try { d = frame.contentDocument; } catch (e) { return; }
1957      if (!d) return;
1958      /* anything that still navigated away (a script link) comes back to the Dashboard */
1959      if (d.URL.indexOf(PAGE) < 0) { ready = false; box.classList.remove('ok'); soon(''); frame.src = APP + PAGE + '?theme=' + theme(); return; }
1960      const t0 = Date.now();
1961      (function wait() {
1962        if (d.querySelector('.ovx-tiles') && d.querySelector('#nvSide')) { prep(); return; }
1963        if (Date.now() - t0 < 10000) setTimeout(wait, 80);
1964      })();
1965    });
1966
1967    function open(push) {
1968      if (!box.hidden) return;
1969      back = document.activeElement;
1970      box.hidden = false; root.classList.add('ptx-open');
1971      requestAnimationFrame(function () { box.classList.add('in'); fit(); });
1972      if (!frame.getAttribute('src')) { wantTour = true; frame.src = APP + PAGE + '?theme=' + theme(); }
1973      else {
1974        if (doc) doc.documentElement.setAttribute('data-theme', theme());
1975        if (ready) show(0); else wantTour = true;
1976      }
1977      if (push && location.hash !== '#tour') history.replaceState(null, '', location.pathname + location.search + '#tour');
1978      box.querySelector('.ptx-x').focus();
1979    }
1980    function close() {
1981      if (box.hidden) return;
1982      stop(); box.classList.remove('in'); root.classList.remove('ptx-open');
1983      setTimeout(function () { box.hidden = true; }, 220);
1984      if (location.hash === '#tour') history.replaceState(null, '', location.pathname + location.search);
1985      if (back && back.focus) back.focus({ preventScroll: true });
1986    }
1987    box.querySelector('.ptx-x').addEventListener('click', close);
1988    box.querySelector('.ptx-re').addEventListener('click', function () { if (ready) show(0); });
1989    mods.addEventListener('click', function (e) {
1990      const b = e.target.closest('button'); if (!b) return;
1991      const m = MODULES[+b.getAttribute('data-m')];
1992      if (m.soon) return;
1993      if (ready) show(STEPS.indexOf(m.steps[0]));
1994    });
1995    box.querySelector('.ptx-demo').addEventListener('click', function (e) {
1996      e.preventDefault(); close();
1997      const g = document.getElementById('started'); if (g) setTimeout(function () { g.scrollIntoView({ behavior: 'smooth' }); }, 240);
1998    });
1999    function keys(e) {
2000      if (box.hidden) return;
2001      const t = e.target, typing = t && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable);
2002      if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (drv) end(); else close(); return; }
2003      if (!drv || typing) return;
2004      if (e.key === 'ArrowRight' || (e.key === 'Enter' && !(t && t.closest && t.closest('button,a')))) { e.preventDefault(); e.stopPropagation(); if (cur === STEPS.length - 1) end(); else show(cur + 1); }
2005      else if (e.key === 'ArrowLeft') { e.preventDefault(); e.stopPropagation(); if (cur > 0) show(cur - 1); }
2006    }
2007    document.addEventListener('keydown', keys, true);
2008    /* every "product tour" link on the page, including the header menus built later */
2009    document.addEventListener('click', function (e) {
2010      const a = e.target.closest && e.target.closest('a');
2011      if (a && !box.contains(a) && /product tour/i.test(a.textContent)) { e.preventDefault(); open(true); }
2012    });
2013    if (location.hash === '#tour') setTimeout(function () { open(false); }, 300);
2014  })();
2015  document.addEventListener('site:ready', function () {
2016    /* the stack builds and the spine draws when they arrive */
2017    ['stack', 'spine'].forEach(function (id) { var el = document.getElementById(id); if (still) { el.classList.add('in'); return; } new IntersectionObserver(function (es, o) { if (es[0].isIntersecting) { el.classList.add('in'); o.disconnect(); } }, { threshold: .3 }).observe(el); });
2018    /* hero captions appear once the console has loaded */
2019    var ov = document.getElementById('hero-ov').__pui, caps = [document.getElementById('cap1'), document.getElementById('cap2')];
2020    function show() { caps.forEach(function (x, i) { setTimeout(function () { x.classList.add('on'); }, still ? 0 : i * 600); }); }
2021    if (still || ov.static) show(); else ov.onTick(function (p) { if (p >= 1) show(); });
2022    document.querySelectorAll('form.f .seg span').forEach(function (s, i, all) { s.addEventListener('click', function () { all.forEach(function (x) { x.classList.toggle('on', x === s); }); document.querySelector('form.f button').textContent = i ? 'Start the 15-day trial' : 'Book a free demo'; }); });
2023  });
2024})();

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.