/* Bottom safe-area inset for bottom-anchored modal surfaces.
   Adds the device's bottom safe-area inset on top of an element's base
   bottom padding so action footers/panels clear the native iOS tab bar
   in the Hotwire Native shell. Set the base via the --grove-safe-bottom-base
   custom property (e.g. Tailwind's [--grove-safe-bottom-base:0.75rem]); it
   defaults to 0, so on web/desktop — where env(safe-area-inset-bottom)
   resolves to 0 — the element keeps its original bottom padding. The inset
   is additive and env()-driven only: no native detection or tab-bar
   constant is hardcoded here. */

/* THE BASE MUST NOT INHERIT, and this registration is what stops it.
   A plain custom property inherits down the whole DOM tree, and the `var(…, 0px)`
   fallback below only applies when the property is UNSET — not when some ancestor
   set it. So an ancestor declaring a base silently re-based every descendant that
   opts into this utility without declaring its own: brand v2's layout reserves 79px
   for the mobile bottom bar on the wrapper around the page body, which handed every
   ModalComponent panel inside it 79px of phantom padding (found in review of #288,
   sc-26474, and reproduced at 375px: 79px inside the layout, 0px outside).

   `inherits: false` scopes each declaration to the element that makes it, which is
   how all callers already use it — every one sets the base on the same element that
   carries `.grove-safe-bottom`. `initial-value: 0px` keeps the unset case identical
   to the old fallback. Where @property is unsupported the declaration is ignored and
   behaviour is exactly as before, which is why base-less callers (ModalComponent)
   also declare `0px` explicitly rather than relying on this alone. */
@property --grove-safe-bottom-base {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.grove-safe-bottom {
  padding-bottom: calc(var(--grove-safe-bottom-base, 0px) + env(safe-area-inset-bottom, 0px));
}

/* Top safe-area inset for top-anchored full-screen surfaces.
   Adds the device's top safe-area inset on top of an element's base top
   padding so headers and top-anchored controls (e.g. a modal close button)
   clear the notch / Dynamic Island / status bar in the Hotwire Native shell
   (viewport-fit=cover). Set the base via the --grove-safe-top-base custom
   property (e.g. Tailwind's [--grove-safe-top-base:1.5rem]); it defaults to
   0, so on web/desktop — where env(safe-area-inset-top) resolves to 0 — the
   element keeps its original top padding. The inset is additive and
   env()-driven only: no native detection or status-bar constant is
   hardcoded here. */
/* Same guard as --grove-safe-bottom-base above, for the same reason: without it a
   base declared on an ancestor (the layout header's 0.5rem, say) would be inherited by
   any descendant carrying `.grove-safe-top` with no base of its own. Added in 0.114.1
   when ModalComponent started using the utility (sc-28778). */
@property --grove-safe-top-base {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.grove-safe-top {
  padding-top: calc(var(--grove-safe-top-base, 0px) + env(safe-area-inset-top, 0px));
}

/* LayoutComponent's real, rendered header height (sc-27664) — published so a
   consumer pinning content under the sticky header (a jump nav, a `scroll-mt`)
   reads the actual number instead of holding its own guess that goes stale
   whenever the header changes (sc-26960, roots#5149). Set on the layout's content
   column (an ancestor of both the header and the page content) via Tailwind's
   `[--grove-header-height:...]`, per brand/breakpoint — see
   LayoutComponent#header_height_classes for the values and where the property is
   declared. Pin against it with e.g. `top-[var(--grove-header-height)]` or
   `top-[calc(var(--grove-header-height)-12px)]`.

   UNLIKE the safe-area bases above, this is left to inherit normally (the
   default `inherits: true`): the whole point is that page content, rendered well
   outside LayoutComponent's own template, can read the value its ancestor set.
   `initial-value: 0px` only matters where no ancestor set it at all, so an
   unguarded consumer degrades to "no offset" rather than an invalid calc(). */
@property --grove-header-height {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

/* Image banner uniform frame (ImageBannerComponent). Matches the For You
   banner frame that shipped in marketplace (carousel.css): 367x90 mobile,
   759x102 desktop. Height derives from width via aspect-ratio so every banner
   is the same height at a given screen size and scales cleanly; banner art is
   authored at these ratios so object-cover fills the frame with nothing
   cropped. */
.grove-image-banner-frame {
  aspect-ratio: 367 / 90;
}

@media (min-width: 768px) {
  .grove-image-banner-frame {
    aspect-ratio: 759 / 102;
  }
}

/* Tall banner frame variant — a much taller ratio than the default For You frame;
   rendering this art in the default frame crops the copy. Same width-derived-height
   mechanics.

   sc-26880 brought it down from 328x140 / 700x160 (Investing file node 2552-3953) to
   328x120 / 700x136. Both numbers are the sizes the art is now exported at — 1312x480
   and 2800x544 at 4x — taken from the design system's Banners section 652:1602 and
   confirmed against the twelve in-context instances on For You 446:35451.

   Shared by For You, the invest dashboard and Learn, deliberately: one frame, one set
   of art. The art has to be re-exported with any change here, or object-cover crops it. */
.grove-image-banner-frame--tall {
  aspect-ratio: 328 / 120;
}

@media (min-width: 768px) {
  .grove-image-banner-frame--tall {
    aspect-ratio: 700 / 136;
  }
}

/* Grove UI Card border */
.grove-card-border {
  border-width: 1px;
  border-style: solid;
}

@supports (border-width: 0.5px) {
  .grove-card-border {
    border-width: 0.5px;
  }
}

/* Mobile-only card border — removed at md breakpoint */
.grove-card-border-mobile {
  border-width: 1px;
  border-style: solid;
}

@supports (border-width: 0.5px) {
  .grove-card-border-mobile {
    border-width: 0.5px;
  }
}

@media (min-width: 768px) {
  .grove-card-border-mobile {
    border-width: 0;
    border-style: none;
  }
}

/* Grove UI Swiper/Carousel Styles */

:root {
  --swiper-navigation-sides-offset: 0;
  --swiper-navigation-size: 30px;
  --swiper-navigation-color: rgb(16, 16, 16);
}

/* Pagination dot colours are scoped to the carousel wrapper (not :root) so a
   single carousel can be themed via inline custom properties on its wrapper
   (see CarouselComponent#pagination_style) without recolouring every other
   carousel in the app. These wrapper-level values are the defaults. */
.grove-carousel-wrapper {
  /* The active dot follows the brand's primary-yellow token (v1 value is the
     old literal #ffcf83, so v1 rendering is unchanged; v2 swaps it with the
     flag). The c-* variables hold space-separated RGB channels. The inactive
     fill goes through the carousel-dot token (v1 value is the old literal
     #d9d9d9, so v1 rendering is unchanged). */
  --swiper-pagination-color: rgb(var(--c-primary-yellow, 255 207 131));
  --swiper-pagination-bullet-inactive-color: rgb(var(--c-carousel-dot, 217 217 217));
  --swiper-pagination-bullet-inactive-opacity: 0.6;
}

/* Brand v2 pagination dots (sc-26577, Figma 2830:18106 / invest comps
   2553:5087): active = a primary-green rounded pill (~22x10), inactive =
   10px carousel-dot circles at full opacity. Scoped to [data-brand="v2"], so
   v1 keeps the yellow 8px dots above; per-carousel inline overrides
   (CarouselComponent#pagination_style) still win over both. */
[data-brand="v2"] .grove-carousel-wrapper {
  --swiper-pagination-color: rgb(var(--c-primary-green, 73 232 133));
  --swiper-pagination-bullet-inactive-opacity: 1;
}

[data-brand="v2"] .grove-carousel-wrapper .swiper-pagination-bullet {
  width: 10px;
  height: 10px;
  border-radius: 9999px;
}

[data-brand="v2"] .grove-carousel-wrapper .swiper-pagination-bullet-active {
  width: 22px;
}

.grove-carousel-wrapper .swiper-button-prev,
.grove-carousel-wrapper .swiper-button-next {
  top: 100px;
  top: calc(50% - 30px);
  bottom: auto;
  z-index: 10;
  margin: 0;
  transform: none;
  width: 36px;
  height: 36px;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

.grove-carousel-wrapper .swiper .swiper-slide {
  opacity: 0;
}

.grove-carousel-wrapper .swiper.swiper-initialized .swiper-slide {
  opacity: 1;
}

.grove-carousel-wrapper .swiper-pagination-bullet {
  width: 8px;
  height: 8px;
  margin: 0 4px;
}

.grove-carousel-wrapper .swiper-button-prev svg,
.grove-carousel-wrapper .swiper-button-next svg {
  width: auto;
  height: auto;
}

.grove-carousel-wrapper .swiper-button-disabled.swiper-button-prev,
.grove-carousel-wrapper .swiper-button-disabled.swiper-button-next {
  opacity: 0 !important;
}

.grove-carousel-wrapper .swiper-slide > div,
.grove-carousel-wrapper .swiper-slide > a,
.grove-carousel-wrapper .swiper-slide > button {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

/* Make cards stretch to equal height within carousel slides */
.grove-carousel-wrapper .swiper-slide > div > .h-full,
.grove-carousel-wrapper .swiper-slide > a > .h-full,
.grove-carousel-wrapper .swiper-slide > button > .h-full {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* Push footer/button to the bottom by growing the text content area */
.grove-carousel-wrapper .swiper-slide > div > .h-full > .flex.flex-col:first-child,
.grove-carousel-wrapper .swiper-slide > a > .h-full > .flex.flex-col:first-child,
.grove-carousel-wrapper .swiper-slide > button > .h-full > .flex.flex-col:first-child {
  flex: 1;
}

.grove-carousel-wrapper .swiper-wrapper {
  align-items: stretch;
}

.grove-carousel-wrapper .swiper-autoheight .swiper-wrapper {
  height: auto !important;
}

.grove-carousel-wrapper .swiper-slide {
  height: auto;
}

.grove-carousel-wrapper .swiper-button-prev {
  left: 12px;
}

.grove-carousel-wrapper .swiper-button-next {
  right: 12px;
  margin-top: 0;
}

.grove-carousel-wrapper .swiper-button-next {
  transform: rotate(0deg) !important;
}

.grove-carousel-wrapper .swiper-button-prev:after,
.grove-carousel-wrapper .swiper-button-next:after {
  content: ''
}

/* Hide scrollbar while preserving scroll functionality */
.grove-scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.grove-scrollbar-hide::-webkit-scrollbar {
  display: none;
}

@media all and (min-width: 768px) {
  .grove-carousel-wrapper:hover .swiper-button-prev,
  .grove-carousel-wrapper:hover .swiper-button-next {
    opacity: 1;
  }
}

/* Overlay arrows are opacity 0 until `.grove-carousel-wrapper:hover` reveals them —
   a reveal touch devices can never trigger. Carousels that opt into mobile
   navigation paint them below the md breakpoint instead, matching the desktop
   hover state (Emily 2026-08-18). Desktop is untouched: at >=768px the hover rule
   above still governs. `swiper-button-disabled` keeps its `!important` opacity 0,
   so an arrow with nowhere to go stays hidden here too. */
@media all and (max-width: 767px) {
  .grove-carousel-wrapper--mobile-arrows .swiper-button-prev,
  .grove-carousel-wrapper--mobile-arrows .swiper-button-next {
    opacity: 1;
  }
}

.wealth-coach-shadow-sm {
  box-shadow: 0 1px 1px rgba(0,0,0,0.25);
}

.wealth-coach-shadow-lg {
  box-shadow: 0 3px 3px rgba(0,0,0,0.25);
}
/* Small arrows variant — inline with pagination, always visible */
.grove-carousel-small-arrow.swiper-button-prev,
.grove-carousel-small-arrow.swiper-button-next {
  position: static;
  top: auto;
  left: auto;
  right: auto;
  margin: 0;
  opacity: 1;
  width: 24px;
  height: 24px;
}

.grove-carousel-small-arrow.swiper-button-prev:after,
.grove-carousel-small-arrow.swiper-button-next:after {
  content: '';
  display: none;
}

.grove-carousel-small-arrow.swiper-button-disabled.swiper-button-prev,
.grove-carousel-small-arrow.swiper-button-disabled.swiper-button-next {
  opacity: 0.35 !important;
  cursor: default;
}

/* Toolkit icon tile shadow */
.grove-toolkit-icon-shadow {
  box-shadow:
    0 2.403px 3.604px 0 rgba(72, 29, 4, 0.05),
    0 4px 14.416px 0 rgba(72, 29, 4, 0.12),
    0 7.208px 7.208px 0.601px rgba(255, 255, 255, 0.30) inset,
    0 -7.208px 7.208px 1.802px rgba(188, 126, 68, 0.10) inset;
}


.grove-toolkit-icon-shadow-alt {
  filter: drop-shadow(0px 4px 5px rgba(10, 10, 10, 0.2));
}

/* Cloud Graphic Screen — on load the clouds drop down from above, overshoot
   past their resting spot, then wiggle/bounce a couple of times into place.
   Pure CSS, no JS. */
@keyframes groveCloudsDrop {
  0% {
    transform: translateY(-12vw);
  }
  58% {
    transform: translateY(4vw);
  }
  76% {
    transform: translateY(-2.2vw);
  }
  90% {
    transform: translateY(1vw);
  }
  100% {
    transform: translateY(0);
  }
}

.grove-clouds-drop {
  transform: translateY(-12vw);
  animation: groveCloudsDrop 0.85s cubic-bezier(0.33, 0, 0.3, 1) 0.1s both;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .grove-clouds-drop {
    animation: none;
    transform: translateY(0);
  }
}

/* Cloud Graphic Screen — the graphic stays hidden until the clouds have finished
   animating in (~0.95s), then pops/zooms in quickly with a slight overshoot. */
@keyframes groveGraphicPop {
  0% {
    opacity: 0;
    transform: scale(0.5);
  }
  70% {
    transform: scale(1.01);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.grove-graphic-pop {
  opacity: 0;
  transform-origin: center;
  animation: groveGraphicPop 0.4s ease-in-out 0.95s both;
  will-change: transform, opacity;
}

@media (prefers-reduced-motion: reduce) {
  .grove-graphic-pop {
    animation: none;
    opacity: 1;
    transform: scale(1);
  }
}

/* AWB Streak Hit modal — the coin spins on Rooty's fingertip and the paparazzi
   flashes pop. Everything lives inside prefers-reduced-motion: no-preference,
   so under reduced motion the elements simply keep their base styles: the coin
   and both flash bursts rest visible and motionless at their full pose, which
   is the frame's own static composition. */
@media (prefers-reduced-motion: no-preference) {
  /* Basketball-on-finger: the wrapper is the perspective camera AND wobbles
     a few degrees to sell the balance; the inner element does the flat-coin
     rotateY loop. The static design rotation stays on the markup's outermost
     wrapper so it composes with both. */
  .grove-awb-coin-wobble {
    perspective: 300px;
    animation: groveAwbCoinWobble 2.6s ease-in-out infinite alternate;
    will-change: transform;
  }

  .grove-awb-coin-spin {
    transform-style: preserve-3d;
    animation: groveAwbCoinSpin 1.6s linear infinite;
    will-change: transform;
  }

  /* Flash pop: a quick bloom that decays fast, invisible for most of the
     cycle. `backwards` keeps a delayed flash invisible until its first pop.
     The two cameras get different durations AND delays (.grove-awb-flash-late)
     so the stagger never locks into sync — a press scrum, not a metronome. */
  .grove-awb-flash {
    animation: groveAwbFlashPop 2.8s ease-out infinite backwards;
    transform-origin: 70% 20%;
    will-change: transform, opacity;
  }

  .grove-awb-flash-late {
    animation-duration: 3.7s;
    animation-delay: 1.9s;
  }
}

@keyframes groveAwbCoinWobble {
  from {
    transform: rotate(-3.5deg);
  }
  to {
    transform: rotate(3.5deg);
  }
}

@keyframes groveAwbCoinSpin {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(360deg);
  }
}

@keyframes groveAwbFlashPop {
  0% {
    opacity: 0;
    transform: scale(0.4);
  }
  6% {
    opacity: 1;
    transform: scale(1.15);
  }
  10% {
    transform: scale(1);
  }
  16% {
    opacity: 0;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(1);
  }

/* Info popover, brand v2 (sc-27149, Figma Onboarding 1219:3943).
   Two things Tailwind utilities cannot do from the component template.

   1. THE TAIL. Popper positions `[data-popper-arrow]` and writes an inline
      transform onto it, so the visible shape has to live on a pseudo-element or
      Popper's transform and ours fight. Flowbite ships CSS for this arrow, but it
      is the host app's dependency, not grove-ui's — the catalog has flowbite's JS
      and no flowbite CSS, and the arrow rendered 0x0 and transparent. Owning the
      shape here means the tail is grove-ui's regardless of what the host loads.

   2. THE LINKS. Popover content is a caller-supplied block, and some of it is
      Sanity-authored (roots' CMS::Types::InfoPopOverType), so the component can
      never reach the anchors to add a class. The frame underlines them. An
      arbitrary variant (`[&_a]:underline`) is not an option: Tailwind's extractor
      does not emit it from an ERB class list, verified against a clean build.

   Colour comes through the same CSS variable Tailwind's `bg-c-pillb-blue` uses, so
   the tail follows the brand rather than pinning a hex. The fallback channels are
   v2's Light Blue. */
.grove-info-tail,
.grove-info-tail::before {
  width: 0.5rem;
  height: 0.5rem;
  background: inherit;
}

/* THE ARROW BOX ITSELF MUST NOT PAINT. `background: inherit` above lands on the
   element as well as the pseudo, and the element is a direct child of the popover
   root that carries the bubble fill — so the un-rotated 8x8 box painted solid, and
   offset half-outside the bubble it read as a flat rectangular notch rather than a
   point. Popper's and Flowbite's canonical recipe pairs `visibility: hidden` on the
   arrow with `visibility: visible` on the pseudo-element; the second half was
   missing. Found in review of #313.

   This also settles the host-Flowbite question: if a host app does load Flowbite's
   arrow CSS, its own `background: inherit` on this element is neutralised by the
   same `visibility: hidden`, so it cannot reintroduce the square. */
.grove-info-tail {
  visibility: hidden;
}

.grove-info-tail::before {
  content: "";
  position: absolute;
  visibility: visible;
  transform: rotate(45deg);
  background: rgb(var(--c-pillb-blue, 201 232 255));
}

/* THE TAIL SITS ON THE EDGE FACING THE TRIGGER, so it points back at the info
   icon. Popper writes `data-popper-placement` on the popover root and positions the
   arrow along the cross axis; the main-axis offset is the stylesheet's job, and
   without it the tail collects at the element's static position (which is how it
   ended up under the bubble instead of above it).

   Half the tail's 8px, so it overhangs the bubble edge by 4px and reads as a point
   rather than a notch. Every placement callers pass is covered: :bottom and :top on
   the metric squares and the rewards page, :left on the paginated table, :right the
   component default. */
[data-popper-placement^="bottom"] > .grove-info-tail {
  top: -0.25rem;
}

[data-popper-placement^="top"] > .grove-info-tail {
  bottom: -0.25rem;
}

[data-popper-placement^="left"] > .grove-info-tail {
  right: -0.25rem;
}

[data-popper-placement^="right"] > .grove-info-tail {
  left: -0.25rem;
}

/* DARK TEXT IS FORCED ON EVERYTHING INSIDE THE BUBBLE, descendants included.
   The v2 bubble is Light Blue, so any white text inside it is invisible, and the
   component cannot prevent that at the call site: the content is a caller-supplied
   block and some of it is Sanity-authored (roots' CMS::Types::InfoPopOverType), so
   an author can introduce white text without touching code and no lint rule would
   ever see it.

   `!important`, and deliberately. The first version of this relied on
   `.grove-info-body *` out-specifying `.text-white`, which is simply wrong: per
   Selectors L3 the universal selector contributes nothing, so that compound is
   (0,1,0) — a TIE with `.text-white`, resolved only by source order, i.e. by the
   accident of this stylesheet being linked after Tailwind's. Worse, a variant
   utility like `hover:text-white` compiles to (0,2,0) and beats it outright at any
   order. Tailwind v3's `@layer` is a build-time sort that is stripped from the
   emitted CSS, so there is no layer to lean on either. Caught in review of #313.

   The frame draws exactly one text colour in this bubble, so forcing it is the
   design rather than a workaround — and the guarantee has to hold against content
   this component cannot see. */
.grove-info-body,
.grove-info-body * {
  color: rgb(var(--c-gray-800, 34 34 34)) !important;
}

.grove-info-body a {
  text-decoration: underline;
  text-decoration-skip-ink: none;
  text-underline-position: from-font;
}
