// 2D Poisson widget, adapted from Bramble.jl docs/src/assets/widgets/poisson_interactive.html
// into a mountable component that takes its colours from the host page.
//
//   BramblePoisson.mount(el, { compact: true, N: 24 })
//
// Host pages may set --bp-accent, --bp-text, --bp-muted, --bp-border, --bp-surface;
// otherwise the widget falls back to --accent, --text, --muted, --border, --surface.
(function () {
  "use strict";

  var CSS = [
    ".bp{--_acc:var(--bp-accent,var(--accent,#309CDE));--_txt:var(--bp-text,var(--text,#1F2937));--_mut:var(--bp-muted,var(--muted,#6B7280));--_bd:var(--bp-border,var(--border,#E5E7EB));--_sf:var(--bp-surface,var(--surface,#F8FAFC));color:var(--_txt);font-variant-numeric:tabular-nums}",
    ".bp *{box-sizing:border-box}",
    ".bp [hidden]{display:none!important}",
    ".bp-controls{display:flex;flex-wrap:wrap;gap:10px 18px;margin-bottom:12px}",
    ".bp-field{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1 1 120px}",
    ".bp-field label{font-size:11px;color:var(--_mut);text-transform:uppercase;letter-spacing:.05em}",
    ".bp select{font:inherit;font-size:13px;color:var(--_txt);background:var(--_sf);border:1px solid var(--_bd);border-radius:6px;padding:4px 6px;width:100%}",
    ".bp input[type=range]{accent-color:var(--_acc);width:100%;min-width:0}",
    ".bp-range{display:flex;align-items:center;gap:8px}",
    ".bp-range output{min-width:2.2em;font-size:13px}",
    ".bp-panels{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:12px}",
    ".bp-panel{display:flex;flex-direction:column;gap:4px;min-width:0}",
    ".bp-title{font-size:12px;color:var(--_mut)}",
    ".bp canvas{width:100%;aspect-ratio:1;max-width:100%;border:1px solid var(--_bd);border-radius:6px;background:var(--_sf);display:block}",
    ".bp-diag{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px 16px;font-size:13px;margin:0}",
    ".bp-diag div{display:flex;flex-direction:column}",
    ".bp-diag dt{color:var(--_mut);font-size:11px;text-transform:uppercase;letter-spacing:.05em}",
    ".bp-diag dd{margin:0;font-weight:600}",
    ".bp-conv{margin-top:16px;display:flex;flex-direction:column;gap:8px}",
    ".bp-conv button{align-self:flex-start;font:inherit;font-size:13px;font-weight:600;color:#fff;background:var(--_acc);border:0;border-radius:6px;padding:7px 14px;cursor:pointer}",
    ".bp-conv button:focus-visible,.bp select:focus-visible,.bp input:focus-visible{outline:2px solid var(--_acc);outline-offset:2px}",
    ".bp-conv canvas{aspect-ratio:16/9}",
    ".bp-note{font-size:12px;color:var(--_mut);margin:0}",
    ".bp-probe{color:var(--_acc);border-color:var(--_bd);background:var(--_sf);position:absolute;width:0;height:0;overflow:hidden}"
  ].join("\n");

  function injectCSS() {
    if (document.getElementById("bp-style")) return;
    var s = document.createElement("style");
    s.id = "bp-style";
    s.textContent = CSS;
    document.head.appendChild(s);
  }

  // Deterministic PRNG (mulberry32) so a given (N, mesh type) pair always draws the same mesh.
  function mulberry32(seed) {
    return function () {
      seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
      var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  // Non-uniform mesh on [0, 1]: independent uniform interior draws, sorted, floored on spacing.
  function buildAxis(N, random, seed) {
    var x = new Float64Array(N + 1), i;
    for (i = 0; i <= N; i++) x[i] = i / N;
    if (!random) return x;
    var rand = mulberry32(seed), h = 1 / N;
    for (i = 1; i < N; i++) x[i] = rand();
    x = Array.prototype.slice.call(x).sort(function (a, b) { return a - b; });
    var minGap = 0.15 * h;
    for (i = 1; i < N; i++) if (x[i] - x[i - 1] < minGap) x[i] = x[i - 1] + minGap;
    var span = x[N] === x[0] ? 1 : (1 - x[0]) / (x[N] - x[0]);
    for (i = 0; i <= N; i++) x[i] = x[0] + (x[i] - x[0]) * span;
    x[0] = 0; x[N] = 1;
    return Float64Array.from(x);
  }

  var SOLUTIONS = {
    sine: { label: "sin(πx) sin(πy)", u: function (x, y) { return Math.sin(Math.PI * x) * Math.sin(Math.PI * y); }, g: function (x, y) { return 2 * Math.PI * Math.PI * Math.sin(Math.PI * x) * Math.sin(Math.PI * y); } },
    exp: { label: "e^(x+y)", u: function (x, y) { return Math.exp(x + y); }, g: function (x, y) { return -2 * Math.exp(x + y); } },
    poly: { label: "(x²−x)(y²−y)", u: function (x, y) { return (x * x - x) * (y * y - y); }, g: function (x, y) { return -2 * ((y * y - y) + (x * x - x)); } }
  };

  // Matrix-free symmetric 5-point finite-volume Laplacian on a non-uniform tensor mesh.
  function assembleProblem(N, X, Y, solKey) {
    var sol = SOLUTIONS[solKey], m = N - 1, dof = m * m;
    var idx = function (k, l) { return l * m + k; };
    var hxm = new Float64Array(m), hxp = new Float64Array(m), hym = new Float64Array(m), hyp = new Float64Array(m), k, l;
    for (k = 0; k < m; k++) { hxm[k] = X[k + 1] - X[k]; hxp[k] = X[k + 2] - X[k + 1]; }
    for (l = 0; l < m; l++) { hym[l] = Y[l + 1] - Y[l]; hyp[l] = Y[l + 2] - Y[l + 1]; }
    var b = new Float64Array(dof), nnz = [];
    for (l = 0; l < m; l++) for (k = 0; k < m; k++) {
      var ii = k + 1, jj = l + 1;
      var faceY = 0.5 * (hym[l] + hyp[l]), faceX = 0.5 * (hxm[k] + hxp[k]);
      var cW = faceY / hxm[k], cE = faceY / hxp[k], cS = faceX / hym[l], cN = faceX / hyp[l];
      var row = idx(k, l), rhs = sol.g(X[ii], Y[jj]) * faceX * faceY;
      if (k === 0) rhs += cW * sol.u(X[0], Y[jj]); else nnz.push([row, idx(k - 1, l)]);
      if (k === m - 1) rhs += cE * sol.u(X[N], Y[jj]); else nnz.push([row, idx(k + 1, l)]);
      if (l === 0) rhs += cS * sol.u(X[ii], Y[0]); else nnz.push([row, idx(k, l - 1)]);
      if (l === m - 1) rhs += cN * sol.u(X[ii], Y[N]); else nnz.push([row, idx(k, l + 1)]);
      nnz.push([row, row]);
      b[row] = rhs;
    }
    function matvec(v, out) {
      for (var l2 = 0; l2 < m; l2++) for (var k2 = 0; k2 < m; k2++) {
        var fY = 0.5 * (hym[l2] + hyp[l2]), fX = 0.5 * (hxm[k2] + hxp[k2]);
        var w = fY / hxm[k2], e = fY / hxp[k2], s = fX / hym[l2], n = fX / hyp[l2];
        var r = idx(k2, l2), acc = (w + e + s + n) * v[r];
        if (k2 > 0) acc -= w * v[idx(k2 - 1, l2)];
        if (k2 < m - 1) acc -= e * v[idx(k2 + 1, l2)];
        if (l2 > 0) acc -= s * v[idx(k2, l2 - 1)];
        if (l2 < m - 1) acc -= n * v[idx(k2, l2 + 1)];
        out[r] = acc;
      }
    }
    return { dof: dof, b: b, matvec: matvec, nnz: nnz, sol: sol, m: m };
  }

  function dot(a, b) { var s = 0; for (var i = 0; i < a.length; i++) s += a[i] * b[i]; return s; }

  // Plain CG; keeps alpha/beta for the Lanczos condition estimate.
  function cg(dof, matvec, b) {
    var x = new Float64Array(dof), r = b.slice(), p = r.slice(), Ap = new Float64Array(dof);
    var rho = dot(r, r), rho0 = rho || 1, alphas = [], betas = [], maxIter = Math.min(3 * dof, 4000);
    for (var it = 0; it < maxIter && rho / rho0 > 1e-24; it++) {
      matvec(p, Ap);
      var alpha = rho / dot(p, Ap), i;
      for (i = 0; i < dof; i++) { x[i] += alpha * p[i]; r[i] -= alpha * Ap[i]; }
      var rhoNext = dot(r, r);
      alphas.push(alpha);
      if (rhoNext / rho0 <= 1e-24) break;
      var beta = rhoNext / rho;
      betas.push(beta);
      for (i = 0; i < dof; i++) p[i] = r[i] + beta * p[i];
      rho = rhoNext;
    }
    return { x: x, alphas: alphas, betas: betas };
  }

  // Extreme eigenvalues of the Lanczos tridiagonal built from the CG scalars (Sturm bisection).
  function conditionEstimate(alphas, betas) {
    var n = alphas.length;
    if (n < 2) return 1;
    var d = new Float64Array(n), g = new Float64Array(n - 1), k;
    d[0] = 1 / alphas[0];
    for (k = 1; k < n; k++) d[k] = 1 / alphas[k] + betas[k - 1] / alphas[k - 1];
    for (k = 0; k < n - 1; k++) g[k] = Math.sqrt(betas[k]) / alphas[k];
    function count(x) {
      var q = d[0] - x, c = q < 0 ? 1 : 0;
      for (var i = 1; i < n; i++) { q = (d[i] - x) - (g[i - 1] * g[i - 1]) / (q === 0 ? 1e-300 : q); if (q < 0) c++; }
      return c;
    }
    var rad = 0;
    for (k = 0; k < n; k++) rad = Math.max(rad, Math.abs(d[k]) + (k > 0 ? Math.abs(g[k - 1]) : 0) + (k < n - 1 ? Math.abs(g[k]) : 0));
    function eig(target) { var lo = -rad, hi = rad; for (var i = 0; i < 60; i++) { var mid = 0.5 * (lo + hi); if (count(mid) < target) lo = mid; else hi = mid; } return hi; }
    return eig(n) / eig(1);
  }

  var COLORMAPS = {
    viridis: ["#440154", "#414487", "#2a788e", "#22a884", "#7ad151", "#fde725"],
    plasma: ["#0d0887", "#6a00a8", "#b12a90", "#e16462", "#fca636", "#f0f921"],
    turbo: ["#30123b", "#4147d6", "#1ba8c9", "#61c93a", "#ecd21a", "#e4460a", "#7a0403"],
    blues: ["#0b2e4f", "#0369A1", "#309CDE", "#9fd3f3", "#f0f9ff"],
    gray: ["#000000", "#ffffff"]
  };
  function hexToRgb(h) { var v = parseInt(h.slice(1), 16); return [(v >> 16) & 255, (v >> 8) & 255, v & 255]; }
  function colormap(name) {
    var st = COLORMAPS[name].map(hexToRgb);
    return function (t) {
      t = Math.max(0, Math.min(1, t));
      var pos = t * (st.length - 1), i0 = Math.floor(pos), i1 = Math.min(i0 + 1, st.length - 1), f = pos - i0, a = st[i0], b = st[i1];
      return "rgb(" + Math.round(a[0] + (b[0] - a[0]) * f) + "," + Math.round(a[1] + (b[1] - a[1]) * f) + "," + Math.round(a[2] + (b[2] - a[2]) * f) + ")";
    };
  }

  function nodalWeights(Z) {
    var N = Z.length - 1, w = new Float64Array(N + 1);
    w[0] = (Z[1] - Z[0]) / 2; w[N] = (Z[N] - Z[N - 1]) / 2;
    for (var i = 1; i < N; i++) w[i] = (Z[i + 1] - Z[i - 1]) / 2;
    return w;
  }

  // Bramble's normₕ and norm₁ₕ (src/space/inner_product.jl).
  function errors(N, X, Y, u, sol) {
    var wx = nodalWeights(X), wy = nodalWeights(Y), n2 = 0, e = new Float64Array((N + 1) * (N + 1)), i, j;
    for (j = 0; j <= N; j++) for (i = 0; i <= N; i++) {
      var v = u[j * (N + 1) + i] - sol.u(X[i], Y[j]);
      e[j * (N + 1) + i] = v; n2 += wx[i] * wy[j] * v * v;
    }
    var s2 = 0;
    for (j = 0; j <= N; j++) for (i = 0; i < N; i++) { var dx = X[i + 1] - X[i], de = e[j * (N + 1) + i + 1] - e[j * (N + 1) + i]; s2 += wy[j] * de * de / dx; }
    for (j = 0; j < N; j++) { var dy = Y[j + 1] - Y[j]; for (i = 0; i <= N; i++) { var d2 = e[(j + 1) * (N + 1) + i] - e[j * (N + 1) + i]; s2 += wx[i] * d2 * d2 / dy; } }
    return { normh: Math.sqrt(n2), norm1h: Math.sqrt(n2 + s2), grid: e };
  }

  function solve(N, random, solKey) {
    var X = buildAxis(N, random, 20260916 + N * 7919), Y = buildAxis(N, random, 20260917 + N * 7919);
    var P = assembleProblem(N, X, Y, solKey), c = cg(P.dof, P.matvec, P.b), m = P.m;
    var u = new Float64Array((N + 1) * (N + 1));
    for (var j = 0; j <= N; j++) for (var i = 0; i <= N; i++) {
      var inner = i > 0 && i < N && j > 0 && j < N;
      u[j * (N + 1) + i] = inner ? c.x[(j - 1) * m + (i - 1)] : P.sol.u(X[i], Y[j]);
    }
    return { N: N, X: X, Y: Y, P: P, u: u, cg: c, err: errors(N, X, Y, u, P.sol) };
  }

  function fitCanvas(cv) {
    var dpr = window.devicePixelRatio || 1, w = cv.clientWidth || 260, h = cv.clientHeight || w;
    if (cv.width !== Math.round(w * dpr) || cv.height !== Math.round(h * dpr)) { cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr); }
    var ctx = cv.getContext("2d");
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    return { ctx: ctx, w: w, h: h };
  }

  // Draws the nodal field as cells centred on the real (nonuniform) node positions,
  // so the mesh distortion is visible in the picture itself.
  function drawField(cv, S, values, cmap, colors, showMesh) {
    var f = fitCanvas(cv), ctx = f.ctx, W = f.w, H = f.h, N = S.N, X = S.X, Y = S.Y;
    var min = Infinity, max = -Infinity, i, j;
    for (i = 0; i < values.length; i++) { if (values[i] < min) min = values[i]; if (values[i] > max) max = values[i]; }
    var range = max - min || 1;
    function mid(Z, k) { return k === 0 ? 0 : k > N ? 1 : 0.5 * (Z[k - 1] + Z[k]); }
    for (j = 0; j <= N; j++) {
      var y0 = mid(Y, j), y1 = mid(Y, j + 1);
      for (i = 0; i <= N; i++) {
        var x0 = mid(X, i), x1 = mid(X, i + 1);
        ctx.fillStyle = cmap((values[j * (N + 1) + i] - min) / range);
        ctx.fillRect(x0 * W, (1 - y1) * H, (x1 - x0) * W + 0.6, (y1 - y0) * H + 0.6);
      }
    }
    if (showMesh) {
      ctx.strokeStyle = "rgba(255,255,255,0.28)"; ctx.lineWidth = 0.7; ctx.beginPath();
      for (i = 0; i <= N; i++) { ctx.moveTo(X[i] * W, 0); ctx.lineTo(X[i] * W, H); }
      for (j = 0; j <= N; j++) { ctx.moveTo(0, (1 - Y[j]) * H); ctx.lineTo(W, (1 - Y[j]) * H); }
      ctx.stroke();
    }
  }

  function drawSpy(cv, S, colors) {
    var f = fitCanvas(cv), ctx = f.ctx, dof = S.P.dof, sc = f.w / dof, d = Math.max(1, sc);
    ctx.fillStyle = colors.surface; ctx.fillRect(0, 0, f.w, f.h);
    ctx.fillStyle = colors.accent;
    for (var n = 0; n < S.P.nnz.length; n++) ctx.fillRect(S.P.nnz[n][1] * sc, S.P.nnz[n][0] * sc, d, d);
  }

  function drawMesh(cv, S, colors) {
    var f = fitCanvas(cv), ctx = f.ctx, mg = 12, w = f.w - 2 * mg, h = f.h - 2 * mg, X = S.X, Y = S.Y, i, j;
    ctx.fillStyle = colors.surface; ctx.fillRect(0, 0, f.w, f.h);
    ctx.strokeStyle = colors.border; ctx.lineWidth = 1; ctx.beginPath();
    for (i = 0; i < X.length; i++) { ctx.moveTo(mg + X[i] * w, mg); ctx.lineTo(mg + X[i] * w, mg + h); }
    for (j = 0; j < Y.length; j++) { ctx.moveTo(mg, mg + (1 - Y[j]) * h); ctx.lineTo(mg + w, mg + (1 - Y[j]) * h); }
    ctx.stroke();
    ctx.fillStyle = colors.accent;
    for (i = 0; i < X.length; i++) for (j = 0; j < Y.length; j++) { ctx.beginPath(); ctx.arc(mg + X[i] * w, mg + (1 - Y[j]) * h, 1.5, 0, 6.2832); ctx.fill(); }
  }

  // Log–log plot of normₕ(uₕ − u) against N, with a slope −2 reference line.
  function drawConvergence(cv, rows, colors) {
    var f = fitCanvas(cv), ctx = f.ctx, W = f.w, H = f.h, L = 58, R = 16, T = 16, B = 38;
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = colors.surface; ctx.fillRect(0, 0, W, H);
    var xs = rows.map(function (r) { return Math.log10(r.N); });
    var ys = rows.map(function (r) { return Math.log10(r.e); });
    var xmin = Math.log10(6), xmax = Math.log10(Math.max.apply(null, rows.map(function (r) { return r.N; })) * 1.4);
    var ymax = Math.ceil(Math.max.apply(null, ys)), ymin = Math.floor(Math.min.apply(null, ys));
    if (ymax - ymin < 2) ymin = ymax - 2;
    var px = function (v) { return L + (v - xmin) / (xmax - xmin) * (W - L - R); };
    var py = function (v) { return T + (ymax - v) / (ymax - ymin) * (H - T - B); };
    ctx.font = "11px system-ui, sans-serif"; ctx.textBaseline = "middle";
    ctx.strokeStyle = colors.border; ctx.fillStyle = colors.muted; ctx.lineWidth = 1;
    for (var e = ymin; e <= ymax; e++) {
      ctx.beginPath(); ctx.moveTo(L, py(e)); ctx.lineTo(W - R, py(e)); ctx.stroke();
      ctx.textAlign = "right"; ctx.fillText("1e" + e, L - 6, py(e));
    }
    ctx.textAlign = "center"; ctx.textBaseline = "top";
    rows.forEach(function (r) { ctx.fillText(String(r.N), px(Math.log10(r.N)), H - B + 6); });
    ctx.fillText("N (points per direction)", L + (W - L - R) / 2, H - 16);
    // reference slope −2 through the first point, drawn dashed
    ctx.setLineDash([4, 4]); ctx.strokeStyle = colors.muted; ctx.beginPath();
    var x0 = xs[0], y0 = ys[0] + 0.25, x1 = xs[xs.length - 1];
    ctx.moveTo(px(x0), py(y0)); ctx.lineTo(px(x1), py(y0 - 2 * (x1 - x0))); ctx.stroke(); ctx.setLineDash([]);
    ctx.textAlign = "left"; ctx.textBaseline = "bottom";
    ctx.fillText("slope −2", px(x1) + 4, py(y0 - 2 * (x1 - x0)) - 2);
    ctx.strokeStyle = colors.accent; ctx.lineWidth = 2; ctx.beginPath();
    xs.forEach(function (x, i) { if (i) ctx.lineTo(px(x), py(ys[i])); else ctx.moveTo(px(x), py(ys[i])); }); ctx.stroke();
    ctx.fillStyle = colors.accent;
    xs.forEach(function (x, i) { ctx.beginPath(); ctx.arc(px(x), py(ys[i]), 3.5, 0, 6.2832); ctx.fill(); });
  }

  function fmt(x) { return isFinite(x) ? x.toExponential(3) : "–"; }

  var uid = 0;
  function mount(root, opts) {
    injectCSS();
    opts = opts || {};
    var compact = !!opts.compact, p = "bp" + (++uid) + "-";
    root.classList.add("bp");
    var sel = function (id, label, options, value) {
      return '<div class="bp-field"><label for="' + p + id + '">' + label + '</label><select id="' + p + id + '">' +
        options.map(function (o) { return '<option value="' + o[0] + '"' + (o[0] === value ? " selected" : "") + ">" + o[1] + "</option>"; }).join("") + "</select></div>";
    };
    var html = '<span class="bp-probe" aria-hidden="true"></span><div class="bp-controls">' +
      '<div class="bp-field" style="flex-basis:170px"><label for="' + p + 'n">Resolution N</label><div class="bp-range"><input type="range" id="' + p + 'n" min="8" max="' + (compact ? 40 : 48) + '" step="1" value="' + (opts.N || 24) + '"><output id="' + p + 'nl"></output></div></div>' +
      sel("mesh", "Mesh", [["uniform", "Uniform"], ["random", "Random nonuniform"]], "random");
    if (!compact) {
      html += sel("sol", "Exact solution", [["sine", "sin(πx) sin(πy)"], ["exp", "e^(x+y)"], ["poly", "(x²−x)(y²−y)"]], "sine") +
        sel("view", "Field", [["solution", "uₕ"], ["error", "|uₕ − u|"]], "solution") +
        sel("p2", "Panel 2", [["mesh", "Mesh nodes"], ["spy", "Matrix sparsity"]], "mesh") +
        sel("cmap", "Colormap", [["blues", "Coimbra blues"], ["viridis", "Viridis"], ["plasma", "Plasma"], ["turbo", "Turbo"]], opts.colormap || "blues");
    }
    html += "</div>";
    if (compact) {
      html += '<div class="bp-panel"><canvas id="' + p + 'f" role="img" aria-label="Discrete solution on the mesh"></canvas></div>' +
        '<p class="bp-note" id="' + p + 'cap" style="margin-top:8px"></p>';
    } else {
      html += '<div class="bp-panels"><div class="bp-panel"><span class="bp-title" id="' + p + 'ft">uₕ(x, y)</span><canvas id="' + p + 'f" role="img" aria-label="Discrete solution"></canvas></div>' +
        '<div class="bp-panel"><span class="bp-title" id="' + p + 't2">Mesh nodes</span><canvas id="' + p + 'c2" role="img" aria-label="Mesh or sparsity pattern"></canvas></div></div>' +
        '<dl class="bp-diag"><div><dt>Unknowns</dt><dd id="' + p + 'dof"></dd></div><div><dt>Nonzeros</dt><dd id="' + p + 'nnz"></dd></div>' +
        '<div><dt>normₕ(uₕ − u)</dt><dd id="' + p + 'l2"></dd></div><div><dt>norm₁ₕ(uₕ − u)</dt><dd id="' + p + 'h1"></dd></div><div><dt>κ(A) estimate</dt><dd id="' + p + 'k"></dd></div></dl>' +
        '<div class="bp-conv"><button type="button" id="' + p + 'run">Run convergence study</button>' +
        '<canvas id="' + p + 'cv" role="img" aria-label="Convergence plot" hidden></canvas><p class="bp-note" id="' + p + 'cvn">Solves on N = 8, 16, 32, 64 with the current mesh type and fits the observed order.</p></div>';
    }
    root.innerHTML = html;
    var $ = function (id) { return document.getElementById(p + id); };
    var probe = root.querySelector(".bp-probe"), last = null, convRows = null;

    function colors() {
      var cs = getComputedStyle(probe);
      return { accent: cs.color, border: cs.borderTopColor, surface: cs.backgroundColor, muted: getComputedStyle(root).getPropertyValue("--_mut").trim() || "#6B7280" };
    }
    function state() {
      return { N: parseInt($("n").value, 10), random: $("mesh").value === "random", sol: compact ? "sine" : $("sol").value, view: compact ? "solution" : $("view").value, p2: compact ? null : $("p2").value, cmap: compact ? (opts.colormap || "blues") : $("cmap").value };
    }
    function draw() {
      var s = state(), c = colors();
      if (!last || last.N !== s.N || last.random !== s.random || last.sol !== s.sol) {
        last = solve(s.N, s.random, s.sol); last.random = s.random; last.sol = s.sol;
      }
      $("nl").textContent = s.N;
      var vals = s.view === "solution" ? last.u : last.err.grid.map(Math.abs);
      drawField($("f"), last, vals, colormap(s.cmap), c, compact || s.N <= 32);
      if (compact) {
        $("cap").textContent = (s.random ? "Random nonuniform" : "Uniform") + " " + s.N + " × " + s.N + " mesh · −Δu = 2π² sin(πx) sin(πy) · normₕ(uₕ − u) = " + fmt(last.err.normh);
        return;
      }
      $("ft").textContent = s.view === "solution" ? "uₕ(x, y)" : "|uₕ − u|(x, y)";
      $("t2").textContent = s.p2 === "spy" ? "Matrix sparsity" : "Mesh nodes";
      if (s.p2 === "spy") drawSpy($("c2"), last, c); else drawMesh($("c2"), last, c);
      $("dof").textContent = last.P.dof;
      $("nnz").textContent = last.P.nnz.length;
      $("l2").textContent = fmt(last.err.normh);
      $("h1").textContent = fmt(last.err.norm1h);
      $("k").textContent = fmt(conditionEstimate(last.cg.alphas, last.cg.betas));
      if (convRows) drawConvergence($("cv"), convRows, c);
    }
    root.addEventListener("input", function (e) { if (e.target.id === p + "mesh" || e.target.id === p + "sol") convRows = null, !compact && ($("cv").hidden = true); draw(); });
    if (!compact) {
      $("run").addEventListener("click", function () {
        var s = state();
        convRows = [8, 16, 32, 64].map(function (N) { return { N: N, e: solve(N, s.random, s.sol).err.normh }; });
        var a = convRows[convRows.length - 2], b = convRows[convRows.length - 1];
        var order = Math.log(a.e / b.e) / Math.log(b.N / a.N);
        $("cv").hidden = false;
        $("cvn").textContent = "Observed order between N = 32 and 64: " + order.toFixed(2) + " on a " + (s.random ? "random nonuniform" : "uniform") + " mesh. Second order holds even though the local truncation error on the nonuniform mesh is only first order: this is supraconvergence.";
        draw();
      });
    }
    if (window.ResizeObserver) new ResizeObserver(function () { draw(); }).observe(root);
    new MutationObserver(function () { requestAnimationFrame(draw); }).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
    if (window.matchMedia) window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", draw);
    draw();
    return { redraw: draw };
  }

  window.BramblePoisson = { mount: mount, solve: solve };
})();
