{%- assign n_posts = site.posts | size -%}
{%- assign cats = site.posts | map: "category" | uniq -%}

<!-- ---- Hero ---- -->
<section class="hero" aria-label="Introduction">
  <div class="hero-copy">
    <div class="eyebrow">
      <p class="label">(issue) rev. <code>{{ site.revision }}</code></p>
      <p class="label">(built) {{ site.time | date: "%Y-%m-%d" }}</p>
      <p class="label">(status) under revision</p>
    </div>
    <h1 class="display hero-title"><span>null</span><span>iche</span></h1>
    <p class="hero-dek">An infosec and maths journal, written by <b>The Narrator</b></p>
    <div class="hero-math">
      <span class="label">(def.)</span>
      <span>$\text{nulliche} := S \in \mathcal{N} \text{ with } \mu(S) = 0$</span>
      <span class="label">— a niche nobody is paid to study</span>
    </div>
  </div>
  <figure class="plate hero-fig">
    <canvas id="fp" width="640" height="823" aria-label="A strange attractor generated from a hash of your browser's characteristics; unique to you, computed locally."></canvas>
    <noscript><img src="/assets/img/melencolia.webp" srcset="/assets/img/melencolia-480.webp 480w, /assets/img/melencolia-720.webp 720w, /assets/img/melencolia.webp 960w" sizes="(max-width: 60em) 26rem, 40vw" width="960" height="1234" alt="Melencolia I, Albrecht Dürer, 1514."></noscript>
    <figcaption><span>(fig. 1) de Jong attractor, seed <code id="fp-id">········</code></span><span>this picture is your unique fingerprint</span></figcaption>
  </figure>
</section>

<!-- ---- 01 Journal ---- -->
<section class="sec" id="journal" aria-labelledby="journal-h">
  <div class="sec-head">
    <h2 class="display sec-title" id="journal-h">Research/Posts</h2>
    <p class="label">(01) journal — {{ n_posts }} {% if n_posts == 1 %}entry{% else %}entries{% endif %}</p>
  </div>
  <div class="cats">
    <p class="label">category</p>
    {%- for c in cats -%}
    <a class="rgb" href="/{{ c }}/"><span class="n">{{ forloop.index }}:</span>{{ c }}</a>
    {%- endfor -%}
    <a class="rgb" href="/journal/"><span class="n">∀:</span>everything</a>
  </div>
  <div class="span-main">
    {%- assign recent = site.posts | slice: 0, 6 -%}
    {%- include entries.html posts=recent -%}
    <p class="rows-more"><a class="rgb" href="/journal/">→ the complete journal</a></p>
  </div>
</section>

<!-- ---- 02 Profile ---- -->
<section class="sec" id="profile" aria-labelledby="profile-h">
  <div class="sec-head">
    <h2 class="display sec-title" id="profile-h">Who</h2>
    <p class="label">(02) profile</p>
  </div>
  <div class="profile-copy">
    <p><b>nulliche</b> (<i>null-eesh</i>: null + niche) is the working journal of a novice researcher. It records attempts: taking a construction from mathematics, bending it until it does something inside a computer it was never meant to do, and writing down honestly how well that went.</p>
    <p>Every article is written by hand. No language model is consulted, prompted or tolerated in the writing. There are no sponsors, no analytics, and no ambition beyond being read by the right people at the wrong hours.</p>
  </div>
  <dl class="spec-table">
    <div><dt>field</dt><dd>maths · computer science · security</dd></div>
    <div><dt>rank</dt><dd>novice researcher, n = 1</dd></div>
    <div><dt>methods</dt><dd>reverse engineering, exploit dev, signal theory, digital anthropology</dd></div>
    <div><dt>tooling</dt><dd>a shell, a text editor, a git forge</dd></div>
    <div><dt>ai in writing</dt><dd>none</dd></div>
    <div><dt>source</dt><dd><a href="https://git.nonaxiomatic.systems/Narrator" rel="noopener">git.nonaxiomatic.systems</a></dd></div>
  </dl>
  <blockquote class="quote">
    <p>We are exercising the will of parasites. It is time to take the wheel.</p>
    <cite>— from <a href="/privacy/2025/12/29/A-totally-non-political-post-about-MACHINE-GOD.html">a totally non-political post about machine god</a></cite>
  </blockquote>
</section>

<!-- ---- 03 Contact ---- -->
<section class="sec" id="contact" aria-labelledby="contact-h">
  <div class="sec-head">
    <h2 class="display sec-title" id="contact-h">Write</h2>
    <p class="label">(03) contact — arguments welcome, they are more useful than praise</p>
  </div>
  <a class="contact-mail" href="mailto:narrator@nulliche.dev">narrator@nulliche.dev</a>
  <div class="contact-links">
    <a class="rgb" href="/keys/">public keys (age, ssh)</a>
    <a class="rgb" href="/support/">support the work</a>
    <a class="rgb" href="/feed.xml">rss</a>
    <a class="rgb" href="/.raw">this page as source</a>
  </div>
  <div class="contact-code">
    <span class="sigil sigil-lg" style="--sigil:url({{ "narrator@nulliche.dev" | attractor }})" aria-hidden="true"></span>
    <p class="label">(sigil) sha-256 of the address, as an attractor</p>
  </div>
</section>

<script>
(function () {
  'use strict';
  /* ---- Fig. 1: a strange attractor seeded by this browser's fingerprint ----
     The fingerprint is a handful of things the browser already exposes,
     hashed with SHA-256 here and used only to pick the attractor's four
     parameters. Nothing is stored or transmitted. */
  var canvas = document.getElementById('fp'), ctx = canvas.getContext('2d'), idEl = document.getElementById('fp-id');
  var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
  // Size the bitmap to the plate as displayed, so the dither is 1:1 with device
  // pixels and never resampled into moiré.
  (function () {
    var r = canvas.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 2);
    var w = Math.round((r.width || 640) * dpr), h = Math.round(w * 823 / 640);
    if (w > 100) { canvas.width = w; canvas.height = h; }
  })();
  var W = canvas.width, H = canvas.height;

  function fingerprint() {
    var n = navigator, s = screen, parts = [
      n.userAgent, n.language, (n.languages || []).join(','), n.platform || '', n.hardwareConcurrency || '', n.deviceMemory || '',
      s.width, s.height, s.colorDepth, devicePixelRatio, n.maxTouchPoints || 0,
      Intl.DateTimeFormat().resolvedOptions().timeZone || '', new Date().getTimezoneOffset()
    ];
    try {
      var c = document.createElement('canvas'); c.width = 160; c.height = 40; var x = c.getContext('2d');
      x.textBaseline = 'alphabetic'; x.font = '16px "Big Shoulders Display", sans-serif'; x.fillStyle = '#c8364a'; x.fillRect(10, 5, 60, 20);
      x.fillStyle = '#ece6d9'; x.fillText('nulliche ∮ ' + Math.PI, 2, 28); parts.push(c.toDataURL());
    } catch (e) {}
    try {
      var g = document.createElement('canvas').getContext('webgl'), d = g && g.getExtension('WEBGL_debug_renderer_info');
      if (d) parts.push(g.getParameter(d.UNMASKED_RENDERER_WEBGL));
    } catch (e) {}
    return parts.join('|');
  }
  function sha256(str) {
    return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function (buf) { return new Uint8Array(buf); });
  }
  function fallback(str) { // FNV-1a spread into 32 bytes if subtle crypto is unavailable
    var out = new Uint8Array(32), h = 2166136261;
    for (var i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
    for (var k = 0; k < 32; k++) { h ^= h >>> 13; h = Math.imul(h, 0x5bd1e995); h ^= h >>> 15; out[k] = h & 255; }
    return Promise.resolve(out);
  }

  function draw(bytes) {
    var hex = ''; for (var i = 0; i < 4; i++) hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);
    idEl.textContent = hex;
    // Four parameters in [-π, π] from 16-bit slices of the hash.
    var P = function (i) { return ((bytes[i] << 8 | bytes[i + 1]) / 65535 * 2 - 1) * Math.PI; };
    var a = P(4), b = P(6), c = P(8), d = P(10), attempt = 0;
    var wrap = function (v) { return ((v + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI; };
    function nudge() {
      var n = 0.61803398875 * (attempt + 1) + bytes[12 + (attempt & 7)] / 255; attempt++;
      a = wrap(a + n); b = wrap(b + n * 0.5); c = wrap(c - n); d = wrap(d - n * 0.5);
    }
    var bayer = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
    var img = ctx.createImageData(W, H), px = img.data;

    // Some parameter sets collapse to a point or a short cycle. Warm up,
    // measure how much of a coarse grid the orbit visits, and find its
    // bounding box so the form is fitted and centred on the plate.
    function warm() {
      for (var tries = 0; tries < 24; tries++, nudge()) {
        var seen = new Uint8Array(64 * 82), count = 0, wx = 0.1, wy = 0.1, ok = true;
        var minx = 9, maxx = -9, miny = 9, maxy = -9;
        for (var w = 0; w < 40000; w++) {
          var tx = Math.sin(a * wy) - Math.cos(b * wx), ty = Math.sin(c * wx) - Math.cos(d * wy);
          wx = tx; wy = ty;
          if (wx !== wx || wy !== wy) { ok = false; break; }
          if (w > 200) { if (wx < minx) minx = wx; if (wx > maxx) maxx = wx; if (wy < miny) miny = wy; if (wy > maxy) maxy = wy; }
          var gi = (((wy + 2.2) / 4.4 * 82) | 0) * 64 + (((wx + 2.2) / 4.4 * 64) | 0);
          if (gi >= 0 && gi < seen.length && !seen[gi]) { seen[gi] = 1; count++; }
        }
        if (ok && count > 420 && maxx - minx > 0.4 && maxy - miny > 0.4) return [minx, maxx, miny, maxy];
      }
      return [-2, 2, -2, 2];
    }

    function render() {
      var box = warm(), m = 0.08;
      var scale = Math.min(W * (1 - 2 * m) / (box[1] - box[0]), H * (1 - 2 * m) / (box[3] - box[2]));
      var cx = W / 2 - (box[0] + box[1]) / 2 * scale, cy = H / 2 - (box[2] + box[3]) / 2 * scale;
      var density = new Float32Array(W * H), x = 0.1, y = 0.1, maxd = 0;
      var TOTAL = 2600000, CHUNK = reduced ? TOTAL : 260000, done = 0;
      function step(n) {
        for (var i = 0; i < n; i++) {
          var nx = Math.sin(a * y) - Math.cos(b * x), ny = Math.sin(c * x) - Math.cos(d * y);
          x = nx; y = ny;
          var ix = (cx + x * scale) | 0, iy = (cy + y * scale) | 0;
          if (ix >= 0 && ix < W && iy >= 0 && iy < H) { var v = ++density[iy * W + ix]; if (v > maxd) maxd = v; }
        }
        done += n;
      }
      function paint() {
        // Tone-map the density logarithmically, then an ordered 4×4 dither to ink / no ink.
        var lm = Math.log(1 + maxd), gamma = 1.35, p = 0, inked = 0;
        for (var yy = 0; yy < H; yy++) for (var xx = 0; xx < W; xx++, p += 4) {
          var t = Math.pow(Math.log(1 + density[yy * W + xx]) / lm, 1 / gamma);
          var ink = t * 16 > bayer[(yy & 3) * 4 + (xx & 3)] + 0.5;
          if (ink) inked++;
          px[p] = px[p + 1] = px[p + 2] = ink ? 0 : 255; px[p + 3] = 255;
        }
        ctx.putImageData(img, 0, 0);
        return inked / (W * H);
      }
      (function frame() {
        step(Math.min(CHUNK, TOTAL - done));
        var cover = paint();
        if (done < TOTAL) requestAnimationFrame(frame);
        else if ((cover < 0.012 || cover > 0.6) && attempt < 40) { nudge(); render(); }   // dust or a solid block: try the next seed step
      })();
    }
    render();
  }

  var str = fingerprint();
  (crypto && crypto.subtle ? sha256(str).catch(function () { return fallback(str); }) : fallback(str)).then(draw);
})();
</script>
