diff --git a/static/css/04-interactive/_buttons.css b/static/css/04-interactive/_buttons.css
index b3f6875..4c4528b 100644
--- a/static/css/04-interactive/_buttons.css
+++ b/static/css/04-interactive/_buttons.css
@@ -78,7 +78,7 @@
/* Print-Friendly Button (second from top) */
.print-friendly-btn {
position: fixed;
- bottom: 18rem; /* Below download button (22rem) */
+ bottom: 22rem; /* Below download button (26rem) */
left: 2rem;
width: 50px;
height: 50px;
@@ -127,7 +127,7 @@
/* Download Button (TOP POSITION) */
.download-btn {
position: fixed;
- bottom: 22rem; /* Top button position */
+ bottom: 26rem; /* Top button position */
left: 2rem;
width: 50px;
height: 50px;
@@ -172,13 +172,89 @@
background: #cd6060 !important; /* PDF red - matches hover */
}
-/* Mobile adjustments */
-@media (max-width: 768px) {
- .shortcuts-btn {
- bottom: 5.5rem; /* Above back-to-top button (1.5rem + 45px + gap) */
- left: 1.5rem; /* LEFT SIDE on mobile too */
- width: 45px;
- height: 45px;
- }
+/* CMD+K Command Bar Button (TOP position - first button) */
+.cmd-k-btn {
+ position: fixed;
+ bottom: 30rem; /* TOP position - above download button */
+ left: 2rem; /* Left side */
+ width: 50px;
+ height: 50px;
+ background: var(--black-bar, #2b2b2b);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ transition: all 0.3s ease;
+ z-index: 999;
+ opacity: 0.6;
+}
+
+.cmd-k-btn:hover {
+ opacity: 1;
+ transform: translateY(-3px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
+ background: #00897b; /* Teal - distinct from zoom button purple */
+}
+
+.cmd-k-btn.at-bottom {
+ opacity: 1;
+ background: #00897b; /* Teal - distinct from zoom button purple */
+}
+
+.cmd-k-btn:active {
+ transform: translateY(-1px);
+}
+
+/* Mobile adjustments - now handled by fluid sizing in _scroll-behavior.css
+ The 900px media query there uses clamp() for smooth scaling of all buttons and icons */
+
+
+/* =============================================================================
+ NINJA-KEYS COMMAND BAR STYLING
+ ============================================================================= */
+
+ninja-keys {
+ --ninja-font-family: 'Quicksand', -apple-system, BlinkMacSystemFont, sans-serif;
+ --ninja-accent-color: #667eea;
+ --ninja-z-index: 10000;
+ --ninja-width: 640px;
+ --ninja-backdrop-filter: blur(8px);
+
+ /* Light mode colors */
+ --ninja-modal-background: rgba(255, 255, 255, 0.95);
+ --ninja-modal-shadow: 0 16px 70px rgba(0, 0, 0, 0.2);
+ --ninja-text-color: #1a1a1a;
+ --ninja-secondary-text-color: #666;
+ --ninja-actions-background: #f5f5f5;
+ --ninja-selected-background: #667eea;
+ --ninja-selected-text-color: white;
+ --ninja-key-background: #e0e0e0;
+ --ninja-key-text-color: #333;
+ --ninja-footer-background: #f9f9f9;
+ --ninja-placeholder-color: #999;
+}
+
+/* Dark mode colors */
+[data-color-theme="dark"] ninja-keys {
+ --ninja-modal-background: rgba(40, 40, 40, 0.95);
+ --ninja-text-color: #e0e0e0;
+ --ninja-secondary-text-color: #999;
+ --ninja-actions-background: #2a2a2a;
+ --ninja-key-background: #444;
+ --ninja-key-text-color: #e0e0e0;
+ --ninja-footer-background: #2a2a2a;
+ --ninja-placeholder-color: #777;
+}
+
+/* Shortcut highlight in shortcuts modal */
+.shortcut-highlight {
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+ border-radius: 8px;
+ padding: 0.5rem;
+ margin: -0.5rem;
}
diff --git a/static/css/04-interactive/_scroll-behavior.css b/static/css/04-interactive/_scroll-behavior.css
index 03ea11a..74ca6a4 100644
--- a/static/css/04-interactive/_scroll-behavior.css
+++ b/static/css/04-interactive/_scroll-behavior.css
@@ -109,6 +109,20 @@
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
}
+/* Default icon sizes for floating buttons (desktop) */
+.cmd-k-btn iconify-icon,
+.download-btn iconify-icon,
+.print-friendly-btn iconify-icon,
+.fixed-btn.contact-btn iconify-icon,
+.shortcuts-btn iconify-icon,
+.info-button iconify-icon,
+.back-to-top iconify-icon,
+.color-theme-switcher iconify-icon {
+ width: 24px;
+ height: 24px;
+ font-size: 24px;
+}
+
/* Hide keyboard shortcuts button on real mobile devices (no keyboard) */
.is-mobile-device .shortcuts-btn,
.is-mobile-device .zoom-toggle-btn,
@@ -125,21 +139,56 @@
}
/* Reset fixed positioning for FLEXBOX buttons on mobile (exclude back-to-top) */
+ /* Use fluid sizing: at 900px = 50px, at 380px = 36px, scales linearly */
+ /* Formula: 2.7vw + 25.7px gives 36px at 380px and 50px at 900px */
+ .cmd-k-btn,
.download-btn,
.print-friendly-btn,
+ .fixed-btn.contact-btn,
.shortcuts-btn,
.info-button {
position: fixed !important;
bottom: 1.5rem !important;
left: auto !important;
right: auto !important;
- width: 50px !important;
- height: 50px !important;
+ /* Fluid button size: 36px at 380px, 50px at 900px */
+ width: clamp(36px, calc(2.7vw + 25.7px), 50px) !important;
+ height: clamp(36px, calc(2.7vw + 25.7px), 50px) !important;
/* Removed opacity: 1 !important to allow .footer-hovered to work */
transform: none !important;
}
+ /* Also apply fluid sizing to back-to-top */
+ .back-to-top {
+ width: clamp(36px, calc(2.7vw + 25.7px), 50px) !important;
+ height: clamp(36px, calc(2.7vw + 25.7px), 50px) !important;
+ }
+
+ /* Scale icons to fit buttons properly */
+ /* Icons ~50% of button size: 18px at 36px button, 24px at 50px button */
+ /* Formula: 1.15vw + 13.6px gives 18px at 380px and 24px at 900px */
+ .cmd-k-btn iconify-icon,
+ .download-btn iconify-icon,
+ .print-friendly-btn iconify-icon,
+ .fixed-btn.contact-btn iconify-icon,
+ .shortcuts-btn iconify-icon,
+ .info-button iconify-icon,
+ .back-to-top iconify-icon,
+ .color-theme-switcher iconify-icon {
+ width: clamp(18px, calc(1.15vw + 13.6px), 24px) !important;
+ height: clamp(18px, calc(1.15vw + 13.6px), 24px) !important;
+ font-size: clamp(18px, calc(1.15vw + 13.6px), 24px) !important;
+ /* Force override HTML width/height attributes */
+ min-width: 0 !important;
+ max-width: clamp(18px, calc(1.15vw + 13.6px), 24px) !important;
+ }
+
/* Mobile: Show colors at full opacity (no transparency with blur bar) */
+ .cmd-k-btn {
+ background: rgba(0, 137, 123, 1) !important; /* Teal - full opacity */
+ opacity: 1 !important; /* Override base opacity */
+ }
+
.download-btn {
background: rgba(205, 96, 96, 1) !important; /* PDF red - full opacity */
opacity: 1 !important; /* Override base opacity */
@@ -154,6 +203,11 @@
color: #27ae60 !important; /* Green icon */
}
+ .fixed-btn.contact-btn {
+ background: rgba(52, 152, 219, 1) !important; /* Blue - full opacity */
+ opacity: 1 !important; /* Override base opacity */
+ }
+
.shortcuts-btn {
background: rgba(243, 156, 18, 1) !important; /* Orange - full opacity */
opacity: 1 !important; /* Override base opacity */
@@ -170,63 +224,82 @@
}
/* Flexbox container behavior - buttons arrange themselves */
- /* Buttons will be positioned using JavaScript or individual positioning */
- /* For now, use fixed spacing from center */
+ /* Fluid positioning that scales with viewport */
+ /* At 900px: 8 * 50px + 7 * 8px = 456px total, start at -228px */
+ /* At 380px: 8 * 36px + 7 * 4px = 316px total, start at -158px */
- /* 6 buttons: Download, Print, Shortcuts, Theme, Info, Back-to-top */
- /* Spacing: 10px gap between buttons, centered horizontally */
- /* Total width: 6 * 50px + 5 * 10px = 350px */
- /* Start position: 50% - 175px */
+ .cmd-k-btn {
+ /* First button: -228px at 900px, -158px at 380px */
+ left: calc(50% - clamp(158px, calc(158px + (228 - 158) * ((100vw - 380px) / (900 - 380))), 228px)) !important;
+ }
.download-btn {
- left: calc(50% - 175px) !important; /* First button */
+ /* Second button: -170px at 900px, -118px at 380px */
+ left: calc(50% - clamp(118px, calc(118px + (170 - 118) * ((100vw - 380px) / (900 - 380))), 170px)) !important;
}
.print-friendly-btn {
- left: calc(50% - 115px) !important; /* Second button */
+ /* Third button: -112px at 900px, -78px at 380px */
+ left: calc(50% - clamp(78px, calc(78px + (112 - 78) * ((100vw - 380px) / (900 - 380))), 112px)) !important;
+ }
+
+ .fixed-btn.contact-btn {
+ /* Fourth button: -54px at 900px, -38px at 380px */
+ left: calc(50% - clamp(38px, calc(38px + (54 - 38) * ((100vw - 380px) / (900 - 380))), 54px)) !important;
}
.shortcuts-btn {
- left: calc(50% - 55px) !important; /* Third button */
+ /* Fifth button: +4px at 900px, +2px at 380px */
+ left: calc(50% + clamp(2px, calc(2px + (4 - 2) * ((100vw - 380px) / (900 - 380))), 4px)) !important;
}
- /* Theme switcher button - fourth position (defined in _themes.css) */
- /* left: calc(50% + 5px) !important; */
+ /* Theme switcher button - sixth position (defined in _themes.css) */
+ /* +62px at 900px, +42px at 380px */
.info-button {
- left: calc(50% + 65px) !important; /* Fifth button */
+ /* Seventh button: +120px at 900px, +82px at 380px */
+ left: calc(50% + clamp(82px, calc(82px + (120 - 82) * ((100vw - 380px) / (900 - 380))), 120px)) !important;
}
- /* Back-to-top button - now part of the button row (sixth button) */
+ /* Back-to-top button - now part of the button row (eighth button) */
.back-to-top {
position: fixed !important;
bottom: 1.5rem !important;
- left: calc(50% + 125px) !important; /* Sixth button (last) */
+ /* Eighth button: +178px at 900px, +122px at 380px */
+ left: calc(50% + clamp(122px, calc(122px + (178 - 122) * ((100vw - 380px) / (900 - 380))), 178px)) !important;
right: auto !important; /* Override previous right positioning */
- width: 50px !important;
- height: 50px !important;
- /* Removed fixed opacity - will be controlled by .at-bottom class */
+ /* Fluid size already set above, no need to repeat */
display: flex !important; /* Ensure it's always displayed */
}
- /* REAL MOBILE DEVICES: 5 buttons without shortcuts (no gap) */
- /* Total width: 5 * 50px + 4 * 10px = 290px, start at 50% - 145px */
+ /* REAL MOBILE DEVICES: 7 buttons without shortcuts */
+ /* At 900px: 7 * 50px + 6 * 8px = 398px, start at -199px */
+ /* At 380px: 7 * 36px + 6 * 4px = 276px, start at -138px */
+ .is-mobile-device .cmd-k-btn {
+ left: calc(50% - clamp(138px, calc(138px + (199 - 138) * ((100vw - 380px) / (900 - 380))), 199px)) !important;
+ }
+
.is-mobile-device .download-btn {
- left: calc(50% - 145px) !important; /* First button */
+ left: calc(50% - clamp(98px, calc(98px + (141 - 98) * ((100vw - 380px) / (900 - 380))), 141px)) !important;
}
.is-mobile-device .print-friendly-btn {
- left: calc(50% - 85px) !important; /* Second button */
+ left: calc(50% - clamp(58px, calc(58px + (83 - 58) * ((100vw - 380px) / (900 - 380))), 83px)) !important;
}
- /* Theme switcher on mobile - third position (see _themes.css for override) */
+ .is-mobile-device .fixed-btn.contact-btn {
+ left: calc(50% - clamp(18px, calc(18px + (25 - 18) * ((100vw - 380px) / (900 - 380))), 25px)) !important;
+ }
+
+ /* Theme switcher on mobile - fifth position (see _themes.css for override) */
+ /* +33px at 900px, +22px at 380px */
.is-mobile-device .info-button {
- left: calc(50% + 35px) !important; /* Fourth button */
+ left: calc(50% + clamp(62px, calc(62px + (91 - 62) * ((100vw - 380px) / (900 - 380))), 91px)) !important;
}
.is-mobile-device .back-to-top {
- left: calc(50% + 95px) !important; /* Fifth button (last) */
+ left: calc(50% + clamp(102px, calc(102px + (149 - 102) * ((100vw - 380px) / (900 - 380))), 149px)) !important;
}
/* Always show back-to-top on mobile (don't wait for scroll) */
@@ -235,6 +308,12 @@
}
/* Hover effects - Full color opacity on hover */
+ .cmd-k-btn:hover {
+ background: rgba(0, 137, 123, 1) !important; /* Full teal opacity */
+ transform: translateY(-3px) !important;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important;
+ }
+
.download-btn:hover,
.download-btn.pdf-hover-sync {
background: rgba(205, 96, 96, 1) !important; /* Full red opacity */
@@ -249,6 +328,12 @@
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important;
}
+ .fixed-btn.contact-btn:hover {
+ background: rgba(52, 152, 219, 1) !important; /* Full blue opacity */
+ transform: translateY(-3px) !important;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important;
+ }
+
.shortcuts-btn:hover {
background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */
transform: translateY(-3px) !important;
@@ -268,6 +353,12 @@
}
/* Keep at-bottom state - full opacity colors for each button */
+ .cmd-k-btn.at-bottom {
+ background: rgba(0, 137, 123, 1) !important; /* Full teal opacity */
+ opacity: 1 !important;
+ transform: none !important;
+ }
+
.download-btn.at-bottom {
background: rgba(205, 96, 96, 1) !important; /* Full red opacity */
opacity: 1 !important;
@@ -280,6 +371,12 @@
transform: none !important;
}
+ .fixed-btn.contact-btn.at-bottom {
+ background: rgba(52, 152, 219, 1) !important; /* Full blue opacity */
+ opacity: 1 !important;
+ transform: none !important;
+ }
+
.shortcuts-btn.at-bottom {
background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */
opacity: 1 !important;
@@ -299,8 +396,10 @@
}
/* Make all buttons semi-transparent when footer is hovered (applied via JS) */
+ .cmd-k-btn.footer-hovered,
.download-btn.footer-hovered,
.print-friendly-btn.footer-hovered,
+ .fixed-btn.contact-btn.footer-hovered,
.shortcuts-btn.footer-hovered,
.info-button.footer-hovered,
.back-to-top.footer-hovered,
@@ -310,13 +409,6 @@
}
}
-/* Very narrow mobile - Stack buttons vertically or adjust spacing if needed */
-@media (max-width: 483px) {
- /* For very narrow screens, you may need to adjust button spacing or size */
- /* For now, keep the same horizontal layout but buttons might overflow slightly */
- /* Users can scroll horizontally if needed, or we can reduce button sizes */
-}
-
/* ========================================
Mobile: Keep action bar visible (prevent hiding on scroll)
======================================== */
diff --git a/static/css/05-responsive/_breakpoints.css b/static/css/05-responsive/_breakpoints.css
index f412c23..dbe1641 100644
--- a/static/css/05-responsive/_breakpoints.css
+++ b/static/css/05-responsive/_breakpoints.css
@@ -1030,18 +1030,36 @@
visibility: visible !important;
}
- /* Adjust bottom button bar for landscape - smaller buttons */
+ /* Adjust bottom button bar for landscape - fluid sizing
+ Buttons: clamp(32px, calc(2.2vw + 19.6px), 40px) - scales from 32px at 380px to 40px at 915px
+ Icons: clamp(16px, calc(1.1vw + 9.8px), 20px) - scales from 16px at 380px to 20px at 915px */
+ .cmd-k-btn,
.download-btn,
.print-friendly-btn,
+ .fixed-btn.contact-btn,
.shortcuts-btn,
.info-button,
.back-to-top,
.color-theme-switcher {
- width: 40px !important;
- height: 40px !important;
+ width: clamp(32px, calc(2.2vw + 19.6px), 40px) !important;
+ height: clamp(32px, calc(2.2vw + 19.6px), 40px) !important;
bottom: 1rem !important;
}
+ /* Scale icons proportionally for landscape buttons */
+ .cmd-k-btn iconify-icon,
+ .download-btn iconify-icon,
+ .print-friendly-btn iconify-icon,
+ .fixed-btn.contact-btn iconify-icon,
+ .shortcuts-btn iconify-icon,
+ .info-button iconify-icon,
+ .back-to-top iconify-icon,
+ .color-theme-switcher iconify-icon {
+ width: clamp(16px, calc(1.1vw + 9.8px), 20px) !important;
+ height: clamp(16px, calc(1.1vw + 9.8px), 20px) !important;
+ font-size: clamp(16px, calc(1.1vw + 9.8px), 20px) !important;
+ }
+
/* Recalculate button positions for smaller 40px buttons */
/* 6 buttons: 6 * 40px + 5 * 10px = 290px total */
.download-btn {
diff --git a/templates/partials/color-theme-switcher.html b/templates/partials/color-theme-switcher.html
index f9eb7a6..d2e47f8 100644
--- a/templates/partials/color-theme-switcher.html
+++ b/templates/partials/color-theme-switcher.html
@@ -5,7 +5,7 @@
class="color-theme-switcher no-print has-tooltip"
aria-label="Toggle color theme"
data-tooltip="Toggle color theme">
-
+
diff --git a/templates/partials/widgets/back-to-top.html b/templates/partials/widgets/back-to-top.html
index 0903074..7bb3521 100644
--- a/templates/partials/widgets/back-to-top.html
+++ b/templates/partials/widgets/back-to-top.html
@@ -6,6 +6,6 @@
data-tooltip="{{.UI.Widgets.BackToTop.Tooltip}}"
style="display: none;"
_="on click call scrollToTop(event)">
-
+
{{end}}
diff --git a/templates/partials/widgets/cmd-k-button.html b/templates/partials/widgets/cmd-k-button.html
new file mode 100644
index 0000000..cfe9649
--- /dev/null
+++ b/templates/partials/widgets/cmd-k-button.html
@@ -0,0 +1,11 @@
+{{define "cmd-k-button"}}
+
+
+{{end}}
diff --git a/templates/partials/widgets/contact-button.html b/templates/partials/widgets/contact-button.html
index 7800178..edc6821 100644
--- a/templates/partials/widgets/contact-button.html
+++ b/templates/partials/widgets/contact-button.html
@@ -6,6 +6,6 @@
onclick="document.getElementById('contact-modal').showModal()"
aria-label="{{.UI.Widgets.Contact.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Contact.Tooltip}}">
-
+
{{end}}
diff --git a/templates/partials/widgets/download-button.html b/templates/partials/widgets/download-button.html
index 66d5b8b..7b94465 100644
--- a/templates/partials/widgets/download-button.html
+++ b/templates/partials/widgets/download-button.html
@@ -8,6 +8,6 @@
onclick="openPdfModal()"
_="on mouseenter call syncPdfHover(true)
on mouseleave call syncPdfHover(false)">
-
+
{{end}}
diff --git a/templates/partials/widgets/info-button.html b/templates/partials/widgets/info-button.html
index f60c032..faede72 100644
--- a/templates/partials/widgets/info-button.html
+++ b/templates/partials/widgets/info-button.html
@@ -4,6 +4,6 @@
aria-label="{{.UI.Widgets.Info.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Info.Tooltip}}"
onclick="document.getElementById('info-modal').showModal()">
-
+
{{end}}
diff --git a/templates/partials/widgets/print-friendly-button.html b/templates/partials/widgets/print-friendly-button.html
index ad2fcd1..9efe667 100644
--- a/templates/partials/widgets/print-friendly-button.html
+++ b/templates/partials/widgets/print-friendly-button.html
@@ -8,6 +8,6 @@
onclick="window.print()"
_="on mouseenter call syncPrintHover(true)
on mouseleave call syncPrintHover(false)">
-
+
{{end}}
diff --git a/templates/partials/widgets/shortcuts-button.html b/templates/partials/widgets/shortcuts-button.html
index 09af3cb..73b3fd8 100644
--- a/templates/partials/widgets/shortcuts-button.html
+++ b/templates/partials/widgets/shortcuts-button.html
@@ -6,6 +6,6 @@
onclick="document.getElementById('shortcuts-modal').showModal()"
aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}">
-
+
{{end}}
diff --git a/tests/mjs/74-button-icon-fluid-sizing.test.mjs b/tests/mjs/74-button-icon-fluid-sizing.test.mjs
new file mode 100644
index 0000000..86f2c19
--- /dev/null
+++ b/tests/mjs/74-button-icon-fluid-sizing.test.mjs
@@ -0,0 +1,215 @@
+#!/usr/bin/env node
+/**
+ * Test: Button and Icon Fluid Sizing
+ *
+ * Verifies that on mobile view (max-width: 900px):
+ * - Buttons scale fluidly from 36px (at 380px viewport) to 50px (at 900px viewport)
+ * - Icons scale proportionally from 18px to 24px
+ * - Icons properly override HTML width/height attributes
+ */
+
+import { chromium } from 'playwright';
+
+const TEST_URL = 'http://localhost:1999';
+// Use height > width to ensure portrait orientation (avoid landscape breakpoint)
+const VIEWPORT_HEIGHT = 1000;
+
+// Expected sizes at different viewport widths based on CSS formulas:
+// Buttons: clamp(36px, calc(2.7vw + 25.7px), 50px)
+// Icons: clamp(18px, calc(1.15vw + 13.6px), 24px)
+function expectedSizes(viewportWidth) {
+ const btnCalc = 0.027 * viewportWidth + 25.7;
+ const iconCalc = 0.0115 * viewportWidth + 13.6;
+ return {
+ button: Math.min(50, Math.max(36, btnCalc)),
+ icon: Math.min(24, Math.max(18, iconCalc))
+ };
+}
+
+async function testButtonIconFluidSizing() {
+ console.log('๐งช Testing Button and Icon Fluid Sizing');
+ console.log('='.repeat(70));
+
+ const browser = await chromium.launch({ headless: true });
+ const context = await browser.newContext({
+ viewport: { width: 900, height: VIEWPORT_HEIGHT },
+ deviceScaleFactor: 1,
+ });
+ const page = await context.newPage();
+
+ // Disable cache
+ await page.route('**/*', (route) => {
+ route.continue({
+ headers: {
+ ...route.request().headers(),
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ },
+ });
+ });
+
+ try {
+ await page.goto(TEST_URL, { waitUntil: 'networkidle' });
+ console.log(`โ
Navigated to ${TEST_URL}`);
+ await page.waitForTimeout(500);
+
+ const viewportWidths = [900, 700, 500, 380];
+ const buttons = [
+ { selector: '.cmd-k-btn', name: 'CmdK' },
+ { selector: '.download-btn', name: 'Download' },
+ { selector: '.print-friendly-btn', name: 'Print' },
+ { selector: '.fixed-btn.contact-btn', name: 'Contact' },
+ { selector: '.shortcuts-btn', name: 'Shortcuts' },
+ { selector: '.color-theme-switcher', name: 'Theme' },
+ { selector: '.info-button', name: 'Info' },
+ { selector: '.back-to-top', name: 'BackToTop' },
+ ];
+
+ let allTestsPassed = true;
+ const tolerance = 4; // Allow 4px tolerance for rounding
+
+ for (const width of viewportWidths) {
+ await page.setViewportSize({ width, height: VIEWPORT_HEIGHT });
+ await page.waitForTimeout(300);
+
+ const expected = expectedSizes(width);
+ console.log(`\n๐ Viewport: ${width}px (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`);
+ console.log('-'.repeat(70));
+
+ for (const btn of buttons) {
+ try {
+ const buttonElement = await page.$(btn.selector);
+ if (!buttonElement) {
+ // Some buttons may be hidden on certain viewports
+ continue;
+ }
+
+ const isVisible = await buttonElement.isVisible();
+ if (!isVisible) {
+ continue;
+ }
+
+ // Get button bounding box
+ const btnBox = await buttonElement.boundingBox();
+ if (!btnBox) continue;
+
+ const btnWidth = Math.round(btnBox.width);
+ const btnHeight = Math.round(btnBox.height);
+
+ // Get icon inside button
+ const iconElement = await buttonElement.$('iconify-icon');
+ let iconWidth = 0;
+ let iconHeight = 0;
+
+ if (iconElement) {
+ const iconBox = await iconElement.boundingBox();
+ if (iconBox) {
+ iconWidth = Math.round(iconBox.width);
+ iconHeight = Math.round(iconBox.height);
+ }
+ }
+
+ // Check if sizes are within tolerance of expected
+ const btnOk = Math.abs(btnWidth - expected.button) <= tolerance;
+ const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance;
+
+ const status = btnOk && iconOk ? 'โ
' : 'โ';
+
+ if (btnOk && iconOk) {
+ console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}x${iconHeight}`);
+ } else {
+ console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth}x${iconHeight} (expected ~${Math.round(expected.icon)})`);
+ allTestsPassed = false;
+ }
+
+ } catch (error) {
+ // Button may not exist
+ }
+ }
+ }
+
+ // Test landscape orientation (width > height)
+ console.log('\n\n๐ LANDSCAPE ORIENTATION TESTS');
+ console.log('='.repeat(70));
+
+ // Landscape viewports: width > height triggers landscape media query
+ // Note: At 400x400, width == height so CSS treats it as portrait, not landscape
+ const landscapeViewports = [
+ { width: 800, height: 400 }, // Clear landscape
+ { width: 600, height: 350 }, // Clear landscape
+ { width: 500, height: 300 }, // Clear landscape
+ ];
+
+ // Landscape formula: clamp(32px, calc(2.2vw + 19.6px), 40px) for buttons
+ // clamp(16px, calc(1.1vw + 9.8px), 20px) for icons
+ function expectedLandscapeSizes(viewportWidth) {
+ const btnCalc = 0.022 * viewportWidth + 19.6;
+ const iconCalc = 0.011 * viewportWidth + 9.8;
+ return {
+ button: Math.min(40, Math.max(32, btnCalc)),
+ icon: Math.min(20, Math.max(16, iconCalc))
+ };
+ }
+
+ for (const viewport of landscapeViewports) {
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
+ await page.waitForTimeout(300);
+
+ const expected = expectedLandscapeSizes(viewport.width);
+ console.log(`\n๐ Landscape ${viewport.width}x${viewport.height} (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`);
+ console.log('-'.repeat(70));
+
+ for (const btn of buttons) {
+ try {
+ const buttonElement = await page.$(btn.selector);
+ if (!buttonElement) continue;
+ const isVisible = await buttonElement.isVisible();
+ if (!isVisible) continue;
+
+ const btnBox = await buttonElement.boundingBox();
+ if (!btnBox) continue;
+
+ const btnWidth = Math.round(btnBox.width);
+ const btnHeight = Math.round(btnBox.height);
+
+ const iconElement = await buttonElement.$('iconify-icon');
+ let iconWidth = 0;
+ if (iconElement) {
+ const iconBox = await iconElement.boundingBox();
+ if (iconBox) iconWidth = Math.round(iconBox.width);
+ }
+
+ const btnOk = Math.abs(btnWidth - expected.button) <= tolerance;
+ const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance;
+ const status = btnOk && iconOk ? 'โ
' : 'โ';
+
+ if (btnOk && iconOk) {
+ console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}`);
+ } else {
+ console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth} (expected ~${Math.round(expected.icon)})`);
+ allTestsPassed = false;
+ }
+ } catch (error) {}
+ }
+ }
+
+ console.log('\n' + '='.repeat(70));
+
+ if (allTestsPassed) {
+ console.log('\nโ
ALL TESTS PASSED - Button and icon fluid sizing working correctly!');
+ console.log(' โข Portrait: Buttons 36-50px, Icons 18-24px');
+ console.log(' โข Landscape: Buttons 32-40px, Icons 16-20px');
+ } else {
+ console.log('\nโ SOME TESTS FAILED - Check output above');
+ }
+
+ await browser.close();
+ process.exit(allTestsPassed ? 0 : 1);
+
+ } catch (error) {
+ console.error('\nโ Test error:', error);
+ await browser.close();
+ process.exit(1);
+ }
+}
+
+testButtonIconFluidSizing();
diff --git a/tests/mjs/75-debug-button-icons.mjs b/tests/mjs/75-debug-button-icons.mjs
new file mode 100644
index 0000000..78513e2
--- /dev/null
+++ b/tests/mjs/75-debug-button-icons.mjs
@@ -0,0 +1,137 @@
+#!/usr/bin/env node
+/**
+ * Debug: Check actual computed styles for buttons and icons
+ */
+
+import { chromium } from 'playwright';
+
+const TEST_URL = 'http://localhost:1999';
+
+async function debugButtonIcons() {
+ console.log('๐ Debugging Button and Icon Actual Computed Styles');
+ console.log('='.repeat(70));
+
+ const browser = await chromium.launch({ headless: true });
+ const context = await browser.newContext({
+ viewport: { width: 375, height: 812 }, // iPhone X
+ deviceScaleFactor: 2,
+ });
+ const page = await context.newPage();
+
+ await page.goto(TEST_URL, { waitUntil: 'networkidle' });
+ await page.waitForTimeout(1000);
+
+ console.log('\n๐ฑ Viewport: 375x812 (iPhone X portrait)\n');
+
+ const buttons = [
+ '.cmd-k-btn',
+ '.download-btn',
+ '.print-friendly-btn',
+ '.fixed-btn.contact-btn',
+ '.shortcuts-btn',
+ '.color-theme-switcher',
+ '.info-button',
+ '.back-to-top',
+ ];
+
+ for (const selector of buttons) {
+ try {
+ const result = await page.evaluate((sel) => {
+ const btn = document.querySelector(sel);
+ if (!btn) return null;
+
+ const btnStyles = window.getComputedStyle(btn);
+ const icon = btn.querySelector('iconify-icon');
+
+ let iconInfo = null;
+ if (icon) {
+ const iconStyles = window.getComputedStyle(icon);
+ iconInfo = {
+ width: iconStyles.width,
+ height: iconStyles.height,
+ fontSize: iconStyles.fontSize,
+ // Check HTML attributes
+ htmlWidth: icon.getAttribute('width'),
+ htmlHeight: icon.getAttribute('height'),
+ // Check inline style
+ inlineStyle: icon.getAttribute('style'),
+ };
+ }
+
+ return {
+ selector: sel,
+ button: {
+ width: btnStyles.width,
+ height: btnStyles.height,
+ },
+ icon: iconInfo,
+ };
+ }, selector);
+
+ if (result) {
+ console.log(`${selector}:`);
+ console.log(` Button: ${result.button.width} x ${result.button.height}`);
+ if (result.icon) {
+ console.log(` Icon computed: ${result.icon.width} x ${result.icon.height} (font-size: ${result.icon.fontSize})`);
+ console.log(` Icon HTML attrs: width="${result.icon.htmlWidth}" height="${result.icon.htmlHeight}"`);
+ if (result.icon.inlineStyle) {
+ console.log(` Icon inline style: ${result.icon.inlineStyle}`);
+ }
+ }
+ console.log('');
+ }
+ } catch (e) {
+ console.log(`${selector}: Error - ${e.message}\n`);
+ }
+ }
+
+ // Also check what CSS rules are being applied
+ console.log('\n๐ Checking CSS rule specificity for iconify-icon...\n');
+
+ const cssInfo = await page.evaluate(() => {
+ const icon = document.querySelector('.download-btn iconify-icon');
+ if (!icon) return 'No icon found';
+
+ // Get all stylesheets and find rules for iconify-icon
+ const sheets = [...document.styleSheets];
+ const rules = [];
+
+ for (const sheet of sheets) {
+ try {
+ const cssRules = [...sheet.cssRules];
+ for (const rule of cssRules) {
+ if (rule.selectorText && rule.selectorText.includes('iconify-icon')) {
+ rules.push({
+ selector: rule.selectorText,
+ width: rule.style.width,
+ height: rule.style.height,
+ fontSize: rule.style.fontSize,
+ });
+ }
+ }
+ } catch (e) {
+ // CORS restrictions on external stylesheets
+ }
+ }
+
+ return rules;
+ });
+
+ console.log('CSS rules targeting iconify-icon:');
+ if (Array.isArray(cssInfo)) {
+ for (const rule of cssInfo) {
+ if (rule.width || rule.height || rule.fontSize) {
+ console.log(` ${rule.selector}`);
+ if (rule.width) console.log(` width: ${rule.width}`);
+ if (rule.height) console.log(` height: ${rule.height}`);
+ if (rule.fontSize) console.log(` font-size: ${rule.fontSize}`);
+ }
+ }
+ } else {
+ console.log(cssInfo);
+ }
+
+ await browser.close();
+}
+
+debugButtonIcons().catch(console.error);
diff --git a/tests/mjs/76-visual-verification.mjs b/tests/mjs/76-visual-verification.mjs
new file mode 100644
index 0000000..e8537cd
--- /dev/null
+++ b/tests/mjs/76-visual-verification.mjs
@@ -0,0 +1,56 @@
+import { chromium } from 'playwright';
+
+const browser = await chromium.launch();
+const context = await browser.newContext({
+ viewport: { width: 375, height: 812 }
+});
+const page = await context.newPage();
+
+await page.goto('http://localhost:1999');
+await page.waitForTimeout(1500);
+
+// Scroll to show back-to-top button
+await page.evaluate(() => window.scrollTo(0, 500));
+await page.waitForTimeout(500);
+
+// Take screenshot of bottom-left floating buttons area
+await page.screenshot({
+ path: 'tests/screenshots/button-icon-scaling-375px.png',
+ clip: { x: 0, y: 600, width: 200, height: 200 }
+});
+
+// Take full page for context
+await page.screenshot({
+ path: 'tests/screenshots/full-page-375px.png',
+ fullPage: false
+});
+
+console.log('๐ธ Screenshots saved to tests/screenshots/');
+
+// Print actual sizes for verification
+const sizes = await page.evaluate(() => {
+ const results = [];
+ const selectors = ['.cmd-k-btn', '.download-btn', '.print-friendly-btn',
+ '.fixed-btn.contact-btn', '.shortcuts-btn',
+ '.color-theme-switcher', '.info-button', '.back-to-top'];
+
+ for (const sel of selectors) {
+ const btn = document.querySelector(sel);
+ if (btn) {
+ const btnStyle = getComputedStyle(btn);
+ const icon = btn.querySelector('iconify-icon');
+ const iconStyle = icon ? getComputedStyle(icon) : null;
+ results.push({
+ selector: sel,
+ button: Math.round(Number.parseFloat(btnStyle.width)) + 'x' + Math.round(Number.parseFloat(btnStyle.height)),
+ icon: iconStyle ? Math.round(Number.parseFloat(iconStyle.width)) + 'x' + Math.round(Number.parseFloat(iconStyle.height)) : 'N/A'
+ });
+ }
+ }
+ return results;
+});
+
+console.log('\n๐ Sizes at 375px viewport:');
+sizes.forEach(s => console.log(' ' + s.selector + ': button=' + s.button + ', icon=' + s.icon));
+
+await browser.close();