/* =========================================================================
   xSERVA — motion states
   =========================================================================

   The CSS half of the motion system. GSAP owns every animation; this file owns
   only the states GSAP animates from and to, plus the handful of ambient
   effects that are genuinely cheaper as CSS than as tweens.

   THE VISIBILITY CONTRACT, restated because breaking it is expensive:
   nothing here hides content except under `html.js`, which boot.js adds
   synchronously and removes again if motion never comes up. With JavaScript
   off, blocked, or failed, every rule below is inert and the page is plain,
   complete and readable. See the header of boot.js for the full reasoning.
   ========================================================================= */

/* -------------------------------------------------------------- reveals -- */
/* The resting hidden state. GSAP re-declares this inline the moment it runs;
   the rule exists to cover the gap between first paint and motion.js
   executing, which is exactly where the flash-then-jump used to happen. */
.js [data-reveal] {
  opacity: 0;
  visibility: hidden;
}

/* The handoff back from GSAP. motion.js adds this class when an entrance
   finishes and then strips the inline styles it wrote — without a visible
   state to fall back to, stripping those styles re-applies the rule above and
   the section disappears again the moment it has finished arriving. See the
   note in motion.js §3. */
.js [data-reveal].is-in {
  opacity: 1;
  visibility: visible;
}

/* There used to be a rule here forcing every [data-reveal] visible under
   `prefers-reduced-motion`, on the assumption that reduced motion meant no
   reveal at all. It does not any more: reduced mode still fades content in,
   it just does not move it (see the header of boot.js). An !important
   opacity:1 would fight that fade and win, so the rule is gone. The safety it
   provided is unchanged — `.js` is only ever on the page when JavaScript is
   running to take the content back off again. */

/* ---------------------------------------------------------- split lines -- */
/* SplitText writes these wrappers. The mask is what makes a line rise out of
   nothing rather than slide across whatever sits above it, and it must clip on
   the block axis only — clipping both axes shears the descenders and the
   overshoot on an italic or a rounded glyph. */
.split-line { overflow: hidden; padding-block: 0.06em; }
[data-split] { text-wrap: balance; }

/* --------------------------------------------------------------- hero ---- */
/* The hero is the one section allowed to be taller than the standard rhythm.
   min-height rather than a fixed height so a long headline in a narrow window
   pushes the section down instead of overflowing it. svh, not vh: on mobile
   Safari and Chrome, vh is the height with the URL bar hidden, so a 100vh hero
   is always taller than the screen on first paint and the CTA sits below the
   fold on exactly the devices where that matters most. */
.hero {
  position: relative;
  min-height: min(88svh, 900px);
  display: flex;
  align-items: center;
  padding-block: calc(var(--space-6) + var(--space-3)) var(--space-6);
}
.hero-copy > * { max-width: 62ch; }
.hero-copy .cluster { margin-top: var(--space-4); }
.hero-copy .lead { margin-top: var(--space-3); }

/* The hero photograph. Framed rather than bled: a raised panel with a bright
   top rim reads as a piece of the interface, where a full-bleed photo reads as
   stock imagery behind the words. */
.hero-visual .photo-figure { position: relative; }
.hero-visual .photo {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-xl);
  border: 1px solid var(--glass-border-2);
  box-shadow: var(--shadow-lg);
}
.hero-visual .photo-figure::after {
  height: 45%;
  border-radius: 0 0 var(--radius-xl) var(--radius-xl);
  background: linear-gradient(to bottom, transparent, rgba(5, 6, 10, 0.5));
}
/* The accent bloom behind the frame, which is what stops a rectangular photo
   from looking pasted onto the page. Blurred, static, and behind everything. */
.hero-visual::before {
  content: '';
  position: absolute;
  inset: 8% -6% -6% 8%;
  z-index: -1;
  border-radius: var(--radius-xl);
  background: linear-gradient(135deg, rgba(79, 209, 255, 0.3), rgba(139, 107, 255, 0.22));
  filter: blur(60px);
  opacity: 0.55;
  pointer-events: none;
}
.hero-visual { position: relative; }

@media (max-width: 899px) {
  .hero { min-height: 0; padding-block: var(--space-5) var(--space-6); }
  .hero-visual { margin-top: var(--space-4); }
}
.hero-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 130px), 1fr));
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--glass-border);
}
.hero-stats p { font-size: 0.88rem; color: var(--text-muted); margin-top: 2px; }

.stat-counter {
  display: block;
  font-size: clamp(2rem, 4vw, 2.9rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  background: var(--gradient-accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-canvas { width: 100%; height: 360px; display: block; }
/* will-change only where something actually animates for the length of a
   scroll. Setting it site-wide "just in case" promotes layers the compositor
   then has to keep around, which costs memory on exactly the low-end phones it
   was meant to help. */
.hero-visual { will-change: transform; }

/* The scanning grid behind a hero. Static geometry, masked so it fades out
   before it reaches the copy — a grid running under text at full strength is
   the single fastest way to make body copy hard to read. */
.scan-grid { position: relative; isolation: isolate; }
.scan-grid::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(79, 209, 255, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(79, 209, 255, 0.055) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 75% 65% at 50% 40%, #000 35%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 75% 65% at 50% 40%, #000 35%, transparent 78%);
}

/* ------------------------------------------------------- flow diagram ---- */
.flow-diagram-wrap { margin-top: var(--space-4); }
.flow-diagram {
  width: 100%;
  max-width: 900px;
  height: auto;
  display: block;
  margin-inline: auto;
  overflow: visible;
}

.flow-line { fill: none; stroke: var(--glass-border-2); stroke-width: 1.5; }
.flow-node circle { fill: var(--surface-2); stroke: var(--glass-border-2); stroke-width: 1.5; }
.flow-node.flow-hub circle {
  fill: var(--surface-2);
  stroke: var(--accent);
  stroke-width: 1.5;
  filter: drop-shadow(0 0 14px rgba(79, 209, 255, 0.35));
}
.flow-node-icon { color: var(--accent); width: 32px; height: 32px; }
.flow-node-icon svg { width: 100%; height: 100%; display: block; }

.flow-label {
  text-anchor: middle;
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  fill: var(--text-muted);
}
.flow-hub-label { fill: var(--text-primary); font-weight: 660; font-size: 15px; }

/* The travelling dots. offset-path is doing real work here — the alternative
   is seven keyframed translate sequences that have to be rewritten by hand
   every time a curve moves. motion.js holds the group hidden until the lines
   have finished drawing, so they never run along a path that is not there yet. */
.flow-dot {
  fill: var(--accent);
  filter: drop-shadow(0 0 5px var(--accent));
  offset-rotate: 0deg;
  animation: flow-dot-move 3.5s linear infinite;
}
.flow-dot-in1  { offset-path: path('M90,70 C270,70 300,150 450,205'); }
.flow-dot-in2  { offset-path: path('M90,160 C250,160 320,180 450,205'); }
.flow-dot-in3  { offset-path: path('M90,250 C250,250 320,230 450,205'); }
.flow-dot-in4  { offset-path: path('M90,340 C270,340 300,260 450,205'); }
.flow-dot-out1 { offset-path: path('M450,205 C600,205 650,120 810,100'); }
.flow-dot-out2 { offset-path: path('M450,205 C600,205 650,205 810,205'); }
.flow-dot-out3 { offset-path: path('M450,205 C600,205 650,290 810,310'); }

@keyframes flow-dot-move {
  0%   { offset-distance: 0%;   opacity: 0; }
  10%  { opacity: 1; }
  90%  { opacity: 1; }
  100% { offset-distance: 100%; opacity: 0; }
}

/* Travelling dots ARE objects moving across the reader's field of view, so
   they stop in reduced mode. Keyed off the resolved mode published by boot.js
   rather than off the media query directly — a media query cannot be
   overridden by the Motion control in the footer, and an attribute can. The
   media query is kept below purely as the no-JavaScript fallback, where no
   attribute is ever set. */
:root[data-motion="reduced"] .flow-dots { display: none; }
:root[data-motion="reduced"] .flow-dot { animation: none; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .flow-dots { display: none; }
  :root:not([data-motion="full"]) .flow-dot { animation: none; }
}

/* --------------------------------------------------- feature mini-art ---- */
.mini-diagram {
  width: 100%;
  max-width: 200px;
  height: auto;
  max-height: 60px;
  display: block;
  margin-top: var(--space-2);
  overflow: visible;
}
/* Overrides the generic 30px .feature-card svg sizing (specificity 0,2,2 against 0,2,1). */
.feature-card svg.mini-diagram {
  width: 100%;
  height: auto;
  max-width: 200px;
  margin-top: var(--space-2);
  margin-bottom: 0;
}

.rule-flow-line { stroke: var(--glass-border-2); stroke-width: 1.5; }
.rule-flow-node circle { fill: var(--surface-2); stroke: var(--glass-border-2); stroke-width: 1.5; }
.rule-flow-node text { fill: var(--text-muted); font-family: var(--font-sans); font-size: 9px; text-anchor: middle; }

.rule-flow-node.pulse-trigger circle   { animation: pulse-sequence 2.4s ease-in-out infinite; }
.rule-flow-node.pulse-condition circle { animation: pulse-sequence 2.4s ease-in-out infinite 0.6s; }
.rule-flow-node.pulse-action circle    { animation: pulse-sequence 2.4s ease-in-out infinite 1.2s; }

@keyframes pulse-sequence {
  0%, 75%, 100% { stroke: var(--glass-border-2); filter: none; }
  8%            { stroke: var(--accent); filter: drop-shadow(0 0 6px var(--accent)); }
  25%           { stroke: var(--glass-border-2); filter: none; }
}

.chart-mini-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 220;
  stroke-dashoffset: 220;
  animation: chart-line-draw 2.4s ease-in-out infinite;
}
@keyframes chart-line-draw {
  0%        { stroke-dashoffset: 220; }
  60%, 100% { stroke-dashoffset: 0; }
}

.chart-live-dot {
  fill: var(--accent);
  animation: chart-live-pulse 1.6s ease-in-out infinite;
  transform-origin: center;
  transform-box: fill-box;
}
@keyframes chart-live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.4; transform: scale(1.5); }
}

/* The rule-engine pulse and the live dot both scale, so they stop; the trend
   line only changes its own length, but it loops forever, and a permanent
   loop is worth stopping either way. */
:root[data-motion="reduced"] .rule-flow-node circle { animation: none !important; }
:root[data-motion="reduced"] .chart-mini-line { animation: none; stroke-dashoffset: 0; }
:root[data-motion="reduced"] .chart-live-dot { animation: none; opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) .rule-flow-node circle { animation: none !important; }
  :root:not([data-motion="full"]) .chart-mini-line { animation: none; stroke-dashoffset: 0; }
  :root:not([data-motion="full"]) .chart-live-dot { animation: none; opacity: 1; transform: none; }
}
