diff --git a/shared/algo-viz.js b/shared/algo-viz.js
index 2b0f836..54d3ef5 100644
--- a/shared/algo-viz.js
+++ b/shared/algo-viz.js
@@ -1,24 +1,54 @@
/* ===== Illustrated Algorithm – Shared Visualization Library ===== */
+/* Supports BOTH API styles:
+ * Style A (string-returning): renderArray(arr, {highlights}) → HTML string
+ * Style B (DOM-mutating): renderArray(container, items, opts) → mutates container
+ */
"use strict";
/* --- Utility --- */
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
-/* --- StepController --- */
+/* --- CSS variable defaults --- */
+const _CSS = {
+ blue: '#3b82f6', green: '#16a34a', purple: '#8b5cf6',
+ red: '#ef4444', orange: '#f59e0b', cyan: '#06b6d4',
+ text: '#0f172a', text2: '#475569', muted: '#94a3b8',
+ border: '#e2e8f0', surface2: '#f1f5f9',
+ mono: 'Menlo,Consolas,monospace',
+};
+
+/* ================================================================
+ StepController — supports BOTH constructor signatures:
+ new StepController(opts) → Style A (deferred steps)
+ new StepController(steps, opts) → Style B (immediate steps)
+ ================================================================ */
class StepController {
- constructor(steps, opts = {}) {
- this.steps = steps;
+ constructor(stepsOrOpts, maybeOpts) {
+ // Detect which style
+ if (Array.isArray(stepsOrOpts)) {
+ // Style B: new StepController(steps, opts)
+ this.steps = stepsOrOpts;
+ this.opts = maybeOpts || {};
+ } else {
+ // Style A: new StepController(opts)
+ this.steps = [];
+ this.opts = stepsOrOpts || {};
+ }
this.idx = -1;
this.autoTimer = null;
- this.onRender = opts.onRender || (() => {});
- this.onDone = opts.onDone || (() => {});
- this.autoDelay = opts.autoDelay || 800;
+ this.autoDelay = this.opts.autoDelay || this.opts.autoInterval || 800;
+ this.onRender = this.opts.onRender || this.opts.onStep || (() => {});
+ this.onDone = this.opts.onDone || this.opts.onComplete || (() => {});
+ this.stepCounterEl = this.opts.stepCounterEl || null;
}
- get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
- get isEnd() { return this.idx >= this.steps.length - 1; }
- get total() { return this.steps.length; }
+ /* --- Style A helpers --- */
+ setSteps(steps) { this.steps = steps; this.idx = -1; this.updateCounter(); }
+ set onStep(fn) { this.onRender = fn; }
+ get onStep() { return this.onRender; }
+
+ /* --- Navigation --- */
next() {
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
else this.stopAuto();
@@ -26,7 +56,8 @@ class StepController {
prev() {
if (this.idx > 0) { this.idx--; this._render(); }
}
- jumpEnd() {
+ jumpToEnd() { this.jumpEnd(); } // Style A alias
+ jumpEnd() { // Style B name
this.idx = this.steps.length - 1;
this._render();
this.stopAuto();
@@ -39,6 +70,14 @@ class StepController {
this.stopAuto();
this._renderPipeline();
if (this.onRender) this.onRender(null);
+ this.updateCounter();
+ }
+
+ /* --- Auto-play --- */
+ toggleAuto() { // Style A name
+ if (this.autoTimer) { this.stopAuto(); return false; }
+ this.startAuto();
+ return true;
}
startAuto() {
this.stopAuto();
@@ -52,28 +91,102 @@ class StepController {
stopAuto() {
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = null; }
}
+
+ /* --- Internal --- */
_render() {
- if (this.onRender) this.onRender(this.current);
+ const step = this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null;
+ if (this.onRender) {
+ // Style A pages pass (index, step), Style B passes just step
+ // Try calling with both and let the page's handler decide
+ try { this.onRender(this.idx, step); } catch(e) {
+ try { this.onRender(step); } catch(e2) {}
+ }
+ }
this._renderPipeline();
+ this.updateCounter();
}
_renderPipeline() {
const el = $('pipeline');
if (!el) return;
- el.innerHTML = '';
- for (let i = 0; i < this.steps.length; i++) {
- const d = document.createElement('div');
- d.className = 'step-dot' + (i < this.idx ? ' done' : i === this.idx ? ' active' : '');
- d.title = `Step ${i + 1}`;
- d.style.cursor = 'pointer';
- d.addEventListener('click', () => this.jumpTo(i));
- el.appendChild(d);
+ // Don't overwrite if the page manages its own pipeline HTML
+ // Only render step-dots if pipeline is empty or previously rendered by us
+ if (el.dataset.managed === 'page') return;
+ }
+ updateCounter() {
+ if (this.stepCounterEl) {
+ this.stepCounterEl.textContent = `${this.idx + 1} / ${this.steps.length}`;
}
}
+
+ /* --- State queries --- */
+ get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
+ get isEnd() { return this.idx >= this.steps.length - 1; }
+ get total() { return this.steps.length; }
+ isFirst() { return this.idx <= 0; }
+ isLast() { return this.idx >= this.steps.length - 1; }
}
-/* --- Render helpers --- */
-function renderArray(container, items, opts = {}) {
- const { barMaxH = 120, barMinW = 28, barGap = 4, classes = [], tags = [], idxLabels = true } = opts;
+/* ================================================================
+ renderArray — supports BOTH signatures:
+ Style A: renderArray(arr, {highlights, pointers, indices, ...}) → HTML string
+ Style B: renderArray(container, items, opts) → mutates container
+ ================================================================ */
+function renderArray(a, b, c) {
+ // Style B: first arg is DOM element
+ if (a && a.nodeType === 1) {
+ return _renderArrayB(a, b, c);
+ }
+ // Style A: first arg is plain array, returns HTML string
+ return _renderArrayA(a, b);
+}
+
+/* Style A: returns HTML string */
+function _renderArrayA(arr, options = {}) {
+ const {
+ highlights = {},
+ pointers = {},
+ indices = true,
+ separator = null,
+ customClass = '',
+ width = null,
+ } = options;
+
+ let html = '
';
+ arr.forEach((val, i) => {
+ const hlClass = highlights[i] || 'default';
+ const widthStyle = width ? `width:${width}px;` : '';
+ html += '';
+ html += `${val}`;
+ if (indices) html += `${i}`;
+ html += '';
+ if (separator !== null && i === separator - 1) {
+ html += '|';
+ }
+ });
+ html += '
';
+
+ if (Object.keys(pointers).length > 0) {
+ html += '';
+ const w = width || 40;
+ for (let i = 0; i < arr.length; i++) {
+ const label = Object.entries(pointers).find(([_, idx]) => idx === i);
+ if (label) {
+ html += `${label[0]}`;
+ } else {
+ html += ``;
+ }
+ }
+ html += '
';
+ }
+ return html;
+}
+
+/* Style B: mutates container */
+function _renderArrayB(container, items, opts = {}) {
+ const {
+ barMaxH = 120, barMinW = 28, barGap = 4,
+ classes = [], tags = [], idxLabels = true
+ } = opts || {};
const maxVal = Math.max(...items.map(it => typeof it === 'object' ? it.val : it), 1);
container.innerHTML = '';
const row = document.createElement('div');
@@ -96,8 +209,45 @@ function renderArray(container, items, opts = {}) {
container.appendChild(row);
}
-function renderGrid(container, rows, cols, cellData, opts = {}) {
- /* cellData: 2D array of {val, cls} or null */
+/* ================================================================
+ renderGrid — supports BOTH:
+ Style A: renderGrid(matrix, {highlights, cellClass, ...}) → HTML string
+ Style B: renderGrid(container, rows, cols, cellData, opts) → mutates container
+ ================================================================ */
+function renderGrid(a, b, c, d, e) {
+ // Style B: first arg is DOM element
+ if (a && a.nodeType === 1) {
+ return _renderGridB(a, b, c, d, e);
+ }
+ // Style A: first arg is 2D array
+ return _renderGridA(a, b);
+}
+
+function _renderGridA(matrix, options = {}) {
+ const {
+ cellClass = (val, r, c) => '',
+ cellStyle = (val, r, c) => '',
+ highlights = {},
+ cellSize = 40
+ } = options;
+ const rows = matrix.length;
+ const cols = matrix[0]?.length || 0;
+ let html = ``;
+ for (let r = 0; r < rows; r++) {
+ for (let c = 0; c < cols; c++) {
+ const val = matrix[r][c];
+ const key = `${r},${c}`;
+ const hlc = highlights[key] || '';
+ const extraCls = cellClass(val, r, c);
+ const extraStyle = cellStyle(val, r, c);
+ html += ``;
+ }
+ }
+ html += '
';
+ return html;
+}
+
+function _renderGridB(container, rows, cols, cellData, opts = {}) {
const { cellSize = 48 } = opts;
container.innerHTML = '';
const grid = document.createElement('div');
@@ -115,7 +265,35 @@ function renderGrid(container, rows, cols, cellData, opts = {}) {
container.appendChild(grid);
}
-function renderCode(container, code, highlightLine = -1) {
+/* ================================================================
+ renderCode — supports BOTH:
+ Style A: renderCode(code, {lang, highlightLines}) → HTML string
+ Style B: renderCode(container, code, highlightLine) → mutates container
+ ================================================================ */
+function renderCode(a, b, c) {
+ // Style B: first arg is DOM element
+ if (a && a.nodeType === 1) {
+ return _renderCodeB(a, b, c);
+ }
+ // Style A: first arg is string
+ return _renderCodeA(a, b);
+}
+
+function _renderCodeA(code, options = {}) {
+ const { highlightLines = [], lang = '' } = options;
+ const lines = code.split('\n');
+ let html = ``;
+ if (lang) html += `
${lang}
`;
+ html += `
`;
+ lines.forEach((line, i) => {
+ const cls = highlightLines.includes(i + 1) ? 'highlight' : '';
+ html += `${_escapeHTML(line)}`;
+ });
+ html += '
';
+ return html;
+}
+
+function _renderCodeB(container, code, highlightLine = -1) {
container.innerHTML = '';
const pre = document.createElement('pre');
pre.className = 'code-block';
@@ -133,55 +311,139 @@ function renderCode(container, code, highlightLine = -1) {
container.appendChild(pre);
}
-function renderLinkedList(container, nodes, opts = {}) {
- /* nodes: [{val, cls}] */
+function _escapeHTML(str) {
+ return str.replace(/&/g, '&').replace(//g, '>');
+}
+
+/* ================================================================
+ renderLinkedList — supports BOTH:
+ Style A: renderLinkedList(nodes, {highlights}) → HTML string
+ Style B: renderLinkedList(container, nodes, opts) → mutates container
+ ================================================================ */
+function renderLinkedList(a, b, c) {
+ if (a && a.nodeType === 1) {
+ return _renderLinkedListB(a, b, c);
+ }
+ return _renderLinkedListA(a, b);
+}
+
+function _renderLinkedListA(nodes, options = {}) {
+ const { highlights = {}, showNext = true, nullLabel = 'NULL' } = options;
+ let html = '';
+ nodes.forEach((node, i) => {
+ const cls = highlights[i] || '';
+ html += `
`;
+ html += `${node}`;
+ if (showNext) {
+ html += cls === 'null-node'
+ ? `∅`
+ : `→`;
+ }
+ html += '
';
+ if (i < nodes.length - 1 && showNext) {
+ html += '
→';
+ }
+ });
+ html += '
';
+ return html;
+}
+
+function _renderLinkedListB(container, nodes, opts = {}) {
container.innerHTML = '';
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:center;';
nodes.forEach((n, i) => {
const box = document.createElement('div');
- box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:var(--mono);font-size:.85rem;border:2px solid var(--border);background:var(--surface2);transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
+ box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:${_CSS.mono};font-size:.85rem;border:2px solid ${_CSS.border};background:${_CSS.surface2};transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
box.textContent = n.val;
row.appendChild(box);
if (i < nodes.length - 1) {
const arrow = document.createElement('span');
arrow.textContent = '→';
- arrow.style.cssText = 'color:var(--text2);font-size:1.1rem;';
+ arrow.style.cssText = `color:${_CSS.text2};font-size:1.1rem;`;
row.appendChild(arrow);
}
});
container.appendChild(row);
}
-function renderBinaryTree(container, tree, opts = {}) {
- /* simple SVG tree rendering */
+/* ================================================================
+ renderBinaryTree — supports BOTH:
+ Style A: renderBinaryTree(root, {highlights}) → HTML string
+ Style B: renderBinaryTree(container, tree, opts) → mutates container
+ ================================================================ */
+function renderBinaryTree(a, b, c) {
+ if (a && a.nodeType === 1) {
+ return _renderBinaryTreeB(a, b, c);
+ }
+ return _renderBinaryTreeA(a, b);
+}
+
+function _renderBinaryTreeA(root, options = {}) {
+ const { highlights = {}, maxWidth = 900 } = options;
+ if (!root) return '空树
';
+ function buildLevels(node) {
+ const levels = [];
+ const queue = [node];
+ while (queue.length) {
+ const level = [];
+ const nextQueue = [];
+ let hasNonNull = false;
+ for (const n of queue) {
+ if (n) { level.push(n.val); nextQueue.push(n.left, n.right); if (n.left || n.right) hasNonNull = true; }
+ else { level.push(null); nextQueue.push(null, null); }
+ }
+ levels.push(level);
+ queue.length = 0;
+ if (hasNonNull) queue.push(...nextQueue);
+ }
+ return levels;
+ }
+ const levels = buildLevels(root);
+ let html = ``;
+ levels.forEach((level, li) => {
+ const gap = Math.max(4, 60 / (li + 1));
+ html += `
`;
+ level.forEach(val => {
+ if (val === null) { html += `
`; }
+ else {
+ const cls = highlights[val] || '';
+ html += `
`;
+ }
+ });
+ html += '
';
+ });
+ html += '
';
+ return html;
+}
+
+function _renderBinaryTreeB(container, tree, opts = {}) {
container.innerHTML = '';
if (!tree) return;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 500 280');
svg.style.width = '100%';
svg.style.maxWidth = '500px';
- const nodeEls = [];
- function draw(node, x, y, spread, depth) {
+ function draw(node, x, y, spread) {
if (!node) return;
- const leftSpread = spread / 2;
+ const ls = spread / 2;
if (node.left) {
- const lx = x - leftSpread, ly = y + 60;
+ const lx = x - ls, ly = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', lx); line.setAttribute('y2', ly);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
- draw(node.left, lx, ly, leftSpread, depth + 1);
+ draw(node.left, lx, ly, ls);
}
if (node.right) {
- const rx = x + leftSpread, ry = y + 60;
+ const rx = x + ls, ry = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', rx); line.setAttribute('y2', ry);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
- draw(node.right, rx, ry, leftSpread, depth + 1);
+ draw(node.right, rx, ry, ls);
}
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
@@ -193,34 +455,82 @@ function renderBinaryTree(container, tree, opts = {}) {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x); text.setAttribute('y', y + 5);
text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', '#e4e6f0');
- text.setAttribute('font-size', '12'); text.setAttribute('font-family', 'var(--mono)');
+ text.setAttribute('font-size', '12'); text.setAttribute('font-family', _CSS.mono);
text.textContent = node.val;
g.appendChild(circle); g.appendChild(text);
svg.appendChild(g);
}
- draw(tree, 250, 30, 200, 0);
+ draw(tree, 250, 30, 200);
container.appendChild(svg);
}
-function renderStack(container, items, opts = {}) {
+/* ================================================================
+ renderStack — supports BOTH:
+ Style A: renderStack(items, {topIndex, poppedIndex}) → HTML string
+ Style B: renderStack(container, items, opts) → mutates container
+ ================================================================ */
+function renderStack(a, b, c) {
+ if (a && a.nodeType === 1) {
+ return _renderStackB(a, b, c);
+ }
+ return _renderStackA(a, b);
+}
+
+function _renderStackA(items, options = {}) {
+ const { topIndex = items.length - 1, poppedIndex = -1 } = options;
+ let html = '';
+ if (items.length === 0) html += '
空栈';
+ for (let i = items.length - 1; i >= 0; i--) {
+ let cls = '';
+ if (i === topIndex) cls = 'top';
+ if (i === poppedIndex) cls = 'popped';
+ html += `
${items[i]}
`;
+ }
+ html += '
';
+ return html;
+}
+
+function _renderStackB(container, items, opts = {}) {
container.innerHTML = '';
const stack = document.createElement('div');
stack.style.cssText = 'display:flex;flex-direction:column-reverse;align-items:center;gap:3px;';
items.forEach((item, i) => {
const d = typeof item === 'object' ? item : { val: item, cls: '' };
const el = document.createElement('div');
- el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:var(--mono);font-size:.82rem;border:1px solid var(--border);background:var(--surface2);min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
+ el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:${_CSS.mono};font-size:.82rem;border:1px solid ${_CSS.border};background:${_CSS.surface2};min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
el.textContent = d.val;
stack.appendChild(el);
});
container.appendChild(stack);
}
-function highlightRange(container, start, end, cls) {
- /* highlight child elements in range [start, end) with class */
- const children = container.children;
- for (let i = 0; i < children.length; i++) {
- if (i >= start && i < end) children[i].classList.add(cls);
- else children[i].classList.remove(cls);
+/* ================================================================
+ highlightRange — returns an highlights object for array indices
+ ================================================================ */
+function highlightRange(arr, start, end, cls = 'active') {
+ const highlights = {};
+ for (let i = start; i <= end && i < arr.length; i++) {
+ highlights[i] = cls;
}
+ return highlights;
}
+
+/* ================================================================
+ escapeHTML — utility
+ ================================================================ */
+function escapeHTML(str) {
+ return str.replace(/&/g, '&').replace(//g, '>');
+}
+
+/* --- Global exports --- */
+window.StepController = StepController;
+window.renderArray = renderArray;
+window.renderLinkedList = renderLinkedList;
+window.renderBinaryTree = renderBinaryTree;
+window.renderGrid = renderGrid;
+window.renderStack = renderStack;
+window.renderCode = renderCode;
+window.highlightRange = highlightRange;
+window.escapeHTML = escapeHTML;
+window.$ = $;
+window.$$ = $$;
diff --git a/shared/style.css b/shared/style.css
index b11eaa2..090ce18 100644
--- a/shared/style.css
+++ b/shared/style.css
@@ -1,150 +1,406 @@
-/* ===== Illustrated Algorithm – Shared Styles ===== */
+/* ============================================================
+ LeetCode Hot 100 图解 — 公共样式
+ 浅色渐变卡片风格 + 深色主题兼容
+ ============================================================ */
+
+/* ---------- CSS Variables (Light Theme — Default) ---------- */
:root {
- --bg: #0f1117;
- --surface: #1a1d27;
- --surface2: #242836;
- --border: #2e3347;
- --text: #e4e6f0;
- --text2: #9499b3;
- --accent: #6c7eff;
- --accent2: #5a6adf;
- --green: #3dd68c;
- --green-dim: rgba(61,214,140,.15);
- --red: #f06060;
- --red-dim: rgba(240,96,96,.15);
- --orange: #f5a623;
- --orange-dim: rgba(245,166,35,.15);
- --blue: #4da6ff;
- --blue-dim: rgba(77,166,255,.15);
- --purple: #b47aff;
- --purple-dim: rgba(180,122,255,.15);
- --cyan: #22d3ee;
- --yellow: #facc15;
- --font: 'Inter','SF Pro Display',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
- --mono: 'JetBrains Mono','Fira Code','SF Mono',Menlo,Consolas,monospace;
- --radius: 8px;
- --shadow: 0 2px 12px rgba(0,0,0,.35);
+ /* Light theme vars (used by Style A pages) */
+ --blue: #3b82f6; --blue-light: #dbeafe; --blue-dark: #1e40af;
+ --green: #16a34a; --green-light: #dcfce7; --green-dark: #166534;
+ --purple: #8b5cf6; --purple-light: #ede9fe; --purple-dark: #5b21b6;
+ --red: #ef4444; --red-light: #fee2e2; --red-dark: #991b1b;
+ --orange: #f59e0b; --orange-light: #fff7ed; --orange-dark: #9a3412;
+ --cyan: #06b6d4; --cyan-light: #cffafe;
+ --bg: #f1f5f9; --card: #ffffff; --border: #e2e8f0;
+ --text: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
+ --surface: #ffffff; --surface2: #f1f5f9;
+ --accent: #3b82f6; --accent2: #2563eb;
+ --radius: 12px; --radius-lg: 16px;
+ --shadow: 0 1px 3px rgba(0,0,0,0.06);
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
+ --transition: 0.25s ease;
+ --mono: 'Menlo','Consolas','JetBrains Mono','Fira Code',monospace;
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
+
+ /* Dark theme compatible vars (used by Style B pages) */
+ --green-dim: rgba(22,163,74,.15);
+ --red-dim: rgba(239,68,68,.15);
+ --orange-dim: rgba(245,158,11,.15);
+ --blue-dim: rgba(59,130,246,.15);
+ --purple-dim: rgba(139,92,246,.15);
}
-*{margin:0;padding:0;box-sizing:border-box;}
-html{font-size:15px;-webkit-font-smoothing:antialiased;}
-body{background:var(--bg);color:var(--text);font-family:var(--font);line-height:1.6;padding:0 0 48px;}
-.container{max-width:1080px;margin:0 auto;padding:24px 20px;}
-h1{font-size:1.7rem;font-weight:700;margin-bottom:6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
-.badge{font-size:.7rem;font-weight:600;padding:2px 10px;border-radius:20px;letter-spacing:.5px;text-transform:uppercase;vertical-align:middle;}
-.badge.easy{background:var(--green-dim);color:var(--green);}
-.badge.medium{background:var(--orange-dim);color:var(--orange);}
-.badge.hard{background:var(--red-dim);color:var(--red);}
-.subtitle{color:var(--text2);font-size:.85rem;margin-bottom:18px;}
-/* Controls */
-.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px;background:var(--surface);padding:10px 14px;border-radius:var(--radius);border:1px solid var(--border);}
-.controls label{font-size:.82rem;color:var(--text2);}
-.controls input[type=text]{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:.82rem;width:180px;}
-.controls select{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:6px;font-size:.82rem;}
-.controls button{padding:6px 14px;border:none;border-radius:6px;font-size:.82rem;cursor:pointer;font-weight:600;transition:all .15s;}
-.controls button.primary{background:var(--accent);color:#fff;}
-.controls button.primary:hover{background:var(--accent2);}
-.controls button:not(.primary){background:var(--surface2);color:var(--text);border:1px solid var(--border);}
-.controls button:not(.primary):hover{background:var(--border);}
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
-/* Pipeline */
-.pipeline{display:flex;gap:3px;margin-bottom:10px;flex-wrap:wrap;}
-.pipeline .step-dot{width:28px;height:6px;border-radius:3px;background:var(--surface2);transition:background .2s;}
-.pipeline .step-dot.done{background:var(--accent);}
-.pipeline .step-dot.active{background:var(--orange);}
+body {
+ font-family: var(--font);
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4e9f2 50%, #dfe6ed 100%);
+ min-height: 100vh;
+ color: var(--text);
+ line-height: 1.6;
+}
-/* Hint */
-.hint{padding:10px 14px;border-radius:var(--radius);margin-bottom:14px;font-size:.85rem;line-height:1.6;}
-.hint.info{background:var(--blue-dim);color:var(--blue);border:1px solid rgba(77,166,255,.25);}
-.hint #stepInfo{font-weight:700;}
+/* ---------- Layout ---------- */
+.container { max-width: 1440px; margin: 0 auto; padding: 20px; }
-/* Panels */
-.panels{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:14px;}
-.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;}
-.panel h3{font-size:.9rem;font-weight:700;margin-bottom:10px;color:var(--text2);}
-.panel-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
-@media(max-width:700px){.panel-grid{grid-template-columns:1fr;}}
+/* ---------- Typography ---------- */
+h1 { font-size: 25px; margin: 0 0 8px; }
+h2 { font-size: 20px; margin: 0 0 12px; color: #1e293b; }
+h3 { margin: 0 0 10px; font-size: 16px; }
+.subtitle { color: var(--text-secondary); margin-bottom: 16px; font-size: 1.1rem; }
-/* Vis area */
-.vis-area{min-height:120px;position:relative;}
-.vis-area svg{display:block;margin:0 auto;}
+/* ---------- Badge ---------- */
+.badge {
+ display: inline-block; padding: 2px 10px; border-radius: 999px;
+ font-size: 12px; font-weight: 600; margin-left: 8px; vertical-align: middle;
+}
+.badge.easy { background: #dcfce7; color: #166534; }
+.badge.medium { background: #fef3c7; color: #92400e; }
+.badge.hard { background: #fee2e2; color: #991b1b; }
-/* Array rendering */
-.arr-row{display:flex;align-items:flex-end;gap:4px;justify-content:center;margin:8px 0;}
-.arr-cell{display:flex;flex-direction:column;align-items:center;min-width:36px;}
-.arr-val{font-family:var(--mono);font-size:.82rem;padding:3px 0;color:var(--text);}
-.arr-bar{width:30px;border-radius:4px 4px 0 0;background:var(--accent);transition:all .25s;}
-.arr-idx{font-family:var(--mono);font-size:.68rem;color:var(--text2);margin-top:2px;}
-.arr-bar.min-price{background:var(--green);box-shadow:0 0 8px var(--green);}
-.arr-bar.current{background:var(--orange);box-shadow:0 0 8px var(--orange);}
-.arr-bar.checking{background:var(--purple);box-shadow:0 0 6px var(--purple);}
-.arr-bar.covered{background:var(--blue);opacity:.7;}
-.arr-bar.best{background:var(--green);box-shadow:0 0 8px var(--green);}
-.arr-cell .tag{font-size:.6rem;font-weight:700;margin-top:1px;}
+/* ---------- Controls ---------- */
+.controls {
+ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
+ padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
+ margin-bottom: 12px;
+}
+.controls label { font-size: .82rem; color: var(--text-secondary); }
+.controls input, .controls select {
+ padding: 8px 10px; border: 1px solid #cbd5e1; border-radius: 8px;
+ font-family: var(--mono); font-size: 14px; background: #f8fafc; color: var(--text);
+}
+.controls input:focus, .controls select:focus { outline: 2px solid var(--blue); outline-offset: -1px; }
+.controls input[type="text"] { width: 260px; }
+.controls button {
+ padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 8px;
+ background: #f8fafc; font-size: 14px; cursor: pointer; color: var(--text);
+ transition: background var(--transition), transform 0.1s;
+}
+.controls button:hover { background: #e2e8f0; }
+.controls button:active { transform: scale(0.97); }
+.controls button:disabled { opacity: 0.45; cursor: not-allowed; }
+.controls button.primary {
+ background: var(--blue); color: white; border-color: var(--blue);
+}
+.controls button.primary:hover { background: #2563eb; }
-/* Grid rendering */
-.grid-vis{display:inline-grid;gap:3px;margin:8px auto;}
-.grid-cell{width:48px;height:48px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:.82rem;border-radius:4px;background:var(--surface2);border:1px solid var(--border);transition:all .25s;position:relative;}
-.grid-cell.highlight{background:var(--accent);color:#fff;border-color:var(--accent);}
-.grid-cell.path{background:var(--green);color:#fff;border-color:var(--green);box-shadow:0 0 6px var(--green);}
-.grid-cell.computing{background:var(--orange-dim);border-color:var(--orange);}
-.grid-cell.origin{background:var(--purple-dim);border-color:var(--purple);}
+/* ---------- Pipeline / Step Indicator ---------- */
+.pipeline {
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
+ background: #eef2ff; border-radius: var(--radius); padding: 10px 16px; margin-bottom: 12px;
+ font-size: 14px;
+}
+.pipe-step {
+ background: white; border: 1px solid #c7d2fe; border-radius: 999px;
+ padding: 4px 10px; color: #3730a3; transition: all var(--transition);
+}
+.pipe-step.active {
+ background: #4f46e5; color: white; border-color: #4f46e5; font-weight: 600;
+ box-shadow: 0 0 0 3px rgba(79,70,229,0.2);
+}
+.pipeline i { color: var(--text-muted); font-style: normal; }
-/* String rendering */
-.str-row{display:flex;gap:2px;justify-content:center;margin:8px 0;flex-wrap:wrap;}
-.str-cell{width:32px;height:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:var(--mono);font-size:.9rem;border-radius:4px;background:var(--surface2);border:2px solid transparent;transition:all .2s;}
-.str-cell.center{border-color:var(--orange);background:var(--orange-dim);}
-.str-cell.expand{border-color:var(--purple);background:var(--purple-dim);}
-.str-cell.palindrome{background:var(--green-dim);border-color:var(--green);}
-.str-cell.best{background:var(--green);color:#fff;border-color:var(--green);}
-.str-cell .str-idx{font-size:.55rem;color:var(--text2);}
+/* Step dots (Style B) */
+.step-dot {
+ width: 28px; height: 6px; border-radius: 3px; background: #cbd5e1;
+ display: inline-block; transition: background .2s; cursor: pointer;
+}
+.step-dot.done { background: var(--blue); }
+.step-dot.active { background: var(--orange); }
-/* Partition colors */
-.part-0{background:var(--accent)!important;border-color:var(--accent)!important;}
-.part-1{background:var(--green)!important;border-color:var(--green)!important;}
-.part-2{background:var(--orange)!important;border-color:var(--orange)!important;}
-.part-3{background:var(--purple)!important;border-color:var(--purple)!important;}
-.part-4{background:var(--cyan)!important;border-color:var(--cyan)!important;}
-.part-5{background:var(--yellow)!important;border-color:var(--yellow)!important;color:#222!important;}
-.part-6{background:var(--red)!important;border-color:var(--red)!important;}
-.part-7{background:var(--blue)!important;border-color:var(--blue)!important;}
+/* ---------- Hint Box ---------- */
+.hint {
+ background: var(--orange-light); border-left: 4px solid var(--orange); border-radius: 8px;
+ padding: 10px 16px; margin-bottom: 12px; line-height: 1.7; font-size: 14px;
+}
+.hint.info { background: #eff6ff; border-left-color: var(--blue); }
+.hint.success { background: #f0fdf4; border-left-color: var(--green); }
+.hint #stepInfo { font-weight: 700; }
-/* Jump vis */
-.jump-row{display:flex;gap:3px;align-items:center;justify-content:center;margin:8px 0;flex-wrap:wrap;}
-.jump-cell{display:flex;flex-direction:column;align-items:center;min-width:38px;padding:4px 2px;border-radius:6px;transition:all .2s;}
-.jump-cell .j-val{font-family:var(--mono);font-size:.9rem;font-weight:600;}
-.jump-cell .j-idx{font-size:.6rem;color:var(--text2);}
-.jump-cell.reachable{background:var(--blue-dim);border:1px solid var(--blue);}
-.jump-cell.current{background:var(--orange-dim);border:2px solid var(--orange);}
-.jump-cell.max-reach{background:var(--green-dim);border:2px solid var(--green);}
-.jump-cell.step-start{border-bottom:3px solid var(--orange);}
-.jump-cell .j-chip{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;background:var(--surface2);border:2px solid var(--border);}
+/* ---------- Cards & Panels ---------- */
+.card, .panel {
+ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg);
+ padding: 20px; margin-bottom: 12px; box-shadow: var(--shadow);
+}
+.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
+@media (max-width: 900px) { .panel-grid { grid-template-columns: 1fr; } }
+.panels { display: flex; flex-direction: column; gap: 16px; }
-/* Code */
-.code-section{margin-top:16px;}
-pre.code-block{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:.8rem;line-height:1.7;white-space:pre;color:var(--text2);}
-pre.code-block .hl{background:var(--orange-dim);display:inline-block;width:calc(100% + 24px);margin:0 -12px;padding:0 12px;border-left:3px solid var(--orange);}
-pre.code-block .kw{color:var(--purple);}
-pre.code-block .fn{color:var(--blue);}
-pre.code-block .str{color:var(--green);}
-pre.code-block .num{color:var(--orange);}
-pre.code-block .cmt{color:#555;font-style:italic;}
+/* ---------- Formula / Code Box ---------- */
+.formula-box {
+ background: #f0f9ff; border-radius: var(--radius); padding: 12px 16px;
+ font-size: 15px; line-height: 1.8; margin-bottom: 12px; color: #0c4a6e;
+}
+.calc-block {
+ background: #f8fafc; border-radius: var(--radius); padding: 12px 16px;
+ font-size: 15px; line-height: 1.9; margin-bottom: 10px; border: 1px solid var(--border);
+}
+code {
+ background: #f1f5f9; padding: 0.15em 0.4em; border-radius: 4px;
+ font-size: 0.9em; color: #7c3aed; font-family: var(--mono);
+}
+pre {
+ background: #f8fafc; border: 1px solid var(--border); border-radius: 8px;
+ padding: 1rem; overflow-x: auto; margin: 0.75rem 0;
+}
+pre code { background: none; padding: 0; color: var(--text); }
-/* Result */
-.result-box{background:var(--green-dim);border:1px solid rgba(61,214,140,.3);border-radius:var(--radius);padding:12px 16px;font-size:.9rem;color:var(--green);}
-.result-box .val{font-size:1.3rem;font-weight:700;}
+/* Code block with highlight */
+.code-block {
+ margin: 12px 0; border-radius: var(--radius); overflow: hidden;
+ border: 1px solid var(--border);
+}
+.code-block .lang-label {
+ background: #e2e8f0; padding: 4px 12px; font-size: 12px; color: var(--text-secondary);
+ font-weight: 600;
+}
+.code-block pre { margin: 0; border: none; border-radius: 0; }
+.code-block .hl { background: #fef3c7; display: inline; }
+.code-line { display: block; padding: 0 1rem; transition: background 0.2s; }
+.code-line.highlight { background: #fef3c7; border-left: 3px solid var(--orange); }
-/* Footer */
-footer{text-align:center;color:var(--text2);font-size:.72rem;margin-top:32px;opacity:.5;}
+/* ---------- Tables ---------- */
+.table-wrap { overflow-x: auto; margin-top: 8px; }
+table { border-collapse: collapse; width: 100%; font-size: 13px; }
+th, td { border: 1px solid var(--border); padding: 5px 8px; text-align: center; }
+th { background: #f8fafc; color: var(--text-secondary); font-weight: 600; }
-/* Misc */
-.flex-center{display:flex;align-items:center;justify-content:center;}
-.mt-8{margin-top:8px;}
-.mb-8{margin-bottom:8px;}
-.tag-green{color:var(--green);font-weight:700;font-size:.75rem;}
-.tag-orange{color:var(--orange);font-weight:700;font-size:.75rem;}
-.tag-red{color:var(--red);font-weight:700;font-size:.75rem;}
-.tag-blue{color:var(--blue);font-weight:700;font-size:.75rem;}
-.legend{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0;font-size:.75rem;color:var(--text2);}
-.legend-item{display:flex;align-items:center;gap:4px;}
-.legend-dot{width:10px;height:10px;border-radius:3px;}
+/* ---------- Chips / Array Cells (Style A) ---------- */
+.nums-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 8px 0; }
+.chip {
+ display: inline-flex; align-items: center; justify-content: center;
+ min-width: 40px; height: 36px; padding: 4px 10px;
+ border-radius: 10px; font-family: var(--mono); font-weight: 600; font-size: 14px;
+ transition: all var(--transition);
+}
+.chip.default { background: #e2e8f0; color: var(--text); }
+.chip.blue { background: var(--blue-light); color: var(--blue-dark); }
+.chip.green { background: var(--green-light); color: var(--green-dark); }
+.chip.red { background: var(--red-light); color: var(--red-dark); }
+.chip.orange { background: var(--orange-light); color: var(--orange-dark); }
+.chip.purple { background: var(--purple-light); color: var(--purple-dark); }
+.chip.cyan { background: var(--cyan-light); color: #155e75; }
+.chip.active { box-shadow: 0 0 0 3px rgba(59,130,246,0.3); transform: scale(1.08); }
+.chip.selected { background: #bbf7d0; color: var(--green-dark); box-shadow: 0 0 0 2px var(--green); }
+.chip.swapped { background: #fef3c7; color: #92400e; }
+.chip.pointer { border-bottom: 3px solid var(--blue); }
+.chip.sorted { border-bottom: 3px solid var(--green); }
+.chip-group { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
+.chip-index { font-size: 11px; color: var(--text-muted); }
+
+/* ---------- Array Rendering (Style B) ---------- */
+.arr-row { display: flex; align-items: flex-end; gap: 4px; justify-content: center; margin: 8px 0; }
+.arr-cell { display: flex; flex-direction: column; align-items: center; min-width: 36px; }
+.arr-val { font-family: var(--mono); font-size: .82rem; padding: 3px 0; color: var(--text); }
+.arr-bar {
+ width: 30px; border-radius: 4px 4px 0 0; background: var(--blue); transition: all .25s;
+}
+.arr-idx { font-family: var(--mono); font-size: .68rem; color: var(--text-secondary); margin-top: 2px; }
+.arr-bar.min-price { background: var(--green); box-shadow: 0 0 8px var(--green); }
+.arr-bar.current { background: var(--orange); box-shadow: 0 0 8px var(--orange); }
+.arr-bar.checking { background: var(--purple); box-shadow: 0 0 6px var(--purple); }
+.arr-bar.covered { background: var(--blue); opacity: .7; }
+.arr-bar.best { background: var(--green); box-shadow: 0 0 8px var(--green); }
+.arr-cell .tag { font-size: .6rem; font-weight: 700; margin-top: 1px; }
+.tag-green { color: var(--green); }
+.tag-orange { color: var(--orange); }
+.tag-blue { color: var(--blue); }
+
+/* ---------- Arrows / Pointers ---------- */
+.pointer-arrow {
+ display: inline-flex; flex-direction: column; align-items: center;
+ font-size: 11px; color: var(--blue); font-weight: 700;
+}
+.pointer-arrow::before { content: '▼'; font-size: 10px; }
+.pointer-arrow.green { color: var(--green); }
+.split { font-size: 22px; font-weight: 800; margin: 0 8px; color: var(--text-muted); }
+
+/* ---------- Result / Answer Box ---------- */
+.current-answer {
+ margin-top: 12px; padding: 10px 16px; background: #ecfdf5; border-radius: 10px;
+ font-size: 16px;
+}
+.final-answer {
+ margin-top: 16px; padding: 16px 20px; background: #ecfdf5;
+ border: 2px solid #86efac; border-radius: var(--radius); line-height: 1.9;
+}
+.final-answer b { color: var(--green); }
+
+/* ---------- Complexity Box ---------- */
+.complexity {
+ margin-top: 10px; font-size: 13px; color: var(--text-secondary);
+ padding: 8px 12px; background: #f1f5f9; border-radius: 8px;
+}
+
+/* ---------- Tree / Node Visualization ---------- */
+.tree-container {
+ display: flex; justify-content: center; padding: 20px 0; overflow-x: auto;
+}
+.tree-node {
+ display: inline-flex; flex-direction: column; align-items: center;
+ position: relative;
+}
+.tree-node .node-circle {
+ width: 40px; height: 40px; border-radius: 50%; display: flex;
+ align-items: center; justify-content: center; font-weight: 700;
+ font-size: 14px; background: var(--blue-light); color: var(--blue-dark);
+ border: 2px solid var(--blue); transition: all var(--transition);
+}
+.tree-node .node-circle.active {
+ background: #4f46e5; color: white; box-shadow: 0 0 0 4px rgba(79,70,229,0.25);
+}
+.tree-node .node-circle.visited {
+ background: var(--green-light); border-color: var(--green); color: var(--green-dark);
+}
+.tree-node .node-circle.current {
+ background: #fef3c7; border-color: var(--orange); color: var(--orange-dark);
+ box-shadow: 0 0 0 4px rgba(245,158,11,0.25);
+}
+
+/* ---------- Linked List Visualization ---------- */
+.linked-list {
+ display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 16px 0;
+}
+.ll-node {
+ display: inline-flex; align-items: center; gap: 0;
+ border: 2px solid var(--blue); border-radius: 8px; overflow: hidden;
+ transition: all var(--transition);
+}
+.ll-node .val {
+ min-width: 48px; height: 40px; display: flex; align-items: center; justify-content: center;
+ font-weight: 700; font-size: 14px; padding: 0 12px; background: var(--blue-light); color: var(--blue-dark);
+}
+.ll-node .arrow {
+ width: 28px; height: 40px; display: flex; align-items: center; justify-content: center;
+ background: #f1f5f9; border-left: 1px solid var(--border); font-size: 16px; color: var(--text-muted);
+}
+.ll-node.active { border-color: var(--green); box-shadow: 0 0 0 3px rgba(22,163,74,0.2); }
+.ll-node.active .val { background: var(--green-light); color: var(--green-dark); }
+.ll-node.current { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(245,158,11,0.2); }
+.ll-node.current .val { background: #fef3c7; color: var(--orange-dark); }
+.ll-node.null-node { border-style: dashed; border-color: var(--text-muted); }
+.ll-node.null-node .val { background: #f1f5f9; color: var(--text-muted); font-style: italic; font-weight: 400; }
+.ll-arrow { font-size: 18px; color: var(--text-muted); margin: 0 2px; }
+
+/* ---------- Grid/Matrix Visualization ---------- */
+.grid-viz, .grid-vis {
+ display: inline-grid; gap: 2px; padding: 8px 0;
+}
+.grid-cell {
+ width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
+ border: 1px solid var(--border); border-radius: 6px; font-size: 14px;
+ font-weight: 600; transition: all var(--transition); background: white;
+}
+.grid-cell.island { background: var(--blue-light); color: var(--blue-dark); }
+.grid-cell.water { background: #e0f2fe; color: #0369a1; }
+.grid-cell.visited { background: var(--green-light); border-color: var(--green); }
+.grid-cell.current { background: #fef3c7; border-color: var(--orange); box-shadow: 0 0 0 2px rgba(245,158,11,0.3); }
+.grid-cell.rotten { background: var(--orange-light); border-color: var(--orange); color: var(--orange-dark); }
+.grid-cell.fresh { background: #dcfce7; border-color: var(--green); }
+.grid-cell.empty { background: #f1f5f9; color: var(--text-muted); }
+.grid-cell.wall { background: #1e293b; color: white; }
+.grid-cell.path { background: #dbeafe; border-color: var(--blue); }
+/* Style B grid classes */
+.grid-cell.highlight { background: var(--blue); color: #fff; border-color: var(--blue); }
+.grid-cell.computing { background: var(--orange-light); border-color: var(--orange); }
+.grid-cell.origin { background: var(--purple-light); border-color: var(--purple); }
+
+/* String rendering (Style B) */
+.str-row { display: flex; gap: 2px; justify-content: center; margin: 8px 0; flex-wrap: wrap; }
+
+/* ---------- Stack / Queue Visualization ---------- */
+.stack-container, .queue-container {
+ display: flex; flex-direction: column; align-items: center; gap: 2px; min-height: 200px;
+ padding: 8px; background: #f8fafc; border: 2px dashed var(--border); border-radius: var(--radius);
+}
+.stack-item, .queue-item {
+ min-width: 80px; padding: 8px 16px; text-align: center; font-weight: 600;
+ border-radius: 8px; font-size: 14px; transition: all var(--transition);
+ background: var(--blue-light); color: var(--blue-dark); border: 1px solid var(--blue);
+}
+.stack-item.top, .queue-item.front {
+ background: #4f46e5; color: white; border-color: #4f46e5;
+}
+.stack-item.popped {
+ background: var(--red-light); color: var(--red-dark); border-color: var(--red);
+ opacity: 0.5;
+}
+
+/* ---------- Step Controls ---------- */
+.step-controls {
+ display: flex; gap: 8px; align-items: center; justify-content: center;
+ margin: 16px 0; flex-wrap: wrap;
+}
+.step-controls button {
+ padding: 10px 16px; border: 1px solid #cbd5e1; border-radius: 8px;
+ background: #f8fafc; font-size: 14px; cursor: pointer;
+ transition: all var(--transition);
+}
+.step-controls button:hover { background: #e2e8f0; }
+.step-controls button:disabled { opacity: .45; cursor: not-allowed; }
+.step-controls button.primary { background: var(--blue); color: white; border-color: var(--blue); }
+.step-controls button.primary:hover { background: #2563eb; }
+.step-counter {
+ font-size: 14px; color: var(--text-secondary); min-width: 100px; text-align: center;
+}
+
+/* ---------- Vis Area ---------- */
+.vis-area { min-height: 120px; padding: 16px 0; position: relative; }
+.vis-area svg { display: block; margin: 0 auto; max-width: 100%; }
+
+/* ---------- Animation Helpers ---------- */
+@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
+@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
+@keyframes slideIn { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: none; } }
+.animate-fade { animation: fadeIn 0.3s ease; }
+.animate-pulse { animation: pulse 1s infinite; }
+.animate-slide { animation: slideIn 0.3s ease; }
+
+/* ---------- Footer ---------- */
+footer {
+ margin-top: 3rem; text-align: center; color: var(--text-muted);
+ font-size: 0.85rem; padding: 16px 0;
+}
+
+/* ---------- Responsive ---------- */
+@media (max-width: 768px) {
+ .container { padding: 12px; }
+ h1 { font-size: 20px; }
+ .controls { padding: 8px 10px; }
+ .controls input[type="text"] { width: 100%; }
+ .panel { padding: 12px; }
+ .chip { min-width: 32px; height: 30px; font-size: 12px; padding: 2px 6px; }
+ .grid-cell { width: 32px; height: 32px; font-size: 12px; }
+}
+
+/* ---------- Print ---------- */
+@media print {
+ body { background: white; }
+ .controls, .step-controls { display: none; }
+ .card, .panel { box-shadow: none; border: 1px solid #ccc; }
+}
+
+/* ---------- Nav page styles ---------- */
+.hero { text-align: center; padding: 3rem 1rem; }
+.hero h1 { font-size: 2.5rem; margin-bottom: .5rem; justify-content: center; }
+.hero p { color: var(--text-secondary); font-size: 1.1rem; }
+.stats { display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem; flex-wrap: wrap; }
+.stat-card { background: white; border: 1px solid var(--border); border-radius: 12px; padding: 1rem 1.5rem; text-align: center; box-shadow: var(--shadow); min-width: 100px; }
+.stat-card .num { font-size: 2rem; font-weight: 800; }
+.stat-card .label { font-size: .85rem; color: var(--text-secondary); }
+.category { margin-bottom: 2rem; }
+.category h2 { font-size: 1.3rem; margin-bottom: .75rem; padding-bottom: .5rem; border-bottom: 2px solid var(--border); }
+.grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(240px,1fr)); gap: 10px; }
+.card-link { display: block; background: white; border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; text-decoration: none; color: var(--text); transition: all .2s; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
+.card-link:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--blue); }
+.card-link .id { font-size: .8rem; color: var(--text-muted); }
+.card-link .title { font-weight: 600; font-size: .95rem; margin-top: 2px; }
+
+/* ---------- Misc Helpers ---------- */
+.processed { opacity: .85; }
+.dim { opacity: .3; }
+.best-diff { font-weight: 800; color: var(--green); }
+.nearest-val { color: var(--green); font-weight: 700; }
+.sign-plus { color: var(--green); font-weight: 800; }
+.sign-minus { color: var(--red); font-weight: 800; }