Files
AgendaPro/src/index.css
T
AgendaPro DevandClaude Opus 5 4281567207 fix: two defects that only reproduce over a real network
Neither is visible against localhost, which is why both shipped.

**Install button never appeared on `/login`** — a regression from making the login a
lazy route. `beforeinstallprompt` fires once per page load and is never replayed;
`useInstallPrompt` attached its listener from a `useEffect`, i.e. at mount, and
`InstallAppPrompt` now mounts only after an extra round-trip for its chunk. Locally
that round-trip is a millisecond so the listener still won a race it should never
have been in. Over a real connection the event was long gone, so a user on a slow
link lost the install button entirely.

The listener now lives in `src/lib/installPrompt.ts` and registers when the module
evaluates — `main.tsx` imports it for its side effect before mounting React. The hook
only reads from that store. Reproduced deterministically by delaying
`/assets/LoginPage-*.js` by 1.5s via `route()`: absent before, present after (also at
a 4s delay). `test:pwa` against the HTTPS domain now passes.

**Revenue chart did not animate on a real iPhone.** Measured rather than guessed:
the path animation does run in WebKit, and it triggers with the chart 100% visible at
y=601..715 of an 844px viewport — so neither "broken" nor "fires too early". What
fits is that iOS Safari suspends `requestAnimationFrame` during momentum scrolling
while framer-motion interpolates against wall-clock time: the animation spends its
1.4s without painting a frame and snaps to the end on resume, which looks exactly
like it never ran.

The chart's three animations move to CSS keyframes, which keep their own timeline in
the engine. The component only decides *when* (a `useInView` setting
`data-ld-rev-visible`). `prefers-reduced-motion` resolves in CSS too, and still
resolves to the *drawn* state — a line left at `dashoffset: 1px` with no animation
would be invisible forever. Verified: `getAnimations()` returns a `CSSAnimation`, and
under reduced motion the line renders complete.

Not verified: no physical iPhone here, and Playwright WebKit on Windows does not
reproduce iOS's rAF suspension. This is the standard mitigation and changes nothing
on desktop, but on-device confirmation is still outstanding.

Verified: typecheck clean; landing 13/13, PWA passed, responsive 0 findings, visual
62 screens / 0 errors, unit 39/39, e2e 33/33, admin 17/17, booking 12/12.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 15:09:29 -06:00

774 lines
22 KiB
CSS

@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 ("<Plus/> 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;
}