/* ===================================================================
   OJI STORY — case studies · "The Press"

   A printed dossier being inked and assembled as you scroll. Built on the
   tokens in styles.css (--cream/--ink/--blue/--display/--ease…) rather than
   the raw hexes the old Figma port carried, so a case page re-skins per
   project from one custom property: --accent.

   Every animated rule is followed by its own prefers-reduced-motion block,
   matching the convention used throughout styles.css.
   =================================================================== */

/* ---------- page shell ---------- */
.case-page {
  background: var(--cream);
  color: var(--ink);
}

/* (Cross-document view transitions are declared site-wide in styles.css —
   both documents must opt in, so it can't live only here.) */

/* Full-bleed grid: content sits in the middle track, bands span edge to edge.
   .cs-poster repeats the same tracks so its media can go full while its
   headline block stays in the content column. */
/* --csrail reserves the strip the FIXED .cs-rail widget occupies (the rotated
   project name, progress line and chapter dots pinned to the left edge).
   Without it, content in the left-hand track slides underneath that widget —
   which is exactly what happened to the category label. It only binds when
   space is tight, since the 1fr gutters are already wider on big screens, and
   it drops to 0 below 980px where the rail becomes a top progress bar. */
.casestudy,
.cs-poster {
  --csrail: 0px;
  display: grid;
  grid-template-columns:
    [full-start] minmax(calc(var(--gutter) + var(--csrail)), 1fr)
    [content-start] minmax(0, var(--maxw))
    [content-end] minmax(var(--gutter), 1fr)
    [full-end];
}
/* The rail's right edge measures ~119–127px across these widths (it is
   `left: clamp(14px,2vw,28px)` plus the vertical name, the progress line and
   the dot column). Content must clear that at EVERY width, not just at the
   top of the page: the rail is fixed at viewport centre, so any block whose
   horizontal box overlaps the rail's will collide with it at some scroll
   position — which is how the category label ended up printed over it. */
@media (min-width: 981px) {
  .casestudy, .cs-poster { --csrail: clamp(96px, 8vw, 120px); }
}
.casestudy {
  --accent: var(--blue);
  --accent-ink: var(--cream);
  position: relative;

  /* THE PAGE GRID.
     The content track used to be the site-wide 1280px, but a readable line of
     prose is only ~650px — so at 1600–1920px every paragraph stranded 575 to
     990px of bare cream down its right-hand side. That is the "text is here
     and there" problem: the column was left-aligned inside a track far wider
     than it, and each block stranded a different amount.

     The track is now sized TO the measure and centred instead. Prose fills its
     column, every block shares one left edge, and the leftover space becomes
     symmetric page margin. The full-bleed media (bands, bento, the press) is
     what uses the whole screen — that contrast is the layout. */
  --maxw: 920px;
}
.cs-railgrid,
.cs-sec,
.cs-poster__head {
  display: grid;
  /* one column; the line names are kept so blocks can still address
     main-start / main-end / tail-end without every rule needing to change */
  grid-template-columns: [main-start] minmax(0, 1fr) [main-end tail-end];
  column-gap: clamp(16px, 2.5vw, 48px);
  align-items: start;
}
.casestudy > *,
.cs-poster > * { grid-column: content; }
.casestudy > .cs-full,
.cs-poster > .cs-full { grid-column: full; }

/* NOTE: deliberately no `content-visibility: auto` here. It skips rendering
   for off-screen subtrees, which stops the IntersectionObserver reveal from
   ever firing on a fast scroll — leaving [data-anim] elements stuck at
   opacity:0, i.e. invisible content. The WebP pass already cut these pages
   from ~215 MB to ~12 MB, so the perf it would buy isn't worth that risk. */

/* ---------- breadcrumb ---------- */
.case-crumb {
  grid-column: content;
  padding: 104px 0 0;
  font-family: var(--sans);
  font-weight: 600;
  font-size: .875rem;
  letter-spacing: .01em;
  color: var(--blue);
}
.case-crumb a { color: var(--blue); text-decoration: none; }
.case-crumb a:hover { text-decoration: underline; }
.case-crumb .sep { color: var(--yellow); margin: 0 .5em; }
.case-crumb .cur { opacity: .65; }
/* On a phone this was a 43x19 target — the smallest thing on the page and the
   one that takes you back. The negative margin cancels the padding, so the
   hit area grows to ~43px tall without moving anything. */
@media (max-width: 760px) {
  .case-crumb a { display: inline-block; padding: 12px 6px; margin: -12px -6px; }
}

/* ---------- registration marks ----------
   Printer's crop marks in the corners — the page as a press sheet. */
.cs-regmarks {
  position: fixed;
  inset: 14px;
  pointer-events: none;
  z-index: 3;
  opacity: .5;
}
.cs-regmarks i {
  position: absolute;
  width: 14px; height: 14px;
  border: 0 solid var(--ink);
}
.cs-regmarks i:nth-child(1) { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; }
.cs-regmarks i:nth-child(2) { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; }
.cs-regmarks i:nth-child(3) { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; }
.cs-regmarks i:nth-child(4) { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; }
@media (max-width: 760px) { .cs-regmarks { display: none; } }

/* ---------- POSTER ---------- */
.cs-poster { grid-column: full; position: relative; padding-bottom: clamp(40px, 6vw, 80px); }
/* Hero. It used to be a fixed 44–60vh full-bleed strip with object-fit:cover,
   which on a wide screen is a ~2.7:1 letterbox holding a 4:3 photograph — it
   threw away roughly half of every hero image. The frame now takes the
   image's own proportions (the width/height attributes give the ratio), so
   nothing is cropped, and it's capped so a tall image can't eat the screen. */
/* The hero used to be `width: fit-content; margin: 0 auto` inside the 1280px
   content track — a framed picture floating in the middle of the page with
   bare cream down both sides, the same problem the plates had. It's now a
   full-bleed opening frame: edge to edge, a fixed cinematic ratio, and the
   picture cropped to fill it rather than the frame shrinking to the picture.
   Pages that need a different crop already carry inline object-position. */
/* Same two rules as the bands: never cropped, never taller than the screen.
   The hero is a mounted plate on an accent mat, not a cropped letterbox. */
.cs-poster__media {
  grid-column: full;
  position: relative;
  width: 100%;
  margin: 0 0 clamp(30px, 4.5vw, 60px);
  padding: clamp(16px, 2.6vw, 44px) var(--gutter);
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--cream-3);   /* fallback where color-mix is unsupported */
  background: color-mix(in srgb, var(--accent) 14%, var(--cream-3));
}
/* No fixed height and no object-fit: the width/height attributes give the
   frame the picture's own proportions, so the whole image is visible —
   nothing cropped away and no letterbox bars either. (The poster's parallax
   attribute is removed in the markup to match; drifting the image inside a
   frame it exactly fills would just expose the background behind it.) */
/* `.casestudy` scopes this so it outranks the global `--img-cap` catch-all
   further down (equal specificity would lose on source order, and the image
   then rendered 33px shorter than its frame, showing a cream strip). */
.casestudy .cs-poster__media img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: var(--img-cap);   /* whole hero visible without scrolling */
  object-fit: contain;          /* never crop */
  border: var(--border);
  border-radius: clamp(10px, 1.2vw, 18px);
  box-shadow: var(--shadow-card);
}
/* Masthead. The title used to live inside the left-hand half-column, which
   meant a long single-word project name ("GREENVIRONMENT") rendered wider
   than its track and printed straight over the lede — 69px of overlap at
   1440px. The title now gets its own full-width row and the meta sits beneath
   it, so the headline has the whole content measure to work with at any
   length. `display: contents` promotes the two wrapper <div>s' children to
   grid items, so this needs no markup change. */
/* The masthead runs on the shared page grid, so the name, lede and chips all
   start on the same line as every paragraph further down. The category reads
   as an eyebrow above the name — it can't live in a side column, because the
   fixed .cs-rail owns that strip. */
.cs-poster__head { row-gap: clamp(10px, 1.4vw, 20px); }
.cs-poster__head > div { display: contents; }
.cs-cat   { grid-column: main-start / tail-end; grid-row: 1; }
.cs-name  { grid-column: main-start / tail-end; grid-row: 2; }
.cs-poster__head .cs-lede  { grid-column: main-start / main-end; grid-row: 3; }
.cs-poster__head .cs-offer { grid-column: main-start / main-end; grid-row: 4; }
@media (max-width: 900px) {
  .cs-poster__head { grid-template-columns: minmax(0, 1fr); }
  .cs-name, .cs-cat,
  .cs-poster__head .cs-lede,
  .cs-poster__head .cs-offer { grid-column: 1 / -1; grid-row: auto; }
}

.cs-name {
  font-family: var(--display);
  /* cqi-free fluid size that still fits the longest name we ship; the
     safety net below means an even longer one wraps instead of overflowing */
  font-size: clamp(2rem, 5.6vw, 4.4rem);
  line-height: .96;
  color: var(--blue);
  margin: 0 0 .1em;
  text-wrap: balance;
  overflow-wrap: anywhere;
}
/* inline-flex made each word unbreakable — that is what let a long name run
   past its column. Allowing the per-character spans to wrap keeps the
   deal-out animation and makes overflow impossible at any width. */
.cs-name__w { display: inline-flex; flex-wrap: wrap; }
.cs-name__sp { display: inline-block; }
.cs-name__c { display: inline-block; overflow: hidden; }
.cs-name__c > span {
  display: inline-block;
  transform: translate3d(var(--dx, 0), 105%, 0) rotate(var(--dr, 0));
  opacity: 0;
  transition: transform .78s var(--ease), opacity .5s var(--ease);
}
.cs-name.is-in .cs-name__c > span { transform: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .cs-name__c > span { transform: none; opacity: 1; transition: none; }
}

/* An eyebrow above the project name. */
.cs-cat {
  font-family: var(--sans);
  font-size: clamp(.68rem, .9vw, .78rem);
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--accent);
  margin: 0;
}
.cs-lede {
  font-family: var(--sans);
  font-size: clamp(1rem, 1.35vw, 1.2rem);
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0;
  max-width: none;
}

/* offering chips — the hard-shadow pill, straight from the house kit */
.cs-offer { margin-top: clamp(18px, 2.4vw, 30px); }
.cs-offer__label {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.35rem;
  margin-bottom: .5em;
}
.cs-chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 12px; }
.cs-chips li {
  background: var(--blue);
  color: var(--cream);
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(.95rem, 1.2vw, 1.15rem);
  padding: .62em 1.3em;
  border-radius: var(--radius-pill);
  border: var(--border);
  box-shadow: 5px 6px 0 rgba(28, 20, 10, .85);
}

/* ---------- VIEWPORT-FIT IMAGERY ----------
   Hard rule for these pages: no image is ever taller than the screen it is
   being viewed on. You should never have to scroll to see one picture.

   Before this, 44 images across 8 case pages rendered taller than the
   viewport — a full-bleed band was `width: 100%` at the file's own
   proportions, so a square 1642px export became a 1642px-tall wall and
   bombasi's band1 rendered at 274% of the screen.

   The cap is applied by HEIGHT, and the picture keeps its own proportions,
   so nothing is cropped to achieve it. A wide image still fills the band
   edge to edge; a tall one is centred on the band's tinted ground and reads
   as a plate laid on colour. That tinted ground is what makes the shape
   change look deliberate rather than like a gap. */
/* Raised from 74svh/720px. Because nothing may be cropped, a picture that is
   capped shorter is also narrower — so a low cap directly widens the mat
   either side of it. 84svh still shows any single image whole without
   scrolling (the mat's padding is inside this budget) while letting the
   picture run much closer to the full width of the page. */
:root { --img-cap: min(84svh, 840px); }

/* Catch-all safety net. The rules below cap each image type deliberately;
   this makes it impossible for a new one added later — in a wrapper nobody
   thought to style — to render taller than the screen. */
.casestudy img,
.cs-poster img { max-height: var(--img-cap); }

/* ---------- bands & grids ---------- */
.cs-band {
  margin: 0;
  overflow: hidden;
  background: var(--cream-3);
}
/* TWO RULES, held together:
     1. nothing is ever cropped — no object-fit: cover anywhere;
     2. nothing is ever taller than the screen — you never scroll to see one
        picture whole.

   Those two together mean a portrait photo cannot also be full-bleed: capped
   to the viewport height it is simply narrower than the page. Letting that
   leftover be bare cream is what read as "images floating in the middle", so
   the band's ground is an accent-tinted mat instead. The picture keeps its own
   proportions, sits at its full height, and the mat reads as a deliberate
   mount rather than as a gap. A landscape image still fills the width
   completely and shows no mat at all. */
.cs-band {
  width: 100%;
  display: grid;
  place-items: center;
  padding: clamp(16px, 2.6vw, 44px) var(--gutter);
  background: var(--cream-3);   /* fallback where color-mix is unsupported */
  background: color-mix(in srgb, var(--accent) 14%, var(--cream-3));
}
/* Mounted like a plate: the mat is always present and always deliberate, so a
   portrait picture and a landscape one read the same way instead of one of
   them looking like it failed to fill the space. Same border and shadow as
   every other card on the site. */
/* Newer imagery ships as <picture> (AVIF with a WebP fallback), so every rule
   that used to reach the image with a single `>` now has to step over the
   wrapper too. `picture { display: contents }` below keeps it out of layout;
   these paired selectors keep it out of the cascade. */
.cs-band > img,
.cs-band > picture > img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: var(--img-cap);   /* the viewport cap — never scroll for one image */
  object-fit: contain;          /* never crop */
  display: block;
  border: var(--border);
  border-radius: clamp(10px, 1.2vw, 18px);
  box-shadow: var(--shadow-card);
  will-change: transform;
}
/* Parallax drifted the picture inside its frame, which needed 14% of overflow
   as headroom — i.e. it only worked by cropping. With the images now sized to
   be seen whole, that headroom no longer exists, so the drift is dropped
   rather than left to slide the picture against its own edges. */
.cs-band { position: relative; }

.cs-duo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(10px, 1.4vw, 20px);
  margin: clamp(10px, 1.4vw, 20px) 0;
}
.cs-duo > figure { margin: 0; overflow: hidden; background: var(--cream-3); }
.cs-duo > figure > img,
.cs-duo > figure > picture > img {
  width: 100%;
  height: auto;
  max-height: var(--img-cap);
  object-fit: contain;
  display: block;
}
@media (max-width: 760px) { .cs-duo { grid-template-columns: 1fr; } }

/* three-up colour + pattern band: the identity as a repeating surface */
.cs-trio {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  aspect-ratio: 3 / 1;
}
.cs-trio__cell {
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: clamp(18px, 3vw, 48px);
}
.cs-trio__cell img { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }
.cs-trio__cell img.is-cover { width: 100%; height: 100%; max-width: none; max-height: none; object-fit: cover; }
.cs-trio__cell.is-layered { padding: 0; position: relative; }
.cs-trio__cell.is-layered img.is-over {
  position: absolute;
  width: 62%;
  height: auto;
  max-height: 82%;
  object-fit: contain;
}
@media (max-width: 760px) {
  .cs-trio { grid-template-columns: 1fr; aspect-ratio: auto; }
  .cs-trio__cell { aspect-ratio: 16 / 9; }
}

/* .cs-blurbed (a deliberately blurred, heavily cropped backdrop band) was
   removed — at full width it read as a broken or failed image rather than
   as texture. */

/* framed images inside the content column get the print treatment */
/* Framed plates shrink-wrap the picture: capping by height and letting the
   frame close around it keeps the whole image visible with no bars inside
   the border. */
.cs-plate {
  border: var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  background: var(--cream-2);
  margin: 0 auto;
  width: fit-content;
  max-width: 100%;
}
.cs-plate > img,
.cs-plate > picture > img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: var(--img-cap);
}

/* ---------- BENTO — a justified gallery ----------
   Masonry columns were leaving whole columns empty: with few, tall items the
   balancer would fill column one and strand the rest of the row, so pictures
   sat on one side of the page with nothing beside them.

   This is a justified row instead. Each figure's flex-basis AND flex-grow are
   both proportional to --r (its picture's own aspect ratio, written into the
   markup from the width/height attributes). When widths are proportional to
   ratios, every picture in a row resolves to the SAME height — so a row comes
   out exactly flush edge to edge with nothing cropped and nothing distorted.

   max-width stops a lone item growing past the viewport-height cap, and
   justify-content:center splits whatever is left over evenly instead of
   dumping it all down one side. */
.cs-bento {
  grid-column: full;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  gap: clamp(8px, 1vw, 16px);
  padding: clamp(8px, 1vw, 16px) var(--gutter);
  margin: 0;
  list-style: none;
}
.cs-bento > figure {
  --r: 1.5;                       /* fallback if a ratio wasn't written */
  flex: var(--r) 1 calc(var(--r) * 210px);
  max-width: min(100%, calc(var(--img-cap) * var(--r)));
  margin: 0;
  overflow: hidden;
  background: var(--cream-3);
  border: var(--border);
  border-radius: clamp(10px, 1.2vw, 18px);
  opacity: 0;
  transform: translateY(26px) scale(.985);
  transition: opacity .6s var(--ease), transform .7s var(--ease);
  transition-delay: var(--d, 0ms);
}
.cs-bento > figure.is-in { opacity: 1; transform: none; }
.cs-bento > figure > img,
.cs-bento > figure > picture > img {
  width: 100%;
  height: auto;                   /* the picture's own proportions — no crop */
  max-height: var(--img-cap);     /* never taller than the screen */
  object-fit: contain;
  display: block;
}
/* Flat artwork and logos sit on their own ground with room to breathe. */
.cs-bento > figure.is-pale { background: var(--cream-2); }
.cs-bento > figure.is-pale > img,
.cs-bento > figure.is-pale > picture > img { padding: clamp(10px, 1.6vw, 28px); }

/* A layered cell (from the old .cs-trio) stacks a mark over a pattern. */
.cs-bento > figure.is-layered { position: relative; }
.cs-bento > figure.is-layered > img.is-over {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 62%;
  height: auto;
  max-height: 78%;
  padding: 0;
}

/* Stagger — the existing [data-anim] observer in case.js supplies .is-in. */
.cs-bento > figure:nth-child(1) { --d: 0ms; }
.cs-bento > figure:nth-child(2) { --d: 70ms; }
.cs-bento > figure:nth-child(3) { --d: 140ms; }
.cs-bento > figure:nth-child(4) { --d: 210ms; }
.cs-bento > figure:nth-child(5) { --d: 280ms; }
.cs-bento > figure:nth-child(6) { --d: 350ms; }
.cs-bento > figure:nth-child(7) { --d: 420ms; }
.cs-bento > figure:nth-child(n+8) { --d: 490ms; }

/* Narrow screens: one picture per row, filling the width. */
@media (max-width: 620px) {
  .cs-bento > figure { flex: 1 1 100%; max-width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .cs-bento > figure { opacity: 1; transform: none; transition: none; }
}

/* ---------- section furniture ----------
   THE EDITORIAL RAIL.
   Every text block on the page used to sit somewhere different: .cs-body at
   64ch left, standalone bodies overridden to 70ch *centred*, .cs-story at 54ch
   left. Three placements down one page is what made the copy look like it was
   drifting from side to side.

   One asymmetric grid fixes it. Body copy always occupies the `main` track, so
   every paragraph on every page shares one left edge. The leftover space isn't
   dead any more: the `rail` carries the section's [data-ch] chapter name as a
   marginal label, which is what the whitespace was always supposed to be. */
.cs-sec { padding-block: clamp(56px, 9vw, 128px); }
.cs-sec > * { grid-column: main-start / main-end; }
.cs-sec--tight { padding-block: clamp(36px, 5vw, 72px); }

/* No ::before chapter label here. An earlier pass printed attr(data-ch) into a
   left-hand column, which both collided with the fixed .cs-rail widget and
   repeated the very chapter list that widget already shows. The rail is the
   single home for chapter navigation. */

@media (max-width: 900px) {
  .cs-sec { grid-template-columns: minmax(0, 1fr); row-gap: 10px; }
  /* NB: .cs-story .cs-body also has to be reset, but it can't be done from
     here — see the note in the STORY block further down. */
  .cs-sec > * { grid-column: 1 / -1; }
}

.cs-kicker {
  font-family: var(--display);
  font-size: clamp(1.05rem, 1.7vw, 1.45rem);
  color: var(--blue);
  margin: 0 0 .7em;
  letter-spacing: .01em;
}
/* Sized so a full line lands at ~72ch inside the 920px track — the prose
   fills its column instead of stranding cream beside it. */
.cs-body {
  font-family: var(--sans);
  font-size: clamp(1.05rem, 1.15vw, 1.32rem);
  line-height: 1.55;
  color: var(--ink);
  margin: 0;
  max-width: none;
}
.cs-body + .cs-body { margin-top: 1.1em; }

/* The standalone caption paragraphs used to be centred here (margin-inline:auto
   at 70ch) to make their stranded whitespace symmetric. That patch is what made
   the copy appear to jump from side to side between sections, so it's gone —
   the rail grid above now gives every paragraph the same left edge, and the
   whitespace it was hiding is the chapter rail. */
.cs-sec > .cs-body { max-width: none; }

/* ---------- STORY + dropcap ----------
   The 34ch measure was written for a narrow column but sits in the 1280px
   content track, so the paragraph filled 46% of the width and left ~690px of
   bare cream beside it. Opening the measure up and scaling the type turns it
   into a full-width opening statement that uses the track it's given. */
.cs-story { padding-block: clamp(44px, 6vw, 92px); }
/* The opening statement keeps the shared left edge but runs into the tail
   track — it's the one block on the page that should feel wide. */
.cs-story .cs-body {
  grid-column: main-start / tail-end;
  font-size: clamp(1.2rem, 1.6vw, 1.75rem);
  line-height: 1.42;
  max-width: none;
  text-wrap: pretty;   /* keeps the last line from stranding a single word */
}
@media (max-width: 900px) {
  /* grid-column must be reset HERE, not in the earlier furniture media block:
     that block and the .cs-story .cs-body rule above have equal specificity,
     so the later one wins and the paragraph kept its named-line placement. */
  .cs-story .cs-body {
    grid-column: 1 / -1;
    font-size: clamp(1.05rem, 2.6vw, 1.3rem); line-height: 1.45; max-width: none;
  }
}
/* DROPCAP GEOMETRY.
   A floated cap only sits flush if its *line box* is an exact multiple of the
   parent's line height — the float reserves that box, and the text wraps
   around it. The old rule used font-size:2.5em with line-height:.78, which
   works out to 1.95em against a 1.42 line-height parent: not two lines, not
   one. Every line after the first landed slightly off, which is the "the rest
   of the text goes a bit to the other line" problem.

   Two lines = 2 x 1.42em = 2.84em. With line-height:1 the cap's line box is
   exactly its font-size, so 2.84em lands it dead on two lines.

   The old rule also carried transform: scale(.6) rotate(-8deg). Transforms
   never affect layout, so the reserved float box stayed full-size while the
   painted glyph shrank to 60% and tilted — text wrapping around a box that
   didn't match what you could see. The entrance is now opacity-only, which
   costs nothing in layout. */
/* The cap sits in two different contexts — .cs-story .cs-body (line-height
   1.42) on most pages, and .cs-lede (1.5) on mango-season and off-the-reco.
   A single hard-coded size can only be exact in one of them, so the size is
   derived from whichever line-height it actually lands in. */
.cs-drop {
  font-family: var(--display);
  float: left;
  font-size: calc(2 * var(--cap-lh, 1.42) * 1em);
  line-height: 1;
  color: var(--accent);
  margin: 0 .1em 0 0;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.cs-story .cs-body { --cap-lh: 1.42; }
.cs-lede { --cap-lh: 1.5; }
.is-in .cs-drop, .cs-drop.is-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .cs-drop { opacity: 1; transition: none; }
}
/* the mobile story runs at line-height 1.45 — the derived size follows it */
@media (max-width: 900px) {
  .cs-story .cs-body { --cap-lh: 1.45; }
}

/* ---------- ASKED / BUILT diptych ----------
   The diptych needs its own two columns, so it splits the section's full width
   in half. Its first column still starts on main-start, so "Asked" shares its
   left edge with every other paragraph on the page. */
.cs-ab {
  grid-template-columns:
    [main-start] minmax(0, 1fr)
    [mid]        minmax(0, 1fr)
    [tail-end];
}
.cs-ab > div:first-child { grid-column: main-start / mid; }
.cs-ab > div:last-child  { grid-column: mid / tail-end; }
.cs-ab .cs-body { max-width: none; }
@media (max-width: 900px) {
  .cs-ab { grid-template-columns: minmax(0, 1fr); row-gap: clamp(28px, 5vw, 56px); }
  .cs-ab > div:first-child,
  .cs-ab > div:last-child { grid-column: 1 / -1; }
}

/* ---------- THE PRESS ----------
   Pinned: ink floods the plate, the mark stamps, the shadow lands.

   These are <section>s without .cs-sec, so they inherited the global
   `section { padding-block: clamp(72px,11vw,150px) }` from styles.css. That
   painted an unstyled cream band above and below the full-bleed stage, and —
   with border-box sizing — ate up to 300px out of the 230vh scroll budget, so
   the animation finished early and drifted with viewport height. */
.cs-press, .cs-reel { padding-block: 0; }
/* Was 230vh — two full screens of scrolling for one logo reveal, and because
   the ink only finished flooding at 42% of that, most of it was bare cream.
   That is the "lots of white space" under the story copy. Shorter section,
   and the timings below now front-load the flood so the stage is coloured
   almost immediately. */
.cs-press { position: relative; height: 128vh; }
.cs-press__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}
/* The ink is FULLY covering by default and JS only shortens the reveal.
   It used to start at `inset(100% 0 0 0)` — completely clipped away — so the
   stage was bare cream until the scroll scrub had run. That is what read as a
   1.5-screen hole under the story copy, and it stayed a hole in any context
   where the scrub never ran: a screenshot, a printed page, a browser that
   throttled rAF, or JS failing. The stage is now a solid colour field the
   moment it exists, and the flood is an enhancement on top of it. */
.cs-press__ink {
  position: absolute;
  inset: 0;
  background: var(--accent);
  clip-path: inset(0 0 0 0);
}
/* the mark is either a single logo image or a stacked lockup of several */
.cs-press__mark {
  position: relative;
  width: min(46vw, 460px);
  max-height: 62vh;
  object-fit: contain;
  opacity: 0;
  will-change: transform, filter;
}
/* A lockup of several pieces keeps the artwork's own relative geometry:
   the box carries the original panel's aspect-ratio and each piece is placed
   by percentage, so the arrangement matches the source exactly. */
.cs-press__mark.cs-lockup {
  width: min(52vw, 520px);
  max-height: none;
  height: auto;
}
.cs-press__mark.cs-lockup img {
  position: absolute;
  height: auto;
  display: block;
}
.cs-press__cap {
  position: absolute;
  left: 50%; bottom: clamp(24px, 5vh, 60px);
  transform: translateX(-50%);
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1rem, 1.6vw, 1.4rem);
  color: var(--accent-ink);
  opacity: .8;
  text-align: center;
}
@media (prefers-reduced-motion: reduce) {
  .cs-press { height: auto; }
  .cs-press__stage { position: relative; height: auto; padding: clamp(48px, 8vw, 110px) 0; }
  .cs-press__ink { clip-path: none; }
  .cs-press__mark { opacity: 1; transform: none; }
}

/* Phones get the same static treatment, for a different reason.
   The pinned version cost ~1000px of scroll to show a 154px mark inside a
   780px field of flat colour — and because the mark starts at opacity 0 and
   only fades in partway through the scrub, the first screenful of that field
   was reliably empty. On a phone the pin buys nothing and the emptiness is
   the whole experience, so below 760px the section sizes to its content and
   the mark is drawn at a size that makes it the subject.
   case.js matches this breakpoint and skips the scrub, so nothing writes
   inline opacity/transform over these rules. */
@media (max-width: 760px) {
  .cs-press { height: auto; }
  .cs-press__stage {
    position: relative;
    height: auto;
    min-height: 56vh;
    padding: clamp(52px, 14vw, 104px) var(--gutter);
    gap: clamp(18px, 5vw, 30px);
  }
  .cs-press__ink { clip-path: none; }
  .cs-press__mark {
    opacity: 1;
    transform: none;
    filter: none;
    width: min(76vw, 420px);
    max-height: none;
  }
  .cs-press__mark.cs-lockup { width: min(82vw, 440px); }
  /* the caption can stop being pinned to the bottom of a box that no longer
     has a fixed height, and just follow the mark */
  .cs-press__cap { position: static; transform: none; }
}

/* ---------- PALETTE ---------- */
/* The palette is a brand artefact, not a caption — it runs the full width of
   the page like the bento does, instead of sitting boxed in the centre track. */
.cs-swatches {
  grid-column: main-start / tail-end;
  list-style: none;
  margin: 0 0 clamp(24px, 3vw, 44px);
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  height: clamp(120px, 15vw, 190px);
  border: var(--border);
  border-radius: clamp(10px, 1.2vw, 18px);
  overflow: hidden;
  box-shadow: var(--shadow-card);
}
.cs-swatches li {
  position: relative;
  background: var(--swatch);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform .8s var(--ease);
}
.cs-swatches.is-in li { transform: scaleY(1); }
.cs-swatches__hex {
  position: absolute;
  left: 50%; bottom: 12px;
  transform: translate(-50%, 6px);
  font-family: var(--sans);
  font-weight: 600;
  font-size: .68rem;
  letter-spacing: .06em;
  padding: .35em .6em;
  border-radius: var(--radius-pill);
  background: var(--cream-2);
  color: var(--ink);
  border: 2px solid var(--ink);
  opacity: 0;
  transition: opacity .25s var(--ease), transform .25s var(--ease);
  pointer-events: none;
  white-space: nowrap;
}
.cs-swatches li:hover .cs-swatches__hex,
.cs-swatches li:focus-within .cs-swatches__hex { opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) {
  .cs-swatches li { transform: scaleY(1); transition: none; }
}
@media (max-width: 560px) {
  .cs-swatches { height: auto; grid-auto-flow: row; grid-auto-rows: 62px; }
  .cs-swatches li { transform-origin: left; }
  .cs-swatches li { transform: scaleX(0); }
  .cs-swatches.is-in li { transform: scaleX(1); }
}

/* ---------- CONCEPT explainer ---------- */
.cs-concept__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(20px, 3vw, 44px);
}
.cs-concept__list li {
  display: flex;
  flex-direction: column;
  gap: 14px;
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.cs-concept__list.is-in li { opacity: 1; transform: none; }
.cs-concept__list.is-in li:nth-child(2) { transition-delay: .1s; }
.cs-concept__list.is-in li:nth-child(3) { transition-delay: .2s; }
.cs-concept__list.is-in li:nth-child(4) { transition-delay: .3s; }
/* The forced 4/3 box letterboxed these sketches inside their own frame (a
   208px picture in a 278px box). Dropping the fixed ratio lets the frame close
   around the picture — still uncropped, just without the empty margin. */
.cs-concept__list img {
  width: 100%;
  height: auto;
  max-height: var(--img-cap);
  object-fit: contain;
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 14px;
}
.cs-concept__list p {
  margin: 0;
  font-family: var(--sans);
  font-size: clamp(.95rem, 1.15vw, 1.1rem);
  line-height: 1.4;
  color: var(--ink-soft);
  text-align: center;
}
@media (prefers-reduced-motion: reduce) {
  .cs-concept__list li { opacity: 1; transform: none; transition: none; }
}

/* ---------- REEL: pinned horizontal run ---------- */
.cs-reel { position: relative; }
.cs-reel__vp {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.cs-reel__vp:focus-visible { outline: 3px solid var(--blue); outline-offset: -6px; }
.cs-reel__track {
  display: flex;
  gap: clamp(14px, 2vw, 30px);
  padding-inline: var(--gutter);
  will-change: transform;
}
.cs-reel__track > figure {
  margin: 0;
  flex: 0 0 auto;
  width: clamp(260px, 34vw, 520px);
  overflow: hidden;
  border-radius: var(--radius);
  border: var(--border);
  box-shadow: var(--shadow-card);
  background: var(--cream-2);
}
/* Portrait exports in the reel (a 640×1136 cake) ran 108% of a laptop screen
   at the track's fixed width. Capping by height and letting the card close
   around the picture keeps every frame inside the reel's viewport. */
.cs-reel__track > figure { width: auto; }
.cs-reel__track > figure > img,
.cs-reel__track > figure > picture > img {
  display: block;
  width: auto;
  max-width: clamp(260px, 34vw, 520px);
  height: auto;
  max-height: var(--img-cap);
}
/* logo/pattern plates in a reel get breathing room instead of bleeding */
.cs-reel__track > figure.is-pale {
  display: grid;
  place-items: center;
  padding: clamp(24px, 4vw, 60px);
  background: var(--cream-3);
}
.cs-reel__track > figure.is-pale img { width: auto; max-width: 100%; max-height: 46vh; }

/* A "place" card — a sub-brand in the reel. Unlike the free-sized figures
   above, these are deliberately uniform: twelve places that each got their own
   mark are a system, and a system reads as one only if the frames match. The
   photograph is pre-cropped to 3:2 by tools/build-case-assets.py, so the card
   never has to crop at render time. */
.cs-reel__track > figure.cs-place {
  width: clamp(260px, 32vw, 460px);
  display: grid;
  grid-template-rows: auto auto;
  background: var(--cream-2);
}
.cs-reel__track > figure.cs-place > picture > img {
  width: 100%;
  max-width: none;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  max-height: none;
}
.cs-place__cap {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.4vw, 16px);
  padding: clamp(12px, 1.6vw, 20px) clamp(14px, 1.8vw, 22px);
  border-top: var(--border);
  background: var(--cream-2);
}
/* The mark is trimmed to its artwork and squared off by the asset build, so
   this width is the artwork's width — not a canvas it floats inside. */
.cs-place__cap img {
  width: clamp(38px, 4.2vw, 50px);
  height: auto;
  flex: 0 0 auto;
}
.cs-place__cap b {
  display: block;
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(.9rem, 1.05vw, 1.05rem);
  line-height: 1.2;
  letter-spacing: -.01em;
}
.cs-place__cap span {
  display: block;
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 3px;
}

/* swipeable fallback: small screens and reduced motion */
.cs-reel.is-swipe { height: auto !important; }
.cs-reel.is-swipe .cs-reel__vp {
  position: static;
  height: auto;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-block: clamp(28px, 5vw, 48px);
  -webkit-overflow-scrolling: touch;
}
.cs-reel.is-swipe .cs-reel__track { transform: none !important; }
.cs-reel.is-swipe .cs-reel__track > figure { scroll-snap-align: center; }
@media (prefers-reduced-motion: reduce) {
  .cs-reel { height: auto !important; }
  .cs-reel__vp {
    position: static; height: auto;
    overflow-x: auto; scroll-snap-type: x mandatory;
  }
  .cs-reel__track { transform: none !important; }
}

/* ---------- the Ink Rail ---------- */
.cs-rail {
  position: fixed;
  left: clamp(14px, 2vw, 28px);
  top: 50%;
  transform: translateY(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 14px;
  pointer-events: none;
}
.cs-rail__name {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--display);
  font-size: .72rem;
  letter-spacing: .08em;
  color: var(--ink);
  opacity: .45;
  white-space: nowrap;
}
.cs-rail__line {
  position: relative;
  width: 2px;
  height: clamp(90px, 16vh, 150px);
  background: rgba(28, 20, 10, .16);
  border-radius: 2px;
  overflow: hidden;
}
.cs-rail__fill {
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform-origin: top;
  transform: scaleY(0);
}
.cs-rail__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; pointer-events: auto; }
.cs-rail__list a {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--ink);
}
.cs-rail__list i {
  width: 8px; height: 8px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: transparent;
  transition: background .3s var(--ease), transform .3s var(--ease);
  flex: 0 0 auto;
}
.cs-rail__list a.is-on i { background: var(--accent); transform: scale(1.25); }
/* Absolutely positioned so they contribute NO layout width. Left in flow they
   were invisible (opacity:0) but still stretched the rail's box to the width
   of the longest chapter name — 153px on Godariya — which both pushed the
   page content further right than it needed to be and meant the labels
   printed over that content the moment the rail was hovered. Now they float
   out into the reserved gutter only while hovered. */
.cs-rail__list a { position: relative; }
.cs-rail__list span {
  position: absolute;
  left: calc(100% + 8px);
  top: 50%;
  font-family: var(--sans);
  font-weight: 600;
  font-size: .7rem;
  letter-spacing: .04em;
  opacity: 0;
  transform: translateY(-50%) translateX(-4px);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
  white-space: nowrap;
  pointer-events: none;
}
.cs-rail:hover span,
.cs-rail__list a:focus-visible span { opacity: .8; transform: translateY(-50%); }
.cs-rail__list a:focus-visible { outline: 2px solid var(--blue); outline-offset: 4px; border-radius: 4px; }

/* under 980px the rail becomes a slim progress bar at the top */
@media (max-width: 980px) {
  .cs-rail {
    left: 0; right: 0; top: 0;
    transform: none;
    height: 3px;
    display: block;
  }
  .cs-rail__name, .cs-rail__list { display: none; }
  .cs-rail__line { width: 100%; height: 3px; border-radius: 0; }
  .cs-rail__fill { transform-origin: left; transform: scaleX(0) !important; }
}
@media (prefers-reduced-motion: reduce) {
  .cs-rail__list i, .cs-rail__list span { transition: none; }
}

/* ---------- next / prev chain ---------- */
.cs-chain {
  grid-column: full;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-top: var(--border);
}
.cs-chain__side {
  padding: clamp(28px, 5vw, 64px) var(--gutter);
  text-decoration: none;
  color: var(--ink);
  background: var(--cream-2);
  transition: background .35s var(--ease);
}
.cs-chain__side + .cs-chain__side { border-left: var(--border); }
.cs-chain__side--next { text-align: right; }
.cs-chain__side:hover { background: var(--cream-3); }
.cs-chain__lab {
  display: block;
  font-family: var(--sans);
  font-weight: 600;
  font-size: .74rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .6em;
}
.cs-chain__name {
  display: block;
  font-family: var(--display);
  font-size: clamp(1.1rem, 2.6vw, 2.1rem);
  line-height: 1.05;
  color: var(--blue);
}
.cs-chain__name em { font-style: normal; display: inline-block; transition: transform .3s var(--ease); }
.cs-chain__side:hover em { transform: translateX(6px); }
@media (max-width: 760px) {
  .cs-chain { grid-template-columns: 1fr; }
  .cs-chain__side + .cs-chain__side { border-left: 0; border-top: var(--border); }
}
@media (prefers-reduced-motion: reduce) {
  .cs-chain__name em, .cs-chain__side { transition: none; }
}

/* ---------- the emoticon toy ---------- */
.cs-toy-wrap { position: relative; }
.cs-toy {
  position: relative;
  height: clamp(300px, 46vh, 460px);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--cream-2);
  overflow: hidden;
}
.cs-toy__seed {
  position: absolute;
  inset: 0;
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  justify-content: center;
  gap: clamp(10px, 2vw, 26px);
  padding: 24px;
}
.cs-toy__seed img { width: clamp(56px, 7vw, 84px); height: auto; }
.cs-toy.is-live .cs-toy__seed { opacity: 0; pointer-events: none; }
.cs-toy canvas { position: absolute; inset: 0; }
.cs-toy__hint {
  position: absolute;
  left: 50%; bottom: 14px;
  transform: translateX(-50%);
  font-family: var(--sans);
  font-weight: 600;
  font-size: .8rem;
  color: var(--ink-faint);
  pointer-events: none;
}

/* ===================================================================
   ANIMATION PRIMITIVES — driven by [data-anim], observer adds .is-in
   =================================================================== */

/* rise */
[data-anim="rise"] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity .8s var(--ease), transform .8s var(--ease);
}
[data-anim="rise"].is-in { opacity: 1; transform: none; }

/* wipe — the print pull */
[data-anim="wipe"] {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1s var(--ease);
}
[data-anim="wipe"].is-in { clip-path: inset(0 0 0 0); }

/* slide in from the side, for two-up panels */
[data-anim="from-left"], [data-anim="from-right"] {
  opacity: 0;
  transition: opacity .85s var(--ease), transform .85s var(--ease);
}
[data-anim="from-left"]  { transform: translateX(-40px) rotate(-1.2deg); }
[data-anim="from-right"] { transform: translateX(40px) rotate(1.2deg); }
[data-anim="from-left"].is-in, [data-anim="from-right"].is-in { opacity: 1; transform: none; }

/* masked lines — set up by splitLines() in case.js */
[data-anim="lines"] .ln { display: block; overflow: hidden; }
[data-anim="lines"] .ln__i {
  display: block;
  transform: translateY(105%);
  transition: transform .85s var(--ease);
}
[data-anim="lines"].is-in .ln__i { transform: none; }
/* before the split runs (and when it can't), text must still be visible */
[data-anim="lines"]:not([data-line-split]) { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  [data-anim] { opacity: 1 !important; transform: none !important; clip-path: none !important; transition: none !important; }
  [data-anim="lines"] .ln__i { transform: none !important; transition: none !important; }
}

/* ---------- back-to-work CTA ---------- */
.cs-outro { grid-column: content; padding-block: clamp(48px, 7vw, 96px); text-align: center; }
.cs-outro .split { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
