/* Reviews — testimonial section with 6 avatars arranged around a centered
   headline. Hovering an avatar reveals its associated testimonial bubble;
   bubbles stay sticky-open until another avatar is hovered.

   Layout architecture (desktop ≥1100):
     • The content frame is a 1141×725 Figma reference canvas (230:18730).
       Slot positions are in % of that frame; slot wrappers + bubbles size
       in cqi (container query inline-size) units so the entire layout
       scales proportionally with the canvas width.
     • `.reviews__content` is a CSS container (`container-type: inline-size`)
       so cqi inside resolves to 1% of the canvas inline-size. At viewport
       ≥1408 (1280 container max-width + 128 page-padding) the canvas hits
       1141×725 and cqi = 11.41px — values resolve to the Figma literals
       (avatar 88×88, bubble content 307 wide). Below that, everything
       shrinks uniformly; proportions are preserved by construction so no
       element-pair collision can occur in this range.
     • The trigger button declares an `anchor-name` so the matching bubble
       can position itself relative to the avatar via CSS Anchor Positioning
       (Baseline 2024). A fallback path positions the bubble relative to its
       slot wrapper for older browsers.
     • Bubbles carry `pointer-events: none` always so the mouse passes
       through an open bubble to reach avatars underneath (load-bearing for
       slots 2/3 which spatially overlap by design).

   Below 1100 the layout switches to the mobile scroll-snap carousel — see
   the @media block at the bottom of this file. The threshold is bumped
   above the project-shared 992 because the canvas's per-element text
   scaling becomes uncomfortably small in the 992-1100 band; the carousel
   reads better at those widths. See [[hybrid-rem-typography]] for the
   project's broader typography scaling rationale. */

.reviews {
  background-color: var(--color-bg-canvas);
  padding: var(--section-padding-medium) var(--page-padding-global);
}

.reviews__container {
  position: relative;
  max-width: var(--container-section);
  margin: 0 auto;
}

/* ════════════════════════════════════════════════════════════════════════
   DESKTOP (≥1100) — fluid canvas layout. Slot positions are in % of the
   1141×725 inner Figma frame; slot wrappers + bubble dimensions are in
   cqi (container query inline-size units), so everything scales
   proportionally with the canvas width. See top-of-file comment for the
   full architecture rationale.
   ════════════════════════════════════════════════════════════════════════ */

@media (min-width: 1100px) {
  .reviews__container {
    /* Container holds canvas + headline; aspect-ratio derives its height
       from the 1280 container max-width × the canvas's 725 design height,
       so the container shrinks proportionally with the viewport. */
    aspect-ratio: 1280 / 725;
  }

  .reviews__content {
    position: absolute;
    top: 0;
    /* Canvas frame: 1141×725 inside the 1280 container, offset 139 from
       container left. 139/1280 = 10.86%; 1141/1280 = 89.14%. All inner
       positioning is in % of THIS frame, not the outer container. */
    left: 10.86%;
    width: 89.14%;
    aspect-ratio: 1141 / 725;
    /* Establish a container so children can use cqi (container query
       inline-size) units to scale proportionally with canvas width. */
    container-type: inline-size;
    container-name: reviews-canvas;

    /* ─── Scoped token redefinitions ─────────────────────────────────────
       The avatar + bubble interior read these tokens for font sizes,
       line-heights, paddings, gaps, and corner radii. Redefine them to
       cqi so the entire bubble + avatar text scale uniformly with the
       canvas — preserves design proportions at every viewport, eliminates
       vertical overlap that would otherwise occur as fixed-size bubble
       text wraps more lines at narrower bubble widths.

       Values are derived from each token's design px ÷ 1141 (canvas
       inline at full size). The title's `--font-size-display-xs` is
       intentionally NOT redefined — the headline stays at the full token
       tier across the band.

       Scoped inside the desktop @media block so mobile (<992) keeps the
       :root tokens; cqi at mobile would resolve relative to whatever
       container size the mobile carousel ended up with and shrink text
       unhelpfully. */
    --font-size-body-md: 1.4cqi;
    --line-height-body-md: 2.1cqi;
    --font-size-body-sm: 1.23cqi;
    --line-height-body-sm: 1.75cqi;
    --font-size-header-lg: 3.16cqi;
    --line-height-header-lg: 3.51cqi;
    --spacing-2: 0.35cqi;
    --spacing-3: 0.53cqi;
    --spacing-4: 0.7cqi;
    --spacing-8: 1.4cqi;
    --spacing-12: 2.1cqi;
    --radius-2xl: 1.75cqi;
  }

  /* 123/1141, 280/725, 480/1141 — % of the canvas inner frame */
  .reviews__title {
    position: absolute;
    left: 10.78%;
    top: 38.62%;
    width: 42.07%;
  }

  .reviews__title > :where(h1, h2, h3, p) {
    font-family: var(--font-family-primary);
    /* Title font scales with canvas (3.51cqi = 40px at full canvas) but
       floors at 28/34 so it stays "section heading" prominent at the
       narrow end. The clamp prevents wrap caused by display-xs's 40px
       text exceeding the title container's 42.07% width at viewports
       below ~1280 — a problem pure-token sizing would have left in the
       fluid band. Letter-spacing stays em-based so it tracks the
       scaled font automatically. */
    font-size: clamp(28px, 3.51cqi, 40px);
    line-height: clamp(34px, 4.21cqi, 48px);
    letter-spacing: var(--letter-spacing-display-xs);
    font-weight: var(--font-weight-header);
    color: var(--color-fg-primary);
    margin: 0;
  }

  .reviews__title-accent {
    color: var(--color-interactive-accent);
  }

  /* ─── Slot — generic rule reads per-slot custom properties ─────────────
     Wrapper sized in cqi: 88/1141 = 7.71% of canvas inline. At full canvas
     this resolves to the Figma 88×88 avatar hit area. */
  .reviews__slot {
    position: absolute;
    width: 7.71cqi;
    height: 7.71cqi;
    top: var(--slot-top, auto);
    bottom: var(--slot-bottom, auto);
    left: var(--slot-left, auto);
    right: var(--slot-right, auto);
  }

  /* ─── Per-slot positions — % of canvas frame ──────────────────────────
     Horizontal: x / 1141. Vertical: y / 725. Derived from the original
     Figma group offsets (see git history for the px-based derivation). */
  .reviews__slot--1 {
    --slot-anchor: --reviews-anchor-1;
    --slot-left: 10.78%;
    --slot-top: 9.38%;
    --bubble-side: right;
  }
  .reviews__slot--2 {
    --slot-anchor: --reviews-anchor-2;
    --slot-left: 80.5%;
    --slot-top: 3.31%;
    --bubble-side: left;
  }
  .reviews__slot--3 {
    --slot-anchor: --reviews-anchor-3;
    --slot-right: 0%;
    --slot-top: 29.52%;
    --bubble-side: left;
  }
  .reviews__slot--4 {
    --slot-anchor: --reviews-anchor-4;
    --slot-right: 7.71%;
    --slot-bottom: 12.55%;
    --bubble-side: left;
  }
  .reviews__slot--5 {
    --slot-anchor: --reviews-anchor-5;
    --slot-right: 47.59%;
    --slot-bottom: 23.86%;
    --bubble-side: left;
  }
  .reviews__slot--6 {
    --slot-anchor: --reviews-anchor-6;
    --slot-left: 0%;
    --slot-bottom: 19.17%;
    --bubble-side: right;
  }

  /* ─── Bubble positioning — fallback (relative to slot wrapper) ─────────
     -24px / 6px (--spacing-3) converted to cqi so bubble offsets scale
     proportionally with the canvas. 24/1141 = 2.1cqi; 6/1141 = 0.53cqi. */
  .reviews__bubble {
    position: absolute;
    top: -2.1cqi;
  }

  .reviews__slot[data-bubble-side="right"] .reviews__bubble {
    left: 100%;
    margin-left: 0.53cqi;
  }

  .reviews__slot[data-bubble-side="left"] .reviews__bubble {
    right: 100%;
    margin-right: 0.53cqi;
  }

  /* ─── Bubble positioning — CSS Anchor Positioning (preferred) ─────────
     Browsers that support `anchor-name` get a declarative bubble-to-anchor
     relationship. The trigger button declares itself the anchor; the bubble
     resolves `anchor(top)`, `anchor(left)`, `anchor(right)` against it. */
  /* Trigger fills the slot wrapper at every scale. */
  .reviews__trigger {
    width: 100%;
    height: 100%;
  }

  @supports (anchor-name: --x) {
    .reviews__trigger {
      anchor-name: var(--slot-anchor);
    }

    .reviews__bubble {
      position-anchor: var(--slot-anchor);
      top: calc(anchor(top) - 2.1cqi);
      left: auto;
      right: auto;
      margin: 0;
    }

    .reviews__slot[data-bubble-side="right"] .reviews__bubble {
      left: calc(anchor(right) + 0.53cqi);
    }

    .reviews__slot[data-bubble-side="left"] .reviews__bubble {
      right: calc(anchor(left) + 0.53cqi);
    }
  }

  /* ─── Testimonial bubble — proportional scaling overrides ──────────────
     Component spec is 307×variable @ canvas 1141; override widths and tail
     dimensions to cqi so the bubble shrinks alongside the canvas. Tail
     margin-top stays in cqi to keep the tail-center aligned with the
     scaled avatar center (avatar half-height 3.86cqi + bubble -2.1cqi
     offset − half tail-height 1.05cqi = 4.91cqi). */
  .reviews .testimonial-bubble__content {
    /* 307/1141 = 26.91% of canvas inline */
    width: 26.91cqi;
  }

  .reviews .testimonial-bubble__tail {
    /* 16/1141, 24/1141, 56/1141 — proportional to canvas inline */
    width: 1.4cqi;
    height: 2.1cqi;
    margin-top: 4.91cqi;
  }

  /* Avatar w/h are hardcoded px in _avatar.css (.avatar--lg = 88×88);
     override to cqi so the avatar circle scales with the slot wrapper.
     Font-size scales via the scoped --font-size-header-lg redefinition
     on .reviews__content above. */
  .reviews .avatar--lg {
    width: 7.71cqi;
    height: 7.71cqi;
  }

  /* Star SVGs carry inline width="16" / height="16" attributes; override
     via CSS so each star scales with the bubble at every canvas width. */
  .reviews .testimonial-bubble__star {
    width: 1.4cqi;
    height: 1.4cqi;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   MOBILE / NARROW DESKTOP (<1100) — horizontal scroll-snap carousel with
   2 slides of 3 testimonials each. Each slide takes 100% viewport width;
   slots within stack vertically. Avatars alternate L/R per slot index
   (odd → left, even → right via row-reverse). Slide dots below the
   carousel update via IntersectionObserver in js/modules/reviews.js.
   Threshold sits above the project-shared 992 — see top-of-file comment.
   ════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1099px) {
  .reviews__container {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* 16px between eyebrow and title (which is the first child of __content) */
    gap: var(--spacing-8);
  }

  .reviews__content {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* 48px between title and the carousel list */
    gap: var(--spacing-24);
    width: 100%;
  }

  .reviews__title {
    width: 100%;
  }

  .reviews__title {
    text-align: left;
    margin: 0;
  }

  .reviews__title > :where(h1, h2, h3, p) {
    font-family: var(--font-family-primary);
    font-size: var(--font-size-display-xs);
    line-height: var(--line-height-display-xs);
    letter-spacing: var(--letter-spacing-display-xs);
    font-weight: var(--font-weight-header);
    color: var(--color-fg-primary);
    margin: 0;
  }

  .reviews__title-accent {
    color: var(--color-interactive-accent);
  }

  /* ─── Scroll-snap carousel container ────────────────────────────────── */

  .reviews__list {
    display: flex;
    flex-direction: row;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    width: 100%;
    /* Gap creates visual separation between slides during drag/swipe. Each
       slide is still 100% of the container; the gap sits between them and
       becomes visible only mid-transition. scroll-snap-align: start on each
       slide keeps the snap landing exactly at the slide's left edge. */
    gap: var(--spacing-8);
    /* Hide native scrollbar — interaction is via swipe / mouse-drag + dots. */
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* Enable mouse-drag horizontal scrolling — JS handler in reviews.js
       sets cursor: grabbing while dragging. */
    cursor: grab;
    user-select: none;
  }

  .reviews__list::-webkit-scrollbar {
    display: none;
  }

  .reviews__list.is-dragging {
    cursor: grabbing;
  }

  /* Carve-out — restore text selection on quote / attribution text inside the
     list's user-select: none container so users can long-press / right-click
     to copy testimonial copy. Drag still bypasses selection since the
     surrounding avatars + whitespace are not selectable. */
  .reviews__list .testimonial-bubble__quote,
  .reviews__list .testimonial-bubble__name,
  .reviews__list .testimonial-bubble__position {
    user-select: text;
  }

  .reviews__slide {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    width: 100%;
    scroll-snap-align: start;
    gap: var(--spacing-16);
    box-sizing: border-box;
  }

  /* ─── Slot: avatar + bubble side-by-side, alternating L/R ───────────── */

  .reviews__slot {
    position: static;
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: var(--spacing-3);
    width: 100%;
    max-width: none;
    height: auto;
    /* Override desktop absolute-positioning tokens */
    top: auto;
    bottom: auto;
    left: auto;
    right: auto;
  }

  /* Even-indexed slots: avatar on the right (bubble on the left) */
  .reviews__slot--2,
  .reviews__slot--4,
  .reviews__slot--6 {
    flex-direction: row-reverse;
  }

  /* ─── Avatar size override (lg 88×88 → md 64×64 on mobile) ──────────── */

  .reviews .avatar--lg {
    width: 64px;
    height: 64px;
    font-size: var(--font-size-header-sm);
    line-height: var(--line-height-header-sm);
  }

  /* Trigger sized to avatar + offset down so the avatar's vertical center
     aligns with the bubble's tail center. Avatar center: trigger-top + 32
     = slot-top + 36 + 32 = slot-top + 68. Tail center: bubble-top + 56 + 12
     = slot-top + 68. ✓ aligned.

     Using margin-top (not padding-top) to avoid being reset by the shared
     `.reviews__trigger { padding: 0 }` rule later in source. */
  .reviews__trigger {
    width: 64px;
    height: 64px;
    margin-top: 36px;
    flex-shrink: 0;
    /* Disable sticky-open hover interaction on mobile — all bubbles are
       always visible; the trigger is just a passive container. */
    pointer-events: none;
    cursor: default;
  }

  /* All bubbles visible on mobile (no sticky-open) */
  .reviews__bubble {
    position: static;
    opacity: 1 !important;
    visibility: visible !important;
    transform: none !important;
    flex: 1;
    min-width: 0;
    pointer-events: auto;
  }

  /* Force the bubble's flex container to fill .reviews__bubble width so the
     content rectangle inside doesn't collapse to single-character width. */
  .reviews .testimonial-bubble {
    width: 100%;
    min-width: 0;
  }

  /* Content rectangle takes remaining width after the tail flex sibling. */
  .reviews .testimonial-bubble__content {
    flex: 1;
    min-width: 0;
    width: auto;
    max-width: none;
  }

  /* ─── Tail direction overrides for mobile alternation ───────────────── */
  /* Slots 3, 5: avatar LEFT on mobile, bubble RIGHT → tail must point LEFT.
     Desktop HubL renders these as tail-right; override here. */
  .reviews__slot--3 .testimonial-bubble__tail,
  .reviews__slot--5 .testimonial-bubble__tail {
    order: -1;
    clip-path: polygon(100% 0, 0 50%, 100% 100%);
    margin-right: -1px;
    margin-left: 0;
  }

  /* Slot 6: avatar RIGHT on mobile, bubble LEFT → tail must point RIGHT.
     Desktop HubL renders slot 6 as tail-left; override here. */
  .reviews__slot--6 .testimonial-bubble__tail {
    order: 1;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    margin-left: -1px;
    margin-right: 0;
  }

  /* ─── Slide dots ────────────────────────────────────────────────────── */

  .reviews__slide-dots {
    display: flex;
    gap: var(--spacing-2);
    justify-content: center;
    align-items: center;
    width: 100%;
    margin: 0;
    padding: 0;
  }

  .reviews__slide-dot {
    width: 6px;
    height: 6px;
    padding: 0;
    border: none;
    border-radius: var(--radius-full);
    background-color: var(--color-secondary-200);
    cursor: pointer;
    transition: width var(--motion-base) ease, background-color var(--motion-base) ease;
  }

  .reviews__slide-dot--active {
    width: 24px;
    background-color: var(--color-secondary-500);
  }

  .reviews__slide-dot:focus:not(:focus-visible) {
    outline: none;
  }

  .reviews__slide-dot:focus-visible {
    outline: 2px solid var(--color-interactive-accent);
    outline-offset: 4px;
  }
}

/* Slide dots hidden on desktop (≥1100) — sticky-open hover on canvas is the
   navigation pattern. */
@media (min-width: 1100px) {
  .reviews__slide-dots {
    display: none;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   SHARED — avatar trigger button + bubble visibility states
   ════════════════════════════════════════════════════════════════════════ */

.reviews__trigger {
  display: inline-flex;
  padding: 0;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: var(--radius-full);
  color: inherit;
}

.reviews__trigger:hover,
.reviews__trigger:focus,
.reviews__trigger:active {
  background: transparent;
  color: inherit;
}

.reviews__trigger:focus:not(:focus-visible) {
  outline: none;
}

.reviews__trigger:focus-visible {
  outline: 2px solid var(--color-interactive-accent);
  outline-offset: 2px;
}

/* Bubble visibility — `pointer-events: none` always so an open bubble
   doesn't block hover on avatars underneath. */
.reviews__bubble {
  opacity: 0;
  visibility: hidden;
  transform: scale(0.96);
  transition: opacity var(--motion-fast) ease-out, visibility var(--motion-fast), transform var(--motion-fast) ease-out;
  pointer-events: none;
}

.reviews__bubble[data-active="true"] {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
}
