commit 2f5f37c7b445f3d15e76f0cba409921c716b1fba Author: Juan Marquez Date: Thu Jun 18 17:08:26 2026 +0200 first commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a901f9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.24-alpine AS build +WORKDIR /src +COPY go.mod ./ +COPY main.go ./ +COPY static/ ./static/ +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server . + +FROM scratch +COPY --from=build /server /server +EXPOSE 8080 +ENTRYPOINT ["/server"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..26d82b7 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# May Hair Care — Landing Page + EasyAppointments + +Landing page bonita para May HairCare (Horta, Barcelona) con reservas gestionadas por **EasyAppointments**. + +El frontend (HTML/JS puro) sigue siendo el de la landing, pero **las reservas, clientes y servicios se gestionan completamente con EasyAppointments**. + +## Stack actual + +- **Web (estático)**: Go + proxy reverso a EasyAppointments API +- **Gestión de citas, clientes y servicios**: EasyAppointments (PHP + MySQL) +- **Base de datos**: MySQL (para EA) +- **Docker Compose**: todo junto (web + easyappointments + mysql) + +## Cómo ejecutar (Desarrollo y Producción) + +### 1. Levantar todo + +```sh +docker compose up -d --build +``` + +- Landing + reservas: **http://localhost:8234** +- Panel de administración EasyAppointments: **http://localhost:8888** + +### 2. Primer arranque (importante) + +1. Abre http://localhost:8888 +2. Completa el asistente de instalación de EasyAppointments. +3. Crea un **usuario administrador**. +4. Ve a **Providers** → crea al menos un proveedor (ej. "May"). +5. Ve a **Services** → crea tus servicios (puedes usar la lista anterior de servicios como referencia). +6. Asigna los servicios al proveedor. +7. Configura el **Working Plan** del proveedor (horario). +8. **Anota el ID del Provider** (normalmente 1) y edita `static/index.html` → línea `const PROVIDER_ID = 1;` + +### 3. Variables importantes en compose + +Edita `docker-compose.yml` antes de levantar: + +- `EA_API_USERNAME` + `EA_API_PASSWORD` (o mejor: configura **API Key** en Settings de EA y usa `EA_API_KEY`) +- `BASE_URL` del servicio `easyappointments` +- Contraseña de MySQL + +### Actualizar + +```sh +git pull +docker compose up -d --build +``` + +## Notas sobre la integración + +- Los servicios y la disponibilidad los obtiene el frontend directamente de la API de EasyAppointments. +- Al reservar: + - Busca o crea automáticamente el cliente usando el **email**. + - Crea la cita en EasyAppointments (con todos los servicios seleccionados en las notas). +- La gestión completa (clientes, servicios, citas, proveedores, etc.) se hace desde http://localhost:8888 +- La API antigua (carpeta `api/` + admin.html antigua + services.yml/mail.yml) ha sido eliminada completamente. + +## Comportamiento multiservicio + +El formulario permite seleccionar **varios servicios** (multi-checkbox). + +Cómo funciona actualmente con EasyAppointments: + +- **UI**: Puedes seleccionar múltiples servicios. Se muestra el resumen con nombres unidos por "+" y la duración total sumada. +- **Comprobación de disponibilidad**: Se consulta el endpoint de EA usando el servicio de **mayor duración** de los seleccionados. EA devuelve los slots libres según ese servicio + el horario del proveedor. +- **Al crear la reserva**: + - Se crea **una sola cita** en EasyAppointments. + - Se usa como `serviceId` el servicio de mayor duración (para que coincida con la disponibilidad consultada). + - Se calcula la hora de fin sumando la **duración total** de todos los servicios seleccionados. + - En el campo `notes` se guarda el detalle completo: + ``` + Nombre: ... + Email: ... + Teléfono: ... + Servicios: Corte + Tinte + ... + Duración total: 120 min + Mensaje: ... + ``` + +**Limitación**: EasyAppointments modela una cita = un servicio principal. Los servicios adicionales quedan documentados en las notas del cliente y de la cita. No se crean múltiples registros de cita. + +Recomendación: Si usas combinaciones frecuentes, crea "servicios combinados" directamente en EasyAppointments (ej. "Corte + Tinte Señora - 120min"). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a722144 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + web: + build: . + container_name: may + restart: unless-stopped + ports: + - "8234:8080" + environment: + - EA_API_USERNAME=admin # Usuario admin de EA (o mejor usa EA_API_KEY) + - EA_API_PASSWORD=yourpassword + # - EA_API_KEY=tu-api-key # Recomendado: crea API Key en Settings de EA + depends_on: + - easyappointments + + # EasyAppointments - gestión de citas, clientes y servicios + easyappointments: + image: alextselegidis/easyappointments:latest + container_name: may-easyappointments + restart: unless-stopped + ports: + - "8888:80" # Panel admin: http://localhost:8888 + depends_on: + - mysql + environment: + - BASE_URL=http://localhost:8888 # Cambia en producción + - DEBUG_MODE=FALSE + - DB_HOST=mysql + - DB_NAME=easyappointments + - DB_USERNAME=root + - DB_PASSWORD=secret # Cambia por contraseña fuerte + + mysql: + image: mysql:8.0 + container_name: may-ea-mysql + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=secret + - MYSQL_DATABASE=easyappointments + volumes: + - ea-mysql-data:/var/lib/mysql + +volumes: + ea-mysql-data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9e605b4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module barbershop + +go 1.24.7 diff --git a/karpathy.md b/karpathy.md new file mode 100644 index 0000000..05ddc06 --- /dev/null +++ b/karpathy.md @@ -0,0 +1,59 @@ +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/main.go b/main.go new file mode 100644 index 0000000..e9dd53e --- /dev/null +++ b/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "embed" + "flag" + "fmt" + "io/fs" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" +) + +//go:embed all:static +var static embed.FS + +func main() { + port := flag.Int("port", 8080, "puerto del servidor") + flag.Parse() + + staticFS, err := fs.Sub(static, "static") + if err != nil { + log.Fatal(err) + } + + // Proxy a EasyAppointments API + // El frontend llama a /ea-api/v1/... y aquí se reescribe y se añade auth + eaTarget := "http://easyappointments:80" + if t := os.Getenv("EA_TARGET"); t != "" { + eaTarget = t + } + eaURL, err := url.Parse(eaTarget) + if err != nil { + log.Fatal(err) + } + eaProxy := httputil.NewSingleHostReverseProxy(eaURL) + + http.HandleFunc("/ea-api/", func(w http.ResponseWriter, r *http.Request) { + // /ea-api/v1/services → /index.php/api/v1/services + r.URL.Path = strings.Replace(r.URL.Path, "/ea-api", "/index.php/api", 1) + + // Auth (server-side, nunca se expone al navegador) + if apiKey := os.Getenv("EA_API_KEY"); apiKey != "" { + r.Header.Set("Authorization", "Bearer " + apiKey) + } else if user := os.Getenv("EA_API_USERNAME"); user != "" { + if pass := os.Getenv("EA_API_PASSWORD"); pass != "" { + r.SetBasicAuth(user, pass) + } + } + + eaProxy.ServeHTTP(w, r) + }) + + http.Handle("/", http.FileServer(http.FS(staticFS))) + + addr := fmt.Sprintf(":%d", *port) + log.Printf("Servidor iniciado en http://localhost%s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/static/assets/.gitkeep b/static/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/assets/galeria-1.jpg b/static/assets/galeria-1.jpg new file mode 100644 index 0000000..2695d34 Binary files /dev/null and b/static/assets/galeria-1.jpg differ diff --git a/static/assets/galeria-2.jpg b/static/assets/galeria-2.jpg new file mode 100644 index 0000000..57a5c9a Binary files /dev/null and b/static/assets/galeria-2.jpg differ diff --git a/static/assets/galeria-3.jpg b/static/assets/galeria-3.jpg new file mode 100644 index 0000000..87cddc9 Binary files /dev/null and b/static/assets/galeria-3.jpg differ diff --git a/static/assets/galeria-4.jpg b/static/assets/galeria-4.jpg new file mode 100644 index 0000000..bbeb182 Binary files /dev/null and b/static/assets/galeria-4.jpg differ diff --git a/static/assets/galeria-5.jpg b/static/assets/galeria-5.jpg new file mode 100644 index 0000000..b61f82c Binary files /dev/null and b/static/assets/galeria-5.jpg differ diff --git a/static/assets/galeria-6.jpg b/static/assets/galeria-6.jpg new file mode 100644 index 0000000..1fdddb5 Binary files /dev/null and b/static/assets/galeria-6.jpg differ diff --git a/static/assets/hero.jpg b/static/assets/hero.jpg new file mode 100644 index 0000000..a79394a Binary files /dev/null and b/static/assets/hero.jpg differ diff --git a/static/assets/may.jpg b/static/assets/may.jpg new file mode 100644 index 0000000..070b2ba Binary files /dev/null and b/static/assets/may.jpg differ diff --git a/static/assets/servicio-caballero.jpg b/static/assets/servicio-caballero.jpg new file mode 100644 index 0000000..f08c747 Binary files /dev/null and b/static/assets/servicio-caballero.jpg differ diff --git a/static/assets/servicio-ninos.jpg b/static/assets/servicio-ninos.jpg new file mode 100644 index 0000000..1507300 Binary files /dev/null and b/static/assets/servicio-ninos.jpg differ diff --git a/static/assets/servicio-senora.jpg b/static/assets/servicio-senora.jpg new file mode 100644 index 0000000..33fa213 Binary files /dev/null and b/static/assets/servicio-senora.jpg differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..e01a7f8 --- /dev/null +++ b/static/index.html @@ -0,0 +1,696 @@ + + + + + + May HairCare — Peluquería en Barcelona + + + + + + + + + + + +
+
+ Interior del salón May HairCare +
+
+

May HairCare
Tu peluquería en Horta

+

Cortes, color, peinados y tratamientos capilares para toda la familia. Un espacio pensado para ti.

+ +
+
+ + +
+
+

Nuestros servicios

+

Cuidamos de tu pelo con los mejores productos y técnicas

+
+

Cargando servicios…

+
+
+
+ + +
+
+

Nuestro trabajo

+

Algunos de los looks que hemos creado para nuestras clientas

+ +
+
+ + +
+
+
+
+ May, peluquera profesional en May HairCare +
+
+

May

+

Peluquera & fundadora

+

Con más de 15 años de experiencia en peluquería profesional, May ha hecho del cuidado del cabello su vocación. Especializada en coloración, corte y tratamientos capilares, trabaja con técnicas actuales y productos de alta calidad para ofrecer resultados duraderos. Su trato cercano y la atención personalizada a cada cliente son la base de May HairCare.

+
+
+
+
+ + +
+
+

Horario

+
+
+ Lunes a Viernes + 9:30 — 13:30 / 16:30 — 20:30 +
+
+ Sábados + 9:00 — 14:00 +
+
+ Domingos y festivos + Cerrado +
+
+

Llámanos o reserva online. Te atendemos con o sin cita.

+
+
+ + +
+
+

Reservar cita

+

Elige uno o varios servicios, día y hora para tu cita

+ + +
+

Cargando servicios…

+
+
+ + · + Duración: +
+ +
+ +
+
+ + + +
+
+ LuMaMiJuViDo +
+
+
+ +
+
Selecciona primero el servicio que deseas
+ + +
+
+ +
+
+ + +
+
+

Encuéntranos

+
+
+
+ Dirección +

Carrer d'Horta, 12
08032 Barcelona (Horta)

+
+
+ Teléfono +

933 333 333

+
+
+ Cómo llegar +

Metro L5 — Horta
Bus V19, 45, 102

+
+
+
+ +
+
+
+
+ + + + + + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..4e88d2e --- /dev/null +++ b/static/style.css @@ -0,0 +1,760 @@ +/* ============ RESET & BASE ============ */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --bg: #faf9f7; + --bg-alt: #f3ede6; + --text: #2c2c2c; + --text-mid: #6b6b6b; + --accent: #a67c52; + --accent-dk:#8c6740; + --white: #fff; + --radius: 12px; +} + +html { scroll-behavior: smooth; } +body { + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { color: inherit; text-decoration: none; } +img { max-width: 100%; display: block; } + +.container { + max-width: 1060px; + margin: 0 auto; + padding: 0 24px; +} + +/* ============ NAV ============ */ +.nav { + position: fixed; + top: 0; left: 0; right: 0; + background: rgba(250, 249, 247, 0.92); + backdrop-filter: blur(10px); + z-index: 100; + border-bottom: 1px solid rgba(0,0,0,0.06); +} + +.nav-inner { + display: flex; + justify-content: space-between; + align-items: center; + height: 64px; +} + +.logo { + font-family: 'DM Serif Display', serif; + font-size: 1.3rem; + color: var(--text); +} +.logo span { color: var(--accent); } + +.nav-links { + display: flex; + gap: 28px; + list-style: none; +} +.nav-links a { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-mid); + transition: color 0.2s; +} +.nav-links a:hover { color: var(--accent); } + +.btn-nav { + padding: 8px 20px; + font-size: 0.85rem; +} + +/* ============ HERO ============ */ +.hero { + position: relative; + min-height: 520px; + display: flex; + align-items: center; + justify-content: center; + padding: 140px 0 80px; + text-align: center; + overflow: hidden; +} + +.hero-bg { + position: absolute; + inset: 0; + z-index: 0; +} + +.hero-bg img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.hero-bg::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + rgba(250, 249, 247, 0.75) 0%, + rgba(250, 249, 247, 0.55) 50%, + rgba(250, 249, 247, 0.85) 100% + ); +} + +/* Fallback gradient when no image is loaded */ +.hero-bg img[src=""]::before, +.hero-bg img:not([src])::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(160deg, var(--bg) 40%, var(--bg-alt) 100%); +} + +.hero-content { + position: relative; + z-index: 1; +} + +.hero h1 { + font-family: 'DM Serif Display', serif; + font-size: clamp(2.4rem, 5vw, 3.6rem); + line-height: 1.15; + margin-bottom: 20px; +} + +.hero-sub { + color: var(--text); + font-size: 1.1rem; + font-weight: 500; + max-width: 500px; + margin: 0 auto 32px; + text-shadow: 0 1px 8px rgba(250, 249, 247, 0.9); +} + +.hero-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +/* ============ BUTTONS ============ */ +.btn { + display: inline-block; + padding: 12px 28px; + border-radius: 999px; + font-size: 0.95rem; + font-weight: 600; + transition: all 0.2s; + cursor: pointer; +} + +.btn-primary { + background: var(--accent); + color: var(--white); +} +.btn-primary:hover { background: var(--accent-dk); } + +.btn-secondary { + background: transparent; + color: var(--text); + border: 2px solid rgba(0,0,0,0.12); +} +.btn-secondary:hover { border-color: var(--accent); color: var(--accent); } + +/* ============ SECTIONS ============ */ +.section { padding: 80px 0; } +.section-alt { background: var(--bg-alt); } + +.section-title { + font-family: 'DM Serif Display', serif; + font-size: 2rem; + text-align: center; + margin-bottom: 12px; +} + +.section-subtitle { + text-align: center; + color: var(--text-mid); + font-size: 1rem; + margin-bottom: 48px; + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +/* ============ SERVICES ============ */ +.services-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.service-card { + background: var(--white); + border-radius: var(--radius); + overflow: hidden; + border: 1px solid rgba(0,0,0,0.05); + transition: transform 0.2s, box-shadow 0.2s; +} +.service-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 24px rgba(0,0,0,0.06); +} + +.service-img { + width: 100%; + height: 220px; + overflow: hidden; + background: var(--bg-alt); +} + +.service-img img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.service-body { + padding: 24px; +} + +.service-body h3 { + font-family: 'DM Serif Display', serif; + font-size: 1.3rem; + margin-bottom: 16px; +} + +.service-list { + list-style: none; +} + +.service-list li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid rgba(0,0,0,0.06); + font-size: 0.9rem; + color: var(--text-mid); +} + +.service-list li:last-child { + border-bottom: none; +} + +.price { + font-weight: 600; + color: var(--accent); + white-space: nowrap; +} + +/* ============ GALLERY ============ */ +.gallery-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.gallery-item { + border-radius: var(--radius); + overflow: hidden; + aspect-ratio: 1; + background: var(--bg); +} + +.gallery-item img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; +} + +.gallery-item:hover img { + transform: scale(1.05); +} + +/* ============ SCHEDULE ============ */ +.schedule { + max-width: 500px; + margin: 0 auto; +} + +.schedule-row { + display: flex; + justify-content: space-between; + padding: 14px 0; + border-bottom: 1px solid rgba(0,0,0,0.08); + font-size: 0.95rem; +} +.schedule-row.closed span:last-child { color: #c0392b; font-weight: 500; } + +.schedule-note { + text-align: center; + margin-top: 24px; + color: var(--accent); + font-weight: 600; + font-size: 0.95rem; +} + +/* ============ CONTACT ============ */ +.contact-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: start; +} + +.contact-item { margin-bottom: 20px; } +.contact-item strong { + display: block; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent); + margin-bottom: 4px; +} +.contact-item p { font-size: 0.95rem; color: var(--text-mid); } +.contact-item a:hover { color: var(--accent); } + +/* ============ BOOKING FORM ============ */ +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 16px; +} + +.form-row .form-group { + margin-bottom: 0; +} + +.form-group label { + font-size: 0.85rem; + font-weight: 600; + color: var(--text); +} + +.form-group input, +.form-group select, +.form-group textarea { + padding: 10px 14px; + border: 1px solid rgba(0,0,0,0.14); + border-radius: 8px; + font-size: 0.95rem; + font-family: inherit; + background: var(--white); + color: var(--text); + transition: border-color 0.2s; + width: 100%; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--accent); +} + +.form-group textarea { + resize: vertical; +} + +.booking-result { + max-width: 820px; + margin: 16px auto 0; + padding: 14px 18px; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 500; +} + +.booking-success { + background: #edf7ed; + color: #2e7d32; + border: 1px solid #a5d6a7; +} + +.booking-error { + background: #fdecea; + color: #c62828; + border: 1px solid #ef9a9a; +} + +/* ============ CALENDAR ============ */ +.booking-calendar { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + max-width: 820px; + margin: 0 auto; + align-items: start; +} + +.cal-left { + background: var(--white); + border-radius: var(--radius); + padding: 24px; + border: 1px solid rgba(0,0,0,0.06); +} + +.cal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 20px 0 16px; +} + +.cal-month { + font-family: 'DM Serif Display', serif; + font-size: 1.15rem; +} + +.cal-nav { + background: none; + border: 1px solid rgba(0,0,0,0.12); + border-radius: 8px; + width: 36px; + height: 36px; + font-size: 1.1rem; + cursor: pointer; + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + transition: border-color 0.2s, color 0.2s; + padding: 0; +} +.cal-nav:hover { + border-color: var(--accent); + color: var(--accent); + background: none; +} + +.cal-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-mid); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 8px; +} + +.cal-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; +} + +.cal-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + border-radius: 8px; + font-size: 0.9rem; + cursor: pointer; + transition: background 0.15s, color 0.15s; + color: var(--text); + font-family: inherit; + padding: 0; +} +.cal-day:hover:not(:disabled) { + background: var(--bg-alt); +} + +.cal-empty { + cursor: default; +} + +.cal-disabled { + color: rgba(0,0,0,0.2); + cursor: default; +} + +.cal-selected { + background: var(--accent) !important; + color: var(--white) !important; + font-weight: 600; +} + +/* ============ SLOTS ============ */ +.cal-right { + min-height: 200px; +} + +.slots-placeholder { + color: var(--text-mid); + text-align: center; + padding: 60px 20px; + font-size: 0.95rem; +} + +.slots-title { + font-family: 'DM Serif Display', serif; + font-size: 1.1rem; + margin-bottom: 16px; +} + +.slots-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 24px; +} + +.slot-btn { + padding: 10px 4px; + border: 1px solid rgba(0,0,0,0.12); + border-radius: 8px; + background: var(--white); + font-size: 0.9rem; + cursor: pointer; + transition: all 0.15s; + color: var(--text); + font-family: inherit; +} +.slot-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.slot-occupied { + background: var(--bg-alt); + color: rgba(0,0,0,0.25); + text-decoration: line-through; + cursor: default; + border-color: transparent; +} + +.slot-selected { + background: var(--accent) !important; + color: var(--white) !important; + border-color: var(--accent) !important; + font-weight: 600; +} + +.booking-mini-form { + margin-top: 8px; +} + +.services-loading { + color: var(--text-mid); + text-align: center; + grid-column: 1 / -1; + padding: 40px 0; +} + +/* ============ PROFESIONAL ============ */ +.profesional-grid { + display: grid; + grid-template-columns: 260px 1fr; + gap: 60px; + align-items: center; + max-width: 860px; + margin: 0 auto; +} + +.profesional-img { + border-radius: var(--radius); + overflow: hidden; + aspect-ratio: 3 / 4; +} + +.profesional-img img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profesional-info h3 { + font-family: 'DM Serif Display', serif; + font-size: 2rem; + margin-bottom: 6px; +} + +.profesional-role { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent); + font-weight: 600; + margin-bottom: 20px; +} + +.profesional-info p { + color: var(--text-mid); + font-size: 0.97rem; + line-height: 1.8; +} + +/* ============ SERVICE SELECTOR (booking) ============ */ +.service-selector { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + max-width: 820px; + margin: 0 auto 20px; +} + +.svc-cat-card { + background: var(--white); + border: 1px solid rgba(0,0,0,0.06); + border-radius: var(--radius); + padding: 20px; + transition: opacity 0.2s; +} + +.svc-cat-card.svc-cat-locked { + opacity: 0.35; + pointer-events: none; +} + +.svc-cat-title { + font-family: 'DM Serif Display', serif; + font-size: 1rem; + color: var(--accent); + margin-bottom: 12px; +} + +.svc-options { + display: flex; + flex-direction: column; + gap: 4px; +} + +.svc-option { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 6px 8px; + border-radius: 8px; + transition: background 0.15s; +} + +.svc-option:hover { + background: var(--bg-alt); +} + +.svc-option input[type=checkbox] { + accent-color: var(--accent); + width: 15px; + height: 15px; + cursor: pointer; + flex-shrink: 0; +} + +.svc-name { + flex: 1; + font-size: 0.88rem; + color: var(--text); +} + +.svc-meta { + font-size: 0.78rem; + color: var(--text-mid); + white-space: nowrap; +} + +.service-summary-bar { + display: none; + max-width: 820px; + margin: 0 auto 28px; + padding: 12px 20px; + background: var(--accent); + border-radius: var(--radius); + color: var(--white); + font-size: 0.9rem; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.service-summary-bar #summary-names { + flex: 1; + font-weight: 600; +} + +.service-summary-bar .summary-sep { + opacity: 0.6; +} + +/* ============ FOOTER ============ */ +.footer { + padding: 32px 0; + border-top: 1px solid rgba(0,0,0,0.06); +} + +.footer-inner { + display: flex; + justify-content: space-between; + align-items: center; +} +.footer p { font-size: 0.82rem; color: var(--text-mid); } + +/* ============ RESPONSIVE ============ */ +@media (max-width: 768px) { + .services-grid { + grid-template-columns: 1fr; + } + .gallery-grid { + grid-template-columns: repeat(2, 1fr); + } + .booking-calendar { + grid-template-columns: 1fr; + gap: 24px; + } + .slots-grid { + grid-template-columns: repeat(3, 1fr); + } + .profesional-grid { + grid-template-columns: 200px 1fr; + gap: 36px; + } +} + +@media (max-width: 640px) { + .nav-links { display: none; } + .btn-nav { display: none; } + .contact-grid { grid-template-columns: 1fr; } + .footer-inner { flex-direction: column; gap: 8px; text-align: center; } + .gallery-grid { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } + .form-row { + grid-template-columns: 1fr; + } + .profesional-grid { + grid-template-columns: 1fr; + gap: 32px; + text-align: center; + } + .profesional-img { + max-width: 200px; + margin: 0 auto; + } + .service-selector { + grid-template-columns: 1fr; + } +}