/* ============================================================
   components.css — reusable, self-contained UI pieces: buttons,
   the logo marquee, case cards, and the floating WhatsApp button.
   Depends on tokens in base.css.

   Flat and sober per the brand spec: no gradients, shadows,
   glows, or 3D effects anywhere in this file. Touch targets keep
   a minimum height of var(--tap-min) (44px) per WCAG 2.5.5, since
   most traffic is mobile/touch and there is no hover fallback on
   those devices.

   CTA physics: base.css already gives every <a>/<button> the
   :active scale(0.97) press and the shared transition list — this
   file only adds each variant's :hover treatment (gated behind
   (hover: hover) so touch devices never get a stuck hover state)
   and the directional micro-motion on the arrow glyph.
   ============================================================ */

/* ---------- Buttons ----------
   Level 2 of the page's 3-level CTA hierarchy (level 1: .btn-primary
   below; level 3: plain text links further down). Reads semantic
   tokens only — --border/--text, not --neutro-300/--color-carbon
   (a pre-existing deviation fixed here so this button is correctly
   theme-aware if it's ever placed on a charcoal section, same fix
   category as the --link-hover bug documented in README).
   Hover/focus-visible both lift translateY(-2px) + darken the border
   — the exact idiom .hero-service-link already uses (layout.css),
   reused here for one consistent "secondary interactive" feel
   sitewide. :active explicitly overrides back to translateY(0) +
   the universal scale(0.97): without it, :hover's translateY would
   outrank base.css's plain `a:active{transform:scale(0.97)}` by
   specificity whenever both apply at once (pressing while hovering,
   the normal desktop case), leaving the press silently un-felt. */
.btn-secondary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 1rem 1.375rem; border-radius: var(--radius-sm); font-family: var(--font-body); font-size: var(--text-body); font-weight: 500;
  border: 1px solid var(--border); color: var(--text);
  transition: border-color var(--duration-exit) var(--ease-out),
              color var(--duration-exit) var(--ease-out),
              transform var(--duration-exit) var(--ease-out);
}

@media (hover: hover) {
  .btn-secondary:hover {
    border-color: var(--text);
    transform: translateY(-2px);
    transition-duration: var(--duration-enter);
  }
}
.btn-secondary:focus-visible {
  border-color: var(--text);
  transform: translateY(-2px);
  transition-duration: var(--duration-enter);
}
.btn-secondary:active {
  transform: translateY(0) scale(0.97);
  transition-duration: var(--duration-press);
}

/* Small variant: the case-card "Ver sitio" button — same shape and
   tokens, tighter padding/type since it sits inside a compact card
   rather than standing alone. min-height stays var(--tap-min)
   (inherited) — "small" means less padding, never a sub-44px target. */
.btn-secondary-sm { padding: 0.625rem 1rem; font-size: var(--text-small); }

/* ---------- Primary CTA: full inversion + letterpress ----------
   Rest/hover is a straight color inversion, matching the floating
   WhatsApp button — never a lavender fill (the brand spec forbids
   accent as a large surface, and mixing it with carbon read as
   muddy). Border is always 1px, always present, only its color
   ever changes — width never transitions, so it can't shift layout.

   The offset block behind it is a print-register effect (a solid
   duplicate, no blur/gradient), not a shadow: it never moves, the
   button travels 0 -> 2px -> 4px toward it across rest/hover/active
   and lands flush on it at :active, which is what reads as an
   actual press rather than a decorative hover. Kept to 4px — any
   larger starts reading as a shadow rather than a registration mark.

   Two layers, not one: an element's OWN background always paints
   before (behind) even a negative-z-index pseudo-element of its own
   (CSS paints a box's background first, full stop — z-index only
   reorders content relative to OTHER stacking contexts). A single
   .btn-primary{background:carbon} with a z-index:-1 ::before would
   therefore paint the lavanda block ON TOP of that background across
   most of the face, not tucked behind it. So .btn-primary is just
   the positioning shell (transparent, sized to its child); the real
   visible face — background, text, border — lives on the inner
   .btn-primary-face span, given z-index:1 so it wins over ::before's
   z-index:0 as two siblings compared directly. */
.btn-primary {
  position: relative;
  display: inline-block;
}
.btn-primary::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--color-lavanda);
  transform: translate(4px, 4px);
}
.btn-primary-face {
  position: relative;
  z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 1rem 1.375rem; border-radius: var(--radius-sm); font-family: var(--font-body); font-size: var(--text-body); font-weight: 500;
  /* --btn-primary-bg/text, not raw carbon/crema: this button also
     has to work inside the charcoal mobile menu (see layout.css),
     where a hardcoded carbon face would sit on a carbon panel and
     vanish the same way the link-hover bug did. Rest is the
     surface's colors INVERTED (so it always reads as a filled,
     contrasting block); hover below swaps to the surface's OWN
     colors instead, same inversion logic run the other direction. */
  background: var(--btn-primary-bg);
  color: var(--btn-primary-text);
  border: 1px solid transparent;
  transition: background-color var(--duration-enter) var(--ease-out),
              color var(--duration-enter) var(--ease-out),
              border-color var(--duration-enter) var(--ease-out),
              transform 120ms var(--ease-out);
}
.btn-primary:active .btn-primary-face { transform: translate(4px, 4px); }

/* Hover travels 3px of the 4px cap (was 2px) — a more deliberate
   "about to press" read on the way in, while :active still lands
   flush at the same 4px registration mark as before; the cap itself
   is unchanged (see the comment above: any larger reads as a shadow,
   not a print-register mark). :focus-visible gets the identical
   treatment, not just the generic ring, so keyboard users get the
   same "about to press" read a mouse hover gives — same parity
   pattern as .hero-service-link in layout.css. */
@media (hover: hover) {
  .btn-primary:hover .btn-primary-face {
    background: var(--bg);
    color: var(--text);
    border-color: var(--text);
    transform: translate(3px, 3px);
  }
}
.btn-primary:focus-visible .btn-primary-face {
  background: var(--bg);
  color: var(--text);
  border-color: var(--text);
  transform: translate(3px, 3px);
}

/* Groups the WhatsApp glyph + label as one flex item so
   .btn-primary-face keeps exactly two children (this + .cta-arrow)
   for its own `justify-content: space-between` — see the layout note
   above on .btn-primary-face. */
.btn-primary-label { display: inline-flex; align-items: center; gap: var(--space-1); }

/* The WhatsApp glyph: a calm identity mark at rest, a quick
   scale(1.08) "confidence pop" on hover/focus — the exact idiom
   .hero-service-icon already uses on hover (layout.css), reused here
   so the page has one consistent "icon says yes" motion rather than
   inventing a second. Settles back to scale(1) on :active in sync
   with the face's own press-landing above. */
.btn-whatsapp-icon {
  display: inline-flex; flex-shrink: 0;
  width: 1.125rem; height: 1.125rem;
  transition: transform var(--duration-exit) var(--ease-out);
}
.btn-whatsapp-icon svg { width: 100%; height: 100%; }
@media (hover: hover) {
  .btn-primary:hover .btn-whatsapp-icon {
    transform: scale(1.08);
    transition-duration: var(--duration-enter);
  }
}
.btn-primary:focus-visible .btn-whatsapp-icon {
  transform: scale(1.08);
  transition-duration: var(--duration-enter);
}
.btn-primary:active .btn-whatsapp-icon {
  transform: scale(1);
  transition-duration: var(--duration-press);
}

/* Inline SVG arrow: the "juice" on primary/prominent CTAs.
   Fast to return (--duration-exit), a touch slower to arrive
   (--duration-enter) — the asymmetry itself reads as intentional,
   not bouncy, because both ends are still linear/ease-out. */
.cta-arrow { display: inline-flex; transition: transform var(--duration-exit) var(--ease-out); }
.cta-arrow-down { display: inline-flex; transition: transform var(--duration-exit) var(--ease-out); }

/* Arrow direction follows what the click actually does, not which
   button variant it's styled as: rightward for anything opening
   WhatsApp (btn-primary, btn-secondary-as-CTA, case-link), downward
   only for the literal same-page scroll action. */
@media (hover: hover) {
  :where(.btn-primary, .btn-secondary, .link-arrow):hover .cta-arrow {
    transform: translateX(4px); transition-duration: var(--duration-enter);
  }
  .btn-secondary:hover .cta-arrow-down { transform: translateY(4px); transition-duration: var(--duration-enter); }
}
:where(.btn-primary, .btn-secondary, .link-arrow):active .cta-arrow { transform: translateX(4px); }
.btn-secondary:active .cta-arrow-down { transform: translateY(4px); }

/* "What happens next" microline — secondary reassurance text under
   the hero and contact primary CTAs only (see layout.css). Fixed
   --neutro-500 rather than a section-swapped token: it is only ever
   placed on crema sections. */
.cta-followup {
  margin: 0; font-family: var(--font-body); font-weight: 400;
  font-size: var(--text-small); line-height: 1.5; color: var(--neutro-500);
}

/* ---------- Marquee ---------- */
.marquee { padding: var(--space-2-5) 0; background: var(--bg); color: var(--text); border-block: 1px solid var(--border); overflow: hidden; white-space: nowrap; }
.marquee-track {
  display: inline-block; animation: slide 34s linear infinite;
  font-family: var(--font-body); font-weight: 500; font-size: var(--text-small);
  letter-spacing: 0.08em; text-transform: uppercase;
}
.marquee-track span { color: var(--text-secondary); }

@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
}

/* ---------- Case cards ----------
   Flat: defined by a 1px border, not a fill or blur, to keep the
   editorial "white space does the work" feel. Width is fixed per
   breakpoint in layout.css (capped at 360px — a carousel card, never
   a stretchy grid cell); height is NOT set explicitly — it comes for
   free from the flex row in layout.css stretching every .case-card
   to the tallest one (align-items: stretch is the default on the
   cross axis). .case-desc's line-clamp below plus its flex: 1 keep
   that natural height nearly identical card to card and push each
   card's own stats/link down to the same bottom edge regardless of
   how much description text a given card has. */
.case-card {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden;
}

/* Media: fixed aspect ratio so every card's art zone is the same
   height regardless of the source image's own proportions. The
   outer .case-thumb is just padding (inset margin, never full-bleed);
   the inner .case-thumb-frame carries the one visible 1px --border +
   --bg surface the brand spec calls for, so a square mark (caso2,
   70x70), a wide wordmark (caso3, 267x50) and a tall site screenshot
   (caso1, 1182x1300) all sit centered, uncropped and undistorted at
   the same modest visual size — never filling the card. No stripes:
   a pending case without real art yet should get a plain --bg tile,
   not a placeholder pattern baked into the component. */
.case-thumb {
  aspect-ratio: 16 / 10;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-2-5);
  border-bottom: 1px solid var(--border);
}
.case-thumb-frame {
  width: 100%; height: 100%;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-2);
}
.case-thumb-frame img {
  display: block;
  /* A fixed cap (not a %) so the logo reads at the same consistent
     size on every card no matter how wide the card itself gets
     across breakpoints — object-fit: contain still guarantees no
     stretching/cropping within that box. */
  max-width: 100%;
  max-height: 4.5rem;
  width: auto; height: auto;
  object-fit: contain;
}

.case-body { padding: var(--space-2-5) var(--space-2-5) var(--space-3); display: flex; flex-direction: column; gap: var(--space-1-5); flex: 1; }
.case-head { display: flex; justify-content: space-between; align-items: baseline; }
.case-head strong { font-family: var(--font-heading); font-weight: 500; font-size: var(--text-h4); }
.case-year { font-family: var(--font-body); font-size: var(--text-xs); color: var(--text-muted); }
/* No line-clamp: it was unreliably cropping (the clamped box didn't
   always reserve the right height, letting the last visible line
   overlap .case-stats below it). Equal card height across a row
   already comes for free from flex's align-items: stretch (every
   .case-card stretches to the tallest one); flex: 1 here just pushes
   this card's own stats/link down to that shared bottom edge —
   copy is kept short per case so natural length stays close without
   needing to crop anything. */
.case-desc {
  margin: 0; font-size: var(--text-small); line-height: 1.65; color: var(--text-secondary); flex: 1;
}

.case-stats { display: flex; padding-top: var(--space-2); border-top: 1px solid var(--border); }
/* Our own `display: flex` above overrides the browser's default
   [hidden] styling (author styles beat the UA stylesheet even at
   equal specificity), so it has to be restated explicitly here —
   for any future case whose metrics aren't available yet. A pending
   metric is hidden outright here, never shown as an empty/placeholder
   number. */
.case-stats[hidden] { display: none; }
.case-stats > div { flex: 1; }
/* Thin vertical rule between the two figures, same --border
   language as every other rule on the page — no gap utility, the
   rule's own padding does the spacing. */
.case-stats > div + div { border-left: 1px solid var(--border); padding-left: var(--space-2); }

/* Both figures in a card share one plain style — Inter, not
   Playfair, well under the brand spec's "3x its label" threshold
   for a highlighted figure, so neither counts against the
   max-4-highlighted-figures-per-section budget (that budget stays
   spent entirely by the stats strip's own 4 figures). Consistency
   within a card was the point: a card mixing a large Playfair
   accent with a small bold-Inter number read as two different
   kinds of information when they're the same kind. */
.case-stat-value { font-family: var(--font-body); font-weight: 700; font-size: var(--text-h3); color: var(--text); line-height: 1; }
.case-stat-label { font-size: var(--text-xs); color: var(--text-muted); margin-top: 0.3125rem; }

/* Shared text-link treatment: any inline link that isn't styled as
   a button (currently the "Ver sitio" case-study link, the plain
   nav links, and the footer links). An underline is the affordance
   that makes a link identifiable WITHOUT hovering — never hover-only
   — so it's on at rest, not revealed on hover; hover instead shifts
   color and thickens the line. text-decoration-thickness is used
   for that thickening rather than a transform: it's a paint-only
   property (no layout reflow), same performance rationale as
   preferring transform to width elsewhere, and unlike scaleX it can
   actually express "thicker" rather than "wider". Press swaps the
   inherited global scale(0.97) for a plain opacity dip, since
   scaling inline text reads oddly compared to scaling a button box.

   Hover reads --link-hover, never a literal --color-*: this is the
   exact bug that shipped previously — hover hardcoded to
   --color-carbon reads fine on crema (.link-arrow, plain .nav-link
   on the old dropdown) but is close to invisible on carbon
   (.footer-links a, and .nav-link inside the now-charcoal full-
   screen mobile menu). --link-hover is section-aware (base.css):
   carbon on crema surfaces, crema on charcoal ones.

   :not(.nav-link-cta) stays even though nothing carries that class
   anymore (the nav CTA is .main-nav-cta now, a .btn-primary, not a
   .nav-link at all): dropping it would shrink this selector's own
   specificity — .nav-link:not(.nav-link-cta) is two classes' worth,
   plain .nav-link only one — down to a tie with base.css's global
   a:hover, leaving the override correct only by source-order luck
   instead of a clear margin. Cheaper to keep an inert exclusion than
   to depend on file load order for this to keep working. */
:is(.link-arrow, .nav-link:not(.nav-link-cta), .footer-links a) {
  color: var(--acento-texto);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
  transition: color var(--duration-enter) var(--ease-out),
              text-decoration-thickness var(--duration-enter) var(--ease-out),
              opacity var(--duration-press) var(--ease-out);
}
@media (hover: hover) {
  :is(.link-arrow, .nav-link:not(.nav-link-cta), .footer-links a):hover {
    color: var(--link-hover);
    text-decoration-thickness: 2px;
  }
}
:is(.link-arrow, .nav-link:not(.nav-link-cta), .footer-links a):active {
  transform: none;
  opacity: 0.7;
}

.link-arrow {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: var(--tap-min);
  font-family: var(--font-body); font-weight: 600; font-size: var(--text-body);
}
/* Icons stay on the graphic accent regardless of the link's own
   text color, and never take the underline meant for the text. */
.link-arrow .cta-arrow { color: var(--acento-grafico); text-decoration: none; }

/* "Ver sitio": a real .btn-secondary/.btn-secondary-sm button (see
   above), not a text link — full card width on mobile so it reads as
   the card's one clear action at that size; reverts to hugging its
   own content from tablet up, where it sits beside shorter card
   copy and doesn't need the full row. */
.case-link { width: 100%; justify-content: center; }
@media (min-width: 48em) {
  .case-link { width: auto; align-self: flex-start; justify-content: space-between; }
}

/* ---------- Cases carousel nav ----------
   Level 2 of the page's CTA hierarchy, same idiom as .btn-secondary
   above, just square instead of pill and visually small since they
   sit inline with a label rather than as a standalone CTA. Always
   visible, top-right next to "3 casos" (the carousel runs at every
   breakpoint now — see layout.css — so the controls aren't
   conditional on card count the way an earlier version of this had
   them). Hover/focus-visible read semantic tokens only (border-color/
   transform, never a hardcoded color) and lift translateY(-2px);
   :active explicitly resets to translateY(0) + the universal
   scale(0.97) for the same reason .btn-secondary:active does above —
   otherwise hover's translateY would outrank the plain
   `button:active{transform:scale(0.97)}` from base.css by
   specificity whenever both apply at once. */
.cases-nav-row { display: flex; gap: var(--space-1-5); }
.cases-nav {
  display: flex; align-items: center; justify-content: center;
  width: var(--tap-min); height: var(--tap-min);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
  transition: border-color var(--duration-exit) var(--ease-out),
              transform var(--duration-exit) var(--ease-out);
}
@media (hover: hover) {
  .cases-nav:hover:not(:disabled) {
    border-color: var(--text);
    transform: translateY(-2px);
    transition-duration: var(--duration-enter);
  }
}
.cases-nav:focus-visible:not(:disabled) {
  border-color: var(--text);
  transform: translateY(-2px);
  transition-duration: var(--duration-enter);
}
.cases-nav:active:not(:disabled) {
  transform: translateY(0) scale(0.97);
  transition-duration: var(--duration-press);
}
.cases-nav:disabled { opacity: 0.35; }

/* ---------- About gallery ---------- */
.gallery-item {
  aspect-ratio: 1; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background-image: repeating-linear-gradient(135deg, var(--border) 0 1px, transparent 1px 10px);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-body); font-size: var(--text-xs); color: var(--text-muted);
  text-align: center; padding: var(--space-1-5);
}
/* A small, deliberate charcoal "beat" at card scale — not an accent
   fill (that's never a large surface, see base.css) but the same
   full-bleed-dark-as-contrast device used at section scale. */
.gallery-item.accent {
  background-image: none; background: var(--color-carbon);
  align-items: flex-end; justify-content: flex-start; padding: var(--space-2);
  color: var(--color-crema); font-size: var(--text-small); line-height: 1.35; font-weight: 500; text-align: left;
}

/* ---------- Floating WhatsApp button ----------
   Fixed, brand-flat: solid carbon circle + crema glyph, no
   gradients/shadows/bounce. Lives directly in index.html as a real
   <a> with a working href — the site's primary conversion channel
   can't depend on the module graph loading (see js/components/
   whatsappFloat.js, which only ever finds and enhances it: vibrate-
   on-click and the scroll-fade below, never creates the element).

   Deliberate exception to "components read semantic tokens, not
   --color-* directly": this button must stay the same carbon-on-
   crema regardless of which section (light or dark) happens to be
   scrolled behind it, so it reads the raw brand colors instead of
   the section-relative --bg/--text tokens. */
.whatsapp-float {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 40; /* below .site-header (z-index: 50), which contains the mobile nav panel when open */
  display: flex;
  align-items: center;
  /* No flex `gap` here: it would reserve space even while the label
     is collapsed to max-width:0, breaking the circle at rest. The
     label's own padding-left supplies the gap once it expands. */
  height: 3.5rem;  /* 56px */
  padding: 0 calc((3.5rem - 1.5rem) / 2); /* keeps the 24px glyph optically centered at rest, text-collapsed width */
  border-radius: 1.75rem;
  overflow: hidden;
  white-space: nowrap;
  background: var(--color-carbon);
  /* A thin cream edge keeps it legible over the header/footer/marquee
     sections, which share this exact fill — same "1px rule instead
     of a shadow" approach used for --border sitewide, just
     section-independent. */
  border: 1px solid rgba(var(--crema-rgb), 0.35);
  color: var(--color-crema);
  /* Visible and interactive by default — this is the no-JS state.
     The scroll-fade below is an opt-in on top of this, never a
     precondition for it. */
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  transition: opacity var(--duration-enter) var(--ease-out), transform var(--duration-enter) var(--ease-out),
              background-color var(--duration-enter) var(--ease-out), color var(--duration-enter) var(--ease-out),
              border-color var(--duration-enter) var(--ease-out);
}
.whatsapp-float:active,
.whatsapp-float.fade-ready.is-visible:active { transform: scale(0.94); }

/* Scroll-fade, opt-in only: js/components/whatsappFloat.js adds
   .fade-ready right before it starts observing the hero — the same
   "never park content at opacity:0 waiting for a script that might
   not run" ordering as [data-reveal].reveal-ready in base.css. No
   JS, or the observer never fires, and the button simply stays in
   its default visible state above. */
.whatsapp-float.fade-ready {
  opacity: 0;
  transform: translateY(0.5rem);
  pointer-events: none;
}
.whatsapp-float.fade-ready.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.whatsapp-float-icon {
  flex-shrink: 0; width: 1.5rem; height: 1.5rem; display: flex; align-items: center; justify-content: center;
  transition: transform var(--duration-exit) var(--ease-out);
}
.whatsapp-float-icon svg { width: 100%; height: 100%; }

/* Label hidden by default (mobile keeps the icon alone — the glyph
   is universally recognized and a label would crowd the screen);
   revealed as a slide-out on desktop hover only. */
.whatsapp-float-label {
  max-width: 0; opacity: 0; overflow: hidden;
  font-family: var(--font-body); font-weight: 500; font-size: var(--text-small);
  transition: max-width var(--duration-enter) var(--ease-out), opacity var(--duration-enter) var(--ease-out),
              padding-left var(--duration-enter) var(--ease-out);
}

/* Audited, kept hardcoded on purpose: unlike the link-hover bug,
   this pair is safe regardless of section because it isn't
   section-relative in the first place — rest is ALWAYS carbon
   (never --bg), so hover is always its fixed opposite, crema/carbon,
   full stop. There's no case where this fill matches whatever is
   behind the fixed button, because the button never reads what's
   behind it. */
@media (hover: hover) {
  .whatsapp-float:hover {
    background: var(--color-crema);
    color: var(--color-carbon);
    border-color: var(--color-carbon);
  }
  .whatsapp-float:hover .whatsapp-float-label { max-width: 9rem; opacity: 1; padding-left: var(--space-1); }
  /* Same "confidence pop" the glyph gets on .btn-primary — CSS-only
     family consistency with the other WhatsApp CTAs; this button's
     own static markup/href (index.html) stays untouched. */
  .whatsapp-float:hover .whatsapp-float-icon { transform: scale(1.08); transition-duration: var(--duration-enter); }
}
.whatsapp-float:focus-visible .whatsapp-float-icon { transform: scale(1.08); transition-duration: var(--duration-enter); }

/* Skipped the optional lavender ring from the brief: this button
   already carries a 1px cream edge for legibility over charcoal
   sections (above) — stacking a second ring on a 56px circle read
   as a halo/target-lock rather than a quiet logo echo, so it's cut. */

.whatsapp-float:focus-visible { outline: 2px solid var(--acento-grafico); outline-offset: 3px; }

/* Hidden while the mobile nav panel is open (mobileNav.js toggles
   this class on <body>) so it never overlaps the open menu. */
body.nav-open .whatsapp-float { display: none; }

@media (min-width: 48em) {
  .whatsapp-float { right: 1.25rem; bottom: 1.25rem; }
}

@media (prefers-reduced-motion: reduce) {
  .whatsapp-float,
  .whatsapp-float-label { transition: opacity var(--duration-enter) ease; transform: none; }
}
