/*
Block 02 "Hero". Every value comes from tokens.css, spec: docs/blocks/02-hero.md.
Effects that are neither in the design nor in the spec are deliberately absent here:
no shadows, no hovers that were not asked for. The scene animations (photo entrance,
text entrance, badge float, counters, chart drawing, card tilt) are all CSS and plain
JS, no libraries.
*/

/*
overflow-x: clip and not hidden: hidden on one axis forces the other to auto, and the
section would gain its own scroll container; clip leaves the vertical axis alone.

It is here because of the entrance. The right-hand badge deliberately overhangs the photo,
and during its zoom-out it is --badge-in-scale times wider still, which pushed it past the
right edge of the document — the page grew a horizontal scrollbar for the two seconds the
entrance lasts and then lost it again. Nothing is lost by clipping: at every width where
the enlarged badge crosses the section boundary it is already outside the viewport, so the
clip removes the scrollbar without removing anything that was visible.

The badge still overhangs the photo — the clip is on the section, not on .hero__media.
*/
.hero {
  padding-block: var(--hero-top) var(--hero-bottom);
  overflow-x: clip;
}

.hero__inner {
  width: min(var(--page-max), 100% - var(--page-inset) * 2);
  margin-inline: auto;
}

/*
align-items: stretch is not cosmetic: the text column has to reach the bottom of the
photo, otherwise the buttons do not line up with its bottom edge as they do in the design.
*/
.hero__row {
  display: flex;
  align-items: stretch;
  gap: var(--hero-gap);
}

.hero__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-width: 0;
  max-width: var(--hero-col);
}

.hero__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-hero-title);
  font-weight: var(--fw-display-dark);
  letter-spacing: var(--ls-hero-title);
  line-height: var(--lh-hero-title);
}

/*
The tone of a segment sets both colour and weight: in the design the dark parts of the
heading are bold and the accent parts are light. The list of tones is closed; an
arbitrary colour is not available to the client.

inline-block rather than inline: a segment must not break in the middle of itself. In
the design each segment takes its own line, and at in-between widths the line has to
wrap on a segment boundary, otherwise the tones get mixed inside one line.
*/
.hero__title-segment {
  display: inline-block;
}

.hero__title-segment--dark {
  color: var(--heading-ink);
  font-weight: var(--fw-display-dark);
}

.hero__title-segment--accent {
  color: var(--accent);
  font-weight: var(--fw-display-accent);
}

.hero__lead {
  max-width: var(--hero-lead-w);
  margin: var(--hero-title-gap) 0 0;
  color: var(--ink);
  font-size: var(--fs-hero-lead);
  font-weight: 400;
  letter-spacing: var(--ls-hero-lead);
  line-height: var(--lh-hero-lead);
}

/*
margin-top: auto pins the buttons to the bottom of the column (= to the bottom of the
photo). padding-top keeps a minimum gap if the column turns out shorter than the photo.
*/
.hero__actions {
  display: flex;
  align-items: center;
  gap: var(--hero-btn-gap);
  margin-top: auto;
  padding-top: 40px;
}

.hero-btn {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  font-size: var(--fs-hero-btn);
  font-weight: 500;
  letter-spacing: var(--ls-hero-btn);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}

.hero-btn--solid {
  width: var(--hero-btn-solid-w);
  height: var(--hero-btn-solid-h);
  background: var(--ink);
  color: var(--surface);
}

.hero-btn--outline {
  width: var(--hero-btn-outline-w);
  height: var(--hero-btn-outline-h);
  border: 1px solid var(--hero-btn-outline-border);
  color: var(--ink);
}

/*
perspective goes here and not on the card itself: perspective applies to an element's
children, and the vanishing point has to be shared by the whole media area. Put it on
the card and it would only affect the card's own children, leaving the card flat.
*/
.hero__media {
  position: relative;
  flex: 0 0 auto;
  align-self: flex-start;
  width: var(--hero-media-w);
  perspective: var(--hero-perspective);
}

/*
The card: the photograph and the chart line. This is what tilts; the badges sit outside it.

Radius and clipping moved here from .hero__photo, because the crop knob
(--hero-photo-zoom) scales the image content and it is the card that has to clip it —
otherwise a zoomed frame would spill past the rounded corner.

transform-style: flat is set explicitly. preserve-3d would separate the image and the
chart layer in depth: they lie in one plane, and under tilt the chart line would drift
against the photo.

Three different properties move, and that is not a matter of taste: the entrance uses
translate and scale, the idle sway uses rotate, the cursor tilt uses transform. Collapse
them into a single transform and they start evicting each other — two animations on one
property do not compose.
*/
.hero__frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--hero-media-radius);
  transform: rotateX(var(--hero-tilt-rx, 0deg)) rotateY(var(--hero-tilt-ry, 0deg));
  transform-style: flat;
  animation: hero-frame-sway var(--hero-sway-dur) ease-in-out infinite;
  will-change: transform;

  /* Returning from a tilt: long, on its own. Following the cursor is short, below. */
  transition: transform var(--hero-tilt-return) var(--ease);
}

.hero__frame--tilting {
  transition-duration: var(--hero-tilt-follow);
}

/*
The idle sway travels a closed circle across two axes: it dips forward, then leans
sideways, then back. A circle and not a single-axis pendulum: a pendulum reads as a
shiver, a circle reads as volume. The keyframes are closed, hence infinite without
alternate.

The axis and angle in `rotate` interpolate as a rotation rather than per component, so
the transition between keyframes with different axes takes the shortest arc instead of
collapsing through zero.
*/
@keyframes hero-frame-sway {
  0% {
    rotate: 1 0 0 var(--hero-sway-x);
  }

  25% {
    rotate: 0 1 0 var(--hero-sway-y);
  }

  50% {
    rotate: 1 0 0 calc(var(--hero-sway-x) * -1);
  }

  75% {
    rotate: 0 1 0 calc(var(--hero-sway-y) * -1);
  }

  100% {
    rotate: 1 0 0 var(--hero-sway-x);
  }
}

/*
The card's entrance is its own and comes first in the scene: the photo arrives before
the badges and the text. The final state is the default; the initial one only applies
under .js.
*/
@keyframes hero-frame-in {
  from {
    opacity: 0;
    translate: 0 var(--hero-in-photo-shift);
    scale: var(--hero-in-photo-scale);
  }

  to {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }
}

.js .hero__frame--pending {
  opacity: 0;
  translate: 0 var(--hero-in-photo-shift);
  scale: var(--hero-in-photo-scale);
}

.hero__frame--in {
  animation:
    hero-frame-sway var(--hero-sway-dur) ease-in-out infinite,
    hero-frame-in var(--hero-in-photo-dur) var(--hero-in-ease)
      var(--hero-in-photo-delay) both;
}

.hero__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--hero-media-ratio);
  object-fit: cover;
}

/*
The crop knob for the default photo (see --hero-photo-zoom, which currently sits at 1,
i.e. no crop). It applies to the photograph and to the chart line identically, and it
has to: the line is anchored to the body in the shot — it runs along the chest, following
the chain. Scale the photo alone and the line stays where it is and slides up onto the
face. This is one transform for one image, split across two layers only because the line
must stay a separate layer: in the default photo it was baked in, and it would have
travelled away with the picture the moment the client replaced it.

scale moves, not width: the layout has to stay exactly as it is, the block geometry is
already accepted.
*/
.hero__photo,
.hero-chart {
  scale: var(--hero-photo-zoom);
  transform-origin: var(--hero-photo-origin);
}

/*
Frosted glass. -webkit-backdrop-filter is mandatory: Safari before 18 only knows the
prefixed form, and without it the badge there becomes a flat translucent fill. What gets
blurred is the backdrop under the badge — that is what keeps the white text readable both
over light areas of the photo and over the overhang past its edge, where the page canvas
is what sits underneath.

The shape comes from a clip-path squircle taken from WEIGHT.svg. border-radius is declared
before it and stays as the fallback: where path() is unsupported the clip-path declaration
is dropped and the badge remains a rounded rectangle rather than turning into a sharp one.
The backdrop-filter backdrop is clipped by the same contour, so the glass does not leak
past the corner.
*/
.hero-badge {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: var(--badge-w);
  height: var(--badge-h);
  padding-right: var(--badge-pad-r);
  padding-left: var(--badge-pad-l);
  border-radius: var(--badge-radius);
  background: var(--badge-bg);
  clip-path: var(--badge-clip);
  color: var(--surface);
  -webkit-backdrop-filter: blur(var(--badge-blur));
  backdrop-filter: blur(var(--badge-blur));

  /*
  The float follows a closed path across two axes. The keyframes are closed (the last
  equals the first), so the animation runs infinite without alternate: alternate would
  replay the same path backwards and the movement would read as a pendulum.

  The translate property moves, not transform: transform is busy with the entrance
  (scale) at the same time, and two animations on one property would not compose.

  Each badge's timing is gathered into three variables and overridden in a single block
  below. Otherwise, with two animations, animation-duration and animation-delay would
  have to be duplicated as two-value lists, where the position in the list silently
  decides which animation a number belongs to.
  */
  --badge-float-dur: var(--badge-float-dur-tr);
  --badge-float-delay: 0s;
  --badge-in-delay: var(--badge-in-delay-tr);

  animation: hero-badge-float var(--badge-float-dur) ease-in-out var(--badge-float-delay)
    infinite;
}

/*
The hairline edge around the badge. It is not in the Figma export — this is the client's
decision: without it the glass badge loses its boundary both over light areas of the photo
and over the overhang past its edge, where the page canvas is underneath.

Neither border nor an inset box-shadow works here: both are built on a rounded rectangle,
while the badge is clipped by a squircle that starts curving 17.6px before the corner. An
edge drawn on radius 10 would fall outside the squircle and be cut away by clip-path at
all four corners — that is, it would disappear exactly where the shape reads. So the edge
is drawn as a ring: one contour made of two subpaths, the outer one and a shrunk copy,
with the evenodd rule (--badge-ring-clip).

The pseudo-element is absolutely positioned and therefore does not become a flex item of
the badge — otherwise space-between would spread the dot and the text around it.

The fill is only switched on where the ring contour is actually supported: if
path(evenodd, …) fails to parse, the clip-path declaration is dropped, and without this
guard the edge would flood the whole badge.
*/
.hero-badge::after {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  clip-path: var(--badge-ring-clip);
  pointer-events: none;
}

@supports (clip-path: path(evenodd, "M0 0Z")) {
  .hero-badge::after {
    background: var(--badge-border-color);
  }
}

@keyframes hero-badge-float {
  0% {
    translate: 0 calc(var(--badge-float-y) * -1);
  }

  25% {
    translate: var(--badge-float-x) calc(var(--badge-float-y) * -0.3);
  }

  50% {
    translate: calc(var(--badge-float-x) * 0.35) var(--badge-float-y);
  }

  75% {
    translate: calc(var(--badge-float-x) * -1) calc(var(--badge-float-y) * 0.25);
  }

  100% {
    translate: 0 calc(var(--badge-float-y) * -1);
  }
}

/*
The entrance. The final state is the default, as with the chart: without JS the badge sits
at its own size and is fully visible. The initial state only applies under .js, which is
set on <html> in <head> before the first paint, so the jump from the finished state to the
hidden one is never seen.

The fade and the zoom are two animations, not one. Sharing a duration and an easing is
exactly what made the zoom-out invisible: --badge-in-ease is front-loaded, so the scale was
practically finished while the badge was still transparent, and nothing was left to watch
by the time it became opaque. Split apart, opacity is done inside --badge-in-fade-dur and
the scale goes on settling in plain sight for the rest of --badge-in-dur.

They compose because they animate different properties — the same reason the float, which
owns translate, can run alongside both.
*/
@keyframes hero-badge-fade-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes hero-badge-zoom-in {
  from {
    scale: var(--badge-in-scale);
  }

  to {
    scale: 1;
  }
}

.js .hero-badge--pending {
  opacity: 0;
  scale: var(--badge-in-scale);
}

.hero-badge--in {
  animation:
    hero-badge-float var(--badge-float-dur) ease-in-out var(--badge-float-delay) infinite,
    hero-badge-fade-in var(--badge-in-fade-dur) var(--badge-in-ease) var(--badge-in-delay)
      both,
    hero-badge-zoom-in var(--badge-in-dur) var(--badge-in-zoom-ease) var(--badge-in-delay)
      both;
}

/* The right badge overhangs the edge of the photo — that is how the design has it, do not clip. */
.hero-badge--top-right {
  top: var(--badge-tr-top);
  right: var(--badge-tr-right);
}

.hero-badge--bottom-left {
  bottom: var(--badge-bl-bottom);
  left: var(--badge-bl-left);

  /* A negative delay shifts the phase immediately, with no pause before it starts. */
  --badge-float-dur: var(--badge-float-dur-bl);
  --badge-float-delay: var(--badge-float-delay-bl);
  --badge-in-delay: var(--badge-in-delay-bl);
}

.hero-badge__dot {
  flex: 0 0 auto;
  width: var(--badge-dot);
  height: var(--badge-dot);
  border-radius: 50%;
  background: var(--surface);

  /* The glow ring from filter1_d: even thickness, no blur. */
  box-shadow: 0 0 0 var(--badge-dot-ring) var(--badge-dot-ring-color);
}

.hero-badge__text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-width: 0;
}

.hero-badge__value {
  font-size: var(--fs-badge-value);
  font-weight: 600;
  letter-spacing: var(--ls-badge-value);
  line-height: 1;
}

.hero-badge__label {
  margin-top: var(--badge-gap);
  font-size: var(--fs-badge-label);
  font-weight: 500;
  letter-spacing: var(--ls-badge-label);
  line-height: 1;
  text-transform: uppercase;
}

/*
The chart layer. Radius and overflow live on it and not on .hero__media: the media wrapper
must not clip, the right badge deliberately overhangs it.

translate carries the layer along with the left-hand badge (see --chart-shift-*). It has to
be the translate property and not transform: the crop knob already holds scale on this same
layer, and gathering the two into one transform would make them evict each other.
*/
.hero-chart {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: var(--hero-media-radius);
  color: var(--surface);
  translate: var(--chart-shift-x) var(--chart-shift-y);
  pointer-events: none;
}

.hero-chart__line {
  fill: none;
  stroke: currentcolor;
  stroke-width: var(--chart-stroke);
  stroke-linecap: round;
  stroke-linejoin: round;
}

.hero-chart__dot {
  fill: currentcolor;
}

/*
The final state is the default: without JS the line is drawn in full and the dots are in
place. The initial states only apply under .js, which is set in <head> before the first
paint, so the finished frame is never seen winding back to zero.

dasharray is set on the line itself rather than together with offset in the --pending
state: remove it at the moment of the start and dashoffset stops affecting anything, and
the line appears whole in a single frame while the transition runs down to zero for nothing.
*/
.js .hero-chart__line {
  stroke-dasharray: var(--chart-len);
  stroke-dashoffset: 0;
}

.js .hero-chart--pending .hero-chart__line {
  stroke-dashoffset: var(--chart-len);
}

.js .hero-chart--pending .hero-chart__dot {
  opacity: 0;
}

/*
The line is built point by point and the dots surface in step with it: each dot's delay is
proportional to its place along the line.

Both delays are offset by --chart-start-delay: the chart only starts building after the
badges have surfaced. Otherwise the whole scene starts on one frame and does not read as
a sequence of steps.
*/
.hero-chart--draw .hero-chart__line {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--chart-line-dur) cubic-bezier(0.33, 0, 0.2, 1);
  transition-delay: var(--chart-start-delay);
}

.hero-chart--draw .hero-chart__dot {
  opacity: 1;
  transition: opacity var(--chart-dot-dur) linear;
  transition-delay:
    calc(
      var(--chart-start-delay) + var(--chart-line-dur) / 4 * var(--dot-index) -
        var(--chart-dot-dur)
    );
}

/*
The text entrance. The rules hang on the column and target element types inside it rather
than specific nodes: there can be any number of heading segments, and they pick up the
stagger by themselves — the delay is derived from --segment-index, which the render loop
stamps on each one.

The final state is the default: without JS the text is visible immediately and in full.
The initial state only applies under .js, which is set on <html> in <head> before the
first paint, so the jump from the finished state to the hidden one is never seen.
*/
@keyframes hero-text-in {
  from {
    opacity: 0;
    translate: 0 var(--hero-in-text-shift);
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

.js .hero__text--pending .hero__title-segment,
.js .hero__text--pending .hero__lead,
.js .hero__text--pending .hero__actions {
  opacity: 0;
  translate: 0 var(--hero-in-text-shift);
}

.hero__text--in .hero__title-segment,
.hero__text--in .hero__lead,
.hero__text--in .hero__actions {
  animation: hero-text-in var(--hero-in-text-dur) var(--hero-in-ease) both;
}

.hero__text--in .hero__title-segment {
  animation-delay:
    calc(
      var(--hero-in-title-delay) + var(--segment-index, 0) * var(--hero-in-title-step)
    );
}

.hero__text--in .hero__lead {
  animation-delay: var(--hero-in-lead-delay);
}

.hero__text--in .hero__actions {
  animation-delay: var(--hero-in-actions-delay);
}

/* Mobile: one column, order heading → description → buttons → photo. */
@media (max-width: 700px) {
  .hero__row {
    flex-direction: column;
  }

  .hero__text {
    max-width: none;
  }

  .hero__actions {
    margin-top: 24px;
    padding-top: 0;
  }

  .hero-btn {
    flex: 1 1 0;
    width: auto;
  }

  .hero__media {
    align-self: stretch;
    width: 100%;
  }
}

/*
No motion means the final state immediately: badges in place, line drawn, dots visible.
The rules are spelled out explicitly rather than relying on the global duration clamp from
base.css: on a closed-loop animation, clamping the duration would leave the badge in the
position of its last keyframe, i.e. offset by --badge-float-y.
*/
@media (prefers-reduced-motion: reduce) {
  .hero-badge,
  .js .hero-badge--pending,
  .hero-badge--in {
    opacity: 1;
    translate: none;
    scale: none;
    transform: none;
    animation: none;
  }

  /*
  The card: no entrance, no sway, no tilt. rotate and transform are cleared explicitly —
  the cursor tilt is written by the script into inline custom properties, and without
  zeroing transform it would keep working.
  */
  .hero__frame,
  .js .hero__frame--pending,
  .hero__frame--in {
    opacity: 1;
    translate: none;
    scale: none;
    rotate: none;
    transform: none;
    animation: none;
    transition: none;
  }

  .js .hero__text--pending .hero__title-segment,
  .js .hero__text--pending .hero__lead,
  .js .hero__text--pending .hero__actions,
  .hero__text--in .hero__title-segment,
  .hero__text--in .hero__lead,
  .hero__text--in .hero__actions {
    opacity: 1;
    translate: none;
    animation: none;
  }

  .js .hero-chart--pending .hero-chart__line,
  .js .hero-chart__line,
  .hero-chart__line {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
    transition: none;
  }

  .js .hero-chart--pending .hero-chart__dot,
  .hero-chart__dot {
    opacity: 1;
    transition: none;
  }
}
