/* ==========================================================================
   WHY MARBLE — "THE RESOLVE"   (.rsv, index.html, homepage 05)

   Replaces the offset (css/offset.css, unlinked, kept on disk).

   WHAT THE AUDIT FOUND, AND IT IS NOT WHAT THE OFFSET WAS ACCUSED OF.
   The offset's reading was right and its copy was already the approved
   three lines. Its fault was proportion: measured at 1280 it ran three
   rows of type with a 267x150 photograph pinned to the right of each,
   so the three pictures together covered 120,150px² of a 764px section
   — about 3% of it. EIGHTY-EIGHT PER CENT OF THE SECTION WAS TYPE, and
   the whole agency-versus-Marble opposition was carried by which half
   of a sentence was set in bold. A visitor scanning it saw three lines
   of prose and three postage stamps. "Understand it without reading"
   cannot be met by a section whose only graphic is a font weight.

   THE READING SURVIVES, THE STAGING DOES NOT — the same call as 02.
   Every one of the three approved claims is about the same thing:
   what an agency hands you is a REPRODUCTION of the customer, and
   what Marble hands you is the customer. Attention is a count of
   impressions; a lead is a name on a list; coverage bought at signing
   is a number on a contract. The conversation, the signature and the
   team that turns up are the things themselves.

   SO THE DRAWING IS RESOLUTION. Each claim is one photograph, and the
   photograph is HALF DISSOLVED INTO A DOT SCREEN. Left of the gold
   edge is the agency's version — grey, screened, made of countable
   marks, standing at the same depth as the words. Right of it is the
   same photograph, whole, warm, and 38px NEARER THE READER than the
   type plane. Arm's length and in the room, in one image, with no
   sentence needed to explain it.

   AND THE SCROLL PERFORMS THE ARGUMENT RATHER THAN DECORATING IT. At
   --rsv 0 the edge sits at 100% and every band is nothing but screen:
   dots, grey, a reproduction. The scrub walks the edge left to 62% and
   the real photograph emerges out of the halftone. Pointing at a band
   drives it further, to 34%, and lifts the picture from 38px to 54px so
   it comes toward the reader as it grows. Nothing else in the section
   moves — no camera swing, no staged room.

   THAT ABSENCE IS DELIBERATE. 02 (fold), 03 (forge) and 04 (reach) are
   all staged spaces with a moving camera. A fourth would leave the page
   no change of beat in its middle. Here the camera is fixed, the
   composition is flat-on and editorial, and the only depth is the 38px
   between the reproduction and the thing — which is the argument, not
   a stage. The action is RESOLUTION, not rotation.

   ONE ALIGNED LINE, BROKEN ONCE. At rest the three gold edges sit at
   the same 62% and read as a single rule standing through the section.
   The live band's edge advances to 34%, so the line breaks in exactly
   one place — a stronger pointer to the live row than any highlight,
   and it costs nothing to draw because the rule is just three edges
   that happen to agree.

   THE SEAM IS EXACT BY CONSTRUCTION, NOT BY TUNING. The photograph's
   clip and the gold rule share ONE transformed plane (.rsv__near), so
   they agree in its local coordinates before the perspective divide
   ever happens. As two separately transformed elements they are
   magnified about the same origin but from different boxes and the seam
   opens — measured 4.6px at lift 38, and it would widen with the lift.
   See .rsv__near for the inset that cancels the magnification.

   THE 3D DISCIPLINE: no filter, opacity, mask, clip or overflow on
   .rsv__stage or .rsv__band — each flattens preserve-3d silently. The
   halftone mask and the grading live on the images inside .rsv__screen
   and .rsv__shot, and the clip-paths live on those two wrappers, all of
   them LEAVES with nothing 3D inside them. No .reveal in the stage:
   it sets opacity AND a transform. `overflow-x: clip` on the SECTION,
   which is outside the perspective context.

   REST STATE IS THE FINISHED SCENE (--rsv initial-value 1) with band 01
   live, so no JS, a dead CDN and reduced motion all render three
   resolved photographs rather than three fields of grey dots.
   ========================================================================== */

@property --rsv { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --on  { syntax: "<number>"; inherits: true; initial-value: 0; }

.rsv {
  /* ONE SOURCE OF TRUTH. --edge drives the photograph's clip, the gold
     rule's position and the scrim under the type; --fwd drives the
     plane. Both are written here and inherited, so the seam cannot come
     apart and the words cannot end up over the wrong half. */
  --shut: 100;                         /* edge %, nothing resolved yet */
  --rest: 64;                          /* edge %, settled */
  --open: 46;                          /* edge %, live */
  /* THE CLAIM'S COLUMN, AND THE FLOOR UNDER --open. In a preserve-3d
     context paint order is DEPTH, not z-index, so the photograph at
     +38px is in front of the words at +2px and no stacking value can
     put the type back on top. The live edge therefore must never cross
     the claim: --open is --sayw plus six points of clearance, and the
     claim is capped by its own column rather than by a hand-typed
     width, so the two cannot drift apart at a breakpoint. Measured
     before this rule existed: the longest claim ran to 51% of the band
     on one line and an --open of 34 buried a third of it. */
  --sayw: 40;                          /* the type column, % of the band */
  --persp: 1200;                       /* unitless: .rsv__near does arithmetic with it */
  --near: 38;                          /* the photograph's depth, px */
  --nearlive: 54;                      /* ...and its depth when pointed at */
  --back: 64;                          /* how far back the band starts */
  --bh: clamp(104px, 11.9vw, 152px);

  /* ----------------------------------------------------------------
     TWO CURVES, AND A BAND RESOLVES ON ONE AND CLEARS ON THE OTHER.

     The same pair, to the same values, as css/triptych.css. That is the
     point of them: one motion signature across the page rather than a
     different feel per section, which is what makes a set of sections
     read as one site. The in-curve is an exponential-out - most of its
     distance early, then a long flat tail that reads as the band coming
     to REST rather than stopping. The out-curve runs it back over about
     two thirds of the time.

     THIS MATTERS MOST WHERE TWO BANDS TRADE. On every advance the
     outgoing band and the incoming one move at once, and while they
     shared one duration and one curve the swap read as a crossfade
     between two states. The outgoing one now clears in 0.44s while the
     incoming takes 0.72s to settle - so the eye is released by the old
     claim before the new one has finished arriving, and the sequence
     reads as attention MOVING rather than as a slideshow stepping.
     ---------------------------------------------------------------- */
  --rsv-in: cubic-bezier(0.16, 1, 0.3, 1);
  --rsv-out: cubic-bezier(0.4, 0, 0.65, 1);

  position: relative;
  padding-block: clamp(2.2rem, 1.8rem + 1.8vw, 3.6rem);
  /* Anything at depth projects wider than its layout box. Clip, not
     hidden, so the section never becomes a scroll container; safe
     because .rsv is an ancestor of the perspective element, not a
     preserve-3d parent. */
  overflow-x: clip;
}

/* ---------- head ---------- */

.rsv__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-4);
  margin-bottom: clamp(1.5rem, 1.1rem + 1.5vw, 2.5rem);
}

.rsv__h {
  font-size: var(--text-h2);
  line-height: 1.06;
  letter-spacing: -0.022em;
  font-weight: 300;
  margin: var(--space-2) 0 0;
  text-wrap: balance;
}
.rsv__h em { font-style: normal; color: var(--gold); }

.rsv__on { padding-bottom: 0.35rem; white-space: nowrap; }

/* ---------- the stage ---------- */

.rsv__stage {
  display: grid;
  gap: 10px;
  perspective: calc(var(--persp) * 1px);
  perspective-origin: 50% 50%;
}

.rsv__band {
  position: relative;
  height: var(--bh);
  transform-style: preserve-3d;
  transform: translateZ(var(--fwd));

  display: block;
  border: 0;
  padding: 0;
  margin: 0;
  background: var(--obsidian-raised);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;

  /* --edge AND --lift ARE RESOLVED HERE, NOT ON THE SECTION, and that
     is load-bearing rather than tidiness. A custom property that
     references another is substituted on the element where it is
     DECLARED, and the resolved token is what inherits — so an --edge
     written on .rsv would bake in the section's --on (always 0) and no
     band would ever open. Declared on the band, each one resolves
     against its own live state. The CONSTANTS stay on the section so a
     breakpoint still has one place to change them. */
  /* EACH BAND CUTS ITS OWN WINDOW OUT OF THE SECTION'S SCRUB. --rsv is
     one number for the whole stage and all three bands used to read it
     raw, so the three photographs emerged from their halftone in perfect
     unison - three identical things resolving at one rate, which reads
     as a wipe passing over the stage rather than as three pictures
     coming up. --rsvp is this band's own share of it, opened 0.13 later
     per index and run 2.1x as fast so every band still reaches a full 1
     well before the scrub ends. Same device as --lp in css/triptych.css
     and css/inscription.css.

     IT REPLACES --rsv IN BOTH PLACES OR IN NEITHER. The edge and the
     forward step are the same arrival seen two ways; feeding one the
     staggered number and the other the raw one would have the band
     travelling toward the reader on a different clock from the picture
     resolving inside it. */
  --rsvp: clamp(0, calc((var(--rsv) - var(--rsvi, 0) * 0.13) * 2.1), 1);

  --edge: calc((var(--shut)
    - (var(--shut) - var(--rest)) * var(--rsvp)
    - (var(--rest) - var(--open)) * var(--on)) * 1%);
  --lift: calc(var(--near) + (var(--nearlive) - var(--near)) * var(--on));
  --fwd: calc(var(--back) * -1px * (1 - var(--rsvp)));

  /* the CLEARING list. Going live is on .is-live below, which is the only
     way CSS lets a transition differ by direction. */
  transition: --on 0.44s var(--rsv-out);
}
.rsv__band:nth-child(1) { --rsvi: 0; }
.rsv__band:nth-child(2) { --rsvi: 1; }
.rsv__band:nth-child(3) { --rsvi: 2; }

/* The whole live state is this one number: it drives the edge, the
   photograph's depth and magnification, the grading on both halves and
   the two type colours. Nothing else is written when a band goes live. */
.rsv__band.is-live {
  --on: 1;
  transition: --on 0.72s var(--rsv-in);
}

/* THE REPRODUCTION. Grey, screened, and at the same depth as the words
   — because a reproduction and a description of a thing are the same
   distance from you. It fills the whole band and is simply COVERED from
   --edge rightward, never clipped to meet the photograph: two clipped
   halves at two depths would leave a hairline of ground between them
   that widens as the near one lifts. */
.rsv__screen {
  position: absolute;
  inset: 0;
  z-index: 1;
  transform: translateZ(0);
}
.rsv__screen img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* The halftone. Not a texture laid over a photograph — the
     photograph itself, punched through on a 9px pitch, so what is left
     is a count of marks rather than a picture. Cold and flat against
     the warm, whole version beside it. */
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 43%, transparent 45%);
  mask-image: radial-gradient(circle at 50% 50%, #000 43%, transparent 45%);
  -webkit-mask-size: 9px 9px;
  mask-size: 9px 9px;
  /* NO `transition: filter` HERE, AND TAKING IT OUT IS THE SINGLE THING
     THAT MADE THIS SECTION FEEL SHARP.

     This filter is COMPUTED FROM --on, and --on is itself already
     interpolating over 0.72s on the band. A transition on the filter as
     well meant every frame started a fresh 0.5s transition toward a
     value that was itself only part-way to its target - the value was
     eased twice, arrived late, and was still visibly drifting after the
     geometry it belongs to had settled. That lag is what read as soft
     and unresolved; it was never the durations.

     One clock per thing that moves. --on is this band's clock, and
     everything derived from it is exactly in step with the edge and the
     lift by construction rather than by two numbers matching. */
  filter: grayscale(1) contrast(1.3) brightness(calc(0.72 - 0.14 * var(--on)));
}

/* THE THING, AND ITS EDGE, ON ONE PLANE. The photograph's clip and the
   gold rule must agree to the pixel, and they only do that reliably if
   they share a coordinate system BEFORE the perspective divide — as two
   separately transformed elements they are magnified about the same
   origin but from different boxes, and the seam opens by several pixels
   (measured 4.6px at lift 38). So both live inside .rsv__near, which is
   the only thing carrying translateZ, and inside it "--edge" means the
   same x to both.

   THE INSET CANCELS THE MAGNIFICATION EXACTLY. A box at depth z under
   perspective P projects P/(P-z) larger, so to land back on the band it
   must start (1 - (P-z)/P)/2 = z/(2P) smaller on each side — which is
   `--lift / --persp * 50%`. Both are unitless for exactly this reason.
   It stays exact when the live state raises --lift and when a
   breakpoint changes the perspective (hard-coding the P = 1200 case
   left the phone's 900px stage 2px out), and it is why the band's own
   forward step was dropped: a second, animated depth on top of this one
   cannot be cancelled by a static inset, and the picture would bleed
   out of its band by 37px whenever it was live. */
.rsv__near {
  position: absolute;
  inset: calc(var(--lift) / var(--persp) * 50%);
  z-index: 3;
  transform: translateZ(calc(var(--lift) * 1px));
}
.rsv__shot {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 var(--edge));
}
.rsv__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* --on is the clock; see .rsv__screen img above for why there is no
     transition on this line. The two halves are graded from one number
     and must key up together - a transition here and not there, or two
     that differ by 50ms, shows up directly on the seam between them. */
  filter: brightness(calc(0.86 + 0.14 * var(--on))) saturate(calc(0.94 + 0.06 * var(--on)));
}

/* The seam. Same --edge and the same translateZ as the clip above it,
   so perspective moves both by the identical factor. */
.rsv__edge {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--edge);
  width: 1px;
  z-index: 2;
  background: linear-gradient(
    to bottom,
    rgba(184, 145, 88, 0.25),
    var(--gold) 22%,
    var(--gold) 78%,
    rgba(184, 145, 88, 0.25)
  );
}
/* A short tick at the head of the rule, so the seam reads as a drawn
   edge rather than as the join between two pictures. */
.rsv__edge::before {
  content: "";
  position: absolute;
  left: -3px;
  top: -1px;
  width: 7px;
  height: 1px;
  background: var(--gold);
}

/* ---------- the claim ---------- */

.rsv__say {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 2;
  width: calc(var(--sayw) * 1%);
  display: flex;
  align-items: center;
  gap: 1.15rem;
  padding-inline: clamp(1rem, 2.2vw, 2rem) 1.5rem;
  transform: translateZ(2px);
  /* The screen is dark and open-textured; type over it needs a floor,
     and the floor fades out to the right so it never reaches the seam
     and never looks like a panel sitting on the picture. */
  background: linear-gradient(
    to right,
    rgb(var(--scrim-rgb) / 0.86) 30%,
    rgb(var(--scrim-rgb) / 0.55) 74%,
    rgb(var(--scrim-rgb) / 0) 100%
  );
}

.rsv__n {
  flex: 0 0 auto;
  align-self: flex-start;
  margin-top: 0.15rem;
  font-size: 0.7rem;
  font-weight: 600;
  font-style: normal;
  letter-spacing: 0.16em;
  font-variant-numeric: tabular-nums;
  /* colour-mixed straight off --on, no transition of its own - same
     reason as the two gradings above. */
  color: color-mix(in srgb, var(--gold) calc(38% + 62% * var(--on)), var(--vein));
}

/* The opposition happens INSIDE the sentence, because that is how the
   approved copy wrote it: the <b> is Marble's half, white, against the
   agency's half at --vein. Claim 03 has no agency half at all and that
   is the point of the third claim, not a break in the pattern. */
.rsv__claim {
  font-size: clamp(1rem, 0.86rem + 0.55vw, 1.32rem);
  line-height: 1.36;
  letter-spacing: -0.012em;
  font-weight: 300;
  color: var(--vein);
  text-wrap: pretty;
}
/* THE ONE THING HERE THAT IS *NOT* ON --on, AND DELIBERATELY SO. This
   colour is driven by the .is-live class, not by the band's number, which
   means it can be given a beat of its own - and it is the one element that
   should have one. The picture resolving is the event; the sentence going
   to full white is the CONSEQUENCE of it, so it starts 160ms in, by which
   point the exponential curve has already carried the edge most of its
   travel. Leaving, it goes first and fastest: the claim is what the reader
   is holding, so releasing it is how they are told to look elsewhere. */
.rsv__claim b {
  font-weight: 400;
  color: var(--ink);
  transition: color 0.34s var(--rsv-out);
}
.rsv__band.is-live .rsv__claim b {
  color: var(--white);
  transition: color 0.5s var(--rsv-in) 0.16s;
}

.rsv__band:focus-visible { outline: 1px solid var(--gold); outline-offset: 2px; }

/* ==========================================================================
   NARROWER
   ========================================================================== */

@media (max-width: 1040px) {
  .rsv {
    --sayw: 46;
    --rest: 66;
    --open: 52;
  }
}

@media (max-width: 860px) {
  .rsv__head { grid-template-columns: 1fr; align-items: start; }
  .rsv__on { justify-self: start; }
}

/* ==========================================================================
   PHONE — the seam turns horizontal

   A 340px band split at 56% leaves the claim 190px and the photograph
   150px, and both fail. The argument does not need a VERTICAL seam, it
   needs a seam: the band grows, the screened half goes to the TOP with
   the words on it, and the resolved photograph takes the bottom. Same
   --edge, same three states, read down instead of across.
   ========================================================================== */

@media (max-width: 720px) {
  .rsv {
    --shut: 100;
    /* Same floor as the desktop rule, one axis over: the claim is 43px
       tall here, so the live seam has to stay clear of it vertically or
       the photograph clips the descenders (measured 3px into them at an
       --open of 26). */
    --rest: 46;
    --open: 36;
    --near: 22;
    --nearlive: 32;
    --back: 44;
    --persp: 900;
    --bh: 208px;
  }
  .rsv__stage { gap: 12px; }
  .rsv__screen { clip-path: inset(0 0 18% 0); }
  .rsv__shot { clip-path: inset(var(--edge) 0 0 0); }
  .rsv__edge {
    inset: var(--edge) 0 auto 0;
    width: auto;
    height: 1px;
    background: linear-gradient(
      to right,
      rgba(184, 145, 88, 0.25),
      var(--gold) 18%,
      var(--gold) 82%,
      rgba(184, 145, 88, 0.25)
    );
  }
  .rsv__edge::before { left: 0; top: -3px; width: 1px; height: 7px; }
  .rsv__say {
    inset: 0 0 auto 0;
    width: auto;
    height: var(--edge);
    align-items: center;
    padding-block: 0.5rem;
    background: linear-gradient(
      to bottom,
      rgb(var(--scrim-rgb) / 0.86) 40%,
      rgb(var(--scrim-rgb) / 0.5) 82%,
      rgb(var(--scrim-rgb) / 0) 100%
    );
  }
  .rsv__claim { font-size: 0.98rem; }
}

/* ==========================================================================
   REDUCED MOTION — resolved on arrival, and it stays resolved
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .rsv { --rsv: 1; }
  /* ONLY TWO THINGS STILL CARRY A TRANSITION. The gradings and the folio
     lost theirs when the double-ease came out - they follow --on now, so
     killing --on's duration on the band stops them too, and naming them
     here would be listing elements that have nothing to stop. --on is
     declared twice, on the band and on .is-live, so both need clearing or
     an arriving band keeps its 0.72s. */
  .rsv__band,
  .rsv__band.is-live,
  .rsv__claim b { transition-duration: 0.01ms; transition-delay: 0s; }
}
