diff --git a/PDF-SHORTCUT-IMPLEMENTATION.md b/PDF-SHORTCUT-IMPLEMENTATION.md deleted file mode 100644 index 872a644..0000000 --- a/PDF-SHORTCUT-IMPLEMENTATION.md +++ /dev/null @@ -1,178 +0,0 @@ -# PDF Shortcut URL Implementation Summary - -**Date:** 2025-11-20 -**Status:** ✅ Complete and Tested - -## Features Implemented - -### 1. Year-Aware Shortcut URLs ✅ - -**URLs:** -- `/cv-jamr-2025-en.pdf` → English default CV (short with skills, 5 pages) -- `/cv-jamr-2025-es.pdf` → Spanish default CV (short with skills, 5 pages) - -**Year Validation:** -- ✅ Current year (2025): Works perfectly -- ❌ Old year (2024): Returns 404 -- ❌ Future year (2026): Returns 404 -- 🔄 Auto-updates: Next year, 2026 URLs will work automatically - -**Implementation:** -- Handler: `internal/handlers/cv.go` - `DefaultCVShortcut()` function -- Route check: `Home()` handler detects pattern and delegates -- Redirect: 301 to `/export/pdf?lang={lang}&length=short&icons=show&version=with_skills` - -### 2. PDF Modal Updates ✅ - -**Changes:** -- ❌ Removed: "Current View" option -- ✅ Added: "Default CV (Recommended)" option -- ✅ Highlights: Purple gradient badge, star emoji ⭐, bold text -- ✅ Description: "Short with skills - Recommended" (5 pages) - -**JavaScript:** -- Uses shortcut URL: `/cv-jamr-${year}-${lang}.pdf` -- Auto-detects current year -- Cleaner, more memorable URL - -**Current Order:** -1. Short CV (4 pages) - Clean, no skills -2. Long CV (9 pages) - Extended with skills -3. **⭐ Default CV (5 pages)** - Short with skills, Highlighted - -**Requested Order (optional polish):** -1. Short CV (4 pages) -2. **⭐ Default CV (5 pages)** ← Move to middle -3. Long CV (9 pages) - -*Note: Functional order works fine, visual reordering is optional UX polish.* - -## Testing Results - -### Shortcut URLs -```bash -# English - Works ✅ -curl -I http://localhost:1999/cv-jamr-2025-en.pdf -→ HTTP/1.1 301 Moved Permanently -→ Location: /export/pdf?lang=en&length=short&icons=show&version=with_skills - -# Spanish - Works ✅ -curl -I http://localhost:1999/cv-jamr-2025-es.pdf -→ HTTP/1.1 301 Moved Permanently -→ Location: /export/pdf?lang=es&length=short&icons=show&version=with_skills - -# Invalid year 2024 - Rejected ✅ -curl -I http://localhost:1999/cv-jamr-2024-en.pdf -→ HTTP/1.1 404 Not Found - -# Invalid year 2026 - Rejected ✅ -curl -I http://localhost:1999/cv-jamr-2026-en.pdf -→ HTTP/1.1 404 Not Found -``` - -### Modal Functionality -- ✅ Default option appears with highlighting -- ✅ Star emoji ⭐ visible in badge and title -- ✅ Purple gradient badge stands out -- ✅ Downloads correct PDF (short with skills, 5 pages) -- ✅ Uses memorable shortcut URL - -## Files Changed - -**Backend:** -- `internal/handlers/cv.go` (+42 lines) - - Added `DefaultCVShortcut()` handler - - Added pattern detection in `Home()` handler -- `internal/routes/routes.go` (+3 lines) - - Registered shortcut route (moved before "/" for precedence) - -**Frontend:** -- `templates/partials/modals/pdf-modal.html` (~30 changes) - - Replaced "Current View" card with "Default CV" card - - Added highlighting: gradient badge, star emoji, bold text - - Updated JavaScript to use shortcut URL - - Added `data-cv-format="default"` attribute - -## Code Snippets - -### Backend Handler -```go -// DefaultCVShortcut handles shortcut URLs for default CV downloads -// Pattern: /cv-jamr-{year}-{lang}.pdf (e.g., /cv-jamr-2025-en.pdf) -func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - parts := strings.Split(strings.TrimPrefix(path, "/"), "-") - - // Extract and validate year - yearStr := parts[2] - currentYear := fmt.Sprintf("%d", time.Now().Year()) - if yearStr != currentYear { - http.NotFound(w, r) - return - } - - // Extract and validate language - lang := strings.TrimSuffix(parts[3], ".pdf") - if lang != "en" && lang != "es" { - http.NotFound(w, r) - return - } - - // Redirect to default PDF export - redirectURL := fmt.Sprintf("/export/pdf?lang=%s&length=short&icons=show&version=with_skills", lang) - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) -} -``` - -### Frontend JavaScript -```javascript -if (selectedFormat === 'default') { - // Default CV: use shortcut URL (short with skills, 5 pages) - const currentYear = new Date().getFullYear(); - url = `/cv-jamr-${currentYear}-${lang}.pdf`; -} -``` - -## Benefits - -### For Users -- ✅ **Memorable URL:** `juan.andres.morenorub.io/cv-jamr-2025-en.pdf` -- ✅ **Easy to share:** No complex query parameters -- ✅ **Clear recommendation:** Default option highlighted in modal -- ✅ **Always current:** Year auto-updates - -### For Developer -- ✅ **Simple maintenance:** Year validation automatic -- ✅ **Clean architecture:** Single handler, minimal code -- ✅ **Future-proof:** Works for any future year -- ✅ **SEO friendly:** Clean, semantic URLs - -## Next Steps (Optional) - -### Visual Polish -- [ ] Reorder cards to: Short → Default → Long (currently Short → Long → Default) -- [ ] Add CSS class for `.pdf-option-recommended` styling (currently inline) -- [ ] Add hover effect emphasis for default card - -### Enhancement Ideas -- [ ] Add QR code in modal for default CV shortcut URL -- [ ] Show "Most Downloaded" badge based on analytics -- [ ] Add animation/pulse effect to recommended badge -- [ ] Create even shorter alias: `/cv.pdf` → defaults to current year + Spanish - -## Production URLs - -Once deployed, these URLs will work: -``` -https://juan.andres.morenorub.io/cv-jamr-2025-en.pdf -https://juan.andres.morenorub.io/cv-jamr-2025-es.pdf -``` - -## Conclusion - -✅ **Core functionality complete and tested** -✅ **Year validation working perfectly** -✅ **Modal updated with highlighted default option** -✅ **Shortcut URLs functional and user-friendly** - -The implementation provides a clean, memorable way to access the default CV with automatic year updates. The modal now clearly indicates the recommended option with visual emphasis. diff --git a/PDF-VALIDATION-REPORT.md b/PDF-VALIDATION-REPORT.md deleted file mode 100644 index a8d7dc0..0000000 --- a/PDF-VALIDATION-REPORT.md +++ /dev/null @@ -1,191 +0,0 @@ -# PDF Generation Validation Report - -**Generated:** 2025-11-20 -**Total PDFs:** 8 -**Status:** ✅ All PDFs generated and validated successfully - -## Summary Table - -| PDF File | Pages | Lang | Length | Version | Sidebar Fonts | Status | -|----------|-------|------|--------|---------|---------------|--------| -| cv-short-jamr-2025-en.pdf | 4 | EN | short | clean | N/A (no sidebar) | ✅ PASS | -| cv-short-jamr-2025-es.pdf | 4 | ES | short | clean | N/A (no sidebar) | ✅ PASS | -| cv-short-with-skills-jamr-2025-en.pdf | 5 | EN | short | with_skills | **Compact** (0.94-0.98em) | ✅ PASS | -| cv-short-with-skills-jamr-2025-es.pdf | 5 | ES | short | with_skills | **Compact** (0.94-0.98em) | ✅ PASS | -| cv-long-jamr-2025-en.pdf | 7 | EN | long | clean | N/A (no sidebar) | ✅ PASS | -| cv-long-jamr-2025-es.pdf | 7 | ES | long | clean | N/A (no sidebar) | ✅ PASS | -| cv-long-with-skills-jamr-2025-en.pdf | 9 | EN | long | with_skills | **Full-size** (1.0em) | ✅ PASS | -| cv-long-with-skills-jamr-2025-es.pdf | 9 | ES | long | with_skills | **Full-size** (1.0em) | ✅ PASS | - -## Detailed Validation - -### 1. SHORT + CLEAN (4 pages, no sidebars) - -**cv-short-jamr-2025-en.pdf** -- ✅ Pages: 4 -- ✅ Language: English ("20 years of experience", "Training") -- ✅ Version: Clean (no skills sidebar) -- ✅ File size: 2.2 MB - -**cv-short-jamr-2025-es.pdf** -- ✅ Pages: 4 -- ✅ Language: Spanish ("20 años de experiencia") -- ✅ Version: Clean (no skills sidebar) -- ✅ File size: 2.2 MB - -### 2. SHORT + WITH_SKILLS (5 pages, COMPACT sidebar fonts) - -**cv-short-with-skills-jamr-2025-en.pdf** -- ✅ Pages: 5 (reduced from 6 with compact fonts) -- ✅ Language: English -- ✅ Version: With skills sidebar -- ✅ Compact fonts: Active (0.94-0.98em font reduction) -- ✅ Page count reduction: 16.7% (6→5 pages) -- ✅ File size: 2.2 MB - -**cv-short-with-skills-jamr-2025-es.pdf** -- ✅ Pages: 5 (reduced from 6 with compact fonts) -- ✅ Language: Spanish ("Competencias" sidebar detected) -- ✅ Version: With skills sidebar -- ✅ Compact fonts: Active (0.94-0.98em font reduction) -- ✅ Page count reduction: 16.7% (6→5 pages) -- ✅ File size: 2.2 MB - -### 3. LONG + CLEAN (7 pages, no sidebars) - -**cv-long-jamr-2025-en.pdf** -- ✅ Pages: 7 -- ✅ Language: English -- ✅ Version: Clean (no skills sidebar) -- ✅ Content: Extended content compared to short version -- ✅ File size: 2.2 MB - -**cv-long-jamr-2025-es.pdf** -- ✅ Pages: 7 -- ✅ Language: Spanish -- ✅ Version: Clean (no skills sidebar) -- ✅ Content: Extended content compared to short version -- ✅ File size: 2.2 MB - -### 4. LONG + WITH_SKILLS (9 pages, FULL-SIZE sidebar fonts) - -**cv-long-with-skills-jamr-2025-en.pdf** -- ✅ Pages: 9 -- ✅ Language: English -- ✅ Version: With skills sidebar -- ✅ Sidebar fonts: Full-size (1.0em, NO font reduction) -- ✅ Sidebar layout: 25% left/right sidebars -- ✅ File size: 2.3 MB - -**cv-long-with-skills-jamr-2025-es.pdf** -- ✅ Pages: 9 -- ✅ Language: Spanish -- ✅ Version: With skills sidebar -- ✅ Sidebar fonts: Full-size (1.0em, NO font reduction) -- ✅ Sidebar layout: 25% left/right sidebars -- ✅ File size: 2.3 MB - -## Feature Validation - -### ✅ Compact Sidebar Fonts Feature - -**Activation conditions:** -- Length: `short` ✅ -- Version: `with_skills` ✅ - -**Implementation verified:** -- Cookie detection: `cv-length=short` triggers compact fonts -- Font reduction: 2-6% (0.94-0.98em) -- Page count impact: 6→5 pages (16.7% reduction) -- Only applies to SHORT versions ✅ - -**Long versions confirmed:** -- Do NOT use compact fonts ✅ -- Full-size sidebar fonts (1.0em) ✅ -- 9 pages maintained ✅ - -### ✅ Language Support - -**English (en):** -- Header: "20 years of experience" ✅ -- Sections: "Training", "Experience" ✅ -- Skills sidebar: "Technical Skills" ✅ - -**Spanish (es):** -- Header: "20 años de experiencia" ✅ -- Sections: "Formación", "Experiencia" ✅ -- Skills sidebar: "Competencias Técnicas" ✅ - -### ✅ Breaking Change Validation - -**'extended' → 'long' terminology:** -- All PDFs use 'long' in filenames ✅ -- API accepts `length=long` ✅ -- API rejects `length=extended` (400 error) ✅ -- Migration logic: auto-converts old cookies ✅ - -## Page Count Expectations - -| Configuration | Expected Pages | Actual | Status | -|---------------|----------------|--------|--------| -| Short + Clean | 4 | 4 | ✅ | -| Short + With Skills (compact) | 5 | 5 | ✅ | -| Long + Clean | 7 | 7 | ✅ | -| Long + With Skills (full-size) | 9 | 9 | ✅ | - -## Characteristics Verified - -### ✅ SHORT versions: -1. Concise content (4 pages clean, 5 pages with skills) -2. Compact sidebar fonts reduce page count -3. Both languages working correctly - -### ✅ LONG versions: -1. Extended content (7 pages clean, 9 pages with skills) -2. Full-size sidebar fonts (no reduction) -3. 25% sidebar layout preserved -4. Both languages working correctly - -### ✅ CLEAN versions: -1. No skills sidebars displayed -2. Compact, professional layout -3. Print-optimized CSS active - -### ✅ WITH_SKILLS versions: -1. Skills sidebar visible -2. Accordion headers hidden -3. Page breaks between sections -4. Font size conditional on length parameter - -## File Size Analysis - -| Type | Size Range | Notes | -|------|------------|-------| -| Short Clean | 2.2 MB | Standard size | -| Short With Skills | 2.2 MB | Same as clean (compact fonts) | -| Long Clean | 2.2 MB | Consistent with short | -| Long With Skills | 2.3 MB | Slightly larger (more content + sidebars) | - -**Observation:** File sizes are consistent across configurations, indicating proper PDF optimization. - -## Test Environment - -- **Server:** cv-server running on localhost:1999 -- **Generation method:** HTTP API calls via curl -- **Rate limiting:** Handled with 5-10 second delays between requests -- **Validation tools:** pdfinfo, pdftotext -- **Build:** Latest (post breaking-change commit) - -## Conclusion - -**✅ ALL VALIDATIONS PASSED** - -All 8 PDFs generated successfully with correct: -- Page counts matching expectations -- Language-specific content -- Compact sidebar fonts feature working correctly (short with_skills only) -- Full-size sidebar fonts for long versions -- Breaking change ('extended' → 'long') implemented correctly -- File sizes within expected ranges - -**No issues found. Ready for production use.** diff --git a/PDF-EXPORT-FEATURE.md b/doc/11-PDF-EXPORT.md similarity index 85% rename from PDF-EXPORT-FEATURE.md rename to doc/11-PDF-EXPORT.md index 02e590e..0f6f4d0 100644 --- a/PDF-EXPORT-FEATURE.md +++ b/doc/11-PDF-EXPORT.md @@ -440,6 +440,105 @@ curl -O http://localhost:1999/export/pdf?lang=en&length=long&icons=show&version= # Filename: cv-long-with-skills-jamr-2025-en.pdf ``` +## Shortcut URLs (Year-Aware) + +### Overview + +The application provides memorable shortcut URLs for the default CV (short with skills, 5 pages) that are easy to share and remember. + +**Pattern:** `/cv-jamr-{year}-{lang}.pdf` + +**Examples:** +- `https://juan.andres.morenorub.io/cv-jamr-2025-en.pdf` (English) +- `https://juan.andres.morenorub.io/cv-jamr-2025-es.pdf` (Spanish) + +### Year Validation + +The shortcut URLs include automatic year validation that auto-updates annually: + +**Current Year (2025):** +- ✅ `/cv-jamr-2025-en.pdf` → Works (301 redirect) +- ✅ `/cv-jamr-2025-es.pdf` → Works (301 redirect) + +**Past/Future Years:** +- ❌ `/cv-jamr-2024-en.pdf` → 404 Not Found +- ❌ `/cv-jamr-2026-en.pdf` → 404 Not Found (until 2026) + +**Auto-Update:** Next year (2026), the 2026 URLs will automatically work and 2025 URLs will return 404. + +### Implementation + +**Backend Handler** (`internal/handlers/cv.go`): +```go +func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) { + // Extract year and language from URL path + path := r.URL.Path // e.g., "/cv-jamr-2025-en.pdf" + parts := strings.Split(strings.TrimPrefix(path, "/"), "-") + + yearStr := parts[2] + lang := strings.TrimSuffix(parts[3], ".pdf") + + // Validate year matches current year + currentYear := fmt.Sprintf("%d", time.Now().Year()) + if yearStr != currentYear { + http.NotFound(w, r) + return + } + + // Validate language + if lang != "en" && lang != "es" { + http.NotFound(w, r) + return + } + + // Redirect to default PDF (short with skills) + redirectURL := fmt.Sprintf("/export/pdf?lang=%s&length=short&icons=show&version=with_skills", lang) + http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) +} +``` + +**Route Registration** (`internal/routes/routes.go`): +```go +// Shortcut routes - must be before "/" route for precedence +mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut) +``` + +**Frontend Integration** (`templates/partials/modals/pdf-modal.html`): +```javascript +if (selectedFormat === 'default') { + // Default CV: use shortcut URL + const currentYear = new Date().getFullYear(); + url = `/cv-jamr-${currentYear}-${lang}.pdf`; +} +``` + +### Benefits + +**For Users:** +- ✅ Memorable URL pattern +- ✅ Easy to type and share +- ✅ No complex query parameters +- ✅ Professional appearance + +**For Developers:** +- ✅ Auto-updates yearly (no manual changes) +- ✅ Simple validation logic +- ✅ SEO-friendly clean URLs +- ✅ Minimal maintenance overhead + +### Testing + +```bash +# Test current year (should work) +curl -I http://localhost:1999/cv-jamr-2025-en.pdf +# Expected: HTTP/1.1 301 Moved Permanently +# Location: /export/pdf?lang=en&length=short&icons=show&version=with_skills + +# Test invalid year (should fail) +curl -I http://localhost:1999/cv-jamr-2024-en.pdf +# Expected: HTTP/1.1 404 Not Found +``` + ## Design Philosophy ### Why This Naming Convention? @@ -660,9 +759,35 @@ The system automatically handles year rollovers: - ✅ Backwards compatible with existing user preferences - ✅ Comprehensive documentation and troubleshooting +### Version 2.1.0 (2025-11-20) - Year-Aware Shortcut URLs + +**🎯 New Features:** + +#### Shortcut URLs for Default CV +- **NEW**: Memorable shortcut pattern `/cv-jamr-{year}-{lang}.pdf` +- **NEW**: Automatic year validation (only current year accepted) +- **NEW**: Auto-updates yearly without code changes +- **NEW**: "Default CV (Recommended)" option in PDF modal +- **Benefits**: + - ✅ Easy to remember and share: `juan.andres.morenorub.io/cv-jamr-2025-en.pdf` + - ✅ No complex query parameters + - ✅ Professional, clean URLs + - ✅ 301 redirect to proper PDF export endpoint + +**Implementation:** +- Handler: `DefaultCVShortcut()` in `internal/handlers/cv.go:222-263` +- Route: Registered in `internal/routes/routes.go:17` +- Modal: "Default CV" option with visual highlighting (purple gradient, star emoji) +- Frontend: Dynamic year detection via JavaScript + +**Testing:** +- ✅ Current year URLs (2025): 301 redirect to default PDF +- ✅ Past/future years: 404 Not Found +- ✅ Both languages supported (en, es) + --- -**Last Updated**: 2025-11-19 -**Version**: 2.0.0 (New Naming Convention + Light Mode Enforcement) +**Last Updated**: 2025-11-20 +**Version**: 2.1.0 (Year-Aware Shortcut URLs) **Status**: Production ✅ **Maintainer**: Development Team diff --git a/static/css/main.css.backup b/static/css/main.css.backup deleted file mode 100644 index 1c9aba9..0000000 --- a/static/css/main.css.backup +++ /dev/null @@ -1,4711 +0,0 @@ -/* CV Design - Original Style Recreation */ - -/* Import Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&family=Source+Sans+Pro:wght@400;600&family=Inter:wght@400;500;600;700&display=swap'); - -:root { - --bg-gray: rgb(82, 86, 89); - --sidebar-gray: #d1d4d2; - --black-bar: #2b2b2b; - --paper-white: #ffffff; - --text-dark: rgb(0, 0, 0); - --text-gray: rgb(51, 51, 51); - --accent-blue: #0066cc; - --border-gray: #dddddd; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Quicksand', 'Source Sans Pro', -apple-system, system-ui, sans-serif; - background-color: var(--page-bg); - background-image: - linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), - linear-gradient(180deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px), - linear-gradient(180deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px); - background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px; - background-attachment: fixed; - color: var(--text-secondary); - line-height: 1.5; - font-size: 16px; - font-weight: 400; - overflow-x: auto; /* Allow horizontal scroll when zoomed */ -} - -a { - color: var(--accent-blue); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -/* Single Black Top Bar */ -.action-bar { - background: var(--action-bar-bg); - color: var(--action-bar-text); - position: sticky; - top: 0; - z-index: 100; - box-shadow: var(--shadow-md); - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; -} - -.action-bar-content { - max-width: 100%; - margin: 0 auto; - padding: 0; - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: stretch; - gap: 2rem; - height: 50px; -} - -/* Left: Site Title */ -.site-title { - display: flex; - align-items: center; - gap: 0.75rem; - justify-self: start; - white-space: nowrap; - padding: 0; - height: 100%; -} - -.site-title-left { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.site-icon { - color: #fff; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - height: 36px; - padding: 0 .5rem 0 1.5rem; -} - -/* Mobile icon hidden by default, shown only on mobile */ -.site-icon-mobile { - display: none; - color: #fff; - flex-shrink: 0; - margin-right: 0.5rem; -} - -/* Site logo and title links */ -.site-logo-link, -.site-title-link { - text-decoration: none; - color: inherit; - display: flex; - align-items: center; - height: 36px; - transition: opacity 0.2s ease; -} - -.site-logo-link:hover, -.site-title-link:hover { - opacity: 0.8; - text-decoration: none; -} - -.site-logo-link { - padding: 0; -} - -/* Ensure Iconify icons display properly */ -.iconify, -iconify-icon { - display: inline-block; - vertical-align: middle; -} - -.site-title-text { - font-size: 1.05rem; - font-weight: 500; - color: #fff; - letter-spacing: -0.01em; - line-height: 1; - display: flex; - align-items: center; - height: 36px; - padding: 0 1rem 0 0rem; -} - -/* Center: View controls with labels */ -.view-controls-center { - display: flex; - flex-direction: row; - align-items: center; - gap: 2.5rem; - justify-self: center; - flex-shrink: 0; - white-space: nowrap; - height: 100%; -} - -.selector-group { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.selector-label { - font-size: 0.875rem; - color: rgba(255,255,255,0.85); - font-weight: 500; - white-space: nowrap; - letter-spacing: -0.01em; - line-height: 1; - display: flex; - align-items: center; - height: 36px; -} - -.selector-label span { - color: #27ae60; - font-weight: 600; -} - -.language-toggle, -.cv-length-toggle, -.logo-toggle { - flex-shrink: 0; -} - -/* Right: Action buttons */ -.action-buttons { - justify-self: end; - flex-shrink: 0; -} - -.htmx-indicator { - flex-shrink: 0; -} - -.lang-btn { - padding: 0.4rem 1rem; - border: 1px solid rgba(255,255,255,0.3); - background: transparent; - color: white; - border-radius: 3px; - cursor: pointer; - font-size: 1rem; - font-weight: 400; - text-transform: capitalize; - transition: all 0.2s ease; -} - -.lang-btn:hover { - background: rgba(255,255,255,0.1); - border-color: rgba(255,255,255,0.5); -} - -.lang-btn.active { - background: #27ae60 !important; - border-color: #27ae60 !important; - font-weight: 500; -} - -/* Icon Toggle Switches */ -.icon-toggle { - position: relative; - display: flex; - cursor: pointer; -} - -.icon-toggle input[type="checkbox"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} - -.icon-toggle-slider { - position: relative; - display: inline-flex; - align-items: center; - justify-content: space-between; - width: 75px; - height: 30px; - background: #e0e0e0; - border: 2px solid #d0d0d0; - border-radius: 15px; - padding: 0 6px; - transition: all 0.3s ease; -} - -.icon-toggle-slider::before { - content: ''; - position: absolute; - width: 24px; - height: 24px; - left: 2px; - background: white; - border-radius: 50%; - transition: transform 0.3s ease; - box-shadow: 0 2px 4px rgba(0,0,0,0.3); - z-index: 2; - pointer-events: none; -} - -.icon-toggle input:checked + .icon-toggle-slider::before { - transform: translateX(43px); -} - -.icon-toggle input:checked + .icon-toggle-slider { - background: #27ae60; - border-color: #229954; -} - -.icon-toggle-slider .icon-left, -.icon-toggle-slider .icon-right { - position: absolute; - z-index: 3; - transition: all 0.3s ease; - flex-shrink: 0; - pointer-events: none; -} - -.icon-toggle-slider .icon-left { - left: 6px; -} - -.icon-toggle-slider .icon-right { - right: 6px; -} - -.icon-toggle input:not(:checked) + .icon-toggle-slider .icon-left { - color: #333 !important; - font-weight: bold; -} - -.icon-toggle input:not(:checked) + .icon-toggle-slider .icon-right { - color: #999 !important; - opacity: 0.5; -} - -.icon-toggle input:checked + .icon-toggle-slider .icon-left { - color: rgba(255,255,255,0.4) !important; - opacity: 0.5; -} - -.icon-toggle input:checked + .icon-toggle-slider .icon-right { - color: white !important; - font-weight: bold; -} - -.icon-toggle input:focus + .icon-toggle-slider { - box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.2); -} - -/* Language selector wrapper - contains indicators outside swap target */ -.language-selector-wrapper { - position: relative; - display: inline-flex; - height: 100%; - /* Ensure wrapper doesn't create extra spacing */ - width: fit-content; -} - -/* Language selector - matching action button style */ -.language-selector { - display: inline-flex; - gap: 0; - padding: 0; - padding-left: 1rem; /* Space after the title */ - margin-right: 0; - background: transparent; - border-radius: 0; - height: 100%; - align-items: stretch; -} - -/* Position language indicators next to their respective buttons */ -#lang-indicator-en, -#lang-indicator-es { - position: absolute; - top: 50%; - transform: translateY(-50%); - pointer-events: none; - z-index: 10; -} - -/* Position indicators inside the button visual area */ -#lang-indicator-en { - left: calc(1rem + 50px); /* Inside first button */ -} - -#lang-indicator-es { - left: calc(1rem + 135px); /* Inside second button */ -} - -.selector-btn { - padding: 0 1.5rem; - background: transparent; - color: white; - border: none; - border-radius: 0; - cursor: pointer; - font-size: 1rem; - font-weight: 500; - display: inline-flex; - align-items: center; - justify-content: center; - /* gap: 0.5rem; */ - gap: 0rem; - text-decoration: none; - white-space: nowrap; - letter-spacing: -0.01em; - height: 100%; - line-height: 1; - transition: all 0.2s ease; - outline: none !important; - box-shadow: none !important; - min-width: 50px!important; -} - -.selector-btn:focus, -.selector-btn:focus-visible, -.selector-btn:active { - outline: none !important; - box-shadow: none !important; -} - -.selector-btn:hover { - background: #666; -} - -.selector-btn:hover iconify-icon { - color: #27ae60; -} - -.selector-btn.active { - background: #27ae60; - color: white; -} - -.selector-btn:not(.active) { - background: transparent; - color: white; -} - -/* Language selector buttons - no global animations (applied in responsive range only) */ - -/* Action buttons - transparent with white text */ -.action-btn { - padding: 0 1.5rem; - background: transparent; - color: white; - border: none; - border-radius: 0; - cursor: pointer; - font-size: 1rem; - font-weight: 500; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - text-decoration: none; - white-space: nowrap; - letter-spacing: -0.01em; - height: 100%; - line-height: 1; - transition: background-color 0.3s ease, color 0.3s ease; /* Smooth color transitions */ -} - -.action-btn iconify-icon { - color: white; - transition: color 0.3s ease; /* Smooth icon color transition */ -} - -.action-btn:hover { - background: #ddd; - color: #333; - text-decoration: none; -} - -.action-btn:hover iconify-icon { - color: #27ae60; -} - -/* PDF Download button - gray by default, red on hover */ -.pdf-btn { - background: transparent !important; /* Transparent like other buttons */ - color: white !important; -} - -.pdf-btn:hover, -.pdf-btn.pdf-hover-sync { - background: #cd6060 !important; /* PDF red on hover */ - color: white !important; -} - -.pdf-btn iconify-icon { - color: white !important; - filter: brightness(0) invert(1); /* Always white */ - transition: filter 0.3s ease; -} - -.pdf-btn:hover iconify-icon { - color: white !important; - filter: brightness(0) invert(1); /* Keep white on hover */ -} - -/* Print Friendly button - white bg with green icon on hover */ -.print-btn { - background: transparent !important; - color: white !important; -} - -.print-btn:hover, -.print-btn.print-hover-sync { - background: white !important; /* White background on hover */ - color: #27ae60 !important; /* Green icon on hover */ -} - -.print-btn iconify-icon { - color: white; /* White icon by default */ -} - -.print-btn:hover iconify-icon, -.print-btn.print-hover-sync iconify-icon { - color: #27ae60; /* Green icon on hover */ -} - -/* CV Length Toggle - Center of action bar */ -.cv-length-toggle { - display: flex; - gap: 0.5rem; - justify-self: center; -} - -.length-btn { - padding: 0.4rem 1rem; - border: 1px solid rgba(255,255,255,0.4); - background: rgba(255,255,255,0.1); - color: white; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - font-weight: 500; - transition: all 0.2s ease; -} - -.length-btn:hover { - background: rgba(255,255,255,0.2); - border-color: rgba(255,255,255,0.6); -} - -.length-btn.active { - background: white; - color: #1a1a1a; - border-color: white; - font-weight: 600; -} - -/* Action buttons styling (already positioned by grid) */ -.action-buttons, -.action-buttons-right { - display: flex; - gap: 0; - align-items: stretch; - height: 100%; -} - -.action-buttons-right { - justify-self: end; - margin-left: auto; -} - -/* ============================================================================ - HTMX Loading Indicators - ========================================================================= */ - -/* Base indicator styles - hidden by default with opacity for smooth transitions */ -.htmx-indicator { - opacity: 0; /* Hidden by default */ - transition: opacity 200ms ease-in-out; - pointer-events: none; - display: inline-flex; - align-items: center; - justify-content: center; - position: absolute; /* Remove from layout flow to prevent spacing issues */ -} - -/* Override for when request is active - must come AFTER base rule */ -.htmx-indicator.htmx-request, -#lang-indicator-en.htmx-request, -#lang-indicator-es.htmx-request { - opacity: 1 !important; /* Force visible state */ -} - -/* Ensure iconify-icon indicators override global iconify-icon display style */ -iconify-icon.htmx-indicator { - display: inline-flex; - align-items: center; - justify-content: center; -} - -/* Show indicators during HTMX requests */ -/* Using span wrapper, so target span.htmx-request specifically */ -span.htmx-request.htmx-indicator, -.htmx-request .htmx-indicator, -.htmx-request.htmx-indicator { - opacity: 1 !important; -} - -/* Spinning animation for loading icons */ -.htmx-indicator.spinning { - animation: htmx-spin 1s linear infinite; -} - -@keyframes htmx-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* Indicator size variants */ -.htmx-indicator.small { - width: 14px; - height: 14px; - font-size: 14px; -} - -.htmx-indicator.medium { - width: 18px; - height: 18px; - font-size: 18px; -} - -.htmx-indicator.large { - width: 24px; - height: 24px; - font-size: 24px; -} - -/* Positioning variants */ -.htmx-indicator.inline { - display: inline-flex; - margin-left: 8px; - vertical-align: middle; -} - -.htmx-indicator.inline-start { - display: inline-flex; - margin-right: 8px; - vertical-align: middle; -} - -/* Color variants for different contexts */ -.htmx-indicator.light { - color: rgba(255, 255, 255, 0.9); -} - -.htmx-indicator.dark { - color: rgba(0, 0, 0, 0.7); -} - -.htmx-indicator.accent { - color: #27ae60; -} - -/* Respect reduced motion preference */ -@media (prefers-reduced-motion: reduce) { - .htmx-indicator.spinning { - animation: none; - } - .htmx-indicator { - transition: none; - } -} - -/* Legacy loader class for backward compatibility */ -.loader { - border: 2px solid #f3f3f3; - border-top: 2px solid white; - border-radius: 50%; - width: 20px; - height: 20px; - animation: htmx-spin 1s linear infinite; -} - -/* ============================================================================ - Inline Loading States for HTMX Transitions - ========================================================================= */ - -/* Inline loading states - no blocking overlay, smooth transitions only */ -/* Language selector buttons already have htmx-indicator spinners */ -/* CV content areas show subtle fade during swap */ - -/* Zoom Wrapper - wraps cv-container for zoom functionality */ -.zoom-wrapper { - /* CSS zoom property changes actual layout space (not just visual) */ - /* This allows footer to naturally position right after zoomed content */ -} - -/* Main CV Container */ -.cv-container { - width: 100%; - max-width: 100%; /* Full width to accommodate pages */ - margin: 0 auto; - padding: 20px 0 0 0; /* Top padding to prevent sticky action bar overlap */ - display: block; /* Changed from flex */ -} - -/* Clean theme - no sidebars, centered content */ -.cv-container.theme-clean { - padding: 20px 0 0 0; - transition: all 0.3s ease-in-out; -} - -.theme-clean .cv-page { - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-color); - margin: 0 auto; - max-width: 900px; - transition: all 0.3s ease-in-out; -} - -/* Animate sidebar, header, footer when hiding/showing */ -.cv-sidebar, -.cv-title-badges-header, -.cv-footer { - overflow: hidden; - transition: all 0.3s ease-in-out; -} - -.theme-clean .cv-sidebar, -.theme-clean .cv-title-badges-header, -.theme-clean .cv-footer { - display: none !important; - animation: fadeOutShrink 0.3s ease-in-out; -} - -.theme-clean .page-content { - grid-template-columns: 1fr !important; - transition: grid-template-columns 0.3s ease-in-out; -} - -.theme-clean .cv-main { - grid-column: 1 !important; - padding: 2rem 3rem!important; - transition: all 0.3s ease-in-out; -} - -/* CV Paper - Container for two-page layout */ -.cv-paper { - width: 100%; - background: transparent; /* Remove white background - each page has its own */ - box-shadow: none; /* Remove shadow - each page has its own */ - margin: 0; - position: relative; - display: block; /* Changed from grid to block for stacking pages */ - min-height: auto; /* Changed from 100vh */ - - /* Zoom transform properties */ - transform-origin: top center; /* Scale from top center - page stays anchored at top */ - transition: transform 0.08s linear; /* Smooth, immediate zoom response */ - will-change: transform; /* Hint browser to optimize for transforms */ -} - -/* Page break helpers */ -.page-break { - page-break-after: always; - break-after: page; -} - -.avoid-break { - page-break-inside: avoid; - break-inside: avoid; -} - -/* Sidebar - Left column */ -.cv-sidebar { - background: var(--sidebar-gray); - padding: 4rem 1.5rem; - font-size: 0.9rem; -} - -/* Sidebar Accordion - Hidden on desktop, visible on mobile */ -.sidebar-accordion-header { - display: none; -} - -.sidebar-accordion-content { - /* Always visible on desktop */ -} - -.sidebar-section { - margin-bottom: 2rem; -} - -/* Add margin when sidebar section is collapsed */ -.sidebar-section:has(details:not([open])) { - margin-bottom: 3rem; - margin-top: 0rem; -} - -.sidebar-title { - font-family: 'Quicksand', sans-serif; - font-size: 1.4em; - font-weight: 700; - line-height: 1.3em; - margin-bottom: 10px; - padding: 0; - color: rgb(51, 51, 51); - text-align: left; -} - -/* Collapsible Sidebar Section Styles */ -.sidebar-section details { - margin: 0; -} - -.sidebar-section details summary ~ * { - overflow: hidden; - max-height: 0; - opacity: 0; - transform: translateY(-8px); - transition: max-height 0.5s ease-in-out, - opacity 0.3s ease-in-out, - transform 0.3s ease-in-out; -} - -.sidebar-section details[open] summary ~ * { - max-height: 1500px; - opacity: 1; - transform: translateY(0); -} - -.sidebar-section summary { - cursor: pointer; - list-style: none; - user-select: none; - position: relative; - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Remove default triangle marker */ -.sidebar-section summary::-webkit-details-marker, -.sidebar-section summary::marker { - display: none; -} - -/* Sidebar title - no special positioning */ -.sidebar-section summary .sidebar-title { - margin-bottom: 0; -} - -/* Add custom collapse indicator at the far right (left sidebar) */ -.cv-sidebar-left .sidebar-section summary::after { - content: '▶'; - font-size: 0.8em; - color: rgb(100, 100, 100); - transition: transform 0.2s ease, opacity 0.2s ease; - opacity: 0; - margin-left: 15px; - flex-shrink: 0; -} - -/* Add custom collapse indicator at the far left (right sidebar) */ -.cv-sidebar-right .sidebar-section summary { - flex-direction: row-reverse; - justify-content: space-between; -} - -.cv-sidebar-right .sidebar-section summary .sidebar-title { - text-align: right; - width: 100%; -} - -/* Left-align content in left sidebar */ -.cv-sidebar-left .sidebar-content { - text-align: left; -} - -.cv-sidebar-left .skill-item { - text-align: left; -} - -/* Right-align content in right sidebar */ -.cv-sidebar-right .sidebar-content { - text-align: right; -} - -.cv-sidebar-right .skill-item { - text-align: right; -} - -.cv-sidebar-right .sidebar-section summary::after { - content: '▶'; - font-size: 0.8em; - color: rgb(100, 100, 100); - transition: transform 0.2s ease, opacity 0.2s ease; - opacity: 0; - margin-right: 15px; - flex-shrink: 0; -} - -/* Show indicator on hover or when closed */ -.sidebar-section summary:hover::after, -.sidebar-section details:not([open]) summary::after { - opacity: 1; -} - -/* Rotate indicator when open */ -.sidebar-section details[open] summary::after { - transform: rotate(90deg); -} - -/* Hover effect on sidebar summary */ -.sidebar-section summary:hover .sidebar-title { - color: var(--accent-blue); -} - -.sidebar-content { - font-family: 'Quicksand', sans-serif; - font-size: 0.95rem; - font-weight: 400; - line-height: 1.5; -} - -/* Add breathing space when sidebar section is open */ -.sidebar-section details[open] .sidebar-content { - margin-top: 0.5rem; -} - -.skill-item { - margin-bottom: 0.15rem; - color: rgb(0, 0, 0); - font-weight: 400; -} - -/* Main Content - Right column */ -.cv-main { - background: var(--paper-white); - padding: 3rem 2.5rem 8rem 2.5rem; /* Bottom padding for footer and zoom control clearance */ -} - -/* Professional Title Badges - Spans Both Columns */ -.cv-title-badges-header { - grid-column: 1 / -1; /* Span all columns */ - background: #303030 !important; /* Elegant dark gray */ - padding: 10px 20px; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 0; - border-bottom: 2px solid #34495e; -} - -.title-badge { - font-size: 0.9em; - font-weight: normal; - color: #ccc; - text-transform: uppercase; - white-space: nowrap; -} - -.badge-separator { - color: #ccc; - font-weight: normal; - padding: 0 15px; - position: relative; - top: -1px; -} - -/* Header with photo and name */ -.cv-header { - margin-bottom: 2rem; -} - -.cv-header-content { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 2rem; -} - -.cv-header-left { - flex: 1; - position: relative; - /* Desktop: Add right padding to make room for the photo */ - padding-right: 185px; /* Photo width (150px) + gap (35px) */ -} - -.cv-photo { - width: 150px; - height: 200px; - flex-shrink: 0; - overflow: hidden; - border: 3px solid white; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); - - /* Desktop: Position photo in the right padding area */ - position: absolute; - top: 15px; - right: 15px; /* Margin from the right edge */ -} - -.cv-photo img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.cv-name { - font-family: 'Quicksand', sans-serif; - font-size: 2.2em; - font-weight: 400; - {{/* font-style: italic; */}} - line-height: 1.1; - margin-bottom: 8px; - color: rgb(0, 0, 0); - text-align: right; -} - -.cv-experience-years { - font-family: 'Quicksand', sans-serif; - font-size: 0.9em; - font-weight: 500; - line-height: 1.5; - color: rgb(0, 0, 0); - margin: 0; -} - -.years-experience { - font-family: 'Quicksand', sans-serif; - font-size: 1.25em; - font-weight: 400; - color: #666; - margin: 4px 0 0 0; - line-height: 1.4; - text-align: right; -} - -/* Intro/Excerpt Text - Positioned inside header, matching old React CV */ -.intro-text { - font-family: 'Quicksand', sans-serif; - font-size: 1.0em; - line-height: 1.6; - color: rgb(51, 51, 51); - margin-top: 20px; - text-align: justify; - font-style: italic; -} - -/* Sections */ -.cv-section { - margin-bottom: 3rem; - page-break-inside: avoid; -} - -/* Remove margin when section is collapsed */ -.cv-section:has(details:not([open])) { - margin-bottom: 0; -} - -.section-title { - font-family: 'Quicksand', sans-serif; - font-size: 1.4em; - font-weight: 500; - line-height: 1.2em; - margin: 20px 0 25px 0; - padding: 0; - color: rgb(51, 51, 51); -} - -/* Collapsible Section Styles */ -.cv-section details { - margin: 0; -} - -.cv-section details summary ~ * { - overflow: hidden; - max-height: 0; - opacity: 0; - transform: translateY(-8px); - transition: max-height 0.5s ease-in-out, - opacity 0.3s ease-in-out, - transform 0.3s ease-in-out; -} - -.cv-section details[open] summary ~ * { - max-height: 3000px; - opacity: 1; - transform: translateY(0); -} - -.cv-section summary { - cursor: pointer; - list-style: none; - user-select: none; - position: relative; -} - -/* Remove default triangle marker in all browsers */ -.cv-section summary::-webkit-details-marker, -.cv-section summary::marker { - display: none; -} - -/* Add custom collapse indicator after the title */ -.cv-section summary .section-title { - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.cv-section summary .section-title::after { - content: '▼'; - font-size: 0.8em; - color: rgb(100, 100, 100); - transition: transform 0.2s ease, opacity 0.2s ease; - opacity: 0; - margin-left: 0.5rem; -} - -/* Show indicator on hover or when closed */ -.cv-section summary:hover .section-title::after, -.cv-section details:not([open]) summary .section-title::after { - opacity: 1; -} - -/* Rotate indicator when closed */ -.cv-section details:not([open]) summary .section-title::after { - transform: rotate(-90deg); -} - -/* Hover effect on summary */ -.cv-section summary:hover .section-title { - color: var(--accent-blue); -} - -.summary-text { - font-family: 'Quicksand', sans-serif; - line-height: 1.5; - text-align: justify; - font-size: 0.9em; - font-weight: 400; - color: rgb(0, 0, 0); -} - -/* Experience */ -/* Experience item layout moved to logo-toggle.css */ - -.experience-header { - margin-bottom: 0.6rem; -} - -.experience-title-line { - margin-bottom: 0.3em; -} - -.position { - font-size: 1rem; - font-weight: 500; - margin: 0; - color: var(--text-dark); - margin-bottom: 4px; -} - -.position .position-title { - display: inline-block; - margin-right: 0.3em; -} - -.position .company-name { - display: inline-block; -} - -.current-badge { - display: inline-block; - background: #27ae60; - color: white; - font-weight: 700; - font-size: 0.7em; - padding: 0.2em 0.5em; - border-radius: 3px; - margin-left: 0.5em; - vertical-align: middle; - letter-spacing: 0.5px; -} - -.live-badge { - display: inline-flex; - align-items: center; - gap: 0.3em; - background: #27ae60; - color: white; - font-weight: 700; - font-size: 0.7em; - padding: 0.2em 0.5em; - border-radius: 3px; - margin-left: 0.5em; - vertical-align: middle; - letter-spacing: 0.5px; -} - -.live-badge iconify-icon { - font-size: 1.2em; -} - -.expired-badge { - display: inline-block; - background: #e74c3c; - color: white; - font-weight: 700; - font-size: 0.7em; - padding: 0.2em 0.5em; - border-radius: 3px; - margin-left: 0.5em; - vertical-align: middle; - letter-spacing: 0.5px; -} - -.maintained-badge { - display: inline-block; - background: #3498db; - color: white; - font-weight: 700; - font-size: 0.7em; - padding: 0.2em 0.5em; - border-radius: 3px; - margin-left: 0.5em; - vertical-align: middle; - letter-spacing: 0.5px; -} - -.experience-period, -.experience-separator, -.experience-location, -.experience-duration { - color: #555; - font-weight: 600; - display: inline-block; - font-size: 1.05rem; -} - -.experience-duration { - font-style: italic; -} - -.short-desc { - color: var(--text-dark); - font-size: 0.95rem; - line-height: 1.6; - margin-top: 0.5rem; -} - -.duration-text { - color: #aaa; - font-weight: 500; -} - -.responsibilities { - list-style: none; - margin-top: 1rem; - padding-left: 0; -} - -.responsibilities li { - padding-left: 1.2rem; - margin-bottom: 0.4rem; - position: relative; - font-size: 0.95rem; - color: var(--text-dark); - line-height: 1.5; -} - -.responsibilities li:before { - content: "•"; - position: absolute; - left: 0; - color: var(--text-gray); -} - -/* Responsibilities with company icons (similar to main experience layout) */ -.responsibilities li:has(img), -.responsibilities li:has(iconify-icon) { - display: grid; - grid-template-columns: 60px 1fr; - gap: 1rem; - padding-left: 0; - margin-bottom: 1rem; - align-items: start; -} - -.responsibilities li:has(img):before, -.responsibilities li:has(iconify-icon):before { - display: none; -} - -.responsibilities li img { - width: 60px; - height: 60px; - object-fit: contain; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - padding: 4px; -} - -.responsibilities li iconify-icon.default-company-icon { - width: 60px; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - color: #999; - padding: 8px; -} - -/* Education */ -.education-item { - margin-bottom: 1rem; - font-size: 0.95rem; - line-height: 1.6; - color: var(--text-dark); -} - - -/* Languages */ -.languages-list { - display: flex; - flex-wrap: wrap; - gap: 1.5rem; -} - -.language-item { - font-size: 0.95rem!important; - color: var(--text-dark); - margin-bottom: 0.3rem!important; - line-height: 1.4!important; - margin-left: 2rem!important; -} - -.language-item small { - display: block; - font-size: 0.8em; - margin-top: 0.2rem; - font-style: italic; -} - -/* Experience Items */ -.experience-item { - margin-bottom: 2.5rem; - padding-bottom: 2rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -/* Keep border on all experience items including last one */ - -/* Courses */ -.course-item { - display: flex; - gap: 1.2rem; - align-items: flex-start; - margin-bottom: 2.5rem; - padding-bottom: 2rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -/* Keep border on all course items including last one */ - -.course-icon { - flex-shrink: 0; - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; -} - -.course-icon img { - width: 80px; - height: 80px; - object-fit: contain; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - padding: 4px; -} - -.default-course-icon { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - color: #999; - padding: 10px; -} - -.course-content { - flex: 1; -} - -.course-header { - margin-bottom: 0.5rem; -} - -.course-title { - font-size: 1em; - font-weight: 600; - margin: 0 0 0.3rem 0; - line-height: 1.4; - color: var(--text-dark); -} - -.course-title-text { - display: inline; -} - -.course-institution { - display: inline; - margin-left: 0.5em; - font-weight: normal; -} - -.course-period, -.course-separator, -.course-location, -.course-duration { - color: #555; - font-size: 0.9em; -} - -.course-separator { - color: #999; -} - -.course-desc { - font-size: 0.85em; - color: var(--text-gray); - margin-top: 0.4rem; - line-height: 1.4; - text-align: justify; -} - -/* Projects */ -.project-item { - display: flex; - gap: 1.2rem; - align-items: flex-start; - margin-bottom: 2.5rem; - padding-bottom: 2rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -.project-icon { - flex-shrink: 0; - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; -} - -.project-icon img { - width: 80px; - height: 80px; - object-fit: contain; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - padding: 4px; -} - -.default-project-icon { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - color: #999; - padding: 10px; -} - -.project-content { - flex: 1; -} - -.project-header { - margin-bottom: 0.5rem; -} - -.project-title { - font-size: 1em; - font-weight: 600; - margin: 0 0 0.3rem 0; - line-height: 1.4; - color: var(--text-dark); -} - -.project-title-text { - display: inline; -} - -.project-title-text a { - color: var(--accent-blue); - text-decoration: none; -} - -.project-title-text a:hover { - text-decoration: underline; -} - -.project-period, -.project-separator, -.project-location { - color: #555; - font-size: 0.9em; - font-weight: 600; -} - -.project-separator { - color: #999; -} - -.project-desc { - font-size: 0.95rem; - color: var(--text-dark); - margin-top: 0.5rem; - line-height: 1.6; - text-align: justify; -} - -.project-technologies { - font-size: 0.85em; - color: var(--text-gray); - margin-top: 0.5rem; - line-height: 1.4; -} - -.projects-footer { - margin-top: -1.5rem; - padding-top: 0rem; - text-align: center; - font-size: 0.95rem; - color: var(--text-gray); -} - -.projects-footer p { - margin: 0; -} - -.projects-footer a { - color: var(--accent-blue); - text-decoration: none; -} - -.projects-footer a:hover { - text-decoration: underline; -} - -/* References */ -.reference-item { - margin-bottom: 0!important; - line-height: 1.4!important; - margin-left: 2rem!important; - font-size: 0.95rem!important; -} - -.reference-item a { - - color: var(--accent-blue); - text-decoration: none; - word-break: break-word; -} - -.reference-item a:hover { - text-decoration: underline; -} - -.ref-type { - display: block; - font-size: 0.8em; - color: var(--text-gray); - font-style: italic; - margin-top: 0.2rem; -} - -/* Footer */ -footer { - text-align: center; - padding: 2rem; - color: rgba(255,255,255,0.7); - font-size: 0.85rem; -} - -/* GitHub repository link styling */ -.github-repo-link { - color: whitesmoke !important; - transition: color 0.2s ease-in-out; -} - -.github-repo-link:hover { - color: #66B3FF !important; -} - - -/* CV Version Toggle Animations */ -@keyframes fadeInGrow { - from { - opacity: 0; - max-height: 0; - transform: scaleY(0.8); - transform-origin: top; - } - to { - opacity: 1; - max-height: 5000px; - transform: scaleY(1); - } -} - -@keyframes fadeOutShrink { - from { - opacity: 1; - max-height: 5000px; - transform: scaleY(1); - } - to { - opacity: 0; - max-height: 0; - transform: scaleY(0.8); - transform-origin: top; - } -} - -/* Elements that appear/disappear */ -.long-only, -.short-desc { - overflow: hidden; - transition: all 0.3s ease-in-out; -} - -/* Short CV - Hide detailed content with animation */ -.cv-short .long-only { - display: none; - animation: fadeOutShrink 0.3s ease-in-out; -} - -.cv-short .short-desc { - display: block; - animation: fadeInGrow 0.3s ease-in-out; -} - -/* Long CV - Hide short descriptions with animation */ -.cv-long .short-desc, -.short-desc { - display: none; - animation: fadeOutShrink 0.3s ease-in-out; -} - -.cv-long .long-only { - display: block; - animation: fadeInGrow 0.3s ease-in-out; -} - -.cv-long .responsibilities { - display: block; - animation: fadeInGrow 0.3s ease-in-out; -} - -/* Responsive - tablet/mobile */ -@media (max-width: 900px) { - .cv-paper { - grid-template-columns: 1fr; - box-shadow: none; - } - - .cv-sidebar { - padding: 1.5rem 1rem; - } - - .cv-main { - padding: 1.5rem 1rem; - } - - .cv-name { - font-size: 1.8rem; - } - - .intro-text { - font-size: 0.9em; - margin-top: 15px; - } - - .action-bar-content { - grid-template-columns: 1fr; - gap: 1rem; - padding: 0rem; - } - - .language-toggle, - .cv-length-toggle, - .action-buttons { - justify-self: center !important; - justify-content: center; - width: 100%; - } - - .experience-title-line { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - /* ========== Hide header controls, show in menu ========== */ - /* Keep language selector visible in header */ - .view-controls-center { - display: none !important; - } - - .action-buttons-right { - display: none !important; - } - - /* Show controls and actions in hamburger menu */ - .menu-controls-section, - .menu-actions-section { - display: block !important; - } -} - -.no-print {} - -/* Smooth Transitions for HTMX Swaps */ -.cv-paper { - transition: opacity 200ms ease-in-out; -} - -/* Inline loading states for CV content during language transitions */ -.cv-page-content-wrapper.htmx-swapping { - opacity: 0.5; - transform: scale(0.99); - pointer-events: none; - filter: blur(1px); -} - -.cv-page-content-wrapper.htmx-settling { - opacity: 1; - transform: scale(1); - pointer-events: auto; - filter: blur(0); -} - -/* Respect reduced motion preference */ -@media (prefers-reduced-motion: reduce) { - .cv-page-content-wrapper.htmx-swapping { - transform: none; - filter: none; - opacity: 0.7; - } -} - -/* Focus Styles for Accessibility */ -button:focus, -a:focus { - outline: 2px solid var(--accent-blue); - outline-offset: 2px; -} - -/* Loading indicator animation */ -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Error Toast */ -.error-toast { - position: fixed; - bottom: 2rem; - right: 2rem; - background: #fee2e2; - color: #dc2626; - padding: 1rem 1.5rem; - border-radius: 8px; - border-left: 4px solid #dc2626; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - display: none; /* Hidden by default */ - align-items: center; - gap: 1rem; - max-width: 400px; - z-index: 1000; - font-size: 0.95rem; -} - -/* Show toast with full animation cycle: slide in -> stay -> fade out */ -.error-toast.show { - display: flex; - animation: toastLifecycle 5.5s ease-out forwards; -} - -/* Full lifecycle animation: slide in (0-0.3s), stay (0.3-5s), fade out (5-5.5s) */ -@keyframes toastLifecycle { - 0% { - transform: translateX(120%); - opacity: 0; - } - 5.5% { /* 0.3s / 5.5s = 5.5% */ - transform: translateX(0); - opacity: 1; - } - 90.9% { /* 5s / 5.5s = 90.9% - stay visible */ - transform: translateX(0); - opacity: 1; - } - 100% { /* Final 0.5s - fade out and slide right */ - transform: translateX(120%); - opacity: 0; - } -} - -.error-icon { - font-size: 1.25rem; - flex-shrink: 0; -} - -.error-toast button.error-close { - background: none; - border: none; - font-size: 1.5rem; - color: #dc2626; - cursor: pointer; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - transition: opacity 0.2s; - flex-shrink: 0; - line-height: 1; -} - -.error-toast button.error-close:hover { - opacity: 0.7; -} - -.error-toast button.error-close:focus { - outline: 2px solid #dc2626; - outline-offset: 2px; -} - -/* Mobile responsive error toast */ -@media (max-width: 768px) { - .error-toast { - left: 1rem; - right: 1rem; - bottom: 1rem; - max-width: none; - } -} - -/* =============================================== - TWO-PAGE LAYOUT STYLES - =============================================== */ - -/* Page Container - Each CV page */ -.cv-page { - background: var(--paper-bg); - max-width: 1200px; - margin: 2rem auto; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-color); - transform: scale(0.95); - transform-origin: top center; - transition: transform 0.3s ease; -} - -/* Page Content Grid */ -.page-content { - display: grid; -} - -/* Page 1: Left sidebar + Main content */ -.page-1 .page-content { - grid-template-columns: 300px 1fr; -} - -/* Page 2: Main content + Right sidebar */ -.page-2 .page-content { - grid-template-columns: 1fr 300px; -} - -/* Sidebar positioning */ -.cv-sidebar-left { - grid-column: 1; - grid-row: 1; -} - -.cv-sidebar-right { - grid-column: 2; - grid-row: 1; - text-align: right; -} - -/* Main content positioning */ -.page-1 .cv-main { - grid-column: 2; - grid-row: 1; -} - -.page-2 .cv-main { - grid-column: 1; - grid-row: 1; -} - -/* =============================================== - FOOTER STYLES - =============================================== */ - -.cv-footer { - background: #303030; - color: #ccc; - padding: 20px 0; - margin: 0; - grid-column: 1 / -1; /* Span all columns */ -} - -.footer-content { - list-style: none; - text-align: center; - margin: 0; - padding: 0; -} - -.footer-content li { - display: inline-block; - margin: 0; -} - -.footer-content li > div { - display: inline-block; - margin: 0 20px; - text-align: left; -} - -.footer-label { - width: 200px; - font-size: 1.7em; -} - -.footer-value { - width: 450px; - font-size: 1em; -} - -.footer-value b { - font-weight: normal; - font-size: 1.7em; -} - -.footer-separator { - position: relative; - left: -4%; - font-size: 0.6em; -} - -.footer-separator i { - opacity: 0.3; -} - -.cv-footer a { - color: inherit; -} - -.cv-footer a:hover { - color: #0275d8; - text-decoration: none; - font-weight: bold; -} - -/* =============================================== - PRINT STYLES - Handled by print.css - =============================================== */ -/* All print styles consolidated in /static/css/print.css */ - -/* =============================================== - SECTION STYLES FOR PAGE 2 - =============================================== */ - -.language-item, -.reference-item, -.other-content { - margin-bottom: 0!important; - line-height: 1.4!important; - margin-left: 2rem!important; - font-size: 0.95rem!important; -} - -/* Award item with logo */ -.award-item { - display: flex; - gap: 1.2rem; - align-items: flex-start; - margin-bottom: 2.5rem; - padding-bottom: 2rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - page-break-inside: avoid; - transition: gap 0.3s ease-in-out; -} - -/* Keep border on all award items including last one */ - -/* ======================================== - HIDE ICONS, ICONS, AND BADGES MODE - ======================================== */ - -/* Adjust gap when icons are hidden */ -.cv-paper:not(.show-icons) .award-item { - gap: 0; -} - -/* Hide all icons when .show-icons is not present */ -.cv-paper:not(.show-icons) .company-logo, -.cv-paper:not(.show-icons) .award-logo, -.cv-paper:not(.show-icons) .project-icon, -.cv-paper:not(.show-icons) .course-icon { - display: none !important; -} - -/* Hide icons inside responsibilities (Drolosoft sub-clients, Third Party projects) */ -.cv-paper:not(.show-icons) .responsibilities li img, -.cv-paper:not(.show-icons) .responsibilities li iconify-icon.default-company-icon { - display: none !important; -} - -/* Adjust layout for responsibilities without icons */ -.cv-paper:not(.show-icons) .responsibilities li:has(img), -.cv-paper:not(.show-icons) .responsibilities li:has(iconify-icon) { - display: block !important; - grid-template-columns: none !important; - padding-left: 1.2rem !important; -} - -/* Restore bullet points for responsibilities without icons */ -.cv-paper:not(.show-icons) .responsibilities li:has(img):before, -.cv-paper:not(.show-icons) .responsibilities li:has(iconify-icon):before { - display: block !important; -} - -/* Hide all section icons */ -.cv-paper:not(.show-icons) .section-icon { - display: none !important; -} - -/* Hide all badges (Current, Expired, Maintained By) */ -.cv-paper:not(.show-icons) .current-badge, -.cv-paper:not(.show-icons) .expired-badge, -.cv-paper:not(.show-icons) .maintained-badge { - display: none !important; -} - -/* Adjust experience items layout when icons are hidden */ -.cv-paper:not(.show-icons) .experience-item { - display: block !important; -} - -/* Adjust project and course items layout when icons are hidden */ -.cv-paper:not(.show-icons) .project-item, -.cv-paper:not(.show-icons) .course-item, -.cv-paper:not(.show-icons) .award-item { - display: block !important; - gap: 0 !important; -} - -.award-logo { - flex-shrink: 0; - display: block; -} - -.award-logo img { - width: 80px; - height: 80px; - object-fit: contain; - border-radius: 4px; - border: 1px solid #ddd; - background: white; - padding: 10px; -} - -.default-award-icon { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - border: 1px solid #ddd; - background: #f5f5f5; - color: #999; - padding: 10px; -} - -.award-content { - flex: 1; -} - -.award-item strong, -.course-item strong, -.language-item strong { - font-weight: 600; - color: var(--text-dark); -} - -/* Add checkmarks before language items */ -.language-item::before { - content: "✓"; - color: var(--text-dark); - font-weight: normal; - font-size: 0.85em; - margin-right: 0.4em; -} - -.award-item small, -.course-item small { - color: #666; - font-size: 0.875em; -} - -.award-desc, -.course-desc { - margin-top: 0.5em; - color: var(--text-gray); - font-size: 0.95rem; -} - -.reference-item { - margin-bottom: 0.4em; - line-height: 1.4; -} - -/* Add checkmarks before reference items */ -.reference-item::before { - content: "✓"; - color: var(--accent-blue); - font-weight: normal; - font-size: 0.85em; - margin-right: 0.4em; -} - -.reference-item a { - font-weight: 500; -} - -.ref-type { - color: #999; - margin-left: 0.5em; - font-size: 0.875em; -} - -/* Add checkmark before other content */ -.other-content::before { - content: "✓"; - color: var(--text-dark); - font-weight: normal; - font-size: 0.85em; - margin-right: 0.4em; -} - -/* =============================================== - MOBILE RESPONSIVE - TWO-PAGE LAYOUT - =============================================== */ - -@media (max-width: 900px) { - .cv-page { - margin: 0.5rem; - transform: scale(1); - border: none; - box-shadow: none; - } - - /* Stack layout on mobile using flexbox for proper flow */ - .page-1 .page-content, - .page-2 .page-content { - display: flex !important; - flex-direction: column; - gap: 0; - } - - .cv-sidebar-left, - .cv-sidebar-right { - width: 100% !important; - position: static !important; - grid-column: unset; - grid-row: unset; - padding: 0 !important; - } - - /* ========== Sidebar Accordion on Mobile ========== */ - .sidebar-accordion-header { - display: flex !important; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: var(--sidebar-gray); - cursor: pointer; - user-select: none; - border-bottom: 2px solid rgba(0, 0, 0, 0.1); - transition: background-color 0.2s ease; -text-align: center; - } - - .sidebar-accordion-header:hover { - background: #c5c5c5; - } - - .sidebar-accordion-header:active { - background: #b8b8b8; - } - - .sidebar-accordion-header iconify-icon { - flex-shrink: 0; - } - - .sidebar-accordion-header span { - font-weight: 700; - font-size: 1.3rem; - flex: 1; - } - - .sidebar-accordion-header .chevron { - transition: transform 0.3s ease; - } - - .sidebar-accordion-header.active .chevron { - transform: rotate(180deg); - } - - .sidebar-accordion-content { - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease-out; - padding: 0 1rem; - } - - .sidebar-accordion-content.active { - max-height: 5000px; - transition: max-height 0.5s ease-in; - padding: 1rem 1rem 0.5rem 1rem; - } - - /* Reduce font sizes inside accordion content */ - .sidebar-accordion-content .sidebar-title { - font-size: 1rem; - } - - .sidebar-accordion-content .sidebar-content, - .sidebar-accordion-content .skill-item { - font-size: 0.8rem; - } - - .page-1 .cv-main, - .page-2 .cv-main { - width: 100% !important; - position: static !important; - grid-column: unset; - grid-row: unset; - padding: 2rem 1.5rem !important; - } - - /* Adjust title badges header for mobile */ - .cv-title-badges-header { - padding: 0.75rem 1rem !important; - font-size: 0.85rem; - } - - .badge { - font-size: 0.75rem !important; - padding: 0.25rem 0.5rem !important; - } - - /* Hide header on page 2 for mobile to merge pages */ - .page-2 .cv-title-badges-header { - display: none; - } - - /* Adjust footer for mobile */ - .cv-footer { - padding: 1.5rem 1rem; - } - - .footer-content li { - display: block !important; - margin-bottom: 1.5rem; - } - - .footer-content li > div { - display: block; - margin: 0; - text-align: center; - width: 100%; - } - - .footer-label { - font-size: 0.9rem; - margin-top: 1rem; - color: #777; - } - - .footer-separator { - display: none; - } - - .footer-value { - font-size: 1.2rem; - margin-bottom: 0; - padding: 0; - } - - .footer-value b { - font-size: 1.3rem; - } - - .site-title { - justify-content: space-between; - width: 100%; - } - - /* Ensure proper stacking order */ - .cv-sidebar-left { - order: 1; - } - - .cv-title-badges-header { - order: 2; - } - - .cv-main { - order: 3; - } - - .cv-sidebar-right { - order: 4; - } - - .cv-footer { - order: 5; - } -} - -/* =============================================== - TABLET RESPONSIVE - =============================================== */ - -@media (max-width: 768px) { - /* Further reduce padding on smaller screens */ - .cv-page { - margin: 0.25rem; - } - - .cv-sidebar-left, - .cv-sidebar-right { - width: 100% !important; - padding: 1.5rem 1rem; - } - - /* Force left alignment for all sidebar content on mobile */ - .cv-sidebar-left .sidebar-content, - .cv-sidebar-left .skill-item { - text-align: left !important; - } - /* Force right alignment for all sidebar content on mobile */ - .cv-sidebar-right .sidebar-content, - .cv-sidebar-right .skill-item { - text-align: right !important; - } - .page-1 .cv-main, - .page-2 .cv-main { - width: 100% !important; - padding: 1.5rem 1rem !important; - } - - .cv-title-badges-header { - padding: 0.5rem 0.75rem !important; - font-size: 0.8rem; - } - - .badge { - font-size: 0.7rem !important; - padding: 0.2rem 0.4rem !important; - } - - /* Footer refinements */ - .cv-footer { - padding: 1rem 0.75rem; - } - - .footer-label { - font-size: 1.1rem; - margin-top: 0.75rem; - } - - .footer-value { - font-size: 0.85rem; - } - - .footer-value b { - font-size: 0.95rem; - } - - /* Sidebar title sizing */ - .sidebar-title { - font-size: 1.1rem; - } - - .sidebar-content { - font-size: 0.85rem; - } - - /* Section titles */ - .section-title { - font-size: 1.1rem; - margin-bottom: 1rem; - } -} - -/* Company link styling */ -.company-link { - color: var(--accent-blue); - font-weight: 500; - text-decoration: none; -} - -.company-link:hover { - text-decoration: underline; - color: #0052a3; -} - -/* =============================================== - HAMBURGER MENU & NAVIGATION - =============================================== */ - -/* Hamburger button */ -.hamburger-btn { - background: transparent; - border: none; - color: #fff; - cursor: pointer; - padding: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.2s ease; - border-radius: 4px; - margin: 0 0.5rem; - position: relative; /* For CSS-only hover trigger */ -} - - -.hamburger-btn:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.hamburger-btn:active { - background-color: rgba(255, 255, 255, 0.2); -} - -/* Navigation Menu */ -.navigation-menu { - position: fixed; - top: 50px; /* Height of action bar */ - left: 0; - width: 280px; - max-height: 0; - background: #ffffff; - box-shadow: 2px 0 10px rgba(0, 0, 0, 0.15); - transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; - overflow-y: auto; - z-index: 1000; /* Above fixed buttons (z-index: 999) */ - pointer-events: none; /* Disable pointer events when hidden */ - opacity: 0; -} - -/* Pure CSS Menu Activation - Show menu when hovering hamburger OR menu */ -/* Show when hovering the hamburger button (adjacent in DOM after site-title-left) */ -.hamburger-btn:hover ~ .navigation-menu, -.hamburger-btn:focus ~ .navigation-menu, -/* Show when hovering the menu itself */ -.navigation-menu:hover, -/* Legacy class for backward compatibility */ -.navigation-menu.menu-hover, -.navigation-menu.menu-open { - max-height: calc(100vh - 60px); /* Viewport height minus header + some spacing */ - pointer-events: auto; /* Enable pointer events when visible */ - opacity: 1; -} - -.menu-content { - padding: 1rem 0; -} - -.menu-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.875rem 1.5rem; - color: var(--text-dark); - text-decoration: none; - transition: background-color 0.2s ease, color 0.2s ease; - font-size: 0.95rem; - font-weight: 500; - border-left: 3px solid transparent; -} - -.menu-item:hover { - background-color: rgba(0, 102, 204, 0.08); - color: var(--accent-blue); - border-left-color: var(--accent-blue); - text-decoration: none; -} - -.menu-item iconify-icon { - color: var(--text-gray); - flex-shrink: 0; - transition: color 0.2s ease; -} - -.menu-item:hover iconify-icon { - color: var(--accent-blue); -} - -/* Menu item action controls (Expand All, Collapse All) */ -/* Removed centered text styling - action items now behave like regular menu items */ - -/* Remove extra padding - all menu items should align consistently */ - -/* Submenu styles - hover triggered, opens to the right */ -.menu-item-submenu { - position: relative; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 0 0 1rem 0; -} - -.menu-item.has-submenu { - justify-content: space-between; - position: relative; -} - -.submenu-arrow { - transition: transform 0.2s ease; - margin-left: auto; -} - -/* Rotate arrow slightly on hover */ -.menu-item-submenu:hover .submenu-arrow { - transform: translateX(3px); -} - -.submenu-content { - position: fixed; /* Changed from absolute to fixed to break out of parent overflow */ - left: 232px; /* Slight overlap with menu to eliminate any gap */ - background: #ffffff; - box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.15); - border-radius: 8px; - min-width: 250px; - max-width: 300px; - opacity: 0; - visibility: hidden; - transform: translateX(-3px); - transition: all 0.3s ease; - z-index: 1000; /* Higher z-index to appear above everything */ - padding: 0.5rem 0; - max-height: calc(100vh - 100px); /* Ensure it fits viewport */ - overflow-y: auto; /* Scroll if content is too long */ -} - -/* Show submenu when hovering the submenu container OR the submenu itself */ -.menu-item-submenu:hover .submenu-content, -.submenu-content:hover { - opacity: 1; - visibility: visible; - transform: translateX(0); -} - -/* Legacy class for JS compatibility */ -.menu-item-submenu.submenu-open .submenu-arrow { - transform: translateX(3px); -} - -.menu-item-submenu.submenu-open .submenu-content { - opacity: 1; - visibility: visible; - transform: translateX(0); -} - -.submenu-content .menu-item { - padding: 0.875rem 1.5rem; - font-size: 0.9rem; - border-left: 3px solid transparent; - border-radius: 0; -} - -.submenu-content .menu-item:first-child { - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - -.submenu-content .menu-item:last-child { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; -} - -/* ========== Menu Sections with Separators ========== */ -/* Quick Actions section - always visible */ -.menu-section-wrapper { - padding: 0.5rem 1.5rem 1rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -/* Remove border from last visible section */ -.menu-content > *:last-child, -.menu-content > div:last-child { - border-bottom: none !important; -} - -/* ========== Menu Controls & Actions (Always Visible) ========== */ -/* Always visible in hamburger menu at all screen sizes */ -.menu-controls-section, -.menu-actions-section { - display: block; - padding: 0.5rem 1.5rem 1rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -.menu-item-header { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.875rem 0 0.875rem 0; - color: var(--text-dark); - font-size: 0.85rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; - cursor: default; -} - -/* Disable hover effect for headers */ -.menu-item-header:hover { - background-color: transparent !important; - color: var(--text-dark) !important; - border-left-color: transparent !important; -} - -.menu-item-header iconify-icon { - color: var(--text-gray); - flex-shrink: 0; -} - -.menu-item-header:hover iconify-icon { - color: var(--text-gray) !important; -} - -.menu-item-header span { - flex: 1; -} - -.menu-control-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 0; -} - -.menu-control-label { - display: flex; - align-items: center; - gap: 0.75rem; - color: var(--text-dark); - font-size: 0.9rem; - font-weight: 500; -} - -.menu-control-label iconify-icon { - color: var(--text-gray); -} - -.menu-action-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - padding: 0.875rem 1rem; - margin: 0.25rem 0; - background: rgba(0, 0, 0, 0.03); - border: none; - border-radius: 8px; - color: var(--text-dark); - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - width: 100%; -} - -.menu-action-btn:hover { - background: rgba(0, 102, 204, 0.08); - color: var(--accent-blue); - text-decoration: none; -} - -.menu-action-btn iconify-icon { - color: var(--text-gray); - flex-shrink: 0; - transition: color 0.2s ease; -} - -.menu-action-btn:hover iconify-icon { - color: var(--accent-blue); -} - -/* PDF button in menu - White bg with red icon on hover */ -.menu-pdf-btn:hover, -.menu-pdf-btn.pdf-hover-sync { - background: white !important; - color: #e74c3c !important; -} - -.menu-pdf-btn:hover iconify-icon, -.menu-pdf-btn.pdf-hover-sync iconify-icon { - color: #e74c3c !important; -} - -/* Print button in menu - White bg with green icon on hover */ -.menu-print-btn:hover, -.menu-print-btn.print-hover-sync { - background: white !important; - color: #27ae60 !important; -} - -.menu-print-btn:hover iconify-icon, -.menu-print-btn.print-hover-sync iconify-icon { - color: #27ae60 !important; -} - -/* Section icons in titles */ -.section-icon { - vertical-align: middle; - margin-right: 0.5rem; - color: #7d7d7d; -} - -/* Add invisible separator (blank space) below section titles */ -#experience .section-title, -#awards .section-title, -#courses .section-title, -#projects .section-title { - margin-bottom: 40px !important; -} - -/* Smooth scrolling */ -html { - scroll-behavior: smooth; -} - -/* Add scroll padding to account for fixed header */ -html { - scroll-padding-top: 70px; /* Action bar height + some spacing */ -} - -/* Mobile responsive */ -@media (max-width: 768px) { - .navigation-menu { - width: 240px; - } - - .menu-item { - padding: 0.75rem 1rem; - font-size: 0.9rem; - } - - .site-title { - justify-content: space-between; - width: 100%; - } -} - -/* Hide menu overlay on print */ -@media print { - .navigation-menu { - display: none !important; - } - - .hamburger-btn { - display: none !important; - } -} - -/* ======================================== - Scroll Direction - Hide/Show Header - ======================================== */ - -/* Add smooth transition to header elements */ -.action-bar, -.navigation-menu { - transition: transform 0.3s ease-in-out; -} - -/* Hide header when scrolling down */ -.action-bar.header-hidden { - transform: translateY(-100%); -} - -.navigation-menu.header-hidden { - transform: translateY(-100%); -} - -/* ======================================== - Back to Top Button - ======================================== */ - -.back-to-top { - position: fixed; - bottom: 2rem; - right: 2rem; - width: 50px; - height: 50px; - background: var(--black-bar); - 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); - z-index: 99; - transition: all 0.3s ease; - opacity: 0.2; -} - -.back-to-top:hover { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #27ae60; -} - -.back-to-top.at-bottom { - opacity: 1; - background: #27ae60; -} - -.back-to-top:active { - transform: translateY(-1px); - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); -} - -/* Mobile adjustments */ -@media (max-width: 768px) { - .back-to-top { - bottom: 1.5rem; - right: 1.5rem; - width: 45px; - height: 45px; - } -} - -/* ======================================== - Info Button (Bottom Left) - ======================================== */ - -.info-button { - position: fixed; - bottom: 2rem; - left: 2rem; - width: 50px; - height: 50px; - background: var(--black-bar); - 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); - z-index: 99; - transition: all 0.3s ease; - opacity: 0.6; /* Increased from 0.2 for better discoverability */ -} - -.info-button:hover { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #27ae60; -} - -.info-button.at-bottom { - opacity: 1; - background: #27ae60; -} - -.info-button:active { - transform: translateY(-1px); - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); -} - -/* Mobile adjustments - Flexbox button layout at bottom center */ -@media (max-width: 900px) { - /* Hide only zoom control on mobile */ - .zoom-toggle-btn, - .zoom-control { - display: none !important; - } - - /* Reset fixed positioning for FLEXBOX buttons on mobile (exclude back-to-top) */ - .download-btn, - .print-friendly-btn, - .shortcuts-btn, - .info-button { - position: fixed !important; - bottom: 1.5rem !important; - left: auto !important; - right: auto !important; - width: 50px !important; - height: 50px !important; - opacity: 0.7 !important; - transform: none !important; - } - - /* Keep back-to-top button at bottom-right (same as desktop) */ - .back-to-top { - position: fixed !important; - bottom: 1.5rem !important; - right: 1.5rem !important; - width: 50px !important; - height: 50px !important; - } - - /* Flexbox container behavior - buttons arrange themselves */ - /* Buttons will be positioned using JavaScript or individual positioning */ - /* For now, use fixed spacing from center */ - - /* 5 buttons: Download, Print, Shortcuts, Theme, Info */ - /* Spacing: 10px gap between buttons, centered horizontally */ - /* Total width: 5 * 50px + 4 * 10px = 290px */ - /* Start position: 50% - 145px */ - - .download-btn { - left: calc(50% - 145px) !important; /* First button */ - } - - .print-friendly-btn { - left: calc(50% - 85px) !important; /* Second button */ - } - - .shortcuts-btn { - left: calc(50% - 25px) !important; /* Third button */ - } - - /* Theme switcher button - fourth position (defined in color-theme.css) */ - /* left: calc(50% + 35px) !important; */ - - .info-button { - left: calc(50% + 95px) !important; /* Fifth button (last) */ - } - - /* Hover effects - only Y transform + enhanced shadow */ - .download-btn:hover, - .download-btn.pdf-hover-sync, - .print-friendly-btn:hover, - .print-friendly-btn.print-hover-sync, - .shortcuts-btn:hover, - .info-button:hover, - .back-to-top:hover { - transform: translateY(-3px) !important; - opacity: 1 !important; - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; - } - - /* Keep at-bottom state without transform */ - .info-button.at-bottom, - .shortcuts-btn.at-bottom { - opacity: 1 !important; - transform: none !important; - } -} - -/* Very narrow mobile - Move back-to-top UP on RIGHT side to avoid overlap */ -@media (max-width: 483px) { - .back-to-top { - /* Stay on RIGHT side, just move UP higher */ - right: 1.5rem !important; - bottom: 5.5rem !important; /* Higher position to clear bottom button row */ - } -} - -/* ======================================== - Info Modal - Modern Glassmorphism Design - ======================================== */ - -/* Native element - force centering */ -.info-modal { - border: none; - border-radius: 24px; - padding: 0; - max-width: 420px; - width: calc(100% - 2rem); - background: transparent; - /* Force centering - override any browser defaults */ - position: fixed; - inset: 0; - margin: auto; - /* Constrain height so margin:auto can center vertically */ - max-height: fit-content; -} - -/* Native ::backdrop pseudo-element replaces manual backdrop div */ -.info-modal::backdrop { - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -/* Dialog opening animation - native dialog uses [open] attribute */ -.info-modal[open] { - animation: modalFadeIn 0.3s ease; -} - -@keyframes modalFadeIn { - from { - opacity: 0; - transform: scale(0.9) translateY(20px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -.info-modal-content { - background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), 0 0 100px rgba(39, 174, 96, 0.1); - width: 100%; - padding: 2.5rem; - position: relative; - border: 1px solid rgba(255, 255, 255, 0.8); -} - -.info-modal-close { - position: absolute; - top: 1rem; - right: 1rem; - background: rgba(0, 0, 0, 0.05); - border: none; - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: #333; - transition: all 0.2s ease; - z-index: 10; -} - -.info-modal-close:hover { - background: rgba(0, 0, 0, 0.1); - transform: rotate(90deg); -} - -.info-modal-header { - text-align: center; - margin-bottom: 2rem; -} - -.info-modal-header h2 { - font-size: 1.5rem; - font-weight: 600; - color: #333; - margin: 0 0 1.5rem 0; -} - -.info-modal-cv-title { - font-size: 1.5rem; - font-weight: 700; - color: #f39c12; /* Orange subtitle */ - margin-bottom: 0; - letter-spacing: 0.05em; - display: flex; - align-items: center; - gap: 0.5rem; - justify-content: center; -} - -#info-modal .info-modal-cv-title { - color: #27ae60; -} - -.info-modal-photo { - width: 40px; - height: 53px; - object-fit: cover; - border-radius: 4px; - border: none; - box-shadow: none; -} - -.photo-bracket-wrapper { - position: relative; - display: inline-flex; - align-items: center; - padding: 0 22px; -} - -.photo-bracket-wrapper::before { - content: '{'; - position: absolute; - left: 2px; - font-size: 2rem; - font-weight: 700; - color: #27ae60; - line-height: 1; - top: 8px; -} - -.photo-bracket-wrapper::after { - content: '}'; - position: absolute; - right: 2px; - font-size: 2rem; - font-weight: 700; - color: #27ae60; - line-height: 1; - top: 8px; -} - -.info-modal-body { - color: #333; -} - -.info-modal-description { - font-size: 1rem; - line-height: 1.6; - margin-bottom: 2rem; - color: #444; -} - -.info-modal-description strong { - color: #27ae60; - font-weight: 600; -} - -.info-modal-tech { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - margin-bottom: 2rem; -} - -.info-tech-item { - display: flex; - align-items: center; - justify-content: center; - gap: 0.75rem; - padding: 0.75rem; - background: rgba(39, 174, 96, 0.05); - border-radius: 12px; - border: 1px solid rgba(39, 174, 96, 0.1); - transition: all 0.3s ease; -} - -.info-tech-item:hover { - background: rgba(39, 174, 96, 0.1); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(39, 174, 96, 0.2); -} - -.info-tech-item iconify-icon { - color: #27ae60; - flex-shrink: 0; -} - -.info-tech-item span { - font-size: 0.9rem; - font-weight: 500; - color: #333; -} - -.info-modal-github { - display: flex; - align-items: center; - justify-content: center; - gap: 0.75rem; - padding: 1rem 1.5rem; - background: linear-gradient(135deg, #27ae60 0%, #229954 100%); - color: white; - text-decoration: none; - border-radius: 12px; - font-weight: 600; - font-size: 1rem; - transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3); -} - -.info-modal-github:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(39, 174, 96, 0.4); - background: linear-gradient(135deg, #229954 0%, #27ae60 100%); -} - -.info-modal-github:active { - transform: translateY(0); - box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3); -} - -.info-modal-github-subtext { - text-align: center; - font-size: 0.9rem; - color: #666; - margin-top: 1.5rem; - margin-bottom: 1rem; - font-style: italic; -} - -/* Mobile responsive */ -@media (max-width: 768px) { - .info-modal-content { - padding: 2rem 1.5rem; - max-width: calc(100% - 2rem); - } - - .info-modal-header h2 { - font-size: 1.5rem; - } - - .info-modal-tech { - grid-template-columns: 1fr; - } - - .info-modal-description { - font-size: 0.95rem; - } -} - -/* ======================================== - Desktop: Ensure Sidebar Content Visible (>1280px) - ======================================== */ - -/* ======================================== - Responsive: Medium Screens (901px - 1023px) - ======================================== */ - -@media (min-width: 901px) and (max-width: 1023px) { - - /* ========== Global Font Size Reduction ========== */ - html { - font-size: 14px; /* Reduced from default 16px */ - } - - .cv-name { - font-size: 1.8em; /* Reduced from 2.2em */ - } - - .sidebar-title { - font-size: 0.95rem; - } - - .sidebar-content { - font-size: 0.9rem; - } - - /* ========== Selector Labels - Hide, Show on Hover ========== */ - .selector-label { - max-width: 0; - overflow: hidden; - opacity: 0; - transition: all 0.3s ease; - white-space: nowrap; - } - - .selector-group:hover .selector-label { - max-width: 200px; - opacity: 1; - margin-right: 0.75rem; - } - - /* ========== Language Selector - Collapse to EN/ES ========== */ - .language-selector .selector-btn { - position: relative; - padding: 0.4rem 1rem; /* Keep padding consistent */ - min-width: 50px; - font-size: 0; /* Hide actual text */ - overflow: visible; - transition: font-size 0.3s ease; /* Slower animation */ - display: inline-flex; - justify-content: center; - align-items: center; - } - - /* Show only short version (EN/ES) */ - .language-selector .selector-btn::before { - content: attr(data-short); - font-size: 1rem; /* Restore font size for pseudo-element */ - opacity: 1; - transition: opacity 0.3s ease; /* Slower animation */ - display: block; - width: 100%; - text-align: center; - } - - /* On hover of INDIVIDUAL button only, show full text */ - .language-selector .selector-btn:hover { - font-size: 1rem; /* Restore font size to show full text */ - min-width: auto; - } - - .language-selector .selector-btn:hover::before { - content: ''; /* Hide short version */ - opacity: 0; - } - - /* ========== Action Buttons - Icon Only, Expand on Hover ========== */ - .action-btn { - position: relative; - width: 45px; - overflow: hidden; - transition: width 0.3s ease, padding 0.3s ease; - white-space: nowrap; - text-indent: 0; - } - - /* Hide button text, keep icon */ - .action-btn iconify-icon { - flex-shrink: 0; - } - - .action-btn { - font-size: 0; - padding: 0 0.65rem; - justify-content: center; - } - - /* On hover, show text */ - .action-btn:hover { - width: auto; - font-size: 0.95rem; - padding: 0.65rem 1.5rem; - gap: 0.5rem; - } - - /* ========== Sidebar Content - Hide Text, Show on Hover ========== */ - .sidebar-content { - max-height: 0 !important; - overflow: hidden !important; - opacity: 0 !important; - } - - /* Show sidebar content on hover */ - .skill-category:hover .sidebar-content, - .cv-sidebar-section:hover .sidebar-content { - max-height: 1000px !important; - opacity: 1 !important; - margin-top: 10px !important; - } -} - -/* ======================================== - Responsive: Medium Screens (1024px - 1280px) - ======================================== */ - -@media (min-width: 1024px) and (max-width: 1280px) { - - /* ========== Global Font Size Reduction ========== */ - html { - font-size: 14px; /* Reduced from default 16px */ - } - - .cv-name { - font-size: 1.8em; /* Reduced from 2.2em */ - } - - .sidebar-title { - font-size: 0.95rem; - } - - .sidebar-content { - font-size: 0.9rem; - } - - /* ========== Selector Labels - Hide, Show on Hover ========== */ - .selector-label { - max-width: 0; - overflow: hidden; - opacity: 0; - transition: all 0.3s ease; - white-space: nowrap; - } - - .selector-group:hover .selector-label { - max-width: 200px; - opacity: 1; - margin-right: 0.75rem; - } - - /* ========== Language Selector - Collapse to EN/ES ========== */ - .language-selector .selector-btn { - position: relative; - padding: 0.4rem 1rem; /* Keep padding consistent */ - min-width: 50px; - font-size: 0; /* Hide actual text */ - overflow: visible; - transition: font-size 0.3s ease; /* Slower animation */ - display: inline-flex; - justify-content: center; - align-items: center; - } - - /* Show only short version (EN/ES) */ - .language-selector .selector-btn::before { - content: attr(data-short); - font-size: 1rem; /* Restore font size for pseudo-element */ - opacity: 1; - transition: opacity 0.3s ease; /* Slower animation */ - display: block; - width: 100%; - text-align: center; - } - - /* On hover of INDIVIDUAL button only, show full text */ - .language-selector .selector-btn:hover { - font-size: 1rem; /* Restore font size to show full text */ - min-width: auto; - } - - .language-selector .selector-btn:hover::before { - content: ''; /* Hide short version */ - opacity: 0; - } - - /* ========== Action Buttons - Icon Only, Expand on Hover ========== */ - .action-btn { - position: relative; - width: 45px; - overflow: hidden; - transition: width 0.3s ease, padding 0.3s ease; - white-space: nowrap; - text-indent: 0; - } - - /* Hide button text, keep icon */ - .action-btn iconify-icon { - flex-shrink: 0; - } - - .action-btn { - font-size: 0; - padding: 0 0.65rem; - justify-content: center; - } - - /* On hover, show text */ - .action-btn:hover { - width: auto; - font-size: 0.95rem; - padding: 0.65rem 1.5rem; - gap: 0.5rem; - } - - /* ========== Sidebar Content - Hide Text, Show on Hover ========== */ - .sidebar-content { - max-height: 0 !important; - overflow: hidden !important; - opacity: 0 !important; - } - - /* Show sidebar content on hover */ - .skill-category:hover .sidebar-content, - .cv-sidebar-section:hover .sidebar-content { - max-height: 1000px !important; - opacity: 1 !important; - margin-top: 10px !important; - } -} - -/* ======================================== - Responsive: Small Screens (up to 540px) - ======================================== */ - -/* ======================================== - Responsive: Tablet Screens - Center Photo (up to 768px) - ======================================== */ - -@media (max-width: 768px) { - /* ======================================== - TYPOGRAPHY - Subtle font size reductions - ======================================== */ - .cv-name { - text-align: center; - font-size: 1.6rem; - } - - .years-experience { - text-align: center; - font-size: 1.1em; - } - - .section-title { - font-size: 1.2em; - } - - .sidebar-title { - font-size: 1.2em; - } - - .experience-period, - .experience-separator, - .experience-location, - .experience-duration { - font-size: 0.95rem; - } - - .position { - font-size: 0.95rem; - } - - .short-desc, - .responsibilities li { - font-size: 0.85rem; - } - - /* ======================================== - TEXT ALIGNMENT FIXES - Selective alignment - ======================================== */ - /* Keep justified for intro and skills */ - .intro-text, - .summary-text { - text-align: justify; - font-size: 0.85rem; - line-height: 1.5; - } - - .intro-text { - margin-top: 0; - width: 100%; - } - - /* Left-align for course/project descriptions */ - .course-desc, - .project-desc { - text-align: left !important; - font-size: 0.85rem !important; - line-height: 1.5; - } - - /* ======================================== - HEADER LAYOUT - Centered photo - ======================================== */ - .cv-header-content { - flex-direction: column; - align-items: center; - gap: 1rem; - } - - .cv-header-left { - width: 100%; - position: static; - padding-right: 0; - } - - .cv-photo { - position: static; - width: auto; - height: auto; - max-width: 250px; - margin: 1.5rem auto; - text-align: center; - right: auto; - top: auto; - } - - .cv-photo img { - width: 100%; - height: auto; - max-height: none; - } - - /* ======================================== - UNIFIED LOGO/ICON SIZING - Consistent 60px - ======================================== */ - .company-logo, - .course-icon, - .project-icon, - .award-logo { - width: 60px !important; - height: 60px !important; - flex-shrink: 0; - } - - .company-logo img, - .course-icon img, - .project-icon img, - .award-logo img { - width: 60px !important; - height: 60px !important; - object-fit: contain; - } - - .default-company-icon, - .default-course-icon, - .default-project-icon, - .default-award-icon { - width: 60px !important; - height: 60px !important; - } - - /* ======================================== - CONSISTENT ITEM LAYOUT - Uniform spacing - ======================================== */ - .experience-item, - .course-item, - .project-item, - .award-item { - display: flex; - flex-direction: row; - gap: 1rem !important; - align-items: flex-start; - margin-bottom: 2rem !important; - padding-bottom: 1.5rem !important; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - } - - .experience-item { - margin-bottom: 1.8rem !important; - } - - .experience-content, - .course-content, - .project-content, - .award-content { - flex: 1; - min-width: 0; - } - - /* ======================================== - FONT SIZE CONSISTENCY - Titles and descriptions - ======================================== */ - .course-title, - .project-title, - .award-item strong { - font-size: 0.95rem !important; - line-height: 1.4; - } - - .course-item small, - .project-item small, - .award-item small { - font-size: 0.8rem !important; - } - - .course-desc, - .project-desc, - .award-desc { - font-size: 0.85rem !important; - line-height: 1.5; - } - - /* ======================================== - RESPONSIBILITIES MOBILE OPTIMIZATION - ======================================== */ - .responsibilities li:has(img), - .responsibilities li:has(iconify-icon) { - grid-template-columns: 60px 1fr !important; - gap: 0.75rem !important; - margin-bottom: 0.75rem !important; - } - - .responsibilities li img, - .responsibilities li iconify-icon.default-company-icon { - width: 60px !important; - height: 60px !important; - } - - /* ======================================== - SIDEBAR ITEMS MOBILE OPTIMIZATION - ======================================== */ - .language-item, - .reference-item, - .other-content { - margin-bottom: 0 !important; - line-height: 1.4 !important; - margin-left: 1rem !important; - font-size: 0.85rem !important; - } -} - -/* ======================================== - Responsive: All Mobile Screens (up to 540px) - ======================================== */ - -@media (max-width: 540px) { - /* Simplify action bar grid for mobile: single column */ - .action-bar-content { - grid-template-columns: 1fr; - gap: 0; - padding: 0; - } - - /* Hide center controls on mobile (moved to hamburger menu) */ - .view-controls-center { - display: none; - } - - /* Hide action buttons on small screens (available in hamburger menu) */ - .action-buttons-right { - display: none; - } - - /* Site title uses flexbox with percentage widths */ - .site-title { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - padding: 0 0.5rem; - gap: 0.5rem; - } - - /* Left group (hamburger + title) takes ~50-60% */ - .site-title-left { - display: flex; - align-items: center; - gap: 0.5rem; - flex: 1 1 1; - min-width: 0; - } - - /* Title link is flexible within left group */ - .site-title-link { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - } - - .site-title-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - /* Language selector takes ~30-35% */ - .language-selector { - display: flex; - flex: 0 0 35%; - margin-left: auto; - padding-left: 0; - margin-right: 0; - justify-content: flex-end; - gap: 0.25rem; - } - - /* Hide year from title in mobile view */ - .site-title-year { - display: none; - } - - /* Hide desktop logo, show mobile icon in title */ - .site-logo-link { - display: none; - } - - .site-icon-mobile { - display: inline-flex; - } - - /* ========== Language Selector - Show Short Names Only ========== */ - .language-selector .selector-btn { - position: relative; - padding: 0.4rem 0.75rem; - min-width: 40px; - font-size: 0; /* Hide actual text */ - overflow: visible; - transition: font-size 0.3s ease; - display: inline-flex; - justify-content: center; - align-items: center; - } - - /* Show only short version (EN/ES) */ - .language-selector .selector-btn::before { - content: attr(data-short); - font-size: 0.95rem; - opacity: 1; - transition: opacity 0.3s ease; - display: block; - width: 100%; - text-align: center; - } - - /* Keep short names on hover (no expansion) */ - .language-selector .selector-btn:hover { - font-size: 0; - min-width: 40px; - } - - .language-selector .selector-btn:hover::before { - content: attr(data-short); - opacity: 1; - } -} - -/* ========================================================================== - ZOOM CONTROL - Fixed Bottom Center - ========================================================================== */ - -.zoom-control { - position: fixed; - bottom: 100px; /* Default position, can be dragged */ - left: 50%; - transform: translateX(-50%); - z-index: 900; - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.65rem 1.25rem; - background: rgba(128, 128, 128, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-radius: 50px; - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); - transition: all 0.3s ease; - opacity: 0.7; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; - cursor: move; /* Indicate draggability */ - user-select: none; /* Prevent text selection while dragging */ -} - -/* Zoom control highlight when hovering zoom toggle button */ -.zoom-control.zoom-highlight { - opacity: 1; - box-shadow: 0px 0px 10px 4px rgb(1 113 188 / 80%); - background: rgb(91 91 91); -} - -/* Hidden state for zoom control and show button */ -.zoom-hidden { - display: none !important; -} - -/* Close button for zoom control */ -.zoom-close-btn { - position: absolute; - top: -8px; - right: -8px; - width: 24px; - height: 24px; - background: rgba(128, 128, 128, 0.6); /* Subtle grey */ - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - color: rgba(255, 255, 255, 0.8); /* Light grey icon */ - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - transition: all 0.2s ease; - z-index: 1; - opacity: 0.7; /* Subtle by default */ -} - -.zoom-close-btn:hover { - background: rgba(220, 53, 69, 0.9); /* Red on hover */ - color: white; /* White icon on hover */ - opacity: 1; /* Fully visible on hover */ - transform: scale(1.1); - box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4); -} - -.zoom-control:hover { - opacity: 1; - background: rgb(91 91 91); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); -} - -/* Zoom Values (Labels) */ -.zoom-value { - color: rgba(255, 255, 255, 1); /* 100% white */ - font-size: 0.95rem; /* Bigger */ - font-weight: 500; /* Medium weight */ - min-width: 30px; - text-align: center; -} - -.zoom-value-current { - color: rgba(255, 255, 255, 1); /* 100% white */ - font-weight: 600; - font-size: 1.05rem; /* Bigger */ - min-width: 35px; -} - -/* Range Slider Styling */ -.zoom-slider { - -webkit-appearance: none; - appearance: none; - width: 180px; - height: 5px; - border-radius: 3px; - background: rgba(200, 200, 200, 0.5); - outline: none; - cursor: pointer; - transition: all 0.3s ease; -} - -/* Solid blue color on hover - no gradient */ -.zoom-control:hover .zoom-slider, -.zoom-slider:hover { - background: rgba(145, 190, 236, 1); /* solid blue */ -} - -.zoom-slider:focus { - outline: 2px solid rgba(255, 255, 255, 0.6); - outline-offset: 2px; -} - -/* Webkit Slider Thumb */ -.zoom-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: white; - border: 2px solid rgba(180, 180, 180, 0.8); - cursor: pointer; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease; -} - -.zoom-slider::-webkit-slider-thumb:hover { - transform: scale(1.1); - border-color: rgba(200, 200, 200, 1); - box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); -} - -.zoom-slider::-webkit-slider-thumb:active { - transform: scale(1.05); -} - -/* Firefox Slider Thumb */ -.zoom-slider::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: white; - border: 2px solid rgba(180, 180, 180, 0.8); - cursor: pointer; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease; -} - -.zoom-slider::-moz-range-thumb:hover { - transform: scale(1.1); - border-color: rgba(200, 200, 200, 1); - box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); -} - -.zoom-slider::-moz-range-thumb:active { - transform: scale(1.05); -} - -/* Firefox Range Track */ -.zoom-slider::-moz-range-track { - height: 5px; - border-radius: 3px; - background: rgba(200, 200, 200, 0.5); - transition: all 0.3s ease; -} - -/* Firefox Range Track - Solid blue on hover */ -.zoom-control:hover .zoom-slider::-moz-range-track, -.zoom-slider:hover::-moz-range-track { - background: #3b82f6; /* solid blue */ -} - -/* Reset Button - Circular with dynamic value inside */ -.zoom-reset-btn { - display: flex; - align-items: center; - justify-content: center; - min-width: 44px; - min-height: 44px; - padding: 0.5rem; - background: rgba(200, 200, 200, 0.2); - border: 2px solid rgba(220, 220, 220, 0.3); - border-radius: 50%; - color: rgba(255, 255, 255, 0.8); - font-size: 0.85rem; - font-weight: 700; - cursor: pointer; - transition: all 0.3s ease; - flex-shrink: 0; - margin: 0 -5px 0 10px; -} - -.zoom-reset-btn #zoom-value-current { - color: inherit; - font-size: inherit; - font-weight: inherit; - min-width: auto; -} - -.zoom-reset-btn:hover { - background: rgba(220, 220, 220, 0.4); - border-color: rgba(240, 240, 240, 0.6); - color: white; -} - -/* Green hover only when zoom is not at default (100) */ -.zoom-reset-btn.zoom-not-default:hover { - background: #74aacd; - border-color: #74aacd; - color: white; -} - -.zoom-reset-btn:active { - transform: scale(0.95); -} - -.zoom-reset-btn:focus { - outline: 2px solid rgba(255, 255, 255, 0.6); - outline-offset: 2px; -} - -/* Mobile Responsive - Horizontal button layout defined below (around line 2867) */ -/* Old mobile styles removed - now using horizontal layout with all three buttons */ - -/* Very Small Screens - Ultra Compact */ -@media (max-width: 480px) { - .zoom-control { - bottom: 40px; - padding: 0.35rem 0.7rem; - gap: 0.35rem; - } - - .zoom-slider { - width: 100px; - } - - /* Hide min/max labels on very small screens */ - .zoom-value-min, - .zoom-value-max { - display: none; - } -} - -/* ============================================================================= - HTMX CSS TRANSITIONS - ============================================================================= */ - -/* Inline loading transition styles moved to main section above (~line 1677) */ -/* Prevent layout shift during content fade */ -.cv-page-content-wrapper { - position: relative; -} - -/* ============================================================================= - KEYBOARD SHORTCUTS BUTTON & MODAL - ============================================================================= */ - -/* Shortcuts Button (Fixed Left) - Mirrors info-button on opposite side */ -/* Zoom Toggle Button (above shortcuts button) */ -.zoom-toggle-btn { - position: fixed; - bottom: 10rem; /* Above shortcuts button */ - left: 2rem; - width: 50px; - height: 50px; - background: var(--black-bar); - color: white; /* Match other buttons when inactive */ - 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; /* Match shortcuts button opacity */ -} - -.zoom-toggle-btn:hover { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #3498db; /* Blue hover */ -} - -.zoom-toggle-btn.at-bottom { - opacity: 1; - background: #3498db; /* Blue - matches hover */ -} - -/* No special styling for active state - button looks same whether zoom is on or off */ - -.shortcuts-btn { - position: fixed; - bottom: 6rem; /* Above back-to-top button (2rem + 50px + gap) */ - left: 2rem; /* LEFT SIDE instead of right */ - width: 50px; - height: 50px; - background: var(--black-bar); - 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); - z-index: 99; - transition: all 0.3s ease; - opacity: 0.6; /* Increased from 0.2 for better discoverability */ -} - -.shortcuts-btn:hover { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #f39c12; /* Orange hover */ -} - -.shortcuts-btn.at-bottom { - opacity: 1; - background: #f39c12; /* Orange when at bottom */ -} - -.shortcuts-btn:active { - transform: translateY(-1px); -} - -/* Print-Friendly Button (second from top) */ -.print-friendly-btn { - position: fixed; - bottom: 18rem; /* Below download button (22rem) */ - left: 2rem; - width: 50px; - height: 50px; - background: var(--black-bar); /* Dark background by default */ - color: white; /* White icon by default */ - 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; -} - -.print-friendly-btn iconify-icon { - color: white; /* White icon by default */ -} - -.print-friendly-btn:hover, -.print-friendly-btn.print-hover-sync { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: white !important; /* White background on hover */ - color: #27ae60; /* Green icon on hover */ -} - -.print-friendly-btn:hover iconify-icon, -.print-friendly-btn.print-hover-sync iconify-icon { - color: #27ae60; /* Green icon on hover */ -} - -.print-friendly-btn.at-bottom { - opacity: 1; - background: white !important; /* White background - matches hover */ - color: #27ae60; /* Green - matches hover */ -} - -.print-friendly-btn.at-bottom iconify-icon { - color: #27ae60; /* Green icon when at bottom */ -} - -/* Download Button (TOP POSITION) */ -.download-btn { - position: fixed; - bottom: 22rem; /* Top button position */ - left: 2rem; - width: 50px; - height: 50px; - background: var(--black-bar); - 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; -} - -.download-btn { - background: var(--black-bar); /* Gray by default like other buttons */ - opacity: 0.6; /* Match other buttons */ -} - -.download-btn:hover, -.download-btn.pdf-hover-sync { - opacity: 1; - transform: translateY(-3px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #cd6060 !important; /* PDF red on hover */ -} - -.download-btn iconify-icon { - filter: brightness(0) invert(1); /* Always white */ - transition: filter 0.3s ease; -} - -.download-btn:hover iconify-icon { - filter: brightness(0) invert(1); /* Keep white on hover */ -} - -.download-btn.at-bottom { - opacity: 1; - 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; - } -} - -/* Shortcuts Modal - Very wide for 3-column grid, less tall */ -#shortcuts-modal { - max-width: 900px; /* Much wider - was 750px */ - max-height: 80vh; /* Limit height */ -} - -/* Keyboard icon with green curly brackets (matching info modal style) */ -.keyboard-icon-wrapper { - position: relative; - display: inline-flex; - align-items: center; - padding: 0 22px; -} - -.keyboard-icon-wrapper::before { - content: '{'; - position: absolute; - left: 2px; - font-size: 2rem; - font-weight: 700; - color: #575757ff; /* Dark brackets - matching info modal */ - line-height: 1; - top: -3px; -} - -.keyboard-icon-wrapper::after { - content: '}'; - position: absolute; - right: 2px; - font-size: 2rem; - font-weight: 700; - color: #575757ff; /* Dark brackets - matching info modal */ - line-height: 1; - top: -3px; -} - -.keyboard-icon-wrapper iconify-icon { - color: #f39c12; - position: relative; - top: 1px; -} - -/* Add margin-bottom to subtitle */ -#shortcuts-modal .info-modal-cv-title { - margin-bottom: 0.5rem; -} - -#shortcuts-modal .info-modal-body { - display: grid; - grid-template-columns: 1fr 1fr; /* 2 columns for 5 sections (3+2) */ - gap: 1.2rem 1.5rem; /* row gap, column gap */ - margin-top: 1.5rem; /* Increased spacing since no description */ -} - -/* Shortcuts Modal Content - Extends info-modal styles */ -.shortcuts-section { - margin-top: 0; /* Grid handles spacing */ - background: #f8f9fa; - border: 1px solid #e1e4e8; - border-radius: 8px; - padding: 1rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); -} - -.shortcuts-section:first-of-type { - margin-top: 0; -} - -.shortcuts-section-title { - font-size: 1.05rem; - font-weight: 600; - color: #827a6e; /* Brownish-gray for section header text */ - margin-bottom: 0.75rem; - display: flex; - align-items: center; - gap: 0.5rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid rgba(130, 122, 110, 0.2); /* Matching border */ -} - -.shortcuts-section-title iconify-icon { - color: #f39c12; /* ORANGE icons for section headers */ -} - -.shortcuts-list { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.shortcut-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.5rem 0; -} - -.shortcut-keys { - display: flex; - align-items: center; - gap: 0.4rem; - flex-wrap: wrap; -} - -.shortcut-keys kbd { - font-family: 'Monaco', 'Courier New', monospace; - font-size: 0.75rem; - font-weight: 600; - background: rgba(52, 152, 219, 0.08); /* Light blue background */ - border: 1px solid rgba(52, 152, 219, 0.35); /* Blue border */ - border-radius: 6px; - padding: 0.3rem 0.6rem; - box-shadow: 0 2px 4px rgba(52, 152, 219, 0.12), inset 0 -1px 0 rgba(52, 152, 219, 0.25); - white-space: nowrap; - text-align: center; - color: #3498db; /* Blue text */ - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.2rem; - transition: all 0.2s ease; - min-width: 2rem; -} - -/* Iconify icons inside kbd elements */ -.shortcut-keys kbd iconify-icon { - color: inherit; - vertical-align: middle; - display: inline-flex; -} - -.shortcut-item:hover .shortcut-keys kbd { - background: rgba(52, 152, 219, 0.15); - border-color: rgba(52, 152, 219, 0.5); - box-shadow: 0 2px 6px rgba(52, 152, 219, 0.25); -} - -.shortcut-desc { - flex: 1; - font-size: 0.95rem; - color: var(--text-gray); - line-height: 1.4; -} - -/* Mobile responsive */ -@media (max-width: 768px) { - #shortcuts-modal { - max-width: calc(100% - 2rem); - } - - #shortcuts-modal .info-modal-body { - grid-template-columns: 1fr; /* Single column on mobile */ - gap: 1.5rem; - } -} - -/* Tablet - 2 columns */ -@media (min-width: 769px) and (max-width: 1024px) { - #shortcuts-modal { - max-width: 700px; - } - - #shortcuts-modal .info-modal-body { - grid-template-columns: 1fr 1fr; /* 2 columns on tablet */ - gap: 1.2rem 1.5rem; - } - - .shortcuts-section-title { - font-size: 1rem; - } - - .shortcut-item { - flex-direction: column; - align-items: flex-start; - gap: 0.35rem; - } - - .shortcut-keys kbd { - font-size: 0.7rem; - padding: 0.2rem 0.4rem; - } - - .shortcut-desc { - font-size: 0.9rem; - } -} - -/* ======================================================================== - PDF DOWNLOAD MODAL STYLES - ======================================================================== */ - -/* PDF Modal Specific Overrides */ -.pdf-download-modal { - max-width: 900px; - width: calc(100% - 2rem); -} - -/* Modal Subtitle */ -.pdf-modal-subtitle { - font-size: 0.95rem; - color: var(--text-gray); - margin-top: 0.5rem; - font-weight: 400; -} - -/* PDF Options Grid */ -.pdf-options-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; - margin: 2rem 0 1.5rem 0; -} - -/* PDF Option Card */ -.pdf-option-card { - border: 2px solid transparent; - border-radius: 12px; - padding: 16px; - cursor: pointer; - transition: all 250ms ease; - position: relative; - background: #ffffff; - display: flex; - flex-direction: column; - gap: 12px; -} - -.pdf-option-card:hover { - border-color: #e0e0e0; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); -} - -.pdf-option-card:focus { - outline: 2px solid #4caf50; - outline-offset: 2px; -} - -/* Selected State */ -.pdf-option-card.selected { - border-color: #4caf50; - box-shadow: 0 6px 16px rgba(76, 175, 80, 0.2); - background: #f9fff9; -} - -/* PDF Thumbnail Container */ -.pdf-thumbnail { - background: #ffffff; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 16px; - height: 280px; - display: flex; - flex-direction: column; - gap: 12px; - position: relative; - overflow: hidden; -} - -/* Skeleton Blocks inside Thumbnails */ -.pdf-thumbnail .skeleton-block { - background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.8s ease-in-out infinite; - border-radius: 4px; -} - -/* Custom Placeholder (for Custom CV card) */ -.custom-placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: #999; - text-align: center; -} - -.custom-placeholder iconify-icon { - margin-bottom: 12px; - opacity: 0.5; -} - -.custom-placeholder p { - font-size: 0.9rem; - font-weight: 500; - color: #666; - margin: 0; -} - -/* Thumbnail Badge (Page Count / Coming Soon) */ -.thumbnail-badge { - position: absolute; - top: 8px; - right: 8px; - background: rgba(0, 0, 0, 0.75); - color: white; - font-size: 0.7rem; - font-weight: 600; - padding: 4px 8px; - border-radius: 4px; - letter-spacing: 0.5px; - text-transform: uppercase; -} - -/* PDF Option Info */ -.pdf-option-info { - text-align: center; -} - -.pdf-option-info h3 { - font-size: 1.1rem; - font-weight: 600; - color: var(--text-dark); - margin: 0 0 4px 0; -} - -.pdf-option-info p { - font-size: 0.875rem; - color: var(--text-gray); - margin: 0; - line-height: 1.4; -} - -/* PDF Option Badge (Checkmark) */ -.pdf-option-badge { - position: absolute; - top: 8px; - left: 8px; - opacity: 0; - transform: scale(0.8); - transition: all 250ms ease; - color: #4caf50; -} - -.pdf-option-card.selected .pdf-option-badge { - opacity: 1; - transform: scale(1); -} - -/* PDF Modal Footer */ -.pdf-modal-footer { - display: flex; - justify-content: center; - padding-top: 1rem; - border-top: 1px solid #e0e0e0; - margin-top: 0.5rem; -} - -/* PDF Download Button */ -.pdf-download-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 32px; - font-size: 1rem; - font-weight: 600; - border: none; - border-radius: 8px; - cursor: pointer; - transition: all 250ms ease; - font-family: inherit; -} - -.pdf-download-btn iconify-icon { - flex-shrink: 0; -} - -/* Disabled State */ -.pdf-download-btn:disabled { - background: #e0e0e0; - color: #999999; - cursor: not-allowed; - opacity: 0.6; -} - -/* Enabled State */ -.pdf-download-btn:not(:disabled) { - background: #4caf50; - color: white; -} - -.pdf-download-btn:not(:disabled):hover { - background: #45a049; - box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); - transform: translateY(-1px); -} - -.pdf-download-btn:not(:disabled):active { - transform: translateY(0); -} - -/* Screen Reader Only */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -/* ======================================================================== - RESPONSIVE DESIGN - PDF MODAL - ======================================================================== */ - -/* Tablet: Two columns */ -@media (min-width: 480px) and (max-width: 767px) { - .pdf-options-grid { - grid-template-columns: repeat(2, 1fr); - gap: 16px; - } - - /* Custom card spans full width */ - .pdf-option-card[data-cv-format="custom"] { - grid-column: 1 / -1; - } - - .pdf-thumbnail { - height: 220px; - } -} - -/* Mobile: Single column */ -@media (max-width: 479px) { - .pdf-download-modal { - max-width: calc(100% - 1rem); - } - - .pdf-options-grid { - display: flex; - flex-direction: column; - gap: 16px; - } - - .pdf-thumbnail { - height: 200px; - } - - .pdf-option-info h3 { - font-size: 1rem; - } - - .pdf-option-info p { - font-size: 0.8rem; - } - - .pdf-download-btn { - padding: 10px 24px; - font-size: 0.9rem; - } -} - -/* ======================================================================== - ACCESSIBILITY - REDUCED MOTION - ======================================================================== */ - -@media (prefers-reduced-motion: reduce) { - .pdf-thumbnail .skeleton-block { - animation: none; - background: #e8e8e8; - } - - .pdf-option-card { - transition: none; - } - - .pdf-option-badge { - transition: none; - } - - .pdf-download-btn { - transition: none; - } -} - -/* ======================================================================== - PRINT STYLES - PDF MODAL - ======================================================================== */ - -@media print { - .pdf-download-modal { - display: none !important; - } -} diff --git a/static/js/main.js.backup b/static/js/main.js.backup deleted file mode 100644 index 634d214..0000000 --- a/static/js/main.js.backup +++ /dev/null @@ -1,954 +0,0 @@ -// CV Interactive Features - CSP-Compliant External JavaScript -// Extracted from inline scripts for security hardening -(function() { - 'use strict'; - - // ============================================================================= - // NAVIGATION & MENU SYSTEM - // ============================================================================= - - // Hover-based menu control - function initMenuSystem() { - const hamburgerBtn = document.querySelector('.hamburger-btn'); - const menu = document.getElementById('navigation-menu'); - - if (!hamburgerBtn || !menu) return; - - // Show menu on hamburger hover - hamburgerBtn.addEventListener('mouseenter', function() { - menu.classList.add('menu-hover'); - hamburgerBtn.setAttribute('aria-expanded', 'true'); - }); - - // Hide menu when leaving hamburger (only if not hovering menu) - hamburgerBtn.addEventListener('mouseleave', function() { - setTimeout(() => { - if (!menu.matches(':hover')) { - menu.classList.remove('menu-hover'); - hamburgerBtn.setAttribute('aria-expanded', 'false'); - } - }, 100); - }); - - // Hide menu when leaving menu itself - menu.addEventListener('mouseleave', function() { - menu.classList.remove('menu-hover'); - hamburgerBtn.setAttribute('aria-expanded', 'false'); - }); - - // Position submenu dynamically - const submenuTrigger = document.querySelector('.menu-item-submenu'); - const submenuContent = document.querySelector('.submenu-content'); - - if (submenuTrigger && submenuContent) { - submenuTrigger.addEventListener('mouseenter', function() { - const triggerRect = submenuTrigger.getBoundingClientRect(); - submenuContent.style.top = `${triggerRect.top}px`; - }); - } - } - - // Legacy toggle function - kept for compatibility - window.toggleMenu = function() { - const menu = document.getElementById('navigation-menu'); - const btn = document.querySelector('.hamburger-btn'); - - if (menu.classList.contains('menu-open')) { - menu.classList.remove('menu-open'); - btn.setAttribute('aria-expanded', 'false'); - } else { - menu.classList.add('menu-open'); - btn.setAttribute('aria-expanded', 'true'); - } - }; - - // Flag to keep header visible after navigation - let keepHeaderVisible = false; - - // Toggle sidebar accordion (mobile only) - window.toggleSidebar = function(header) { - const content = header.nextElementSibling; - const isActive = header.classList.contains('active'); - - if (isActive) { - // Close - header.classList.remove('active'); - content.classList.remove('active'); - } else { - // Open - header.classList.add('active'); - content.classList.add('active'); - } - }; - - // Expand all sections - window.expandAllSections = function(event) { - event.preventDefault(); - const allDetails = document.querySelectorAll('details'); - allDetails.forEach(detail => { - detail.setAttribute('open', ''); - }); - }; - - // Collapse all sections - window.collapseAllSections = function(event) { - event.preventDefault(); - const allDetails = document.querySelectorAll('details'); - allDetails.forEach(detail => { - detail.removeAttribute('open'); - }); - }; - - // Toggle submenu - no longer needed for hover, but kept for compatibility - window.toggleSubmenu = function(event) { - event.preventDefault(); - const submenuContainer = event.currentTarget.parentElement; - submenuContainer.classList.toggle('submenu-open'); - }; - - // Scroll to section smoothly - window.scrollToSection = function(sectionId) { - event.preventDefault(); // Prevent default anchor behavior - - const section = document.getElementById(sectionId); - if (section) { - // Ensure header is visible before scrolling - const actionBar = document.querySelector('.action-bar'); - const navMenu = document.querySelector('.navigation-menu'); - actionBar.classList.remove('header-hidden'); - navMenu.classList.remove('header-hidden'); - - // Set flag to keep header visible - keepHeaderVisible = true; - - // Close menu after clicking - navMenu.classList.remove('menu-open'); - document.querySelector('.hamburger-btn').setAttribute('aria-expanded', 'false'); - - // Wait a bit for header to be visible, then calculate offset - setTimeout(() => { - const actionBarHeight = actionBar.offsetHeight; - const offset = actionBarHeight + 20; // Add 20px padding - - const elementPosition = section.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.pageYOffset - offset; - - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth' - }); - }, 100); - } - }; - - // Close menu when clicking outside (only for legacy click-opened menus) - function initClickOutsideHandler() { - document.addEventListener('click', function(event) { - const menu = document.getElementById('navigation-menu'); - const btn = document.querySelector('.hamburger-btn'); - - if (menu && btn && menu.classList.contains('menu-open')) { - if (!menu.contains(event.target) && !btn.contains(event.target)) { - menu.classList.remove('menu-open'); - btn.setAttribute('aria-expanded', 'false'); - } - } - }); - } - - // ============================================================================= - // LANGUAGE & PREFERENCES - // ============================================================================= - - // Track if URL originally had lang parameter - const urlHadLangParam = new URLSearchParams(window.location.search).has('lang'); - - window.selectLanguage = function(lang) { - // Save language preference to localStorage - localStorage.setItem('cv-language', lang); - - // Reload page with new language parameter - const url = new URL(window.location); - url.searchParams.set('lang', lang); - window.location.href = url.toString(); - }; - - window.toggleCVLength = function() { - const headerToggle = document.getElementById('lengthToggle'); - const menuToggle = document.getElementById('lengthToggleMenu'); - const paper = document.querySelector('.cv-paper'); - - // Get the state from whichever toggle was clicked - const isChecked = event?.target?.id === 'lengthToggleMenu' ? menuToggle?.checked : headerToggle?.checked; - - // Sync both toggles - if (headerToggle) headerToggle.checked = isChecked; - if (menuToggle) menuToggle.checked = isChecked; - - // Save current scroll position - const currentScrollY = window.scrollY || window.pageYOffset; - - if (isChecked) { - paper.classList.add('cv-long'); - paper.classList.remove('cv-short'); - localStorage.setItem('cv-length', 'long'); - } else { - paper.classList.add('cv-short'); - paper.classList.remove('cv-long'); - localStorage.setItem('cv-length', 'short'); - } - - // Restore scroll position after DOM updates - requestAnimationFrame(() => { - window.scrollTo(0, currentScrollY); - }); - }; - - window.toggleLogos = function() { - const headerToggle = document.getElementById('logoToggle'); - const menuToggle = document.getElementById('logoToggleMenu'); - const paper = document.querySelector('.cv-paper'); - - // Get the state from whichever toggle was clicked - const isChecked = event?.target?.id === 'logoToggleMenu' ? menuToggle?.checked : headerToggle?.checked; - - // Sync both toggles - if (headerToggle) headerToggle.checked = isChecked; - if (menuToggle) menuToggle.checked = isChecked; - - // Save current scroll position - const currentScrollY = window.scrollY || window.pageYOffset; - - if (isChecked) { - paper.classList.add('show-logos'); - localStorage.setItem('cv-logos', 'show'); - } else { - paper.classList.remove('show-logos'); - localStorage.setItem('cv-logos', 'hide'); - } - - // Restore scroll position after DOM updates - requestAnimationFrame(() => { - window.scrollTo(0, currentScrollY); - }); - }; - - window.toggleTheme = function() { - const headerToggle = document.getElementById('themeToggle'); - const menuToggle = document.getElementById('themeToggleMenu'); - const container = document.querySelector('.cv-container'); - - // Get the state from whichever toggle was clicked - const isChecked = event?.target?.id === 'themeToggleMenu' ? menuToggle?.checked : headerToggle?.checked; - - // Sync both toggles - if (headerToggle) headerToggle.checked = isChecked; - if (menuToggle) menuToggle.checked = isChecked; - - if (isChecked) { - container.classList.add('theme-clean'); - localStorage.setItem('cv-theme', 'clean'); - } else { - container.classList.remove('theme-clean'); - localStorage.setItem('cv-theme', 'default'); - } - }; - - // ============================================================================= - // ZOOM CONTROL - // ============================================================================= - - /** - * Check if we're on mobile viewport - * @returns {boolean} True if mobile (viewport <= 768px) - */ - function isMobileView() { - return window.innerWidth <= 768; - } - - /** - * Initialize zoom control on page load - * Restores saved zoom level from localStorage (desktop only) - */ - function initZoomControl() { - const slider = document.getElementById('zoom-slider'); - const resetBtn = document.getElementById('zoom-reset'); - const zoomWrapper = document.getElementById('zoom-wrapper'); - - if (!slider || !zoomWrapper) return; - - // On mobile, always use 100% zoom (zoom control is hidden anyway) - if (isMobileView()) { - slider.value = 100; - applyZoom(100, false); - return; // Skip event listeners on mobile - } - - // Desktop: Restore saved zoom level from localStorage - const savedZoom = localStorage.getItem('cv-zoom'); - if (savedZoom) { - const zoomValue = parseInt(savedZoom, 10); - slider.value = zoomValue; - applyZoom(zoomValue, false); // false = don't save (already loaded from storage) - } - - // Real-time slider updates - immediate, smooth analog experience - slider.addEventListener('input', function(e) { - const zoomValue = parseInt(e.target.value, 10); - - // Apply zoom and update display immediately for smooth analog feel - updateZoomDisplay(zoomValue); - applyZoom(zoomValue, true); - }); - - // Reset button - if (resetBtn) { - resetBtn.addEventListener('click', function() { - slider.value = 100; - applyZoom(100, true); - slider.focus(); // Return focus to slider for accessibility - }); - } - - // Keyboard shortcuts (Ctrl/Cmd + Plus/Minus/0) - document.addEventListener('keydown', function(e) { - if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { - if (e.key === '=' || e.key === '+') { - e.preventDefault(); - incrementZoom(10); - } else if (e.key === '-') { - e.preventDefault(); - incrementZoom(-10); - } else if (e.key === '0') { - e.preventDefault(); - slider.value = 100; - applyZoom(100, true); - } - } - }); - - // Handle window resize - reset zoom when switching to mobile - let resizeTimeout; - window.addEventListener('resize', function() { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function() { - if (isMobileView()) { - // Reset to 100% zoom when switching to mobile - slider.value = 100; - applyZoom(100, false); - } - }, 250); // Debounce resize events - }); - } - - /** - * Apply zoom transformation to CV paper - * @param {number} zoomValue - Zoom percentage (25-175, centered at 100) - * @param {boolean} saveToStorage - Whether to persist to localStorage - */ - function applyZoom(zoomValue, saveToStorage = true) { - const zoomWrapper = document.getElementById('zoom-wrapper'); - if (!zoomWrapper) return; - - // Convert percentage to decimal (100 = 1.0, 50 = 0.5, etc.) - const zoomLevel = zoomValue / 100; - - requestAnimationFrame(() => { - // Use CSS zoom property - it properly affects layout and extends beyond viewport - zoomWrapper.style.zoom = zoomLevel; - - // When zoom > 100%, allow the wrapper to expand beyond viewport width - // Set width to accommodate the expanded content without bounds - if (zoomLevel > 1) { - // Set width to auto to allow natural expansion - zoomWrapper.style.width = 'auto'; - zoomWrapper.style.minWidth = '100%'; - // Remove max-width constraint to allow horizontal expansion - zoomWrapper.style.maxWidth = 'none'; - } else { - // Reset to default when zoom <= 100% - zoomWrapper.style.width = ''; - zoomWrapper.style.minWidth = ''; - zoomWrapper.style.maxWidth = ''; - } - - // Reset zoom on fixed buttons so they stay same size - const backToTopBtn = document.getElementById('back-to-top'); - const infoBtn = document.getElementById('info-button'); - const inverseZoom = 1 / zoomLevel; - - if (backToTopBtn) backToTopBtn.style.zoom = inverseZoom; - if (infoBtn) infoBtn.style.zoom = inverseZoom; - - // Update display - updateZoomDisplay(zoomValue); - - // Save to localStorage - if (saveToStorage) { - localStorage.setItem('cv-zoom', zoomValue.toString()); - } - - // Update zoom control position for horizontal scroll - updateZoomControlPosition(); - }); - } - - /** - * Update visual display and ARIA attributes - * @param {number} zoomValue - Current zoom percentage - */ - function updateZoomDisplay(zoomValue) { - const slider = document.getElementById('zoom-slider'); - const display = document.getElementById('zoom-value-current'); - const resetBtn = document.getElementById('zoom-reset'); - - if (display) { - display.textContent = zoomValue; - } - - if (slider) { - slider.setAttribute('aria-valuenow', zoomValue); - slider.setAttribute('aria-valuetext', `${zoomValue}%`); - } - - // Add/remove class to enable green hover only when zoom is not 100 - if (resetBtn) { - if (zoomValue !== 100) { - resetBtn.classList.add('zoom-not-default'); - } else { - resetBtn.classList.remove('zoom-not-default'); - } - } - } - - /** - * Increment/decrement zoom by step amount - * @param {number} step - Amount to change (positive or negative) - */ - function incrementZoom(step) { - const slider = document.getElementById('zoom-slider'); - if (!slider) return; - - const currentZoom = parseInt(slider.value, 10); - const newZoom = Math.min(175, Math.max(25, currentZoom + step)); - - slider.value = newZoom; - applyZoom(newZoom, true); - } - - /** - * Update zoom control position based on horizontal scroll - * This keeps the zoom control centered relative to the visible viewport - */ - function updateZoomControlPosition() { - const zoomControl = document.getElementById('zoom-control'); - if (!zoomControl || isMobileView()) return; - - // Only adjust if zoom control is in default centered position - // (not dragged to a custom position) - const savedPosition = localStorage.getItem('cv-zoom-position'); - if (savedPosition) return; // Don't adjust if user has dragged it - - // Get current horizontal scroll position - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - - // Update left position to account for horizontal scroll - if (scrollLeft > 0) { - // Adjust position to stay centered in viewport during horizontal scroll - zoomControl.style.left = `calc(50% + ${scrollLeft}px)`; - } else { - // Reset to center when scroll is at start - zoomControl.style.left = '50%'; - } - } - - /** - * Make zoom control draggable and persist position - */ - function initZoomDragging() { - const zoomControl = document.getElementById('zoom-control'); - if (!zoomControl || isMobileView()) return; - - let isDragging = false; - let currentX, currentY, initialX, initialY; - - // Restore saved position from localStorage - const savedPosition = localStorage.getItem('cv-zoom-position'); - if (savedPosition) { - const { bottom, left } = JSON.parse(savedPosition); - zoomControl.style.bottom = bottom; - zoomControl.style.left = left; - zoomControl.style.transform = 'none'; // Remove centering transform when positioned - } - - // Start drag on mousedown (but not on slider, close button, or reset button) - zoomControl.addEventListener('mousedown', function(e) { - // Ignore if clicking on interactive elements - if (e.target.closest('.zoom-slider, .zoom-close-btn, .zoom-reset-btn')) { - return; - } - - isDragging = true; - zoomControl.style.transition = 'none'; // Disable transitions during drag - - // Get current position - const rect = zoomControl.getBoundingClientRect(); - initialX = e.clientX - rect.left; - initialY = e.clientY - rect.top; - - e.preventDefault(); - }); - - // Drag on mousemove - document.addEventListener('mousemove', function(e) { - if (!isDragging) return; - - e.preventDefault(); - - currentX = e.clientX - initialX; - currentY = e.clientY - initialY; - - // Keep within viewport bounds - const maxX = window.innerWidth - zoomControl.offsetWidth; - const maxY = window.innerHeight - zoomControl.offsetHeight; - - currentX = Math.max(0, Math.min(currentX, maxX)); - currentY = Math.max(0, Math.min(currentY, maxY)); - - // Update position - zoomControl.style.left = currentX + 'px'; - zoomControl.style.bottom = (window.innerHeight - currentY - zoomControl.offsetHeight) + 'px'; - zoomControl.style.transform = 'none'; // Remove centering transform - }); - - // End drag on mouseup - document.addEventListener('mouseup', function() { - if (isDragging) { - isDragging = false; - zoomControl.style.transition = 'all 0.3s ease'; // Re-enable transitions - - // Save position to localStorage - const position = { - bottom: zoomControl.style.bottom, - left: zoomControl.style.left - }; - localStorage.setItem('cv-zoom-position', JSON.stringify(position)); - } - }); - } - - /** - * Hide zoom control and show menu button - */ - function hideZoomControl() { - const zoomControl = document.getElementById('zoom-control'); - const showButton = document.getElementById('show-zoom-menu-btn'); - - if (zoomControl) { - zoomControl.style.display = 'none'; - localStorage.setItem('cv-zoom-visible', 'false'); - } - - if (showButton) { - showButton.style.display = 'block'; - } - } - - /** - * Show zoom control and hide menu button (global function for onclick) - */ - window.showZoomControl = function(event) { - if (event) event.preventDefault(); // Prevent default link behavior - - const zoomControl = document.getElementById('zoom-control'); - const showButton = document.getElementById('show-zoom-menu-btn'); - - if (zoomControl) { - zoomControl.style.display = 'flex'; - localStorage.setItem('cv-zoom-visible', 'true'); - } - - if (showButton) { - showButton.style.display = 'none'; - } - }; - - /** - * Initialize zoom visibility state from localStorage - */ - function initZoomVisibility() { - if (isMobileView()) return; // Always hidden on mobile - - const zoomControl = document.getElementById('zoom-control'); - const showButton = document.getElementById('show-zoom-menu-btn'); - const isVisible = localStorage.getItem('cv-zoom-visible'); - - // Default to visible if not set - if (isVisible === 'false') { - if (zoomControl) zoomControl.style.display = 'none'; - if (showButton) showButton.style.display = 'block'; - } else { - if (zoomControl) zoomControl.style.display = 'flex'; - if (showButton) showButton.style.display = 'none'; - } - - // Setup close button - const closeBtn = document.getElementById('zoom-close'); - if (closeBtn) { - closeBtn.addEventListener('click', function(e) { - e.stopPropagation(); // Prevent drag from starting - hideZoomControl(); - }); - } - } - - // ============================================================================= - // PRINT & PDF - // ============================================================================= - - // Print Friendly - Apply Clean Theme + Short Version for minimal printing - window.printFriendly = function() { - const container = document.querySelector('.cv-container'); - const paper = document.querySelector('.cv-paper'); - const wasClean = container.classList.contains('theme-clean'); - const wasLong = paper.classList.contains('cv-long'); - - // Store current zoom - const currentZoom = localStorage.getItem('cv-zoom') || '100'; - - // Apply clean theme for minimal print (no sidebars, no header, no icons) - if (!wasClean) { - container.classList.add('theme-clean'); - } - - // Force SHORT version for print (hide detailed content) - paper.classList.remove('cv-long'); - paper.classList.add('cv-short'); - - // Temporarily reset zoom for printing - if (paper) { - paper.style.transform = 'scale(1)'; - } - - // Small delay to let CSS apply - setTimeout(() => { - window.print(); - - // Restore original theme and length after print dialog closes - setTimeout(() => { - if (!wasClean) { - container.classList.remove('theme-clean'); - } - // Restore original length - if (wasLong) { - paper.classList.remove('cv-short'); - paper.classList.add('cv-long'); - } - // Restore zoom - if (paper && currentZoom !== '100') { - applyZoom(parseInt(currentZoom, 10), false); - } - }, 100); - }, 50); - }; - - // ============================================================================= - // INITIALIZATION & PREFERENCES - // ============================================================================= - - function initPreferences() { - const paper = document.querySelector('.cv-paper'); - - // Handle language preference - const urlLang = new URLSearchParams(window.location.search).get('lang'); - const savedLang = localStorage.getItem('cv-language'); - - if (!urlLang && savedLang) { - // URL is clean but we have a saved preference - redirect with lang parameter - const url = new URL(window.location); - url.searchParams.set('lang', savedLang); - window.location.replace(url.toString()); - } else if (urlLang) { - // Save URL language to localStorage - localStorage.setItem('cv-language', urlLang); - } - - // Restore CV length preference - const savedLength = localStorage.getItem('cv-length') || 'short'; - const lengthChecked = savedLength === 'long'; - if (lengthChecked) { - paper.classList.add('cv-long'); - paper.classList.remove('cv-short'); - } else { - paper.classList.add('cv-short'); - paper.classList.remove('cv-long'); - } - // Sync both header and menu toggles - const headerLengthToggle = document.getElementById('lengthToggle'); - const menuLengthToggle = document.getElementById('lengthToggleMenu'); - if (headerLengthToggle) headerLengthToggle.checked = lengthChecked; - if (menuLengthToggle) menuLengthToggle.checked = lengthChecked; - - // Restore logos preference - const savedLogos = localStorage.getItem('cv-logos') || 'show'; - const logosChecked = savedLogos === 'show'; - if (logosChecked) { - paper.classList.add('show-logos'); - } else { - paper.classList.remove('show-logos'); - } - // Sync both header and menu toggles - const headerLogoToggle = document.getElementById('logoToggle'); - const menuLogoToggle = document.getElementById('logoToggleMenu'); - if (headerLogoToggle) headerLogoToggle.checked = logosChecked; - if (menuLogoToggle) menuLogoToggle.checked = logosChecked; - - // Restore theme preference - const savedTheme = localStorage.getItem('cv-theme') || 'default'; - const themeChecked = savedTheme === 'clean'; - // Sync both header and menu toggles - const headerThemeToggle = document.getElementById('themeToggle'); - const menuThemeToggle = document.getElementById('themeToggleMenu'); - if (headerThemeToggle) headerThemeToggle.checked = themeChecked; - if (menuThemeToggle) menuThemeToggle.checked = themeChecked; - if (themeChecked) { - window.toggleTheme(); - } - - // Initialize zoom control (zoom level, event listeners) - initZoomControl(); - - // Initialize zoom visibility state (show/hide based on localStorage) - initZoomVisibility(); - - // Initialize zoom dragging (make draggable, restore position) - initZoomDragging(); - } - - // ============================================================================= - // SCROLL BEHAVIOR - // ============================================================================= - - function initScrollBehavior() { - let lastScrollTop = 0; - let scrollThreshold = 100; // Start hiding after 100px scroll - - window.addEventListener('scroll', function() { - const actionBar = document.querySelector('.action-bar'); - const navMenu = document.querySelector('.navigation-menu'); - const backToTopBtn = document.getElementById('back-to-top'); - const infoBtn = document.querySelector('.info-button'); - const currentScroll = window.pageYOffset || document.documentElement.scrollTop; - const isMenuOpen = navMenu.classList.contains('menu-open'); - - // Update zoom control position on horizontal scroll - updateZoomControlPosition(); - - // Check if at bottom of page (within 50px threshold) - const scrollHeight = document.documentElement.scrollHeight; - const clientHeight = document.documentElement.clientHeight; - const isAtBottom = (scrollHeight - currentScroll - clientHeight) < 50; - - // If scrolling up, reset the keepHeaderVisible flag - if (currentScroll < lastScrollTop) { - keepHeaderVisible = false; - } - - // Hide/show header based on scroll direction - if (currentScroll > scrollThreshold) { - if (currentScroll > lastScrollTop && !keepHeaderVisible) { - // Scrolling down - hide header (only if keepHeaderVisible is false) - actionBar.classList.add('header-hidden'); - // Only hide menu if it's open - if (isMenuOpen) { - navMenu.classList.add('header-hidden'); - } - } else { - // Scrolling up - show header - actionBar.classList.remove('header-hidden'); - // Only show menu if it's open - if (isMenuOpen) { - navMenu.classList.remove('header-hidden'); - } - } - } else { - // At top - always show header - actionBar.classList.remove('header-hidden'); - // Only affect menu if it's open - if (isMenuOpen) { - navMenu.classList.remove('header-hidden'); - } - } - - // Show/hide back to top button - if (currentScroll > 300) { - backToTopBtn.style.display = 'flex'; - } else { - backToTopBtn.style.display = 'none'; - } - - // Add/remove at-bottom class for both buttons - if (isAtBottom) { - if (backToTopBtn) backToTopBtn.classList.add('at-bottom'); - if (infoBtn) infoBtn.classList.add('at-bottom'); - } else { - if (backToTopBtn) backToTopBtn.classList.remove('at-bottom'); - if (infoBtn) infoBtn.classList.remove('at-bottom'); - } - - lastScrollTop = currentScroll <= 0 ? 0 : currentScroll; - }, false); - - // Back to top button click handler - const backToTopBtn = document.getElementById('back-to-top'); - if (backToTopBtn) { - backToTopBtn.addEventListener('click', function() { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - }); - } - } - - // ============================================================================= - // MODALS - // ============================================================================= - - // Info Modal Functions - window.openInfoModal = function() { - const modal = document.getElementById('info-modal'); - modal.classList.add('active'); - document.body.style.overflow = 'hidden'; // Prevent scrolling when modal is open - }; - - window.closeInfoModal = function() { - const modal = document.getElementById('info-modal'); - modal.classList.remove('active'); - document.body.style.overflow = ''; // Restore scrolling - }; - - window.closeInfoModalOnBackdrop = function(event) { - if (event.target.id === 'info-modal') { - window.closeInfoModal(); - } - }; - - // PDF Modal Functions - window.openPdfModal = function() { - const modal = document.getElementById('pdf-modal'); - modal.classList.add('active'); - document.body.style.overflow = 'hidden'; // Prevent scrolling when modal is open - }; - - window.closePdfModal = function() { - const modal = document.getElementById('pdf-modal'); - modal.classList.remove('active'); - document.body.style.overflow = ''; // Restore scrolling - }; - - window.closePdfModalOnBackdrop = function(event) { - if (event.target.id === 'pdf-modal') { - window.closePdfModal(); - } - }; - - // Close modals with Escape key - function initModalKeyHandlers() { - document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - window.closeInfoModal(); - window.closePdfModal(); - } - }); - } - - // ============================================================================= - // ERROR HANDLING - // ============================================================================= - - // Error handling utility - window.showError = function(message) { - const errorToast = document.getElementById('error-toast'); - const errorMessage = document.getElementById('error-message'); - errorMessage.textContent = message; - errorToast.style.display = 'flex'; - - // Auto-hide after 5 seconds - setTimeout(() => { - errorToast.style.display = 'none'; - }, 5000); - }; - - // ============================================================================= - // HTMX EVENT HANDLERS - // ============================================================================= - - function initHTMXHandlers() { - // HTMX Global Error Handlers - document.body.addEventListener('htmx:responseError', function(evt) { - console.error('HTMX Response Error:', evt.detail); - const lang = document.documentElement.lang; - const message = lang === 'es' - ? 'Error al cargar el contenido. Por favor, inténtelo de nuevo.' - : 'Failed to load content. Please try again.'; - window.showError(message); - }); - - document.body.addEventListener('htmx:sendError', function(evt) { - console.error('HTMX Send Error:', evt.detail); - const lang = document.documentElement.lang; - const message = lang === 'es' - ? 'Error de conexión. Verifique su conexión a internet.' - : 'Connection error. Please check your internet connection.'; - window.showError(message); - }); - - document.body.addEventListener('htmx:timeout', function(evt) { - console.error('HTMX Timeout:', evt.detail); - const lang = document.documentElement.lang; - const message = lang === 'es' - ? 'La solicitud tardó demasiado. Por favor, inténtelo de nuevo.' - : 'Request timed out. Please try again.'; - window.showError(message); - }); - - document.body.addEventListener('htmx:afterSwap', function(evt) { - // Smooth scroll to top on language change - if (evt.detail.target.id === 'cv-content') { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - // Track HTMX navigation events with Matomo - if (typeof _paq !== 'undefined' && evt.detail.target.id === 'cv-content') { - // Track language change as virtual pageview - const lang = new URLSearchParams(window.location.search).get('lang') || 'en'; - _paq.push(['setCustomUrl', window.location.href]); - _paq.push(['setDocumentTitle', document.title]); - _paq.push(['trackPageView']); - } - }); - - // Log successful swaps for debugging - document.body.addEventListener('htmx:afterRequest', function(evt) { - if (evt.detail.successful) { - console.log('HTMX request successful:', evt.detail.pathInfo.requestPath); - } - }); - } - - // ============================================================================= - // INITIALIZATION - // ============================================================================= - - // Initialize everything when DOM is ready - document.addEventListener('DOMContentLoaded', function() { - initMenuSystem(); - initClickOutsideHandler(); - initPreferences(); - initScrollBehavior(); - initModalKeyHandlers(); - initHTMXHandlers(); - }); - -})();