/* ============================================
   Vulsar — tokens (spec sección 0)
   ============================================ */
:root{
  --bg:            #04040c;
  --surface:       #0f0f22;
  --text:          #eef0f6;
  --text-dim:      #aeb2c9;
  --text-mute:     #6a6f90;
  --border:        rgba(255,255,255,0.10);
  --accent:        #2ED9C3;
  --accent-hover:  #4FE6D3;

  --nav-h: 72px;
  --section-py: 64px;
  --container-w: 1160px;

  /* Spacing scale — use these instead of one-off px values for padding/
     margin/gap so spacing stays consistent across the site. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 32px;
  --space-6: 40px;
}
@media (min-width:900px){
  :root{ --section-py: 128px; }
}

*,*::before,*::after{margin:0;box-sizing:border-box}
/* overflow-x:hidden lives on BOTH html and body deliberately — iOS Safari's
   elastic/rubber-band overscroll bounce is governed by whether the
   document's scrolling root (html) itself has any horizontal
   scrollability, not just body's. Clipping only on body isn't reliably
   enough to suppress the bounce cross-browser even when it visually hides
   the overflowing content, so both need it. */
html{ scroll-behavior:smooth; overflow-x:hidden; }
body{
  /* Ambient continuity now comes entirely from the starfield+nebula canvas
     (js/starfield.js) instead of a static painted-in background — this used
     to also carry 5 radial-gradient teal pools and a separate .ambient-field
     glow-blob layer of its own; both read as flat/muddy washes once the
     live filament nebula was layered on top, so they were removed. Plain
     solid bg here. */
  background:var(--bg);
  color:var(--text);
  font-family:"Onest",sans-serif;
  font-weight:400;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden; /* lets decorative elements (e.g. the dashboard
    assembly loop's oversized entrance state) bleed past their own box
    without ever producing a horizontal scrollbar. */
}
/* Starfield + nebula overlay — own Three.js canvas (js/starfield.js), fixed
   above normal content but well below the navbar. pointer-events:none so it
   never blocks clicks/hovers; only becomes visible once the hero has
   scrolled past (js toggles .is-visible via IntersectionObserver), fading
   in with a plain CSS transition. */
.starfield-canvas{
  position:fixed; inset:0; z-index:2;
  display:block; pointer-events:none;
  opacity:0;
  transition:opacity 1.4s ease;
}
.starfield-canvas.is-visible{ opacity:1; }
@media (prefers-reduced-motion:reduce){
  .starfield-canvas{ transition:none; }
}

img,svg{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button{font-family:inherit}
section[id]{scroll-margin-top:calc(var(--nav-h) + 12px)}

.container{
  max-width:var(--container-w);
  margin:0 auto;
  padding:0 clamp(20px,6vw,48px);
}
.container-narrow{ max-width:760px; }

.section-head{ max-width:640px; margin-bottom:clamp(32px,5vw,72px); }
.section-head.center{ margin-left:auto; margin-right:auto; text-align:center; }

h2{
  font-size:clamp(1.7rem,4vw,2.5rem);
  font-weight:600;
  letter-spacing:-.02em;
  line-height:1.15;
  color:var(--text);
}
h3{ font-size:1.15rem; font-weight:600; color:var(--text); }
.lede{
  margin-top:.9rem;
  color:var(--text-dim);
  font-weight:300;
  font-size:clamp(1rem,1.4vw,1.1rem);
  line-height:1.6;
}
p{ color:var(--text-dim); line-height:1.6; }

/* ============================================
   Nav
   ============================================ */
.nav{
  position:fixed; top:0; left:0; right:0; z-index:100;
  height:var(--nav-h);
  display:flex; align-items:center;
  background:rgba(4,4,12,0.55);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  border-bottom:1px solid var(--border);
}
.nav-inner{
  width:100%;
  padding:0 clamp(20px,6vw,48px);
  display:flex; align-items:center; justify-content:space-between;
}
.logo{ display:flex; align-items:center; gap:10px; }
.logo-mark{ width:22px; height:22px; flex-shrink:0; color:var(--accent); }
/* .logo-word-svg is the navbar's inline wordmark SVG — kept separate
   from .logo-word (plain text, still used as-is in the footer) so
   sizing this for an SVG box doesn't strip the footer span's font
   styling down to nothing. */
.logo-word-svg{ width:82px; height:10px; color:var(--text); }
.logo-word{ font-weight:600; font-size:16px; letter-spacing:-.01em; color:var(--text); }

.nav-links{ display:flex; align-items:center; gap:28px; }
.nav-links a{
  font-size:14px; font-weight:500; color:var(--text-dim);
  transition:color .2s ease;
}
.nav-links a:hover, .nav-links a.active{ color:var(--accent); }

.nav-menu a{
  position:relative;
  padding:8px 16px;
  border-radius:999px;
  color:var(--text-dim);
  transition:background .25s ease, color .25s ease;
}
.nav-menu a:hover{ background:rgba(255,255,255,0.07); color:var(--text); }
.nav-menu a.active{ background:rgba(46,217,195,0.14); color:var(--accent); }
.nav-menu a.active:hover{ background:rgba(46,217,195,0.16); }

.nav-links a.btn-nav{
  display:inline-flex; align-items:center; height:38px; padding:0 18px;
  border-radius:999px; border:0;
  background:#fff; color:var(--bg); font-size:13px; font-weight:600;
  transition:all .2s ease;
}
.nav-links a.btn-nav:hover{ color:var(--bg); transform:translateY(-1px); box-shadow:0 6px 20px rgba(255,255,255,.22); }

@media (min-width:900px){
  /* True 3-column grid (1fr auto 1fr): the two outer columns are equal,
     so the middle column sits at the exact horizontal center of the bar
     regardless of how wide the logo or CTA are — unlike space-between,
     where the center depends on their widths. .nav-menu (unstyled, no
     background/border — just a grouping box) is the single grid item
     that fills the center column; .nav-links stays display:contents so
     it doesn't add a layer of its own. */
  .nav-inner{
    display:grid; grid-template-columns:1fr auto 1fr; align-items:center;
  }
  .logo{ justify-self:start; }
  /* Desktop-only: whole logo group (isotipo + wordmark) scaled up
     together by the same factor (32/22 ≈ 1.4545, computed off the
     original 22px/82×10px mobile base each time, not off the previous
     28px step — avoids compounding rounding across successive bumps), so
     the wordmark stays exactly as much smaller than the isotipo as it
     already was. .nav's own height (var(--nav-h), 72px) is unaffected
     either way: 32px is well inside it, and .logo/.nav-inner's
     align-items:center already keeps the (now taller) group centered
     with no extra work. Mobile keeps the base 22px/82×10px values above —
     this override only exists in this >=900px block. */
  .logo-mark{ width:32px; height:32px; }
  /* align-items:center on .logo (below) centers both SVGs' BOXES, but the
     wordmark's own artwork isn't centered inside its viewBox the way the
     isotipo's is — measured by rendering each in isolation and finding
     its actual ink bounds: the isotipo fills its box edge to edge
     (~0% offset), the wordmark's ink sits about 3.6% of its own height
     above its box's center (more empty space below than above). Box-
     centering two elements whose CONTENT isn't equally centered in each
     still leaves the content looking misaligned. Nudging the wordmark
     down by that same 3.6% (of its 14.55px height here) lines up the two
     elements' actual visual centers instead of just their boxes. Desktop
     only, on purpose — the mobile logo isn't being touched. */
  .logo-word-svg{ width:119.27px; height:14.55px; transform:translateY(0.52px); }
  .nav-links{ display:contents; }
  .nav-menu{
    display:flex; align-items:center; gap:2px; justify-self:center;
    background:rgba(255,255,255,0.05);
    border-radius:999px;
    padding:6px;
  }
  #navLinks a.btn-nav{ justify-self:end; }
}

.nav-toggle{
  position:relative; /* containing block for .nav-toggle-badge below */
  display:flex; flex-direction:column; gap:5px;
  width:32px; height:32px; align-items:center; justify-content:center;
  background:none; border:0; cursor:pointer; z-index:101;
}
/* :not(.nav-toggle-badge) — without this exclusion, .nav-toggle-badge's
   own width/height/background reset below loses the specificity fight
   against this rule (class+type beats a single class) despite coming
   later in the file, which is exactly how the badge previously ended up
   rendering as a 20x2px bar instead of a square behind the X. */
.nav-toggle span:not(.nav-toggle-badge){ width:20px; height:2px; background:var(--text); transition:all .25s ease; }
.nav-toggle.open span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-toggle.open span:nth-child(2){ opacity:0; }
.nav-toggle.open span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }
/* Active-state badge behind the X — a separate absolutely-positioned
   layer filling the button's own already-fixed 32x32 box (inset:0), so it
   can never change the button's size or push/deform the navbar's height:
   the button's box stays exactly what it always was, this just paints a
   rounded fill behind it, wrapping the whole X with the ~6px gap between
   the 20px-wide X and the 32px button edge as padding on all 4 sides.
   Lighter than the navbar (not darker — the navbar's already very dark,
   a darker fill wouldn't read against it), low-opacity white so it's
   subtle but still legible as "on". */
.nav-toggle-badge{
  position:absolute; inset:0; z-index:-1;
  border-radius:10px;
  opacity:0;
  transition:opacity .2s ease, background .2s ease;
}
.nav-toggle.open .nav-toggle-badge{
  opacity:1;
  background:rgba(255,255,255,.10);
}

@media (max-width:899px){
  .nav-toggle{ display:flex; }
  /* Superseded by .nav-panel below — the desktop pill nav just hides on
     mobile now instead of reflowing into a dropdown. */
  .nav-links{ display:none; }
}
@media (min-width:900px){
  .nav-toggle{ display:none; }
}

/* ============================================
   Mobile menu — floating panel + overlay.
   ============================================
   display:none is the UNCONDITIONAL baseline here (not inside the
   max-width:899px query below) — everything else about hiding these at
   rest (opacity:0/visibility:hidden) only exists WITHIN that query, so
   without a real display:none default, >=900px got none of that: no
   position:fixed either, so .nav-panel fell back to being a plain static
   block div — full document width, height auto to its (also unconstrained
   past 899px) content, which is exactly how it rendered as a giant block
   with oversized icons on desktop. The media query below only needs to
   flip this back to display:block; everything else it already sets
   (position:fixed, opacity/visibility for the open/close animation, etc.)
   still fully applies on top of that. */
.nav-panel-overlay, .nav-panel{ display:none; }
@media (max-width:899px){
  body.nav-panel-locked{ overflow:hidden; }
  .nav-panel-overlay{
    display:block;
    position:fixed; inset:0; z-index:90;
    /* Blur only — no dark tint. Content behind reads as diffused/out of
       focus, not dimmed. */
    background:transparent;
    backdrop-filter:blur(6px);
    -webkit-backdrop-filter:blur(6px);
    opacity:0; visibility:hidden;
    transition:opacity .3s ease, visibility 0s linear .3s;
  }
  .nav-panel-overlay.open{
    opacity:1; visibility:visible;
    transition:opacity .3s ease, visibility 0s linear;
  }
  .nav-panel{
    display:block;
    position:fixed; z-index:95;
    top:calc(var(--nav-h) + 12px);
    left:clamp(20px,6vw,48px); right:clamp(20px,6vw,48px);
    max-height:calc(100vh - var(--nav-h) - 24px); overflow-y:auto;
    /* Same fill as .nav itself — the panel reads as an extension of the
       navbar unfolding, not a separately-colored surface. */
    background:rgba(4,4,12,0.55);
    backdrop-filter:blur(14px);
    -webkit-backdrop-filter:blur(14px);
    border:1px solid var(--border);
    border-radius:20px;
    box-shadow:0 24px 60px rgba(0,0,0,.45);
    opacity:0; visibility:hidden; transform:translateY(-14px);
    transform-origin:top center;
    transition:opacity .35s ease, transform .35s cubic-bezier(.22,1,.36,1), visibility 0s linear .35s;
  }
  .nav-panel.open{
    opacity:1; visibility:visible; transform:translateY(0);
    transition:opacity .35s ease, transform .35s cubic-bezier(.22,1,.36,1), visibility 0s linear;
  }
  @media (prefers-reduced-motion:reduce){
    .nav-panel-overlay, .nav-panel{ transition:opacity .01s linear, visibility 0s linear; }
    .nav-panel{ transform:none; } /* no slide, just appear/disappear */
  }
  /* No close button of its own — the hamburger (already morphing into an X,
     see .nav-toggle.open above) is the only open/close control. */
  .nav-panel-list{ display:flex; flex-direction:column; padding:10px; gap:2px; }
  .nav-panel-link{
    display:flex; align-items:center; gap:14px;
    padding:14px 12px; border-radius:14px;
    color:var(--text-dim); font-size:15px; font-weight:500;
    transition:background .2s ease, color .2s ease;
  }
  .nav-panel-link:hover, .nav-panel-link.active{ background:rgba(255,255,255,.06); color:var(--text); }
  .nav-panel-icon{
    display:flex; align-items:center; justify-content:center;
    width:20px; height:20px; flex-shrink:0; color:var(--text-mute);
  }
  .nav-panel-icon svg{ width:100%; height:100%; }
  /* Contacto — same treatment as .btn-nav on the desktop pill nav (solid
     white, dark text), so it reads as the one action among the 3 plain
     links instead of blending into the list. */
  .nav-panel-link-cta{
    margin-top:8px;
    background:#fff; color:var(--bg); font-weight:600;
  }
  .nav-panel-link-cta:hover{ background:#fff; color:var(--bg); }
  /* .nav-panel-link.active (2 classes) otherwise outranks .nav-panel-link-cta
     (1 class) on specificity alone regardless of source order, so without
     this the CTA would lose its solid white background — to the faint
     translucent .active tint instead — the moment #contacto scrolls into
     view, which is exactly the wrong moment for it to stop standing out. */
  .nav-panel-link-cta.active{ background:#fff; color:var(--bg); }
  .nav-panel-link-cta .nav-panel-icon{ color:var(--bg); }
  @media (hover:hover){
    .nav-panel-link-cta:hover{ box-shadow:0 6px 20px rgba(255,255,255,.22); }
  }
}

/* ============================================
   Buttons (reused across whole site)
   ============================================ */
.btn-primary{
  position:relative; display:inline-flex; align-items:center; gap:10px;
  height:54px; padding:0 28px; border-radius:999px; border:0; cursor:pointer;
  font-family:inherit; font-size:15px; font-weight:600; color:#04231e;
  background:var(--accent); overflow:hidden;
  transition:background .2s ease,transform .2s ease,box-shadow .2s ease;
}
.btn-primary:hover{
  background:var(--accent-hover); transform:translateY(-1px);
  box-shadow:0 8px 30px rgba(46,217,195,.32);
}
.btn-primary .sheen{
  position:absolute; top:0; left:-60%; width:45%; height:100%;
  background:linear-gradient(105deg,transparent,rgba(255,255,255,0.45),transparent);
  transform:skewX(-18deg); pointer-events:none;
}
.btn-primary.shine .sheen{ animation:sweep .6s ease forwards; }
@keyframes sweep{ from{left:-60%} to{left:135%} }
.btn-primary svg{ width:24px; height:24px; flex-shrink:0; }

.btn-ghost{
  display:inline-flex; align-items:center; height:54px; padding:0 24px;
  border-radius:999px; border:1px solid rgba(255,255,255,0.16); background:transparent;
  color:var(--text-dim); font-family:inherit; font-size:15px; font-weight:500;
  cursor:pointer; transition:all .2s ease;
}
.btn-ghost:hover{ color:var(--accent); border-color:var(--accent); }

.link-accent{ color:var(--accent); border-bottom:1px solid rgba(46,217,195,.35); transition:border-color .2s ease; }
.link-accent:hover{ border-color:var(--accent); }
@media (max-width:799px){
  /* Same fix as the pricing/product cards: no pointer to hover with, so
     the faint resting underline (.35 alpha) is the only state a tap could
     ever show — bump it to the same full color/opacity as the link text
     itself, permanently. Since this now matches the (still-present)
     :hover value exactly, an accidental tap-triggered :hover is visually
     a no-op — nothing to toggle either way. */
  .link-accent{ border-bottom-color:var(--accent); }
}

.link-consultar{
  display:inline-flex; align-items:center; justify-content:center;
  margin-top:auto; padding:11px 20px;
  border-radius:999px; border:1px solid rgba(46,217,195,.45);
  color:var(--accent); font-size:14px; font-weight:600;
  transition:background .2s ease,color .2s ease,border-color .2s ease;
}
.link-consultar:hover{ background:var(--accent); color:#04231e; border-color:var(--accent); }
/* Solid variant — same shape/sizing as .link-consultar, filled instead of
   outline, for the "Software a medida" CTA (mirrors .btn-plan-select-solid,
   the featured pricing card's filled button). */
.link-consultar-solid{ background:var(--accent); color:#04231e; border-color:transparent; }
.link-consultar-solid:hover{ background:var(--accent-hover); border-color:var(--accent-hover); }

/* ============================================
   Hero — nebula shader background + starfield + fluid ink cursor trail
   ============================================ */
.hero{ position:relative; width:100%; height:100vh; min-height:640px; overflow:hidden; background:var(--bg); }

#bgCanvas{
  position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0;
  opacity:0; transition:opacity 1.1s ease;
}
.stars{ position:absolute; inset:0; z-index:1; }
#inkCanvas{
  position:absolute; inset:0; width:100%; height:100%; display:block; z-index:2; pointer-events:none;
  opacity:0; transition:opacity 1.1s ease;
}
.hero.webgl-active #bgCanvas, .hero.webgl-active #inkCanvas{ opacity:1; }
.stars i{ position:absolute; background:#fff; border-radius:50%; animation:tw 6s ease-in-out infinite; }
@keyframes tw{ 0%,100%{opacity:.18} 50%{opacity:.5} }
.stars i.bright{ animation:twb 5s ease-in-out infinite; }
@keyframes twb{ 0%,100%{opacity:.55;transform:scale(1)} 50%{opacity:1;transform:scale(1.25)} }
.stars i.bright::after{
  content:""; position:absolute; inset:-360%; border-radius:50%;
  background:radial-gradient(circle,rgba(180,255,245,0.22),transparent 60%);
}
.stars i.core{ background:#eafffb; box-shadow:0 0 6px 1px rgba(255,255,255,0.9),0 0 2px rgba(255,255,255,1); }
.float{ position:absolute; animation:floatMove 14s ease-in-out infinite alternate; }
@keyframes floatMove{ from{transform:translate(0,0)} to{transform:translate(var(--fx,6px),var(--fy,-8px))} }

.hero .scrim{
  position:absolute; inset:0; z-index:5; pointer-events:none;
  background:radial-gradient(120% 100% at 22% 34%, rgba(4,4,12,0.5) 0%, rgba(4,4,12,0.2) 42%, transparent 72%);
}
.hero .content{
  position:relative; z-index:10; height:100%; width:100%; display:flex; flex-direction:column;
  align-items:center; justify-content:center; padding:0 clamp(20px,8vw,64px);
}
.hero .hero-panel{
  position:relative; display:flex; flex-direction:column; align-items:center;
  text-align:center; max-width:min(68rem,92vw);
  padding:clamp(40px,7vw,72px) clamp(28px,6vw,64px);
}
/* Backdrop lives on its own layer behind the text/buttons (not on the panel
   itself), so the radial mask can fade its blur+darkening out softly at the
   edges without ever touching the opacity of the actual content — text and
   CTAs stay fully legible no matter how close they sit to the fade zone. */
.hero .hero-panel-bg{
  position:absolute; inset:-8% -6%; z-index:-1; pointer-events:none;
  background:rgba(4,4,12,0.52);
  backdrop-filter:blur(28px) saturate(120%);
  -webkit-backdrop-filter:blur(28px) saturate(120%);
  -webkit-mask-image:radial-gradient(ellipse 62% 68% at 50% 50%, #000 0%, #000 38%, transparent 80%);
  mask-image:radial-gradient(ellipse 62% 68% at 50% 50%, #000 0%, #000 38%, transparent 80%);
}
.hero h1{
  font-size:clamp(2.1rem,5.2vw,4.6rem); font-weight:600; color:var(--text);
  line-height:1.07; letter-spacing:-.03em; max-width:16em;
}
.hero .sub{
  margin-top:1.4rem; font-size:clamp(1rem,1.5vw,1.2rem); color:var(--text-dim);
  font-weight:300; max-width:34rem; line-height:1.55;
}
.hero .cta{ margin-top:2.4rem; display:flex; gap:14px; align-items:center; justify-content:center; flex-wrap:wrap; }

@media (prefers-reduced-motion:reduce){
  .stars .float{ animation:none !important; }
  #bgCanvas, #inkCanvas{ transition:none; }
}

/* ============================================
   Cards con CTA (Servicios — el borde animado vive solo en :hover)
   ============================================ */
.plans-grid{
  display:grid; grid-template-columns:1fr; gap:20px;
  margin-bottom:clamp(48px,7vw,80px);
}
@media (min-width:800px){
  .plans-grid{ grid-template-columns:repeat(3,1fr); gap:24px; }
}
.card-plan{
  --card-bg: linear-gradient(160deg, #14142c 0%, var(--surface) 62%);
  --card-cover-bg: var(--card-bg); /* fully-opaque stand-in for the ring's
    padding-box cover — see .card-plan-featured for why this can't just
    reuse --card-bg unconditionally. */
  position:relative;
  isolation:isolate; /* own stacking context, so ::before's z-index:-1 sits
    behind the card's text/CTA but still above its own flat background —
    without this it paints as a positioned descendant, i.e. above the
    non-positioned content, and hides it. */
  display:flex; flex-direction:column;
  /* Sequential order: name/price → mantenimiento → CTA → divider →
     bullets. All 3 cards list the same 7 features (see .card-features),
     so their natural heights already land within a few px of each other —
     no space-between/anchoring trick needed to equalize them. */
  background:var(--card-bg);
  border:1px solid var(--border);
  border-radius:22px;
  padding:var(--space-4);
  box-shadow:0 22px 48px rgba(0,0,0,.32);
  transition:border-color .3s ease,box-shadow .3s ease;
}
/* transform is also what the scroll-reveal (GSAP) animates on entrance;
   a plain CSS transition on transform fights that tween frame-by-frame and
   never settles. Gate the hover-lift's transition behind .is-revealed,
   a class motion.js adds once the entrance tween completes. */
.card-plan.is-revealed{ transition:transform .3s ease,border-color .3s ease,box-shadow .3s ease; }
/* hover:hover — real pointer only. Touchscreens report hover:none, so this
   never fires on tap (and never gets "stuck" active after one). Below
   799px the look this produces is instead the card's permanent resting
   state (see the max-width:799px block near the bottom of this rule
   group) since there's no pointer there to hover with in the first place. */
@media (hover:hover){
  .card-plan:hover{
    transform:translateY(-3px);
    border-color:rgba(46,217,195,.28);
    box-shadow:0 16px 40px rgba(0,0,0,.28);
  }
}
/* Featured plan (Sitio + panel) — stronger teal presence in the gradient
   itself carries the "recommended" read; no border of its own so the
   hover ring below reads the same here as on the other two cards. */
.card-plan-featured{
  /* Was rgba(46,217,195,.28) at the first stop — semi-transparent, so
     whatever sat behind the card (the section's own background/ambient
     glow) showed through at that corner. On the other two cards --card-bg
     has no alpha at all, so they never had this problem. That transparent
     corner is the "ghost border" — a visible seam where the translucent
     paint met whatever was behind it, only on this card. Fix: pre-mix the
     same teal.28-over-surface blend into an opaque hex (#18484f), so nothing
     behind the card can ever show through. --card-cover-bg (used by the
     hover ring's cover layer) isn't overridden here anymore — it inherits
     the base rule's `var(--card-bg)`, which now resolves to this same
     opaque value automatically. */
  --card-bg: linear-gradient(160deg, #18484f 0%, var(--surface) 74%);
  /* Neutral resting shadow, same as the other two cards — no permanent
     teal glow. An earlier teal-tinted shadow rendered as a visible glowing
     line under the card at rest; removed for the same reason as above:
     nothing should paint teal here except the gradient fill and the
     badge. */
}
/* Landing (1) and Premium (3) — both fully opaque already (no alpha in
   --card-bg), but their default fill (#14142c fading to --surface at just
   62%) sits close enough in value to the page's own near-black background
   that they read as washed-out/translucent next to the vivid nebula behind
   them. Lighter first stop + later fade point gives them more visual body
   without borrowing the featured card's teal identity. */
#plan-landing, #plan-premium{
  --card-bg: linear-gradient(160deg, #1c1c3a 0%, var(--surface) 80%);
}
/* Inline next to the plan name, not floating off the top edge — reads as
   part of the card, not a separate sticker. Tinted outline, not a solid
   fill, so it doesn't fight the solid CTA pill for "the one loud teal
   thing" on this card. min-height is fixed and applied on all 3 cards
   (not just the featured one) — the badge wraps taller than the bare h3
   text alone, so without a shared fixed height the featured card's
   content started ~5px lower than Landing/Premium and threw off the
   bottom alignment of everything below it. */
.card-plan-name-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  min-height:21px;
}
.card-badge{
  display:inline-flex; align-items:center;
  background:rgba(46,217,195,.14); color:var(--accent);
  border:1px solid rgba(46,217,195,.5);
  font-size:10px; font-weight:700; letter-spacing:.02em;
  padding:3px 10px; border-radius:999px; white-space:nowrap;
}
/* Rotating gradient ring — only on hover, a single sweep of light tracing
   the card edge. background-clip layering: an opaque "cover" clipped to
   padding-box sits above a conic-gradient clipped to border-box, leaving
   only the 1px border band showing color. The cover uses --card-cover-bg
   (always fully opaque) rather than --card-bg directly, so it blends with
   each card's look but never lets the ring bleed through the fill. The
   angle itself animates via @property, so the sweep moves without
   transform ever touching this element. */
@property --card-angle{ syntax:'<angle>'; inherits:false; initial-value:0deg; }
.card-plan::before{
  content:"";
  position:absolute; inset:-1px; border-radius:inherit; z-index:-1;
  border:1px solid transparent;
  background:
    var(--card-cover-bg) padding-box,
    conic-gradient(from var(--card-angle),
      transparent 0deg, transparent 265deg,
      var(--accent) 300deg, var(--accent-hover) 325deg, var(--accent) 350deg, transparent 360deg) border-box;
  opacity:0;
  /* Belt-and-suspenders on top of opacity:0 — visibility:hidden takes this
     element out of paint entirely at rest, so there's no layered-
     background-clip + border-radius + opacity compositing edge case left
     that could leave any trace of it visible before hover. The visibility
     transition is delayed on the way out (only flips after opacity finishes
     fading) and immediate on the way in, so the fade itself still looks
     the same. */
  visibility:hidden;
  pointer-events:none;
  transition:opacity .4s ease, visibility 0s linear .4s;
  animation:cardRingSpin 3.6s linear infinite;
  animation-play-state:paused;
}
@media (hover:hover){
  .card-plan:hover::before{
    opacity:1; visibility:visible; animation-play-state:running;
    transition:opacity .4s ease, visibility 0s linear;
  }
}
@keyframes cardRingSpin{ to{ --card-angle:360deg; } }

.card-plan.pulse{ animation:cardPulse 2.4s ease-out 1; }
@keyframes cardPulse{
  0%{ box-shadow:0 0 0 rgba(46,217,195,0); border-color:var(--border); }
  30%{ box-shadow:0 0 0 6px rgba(46,217,195,.16),0 0 40px rgba(46,217,195,.35); border-color:var(--accent); }
  100%{ box-shadow:0 0 0 rgba(46,217,195,0); border-color:var(--border); }
}
@media (prefers-reduced-motion:reduce){
  .card-plan::before{ animation:none; }
  .card-plan:hover{ transform:none; }
}
/* No pointer to hover with below 800px, so the 3 cards show the same
   border/shadow/ring look permanently instead — fixed, not a reaction to
   tap (the hover rules above are gated behind hover:hover so touch never
   triggers or "sticks" them). The lift transform is deliberately left out
   here: translateY(-3px) reads as motion relative to a resting state, and
   there is no resting state to lift away from once this is the only state. */
@media (max-width:799px){
  .card-plan{
    border-color:rgba(46,217,195,.28);
    box-shadow:0 16px 40px rgba(0,0,0,.28);
  }
  .card-plan::before{
    opacity:1; visibility:visible; animation-play-state:running;
  }
}
/* Hierarchy: name is a quiet label (scanned first, but not competing),
   price is the one dominant number WITHIN the card — but clearly smaller
   than the section h2 — everything else, including the CTA, descends
   further from there. */
.card-plan-header{ margin-bottom:0; }
.card-plan h3{
  font-size:.8rem; font-weight:600; color:var(--text-dim);
  text-transform:uppercase; letter-spacing:.07em;
}
.card-plan .price{
  margin-top:var(--space-5); display:flex; align-items:baseline; gap:6px;
}
.price-amount, .price-unit{
  font-size:clamp(2rem,3vw,2.3rem); font-weight:700;
  color:var(--text); letter-spacing:-.02em; line-height:1;
}
/* Premium's "en adelante" — a trailing secondary note, same treatment the
   old "desde" prefix used, just after the number instead of before it. */
.price-suffix{ font-size:12px; font-weight:500; color:var(--text-dim); }

/* Plain single-line divider — the lead-in to the feature list, which sits
   at the very end of the card (name/price → mantenimiento → CTA → divider
   → bullets). No label, just a rule. */
.card-divider{
  margin:var(--space-4) 0 var(--space-4);
  height:1px; background:var(--border);
}
.card-features{
  /* <ul> carries a 40px user-agent padding-left for marker indentation —
     list-style:none removes the marker glyph but not that reserved space,
     so it's reset here as well. Rows themselves stay close together
     (dense list) — the generous spacing lives in the gaps BETWEEN blocks
     (price/mantenimiento/CTA/divider), not inside the list. */
  margin:0; padding:0; list-style:none;
  display:flex; flex-direction:column; gap:var(--space-2);
}
.card-features li{
  display:flex; align-items:flex-start; gap:10px;
  font-size:12px; color:var(--text-dim); line-height:1.45;
}
.card-features svg{
  width:16px; height:16px; flex-shrink:0; margin-top:2px; color:var(--accent);
}
/* Every card lists the full feature set (same items, same order) so the 3
   cards land at the same height without padding tricks — items a plan
   doesn't include stay in place but go quiet: muted text, an ✕ instead of
   a check. Still no strikethrough — being unavailable isn't an error, just
   a lower tier. */
.card-features li.disabled{ color:var(--text-mute); }
.card-features li.disabled svg{ color:var(--text-mute); }

/* Maintenance toggle — sits right under the price now (label → price →
   mantenimiento → CTA → divider → bullets), pure CSS: a hidden checkbox
   drives both the switch look (adjacent-sibling selector) and which
   helper line shows (:has, on the ancestor), no JS required to stay in
   sync. */
.maintenance-toggle{ margin-top:var(--space-5); }
.maintenance-toggle-row{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
}
.maintenance-label{ font-size:13.5px; color:var(--text); font-weight:500; }
.toggle-input{
  position:absolute; width:1px; height:1px; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0; padding:0;
}
.toggle-switch{
  position:relative; display:inline-block; flex-shrink:0;
  width:38px; height:22px; border-radius:999px;
  background:rgba(255,255,255,.12); border:1px solid var(--border);
  cursor:pointer; transition:background .25s ease,border-color .25s ease;
}
.toggle-thumb{
  position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%;
  background:var(--text-dim); transition:transform .25s ease,background .25s ease;
}
.toggle-input:checked + .toggle-switch{ background:rgba(46,217,195,.35); border-color:var(--accent); }
.toggle-input:checked + .toggle-switch .toggle-thumb{ transform:translateX(16px); background:var(--accent); }
.toggle-input:focus-visible + .toggle-switch{ outline:2px solid var(--accent); outline-offset:2px; }
/* The "what's included" line stays put regardless of the switch — only the
   price line is conditional. It uses visibility (not display) so its line
   box is always reserved: flipping the switch never changes the card's
   padding, height, or the button's position underneath it. */
.maintenance-helper{ margin-top:var(--space-2); font-size:12px; color:var(--text-dim); }
.maintenance-price{
  margin-top:var(--space-1); font-size:12px; font-weight:600; color:var(--accent);
  visibility:hidden; opacity:0; transition:opacity .2s ease;
}
.maintenance-toggle:has(.toggle-input:checked) .maintenance-price{ visibility:visible; opacity:1; }

/* CTA stays quiet by default — outline only — so the price remains the one
   loud element on the card. Solid teal is reserved for the hover moment,
   except on the featured plan, which starts solid to keep pulling the eye. */
.btn-plan-select{
  display:flex; align-items:center; justify-content:center; width:100%;
  height:44px; padding:0 22px; margin-top:var(--space-5);
  border-radius:999px; border:1px solid rgba(46,217,195,.45);
  background:transparent; color:var(--accent);
  font-family:inherit; font-size:14px; font-weight:600;
  transition:background .25s ease,color .25s ease,border-color .25s ease,box-shadow .25s ease;
}
.btn-plan-select:hover{
  background:var(--accent); color:#04231e; border-color:var(--accent);
  box-shadow:0 8px 24px rgba(46,217,195,.25);
}
.btn-plan-select-solid{ background:var(--accent); color:#04231e; border-color:transparent; }
.btn-plan-select-solid:hover{ background:var(--accent-hover); border-color:var(--accent-hover); }

/* "Sitios web" and "Software a medida" are sibling subsections under the
   section's own H2 ("Nuestros servicios") — same heading level (h2) and
   same paragraph treatment, both centered on desktop. This one just
   doesn't need the divider above it, since it's the first thing after
   the section head. --sub-gap is the one spacing value both subsections
   share for every "next block" gap after their body copy (Sitios web:
   copy → pricing cards below it; Software a medida: copy → mockup →
   CTA) — title-to-copy stays a separate, smaller, already-shared 14px
   (see .service-intro p / .software-block p below) since that gap reads
   as "label to its own value", tighter than the gap to a whole next
   block. Defined once here so both subsections can never drift apart. */
.service-intro, .software-block{ --sub-gap:clamp(28px,4vw,56px); }
.service-intro{ margin-bottom:var(--sub-gap); text-align:left; }
/* Subordinate to the section h2 ("Nuestros servicios") via smaller size
   and lighter weight (500, not the main h2's bolder default) — full
   --text color, though, not dimmed: these still need to read clearly as
   their own headings, just at a lower rank than "Nuestros servicios". */
.service-intro h2, .software-block h2{
  font-size:clamp(1.15rem,2.2vw,1.4rem);
  font-weight:500;
  color:var(--text);
  /* inline-flex, not block+vertical-align:middle — vertical-align's
     "middle" is a font-metric heuristic (baseline + half the x-height)
     that doesn't reliably land on a rotated square's actual visual
     center, which is what left it looking low no matter how the margin
     was nudged. align-items:center on a real flex container measures
     the diamond's and the text's actual box heights instead, so it's
     exact regardless of font. Still inline-level as its OUTER display
     (inline-flex, not flex), so it's still just inline content inside
     .service-intro/.software-block and keeps responding to their
     text-align exactly as a plain inline heading would (centered on
     desktop, left on mobile — see the text-align rules above). */
  display:inline-flex;
  align-items:center;
}
/* Diamond bullet — a small rotated square, first flex item in the
   heading so it sits left of the text with the gap below, vertically
   centered against it by the flex container above. */
.service-intro h2::before, .software-block h2::before{
  content:"";
  width:7px; height:7px;
  margin-right:10px;
  background:var(--accent);
  transform:rotate(45deg);
  flex-shrink:0;
}
.service-intro p{ margin:14px auto 0; max-width:640px; }

.software-block{
  /* Full-width rule, matching the divider pattern used elsewhere
     (.steps-grid, .faq-list) — the max-width used to live on this element
     itself, so the border-top stopped 640px in instead of spanning the
     container like every other section divider, reading as a stray line
     rather than a subsection break. */
  border-top:1px solid var(--border);
  padding-top:clamp(40px,6vw,64px);
  text-align:left;
}
/* Mobile-first: left is the base/default for both subsections (matches
   the left-aligned reading rhythm of the rest of the section's content —
   the diamond bullet's ::before is inline content directly in front of
   the heading text, so it rides along with whichever alignment wins here,
   never independently positioned). Desktop centers both instead, below.
   MUST come after .software-block's own base rule above: two unscoped
   rules for the same selector at equal specificity resolve by source
   order regardless of which one is "meant" to be the override, so the
   later declaration always has to be the one that should actually win
   at that breakpoint. (This exact ordering mistake — the override
   sitting before the base rule it's supposed to override — is what
   broke this twice already.) */
@media (min-width:800px){
  .service-intro, .software-block{ text-align:center; }
  /* .btn-primary/.link-consultar are inline-flex, so they follow
     text-align — centers them here, flush left on their own natural
     width below 800px instead. */
}
.software-block p{ margin:14px auto var(--sub-gap); max-width:640px; }

/* Dashboard mockup — static recreation of the reference "Mockup
   software.png" (sidebar with nav icons, header with search/bell/avatar,
   6-card grid: resumen general, usuarios activos, distribución, actividad
   reciente, rendimiento, tareas pendientes). Pure HTML-independent inline
   SVG (viewBox 0 0 400 240, same as before) so it scales cleanly with
   .software-visual's own box regardless of final display size. No
   animation of its own — it renders in its final state immediately.
   .software-block's own one-time fade/slide entrance (from
   js/motion.js's reveal('.software-block', ...), gated by prefers-
   reduced-motion already at that call site) still applies to the whole
   section as a unit, mockup included, so it isn't perfectly static in
   the sense of "never moves once" — just that nothing inside it animates
   on its own afterward. Full teal + neutral-gray palette throughout (no
   blue/violet/cyan from the original reference image) — teal for the
   dominant/foreground data (line, primary bars, top donut segments,
   checked task, notification dot), neutral grays for secondary/inactive
   data (dim bars, the donut's "Otros" segment, unchecked tasks, sidebar
   icons) so hierarchy still reads without leaning on other hues. */
.software-visual{
  width:min(400px,88vw);
  aspect-ratio:400/240;
  margin:0 auto var(--sub-gap);
  position:relative;
  border-radius:18px; /* ~= the SVG's own internal rx:16 (in its 400-wide
    viewBox) at this element's real display width — doesn't need to match
    exactly, just close enough that the ring traces the same rounded
    corners the mockup itself already reads as having. */
}
/* Same rotating conic-gradient light-sweep as .card-plan (reusing its
   --card-angle/@keyframes cardRingSpin) — hover-only on desktop,
   permanently on/spinning below 800px. Different technique from the
   pricing cards though: theirs paints BEHIND the card (z-index:-1) with
   an opaque "cover" layer masking everything but a 1px band, which only
   works because the card's own content is a solid, opaque box sitting
   above it. The mockup's SVG is see-through in most of its own box (no
   single solid fill — individual cards/sidebar paint their own bits,
   the frame is stroke-only), so a layer painted BEHIND it never had
   anything to contrast against and was invisible. Instead: paint this
   ON TOP (z-index:1, above the SVG) and use a mask to punch a literal
   transparent hole out of its middle (content-box vs border-box,
   composited with exclude) — padding:1px is the only part that's ever
   actually painted, so it can't tint or cover the mockup no matter what
   the mockup looks like underneath, it just traces the edge. */
.software-visual::before{
  content:"";
  position:absolute; inset:-1px; border-radius:inherit; z-index:1;
  padding:1px;
  background: conic-gradient(from var(--card-angle),
    transparent 0deg, transparent 265deg,
    var(--accent) 300deg, var(--accent-hover) 325deg, var(--accent) 350deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  transition:opacity .4s ease, visibility 0s linear .4s;
  animation:cardRingSpin 3.6s linear infinite;
  animation-play-state:paused;
}
@media (hover:hover){
  .software-visual:hover::before{
    opacity:1; visibility:visible; animation-play-state:running;
    transition:opacity .4s ease, visibility 0s linear;
  }
}
@media (max-width:799px){
  .software-visual::before{
    opacity:1; visibility:visible; animation-play-state:running;
  }
}
@media (prefers-reduced-motion:reduce){
  .software-visual::before{ animation:none; }
}
@media (min-width:800px){
  /* A bit larger/more present on desktop, where there's room — mobile
     keeps its 88vw cap (that's not where the "too small" complaint was;
     illegible fine print there is handled by the placeholder bars below
     instead of just shrinking further). Fixed px, not a vw-based min()
     — at the narrower end of desktop widths (800–900px) a vw cap would
     have come in smaller than the old 400px, shrinking it there instead
     of growing it everywhere as asked. */
  .software-visual{ width:460px; }
}
@media (max-width:799px){
  /* Block-level with its own width, so it ignores .software-block's
     text-align:left (that only steers inline content) and stays centered
     via this margin:auto unless overridden here — drop the auto so the
     mockup lines up on the same left edge as the heading/body/CTA below
     it instead of floating centered above left-aligned text. */
  .software-visual{ margin:0 0 var(--sub-gap); }
}
.dashm{ width:100%; height:100%; display:block; overflow:hidden; }

.dashm-frame{ fill:none; stroke:rgba(46,217,195,.35); stroke-width:1.2; }
.dashm-sidebar{ fill:rgba(46,217,195,.04); }
.dashm-sidebar-divider{ stroke:var(--border); stroke-width:1; }
.dashm-logo-box{ fill:none; stroke:var(--accent); stroke-width:1.4; }
.dashm-logo-icon{ stroke:var(--accent); stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.dashm-nav-active-bg{ fill:var(--accent); }
.dashm-nav-active-icon{ fill:#04231e; }
.dashm-nav-icon{ stroke:var(--text-mute); stroke-width:1.4; }
.dashm-nav-icon circle, .dashm-nav-icon path{ fill:none; }

.dashm-title{ font:700 12px "Onest",sans-serif; fill:var(--text); }
.dashm-subtitle{ font:500 6px "Onest",sans-serif; fill:var(--text-mute); }

.dashm-search-bg{ fill:rgba(255,255,255,.05); stroke:var(--border); stroke-width:1; }
.dashm-search-icon{ stroke:var(--text-mute); stroke-width:1.3; stroke-linecap:round; fill:none; }
.dashm-search-text{ font:500 6.5px "Onest",sans-serif; fill:var(--text-mute); }

.dashm-icon-circle{ fill:none; stroke:var(--border); stroke-width:1; }
.dashm-icon-circle-glyph{ stroke:var(--text-dim); stroke-width:1.2; fill:none; stroke-linecap:round; stroke-linejoin:round; }
.dashm-notif-dot{ fill:var(--accent); }

.dashm-cardbg{ fill:var(--surface); stroke:var(--border); stroke-width:1; }
.dashm-card-title{ font:600 7.5px "Onest",sans-serif; fill:var(--text); }

/* Mobile placeholder bars — below 800px, fine-print text (deltas,
   legend, activity/task rows, search placeholder, subtitle) swaps for a
   plain rounded bar in roughly the same color the text would have been,
   instead of shrinking prose past legibility. The 3 headline stats
   (1.4K/7.4K/98%), the main title and the 6 card titles stay real text
   at every width — they're either large enough or short/important
   enough to identify each card that shrinking them further isn't worth
   losing them as text. */
.dashm-mobile-only{ display:none; }
@media (max-width:799px){
  .dashm-desktop-only{ display:none; }
  .dashm-mobile-only{ display:inline; }
}
.dashm-placeholder{ fill:rgba(255,255,255,.14); }
.dashm-placeholder-dim{ fill:rgba(255,255,255,.08); }
.dashm-placeholder-light{ fill:rgba(255,255,255,.22); }
.dashm-placeholder-accent{ fill:rgba(46,217,195,.35); }

.dashm-stat{ font:700 17px "Onest",sans-serif; fill:var(--text); }
.dashm-delta{ font:600 5.5px "Onest",sans-serif; fill:var(--accent); }

.dashm-area-line{ stroke:var(--accent); stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.dashm-line{ stroke:var(--accent); stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }

.dashm-bar{ fill:var(--accent); }
.dashm-bar-dim{ fill:rgba(255,255,255,.16); }

.dashm-donut-track{ stroke:rgba(255,255,255,.1); stroke-width:10; }
.dashm-donut-seg{ stroke-width:10; }
.dashm-donut-seg-1{ stroke:var(--accent); }
.dashm-donut-seg-2{ stroke:var(--accent-hover); }
.dashm-donut-seg-3{ stroke:rgba(255,255,255,.22); }
.dashm-legend-dot-1{ fill:var(--accent); }
.dashm-legend-dot-2{ fill:var(--accent-hover); }
.dashm-legend-dot-3{ fill:rgba(255,255,255,.35); }
.dashm-legend-label{ font:500 6px "Onest",sans-serif; fill:var(--text-dim); }
.dashm-legend-value{ font:600 6px "Onest",sans-serif; fill:var(--text); }

.dashm-activity-icon-bg{ fill:rgba(46,217,195,.12); }
.dashm-activity-icon{ stroke:var(--accent); stroke-width:1.3; stroke-linecap:round; stroke-linejoin:round; }
.dashm-activity-label{ font:500 5.4px "Onest",sans-serif; fill:var(--text-dim); }
.dashm-activity-time{ font:400 4.8px "Onest",sans-serif; fill:var(--text-mute); }

.dashm-task-check-on{ fill:var(--accent); }
.dashm-task-check-mark{ stroke:#04231e; stroke-width:1.3; stroke-linecap:round; stroke-linejoin:round; }
.dashm-task-check-off{ stroke:var(--border); stroke-width:1.3; }
.dashm-task-label{ font:500 5.7px "Onest",sans-serif; fill:var(--text-dim); }
.dashm-task-label-on{ fill:var(--text); }

/* ============================================
   Productos
   ============================================ */
.products-grid{
  display:grid; grid-template-columns:1fr; gap:20px;
  align-items:stretch; /* explicit, not just relying on the default — this
    is what forces every .card-product in a row to the height of the
    tallest one, instead of each sizing to its own content. */
}
@media (min-width:800px){
  .products-grid{ grid-template-columns:repeat(3,1fr); gap:24px; }
}
/* Informational, not transactional — no price, no switch, so the card
   itself has to say "different family" from the pricing cards. A teal
   icon chip up top does that at a glance; the hover stays a plain
   border+lift, never the pricing cards' rotating ring. */
.card-product{
  display:flex; flex-direction:column; align-items:flex-start;
  height:100%; /* fills the grid cell's stretched height (not just relies
    on it) — belt-and-suspenders so .link-consultar's margin-top:auto
    below always has real space to push against, in every card, not just
    the tallest one. */
  /* CTA pinned to the bottom via margin-top:auto on .link-consultar below. */
  border:1px solid var(--border);
  border-radius:22px;
  padding:clamp(24px,3vw,32px);
  background:transparent;
  transition:border-color .3s ease,background .3s ease;
}
.card-product.is-revealed{ transition:transform .3s ease,border-color .3s ease,background .3s ease; }
/* hover:hover — see .card-plan:hover above for why (real pointer only,
   so touch never triggers or "sticks" it). */
@media (hover:hover){
  .card-product:hover{
    transform:translateY(-3px);
    border-color:rgba(46,217,195,.3);
    background:rgba(46,217,195,.035);
  }
}
@media (prefers-reduced-motion:reduce){
  .card-product:hover{ transform:none; }
}
/* No pointer below 800px — same border/background look, permanently, as
   the resting state instead of a hover reaction (see .card-plan for the
   same treatment and why the lift transform is left out). */
@media (max-width:799px){
  .card-product{
    border-color:rgba(46,217,195,.3);
    background:rgba(46,217,195,.035);
  }
}
.card-product-icon{
  display:flex; align-items:center; justify-content:center;
  width:44px; height:44px; margin-bottom:20px;
  border-radius:12px; background:rgba(46,217,195,.12);
}
.card-product-icon svg{ width:22px; height:22px; color:var(--accent); }
/* margin-bottom here (not just margin-top:auto on the button below) is
   what guarantees the gap: flex item margins don't collapse, so this
   always holds at least --space-4 between the text and the CTA. On cards
   with less text, margin-top:auto on .link-consultar still eats whatever
   space is left over on top of that, pushing it further down — more air
   there is fine. On cards with more text (little/no leftover space), this
   margin-bottom is the only thing keeping the button off the last line. */
.card-product p{ margin-top:12px; margin-bottom:var(--space-4); font-size:14.5px; }

/* ============================================
   Cómo trabajamos — connected timeline with a per-step progress ring, a
   filled node, a soft highlight behind the active step's text, and a
   connector that fills into the next node. Base CSS renders the END state
   (every ring drawn, every node filled, every connector full, every step
   highlighted) — that's what shows with no JS and under
   prefers-reduced-motion. When motion is enabled, motion.js dims
   everything back to "pending" and replays the sequence — ring draws,
   node fills, step highlights, connector fills, repeat — each time the
   section scrolls into view (see motion.js for the replay logic).
   ============================================ */
.steps-grid{
  display:flex; flex-direction:column;
}
@media (min-width:800px){
  .steps-grid{ flex-direction:row; }
}
.step{
  position:relative;
  display:flex; flex-direction:row;
  --step-gap:clamp(28px,5vw,36px);
  --step-pad-v:16px;
  /* The icon-to-text gap is tied to the same --step-pad-v as the card's
     own outer padding — the inner gap must never read as more generous
     than the outer margin it sits inside. Padding on all four sides
     gives .step-highlight (which matches .step's own box exactly, see
     below) even breathing room around the icon/text instead of hugging
     it on top/bottom while floating off it on left/right. Bottom padding
     is two things stacked: the visible breathing room under the content
     (--step-pad-v, matching the other three sides) plus the connector's
     own gap to the next card (--step-gap) — .step-highlight's bottom
     inset only backs off by --step-gap, so it still lands flush
     --step-pad-v under the content, same as the other three sides. */
  gap:var(--step-pad-v);
  padding:var(--step-pad-v) var(--step-pad-v) calc(var(--step-gap) + var(--step-pad-v));
}
.step:last-child{ padding-bottom:var(--step-pad-v); } /* no connector gap after the last card, just the same breathing room */
@media (min-width:800px){
  .step{
    flex:1; flex-direction:column; align-items:center; text-align:center;
    gap:20px;
    /* Desktop columns sit edge-to-edge with no gap (see .step-highlight
       comment below) — no side padding here, only the mobile card needs it. */
    padding:0;
  }
}
/* Soft highlight behind the whole step (number + title + body) — not a
   bordered card, just a low-opacity glow plus a perimeter stroke that
   draws itself closed once the glow has faded in. A real element (not
   ::before) because it needs an SVG child for the drawn border. Bleeds a
   little past the content, sits behind it (z-index:-1) and never shifts
   layout since it's absolutely positioned. Horizontal bleed must stay
   inside the step's own column on desktop (steps sit edge-to-edge in a
   row with no gap) or adjacent active steps' glows overlap into a visibly
   darker double-opacity band at the shared border.
   On mobile there IS no separate gap to bleed into: padding-bottom lives
   INSIDE .step's own box, so .step's rendered bottom edge already sits at
   the exact point the next step begins — any vertical bleed there crosses
   straight into the neighbor's box (bleeding down from step N and up from
   step N+1 overlap at that shared point, independent of how big the gap
   is). So on mobile the highlight is inset UP by --step-gap instead of
   bled down, stopping exactly at the content's own bottom edge — the
   whole padding-bottom gap (where the connector line lives) stays outside
   the highlight box on every step. Horizontally, mobile has only one
   column (no side-by-side neighbor to leave room for) but .container's
   own side padding is what reads as the site's margin, so the highlight
   sits flush with .step's own left/right edges instead of bleeding past
   them into that margin. Desktop bleeds/insets on all four sides since
   its layout (columns side by side, no stacking) has no shared edges in
   either axis. */
.step-highlight{
  position:absolute; z-index:-1;
  top:0; bottom:var(--step-gap); left:0; right:0;
  border-radius:20px;
}
.step:last-child .step-highlight{ bottom:0; } /* no padding-bottom to inset past */
@media (min-width:800px){
  /* left/right are positive (inset inward, not bleeding out) — the
     columns still sit edge-to-edge with no gap, but shrinking the glow
     12px in from each side leaves a visible ~24px gap between two
     adjacent active highlights instead of them touching at the shared
     column boundary. */
  .step-highlight{ top:-16px; bottom:-16px; left:12px; right:12px; }
}
/* Separate element (not the wrapper's own opacity) so the perimeter
   border SVG below can draw itself in while the glow is still hidden —
   the wrapper itself stays opacity:1 at all times, only the fill dims. */
.step-highlight-bg{
  position:absolute; inset:0; border-radius:inherit;
  background:rgba(46,217,195,.07);
  opacity:1; transition:opacity .6s ease;
}
.step-highlight.is-pending .step-highlight-bg{ opacity:0; }
.step-highlight-ring{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
.step-highlight-ring-path{
  fill:none; stroke:var(--accent); stroke-width:1.5;
  /* pathLength="100" on the <rect> (set in the HTML) normalizes the
     dasharray/dashoffset units to 0–100 regardless of the box's actual
     rendered perimeter — which varies per step and per breakpoint — so
     this single dasharray works everywhere without per-instance math. */
  stroke-dasharray:100; stroke-dashoffset:0; /* resting = fully drawn */
}
.step-node-wrap{ flex-shrink:0; width:44px; }
.step-node{
  position:relative; z-index:1; display:flex; align-items:center; justify-content:center;
  width:44px; height:44px; flex-shrink:0;
}
.step-ring{ position:absolute; inset:0; width:100%; height:100%; transform:rotate(-90deg); }
.step-ring-track{ fill:none; stroke:var(--border); stroke-width:2; }
.step-ring-progress{
  fill:none; stroke:var(--accent); stroke-width:2; stroke-linecap:round;
  stroke-dasharray:113.1; stroke-dashoffset:0; /* resting = fully drawn; JS
    animates this from 113.1→0 to "draw" the ring on each play. */
}
.step-node-fill{
  position:absolute; inset:5px; border-radius:50%; background:var(--accent);
  opacity:1; transition:opacity .3s ease;
}
.step-num{
  position:relative; z-index:1;
  font-size:13px; font-weight:700; letter-spacing:.02em; color:#04231e;
  transition:color .3s ease;
}
/* Dimmed "not reached yet" look — only ever applied by JS, and only when
   motion is enabled (see motion.js). The ring itself isn't toggled by this
   class: its dash-offset is set directly by GSAP so it can be animated. */
.step-node.is-pending .step-node-fill{ opacity:0; }
.step-node.is-pending .step-num{ color:var(--text-mute); }
/* Starts at the highlight card's own stroke edge, not the node — a direct
   child of .step now (moved out of .step-node-wrap) so its containing
   block is .step itself, matching .step-highlight's box. Vertically it's
   centered on that box: .step-highlight's top/bottom insets are
   symmetric, so .step's own vertical center already IS the highlight's
   vertical center — no separate math needed for that. */
.step-connector{
  position:absolute; overflow:hidden;
  /* Lives in the --step-gap slice of the step's padding-bottom (the part
     after --step-pad-v's breathing room, see .step) — .step-highlight's
     own bottom edge sits at exactly (100% - step-gap) too, so the
     connector picks up right where the card visually ends, and (100%,
     i.e. bottom:0) is exactly the next card's own top edge — touching
     both borders, same as the desktop version below already does. No
     neutral track — only the teal fill (below) is ever visible, drawn
     in by GSAP from zero length. */
  top:calc(100% - var(--step-gap)); bottom:0; left:22px; width:2px; transform:translateX(-50%);
}
@media (min-width:800px){
  .step-connector{
    top:50%; bottom:auto; left:calc(100% - 12px); right:-12px;
    width:auto; height:2px; transform:translateY(-50%);
  }
}
.step-connector-fill{
  position:absolute; inset:0; background:var(--accent);
  transform:scaleY(1); transform-origin:top;
}
@media (min-width:800px){
  .step-connector-fill{ transform:scaleX(1); transform-origin:left; }
}
.step-content{ padding-top:2px; }
@media (min-width:800px){
  .step-content{ max-width:220px; }
}
.step h3{ font-size:1.05rem; }
.step p{ margin-top:10px; font-size:14.5px; }
@media (prefers-reduced-motion:reduce){
  .step-highlight-bg, .step-node-fill{ transition:none; }
}

/* ============================================
   Por qué Vulsar — grouped by a teal accent rule on the left, not another
   bordered card (the site already has plenty of those between pricing and
   productos). Just enough structure to read as one group of 3, not text
   floating loose.
   ============================================ */
.why-grid{
  display:grid; grid-template-columns:1fr; gap:clamp(32px,5vw,48px);
}
@media (min-width:800px){
  .why-grid{ grid-template-columns:repeat(3,1fr); gap:32px; }
}
.why-item{
  border-left:2px solid rgba(46,217,195,.35);
  padding-left:20px;
}
.why-item .icon{ width:44px; height:44px; color:var(--accent); margin-bottom:26px; }
.why-item h3{ font-size:1.1rem; }
.why-item p{ margin-top:12px; font-size:14.5px; }

/* ============================================
   Qué es Vulsar — centered statement, not a grid section: set apart from
   the left-aligned service/product blocks below for rhythm variety.
   ============================================ */
.container-statement{ max-width:620px; text-align:center; }
#que-es .section-head.center{ margin-left:auto; margin-right:auto; }
#que-es p{ margin-top:18px; font-size:16px; }
#que-es p:first-of-type{ margin-top:0; }

/* ============================================
   FAQ
   ============================================ */
.faq-list{ display:flex; flex-direction:column; gap:12px; }
.faq-item{
  background:rgba(255,255,255,.035);
  border:1px solid var(--border);
  border-radius:16px;
  overflow:hidden;
  transition:border-color .25s ease, background-color .25s ease;
}
.faq-item:hover{ border-color:rgba(255,255,255,.2); }
.faq-item.open{ border-color:rgba(46,217,195,.32); background:rgba(255,255,255,.045); }
.faq-q{
  width:100%; display:flex; align-items:center; justify-content:space-between; gap:16px;
  background:none; border:0; cursor:pointer; text-align:left;
  padding:20px 24px; color:var(--text); font-size:15.5px; font-weight:500; font-family:inherit;
}
.faq-chevron{ width:18px; height:18px; flex-shrink:0; color:var(--text-mute); transition:transform .25s ease, color .25s ease; }
.faq-item.open .faq-chevron{ transform:rotate(180deg); color:var(--accent); }
.faq-a{ max-height:0; overflow:hidden; transition:max-height .35s ease; }
.faq-a p{ padding:0 24px 22px; font-size:14.5px; max-width:640px; }

/* ============================================
   Contacto — split asimétrico, sin card
   ============================================ */
#contacto{
  position:relative;
  background:radial-gradient(80% 60% at 15% 30%, rgba(46,217,195,0.05), transparent 60%);
}
.contact-split{
  display:grid; grid-template-columns:1fr; gap:clamp(40px,6vw,64px);
}
@media (min-width:860px){
  .contact-split{ grid-template-columns:1fr 1fr; align-items:start; column-gap:0; }
  .contact-left{ padding-right:clamp(20px,3vw,32px); }
  .contact-right{ padding-left:clamp(20px,3vw,32px); border-left:1px solid var(--border); }
}
@media (max-width:859px){
  /* Stacked layout has no shared side edge for a vertical rule to sit on
     (that's what border-left above uses at >=860px) — a horizontal one
     between the WhatsApp option and the form instead, same divider
     pattern as .software-block/.steps-grid: border-top + padding-top,
     sitting on top of whatever gap already separates the two blocks. */
  .contact-right{ border-top:1px solid var(--border); padding-top:clamp(28px,5vw,40px); }
}
.contact-left h2{ max-width:12em; }
.contact-left .btn-primary{ margin-top:2.2rem; }

.form-lead{ font-size:13px; color:var(--text-mute); text-transform:uppercase; letter-spacing:.06em; margin-bottom:24px; }
#contactForm{ display:flex; flex-direction:column; gap:22px; }
.field{ display:flex; flex-direction:column; gap:8px; }
.field label{ font-size:13px; color:var(--text-dim); }
.field input, .field textarea{
  background:rgba(255,255,255,.07); border:1.5px solid transparent; border-radius:10px;
  color:var(--text); font-family:inherit; font-size:15px; padding:14px 16px;
  resize:none; transition:background-color .2s ease, border-color .2s ease, box-shadow .2s ease;
}
/* hover:hover — real pointer only. Without this, a touch-drag while
   scrolling (finger lands on an input, moves before lifting) can trigger
   :hover on mobile and paint the border teal with nothing selected —
   :focus below is untouched and still works exactly as before. */
@media (hover:hover){
  .field input:hover, .field textarea:hover{ border-color:rgba(46,217,195,.4); }
}
.field input:focus, .field textarea:focus{
  outline:0; background:rgba(255,255,255,.1); border-color:var(--accent);
  box-shadow:0 0 0 3px rgba(46,217,195,.16);
}
.field input::placeholder, .field textarea::placeholder{ color:var(--text-mute); }
#contactForm .btn-ghost{
  align-self:flex-start; margin-top:8px; border-radius:16px;
  border:1.5px solid var(--accent); background:var(--accent); color:#04231e; font-weight:600;
  transition:background .2s ease, transform .2s ease, box-shadow .2s ease;
}
#contactForm .btn-ghost:hover{
  background:var(--accent-hover); color:#04231e;
  transform:translateY(-1px); box-shadow:0 8px 26px rgba(46,217,195,.28);
}

/* ============================================
   Footer
   ============================================ */
.site-footer{ border-top:1px solid var(--border); padding:32px 0; }
.footer-inner{
  display:flex; flex-direction:column; gap:10px; align-items:flex-start;
}
@media (min-width:600px){
  .footer-inner{ flex-direction:row; align-items:center; justify-content:space-between; }
}
.footer-inner p{ font-size:13px; color:var(--text-mute); }

/* ============================================
   Section rhythm
   ============================================ */
section:not(.hero){ padding:var(--section-py) 0; }

/* Cómo trabajamos gets extra breathing room on top of the base rhythm —
   it follows the densest part of the page (plans grid + products grid)
   and needs a clearer pause before its own sequential timeline starts. */
#como-trabajamos{ padding-top:calc(var(--section-py) + clamp(32px,6vw,88px)); }
