/* ============================================
   ANIMATIONS.CSS — keyframes / scroll entrances / hover
   KUNLUN BIO — ORVENNE-1
   FICTIONAL DEMO. NOT A REAL PRODUCT.
   ============================================ */

/* ============================================
   Scroll entrance states

   Every start state below is scoped to .js-reveal, which app.js
   adds to <html> only once it has an observer ready to take it off
   again. The reason is compliance, not neatness: the fiction band
   on the partner screen, the fabricated-figures notice on the data
   screen and the whole About This Page block are .reveal elements,
   while the product headline is not. An unscoped opacity:0 means a
   blocked script leaves the claim on screen and the disclaimers
   invisible. Without JS nothing here applies and the page is
   simply, statically, all there.
   ============================================ */
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.js-reveal .reveal.is-in { opacity: 1; transform: translateY(0); }

.js-reveal .reveal-l {
  opacity: 0;
  transform: translateX(-38px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.js-reveal .reveal-l.is-in { opacity: 1; transform: translateX(0); }

.js-reveal .reveal-r {
  opacity: 0;
  transform: translateX(38px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.js-reveal .reveal-r.is-in { opacity: 1; transform: translateX(0); }

.js-reveal .reveal-s {
  opacity: 0;
  transform: scale(0.96);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.js-reveal .reveal-s.is-in { opacity: 1; transform: scale(1); }

/* Clip-up: reserved for section titles. Reads as typesetting,
   not as a fade. */
.js-reveal .reveal-clip {
  clip-path: inset(0 0 100% 0);
  transform: translateY(14px);
  opacity: 0;
  transition: clip-path 1s var(--ease),
              transform 1s var(--ease),
              opacity 0.6s var(--ease);
}

/* Clipped titles reveal from their own observer entry OR from any
   revealed ancestor. The ancestor rule is the one that matters:
   an element whose own clip-path has collapsed it reports no
   intersection area, so it never fires on its own. */
.js-reveal .reveal-clip.is-in,
.js-reveal .is-in .reveal-clip {
  clip-path: inset(0 0 0 0);
  transform: translateY(0);
  opacity: 1;
}

.stagger > *:nth-child(1)  { transition-delay: 0ms; }
.stagger > *:nth-child(2)  { transition-delay: 80ms; }
.stagger > *:nth-child(3)  { transition-delay: 160ms; }
.stagger > *:nth-child(4)  { transition-delay: 240ms; }
.stagger > *:nth-child(5)  { transition-delay: 320ms; }
.stagger > *:nth-child(6)  { transition-delay: 400ms; }
.stagger > *:nth-child(7)  { transition-delay: 480ms; }
.stagger > *:nth-child(8)  { transition-delay: 560ms; }

/* Fade images in after decode so nothing flashes white */
.js-imgfade .section-photo__img,
.js-imgfade .media__img,
.js-imgfade .hero__photo-img {
  opacity: 0;
  transition: opacity 0.9s var(--ease);
}

.js-imgfade .section-photo__img.is-loaded,
.js-imgfade .media__img.is-loaded,
.js-imgfade .hero__photo-img.is-loaded { opacity: 1; }

/* ============================================
   SVG line drawing
   Every schematic path carries .draw and a
   --len custom property matching its own length.
   ============================================ */
/* Scoped to .js-reveal for the same reason as the entrance states:
   the inline --len on each path means an unscoped rule hides the
   whole schematic when the script does not arrive. Off that class
   the diagrams are just drawn. */
.js-reveal .draw {
  stroke-dasharray: var(--len, none);
  stroke-dashoffset: var(--len, 0);
  transition: stroke-dashoffset 1.7s var(--ease);
}

.js-reveal .is-in .draw,
.js-reveal .draw.is-in { stroke-dashoffset: 0; }

/* Stagger the strokes so a diagram assembles rather than blinks */
.is-in .draw--d1 { transition-delay: 0.10s; }
.is-in .draw--d2 { transition-delay: 0.28s; }
.is-in .draw--d3 { transition-delay: 0.46s; }
.is-in .draw--d4 { transition-delay: 0.64s; }
.is-in .draw--d5 { transition-delay: 0.82s; }
.is-in .draw--d6 { transition-delay: 1.00s; }

/* Fills, labels and dots arrive after their lines are drawn.
   Scoped like the strokes: several of these are the FICTIONAL DEMO
   DATA legends sitting inside the artwork. */
.js-reveal .pop {
  opacity: 0;
  transition: opacity 0.7s var(--ease);
}

.js-reveal .is-in .pop { opacity: 1; }
.is-in .pop--d1 { transition-delay: 0.55s; }
.is-in .pop--d2 { transition-delay: 0.80s; }
.is-in .pop--d3 { transition-delay: 1.05s; }
.is-in .pop--d4 { transition-delay: 1.30s; }
.is-in .pop--d5 { transition-delay: 1.55s; }

/* The comparison curve fills its delta band last of all */
.js-reveal .is-in .band { transition: opacity 0.9s 1.1s var(--ease); opacity: 1; }
.js-reveal .band { opacity: 0; }

/* ============================================
   Keyframes
   ============================================ */
@keyframes fadeUp   { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeDown { from { opacity: 0; transform: translateY(-14px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn   { from { opacity: 0; } to { opacity: 1; } }

@keyframes titleRise {
  0%   { opacity: 0; transform: translateY(26px); clip-path: inset(0 0 100% 0); }
  100% { opacity: 1; transform: translateY(0);    clip-path: inset(0 0 -10% 0); }
}

/* Cell breathing: the mark's glow */
@keyframes bioPulse {
  0%, 100% { box-shadow: 0 0 14px var(--glow-32), inset 0 0 10px var(--glow-16); }
  50%      { box-shadow: 0 0 26px var(--glow-32), inset 0 0 18px var(--glow-32); }
}

@keyframes dotPulse {
  0%, 100% { transform: scale(1);    opacity: 1; }
  50%      { transform: scale(0.72); opacity: 0.72; }
}

@keyframes ringOut {
  0%   { transform: scale(0.85); opacity: 0.7; }
  100% { transform: scale(1.45); opacity: 0; }
}

@keyframes scanSweep {
  0%   { transform: translateY(-110%); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateY(110%);  opacity: 0; }
}

@keyframes lineGrowX { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@keyframes scrollHint {
  0%   { transform: translateY(-100%); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: translateY(100%);  opacity: 0; }
}

/* Top banner arrives once, flashes once, then behaves */
@keyframes noticeAttention { 0% { transform: translateY(-100%); } 100% { transform: translateY(0); } }

@keyframes noticeGlow {
  0%, 100% { box-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); }
  35%      { box-shadow: 0 2px 30px rgba(255, 209, 102, 0.42); }
}

/* Instrument sampling flicker on the hero readings */
@keyframes sampleBlink {
  0%, 92%, 100% { opacity: 1; }
  95%           { opacity: 0.35; }
}

/* Slow travelling dash: used on the mechanism rail */
@keyframes railFlow { to { stroke-dashoffset: -220; } }

@keyframes heroScanDrift { from { background-position: 0 0; } to { background-position: 0 200px; } }

/* ============================================
   Standing motion
   ============================================ */
.notice {
  animation: noticeAttention 0.7s 0.1s var(--ease) backwards,
             noticeGlow 1.6s 0.9s var(--ease-io) 2;
}

.nav__mark { animation: bioPulse 3.6s var(--ease-io) infinite; }
.nav__mark::before { animation: dotPulse 3.6s var(--ease-io) infinite; }

.nav__fiction::before,
.notice__icon,
.fict-band__icon { animation: dotPulse 2.4s var(--ease-io) infinite; }

/* Hero cascade */
.hero__eyebrow { animation: fadeDown 0.9s 0.25s var(--ease) backwards; }
.hero__title   { animation: titleRise 1.15s 0.4s var(--ease) backwards; }
.hero__lede    { animation: fadeUp 1s 0.65s var(--ease) backwards; }
.hero__actions { animation: fadeUp 1s 0.78s var(--ease) backwards; }
.hero__specimen { animation: fadeUp 1s 0.95s var(--ease) backwards; }
.hero__structure { animation: fadeIn 1.4s 0.5s var(--ease) backwards; }
.hero__stats   { animation: fadeUp 1.1s 1.05s var(--ease) backwards; }
.hero__scroll  { animation: fadeIn 1s 1.5s var(--ease) backwards; }

.hero__scroll-line { position: relative; overflow: hidden; }

.hero__scroll-line::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--jade-soft);
  box-shadow: 0 0 8px var(--jade);
  animation: scrollHint 2.4s var(--ease-io) infinite;
}

.hero__scan { background-position: 0 0; animation: heroScanDrift 22s linear infinite; }

.hero__stat-value { animation: sampleBlink 7s var(--ease-io) infinite; }
.hero__stat:nth-child(2) .hero__stat-value { animation-delay: 1.6s; }

/* Mechanism rail: the transport dashes keep moving after the draw */
.rail__flow {
  stroke-dasharray: 3 9;
  animation: railFlow 7s linear infinite;
}

.rail::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 34%;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, rgba(61, 220, 151, 0.05), transparent);
  animation: scanSweep 6.5s var(--ease-io) infinite;
}

/* Card sheen: one cold pass on hover */
.sheen {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(112deg,
    transparent 36%,
    rgba(127, 240, 192, 0.10) 48%,
    rgba(127, 240, 192, 0.02) 55%,
    transparent 68%);
  opacity: 0;
  transform: translateX(-115%);
  transition: transform 1.05s var(--ease), opacity 0.35s var(--ease);
}

.voice:hover .sheen,
.form-card:hover .sheen,
.kpi:hover .sheen,
.product__shot:hover .sheen {
  opacity: 1;
  transform: translateX(115%);
}

/* Section divider grows in */
.section + .section::before {
  transform: translateX(-50%) scaleX(0);
  transform-origin: 50% 50%;
  transition: transform 1.2s var(--ease);
}

.section.is-in + .section::before,
.section + .section.is-in::before { transform: translateX(-50%) scaleX(1); }

/* Phase rule grows out of the timeline */
.phase { position: relative; }

.phase::before {
  content: '';
  position: absolute;
  top: -2px;
  left: 0;
  height: 2px;
  width: 0;
  background: var(--jade);
  box-shadow: 0 0 12px var(--glow-32);
  transition: width 1.1s var(--ease);
}

.phase.is-in::before { width: 100%; }
.phase:nth-child(2).is-in::before { transition-delay: 0.18s; }
.phase:nth-child(3).is-in::before { transition-delay: 0.36s; }

/* About This Page: brightens on entry so it cannot be skimmed past */
.about-page { transition: box-shadow var(--t-slow) var(--ease), border-color var(--t-slow) var(--ease); }

.about-page.is-in {
  border-color: var(--warn);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5),
              inset 0 1px 0 rgba(255, 209, 102, 0.2),
              0 0 60px rgba(255, 209, 102, 0.14);
}

.btn:active { transform: translateY(0) scale(0.985); }

.spec tr,
.lexicon tbody tr { transition: background var(--t-fast) var(--ease); }
.spec tr:hover { background: rgba(61, 220, 151, 0.045); }

/* ============================================
   Touch / coarse pointer
   ============================================ */
@media (hover: none), (pointer: coarse) {
  .sheen { display: none; }
  .rail::after { animation-duration: 9s; }
}

/* ============================================
   Reduced motion
   ============================================ */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .reveal, .reveal-l, .reveal-r, .reveal-s, .reveal-clip {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: none;
  }

  .draw { stroke-dashoffset: 0; transition: none; }
  .pop, .band { opacity: 1; transition: none; }
  .phase::before { width: 100%; transition: none; }

  .js-imgfade .section-photo__img,
  .js-imgfade .media__img,
  .js-imgfade .hero__photo-img { opacity: 1; transition: none; }

  .sheen,
  .hero__scan,
  .rail::after,
  .rail__flow,
  .hero__scroll-line::after { display: none; }

  .hero__photo-img,
  .section-photo__img { transform: none !important; will-change: auto; }

  .section + .section::before { transform: translateX(-50%) scaleX(1); }

  /* The banner never animates away and must stay visible */
  .notice { animation: none; transform: translateY(0); }
}

/* ============================================
   ============================================
   INTERACTION LAYER
   Driven by js/interactions.js. Everything below is additive:
   with that script absent none of these classes are ever applied
   and the page above is unchanged.

   Two surfaces are kept out of all of it on purpose — the amber
   fiction markers (banner, nav badge, fict-band, About This Page)
   and the plotted data. A disclaimer that leans toward the cursor
   reads as decoration, and a chart that does is worse.
   ============================================
   ============================================ */

/* ============================================
   New keyframes
   ============================================ */

/* The stylus halo, while a line is being drawn under it */
@keyframes stylusRing {
  0%, 100% { opacity: 0.9; transform: scale(0.84); }
  50%      { opacity: 0.28; transform: scale(1.2); }
}

/* Cursor ring arrival — plays once, on the first pointer move */
@keyframes cursorWake {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ============================================
   Scroll progress — sharing the top edge with the banner
   The bar rides the underside of the fiction banner while the
   banner is up, and takes the top of the viewport only once the
   banner has retracted. Same duration and curve as the banner and
   the nav, so the three move as one object. It never covers the
   disclaimer, its amber rule, or its close button.
   ============================================ */
.progress {
  top: var(--notice-h);
  transition: top var(--t-med) var(--ease);
}

body.is-notice-hidden .progress { top: 0; }

/* ============================================
   1. Custom cursor
   ============================================ */
.cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 2000;
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid var(--jade);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  will-change: transform;
  transition: opacity 0.3s var(--ease),
              border-color 0.25s var(--ease),
              background-color 0.25s var(--ease),
              box-shadow 0.25s var(--ease);
}

.cursor.is-on { opacity: 1; animation: cursorWake 0.35s var(--ease) backwards; }

.cursor.is-link {
  border-color: var(--jade-soft);
  background-color: rgba(61, 220, 151, 0.10);
  box-shadow: 0 0 22px var(--glow-16);
}

.cursor.is-diagram {
  border-style: dashed;
  border-color: var(--jade-soft);
  background-color: transparent;
}

.cursor__glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1;
  color: var(--jade-soft);
  opacity: 0;
  /* counter-scaled by JS so the glyph stays one size as the ring grows */
  transform: scale(var(--glyph-s, 1));
  transition: opacity 0.2s var(--ease);
}

.cursor.is-jump .cursor__glyph,
.cursor.is-diagram .cursor__glyph { opacity: 1; }

/* The hard dot keeps the true position. The ring is allowed to lag;
   this is not. */
.cursor-dot {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 2001;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: var(--jade-soft);
  box-shadow: 0 0 10px var(--glow-32);
  pointer-events: none;
  opacity: 0;
  will-change: transform;
  transition: opacity 0.3s var(--ease);
}

.cursor-dot.is-on { opacity: 1; }

/* Hiding the native cursor, with one exception that matters:
   this page is four thousand words long, and taking the caret away
   over running text to protect a decoration is the wrong trade. */
html.js-cursor,
html.js-cursor body { cursor: none; }

html.js-cursor p,
html.js-cursor li,
html.js-cursor td,
html.js-cursor th,
html.js-cursor dt,
html.js-cursor dd,
html.js-cursor h1,
html.js-cursor h2,
html.js-cursor h3,
html.js-cursor h4,
html.js-cursor caption,
html.js-cursor figcaption,
html.js-cursor blockquote { cursor: text; }

/* Anything clickable goes back to none — must stay after the block
   above, since a link inside a list item would otherwise win. */
html.js-cursor a,
html.js-cursor button,
html.js-cursor summary,
html.js-cursor label,
html.js-cursor canvas,
html.js-cursor .nav__toggle { cursor: none; }

/* ============================================
   5. Pointer spotlight
   A fixed-size disc that is translated, never repainted — the
   gradient is baked once and the element rides the compositor.
   Low enough to read as the room lighting rather than as a beam.
   ============================================ */
.spotlight {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 2;
  width: 1100px;
  height: 1100px;
  margin: -550px 0 0 -550px;
  pointer-events: none;
  opacity: 0;
  will-change: transform;
  background: radial-gradient(circle closest-side,
    rgba(61, 220, 151, 0.062) 0%,
    rgba(61, 220, 151, 0.024) 42%,
    rgba(61, 220, 151, 0) 72%);
  transition: opacity 0.8s var(--ease);
}

.spotlight.is-on { opacity: 1; }

/* ============================================
   2. Card tilt and the glint that follows the pointer
   Tilt is for the numeric tiles only. The quote cards get the
   light and keep their geometry: they hold a paragraph each, and
   text that leans is text that is harder to read.

   The transform is assembled from custom properties so that one
   declaration can serve the scroll entrance, the hover lift and
   the tilt without any of the three overwriting the others.
   ============================================ */
.tilt.is-in {
  transform: perspective(760px)
             translateY(var(--tilt-lift, 0px))
             rotateX(var(--tilt-rx, 0deg))
             rotateY(var(--tilt-ry, 0deg));
  /* Longhands, not the shorthand. Both of these grids are .stagger
     containers, and the shorthand would reset the nth-child
     transition-delay that gives them their entrance rhythm.
     The rest restates the card's own transitions from
     components.css, so its hover states survive having transform
     re-timed underneath them. */
  transition-property: transform, opacity, border-color, box-shadow, background;
  transition-duration: var(--tilt-t, var(--t-slow)), var(--t-slow),
                       var(--t-med), var(--t-med), var(--t-med);
  transition-timing-function: var(--ease);
}

/* While the pointer is on the card, JS owns the easing frame by
   frame — including the stagger delay, which has done its job by
   then and would only show up as lag. Letting go hands both back. */
.tilt.is-live {
  --tilt-t: 0s;
  transition-delay: 0s;
  will-change: transform;
}

.kpi.tilt:hover       { --tilt-lift: -2px; }
.form-card.tilt:hover { --tilt-lift: -3px; }

.glint::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;                       /* under .sheen, which is 5 */
  pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(220px circle at var(--hl-x, 50%) var(--hl-y, 50%),
    rgba(127, 240, 192, 0.13) 0%,
    rgba(61, 220, 151, 0.045) 42%,
    transparent 68%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

.glint.is-live::before { opacity: 1; }

/* ============================================
   4. Magnetic buttons
   The offset lives in a variable so the hover lift and the active
   press can both keep it. Not applied to the banner's close
   button: that control has to be exactly where it looks.
   ============================================ */
.btn,
.nav__cta { transform: translate3d(var(--mx, 0px), var(--my, 0px), 0); }

.nav__cta {
  transition: background var(--t-med) var(--ease),
              color var(--t-med) var(--ease),
              box-shadow var(--t-med) var(--ease),
              transform var(--t-fast) var(--ease);
}

.btn--solid:hover,
.btn--ghost:hover {
  transform: translate3d(var(--mx, 0px), calc(var(--my, 0px) - 2px), 0);
}

.btn:active {
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0) scale(0.985);
}

/* ============================================
   3. Photographs under the pointer
   The image comes up and its scrim comes up with it, so a caption
   never loses contrast against a brightening photograph. The
   background plates are left alone — app.js already owns those.
   ============================================ */
.media__scrim::after,
.micro__scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.7s var(--ease);
}

.media__scrim::after {
  background: linear-gradient(to top,
    rgba(7, 16, 13, 0.9) 0%,
    rgba(7, 16, 13, 0.46) 30%,
    rgba(7, 16, 13, 0) 66%);
}

.micro__scrim::after {
  background: radial-gradient(62% 62% at 50% 46%,
    rgba(7, 16, 13, 0) 0%,
    rgba(7, 16, 13, 0.44) 62%,
    rgba(7, 16, 13, 0.84) 100%);
}

.product__shot:hover .media__scrim::after,
.micro:hover .micro__scrim::after { opacity: 1; }

.micro img { transition: transform 1.3s var(--ease), filter 0.8s var(--ease); }
.micro__tint { transition: opacity var(--t-med) var(--ease); }

.micro:hover img {
  transform: scale(1.09);
  filter: contrast(1.06) brightness(1.02) saturate(1.02) hue-rotate(-8deg);
}

.micro:hover .micro__tint { opacity: 0.4; }

.foil__img { transition: transform 1.4s var(--ease), filter 0.9s var(--ease); }

.foil:hover .foil__img {
  transform: scale(1.34);
  filter: grayscale(1) blur(3.4px) contrast(1.4) brightness(0.62);
}

/* ============================================
   8. Data visualisation entrances
   The .draw / .pop system above already assembles the schematics.
   These are the pieces interactions.js adds on top of it so the
   diagrams read as being drawn rather than as fading up.
   ============================================ */

/* The chart lays down its frame before it plots anything */
.draw-grid {
  stroke-dasharray: var(--len, none);
  stroke-dashoffset: var(--len, 0);
  transition: stroke-dashoffset 0.7s var(--ease);
}

.is-in .draw-grid,
.draw-grid.is-in { stroke-dashoffset: 0; }

.grid-fade {
  opacity: 0;
  transition: opacity 0.6s 0.24s var(--ease);
}

.is-in .grid-fade { opacity: 1; }

/* Fig. 1: the bars are an argument about length, so they grow to
   it. The write-back block grows rightward out of the threshold
   line, which is exactly what the caption says happens. */
.grow-x {
  transform-box: fill-box;
  transform-origin: 0% 50%;
  transform: scaleX(0.025);
  transition: opacity 0.7s var(--ease), transform 1s var(--ease);
}

.is-in .grow-x { transform: scaleX(1); }

/* The stylus that rides the tip of a line while it is drawn */
.stylus { pointer-events: none; }

.stylus__tip { fill: var(--jade-soft); }

.stylus__halo {
  fill: none;
  stroke: rgba(127, 240, 192, 0.5);
  stroke-width: 1;
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: stylusRing 0.9s var(--ease-io) infinite;
}

/* ============================================
   9. Schematics turn with the pointer
   On the drawing, not on the panel: the frame, the caption and the
   fiction note underneath it all stay flat and square.
   ============================================ */
.schem svg { transition: transform 0.7s var(--ease); }
.schem.is-live svg { transition: transform 0s; will-change: transform; }

/* ============================================
   10. Sequences that print rather than appear
   Register rows and the roster. Card grids already stagger from
   .stagger above and are left alone.
   ============================================ */
.rise { opacity: 0; transform: translateY(9px); }
.rise.is-in { opacity: 1; transform: none; }

.ledger tbody tr.rise,
.crew__row.rise {
  transition: opacity 0.65s var(--ease),
              transform 0.65s var(--ease),
              background var(--t-med) var(--ease);
}

/* ============================================
   Touch / coarse pointer — everything above that needs a pointer
   comes off. interactions.js does not install most of it in the
   first place; this is the belt to that pair of braces, for hybrid
   devices that report both.
   ============================================ */
@media (hover: none), (pointer: coarse) {
  .cursor,
  .cursor-dot,
  .spotlight { display: none; }

  .glint::before { display: none; }
  .tilt.is-in { transform: none; }
  .schem svg { transform: none; }
  .btn, .nav__cta { --mx: 0px; --my: 0px; }

  /* The custom cursor hides the native one on the whole document.
     A touch device has neither, but a hybrid reports both, and a
     stylus or trackpad user on such a device would be left with an
     invisible pointer once .cursor is display:none above. */
  html.js-cursor,
  html.js-cursor body,
  html.js-cursor a,
  html.js-cursor button,
  html.js-cursor canvas { cursor: auto; }
}

/* On a phone the entrances come in from the side, which is 38px of
   horizontal travel on a 375px screen: it reads as a wobble, and
   body{overflow-x:hidden} is what keeps it from becoming a
   sideways scroll. The same reveals run vertically instead.

   Raised from 720 to the 1080 stop, with the rest of the tablet
   pass. `.wrap` runs edge to edge below 1268px, so a 38px lateral
   entrance has nowhere to travel: at 768 every un-revealed
   .reveal-r sat at x 62–782 against a 768 viewport, and
   documentElement.scrollWidth read 782. Clipped by the body, but
   still a page wider than the screen. Above 1080 the entrance
   stays lateral and the same 14px of clipped travel remains — the
   desktop composition is out of scope for this pass.

   Scoped to .js-reveal, which is what makes these rules bite at all:
   the start states they are overriding are `.js-reveal .reveal-l`,
   two classes, and a bare `.reveal-l` here lost the cascade to them
   at every width. The lateral entrance was still running on phones. */
@media (max-width: 1080px) {
  .js-reveal .reveal-l,
  .js-reveal .reveal-r { transform: translateY(24px); }

  .js-reveal .reveal-l.is-in,
  .js-reveal .reveal-r.is-in { transform: translateY(0); }
}

/* ============================================
   Reduced motion — no cursor, no spotlight, no tilt, no turn, no
   magnetism, no parallax. The diagrams and the chart frames are
   simply drawn, and every sequence is simply present.
   ============================================ */
@media (prefers-reduced-motion: reduce) {
  .cursor,
  .cursor-dot,
  .spotlight,
  .stylus { display: none; }

  html.js-cursor,
  html.js-cursor body,
  html.js-cursor a,
  html.js-cursor button,
  html.js-cursor canvas { cursor: auto; }

  .glint::before { display: none; }

  .tilt.is-in { transform: none; transition: none; }
  .schem svg { transform: none !important; transition: none; }

  /* Zeroing the offsets is enough: the buttons fall back to exactly
     the hover lift they had before any of this existed. */
  .btn, .nav__cta { --mx: 0px; --my: 0px; }

  .draw-grid { stroke-dashoffset: 0; transition: none; }
  .grid-fade { opacity: 1; transition: none; }
  .grow-x { transform: scaleX(1); transition: none; }

  .rise { opacity: 1; transform: none; transition: none; }

  .hero__inner > div,
  .hero__structure { transform: none !important; }

  .hero__scroll { opacity: 1 !important; }
}
