@tailwind base; @tailwind components; @tailwind utilities; :root { color-scheme: light; /* Insets del sistema (Dynamic Island, notch, home indicator). Con viewport-fit=cover el contenido se dibuja bajo esas zonas, así que el chrome de la app tiene que descontarlas explícitamente. */ --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); --safe-left: env(safe-area-inset-left, 0px); --safe-right: env(safe-area-inset-right, 0px); } * { -webkit-tap-highlight-color: transparent; } html { /* iOS reescala el texto al rotar a landscape si no se fija. */ -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } html, body, #root { height: 100%; } /* El shell del panel vive en un #root de altura fija; la landing, no. Se relaja solo cuando la landing está montada, así el panel conserva su altura fija. :has() está soportado en Safari 15.4+, dentro del rango de iOS objetivo. */ body:has(.landing) #root { height: auto; min-height: 100%; } body { margin: 0; font-family: Inter, system-ui, sans-serif; background: #f6f7fb; color: #0f172a; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; /* Red de seguridad: ningún hijo debe poder desplazar la página en horizontal. */ overflow-x: hidden; overscroll-behavior-y: none; } /* Evita el zoom por doble toque y el retardo de 300 ms en controles. */ button, a, [role="button"], label, summary { touch-action: manipulation; } /* Scrollbars visibles solo con puntero fino. En táctil deben ser overlay (como en iOS nativo): un scrollbar de 10 px roba ancho al layout y descuadra el cálculo de 100% en pantallas estrechas. */ @media (pointer: fine) { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 9999px; border: 2px solid transparent; background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { background: #9ca3af; background-clip: padding-box; } } /* FullCalendar overrides */ .fc { --fc-border-color: #eef0f4; --fc-page-bg-color: #ffffff; --fc-neutral-bg-color: #fafbfd; --fc-today-bg-color: #eef4ff; --fc-now-indicator-color: #f17616; --fc-button-bg-color: #ffffff; --fc-button-border-color: #e5e7eb; --fc-button-text-color: #374151; --fc-button-hover-bg-color: #f9fafb; --fc-button-hover-border-color: #d1d5db; --fc-button-active-bg-color: #3b66ff; --fc-button-active-border-color: #3b66ff; font-family: Inter, system-ui, sans-serif; } .fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #0f172a; } .fc .fc-toolbar.fc-header-toolbar { position: sticky; top: 0; z-index: 10; background: #ffffff; padding: 0.4rem 0.6rem; margin: -0.4rem -0.6rem 0; flex-wrap: wrap; gap: 0.5rem 0.75rem; } .fc .fc-toolbar-chunk { display: flex; } .fc .fc-toolbar-chunk .fc-button-group { flex-wrap: wrap; } @media (max-width: 640px) { .fc .fc-toolbar-title { font-size: 1rem; width: 100%; text-align: left; order: -1; } .fc .fc-toolbar-chunk { flex: 1 1 auto; } .fc .fc-toolbar-chunk:last-child { margin-left: auto; } .fc .fc-button { padding: 0.35rem 0.55rem; font-size: 0.72rem; } } .fc .fc-button { border-radius: 0.6rem; padding: 0.4rem 0.75rem; font-weight: 600; font-size: 0.8rem; text-transform: capitalize; box-shadow: none !important; } .fc .fc-button:not(:disabled):not(.fc-button-active):hover { background: #f3f4f6; } .fc .fc-button-primary:not(:disabled).fc-button-active { color: #ffffff; background: var(--fc-button-active-bg-color, #3b66ff); border-color: var(--fc-button-active-border-color, #3b66ff); } .fc .fc-button-primary:not(:disabled).fc-button-active:hover { background: #2447f5; border-color: #2447f5; color: #ffffff; } .fc .fc-button:disabled { opacity: 0.5; } .fc .fc-col-header-cell-cushion { padding: 0.6rem 0; font-weight: 600; font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; } .fc .fc-daygrid-day-number, .fc .fc-col-header-cell-cushion { text-decoration: none; } .fc .fc-daygrid-day { transition: background 0.12s; } .fc .fc-daygrid-day:hover { background: #fafbfd; } .fc-event { border: none !important; border-radius: 0.45rem !important; padding: 2px 5px !important; font-size: 0.74rem !important; font-weight: 600 !important; cursor: pointer; transition: transform 0.12s, box-shadow 0.12s; overflow: hidden; } .fc-event:hover { transform: translateY(-1px); box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.25); } .fc-daygrid-event { margin-top: 2px; } .fc .fc-timegrid-slot { height: 2.4rem; } /* En táctil los slots se ajustan por tamaño de pantalla: la grilla de 08:00 a 21:00 son 26 slots, así que una altura fija de 2.4rem produce ~1000 px y desborda cualquier teléfono. El calendario scrollea internamente (como Google Calendar) en lugar de estirar la página. */ @media (max-width: 640px) { .fc .fc-timegrid-slot { height: 2.1rem; } } @media (min-width: 641px) and (max-width: 1024px) { .fc .fc-timegrid-slot { height: 2.25rem; } } /* El scroller interno del timegrid necesita arrastre fluido en iOS. */ .fc .fc-scroller { -webkit-overflow-scrolling: touch; } .fc-timegrid-event { overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); } .fc-timegrid-event-harness { margin-right: 2px !important; } /* When slotEventOverlap is false, FullCalendar puts a small gap between side-by-side events; tighten it a touch so each event gets more readable width without bleeding into its neighbour. */ .fc .fc-timegrid-col-events { column-gap: 2px; } /* List view (great on mobile) */ .fc .fc-list { border: none; } .fc .fc-list-day-cushion { background: #fafbfd; font-weight: 700; color: #0f172a; padding: 0.6rem 0.9rem; } .fc .fc-list-event-time { font-weight: 600; color: #4b5563; font-size: 0.78rem; } .fc .fc-list-event-title { font-weight: 600; color: #0f172a; } .fc .fc-list-event:hover td { background: #f9fafb; } .fc .fc-list-empty { background: transparent; color: #94a3b8; padding: 2.5rem 1rem; font-weight: 500; } /* Móvil: la vista Semana necesita un ancho mínimo o las 7 columnas quedan ilegibles; el card la envuelve en overflow-auto, así que scrollea dentro de la tarjeta sin arrastrar la página. La vista Mes sí cabe en un teléfono (7 columnas de ~50 px), así que no se le fuerza ancho mínimo. */ @media (max-width: 640px) { .fc-timeGridWeek-view .fc-scrollgrid-section > table, .fc-timeGridWeek-view .fc-scrollgrid { min-width: 560px; } /* Objetivos táctiles más grandes para los eventos */ .fc-event { min-height: 30px; padding: 3px 6px !important; } .fc .fc-button { padding: 0.45rem 0.6rem; } /* Etiquetas de hora más estrechas: en 390 px cada píxel del eje cuenta. */ .fc .fc-timegrid-axis, .fc .fc-timegrid-slot-label { font-size: 0.65rem; } .fc .fc-timegrid-axis-frame, .fc .fc-timegrid-slot-label-frame { padding-right: 2px; } .fc .fc-col-header-cell-cushion { padding: 0.45rem 0; font-size: 0.65rem; } /* La vista Lista es la más cómoda en teléfono: más aire al tocar. */ .fc .fc-list-event { font-size: 0.85rem; } .fc .fc-list-event td { padding: 0.7rem 0.6rem; } } /* Tablet (iPad portrait y mini): la semana completa cabe sin scroll lateral, solo hay que evitar que herede el ancho mínimo del teléfono. */ @media (min-width: 641px) and (max-width: 1024px) { .fc-timeGridWeek-view .fc-scrollgrid, .fc-dayGridMonth-view .fc-scrollgrid { min-width: 0; } .fc .fc-toolbar-title { font-size: 1.05rem; } .fc .fc-button { font-size: 0.75rem; } } /* Toolbar del calendario tocable en cualquier pantalla táctil. Va por tipo de puntero y no por ancho: un iPad en landscape mide 1180px (tier desktop) pero se sigue tocando con el dedo. No se hereda el min-height de .btn (44px) para no disparar el alto de la barra de navegación del calendario. */ @media (pointer: coarse) { .fc .fc-button { min-height: 40px; } } /* Utility classes */ .card { background: #ffffff; border: 1px solid #eef0f4; border-radius: 1rem; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; font-weight: 600; border-radius: 0.65rem; padding: 0.55rem 0.95rem; font-size: 0.875rem; transition: all 0.15s; cursor: pointer; border: 1px solid transparent; white-space: nowrap; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: #3b66ff; color: #ffffff; } .btn-primary:hover:not(:disabled) { background: #2447f5; } .btn-secondary { background: #ffffff; color: #374151; border-color: #e5e7eb; } .btn-secondary:hover:not(:disabled) { background: #f9fafb; border-color: #d1d5db; } .btn-ghost { background: transparent; color: #4b5563; } .btn-ghost:hover:not(:disabled) { background: #f3f4f6; } .btn-danger { background: #fee2e2; color: #b91c1c; } .btn-danger:hover:not(:disabled) { background: #fecaca; } /* ---- Safe areas y alturas de viewport ---- */ .safe-area-bottom { padding-bottom: max(0.75rem, var(--safe-bottom)); } /* Chrome superior (headers fijos): descuenta la Dynamic Island / notch. */ .safe-top { padding-top: var(--safe-top); } /* Landscape con notch: el contenido no debe quedar bajo la esquina redondeada. */ .safe-x { padding-left: var(--safe-left); padding-right: var(--safe-right); } /* Altura de viewport estable en iOS. 100vh NO descuenta la barra de URL dinámica de Safari, así que el shell queda más alto que el área visible y la página se puede desplazar. dvh sí la descuenta; vh queda como fallback. */ .h-screen-safe { height: 100vh; height: 100dvh; } .max-h-screen-safe { max-height: 100vh; max-height: 100dvh; } .min-h-screen-safe { min-height: 100vh; min-height: 100dvh; } /* Franjas con scroll lateral (chips de filtro) sin barra visible: en móvil la barra roba alto y el gesto de arrastre ya comunica que hay más contenido. */ .scrollbar-none { scrollbar-width: none; -ms-overflow-style: none; } .scrollbar-none::-webkit-scrollbar { display: none; width: 0; height: 0; } @layer components { .input, .select, .textarea { width: 100%; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 0.65rem; padding: 0.55rem 0.75rem; font-size: 0.875rem; color: #0f172a; transition: border-color 0.15s, box-shadow 0.15s; outline: none; } .input:focus, .select:focus, .textarea:focus { border-color: #3b66ff; box-shadow: 0 0 0 3px rgba(59, 102, 255, 0.15); } /* ---- Landing pública ---- Scope propio porque el panel vive en #f6f7fb y la landing tiene su propia identidad. Va DENTRO de la capa (el default del repo) para que cualquier utilidad de Tailwind aplicada en el JSX gane por especificidad de capa. La paleta NO se inventa: son los colores que la plataforma ya usa. `--ld-azul` y `--ld-naranja` son exactamente el cuadrado y el punto del logo (`public/favicon.svg`), la tinta y los fondos son los del panel, y los seis tonos de acento son los que el seed asigna a los avatares de empleados y a los servicios (`server/lib/templates.ts`). Por eso el hilo de la página —«sus colores se vuelven sus números»— es literal: el abanico del hero trae los mismos hex que la dueña verá en sus fichas. */ .landing { /* Marca */ --ld-azul: #3b66ff; /* brand-500: primario del logo y de .btn-primary */ --ld-azul-hondo: #1c36dc; /* brand-700: el hover que ya usa el panel */ --ld-naranja: #f17616; /* accent-500: el punto del logo */ /* Acentos: los colores de avatares y servicios del panel, sin cambiar un dígito */ --ld-indigo: #6366f1; --ld-purpura: #a855f7; --ld-rosa: #ec4899; --ld-ambar: #f59e0b; --ld-esmeralda: #10b981; --ld-turquesa: #14b8a6; /* Estructura: idéntica a la del panel */ --ld-tinta: #0f172a; --ld-papel: #ffffff; --ld-lienzo: #f6f7fb; /* el fondo del body de la app */ --ld-nube: #eef4ff; /* brand-50: el mismo lavado del "hoy" en el calendario */ --ld-crema: #fff7ed; /* orange-50, ya presente en el panel */ --ld-muted: #64748b; --ld-hairline: #eef0f4; min-height: 100vh; min-height: 100dvh; overflow-x: clip; font-family: Inter, system-ui, sans-serif; color: var(--ld-tinta); background: var(--ld-papel); } /* Lavados de sección. Los tres salen del panel, así que pasar de la landing al producto no se siente como cambiar de sitio. */ .landing-papel { background: var(--ld-papel); } .landing-nube { background: var(--ld-nube); } .landing-crema { background: var(--ld-lienzo); } /* Display: Fraunces con el eje WONK abierto. Es un serif suave y con carácter, no una Didone de alto contraste: a 56px+ se lee cálido en vez de editorial-frío, que es lo que este público necesita. */ .ld-display { font-family: Fraunces, Georgia, "Times New Roman", serif; font-variation-settings: "SOFT" 40, "WONK" 1; font-size: clamp(2.5rem, 7.2vw, 5.5rem); line-height: 1; letter-spacing: -0.022em; font-weight: 800; } .ld-title { font-family: Fraunces, Georgia, "Times New Roman", serif; font-variation-settings: "SOFT" 40, "WONK" 1; font-size: clamp(1.875rem, 4.4vw, 3.15rem); line-height: 1.08; letter-spacing: -0.015em; font-weight: 800; } /* Cuerpo en Inter, la misma tipografía del panel, a 17px mínimo y 1.6 de interlínea: el público incluye personas de 50+ y la legibilidad pesa más que la densidad. */ .ld-lead { font-size: clamp(1.0625rem, 1.9vw, 1.3125rem); line-height: 1.6; font-weight: 500; } .ld-body { font-size: 1.0625rem; line-height: 1.65; } .ld-eyebrow { font-size: 0.8125rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; } .ld-section { padding-block: clamp(4.5rem, 10vh, 9rem); padding-inline: max(1.25rem, var(--safe-left)) max(1.25rem, var(--safe-right)); } .ld-wrap { width: 100%; max-width: 72rem; margin-inline: auto; } /* Canaleta lateral que además descuenta el notch en landscape. NO combines `.safe-x` con `px-*` en el mismo elemento: `.safe-x` vive fuera de `@layer` y por tanto gana a las utilidades de Tailwind, así que deja el padding en 0 siempre que el inset del sistema sea 0 — es decir, en todo lo que no sea un iPhone en landscape. El resultado es contenido pegado al borde en móvil. Esta clase resuelve las dos cosas en una sola declaración, como ya hace `.ld-section`. */ .ld-gutter { padding-inline: max(1.25rem, var(--safe-left)) max(1.25rem, var(--safe-right)); } /* Números que se interpolan frame a frame. Sin tabular-nums cada dígito cambia de ancho y la línea entera relayoutea en cada frame. Inter porque Fraunces no tiene cifras tabulares. */ .ld-num { font-family: Inter, system-ui, sans-serif; font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; } /* Botón principal: el mismo azul de `.btn-primary` del panel, con el degradado brand-500 → brand-700 (los dos tonos que la app ya usa en normal y hover). El naranja del logo no se usa como fondo de acción: en el favicon es el punto que marca, no la superficie que trabaja, y en la app tampoco es un color de botón. */ .ld-cta { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; min-height: 56px; padding-inline: 2rem; border-radius: 9999px; font-weight: 800; font-size: 1rem; color: #fff; background: linear-gradient(100deg, var(--ld-azul), var(--ld-azul-hondo)); box-shadow: 0 12px 30px -10px rgba(59, 102, 255, 0.5); transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s; } .ld-cta:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 18px 40px -12px rgba(59, 102, 255, 0.58); } .ld-cta:disabled { opacity: 0.55; cursor: not-allowed; } .ld-cta-ghost { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; min-height: 56px; padding-inline: 1.75rem; border-radius: 9999px; font-weight: 700; font-size: 1rem; color: var(--ld-tinta); background: rgba(255, 255, 255, 0.8); border: 1.5px solid #e5e7eb; transition: background 0.2s, border-color 0.2s; } .ld-cta-ghost:hover { background: #fff; border-color: #d1d5db; } /* Foco visible propio: el degradado del CTA tapa el outline por defecto. */ .landing a:focus-visible, .landing button:focus-visible { outline: 3px solid var(--ld-azul); outline-offset: 3px; } /* Titular con degradado: naranja→ámbar. Es el papel que el naranja tiene en el logo —el punto que marca el renglón importante— trasladado a la frase que carga la promesa. El color sólido va primero como fallback: `color: transparent` sin `background-clip: text` deja el texto invisible, y ese fallo se llevaría media frase del hero. */ .ld-gradient-text { color: var(--ld-naranja); } @supports ((-webkit-background-clip: text) or (background-clip: text)) { .ld-gradient-text { background-image: linear-gradient(100deg, var(--ld-naranja), var(--ld-ambar)); -webkit-background-clip: text; background-clip: text; color: transparent; } } /* ---- Gráfica de ingresos de la landing ---- Se anima con CSS y NO con framer-motion a propósito. iOS Safari suspende `requestAnimationFrame` mientras dura el scroll por inercia, y framer-motion interpola contra el reloj de pared: la animación consume su duración sin pintar un solo fotograma y al reanudarse salta al estado final. En un iPhone se ve exactamente como si nunca hubiera animado, que es el defecto que se reportó. Una animación CSS lleva su propia línea de tiempo y sí se dibuja. El easing es el mismo `EASE_EXPO` de src/lib/motion.ts; si cambia uno, cambia el otro. */ .ld-rev-linea { stroke-dasharray: 1px 1px; /* con pathLength="1" las unidades son la longitud total */ stroke-dashoffset: 1px; } .ld-rev-area { opacity: 0; } .ld-rev-punto { /* `fill-box` para que el centro del círculo sea el origen y no el del lienzo SVG. */ transform-box: fill-box; transform-origin: center; transform: scale(0); } [data-ld-rev-visible="true"] .ld-rev-linea { animation: ld-rev-trazo 1.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; } [data-ld-rev-visible="true"] .ld-rev-area { animation: ld-rev-aparecer 0.9s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards; } [data-ld-rev-visible="true"] .ld-rev-punto { animation: ld-rev-aterrizar 0.5s cubic-bezier(0.16, 1, 0.3, 1) 1.3s forwards; } } @keyframes ld-rev-trazo { from { stroke-dashoffset: 1px; } to { stroke-dashoffset: 0px; } } @keyframes ld-rev-aparecer { from { opacity: 0; } to { opacity: 1; } } @keyframes ld-rev-aterrizar { from { transform: scale(0); } to { transform: scale(1); } } /* Con movimiento reducido la gráfica sale ya dibujada. Igual que el resto de la landing, el estado inicial resuelve al FINAL y no se acorta la transición: si dejara la línea en `dashoffset: 1px` sin animación, quedaría invisible para siempre. */ @media (prefers-reduced-motion: reduce) { .ld-rev-linea, [data-ld-rev-visible="true"] .ld-rev-linea { stroke-dashoffset: 0px; animation: none; } .ld-rev-area, [data-ld-rev-visible="true"] .ld-rev-area { opacity: 1; animation: none; } .ld-rev-punto, [data-ld-rev-visible="true"] .ld-rev-punto { transform: scale(1); animation: none; } } /* ---- Anti auto-zoom de iOS (fuera de @layer para ganar a las utilidades) ---- Mobile Safari hace zoom al enfocar cualquier campo con font-size < 16px y no revierte al desenfocar: la vista queda escalada y corrida. Ese es el origen del "arranca con zoom y desplazada" al tocar el campo de correo en el login. Se fuerzan 16px en todo puntero grueso (iPhone y iPad) en lugar de bloquear el zoom con maximum-scale=1, que rompería el pinch-zoom de accesibilidad. `select` incluido: iOS también zoomea al abrir un desplegable pequeño. */ @media (pointer: coarse) { input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]), select, textarea, .input, .select, .textarea { font-size: 16px; } /* Objetivos táctiles cómodos (Apple recomienda 44 pt). */ .btn, .input, .select, .textarea { min-height: 44px; } /* Para controles compactos (chips de filtro, grupos segmentados) donde 44px rompería la densidad de la barra. Se decide por tipo de puntero y no por ancho de pantalla: un iPad se toca con el dedo igual que un iPhone, así que un breakpoint `sm:` dejaría fuera justamente a las tablets. */ .tap-target { min-height: 40px; } /* Botones de solo icono (editar, borrar, cerrar, menú): con p-1.5 miden 28px, por debajo de lo tocable con el pulgar. Se marcan con `.icon-btn` en lugar de detectarlos por selector: se intentó `button:has(> svg:only-child)` y es engañoso, porque `:only-child` solo mira hijos ELEMENTO y un botón con etiqueta (" Nueva cita") también encaja — el texto es un nodo de texto. Esa regla, al fijar `display`, convirtió los NavLink del menú lateral en inline-flex y los repartió en dos columnas. */ .icon-btn { min-height: 40px; min-width: 40px; } /* Se centra el icono, no se cambia el `display` del botón. El preflight de Tailwind pone `svg { display: block }`, así que el icono ignora `text-align` y `margin-inline: auto` es lo que lo centra en la caja ampliada. Tocar el display aquí sería contraproducente: esta regla está fuera de @layer y ganaría a las utilidades, de modo que un `hidden`/`lg:block` dejaría de funcionar y el botón de colapsar aparecería en el móvil. */ .icon-btn > svg { margin-inline: auto; } } .label { display: block; font-size: 0.78rem; font-weight: 600; color: #4b5563; margin-bottom: 0.3rem; } .chip { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.15rem 0.55rem; border-radius: 9999px; font-size: 0.72rem; font-weight: 600; }