diff --git a/css/styles.css b/css/styles.css index f35a254..c559fc1 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,3 +1,23 @@ +/* ========================================================================== + Layout Stability + ========================================================================== */ + +/* Reserve scrollbar gutter to prevent layout shift when body.overflow + toggles between auto and hidden (openModal/closeModal). This stops + the page from "jumping" when the user opens or closes a project modal. */ +html { + scrollbar-gutter: stable; +} + +/* When a modal is open, prevent background scroll without causing the + scrollbar to disappear (which would shift the layout). */ +body.modal-open { + overflow: hidden; + /* Compensate for any scrollbar difference on platforms where + scrollbar-gutter isn't fully respected. */ + padding-right: var(--scrollbar-width, 0px); +} + /* ========================================================================== Self-Hosted Fonts & Icons ========================================================================== */ @@ -60,37 +80,16 @@ src: url('../fonts/instrument/instrument-serif-v5-latin-italic.woff2') format('woff2'); } -/* Material Symbols Outlined */ -@font-face { - font-display: block; - font-family: 'Material Symbols Outlined'; - font-style: normal; - font-weight: 100 700; - src: url('../fonts/material-symbols-outlined.woff2') format('woff2'); -} +/* Material Symbols replaced by inline SVGs (see .icon below) */ -.material-symbols-outlined { - font-family: 'Material Symbols Outlined'; - font-weight: normal; - font-style: normal; - font-size: 24px; +/* Inline icon (replaces Material Symbols font) */ +.icon { width: 1em; height: 1em; - overflow: hidden; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-flex; - align-items: center; - justify-content: center; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - font-feature-settings: 'liga'; - -webkit-font-feature-settings: 'liga'; + fill: currentColor; + flex-shrink: 0; + vertical-align: middle; + display: inline-block; } /* Glass Morphism Effect */ diff --git a/fonts/material-symbols-outlined.woff2 b/fonts/material-symbols-outlined.woff2 deleted file mode 100644 index a876954..0000000 Binary files a/fonts/material-symbols-outlined.woff2 and /dev/null differ diff --git a/huella.html b/huella.html index 94f64c5..a87b6c0 100644 --- a/huella.html +++ b/huella.html @@ -29,7 +29,6 @@ }, }; - - + + + diff --git a/index-en.html b/index-en.html index 88d8a78..d4e34a2 100644 --- a/index-en.html +++ b/index-en.html @@ -452,12 +452,18 @@ > Infrastructure + + + + + + +
+
+
+ query_stats +

+ Cases Tancades +

+
+

+ Open diagnostic of housing in the Balearic Islands: + cross-references official data from INE, Airbnb, and + Govern Balear HUT registries to show how many flats + sit empty against the pressure of tourist rentals. +

+
+
+ Operational + Next.js · Open data INE/Govern Balear +
+
+ + +
On the roadmapOperational Docker • Hetzner
On the roadmapOperational n8n • Ollama • Whisper
- -
-
-
- volunteer_activism -

- Open Source Contribution -

-
-
-
- Not started - Forgejo • GitHub -
-
-
- +
@@ -786,34 +838,44 @@
query_statsvolunteer_activism

- Cases Tancades + Open Source Contribution

-

- Open diagnostic of housing in the Balearic Islands: - cross-references official data from INE, Airbnb, and - Govern Balear HUT registries to show how many flats - sit empty against the pressure of tourist rentals. -

OperationalNot started Next.js · Open data INE/Govern BalearForgejo • GitHub
+ + + +
+ + + + + + + + + + + + + +
- - + - --> + + - + + + diff --git a/index.html b/index.html index 6e81865..61ed9ed 100644 --- a/index.html +++ b/index.html @@ -466,12 +466,18 @@ > Infraestructura + + + + + + +
+
+
+ query_stats +

+ Cases Tancades +

+
+

+ Diagnóstico abierto de la vivienda en Balears: cruza + datos oficiales del INE, Airbnb y registros HUT del + Govern para mostrar cuántos pisos están vacíos frente + a la presión del alquiler turístico. +

+
+
+ Operativo + Next.js · Datos abiertos INE/Govern Balear +
+
+ + +
En el roadmapOperativo Docker • Hetzner
En el roadmapOperativo n8n • Ollama • Whisper
- -
-
-
- volunteer_activism -

- Contribución Open Source -

-
-
-
- Sin iniciar - Forgejo • GitHub -
-
-
- +
@@ -803,34 +855,44 @@
query_statsvolunteer_activism

- Cases Tancades + Contribución Open Source

-

- Diagnóstico abierto de la vivienda en Balears: cruza - datos oficiales del INE, Airbnb y registros HUT del - Govern para mostrar cuántos pisos están vacíos frente - a la presión del alquiler turístico. -

OperativoSin iniciar Next.js · Datos abiertos INE/Govern BalearForgejo • GitHub
+ + + +
+ + + + + + + + + + + + + +
- - + @@ -1459,11 +1522,12 @@ @@ -1472,22 +1536,11 @@ Stack Tecnológico
- Workflow Automation - FFmpeg (vídeo) - OpenAI API - Social Media APIs + Ollama (LLM local) + Navidrome (servidor) + Hetzner (infraestructura verde) + Docker + HTML estático ligero
@@ -1496,9 +1549,10 @@ Resultado

- Automatización completa de la cadena de producción de contenido, - reduciendo tiempo manual en un 90% y garantizando consistencia - en la calidad y el formato de publicaciones. + Sitio activo en producción, publicando diariamente contenido + ambiental curado por IA local. La arquitectura totalmente + self-hosted garantiza soberanía sobre los datos y cero + dependencia de servicios cloud externos.

@@ -1511,9 +1565,12 @@ - --> + + - + + + diff --git a/js/main.js b/js/main.js index e247c1e..7a79fa6 100644 --- a/js/main.js +++ b/js/main.js @@ -61,9 +61,33 @@ function scrollToContact() { /** Element that had focus before a modal was opened */ let _modalPreviousFocus = null; +/** Scrollbar width in px, measured on first open */ +let _scrollbarWidth = null; + +/** + * Measure and cache the browser scrollbar width so we can compensate + * for layout shift when body.overflow is toggled. + */ +function getScrollbarWidth() { + if (_scrollbarWidth !== null) return _scrollbarWidth; + const outer = document.createElement("div"); + outer.style.cssText = + "visibility:hidden;position:absolute;overflow:scroll;width:100px;height:100px;"; + document.body.appendChild(outer); + const inner = document.createElement("div"); + inner.style.width = "100%"; + outer.appendChild(inner); + const w = outer.offsetWidth - inner.offsetWidth; + outer.remove(); + _scrollbarWidth = w; + return w; +} + /** * Open modal by project ID. * Saves the currently focused element and moves focus into the modal. + * Uses a CSS class (body.modal-open) so the scrollbar gutter stays reserved + * via html { scrollbar-gutter: stable }, preventing page jumps. * @param {string} projectId - The project identifier */ function openModal(projectId) { @@ -71,17 +95,21 @@ function openModal(projectId) { if (modal) { _modalPreviousFocus = document.activeElement; modal.classList.remove("hidden"); - document.body.style.overflow = "hidden"; + document.body.classList.add("modal-open"); + // Defensive fallback for browsers without scrollbar-gutter support + document.body.style.paddingRight = getScrollbarWidth() + "px"; - // Move focus to the first focusable element inside the modal + // Move focus to the first focusable element inside the modal. + // PreventScroll keeps the page from jumping when the focused + // element would otherwise trigger a scroll-into-view. const focusable = modal.querySelectorAll( 'button, [href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])' ); if (focusable.length > 0) { - focusable[0].focus(); + focusable[0].focus({ preventScroll: true }); } else { modal.setAttribute("tabindex", "-1"); - modal.focus(); + modal.focus({ preventScroll: true }); } } } @@ -95,11 +123,17 @@ function closeModal(projectId) { const modal = document.getElementById(projectId + "-modal"); if (modal) { modal.classList.add("hidden"); - document.body.style.overflow = "auto"; + document.body.classList.remove("modal-open"); + document.body.style.paddingRight = ""; - // Return focus to the element that triggered the modal + // Return focus to the element that triggered the modal. + // preventScroll keeps the page from jumping back to that element. if (_modalPreviousFocus) { - _modalPreviousFocus.focus(); + try { + _modalPreviousFocus.focus({ preventScroll: true }); + } catch (e) { + _modalPreviousFocus.focus(); + } _modalPreviousFocus = null; } } diff --git a/privacidad-ca.html b/privacidad-ca.html index 8cd801e..7d275e5 100644 --- a/privacidad-ca.html +++ b/privacidad-ca.html @@ -32,7 +32,6 @@ -