diff --git a/templates/index.html b/templates/index.html
index 6614595..e05527e 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -157,10 +157,10 @@
{{template "cv-content.html" .}}
-
- {{template "page-footer" .}}
+ {{template "page-footer" .}}
+
{{template "error-toast" .}}
{{template "back-to-top" .}}
{{template "info-button" .}}
diff --git a/tests/mjs/11-zoom-ui-exclusion.test.mjs b/tests/mjs/11-zoom-ui-exclusion.test.mjs
new file mode 100644
index 0000000..0ad3f13
--- /dev/null
+++ b/tests/mjs/11-zoom-ui-exclusion.test.mjs
@@ -0,0 +1,243 @@
+#!/usr/bin/env bun
+/**
+ * ZOOM UI EXCLUSION TEST
+ * =======================
+ * Verifies that UI elements (footer, menu, buttons) are NOT affected by zoom
+ *
+ * Test Requirements:
+ * - Footer should NOT be inside #zoom-wrapper
+ * - Action bar should NOT be inside #zoom-wrapper
+ * - Hamburger menu should NOT be inside #zoom-wrapper
+ * - Fixed buttons should NOT be inside #zoom-wrapper
+ * - Only .cv-container content should be affected by zoom
+ *
+ * Test Flow:
+ * 1. Load page and show zoom control
+ * 2. Measure initial sizes of UI elements
+ * 3. Set zoom to 200%
+ * 4. Verify UI elements remain same size
+ * 5. Verify CV content has doubled in size
+ */
+
+import { chromium } from "playwright";
+
+const URL = "http://localhost:1999";
+
+async function testZoomUIExclusion() {
+ console.log("๐งช ZOOM UI EXCLUSION TEST\n");
+ console.log("=".repeat(70));
+
+ const browser = await chromium.launch({ headless: false });
+ const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
+
+ const errors = [];
+ const testResults = [];
+
+ page.on('console', msg => {
+ const text = msg.text();
+ if (msg.type() === 'error') {
+ errors.push(text);
+ console.log(`โ ERROR: ${text}`);
+ }
+ });
+
+ page.on('pageerror', err => {
+ errors.push(err.message);
+ console.log(`โ PAGE ERROR: ${err.message}`);
+ });
+
+ // ========================================================================
+ // TEST 1: Verify DOM Structure
+ // ========================================================================
+ console.log("\n1๏ธโฃ Verifying DOM structure (UI outside zoom-wrapper)...");
+ await page.goto(URL);
+ await page.waitForTimeout(2000);
+
+ // Set zoom visible
+ await page.evaluate(() => localStorage.setItem('cv-zoom-visible', 'true'));
+ await page.reload();
+ await page.waitForTimeout(2000);
+
+ // Check that footer is NOT inside zoom-wrapper
+ const footerOutsideZoom = await page.evaluate(() => {
+ const footer = document.querySelector('.page-footer');
+ const zoomWrapper = document.querySelector('#zoom-wrapper');
+ if (!footer || !zoomWrapper) return false;
+ return !zoomWrapper.contains(footer);
+ });
+
+ // Check that action bar is NOT inside zoom-wrapper
+ const actionBarOutsideZoom = await page.evaluate(() => {
+ const actionBar = document.querySelector('.action-bar');
+ const zoomWrapper = document.querySelector('#zoom-wrapper');
+ if (!actionBar || !zoomWrapper) return false;
+ return !zoomWrapper.contains(actionBar);
+ });
+
+ // Check that hamburger menu is NOT inside zoom-wrapper
+ const menuOutsideZoom = await page.evaluate(() => {
+ const menu = document.querySelector('.navigation-menu');
+ const zoomWrapper = document.querySelector('#zoom-wrapper');
+ if (!menu || !zoomWrapper) return false;
+ return !zoomWrapper.contains(menu);
+ });
+
+ // Check that CV content IS inside zoom-wrapper
+ const cvInsideZoom = await page.evaluate(() => {
+ const cv = document.querySelector('.cv-container');
+ const zoomWrapper = document.querySelector('#zoom-wrapper');
+ if (!cv || !zoomWrapper) return false;
+ return zoomWrapper.contains(cv);
+ });
+
+ const test1Passed = footerOutsideZoom && actionBarOutsideZoom && menuOutsideZoom && cvInsideZoom;
+
+ console.log(` Footer outside zoom-wrapper: ${footerOutsideZoom ? 'โ
YES' : 'โ NO'}`);
+ console.log(` Action bar outside zoom-wrapper: ${actionBarOutsideZoom ? 'โ
YES' : 'โ NO'}`);
+ console.log(` Menu outside zoom-wrapper: ${menuOutsideZoom ? 'โ
YES' : 'โ NO'}`);
+ console.log(` CV content inside zoom-wrapper: ${cvInsideZoom ? 'โ
YES' : 'โ NO'}`);
+ console.log(` ${test1Passed ? 'โ
PASS' : 'โ FAIL'}`);
+
+ testResults.push({ test: 'DOM Structure - UI Outside Zoom', passed: test1Passed });
+
+ // ========================================================================
+ // TEST 2: Measure UI Elements at 100% Zoom
+ // ========================================================================
+ console.log("\n2๏ธโฃ Measuring UI elements at 100% zoom...");
+
+ const initialSizes = await page.evaluate(() => {
+ const footer = document.querySelector('.page-footer');
+ const actionBar = document.querySelector('.action-bar');
+ const cvPaper = document.querySelector('.cv-paper');
+
+ return {
+ footerHeight: footer ? footer.offsetHeight : 0,
+ actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
+ cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
+ };
+ });
+
+ console.log(` Footer height: ${initialSizes.footerHeight}px`);
+ console.log(` Action bar height: ${initialSizes.actionBarHeight}px`);
+ console.log(` CV width: ${initialSizes.cvWidth}px`);
+
+ // ========================================================================
+ // TEST 3: Set Zoom to 200% and Re-measure
+ // ========================================================================
+ console.log("\n3๏ธโฃ Setting zoom to 200% and re-measuring...");
+
+ const slider = await page.$('#zoom-slider');
+ if (slider) {
+ await slider.evaluate(el => {
+ el.value = '200';
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ });
+ await page.waitForTimeout(500);
+ }
+
+ const zoomedSizes = await page.evaluate(() => {
+ const footer = document.querySelector('.page-footer');
+ const actionBar = document.querySelector('.action-bar');
+ const cvPaper = document.querySelector('.cv-paper');
+
+ return {
+ footerHeight: footer ? footer.offsetHeight : 0,
+ actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
+ cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
+ };
+ });
+
+ console.log(` Footer height: ${zoomedSizes.footerHeight}px`);
+ console.log(` Action bar height: ${zoomedSizes.actionBarHeight}px`);
+ console.log(` CV width: ${zoomedSizes.cvWidth}px`);
+
+ // ========================================================================
+ // TEST 4: Verify UI Elements Unchanged
+ // ========================================================================
+ console.log("\n4๏ธโฃ Verifying UI elements unchanged at 200% zoom...");
+
+ const footerUnchanged = Math.abs(initialSizes.footerHeight - zoomedSizes.footerHeight) < 5;
+ const actionBarUnchanged = Math.abs(initialSizes.actionBarHeight - zoomedSizes.actionBarHeight) < 5;
+
+ // CV should be approximately doubled (with some tolerance)
+ const cvGrew = zoomedSizes.cvWidth > initialSizes.cvWidth * 1.8;
+
+ const test4Passed = footerUnchanged && actionBarUnchanged && cvGrew;
+
+ console.log(` Footer unchanged: ${footerUnchanged ? 'โ
YES' : 'โ NO'} (${initialSizes.footerHeight}px โ ${zoomedSizes.footerHeight}px)`);
+ console.log(` Action bar unchanged: ${actionBarUnchanged ? 'โ
YES' : 'โ NO'} (${initialSizes.actionBarHeight}px โ ${zoomedSizes.actionBarHeight}px)`);
+ console.log(` CV content grew: ${cvGrew ? 'โ
YES' : 'โ NO'} (${initialSizes.cvWidth}px โ ${zoomedSizes.cvWidth}px)`);
+ console.log(` ${test4Passed ? 'โ
PASS' : 'โ FAIL'}`);
+
+ testResults.push({ test: 'UI Elements Unchanged at 200%', passed: test4Passed });
+
+ // ========================================================================
+ // TEST 5: Reset and Verify
+ // ========================================================================
+ console.log("\n5๏ธโฃ Resetting zoom to 100%...");
+
+ const resetBtn = await page.$('#zoom-reset');
+ if (resetBtn) {
+ await resetBtn.click();
+ await page.waitForTimeout(500);
+ }
+
+ const resetSizes = await page.evaluate(() => {
+ const footer = document.querySelector('.page-footer');
+ const actionBar = document.querySelector('.action-bar');
+ const cvPaper = document.querySelector('.cv-paper');
+
+ return {
+ footerHeight: footer ? footer.offsetHeight : 0,
+ actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
+ cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
+ };
+ });
+
+ const resetPassed = Math.abs(resetSizes.cvWidth - initialSizes.cvWidth) < 10;
+
+ console.log(` CV width restored: ${resetPassed ? 'โ
YES' : 'โ NO'} (${resetSizes.cvWidth}px vs ${initialSizes.cvWidth}px initial)`);
+ console.log(` ${resetPassed ? 'โ
PASS' : 'โ FAIL'}`);
+
+ testResults.push({ test: 'Zoom Reset to 100%', passed: resetPassed });
+
+ // ========================================================================
+ // FINAL SUMMARY
+ // ========================================================================
+ console.log("\n" + "=".repeat(70));
+ console.log("๐ TEST SUMMARY\n");
+
+ const totalTests = testResults.length;
+ const passedTests = testResults.filter(r => r.passed).length;
+ const failedTests = totalTests - passedTests;
+
+ testResults.forEach(result => {
+ console.log(` ${result.passed ? 'โ
' : 'โ'} ${result.test}`);
+ });
+
+ console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
+
+ if (errors.length === 0) {
+ console.log("\nโ
NO CONSOLE ERRORS");
+ } else {
+ console.log(`\nโ ${errors.length} CONSOLE ERRORS FOUND:\n`);
+ errors.forEach((err, i) => {
+ console.log(`${i + 1}. ${err}`);
+ });
+ }
+
+ console.log("=".repeat(70) + "\n");
+
+ if (failedTests === 0 && errors.length === 0) {
+ console.log("๐ ALL TESTS PASSED! UI elements properly excluded from zoom.");
+ } else {
+ console.log("โ ๏ธ SOME TESTS FAILED - See details above");
+ }
+
+ console.log("\nBrowser will stay open for manual inspection.");
+ console.log("Press Ctrl+C when done.\n");
+
+ await new Promise(() => {}); // Keep browser open
+}
+
+await testZoomUIExclusion();