/* Base elements + a small set of semantic, CMS-agnostic helpers.
   Class naming: site-*, article-*, review-*: no framework or builder prefixes. */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background-color: var(--bg-page);
  color: var(--text-body);
  font-family: var(--font-body);
  font-size: var(--step-body);
  line-height: var(--lh-body);
  font-weight: var(--weight-regular);
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection {
  background: var(--selection-bg);
  color: var(--text-primary);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  font-family: var(--font-display);
  color: var(--text-primary);
  font-weight: var(--weight-semibold);
  line-height: var(--lh-heading);
  letter-spacing: var(--track-heading);
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: var(--text-signal);
  text-decoration: none;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
  transition: color var(--dur-fast) var(--ease-out),
    text-decoration-color var(--dur-fast) var(--ease-out);
}

a:hover {
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-color: var(--signal);
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

code,
kbd,
samp,
pre {
  font-family: var(--font-mono);
  font-feature-settings: "liga" 0;
}

hr {
  border: 0;
  border-top: var(--border-width) solid var(--border-hairline);
  margin: var(--space-xl) 0;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* ── focus: one visible ring everywhere, keyboard-first ── */
:focus-visible {
  outline: var(--focus-width) solid var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-xs);
}
:focus:not(:focus-visible) {
  outline: none;
}

.skip-link {
  position: absolute;
  left: var(--space-md);
  top: var(--space-md);
  z-index: 100;
  transform: translateY(-200%);
  background: var(--signal);
  color: var(--text-on-signal);
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  transition: transform var(--dur-base) var(--ease-out);
}
.skip-link:focus {
  transform: translateY(0);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── layout helpers ── */
.site-container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.site-section {
  padding-block: var(--section-y);
}

/* ── brand motifs ── */
/* Perforation field: the signature background texture. */
.dot-field {
  background-image: var(--texture-dots);
  background-size: var(--texture-dots-size);
}

/* Blueprint grid: used behind heroes and stat bands, always masked out. */
.grid-field {
  background-image: var(--texture-grid);
  background-size: var(--texture-grid-size);
  mask-image: radial-gradient(120% 100% at 50% 0%, #000 10%, transparent 75%);
}

/* Micro label with a leading tick: the system's connective tissue. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--step-micro);
  font-weight: var(--weight-medium);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.eyebrow::before {
  content: "";
  width: 18px;
  height: 2px;
  background: var(--signal);
}

/* ── signature motion ─────────────────────────────────────────────────────────
   Four named animations, used sparingly and never on text the reader is reading.
   All of them stop under prefers-reduced-motion (see below).
   ─────────────────────────────────────────────────────────────────────────── */

/* A single instrument line travelling down the hero grid. */
@keyframes signal-sweep {
  0% {
    transform: translateY(-10%);
    opacity: 0;
  }
  12% {
    opacity: 1;
  }
  88% {
    opacity: 1;
  }
  100% {
    transform: translateY(1000%);
    opacity: 0;
  }
}


@keyframes rise-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Entrance for hero copy: a shorter, softer rise so stacked lines read as one
   movement rather than six separate ones. */
@keyframes rise-in-soft {
  from {
    opacity: 0;
    transform: translateY(9px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Horizontal wipe, for hover underlines and for bars that fill. */
@keyframes wipe-in {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes settle {
  from {
    opacity: 0;
    transform: scale(1.03);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ── motion utilities ─────────────────────────────────────────────────────────
   Everything here animates transform or opacity only, and everything except the
   scroll progress bar is one shot. Nothing runs on a JS timer or listener.
   ─────────────────────────────────────────────────────────────────────────── */

/* Children rise in sequence on load. Capped at eight steps so a long list never
   makes the reader wait. */
.stagger > * {
  animation: rise-in-soft 520ms var(--ease-out) both;
}
.stagger > *:nth-child(1) {
  animation-delay: 40ms;
}
.stagger > *:nth-child(2) {
  animation-delay: 110ms;
}
.stagger > *:nth-child(3) {
  animation-delay: 180ms;
}
.stagger > *:nth-child(4) {
  animation-delay: 250ms;
}
.stagger > *:nth-child(5) {
  animation-delay: 320ms;
}
.stagger > *:nth-child(6) {
  animation-delay: 390ms;
}
.stagger > *:nth-child(n + 7) {
  animation-delay: 440ms;
}

/* Underline that wipes out from the left instead of fading in. */
.link-wipe {
  position: relative;
  text-decoration: none;
}
.link-wipe::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}
.link-wipe:hover::after,
.link-wipe:focus-visible::after {
  transform: scaleX(1);
}

/* Reading progress. Scroll-driven, so the browser handles it off the main thread
   with no scroll listener. Browsers without scroll-linked animations never scale
   it up, so it stays invisible instead of needing a JS fallback. */
.read-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 41;
  background: var(--grad-edge);
  transform: scaleX(0);
  transform-origin: left;
  pointer-events: none;
}

@supports (animation-timeline: scroll()) {
  .read-progress {
    animation: wipe-in linear both;
    animation-timeline: scroll(root block);
  }
}

/* Bars that fill once, shortly after load: review scores, meters, progress.
   Deliberately NOT scroll-linked. Every bar in this system sits inside a Card,
   and the Card's `overflow: hidden` (which rounds the edge rule and the texture
   corner) makes it the scroll container for its own contents, so a view()
   timeline there would never advance and the bar would sit permanently at
   scaleX(0), invisible. */
.bar-fill {
  transform: scaleX(0);
  transform-origin: left;
  animation: wipe-in 720ms var(--ease-out) 260ms both;
}

/* Media that settles rather than cutting in. */
.settle-in {
  animation: settle 900ms var(--ease-out) both;
}

/* The perforation field is static. An animated masked layer of this size
   wedges the compositor when the page is re-mounted, and the movement was
   never legible at this opacity anyway. */

/* Crop marks: four corner brackets that snap in on hover. The system's
   instrumentation motif applied to interactive panels. */
.crop-marks {
  position: relative;
}
.crop-marks > .crop-mark {
  position: absolute;
  width: 11px;
  height: 11px;
  border: 0 solid var(--signal);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
  pointer-events: none;
}
.crop-marks:hover > .crop-mark,
.crop-marks:focus-within > .crop-mark {
  opacity: 1;
  transform: none;
}
.crop-mark[data-corner="tl"] {
  top: 7px;
  left: 7px;
  border-top-width: 1px;
  border-left-width: 1px;
  transform: translate(4px, 4px);
}
.crop-mark[data-corner="tr"] {
  top: 7px;
  right: 7px;
  border-top-width: 1px;
  border-right-width: 1px;
  transform: translate(-4px, 4px);
}
.crop-mark[data-corner="bl"] {
  bottom: 7px;
  left: 7px;
  border-bottom-width: 1px;
  border-left-width: 1px;
  transform: translate(4px, -4px);
}
.crop-mark[data-corner="br"] {
  bottom: 7px;
  right: 7px;
  border-bottom-width: 1px;
  border-right-width: 1px;
  transform: translate(-4px, -4px);
}

@media (prefers-reduced-motion: reduce) {
  /* The progress bar is the one element whose end state is wrong when animation
     is disabled: it would sit permanently full. Hide it instead. */
  .read-progress {
    display: none;
  }

  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── signal stack ─────────────────────────────────────────────────────────────
   A pulse down the spine, and each layer lighting as it passes. Both are small
   unmasked elements, so they stay cheap however long the page is left open.
   ─────────────────────────────────────────────────────────────────────────── */
@keyframes stack-travel {
  0% {
    transform: translateY(0);
    opacity: 0;
  }
  8% {
    opacity: 1;
  }
  92% {
    opacity: 1;
  }
  100% {
    transform: translateY(var(--stack-travel, 220px));
    opacity: 0;
  }
}

@keyframes stack-scan {
  0%,
  100% {
    opacity: 0;
  }
  6%,
  18% {
    opacity: 1;
  }
  30% {
    opacity: 0;
  }
}

.stack-pulse {
  animation-name: stack-travel;
  animation-timing-function: var(--ease-in-out);
  animation-iteration-count: infinite;
}

.stack-scan {
  animation-name: stack-scan;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The hero recedes as the reader leaves the top of the page. */
@keyframes hero-exit {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0.2;
    transform: translateY(-44px) scale(0.99);
  }
}

@supports (animation-timeline: view()) {
  .hero-exit {
    animation: hero-exit linear both;
    animation-timeline: view();
    animation-range: cover 50% cover 100%;
  }
}

/* ── scroll-driven motion ─────────────────────────────────────────────────────
   Tied to the document scroll through native scroll-linked animations, so it
   runs off the main thread with no scroll listener. Browsers without support
   show the end state.

   Ranges are keyed to `cover`, never `entry`. An `entry` range is measured
   against the element's own height, so a card taller than the viewport can
   never finish entering and stays invisible far past the point where it is
   plainly on screen.
   ─────────────────────────────────────────────────────────────────────────── */
@keyframes enter-up {
  from {
    opacity: 0;
    transform: translateY(26px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes rule-draw {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes numeral-drift {
  from {
    transform: translateY(26px);
  }
  to {
    transform: translateY(-26px);
  }
}

@keyframes figure-rise {
  from {
    opacity: 0;
    transform: translateY(18px) scale(0.94);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@supports (animation-timeline: view()) {
  .enter-up {
    animation: enter-up 700ms var(--ease-out) both;
    animation-timeline: view();
    animation-range: cover 6% cover 40%;
  }

  .enter-figure {
    animation: figure-rise 800ms var(--ease-out) both;
    animation-timeline: view();
    animation-range: cover 8% cover 44%;
  }

  .rule-draw {
    transform-origin: left;
    animation: rule-draw 600ms var(--ease-out) both;
    animation-timeline: view();
    animation-range: cover 6% cover 38%;
  }

  .numeral-drift {
    animation: numeral-drift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}

/* Children arrive one after another as the block scrolls in. The animation sits
   on a wrapper, never on the card itself: a scroll-linked animation never
   finishes, so its `both` fill stays active forever and outranks any inline
   transform a component sets at runtime, which silently kills hover lifts. */
.stagger-scroll > * {
  display: grid;
}

@supports (animation-timeline: view()) {
  .stagger-scroll > * {
    animation: enter-up 700ms var(--ease-out) both;
    animation-timeline: view();
  }
  .stagger-scroll > *:nth-child(1) {
    animation-range: cover 6% cover 40%;
  }
  .stagger-scroll > *:nth-child(2) {
    animation-range: cover 11% cover 45%;
  }
  .stagger-scroll > *:nth-child(3) {
    animation-range: cover 16% cover 50%;
  }
  .stagger-scroll > *:nth-child(n + 4) {
    animation-range: cover 21% cover 55%;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Scroll-linked animations have no duration for the global rule to shorten,
     so they are switched off outright rather than frozen on frame one. */
  .enter-up,
  .enter-figure,
  .rule-draw,
  .numeral-drift,
  .hero-exit,
  .stagger-scroll > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .stack-pulse,
  .stack-scan {
    animation: none !important;
  }
  .stack-pulse {
    opacity: 0 !important;
  }
}

/*
 * Settled state for reveals the scroll timeline cannot finish (page bottom).
 * Must outrank the scroll-linked animation's `both` fill, hence !important.
 */
.enter-up.is-settled,
.stagger-scroll > .is-settled {
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
}

/* ── navigation ───────────────────────────────────────────────────────────────
   The masthead settles as the reader leaves the top of the page: the surface
   goes solid and a hairline appears beneath it. Both are opacity on absolutely
   positioned layers, never background-color on the bar itself, which would
   repaint the full width of the viewport on every frame.
   ─────────────────────────────────────────────────────────────────────────── */

@keyframes veil-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@supports (animation-timeline: scroll()) {
  .nav-veil,
  .nav-rule {
    animation: veil-in linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 180px;
  }
}

/* The wordmark's perforation dot breathes, the one always-on beat in the UI.
   The pulse runs on a separate ring element behind the dot, scaling and fading
   rather than growing a box-shadow. */
@keyframes dot-ring {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  70%,
  100% {
    transform: scale(3.4);
    opacity: 0;
  }
}

.dot-ring {
  animation: dot-ring 3.4s var(--ease-out) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .nav-veil,
  .nav-rule {
    opacity: 1 !important;
    animation: none !important;
  }
  .dot-ring {
    animation: none !important;
    opacity: 0 !important;
  }
}

/* Elementor emits `.elementor-element.e-con-full { max-width: 100% }` at 0,2,0,
   which e-con-full outranks `.site-container` at 0,1,0. Containers carrying both
   `site-section` and `site-container` therefore went full bleed while nested
   ones stayed boxed, so home bands rendered 1920px wide instead of the kit's
   1180px. Doubling the class lifts this to 0,3,0 without an !important. */
.elementor .site-container.site-container {
  max-width: var(--container);
}

/* Hello Elementor's base sheet paints every button
   `button:focus, button:hover { background-color: #c36; color: #fff }` at 0,1,1,
   which outranks a single-class component rule. Every bare-button control in
   this theme therefore flashes pink on hover or click unless it outranks that.
   Doubling each class lifts these to 0,2,1 without reaching for !important. */
.site-nav-burger.site-nav-burger:hover,
.site-nav-burger.site-nav-burger:focus {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.code-block-copy.code-block-copy:hover,
.code-block-copy.code-block-copy:focus {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-strong);
}

/* Elementor's global kit paints every heading widget in the signal colour. That
   is the accent, not a heading colour: it turned the home h1 and the review
   shelf headings entirely green, which erased the whole point of the single
   signal word inside them. Headings take the reading colour; components that
   want signal set it on the span themselves.

   Elementor's own rule is `.elementor-widget-heading .elementor-heading-title`
   at 0,2,0, so qualifying by tag lifts these to 0,2,1. Heading widgets rendered
   as `p` are deliberately left alone: they are used as eyebrows. */
.elementor-widget-heading h1.elementor-heading-title,
.elementor-widget-heading h2.elementor-heading-title,
.elementor-widget-heading h3.elementor-heading-title,
.elementor-widget-heading h4.elementor-heading-title,
.elementor-widget-heading h5.elementor-heading-title,
.elementor-widget-heading h6.elementor-heading-title {
  color: var(--text-primary);
}
