Merge pull request 'Add theme switcher, tool search, and vendored Bootstrap Icons (v0.6.4)' (#1) from theme-switcher into main

Reviewed-on: https://codeberg.org/listyantidewi/your-everyday-tools/pulls/1
This commit is contained in:
listyantidewi
2026-06-09 16:36:00 +02:00
16 changed files with 470 additions and 84 deletions
+13
View File
@@ -2,6 +2,19 @@
All notable changes to **Your Everyday Tools** are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project loosely follows [Semantic Versioning](https://semver.org/).
## [0.6.4] — 2026-06-07
### Added — UI navigation and theming
- Added a theme switcher (system / light / dark) in the top bar. Preference is persisted in `localStorage` and applied before first paint to avoid flash.
- Added tool search on the home page — filter tool cards by name or description.
- Added global search in the top bar — available on every page with a dropdown of matching tools.
### Changed — Icons and theme UX
- Replaced the Bootstrap Icons shim with fully vendored font files (`woff` / `woff2`) for offline use.
- Theme transitions animate smoothly; the active theme button shows a brief spin on click.
## [0.6.3] — 2026-06-06
### Added — Local conversion fidelity layer
+6 -5
View File
@@ -365,8 +365,8 @@ your-everyday-tools/
│ ├── media_tools.py # FFmpeg-powered audio & video tools
│ └── capabilities.py # /capabilities endpoint
├── templates/
│ ├── base.html # Main layout (sidebar + content area)
│ ├── index.html # Home page with tool cards
│ ├── base.html # Main layout (sidebar, global search, theme switcher)
│ ├── index.html # Home page with tool cards and search
│ ├── upload_tool.html # Universal template for all file-based tools
│ └── tools/ # Individual client-side tool templates
│ ├── calculator.html
@@ -393,8 +393,9 @@ your-everyday-tools/
│ └── hash_generator.html
└── static/
├── css/style.css # All styles, no framework
├── css/icons.css # Local icon shim; no CDN required
── js/main.js # File upload, AJAX, sidebar, shared logic
├── css/icons.css # Vendored Bootstrap Icons; no CDN required
── fonts/bootstrap-icons.woff2 # Bootstrap Icons font files
└── js/main.js # Sidebar, theme, search, file upload, shared logic
```
### Architecture Notes
@@ -402,7 +403,7 @@ your-everyday-tools/
- **One universal template** — `upload_tool.html` powers all 25+ server-side tools. Each route passes title, description, accepted file types, and form options as template variables. No per-tool template duplication.
- **Client-side tools** (text utilities, calculators, security tools) run entirely in the browser with vanilla JavaScript — zero server round-trips.
- **Local-first processing** — pure browser tools never leave the page; server routes process files locally. Some engines such as LibreOffice, FFmpeg, ODA, and pdf2docx use isolated temporary directories when their CLI/library workflow requires files.
- **No CSS framework or CDN dependency** — custom CSS with CSS Grid, Flexbox, CSS custom properties, and a local icon shim.
- **No CSS framework or CDN dependency** — custom CSS with CSS Grid, Flexbox, CSS custom properties, and vendored Bootstrap Icons.
- **Graceful degradation** — optional packages and external binaries (`LibreOffice`, `FFmpeg`, `ffprobe`, `Tesseract`, ODA File Converter, `rembg`, `pyzbar`, `pdf2docx`, `pdfplumber`, `pytesseract`, `pillow-heif`, Whisper, etc.) are reported through `/capabilities` and tool-page status banners. Missing high-fidelity engines either show a clear unavailable state or require explicit basic fallback consent.
---
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 461 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 229 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 491 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 230 KiB

+5 -62
View File
File diff suppressed because one or more lines are too long
+218 -14
View File
@@ -7,17 +7,57 @@
--success: #2ec4b6;
--danger: #e63946;
--warning: #f4a261;
/* surface palette */
--bg: #f5f6fa;
--surface: #ffffff;
--text: #2d3436;
--text-light: #636e72;
--muted: var(--text-light);
--border: #dfe6e9;
--shadow: 0 2px 8px rgba(0,0,0,0.08);
/* semantic accent */
--accent-soft: #eef1ff;
--accent-soft-hover: #dde3ff;
/* notes/warning block */
--note-bg: #fff8e1;
--note-border: #f5b700;
--note-text: #5a4200;
--note-summary: #8a6300;
--note-link: #b26f00;
--note-code-bg: rgba(0,0,0,.06);
/* upload hover */
--upload-hover-bg: #f0f3ff;
/* result states */
--result-success-bg: #e8faf8;
--result-success-text: #1a7a6d;
--result-error-bg: #fdeaea;
/* layout */
--sidebar-w: 260px;
--radius: 8px;
--shadow: 0 2px 8px rgba(0,0,0,0.08);
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
[data-theme="dark"] {
--bg: #1a1d23;
--surface: #22262e;
--text: #e4e6eb;
--text-light: #9aa0ac;
--border: #3a3f4b;
--shadow: 0 2px 8px rgba(0,0,0,0.35);
--accent-soft: rgba(67,97,238,.2);
--accent-soft-hover: rgba(67,97,238,.35);
--note-bg: #2a2000;
--note-border: #c49a00;
--note-text: #f0c040;
--note-summary: #d4a800;
--note-link: #e8c000;
--note-code-bg: rgba(255,255,255,.08);
--upload-hover-bg: rgba(67,97,238,.12);
--result-success-bg: #082e2a;
--result-success-text: #4ecdc4;
--result-error-bg: #2d0a0a;
}
html { font-size: 15px; }
body {
font-family: var(--font);
@@ -98,7 +138,7 @@ a { color: var(--primary); text-decoration: none; }
transition: background .15s, color .15s;
}
.nav-item:hover, .nav-item.active {
background: #eef1ff;
background: var(--accent-soft);
color: var(--primary);
}
@@ -123,6 +163,87 @@ a { color: var(--primary); text-decoration: none; }
z-index: 50;
}
.top-title { font-weight: 600; font-size: 1.05rem; }
.global-search {
flex: 1;
position: relative;
max-width: 360px;
}
.global-search input {
width: 100%;
padding: .4rem .8rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-size: .9rem;
font-family: var(--font);
outline: none;
transition: border-color .15s, box-shadow .15s;
}
.global-search input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67,97,238,.12);
}
.global-search-dropdown {
display: none;
position: absolute;
top: calc(100% + .4rem);
left: 0;
right: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
max-height: 320px;
overflow-y: auto;
z-index: 200;
}
.global-search-dropdown.open { display: block; }
.global-search-result {
display: flex;
align-items: center;
gap: .75rem;
padding: .55rem .85rem;
color: var(--text);
font-size: .875rem;
text-decoration: none;
transition: background .1s;
}
.global-search-result:hover,
.global-search-result.active { background: var(--bg); color: var(--text); }
.global-search-result .result-info {
display: flex;
flex-direction: column;
gap: .1rem;
min-width: 0;
}
.global-search-result .result-name { font-weight: 500; }
.global-search-result .result-desc {
font-size: .75rem;
color: var(--text-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-search-result .result-cat {
margin-left: auto;
flex-shrink: 0;
font-size: .72rem;
color: var(--text-light);
white-space: nowrap;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: .1rem .4rem;
}
.global-search-empty {
padding: .8rem;
color: var(--text-light);
font-size: .85rem;
text-align: center;
}
.menu-btn {
display: none;
border: none;
@@ -132,6 +253,58 @@ a { color: var(--primary); text-decoration: none; }
color: var(--text);
}
/* ── Theme Switcher ───────────────────────────── */
.theme-switcher {
display: flex;
align-items: center;
gap: 2px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 20px;
padding: 3px;
margin-left: auto;
transition: background .3s ease, border-color .3s ease;
}
.theme-btn {
border: none;
background: none;
cursor: pointer;
color: var(--text-light);
border-radius: 16px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: .9rem;
transition: background .2s ease, color .2s ease, transform .2s ease, box-shadow .2s ease;
}
.theme-btn:hover { background: var(--border); color: var(--text); transform: scale(1.1); }
.theme-btn:active { transform: scale(.92); }
.theme-btn.active {
background: var(--surface);
color: var(--primary);
box-shadow: 0 1px 4px rgba(0,0,0,.18);
}
.theme-btn i {
transition: transform .35s ease;
}
.theme-btn.spinning i {
transform: rotate(360deg);
}
/* ── Theme transition (only when user switches, not on page load) ── */
html.theme-animate,
html.theme-animate *,
html.theme-animate *::before,
html.theme-animate *::after {
transition:
background-color .35s ease,
color .35s ease,
border-color .35s ease,
box-shadow .35s ease !important;
}
.content-area {
flex: 1;
padding: 2rem;
@@ -180,6 +353,34 @@ a { color: var(--primary); text-decoration: none; }
.home-hero h1 { font-size: 2rem; margin-bottom: .4rem; }
.home-hero p { color: var(--text-light); font-size: 1.05rem; }
.home-search {
margin: 1.25rem auto 0;
max-width: 480px;
}
.home-search input {
width: 100%;
padding: .55rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
font-size: .95rem;
font-family: var(--font);
outline: none;
transition: border-color .15s, box-shadow .15s;
}
.home-search input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67,97,238,.12);
}
.search-empty {
text-align: center;
padding: 2rem;
color: var(--text-light);
font-size: .95rem;
}
.category-section { margin-bottom: 2rem; }
.category-title {
font-size: 1.1rem;
@@ -227,19 +428,19 @@ a { color: var(--primary); text-decoration: none; }
.tool-header p { color: var(--text-light); }
.tool-notes {
background: #fff8e1;
border-left: 3px solid #f5b700;
background: var(--note-bg);
border-left: 3px solid var(--note-border);
border-radius: 4px;
padding: .75rem 1rem;
margin-bottom: 1.25rem;
font-size: .88rem;
line-height: 1.5;
color: #5a4200;
color: var(--note-text);
}
.tool-notes p { margin: 0 0 .5rem; }
.tool-notes p:last-child { margin-bottom: 0; }
.tool-notes code {
background: rgba(0,0,0,.06);
background: var(--note-code-bg);
padding: .1rem .35rem;
border-radius: 3px;
font-size: .82rem;
@@ -248,12 +449,12 @@ a { color: var(--primary); text-decoration: none; }
.tool-notes details > summary {
cursor: pointer;
font-weight: 600;
color: #8a6300;
color: var(--note-summary);
}
.tool-notes details[open] > summary { margin-bottom: .5rem; }
.tool-notes ol, .tool-notes ul { margin: .25rem 0 .5rem 1.2rem; padding: 0; }
.tool-notes li { margin-bottom: .2rem; }
.tool-notes a { color: #b26f00; }
.tool-notes a { color: var(--note-link); }
.capability-status {
border: 1px solid var(--border);
@@ -308,7 +509,7 @@ a { color: var(--primary); text-decoration: none; }
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--primary);
background: #f0f3ff;
background: var(--upload-hover-bg);
}
.upload-zone input[type="file"] {
position: absolute;
@@ -362,6 +563,7 @@ textarea {
font: inherit;
font-size: .9rem;
background: var(--surface);
color: var(--text);
transition: border-color .15s;
}
input:focus, select:focus, textarea:focus {
@@ -397,12 +599,12 @@ textarea { resize: vertical; }
flex-wrap: wrap;
}
.result-success {
background: #e8faf8;
color: #1a7a6d;
background: var(--result-success-bg);
color: var(--result-success-text);
}
.result-success .btn { margin-left: auto; }
.result-error {
background: #fdeaea;
background: var(--result-error-bg);
color: var(--danger);
}
.result-text-box {
@@ -474,6 +676,8 @@ textarea { resize: vertical; }
font-family: "Consolas", "Monaco", monospace;
font-size: .85rem;
outline: none;
background: var(--surface);
color: var(--text);
}
.pane-body pre {
min-height: 250px;
@@ -511,8 +715,8 @@ textarea { resize: vertical; }
transition: background .1s;
}
.calc-btn:hover { background: var(--bg); }
.calc-btn.op { background: #eef1ff; color: var(--primary); font-weight: 600; }
.calc-btn.op:hover { background: #dde3ff; }
.calc-btn.op { background: var(--accent-soft); color: var(--primary); font-weight: 600; }
.calc-btn.op:hover { background: var(--accent-soft-hover); }
.calc-btn.eq { background: var(--primary); color: #fff; }
.calc-btn.eq:hover { background: var(--primary-dark); }
.calc-btn.span2 { grid-column: span 2; }
Binary file not shown.
Binary file not shown.
+198
View File
@@ -1,3 +1,61 @@
/* ── Theme ────────────────────────────────────── */
const THEME_KEY = "theme";
function getStoredTheme() {
return localStorage.getItem(THEME_KEY) || "system";
}
function resolveTheme(mode) {
if (mode === "dark") return "dark";
if (mode === "light") return "light";
return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
}
function applyTheme(mode, animate) {
localStorage.setItem(THEME_KEY, mode);
const html = document.documentElement;
if (animate) {
html.classList.add("theme-animate");
}
html.dataset.theme = resolveTheme(mode);
document.querySelectorAll(".theme-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.themeMode === mode);
});
if (animate) {
clearTimeout(applyTheme._timer);
applyTheme._timer = setTimeout(() => html.classList.remove("theme-animate"), 400);
}
}
function initTheme() {
const mode = getStoredTheme();
applyTheme(mode, false);
document.querySelectorAll(".theme-btn").forEach(btn => {
btn.addEventListener("click", () => {
// Spin the icon
const icon = btn.querySelector("i");
if (icon) {
btn.classList.remove("spinning");
void btn.offsetWidth; // force reflow to restart animation
btn.classList.add("spinning");
setTimeout(() => btn.classList.remove("spinning"), 380);
}
applyTheme(btn.dataset.themeMode, true);
});
});
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if (getStoredTheme() === "system") {
document.documentElement.dataset.theme = resolveTheme("system");
}
});
}
}
/* ── Sidebar ──────────────────────────────────── */
function toggleCategory(btn) {
btn.classList.toggle("open");
@@ -30,12 +88,152 @@ document.addEventListener("DOMContentLoaded", () => {
}
});
initTheme();
initToolSearch();
initGlobalSearch();
initUploadZone();
initToolForm();
initDependentOptions();
initCapabilityStatus();
});
/* ── Global Toolbar Search ────────────────────── */
function initGlobalSearch() {
const wrapper = document.getElementById("global-search");
const input = document.getElementById("global-search-input");
const dropdown = document.getElementById("global-search-dropdown");
if (!wrapper || !input || !dropdown) return;
// Hide on home page; show everywhere else
if (window.location.pathname === "/") {
wrapper.style.display = "none";
return;
}
wrapper.style.display = "";
// Build tool list from sidebar nav items (already in DOM)
const tools = [];
document.querySelectorAll(".nav-category").forEach(cat => {
const catBtn = cat.querySelector(".nav-category-btn");
const catName = catBtn ? catBtn.textContent.trim() : "";
cat.querySelectorAll(".nav-item").forEach(a => {
tools.push({
name: a.textContent.trim(),
href: a.getAttribute("href"),
cat: catName,
desc: a.dataset.desc || "",
});
});
});
let activeIdx = -1;
function openDropdown() {
dropdown.classList.add("open");
input.setAttribute("aria-expanded", "true");
}
function closeDropdown() {
dropdown.classList.remove("open");
input.setAttribute("aria-expanded", "false");
activeIdx = -1;
}
function renderResults(query) {
if (!query) { closeDropdown(); return; }
const matches = tools.filter(t =>
t.name.toLowerCase().includes(query) ||
t.cat.toLowerCase().includes(query) ||
t.desc.includes(query)
).slice(0, 8);
if (matches.length === 0) {
dropdown.innerHTML = `<div class="global-search-empty">No tools found.</div>`;
openDropdown();
return;
}
dropdown.innerHTML = matches.map((t, i) => `
<a href="${t.href}" class="global-search-result" role="option" data-idx="${i}">
<span class="result-info">
<span class="result-name">${escapeHtml(t.name)}</span>
<span class="result-desc">${escapeHtml(t.desc)}</span>
</span>
<span class="result-cat">${escapeHtml(t.cat)}</span>
</a>
`).join("");
activeIdx = -1;
openDropdown();
}
function setActive(idx) {
const items = dropdown.querySelectorAll(".global-search-result");
items.forEach(el => el.classList.remove("active"));
if (idx >= 0 && idx < items.length) {
items[idx].classList.add("active");
items[idx].scrollIntoView({ block: "nearest" });
}
activeIdx = idx;
}
input.addEventListener("input", () => {
renderResults(input.value.trim().toLowerCase());
});
input.addEventListener("keydown", e => {
const items = dropdown.querySelectorAll(".global-search-result");
if (e.key === "ArrowDown") {
e.preventDefault();
setActive(Math.min(activeIdx + 1, items.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive(Math.max(activeIdx - 1, 0));
} else if (e.key === "Enter") {
if (activeIdx >= 0 && items[activeIdx]) {
window.location.href = items[activeIdx].getAttribute("href");
}
} else if (e.key === "Escape") {
input.blur();
closeDropdown();
}
});
document.addEventListener("click", e => {
if (!wrapper.contains(e.target)) closeDropdown();
});
}
/* ── Home Page Tool Search ────────────────────── */
function initToolSearch() {
const input = document.getElementById("tool-search");
if (!input) return;
const cards = Array.from(document.querySelectorAll(".tool-card"));
const sections = Array.from(document.querySelectorAll(".category-section"));
const empty = document.getElementById("search-empty");
input.addEventListener("input", () => {
const query = input.value.trim().toLowerCase();
cards.forEach(card => {
const match = !query || card.dataset.search.includes(query);
card.style.display = match ? "" : "none";
});
sections.forEach(section => {
const hasVisible = Array.from(section.querySelectorAll(".tool-card"))
.some(c => c.style.display !== "none");
section.style.display = hasVisible ? "" : "none";
});
if (empty) {
const anyVisible = cards.some(c => c.style.display !== "none");
empty.style.display = anyVisible ? "none" : "";
}
});
}
async function initCapabilityStatus() {
const box = document.getElementById("capability-status");
if (!box) return;
+19 -1
View File
@@ -4,6 +4,15 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Your Everyday Tools{% endblock %}</title>
<script>
(function () {
var stored = localStorage.getItem('theme') || 'system';
var resolved = stored === 'dark' ? 'dark'
: stored === 'light' ? 'light'
: (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.dataset.theme = resolved;
})();
</script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/icons.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
@@ -21,7 +30,7 @@
</button>
<div class="nav-items">
{% for tool in cat.tools %}
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item">
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item" data-desc="{{ tool.desc | lower }}">
<i class="bi {{ tool.icon }}"></i> {{ tool.name }}
</a>
{% endfor %}
@@ -37,6 +46,15 @@
<header class="top-bar">
<button class="menu-btn" onclick="openSidebar()"><i class="bi bi-list"></i></button>
<span class="top-title">{% block top_title %}Your Everyday Tools{% endblock %}</span>
<div class="global-search" id="global-search">
<input type="text" id="global-search-input" placeholder="Search tools..." autocomplete="off" aria-label="Search tools" aria-expanded="false" aria-controls="global-search-dropdown">
<div class="global-search-dropdown" id="global-search-dropdown" role="listbox"></div>
</div>
<div class="theme-switcher" id="theme-switcher" role="group" aria-label="Color theme">
<button class="theme-btn" data-theme-mode="system" title="System theme"><i class="bi bi-circle-half"></i></button>
<button class="theme-btn" data-theme-mode="light" title="Light theme"><i class="bi bi-sun-fill"></i></button>
<button class="theme-btn" data-theme-mode="dark" title="Dark theme"><i class="bi bi-moon-fill"></i></button>
</div>
</header>
<div class="content-area">
{% block content %}{% endblock %}
+5 -1
View File
@@ -5,6 +5,9 @@
<div class="home-hero">
<h1>Your Everyday Tools</h1>
<p>All the utilities you need, in one place.</p>
<div class="home-search">
<input type="text" id="tool-search" placeholder="Search tools..." autocomplete="off">
</div>
</div>
{% for cat in tool_categories %}
@@ -14,7 +17,7 @@
</div>
<div class="tools-grid">
{% for tool in cat.tools %}
<a href="/{{ cat.id }}/{{ tool.id }}" class="tool-card">
<a href="/{{ cat.id }}/{{ tool.id }}" class="tool-card" data-search="{{ (tool.name ~ ' ' ~ tool.desc) | lower }}">
<div class="card-icon"><i class="bi {{ tool.icon }}"></i></div>
<div class="card-body">
<h3>{{ tool.name }}</h3>
@@ -25,4 +28,5 @@
</div>
</div>
{% endfor %}
<p id="search-empty" class="search-empty" style="display:none;">No tools match your search.</p>
{% endblock %}
+1 -1
View File
@@ -67,7 +67,7 @@ function showStrength(pw, poolSize) {
let label, color, pct;
if (entropy < 40) { label = "Weak"; color = "var(--danger)"; pct = 25; }
else if (entropy < 60) { label = "Fair"; color = "var(--warning)"; pct = 50; }
else if (entropy < 80) { label = "Strong"; color = "#2ec4b6"; pct = 75; }
else if (entropy < 80) { label = "Strong"; color = "var(--success)"; pct = 75; }
else { label = "Very Strong"; color = "var(--primary)"; pct = 100; }
document.getElementById("pw-strength").innerHTML =
+2
View File
@@ -46,6 +46,8 @@
.match-idx { color: var(--text-light); min-width: 2rem; }
.match-val { font-family: Consolas,Monaco,monospace; }
.match-pos { color: var(--text-light); font-size: .8rem; }
[data-theme="dark"] .regex-hl { background: rgba(244,162,97,.22); }
[data-theme="dark"] .regex-hl:nth-of-type(even) { background: rgba(23,162,184,.22); border-bottom-color: #17a2b8; }
</style>
<script>
function testRegex() {
+3
View File
@@ -39,6 +39,9 @@
.diff-stats { font-size: .85rem; margin-bottom: .5rem; }
.diff-stats .add { color: #28a745; font-weight: 600; }
.diff-stats .del { color: var(--danger); font-weight: 600; }
[data-theme="dark"] .diff-add { background: #0d2e14; }
[data-theme="dark"] .diff-del { background: #2e0d0d; }
[data-theme="dark"] .diff-stats .add { color: #4ecdc4; }
</style>
<script>
function computeDiff() {