/* ============================================================================
   Mellivo — the stage (stage.html)

   Built from nothing rather than from a template: no nav, no hero, no
   sections, no footer bar. The page is a single window onto Mellivo at work,
   and three marks laid over it — the wordmark, one line, one address.

   Locked to near-black. This page is one thing in one light; a theme toggle
   would be a control with nothing to control.
   ========================================================================== */

:root {
  --bg: #0A0A0C;
  --ink: #FDFCFC;
  --ink-50: rgba(253, 252, 252, 0.5);
  --ink-70: rgba(253, 252, 252, 0.72);
  --olive: #6C7A3A;
  /* arrivals decelerate hard and settle; departures gather speed and leave */
  --ease-rise: cubic-bezier(0.16, 1, 0.30, 1);
  --ease-leave: cubic-bezier(0.55, 0, 0.25, 1);
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Consolas, monospace;
  --ease-soft: cubic-bezier(0.45, 0, 0.15, 1);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  overflow: hidden;                      /* one screen, never a scroll */
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.stage {
  position: relative;
  width: 100%;
  height: 100dvh;
  overflow: hidden;
  background: var(--bg);
}

/* ---------------- the loading screen ----------------
   The claw draws itself, holds, and whips away, in the direction the strokes
   are actually struck — so the mark reads as being made rather than as a
   spinner parked on the page. The real PNG is what gets revealed, so the logo
   shape is exact rather than traced or approximated. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  background: var(--bg);
  transition: opacity 620ms var(--ease-soft), visibility 0s linear 620ms;
}
.loader.is-done { opacity: 0; visibility: hidden; }

/* The mark is cut, not drawn. A hard edge runs square across the slash axis
   and sweeps the whole logo open in one pass, so the three claws part
   together — they are one paw, not three strokes taking turns.

   Revealing each claw separately along its own centreline was wrong twice
   over: the claws are pointed at BOTH ends and fattest in the middle, so
   "tip to tail" had no meaning, and a wide round-capped stroke pushes a
   bulging leading edge that reads as the shape being flooded with white
   rather than opened. A straight edge cuts. */
.loader__claw {
  display: block;
  width: clamp(96px, 10vw, 150px);
  height: auto;
}

/* Travel comes from the mark's own geometry. The group rotates about the
   origin, so local x is a straight projection onto the slash axis
   (0.8376x - 0.5463y) and the ink measures -191.6..344.6 along it. The rect
   starts at local x, so 350 parks its edge just clear of the top points and
   -200 carries it just past the bottom ones — no dead travel at either end.
   (Rotating about the mark's centre instead puts the local origin out at
   user -66,162, which is what silently broke the first attempt.) */
.cut {
  transform: translateX(350px);
  animation: slash 1.2s linear infinite;
}

/* No pause anywhere in the loop: the mark is cut, and it is already fading as
   the next strike comes. The strike itself stays ~290ms — the percentage is
   set against the cycle to keep that absolute speed. */
@keyframes slash {
  0%   { transform: translateX(350px);  opacity: 1;
         animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); }
  24%  { transform: translateX(-200px); opacity: 1; }   /* cut */
  100% { transform: translateX(-200px); opacity: 0; }   /* clears for the next */
}

/* Hand-over lands on an iteration boundary — the third slash has finished and
   faded, and the fourth has not cut yet. Just stop the loop there: dropping
   the animation falls back to the uncut rest state above, which is already
   invisible. Pinning it open instead would flash the mark back for a fourth
   appearance on the way out. */
.loader.is-done .cut { animation: none; }

/* ---------------- the window, in planes ----------------
   Not one flat picture but three at different depths, all driven by a single
   pointer position: --dx/--dy are normalised -1..1 and each plane scales them
   by its own --d, so the far mark barely stirs while Mellivo leads.

   This is response, not performance — nothing moves unless the visitor does —
   which is exactly why it survives reduced motion (see the block at the foot
   of this file) where an auto-playing loop would not. */
.hero {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  background: var(--bg);
}

.hero__l {
  position: absolute;
  display: block;
  will-change: transform;
  /* --cx is a layout nudge (used to centre on narrow screens) kept as a
     separate function so it composes with the parallax rather than
     replacing it — overriding `transform` wholesale would silently kill the
     movement for anyone with a pointer at a narrow width. */
  transform: translate3d(calc(var(--dx, 0) * var(--d, 0) * 1px),
                         calc(var(--dy, 0) * var(--d, 0) * 1px), 0)
             translateX(var(--cx, 0px));
}

/* ---------------- the contour lines ----------------
   Deepest plane, and the reason the ground stops reading as a void. Three
   groups, each drifting on its own long cycle in its own direction, so the
   field never repeats a configuration you can catch. Kept far below the
   copy in contrast — at these alphas they are felt rather than read, which
   is the whole point: texture, not decoration competing with the statement.

   Oversized and inset negatively so the drift never walks a line end into
   frame. */
.hero__lines {
  --d: 3;
  /* Three cuts, intersected, so the lines only ever live in open ground:

     1. clear of the statement — measured off the column itself, gutter plus
        its own width, rather than a guessed percentage. A fixed 30% held on a
        wide window and leaked straight onto the copy as soon as it narrowed;
     2. clear of Mellivo — a soft hole where he stands, so nothing is drawn
        across the animal;
     3. clear of the marks along the bottom.

     Inset is 0 on purpose. With a negative inset the mask percentages stop
     agreeing with the viewport and every offset here would need correcting
     for it; coverage comes from the paths overshooting the viewBox instead. */
  inset: 0;
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0 calc(var(--gutter) + var(--copyw) + 3vw),
                    #000 calc(var(--gutter) + var(--copyw) + 18vw)),
    radial-gradient(ellipse 34% 58% at 71.5% 55%, transparent 0 56%, #000 100%),
    linear-gradient(180deg, #000 0%, #000 64%, transparent 93%);
  -webkit-mask-composite: source-in, source-in, source-in;
          mask-image:
    linear-gradient(90deg, transparent 0 calc(var(--gutter) + var(--copyw) + 3vw),
                    #000 calc(var(--gutter) + var(--copyw) + 18vw)),
    radial-gradient(ellipse 34% 58% at 71.5% 55%, transparent 0 56%, #000 100%),
    linear-gradient(180deg, #000 0%, #000 64%, transparent 93%);
          mask-composite: intersect, intersect, intersect;
}
/* The lower-left field. Its own cuts, not a mirrored copy of the ones above:
   the obstacles down here are different — the statement ends around 70% of
   the height, and the status mark sits in the corner below. So this one fades
   in once the copy is past, holds the left of the frame, and clears out
   before it reaches the mark. */
.hero__lines--bl {
  --d: 4;
  /* The status mark gets a hole rather than a bottom-wide fade. Fading the
     whole bottom edge collapsed the field: the statement runs to ~76% of the
     height and the mark sits at ~92%, so a fade at each end left almost no
     band between them. Carving only where the words actually are gives the
     corner back. */
  -webkit-mask-image:
    linear-gradient(180deg, transparent 0 76%, #000 86%),
    linear-gradient(90deg, #000 0 28%, transparent 48%),
    radial-gradient(ellipse 19% 14% at 10% 95%, transparent 0 50%, #000 100%);
  -webkit-mask-composite: source-in, source-in, source-in;
          mask-image:
    linear-gradient(180deg, transparent 0 76%, #000 86%),
    linear-gradient(90deg, #000 0 28%, transparent 48%),
    radial-gradient(ellipse 19% 14% at 10% 95%, transparent 0 50%, #000 100%);
          mask-composite: intersect, intersect, intersect;
}
/* out of phase with the upper field, so the page never pulses as one */
.hero__lines--bl .ln-far  { animation-delay: -13s; }
.hero__lines--bl .ln-mid  { animation-delay: -8s; }
.hero__lines--bl .ln-near { animation-delay: -17s; }

.hero__lines svg { display: block; width: 100%; height: 100%; }
.hero__lines g { fill: none; will-change: transform; }
/* Alphas chosen against measured contrast, not by eye: 0.11/0.15/0.21 land at
   roughly 1.2, 1.4 and 1.65 against the #0A0A0C ground. Subtle is 1.2–1.7 —
   below about 1.1 a hairline simply is not there, whatever the DOM says. */
.ln-far  { stroke: rgba(253, 252, 252, 0.11); stroke-width: 1.1;  animation: drift-a 32s ease-in-out infinite; }
.ln-mid  { stroke: rgba(253, 252, 252, 0.15); stroke-width: 1.2;  animation: drift-b 26s ease-in-out infinite; }
.ln-near { stroke: rgba(253, 252, 252, 0.21); stroke-width: 1.45; animation: drift-c 21s ease-in-out infinite; }

/* Travel and tempo set so the drift is actually perceptible: roughly 3.8px a
   second. The previous pass moved 19px over 62s — 0.3px/s, which is moving in
   the arithmetic and stationary to the eye. If it cannot be seen it is not
   doing the job it was added for. */
@keyframes drift-a {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(-80px, 42px, 0); }
}
@keyframes drift-b {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(62px, -50px, 0); }
}
@keyframes drift-c {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(-48px, -34px, 0); }
}

/* the pool he stands in. It gathers as the pointer nears him, so the light
   appears to acknowledge the visitor before he does. */
.hero__glow {
  /* Held still with him — a pool of light sliding out from under a stationary
     figure is worse than either moving. It still gathers as you approach;
     that is a change in light, not a change in position. */
  --d: 0;
  right: 4%;
  bottom: 7%;
  width: min(74vh, 820px);
  height: min(74vh, 820px);
  opacity: calc(0.30 + 0.45 * var(--near, 0));
  background: radial-gradient(closest-side,
              rgba(150, 158, 120, 0.16) 0%,
              rgba(120, 128, 96, 0.07) 44%,
              transparent 74%);
}

/* nearest: Mellivo, in two registered layers occupying the same box. Both are
   the same drawing at the same size, so the reveal never shifts or rescales —
   the reduction was derived from the full art, which is what guarantees it. */
.hero__badger {
  /* No parallax on the figure. He is the subject, and sliding the subject
     around under the cursor reads as a trick rather than as depth — the
     reveal is the interaction, and it does not need help. The contour fields
     keep their few pixels of travel; drifting ground is depth, a drifting
     subject is a gimmick. */
  --d: 0;
  --grow: calc(var(--near, 0) * (1 + 0.42 * var(--pace, 0)));
  right: 13%;
  bottom: 13%;        /* clear of the address in the corner */
  /* Also vmin, and capped tighter than the type is. He held 640px from 1920
     upward while the type froze at 20.8px, so on a large monitor he kept the
     whole of his height while the words stopped growing — which is the
     imbalance, not his absolute size. Holding him near where he was and
     letting the statement carry on scaling closes it. */
  height: clamp(320px, 56vmin, 620px);
  width: auto;
}

/* What you get by default: the line, holding the dark.

   Held near full strength on purpose. Sitting it back at 0.62 was carried
   over from when this layer was the solid figure, where recession read as
   depth. As an outline it covers 8% of the box against the full drawing's
   65%, so the same 0.62 put a 2px line at #949189 on a #0A0A0C page — present
   in the DOM and invisible in the room. A line has no mass to spare. */
.hero__min {
  opacity: calc(0.92 + 0.08 * var(--near, 0));

  /* Cut the line out wherever the drawing is being revealed, instead of
     leaving both stacked. The two are not the same silhouette — the line was
     drawn separately from the painting — so with both present the line either
     pokes past the coloured edge or disappears under his black body, which is
     the outline appearing to turn dark along his back.

     Same six blobs as the reveal, subtracted from a full-coverage base, so
     this is exactly the inverse of what the drawing shows. One layer at a
     time, everywhere. */
  /* Order matters, and not in the direction it reads. `subtract` takes the
     LAYER MINUS THE LAYERS BELOW IT — not the other way round. Listing the
     blobs first and the base last therefore computed "blobs minus base",
     which at rest is transparent-minus-opaque: the line vanished entirely.

     Base first with subtract, blobs beneath it with add. Measured both ways
     by rasterising the construction and reading the alpha: this order gives
     255 everywhere at rest and a clean hole exactly under the reveal. */
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-image:
    linear-gradient(#000, #000),
    radial-gradient(ellipse calc(30% * var(--grow)) calc(26% * var(--grow)) at var(--b1x, 50%) var(--b1y, 50%), #000 78%, transparent 100%),
    radial-gradient(ellipse calc(25% * var(--grow)) calc(22% * var(--grow)) at var(--b2x, 50%) var(--b2y, 50%), #000 78%, transparent 100%),
    radial-gradient(ellipse calc(21% * var(--grow)) calc(18% * var(--grow)) at var(--b3x, 50%) var(--b3y, 50%), #000 76%, transparent 100%),
    radial-gradient(ellipse calc(17% * var(--grow)) calc(15% * var(--grow)) at var(--b4x, 50%) var(--b4y, 50%), #000 76%, transparent 100%),
    radial-gradient(ellipse calc(14% * var(--grow)) calc(12% * var(--grow)) at var(--b5x, 50%) var(--b5y, 50%), #000 74%, transparent 100%),
    radial-gradient(ellipse calc(11% * var(--grow)) calc(10% * var(--grow)) at var(--b6x, 50%) var(--b6y, 50%), #000 74%, transparent 100%);
  -webkit-mask-composite: destination-out, source-over, source-over,
                          source-over, source-over, source-over, source-over;
          mask-image:
    linear-gradient(#000, #000),
    radial-gradient(ellipse calc(30% * var(--grow)) calc(26% * var(--grow)) at var(--b1x, 50%) var(--b1y, 50%), #000 78%, transparent 100%),
    radial-gradient(ellipse calc(25% * var(--grow)) calc(22% * var(--grow)) at var(--b2x, 50%) var(--b2y, 50%), #000 78%, transparent 100%),
    radial-gradient(ellipse calc(21% * var(--grow)) calc(18% * var(--grow)) at var(--b3x, 50%) var(--b3y, 50%), #000 76%, transparent 100%),
    radial-gradient(ellipse calc(17% * var(--grow)) calc(15% * var(--grow)) at var(--b4x, 50%) var(--b4y, 50%), #000 76%, transparent 100%),
    radial-gradient(ellipse calc(14% * var(--grow)) calc(12% * var(--grow)) at var(--b5x, 50%) var(--b5y, 50%), #000 74%, transparent 100%),
    radial-gradient(ellipse calc(11% * var(--grow)) calc(10% * var(--grow)) at var(--b6x, 50%) var(--b6y, 50%), #000 74%, transparent 100%);
          mask-composite: subtract, add, add, add, add, add, add;
}

/* The drawing itself, revealed only under the pointer. The mask is three
   blobs, each trailing at its own rate (see stage.js), so their union
   stretches and lags rather than sliding as one disc — that difference in
   rates is what reads as viscosity. Radii scale with --near, so at rest the
   blobs collapse to nothing and the full layer is genuinely absent. */
/* The reference does this in a WebGL shader: a cursor-centred reveal whose
   edge is displaced by animated noise, with size driven by cursor pace. Its
   uniforms name the parts — uMouseCoords, REVEAL_SIZE, DISTORT_SCALE,
   NOISE_DETAIL, uTime, uMousePace.

   No shader here, so the same three properties are built from four masks:
     · they trail the pointer at different rates       (the lag)
     · each drifts on its own slow oscillation         (the churn — stands in
       for noise over time, so the edge never sits still even when you do)
     · all of them swell with pointer speed            (uMousePace)
   The union of four wobbling, differently-lagged blobs is what reads as
   something viscous rather than as a moving hole. */
/* the wrapper carries the displacement; the image inside carries the mask */
.hero__defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.hero__wet {
  aspect-ratio: 700 / 814;
  filter: url("#liquid");
}
.hero__wet .hero__full { display: block; width: 100%; height: 100%; }

/* --grow is declared once on .hero__badger and inherited by both layers, so
   the reveal and its inverse can never drift apart. */
.hero__full {
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-composite: source-over;
          mask-composite: add;
  /* Hard stops, not soft ones. At a 46% falloff the reveal read as a
     spotlight shone on him; carrying opacity to 84% before it releases gives
     the blob an actual boundary, and boundaries are what the displacement
     filter has something to bite on. Six of them, so the union is lumpy
     rather than round. */
  -webkit-mask-image:
    radial-gradient(ellipse calc(30% * var(--grow)) calc(26% * var(--grow)) at var(--b1x, 50%) var(--b1y, 50%), #000 84%, transparent 100%),
    radial-gradient(ellipse calc(25% * var(--grow)) calc(22% * var(--grow)) at var(--b2x, 50%) var(--b2y, 50%), #000 84%, transparent 100%),
    radial-gradient(ellipse calc(21% * var(--grow)) calc(18% * var(--grow)) at var(--b3x, 50%) var(--b3y, 50%), #000 82%, transparent 100%),
    radial-gradient(ellipse calc(17% * var(--grow)) calc(15% * var(--grow)) at var(--b4x, 50%) var(--b4y, 50%), #000 82%, transparent 100%),
    radial-gradient(ellipse calc(14% * var(--grow)) calc(12% * var(--grow)) at var(--b5x, 50%) var(--b5y, 50%), #000 80%, transparent 100%),
    radial-gradient(ellipse calc(11% * var(--grow)) calc(10% * var(--grow)) at var(--b6x, 50%) var(--b6y, 50%), #000 80%, transparent 100%);
          mask-image:
    radial-gradient(ellipse calc(30% * var(--grow)) calc(26% * var(--grow)) at var(--b1x, 50%) var(--b1y, 50%), #000 84%, transparent 100%),
    radial-gradient(ellipse calc(25% * var(--grow)) calc(22% * var(--grow)) at var(--b2x, 50%) var(--b2y, 50%), #000 84%, transparent 100%),
    radial-gradient(ellipse calc(21% * var(--grow)) calc(18% * var(--grow)) at var(--b3x, 50%) var(--b3y, 50%), #000 82%, transparent 100%),
    radial-gradient(ellipse calc(17% * var(--grow)) calc(15% * var(--grow)) at var(--b4x, 50%) var(--b4y, 50%), #000 82%, transparent 100%),
    radial-gradient(ellipse calc(14% * var(--grow)) calc(12% * var(--grow)) at var(--b5x, 50%) var(--b5y, 50%), #000 80%, transparent 100%),
    radial-gradient(ellipse calc(11% * var(--grow)) calc(10% * var(--grow)) at var(--b6x, 50%) var(--b6y, 50%), #000 80%, transparent 100%);
}

/* Without JS there is no pointer to answer, so the full drawing simply
   stands and the line layer steps aside. */
html:not(.js) .hero__min  { opacity: 0; }
html:not(.js) .hero__full { -webkit-mask-image: none; mask-image: none; }

/* Legibility without a slab: the image simply falls into the dark at the
   bottom and the corners, so the marks sit on ground rather than on a bar. */
.stage__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(10, 10, 12, 0.34) 0%, transparent 22%,
                    transparent 58%, rgba(10, 10, 12, 0.72) 92%, var(--bg) 100%),
    radial-gradient(120% 86% at 62% 46%, transparent 52%, rgba(10, 10, 12, 0.55) 100%);
}

/* the same whisper of material the rest of the brand carries */
.stage__grain {
  position: absolute;
  inset: -50%;
  width: 200%;
  height: 200%;
  pointer-events: none;
  opacity: 0.026;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------------- the marks ----------------
   One gutter and one edge inset, shared by every mark, so the wordmark, the
   statement and the status all hang off the same left axis and the address
   mirrors it on the right. */
.stage {
  --gutter: clamp(24px, 5vw, 76px);
  --edge: clamp(28px, 5.5vh, 58px);
  /* One measure for the statement column, used by the column itself and by
     the contour mask that has to stay clear of it. Two copies of the same
     number would drift apart the moment either changed. */
  --copyw: clamp(360px, 46vmin, 660px);
}

.stage__logo {
  position: absolute;
  top: var(--edge);
  left: var(--gutter);
  z-index: 2;
  display: block;
  height: clamp(34px, 5.2vh, 52px);
  width: auto;
}

/* The statement, held in the open dark beside the figure. Headline and
   paragraph share one grid cell so they occupy the same optical centre —
   the second takes the first's place rather than appearing somewhere new. */
.stage__copy {
  position: absolute;
  left: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 3.2vh, 40px);
  width: var(--copyw);
}
.stage__slides { display: grid; }
.stage__type {
  grid-area: 1 / 1;
  align-self: center;
  opacity: 0;                      /* raised by script once lines are measured */
}
html:not(.js) .stage__type { opacity: 1; }   /* no script: plain, readable text */

.stage__title {
  /* tuned so the line breaks fall "AI for decisions / that have to be /
     right." — balanced lines, and the operative word left standing alone */
  /* Sized on vmin, not vw. On an ultrawide, vw is the long edge, so
     width-driven type stops growing the moment it hits its cap and the page
     freezes at 1920 no matter how big the screen gets. vmin follows the short
     edge, which on a single-viewport layout is the dimension that actually
     constrains the composition. Calibrated so 1920x1080 lands where it
     already was; everything above it now keeps scaling. */
  max-width: 11ch;
  font-size: clamp(2.1rem, 6.5vmin, 5.6rem);
  line-height: 1.06;
  letter-spacing: -0.033em;
  font-weight: 400;
  color: var(--ink);
}
.stage__lede {
  font-size: clamp(1.1rem, 2.3vmin, 2.1rem);
  line-height: 1.6;
  letter-spacing: -0.003em;
  color: var(--ink-70);
}

/* An olive rule held against the paragraph — the statement voice, marked as
   quoted rather than just set. It is built per line rather than on the
   paragraph, for two reasons: every slide's container stays visible and
   shares one grid cell, so a border there would sit on screen through all
   four; and building it from the lines means it draws itself downward as
   they arrive, and leaves with them. */
.stage__lede .ln { padding-left: 1.5em; }
.stage__lede .ln__i { position: relative; }
.stage__lede .ln__i::before {
  content: "";
  position: absolute;
  left: -1.5em;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--olive);
  opacity: 0.62;
}

/* slide 3 — the three guarantees. Term on its own line, the plain-English
   consequence beneath it, so each reads as a claim and its meaning. */
/* Each guarantee gets a rule over it with an olive lead-in — the same accent
   the index and the paragraph carry, so the three middle slides read as one
   family rather than three unrelated layouts. The rule spans the measure, so
   the column is held to its full width the way the index holds it. */
.stage__points { list-style: none; display: grid; gap: clamp(20px, 3vh, 34px); }
.stage__points .ln__i::before {
  content: "";
  display: block;
  height: 1px;
  margin-bottom: 0.78em;
  background: linear-gradient(90deg,
              var(--olive) 0,
              var(--olive) 38px,
              rgba(253, 252, 252, 0.16) 38px,
              transparent 100%);
}
.pt__k {
  display: block;
  font-size: clamp(1.3rem, 2.74vmin, 2.5rem);
  letter-spacing: -0.02em;
  color: var(--ink);
  margin-bottom: 6px;
}
.pt__v {
  display: block;
  font-size: clamp(0.98rem, 1.83vmin, 1.7rem);
  line-height: 1.5;
  color: var(--ink-70);
}

/* slide 4 — the markets, set as an index rather than a run-on line: the
   evidence voice, one per line, so each lands on its own lift */
/* Set as a numbered index with a rule carrying each entry across the measure.
   As five bare words it read as an unfinished list; numbered and ruled it
   reads as a register of where this is deployed — and it holds the width
   instead of leaving the column half empty. The numerals are presentational
   (CSS counters), so the markup still says exactly what it said. */
.stage__sectors {
  list-style: none;
  display: grid;
  gap: clamp(12px, 2.2vh, 24px);
  counter-reset: sector;
}
.stage__sectors .ln { counter-increment: sector; }
.stage__sectors .ln__i {
  display: flex;
  align-items: center;
  gap: 0.9em;
  font-family: var(--font-mono);
  font-size: clamp(1rem, 1.93vmin, 1.8rem);
  letter-spacing: 0.17em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--ink);
}
.stage__sectors .ln__i::before {
  content: counter(sector, decimal-leading-zero);
  flex: 0 0 auto;
  color: var(--olive);
  font-size: 0.74em;
  letter-spacing: 0.08em;
}
/* the rule takes up whatever the word leaves, so every entry ends flush */
.stage__sectors .ln__i::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  min-width: 12px;
  background: linear-gradient(90deg, rgba(253, 252, 252, 0.17), transparent);
}

/* ---------------- the masked line lift ----------------
   Each line travels inside its own clipped box: in from below, out through
   the top. No scaling, no bouncing, no sliding in from off-screen — the only
   thing that moves is the line itself, behind a hard edge.

   Direction is a variable, so stepping back reverses the physics: lines
   arrive from above and leave downward, which reads as going back rather
   than as more of the same forward march. */
.ln { display: block; overflow: hidden; }
.ln__i {
  display: block;
  transform: translateY(var(--from, 112%));
  opacity: 0;
  will-change: transform, opacity;
}
.is-in .ln__i {
  transform: none;
  opacity: 1;
  transition: transform 980ms var(--ease-rise), opacity 760ms var(--ease-rise);
}
.is-out .ln__i {
  transform: translateY(var(--to, -112%));
  opacity: 0;
  transition: transform 620ms var(--ease-leave), opacity 520ms var(--ease-leave) 60ms;
}
/* while lines are being regrouped for a new width, nothing may animate */
.no-anim .ln__i { transition: none !important; }

/* ---------------- stepping back through the slides ----------------
   Held back until the run has settled, so nothing invites a click while the
   statement is still making itself. Translucent at rest; the border and fill
   only firm up under the pointer. */
.stage__nav {
  display: flex;
  gap: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 800ms var(--ease-soft);
}
.stage__nav.is-on { opacity: 1; pointer-events: auto; }

.nav__arrow {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(253, 252, 252, 0.15);
  background: rgba(253, 252, 252, 0.035);
  color: var(--ink-50);
  cursor: pointer;
  transition: color 350ms var(--ease-soft), border-color 350ms var(--ease-soft),
              background-color 350ms var(--ease-soft), opacity 350ms var(--ease-soft);
}
.nav__arrow svg { display: block; width: 16px; height: 16px; }
.nav__arrow:hover:not(:disabled) {
  color: var(--ink);
  border-color: rgba(253, 252, 252, 0.42);
  background: rgba(253, 252, 252, 0.09);
}
.nav__arrow:disabled { opacity: 0.26; cursor: default; }
.nav__arrow:focus-visible { outline: 1px solid var(--olive); outline-offset: 3px; }

.stage__marks {
  position: absolute;
  left: var(--gutter);
  bottom: var(--edge);
  z-index: 2;
}
.stage__line {
  display: flex;
  align-items: center;
  gap: 11px;
  font-family: var(--font-mono);
  font-size: clamp(10.5px, 1.05vw, 12.5px);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-50);
}
/* the one deliberate olive accent on the page */
.stage__line::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--olive);
  flex: none;
}

.stage__mail {
  position: absolute;
  right: var(--gutter);
  bottom: var(--edge);
  z-index: 2;
  font-family: var(--font-mono);
  font-size: clamp(10.5px, 1.05vw, 12px);
  letter-spacing: 0.14em;
  color: var(--ink-50);
  text-decoration: none;
  transition: color 0.35s var(--ease-soft);
}
.stage__mail:hover { color: var(--ink); }

/* ---------------- the address types itself ----------------
   Every character is present from the start and simply hidden, so the box
   never changes width and nothing shifts as it fills — and it survives a
   resize without re-measuring. The caret is absolutely positioned off the
   last revealed character, so it costs no layout either. */
/* Nothing is there until it is typed. Held off before first paint so the
   address never renders and then restarts itself. Without script it simply
   stays as plain text. */
html.js .stage__mail { opacity: 0; }
.mail__t { white-space: pre; }
.mail__t span { visibility: hidden; position: relative; }
.mail__t span.on { visibility: visible; }
.mail__t span.cursor::after {
  content: "";
  position: absolute;
  right: -0.34em;
  top: 0.08em;
  bottom: 0.04em;
  width: 1px;
  background: var(--olive);
}
.is-typing .mail__t span.cursor::after { animation: caret 1.05s steps(1) infinite; }
.is-typed  .mail__t span.cursor::after { opacity: 0; transition: opacity 620ms var(--ease-soft); }

@keyframes caret {
  0%, 55%      { opacity: 1; }
  55.01%, 100% { opacity: 0; }
}

/* ---------------- narrow screens ----------------
   No room beside the figure, so the order becomes vertical: he holds the
   upper frame, the statement and the marks stack beneath him. */
@media (max-width: 760px) {
  .stage { --gutter: 24px; --edge: 26px; }
  /* he takes the upper frame only, so the copy below him is never crossed */
  /* the arrows add height below the copy, so he yields a little more room */
  /* He takes the band between the wordmark and the copy, and only that band —
     roughly 210px on a 375x812 screen. Sized to clear both: at 42% he ran
     straight through the wordmark above him and the statement below. */
  /* No lower-left corner to speak of at this width: the statement runs to
     ~86% of the height and the status mark starts at 89%, so the field would
     land on the copy rather than beside it. The upper field carries the
     texture on its own here. */
  .hero__lines--bl { display: none; }

  .hero__badger { --cx: 50%; right: 50%; bottom: auto; top: 11%; height: 20.5%; }
  .hero__glow   { --cx: 50%; right: 50%; bottom: auto; top: 2%; width: 92vw; height: 32%; }
  .hero__mark   { width: 92vw; top: 6%; right: 4%; }
  .stage__copy {
    top: auto;
    bottom: 112px;
    transform: none;
    width: auto;
    right: var(--gutter);
  }
  .stage__title { max-width: 15ch; font-size: clamp(1.75rem, 8vw, 2.4rem); }
  .stage__lede { font-size: 1.02rem; line-height: 1.55; }
  .stage__marks { bottom: 74px; }
  .stage__mail { left: 24px; right: auto; bottom: 30px; }
}

/* ---------------- reduced motion ----------------
   The sequence still runs: the paragraph is content, and withholding it would
   be a broken page, not a calm one. What goes is the travel — same beats,
   delivered as a cross-fade, with the clipping released so nothing is masked. */
@media (prefers-reduced-motion: reduce) {
  /* The mark still cuts. Reduced motion is about vestibular movement, and
     nothing here travels across the page, scales or parallaxes — it is a
     reveal in place at 130px, the same category as the address typing
     itself, which also runs. Suppressing it would remove the loading
     screen's only signal of progress and leave a logo pulsing at someone
     for no reason.

     Only the tempo changes — same cut, taken slower. 1.7s keeps the strike
     itself at ~410ms, which is the pace that was signed off. */
  .cut { animation-duration: 1.7s; }

  /* The contour drift stays, eased off by about a third — not stopped, and
     not slowed into stillness. Taking it to 150s last time left it moving at
     0.3px a second, which is indistinguishable from a static image and simply
     hands back the flat void the lines exist to fix. */
  .ln-far  { animation-duration: 42s; }
  .ln-mid  { animation-duration: 34s; }
  .ln-near { animation-duration: 27s; }

  .ln { overflow: visible; }
  .ln__i { transform: none; }
  .is-in .ln__i  { transform: none; opacity: 1; transition: opacity 560ms linear; }
  .is-out .ln__i { transform: none; opacity: 0; transition: opacity 420ms linear; }
}
