From df283a3d3af02cc8f44f3173b8a2a874ada45c78 Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 24 Aug 2026 04:51:01 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E4=B8=A4=E5=A5=97API?= =?UTF-8?q?=E7=9A=84algo-viz.js=E5=92=8Cstyle.css?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/algo-viz.js | 406 ++++++++++++++++++++++++++++++----- shared/style.css | 524 +++++++++++++++++++++++++++++++++------------ 2 files changed, 748 insertions(+), 182 deletions(-) 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 += `
${val}
`; + } + } + 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 += `
${val}
`; + } + }); + 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; }