PageSourceSearch

https://reactivezero.com/js/animations.js?v=20260915-phone

js reactivezero.com collected 2026-09-26 14:38:43 UTC 350,455 bytes, 7,337 lines download raw bytes

1(function () {
2  'use strict';
3
4  /* theme-aware ink for canvases drawn on the page background */
5  function rzInk(alpha) {
6    var dark = document.documentElement.getAttribute('data-theme') === 'dark';
7    return 'rgba(' + (dark ? '236,238,244' : '20,21,26') + ',' + alpha + ')';
8  }
9
10
11  /* ── Hi-DPI canvas helper: back the canvas with (rendered px × devicePixelRatio)
12     and scale the 2D transform so drawing code keeps its logical W×H coords. ── */
13  function rzFitCanvas(cv, g, W, H) {
14    function fit() {
15      var rect = cv.getBoundingClientRect();
16      var cssW = rect.width || W;
17      var scale = Math.min(4, Math.max(1, (cssW / W) * (window.devicePixelRatio || 1)));
18      var pw = Math.round(W * scale), ph = Math.round(H * scale);
19      if (cv.width !== pw || cv.height !== ph) { cv.width = pw; cv.height = ph; }
20      g.setTransform(scale, 0, 0, scale, 0, 0);
21    }
22    fit();
23    window.addEventListener('resize', fit);
24    window.addEventListener('load', fit);
25    document.addEventListener('rz-why-variant', fit);   /* re-fit when a hidden variant is shown */
26    document.addEventListener('rz-theme-change', fit);
27    return fit;
28  }
29  window.rzFitCanvas = rzFitCanvas;
30
31  /* ── 1. SCROLL REVEAL ──────────────────────────────────────── */
32  var revealObs = new IntersectionObserver(function (entries) {
33    entries.forEach(function (entry) {
34      if (entry.isIntersecting) {
35        entry.target.classList.add('is-visible');
36        revealObs.unobserve(entry.target);
37      }
38    });
39  }, { threshold: 0.10 });
40
41  document.querySelectorAll('.rz-reveal').forEach(function (el) {
42    revealObs.observe(el);
43  });
44
45  /* ── 2. TYPEWRITER TAGLINE ─────────────────────────────────── */
46  var tw = document.getElementById('rz-tw');
47  if (tw) {
48    var lines = [
49      '> Penetration Testing & Red Teaming',
50      '> Incident Response — 24/7 On-Call',
51      '> Zero-Day Threat Intelligence',
52      '> Cloud & OT Security Assessments'
53    ];
54    var li = 0, ci = 0, deleting = false, paused = false;
55
56    /* look the line up in the i18n string table for the active language */
57    function twLine(i) {
58      var lang = document.documentElement.lang || 'en';
59      if (lang === 'en') return lines[i];
60      var S = window.RZ_I18N_STRINGS || {};
61      return (S[lang] && S[lang][lines[i]]) || lines[i];
62    }
63
64    function tick() {
65      if (paused) return;
66      var line = twLine(li);
67      if (!deleting) {
68        ci++;
69        tw.textContent = line.slice(0, ci);
70        if (ci >= line.length) {
71          deleting = true;
72          paused = true;
73          setTimeout(function () { paused = false; tick(); }, 2200);
74          return;
75        }
76      } else {
77        ci--;
78        tw.textContent = line.slice(0, ci);
79        if (ci <= 0) {
80          deleting = false;
81          li = (li + 1) % lines.length;
82        }
83      }
84      setTimeout(tick, deleting ? 32 : 55);
85    }
86
87    setTimeout(tick, 900);
88  }
89
90  /* ── 3. PARTICLE NETWORK (hero background) ─────────────────── */
91  var canvas = document.getElementById('rz-particle-canvas');
92  if (canvas) {
93    var ctx = canvas.getContext('2d');
94    var pts  = [];
95    var COUNT    = 65;
96    var MAX_DIST = 130;
97    var RAF;
98    /* five equal accent colours for the node dots */
99    var ACC = ['#7d14d7', '#5801f5', '#f50034', '#85f403', '#e6e414'];
100
101    function resize() {
102      canvas.width  = canvas.offsetWidth  || window.innerWidth;
103      canvas.height = canvas.offsetHeight || window.innerHeight;
104    }
105
106    function spawn() {
107      pts = [];
108      for (var i = 0; i < COUNT; i++) {
109        var big = Math.random() < 0.15;
110        pts.push({
111          x:  Math.random() * canvas.width,
112          y:  Math.random() * canvas.height,
113          vx: (Math.random() - 0.5) * 0.42,
114          vy: (Math.random() - 0.5) * 0.30,
115          r:  big ? (Math.random() * 2 + 2.5) : (Math.random() * 1.5 + 1.2),
116          bright: big,
117          col: ACC[i % ACC.length]
118        });
119      }
120    }
121
122    function draw() {
123      ctx.clearRect(0, 0, canvas.width, canvas.height);
124
125      for (var i = 0; i < pts.length; i++) {
126        var p = pts[i];
127        p.x += p.vx;
128        p.y += p.vy;
129        if (p.x < 0 || p.x > canvas.width)  p.vx *= -1;
130        if (p.y < 0 || p.y > canvas.height)  p.vy *= -1;
131      }
132
133      for (var i = 0; i < pts.length; i++) {
134        for (var j = i + 1; j < pts.length; j++) {
135          var dx = pts[i].x - pts[j].x;
136          var dy = pts[i].y - pts[j].y;
137          var d  = Math.sqrt(dx * dx + dy * dy);
138          if (d < MAX_DIST) {
139            var a = (1 - d / MAX_DIST) * 0.55;
140            ctx.strokeStyle = rzInk(a * 0.5);
141            ctx.lineWidth   = 0.8;
142            ctx.beginPath();
143            ctx.moveTo(pts[i].x, pts[i].y);
144            ctx.lineTo(pts[j].x, pts[j].y);
145            ctx.stroke();
146          }
147        }
148      }
149
150      for (var i = 0; i < pts.length; i++) {
151        ctx.beginPath();
152        ctx.arc(pts[i].x, pts[i].y, pts[i].r, 0, Math.PI * 2);
153        ctx.fillStyle = pts[i].col;
154        ctx.globalAlpha = pts[i].bright ? 0.95 : 0.6;
155        ctx.fill();
156      }
157      ctx.globalAlpha = 1;
158
159      RAF = requestAnimationFrame(draw);
160    }
161
162    document.addEventListener('visibilitychange', function () {
163      if (document.hidden) { cancelAnimationFrame(RAF); }
164      else { RAF = requestAnimationFrame(draw); }
165    });
166
167    resize();
168    spawn();
169    draw();
170
171    window.addEventListener('resize', function () { resize(); spawn(); });
172  }
173
174  /* ── 4. THREAT RADAR (hero right side) ─────────────────────── */
175  var radarCanvas = document.getElementById('rz-radar-canvas');
176  if (radarCanvas) {
177    var RC  = radarCanvas.getContext('2d');
178    var RW  = 400, RH = 400, CX = 200, CY = 200, RAD = 182;
179    rzFitCanvas(radarCanvas, RC, RW, RH);
180
181    var threats = [
182      { a: 38,  d: 0.52, lbl: 'CVE-2024-4577',  r: 4.5, lit: 0 },
183      { a: 105, d: 0.74, lbl: 'PORT SCAN',       r: 3.5, lit: 0 },
184      { a: 172, d: 0.40, lbl: 'BRUTEFORCE',      r: 3.5, lit: 0 },
185      { a: 238, d: 0.65, lbl: 'C2 BEACON',       r: 4,   lit: 0 },
186      { a: 295, d: 0.82, lbl: 'DATA EXFIL',      r: 4,   lit: 0 },
187      { a: 62,  d: 0.88, lbl: 'RECON ACTIVE',    r: 3,   lit: 0 },
188      { a: 148, d: 0.30, lbl: 'PIVOT ATTEMPT',   r: 3,   lit: 0 },
189    ];
190
191    var sweep = 270; /* start pointing up */
192    var SPEED = 1.0;
193    /* five equal accent colours for the blips */
194    var ACCr = ['#7d14d7', '#5801f5', '#f50034', '#85f403', '#e6e414'];
195
196    function bx(t) { return CX + Math.cos(t.a * Math.PI / 180) * RAD * t.d; }
197    function by(t) { return CY + Math.sin(t.a * Math.PI / 180) * RAD * t.d; }
198
199    function drawRadar() {
200      RC.clearRect(0, 0, RW, RH);
201
202      /* ── background disc (barely-there on white) ── */
203      RC.beginPath();
204      RC.arc(CX, CY, RAD + 4, 0, Math.PI * 2);
205      RC.fillStyle = rzInk(0.025);
206      RC.fill();
207
208      /* ── outer border ── */
209      RC.beginPath();
210      RC.arc(CX, CY, RAD, 0, Math.PI * 2);
211      RC.strokeStyle = rzInk(0.35);
212      RC.lineWidth = 1.6;
213      RC.stroke();
214
215      /* ── range rings ── */
216      [0.25, 0.5, 0.75, 1].forEach(function (f) {
217        RC.beginPath();
218        RC.arc(CX, CY, RAD * f, 0, Math.PI * 2);
219        RC.strokeStyle = f === 1 ? rzInk(0.22) : rzInk(0.09);
220        RC.lineWidth = f === 1 ? 1.4 : 0.8;
221        RC.stroke();
222      });
223
224      /* ── spoke lines ── */
225      for (var deg = 0; deg < 360; deg += 30) {
226        var r  = deg * Math.PI / 180;
227        RC.beginPath();
228        RC.moveTo(CX, CY);
229        RC.lineTo(CX + Math.cos(r) * RAD, CY + Math.sin(r) * RAD);
230        RC.strokeStyle = rzInk(0.05);
231        RC.lineWidth = 0.7;
232        RC.stroke();
233      }
234
235      /* ── sweep trail (90° fade) ── */
236      for (var i = 88; i >= 0; i--) {
237        var a1 = (sweep - i)     * Math.PI / 180;
238        var a2 = (sweep - i - 1) * Math.PI / 180;
239        var op = (1 - i / 88) * 0.14;
240        RC.beginPath();
241        RC.moveTo(CX, CY);
242        RC.arc(CX, CY, RAD, a1, a2, true);
243        RC.closePath();
244        RC.fillStyle = rzInk(op);
245        RC.fill();
246      }
247
248      /* ── sweep line ── */
249      var sr = sweep * Math.PI / 180;
250      RC.beginPath();
251      RC.moveTo(CX, CY);
252      RC.lineTo(CX + Math.cos(sr) * RAD, CY + Math.sin(sr) * RAD);
253      RC.strokeStyle = rzInk(0.65);
254      RC.lineWidth = 2;
255      RC.stroke();
256
257      /* ── blips (each a different accent, equal peers) ── */
258      threats.forEach(function (t, idx) {
259        if (!t.col) t.col = ACCr[idx % ACCr.length];
260        var diff = ((sweep - t.a) % 360 + 360) % 360;
261        if (diff < 2) t.lit = 1.0;
262        t.lit = Math.max(0, t.lit - 0.004);
263
264        if (t.lit < 0.05) return;
265
266        var tx = bx(t), ty = by(t);
267
268        /* expanding ping ring */
269        if (diff < 45 && t.lit > 0.3) {
270          var ringR = diff * 1.8;
271          RC.globalAlpha = t.lit * 0.45;
272          RC.beginPath();
273          RC.arc(tx, ty, t.r + ringR, 0, Math.PI * 2);
274          RC.strokeStyle = t.col;
275          RC.lineWidth = 0.9;
276          RC.stroke();
277        }
278
279        /* dot */
280        RC.globalAlpha = Math.min(t.lit * 1.4, 0.95);
281        RC.beginPath();
282        RC.arc(tx, ty, t.r, 0, Math.PI * 2);
283        RC.fillStyle = t.col;
284        RC.fill();
285
286        /* glow */
287        RC.globalAlpha = t.lit * 0.3;
288        RC.beginPath();
289        RC.arc(tx, ty, t.r + 2, 0, Math.PI * 2);
290        RC.strokeStyle = t.col;
291        RC.lineWidth = 1;
292        RC.stroke();
293
294        /* label */
295        if (t.lit > 0.25) {
296          var lx = tx + (tx > CX ? t.r + 5 : -(t.lbl.length * 5.2 + t.r + 5));
297          var ly = ty - t.r - 3;
298          if (lx < 5) lx = 5;
299          if (lx + t.lbl.length * 5.2 > RW - 5) lx = RW - t.lbl.length * 5.2 - 5;
300          RC.globalAlpha = t.lit * 0.95;
301          RC.fillStyle = t.col;
302          RC.font = 'bold 8.5px "Courier New", monospace';
303          RC.fillText(t.lbl, lx, ly);
304        }
305        RC.globalAlpha = 1;
306      });
307
308      /* ── centre dot ── */
309      RC.beginPath();
310      RC.arc(CX, CY, 3.5, 0, Math.PI * 2);
311      RC.fillStyle = rzInk(1);
312      RC.fill();
313      RC.beginPath();
314      RC.arc(CX, CY, 7, 0, Math.PI * 2);
315      RC.strokeStyle = rzInk(0.3);
316      RC.lineWidth = 1;
317      RC.stroke();
318
319      /* ── status label ── */
320      RC.fillStyle = rzInk(0.5);
321      RC.font = 'bold 9px "Courier New", monospace';
322      RC.textAlign = 'center';
323      RC.fillText('● SENTINEL SCAN ACTIVE', CX, RH - 8);
324      RC.textAlign = 'left';
325
326      /* ── degree markers ── */
327      RC.fillStyle = rzInk(0.28);
328      RC.font = '8px "Courier New", monospace';
329      RC.textAlign = 'center';
330      ['0°','90°','180°','270°'].forEach(function (lbl, i) {
331        var ar = i * 90 * Math.PI / 180;
332        var mx = CX + Math.cos(ar) * (RAD + 14);
333        var my = CY + Math.sin(ar) * (RAD + 14) + 3;
334        RC.fillText(lbl, mx, my);
335      });
336      RC.textAlign = 'left';
337
338      sweep = (sweep + SPEED) % 360;
339      requestAnimationFrame(drawRadar);
340    }
341
342    drawRadar();
343  }
344
345  /* ══════════════════════════════════════════════════════════════
346     SERVICE & INDUSTRY PAGE ANIMATIONS
347  ══════════════════════════════════════════════════════════════ */
348
349  /* ── 6. PENTESTING — Network Port Scanner ──────────────────── */
350  (function () {
351    /* ── 6. PENTESTING — Attack-surface constellation ─────────────
352       A target hub, two orbit rings, eight services. Ink probes leave
353       the hub in sequence; open services settle into an accent ring,
354       closed ones stay hollow. The outer bezel turns slowly. Rhymes
355       with Phase 02 of the engagement track (Reconnaissance → the
356       attack-surface map). Blueprint-on-white, tokens read at runtime,
357       time-based easing, ~13 s loop with a hold. ── */
358    var cv = document.getElementById('rz-visual-pentest');
359    if (!cv) return;
360    var W = 480, H = 480;
361    var g = cv.getContext('2d');
362    if (!g) return;
363    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
364  
365    var PI = Math.PI, TAU = PI * 2;
366    var CX = 240, CY = 232;
367    var R_HUB = 15, R1 = 88, R2 = 168, R_NODE = 11;
368    var LOOP = 13.0;                     /* seconds per cycle */
369    var T_IDLE = 0.9, T_STEP = 0.9, T_TRAVEL = 0.75;
370    var T_PING = 0.5, T_BLOOM = 0.8, T_SETTLE = 0.6, T_FADE = 0.9;
371    var T_TEXT = 0.35;                   /* chip text crossfade */
372    var ROT_PERIOD = 100;                /* seconds per turn of the outer bezel */
373    var N_OPEN = 3;
374  
375    /* probe order runs clockwise, alternating inner / outer ring */
376    var NODES = [
377      { port: '443',  svc: 'HTTPS',    r: R1, a: 337.5, open: true  },
378      { port: '445',  svc: 'SMB',      r: R2, a: 22.5,  open: false },
379      { port: '22',   svc: 'SSH',      r: R1, a: 67.5,  open: true  },
380      { port: '3306', svc: 'MYSQL',    r: R2, a: 112.5, open: false },
381      { port: '21',   svc: 'FTP',      r: R1, a: 157.5, open: false },
382      { port: '3389', svc: 'RDP',      r: R2, a: 202.5, open: true  },
383      { port: '25',   svc: 'SMTP',     r: R1, a: 247.5, open: false },
384      { port: '8080', svc: 'HTTP-ALT', r: R2, a: 292.5, open: false }
385    ];
386    var N = NODES.length, i, k = 0;
387    for (i = 0; i < N; i++) {
388      var nd = NODES[i], rad = nd.a * PI / 180;
389      nd.cos = Math.cos(rad); nd.sin = Math.sin(rad);
390      nd.x = CX + nd.cos * nd.r; nd.y = CY + nd.sin * nd.r;
391      nd.label = nd.port + ' · ' + nd.svc;
392      if (Math.abs(nd.cos) > 0.5) {        /* beside the node, never across the ring */
393        nd.align = nd.cos > 0 ? 'left' : 'right';
394        nd.lx = nd.x + (nd.cos > 0 ? 1 : -1) * (R_NODE + 5);
395        nd.ly = nd.y + nd.sin * (R_NODE + 9) + 3;
396      } else {                              /* above / below the node */
397        nd.align = 'center';
398        nd.lx = nd.x + nd.cos * (R_NODE + 4);
399        nd.ly = nd.sin > 0 ? nd.y + R_NODE + 12 : nd.y - R_NODE - 5;
400      }
401      nd.openIdx = nd.open ? k++ : -1;   /* which of the open services (for accent rotation) */
402      nd.tStart = T_IDLE + i * T_STEP;
403      nd.tArrive = nd.tStart + T_TRAVEL;
404    }
405  
406    /* five equal accents, house rotation order; fall back if a token is missing */
407    var ACC_KEYS = ['purple', 'lime', 'crimson', 'indigo', 'yellow'];
408    var ACC_FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' };
409    function tokens() {
410      var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark';
411      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
412      function rd(name, fb) {
413        var v = '';
414        try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {}
415        v = v.replace(/^\s+|\s+$/g, '');
416        return v || fb;
417      }
418      var acc = [], j;
419      for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]]));
420      return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') };
421    }
422  
423    /* ── easing + small helpers ── */
424    function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
425    function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
426    function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
427    function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); }
428    function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); }
429    function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); }
430    function roundRect(x, y, w, h, r) {
431      g.beginPath();
432      g.moveTo(x + r, y);
433      g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r);
434      g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r);
435      g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r);
436      g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r);
437      g.closePath();
438    }
439    /* letter-spaced text (canvas letterSpacing is not universal) */
440    function spaced(txt, x, y, sp, align) {
441      var ws = [], w = 0, j;
442      for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; }
443      w += sp * (txt.length - 1);
444      var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x);
445      g.textAlign = 'left';
446      for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y);
446 px += ws[j] + sp; }
447      return w;
448    }
449    var FONT_LABEL = "600 8.5px 'Oxanium', sans-serif";
450    var FONT_TAG = "700 8.5px 'Oxanium', sans-serif";
451    var FONT_MONO = "bold 9px 'Courier New', monospace";
452    var FONT_MONO_S = "bold 8.5px 'Courier New', monospace";
453    var CHIP_FINAL = 'SURFACE MAPPED · ' + N + '/' + N + ' · ' + N_OPEN + ' OPEN';
454  
455    /* chip text crossfade memory (the only non-derived state) */
456    var chipText = '', chipPrev = '', chipChangedAt = -10;
457  
458    /* ── one composed frame at loop-time t (0..LOOP), absolute time T ── */
459    function scene(t, T, tk, instant) {
460      g.clearRect(0, 0, W, H);
461      g.lineCap = 'round'; g.lineJoin = 'round';
462      g.textBaseline = 'alphabetic';
463  
464      var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE));   /* end-of-cycle fade */
465      var cycle = Math.floor(T / LOOP);
466      var acc = tk.acc, paper = tk.paper;
467      function accentFor(nd) { return acc[(cycle * N_OPEN + nd.openIdx) % acc.length]; }
468  
469      /* — corner crop-brackets (the sheet) — */
470      g.strokeStyle = rzInk(0.45); g.lineWidth = 1;
471      var m = 8.5, L = 12;
472      line(m, m, m + L, m); line(m, m, m, m + L);
473      line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L);
474      line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L);
475      line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L);
476  
477      /* — eyebrow + phase readout — */
478      g.fillStyle = rzInk(0.62); g.font = FONT_LABEL;
479      var ew = spaced('ATTACK-SURFACE MAP', 27, 19, 1.2, 'left');
480      g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1);
481      g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right';
482      g.fillText('PHASE 02 · D2→D4', W - 27, 19);
483  
484      /* — inner orbit (dashed) — */
485      g.strokeStyle = rzInk(0.16); g.lineWidth = 1;
486      if (g.setLineDash) g.setLineDash([2, 6]);
487      circle(CX, CY, R1); g.stroke();
488      if (g.setLineDash) g.setLineDash([]);
489  
490      /* — outer orbit + slowly turning ruler bezel — */
491      g.strokeStyle = rzInk(0.13); circle(CX, CY, R2); g.stroke();
492      var rot = (T / ROT_PERIOD) * TAU, j, a, c, s, len, major;
493      for (j = 0; j < 48; j++) {
494        major = (j % 4) === 0;
495        a = rot + j * TAU / 48; c = Math.cos(a); s = Math.sin(a);
496        len = major ? 7 : 3.5;
497        g.strokeStyle = rzInk(major ? 0.36 : 0.2);
498        line(CX + c * R2, CY + s * R2, CX + c * (R2 - len), CY + s * (R2 - len));
499      }
500  
501      /* — per-node timing — */
502      var st = [], p = [], nd;
503      for (j = 0; j < N; j++) {
504        nd = NODES[j];
505        p[j] = clamp01((t - nd.tStart) / T_TRAVEL);
506        st[j] = easeOut(clamp01((t - nd.tArrive) / T_SETTLE)) * keep;
507      }
508  
509      /* — spokes — */
510      for (j = 0; j < N; j++) {
511        nd = NODES[j];
512        var glow = (p[j] > 0 && p[j] < 1) ? Math.sin(p[j] * PI) * 0.16 : 0;
513        var alpha = 0.13 + glow + (nd.open ? 0.13 : 0.03) * st[j];
514        g.strokeStyle = rzInk(alpha); g.lineWidth = 1;
515        line(CX + nd.cos * (R_HUB + 2), CY + nd.sin * (R_HUB + 2), CX + nd.cos * (nd.r - R_NODE - 2), CY + nd.sin * (nd.r - R_NODE - 2));
516      }
517  
518      /* — probes in flight (comet tail + dot) — */
519      for (j = 0; j < N; j++) {
520        if (!(p[j] > 0 && p[j] < 1)) continue;
521        nd = NODES[j];
522        var rA = R_HUB + 3, rB = nd.r - R_NODE - 3;
523        var pe = easeInOut(p[j]), pt = Math.max(0, pe - 0.16);
524        var r1 = rA + (rB - rA) * pe, r0 = rA + (rB - rA) * pt;
525        var dotA = Math.min(1, p[j] / 0.08);
526        var gr = g.createLinearGradient(CX + nd.cos * r0, CY + nd.sin * r0, CX + nd.cos * r1, CY + nd.sin * r1);
527        gr.addColorStop(0, rzInk(0)); gr.addColorStop(1, rzInk(0.45 * dotA));
528        g.strokeStyle = gr; g.lineWidth = 1.6;
529        line(CX + nd.cos * r0, CY + nd.sin * r0, CX + nd.cos * r1, CY + nd.sin * r1);
530        g.fillStyle = rzInk(0.9 * dotA); circle(CX + nd.cos * r1, CY + nd.sin * r1, 2.6); g.fill();
531      }
532  
533      /* — arrival blooms — */
534      for (j = 0; j < N; j++) {
535        nd = NODES[j];
536        var bl = clamp01((t - nd.tArrive) / T_BLOOM);
537        if (bl <= 0 || bl >= 1) continue;
538        var br = R_NODE + 12 * easeOutQuint(bl);
539        g.lineWidth = 1.2;
540        if (nd.open) { g.strokeStyle = accentFor(nd); g.globalAlpha = (1 - bl) * 0.6; }
541        else { g.strokeStyle = rzInk((1 - bl) * 0.45); }
542        circle(nd.x, nd.y, br); g.stroke();
543        g.globalAlpha = 1;
544      }
545  
546      /* — service nodes — */
547      for (j = 0; j < N; j++) {
548        nd = NODES[j];
549        var sv = st[j];
550        g.fillStyle = paper; circle(nd.x, nd.y, R_NODE); g.fill();
551        if (nd.open) {
552          /* ink ring hands over to a double accent ring + accent dot */
553          if (sv < 1) { g.strokeStyle = rzInk(0.32 * (1 - sv)); g.lineWidth = 1; circle(nd.x, nd.y, R_NODE); g.stroke(); }
554          if (sv > 0) {
555            var col = accentFor(nd);
556            g.strokeStyle = col; g.fillStyle = col;
557            g.globalAlpha = sv; g.lineWidth = 1.6; circle(nd.x, nd.y, R_NODE); g.stroke();
558            g.globalAlpha = sv * 0.55; g.lineWidth = 1; circle(nd.x, nd.y, R_NODE + 5); g.stroke();
559            g.globalAlpha = sv; circle(nd.x, nd.y, 2.4); g.fill();
560            g.globalAlpha = 1;
561          }
562        } else {
563          /* closed: stays hollow, firms up, takes a small ink dash */
564          g.strokeStyle = rzInk(0.32 + 0.3 * sv); g.lineWidth = 1; circle(nd.x, nd.y, R_NODE); g.stroke();
565          if (sv > 0) { g.strokeStyle = rzInk(0.6 * sv); g.lineWidth = 1.2; line(nd.x - 3.5, nd.y, nd.x + 3.5, nd.y); }
566        }
567        /* port · service micro-label */
568        g.font = FONT_MONO_S; g.textAlign = nd.align;
569        g.fillStyle = rzInk(0.38 + (nd.open ? 0.47 : 0.2) * sv);
570        g.fillText(nd.label, nd.lx, nd.ly);
571      }
572  
573      /* — hub: double ink ring, probe pings, TARGET tag — */
574      for (j = 0; j < N; j++) {
575        var pg = clamp01((t - NODES[j].tStart) / T_PING);
576        if (pg <= 0 || pg >= 1) continue;
577        g.strokeStyle = rzInk((1 - pg) * 0.35); g.lineWidth = 1;
578        circle(CX, CY, R_HUB + 9 * easeOut(pg)); g.stroke();
579      }
580      g.fillStyle = paper; circle(CX, CY, R_HUB); g.fill();
581      g.strokeStyle = rzInk(0.9); g.lineWidth = 1.3; circle(CX, CY, R_HUB); g.stroke();
582      g.strokeStyle = rzInk(0.5); g.lineWidth = 1; circle(CX, CY, 9.5); g.stroke();
583      g.fillStyle = rzInk(0.9); circle(CX, CY, 2.2); g.fill();
584      g.font = FONT_TAG;
585      var tw = 0;
586      for (j = 0; j < 6; j++) tw += g.measureText('TARGET'.charAt(j)).width;
587      tw += 1.3 * 5;
588      g.fillStyle = paper; roundRect(CX - tw / 2 - 4, CY + 24, tw + 8, 13, 3); g.fill();   /* label halo */
589      g.fillStyle = rzInk(0.8);
590      spaced('TARGET', CX, CY + 34, 1.3, 'center');
591  
592      /* — status chip — */
593      var n = 0, o = 0;
594      for (j = 0; j < N; j++) if (t >= NODES[j].tArrive) { n++; if (NODES[j].open) o++; }
595      var txt = n === N ? CHIP_FINAL : (n === 0 ? 'PROBING SURFACE · 0/' + N : 'PROBING SURFACE · ' + n + '/' + N + ' · ' + o + ' OPEN');
596      if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; }
597      var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT));
598      if (chipChangedAt > t) e = 1;   /* clock wrapped */
599      g.font = FONT_MONO;
600      var cw = g.measureText(CHIP_FINAL).width + 30, ch = 24, cx = CX - cw / 2, cy = 432;
601      roundRect(cx, cy, cw, ch, 4);
602      g.fillStyle = paper; g.fill();
603      g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
604      g.textAlign = 'center'; g.textBaseline = 'middle';
605      if (e < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - e)); g.fillText(chipPrev, CX, cy + ch / 2 + 0.5); }
606      g.fillStyle = rzInk(0.78 * e); g.fillText(chipText, CX, cy + ch / 2 + 0.5);
607      g.textBaseline = 'alphabetic';
608    }
609  
610    /* ── reduced motion: one composed, fully mapped frame ── */
611    var reduce = false;
612    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
613    if (reduce) {
614      var drawStatic = function () { scene(LOOP - T_FADE - 0.05, 0, tokens(), true); };
615      drawStatic();
616      window.addEventListener('resize', drawStatic);
617      window.addEventListener('load', drawStatic);
618      document.addEventListener('rz-theme-change', drawStatic);
619      if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
620      return;
621    }
622  
623    /* ── time-based loop; pauses when the tab is hidden or the hero is off-screen ── */
624    var raf = null, t0 = null, pausedAt = null, inView = true;
625    function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); }
626    function frame(ts) {
627      raf = null;
628      if (t0 === null) t0 = ts;
629      var T = Math.max(0, (ts - t0) / 1000);
630      scene(T % LOOP, T, tokens(), false);
631      raf = requestAnimationFrame(frame);
632    }
633    function start() {
634      if (raf !== null || document.hidden || !inView) return;
635      if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt;   /* resume without a time jump */
636      pausedAt = null;
637      raf = requestAnimationFrame(frame);
638    }
639    function stop() {
640      if (raf === null) return;
641      cancelAnimationFrame(raf); raf = null; pausedAt = now();
642    }
643    document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
644    if ('IntersectionObserver' in window) {
645      try {
646        new IntersectionObserver(function (entries) {
647          for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting;
648          if (inView) start(); else stop();
649        }, { threshold: 0 }).observe(cv);
650      } catch (e) {}
651    }
652    start();
653  })();
654
655  /* ── 7. RED TEAM — ATT&CK Kill Chain ───────────────────────── */
656  (function () {
657    /* ── 7. RED TEAM — Kill-Chain Matrix ───────────────────────────
658       Rhymes with the page's "Operation dossier" section: a 5×4 tactic
659       matrix (RECON · ACCESS · FOOTHOLD · MOVE · OBJECTIVE) in which one
660       cell per column is lit in sequence by a travelling signal. Flow
661       connectors are drawn in the column accent, then settle into a faint
662       ink trail; a ruler-tick timeline scrubber with an eased caret tracks
663       the phase; once the objective is reached, a lime "detection
664       opportunity" tick is pinned to the MOVE cell. Time-based and eased,
665       pauses when hidden / off-screen, one static frame under reduced motion. */
666    var cv = document.getElementById('rz-visual-redteam');
667    if (!cv) return;
668    var W = 480, H = 480;
669    var g = cv.getContext('2d');
670    if (!g) return;
671    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
672  
673    /* columns mirror the section's five tactics; readouts are illustrative */
674    var COLS = [
675      { lbl: 'RECON',     read: 'TA0043 · T1595 ACTIVE SCANNING', row: 2, tok: '--rz-purple',  fb: '#7d14d7' },
676      { lbl: 'ACCESS',    read: 'TA0001 · T1566 PHISHING',        row: 1, tok: '--rz-lime',    fb: '#85f403' },
677      { lbl: 'FOOTHOLD',  read: 'TA0011 · T1071 APP-LAYER C2',    row: 0, tok: '--rz-crimson', fb: '#f50034' },
678      { lbl: 'MOVE',      read: 'TA0008 · T1550 PASS-THE-HASH',   row: 1, tok: '--rz-indigo',  fb: '#5801f5' },
679      { lbl: 'OBJECTIVE', read: 'TA0010 · T1041 EXFIL OVER C2',   row: 2, tok: '--rz-yellow',  fb: '#e6e414' }
680    ];
681    var N = COLS.length, ROWS = 4, DET = 3;            /* detection tick lands on MOVE */
682  
683    /* geometry (logical px) */
684    var CW = 58, CH = 44, PX = 88, PY = 58, X0 = 64, Y0 = 136;   /* cell size, pitch, first centre */
685    var CR = [3, 8, 8, 8];                                       /* folder-card corner radii */
686    var HEAD_Y = 50, LABEL_Y = 100, RAIL_Y = 378, RAIL_X0 = 48, RAIL_X1 = 432, FOOT_Y = 425;
687  
688    /* cadence: 500ms lead-in · 5 × (800ms travel + 700ms dwell) · detection · hold · 600ms reset */
689    var T_IN = 500, TRAVEL = 800, PHASE = 1500, FADE = 600;
690    var T_DONE = T_IN + (N - 1) * PHASE + TRAVEL;      /* objective reached      7300 */
691    var T_DET  = T_IN + N * PHASE + 200;               /* detection tick appears 8200 */
692    var LOOP   = 11400, T_FADE = LOOP - FADE;          /* ~11.4 s per cycle */
693  
694    var reduce = false;
695    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
696  
697    function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
698    function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
699    function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
700    function easeOut5(p) { return 1 - Math.pow(1 - p, 5); }
701    function pad(n) { return (n < 10 ? '0' : '') + n; }
702    function cx(c) { return X0 + c * PX; }
703    function cy(r) { return Y0 + r * PY; }
704  
705    /* ── colour tokens: read from CSS each frame so theme switches are picked up ── */
706    function parseColor(v, fb) {
707      var m, r = -1, gg = -1, b = -1;
708      v = (v || '').replace(/^\s+|\s+$/g, '');
709      if (v.charAt(0) === '#') {
710        if (v.length === 4) {
711          r = parseInt(v.charAt(1) + v.charAt(1), 16); gg = parseInt(v.charAt(2) + v.charAt(2), 16); b = parseInt(v.charAt(3) + v.charAt(3), 16);
712        } else if (v.length >= 7) {
713          r = parseInt(v.substr(1, 2), 16); gg = parseInt(v.substr(3, 2), 16); b = parseInt(v.substr(5, 2), 16);
714        }
715      } else if ((m = /rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(v))) {
716        r = Math.round(+m[1]); gg = Math.round(+m[2]); b = Math.round(+m[3]);
717      }
718      if (isNaN(r) || isNaN(gg) || isNaN(b) || r < 0 || gg < 0 || b < 0) {
719        return v === fb ? [125, 20, 215] : parseColor(fb, fb);
720      }
721      return [r, gg, b];
722    }
723    function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }
724    function tokens() {
725      var cs = null, out = { paper: '#ffffff', acc: [], lime: null }, i;
726      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
727      function t(name, fb) {
728        var v = '';
729        if (cs) { try { v = cs.getPropertyValue(name); } catch (e) {} }
730        v = (v || '').replace(/^\s+|\s+$/g, '');
731        return v || fb;
732      }
733      out.paper = t('--rz-paper', '#ffffff');
734      for (i = 0; i < N; i++) out.acc[i] = parseColor(t(COLS[i].tok, COLS[i].fb), COLS[i].fb);
735      out.lime = parseColor(t('--rz-lime', '#85f403'), '#85f403');
736      return out;
737    }
738  
739    /* ── drawing helpers ── */
740    function rrect(x, y, w, h, r) {   /* per-corner radii, no roundRect dependency */
741      var tl = r[0], tr = r[1], br = r[2], bl = r[3];
742      g.beginPath();
743      g.moveTo(x + tl, y);
744      g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr);
745      g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br);
746      g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl);
747      g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl);
748      g.closePath();
749    }
750    function tracked(str, x, y, track, align) {   /* letter-spaced text; returns its start x and width */
751      var i, w = [], total = 0, sx;
752      for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; }
753      total += track * (str.length - 1);
754      sx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x);
755      g.textAlign = 'left';
756      for (i = 0, x = sx; i < str.length; i++) { g.fillText(str.charAt(i), x, y); x += w[i] + track; }
757      return { x: sx, w: total };
758    }
759    /* paper chip whose text swaps between a and b with progress f (slide + fade) */
760    function chip(a, b, f, x, yMid, align, paper) {
761      var wa, wb, w, h = 22, bx, by;
762      g.font = "700 9px 'Courier New', monospace";
763      wa = g.measureText(a).width + 20; wb = g.measureText(b).width + 20;
764      w = wa + (wb - wa) * f;
765      bx = align === 'right' ? x - w : x; by = yMid - h / 2;
766      rrect(bx, by, w, h, [3, 6, 6, 6]);
767      g.fillStyle = paper; g.fill();
768      g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
769      g.textAlign = 'left'; g.textBaseline = 'middle';
770      if (f < 1) { g.fillStyle = rzInk(0.75 * (1 - f)); g.fillText(a, bx + 10, yMid + 0.5 - 4 * f); }
771      if (f > 0) { g.fillStyle = rzInk(0.75 * f);       g.fillText(b, bx + 10, yMid + 0.5 + 4 * (1 - f)); }
772      g.textBaseline = 'alphabetic';
773    }
774  
775    /* ── signal path: one flow connector per column, flattened once ── */
776    function cubic(x0, y0, x1, y1, steps) {   /* horizontal tangents at both ends */
777      var h = (x1 - x0) * 0.5, pts = [], i, t, u;
778      for (i = 0; i <= steps; i++) {
779        t = i / steps; u = 1 - t;
780        pts.push({
781          x: u * u * u * x0 + 3 * u * u * t * (x0 + h) + 3 * u * t * t * (x1 - h) + t * t * t * x1,
782          y: y0 + (y1 - y0) * (3 * t * t - 2 * t * t * t)
783        });
784      }
785      return pts;
786    }
787    function buildSeg(pts) {
788      var cum = [0], L = 0, i, dx, dy;
789      for (i = 1; i < pts.length; i++) { dx = pts[i].x - pts[i - 1].x; dy = pts[i].y - pts[i - 1].y; L += Math.sqrt(dx * dx + dy * dy); cum[i] = L; }
790      return { pts: pts, cum: cum, len: L };
791    }
792    var SEGS = [], k;
793    SEGS[0] = buildSeg([{ x: 6, y: cy(COLS[0].row) }, { x: cx(0) - CW / 2, y: cy(COLS[0].row) }]);   /* lead-in from outside */
794    for (k = 1; k < N; k++) {
795      SEGS[k] = buildSeg(cubic(cx(k - 1) + CW / 2, cy(COLS[k - 1].row), cx(k) - CW / 2, cy(COLS[k].row), 32));
796    }
797    function pointAt(s, p) {
798      var d = s.len * p, i, a, b, f;
799      if (p <= 0) return s.pts[0];
800      if (p >= 1) return s.pts[s.pts.length - 1];
801      for (i = 1; i < s.pts.length; i++) {
802        if (s.cum[i] >= d) {
803          a = s.pts[i - 1]; b = s.pts[i]; f = (d - s.cum[i - 1]) / ((s.cum[i] - s.cum[i - 1]) || 1);
804          return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
805        }
806      }
807      return s.pts[s.pts.length - 1];
808    }
809    function tracePartial(s, p) {
810      var d = s.len * p, i, e;
811      g.beginPath(); g.moveTo(s.pts[0].x, s.pts[0].y);
812      for (i = 1; i < s.pts.length; i++) { if (s.cum[i] <= d) g.lineTo(s.pts[i].x, s.pts[i].y); else break; }
813      e = pointAt(s, p); g.lineTo(e.x, e.y);
814    }
815  
816    /* ── everything derives from the loop time — no per-frame state, no jitter ── */
817    function phaseStart(i) { return T_IN + i * PHASE; }
818    function segState(t, i) {
819      var s = phaseStart(i), landed = t >= s + TRAVEL;
820      return {
821        started: t >= s,
822        p: t < s ? 0 : easeInOut(clamp01((t - s) / TRAVEL)),
823        landed: landed,
824        age: landed ? t - s - TRAVEL : 0
825      };
826    }
827    function caretX(t) {
828      var i, local, p, from, q;
829      if (t < T_IN) return RAIL_X0;
830      if (t >= T_FADE) { q = easeInOut(clamp01((t - T_FADE) / FADE)); return cx(N - 1) + (RAIL_X0 - cx(N - 1)) * q; }
831      i = Math.min(N - 1, Math.floor((t - T_IN) / PHASE));
832      local = t - phaseStart(i);
833      p = easeInOut(clamp01(local / TRAVEL));
834      from = i === 0 ? RAIL_X0 : cx(i - 1);
835      return from + (cx(i) - from) * p;
836    }
837  
838    function draw(t, loop) {
839      var tk = tokens(), st = [], i, r, c, x, y, a, f, q, e, pt, mix, cur, prev, swap, G = 1, hA, det, w, lit, o;
840      var done, dA, txtA, txtB, dc, dr, bx, by, bs, lp, ca, rise, chipX, chipY, lime;
841  
842      if (t >= T_FADE) G = 1 - easeInOut(clamp01((t - T_FADE) / FADE));   /* clean reset: dynamic marks fade */
843      for (i = 0; i < N; i++) st[i] = segState(t, i);
844      cur = t < T_IN ? -1 : Math.min(N - 1, Math.floor((t - T_IN) / PHASE));
845      /* readout swap: previous → current column, eased over 300ms from the phase start */
846      if (cur < 0) { prev = N - 1; swap = loop > 0 ? easeOut(clamp01(t / 300)) : 1; cur = 0; }
847      else { prev = cur > 0 ? cur - 1 : cur; swap = cur > 0 ? easeOut(clamp01((t - phaseStart(cur)) / 300)) : 1; }
848      lime = tk.lime;
849  
850      g.clearRect(0, 0, W, H);
851      g.textBaseline = 'alphabetic';
852      g.lineJoin = 'round';
853  
854      /* ── corner crop-brackets ── */
855      g.lineCap = 'butt';
856      g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5;
857      g.beginPath();
858      g.moveTo(16, 30);  g.lineTo(16, 16);  g.lineTo(30, 16);
859      g.moveTo(450, 16); g.lineTo(464, 16); g.lineTo(464, 30);
860      g.moveTo(464, 450); g.lineTo(464, 464); g.lineTo(450, 464);
861      g.moveTo(30, 464); g.lineTo(16, 464); g.lineTo(16, 450);
862      g.stroke();
863      g.lineCap = 'round';
864  
865      /* ── header: eyebrow + phase chip ── */
866      g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55);
867      tracked('ADVERSARY EMULATION', 36, HEAD_Y + 4, 1.5, 'left');
868      txtA = 'PHASE ' + pad(prev + 1) + ' / ' + pad(N);
869      txtB = 'PHASE ' + pad(cur + 1) + ' / ' + pad(N);
870      chip(txtA, txtB, swap, 444, HEAD_Y, 'right', tk.paper);
871  
872      /* ── column labels (brighten on landing, accent underline) ── */
873      g.font = "600 9.5px 'Oxanium', sans-serif";
874      for (i = 0; i < N; i++) {
875        lit = st[i].landed ? easeOut(clamp01(st[i].age / 350)) * G : 0;
876        g.fillStyle = rzInk(0.42 + 0.46 * lit);
877        tracked(COLS[i].lbl, cx(i), LABEL_Y, 1.2, 'center');
878        if (lit > 0) { g.fillStyle = rgba(tk.acc[i], lit); g.fillRect(cx(i) - 9, LABEL_Y + 5, 18, 2); }
879      }
880  
881      /* detection annotation progress (used by the slot outline below) */
882      det = t >= T_DET ? 1 : 0;
883      ca = det ? easeOut(clamp01((t - T_DET - 300) / 450)) * G : 0;
884  
885      /* ── matrix: 20 quiet cell outlines ── */
886      g.lineWidth = 1;
887      for (r = 0; r < ROWS; r++) {
888        for (c = 0; c < N;
888 c++) {
889          a = 0.16;
890          if (c === DET && r === COLS[DET].row + 1) a *= (1 - ca);   /* this slot hosts the detection note */
891          if (a <= 0.002) continue;
892          rrect(cx(c) - CW / 2, cy(r) - CH / 2, CW, CH, CR);
893          g.strokeStyle = rzInk(a); g.stroke();
894        }
895      }
896  
897      /* ── connectors: accent while in flight, settling to a faint ink trail ── */
898      for (i = 0; i < N; i++) {
899        if (!st[i].started || st[i].p <= 0) continue;
900        mix = st[i].landed ? easeOut(clamp01((st[i].age - 100) / 500)) : 0;
901        tracePartial(SEGS[i], st[i].p);
902        if (mix < 1) { g.strokeStyle = rgba(tk.acc[i], 0.95 * (1 - mix) * G); g.lineWidth = 1.5; g.stroke(); }
903        if (mix > 0) { g.strokeStyle = rzInk(0.3 * mix * G); g.lineWidth = 1.25; g.stroke(); }
904      }
905  
906      /* ── lit cells: 18% accent fill + accent ring, settle pulse on landing ── */
907      for (i = 0; i < N; i++) {
908        if (!st[i].landed) continue;
909        f = easeOut(clamp01(st[i].age / 350)) * G;
910        x = cx(i) - CW / 2; y = cy(COLS[i].row) - CH / 2;
911        rrect(x, y, CW, CH, CR);
912        g.fillStyle = rgba(tk.acc[i], 0.18 * f); g.fill();
913        g.strokeStyle = rgba(tk.acc[i], 0.9 * f); g.lineWidth = 1.5; g.stroke();
914        q = clamp01(st[i].age / 700);
915        if (q < 1) {
916          e = 8 * easeOut(q);
917          rrect(x - e, y - e, CW + 2 * e, CH + 2 * e, [3 + e, 8 + e, 8 + e, 8 + e]);
918          g.strokeStyle = rgba(tk.acc[i], 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke();
919        }
920      }
921  
922      /* ── signal head (only while a connector is being drawn) ── */
923      for (i = 0; i < N; i++) {
924        if (!st[i].started || st[i].p <= 0) continue;
925        hA = st[i].landed ? 1 - clamp01(st[i].age / 150) : 1;
926        if (hA <= 0) continue;
927        pt = pointAt(SEGS[i], st[i].p);
928        g.fillStyle = rgba(tk.acc[i], 0.18 * hA * G); g.beginPath(); g.arc(pt.x, pt.y, 8, 0, Math.PI * 2); g.fill();
929        g.fillStyle = rgba(tk.acc[i], hA * G);        g.beginPath(); g.arc(pt.x, pt.y, 3.5, 0, Math.PI * 2); g.fill();
930      }
931  
932      /* ── detection opportunity: lime tick on the MOVE cell + a paper note in the slot below ── */
933      if (det && G > 0) {
934        dc = DET; dr = COLS[DET].row;
935        bx = cx(dc) + CW / 2 - 1; by = cy(dr) - CH / 2 + 1;              /* badge on the cell's top-right corner */
936        bs = easeOut5(clamp01((t - T_DET) / 450));
937        lp = easeOut(clamp01((t - T_DET - 150) / 400));
938        if (bs > 0) {
939          g.save(); g.translate(bx, by); g.scale(bs, bs); g.globalAlpha = G;
940          g.fillStyle = tk.paper; g.beginPath(); g.arc(0, 0, 7, 0, Math.PI * 2); g.fill();
941          g.strokeStyle = rgba(lime, 1); g.lineWidth = 1.5; g.stroke();
942          g.beginPath(); g.moveTo(-3, 0.5); g.lineTo(-1, 2.5); g.lineTo(3, -2.5); g.stroke();
943          g.restore();
944        }
945        if (lp > 0) {   /* leader from the cell's bottom edge to the note */
946          y = cy(dr) + CH / 2;
947          g.strokeStyle = rgba(lime, 0.9 * G); g.lineWidth = 1;
948          g.beginPath(); g.moveTo(cx(dc), y); g.lineTo(cx(dc), y + (PY - CH) * lp); g.stroke();
949        }
950        if (ca > 0) {
951          rise = 4 * (1 - ca / (G || 1));
952          chipX = cx(dc) - 34; chipY = cy(dr + 1) - CH / 2 + rise;
953          g.globalAlpha = ca;
954          rrect(chipX, chipY, 68, CH, CR);
955          g.fillStyle = tk.paper; g.fill();
956          g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
957          g.fillStyle = rgba(lime, 1); g.fillRect(chipX + 4, chipY + 9, 2.5, CH - 18);
958          g.font = "700 7.5px 'Courier New', monospace"; g.fillStyle = rzInk(0.8);
959          tracked('DETECTION', chipX + 12, chipY + 19, 0.5, 'left');
960          tracked('OPPORTUNITY', chipX + 12, chipY + 30, 0.5, 'left');
961          g.globalAlpha = 1;
962        }
963      }
964  
965      /* ── timeline scrubber: ruler ticks, rail, notches, caret ── */
966      g.lineCap = 'butt'; g.lineWidth = 1;
967      for (x = RAIL_X0; x <= RAIL_X1; x += 12) {
968        a = Math.min(1, (x - RAIL_X0) / 48, (RAIL_X1 - x) / 48);
969        if (a <= 0) continue;
970        g.strokeStyle = rzInk(0.22 * a);
971        g.beginPath(); g.moveTo(x + 0.5, RAIL_Y - 4.5); g.lineTo(x + 0.5, RAIL_Y + 4.5); g.stroke();
972      }
973      g.strokeStyle = rzInk(0.16); g.lineWidth = 1.5;
974      g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(RAIL_X1, RAIL_Y); g.stroke();
975      g.lineCap = 'round';
976      for (i = 0; i < N; i++) {
977        lit = st[i].landed ? easeOut(clamp01(st[i].age / 300)) * G : 0;
978        g.beginPath(); g.arc(cx(i), RAIL_Y, 4.5, 0, Math.PI * 2);
979        g.fillStyle = tk.paper; g.fill();
980        g.strokeStyle = rzInk(0.3); g.lineWidth = 1.25; g.stroke();
981        if (lit > 0) {
982          g.fillStyle = rgba(tk.acc[i], lit); g.fill();
983          g.strokeStyle = rgba(tk.acc[i], lit); g.stroke();
984        }
985        q = st[i].landed ? clamp01(st[i].age / 700) : 1;
986        if (q < 1) {
987          g.beginPath(); g.arc(cx(i), RAIL_Y, 4.5 + 7 * easeOut(q), 0, Math.PI * 2);
988          g.strokeStyle = rgba(tk.acc[i], 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke();
989        }
990      }
991      x = caretX(t);
992      o = (t < T_IN) ? 0 : G;                          /* halo: column accent while running, ink at rest */
993      g.beginPath(); g.arc(x, RAIL_Y, 9, 0, Math.PI * 2);
994      g.fillStyle = rzInk(0.08 * (1 - o)); g.fill();
995      if (o > 0) { g.fillStyle = rgba(tk.acc[cur], 0.18 * o); g.fill(); }
996      g.fillStyle = rzInk(0.9);
997      rrect(x - 1.25, RAIL_Y - 10, 2.5, 20, [1.25, 1.25, 1.25, 1.25]); g.fill();
998      g.beginPath(); g.moveTo(x - 4, RAIL_Y - 15); g.lineTo(x + 4, RAIL_Y - 15); g.lineTo(x, RAIL_Y - 10); g.closePath(); g.fill();
999  
1000      /* ── footer: tactic / technique readout, status ── */
1001      chip(COLS[prev].read, COLS[cur].read, swap, 36, FOOT_Y, 'left', tk.paper);
1002      done = t >= T_DONE;
1003      dA = done ? easeOut(clamp01((t - T_DONE) / 350)) * G : 0;
1004      g.font = "600 9px 'Oxanium', sans-serif";
1005      if (dA < 1) {   /* slide-swap, same motion as the chips */
1006        y = FOOT_Y - 4 * dA;
1007        g.fillStyle = rzInk(0.5 * (1 - dA));
1008        w = tracked('REPLAYING KILL CHAIN', 444, y + 3, 1.2, 'right');
1009        g.fillStyle = rzInk(0.7 * (1 - dA)); g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill();
1010      }
1011      if (dA > 0) {
1012        y = FOOT_Y + 4 * (1 - dA);
1013        g.fillStyle = rzInk(0.7 * dA);
1014        w = tracked('OBJECTIVES REACHED', 444, y + 3, 1.2, 'right');
1015        g.fillStyle = rgba(lime, dA); g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill();
1016      }
1017    }
1018  
1019    /* ── reduced motion: one composed frame, redrawn on resize / theme / font load ── */
1020    var T_STATIC = T_DET + 1200;
1021    function drawStatic() { draw(T_STATIC, 1); }
1022    if (reduce) {
1023      drawStatic();
1024      window.addEventListener('resize', drawStatic);
1025      window.addEventListener('load', drawStatic);
1026      document.addEventListener('rz-theme-change', drawStatic);
1027      if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
1028      return;
1029    }
1030  
1031    /* ── time-based loop; pauses when the tab is hidden or the canvas is off-screen ── */
1032    var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true;
1033    function nowMs() { return (window.performance && performance.now) ? performance.now() : Date.now(); }
1034    function frame(now) {
1035      var el;
1036      raf = 0;
1037      if (typeof now !== 'number') now = nowMs();
1038      if (!started) { started = true; t0 = now; }
1039      el = now - t0;
1040      draw(el % LOOP, Math.floor(el / LOOP));
1041      if (running) raf = requestAnimationFrame(frame);
1042    }
1043    function start() {
1044      if (running) return;
1045      running = true;
1046      if (pausedAt && started) t0 += nowMs() - pausedAt;
1047      pausedAt = 0;
1048      if (!raf) raf = requestAnimationFrame(frame);
1049    }
1050    function stop() {
1051      if (!running) return;
1052      running = false; pausedAt = nowMs();
1053      if (raf) { cancelAnimationFrame(raf); raf = 0; }
1054    }
1055    function sync() { if (document.hidden || !inView) stop(); else start(); }
1056    document.addEventListener('visibilitychange', sync);
1057    if ('IntersectionObserver' in window) {
1058      try {
1059        new IntersectionObserver(function (entries) {
1060          for (var i = 0; i < entries.length; i++) inView = entries[i].isIntersecting;
1061          sync();
1062        }, { threshold: 0 }).observe(cv);
1063      } catch (e) {}
1064    }
1065    sync();
1066  })();
1067
1068  /* ── 8. INCIDENT RESPONSE — IR Playbook Timeline ───────────── */
1069  (function () {
1070  /* Response Dial — hero visual for incident-response.html.
1071     A circular instrument that rhymes with the page's Response Clock
1072     section: fine ink tick ring, six faint phase arcs, an eased hand
1073     that settles at every phase boundary, the active arc filling with
1074     its accent as the hand passes, a small pulse at each boundary, and
1075     a paper chip in the centre with the phase and a T+ counter.
1076     Everything derives from loop time (no per-frame state, no jitter);
1077     pauses when the tab is hidden or the canvas is off-screen; draws
1078     one composed frame under prefers-reduced-motion. ─────────────── */
1079    var cv = document.getElementById('rz-visual-ir');
1080    if (!cv) return;
1081    var W = 480, H = 480, CX = 240, CY = 240;
1082    var g = cv.getContext('2d');
1083    if (!g) return;
1084    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
1085  
1086    /* phases mirror the Response Clock section — accents cycle like its
1087       nth-child(5n+k) rule; timings are illustrative, in minutes */
1088    var PHASES = [
1089      { name: 'PREPARE',   read: 'STANDBY', t0: null, t1: null, step: 0  },
1090      { name: 'IDENTIFY',  read: 'T+00:00', t0: 0,    t1: 60,   step: 1  },
1091      { name: 'CONTAIN',   read: 'T+01:00', t0: 60,   t1: 480,  step: 5  },
1092      { name: 'ERADICATE', read: 'T+08:00', t0: 480,  t1: 1440, step: 15 },
1093      { name: 'RECOVER',   read: 'T+24:00', t0: 1440, t1: 4320, step: 30 },
1094      { name: 'LEARN',     read: 'T+72:00', t0: 4320, t1: 5760, step: 30 }
1095    ];
1096    var TOK = ['--rz-purple', '--rz-lime', '--rz-crimson', '--rz-indigo', '--rz-yellow'];
1097    var FB  = ['#7d14d7',     '#85f403',   '#f50034',      '#5801f5',     '#e6e414'];
1098    var N = 6, SECTOR = 60, GAP = 3;
1099  
1100    /* cadence: eased sweep, settle at the boundary, hold once the cycle
1101       completes, then a soft reset — 12.8 s per loop */
1102    var SWEEP = 1500, DWELL = 380, PH = SWEEP + DWELL;
1103    var END_HOLD = 1500, RESET = 520, PULSE = 900;
1104    var LOOP = N * PH + END_HOLD;
1105  
1106    /* geometry — same proportions as the section dial */
1107    var R_NUM = 222, R_RING = 200, R_ARC = 166, ARC_W = 12, R_READ = 134;
1108    var R_HAND = 184, R_DISC = 104, R_DISC_IN = 96, R_TRAIL_OUT = 150, R_TRAIL_IN = 118;
1109  
1110    var reduce = false;
1111    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
1112  
1113    function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
1114    function easeInOut(p) { p = clamp01(p); return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
1115    function easeOut(p) { p = clamp01(p); return 1 - Math.pow(1 - p, 3); }
1116    function smooth(p) { p = clamp01(p); return p * p * (3 - 2 * p); }
1117    function rad(deg) { return (deg - 90) * Math.PI / 180; }         /* 0° at 12 o'clock, clockwise */
1118    function px(r, deg) { return CX + r * Math.cos(rad(deg)); }
1119    function py(r, deg) { return CY + r * Math.sin(rad(deg)); }
1120    function pad(n) { return (n < 10 ? '0' : '') + n; }
1121    function fmt(min) { var m = Math.max(0, Math.floor(min)); return 'T+' + pad(Math.floor(m / 60)) + ':' + pad(m % 60); }
1122  
1123    /* design tokens, read once per frame so a theme switch is picked up */
1124    function tokens() {
1125      var cs = null, out = { paper: '#ffffff', cols: FB.slice() }, i, v;
1126      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
1127      if (!cs) return out;
1128      v = (cs.getPropertyValue('--rz-paper') || '').replace(/^\s+|\s+$/g, '');
1129      if (v) out.paper = v;
1130      for (i = 0; i < TOK.length; i++) {
1131        v = (cs.getPropertyValue(TOK[i]) || '').replace(/^\s+|\s+$/g, '');
1132        if (v) out.cols[i] = v;
1133      }
1134      return out;
1135    }
1136  
1137    /* letter-spaced text (canvas letterSpacing is not universal) */
1138    function tracked(str, x, y, track, align) {
1139      var i, w = [], total = 0, cx;
1140      for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; }
1141      total += track * (str.length - 1);
1142      cx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x);
1143      g.textAlign = 'left';
1144      for (i = 0; i < str.length; i++) { g.fillText(str.charAt(i), cx, y); cx += w[i] + track; }
1145    }
1146  
1147    function arc(r, a0, a1, width, style, alpha) {
1148      if (a1 - a0 < 0.05 || alpha <= 0.002) return;
1149      g.beginPath(); g.arc(CX, CY, r, rad(a0), rad(a1));
1150      g.lineWidth = width; g.lineCap = 'butt'; g.strokeStyle = style;
1151      g.globalAlpha = alpha; g.stroke(); g.globalAlpha = 1;
1152    }
1153    function wedge(a0, a1, alpha) {            /* swept trail between the chip and the arcs */
1154      if (a1 - a0 < 0.5 || alpha <= 0.002) return;
1155      g.beginPath();
1156      g.arc(CX, CY, R_TRAIL_OUT, rad(a0), rad(a1));
1157      g.arc(CX, CY, R_TRAIL_IN, rad(a1), rad(a0), true);
1158      g.closePath();
1159      g.fillStyle = rzInk(alpha); g.fill();
1160    }
1161    function tickLine(r0, r1, deg) { g.moveTo(px(r0, deg), py(r0, deg)); g.lineTo(px(r1, deg), py(r1, deg)); }
1162  
1163    /* the whole scene is a pure function of loop time t ∈ [0, LOOP) */
1164    function stateAt(t) {
1165      var s = { phase: 0, u: 0, angle: 0, dwell: 0, hold: false, reset: 0, w: [0, 0, 0, 0, 0, 0] };
1166      var i, lt, k, et;
1167      if (t < N * PH) {
1168        i = Math.floor(t / PH); lt = t - i * PH;
1169        s.phase = i;
1170        if (lt < SWEEP) {
1171          s.u = easeInOut(lt / SWEEP);
1172          s.angle = i * SECTOR + SECTOR * s.u;
1173          s.w[i] = 1;
1174        } else {                                  /* settled on the boundary */
1175          s.u = 1; s.dwell = (lt - SWEEP) / DWELL; s.angle = (i + 1) * SECTOR;
1176          k = smooth(s.dwell);
1177          if (i + 1 < N) { s.w[i] = 1 - k; s.w[i + 1] = k; } else { s.w[i] = 1; }
1178        }
1179      } else {                                    /* cycle complete: hold, then soft reset */
1180        s.phase = N - 1; s.u = 1; s.dwell = 1; s.angle = 360; s.hold = true;
1181        et = t - N * PH;
1182        s.reset = et > END_HOLD - RESET ? smooth((et - (END_HOLD - RESET)) / RESET) : 0;
1183        s.w[N - 1] = 1 - s.reset; s.w[0] = s.reset;
1184      }
1185      return s;
1186    }
1187  
1188    function counterText(j, s) {
1189      var ph = PHASES[j], u, m;
1190      if (ph.t0 === null) return ph.read;
1191      u = s.hold ? 1 : (j < s.phase ? 1 : (j > s.phase ? 0 : s.u));
1192      m = ph.t0 + (ph.t1 - ph.t0) * u;
1193      m = Math.floor(m / ph.step) * ph.step;
1194      return fmt(m);
1195    }
1196  
1197    function draw(t) {
1198      var s = stateAt(t), tk = tokens(), dk = smooth(s.dwell);
1199      var j, k, a0, a1, base, wgt, mid, e, p, age, ang, x, y, active, txt;
1200      g.clearRect(0, 0, W, H);
1201      g.textBaseline = 'middle';
1202      if (g.setLineDash) g.setLineDash([]);
1203  
1204      /* ── frame furniture: crop brackets, eyebrow, foot note ── */
1205      g.fillStyle = rzInk(0.3);
1206      g.fillRect(6, 6, 14, 2);          g.fillRect(6, 6, 2, 14);
1207      g.fillRect(W - 20, 6, 14, 2);
1207     g.fillRect(W - 8, 6, 2, 14);
1208      g.fillRect(6, H - 8, 14, 2);      g.fillRect(6, H - 20, 2, 14);
1209      g.fillRect(W - 20, H - 8, 14, 2); g.fillRect(W - 8, H - 20, 2, 14);
1210      g.fillStyle = rzInk(0.85); g.fillRect(16, 19, 18, 2);
1211      g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55);
1212      tracked('RESPONSE CLOCK', 42, 20, 1.3, 'left');
1213      g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.4);
1214      tracked('TIMINGS ILLUSTRATIVE', CX, H - 9, 0.6, 'center');
1215  
1216      /* ── tick ring ── */
1217      g.strokeStyle = rzInk(0.13); g.lineWidth = 1;
1218      g.beginPath(); g.arc(CX, CY, R_RING, 0, Math.PI * 2); g.stroke();
1219      g.strokeStyle = rzInk(0.22); g.lineWidth = 1; g.beginPath();
1220      for (j = 0; j < 60; j++) { if (j % 5) tickLine(194, R_RING, j * 6); }
1221      g.stroke();
1222      g.strokeStyle = rzInk(0.35); g.lineWidth = 1.4; g.beginPath();
1223      for (j = 1; j < 12; j += 2) tickLine(189, R_RING, j * 30);
1224      g.stroke();
1225      g.strokeStyle = rzInk(0.6); g.lineWidth = 1.6; g.beginPath();
1226      for (j = 0; j < N; j++) tickLine(186, 208, j * SECTOR);
1227      g.stroke();
1228  
1229      /* ── swept trail ── */
1230      if (!s.hold) {
1231        if (s.dwell > 0) wedge(s.phase * SECTOR, (s.phase + 1) * SECTOR, 0.055 * (1 - dk));
1232        else wedge(s.phase * SECTOR, s.angle, 0.055);
1233      }
1234  
1235      /* ── phase arcs: pending → accent while active → neutral once done ── */
1236      for (j = 0; j < N; j++) {
1237        a0 = j * SECTOR + GAP; a1 = (j + 1) * SECTOR - GAP;
1238        if (s.hold) base = 0.24 + (0.10 - 0.24) * s.reset;
1239        else if (j < s.phase) base = 0.24;
1240        else if (j === s.phase) base = s.dwell > 0 ? 0.24 : 0.10;
1241        else base = 0.10;
1242        arc(R_ARC, a0, a1, ARC_W, rzInk(base), 1);
1243        if (!s.hold && j === s.phase) {
1244          if (s.dwell > 0) arc(R_ARC, a0, a1, ARC_W, tk.cols[j % 5], 1 - dk);
1245          else {
1246            e = j * SECTOR + Math.min(SECTOR - GAP, Math.max(GAP, SECTOR * s.u));
1247            arc(R_ARC, a0, e, ARC_W, tk.cols[j % 5], 1);
1248          }
1249        }
1250      }
1251  
1252      /* ── rim numerals + micro readouts ── */
1253      for (j = 0; j < N; j++) {
1254        mid = j * SECTOR + SECTOR / 2; wgt = s.w[j];
1255        g.font = "700 12px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.45 + 0.5 * wgt); g.textAlign = 'center';
1256        g.fillText(pad(j + 1), px(R_NUM, mid), py(R_NUM, mid));
1257        g.font = "9px 'Courier New', monospace"; g.fillStyle = rzInk(0.42 + 0.45 * wgt);
1258        g.fillText(PHASES[j].read, px(R_READ, mid), py(R_READ, mid));
1259      }
1260  
1261      /* ── boundary pulses: fired the moment the hand settles ── */
1262      for (j = 0; j < N; j++) {
1263        age = t - (j * PH + SWEEP);
1264        if (age < 0 || age >= PULSE) continue;
1265        p = age / PULSE; ang = (j + 1) * SECTOR; x = px(R_ARC, ang); y = py(R_ARC, ang);
1266        g.strokeStyle = tk.cols[j % 5]; g.fillStyle = tk.cols[j % 5];
1267        g.globalAlpha = Math.pow(1 - p, 1.6) * 0.9; g.lineWidth = 1.4;
1268        g.beginPath(); g.arc(x, y, 4 + 26 * easeOut(p), 0, Math.PI * 2); g.stroke();
1269        g.beginPath(); g.arc(x, y, 2.6, 0, Math.PI * 2); g.fill();
1270        g.globalAlpha = (1 - p) * 0.9; g.lineWidth = 1.8;
1271        g.beginPath(); tickLine(186, 208, ang); g.stroke();
1272        g.globalAlpha = 1;
1273      }
1274  
1275      /* ── hand ── */
1276      g.strokeStyle = rzInk(0.92); g.fillStyle = rzInk(0.92); g.lineWidth = 2; g.lineCap = 'round';
1277      g.beginPath(); g.moveTo(CX, CY); g.lineTo(px(R_HAND, s.angle), py(R_HAND, s.angle)); g.stroke();
1278      g.beginPath(); g.arc(px(R_HAND, s.angle), py(R_HAND, s.angle), 3.2, 0, Math.PI * 2); g.fill();
1279      g.lineCap = 'butt';
1280  
1281      /* ── centre paper chip ── */
1282      g.fillStyle = tk.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1;
1283      g.beginPath(); g.arc(CX, CY, R_DISC, 0, Math.PI * 2); g.fill(); g.stroke();
1284      if (g.setLineDash) {
1285        g.setLineDash([2, 4]); g.strokeStyle = rzInk(0.08);
1286        g.beginPath(); g.arc(CX, CY, R_DISC_IN, 0, Math.PI * 2); g.stroke();
1287        g.setLineDash([]);
1288      }
1289  
1290      /* kicker / numeral / name / accent mark / counter — crossfaded at boundaries */
1291      g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.5);
1292      tracked('PHASE', CX, CY - 47, 1.6, 'center');
1293      active = [];
1294      for (j = 0; j < N; j++) { if (s.w[j] > 0.003) active.push(j); }
1295      for (k = 0; k < active.length; k++) {
1296        j = active[k]; wgt = s.w[j];
1297        g.globalAlpha = wgt;
1298        g.font = "800 30px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.95); g.textAlign = 'center';
1299        g.fillText(pad(j + 1), CX, CY - 14);
1300        g.font = "700 11px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.8);
1301        tracked(PHASES[j].name, CX, CY + 14, 1.1, 'center');
1302        g.fillStyle = tk.cols[j % 5]; g.fillRect(CX - 11, CY + 27, 22, 2);
1303        g.globalAlpha = 1;
1304      }
1305      /* the counter is continuous across a boundary, so never dim it when both texts agree */
1306      g.font = "700 14px 'Courier New', monospace"; g.fillStyle = rzInk(0.9); g.textAlign = 'center';
1307      if (active.length === 2 && counterText(active[0], s) === counterText(active[1], s)) {
1308        g.fillText(counterText(active[0], s), CX, CY + 46);
1309      } else {
1310        for (k = 0; k < active.length; k++) {
1311          j = active[k]; txt = counterText(j, s);
1312          g.globalAlpha = s.w[j]; g.fillText(txt, CX, CY + 46); g.globalAlpha = 1;
1313        }
1314      }
1315    }
1316  
1317    /* ── reduced motion: one composed frame, redrawn on resize / theme / font load ── */
1318    var T_STATIC = 2 * PH + 0.62 * SWEEP;
1319    function drawStatic() { draw(T_STATIC); }
1320    if (reduce) {
1321      drawStatic();
1322      window.addEventListener('resize', drawStatic);
1323      window.addEventListener('load', drawStatic);
1324      document.addEventListener('rz-theme-change', drawStatic);
1325      try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic); } catch (e) {}
1326      return;
1327    }
1328  
1329    /* ── time-based loop; pauses when the tab is hidden or the canvas is off-screen ── */
1330    var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true;
1331    function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); }
1332    function frame(ts) {
1333      raf = 0;
1334      if (!started) { started = true; t0 = ts; }
1335      draw((ts - t0) % LOOP);
1336      if (running) raf = requestAnimationFrame(frame);
1337    }
1338    function start() {
1339      if (running) return;
1340      running = true;
1341      if (pausedAt && started) t0 += now() - pausedAt;
1342      pausedAt = 0;
1343      if (!raf) raf = requestAnimationFrame(frame);
1344    }
1345    function stop() {
1346      if (!running) return;
1347      running = false; pausedAt = now();
1348      if (raf) { cancelAnimationFrame(raf); raf = 0; }
1349    }
1350    function sync() { if (document.hidden || !inView) stop(); else start(); }
1351    document.addEventListener('visibilitychange', sync);
1352    if ('IntersectionObserver' in window) {
1353      new IntersectionObserver(function (entries) { inView = entries[0].isIntersecting; sync(); }, { threshold: 0 }).observe(cv);
1354    }
1355    sync();
1356  })();
1357
1358  /* ── 9. CLOUD SECURITY — Architecture Scan ─────────────────── */
1359  (function () {
1360      var cv = document.getElementById('rz-visual-cloud');
1361      if (!cv) return;
1362      var W = 480, H = 480;
1363      var g = cv.getContext('2d');
1364      if (!g) return;
1365      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
1366  
1367      /* ------------------------------------------------------------
1368         LAYERED PLANES — "Sheet 01" of the page's Architecture Blueprint.
1369         Four isometric planes (Identity → Data) stacked on a z-axis, each
1370         with ink check-nodes. A scan line sweeps the stack; as it crosses a
1371         plane its nodes resolve to a lime check or a crimson cross and the
1372         plane's front edge takes its accent. One finding gets a leader line
1373         (accent anchor on the plane edge, ink tick at the chip) to a paper
1374         chip. Tokens read at runtime, neutrals via rzInk(); time-based and
1375         eased; 12.6 s loop with a long hold; the stack floats as one object.
1376         ------------------------------------------------------------ */
1377  
1378      /* ---- geometry (logical px) ---- */
1379      var N = 4;
1380      var PX = 30, PW = 210, PH = 58, SH = 46, DX = 10, PY0 = 82, GAP = 84;
1381      var ROT = ['purple', 'lime', 'crimson', 'indigo', 'yellow'];   /* site accent rotation */
1382      var PLANES = [
1383        { idx: 'L1', name: 'IDENTITY', nodes: [[0.17, 0.30, 0], [0.42, 0.30, 0], [0.67, 0.30, 0], [0.50, 0.68, 0]] },
1384        { idx: 'L2', name: 'NETWORK',  nodes: [[0.15, 0.30, 0], [0.40, 0.30, 0], [0.65, 0.30, 1], [0.35, 0.68, 0]] },
1385        { idx: 'L3', name: 'COMPUTE',  nodes: [[0.20, 0.32, 0], [0.46, 0.32, 0], [0.72, 0.32, 0]] },
1386        { idx: 'L4', name: 'DATA',     nodes: [[0.16, 0.30, 0], [0.42, 0.30, 0], [0.68, 0.30, 0], [0.32, 0.68, 0]] }
1387      ];
1388      /* a point on plane i: u along the front edge, v back→front (0 = far edge) */
1389      function pt(i, u, v) {
1390        return { x: PX + (N - 1 - i) * DX + SH * (1 - v) + u * PW, y: PY0 + i * GAP + v * PH };
1391      }
1392  
1393      /* ---- timeline (seconds) ---- */
1394      var T_IN = 0.55, MOVE = 1.15, HOLD = 0.45, SEG = MOVE + HOLD;
1395      var T_SCAN_END = T_IN + N * SEG;                 /* 6.95 — line leaves the stack */
1396      var T_RESET = 11.2, RESET_D = 0.7, T_LOOP = 12.6;
1397  
1398      function clamp01(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
1399      function inOut3(x) { x = clamp01(x); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
1400      function invInOut3(q) { q = clamp01(q); return q < 0.5 ? Math.pow(q / 4, 1 / 3) : 1 - Math.pow(2 * (1 - q), 1 / 3) / 2; }
1401      function out3(x) { x = clamp01(x); return 1 - Math.pow(1 - x, 3); }
1402      function out5(x) { x = clamp01(x); return 1 - Math.pow(1 - x, 5); }
1403  
1404      /* scan waypoints: rest above each plane, exit below the last */
1405      var WAY = [], i, k, p, n;
1406      for (i = 0; i < N; i++) {
1407        PLANES[i].top = PY0 + i * GAP;
1408        PLANES[i].bot = PLANES[i].top + PH;
1409        PLANES[i].acc = ROT[i % ROT.length];
1410        PLANES[i].list = [];
1411        PLANES[i].tDone = T_IN + i * SEG + MOVE;     /* plane fully crossed */
1412        WAY.push(PLANES[i].top - 16);
1413      }
1414      WAY.push(PLANES[N - 1].bot + 16);
1415  
1416      function scanY(t) {
1417        var s = t - T_IN;
1418        if (s <= 0) return WAY[0];
1419        var seg = Math.floor(s / SEG);
1420        if (seg >= N) return WAY[N];
1421        var u = s - seg * SEG, q = u < MOVE ? inOut3(u / MOVE) : 1;
1422        return WAY[seg] + (WAY[seg + 1] - WAY[seg]) * q;
1423      }
1424      function scanAlpha(t) {
1425        if (t < T_IN) return out3((t - 0.1) / 0.45);
1426        if (t < T_SCAN_END) return 1;
1427        return 1 - out3((t - T_SCAN_END) / 0.5);
1428      }
1429  
1430      /* nodes, with the exact moment the line crosses each one (inverse of the ease) */
1431      var nodes = [], finding = null;
1432      for (i = 0; i < N; i++) {
1433        for (k = 0; k < PLANES[i].nodes.length; k++) {
1434          p = PLANES[i].nodes[k];
1435          var q = pt(i, p[0], p[1]);
1436          n = { x: q.x, y: q.y, u: p[0], v: p[1], plane: i, bad: p[2] === 1 };
1437          n.tHit = T_IN + i * SEG + MOVE * invInOut3((n.y - WAY[i]) / (WAY[i + 1] - WAY[i]));
1438          nodes.push(n); PLANES[i].list.push(n);
1439          if (n.bad && !finding) finding = n;
1440        }
1441      }
1442      var ANCHOR_X = pt(finding.plane, 1, finding.v).x;   /* plane right edge at the finding's depth */
1443      var T_CHIP = finding.tHit + 0.5;
1444  
1445      /* ---- colour tokens: read once per frame, parsed to triplets ---- */
1446      var FALL = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414' };
1447      function parseCol(s) {
1448        s = (s || '').replace(/^\s+|\s+$/g, '');
1449        var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s), h, v;
1450        if (m) {
1451          h = m[1];
1452          if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
1453          v = parseInt(h, 16);
1454          return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
1455        }
1456        m = /rgba?\(([^)]+)\)/.exec(s);
1457        if (m) { v = m[1].split(','); return [parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2])]; }
1458        return null;
1459      }
1460      function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }
1461      function readTokens() {
1462        var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark';
1463        try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
1464        function tok(name, fb) {
1465          var v = null;
1466          if (cs) { try { v = parseCol(cs.getPropertyValue(name)); } catch (e2) {} }
1467          return v || parseCol(fb);
1468        }
1469        return {
1470          dark: dark,
1471          purple:  tok('--rz-purple',  FALL.purple),
1472          indigo:  tok('--rz-indigo',  FALL.indigo),
1473          crimson: tok('--rz-crimson', FALL.crimson),
1474          lime:    tok('--rz-lime',    FALL.lime),
1475          yellow:  tok('--rz-yellow',  FALL.yellow),
1476          paper:   tok('--rz-paper',   dark ? '#0e0f14' : '#ffffff')
1477        };
1478      }
1479  
1480      /* ---- drawing helpers ---- */
1481      var F_EYE  = '600 9.5px "Oxanium", sans-serif';
1482      var F_NAME = '700 10px "Oxanium", sans-serif';
1483      var F_MONO = '8.5px "Courier New", Courier, monospace';
1484      var F_MONO_B = '700 8.5px "Courier New", Courier, monospace';
1485  
1486      /* letter-spaced text (canvas letterSpacing is not universal); returns the width */
1487      function tracked(str, x, y, track, align) {
1488        var w = 0, j, ch, cx;
1489        for (j = 0; j < str.length; j++) w += g.measureText(str.charAt(j)).width;
1490        w += track * (str.length - 1);
1491        cx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x;
1492        g.textAlign = 'left';
1493        for (j = 0; j < str.length; j++) { ch = str.charAt(j); g.fillText(ch, cx, y); cx += g.measureText(ch).width + track; }
1494        return w;
1495      }
1496      function pad2(v) { return (v < 10 ? '0' : '') + v; }
1497      function roundRect(x, y, w, h, r) {
1498        g.beginPath();
1499        g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r);
1500        g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1501        g.lineTo(x + r, y + h); g.quadraticCurveTo(x, y + h, x, y + h - r);
1502        g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); g.closePath();
1503      }
1504      /* stroke the first `prog` (0..1) of a polyline — marks draw in stroke by stroke */
1505      function partialPoly(pts, prog) {
1506        var total = 0, segs = [], j, dx, dy, L, left, f;
1507        for (j = 1; j < pts.length; j++) { dx = pts[j][0] - pts[j - 1][0];
1507 dy = pts[j][1] - pts[j - 1][1]; L = Math.sqrt(dx * dx + dy * dy); segs.push(L); total += L; }
1508        left = total * clamp01(prog);
1509        if (left <= 0) return;
1510        g.beginPath(); g.moveTo(pts[0][0], pts[0][1]);
1511        for (j = 1; j < pts.length; j++) {
1512          if (left >= segs[j - 1]) { g.lineTo(pts[j][0], pts[j][1]); left -= segs[j - 1]; }
1513          else { f = segs[j - 1] ? left / segs[j - 1] : 0; g.lineTo(pts[j - 1][0] + (pts[j][0] - pts[j - 1][0]) * f, pts[j - 1][1] + (pts[j][1] - pts[j - 1][1]) * f); break; }
1514        }
1515        g.stroke();
1516      }
1517      function checkMark(x, y, prog) { partialPoly([[x - 2.8, y + 0.2], [x - 0.9, y + 2.1], [x + 3.0, y - 2.4]], prog); }
1518      function crossMark(x, y, prog) {
1519        partialPoly([[x - 2.5, y - 2.5], [x + 2.5, y + 2.5]], prog * 2);
1520        if (prog > 0.5) partialPoly([[x + 2.5, y - 2.5], [x - 2.5, y + 2.5]], (prog - 0.5) * 2);
1521      }
1522  
1523      /* ---------------- sheet chrome (static; does not float) ---------------- */
1524      function drawSheet(C, t, F, done, pass, fail) {
1525        var inset = 8, arm = 14, j, x, y, sx, sy;
1526        /* corner crop-brackets */
1527        g.strokeStyle = rzInk(0.3); g.lineWidth = 1.25; g.lineCap = 'butt';
1528        for (j = 0; j < 4; j++) {
1529          x = j & 1 ? W - inset : inset; y = j >> 1 ? H - inset : inset; sx = j & 1 ? -1 : 1; sy = j >> 1 ? -1 : 1;
1530          g.beginPath(); g.moveTo(x, y + sy * arm); g.lineTo(x, y); g.lineTo(x + sx * arm, y); g.stroke();
1531        }
1532        /* head: eyebrow · five equal swatches · sheet readout */
1533        g.textBaseline = 'alphabetic';
1534        g.fillStyle = rzInk(0.55); g.font = F_EYE;
1535        var ew = tracked('ARCHITECTURE BLUEPRINT', 30, 33, 1.5, 'left');
1536        for (j = 0; j < 5; j++) { g.fillStyle = rgba(C[ROT[j]], 0.95); g.fillRect(30 + ew + 12 + j * 9, 26, 6, 6); }
1537        /* live plane count crossfades through the reset */
1538        var mix = Math.abs(2 * F - 1), shown = F >= 0.5 ? done : 0;
1539        g.fillStyle = rzInk(0.58 * (0.3 + 0.7 * mix)); g.font = F_MONO;
1540        tracked('SHEET 01 · SCAN ' + pad2(shown) + '/' + pad2(N), W - 30, 33, 0.8, 'right');
1541        /* ruler */
1542        var rx0 = 30, rx1 = W - 30, ry = 46;
1543        g.strokeStyle = rzInk(0.16); g.lineWidth = 1;
1544        g.beginPath(); g.moveTo(rx0, ry + 0.5); g.lineTo(rx1, ry + 0.5); g.stroke();
1545        g.strokeStyle = rzInk(0.26);
1546        g.beginPath();
1547        for (j = 0; rx0 + j <= rx1; j += 8) { var h = j % 40 === 0 ? 7 : 3; g.moveTo(rx0 + j + 0.5, ry); g.lineTo(rx0 + j + 0.5, ry - h); }
1548        g.stroke();
1549        /* foot: hairline, mark legend, live tally */
1550        var fy = H - 30;
1551        g.strokeStyle = rzInk(0.1); g.beginPath(); g.moveTo(30, fy - 13.5); g.lineTo(W - 30, fy - 13.5); g.stroke();
1552        g.lineWidth = 1.5; g.lineCap = 'round'; g.lineJoin = 'round';
1553        g.strokeStyle = rgba(C.lime, 0.95); checkMark(35, fy - 0.5, 1);
1554        g.fillStyle = rzInk(0.58); g.font = F_MONO; g.textBaseline = 'middle';
1555        var lw = tracked('PASS', 43, fy, 0.8, 'left');
1556        g.strokeStyle = rgba(C.crimson, 0.95); crossMark(43 + lw + 14, fy - 0.5, 1);
1557        g.fillStyle = rzInk(0.58);
1558        tracked('FINDING', 43 + lw + 22, fy, 0.8, 'left');
1559        var sp = F >= 0.5 ? pass : 0, sf = F >= 0.5 ? fail : 0;
1560        g.fillStyle = rzInk(0.58 * (0.3 + 0.7 * mix));
1561        tracked('PASS ' + pad2(sp) + '  ·  FINDINGS ' + pad2(sf), W - 30, fy, 0.8, 'right');
1562        g.textBaseline = 'alphabetic'; g.lineCap = 'butt';
1563      }
1564  
1565      /* ---------------- one plane ---------------- */
1566      function drawPlane(C, i, t, F, sy, sA) {
1567        var P = PLANES[i], a = pt(i, 0, 0), b = pt(i, 1, 0), c = pt(i, 1, 1), d = pt(i, 0, 1), j, r;
1568        /* translucent paper face */
1569        g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.lineTo(c.x, c.y); g.lineTo(d.x, d.y); g.closePath();
1570        g.fillStyle = rgba(C.paper, 0.86); g.fill();
1571        /* dotted isometric grid */
1572        g.fillStyle = rzInk(0.17);
1573        var rows = [0.17, 0.335, 0.5, 0.665, 0.83], vv, uu;
1574        for (j = 0; j < rows.length; j++) {
1575          vv = rows[j];
1576          for (uu = 8; uu <= PW - 8; uu += 12) {
1577            r = pt(i, uu / PW, vv);
1578            g.beginPath(); g.arc(r.x, r.y, 0.9, 0, 6.2832); g.fill();
1579          }
1580        }
1581        /* outline */
1582        g.strokeStyle = rzInk(0.34); g.lineWidth = 1; g.lineJoin = 'miter';
1583        g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.lineTo(c.x, c.y); g.lineTo(d.x, d.y); g.closePath(); g.stroke();
1584        /* front edge: ink depth strip always; accent strip grows as the line crosses the plane */
1585        g.fillStyle = rzInk(0.12); g.fillRect(d.x, d.y + 4, PW, 3);
1586        var prog = clamp01((sy - P.top) / PH) * F;
1587        if (prog > 0) { g.fillStyle = rgba(C[P.acc], 1); g.fillRect(d.x, d.y + 1, PW * prog, 3); }
1588        /* dashed topology edge through the nodes */
1589        g.strokeStyle = rzInk(0.22); g.lineWidth = 1; g.setLineDash([3, 3]);
1590        g.beginPath();
1591        for (j = 0; j < P.list.length; j++) { if (j === 0) g.moveTo(P.list[j].x, P.list[j].y); else g.lineTo(P.list[j].x, P.list[j].y); }
1592        g.stroke(); g.setLineDash([]);
1593        /* caption, bottom-left, on a paper halo */
1594        var cap = pt(i, 0.03, 0.87);
1595        g.font = F_MONO; var w1 = 0, cw;
1596        for (j = 0; j < P.idx.length; j++) w1 += g.measureText(P.idx.charAt(j)).width + 1;
1597        g.font = F_NAME; cw = w1 + 6;
1598        for (j = 0; j < P.name.length; j++) cw += g.measureText(P.name.charAt(j)).width + 1.4;
1599        g.fillStyle = rgba(C.paper, 1); g.fillRect(cap.x - 3, cap.y - 7, cw + 6, 12);
1600        g.textBaseline = 'alphabetic';
1601        g.fillStyle = rzInk(0.55); g.font = F_MONO;
1602        var lw = tracked(P.idx, cap.x, cap.y + 3, 1, 'left');
1603        g.fillStyle = rzInk(0.88); g.font = F_NAME;
1604        tracked(P.name, cap.x + lw + 7, cap.y + 3, 1.4, 'left');
1605        /* severity marks, top-left, once the plane has been crossed (finding planes only) */
1606        var nf = 0;
1607        for (j = 0; j < P.list.length; j++) if (P.list[j].bad) nf++;
1608        if (nf > 0 && t >= P.tDone && F > 0) {
1609          var sa = out3((t - P.tDone) / 0.45) * F, s0 = pt(i, 0.06, 0.2);
1610          g.globalAlpha = sa;
1611          for (j = 0; j < nf; j++) { g.fillStyle = rzInk(0.9); g.fillRect(s0.x + j * 10, s0.y - 3.5, 7, 7); }
1612          g.globalAlpha = 1;
1613        }
1614        /* nodes */
1615        for (j = 0; j < P.list.length; j++) drawNode(C, P.list[j], t, F);
1616      }
1617  
1618      function drawNode(C, n, t, F) {
1619        var hit = t >= n.tHit && F > 0, col = n.bad ? C.crimson : C.lime, r = 5.5, since = hit ? t - n.tHit : 0, q;
1620        /* resolve: one soft ring expanding out of the node */
1621        if (hit && since < 0.9) {
1622          q = since / 0.9;
1623          g.strokeStyle = rgba(col, 0.6 * Math.pow(1 - q, 1.6) * F); g.lineWidth = 1;
1624          g.beginPath(); g.arc(n.x, n.y, r + 15 * out3(q), 0, 6.2832); g.stroke();
1625        }
1626        /* the finding keeps a slow breathing ring */
1627        if (hit && n.bad) {
1628          g.strokeStyle = rgba(col, (0.18 + 0.1 * Math.sin(t * 6.2832 / 3.4)) * F); g.lineWidth = 1;
1629          g.beginPath(); g.arc(n.x, n.y, 11, 0, 6.2832); g.stroke();
1630        }
1631        /* node */
1632        g.beginPath(); g.arc(n.x, n.y, r, 0, 6.2832);
1633        g.fillStyle = rgba(C.paper, 1); g.fill();
1634        g.strokeStyle = rzInk(0.42 + (hit ? 0.46 * F : 0)); g.lineWidth = hit ? 1.2 : 1.1; g.stroke();
1635        /* mark */
1636        if (hit) {
1637          var mp = out5(since / 0.5);
1638          g.strokeStyle = rgba(col, F); g.lineWidth = 1.6; g.lineCap = 'round'; g.lineJoin = 'round';
1639          if (n.bad) crossMark(n.x, n.y, mp); else checkMark(n.x, n.y, mp);
1640          g.lineCap = 'butt';
1641        }
1642      }
1643  
1644      /* ---------------- leader + paper chip ---------------- */
1645      function drawCallout(C, t, F) {
1646        if (t < T_CHIP || F <= 0) return;
1647        var lp = out3((t - T_CHIP) / 0.55);
1648        var ca = out3((t - T_CHIP - 0.3) / 0.35) * F;
1649        var n = finding, ny = n.y, label = 'PLANE ' + pad2(n.plane + 1) + ' · 1 FINDING', tw = 0, j;
1650        g.font = F_MONO_B;
1651        for (j = 0; j < label.length; j++) tw += g.measureText(label.charAt(j)).width + 0.6;
1652        var chipH = 22, chipW = Math.round(tw + 26), chipX = W - 22 - chipW, chipY = Math.round(ny - chipH / 2);
1653        if (chipX < ANCHOR_X + 24) chipX = ANCHOR_X + 24;
1654        var L1 = ANCHOR_X - (n.x + 7), L2 = chipX - 3 - ANCHOR_X, run = (L1 + L2) * lp;
1655        /* stub across the plane, accent anchor on its edge, hairline on to the chip */
1656        g.lineWidth = 1;
1657        g.strokeStyle = rzInk(0.42 * F);
1658        g.beginPath(); g.moveTo(n.x + 7, ny + 0.5); g.lineTo(n.x + 7 + Math.min(run, L1), ny + 0.5); g.stroke();
1659        if (run > L1) {
1660          g.strokeStyle = rzInk(0.3 * F);
1661          g.beginPath(); g.moveTo(ANCHOR_X, ny + 0.5); g.lineTo(ANCHOR_X + Math.min(run - L1, L2), ny + 0.5); g.stroke();
1662          var as = out3((run - L1) / 10);
1663          g.beginPath(); g.arc(ANCHOR_X, ny, 3.5 * as, 0, 6.2832);
1664          g.fillStyle = rgba(C.crimson, F); g.fill();
1665          g.strokeStyle = rgba(C.paper, F); g.lineWidth = 1.5; g.stroke();
1666        }
1667        if (lp >= 1) {
1668          g.strokeStyle = rzInk(0.9 * F); g.lineWidth = 1;
1669          g.beginPath(); g.moveTo(chipX - 2.5, ny - 5.5); g.lineTo(chipX - 2.5, ny + 5.5); g.stroke();
1670        }
1671        if (ca <= 0) return;
1672        /* chip: paper, 1px ink hairline, crimson bar, soft lift */
1673        g.save();
1674        g.globalAlpha = ca;
1675        g.shadowColor = C.dark ? 'rgba(0,0,0,0.55)' : 'rgba(20,21,26,0.10)';
1676        g.shadowBlur = 12; g.shadowOffsetY = 4;
1677        roundRect(chipX + 0.5, chipY + 0.5, chipW, chipH, 4);
1678        g.fillStyle = rgba(C.paper, 1); g.fill();
1679        g.shadowColor = 'transparent'; g.shadowBlur = 0; g.shadowOffsetY = 0;
1680        g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
1681        g.save(); roundRect(chipX + 0.5, chipY + 0.5, chipW, chipH, 4); g.clip();
1682        g.fillStyle = rgba(C.crimson, 1); g.fillRect(chipX, chipY, 3, chipH + 1);
1683        g.restore();
1684        g.fillStyle = rzInk(0.85); g.font = F_MONO_B; g.textBaseline = 'middle';
1685        tracked(label, chipX + 14, ny + 1, 0.6, 'left');
1686        g.textBaseline = 'alphabetic';
1687        g.restore();
1688      }
1689  
1690      /* ---------------- scan line: hugs the plane it is crossing, re-aims during the hold ---------------- */
1691      function scanExtent(t) {
1692        var s = t - T_IN, seg = s <= 0 ? 0 : Math.floor(s / SEG), u = s - seg * SEG, k0, k1, m;
1693        if (seg >= N) { seg = N - 1; u = MOVE; }
1694        k0 = seg; k1 = Math.min(N - 1, seg + 1);
1695        m = (s > 0 && u > MOVE) ? inOut3((u - MOVE) / HOLD) : 0;
1696        var l0 = pt(k0, 0, 1).x, l1 = pt(k1, 0, 1).x, r0 = pt(k0, 1, 0).x, r1 = pt(k1, 1, 0).x;
1697        return [l0 + (l1 - l0) * m - 10, r0 + (r1 - r0) * m + 10];
1698      }
1699      function drawScan(C, t, sy, sA) {
1700        if (sA <= 0) return;
1701        var ex = scanExtent(t), x0 = ex[0], x1 = ex[1];
1702        var grad = g.createLinearGradient(0, sy - 18, 0, sy);
1703        grad.addColorStop(0, rzInk(0)); grad.addColorStop(1, rzInk(0.06 * sA));
1704        g.fillStyle = grad; g.fillRect(x0, sy - 18, x1 - x0, 18);
1705        g.strokeStyle = rzInk(0.72 * sA); g.lineWidth = 1;
1706        g.beginPath(); g.moveTo(x0, sy + 0.5); g.lineTo(x1, sy + 0.5); g.stroke();
1707        g.beginPath();
1708        g.moveTo(x0 + 0.5, sy - 4); g.lineTo(x0 + 0.5, sy + 5);
1709        g.moveTo(x1 - 0.5, sy - 4); g.lineTo(x1 - 0.5, sy + 5);
1710        g.stroke();
1711      }
1712  
1713      /* ---------------- compose one frame: t = loop time, ta = absolute time (float) ---------------- */
1714      function draw(t, ta) {
1715        var C = readTokens(), j;
1716        var F = t < T_RESET ? 1 : 1 - inOut3((t - T_RESET) / RESET_D);
1717        var sy = scanY(t), sA = clamp01(scanAlpha(t));
1718        var done = 0, pass = 0, fail = 0;
1719        for (j = 0; j < N; j++) if (t >= PLANES[j].tDone) done++;
1720        for (j = 0; j < nodes.length; j++) if (t >= nodes[j].tHit) { if (nodes[j].bad) fail++; else pass++; }
1721  
1722        g.clearRect(0, 0, W, H);
1723        drawSheet(C, t, F, done, pass, fail);
1724  
1725        /* the stack floats as one object (absolute time, so the loop seam never jumps) */
1726        var fx = ta ? 1.5 * Math.sin(ta * 6.2832 / 9.3) : 0;
1727        var fy = ta ? 3.0 * Math.sin(ta * 6.2832 / 6.7) : 0;
1728        g.save(); g.translate(fx, fy);
1729        /* dashed z-axis risers joining the planes into one stack */
1730        var d0 = pt(0, 0, 1), d3 = pt(N - 1, 0, 1), b0 = pt(0, 1, 0), b3 = pt(N - 1, 1, 0);
1731        g.strokeStyle = rzInk(0.2); g.lineWidth = 1; g.setLineDash([2, 4]);
1732        g.beginPath(); g.moveTo(d3.x, d3.y); g.lineTo(d0.x, d0.y); g.moveTo(b3.x, b3.y); g.lineTo(b0.x, b0.y); g.stroke();
1733        g.setLineDash([]);
1734        for (j = 0; j < N; j++) drawPlane(C, j, t, F, sy, sA);
1735        drawCallout(C, t, F);
1736        drawScan(C, t, sy, sA);
1737        g.restore();
1738      }
1739  
1740      /* ---------------- loop / reduced motion / visibility ---------------- */
1741      var reduce = false;
1742      try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
1743      function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); }
1744  
1745      if (reduce) {
1746        /* one composed frame: scan complete, marks and chip resolved, no float */
1747        var still = function () { draw(9.0, 0); };
1748        still();
1749        if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(still);
1750        window.addEventListener('resize', still);
1751        window.addEventListener('load', still);
1752        document.addEventListener('rz-theme-change', still);
1753        return;
1754      }
1755  
1756      var raf = 0, t0 = now(), pausedAt = 0;
1757      function frame() {
1758        raf = 0;
1759        var el = (now() - t0) / 1000;
1760        draw(el % T_LOOP, el);
1761        raf = requestAnimationFrame(frame);
1762      }
1763      function start() { if (!raf) raf = requestAnimationFrame(frame); }
1764      function stop() { if (raf) { cancelAnimationFrame(raf); raf = 0; } }
1765      document.addEventListener('visibilitychange', function () {
1766        if (document.hidden) { pausedAt = now(); stop(); }
1767        else { if (pausedAt) { t0 += now() - pausedAt; pausedAt = 0; } start(); }
1768      });
1769      if (document.hidden) pausedAt = now(); else start();
1770    })();
1771
1772  /* ── 10. CODE REVIEW — SAST Vulnerability Scanner ──────────── */
1773  (function () {
1774      /* ── 10. CODE REVIEW — Code Scan (diff pane on paper) ─────────
1775         A paper diff pane in the language of the RZ-METHOD:codereview
1776         section: Courier line numbers + sign column, ink token bars for
1777         code, a review cursor that reads line by line, and three findings
1778         that get a −/+ accent gutter pair, a numbered accent-ring pin and
1779         a paper cue chip (INJECTION · AUTHZ · SECRET). Everything neutral
1780         goes through rzInk(); paper + accents are read from the CSS
1781         tokens each frame, so dark mode follows automatically.
1782         Timeline is time-based (~13.5 s loop with an end hold), eased,
1783         pauses when the tab is hidden, and collapses to one composed
1784         static frame under prefers-reduced-motion.                   */
1785      var cv = document.getElementById('rz-visual-code');
1786      if (!cv) return;
1787      var W = 480, H = 480;
1788      var g = cv.getContext('2d');
1789      if (!g) return;
1790      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
1791  
1792      /* ---- tokens: read each frame, light values as fallback ---- */
1793      var FB = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' };
1794      function readTokens() {
1795        var cs = null, t = {};
1796        try { cs = window.getComputedStyle(document.documentElement); } catch (e) { cs = null; }
1797        function tok(name, fb) {
1798          var v = '';
1799          if (cs) { try { v = (cs.getPropertyValue(name) || '').replace(/^\s+|\s+$/g, ''); } catch (e) { v = ''; } }
1800          return v || fb;
1801        }
1802        t.purple  = tok('--rz-purple',  FB.purple);
1803        t.indigo  = tok('--rz-indigo',  FB.indigo);
1804        t.crimson = tok('--rz-crimson', FB.crimson);
1805        t.lime    = tok('--rz-lime',    FB.lime);
1806        t.yellow  = tok('--rz-yellow',  FB.yellow);
1807        t.paper   = tok('--rz-paper',   FB.paper);
1808        t.dark    = document.documentElement.getAttribute('data-theme') === 'dark';
1809        return t;
1810      }
1811      function withAlpha(col, a) {
1812        var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(col);
1813        if (!m) return col;
1814        var h = m[1];
1815        if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
1816        var n = parseInt(h, 16);
1817        return 'rgba(' + (n >> 16 & 255) + ',' + (n >> 8 & 255) + ',' + (n & 255) + ',' + a + ')';
1818      }
1819  
1820      /* ---- easing / helpers ---- */
1821      function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
1822      function easeInOutCubic(u) { u = clamp01(u); return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2; }
1823      function easeOutCubic(u) { u = clamp01(u); return 1 - Math.pow(1 - u, 3); }
1824      function lerp(a, b, u) { return a + (b - a) * u; }
1825      function rrect(x, y, w, h, tl, tr, br, bl) {
1826        g.beginPath();
1827        g.moveTo(x + tl, y);
1828        g.lineTo(x + w - tr, y);  g.arcTo(x + w, y, x + w, y + tr, tr);
1829        g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br);
1830        g.lineTo(x + bl, y + h);  g.arcTo(x, y + h, x, y + h - bl, bl);
1831        g.lineTo(x, y + tl);      g.arcTo(x, y, x + tl, y, tl);
1832        g.closePath();
1833      }
1834      /* tracked text (canvas letterSpacing is not universal): returns width */
1835      function spaced(str, x, y, sp, align) {
1836        var i, w = 0, ws = [];
1837        for (i = 0; i < str.length; i++) { ws[i] = g.measureText(str.charAt(i)).width;
1837 w += ws[i]; }
1838        w += sp * (str.length - 1);
1839        var cx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x;
1840        var keep = g.textAlign; g.textAlign = 'left';
1841        for (i = 0; i < str.length; i++) { g.fillText(str.charAt(i), cx, y); cx += ws[i] + sp; }
1842        g.textAlign = keep;
1843        return w;
1844      }
1845      var OX = "'Oxanium', sans-serif", MONO = "'Courier New', monospace";
1846  
1847      /* ---- geometry (logical 480×480) ---- */
1848      var CX0 = 22, CY0 = 26, CW = 436, CH = 428;      /* the pane */
1849      var CX1 = CX0 + CW, CY1 = CY0 + CH;              /* 458 / 454 */
1850      var HEAD_H = 42;                                 /* pane header band */
1851      var ROW_H = 22, N = 14, ROWS_Y = CY0 + HEAD_H + 12;
1852      var BODY_Y1 = ROWS_Y + N * ROW_H;                /* 388 */
1853      var FOOT_Y = BODY_Y1 + 16, HAZ_H = 7;            /* status bar + hazard strip */
1854      var FOOT_H = CY1 - HAZ_H - FOOT_Y;
1855      var LN_X = CX0 + 30, SIGN_X = CX0 + 40, CODE_X = CX0 + 50, INDENT = 16;
1856      var BAR_H = 6, GAP = 6, CHIP_R = CX1 - 16;
1857  
1858      /* ---- mock code: [width, ink alpha] token bars per row ---- */
1859      var ROWS = [
1860        { ln: 82, ind: 0, seg: [[22, .38], [70, .24], [34, .16]] },
1861        { ln: 83, ind: 1, seg: [[46, .24], [10, .16], [58, .18]] },
1862        { ln: 84, ind: 1, seg: [[28, .38], [36, .24], [62, .14]] },
1863        { ln: 85, ind: 1, seg: [[40, .24], [14, .16], [96, .14]], find: 0 },
1864        { ln: 85, ind: 1, seg: [[40, .24], [14, .16], [72, .18], [30, .14]], fix: 0 },
1865        { ln: 86, ind: 1, seg: [[22, .38], [52, .24]] },
1866        { ln: 87, ind: 0, seg: [[22, .38], [78, .24], [40, .16]] },
1867        { ln: 88, ind: 1, seg: [[30, .38], [54, .24], [40, .16]], find: 1 },
1868        { ln: 88, ind: 1, seg: [[22, .38], [34, .24], [12, .16], [64, .18]], fix: 1 },
1869        { ln: 89, ind: 2, seg: [[44, .24], [30, .16]] },
1870        { ln: 90, ind: 1, seg: [[30, .38], [60, .24]] },
1871        { ln: 91, ind: 1, seg: [[46, .24], [10, .16], [88, .14]], find: 2 },
1872        { ln: 91, ind: 1, seg: [[46, .24], [10, .16], [40, .24], [44, .16]], fix: 2 },
1873        { ln: 92, ind: 1, seg: [[30, .38], [48, .24], [22, .16]] }
1874      ];
1875      var FINDINGS = [
1876        { row: 3,  fixRow: 4,  label: 'INJECTION', acc: 'purple'  },
1877        { row: 7,  fixRow: 8,  label: 'AUTHZ',     acc: 'crimson' },
1878        { row: 11, fixRow: 12, label: 'SECRET',    acc: 'indigo'  }
1879      ];
1880      function rowBarsEnd(row) {
1881        var x = CODE_X + row.ind * INDENT, i;
1882        for (i = 0; i < row.seg.length; i++) x += row.seg[i][0] + (i ? GAP : 0);
1883        return x;
1884      }
1885  
1886      /* ---- timeline (seconds) ---- */
1887      var T_HOLD0 = 0.8, MOVE = 0.24, DW_CTX = 0.22, DW_FIND = 1.15, DW_FIX = 0.6;
1888      var arrive = [], dwell = [], i, tt = T_HOLD0;
1889      for (i = 0; i < N; i++) {
1890        arrive[i] = tt;
1891        dwell[i] = ROWS[i].find != null ? DW_FIND : ROWS[i].fix != null ? DW_FIX : DW_CTX;
1892        tt += dwell[i] + MOVE;
1893      }
1894      var T_END = arrive[N - 1] + dwell[N - 1];       /* cursor finished reading */
1895      var CUR_OUT = 0.4, HOLD_END = 1.6, RESET = 0.6;
1896      var DUR = T_END + CUR_OUT + HOLD_END + RESET;   /* ≈ 13.5 s */
1897      var T_STATIC = arrive[N - 1] + 0.1;             /* composed frame for reduced motion */
1898  
1899      function cursorRow(t) {
1900        if (t <= arrive[0]) return 0;
1901        for (var k = 0; k < N; k++) {
1902          var leave = arrive[k] + dwell[k];
1903          if (t <= leave || k === N - 1) return k;
1904          if (t < arrive[k + 1]) return k + easeInOutCubic((t - leave) / (arrive[k + 1] - leave));
1905        }
1906        return N - 1;
1907      }
1908  
1909      /* ---- one frame ---- */
1910      function draw(t) {
1911        g.clearRect(0, 0, W, H);
1912        var tk = readTokens();
1913        var ACC = { purple: tk.purple, indigo: tk.indigo, crimson: tk.crimson, lime: tk.lime, yellow: tk.yellow };
1914        var SPECTRUM = [tk.purple, tk.crimson, tk.lime, tk.indigo, tk.yellow];
1915  
1916        var gfade = t > DUR - RESET ? 1 - easeInOutCubic((t - (DUR - RESET)) / RESET) : 1;
1917        var cRow = cursorRow(t);
1918        var cAlpha = t < 0.4 ? easeOutCubic(t / 0.4) : t < T_END ? 1 : 1 - easeInOutCubic((t - T_END) / CUR_OUT);
1919        var readAll = easeOutCubic((t - T_END) / 0.4);
1920        var numA = (t < 0.3 ? easeOutCubic(t / 0.3) : 1) * gfade;   /* live counters fade through the loop seam */
1921        var k, r, x, y, yc, j;
1922  
1923        /* finding / fix reveal envelopes */
1924        var F = [];
1925        var nMinus = 0, nPlus = 0;
1926        for (k = 0; k < FINDINGS.length; k++) {
1927          var a = arrive[FINDINGS[k].row], a2 = arrive[FINDINGS[k].fixRow];
1928          F[k] = {
1929            e1: easeOutCubic((t - a) / 0.45) * gfade,          /* gutter, sign, strike */
1930            e2: easeOutCubic((t - a - 0.10) / 0.40) * gfade,   /* pin */
1931            e3: easeInOutCubic((t - a - 0.20) / 0.40) * gfade, /* leader */
1932            e4: easeOutCubic((t - a - 0.40) / 0.45) * gfade,   /* chip */
1933            f1: easeOutCubic((t - a2) / 0.45) * gfade,         /* fix row */
1934            a2: a2
1935          };
1936          if (t >= a) nMinus++;
1937          if (t >= a2) nPlus++;
1938        }
1939  
1940        /* ── pane: paper card with soft lift, 4/12/12/12 radius ── */
1941        g.save();
1942        g.shadowColor = tk.dark ? 'rgba(0,0,0,0.35)' : 'rgba(20,21,26,0.06)';
1943        g.shadowBlur = 14; g.shadowOffsetY = 2;
1944        rrect(CX0, CY0, CW, CH, 4, 12, 12, 12);
1945        g.fillStyle = tk.paper; g.fill();
1946        g.restore();
1947  
1948        g.save();
1949        rrect(CX0, CY0, CW, CH, 4, 12, 12, 12);
1950        g.clip();
1951  
1952        /* header band */
1953        g.fillStyle = rzInk(0.05);
1954        g.fillRect(CX0, CY0, CW, HEAD_H);
1955        g.fillStyle = rzInk(0.13);
1956        g.fillRect(CX0, CY0 + HEAD_H, CW, 1);
1957  
1958        /* ruler ticks along the top threshold (masked at both ends) */
1959        for (x = CX0 + 12; x < CX1 - 4; x += 24) {
1960          var u = (x - CX0) / CW, mask = u < 0.12 ? u / 0.12 : u > 0.88 ? (1 - u) / 0.12 : 1;
1961          g.fillStyle = rzInk(0.24 * mask);
1962          g.fillRect(x, CY0, 1, 9);
1963        }
1964  
1965        /* header readouts */
1966        g.textBaseline = 'middle';
1967        yc = CY0 + HEAD_H / 2 + 1;
1968        g.textAlign = 'left';
1969        g.font = '9px ' + MONO; g.fillStyle = rzInk(0.5);
1970        g.fillText('c4d2a90', CX0 + 14, yc);
1971        var hw = g.measureText('c4d2a90').width;
1972        g.font = 'bold 9.5px ' + MONO; g.fillStyle = rzInk(0.92);
1973        g.fillText('api/orders.py', CX0 + 14 + hw + 10, yc);
1974        g.textAlign = 'right';
1975        g.font = '9px ' + MONO; g.fillStyle = rzInk(0.5);
1976        g.fillText('03/05', CX1 - 14, yc);
1977        var iw = g.measureText('03/05').width;
1978        g.fillStyle = rzInk(0.5 * numA);
1979        g.fillText('+' + nPlus + ' −' + nMinus, CX1 - 14 - iw - 12, yc);
1980  
1981        /* ── review cursor band (under the rows) ── */
1982        if (cAlpha > 0.001) {
1983          y = ROWS_Y + cRow * ROW_H;
1984          g.fillStyle = rzInk(0.045 * cAlpha);
1985          g.fillRect(CX0, y, CW, ROW_H);
1986          g.fillStyle = rzInk(0.30 * cAlpha);
1987          g.fillRect(CX0, y, CW, 1);
1988          g.fillStyle = rzInk(0.12 * cAlpha);
1989          g.fillRect(CX0, y + ROW_H - 1, CW, 1);
1990          /* caret */
1991          g.fillStyle = rzInk(0.75 * cAlpha);
1992          g.beginPath();
1993          g.moveTo(CX0 + 7, y + ROW_H / 2 - 4);
1994          g.lineTo(CX0 + 12, y + ROW_H / 2);
1995          g.lineTo(CX0 + 7, y + ROW_H / 2 + 4);
1996          g.closePath(); g.fill();
1997        }
1998  
1999        /* ── rows ── */
2000        for (r = 0; r < N; r++) {
2001          var row = ROWS[r];
2002          y = ROWS_Y + r * ROW_H; yc = y + ROW_H / 2;
2003          var read = Math.max(clamp01(cRow - r), readAll) * gfade;
2004          var rm = 0.62 + 0.38 * read;                 /* unread rows sit lighter */
2005          var fx = CODE_X + row.ind * INDENT;
2006          var fk = null;
2007          if (row.find != null) fk = F[row.find];
2008          if (row.fix != null) {
2009            fk = F[row.fix];
2010            if (fk.f1 <= 0.001) continue;              /* fix line not written yet */
2011            var acc = ACC[FINDINGS[row.fix].acc];
2012            g.fillStyle = rzInk(0.05 * fk.f1);
2013            g.fillRect(CX0, y, CW, ROW_H);
2014            g.fillStyle = withAlpha(acc, fk.f1);
2015            g.fillRect(CX0, yc - ROW_H / 2 * fk.f1, 3, ROW_H * fk.f1);
2016            g.textAlign = 'right'; g.font = '9px ' + MONO; g.fillStyle = rzInk(0.42 * fk.f1);
2017            g.fillText(String(row.ln), LN_X, yc + 0.5);
2018            g.textAlign = 'center'; g.font = 'bold 10px ' + MONO; g.fillStyle = rzInk(0.9 * fk.f1);
2019            g.fillText('+', SIGN_X, yc + 0.5);
2020            for (j = 0; j < row.seg.length; j++) {
2021              var gw = easeOutCubic((t - fk.a2 - 0.06 * j) / 0.4) * gfade;
2022              if (gw > 0) {
2023                g.fillStyle = rzInk((row.seg[j][1] + 0.1) * gw);
2024                rrect(fx, yc - BAR_H / 2, row.seg[j][0] * gw, BAR_H, 3, 3, 3, 3); g.fill();
2025              }
2026              fx += row.seg[j][0] + GAP;
2027            }
2028            continue;
2029          }
2030          /* context / finding row */
2031          g.textAlign = 'right'; g.font = '9px ' + MONO;
2032          g.fillStyle = rzInk(0.42 * (0.72 + 0.28 * read));
2033          g.fillText(String(row.ln), LN_X, yc + 0.5);
2034          var e1 = fk ? fk.e1 : 0;
2035          if (fk && e1 > 0) {
2036            var accF = ACC[FINDINGS[row.find].acc];
2037            g.fillStyle = withAlpha(accF, e1);
2038            g.fillRect(CX0, yc - ROW_H / 2 * e1, 3, ROW_H * e1);
2039            g.textAlign = 'center'; g.font = 'bold 10px ' + MONO; g.fillStyle = rzInk(0.9 * e1);
2040            g.fillText('−', SIGN_X, yc + 0.5);
2041          }
2042          for (j = 0; j < row.seg.length; j++) {
2043            g.fillStyle = rzInk(lerp(row.seg[j][1] * rm, 0.13, e1));
2044            rrect(fx, yc - BAR_H / 2, row.seg[j][0], BAR_H, 3, 3, 3, 3); g.fill();
2045            fx += row.seg[j][0] + GAP;
2046          }
2047          if (e1 > 0) {                                /* strike-through on the flagged line */
2048            var sx0 = CODE_X + row.ind * INDENT, sx1 = fx - GAP;
2049            g.fillStyle = rzInk(0.55 * e1);
2050            g.fillRect(sx0, yc - 0.5, (sx1 - sx0) * e1, 1);
2051          }
2052        }
2053  
2054        /* ── pins, leaders, chips ── */
2055        for (k = 0; k < FINDINGS.length; k++) {
2056          var fd = FINDINGS[k], fe = F[k];
2057          if (fe.e2 <= 0.001) continue;
2058          var accP = ACC[fd.acc];
2059          var py = ROWS_Y + fd.row * ROW_H + ROW_H / 2;
2060          var px = rowBarsEnd(ROWS[fd.row]) + 15;
2061          /* chip metrics */
2062          g.font = '600 8.5px ' + OX;
2063          var lw = 0;
2064          for (j = 0; j < fd.label.length; j++) lw += g.measureText(fd.label.charAt(j)).width;
2065          lw += 1.2 * (fd.label.length - 1);
2066          var chipW = Math.round(lw + 20), chipH = 18;
2067          var chipX = CHIP_R - chipW + 6 * (1 - fe.e4), chipY = py - chipH / 2;
2068          /* leader */
2069          if (fe.e3 > 0) {
2070            g.strokeStyle = withAlpha(accP, 0.6 * fe.e3); g.lineWidth = 1;
2071            g.beginPath();
2072            g.moveTo(px + 9, py + 0.5);
2073            g.lineTo(px + 9 + (CHIP_R - chipW - 4 - (px + 9)) * fe.e3, py + 0.5);
2074            g.stroke();
2075          }
2076          /* numbered pin with accent ring */
2077          var pr = 7 * (0.4 + 0.6 * fe.e2);
2078          g.beginPath(); g.arc(px, py, pr, 0, Math.PI * 2);
2079          g.fillStyle = tk.paper; g.fill();
2080          g.strokeStyle = withAlpha(accP, fe.e2); g.lineWidth = 2; g.stroke();
2081          var na = clamp01((fe.e2 - 0.5) * 2);
2082          if (na > 0) {
2083            g.textAlign = 'center'; g.font = '700 8px ' + OX; g.fillStyle = rzInk(0.95 * na);
2084            g.fillText(String(k + 1), px, py + 0.5);
2085          }
2086          /* paper cue chip */
2087          if (fe.e4 > 0) {
2088            rrect(chipX, chipY, chipW, chipH, 4, 4, 4, 4);
2089            g.fillStyle = tk.paper; g.fill();
2090            g.strokeStyle = rzInk(0.15 * fe.e4); g.lineWidth = 1; g.stroke();
2091            g.save();
2092            rrect(chipX, chipY, chipW, chipH, 4, 4, 4, 4); g.clip();
2093            g.fillStyle = withAlpha(accP, fe.e4);
2094            g.fillRect(chipX, chipY, 2, chipH);
2095            g.restore();
2096            g.font = '600 8.5px ' + OX; g.fillStyle = rzInk(0.9 * fe.e4);
2097            spaced(fd.label, chipX + 11, py + 0.5, 1.2, 'left');
2098          }
2099        }
2100  
2101        /* ── status bar ── */
2102        g.fillStyle = rzInk(0.13);
2103        g.fillRect(CX0, FOOT_Y, CW, 1);
2104        var yf = FOOT_Y + FOOT_H / 2 + 0.5;
2105        for (k = 0; k < 5; k++) {                      /* five equal accents, eyebrow mark */
2106          g.fillStyle = SPECTRUM[k];
2107          g.fillRect(CX0 + 14 + k * 5, yf - 1.5, 5, 3);
2108        }
2109        var nFound = 0;
2110        for (k = 0; k < FINDINGS.length; k++) if (F[k].e1 > 0) nFound++;
2111        g.textAlign = 'left'; g.font = '600 9.5px ' + OX; g.fillStyle = rzInk(0.85);
2112        var tx = CX0 + 14 + 25 + 10;
2113        tx += spaced('5 FILES · ', tx, yf, 1.3, 'left') + 1.3;
2114        g.fillStyle = rzInk(0.85 * numA);
2115        tx += spaced(String(nFound), tx, yf, 1.3, 'left') + 1.3;
2116        g.fillStyle = rzInk(0.85);
2117        spaced(' FINDINGS', tx, yf, 1.3, 'left');
2118        /* file progress: 5 segments, current file fills with the cursor */
2119        var segW = 16, segGap = 4, sx = CX1 - 14 - (5 * segW + 4 * segGap);
2120        var prog = clamp01(cRow / (N - 1)) * gfade;
2121        for (k = 0; k < 5; k++) {
2122          var sxx = sx + k * (segW + segGap);
2123          g.fillStyle = rzInk(k < 2 ? 0.7 : 0.15);
2124          g.fillRect(sxx, yf - 1.5, segW, 3);
2125          if (k === 2 && prog > 0) { g.fillStyle = rzInk(0.7); g.fillRect(sxx, yf - 1.5, segW * prog, 3); }
2126        }
2127        var lnNow = ROWS[Math.round(cRow)].ln;
2128        g.textAlign = 'right'; g.font = '8.5px ' + MONO; g.fillStyle = rzInk(0.5 * numA);
2129        g.fillText('LN ' + (lnNow < 100 ? '0' : '') + lnNow, sx - 12, yf);
2130  
2131        /* ── hazard threshold along the bottom edge ── */
2132        g.fillStyle = rzInk(0.07);
2133        g.fillRect(CX0, CY1 - HAZ_H - 1, CW, 1);
2134        g.save();
2135        g.beginPath(); g.rect(CX0, CY1 - HAZ_H, CW, HAZ_H); g.clip();
2136        g.strokeStyle = rzInk(0.2); g.lineWidth = 4.5;
2137        g.beginPath();
2138        for (x = CX0 - 10; x < CX1 + 10; x += 19.8) {
2139          g.moveTo(x, CY1 + 2); g.lineTo(x + 12, CY1 - HAZ_H - 2);
2140        }
2141        g.stroke();
2142        g.restore();
2143  
2144        /* ── corner crop-brackets ── */
2145        g.fillStyle = rzInk(0.28);
2146        var bx0 = CX0 + 7, by0 = CY0 + 7, bx1 = CX1 - 7, by1 = CY1 - 7;
2147        g.fillRect(bx0, by0, 13, 2); g.fillRect(bx0, by0, 2, 13);
2148        g.fillRect(bx1 - 13, by0, 13, 2); g.fillRect(bx1 - 2, by0, 2, 13);
2149        g.fillRect(bx0, by1 - 2, 13, 2); g.fillRect(bx0, by1 - 13, 2, 13);
2150        g.fillRect(bx1 - 13, by1 - 2, 13, 2); g.fillRect(bx1 - 2, by1 - 13, 2, 13);
2151  
2152        g.restore();  /* pane clip */
2153  
2154        /* pane border */
2155        rrect(CX0 + 0.5, CY0 + 0.5, CW - 1, CH - 1, 4, 12, 12, 12);
2156        g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
2157      }
2158  
2159      /* ---- reduced motion: one composed frame, redrawn only if the
2160             backing store is reset (resize / theme) ---- */
2161      var reduce = false;
2162      try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) { reduce = false; }
2163      if (reduce) {
2164        var drawStatic = function () { draw(T_STATIC); };
2165        drawStatic();
2166        try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic); } catch (e) {}
2167        window.addEventListener('resize', drawStatic);
2168        window.addEventListener('load', drawStatic);
2169        document.addEventListener('rz-theme-change', drawStatic);
2170        document.addEventListener('rz-why-variant', drawStatic);
2171        return;
2172      }
2173  
2174      /* ---- RAF loop, time-based, paused while hidden ---- */
2175      var raf = 0, running = false, t0 = performance.now(), hiddenAt = 0;
2176      function loop(now) {
2177        raf = requestAnimationFrame(loop);
2178        if (cv.offsetParent === null) return;   /* wrapper is display:none (≤900px): skip the draw */
2179        draw(((now - t0) / 1000) % DUR);
2180      }
2181      function start() { if (running) return; running = true; raf = requestAnimationFrame(loop); }
2182      function stop() { running = false; if (raf) cancelAnimationFrame(raf); raf = 0; }
2183      document.addEventListener('visibilitychange', function () {
2184        if (document.hidden) { hiddenAt = performance.now(); stop(); }
2185        else { if (hiddenAt) { t0 += performance.now() - hiddenAt; hiddenAt = 0; } start(); }
2186      });
2187      if (document.hidden) { hiddenAt = performance.now(); draw(0); } else { start(); }
2188  })();
2189
2190  /* ── 11. DIGITAL FORENSICS — Hex Dump Evidence Extractor ────── */
2191  (function () {
2192      'use strict';
2193      /* ── 11. DIGITAL FORENSICS — Evidence Ledger (chain of custody) ──
2194         A vertical ruler rail on the left; five paper evidence cards
2195         (EV-001…EV-005) file in one at a time, a SHA-256 fragment types
2196         in, an accent double-ring seal stamps the card (with a subtle
2197         squash), and chain links draw between consecutive checkpoints.
2198         Ends on an "INTEGRITY VERIFIED" tick, holds, fades, loops.
2199         Tokens are read every frame (paper + five accents); neutral
2200         ink goes through rzInk(). Transparent background. */
2201      var cv = document.getElementById('rz-visual-forensics');
2202      if (!cv) return;
2203      var W = 480, H = 480;
2204      var g = cv.getContext('2d');
2205      if (!g) return;
2206      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
2207  
2208      /* ---- layout (logical px) ---- */
2209      var N = 5;
2210      var RAIL_X = 52, RAIL_TOP = 48;
2211      var CARD_X = 82, CARD_W = 362, CARD_H = 52, CARD_GAP = 18, BODY_Y = 58;
2212      var HEAD_RULE_Y = 40, FOOT_RULE_Y = 404, RAIL_BOT = FOOT_RULE_Y;
2213  
2214      /* ---- illustrative ledger entries (ids + readouts only) ---- */
2215      var ENTRIES = [
2216        { id: 'EV-001', label: 'TRIAGE',      t: 'T+0h',  hash: '9c2e17a4d0f3b8e6…b04d' },
2217        { id: 'EV-002', label: 'ACQUISITION', t: 'T+4h',  hash: '4fa1d3c07e92b5a8…77e2' },
2218        { id: 'EV-003', label: 'TIMELINE',    t: 'T+2d',  hash: 'e08b5c1f6a3d9427…1a9f' },
2219        { id: 'EV-004', label: 'ROOT CAUSE',  t: 'T+5d',  hash: '71d4aa2e8c5f03b9…c3e6' },
2220        { id: 'EV-005', label: 'REPORT',      t: 'T+10d', hash: 'b6f2093d5e7a1c48…58d1' }
2221      ];
2222      /* same five-accent rotation as the methodology ledger rows */
2223      var ACCENT_VARS = ['--rz-purple', '--rz-lime', '--rz-crimson', '--rz-indigo', '--rz-yellow'];
2224      var ACCENT_FALLBACK = ['#7d14d7', '#85f403', '#f50034', '#5801f5', '#e6e414'];
2225  
2226      /* ---- timeline (seconds) ---- */
2227      var T_INTRO = 0.5, T_PITCH = 1.55;
2228      var C_SLIDE = [0, 0.45], C_TYPE = [0.35, 1.0], C_STAMP = [1.02, 1.44], C_LINK = [1.25, 1.75];
2229      var T_LAST = T_INTRO + (N - 1) * T_PITCH + C_LINK[1];   /* 8.45 — last card filed */
2230      var T_TAIL = [T_LAST, T_LAST + 0.4];                     /* rail runs down to the footer */
2231      var T_VERIFY = [T_LAST + 0.4, T_LAST + 1.0];             /* ring + tick + label */
2232      var T_HOLD_END = T_LAST + 4.2;                           /* 12.65 — hold on the sealed ledger */
2233      var T_OUT = [T_HOLD_END, T_HOLD_END + 0.6];              /* soft fade before the loop */
2234      var LOOP = T_OUT[1] + 0.2;                               /* ~13.45 s */
2235  
2236      var reduce = false;
2237      try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
2238  
2239      /* ---- helpers ---- */
2240      function seg(t, a, b) { var p = (t - a) / (b - a); return p <= 0 ? 0 : (p >= 1 ? 1 : p); }
2241      function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); }
2242      function easeOutCubic(p) { return 1 - Math.pow(1 - p, 3); }
2243      function easeInOutCubic(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
2244      function pad2(n) { return n < 10 ? '0' + n : '' + n; }
2245      function fOx(w, px) { return w + ' ' + px + 'px Oxanium, sans-serif'; }
2246      function fMono(w, px) { return w + ' ' + px + 'px "Courier New", Courier, monospace'; }
2247  
2248      /* design tokens, read once per frame */
2249      function theme() {
2250        var cs = null, acc = [], i;
2251        try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
2252        function v(name, fb) {
2253          var s = cs ? cs.getPropertyValue(name) : '';
2254          s = s ? s.replace(/^\s+|\s+$/g, '') : '';
2255          return s || fb;
2256        }
2257        for (i = 0; i < N; i++) acc.push(v(ACCENT_VARS[i], ACCENT_FALLBACK[i]));
2258        return {
2259          dark: document.documentElement.getAttribute('data-theme') === 'dark',
2260          paper: v('--rz-paper', '#ffffff'),
2261          acc: acc
2262        };
2263      }
2264  
2265      /* rounded-rect path; r = number or [tl, tr, br, bl] */
2266      function rr(x, y, w, h, r) {
2267        var tl, tr, br, bl;
2268        if (typeof r === 'number') { tl = tr = br = bl = r; } else { tl = r[0]; tr = r[1]; br = r[2]; bl = r[3]; }
2269        g.beginPath();
2270        g.moveTo(x + tl, y);
2271        g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr);
2272        g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br);
2273        g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl);
2274        g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl);
2275        g.closePath();
2276      }
2277  
2278      /* letter-spaced text (uppercase eyebrows); align 'left' | 'right' */
2279      function tracked(txt, x, y, px, em, align) {
2280        var sp = px * em, i, n = txt.length, widths = [], total = 0, cx, prev = g.textAlign;
2281        for (i = 0; i < n; i++) { widths[i] = g.measureText(txt.charAt(i)).width; total += widths[i]; }
2282        total += sp * (n - 1);
2283        cx = align === 'right' ? x - total : x;
2284        g.textAlign = 'left';
2285        for (i = 0; i < n; i++) { g.fillText(txt.charAt(i), cx, y); cx += widths[i] + sp; }
2286        g.textAlign = prev;
2287      }
2288  
2289      function dotY(i) { return BODY_Y + i * (CARD_H + CARD_GAP) + CARD_H / 2; }
2290  
2291      /* per-card state from its local time */
2292      function cardState(lt) {
2293        var s = { on: lt >= 0, slide: 0, typed: 0, caret: false, stampP: 0, dotFill: 0,
2294                  sealScale: 1, sealAlpha: 0, squash: 1, link: 0 };
2295        if (lt < 0) return s;
2296        s.slide = easeOutQuint(seg(lt, C_SLIDE[0], C_SLIDE[1]));
2297        s.typed = seg(lt, C_TYPE[0], C_TYPE[1]);
2298        var pSt = seg(lt, C_STAMP[0], C_STAMP[1]);
2299        s.stampP = pSt;
2300        s.caret = lt >= 0.18 && pSt <= 0;
2301        if (pSt > 0) {
2302          /* stamp: fly in large, land slightly under, settle (rhymes with the CSS keyframe) */
2303          if (pSt < 0.6) { s.sealScale = 1.9 - 0.96 * easeOutCubic(pSt / 0.6); s.sealAlpha = Math.min(1, pSt / 0.14); }
2304          else { s.sealScale = 0.94 + 0.06 * easeInOutCubic((pSt - 0.6) / 0.4); s.sealAlpha = 1; }
2305          s.squash = 1 - 0.032 * Math.sin(Math.PI * seg(pSt, 0.42, 0.92));   /* card takes the impact */
2306          s.dotFill = seg(pSt, 0.55, 0.8);                                     /* checkpoint lights */
2307        }
2308        s.link = easeInOutCubic(seg(lt, C_LINK[0], C_LINK[1]));
2309        return s;
2310      }
2311  
2312      /* ---- drawing ---- */
2313      function drawCrops() {
2314        var m = 8, L = 14;
2315        g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5; g.lineCap = 'butt';
2316        g.beginPath();
2317        g.moveTo(m, m + L); g.lineTo(m, m); g.lineTo(m + L, m);
2318        g.moveTo(W - m - L, m); g.lineTo(W - m, m); g.lineTo(W - m, m + L);
2319        g.moveTo(W - m, H - m - L); g.lineTo(W - m, H - m); g.lineTo(W - m - L, H - m);
2320        g.moveTo(m + L, H - m); g.lineTo(m, H - m); g.lineTo(m, H - m - L);
2321        g.stroke();
2322      }
2323  
2324      function drawTape(x, y, w, h, th) {
2325        var k;
2326        g.save();
2327        g.beginPath(); g.rect(x, y, w, h); g.clip();
2328        g.strokeStyle = rzInk(th.dark ? 0.16 : 0.13); g.lineWidth = 3.5; g.lineCap = 'butt';
2329        g.beginPath();
2330        for (k = -h; k < w + h; k += 8) { g.moveTo(x + k, y + h + 1); g.lineTo(x + k + h + 2, y - 1); }
2331        g.stroke();
2332        g.restore();
2333      }
2334  
2335      function drawHeader(th, cards, fade) {
2336        var i, x, la, hgt;
2337        g.textAlign = 'left';
2338        g.font = fOx('600', 9); g.fillStyle = rzInk(0.88);
2339        tracked('EVIDENCE CUSTODY LOG', 20, 21, 9, 0.16, 'left');
2340        drawTape(256, 13, 44, 8, th);
2341        g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45);
2342        tracked('CASE REF', 314, 20, 7.5, 0.12, 'left');
2343        g.fillStyle = rzInk(0.3); g.fillRect(364, 13, 26, 7); g.fillRect(394, 13, 12, 7);   /* redacted */
2344        /* five swatches light up as entries are sealed */
2345        for (i = 0; i < N; i++) {
2346          x = 416 + i * 9;
2347          g.fillStyle = rzInk(0.12); g.fillRect(x, 13, 6, 6);
2348          la = cards[i].dotFill * fade;
2349          if (la > 0) { g.save(); g.globalAlpha = la; g.fillStyle = th.acc[i]; g.fillRect(x, 13, 6, 6); g.restore(); }
2350        }
2351        /* ruler */
2352        g.strokeStyle = rzInk(0.13); g.lineWidth = 1;
2353        g.beginPath(); g.moveTo(20, HEAD_RULE_Y + 0.5); g.lineTo(460, HEAD_RULE_Y + 0.5); g.stroke();
2354        g.strokeStyle = rzInk(0.22);
2355        g.beginPath();
2356        for (x = 20; x <= 460; x += 8) { hgt = ((x - 20) % 40 === 0) ? 9 : 4; g.moveTo(x + 0.5, HEAD_RULE_Y); g.lineTo(x + 0.5, HEAD_RULE_Y - hgt); }
2357        g.stroke();
2358      }
2359  
2360      function drawRail() {
2361        var y, len;
2362        g.strokeStyle = rzInk(0.13); g.lineWidth = 1;
2363        g.beginPath(); g.moveTo(RAIL_X, RAIL_TOP); g.lineTo(RAIL_X, RAIL_BOT); g.stroke();
2364        g.strokeStyle = rzInk(0.22);
2365        g.beginPath();
2366        for (y = RAIL_TOP; y <= RAIL_BOT; y += 8) { len = ((y - RAIL_TOP) % 32 === 0) ? 9 : 4; g.moveTo(RAIL_X + 5, y + 0.5); g.lineTo(RAIL_X + 5 + len, y + 0.5); }
2367        g.stroke();
2368        g.save();
2369        g.translate(30, RAIL_BOT - 4); g.rotate(-Math.PI / 2);
2370        g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45); g.textAlign = 'left';
2371        tracked('CHAIN OF CUSTODY', 0, 0, 7.5, 0.16, 'left');
2372        g.restore();
2373      }
2374  
2375      function drawLink(x, ym, q, th, fade) {
2376        g.save();
2377        g.globalAlpha = fade * q;
2378        g.translate(x, ym); g.scale(0.6 + 0.4 * q, 0.6 + 0.4 * q);
2379        g.lineWidth = 1.4; g.strokeStyle = rzInk(0.85); g.fillStyle = th.paper;
2380        rr(-3.5, -11, 7, 13, 3.5); g.fill(); g.stroke();
2381        rr(-3.5, -2, 7, 13, 3.5); g.fill(); g.stroke();
2382        g.restore();
2383      }
2384  
2385      function drawIntegrity(th, cards, pTail, fade) {
2386        var yEnd = RAIL_TOP, i, ya, yb, p, a, y;
2387        for (i = 0; i < N; i++) {
2388          ya = i === 0 ? RAIL_TOP : dotY(i - 1); yb = dotY(i); p = cards[i].link;
2389          if (p <= 0) break;
2390          yEnd = ya + (yb - ya) * p;
2391          if (p < 1) break;
2392        }
2393        if (i === N && pTail > 0) yEnd = dotY(N - 1) + (RAIL_BOT - dotY(N - 1)) * pTail;
2394        if (yEnd > RAIL_TOP && fade > 0) {
2395          g.save(); g.globalAlpha = fade;
2396          g.strokeStyle = rzInk(0.9); g.lineWidth = 2; g.lineCap = 'butt';
2397          g.beginPath(); g.moveTo(RAIL_X, RAIL_TOP); g.lineTo(RAIL_X, yEnd); g.stroke();
2398          for (i = 1; i < N; i++) {
2399            p = cards[i].link; if (p <= 0.5) continue;
2400            drawLink(RAIL_X, (dotY(i - 1) + dotY(i)) / 2, easeOutCubic(seg(p, 0.5, 0.9)), th, fade);
2401          }
2402          g.restore();
2403        }
2404        /* checkpoint dots: hollow ink ring, filled with the card's accent on sealing */
2405        for (i = 0; i < N; i++) {
2406          if (!cards[i].on) continue;
2407          a = cards[i].slide * fade; if (a <= 0) continue;
2408          y = dotY(i);
2409          g.save(); g.globalAlpha = a;
2410          g.fillStyle = th.paper; g.beginPath(); g.arc(RAIL_X, y, 6.5, 0, Math.PI * 2); g.fill();
2411          g.strokeStyle = rzInk(0.85); g.lineWidth = 1.5; g.beginPath(); g.arc(RAIL_X, y, 3.8, 0, Math.PI * 2); g.stroke();
2412          if (cards[i].dotFill > 0) {
2413            g.globalAlpha = a * cards[i].dotFill;
2414            g.fillStyle = th.acc[i]; g.beginPath(); g.arc(RAIL_X, y, 4.7, 0, Math.PI * 2); g.fill();
2415          }
2416          g.restore();
2417        }
2418      }
2419  
2420      function drawSeal(cx, cy, s, acc, a) {
2421        g.save();
2422        g.globalAlpha = a * s.sealAlpha;
2423        g.translate(cx, cy); g.scale(s.sealScale, s.sealScale); g.rotate(-0.12);
2424        g.strokeStyle = acc; g.lineWidth = 2;
2425        g.beginPath(); g.arc(0, 0, 14, 0, Math.PI * 2); g.stroke();
2426        g.globalAlpha = a * s.sealAlpha * 0.6; g.lineWidth = 1;
2427        g.beginPath(); g.arc(0, 0, 10.5, 0, Math.PI * 2); g.stroke();
2428        g.globalAlpha = a * s.sealAlpha;
2429        g.strokeStyle = rzInk(0.9); g.lineWidth = 2; g.lineCap = 'round'; g.lineJoin = 'round';
2430        g.beginPath(); g.moveTo(-5, 0.5); g.lineTo(-1.5, 4); g.lineTo(5.5, -4); g.stroke();
2431        g.restore();
2432      }
2433  
2434      function drawCard(i, s, th, fade) {
2435        if (!s.on) return;
2436        var a = s.slide * fade;
2437        if (a <= 0) return;
2438        var e = ENTRIES[i];
2439        var x = CARD_X + (1 - s.slide) * 26, y = BODY_Y + i * (CARD_H + CARD_GAP);
2440        var w = CARD_W, h = CARD_H, n, typedTxt, tw;
2441        g.save();
2442        g.globalAlpha = a;
2443        g.translate(x + w / 2, y + h / 2); g.scale(1, s.squash); g.translate(-(x + w / 2), -(y + h / 2));
2444        /* soft lift */
2445        g.fillStyle = th.dark ? 'rgba(0,0,0,0.28)' : 'rgba(20,21,26,0.045)';
2446        rr(x + 1, y + 2, w - 2, h, [3, 8, 8, 8]); g.fill();
2447        g.fillStyle = th.dark ? 'rgba(0,0,0,0.16)' : 'rgba(20,21,26,0.022)';
2448        rr(x + 2, y + 4, w - 4, h, [3, 8, 8, 8]); g.fill();
2449        /* paper chip */
2450        g.fillStyle = th.paper; rr(x, y, w, h, [3, 8, 8, 8]); g.fill();
2451        g.strokeStyle = rzInk(0.16); g.lineWidth = 1; g.stroke();
2452        /* accent tick + id + label + time */
2453        g.fillStyle = th.acc[i]; g.fillRect(x + 12, y + 13, 3, 12);
2454        g.textAlign = 'left'; g.font = fOx('700', 11); g.fillStyle = rzInk(0.92); g.fillText(e.id, x + 21, y + 23);
2455        g.font = fOx('600', 8); g.fillStyle = rzInk(0.5); tracked(e.label, x + 72, y + 22, 8, 0.12, 'left');
2456        g.font = fMono('700', 9); g.fillStyle = rzInk(0.55); g.textAlign = 'right'; g.fillText(e.t, x + w - 64, y + 23);
2457        /* hash line */
2458        g.textAlign = 'left'; g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45); tracked('SHA-256', x + 12, y + 42, 7.5, 0.12, 'left');
2459        n = Math.floor(s.typed * e.hash.length + 1e-6);
2460        typedTxt = e.hash.substring(0, n);
2461        g.font = fMono('400', 9.5); g.fillStyle = rzInk(0.82);
2462        if (n > 0) g.fillText(typedTxt, x + 58, y + 42);
2463        if (s.caret) { tw = n > 0 ? g.measureText(typedTxt).width : 0; g.fillStyle = rzInk(0.5); g.fillRect(x + 58 + tw + 1, y + 34, 5, 10); }
2464        if (s.stampP > 0) drawSeal(x + w - 30, y + h / 2, s, th.acc[i], a);
2465        g.restore();
2466      }
2467  
2468      function drawTick(cx, cy, p) {
2469        if (p <= 0) return;
2470        var A = [-3.4, 0.4], B = [-1, 2.8], C = [3.8, -2.6];
2471        var l1 = Math.sqrt(Math.pow(B[0] - A[0], 2) + Math.pow(B[1] - A[1], 2));
2472        var l2 = Math.sqrt(Math.pow(C[0] - B[0], 2) + Math.pow(C[1] - B[1], 2));
2473        var d = p * (l1 + l2), u, v;
2474        g.beginPath(); g.moveTo(cx + A[0], cy + A[1]);
2475        if (d <= l1) { u = d / l1; g.lineTo(cx + A[0] + (B[0] - A[0]) * u, cy + A[1] + (B[1] - A[1]) * u); }
2476        else { g.lineTo(cx + B[0], cy + B[1]); v = (d - l1) / l2; g.lineTo(cx + B[0] + (C[0] - B[0]) * v, cy + B[1] + (C[1] - B[1]) * v); }
2477        g.stroke();
2478      }
2479  
2480      function drawFooter(t, sealed, pVer, fade) {
2481        var x, SY = 432, aLive, pulse, pr, pt, pl;
2482        g.strokeStyle = rzInk(0.13); g.lineWidth = 1;
2483        g.beginPath(); g.moveTo(20, FOOT_RULE_Y + 0.5); g.lineTo(460, FOOT_RULE_Y + 0.5); g.stroke();
2484        g.strokeStyle = rzInk(0.22); g.beginPath();
2485        for (x = 20; x <= 460; x += 24) { g.moveTo(x + 0.5, FOOT_RULE_Y + 1); g.lineTo(x + 0.5, FOOT_RULE_Y + 7); }
2486        g.stroke();
2487        g.font = fMono('400', 8); g.fillStyle = rzInk(0.45);
2488        tracked('SHA-256 · UTC · WRITE-BLOCKED', 458, SY + 3.5, 8, 0.06, 'right');
2489        /* live status → verified */
2490        aLive = (1 - seg(pVer, 0, 0.35)) * fade * seg(t, 0, 0.4);
2491        if (aLive > 0) {
2492          g.save(); g.globalAlpha = aLive;
2493          pulse = reduce ? 0.8 : 0.3 + 0.55 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2 / 1.6));
2494          g.fillStyle = rzInk(pulse); g.beginPath(); g.arc(24, SY, 3, 0, Math.PI * 2); g.fill();
2495          g.font = fOx('600', 9); g.fillStyle = rzInk(0.7); tracked('SEALING', 34, SY + 3.5, 9, 0.14, 'left');
2496          g.font = fMono('700', 9); g.fillStyle = rzInk(0.5); g.textAlign = 'left'; g.fillText(pad2(sealed) + '/' + pad2(N), 96, SY + 3.5);
2497          g.restore();
2498        }
2499        if (pVer > 0 && fade > 0) {
2500          g.save(); g.globalAlpha = fade;
2501          pr = easeOutCubic(seg(pVer, 0, 0.55));
2502          g.strokeStyle = rzInk(0.85); g.lineWidth = 1.5; g.lineCap = 'round'; g.lineJoin = 'round';
2503          g.beginPath(); g.arc(24, SY, 7, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * pr); g.stroke();
2504          pt = easeOutCubic(seg(pVer, 0.35, 0.8));
2505          g.lineWidth = 1.8; drawTick(24, SY, pt);
2506          pl = easeOutCubic(seg(pVer, 0.45, 1));
2507          g.globalAlpha = fade * pl;
2508          g.font = fOx('700', 10.5); g.fillStyle = rzInk(0.92);
2509          tracked('INTEGRITY VERIFIED', 38 - 6 * (1 - pl), SY + 4, 10.5, 0.12, 'left');
2510          g.restore();
2511        }
2512      }
2513  
2514      function draw(t, th) {
2515        var i, cards = [], sealed = 0, st;
2516        var fade = 1 - easeInOutCubic(seg(t, T_OUT[0], T_OUT[1]));
2517        g.clearRect(0, 0, W, H);
2518        g.textBaseline = 'alphabetic'; g.lineJoin = 'miter';
2519        for (i = 0; i < N; i++) {
2520          st = cardState(t - (T_INTRO + i * T_PITCH));
2521          if (st.dotFill >= 1) sealed++;
2522          cards.push(st);
2523        }
2524        drawCrops();
2525        drawHeader(th, cards, fade);
2526        drawRail();
2527        drawIntegrity(th, cards, easeInOutCubic(seg(t, T_TAIL[0], T_TAIL[1])), fade);
2528        for (i = 0; i < N; i++) drawCard(i, cards[i], th, fade);
2529        drawFooter(t, sealed, seg(t, T_VERIFY[0], T_VERIFY[1]), fade);
2530      }
2531  
2532      /* ---- reduced motion: one composed frame of the sealed ledger ---- */
2533      if (reduce) {
2534        var pending = 0;
2535        var redraw = function () {
2536          if (pending) return;
2537          pending = requestAnimationFrame(function () { pending = 0; draw(T_HOLD_END - 0.5, theme()); });
2538        };
2539        redraw();
2540        window.addEventListener('resize', redraw);
2541        window.addEventListener('load', redraw);
2542        document.addEventListener('rz-theme-change', redraw);
2543        document.addEventListener('rz-why-variant', redraw);
2544        if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(redraw);
2545        return;
2546      }
2547  
2548      /* ---- RAF loop, paused while the tab is hidden ---- */
2549      var raf = 0, t0 = 0, pausedAt = 0;
2550      function loop(now) {
2551        raf = 0;
2552        if (!t0) t0 = now;
2553        if (cv.clientWidth) draw(((now - t0) / 1000) % LOOP, theme());   /* skip when hidden by layout */
2554        raf = requestAnimationFrame(loop);
2555      }
2556      function start() { if (!raf) raf = requestAnimationFrame(loop); }
2557      function stop() { if (raf) { cancelAnimationFrame(raf); raf = 0; } }
2558      document.addEventListener('visibilitychange', function () {
2559        if (document.hidden) { pausedAt = performance.now(); stop(); }
2560        else { if (t0 && pausedAt) t0 += performance.now() - pausedAt; pausedAt = 0; start(); }
2561      });
2562      start();
2563    })();
2564
2565  /* ── 12. MALWARE ANALYSIS — Sandbox Process Tree ───────────── */
2566  (function () {
2567    var cv = document.getElementById('rz-visual-malware');
2568    if (!cv) return;
2569    var W = 480, H = 480;
2570    var g = cv.getContext('2d');
2571    if (!g) return;
2572    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
2573  
2574    /* ── timeline (seconds, one loop) ──────────────────────────────
2575       STATIC lane: sample in, hashed          0.0 –  2.4
2576       DYNAMIC lane: process tree spawns       2.4 –  8.0
2577       RE lane: flagged nodes resolved         8.0 –  9.9
2578       verdict chip, tree settles, hold        9.9 – 12.7
2579       fade out, clean loop                   12.7 – 13.6 */
2580    var LOOP = 13.6;
2581    var T_ROOT = 0.3, T_HASH = 1.0, T_DYN = 2.4, T_RE = 8.0, T_VERD = 9.9, T_SETTLE = 10.6, T_OUT = 12.7;
2582    var FADE = 0.8, SW = 0.6, LINK_D = 0.55, SPAWN_LAG = 0.42, SPAWN_D = 0.45, T_STATIC = 11.8;
2583  
2584    /* ── frame geometry (mirrors the methodology "isolated lab" frame) ── */
2585    var FX = 16, FY = 16, FW = 448, FH = 448, PAD = 18, BUS = 46;
2586  
2587    /* ── the tree: t = link start, flag = suspicious at, tagAt = resolved in RE ── */
2588    var nodes = [
2589      { id: 'sample.exe',          x: 240, y: 104, r: 9, p: -1, t: T_ROOT, root: true },
2590      { id: 'cmd.exe',             x: 126, y: 176, r: 7, p: 0,  t: 2.70, stub: true },
2591      { id: 'svchost.exe',         x: 240, y: 176, r: 7, p: 0,  t: 3.25 },
2592      { id: 'powershell.exe',      x: 354, y: 176, r: 7, p: 0,  t: 3.80, flag: 4.70, tag: 'T1059.001', tagAt: 8.25 },
2593      { id: 'whoami.exe',          x: 52,  y: 248, r: 7, p: 1,  t: 4.55, stub: true },
2594      { id: 'net.exe',             x: 126, y: 248, r: 7, p: 1,  t: 4.95 },
2595      { id: 'schtasks.exe',        x: 200, y: 248, r: 7, p: 1,  t: 5.35, flag: 6.10, tag: 'T1053.005', tagAt: 8.65 },
2596      { id: 'rundll32.exe',        x: 310, y: 248, r: 7, p: 3,  t: 5.95, stub: true, flag: 6.90, tag: 'T1218.011', tagAt: 9.05 },
2597      { id: 'conhost.exe',         x: 398, y: 248, r: 7, p: 3,  t: 6.35 },
2598      { id: 'cdn-•••••[.]top:443', x: 310, y: 320, r: 8, p: 7, t: 7.10, stub: true, net: true, flag: 7.85, tag: 'T1071.001', tagAt: 9.45 }
2599    ];
2600    /* junction dots where a parent fans out to several children */
2601    (function () {
2602      var i, n, kids = {};
2603      for (i = 1; i < nodes.length; i++) { n = nodes[i]; kids[n.p] = (kids[n.p] || 0) + 1; if (n.stub) nodes[n.p].fan = n.t + 0.22; }
2604      for (i = 0; i < nodes.length; i++) if (kids[i] < 2) nodes[i].fan = 0;
2605    })();
2606  
2607    var LANES = [{ lbl: 'STATIC', key: 'indigo' }, { lbl: 'DYNAMIC', key: 'crimson' }, { lbl: 'RE', key: 'lime' }];
2608    var HASH = 'sha256 3f9a…e21c';
2609    var VERDICT = 'FAMILY MATCHED · IOCs 12';
2610    var VERDICT_SUB = 'AsyncRAT · T1071.001';
2611  
2612    /* ── helpers ── */
2613    function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
2614    function seg(t, s, d) { return clamp01((t - s) / d); }
2615    function eio(t) { t = clamp01(t); return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
2616    function eo(t) { t = clamp01(t); return 1 - Math.pow(1 - t, 3); }
2617    function lerp(a, b, k) { return a + (b - a) * k; }
2618    function lerpSpan(a, b, k) { return [lerp(a[0], b[0], k), lerp(a[1], b[1], k)]; }
2619    function pad2(n) { return (n < 10 ? '0' : '') + n; }
2620    function now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); }
2621  
2622    var LIGHT = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' };
2623    var DARK  = { purple: '#9a45ee', indigo: '#7a56ff', crimson: '#ff3355', lime: '#8ef513', yellow: '#f0ee2a', paper: '#0e0f14' };
2624    function tokens() {
2625      var root = document.documentElement, fb = root.getAttribute('data-theme') === 'dark' ? DARK : LIGHT, cs = null, out = {}, k, v;
2626      try { cs = window.getComputedStyle(root); } catch (e) {}
2627      for (k in fb) if (fb.hasOwnProperty(k)) {
2628        v = '';
2629        if (cs) { try { v = (cs.getPropertyValue('--rz-' + k) || '').replace(/^\s+|\s+$/g, ''); } catch (e2) {} }
2630        out[k] = v || fb[k];
2631      }
2632      return out;
2633    }
2634  
2635    function spacedW(txt, sp) { var i, w = 0; for (i = 0; i < txt.length; i++) w += g.measureText(txt.charAt(i)).width + (i < txt.length - 1 ? sp : 0); return w; }
2636    function spaced(txt, x, y, sp, align) {
2637      var w = spacedW(txt, sp), sx = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x, i, ch, prev = g.textAlign;
2638      g.textAlign = 'left';
2639      for (i = 0; i < txt.length; i++) { ch = txt.charAt(i); g.fillText(ch, sx, y); sx += g.measureText(ch).width + sp; }
2640      g.textAlign = prev;
2641      return w;
2642    }
2643    function rrect(x, y, w, h, r) {
2644      g.beginPath(); g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r);
2645      g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h); g.lineTo(x + r, y + h);
2646      g.quadraticCurveTo(x, y + h, x, y + h - r); g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); g.closePath();
2647    }
2648    function chip(x, y, w, h, tk) { rrect(x + 0.5, y + 0.5, w - 1, h - 1, 5); g.fillStyle = tk.paper; g.fill(); g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke(); }
2649    function shape(n, r) {
2650      g.beginPath();
2651      if (n.net) { g.moveTo(n.x, n.y - r); g.lineTo(n.x + r, n.y); g.lineTo(n.x, n.y + r); g.lineTo(n.x - r, n.y); g.closePath(); }
2652      else g.arc(n.x, n.y, r, 0, Math.PI * 2);
2653    }
2654    /* orthogonal polyline drawn up to a fraction of its length */
2655    function partial(pts, frac) {
2656      var i, l, lens = [], total = 0, left, k;
2657      for (i = 1; i < pts.length; i++) { l = Math.abs(pts[i][0] - pts[i - 1][0]) + Math.abs(pts[i][1] - pts[i - 1][1]); lens.push(l); total += l; }
2658      left = total * frac;
2659      g.beginPath(); g.moveTo(pts[0][0], pts[0][1]);
2660      for (i = 1; i < pts.length; i++) {
2661        l = lens[i - 1];
2662        if (left >= l) { g.lineTo(pts[i][0], pts[i][1]); left -= l; }
2663        else { k = l > 0 ? left / l : 0; g.lineTo(lerp(pts[i - 1][0], pts[i][0], k), lerp(pts[i - 1][1], pts[i][1], k)); break; }
2664      }
2665      g.stroke();
2666    }
2667    function hazard(x, y, w, h) {
2668      var s;
2669      g.save(); g.beginPath(); g.rect(x, y, w, h); g.clip();
2670      g.strokeStyle = rzInk(0.28); g.lineWidth = 5;
2671      for (s = -h; s < w + h; s += 14.14) { g.beginPath(); g.moveTo(x + s, y + h + 1); g.lineTo(x + s + h + 2, y - 1); g.stroke(); }
2672      g.restore();
2673    }
2674  
2675    /* ── phase state for the lane header ── */
2676    function laneWeights(t) {
2677      var k;
2678      if (t < T_DYN) return [1, 0, 0];
2679      if (t < T_DYN + SW) { k = eio((t - T_DYN) / SW); return [1 - k, k, 0]; }
2680      if (t < T_RE) return [0, 1, 0];
2681      if (t < T_RE + SW) { k = eio((t - T_RE) / SW); return [0, 1 - k, k]; }
2682      if (t < T_VERD) return [0, 0, 1];
2683      if (t < T_VERD + 0.7) { k = eio((t - T_VERD) / 0.7); return [k, k, 1]; }
2684      if (t < T_OUT) return [1, 1, 1];
2685      k = eio((t - T_OUT) / FADE); return [1, 1 - k, 1 - k];
2686    }
2687    function underline(t, sp) {
2688      var full = [sp[0][0], sp[2][1]];
2689      if (t < T_DYN) return sp[0];
2690      if (t < T_DYN + SW) return lerpSpan(sp[0], sp[1], eio((t - T_DYN) / SW));
2691      if (t < T_RE) return sp[1];
2692      if (t < T_RE + SW) return lerpSpan(sp[1], sp[2], eio((t - T_RE) / SW));
2693      if (t < T_VERD) return sp[2];
2694      if (t < T_VERD + 0.7) return lerpSpan(sp[2], full, eio((t - T_VERD) / 0.7));
2695      if (t < T_OUT) return full;
2696      return lerpSpan(full, sp[0], eio((t - T_OUT) / FADE));
2697    }
2698    function readout(t) {
2699      var B = [T_HASH, T_DYN, T_RE, T_VERD, T_OUT + FADE * 0.5], i, d = 1;
2700      var txt = t < B[0] ? 't+0' : t < B[1] ? 't+4h' : t < B[2] ? 't+12h' : t < B[3] ? 't+48h' : t < B[4] ? 't+5d' : 't+0';
2701      for (i = 0; i < B.length; i++) d = Math.min(d, Math.abs(t - B[i]) / 0.25);
2702      return { txt: txt, a: eo(d) };
2703    }
2704  
2705    /* ── static chrome: frame, crop brackets, label, hazard, ruler ── */
2706    function drawFrame(tk) {
2707      var o = 6, L = 14, x0 = FX - o, y0 = FY - o, x1 = FX + FW + o, y1 = FY + FH + o, x, f, a, lw;
2708      g.lineWidth = 1; g.strokeStyle = rzInk(0.13); g.strokeRect(FX + 0.5, FY + 0.5, FW - 1, FH - 1);
2709      g.lineWidth = 2; g.strokeStyle = rzInk(0.85); g.beginPath();
2710      g.moveTo(x0, y0 + L); g.lineTo(x0, y0); g.lineTo(x0 + L, y0);
2711      g.moveTo(x1 - L, y0); g.lineTo(x1, y0); g.lineTo(x1, y0 + L);
2712      g.moveTo(x0, y1 - L); g.lineTo(x0, y1); g.lineTo(x0 + L, y1);
2713      g.moveTo(x1 - L, y1); g.lineTo(x1, y1); g.lineTo(x1, y1 - L);
2714      g.stroke();
2715      /* frame label knocks out the border on a paper chip */
2716      g.font = "8.5px 'Courier New', monospace"; g.textBaseline = 'middle';
2717      lw = spacedW('ISOLATED LAB', 0.7);
2718      g.fillStyle = tk.paper; g.fillRect(FX + PAD - 6, FY - 7, lw + 12, 14);
2719      g.fillStyle = rzInk(0.55); spaced('ISOLATED LAB', FX + PAD, FY + 0.5, 0.7, 'left');
2720      g.textBaseline = 'alphabetic';
2721      hazard(FX + FW - PAD - 64, FY - 1, 64, 7);
2722      for (x = FX + 12; x < FX + FW; x += 24) {
2723        f = (x - FX) / FW; a = Math.min(1, Math.min(f, 1 - f) / 0.12);
2724        g.fillStyle = rzInk(0.22 * a); g.fillRect(x, FY + FH - 8, 1, 7);
2725      }
2726      g.fillStyle = rzInk(0.45); g.font = "8.5px 'Courier New', monospace";
2727      spaced('PROCESS TREE', FX + PAD, FY + FH - 17, 0.7, 'left');
2728    }
2729  
2730    /* ── lane header: STATIC · DYNAMIC · RE with a sliding ink cursor ── */
2731    function drawHeader(t, tk) {
2732      var y = 52, x = FX + PAD, i, sp = [], w = laneWeights(t), lw, sx, u, r;
2733      g.font = "600 10px 'Oxanium', sans-serif"; g.textBaseline = 'alphabetic'; g.textAlign = 'left';
2734      for (i = 0; i < LANES.length; i++) {
2735        sx = x;
2736        g.fillStyle = tk[LANES[i].key]; rrect(x, y - 7, 6, 6, 1.5); g.fill();
2737        x += 12;
2738        lw = spacedW(LANES[i].lbl, 1.4);
2739        g.fillStyle = rzInk(0.42 + 0.48 * w[i]); spaced(LANES[i].lbl, x, y, 1.4, 'left');
2740        x += lw; sp.push([sx, x]);
2741        if (i < LANES.length - 1) { g.fillStyle = rzInk(0.35); g.textAlign = 'center'; g.fillText('·', x + 11, y); g.textAlign = 'left'; x += 22; }
2742      }
2743      u = underline(t, sp);
2744      g.fillStyle = rzInk(0.85); g.fillRect(u[0], y + 7, u[1] - u[0], 1);
2745      r = readout(t);
2746      g.font = "9px 'Courier New', monospace"; g.textAlign = 'right'; g.fillStyle = rzInk(0.5 * r.a);
2747      g.fillText(r.txt, FX + FW - PAD, y); g.textAlign = 'left';
2748    }
2749  
2750    /* ── the animated part: links, nodes, flags, tags, chips ── */
2751    function drawTree(t, tk, out) {
2752      var i, n, par, lp, ym, pts, jd, s, r, settle, k, q, f, pg, pq, la, ta, ha, root, cx, cy, hw, va, l1w, cw, ch, vx, vy, pc = 0, fc = 0, t0;
2753  
2754      /* links: ink elbows from under a parent's label to the top of each child */
2755      g.globalAlpha = out; g.lineWidth = 1; g.strokeStyle = rzInk(0.55); g.lineCap = 'butt'; g.lineJoin = 'miter';
2756      for (i = 1; i < nodes.length; i++) {
2757        n = nodes[i]; par = nodes[n.p];
2758        lp = seg(t, n.t, LINK_D); if (lp <= 0) continue;
2759        ym = par.y + BUS;
2760        if (n.stub) pts = [[par.x + 0.5, par.y + par.r + 18], [par.x + 0.5, ym + 0.5], [n.x + 0.5, ym + 0.5], [n.x + 0.5, n.y - n.r - 2]];
2761        else pts = [[par.x + 0.5, ym + 0.5], [n.x + 0.5, ym + 0.5], [n.x + 0.5, n.y - n.r - 2]];
2762        partial(pts, eio(lp));
2763      }
2764      for (i = 0; i < nodes.length; i++) {
2765        n = nodes[i]; if (!n.fan) continue;
2766        jd = seg(t, n.fan, 0.3); if (jd <= 0) continue;
2767        g.globalAlpha = out * eo(jd); g.fillStyle = rzInk(0.85);
2768        g.beginPath(); g.arc(n.x + 0.5, n.y + BUS + 0.5, 2.5, 0, Math.PI * 2); g.fill();
2769      }
2770  
2771      /* nodes */
2772      for (i = 0; i < nodes.length; i++) {
2773        n = nodes[i]; t0 = n.t + (n.root ? 0 : SPAWN_LAG);
2774        s = eo(seg(t, t0, SPAWN_D)); if (s <= 0) continue;
2775        if (seg(t, t0, SPAWN_D) > 0.5) pc++;
2776        r = n.r * s;
2777        g.globalAlpha = out;
2778        shape(n, r); g.fillStyle = tk.paper; g.fill();
2779        g.lineWidth = n.root ? 1.6 : 1.3; g.strokeStyle = rzInk(n.root ? 0.8 : 0.6); shape(n, r); g.stroke();
2780  
2781        if (n.root) {
2782          g.fillStyle = tk.purple; rrect(n.x - 2.5, n.y - 2.5, 5, 5, 1); g.fill();
2783          settle = 1 - eio(seg(t, T_SETTLE, 0.8));
2784          k = ((t - T_ROOT) % 2.6) / 2.6;
2785          if (t >= T_ROOT + 0.4 && k < 0.7 && settle > 0) {
2786            q = eo(k / 0.7);
2787            g.globalAlpha = out * settle * 0.45 * (1 - q); g.lineWidth = 1.2; g.strokeStyle = tk.purple;
2788            g.beginPath(); g.arc(n.x, n.y, n.r + 10 * q, 0, Math.PI * 2); g.stroke();
2789            g.globalAlpha = out;
2790          }
2791        }
2792  
2793        if (n.flag) {
2794          f = eio(seg(t, n.flag, 0.5));
2795          if (seg(t, n.flag, 0.5) > 0.5) fc++;
2796          if (f > 0) {
2797            g.globalAlpha = out * f; g.lineWidth = 1.5; g.strokeStyle = tk.crimson; shape(n, r); g.stroke();
2798            g.fillStyle = tk.crimson; g.beginPath(); g.arc(n.x, n.y, 2.2 * f, 0, Math.PI * 2); g.fill();
2799            pg = seg(t, n.flag, 0.9);
2800            if (pg > 0 && pg < 1) {
2801              pq = eo(pg); g.globalAlpha = out * 0.5 * (1 - pq); g.lineWidth = 1;
2802              g.beginPath(); g.arc(n.x, n.y, n.r + 9 * pq, 0, Math.PI * 2); g.stroke();
2803            }
2804            g.globalAlpha = out;
2805          }
2806        }
2807  
2808        la = seg(t, t0 + 0.12, 0.35);
2809        if (la > 0) {
2810          g.globalAlpha = out * eo(la); g.textAlign = 'center'; g.textBaseline = 'alphabetic';
2811          if (n.net) { g.font = "8.5px 'Courier New', monospace"; g.fillStyle = rzInk(0.7); }
2812          else { g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.85); }
2813          g.fillText(n.id, n.x, n.y + n.r + 12); g.textAlign = 'left';
2814        }
2815        if (n.tag) {
2816          ta = seg(t, n.tagAt, 0.4);
2817          if (ta > 0) {
2818            g.globalAlpha = out * eo(ta); g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.6);
2819            g.fillText(n.tag, n.x + n.r + 6 + (1 - eo(ta)) * 4, n.y + 3);
2820          }
2821        }
2822        g.globalAlpha = out;
2823      }
2824  
2825      /* hash readout beside the sample (STATIC lane) */
2826      ha = eo(seg(t, T_HASH, 0.45));
2827      if (ha > 0) {
2828        root = nodes[0]; cx = root.x + root.r + 4 + (1 - ha) * 6; cy = root.y;
2829        g.globalAlpha = out * ha;
2830        g.setLineDash([2, 3]); g.lineWidth = 1; g.strokeStyle = rzInk(0.4);
2831        g.beginPath(); g.moveTo(cx, cy + 0.5); g.lineTo(cx + 10, cy + 0.5); g.stroke(); g.setLineDash([]);
2832        g.font = "8px 'Courier New', monospace"; hw = g.measureText(HASH).width;
2833        chip(cx + 12, cy - 9, hw + 14, 18, tk);
2834        g.textBaseline = 'middle'; g.fillStyle = rzInk(0.6); g.fillText(HASH, cx + 19, cy + 0.5); g.textBaseline = 'alphabetic';
2835      }
2836  
2837      /* verdict chip (yellow tab, like the methodology's verdict node) */
2838      va = eo(seg(t, T_VERD, 0.6));
2839      if (va > 0) {
2840        g.globalAlpha = out * va;
2841        g.font = "700 11px 'Oxanium', sans-serif"; l1w = g.measureText(VERDICT).width;
2842        cw = Math.ceil(l1w) + 44; ch = 40; vx = Math.round(240 - cw / 2); vy = 382 + (1 - va) * 8;
2843        chip(vx, vy, cw, ch, tk);
2844        g.fillStyle = tk.yellow; g.fillRect(vx + 14, vy, cw - 28, 3);
2845        g.textAlign = 'center'; g.fillStyle = rzInk(0.92); g.fillText(VERDICT, 240, vy + 18);
2846        g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.5); g.fillText(VERDICT_SUB, 240, vy + 31);
2847        g.textAlign = 'left';
2848      }
2849  
2850      /* live counters, bottom right */
2851      g.globalAlpha = out; g.font = "8.5px 'Courier New', monospace"; g.textAlign = 'right'; g.fillStyle = rzInk(0.5);
2852      g.fillText('PROC ' + pad2(pc) + ' · FLAG ' + pad2(fc), FX + FW - PAD, FY + FH - 17); g.textAlign = 'left';
2853    }
2854  
2855    function draw(t) {
2856      var tk = tokens(), out = t < T_OUT ? 1 : 1 - eio((t - T_OUT) / FADE);
2857      g.clearRect(0, 0, W, H);
2858      g.globalAlpha = 1; g.setLineDash([]); g.lineCap = 'butt'; g.lineJoin = 'miter'; g.textAlign = 'left'; g.textBaseline = 'alphabetic';
2859      drawFrame(tk);
2860      drawHeader(t, tk);
2861      if (out > 0) drawTree(t, tk, out);
2862      g.globalAlpha = 1;
2863    }
2864  
2865    /* ── reduced motion: one composed frame, redrawn only when the canvas refits ── */
2866    var reduce = false;
2867    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
2868    if (reduce) {
2869      var still = function () { window.requestAnimationFrame(function () { draw(T_STATIC); }); };
2870      still();
2871      window.addEventListener('resize', still); window.addEventListener('load', still);
2872      document.addEventListener('rz-theme-change', still); document.addEventListener('rz-why-variant', still);
2873      return;
2874    }
2875  
2876    /* ── time-based loop, paused while the tab is hidden ── */
2877    var t0 = now(), pausedAt = null, raf = 0;
2878    function frame(ts) {
2879      raf = 0; if (document.hidden) return;
2880      draw(((ts - t0) / 1000) % LOOP);
2881      raf = window.requestAnimationFrame(frame);
2882    }
2883    function start() { if (!raf) raf = window.requestAnimationFrame(frame); }
2884    document.addEventListener('visibilitychange', function () {
2885      if (document.hidden) { if (raf) { window.cancelAnimationFrame(raf); raf = 0; } pausedAt = now(); }
2886      else { if (pausedAt !== null) { t0 += now() - pausedAt; pausedAt = null; } start(); }
2887    });
2888    if (document.hidden) pausedAt = now(); else start();
2889  })();
2890
2891  /* ── 13. MOBILE APP TESTING — Security Scanner ──────────────── */
2892  (function () {
2893    'use strict';
2894    /* ── 13. MOBILE APP TESTING — Device layers (exploded view) ──
2895       A blueprint phone whose four inner planes (UI · RUNTIME · STORAGE ·
2896       NETWORK) lift apart into an exploded stack. A scan visits each
2897       plane top-down, a finding pin lands on STORAGE, the stack holds,
2898       then settles back. Same rotateX(54°)·rotateZ(-40°) view as the
2899       page's "Testing, layer by layer" methodology scene. Tokens are
2900       read every frame; neutral ink goes through rzInk(). Transparent
2901       background · time-based, eased · pauses when hidden · one static
2902       frame under prefers-reduced-motion. ES5, no globals. */
2903    var cv = document.getElementById('rz-visual-mobile');
2904    if (!cv) return;
2905    var W = 480, H = 480;
2906    var g = cv.getContext('2d');
2907    if (!g) return;
2908    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
2909  
2910    /* ---- projection (orthographic): u = phone width, v = phone height, z = stack normal ---- */
2911    var UX = 0.766, UY = -0.378, VX = 0.643, VY = 0.450, ZY = -0.809;
2912    var PW = 132, PH = 264, PR = 22;                              /* shell, local px */
2913    var INS = 8, SW = PW - 2 * INS, SH = PH - 2 * INS, SR = 16;   /* inner planes */
2914    var OX = 104, OY = 262;                                       /* shell origin on screen */
2915    var GAP = 52, HL_LIFT = 10;                                   /* exploded gap / highlight lift (local z) */
2916    var GS = 1.16, GY = 14;                                       /* glyph space 100×190 → plane */
2917    var CHIP_X = 356, CHIP_W = 112;                               /* finding chip column / width */
2918  
2919    var LAYERS = [   /* index 0 sits in the shell, 3 is the glass */
2920      { name: 'NETWORK', sub: 'TLS · API',     num: '04', v: '--rz-indigo',  fb: '#5801f5' },
2921      { name: 'STORAGE', sub: 'KEYCHAIN · DB', num: '03', v: '--rz-crimson', fb: '#f50034' },
2922      { name: 'RUNTIME', sub: 'IPC · HOOKS',   num: '02', v: '--rz-lime',    fb: '#85f403' },
2923      { name: 'UI',      sub: 'APP LOGIC',     num: '01', v: '--rz-purple',  fb: '#7d14d7' }
2924    ];
2925    var SCAN_ORDER = [3, 2, 1, 0];                 /* attacker works down: UI → NETWORK */
2926    var PIN_K = 1, PIN_U = 72, PIN_V = 229;        /* finding pin on STORAGE (front rim of the DB; clears the plane above) */
2927    var SPECTRUM = ['--rz-purple', '--rz-crimson', '--rz-lime', '--rz-indigo', '--rz-yellow'];
2928    var SPECTRUM_FB = ['#7d14d7', '#f50034', '#85f403', '#5801f5', '#e6e414'];
2929  
2930    /* ---- timeline (seconds) ---- */
2931    var T_EXPLODE = 0.8, EXPLODE_DUR = 1.25, STAGGER = 0.08;
2932    var T_SCAN = 2.7, SCAN_DUR = 1.5;
2933    var T_PIN = T_SCAN + 2 * SCAN_DUR + 1.2;          /* 6.9 — as the STORAGE sweep lands */
2934    var T_HOLD = T_SCAN + 4 * SCAN_DUR;               /* 8.7 */
2935    var T_COLLAPSE = T_HOLD + 1.4;                    /* 10.1 */
2936    var LOOP = T_COLLAPSE + EXPLODE_DUR + 3 * STAGGER + 1.5;   /* ≈ 13.1 */
2937  
2938    var reduce = false;
2939    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
2940    var nowFn = (window.performance && performance.now) ? function () { return performance.now(); } : function () { return Date.now(); };
2941  
2942    /* ---- easing / helpers ---- */
2943    function seg(t, a, b) { var p = (t - a) / (b - a); return p <= 0 ? 0 : (p >= 1 ? 1 : p); }
2944    function ioQuint(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; }
2945    function ioCubic(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
2946    function outCubic(p) { return 1 - Math.pow(1 - p, 3); }
2947    function outBack(p) { var c1 = 1.2, c3 = c1 + 1; return 1 + c3 * Math.pow(p - 1, 3) + c1 * Math.pow(p - 1, 2); }
2948    function fOx(w, px) { return w + ' ' + px + 'px Oxanium, sans-serif'; }
2949    function fMono(w, px) { return w + ' ' + px + 'px "Courier New", Courier, monospace'; }
2950  
2951    /* colour parsing (#rgb / #rrggbb / rgb[a]()) with a memo so per-frame reads stay cheap */
2952    var memo = {};
2953    function rgb(str, fb) {
2954      var s = (str || '').replace(/^\s+|\s+$/g, ''), m, out;
2955      if (memo[s]) return memo[s];
2956      if ((m = /^#([0-9a-f]{3})$/i.exec(s))) out = [parseInt(m[1].charAt(0) + m[1].charAt(0), 16), parseInt(m[1].charAt(1) + m[1].charAt(1), 16), parseInt(m[1].charAt(2) + m[1].charAt(2), 16)];
2957      else if ((m = /^#([0-9a-f]{6})$/i.exec(s))) out = [parseInt(m[1].substr(0, 2), 16), parseInt(m[1].substr(2, 2), 16), parseInt(m[1].substr(4, 2), 16)];
2958      else if ((m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i.exec(s))) out = [+m[1], +m[2], +m[3]];
2959      else out = rgb(fb, '#000000');
2960      memo[s] = out;
2961      return out;
2962    }
2963    function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + (a < 0 ? 0 : a > 1 ? 1 : a) + ')'; }
2964    function mix(a, b, t) { return [Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), Math.round(a[2] + (b[2] - a[2]) * t)]; }
2965  
2966    /* design tokens, read once per frame */
2967    function theme() {
2968      var cs = null, i, T;
2969      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
2970      function v(name, fb) { var s = cs ? cs.getPropertyValue(name) : ''; s = s ? s.replace(/^\s+|\s+$/g, '') : ''; return s || fb; }
2971      T = { dark: document.documentElement.getAttribute('data-theme') === 'dark', acc: [], spec: [] };
2972      T.paper = rgb(v('--rz-paper', T.dark ? '#0e0f14' : '#ffffff'), T.dark ? '#0e0f14' : '#ffffff');
2973      T.ink = rgb(v('--rz-ink', T.dark ? '#eceef4' : '#14151a'), T.dark ? '#eceef4' : '#14151a');
2974      for (i = 0; i < 4; i++) T.acc.push(rgb(v(LAYERS[i].v, LAYERS[i].fb), LAYERS[i].fb));
2975      for (i = 0; i < 5; i++) T.spec.push(rgb(v(SPECTRUM[i], SPECTRUM_FB[i]), SPECTRUM_FB[i]));
2976      return T;
2977    }
2978  
2979    /* rounded-rect path (arcTo → projects to true ellipses under the plane transform) */
2980    function rr(x, y, w, h, r) {
2981      g.moveTo(x + r, y);
2982      g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r);
2983      g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r);
2984      g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r);
2985      g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r);
2986      g.closePath();
2987    }
2988    /* letter-spaced text; align 'left' | 'right' | 'center' */
2989    function tracked(txt, x, y, px, em, align) {
2990      var sp = px * em, i, n = txt.length, widths = [], total = 0, cx, prev = g.textAlign;
2991      for (i = 0; i < n; i++) { widths[i] = g.measureText(txt.charAt(i)).width; total += widths[i]; }
2992      total += sp * (n - 1);
2993      cx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x);
2994      g.textAlign = 'left';
2995      for (i = 0; i < n; i++) { g.fillText(txt.charAt(i), cx, y); cx += widths[i] + sp; }
2996      g.textAlign = prev;
2997      return total;
2998    }
2999  
3000    /* ---- plane space: paths are built under the projection, strokes happen after restore (crisp 1px) ---- */
3001    function shellXf() { g.transform(UX, UY, VX, VY, OX, OY); }
3002    function planeXf(lift) { g.transform(UX, UY, VX, VY, OX + INS * (UX + VX), OY + INS * (UY + VY) + lift * ZY); }
3003    function proj(lift, u, v) { return { x: OX + (INS + u) * UX + (INS + v) * VX, y: OY + (INS + u) * UY + (INS + v) * VY + lift * ZY }; }
3004    function planePath(lift) { g.save(); planeXf(lift); g.beginPath(); rr(0, 0, SW, SH, SR);
3004 g.restore(); }
3005    function glyphXf(lift) { planeXf(lift); g.translate(0, GY); g.scale(GS, GS); }
3006  
3007    /* ---- layer glyphs (100×190 space, same art as the methodology planes); part = 'ink' | 'acc' | 'accFill' ---- */
3008    function gRR(x, y, w, h, r) { rr(x, y, w, h, r); }
3009    function gLine(x1, y1, x2, y2) { g.moveTo(x1, y1); g.lineTo(x2, y2); }
3010    function gPoly(pts) { var i; g.moveTo(pts[0], pts[1]); for (i = 2; i < pts.length; i += 2) g.lineTo(pts[i], pts[i + 1]); }
3011    function gCircle(cx, cy, r) { g.moveTo(cx + r, cy); g.arc(cx, cy, r, 0, Math.PI * 2); }
3012    function gEllipse(cx, cy, rx, ry) { g.save(); g.translate(cx, cy); g.scale(1, ry / rx); g.moveTo(rx, 0); g.arc(0, 0, rx, 0, Math.PI * 2); g.restore(); }
3013    function cylinderCurve(x, y) { /* front rim of a 48-wide cylinder, apex 8 below y */
3014      g.moveTo(x, y); g.bezierCurveTo(x, y + 4, x + 11, y + 8, x + 24, y + 8); g.bezierCurveTo(x + 37, y + 8, x + 48, y + 4, x + 48, y);
3015    }
3016    /* SVG-style circular arc from the current point (x1,y1) to (x2,y2); small arc, sweep = 1 → clockwise on screen */
3017    function svgArc(x1, y1, r, x2, y2) {
3018      var dx = x2 - x1, dy = y2 - y1, d = Math.sqrt(dx * dx + dy * dy) / 2, h, mx, my, cx, cy;
3019      if (r < d) r = d;
3020      h = Math.sqrt(Math.max(0, r * r - d * d));
3021      mx = (x1 + x2) / 2; my = (y1 + y2) / 2;
3022      cx = mx - dy / (2 * d) * h; cy = my + dx / (2 * d) * h;
3023      g.arc(cx, cy, r, Math.atan2(y1 - cy, x1 - cx), Math.atan2(y2 - cy, x2 - cx), false);
3024    }
3025    function cloud(x, y) {  /* methodology cloud path, translated */
3026      g.moveTo(x + 30, y + 44);
3027      svgArc(x + 30, y + 44, 12, x + 53, y + 40);
3028      svgArc(x + 53, y + 40, 10, x + 62, y + 56);
3029      g.lineTo(x + 32, y + 56);
3030      svgArc(x + 32, y + 56, 9, x + 30, y + 44);
3031      g.closePath();
3032    }
3033    function brace(x, y, dir) {  /* { (dir 1) or } (dir -1), 40 tall, opening at x */
3034      g.moveTo(x, y);
3035      g.bezierCurveTo(x - 8 * dir, y, x - 8 * dir, y + 8, x - 8 * dir, y + 14);
3036      g.bezierCurveTo(x - 8 * dir, y + 20, x - 14 * dir, y + 20, x - 14 * dir, y + 20);
3037      g.bezierCurveTo(x - 8 * dir, y + 20, x - 8 * dir, y + 20, x - 8 * dir, y + 26);
3038      g.bezierCurveTo(x - 8 * dir, y + 32, x - 8 * dir, y + 40, x, y + 40);
3039    }
3040    /* Lower planes are partly hidden by the plane above while exploded, so each glyph keeps its
3041       content in the always-visible L (left strip u<40 · bottom strip v>154 in glyph space). */
3042    var GLYPH = [
3043      function (part) {  /* NETWORK: traffic arrows (left) · cloud (front, accent) · brackets */
3044        if (part === 'ink') {
3045          gLine(33, 62, 33, 40); gLine(33, 62, 29, 57); gLine(33, 62, 37, 57);
3046          gPoly([76, 164, 71, 170, 76, 176]); gPoly([88, 164, 93, 170, 88, 176]);
3047        } else if (part === 'acc') {
3048          gLine(21, 40, 21, 62); gLine(21, 40, 17, 45); gLine(21, 40, 25, 45);
3049          cloud(0, 126);
3050          gLine(80, 166, 84, 174);
3051        }
3052      },
3053      function (part) {  /* STORAGE: key (left) · database cylinder (front-right, carries the pin) */
3054        if (part === 'ink') {
3055          gCircle(23, 42, 7);
3056          gLine(28, 47, 41, 60); gLine(35, 54, 39, 50); gLine(39, 58, 43, 54);
3057          gLine(34, 160, 34, 178); gLine(82, 160, 82, 178);
3058          cylinderCurve(34, 178); cylinderCurve(34, 169);
3059        } else if (part === 'acc') {
3060          gEllipse(58, 160, 24, 8);
3061        }
3062      },
3063      function (part) {  /* RUNTIME: IPC boxes (left) · hook braces (front) */
3064        if (part === 'ink') {
3065          gRR(13, 38, 22, 16, 3); gRR(13, 72, 22, 16, 3);
3066          brace(30, 152, 1); brace(70, 152, -1);
3067        } else if (part === 'acc') {
3068          gLine(20, 58, 20, 68); gLine(28, 68, 28, 58);
3069          gPoly([17, 65, 20, 68, 23, 65]); gPoly([25, 61, 28, 58, 31, 61]);
3070        } else if (part === 'accFill') {
3071          gCircle(50, 172, 3);
3072        }
3073      },
3074      function (part) {  /* UI: wireframe screen */
3075        if (part === 'ink') {
3076          gRR(18, 26, 64, 20, 3); gRR(18, 54, 64, 14, 3); gRR(18, 76, 64, 14, 3);
3077        } else if (part === 'acc') {
3078          gRR(18, 10, 30, 6, 3); gRR(18, 120, 42, 12, 6); gLine(70, 126, 82, 126);
3079        }
3080      }
3081    ];
3082  
3083    /* ---- animation state from loop time ---- */
3084    function state(t) {
3085      var s = { lift: [0, 0, 0, 0], hl: [0, 0, 0, 0], sweep: [-1, -1, -1, -1], label: [0, 0, 0, 0],
3086                pin: 0, lead: 0, chip: 0, explode: 0, scanK: -1, read: '' };
3087      var k, i, e, up, dn, lt, off, fadeOut;
3088      fadeOut = 1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.4);
3089      for (k = 0; k < 4; k++) {
3090        off = (3 - k) * STAGGER;
3091        up = ioQuint(seg(t, T_EXPLODE + off, T_EXPLODE + off + EXPLODE_DUR));
3092        dn = ioQuint(seg(t, T_COLLAPSE + k * STAGGER, T_COLLAPSE + k * STAGGER + EXPLODE_DUR));
3093        e = up * (1 - dn);
3094        s.lift[k] = GAP * k * e;
3095        if (k === 3) s.explode = e;
3096        s.label[k] = outCubic(seg(t, T_EXPLODE + off + 0.55, T_EXPLODE + off + 1.15)) * fadeOut;
3097      }
3098      for (i = 0; i < 4; i++) {
3099        k = SCAN_ORDER[i];
3100        lt = t - (T_SCAN + i * SCAN_DUR);
3101        if (lt >= 0 && lt < SCAN_DUR) {
3102          s.hl[k] = ioCubic(seg(lt, 0, 0.3)) * (1 - ioCubic(seg(lt, 1.2, 1.5)));
3103          s.sweep[k] = ioCubic(seg(lt, 0.2, 1.2));
3104          s.lift[k] += HL_LIFT * s.hl[k];
3105          s.scanK = k;
3106        }
3107      }
3108      s.pin = outBack(seg(t, T_PIN, T_PIN + 0.45)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3));
3109      s.lead = ioCubic(seg(t, T_PIN + 0.3, T_PIN + 0.7)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3));
3110      s.chip = outCubic(seg(t, T_PIN + 0.6, T_PIN + 0.95)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3));
3111      if (t < T_EXPLODE + 0.5 || t >= T_COLLAPSE + 0.6) s.read = 'ASSEMBLED · 04 LAYERS';
3112      else if (t < T_SCAN) s.read = 'EXPLODED · 04 LAYERS';
3113      else if (s.scanK >= 0) s.read = 'SCAN · ' + LAYERS[s.scanK].num + ' ' + LAYERS[s.scanK].name;
3114      else if (t < T_COLLAPSE + 0.6) s.read = 'FINDINGS · 01 OPEN';
3115      return s;
3116    }
3117  
3118    /* readout crossfade (discrete text, soft change) */
3119    var readTxt = '', readSince = 0;
3120    function readAlpha(txt) {
3121      var n = nowFn();
3122      if (txt !== readTxt) { readTxt = txt; readSince = n; }
3123      return Math.min(1, (n - readSince) / 280);
3124    }
3125  
3126    /* ---- drawing ---- */
3127    function drawFurniture(T, s) {
3128      var i, x, y, a = 14, ins = 4, readA = reduce ? 1 : readAlpha(s.read);
3129      /* corner crop-brackets (blueprint cue, as on the methodology figure) */
3130      g.lineWidth = 1.5; g.strokeStyle = rzInk(0.30); g.beginPath();
3131      g.moveTo(ins, ins + a); g.lineTo(ins, ins); g.lineTo(ins + a, ins);
3132      g.moveTo(W - ins - a, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + a);
3133      g.moveTo(W - ins, H - ins - a); g.lineTo(W - ins, H - ins); g.lineTo(W - ins - a, H - ins);
3134      g.moveTo(ins + a, H - ins); g.lineTo(ins, H - ins); g.lineTo(ins, H - ins - a);
3135      g.stroke();
3136      /* status chip (top-left): eyebrow + spectrum bar + micro-readout */
3137      x = 18; y = 18;
3138      g.beginPath(); rr(x, y, 158, 40, 5);
3139      g.fillStyle = rgba(T.paper, 0.92); g.fill();
3140      g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke();
3141      for (i = 0; i < 5;
3141 i++) { g.fillStyle = rgba(T.spec[i], 1); g.fillRect(x + 12 + i * 5, y + 11, 5, 4); }
3142      g.font = fOx(600, 9); g.fillStyle = rzInk(0.85); g.textBaseline = 'alphabetic';
3143      tracked('DEVICE TEARDOWN', x + 44, y + 18, 9, 0.14, 'left');
3144      g.font = fMono(400, 8.5); g.fillStyle = rzInk(0.55 * readA);
3145      tracked(s.read, x + 12, y + 32, 8.5, 0.06, 'left');
3146      /* figure caption (bottom-left) */
3147      g.font = fMono(400, 8); g.fillStyle = rzInk(0.5);
3148      tracked('FIG · EXPLODED VIEW', 18, H - 16, 8, 0.14, 'left');
3149      /* z-axis ruler: long ticks at the four exploded layer heights (the label rows), minor ticks every 13 z */
3150      x = 64; y = OY + INS * (UY + VY) + 22 * VY;
3151      g.lineWidth = 1; g.strokeStyle = rzInk(0.22); g.beginPath();
3152      g.moveTo(x, y + 8); g.lineTo(x, y + 3 * GAP * ZY - 8);
3153      for (i = 0; i <= 3; i++) { g.moveTo(x - 4, y + i * GAP * ZY); g.lineTo(x + 4, y + i * GAP * ZY); }
3154      for (i = 1; i < 12; i++) { g.moveTo(x - 2, y + i * 13 * ZY); g.lineTo(x + 2, y + i * 13 * ZY); }
3155      g.stroke();
3156      g.font = fMono(400, 7.5); g.fillStyle = rzInk(0.45); g.textAlign = 'center';
3157      g.fillText('Z', x, y + 3 * GAP * ZY - 14);
3158      g.textAlign = 'left';
3159    }
3160  
3161    function drawShell(T) {
3162      g.save(); shellXf(); g.beginPath(); rr(0, 0, PW, PH, PR); g.restore();
3163      g.save();
3164      g.shadowColor = T.dark ? 'rgba(0,0,0,0.55)' : 'rgba(20,21,26,0.14)';
3165      g.shadowBlur = 22; g.shadowOffsetY = 10;
3166      g.fillStyle = rgba(mix(T.paper, T.ink, 0.03), 0.96); g.fill();
3167      g.restore();
3168      g.lineWidth = 1.4; g.strokeStyle = rzInk(0.72); g.stroke();
3169      /* side keys along the back-left edge */
3170      g.save(); shellXf(); g.beginPath();
3171      g.moveTo(-3, 58); g.lineTo(-3, 76); g.moveTo(-3, 84); g.lineTo(-3, 102); g.moveTo(PW + 3, 66); g.lineTo(PW + 3, 92);
3172      g.restore();
3173      g.lineWidth = 1.6; g.strokeStyle = rzInk(0.45); g.stroke();
3174    }
3175  
3176    function drawPlane(k, s, T) {
3177      var acc = T.acc[k], hl = s.hl[k], lift = s.lift[k], u, v, sw, grad, c, lifted;
3178      lifted = k > 0 ? Math.min(1, lift / (GAP * k)) : 0;
3179      /* body + shadow */
3180      planePath(lift);
3181      g.save();
3182      if (lifted > 0.02) {
3183        g.shadowColor = T.dark ? 'rgba(0,0,0,' + (0.5 * lifted).toFixed(3) + ')' : 'rgba(20,21,26,' + (0.13 * lifted).toFixed(3) + ')';
3184        g.shadowBlur = 18; g.shadowOffsetY = 9;
3185      }
3186      g.fillStyle = rgba(mix(mix(T.paper, T.ink, 0.05), acc, 0.13 * hl), 0.97); g.fill();
3187      g.restore();
3188      /* interior (clipped) */
3189      g.save(); planePath(lift); g.clip();
3190      g.save(); planeXf(lift); g.beginPath();
3191      for (u = 7.5; u < SW; u += 15) for (v = 7.5; v < SH; v += 15) g.rect(u - 0.6, v - 0.6, 1.2, 1.2);
3192      g.restore(); g.fillStyle = rzInk(0.16); g.fill();
3193      g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('ink'); g.restore();
3194      g.lineWidth = 1.15; g.lineJoin = 'round'; g.lineCap = 'round'; g.strokeStyle = rzInk(0.5 + 0.25 * hl); g.stroke();
3195      g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('acc'); g.restore();
3196      g.lineWidth = 1.35; g.strokeStyle = rgba(acc, 0.92); g.stroke();
3197      g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('accFill'); g.restore();
3198      g.fillStyle = rgba(acc, 0.95); g.fill();
3199      if (k === 3) {   /* glass: notch + home indicator */
3200        g.save(); planeXf(lift); g.beginPath(); rr(SW / 2 - 18, 6, 36, 7, 3.5); rr(SW / 2 - 16, SH - 10, 32, 3, 1.5); g.restore();
3201        g.lineWidth = 1; g.strokeStyle = rzInk(0.55); g.stroke();
3202      }
3203      if (s.sweep[k] >= 0 && hl > 0) {   /* scan: soft band trailing a thin accent line */
3204        sw = s.sweep[k] * SH;
3205        g.save(); planeXf(lift);
3206        grad = g.createLinearGradient(0, sw - 46, 0, sw);
3207        grad.addColorStop(0, rgba(acc, 0)); grad.addColorStop(1, rgba(acc, 0.2 * hl));
3208        g.fillStyle = grad; g.fillRect(-2, sw - 46, SW + 4, 46);
3209        g.beginPath(); g.moveTo(-2, sw); g.lineTo(SW + 2, sw);
3210        g.restore();
3211        g.lineWidth = 1.4; g.strokeStyle = rgba(acc, 0.95 * hl); g.stroke();
3212      }
3213      g.restore();
3214      /* border (ink → accent while scanned) + accent front edge */
3215      planePath(lift);
3216      c = mix(T.ink, acc, hl);
3217      g.lineWidth = 1; g.strokeStyle = rgba(c, 0.30 + 0.65 * hl); g.stroke();
3218      g.save(); planeXf(lift); g.beginPath(); g.moveTo(SR, SH); g.lineTo(SW - SR, SH);
3218 g.restore();
3219      g.lineWidth = 2.2; g.lineCap = 'butt'; g.strokeStyle = rgba(acc, 0.9); g.stroke();
3220      /* number tab (lives in the plane, like the CSS tab) */
3221      g.save(); planeXf(lift); g.beginPath(); rr(7, 7, 20, 13, 3); g.restore();
3222      g.fillStyle = rgba(T.paper, 0.85); g.fill();
3223      g.lineWidth = 1; g.strokeStyle = rgba(acc, 0.95); g.stroke();
3224      g.save(); planeXf(lift);
3225      g.font = fMono(700, 8.5); g.fillStyle = rgba(acc, 1); g.textAlign = 'center'; g.textBaseline = 'middle';
3226      g.fillText(LAYERS[k].num, 17, 13.5);
3227      g.restore();
3228      g.textAlign = 'left'; g.textBaseline = 'alphabetic';
3229    }
3230  
3231    function drawLabels(s, T) {
3232      var k, a, p, xr, nw, acc, L;
3233      for (k = 0; k < 4; k++) {
3234        a = s.label[k]; if (a <= 0.01) continue;
3235        L = LAYERS[k]; acc = T.acc[k];
3236        p = proj(s.lift[k], 0, 22);
3237        xr = p.x - 26;
3238        /* leader + anchor dot (accent while scanned) */
3239        g.lineWidth = 1; g.strokeStyle = s.hl[k] > 0 ? rgba(acc, a * (0.4 + 0.6 * s.hl[k])) : rzInk(0.4 * a);
3240        g.beginPath(); g.moveTo(p.x - 3, p.y); g.lineTo(xr + 6, p.y); g.stroke();
3241        g.beginPath(); g.arc(p.x, p.y, 1.8, 0, Math.PI * 2);
3242        g.fillStyle = s.hl[k] > 0 ? rgba(acc, a) : rzInk(0.65 * a); g.fill();
3243        /* 0n NAME / sub-line */
3244        g.font = fOx(700, 11); g.fillStyle = rzInk((0.72 + 0.28 * s.hl[k]) * a);
3245        nw = tracked(L.name, xr, p.y + 4, 11, 0.08, 'right');
3246        g.font = fMono(700, 9); g.fillStyle = rgba(acc, a); g.textAlign = 'right';
3247        g.fillText(L.num, xr - nw - 7, p.y + 4);
3248        g.font = fMono(400, 8); g.fillStyle = rzInk(0.5 * a);
3249        tracked(L.sub, xr, p.y + 16, 8, 0.06, 'right');
3250        g.textAlign = 'left';
3251      }
3252    }
3253  
3254    function drawPin(s, T) {
3255      var acc = T.acc[PIN_K], p, hx, hy, pop, a, lx, cx, cy, cw = CHIP_W, ch = 32;
3256      if (s.pin <= 0.01) return;
3257      pop = s.pin; a = Math.min(1, pop * 2.5);
3258      p = proj(s.lift[PIN_K], PIN_U, PIN_V);
3259      hx = p.x; hy = p.y - 14 * pop;
3260      g.lineWidth = 1.2; g.strokeStyle = rgba(acc, a);
3261      g.beginPath(); g.moveTo(p.x, p.y); g.lineTo(hx, hy); g.stroke();
3262      g.beginPath(); g.arc(p.x, p.y, 1.8, 0, Math.PI * 2); g.fillStyle = rgba(acc, a); g.fill();
3263      g.beginPath(); g.arc(hx, hy, 4.6, 0, Math.PI * 2);
3264      g.fillStyle = rgba(T.paper, a); g.fill();
3265      g.lineWidth = 1.6; g.strokeStyle = rgba(acc, a); g.stroke();
3266      g.beginPath(); g.arc(hx, hy, 1.6, 0, Math.PI * 2); g.fillStyle = rgba(acc, a); g.fill();
3267      /* leader to the chip column */
3268      if (s.lead > 0.01) {
3269        lx = hx + 7 + (CHIP_X - hx - 7) * s.lead;
3270        g.lineWidth = 1; g.strokeStyle = rzInk(0.45 * a);
3271        g.beginPath(); g.moveTo(hx + 7, hy); g.lineTo(lx, hy); g.stroke();
3272      }
3273      /* finding chip (paper, 1px ink border) */
3274      if (s.chip > 0.01) {
3275        cx = CHIP_X + 4 + 6 * (1 - s.chip); cy = hy - ch / 2;
3276        g.save(); g.globalAlpha = s.chip;
3277        g.beginPath(); rr(cx, cy, cw, ch, 4);
3278        g.fillStyle = rgba(T.paper, 0.96); g.fill();
3279        g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke();
3280        g.fillStyle = rgba(acc, 1); g.fillRect(cx + 9, cy + 8, 5, 5);
3281        g.font = fOx(700, 9); g.fillStyle = rzInk(0.88);
3282        tracked('F-01 · HIGH', cx + 20, cy + 13.5, 9, 0.1, 'left');
3283        g.font = fMono(400, 8.5); g.fillStyle = rzInk(0.58);
3284        tracked('SQLITE · CLEARTEXT', cx + 9, cy + 25.5, 8.5, 0.03, 'left');
3285        g.restore();
3286      }
3287    }
3288  
3289    function draw(s) {
3290      var T = theme(), k;
3291      g.clearRect(0, 0, W, H);
3292      g.lineJoin = 'round'; g.lineCap = 'butt'; g.textAlign = 'left'; g.textBaseline = 'alphabetic';
3293      drawFurniture(T, s);
3294      drawShell(T);
3295      for (k = 0; k < 4; k++) drawPlane(k, s, T);
3296      drawLabels(s, T);
3297      drawPin(s, T);
3298    }
3299  
3300    /* ---- run ---- */
3301    if (reduce) {
3302      /* one composed frame: exploded, STORAGE highlighted, finding pinned */
3303      var st = state(T_HOLD + 0.5);
3304      st.hl[PIN_K] = 1; st.read = 'FINDINGS · 01 OPEN';
3305      var drawStatic = function () { draw(st); };
3306      drawStatic();
3307      if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic);
3308      window.addEventListener('resize', drawStatic);
3309      document.addEventListener('rz-theme-change', drawStatic);
3310      return;
3311    }
3312    var raf = 0, running = false, visible = true, t0 = nowFn();
3313    function loop() {
3314      raf = requestAnimationFrame(loop);
3315      draw(state(((nowFn() - t0) / 1000) % LOOP));
3316    }
3317    function start() { if (!running && visible && !document.hidden) { running = true; raf = requestAnimationFrame(loop); } }
3318    function stop() { if (running) { running = false; cancelAnimationFrame(raf); } }
3319    document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
3320    if ('IntersectionObserver' in window) {
3321      new IntersectionObserver(function (entries) {
3322        visible = !!(entries[0] && entries[0].isIntersecting);
3323        if (visible) start(); else stop();
3324      }, { threshold: 0 }).observe(cv);
3325    }
3326    start();
3327  })();
3328
3329  /* ── 14. PHISHING — Social Engineering Funnel ───────────────── */
3330  (function () {
3331    /* ── 14. PHISHING — Campaign mail flow ─────────────────────────
3332       Envelopes leave the target list, cross the mail gateway, open
3333       en route and are held one beat at the lure detector: most pass,
3334       three are flagged crimson (clicked), one turns lime (reported),
3335       one bounces at the gateway, one never opens. Below, the same
3336       five-stage campaign funnel as the methodology section fills in
3337       eased steps as each event happens. Blueprint-on-white, tokens
3338       read at runtime, time-based easing, ~12.4 s loop with a hold. ── */
3339    var cv = document.getElementById('rz-visual-phishing');
3340    if (!cv) return;
3341    var W = 480, H = 480;
3342    var g = cv.getContext('2d');
3343    if (!g) return;
3344    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
3345  
3346    var PI = Math.PI, TAU = PI * 2;
3347  
3348    /* ── lane geometry ── */
3349    var LY = 136;                                   /* lane centre line */
3350    var X_IN = 66, X_GATE = 160, X_BNC = 148, X_DET = 320, X_END = 440;
3351    var GAP = 12;                                   /* half-height of the gateway slot */
3352    var R_DET = 24, R_BRK = 34;                     /* detector ring / bracket square */
3353    var TAG_DY = 38;                                /* verdict tag hangs this far under the lane */
3354  
3355    /* ── funnel geometry ── */
3356    var F_HEAD = 264, F_RULE = 278, F_BASE = 396, F_H = 92, F_BW = 60;
3357    var F_LBL = 412, F_PCT = 426;
3358  
3359    /* ── timeline (seconds) ── */
3360    var LOOP = 12.4, T_FADE = 0.9;
3361    var T0 = 0.7, CAD = 0.95;                       /* first send, cadence */
3362    var D1 = 0.8, D2 = 1.0, D3 = 0.4, D4 = 1.2;     /* list→gate, gate→detector, hold, detector→exit */
3363    var T_A = D1, T_B = D1 + D2, T_C = T_B + D3, T_X = T_C + D4;
3364    var T_EVT = 0.55;                               /* one funnel increment */
3365    var T_TEXT = 0.35;                              /* chip text crossfade */
3366  
3367    /* ── roster: pass | click | bounce | closed | report ── */
3368    var KIND = ['pass', 'click', 'bounce', 'click', 'report', 'closed', 'pass', 'click'];
3369    var N = KIND.length;
3370    var ENV = [], EVT = [[], [], [], [], []], i;
3371    for (i = 0; i < N; i++) {
3372      var k = KIND[i], s = T0 + i * CAD;
3373      var e = {
3374        kind: k, s: s,
3375        opens: k !== 'bounce' && k !== 'closed',
3376        stage: k === 'click' ? 3 : (k === 'report' ? 4 : -1),
3377        acc: k === 'click' ? 2 : 3,                   /* crimson / lime */
3378        tag: k === 'click' ? 'CLICKED' : 'REPORTED'
3379      };
3380      ENV.push(e);
3381      EVT[0].push(s);                                /* targeted */
3382      if (k !== 'bounce') EVT[1].push(s + T_A);      /* delivered */
3383      if (e.opens) EVT[2].push(s + T_A + D2 * 0.5);  /* opened */
3384      if (e.stage > 0) EVT[e.stage].push(s + T_B + 0.25);
3385    }
3386    var STAGES = [
3387      { n: '01', name: 'TARGETED',  lvl: 100, acc: 0 },
3388      { n: '02', name: 'DELIVERED', lvl: 76,  acc: 1 },
3389      { n: '03', name: 'OPENED',    lvl: 54,  acc: 4 },
3390      { n: '04', name: 'CLICKED',   lvl: 36,  acc: 2 },
3391      { n: '05', name: 'REPORTED',  lvl: 22,  acc: 3 }
3392    ];
3393    var N_CLICK = EVT[3].length, N_REP = EVT[4].length;
3394  
3395    /* five equal accents; fall back if a token is missing */
3396    var ACC_KEYS = ['purple', 'indigo', 'crimson', 'lime', 'yellow'];
3397    var ACC_FALL = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414' };
3398    function tokens() {
3399      var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark';
3400      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
3401      function rd(name, fb) {
3402        var v = '';
3403        try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {}
3404        v = v.replace(/^\s+|\s+$/g, '');
3405        return v || fb;
3406      }
3407      var acc = [], j;
3408      for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]]));
3409      return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') };
3410    }
3411  
3412    /* ── easing + small helpers ── */
3413    function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
3414    function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
3415    function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
3416    function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); }
3417    function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); }
3418    function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); }
3419    function dash(a) { if (g.setLineDash) g.setLineDash(a); }
3420    function roundRect(x, y, w, h, r) {
3421      g.beginPath();
3422      g.moveTo(x + r, y);
3423      g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r);
3424      g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r);
3425      g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r);
3426      g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r);
3427      g.closePath();
3428    }
3429    /* letter-spaced text (canvas letterSpacing is not universal) */
3430    function spacedW(txt, sp) {
3431      var w = 0, j;
3432      for (j = 0; j < txt.length; j++) w += g.measureText(txt.charAt(j)).width;
3433      return w + sp * (txt.length - 1);
3434    }
3435    function spaced(txt, x, y, sp, align) {
3436      var w = spacedW(txt, sp), j;
3437      var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x);
3438      g.textAlign = 'left';
3439      for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y); px += g.measureText(txt.charAt(j)).width + sp; }
3440      return w;
3441    }
3442    /* ruler: hairline with ticks standing on it */
3443    function ruler(x0, x1, y) {
3444      var x, major;
3445      g.lineWidth = 1;
3446      g.strokeStyle = rzInk(0.2); line(x0, y, x1, y);
3447      for (x = x0; x <= x1; x += 8) {
3448        major = ((x - x0) % 40) === 0;
3449        g.strokeStyle = rzInk(major ? 0.36 : 0.2);
3450        line(x, y, x, y - (major ? 6 : 3));
3451      }
3452    }
3453    /* hazard stripes clipped to a small band */
3454    function hazard(x, y, w, h, alpha) {
3455      var d;
3456      g.save();
3457      g.beginPath(); g.rect(x, y, w, h); g.clip();
3458      g.strokeStyle = rzInk(alpha); g.lineWidth = 1;
3459      for (d = -h; d < w + h; d += 5) line(x + d, y + h, x + d + h, y);
3460      g.restore();
3461    }
3462    /* corner crop-brackets around a square of half-size r */
3463    function brackets(cx, cy, r, arm, alpha) {
3464      g.strokeStyle = rzInk(alpha); g.lineWidth = 1;
3465      line(cx - r, cy - r, cx - r + arm, cy - r); line(cx - r, cy - r, cx - r, cy - r + arm);
3466      line(cx + r, cy - r, cx + r - arm, cy - r); line(cx + r, cy - r, cx + r, cy - r + arm);
3467      line(cx - r, cy + r, cx - r + arm, cy + r); line(cx - r, cy + r, cx - r, cy + r - arm);
3468      line(cx + r, cy + r, cx + r - arm, cy + r); line(cx + r, cy + r, cx + r, cy + r - arm);
3469    }
3470  
3471    var FONT_LABEL = "600 8.5px 'Oxanium', sans-serif";
3472    var FONT_STAGE = "600 7.5px 'Oxanium', sans-serif";
3473    var FONT_STAGE_N = "800 10px 'Oxanium', sans-serif";
3474    var FONT_MONO = "bold 9px 'Courier New', monospace";
3475    var FONT_MONO_S = "bold 8px 'Courier New', monospace";
3476  
3477    /* chip text crossfade memory (the only non-derived state) */
3478    var chipText = '', chipPrev = '', chipChangedAt = -10;
3479  
3480    /* ── envelope glyph: paper body, ink outline, flap lifts as it opens ── */
3481    function envelope(x, y, a, open, paper) {
3482      if (a <= 0.005) return;
3483      g.globalAlpha = a;
3484      roundRect(x - 12, y - 8, 24, 16, 2);
3485      g.fillStyle = paper; g.fill();
3486      g.strokeStyle = rzInk(0.78); g.lineWidth = 1.2; g.stroke();
3487      var tipY = y + 1 - 16 * open;                  /* flap tip: inside the body → lifted above it */
3488      g.beginPath(); g.moveTo(x - 12, y - 8); g.lineTo(x, tipY); g.lineTo(x + 12, y - 8); g.stroke();
3489      if (open > 0.5) {                                /* the letter shows once the flap is up */
3490        var la = (open - 0.5) * 2;
3491        g.strokeStyle = rzInk(0.42 * la); g.lineWidth = 1;
3492        line(x - 7, y - 1, x + 7, y - 1); line(x - 7, y + 3, x + 3, y + 3);
3493      }
3494      g.globalAlpha = 1;
3495    }
3496  
3497    /* ── per-envelope state at own time o ── */
3498    function envState(e, t) {
3499      var o = t - e.s, p, q;
3500      if (o <= 0 || o >= T_X) return null;
3501      var st = { x: 0, a: 1, open: 0, fa: 0 };
3502      if (e.kind === 'bounce') {
3503        if (o >= T_A + 0.5) return null;
3504        if (o < T_A) { p = o / T_A; st.x = X_IN + (X_BNC - X_IN) * easeInOut(p); st.a = easeOut(clamp01(p / 0.2)); }
3505        else { q = clamp01((o - T_A - 0.1) / 0.4); st.x = X_BNC - 8 * easeOut(q); st.a = 1 - easeInOut(q); }
3506        return st;
3507      }
3508      if (o < T_A) { p = o / T_A; st.x = X_IN + (X_GATE - X_IN) * easeInOut(p); st.a = easeOut(clamp01(p / 0.2)); }
3509      else if (o < T_B) { p = (o - T_A) / D2; st.x = X_GATE + (X_DET - X_GATE) * easeInOut(p); if (e.opens) st.open = easeInOut(clamp01(p / 0.5)); }
3510      else if (o < T_C) { st.x = X_DET; if (e.opens) st.open = 1; }
3511      else { p = (o - T_C) / D4; st.x = X_DET + (X_END - X_DET) * easeInOut(p); if (e.opens) st.open = 1; st.a = 1 - easeInOut(clamp01((p - 0.78) / 0.22)); }
3512      if (e.kind === 'closed') st.a *= 0.42;
3513      if (e.stage > 0) st.fa = easeOut(clamp01((o - (T_B + 0.2)) / 0.3));
3514      return st;
3515    }
3516  
3517    function stageFill(si, t) {
3518      var ev = EVT[si], sum = 0, j;
3519      for (j = 0; j < ev.length; j++) sum += easeOut(clamp01((t - ev[j]) / T_EVT));
3520      return ev.length ? sum / ev.length : 0;
3521    }
3522  
3523    /* ── one composed frame at loop-time t (0..LOOP), absolute time T ── */
3524    function scene(t, T, tk, instant) {
3525      g.clearRect(0, 0, W, H);
3526      g.lineCap = 'round'; g.lineJoin = 'round';
3527      g.textBaseline = 'alphabetic';
3528  
3529      var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE));   /* end-of-cycle reset */
3530      var cycle = Math.floor(T / LOOP);
3531      var acc = tk.acc, paper = tk.paper;
3532      var wave = 'WAVE 0' + (1 + cycle % 4);
3533      var j, e, st, o, p, x, w, a;
3534  
3535      /* — corner crop-brackets (the sheet) — */
3536      g.strokeStyle = rzInk(0.45); g.lineWidth = 1;
3537      var m = 8.5, L = 12;
3538      line(m, m, m + L, m); line(m, m, m, m + L);
3539      line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L);
3540      line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L);
3541      line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L);
3542  
3543      /* — eyebrow + phase readout — */
3544      g.fillStyle = rzInk(0.62); g.font = FONT_LABEL;
3545      var ew = spaced('CAMPAIGN MAIL FLOW', 27, 19, 1.2, 'left');
3546      g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1);
3547      g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right';
3548      g.fillText('PHASE 03 · WAVE SEND', W - 27, 19);
3549  
3550      /* — station labels with leaders — */
3551      g.fillStyle = rzInk(0.6); g.font = FONT_LABEL;
3552      spaced('GATEWAY', X_GATE, 74, 1.2, 'center');
3553      spaced('LURE DETECTOR', X_DET, 74, 1.2, 'center');
3554      g.strokeStyle = rzInk(0.28); g.lineWidth = 1;
3555      line(X_GATE, 80, X_GATE, LY - 46); line(X_DET, 80, X_DET, LY - 44);
3556  
3557      /* — lane: target list glyph, dashed line, end arrow — */
3558      g.strokeStyle = rzInk(0.5); g.lineWidth = 1;
3559      line(43, LY - 6, 53, LY - 6); line(43, LY, 53, LY); line(43, LY + 6, 53, LY + 6);
3560      g.strokeStyle = rzInk(0.22); dash([2, 5]);
3561      line(60, LY, 436, LY);
3562      dash([]);
3563      g.strokeStyle = rzInk(0.5);
3564      line(437, LY - 4, 442, LY); line(437, LY + 4, 442, LY);
3565  
3566      /* — gateway: slotted threshold with a hazard band — */
3567      hazard(X_GATE - 9, LY - 40, 6, 40 - GAP, 0.16);
3568      hazard(X_GATE - 9, LY + GAP, 6, 40 - GAP, 0.16);
3569      g.strokeStyle = rzInk(0.55); g.lineWidth = 1.2;
3570      line(X_GATE, LY - 40, X_GATE, LY - GAP); line(X_GATE, LY + GAP, X_GATE, LY + 40);
3571      g.lineWidth = 1;
3572      line(X_GATE - 3, LY - GAP, X_GATE + 3, LY - GAP); line(X_GATE - 3, LY + GAP, X_GATE + 3, LY + GAP);
3573  
3574      /* — lure detector: ring, cross ticks, crop-bracket corners — */
3575      g.strokeStyle = rzInk(0.22); g.lineWidth = 1; circle(X_DET, LY, R_DET); g.stroke();
3576      g.strokeStyle = rzInk(0.35);
3577      line(X_DET, LY - R_DET, X_DET, LY - R_DET - 4); line(X_DET, LY + R_DET, X_DET, LY + R_DET + 4);
3578      line(X_DET - R_DET, LY, X_DET - R_DET - 4, LY); line(X_DET + R_DET, LY, X_DET + R_DET + 4, LY);
3579      brackets(X_DET, LY, R_BRK, 8, 0.42);
3580  
3581      /* — events at the stations: gate blooms, slot closing, scan arcs — */
3582      for (j = 0; j < N; j++) {
3583        e = ENV[j]; o = t - e.s;
3584        if (e.kind === 'bounce') {
3585          p = clamp01((o - T_A) / 0.5);
3586          if (p > 0 && p < 1) {                        /* the slot closes for a beat */
3587            g.strokeStyle = rzInk(0.7 * Math.sin(p * PI)); g.lineWidth = 1.2;
3588            line(X_GATE, LY - GAP, X_GATE, LY + GAP);
3589          }
3590          continue;
3591        }
3592        p = clamp01((o - T_A) / 0.55);
3593        if (p > 0 && p < 1) {                          /* delivered: ink ring leaves the gate */
3594          g.strokeStyle = rzInk((1 - p) * 0.4); g.lineWidth = 1;
3595          circle(X_GATE, LY, 6 + 16 * easeOutQuint(p)); g.stroke();
3596        }
3597        var sp = clamp01((o - T_B) / 0.3);             /* scan arc sweeps once round the reticle */
3598        var sf = 1 - easeOut(clamp01((o - T_B - 0.3) / 0.3));
3599        if (sp > 0 && sf > 0) {
3600          g.lineWidth = 1.5;
3601          if (e.stage > 0) { g.strokeStyle = acc[e.acc]; g.globalAlpha = 0.9 * sf; }
3602          else { g.strokeStyle = rzInk(0.55 * sf); }
3603          g.beginPath(); g.arc(X_DET, LY, R_DET, -PI / 2, -PI / 2 + TAU * easeInOut(sp)); g.stroke();
3604          g.globalAlpha = 1;
3605        }
3606      }
3607  
3608      /* — envelopes in flight, verdict outlines and tags — */
3609      g.font = FONT_MONO_S;
3610      for (j = 0; j < N; j++) {
3611        e = ENV[j]; st = envState(e, t);
3612        if (!st) continue;
3613        x = st.x;
3614        envelope(x, LY, st.a, st.open, paper);
3615        if (st.fa > 0 && st.a > 0.005) {
3616          a = st.fa * st.a;
3617          g.globalAlpha = a * 0.9;
3618          g.strokeStyle = acc[e.acc]; g.lineWidth = 1.2; dash([3, 3]);
3619          roundRect(x - 15, LY - 11, 30, 22, 3); g.stroke();
3620          dash([]);
3621          /* verdict tag: paper chip, accent swatch, ink text */
3622          w = g.measureText(e.tag).width + 24;
3623          g.globalAlpha = a;
3624          roundRect(x - w / 2, LY + TAG_DY, w, 14, 3);
3625          g.fillStyle = paper; g.fill();
3626          g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
3627          g.fillStyle = acc[e.acc]; g.fillRect(x - w / 2 + 7, LY + TAG_DY + 4.5, 5, 5);
3628          g.fillStyle = rzInk(0.78); g.textAlign = 'left'; g.textBaseline = 'middle';
3629          g.fillText(e.tag, x - w / 2 + 17, LY + TAG_DY + 7.5);
3630          g.textBaseline = 'alphabetic';
3631          g.globalAlpha = 1;
3632        }
3633      }
3634  
3635      /* — status chip — */
3636      var sent = 0, clicked = 0, reported = 0;
3637      for (j = 0; j < N; j++) {
3638        e = ENV[j];
3639        if (t >= e.s) sent++;
3640        if (e.stage > 0 && t >= e.s + T_B + 0.25) { if (e.stage === 3) clicked++; else reported++; }
3641      }
3642      var txt = wave + ' · SENT ' + sent + '/' + N;
3643      if (clicked) txt += ' · CLICKED ' + clicked;
3644      if (reported) txt += ' · REPORTED ' + reported;
3645      if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; }
3646      var te = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT));
3647      if (chipChangedAt > t) te = 1;   /* clock wrapped */
3648      g.font = FONT_MONO;
3649      var cw = g.measureText('WAVE 00 · SENT ' + N + '/' + N + ' · CLICKED ' + N_CLICK + ' · REPORTED ' + N_REP).width + 30;
3650      var ch = 24, cx = W / 2 - cw / 2, cy = 200;
3651      roundRect(cx, cy, cw, ch, 4);
3652      g.fillStyle = paper; g.fill();
3653      g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
3654      g.textAlign = 'center'; g.textBaseline = 'middle';
3655      if (te < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - te));
3655 g.fillText(chipPrev, W / 2, cy + ch / 2 + 0.5); }
3656      g.fillStyle = rzInk(0.78 * te); g.fillText(chipText, W / 2, cy + ch / 2 + 0.5);
3657      g.textBaseline = 'alphabetic';
3658  
3659      /* — funnel panel head: readout + five swatches — */
3660      g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'left';
3661      g.fillText('CAMPAIGN FUNNEL', 40, F_HEAD);
3662      for (j = 0; j < 5; j++) { g.fillStyle = acc[j]; g.fillRect(440 - 6 - (4 - j) * 9, F_HEAD - 7, 6, 6); }
3663      ruler(40, 440, F_RULE);
3664  
3665      /* — dashed spine through the funnel's centre line — */
3666      g.strokeStyle = rzInk(0.22); g.lineWidth = 1; dash([3, 4]);
3667      line(40, F_BASE - F_H / 2, 440, F_BASE - F_H / 2);
3668      dash([]);
3669  
3670      /* — five stepped ink slabs with accent caps — */
3671      for (j = 0; j < 5; j++) {
3672        var sg = STAGES[j], fill = stageFill(j, t) * keep;
3673        var bx = 80 + j * 80 - F_BW / 2, bh = F_H * (sg.lvl / 100) * fill, yy;
3674        if (bh > 0.5) {
3675          g.fillStyle = rzInk(0.86); g.fillRect(bx, F_BASE - bh, F_BW, bh);
3676          g.fillStyle = paper;
3677          for (yy = F_BASE - 10; yy > F_BASE - bh + 1; yy -= 10) g.fillRect(bx, yy, F_BW, 1);
3678          g.fillStyle = acc[sg.acc]; g.fillRect(bx, F_BASE - bh, F_BW, Math.min(3, bh));
3679        }
3680        /* stage number + name, centred as one line */
3681        g.font = FONT_STAGE_N; var nw = g.measureText(sg.n).width;
3682        g.font = FONT_STAGE; var sw = spacedW(sg.name, 0.9);
3683        var lx = 80 + j * 80 - (nw + 4 + sw) / 2;
3684        g.font = FONT_STAGE_N; g.fillStyle = rzInk(0.85); g.textAlign = 'left'; g.fillText(sg.n, lx, F_LBL);
3685        g.font = FONT_STAGE; g.fillStyle = rzInk(0.55); spaced(sg.name, lx + nw + 4, F_LBL, 0.9, 'left');
3686        g.font = FONT_MONO_S; g.fillStyle = rzInk(0.5); g.textAlign = 'center';
3687        g.fillText(Math.round(sg.lvl * fill) + '%', 80 + j * 80, F_PCT);
3688      }
3689      g.strokeStyle = rzInk(0.3); g.lineWidth = 1; line(40, F_BASE + 0.5, 440, F_BASE + 0.5);
3690    }
3691  
3692    /* ── reduced motion: one composed mid-wave frame ── */
3693    var reduce = false;
3694    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
3695    if (reduce) {
3696      var tStatic = ENV[4].s + T_C - 0.08;   /* reported envelope held at the detector, a clicked one leaving */
3697      var drawStatic = function () { scene(tStatic, tStatic, tokens(), true); };
3698      drawStatic();
3699      window.addEventListener('resize', drawStatic);
3700      window.addEventListener('load', drawStatic);
3701      document.addEventListener('rz-theme-change', drawStatic);
3702      if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
3703      return;
3704    }
3705  
3706    /* ── time-based loop; pauses when the tab is hidden or the hero is off-screen ── */
3707    var raf = null, t0 = null, pausedAt = null, inView = true;
3708    function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); }
3709    function frame(ts) {
3710      raf = null;
3711      if (t0 === null) t0 = ts;
3712      var T = Math.max(0, (ts - t0) / 1000);
3713      scene(T % LOOP, T, tokens(), false);
3714      raf = requestAnimationFrame(frame);
3715    }
3716    function start() {
3717      if (raf !== null || document.hidden || !inView) return;
3718      if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt;   /* resume without a time jump */
3719      pausedAt = null;
3720      raf = requestAnimationFrame(frame);
3721    }
3722    function stop() {
3723      if (raf === null) return;
3724      cancelAnimationFrame(raf); raf = null; pausedAt = now();
3725    }
3726    document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
3727    if ('IntersectionObserver' in window) {
3728      try {
3729        new IntersectionObserver(function (entries) {
3730          for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting;
3731          if (inView) start(); else stop();
3732        }, { threshold: 0 }).observe(cv);
3733      } catch (e) {}
3734    }
3735    start();
3736  })();
3737
3738  /* ── 15. CUSTOM PROJECTS — Security Architecture Builder ──────── */
3739  (function () {
3740      'use strict';
3741      /* ── 15. CUSTOM PROJECTS — Modular Assembly ──────────────────────
3742         Nine paper modules (ink squares with accent folder tabs) fly in
3743         from the edges and snap into a 3×3 blueprint grid; tongue-and-
3744         groove pins draw between seated neighbours; on completion the
3745         crop-brackets pulse once and the status chip reads ASSEMBLED.
3746         The build then lifts apart gently and the loop repeats.
3747         Time-based, eased, transparent, token-driven (light + dark). ── */
3748      var cv = document.getElementById('rz-visual-custom');
3749      if (!cv) return;
3750      var W = 480, H = 480;
3751      var g = cv.getContext('2d');
3752      if (!g) return;
3753      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
3754  
3755      /* ---- layout (logical px) ---- */
3756      var TILE = 88, GAP = 20;
3757      var GX = 88, GY = 90;                               /* grid origin: 3×88 + 2×20 = 304, centred */
3758      var FX0 = 56, FY0 = 62, FX1 = 424, FY1 = 422;       /* crop-bracket frame */
3759      var HEAD_Y = 33, RULER_Y = 50, CHIP_Y = 436, CHIP_H = 22;   /* header baseline, ruler, status chip */
3760  
3761      /* ---- timeline (seconds) ---- */
3762      var T_START = 0.35, T_STAG = 0.42, T_FLY = 0.78;    /* fly-in: start, stagger, duration */
3763      var T_CONN = 0.35, T_RIVET = 0.18;                  /* pin draw, rivet pop */
3764      var T_PULSE = 5.0, T_PULSE_D = 1.0;                 /* bracket pulse */
3765      var T_DONE = 5.1;                                   /* chip flips to ASSEMBLED */
3766      var T_OUT = 8.6, T_OUT_STAG = 0.16, T_OUT_D = 0.6;  /* lift-off (reverse order) */
3767      var PERIOD = 12.2;                                  /* full loop incl. holds */
3768      var FLY_D = 84, OUT_D = 22, SET_D = 18;             /* side-column travel (stays inside the canvas), lift-off, middle-column set-down */
3769  
3770      /* ---- easing ---- */
3771      function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
3772      function ioQuint(t) { return t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2; }
3773      function ioCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
3774      function outCubic(t) { return 1 - Math.pow(1 - t, 3); }
3775      function inCubic(t) { return t * t * t; }
3776  
3777      /* ---- modules: reading order, five-accent rotation, nearest-edge approach ---- */
3778      var ACC = ['purple', 'lime', 'crimson', 'indigo', 'yellow'];
3779      var tiles = [], conns = [], i;
3780      for (i = 0; i < 9; i++) {
3781        var c = i % 3, r = Math.floor(i / 3);
3782        var side = (c !== 1);                            /* side columns fly in from the edges … */
3783        var dx = side ? c - 1 : 0, dy = side ? 0 : r - 1;  /* … the middle column is set down from above the board */
3784        tiles.push({
3785          x: GX + c * (TILE + GAP), y: GY + r * (TILE + GAP),
3786          dx: dx, dy: dy,
3787          dIn: side ? FLY_D : SET_D, dOut: side ? OUT_D : SET_D,
3788          rot0: ((i % 2) ? 1 : -1) * (side ? 2.4 : 0.8) * Math.PI / 180,
3789          pop: side ? 0.08 : 0.14,
3790          acc: ACC[i % 5],
3791          num: '0' + (i + 1),
3792          tIn: T_START + i * T_STAG,
3793          tOut: T_OUT + (8 - i) * T_OUT_STAG
3794        });
3795        if (c < 2) conns.push({ a: i, b: i + 1, h: true });
3796        if (r < 2) conns.push({ a: i, b: i + 3, h: false });
3797      }
3798  
3799      function tileState(tl, t) {
3800        var s = { on: false, a: 0, ox: 0, oy: 0, rot: 0, sc: 1, lift: 0, seated: false };
3801        if (t < tl.tIn) return s;
3802        if (t < tl.tIn + T_FLY) {
3803          var p = (t - tl.tIn) / T_FLY, e = ioQuint(p);
3804          s.on = true;
3805          s.a = outCubic(clamp01(p / 0.5));
3806          s.ox = tl.dx * tl.dIn * (1 - e);
3807          s.oy = tl.dy * tl.dIn * (1 - e);
3808          s.rot = tl.rot0 * (1 - e);
3809          s.sc = 1 + tl.pop * (1 - e);
3810          s.lift = 1 - e;
3811          return s;
3812        }
3813        if (t < tl.tOut) { s.on = true; s.a = 1; s.seated = true; return s; }
3814        if (t < tl.tOut + T_OUT_D) {
3815          var q = (t - tl.tOut) / T_OUT_D, f = ioCubic(q);
3816          s.on = true;
3817          s.a = 1 - outCubic(clamp01((q - 0.2) / 0.8));
3818          s.ox = tl.dx * tl.dOut * f;
3819          s.oy = tl.dy * tl.dOut * f;
3820          s.sc = 1 + 0.05 * f;
3821          s.lift = 0.6 * f;
3822          return s;
3823        }
3824        return s;
3825      }
3826  
3827      function meterAlpha(tl, t) {
3828        var seatAt = tl.tIn + T_FLY;
3829        if (t < seatAt) return 0;
3830        if (t < tl.tOut) return outCubic(clamp01((t - seatAt) / 0.25));
3831        return 1 - clamp01((t - tl.tOut) / 0.3);
3832      }
3833  
3834      function connState(cn, t) {
3835        var ta = tiles[cn.a], tb = tiles[cn.b];
3836        var tOn = Math.max(ta.tIn, tb.tIn) + T_FLY + 0.05;
3837        var tOff = Math.min(ta.tOut, tb.tOut) - 0.30;      /* unplug just before either module lifts */
3838        if (t < tOn || t >= tOff + 0.28) return null;
3839        if (t < tOn + T_CONN) return { e: outCubic((t - tOn) / T_CONN), rivet: 0 };
3840        if (t < tOff) return { e: 1, rivet: outCubic(clamp01((t - tOn - T_CONN) / T_RIVET)) };
3841        var q = (t - tOff) / 0.28;
3842        return { e: 1 - inCubic(q), rivet: 1 - clamp01(q / 0.3) };
3843      }
3844  
3845      /* ---- tokens: read once per frame; fall back to the site palette ---- */
3846      function palette() {
3847        var dark = document.documentElement.getAttribute('data-theme') === 'dark';
3848        var cs = null;
3849        try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
3850        function tok(n, fb) {
3851          var v = cs ? cs.getPropertyValue(n) : '';
3852          v = v ? v.replace(/^\s+|\s+$/g, '') : '';
3853          return v || fb;
3854        }
3855        return {
3856          paper:   tok('--rz-paper',   dark ? '#0e0f14' : '#ffffff'),
3857          purple:  tok('--rz-purple',  dark ? '#9a45ee' : '#7d14d7'),
3858          indigo:  tok('--rz-indigo',  dark ? '#7a56ff' : '#5801f5'),
3859          crimson: tok('--rz-crimson', dark ? '#ff3355' : '#f50034'),
3860          lime:    tok('--rz-lime',    dark ? '#8ef513' : '#85f403'),
3861          yellow:  tok('--rz-yellow',  dark ? '#f0ee2a' : '#e6e414'),
3862          shadow:  function (a) { return (dark ? 'rgba(0,0,0,' : 'rgba(20,21,26,') + a + ')'; }
3863        };
3864      }
3865  
3866      /* ---- primitives ---- */
3867      function rrect(x, y, w, h, tl, tr, br, bl) {
3868        g.beginPath();
3869        g.moveTo(x + tl, y);
3870        g.lineTo(x + w - tr, y);
3871        if (tr) g.arcTo(x + w, y, x + w, y + tr, tr); else g.lineTo(x + w, y);
3872        g.lineTo(x + w, y + h - br);
3873        if (br) g.arcTo(x + w, y + h, x + w - br, y + h, br); else g.lineTo(x + w, y + h);
3874        g.lineTo(x + bl, y + h);
3875        if (bl) g.arcTo(x, y + h, x, y + h - bl, bl); else g.lineTo(x, y + h);
3876        g.lineTo(x, y + tl);
3877        if (tl) g.arcTo(x, y, x + tl, y, tl); else g.lineTo(x, y);
3878        g.closePath();
3879      }
3880      function spacedWidth(str, sp) {
3881        var w = 0, k;
3882        for (k = 0; k < str.length; k++) w += g.measureText(str.charAt(k)).width;
3883        return w + sp * (str.length - 1);
3884      }
3885      function spaced(str, x, y, sp, align) {
3886        var w = spacedWidth(str, sp), k;
3887        var cx = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x);
3888        g.textAlign = 'left';
3889        for (k = 0; k < str.length; k++) {
3890          var ch = str.charAt(k);
3891          g.fillText(ch, cx, y);
3892          cx += g.measureText(ch).width + sp;
3893        }
3894        return w;
3895      }
3896      var MONO = '9px "Courier New", Courier, monospace';
3897      var MONO_S = '8px "Courier New", Courier, monospace';
3898      var LABEL = '600 10px "Oxanium", sans-serif';
3899  
3900      /* ---- parts ---- */
3901      function drawHeader(pal, t, seated) {
3902        var k;
3903        g.font = LABEL; g.fillStyle = rzInk(0.55);
3904        var ew = spaced('MODULAR BUILD', FX0, HEAD_Y, 1.4, 'left');
3905        var sx = FX0 + ew + 12;
3906        for (k = 0; k < 5; k++) { g.fillStyle = pal[ACC[k]]; g.fillRect(sx + k * 8, HEAD_Y - 8, 5, 5); }
3907  
3908        /* readout chip: "MOD n/09" + one meter square per module */
3909        g.font = MONO;
3910        var label = 'MOD 0' + seated + '/09', lw = g.measureText(label).width;
3911        var mw = 9 * 7 + 8 * 3, padX = 10, cw = Math.ceil(padX + lw + 10 + mw + padX);
3912        var cx0 = FX1 - cw, cy0 = HEAD_Y - 15;
3913        g.fillStyle = pal.paper; rrect(cx0, cy0, cw, CHIP_H, 4, 4, 4, 4); g.fill();
3914        g.lineWidth = 1; g.strokeStyle = rzInk(0.15);
3915        rrect(cx0 + 0.5, cy0 + 0.5, cw - 1, CHIP_H - 1, 4, 4, 4, 4); g.stroke();
3916        g.textAlign = 'left'; g.fillStyle = rzInk(0.55);
3917        g.fillText(label, cx0 + padX, HEAD_Y);
3918        var mx = FX1 - padX - mw, my = cy0 + (CHIP_H - 7) / 2;
3919        for (k = 0; k < 9; k++) {
3920          var bx = mx + k * 10, a = meterAlpha(tiles[k], t);
3921          g.strokeStyle = rzInk(0.24);
3922          g.strokeRect(bx + 0.5, my + 0.5, 6, 6);
3923          if (a > 0) {
3924            g.globalAlpha = a; g.fillStyle = pal[tiles[k].acc];
3925            g.fillRect(bx, my, 7, 7);
3926            g.globalAlpha = 1;
3927          }
3928        }
3929      }
3930  
3931      function drawRuler() {
3932        var x;
3933        g.lineWidth = 1;
3934        g.strokeStyle = rzInk(0.13);
3935        g.beginPath(); g.moveTo(FX0, RULER_Y + 0.5); g.lineTo(FX1, RULER_Y + 0.5); g.stroke();
3936        g.strokeStyle = rzInk(0.22);
3937        g.beginPath();
3938        for (x = FX0; x <= FX1; x += 6) {
3939          var hh = ((x - FX0) % 30 === 0) ? 7 : 3;
3940          g.moveTo(x + 0.5, RULER_Y); g.lineTo(x + 0.5, RULER_Y - hh);
3941        }
3942        g.stroke();
3943      }
3944  
3945      function drawBrackets(bloom) {
3946        var len = 16 + 4 * bloom, off = 5 * bloom;
3947        var x0 = FX0 - off, y0 = FY0 - off, x1 = FX1 + off, y1 = FY1 + off;
3948        g.strokeStyle = rzInk(0.28 + 0.55 * bloom); g.lineWidth = 2;
3949        g.beginPath();
3950        g.moveTo(x0, y0 + 1); g.lineTo(x0 + len, y0 + 1); g.moveTo(x0 + 1, y0); g.lineTo(x0 + 1, y0 + len);
3951        g.moveTo(x1, y0 + 1); g.lineTo(x1 - len, y0 + 1); g.moveTo(x1 - 1, y0); g.lineTo(x1 - 1, y0 + len);
3952        g.moveTo(x0, y1 - 1); g.lineTo(x0 + len, y1 - 1); g.moveTo(x0 + 1, y1); g.lineTo(x0 + 1, y1 - len);
3953        g.moveTo(x1, y1 - 1); g.lineTo(x1 - len, y1 - 1); g.moveTo(x1 - 1, y1); g.lineTo(x1 - 1, y1 - len);
3954        g.stroke();
3955        g.lineWidth = 1;
3956      }
3957  
3958      function drawSlot(tl, a, d) {
3959        var x = tl.x, y = tl.y, cx = x + TILE / 2, cy = y + TILE / 2;
3960        g.lineWidth = 1;
3961        if (a > 0.01) {
3962          g.strokeStyle = rzInk(0.24 * a); g.setLineDash([4, 4]);
3963          rrect(x + 0.5, y + 0.5, TILE - 1, TILE - 1, 0, 10, 10, 10); g.stroke();
3964          g.setLineDash([]);
3965        }
3966        if (d <= 0.01) return;
3967        a = d;
3968        g.strokeStyle = rzInk(0.2 * a);
3969        g.beginPath();
3970        g.moveTo(cx - 5, cy + 0.5); g.lineTo(cx + 5, cy + 0.5);
3971        g.moveTo(cx + 0.5, cy - 5); g.lineTo(cx + 0.5, cy + 5);
3972        g.stroke();
3973        g.font = MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.32 * a);
3974        g.fillText(tl.num, x + TILE - 10, y + TILE - 9);
3975      }
3976  
3977      function drawConn(cn, st, pal) {
3978        var ta = tiles[cn.a], len = GAP * st.e, T = 8;
3979        if (len < 0.5) return;
3980        g.lineWidth = 1;
3981        if (cn.h) {
3982          var x0 = ta.x + TILE, yc = ta.y + TILE / 2;
3983          g.fillStyle = pal.paper; rrect(x0, yc - T / 2, len, T, 0, 3, 3, 0); g.fill();
3984          g.strokeStyle = rzInk(0.3); rrect(x0, yc - T / 2 + 0.5, len, T - 1, 0, 3, 3, 0); g.stroke();
3985          if (st.rivet > 0) {
3986            g.fillStyle = rzInk(0.5); g.beginPath();
3987            g.arc(x0 + GAP / 2, yc, 1.7 * st.rivet, 0, Math.PI * 2); g.fill();
3988          }
3989        } else {
3990          var xc = ta.x + TILE / 2, y0 = ta.y + TILE;
3991          g.fillStyle = pal.paper; rrect(xc - T / 2, y0, T, len, 0, 0, 3, 3); g.fill();
3992          g.strokeStyle = rzInk(0.3); rrect(xc - T / 2 + 0.5, y0, T - 1, len, 0, 0, 3, 3); g.stroke();
3993          if (st.rivet > 0) {
3994            g.fillStyle = rzInk(0.5); g.beginPath();
3995            g.arc(xc, y0 + GAP / 2, 1.7 * st.rivet, 0, Math.PI * 2); g.fill();
3996          }
3997        }
3998      }
3999  
4000      /* module drawn centred on the origin; caller applies translate / rotate / scale */
4001      function drawTile(tl, st, pal) {
4002        var w = TILE, h = TILE, x = -w / 2, y = -h / 2;
4003        if (st.lift > 0.001) {
4004          g.shadowColor = pal.shadow(0.3 * st.lift);
4005          g.shadowBlur = 26 * st.lift;
4006          g.shadowOffsetY = 12 * st.lift;
4007        }
4008        g.fillStyle = pal.paper;
4009        rrect(x, y, w, h, 0, 10, 10, 10); g.fill();
4010        g.shadowColor = 'rgba(0,0,0,0)'; g.shadowBlur = 0; g.shadowOffsetY = 0;
4011  
4012        g.lineWidth = 1; g.strokeStyle = rzInk(0.26);
4013        rrect(x + 0.5, y + 0.5, w - 1, h - 1, 0, 10, 10, 10); g.stroke();
4014  
4015        /* folder tab: paper, ink hairline, accent top bar, slanted trailing edge */
4016        g.beginPath();
4017        g.moveTo(x + 0.5, y); g.lineTo(x + 0.5, y - 9.5); g.lineTo(x + 25, y - 9.5); g.lineTo(x + 32, y);
4018        g.closePath();
4019        g.fillStyle = pal.paper; g.fill();
4020        g.beginPath();
4021        g.moveTo(x + 0.5, y); g.lineTo(x + 0.5, y - 9.5); g.lineTo(x + 25, y - 9.5); g.lineTo(x + 32, y);
4022        g.strokeStyle = rzInk(0.26); g.stroke();
4023        g.beginPath();
4024        g.moveTo(x, y - 10); g.lineTo(x + 25.5, y - 10); g.lineTo(x + 27.8, y - 7); g.lineTo(x, y - 7);
4025        g.closePath();
4026        g.fillStyle = pal[tl.acc]; g.fill();
4027  
4028        /* module content: title bar, two text hairlines, dashed divider, output line, index */
4029        g.fillStyle = rzInk(0.62); g.fillRect(x + 12, y + 18, 34, 2);
4030        g.fillStyle = rzInk(0.16); g.fillRect(x + 12, y + 29, 54, 1); g.fillRect(x + 12, y + 37, 40, 1);
4031        g.strokeStyle = rzInk(0.16); g.setLineDash([3, 3]);
4032        g.beginPath(); g.moveTo(x + 12, y + 56.5); g.lineTo(x + w - 12, y + 56.5); g.stroke();
4033        g.setLineDash([]);
4034        g.fillStyle = rzInk(0.4); g.fillRect(x + 12, y + 66, 22, 1.5);
4035        g.font = MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.5);
4036        g.fillText(tl.num, x + w - 10, y + h - 9);
4037      }
4038  
4039      function drawChip(pal, done) {
4040        var padX = 12, dot = 6, gapD = 8;
4041        g.font = LABEL;
4042        var wA = spacedWidth('BUILD 01 · ASSEMBLING', 1);
4043        var wB = spacedWidth('BUILD 01 · ASSEMBLED', 1);
4044        var w = Math.ceil(padX * 2 + dot + gapD + Math.max(wA, wB));
4045        var x = FX0, y = CHIP_Y;
4046        g.fillStyle = pal.paper; rrect(x, y, w, CHIP_H, 4, 4, 4, 4); g.fill();
4047        g.lineWidth = 1; g.strokeStyle = rzInk(0.15);
4048        rrect(x + 0.5, y + 0.5, w - 1, CHIP_H - 1, 4, 4, 4, 4); g.stroke();
4049  
4050        var dx = x + padX + dot / 2, dy = y + CHIP_H / 2;
4051        g.strokeStyle = rzInk(0.45);
4052        g.beginPath(); g.arc(dx, dy, dot / 2 - 0.5, 0, Math.PI * 2); g.stroke();
4053        if (done > 0) {
4054          g.globalAlpha = done; g.fillStyle = rzInk(0.85);
4055          g.beginPath(); g.arc(dx, dy, dot / 2, 0, Math.PI * 2); g.fill();
4056          g.globalAlpha = 1;
4057        }
4058        var tx = x + padX + dot + gapD, ty = y + CHIP_H / 2 + 3.5;
4059        if (done < 1) { g.fillStyle = rzInk(0.72 * (1 - done)); spaced('BUILD 01 · ASSEMBLING', tx, ty, 1, 'left'); }
4060        if (done > 0) { g.fillStyle = rzInk(0.85 * done); spaced('BUILD 01 · ASSEMBLED', tx, ty, 1, 'left'); }
4061      }
4062  
4063      /* ---- compose one frame at loop time t (seconds) ---- */
4064      function render(t) {
4065        var pal = palette(), states = [], seated = 0, k, st;
4066        g.clearRect(0, 0, W, H);
4067        g.textBaseline = 'alphabetic'; g.lineCap = 'butt'; g.lineJoin = 'miter';
4068        g.globalAlpha = 1; g.setLineDash([]);
4069  
4070        for (k = 0; k < 9; k++) { states[k] = tileState(tiles[k], t); if (states[k].seated) seated++; }
4071  
4072        drawHeader(pal, t, seated);
4073        drawRuler();
4074        drawBrackets(Math.sin(Math.PI * clamp01((t - T_PULSE) / T_PULSE_D)));
4075  
4076        for (k = 0; k < 9; k++) { st = states[k]; drawSlot(tiles[k], 1 - 0.9 * st.a * (1 - st.lift), 1 - st.a); }
4077        for (k = 0; k < conns.length; k++) { var cs = connState(conns[k], t); if (cs) drawConn(conns[k], cs, pal); }
4078  
4079        /* seated modules first, then anything in the air on top */
4080        var pass;
4081        for (pass = 0; pass < 2; pass++) {
4082          for (k = 0; k < 9; k++) {
4083            st = states[k];
4084            if (!st.on || (pass === 0) !== (st.lift < 0.001)) continue;
4085            g.save();
4086            g.globalAlpha = st.a;
4087            g.translate(tiles[k].x + TILE / 2 + st.ox, tiles[k].y + TILE / 2 + st.oy);
4088            g.rotate(st.rot); g.scale(st.sc, st.sc);
4089            drawTile(tiles[k], st, pal);
4090            g.restore();
4091          }
4092        }
4093  
4094        var done = outCubic(clamp01((t - T_DONE) / 0.35)) * (1 - outCubic(clamp01((t - T_OUT) / 0.4)));
4095        drawChip(pal, done);
4096      }
4097  
4098      /* ---- loop: time-based, pauses when hidden, static frame under reduced motion ---- */
4099      var reduce = false, mq = null;
4100      try { mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; reduce = !!(mq && mq.matches); } catch (e) {}
4101      function now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); }
4102      var t0 = now(), raf = 0, running = false;
4103  
4104      function frame() {
4105        raf = 0;
4106        if (!running) return;
4107        render(((now() - t0) / 1000) % PERIOD);
4108        raf = requestAnimationFrame(frame);
4109      }
4110      function start() { if (running || reduce || document.hidden) return; running = true; if (!raf) raf = requestAnimationFrame(frame); }
4111      function stop() { running = false; if (raf) { cancelAnimationFrame(raf); raf = 0; } }
4112      function drawStatic() { render(7.0); }               /* fully assembled, brackets at rest */
4113      function onEnv() { if (!running) drawStatic(); }     /* re-fit / theme change while static */
4114  
4115      document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
4116      window.addEventListener('resize', onEnv);
4117      window.addEventListener('load', onEnv);
4118      document.addEventListener('rz-theme-change', onEnv);
4119      try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(onEnv); } catch (e) {}
4120      try {
4121        var onMq = function (e) { reduce = !!e.matches; if (reduce) { stop(); drawStatic(); } else { start(); } };
4122        if (mq && mq.addEventListener) mq.addEventListener('change', onMq);
4123        else if (mq && mq.addListener) mq.addListener(onMq);
4124      } catch (e) {}
4125  
4126      if (reduce) drawStatic(); else start();
4127  })();
4128
4129  /* ── 16. CRITICAL INFRASTRUCTURE — ICS/SCADA Monitor ─────────── */
4130  (function () {
4131      /* ── 16. CRITICAL INFRASTRUCTURE — ICS schematic (P&ID sheet) ──
4132         Control net (L2) → segmentation conduit C-01 → fieldbus (L1) →
4133         process piping (L0). Flow dashes run through the pipe and FT-101
4134         traces onto a paper chip (lime). An unauthorized write travels
4135         from the off-sheet IT connector down to PLC-01 (crimson pulse),
4136         the conduit closes (indigo), the trace steadies and the next
4137         attempt is stopped at the gate. Rhymes with the page's zones &
4138         conduits methodology. Blueprint-on-white, tokens read at runtime,
4139         time-based easing, ~12.6 s loop with holds. ── */
4140      var cv = document.getElementById('rz-visual-ics');
4141      if (!cv) return;
4142      var W = 480, H = 480;
4143      var g = cv.getContext('2d');
4144      if (!g) return;
4145      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
4146  
4147      var PI = Math.PI, TAU = PI * 2;
4148      var LOOP = 12.6;                                              /* seconds per cycle */
4149      var T_P1 = 2.3, D_P1 = 1.2, T_HIT = T_P1 + D_P1;              /* write reaches PLC-01 at 3.5 s */
4150      var T_GATE = 5.4, D_GATE = 0.8, T_CLOSED = T_GATE + D_GATE;   /* conduit closed at 6.2 s */
4151      var T_CALM = 6.3, D_CALM = 2.0;                               /* trace steadies 6.3 → 8.3 s */
4152      var T_P2 = 8.6, D_P2 = 0.8, T_BLOCK = T_P2 + D_P2;            /* second attempt stopped at 9.4 s */
4153      var T_OPEN = 11.6, D_OPEN = 0.8;                              /* conduit re-opens for the next cycle */
4154      var T_TEXT = 0.35;                                            /* chip text crossfade */
4155      var SPARK_WIN = 6.0, SPARK_N = 96;                            /* strip-chart window (s) / samples */
4156      var FLOW_PX = 22;                                             /* pipe dash speed, px/s */
4157  
4158      /* ── sheet geometry (logical 480×480) ── */
4159      var Y_L2 = 70, Y_L1 = 176, Y_L0 = 300, SX = 240;
4160      var HMI  = { x: 64,  y: 57,  w: 72, h: 26 };
4161      var PLC  = { x: 118, y: 163, w: 64, h: 26 };
4162      var RTU  = { x: 298, y: 163, w: 64, h: 26 };
4163      var GATE = { x: 216, y: 108, w: 48, h: 24 };
4164      var OFF  = { tip: 372, x: 382, r: 424, hh: 10 };
4165      var TANK = { x: 352, y: 268, w: 52, h: 70 };
4166      var FT   = { x: 270, y: Y_L0, r: 13 };
4167      var PT   = { x: 378, y: 241, r: 13 };
4168      var VALVE = { x: 120, y: Y_L0 };
4169      var PUMP  = { x: 190, y: Y_L0, r: 11 };
4170      var PIPE0 = 56, PIPE1 = TANK.x;
4171      var SPARK = { x: 56, y: 342, w: 176, h: 62 };
4172      var CHIP_Y = 428, CHIP_H = 24;
4173      var PATH1 = [[OFF.tip, Y_L2], [SX, Y_L2], [SX, Y_L1], [PLC.x + PLC.w, Y_L1]];
4174      var PATH2 = [[OFF.tip, Y_L2], [SX, Y_L2], [SX, GATE.y - 4]];   /* stops against the closed conduit */
4175  
4176      /* ── tokens: five equal accents + paper, read at runtime with fallbacks ── */
4177      var FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' };
4178      function tokens() {
4179        var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark';
4180        try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
4181        function rd(name, fb) {
4182          var v = '';
4183          try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {}
4184          v = v.replace(/^\s+|\s+$/g, '');
4185          return v || fb;
4186        }
4187        return {
4188          purple: rd('--rz-purple', FALL.purple), lime: rd('--rz-lime', FALL.lime),
4189          crimson: rd('--rz-crimson', FALL.crimson), indigo: rd('--rz-indigo', FALL.indigo),
4190          yellow: rd('--rz-yellow', FALL.yellow),
4191          paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff')
4192        };
4193      }
4194  
4195      /* ── easing + drawing helpers ── */
4196      function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
4197      function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
4198      function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
4199      function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); }
4200      function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); }
4201      function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); }
4202      function roundRect(x, y, w, h, r) {
4203        g.beginPath();
4204        g.moveTo(x + r, y);
4205        g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r);
4206        g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r);
4207        g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r);
4208        g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r);
4209        g.closePath();
4210      }
4211      function dash(arr) { if (g.setLineDash) g.setLineDash(arr); }
4212      /* letter-spaced text (canvas letterSpacing is not universal) */
4213      function spaced(txt, x, y, sp, align) {
4214        var ws = [], w = 0, j;
4215        for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; }
4216        w += sp * (txt.length - 1);
4217        var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x);
4218        g.textAlign = 'left';
4219        for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y);
4219 px += ws[j] + sp; }
4220        return w;
4221      }
4222      /* orthogonal polyline helpers (probe routes) */
4223      function pathLen(p) {
4224        var L = 0, i;
4225        for (i = 1; i < p.length; i++) L += Math.abs(p[i][0] - p[i - 1][0]) + Math.abs(p[i][1] - p[i - 1][1]);
4226        return L;
4227      }
4228      function pathPoint(p, d) {
4229        var i, sl, f;
4230        for (i = 1; i < p.length; i++) {
4231          sl = Math.abs(p[i][0] - p[i - 1][0]) + Math.abs(p[i][1] - p[i - 1][1]);
4232          if (d <= sl) { f = sl ? d / sl : 0; return [p[i - 1][0] + (p[i][0] - p[i - 1][0]) * f, p[i - 1][1] + (p[i][1] - p[i - 1][1]) * f]; }
4233          d -= sl;
4234        }
4235        return [p[p.length - 1][0], p[p.length - 1][1]];
4236      }
4237      /* ink probe: short fading tail + dot, riding a route */
4238      function probe(path, prog, alpha) {
4239        var d = pathLen(path) * prog, i, a, b, k = 7, step = 4;
4240        g.lineWidth = 1.6;
4241        for (i = k; i >= 1; i--) {
4242          a = pathPoint(path, Math.max(0, d - i * step)); b = pathPoint(path, Math.max(0, d - (i - 1) * step));
4243          g.strokeStyle = rzInk(alpha * 0.5 * (1 - i / (k + 1))); line(a[0], a[1], b[0], b[1]);
4244        }
4245        var h = pathPoint(path, d);
4246        g.fillStyle = rzInk(0.9 * alpha); circle(h[0], h[1], 2.6); g.fill();
4247      }
4248      /* ISA instrument bubble: circle, centre line, tag over number */
4249      function bubble(b, tag, num, paper) {
4250        g.fillStyle = paper; circle(b.x, b.y, b.r); g.fill();
4251        g.strokeStyle = rzInk(0.7); g.lineWidth = 1; circle(b.x, b.y, b.r); g.stroke();
4252        g.strokeStyle = rzInk(0.4); line(b.x - b.r, b.y, b.x + b.r, b.y);
4253        g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.8);
4254        g.fillText(tag, b.x, b.y - 3); g.fillText(num, b.x, b.y + 7.5);
4255      }
4256      function nodeBox(b, label, paper) {
4257        roundRect(b.x, b.y, b.w, b.h, 3); g.fillStyle = paper; g.fill();
4258        g.strokeStyle = rzInk(0.62); g.lineWidth = 1; g.stroke();
4259        g.font = FONT_NODE; g.textBaseline = 'middle'; g.fillStyle = rzInk(0.88);
4260        spaced(label, b.x + b.w / 2, b.y + b.h / 2 + 0.5, 0.6, 'center');
4261        g.textBaseline = 'alphabetic';
4262      }
4263  
4264      var FONT_LABEL  = "600 8.5px 'Oxanium', sans-serif";
4265      var FONT_NODE   = "700 10px 'Oxanium', sans-serif";
4266      var FONT_MONO   = "bold 9px 'Courier New', monospace";
4267      var FONT_MONO_S = "bold 8px 'Courier New', monospace";
4268      var CHIP_TXT = [
4269        'PROCESS NOMINAL · FT-101 42.0 m³/h',
4270        'UNAUTHORIZED WRITE · PLC-01',
4271        'CONDUIT C-01 CLOSED · ZONE ISOLATED',
4272        'WRITE BLOCKED · PROCESS STEADY'
4273      ];
4274  
4275      /* FT-101 flow trace: a smooth function of loop time (no per-frame randomness) */
4276      function flowAt(tl) {
4277        var base = 0.55 * Math.sin(TAU * tl / (LOOP / 2)) + 0.35 * Math.sin(TAU * tl / (LOOP / 5) + 1.1);
4278        var env = easeInOut(clamp01((tl - T_HIT) / 1.0)) * (1 - easeInOut(clamp01((tl - T_CALM) / D_CALM)));
4279        var dist = env * (3.4 * Math.sin(TAU * tl * 1.3) + 1.7 * Math.sin(TAU * tl * 2.9 + 0.7));
4280        return 42.0 + base + dist;
4281      }
4282      function loopT(tau) { return ((tau % LOOP) + LOOP) % LOOP; }
4283  
4284      /* chip text crossfade memory (the only non-derived state) */
4285      var chipIdx = -1, chipPrev = -1, chipChangedAt = -10;
4286  
4287      /* ── one composed frame at loop-time t (0..LOOP), absolute time T ── */
4288      function scene(t, T, tk, instant) {
4289        g.clearRect(0, 0, W, H);
4290        g.lineCap = 'round'; g.lineJoin = 'round';
4291        g.textBaseline = 'alphabetic';
4292        var paper = tk.paper, j, p, d, a;
4293  
4294        /* — corner crop-brackets (the sheet) — */
4295        g.strokeStyle = rzInk(0.45); g.lineWidth = 1;
4296        var m = 8.5, L = 12;
4297        line(m, m, m + L, m); line(m, m, m, m + L);
4298        line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L);
4299        line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L);
4300        line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L);
4301  
4302        /* — eyebrow · swatches · sheet readout · ruler strip — */
4303        g.fillStyle = rzInk(0.62); g.font = FONT_LABEL;
4304        var ew = spaced('ICS SCHEMATIC', 27, 19, 1.2, 'left');
4305        g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1);
4306        g.font = FONT_MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.55);
4307        var rt = 'P&ID · IEC 62443', rw = g.measureText(rt).width;
4308        g.fillText(rt, W - 27, 19);
4309        var sw = [tk.purple, tk.lime, tk.crimson, tk.indigo, tk.yellow], sx0 = W - 27 - rw - 12 - 37;
4310        for (j = 0; j < 5; j++) { g.fillStyle = sw[j]; g.fillRect(sx0 + j * 8, 12, 5, 5); }
4311        g.fillStyle = rzInk(0.14); g.fillRect(27, 32, W - 54, 1);
4312        for (j = 0; j * 6 <= W - 54; j++) {
4313          var major = (j % 5) === 0;
4314          g.fillStyle = rzInk(major ? 0.3 : 0.18);
4315          g.fillRect(27 + j * 6, 32 - (major ? 6 : 3), 1, major ? 6 : 3);
4316        }
4317  
4318        /* — level tags — */
4319        g.font = FONT_MONO_S; g.textAlign = 'left'; g.fillStyle = rzInk(0.42);
4320        g.fillText('L2', 27, Y_L2 + 3); g.fillText('L1', 27, Y_L1 + 3); g.fillText('L0', 27, Y_L0 + 3);
4321  
4322        /* — network lines: control net, spine, fieldbus — */
4323        g.strokeStyle = rzInk(0.5); g.lineWidth = 1.2;
4324        line(HMI.x + HMI.w, Y_L2, OFF.tip, Y_L2);
4325        line(SX, Y_L2, SX, Y_L1);
4326        line(PLC.x + PLC.w, Y_L1, RTU.x, Y_L1);
4327  
4328        /* — instrument signal lines (dashed, orthogonal) — */
4329        g.strokeStyle = rzInk(0.36); g.lineWidth = 1; dash([3, 3]);
4330        g.beginPath(); g.moveTo(PLC.x + PLC.w / 2, PLC.y + PLC.h); g.lineTo(PLC.x + PLC.w / 2, 212); g.lineTo(FT.x, 212); g.lineTo(FT.x, FT.y - FT.r); g.stroke();
4331        g.beginPath(); g.moveTo(RTU.x + RTU.w / 2, RTU.y + RTU.h); g.lineTo(RTU.x + RTU.w / 2, 212); g.lineTo(PT.x, 212); g.lineTo(PT.x, PT.y - PT.r); g.stroke();
4332        dash([]);
4333  
4334        /* — process pipe with moving flow dashes — */
4335        g.strokeStyle = rzInk(0.16); g.lineWidth = 3; line(PIPE0, Y_L0, PIPE1, Y_L0);
4336        dash([7, 9]); g.lineDashOffset = -((T * FLOW_PX) % 16);
4337        g.strokeStyle = rzInk(0.55); g.lineWidth = 1.4; line(PIPE0, Y_L0, PIPE1, Y_L0);
4338        dash([]); g.lineDashOffset = 0;
4339        g.strokeStyle = rzInk(0.5); g.lineWidth = 1; line(PIPE0, Y_L0 - 5, PIPE0, Y_L0 + 5);   /* inlet break */
4340        g.fillStyle = rzInk(0.6);
4341        var ax = [84, 229];
4342        for (j = 0; j < 2; j++) { g.beginPath(); g.moveTo(ax[j] - 4, Y_L0 - 4); g.lineTo(ax[j] + 3, Y_L0); g.lineTo(ax[j] - 4, Y_L0 + 4); g.closePath(); g.fill(); }
4343  
4344        /* — leader from FT-101 to its trace chip — */
4345        g.strokeStyle = rzInk(0.28); g.lineWidth = 1;
4346        g.beginPath(); g.moveTo(FT.x, FT.y + FT.r); g.lineTo(FT.x, 352); g.lineTo(SPARK.x + SPARK.w, 352); g.stroke();
4347        g.fillStyle = rzInk(0.45); circle(SPARK.x + SPARK.w, 352, 1.8); g.fill();
4348  
4349        /* — probes: the write that lands, and the one the conduit stops — */
4350        if (t >= T_P1 && t < T_HIT + 0.2) {
4351          p = easeInOut(clamp01((t - T_P1) / D_P1));
4352          probe(PATH1, p, 1 - clamp01((t - T_HIT) / 0.2));
4353        }
4354        if (t >= T_P2 && t < T_BLOCK + 0.9) {   /* stopped at the conduit, then dissolves */
4355          p = easeInOut(clamp01((t - T_P2) / D_P2));
4356          probe(PATH2, p, 1 - easeInOut(clamp01((t - T_BLOCK) / 0.9)));
4357        }
4358  
4359        /* — segmentation conduit C-01 (gate) — */
4360        var closed = easeInOut(clamp01((t - T_GATE) / D_GATE)) * (1 - easeInOut(clamp01((t - T_OPEN) / D_OPEN)));
4361        roundRect(GATE.x, GATE.y, GATE.w, GATE.h, 3); g.fillStyle = paper; g.fill();
4362        g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.stroke();
4363        var gy = GATE.y + GATE.h / 2, gl = 5 + 15 * closed;
4364        if (closed < 1) {   /* open: the spine shows through, dashed */
4365          g.strokeStyle = rzInk(0.3 * (1 - closed)); dash([2, 3]); line(SX, GATE.y + 3, SX, GATE.y + GATE.h - 3); dash([]);
4366        }
4367        g.lineWidth = 2.5;
4368        g.strokeStyle = rzInk(0.72 * (1 - closed));
4369        line(GATE.x + 4, gy, GATE.x + 4 + gl, gy); line(GATE.x + GATE.w - 4, gy, GATE.x + GATE.w - 4 - gl, gy);
4370        if (closed > 0) {
4371          g.strokeStyle = tk.indigo; g.globalAlpha = closed;
4372          line(GATE.x + 4, gy, GATE.x + 4 + gl, gy); line(GATE.x + GATE.w - 4, gy, GATE.x + GATE.w - 4 - gl, gy);
4373          g.globalAlpha = 1;
4374        }
4375        g.font = FONT_MONO_S; g.textAlign = 'left';
4376        g.fillStyle = rzInk(0.55 * (1 - closed)); g.fillText('C-01', GATE.x + GATE.w + 8, gy + 3);
4377        if (closed > 0) { g.fillStyle = tk.indigo; g.globalAlpha = 0.95 * closed; g.fillText('C-01', GATE.x + GATE.w + 8, gy + 3); g.globalAlpha = 1; }
4378        /* indigo blooms: on close, and when the second write is stopped */
4379        var bt = [T_CLOSED, T_BLOCK];
4380        for (j = 0; j < 2; j++) {
4381          p = clamp01((t - bt[j]) / 0.7);
4382          if (p <= 0 || p >= 1) continue;
4383          d = 2 + 14 * easeOutQuint(p);
4384          roundRect(GATE.x - d, GATE.y - d, GATE.w + 2 * d, GATE.h + 2 * d, 3 + d * 0.5);
4385          g.strokeStyle = tk.indigo; g.lineWidth = 1.2; g.globalAlpha = (1 - p) * 0.55; g.stroke(); g.globalAlpha = 1;
4386        }
4387  
4388        /* — off-sheet connector (L4 · IT), HMI, RTU, junctions — */
4389        g.beginPath(); g.moveTo(OFF.tip, Y_L2); g.lineTo(OFF.x, Y_L2 - OFF.hh); g.lineTo(OFF.r, Y_L2 - OFF.hh);
4390        g.lineTo(OFF.r, Y_L2 + OFF.hh); g.lineTo(OFF.x, Y_L2 + OFF.hh); g.closePath();
4391        g.fillStyle = paper; g.fill(); g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.stroke();
4392        g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.62);
4393        g.fillText('L4 · IT', (OFF.x + OFF.r) / 2 - 2, Y_L2 + 3);
4394        nodeBox(HMI, 'HMI', paper);
4395        nodeBox(RTU, 'RTU-02', paper);
4396        g.fillStyle = rzInk(0.7); circle(SX, Y_L2, 2.2); g.fill(); circle(SX, Y_L1, 2.2); g.fill();
4397  
4398        /* — PLC-01 with the crimson alert — */
4399        var al = easeOut(clamp01((t - T_HIT) / 0.3)) * (1 - easeInOut(clamp01((t - 6.4) / 1.2)));
4400        for (j = 0; j < 3; j++) {
4401          p = clamp01((t - (T_HIT + j * 0.5)) / 1.0);
4402          if (p <= 0 || p >= 1) continue;
4403          d = 3 + 20 * easeOutQuint(p);
4404          roundRect(PLC.x - d, PLC.y - d, PLC.w + 2 * d, PLC.h + 2 * d, 3 + d * 0.5);
4405          g.strokeStyle = tk.crimson; g.lineWidth = 1.2; g.globalAlpha = (1 - p) * 0.55; g.stroke(); g.globalAlpha = 1;
4406        }
4407        nodeBox(PLC, 'PLC-01', paper);
4408        if (al > 0) {
4409          roundRect(PLC.x, PLC.y, PLC.w, PLC.h, 3);
4410          g.strokeStyle = tk.crimson; g.lineWidth = 1.5; g.globalAlpha = al; g.stroke();
4411          g.fillStyle = tk.crimson; circle(PLC.x + PLC.w - 7, PLC.y + 7, 2.2); g.fill();
4412          g.globalAlpha = 1;
4413        }
4414  
4415        /* — process equipment (L0): control valve, pump, FT-101, tank, PT-102 — */
4416        g.fillStyle = paper; g.strokeStyle = rzInk(0.7); g.lineWidth = 1;
4417        g.beginPath(); g.moveTo(VALVE.x - 12, VALVE.y - 7); g.lineTo(VALVE.x - 12, VALVE.y + 7); g.lineTo(VALVE.x, VALVE.y); g.closePath(); g.fill(); g.stroke();
4418        g.beginPath(); g.moveTo(VALVE.x + 12, VALVE.y - 7); g.lineTo(VALVE.x + 12, VALVE.y + 7); g.lineTo(VALVE.x, VALVE.y); g.closePath(); g.fill(); g.stroke();
4419        line(VALVE.x, VALVE.y - 7, VALVE.x, VALVE.y - 17);
4420        g.beginPath(); g.arc(VALVE.x, VALVE.y - 17, 8, PI, TAU); g.closePath(); g.fillStyle = paper; g.fill(); g.stroke();
4421        g.fillStyle = paper;
4421 circle(PUMP.x, PUMP.y, PUMP.r); g.fill(); g.stroke();
4422        g.fillStyle = rzInk(0.62);
4423        g.beginPath(); g.moveTo(PUMP.x - 5, PUMP.y - 6); g.lineTo(PUMP.x - 5, PUMP.y + 6); g.lineTo(PUMP.x + 7, PUMP.y); g.closePath(); g.fill();
4424        g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.5);
4425        g.fillText('FV-101', VALVE.x, Y_L0 + 22); g.fillText('P-101', PUMP.x, Y_L0 + 22);
4426        bubble(FT, 'FT', '101', paper);
4427        roundRect(TANK.x, TANK.y, TANK.w, TANK.h, 6); g.fillStyle = paper; g.fill();
4428        g.save(); g.clip();
4429        g.fillStyle = rzInk(0.06); g.fillRect(TANK.x, TANK.y + 38, TANK.w, TANK.h - 38);
4430        g.strokeStyle = rzInk(0.35); g.lineWidth = 1; line(TANK.x, TANK.y + 38, TANK.x + TANK.w, TANK.y + 38);
4431        g.restore();
4432        roundRect(TANK.x, TANK.y, TANK.w, TANK.h, 6); g.strokeStyle = rzInk(0.7); g.lineWidth = 1; g.stroke();
4433        g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.5);
4434        g.fillText('T-101', TANK.x + TANK.w / 2, TANK.y + TANK.h + 14);
4435        g.strokeStyle = rzInk(0.5); line(PT.x, PT.y + PT.r, PT.x, TANK.y);
4436        bubble(PT, 'PT', '102', paper);
4437  
4438        /* — FT-101 trace chip (paper, lime strip-chart) — */
4439        roundRect(SPARK.x, SPARK.y, SPARK.w, SPARK.h, 4); g.fillStyle = paper; g.fill();
4440        g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
4441        g.font = FONT_MONO_S; g.textAlign = 'left'; g.fillStyle = rzInk(0.55);
4442        g.fillText('FT-101 · FLOW', SPARK.x + 9, SPARK.y + 14);
4443        var vq = flowAt(loopT(Math.floor(t * 2) / 2));
4444        g.font = FONT_MONO; g.textAlign = 'right'; g.fillStyle = rzInk(0.82);
4445        g.fillText(vq.toFixed(1) + ' m³/h', SPARK.x + SPARK.w - 9, SPARK.y + 14);
4446        var px = SPARK.x + 9, pw = SPARK.w - 18, py = SPARK.y + 24, ph = SPARK.h - 32;
4447        g.strokeStyle = rzInk(0.12); dash([2, 4]); line(px, py + ph / 2, px + pw, py + ph / 2); dash([]);
4448        g.strokeStyle = tk.lime; g.lineWidth = 1.5; g.beginPath();
4449        var lx = 0, ly = 0;
4450        for (j = 0; j < SPARK_N; j++) {
4451          var tau = T - SPARK_WIN + SPARK_WIN * j / (SPARK_N - 1);
4452          lx = px + pw * j / (SPARK_N - 1);
4453          ly = py + ph * (1 - clamp01((flowAt(loopT(tau)) - 35) / 14));
4454          if (j === 0) g.moveTo(lx, ly); else g.lineTo(lx, ly);
4455        }
4456        g.stroke();
4457        g.fillStyle = tk.lime; circle(lx, ly, 2.2); g.fill();
4458  
4459        /* — status chip — */
4460        var idx = (t >= T_HIT && t < T_CLOSED) ? 1 : (t >= T_CLOSED && t < T_BLOCK) ? 2 : (t >= T_BLOCK && t < T_OPEN + 0.1) ? 3 : 0;
4461        if (idx !== chipIdx) { chipPrev = chipIdx; chipIdx = idx; chipChangedAt = t; }
4462        var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT));
4463        if (chipChangedAt > t) e = 1;   /* clock wrapped */
4464        var mk = [tk.lime, tk.crimson, tk.indigo, tk.purple];
4465        g.font = FONT_MONO;
4466        var cw = 0;
4467        for (j = 0; j < CHIP_TXT.length; j++) cw = Math.max(cw, g.measureText(CHIP_TXT[j]).width);
4468        cw += 40;
4469        var cx = W / 2 - cw / 2, cy = CHIP_Y;
4470        roundRect(cx, cy, cw, CHIP_H, 4); g.fillStyle = paper; g.fill();
4471        g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
4472        g.textAlign = 'left'; g.textBaseline = 'middle';
4473        if (e < 1 && chipPrev >= 0) {
4474          g.fillStyle = mk[chipPrev]; g.globalAlpha = 1 - e; g.fillRect(cx + 12, cy + CHIP_H / 2 - 2.5, 5, 5); g.globalAlpha = 1;
4475          g.fillStyle = rzInk(0.78 * (1 - e)); g.fillText(CHIP_TXT[chipPrev], cx + 25, cy + CHIP_H / 2 + 0.5);
4476        }
4477        g.fillStyle = mk[chipIdx]; g.globalAlpha = e; g.fillRect(cx + 12, cy + CHIP_H / 2 - 2.5, 5, 5); g.globalAlpha = 1;
4478        g.fillStyle = rzInk(0.78 * e); g.fillText(CHIP_TXT[chipIdx], cx + 25, cy + CHIP_H / 2 + 0.5);
4479        g.textBaseline = 'alphabetic';
4480      }
4481  
4482      /* ── reduced motion: one composed frame (write landed, conduit closed, trace disturbed) ── */
4483      var reduce = false;
4484      try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
4485      if (reduce) {
4486        var drawStatic = function () { scene(6.35, 6.35, tokens(), true); };
4487        drawStatic();
4488        window.addEventListener('resize', drawStatic);
4489        window.addEventListener('load', drawStatic);
4490        document.addEventListener('rz-theme-change', drawStatic);
4491        if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
4492        return;
4493      }
4494  
4495      /* ── time-based loop; pauses when the tab is hidden or the hero is off-screen ── */
4496      var raf = null, t0 = null, pausedAt = null, inView = true;
4497      function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); }
4498      function frame(ts) {
4499        raf = null;
4500        if (t0 === null) t0 = ts;
4501        var T = Math.max(0, (ts - t0) / 1000);
4502        scene(T % LOOP, T, tokens(), false);
4503        raf = requestAnimationFrame(frame);
4504      }
4505      function start() {
4506        if (raf !== null || document.hidden || !inView) return;
4507        if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt;   /* resume without a time jump */
4508        pausedAt = null;
4509        raf = requestAnimationFrame(frame);
4510      }
4511      function stop() {
4512        if (raf === null) return;
4513        cancelAnimationFrame(raf); raf = null; pausedAt = now();
4514      }
4515      document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
4516      if ('IntersectionObserver' in window) {
4517        try {
4518          new IntersectionObserver(function (entries) {
4519            for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting;
4520            if (inView) start(); else stop();
4521          }, { threshold: 0 }).observe(cv);
4522        } catch (e) {}
4523      }
4524      start();
4525  })();
4526
4527  /* ── 17. FINANCE — Transaction Fraud Detection ─────────────── */
4528  (function () {
4529    /* ── 17. FINANCE — Transaction Graph ─────────────────────────────
4530       Rhymes with the page's transaction-monitoring story (DORA / PCI DSS):
4531       eight account nodes (ink rings, ACC-#### micro-labels) joined by a
4532       faint blueprint graph. Transfers travel the edges as ink dots; one
4533       three-hop path lights crimson edge by edge, the destination account
4534       gets a crimson ring and a paper risk chip ("RISK 0.92 · HOLD"), a
4535       ruler-tick risk scale below tracks the score, and the case then
4536       resolves in lime ("CLEARED"). Time-based and eased, pauses when
4537       hidden / off-screen, one static frame under reduced motion.
4538       All readouts are illustrative. */
4539    var cv = document.getElementById('rz-visual-finance');
4540    if (!cv) return;
4541    var W = 480, H = 480;
4542    var g = cv.getContext('2d');
4543    if (!g) return;
4544    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
4545  
4546    /* ── graph (logical px): node centres + micro-label offsets ── */
4547    var R = 13;
4548    var NODES = [
4549      { id: 'ACC-1041', x: 118, y: 152, lx: 0,   ly: 25,  al: 'center' },
4550      { id: 'ACC-2270', x: 242, y: 116, lx: 0,   ly: -20, al: 'center' },
4551      { id: 'ACC-3385', x: 372, y: 152, lx: 0,   ly: -20, al: 'center' },
4552      { id: 'ACC-4416', x: 96,  y: 240, lx: -19, ly: 3,   al: 'right'  },
4553      { id: 'ACC-5528', x: 214, y: 212, lx: 4,   ly: 31,  al: 'left'   },
4554      { id: 'ACC-6639', x: 326, y: 236, lx: -6,  ly: 25,  al: 'center' },
4555      { id: 'ACC-7823', x: 146, y: 322, lx: 0,   ly: 25,  al: 'center' },
4556      { id: 'ACC-8907', x: 398, y: 312, lx: 0,   ly: 25,  al: 'center' }
4557    ];
4558    var EDGES = [[0, 1], [1, 2], [0, 3], [0, 4], [1, 4], [2, 5], [4, 5], [3, 6], [4, 6], [5, 7], [2, 7], [3, 4]];
4559    var DEST = 7, THRESH = 0.80;
4560  
4561    /* ── cadence (ms): transfers in start order; s:1 = the suspicious three-hop path ── */
4562    var TRAVEL = 750, TRAVEL_S = 800;
4563    var FLOWS = [
4564      { t: 700,   a: 1, b: 0 },
4565      { t: 1450,  a: 3, b: 6 },
4566      { t: 2200,  a: 2, b: 5 },
4567      { t: 2900,  a: 4, b: 1 },
4568      { t: 3500,  a: 7, b: 2 },
4569      { t: 3900,  a: 0, b: 4, s: 1 },
4570      { t: 4900,  a: 4, b: 5, s: 1 },
4571      { t: 5300,  a: 2, b: 1 },
4572      { t: 5900,  a: 5, b: 7, s: 1 },
4573      { t: 8300,  a: 1, b: 2 },
4574      { t: 10500, a: 6, b: 4 }
4575    ];
4576    var NF = FLOWS.length, TXN_BASE = 128;
4577    var T_HOLD = 6700, T_CLEAR = 9400, FADE = 600, LOOP = 12600, T_FADE = LOOP - FADE;   /* ~12.6 s per cycle */
4578    /* crimson ring per account on the path: origin as the hop leaves, the rest as each hop lands */
4579    var SUS_ON = { 0: 3900, 4: 4700, 5: 5700, 7: T_HOLD };
4580    /* risk score keyframes: [time, target, ease ms] — eased, never stepped */
4581    var SCORE = [[0, 0.05], [1450, 0.08], [2200, 0.06], [2950, 0.11], [3650, 0.07], [4250, 0.09],
4582                 [4700, 0.34], [5700, 0.66], [6700, 0.92], [T_CLEAR, 0.05, 700], [11250, 0.08], [11900, 0.05]];
4583    /* captions (paper chip, bottom-left) and status (bottom-right) */
4584    var CAP = [
4585      { t: 0,            s: 'SCORING · FAN-OUT · VELOCITY' },
4586      { t: 4900,         s: 'PATH ANOMALY · 3 HOPS' },
4587      { t: T_HOLD + 300, s: 'HOLD · €38,500 · ANALYST REVIEW' },
4588      { t: T_CLEAR,      s: 'REVIEW CLOSED · FUNDS RELEASED' }
4589    ];
4590    var STAT = [
4591      { t: 0,       s: 'MONITORING', c: 'ink' },
4592      { t: T_HOLD,  s: 'ON HOLD',    c: 'crimson' },
4593      { t: T_CLEAR, s: 'RESOLVED',   c: 'lime' }
4594    ];
4595  
4596    var HEAD_Y = 50, RAIL_Y = 390, RAIL_X0 = 112, RAIL_X1 = 432, FOOT_Y = 428;
4597  
4598    var reduce = false;
4599    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
4600  
4601    function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
4602    function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
4603    function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
4604    function easeOut5(p) { return 1 - Math.pow(1 - p, 5); }
4605    function pad4(n) { return ('0000' + n).slice(-4); }
4606  
4607    /* ── colour tokens: read from CSS each frame so theme switches are picked up ── */
4608    function parseColor(v, fb) {
4609      var m, r = -1, gg = -1, b = -1;
4610      v = (v || '').replace(/^\s+|\s+$/g, '');
4611      if (v.charAt(0) === '#') {
4612        if (v.length === 4) {
4613          r = parseInt(v.charAt(1) + v.charAt(1), 16); gg = parseInt(v.charAt(2) + v.charAt(2), 16); b = parseInt(v.charAt(3) + v.charAt(3), 16);
4614        } else if (v.length >= 7) {
4615          r = parseInt(v.substr(1, 2), 16); gg = parseInt(v.substr(3, 2), 16); b = parseInt(v.substr(5, 2), 16);
4616        }
4617      } else if ((m = /rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(v))) {
4618        r = Math.round(+m[1]); gg = Math.round(+m[2]); b = Math.round(+m[3]);
4619      }
4620      if (isNaN(r) || isNaN(gg) || isNaN(b) || r < 0 || gg < 0 || b < 0) {
4621        return v === fb ? [125, 20, 215] : parseColor(fb, fb);
4622      }
4623      return [r, gg, b];
4624    }
4625    function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }
4626    function mixC(a, b, f) {
4627      return [Math.round(a[0] + (b[0] - a[0]) * f), Math.round(a[1] + (b[1] - a[1]) * f), Math.round(a[2] + (b[2] - a[2]) * f)];
4628    }
4629    function inkRGB() { return document.documentElement.getAttribute('data-theme') === 'dark' ? [236, 238, 244] : [20, 21, 26]; }
4630    function tokens() {
4631      var cs = null, out;
4632      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
4633      function t(name, fb) {
4634        var v = '';
4635        if (cs) { try { v = cs.getPropertyValue(name); } catch (e) {} }
4636        v = (v || '').replace(/^\s+|\s+$/g, '');
4637        return v || fb;
4638      }
4639      out = { paper: t('--rz-paper', '#ffffff') };
4640      out.crimson = parseColor(t('--rz-crimson', '#f50034'), '#f50034');
4641      out.lime    = parseColor(t('--rz-lime',    '#85f403'), '#85f403');
4642      out.purple  = parseColor(t('--rz-purple',  '#7d14d7'), '#7d14d7');
4643      out.indigo  = parseColor(t('--rz-indigo',  '#5801f5'), '#5801f5');
4644      out.yellow  = parseColor(t('--rz-yellow',  '#e6e414'), '#e6e414');
4645      out.ink = inkRGB();
4646      return out;
4647    }
4648  
4649    /* ── drawing helpers ── */
4650    function rrect(x, y, w, h, r) {   /* per-corner radii, no roundRect dependency */
4651      var tl = r[0], tr = r[1], br = r[2], bl = r[3];
4652      g.beginPath();
4653      g.moveTo(x + tl, y);
4654      g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr);
4655      g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br);
4656      g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl);
4657      g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl);
4658      g.closePath();
4659    }
4660    function tracked(str, x, y, track, align) {   /* letter-spaced text; returns its start x and width */
4661      var i, w = [], total = 0, sx;
4662      for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; }
4663      total += track * (str.length - 1);
4664      sx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x);
4665      g.textAlign = 'left';
4666      for (i = 0, x = sx; i < str.length; i++) { g.fillText(str.charAt(i), x, y); x += w[i] + track; }
4667      return { x: sx, w: total };
4668    }
4669    /* paper chip whose text swaps between a and b with progress f (slide + fade);
4670       bar = optional accent rgb for a folder-tab side bar */
4671    function chip(a, b, f, x, yMid, align, paper, bar) {
4672      var pad = bar ? 18 : 10, wa, wb, w, h = 22, bx, by;
4673      g.font = "700 9px 'Courier New', monospace";
4674      wa = g.measureText(a).width + pad + 10; wb = g.measureText(b).width + pad + 10;
4675      w = wa + (wb - wa) * f;
4676      bx = align === 'right' ? x - w : x; by = yMid - h / 2;
4677      rrect(bx, by, w, h, [3, 6, 6, 6]);
4678      g.fillStyle = paper; g.fill();
4679      g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
4680      if (bar) { g.fillStyle = rgba(bar, 1); g.fillRect(bx + 5, by + 6, 2.5, h - 12); }
4681      g.textAlign = 'left'; g.textBaseline = 'middle';
4682      if (f < 1) { g.fillStyle = rzInk(0.78 * (1 - f)); g.fillText(a, bx + pad, yMid + 0.5 - 4 * f); }
4683      if (f > 0) { g.fillStyle = rzInk(0.78 * f);       g.fillText(b, bx + pad, yMid + 0.5 + 4 * (1 - f)); }
4684      g.textBaseline = 'alphabetic';
4685      return { x: bx, w: w };
4686    }
4687    /* which entry of a timed list is current, plus the eased swap from the previous one */
4688    function stage(list, t, loop) {
4689      var i = 0, k;
4690      for (k = 1; k < list.length; k++) if (t >= list[k].t) i = k;
4691      if (i === 0) return { prev: list[list.length - 1], cur: list[0], f: loop > 0 ? easeOut(clamp01(t / 350)) : 1 };
4692      return { prev: list[i - 1], cur: list[i], f: easeOut(clamp01((t - list[i].t) / 350)) };
4693    }
4694  
4695    /* ── everything derives from the loop time — no per-frame state, no jitter ── */
4696    function flowState(f, t) {
4697      var d = f.s ? TRAVEL_S : TRAVEL, started = t >= f.t, landed = t >= f.t + d;
4698      return { started: started, p: started ? easeInOut(clamp01((t - f.t) / d)) : 0, landed: landed, age: landed ? t - f.t - d : 0 };
4699    }
4700    function score(t) {
4701      var v = SCORE[0][1], i, k;
4702      for (i = 1; i < SCORE.length; i++) {
4703        k = SCORE[i];
4704        if (t < k[0]) break;
4705        v += (k[1] - v) * easeOut5(clamp01((t - k[0]) / (k[2] || 500)));
4706      }
4707      return v;
4708    }
4709    function txnCount(t) { var n = 0, i; for (i = 0; i < NF; i++) if (t >= FLOWS[i].t) n++; return n; }
4710  
4711    function draw(t, loop) {
4712      var tk = tokens(), st = [], i, j, a, b, f, q, x, y, w, e, m, pos, G = 1, n, sw, s, hot, sx, tx;
4713      var brt = [], susA = [], susMix, cl, clr, held, lp, ca, rise, bs, cap, stt, col, r, o, hA, ink;
4714  
4715      if (t >= T_FADE) G = 1 - easeInOut(clamp01((t - T_FADE) / FADE));   /* clean reset: dynamic marks fade */
4716      for (i = 0; i < NF; i++) st[i] = flowState(FLOWS[i], t);
4717      held = t >= T_HOLD;
4718      cl = t >= T_CLEAR ? easeOut(clamp01((t - T_CLEAR) / 600)) : 0;      /* crimson settles to ink after clearance */
4719      clr = t >= T_CLEAR ? easeOut(clamp01((t - T_CLEAR) / 400)) : 0;     /* destination ring: crimson → lime */
4720      ink = tk.ink;
4721  
4722      /* per-node brightness (recent landings) and crimson ring alpha */
4723      for (i = 0; i < NODES.length; i++) {
4724        brt[i] = 0; susA[i] = 0;
4725        if (SUS_ON[i] !== undefined && t >= SUS_ON[i]) {
4726          susA[i] = easeOut(clamp01((t - SUS_ON[i]) / 300)) * (i === DEST ? 1 : 0.6) * G;
4727          if (i !== DEST) susA[i] *= (1 - cl);
4728        }
4729      }
4730      for (i = 0; i < NF; i++) {
4731        if (st[i].landed) brt[FLOWS[i].b] = Math.max(brt[FLOWS[i].b], 1 - clamp01(st[i].age / 900));
4732      }
4733  
4734      g.clearRect(0, 0, W, H);
4735      g.textBaseline = 'alphabetic';
4736      g.lineJoin = 'round';
4737  
4738      /* ── corner crop-brackets ── */
4739      g.lineCap = 'butt';
4740      g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5;
4741      g.beginPath();
4742      g.moveTo(16, 30);  g.lineTo(16, 16);  g.lineTo(30, 16);
4743      g.moveTo(450, 16); g.lineTo(464, 16); g.lineTo(464, 30);
4744      g.moveTo(464, 450); g.lineTo(464, 464); g.lineTo(450, 464);
4745      g.moveTo(30, 464); g.lineTo(16, 464); g.lineTo(16, 450);
4746      g.stroke();
4747      g.lineCap = 'round';
4748  
4749      /* ── header: eyebrow · five swatches · transaction counter chip ── */
4750      g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55);
4751      w = tracked('TRANSACTION MONITORING', 36, HEAD_Y + 4, 1.5, 'left');
4752      x = w.x + w.w + 12;
4753      col = [tk.purple, tk.lime, tk.crimson, tk.indigo, tk.yellow];
4754      for (i = 0; i < 5; i++) { g.fillStyle = rgba(col[i], 1); g.fillRect(x + i * 9, HEAD_Y - 3, 6, 6); }
4755      n = txnCount(t);
4756      sw = n > 0 ? easeOut(clamp01((t - FLOWS[n - 1].t) / 300)) : 1;
4757      n += TXN_BASE + loop * NF;
4758      chip('TXN ' + pad4(n - 1), 'TXN ' + pad4(n), sw, 444, HEAD_Y, 'right', tk.paper, null);
4759  
4760      /* ── the graph: quiet blueprint edges ── */
4761      g.lineCap = 'butt'; g.lineWidth = 1; g.strokeStyle = rzInk(0.14);
4762      g.beginPath();
4763      for (i = 0; i < EDGES.length; i++) {
4764        a = NODES[EDGES[i][0]]; b = NODES[EDGES[i][1]];
4765        g.moveTo(a.x, a.y); g.lineTo(b.x, b.y);
4766      }
4767      g.stroke();
4768      g.lineCap = 'round';
4769  
4770      /* ── traces: ink while a transfer is in flight (then fading); crimson for the path, persisting ── */
4771      for (i = 0; i < NF; i++) {
4772        f = FLOWS[i]; s = st[i];
4773        if (!s.started || s.p <= 0) continue;
4774        a = NODES[f.a]; b = NODES[f.b];
4775        pos = { x: a.x + (b.x - a.x) * s.p, y: a.y + (b.y - a.y) * s.p };
4776        if (f.s) {
4777          o = 0.9 * (1 - cl) * G; m = 0.32 * cl * G;
4778          g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(pos.x, pos.y);
4779          if (o > 0.002) { g.strokeStyle = rgba(tk.crimson, o); g.lineWidth = 1.5; g.stroke(); }
4780          if (m > 0.002) { g.strokeStyle = rzInk(m); g.lineWidth = 1.25; g.stroke(); }
4781        } else {
4782          o = 0.45 * (s.landed ? 1 - clamp01(s.age / 600) : 1) * G;
4783          if (o <= 0.002) continue;
4784          g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(pos.x, pos.y);
4785          g.strokeStyle = rzInk(o); g.lineWidth = 1.25; g.stroke();
4786        }
4787      }
4788  
4789      /* ── transfers: a dot per edge in flight (ink; crimson along the path) ── */
4790      for (i = 0; i < NF; i++) {
4791        f = FLOWS[i]; s = st[i];
4792        if (!s.started) continue;
4793        hA = s.landed ? 1 - clamp01(s.age / 150) : 1;
4794        if (hA <= 0) continue;
4795        a = NODES[f.a]; b = NODES[f.b];
4796        pos = { x: a.x + (b.x - a.x) * s.p, y: a.y + (b.y - a.y) * s.p };
4797        col = f.s ? tk.crimson : ink;
4798        g.fillStyle = rgba(col, (f.s ? 0.18 : 0.10) * hA * G); g.beginPath(); g.arc(pos.x, pos.y, f.s ? 8 : 7, 0, Math.PI * 2); g.fill();
4799        g.fillStyle = rgba(col, (f.s ? 1 : 0.85) * hA * G);    g.beginPath(); g.arc(pos.x, pos.y, f.s ? 3.5 : 3, 0, Math.PI * 2); g.fill();
4800      }
4801  
4802      /* ── accounts: paper disc + ink ring; landing pulse; crimson / lime rings on the path ── */
4803      for (i = 0; i < NODES.length; i++) {
4804        a = NODES[i];
4805        g.beginPath(); g.arc(a.x, a.y, R, 0, Math.PI * 2);
4806        g.fillStyle = tk.paper; g.fill();
4807        g.strokeStyle = rzInk(0.5 + 0.4 * brt[i]); g.lineWidth = 1.25; g.stroke();
4808        g.fillStyle = rzInk(0.45 + 0.35 * brt[i]); g.beginPath(); g.arc(a.x, a.y, 2, 0, Math.PI * 2); g.fill();
4809        if (susA[i] > 0.002) {
4810          col = i === DEST ? mixC(tk.crimson, tk.lime, clr) : tk.crimson;
4811          g.beginPath(); g.arc(a.x, a.y, R, 0, Math.PI * 2);
4812          g.strokeStyle = rgba(col, susA[i]); g.lineWidth = i === DEST ? 1.75 : 1.5; g.stroke();
4813          if (i === DEST) {   /* outer hold ring */
4814            g.beginPath(); g.arc(a.x, a.y, R + 5, 0, Math.PI * 2);
4815            g.strokeStyle = rgba(col, 0.45 * susA[i]); g.lineWidth = 1; g.stroke();
4816          }
4817        }
4818      }
4819      for (i = 0; i < NF; i++) {   /* landing pulses */
4820        s = st[i];
4821        if (!s.landed || s.age >= 700) continue;
4822        q = clamp01(s.age / 700); b = NODES[FLOWS[i].b];
4823        g.beginPath(); g.arc(b.x, b.y, R + 11 * easeOut(q), 0, Math.PI * 2);
4824        g.strokeStyle = rgba(FLOWS[i].s ? tk.crimson : ink, 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke();
4825      }
4826  
4827      /* ── micro-labels on paper knock-outs ── */
4828      g.font = "8px 'Courier New', monospace"; g.textAlign = 'left';
4829      for (i = 0; i < NODES.length; i++) {
4830        a = NODES[i]; w = g.measureText(a.id).width;
4831        x = a.al === 'center' ? a.x + a.lx - w / 2 : (a.al === 'right' ? a.x + a.lx - w : a.x + a.lx);
4832        y = a.y + a.ly;
4833        g.fillStyle = tk.paper; g.fillRect(x - 3, y - 8, w + 6, 11);
4834        g.fillStyle = rzInk(0.55 + 0.3 * Math.max(brt[i], susA[i] / (G || 1)));
4835        g.fillText(a.id, x, y);
4836      }
4837  
4838      /* ── destination annotation: leader, risk chip (HOLD → CLEARED), clearance badge ── */
4839      if (held && G > 0) {
4840        b = NODES[DEST];
4841        col = mixC(tk.crimson, tk.lime, clr);
4842        lp = easeOut(clamp01((t - T_HOLD - 150) / 350));
4843        if (lp > 0) {
4844          g.strokeStyle = rgba(col, 0.9 * G); g.lineWidth = 1;
4845          g.beginPath(); g.moveTo(b.x - R - 1, b.y); g.lineTo(b.x - R - 1 - 23 * lp, b.y); g.stroke();
4846        }
4847        ca = easeOut(clamp01((t - T_HOLD - 300) / 450));
4848        if (ca > 0) {
4849          rise = 4 * (1 - ca);
4850          g.globalAlpha = ca * G;
4851          chip('RISK 0.92 · HOLD', 'CLEARED', easeOut(clamp01((t - T_CLEAR) / 350)), b.x - R - 24, b.y + rise, 'right', tk.paper, col);
4852          g.globalAlpha = 1;
4853        }
4854        bs = easeOut5(clamp01((t - T_CLEAR) / 450));
4855        if (bs > 0) {   /* check badge on the ring's top-right */
4856          g.save(); g.translate(b.x + 9.2, b.y - 9.2); g.scale(bs, bs); g.globalAlpha = G;
4857          g.fillStyle = tk.paper; g.beginPath(); g.arc(0, 0, 7, 0, Math.PI * 2); g.fill();
4858          g.strokeStyle = rgba(tk.lime, 1); g.lineWidth = 1.5; g.stroke();
4859          g.beginPath(); g.moveTo(-3, 0.5); g.lineTo(-1, 2.5); g.lineTo(3, -2.5); g.stroke();
4860          g.restore();
4861        }
4862      }
4863  
4864      /* ── risk scale: ruler ticks, hairline rail, threshold mark, eased caret + readout ── */
4865      s = score(t);
4866      hot = clamp01((s - (THRESH - 0.03)) / 0.05);
4867      sx = RAIL_X0 + s * (RAIL_X1 - RAIL_X0);
4868      tx = RAIL_X0 + THRESH * (RAIL_X1 - RAIL_X0);
4869      g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55);
4870      tracked('RISK SCORE', 36, RAIL_Y + 3, 1.2, 'left');
4871      g.lineCap = 'butt'; g.lineWidth = 1;
4872      for (i = 0; i <= 20; i++) {
4873        x = RAIL_X0 + i * 16; r = i % 5 === 0 ? 5 : 2.5;
4874        g.strokeStyle = rzInk(0.24);
4875        g.beginPath(); g.moveTo(x + 0.5, RAIL_Y - r); g.lineTo(x + 0.5, RAIL_Y + r); g.stroke();
4876      }
4877      g.strokeStyle = rzInk(0.16); g.lineWidth = 1.5;
4878      g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(RAIL_X1, RAIL_Y); g.stroke();
4879      /* progress line: ink up to the threshold, crimson beyond it */
4880      g.lineWidth = 1.5;
4881      g.strokeStyle = rzInk(0.4);
4882      g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(Math.min(sx, tx), RAIL_Y); g.stroke();
4883      if (sx > tx) {
4884        g.strokeStyle = rgba(tk.crimson, 0.9);
4885        g.beginPath(); g.moveTo(tx, RAIL_Y); g.lineTo(sx, RAIL_Y); g.stroke();
4886      }
4887      g.strokeStyle = rgba(tk.crimson, 0.9); g.lineWidth = 2;
4888      g.beginPath(); g.moveTo(tx, RAIL_Y - 6); g.lineTo(tx, RAIL_Y + 6); g.stroke();
4889      g.font = "8px 'Courier New', monospace"; g.textAlign = 'center';
4890      g.fillStyle = rzInk(0.45);
4891      g.fillText('0', RAIL_X0, RAIL_Y + 17); g.fillText('1.0', RAIL_X1, RAIL_Y + 17);
4892      g.fillStyle = rgba(tk.crimson, 0.9); g.fillText('0.80', tx, RAIL_Y + 17);
4893      g.lineCap = 'round';
4894      col = mixC(ink, tk.crimson, hot);
4895      g.beginPath(); g.arc(sx, RAIL_Y, 8, 0, Math.PI * 2);
4896      g.fillStyle = rgba(col, 0.08 + 0.1 * hot); g.fill();
4897      g.fillStyle = rzInk(0.9);
4898      rrect(sx - 1.25, RAIL_Y - 9, 2.5, 18, [1.25, 1.25, 1.25, 1.25]); g.fill();
4899      g.font = "700 9px 'Courier New', monospace"; g.textAlign = 'center';
4900      g.fillStyle = rgba(col, 0.8 + 0.15 * hot);
4901      g.fillText(s.toFixed(2), sx, RAIL_Y - 14);
4902  
4903      /* ── footer: caption chip, status ── */
4904      cap = stage(CAP, t, loop);
4905      chip(cap.prev.s, cap.cur.s, cap.f, 36, FOOT_Y, 'left', tk.paper, null);
4906      stt = stage(STAT, t, loop);
4907      g.font = "600 9px 'Oxanium', sans-serif";
4908      for (j = 0; j < 2; j++) {
4909        e = j === 0 ? stt.prev : stt.cur;
4910        f = j === 0 ? 1 - stt.f : stt.f;
4911        if (f <= 0) continue;
4912        y = j === 0 ? FOOT_Y - 4 * (1 - f) : FOOT_Y + 4 * (1 - f);
4913        g.fillStyle = rzInk(0.6 * f);
4914        w = tracked(e.s, 444, y + 3, 1.2, 'right');
4915        g.fillStyle = e.c === 'ink' ? rzInk(0.7 * f) : rgba(e.c === 'lime' ? tk.lime : tk.crimson, f);
4916        g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill();
4917      }
4918    }
4919  
4920    /* ── reduced motion: one composed frame (the hold), redrawn on resize / theme / font load ── */
4921    var T_STATIC = T_HOLD + 1500;
4922    function drawStatic() { draw(T_STATIC, 1); }
4923    if (reduce) {
4924      drawStatic();
4925      window.addEventListener('resize', drawStatic);
4926      window.addEventListener('load', drawStatic);
4927      document.addEventListener('rz-theme-change', drawStatic);
4928      if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
4929      return;
4930    }
4931  
4932    /* ── time-based loop; pauses when the tab is hidden or the canvas is off-screen ── */
4933    var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true;
4934    function nowMs() { return (window.performance && performance.now) ? performance.now() : Date.now(); }
4935    function frame(now) {
4936      var el;
4937      raf = 0;
4938      if (typeof now !== 'number') now = nowMs();
4939      if (!started) { started = true; t0 = now; }
4940      el = now - t0;
4941      draw(el % LOOP, Math.floor(el / LOOP));
4942      if (running) raf = requestAnimationFrame(frame);
4943    }
4944    function start() {
4945      if (running) return;
4946      running = true;
4947      if (pausedAt && started) t0 += nowMs() - pausedAt;
4948      pausedAt = 0;
4949      if (!raf) raf = requestAnimationFrame(frame);
4950    }
4951    function stop() {
4952      if (!running) return;
4953      running = false; pausedAt = nowMs();
4954      if (raf) { cancelAnimationFrame(raf); raf = 0; }
4955    }
4956    function sync() { if (document.hidden || !inView) stop(); else start(); }
4957    document.addEventListener('visibilitychange', sync);
4958    if ('IntersectionObserver' in window) {
4959      try {
4960        new IntersectionObserver(function (entries) {
4961          for (var i = 0; i < entries.length; i++) inView = entries[i].isIntersecting;
4962          sync();
4963        }, { threshold: 0 }).observe(cv);
4964      } catch (e) {}
4965    }
4966    sync();
4967  })();
4968
4969  /* ── 18. GOVERNMENT — APT Threat Intelligence ──────────────── */
4970  (function () {
4971      /* ── 18. GOVERNMENT — Threat-intel board ──────────────────────
4972         A honeycomb of asset cells on the sheet. Intel arrives as ink
4973         dots from four feed sources in the sheet's corners; every hit
4974         looks alike until correlation lines draw between the related
4975         ones. Six links (six shared TTPs) lock a five-cell cluster —
4976         the only place the five accents appear — and the dossier chip
4977         resolves. Blueprint on white, tokens read at runtime, time-based
4978         easing, ~12.4 s loop with a hold. ── */
4979      var cv = document.getElementById('rz-visual-gov');
4980      if (!cv) return;
4981      var W = 480, H = 480;
4982      var g = cv.getContext('2d');
4983      if (!g) return;
4984      rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
4985  
4986      var PI = Math.PI, TAU = PI * 2;
4987      var CX = 240, CY = 228;
4988      var R = 22, RINGS = 4;                       /* cell circumradius, honeycomb rings */
4989      var HW = Math.sqrt(3) * R, VS = 1.5 * R;     /* column pitch, row pitch */
4990      var LOOP = 12.4;                             /* seconds per cycle */
4991      var T_FIRST = 0.7, T_STEP = 0.5, T_TRAVEL = 0.75, T_PING = 0.5;
4992      var T_BLOOM = 0.7, T_SETTLE = 0.5, T_LINK = 0.6;
4993      var T_LOCK = 6.5, T_LOCKIN = 0.6, T_LOCKSTEP = 0.1;
4994      var T_FADE = 0.9, T_TEXT = 0.35;
4995      var N_ACC = 5;
4996  
4997      /* pointy-top hexagon, first vertex at the top */
4998      var HC = [], HS = [], k;
4999      for (k = 0; k < 6; k++) { HC.push(Math.cos((60 * k - 90) * PI / 180)); HS.push(Math.sin((60 * k - 90) * PI / 180)); }
5000      function cellX(q, r) { return CX + HW * (q + r / 2); }
5001      function cellY(q, r) { return CY + VS * r; }
5002  
5003      /* the board: every cell within RINGS of the centre (axial coords) */
5004      var CELLS = [], q, r;
5005      for (r = -RINGS; r <= RINGS; r++) {
5006        for (q = Math.max(-RINGS, -r - RINGS); q <= Math.min(RINGS, -r + RINGS); q++) CELLS.push({ x: cellX(q, r), y: cellY(q, r) });
5007      }
5008  
5009      /* feed sources in the sheet's corners; side = label to the right (1) / left (-1) */
5010      var SRC = [
5011        { name: 'CERT',    x: 40,  y: 64,  side: 1 },
5012        { name: 'ISAC',    x: 440, y: 64,  side: -1 },
5013        { name: 'OSINT',   x: 40,  y: 392, side: 1 },
5014        { name: 'PARTNER', x: 440, y: 392, side: -1 }
5015      ];
5016  
5017      /* intel hits in arrival order; c = cluster index (A..E) or -1 for noise */
5018      var HITS = [
5019        { q: -2, r: -1, src: 0, c: 0 },
5020        { q: 3,  r: -3, src: 1, c: -1 },
5021        { q: 0,  r: -2, src: 1, c: 1 },
5022        { q: -4, r: 2,  src: 2, c: -1 },
5023        { q: 2,  r: -1, src: 1, c: 2 },
5024        { q: 3,  r: 1,  src: 3, c: -1 },
5025        { q: 1,  r: 1,  src: 3, c: 3 },
5026        { q: 0,  r: -4, src: 0, c: -1 },
5027        { q: -1, r: 2,  src: 2, c: 4 }
5028      ];
5029      var NH = HITS.length, CL = [], i, h;
5030      for (i = 0; i < NH; i++) {
5031        h = HITS[i];
5032        h.x = cellX(h.q, h.r); h.y = cellY(h.q, h.r);
5033        h.t0 = T_FIRST + i * T_STEP;             /* leaves the source */
5034        h.t1 = h.t0 + T_TRAVEL;                  /* lands on the cell */
5035        if (h.c >= 0) CL[h.c] = h;
5036      }
5037  
5038      /* correlation links between cluster cells — six of them, one per shared TTP */
5039      var LINKS = [
5040        { a: 0, b: 1, d: 0.2 }, { a: 1, b: 2, d: 0.2 }, { a: 2, b: 3, d: 0.2 },
5041        { a: 1, b: 3, d: 0.35 }, { a: 3, b: 4, d: 0.2 }, { a: 4, b: 0, d: 0.35 }
5042      ];
5043      var NL = LINKS.length, ln;
5044      for (i = 0; i < NL; i++) {
5045        ln = LINKS[i];
5046        ln.t0 = Math.max(CL[ln.a].t1, CL[ln.b].t1) + ln.d;
5047        var ax = CL[ln.a].x, ay = CL[ln.a].y, bx = CL[ln.b].x, by = CL[ln.b].y;
5048        var dx = bx - ax, dy = by - ay, len = Math.sqrt(dx * dx + dy * dy), trim = R - 3;
5049        ln.x1 = ax + dx / len * trim; ln.y1 = ay + dy / len * trim;   /* edge to edge, not centre to centre */
5050        ln.x2 = bx - dx / len * trim;
5050 ln.y2 = by - dy / len * trim;
5051      }
5052  
5053      /* five equal accents, house rotation order; fall back if a token is missing */
5054      var ACC_KEYS = ['purple', 'lime', 'crimson', 'indigo', 'yellow'];
5055      var ACC_FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' };
5056      function tokens() {
5057        var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark';
5058        try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
5059        function rd(name, fb) {
5060          var v = '';
5061          try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {}
5062          v = v.replace(/^\s+|\s+$/g, '');
5063          return v || fb;
5064        }
5065        var acc = [], j;
5066        for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]]));
5067        return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') };
5068      }
5069  
5070      /* ── easing + small helpers ── */
5071      function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }
5072      function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
5073      function easeOut(p) { return 1 - Math.pow(1 - p, 3); }
5074      function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); }
5075      function hex(x, y, rad) {
5076        g.beginPath();
5077        g.moveTo(x + HC[0] * rad, y + HS[0] * rad);
5078        for (var j = 1; j < 6; j++) g.lineTo(x + HC[j] * rad, y + HS[j] * rad);
5079        g.closePath();
5080      }
5081      function circle(x, y, rad) { g.beginPath(); g.arc(x, y, rad, 0, TAU); }
5082      function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); }
5083      /* letter-spaced text (canvas letterSpacing is not universal) */
5084      function spaced(txt, x, y, sp, align) {
5085        var ws = [], w = 0, j;
5086        for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; }
5087        w += sp * (txt.length - 1);
5088        var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x);
5089        g.textAlign = 'left';
5090        for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y); px += ws[j] + sp; }
5091        return w;
5092      }
5093      /* dossier chip: rounded body with a small folder tab at the top-left */
5094      function chipPath(x, y, w, hh) {
5095        var rr = 4, tw = 22, th = 5;
5096        g.beginPath();
5097        g.moveTo(x, y + hh - rr);
5098        g.lineTo(x, y - th + 2); g.arcTo(x, y - th, x + 2, y - th, 2);
5099        g.lineTo(x + tw - 4, y - th); g.lineTo(x + tw, y);
5100        g.lineTo(x + w - rr, y); g.arcTo(x + w, y, x + w, y + rr, rr);
5101        g.lineTo(x + w, y + hh - rr); g.arcTo(x + w, y + hh, x + w - rr, y + hh, rr);
5102        g.lineTo(x + rr, y + hh); g.arcTo(x, y + hh, x, y + hh - rr, rr);
5103        g.closePath();
5104      }
5105      var FONT_LABEL = "600 10px 'Oxanium', sans-serif";
5106      var FONT_MONO = "bold 9px 'Courier New', monospace";
5107      var FONT_MONO_S = "bold 8.5px 'Courier New', monospace";
5108      var CHIP_IDLE = 'INTEL FEED · 04 SOURCES';
5109      var CHIP_FINAL = 'APT · TTPs 6 · CONFIDENCE HIGH';
5110  
5111      /* chip text crossfade memory (the only non-derived state) */
5112      var chipText = '', chipPrev = '', chipChangedAt = -10;
5113  
5114      /* ── one composed frame at loop-time t (0..LOOP), absolute time T ── */
5115      function scene(t, T, tk, instant) {
5116        g.clearRect(0, 0, W, H);
5117        g.lineCap = 'round'; g.lineJoin = 'round';
5118        g.textBaseline = 'alphabetic';
5119  
5120        var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE));   /* end-of-cycle fade */
5121        var cycle = Math.floor(T / LOOP);
5122        var acc = tk.acc, paper = tk.paper;
5123        var lockAll = easeOut(clamp01((t - T_LOCK) / T_LOCKIN)) * keep;
5124        var j, x, y, a;
5125  
5126        /* — corner crop-brackets (the sheet) — */
5127        g.strokeStyle = rzInk(0.45); g.lineWidth = 1;
5128        var m = 8.5, L = 12;
5129        line(m, m, m + L, m); line(m, m, m, m + L);
5130        line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L);
5131        line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L);
5132        line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L);
5133  
5134        /* — eyebrow + window readout — */
5135        g.fillStyle = rzInk(0.62); g.font = FONT_LABEL;
5136        var ew = spaced('THREAT-INTEL BOARD', 27, 20, 1.4, 'left');
5137        g.fillStyle = rzInk(0.22);
5137 g.fillRect(27, 24, ew, 1);
5138        g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right';
5139        g.fillText('WINDOW · 24H', W - 27, 20);
5140  
5141        /* — the honeycomb of asset cells — */
5142        g.strokeStyle = rzInk(0.14); g.lineWidth = 1;
5143        for (j = 0; j < CELLS.length; j++) { hex(CELLS[j].x, CELLS[j].y, R - 1.5); g.stroke(); }
5144  
5145        /* — per-hit timing — */
5146        var fl = [], st = [], n = 0;
5147        for (j = 0; j < NH; j++) {
5148          h = HITS[j];
5149          fl[j] = clamp01((t - h.t0) / T_TRAVEL);
5150          st[j] = easeOut(clamp01((t - h.t1) / T_SETTLE)) * keep;
5151          if (t >= h.t1) n++;
5152        }
5153  
5154        /* — feed sources: port glyph, label, departure pings — */
5155        for (j = 0; j < SRC.length; j++) {
5156          var s = SRC[j], live = 0, q2;
5157          for (q2 = 0; q2 < NH; q2++) {
5158            if (HITS[q2].src !== j) continue;
5159            var pg = clamp01((t - HITS[q2].t0) / T_PING);
5160            if (pg > 0 && pg < 1) {
5161              g.strokeStyle = rzInk((1 - pg) * 0.4); g.lineWidth = 1;
5162              circle(s.x, s.y, 5 + 11 * easeOut(pg)); g.stroke();
5163            }
5164            if (fl[q2] > 0 && fl[q2] < 1) live = 1;
5165          }
5166          g.fillStyle = paper; g.fillRect(s.x - 4, s.y - 4, 8, 8);
5167          g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.strokeRect(s.x - 3.5, s.y - 3.5, 7, 7);
5168          g.fillStyle = rzInk(0.35 + 0.55 * live); circle(s.x, s.y, 1.6); g.fill();
5169          g.font = FONT_MONO_S; g.textAlign = s.side > 0 ? 'left' : 'right';
5170          g.fillStyle = rzInk(0.55);
5171          g.fillText(s.name, s.x + s.side * 10, s.y + 3);
5172        }
5173  
5174        /* — correlation links, drawn edge to edge as both ends land — */
5175        var linked = 0;
5176        for (j = 0; j < NL; j++) {
5177          ln = LINKS[j];
5178          var lp = easeInOut(clamp01((t - ln.t0) / T_LINK));
5179          if (lp <= 0) continue;
5180          if (lp >= 1) linked++;
5181          g.strokeStyle = rzInk((0.32 + 0.14 * lockAll) * keep); g.lineWidth = 1 + 0.15 * lockAll;
5182          line(ln.x1, ln.y1, ln.x1 + (ln.x2 - ln.x1) * lp, ln.y1 + (ln.y2 - ln.y1) * lp);
5183        }
5184  
5185        /* — landed cells: identical ink marks until the cluster locks — */
5186        for (j = 0; j < NH; j++) {
5187          var sv = st[j];
5188          if (sv <= 0) continue;
5189          h = HITS[j];
5190          if (h.c >= 0) {
5191            var lk = easeOut(clamp01((t - (T_LOCK + h.c * T_LOCKSTEP)) / T_LOCKIN)) * keep;
5192            var col = acc[(cycle + h.c) % N_ACC];
5193            /* ink mark hands over to an accent tint, ring and dot */
5194            g.fillStyle = rzInk(0.07 * sv * (1 - lk)); hex(h.x, h.y, R - 1.5); g.fill();
5195            g.strokeStyle = rzInk(0.28 * sv * (1 - lk)); g.lineWidth = 1; hex(h.x, h.y, R - 1.5); g.stroke();
5196            if (lk > 0) {
5197              g.fillStyle = col; g.strokeStyle = col;
5198              g.globalAlpha = 0.1 * lk; hex(h.x, h.y, R - 1.5); g.fill();
5199              g.globalAlpha = lk; g.lineWidth = 1.5; hex(h.x, h.y, R - 2.5); g.stroke();
5200              g.globalAlpha = 1;
5201            }
5202            if (lk < 1) { g.fillStyle = rzInk(0.75 * sv * (1 - lk)); circle(h.x, h.y, 2.4); g.fill(); }
5203            if (lk > 0) { g.fillStyle = col; g.globalAlpha = lk; circle(h.x, h.y, 2.4); g.fill(); g.globalAlpha = 1; }
5204          } else {
5205            /* noise: same mark, dismissed with a small dash once the cluster locks */
5206            var dim = 1 - 0.55 * lockAll;
5207            g.fillStyle = rzInk(0.07 * sv * dim); hex(h.x, h.y, R - 1.5); g.fill();
5208            g.strokeStyle = rzInk(0.28 * sv * dim); g.lineWidth = 1; hex(h.x, h.y, R - 1.5); g.stroke();
5209            g.fillStyle = rzInk(0.75 * sv * dim); circle(h.x, h.y, 2.4); g.fill();
5210            if (lockAll > 0) { g.strokeStyle = rzInk(0.5 * lockAll * sv); g.lineWidth = 1.2; line(h.x - 4, h.y + 7, h.x + 4, h.y + 7); }
5211          }
5212        }
5213  
5214        /* — arrival blooms (ink) and lock blooms (accent) — */
5215        for (j = 0; j < NH; j++) {
5216          h = HITS[j];
5217          var bl = clamp01((t - h.t1) / T_BLOOM);
5218          if (bl > 0 && bl < 1) {
5219            g.strokeStyle = rzInk((1 - bl) * 0.45); g.lineWidth = 1.2;
5220            hex(h.x, h.y, R + 12 * easeOutQuint(bl)); g.stroke();
5221          }
5222          if (h.c < 0) continue;
5223          var lb = clamp01((t - (T_LOCK + h.c * T_LOCKSTEP)) / T_BLOOM);
5224          if (lb > 0 && lb < 1) {
5225            g.strokeStyle = acc[(cycle + h.c) % N_ACC]; g.globalAlpha = (1 - lb) * 0.55 * keep; g.lineWidth = 1.2;
5226            hex(h.x, h.y, R + 14 * easeOutQuint(lb)); g.stroke();
5227            g.globalAlpha = 1;
5228          }
5229        }
5230  
5231        /* — intel dots in flight (comet tail + dot) — */
5232        for (j = 0; j < NH; j++) {
5233          if (!(fl[j] > 0 && fl[j] < 1)) continue;
5234          h = HITS[j];
5235          var so = SRC[h.src];
5236          var pe = easeInOut(fl[j]), pt = Math.max(0, pe - 0.16);
5237          var hx = so.x + (h.x - so.x) * pe, hy = so.y + (h.y - so.y) * pe;
5238          var tx = so.x + (h.x - so.x) * pt, ty = so.y + (h.y - so.y) * pt;
5239          var dotA = Math.min(1, fl[j] / 0.08);
5240          var gr = g.createLinearGradient(tx, ty, hx, hy);
5241          gr.addColorStop(0, rzInk(0)); gr.addColorStop(1, rzInk(0.45 * dotA));
5242          g.strokeStyle = gr; g.lineWidth = 1.6; line(tx, ty, hx, hy);
5243          g.fillStyle = rzInk(0.9 * dotA); circle(hx, hy, 2.6); g.fill();
5244        }
5245  
5246        /* — dossier chip — */
5247        var txt = n === 0 ? CHIP_IDLE : (t >= T_LOCK ? CHIP_FINAL : 'CORRELATING · ' + n + ' HITS · ' + linked + ' LINKED');
5248        if (t >= LOOP - T_FADE) txt = CHIP_IDLE;
5249        if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; }
5250        var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT));
5251        if (chipChangedAt > t) e = 1;   /* clock wrapped */
5252        g.font = FONT_MONO;
5253        var cw = g.measureText(CHIP_FINAL).width + 30, ch = 24, cx = CX - cw / 2, cy = 432;
5254        chipPath(cx, cy, cw, ch);
5255        g.fillStyle = paper; g.fill();
5256        g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke();
5257        g.textAlign = 'center'; g.textBaseline = 'middle';
5258        if (e < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - e));
5258 g.fillText(chipPrev, CX, cy + ch / 2 + 0.5); }
5259        g.fillStyle = rzInk(0.78 * e); g.fillText(chipText, CX, cy + ch / 2 + 0.5);
5260        g.textBaseline = 'alphabetic';
5261      }
5262  
5263      /* ── reduced motion: one composed, fully correlated frame ── */
5264      var reduce = false;
5265      try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
5266      if (reduce) {
5267        var drawStatic = function () { scene(LOOP - T_FADE - 0.05, 0, tokens(), true); };
5268        drawStatic();
5269        window.addEventListener('resize', drawStatic);
5270        window.addEventListener('load', drawStatic);
5271        document.addEventListener('rz-theme-change', drawStatic);
5272        if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic);
5273        return;
5274      }
5275  
5276      /* ── time-based loop; pauses when the tab is hidden or the hero is off-screen ── */
5277      var raf = null, t0 = null, pausedAt = null, inView = true;
5278      function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); }
5279      function frame(ts) {
5280        raf = null;
5281        if (t0 === null) t0 = ts;
5282        var T = Math.max(0, (ts - t0) / 1000);
5283        scene(T % LOOP, T, tokens(), false);
5284        raf = requestAnimationFrame(frame);
5285      }
5286      function start() {
5287        if (raf !== null || document.hidden || !inView) return;
5288        if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt;   /* resume without a time jump */
5289        pausedAt = null;
5290        raf = requestAnimationFrame(frame);
5291      }
5292      function stop() {
5293        if (raf === null) return;
5294        cancelAnimationFrame(raf); raf = null; pausedAt = now();
5295      }
5296      document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
5297      if ('IntersectionObserver' in window) {
5298        try {
5299          new IntersectionObserver(function (entries) {
5300            for (var q3 = 0; q3 < entries.length; q3++) inView = entries[q3].isIntersecting;
5301            if (inView) start(); else stop();
5302          }, { threshold: 0 }).observe(cv);
5303        } catch (e) {}
5304      }
5305      start();
5306    })();
5307
5308  /* ── 19. HEALTHCARE — Medical IoT Security ──────────────────── */
5309  (function () {
5310    var cv = document.getElementById('rz-visual-health');
5311    if (!cv) return;
5312    var W = 480, H = 480;
5313    var g = cv.getContext('2d');
5314    if (!g) return;
5315    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store */
5316  
5317    var TAU = Math.PI * 2;
5318  
5319    /* ── timeline (seconds) — one calm loop with a long hold ── */
5320    var LOOP = 12.6;
5321    var T_ANOM = 2.2;    /* anomaly surfaces on the pump               */
5322    var T_RING = 4.6;    /* segmentation ring draws in (1.1s)          */
5323    var T_SEVER = 5.3;   /* links to the pump are cut (0.8s)           */
5324    var T_ROUTE = 6.0;   /* mesh re-routes wearable → gateway (1.0s)   */
5325    var T_RESET = 11.0;  /* everything eases back to nominal (1.2s)    */
5326  
5327    /* ── palette: read tokens once per frame, fall back per theme ── */
5328    var FB = {
5329      light: { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' },
5330      dark:  { purple: '#9a45ee', indigo: '#7a56ff', crimson: '#ff3355', lime: '#8ef513', yellow: '#f0ee2a', paper: '#0e0f14' }
5331    };
5332    var pal = {};
5333    function readPalette() {
5334      var dark = document.documentElement.getAttribute('data-theme') === 'dark';
5335      var fb = dark ? FB.dark : FB.light, cs = null;
5336      try { cs = window.getComputedStyle(document.documentElement); } catch (e) {}
5337      function tok(n, f) { var v = cs ? cs.getPropertyValue(n) : ''; v = (v || '').replace(/\s/g, ''); return v || f; }
5338      pal.purple  = tok('--rz-purple',  fb.purple);
5339      pal.indigo  = tok('--rz-indigo',  fb.indigo);
5340      pal.crimson = tok('--rz-crimson', fb.crimson);
5341      pal.lime    = tok('--rz-lime',    fb.lime);
5342      pal.yellow  = tok('--rz-yellow',  fb.yellow);
5343      pal.paper   = tok('--rz-paper',   fb.paper);
5344    }
5345    /* colour token + alpha → rgba() (hex or rgb() input; anything else passes through) */
5346    function tint(c, a) {
5347      var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c);
5348      if (m) {
5349        var h = m[1];
5350        if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
5351        return 'rgba(' + parseInt(h.substr(0, 2), 16) + ',' + parseInt(h.substr(2, 2), 16) + ',' + parseInt(h.substr(4, 2), 16) + ',' + a + ')';
5352      }
5353      var r = /^rgba?\(([^)]+)\)$/i.exec(c);
5354      if (r) { var p = r[1].split(','); return 'rgba(' + p[0] + ',' + p[1] + ',' + p[2] + ',' + a + ')'; }
5355      return c;
5356    }
5357  
5358    /* ── easing ── */
5359    function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
5360    function cubicIO(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
5361    function quintIO(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; }
5362    function cubicOut(p) { return 1 - Math.pow(1 - p, 3); }
5363    function seg(t, t0, dur, ease) { return ease(clamp01((t - t0) / dur)); }
5364    function lerp(a, b, p) { return a + (b - a) * p; }
5365  
5366    /* ── path helpers ── */
5367    function rr(x, y, w, h, r) {   /* rounded rect, uniform radius */
5368      g.moveTo(x + r, y);
5369      g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r);
5370      g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
5371      g.lineTo(x + r, y + h); g.quadraticCurveTo(x, y + h, x, y + h - r);
5372      g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y);
5373      g.closePath();
5374    }
5375    function tabRect(x, y, w, h) {  /* folder-tab radius: 4 / 10 / 10 / 10 (rhymes with the page cards) */
5376      var a = 4, b = 10;
5377      g.moveTo(x + a, y);
5378      g.lineTo(x + w - b, y); g.quadraticCurveTo(x + w, y, x + w, y + b);
5379      g.lineTo(x + w, y + h - b); g.quadraticCurveTo(x + w, y + h, x + w - b, y + h);
5380      g.lineTo(x + b, y + h); g.quadraticCurveTo(x, y + h, x, y + h - b);
5381      g.lineTo(x, y + a); g.quadraticCurveTo(x, y, x + a, y);
5382      g.closePath();
5383    }
5384    /* tracked text (canvas letterSpacing is not universal) — returns drawn width */
5385    function spaced(txt, x, y, sp, align) {
5386      var i, w = 0, ws = [];
5387      for (i = 0; i < txt.length; i++) { ws[i] = g.measureText(txt.charAt(i)).width;
5387 w += ws[i]; }
5388      w += sp * (txt.length - 1);
5389      var sx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x;
5390      g.textAlign = 'left';
5391      for (i = 0; i < txt.length; i++) { g.fillText(txt.charAt(i), sx, y); sx += ws[i] + sp; }
5392      return w;
5393    }
5394    function spacedWidth(txt, sp) {
5395      var i, w = 0;
5396      for (i = 0; i < txt.length; i++) w += g.measureText(txt.charAt(i)).width;
5397      return w + sp * (txt.length - 1);
5398    }
5399  
5400    /* ── device glyphs (ink strokes, local coords centred on the node) ── */
5401    function gMonitor() {
5402      g.beginPath(); rr(-9, -8, 18, 13, 2); g.stroke();
5403      g.beginPath(); g.moveTo(-6, -1.5); g.lineTo(-3, -1.5); g.lineTo(-1.5, -5); g.lineTo(0.5, 1.5); g.lineTo(2, -1.5); g.lineTo(6, -1.5); g.stroke();
5404      g.beginPath(); g.moveTo(0, 5); g.lineTo(0, 8.5); g.moveTo(-4.5, 8.5); g.lineTo(4.5, 8.5); g.stroke();
5405    }
5406    function gPump() {
5407      g.beginPath(); rr(-6.5, -9.5, 13, 19, 2.5); g.stroke();
5408      g.beginPath(); rr(-4, -6.5, 8, 4.5, 1); g.stroke();
5409      g.beginPath(); g.arc(-2.5, 3, 1.1, 0, TAU); g.moveTo(3.6, 3); g.arc(2.5, 3, 1.1, 0, TAU); g.fill();
5410      g.beginPath(); g.moveTo(-6.5, -3); g.lineTo(-10.5, -3); g.lineTo(-10.5, -9); g.stroke();
5411      g.beginPath(); g.arc(-10.5, -11.5, 1.6, 0, TAU); g.stroke();
5412    }
5413    function gImaging() {
5414      g.beginPath(); g.arc(0, -1, 9.5, 0, TAU); g.stroke();
5415      g.beginPath(); g.arc(0, -1, 4, 0, TAU); g.stroke();
5416      g.beginPath(); g.moveTo(-11, 10.5); g.lineTo(11, 10.5); g.stroke();
5417    }
5418    function gBadge() {
5419      g.beginPath(); rr(-7, -8, 14, 17, 2); g.stroke();
5420      g.beginPath(); g.moveTo(-2, -8); g.lineTo(-2, -11.5); g.lineTo(2, -11.5); g.lineTo(2, -8); g.stroke();
5421      g.beginPath(); g.arc(0, -2.5, 2.4, 0, TAU); g.stroke();
5422      g.beginPath(); g.moveTo(-4, 2.5); g.lineTo(4, 2.5); g.moveTo(-3, 5.5); g.lineTo(3, 5.5); g.stroke();
5423    }
5424    function gGateway() {
5425      g.beginPath(); rr(-9.5, -1.5, 19, 10, 2); g.stroke();
5426      g.beginPath(); g.arc(-5, 3.5, 1.1, 0, TAU); g.moveTo(1.1, 3.5); g.arc(0, 3.5, 1.1, 0, TAU); g.moveTo(6.1, 3.5); g.arc(5, 3.5, 1.1, 0, TAU); g.fill();
5427      g.beginPath(); g.moveTo(-4.5, -1.5); g.lineTo(-4.5, -9); g.moveTo(4.5, -1.5); g.lineTo(4.5, -9); g.stroke();
5428      g.beginPath(); g.moveTo(-6.5, -9); g.lineTo(-2.5, -9); g.moveTo(2.5, -9); g.lineTo(6.5, -9); g.stroke();
5429    }
5430    function gWearable() {
5431      g.beginPath(); rr(-6.5, -6.5, 13, 13, 3); g.stroke();
5432      g.beginPath(); rr(-3.5, -11, 7, 4, 1); g.stroke();
5433      g.beginPath(); rr(-3.5, 7, 7, 4, 1); g.stroke();
5434      g.beginPath(); g.moveTo(0, 0); g.lineTo(0, -3.5); g.moveTo(0, 0); g.lineTo(2.5, 1); g.stroke();
5435    }
5436  
5437    /* ── mesh ── */
5438    var nodes = [
5439      { lbl: 'WEARABLE', x: 256, y: 104, glyph: gWearable },
5440      { lbl: 'MONITOR',  x: 112, y: 158, glyph: gMonitor  },
5441      { lbl: 'PUMP',     x: 386, y: 158, glyph: gPump     },
5442      { lbl: 'GATEWAY',  x: 250, y: 238, glyph: gGateway, hub: true },
5443      { lbl: 'BADGE',    x: 110, y: 316, glyph: gBadge    },
5444      { lbl: 'IMAGING',  x: 392, y: 316, glyph: gImaging  }
5445    ];
5446    var ANOM = 2;                                     /* the infusion pump misbehaves */
5447    var edges = [[0, 1], [0, 2], [1, 3], [2, 3], [3, 5], [3, 4], [4, 1], [5, 2], [4, 5]];
5448    var reroute = [0, 3];                             /* wearable → gateway, established after isolation */
5449    var NODE_R = 22;
5450    var RING_R = 46;
5451  
5452    /* ── vitals trace (P · QRS · T), phase 0..1 ── */
5453    function bump(p, c, w, a) { var d = (p - c) / w; return a * Math.exp(-d * d); }
5454    function ecg(p) {
5455      return bump(p, 0.18, 0.030, 0.13) - bump(p, 0.360, 0.007, 0.08) + bump(p, 0.385, 0.011, 1.0)
5456           - bump(p, 0.412, 0.009, 0.24) + bump(p, 0.58, 0.045, 0.26);
5457    }
5458  
5459    /* ── frame ── */
5460    function draw(now, t) {
5461      g.clearRect(0, 0, W, H);
5462      readPalette();
5463      g.lineCap = 'round'; g.lineJoin = 'round'; g.textBaseline = 'middle';
5464  
5465      /* phases */
5466      var resetF  = seg(t, T_RESET, 1.2, cubicIO);
5467      var anom    = seg(t, T_ANOM, 0.5, cubicOut) * (1 - resetF);
5468      var ringP   = seg(t, T_RING, 1.1, quintIO);
5469      var ringA   = 1 - resetF;
5470      var tickA   = seg(t, T_RING + 1.15, 0.4, cubicOut) * ringA;
5471      var hazA    = seg(t, T_RING + 0.9, 0.7, cubicIO) * ringA;
5472      var sever   = seg(t, T_SEVER, 0.8, cubicIO);
5473      var restore = seg(t, T_RESET, 1.2, cubicIO);
5474      var cutF    = 1 - sever + sever * restore;        /* 1 = link intact, 0 = link cut */
5475      var routeP  = seg(t, T_ROUTE, 1.0, quintIO);
5476      var routeA  = 1 - resetF;
5477      var routeOn = seg(t, T_ROUTE + 1.0, 0.4, cubicOut) * routeA;
5478      var chipA   = seg(t, T_ANOM + 0.25, 0.5, cubicOut) * (1 - resetF);
5479      var isoF    = seg(t, T_SEVER + 0.2, 0.5, cubicIO);
5480      var breathe = 0.75 + 0.25 * Math.sin(now / 1000 * TAU / 1.8);
5481  
5482      var i, a, b, x, y, m;
5483  
5484      function edgeFactor(k) {                          /* 1 = link intact, 0 = link cut */
5485        var e = edges[k];
5486        return (e[0] === ANOM || e[1] === ANOM) ? cutF : 1;
5487      }
5488      function packet(ax, ay, bx, by, phase, alpha) {   /* telemetry packet (purple) */
5489        var px = lerp(ax, bx, phase), py = lerp(ay, by, phase);
5490        g.fillStyle = tint(pal.purple, 0.15 * alpha);
5491        g.beginPath(); g.arc(px, py, 4.5, 0, TAU); g.fill();
5492        g.fillStyle = tint(pal.purple, 0.9 * alpha);
5493        g.beginPath(); g.arc(px, py, 2.1, 0, TAU); g.fill();
5494      }
5495  
5496      /* ── frame furniture: ruler ticks (top edge) + corner crop-brackets ── */
5497      g.lineWidth = 1;
5498      for (x = 24; x < W; x += 24) {
5499        var fx = x / W;
5500        m = fx < 0.12 ? fx / 0.12 : fx > 0.88 ? (1 - fx) / 0.12 : 1;
5501        g.strokeStyle = rzInk(0.22 * m);
5502        g.beginPath(); g.moveTo(x + 0.5, 0); g.lineTo(x + 0.5, 7); g.stroke();
5503      }
5504      g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5;
5505      var ins = 5, arm = 14;
5506      g.beginPath();
5507      g.moveTo(ins, ins + arm); g.lineTo(ins, ins); g.lineTo(ins + arm, ins);
5508      g.moveTo(W - ins - arm, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + arm);
5509      g.moveTo(ins, H - ins - arm); g.lineTo(ins, H - ins); g.lineTo(ins + arm, H - ins);
5510      g.moveTo(W - ins - arm, H - ins); g.lineTo(W - ins, H - ins); g.lineTo(W - ins, H - ins - arm);
5511      g.stroke();
5512  
5513      /* ── header: eyebrow + micro readout ── */
5514      g.strokeStyle = rzInk(0.85); g.lineWidth = 2;
5515      g.beginPath(); g.moveTo(26, 40); g.lineTo(44, 40); g.stroke();
5516      g.font = '600 10px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.62);
5517      spaced('MEDICAL IoT MESH', 52, 40, 1.4, 'left');
5518      var links = 0;
5519      for (i = 0; i < edges.length; i++) links += (edgeFactor(i) > 0.5) ? 1 : 0;
5520      if (routeP > 0.5 && routeA > 0.5) links += 1;
5521      g.font = '9px "Courier New", monospace'; g.fillStyle = rzInk(0.5);
5522      spaced('WARD 3  ·  6 NODES  ·  ' + links + ' LINKS', 26, 57, 0.4, 'left');
5523  
5524      /* ── mesh links ── */
5525      g.lineWidth = 1;
5526      for (i = 0; i < edges.length; i++) {
5527        var f = edgeFactor(i);
5528        a = nodes[edges[i][0]]; b = nodes[edges[i][1]];
5529        if (f < 0.999) {                                   /* cut link: faint dashed ghost keeps the topology legible */
5530          g.strokeStyle = rzInk(0.09 * (1 - f)); g.setLineDash([3, 5]);
5531          g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.stroke();
5532          g.setLineDash([]);
5533        }
5534        if (f <= 0.001) continue;
5535        g.strokeStyle = rzInk(0.24 * f);
5536        g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.stroke();
5537      }
5538      /* re-route: grows from the wearable to the gateway, yellow leading mark while drawing */
5539      if (routeP > 0 && routeA > 0.001) {
5540        a = nodes[reroute[0]]; b = nodes[reroute[1]];
5541        x = lerp(a.x, b.x, routeP); y = lerp(a.y, b.y, routeP);
5542        g.strokeStyle = rzInk(0.24 * routeA); g.lineWidth = 1;
5543        g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(x, y); g.stroke();
5544        if (routeP < 1) {
5545          var lead = Math.sin(Math.PI * routeP);
5546          g.fillStyle = tint(pal.yellow, 0.95 * lead);
5547          g.beginPath(); g.arc(x, y, 2.6, 0, TAU); g.fill();
5548          g.strokeStyle = tint(pal.yellow, 0.35 * lead); g.lineWidth = 1;
5549          g.beginPath(); g.arc(x, y, 6, 0, TAU); g.stroke();
5550        }
5551      }
5552  
5553      /* ── telemetry packets travelling the live links ── */
5554      for (i = 0; i < edges.length; i++) {
5555        var ef = edgeFactor(i);
5556        if (ef <= 0.001) continue;
5557        a = nodes[edges[i][0]]; b = nodes[edges[i][1]];
5558        var ph = (now / 1000 / 3.6 + i * 0.29) % 1;
5559        if (i % 2) ph = 1 - ph;
5560        packet(a.x, a.y, b.x, b.y, ph, ef);
5561      }
5562      if (routeOn > 0.001) {
5563        a = nodes[reroute[0]]; b = nodes[reroute[1]];
5564        packet(a.x, a.y, b.x, b.y, (now / 1000 / 3.6 + 0.61) % 1, routeOn);
5565      }
5566  
5567      /* ── segmentation: hazard annulus, indigo ring, compass ticks ── */
5568      var P = nodes[ANOM];
5569      if (hazA > 0.001) {
5570        g.save();
5571        g.beginPath(); g.arc(P.x, P.y, RING_R - 5, 0, TAU); g.arc(P.x, P.y, NODE_R + 6, 0, TAU, true); g.clip();
5572        g.strokeStyle = rzInk(0.10 * hazA); g.lineWidth = 2;
5573        g.beginPath();
5574        for (var k = -104; k <= 104; k += 7) { g.moveTo(P.x + k - 52, P.y + 52); g.lineTo(P.x + k + 52, P.y - 52); }
5575        g.stroke();
5576        g.restore();
5577      }
5578      if (ringP > 0 && ringA > 0.001) {
5579        g.strokeStyle = tint(pal.indigo, 0.9 * ringA); g.lineWidth = 1.5;
5580        g.beginPath(); g.arc(P.x, P.y, RING_R, -Math.PI / 2, -Math.PI / 2 + TAU * ringP); g.stroke();
5581        if (ringP < 1) {
5582          var ex = P.x + Math.cos(-Math.PI / 2 + TAU * ringP) * RING_R, ey = P.y + Math.sin(-Math.PI / 2 + TAU * ringP) * RING_R;
5583          g.fillStyle = tint(pal.indigo, 0.95 * ringA);
5584          g.beginPath(); g.arc(ex, ey, 2.4, 0, TAU); g.fill();
5585        }
5586      }
5587      if (tickA > 0.001) {
5588        g.strokeStyle = tint(pal.indigo, 0.8 * tickA); g.lineWidth = 1.2;
5589        g.beginPath();
5590        for (i = 0; i < 4; i++) {
5591          var an = i * Math.PI / 2, c = Math.cos(an), s = Math.sin(an);
5592          g.moveTo(P.x + c * (RING_R + 5), P.y + s * (RING_R + 5)); g.lineTo(P.x + c * (RING_R + 11), P.y + s * (RING_R + 11));
5593        }
5594        g.stroke();
5595      }
5596  
5597      /* ── anomaly pulses (crimson, thin, expanding) ── */
5598      for (i = 0; i < 3; i++) {
5599        var ps = (t - (T_ANOM + i * 0.75)) / 1.7;
5600        if (ps <= 0 || ps >= 1) continue;
5601        var pr = NODE_R + 2 + 40 * cubicOut(ps);
5602        g.strokeStyle = tint(pal.crimson, 0.7 * (1 - ps) * (1 - ps)); g.lineWidth = 1.2;
5603        g.beginPath(); g.arc(P.x, P.y, pr, 0, TAU); g.stroke();
5604      }
5605  
5606      /* ── nodes ── */
5607      for (i = 0; i < nodes.length; i++) {
5608        var n = nodes[i], r = n.hub ? NODE_R + 2 : NODE_R;
5609        var isA = (i === ANOM) ? anom : 0;
5610        if (n.hub) {
5611          g.strokeStyle = rzInk(0.13); g.lineWidth = 1; g.setLineDash([2, 4]);
5612          g.beginPath(); g.arc(n.x, n.y, r + 7, 0, TAU); g.stroke();
5613          g.setLineDash([]);
5614        }
5615        g.fillStyle = pal.paper;
5616        g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.fill();
5617        g.strokeStyle = rzInk(0.2 * (1 - isA)); g.lineWidth = 1;
5618        g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.stroke();
5619        if (isA > 0.001) {
5620          g.strokeStyle = tint(pal.crimson, 0.85 * isA); g.lineWidth = 1 + 0.5 * isA;
5621          g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.stroke();
5622          g.fillStyle = pal.paper;
5623          g.beginPath(); g.arc(n.x + 16, n.y - 16, 4.5, 0, TAU); g.fill();
5624          g.fillStyle = tint(pal.crimson, isA * breathe);
5625          g.beginPath(); g.arc(n.x + 16, n.y - 16, 2.6, 0, TAU); g.fill();
5626        }
5627        g.save(); g.translate(n.x, n.y);
5628        g.strokeStyle = rzInk(0.82); g.fillStyle = rzInk(0.82); g.lineWidth = 1.3;
5629        n.glyph();
5630        g.restore();
5631        g.font = '600 9px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.55);
5632        spaced(n.lbl, n.x, n.y + r + 13, 1, 'center');
5633      }
5634  
5635      /* ── status chip near the pump (paper chip, crimson → indigo mark) ── */
5636      if (chipA > 0.001) {
5637        g.font = '8.5px "Courier New", monospace';
5638        var s1 = 'ANOMALY · NODE 03', s2 = 'ISOLATED · SEG 03';
5639        var tw = Math.max(spacedWidth(s1, 0.5), spacedWidth(s2, 0.5));
5640        var cw = tw + 30, ch = 20, cx = 454 - cw, cy = 66 + 6 * (1 - chipA);
5641        g.globalAlpha = chipA;
5642        g.fillStyle = pal.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1;
5643        g.beginPath(); tabRect(cx, cy, cw, ch); g.fill(); g.stroke();
5644        g.fillStyle = tint(pal.crimson, (1 - isoF) * breathe);
5645        g.beginPath(); g.arc(cx + 11, cy + ch / 2, 2.6, 0, TAU); g.fill();
5646        g.fillStyle = tint(pal.indigo, isoF);
5647        g.beginPath(); g.arc(cx + 11, cy + ch / 2, 2.6, 0, TAU); g.fill();
5648        g.fillStyle = rzInk(0.72 * (1 - isoF)); spaced(s1, cx + 20, cy + ch / 2 + 0.5, 0.5, 'left');
5649        g.fillStyle = rzInk(0.72 * isoF);       spaced(s2, cx + 20, cy + ch / 2 + 0.5, 0.5, 'left');
5650        g.globalAlpha = 1;
5651      }
5652  
5653      /* ── vitals chip: the patient trace never wavers ── */
5654      var vx = 26, vy = 396, vw = 428, vh = 50;
5655      g.fillStyle = pal.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1;
5656      g.beginPath(); tabRect(vx, vy, vw, vh); g.fill(); g.stroke();
5657      g.strokeStyle = tint(pal.lime, 0.95); g.lineWidth = 2;
5658      g.beginPath(); g.moveTo(vx + 4, vy + 1); g.lineTo(vx + 62, vy + 1); g.stroke();
5659      g.font = '600 9px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.55);
5660      spaced('VITALS', vx + 16, vy + 17, 1.3, 'left');
5661      g.font = '9.5px "Courier New", monospace'; g.fillStyle = rzInk(0.82);
5662      spaced('72 BPM   SPO2 98%', vx + 16, vy + 34, 0.4, 'left');
5663      g.strokeStyle = rzInk(0.12); g.lineWidth = 1;
5664      g.beginPath(); g.moveTo(vx + 140.5, vy + 11); g.lineTo(vx + 140.5, vy + vh - 11); g.stroke();
5665  
5666      var sx0 = vx + 156, sx1 = vx + vw - 18, base = vy + 29, amp = 15;
5667      var period = 60 / 72, pxPerBeat = 112;
5668      g.strokeStyle = rzInk(0.08); g.lineWidth = 1;
5669      g.beginPath(); g.moveTo(sx0, base + 0.5); g.lineTo(sx1, base + 0.5); g.stroke();
5670      var chunks = 10, span = (sx1 - sx0) / chunks, tsec = now / 1000;
5671      g.lineWidth = 1.5;
5672      for (i = 0; i < chunks; i++) {
5673        var ca = 0.28 + 0.72 * ((i + 1) / chunks);
5674        g.strokeStyle = tint(pal.lime, ca);
5675        g.beginPath();
5676        var xs = sx0 + i * span, xe = xs + span;
5677        for (x = xs; x <= xe + 0.001; x += 1) {
5678          var phs = ((sx1 - x) / pxPerBeat) - tsec / period;
5679          phs = phs - Math.floor(phs);
5680          var yy = base - ecg(1 - phs) * amp;
5681          if (x === xs) g.moveTo(x, yy); else g.lineTo(x, yy);
5682        }
5683        g.stroke();
5684      }
5685      var lp = (0 - tsec / period); lp = lp - Math.floor(lp);
5686      var ly = base - ecg(1 - lp) * amp;
5687      g.fillStyle = tint(pal.lime, 0.25);
5688      g.beginPath(); g.arc(sx1, ly, 5, 0, TAU); g.fill();
5689      g.fillStyle = tint(pal.lime, 1);
5690      g.beginPath(); g.arc(sx1, ly, 2.3, 0, TAU); g.fill();
5691    }
5692  
5693    /* ── scheduling: time-based loop, paused while hidden, one still under reduced motion ── */
5694    var reduce = false;
5695    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
5696    if (reduce) { draw(8400, 8.4); return; }
5697  
5698    var t0 = performance.now(), raf = 0, running = false;
5699    function tick(now) {
5700      raf = requestAnimationFrame(tick);
5701      draw(now, ((now - t0) / 1000) % LOOP);
5702    }
5703    function start() { if (running) return; running = true; raf = requestAnimationFrame(tick); }
5704    function stop() { if (!running) return; running = false; cancelAnimationFrame(raf); }
5705    document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
5706    if (!document.hidden) start();
5707  })();
5708
5709  /* ── 20. STARTUPS/SAAS — Zero Trust Architecture ───────────── */
5710  (function () {
5711    /* ── 20. STARTUPS/SAAS — Zero-trust gates ─────────────────────
5712       Three concentric policy rings (IDENTITY · DEVICE · CONTEXT)
5713       around a core. Requests enter from the edge, hold at every
5714       gate for an accent pulse, and are either admitted (lime) or
5715       bounced (crimson). Time-based, eased, 14.4 s loop with a hold.
5716       Blueprint-on-white: ink lines via rzInk(), paper chips via the
5717       --rz-paper token, accents read from CSS variables per frame. */
5718    var cv = document.getElementById('rz-visual-saas');
5719    if (!cv) return;
5720    var W = 480, H = 480;
5721    var g = cv.getContext('2d');
5722    if (!g) return;
5723    rzFitCanvas(cv, g, W, H);   /* Hi-DPI backing store + zoom-aware transform */
5724  
5725    /* ── geometry ─────────────────────────────────────────────── */
5726    var CX = 240, CY = 240;
5727    var CORE_R = 26, ENTRY_R = 232, DIAL_R = 190, ORBIT_R = 38;
5728    var TAU = Math.PI * 2;
5729    var GATES = [
5730      { label: 'IDENTITY', r: 176, key: 'purple', dash: null },
5731      { label: 'DEVICE',   r: 128, key: 'indigo', dash: [6, 4] },
5732      { label: 'CONTEXT',  r: 80,  key: 'yellow', dash: [1.5, 4] }
5733    ];
5734  
5735    /* ── choreography (seconds) ───────────────────────────────── */
5736    var LOOP = 14.4;
5737    var TRAVEL = [1.1, 0.8, 0.7], HOLD = 0.45, BOUNCE = 1.2, TO_CORE = 0.6, ABSORB = 0.45;
5738    var STAMP_LIFE = 2.4, TRAIL = 0.18, FADE_IN = 0.35;
5739    /* a: entry angle (deg, 0 = right, 90 = down) · s: start · deny: 0 = admitted, n = refused at gate n */
5740    var REQS = [
5741      { a: -40,  s: 0.0, deny: 0, ms: '2.8' },
5742      { a: 160,  s: 1.6, deny: 1, ms: '' },
5743      { a: 20,   s: 2.8, deny: 0, ms: '3.4' },
5744      { a: 205,  s: 4.3, deny: 0, ms: '3.1' },
5745      { a: -125, s: 5.7, deny: 3, ms: '' },
5746      { a: 45,   s: 7.2, deny: 0, ms: '2.9' },
5747      { a: 130,  s: 8.7, deny: 0, ms: '3.6' }
5748    ];
5749  
5750    function build(rq) {
5751      var segs = [], t = 0, r = ENTRY_R, k;
5752      rq.holdEnd = [];
5753      for (k = 0; k < GATES.length; k++) {
5754        segs.push({ type: 'travel', t0: t, d: TRAVEL[k], from: r, to: GATES[k].r, gate: k }); t += TRAVEL[k];
5755        segs.push({ type: 'hold', t0: t, d: HOLD, at: GATES[k].r, gate: k }); t += HOLD;
5756        rq.holdEnd[k] = t;
5757        r = GATES[k].r;
5758        if (rq.deny === k + 1) {
5759          rq.resolve = t;
5760          segs.push({ type: 'bounce', t0: t, d: BOUNCE, from: r, to: r + 64, gate: k }); t += BOUNCE;
5761          break;
5762        }
5763      }
5764      if (!rq.deny) {
5765        segs.push({ type: 'travel', t0: t, d: TO_CORE, from: r, to: CORE_R + 4, gate: 3 }); t += TO_CORE;
5766        rq.resolve = t;
5767        segs.push({ type: 'absorb', t0: t, d: ABSORB, at: CORE_R + 4 }); t += ABSORB;
5768      }
5769      rq.segs = segs; rq.dur = t;
5770      var rad = rq.a * Math.PI / 180;
5771      rq.rad = rad; rq.cos = Math.cos(rad); rq.sin = Math.sin(rad);
5772    }
5773    for (var bi = 0; bi < REQS.length; bi++) build(REQS[bi]);
5774  
5775    /* ── easing ───────────────────────────────────────────────── */
5776    function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
5777    function easeQ(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; }
5778    function easeOutC(p) { return 1 - Math.pow(1 - p, 3); }
5779  
5780    /* state of one request at local time lt (null when not on stage) */
5781    function stateAt(rq, lt) {
5782      if (lt < 0) lt = 0;
5783      if (lt >= rq.dur) return null;
5784      var segs = rq.segs, i, s, p, st;
5785      for (i = 0; i < segs.length; i++) {
5786        s = segs[i];
5787        if (lt < s.t0 + s.d || i === segs.length - 1) {
5788          p = clamp01((lt - s.t0) / s.d);
5789          st = { seg: s, p: p, r: s.at || 0, alpha: 1, tone: 'ink' };
5790          if (s.type === 'travel') { st.r = s.from + (s.to - s.from) * easeQ(p); if (s.gate === 3) st.tone = 'allow'; }
5791          else if (s.type === 'bounce') { st.r = s.from + (s.to - s.from) * easeOutC(p); st.tone = 'deny'; st.alpha = p < 0.45 ? 1 : 1 - (p - 0.45) / 0.55; }
5792          else if (s.type === 'absorb') { st.r = s.at - 8 * p; st.tone = 'allow'; st.alpha = 1 - p; }
5793          if (lt < FADE_IN) st.alpha *= lt / FADE_IN;
5794          return st;
5795        }
5796      }
5797      return null;
5798    }
5799  
5800    /* ── drawing helpers ──────────────────────────────────────── */
5801    function trackW(text, sp) {
5802      var w = 0, n = text.length, i;
5803      for (i = 0; i < n; i++) w += g.measureText(text.charAt(i)).width;
5804      return w + sp * (n - 1);
5805    }
5806    function track(text, x, y, sp, align) {
5807      var w = trackW(text, sp), n = text.length, i, cx = x, ch;
5808      if (align === 'center') cx = x - w / 2; else if (align === 'right') cx = x - w;
5809      g.textAlign = 'left';
5810      for (i = 0; i < n; i++) { ch = text.charAt(i); g.fillText(ch, cx, y); cx += g.measureText(ch).width + sp; }
5811      return w;
5812    }
5813    function rr(x, y, w, h, r) {
5814      g.beginPath();
5815      g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r);
5816      g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r);
5817      g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r);
5818      g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); g.closePath();
5819    }
5820    function strokeA(col, a, lw) { g.strokeStyle = col; g.lineWidth = lw; g.globalAlpha = clamp01(a); g.stroke(); g.globalAlpha = 1; }
5821    function fillA(col, a) { g.fillStyle = col; g.globalAlpha = clamp01(a); g.fill(); g.globalAlpha = 1; }
5822    function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); }
5823  
5824    /* ── one composed frame at loop time t (seconds) ──────────── */
5825    function draw(t) {
5826      var root = document.documentElement, cs = null;
5827      try { cs = getComputedStyle(root); } catch (e) {}
5828      var dark = root.getAttribute('data-theme') === 'dark';
5829      function tok(name, fb) {
5830        var v = '';
5831        try { v = cs ? cs.getPropertyValue(name) : ''; } catch (e) {}
5832        v = (v || '').replace(/^\s+|\s+$/g, '');
5833        return v || fb;
5834      }
5835      var AC = {
5836        purple:  tok('--rz-purple',  dark ? '#9a45ee' : '#7d14d7'),
5837        indigo:  tok('--rz-indigo',  dark ? '#7a56ff' : '#5801f5'),
5838        crimson: tok('--rz-crimson', dark ? '#ff3355' : '#f50034'),
5839        lime:    tok('--rz-lime',    dark ? '#8ef513' : '#85f403'),
5840        yellow:  tok('--rz-yellow',  dark ? '#f0ee2a' : '#e6e414')
5841      };
5842      var PAPER = tok('--rz-paper', dark ? '#0e0f14' : '#ffffff');
5843      var SWATCH = [AC.purple, AC.lime, AC.crimson, AC.indigo, AC.yellow];
5844      function toneCol(tone) { return tone === 'allow' ? AC.lime : tone === 'deny' ? AC.crimson : rzInk(0.8); }
5845  
5846      var i, k, rq, lt, st, px, py, x, y, a, len, u, col, gate, off, rt;
5847  
5848      g.clearRect(0, 0, W, H);
5849      g.setLineDash([]);
5850      g.lineCap = 'butt';
5851      g.textBaseline = 'alphabetic';
5852  
5853      /* corner crop-brackets (plate motif) */
5854      g.strokeStyle = rzInk(0.28); g.lineWidth = 1.5;
5855      var ins = 8, bl = 12;
5856      g.beginPath();
5857      g.moveTo(ins, ins + bl); g.lineTo(ins, ins); g.lineTo(ins + bl, ins);
5858      g.moveTo(W - ins - bl, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + bl);
5859      g.moveTo(ins, H - ins - bl); g.lineTo(ins, H - ins); g.lineTo(ins + bl, H - ins);
5860      g.moveTo(W - ins - bl, H - ins); g.lineTo(W - ins, H - ins); g.lineTo(W - ins, H - ins - bl);
5861      g.stroke();
5862  
5863      /* perimeter dial: ruler ticks every 6°, long every 30° */
5864      g.lineWidth = 1;
5865      for (i = 0; i < 60; i++) {
5866        a = i * 6 * Math.PI / 180;
5867        len = (i % 5 === 0) ? 8 : 4;
5868        g.beginPath();
5869        g.moveTo(CX + Math.cos(a) * DIAL_R, CY + Math.sin(a) * DIAL_R);
5870        g.lineTo(CX + Math.cos(a) * (DIAL_R + len), CY + Math.sin(a) * (DIAL_R + len));
5871        g.strokeStyle = rzInk(i % 5 === 0 ? 0.34 : 0.18);
5872        g.stroke();
5873      }
5874  
5875      /* policy rings: solid / dashed / dotted — line types, not colours */
5876      for (k = 0; k < GATES.length; k++) {
5877        gate = GATES[k];
5878        g.setLineDash(gate.dash || []);
5879        circle(CX, CY, gate.r);
5880        strokeA(rzInk(0.28), 1, 1);
5881      }
5882      g.setLineDash([]);
5883  
5884      /* slow orbit around the core (continuous verification) */
5885      g.save();
5886      g.translate(CX, CY); g.rotate(t * TAU / 48);
5887      g.setLineDash([1.5, 5.5]);
5888      circle(0, 0, ORBIT_R);
5889      strokeA(rzInk(0.32), 1, 1);
5890      g.restore();
5891      g.setLineDash([]);
5892  
5893      /* ring labels: paper knockout on the ring, accent square, ink text */
5894      g.font = "600 10px 'Oxanium', sans-serif";
5895      g.textBaseline = 'middle';
5896      for (k = 0; k < GATES.length; k++) {
5897        gate = GATES[k];
5898        y = CY - gate.r;
5899        var tw = trackW(gate.label, 1.4), sq = 5, gap = 7, total = sq + gap + tw, x0 = CX - total / 2;
5900        g.fillStyle = PAPER; g.fillRect(x0 - 9, y - 8, total + 18, 16);
5901        g.fillStyle = AC[gate.key]; g.fillRect(x0, y - sq / 2, sq, sq);
5902        g.fillStyle = rzInk(0.78);
5903        track(gate.label, x0 + sq + gap, y + 0.5, 1.4, 'left');
5904      }
5905      g.textBaseline = 'alphabetic';
5906  
5907      /* verdict stamps left on the rings (also from the previous cycle, so the loop seam is clean) */
5908      for (i = 0; i < REQS.length; i++) {
5909        rq = REQS[i];
5910        for (off = 0; off <= 1; off++) {
5911          lt = t - rq.s + off * LOOP;
5912          for (k = 0; k < rq.holdEnd.length; k++) {
5913            u = (lt - rq.holdEnd[k]) / STAMP_LIFE;
5914            if (u < 0 || u >= 1) continue;
5915            px = CX + rq.cos * GATES[k].r; py = CY + rq.sin * GATES[k].r;
5916            if (rq.deny === k + 1) {
5917              /* closed gate: crimson bar across the request path */
5918              g.beginPath();
5919              g.moveTo(px - rq.sin * 7, py + rq.cos * 7); g.lineTo(px + rq.sin * 7, py - rq.cos * 7);
5920              g.lineCap = 'round'; strokeA(AC.crimson, (1 - u) * 0.95, 2); g.lineCap = 'butt';
5921            } else {
5922              circle(px, py, 2.4); fillA(AC[GATES[k].key], (1 - u) * 0.95);
5923            }
5924          }
5925        }
5926      }
5927  
5928      /* live requests: gate pulses, core admission, trails, dots */
5929      var coreFlash = 0;
5930      for (i = 0; i < REQS.length; i++) {
5931        rq = REQS[i];
5932        lt = t - rq.s;
5933        st = stateAt(rq, lt);
5934        if (!st) continue;
5935        px = CX + rq.cos * st.r; py = CY + rq.sin * st.r;
5936  
5937        if (st.seg.type === 'hold') {
5938          gate = GATES[st.seg.gate]; col = AC[gate.key];
5939          /* accent arc lights on the ring around the crossing */
5940          g.beginPath(); g.arc(CX, CY, gate.r, rq.rad - 0.16, rq.rad + 0.16);
5941          g.lineCap = 'round'; strokeA(col, Math.sin(Math.PI * st.p) * 0.95, 2); g.lineCap = 'butt';
5942          /* tiny expanding halo */
5943          circle(px, py, 4 + 12 * easeOutC(st.p)); strokeA(col, (1 - st.p) * 0.55, 1);
5944        }
5945        if (st.seg.type === 'absorb') {
5946          circle(CX, CY, CORE_R + 18 * easeOutC(st.p)); strokeA(AC.lime, (1 - st.p) * 0.8, 1.5);
5947          if (1 - st.p > coreFlash) coreFlash = 1 - st.p;
5948        }
5949  
5950        /* short trail behind the dot */
5951        var st2 = stateAt(rq, lt - TRAIL);
5952        if (st2 && Math.abs(st2.r - st.r) > 0.5) {
5953          g.beginPath();
5954          g.moveTo(CX + rq.cos * st2.r, CY + rq.sin * st2.r); g.lineTo(px, py);
5955          g.lineCap = 'round'; strokeA(toneCol(st.tone), st.alpha * 0.4, 1.4); g.lineCap = 'butt';
5956        }
5957        /* the request token: paper node, toned stroke */
5958        circle(px, py, 3.8);
5959        fillA(PAPER, st.alpha);
5960        strokeA(toneCol(st.tone), st.alpha, 1.4);
5961      }
5962  
5963      /* core */
5964      circle(CX, CY, CORE_R); g.fillStyle = PAPER; g.fill(); strokeA(rzInk(0.6), 1, 1.2);
5965      circle(CX, CY - 5, 3.2); g.fillStyle = rzInk(0.85); g.fill();
5966      if (coreFlash > 0) { circle(CX, CY - 5, 3.2); fillA(AC.lime, coreFlash); }
5967      g.font = "700 8px 'Oxanium', sans-serif"; g.textBaseline = 'middle'; g.fillStyle = rzInk(0.72);
5968      track('CORE', CX, CY + 10, 1.0, 'center');
5969      g.textBaseline = 'alphabetic';
5970  
5971      /* header: eyebrow · five swatches · motto */
5972      g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.62);
5973      var ew = track('ZERO TRUST', 26, 24, 1.4, 'left');
5974      for (i = 0; i < 5; i++) { g.fillStyle = SWATCH[i]; g.fillRect(26 + ew + 12 + i * 8, 16, 5, 5); }
5975      g.font = "8px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.42);
5976      track('NEVER TRUST · ALWAYS VERIFY', 26, 38, 0.6, 'left');
5977  
5978      /* policy chip (paper, 1px ink border) */
5979      g.font = "600 9px 'Oxanium', sans-serif";
5980      var ct = 'POLICY · 3 CHECKS', cw = trackW(ct, 1.2) + 22, ch = 20, cx0 = W - 26 - cw, cy0 = 14;
5981      rr(cx0, cy0, cw, ch, 4); g.fillStyle = PAPER; g.fill(); strokeA(rzInk(0.15), 1, 1);
5982      g.textBaseline = 'middle'; g.fillStyle = rzInk(0.72);
5983      track(ct, cx0 + 11, cy0 + ch / 2 + 0.5, 1.2, 'left');
5984      g.textBaseline = 'alphabetic';
5985  
5986      /* footer: ruler + last verdict readout */
5987      var ry = 444, rx0 = 26, rx1 = W - 26;
5988      g.beginPath(); g.moveTo(rx0, ry + 0.5); g.lineTo(rx1, ry + 0.5); strokeA(rzInk(0.18), 1, 1);
5989      g.beginPath();
5990      for (x = rx0; x <= rx1; x += 8) { g.moveTo(x + 0.5, ry); g.lineTo(x + 0.5, ry - (((x - rx0) % 40 === 0) ? 6 : 3)); }
5991      strokeA(rzInk(0.28), 1, 1);
5992  
5993      var last = null, lastT = -1;
5994      for (i = 0; i < REQS.length; i++) { rt = REQS[i].s + REQS[i].resolve; if (t >= rt && rt > lastT) { lastT = rt; last = REQS[i]; } }
5995      var vAlpha = 1;
5996      if (!last) last = REQS[REQS.length - 1]; else vAlpha = clamp01((t - lastT) / 0.4);
5997      g.font = "600 8px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.5);
5998      var lw = track('LAST VERDICT', rx0, 463, 1.2, 'left');
5999      var vcol = last.deny ? AC.crimson : AC.lime;
6000      g.fillStyle = vcol; g.globalAlpha = vAlpha; g.fillRect(rx0 + lw + 10, 456, 5, 5); g.globalAlpha = 1;
6001      g.font = "9px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.72 * vAlpha);
6002      track(last.deny ? 'DENY · ' + GATES[last.deny - 1].label : 'ALLOW · ' + last.ms + ' ms', rx0 + lw + 21, 463, 0.6, 'left');
6003      g.font = "8px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.45);
6004      track('TTL 300 s', rx1, 463, 0.6, 'right');
6005    }
6006  
6007    /* ── scheduling ───────────────────────────────────────────── */
6008    var reduce = false;
6009    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
6010    if (reduce) {
6011      /* one composed still: an admission at the core, a refusal bouncing, a token mid-gate */
6012      var still = function () { draw(4.75); };
6013      still();
6014      window.addEventListener('resize', still);
6015      window.addEventListener('load', still);
6016      document.addEventListener('rz-theme-change', still);
6017      document.addEventListener('rz-why-variant', still);
6018      if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(still, function () {});
6019      return;
6020    }
6021  
6022    function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); }
6023    var t0 = now(), raf = 0, running = false;
6024    function frame() {
6025      raf = 0;
6026      if (!running) return;
6027      draw(((now() - t0) / 1000) % LOOP);
6028      raf = requestAnimationFrame(frame);
6029    }
6030    function start() { if (running) return; running = true; if (!raf) raf = requestAnimationFrame(frame); }
6031    function stop() { running = false; if (raf) { cancelAnimationFrame(raf); raf = 0; } }
6032    document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
6033    if (!document.hidden) start();
6034  })();
6035
6036  /* ── 21. CONSULTANCY BAND — Live SOC Operations Terminal ───── */
6037  (function () {
6038    var cv = document.getElementById('rz-consult-canvas');
6039    if (!cv) return;
6040    var W = 360, H = 520;
6041    var g = cv.getContext('2d');
6042    rzFitCanvas(cv, g, W, H);
6043
6044    var alerts = [
6045      { time:'04:12', type:'RANSOMWARE PRE-EXEC', status:'NEUTRALIZED', sev:2 },
6046      { time:'03:58', type:'C2 BEACON',           status:'BLOCKED',     sev:2 },
6047      { time:'03:44', type:'LATERAL MOVEMENT',    status:'CONTAINED',   sev:2 },
6048      { time:'03:31', type:'DATA EXFILTRATION',   status:'STOPPED',     sev:2 },
6049      { time:'03:17', type:'BRUTEFORCE SSH',       status:'CONTAINED',   sev:1 },
6050      { time:'03:02', type:'SQL INJECTION',        status:'PATCHED',     sev:1 },
6051      { time:'02:49', type:'PORT SCAN',            status:'DISMISSED',   sev:0 },
6052      { time:'02:33', type:'PHISHING EMAIL',       status:'QUARANTINED', sev:1 },
6053      { time:'02:18', type:'PRIV ESCALATION',      status:'BLOCKED',     sev:2 },
6054      { time:'02:05', type:'RECON ACTIVE',         status:'MONITORING',  sev:0 },
6055    ];
6056
6057    var metrics = [
6058      { lbl:'MEAN TIME TO RESPOND', val:14, unit:'MIN', max:60, col:'rgba(74,222,128,0.85)' },
6059      { lbl:'CONTAINMENT RATE',     val:100, unit:'%',  max:100, col:'rgba(74,222,128,0.85)' },
6060      { lbl:'FALSE POSITIVE RATE',  val:2,   unit:'%',  max:20,  col:'rgba(74,222,128,0.85)' },
6061    ];
6062
6063    var tick = 0;
6064    var visibleAlerts = 0;
6065    var alertTimer = 0;
6066    var scrollOff = 0;
6067    var blinkOn = true;
6068
6069    // Uptime clock start
6070    var startTime = Date.now();
6071
6072    function pad(n) { return n < 10 ? '0' + n : '' + n; }
6073
6074    function drawBar(bx, by, bw, bh, pct, col) {
6075      // bg track
6076      g.fillStyle = 'rgba(40,15,15,0.8)';
6077      g.beginPath(); g.roundRect(bx, by, bw, bh, 3); g.fill();
6078      // fill
6079      g.fillStyle = col;
6080      g.beginPath(); g.roundRect(bx, by, bw * pct, bh, 3); g.fill();
6081      // shimmer
6082      g.fillStyle = 'rgba(255,255,255,0.06)';
6083      g.beginPath(); g.roundRect(bx, by, bw * pct, bh * 0.4, 2); g.fill();
6084    }
6085
6086    function frame() {
6087      tick++;
6088      g.clearRect(0, 0, W, H);
6089
6090      // ── Background ──────────────────────────────────────────────
6091      g.fillStyle = 'rgba(16,15,20,0.97)';
6092      g.beginPath(); g.roundRect(0, 0, W, H, 18); g.fill();
6093
6094      // subtle scanlines
6095      for (var sy = 0; sy < H; sy += 4) {
6096        g.fillStyle = 'rgba(0,0,0,0.08)';
6097        g.fillRect(0, sy, W, 1);
6098      }
6099
6100      // subtle red vignette
6101      var vig = g.createRadialGradient(W/2, H/2, H*0.3, W/2, H/2, H*0.8);
6102      vig.addColorStop(0, 'rgba(0,0,0,0)');
6103      vig.addColorStop(1, 'rgba(125,20,215,0.07)');
6104      g.fillStyle = vig;
6105      g.beginPath(); g.roundRect(0, 0, W, H, 18); g.fill();
6106
6107      // ── Header bar ──────────────────────────────────────────────
6108      g.fillStyle = 'rgba(125,20,215,0.14)';
6109      g.beginPath(); g.roundRect(0, 0, W, 52, [18, 18, 0, 0]); g.fill();
6110      g.strokeStyle = 'rgba(125,20,215,0.35)';
6111      g.lineWidth = 0.8;
6112      g.beginPath(); g.moveTo(0, 52); g.lineTo(W, 52); g.stroke();
6113
6114      // logo + title
6115      g.textAlign = 'left';
6116      g.font = 'bold 11px "Courier New", monospace';
6117      g.fillStyle = '#f4f2fb';
6118      g.fillText('REACTIVEZERO', 16, 22);
6119      g.font = '7.5px "Courier New", monospace';
6120      g.fillStyle = 'rgba(125,20,215,0.7)';
6121      g.fillText('SOC OPERATIONS CENTER', 16, 36);
6122
6123      // live indicator
6124      var livePulse = 0.55 + 0.45 * Math.sin(tick / 18);
6125      g.beginPath(); g.arc(W - 18, 20, 5, 0, Math.PI * 2);
6126      g.fillStyle = 'rgba(74,222,128,' + livePulse + ')'; g.fill();
6127      g.textAlign = 'right';
6128      g.font = 'bold 7.5px "Courier New", monospace';
6129      g.fillStyle = 'rgba(74,222,128,0.8)';
6130      g.fillText('LIVE', W - 26, 23);
6131
6132      // uptime clock
6133      var elapsed = Math.floor((Date.now() - startTime) / 1000);
6134      var hh = Math.floor(elapsed / 3600), mm = Math.floor((elapsed % 3600) / 60), ss = elapsed % 60;
6135      g.font = '7px "Courier New", monospace';
6136      g.fillStyle = 'rgba(125,20,215,0.5)';
6137      g.fillText('UP ' + pad(hh) + ':' + pad(mm) + ':' + pad(ss), W - 14, 38);
6138
6139      // ── Incident counter ────────────────────────────────────────
6140      var IY = 68;
6141      g.strokeStyle = 'rgba(125,20,215,0.15)';
6142      g.lineWidth = 0.7;
6143      g.strokeRect(10, IY, W - 20, 52);
6144      g.fillStyle = 'rgba(125,20,215,0.07)';
6145      g.fillRect(10, IY, W - 20, 52);
6146
6147      g.textAlign = 'center';
6148      g.font = 'bold 32px "Courier New", monospace';
6149      g.fillStyle = 'rgba(74,222,128,0.9)';
6150      g.fillText('47', W / 2, IY + 36);
6151      g.font = '7px "Courier New", monospace';
6152      g.fillStyle = 'rgba(125,20,215,0.55)';
6153      g.fillText('INCIDENTS HANDLED TODAY  ·  0 UNRESOLVED', W / 2, IY + 48);
6154
6155      // ── Metrics ─────────────────────────────────────────────────
6156      var MY = 138;
6157      g.textAlign = 'left';
6158      g.font = 'bold 7px "Courier New", monospace';
6159      g.fillStyle = 'rgba(125,20,215,0.5)';
6160      g.fillText('─── PERFORMANCE METRICS', 14, MY);
6161
6162      metrics.forEach(function (m, i) {
6163        var my = MY + 18 + i * 38;
6164        g.font = '7.5px "Courier New", monospace';
6165        g.fillStyle = 'rgba(180,155,155,0.75)';
6166        g.textAlign = 'left';
6167        g.fillText(m.lbl, 14, my);
6168        g.textAlign = 'right';
6169        g.font = 'bold 9px "Courier New", monospace';
6170        g.fillStyle = m.col;
6171        g.fillText(m.val + m.unit, W - 14, my);
6172        drawBar(14, my + 4, W - 28, 7, m.val / m.max, m.col);
6173      });
6174
6175      // ── Divider ─────────────────────────────────────────────────
6176      var DY = MY + 18 + metrics.length * 38 + 8;
6177      g.strokeStyle = 'rgba(125,20,215,0.2)';
6178      g.lineWidth = 0.7;
6179      g.setLineDash([4, 4]);
6180      g.beginPath(); g.moveTo(14, DY); g.lineTo(W - 14, DY); g.stroke();
6181      g.setLineDash([]);
6182
6183      // ── Alert stream ────────────────────────────────────────────
6184      var ALY = DY + 14;
6185      g.textAlign = 'left';
6186      g.font = 'bold 7px "Courier New", monospace';
6187      g.fillStyle = 'rgba(125,20,215,0.5)';
6188      g.fillText('─── LIVE ALERT STREAM', 14, ALY);
6189
6190      // reveal one more alert every ~55 frames, cycle after all shown
6191      alertTimer++;
6192      if (alertTimer > 55) {
6193        alertTimer = 0;
6194        visibleAlerts = (visibleAlerts + 1) % (alerts.length + 1);
6195      }
6196
6197      var maxRows = 6;
6198      var shown = Math.min(visibleAlerts, alerts.length);
6199      var startIdx = Math.max(0, shown - maxRows);
6200
6201      for (var ai = startIdx; ai < shown; ai++) {
6202        var al = alerts[ai];
6203        var row = ai - startIdx;
6204        var ay = ALY + 16 + row * 30;
6205        var isNew = (ai === shown - 1) && alertTimer < 20;
6206
6207        // row bg
6208        g.fillStyle = isNew ? 'rgba(125,20,215,0.12)' : 'rgba(20,8,8,0.5)';
6209        g.beginPath(); g.roundRect(12, ay - 12, W - 24, 24, 4); g.fill();
6210        if (isNew) {
6211          g.strokeStyle = 'rgba(125,20,215,0.5)'; g.lineWidth = 0.8; g.stroke();
6212        }
6213
6214        // severity dot
6215        var dotCol = al.sev === 2 ? '#c56bff' : al.sev === 1 ? '#ffaa44' : 'rgba(150,130,130,0.6)';
6216        g.beginPath(); g.arc(22, ay - 1, 4, 0, Math.PI * 2);
6217        g.fillStyle = dotCol; g.fill();
6218
6219        // time
6220        g.textAlign = 'left';
6221        g.font = '7.5px "Courier New", monospace';
6222        g.fillStyle = 'rgba(130,115,160,0.6)';
6223        g.fillText(al.time, 30, ay + 2);
6224
6225        // alert type
6226        g.font = 'bold 8px "Courier New", monospace';
6227        g.fillStyle = isNew ? '#f4f2fb' : 'rgba(200,170,170,0.75)';
6228        g.fillText(al.type, 68, ay + 2);
6229
6230        // status badge
6231        var statCol = al.sev === 2 ? 'rgba(74,222,128,0.85)' : al.sev === 1 ? 'rgba(74,222,128,0.7)' : 'rgba(130,120,120,0.6)';
6232        g.textAlign = 'right';
6233        g.font = 'bold 6.5px "Courier New", monospace';
6234        g.fillStyle = statCol;
6235        g.fillText('✓ ' + al.status, W - 14, ay + 2);
6236      }
6237
6238      // blinking cursor on last row
6239      if (shown < alerts.length) {
6240        var curY = ALY + 16 + Math.min(shown, maxRows) * 30;
6241        if (tick % 40 < 25) {
6242          g.fillStyle = 'rgba(125,20,215,0.7)';
6243          g.fillRect(14, curY - 11, 6, 14);
6244        }
6245      }
6246
6247      // ── Footer ──────────────────────────────────────────────────
6248      g.strokeStyle = 'rgba(125,20,215,0.15)';
6249      g.lineWidth = 0.7;
6250      g.beginPath(); g.moveTo(0, H - 32); g.lineTo(W, H - 32); g.stroke();
6251
6252      g.textAlign = 'center';
6253      g.font = '7px "Courier New", monospace';
6254      g.fillStyle = 'rgba(125,20,215,0.4)';
6255      g.fillText('OSCP · CEH · CHFI · ISO27001', W / 2, H - 17);
6256      g.fillStyle = 'rgba(130,115,160,0.35)';
6257      g.fillText('REACTIVEZERO  ·  ROTTERDAM, THE NETHERLANDS', W / 2, H - 6);
6258
6259      requestAnimationFrame(frame);
6260    }
6261
6262    frame();
6263  })();
6264
6265  /* ── 5. GLITCH: sync data-text with i18n ───────────────────── */
6266  var heroH1 = document.querySelector('.section_header15 h1.heading-style-h1');
6267  if (heroH1) {
6268    heroH1.setAttribute('data-text', heroH1.textContent.trim());
6269    new MutationObserver(function () {
6270      heroH1.setAttribute('data-text', heroH1.textContent.trim());
6271    }).observe(heroH1, { childList: true, subtree: true, characterData: true });
6272  }
6273
6274  /* ── 6. NAV DROPDOWNS: keep open while moving to the items ──────
6275     Webflow's hover dropdown closed the menu the instant the cursor left the
6276     narrow toggle — including the empty "dead zone" beside it — so items were
6277     unreachable. We take over hover control on desktop with a short close
6278     delay: leaving schedules a close, re-entering (e.g. landing on the list)
6279     cancels it. The list visibility is driven by the `rz-open` class (see
6280     redesign.css). On tablet/mobile (<992px) the menu stays click-toggled. */
6281  (function () {
6282    var DESKTOP = '(min-width: 992px)';
6283    var CLOSE_DELAY = 250;
6284
6285    Array.prototype.forEach.call(
6286      document.querySelectorAll('.navbar2_menu-dropdown'),
6287      function (dd) {
6288        var list = dd.querySelector('.navbar2_dropdown-list');
6289        var toggle = dd.querySelector('.navbar2_dropdwn-toggle');
6290        if (!list || !toggle) return;
6291
6292        var timer = null;
6293
6294        function isDesktop() { return window.matchMedia(DESKTOP).matches; }
6295
6296        function open() {
6297          if (!isDesktop()) return;
6298          clearTimeout(timer);
6299          dd.classList.add('rz-open');
6300          list.classList.add('w--open');
6301          toggle.classList.add('w--open');
6302          toggle.setAttribute('aria-expanded', 'true');
6303        }
6304
6305        function close() {
6306          dd.classList.remove('rz-open');
6307          list.classList.remove('w--open');
6308          toggle.classList.remove('w--open');
6309          toggle.setAttribute('aria-expanded', 'false');
6310        }
6311
6312        function scheduleClose() {
6313          if (!isDesktop()) return;
6314          clearTimeout(timer);
6315          timer = setTimeout(close, CLOSE_DELAY);
6316        }
6317
6318        dd.addEventListener('mouseenter', open);
6319        dd.addEventListener('mouseleave', scheduleClose);
6320        list.addEventListener('mouseenter', open);
6321        // Close immediately once a destination is chosen.
6322        list.addEventListener('click', function () { clearTimeout(timer); close(); });
6323      }
6324    );
6325  })();
6326
6327})();
6328
6329/* ==== RZ-VARIANT:creds START ==== */
6330/* why-us "creds" visual: arm the CSS stagger and fire it on first view.
6331   No canvas, no globals beyond the init guard; static without JS. */
6332(function () {
6333  'use strict';
6334  if (window.__rzVCredsInit) return;
6335  window.__rzVCredsInit = true;
6336
6337  function addClass(el, name) {
6338    if (el.classList) el.classList.add(name);
6339    else if ((' ' + el.className + ' ').indexOf(' ' + name + ' ') < 0) el.className += ' ' + name;
6340  }
6341
6342  function init() {
6343    var root = document.querySelector('.rz-v-creds[data-variant="creds"]');
6344    if (!root) return;
6345
6346    var reduce = false;
6347    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
6348
6349    if (reduce || !('IntersectionObserver' in window)) {
6350      addClass(root, 'rz-v-creds--in');
6351      return;
6352    }
6353
6354    addClass(root, 'rz-v-creds--armed');
6355    var io = new IntersectionObserver(function (entries) {
6356      for (var i = 0; i < entries.length; i++) {
6357        if (entries[i].isIntersecting) {
6358          addClass(root, 'rz-v-creds--in');
6359          io.disconnect();
6360          return;
6361        }
6362      }
6363    }, { threshold: 0.15 });
6364    io.observe(root);
6365  }
6366
6367  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
6368  else init();
6369})();
6370/* ==== RZ-VARIANT:creds END ==== */
6371
6372
6373
6374/* ==== RZ-METHOD:pentest START ==== */
6375/* ==== RZ-METHOD-inner:pentest — engagement track ==== */
6376(function () {
6377  'use strict';
6378  var root = document.querySelector('.rz-m-pentest[data-method="pentest"]');
6379  if (!root) return;
6380
6381  var segs = [].slice.call(root.querySelectorAll('.rz-m-pentest-seg'));
6382  if (!segs.length) return;
6383  var btns = [], panels = [];
6384  for (var i = 0; i < segs.length; i++) {
6385    btns.push(segs[i].querySelector('.rz-m-pentest-seg-btn'));
6386    panels.push(segs[i].querySelector('.rz-m-pentest-panel'));
6387    if (!btns[i] || !panels[i]) return;
6388  }
6389  var grid = root.querySelector('.rz-m-pentest-segments');
6390  var caret = root.querySelector('.rz-m-pentest-caret');
6391
6392  var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
6393  var reduce = !!(mq && mq.matches);
6394  var active = -1, touched = false, paused = false, inView = false;
6395  var hoverTimer = null, autoTimer = null, rafId = null;
6396  var STEP_MS = 4200, HOVER_MS = 110;
6397
6398  function addClass(el, c) { if (el.className.indexOf(c) < 0) el.className += (el.className ? ' ' : '') + c; }
6399  function removeClass(el, c) { el.className = el.className.replace(new RegExp('(^|\\s)' + c + '(?!\\S)', 'g'), ' ').replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); }
6400
6401  /* layout mode is decided by CSS (breakpoint lives there, not here) */
6402  function mode() {
6403    var v = '';
6404    try { v = window.getComputedStyle(root).getPropertyValue('--rz-m-pentest-mode') || ''; } catch (e) {}
6405    return v.replace(/\s/g, '') === 'track' ? 'track' : 'stack';
6406  }
6407
6408  function placeCaret() {
6409    if (!caret) return;
6410    if (mode() !== 'track' || active < 0) { caret.hidden = true; return; }
6411    var b = btns[active];
6412    var x = b.offsetLeft + b.offsetWidth / 2;
6413    var w = grid.clientWidth || 1;
6414    caret.hidden = false;
6415    caret.style.transform = 'translateX(' + Math.round(x) + 'px)';
6416    if (x > w * 0.72) addClass(caret, 'is-flip'); else removeClass(caret, 'is-flip');
6417  }
6418
6419  function setActive(idx) {
6420    active = idx;
6421    for (var k = 0; k < segs.length; k++) {
6422      var on = k === idx;
6423      if (on) addClass(segs[k], 'is-active'); else removeClass(segs[k], 'is-active');
6424      btns[k].setAttribute('aria-expanded', on ? 'true' : 'false');
6425      panels[k].hidden = !on;
6426    }
6427    placeCaret();
6428  }
6429
6430  /* ── auto-advance: the caret walks the track until the reader takes over ── */
6431  function stopAuto() { if (autoTimer) { clearTimeout(autoTimer); autoTimer = null; } }
6432  function scheduleAuto() {
6433    stopAuto();
6434    if (reduce || touched || paused || !inView || mode() !== 'track') return;
6435    autoTimer = setTimeout(function () {
6436      autoTimer = null;
6437      if (document.hidden) { scheduleAuto(); return; }
6438      if (mode() === 'track' && !touched && !paused && inView) setActive((active + 1) % segs.length);
6439      scheduleAuto();
6440    }, STEP_MS);
6441  }
6442  function userTookOver() { touched = true; stopAuto(); }
6443
6444  /* ── wiring ── */
6445  function bind(i) {
6446    var b = btns[i];
6447    b.addEventListener('click', function () {
6448      userTookOver();
6449      if (mode() === 'stack' && active === i) setActive(-1); else setActive(i);
6450    });
6451    b.addEventListener('mouseenter', function () {
6452      if (mode() !== 'track') return;
6453      if (hoverTimer) clearTimeout(hoverTimer);
6454      hoverTimer = setTimeout(function () { hoverTimer = null; userTookOver(); if (active !== i) setActive(i); }, HOVER_MS);
6455    });
6456    b.addEventListener('mouseleave', function () { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } });
6457    b.addEventListener('keydown', function (e) {
6458      var key = e.key || '';
6459      var next = -1;
6460      if (key === 'ArrowRight' || key === 'ArrowDown') next = (i + 1) % segs.length;
6461      else if (key === 'ArrowLeft' || key === 'ArrowUp') next = (i - 1 + segs.length) % segs.length;
6462      else if (key === 'Home') next = 0;
6463      else if (key === 'End') next = segs.length - 1;
6464      if (next < 0) return;
6465      e.preventDefault();
6466      userTookOver();
6467      btns[next].focus();
6468      if (mode() === 'track') setActive(next);
6469    });
6470  }
6471  for (var j = 0; j < segs.length; j++) bind(j);
6472
6473  root.addEventListener('mouseenter', function () { paused = true; stopAuto(); });
6474  root.addEventListener('mouseleave', function () { paused = false; scheduleAuto(); });
6475  root.addEventListener('focusin', userTookOver);
6476
6477  var lastMode = null;
6478  function onResize() {
6479    if (rafId) return;
6480    rafId = (window.requestAnimationFrame || function (f) { return setTimeout(f, 16); })(function () {
6481      rafId = null;
6482      var m = mode();
6483      if (m !== lastMode) {
6484        lastMode = m;
6485        if (m === 'track' && active < 0) setActive(0);
6486      }
6487      placeCaret();
6488      scheduleAuto();
6489    });
6490  }
6491  window.addEventListener('resize', onResize);
6492  window.addEventListener('orientationchange', onResize);
6493  window.addEventListener('load', placeCaret);
6494  document.addEventListener('rz-theme-change', placeCaret);
6495
6496  /* ── init: JS present → collapse to one open phase, arm the reveal ── */
6497  addClass(root, 'rz-m-pentest--js');
6498  lastMode = mode();
6499  setActive(0);
6500  if (!reduce) addClass(root, 'rz-m-pentest--armed');
6501
6502  var revealed = false;
6503  function reveal() {
6504    if (revealed) return;
6505    revealed = true;
6506    addClass(root, 'rz-m-pentest--in');
6507    placeCaret();
6508    setTimeout(function () { removeClass(root, 'rz-m-pentest--armed'); }, 1600);
6509    scheduleAuto();
6510  }
6511  var fallback = setTimeout(function () { inView = true; reveal(); }, 1500);
6512
6513  if ('IntersectionObserver' in window) {
6514    var io = new IntersectionObserver(function (entries) {
6515      for (var n = 0; n < entries.length; n++) {
6516        inView = entries[n].isIntersecting;
6517        if (inView) { clearTimeout(fallback); reveal(); scheduleAuto(); } else stopAuto();
6518      }
6519    }, { threshold: 0.2 });
6520    io.observe(root);
6521  } else {
6522    inView = true;
6523    reveal();
6524  }
6525  if (typeof document.addEventListener === 'function') {
6526    document.addEventListener('visibilitychange', function () { if (!document.hidden) scheduleAuto(); else stopAuto(); });
6527  }
6528})();
6529/* ==== RZ-METHOD:pentest END ==== */
6530
6531/* ==== RZ-METHOD:redteam START ==== */
6532/* ============================================================
6533   METHODOLOGY — RED TEAMING · "OPERATION DOSSIER"
6534   Timeline scrubber lights each ATT&CK column in sequence.
6535   ES5 IIFE · self-init · IntersectionObserver + 1.5s fallback ·
6536   respects prefers-reduced-motion (shows final state) ·
6537   all visible strings live in the DOM (i18n-safe).
6538   ============================================================ */
6539(function () {
6540  'use strict';
6541  if (window.__rzMRedteamInit) return;
6542
6543  var root = document.querySelector('.rz-m-redteam[data-method="redteam"]');
6544  if (!root) return;
6545  window.__rzMRedteamInit = true;
6546
6547  var cols = [].slice.call(root.querySelectorAll('.rz-m-redteam-col'));
6548  var N = cols.length;
6549  if (!N) return;
6550
6551  var handle   = root.querySelector('.rz-m-redteam-scrub-handle');
6552  var readout  = root.querySelector('.rz-m-redteam-scrub-readout');
6553  var notches  = [].slice.call(root.querySelectorAll('.rz-m-redteam-track-notches span'));
6554  var stRun    = root.querySelector('.rz-m-redteam-status[data-state="run"]');
6555  var stDone   = root.querySelector('.rz-m-redteam-status[data-state="done"]');
6556
6557  /* five equal accents, in the site's rotation order */
6558  var ACCENTS = ['var(--rz-purple)', 'var(--rz-lime)', 'var(--rz-crimson)', 'var(--rz-indigo)', 'var(--rz-yellow)'];
6559
6560  var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
6561  function reduced() { return !!(mq && mq.matches); }
6562
6563  var DWELL = 1500;      /* per-phase dwell (ms) */
6564  var DONE_HOLD = 2800;  /* pause on the completed chain before looping */
6565
6566  var cur = 0, timer = 0, started = false, paused = false, pauseTimer = 0;
6567
6568  function textOf(el) { return el ? el.textContent.replace(/\s+/g, ' ').trim() : ''; }
6569  function codeOf(c)  { return textOf(c.querySelector('.rz-m-redteam-col-code')); }
6570  function titleOf(c) { return textOf(c.querySelector('.rz-m-redteam-col-title')); }
6571
6572  function setAccent(idx) {
6573    var a = ACCENTS[((idx % N) + N) % N % ACCENTS.length];
6574    root.style.setProperty('--rt-active', idx < 0 ? 'var(--rz-ink)' : a);
6575  }
6576
6577  /* cumulative "kill chain" lighting up to `upto`, active marker on `activeIdx` */
6578  function light(upto, activeIdx) {
6579    for (var i = 0; i < N; i++) {
6580      var lit = i <= upto;
6581      cols[i].classList.toggle('is-lit', lit);
6582      cols[i].classList.toggle('is-active', i === activeIdx);
6583      if (notches[i]) notches[i].classList.toggle('is-lit', lit);
6584    }
6585  }
6586
6587  function moveHandle(idx) {
6588    if (!handle) return;
6589    var pct = ((idx + 0.5) / N) * 100;
6590    handle.style.left = pct.toFixed(2) + '%';
6591  }
6592
6593  function setReadout(idx, done) {
6594    if (!readout) return;
6595    if (done) { readout.textContent = codeOf(cols[N - 1]); return; }
6596    var c = cols[idx];
6597    readout.textContent = codeOf(c) + ' · ' + titleOf(c);
6598  }
6599
6600  function status(done) {
6601    if (stRun)  stRun.hidden = !!done;
6602    if (stDone) stDone.hidden = !done;
6603  }
6604
6605  function showPhase(idx) {
6606    setAccent(idx);
6607    light(idx, idx);
6608    moveHandle(idx);
6609    setReadout(idx, false);
6610    status(false);
6611  }
6612
6613  function finalState() {
6614    setAccent(-1);
6615    light(N - 1, -1);
6616    moveHandle(N - 1);
6617    setReadout(N - 1, true);
6618    status(true);
6619  }
6620
6621  function clearTimers() {
6622    if (timer) { clearTimeout(timer); timer = 0; }
6623  }
6624
6625  function run() {
6626    if (paused) return;
6627    clearTimers();
6628    showPhase(cur);
6629    timer = setTimeout(function () {
6630      cur++;
6631      if (cur >= N) {
6632        finalState();
6633        timer = setTimeout(function () {
6634          cur = 0;
6635          light(-1, -1);
6636          run();
6637        }, DONE_HOLD);
6638      } else {
6639        run();
6640      }
6641    }, DWELL);
6642  }
6643
6644  function start() {
6645    if (started) return;
6646    started = true;
6647    if (reduced()) { finalState(); return; }
6648    cur = 0;
6649    run();
6650  }
6651
6652  /* header buttons: jump to a phase and pause the auto-play briefly */
6653  function selectPhase(i) {
6654    paused = true;
6655    clearTimers();
6656    cur = i;
6657    if (i >
6657= N - 1) { finalState(); } else { showPhase(i); }
6658    if (pauseTimer) clearTimeout(pauseTimer);
6659    pauseTimer = setTimeout(function () {
6660      paused = false;
6661      if (started && !reduced()) { cur = 0; light(-1, -1); run(); }
6662    }, 6000);
6663  }
6664
6665  cols.forEach(function (c, i) {
6666    var head = c.querySelector('.rz-m-redteam-col-head');
6667    if (!head) return;
6668    head.addEventListener('click', function () { selectPhase(i); });
6669  });
6670
6671  /* start when scrolled into view, with a guaranteed fallback */
6672  var io = null;
6673  if ('IntersectionObserver' in window) {
6674    io = new IntersectionObserver(function (entries) {
6675      for (var k = 0; k < entries.length; k++) {
6676        if (entries[k].isIntersecting) { start(); if (io) io.disconnect(); break; }
6677      }
6678    }, { threshold: 0.2 });
6679    io.observe(root);
6680  }
6681  setTimeout(function () { start(); if (io) { io.disconnect(); io = null; } }, 1500);
6682
6683  /* if the motion preference flips to "reduce" mid-run, settle on final state */
6684  if (mq) {
6685    var onMq = function () { if (mq.matches && started) { paused = true; clearTimers(); finalState(); } };
6686    if (mq.addEventListener) mq.addEventListener('change', onMq);
6687    else if (mq.addListener) mq.addListener(onMq);
6688  }
6689})();
6690/* ==== RZ-METHOD:redteam END ==== */
6691
6692/* ==== RZ-METHOD:cloud START ==== */
6693/* ── METHODOLOGY (cloud) — draw-in reveal for the architecture blueprint ── */
6694(function () {
6695  'use strict';
6696  var root = document.querySelector('.rz-m-cloud[data-method="cloud"]');
6697  if (!root) return;
6698
6699  /* reduced motion: final state immediately, nothing armed */
6700  var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
6701  if (mq && mq.matches) { root.classList.add('is-in'); return; }
6702
6703  var shown = false;
6704  function show() {
6705    if (shown) return;
6706    shown = true;
6707    root.classList.add('is-in');
6708  }
6709
6710  root.classList.add('is-armed');
6711
6712  if ('IntersectionObserver' in window) {
6713    var io = new IntersectionObserver(function (entries) {
6714      for (var i = 0; i < entries.length; i++) {
6715        if (entries[i].isIntersecting) { show(); io.disconnect(); return; }
6716      }
6717    }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
6718    io.observe(root);
6719    /* safety net: never leave the section hidden if the observer never fires */
6720    setTimeout(show, 5000);
6721  } else {
6722    setTimeout(show, 1500);
6723  }
6724})();
6725/* ==== RZ-METHOD:cloud END ==== */
6726
6727/* ==== RZ-METHOD:codereview START ==== */
6728/* ── RZ-METHOD: codereview — staggered first-view reveal of the PR graph ── */
6729(function () {
6730  'use strict';
6731  var root = document.querySelector('.rz-m-codereview[data-method="codereview"]');
6732  if (!root) return;
6733
6734  var reduce = false;
6735  try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
6736
6737  /* reduced motion or no IntersectionObserver → final state immediately, never animate */
6738  if (reduce || !('IntersectionObserver' in window)) {
6739    root.classList.add('rz-m-codereview--in');
6740    return;
6741  }
6742
6743  root.classList.add('rz-m-codereview--armed');
6744  var shown = false, timer = null, io = null;
6745
6746  function show() {
6747    if (shown) return;
6748    shown = true;
6749    if (timer) { clearTimeout(timer); timer = null; }
6750    if (io) { try { io.disconnect(); } catch (e) {} io = null; }
6751    root.classList.add('rz-m-codereview--in');
6752  }
6753
6754  try {
6755    io = new IntersectionObserver(function (entries) {
6756      for (var i = 0; i < entries.length; i++) {
6757        if (entries[i].isIntersecting) { show(); return; }
6758      }
6759    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
6760    io.observe(root);
6761  } catch (e) {
6762    show();
6763    return;
6764  }
6765
6766  /* safety net: content is never left hidden */
6767  timer = setTimeout(show, 1500);
6768})();
6769/* ==== RZ-METHOD:codereview END ==== */
6770
6771/* ==== RZ-METHOD:phishing START ==== */
6772/* ── PHISHING METHODOLOGY — Campaign Funnel (self-init, ES5) ─────────── */
6773(function () {
6774  'use strict';
6775  var root = document.querySelector('.rz-m-phishing[data-method="phishing"]');
6776  if (!root) return;
6777
6778  var reduce = false;
6779  try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
6780
6781  /* ── first-view reveal: armed only when we can drive it; content is
6782     never left hidden — an observer miss falls back to a timer. ── */
6783  var shown = false;
6784  function show() {
6785    if (shown) return;
6786    shown = true;
6787    root.classList.add('rz-m-phishing--in');
6788  }
6789  if (!reduce && 'IntersectionObserver' in window) {
6790    root.classList.add('rz-m-phishing--armed');
6791    var fired = false;
6792    var io = new IntersectionObserver(function (entries) {
6793      fired = true;
6794      for (var i = 0; i < entries.length; i++) {
6795        if (entries[i].isIntersecting) { show(); io.disconnect(); break; }
6796      }
6797    }, { threshold: 0.06, rootMargin: '0px 0px -8% 0px' });
6798    io.observe(root);
6799    /* 1.5s: observer never reported → reveal anyway; 6s: hard cap regardless */
6800    setTimeout(function () { if (!fired) show(); }, 1500);
6801    setTimeout(show, 6000);
6802    window.addEventListener('beforeprint', show);
6803  } else {
6804    show();
6805  }
6806
6807  /* ── cross-highlight: hovering a phase lights its funnel stage and
6808     vice-versa (pointer-only nicety; nothing depends on it). ── */
6809  var stages = root.querySelectorAll('.rz-m-phishing-stage');
6810  var phases = root.querySelectorAll('.rz-m-phishing-phase');
6811  function setHot(n, on) {
6812    var i;
6813    for (i = 0; i < stages.length; i++) {
6814      if (stages[i].getAttribute('data-stage') === n) stages[i].classList[on ? 'add' : 'remove']('rz-m-phishing-hot');
6815    }
6816    for (i = 0; i < phases.length; i++) {
6817      if (phases[i].getAttribute('data-stage') === n) phases[i].classList[on ? 'add' : 'remove']('rz-m-phishing-hot');
6818    }
6819  }
6820  function bind(list) {
6821    for (var i = 0; i < list.length; i++) {
6822      (function (el) {
6823        var n = el.getAttribute('data-stage');
6824        el.addEventListener('mouseenter', function () { setHot(n, true); });
6825        el.addEventListener('mouseleave', function () { setHot(n, false); });
6826      })(list[i]);
6827    }
6828  }
6829  bind(stages);
6830  bind(phases);
6831})();
6832/* ==== RZ-METHOD:phishing END ==== */
6833
6834/* ==== RZ-METHOD:mobile START ==== */
6835/* ── MOBILE APP TESTING — "Device teardown" methodology block ──
6836   Self-inits only if its root exists. Reveal-on-scroll with a
6837   1.5s fallback + no-IO + no-JS + reduced-motion paths so content
6838   is never left hidden. Cross-highlights layer planes <-> phase
6839   cards on hover/focus (pointer-only enhancement). ES5 IIFE. */
6840(function () {
6841  'use strict';
6842  if (window.rzMMobileInit) return;
6843
6844  function hasClass(c, k) { return new RegExp('(^|\\s)' + k + '(\\s|$)').test(c); }
6845  function addClass(el, k) { if (!hasClass(el.className, k)) el.className = (el.className + ' ' + k).replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); }
6846  function remClass(el, k) { el.className = el.className.replace(new RegExp('(^|\\s)' + k + '(?=\\s|$)', 'g'), ' ').replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); }
6847
6848  function init() {
6849    var root = document.querySelector('.rz-m-mobile[data-method="mobile"]');
6850    if (!root || root.getAttribute('data-rz-m-ready')) return;
6851    root.setAttribute('data-rz-m-ready', '1');
6852
6853    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6854
6855    /* Entrance is opt-in via .rz-m-js: without JS nothing is hidden.
6856       Under reduced motion we skip it entirely (final state shown). */
6857    if (!reduce) {
6858      addClass(root, 'rz-m-js');
6859      var shown = false;
6860      var show = function () { if (!shown) { shown = true; addClass(root, 'is-in'); } };
6861      if (!('IntersectionObserver' in window)) {
6862        show();
6863      } else {
6864        var io = new IntersectionObserver(function (entries) {
6865          for (var i = 0; i < entries.length; i++) {
6866            if (entries[i].isIntersecting) { io.disconnect(); show(); return; }
6867          }
6868        }, { threshold: 0.15 });
6869        io.observe(root);
6870        setTimeout(function () { io.disconnect(); show(); }, 1500); /* fallback */
6871      }
6872    }
6873
6874    /* Cross-highlight: hovering/focusing a plane lights its phase card, and vice versa. */
6875    var planes = root.querySelectorAll('.rz-m-mobile-plane');
6876    var phases = root.querySelectorAll('.rz-m-mobile-phase');
6877
6878    function setActive(idx, on) {
6879      var i, el;
6880      for (i = 0;
6880 i < planes.length; i++) { el = planes[i]; if (el.getAttribute('data-layer') === idx) { on ? addClass(el, 'is-active') : remClass(el, 'is-active'); } }
6881      for (i = 0; i < phases.length; i++) { el = phases[i]; if (el.getAttribute('data-phase') === idx) { on ? addClass(el, 'is-active') : remClass(el, 'is-active'); } }
6882    }
6883
6884    function bind(list, attr) {
6885      for (var i = 0; i < list.length; i++) {
6886        (function (el) {
6887          var idx = el.getAttribute(attr);
6888          el.addEventListener('mouseenter', function () { setActive(idx, true); });
6889          el.addEventListener('mouseleave', function () { setActive(idx, false); });
6890          el.addEventListener('focusin', function () { setActive(idx, true); });
6891          el.addEventListener('focusout', function () { setActive(idx, false); });
6892        })(list[i]);
6893      }
6894    }
6895    bind(planes, 'data-layer');
6896    bind(phases, 'data-phase');
6897  }
6898
6899  window.rzMMobileInit = init;
6900  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
6901  else init();
6902})();
6903/* ==== RZ-METHOD:mobile END ==== */
6904
6905/* ==== RZ-METHOD:forensics START ==== */
6906/* ── METHOD · DIGITAL FORENSICS — chain-of-custody ledger reveal ── */
6907(function () {
6908  'use strict';
6909  var root = document.querySelector('.rz-m-forensics[data-method="forensics"]');
6910  if (!root) return;
6911
6912  var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
6913  var reduce = !!(mq && mq.matches);
6914  var hashes = root.querySelectorAll('.rz-m-forensics-hash');
6915  var live = root.querySelector('.rz-m-forensics-status[data-state="live"]');
6916  var done = root.querySelector('.rz-m-forensics-status[data-state="done"]');
6917  var HEX = '0123456789abcdef';
6918  var revealed = false, sealed = false;
6919
6920  function setText(el, txt) {
6921    if (el.firstChild && el.firstChild.nodeType === 3 && !el.firstChild.nextSibling) {
6922      el.firstChild.nodeValue = txt;       /* keep the same text node (i18n cache) */
6923    } else {
6924      el.textContent = txt;
6925    }
6926  }
6927
6928  /* hex "settling" on a hash cell: non-hex glyphs (the ellipsis) stay put */
6929  function settle(el, delay) {
6930    var target = el.getAttribute('data-hash') || el.textContent;
6931    var frame = 0, frames = 12;
6932    setTimeout(function step() {
6933      var out = '', i, c;
6934      for (i = 0; i < target.length; i++) {
6935        c = target.charAt(i);
6936        if (HEX.indexOf(c) === -1) { out += c; continue; }
6937        out += (Math.random() < frame / frames) ? c : HEX.charAt(Math.floor(Math.random() * 16));
6938      }
6939      frame++;
6940      if (frame <= frames) { setText(el, out); setTimeout(step, 40); }
6941      else { setText(el, target); }
6942    }, delay);
6943  }
6944
6945  function markSealed() {
6946    if (sealed) return;
6947    sealed = true;
6948    root.classList.add('rz-m-forensics--sealed');
6949    if (live) live.hidden = true;
6950    if (done) done.hidden = false;
6951  }
6952
6953  function reveal() {
6954    if (revealed) return;
6955    revealed = true;
6956    root.classList.add('rz-m-forensics--in');
6957    if (reduce) { markSealed(); return; }
6958    var i;
6959    for (i = 0; i < hashes.length; i++) settle(hashes[i], 300 + i * 160);
6960    setTimeout(markSealed, 2000);
6961  }
6962
6963  /* reduced motion or no observer: final state straight away */
6964  if (reduce || !('IntersectionObserver' in window)) { reveal(); return; }
6965
6966  root.classList.add('rz-m-forensics--armed');
6967  if (live && done) { done.hidden = true; live.hidden = false; }
6968
6969  var io = new IntersectionObserver(function (entries) {
6970    var i;
6971    for (i = 0; i < entries.length; i++) {
6972      if (entries[i].isIntersecting) { reveal(); io.disconnect(); return; }
6973    }
6974  }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
6975  io.observe(root);
6976
6977  /* fallbacks: never leave the ledger hidden */
6978  function inView() {
6979    var r = root.getBoundingClientRect();
6980    var vh = window.innerHeight || document.documentElement.clientHeight;
6981    return r.top < vh * 0.92 && r.bottom > 0;
6982  }
6983  function onScroll() {
6984    if (revealed) { window.removeEventListener('scroll', onScroll); return; }
6985    if (inView()) reveal();
6986  }
6987  setTimeout(function () {
6988    if (revealed) return;
6989    if (inView()) { reveal(); return; }
6990    window.addEventListener('scroll', onScroll, { passive: true });
6991    window.addEventListener('resize', onScroll);
6992    setTimeout(reveal, 2500);      /* hard stop: content is never left hidden */
6993  }, 1500);
6994})();
6995/* ==== RZ-METHOD:forensics END ==== */
6996
6997/* ==== RZ-METHOD:incident START ==== */
6998/* ==== RZ-METHOD-inner:incident — Response Clock ==== */
6999(function () {
7000  'use strict';
7001  var root = document.querySelector('.rz-m-incident[data-method="incident"]');
7002  if (!root) return;
7003
7004  var CX = 230, CY = 230, SECTOR = 60, N = 6;
7005  var PHASE_MS = 6000;        /* sweep time per phase */
7006  var DWELL_MS = 9000;        /* hold after a user selection */
7007  var EASE_MS = 550;          /* hand travel on selection */
7008
7009  var steps = [].slice.call(root.querySelectorAll('.rz-m-incident-step'));
7010  var tabs = [].slice.call(root.querySelectorAll('.rz-m-incident-tab'));
7011  var arcs = [].slice.call(root.querySelectorAll('.rz-m-incident-arc'));
7012  var rimNums = [].slice.call(root.querySelectorAll('.rz-m-incident-rim-num'));
7013  var readouts = [].slice.call(root.querySelectorAll('.rz-m-incident-dial-readout'));
7014  var wedges = [].slice.call(root.querySelectorAll('.rz-m-incident-wedge'));
7015  var hand = root.querySelector('.rz-m-incident-hand');
7016  var trail = root.querySelector('.rz-m-incident-trail');
7017  var cNum = root.querySelector('.rz-m-incident-centre-num');
7018  var cTime = root.querySelector('.rz-m-incident-centre-time');
7019  if (steps.length !== N || !hand) return;
7020
7021  var reduce = false;
7022  try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
7023  var mqPhone = null;
7024  try { mqPhone = window.matchMedia ? window.matchMedia('(max-width: 767px)') : null; } catch (e) {}
7025  function stacked() { return !!(mqPhone && mqPhone.matches); }
7026
7027  /* illustrative readouts in minutes, from the markup */
7028  var times = steps.map(function (li) {
7029    var t0 = li.getAttribute('data-rz-t0'), t1 = li.getAttribute('data-rz-t1');
7030    return (t0 === '' || t0 === null) ? null : { t0: parseFloat(t0), t1: parseFloat(t1) };
7031  });
7032
7033  root.className += ' is-js';
7034
7035  var phase = 0, angle = 30, hover = false, focused = false;
7036  var ease = null, dwellUntil = 0, raf = null, last = 0, lastTime = '', inView = true;
7037
7038  function pad(n) { return (n < 10 ? '0' : '') + n; }
7039  function fmt(min) { var m = Math.max(0, Math.floor(min)); return 'T+' + pad(Math.floor(m / 60)) + ':' + pad(m % 60); }
7040  function pt(r, deg) { var a = deg * Math.PI / 180; return (CX + r * Math.sin(a)).toFixed(2) + ' ' + (CY - r * Math.cos(a)).toFixed(2); }
7041
7042  function setPhase(i) {
7043    phase = i;
7044    var st = stacked();
7045    steps.forEach(function (li, k) {
7046      var on = k === i;
7047      li.className = li.className.replace(/\s*\bis-active\b/g, '') + (on ? ' is-active' : '');
7048      if (on) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current');
7049      tabs[k].setAttribute('aria-expanded', (on || st) ? 'true' : 'false');
7050    });
7051    arcs.forEach(function (a, k) {
7052      a.className.baseVal = 'rz-m-incident-arc' + (k === i ? ' is-active' : (k < i ? ' is-done' : ''));
7053    });
7054    rimNums.forEach(function (t, k) { t.className.baseVal = 'rz-m-incident-rim-num' + (k === i ? ' is-active' : ''); });
7055    readouts.forEach(function (t, k) { t.className.baseVal = 'rz-m-incident-dial-readout' + (k === i ? ' is-active' : ''); });
7056    if (cNum) cNum.textContent = pad(i + 1);
7057  }
7058
7059  function render() {
7060    hand.setAttribute('transform', 'rotate(' + angle.toFixed(2) + ' ' + CX + ' ' + CY + ')');
7061    var s0 = phase * SECTOR, local = angle - s0;
7062    if (trail) {
7063      if (local > 0.5 && local < SECTOR) {
7064        var a1 = s0 + local, big = local > 180 ? 1 : 0;
7065        trail.setAttribute('d', 'M' + pt(150, s0) + ' A150 150 0 ' + big + ' 1 ' + pt(150, a1) +
7066          ' L' + pt(112, a1) + ' A112 112 0 ' + big + ' 0 ' + pt(112, s0) + ' Z');
7067      } else trail.setAttribute('d', '');
7068    }
7069    if (cTime) {
7070      var t = times[phase], txt;
7071      if (!t) {
7072        var span = steps[phase].querySelector('.rz-m-incident-tab-time');
7073        txt = span ? span.textContent : '';
7074      } else {
7075        var f = Math.min(1, Math.max(0, local / SECTOR));
7076        txt = fmt(t.t0 + (t.t1 - t.t0) * f);
7077      }
7078      if (txt !== lastTime) { lastTime = txt; cTime.textContent = txt; }
7079    }
7080  }
7081
7082  function select(i, now) {
7083    if (i === phase && !ease) { dwellUntil = (now || performance.now()) + DWELL_MS; return; }
7084    setPhase(i);
7085    if (reduce) { angle = i * SECTOR + 30; render(); return; }
7086    var target = i * SECTOR + 3;
7087    var delta = ((target - angle + 540) % 360) - 180;
7088    ease = { from: angle, delta: delta, start: now || performance.now() };
7089    if (!raf) start();
7090  }
7091
7092  function frame(now) {
7093    raf = null;
7094    var dt = Math.min(64, now - (last || now)); last = now;
7095    if (ease) {
7096      var p = Math.min(1, (now - ease.start) / EASE_MS);
7097      var k = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
7098      angle = (ease.from + ease.delta * k + 360) % 360;
7099      if (p >= 1) { ease = null; dwellUntil = now + DWELL_MS; }
7100    } else if (!hover && !focused && now >= dwellUntil) {
7101      angle = (angle + dt * SECTOR / PHASE_MS) % 360;
7102      var np = Math.floor(angle / SECTOR);
7103      if (np !== phase) setPhase(np);
7104    }
7105    render();
7106    if (inView && !reduce) raf = requestAnimationFrame(frame);
7107  }
7108  function start() { if (!raf && !reduce) { last = 0; raf = requestAnimationFrame(frame); } }
7109  function stop() { if (raf) { cancelAnimationFrame(raf); raf = null; } }
7110
7111  /* ── interaction ── */
7112  tabs.forEach(function (btn, i) {
7113    btn.addEventListener('click', function () { select(i); });
7114    btn.addEventListener('keydown', function (e) {
7115      var j = -1;
7116      if (e.key === 'ArrowDown' || e.key === 'ArrowRight') j = (i + 1) % N;
7117      else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') j = (i + N - 1) % N;
7118      else if (e.key === 'Home') j = 0;
7119      else if (e.key === 'End') j = N - 1;
7120      if (j >= 0) { e.preventDefault(); tabs[j].focus(); }
7121    });
7122  });
7123  wedges.forEach(function (w) {
7124    w.addEventListener('click', function () { select(parseInt(w.getAttribute('data-rz-phase'), 10) || 0); });
7125  });
7126  if (window.PointerEvent) {
7127    root.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') hover = true; });
7128    root.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') hover = false; });
7129  } else {
7130    root.addEventListener('mouseenter', function () { hover = true; });
7131    root.addEventListener('mouseleave', function () { hover = false; });
7132  }
7133  root.addEventListener('focusin', function () { focused = true; });
7134  root.addEventListener('focusout', function (e) { focused = !!(e.relatedTarget && root.contains(e.relatedTarget)); });
7135  if (mqPhone) {
7136    var onMq = function () { setPhase(phase); };
7137    if (mqPhone.addEventListener) mqPhone.addEventListener('change', onMq);
7138    else if (mqPhone.addListener) mqPhone.addListener(onMq);
7139  }
7140
7141  /* ── arm (draw-in) + run only while in view; 1.5 s fallback so nothing stays hidden ── */
7142  var armed = false;
7143  function arm() { if (!armed) { armed = true; root.className += ' is-armed'; } }
7144  setTimeout(arm, 1500);
7145  if (reduce) arm();
7146
7147  setPhase(0);
7148  dwellUntil = performance.now() + 2500;
7149  render();
7150
7151  if ('IntersectionObserver' in window) {
7152    var io = new IntersectionObserver(function (entries) {
7153      inView = entries[0].isIntersecting;
7154      if (inView) { arm(); start(); } else stop();
7155    }, { threshold: 0.08 });
7156    io.observe(root);
7157  } else { arm(); start(); }
7158})();
7159/* ==== RZ-METHOD-inner:incident END ==== */
7160/* ==== RZ-METHOD:incident END ==== */
7161
7162/* ==== RZ-METHOD:malware START ==== */
7163/* ── METHODOLOGY: malware-analysis.html — Sandbox Pipeline reveal ──
7164   Arms the pipeline (cards + connectors hidden) only once JS is
7165   running, then reveals on first intersection. Safety net: a 1.5s
7166   timeout reveals regardless, and reduced-motion / no-IO browsers
7167   are shown the final state immediately. */
7168(function () {
7169  'use strict';
7170  var root = document.querySelector('.rz-m-malware[data-method="malware"]');
7171  if (!root) return;
7172
7173  function addClass(el, c) { if ((' ' + el.className + ' ').indexOf(' ' + c + ' ') < 0) el.className += ' ' + c; }
7174
7175  var reduce = false;
7176  try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
7177
7178  if (reduce || !('IntersectionObserver' in window)) { addClass(root, 'is-live'); return; }
7179
7180  addClass(root, 'is-armed');
7181
7182  var io = null, done = false;
7183  function live() {
7184    if (done) return;
7185    done = true;
7186    addClass(root, 'is-live');
7187    if (io) { try { io.disconnect(); } catch (e) {} }
7188  }
7189
7190  try {
7191    io = new IntersectionObserver(function (entries) {
7192      for (var i = 0; i < entries.length; i++) {
7193        if (entries[i].isIntersecting) { live(); break; }
7194      }
7195    }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
7196    io.observe(root);
7197  } catch (e) { live(); }
7198
7199  window.setTimeout(live, 1500);   /* never leave the section hidden */
7200})();
7201/* ==== RZ-METHOD:malware END ==== */
7202
7203/* ==== RZ-METHOD:custom START ==== */
7204/* ── METHODOLOGY (custom) — modules snap into the blueprint board on first view ── */
7205(function () {
7206  'use strict';
7207  var root = document.querySelector('.rz-m-custom[data-method="custom"]');
7208  if (!root) return;
7209
7210  /* The built board is the CSS default; we only "disassemble" it (is-armed) when
7211     motion is allowed, then snap it together (is-in) once it scrolls into view. */
7212  var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
7213  if (mq && mq.matches) return;
7214
7215  var shown = false;
7216  function show() {
7217    if (shown) return;
7218    shown = true;
7219    root.classList.add('is-in');
7220    window.removeEventListener('scroll', onScroll);
7221    window.removeEventListener('resize', onScroll);
7222  }
7223  function inView() {
7224    var r = root.getBoundingClientRect();
7225    var vh = window.innerHeight || document.documentElement.clientHeight || 0;
7226    return r.bottom > 0 && r.top < vh * 0.92;
7227  }
7228  function onScroll() { if (inView()) show(); }
7229
7230  root.classList.add('is-armed');
7231
7232  if ('IntersectionObserver' in window) {
7233    var io = new IntersectionObserver(function (entries) {
7234      for (var i = 0; i < entries.length; i++) {
7235        if (entries[i].isIntersecting) { show(); io.disconnect(); return; }
7236      }
7237    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
7238    io.observe(root);
7239    /* safety net: the board is never left disassembled if the observer never fires */
7240    setTimeout(show, 5000);
7241  } else {
7242    setTimeout(show, 1500);
7243  }
7244  /* active fallback alongside the observer: any scroll that brings it on screen assembles it */
7245  window.addEventListener('scroll', onScroll, false);
7246  window.addEventListener('resize', onScroll, false);
7247})();
7248/* ==== RZ-METHOD:custom END ==== */
7249
7250/* ==== RZ-ABOUT START ==== */
7251/* ==== RZ-PAGE:about START ==== */
7252/* about.html: stagger the section reveals on first view and mark the active
7253   site language on the identity card. ES5, self-init only when .rz-about is
7254   present, static (nothing hidden) without JS or under reduced motion. */
7255(function () {
7256  'use strict';
7257  if (window.__rzAboutInit) return;
7258  window.__rzAboutInit = true;
7259
7260  function addClass(el, name) {
7261    if (el.classList) el.classList.add(name);
7262    else if ((' ' + el.className + ' ').indexOf(' ' + name + ' ') < 0) el.className += ' ' + name;
7263  }
7264  function removeClass(el, name) {
7265    if (el.classList) el.classList.remove(name);
7266    else el.className = (' ' + el.className + ' ').replace(' ' + name + ' ', ' ').replace(/^\s+|\s+$/g, '');
7267  }
7268
7269  function init() {
7270    var root = document.querySelector('.rz-about[data-page="about"]');
7271    if (!root) return;
7272
7273    /* ---- identity card: underline the active language (follows i18n.js via html[lang]) ---- */
7274    var langs = root.querySelectorAll('.rz-about-idcard-lang');
7275    function markLang() {
7276      var cur = (document.documentElement.getAttribute('lang') || 'en').split('-')[0].toLowerCase();
7277      var known = false, i;
7278      for (i = 0; i < langs.length; i++) {
7279        if (langs[i].getAttribute('data-lang') === cur) known = true;
7280      }
7281      if (!known) cur = 'en';
7282      for (i = 0; i < langs.length; i++) {
7283        if (langs[i].getAttribute('data-lang') === cur) addClass(langs[i], 'is-active');
7284        else removeClass(langs[i], 'is-active');
7285      }
7286    }
7287    if (langs.length) {
7288      markLang();
7289      if (window.MutationObserver) {
7290        try {
7291          new MutationObserver(markLang).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
7292        } catch (e) {}
7293      }
7294    }
7295
7296    /* ---- staggered first-view reveal per group ---- */
7297    var reduce = false;
7298    try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {}
7299
7300    var groups = root.querySelectorAll('[data-rz-about-reveal]');
7301    if (reduce || !groups.length || !('IntersectionObserver' in window)) return;   /* stays static */
7302
7303    var g, j, items;
7304    for (g = 0; g < groups.length; g++) {
7305      items = groups[g].querySelectorAll('.rz-about-anim');
7306      for (j = 0; j < items.length; j++) {
7307        if (items[j].style && items[j].style.setProperty) {
7308          items[j].style.setProperty('--rz-about-d', Math.min(j * 70, 560) + 'ms');
7309        }
7310      }
7311    }
7312    addClass(root, 'rz-about--js');
7313
7314    var io = new IntersectionObserver(function (entries) {
7315      for (var i = 0; i < entries.length; i++) {
7316        if (entries[i].isIntersecting) {
7317          addClass(entries[i].target, 'is-in');
7318          io.unobserve(entries[i].target);
7319        }
7320      }
7321    }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
7322    for (g = 0; g < groups.length; g++) io.observe(groups[g]);
7323
7324    /* safety net: never leave a group hidden (e.g. anchor jumps past it) */
7325    window.setTimeout(function () {
7326      for (var k = 0; k < groups.length; k++) {
7327        var r = groups[k].getBoundingClientRect();
7328        if (r.top < (window.innerHeight || document.documentElement.clientHeight)) addClass(groups[k], 'is-in');
7329      }
7330    }, 1200);
7331  }
7332
7333  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
7334  else init();
7335})();
7336/* ==== RZ-PAGE:about END ==== */
7337/* ==== RZ-ABOUT END ==== */

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.