Update Playwright tests to use new 'icons' terminology:
- Update class name checks: show-logos → show-icons
- Update localStorage keys: cv-logos → cv-icons
- Update UI text references: Logos → Icons
- Update test output messages
Files modified:
- tests/test-all-features.js - Icon class and persistence checks
- tests/test-all-toggles.js - Icon toggle selector
- tests/test-toggle-complete.js - Icon localStorage key
No functional changes, only terminology updates to match
current codebase naming conventions.
PROBLEM:
- htmx:swapError with "Cannot read properties of null (reading 'insertBefore')" on double-click
- Toggle animations were "digital" (instant snap) instead of "analogical" (smooth slide)
- Conflict between server templates with hx-swap-oob and client-side hyperscript
ROOT CAUSE:
- Server templates returned HTML with hx-swap="outerHTML" + hx-swap-oob="true"
- This destroyed and recreated DOM elements during swap
- Second click tried to insert into null parent (element was destroyed)
- CSS transitions broke because element was destroyed mid-animation
SOLUTION:
- Remove all HTML from toggle templates (length-toggle.html, logo-toggle.html, theme-toggle.html)
- Templates now return empty comment: "<!-- Template not used - toggles use hx-swap="none" with inline hyperscript -->"
- Toggles use hx-swap="none" to prevent any DOM replacement
- All visual updates handled client-side via inline hyperscript
- Server only saves cookies in background (no HTML returned)
BENEFITS:
- ✅ No more null reference errors (no DOM destruction)
- ✅ Smooth CSS transitions work perfectly (element preserved)
- ✅ Desktop/mobile toggles sync via direct ID manipulation
- ✅ Zero HTMX swap conflicts
- ✅ Clean separation: client handles visuals, server persists state
DOCUMENTATION:
- Updated MODERN-WEB-TECHNIQUES.md with Phase 8
- Documented the complete debug journey and solution
- Added architecture pattern for client-first toggles
- Moved menu hover logic from JavaScript to CSS selectors, reducing JS to minimal bridge code
- Replaced JavaScript-based toast timing with pure CSS animation lifecycle (slide in → stay → fade out)
- Removed unnecessary event handlers and legacy compatibility code for cleaner implementation
- Removed over-engineered cache system for static CV data that only changes on deployment
- Extracted all route configuration to internal/routes/routes.go for better organization
- Implemented rate limiting and cache control middleware for PDF endpoint protection
- Replace CSS zoom with transform: scale() for proper viewport extension
- Add dynamic margin-bottom to position footer correctly
- Remove zoom: 1 reset from fixed buttons (no longer needed)
- Enables true zoom from 10% to 500% that extends beyond viewport
- Add green background (#27ae60) to info and back-to-top buttons when at page bottom
- Implement bottom detection (within 50px threshold) in scroll handler
- Add conditional green hover to zoom reset button (only when zoom ≠ 100%)
- Enhance UX with visual feedback for scroll position and zoom state
Switched from transform: scale() to CSS zoom property on zoom-wrapper.
CSS zoom changes actual layout space, not just visual rendering:
- At 50% zoom, wrapper takes 50% space (no reserved empty space)
- Footer naturally follows right after zoomed content
- At 200% zoom, content extends beyond viewport with scrolling
- Fixes the large gray gap between content and footer
- Created zoom-wrapper div around cv-container
- Zoom now applies to wrapper only, footer adjusts naturally below
- Footer no longer scaled, stays at normal size
- Fixes gap between content and footer at low zoom levels
- Reduced back-to-top button size (35px default, grows to 50px on hover)
- Cleaner separation of concerns for zoom functionality
- Moved "Zoom" button from action bar to hamburger menu under "Acciones Rápidas"
- Close button (X) now grey/subtle by default (opacity: 0.7)
- Close button turns red only on hover for clear indication
- Updated JavaScript to reference show-zoom-menu-btn instead of show-zoom-btn
- Added preventDefault to showZoomControl to prevent link navigation
CSS zoom property had constraints preventing proper zoom beyond 100%.
Switched back to transform: scale() which provides true visual zoom
that can extend beyond viewport bounds at 100-200% range.
- Add close button (X) to zoom control widget
- Make zoom control draggable anywhere on screen
- Persist dragged position in localStorage (cv-zoom-position)
- Add "Zoom" button to action bar to show control when hidden
- Persist visibility state in localStorage (cv-zoom-visible)
- Cursor changes to "move" to indicate draggability
- Interactive elements (slider, buttons) don't trigger drag
- Position stays within viewport bounds
- All features work on desktop only (zoom hidden on mobile)
- Hide zoom control on mobile viewports (≤768px) via CSS
- Skip loading saved zoom on mobile, always use 100%
- Add resize handler to reset zoom when switching to mobile view
- Clean up unnecessary mobile-specific zoom styles
- Prevents confusion on touch devices where zoom is less useful
Changed from transform: scale() to CSS zoom property which affects actual
document layout space. At 50% zoom, content now takes 50% space instead of
reserving full 100% height. This eliminates the large gray empty space issue.
Removed proportional scroll adjustment that was causing page to jump down
when zooming. The transform-origin: top center property keeps content
anchored at top naturally without needing scroll compensation.
Changed zoom target from .cv-paper to .cv-container and .cv-footer to fix issue
where 50% zoom still reserved 100% document space creating empty areas. Both
elements now scale together, properly reducing space at low zoom levels while
keeping main action bar unaffected.
- Modified applyZoom() to target .cv-container and .cv-footer
- Added transform-origin, transition, and will-change to both CSS selectors
- Maintains proportional scroll compensation for smooth visual experience
The dynamic height adjustment was setting a fixed height on .cv-paper,
but the footer is a sibling element OUTSIDE .cv-paper in the DOM.
This caused the footer to be positioned incorrectly and hidden.
Removed:
- dataset.originalHeight storage
- height calculation and setting
- All dynamic height adjustment logic
The CSS transform: scale() will handle the visual zoom without
affecting the natural document flow, allowing the footer to
appear properly after the content.
Result: Footer should now be visible at all zoom levels
Increased .cv-main bottom padding from 1rem to 8rem to ensure:
- Footer is fully visible and not hidden behind content
- Adequate clearance for zoom control (positioned at bottom: 100px)
- Proper breathing room between content and footer
Result: Footer now properly visible in normal view
The margin-top: -60px on .cv-footer was pulling the footer up too much,
hiding the GitHub link and other footer content.
Removed the negative margin - the reduced .cv-main bottom padding
(1rem instead of 3rem) already provides better spacing without hiding content.
Result:
- Footer content (GitHub, LinkedIn links) now visible
- Still has better spacing than before
- Zoom control at 100px bottom clears footer properly
Fixed two issues with zoom and footer:
1. Dynamic height adjustment based on zoom level:
- Store original height on first zoom (dataset.originalHeight)
- Calculate scaled height: originalHeight × scale factor
- Set container height dynamically to match visual content
- Example: 50% zoom → height = originalHeight × 0.5
- Prevents empty space below content at low zoom
- Prevents overflow at high zoom
2. Footer spacing and positioning:
- Reduced .cv-main bottom padding: 3rem → 1rem
- Added .cv-footer margin-top: -60px to pull footer up
- Increased .zoom-control bottom: 70px → 100px
- Zoom control now clears footer GitHub link
Result:
- Footer appears immediately after content at any zoom level
- No wasted empty space below content
- Zoom control doesn't overlap GitHub link
- Page height dynamically adapts to zoom level
When zooming, the page was moving up/down because the scroll position
wasn't being adjusted to account for the scale change.
Solution: Proportional scroll compensation
- Track old scale before applying new scale
- Calculate scale ratio (newScale / oldScale)
- Adjust scroll position: newScrollTop = oldScrollTop * scaleRatio
Example:
- At 100% zoom, scrolled to pixel 1000
- Zoom to 50%: content at pixel 1000 is now at pixel 500
- Adjust scroll to 500 to keep same content visible
Result:
- Page stays visually in the same place when zooming
- Content you're viewing remains stable
- No jumping or movement during zoom transitions
- Works at all zoom levels (50%-200%)
The viewport-centered zoom approach was creating empty space above
the content when zooming to 50%, causing the CV to move down.
Solution: Simple top-anchored zoom
- Removed all complex scroll compensation logic
- Set transform-origin to "top center" (fixed position)
- Page now scales naturally from the top without movement
- No dynamic transform-origin calculations
- No scroll position adjustments
Result:
- Page stays anchored at top during zoom
- No empty space created above content
- Clean, predictable zoom behavior
- Works correctly at all zoom levels (50%-200%)
The page simply scales up/down from the top center point,
maintaining its position without any jumping or space issues.
Changed zoom behavior from "page-relative" to "viewport-relative":
Before:
- Zoomed from top of page causing perspective/depth effect
- Content appeared to move "through" the viewer
- Disorienting experience when scrolled down
After:
- Zooms from center of YOUR viewport (where you're looking)
- Content expands both above and below your position
- You stay at your original viewing point - no movement
- Like pinch-to-zoom: magnifies what you're currently viewing
Technical implementation:
1. Calculate viewport center relative to page (scrollTop + viewportHeight/2)
2. Convert to percentage of page height
3. Set transform-origin dynamically to that percentage
4. Apply scale transform from your viewing position
5. Adjust scroll to keep same content at viewport center
CSS changes:
- Default transform-origin: center center (was top center)
- Added transition for transform-origin: 0s (instant, no animation)
- Maintains smooth 0.08s linear transform transition
Result: Natural, stable zoom that feels like magnifying glass
When zooming, the page was scaling from a fixed point causing content
to appear to "move through" the viewport like a 3D perspective effect.
This made the zoom feel disorienting as the visible content would shift.
Solution: Proportional scroll compensation
- Track current scale before applying new scale
- Calculate scale ratio (newScale / currentScale)
- Adjust scroll position by multiplying by scale ratio
- Example: scrollTop 1000px at 100% → 1500px at 150%
Benefits:
- Content you're viewing stays in the same visual position
- Zoom feels more like "magnification" than "perspective"
- Smooth, stable zoom experience without content shifting
- Eliminates the "going through me and getting farther" effect
Technical changes:
- Extract current scale from transform property via regex
- Calculate proportional scroll adjustment
- Apply synchronized transform + scroll in requestAnimationFrame
Previously, HTML in short descriptions was being escaped and displayed
as raw text instead of rendering properly. This happened because the
safeHTML template function had been removed for security reasons.
Changes:
- Added safeHTML function back to template.FuncMap (template.go:53-55)
- Updated three template locations to use safeHTML pipe:
* Experience descriptions (cv-content.html:122)
* Award descriptions (cv-content.html:180)
* Project descriptions (cv-content.html:232)
Security note:
The safeHTML function is safe to use here because CV data comes from
trusted YAML files controlled by the site owner, not user input.
Clear documentation added to prevent misuse with untrusted content.
Examples now rendering correctly:
- Award: "Premio por excelencia en marketing B2B...con <a href=...>Clicplan</a>"
- Projects: Links to Lidering, Jorpack, Delivery Bikes BCN, Mobbeel