This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Jekyll-based academic/professional portfolio site (aaronzoll.github.io) using the Minimal Mistakes remote theme. It showcases research, teaching materials, and interactive math visualizations built with Desmos.
# Local development server with live reload
bundle exec jekyll serve
# Build for production
bundle exec jekyll build
# Install/update Ruby dependencies
bundle install
The site auto-deploys to GitHub Pages on push to main.
.markdown files — main site pages (index, about, research, teaching, projects, contact, CV)desmos/ — 18 individual interactive Desmos graph pages; use desmos/_template.md when adding new onesgallery.markdown — Gallery page (permalink /gallery/) linking all Desmos visualizations; styled to match the rest of the site. DesmosGallery.html is the legacy version, kept unlinked in case of rollback.widgets/ — standalone interactive research tools. Each is a two-file pair: a thin *.md page (layout: widget) plus the actual app in a self-contained HTML file it points at. See “Widgets” below._config.yml — site config: remote theme, author profile, plugins, MathJax/spaceship settings_data/navigation.yml — main nav links (Home, About, Research, Teaching, Desmos Gallery)_includes/mathjax-config.html — MathJax 3 setup; disables jekyll-spaceship math processors to avoid conflictsThe Minimal Mistakes theme is pulled as a remote theme — local files in _includes/, _layouts/, and _sass/ override the remote theme’s defaults.
Sass lives in _sass/minimal-mistakes/ and is compiled from assets/css/main.scss. Custom per-page styles are embedded in front matter or inline <style> blocks within the markdown.
desmos/ embeds a Desmos calculator. The preferred method is a JSON embed (downloaded from Desmos); the template also shows an iframe fallback.assets/particles.json, library at assets/js/particles.js._includes/mathjax-config.html. Pages must include `(function () { // theorem-like environment name -> [display name, isProof] var THM_ENVS = { theorem: [‘Theorem’, false], definition: [‘Definition’, false], proposition: [‘Proposition’, false], lemma: [‘Lemma’, false], corollary: [‘Corollary’, false], remark: [‘Remark’, false], example: [‘Example’, false], problem: [‘Problem’, false], algorithm: [‘Algorithm’, false], assumption: [‘Assumption’, false], proof: [‘Proof’, true], }; var DISPLAY_ENVS = [‘align’, ‘alignat’, ‘equation’, ‘gather’, ‘multline’, ‘flalign’, ‘split’]; var NUMBERED_NAMES = Object.keys(THM_ENVS).filter(function (n) { return !THM_ENVS[n][1]; }); var THM_ALT = NUMBERED_NAMES.join(‘|’);
function slugify(key) { return ‘latex-label-‘ + String(key).replace(/[^a-zA-Z0-9_-]+/g, ‘-‘); }
// ── algorithmic pseudocode (algpseudocode-style: \State, \If{}…\EndIf, etc.) ────── // Balanced-brace argument extraction: s[i] must be ‘{‘. Returns [innerText, indexAfterClosingBrace] // or null if unbalanced. Depth-aware so \frac{a}{b}-style nested braces inside a // condition don’t terminate early, and { / } (escaped, e.g. set-builder notation) // don’t affect depth. function extractArg(s, i) { if (s[i] !== ‘{‘) return null; var depth = 0; for (var j = i; j < s.length; j++) { var c = s[j]; if (c === ‘\’) { j++; continue; } if (c === ‘{‘) depth++; else if (c === ‘}’) { depth–; if (depth === 0) return [s.slice(i + 1, j), j + 1]; } } return null; } function skipWs(s, i) { while (i < s.length && /\s/.test(s[i])) i++; return i; }
var ALGO_ARG_CMDS = { If: 1, ElsIf: 1, ElseIf: 1, For: 1, ForAll: 1, While: 1, Until: 1, Comment: 1 }; var ALGO_CONTENT_CMDS = { State: 1, Require: 1, Ensure: 1, Return: 1, Print: 1 }; var ALGO_CMD_RE = /\(State|ElsIf|ElseIf|If|ForAll|For|EndFor|EndIf|While|EndWhile|Repeat|Until|EndFunction|Function|EndProcedure|Procedure|Return|Comment|Require|Ensure|Print)\b/g;
function tokenizeAlgorithmic(body) { var matches = [], m; ALGO_CMD_RE.lastIndex = 0; while ((m = ALGO_CMD_RE.exec(body))) matches.push({ cmd: m[1], start: m.index, after: m.index + m[0].length }); var tokens = []; for (var idx = 0; idx < matches.length; idx++) { var cur = matches[idx], cmd = cur.cmd, pos = cur.after; if (cmd === ‘Function’ || cmd === ‘Procedure’) { pos = skipWs(body, pos); var a1 = extractArg(body, pos), name = a1 ? a1[0] : ‘’; pos = a1 ? skipWs(body, a1[1]) : pos; var a2 = extractArg(body, pos), params = a2 ? a2[0] : ‘’; tokens.push({ cmd: cmd, name: name, params: params }); } else if (ALGO_ARG_CMDS[cmd]) { pos = skipWs(body, pos); var a = extractArg(body, pos); tokens.push({ cmd: cmd, arg: a ? a[0] : ‘’ }); } else if (ALGO_CONTENT_CMDS[cmd]) { var end = (idx + 1 < matches.length) ? matches[idx + 1].start : body.length; tokens.push({ cmd: cmd, content: body.slice(pos, end).trim() }); } else { tokens.push({ cmd: cmd }); // Else, EndIf, EndFor, EndWhile, Repeat, EndFunction, EndProcedure } } return tokens; }
// Text-mode macros algpseudocode allows outside explicit math mode, plus inline \Call{}{}.
function algoInlineText(s, inlineFormat) {
s = s.replace(/\Call{([^}])}{([^}])}/g, ‘$1($2)’);
s = s.replace(/\gets\b/g, ‘←’);
s = s.replace(/\to\b/g, ‘→’);
s = s.replace(/\land\b/g, ‘∧’);
s = s.replace(/\lor\b/g, ‘∨’);
s = s.replace(/\lnot\b|\neg\b/g, ‘¬’);
s = s.replace(/\True\b/g, ‘true’);
s = s.replace(/\False\b/g, ‘false’);
s = s.replace(/\text{([^}]*)}/g, ‘$1’);
return inlineFormat(s);
}
// Render tokens into indented, line-numbered pseudocode. \Comment{} attaches to the
// immediately preceding line instead of starting a new one, matching how it’s used
// in source (\State ... \Comment{...}).
function renderAlgoLines(tokens, inlineFormat) {
var out = [], depth = 0, lineNo = 0;
function emit(depthForLine, html) {
lineNo++;
out.push(‘<div class="algo-line" style="--algo-depth:' + depthForLine + '">’ +
‘’ + lineNo + ‘’ +
‘’ + html + ‘</div>’);
}
function attachComment(html) {
if (out.length) {
out[out.length - 1] = out[out.length - 1].replace(
‘</span></div>’, ‘ ▷ ‘ + html + ‘</span></div>’
);
} else {
emit(depth, ‘▷ ‘ + html + ‘’);
}
}
tokens.forEach(function (t) {
var kw = ‘’;
if (t.cmd === ‘State’) { emit(depth, algoInlineText(t.content, inlineFormat)); }
else if (t.cmd === ‘Require’) { emit(depth, kw + ‘Require: ‘ + algoInlineText(t.content, inlineFormat)); }
else if (t.cmd === ‘Ensure’) { emit(depth, kw + ‘Ensure:</span> ‘ + algoInlineText(t.content, inlineFormat)); }
else if (t.cmd === ‘Return’) { emit(depth, kw + ‘return</span> ‘ + algoInlineText(t.content, inlineFormat)); }
else if (t.cmd === ‘Print’) { emit(depth, kw + ‘print</span> ‘ + algoInlineText(t.content, inlineFormat)); }
else if (t.cmd === ‘Comment’) { attachComment(algoInlineText(t.arg, inlineFormat)); }
else if (t.cmd === ‘If’) { emit(depth, kw + ‘if</span> ‘ + algoInlineText(t.arg, inlineFormat) + ‘ ‘ + kw + ‘then</span>’); depth++; }
else if (t.cmd === ‘ElsIf’ || t.cmd === ‘ElseIf’) { depth–; emit(depth, kw + ‘else if</span> ‘ + algoInlineText(t.arg, inlineFormat) + ‘ ‘ + kw + ‘then</span>’); depth++; }
else if (t.cmd === ‘Else’) { depth–; emit(depth, kw + ‘else</span>’); depth++; }
else if (t.cmd === ‘EndIf’) { depth = Math.max(0, depth - 1); emit(depth, kw + ‘end if</span>’); }
else if (t.cmd === ‘For’) { emit(depth, kw + ‘for</span> ‘ + algoInlineText(t.arg, inlineFormat) + ‘ ‘ + kw + ‘do</span>’); depth++; }
else if (t.cmd === ‘ForAll’) { emit(depth, kw + ‘for all</span> ‘ + algoInlineText(t.arg, inlineFormat) + ‘ ‘ + kw + ‘do</span>’); depth++; }
else if (t.cmd === ‘EndFor’) { depth = Math.max(0, depth - 1); emit(depth, kw + ‘end for</span>’); }
else if (t.cmd === ‘While’) { emit(depth, kw + ‘while</span> ‘ + algoInlineText(t.arg, inlineFormat) + ‘ ‘ + kw + ‘do</span>’); depth++; }
else if (t.cmd === ‘EndWhile’){ depth = Math.max(0, depth - 1); emit(depth, kw + ‘end while</span>’); }
else if (t.cmd === ‘Repeat’) { emit(depth, kw + ‘repeat</span>’); depth++; }
else if (t.cmd === ‘Until’) { depth = Math.max(0, depth - 1); emit(depth, kw + ‘until</span> ‘ + algoInlineText(t.arg, inlineFormat)); }
else if (t.cmd === ‘Function’){ emit(depth, kw + ‘function</span> ' + t.name + '(' + t.params + ')’); depth++; }
else if (t.cmd === ‘EndFunction’) { depth = Math.max(0, depth - 1); emit(depth, kw + ‘end function</span>’); }
else if (t.cmd === ‘Procedure’) { emit(depth, kw + ‘procedure</span> ' + t.name + '(' + t.params + ')’); depth++; }
else if (t.cmd === ‘EndProcedure’){ depth = Math.max(0, depth - 1); emit(depth, kw + ‘end procedure</span>’); }
});
return ‘<div class="algo-pseudocode">’ + out.join(‘’) + ‘</div>’;
}
// Applied identically before both the Phase A scan and the Phase B render, so the
// theorem-env match count/order the two passes see is guaranteed identical (otherwise
// e.g. a stray thmtools {} artifact could shift phase B’s numbering out of sync
// with the labels phase A already recorded).
function normalize(h) {
h = h.replace(/``/g, ‘“’);
h = h.replace(/\begin{figure*?}[\s\S]?\end{figure*?}/g, ‘’);
h = h.replace(/\begin{tikzpicture}[\s\S]?\end{tikzpicture}/g, ‘’);
h = h.replace(/\begin{(\w+)}({}){1,2}/g, ‘\begin{$1}’);
return h;
}
// Read-only: find every \label{…} inside display-math source and record it as an // equation-type label (MathJax resolves these itself once we hand it the raw TeX). function scanEqLabels(raw, registry) { function harvest(body) { var lm, lre = /\label{([^}])}/g; while ((lm = lre.exec(body))) { registry.labels[lm[1]] = { type: ‘eq’ }; } } DISPLAY_ENVS.forEach(function (e) { [e, e + ‘’].forEach(function (env) { var esc = env.replace(‘’, ‘\’); var re = new RegExp(‘\\begin\{‘ + esc + ‘\}([\s\S]?)\\end\{‘ + esc + ‘\}’, ‘g’), m; while ((m = re.exec(raw))) harvest(m[1]); }); }); [/\[([\s\S]?)\]/g, /$$([\s\S]*?)$$/g].forEach(function (re) { var m; while ((m = re.exec(raw))) harvest(m[1]); }); }
// Read-only: find every theorem-like environment in document order, assign it the // next number in the page-wide shared counter, and (if labeled) register it. function scanThmLabels(raw, registry) { var re = new RegExp(‘\\begin\{(‘ + THM_ALT + ‘)\}(?:\[[^\]]\])?([\s\S]?)\\end\{\1\}’, ‘g’), m; while ((m = re.exec(raw))) { var name = m[1], body = m[2]; registry.thmCounter++; var lm = /\label{([^}]*)}/.exec(body); if (lm) { registry.labels[lm[1]] = { type: ‘thm’, name: THM_ENVS[name][0], num: registry.thmCounter, anchor: slugify(lm[1]) }; } } }
function scanLabels(blocks) { var registry = { labels: {}, thmCounter: 0 }; blocks.forEach(function (el) { var raw = normalize(el.innerHTML); scanEqLabels(raw, registry); scanThmLabels(raw, registry); }); return registry; }
function preprocessLatex(el, registry) { var h = normalize(el.innerHTML); var stash = [];
function save(s) { var i = stash.length; stash.push(s); return '\x00' + i + '\x00'; }
function restoreAll(s) {
// Iteratively restore in case stashed blocks contain other stash markers
var prev;
do {
prev = s;
s = s.replace(/\x00(\d+)\x00/g, function (_, i) { return stash[+i]; });
} while (s !== prev);
return s;
}
// 1. Stash and wrap standalone LaTeX display environments in \[...\]
// (labels inside survive untouched — MathJax needs to see them to number/resolve them)
DISPLAY_ENVS.forEach(function (e) {
[e, e + '*'].forEach(function (env) {
var esc = env.replace('*', '\\*');
h = h.replace(
new RegExp('\\\\begin\\{' + esc + '\\}([\\s\\S]*?)\\\\end\\{' + esc + '\\}', 'g'),
function (m) { return save('\\[' + m + '\\]'); }
);
});
});
// 2. Stash existing \[...\] and $$...$$ display math
h = h.replace(/\\\[([\s\S]*?)\\\]/g, function (m) { return save(m); });
h = h.replace(/\$\$([\s\S]*?)\$\$/g, function (m) { return save(m); });
// 3. Stash list environments (must precede theorem envs so lists inside them render)
// inlineFormat is a function declaration below and is hoisted, so safe to call here.
h = h.replace(/\\begin\{itemize\}([\s\S]*?)\\end\{itemize\}/g, function (_, c) {
var items = c.split(/\\item\b/).slice(1).map(function (s) {
return '<li>' + inlineFormat(s.replace(/^\s*\[[^\]]*\]\s*/, '').trim()) + '</li>';
});
return save('<ul>' + items.join('') + '</ul>');
});
h = h.replace(/\\begin\{enumerate\}(\[[^\]]*\])?([\s\S]*?)\\end\{enumerate\}/g, function (_, opt, c) {
var attr = '';
if (opt) {
var inner = opt.slice(1, -1).trim();
if (/^[a-hj-z][).]*$/.test(inner)) attr = ' type="a"';
else if (/^i[).]*$/.test(inner)) attr = ' type="i"';
else if (/^[A-HJ-Z][).]*$/.test(inner)) attr = ' type="A"';
else if (/^I[).]*$/.test(inner)) attr = ' type="I"';
}
var items = c.split(/\\item\b/).slice(1).map(function (s) {
return '<li>' + inlineFormat(s.replace(/^\s*\[[^\]]*\]\s*/, '').trim()) + '</li>';
});
return save('<ol' + attr + '>' + items.join('') + '</ol>');
});
function inlineFormat(s) {
s = s.replace(/\\textbf\{([^}]*)\}/g, '<strong>$1</strong>');
s = s.replace(/\\textit\{([^}]*)\}/g, '<em>$1</em>');
s = s.replace(/\\emph\{([^}]*)\}/g, '<em>$1</em>');
s = s.replace(/\\texttt\{([^}]*)\}/g, '<code>$1</code>');
s = s.replace(/\\underline\{([^}]*)\}/g, '<u>$1</u>');
return s;
}
// \begin{algorithm} bodies get pseudocode formatting instead of the generic
// inlineFormat treatment: pull out \caption{}, then render the nested
// \begin{algorithmic}...\end{algorithmic} block (falling back to plain text if
// there isn't one — e.g. algorithm2e-style input).
function renderAlgorithmBody(body) {
var caption = '';
var capM = /\\caption\{/.exec(body);
if (capM) {
var capArg = extractArg(body, capM.index + capM[0].length - 1);
if (capArg) {
caption = inlineFormat(capArg[0]);
body = body.slice(0, capM.index) + body.slice(capArg[1]);
}
}
var algoM = /\\begin\{algorithmic\}(?:\[[^\]]*\])?([\s\S]*?)\\end\{algorithmic\}/.exec(body);
var codeHtml = algoM
? renderAlgoLines(tokenizeAlgorithmic(algoM[1]), inlineFormat)
: ' ' + inlineFormat(body).trim();
return { caption: caption, codeHtml: codeHtml };
}
// 4. Convert theorem environments to HTML (numbered from the shared registry, same
// document-order pass as scanThmLabels so the numbers line up), then stash.
// \label is stripped from the body — it's now baked into the title + anchor id.
var thmRe = new RegExp('\\\\begin\\{(' + THM_ALT + ')\\}(\\[([^\\]]*)\\])?([\\s\\S]*?)\\\\end\\{\\1\\}', 'g');
h = h.replace(thmRe, function (_, name, _br, opt, body) {
registry.thmCounter++;
var num = registry.thmCounter;
var lm = /\\label\{([^}]*)\}/.exec(body);
var anchor = lm ? slugify(lm[1]) : 'latex-thm-' + num;
body = body.replace(/\\label\{[^}]*\}/g, '');
var label = THM_ENVS[name][0];
var html;
if (name === 'algorithm') {
var parts = renderAlgorithmBody(body);
var t = label + ' ' + num + (parts.caption ? ': ' + parts.caption : '') + (opt ? ' (' + opt + ')' : '');
html = '<div class="math-env math-env-algorithm" id="' + anchor + '">' +
'<span class="math-env-title">' + t + '</span>' + parts.codeHtml + '</div>';
} else {
var body2 = inlineFormat(body);
var t2 = label + ' ' + num + (opt ? ' (' + opt + ')' : '');
html = '<div class="math-env math-env-' + name + '" id="' + anchor + '">' +
'<span class="math-env-title">' + t2 + '.</span> ' +
body2 + '</div>';
}
return save(html);
});
// Proof (unnumbered, not part of the shared counter)
h = h.replace(/\\begin\{proof\}(\[([^\]]*)\])?([\s\S]*?)\\end\{proof\}/g, function (_, _br, opt, body) {
var cleaned = inlineFormat(body)
.replace(/\\qed\b/g, '')
.replace(/\\hfill\s*\$\\square\$\s*/g, '');
var title = opt ? 'Proof (' + opt + ').' : 'Proof.';
var html = '<div class="math-proof"><span class="math-env-title">' + title + '</span> ' +
cleaned +
'<div class="proof-end">$\\square$</div></div>';
return save(html);
});
// 5. Stash section headings
h = h.replace(/\\section\*?\{([^}]*)\}/g, function (_, t) { return save('<h2>' + t + '</h2>'); });
h = h.replace(/\\subsection\*?\{([^}]*)\}/g, function (_, t) { return save('<h3>' + t + '</h3>'); });
h = h.replace(/\\subsubsection\*?\{([^}]*)\}/g, function (_, t) { return save('<h4>' + t + '</h4>'); });
// 6. Inline text formatting (simple, non-nested arguments)
h = h.replace(/\\textbf\{([^}]*)\}/g, '<strong>$1</strong>');
h = h.replace(/\\textit\{([^}]*)\}/g, '<em>$1</em>');
h = h.replace(/\\emph\{([^}]*)\}/g, '<em>$1</em>');
h = h.replace(/\\texttt\{([^}]*)\}/g, '<code>$1</code>');
h = h.replace(/\\underline\{([^}]*)\}/g, '<u>$1</u>');
h = h.replace(/\\medskip\b/g, '<div style="margin:0.6rem 0"></div>');
h = h.replace(/\\bigskip\b/g, '<div style="margin:1.2rem 0"></div>');
h = h.replace(/\\smallskip\b/g,'<div style="margin:0.3rem 0"></div>');
h = h.replace(/\\noindent\b/g, '');
h = h.replace(/\\newline\b|\\\\(?!\[)/g, '<br>');
// 7. Resolve cross-references and citations using the page-wide registry.
// A stray \label{} not inside a display-math or theorem env has no numbered
// target, so (as before) it's just dropped.
h = h.replace(/\\label\{[^}]*\}/g, '');
function brokenRef(key) {
return '<span class="latex-ref-broken" title="undefined label: ' + key + '">??</span>';
}
// \eqref{} — if it targets a real equation label, hand it to MathJax verbatim (native
// numbering + hyperlink); otherwise fall through to the same resolution as \ref.
h = h.replace(/\\eqref\{([^}]*)\}/g, function (m, key) {
var e = registry.labels[key];
if (e && e.type === 'eq') return '\\(\\eqref{' + key + '}\\)';
if (e && e.type === 'thm') return '<a class="latex-ref" href="#' + e.anchor + '">' + e.name + ' ' + e.num + '</a>';
return brokenRef(key);
});
// \ref{} / \Cref{} / \cref{} — equation labels delegate to MathJax; theorem labels
// resolve to a clickable, numbered reference (\Cref/\cref also prefix the env name).
h = h.replace(/\\(Cref|cref|ref)\{([^}]*)\}/g, function (m, cmd, key) {
var e = registry.labels[key];
if (!e) return brokenRef(key);
if (e.type === 'eq') return '\\(\\ref{' + key + '}\\)';
var name = cmd === 'Cref' ? e.name : (cmd === 'cref' ? e.name.toLowerCase() : null);
var text = name ? (name + ' ' + e.num) : String(e.num);
return '<a class="latex-ref" href="#' + e.anchor + '">' + text + '</a>';
});
// \cite{} / \citep{} / \citet{} — no bibliography is configured on this site, so
// render the citation key(s) as a bracketed, visibly-distinct marker rather than
// letting the raw command leak through or silently vanish.
h = h.replace(/\\cite[pt]?\{([^}]*)\}/g, function (m, keys) {
var list = keys.split(',').map(function (k) { return k.trim(); }).join(', ');
return '<span class="latex-cite" title="citation key(s), no bibliography configured">[' + list + ']</span>';
});
// 8. Paragraph wrapping: split on blank lines
var chunks = h.split(/\n{2,}/);
h = chunks.map(function (chunk) {
chunk = chunk.trim();
if (!chunk) return '';
// Chunk that is purely a stash marker — restore as-is (block element)
if (/^\x00\d+\x00$/.test(chunk)) return chunk;
return '<p>' + chunk + '</p>';
}).filter(Boolean).join('\n');
el.innerHTML = restoreAll(h); }
window._preprocessAllLatex = function () { var blocks = Array.prototype.slice.call(document.querySelectorAll(‘.latex-body’)); if (!blocks.length) return; var registry = scanLabels(blocks); // Phase A left thmCounter at the page-wide total while building the label->number // map; reset it so Phase B recounts from 1, retracing the identical sequence of // theorem-env matches (same content, same order) and landing on the same numbers. registry.thmCounter = 0; blocks.forEach(function (el) { preprocessLatex(el, registry); }); }; })();
// ── MathJax 3 configuration ──────────────────────────────────────────────────
window.MathJax = {
tex: {
inlineMath: [[’$’, ‘$’], [’\(‘, ‘\)’]],
displayMath: [[’\(', '\)’], [’\[’, ‘\]’]],
tags: ‘ams’,
macros: {
// ── Trace / matrix ops ──────────────────────────────────────────────
tr: ‘\mathrm{Tr}’,
trace: ‘\mathrm{Tr}’,
vect: ‘\mathbf{vec}’,
bvec: [’{\mathbf{#1}}’, 1],
matrx: [’\begin{bmatrix}#1\end{bmatrix}’, 1],
diag: ‘\operatorname{diag}’,
Diag: ‘\operatorname{Diag}’,
rank: ‘\operatorname{rank}’,
sign: ‘\operatorname{sign}’,
// ── Calligraphic / script ────────────────────────────────────────────
cI: ‘\mathcal{I}’,
cX: ‘\mathcal{X}’,
cB: ‘\mathcal{B}’,
cE: ‘\mathcal{E}’,
cA: ‘\mathcal{A}’,
cU: ‘\mathcal{U}’,
cS: ‘\mathcal{S}’,
Lcal: ‘\mathcal{L}’,
Fscr: ‘\mathscr{F}’,
// ── Blackboard bold ──────────────────────────────────────────────────
F: ‘\mathbb{F}’,
C: ‘\mathbb{C}’,
R: ‘\mathbb{R}’,
N: ‘\mathbb{N}’,
NN: ‘\mathbb{N}’,
ZZ: ‘\mathbb{Z}’,
Q: ‘\mathbb{Q}’,
EE: ‘\mathbb{E}’,
Expect: ‘\mathbb{E}’,
// ── Linear algebra / analysis operators ──────────────────────────────
Dim: ‘\operatorname{dim}’,
spann: ‘\operatorname{span}’,
im: ‘\operatorname{Im}’,
gph: ‘\operatorname{gph}’,
supp: ‘\operatorname{supp}’,
lip: ‘\operatorname{lip}’,
var: ‘\operatorname{var}’,
Var: ‘\operatorname{Var}’,
op: ‘\mathrm{op}’,
dist: ‘{\mathbf{dist}}’,
proj: ‘\operatorname{proj}’,
prox: ‘\operatorname{prox}’,
St: ‘\text{subject to}’,
// ── Delimiters / paired macros ───────────────────────────────────────
norm: [’\left\|#1\right\|’, 1],
opnorm: [’\left\|#1\right\|_{\mathrm{op}}’, 1],
abs: [’\left|#1\right|’, 1],
inner: [’\left\langle#1,\,#2\right\rangle’, 2],
Prob: [’\mathbb{P}\!\left(#1\right)’, 1],
Expec: [’\mathbb{E}\!\left(#1\right)’, 1],
dom: [’\operatorname{dom}(#1)’, 1],
// ── Convex analysis ──────────────────────────────────────────────────
epi: ‘\operatorname{epi}’,
hypo: ‘\operatorname{hypo}’,
interior: ‘\operatorname{int}’,
bdry: ‘\operatorname{bdy}’,
relint: ‘\operatorname{ri}’,
argmin: ‘\operatorname{argmin}’,
argmax: ‘\operatorname{argmax}’,
mini: ‘\operatorname{minimize}’,
ls: ‘\operatorname{limsup}’,
// ── Misc ─────────────────────────────────────────────────────────────
bcdot: ‘\ \mathbf{\cdot}\ ‘,
eps: ‘\varepsilon’,
Holder: ‘\text{Hölder}’,
},
},
options: {
skipHtmlTags: [‘script’, ‘noscript’, ‘style’, ‘textarea’, ‘pre’, ‘code’],
},
startup: {
ready() {
function doPreprocess() {
window._preprocessAllLatex();
MathJax.startup.defaultReady();
}
if (document.readyState === ‘loading’) {
document.addEventListener(‘DOMContentLoaded’, doPreprocess);
} else {
doPreprocess();
}
},
},
};
</script>
` in their front matter header or body.
Standalone interactive tools live at /widgets/<name>/ and are built from two pieces:
The page — widgets/<name>.md with layout: widget and no body markup beyond controls and the write-up. Front matter:
layout: widget
title: "Human-readable title"
widget_src: "/widgets/<name>_code.html" # or /assets/tools/<name>.html
widget_height: 860 # desktop iframe height, px
widget_height_mobile: 680 # ≤700px viewport
widget_fixed_height: true # only for apps that fill their frame
back_url: "/research"
back_label: "Research"
_layouts/widget.html supplies the title row, back link, “jump to write-up” link, the wood frame around the iframe, and the hairline-separated write-up section. It splits page content on an <!--writeup--> marker: everything before it renders directly under the iframe (used for host-page controls, as in widgets/polytope.md), everything after is the prose write-up. Omit the marker entirely when the page is only prose. Unless widget_fixed_height is set, the layout listens for postMessage({embedHeight}) from the iframe and resizes to fit.
The app — a self-contained HTML file (the iframe contents). It must not draw its own outer frame; the layout already provides one.
All widget apps share one look, so they read as instruments from the same shop: warm off-white ground, slate ink, hairline rules, tiny uppercase monospace labels, one slate accent, no heavy chrome. It is codified in assets/css/themes/widget-theme.css — link it and write only tool-specific rules:
<link rel="stylesheet" href="/assets/css/themes/widget-theme.css">
Use its class API so themes stay swappable: .w-plate (container), .w-bar / .w-group / .w-label / .w-val (control bar), .w-rule, .w-hint, .w-stage / .w-figure (the framed canvas), .w-scrim / .w-sheet (modal). Buttons, sliders, and text inputs are styled by element; express toggle state with aria-pressed="true".
Alternate theme: assets/css/themes/widget-theme-chalk-wood.css is Chalk Studio’s original wood/cream look (cream toolbar, brown rules, ridged wood board edge, system sans), preserved as a drop-in. It imports the base file and only overrides tokens, so swapping the <link> href re-skins a widget with no markup changes — the route to take if the house style should move that way instead.
widgets/polytope_code.html and widgets/optimal-couch_code.html predate the shared file and still inline the same palette; they’re the visual reference, but new widgets should link the stylesheet rather than copy it.
Most content pages use the splash layout with a hero header block (overlay color + image from picsum.photos). Research and teaching pages contain embedded HTML/CSS for custom grid layouts and slide presentations.
assets/ subdirectoriesassets/images/desmos/_template.md and linked from both gallery.markdown and teaching.markdownwidgets/, use layout: widget and the widget theme (see “Widgets”), and are listed in widgets.markdown with a thumbnail in widgets/images/_site/ directory is the generated build output — do not edit files there directly