diff --git a/public/css/styles.css b/public/css/styles.css
index c5ffbdb..09ea627 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -7,9 +7,9 @@
}
.app-container {
- background: white;
+ background: var(--cv-bg-surface);
border-radius: 20px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--cv-shadow-lg);
padding: 40px;
max-width: 1400px;
margin: 0 auto;
@@ -18,7 +18,7 @@
/* Header */
.app-title {
- color: #333;
+ color: var(--cv-text-primary);
margin-bottom: 10px;
font-size: 32px;
text-align: center;
@@ -26,14 +26,14 @@
.app-subtitle {
text-align: center;
- color: #666;
+ color: var(--cv-text-secondary);
margin-bottom: 30px;
font-size: 16px;
}
/* Demo Section */
.demo-section {
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
@@ -41,7 +41,7 @@
.demo-title {
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
margin-bottom: 15px;
font-size: 16px;
}
@@ -56,7 +56,7 @@
grid-column: 1 / -1;
margin-top: 15px;
margin-bottom: 10px;
- color: #667eea;
+ color: var(--cv-accent);
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
@@ -64,24 +64,24 @@
}
.demo-item {
- background: white;
+ background: var(--cv-bg-surface);
padding: 15px;
border-radius: 8px;
- border: 2px solid #ddd;
+ border: 2px solid var(--cv-border);
cursor: pointer;
transition: all 0.3s ease;
}
.demo-item:hover {
- border-color: #667eea;
+ border-color: var(--cv-accent);
transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+ box-shadow: var(--cv-shadow-md);
}
.demo-number {
font-size: 12px;
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
margin-bottom: 8px;
}
@@ -93,11 +93,12 @@
display: flex;
align-items: center;
justify-content: center;
+ color: var(--cv-text-primary);
}
/* Input Section */
.input-section {
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
padding: 25px;
border-radius: 12px;
margin-bottom: 20px;
@@ -105,16 +106,16 @@
.section-title {
font-weight: 700;
- color: #333;
+ color: var(--cv-text-primary);
margin-bottom: 20px;
font-size: 18px;
}
.limit-builder {
- background: white;
+ background: var(--cv-bg-surface);
padding: 25px;
border-radius: 12px;
- border: 3px solid #667eea;
+ border: 3px solid var(--cv-accent);
margin-bottom: 20px;
}
@@ -135,13 +136,13 @@
.limit-label {
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
font-size: 28px;
}
.limit-arrow {
font-size: 28px;
- color: #667eea;
+ color: var(--cv-accent);
}
/* Multi-variable container */
@@ -156,57 +157,60 @@
display: flex;
align-items: center;
gap: 10px;
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
padding: 10px 15px;
border-radius: 8px;
- border: 2px solid #e0e0e0;
+ border: 2px solid var(--cv-border);
transition: all 0.3s ease;
}
.variable-row:hover {
- border-color: #667eea;
- background: white;
+ border-color: var(--cv-accent);
+ background: var(--cv-bg-surface);
}
.variable-name-input {
padding: 8px 12px;
- border: 2px solid #667eea;
+ border: 2px solid var(--cv-accent);
border-radius: 6px;
font-size: 18px;
width: 60px;
text-align: center;
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
+ background: var(--cv-input-bg);
}
.variable-name-input:focus {
outline: none;
- border-color: #764ba2;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+ border-color: var(--cv-accent-hover);
+ box-shadow: 0 0 0 3px var(--cv-accent-light);
}
.limit-input-inline {
padding: 8px 12px;
- border: 2px solid #667eea;
+ border: 2px solid var(--cv-accent);
border-radius: 6px;
font-size: 18px;
width: 140px;
text-align: center;
font-weight: 600;
+ background: var(--cv-input-bg);
+ color: var(--cv-text-primary);
}
.limit-input-inline:focus {
outline: none;
- border-color: #764ba2;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+ border-color: var(--cv-accent-hover);
+ box-shadow: 0 0 0 3px var(--cv-accent-light);
}
.add-var-btn,
.remove-var-btn {
padding: 6px 12px;
- border: 2px solid #667eea;
- background: white;
- color: #667eea;
+ border: 2px solid var(--cv-accent);
+ background: var(--cv-bg-surface);
+ color: var(--cv-accent);
border-radius: 6px;
cursor: pointer;
font-size: 20px;
@@ -216,42 +220,42 @@
}
.add-var-btn:hover {
- background: #667eea;
- color: white;
+ background: var(--cv-accent);
+ color: var(--cv-text-inverse);
transform: scale(1.1);
}
.remove-var-btn {
- border-color: #dc3545;
- color: #dc3545;
+ border-color: var(--cv-error);
+ color: var(--cv-error);
}
.remove-var-btn:hover {
- background: #dc3545;
+ background: var(--cv-error);
color: white;
transform: scale(1.1);
}
.direction-select {
padding: 8px 12px;
- border: 2px solid #667eea;
+ border: 2px solid var(--cv-accent);
border-radius: 6px;
font-size: 16px;
font-weight: 600;
- color: #667eea;
- background: white;
+ color: var(--cv-accent);
+ background: var(--cv-input-bg);
cursor: pointer;
margin-left: auto;
}
.direction-select:focus {
outline: none;
- border-color: #764ba2;
+ border-color: var(--cv-accent-hover);
}
.function-label {
font-weight: 600;
- color: #333;
+ color: var(--cv-text-primary);
margin-bottom: 10px;
font-size: 14px;
}
@@ -259,17 +263,18 @@
.function-field {
min-height: 80px;
padding: 20px;
- border: 3px solid #667eea;
+ border: 3px solid var(--cv-accent);
border-radius: 12px;
font-size: 24px;
- background: white;
+ background: var(--cv-input-bg);
+ color: var(--cv-text-primary);
cursor: text;
transition: all 0.3s ease;
}
.function-field:focus-within {
- border-color: #764ba2;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+ border-color: var(--cv-accent-hover);
+ box-shadow: 0 0 0 3px var(--cv-accent-light);
}
/* Toolbar */
@@ -279,7 +284,7 @@
.toolbar-title {
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
margin-bottom: 10px;
font-size: 13px;
text-transform: uppercase;
@@ -291,15 +296,15 @@
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
gap: 8px;
padding: 15px;
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
border-radius: 12px;
}
.toolbar-button {
padding: 10px;
- border: 2px solid #667eea;
- background: white;
- color: #667eea;
+ border: 2px solid var(--cv-accent);
+ background: var(--cv-bg-surface);
+ color: var(--cv-accent);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
@@ -308,8 +313,8 @@
}
.toolbar-button:hover {
- background: #667eea;
- color: white;
+ background: var(--cv-accent);
+ color: var(--cv-text-inverse);
transform: translateY(-2px);
}
@@ -326,20 +331,21 @@
.calculate-button {
padding: 18px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
+ background: var(--cv-btn-eq-bg);
+ color: var(--cv-btn-eq-text);
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+ box-shadow: var(--cv-shadow-md);
}
.calculate-button:hover {
+ background: var(--cv-btn-eq-hover);
transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+ box-shadow: var(--cv-shadow-lg);
}
.calculate-button:active {
@@ -348,9 +354,9 @@
.clear-button {
padding: 18px;
- background: white;
- color: #667eea;
- border: 3px solid #667eea;
+ background: var(--cv-btn-clear-bg);
+ color: var(--cv-btn-clear-text);
+ border: 3px solid var(--cv-btn-clear-text);
border-radius: 12px;
font-size: 18px;
font-weight: 700;
@@ -359,8 +365,9 @@
}
.clear-button:hover {
- background: #667eea;
+ background: var(--cv-error);
color: white;
+ border-color: var(--cv-error);
transform: translateY(-2px);
}
@@ -374,7 +381,8 @@
}
.answer-box {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ background: var(--cv-accent-light);
+ border: 2px solid var(--cv-accent);
padding: 30px;
border-radius: 12px;
margin-bottom: 20px;
@@ -382,20 +390,20 @@
}
.answer-label {
- color: rgba(255, 255, 255, 0.9);
+ color: var(--cv-accent-text);
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
}
.answer-value {
- color: white;
+ color: var(--cv-accent);
font-size: 36px;
font-weight: 700;
}
.steps-box {
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
padding: 25px;
border-radius: 12px;
}
@@ -403,7 +411,7 @@
.steps-title {
font-size: 20px;
font-weight: 700;
- color: #333;
+ color: var(--cv-text-primary);
margin-bottom: 20px;
}
@@ -414,33 +422,34 @@
}
.step-item {
- background: white;
+ background: var(--cv-bg-surface);
padding: 20px;
border-radius: 12px;
- border: 2px solid #e0e0e0;
+ border: 2px solid var(--cv-border);
transition: all 0.3s ease;
}
.step-item:hover {
- border-color: #667eea;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
+ border-color: var(--cv-accent);
+ box-shadow: var(--cv-shadow-sm);
}
.step-number {
font-size: 16px;
font-weight: 700;
- color: #667eea;
+ color: var(--cv-accent);
margin-bottom: 10px;
}
.step-content {
- color: #555;
+ color: var(--cv-text-secondary);
line-height: 1.6;
margin-bottom: 10px;
}
.step-math {
- background: #f8f9fa;
+ background: var(--cv-bg-sunken);
+ color: var(--cv-text-primary);
padding: 15px;
border-radius: 8px;
margin-top: 10px;
@@ -452,13 +461,14 @@
position: fixed;
bottom: 30px;
right: 30px;
- background: #333;
- color: white;
+ background: var(--cv-bg-raised);
+ color: var(--cv-text-primary);
+ border: 1px solid var(--cv-border-strong);
padding: 15px 25px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--cv-shadow-lg);
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
diff --git a/src/App.js b/src/App.js
index a3c10f8..c48d44c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -3,19 +3,17 @@ import { AuthProvider } from "./context/AuthContext";
import { ProgressProvider } from "./context/ProgressContext";
import Layout from "./components/Layout";
import ScrollToTop from "./utils/ScrollToTop";
-import IntegralsPart1 from "./pages/IntegralsPart1";
-import IntegralsPart2 from "./pages/IntegralsPart2";
+import ErrorBoundary from "./components/ErrorBoundary";
-// Pages
import Home from "./pages/Home";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Dashboard from "./pages/Dashboard";
import AISolver from "./pages/AISolver";
import NotFound from "./pages/NotFound";
-import ErrorBoundary from "./components/ErrorBoundary";
-// Guide parts
+import IntegralsPart1 from "./pages/IntegralsPart1";
+import IntegralsPart2 from "./pages/IntegralsPart2";
import PartialPart1 from "./pages/PartialPart1";
import PartialPart2 from "./pages/PartialPart2";
import VectorPart1 from "./pages/VectorPart1";
@@ -23,7 +21,6 @@ import VectorPart2 from "./pages/VectorPart2";
import LimitsPart1 from "./pages/LimitsPart1";
import LimitsPart2 from "./pages/LimitsPart2";
-// Tools
import ContinuityFinder from "./pages/ContinuityFinder";
import ExtremeValueFunction from "./pages/ExtremeValueFinder";
import VolumeCalculator from "./pages/VolumeCalculator";
@@ -37,6 +34,7 @@ function App() {
+ {/* Home */}
} />} />
{/* Auth */}
@@ -62,22 +60,21 @@ function App() {
} />} />
} />} />
- {/* Multiple Integrals */}
+ {/* Multiple Integrals */}
} />
} />} />
} />} />
- {/* Tools */}
+ {/* Tools */}
} />} />
} />} />
} />} />
-
} />
} />} />
- {/* Catch-all route */}
+ {/* Catch-all */}
} />} />
-
+
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
index aa034d2..3d828d1 100644
--- a/src/components/Header.jsx
+++ b/src/components/Header.jsx
@@ -3,14 +3,14 @@ import { NavLink, Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
const navLinks = [
- { to: "/simple-concepts", label: "Concepts", type: "Guide" },
- { to: "/partial-derivatives/1", label: "Partials", type: "Guide" },
- { to: "/vector-calculus/1", label: "Vectors", type: "Guide" },
- { to: "/test", label: "Continuity", type: "Tool" },
- { to: "/extreme", label: "Extrema", type: "Tool" },
- { to: "/volumecalculator", label: "Integrals", type: "Tool" },
- { to: "/taylorx", label: "TaylorX", type: "Tool" },
- { to: "/ai-solver", label: "AI Solver", type: "Tool" },
+ { to: "/simple-concepts", label: "Concepts", type: "Guide" },
+ { to: "/partial-derivatives/1",label: "Partials", type: "Guide" },
+ { to: "/vector-calculus/1", label: "Vectors", type: "Guide" },
+ { to: "/test", label: "Continuity",type: "Tool" },
+ { to: "/extreme", label: "Extrema", type: "Tool" },
+ { to: "/volumecalculator", label: "Integrals", type: "Tool" },
+ { to: "/taylorx", label: "TaylorX", type: "Tool" },
+ { to: "/ai-solver", label: "AI Solver", type: "Tool" },
];
function Header({ darkMode, onToggleDark }) {
@@ -18,26 +18,37 @@ function Header({ darkMode, onToggleDark }) {
const [menuOpen, setMenuOpen] = useState(false);
const headerRef = useRef(null);
+ // Close menu on outside click OR Escape key โ combined into one effect
useEffect(() => {
- const handler = (e) => {
+ const handleClick = (e) => {
if (headerRef.current && !headerRef.current.contains(e.target)) {
setMenuOpen(false);
}
};
- document.addEventListener("mousedown", handler);
- return () => document.removeEventListener("mousedown", handler);
- }, []);
-
- useEffect(() => {
- const handler = (e) => {
+ const handleKey = (e) => {
if (e.key === "Escape") setMenuOpen(false);
};
- document.addEventListener("keydown", handler);
- return () => document.removeEventListener("keydown", handler);
+ document.addEventListener("mousedown", handleClick);
+ document.addEventListener("keydown", handleKey);
+ return () => {
+ document.removeEventListener("mousedown", handleClick);
+ document.removeEventListener("keydown", handleKey);
+ };
}, []);
+ const handleLogout = () => {
+ try {
+ logout();
+ } catch (err) {
+ console.error("Logout failed:", err);
+ }
+ setMenuOpen(false);
+ };
+
return (
+
+ {/* Brand */}
setMenuOpen(false)}>
โ
@@ -46,6 +57,7 @@ function Header({ darkMode, onToggleDark }) {
+ {/* Desktop nav */}
+ {/* Controls: theme toggle + auth + hamburger */}
{user ? (
- {user.username[0].toUpperCase()}
+ {user.username?.[0]?.toUpperCase() ?? "U"}
) : (
- Sign in
+ Sign in
Sign up
)}
@@ -89,6 +102,7 @@ function Header({ darkMode, onToggleDark }) {
+ {/* Mobile nav */}
+
);
}
diff --git a/src/components/LatexKeyboard.js b/src/components/LatexKeyboard.js
new file mode 100644
index 0000000..3235671
--- /dev/null
+++ b/src/components/LatexKeyboard.js
@@ -0,0 +1,308 @@
+import React, { useState, useCallback, useRef } from 'react';
+import katex from 'katex';
+import 'katex/dist/katex.min.css';
+import './LatexKeyboard.css';
+import LatexKeyboard from '../components/LatexKeyboard';
+
+/* ============================================================
+ LaTeX Math Keyboard
+ Props:
+ value {string} โ controlled LaTeX string
+ onChange {function} โ called with new LaTeX string
+ onInsert {function} โ called when user inserts from keyboard
+ placeholder {string} โ placeholder text when empty
+ compact {boolean} โ single-row tab strip instead of full panel
+ ============================================================ */
+
+const KEY_GROUPS = [
+ {
+ id: 'basic',
+ label: 'Basic',
+ icon: 'ยฑ',
+ keys: [
+ { display: 'xยฒ', latex: '^{2}', label: 'squared' },
+ { display: 'xโฟ', latex: '^{}', label: 'power', cursor: -1 },
+ { display: 'โ', latex: '\\sqrt{}', label: 'square root', cursor: -1 },
+ { display: 'โฟโ', latex: '\\sqrt[n]{}', label: 'nth root', cursor: -1 },
+ { display: '|x|', latex: '\\left|\\right|', label: 'absolute value' },
+ { display: 'a/b', latex: '\\frac{}{}', label: 'fraction', cursor: -1 },
+ { display: 'ยฑ', latex: '\\pm', label: 'plus minus' },
+ { display: 'โ ', latex: '\\neq', label: 'not equal' },
+ { display: 'โค', latex: '\\leq', label: 'less or equal' },
+ { display: 'โฅ', latex: '\\geq', label: 'greater or equal' },
+ { display: 'โ', latex: '\\approx', label: 'approx' },
+ { display: 'โ', latex: '\\infty', label: 'infinity' },
+ { display: '%', latex: '\\%', label: 'percent' },
+ { display: 'ยท', latex: '\\cdot', label: 'dot product' },
+ { display: 'ร', latex: '\\times', label: 'times' },
+ { display: 'รท', latex: '\\div', label: 'divide' },
+ ],
+ },
+ {
+ id: 'calculus',
+ label: 'Calculus',
+ icon: 'โซ',
+ keys: [
+ { display: 'd/dx', latex: '\\frac{d}{dx}', label: 'derivative' },
+ { display: 'dยฒ/dxยฒ',latex: '\\frac{d^2}{dx^2}', label: '2nd derivative' },
+ { display: 'โ/โx', latex: '\\frac{\\partial}{\\partial x}', label: 'partial deriv.' },
+ { display: 'โซ', latex: '\\int', label: 'integral' },
+ { display: 'โซab', latex: '\\int_{}^{}', label: 'definite integral', cursor: -1 },
+ { display: 'โฌ', latex: '\\iint', label: 'double integral' },
+ { display: 'โญ', latex: '\\iiint', label: 'triple integral' },
+ { display: 'โฎ', latex: '\\oint', label: 'contour integral' },
+ { display: 'lim', latex: '\\lim_{ \\to }', label: 'limit' },
+ { display: 'ฮฃ', latex: '\\sum_{}^{}', label: 'sum', cursor: -1 },
+ { display: 'ฮ ', latex: '\\prod_{}^{}', label: 'product', cursor: -1 },
+ { display: 'โ', latex: '\\nabla', label: 'nabla / gradient' },
+ ],
+ },
+ {
+ id: 'trig',
+ label: 'Trig',
+ icon: 'โฟ',
+ keys: [
+ { display: 'sin', latex: '\\sin', label: 'sine' },
+ { display: 'cos', latex: '\\cos', label: 'cosine' },
+ { display: 'tan', latex: '\\tan', label: 'tangent' },
+ { display: 'csc', latex: '\\csc', label: 'cosecant' },
+ { display: 'sec', latex: '\\sec', label: 'secant' },
+ { display: 'cot', latex: '\\cot', label: 'cotangent' },
+ { display: 'sinโปยน', latex: '\\arcsin', label: 'arcsine' },
+ { display: 'cosโปยน', latex: '\\arccos', label: 'arccosine' },
+ { display: 'tanโปยน', latex: '\\arctan', label: 'arctangent' },
+ { display: 'sinh', latex: '\\sinh', label: 'hyperbolic sin' },
+ { display: 'cosh', latex: '\\cosh', label: 'hyperbolic cos' },
+ { display: 'tanh', latex: '\\tanh', label: 'hyperbolic tan' },
+ { display: 'log', latex: '\\log', label: 'log base 10' },
+ { display: 'ln', latex: '\\ln', label: 'natural log' },
+ { display: 'logโ', latex: '\\log_{}', label: 'log base n', cursor: -1 },
+ { display: 'eหฃ', latex: 'e^{}', label: 'exponential', cursor: -1 },
+ ],
+ },
+ {
+ id: 'greek',
+ label: 'Greek',
+ icon: 'ฮฑ',
+ keys: [
+ { display: 'ฮฑ', latex: '\\alpha', label: 'alpha' },
+ { display: 'ฮฒ', latex: '\\beta', label: 'beta' },
+ { display: 'ฮณ', latex: '\\gamma', label: 'gamma' },
+ { display: 'ฮ', latex: '\\Gamma', label: 'Gamma (upper)' },
+ { display: 'ฮด', latex: '\\delta', label: 'delta' },
+ { display: 'ฮ', latex: '\\Delta', label: 'Delta (upper)' },
+ { display: 'ฮต', latex: '\\epsilon', label: 'epsilon' },
+ { display: 'ฮถ', latex: '\\zeta', label: 'zeta' },
+ { display: 'ฮท', latex: '\\eta', label: 'eta' },
+ { display: 'ฮธ', latex: '\\theta', label: 'theta' },
+ { display: 'ฮ', latex: '\\Theta', label: 'Theta (upper)' },
+ { display: 'ฮป', latex: '\\lambda', label: 'lambda' },
+ { display: 'ฮ', latex: '\\Lambda', label: 'Lambda (upper)' },
+ { display: 'ฮผ', latex: '\\mu', label: 'mu' },
+ { display: 'ฮฝ', latex: '\\nu', label: 'nu' },
+ { display: 'ฯ', latex: '\\pi', label: 'pi' },
+ { display: 'ฮ ', latex: '\\Pi', label: 'Pi (upper)' },
+ { display: 'ฯ', latex: '\\rho', label: 'rho' },
+ { display: 'ฯ', latex: '\\sigma', label: 'sigma' },
+ { display: 'ฮฃ', latex: '\\Sigma', label: 'Sigma (upper)' },
+ { display: 'ฯ', latex: '\\tau', label: 'tau' },
+ { display: 'ฯ', latex: '\\phi', label: 'phi' },
+ { display: 'ฮฆ', latex: '\\Phi', label: 'Phi (upper)' },
+ { display: 'ฯ', latex: '\\chi', label: 'chi' },
+ { display: 'ฯ', latex: '\\psi', label: 'psi' },
+ { display: 'ฮจ', latex: '\\Psi', label: 'Psi (upper)' },
+ { display: 'ฯ', latex: '\\omega', label: 'omega' },
+ { display: 'ฮฉ', latex: '\\Omega', label: 'Omega (upper)' },
+ ],
+ },
+ {
+ id: 'symbols',
+ label: 'Symbols',
+ icon: 'โ',
+ keys: [
+ { display: 'โ', latex: '\\in', label: 'element of' },
+ { display: 'โ', latex: '\\notin', label: 'not element of' },
+ { display: 'โ', latex: '\\subset', label: 'subset' },
+ { display: 'โ', latex: '\\subseteq', label: 'subset or equal' },
+ { display: 'โ', latex: '\\supset', label: 'superset' },
+ { display: 'โช', latex: '\\cup', label: 'union' },
+ { display: 'โฉ', latex: '\\cap', label: 'intersection' },
+ { display: 'โ
', latex: '\\emptyset', label: 'empty set' },
+ { display: 'โ', latex: '\\mathbb{R}', label: 'real numbers' },
+ { display: 'โค', latex: '\\mathbb{Z}', label: 'integers' },
+ { display: 'โ', latex: '\\mathbb{N}', label: 'naturals' },
+ { display: 'โ', latex: '\\mathbb{Q}', label: 'rationals' },
+ { display: 'โ', latex: '\\forall', label: 'for all' },
+ { display: 'โ', latex: '\\exists', label: 'there exists' },
+ { display: 'ยฌ', latex: '\\neg', label: 'negation' },
+ { display: 'โง', latex: '\\land', label: 'and' },
+ { display: 'โจ', latex: '\\lor', label: 'or' },
+ { display: 'โ', latex: '\\rightarrow', label: 'right arrow' },
+ { display: 'โ', latex: '\\leftrightarrow', label: 'biconditional' },
+ { display: 'โ', latex: '\\Rightarrow', label: 'implies' },
+ { display: 'โน', latex: '\\implies', label: 'implies (long)' },
+ { display: 'โบ', latex: '\\iff', label: 'iff' },
+ ],
+ },
+];
+
+/* Renders a LaTeX string safely. Returns null if katex throws. */
+function renderLatex(latex) {
+ if (!latex || !latex.trim()) return null;
+ try {
+ return katex.renderToString(latex, {
+ throwOnError: false,
+ displayMode: true,
+ output: 'html',
+ });
+ } catch {
+ return null;
+ }
+}
+
+export default function LatexKeyboard({
+ value = '',
+ onChange,
+ onInsert,
+ placeholder = 'Type or tap keys to build a math expressionโฆ',
+ compact = false,
+}) {
+ const [activeGroup, setActiveGroup] = useState('basic');
+ const [inputValue, setInputValue] = useState(value);
+ const inputRef = useRef(null);
+
+ /* Sync internal state if parent changes value prop */
+ React.useEffect(() => {
+ setInputValue(value);
+ }, [value]);
+
+ const handleInputChange = useCallback((e) => {
+ const v = e.target.value;
+ setInputValue(v);
+ onChange?.(v);
+ }, [onChange]);
+
+ const insertLatex = useCallback((latex, cursorOffset) => {
+ const el = inputRef.current;
+ if (!el) {
+ const next = inputValue + latex;
+ setInputValue(next);
+ onChange?.(next);
+ onInsert?.(latex);
+ return;
+ }
+
+ const start = el.selectionStart ?? inputValue.length;
+ const end = el.selectionEnd ?? inputValue.length;
+ const next = inputValue.slice(0, start) + latex + inputValue.slice(end);
+ setInputValue(next);
+ onChange?.(next);
+ onInsert?.(latex);
+
+ /* Restore focus and position cursor inside braces when cursorOffset is set */
+ requestAnimationFrame(() => {
+ el.focus();
+ const pos = cursorOffset !== undefined
+ ? start + latex.length + cursorOffset
+ : start + latex.length;
+ el.setSelectionRange(pos, pos);
+ });
+ }, [inputValue, onChange, onInsert]);
+
+ const currentGroup = KEY_GROUPS.find(g => g.id === activeGroup) ?? KEY_GROUPS[0];
+ const rendered = renderLatex(inputValue);
+
+ return (
+
+
+ {/* โโ Input row โโ */}
+
+
+ { setInputValue(''); onChange?.(''); }}
+ aria-label="Clear input"
+ title="Clear"
+ >
+ โ
+
+
+
+ {/* โโ Rendered preview โโ */}
+
+
Preview
+ {rendered ? (
+
+ ) : (
+
+ {inputValue ? 'Invalid LaTeX' : 'Expression preview appears here'}
+
+ )}
+
+
+ {/* โโ Tab strip โโ */}
+
+ {KEY_GROUPS.map(group => (
+ setActiveGroup(group.id)}
+ >
+ {group.icon}
+ {!compact && {group.label}}
+
+ ))}
+
+
+ {/* โโ Key panel โโ */}
+
+ {currentGroup.keys.map((key) => (
+ insertLatex(key.latex, key.cursor)}
+ title={key.label}
+ aria-label={`Insert ${key.label}`}
+ >
+ {key.display}
+
+ ))}
+
+
+ {/* โโ Copy row โโ */}
+
+ LaTeX:
+ {inputValue || 'โ'}
+ navigator.clipboard?.writeText(inputValue)}
+ disabled={!inputValue}
+ aria-label="Copy LaTeX to clipboard"
+ >
+ Copy
+
+
+
+ );
+}
diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx
index c2e0925..3ac1864 100644
--- a/src/components/Layout.jsx
+++ b/src/components/Layout.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
-function Layout(props) {
+function Layout({ body }) {
const [darkMode, setDarkMode] = useState(() => {
try {
return localStorage.getItem("calculus-dark") === "true";
@@ -12,7 +12,6 @@ function Layout(props) {
});
useEffect(() => {
- // Apply to both html and body so all selectors work
const root = document.documentElement;
if (darkMode) {
root.setAttribute("data-theme", "dark");
@@ -31,7 +30,7 @@ function Layout(props) {
return (
<>
- {props.body}
+ {body}
>
);
diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx
new file mode 100644
index 0000000..6118790
--- /dev/null
+++ b/src/components/ThemeToggle.jsx
@@ -0,0 +1,14 @@
+import { useTheme } from "../context/ThemeContext";
+
+export default function ThemeToggle() {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+ {theme === "light" ? "๐ Dark" : "โ๏ธ Light"}
+
+ );
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 889376e..1579d53 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1517,3 +1517,26 @@ body::before {
font-size: clamp(2.25rem, 12vw, 3.3rem);
}
}
+.container {
+ width: 100%;
+ max-width: 1280px;
+ margin: auto;
+ padding: 1rem;
+}
+
+.grid {
+ display: grid;
+ gap: 1rem;
+}
+
+@media (min-width: 768px) {
+ .grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (min-width: 1024px) {
+ .grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index d563c0f..32c71ca 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
+import './styles/theme.css';
+import './styles/responsive.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
diff --git a/src/pages/VolumeCalculator.jsx b/src/pages/VolumeCalculator.jsx
index 41a0984..9045163 100644
--- a/src/pages/VolumeCalculator.jsx
+++ b/src/pages/VolumeCalculator.jsx
@@ -1,5 +1,4 @@
-
-import { useEffect, useRef, useState } from "react";
+import { useState, useEffect, useRef } from "react";
import { InlineMath, BlockMath } from "../components/Math";
// ============================================================================
@@ -288,7 +287,6 @@ const mathEval = (() => {
};
})();
-// Gauss-Kronrod 15-point
const GK_NODES_15 = [-0.9914553711208126, -0.9491079123427585, -0.8648644233597691, -0.7401241915785544, -0.5860872354676911, -0.4058451513773972, -0.2077849550078985, 0, 0.2077849550078985, 0.4058451513773972, 0.5860872354676911, 0.7401241915785544, 0.8648644233597691, 0.9491079123427585, 0.9914553711208126];
const GK_WEIGHTS_15 = [0.02293532201052922, 0.06309209262997856, 0.1047900103222502, 0.1406532597155259, 0.1690047266392679, 0.1903505780647854, 0.2044329400752989, 0.2094821410847278, 0.2044329400752989, 0.1903505780647854, 0.1690047266392679, 0.1406532597155259, 0.1047900103222502, 0.06309209262997856, 0.02293532201052922];
@@ -404,6 +402,7 @@ function solveDoubleIntegral(integrand, bounds, order, opts = {}) {
const outerMinVal = evalBound(outerMin);
const outerMaxVal = evalBound(outerMax);
+
steps.push({
title: 'Outer Bounds',
content: `${outerVar} ranges from:`,
@@ -432,12 +431,25 @@ function solveDoubleIntegral(integrand, bounds, order, opts = {}) {
}
const analytical = getAnalyticalForm(integrand, bounds);
- if (analytical) steps.push({ title: 'Analytical Solution', content: 'Known closed form:', formula: `Result = ${analytical}`, formulaLatex: `\\text{Result} = ${analytical}` });
+ if (analytical) {
+ steps.push({
+ title: 'Analytical Solution',
+ content: 'Known closed form:',
+ formula: `Result = ${analytical}`,
+ formulaLatex: `\\text{Result} = ${analytical}`
+ });
+ }
- if (hasInf) steps.push({ title: 'Improper Integral', content: 'Using variable transformation for infinite bounds', formula: 't = x/(1-xยฒ) mapping', formulaLatex: 't = \\frac{x}{1-x^2}' });
+ if (hasInf) {
+ steps.push({
+ title: 'Improper Integral',
+ content: 'Using variable transformation for infinite bounds',
+ formula: 't = x/(1-xยฒ) mapping',
+ formulaLatex: 't = \\frac{x}{1-x^2}'
+ });
+ }
let result, innerEvals = 0, outerEvals = 0;
-
if (order === 'dydx') {
result = integrate1DSmart((x) => {
outerEvals++;
@@ -473,44 +485,11 @@ function solveDoubleIntegral(integrand, bounds, order, opts = {}) {
}
const PRESETS = [
- {
- label: "Basic Iterated", presets: [
- { name: "Ex 1: 2xy over [0,2]ร[0,1]", integrand: "2*x*y", xMin: "0", xMax: "2", yMin: "0", yMax: "1", order: "dydx" },
- { name: "Ex 2: x-y over [0,1]ร[-1,0]", integrand: "x-y", xMin: "0", xMax: "1", yMin: "-1", yMax: "0", order: "dydx" },
- { name: "Ex 5: 4-yยฒ over [0,2]ร[0,3]", integrand: "4-y^2", xMin: "0", xMax: "2", yMin: "0", yMax: "3", order: "dydx" },
- { name: "Ex 9: exp(x+y) with ln bounds", integrand: "exp(x+y)", xMin: "0", xMax: "ln(2)", yMin: "0", yMax: "ln(5)", order: "dydx" },
- ]
- },
- {
- label: "Variable Bounds", presets: [
- { name: "Triangular: x+y, yโ[0,1-x]", integrand: "x+y", xMin: "0", xMax: "1", yMin: "0", yMax: "1-x", order: "dydx" },
- { name: "Circular: 1, xยฒ+yยฒโค1", integrand: "1", xMin: "-1", xMax: "1", yMin: "-sqrt(1-x^2)", yMax: "sqrt(1-x^2)", order: "dydx" },
- { name: "Parabolic: xยฒ, xโ[0,โy]", integrand: "x^2", xMin: "0", xMax: "sqrt(y)", yMin: "0", yMax: "4", order: "dxdy" },
- ]
- },
- {
- label: "Special Functions", presets: [
- { name: "Bessel Jโ(xยฒ+yยฒ)", integrand: "besselj0(x^2+y^2)", xMin: "0", xMax: "3", yMin: "0", yMax: "3", order: "dydx" },
- { name: "Error function erf(x+y)", integrand: "erf(x+y)", xMin: "0", xMax: "2", yMin: "0", yMax: "2", order: "dydx" },
- { name: "Gamma ฮ(x+y)", integrand: "gamma(x+y)", xMin: "0.1", xMax: "1", yMin: "0.1", yMax: "1", order: "dydx" },
- { name: "Heaviside H(x-y)", integrand: "heaviside(x-y)", xMin: "0", xMax: "2", yMin: "0", yMax: "2", order: "dydx" },
- ]
- },
- {
- label: "Infinite/Improper", presets: [
- { name: "Gaussian over โยฒ", integrand: "exp(-x^2-y^2)", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" },
- { name: "Exponential decay", integrand: "exp(-abs(x)-abs(y))", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" },
- { name: "Semi-infinite: e^(-xy)", integrand: "exp(-x*y)", xMin: "0", xMax: "inf", yMin: "1", yMax: "2", order: "dydx" },
- { name: "Bivariate normal", integrand: "exp(-(x^2+y^2)/2)/(2*pi)", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" },
- ]
- },
- {
- label: "Oscillatory", presets: [
- { name: "Rapid oscillation: sin(100x)cos(100y)", integrand: "sin(100*x)*cos(100*y)", xMin: "0", xMax: "2*pi", yMin: "0", yMax: "2*pi", order: "dydx" },
- { name: "Fresnel: sin(xยฒ)cos(yยฒ)", integrand: "sin(x^2)*cos(y^2)", xMin: "0", xMax: "3", yMin: "0", yMax: "3", order: "dydx" },
- { name: "Sinc: sin(xยฒ+yยฒ)/(xยฒ+yยฒ)", integrand: "sinc(x^2+y^2)", xMin: "0", xMax: "5", yMin: "0", yMax: "5", order: "dydx" },
- ]
- },
+ { label: "Basic Iterated", presets: [{ name: "Ex 1: 2xy over [0,2]ร[0,1]", integrand: "2*x*y", xMin: "0", xMax: "2", yMin: "0", yMax: "1", order: "dydx" }, { name: "Ex 2: x-y over [0,1]ร[-1,0]", integrand: "x-y", xMin: "0", xMax: "1", yMin: "-1", yMax: "0", order: "dydx" }, { name: "Ex 5: 4-yยฒ over [0,2]ร[0,3]", integrand: "4-y^2", xMin: "0", xMax: "2", yMin: "0", yMax: "3", order: "dydx" }, { name: "Ex 9: exp(x+y) with ln bounds", integrand: "exp(x+y)", xMin: "0", xMax: "ln(2)", yMin: "0", yMax: "ln(5)", order: "dydx" }] },
+ { label: "Variable Bounds", presets: [{ name: "Triangular: x+y, yโ[0,1-x]", integrand: "x+y", xMin: "0", xMax: "1", yMin: "0", yMax: "1-x", order: "dydx" }, { name: "Circular: 1, xยฒ+yยฒโค1", integrand: "1", xMin: "-1", xMax: "1", yMin: "-sqrt(1-x^2)", yMax: "sqrt(1-x^2)", order: "dydx" }, { name: "Parabolic: xยฒ, xโ[0,โy]", integrand: "x^2", xMin: "0", xMax: "sqrt(y)", yMin: "0", yMax: "4", order: "dxdy" }] },
+ { label: "Special Functions", presets: [{ name: "Bessel Jโ(xยฒ+yยฒ)", integrand: "besselj0(x^2+y^2)", xMin: "0", xMax: "3", yMin: "0", yMax: "3", order: "dydx" }, { name: "Error function erf(x+y)", integrand: "erf(x+y)", xMin: "0", xMax: "2", yMin: "0", yMax: "2", order: "dydx" }, { name: "Gamma ฮ(x+y)", integrand: "gamma(x+y)", xMin: "0.1", xMax: "1", yMin: "0.1", yMax: "1", order: "dydx" }, { name: "Heaviside H(x-y)", integrand: "heaviside(x-y)", xMin: "0", xMax: "2", yMin: "0", yMax: "2", order: "dydx" }] },
+ { label: "Infinite/Improper", presets: [{ name: "Gaussian over โยฒ", integrand: "exp(-x^2-y^2)", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" }, { name: "Exponential decay", integrand: "exp(-abs(x)-abs(y))", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" }, { name: "Semi-infinite: e^(-xy)", integrand: "exp(-x*y)", xMin: "0", xMax: "inf", yMin: "1", yMax: "2", order: "dydx" }, { name: "Bivariate normal", integrand: "exp(-(x^2+y^2)/2)/(2*pi)", xMin: "-inf", xMax: "inf", yMin: "-inf", yMax: "inf", order: "dydx" }] },
+ { label: "Oscillatory", presets: [{ name: "Rapid oscillation: sin(100x)cos(100y)", integrand: "sin(100*x)*cos(100*y)", xMin: "0", xMax: "2*pi", yMin: "0", yMax: "2*pi", order: "dydx" }, { name: "Fresnel: sin(xยฒ)cos(yยฒ)", integrand: "sin(x^2)*cos(y^2)", xMin: "0", xMax: "3", yMin: "0", yMax: "3", order: "dydx" }, { name: "Sinc: sin(xยฒ+yยฒ)/(xยฒ+yยฒ)", integrand: "sinc(x^2+y^2)", xMin: "0", xMax: "5", yMin: "0", yMax: "5", order: "dydx" }] },
];
export default function DoubleIntegralSolver() {
@@ -575,22 +554,34 @@ export default function DoubleIntegralSolver() {
}, 100);
}
- const CLR = { accent: '#7c3aed', light: '#f5f3ff', border: '#c4b5fd', text: '#1f2937', sub: '#6b7280' };
-
return (
-
+
+
+ {/* โโ Page title โโ */}
-
โฌ Double Integral Solver
-
All complex cases: infinite bounds, singularities, special functions, oscillatory integrands
+
+ โฌ Double Integral Solver
+
+
+ All complex cases: infinite bounds, singularities, special functions, oscillatory integrands
+
-
-
-
Configure Integral
+ {/* โโ Two-column grid โโ */}
+
+ {/* โโ LEFT: Configure panel โโ */}
+
+
+ Configure Integral
+
+
+ {/* Integrand input */}
-
+
-
-
+ {/* Bounds grid */}
+
{[{ l: 'x min', v: xMin, s: setXMin }, { l: 'x max', v: xMax, s: setXMax }, { l: 'y min', v: yMin, s: setYMin }, { l: 'y max', v: yMax, s: setYMax }].map((b, i) => (
-
- b.s(e.target.value)} style={{ width: '100%', padding: '10px', fontSize: '13px', border: `2px solid ${CLR.border}`, borderRadius: '8px', fontFamily: 'monospace' }} />
+
+ b.s(e.target.value)}
+ style={{ width: '100%', padding: '10px', fontSize: '13px', border: '2px solid var(--cv-border)', borderRadius: '8px', fontFamily: 'monospace', background: 'var(--cv-input-bg)', color: 'var(--cv-text-primary)' }}
+ />
))}
+ {/* Integration order */}
-
+
{['dydx', 'dxdy'].map(o => (
- setOrder(o)} style={{ flex: 1, padding: '10px', fontSize: '13px', fontWeight: '600', border: `2px solid ${order === o ? CLR.accent : CLR.border}`, background: order === o ? CLR.light : 'white', color: order === o ? CLR.accent : CLR.sub, borderRadius: '8px', cursor: 'pointer' }}>
+ setOrder(o)}
+ className={`cv-btn ${order === o ? 'cv-btn--operator' : 'cv-btn--function'}`}
+ style={{ flex: 1, minHeight: '44px', fontSize: '13px', borderRadius: '8px' }}
+ >
{o === 'dydx' ? 'โซโซ f dy dx' : 'โซโซ f dx dy'}
))}
-
+ {/* Solve button */}
+
{computing ? 'Computing...' : '๐งฎ Solve Integral'}
-
-
Preset Problems
+ {/* Presets */}
+
+
+ Preset Problems
+
{PRESETS.map((c, i) => (
- setActiveCat(i)} style={{ padding: '5px 10px', fontSize: '11px', fontWeight: '600', border: `1.5px solid ${activeCat === i ? CLR.accent : '#e5e7eb'}`, background: activeCat === i ? CLR.light : '#f9fafb', color: activeCat === i ? CLR.accent : '#6b7280', borderRadius: '6px', cursor: 'pointer' }}>{c.label}
+ setActiveCat(i)}
+ className={`cv-btn ${activeCat === i ? 'cv-btn--operator' : 'cv-btn--function'}`}
+ style={{ padding: '5px 10px', fontSize: '11px', minHeight: '30px', borderRadius: '6px' }}
+ >
+ {c.label}
+
))}
{PRESETS[activeCat].presets.map((p, i) => (
- loadPreset(p)} style={{ padding: '8px 10px', background: '#f8f9fb', border: '1.5px solid #e5e7eb', borderRadius: '6px', cursor: 'pointer', textAlign: 'left', fontSize: '12px' }}>{p.name}
+ loadPreset(p)}
+ className="cv-btn cv-btn--digit"
+ style={{ padding: '8px 10px', textAlign: 'left', fontSize: '12px', minHeight: '36px', borderRadius: '6px' }}
+ >
+ {p.name}
+
))}
-
-
Solution
+ {/* โโ RIGHT: Solution panel โโ */}
+
+
+ Solution
+
- {error &&
Error: {error}
}
+ {error && (
+
+ Error: {error}
+
+ )}
{result && (
-
-
RESULT
-
{result.result.toFixed(10)}
- {result.analytical &&
Analytical: {result.analytical}
}
+
+
RESULT
+
+ {result.result.toFixed(10)}
+
+ {result.analytical && (
+
+ Analytical: {result.analytical}
+
+ )}
{showSteps && result.steps.map((step, i) => (
-
-
Step {i + 1}: {step.title}
-
{step.content}
-
+
+
+ Step {i + 1}: {step.title}
+
+
{step.content}
+
{step.formulaLatex ? (
) : (
@@ -691,11 +734,18 @@ export default function DoubleIntegralSolver() {
)}
- {!result && !error &&
โฌ
Enter integral and click Solve
}
+ {!result && !error && (
+
+
โฌ
+
Enter integral and click Solve
+
+ )}
-
-
SYNTAX
-
x^2, sqrt(x), exp(x), ln(x), sin(x), besselj0(x), erf(x), gamma(x), inf
+
+
SYNTAX
+
+ x^2, sqrt(x), exp(x), ln(x), sin(x), besselj0(x), erf(x), gamma(x), inf
+
diff --git a/src/styles/LatexKeyboard.css b/src/styles/LatexKeyboard.css
new file mode 100644
index 0000000..bd3b86a
--- /dev/null
+++ b/src/styles/LatexKeyboard.css
@@ -0,0 +1,312 @@
+/* ============================================================
+ LatexKeyboard.css โ styles for the LaTeX math keyboard
+ Requires theme.css to be loaded first for CV tokens.
+ ============================================================ */
+
+.lkb {
+ display: flex;
+ flex-direction: column;
+ background: var(--cv-bg-surface);
+ border: 1px solid var(--cv-border);
+ border-radius: var(--cv-radius-lg);
+ overflow: hidden;
+ font-family: var(--cv-font-ui);
+ width: 100%;
+}
+
+/* โโ Input row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb__input-row {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ border-bottom: 1px solid var(--cv-border);
+}
+
+.lkb__input {
+ flex: 1;
+ resize: none;
+ border: none;
+ outline: none;
+ padding: 10px 14px;
+ font-family: var(--cv-font-mono);
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--cv-text-primary);
+ background: var(--cv-bg-raised);
+ min-height: 56px;
+}
+
+.lkb__input::placeholder {
+ color: var(--cv-input-placeholder);
+ font-family: var(--cv-font-ui);
+ font-size: 13px;
+}
+
+.lkb__input:focus {
+ background: var(--cv-input-bg);
+ box-shadow: inset 0 0 0 2px var(--cv-input-focus);
+}
+
+.lkb__clear-btn {
+ padding: 0 14px;
+ border: none;
+ background: var(--cv-bg-sunken);
+ color: var(--cv-text-muted);
+ cursor: pointer;
+ font-size: 14px;
+ transition: color var(--cv-transition-fast), background var(--cv-transition-fast);
+ border-left: 1px solid var(--cv-border);
+}
+
+.lkb__clear-btn:hover {
+ color: var(--cv-error);
+ background: var(--cv-error-light);
+}
+
+/* โโ Preview area โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb__preview {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 16px;
+ min-height: 52px;
+ border-bottom: 1px solid var(--cv-border);
+ background: var(--cv-display-bg);
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: thin;
+}
+
+.lkb__preview-label {
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cv-display-expr);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.lkb__preview-math {
+ color: var(--cv-display-text);
+ /* KaTeX uses its own sizing โ constrain extreme overflow */
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+/* KaTeX light/dark handling โ ensure readability on dark bg */
+.lkb__preview-math .katex {
+ color: var(--cv-display-text);
+}
+
+.lkb__preview-empty {
+ font-size: 13px;
+ color: var(--cv-display-expr);
+ font-style: italic;
+}
+
+/* โโ Tab strip โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb__tabs {
+ display: flex;
+ border-bottom: 1px solid var(--cv-border);
+ background: var(--cv-bg-raised);
+ overflow-x: auto;
+ scrollbar-width: none;
+}
+
+.lkb__tabs::-webkit-scrollbar { display: none; }
+
+.lkb__tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 16px;
+ border: none;
+ background: transparent;
+ color: var(--cv-text-secondary);
+ font-size: 13px;
+ font-weight: 400;
+ cursor: pointer;
+ white-space: nowrap;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ transition:
+ color var(--cv-transition-fast),
+ border-color var(--cv-transition-fast);
+}
+
+.lkb__tab:hover {
+ color: var(--cv-text-primary);
+ background: var(--cv-bg-overlay);
+}
+
+.lkb__tab--active {
+ color: var(--cv-accent);
+ border-bottom-color: var(--cv-accent);
+ font-weight: 500;
+}
+
+.lkb__tab-icon {
+ font-size: 15px;
+ line-height: 1;
+}
+
+/* โโ Key panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb__panel {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
+ gap: 4px;
+ padding: 10px;
+ background: var(--cv-bg-sunken);
+}
+
+.lkb__key {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 44px;
+ padding: 4px 6px;
+ border-radius: var(--cv-radius-sm);
+ border: 1px solid var(--cv-border);
+ background: var(--cv-btn-digit-bg);
+ color: var(--cv-btn-digit-text);
+ font-size: 15px;
+ font-family: var(--cv-font-math);
+ cursor: pointer;
+ transition:
+ background-color var(--cv-transition-fast),
+ transform var(--cv-transition-fast),
+ border-color var(--cv-transition-fast);
+ touch-action: manipulation;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.lkb__key:hover {
+ background: var(--cv-btn-digit-hover);
+ border-color: var(--cv-border-strong);
+}
+
+.lkb__key:active {
+ transform: scale(0.92);
+ background: var(--cv-accent-light);
+ color: var(--cv-accent-text);
+ border-color: var(--cv-accent);
+}
+
+.lkb__key:focus-visible {
+ outline: 2px solid var(--cv-accent);
+ outline-offset: 1px;
+}
+
+/* โโ Footer / copy row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb__footer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-top: 1px solid var(--cv-border);
+ background: var(--cv-bg-raised);
+ min-height: 38px;
+ overflow: hidden;
+}
+
+.lkb__raw-label {
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--cv-text-muted);
+ flex-shrink: 0;
+}
+
+.lkb__raw {
+ flex: 1;
+ font-family: var(--cv-font-mono);
+ font-size: 12px;
+ color: var(--cv-text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.lkb__copy-btn {
+ flex-shrink: 0;
+ padding: 4px 12px;
+ border-radius: var(--cv-radius-pill);
+ border: 1px solid var(--cv-border-strong);
+ background: var(--cv-bg-surface);
+ color: var(--cv-text-secondary);
+ font-size: 12px;
+ cursor: pointer;
+ transition: background var(--cv-transition-fast), color var(--cv-transition-fast);
+}
+
+.lkb__copy-btn:hover:not(:disabled) {
+ background: var(--cv-accent-light);
+ color: var(--cv-accent-text);
+ border-color: var(--cv-accent);
+}
+
+.lkb__copy-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+/* โโ Compact variant (icon-only tabs) โโโโโโโโโโโโโโโโโโโโโโโโ */
+.lkb--compact .lkb__tab {
+ padding: 8px 12px;
+}
+
+.lkb--compact .lkb__tab-label {
+ display: none;
+}
+
+.lkb--compact .lkb__panel {
+ grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
+}
+
+.lkb--compact .lkb__key {
+ min-height: 40px;
+ font-size: 14px;
+}
+
+/* โโ Responsive โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+@media (max-width: 600px) {
+ .lkb__panel {
+ grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
+ gap: 3px;
+ padding: 8px;
+ }
+
+ .lkb__key {
+ min-height: 44px;
+ font-size: 14px;
+ }
+
+ .lkb__tab {
+ padding: 8px 12px;
+ }
+
+ .lkb__tab-label {
+ display: none;
+ }
+
+ .lkb__preview {
+ padding: 8px 12px;
+ min-height: 46px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .lkb__key,
+ .lkb__tab,
+ .lkb__input,
+ .lkb__copy-btn,
+ .lkb__clear-btn {
+ transition: none;
+ }
+ .lkb__key:active {
+ transform: none;
+ }
+}
diff --git a/src/styles/responsive.css b/src/styles/responsive.css
new file mode 100644
index 0000000..f6b7ba7
--- /dev/null
+++ b/src/styles/responsive.css
@@ -0,0 +1,343 @@
+/* ============================================================
+ CalcVoyager โ Responsive Utilities
+ Import after theme.css. Covers calculator grid, layout,
+ touch targets, and typography scaling.
+ ============================================================ */
+
+/* โโ Breakpoints (reference โ used in media queries below)
+ xs: < 400px (very small phones)
+ sm: < 600px (phones)
+ md: < 900px (tablets / landscape phones)
+ lg: >= 900px (desktops)
+ ============================================================ */
+
+/* โโ App shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+.cv-app {
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ background-color: var(--cv-bg-page);
+}
+
+.cv-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 20px;
+ background: var(--cv-bg-surface);
+ border-bottom: 1px solid var(--cv-border);
+ gap: 12px;
+}
+
+.cv-header-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--cv-text-primary);
+ margin: 0;
+}
+
+.cv-main {
+ flex: 1;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding: 24px 16px;
+}
+
+/* โโ Calculator card โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-calculator {
+ background: var(--cv-bg-surface);
+ border-radius: var(--cv-radius-lg);
+ box-shadow: var(--cv-shadow-lg);
+ border: 1px solid var(--cv-border);
+ width: 100%;
+ max-width: 420px;
+ overflow: hidden;
+}
+
+/* โโ Display panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-display {
+ background: var(--cv-display-bg);
+ padding: 20px 20px 16px;
+ min-height: 100px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 4px;
+}
+
+.cv-display__expr {
+ font-family: var(--cv-font-mono);
+ font-size: 13px;
+ color: var(--cv-display-expr);
+ min-height: 18px;
+ word-break: break-all;
+ text-align: right;
+}
+
+.cv-display__value {
+ font-family: var(--cv-font-mono);
+ font-size: 36px;
+ font-weight: 300;
+ color: var(--cv-display-text);
+ word-break: break-all;
+ text-align: right;
+ line-height: 1.2;
+}
+
+/* โโ Button grid โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-keypad {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1px;
+ background: var(--cv-border);
+ padding: 1px;
+}
+
+.cv-keypad--scientific {
+ grid-template-columns: repeat(5, 1fr);
+}
+
+/* โโ Individual buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-btn {
+ /* min touch target */
+ min-height: 60px;
+ min-width: 44px;
+ padding: 0 8px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ font-family: var(--cv-font-ui);
+ font-size: 18px;
+ font-weight: 400;
+ cursor: pointer;
+ border: none;
+ outline: none;
+ border-radius: 0;
+
+ transition:
+ background-color var(--cv-transition-fast),
+ transform var(--cv-transition-fast);
+
+ /* Prevent double-tap zoom on mobile */
+ touch-action: manipulation;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.cv-btn:active {
+ transform: scale(0.94);
+}
+
+.cv-btn:focus-visible {
+ outline: 2px solid var(--cv-accent);
+ outline-offset: -2px;
+ z-index: 1;
+}
+
+/* Button variants */
+.cv-btn--digit {
+ background: var(--cv-btn-digit-bg);
+ color: var(--cv-btn-digit-text);
+}
+.cv-btn--digit:hover { background: var(--cv-btn-digit-hover); }
+
+.cv-btn--operator {
+ background: var(--cv-btn-op-bg);
+ color: var(--cv-btn-op-text);
+ font-weight: 500;
+}
+.cv-btn--operator:hover { background: var(--cv-btn-op-hover); }
+
+.cv-btn--function {
+ background: var(--cv-btn-fn-bg);
+ color: var(--cv-btn-fn-text);
+ font-size: 14px;
+}
+.cv-btn--function:hover { background: var(--cv-btn-fn-hover); }
+
+.cv-btn--equals {
+ background: var(--cv-btn-eq-bg);
+ color: var(--cv-btn-eq-text);
+ font-weight: 600;
+}
+.cv-btn--equals:hover { background: var(--cv-btn-eq-hover); }
+
+.cv-btn--clear {
+ background: var(--cv-btn-clear-bg);
+ color: var(--cv-btn-clear-text);
+ font-weight: 500;
+}
+.cv-btn--clear:hover { background: var(--cv-btn-clear-hover); }
+
+.cv-btn--wide {
+ grid-column: span 2;
+}
+
+/* โโ History panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-history {
+ background: var(--cv-bg-surface);
+ border: 1px solid var(--cv-border);
+ border-radius: var(--cv-radius-md);
+ overflow: hidden;
+}
+
+.cv-history__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--cv-border);
+ background: var(--cv-bg-raised);
+}
+
+.cv-history__title {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--cv-text-secondary);
+ margin: 0;
+}
+
+.cv-history__list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 280px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+
+.cv-history__item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--cv-border);
+ cursor: pointer;
+ transition: background var(--cv-transition-fast);
+}
+
+.cv-history__item:last-child { border-bottom: none; }
+.cv-history__item:hover { background: var(--cv-bg-overlay); }
+
+.cv-history__expr {
+ font-size: 12px;
+ color: var(--cv-text-muted);
+ font-family: var(--cv-font-mono);
+}
+
+.cv-history__result {
+ font-size: 16px;
+ color: var(--cv-text-primary);
+ font-family: var(--cv-font-mono);
+}
+
+/* โโ Utility classes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ*/
+.cv-sr-only {
+ position: absolute;
+ width: 1px; height: 1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ white-space: nowrap;
+}
+
+.cv-visually-hidden { @extend .cv-sr-only; }
+
+/* โโ Responsive overrides โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+/* Tablet: loosen layout a little */
+@media (max-width: 900px) {
+ .cv-main {
+ padding: 20px 12px;
+ }
+ .cv-calculator {
+ max-width: 480px;
+ }
+}
+
+/* Phone: tighter, finger-friendly */
+@media (max-width: 600px) {
+ .cv-header {
+ padding: 10px 14px;
+ }
+ .cv-header-title {
+ font-size: 16px;
+ }
+ .cv-main {
+ padding: 12px 8px;
+ align-items: flex-start;
+ }
+ .cv-calculator {
+ max-width: 100%;
+ border-radius: var(--cv-radius-md);
+ }
+ .cv-display {
+ padding: 16px 16px 12px;
+ min-height: 88px;
+ }
+ .cv-display__value {
+ font-size: 30px;
+ }
+ .cv-btn {
+ min-height: 56px;
+ font-size: 16px;
+ }
+ .cv-btn--function {
+ font-size: 12px;
+ }
+}
+
+/* Very small phones */
+@media (max-width: 400px) {
+ .cv-display__value {
+ font-size: 26px;
+ }
+ .cv-btn {
+ min-height: 52px;
+ font-size: 15px;
+ }
+ .cv-btn--function {
+ font-size: 11px;
+ }
+}
+
+/* โโ Landscape phone: two-column layout with display fixed โโ */
+@media (max-width: 768px) and (orientation: landscape) {
+ .cv-main {
+ padding: 8px;
+ }
+ .cv-calculator {
+ display: grid;
+ grid-template-columns: 1fr 1.6fr;
+ max-width: 640px;
+ max-height: 96dvh;
+ }
+ .cv-display {
+ min-height: unset;
+ justify-content: flex-end;
+ overflow-y: auto;
+ }
+ .cv-keypad {
+ overflow-y: auto;
+ max-height: 96dvh;
+ }
+ .cv-btn {
+ min-height: 48px;
+ }
+}
+
+/* โโ Reduced motion โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+@media (prefers-reduced-motion: reduce) {
+ .cv-btn {
+ transition: none;
+ }
+ .cv-btn:active {
+ transform: none;
+ }
+ body {
+ transition: none;
+ }
+}
diff --git a/src/styles/theme.css b/src/styles/theme.css
new file mode 100644
index 0000000..608a935
--- /dev/null
+++ b/src/styles/theme.css
@@ -0,0 +1,255 @@
+/* ============================================================
+ CalcVoyager โ Theme System
+ Usage: Add data-theme="dark" to to force dark mode.
+ Defaults to OS preference via prefers-color-scheme.
+ ============================================================ */
+
+:root {
+ /* Surfaces */
+ --cv-bg-page: #f5f5f5;
+ --cv-bg-surface: #ffffff;
+ --cv-bg-raised: #ffffff;
+ --cv-bg-sunken: #efefef;
+ --cv-bg-overlay: rgba(0, 0, 0, 0.04);
+
+ /* Borders */
+ --cv-border: rgba(0, 0, 0, 0.12);
+ --cv-border-strong: rgba(0, 0, 0, 0.24);
+
+ /* Text */
+ --cv-text-primary: #111111;
+ --cv-text-secondary: #555555;
+ --cv-text-muted: #888888;
+ --cv-text-inverse: #ffffff;
+
+ /* Brand / Accent */
+ --cv-accent: #2563eb;
+ --cv-accent-hover: #1d4ed8;
+ --cv-accent-light: #dbeafe;
+ --cv-accent-text: #1e40af;
+
+ /* Semantic */
+ --cv-success: #16a34a;
+ --cv-success-light: #dcfce7;
+ --cv-error: #dc2626;
+ --cv-error-light: #fee2e2;
+ --cv-warning: #d97706;
+ --cv-warning-light: #fef3c7;
+
+ /* Calculator-specific button tokens */
+ --cv-btn-digit-bg: #ffffff;
+ --cv-btn-digit-text: #111111;
+ --cv-btn-digit-hover: #f0f0f0;
+ --cv-btn-op-bg: #e8f0fe;
+ --cv-btn-op-text: #1d4ed8;
+ --cv-btn-op-hover: #dbeafe;
+ --cv-btn-fn-bg: #f3f3f3;
+ --cv-btn-fn-text: #444444;
+ --cv-btn-fn-hover: #e8e8e8;
+ --cv-btn-eq-bg: #2563eb;
+ --cv-btn-eq-text: #ffffff;
+ --cv-btn-eq-hover: #1d4ed8;
+ --cv-btn-clear-bg: #fee2e2;
+ --cv-btn-clear-text: #b91c1c;
+ --cv-btn-clear-hover: #fecaca;
+
+ /* Display */
+ --cv-display-bg: #1a1a2e;
+ --cv-display-text: #e2e8f0;
+ --cv-display-expr: #94a3b8;
+ --cv-display-cursor: #60a5fa;
+
+ /* Input / Form */
+ --cv-input-bg: #ffffff;
+ --cv-input-border: rgba(0, 0, 0, 0.18);
+ --cv-input-focus: #2563eb;
+ --cv-input-placeholder: #aaaaaa;
+
+ /* Shadow */
+ --cv-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --cv-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.10);
+ --cv-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
+
+ /* Typography */
+ --cv-font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --cv-font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
+ --cv-font-math: 'KaTeX_Main', 'Computer Modern', serif;
+
+ /* Motion */
+ --cv-transition-fast: 100ms ease;
+ --cv-transition-base: 180ms ease;
+ --cv-transition-slow: 300ms ease;
+
+ /* Radius */
+ --cv-radius-sm: 6px;
+ --cv-radius-md: 10px;
+ --cv-radius-lg: 16px;
+ --cv-radius-xl: 24px;
+ --cv-radius-pill: 999px;
+}
+
+/* โโ Dark mode via OS preference โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --cv-bg-page: #0f0f13;
+ --cv-bg-surface: #1a1a22;
+ --cv-bg-raised: #222230;
+ --cv-bg-sunken: #12121a;
+ --cv-bg-overlay: rgba(255, 255, 255, 0.05);
+
+ --cv-border: rgba(255, 255, 255, 0.10);
+ --cv-border-strong: rgba(255, 255, 255, 0.22);
+
+ --cv-text-primary: #f0f0f5;
+ --cv-text-secondary: #a0a0b0;
+ --cv-text-muted: #666680;
+ --cv-text-inverse: #111111;
+
+ --cv-accent: #60a5fa;
+ --cv-accent-hover: #93c5fd;
+ --cv-accent-light: #1e3a5f;
+ --cv-accent-text: #93c5fd;
+
+ --cv-success: #4ade80;
+ --cv-success-light: #14532d;
+ --cv-error: #f87171;
+ --cv-error-light: #7f1d1d;
+ --cv-warning: #fbbf24;
+ --cv-warning-light: #78350f;
+
+ --cv-btn-digit-bg: #222230;
+ --cv-btn-digit-text: #f0f0f5;
+ --cv-btn-digit-hover: #2c2c3e;
+ --cv-btn-op-bg: #1e2d4a;
+ --cv-btn-op-text: #93c5fd;
+ --cv-btn-op-hover: #253a5e;
+ --cv-btn-fn-bg: #1d1d28;
+ --cv-btn-fn-text: #c0c0d0;
+ --cv-btn-fn-hover: #252535;
+ --cv-btn-eq-bg: #3b82f6;
+ --cv-btn-eq-text: #ffffff;
+ --cv-btn-eq-hover: #60a5fa;
+ --cv-btn-clear-bg: #4a1515;
+ --cv-btn-clear-text: #fca5a5;
+ --cv-btn-clear-hover: #5c1c1c;
+
+ --cv-display-bg: #080810;
+ --cv-display-text: #e2e8f0;
+ --cv-display-expr: #64748b;
+ --cv-display-cursor: #60a5fa;
+
+ --cv-input-bg: #1a1a22;
+ --cv-input-border: rgba(255, 255, 255, 0.14);
+ --cv-input-focus: #60a5fa;
+ --cv-input-placeholder: #555566;
+
+ --cv-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.40);
+ --cv-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.50);
+ --cv-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.60);
+ }
+}
+
+/* โโ Manual override: force dark โโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+[data-theme="dark"] {
+ --cv-bg-page: #0f0f13;
+ --cv-bg-surface: #1a1a22;
+ --cv-bg-raised: #222230;
+ --cv-bg-sunken: #12121a;
+ --cv-bg-overlay: rgba(255, 255, 255, 0.05);
+
+ --cv-border: rgba(255, 255, 255, 0.10);
+ --cv-border-strong: rgba(255, 255, 255, 0.22);
+
+ --cv-text-primary: #f0f0f5;
+ --cv-text-secondary: #a0a0b0;
+ --cv-text-muted: #666680;
+ --cv-text-inverse: #111111;
+
+ --cv-accent: #60a5fa;
+ --cv-accent-hover: #93c5fd;
+ --cv-accent-light: #1e3a5f;
+ --cv-accent-text: #93c5fd;
+
+ --cv-success: #4ade80;
+ --cv-success-light: #14532d;
+ --cv-error: #f87171;
+ --cv-error-light: #7f1d1d;
+ --cv-warning: #fbbf24;
+ --cv-warning-light: #78350f;
+
+ --cv-btn-digit-bg: #222230;
+ --cv-btn-digit-text: #f0f0f5;
+ --cv-btn-digit-hover: #2c2c3e;
+ --cv-btn-op-bg: #1e2d4a;
+ --cv-btn-op-text: #93c5fd;
+ --cv-btn-op-hover: #253a5e;
+ --cv-btn-fn-bg: #1d1d28;
+ --cv-btn-fn-text: #c0c0d0;
+ --cv-btn-fn-hover: #252535;
+ --cv-btn-eq-bg: #3b82f6;
+ --cv-btn-eq-text: #ffffff;
+ --cv-btn-eq-hover: #60a5fa;
+ --cv-btn-clear-bg: #4a1515;
+ --cv-btn-clear-text: #fca5a5;
+ --cv-btn-clear-hover: #5c1c1c;
+
+ --cv-display-bg: #080810;
+ --cv-display-text: #e2e8f0;
+ --cv-display-expr: #64748b;
+ --cv-display-cursor: #60a5fa;
+
+ --cv-input-bg: #1a1a22;
+ --cv-input-border: rgba(255, 255, 255, 0.14);
+ --cv-input-focus: #60a5fa;
+ --cv-input-placeholder: #555566;
+
+ --cv-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.40);
+ --cv-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.50);
+ --cv-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.60);
+}
+
+/* โโ Manual override: force light โโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+[data-theme="light"] {
+ /* explicit resets handled by :root defaults above */
+ color-scheme: light;
+}
+
+/* โโ Base resets using theme tokens โโโโโโโโโโโโโโโโโโโโโโโโ */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: var(--cv-font-ui);
+ background-color: var(--cv-bg-page);
+ color: var(--cv-text-primary);
+ transition: background-color var(--cv-transition-slow), color var(--cv-transition-slow);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* โโ Dark mode toggle button (drop anywhere in layout) โโโโโโ */
+.cv-theme-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 14px;
+ border-radius: var(--cv-radius-pill);
+ border: 1px solid var(--cv-border-strong);
+ background: var(--cv-bg-raised);
+ color: var(--cv-text-secondary);
+ font-size: 13px;
+ font-family: var(--cv-font-ui);
+ cursor: pointer;
+ transition: background var(--cv-transition-fast), color var(--cv-transition-fast);
+}
+
+.cv-theme-toggle:hover {
+ background: var(--cv-bg-sunken);
+ color: var(--cv-text-primary);
+}
+
+.cv-theme-toggle .cv-theme-icon {
+ font-size: 15px;
+ line-height: 1;
+}