diff --git a/.gitignore b/.gitignore
index 0b222a3..45ef76a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,11 @@ __pycache__/
*.py[cod]
*$py.class
-# Variables de entorno
-
+# Variables de entorno (solo ignoramos las locales/sobre-escrituras)
+# Mantenemos .env y .env.example versionados para compartir la configuración
+# de producción y el esqueleto de variables. Nunca subimos .env.local.
+.env.local
+.env.*.local
# Configuraciones del editor
.vscode/
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..1f82712
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,21 @@
+# Backend environment variables (FastAPI)
+# ----------------------------------------
+# Copia este archivo como `.env` para desarrollo local y rellena los valores.
+#
+# docker-compose levanta el backend con `env_file: backend/.env`.
+
+# Google OAuth (https://console.cloud.google.com/apis/credentials)
+# IMPORTANTE: la REDIRECT_URI es la URL a la que Google devuelve al usuario,
+# por tanto debe coincidir con la URL pública del backend tal como la ve el
+# navegador. Usando docker-compose, el backend está expuesto en el host en
+# el puerto 8070, así que:
+# http://localhost:8070/api/auth/google/callback
+# Si ejecutas el backend fuera de Docker en el puerto 8000, usa:
+# http://localhost:8000/api/auth/google/callback
+# Esta URI debe estar registrada también en la consola de Google Cloud.
+GOOGLE_CLIENT_ID=tu-client-id.apps.googleusercontent.com
+GOOGLE_CLIENT_SECRET=tu-client-secret
+GOOGLE_REDIRECT_URI=http://localhost:8070/api/auth/google/callback
+
+# Clave para firmar los JWT (usa algo largo y aleatorio en producción)
+SECRET_KEY=cambia-esta-clave-en-produccion
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000..3c23460
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,22 @@
+# Frontend environment variables (Vite)
+# --------------------------------------
+# Copia este archivo como `.env.local` para desarrollo local.
+# Vite carga automáticamente `.env.local` encima de `.env`, así que
+# los valores aquí definidos sobrescribirán los de producción en dev.
+#
+# cp .env.example .env.local
+#
+# IMPORTANTE: Sólo las variables con prefijo VITE_ se exponen al cliente.
+
+# URL base de la API del backend (la ve el NAVEGADOR, no el contenedor).
+#
+# - Dev con docker-compose (recomendado): el backend está expuesto en el
+# puerto 8070 del host (mapea 8070 -> 8000 del contenedor).
+# VITE_API_URL=http://localhost:8070/api
+#
+# - Backend ejecutado fuera de Docker (uvicorn --port 8000):
+# VITE_API_URL=http://localhost:8000/api
+#
+# - Producción (ya definido en .env):
+# VITE_API_URL=http://sinbad2.ujaen.es:8070/api
+VITE_API_URL=http://localhost:8070/api
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a547bf3..efe0071 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -12,6 +12,11 @@ dist
dist-ssr
*.local
+# Variables de entorno locales de Vite
+# (.env y .env.example sí se versionan)
+.env.local
+.env.*.local
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx
index 565cc84..252a6fb 100644
--- a/frontend/src/components/membershipFunction/Controls.jsx
+++ b/frontend/src/components/membershipFunction/Controls.jsx
@@ -1,4 +1,105 @@
-export default function Controls({
+import { useState } from 'react';
+
+function SliderInput({ label, value, min, max, step, color, onChange, decimals = 3 }) {
+ const [draft, setDraft] = useState(null);
+
+ const numericValue = Number(value);
+ const displayValue = draft !== null ? draft : numericValue.toFixed(decimals);
+
+ const commitDraft = () => {
+ if (draft === null) return;
+
+ const trimmed = draft.trim();
+ if (trimmed === '' || trimmed === '-' || trimmed === '.' || trimmed === '-.') {
+ setDraft(null);
+ return;
+ }
+
+ const parsed = parseFloat(trimmed);
+ if (Number.isNaN(parsed)) {
+ setDraft(null);
+ return;
+ }
+
+ const clamped = Math.min(max, Math.max(min, parsed));
+ onChange(clamped);
+ setDraft(null);
+ };
+
+ const handleNumberChange = (e) => {
+ const raw = e.target.value;
+
+ if (raw === '') {
+ setDraft('');
+ return;
+ }
+
+ const num = e.target.valueAsNumber;
+
+ if (Number.isNaN(num)) {
+ setDraft(raw);
+ return;
+ }
+
+ if (num > max) {
+ setDraft(max.toFixed(decimals));
+ onChange(max);
+ return;
+ }
+
+ if (num < min) {
+ setDraft(raw);
+ return;
+ }
+
+ setDraft(raw);
+ onChange(num);
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ e.currentTarget.blur();
+ } else if (e.key === 'Escape') {
+ setDraft(null);
+ e.currentTarget.blur();
+ }
+ };
+
+ return (
+