/* ==========================================================================
   HOLDERA — hero landing
   Art direction: light grey studio hero, grotesk type, orange accent, and a
   warm near-black "sheet" that slides up over the hero on scroll while the
   brain point cloud sinks behind it (brain.js). Dark from the sheet to the
   footer since 08-09-2026.
   Fonts: Geist + Geist Mono (one superfamily). Colour lives only in :root (incl. alpha layers).
   Three colour groups: the light hero, --sh-* for the dark content sheet, and
   --on-ink-* for the surfaces that were dark before the sheet was (side
   drawer, floating island, hero bar).
   Named exceptions: 6px double-bezel padding; hero entrance choreography at 900/1100ms.
   ========================================================================== */

/* Geist + Geist Mono, alojadas aquí (SIL OFL). Generado por _build/fetch-fonts.js
   el 2026-09-08: subconjuntos latin y latin-ext, variables en peso. */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("assets/fonts/geist-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("assets/fonts/geist-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("assets/fonts/geist-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("assets/fonts/geist-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --font: "Geist", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* light hero */
  --bg: #e5e5e5;
  --bg-2: #f1f1f1;
  --bg-3: #cbcbcb;
  --surface: #ededed;
  --surface-2: #f7f7f7;
  --text: #141414;
  --text-2: #3d3d3d;
  --text-muted: #666666;
  --text-fade: #7a7a7a;
  --border: rgba(20, 20, 20, 0.10);
  --border-strong: rgba(20, 20, 20, 0.22);
  --white: #ffffff;
  --white-40: rgba(255, 255, 255, 0.40);
  --white-70: rgba(255, 255, 255, 0.70);

  /* ink (buttons, marks, bar) */
  --ink: #101010;
  --ink-2: #1d1d1d;
  --ink-hi: rgba(255, 255, 255, 0.12);

  /* accent */
  --accent: #f08a24;
  --accent-hover: #ff9a37;
  /* --accent-ink is "the accent, safe as TEXT on the content sheet". Its VALUE
     flips with the sheet, its meaning does not — which is why inverting the
     sheet needed no selector changes here. On the old near-white sheet #f08a24
     measured 2.42:1 and failed AA, so it was darkened to #a8530a (5.19:1). On
     the dark sheet the failure is the other way round — #a8530a measures 2.5:1
     on #121110 — so it is lightened instead. Decorative accent (dots, halos,
     pulses, the fish rim) still uses --accent. */
  --accent-ink: #9c4c08;
  /* The accent as TEXT on the LIGHT HERO. It exists because --accent-ink could
     not simply be flipped when the sheet went dark: one of its consumers was
     never on the sheet at all. #f08a24 measures 2.42:1 on the hero grey; this
     is the old --accent-ink value kept for exactly that job.
     Darkened from #a8530a on 08-09-2026 because #a8530a is 4.27:1 on the hero
     grey — enough for a decorative mark (3:1), not for body text. #9c4c08 is
     4.83:1.
     Since the pivot (19-09-2026) it has ONE consumer left: the white tick that
     accent-color draws inside the consent checkbox, 6.09:1 on this fill. The
     five hero stars that used to be the other one are gone — they announced a
     rating nobody had confirmed. */
  --accent-hero: var(--accent-ink);
  --accent-soft: rgba(240, 138, 36, 0.18);

  /* Perlin wave field (waves.js reads both). The thread is WHITE, not ink, and
     that is an accessibility decision rather than a taste one. The palest stop
     of the H1 gradient (--text-fade #7a7a7a) measures 3.41:1 on the hero grey,
     and the house floor for large text is 3:1 — there is almost no headroom.
     An ink hairline behind that word drops the local ground to ~#d5d5d5 and the
     ratio to 2.93:1, i.e. a fail. A white hairline LIFTS the local ground
     instead, so every piece of hero text ends up with MORE contrast than it had
     before the field existed, not less. Measured, not assumed:
     _build/contrast.js checks the woven ground. */
  /* There is no --wave-line-on-dark. There was, and it could never be seen:
     the field is z-index 0 inside the hero and the showcase veil is z-index 3
     at 93% opacity, so only 7% of the thread survives — about 1/255. Recolouring
     something that is covered is dead code, so the whole tone-lerp went with it. */
  --wave-line: rgba(255, 255, 255, 0.62);
  /* The field is masked so it reads as weather rather than wallpaper: full in
     the lower middle where the cloud and the wordmark live, gone at the top
     corners where the nav and the card sit. */
  --wave-mask: radial-gradient(116% 98% at 54% 82%,
    rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 34%, rgba(0, 0, 0, 0.72) 58%,
    rgba(0, 0, 0, 0.30) 79%, rgba(0, 0, 0, 0) 97%);

  /* Service showcase (directional hover reveal). The veil is what turns the
     light hero dark while a service is held: warm near-black, same family as
     the content sheet, never #000000. */
  /* The big title sits ON TOP of the artwork, so it has to survive whatever
     lands under it. This is the shadow that buys it its contrast back.
     Widened and deepened on 19-09-2026, and measured, not guessed: the art is
     no longer five stock photographs but five screenshots of the panel, and
     the panel is now a LIGHT interface. With the old 58%/62% core the ends of
     a long word ("Revenue y reservas" is 634px of glyphs) fell outside the
     dense part of the gradient and landed on rgb(186,186,185) — 1.75:1 against
     #f5f3ef, where a 75px title owes 3:1. Measured with _build/png-probe.js on
     the real pixels, with the title made transparent so the probe reads the
     BACKGROUND and not the glyphs themselves. Four stops rather than three so
     the edge of the scrim is not a visible disc. */
  /* There is no --sc-img-shadow either. clip-path clips an element's ENTIRE
     rendering, outer box-shadows included, and .sc__img is clipped by
     definition — even fully open, its polygon is exactly the border box. Two
     drop shadows were being declared and clipped to nothing on every panel.
     The panels are separated from the backdrop by the inset hairline below. */

  /* planet — --planet-* are read by planet.js, --orbit-title is the resting
     colour of a step title before the scroll wipe brightens it */
  /* The resting colour of a step title, before the scroll wipe brings the
     bright copy in over it. It is the FALLBACK reading of the word, not a
     shadow: at 38-108px this is WCAG large text and owes 3:1. The first value
     tried here, #4a443c, measured 1.96:1 and the guard caught it. 4.52:1 keeps
     a clear step down from the wiped-in --sh-text (17.02:1) with room to spare. */
  --orbit-title: #847b6c;

  /* The mobile hero->sheet seam. On desktop the sheet SLIDES OVER the hero as
     a rounded card with a shadow, so the boundary is a designed edge. Below
     901px there is no pin and no overlap (main{margin-top:0}), so light grey
     would meet near-black on a straight line — banned by the house floor,
     which asks for >=3 intermediate stops or >=120px of overlap. These are
     the three stops; the ramp below runs over 200px. */
  --seam-1: #e9e7e4;
  --seam-2: #f0eeea;
  --seam-3: #f4f2ee;

  /* brain hologram — read by brain.js, which is why they live here and not in
     the shader. The cloud is shaded by measured surface curvature: crowns of
     the gyri take --brain-crown, the sulci between them take --brain-sulcus.
     Two warm neutrals a long way apart in value and close in hue, so the folds
     read as structure without introducing a second colour to the palette. */
  /* These are the colours the shader MIXES, not the colours you see: each point
     is drawn at partial alpha over --bg #e5e5e5, which lifts it a long way. A
     crown at #3a342c composites to roughly #6a625a on the hero, which is the
     mid tone we actually want. Picking the on-screen value here instead gives a
     dusty, washed-out cloud — that was the first attempt. */
  --brain-crown: #3a342c;
  --brain-sulcus: #0c0b0a;

  /* Ya no hay --brand-*. Eran los ocho colores de marca (OpenAI, Anthropic,
     Google Cloud, Azure, n8n, Make, HubSpot, Zapier) que tintaban el logotipo
     al pasar el ratón, y se fueron con la tira de logotipos en el pivote a
     hoteles (19-09-2026): en un producto de operaciones hoteleras esos ocho
     logotipos afirmaban integraciones que no existen, y dos de ellos afirmaban
     IA que el producto no lleva. Los generaba _build/build-partner-logos.js
     junto con el marcado; si alguna vez vuelven, vuelven desde ahí. */

  /* content sheet (DARK) — slides up over the light hero.
     Inverted 07-09-2026 on Alex's brief: the hero stays light grey, and from the
     moment the sheet rises everything is dark to the footer. The token NAMES are
     unchanged on purpose — every rule in this file already reads the sheet's
     colour from --sh-*, so the inversion is a palette swap, not a rewrite.
     Warm near-blacks, never #000000 (banned): the sheet has to sit under a warm
     grey hero and a #f08a24 accent without turning blue. */
  --sh-bg: #f7f5f1;
  --sh-bg-2: #f2efea;
  --sh-surface: #ffffff;
  --sh-surface-2: #fbf9f5;
  --sh-text: #16130f;
  --sh-text-2: #3a352d;
  --sh-muted: #5f594e;
  /* La parada más clara del degradado de un titular sobre la hoja: el
     espejo cálido de --text-fade (#7a7a7a), que es la misma parada sobre
     el hero. Sobre la hoja clara mide 4,30:1 y sobre una tarjeta 4,68:1 —
     mejor que el 3,41:1 del original, porque la hoja es más clara que el
     gris del hero. Un titular es texto grande y debe 3:1. */
  --sh-fade: #7a736a;
  --sh-border: rgba(22, 19, 15, 0.10);
  --sh-border-strong: rgba(22, 19, 15, 0.20);
  /* Form fields still need their own, heavier border, and for the mirror of the
     old reason: a #1c1a17 field on a #191715 panel is ~1.06:1, so the border is
     the ONLY thing identifying the control and WCAG 1.4.11 asks for 3:1. Kept
     separate from --sh-border-strong so the .tag pills stay a hairline. */
  --sh-field-border: rgba(22, 19, 15, 0.56);
  /* The boundary of a CONTROL whose only affordance is its outline — the ghost
     and secondary buttons. --sh-border-strong is a decorative hairline and
     composites to just 1.95:1 on --sh-bg, under the 3:1 WCAG 1.4.11 asks of
     anything needed to identify a control. (It failed on the old light sheet
     too, at ~1.5:1 — this is a fix, not a regression.) 0.36 is the lowest step
     that clears 3:1 against ALL FOUR sheet backgrounds: 3.24 / 3.27 / 3.27 /
     3.23. Do not lower it without re-running _build/contrast.js. */
  --sh-control-border: rgba(22, 19, 15, 0.46);
  --sh-tint: rgba(22, 19, 15, 0.035);
  /* On a dark surface a white inset bevel finally means something — it measured
     1.00:1 on the old near-white sheet and was removed. This is the light-source
     highlight along the top edge of a raised card. */
  --sh-bevel: none;

  /* on ink — the surfaces that stay dark on a light page:
     side drawer, floating island, hero bar, terminal */
  --on-ink-text: #16130f;
  --on-ink-text-2: #3a352d;
  --on-ink-muted: #5f594e;
  --on-ink-border: rgba(22, 19, 15, 0.10);
  --on-ink-border-strong: rgba(22, 19, 15, 0.20);
  --on-ink-hi: rgba(22, 19, 15, 0.055);

  /* alpha layers (white / black / brand tints) — every translucent colour used below */
  --w-0: rgba(255, 255, 255, 0);
  --w-28: rgba(255, 255, 255, 0.28);
  --w-35: rgba(255, 255, 255, 0.35);
  --w-60: rgba(255, 255, 255, 0.60);
  /* the inset hairline of the showcase panels */
  --w-22: rgba(255, 250, 242, 0.22);
  --w-90: rgba(255, 255, 255, 0.90);
  --k-0: rgba(0, 0, 0, 0);
  --k-06: rgba(0, 0, 0, 0.06);
  --k-08: rgba(0, 0, 0, 0.08);
  /* black alpha on a LIGHT surface — the only one left is the arrow ring on the
     accent panel of the hero card. It was removed with the viz strokes and came
     back when that panel needed it. */
  --k-24: rgba(0, 0, 0, 0.24);
  --text-a0: rgba(20, 20, 20, 0);
  --text-a06: rgba(20, 20, 20, 0.06);
  --text-a14: rgba(20, 20, 20, 0.14);
  --accent-a0: rgba(240, 138, 36, 0);
  --accent-a09: rgba(240, 138, 36, 0.09);
  --accent-a10: rgba(240, 138, 36, 0.10);
  --bar-a: rgba(255, 255, 255, 0.94);
  --bar-b: rgba(255, 255, 255, 0.80);
  /* the fixed nav's glass: light while it rides over the hero / the page band,
     dark once the sheet has reached it (paso 6) */
  --nav-glass-light: rgba(229, 229, 229, 0.82);
  --nav-glass: rgba(247, 245, 241, 0.82);
  --panel: #ffffff;

  /* shape, motion, layout */
  --r-xl: 32px;
  --r-lg: 24px;
  --r-md: 16px;
  --r-sm: 10px;
  --r-pill: 999px;
  --r-sheet: 40px;
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-in: cubic-bezier(0.55, 0, 1, 0.45);
  --t-fast: 200ms;
  --t-mid: 420ms;
  --t-slow: 900ms;
  --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px -8px rgba(0, 0, 0, 0.10), 0 28px 56px -24px rgba(0, 0, 0, 0.16);
  --shadow-ink: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 8px 20px -10px rgba(0, 0, 0, 0.55), 0 24px 44px -20px rgba(0, 0, 0, 0.45);
  --shadow-dk: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 30px 80px -30px rgba(0, 0, 0, 0.7);
  /* Elevation on the DARK sheet. A drop shadow lifts a light card off a light
     ground; on a dark ground there is nothing left to darken, so the card is
     raised by a lit TOP EDGE instead and the shadow only deepens the seat.
     That is why --shadow-sh leads with the bevel. */
  --shadow-sh: 0 1px 2px rgba(22, 19, 15, 0.05), 0 8px 20px -10px rgba(22, 19, 15, 0.10), 0 28px 56px -28px rgba(22, 19, 15, 0.14);
  /* The sheet's own edge, where the dark sheet meets the light hero above it.
     Here a dark shadow DOES work — it is cast upward onto the light hero — and
     the hairline is light so the sheet reads as a lid with a lit rim. */
  --shadow-sheet: 0 -1px 0 rgba(22, 19, 15, 0.07), 0 -14px 34px -14px rgba(22, 19, 15, 0.12), 0 -44px 110px -24px rgba(22, 19, 15, 0.10);
  --nav-h: 76px;
  /* 80px, no 46: la barra inferior dejo de ser cuatro chips de texto y es
     la tira giratoria de integraciones, que necesita alto para los logos.
     🔴 Se sube AQUI, en :root, y no dentro de .hero__bar--mrq. Motivo: la
     linea 494 reserva el suelo de .hero__left con
     calc(var(--bar-inset) + var(--bar-h) + 26px) leyendo la RAIZ. Si la
     altura se reescribe solo en la barra, la columna izquierda sigue
     reservando 46 y se mete 34px dentro de la tira. */
  --bar-h: 80px;
  --bar-inset: 20px;
  --gutter: clamp(20px, 2.5vw, 36px);
  --z-nav: 30;   /* above the sheet (10) and the footer, below grain, drawer and skip link */
  --z-sheet: 10;
  --z-grain: 60;
  --z-drawer: 80;

  color-scheme: light;
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
/* scroll-padding-top existe porque la cabecera es FIJA (paso 6). Sin esto, un
   salto de ancla deja el elemento a top:0 y la barra le tapa los primeros
   --nav-h px: medido en movil, cada seccion perdia 64px justos y el rotulo
   («SERVICIOS», «CONTACTO») quedaba cortado bajo el cristal. Afecta a las
   siete paginas y tambien a escritorio, donde --nav-h es 76px. */
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; scroll-padding-top: calc(var(--nav-h) + 12px); }
html.is-locked { overflow: hidden; }
body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.6;
  /* --sh-text, not --text: the background here is a SHEET token and is now
     dark. .hero sets its own color: var(--text), so the light hero is
     unaffected by this. */
  color: var(--sh-text);
  background: var(--sh-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { max-width: 100%; display: block; }
[hidden] { display: none !important; }
a { color: inherit; }
h1, h2, h3 { margin: 0; font-weight: 500; }
button { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 4px; }
/* The sheet is DARK now, so an --ink (#101010) focus ring on it is invisible —
   this line used to be the light-sheet counterpart of the drawer rule below and
   flipped with the sheet. --sh-text measures 17:1 on --sh-bg. */
.sheet :focus-visible, .footer :focus-visible { outline-color: var(--sh-text); }
.drawer :focus-visible { outline-color: var(--on-ink-text); }
.container { width: min(1240px, 100% - 2 * var(--gutter)); margin-inline: auto; }

.skip-link {
  position: fixed; top: 12px; left: 12px; z-index: 100;
  padding: 8px 16px; border-radius: var(--r-pill);
  background: var(--ink); color: var(--white); font-size: 13px; text-decoration: none;
  transform: translateY(-200%); transition: transform var(--t-fast) var(--ease);
}
.skip-link:focus { transform: none; }

.grain {
  position: fixed; inset: 0; z-index: var(--z-grain); pointer-events: none; opacity: 0.045;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   Buttons — one 3-variant system
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: 12px;
  border-radius: var(--r-pill); border: 1px solid transparent;
  font-size: 13px; font-weight: 500; letter-spacing: 0.01em; line-height: 1;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: transform var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), box-shadow var(--t-mid) var(--ease);
}
.btn:active { transform: scale(0.98); }
.btn__icon {
  width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
  background: var(--ink-hi);
  transition: transform var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.btn--primary { padding: 6px 6px 6px 20px; background: var(--ink); color: var(--white); box-shadow: var(--shadow-ink); }
.btn--primary:hover { background: var(--ink-2); }
.btn--primary:hover .btn__icon { transform: translate(2px, -1px) scale(1.06); background: var(--accent); }
.btn--secondary { padding: 12px 24px; border-color: var(--border-strong); color: var(--text); background: var(--white-40); }
.btn--secondary:hover { background: var(--white-70); border-color: var(--text-2); }
.btn--ghost { padding: 12px 8px; color: var(--text-2); background: transparent; }
.btn--ghost:hover { color: var(--text); }
/* dark-surface variants */
.btn--on-dark.btn--primary { background: var(--white); color: var(--ink); box-shadow: var(--shadow-dk); }
.btn--on-dark.btn--primary .btn__icon { background: var(--k-08); }
.btn--on-dark.btn--primary:hover { background: var(--surface-2); }
.sheet .btn--secondary { border-color: var(--sh-control-border); color: var(--sh-text); background: transparent; }
.sheet .btn--secondary:hover { background: var(--sh-tint); }
.btn--on-dark.btn--secondary { border-color: var(--on-ink-border-strong); color: var(--on-ink-text); background: transparent; }
.btn--on-dark.btn--secondary:hover { background: var(--on-ink-hi); border-color: var(--on-ink-text-2); }
/* size / shape modifiers */
.btn--sm { min-height: 40px; padding: 8px 16px 8px 12px; gap: 8px; font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.btn--icon { width: 40px; height: 40px; padding: 0; justify-content: center; }

.burger { position: relative; width: 16px; height: 10px; display: inline-block; }
.burger i {
  position: absolute; left: 0; width: 100%; height: 1.5px; border-radius: 2px; background: currentColor;
  transition: transform var(--t-fast) var(--ease), top var(--t-fast) var(--ease);
}
.burger i:first-child { top: 0; }
.burger i:last-child { top: 8.5px; }
.burger.is-x i:first-child { top: 4.25px; transform: rotate(45deg); }
.burger.is-x i:last-child { top: 4.25px; transform: rotate(-45deg); }
[aria-expanded="true"] .burger i:first-child { top: 4.25px; transform: rotate(45deg); }
[aria-expanded="true"] .burger i:last-child { top: 4.25px; transform: rotate(-45deg); }

/* --------------------------------------------------------------------------
   Hero stage
   -------------------------------------------------------------------------- */
.hero-stage { position: relative; height: 250dvh; }
.hero-sentinel { position: absolute; top: 150dvh; left: 0; width: 1px; height: 1px; } /* = where the sheet fully covers the hero */
.hero {
  position: sticky; top: 0; height: 100dvh; min-height: min(640px, 100dvh); overflow: hidden;
  color: var(--text);
  background:
    radial-gradient(120% 95% at 50% 32%, var(--bg-2) 0%, var(--bg) 46%, var(--bg-3) 100%);
}
.hero::after { /* soft vignette */
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, var(--w-35) 0%, var(--w-0) 22%, var(--k-0) 78%, var(--k-06) 100%);
  /* the white lift at the top would sit ON the dark showcase and grey it out */
  opacity: 1; transition: opacity var(--t-mid) var(--ease);
}

/* Perlin wave field — z-index 0, i.e. under the cloud (1), the copy (2), the
   bar (4) and the nav (5). It never covers anything. See --wave-line for why
   the thread is white. */
.hero__waves { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.hero__waves canvas {
  display: block; width: 100%; height: 100%;
  -webkit-mask-image: var(--wave-mask); mask-image: var(--wave-mask);
}

/* Nav — FIXED since paso 6: the bar follows the scroll on every page. It sits
   at the top of <body>, outside the hero (which goes inert once the sheet
   covers it). Three states written by script.js:
     is-scrolled   past 8px: a light glass so the bar stays legible over the
                   page band / the pinned hero, and it compresses to 64px
     is-over-sheet the dark sheet has reached the bar: dark glass, on-ink text
     is-veiled     the service showcase has darkened the hero: on-ink text */
.nav {
  position: fixed; inset: 0 0 auto 0; height: var(--nav-h); z-index: var(--z-nav);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 0 var(--gutter);
  /* its own ink: as a child of <body> it would inherit the SHEET's light text
     (the brand name vanished on the light hero the first time round) */
  color: var(--text);
  background: transparent;
  transition: background-color var(--t-mid) var(--ease), height var(--t-mid) var(--ease),
              box-shadow var(--t-mid) var(--ease), opacity var(--t-slow) var(--ease), transform 1100ms var(--ease);
}
.nav.is-scrolled {
  height: 64px;
  background: var(--nav-glass-light);
  backdrop-filter: blur(18px) saturate(1.1); -webkit-backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 1px 0 var(--border);
}
.nav.is-over-sheet {
  background: var(--nav-glass);
  box-shadow: 0 1px 0 var(--on-ink-border);
}
/* entrance: the nav is no longer under .hero [data-enter], so it has its own;
   script.js adds is-ready with the hero's, or at once on pages without one */
.has-js .nav[data-enter] { opacity: 0; transform: translateY(-10px); transition-delay: 120ms; }
.has-js .nav[data-enter].is-ready { opacity: 1; transform: none; transition-delay: 0s; }
.nav__brand {
  justify-self: start; display: inline-flex; align-items: center; gap: 12px;
  font-size: 17px; font-weight: 600; letter-spacing: -0.01em; text-decoration: none;
}
.nav__mark {
  width: 30px; height: 30px; border-radius: var(--r-sm); background: var(--ink); display: grid; place-items: center;
  box-shadow: var(--shadow-ink);
}
.nav__mark img { width: 11px; height: auto; }
.nav__links { display: flex; align-items: center; gap: 28px; font-size: 13px; font-weight: 500; }
.nav__links > a {
  position: relative; display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 0; color: var(--text-2); text-decoration: none; background: none; border: 0; cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}
.nav__links > a:hover { color: var(--text); }
.nav__links .is-active { color: var(--text); }
.nav__links .is-active::after {
  content: ""; position: absolute; left: 50%; bottom: -1px; width: 4px; height: 4px; border-radius: 50%;
  background: var(--text); transform: translateX(-50%);
}
.nav__right { justify-self: end; display: flex; align-items: center; gap: 20px; }
.nav__tz { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.nav__tz-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.18em; color: var(--text-muted); }
.nav__tz-time { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* Idioma (paso 15): ES | EN, pegado a la marca. A la DERECHA no cabe: entre
   1121 y 1440px el reloj y el botón Menú ya llenan su columna al píxel, y como
   la barra es 1fr auto 1fr, cualquier ancho de más ahí descentra los enlaces
   del medio. A la izquierda sobra sitio en móvil y desde ~1245px; entre 901 y
   1244 se esconde de la barra y queda en el drawer, que existe en todo ancho.
   Medido con _build/i18n/nav-fit.js: con esto los enlaces no se mueven ni un
   píxel en ningún ancho. Son enlaces normales a la página gemela: no se guarda
   nada en el navegador (privacidad §8). */
.nav__start { justify-self: start; display: flex; align-items: center; gap: 16px; }
.lang {
  display: inline-flex; align-items: center; gap: 2px; padding: 2px;
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.06em; line-height: 1;
}
.lang__opt {
  position: relative; display: inline-grid; place-items: center; min-width: 28px; height: 24px; padding: 0 6px;
  border-radius: var(--r-pill); color: var(--text-muted); text-decoration: none;
  transition: color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
/* 44px de alto para el dedo sin que la pastilla crezca */
.lang__opt::after { content: ""; position: absolute; inset: -10px -1px; }
.lang__opt:hover { color: var(--text); }
.lang__opt[aria-current="true"], .lang__opt[aria-current="true"]:hover { background: var(--ink); color: var(--white); }
.nav.is-veiled .lang, .nav.is-over-sheet .lang, .drawer .lang { border-color: var(--on-ink-border-strong); }
.nav.is-veiled .lang__opt, .nav.is-over-sheet .lang__opt, .drawer .lang__opt { color: var(--on-ink-muted); }
.nav.is-veiled .lang__opt:hover, .nav.is-over-sheet .lang__opt:hover, .drawer .lang__opt:hover { color: var(--on-ink-text); }
.nav.is-veiled .lang__opt[aria-current="true"], .nav.is-over-sheet .lang__opt[aria-current="true"],
.drawer .lang__opt[aria-current="true"] { background: var(--ink); color: var(--white); }
.drawer .lang { margin-left: auto; margin-right: 12px; }
@media (min-width: 901px) and (max-width: 1244px) {
  .nav .lang { display: none; }
}

/* Hero copy (left) */
/* ONE column, not two independently anchored blocks. The copy used to be
   anchored to the TOP and the service list to the BOTTOM of the same corner,
   with nothing coupling them: at 1440x900 that happened to leave 31px of gap,
   and below ~870px of viewport height they overlapped — the list (z-index 4)
   landing on the two hero CTAs (z-index 2) and swallowing their clicks.
   Measured at 1366x768 before the fix: elementFromPoint at the centre of
   "Habla con nosotros" returned the LI of "Asesorías 1:1".
   space-between keeps the copy at the top and the list above the bar, and they
   can no longer cross at any height. */
.hero__left {
  position: absolute; left: var(--gutter); z-index: 4;
  top: clamp(152px, 21vh, 208px);
  bottom: calc(var(--bar-inset) + var(--bar-h) + 26px);
  display: flex; flex-direction: column; justify-content: space-between; gap: 28px;
  pointer-events: none;   /* the column is a spacer; its children take the clicks */
}
.hero__left > * { pointer-events: auto; }
.hero__copy { width: min(700px, 49vw); }
.eyebrow {
  display: flex; align-items: center; gap: 8px; margin: 0 0 24px;
  font-size: 12px; letter-spacing: 0.02em; color: var(--text-muted);
}
.eyebrow__bar { width: 1px; height: 14px; background: var(--text-muted); }
.hero__title {
  font-size: clamp(46px, 5.1vw, 74px); line-height: 0.98; letter-spacing: -0.035em; font-weight: 500;
  margin: 0 0 24px; color: var(--text);
}
.hero__title .line { display: block; overflow: hidden; padding-bottom: 0.06em; margin-bottom: -0.06em; }
/* The gradient lives on each LINE, not on the h1. background-clip: text only
   masks text that paints in the element's own layer, and during the entrance
   every line is a transformed (composited) inline-block: with the clip on the
   h1 the three lines slid in INVISIBLE and popped into place when the
   transition ended — caught on the mobile pass-1 screenshot (title box laid
   out, no glyphs). Each line carries its own copy of the sweep, sized to three
   lines and offset per line, so the block still darkens top-left and fades
   bottom-right as one gradient. */
.hero__title .line > span {
  display: inline-block;
  background: linear-gradient(100deg, var(--text) 0%, var(--text-2) 46%, var(--text-fade) 100%);
  background-size: 100% 300%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero__title .line:nth-child(2) > span { background-position: 0 50%; }
.hero__title .line:nth-child(3) > span { background-position: 0 100%; }
.hero__proof { display: flex; align-items: center; gap: 12px; margin: 0 0 32px; font-size: 13px; color: var(--text-2); }
.hero__actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* Scene: wordmark, figure, bar */
.hero__scene { position: absolute; inset: 0; pointer-events: none; }
.hero__wordmark-wrap {
  position: absolute; left: 0; right: 0; bottom: -0.1em; z-index: 1;
  display: flex; justify-content: center; font-size: clamp(120px, 21.5vw, 330px); line-height: 1;
}
.hero__wordmark {
  font-size: inherit; font-weight: 600; letter-spacing: -0.03em; line-height: 1; white-space: nowrap;
  background: linear-gradient(180deg, var(--text-a14) 0%, var(--text-a06) 60%, var(--text-a0) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent; user-select: none;
}
/* The cloud floats rather than standing on the bottom edge, so it centres — and
   it sits BELOW the copy (z-index 1, not 3). The robot could sit on top because
   it was an opaque, narrow, bottom-anchored silhouette; a 10.000-point halo is
   494px wide with no edge, and at 1440px there is no horizontal position that
   clears both the headline and the aside. Behind the copy the H1 glyphs stay
   solid and the cloud still reads over the HOLDERA wordmark. */
.hero__figure-wrap { position: absolute; inset: 0; z-index: 1; display: flex; justify-content: center; align-items: center; }
/* The brain hologram: a 10.000-point cloud pulled out of Brain.fbx and drawn in
   raw WebGL (see brain.js), with a poster image as the fallback.
   No aspect-ratio here: with an explicit height it only fills in the width,
   which max-width then clamps without recomputing the height — so the box was
   never actually square and the fixed-fov projection cropped the cloud. Width
   and height are both stated instead, and brain.js fits to whichever is tighter. */
.hero__figure {
  position: relative; margin: 0;
  height: min(96%, 96dvh); width: min(52vw, 96dvh);
  will-change: transform, opacity;
}
.brain__poster, .brain__canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* The poster is a CSS background rather than an <img> so a browser that paints
   the cloud never downloads it: it is fetched only with no JS (.has-js never
   lands, so the first rule matches) or when brain.js reports no WebGL. */
.brain__poster { background: center / contain no-repeat; transition: opacity 700ms var(--ease); }
html:not(.has-js) .brain__poster,
.hero__figure.is-fallback .brain__poster { background-image: url("assets/img/brain-poster.png"); }
.hero__figure.is-live .brain__poster { opacity: 0; }
.brain__canvas { display: block; opacity: 0; transition: opacity 700ms var(--ease); }
.hero__figure.is-live .brain__canvas { opacity: 1; }
/* Designed loading state, zero bytes: while the geometry is in flight the
   figure is a soft breathing disc rather than an empty hole. On a fast link
   this is gone before the hero entrance finishes; on a congested one it is
   what stands in for the cloud. Reduced motion stills it (see the block at the
   end of this file, which disables every animation). */
.hero__figure:not(.is-live):not(.is-fallback)::after {
  content: ""; position: absolute; inset: 18%; border-radius: 50%;
  background: radial-gradient(circle at 50% 45%, var(--k-06), var(--k-0) 70%);
  animation: brain-wait 2.4s var(--ease) infinite;
}
@keyframes brain-wait { 0%, 100% { opacity: 0.5; transform: scale(0.96); } 50% { opacity: 1; transform: scale(1); } }
/* Four service chips with equal gaps between them: max-content tracks laid
   out with space-between, so the spacing is even regardless of how long the
   labels are. Still a grid, so the 2x2 mobile override below just works. */
.hero__bar {
  position: absolute; left: var(--bar-inset); right: var(--bar-inset); bottom: var(--bar-inset);
  height: var(--bar-h); z-index: 4; pointer-events: auto;
  display: grid; grid-template-columns: repeat(4, max-content); justify-content: space-between; align-items: center; gap: 16px;
  padding: 0 clamp(20px, 2.4vw, 32px);
  border-radius: var(--r-md); border: 1px solid var(--on-ink-hi);
  background: linear-gradient(90deg, var(--bar-a) 0%, var(--bar-b) 50%, var(--bar-a) 100%);
  color: var(--text-2); font-size: 12px; letter-spacing: 0.01em;
  box-shadow: var(--shadow-soft);
}
.hero__bar > * { display: inline-flex; align-items: center; min-width: 0; color: inherit; }

/* Aside (right): la tarjeta del panel demo + la tarjeta de traza */
.hero__aside {
  position: absolute; right: var(--gutter); top: clamp(152px, 21vh, 208px); z-index: 2;
  width: min(372px, 27vw);
}
/* The flex lives on the INNER wrapper, not on .hero__aside: GSAP owns the
   outer element's opacity/transform on scroll and the showcase owns the
   inner one's, so neither ever writes the other's property. Same reason as
   .hero__copy-in. */
.hero__aside-in { display: flex; flex-direction: column; gap: 44px; }
.hero__copy-in, .hero__aside-in {
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease), visibility 0s;
}
.hero__figure-wrap, .hero__wordmark-wrap {
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
}
.widget {
  /* position + overflow are load-bearing for the pixel reveal: .widget__alt and
     the pixel grid are absolutely positioned with inset, and without a
     positioned ancestor here they resolved against .hero__aside and spilled
     down over whatever sits below in the column (hoy, la tarjeta de traza). */
  position: relative; overflow: hidden;
  display: block; padding: 6px; border-radius: var(--r-xl); text-decoration: none; color: inherit;
  background: var(--w-28); border: 1px solid var(--w-60);
  box-shadow: var(--shadow-soft);
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.widget:hover { transform: translateY(-3px); }
.widget__core {
  position: relative; display: grid; grid-template-columns: 96px 1fr; gap: 16px; min-height: 176px; padding: 16px;
  border-radius: calc(var(--r-xl) - 6px);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  box-shadow: inset 0 1px 1px var(--w-90), inset 0 0 0 1px var(--border);
}
.widget__mark {
  width: 96px; height: 96px; border-radius: var(--r-lg); background: var(--ink); display: grid; place-items: center;
  box-shadow: var(--shadow-ink);
}
.widget__mark img { width: 20px; height: auto; }
.widget__text { align-self: center; margin: 0; padding-right: 28px; font-size: 18px; line-height: 1.25; letter-spacing: -0.01em; font-weight: 500; color: var(--text); }
.widget__foot { grid-column: 1 / -1; display: flex; align-items: center; justify-content: flex-end; gap: 16px; margin-top: auto; }
.widget__arrow {
  width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid var(--border-strong); color: var(--text);
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.widget:hover .widget__arrow { background: var(--ink); color: var(--white); transform: translate(2px, -1px); }

/* --------------------------------------------------------------------------
   Pixelated reveal — the hero card swaps state behind a flicker of random
   square pixels (reference: "Pixelated Image Reveal").

   The reference drives it with GSAP staggers. This does it in plain JS
   (script.js) for the same reason brain.js/fish.js/planet.js exist: the page
   must not need a CDN to work. The pixels are the CARD's own colours, and the
   two states are two content states, not two photos — the site has no stock
   imagery and invents none.
   -------------------------------------------------------------------------- */
.widget__alt, .widget__pixels {
  position: absolute; inset: 6px; border-radius: calc(var(--r-xl) - 6px);
  pointer-events: none;
}
.widget__alt {
  display: none;
  grid-template-columns: 96px 1fr; gap: 16px; padding: 16px;
  background: linear-gradient(180deg, var(--accent-hover), var(--accent));
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--k-08);
}
.widget__alt-mark {
  width: 96px; height: 96px; border-radius: var(--r-lg); background: var(--w-60);
  display: grid; place-items: center;
}
.widget__alt-mark img { width: 20px; height: auto; }
.widget__alt-text {
  align-self: center; margin: 0; padding-right: 28px;
  font-size: 18px; line-height: 1.25; letter-spacing: -0.01em; font-weight: 500;
}
.widget__alt-foot {
  grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-top: auto;
}
.widget__alt-label {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
}
.widget__arrow--alt { border-color: var(--k-24); color: var(--ink); }
/* the card is a link, so the swapped-in state must not eat the click */
[data-pixel-reveal].is-swapped .widget__alt { display: grid; }

.widget__pixels { overflow: hidden; }
.widget__pixel {
  position: absolute; display: none;
  /* the pixels read as the card breaking up, so they are the card's own ink */
  background: var(--ink);
}
/* the second half of the flicker happens over the accent panel, where ink
   pixels would disappear into it */
[data-pixel-reveal].is-swapped .widget__pixel { background: var(--accent-hover); }

@media (prefers-reduced-motion: reduce) {
  /* no flicker: script.js swaps the layer directly and skips the grid entirely */
  .widget__pixels { display: none; }
}

/* --------------------------------------------------------------------------
   Trace card — the evidence chain, in the hero's right column.

   It took the slot of the eight technology logos. A logo strip on a hotel
   operations product claims integrations that do not exist, so the slot now
   carries the one claim the product can actually back: every figure opens and
   shows the four things behind it. Built to read as a sibling of .widget —
   same radius family, same hairline, same type scale, no new tokens.
   -------------------------------------------------------------------------- */
.trace__label { margin: 0 0 14px; text-align: right; font-size: 12px; color: var(--text-muted); }
.trace__list {
  list-style: none; margin: 0 0 14px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-2); overflow: hidden;
}
.trace__list li { display: flex; align-items: baseline; gap: 10px; padding: 9px 14px; }
.trace__list li + li { border-top: 1px solid var(--border); }
.trace__n {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.trace__t { font-size: 13px; font-weight: 500; letter-spacing: -0.01em; color: var(--text-2); }
.trace__link {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  font-size: 12px; font-weight: 500; color: var(--text); text-decoration: none;
}
.trace__arrow {
  display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%;
  border: 1px solid var(--border-strong); color: var(--text);
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.trace__link:hover .trace__arrow { background: var(--ink); color: var(--white); transform: translate(2px, -1px); }


/* The nav changes clothes on a dark ground: the showcase veil (is-veiled) and
   the sheet (is-over-sheet) share the on-ink palette. */
.nav.is-veiled .nav__brand, .nav.is-over-sheet .nav__brand,
.nav.is-veiled .nav__links > a, .nav.is-over-sheet .nav__links > a,
.nav.is-veiled .nav__links .is-active, .nav.is-over-sheet .nav__links .is-active { color: var(--on-ink-text); }
.nav.is-veiled .nav__links .is-active::after, .nav.is-over-sheet .nav__links .is-active::after { background: var(--on-ink-text); }
.nav.is-veiled .nav__tz-label, .nav.is-over-sheet .nav__tz-label { color: var(--on-ink-muted); }
.nav.is-veiled .nav__tz-time, .nav.is-over-sheet .nav__tz-time { color: var(--on-ink-text-2); }
/* El chip del monograma es de tinta SIEMPRE. Tenía un aclarado para
   cuando la nav pasaba sobre la hoja oscura; con toda la web clara ese
   aclarado dejaba un chip casi blanco con el monograma blanco dentro. */
.nav.is-veiled .nav__brand:hover, .nav.is-over-sheet .nav__brand:hover,
.nav.is-veiled .nav__links > a:hover, .nav.is-over-sheet .nav__links > a:hover,
/* --sh-control-border, not --on-ink-border-strong: the MENÚ button is a ghost
   control whose ONLY affordance is its outline, so WCAG 1.4.11 asks 3:1.
   --on-ink-border-strong composites to 1.64:1 on the veil and the guard caught
   it. This is the same distinction the content sheet already makes between a
   decorative hairline and a control's boundary. */
.nav.is-veiled .btn--secondary, .nav.is-over-sheet .btn--secondary {
  border-color: var(--sh-control-border); color: var(--on-ink-text); background: transparent;
}
.nav.is-veiled .btn--secondary:hover, .nav.is-over-sheet .btn--secondary:hover { background: var(--on-ink-hi); border-color: var(--on-ink-text-2); }
.nav.is-veiled :focus-visible, .nav.is-over-sheet :focus-visible { outline-color: var(--on-ink-text); }
.nav__brand, .nav__links > a, .nav__tz-label, .nav__tz-time, .nav__mark {
  transition: color var(--t-mid) var(--ease), background-color var(--t-mid) var(--ease);
}

/* --------------------------------------------------------------------------
   Load-in choreography (gated by .has-js so content shows without JS)
   -------------------------------------------------------------------------- */
.has-js .hero [data-enter] {
  opacity: 0; transform: translateY(22px);
  transition: opacity var(--t-slow) var(--ease), transform 1100ms var(--ease);
  transition-delay: calc(var(--d, 0) * 1ms + 120ms);
}
.has-js .hero .hero__title .line > [data-enter] { transform: translateY(110%); }
.has-js .hero .hero__figure[data-enter] { transform: translateY(40px) scale(0.97); }
.has-js .hero.is-ready [data-enter],
.has-js .hero.is-ready .hero__title .line > [data-enter],
.has-js .hero.is-ready .hero__figure[data-enter] { opacity: 1; transform: none; }
/* GSAP owns .hero__figure transform once ready; keep the transition off then */
.has-js .hero.is-ready.is-scrolling [data-enter] { transition: none; }
/* …and once the entrance has finished (is-settled, 2.1 s after is-ready) the
   two elements GSAP writes to with the scroll never transition again. Before
   this, scrolling back to the top re-armed the 900 ms entrance transition on
   top of the scrub's inline writes and the nav stayed invisible for up to
   ~1.8 s — long enough to read as "the menu is gone". */
.has-js .hero.is-settled .hero__wordmark { transition: none; }

/* --------------------------------------------------------------------------
   Content sheet (dark) — slides up over the sticky hero
   -------------------------------------------------------------------------- */
main {
  position: relative; z-index: var(--z-sheet); margin-top: -100dvh;
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  /* clip, NOT hidden: overflow:hidden turns main into a scroll container and
     any position:sticky inside it (the services deck) stops sticking to the
     window. overflow:clip clips the same way without that side effect. */
  overflow: clip;
  background: var(--sh-bg); color: var(--sh-text);
  box-shadow: var(--shadow-sheet);
}
.sheet { position: relative; padding: clamp(96px, 12vw, 168px) 0; }
.sheet--process {
  background: linear-gradient(180deg, var(--sh-bg-2) 0%, var(--sh-bg-2) calc(100% - 200px), var(--sh-bg) 100%);
}
.sheet--cta {
  background:
    radial-gradient(60% 50% at 80% 60%, var(--accent-a09) 0%, var(--accent-a0) 60%),
    var(--sh-bg);
}
.tag {
  display: inline-flex; align-items: center; gap: 10px; margin: 0 0 28px;
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 400;
  color: var(--sh-text-2);
}
.tag::before { content: ""; width: 1px; height: 13px; background: var(--accent-ink); }
/* Mismo tratamiento que .hero__title y .phead__title: un solo degradado de
   tres paradas sobre el título ENTERO. Antes el apagado lo hacía un <span>
   a mitad de frase y se leía como un cambio de fuente.
   El interlineado baja de 1.02 a 0.98, que es el del hero y el de las
   cabeceras de página: los tres titulares grandes del sitio se componen
   ahora igual. */
.sheet__title {
  max-width: 15ch; margin: 0 0 28px;
  font-size: clamp(38px, 5vw, 74px); line-height: 0.98; letter-spacing: -0.035em; font-weight: 500;
  background: linear-gradient(100deg, var(--sh-text) 0%, var(--sh-text-2) 46%, var(--sh-fade) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  text-wrap: balance;
}
/* `.fade` ya no pinta nada: su padre lleva el degradado y lo recorta sobre
   todo el texto, hijos incluidos. El span hereda `color: transparent` y el
   degradado del padre se ve a través, así que los 42 <span class="fade">
   de las 15 páginas siguen en su sitio sin tocar ni una. Se conserva como
   gancho por si vuelve a hacer falta distinguir la segunda mitad. */
.fade { color: inherit; }
.sheet__lead { max-width: 58ch; margin: 0; font-size: clamp(16px, 1.3vw, 19px); line-height: 1.6; color: var(--sh-text-2); }

/* Process */
/* --------------------------------------------------------------------------
   Orbit — "Cómo trabajamos": the four steps sliding past a spinning planet.

   Rebuild of the reference "Scroll-Spinning Earth" component. Two things there
   are reproduced exactly, both clip-path animations:

     1. TITLE WIPE. Each title is two stacked copies. The bottom one is quiet;
        the top one is clipped from the right with inset(0 X% 0 0) and X runs
        100 -> 0 as the title crosses the viewport, so the bright copy wipes in
        left to right. The reference drives it with framer-motion's useScroll;
        here it is a --wipe custom property written by script.js.

     2. DESCRIPTION BAND. The reference opens a full-bleed bar with
        clip-path: inset(50% 0 50%) -> inset(0 0 0), i.e. it splits open from a
        centre line. Same here — on hover AND on focus-within, so it is
        reachable from the keyboard, which the reference's onMouseOver-only
        version is not.

   The reference's red bar becomes --accent; its beige titles become the sheet's
   own text tokens. No colour is carried over from the reference.
   -------------------------------------------------------------------------- */
.orbit { position: relative; margin: clamp(48px, 6vw, 88px) 0 clamp(40px, 5vw, 72px); }
.orbit__sky {
  position: absolute; inset: 0; display: grid; place-items: center;
  pointer-events: none;
}
.orbit__gl {
  width: min(78vh, 62vw, 620px); height: min(78vh, 62vw, 620px);
  display: block; opacity: 0; transition: opacity 1100ms var(--ease);
}
.orbit.is-live .orbit__gl { opacity: 1; }

.orbit__steps {
  position: relative; list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column;
}
.ostep { position: relative; padding: clamp(6px, 1vw, 14px) 0; }
/* Focusing a step OPENS its accent band, so the ring is drawn against orange,
   not against the sheet — hence --ink (7.58:1 on --accent) rather than the
   sheet's usual light ring, which would sit at 2.3:1 on that band.
   Do NOT add a companion `.ostep:focus { outline: none }` to suppress a double
   ring: it has the same specificity as this rule and, coming later, silently
   removed the focus indicator altogether. */
.has-js .ostep:focus-visible { outline: 2px solid var(--ink); outline-offset: -6px; border-radius: var(--r-sm); }
.ostep__row {
  width: min(1240px, 100% - 2 * var(--gutter)); margin-inline: auto;
  display: flex; align-items: center; gap: clamp(14px, 1.6vw, 26px);
}
.ostep__num {
  font-family: var(--font-mono); font-size: clamp(11px, 0.9vw, 13px);
  letter-spacing: 0.14em; color: var(--accent-ink); flex: none;
}
.ostep__title {
  position: relative; margin: 0;
  font-size: clamp(38px, 7.2vw, 108px); line-height: 1.02;
  letter-spacing: -0.04em; font-weight: 500; text-transform: uppercase;
  color: var(--orbit-title);
}
/* The bright copy, wiped in from the left as the title scrolls up. It is a
   PSEUDO-ELEMENT reading data-word, not a second span: two spans with the
   same word and no space between them were crawled as one glued token
   («EscuchamosEscuchamos»). The "/ \"\"" alt keeps the generated text out of
   the accessible name, so a screen reader hears the word once. */
.ostep__title::before {
  content: attr(data-word) / "";
  position: absolute; inset: 0; color: var(--sh-text);
  clip-path: inset(0 100% 0 0);
  pointer-events: none;
}
.has-js .ostep__title::before { clip-path: inset(0 calc(var(--wipe, 100) * 1%) 0 0); }
/* Without JS --wipe never arrives, so the fallback above would clip the bright
   copy away entirely. That is fine — the title's own text carries the word.
   The title is never invisible. */

.ostep__desc {
  margin: 0; font-size: 14px; line-height: 1.55; color: var(--sh-muted);
  width: min(1240px, 100% - 2 * var(--gutter)); margin-inline: auto;
  max-width: 58ch; padding-top: 6px;
}

/* ---- the accent band, on hover and on focus ---- */
.has-js .ostep__desc {
  /* The band fills the WHOLE step, not a slim strip across its middle. A strip
     was the first attempt and it broke the title: the word turns --ink when the
     band opens, so every part of it above and below the strip became dark ink
     on the dark sheet — invisible. Covering the full row keeps the darkened
     word legible over orange for its entire height. */
  position: absolute; inset: 0; z-index: 2;
  width: 100%; max-width: none; margin: 0;
  display: flex; align-items: center; justify-content: flex-end;
  padding: 0 var(--gutter);
  background: var(--accent); color: var(--ink);
  font-size: clamp(14px, 1.15vw, 18px); line-height: 1.45; text-align: right;
  clip-path: inset(50% 0 50%);
  transition: clip-path 420ms var(--ease);
  pointer-events: none;
}
/* the copy sits to the RIGHT of the big word, as in the reference */
.has-js .ostep__desc { padding-left: 52%; }
.has-js .ostep:hover .ostep__desc,
.has-js .ostep:focus-within .ostep__desc { clip-path: inset(0 0 0); }
/* the row has to sit above the band so the big word is never covered by it */
.has-js .ostep__row { position: relative; z-index: 3; }
/* ...and the band must not wash out the word it belongs to */
.has-js .ostep:hover .ostep__title,
.has-js .ostep:focus-within .ostep__title { color: var(--ink); }
.has-js .ostep:hover .ostep__title::before,
.has-js .ostep:focus-within .ostep__title::before { color: var(--ink); }
.has-js .ostep:hover .ostep__num,
.has-js .ostep:focus-within .ostep__num { color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .orbit__gl { transition: none; }
  .has-js .ostep__desc { transition: none; }
  /* a wipe that cannot animate must not hide the bright copy either */
  .has-js .ostep__title::before { clip-path: inset(0 0 0 0); }
}

@media (max-width: 900px) {
  /* No hover on touch, so the band would never open. The description goes back
     to being a plain paragraph under its title. */
  .has-js .ostep__desc {
    position: static; transform: none; clip-path: none;
    background: transparent; color: var(--sh-muted);
    width: min(1240px, 100% - 2 * var(--gutter)); margin-inline: auto;
    max-width: 58ch; padding: 6px 0 0; display: block; pointer-events: auto;
    text-align: left;   /* the desktop band is right-aligned; a plain paragraph is not */
  }
  /* 🔴 EL PLANETA SALE DE DEBAJO DEL TEXTO.
     .orbit__sky es `position: absolute; inset: 0` con el canvas centrado, y
     .orbit__steps va encima. En escritorio funciona: los titulos miden hasta
     108px y el planeta (620px) queda como telon de fondo por detras de letras
     enormes. En movil la caja es corta y el planeta (273px) aterriza JUSTO
     sobre los pasos 02 y 03.
     Medido con _build/png-probe.js sobre los pixeles reales de una captura a
     390x844: «Disenamos» 1,12:1 y su descripcion 1,00:1; «Integramos» 1,21:1 y
     su descripcion 1,00:1. Un ratio de 1,00:1 significa que el texto y el
     fondo tienen la MISMA luminancia — el texto es literalmente invisible.
     Los pasos 01 y 04, fuera del disco, median 4,5:1 y 6,3:1.
     Aqui el cielo deja de ser una capa absoluta y pasa a ser un bloque normal
     POR ENCIMA de la lista: el planeta se ve entero como ilustracion de la
     seccion y los cuatro pasos se leen sobre la hoja limpia. */
  .orbit { display: flex; flex-direction: column; }
  .orbit__sky {
    position: static; inset: auto; order: -1;
    margin-bottom: clamp(24px, 5vw, 40px);
  }
  .orbit__gl { width: min(62vw, 300px); height: min(62vw, 300px); }
  .ostep { padding: 14px 0; }
  /* ...and with no band, the desktop hover/focus colours (ink, for reading
     over the orange band) would paint the word ink on the near-black sheet:
     a focused step title at ~1.1:1. Keep the resting colours here. */
  .has-js .ostep:hover .ostep__title, .has-js .ostep:focus-within .ostep__title { color: var(--orbit-title); }
  .has-js .ostep:hover .ostep__title::before, .has-js .ostep:focus-within .ostep__title::before { color: var(--sh-text); }
  .has-js .ostep:hover .ostep__num, .has-js .ostep:focus-within .ostep__num { color: var(--accent-ink); }
  /* 🔴 ...y por lo mismo, el ANILLO DE FOCO tambien cambia de color aqui.
     El anillo base es `2px solid var(--ink)` y eso es correcto EN ESCRITORIO,
     donde enfocar un paso abre su banda naranja y el aro se dibuja sobre ella
     (--ink mide 7,58:1 sobre --accent). En movil no hay banda —las tres reglas
     de aqui arriba la desactivan a proposito— asi que el aro de tinta #101010
     se queda sobre la hoja #121110: medido, 1,01:1. O sea invisible.
     Lo sufre quien navega con teclado externo, control por conmutadores o el
     foco de VoiceOver/TalkBack, justo los cuatro <li tabindex="0"> del proceso.
     --sh-text da 17,02:1. Misma especificidad (0,3,0) que la regla base y va
     despues en el archivo, asi que solo pisa el color del contorno. */
  .has-js .ostep:focus-visible { outline-color: var(--sh-text); }
}

.stats { display: flex; flex-wrap: wrap; gap: 32px 64px; margin-top: clamp(48px, 5vw, 72px); padding-top: 36px; border-top: 1px solid var(--sh-border); }
.stat b { display: block; font-size: clamp(40px, 4.2vw, 60px); font-weight: 500; letter-spacing: -0.035em; line-height: 1; margin-bottom: 8px; }
.stat span { font-size: 13px; color: var(--sh-muted); }

/* CTA + form */
.cta { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(40px, 6vw, 96px); align-items: start; }
.form { padding: 6px; border-radius: var(--r-xl); background: var(--sh-tint); border: 1px solid var(--sh-border); box-shadow: var(--shadow-sh); }
.form__shell {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: clamp(24px, 2.5vw, 32px);
  border-radius: calc(var(--r-xl) - 6px); background: var(--sh-surface-2); box-shadow: var(--sh-bevel);
}
.form__field { display: flex; flex-direction: column; gap: 8px; }
.form__field--full { grid-column: 1 / -1; }
.form label { font-size: 12px; letter-spacing: 0.04em; color: var(--sh-text-2); }
.form input, .form textarea {
  width: 100%; padding: 12px 16px; border-radius: var(--r-md); border: 1px solid var(--sh-field-border);
  background: var(--sh-surface); color: var(--sh-text); font: inherit; font-size: 15px; resize: vertical;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
/* Text fields swap the outline for a coloured border plus a soft halo. Checkboxes
   must NOT be in here: they paint natively, so border-color and box-shadow are
   ignored and suppressing the outline left the required consent box with no
   visible focus at all. They keep the global :focus-visible ring instead. */
.form input:not([type="checkbox"]):focus, .form textarea:focus { outline: none; border-color: var(--accent-ink); box-shadow: 0 0 0 4px var(--accent-soft); }
.form .is-invalid { border-color: var(--accent-ink); }
.form__consent { flex-direction: row; align-items: flex-start; gap: 12px; }
.form__consent input { width: 18px; height: 18px; flex: none; margin: 2px 0 0; padding: 0; border-radius: 4px; accent-color: var(--accent-hero); }
.form__consent input.is-invalid { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.form__consent label { font-size: 13px; letter-spacing: 0; line-height: 1.5; color: var(--sh-text-2); }
.form__legal { grid-column: 1 / -1; margin: 0; font-size: 12px; line-height: 1.5; color: var(--sh-muted); }
.field__err { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 12px; color: var(--accent-ink); }
.field__err::before { content: "!"; width: 16px; height: 16px; flex: none; border-radius: 50%; border: 1px solid currentColor; display: grid; place-items: center; font-size: 11px; font-weight: 600; }
.form__error { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--accent-ink); }
.form__submit { grid-column: 1 / -1; justify-self: start; margin-top: 8px; }
.form__done { display: flex; align-items: center; gap: 16px; padding: 36px 32px; border-radius: calc(var(--r-xl) - 6px); background: var(--sh-surface-2); }
.form__done-mark { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; background: var(--accent); color: var(--ink); font-weight: 600; }
.form__done p { margin: 0; color: var(--sh-text-2); }

/* Footer — three columns (brand, site map, contact) over a legal line. The
   markup is shared by every page (_build/shell/footer.html) and guarded by
   _build/check-shell.js. Contact data is still unconfirmed: each link is a
   [data-placeholder] with a visible "[… por confirmar]" label, styled so it
   cannot be mistaken for real data. */
.footer { position: relative; z-index: var(--z-sheet); background: var(--sh-bg-2); color: var(--sh-muted); border-top: 1px solid var(--sh-border); padding: clamp(56px, 7vw, 96px) 0 32px; }
.footer__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 40px clamp(32px, 5vw, 96px); align-items: start; }
.footer__brand { max-width: 38ch; }
.footer__logo { width: 64px; height: auto; opacity: 0.9; margin-bottom: 20px; }
.footer__line { margin: 0; font-size: 14px; line-height: 1.6; color: var(--sh-text-2); }
.footer__head { margin: 0 0 16px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--sh-text-2); }
.footer__nav, .footer__contact { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; font-size: 14px; }
.footer__nav a, .footer__contact a { color: var(--sh-text-2); text-decoration: none; transition: color var(--t-fast) var(--ease); }
.footer__nav a:hover, .footer__contact a:hover { color: var(--sh-text); }
.footer__addr { margin: 6px 0 0; font-size: 13px; color: var(--sh-muted); }
.footer__bottom { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px 24px; margin-top: clamp(40px, 5vw, 64px); padding-top: 24px; border-top: 1px solid var(--sh-border); font-size: 13px; }
.footer__copy { margin: 0; }
.footer__legal { display: flex; gap: 20px; }
.footer__legal a { color: var(--sh-muted); text-decoration: none; transition: color var(--t-fast) var(--ease); }
.footer__legal a:hover { color: var(--sh-text); }
/* A placeholder reads as a placeholder: mono, dashed underline, muted. Never
   restyle this to look like a real link — it exists so unconfirmed data cannot
   ship looking real (see CLAUDE.md, "Pendiente"). */
[data-placeholder] { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.02em; color: var(--sh-muted); border-bottom: 1px dashed var(--sh-border-strong); }
.footer__contact a[data-placeholder] { color: var(--sh-muted); }

/* Scroll reveals (gated) */
.has-js .reveal { opacity: 0; transform: translateY(44px); }
.has-js .reveal[data-reveal="left"] { transform: translateX(-88px); }
.has-js .reveal[data-reveal="right"] { transform: translateX(88px); }
.has-js .reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity 500ms var(--ease), transform 600ms var(--ease);
  transition-delay: calc(var(--i, 0) * 80ms);
}


/* Drawer */
/* Full screen since 08-09-2026 (encargo de Alex): one column, no scrim — the
   panel IS the screen, so there is nothing left behind it to dim. */
.drawer { position: fixed; inset: 0; z-index: var(--z-drawer); display: grid; grid-template-columns: 1fr; visibility: hidden; }
.drawer.is-open { visibility: visible; }
.drawer__panel {
  display: flex; flex-direction: column; gap: 32px; padding: 24px 32px 32px;
  background: var(--panel); color: var(--on-ink-text); border-left: 1px solid var(--on-ink-border-strong);
  transform: translateY(-2%); opacity: 0;
  transition: transform var(--t-mid) var(--ease), opacity var(--t-mid) var(--ease);
}
.drawer.is-open .drawer__panel { transform: none; opacity: 1; }
.drawer__panel { border-left: 0; overflow-y: auto; }
.drawer__head { display: flex; align-items: center; justify-content: space-between; min-height: 28px; }
.drawer__brand { display: inline-flex; align-items: center; gap: 12px; font-weight: 600; font-size: 17px; }
.drawer__brand img { width: 11px; height: auto; }
/* leaving states ease in */
.drawer:not(.is-open) .drawer__panel { transition-timing-function: var(--ease-in); }
/* --------------------------------------------------------------------------
   Marquee menu rows — hovering a full-width menu link slides a contrasting band
   in from the NEAREST EDGE, filled with an endless marquee of words and rounded
   images. (Reference: "Marquee Menu Rows".)

   The reference computes the closest edge in JS and drives the slide with GSAP.
   Same idea here, but the slide is a CSS transition on a custom property that
   script.js sets to -101% or 101% depending on the edge — so if GSAP never
   loads, or JS dies, the rows are still ordinary links and the band simply
   never appears. The scroll of the marquee itself is a CSS keyframe, as in the
   reference.

   Everything is re-themed: the reference's pale blue band becomes --accent with
   --ink text, and the type is the page's own scale.
   -------------------------------------------------------------------------- */
.mrows { display: flex; flex-direction: column; margin: 0; flex: 1; justify-content: center; }
.mrow__link {
  position: relative; display: block; overflow: hidden;
  text-decoration: none; color: var(--on-ink-text);
  border-top: 1px solid var(--on-ink-border);
}
/* the staggered entrance is the DRAWER's, not the row's */
.drawer .mrow__link {
  opacity: 0; transform: translateY(10px);
  transition: opacity var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
  transition-delay: calc(var(--i, 0) * 45ms + 90ms);
}
.mrows > *:last-child .mrow__link, .mrows .mrow__link:last-child { border-bottom: 1px solid var(--on-ink-border); }
.drawer.is-open .mrow__link { opacity: 1; transform: none; }
.drawer:not(.is-open) .mrow__link { transition-timing-function: var(--ease-in); transition-delay: 0s; }

.mrow__label {
  display: block; padding: clamp(6px, 1.1vh, 14px) var(--gutter);
  /* seven rows since the nav grew (Cómo trabajamos, Panel demo): at 68px the
     stack was 861px tall and the drawer has no scroll; 52px keeps 7 rows in
     ~620px at 1440x900 and ~540px at 1366x768 */
  font-size: clamp(26px, 4.2vw, 52px); line-height: 1.16; font-weight: 500;
  letter-spacing: -0.035em; text-transform: uppercase;
}

.mrow__band {
  /* A band is a full-row strip holding eight background images, and it is
     translated out of sight until you hover it. In the drawer that cost
     nothing, because a closed drawer is visibility:hidden and never painted.
     On the page it is different: five always-laid-out bands = 40 image paints
     the browser does for something nobody can see. content-visibility lets it
     skip the whole subtree until the band is actually needed. */
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  background: var(--accent); color: var(--ink);
  transform: translate3d(0, var(--edge, 101%), 0);
  transition: transform 600ms var(--ease);
}
.mrow__track {
  display: block; height: 100%; width: 100%;
  transform: translate3d(0, calc(var(--edge, 101%) * -1), 0);
  transition: transform 600ms var(--ease);
}
.mrow__link:hover .mrow__band,
.mrow__link:focus-visible .mrow__band,
.mrow__link:hover .mrow__track,
.mrow__link:focus-visible .mrow__track { transform: translate3d(0, 0, 0); }

.mrow__inner {
  display: flex; align-items: center; height: 100%; width: max-content;
  animation: mrow-scroll 18s linear infinite;
  /* PAUSED until the band is actually on screen. There are ten of these once
     the services list exists (five menu rows + five service rows) and every one
     was scrolling forever behind a hidden band; with will-change forcing a
     compositor layer each, screenshots of the page began timing out. The
     marquee only needs to move while you can see it. */
  animation-play-state: paused;
}
.mrow__link:hover .mrow__inner,
.mrow__link:focus-visible .mrow__inner { animation-play-state: running; }
.mrow__inner span {
  white-space: nowrap; padding: 0 1.4vw;
  font-size: clamp(24px, 4vw, 52px); font-weight: 400; letter-spacing: -0.03em;
  text-transform: uppercase; color: var(--ink);
}
.mrow__img {
  flex: none; width: clamp(76px, 9vw, 128px); height: 66%;
  margin: 0 1.2vw; border-radius: var(--r-pill);
  background-size: cover; background-position: 50% 50%;
}
@keyframes mrow-scroll { to { transform: translate3d(-50%, 0, 0); } }

@media (prefers-reduced-motion: reduce) {
  /* no slide and no scroll. The band still appears, instantly, so hovering
     still does something; it just does not move. */
  .mrow__band, .mrow__track { transition: none; }
  .mrow__inner { animation: none; }
  .drawer .mrow__link { opacity: 1; transform: none; transition: none; }
}
@media (max-width: 900px) {
  /* no hover on touch: the band would never open, so the rows stay plain */
  .mrow__band { display: none; }
  .mrow__label { font-size: clamp(26px, 8vw, 40px); }
}

/* --------------------------------------------------------------------------
   Servicios — deck of five slides driven by scroll ("Async Slide Transitions").

   The reference swaps pages on CLICK: the outgoing one goes x:-50%, scale 0.8,
   opacity 0.4 while the incoming one travels from x:100% to 0, both on the same
   custom ease. Alex wanted that motion, but advanced by SCROLLING — so instead
   of a timeline per click, script.js maps scroll position to a continuous
   progress and writes the same three properties every frame. The eye still
   reads it as one slide clicking over to the next.

   No GSAP: the reference needs CustomEase and SplitText, neither of which this
   project loads, and a scroll-linked deck should not stop working because a CDN
   is slow. The ease is applied in JS; the entrance of the type is CSS.

   WITHOUT JS the slides are five ordinary blocks stacked down the page, all
   readable — every absolute position below is behind .has-js.
   -------------------------------------------------------------------------- */
/* The always-visible index of the five services (see index.html). Pills with
   a control border (3:1), each a link to its slide's anchor. */
.svc-index { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 10px; }
.svc-index a {
  display: inline-flex; align-items: baseline; gap: 8px; padding: 8px 14px;
  border-radius: var(--r-pill); border: 1px solid var(--sh-control-border);
  color: var(--sh-text); text-decoration: none; font-size: 13px; font-weight: 500;
  transition: background-color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.svc-index a small { font-size: 12px; font-weight: 400; color: var(--sh-muted); }
.svc-index a:hover { background: var(--sh-tint); border-color: var(--sh-text-2); }
@media (max-width: 560px) { .svc-index a small { display: none; } }

.deck { margin-top: clamp(32px, 4vw, 56px); }
/* La fotografia de cada diapositiva es SOLO de movil. En escritorio el deck
   vive dentro del escaparate de puntero del hero, que ya ensena esta misma
   obra a pantalla completa; repetirla aqui seria verla dos veces. En movil ese
   escaparate no existe (no hay puntero) y la diapositiva se quedaba con una
   tabla, una palabra y ~600px de negro. Ver _build/migrations/patch-mobile-deck-art.js. */
.deck__art { display: none; }
.deck__slide { padding: clamp(32px, 5vw, 64px) 0; border-top: 1px solid var(--sh-border); }
.deck__inner { width: min(1240px, 100% - 2 * var(--gutter)); margin-inline: auto; }
.deck__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 clamp(24px, 4vw, 72px); }
.deck__col { list-style: none; margin: 0; padding: 0; }
.deck__row {
  position: relative; display: flex; justify-content: space-between; gap: 16px;
  padding: 10px 0; font-size: clamp(11px, 0.85vw, 13px);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.deck__k { color: var(--sh-muted); }
.deck__v { color: var(--sh-text); text-align: right; }
.deck__line {
  position: absolute; left: 0; bottom: 0; width: 100%; height: 1px;
  background: var(--sh-border-strong); transform-origin: left center;
}
.deck__title {
  margin: clamp(24px, 4vh, 56px) 0 0;
  font-size: clamp(30px, 9vw, 132px); line-height: 0.9; font-weight: 500;
  /* the mask for the entrance is overflow:hidden, and at line-height .9 it
     shaved the accent off the O of TRANSFORMACION / AUTOMATIZACION */
  padding-top: 0.1em;
  letter-spacing: -0.045em; color: var(--sh-text);
  overflow: hidden;
}

/* ---- with JS: one sticky viewport, five stacked slides ---- */
.has-js .deck__stage { position: relative; height: 460dvh; }
.has-js .deck__viewport {
  position: sticky; top: 0; height: 100dvh; overflow: hidden;
  display: grid; align-items: center;
}
.has-js .deck__slide {
  position: absolute; inset: 0; border-top: 0; padding: 0;
  display: grid; align-content: center;
  /* OPAQUE. In the video the incoming page is a solid panel that covers the
     one it replaces; transparent slides just ghost through each other, which is
     what the first attempt did. */
  background: var(--sh-bg);
  /* written every frame by script.js */
  transform: translate3d(var(--x, 0), var(--y, 0), 0) scale(var(--s, 1));
  opacity: var(--o, 1);
  transform-origin: 50% 50%;
  backface-visibility: hidden;
  pointer-events: none;
}
.has-js .deck__slide.is-live { pointer-events: auto; }
/* a slide nowhere near the viewport costs nothing */
.has-js .deck__slide.is-off { visibility: hidden; }

/* the reference wipes its rules in from the side as a slide arrives */
.has-js .deck__line { transform: scaleX(0); transition: transform 900ms var(--ease); transition-delay: calc(var(--i, 0) * 70ms + 120ms); }
.has-js .deck__line--r { transform-origin: right center; }
.has-js .deck__slide.is-entered .deck__line { transform: scaleX(1); }
.has-js .deck__k, .has-js .deck__v, .has-js .deck__title {
  transform: translateY(60%); opacity: 0;
  transition: transform 900ms var(--ease), opacity 600ms var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms + 90ms);
}
.has-js .deck__title { transition-delay: 60ms; }
.has-js .deck__slide.is-entered .deck__k,
.has-js .deck__slide.is-entered .deck__v,
.has-js .deck__slide.is-entered .deck__title { transform: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* no sliding deck at all: back to five plain stacked blocks */
  .has-js .deck__stage { height: auto; }
  .has-js .deck__viewport { position: static; height: auto; display: block; overflow: visible; }
  .has-js .deck__slide {
    position: static; transform: none !important; opacity: 1 !important;
    visibility: visible !important; padding: clamp(32px, 5vw, 64px) 0;
    border-top: 1px solid var(--sh-border); pointer-events: auto;
  }
  .has-js .deck__line { transform: scaleX(1); transition: none; }
  .has-js .deck__k, .has-js .deck__v, .has-js .deck__title { transform: none; opacity: 1; transition: none; }
}
@media (max-width: 900px) {
  .deck__grid { grid-template-columns: 1fr; gap: 0; }
  .has-js .deck__stage { height: 420dvh; }
  /* 9.6vw, not 13vw: «TRANSFORMACIÓN» is ~8.9em wide at this tracking and the
     title is overflow:hidden (the entrance mask), so at 390px anything above
     ~39px clips the word. 9.6vw is 37px there. */
  .deck__title { font-size: clamp(26px, 9.6vw, 84px); }

  /* La diapositiva esta centrada verticalmente (`align-content: center`) dentro
     de un viewport de 100dvh, asi que el hueco quedaba repartido arriba y
     abajo del bloque. Al crecer el contenido con la foto, el centrado la
     absorbe sola: no hace falta posicionar nada.
     La altura va en dvh y no en px para que la foto no empuje la tabla fuera
     de pantalla en un movil bajo (360x640: 26dvh = 166px). */
  .deck__art {
    display: block; margin: clamp(20px, 4vw, 32px) 0 0;
    border-radius: var(--r-lg); overflow: hidden;
    /* filete INTERIOR, no `border`: es el mismo recurso que .sc__img en el
       escaparate de escritorio, y sobre una hoja casi negra un hilo blanco al
       22% separa la foto del fondo sin dibujar una caja. */
    box-shadow: inset 0 0 0 1px var(--w-22);
  }
  .deck__art img {
    display: block; width: 100%;
    height: clamp(150px, 26dvh, 250px);
    /* center 45% y saturate(.9), igual que el escaparate: el punto focal alto
       salva las caras al recortar (dos de los cinco paneles son verticales) y
       la bajada de saturacion mete la foto en la paleta casi monocroma de la
       hoja en vez de dejarla como un parche de color. */
    object-fit: cover; object-position: center 45%;
    filter: saturate(0.9);
  }
}

.drawer__foot { margin-top: auto; display: flex; flex-direction: column; gap: 20px; }
.drawer__foot .btn { justify-content: space-between; }
.drawer__meta { display: flex; justify-content: space-between; margin: 0; font-size: 12px; color: var(--on-ink-muted); font-family: var(--font-mono); }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@media (max-width: 1280px) {
  .hero__copy { width: min(520px, 44vw); }
  .hero__aside { width: min(340px, 30vw); }
  .widget__core { grid-template-columns: 72px 1fr; gap: 12px; min-height: 0; }
  .widget__mark { width: 72px; height: 72px; border-radius: var(--r-md); }
  .widget__text { font-size: 16px; padding-right: 24px; }
}

/* --------------------------------------------------------------------------
   SHORT desktop viewports. Until now the hero was only ever checked at
   1440x900, where everything happens to fit. It has to hold, between the nav
   and the bottom bar: the eyebrow, a three-line H1, the proof line, two CTAs
   AND the five-item service list. A maximised window on a 1366x768 laptop
   gives about 660-690px of viewport, and at that height the column simply does
   not fit — the list ran into the bar. So the whole left column tightens
   together, in two steps, rather than any one piece being cut.
   These are the first HEIGHT queries in the file; everything else keys off
   width. Re-measure with the four-viewport check in the task notes if you
   touch the H1 size, the list size or --bar-h.
   -------------------------------------------------------------------------- */
/* 895px, and the number is derived, not guessed. The column needs
   top + copy(382) + gap(28) + list(206) + bottom(92) <= H, and in this range
   top is 0.21H, so it fits while 0.21H + 708 <= H, i.e. H >= 896. At exactly
   1440x900 — the size Alex has been reviewing — that leaves ~4px, which
   space-between spends on the gap, and the H1 he approved keeps its 73px. One
   pixel lower and the tightening starts.
   (An earlier attempt put this at 865 "to be safe" and was wrong in the
   dangerous direction: at 866 the column was already overflowing by 24px and
   the list cleared the bottom bar by 2px, by luck.) */
@media (min-width: 901px) and (max-height: 895px) {
  .hero__left { top: clamp(96px, 13vh, 190px); gap: clamp(14px, 2.2vh, 28px); }
  /* the aside shares the column's top, or the two heads drift apart */
  .hero__aside { top: clamp(96px, 13vh, 190px); }
  .hero__title { font-size: clamp(38px, 4.3vw, 66px); margin-bottom: 18px; }
  .hero__copy .eyebrow { margin-bottom: 16px; }
  .hero__proof { margin-bottom: 22px; }
  .hero__works-label { margin-bottom: 8px; }
  .hero__works-list a { padding: 2px 0; font-size: clamp(14px, 1.05vw, 16px); }

  /* --- aside vs tira ---
     La columna DERECHA tambien tiene suelo, y hasta ahora no lo tenia: es
     `position: absolute` con `top` y sin `bottom`, asi que crecia libremente
     hacia abajo. Con la barra inferior a 46px de chips de texto no llegaba a
     tocarla; con la tira de integraciones a 80px si. Medido a 1366x620 antes
     de esto: el borde inferior del aside caia 49px POR DEBAJO del borde
     superior de la tira, y la tira (z-index 4) pinta encima del aside
     (z-index 2) — o sea, se comia la rejilla de marcas.
     Es el mismo suelo que ya reservaba .hero__left en la linea ~494, con el
     mismo calculo, para que las dos columnas se comporten igual. */
  .hero__aside {
    max-height: calc(100dvh - clamp(96px, 13vh, 190px) - var(--bar-inset) - var(--bar-h) - 26px);
    overflow: visible;
  }
  .hero__aside-in { gap: clamp(16px, 2.6vh, 44px); }
}
@media (min-width: 901px) and (max-height: 760px) {
  .hero__left { top: clamp(84px, 11vh, 120px); }
  .hero__aside { top: clamp(84px, 11vh, 120px); }
  .hero__title { font-size: clamp(34px, 3.6vw, 52px); }
  /* El alto va en :root para que lo vea tambien la reserva de suelo de
     .hero__left (linea ~494). Antes estaba en .hero__bar y no lo veia. */
  :root { --bar-h: 62px; }
  .hero__bar { font-size: 11px; }
  /* El tope se recalcula con el top de ESTE escalon, no el del anterior. */
  .hero__aside {
    max-height: calc(100dvh - clamp(84px, 11vh, 120px) - var(--bar-inset) - var(--bar-h) - 26px);
  }
}

/* nav has to give way before the centre column touches the right one */
@media (max-width: 1080px) {
  .nav__links { gap: 16px; }
  .nav__right { gap: 12px; }
}
@media (max-width: 1120px) {
  .nav__tz { display: none; }
}

@media (max-width: 900px) {
  :root { --nav-h: 64px; }
  /* the seam ramp — see --seam-* in :root. Keyed to the FIRST sheet, not to
     .sheet--intro, which was removed with the "Qué hacemos" section. */
  main > .sheet:first-child {
    background:
      radial-gradient(70% 40% at 50% 0%, var(--accent-a10) 0%, var(--accent-a0) 60%),
      linear-gradient(180deg,
        var(--bg-3) 0px,
        var(--seam-1) 48px,
        var(--seam-2) 104px,
        var(--seam-3) 152px,
        var(--sh-bg) 200px,
        var(--sh-bg) calc(100% - 200px),
        var(--sh-bg-2) 100%);
  }
  .hero-stage { height: auto; }
  .hero { position: relative; height: auto; min-height: 100dvh; display: flex; flex-direction: column; padding-top: var(--nav-h); }
  .hero-sentinel { top: min(100dvh, 100%); }
  .nav { grid-template-columns: 1fr auto; }
  .nav__links, .nav__tz { display: none; }
  /* display: contents dissolves the desktop left column so .hero__copy and
     .hero__works become direct flex items of .hero again and keep their own
     order. The DOM already has them in visual order, so this is belt and
     braces rather than the thing holding the reading order together. */
  .hero__left { display: contents; }
  .hero__copy, .hero__aside { position: static; width: auto; padding: 0 var(--gutter); }
  .hero__copy { order: 1; margin-top: 20px; }
  .hero__title { font-size: clamp(34px, 9.4vw, 48px); letter-spacing: -0.03em; }
  /* 48dvh, no 58dvh. Medido a 390x844 con _build/png-probe.js sobre una captura
     con el cerebro aislado: en una caja de 490px la nube ocupaba 219px de alto
     — el 45% — y dejaba 116px vacios por encima. El cerebro NO encoge al
     recortar la caja: con FIT_ASPECT 0.88 en brain.js y una caja mas alta que
     ancha, el encuadre esta limitado por el ANCHO (por debajo de ese aspecto el
     fov se abre en vertical, que es justo lo que agrandaba el vacio). Asi que
     esos px eran hueco puro. */
  .hero__scene { order: 3; position: relative; inset: auto; height: min(48dvh, 410px); margin-top: 4px; }
  /* the canvas fills the scene box here: a square figure centred in a taller
     box left a dead band above and below the cloud */
  .hero__figure { height: 100%; width: 100%; max-width: none; max-height: 100%; aspect-ratio: auto; }
  .hero__wordmark-wrap { font-size: clamp(96px, 26vw, 200px); }
  .hero__bar {
    --bar-inset: 12px; --bar-h: auto;
    grid-template-columns: repeat(2, 1fr); justify-content: stretch; gap: 8px 16px; padding: 12px 16px; font-size: 11px;
  }
  .hero__bar > * { justify-content: flex-start; }
  .hero__aside { order: 4; padding-top: 40px; padding-bottom: 40px; }
  .hero__aside-in { gap: 32px; }
  .trace__label { text-align: left; }
  /* 44px de zona táctil, como el resto de enlaces del hero desde el paso 8: en
     escritorio mide 24px, que cumple WCAG 2.5.8 justo justo pero no es la
     ergonomía de la casa. Se gana por min-height, sin tocar el tamaño de letra. */
  .trace__link { justify-content: flex-start; min-height: 44px; }
  /* No radius here. Below 901px the sheet does not overlap the hero, so a
     rounded lid only exposed the dark body in two "ears" between the light
     hero and the light-grey start of the seam ramp. The seam IS the edge. */
  main { margin-top: 0; border-radius: 0; }
  .feature, .feature--alt { grid-template-columns: 1fr; }
  .feature--alt .feature__visual { order: 0; }
  .feature__visual { min-height: 240px; }
  .cta { grid-template-columns: 1fr; }
  .form__shell { grid-template-columns: 1fr; }
  .drawer { grid-template-columns: 1fr; }

  /* 🔴 16px, no 15px. iOS Safari HACE ZOOM a la pagina al enfocar un campo
     cuyo font-size sea menor de 16px, y despues no vuelve solo: el usuario se
     queda con la web ampliada y desplazada a media pantalla. Medido: los
     cuatro campos de #contacto y los de contacto.html estaban a 15px. Es el
     defecto movil mas visible de todo el formulario y no lo detecta Lighthouse.
     En escritorio siguen a 15px: esta regla vive solo aqui. */
  .form input, .form textarea, .form select { font-size: 16px; }

  /* La casilla de consentimiento es OBLIGATORIA y su cuadro mide 18x18: un
     objetivo diminuto para el unico control que bloquea el envio.
     NO se agranda con un ::after sobre el <input>: un checkbox es un elemento
     REEMPLAZADO y sus pseudo-elementos no se pintan — esa regla no haria nada.
     Lo que se agranda es la ETIQUETA, que ya esta asociada por for/id, asi que
     tocarla marca la casilla. 44px de alto de zona tactil, el dibujo intacto. */
  .form__consent { align-items: center; gap: 14px; }
  .form__consent label { min-height: 44px; display: flex; align-items: center; cursor: pointer; }

  /* ------------------------------------------------------------------
     Objetivos tactiles: 44x44 en todo lo que se toca con el dedo.
     Matiz importante, para que nadie «arregle» esto al reves mas tarde:
     Lighthouse movil ya daba a11y 100 y su auditoria `target-size` PASABA,
     porque mide el minimo de WCAG 2.2 AA (24x24, criterio 2.5.8). O sea que
     esto NO era una violacion WCAG. Es ergonomia: 44x44 es el minimo comodo
     (Apple HIG y WCAG 2.5.5 AAA) y es la vara de una web de este nivel.
     Medido a 390x844 antes de tocar nada: MENU 93x40, cerrar del drawer 40x40,
     enlaces del pie 22px de alto, lista de servicios del hero 34px.
     Todo crece por PADDING o min-height, nunca por font-size: el dibujo no se
     mueve, solo el area que responde al dedo.
     ------------------------------------------------------------------ */
  .btn { min-height: 44px; }
  .btn--sm { min-height: 44px; }
  .btn--icon { width: 44px; height: 44px; }
  .nav__brand { min-height: 44px; }
  .skip-link { min-height: 44px; display: inline-flex; align-items: center; }
  /* los cinco pills del indice de servicios: 39px -> 44px por padding */
  .svc-index a { padding-block: 11px; }
  .footer__nav, .footer__contact { gap: 2px; }
  .footer__nav a, .footer__contact a,
  .footer__contact [data-placeholder] {
    display: flex; align-items: center; min-height: 44px;
  }
  .footer__legal { gap: 8px; }
  .footer__legal a { display: flex; align-items: center; min-height: 44px; padding: 0 12px; margin: 0 -12px; }
}

@media (min-width: 601px) and (max-width: 900px) {
  .hero__title { font-size: clamp(48px, 6.6vw, 64px); }
  .hero__scene { height: min(58dvh, 620px); }
  .hero__aside-in { flex-direction: row; align-items: flex-start; gap: 40px; }
  .hero__aside-in > * { flex: 1 1 0; min-width: 0; }
}

@media (max-width: 560px) {
  .hero__proof { flex-wrap: wrap; }
  .widget__core { grid-template-columns: 72px 1fr; min-height: 0; }
  .widget__mark { width: 72px; height: 72px; border-radius: var(--r-md); }
  .widget__text { font-size: 16px; }
  .footer__grid { grid-template-columns: 1fr; gap: 36px; }
}

/* --------------------------------------------------------------------------
   Reduced motion — everything visible, nothing moves
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .has-js .hero [data-enter], .has-js .hero .hero__title .line > [data-enter], .has-js .hero .hero__figure[data-enter],
  .has-js .reveal, .term__lines li { opacity: 1 !important; transform: none !important; }
  .drawer__links a { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   Movil + movimiento reducido — el ultimo bloque del archivo, a proposito
   --------------------------------------------------------------------------
   🔴 Choque de cascada real, no teorico. Dos reglas con la MISMA especificidad
   (0,2,0) escriben la altura del escenario del deck:

     @media (prefers-reduced-motion: reduce) { .has-js .deck__stage { height: auto } }
     @media (max-width: 900px)               { .has-js .deck__stage { height: 420dvh } }

   La segunda va DESPUES en el archivo, asi que gana. Resultado en un movil con
   «Reducir movimiento» activado (Ajustes > Accesibilidad > Movimiento en iOS,
   que mucha gente lleva puesto): las cinco diapositivas se vuelven bloques
   estaticos —eso lo hacen otras reglas del bloque de arriba, todas con
   !important, y esas SI se aplican— pero el escenario conserva sus 420dvh.
   A 390x844 eso son 3.545px de escenario para ~1.810px de contenido: unos
   1.735px de negro vacio que el usuario tiene que recorrer con el dedo.

   Va al final y como consulta COMBINADA en vez de acotar el bloque movil con
   `and (prefers-reduced-motion: no-preference)`, porque acotarlo se llevaria
   por delante `.deck__grid { grid-template-columns: 1fr }` y el `9.6vw` del
   titulo, que un movil con movimiento reducido sigue necesitando igual.
   -------------------------------------------------------------------------- */
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .has-js .deck__stage { height: auto; }
}


/* ==========================================================================
   MENUS DESPLEGABLES (Funciones e Integraciones) — _build/parts/menus.css
   Apilado aqui, no en archivo aparte: las 17 paginas ya cargan styles.css.
   Para editarlo, se edita el part y se vuelve a apilar. */
/* ===========================================================================
   HOLDERA · LOS DOS DESPLEGABLES DE LA BARRA  —  menus.css

   COMO SE INTEGRA (leer antes de pegar)
   -------------------------------------
   APENDAR ESTE ARCHIVO ENTERO AL FINAL DE styles.css. No es estetica: los
   guardias `_build/check-tokens.js` y `_build/contrast.js` llevan la lista de
   hojas ESCRITA A MANO (hoy ['styles.css','pages.css','hotel-anim.css',
   'sections.css']), asi que un .css nuevo no lo mira ninguno — pasaria las
   cinco puertas sin que nadie haya comprobado ni sus tokens ni su contraste,
   que es justo la clase de verde mentiroso que este proyecto ya ha pagado dos
   veces. Si se prefiere archivo aparte, hay que anadirlo a esas dos listas Y
   al <link> de las siete paginas.

   POR QUE SE PUEDE APENDAR AL FINAL SIN ROMPER NADA
   ------------------------------------------------
   1. Lo unico que este archivo le discute a styles.css es el aspecto del
      disparador. `.nav__links > a` (0,2,0) y `.nav__links > .mnu__trigger`
      (0,2,0) empatan en especificidad, asi que gana la ultima del archivo —
      y tiene que ganar la de aqui, porque menus.js sustituye el <a> por un
      <button> y la regla de styles.css deja de aplicarle. Las dos declaran
      lo MISMO; la de aqui existe para que el boton no se caiga al estilo por
      defecto del navegador.
   2. El orden INTERNO si importa, y es el de la casa: base -> movil ->
      movimiento reducido. El bloque de movimiento reducido va el ultimo por
      la trampa del paso 8 (dos @media con la misma especificidad: gana la
      que va despues en el archivo).

   LA DECISION DE COLOR QUE HAY QUE ENTENDER ANTES DE TOCAR NADA
   -------------------------------------------------------------
   🔴 El panel es OPACO y su paleta NO cambia con el estado de la barra.
   La barra tiene tres vestidos que escribe script.js: `is-scrolled` (cristal
   claro --nav-glass-light sobre el hero gris), `is-over-sheet` (cristal
   oscuro --nav-glass sobre la hoja) e `is-veiled` (sin cristal, sobre el
   telon del escaparate). Un panel translucido tendria que leerse encima de
   los tres fondos distintos, y eso son tres auditorias de contraste que
   caducan cada vez que alguien cambia un fondo. Un panel opaco de tinta
   (--panel, el mismo cuerpo que el drawer) tiene UN solo juego de ratios,
   medido una vez y valido en las tres situaciones. Es ademas lo que ya dice
   el sistema: el grupo --on-ink-* existe precisamente para "superficies
   oscuras sobre pagina clara".

   Ratios medidos sobre --panel #23201c (calculados, y comprobados despues en
   pagina con _build/a11y-probe.js):
     --on-ink-text   #f4f4f4  14,75:1   nombres, logos en hover
     --on-ink-text-2 #c9c9c9   9,79:1   descripciones, logos en reposo
     --on-ink-muted  #8f8f8f   5,01:1   rotulos de categoria (11px)
     --accent-ink    #f59b45   7,46:1   el enlace "ver todas" y los hovers
   Sobre la ficha (--mnu-tile, blanco al 5% => #2e2b27) bajan un escalon:
     --on-ink-text   12,81:1   --on-ink-text-2  8,51:1
   🔴 --on-ink-muted sobre la FICHA mide 4,36:1 y NO llega a 4,5. Por eso los
   rotulos de categoria van sobre el panel desnudo y nunca dentro de una
   ficha, y por eso las descripciones son --on-ink-text-2 y no --on-ink-muted:
   la fila se tinta en hover y --on-ink-muted se caeria al pasar el raton.
   =========================================================================== */

:root {
  /* Cuerpo del panel: el mismo del drawer. Opaco a proposito (ver arriba). */
  --mnu-surface: #ffffff;
  /* Relleno de una ficha de logo y su hover. Son las dos unicas capas alfa
     nuevas de este archivo; todo lo demas es alias de un token que ya existe. */
  --mnu-tile: rgba(22, 19, 15, 0.045);
  --mnu-tile-hover: rgba(22, 19, 15, 0.075);
  /* El pie del panel, medio escalon por encima del cuerpo.
     🔴 0.02 NO es un numero suelto: la nota del pie va en --on-ink-muted, que
     es el unico texto del panel sin holgura. Medido sobre --panel:
        0.02 -> 4,75:1    0.03 -> 4,62:1    0.04 -> 4,48:1  SUSPENSO
     A partir de 0.04 el pie ilumina lo justo para tirar la nota por debajo de
     4,5. Si hace falta un pie mas claro, lo que sube es el TEXTO a
     --mnu-text-2, no el fondo. */
  --mnu-foot: rgba(22, 19, 15, 0.025);
  --mnu-hairline: var(--on-ink-border);
  --mnu-edge: var(--on-ink-border-strong);
  --mnu-text: #16130f;
  --mnu-text-2: #3a352d;
  --mnu-muted: #5f594e;
  --mnu-accent: var(--accent-ink);
  /* Sombra APILADA, nunca de una capa (prohibido por el CLAUDE.md global):
     bisel arriba + asiento + dos caidas. Es la misma receta que --shadow-ink,
     con mas alcance porque el panel flota mas alto. */
  --mnu-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 2px 4px rgba(0, 0, 0, 0.28),
    0 18px 40px -18px rgba(0, 0, 0, 0.55),
    0 52px 96px -44px rgba(0, 0, 0, 0.50);

  /* Anchos. Integraciones es un indice vertical deliberadamente compacto;
     los dos se recortan al viewport, que por debajo de 901px ya no existe
     (ahi la barra esconde .nav__links y manda el drawer). */
  --mnu-w-fn: 620px;
  --mnu-w-ints: 320px;
  --mnu-w-como: 400px;
  /* Separacion entre el borde inferior de la barra y el panel. 6px: lo justo
     para que se lea como una pieza aparte sin abrir un pasillo por el que el
     raton se salga de camino al panel. */
  --mnu-drop: 6px;
  /* La X del panel. La reescribe menus.js sobre el propio panel al abrirlo,
     para que cuelgue de SU disparador. Se declara aqui y no como segundo
     argumento de var() por dos motivos: el valor por defecto (centrado en la
     barra, que es lo que se ve si el JS no llega) vive donde viven todos los
     valores de este proyecto, y `_build/check-tokens.js` no mira los .js de
     _build/parts, asi que un token que solo existiera en JS saldria como
     "usado y nunca definido" en cuanto esto se apende a styles.css. */
  --mnu-x: 50%;
}

/* --------------------------------------------------------------------------
   El disparador en la barra
   Mismo aspecto que .nav__links > a, repetido aqui porque menus.js sustituye
   el <a> por un <button> en cuanto arranca y aquella regla deja de aplicarle.
   Sin JS el selector cae sobre el <a> y declara lo mismo: no hay salto.
   -------------------------------------------------------------------------- */
.nav__links > .mnu__trigger {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  margin: 0; padding: 8px 0; border: 0; background: none; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 500; line-height: 1.25;
  letter-spacing: inherit;
  color: var(--text-2); text-decoration: none;
  transition: color var(--t-fast) var(--ease);
}
.nav__links > .mnu__trigger:hover,
.nav__links > .mnu__trigger[aria-expanded="true"] { color: var(--text); }

/* Sobre tinta: el escaparate y la hoja comparten la paleta on-ink. Repetido
   por el mismo motivo que arriba (el <button> no es un <a>). */
.nav.is-veiled .nav__links > .mnu__trigger,
.nav.is-over-sheet .nav__links > .mnu__trigger { color: var(--mnu-text-2); }
.nav.is-veiled .nav__links > .mnu__trigger:hover,
.nav.is-over-sheet .nav__links > .mnu__trigger:hover,
.nav.is-veiled .nav__links > .mnu__trigger[aria-expanded="true"],
.nav.is-over-sheet .nav__links > .mnu__trigger[aria-expanded="true"] { color: var(--mnu-text); }

.mnu__chev {
  display: inline-flex; margin-top: 1px;
  transition: transform var(--t-fast) var(--ease);
}
.mnu__trigger[aria-expanded="true"] .mnu__chev { transform: rotate(180deg); }

/* --------------------------------------------------------------------------
   El panel

   Se posiciona contra .nav (que es position: fixed desde el paso 6): ni
   .nav__links ni el propio disparador introducen contexto de posicion, de modo
   que el ancestro posicionado mas cercano ES la barra. Asi `top: 100%` sigue a
   la barra cuando se comprime de 76px a 64px al hacer scroll, sin que nadie
   tenga que recalcular nada.

   La X la escribe menus.js en --mnu-x al abrir: el panel arranca bajo SU
   disparador (como en la referencia, cada desplegable cuelga de su entrada) y
   se recorta para no salirse del margen. El valor por defecto, 50%, es el que
   se ve si el JS no llega a escribirlo — centrado en la barra, que es el unico
   sitio razonable sin saber de quien cuelga.

   z-index: var(--z-nav). Dentro del contexto de apilamiento de la barra basta
   con estar por encima de sus hermanos (el boton MENU va despues en el DOM);
   hacia fuera manda el 30 de la barra, que ya esta por encima de la hoja (10)
   y por debajo del grano (60) y del drawer (80). No hay ningun z-index suelto.
   -------------------------------------------------------------------------- */
.mnu__panel {
  position: absolute;
  top: calc(100% + var(--mnu-drop));
  left: var(--mnu-x);
  z-index: var(--z-nav);
  width: min(var(--mnu-w-fn), calc(100vw - var(--gutter) * 2));
  max-height: calc(100dvh - var(--nav-h) - 32px);
  overflow-y: auto;
  background: var(--mnu-surface);
  color: var(--mnu-text-2);
  border: 1px solid var(--mnu-edge);
  border-radius: var(--r-lg);
  box-shadow: var(--mnu-shadow);
  text-align: left;
  /* 220ms y ease-out: es un elemento que ENTRA (tabla de motion de la casa,
     150-250ms para una micro-interaccion). Al cerrar no hay animacion: el
     panel se va con el atributo hidden y desaparece en el acto, que es lo que
     un menu tiene que hacer. */
  animation: mnu-in 220ms var(--ease) both;
}
.mnu__panel--ints { width: min(var(--mnu-w-ints), calc(100vw - var(--gutter) * 2)); }

@keyframes mnu-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* El foco dentro del panel se ve sobre tinta, igual que en el drawer. */
.mnu__panel :focus-visible { outline-color: var(--mnu-text); }

.mnu__inner { padding: 24px 24px 20px; }
.mnu__inner--ints { padding: 10px; }
.mnu__cols { display: grid; }
.mnu__cols--fn { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 24px; }
/* Funciones con dos grupos (La operación, Lo comercial) desde que Trazabilidad,
   Definiciones y Catálogo pasaron a «Cómo funciona» (28-09-2026). */
.mnu__cols--fn.mnu__cols--c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.mnu__panel--como { width: min(var(--mnu-w-como), calc(100vw - var(--gutter) * 2)); }

.mnu__cat {
  margin: 0 0 8px; padding: 0 8px;
  font-family: var(--font-mono);
  font-size: 11px; font-weight: 500; line-height: 1.4;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--mnu-muted);
}
.mnu__list { list-style: none; margin: 0; padding: 0; }

/* Una entrada: NOMBRE CORTO arriba y frase debajo, que es como lo hace la
   referencia. Los titulos de features.json son frases enteras y no caben en
   un menu; los nombres cortos se derivan del campo `clave` (ver menus.html). */
.mnu__item {
  position: relative; display: block;
  padding: 8px 8px 8px 12px;
  border-radius: var(--r-sm);
  text-decoration: none; color: inherit;
  transition: background-color var(--t-fast) var(--ease);
}
/* La cuna de acento que aparece por la izquierda al apuntar una entrada. Va
   como pseudo-elemento y no como border-left para que no descuadre el texto. */
.mnu__item::before {
  content: ""; position: absolute; left: 4px; top: 10px; bottom: 10px; width: 2px;
  border-radius: var(--r-pill); background: var(--mnu-accent);
  opacity: 0; transform: scaleY(0.4); transform-origin: 50% 50%;
  transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.mnu__item:hover, .mnu__item:focus-visible { background: var(--mnu-tile); }
.mnu__item:hover::before, .mnu__item:focus-visible::before { opacity: 1; transform: none; }

.mnu__name {
  display: block;
  font-size: 14px; font-weight: 600; letter-spacing: -0.01em; line-height: 1.35;
  color: var(--mnu-text);
}
.mnu__desc {
  display: block; margin-top: 2px;
  font-size: 12px; line-height: 1.45;
  color: var(--mnu-text-2);
}

/* Integraciones usa la misma cuna de acento que las funciones, pero cada
   entrada ocupa toda la anchura y remata con una flecha hacia su seccion. */
.mnu__int-item {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  min-height: 44px; padding: 10px 12px;
}
.mnu__int-item .mnu__arrow { opacity: 0.65; transform: translateX(-2px); }
.mnu__int-item:hover .mnu__arrow,
.mnu__int-item:focus-visible .mnu__arrow { opacity: 1; transform: translateX(2px); }

/* --------------------------------------------------------------------------
   El pie del panel
   -------------------------------------------------------------------------- */
.mnu__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 12px 24px; min-height: 52px;
  background: var(--mnu-foot);
  border-top: 1px solid var(--mnu-hairline);
}
.mnu__note { margin: 0; font-size: 12px; line-height: 1.45; color: var(--mnu-muted); }
.mnu__all {
  flex: none;
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 500; letter-spacing: -0.005em;
  color: var(--mnu-accent); text-decoration: none;
  transition: color var(--t-fast) var(--ease);
}
.mnu__all:hover { color: var(--mnu-text); }
.mnu__arrow { display: inline-flex; transition: transform var(--t-fast) var(--ease); }
.mnu__all:hover .mnu__arrow { transform: translateX(3px); }

/* --------------------------------------------------------------------------
   Los mismos dos menus DENTRO DEL DRAWER (movil y tablet)

   La fila sigue siendo un .mrow__link con su .mrow__band, asi que el efecto
   de banda que ya escribe script.js la sigue cogiendo. Lo unico nuevo es el
   chevron y la lista desplegable que cuelga debajo.
   -------------------------------------------------------------------------- */
.msub { display: block; }
.msub__head { display: flex; align-items: center; justify-content: space-between; width: 100%; }
/* El disparador del drawer tambien pasa de <a> a <button>: hay que devolverle
   lo que el navegador le quita a un boton. */
.drawer button.msub__head {
  margin: 0; padding: 0; border-left: 0; border-right: 0; border-bottom: 0;
  background: none; font: inherit; text-align: left; cursor: pointer;
  color: var(--on-ink-text);
}
.msub__head .mrow__label { flex: 1 1 auto; }
.msub__chev {
  flex: none; display: inline-flex; margin-right: var(--gutter);
  color: var(--mnu-muted);
  transition: transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.msub__head[aria-expanded="true"] .msub__chev { transform: rotate(180deg); color: var(--mnu-text); }

.msub__list {
  display: flex; flex-direction: column;
  padding: 8px var(--gutter) 16px;
  background: var(--mnu-tile);
}
.msub__item {
  display: block; padding: 10px 12px; min-height: 44px;
  border-radius: var(--r-sm); text-decoration: none; color: inherit;
  transition: background-color var(--t-fast) var(--ease);
}
.msub__item:hover { background: var(--mnu-tile-hover); }
.msub__name { display: block; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--mnu-text); }
.msub__desc { display: block; margin-top: 2px; font-size: 13px; line-height: 1.45; color: var(--mnu-text-2); }
.msub__all {
  display: inline-flex; align-items: center; gap: 8px; align-self: flex-start;
  margin-top: 8px; padding: 10px 12px; min-height: 44px;
  font-size: 14px; font-weight: 500;
  color: var(--mnu-accent); text-decoration: none;
}
.msub__all:hover { color: var(--mnu-text); }

/* --------------------------------------------------------------------------
   Movil / tablet
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* Por debajo de 901px .nav__links es display:none, asi que los dos paneles
     no se pintan nunca: manda el drawer. Esto es cinturon y tirantes por si
     alguien cambia aquel breakpoint sin mirar aqui. */
  .mnu__panel { display: none; }
}

/* --------------------------------------------------------------------------
   Movimiento reducido
   El bloque que cierra styles.css ya apaga toda animacion con
   `*, *::before, *::after { animation: none !important }`, y ese !important le
   gana a esta hoja aunque vaya despues. Se repite aqui, explicito, porque este
   archivo puede acabar en otro sitio y porque una entrada que aparece de golpe
   es EXACTAMENTE lo que se quiere: el panel sigue abriendose, sin deslizarse.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .mnu__panel { animation: none; }
  .mnu__chev, .msub__chev, .mnu__arrow, .mnu__item::before { transition: none; }
}

/* ==========================================================================
   PASO 14 — Términos técnicos con su ventana, e iconos de sección

   Reunión con Alex (27-09-2026): «tener el lenguaje técnico en bold y poder
   poner el cursor encima y que salga una ventana pequeña que te defina what
   it stands for». El marcado es <abbr class="term" data-term="…"> y lo
   escriben los generadores desde _build/contenido/glosario.json; la ventana
   la monta terminos.js. Aquí solo el aspecto.

   Los iconos de sección (.sic) son los del diseño «Iconos secciones Holdera»
   que pasó Alex: retícula de 48, trazo de 1,6 y un único detalle en cobre.
   Los escribe _build/contenido/iconos.js.

   Va ANTES del bloque logos:brand, que _build/integraciones/build-brand-tokens.js
   reescribe entre sus dos marcas y que tiene que seguir siendo el final del
   archivo.
   ========================================================================== */
:root {
  /* El cobre del diseño de iconos: el único color de cada icono. Es
     decorativo —el icono siempre lleva su nombre escrito al lado—, así que
     no debe contraste; es el valor exacto del diseño. */
  --sic-accent: #b8612a;
  /* El subrayado punteado de un término. Dice «aquí hay una definición» sin
     confundirse con un enlace, que en este sitio nunca va punteado. */
  --term-line: rgba(22, 19, 15, 0.42);
  /* La ventana: tinta cálida y texto claro, la pareja on-ink de la casa.
     Medido sobre #16130f: texto 17,1:1 · secundario 10,2:1 · cobre 8,7:1. */
  --term-tip-bg: #16130f;
  --term-tip-text: #f7f5f1;
  --term-tip-muted: rgba(247, 245, 241, 0.74);
  --term-tip-rule: rgba(247, 245, 241, 0.16);
  --term-tip-accent: #f59b45;
  /* Sombra apilada, nunca de una capa (CLAUDE.md global). */
  --term-tip-shadow:
    0 1px 2px rgba(22, 19, 15, 0.18),
    0 8px 20px -6px rgba(22, 19, 15, 0.30),
    0 24px 48px -16px rgba(22, 19, 15, 0.34);
  /* Por encima de la barra (30) y del grano (60), por debajo del drawer (80):
     una ventana abierta no puede quedar tapada por la nav fija, y el drawer
     abierto sí la tiene que tapar a ella. */
  --z-tip: 70;
}

/* ---------------------------------------------------------------- iconos */
.sic { display: block; flex: none; color: inherit; overflow: visible; }
.sic .sic-a { stroke: var(--sic-accent); }
.sic .sic-af { fill: var(--sic-accent); }
.sic .sic-dot { fill: currentColor; stroke: none; }

/* Los mismos iconos en el desplegable de Funciones y en el drawer: 24 px a la
   izquierda del nombre, como la fila «EN LA WEB · 24 PX» del diseño. */
.mnu__item--ic { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 0 12px; align-items: start; padding-left: 14px; }
.mnu__item--ic .sic { margin-top: 1px; color: var(--mnu-text); }
.mnu__txt { display: block; min-width: 0; }
.msub__item--ic { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 0 12px; align-items: start; }
.msub__item--ic .sic { margin-top: 1px; color: var(--mnu-text); }
.msub__txt { display: block; min-width: 0; }
.mnu__foot-links { display: flex; align-items: center; gap: 24px; }

/* --------------------------------------------------------------- términos */
abbr.term {
  font-weight: 600;
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--term-line);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-skip-ink: none;
  cursor: help;
  border-radius: 3px;
  transition: text-decoration-color var(--t-fast) var(--ease);
}
abbr.term:hover,
abbr.term.is-open { text-decoration-color: var(--accent-ink); }
abbr.term:focus-visible { outline: 2px solid var(--sh-text); outline-offset: 2px; }

.term-tip {
  position: fixed; left: 0; top: 0; z-index: var(--z-tip);
  box-sizing: border-box;
  width: max-content; max-width: min(320px, calc(100vw - 16px));
  margin: 0; padding: 12px 14px 13px;
  border-radius: 12px;
  background: var(--term-tip-bg); color: var(--term-tip-text);
  box-shadow: var(--term-tip-shadow);
  font-family: var(--font); font-size: 13px; font-weight: 400; line-height: 1.45;
  letter-spacing: 0; text-align: left; text-transform: none; white-space: normal;
  opacity: 0; transform: translateY(4px);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease);
}
.term-tip.is-below { transform: translateY(-4px); }
.term-tip.is-in { opacity: 1; transform: none; }
.term-tip[hidden], .term-tip [hidden] { display: none; }
/* La flecha apunta al término aunque la ventana se haya corrido para caber. */
.term-tip::after {
  content: ""; position: absolute; left: var(--tip-arrow, 50%); bottom: -5px;
  width: 10px; height: 10px; margin-left: -5px;
  background: var(--term-tip-bg); border-radius: 2px; transform: rotate(45deg);
}
.term-tip.is-below::after { bottom: auto; top: -5px; }
.term-tip__head { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.term-tip__t { font-size: 14px; font-weight: 600; letter-spacing: -0.005em; }
.term-tip__en { font-size: 12.5px; color: var(--term-tip-accent); }
.term-tip__es { margin: 2px 0 0; font-size: 12px; color: var(--term-tip-muted); }
.term-tip__d { margin: 6px 0 0; }
.term-tip__f {
  margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--term-tip-rule);
  font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5; color: var(--term-tip-muted);
}

@media (prefers-reduced-motion: reduce) {
  .term-tip, .term-tip.is-below { transition: none; transform: none; }
  abbr.term { transition: none; }
}

/* logos:brand-start */
/* Generado por `node _build/integraciones/build-brand-tokens.js`. No editar a mano:
   se reescribe entero. El color de cada marca es el que publica su empresa,
   oscurecido lo justo para llegar a 3:1 sobre CADA uno de los suelos donde
   aterriza: #f5f5f5 y #ffffff (la celda del hero y la tarjeta del
   catalogo). El original va en el comentario de cada linea. */
:root {
  --brand-hijiffy: #1a1e42; /* HiJiffy: #1a1e42, sin tocar, 14.72:1 */
  --brand-holded: #ef4444; /* Holded: #ef4444, sin tocar, 3.45:1 */
  --brand-monei: #009d90; /* Monei: #00c1b1 -> 3.09:1 */
  --brand-pricelabs: #ff4646; /* PriceLabs: #ff4646, sin tocar, 3.10:1 */
  --brand-redsys: #d17522; /* Redsys: #dc7c26 -> 3.07:1 */
  --brand-revbell: #022626; /* Revbell: #022626, sin tocar, 14.73:1 */
  --brand-salto-ks: #111111; /* Salto KS: #111111, sin tocar, 17.32:1 */
  --brand-sap: #0094e5; /* SAP: #0faaff -> 3.03:1 */
  --brand-siteminder: #00033b; /* SiteMinder: #00033b, sin tocar, 17.90:1 */
  --brand-stripe: #635bff; /* Stripe: #635bff, sin tocar, 4.31:1 */
}

/* El puente: cada elemento que pinta una marca lleva data-brand="<slug>" y
   con esto hereda su color en una sola variable, asi el consumidor escribe
   UNA regla de hover y no cuarenta. Sin token, `--brand` no existe y el
   hover cae en el color de reposo: ninguna marca se queda invisible. */
[data-brand="hijiffy"] { --brand: var(--brand-hijiffy); }
[data-brand="holded"] { --brand: var(--brand-holded); }
[data-brand="monei"] { --brand: var(--brand-monei); }
[data-brand="pricelabs"] { --brand: var(--brand-pricelabs); }
[data-brand="redsys"] { --brand: var(--brand-redsys); }
[data-brand="revbell"] { --brand: var(--brand-revbell); }
[data-brand="salto-ks"] { --brand: var(--brand-salto-ks); }
[data-brand="sap"] { --brand: var(--brand-sap); }
[data-brand="siteminder"] { --brand: var(--brand-siteminder); }
[data-brand="stripe"] { --brand: var(--brand-stripe); }

[data-brand="a3"] { --mark: url("assets/img/integraciones/a3.svg"); --mark-ratio: 24.93 / 24; }
[data-brand="agora"] { --mark: url("assets/img/integraciones/agora.svg"); --mark-ratio: 53.28 / 24; }
[data-brand="asincorp"] { --mark: url("assets/img/integraciones/asincorp.svg"); --mark-ratio: 79.5 / 24; }
[data-brand="cashdro"] { --mark: url("assets/img/integraciones/cashdro.svg"); --mark-ratio: 75.59 / 24; }
[data-brand="channex"] { --mark: url("assets/img/integraciones/channex.svg"); --mark-ratio: 78.41 / 24; }
[data-brand="char"] { --mark: url("assets/img/integraciones/char.svg"); --mark-ratio: 42.72 / 24; }
[data-brand="clavecon"] { --mark: url("assets/img/integraciones/clavecon.svg"); --mark-ratio: 82.19 / 24; }
[data-brand="clientify"] { --mark: url("assets/img/integraciones/clientify.svg"); --mark-ratio: 75.08 / 24; }
[data-brand="danalock"] { --mark: url("assets/img/integraciones/danalock.svg"); --mark-ratio: 84.08 / 24; }
[data-brand="dataria"] { --mark: url("assets/img/integraciones/dataria.svg"); --mark-ratio: 64.02 / 24; }
[data-brand="hijiffy"] { --mark: url("assets/img/integraciones/hijiffy.svg"); --mark-ratio: 108.66 / 24; }
[data-brand="hiopos"] { --mark: url("assets/img/integraciones/hiopos.svg"); --mark-ratio: 61.46 / 24; }
[data-brand="holded"] { --mark: url("assets/img/integraciones/holded.svg"); --mark-ratio: 145.05 / 24; }
[data-brand="hubos"] { --mark: url("assets/img/integraciones/hubos.svg"); --mark-ratio: 61.27 / 24; }
[data-brand="monei"] { --mark: url("assets/img/integraciones/monei.svg"); --mark-ratio: 110.43 / 24; }
[data-brand="neobookings"] { --mark: url("assets/img/integraciones/neobookings.svg"); --mark-ratio: 116.4 / 24; }
[data-brand="paycomet"] { --mark: url("assets/img/integraciones/paycomet.svg"); --mark-ratio: 130.53 / 24; }
[data-brand="prestige"] { --mark: url("assets/img/integraciones/prestige.svg"); --mark-ratio: 74.01 / 24; }
[data-brand="pricelabs"] { --mark: url("assets/img/integraciones/pricelabs.svg"); --mark-ratio: 88.22 / 24; }
[data-brand="prinex"] { --mark: url("assets/img/integraciones/prinex.svg"); --mark-ratio: 57.3 / 24; }
[data-brand="redsys"] { --mark: url("assets/img/integraciones/redsys.svg"); --mark-ratio: 23.96 / 24; }
[data-brand="revbell"] { --mark: url("assets/img/integraciones/revbell.svg"); --mark-ratio: 89.02 / 24; }
[data-brand="revctrldata"] { --mark: url("assets/img/integraciones/revctrldata.svg"); --mark-ratio: 108.36 / 24; }
[data-brand="revo"] { --mark: url("assets/img/integraciones/revo.svg"); --mark-ratio: 120.47 / 24; }
[data-brand="revtool"] { --mark: url("assets/img/integraciones/revtool.svg"); --mark-ratio: 68.21 / 24; }
[data-brand="roomcloud"] { --mark: url("assets/img/integraciones/roomcloud.svg"); --mark-ratio: 102.59 / 24; }
[data-brand="roompricegenie"] { --mark: url("assets/img/integraciones/roompricegenie.svg"); --mark-ratio: 146.97 / 24; }
[data-brand="sage-200"] { --mark: url("assets/img/integraciones/sage-200.svg"); --mark-ratio: 43.08 / 24; }
[data-brand="sage-50"] { --mark: url("assets/img/integraciones/sage-50.svg"); --mark-ratio: 43.08 / 24; }
[data-brand="salto-ks"] { --mark: url("assets/img/integraciones/salto-ks.svg"); --mark-ratio: 90 / 24; }
[data-brand="sap"] { --mark: url("assets/img/integraciones/sap.svg"); --mark-ratio: 48.53 / 24; }
[data-brand="siigo"] { --mark: url("assets/img/integraciones/siigo.svg"); --mark-ratio: 44.33 / 24; }
[data-brand="siteminder"] { --mark: url("assets/img/integraciones/siteminder.svg"); --mark-ratio: 169.78 / 24; }
[data-brand="spalopia"] { --mark: url("assets/img/integraciones/spalopia.svg"); --mark-ratio: 77.31 / 24; }
[data-brand="stripe"] { --mark: url("assets/img/integraciones/stripe.svg"); --mark-ratio: 17.13 / 24; }
[data-brand="tesa"] { --mark: url("assets/img/integraciones/tesa.svg"); --mark-ratio: 40.75 / 24; }
[data-brand="thenetrevenue"] { --mark: url("assets/img/integraciones/thenetrevenue.svg"); --mark-ratio: 139.86 / 24; }
[data-brand="ttlock"] { --mark: url("assets/img/integraciones/ttlock.svg"); --mark-ratio: 64.87 / 24; }
[data-brand="wubook"] { --mark: url("assets/img/integraciones/wubook.svg"); --mark-ratio: 74.54 / 24; }
[data-brand="yacan-smart"] { --mark: url("assets/img/integraciones/yacan-smart.svg"); --mark-ratio: 111.76 / 24; }
[data-brand="yieldplanet"] { --mark: url("assets/img/integraciones/yieldplanet.svg"); --mark-ratio: 101.15 / 24; }
/* logos:brand-end */
