/* rx-block: rx-stats */

/* --- Stats band ------------------------------------------------------------
   Two halves that do NOT share a grid logic. The intro is ordinary flow; the
   right-hand field is a fixed-aspect canvas carrying five arcs and four
   metrics pinned to points on those arcs.

   FIELD COORDINATES. Every number on the right-hand side comes from one box:
   Figma x 600..1512, y 4230..4800 — 912 x 570. Percentages are of that box,
   and cqw is 1/912 of its width, so 69px of Figma type is 7.5658cqw. Nothing
   here is eyeballed: change the field's width and the arcs, the dots and the
   figures all rescale together and the dots stay on their circles.

   The circles: r = 1152.454, centres all at field y = 313.876, x stepping
   162.815 from -1074.034. Only about 56 degrees of each is ever inside the
   section, which is what makes them read as near-vertical arcs rather than
   circles. The section clips them (overflow: hidden below).
   --------------------------------------------------------------------------- */

.rx-stats {
  /* `clip`, NOT `hidden`. Both crop the arcs at the section edge, but
     `overflow: hidden` makes this a scroll container — and the scroll drift
     below resolves its view() timeline against the nearest scroll container,
     so with `hidden` the metrics were pinned to a scrollport that never
     scrolls and the drift sat frozen at one value. `clip` crops without
     creating one, and the timeline resolves to the page. */
  overflow: clip;

  /* The arcs need room above and below the metrics — in the design they run
     226px clear of the card above and 285px clear of the card below, and that
     empty sweep is most of what the section is. Percentages of the section's
     own width, so the margin around the composition scales with it rather
     than staying a fixed band that the arcs outgrow.
     Written as the section's own padding VARIABLES, not as padding, so an
     editor's spacing choice on the module still lands: a chosen value sets
     the same property and simply replaces this default. */
  /* THE SECTION IS A FULL VIEWPORT TALL. That is the design: the arcs sweep
     an empty stage and the metrics sit in the middle of it, so the block is
     sized by the viewport rather than by its own contents plus padding.

     The padding is now only a floor for short viewports — the min-height does
     the work and the content is centred in whatever is left. The old
     percentage padding (226/285 of 1512) was standing in for the height and
     got the proportion right only at one window size. */
  min-block-size: 100svh;
  display: grid;
  align-content: center;

  --rx-section-pt: var(--rx-space-9);
  --rx-section-pb: var(--rx-space-9);


  /* --- Motion ------------------------------------------------------------
     The arcs draw from the top down; each one drops its dot at the point
     where its metric lives, and the figure follows a beat later. The four
     go left to right, --rx-stats-step apart, which is also the order the
     arcs cross the frame.

     WHAT MOVES WHAT. Three layers, on three CSS properties that compose
     rather than overwrite each other, which is what lets each one be timed
     and driven independently:

       .rx-stats__dot    `transform` — a rotation about its circle's centre.
                         This is the entrance: real travel along the real arc,
                         not an approximation of it.
       .rx-stats__value  `transform` — fades in while sliding OUT from the
       .rx-stats__label                dot, so the figure reads as issued by
                                       its marker.
       .rx-stats__item   `translate`  — the scroll drift, carrying the dot and
                                       its figure together.

     An earlier pass rode the whole item in on the arc's chord so the dot and
     type travelled as one rigid block. It was off the line, visibly: a chord
     across 22 degrees departs from the arc by a 21px sagitta at the midpoint,
     which is small against the 440px of travel but not small against a 30px
     dot sitting on a hairline. Rotating the item instead would put the type
     on a 22 degree tilt. Hence the split — only the dot rotates. */
  --rx-stats-step: 150ms;
  --rx-stats-draw: 1200ms;
  --rx-stats-ride-dur: 1000ms;
  --rx-stats-ride-lag: 100ms;
  --rx-stats-text-dur: 620ms;
  --rx-stats-text-lag: 430ms;
  /* How far back toward the dot the type starts. It travels OUT from there to
     its resting place, so the figure reads as being issued by its marker
     rather than fading up in position. Short on purpose — the dot has just
     crossed 440px, and type that moves as far as the thing that delivered it
     stops looking delivered. */
  --rx-stats-text-from: 2.2cqw;
  /* How far back up its own arc a dot starts, in degrees. One value for all
     four: they are on circles of the same radius, so the same angle is the
     same distance travelled. */
  --rx-stats-sweep: -22deg;
  /* The marker wakes up: it travels as a bare core ring and only takes its
     outer ring once it has arrived, so a dot in flight reads as dormant and a
     dot on station reads as live. */
  --rx-stats-open-dur: 520ms;
  --rx-stats-open-at: 900ms;
  /* The shared decelerate curve. An alias, not a copy: re-point this if the
     counter should ease differently from the rest of the site. */
  --rx-stats-ease: var(--rx-ease-out);

  /* --- The marker's activated ring ---------------------------------------
     Modelled on the button halo in _buttons.css: a thin ring that grows OUT
     of the shape it belongs to and stops. Same idea, smaller numbers — the
     button's 9px spread and 2px stroke are sized for a 45px pill and read as
     a heavy collar around a 15px dot.

     Figma draws this ring at 30.5px with a 2px #2a2a2a stroke. Both are
     reduced here on purpose: at the drawn weight the ring is the loudest
     thing in the block, competing with the figures it is meant to be
     annotating. The colour goes to the gold the arcs are already drawn in,
     so the marker belongs to its line rather than to the type.

     --spread is the halo's own token name: how far the ring parks from the
     core it grew out of. */
  --rx-stats-dot-core: 1.5998cqw;   /* 14.59 — the dormant ring, Figma's */
  --rx-stats-dot-spread: 0.44cqw;   /* ~4px, against the button's 9 */
  --rx-stats-dot-ring: 0.11cqw;     /* ~1px, against Figma's 2 */
  --rx-stats-dot-halo: var(--rx-gold-base);
  /* No overshoot: the button's halo eases out and stops, and the marker is
     quoting that gesture. */
  --rx-stats-ease-open: var(--rx-ease-out);
}

/* The field bleeds to the section's right edge, exactly as it does in the
   design (600..1512 of a 1512 frame). That is why this is not a container:
   constraining it to the measure would shrink the whole construction and the
   figures with it. */
.rx-stats__inner {
  container-type: inline-size;
  display: grid;
  grid-template-columns: 39.683% 60.317%;
  align-items: start;
}

.rx-stats__intro {
  /* Figma puts the copy at x=173 of 1512 and the heading 181 below the top of
     the field. Both expressed against the section width so the relationship
     to the arcs holds at any size; the gutter is the floor. */
  padding-left: max(var(--rx-gutter), 11.442cqw);
  padding-top: 11.97cqw;
  padding-right: var(--rx-space-6);
}

.rx-stats__heading { max-width: 12ch; }
.rx-stats__body {
  margin-top: var(--rx-space-5);
  /* Figma sets the measure at 303 of 1512 — narrow enough that the copy sits
     as a block against the heading rather than running under the arcs. */
  max-width: 20.04cqw;
}

/* --- The field ------------------------------------------------------------- */

.rx-stats__field--orbit {
  position: relative;
  container-type: inline-size;
  /* THE ARCS MUST SPAN THE BLOCK, top edge to bottom edge.

     They cannot be stretched or scaled to do it: the dots sit on the arcs only
     because the arc box is exactly 96.491% x 236.842% of the field, which maps
     the viewBox 1:1 onto field coordinates. Touch that mapping and the dots
     come off the lines. So the FIELD has to be big enough instead:

       arc span   = 2.36842 * fieldHeight  >=  sectionHeight
       fieldWidth = 1.6 * fieldHeight      >=  0.6756 * sectionHeight

     and the section is 100svh, so the field is at least 67.55svh wide. This is
     a MINIMUM. It was first written as max-inline-size — a maximum — which
     guaranteed the opposite of the requirement, and made the lines stop further
     short the taller the window got.

     Overflowing the column to the right is correct: the design crops the arcs
     at the frame edge, which is why only ~56 degrees of each circle is ever in
     view. .rx-stats already clips. */
  /* Capped at the column so the minimum can never exceed the screen: 67.55svh
     is 511px on a 757-tall phone, which pushed the whole field out of a 500px
     viewport. The arcs give up their full sweep before the layout does. */
  min-inline-size: min(67.55svh, 100%);

  /* THE DASH LENGTH IS pathLength, and it is a plain 1000.

     This was `160cqw` — a share of the field's own width — on the reasoning
     that `vector-effect: non-scaling-stroke` makes the dash resolve in SCREEN
     PIXELS, so the markup's `pathLength="1000"` could not reach it. MEASURED
     2026-09-01, that is not what Chrome does: with `pathLength="1000"` set, a
     680px dash paints exactly 68% of the arc, and removing the dash entirely
     paints 99.5%. The normalisation applies.

     Which means 160cqw was only ever right by accident. On a wide field it
     resolves ABOVE 1000 (1459 at the desktop's 912) and covers the whole
     normalised path, so the arcs looked complete. On the phone's 402-425 field
     it resolves to 643-680 — under 1000 — and every arc stopped about two
     thirds of the way across. The narrower the screen, the shorter the line.

     A plain 1000 is the whole path at every width, because that is what the
     attribute normalises it to. There is nothing left to track. */
  --rx-stats-arc-len: 1000;
  /* Locks the coordinate space: with the ratio fixed, a percentage of height
     and a percentage of width are two views of the same number, so the arcs
     (sized off height) and the dots (sized off width) cannot drift apart. */
  aspect-ratio: 912 / 570;
}

/* Sized and placed so the viewBox maps 1:1 onto field coordinates:
   x -140..740 is -15.351%..81.140% of 912, y -360..990 is -63.158%..236.842%
   of 570. The arcs run well outside the field on every side; the section
   clips them, which is what Figma's frame does too. */
.rx-stats__field--orbit .rx-stats__arcs {
  position: absolute;
  left: -15.351%;
  top: -63.158%;
  width: 96.491%;
  height: 236.842%;
  overflow: visible;
  pointer-events: none;
}

.rx-stats__arcs--stacked { display: none; }

.rx-stats__arc {
  fill: none;
  stroke: var(--rx-gold-t1);
  stroke-width: 1;
  /* The arcs are hairlines in the design and must stay hairlines however far
     the field is scaled, so the stroke opts out of the viewBox transform. */
  vector-effect: non-scaling-stroke;
}

/* --- Metrics --------------------------------------------------------------- */

.rx-stats__list { list-style: none; margin: 0; padding: 0; }

.rx-stats__field--orbit .rx-stats__list {
  position: absolute;
  inset: 0;
}

.rx-stats__field--orbit .rx-stats__item {
  position: absolute;
  width: 25.192cqw;   /* 229.75 */
}

/* Figma group origins, as percentages of the field. Each is the top-left of
   the group; the dot centre sits 15.25 in and down from it, and the type
   hangs off that.

   Each item carries the numbers its motion is built from, all derived from
   where it sits on its own circle:

     --rx-stats-org      where that circle's CENTRE falls, expressed in the
                         dot's own coordinates. The dot rotates about this
                         point, which is what puts its travel exactly on the
                         arc rather than near it.
     --rx-stats-tx/ty    the unit tangent at rest — the direction the arc
                         actually runs through this point.
     --rx-stats-amp      how far it drifts along that tangent on scroll. All
                         four POSITIVE, so the group moves as a shoal in one
                         direction; the rates differ so it does not read as
                         one rigid block sliding. The spread between the
                         largest and smallest is ~12px against gaps of 150px
                         and more, so no two can close on each other. */
.rx-stats__field--orbit .rx-stats__item--1 {
  left: 6.798%;  top: 53.670%;
  --rx-stats-org: -126.238cqw -0.800cqw;
  --rx-stats-tx: -0.00634;   --rx-stats-ty: 0.99998;
  --rx-stats-amp: 0.9cqw;
}

.rx-stats__field--orbit .rx-stats__item--2 {
  left: 22.699%; top: 14.773%;
  --rx-stats-org: -124.285cqw 23.510cqw;
  --rx-stats-tx: 0.18578;    --rx-stats-ty: 0.98259;
  --rx-stats-amp: 1.6cqw;
}

.rx-stats__field--orbit .rx-stats__item--3 {
  left: 42.545%; top: 65.118%;
  --rx-stats-org: -126.280cqw -7.955cqw;
  --rx-stats-tx: -0.06288;   --rx-stats-ty: 0.99802;
  --rx-stats-amp: 2.2cqw;
}

.rx-stats__field--orbit .rx-stats__item--4 {
  left: 59.986%; top: 31.923%;
  --rx-stats-org: -125.867cqw 12.792cqw;
  --rx-stats-tx: 0.10112;    --rx-stats-ty: 0.99488;
  --rx-stats-amp: 1.3cqw;
}

/* The marker, in two parts, because it has two states.

     ::after   the CORE — a small ring, Figma's r 6.29488 stroked 2. This is
               the dormant state, and all there is while the dot is in
               flight. The arc runs straight through it.
     ::before  the HALO — the activated state. Grows out of the core the way
               the button's halo grows out of its pill, and brings the page
               ground with it as a fill, so a live dot sits ON its line where
               a dormant one is still just a bead threaded onto it.

   ::before is declared first so it paints under the core.

   The element itself is a ZERO-SIZED anchor pinned at the dot's centre, with
   both rings centred on it. That is what lets the halo change size without
   moving anything: with a box, growing the ring would have to grow the box,
   and the centre would drift by half the difference. It also makes the
   rotation origin below a straight vector from the centre of the dot to the
   centre of its circle, with no half-widths folded in. */
.rx-stats__dot {
  position: absolute;
  /* 15.2529 of Figma's group origin, in field units. */
  left: 1.6725cqw;
  top: 1.6725cqw;
  width: 0;
  height: 0;
  /* The circle's centre, ~126cqw away off the left of the field. The dot
     rotates about it, so its entrance IS the arc. */
  transform-origin: var(--rx-stats-org);
}

.rx-stats__dot::before,
.rx-stats__dot::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  border-radius: 50%;
  /* The box is zero-sized, so its origin IS the dot centre; this pulls each
     ring back by half its own size to sit on it. `translate` rather than a
     transform, so the halo's growth can use `scale` without disturbing it. */
  translate: -50% -50%;
}

.rx-stats__dot::after {
  width: var(--rx-stats-dot-core);
  height: var(--rx-stats-dot-core);
  border: var(--rx-stats-dot-ring) solid var(--rx-brown-base);
}

/* Sized at its FINAL diameter — core plus the spread on each side — and grown
   into place with `scale`, from exactly the core's own size to full. That is
   the halo motion: the ring starts flush around the core and pushes outward.
   Scaling from zero instead would read as a dot popping, not a ring opening. */
.rx-stats__dot::before {
  width: calc(var(--rx-stats-dot-core) + var(--rx-stats-dot-spread) * 2);
  height: calc(var(--rx-stats-dot-core) + var(--rx-stats-dot-spread) * 2);
  border: var(--rx-stats-dot-ring) solid var(--rx-stats-dot-halo);
  background: var(--rx-gold-t5);
}

.rx-stats__value {
  margin: 0;
  color: var(--rx-heading);
  font-size: var(--rx-text-4xl);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.02em;
}

.rx-stats__label {
  margin: var(--rx-space-2) 0 0;
  max-width: 18ch;
  font-size: var(--rx-text-sm);
  font-weight: 500;
  line-height: 1.22;
  color: var(--rx-brown-base);
}

/* In the field the type is absolutely placed, not stacked: Figma's boxes are
   cap-trimmed, so each top is the cap line. The offsets below convert that to
   a normal text box — for Inter at line-height L the cap sits
   (L - 1.2104)/2 + 0.2418 em below the box top: 12.19px at 69/1.08, 4.44px at
   18/1.22. */
.rx-stats__field--orbit .rx-stats__value,
.rx-stats__field--orbit .rx-stats__label {
  position: absolute;
  left: 5.3457cqw;   /* 48.753 */
  margin: 0;
}

.rx-stats__field--orbit .rx-stats__value {
  top: 0.3355cqw;
  font-size: 7.5658cqw;   /* 69 */
  white-space: nowrap;
}

.rx-stats__field--orbit .rx-stats__label {
  top: 8.3125cqw;
  width: 19.846cqw;      /* 181 */
  max-width: none;
  font-size: 1.9737cqw;  /* 18 */
}

/* --- Entrance --------------------------------------------------------------
   The section rides the global reveal driver for its TRIGGER only: it takes
   [data-rx-reveal] so the driver stamps [data-rx-revealed] when the block
   comes into view, and then opts out of the driver's own fade-and-scale,
   which would otherwise move the whole field as one lump and fight the
   transforms below.

   Everything is scoped to [data-rx-reveal-ready], the flag main.js only sets
   once it is actually running. No JS, or reduced motion, and the arcs are
   drawn, the dots are parked and the figures are up — the finished state is
   the default. ------------------------------------------------------------ */

[data-rx-reveal-ready] .rx-stats__inner[data-rx-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
  will-change: auto;
}

/* Each element's own place in the sequence. --rx-stats-i is set per item and
   per arc in the markup. */
.rx-stats__arc,
.rx-stats__item {
  --rx-stats-delay: calc((var(--rx-stats-i, 1) - 1) * var(--rx-stats-step));
}

/* --- Hidden state ---------------------------------------------------------
   What every animated part looks like before its turn comes. Kept as its own
   block rather than folded into the keyframes because `animation-fill-mode:
   backwards` only covers an element that HAS an animation — and below 1025
   they are all switched off, where these have to hold anyway. */

[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__arc {
  stroke-dashoffset: var(--rx-stats-arc-len);
}

[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__dot {
  transform: rotate(var(--rx-stats-sweep));
}

[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__dot::before {
  opacity: 0;
  scale: calc(var(--rx-stats-dot-core) / (var(--rx-stats-dot-core) + var(--rx-stats-dot-spread) * 2));
}

[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__dot::after,
[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__intro {
  opacity: 0;
}

[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__value,
[data-rx-reveal-ready] .rx-stats__inner:not([data-rx-revealed]) .rx-stats__label {
  opacity: 0;
  transform: translateX(calc(var(--rx-stats-text-from) * -1));
}

/* --- Entrance ------------------------------------------------------------- */

[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__arc {
  animation: rx-stats-draw var(--rx-stats-draw) var(--rx-stats-ease)
             var(--rx-stats-delay) both;
}

/* The dot rides in on its own circle — a rotation about a centre ~126cqw off
   to the left. Nothing else rotates, so nothing else has to be held level. */
[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__dot {
  animation: rx-stats-ride var(--rx-stats-ride-dur) var(--rx-stats-ease)
             calc(var(--rx-stats-delay) + var(--rx-stats-ride-lag)) both;
}

[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__dot::after {
  animation: rx-stats-fade var(--rx-stats-text-dur) var(--rx-stats-ease)
             calc(var(--rx-stats-delay) + var(--rx-stats-ride-lag)) both;
}

/* Activation. Held until the ride is over, so the ring opens on a dot that
   has already stopped — the two never overlap, which is what makes the
   arrival read as an event rather than as part of the travel. */
[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__dot::before {
  animation: rx-stats-open var(--rx-stats-open-dur) var(--rx-stats-ease-open)
             calc(var(--rx-stats-delay) + var(--rx-stats-open-at)) both;
}

/* The figure is issued BY the marker: it fades up while sliding out from the
   dot to its resting place. The label follows a beat later and travels a
   shorter distance, so the two do not move as one slab. */
[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__value {
  animation: rx-stats-emerge var(--rx-stats-text-dur) var(--rx-stats-ease)
             calc(var(--rx-stats-delay) + var(--rx-stats-text-lag)) both;
}

/* Shorter travel than the figure — the two arrive from the same place but
   not in lockstep. */
.rx-stats__label { --rx-stats-text-from: 1.4cqw; }

[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__label {
  animation: rx-stats-emerge var(--rx-stats-text-dur) var(--rx-stats-ease)
             calc(var(--rx-stats-delay) + var(--rx-stats-text-lag) + 90ms) both;
}

[data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__intro {
  animation: rx-stats-rise var(--rx-stats-text-dur) var(--rx-stats-ease) both;
}

@keyframes rx-stats-draw {
  /* --rx-stats-arc-len, NOT 1000: vector-effect: non-scaling-stroke puts the
     dash in screen pixels, so pathLength does not apply. See the field. */
  from { stroke-dasharray: var(--rx-stats-arc-len); stroke-dashoffset: var(--rx-stats-arc-len); }
  to   { stroke-dasharray: var(--rx-stats-arc-len); stroke-dashoffset: 0; }
}

@keyframes rx-stats-ride {
  from { transform: rotate(var(--rx-stats-sweep)); }
  to   { transform: rotate(0deg); }
}

@keyframes rx-stats-open {
  /* Out of the core, not out of nothing: the start scale is the ratio of the
     core to the halo's full diameter, so the ring begins life flush around
     the dormant dot. */
  from { opacity: 0; scale: calc(var(--rx-stats-dot-core) / (var(--rx-stats-dot-core) + var(--rx-stats-dot-spread) * 2)); }
  to   { opacity: 1; scale: 1; }
}

@keyframes rx-stats-emerge {
  from { opacity: 0; transform: translateX(calc(var(--rx-stats-text-from) * -1)); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes rx-stats-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes rx-stats-rise {
  from { opacity: 0; transform: translateY(0.6em); }
  to   { opacity: 1; transform: translateY(0); }
}

/* --- Scroll drift ----------------------------------------------------------
   Once it has arrived, each metric keeps creeping along its own arc as the
   page moves — the dot and its figure together, since this sits on the item
   that holds both. --rx-stats-tx/ty is the unit tangent at rest, so the
   direction is the one the arc actually runs in; over this few pixels the
   tangent and the curve are the same line.

   All four go the SAME WAY and at four different rates. Rest is the midpoint
   of the travel, so the block is at its designed position when it is centred
   in the viewport and leans a little either side of that.

   This animates `translate`, NOT `transform` — the dot's entrance owns
   `transform` and the halo's growth owns `scale`. Three separate properties,
   composed by the browser, so one element can run animations on two different
   timelines and a pseudo-element can run a third without any of them
   clobbering another.

   No JS: a view() timeline is scroll position by definition, so it tracks the
   page and reverses with it. Where the timeline is unsupported the metrics
   simply sit still, which is the same thing the reduced-motion branch does. */

@supports (animation-timeline: view()) {
  .rx-stats__field--orbit .rx-stats__item {
    animation: rx-stats-drift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}

@keyframes rx-stats-drift {
  from {
    translate: calc(var(--rx-stats-tx) * var(--rx-stats-amp) * -1)
               calc(var(--rx-stats-ty) * var(--rx-stats-amp) * -1);
  }
  to {
    translate: calc(var(--rx-stats-tx) * var(--rx-stats-amp))
               calc(var(--rx-stats-ty) * var(--rx-stats-amp));
  }
}

/* --- Responsive ------------------------------------------------------------
   The arcs are a 1512-wide composition and there is no design below it. Under
   1025 the field gives up its coordinate space and the metrics go back to a
   plain staggered grid; the arcs and the dots go with it. */

@media (max-width: 1024px) {
  /* NO VIEWPORT STAGE DOWN HERE. At 1512 the section is 100svh with its content
     centred, because the design is arcs sweeping an empty stage with the
     metrics in the middle of it. The phone frame draws the opposite: the
     composition fills the frame and there is no empty stage to hold.

     Keeping both was what put the metrics through the section below. `center`
     overflows EQUALLY at both ends once the content is taller than the box, so
     the last metric spilled straight past the section's own bottom padding —
     measured 96px into the logo bar. Centring cannot be relied on to respect
     padding; only start alignment can. */
  .rx-stats {
    min-block-size: auto;
    align-content: start;

    /* THE SECTION HAS TO CARRY ITS OWN CLEARANCE HERE, and it overrides the
       page's spacing choice to do it.

       home.html gives this module space_top: "none" / space_bottom: "none",
       and the block after it space_top: "overlap" — a deliberate -96px margin
       (`.rx-section--pt-overlap` in _utilities.css) that pulls the story block
       up over the stats. Both are correct at 1512, where the section is a
       100svh stage with its content centred and hundreds of pixels of empty
       arc-sweep at each end for the overlap to eat into.

       The phone frame has no stage — the composition fills the frame — so
       "none" plus a -96 overlap put the story block straight through the last
       metric. Measured before this: the next section's top edge landed 92px
       ABOVE the fourth caption's floor.

       BOTH ENDS ARE OVERLAPPED, which is easy to miss because the two use
       different modifiers: feature-card above carries space_bottom "overlap"
       (`.rx-section--pb-overlap`, margin-bottom -96) and story-feature below
       carries space_top "overlap" (margin-top -96). Measured before this: the
       intro started 48px above the previous section's floor and the next
       section's top edge landed 92px above the fourth caption's.

       So below 1025 the choice variables are overridden rather than the base
       ones — `--rx-section-pt` alone would still lose to the utility class's
       `--rx-section-pt-choice: 0` — and each end carries one --rx-space-9
       extra, which is exactly what its overlap takes back. */
    --rx-section-pt-choice: calc(var(--rx-section-y) + var(--rx-space-9));
    --rx-section-pb-choice: calc(var(--rx-section-y) + var(--rx-space-9));
  }

  .rx-stats__inner {
    grid-template-columns: minmax(0, 1fr);
    /* The field bleeds to the section edge at desktop, so .rx-stats__inner is
       deliberately not a container there. Once the arcs are gone there is
       nothing to bleed, and the block needs the gutter back. */
    width: min(100% - var(--rx-gutter) * 2, var(--rx-container-max));
    margin-inline: auto;
  }

  .rx-stats__intro {
    padding-left: 0;
    padding-top: 0;
    padding-right: 0;
    margin-bottom: var(--rx-space-8);
  }

  .rx-stats__heading { max-width: none; }
  .rx-stats__body { max-width: 42ch; }

  /* The drift is the only thing still on the item down here, and it is not
     gated on the reveal flag, so this has to kill it unconditionally rather
     than only in the revealed state. Same specificity as the @supports rule
     that sets it and later in the file. */
  .rx-stats__field--orbit .rx-stats__item {
    animation: none;
    translate: none;
  }

  /* The type is ISSUED BY THE DOT here, exactly as it is at 1512 — so the two
     overrides that used to sit here are gone. They pinned the figure and
     caption in place and swapped their entrance for a plain fade, on the
     reasoning that they could not slide out of a dot that was not drawn. The
     dot IS drawn now, so the desktop rules stand and the type travels out of
     it. --rx-stats-text-from is restated in the field's own unit above, since
     the desktop's 2.2cqw is a share of a 912-wide field, not a 402 one. */

  /* THE STACKED LAYOUT IS DRAWN, from the phone frame 3157:8086. Everything
     below is transcribed from it rather than derived from the desktop
     composition, which is what the first pass did.

     The shape: four arcs sweeping across, one per metric, each metric hung
     BELOW a dot that sits on its own arc — figure and caption centred under
     the dot, not beside it. The four are staggered horizontally, so the eye
     runs down a zigzag rather than a column.

     The field IS the drawn 402-wide frame. Its aspect is the frame's own
     (402 / 677.663, the metric band from dot 1's top to caption 4's floor),
     which is also the arcs' viewBox — so the mapping is 1:1 and every
     percentage below is a drawn coordinate divided by the frame.

     --rx-stats-mu is one design pixel: 1cqw is 1% of the field, and the field
     is the 402 frame, so 100/402 = 0.24876cqw. Capped at 462 so the 69px
     figure does not grow to 130 on a large phone — the drawing scales, and
     stops. */
  .rx-stats__field--orbit {
    /* FULL BLEED, because the drawn frame IS the viewport. The 22.35 of empty
       page each side of the composition is the frame's own margin and is
       already in the coordinates — leaving the field inside .rx-stats__inner's
       gutters charged for it twice and rendered the whole drawing at 87%, so
       the 69px figure came out at 60. Centred by offset rather than by a
       negative percentage margin, which resolves against the wrong box here. */
    position: relative;
    left: 50%;
    translate: -50% 0;
    inline-size: min(100vw, 462px);
    max-inline-size: none;
    min-inline-size: 0;
    aspect-ratio: 402 / 677.663;
    /* The ratio is the DRAWN band and has to stay exact — it is the arcs'
       viewBox and every item percentage resolves against it. But Figma's
       caption box is cap-trimmed at 35 tall where our line box is 43.9, so the
       last caption sits ~7px below the band's floor. Given back as margin,
       which cannot disturb the coordinate space the way padding would:
       percentages on an absolutely positioned child resolve against the
       PADDING box. */
    margin-block-end: calc(12 * var(--rx-stats-mu));
    --rx-stats-mu: 0.24876cqw;
    /* Drawn 23.919 overall (3157:8147). Split across the existing two rings so
       the halo still opens out of the core: 15.9 + 4 either side = 23.9. */
    --rx-stats-dot-core: calc(15.9 * var(--rx-stats-mu));
    --rx-stats-dot-spread: calc(4 * var(--rx-stats-mu));
    --rx-stats-dot-ring: max(1px, calc(1.2 * var(--rx-stats-mu)));
    /* The ride is a rotation about a centre 1149 BELOW the dot, so at this
       radius a small angle is a long, almost flat travel.

       MATCHED TO THE DESKTOP'S PROPORTION, not picked by eye. At 1512 the dot
       swings 22 degrees about a 1151 radius = 442px across a 1512 frame, which
       is 29% of it. 29% of 402 is 117, so the angle here is 117 / 1149 rad =
       5.8 degrees. The first pass used -8, which is 161px — 40% of the frame,
       half again further than 1512 travels proportionally, and that extra
       distance is most of what read as "too fast": the same scroll had to
       carry a longer journey.

       The text is matched the same way. Its 20 is close to the desktop's
       2.2cqw (20px at a 912 field) in ABSOLUTE terms, which is right here
       because the figure is 69px at both sizes — the type does not shrink on
       the phone, so the distance it slides should not either. */
    --rx-stats-sweep: -5.8deg;
    --rx-stats-text-from: calc(20 * var(--rx-stats-mu));

    /* THE STEPS DO NOT OVERLAP HERE, and that is a change of ORDER from 1512.
       Desktop runs text-lag 430 against open-at 900, so the figure is already
       emerging while the dot is still travelling and the halo lands last —
       which works when four metrics are staggered 150ms apart and read as one
       sweep across the band. Stacked, each metric is alone on the screen and
       its own sequence is the whole event, so it plays in three separate
       beats: the dot arrives, the halo confirms it, then the type is issued.

       Measured before this: the figure was at 0.71 opacity by 667ms with the
       dot still 13px short of its resting place and the halo at zero.

         arc     0 -> 700
         ride    50 -> 490      (440ms over 123px)
         halo    490 -> 790     on a dot that has just parked
         figure  580 -> 960
         caption 650 -> 1030    (+70, so the two do not move as one slab)

       AND THE WHOLE THING RESOLVES IN ABOUT A SECOND. The first pass at these
       beats ran to 1660ms, which is longer than a reader takes to scroll a
       metric past: the sequence was still playing when it left the screen, so
       on a normal scroll you never saw it finish. Every duration here is about
       60% of that pass — enough to keep three separate beats, short enough to
       land while the metric is still on screen.

       The ride is shorter than the desktop's 1000ms for a second reason: the
       travel is shorter, 123px against 442. At 1000ms it crawls — 0.12px/ms
       against the desktop's 0.44. At 440 it is 0.28. */
    --rx-stats-draw: 700ms;
    --rx-stats-ride-lag: 50ms;
    --rx-stats-ride-dur: 440ms;
    --rx-stats-open-at: 490ms;
    --rx-stats-open-dur: 300ms;
    --rx-stats-text-lag: 580ms;
    --rx-stats-text-dur: 380ms;
  }

  .rx-stats__field--orbit .rx-stats__list {
    position: absolute;
    inset: 0;
    display: block;
  }

  .rx-stats__list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--rx-space-8) var(--rx-space-7);
  }

  /* Each metric is a 181-wide block centred on its own axis, with the dot
     pinned above it. `left` is the drawn text centre as a share of the frame;
     the negative start margin takes it back by half the block, so the block is
     centred on that axis without a `translate` — which the scroll drift owns. */
  .rx-stats__field--orbit .rx-stats__item {
    position: absolute;
    inline-size: calc(181 * var(--rx-stats-mu));
    margin-inline-start: calc(-90.5 * var(--rx-stats-mu));
    text-align: center;
    /* Figure's cap-top is 43.664 below the item's top; a 69/1.08 line box
       overshoots its cap by (1.08 - 1.21) / 2 + .242 = .177em = 12.21. */
    padding-block-start: calc(31.454 * var(--rx-stats-mu));
  }

  .rx-stats__field--orbit .rx-stats__item--1 { left: 63.720%; top:  0.000%; --rx-stats-org: calc(-58.877 * var(--rx-stats-mu)) calc(1149.159 * var(--rx-stats-mu)); }
  .rx-stats__field--orbit .rx-stats__item--2 { left: 28.071%; top: 26.267%; --rx-stats-org: calc( 84.431 * var(--rx-stats-mu)) calc(1149.159 * var(--rx-stats-mu)); }
  .rx-stats__field--orbit .rx-stats__item--3 { left: 56.918%; top: 52.533%; --rx-stats-org: calc(-31.535 * var(--rx-stats-mu)) calc(1149.159 * var(--rx-stats-mu)); }
  .rx-stats__field--orbit .rx-stats__item--4 { left: 38.488%; top: 78.800%; --rx-stats-org: calc( 42.554 * var(--rx-stats-mu)) calc(1149.159 * var(--rx-stats-mu)); }

  /* The dot sits 11.96 down and 11.955 LEFT of the block's centre — it is not
     centred over the figure, and that offset is what puts it on the arc.
     Measured against the circles, the drawn dots land at radius 1149.6 to
     1152.3 against a drawn 1152.454: Figma's own nudge, under 3px on a 1152
     radius, and invisible at a 24px ring. Transcribed rather than re-derived,
     the same tolerance the frame insets already carry. */
  .rx-stats__field--orbit .rx-stats__dot {
    display: block;
    left: calc(50% - 11.955 * var(--rx-stats-mu));
    top: calc(11.96 * var(--rx-stats-mu));
    transform-origin: var(--rx-stats-org);
  }

  /* Figure 69/1.08 SemiBold (3157:8150), caption 18/1.22 Medium (3157:8146),
     both centred and both Dark Brown/Base. The caption's drawn gap under the
     figure comes out at -1.75 once both line boxes' leading is taken off the
     14.84 of ink, so it is zero. */
  .rx-stats__field--orbit .rx-stats__value,
  .rx-stats__field--orbit .rx-stats__label {
    position: static;
    left: auto;
    top: auto;
    width: auto;
    max-width: none;
    white-space: normal;
    text-align: center;
  }

  .rx-stats__field--orbit .rx-stats__value {
    font-size: calc(69 * var(--rx-stats-mu));
    line-height: 1.08;
    letter-spacing: -0.02em;
    white-space: nowrap;
  }

  .rx-stats__field--orbit .rx-stats__label {
    margin-top: 0;
    font-size: calc(18 * var(--rx-stats-mu));
    font-weight: 500;
    line-height: 1.22;
  }
}

/* The invented vertical scatter that used to live here is gone. It existed to
   stop a plain 2x2 grid reading as a table; the drawn layout staggers the four
   itself, on measured coordinates, so a second scatter on top would fight it.
   It only ever applied to the NON-orbit fallback anyway (any count but four),
   which is a plain grid and can keep it. */
@media (max-width: 1024px) {
  .rx-stats__field:not(.rx-stats__field--orbit) .rx-stats__item--2 { translate: 0 -2.5rem; }
  .rx-stats__field:not(.rx-stats__field--orbit) .rx-stats__item--3 { translate: 0 1.5rem; }
  .rx-stats__field:not(.rx-stats__field--orbit) .rx-stats__item--4 { translate: 0 -1rem; }
}

/* --- Each metric on its OWN trigger ---------------------------------------
   At 1512 the band is one event: the section arrives, the four arcs draw left
   to right --rx-stats-step apart and the figures follow. That works because
   the whole composition is on screen at once.

   Stacked it is not — the four run 678 design pixels down the page, so a
   single block-level trigger fires all four while only the first is in view,
   and the rest have finished before the reader reaches them.

   WHAT THIS IS NOT: the properties are not tied to scroll position. A first
   pass drove them from a view() timeline, which scrubs the whole sequence
   back and forth with the scrollbar and collapses the three steps into one
   continuous smear. Each metric plays the SAME time-based sequence the
   desktop plays; the only thing scroll decides is WHEN it starts.

   main.js stamps [data-rx-stat-in] on each item, and on its matching arc, as
   that item comes into view. The reveal driver cannot do this — it groups by
   closest('[data-rx-reveal-focus], [data-rx-reveal-group]'), so everything in
   a scope shares one trigger, and giving each item its own data-rx-reveal
   would make it its own scope at EVERY width and take the desktop
   choreography apart to fix the phone.

   THE STEPS, in order, and they must not overlap:
     1. the arc draws
     2. the dot rides in ALONG it              (ride-lag, 100ms)
     3. the halo activates on the parked dot   (open-at, 900ms)
     4. the figure emerges out of the dot      (text-lag, 430ms)
        and the caption follows 90ms later
   These are the desktop's own tokens and its own keyframes. The only thing
   dropped is --rx-stats-delay, the left-to-right stagger that separates the
   four at 1512: each metric is its own event here, so it starts at zero. */
@media (max-width: 1024px) {
  /* OFF: the block-level entrance fires all four the moment the section
     arrives, which is the behaviour being replaced. Same specificity as the
     per-item rules below and earlier in the file, so those win. */
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__arcs--stacked .rx-stats__arc,
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__item .rx-stats__dot,
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__item .rx-stats__dot::before,
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__item .rx-stats__dot::after,
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__item .rx-stats__value,
  [data-rx-reveal-ready] .rx-stats__inner[data-rx-revealed] .rx-stats__item .rx-stats__label {
    animation: none;
  }

  /* Hidden until its OWN turn, not until the block's. */
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__arcs--stacked .rx-stats__arc:not([data-rx-stat-in]) {
    stroke-dashoffset: var(--rx-stats-arc-len);
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item:not([data-rx-stat-in]) .rx-stats__dot {
    transform: rotate(var(--rx-stats-sweep));
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item:not([data-rx-stat-in]) .rx-stats__dot::before {
    opacity: 0;
    scale: calc(var(--rx-stats-dot-core) / (var(--rx-stats-dot-core) + var(--rx-stats-dot-spread) * 2));
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item:not([data-rx-stat-in]) .rx-stats__dot::after {
    opacity: 0;
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item:not([data-rx-stat-in]) .rx-stats__value,
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item:not([data-rx-stat-in]) .rx-stats__label {
    opacity: 0;
    transform: translateX(calc(var(--rx-stats-text-from) * -1));
  }

  /* 1. the line */
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__arcs--stacked .rx-stats__arc[data-rx-stat-in] {
    animation: rx-stats-draw var(--rx-stats-draw) var(--rx-stats-ease) 0ms both;
  }

  /* 2. the dot rides along it */
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item[data-rx-stat-in] .rx-stats__dot {
    animation: rx-stats-ride var(--rx-stats-ride-dur) var(--rx-stats-ease)
               var(--rx-stats-ride-lag) both;
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item[data-rx-stat-in] .rx-stats__dot::after {
    animation: rx-stats-fade var(--rx-stats-text-dur) var(--rx-stats-ease)
               var(--rx-stats-ride-lag) both;
  }

  /* 3. the halo activates, on a dot that has already stopped */
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item[data-rx-stat-in] .rx-stats__dot::before {
    animation: rx-stats-open var(--rx-stats-open-dur) var(--rx-stats-ease-open)
               var(--rx-stats-open-at) both;
  }

  /* 4. the type is issued by the dot */
  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item[data-rx-stat-in] .rx-stats__value {
    animation: rx-stats-emerge var(--rx-stats-text-dur) var(--rx-stats-ease)
               var(--rx-stats-text-lag) both;
  }

  [data-rx-reveal-ready] .rx-stats__field--orbit .rx-stats__item[data-rx-stat-in] .rx-stats__label {
    animation: rx-stats-emerge var(--rx-stats-text-dur) var(--rx-stats-ease)
               calc(var(--rx-stats-text-lag) + 70ms) both;
  }
}

/* The phone frame IS the stacked layout — see the drawn block above. Nothing
   extra is needed below 768: the composition scales with the field, which is
   capped, and the only thing that changes is how much margin sits beside it. */

@media (prefers-reduced-motion: reduce) {
  /* The driver stamps [data-rx-revealed] immediately here and never sets the
     ready flag, so nothing above applies — this is the belt to that brace. */
  .rx-stats__arc,
  .rx-stats__item,
  .rx-stats__dot,
  .rx-stats__value,
  .rx-stats__label,
  .rx-stats__intro {
    animation: none !important;
    opacity: 1;
    transform: none;
    translate: none;
    scale: none;
    stroke-dashoffset: 0;
  }

  /* The rings are held on their centre BY a translate, so they cannot join
     the blanket reset above — clearing it would drop both of them down and
     right by half their own size. Only the halo's growth is undone. */
  .rx-stats__dot::before,
  .rx-stats__dot::after {
    animation: none !important;
    opacity: 1;
    scale: none;
  }
}

/* --- Stacked: its own drawing, not the desktop one turned --------------------
   The desktop arcs are five near-vertical sweeps registered to a two-column
   composition. The phone frame (3157:8086) draws four near-horizontal ones
   registered to a staggered single column, so this is a SECOND path set in the
   markup rather than the first one transformed.

   It was `rotate: 90deg` on a stretched copy of the desktop svg for a while,
   and that is what forced the dots to be dropped: preserveAspectRatio="none"
   maps the viewBox linearly onto the box, which is the only reason a dot
   placed by percentage sits ON its arc, and rotating the box destroys that
   mapping.

   Here the field carries the viewBox's own ratio, so the map is 1:1 with no
   stretch at all and the arcs keep their true curvature. overflow stays
   visible: the paths run x -40 to 442 so they bleed past both edges. */
@media (max-width: 1024px) {
  .rx-stats__arcs--flat { display: none; }

  .rx-stats__field--orbit .rx-stats__arcs--stacked {
    display: block;
    position: absolute;
    inset: 0;
    width: auto;
    height: auto;
    overflow: visible;
    pointer-events: none;
  }
}
