Private/ReportScript.ps1

# What the Report does in a browser: its script, carried inside the page. See Private/Report.ps1.
#
# Comfort only. The Report is complete without it: <details> folds by itself.
$script:ReportScript = @'
<script>
(function(){
  function each(sel, fn){ Array.prototype.forEach.call(document.querySelectorAll(sel), fn); }
  // One button opens and folds all of a part: everything, where anything shown is folded,
  // and nothing, where all of it is open. Its sign says which a click will do, and is said
  // again whenever something opens or folds - by hand, by a view, by a search.
  var foldSigns = {
    open: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 6l5 5 5-5M7 13l5 5 5-5"/></svg>',
    shut: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 18l5-5 5 5M7 11l5-5 5 5"/></svg>'
  };
  function shownOnly(list){ return Array.prototype.filter.call(list, function(x){ return !x.hidden && !x.classList.contains('sx') && x.style.display !== 'none' && x.offsetParent !== null; }); }
  function foldItems(b){
    if (b.classList.contains('gall')) return shownOnly(b.closest('details.group').querySelectorAll('.row.more'));
    if (b.hasAttribute('data-cards')) return shownOnly(document.querySelectorAll('#ch-action .card.more'));
    // The folds, and not the help inside one: that is opened by whoever wants to read it.
    return shownOnly(document.querySelectorAll(b.getAttribute('data-fold') + ' details:not(.help)'));
  }
  function isOpen(x){ return x.tagName === 'DETAILS' ? x.open : x.classList.contains('open'); }
  function syncFold(){
    each('[data-fold],[data-cards],.gall', function(b){
      var g = b.classList.contains('gall') ? b.closest('details.group') : null;
      var items = foldItems(b);
      var open = (g && !g.open) || !items.length || items.some(function(x){ return !isOpen(x); });
      b.innerHTML = open ? foldSigns.open : foldSigns.shut;
      b.title = b.getAttribute(open ? 'data-open' : 'data-shut') || '';
      b.setAttribute('aria-label', b.title);
    });
  }
  var syncing = false;
  function syncSoon(){ if (syncing) return; syncing = true; setTimeout(function(){ syncing = false; syncFold(); }, 0); }
  document.addEventListener('toggle', syncSoon, true);
  document.addEventListener('click', function(e){
    var b = e.target.closest ? e.target.closest('[data-fold],[data-cards],.gall') : null;
    if (!b) return;
    // The button of an area sits in its head: a click on it opens the area, and folds nothing.
    var g = b.classList.contains('gall') ? b.closest('details.group') : null, forced = false;
    if (g) { e.preventDefault(); e.stopPropagation(); if (!g.open) { g.open = true; forced = true; markMore(); } }
    var items = foldItems(b), open = forced || items.some(function(x){ return !isOpen(x); });
    items.forEach(function(x){ if (x.tagName === 'DETAILS') x.open = open; else x.classList.toggle('open', open); });
    syncFold();
  }, true);
  var q = document.getElementById('evidence-filter');
  if (q) q.addEventListener('input', function(){
    var words = q.value.toLowerCase().split(/\s+/).filter(Boolean);
    each('#evidence details:not(.help)', function(d){
      var t = d.getAttribute('data-title') || '';
      d.style.display = words.every(function(w){ return t.indexOf(w) >= 0; }) ? '' : 'none';
    });
    // A heading with nothing left under it goes too.
    each('#evidence .kind', function(k){
      var left = Array.prototype.some.call(k.querySelectorAll('details:not(.help)'), function(d){ return d.style.display !== 'none'; });
      k.style.display = left ? '' : 'none';
    });
    each('#evidence .egroup', function(g){
      var left = Array.prototype.some.call(g.querySelectorAll('.kind'), function(k){ return k.style.display !== 'none'; });
      g.style.display = left ? '' : 'none';
    });
  });
  // The tree beside the Report. Picking an entry shows that part of the Report and
  // nothing else; a Finding is one line until somebody opens it. Without this script
  // every chapter stands under the other and everything is open.
  var root = document.documentElement, nav = document.getElementById('nav'), view = 'overview';
  // Which Findings have more than their one line: further lines, what it means, what to
  // do, where the evidence is - or a first line too long for the width, cut off with
  // "...". Only those carry the sign and open on a click: a line that opens to the same
  // line was a click that did nothing. Decided where they are shown, as the width there
  // decides what is cut off.
  function markMore(){
    each('#ch-action .card,#checks .row', function(el){
      if (el.classList.contains('open') || el.offsetParent === null) return;
      var card = el.classList.contains('card');
      var more = !!el.querySelector(card ? '.parts>li:nth-child(2),.meaning,.todo,.refs' : '.val>div:nth-child(2),.meaning,.hint,.refs');
      var first = el.querySelector(card ? '.parts>li:first-child' : '.val>div:first-child');
      if (!more && first && first.scrollWidth > first.clientWidth + 1) more = true;
      el.classList.toggle('more', more);
    });
  }
  window.addEventListener('resize', function(){ markMore(); });
  // Ends a search; the search below says how. A link ends it.
  var stopFind = function(){};
  // A pick while something is searched for searches there; the search below says how.
  var findIn = function(){ return false; };
  // Told of every view shown, so that the search can say where it would look.
  var viewShown = function(){};
  function chapter(name){ return document.querySelector('[data-chapter="' + name + '"]'); }
  // A view in the address: "#v:cat:Network". A Finding's own address is its id, "#r-12",
  // and stays what it is.
  function viewOfHash(){
    var h = location.hash.slice(1);
    try { h = decodeURIComponent(h); } catch (x) { }
    return h.indexOf('v:') === 0 ? h.slice(2) : '';
  }
  // A view picked in the tree is a step of the browser's history; the same view again is not.
  // Not "go": the search has a go of its own, declared in a block, and it took this one's place.
  function goView(next){
    closeFull();
    show(next);
    try {
      if (!(history.state && history.state.view === next)) history.pushState({ view: next }, '', '#v:' + encodeURIComponent(next));
    } catch (x) { }
  }
  // What the head of an area counts is what stands under it: picked for one application,
  // "Apps" with the counts of every application was read as that application's.
  var levels = ['FAIL', 'WARN', 'INFO', 'OK'];
  function tally(g, rows){
    var s = g.querySelector('summary'), b = s ? s.querySelector('b') : null;
    if (!b) return;
    if (!g.whole) g.whole = { cls: s.className, html: Array.prototype.map.call(s.querySelectorAll('.badge'), function(x){ return x.outerHTML; }).join(' ') };
    var cls = g.whole.cls, html = g.whole.html;
    if (rows) {
      var n = {};
      rows.forEach(function(r){ levels.forEach(function(v){ if (r.classList.contains(v)) n[v] = (n[v] || 0) + 1; }); });
      var seen = levels.filter(function(v){ return n[v]; });
      html = seen.map(function(v){ return '<span class="badge ' + v + '">' + n[v] + ' ' + v + '</span>'; }).join(' ');
      if (seen.length) cls = seen[0];
    }
    Array.prototype.forEach.call(s.querySelectorAll('.badge'), function(x){ x.parentNode.removeChild(x); });
    s.className = cls;
    (s.querySelector('.gall') || b).insertAdjacentHTML('afterend', html);
  }
  function show(next){
    if (!nav) return;
    view = next;
    document.body.setAttribute('data-view', next);
    var isCat = next.indexOf('cat:') === 0, isApp = next.indexOf('app:') === 0, isFind = next === 'find';
    var narrowed = next === 'info' || isCat || isApp || isFind;
    // What machine this is and how the Run went is a chapter, and no head over every
    // other: it stood above whatever was picked, and pushed that out of sight. What puts
    // the Report itself in doubt is said with the overview and with what needs doing.
    var want = { overview: next === 'overview', alerts: next === 'overview' || next === 'action', leads: next === 'action',
                 action: next === 'action', checks: next === 'all' || narrowed, evidence: next === 'evidence' || isFind, glossary: next === 'glossary' || isFind, network: next === 'network' };
    for (var k in want) { var c = chapter(k); if (c) c.hidden = !want[k]; }
    var key = next.slice(4);
    each('#checks details.group', function(g){
      var any = false, head = null, under = false, kept = [];
      Array.prototype.forEach.call(g.children, function(el){
        if (el.classList.contains('subject')) { if (head) head.hidden = !under; head = el; under = false; return; }
        if (!el.classList.contains('row')) return;
        var app = el.getAttribute('data-app') || '';
        var keep = !narrowed || (next === 'info' && el.classList.contains('INFO')) || (isCat && g.getAttribute('data-cat') === key && !app) || (isApp && app === key) ||
                   (isFind && !el.classList.contains('sx'));
        el.hidden = !keep;
        if (keep) { any = true; under = true; kept.push(el); }
      });
      if (head) head.hidden = !under;
      g.hidden = !any;
      tally(g, narrowed ? kept : null);
      // One area or one application is what was asked for, and stands open. The hints and
      // all Checks are many areas: each folded, to be opened by whoever wants one.
      if (want.checks) g.open = (isCat || isApp || isFind) && any;
    });
    var note = document.getElementById('viewnote');
    if (note) {
      note.hidden = !narrowed || isFind;
      var picked = nav.querySelector('[data-pick="' + next.replace(/"/g, '\\"') + '"] .nm');
      note.querySelector('b').textContent = picked ? picked.textContent : '';
    }
    each('#nav [data-pick]', function(b){ b.setAttribute('aria-current', String(b.getAttribute('data-pick') === next)); });
    viewShown(next);
    markMore();
    syncFold();
  }
  if (nav) {
    root.classList.add('js');
    nav.addEventListener('click', function(e){
      var b = e.target.closest ? e.target.closest('a,button') : null;
      if (!b) return;
      if (b.hasAttribute('data-pick')) { e.preventDefault(); if (!findIn(b.getAttribute('data-pick'))) goView(b.getAttribute('data-pick')); window.scrollTo(0, 0); return; }
      if (b.classList.contains('gh')) { b.setAttribute('aria-expanded', String(b.getAttribute('aria-expanded') === 'false')); return; }
      if (b.classList.contains('more')) {
        var g = b.parentNode, all = g.classList.toggle('all');
        b.textContent = all ? nav.getAttribute('data-less') : nav.getAttribute('data-more').replace('{0}', b.getAttribute('data-count'));
      }
    });
    var nq = document.getElementById('nav-filter');
    if (nq) nq.addEventListener('input', function(){
      var word = nq.value.toLowerCase().trim();
      each('#nav .navgroup', function(g){
        g.classList.toggle('searching', !!word);
        Array.prototype.forEach.call(g.querySelectorAll('[data-pick]'), function(b){ b.hidden = !!word && b.textContent.toLowerCase().indexOf(word) < 0; });
      });
    });
    var back = document.querySelector('#viewnote button');
    if (back) back.addEventListener('click', function(){ goView('all'); });
    // A Finding opens where it is clicked - but not by marking text in it, and not by a
    // link or a button inside it.
    document.addEventListener('click', function(e){
      if (!e.target.closest || e.target.closest('a,button,input,summary,.long')) return;
      if (window.getSelection && window.getSelection().toString()) return;
      // Anywhere in it: a line with one small place to hit is a line that is missed.
      var item = e.target.closest('.card') || e.target.closest('#checks .row');
      if (!item) return;
      // The second click of a double-click marks a word, and is no click to fold.
      if (e.detail > 1) return;
      // An open one closes from its head only: a click in its text is a click to read it,
      // and a double-click to mark a word closed it under the pointer. One with nothing
      // more does not open.
      if (item.classList.contains('open') && !e.target.closest('.card>header,#checks .row .what')) return;
      if (!item.classList.contains('open') && !item.classList.contains('more')) return;
      item.classList.toggle('open');
      syncFold();
    });
    // A Report opens on what machine this is and how the Run went: from there the tree
    // leads to what stands out, and the reader knows what it is about. Opened again with
    // a view in its address - reloaded, or sent as a link - it opens on that view.
    var start = viewOfHash();
    show(start || 'overview');
    try { history.replaceState({ view: start || 'overview' }, ''); } catch (x) { }
    // Every line of one area opened, or folded, from its head, with the words of the
    // button over all of them.
    var all = document.querySelector('[data-fold="#checks"]');
    if (all) {
      each('#checks details.group>summary', function(s){
        var b = s.querySelector('b'); if (!b) return;
        var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'gall ic';
        btn.setAttribute('data-open', all.getAttribute('data-open') || ''); btn.setAttribute('data-shut', all.getAttribute('data-shut') || '');
        b.insertAdjacentElement('afterend', btn);
      });
    }
    syncFold();
  }
  // A Hint that names a piece of evidence leads there: shown again if filtered away, and open.
  function reveal(){
    var d = location.hash.length > 1 ? document.getElementById(location.hash.slice(1)) : null;
    if (!d) return;
    // Out of a table over the whole window: the window is the Report's again.
    closeFull();
    stopFind();
    // In whatever chapter it is: that chapter is shown, and the Finding opened.
    var home = d.closest ? d.closest('[data-chapter]') : null;
    if (nav && home) {
      var name = home.getAttribute('data-chapter');
      show(name === 'checks' ? 'all' : (name === 'evidence' || name === 'overview' || name === 'glossary' || name === 'network' ? name : 'action'));
      if (d.classList.contains('card') || d.classList.contains('row')) d.classList.add('open');
    }
    // A Section is a fold of its own; a Finding's line sits in the fold of its Category.
    var fold = d.tagName === 'DETAILS' ? d : (d.closest ? d.closest('details') : null);
    if (fold) { fold.style.display = ''; if (fold.parentNode && fold.parentNode.style) fold.parentNode.style.display = ''; fold.open = true; }
    // Below the search, which stands over the top of the page, and outlined for a moment:
    // at the very top it was hidden behind the search, and among its neighbours not found.
    d.scrollIntoView();
    d.classList.remove('jumped'); void d.offsetWidth; d.classList.add('jumped');
    // Taken off once it has run: a part shown again starts its animations again, and every
    // entry ever jumped to lit up once more.
    d.addEventListener('animationend', function done(){ d.classList.remove('jumped'); d.removeEventListener('animationend', done); });
  }
  window.addEventListener('hashchange', reveal);
  // Back and forward go from view to view as the tree was used, and from a Finding a link
  // led to back to where the link was.
  window.addEventListener('popstate', function(e){
    if (!nav) return;
    var to = (e.state && e.state.view) || viewOfHash();
    if (to) { stopFind(); show(to); }
  });
  document.addEventListener('click', function(e){ if (e.target.closest && e.target.closest('a.jump')) setTimeout(reveal, 0); });
  reveal();
  document.addEventListener('click', function(e){
    var el = e.target.closest ? e.target.closest('.long') : null;
    if (el && !window.getSelection().toString()) el.classList.toggle('open');
  });
  // A Finding as text, for a ticket or a mail. The clipboard of a page opened from a
  // file is not everywhere to be had, so there is the old way behind the new one.
  function put(text, done){
    function old(){
      var t = document.createElement('textarea'); t.value = text; t.style.position = 'fixed'; t.style.opacity = '0';
      document.body.appendChild(t); t.select();
      try { if (document.execCommand('copy')) done(); } catch (x) {}
      document.body.removeChild(t);
    }
    if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, old); else old();
  }
  // A table of the evidence as it is shown - filtered, searched - with its title and its
  // headings, a tab between the cells: pasted into Excel it is a table, into a ticket it
  // is lines. Text that is no table is copied as it stands.
  function tableText(b){
    var d = b.closest('details:not(.help)'), head = d ? d.querySelector('summary b') : null;
    var lines = [head ? head.textContent.trim() : ''];
    var t = d ? d.querySelector('table.data') : null;
    var cell = function(c){ return c.textContent.replace(/\s+/g, ' ').trim(); };
    if (t) {
      lines.push(Array.prototype.map.call(t.tHead.rows[0].cells, cell).join('\t'));
      Array.prototype.forEach.call(t.tBodies[0].rows, function(r){
        if (r.classList.contains('grp') || r.classList.contains('sx') || r.style.display === 'none') return;
        lines.push(Array.prototype.map.call(r.cells, cell).join('\t'));
      });
    } else {
      var pre = d ? d.querySelector('.evi pre') : null;
      if (pre) lines.push(pre.textContent);
    }
    return lines.join('\n');
  }
  var doneSign = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
  document.addEventListener('click', function(e){
    var b = e.target.closest ? e.target.closest('button.copy') : null;
    if (!b) return;
    // In the head of a fold, and no click on the fold.
    e.preventDefault(); e.stopPropagation();
    var sign = b.innerHTML, title = b.title;
    put(b.classList.contains('tcopy') ? tableText(b) : (b.getAttribute('data-copy') || ''), function(){
      b.innerHTML = doneSign; b.title = b.getAttribute('data-done') || title; b.classList.add('done');
      setTimeout(function(){ b.innerHTML = sign; b.title = title; b.classList.remove('done'); }, 1500);
    });
  });
  var copySign = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>';
  var fullIcon = {
    open: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>',
    shut: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>'
  };
  // What a search or a filter of the Report understands. Words are all wanted; ODER or |
  // between two is either; -word, !word or NICHT word is not wanted; ( ) groups; "in
  // quotes" is one; Spalte:Wert looks in that column of a table only (Pruefung:,
  // Anwendung:, Bereich: on a Finding); FAIL WARN INFO OK in capitals is a state. The
  // words of the language are in capitals, so that "oder" is still a word to find. Case
  // and umlauts do not count: "domane" finds "Domaene" as written with one.
  var Q = (function(){
    // Folded letter for letter, so that a place in the folded text is the same place in the text.
    function fold(s){
      return (s || '').toLowerCase().replace(/[\u00e0-\u00e5]/g, 'a').replace(/[\u00f2-\u00f6]/g, 'o').replace(/[\u00f9-\u00fc]/g, 'u')
        .replace(/[\u00e8-\u00eb]/g, 'e').replace(/\u00df/g, 's').replace(/\u00a0/g, ' ');
    }
    function wrap(neg, node){ return neg ? { t: 'not', a: node } : node; }
    function tokens(v){
      var out = [], i = 0, n = v.length;
      while (i < n) {
        var c = v.charAt(i);
        if (/\s/.test(c)) { i++; continue; }
        if (c === '(' || c === ')' || c === '|') { out.push({ k: c }); i++; continue; }
        var neg = false;
        if ((c === '-' || c === '!') && i + 1 < n && !/\s/.test(v.charAt(i + 1))) {
          neg = true; i++;
          if (v.charAt(i) === '(') { out.push({ k: 'not' }); continue; }
        }
        // A column: a name with a letter in it, of two letters or more - "C:\" is a drive,
        // a time of day such as 13:35, "https:" an address, "fe80::1" an address.
        var field = '', m = /^([^\s"():|]+):(?=[^\s:])/.exec(v.slice(i));
        if (m && m[1].length > 1 && /[A-Za-z\u00c0-\u024f]/.test(m[1]) && !/^(https?|ftp|file|ldaps?|smb)$/i.test(m[1])) { field = m[1]; i += m[0].length; }
        var word = '', quoted = false;
        if (v.charAt(i) === '"') {
          var e = v.indexOf('"', i + 1); if (e < 0) e = n;
          word = v.slice(i + 1, e); i = e + 1; quoted = true;
        } else {
          var s = i; while (i < n && !/[\s()|]/.test(v.charAt(i))) i++;
          word = v.slice(s, i);
        }
        if (!quoted && !field && !neg) {
          if (word === 'ODER' || word === 'OR') { out.push({ k: '|' }); continue; }
          if (word === 'UND' || word === 'AND' || word === '&') continue;
          if (word === 'NICHT' || word === 'NOT') { out.push({ k: 'not' }); continue; }
        }
        if (!quoted && !field && levels.indexOf(word) >= 0) { out.push({ k: 't', node: wrap(neg, { t: 'level', l: word }) }); continue; }
        word = fold(word);
        if (!word.trim()) continue;
        out.push({ k: 't', node: wrap(neg, field ? { t: 'field', f: fold(field), w: word } : { t: 'word', w: word }) });
      }
      return out;
    }
    function parse(v){
      var t = tokens(v || ''), p = 0;
      function one(list, kind){ list = list.filter(Boolean); return list.length > 1 ? { t: kind, a: list } : (list[0] || null); }
      function either(){ var a = [all()]; while (p < t.length && t[p].k === '|') { p++; a.push(all()); } return one(a, 'or'); }
      function all(){ var a = []; while (p < t.length && t[p].k !== '|' && t[p].k !== ')') a.push(unary()); return one(a, 'and'); }
      function unary(){
        var k = t[p]; if (!k) return null; p++;
        if (k.k === 'not') { var x = unary(); return x ? { t: 'not', a: x } : null; }
        if (k.k === '(') { var e = either(); if (t[p] && t[p].k === ')') p++; return e; }
        return k.node || null;
      }
      var root = either();
      // A ")" too many is passed over, and what follows it still counts.
      while (p < t.length) { p++; var more = either(); if (more) root = root ? { t: 'and', a: [root, more] } : more; }
      return root;
    }
    // Whether an item is wanted. An item says what it is as text, and may say whether it
    // is in a state and what is in a column of it; where it cannot say, the column's word
    // is looked for in its whole text.
    function test(node, item){
      if (!node) return true;
      if (node.t === 'and') return node.a.every(function(x){ return test(x, item); });
      if (node.t === 'or') return node.a.some(function(x){ return test(x, item); });
      if (node.t === 'not') return !test(node.a, item);
      if (node.t === 'level') return item.level ? !!item.level(node.l) : false;
      if (node.t === 'field' && item.field) { var r = item.field(node.f, node.w); if (r !== null && r !== undefined) return !!r; }
      return item.text.indexOf(node.w) >= 0;
    }
    // The words to mark: those that are wanted, and not those that are not.
    function words(node, out, neg){
      out = out || [];
      if (!node) return out;
      if (node.t === 'not') return words(node.a, out, !neg);
      if (node.t === 'and' || node.t === 'or') { node.a.forEach(function(x){ words(x, out, neg); }); return out; }
      if (!neg && node.w && out.indexOf(node.w) < 0) out.push(node.w);
      return out;
    }
    // Whether a query asks for any word at all: one for a state alone is about the Findings.
    function hasWords(node){
      if (!node) return false;
      if (node.t === 'not') return hasWords(node.a);
      if (node.t === 'and' || node.t === 'or') return node.a.some(hasWords);
      return node.t !== 'level';
    }
    // A column of a table by the start of its heading or any part of it.
    function column(heads, f){
      for (var i = 0; i < heads.length; i++) { if (heads[i].indexOf(f) === 0) return i; }
      for (var j = 0; j < heads.length; j++) { if (heads[j].indexOf(f) >= 0) return j; }
      return -1;
    }
    return { fold: fold, parse: parse, test: test, words: words, hasWords: hasWords, column: column };
  })();
  // A table in the order of any column, and narrowed to the rows that hold a word.
  // What a cell is sorted as: a number or a date where every cell of the column is
  // one (as German writes them, and as the Run's own stamps are), else its text.
  function sortKey(s){
    s = (s || '').replace(/\u00a0/g, ' ').trim();
    if (!s) return null;
    var m = s.match(/^(\d{2})\.(\d{2})\.(\d{4})(?:,?\s+(\d{2}):(\d{2})(?::(\d{2}))?)?$/);
    if (m) return new Date(+m[3], m[2] - 1, +m[1], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)).getTime();
    m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
    if (m) return new Date(+m[1], m[2] - 1, +m[3], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)).getTime();
    var t = s.replace(/\s?(%|ms|s|KB|MB|GB|TB|Bit)$/, '');
    if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(t)) return parseFloat(t.replace(/\./g, '').replace(',', '.'));
    if (/^-?\d+([.,]\d+)?$/.test(t)) return parseFloat(t.replace(',', '.'));
    return NaN;
  }
  function rowItem(table, r){
    if (r._ft === undefined) r._ft = Q.fold(r.textContent);
    return { text: r._ft, field: function(f, w){
      var c = Q.column(table._heads || [], f);
      if (c < 0) return null;
      var cell = r.cells[c];
      return Q.fold(cell ? cell.textContent : '').indexOf(w) >= 0;
    } };
  }
  each('table.data', function(table){
    var body = table.tBodies[0], head = table.tHead;
    if (!body || !head) return;
    var rows = Array.prototype.slice.call(body.rows);
    rows.forEach(function(r, i){ r._i = i; });
    var heads = Array.prototype.slice.call(head.rows[0].cells);
    heads.forEach(function(th, col){
      th.title = table.getAttribute('data-sort') || '';
      th.addEventListener('click', function(){
        var dir = th.getAttribute('data-dir') === '1' ? -1 : (th.getAttribute('data-dir') === '-1' ? 0 : 1);
        heads.forEach(function(h){ h.removeAttribute('data-dir'); });
        var order = rows.slice();
        if (dir) {
          th.setAttribute('data-dir', dir);
          var keys = rows.map(function(r){ return sortKey(r.cells[col] ? r.cells[col].textContent : ''); });
          var numeric = keys.some(function(k){ return k !== null; }) && keys.every(function(k){ return k === null || !isNaN(k); });
          order.sort(function(a, b){
            var x = a.cells[col] ? a.cells[col].textContent.trim() : '', y = b.cells[col] ? b.cells[col].textContent.trim() : '';
            // An empty cell stands last, whichever way the column is sorted.
            if (!x || !y) return (x ? -1 : 0) + (y ? 1 : 0) || a._i - b._i;
            var c = numeric ? keys[a._i] - keys[b._i] : x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' });
            return c * dir || a._i - b._i;
          });
        }
        order.forEach(function(r){ body.appendChild(r); });
        regroup();
      });
    });
    // The headings, folded, for Spalte:Wert.
    var foldedHeads = heads.map(function(h){ return Q.fold(h.textContent.trim()); });
    table._heads = foldedHeads;
    // Rows by a column: its values, or by day or hour of a time, the network of an
    // address, the server of a path. Each group a line of its own that folds what is
    // under it, with how many rows are in it; sorting sorts inside the groups, a filter
    // and a search count what is left of each.
    var groupBy = null, shut = {};
    function cellText(r, c){ var x = r.cells[c]; return x ? x.textContent.replace(/\u00a0/g, ' ').trim() : ''; }
    function two(n){ return (n < 10 ? '0' : '') + n; }
    // A date as German writes it or as the Run stamps it, by how it is written: a number is no date.
    function isTime(s){ return /^(\d{2}\.\d{2}\.\d{4}|\d{4}-\d{2}-\d{2})/.test(s); }
    function keyOf(r){
      var s = cellText(r, groupBy.col);
      if (!s) return '';
      if (groupBy.kind === 'day' || groupBy.kind === 'hour' || groupBy.kind === 'month') {
        var k = sortKey(s);
        if (isTime(s) && typeof k === 'number' && !isNaN(k)) {
          var d = new Date(k), day = two(d.getDate()) + '.' + two(d.getMonth() + 1) + '.' + d.getFullYear();
          if (groupBy.kind === 'month') return two(d.getMonth() + 1) + '.' + d.getFullYear();
          return groupBy.kind === 'day' ? day : day + ' ' + two(d.getHours()) + ':00';
        }
      }
      if (groupBy.kind === 'net') { var m = s.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}/); if (m) return m[1] + '.0/24'; }
      if (groupBy.kind === 'server') { var u = s.match(/^\\\\([^\\]+)/); if (u) return '\\\\' + u[1]; }
      return s;
    }
    function regroup(){
      Array.prototype.forEach.call(body.querySelectorAll('tr.grp'), function(g){ body.removeChild(g); });
      rows.forEach(function(r){ r.classList.remove('gx', 'gin', 'galt'); });
      if (!groupBy) return;
      var groups = [], byKey = {};
      Array.prototype.forEach.call(body.rows, function(r){
        // "INTEL" and "Intel" are one group, under the first way it is written.
        var label = keyOf(r), k = Q.fold(label).trim();
        if (!Object.prototype.hasOwnProperty.call(byKey, k)) { byKey[k] = { k: k, label: label, rows: [], at: null }; groups.push(byKey[k]); }
        byKey[k].rows.push(r);
        var t = sortKey(cellText(r, groupBy.col));
        if (typeof t === 'number' && !isNaN(t) && (byKey[k].at === null || t > byKey[k].at)) byKey[k].at = t;
      });
      // A time newest first; anything else the largest group first.
      if (groupBy.kind === 'day' || groupBy.kind === 'hour' || groupBy.kind === 'month') groups.sort(function(a, b){ return (b.at || 0) - (a.at || 0); });
      else groups.sort(function(a, b){ return b.rows.length - a.rows.length || a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: 'base' }); });
      // Every row of a group carries its mark, and every second group a tint: which rows
      // belong together is seen in the table, and not only in the line over them.
      groups.forEach(function(g, gi){
        var alt = gi % 2 === 1;
        var tr = document.createElement('tr'); tr.className = alt ? 'grp galt' : 'grp';
        var td = document.createElement('td'); td.colSpan = heads.length;
        var name = document.createElement('span'); name.className = 'gk'; name.textContent = g.label || table.getAttribute('data-gempty') || '';
        var left = g.rows.filter(function(r){ return r.style.display !== 'none' && !r.classList.contains('sx'); }).length;
        var n = document.createElement('span'); n.className = 'gn';
        n.textContent = (table.getAttribute('data-grows') || '{0}').replace('{0}', left);
        td.appendChild(name); td.appendChild(n); tr.appendChild(td);
        tr.hidden = !left; tr._key = g.k;
        if (shut[g.k]) tr.classList.add('shut');
        body.appendChild(tr);
        g.rows.forEach(function(r){ r.classList.add('gin'); if (alt) r.classList.add('galt'); if (shut[g.k]) r.classList.add('gx'); body.appendChild(r); });
      });
    }
    table._regroup = regroup;
    body.addEventListener('click', function(e){
      var g = e.target.closest ? e.target.closest('tr.grp') : null;
      if (!g) return;
      shut[g._key] = !shut[g._key];
      regroup();
    });
    var box = table.parentNode && table.parentNode.classList && table.parentNode.classList.contains('evi') ? table.parentNode : table;
    var bar = document.createElement('div'); bar.className = 'tbar';
    box.parentNode.insertBefore(bar, box);
    // A filter and the groups for a table long enough to want them.
    if (rows.length >= 8) {
      var input = document.createElement('input'); input.type = 'search'; input.className = 'search tfilter';
      input.placeholder = table.getAttribute('data-filter') || '';
      input.title = table.getAttribute('data-filterhelp') || '';
      var count = document.createElement('span'); count.className = 'tcount';
      bar.appendChild(input); bar.appendChild(count); bar.classList.add('hasfilter');
      input.addEventListener('input', function(){
        var q = Q.parse(input.value), shown = 0;
        rows.forEach(function(r){
          var keep = Q.test(q, rowItem(table, r));
          r.style.display = keep ? '' : 'none';
          if (keep) shown++;
        });
        count.textContent = q ? (table.getAttribute('data-shown') || '').replace('{0}', shown).replace('{1}', rows.length) : '';
        regroup();
      });
      // What a column can be grouped by: its values where they repeat, and a time by day
      // and by hour, an address by its network, a path by its server.
      var choose = document.createElement('select'); choose.className = 'tgroup';
      choose.title = table.getAttribute('data-group') || ''; choose.setAttribute('aria-label', choose.title);
      var none = document.createElement('option'); none.value = ''; none.textContent = table.getAttribute('data-gnone') || '';
      choose.appendChild(none);
      var add = function(value, label){ var o = document.createElement('option'); o.value = value; o.textContent = label; choose.appendChild(o); };
      heads.forEach(function(th, c){
        var name = th.textContent.trim(), values = rows.map(function(r){ return cellText(r, c); }).filter(Boolean);
        if (!values.length) return;
        var distinct = {}; values.forEach(function(x){ distinct[x] = 1; });
        var every = function(re){ return values.every(function(x){ return re.test(x); }); };
        var times = values.every(function(x){ var k = sortKey(x); return isTime(x) && typeof k === 'number' && !isNaN(k); });
        var hours = times && values.some(function(x){ return /\d{1,2}:\d{2}/.test(x); });
        if (Object.keys(distinct).length < values.length) add('value:' + c, name);
        if (times) add('month:' + c, (table.getAttribute('data-gmonth') || '{0}').replace('{0}', name));
        if (times) add('day:' + c, (table.getAttribute('data-gday') || '{0}').replace('{0}', name));
        if (hours) add('hour:' + c, (table.getAttribute('data-ghour') || '{0}').replace('{0}', name));
        if (every(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) add('net:' + c, (table.getAttribute('data-gnet') || '{0}').replace('{0}', name));
        if (every(/^\\\\[^\\]+/)) add('server:' + c, (table.getAttribute('data-gserver') || '{0}').replace('{0}', name));
      });
      if (choose.options.length > 1) {
        choose.addEventListener('change', function(){
          var v = choose.value; shut = {};
          groupBy = v ? { kind: v.split(':')[0], col: +v.split(':')[1] } : null;
          regroup();
        });
        bar.appendChild(choose);
      }
    }
    // Every table of the evidence can be shown over the whole window, from a sign in its
    // head; the Report beside it keeps its width. On every table, and not only where one
    // was measured too wide: a table is measured only once it is open and shown, and the
    // ones that needed it most were the ones without the sign.
    var sec = box !== table ? table.closest('details:not(.help)') : null, head = sec ? sec.querySelector('summary') : null;
    if (head && head.parentNode === sec) {
      var wide = document.createElement('button'); wide.type = 'button'; wide.className = 'wide';
      wide.innerHTML = fullIcon.open; wide.title = table.getAttribute('data-wide') || ''; wide.setAttribute('aria-label', wide.title);
      // Behind the chip of the rights, and so before the room kept for the help's "?",
      // which stands over the end of the head.
      var chip = head.querySelector('.priv, .chip');
      head.insertBefore(wide, chip ? chip.nextSibling : null);
      var copy = document.createElement('button'); copy.type = 'button'; copy.className = 'copy tcopy';
      copy.innerHTML = copySign; copy.title = table.getAttribute('data-tcopy') || ''; copy.setAttribute('aria-label', copy.title);
      copy.setAttribute('data-done', table.getAttribute('data-tcopied') || '');
      head.insertBefore(copy, wide);
    }
  });
  // The help of a Section opened from a "?" in its head, the first of its buttons and in
  // their line. Without the script the help keeps its own "?", which stands over the end
  // of the head where no button of the script is.
  each('#evidence details:not(.help)', function(d){
    var help = null, head = null;
    Array.prototype.forEach.call(d.children, function(c){
      if (c.tagName === 'SUMMARY') head = c;
      else if (c.tagName === 'DETAILS' && c.classList.contains('help')) help = c;
    });
    if (!help || !head) return;
    var own = help.querySelector('summary');
    var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'hbtn'; btn.textContent = '?';
    btn.title = own ? own.getAttribute('title') || '' : ''; btn.setAttribute('aria-label', btn.title);
    btn.setAttribute('aria-expanded', String(help.open));
    var first = head.querySelector('.priv, .chip, .tcopy, .wide');
    if (first) head.insertBefore(btn, first); else head.appendChild(btn);
    btn._help = help;
  });
  document.addEventListener('click', function(e){
    var b = e.target.closest ? e.target.closest('summary .hbtn') : null;
    if (!b) return;
    // In the head of the fold, and no click on the fold.
    e.preventDefault(); e.stopPropagation();
    var d = b.closest('details:not(.help)');
    if (d && !d.open) d.open = true;
    b._help.open = !b._help.open;
    b.setAttribute('aria-expanded', String(b._help.open));
  }, true);
  var full = null;
  function closeFull(){
    if (!full) return;
    full.classList.remove('fullview'); document.body.classList.remove('has-full');
    var b = full.querySelector('summary .wide'), t = full.querySelector('table.data');
    if (b && t) { b.innerHTML = fullIcon.open; b.title = t.getAttribute('data-wide') || ''; b.setAttribute('aria-label', b.title); }
    var was = full; full = null; was.scrollIntoView({ block: 'nearest' });
  }
  document.addEventListener('click', function(e){
    var b = e.target.closest ? e.target.closest('summary .wide') : null;
    if (b) {
      // In the head of the fold, and no click on the fold: the table stays open.
      e.preventDefault(); e.stopPropagation();
      var d = b.closest('details:not(.help)'), t = d ? d.querySelector('table.data') : null;
      if (!d) return;
      if (full === d) { closeFull(); return; }
      closeFull();
      full = d; d.open = true; d.classList.add('fullview'); document.body.classList.add('has-full');
      if (t) { b.innerHTML = fullIcon.shut; b.title = t.getAttribute('data-narrow') || ''; b.setAttribute('aria-label', b.title); }
      return;
    }
    // A click beside the table, on the dimmed Report, closes it.
    if (full && e.target.closest && !e.target.closest('details.fullview')) closeFull();
  }, true);
  document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && full) { e.stopImmediatePropagation(); closeFull(); } }, true);
  // A search in the part of the Report that is shown - an area, an application, the
  // hints, every Check, what stands out, the evidence or the terms - and through all of
  // it where asked, or from the overview. Words are all wanted, "in quotes" is one,
  // -word is not wanted, FAIL WARN INFO OK in capitals narrow the Findings to that
  // state. Case and umlauts do not count: "domane" finds "Domaene" as written with one.
  var finder = document.getElementById('finder');
  if (finder && nav) {
    var fcount = document.getElementById('fcount'), fscope = document.getElementById('fscope'), fwide = document.getElementById('fwide');
    var hits = [], at = -1, opened = [], shut = [], from = null, everywhere = false, timer = null;
    var fold = Q.fold; function textOf(el, part){ if (el._fs === undefined) el._fs = fold(part ? part() : el.textContent); return el._fs; }
    // A Finding's line by its columns: what it is called, what it found, its application,
    // its area.
    function rowField(r){
      return function(f, w){
        var b = r.querySelector('.what b'), v = r.querySelector('.val'), g = r.closest('details.group'), h = g ? g.querySelector('summary b') : null;
        var by = { prufung: b, befund: b, check: b, wert: v, value: v, bereich: h, area: h };
        if (f === 'anwendung' || f === 'app') return fold(r.getAttribute('data-app') || '').indexOf(w) >= 0;
        for (var k in by) { if (k.indexOf(f) === 0) return fold(by[k] ? by[k].textContent : '').indexOf(w) >= 0; }
        return null;
      };
    }
    function rowText(r){
      var g = r.closest('details.group'), b = r.querySelector('.what b'), v = r.querySelector('.val'), h = g ? g.querySelector('summary b') : null;
      return [b ? b.textContent : '', v ? v.textContent : '', r.getAttribute('data-app') || '', h ? h.textContent : ''].join(' ');
    }
    function nameOf(pick){
      var e = nav.querySelector('[data-pick="' + pick.replace(/"/g, '\\"') + '"] .nm');
      return e ? e.textContent : pick;
    }
    // Where a search looks: the part shown, or everything. The overview is no part to search in.
    function home(){ var s = from !== null ? from : view; return everywhere || s === 'overview' || s === 'network' || s === 'find' ? null : s; }
    function say(){
      var s = home();
      fscope.textContent = s ? fscope.getAttribute('data-in').replace('{0}', nameOf(s)) : fscope.getAttribute('data-all');
      var base = from !== null ? from : view, scoped = base !== 'overview' && base !== 'find';
      fwide.hidden = !finder.value.trim() || !scoped;
      fwide.textContent = everywhere ? fwide.getAttribute('data-back').replace('{0}', nameOf(base)) : fwide.getAttribute('data-all');
    }
    viewShown = function(next){ if (next !== 'find' && from === null) say(); };
    function reset(){
      hits.forEach(function(m){ var p = m.parentNode; if (p) { p.replaceChild(document.createTextNode(m.textContent), m); p.normalize(); } });
      hits = []; at = -1;
      opened.forEach(function(r){ r.classList.remove('open'); }); opened = [];
      shut.forEach(function(d){ d.open = false; }); shut = [];
      each('.sx', function(el){ el.classList.remove('sx'); });
      each('table.data', function(tb){ if (tb._regroup) tb._regroup(); });
      root.classList.remove('finding');
    }
    stopFind = function(){ if (!finder.value && from === null) return; finder.value = ''; reset(); from = null; everywhere = false; fcount.textContent = ''; say(); };
    findIn = function(pick){
      if (!finder.value.trim()) return false;
      reset(); from = pick; everywhere = false; run(); return true;
    };
    function mark(node, words){
      if (!node || hits.length >= 2000) return;
      var walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false), list = [], n;
      while ((n = walk.nextNode())) { if (!(n.parentNode.closest && n.parentNode.closest('button,mark'))) list.push(n); }
      list.forEach(function(t){
        var s = fold(t.nodeValue), cuts = [];
        words.forEach(function(w){ var i = 0; while ((i = s.indexOf(w, i)) >= 0) { cuts.push([i, i + w.length]); i += w.length; } });
        if (!cuts.length || hits.length >= 2000) return;
        cuts.sort(function(a, b){ return a[0] - b[0]; });
        var joined = [];
        cuts.forEach(function(c){ var l = joined[joined.length - 1]; if (l && c[0] <= l[1]) l[1] = Math.max(l[1], c[1]); else joined.push(c.slice()); });
        var text = t.nodeValue, frag = document.createDocumentFragment(), pos = 0;
        joined.forEach(function(c){
          frag.appendChild(document.createTextNode(text.slice(pos, c[0])));
          var m = document.createElement('mark'); m.className = 'hit'; m.textContent = text.slice(c[0], c[1]);
          frag.appendChild(m); hits.push(m); pos = c[1];
        });
        frag.appendChild(document.createTextNode(text.slice(pos)));
        t.parentNode.replaceChild(frag, t);
      });
    }
    // Whether a Finding's line is in the part searched.
    function inPart(r, s){
      if (s === null || s === 'all') return true;
      var app = r.getAttribute('data-app') || '', g = r.closest('details.group');
      if (s === 'info') return r.classList.contains('INFO');
      if (s.indexOf('cat:') === 0) return !app && !!g && g.getAttribute('data-cat') === s.slice(4);
      if (s.indexOf('app:') === 0) return app === s.slice(4);
      return false;
    }
    function run(){
      var v = finder.value.trim();
      reset();
      if (!v) { fcount.textContent = ''; if (from !== null) { var back = from; from = null; everywhere = false; show(back); } say(); return; }
      if (from === null) from = view === 'find' ? 'all' : view;
      var q = Q.parse(v), s = home(), parts = [], n = 0, wanted = Q.words(q), anyWord = Q.hasWords(q);
      var state = function(el){ return function(l){ return el.classList.contains(l); }; };
      // A hit only in what a closed line or card hides opens it.
      var reveal = function(el, seenText){
        if (wanted.length && !wanted.some(function(w){ return seenText.indexOf(w) >= 0; }) && !el.classList.contains('open')) { el.classList.add('open'); opened.push(el); }
      };
      root.classList.add('finding');
      var words = wanted.slice().sort(function(a, b){ return b.length - a.length; });
      if (s === 'action') {
        // What stands out is searched as it stands there: card by card.
        each('#ch-action .card', function(c){
          if (!Q.test(q, { text: textOf(c), level: state(c) })) { c.classList.add('sx'); return; }
          n++;
          var h = c.querySelector('h3'), first = c.querySelector('.parts>li');
          reveal(c, fold((h ? h.textContent : '') + ' ' + (first ? first.textContent : '')));
        });
        show('action');
        var lc = chapter('leads'); if (lc) lc.hidden = true;
        if (words.length) each('#ch-action .card:not(.sx)', function(c){ mark(c, words); });
        parts.push(fcount.getAttribute('data-rows').replace('{0}', n));
      } else {
        var rowsN = 0, secN = 0, termN = 0, rowsToo = s === null || s === 'all' || s === 'info' || s.indexOf('cat:') === 0 || s.indexOf('app:') === 0;
        each('#checks .row', function(r){
          if (!(rowsToo && inPart(r, s) && Q.test(q, { text: textOf(r, function(){ return rowText(r); }), level: state(r), field: rowField(r) }))) { r.classList.add('sx'); return; }
          rowsN++;
          var b = r.querySelector('.what b'), first = r.querySelector('.val>div');
          reveal(r, fold((b ? b.textContent : '') + ' ' + (first ? first.textContent : '')));
        });
        show('find');
        // The evidence: a table whose title is wanted stands whole, any other with the rows that are.
        // A search for a state alone is a search among the Findings.
        var evidenceToo = (s === null || s === 'evidence') && anyWord, termsToo = (s === null || s === 'glossary') && anyWord;
        each('#evidence details:not(.help)', function(d){
          var any = false;
          if (evidenceToo) {
            if (Q.test(q, { text: fold(d.getAttribute('data-title') || '') })) any = true;
            else {
              var tb = d.querySelector('table.data'), trs = d.querySelectorAll('tbody tr:not(.grp)');
              Array.prototype.forEach.call(trs, function(tr){ if (Q.test(q, tb ? rowItem(tb, tr) : { text: textOf(tr) })) any = true; else tr.classList.add('sx'); });
              if (!trs.length && Q.test(q, { text: textOf(d) })) any = true;
            }
          }
          if (!any) { d.classList.add('sx'); return; }
          secN++;
          if (!d.open) { d.open = true; shut.push(d); }
        });
        each('#evidence .kind', function(k){ if (!k.querySelector('details:not(.help):not(.sx)')) k.classList.add('sx'); });
        each('#evidence .egroup', function(g){ if (!g.querySelector('.kind:not(.sx)')) g.classList.add('sx'); });
        each('#ch-glossary .glossary>div', function(e){ if (termsToo && Q.test(q, { text: textOf(e) })) termN++; else e.classList.add('sx'); });
        each('table.data', function(tb){ if (tb._regroup) tb._regroup(); });
        var c;
        if ((c = chapter('checks'))) c.hidden = !rowsN;
        if ((c = chapter('evidence'))) c.hidden = !secN;
        if ((c = chapter('glossary'))) c.hidden = !termN;
        if (words.length) {
          each('#checks .row:not(.sx)', function(r){ mark(r.querySelector('.what b'), words); mark(r.querySelector('.val'), words); });
          each('#evidence details:not(.help):not(.sx)', function(d){
            mark(d.querySelector('summary'), words);
            Array.prototype.forEach.call(d.querySelectorAll('tbody tr:not(.sx):not(.grp)'), function(tr){ mark(tr, words); });
          });
          each('#ch-glossary .glossary>div:not(.sx)', function(e){ mark(e, words); });
        }
        if (rowsToo) parts.push(fcount.getAttribute('data-rows').replace('{0}', rowsN));
        if (s === null || s === 'evidence') parts.push(fcount.getAttribute('data-secs').replace('{0}', secN));
        if (s === null || s === 'glossary') parts.push(fcount.getAttribute('data-terms').replace('{0}', termN));
        n = rowsN + secN + termN;
      }
      // The part searched is the part picked in the tree.
      each('#nav [data-pick]', function(b){ b.setAttribute('aria-current', String(!everywhere && b.getAttribute('data-pick') === from)); });
      fcount.textContent = n ? parts.join(' \u00b7 ') + (hits.length ? ' \u00b7 ' + fcount.getAttribute('data-hits').replace('{0}', hits.length) : '') : fcount.getAttribute('data-none');
      say();
      window.scrollTo(0, 0);
    }
    // From one place found to the next, opening what hides it.
    function go(step){
      if (!hits.length) return;
      if (at >= 0 && hits[at]) hits[at].classList.remove('now');
      at = (at + step + hits.length) % hits.length;
      var m = hits[at], r = m.closest('.row') || m.closest('.card');
      if (r && !r.classList.contains('open')) { r.classList.add('open'); opened.push(r); }
      var long = m.closest('.long'); if (long) long.classList.add('open');
      m.classList.add('now');
      m.scrollIntoView({ block: 'center' });
      fcount.textContent = fcount.getAttribute('data-at').replace('{0}', at + 1).replace('{1}', hits.length);
    }
    fwide.addEventListener('click', function(){ everywhere = !everywhere; run(); finder.focus(); });
    // The help of the search, opened by its "?" and closed by it, by Esc or by a click beside it.
    var fhelp = document.getElementById('fhelp'), fhelptext = document.getElementById('fhelptext');
    function helpOpen(on){ if (!fhelp || !fhelptext) return; fhelptext.hidden = !on; fhelp.setAttribute('aria-expanded', String(on)); }
    if (fhelp) fhelp.addEventListener('click', function(e){ e.stopPropagation(); helpOpen(fhelptext.hidden); });
    document.addEventListener('click', function(e){ if (fhelptext && !fhelptext.hidden && !(e.target.closest && e.target.closest('#fhelptext'))) helpOpen(false); });
    document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && fhelptext && !fhelptext.hidden) helpOpen(false); });
    finder.addEventListener('input', function(){ clearTimeout(timer); timer = setTimeout(run, 180); });
    finder.addEventListener('keydown', function(e){
      if (e.key === 'Enter') { e.preventDefault(); clearTimeout(timer); if (at < 0 && !hits.length) run(); go(e.shiftKey ? -1 : 1); }
      else if (e.key === 'Escape') { e.preventDefault(); finder.value = ''; run(); }
    });
    document.addEventListener('keydown', function(e){
      var t = e.target.tagName;
      if (e.key === '/' && t !== 'INPUT' && t !== 'TEXTAREA' && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); finder.focus(); finder.select(); }
    });
    say();
  }
  // A term of the glossary explained where it stands, on the pointer: the first time in
  // each Finding, card, suspicion and table, and not every time - a page of dotted
  // words is a page nobody reads. A table is done when it is opened: the evidence of a
  // large Report is megabytes, and most of it is never opened.
  var termData = document.getElementById('terms'), terms = [], termRe = null;
  try { terms = termData ? JSON.parse(termData.textContent) : []; } catch (x) { terms = []; }
  if (terms.length) {
    try {
      termRe = new RegExp('(?<![\\p{L}\\p{N}_])(?:' + terms.map(function(e){ return '(' + e.p + ')'; }).join('|') + ')(?![\\p{L}\\p{N}_])', 'gu');
    } catch (x) { termRe = null; }
  }
  function explain(box){
    if (!termRe || !box || box._terms) return;
    box._terms = true;
    var seen = {}, walk = document.createTreeWalker(box, NodeFilter.SHOW_TEXT, null, false), nodes = [], n;
    while ((n = walk.nextNode())) {
      var up = n.parentNode;
      if (up.closest && up.closest('button,abbr,mark,script,style,input,textarea,.helptext,details.help')) continue;
      nodes.push(n);
    }
    nodes.forEach(function(t){
      var s = t.nodeValue, m, cuts = [];
      termRe.lastIndex = 0;
      while ((m = termRe.exec(s))) {
        var k = -1;
        for (var i = 1; i < m.length; i++) { if (m[i] !== undefined) { k = i - 1; break; } }
        if (k < 0 || seen[k]) continue;
        seen[k] = true; cuts.push([m.index, m.index + m[0].length, k]);
      }
      if (!cuts.length) return;
      var frag = document.createDocumentFragment(), pos = 0;
      cuts.forEach(function(c){
        frag.appendChild(document.createTextNode(s.slice(pos, c[0])));
        var a = document.createElement('abbr'); a.className = 'term';
        a.title = terms[c[2]].t + ': ' + terms[c[2]].x;
        a.textContent = s.slice(c[0], c[1]);
        frag.appendChild(a); pos = c[1];
      });
      frag.appendChild(document.createTextNode(s.slice(pos)));
      t.parentNode.replaceChild(frag, t);
    });
  }
  if (termRe) {
    each('#checks .row,#ch-action .card,.leadbox,.leadgap,.runbox,.alert', explain);
    each('#evidence details[open]:not(.help)', explain);
    document.addEventListener('toggle', function(e){ var d = e.target; if (d && d.open && d.matches && d.matches('#evidence details:not(.help)')) explain(d); }, true);
  }
  // Paper has nothing to click: everything folded is printed open, and folded back after.
  // Not the help of a table: that is printed where the reader has opened it, and only there.
  // The whole Report, too: every chapter, and every Finding in full.
  var before = [];
  window.addEventListener('beforeprint', function(){
    stopFind();
    before = []; each('details:not(.help)', function(d){ before.push([d, d.open]); d.open = true; });
    if (nav) { root.classList.remove('js'); each('[data-chapter],#checks details.group,#checks .row,#checks .subject', function(el){ el.hidden = false; }); var vn = document.getElementById('viewnote'); if (vn) vn.hidden = true; }
  });
  window.addEventListener('afterprint', function(){
    before.forEach(function(p){ p[0].open = p[1]; });
    if (nav) { root.classList.add('js'); show(view); }
  });
})();
</script>
'@