/* ============================================================================
   JI JEWELRY — THE CASE STUDY LAYOUT
   ----------------------------------------------------------------------------
   The editorial layout a portfolio project gets when it carries a `caseStudy`
   block in data.js. project.html is the only page that loads this file, and
   every rule in it is scoped to `.cs`, so none of it can reach the shop, the
   builder, the homepage, or the projects that do not opt in.

   It borrows the site's tokens rather than inventing a palette: the same ink,
   the same paper, the same three faces, the same drafting marks. What is new
   is the scale — this page is read, not scanned, so the type runs bigger and
   the air between sections runs wider than anywhere else on the site.

   The two warm tones are pulled from the photographs themselves, redwood and
   lace, and they appear on about four elements in the whole page. They are a
   seasoning for this one story, not a second brand.
   ========================================================================== */

.cs{
  --cs-wood:      #7C4A2E;              /* redwood, on paper */
  --cs-wood-lit:  #D0966A;              /* redwood, on ink */
  --cs-ivory:     #EDE5D8;              /* the lace */
  --cs-rule:      rgba(22,23,26,.13);
  --cs-rule-ink:  rgba(242,241,238,.15);
  --cs-band:      clamp(46px,5.6vw,88px);   /* was half again as much */

  /* ------------------------------------------ THE MATERIALS, FROM THE RING
     Not chosen. SAMPLED, out of assets/img/projects/lace-ring/finished.jpg —
     Jake's own photograph of the finished piece — so the three material plates
     and the ring in the reveal above them are the same colours rather than
     merely near each other.

     The useful finding was the damascus. Stock damascus imagery is cold: blue
     steel, white silver. His is not. Etched and taken dark, the layers come
     back BONE and PEWTER over a warm near-black — measured at #BDAE9D and
     #A89A88 against #0F0A06 — and a cold-silver treatment beside the real
     photograph looks like a different piece of metal. Everything here is warm
     for that reason.

     The redwood took a second pass to measure honestly. Sampling the channel
     as a rectangle gives a washed-out tan, because most of that rectangle is
     lace cord and cured resin rather than wood. Sampling only the pixels that
     ARE wood — warm hue, saturated, not neutral — gives #54200D: hue 16.5,
     saturation 0.74, lightness 0.19. Deep, red, and dark. The tan is the shine
     on the resin sitting over it, and using the tan as the wood's own colour
     is exactly what made one pass read as dusty mauve and the next as caramel.
     It survives here as the sawdust in the reveal, which is what it looks
     like, and nowhere else. */
  --cs-m-dam-dark:  #0F0A06;   /* the deepest layers, warm near-black */
  --cs-m-dam-mid:   #463A2B;
  --cs-m-dam-light: #BDAE9D;   /* the bright layers. Pewter, NOT silver */
  --cs-m-wood-deep: #2F0902;   /* wood-only mask, darkest twelfth */
  --cs-m-wood-mid:  #54200D;   /* wood-only mask, mean */
  --cs-m-wood-lit:  #C6AA95;   /* NOT the wood: resin shine, and the sawdust */
  --cs-m-lace:      #DDD7D0;   /* the dress */
  /* Forge heat. The one colour on this page that is not in the photograph,
     because the photograph is of a finished ring and not of the making of it.
     Kept to two stops and gone by the end. */
  --cs-m-heat:      #FF9A4A;
}

/* Every section owns its vertical rhythm through one variable, so the whole
   page can be tightened or opened up from one place. */
.cs-sect{padding-block:var(--cs-band)}
.cs-sect-ink{background:var(--ink);color:var(--on-ink)}
.cs-sect-tight{padding-block:clamp(34px,4vw,58px)}

.cs-wrap{width:100%;max-width:1180px;margin-inline:auto;padding-inline:var(--gut)}
.cs-narrow{max-width:760px}

/* THE PAIR. A wide screen with one left-aligned column on it leaves half the
   band empty, which reads as unfinished rather than as air. Almost every
   section here is really two blocks — a statement and the thing it is about —
   so they are set as a pair and the gap between them becomes the negative
   space instead of the right-hand third of the screen. Below 900px the pair
   is one column, in the order it is written. */
.cs-duo{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(26px,4.5vw,80px);align-items:start;
}
.cs-duo-mid{align-items:center}
.cs-duo-art{display:grid;grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr)}

.cs-kick{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;
  color:var(--text-faint);display:flex;align-items:center;gap:12px;margin:0 0 18px;
}
.cs-kick::before{content:"";width:26px;height:1px;background:currentColor;flex:none;opacity:.7}
.cs-sect-ink .cs-kick{color:var(--on-ink-faint)}

/* --------------------------------------------------------------- 1. OPENING
   Type only, and short. The concept drawing is the left half of the wipe
   directly below it, so showing it here as well cost a screen of scrolling
   to say the same thing twice. */
.cs-open{padding-top:calc(var(--hdr) + clamp(20px,3vw,40px));padding-bottom:clamp(30px,4vw,56px)}
.cs-open .crumb{margin-bottom:clamp(20px,3vw,36px)}
.cs-open-in{
  display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);
  gap:clamp(18px,3.4vw,56px);align-items:end;
}
.cs-title{
  font-family:var(--display);font-weight:800;
  font-size:clamp(36px,5.8vw,76px);line-height:.98;letter-spacing:-.035em;
  margin:0;text-wrap:balance;
}
.cs-lead{
  font-size:clamp(15.5px,1.25vw,18.5px);line-height:1.58;color:var(--text-soft);
  max-width:40ch;margin:0;text-wrap:pretty;
}
.cs-facts{
  display:flex;flex-wrap:wrap;gap:8px 22px;margin-top:18px;
  padding-top:16px;border-top:1px solid var(--cs-rule);
  font-family:var(--mono);font-size:10.5px;letter-spacing:.15em;text-transform:uppercase;
  color:var(--text-faint);
}
.cs-facts b{color:var(--text);font-weight:500}

/* THE PLATE. A drawing pinned to a board: paper ground, hairline frame, and
   register ticks at the corners, which is the site's own drafting language.
   Kept because a future case study may still want to open on one. */
.cs-plate{position:relative;padding:clamp(14px,1.8vw,24px);background:var(--paper-2);
  border:1px solid var(--cs-rule);border-radius:var(--radius-lg)}
.cs-plate img{display:block;width:100%;height:auto;border-radius:2px;
  max-height:min(62vh,620px);object-fit:contain}
.cs-plate::before,.cs-plate::after{
  content:"";position:absolute;width:14px;height:14px;pointer-events:none;
  border:1px solid var(--draft);
}
.cs-plate::before{top:7px;left:7px;border-right:0;border-bottom:0}
.cs-plate::after{bottom:7px;right:7px;border-left:0;border-top:0}
.cs-plate-tag{
  position:absolute;top:calc(100% + 12px);left:0;
  font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--text-faint);
}

/* ---------------------------------------------------------------- 2. THESIS
   The statement of the piece. Oversized, on ink, nothing else on screen. */
.cs-thesis h2{
  font-size:clamp(32px,5vw,64px);line-height:1.02;letter-spacing:-.032em;
  margin:0;max-width:13ch;text-wrap:balance;
}
.cs-thesis p{
  font-size:clamp(16px,1.4vw,20px);line-height:1.62;color:var(--on-ink-soft);
  max-width:50ch;margin:0 0 18px;text-wrap:pretty;
}
.cs-thesis .cs-duo{align-items:center}
.cs-thesis p:last-child{margin-bottom:0}
.cs-thesis p:last-child{color:var(--on-ink)}

/* ------------------------------------------------------------- 3. MATERIALS
   Three across. As three full-width rows they were three separate screens of
   scrolling for three short paragraphs; side by side they are still three
   designed moments and the whole idea lands at once. */
.cs-mats-head{max-width:40ch;margin-bottom:clamp(22px,3vw,38px)}
.cs-mats-head h2{
  font-size:clamp(26px,3.4vw,44px);letter-spacing:-.03em;line-height:1.04;margin:0;
}
.cs-mats{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(18px,2.6vw,40px);
}
.cs-mat{display:flex;flex-direction:column;gap:clamp(14px,1.8vw,22px)}

/* The plate. It was a pale panel with a line drawing on it; it is a dark
   window onto the material now. That is the one change of appearance in this
   section and it is deliberate: the materials themselves are dark, warm and
   lit, and a photograph of black damascus on a pale grey card reads as a
   sample chip rather than as the thing the ring is made of. Everything around
   it — the grid, the numbers, the type, the copy, the spacing — is untouched. */
.cs-mat-art{
  position:relative;aspect-ratio:5/4;border-radius:var(--radius-lg);
  background:var(--cs-m-dam-dark);border:1px solid var(--cs-rule);
  overflow:hidden;flex:none;
  isolation:isolate;            /* keeps the blend modes below off the page */
}
.cs-mat-art svg{display:block;width:100%;height:100%}

/* ==========================================================================
   THE MATERIAL REVEAL
   --------------------------------------------------------------------------
   Driven by assets/js/material-reveal.js. Read the header of that file first;
   the mechanism is one travelling edge per plate and everything below is the
   dressing on it.

   Three states, and they matter:
     (nothing)  armed, not yet seen. The material is hidden.
     .is-in     it has entered the viewport. The one run happens here.
     .is-done   the run has finished. Holds the final state so nothing
                depends on a fill-mode surviving a repaint, and so the
                keyframes can be dropped entirely.
     .is-still  reduced motion. Straight to the end, no animation at all.
   ========================================================================= */

.cs-mr{position:relative;overflow:hidden}
.cs-mr > img,
.cs-mr > svg:not(.cs-mr-thread){
  display:block;width:100%;height:100%;
  /* The starting state of every material: present, but not yet revealed.
     Set here rather than in the keyframes so a plate that never scrolls into
     view is still correct, and so is one on a browser with no observer. */
  opacity:0;
}
/* object-fit is for the PHOTOGRAPH and only the photograph.

   It used to be on both, and an inline <svg> with a viewBox is a replaced
   element as far as object-fit is concerned — so the rendered material was
   being scaled by preserveAspectRatio and then scaled AGAIN by object-fit.
   The pattern came out at the wrong size and, because a fine high-contrast
   texture averages toward mid-grey when it is scaled down, the black damascus
   rendered as light grey-brown and the redwood as tan. It looked like a
   colour bug and it was a geometry bug: the identical SVG rendered on a page
   without this stylesheet came out correct, which is what gave it away. */
.cs-mr > img{object-fit:cover}
/* The finished state. A `forwards` animation outranks a plain declaration, so
   while the keyframes are still filling, this rule loses to them — which is
   fine, because every one of those keyframe sets ENDS on exactly these values.
   What it is really for is the case where the animation never ran or was
   interrupted, so a plate can never be left invisible. */
.cs-mr.is-done > img,
.cs-mr.is-done > svg:not(.cs-mr-thread){opacity:1;filter:none;transform:none}

/* The drawn plate, when there is no photograph for a material yet. It was
   stroked in drafting graphite for a pale card; on the dark plate it takes
   the material's own colour instead, so even the placeholder is the right
   colour for the ring. A CSS rule beats the stroke attribute on the svg. */
/* EVERY ONE OF THESE EXCLUDES .cs-mr-thread, and that is not fussiness. The
   stitch thread is an <svg> and a direct child of .cs-mr, so it matched every
   rule written for "the drawn plate" — including the one holding the plate at
   opacity 0 until it is revealed. The stitches were being drawn, clipped and
   shadowed perfectly, at zero opacity, and nothing anywhere errored. */
/* No stroke. These rules used to tint the line drawings the plates once were;
   a rendered material is filled paths and the inherited stroke was outlining
   every one of its 124 bands in pewter. */
.cs-mr > svg:not(.cs-mr-thread){opacity:0}

/* No ground under a rendered plate either. It used to be needed because a
   thin line drawing on flat black is a diagram and needs something behind it;
   a rendered material paints its own base, its own layers and its own light,
   and a gradient underneath it can only show at the rounded corners. */


/* ------------------------------------------------------------- THE EDGE
   One element, one transform, travelling once across the plate. Ahead of it
   is a wall the colour of the plate, which is what actually hides the
   material; at its leading edge is whatever that material is being worked
   with. 122% wide so the wall still covers the plate when the edge itself
   is at the far side. */
.cs-mr-edge{
  position:absolute;inset:0 auto 0 0;width:122%;
  transform:translate3d(-22%,0,0);
  will-change:transform;pointer-events:none;z-index:2;
}
.cs-mr-edge::before{            /* the wall: everything not yet revealed */
  content:"";position:absolute;inset:0 0 0 18%;
  /* Near-black rather than the plate's own ground, because the plate grounds
     are lit and a wall the same colour as a lit ground shows its own edge.
     What is ahead of the work has not been made yet; it should be dark.

     And the front of the wall is a short gradient rather than a straight cut.
     A razor line between a lit photograph and pure black reads as a slideshow
     wipe; eighteen pixels of falloff reads as the material coming up. */
  background:linear-gradient(90deg,
    rgba(7,5,5,0) 0%, rgba(7,5,5,.55) 1.6%, #070505 4.5%, #070505 100%);
}
.cs-mr-edge::after{             /* the face: what is happening at the front */
  content:"";position:absolute;inset:0;
}
.cs-mr.is-in .cs-mr-edge{animation:cs-mr-sweep 2.05s cubic-bezier(.42,.06,.22,1) forwards}
.cs-mr.is-done .cs-mr-edge,
.cs-mr.is-still .cs-mr-edge{display:none}

@keyframes cs-mr-sweep{
  from{transform:translate3d(-22%,0,0)}
  to  {transform:translate3d(100%,0,0)}
}

/* ------------------------------------------------------------ 01. FORGE
   Black damascus. It arrives out of the dark under heat, and the heat leaves.
   The steel is never orange — the GLOW is orange, briefly, at the edge where
   the work is happening, and the material behind it is already cool. */
.cs-mr[data-mat-reveal="forge"] .cs-mr-edge::after{
  background:linear-gradient(90deg,
    transparent 0%,
    rgba(255,154,74,.12) 9%,
    rgba(255,154,74,.46) 16%,
    rgba(255,205,150,.72) 18.4%,
    rgba(255,154,74,.30) 21%,
    transparent 30%);
  filter:blur(6px);
}
.cs-mr[data-mat-reveal="forge"].is-in > img,
.cs-mr[data-mat-reveal="forge"].is-in > svg:not(.cs-mr-thread){
  animation:cs-mr-cool 2.5s cubic-bezier(.22,.61,.36,1) forwards;
}
/* Cooling: it comes up crushed and warm and settles to the real photograph.
   It ends on filter:none rather than on a value, so the last frame is the
   untouched picture and nothing is left permanently tinted. */
@keyframes cs-mr-cool{
  0%  {opacity:0;   filter:brightness(.30) contrast(1.5) sepia(.55) saturate(1.5)}
  22% {opacity:.55; filter:brightness(.62) contrast(1.34) sepia(.42) saturate(1.35)}
  62% {opacity:1;   filter:brightness(.92) contrast(1.12) sepia(.14) saturate(1.1)}
  100%{opacity:1;   filter:none}
}

/* Embers. Seven, one to two pixels, gone in under two seconds and never
   repeated. They rise behind the edge — in the part that has already been
   worked — which is where sparks come off a bar, and they are the only thing
   on this page that is allowed to look hot. */
.cs-mr-ember,.cs-mr-mote{
  position:absolute;left:var(--x);top:var(--y);
  width:var(--s);height:var(--s);border-radius:50%;
  opacity:0;pointer-events:none;z-index:1;
}
.cs-mr-ember{
  background:var(--cs-m-heat);
  box-shadow:0 0 5px 1px rgba(255,154,74,.55);
}
.cs-mr.is-in .cs-mr-ember{
  animation:cs-mr-spark 1.5s ease-out var(--d) forwards;
}
@keyframes cs-mr-spark{
  0%  {opacity:0;   transform:translate3d(0,0,0) scale(.6)}
  18% {opacity:.72; transform:translate3d(calc(var(--dx) * .25),calc(var(--rise) * -.2),0) scale(1)}
  70% {opacity:.34}
  100%{opacity:0;   transform:translate3d(var(--dx),calc(var(--rise) * -1),0) scale(.5)}
}

/* ------------------------------------------------------------ 02. STITCH
   The lace. A line of stitches crosses the plate and the lace is there behind
   it. The thread is one pixel of the dress's own ivory, the stitches are a
   dash pattern on that line, and the bright point at the front is the only
   thing standing in for a needle. There is no needle. */
.cs-mr[data-mat-reveal="stitch"] .cs-mr-edge::after{
  /* the point of the work: a small, soft ivory glow, nothing more */
  background:radial-gradient(9px 9px at 18% 64%,
    rgba(255,255,255,.9) 0%, rgba(221,215,208,.45) 42%, transparent 100%);
}
.cs-mr[data-mat-reveal="stitch"].is-in > img{
  animation:cs-mr-settle 2.2s cubic-bezier(.22,.61,.36,1) forwards;
}
@keyframes cs-mr-settle{
  0%  {opacity:0;  transform:scale(1.035)}
  30% {opacity:1}
  100%{opacity:1;  transform:scale(1)}
}

/* The thread. Drawn by its own dash offset at the same rate the edge travels,
   so the stitches are always AT the frontier and never running ahead of what
   has been revealed. It fades once it has crossed: what should be left on
   screen is the lace, not a line over it. */
.cs-mr-thread{
  position:absolute;inset:0;width:100%;height:100%;
  z-index:3;pointer-events:none;overflow:visible;
}
.cs-mr-thread path{
  fill:none;stroke:#FFFDF8;stroke-width:1.3;
  stroke-linecap:round;
  stroke-dasharray:5.5 4.5;          /* actual stitches, not a solid line */
  opacity:0;
}
/* Ivory thread on ivory lace is invisible, which is exactly what happened the
   first time. The cord laid into the real ring reads for one reason — it is
   raised and it drops a shadow — so the thread gets the same. Not an outline,
   which would look drawn on; a soft shadow under it, which looks like it is
   sitting on the fabric. */
.cs-mr-thread{filter:drop-shadow(0 1px 1.6px rgba(24,18,12,.6))}
.cs-mr.is-in .cs-mr-thread path{
  animation:cs-mr-stitch 2.05s cubic-bezier(.42,.06,.22,1) forwards;
}
/* Revealed left to right at the same rate as the edge, because the path
   already spends its dash pattern on BEING stitches and one path cannot do
   both that and a draw-on.

   clip-path, not a mask. The first attempt masked with
   `linear-gradient(90deg,#000 0 0)` at an animated mask-size — a degenerate
   gradient (two stops at the same position) behind a property that does not
   interpolate the way it looks like it should. The result was an svg masked
   out completely: the bright point at the frontier appeared, the stitches
   behind it never did, and nothing errored. clip-path inset is animatable,
   supported everywhere, and clips without distorting the line. */
.cs-mr-thread{clip-path:inset(0 100% 0 0)}
.cs-mr.is-in .cs-mr-thread{
  animation:cs-mr-thread-in 2.05s cubic-bezier(.42,.06,.22,1) forwards;
}
@keyframes cs-mr-thread-in{
  from{clip-path:inset(0 100% 0 0)}
  to  {clip-path:inset(0 0% 0 0)}
}
@keyframes cs-mr-stitch{
  0%  {opacity:0}
  12% {opacity:.92}
  74% {opacity:.92}
  100%{opacity:0}
}
.cs-mr.is-done .cs-mr-thread,
.cs-mr.is-still .cs-mr-thread{display:none}

/* ------------------------------------------------------------- 03. GRAIN
   Redwood. It comes up out of the dark, out of focus, and resolves. No
   particles worth the name — five motes of dust, drifting, barely there. */
.cs-mr[data-mat-reveal="grain"] .cs-mr-edge::after{
  background:linear-gradient(90deg,
    transparent 0%,
    rgba(198,170,149,.10) 10%,
    rgba(198,170,149,.34) 17%,
    rgba(232,214,198,.46) 18.6%,
    rgba(198,170,149,.16) 22%,
    transparent 32%);
  filter:blur(7px);
}
.cs-mr[data-mat-reveal="grain"].is-in > img,
.cs-mr[data-mat-reveal="grain"].is-in > svg:not(.cs-mr-thread){
  animation:cs-mr-resolve 2.4s cubic-bezier(.22,.61,.36,1) forwards;
}
@keyframes cs-mr-resolve{
  0%  {opacity:0;   filter:blur(13px) brightness(.28) saturate(.55); transform:scale(1.06)}
  26% {opacity:.7;  filter:blur(7px)  brightness(.55) saturate(.8);  transform:scale(1.04)}
  68% {opacity:1;   filter:blur(1.6px) brightness(.9) saturate(1);   transform:scale(1.01)}
  100%{opacity:1;   filter:none;                                     transform:scale(1)}
}
.cs-mr-mote{
  background:var(--cs-m-wood-lit);
  box-shadow:0 0 4px 1px rgba(198,170,149,.35);
}
.cs-mr.is-in .cs-mr-mote{
  animation:cs-mr-drift 2.3s ease-out var(--d) forwards;
}
@keyframes cs-mr-drift{
  0%  {opacity:0;   transform:translate3d(0,0,0)}
  22% {opacity:.5}
  72% {opacity:.26}
  100%{opacity:0;   transform:translate3d(var(--dx),var(--dy),0)}
}

/* ------------------------------------------------------------------ REST
   When the run is over there is nothing left on the plate but the material.
   Every piece of dressing is removed from the box rather than left at zero
   opacity, so nothing is still being composited after the animation. */
.cs-mr.is-done .cs-mr-ember,
.cs-mr.is-done .cs-mr-mote,
.cs-mr.is-still .cs-mr-ember,
.cs-mr.is-still .cs-mr-mote{display:none}

/* REDUCED MOTION. No edge, no embers, no dust, no travel, no scale — and
   still not nothing. The rule is no large or vestibular movement, not a page
   that appears broken, so the material fades up in place over a second and a
   half and that is the whole of it. material-reveal.js inserts no dressing at
   all in this mode; this block is what makes the fade happen and what catches
   the case where the setting changes after the page has loaded. */
.cs-mr.is-still > img,
.cs-mr.is-still > svg:not(.cs-mr-thread){
  opacity:0;transition:opacity 1.5s cubic-bezier(.22,.61,.36,1);
  filter:none;transform:none;animation:none;
}
.cs-mr.is-still.is-in > img,
.cs-mr.is-still.is-in > svg:not(.cs-mr-thread){opacity:1}

@media (prefers-reduced-motion:reduce){
  .cs-mr > img,.cs-mr > svg:not(.cs-mr-thread){
    filter:none !important;transform:none !important;animation:none !important;
  }
  .cs-mr:not(.is-still) > img,
  .cs-mr:not(.is-still) > svg:not(.cs-mr-thread){opacity:1 !important}
  .cs-mr-edge,.cs-mr-ember,.cs-mr-mote,.cs-mr-thread{display:none !important}
}
.cs-mat-n{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;color:var(--text-faint);
  display:block;margin-bottom:9px;
}
.cs-mat h3{
  font-size:clamp(20px,2.1vw,27px);line-height:1.06;letter-spacing:-.028em;margin:0;
}
.cs-mat-role{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--cs-wood);margin:9px 0 0;
}
.cs-mat p.cs-mat-body{
  font-size:14.5px;line-height:1.6;color:var(--text-soft);margin:12px 0 0;text-wrap:pretty;
}

/* --------------------------------------------------------------- 4. TEXTURE
   The lace photograph, full width, one line over it. Capped so a portrait
   photograph used as a band never eats the whole screen. */
.cs-tex{position:relative;overflow:hidden;background:var(--ink)}
.cs-tex img{
  display:block;width:100%;height:clamp(190px,30vh,300px);object-fit:cover;
  object-position:center 42%;
}
.cs-tex-in{
  position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;
  padding:clamp(22px,4vw,54px);
  background:linear-gradient(to top,rgba(24,18,12,.62),rgba(24,18,12,.12) 52%,rgba(24,18,12,0));
}
.cs-tex-in .cs-kick{color:var(--cs-ivory);opacity:.85;margin-bottom:10px}
.cs-tex-line{
  font-family:var(--display);font-weight:700;color:#fff;
  font-size:clamp(22px,3vw,38px);line-height:1.05;letter-spacing:-.03em;margin:0;
  text-shadow:0 1px 24px rgba(24,18,12,.5);
}

/* -------------------------------------------------------------- 5. TIMELINE
   A drafting dimension line, because that is what this site already draws
   everywhere else. Horizontal with tick ends on a wide screen, vertical on a
   phone, and the rule itself draws in when it arrives. */
.cs-tl{margin-top:clamp(30px,4vw,52px)}

/* A dimension line: a hairline with a tick at each end and a station at each
   date. Drawn in CSS rather than in a stretched SVG, because a viewBox scaled
   to 1180px turns every circle into an oval and every tick into a wedge. It
   scales in from the left when the block arrives, which is the same gesture
   the leader lines elsewhere on the site make. */
.cs-tl-rule{
  position:relative;height:1px;background:var(--draft);
  transform:scaleX(0);transform-origin:left center;
  transition:transform 1.15s var(--ease-out) .1s;
}
.rv.in .cs-tl-rule{transform:none}
.cs-tl-rule::before,.cs-tl-rule::after{
  content:"";position:absolute;top:-6px;width:1px;height:13px;background:var(--draft-strong);
}
.cs-tl-rule::before{left:0}
.cs-tl-rule::after{right:0}

.cs-tl-stops{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(16px,3vw,40px);margin:0;padding:0;list-style:none;
}
.cs-tl-stop{position:relative;padding-top:26px}
/* the station sits on the rule, above its own date */
.cs-tl-stop::before{
  content:"";position:absolute;top:-4.5px;width:9px;height:9px;border-radius:50%;
  background:var(--paper);border:1px solid var(--draft-strong);
  opacity:0;transition:opacity .5s var(--ease) .6s;
}
.rv.in .cs-tl-stop::before{opacity:1}
.cs-tl-stop:nth-child(1)::before{left:-1px}
.cs-tl-stop:nth-child(2)::before{left:50%;margin-left:-4.5px}
.cs-tl-stop:nth-child(3)::before{right:-1px}
.cs-tl-stop:nth-child(2){text-align:center}
.cs-tl-stop:nth-child(3){text-align:right}
.cs-tl-l{
  font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--text-faint);display:block;margin-bottom:9px;
}
.cs-tl-d{
  font-family:var(--display);font-weight:700;font-size:clamp(16px,1.9vw,24px);
  letter-spacing:-.02em;line-height:1.15;display:block;
}
.cs-tl-note{
  font-size:13.5px;line-height:1.5;color:var(--text-soft);margin:10px 0 0;max-width:26ch;
}
.cs-tl-stop:nth-child(2) .cs-tl-note{margin-inline:auto}
.cs-tl-stop:nth-child(3) .cs-tl-note{margin-left:auto}

/* ---------------------------------------------------------------- 6. REVEAL
   Concept on the left of the wipe, the finished photograph on the right, the
   visitor moves the line. --rv is the single source of position: the clip,
   the handle and both labels all read from it.

   The two photographs are 0.5628 and 0.5632 wide to tall, so they sit in one
   frame with no crop at all and nothing shifts as the line travels. */
.cs-rv{
  --rv:.78;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  /* The frame is taller than the heading, and it spans both rows. Letting
     both rows be `auto` split that extra height between them and left the
     caption stranded at the bottom of the column; giving row two the slack
     keeps the caption directly under the heading where it belongs. */
  grid-template-rows:auto 1fr;
  gap:0 clamp(26px,4.5vw,80px);align-items:center;
}
/* The frame spans both rows of the left column, so the caption can sit under
   the heading on a wide screen and still fall BELOW the picture when the
   columns stack. A caption describing a photograph the visitor has not seen
   yet, printed above it, is the one order that makes no sense. */
.cs-rv-copy{grid-area:1 / 1;align-self:end}
.cs-rv-frame{grid-area:1 / 2 / span 2 / 3;align-self:center}
.cs-rv-cap{grid-area:2 / 1;align-self:start}
.cs-rv-copy h2{
  font-size:clamp(28px,4.2vw,54px);letter-spacing:-.032em;line-height:1.02;
  margin:0;max-width:11ch;
}
.cs-rv-frame{
  position:relative;margin-inline:auto;
  aspect-ratio:941/1672;
  height:min(66vh,600px);width:auto;max-width:min(88vw,400px);
  border-radius:var(--radius-lg);overflow:hidden;
  background:var(--ink-2);border:1px solid var(--cs-rule-ink);
  touch-action:pan-y;                  /* let the page still scroll on a phone */
}
.cs-rv-frame img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  user-select:none;-webkit-user-drag:none;
}
/* The finished ring sits on top and is clipped back to the line. */
.cs-rv-after{clip-path:inset(0 0 0 calc(var(--rv) * 100%))}

/* The line itself, and the grab. */
.cs-rv-bar{
  position:absolute;top:0;bottom:0;left:calc(var(--rv) * 100%);width:1px;
  background:rgba(255,255,255,.85);transform:translateX(-.5px);pointer-events:none;
  box-shadow:0 0 18px rgba(0,0,0,.45);
}
.cs-rv-knob{
  position:absolute;top:50%;left:calc(var(--rv) * 100%);
  width:46px;height:46px;margin:-23px 0 0 -23px;border-radius:50%;
  border:1px solid rgba(255,255,255,.9);background:rgba(12,12,14,.42);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  display:grid;place-items:center;pointer-events:none;
  transition:transform var(--t-fast) var(--ease),background var(--t-fast) var(--ease);
}
.cs-rv-knob svg{width:22px;height:22px;stroke:#fff;fill:none;stroke-width:1.5}
.cs-rv:hover .cs-rv-knob{transform:scale(1.06)}

/* The real control. Invisible, sits over the whole frame, and is a genuine
   range input — so it drags with a mouse, drags with a thumb, takes arrow
   keys, and announces itself. Faking this with a div loses all four. */
.cs-rv-input{
  position:absolute;inset:0;width:100%;height:100%;margin:0;
  opacity:0;cursor:ew-resize;-webkit-appearance:none;appearance:none;background:none;
}
.cs-rv-input::-webkit-slider-thumb{-webkit-appearance:none;width:46px;height:100%}
.cs-rv-input::-moz-range-thumb{width:46px;height:100%;border:0;background:none}
.cs-rv-input:focus-visible{opacity:1;outline:2px solid var(--accent-lit);outline-offset:3px}
.cs-rv-input:focus-visible::-webkit-slider-thumb{background:rgba(var(--accent-lit-rgb),.35)}

/* The labels sit ON the picture, over the half each one names, so they report
   which side is which instead of floating underneath unattached. Each fades
   as its own half is given up. */
.cs-rv-tag{
  position:absolute;top:12px;z-index:2;pointer-events:none;
  font-family:var(--mono);font-size:9.5px;letter-spacing:.22em;text-transform:uppercase;
  padding:6px 10px;border-radius:100px;
  background:rgba(12,12,14,.5);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);
  color:rgba(242,241,238,.5);
  transition:color var(--t-mid) var(--ease),opacity var(--t-mid) var(--ease);
}
.cs-rv-tag-a{left:12px}
.cs-rv-tag-b{right:12px}
.cs-rv-tag.on{color:#fff}
.cs-rv-hint{
  margin:16px 0 0;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--on-ink-faint);
  transition:opacity var(--t-mid) var(--ease);
}
.cs-rv.moved .cs-rv-hint{opacity:0}
.cs-rv-cap{
  margin:clamp(18px,2.2vw,26px) 0 0;max-width:38ch;
  font-size:clamp(14.5px,1.1vw,16.5px);line-height:1.6;color:var(--on-ink-soft);
}

/* --------------------------------------------------------- THE LAST PICTURE
   Full width, under the call to action, nothing over it. The page has said
   everything by this point, so the last thing on it is the object. */
.cs-close{
  background:var(--ink);overflow:hidden;
  padding-block:clamp(34px,4.4vw,68px);
  display:flex;justify-content:center;
}
/* Shown whole, not cropped into a band. Jake framed this one: the ring sits
   a third of the way down with the light coming through behind it, and any
   wide crop that fits a band cuts the ring in half. Sized by height so it
   behaves the same on a phone and a desktop. */
.cs-close img{
  display:block;aspect-ratio:941/1672;
  height:min(70vh,660px);width:auto;max-width:88vw;
  object-fit:cover;border-radius:var(--radius-lg);
}

/* ------------------------------------------------------------- 8. REACTIONS
   Her words, then his. Two moments with air between them, never a pair of
   testimonial cards side by side. */
.cs-react{max-width:880px;margin-inline:auto}
.cs-react + .cs-react{
  margin-top:clamp(30px,4vw,56px);padding-top:clamp(28px,3.6vw,48px);
  border-top:1px solid var(--cs-rule-ink);
}
.cs-react-when{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--on-ink-faint);margin:0 0 clamp(14px,1.8vw,20px);
}
.cs-react blockquote{
  margin:0;font-family:var(--display);font-weight:700;
  font-size:clamp(22px,3.2vw,42px);line-height:1.14;letter-spacing:-.028em;
  color:var(--on-ink);text-wrap:pretty;
}
/* The opening mark hangs into the margin, so the first letter still lines up
   with everything above it. */
.cs-react blockquote::before{content:"\201C";margin-left:-.42em}

/* -------------------------------------------------------------------- 9. CTA */
.cs-cta{padding-block:clamp(38px,4.6vw,70px)}
.cs-cta h2{
  font-size:clamp(26px,3.8vw,48px);line-height:1.04;letter-spacing:-.032em;
  margin:0 0 18px;max-width:18ch;text-wrap:balance;
}
.cs-cta p{font-size:clamp(15.5px,1.25vw,18px);line-height:1.62;color:var(--text-soft);
  max-width:46ch;margin:0 0 clamp(20px,2.6vw,30px);text-wrap:pretty}
.cs-cta-btns{display:flex;gap:12px;flex-wrap:wrap}
.cs-cta .btn{min-height:52px}

/* Entrances use the site's own .rv class and its observer. Nothing to add. */

/* ------------------------------------------------------------------- NARROW */
@media (max-width:900px){
  .cs-duo,.cs-duo-art{grid-template-columns:1fr;gap:clamp(24px,5vw,40px)}
  .cs-open-in{grid-template-columns:1fr;gap:clamp(16px,3vw,26px);align-items:start}

  /* THE REVEAL COLLAPSES. One column, and the grid areas released so the
     three parts fall in the order they are written: heading, picture,
     caption. Leaving the areas set kept the two-column placement and pushed
     the frame clean off the right of a phone. */
  .cs-rv{grid-template-columns:1fr;grid-template-rows:auto;gap:clamp(18px,4vw,28px)}
  .cs-rv-copy,.cs-rv-frame,.cs-rv-cap{grid-area:auto;align-self:auto}
  .cs-rv-cap{margin-top:0}

  /* The dimension line stands up. Three stops down the page on one vertical
     rule, which is the only shape that survives 390px: a horizontal line with
     three dates hung off it turns into three columns of two words each. */
  .cs-tl-rule{display:none}
  .cs-tl-stops{grid-template-columns:1fr;gap:0}
  /* Matching the nth-child specificity of the wide rules, rather than
     reaching for !important to out-shout them. */
  .cs-tl-stop,
  .cs-tl-stop:nth-child(2),
  .cs-tl-stop:nth-child(3){
    text-align:left;position:relative;
    padding:0 0 clamp(26px,5vw,38px) 26px;border-left:1px solid var(--cs-rule);
  }
  .cs-tl-stop:last-child{padding-bottom:0;border-left-color:transparent}
  /* one rule for all three: the nth-child placements above are undone here */
  .cs-tl-stop:nth-child(1)::before,
  .cs-tl-stop:nth-child(2)::before,
  .cs-tl-stop:nth-child(3)::before{
    content:"";position:absolute;left:-4.5px;right:auto;top:3px;
    width:8px;height:8px;margin:0;
    border-radius:50%;background:var(--paper);border:1px solid var(--draft-strong);
    opacity:1;
  }
  .cs-tl-note,.cs-tl-stop:nth-child(2) .cs-tl-note,.cs-tl-stop:nth-child(3) .cs-tl-note{
    margin-left:0;margin-right:0;
  }
  .cs-react blockquote::before{margin-left:0}
}

/* Below this the three-across becomes three stacked, and the plate goes back
   ABOVE its own paragraph rather than beside it.

   It used to be a row — a 94px thumbnail next to the text — and that was the
   right answer for three line drawings, because it held the section to a third
   of the height. It is the wrong answer now. The plate IS the material, and
   94x75 cannot carry a reveal: the stitches are two pixels of thread, the
   embers are invisible, and the one audience most likely to see this page —
   somebody who tapped through from Instagram on a phone — gets the version
   with nothing in it.

   So the plate is full width and 16:10 rather than 5:4: cinematic, and about
   220px each rather than the 280 a 5:4 would cost. The section grows by
   roughly half a screen in total. That is the price of the effect existing on
   a phone at all, and it is worth paying. */
@media (max-width:700px){
  .cs-mats{grid-template-columns:1fr;gap:clamp(22px,5vw,30px)}
  .cs-mat{
    flex-direction:column;align-items:stretch;gap:14px;
    padding-top:clamp(18px,4vw,24px);border-top:1px solid var(--cs-rule);
  }
  .cs-mat:first-child{padding-top:0;border-top:0}
  .cs-mat-art{width:100%;aspect-ratio:16/10}
  .cs-mat-txt{min-width:0}
  .cs-mat-n{margin-bottom:6px}
  .cs-mat p.cs-mat-body{font-size:14.5px;margin-top:9px}
}

@media (max-width:520px){
  .cs-rv-frame{height:min(62vh,480px)}
  .cs-facts{gap:8px 18px}
}

/* ------------------------------------------------- THE RAIL AFTER THE PIECE
   Three other projects, in one row that scrolls sideways. Stacked, they were
   2200px of tail on a phone: more scrolling after the story ended than the
   story itself had. Native overflow scrolling, no script, and it snaps.

   Scoped to .cs-more, which only the case-study branch of project.html adds,
   so the standard project page keeps the wrapped grid it has always had. */
.cs-more .pf{overflow:visible}
.cs-more .pf-track{
  display:flex;gap:clamp(12px,1.8vw,22px);
  overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x proximity;
  -webkit-overflow-scrolling:touch;
  padding-bottom:8px;
}
.cs-more .pf-card{
  flex:0 0 clamp(228px,28vw,300px);width:auto;scroll-snap-align:start;
}
.cs-more .pf-card .pf-art{aspect-ratio:4/3}
/* The description is what makes these cards tall, and a link at the foot of a
   page somebody has already read needs a title and its materials, not a
   paragraph. */
.cs-more .pf-card .pf-body p{display:none}
.cs-more{padding-block:clamp(38px,4.6vw,72px)}

/* ============================================================================
   THE FILM
   ----------------------------------------------------------------------------
   A clip of the ring, shown at the shape it was shot in rather than forced
   into the full-bleed texture band — those clips are portrait, and a portrait
   frame cover-cropped into a 300px strip is a sliver of the middle of it.

   Copy on the left, clip on the right, the same arrangement as the reveal, so
   the two sections read as siblings rather than two different ideas.
   ========================================================================== */
.cs-film{padding:clamp(56px,7vw,110px) 0}
.cs-film-in{display:grid;grid-template-columns:minmax(0,1fr) auto;
            gap:clamp(24px,5vw,72px);align-items:center}
.cs-film-copy h2{
  font-size:clamp(26px,3.8vw,46px);letter-spacing:-.03em;line-height:1.04;
  margin:10px 0 0;max-width:12ch;color:var(--cs-ivory);
}
.cs-film-body{margin:18px 0 0;max-width:42ch;color:var(--cs-ivory);opacity:.72;
              font-size:15px;line-height:1.7}

.cs-film-frame{
  position:relative;overflow:hidden;border-radius:var(--radius-lg);
  aspect-ratio:1244/1664;
  height:min(64vh,580px);width:auto;max-width:min(88vw,400px);
  background:#0A0A0C;
  box-shadow:0 30px 70px rgba(0,0,0,.5);
}
.cs-film-frame video{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block;
}

@media (max-width:860px){
  .cs-film-in{grid-template-columns:1fr;justify-items:start}
  .cs-film-frame{height:auto;width:100%;max-width:min(94vw,420px);
                 margin-inline:auto}
  .cs-film-copy h2{max-width:none}
}

/* Reduced motion is handled in the renderer, not here: `display:none` on the
   video also hides its poster, which left an empty black box — the opposite
   of the intent. case-study.js withholds `autoplay` and `loop` instead, so
   the element stays, the poster frame shows, and the visitor can press play
   themselves if they want it. */
