← Fractal Explorer results

Fractal Explorer

Ornith 1.5 35B A3B · full

Initial view of Fractal Explorer

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 44,019 bytes · SHA-256 2df937649682
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fractal Explorer — Mandelbrot &amp; Julia</title>
<style>
  /* ============================================================
     GLOBAL / LAYOUT
     ============================================================ */
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #05060a; }
  body {
    font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
    color: #e8ecf4; user-select: none; -webkit-user-select: none;
  }
  #stage { position: fixed; inset: 0; }
  canvas#fractal { display: block; width: 100%; height: 100%; image-rendering: pixelated; }

  /* ============================================================
     GLASSY HUD
     ============================================================ */
  #hud {
    position: fixed; top: 14px; left: 14px; z-index: 20;
    padding: 12px 15px; min-width: 210px;
    background: rgba(14, 18, 28, 0.55);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid rgba(255,255,255,0.10);
    border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    font-size: 12px; line-height: 1.6;
    transition: opacity 0.4s ease, transform 0.4s ease;
    pointer-events: none;
  }
  #hud.hidden { opacity: 0; transform: translateY(-8px); }
  #hud .title { font-size: 12px; font-weight: 700; letter-spacing: 1px; color: #8fd3ff; text-transform: uppercase; margin-bottom: 6px; }
  #hud .row { display: flex; justify-content: space-between; gap: 16px; }
  #hud .k { color: #7a8699; }
  #hud .v { color: #eef3fb; font-variant-numeric: tabular-nums; }
  #hud .v.acc { color: #ffd28f; }
  #hud .fps { color: #7dffa6; }

  /* ============================================================
     CONTROL PANEL (glassy)
     ============================================================ */
  #panel {
    position: fixed; top: 14px; right: 14px; z-index: 20;
    width: 250px; max-height: calc(100vh - 28px); overflow-y: auto;
    padding: 14px 16px;
    background: rgba(14, 18, 28, 0.55);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid rgba(255,255,255,0.10);
    border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.45);
    font-size: 12px;
    transition: opacity 0.35s ease, transform 0.35s ease;
  }
  #panel.hidden { opacity: 0; transform: translateX(10px); pointer-events: none; }
  #panel h2 { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #8fd3ff; margin-bottom: 10px; }
  #panel .group { margin-bottom: 14px; }
  #panel label { display: block; color: #9aa6b8; margin-bottom: 4px; }
  #panel .val { color: #eef3fb; float: right; font-variant-numeric: tabular-nums; }
  input[type=range] { width: 100%; accent-color: #8fd3ff; cursor: pointer; }
  .row2 { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
  button.btn {
    background: rgba(255,255,255,0.07); color: #eef3fb; border: 1px solid rgba(255,255,255,0.14);
    padding: 6px 10px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 11px;
    transition: background 0.15s, border-color 0.15s;
  }
  button.btn:hover { background: rgba(143,211,255,0.18); border-color: rgba(143,211,255,0.4); }
  button.btn:active { transform: translateY(1px); }
  button.btn.active { background: rgba(143,211,255,0.28); border-color: #8fd3ff; color: #fff; }
  button.btn:disabled { opacity: 0.35; cursor: default; }

  /* Palette swatch chips */
  .swatches { display: flex; gap: 7px; flex-wrap: wrap; }
  .swatch {
    width: 34px; height: 22px; border-radius: 6px; cursor: pointer;
    border: 2px solid transparent; overflow: hidden; position: relative;
    transition: border-color 0.15s, transform 0.15s;
  }
  .swatch:hover { transform: translateY(-2px); }
  .swatch.active { border-color: #fff; box-shadow: 0 0 0 2px rgba(143,211,255,0.4); }

  /* Bookmarks */
  .bookmark { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left;
    background: rgba(255,255,255,0.05); color: #d7dfec; border: 1px solid rgba(255,255,255,0.10);
    padding: 5px 8px; border-radius: 7px; cursor: pointer; font-size: 11px; margin-bottom: 5px; font-family: inherit; }
  .bookmark:hover { background: rgba(143,211,255,0.15); }
  .bookmark .del { margin-left: auto; color: #7a8699; padding: 0 2px; }
  .bookmark .del:hover { color: #ff8f8f; }
  #bmName { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14); color: #eef3fb;
    padding: 5px 8px; border-radius: 7px; font-family: inherit; font-size: 11px; width: 110px; }

  /* ============================================================
     OVERLAYS: zoom box, crosshair, hint
     ============================================================ */
  #zoombox {
    position: fixed; z-index: 15; pointer-events: none; display: none;
    border: 1.5px solid rgba(255,255,255,0.9);
    box-shadow: inset 0 0 0 999px rgba(4,6,12,0.28);
    border-radius: 1px;
  }
  #crosshair { position: fixed; z-index: 14; pointer-events: none; display: none; }
  #crosshair .bar { position: absolute; background: rgba(143,211,255,0.7); }
  #crosshair .h { height: 1px; width: 26px; top: -0.5px; left: -13px; }
  #crosshair .v { width: 1px; height: 26px; left: -0.5px; top: -13px; }

  #hint {
    position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 12;
    padding: 8px 16px; font-size: 11.5px; color: #c6d0df;
    background: rgba(14,18,28,0.55); backdrop-filter: blur(10px);
    border: 1px solid rgba(255,255,255,0.08); border-radius: 20px;
    transition: opacity 0.5s ease; white-space: nowrap;
  }
  #hint kbd { display: inline-block; padding: 1px 6px; margin: 0 2px; border: 1px solid rgba(255,255,255,0.2);
    border-radius: 4px; background: rgba(255,255,255,0.06); font-size: 10px; color: #eef3fb; }
  #hint.hidden { opacity: 0; }

  /* Precision warning toast */
  #toast {
    position: fixed; bottom: 70px; left: 50%; transform: translateX(-50%) translateY(20px); z-index: 30;
    padding: 10px 18px; font-size: 12px; color: #ffe0b0;
    background: rgba(60,30,10,0.8); border: 1px solid rgba(255,180,80,0.4);
    border-radius: 10px; opacity: 0; pointer-events: none; transition: all 0.35s ease;
  }
  #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

  /* Toggle HUD button */
  #hudToggle {
    position: fixed; top: 14px; left: 14px; z-index: 25; width: 34px; height: 34px;
    display: none; align-items: center; justify-content: center;
    background: rgba(14,18,28,0.55); backdrop-filter: blur(10px);
    border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; color: #eef3fb; cursor: pointer; font-size: 16px;
  }
  body.hud-hidden #hud, body.hud-hidden #panel { opacity: 0; transform: translateY(-8px) translateX(10px); pointer-events: none; }
  body.hud-hidden #hudToggle { display: flex; }
</style>
</head>
<body>
  <div id="stage">
    <canvas id="fractal"></canvas>
  </div>

  <div id="hud">
    <div class="title">Fractal Explorer</div>
    <div class="row"><span class="k">Set</span><span class="v" id="hType">Mandelbrot</span></div>
    <div class="row"><span class="k">Magnification</span><span class="v acc" id="hMag">×1</span></div>
    <div class="row"><span class="k">Center Re</span><span class="v" id="hRe">0</span></div>
    <div class="row"><span class="k">Center Im</span><span class="v" id="hIm">0</span></div>
    <div class="row"><span class="k">Iterations</span><span class="v" id="hIter">256</span></div>
    <div class="row"><span class="k">FPS</span><span class="v fps" id="hFps">–</span></div>
  </div>

  <div id="hudToggle" role="button" tabindex="0" aria-label="Show controls">☰</div>

  <div id="panel">
    <h2>Controls</h2>

    <div class="group">
      <div class="row2">
        <button class="btn" id="btnMandel">Mandelbrot</button>
        <button class="btn" id="btnJulia">Julia</button>
      </div>
      <div class="row2">
        <label style="flex:1;display:flex;align-items:center;gap:6px;cursor:pointer;">
          <input type="checkbox" id="chkPreview"> <span>Preview Julia on hover</span>
        </label>
      </div>
    </div>

    <div class="group">
      <label>Palette</label>
      <div class="swatches" id="swatches"></div>
    </div>

    <div class="group">
      <label>Iterations <span class="val" id="iterVal">auto</span></label>
      <input type="range" id="iterSlider" min="0" max="10000" step="1" value="0">
    </div>
    <div class="group">
      <label>Cycle speed <span class="val" id="speedVal">0.25</span></label>
      <input type="range" id="speedSlider" min="0" max="2" step="0.01" value="0.25">
    </div>
    <div class="group">
      <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
        <input type="checkbox" id="chkAnim"> <span>Animate color drift</span>
      </label>
    </div>

    <div class="group">
      <div class="row2">
        <button class="btn" id="btnBack" disabled>◀ Back</button>
        <button class="btn" id="btnReset">⟲ Reset</button>
      </div>
    </div>

    <div class="group">
      <label>Bookmark</label>
      <div class="row2">
        <input id="bmName" placeholder="name…" aria-label="Bookmark name">
        <button class="btn" id="btnSave">Save</button>
      </div>
      <div id="bookmarkList"></div>
    </div>

    <div class="group" style="border-top:1px solid rgba(255,255,255,0.08);padding-top:10px;">
      <label style="opacity:.7">Keys</label>
      <div style="opacity:.6;font-size:11px;line-height:1.7">
        Drag: zoom box · Shift+Drag: pan · Wheel: zoom<br>
        Arrows: pan · +/- : zoom · J: Julia · H: hide · F: fps lock
      </div>
    </div>
  </div>

  <div id="zoombox"></div>
  <div id="crosshair"><div class="bar h"></div><div class="bar v"></div></div>
  <div id="hint">
    <kbd>Drag</kbd> zoom · <kbd>Shift+Drag</kbd> pan · <kbd>Wheel</kbd> zoom · <kbd>↑↓←→</kbd> pan · <kbd>+/-</kbd> zoom · <kbd>J</kbd> Julia · <kbd>H</kbd> hide
  </div>
  <div id="toast"></div>

<script>
"use strict";
/* ============================================================
   FRACTAL EXPLORER — vanilla JS Mandelbrot & Julia
   Sections:
     1. Tunable constants
     2. Palettes & LUT
     3. View state & mapping
     4. Iteration math (allocation-free) + smooth coloring
     5. Web-Worker based progressive renderer
     6. Navigation (rect-zoom, wheel, pan, history)
     7. Julia mode & live preview
     8. HUD, controls, keyboard
     9. Bookmarks (localStorage) + URL hash
     10. window.fractal debug interface
   ============================================================ */

/* -----------------------------------------------------------
   1. TUNABLE CONSTANTS
   ----------------------------------------------------------- */
const CFG = {
  FULL_SCALE: 3.0,            // complex height of the initial view (mag = FULL_SCALE/scale)
  MAX_MAGNIFICATION: 1e14,    // double-precision ceiling; zoom is blocked past this
  BAILOUT_R2: 4.0,            // |z|^2 escape threshold (escape radius = 2)
  BASE_ITER: 120,             // base iteration count
  ITER_SCALE: 26,             // iterations added per doubling of magnification
  MAX_ITER_CAP: 8192,         // hard cap on iterations
  COARSE_RES: 0.25,           // progressive coarse pass resolution (fraction of full)
  REFINE_DELAY: 80,           // ms before scheduling the full-res refine pass
  LUT_SIZE: 512,              // palette lookup table length
};
const NUM_WORKERS = Math.max(1, (navigator.hardwareConcurrency || 4));

/* -----------------------------------------------------------
   2. PALETTES  (stops at t in [0,1]; psychedelic wraps for cyclic)
   ----------------------------------------------------------- */
const PALETTES = {
  fire:        { cyclic: false, stops: [[0,[10,0,8]],[0.30,[210,40,18]],[0.62,[250,170,24]],[0.85,[255,235,90]],[1,[255,255,235]]] },
  ocean:       { cyclic: false, stops: [[0,[2,6,20]],[0.35,[10,60,150]],[0.65,[30,170,210]],[0.85,[150,230,245]],[1,[235,255,255]]] },
  mono:        { cyclic: false, stops: [[0,[0,0,0]],[0.5,[90,95,105]],[1,[250,252,255]]] },
  psychedelic: { cyclic: true,  stops: [[0,[255,0,255]],[0.333,[0,255,255]],[0.667,[255,255,0]],[1,[255,0,255]]] },
  aurora:      { cyclic: false, stops: [[0,[12,0,34]],[0.3,[70,40,200]],[0.55,[40,210,160]],[0.8,[120,240,220]],[1,[240,255,240]]] },
  inferno:     { cyclic: false, stops: [[0,[0,0,3]],[0.25,[90,7,120]],[0.5,[180,28,150]],[0.75,[240,140,40]],[1,[255,245,170]]] },
};
const PALETTE_NAMES = Object.keys(PALETTES);

/* Build a flat Uint8Array LUT [r,g,b,...] via linear interpolation between stops */
function buildLUT(palName) {
  const pal = PALETTES[palName];
  const n = CFG.LUT_SIZE;
  const lut = new Uint8Array(n * 3);
  const stops = pal.stops;
  for (let i = 0; i < n; i++) {
    const t = n <= 1 ? 0 : i / (n - 1);
    let a = stops[0], b = stops[stops.length - 1];
    for (let s = 0; s < stops.length - 1; s++) {
      if (t >= stops[s][0] && t <= stops[s + 1][0]) { a = stops[s]; b = stops[s + 1]; break; }
    }
    const span = b[0] - a[0] || 1;
    const f = (t - a[0]) / span;
    lut[i * 3]     = Math.round(a[1][0] + (b[1][0] - a[1][0]) * f);
    lut[i * 3 + 1] = Math.round(a[1][1] + (b[1][1] - a[1][1]) * f);
    lut[i * 3 + 2] = Math.round(a[1][2] + (b[1][2] - a[1][2]) * f);
  }
  return { lut, cyclic: pal.cyclic };
}

/* ============================================================
   3. VIEW STATE & MAPPING
   ============================================================ */
const canvas = document.getElementById('fractal');
const ctx = canvas.getContext('2d', { alpha: false });

const view = { re: -0.5, im: 0.0, scale: CFG.FULL_SCALE };
let hist = [];
let histIdx = -1;

function magnification() { return CFG.FULL_SCALE / view.scale; }
function aspectRatio() { return displayW / displayH; }
function pixelToRe(px) { return view.re + (px - displayW * 0.5) * (view.scale * aspectRatio() / displayW); }
function pixelToIm(py) { return view.im + (py - displayH * 0.5) * (view.scale / displayH); }
function reToPixel(re) { return (re - view.re) / (view.scale / displayW) + displayW * 0.5; }
function imToPixel(im) { return (im - view.im) / (view.scale / displayH) + displayH * 0.5; }

let displayW = 0, displayH = 0;

function resizeCanvas() {
  const rect = canvas.getBoundingClientRect();
  displayW = Math.max(2, Math.round(rect.width));
  displayH = Math.max(2, Math.round(rect.height));
  canvas.width = displayW;
  canvas.height = displayH;
  ctx.imageSmoothingEnabled = false;
}

/* ============================================================
   4. ITERATION MATH (allocation-free) + SMOOTH COLORING
   -----------------------------------------------------------
   Standard double orbit. Returns { n, smooth }.
   smooth = n + 1 - log2( log2|z| )  (normalized so bands don't appear).
   Interior points (never escape) return smooth = null.
   ============================================================ */
let maxIter = CFG.BASE_ITER;
function computeMaxIter() {
  if (manualIter > 0) return Math.min(CFG.MAX_ITER_CAP, manualIter);
  const mag = magnification();
  let m = CFG.BASE_ITER + CFG.ITER_SCALE * Math.log2(Math.max(1, mag));
  return Math.min(CFG.MAX_ITER_CAP, Math.floor(m));
}

// Core Mandelbrot orbit. Allocation-free: only scalar locals.
function orbit(re0, im0) {
  const R2 = CFG.BAILOUT_R2;
  let zr = 0, zi = 0, zr2 = 0, zi2 = 0, n = 0;
  const mi = maxIter;
  for (; n < mi; n++) {
    if (zr2 + zi2 > R2) break;
    zi = 2 * zr * zi + im0;
    zr = zr2 - zi2 + re0;
    zr2 = zr * zr; zi2 = zi * zi;
  }
  if (n >= mi) return { n, smooth: null };
  const mod2 = zr2 + zi2;
  const log2mod = 0.5 * Math.log(mod2) / Math.LN2; // log2|z| = log2 sqrt(mod2)
  const smooth = n + 1 - Math.log(log2mod) / Math.LN2;
  return { n, smooth };
}

function iterationsAt(re0, im0) { return orbit(re0, im0).n; }

// current palette state
let currentPalette = 'fire';
let paletteLUT = buildLUT(currentPalette);
let cycleSpeed = 0.25;
let cycleOffset = 0;
let animating = false;

// Colorize a Float32 iteration buffer into an ImageData (colorize pass only —
// does NOT recompute iterations). Interior (val<0) -> black.
function colorize(iter, w, h) {
  const img = ctx.createImageData(w, h);
  const data = img.data;
  const lut = paletteLUT.lut;
  const cyc = paletteLUT.cyclic;
  const n = CFG.LUT_SIZE;
  const off = ((cycleOffset % 1) + 1) % 1;
  for (let i = 0, p = 0; i < w * h; i++, p += 4) {
    const val = iter[i];
    if (val < 0) { data[p + 3] = 255; continue; }
    let pos = (val * cycleSpeed + off) % 1;
    if (pos < 0) pos += 1;
    if (!cyc) { if (pos < 0) pos = 0; if (pos > 1) pos = 1; }
    const li = (Math.min(Math.floor(pos * n), n - 1)) * 3;
    data[p] = lut[li]; data[p + 1] = lut[li + 1]; data[p + 2] = lut[li + 2];
    data[p + 3] = 255;
  }
  return img;
}

/* ============================================================
   5. WEB-WORKER BASED PROGRESSIVE RENDERER
   -----------------------------------------------------------
   Worker computes a contiguous horizontal band of smooth iteration
   counts into a Float32Array (transferable, allocation-free per pixel).
   Main thread merges bands into one buffer, colorizes, and blits —
   but only if the render token is still current (cancellation).
   Progressive: coarse pass (1/4 res) immediately, full res later.
   ============================================================ */
const workerCode = `
"use strict";
self.onmessage = function (e) {
  var d = e.data;
  var w = d.w, h = d.h, re = d.re, im = d.im, scale = d.scale;
  var maxIter = d.maxIter, y0 = d.y0, yh = d.yh, token = d.token;
  var mode = d.mode, cRe = d.cRe, cIm = d.cIm;
  var reInc = scale / h, imInc = scale / h;
  var reBase = re - (w * 0.5) * reInc;
  var imBase = im - (h * 0.5) * imInc;
  var R2 = 4.0;
  var out = new Float32Array(yh * w);
  for (var row = 0; row < yh; row++) {
    var im0 = imBase + (y0 + row) * imInc;
    var oBase = row * w;
    for (var col = 0; col < w; col++) {
      var r0 = reBase + col * reInc;
      var zr, zi, zr2, zi2, n = 0, cR, cI;
      if (mode === "julia") { cR = cRe; cI = cIm; zr = r0; zi = im0; }
      else { cR = r0; cI = im0; zr = 0; zi = 0; }
      zr2 = zr * zr; zi2 = zi * zi;
      for (; n < maxIter; n++) {
        if (zr2 + zi2 > R2) break;
        zi = 2 * zr * zi + cI;
        zr = zr2 - zi2 + cR;
        zr2 = zr * zr; zi2 = zi * zi;
      }
      if (n >= maxIter) { out[oBase + col] = -1; continue; }
      var mod2 = zr2 + zi2;
      var log2mod = 0.5 * Math.log(mod2) / Math.LN2;
      out[oBase + col] = n + 1 - Math.log(log2mod) / Math.LN2;
    }
  }
  self.postMessage({ token: token, y0: y0, out: out }, [out.buffer]);
};
`;

let workerBlobURL = null;
let workers = [];
function initWorkers() {
  try {
    workerBlobURL = URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }));
    for (let i = 0; i < NUM_WORKERS; i++) {
      const w = new Worker(workerBlobURL);
      w.onmessage = onWorkerMessage;
      workers.push(w);
    }
  } catch (err) { workers = []; }
}

let renderToken = 0;
let pendingFullTimer = null;
let lastIterBuf = null;
let lastDims = { w: 0, h: 0 };

// onWorkerMessage: write the band into the current request's merged buffer,
// decrement doneBuckets, finish the frame when all bands arrive.
let currentReq = null;
let doneBuckets = 0;

function onWorkerMessage(e) {
  const { token, y0, out } = e.data;
  if (!currentReq || token !== currentReq.token) return; // stale -> drop
  currentReq.merged.set(out, y0 * currentReq.w);
  if (--doneBuckets <= 0) finishFrame(currentReq);
}

function finishFrame(req) {
  if (req.token !== renderToken) return;
  if (req.coarseCb) { req.coarseCb(req.w, req.h, req.merged); return; }
  const img = colorize(req.merged, req.w, req.h);
  ctx.putImageData(img, 0, 0);
  lastIterBuf = req.merged;
  lastDims = { w: req.w, h: req.h };
}

function getTempCanvas(w, h) {
  if (tempCanvas && tempW === w && tempH === h) return tempCanvas;
  tempCanvas = document.createElement('canvas');
  tempCanvas.width = w; tempCanvas.height = h;
  tempW = w; tempH = h;
  return tempCanvas;
}
let tempCanvas = null, tempW = 0, tempH = 0;

// Launch one horizontal band per worker over a w×h image.
function runBanks(token, w, h, coarseCb) {
  const req = { token, w, h, merged: new Float32Array(w * h), coarseCb };
  currentReq = req;
  const bands = Math.min(NUM_WORKERS, Math.max(1, h));
  const bandH = Math.ceil(h / bands);
  doneBuckets = 0;
  const msg = { w, h, re: view.re, im: view.im, scale: view.scale,
                maxIter, token, mode: renderMode, cRe: renderC.re, cIm: renderC.im };
  let workersUsed = 0;
  for (let b = 0; b < bands; b++) {
    const y0 = Math.min(h, b * bandH);
    const yh = Math.min(bandH, h - y0);
    if (yh <= 0) { doneBuckets++; continue; }
    if (workers[b]) {
      const out = new Float32Array(yh * w);
      msg.y0 = y0; msg.yh = yh;
      workers[b].postMessage(msg, [out.buffer]);
      workersUsed++;
      doneBuckets++;
    } else {
      // Synchronous fallback (no workers): compute the band inline, write into
      // the merged buffer, and finish immediately if this is the last band.
      computeBand(req.merged, w, y0, yh, view.re, view.im, view.scale, maxIter, renderMode, renderC);
      doneBuckets++;
      if (doneBuckets >= bands) finishFrame(req);
    }
  }
}

// Fallback row/band computation (used only if no workers available).
function computeRow(out, y, w, re, im, scale, maxIter, mode, c) {
  const R2 = CFG.BAILOUT_R2;
  const reInc = scale / displayH, imInc = scale / displayH;
  const reBase = re - (w * 0.5) * reInc;
  const im0 = im + (y - displayH * 0.5) * imInc;
  const oBase = y * w;
  for (let col = 0; col < w; col++) {
    const r0 = reBase + col * reInc;
    let zr, zi, zr2, zi2, n = 0, cR, cI;
    if (mode === 'julia') { cR = c.re; cI = c.im; zr = r0; zi = im0; }
    else { cR = r0; cI = im0; zr = 0; zi = 0; }
    zr2 = zr * zr; zi2 = zi * zi;
    for (; n < maxIter; n++) { if (zr2 + zi2 > R2) break; zi = 2 * zr * zi + cI; zr = zr2 - zi2 + cR; zr2 = zr * zr; zi2 = zi * zi; }
    if (n >= maxIter) { out[oBase + col] = -1; continue; }
    const mod2 = zr2 + zi2;
    out[oBase + col] = n + 1 - Math.log(0.5 * Math.log(mod2) / Math.LN2) / Math.LN2;
  }
}
function computeBand(out, w, y0, yh, re, im, scale, maxIter, mode, c, _c2) {
  for (let y = 0; y < yh; y++) computeRow(out, y0 + y, w, re, im, scale, maxIter, mode, c);
}

// Progressive render: coarse pass now, full-res refine later.
function scheduleRender() {
  if (pendingFullTimer) { clearTimeout(pendingFullTimer); pendingFullTimer = null; }
  const token = ++renderToken;
  const coarseW = Math.max(1, Math.round(displayW * CFG.COARSE_RES));
  const coarseH = Math.max(1, Math.round(displayH * CFG.COARSE_RES));
  runBanks(token, coarseW, coarseH, (w, h, merged) => {
    if (token !== renderToken) return;
    // blit coarse scaled up (pixelated) to fill the canvas
    const tmp = getTempCanvas(w, h);
    tmp.getContext('2d').putImageData(colorize(merged, w, h), 0, 0);
    ctx.imageSmoothingEnabled = false;
    ctx.clearRect(0, 0, displayW, displayH);
    ctx.drawImage(tmp, 0, 0, displayW, displayH);
    pendingFullTimer = setTimeout(() => {
      if (token !== renderToken) return;
      pendingFullTimer = null;
      runBanks(token, displayW, displayH, null);
    }, CFG.REFINE_DELAY);
  });
}

/* ============================================================
   6. NAVIGATION
   ============================================================ */
let isDragging = false, dragKind = null, dragStartPx = 0, dragStartPy = 0, dragMoved = false;

function clampScale(s) {
  const mag = CFG.FULL_SCALE / s;
  if (mag > CFG.MAX_MAGNIFICATION) {
    toast('Precision limit reached (~1e' + Math.floor(Math.log10(CFG.MAX_MAGNIFICATION)) + ') — zoom locked');
    return CFG.FULL_SCALE / CFG.MAX_MAGNIFICATION;
  }
  return s;
}

function pushHistory() {
  hist = hist.slice(0, histIdx + 1);
  hist.push({ re: view.re, im: view.im, scale: view.scale });
  histIdx = hist.length - 1;
  updateHistoryButtons();
}

function setView({ re, im, scale }, push = true) {
  view.re = re; view.im = im; view.scale = clampScale(scale);
  maxIter = computeMaxIter();
  if (push) pushHistory();
  scheduleRender();
  updateHUD();
  updateHash();
}

// Wheel zoom toward cursor
function onWheel(e) {
  e.preventDefault();
  const factor = Math.pow(1.0015, -e.deltaY);
  const beforeRe = pixelToRe(e.offsetX), beforeIm = pixelToIm(e.offsetY);
  const newScale = clampScale(view.scale / factor);
  view.re = beforeRe - (e.offsetX - displayW * 0.5) * (newScale * aspectRatio() / displayW);
  view.im = beforeIm - (e.offsetY - displayH * 0.5) * (newScale / displayH);
  view.scale = newScale;
  maxIter = computeMaxIter();
  pushHistory();
  scheduleRender();
  updateHUD(); updateHash();
}

function pointerDown(e) {
  if (e.button === 1 || e.shiftKey) dragKind = 'pan';
  else if (e.button === 0) dragKind = 'zoom';
  else return;
  isDragging = true; dragMoved = false;
  dragStartPx = e.clientX; dragStartPy = e.clientY;
  zoomBox.style.display = 'none';
  canvas.style.cursor = dragKind === 'pan' ? 'grab' : 'crosshair';
  window.addEventListener('mousemove', onMove);
  window.addEventListener('mouseup', onUp);
  e.preventDefault();
  resetIdle();
}

function onMove(e) {
  updateCrosshair(e.clientX, e.clientY);
  if (!isDragging) return;
  const dx = e.clientX - dragStartPx;
  const dy = e.clientY - dragStartPy;
  if (Math.abs(dx) + Math.abs(dy) > 3) dragMoved = true;
  if (dragKind === 'pan') {
    view.re -= dx / displayW * view.scale;
    view.im += dy / displayH * view.scale;
    maxIter = computeMaxIter();
    scheduleRender();
  } else {
    const x = Math.min(dragStartPx, e.clientX);
    const y = Math.min(dragStartPy, e.clientY);
    zoomBox.style.left = x + 'px';
    zoomBox.style.top = y + 'px';
    zoomBox.style.width = Math.abs(dx) + 'px';
    zoomBox.style.height = Math.abs(dy) + 'px';
    zoomBox.style.display = 'block';
  }
}

function onUp(e) {
  if (!isDragging) return;
  isDragging = false;
  window.removeEventListener('mousemove', onMove);
  window.removeEventListener('mouseup', onUp);
  canvas.style.cursor = 'crosshair';
  zoomBox.style.display = 'none';
  if (dragKind === 'pan') { pushHistory(); updateHUD(); updateHash(); return; }
  if (!dragMoved) { handleCanvasClick(e.clientX, e.clientY); return; }
  // rect zoom: frame the selected rectangle exactly (square pixels)
  const x0 = Math.min(dragStartPx, e.clientX), x1 = Math.max(dragStartPx, e.clientX);
  const y0 = Math.min(dragStartPy, e.clientY), y1 = Math.max(dragStartPy, e.clientY);
  const selRe0 = pixelToRe(x0), selRe1 = pixelToRe(x1);
  const selIm0 = pixelToIm(y0), selIm1 = pixelToIm(y1);
  const newScale = (y1 - y0) / displayH * view.scale;
  setView({ re: (selRe0 + selRe1) / 2, im: (selIm0 + selIm1) / 2, scale: newScale });
}

// Arrow-key panning and +/- zoom
function onKey(e) {
  const panStep = view.scale * 0.08;
  if (e.key === 'ArrowLeft') { view.re -= panStep; scheduleRender(); }
  else if (e.key === 'ArrowRight') { view.re += panStep; scheduleRender(); }
  else if (e.key === 'ArrowUp') { view.im -= panStep; scheduleRender(); }
  else if (e.key === 'ArrowDown') { view.im += panStep; scheduleRender(); }
  else if (e.key === '+' || e.key === '=') { zoomBy(1 / 1.3); }
  else if (e.key === '-') { zoomBy(1.3); }
  else if (e.key === 'j' || e.key === 'J') { toggleJulia(); return; }
  else if (e.key === 'h' || e.key === 'H') { toggleHUD(); return; }
  else if (e.key === 'f' || e.key === 'F') { toggleFPS(); return; }
  else return;
  e.preventDefault();
  pushHistory(); updateHUD(); updateHash();
}
function zoomBy(f) {
  const ns = clampScale(view.scale * f);
  view.scale = ns;
  maxIter = computeMaxIter();
  scheduleRender();
}

/* ============================================================
   7. JULIA MODE & LIVE PREVIEW
   -----------------------------------------------------------
   renderMode: 'mb'  -> c = plane point, z0 = 0   (Mandelbrot)
   renderMode: 'julia'-> c = renderC (constant), z0 = plane point (Julia or preview)
   ============================================================ */
let juliaMode = false;
let juliaC = { re: -0.4, im: 0.6 };
let previewJulia = false;
let renderMode = 'mb';
let renderC = { re: -0.4, im: 0.6 };
let mouseX = -1, mouseY = -1, previewSeq = 0;

function handleCanvasClick(px, py) {
  const re = pixelToRe(px), im = pixelToIm(py);
  if (previewJulia && !juliaMode) {
    juliaC = { re, im };
    juliaMode = true;
    setView({ re, im, scale: view.scale }, true);
  } else if (juliaMode) {
    juliaC = { re, im };
    scheduleRender();
  }
  updateHUD(); updateHash();
}

function toggleJulia() {
  juliaMode = !juliaMode;
  if (juliaMode) juliaC = { re: view.re, im: view.im };
  setJuliaButtons();
  scheduleRender(); updateHUD(); updateHash();
}

function applyRenderState() {
  if (juliaMode) { renderMode = 'julia'; renderC = juliaC; }
  else if (previewJulia) { renderMode = 'julia'; renderC = { re: mouseX_re(), im: mouseY_re() }; }
  else { renderMode = 'mb'; renderC = { re: 0, im: 0 }; }
}
function mouseX_re() { return pixelToRe(mouseX); }
function mouseY_re() { return pixelToIm(mouseY); }

function setJuliaButtons() {
  btnMandel.classList.toggle('active', !juliaMode);
  btnJulia.classList.toggle('active', juliaMode);
  hTypeEl.textContent = juliaMode ? 'Julia (c=' + juliaC.re.toFixed(4) + ',' + juliaC.im.toFixed(4) + ')' : 'Mandelbrot';
}

/* ============================================================
   8. HUD / CONTROLS / KEYBOARD / ANIMATION LOOP
   ============================================================ */
const hud = document.getElementById('hud');
const hudToggle = document.getElementById('hudToggle');
const hTypeEl = document.getElementById('hType');
const hMagEl = document.getElementById('hMag');
const hReEl = document.getElementById('hRe');
const hImEl = document.getElementById('hIm');
const hIterEl = document.getElementById('hIter');
const hFpsEl = document.getElementById('hFps');
const zoomBox = document.getElementById('zoombox');
const crosshair = document.getElementById('crosshair');
const hint = document.getElementById('hint');
const toastEl = document.getElementById('toast');

let hudHidden = false, fpsLock = false;
let idleTimer = null;
let fps = 0, frameTimes = [], lastFrameT = performance.now();

function updateHUD() {
  const mag = magnification();
  hMagEl.textContent = '×' + mag.toExponential(2);
  const dp = (Math.abs(view.re) < 1e-7) ? 12 : 7;
  const dpi = (Math.abs(view.im) < 1e-7) ? 12 : 7;
  hReEl.textContent = view.re.toFixed(dp);
  hImEl.textContent = view.im.toFixed(dpi);
  hIterEl.textContent = maxIter;
  if (juliaMode) setJuliaButtons();
}
function updateHash() { /* see section 9 */ }

function toast(msg) {
  toastEl.textContent = msg;
  toastEl.classList.add('show');
  clearTimeout(toast._t);
  toast._t = setTimeout(() => toastEl.classList.remove('show'), 2200);
}

function toggleHUD() {
  hudHidden = !hudHidden;
  document.body.classList.toggle('hud-hidden', hudHidden);
}
function toggleFPS() {
  fpsLock = !fpsLock;
  toast('FPS ' + (fpsLock ? 'locked' : 'free'));
}

function resetIdle() {
  document.body.classList.remove('hud-hidden'); hudHidden = false;
  hud.classList.remove('hidden'); panel.classList.remove('hidden'); hint.classList.remove('hidden');
  clearTimeout(idleTimer);
  idleTimer = setTimeout(() => {
    hud.classList.add('hidden'); panel.classList.add('hidden'); hint.classList.add('hidden');
  }, 4500);
}

function updateCrosshair(px, py) {
  mouseX = px; mouseY = py;
  if (previewJulia) {
    crosshair.style.display = 'block';
    crosshair.style.left = (px - 13) + 'px';
    crosshair.style.top = (py - 13) + 'px';
  } else crosshair.style.display = 'none';
}

/* Palette chips */
const swatchesEl = document.getElementById('swatches');
function buildSwatches() {
  swatchesEl.innerHTML = '';
  PALETTE_NAMES.forEach(name => {
    const pal = PALETTES[name];
    const chip = document.createElement('div');
    chip.className = 'swatch' + (name === currentPalette ? ' active' : '');
    chip.dataset.name = name; chip.title = name;
    chip.tabIndex = 0; chip.role = 'button'; chip.setAttribute('aria-label', 'Palette ' + name);
    const stops = pal.stops.map(s => `rgb(${s[1].join(',')}) ${(s[0] * 100).toFixed(0)}%`).join(', ');
    chip.style.background = `linear-gradient(90deg, ${stops})`;
    chip.addEventListener('click', () => setPalette(name));
    chip.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setPalette(name); } });
    swatchesEl.appendChild(chip);
  });
}
function setPalette(name) {
  if (!PALETTE_NAMES.includes(name)) return;
  currentPalette = name;
  paletteLUT = buildLUT(name);
  buildSwatches();
  if (lastIterBuf) { const img = colorize(lastIterBuf, lastDims.w, lastDims.h); ctx.putImageData(img, 0, 0); }
  updateHash();
}

/* Sliders */
const iterSlider = document.getElementById('iterSlider');
const iterValEl = document.getElementById('iterVal');
const speedSlider = document.getElementById('speedSlider');
const speedValEl = document.getElementById('speedVal');
let manualIter = 0;

iterSlider.addEventListener('input', () => {
  manualIter = parseInt(iterSlider.value, 10);
  maxIter = computeMaxIter();
  iterValEl.textContent = manualIter > 0 ? manualIter : 'auto';
  scheduleRender();
});
speedSlider.addEventListener('input', () => {
  cycleSpeed = parseFloat(speedSlider.value);
  speedValEl.textContent = cycleSpeed.toFixed(2);
  if (lastIterBuf) { const img = colorize(lastIterBuf, lastDims.w, lastDims.h); ctx.putImageData(img, 0, 0); }
});
const chkAnim = document.getElementById('chkAnim');
chkAnim.addEventListener('change', () => { animating = chkAnim.checked; });

/* Buttons */
const btnMandel = document.getElementById('btnMandel');
const btnJulia = document.getElementById('btnJulia');
const btnBack = document.getElementById('btnBack');
const btnReset = document.getElementById('btnReset');
const chkPreview = document.getElementById('chkPreview');

btnMandel.addEventListener('click', () => { if (juliaMode) { juliaMode = false; scheduleRender(); setJuliaButtons(); updateHUD(); updateHash(); } });
btnJulia.addEventListener('click', () => { if (!juliaMode) { toggleJulia(); } });
btnReset.addEventListener('click', () => { setView({ re: -0.5, im: 0.0, scale: CFG.FULL_SCALE }, true); });
btnBack.addEventListener('click', () => {
  if (histIdx > 0) {
    histIdx--;
    const s = hist[histIdx];
    view.re = s.re; view.im = s.im; view.scale = s.scale;
    maxIter = computeMaxIter();
    scheduleRender(); updateHUD(); updateHash(); updateHistoryButtons();
  }
});
chkPreview.addEventListener('change', () => {
  previewJulia = chkPreview.checked;
  updateCrosshair(mouseX, mouseY);
  if (previewJulia) { applyRenderState(); throttlePreview(); }
});
hudToggle.addEventListener('click', () => { document.body.classList.remove('hud-hidden'); hudHidden = false; resetIdle(); });
hudToggle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); hudToggle.click(); } });

function updateHistoryButtons() { btnBack.disabled = histIdx <= 0; }

// Throttle Julia preview renders on hover
let previewThrottle = null;
function throttlePreview() {
  if (previewThrottle) return;
  previewThrottle = setTimeout(() => {
    previewThrottle = null;
    if (previewJulia && !juliaMode) { applyRenderState(); scheduleRender(); }
  }, 60);
}

/* Animation / render loop */
let loopRunning = true;
function loop(now) {
  const dt = now - lastFrameT; lastFrameT = now;
  frameTimes.push(dt);
  if (frameTimes.length > 30) frameTimes.shift();
  if (!fpsLock) {
    const avg = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
    fps = Math.round(1000 / avg);
    hFpsEl.textContent = fps;
  }
  if (animating && lastIterBuf) {
    cycleOffset = (cycleOffset + 0.003) % 1;
    const img = colorize(lastIterBuf, lastDims.w, lastDims.h);
    ctx.putImageData(img, 0, 0);
  }
  requestAnimationFrame(loop);
}

/* ============================================================
   9. BOOKMARKS (localStorage) + URL HASH
   ============================================================ */
const LS_KEY = 'fractalBookmarks.v1';
function loadBookmarks() {
  try { return JSON.parse(localStorage.getItem(LS_KEY)) || []; } catch (e) { return []; }
}
function saveBookmarks(list) {
  try { localStorage.setItem(LS_KEY, JSON.stringify(list)); } catch (e) {}
}
function renderBookmarks() {
  const list = loadBookmarks();
  const box = document.getElementById('bookmarkList');
  box.innerHTML = '';
  list.forEach((bm, i) => {
    const b = document.createElement('button');
    b.className = 'bookmark';
    b.innerHTML = `<span>${escapeHtml(bm.name)}</span><span class="del" title="Delete">✕</span>`;
    b.querySelector('span').addEventListener('click', () => restoreBookmark(i));
    b.querySelector('.del').addEventListener('click', (e) => {
      e.stopPropagation();
      list.splice(i, 1); saveBookmarks(list); renderBookmarks();
    });
    box.appendChild(b);
  });
}
function escapeHtml(s) {
  return (s + '').replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m]));
}

function currentBookmark() {
  return {
    type: juliaMode ? 'julia' : 'mandelbrot',
    re: view.re, im: view.im, scale: view.scale,
    palette: currentPalette, iter: manualIter,
    juliaC: juliaC, cycleSpeed,
  };
}
function saveBookmark() {
  const name = document.getElementById('bmName').value.trim() || ('view' + (loadBookmarks().length + 1));
  const list = loadBookmarks();
  list.push({ name, ...currentBookmark() });
  saveBookmarks(list);
  document.getElementById('bmName').value = '';
  renderBookmarks();
}
function restoreBookmark(i) {
  const bm = loadBookmarks()[i];
  if (!bm) return;
  juliaMode = bm.type === 'julia';
  juliaC = bm.juliaC || { re: view.re, im: view.im };
  manualIter = bm.iter || 0;
  cycleSpeed = bm.cycleSpeed || cycleSpeed;
  iterSlider.value = manualIter; iterValEl.textContent = manualIter > 0 ? manualIter : 'auto';
  speedSlider.value = cycleSpeed; speedValEl.textContent = cycleSpeed.toFixed(2);
  setPalette(bm.palette || 'fire');
  setView({ re: bm.re, im: bm.im, scale: bm.scale }, true);
  setJuliaButtons();
  updateHUD(); updateHash();
  toast('Restored: ' + bm.name);
}
const btnSave = document.getElementById('btnSave');
btnSave.addEventListener('click', saveBookmark);

/* URL hash state */
function encodeHash() {
  const b = currentBookmark();
  const parts = [b.type, 're=' + b.re.toPrecision(12), 'im=' + b.im.toPrecision(12), 'scale=' + b.scale.toPrecision(12)];
  if (b.type === 'julia') parts.push('c_re=' + b.juliaC.re.toPrecision(10), 'c_im=' + b.juliaC.im.toPrecision(10));
  parts.push('pal=' + b.palette);
  return '#' + parts.join('&');
}
function updateHash() { try { history.replaceState(null, '', encodeHash()); } catch (e) {} }
function loadFromHash() {
  const h = location.hash.replace(/^#/, '');
  if (!h) return false;
  const p = {};
  h.split('&').forEach(s => { const i = s.indexOf('='); if (i > 0) p[s.slice(0, i)] = decodeURIComponent(s.slice(i + 1)); });
  if (!p.re) return false;
  juliaMode = p.type === 'julia';
  juliaC = { re: parseFloat(p.c_re) || view.re, im: parseFloat(p.c_im) || view.im };
  manualIter = parseInt(p.iter || '0', 10);
  setPalette(p.pal || 'fire');
  setView({ re: parseFloat(p.re), im: parseFloat(p.im), scale: parseFloat(p.scale) }, false);
  setJuliaButtons();
  return true;
}

/* ============================================================
   10. window.fractal DEBUG INTERFACE
   ============================================================ */
window.fractal = {
  iterationsAt: (re, im) => iterationsAt(re, im),
  orbit: (re, im) => orbit(re, im),
  setView: (o) => setView(o, true),
  getView: () => ({ re: view.re, im: view.im, scale: view.scale }),
  getJuliaC: () => juliaC,
  setJuliaC: (c) => { juliaC = c; scheduleRender(); },
  setPalette: (n) => setPalette(n),
  getPalette: () => currentPalette,
  getIterations: () => maxIter,
  magnification: () => magnification(),
  _render: () => scheduleRender(),
};

/* ============================================================
   BOOTSTRAP
   ============================================================ */
function init() {
  resizeCanvas();
  initWorkers();
  maxIter = computeMaxIter();

  canvas.addEventListener('mousedown', pointerDown);
  canvas.addEventListener('auxclick', (e) => { if (e.button === 1) pointerDown(e); });
  canvas.addEventListener('wheel', onWheel, { passive: false });
  canvas.addEventListener('contextmenu', (e) => e.preventDefault());
  window.addEventListener('keydown', onKey);
  window.addEventListener('resize', () => { resizeCanvas(); scheduleRender(); });
  window.addEventListener('blur', () => { isDragging = false; });
  canvas.addEventListener('touchstart', onTouchStart, { passive: false });
  canvas.addEventListener('touchmove', onTouchMove, { passive: false });
  canvas.addEventListener('touchend', onTouchEnd);

  // Permanent mousemove for Julia live-preview: update c and re-render.
  canvas.addEventListener('mousemove', (e) => {
    updateCrosshair(e.clientX, e.clientY);
    if (previewJulia && !juliaMode) {
      renderC = { re: pixelToRe(e.clientX), im: pixelToIm(e.clientY) };
      console.log('PREVIEW mousemove at', e.clientX, e.clientY, 'renderC=', JSON.stringify(renderC));
      throttlePreview();
    }
  });

  buildSwatches();
  renderBookmarks();
  setJuliaButtons();

  const restored = loadFromHash();
  if (!restored) { setView({ re: -0.5, im: 0.0, scale: CFG.FULL_SCALE }, false); }

  updateHUD();
  resetIdle();
  requestAnimationFrame(loop);
  setTimeout(() => hint.classList.add('hidden'), 8000);
}

// basic touch handlers
let touchStartDist = 0, touchStartScale = 0, touchMid = null;
function onTouchStart(e) {
  if (e.touches.length === 2) {
    e.preventDefault();
    const a = e.touches[0], b = e.touches[1];
    touchStartDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
    touchStartScale = view.scale;
    touchMid = { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
  } else if (e.touches.length === 1) {
    isDragging = true; dragKind = 'pan'; dragStartPx = e.touches[0].clientX; dragStartPy = e.touches[0].clientY;
  }
}
function onTouchMove(e) {
  if (e.touches.length === 2) {
    e.preventDefault();
    const a = e.touches[0], b = e.touches[1];
    const d = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
    const ns = clampScale(touchStartScale * touchStartDist / d);
    const beforeRe = pixelToRe(touchMid.x), beforeIm = pixelToIm(touchMid.y);
    view.re = beforeRe - (touchMid.x - displayW / 2) * (ns / displayW);
    view.im = beforeIm - (touchMid.y - displayH / 2) * (ns / displayH);
    view.scale = ns; maxIter = computeMaxIter(); scheduleRender();
  } else if (e.touches.length === 1 && isDragging) {
    const dx = e.touches[0].clientX - dragStartPx, dy = e.touches[0].clientY - dragStartPy;
    view.re -= dx / displayW * view.scale; view.im += dy / displayH * view.scale;
    maxIter = computeMaxIter(); scheduleRender();
  }
}
function onTouchEnd(e) { if (e.touches.length === 0) { isDragging = false; pushHistory(); } }

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"852fa659-75ce-4dc6-80ec-a7c5dc9628f4","tokensIn":9438309,"tokensOut":152089,"tokensTotal":9590398,"cost":0,"turns":109,"toolCalls":108,"failedToolCalls":3,"timestamp":"2026-08-23T19:37:46.361Z"} -->