/* ============================================================================
   VICTORIA'S SECRET — SOCIAL CAMPAIGN CREATIVES
   A campaign lookbook: a full-bleed hero, five sticky carousel chapters, a
   hand-composed wall of 35 pieces.

   Written mobile-first for real: every rule outside a media query targets a
   ~390px canvas, and larger screens only ever ADD to it through min-width
   blocks. Nothing here is a desktop layout folded back down. That matters
   more on this page than on any other case study in the portfolio, because
   the campaign is overwhelmingly portrait material made for a phone.

   Layered on top of style.css, which already supplies the design tokens, the
   topbar/footer chrome, the reveal system, the .count count-up hook and the
   .btn set.

   The chrome section repeats the equivalents in casestudy-disneyxbbw.css and
   the other live studies at IDENTICAL values so the case studies read as one
   system. They are duplicated rather than shared for the reason those files
   already give: each also carries its own layout, which has no business
   loading here, and lifting the common ~200 lines into a shared casestudy.css
   would let adding this page regress five live ones.

   ---------------------------------------------------------------------------
   COLOUR — one PORTFOLIO ACCENT and one CAMPAIGN ACCENT, and they are not
   interchangeable.

   #b162fe (--primary-red, aliased --cs-red) is the PORTFOLIO's mark. It
   carries the shared chrome and nothing else: the topbar arrow, the eyebrow
   rules, .btn--primary, the next-project arrow. It does not change per
   client, on this page or any other.

   The blush (--vs-blush #E8BDC8) and rose (--vs-rose #B86F82) are the
   CAMPAIGN's, sampled off the masters. They carry state that belongs to this
   work — the active chapter number, the scroll progress bar, the hovered
   figure in the scale section, the media hover border. Pink never becomes the
   ground and never sets a heading: at page scale it is maybe two percent of
   the pixels, which is what keeps a Victoria's Secret case study from
   collapsing into a pink page.

   Warm ivory (--vs-ivory #F5F0EA) appears exactly once as a ground, under
   Chapter 03, as the contrast beat that stops five near-black chapters
   reading as one section.
   ---------------------------------------------------------------------------

     1  Page tokens & gutter
     2  Chrome adjustments
     3  Primitives — media card, sr-only, cursor label
     4  Editorial furniture — headings, section rhythm, breaks
     5  Hero
     6  Campaign scale
     7  The vertical runway — chapter shell
     8  The vertical runway — chapter variants
     9  The campaign wall
    10  Closing
    11  CTA & next
    12  Media viewer
    13  TABLET         min-width: 768px
    14  DESKTOP        min-width: 1025px
    15  LARGE DESKTOP  min-width: 1440px
    16  Reduced motion
   ========================================================================= */

/* --------------------------------------------------------- 1 tokens & gutter */
:root {
  /* Carried straight from the other studies so every case study shares one
     type colour and one hairline weight. --cs-red IS --primary-red. */
  --cs-red:    #b162fe;
  --cs-white:  #f7f5f2;
  --cs-grey:   #a8a5a0;
  /* The other studies set --cs-dim to #6e6b66. On #090909 that measures
     3.68:1, which is under AA for the small uppercase metadata this page
     leans on heavily — slide counters, cell captions, the asset-count line,
     the next-project label. Lifted to 5.47:1 here. It still sits a clear step
     below --cs-grey (7.1:1), so the three-level hierarchy survives. */
  --cs-dim:    #8a8681;
  --cs-line:   rgba(247, 245, 242, .1);
  --cs-line-2: rgba(247, 245, 242, .18);
  --cs-panel:  #101011;

  /* Campaign palette. Sampled from the masters, not invented. */
  --vs-black:  #090909;
  --vs-ivory:  #f5f0ea;
  --vs-blush:  #e8bdc8;
  --vs-rose:   #b86f82;
  --vs-grey:   #b8b4b2;
  --vs-blush-lo: rgba(232, 189, 200, .16);

  --cs-space: 5.5rem;
  --vs-h2:    2.1rem;

  /* 22px of horizontal breathing room on mobile — style.css drops the global
     gutter to 18.4px under 640px, which is tighter than this page wants. */
  --gutter: 1.375rem;

  /* Slide heights for the runway. On a phone slides are sized by WIDTH (a
     story slide is meant to be nearly the whole screen); from the tablet up
     they are sized by HEIGHT so a 4:5 and a 9:16 chapter present at the same
     optical scale instead of the story slides towering over the others. */
  --slide-gap: 1rem;
}

/* ------------------------------------------------------- 2 chrome adjustments */
/* style.css makes the topbar transparent under 640px because the homepage hero
   sits behind it. Here it is a plain bar over a plain canvas. */
@media (max-width: 640px) {
  .topbar { background: var(--vs-black); }
}

.cs-brand { display: flex; align-items: center; gap: .6rem; min-height: 44px; }
.cs-brand__arrow {
  width: 1.3rem; height: .6rem; color: var(--cs-grey);
  transition: transform .3s var(--ease), color .3s var(--ease);
}
.cs-brand:hover .cs-brand__arrow,
.cs-brand:focus-visible .cs-brand__arrow { transform: translateX(-5px); color: var(--cs-red); }

.vsi-body { background: var(--vs-black); }

/* The page is a stack of full-bleed sections; a stray horizontal scrollbar
   from one mis-measured rail would be visible on every one of them.

   CLIP, NOT HIDDEN, and this is load-bearing: overflow-x:hidden on the body
   makes it a scroll container, and a scroll container between the viewport
   and a position:sticky element is what stops that element ever sticking.
   Five sticky chapters depend on this staying `clip`. Safari below 16 does
   not support it and simply does not clip — which is the harmless half of
   the trade, since nothing on this page is supposed to overflow anyway. */
.vsi-body { overflow-x: clip; }

/* ============================================================= 3 primitives */

/* Visually hidden but announced. Used for the "×" in Chapter 05's title,
   which sighted readers see as a symbol and screen readers should hear as a
   word boundary rather than "multiplication sign". */
.vsi-sr {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}

/* THE media card. One button, used by every slide, post and clip on the page,
   so there is a single hover state, a single focus ring and a single corner
   radius to maintain.

   No aspect-ratio here on purpose. Every <img> carries real width/height
   attributes, so the browser derives the box's ratio from the file itself
   before it loads. That is what guarantees no crop and no layout shift on a
   page mixing 4:5, 9:16 and 1:1 in the same row. */
.vsi-media {
  position: relative;
  display: block;
  width: 100%;
  margin: 0; padding: 0;
  border: 1px solid var(--cs-line);
  background: var(--cs-panel);
  overflow: hidden;
  cursor: pointer;
  color: inherit;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .45s var(--ease), box-shadow .45s var(--ease);
}
.vsi-media__img {
  display: block;
  width: 100%; height: auto;
  transition: transform .9s var(--ease-out), filter .5s var(--ease);
}
.vsi-media__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.vsi-media.is-playing .vsi-media__video { opacity: 1; }

.vsi-media:hover,
.vsi-media:focus-visible {
  border-color: var(--vs-blush);
  box-shadow: 0 0 0 1px var(--vs-blush-lo);
}
.vsi-media:hover .vsi-media__img { transform: scale(1.025); }
.vsi-media:focus-visible { outline: 2px solid var(--vs-blush); outline-offset: 3px; }

/* Slide number, bottom-left. Sits on its own scrim rather than on the
   photograph, because half these masters are near-white at the edges. */
.vsi-media__index {
  position: absolute; left: 0; bottom: 0;
  padding: .5rem .7rem .45rem;
  font-family: var(--font-cond); font-weight: 400;
  font-size: .68rem; letter-spacing: .18em;
  color: var(--cs-white);
  background: linear-gradient(135deg, rgba(9, 9, 9, .78), rgba(9, 9, 9, .35));
  backdrop-filter: blur(2px);
}

/* Play badge + duration. Both are decoration: the card announces itself as a
   reel through its accessible name, not through these. */
.vsi-media__play {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  display: grid; place-items: center;
  width: 2.9rem; height: 2.9rem;
  border: 1px solid rgba(247, 245, 242, .55);
  border-radius: 50%;
  background: rgba(9, 9, 9, .34);
  backdrop-filter: blur(3px);
  transition: opacity .45s var(--ease), transform .45s var(--ease-out),
              background-color .45s var(--ease);
}
.vsi-media__play svg { width: 1rem; height: 1rem; fill: var(--cs-white); }
.vsi-media:hover .vsi-media__play { background: rgba(184, 111, 130, .5); }
.vsi-media.is-playing .vsi-media__play { opacity: 0; transform: translate(-50%, -50%) scale(.86); }

.vsi-media__time {
  position: absolute; right: .55rem; bottom: .55rem;
  padding: .22rem .45rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .62rem; letter-spacing: .12em;
  color: var(--cs-white);
  background: rgba(9, 9, 9, .6);
  transition: opacity .4s var(--ease);
}
.vsi-media.is-playing .vsi-media__time { opacity: 0; }

/* Desktop cursor label. Hidden entirely until the JS opts in, and it never
   exists on a touch device or under reduced motion. */
.vsi-cursor {
  position: fixed; top: 0; left: 0; z-index: 55;
  display: none;
  pointer-events: none;
  padding: .5rem .85rem;
  font-family: var(--font-cond); font-weight: 400;
  font-size: .6rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--vs-black);
  background: var(--vs-blush);
  border-radius: 999px;
  opacity: 0;
  transform: translate3d(var(--cx, 0), var(--cy, 0), 0) translate(-50%, -50%) scale(.6);
  transition: opacity .3s var(--ease), scale .3s var(--ease-out);
}
.vsi-cursor.is-on { display: block; }
.vsi-cursor.is-live { opacity: 1; scale: 1; }

/* =================================================== 4 editorial furniture */
.cs-eyebrow {
  display: flex; align-items: center; gap: .75rem;
  font-family: var(--font-cond); font-weight: 400;
  font-size: .66rem; letter-spacing: .28em; text-transform: uppercase;
  color: var(--cs-grey);
}
.cs-eyebrow__rule { width: 1.6rem; height: 1px; background: var(--cs-red); flex: none; }

/* Every campaign-facing heading is the editorial serif. Oswald stays on
   labels, numerals and metadata; Inter stays on prose. That split is the
   typographic system of this page and it does not vary by section. */
.vsi-h2 {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: var(--vs-h2);
  line-height: 1.08;
  letter-spacing: -.005em;
  color: var(--cs-white);
  margin-top: .8rem;
}

.vsi-runway__note,
.vsi-wall__note {
  margin-top: 1rem;
  max-width: 46ch;
  font-size: .9rem; line-height: 1.65;
  color: var(--cs-grey);
}

/* Typographic breaks. Four of them across the wall, placed between groups of
   rows rather than after each one — they exist to slow the page down, and a
   pause after every row is not a pause. */
.vsi-break {
  padding-block: clamp(3.5rem, 11vw, 7rem);
}
.vsi-break__line {
  font-family: var(--font-serif);
  font-weight: 300;
  font-size: clamp(1.9rem, 9vw, 3.4rem);
  line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--cs-white);
  max-width: 16ch;
}
.vsi-break--right .vsi-break__line { margin-left: auto; text-align: right; }

/* ==================================================================== 5 hero */
.vsi-hero {
  position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
  min-height: 88svh;
  overflow: hidden;
  isolation: isolate;
}

/* STACKING, and it is load-bearing. The scrim must paint OVER the photograph.
   It was written once as z-index:-1 inside this container and it silently did
   the opposite: a negative z-index child paints before the non-positioned
   in-flow content of its stacking context, so the gradient went UNDER the
   <img> and the hero measured 200/255 behind ivory type. Everything here is
   positive and explicit for that reason — image 1, scrim 2, copy 3.

   The <img> is absolutely positioned rather than sized at 100%/100%, because
   its parent is a <picture>, which is an inline box of auto height: a
   percentage height against it resolves to auto and object-fit: cover never
   engages. Absolute positioning takes it out of that flow and lays it against
   .vsi-hero__media instead, which has real dimensions. */
.vsi-hero__media { position: absolute; inset: 0; z-index: 0; }
.vsi-hero__img {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 54% 28%;
  /* The zoom is written as a custom property so the scroll handler only ever
     touches one number and never a transform string. */
  transform: scale(calc(1 + var(--hero-zoom, 0) * .1));
  transform-origin: 54% 30%;
  will-change: transform;
}

/* THE SCRIM. Two washes, not one:
     bottom-up  for the phone lockup, which sits low over the model's skirt
     left-right for the desktop lockup, which sits over the empty left half
                of the banner

   Both are far heavier than a dark-theme scrim usually needs, because the
   photograph underneath is BRIGHT — a white studio sweep, the opposite of
   what a dark hero normally sits on. These stops were tuned by measuring the
   rendered pixels behind each line of type at 390, 1440 and 1920 and
   computing the real contrast ratio, not by eye. The binding constraint is
   .vsi-hero__sub, which is blush (#e8bdc8) rather than ivory and therefore
   the first thing to fail. Do not lighten any stop without re-measuring that
   line.

   RE-MEASURED at all three widths when the hero became hero-banner-vsi.png,
   whose white sweep is brighter than the pink cyclorama these stops were
   first set against. Every line still clears AA against the lightest pixels
   under it: worst case 5.52:1, the desktop scroll cue at 1920, which is the
   one line sitting out in the lit half of the frame. __sub holds at 9.50:1.
   Nothing had to move — the stops were already carrying more than the
   cyclorama needed. */
.vsi-hero__scrim {
  position: absolute; inset: 0; z-index: 2;
  background:
    linear-gradient(to top,
      rgba(9, 9, 9, .97) 0%,
      rgba(9, 9, 9, .95) 28%,
      rgba(9, 9, 9, .9) 44%,
      rgba(9, 9, 9, .72) 58%,
      rgba(9, 9, 9, .3) 74%,
      rgba(9, 9, 9, .12) 88%,
      rgba(9, 9, 9, .3) 100%);
}

.vsi-hero__inner {
  position: relative;
  z-index: 3;
  padding-block: clamp(2.5rem, 9vw, 4rem) clamp(1.5rem, 5vw, 2.5rem);
}
.vsi-hero__copy { max-width: 30ch; }

.vsi-hero__title {
  margin-top: 1rem;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(2.6rem, 13vw, 4.4rem);
  line-height: .98;
  letter-spacing: -.015em;
  color: var(--vs-ivory);
}
.vsi-hero__sub {
  margin-top: .55rem;
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(1.05rem, 4.6vw, 1.5rem);
  color: var(--vs-blush);
}
.vsi-hero__scale {
  margin-top: 1.4rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .72rem; letter-spacing: .2em; text-transform: uppercase;
  line-height: 1.9;
  color: var(--cs-white);
}
.vsi-hero__stat { white-space: nowrap; }
.vsi-hero__dot { color: var(--vs-blush); margin-inline: .25em; }
.vsi-hero__desc {
  margin-top: .9rem;
  font-size: .88rem; line-height: 1.6;
  color: #d9d5d2;
  max-width: 34ch;
}

/* The eyebrow and the scroll cue are the two lines that sit where the scrim
   is thinnest — the eyebrow at the top of the copy stack on a phone, the cue
   at the bottom-right of the frame on desktop, over the bare studio wall.
   Both are set in --cs-white rather than the --cs-grey they would take
   elsewhere on the page. Measured: grey put them at 2.2:1 and 2.5:1. */
.vsi-hero .cs-eyebrow { color: var(--cs-white); }

.vsi-hero__cue {
  display: inline-flex; align-items: center; gap: .7rem;
  margin-top: 2.2rem;
  min-height: 44px;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .6rem; letter-spacing: .3em; text-transform: uppercase;
  color: var(--cs-white);
  transition: color .3s var(--ease);
}
.vsi-hero__cue:hover { color: var(--vs-blush); }
.vsi-hero__cue-line {
  display: block; width: 3rem; height: 1px;
  background: linear-gradient(90deg, var(--vs-blush), transparent);
  transform-origin: left;
  animation: vsi-cue 2.6s var(--ease) infinite;
}
@keyframes vsi-cue {
  0%, 100% { transform: scaleX(.4); opacity: .4; }
  50%      { transform: scaleX(1);  opacity: 1; }
}

/* ========================================================== 6 campaign scale */
.vsi-scale {
  position: relative;
  padding-block: clamp(4rem, 13vw, 8rem);
  overflow: hidden;
  isolation: isolate;
}

.vsi-scale__bg { position: absolute; inset: 0; z-index: -1; }
.vsi-scale__layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.06);
  transition: opacity .9s var(--ease), transform 1.4s var(--ease-out);
}
.vsi-scale__layer.is-live { opacity: .3; transform: scale(1); }
.vsi-scale__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .9s var(--ease);
}
.vsi-scale__video.is-live { opacity: .32; }
/* The wash is what keeps the type legible over whatever is behind it. It is
   part of the backdrop, not an effect — never remove it to "show more of the
   image". */
.vsi-scale__wash {
  position: absolute; inset: 0;
  background:
    linear-gradient(to bottom, var(--vs-black) 0%, rgba(9, 9, 9, .72) 30%,
                   rgba(9, 9, 9, .72) 70%, var(--vs-black) 100%);
}

.vsi-scale__list { margin-top: clamp(2.2rem, 8vw, 3.6rem); }
.vsi-scale__item {
  display: flex; align-items: baseline; gap: 1rem;
  padding-block: clamp(1rem, 4vw, 1.6rem);
  border-top: 1px solid var(--cs-line);
}
.vsi-scale__item:last-child { border-bottom: 1px solid var(--cs-line); }

.vsi-scale__num {
  font-family: var(--font-cond); font-weight: 200;
  font-size: clamp(3.4rem, 19vw, 6rem);
  line-height: .82;
  letter-spacing: -.02em;
  color: var(--cs-white);
  transition: color .5s var(--ease);
}
.vsi-scale__label {
  font-family: var(--font-cond); font-weight: 300;
  font-size: clamp(.7rem, 2.4vw, .84rem);
  letter-spacing: .24em; text-transform: uppercase;
  color: var(--cs-grey);
  transition: color .5s var(--ease);
}
.vsi-scale__item.is-live .vsi-scale__num   { color: var(--vs-blush); }
.vsi-scale__item.is-live .vsi-scale__label { color: var(--vs-rose); }

.vsi-scale__copy { margin-top: clamp(1.8rem, 6vw, 2.6rem); }
.vsi-scale__lead {
  font-family: var(--font-serif);
  font-weight: 300;
  font-size: clamp(1.15rem, 4.6vw, 1.5rem);
  line-height: 1.45;
  color: var(--cs-white);
  max-width: 34ch;
}
.vsi-scale__fine {
  margin-top: .9rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .7rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--cs-dim);
}

/* ============================================ 7 the vertical runway — shell */
.vsi-runway { padding-block: clamp(3.5rem, 11vw, 6rem) 0; }
.vsi-runway__head { padding-bottom: clamp(2.5rem, 8vw, 4rem); }

.vsi-chapter { position: relative; }

/* BASE STATE = the swipe gallery. This is what phones get, what reduced
   motion gets and what a JavaScript failure gets. The sticky desktop
   behaviour in section 14 is added on top of it and can be removed without
   leaving anything broken behind. */
.vsi-chapter__track { position: relative; }
.vsi-chapter__stage { position: relative; }
.vsi-chapter__inner { position: relative; }

.vsi-chapter__head {
  padding-inline: var(--gutter);
  padding-bottom: 1.5rem;
}
.vsi-chapter__num {
  font-family: var(--font-cond); font-weight: 400;
  font-size: .64rem; letter-spacing: .3em; text-transform: uppercase;
  color: var(--vs-rose);
}
.vsi-chapter__title {
  margin-top: .5rem;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.9rem, 8.5vw, 2.8rem);
  line-height: 1.04;
  letter-spacing: -.01em;
  color: var(--cs-white);
}
.vsi-x { color: var(--vs-blush); font-style: italic; }
.vsi-chapter__meta {
  margin-top: .7rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .66rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--cs-grey);
}
.vsi-chapter__note {
  margin-top: .9rem;
  max-width: 38ch;
  font-size: .86rem; line-height: 1.6;
  color: var(--cs-dim);
}

.vsi-chapter__progress {
  position: relative;
  margin-top: 1.4rem;
  height: 1px;
  background: var(--cs-line);
  overflow: hidden;
}
.vsi-chapter__bar {
  display: block; height: 100%; width: 100%;
  background: var(--vs-blush);
  transform: scaleX(var(--p, 0));
  transform-origin: left;
}
.vsi-chapter__counter {
  margin-top: .6rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .62rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--cs-dim);
}

.vsi-chapter__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;

  /* BOTH axes, and pan-x is the whole point. This read `pan-y pinch-zoom`,
     which says "the only panning allowed here is vertical" — so a horizontal
     finger swipe was rejected by the compositor before it ever reached the
     scroller and the rails were completely immovable on a phone. Verified: 0
     of 5 rails responded to a synthesized touch swipe; all 5 do now.

     Listing pan-x AND pan-y is what keeps a vertical flick scrolling the
     page: the browser picks the axis from the gesture, so the rail takes
     horizontal movement and the page still takes vertical. Dropping pan-y
     here would fix the carousel and trap the page instead. */
  touch-action: pan-x pan-y pinch-zoom;
}
.vsi-chapter__viewport::-webkit-scrollbar { display: none; }
.vsi-chapter__viewport:focus-visible {
  outline: 2px solid var(--vs-blush);
  outline-offset: -2px;
}

.vsi-chapter__rail {
  display: flex;
  gap: var(--slide-gap);
  padding-inline: var(--gutter);
  padding-bottom: clamp(2.5rem, 9vw, 4rem);
  width: max-content;
}
.vsi-slide {
  flex: none;
  scroll-snap-align: center;
  width: var(--slide-w);
}

/* Slides are sized by width on a phone: a story slide should be most of the
   screen, a 4:5 slide slightly less, and both keep their true ratio because
   the <img> carries width/height. */
.vsi-chapter--story   { --slide-w: min(74vw, 20rem); }
.vsi-chapter--portrait { --slide-w: min(80vw, 24rem); }

/* ========================================= 8 the vertical runway — variants */

/* Chapter 03 inverts the page. The ivory ground needs its own type colours;
   every one of these is a contrast decision, not a tint. */
.vsi-chapter--ivory { background: var(--vs-ivory); color: #1c1719; }
.vsi-chapter--ivory .vsi-chapter__title   { color: #14100f; }
.vsi-chapter--ivory .vsi-chapter__num     { color: #9a4f62; }
.vsi-chapter--ivory .vsi-chapter__meta    { color: #5d5652; }
.vsi-chapter--ivory .vsi-chapter__note    { color: #6b635e; }
.vsi-chapter--ivory .vsi-chapter__counter { color: #6b635e; }
.vsi-chapter--ivory .vsi-chapter__progress { background: rgba(20, 16, 15, .14); }
.vsi-chapter--ivory .vsi-chapter__bar     { background: #b86f82; }
.vsi-chapter--ivory .vsi-media {
  border-color: rgba(20, 16, 15, .12);
  background: #e9e2da;
}
.vsi-chapter--ivory .vsi-media:hover,
.vsi-chapter--ivory .vsi-media:focus-visible { border-color: var(--vs-rose); }
.vsi-chapter--ivory .vsi-chapter__viewport:focus-visible { outline-color: var(--vs-rose); }

/* Chapter 04 runs its slides larger — it is the talent chapter and the one
   the hero has already promised. */
.vsi-chapter--feature { --slide-w: min(82vw, 22rem); }

/* Chapter 05, the spotlight. Centre-padding lets the first and last slides
   reach the middle of the stage; the inactive slides pull back so the active
   one reads as the one on stage with its neighbours previewed either side. */
.vsi-chapter--spotlight .vsi-slide {
  transition: transform .6s var(--ease-out), opacity .6s var(--ease);
}
.vsi-chapter--spotlight .vsi-slide[data-rel="off"] {
  transform: scale(.86);
  opacity: .42;
}

/* ========================================================= 9 the campaign wall */
.vsi-wall { padding-block: clamp(4rem, 13vw, 8rem) clamp(2rem, 6vw, 4rem); }
.vsi-wall__head { padding-bottom: clamp(2.5rem, 8vw, 4rem); }

/* Every row is its own grid. One column on a phone — one major asset at a
   time is the whole point of a portrait-first campaign — with the square
   performance creatives allowed to pair up, since they are secondary and a
   full screen each would pad the page for no gain. */
.vsi-row {
  display: grid;
  gap: clamp(1rem, 4vw, 1.6rem);
  padding-block: clamp(1rem, 4vw, 2rem);
}
.vsi-row--trio { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.vsi-row--trio .vsi-cell:last-child { grid-column: 1 / -1; max-width: 62%; }

.vsi-row__stack { display: grid; gap: clamp(1rem, 4vw, 1.6rem); }

.vsi-cell { margin: 0; min-width: 0; }
.vsi-cell__cap {
  margin-top: .7rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .64rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--cs-dim);
}

.vsi-row__aside { display: flex; align-items: center; }
.vsi-row__quote {
  font-family: var(--font-serif);
  font-weight: 300;
  font-size: clamp(1.05rem, 4.4vw, 1.35rem);
  line-height: 1.5;
  color: var(--cs-grey);
  max-width: 30ch;
}

/* ============================================================= 10 closing */
/* All that is left of the old closing showcase. The stage, its five items,
   the sound control and the index list are gone with the section; only the
   statement + meta pair survives, because it sits above the shared .cs-cta
   and .cs-next chrome and has to keep its rhythm with them. */
.vsi-close {
  padding-block: clamp(3rem, 10vw, 5rem) clamp(3rem, 9vw, 5rem);
}
.vsi-close__statement {
  font-family: var(--font-serif);
  font-weight: 300;
  font-size: clamp(1.8rem, 8vw, 2.9rem);
  line-height: 1.18;
  letter-spacing: -.01em;
  color: var(--cs-white);
}
.vsi-close__meta {
  margin-top: 1.2rem;
  font-family: var(--font-cond); font-weight: 300;
  font-size: .68rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--cs-grey);
}
.vsi-close__meta span { color: var(--vs-rose); margin-inline: .2em; }

/* ================================================================ 11 CTA & next */
.cs-cta { margin-top: clamp(2.5rem, 8vw, 4rem); }
.cs-cta__actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.6rem;
}
.cs-cta__actions .btn { min-height: 44px; }

.cs-next {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem 1.4rem;
  margin-top: clamp(2.5rem, 8vw, 4rem);
  padding-top: clamp(1.4rem, 4vw, 2rem);
  border-top: 1px solid var(--cs-line);
}
.cs-next__label {
  font-family: var(--font-cond); font-weight: 300;
  font-size: .64rem; letter-spacing: .26em; text-transform: uppercase;
  color: var(--cs-dim);
}
.cs-next__link {
  display: inline-flex; align-items: center; gap: 1rem;
  min-height: 44px;
  font-family: var(--font-serif);
  font-size: clamp(1.4rem, 6vw, 2rem);
  color: var(--cs-white);
  transition: color .35s var(--ease);
}
.cs-next__link .arrow { width: 2.1rem; color: var(--cs-red); }
.cs-next__link:hover, .cs-next__link:focus-visible { color: var(--cs-red); }
.cs-next__link:hover .arrow { transform: translateX(6px); }

/* ============================================================ 12 media viewer */
.vsi-lb {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
}
.vsi-lb[hidden] { display: none; }

.vsi-lb__backdrop {
  position: absolute; inset: 0;
  background: rgba(5, 5, 5, .95);
  backdrop-filter: blur(6px);
}
.vsi-lb__dialog {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: .9rem;
  width: min(100%, 1200px);
  max-height: 100svh;
  padding: 3.2rem 1rem 1rem;
}
.vsi-lb__stage {
  display: grid; place-items: center;
  width: 100%;
  min-height: 0;
  flex: 1 1 auto;
}
.vsi-lb__stage img,
.vsi-lb__stage video {
  display: block;
  width: auto; height: auto;
  max-width: 100%;
  max-height: calc(100svh - 12rem);
  object-fit: contain;
  background: #0b0b0b;
}
.vsi-lb__caption {
  font-family: var(--font-cond); font-weight: 300;
  font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--cs-grey);
  text-align: center;
  max-width: 60ch;
}
.vsi-lb__bar {
  display: flex; align-items: center; gap: 1.2rem;
}
.vsi-lb__nav,
.vsi-lb__close {
  display: grid; place-items: center;
  width: 44px; height: 44px;
  background: none;
  border: 1px solid var(--cs-line-2);
  border-radius: 50%;
  cursor: pointer;
  transition: border-color .3s var(--ease), background-color .3s var(--ease);
}
.vsi-lb__nav svg,
.vsi-lb__close svg {
  width: 1.05rem; height: 1.05rem;
  fill: none; stroke: var(--cs-white); stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}
.vsi-lb__nav:hover,
.vsi-lb__close:hover { border-color: var(--vs-blush); background: rgba(232, 189, 200, .14); }
.vsi-lb__nav:focus-visible,
.vsi-lb__close:focus-visible { outline: 2px solid var(--vs-blush); outline-offset: 2px; }
.vsi-lb__nav:disabled { opacity: .28; cursor: default; }
.vsi-lb__count {
  font-family: var(--font-cond); font-weight: 300;
  font-size: .68rem; letter-spacing: .18em;
  color: var(--cs-dim);
  min-width: 5ch; text-align: center;
}
.vsi-lb__close { position: absolute; top: .9rem; right: .9rem; z-index: 2; }

/* ==================================================== 13 TABLET  ≥768px */
@media (min-width: 768px) {
  :root { --vs-h2: 2.6rem; --slide-gap: 1.25rem; }

  .vsi-hero__copy { max-width: 34ch; }

  .vsi-scale__item { gap: 1.6rem; }

  /* From here up, slides are sized by HEIGHT so the 4:5 and 9:16 chapters
     present at the same optical scale. */
  .vsi-chapter--story    { --slide-h: 58vh; --slide-w: calc(var(--slide-h) * 0.5625); }
  .vsi-chapter--portrait { --slide-h: 58vh; --slide-w: calc(var(--slide-h) * 0.8); }
  .vsi-chapter--feature  { --slide-h: 62vh; }

  .vsi-row--trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .vsi-row--trio .vsi-cell:last-child { grid-column: auto; max-width: none; }
  .vsi-row--duo  { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vsi-row--band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .vsi-row--duo-wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The three asymmetric rows get a tablet composition of their own rather
     than falling through to the one-column phone stack. Without this, 768 to
     1024 — iPad portrait and landscape — showed a 9:16 reel at the full
     measure, which is a two-screen-tall card. */
  .vsi-row--lead { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
  .vsi-row--oversize { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; }
  .vsi-row--split { grid-template-columns: 1.05fr 1fr; align-items: start; }
  .vsi-row--solo { justify-items: center; }
  .vsi-row--solo .vsi-cell { width: min(100%, 26rem); }

  /* The film opens this section, and a 16:9 film in a portrait viewport is a
     220px-tall strip. A floor on the stage lets it letterbox inside a
     cinematic box instead of collapsing to one; a 9:16 reel exceeds the floor
     on its own and is unaffected. */
}

/* =================================================== 14 DESKTOP  ≥1025px
   1025, not 1024: style.css drops --shell to 900px at max-width:1024px, and a
   desktop layer opening at min-width:1024px would collide with it for exactly
   one pixel of viewport — a 900px shell holding a layout built for 1480. */
@media (min-width: 1025px) {
  :root { --vs-h2: 3.2rem; --slide-gap: 1.5rem; }

  .vsi-hero { min-height: 92svh; }
  .vsi-hero__img { object-position: 50% 34%; }
  /* Desktop reads left to right, so the scrim does too. The bottom wash stays
     underneath it for the scroll cue. */
  .vsi-hero__scrim {
    background:
      linear-gradient(to right,
        rgba(9, 9, 9, .96) 0%,
        rgba(9, 9, 9, .94) 18%,
        rgba(9, 9, 9, .84) 30%,
        rgba(9, 9, 9, .42) 46%,
        rgba(9, 9, 9, .1) 64%,
        rgba(9, 9, 9, .04) 78%,
        rgba(9, 9, 9, .26) 100%),
      linear-gradient(to top, rgba(9, 9, 9, .8), transparent 36%);
  }
  .vsi-hero__inner {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 3rem;
    padding-block: 5rem 3rem;
  }
  /* A rem measure, not a ch one. `ch` here resolves against the block's own
     inherited Inter, while the widest line in it is the Oswald scale line at
     .2em tracking — 24ch measured 214px against a line that needs ~330px, and
     "19 Reels" fell to a second row. 22rem holds it on one line and still
     keeps the H1 breaking after "Victoria's", which is the intended lockup. */
  .vsi-hero__copy { max-width: 22rem; }
  .vsi-hero__title { font-size: clamp(3.6rem, 5.4vw, 5.6rem); }
  .vsi-hero__desc { font-size: .95rem; max-width: 38ch; }
  .vsi-hero__cue { margin-top: 0; }

  /* --- scale: three columns of figures ------------------------------- */
  .vsi-scale__list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0 clamp(1.5rem, 3vw, 3rem);
    border-top: 1px solid var(--cs-line);
  }
  .vsi-scale__item {
    flex-direction: column; align-items: flex-start; gap: .8rem;
    padding-block: clamp(2rem, 3.5vw, 3rem);
    border-top: 0;
  }
  .vsi-scale__item:last-child { border-bottom: 0; }
  .vsi-scale__num { font-size: clamp(5rem, 8.5vw, 8.5rem); }
  .vsi-scale__copy {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    align-items: end;
    gap: 2.5rem;
    padding-top: clamp(2rem, 4vw, 3rem);
    border-top: 1px solid var(--cs-line);
  }
  .vsi-scale__fine { margin-top: 0; text-align: right; }

  /* --- THE STICKY RUNWAY --------------------------------------------
     .is-sticky is added by casestudy-vsi.js, and only when the pointer is
     on a desktop-width viewport AND reduced motion is off. Everything in
     this block is therefore opt-in twice over: by breakpoint and by class.

     The track is tall (100vh plus the rail's own overflow, written to
     --travel by the script). The stage pins inside it. The rail is moved
     by transform, never by scrollLeft, so the browser composites it
     instead of re-running layout on every frame. */
  .vsi-chapter.is-sticky .vsi-chapter__track {
    height: calc(100vh + var(--travel, 0px));
  }
  .vsi-chapter.is-sticky .vsi-chapter__stage {
    position: sticky; top: 0;
    height: 100vh;
    display: flex; align-items: center;
    overflow: hidden;
  }
  .vsi-chapter.is-sticky .vsi-chapter__inner {
    display: grid;
    grid-template-columns: minmax(16rem, 24%) minmax(0, 1fr);
    align-items: center;
    gap: clamp(2rem, 4vw, 4rem);
    width: 100%;
    max-width: var(--shell);
    margin-inline: auto;
    padding-left: var(--gutter);
  }
  .vsi-chapter.is-sticky .vsi-chapter__head { padding: 0; }
  .vsi-chapter.is-sticky .vsi-chapter__viewport {
    overflow: hidden;
    scroll-snap-type: none;
  }
  .vsi-chapter.is-sticky .vsi-chapter__rail {
    padding-inline: 0;
    padding-bottom: 0;
    transform: translate3d(var(--x, 0px), 0, 0);
    will-change: transform;
  }
  .vsi-chapter.is-sticky .vsi-slide {
    height: var(--slide-h);
    width: auto;
  }
  /* width:auto all the way down is what lets a slide take its width from the
     photograph rather than the other way round: the <li> has a definite
     height, the button fills it, and the <img> resolves its own width from
     the intrinsic ratio the width/height attributes already declared. That
     is why a 4:5 and a 9:16 chapter can share one rail with no crop. */
  .vsi-chapter.is-sticky .vsi-media { height: 100%; width: auto; }
  .vsi-chapter.is-sticky .vsi-media__img { height: 100%; width: auto; }

  /* the spotlight chapter anchors its rail to the centre of the stage */
  .vsi-chapter--spotlight.is-sticky .vsi-chapter__rail {
    padding-inline: calc((100% - var(--slide-w)) / 2);
  }

  /* Non-sticky desktop (reduced motion, or JS off) still gets height-sized
     slides and a scrollable rail — just without the pinning. */
  .vsi-chapter:not(.is-sticky) .vsi-slide { height: auto; }

  .vsi-chapter--story    { --slide-h: 66vh; }
  .vsi-chapter--portrait { --slide-h: 66vh; }
  .vsi-chapter--feature  { --slide-h: 72vh; }

  .vsi-chapter__title { font-size: clamp(2.2rem, 2.6vw, 3rem); }
  .vsi-chapter__note { font-size: .82rem; }

  /* --- the wall ------------------------------------------------------ */
  .vsi-row { gap: clamp(1.2rem, 2vw, 2rem); padding-block: clamp(1.5rem, 3vw, 2.5rem); }
  .vsi-row--lead     { grid-template-columns: 5fr 4fr 3fr; align-items: start; }
  .vsi-row--reverse  { direction: rtl; }
  .vsi-row--reverse > * { direction: ltr; }
  .vsi-row--duo      { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 64rem; margin-inline: auto; }
  .vsi-row--oversize { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; }
  .vsi-row--trio     { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .vsi-row--split    { grid-template-columns: 1.05fr 1fr; align-items: start; }
  .vsi-row--band     { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .vsi-row--duo-wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vsi-row--solo .vsi-cell { width: min(100%, 30rem); }

  .vsi-break { padding-block: clamp(5rem, 9vw, 9rem); }
  .vsi-break__line { font-size: clamp(3rem, 6vw, 5.6rem); max-width: 20ch; }

  /* --- closing ------------------------------------------------------- */
  .vsi-close__inner {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    align-items: end;
    gap: 3rem;
  }
  .vsi-close__meta { margin-top: 0; text-align: right; }
  .cs-cta, .cs-next { grid-column: 1 / -1; }
}

/* ============================================= 15 LARGE DESKTOP  ≥1440px */
@media (min-width: 1440px) {
  :root { --vs-h2: 3.8rem; }
  .vsi-chapter--story    { --slide-h: 70vh; }
  .vsi-chapter--portrait { --slide-h: 70vh; }
  .vsi-chapter--feature  { --slide-h: 76vh; }
  .vsi-hero__title { font-size: 6rem; }
  .vsi-chapter.is-sticky .vsi-chapter__inner {
    grid-template-columns: minmax(18rem, 22%) minmax(0, 1fr);
  }
}

/* ========================================================= 16 reduced motion
   Stated preference, honoured across the board: no hero zoom, no sticky
   pinning (the JS also refuses to add .is-sticky), no scroll cue pulse, no
   card lift, no crossfades that move. Content is never gated behind any of
   them — the runway is a swipe gallery and the showcase is a poster with a
   play control, both of which work exactly as well standing still. */
@media (prefers-reduced-motion: reduce) {
  .vsi-hero__img { transform: none !important; }
  .vsi-hero__cue-line { animation: none; transform: scaleX(1); opacity: .8; }
  .vsi-media__img,
  .vsi-media__play,
  .vsi-scale__layer,
  .vsi-slide { transition-duration: .01ms !important; }
  .vsi-media:hover .vsi-media__img { transform: none; }
  .vsi-cursor { display: none !important; }
  .vsi-chapter__viewport { scroll-behavior: auto; }
}
