/* ═══════════════════════════════════════════════════════════════════════════
   case-study.css — páginas de estudo de caso (long-form).

   Portado do módulo de referência e religado aos tokens deste portfólio:
   o verde da referência virou o vinho da marca (--color-green), o destaque
   por página virou --cs-accent, e as fontes passaram a Sora. A estrutura,
   os nomes de classe e o comportamento dos componentes são os originais,
   para que o CSS e o JS continuem falando a mesma língua.
   ═════════════════════════════════════════════════════════════════════════ */

body.cs-page {
  /* ── Tokens do módulo ─────────────────────────────────────────────────── */
  --cs-font-display: 'Sora', system-ui, -apple-system, sans-serif;
  --cs-font-ui: 'Sora', system-ui, -apple-system, sans-serif;

  --cs-text-display: clamp(2rem, 4.44vw, 4rem);
  --cs-text-h2: clamp(1.5rem, 6.8vw, 2rem);
  --cs-text-lead: clamp(1.125rem, 1.25vw, 1.25rem);
  --cs-text-body: clamp(1rem, 1.1vw, 1.125rem);
  --cs-text-label: clamp(0.75rem, 0.85vw, 0.875rem);
  --cs-text-meta: clamp(0.75rem, 0.85vw, 0.875rem);
  --cs-text-ui: clamp(0.875rem, 0.95vw, 1rem);

  /* Destaque da página. Na referência isto trocava por projeto (spacefire,
     roxo do dashboard…); aqui sai do coral da marca. Uma página pode
     sobrescrever no próprio <body> se o projeto pedir outra cor. */
  --cs-accent: var(--color-coral);

  --cs-bar-ground: var(--color-cream);
  --cs-rule-scale: 1;
  --duo-gap: 16px;
  --strip-edge: 24px;
  --jn-dot-opacity: 0.18;
}

/* ═══════════════════════════════════════════════════════════════════════════
   case-study.css — long-form case study pages

   Loaded AFTER style.css, by standalone case study pages only (jupiter.html).
   Everything here is layout. Type, colour, radius and the two font families
   all come from style.css tokens — nothing new is invented.

   ── Why a second file ──────────────────────────────────────────────────────
   style.css is already 3.3k lines and owns the landing page plus the two
   .case-* overlays. A 17-section long-form page needs a dozen layout patterns
   that nothing else on the site uses. Bolting them onto the monolith buys
   nothing: no other page loads them. This mirrors the per-component JS split
   already in the project (sticker-field.js, testimonials.js, orbit-reel.js).

   ── .case-* vs .cs-* ───────────────────────────────────────────────────────
   .case-*  = the overlay case studies inside index.html (Wave, Mosaico).
              Short-form: hero, text, metrics, gallery, team.
   .cs-*    = this file. Long-form editorial page, its own URL.
   They share tokens, fonts and the .case-metric-* stat treatment, which this
   file reuses directly rather than re-cutting. They do not share layout.

   ── Mobile-first, deliberately ─────────────────────────────────────────────
   style.css is desktop-first (base rules + max-width: 640px overrides). This
   file goes the other way: base rules are the narrow layout, and min-width
   queries scale up. That is what the brief for this page asked for, and since
   nothing else loads this file the two conventions cannot collide.
═══════════════════════════════════════════════════════════════════════════ */


/* ─── Page shell ──────────────────────────────────────────────────────────
   Cream ground, same as .case-overlay. The landing page's snap scroll,
   Lenis and GSAP are all absent here on purpose — this is a document you
   read, so it uses the browser's own scrolling.                          */

.cs-page {
  background: var(--color-cream);
  color: var(--color-ink);
  /* Crossfade, not a jump — the landing page's own figure for the same
     move. Only the ground travels; foreground tokens stay put, because they
     are flipped per-section by .cs-ground--green rather than page-wide, and
     a page-wide flip would take the cream sections either side of it with
     it. */
  transition: background-color 0.6s ease;

  /* Side margin: 6.4vw matches the .case-overlay mobile inset, 8.33vw the
     desktop one (= --page-margin = --case-margin). Defined on the page rather
     than on .cs because the top bar needs it too. */
  --cs-margin: 6.4vw;

  /* The one width every section's content shares. Matches --meta-pad's own
     card exactly (950px), not a separately-chosen number — the two used to
     be independent, which is what let the info card sit narrower than
     everything around it. Now the card is simply full-width inside a column
     that is already this size; nothing about the card itself needed to
     change. Ground bands (.cs-band) still bleed to the viewport edge for
     their background — that is an existing, separate device for marking a
     real change in what the page is doing, not a column — but their own
     text content is wrapped in .cs-band-copy, which reads this same token,
     so a reader's eye meets the same left edge whether the ground under it
     is cream or dark. */
  --cs-content-max: 950px;

  /* Vertical rhythm. Sections are far apart, blocks inside them are close.
     All multiples of 4. +40px at every breakpoint on instruction (80/120/160
     -> 120/160/200), so the ratio between the three steps is unchanged. */
  --cs-sec-gap: 120px;   /* between numbered sections */
  --cs-blk-gap: 32px;   /* between blocks inside one section */

  --cs-bar-h: 56px;

  /* The article's own top padding, tokenized rather than inlined because
     .cs-hook cancels it — see the quarter-viewport note there. */
  --cs-pad-top: 96px;

  /* Hairline + secondary text.

     #a3a3a3 is the site's de-facto secondary grey — .work-label, .work-date,
     .case-team-label and .work-subtitle all hardcode it — but every one of
     those that reads well lives in #work, on the dark ground, where it
     measures 6.6:1. On this page's cream it is 2.29:1, less than half the
     4.5:1 floor the project sets for body text, and the labels here are real
     content: the spec sheet keys, the benchmark lenses, the persona meta.

     So: dark at 66% over cream instead. Measured 5.1:1. Noticeably darker
     than #a3a3a3, which is the point.

     Worth knowing: .case-team-label in style.css has exactly this problem —
     #a3a3a3 on the cream .case-overlay — and is not fixed here. */
  /* Foreground for everything that sits on the current ground. Cream is the
     page default; a .cs-band--dark redefines this and the three below. */
  --cs-fg:    var(--color-ink);

  /* Cream-on-dark, defined once. Two separate things need these: a dark band,
     and the fixed bar when it is sitting over one. The bar is outside the band
     in the DOM, so it cannot inherit the band's own --cs-label / --cs-rule and
     was carrying its own copies of the numbers. Hoisted here so the bar and
     the band cannot drift to different greys.
     Measured on --color-dark: label 8.4:1, well clear of the 4.5:1 floor. */
  --cs-on-dark-label: rgba(245, 244, 240, 0.72);
  --cs-on-dark-rule:  rgba(245, 244, 240, 0.20);

  --cs-rule:  rgba(30, 30, 32, 0.16);   /* = --color-border in CLAUDE.md */
  --cs-label: rgba(30, 30, 32, 0.66);
  /* A step up from --cs-label for labels that need to out-rank the body they
     sit against — eyebrows and the prologue's annotation keys. Darker, and
     paired with weight 700, so the label reads as a heading of its line rather
     than as quiet metadata. Still short of --cs-fg, which is reserved for real
     headings and running copy. */
  --cs-label-strong: rgba(30, 30, 32, 0.86);
  --cs-tint:  rgba(30, 30, 32, 0.03);   /* placeholder + aside ground */
  --cs-dash:  rgba(30, 30, 32, 0.28);   /* every placeholder outline */

  /* The gap from an eyebrow to the heading it introduces. Deliberately tighter
     than --cs-blk-gap (32-40px): an eyebrow and its heading are one unit, and
     at the block gap they read as two separate things. One value, applied
     wherever an eyebrow sits above a heading. */
  --cs-eyebrow-gap: 20px;

  /* Raised card surface. The §2 info card is the first use; any later section
     that needs a panel lifted off the cream ground pulls these three (via the
     .cs-card class, or by referencing the tokens) so the treatment cannot
     drift from one card to the next. Shadow recipe is the site's own
     resting-card shadow, mixed against --color-dark rather than a bespoke
     rgba so it re-tints if the ink colour ever changes. */
  --cs-card-bg:     var(--color-paper);
  --cs-card-radius: var(--radius-lg);
  --cs-card-shadow: var(--card-shadow);

  /* ── Paired phone screens ────────────────────────────────────────────────
     §3's then/now and §10-20's before/after are the same object: two screens
     of deliberately different size, stood on one baseline over a dot field,
     the later one haloed. These four were §3's private --pro-* values until
     the screen sections adopted the same treatment; they are hoisted here so
     the two compositions cannot drift, which is the whole point of the
     instruction that they match.

     --cs-screen-step  how much larger the second screen is than the first.
                       The size jump IS the argument in both sections.
     --cs-screen-glow  a warm halo marking where the story lands: a coral
                       layer under the neutral card shadow, wide and soft with
                       no offset, so it reads as a glow and not a second
                       shadow.
     --cs-screen-hover how far a hovered screen grows. Felt more than seen.
     --cs-screen-dots  fainter than the landing page's fields (0.32) — this
                       one sits behind artwork, not behind running text. */
  --cs-screen-step:  1.2;
  --cs-screen-glow:  0 2px 44px 4px color-mix(in srgb, var(--cs-accent) 20%, transparent);
  --cs-screen-hover: 1.08;
  --cs-screen-dots:  0.18;

  /* Screens carry a heavier shadow than the rest of the card system, and the
     reason is the artwork rather than the hierarchy: the Jupiter product
     screens are drawn on the app's own cream ground, measured at
     rgb(245,243,239) against this page's rgb(245,244,240). A delta of one
     level in two channels — the edge is invisible, and the shadow is the only
     thing separating a screen from the page it sits on. --cs-card-shadow, at
     14% with a -8px spread, is not enough on its own for that.

     Three layers, no ring: an ambient with no offset so the TOP edge reads
     too (a purely downward shadow leaves it merged), a tight contact, and the
     same soft depth the cards have. The screens must not gain a border —
     that is a standing instruction — so every edge here is drawn with light,
     not with a stroke. Still mixed against --color-dark, so this is the card
     recipe turned up rather than a bespoke colour. */
  --cs-screen-shadow:
    0 0 4px 0    color-mix(in srgb, var(--color-ink)  9%, transparent),
    0 3px 8px -2px color-mix(in srgb, var(--color-ink) 15%, transparent),
    0 14px 32px -10px color-mix(in srgb, var(--color-ink) 20%, transparent);

  /* The page's phone-frame width — what a full-size screen renders at. One
     token so the 1200px step-up below reaches the real screens, the
     placeholders still waiting on assets, and the stepped-down before screen
     together; they were three separate max-widths and drifted apart once. */
  --cs-screen-w:     280px;

  /* ── Cursor bubble (.cs-bubble) ──────────────────────────────────────────
     A FigJam/Figma multiplayer chat bubble: a pointer at one top corner and a
     filled label hanging off it. Tokens live here, not on the component, so a
     later section can retint one bubble without forking the recipe.

     The ground is spacefire exactly — #FC7A69, the brand value, not a darker
     stand-in.

     The ink is white, on instruction, and that is a knowing trade rather than
     an oversight: white on #FC7A69 measures 2.58:1, which fails WCAG AA and
     fails the 3:1 large-text floor too, so no size or weight setting rescues
     it. Dark ink measures 6.44:1 and was shown side by side against it; white
     was chosen anyway, because the alternative that keeps white legible is a
     darkened ground that stops reading as spacefire. The bubble carries a
     short conclusion that is also stated in the running copy around it, so no
     information is only available here. Do not "fix" this by darkening the
     ground — that has been tried and rejected. Re-measure before changing
     either value. */
  --cs-bubble-bg:     var(--cs-accent);
  --cs-bubble-fg:     var(--color-white);
  /* Three corners match the cards the bubble sits under, so a callout and a
     card read as the same family of surface. The fourth — the one the pointer
     attaches to — is nearly square, which is what makes the bubble read as
     belonging to that cursor. That 1px is the only literal radius here and is
     specified by name; see CLAUDE.md's radius exceptions. */
  --cs-bubble-radius:      var(--cs-card-radius);
  --cs-bubble-radius-tip:  1px;
  --cs-bubble-pad:    16px;
  --cs-bubble-cursor: 24px;   /* pointer's bounding box, square */
  /* Clear air between the pointer's tail and the bubble's corner. The two are
     one gesture but two objects, and letting them touch reads as a tooltip
     tail rather than as a cursor with a label. */
  --cs-bubble-cursor-gap: 8px;
  /* Shape-accurate, so it wraps the pointer too — see the filter note on the
     component. Two stops: a tight contact shadow plus a wider, softer cast,
     which is what reads as "hovering" rather than "outlined". */
  --cs-bubble-shadow:
    drop-shadow(0 1px 1px color-mix(in srgb, var(--color-ink) 16%, transparent))
    drop-shadow(0 8px 16px color-mix(in srgb, var(--color-ink) 20%, transparent));
}

/* The article itself. This is what gives the page its vertical rhythm — the
   sections themselves carry no margin, so without this rule every section
   sits flush against the one above and the numbered structure stops reading
   as structure.

   It is also the element that pays --cs-margin, which is what makes
   .cs-band's negative inline margin land on the viewport edge, and pays
   --cs-pad-top, which .cs-hook cancels to start the hero at the top. */
.cs {
  display: flex;
  flex-direction: column;
  gap: var(--cs-sec-gap);
  padding: var(--cs-pad-top) var(--cs-margin) var(--cs-sec-gap);
}

@media (min-width: 641px) {

  .cs-page {
    --cs-margin: calc(6.4vw + 40px);
    --cs-sec-gap: 160px;
    --cs-blk-gap: 40px;
    --cs-bar-h: 64px;
    --cs-pad-top: 120px;
  }
}

@media (min-width: 641px) {
  body.cs-page {
    --cs-text-display: clamp(2.5rem, 4.44vw, 4rem);
    --cs-text-h2: clamp(1.75rem, 2.22vw, 2rem);
  }
}

@media (min-width: 1025px) {

  .cs-page {
    --cs-margin: calc(8.33vw + 40px);
    --cs-sec-gap: 200px;
    --cs-pad-top: 152px;
  }
}

/* ─── Ground bands ────────────────────────────────────────────────────────
   The page was one continuous cream ground for seventeen sections, which is
   why it read flat however well each section was set: nothing told the eye
   where it had arrived. A band is a section that takes over the full width
   and changes the ground under it.

   Used sparingly and structurally, not decoratively — the impact numbers
   (twice, and they are the same numbers, so the ground is what makes the
   second reading land differently) and the untangle. Three moments across a
   long document, each marking a real change in what the page is doing.

   ── How the full-bleed works ───────────────────────────────────────────────
   .cs pays --cs-margin as padding. A band cancels exactly that with a
   negative inline margin and re-pays it as its own padding, so the ground
   reaches the viewport edge while the content column never moves. Because
   both come from the same token they cannot drift apart at any breakpoint.

   Everything inside a band reads its colour from --cs-fg / --cs-label /
   --cs-rule, which the band redefines. Shared type (.cs-h2, .cs-lede) points
   at --cs-fg rather than --color-dark for exactly this reason: one selector
   set works on either ground.                                              */

.cs-band {
  margin-inline: calc(-1 * var(--cs-margin));
  padding: var(--cs-sec-gap) var(--cs-margin);
  background: var(--cs-band-bg, transparent);
}

.cs-band--dark {
  --cs-band-bg: var(--color-ink);
  --cs-fg:    var(--color-cream);
  /* Cream at 72% over --color-dark measures 8.4:1 — well clear of the 4.5:1
     floor, and the same relationship to its ground that --cs-label has to
     cream (5.1:1) rather than a guess at "looks about right". */
  --cs-label: var(--cs-on-dark-label);
  --cs-label-strong: rgba(245, 244, 240, 0.92);
  --cs-rule:  var(--cs-on-dark-rule);
  --cs-tint:  rgba(245, 244, 240, 0.06);
  --cs-dash:  rgba(245, 244, 240, 0.32);
  color: var(--cs-fg);
}

/* ── A full-viewport ground, the landing page's about fold on this page ─────
   Three things make that fold what it is: it owns the whole screen, the page
   crossfades to its colour as it takes over, and the dot field sits on it.
   All three are reproduced here; the type inside is untouched.

   The section paints its own ground AND .cs-page crossfades to the same
   colour underneath it (case-study.js sets data-cs-ground). Both, not either:
   the crossfade is what makes the change read as the page turning over rather
   than a coloured block scrolling past, but it is script-driven, and a
   script-driven background with script-independent cream type on it would be
   cream-on-cream the moment the script did not run. The section's own
   background is what makes the type safe without it.                       */
.cs-ground {
  /* min-height, not height: .cs-band's --cs-sec-gap padding is inside this
     box, and at the narrow end the copy plus that padding is taller than the
     viewport. A fixed height would clip it. */
  min-height: 100svh;
  justify-content: center;
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* Cream on green, matching the landing page's own treatment of this ground.
   Same token-flip shape as .cs-band--dark above, so every shared type rule
   (.cs-h2, .cs-lede, .cs-eyebrow) lands correctly with no per-element
   overrides. */
.cs-ground--green {
  /* Was var(--color-green) (#49a862); now the same near-black the impact
     band and the spectrum tiles use, on instruction -- see --color-charcoal's
     own note in style.css. This incidentally clears a contrast gap the two
     comments below were written around: cream on #49a862 measured 2.70:1,
     under the 3:1 large-text floor; cream on #2b2d33 measures 12.5:1, clear
     of even the 7:1 AAA floor. Left both comments in place rather than
     rewritten, because they still explain why --cs-fg and --cs-label are
     full-opacity cream rather than the page's usual 72% label -- that
     reasoning does not depend on which ground it was measured against, and
     full-opacity cream at 72% flattened over this ground still measures
     7.24:1, so the choice costs nothing here either. */
  --cs-band-bg: var(--color-charcoal);
  /* Cream, on instruction -- reverses the dark-ink swap from two rounds ago.
     That round measured cream at 2.70:1 here, below the 3:1 large-text floor,
     and switched to dark ink (5.60:1) to clear it. Told to keep it light
     regardless: still 2.70:1, still under AA, same number as before. Left as
     a known, named gap rather than silently "fixed" again -- see the
     PORTFOLIO_DIRECTION.md row for the reasoning either way, since this has
     now gone back and forth once. */
  --cs-fg:    var(--color-cream);
  /* Full opacity, on instruction -- was --cs-on-dark-label (cream at 72%,
     the figure every other dark ground on this page uses for its secondary
     text). That 72% was already short of AA here at 2.09:1; full cream only
     brings it up to --cs-fg's own 2.70:1, the same shortfall the heading
     already carries and the same trade-off already named above. Body copy
     and the heading now read as one weight of "still under AA", not two. */
  --cs-label: var(--color-cream);
  --cs-label-strong: rgba(245, 244, 240, 0.92);
  --cs-rule:  var(--cs-on-dark-rule);
  --cs-tint:  rgba(245, 244, 240, 0.06);
  --cs-dash:  rgba(245, 244, 240, 0.32);
  color: var(--cs-fg);
}

/* A band that runs to the top of the next one with no cream seam between. */
.cs-band + .cs-band { margin-top: calc(-1 * var(--cs-sec-gap)); }

/* One section. Blocks inside stack at the tighter rhythm. */
.cs-sec {
  display: flex;
  flex-direction: column;
  gap: var(--cs-blk-gap);
}

.cs-packaging-gallery-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-template-rows: clamp(240px, 29vw, 360px) clamp(240px, 29vw, 360px);
  gap: clamp(8px, 1.2vw, 16px);
}

.cs-packaging-figure { min-width: 0; min-height: 0; }
.cs-packaging-figure:nth-child(1) { grid-column: 1 / 8; grid-row: 1; }
.cs-packaging-figure:nth-child(2) { grid-column: 8 / -1; grid-row: 1 / 3; }
.cs-packaging-figure:nth-child(3) { grid-column: 1 / 8; grid-row: 2; }

.cs-screen-gallery-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: auto;
}
.cs-screen-gallery-grid .cs-packaging-figure:nth-child(n) { grid-column: auto; grid-row: auto; }
.cs-screen-gallery-grid .cs-packaging-image {
  aspect-ratio: 4 / 3;
  height: auto;
  overflow: hidden;
  border-radius: 16px;
  background: #eef1f5;
}
.cs-screen-gallery-grid .cs-packaging-image img { height: 100%; }

.cs-packaging-gallery-grid--portrait {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: clamp(360px, 44vw, 640px);
}
.cs-packaging-gallery-grid--portrait .cs-packaging-figure:nth-child(n) {
  grid-column: auto;
  grid-row: auto;
}
.cs-packaging-gallery-grid--portrait .cs-packaging-image img { object-fit: cover; }

.cs-packaging-figure {
  min-width: 0;
  margin: 0;
}

.cs-packaging-image {
  display: grid;
  width: 100%;
  height: 100%;
  place-items: center;
  padding: 0;
  overflow: hidden;
  background: transparent;
  border: 0;
  cursor: zoom-in;
}

.cs-packaging-image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: transform 360ms cubic-bezier(.2, .7, .2, 1);
}

.cs-packaging-image:hover img { transform: scale(1.025); }
.cs-packaging-image:focus-visible {
  outline: 3px solid var(--cs-accent);
  outline-offset: 4px;
}

.cs-gallery-lightbox {
  position: fixed;
  inset: 0;
  grid-template-columns: 56px minmax(0, 1fr) 56px;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 20px;
  width: 100vw;
  height: 100vh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: clamp(68px, 8vw, 96px) clamp(16px, 4vw, 64px) 28px;
  overflow: hidden;
  color: #fff;
  background: #171615;
  border: 0;
}

.cs-gallery-lightbox[open] { display: grid; }
.cs-gallery-lightbox::backdrop { background: rgb(12 11 10 / 88%); }

.cs-gallery-lightbox-image {
  grid-column: 2;
  grid-row: 1;
  display: block;
  width: 100%;
  height: 100%;
  max-height: calc(100dvh - 170px);
  justify-self: center;
  object-fit: contain;
}

.cs-gallery-close,
.cs-gallery-arrow {
  display: grid;
  width: 48px;
  height: 48px;
  place-items: center;
  padding: 0;
  color: #fff;
  background: rgb(255 255 255 / 12%);
  border: 1px solid rgb(255 255 255 / 22%);
  border-radius: 50%;
  cursor: pointer;
  font: 400 30px/1 var(--cs-font-ui);
}

.cs-gallery-close {
  position: absolute;
  top: 20px;
  right: 24px;
  font-size: 30px;
}

.cs-gallery-arrow { align-self: center; }
.cs-gallery-arrow--prev { grid-column: 1; grid-row: 1; }
.cs-gallery-arrow--next { grid-column: 3; grid-row: 1; }
.cs-gallery-arrow[hidden] { display: none; }

.cs-gallery-close:hover,
.cs-gallery-arrow:hover { background: rgb(255 255 255 / 22%); }

.cs-gallery-close:focus-visible,
.cs-gallery-arrow:focus-visible { outline: 3px solid var(--cs-accent); outline-offset: 3px; }

.cs-gallery-lightbox-meta {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  min-height: 28px;
  color: rgb(255 255 255 / 76%);
  font: 500 var(--cs-text-label)/1.4 var(--cs-font-ui);
}

.cs-gallery-lightbox-meta p { margin: 0; }
}

@media (max-width: 640px) {
  .cs-packaging-gallery-grid { grid-template-columns: 1fr; grid-template-rows: auto; }
  .cs-packaging-figure:nth-child(n) { grid-column: 1; grid-row: auto; }
  .cs-packaging-image { height: auto; aspect-ratio: auto; }
  .cs-packaging-image img { height: auto; }
  .cs-screen-gallery-grid .cs-packaging-image { aspect-ratio: 4 / 3; }
  .cs-screen-gallery-grid .cs-packaging-image img { height: 100%; }
  .cs-gallery-lightbox {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
    gap: 12px;
    padding: 72px 16px 20px;
  }
  .cs-gallery-lightbox-image { grid-column: 1 / -1; max-height: calc(100dvh - 150px); }
  .cs-gallery-arrow { position: absolute; top: 50%; transform: translateY(-50%); }
  .cs-gallery-arrow--prev { left: 12px; }
  .cs-gallery-arrow--next { right: 12px; }
  .cs-gallery-lightbox-meta { grid-column: 1 / -1; }
}

/* One column, one width, used everywhere. :not(.cs-band) is the only carve-out
   — a band's own element has to stay full-bleed for its background, so ITS
   content is capped by .cs-band-copy one level in instead. Every plain
   section gets this for free with no markup change: the flex stretch above
   already gives each .cs-sec the full padded width, so a max-width narrower
   than that plus an auto inline margin is enough to centre it exactly like a
   block element would. */
.cs-sec:not(.cs-band) {
  /* width:100% is doing the real work; margin-inline:auto is only there to
     centre what max-width then trims off. Tried align-items:stretch on .cs
     first, on the theory that its initial value ("normal") wasn't resolving
     to stretch — it was already resolving correctly, and stretch was never
     the problem. The actual cause: per the flexbox spec, an "auto" cross-axis
     margin on a flex item cancels stretch outright and falls back to the
     item's own shrink-to-fit width, THEN centres that with the margin. So
     any section whose content was naturally narrower than 950px — a lone
     heading and one diagram frame, a short paragraph — sat at its own
     content width instead of the shared one; sections with enough content to
     fill 950px anyway (most of them) looked correct by accident. width:100%
     is an explicit, non-auto value, which keeps stretch out of the decision
     entirely: the box is always min(available width, 950px), full stop. */
  width: 100%;
  max-width: var(--cs-content-max);
  margin-inline: auto;
}

/* Reusable raised panel on the cream ground. Carries only the surface — colour,
   corner, shadow — from the --cs-card-* tokens; a component that uses it owns
   its own padding and internal layout (see .cs-meta). Add it to any future
   section that needs the same lifted card as §2's info card. */
.cs-card {
  background: var(--cs-card-bg);
  border-radius: var(--cs-card-radius);
  box-shadow: var(--cs-card-shadow);
}

/* ─── .cs-bubble — the cursor callout ──────────────────────────────────────
   A multiplayer cursor bubble, borrowed from Figma/FigJam: a pointer sitting
   at one top corner with a filled label hanging off it, so a remark reads as
   someone pointing at the thing above it rather than as more page copy.

   Markup is the class and a direction, nothing else — the pointer is drawn by
   the component:

       <aside class="cs-bubble cs-bubble--left" data-motion="bubble">
         <p class="cs-eyebrow">The takeaway</p>
         <p class="cs-bubble-line">...</p>
       </aside>

   --left puts the pointer at the top-left corner aiming up and out to the
   left; --right mirrors it. Pick whichever aims at the thing being remarked
   on; the bubble then grows away from that corner, so the copy always lands
   on the side with room for it.

   Three things here are load-bearing:

   1. It hugs. width: fit-content, so a three-word bubble is three words wide.
      --cs-bubble-measure is only a ceiling for when the text is long enough
      to need a reading measure; short content never reaches it.

   2. The shadow is a `filter`, not a `box-shadow`. box-shadow follows the
      border box, which would draw a rounded rectangle's shadow and leave the
      pointer flat and pasted-on. drop-shadow works off rendered alpha, and a
      filter on an element applies to its pseudo-elements too, so one
      declaration wraps bubble and pointer as a single silhouette. The white
      text casts nothing, because it sits on an opaque fill and so does not
      change that silhouette.

   3. The corner nearest the pointer is --cs-bubble-radius-tip (1px) and the
      other three are --cs-bubble-radius (4px). Both are literal values rather
      than --radius, at Shriya's explicit direction, and the asymmetry is the
      point: a near-square corner reads as attached to the cursor, while a
      matched 4px would read as a rectangle that happens to have an arrow near
      it. See CLAUDE.md's radius-exception list.

   The pointer is two stacked masked boxes: ::before is the dart in white,
   ::after the same dart inset by 1px in spacefire, which leaves a 1px white
   border running the whole way around the fill, as Figma's own cursors have.
   The inset comes from the masks themselves rather than from scaling the
   box — each mask fills AND strokes the same path with a round linejoin (the
   stroke is also what rounds the dart's three corners, which a bare polygon
   cannot do), and a stroke grows a silhouette by half its width. 2.6 against
   0.93 is a 0.835-unit ring, which at the 1.2x this 20-unit viewBox is scaled
   to inside a 24px box lands on 1px. Change --cs-bubble-cursor and the ring
   scales with it, which is the right behaviour: a keyline that stayed 1px on
   a 48px cursor would read as hairline dirt. */
.cs-bubble {
  --cs-bubble-measure: 34em;
  /* The dart, pointing up and left, at two sizes: the outer silhouette and
     the same path 1px in. `black` rather than a hex so the data URI needs no
     escaped #. */
  --cs-bubble-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2 2 L17 9.2 L9.8 9.8 L9.2 17 Z" fill="black" stroke="black" stroke-width="2.6" stroke-linejoin="round"/></svg>');
  --cs-bubble-mask-inner: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2 2 L17 9.2 L9.8 9.8 L9.2 17 Z" fill="black" stroke="black" stroke-width="0.93" stroke-linejoin="round"/></svg>');

  position: relative;
  width: fit-content;
  max-width: var(--cs-bubble-measure);
  /* Reserves exactly the part of the pointer that overhangs the top edge, so
     the bubble can be dropped under anything without the dart colliding. */
  margin-top: calc(var(--cs-bubble-cursor) / 2 + var(--cs-bubble-cursor-gap));
  padding: var(--cs-bubble-pad);
  border-radius: var(--cs-bubble-radius);
  background: var(--cs-bubble-bg);
  color: var(--cs-bubble-fg);
  filter: var(--cs-bubble-shadow);
}

.cs-bubble::before,
.cs-bubble::after {
  content: '';
  position: absolute;
  width: var(--cs-bubble-cursor);
  height: var(--cs-bubble-cursor);
  /* The dart's notch sits at the centre of its own box, so pulling the box
     back by half the cursor would land that notch exactly on the bubble's
     corner. --cs-bubble-cursor-gap then pulls it further out again, which is
     what opens the air between the tail and the corner. */
  top: calc(var(--cs-bubble-cursor) / -2 - var(--cs-bubble-cursor-gap));
}

.cs-bubble::before {
  background: var(--color-white);
  -webkit-mask: var(--cs-bubble-mask) center / contain no-repeat;
  mask: var(--cs-bubble-mask) center / contain no-repeat;
}

.cs-bubble::after {
  background: var(--cs-bubble-bg);
  -webkit-mask: var(--cs-bubble-mask-inner) center / contain no-repeat;
  mask: var(--cs-bubble-mask-inner) center / contain no-repeat;
}

/* --- Direction ---------------------------------------------------------- */

/* The pointer's position is the only place this geometry is written down; the
   `bubble` motion primitive measures the dart's computed box to find its
   growth anchor rather than keeping a second copy of these numbers. */
.cs-bubble--left { border-top-left-radius: var(--cs-bubble-radius-tip); }

.cs-bubble--right {
  border-top-right-radius: var(--cs-bubble-radius-tip);
  margin-left: auto;
}

.cs-bubble--left::before,
.cs-bubble--left::after { left: calc(var(--cs-bubble-cursor) / -2 - var(--cs-bubble-cursor-gap)); }

.cs-bubble--right::before,
.cs-bubble--right::after {
  right: calc(var(--cs-bubble-cursor) / -2 - var(--cs-bubble-cursor-gap));
  transform: scaleX(-1);
}

/* --- Contents ----------------------------------------------------------- */

/* The eyebrow keeps its size, weight and tracking and only changes ink — the
   grey it carries on cream is under 2:1 on this ground. Solid, not a
   translucent one: at --text-label this is small text and needs the full
   4.5:1, which any alpha below 1 gives up. Hierarchy inside the bubble comes
   from size and case instead, which is what it came from outside too. */
.cs-bubble .cs-eyebrow {
  color: var(--cs-bubble-fg);
  /* 400, not the eyebrow's usual 700: a bubble sets no bold anywhere, and this
     was the last of it. Geist loads 400 and 700 only — there is no 500 to
     split the difference with (see CLAUDE.md) — so it goes all the way down.
     The label still reads as a label on case and tracking alone, which is
     most of what was carrying it. */
  font-weight: 400;
  margin-bottom: 8px;
}

.cs-bubble-line {
  font-family: var(--cs-font-display);
  font-weight: 500;
  font-size: var(--cs-text-lead);
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--cs-bubble-fg);
  margin: 0;
}

/* .cs-bubble-hi was here — the closing clause at weight 700, carrying the
   emphasis the spacefire accent used to. Removed on instruction: a bubble
   sets no bold. The clause is the end of the sentence and lands on its own. */

/* The text half of a ground band — heading and lede — sized and
   centred the same way as everything else on the page. The band's own
   diagram or number layout is NOT inside this: it stays a direct child of
   the section so it can keep using the section's real full-bleed width for
   whatever visual device it needs (the untangle diagram's own pinned canvas,
   for one). */
.cs-band-copy {
  /* width:100% first, same reasoning as .cs-sec:not(.cs-band) above — an auto
     margin alone would let this shrink to its own content's width instead of
     the shared one. */
  width: 100%;
  max-width: var(--cs-content-max);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: var(--cs-blk-gap);
}

/* ─── Shared type ─────────────────────────────────────────────────────────
   Three roles, used by every section. Nothing else sets font-family. */

/* Section heading.

   The page used to carry only tiny grey eyebrows, which meant it had no
   typographic hierarchy at all between the hook and the body — every section
   opened at the same whisper, and nothing announced itself while scrolling.

   Sized between the body and the hook so it reads as a chapter without
   competing with the thesis: 24px on a phone, 48px at 1440. None of the
   :root type tokens sits in that gap — --text-body tops out at 36 and
   --text-case starts at 64, which is the hook's own size — so this is a
   local token rather than a hijacked one.

   The 1.5rem floor is set against the hook's, not chosen for its own sake.
   --text-case bottoms out at 2rem, and a 1.75rem floor here put the two
   within 4px of each other on a phone — both clamped, hierarchy gone. At
   1.5rem the heading stays three-quarters of the hook at every width. */
/* Promoted to style.css as --text-case-h2 on 2026-09-29, so the About-me
   overlay's reel headline can be sized to the same scale without index.html
   having to load this stylesheet. The clamp itself is unchanged — the value
   now has one home, and .cs-h2 keeps its local alias so every rule below
   still reads in this file's own terms. */
.cs-page { --cs-h2: var(--cs-text-h2); }

.cs-h2 {
  font-family: var(--cs-font-display);
  /* 500, matching the project header (.cs-hook-line, --hook-title-weight: 500)
     — every section heading from the prologue on carries the same weight as
     the title of the piece. Also what CLAUDE.md's type scale already assigns
     to "the case-study page's sub-headings"; this was 700 before, a drift from
     that. .cs-screen-framing (§10-20) matches. */
  font-weight: 500;
  font-size: var(--cs-h2);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--cs-fg);
  max-width: 18em;
  margin: 0;
}

/* Eyebrow — the small uppercase label that introduces a section heading.
   Weight 700 for emphasis over the body, but the label grey rather than the
   strong ink: on instruction, every eyebrow on the page is now grey. The
   heading right below carries the section; the eyebrow just names it. */
.cs-eyebrow {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cs-label);
  margin: 0;
}

/* An eyebrow and the heading it introduces are one unit, so the space between
   them is --cs-eyebrow-gap (20px), tighter than the --cs-blk-gap the section's
   flex layout puts between every other pair of blocks. Expressed as a negative
   top margin that cancels the block gap down to the eyebrow gap, so it stays
   20px at both the 32px and 40px block-gap breakpoints. Applies anywhere an
   eyebrow precedes a heading. */
.cs-eyebrow + .cs-h2,
.cs-eyebrow + .cs-display,
.cs-eyebrow + .cs-results {
  margin-top: calc(var(--cs-eyebrow-gap) - var(--cs-blk-gap));
}

/* Where a subhead follows that heading, it now closes to the same
   --cs-eyebrow-gap rather than the section's normal --cs-blk-gap — on
   instruction, so an eyebrow/heading/subhead group reads as one tight unit
   with even spacing throughout, not 20px then 32-40px. Scoped to headings
   that sit directly after an eyebrow: a heading with no eyebrow above it
   keeps the section's ordinary block rhythm before its own subhead. */
.cs-eyebrow + .cs-h2 + .cs-lede,
.cs-eyebrow + .cs-display + .cs-lede {
  margin-top: calc(var(--cs-eyebrow-gap) - var(--cs-blk-gap));
}

/* The page's largest type. Same recipe as .case-title. */
.cs-display {
  font-family: var(--cs-font-display);
  font-weight: 700;
  font-size: var(--cs-text-display);
  line-height: 1.125;
  letter-spacing: -0.02em;
  color: var(--color-ink);
  margin: 0;
}

/* Section-opening statement — between display and body. */
.cs-lede {
  font-family: var(--cs-font-display);
  font-weight: 400;
  font-size: var(--cs-text-body);
  line-height: 1.45;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
  /* 32em, not 24. The source's subheads are real paragraphs of 40-60 words,
     and at 24em they wrapped to six or seven short lines that read as a
     column of fragments. 32em holds roughly 60-65 characters, which is inside
     the comfortable measure and stops the ragging. */
  max-width: 32em;
  margin: 0;
}

/* ─── Top bar ─────────────────────────────────────────────────────────────
   Carries the one persistent control: back to the portfolio.

   It was a floating corner button first. That does not work on this page: the
   content column starts at --cs-margin (40px at 630px wide, 24px on a phone),
   so any floating control sits on top of the running text — measured, it
   covered the first two characters of a section lede and cut through a
   paragraph in the deep dive. There is no gutter to hide in at any width the
   page actually gets read at.

   A full-width opaque bar has the same footprint but reads as chrome rather
   than as something broken, and it is the pattern the landing page's own nav
   already uses. Content scrolling under it is then expected.               */

/* The bar themes with whatever ground is under it, the way the landing page's
   nav does with body[data-bg]. Without this it stays a cream slab sitting on
   top of a dark band, which reads as a rendering fault rather than as chrome.
   The state is set by case-study.js from scroll position, so it works with or
   without GSAP. */
/* Set by case-study.js when a .cs-ground section holds the middle of the
   viewport. The section is 100svh, so at the moment this flips it already
   fills the screen — which is what keeps the crossfade off the cream
   sections above and below it.

   "green" here is a STATE NAME, not a colour any more -- case-study.js reads
   it off the section's own data-ground="green" attribute, and renaming that
   plumbing was out of scope for a colour swap. What the state paints is
   --color-charcoal now, same as .cs-ground--green itself; the crossfade has
   to match the section it is leading into or the "page turning over" effect
   reads as a colour mismatch instead. */
/* "dark" is §17's state, and unlike "green" it IS the colour's name. Same
   device, same 0.6s on .cs-page above: the page turns over to the section's
   ground while that section holds the middle of the viewport, instead of a
   coloured block sliding past a cream page with a hard edge at each end.

   ── The foreground travels too, and it has to ──────────────────────────────
   .cs-page's own comment above says only the ground moves, "because a
   page-wide flip would take the cream sections either side of it with it".
   Taking them with it is the POINT, and not doing so was a bug.

   The claim this rested on -- that a 100svh ground "already fills the screen
   at the moment this flips, which is what keeps the crossfade off the cream
   sections above and below it" -- is not true of a -50%/-50% root margin.
   That margin is a zero-height line across the middle of the viewport, so the
   state turns on when the section's TOP crosses it: the section covers the
   bottom half and a cream neighbour still holds the top half. Measured on
   §5, the one the comment was written about: 309px of §4's tail is on screen
   when it flips, and 308px of §6's head when it leaves. It went unnoticed
   only because those two strips happen to be near-empty. §16's tail is not --
   it is the "After" caption, and it went dark-grey-on-charcoal.

   So this does what the landing page does: body[data-bg="green"] sets
   background-color AND color, and the whole fold turns over together. Same
   token set .cs-band--dark flips, so a cream section caught mid-fade reads
   as cream type on a darkening ground rather than as dark type disappearing
   into it. .cs-band--dark and .cs-ground--green set these on themselves and
   are more specific inside their own subtrees, so the dark sections are not
   affected by this at all -- it only reaches the transparent ones. */
.cs-page[data-cs-ground="green"],
.cs-page[data-cs-ground="dark"] {
  background: var(--color-charcoal);
  --cs-fg:    var(--color-cream);
  --cs-label: var(--cs-on-dark-label);
  --cs-label-strong: rgba(245, 244, 240, 0.92);
  --cs-rule:  var(--cs-on-dark-rule);
  --cs-tint:  rgba(245, 244, 240, 0.06);
  --cs-dash:  rgba(245, 244, 240, 0.32);
}

/* Custom properties do not transition, so without this the type would SNAP
   to cream while the ground took 0.6s to arrive -- the one thing more
   noticeable than the dark text was. Listed by role rather than applied with
   a wildcard: these are the type classes that can appear in the strip a
   crossfade exposes, and a blanket transition would also catch every
   component that animates colour on hover and slow it to 0.6s. */
.cs-page .cs-eyebrow,
.cs-page .cs-h2,
.cs-page .cs-lede,
.cs-page .cs-sub,
.cs-page .cs-sub strong,
.cs-page .cs-pro-key,
.cs-page .cs-pro-text,
.cs-page .cs-screen-title,
.cs-page .cs-screen-framing {
  transition: color 0.6s ease;
}

/* ── The bar's ground ──────────────────────────────────────────────────────
   One declaration for all three states. --cs-bar-ground is written onto the
   body by case-study.js, read off the band the bar is actually over, so the
   bar cannot sit a shade off the thing behind it; the fallback is only for
   the frame before the first paint and for the no-JS path.

   It also follows the page's own crossfade. While data-cs-ground is set the
   page is charcoal everywhere, including the strip under the bar, so the bar
   has to travel with it — otherwise it stays cream over a charcoal page for
   the length of the fade. Both selectors resolve to the same colour, and the
   transition below is 0.6s to match .cs-page's exactly rather than finishing
   ahead of it. */
.cs-page[data-cs-bar="dark"] .cs-bar,
.cs-page[data-cs-bar="green"] .cs-bar,
.cs-page[data-cs-ground="green"] .cs-bar,
.cs-page[data-cs-ground="dark"] .cs-bar {
  background: var(--cs-bar-ground, var(--color-charcoal));
  border-bottom-color: var(--cs-on-dark-rule);
}

.cs-page[data-cs-bar="dark"] .cs-back,
.cs-page[data-cs-bar="green"] .cs-back,
.cs-page[data-cs-ground="green"] .cs-back,
.cs-page[data-cs-ground="dark"] .cs-back { color: var(--color-cream); }

.cs-page[data-cs-bar="dark"] .cs-progress,
.cs-page[data-cs-bar="green"] .cs-progress,
.cs-page[data-cs-ground="green"] .cs-progress,
.cs-page[data-cs-ground="dark"] .cs-progress { background: var(--color-cream); }

/* The separate "green" bar block that used to sit here is folded into the one
   above. It existed because that state wanted --color-charcoal while the
   generic dark state wanted --color-dark; now the colour comes off the
   section itself for every state, so there is nothing left for it to say.
   Cream on charcoal measures 12.5:1, which is what made that distinction
   safe to drop rather than merely tidy. */

/* Colour is the only thing that changes. 0.6s, not 0.4: it is the same figure
   .cs-page uses for its own crossfade, and while data-cs-ground is set these
   two are painting the same colour over the same moment. At 0.4 the bar
   arrived first and read as a slab that had changed independently of the
   page under it. */
.cs-bar,
.cs-bar .cs-back,
.cs-progress {
  transition: background-color 0.6s ease, color 0.6s ease, border-color 0.6s ease;
}

.cs-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--cs-bar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 var(--cs-margin);
  background: var(--color-cream);
  border-bottom: 1px solid var(--cs-rule);
}

/* Both bar controls carry a 44px-tall touch box. The bar is a fixed 56/64px,
   so the extra height is paid as vertical padding and negative inline margin
   rather than by growing the control's visible box: nothing moves, and the
   hit area stops being the 16px cap-height it was measured at. */
.cs-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding-block: 12px;
  margin-block: -12px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink);
  text-decoration: none;
  transition: opacity 0.25s ease;
}

.cs-back:hover { opacity: 0.6; }

.cs-back svg { display: block; }

.cs-back-transition {
  position: fixed;
  z-index: 10000;
  inset: 0;
  background: var(--color-cream);
  pointer-events: none;
  will-change: clip-path;
}

/* Grid and flex children default to min-width: auto, which refuses to shrink
   below their content's minimum width. For anything holding a .cs-ph that
   minimum is circular: the placeholder has an aspect-ratio, its label wraps to
   more lines as the box narrows, the extra height feeds back through the ratio
   as extra width, and the track settles wider than the container it sits in.
   Measured at 375px before this rule: a .cs-ba card resolved to 352.3px inside
   a 327px grid, pushing the document 1px past the viewport.

   min-width: 0 lets these shrink to the track instead of dictating it. */
.cs-ph,
.cs-duo-face, .cs-approach, .cs-spec-step, .cs-sub, .cs-voice, .cs-ba-shot {
  min-width: 0;
}

/* A real screen capture standing in a .cs-ph--phone's slot. It carries the
   SAME 9/19.5 as the placeholder, so a before shot, an after shot and a slot
   still waiting on its asset are all one size — every screen on this page
   renders identically whatever its source happened to measure.

   Every asset is normalised to that ratio when it is added (artboard frame
   cropped off, rounded corners squared, then resampled), so `cover` has
   nothing to crop today; it is here so a future screen that arrives a few
   pixels off ratio loses a hair off one edge instead of breaking the row.

   The shadow is the card system's recipe turned up (--cs-screen-shadow, mixed
   against the same --color-dark), so these still read as the same family as
   the benchmark previews and the info card — but heavier, because several of
   the captures are drawn on a cream that measures one level off this page's
   own ground and would otherwise have no visible edge at all. */
.cs-shot-img {
  display: block;
  width: 100%;
  max-width: var(--cs-screen-w);
  aspect-ratio: 9 / 19.5;
  height: auto;
  object-fit: cover;
  border-radius: var(--radius);
  box-shadow: var(--cs-screen-shadow);
}

/* ═══ §1 — Hook ══════════════════════════════════════════════════════════ */

/* Centred, unlike every other section on the page — the opening is a title
   card, and the rest is a document. The switch is deliberate and does not
   propagate: .cs-sec's own children stay left-aligned everywhere else.

   The whitespace above the icon is re-paid from the top of <main>: the article
   pays --cs-pad-top to clear the fixed bar, .cs-hook cancels that with a
   negative margin and pays --hook-space (26svh) instead. The bar then covers
   the top --cs-bar-h of it, so the VISIBLE gap above the icon is --hook-clear
   (--hook-space minus the bar). That visible number is what the gap below the
   subhead is matched to — see .cs-hook's margin-bottom — so the fold no longer
   reads lopsided. svh not vh so a mobile browser's collapsing toolbar cannot
   change where the icon sits mid-scroll.

   ── Every value in this fold is a custom property ──
   Not decoration: it is what makes the fold dialable. case-study-dial.js
   (?dial) overrides these on .cs-page and nothing else, so the panel needs no
   knowledge of these selectors, and an untouched control leaves the CSS
   default — including its clamp() — completely intact. Defaults live here,
   not in the JS, so deleting the dial kit changes nothing on the page.

   ── Dialled and finalised 2026-09-02 ──
   The two font sizes came out of the panel as fixed px (60 and 24), which is
   what a slider can express. Shipping them as fixed px would have put a 60px
   headline on a 375px phone, so both are re-expressed as clamps whose CEILING
   is the dialled value:

       60px -> clamp(2rem, 5vw, 3.75rem)     3.75rem = 60px
       24px -> clamp(1rem, 2vw, 1.5rem)      1.5rem  = 24px

   The vw terms are set so each one reaches its ceiling by ~1200px and stays
   there. That means every desktop width — 1280, 1440, 1512, 1920 — renders
   exactly the dialled number, so it does not matter which viewport the tuning
   happened at, and the type still scales down instead of overflowing a phone.

   --hook-gap came back as 24px, identical to the default, so it is left alone
   rather than restated. --hook-icon, --hook-sub-weight and --hook-sub-lh were
   never touched in the panel and keep their defaults. */
.cs-page {
  --hook-space:         26svh;
  /* The whitespace a reader sees before the title: --hook-space minus the
     fixed bar. The margin below the subhead uses the same value to balance
     the opening. */
  --hook-clear:         calc(var(--hook-space) - var(--cs-bar-h));
  --hook-gap:           24px;

  --hook-title-size:    clamp(2rem, 5vw, 3.75rem);   /* -> 60px from ~1200px up */
  --hook-title-weight:  500;
  --hook-title-lh:      1.12;
  --hook-title-measure: 13.5em;

  --hook-sub-size:      clamp(1rem, 2vw, 1.5rem);    /* -> 24px from ~1200px up */
  --hook-sub-weight:    400;
  --hook-sub-lh:        1.45;
  --hook-sub-measure:   25em;

  /* An opaque grey, not the translucent dark --cs-label the fold's other greys
     use. --cs-label is rgba(30,30,32,0.66), which on this cream renders as
     ~#6A6A6C — this is that same rendered value, set solid. The subhead carries
     the 🇮🇳 flag emoji: a colour with an alpha channel drops a translucency
     veil over everything painted in the element, and the emoji (a full-colour
     glyph, not a monochrome one tinted by `color`) visibly dulls under it. A
     solid grey keeps the type at the same weight on the page and lets the flag
     render at full strength. Contrast on cream: 5.2:1, well over the body floor. */
  --hook-sub-color:     #6A6A6C;

  /* Spacefire. This hex is the brand value, given by name — if a brief says
     "spacefire", it means #FC7A69 and nothing nearby. See the measurement
     note on .cs-hook-highlight for what that costs in contrast. */
  --hook-highlight:     #FC7A69;

  /* The site's only red, and it carries meaning rather than decoration: a
     thing that went wrong. Spacefire is the brand's warm accent and reads as
     positive, so it cannot also be the failure colour — §6 uses both at once
     and needs them to say opposite things.

     #C9302A measures 5.10:1 on cream, and 2.06 against spacefire itself, which
     is what keeps the two legible as different signals rather than as two
     shades of the same one. Do not reach for this for emphasis. */
  --cs-negative:        #C9302A;
}

.cs-hook {
  align-items: center;
  text-align: center;
  gap: var(--hook-gap);
  margin-top: calc(-1 * var(--cs-pad-top));

  /* --hook-space (26svh) from the top of <main>. The fixed bar covers the top
     --cs-bar-h of that, so what the reader sees before the title is
     --hook-clear (= --hook-space - --cs-bar-h). The gap below matches it. */
  padding-top: var(--hook-space);

  /* Make the gap below the subhead equal the VISIBLE gap before the title
     (--hook-clear), not the generic --cs-sec-gap every other boundary uses.
     .cs already lays --cs-sec-gap here via flex `gap`; this margin is the
     difference — a small positive top-up at desktop (--hook-clear 170 vs gap
     160), a negative trim where the gap is larger. Formula, so it tracks
     --hook-space, --cs-bar-h and --cs-sec-gap at every breakpoint. The max()
     floor keeps ≥24px of real gap on a pathologically short viewport where
     --hook-clear would otherwise collapse it. */
  margin-bottom: max(
    calc(24px - var(--cs-sec-gap)),
    calc(var(--hook-clear) - var(--cs-sec-gap))
  );
}

/* .cs-hook's own margin-bottom above is tuned to ITS internal rhythm — the
   gap under the subhead matches the visible space before the title, not the
   page's generic --cs-sec-gap. Good for the hero on its own terms, but it
   meant the meta card directly below inherited that tuning as ITS gap
   above, while its gap below (to whatever follows) is the plain
   --cs-sec-gap from .cs's flex `gap` — measured 190px above the card
   against 160px below it, asymmetric by exactly the hero's own top-up.

   This cancels that top-up rather than touching .cs-hook, which stays
   correct for the hero. Negating the identical expression keeps the two
   in lockstep at every breakpoint — --hook-space is in svh and --cs-bar-h
   and --cs-sec-gap both step at 640/1024, so a fixed pixel here would only
   have cancelled it at one width. */
.cs-hook + .cs-sec {
  margin-top: calc(-1 * max(
    calc(24px - var(--cs-sec-gap)),
    calc(var(--hook-clear) - var(--cs-sec-gap))
  ));
}

/* The hook line itself — the biggest type on the page. Narrower than the old
   left-aligned cap: centred type needs a tighter measure to hold its shape,
   or the ragged edges read as accidental rather than composed.

   Size/weight/leading are restated here rather than inherited from
   .cs-display so the dial can reach them. Equal specificity, later in the
   file, so these win. */
.cs-hook-line {
  font-size: var(--hook-title-size);
  font-weight: var(--hook-title-weight);
  line-height: var(--hook-title-lh);
  max-width: var(--hook-title-measure);
}

.cs-hook-logo {
  display: block;
  width: min(100%, clamp(14rem, 44vw, 42rem));
  height: auto;
  margin-inline: auto;
}

/* The one number the reader should carry away from the fold. Highlighted
   rather than the surrounding words: it is the single scannable proof-point
   in the sentence — "onboarding" and "deposits" are context, the figure is
   the claim — and colouring only it keeps the rest of the line doing its job
   as plain, confident type instead of competing for the same attention.

   Colour is derived from the Jupiter icon's own coral, sampled directly from
   img/jupiter-icon.jpg (averaged, white glyph and black excluded: #F77468 —
   HSL H5 L69 S89) rather than picked by eye. That sample measures 2.7:1 on
   this page's cream. Two darker stand-ins were tried here — #f43d2f, then
   #ea5e3e at 3.26:1 — both chosen to buy contrast. Neither is spacefire.
   Shriya named the brand value directly: spacefire is #FC7A69, so that is
   what the token holds now.

   The cost, measured: #FC7A69 is 2.47:1 on cream. That is under the 3:1 floor
   this project holds even for large text, so the $2.4M is now below AA for
   its size where before it was marginally over. It is three display-size
   words inside a sentence that reads identically without colour, and the
   brand hex is not a judgement call this file gets to make — but it IS a
   regression against the old value, so it is written down rather than
   absorbed silently. */
.cs-hook-highlight {
  color: var(--cs-accent);
}

/* The company line — now the fold's only subhead. Greyed and a step down from
   the title, so the fold reads as one statement plus its context rather than
   two competing ones. */
.cs-hook-sub {
  font-family: var(--cs-font-ui);
  font-size: var(--hook-sub-size);
  font-weight: var(--hook-sub-weight);
  line-height: var(--hook-sub-lh);
  color: var(--hook-sub-color);
  max-width: var(--hook-sub-measure);
  margin: 0;
}

/* ═══ §2 — Orientation ═══════════════════════════════════════════════════
   One horizontal card holding role / timeline / team as three columns, the
   layout the Collectibles study uses for the same job. It replaced a stacked
   four-row spec sheet, and it carries no heading: the column labels already
   say what it is, so a title above it was repeating itself.

   White on the cream ground rather than a tint, so the card lifts off the
   page the way the reference's does. Corner is --radius; the reference's
   larger radius is its own visual language, not part of the layout.

   The company line that used to be the fourth row moved up into the hero —
   also following the reference, which puts its "what is Swiggy" explainer in
   the opening panel rather than in the meta card. That leaves three columns
   whose values are short enough to sit side by side.

   Every value here is a custom property for the same reason fold 1's are:
   case-study-dial.js reaches them by name. Defaults live here, so removing
   the dial kit changes nothing. */
.cs-page {
  --meta-pad:        24px;
  --meta-gap:        24px;   /* between columns */
  --meta-stack:      8px;    /* label -> value, inside one column */
  /* The dial kit reaches this by name, so it stays a --meta-* knob; its
     default is now the shared card corner rather than a second reference to
     --radius. */
  --meta-radius:     var(--cs-card-radius);

  --meta-key-size:   var(--cs-text-label);
  --meta-key-weight: 400;
  --meta-key-track:  0.1em;

  --meta-val-size:   var(--cs-text-body);
  /* Was 700, then 400 (matched to the hook subhead's regular weight, so the
     value read as calm information rather than a second headline). Now 500,
     matched instead to .cs-bubble-line — both are var(--cs-font-display), so this
     is Clash Display's own medium file in both places, not a synthesis. */
  --meta-val-weight: 500;
  --meta-val-lh:     1.35;
}

.cs-meta {
  display: grid;
  gap: var(--meta-gap);
  padding: var(--meta-pad);

  /* Geometrically equal to the gap above (both exactly --cs-sec-gap, via the
     .cs-hook + .cs-sec rule above cancelling the hero's own top-up) still
     read as tighter below on review — the card's flat white edge sits hard
     against cream, then colourful phone screens start immediately after it,
     which carries more visual weight than the plain type above the card and
     makes an identical gap look shorter. Requested explicitly at +24px
     rather than re-balanced to equal again.

     On .cs-meta, not the section wrapping it: a <section> is a flex item of
     .cs and establishes its own block formatting context, so a margin here
     grows this section's own box rather than collapsing through it into the
     flex `gap` — the gap above is untouched. */
  margin-bottom: 24px;

  /* Surface from the shared card tokens (see --cs-card-* and .cs-card). This
     card is the reference use; a later section wanting the same panel adds the
     .cs-card class rather than re-typing these. border-radius stays routed
     through --meta-radius so the dial kit's corner control still bites. */
  background: var(--cs-card-bg);
  border-radius: var(--meta-radius);

  /* No width or centring of its own any more — the section wrapping it is
     already exactly --cs-content-max wide (every section is, now), so the
     card simply fills it. It used to carry its own separate 950px cap; that
     is what let it sit narrower than everything else on the page, since
     every other section was still the wider, uncapped column. One page-wide
     rule now does what two independent ones used to.

     950px is still the number in play, unchanged from when it was tuned: the
     three column values are auto-sized to their own content, and forcing the
     column narrower makes the grid compress all three at once rather than
     wrapping only the longest — at 640px, tested, "My role" broke to three
     ragged lines and "Team" to five. At 950px only Team wraps, cleanly, to
     two even lines; Role and Duration hold their single line. Measured at
     six widths, not picked by eye.

     The border is gone; a soft shadow separates the card from the page
     instead — now --cs-card-shadow, shared with every other card. */
  box-shadow: var(--cs-card-shadow);
}

.cs-meta-item {
  display: flex;
  flex-direction: column;
  gap: var(--meta-stack);
  min-width: 0;
}

.cs-meta-key {
  font-family: var(--cs-font-ui);
  font-size: var(--meta-key-size);
  font-weight: var(--meta-key-weight);
  line-height: 1.4;
  letter-spacing: var(--meta-key-track);
  text-transform: uppercase;
  color: var(--cs-label);
}

/* Bold, matching the reference card: the label is the quiet half of each
   column and the value is the answer, so they differ by weight as well as by
   size and colour. */
.cs-meta-val {
  font-family: var(--cs-font-display);
  font-size: var(--meta-val-size);
  font-weight: var(--meta-val-weight);
  line-height: var(--meta-val-lh);
  color: var(--color-ink);
  margin: 0;
}

@media (min-width: 641px) {

  .cs-page { --meta-gap: 40px; --meta-pad: 40px; }

  /* Three equal columns. An earlier build used repeat(3, auto) + space-between
     so each value took only the width it needed — but with "My role" and
     "Team" both carrying long values and "Duration" carrying "1.5 months", the
     card ended up visibly heavier on the left two thirds. Equal 1fr columns
     was asked for directly: every column is the same width, the two longer
     values wrap within their own third, and the card reads balanced. */
  .cs-meta { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 1025px) {

  .cs-page { --meta-gap: 56px; --meta-pad: 48px; }
}

/* The legacy spec sheet that used to live here is gone, and it had to go:
   §17's strategy spectrum took the name .cs-spec, so this dead rule stopped
   being harmlessly unused and started styling a live section from six
   thousand lines away. It was contributing a border-top — the stray divider
   above the tab row — and a max-width: 56em that pulled §17's right edge in
   off the column, which is why that section's right margin did not match
   §7's. Neither was overridden, because neither property appears in §17's
   own .cs-spec rule; there was nothing to notice.

   Its comment said "delete if nothing picks it up". Something picked up the
   name instead, which is the worse case, so: deleted. §2 is .cs-meta.     */


/* ═══ §3 / §14 — Stats ═══════════════════════════════════════════════════
   Numbers and labels reuse .case-metric-number / .case-metric-label from
   style.css verbatim. Only the grid is new here: .case-metrics is a fixed
   760px flex row, which cannot stack.                                    */

.cs-stats {
  display: grid;
  gap: 32px;
  border-top: 1px solid var(--color-ink);
  padding-top: 24px;
}

/* The four screens, sized identically to every §10-20 "after" screen on the
   page. A descendant selector, not .cs-strip > *, because the splash image
   sits inside a <picture> for its reduced-motion still — the box that needs
   this width is the <img> either way. */
.cs-strip .cs-shot-img {
  width: calc(var(--pro-card-w) * var(--cs-screen-step));
  max-width: none;
}

/* .cs-quotes / .cs-quote lived here — a second, conflicting copy of the pivot
   section's quote treatment, shadowed by another set further down the file and
   used by nothing once §4 was rebuilt around .cs-voice cards. Deleted rather
   than left to rot; git has it. */


/* ═══ §7 / §16 — Step markers ═══════════════════════════════════════════
   One pattern, two uses: the mandate sequence (§7) and the since-shipping
   timeline (§16). Same shape, so it is the same component rather than two.

   Square markers, no dots: --radius-round is reserved for the three floating
   circular icon buttons (see CLAUDE.md), and a literal border-radius is not
   allowed, so the marker is simply a square. It suits the type anyway.

   Flex-wrap rather than a media query — the row breaks wherever it runs out
   of width, at any viewport, and the connectors sit between siblings so a
   wrapped line still reads correctly.                                    */

.cs-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  list-style: none;
  padding: 0;
  margin: 0;
}

/* ═══ §9 — Deep dive ════════════════════════════════════════════════════
   Four sub-artifacts, four shapes. Diagrams run full width; benchmarks are
   a dense label grid (no screenshots); personas are two compact cards.

   §9's own .cs-sub wrapper (display:flex, column, gap 16px) used to live
   here and is gone. Nothing in the markup used it any more, but the SECTION
   SUBHEAD further down this file is also called .cs-sub, so this dead rule
   was silently making all four of those paragraphs flex containers.

   That is invisible while such a paragraph holds nothing but text — one
   anonymous flex item, laid out exactly like a block. It stops being
   invisible the moment one contains an element: §17's subhead bolds a clause,
   and the <strong> blockified into a flex item, breaking the sentence onto
   three lines with the bold phrase alone in the middle. Same failure as the
   legacy .cs-spec rule two thousand lines up — a dead selector that stopped
   being dead when a new component took its name.                          */

/* ═══ §10 — The existing journey, untangled ═════════════════════════════
   A before/after diagram on the dark band: eight scattered steps that
   regroup into four bands. Built by case-study-untangle.js — read the note
   at the top of that file before changing anything here, because the layout
   is computed from the --ut-* tokens below and nothing else.

   The argument is conservation: the same eight nodes are on screen in both
   states. That is why the two states are one node set, not two drawings.

   Everything here is layout and colour. No element is hidden by this
   stylesheet — every hidden state in this section is written by the script,
   onto elements the script itself created.                                */

/* The band's own --cs-sec-gap padding is 160px a side at 1025+, which a
   100svh pin cannot afford. Trimmed on this one section rather than on
   .cs-band, which two other sections use at full depth. */
.cs-ut-band { padding-block: 32px; }

/* ═══ §6 — .cs-jn, the journey shown twice ════════════════════════════════
   Eleven steps as pills, switched between a descending staircase (before) and
   five merged stage blocks on one level (after), by a segmented control.

   The same eleven <li> nodes serve both states. They are re-positioned, never
   re-created, which is what makes the conservation claim structural instead of
   rhetorical: there is no code path that could show a different set in one
   view than the other.

   Two layout modes:
   - No JS / not yet measured: the <ol> is a plain readable list. Everything
     below that positions anything is gated behind .is-live, which the script
     adds only once it has measured the canvas.
   - .is-live: pills go absolute and animate left/top/width/border-radius.

   Colour comes from the band's own tokens (--cs-fg, --cs-label, --cs-rule,
   --cs-tint), all of which .cs-band--dark redefines, so the component themes
   itself on the dark ground without a single dark-specific rule. */
.cs-jn-sec { --cs-blk-gap: 28px; }

/* Decorative ground. The pattern itself comes from the shared rule in
   style.css — this sets only the box, the fade and the weight, the same split
   .cs-pro-dots uses so the two fields cannot drift apart.

   Inset past the canvas on every side so the radial mask has faded the dots to
   nothing well before the box ends: the field reads as light behind the
   staircase rather than as a panel with an edge. */
.cs-jn-dots {
  position: absolute;
  /* Vertical overhang only. An absolutely positioned child still counts toward
     the scroller's scrollWidth, so a horizontal one made the after view 982px
     wide inside a 950px scroller and left it scrollable when it should be
     inert. Nothing is lost: the radial mask has already faded the dots out
     well inside these edges. */
  inset: -24px 0;
  opacity: var(--jn-dot-opacity);

  /* Edge-anchored fade, not the prologue's proportional radial one. This canvas
     changes width dramatically between the two views — 2112px when the
     staircase is scrolling, 950 when it collapses — and a radial ellipse at
     50% put its bright centre a thousand pixels off to the right, leaving the
     START of the staircase, which is where the reader begins, almost bare.
     A fade measured in px from each edge holds at any width.

     Two gradients intersected: horizontal and vertical. mask-composite is the
     standard property; the -webkit- pair is what Safari still reads, and its
     'source-in' is the same operation. */
  --jn-fade-x: 96px;
  --jn-fade-y: 56px;
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 var(--jn-fade-x), #000 calc(100% - var(--jn-fade-x)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--jn-fade-y), #000 calc(100% - var(--jn-fade-y)), transparent);
          mask-image:
    linear-gradient(to right, transparent, #000 var(--jn-fade-x), #000 calc(100% - var(--jn-fade-x)), transparent),
    linear-gradient(to bottom, transparent, #000 var(--jn-fade-y), #000 calc(100% - var(--jn-fade-y)), transparent);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
  z-index: 0;
}

/* ── The dark ground ──────────────────────────────────────────────────────
   Same near-black as §5 and §21, and set the same way .cs-impact sets it:
   scoped here, overriding .cs-band--dark's own --cs-band-bg rather than
   editing that class, which is a generic flip meant for reuse and should not
   inherit one section's specific colour. Everything else -- --cs-fg,
   --cs-label, --cs-label-strong, --cs-rule -- stays .cs-band--dark's own.

   min-height, and it is load-bearing rather than cosmetic. This section
   carries data-ground, so the page crossfades to its colour while it holds
   the middle of the viewport -- and that device only stays off the cream
   sections either side if the section already fills the screen by the time
   the fade starts. .cs-ground states the same requirement as a rule; this
   section is taller than 100svh at every width the page supports, so the
   declaration guarantees an invariant rather than changing any layout. */
#screen-iterations {
  --cs-band-bg: var(--color-charcoal);
  min-height: 100svh;
}

/* Same recipe as .cs-back so the two exits read as one control, not two. */
.cs-outro-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border: 1px solid var(--cs-rule);
  border-radius: var(--radius);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink);
  text-decoration: none;
  transition: border-color 0.25s ease, opacity 0.25s ease;
}

/* ═══ Impact — the promise and the payoff ══════════════════════════════════
   §3 and §14 are the same three numbers, and they are deliberately built as
   one component used twice with the hierarchy inverted.

   §3  numbers loud (--text-section), sentence quiet.   The promise.
   §14 numbers quiet (--text-body),  sentence loud.     The payoff.

   Sharing ground, table structure and left edge is what makes a reader
   recognise the callback 13,000px later before they have read a word. The
   inversion is what stops the second appearance from being a repeat: by then
   the figures are established and the sentence about them is the new thing.

   No borders anywhere in either. The dark ground is the only container — a
   metric card is exactly what this page has too many of already.           */

.cs-impact {
  /* Overrides .cs-band--dark's own --cs-band-bg (var(--color-ink),
     #1e1e20) with this section's own near-black, #2b2d33, on instruction.
     Scoped here rather than edited on .cs-band--dark itself: that class is a
     generic "dark ground" flip meant for reuse, and baking one section's
     specific colour into it would make a future dark band inherit a colour
     it never asked for. --cs-fg/--cs-label/etc. stay .cs-band--dark's own,
     tuned for cream against a near-black ground -- and #2b2d33 is close
     enough to #1e1e20 that nothing there needed re-measuring: cream at 72%
     flattened over it is 7.24:1, still clear of the 4.5:1 floor the original
     8.4:1 was tuned past. */
  --cs-band-bg: var(--color-charcoal);
  /* Taller than a normal band: this is a full stop in the argument, and the
     air around the numbers is what makes them read as a result rather than
     as a row of statistics. */
  padding-block: calc(var(--cs-sec-gap) + 40px);
  /* Positioning context for the dot field, and its own stacking context so
     the field's z-index: -1 stays local -- it has to sit above this band's
     own background but below its copy, and nothing outside should be able
     to land between the two. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* The same field the landing page and §5 carry, inverted for a dark ground.
   Inset by --cs-margin rather than run to the bleed, which is what every
   other field on the site does -- it is a margin-to-margin panel, not an
   edge-to-edge texture. Cream at 0.10 rather than the shared 0.32: that
   figure is tuned for dark dots on a light ground, and cream on near-black
   at the same value reads as static rather than texture. */
.cs-impact-dots {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--cs-margin);
  right: var(--cs-margin);
  z-index: -1;
  --dot-ink: var(--color-cream);
  --dot-opacity: 0.10;
}

/* A ledger, not a dashboard. Three entries, each a full-width row: the
   figure on the left at display scale, its reading on the right, a hairline
   above every row and one closing the set. That structure is what lets the
   figures get this big -- across in three columns they were bounded by a
   third of the column (~84px), stacked down the left they left half the
   width empty. A row spends the whole width on one figure and its reading,
   so the number can run to ~150px and the right side is occupied by
   something that belongs there.

   The three stay equal to each other: same size, same weight, same shape.
   Equal hierarchy was the instruction and it still holds -- what changed is
   how much room all three get, not their relationship. */
.cs-results {
  display: flex;
  flex-direction: column;
}

.cs-result {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  padding-block: 40px;
}

@media (min-width: 768px) {

  .cs-result {
    /* 3:2 rather than a fixed label column: at 768 a fixed 320px label would
       leave the figure less room than "$2.4M" needs. Fractions hold the
       proportion at every width instead. */
    grid-template-columns: 3fr 2fr;
    gap: 40px;
    /* end first, as the fallback: anything that does not understand "last
       baseline" keeps bottom-aligned boxes rather than dropping to stretch,
       which would pull the reading to full row height. */
    align-items: end;
    /* LAST baseline, not first: the readings run to two lines, and it is the
       line they end on that should sit on the figure's baseline. Bottom-
       aligning the boxes (align-items: end) left the reading's baseline
       ~18px high of the figure's, because the figure's box carries ~26px of
       descent below its baseline at this size and the reading's carries ~8px. */
    align-items: last baseline;
    padding-block: 56px;
  }
}

/* Between the rows only -- nothing above the first or below the last, so the
   three are separated without being boxed in.

   Pseudo-elements rather than borders because they are drawn on arrival and
   a border cannot be scaled. --cs-rule-scale is what the motion layer
   animates; with no motion layer it is absent and the fallback paints them
   full width, which is the static page. */
.cs-result + .cs-result::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--cs-rule);
  transform: scaleX(var(--cs-rule-scale, 1));
  transform-origin: left center;
}

.cs-result-num {
  font-family: var(--cs-font-display);
  font-weight: 700;
  /* Scoped deliberately rather than reaching for --text-section, which tops
     out at 108px and is shared with the work-section titles: this figure
     wants to be the largest type in the case study and nothing else should
     move with it. The -4px is taken off all three stops rather than just the
     preferred one, so the reduction holds at the floor and the ceiling too
     and the curve keeps its shape: 60px / 9vw-4px / 148px, landing ~126px at
     1440. "$2.4M", the longest figure, sets ~477px there against a 546px
     column. */
  font-size: clamp(3.75rem, calc(9vw - 4px), 9.25rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--cs-fg);
}

/* Up from --text-ui. Against a 130px figure the old 16px read as a caption
   that had wandered in; --text-lead (15-30px) is a reading, which is what
   it is. */
.cs-result-label {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-lead);
  line-height: 1.4;
  color: var(--cs-label);
  max-width: 22em;
}

/* Digit reels used by the results count-up. Each cell clips its vertical
   strip so only the current numeral is visible while it rolls into place. */
.cs-odo {
  display: inline-flex;
  align-items: baseline;
  white-space: pre;
}

.cs-odo-cell {
  display: inline-block;
  position: relative;
  flex: 0 0 auto;
  height: 1em;
  overflow: hidden;
  vertical-align: bottom;
}

.cs-odo-strip {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.cs-odo-strip > span {
  display: block;
  flex: 0 0 1em;
  height: 1em;
  line-height: 1;
}

.cs-odo-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  clip-path: inset(50%);
}

/* Only under the motion layer, and only until the reel is built and its
   first frame set -- see the safety note at the top of case-study-motion.js.
   Without that class this never applies and the figures are simply there. */
body.cs-motion .cs-result-num {
  opacity: 0;
}

/* The promise line under §3's numbers. */
.cs-impact-promise {
  font-family: var(--cs-font-display);
  font-weight: 400;
  font-size: var(--cs-text-body);
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
  max-width: 28em;
  margin: 0;
}

/* ── §14, inverted ────────────────────────────────────────────────────────
   The receipt: same table, dropped to body scale and to the label colour, so
   it reads as a reference the reader already holds rather than as news. */
.cs-results--receipt .cs-result-num {
  font-size: var(--cs-text-body);
  color: var(--cs-label);
}

.cs-results--receipt .cs-result-label {
  font-size: var(--cs-text-meta);
}

@media (min-width: 641px) {

  .cs-results--receipt .cs-result-num { min-width: 12ch; }
}

.cs-impact--full { gap: 40px; }

/* ── The detail cards ───────────────────────────────────────────────────── */
.cs-duo-cards {
  display: flex;
  flex-direction: column;
  gap: var(--duo-gap);
  min-width: 0;
}

.cs-duo-cards,
.cs-duo-figure { animation: cs-duo-swap 0.35s ease both; }

/* Surface comes from .cs-card; this owns only padding and internal layout.

   --duo-chip / --duo-chip-gap live HERE, not on .cs-duo-head: the head and the
   bullet list are siblings, and a custom property set on one sibling is
   invisible to the other -- it only inherits to descendants. Scoped to their
   shared parent, both the chip's own box and the bullets below can read the
   same two numbers, which is what keeps the title's left edge and the
   bullets' alignment locked together. */
.cs-duo-card {
  --duo-chip: 36px;
  --duo-chip-gap: 12px;
  padding: 24px;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* --duo-chip / --duo-chip-gap tie the head row to the bullet alignment below:
   both the title's left edge and the bullets' dot-centre and text-start are
   DERIVED from these two numbers, so the three cannot drift out of step with
   each other again. */
.cs-duo-head {
  display: flex;
  align-items: center;
  gap: var(--duo-chip-gap, 12px);
  /* Down from 16px to match .cs-duo-list's own 12px gap -- one rhythm from
     the icon row through every bullet, not a bespoke number for this one
     gap. */
  margin-bottom: 12px;
}

/* The background box is gone -- on instruction, the icons alone carry the
   row now. Box model is UNCHANGED (still 36x36, centring a 20x20 svg) so nothing
   downstream shifts: the title's left edge and the bullets' alignment below
   are both measured off this box's width, not off where a background used to
   be painted. */
.cs-duo-chip {
  width: var(--duo-chip, 36px);
  height: var(--duo-chip, 36px);
  flex: none;
  display: grid;
  place-items: center;
}

.cs-duo-chip svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: var(--cs-fg);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cs-duo-card-title {
  margin: 0;
  font-family: var(--cs-font-display);
  /* Matched to .cs-duo-quote's weight on instruction -- was 400. Both are
     var(--cs-font-display), so this is Clash Display's real medium file in both
     places, not a synthesis. */
  font-weight: 500;
  font-size: var(--cs-text-lead);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
}

/* Text starts at chip-width + chip-gap -- exactly where .cs-duo-card-title's
   text starts -- so a bullet reads flush with the heading above it instead of
   at its own, narrower indent. This was the actual bug: the dot's old
   left:0/padding-left:16 had no relationship to the head row's 36px chip and
   12px gap, so the two sat 32px apart. */
.cs-duo-list li {
  position: relative;
  padding-left: calc(var(--duo-chip, 36px) + var(--duo-chip-gap, 12px));
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.45;
  color: var(--cs-label);
}

@media (max-width: 640px) {

  .cs-duo-card { padding: 20px; }
}

@media (prefers-reduced-motion: reduce) {

  .cs-duo-cards,
  .cs-duo-figure { animation: none; }
}

/* ═══ LupaApp — mockup 3D e seletor de telas ═════════════════════════════ */
.cs-screen-flow {
  align-items: center;
  text-align: center;
}

.cs-screen-flow > .cs-eyebrow,
.cs-screen-flow > .cs-h2,
.cs-screen-flow > .cs-sub { width: 100%; }

.cs-phone-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(300px, 1fr);
  align-items: center;
  gap: clamp(24px, 4vw, 56px);
  width: 100%;
}

.cs-phone-preview {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: center;
}

.cs-phone-navigation { min-width: 0; width: 100%; }

.cs-phone-stage {
  position: relative;
  display: grid;
  width: 100%;
  height: clamp(460px, 48vw, 580px);
  min-height: 0;
  place-items: center;
  isolation: isolate;
}

.cs-phone-model {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  background: transparent;
  opacity: 0;
  transition: opacity 350ms ease;
  --poster-color: transparent;
}

.cs-screen-flow[data-model-ready="true"] .cs-phone-model { opacity: 1; }

.cs-phone-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  opacity: 1;
  transition: opacity 350ms ease;
}

.cs-screen-flow[data-model-ready="true"] .cs-phone-fallback { opacity: 0; visibility: hidden; }

.cs-phone-fallback-frame {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: inherit;
}

.cs-phone-device {
  --phone-rotation: -11deg;
  position: relative;
  width: clamp(224px, 25vw, 270px);
  padding: 17px 8px 10px;
  background: linear-gradient(108deg, #55565a 0%, #131416 13%, #070809 54%, #292a2d 88%, #5b5c60 100%);
  border: 1px solid #707176;
  border-radius: 37px;
  box-shadow: 0 34px 55px -28px rgb(27 26 23 / 48%), 0 7px 13px rgb(27 26 23 / 20%);
  transform: rotateY(var(--phone-rotation)) rotateX(2deg);
  transform-style: preserve-3d;
  transition: transform 220ms ease, box-shadow 220ms ease;
  will-change: transform;
}

.cs-phone-device::before {
  position: absolute;
  top: 22%;
  right: -5px;
  width: 5px;
  height: 68px;
  content: '';
  background: linear-gradient(90deg, #191a1c, #66676b);
  border-radius: 0 4px 4px 0;
  transform: translateZ(-2px);
}

.cs-phone-speaker,
.cs-phone-camera { position: absolute; z-index: 1; top: 7px; display: block; background: #030405; }
.cs-phone-speaker { left: calc(50% - 17px); width: 25px; height: 3px; border-radius: 4px; }
.cs-phone-camera { left: calc(50% + 14px); width: 5px; height: 5px; border-radius: 50%; box-shadow: inset 0 0 0 1px #37383b; }

.cs-phone-screen {
  width: 100%;
  aspect-ratio: 9 / 20;
  overflow: hidden;
  background: #f6f6f6;
  border-radius: 27px;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 28%);
}

.cs-phone-frame {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: inherit;
}

.cs-phone-side-key {
  position: absolute;
  top: 15%;
  left: -4px;
  width: 4px;
  height: 43px;
  background: linear-gradient(90deg, #66676b, #191a1c);
  border-radius: 4px 0 0 4px;
  transform: translateZ(-2px);
}

.cs-phone-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 8px;
}

.cs-phone-rotation-toggle {
  display: inline-flex;
  min-height: 42px;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  color: var(--color-ink);
  background: var(--color-white);
  border: 1px solid color-mix(in srgb, var(--color-ink) 14%, transparent);
  border-radius: 999px;
  box-shadow: 0 5px 18px rgb(27 26 23 / 8%);
  cursor: pointer;
  font: 600 var(--cs-text-label)/1.2 var(--cs-font-ui);
  transition: transform 180ms ease, border-color 180ms ease;
}

.cs-phone-rotation-toggle:hover { transform: translateY(-2px); border-color: var(--cs-accent); }
.cs-phone-rotation-toggle:focus-visible { outline: 3px solid var(--cs-accent); outline-offset: 3px; }

.cs-pouch-viewer { align-items: center; text-align: center; }
.cs-pouch-viewer > .cs-eyebrow,
.cs-pouch-viewer > .cs-h2,
.cs-pouch-viewer > .cs-sub { width: 100%; }

.cs-pouch-stage {
  position: relative;
  display: grid;
  width: min(100%, 760px);
  height: clamp(560px, 54vw, 660px);
  place-items: center;
  isolation: isolate;
}

.cs-pouch-model,
.cs-pouch-fallback,
.cs-lira-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.cs-pouch-model {
  background: transparent;
  opacity: 0;
  transition: opacity 350ms ease;
  --poster-color: transparent;
}

.cs-lira-canvas {
  background: transparent;
  opacity: 0;
  touch-action: pan-y;
  transition: opacity 350ms ease;
}

.cs-pouch-fallback {
  object-fit: contain;
  opacity: 1;
  transition: opacity 350ms ease;
}

.cs-pouch-loading {
  position: absolute;
  z-index: 1;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 14px;
  color: var(--color-ink);
  font: 600 var(--cs-text-label)/1.3 var(--cs-font-ui);
  pointer-events: none;
  transition: opacity 220ms ease, visibility 220ms ease;
}

.cs-pouch-loading[hidden] { display: none; }

.cs-pouch-loading-indicator {
  width: 44px;
  height: 44px;
  border: 3px solid color-mix(in srgb, var(--color-ink) 16%, transparent);
  border-top-color: var(--cs-accent);
  border-radius: 50%;
  animation: cs-pouch-loading-spin 800ms linear infinite;
}

@keyframes cs-pouch-loading-spin {
  to { transform: rotate(360deg); }
}

.cs-pouch-viewer[data-model-ready="true"] .cs-pouch-model { opacity: 1; }
.cs-lira-viewer[data-model-ready="true"] .cs-lira-canvas { opacity: 1; }
.cs-pouch-viewer[data-model-ready="true"] .cs-pouch-fallback { opacity: 0; visibility: hidden; }

.cs-pouch-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
}

.cs-pouch-status {
  margin: 0;
  color: var(--cs-label);
  font: 500 var(--cs-text-label)/1.5 var(--cs-font-ui);
}

@media (max-width: 640px) {
  .cs-pouch-stage { height: clamp(460px, 118vw, 520px); }
}

@media (prefers-reduced-motion: reduce) {
  .cs-pouch-loading-indicator { animation: none; }
}

.cs-phone-controls p {
  color: var(--cs-label);
  font: 500 var(--cs-text-label)/1.4 var(--cs-font-ui);
  margin: 0;
}

.cs-phone-categories {
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 8px;
  width: 100%;
  margin: 0 0 16px;
}

.cs-phone-category {
  min-height: 42px;
  padding: 9px 16px;
  color: var(--cs-label);
  background: color-mix(in srgb, var(--color-white) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-ink) 12%, transparent);
  border-radius: 999px;
  cursor: pointer;
  font: 600 var(--cs-text-label)/1.2 var(--cs-font-ui);
  transition: color 180ms ease, background-color 180ms ease, border-color 180ms ease, transform 180ms ease;
}

.cs-phone-category[aria-selected="true"] {
  color: var(--color-ink);
  background: var(--color-white);
  border-color: var(--cs-accent);
  box-shadow: 0 0 0 1px var(--cs-accent);
}

.cs-phone-category:hover { transform: translateY(-2px); }
.cs-phone-category:focus-visible { outline: 3px solid var(--cs-accent); outline-offset: 3px; }

.cs-phone-group { width: 100%; margin: 0; }

.cs-phone-chooser {
  display: grid;
  width: 100%;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  margin: 0;
}

.cs-phone-choice {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  color: var(--cs-label);
  text-align: left;
  text-decoration: none;
  background: color-mix(in srgb, var(--color-white) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-ink) 10%, transparent);
  border-radius: 12px;
  cursor: pointer;
  transition: background-color 180ms ease, border-color 180ms ease, transform 180ms ease;
}

.cs-phone-choice img {
  flex: 0 0 auto;
  width: 32px;
  height: 68px;
  object-fit: contain;
  border-radius: 5px;
  box-shadow: 0 2px 8px rgb(27 26 23 / 16%);
}

.cs-phone-choice span { font: 600 var(--cs-text-label)/1.3 var(--cs-font-ui); }
.cs-phone-choice[aria-selected="true"] { color: var(--color-ink); background: var(--color-white); border-color: var(--cs-accent); box-shadow: 0 0 0 1px var(--cs-accent); }
.cs-phone-choice:hover { transform: translateY(-2px); }
.cs-phone-choice:focus-visible { outline: 3px solid var(--cs-accent); outline-offset: 3px; }
.cs-phone-current { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }

@media (max-width: 767px) {
  .cs-phone-layout { display: flex; flex-direction: column; align-items: stretch; gap: 18px; }
  .cs-phone-navigation { order: -1; }
  .cs-phone-categories { justify-content: center; margin-bottom: 12px; }
  .cs-phone-stage { height: clamp(455px, 145vw, 540px); }
  .cs-phone-device { width: clamp(215px, 64vw, 248px); }
  .cs-phone-chooser { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .cs-phone-choice { padding: 8px; gap: 9px; }
  .cs-phone-choice img { width: 31px; height: 66px; }
}

@media (max-width: 380px) {
  .cs-phone-stage { height: clamp(440px, 150vw, 500px); }
  .cs-phone-controls { flex-direction: column; gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .cs-phone-model,
  .cs-phone-fallback,
  .cs-phone-rotation-toggle,
.cs-phone-category,
  .cs-phone-choice { transition: none; }
  .cs-phone-rotation-toggle:hover,
.cs-phone-category:hover,
  .cs-phone-choice:hover { transform: none; }
}

/* ═══ §13 — the objection ══════════════════════════════════════════════════
   The iteration captions that used to live here went with the markup they
   styled: §13's six screens carry their reasoning in a .cs-bubble each now,
   not in a caption under a column. See .cs-spec. */

/* ── The objection ────────────────────────────────────────────────────────
   A defence addressed to a reader about to object, so it gets its own ground
   rather than a tint. Deliberately short — it is an interruption, not a
   section, and it should feel like a page from a different document. */
.cs-objection {
  padding-block: calc(var(--cs-sec-gap) - 16px);
  gap: 24px;
}

/* ═══ What did I do? ═══════════════════════════════════════════════════════
   A real heading, deliberately bigger than every other section title on the
   page — .cs-h2 sizes this fold's headings up to 56px; this one runs up to
   108px, reusing --text-section (the scale the landing page's own big work
   titles use) rather than inventing a new number. On direct instruction: a
   four-second skim item earns a heading this size only because it was asked
   for, not as a judgement call made here.

   Previously larger than the other headers on instruction; now inherits .cs-h2 entirely. */
.cs-did-heading { /* inherits .cs-h2 size, like every other case-study header */ }

/* Hidden for now, at Shriya's request — not deleted, since she wants it back.
   ?did in the URL brings it back, matching the ?notes / ?dial pattern
   case-study.js already uses for exactly this kind of "on, but not by
   default" content. Hidden in CSS rather than only in JS, so it stays hidden
   even if the script never runs — the safe default for something someone
   asked to have turned off is off, not "off unless something fails". */
.cs-did-sec { display: none; }

body.cs-show-did .cs-did-sec { display: flex; }

/* Pills, not the bordered tags an earlier build of this section used (see the
   markup comment for that history). Genuinely pill-shaped: border-radius is
   a literal 999px here, not var(--radius-round) — that token is reserved by
   CLAUDE.md for three specific floating circular icon buttons and its own
   rule says not to extend it, so a fourth, unrelated use gets its own literal
   value instead of quietly breaking that rule. Documented as the fifth
   exception to the two-radius system, in CLAUDE.md, at Shriya's explicit
   request.

   .cs-pills shares this declaration rather than repeating it: §16's three
   scope tags (Scale for 3-6 products / Dynamic state-handling / First fold
   optimisation) want the exact same pill — border, padding, radius, type —
   just under a name that does not carry "what did I do?"'s meaning. One
   recipe, two call sites, so a future change to the shape only has one place
   to happen. */
.cs-did,
.cs-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.cs-did li,
.cs-pills li {
  padding: 12px 24px;
  border: 1px solid var(--cs-rule);
  border-radius: 999px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  line-height: 1.2;
  color: var(--cs-fg);
}

/* ─── Pinned bands keep their full bleed ───────────────────────────────────
   ScrollTrigger's pin does two things that break .cs-band's full-bleed:

     1. it moves the element into a .pin-spacer and transfers the negative
        inline margins onto that spacer, and
     2. it writes an inline px width taken from the element's content box
        *before* pinning, then sets position: fixed.

   Measured at 1440: the band came out 1121px wide with margin 0, leaving a
   319px cream gutter down the right of a section that is supposed to reach
   both edges. It read as a rendering fault rather than as a band.

   The override wins because a stylesheet !important outranks GSAP's inline
   width. 100% rather than 100vw deliberately: for a fixed element it resolves
   against the viewport, and for the relative state at either end of the pin it
   resolves against the spacer — which already carries the negative margins and
   is therefore already full-bleed. One rule, correct in both states, and no
   scrollbar-width error of the kind 100vw introduces.

   Scoped to .pin-spacer so an unpinned band is untouched.                  */
.pin-spacer > .cs-band {
  width: 100% !important;
  /* GSAP writes max-width alongside width, so overriding width alone is not
     enough — measured: width resolved to 1440 and was then clamped straight
     back to 1121 by the inline max-width. Both have to go. */
  max-width: none !important;
  margin-inline: 0 !important;
}

/* ─── Focus ───────────────────────────────────────────────────────────────
   The page's four focusable things were falling back to Chrome's default
   focus ring — a 1px auto outline in rgb(0, 95, 204). It is visible, so this
   is not a compliance fix, but it is browser blue on a page with a four
   colour palette, and on the bar's dark state it is the lowest-contrast
   thing on screen.

   style.css defines a house ring for the landing nav (2px solid, 4px offset)
   and this page inherits none of it, because those rules are scoped to
   .nav-link and .nav-logo. Same recipe, restated against this page's own
   foreground token so it flips with the ground the way everything else here
   does — dark on cream, cream on a dark band.

   :focus-visible, not :focus, so a mouse click on the notes toggle does not
   leave a ring behind it. */
.cs-page :focus-visible {
  outline: 2px solid var(--cs-fg);
  outline-offset: 4px;
  border-radius: var(--radius);
}

/* The bar sits outside any band, so it takes the on-dark foreground from the
   same token pair the rest of its dark state uses. */
.cs-page[data-cs-bar="dark"] .cs-bar :focus-visible,
.cs-page[data-cs-bar="green"] .cs-bar :focus-visible {
  outline-color: var(--color-cream);
}

/* ═══ §4 — The ask, the signals, the evidence, the takeaway ════════════════

   ── The scale this section is held to ──────────────────────────────────────
   Every text element here resolves to one of FOUR sizes and THREE weights, and
   nothing in this section may add a fifth or a fourth:

     --text-label  700  eyebrows ("The ask", "What people actually said",
                        "The takeaway") and the method tag
                   400  the "20 conversations" figure
     --text-ui     400  the subhead / the attribution
     --text-lead   400  the verbatim  /  500  the takeaway line
     --cs-h2       500  the section heading

   Four, not five, because the fifth was fake: --text-meta's clamp is within
   0.02px of --text-ui's below ~1620px. Two tokens, one rendered size, no step.

   Anything doing an eyebrow's job takes the .cs-eyebrow CLASS, not a lookalike
   recipe — that is what let the evidence label drift to 0.1em tracking against
   the eyebrow's 0.08em. The method tag repeats the eyebrow's numbers; only ink
   differs.

   ── Spacing ────────────────────────────────────────────────────────────────
   Only 4 / 8 / 12 / 16 / 24 / 48 appear below.

   ── What was cut ───────────────────────────────────────────────────────────
   The full-width divider and its two meta figures, the coral mark and accent
   on the takeaway, and — this pass — the two-signal stats row that sat between
   the subhead and the evidence. That takes the section from four accent uses
   to one (the green method tag): heading -> subhead -> evidence -> conclusion,
   nothing competing across it.                                             */

.cs-page {
  /* The method tag. Green, as the reference has it, but derived rather than
     picked: the ink is --color-green pulled 45% toward --color-dark so it
     clears the contrast floor (5.3:1 on the chip), which the raw accent never
     would — CLAUDE.md records it at 2.7:1 and bars it as type. */
  --ask-chip-fg: color-mix(in srgb, var(--color-green) 55%, var(--color-ink));
  --ask-chip-bg: color-mix(in srgb, var(--color-green) 12%, var(--color-white));

  /* One measure for the heading, the subhead AND the takeaway line — all
     three end at the same right edge, on instruction. 54rem (864px) is the
     prologue heading's own measure at desktop (.cs-h2's 18em resolves to
     ~863px there), so §4's opening reads at the same width as §3's rather than
     the tighter column it had. A rem value, not each element's own em: an em
     measure resolves against font-size, so the same "18em" on the h2 and on
     the Geist subhead would be two different widths — which is the bug this
     token exists to close. */
  --ask-measure: min(100%, 54rem);
}

/* The takeaway is no longer in this list: it is a .cs-bubble now, and a bubble
   hugs its own text rather than ruling off at a shared measure. */
.cs-ask .cs-h2,
.cs-ask .cs-sub { max-width: var(--ask-measure); }

/* The takeaway bubble ends exactly where the third card ends, on instruction —
   which, now that the evidence block no longer bleeds, is simply the section's
   right edge. The cards grid fills the column, so its last card and the bubble
   land on the same number with nothing to keep in step.

   The third card is rotated 1.5deg, so its *visual* corner reaches a couple of
   px further; the alignment is to the card's layout edge, which is the one the
   eye reads as the column's edge.

   Placement, not a component property, so it lives here rather than on
   .cs-bubble. */
.cs-ask .cs-bubble { margin-left: auto; }

/* A section subhead set in body copy rather than the display face — the same
   Geist / --text-ui / label-grey as the running prose elsewhere on the page.
   Used where a section opens on a heading and a supporting paragraph, not a
   display-weight lede (§4, §5 and §16 so far). Sits on .cs-lede in the
   markup, so it inherits that element's margin and is overridden only where
   it needs to differ; being later in the file, it wins the type properties.

   §4 additionally caps this at --ask-measure via the selector above — a
   one-off, so its opening matches §3's. Everywhere else it takes the 40em
   below, a normal paragraph measure at this size. */
.cs-sub {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-body);
  font-weight: 400;
  line-height: 1.5;
  /* .cs-lede sets -0.01em for the display face it normally wears; this is
     Geist, which tracks normal like the rest of the body copy. */
  letter-spacing: normal;
  color: var(--cs-label);
  max-width: 40em;
}

/* The bolded clause inside one. Weight and ink, no colour of its own — the
   emphasis is on a phrase inside running copy, not a label, so it takes the
   same treatment .cs-meta-key does: 700 at --cs-label-strong, which is
   defined for the light ground and redefined by every dark one. */
.cs-sub strong {
  font-weight: 700;
  color: var(--cs-label-strong);
}

/* A subhead that wraps on the same right edge as the .cs-h2 above it rather
   than on its own 40em measure. 40em of 14px body copy is ~560px against the
   heading's 18em of --cs-h2 (863px at 1440), so by default the two ragged
   edges sit 300px apart and read as a mistake in a section that opens on
   both.

   Written as 18 x the HEADING's font size, not as a figure in this element's
   own ems — that is what makes it the same measure rather than a number that
   happens to match at one width. --cs-h2 is a clamp(), so this tracks its
   whole fluid curve on its own. */
.cs-sub--wide { max-width: calc(18 * var(--cs-h2)); }

/* ── The evidence ────────────────────────────────────────────────────────
   No container ground any more — removed on instruction. The cards sit
   straight on the page, tilted, so the head row and the grid both align to
   the section's own column and there is nothing to keep them apart. */
.cs-voices {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* The "What people actually said / 20 conversations" head is gone on
   instruction — the three badged cards introduce themselves, so the section
   runs subhead straight into evidence with nothing announcing it. */

.cs-voices-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 16px;
}

/* One card per method, on the shared .cs-card surface (markup) — same white /
   --radius / soft-shadow as §2's info card and §3's screens. */
.cs-voice {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px;
  min-width: 0;
  position: relative;
}

/* The method tag.

   Its corner is HALF the card's, which is what makes the two read as the same
   corner treatment. They were both a literal 8px and still looked mismatched,
   and the measurement says why: 8px is 32% of this tag's 25px height but only
   4% of the card's 227px. Identical numbers, two different-looking corners.
   Scaling the radius with the element is what matches them to the eye — and it
   stays tied to the card's own token, so the pair cannot drift.

   Type is the eyebrow recipe exactly (--text-label / 700 / 0.08em); only the
   ink and the ground differ. It hugs its text: align-self covers the flex
   layout below 1024, justify-self the subgrid layout at and above it, where a
   grid item would otherwise stretch the full column. */
.cs-voice-method {
  align-self: flex-start;
  justify-self: start;
  padding: 4px 12px;
  border-radius: calc(var(--cs-card-radius) / 2);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ask-chip-fg);
  background: var(--ask-chip-bg);
}

/* The verbatim — the only human voice in the case study, set in the body face
   rather than the display one: it is someone being quoted mid-sentence, not a
   heading, and italic is how running prose marks that everywhere else. Real
   italic (Geist's own 400i, loaded in jupiter.html), not a synthesised slant —
   compared side by side against the browser's faux-oblique and kept the real
   face. --text-lead, the section's statement size, shared with the takeaway
   line. Straight ASCII quote marks, per CLAUDE.md. */
.cs-voice-quote {
  font-family: var(--cs-font-ui);
  font-style: italic;
  font-weight: 400;
  font-size: var(--cs-text-lead);
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
  margin: 0;
  /* Pushes the attribution to the card's bottom edge in the flex fallback. */
  flex: 1;
}

/* Who said it, on a hairline. The rule sits at the top of the card's third
   subgrid row, so it lands level across all three cards whatever the quote
   above it did.

   Small and quiet at --text-label, but sentence case, on instruction — and
   with tracking back to normal, because 0.08em is a device for uppercase
   labels and only makes running words look loose. */
.cs-voice-who {
  border-top: 1px solid var(--cs-rule);
  padding-top: 16px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: normal;
  color: var(--cs-label);
}

/* ── The takeaway ─────────────────────────────────────────────────────────
   The conclusion the three verbatims add up to.

   .cs-takeaway / -line / -hi lived here — an eyebrow over a plain-ink line
   ruled off at --ask-measure, with its closing clause in spacefire. It is a
   .cs-bubble now (see the component near .cs-card), which says the same thing
   structurally that the copy says: the conclusion came out of the cards, so it
   hangs off a cursor pointing at them. The type recipe moved to
   .cs-bubble-line unchanged; only the ink and the measure differ. */

/* .cs-widen — the three-step "conversion optimisation -> wider experience ->
   company-level priority" progression set in growing type — was removed on
   instruction. §5's subhead now carries that arc in prose. */

/* ═══ §3 — The prologue's annotated then/now ══════════════════════════════
   Two real product shots on a dot field, with four annotations hung at the
   corners and a leader line running from each toward the stage.

   ── No frame around the visual ─────────────────────────────────────────────
   Deliberate, per direct instruction. Both PNGs are full opaque phone screens,
   so a container with a tinted ground and a radius would be drawing a second
   card around a card. The corner and shadow come from the --cs-card-* tokens
   on the <img> itself; the dot field is the only ground.

   ── The two screens are deliberately different sizes ───────────────────────
   The "now" screen renders --cs-screen-step (1.2x) larger than the "then" one,
   on instruction: the size jump is the argument, a bigger, denser product
   grown out of a small one. --pro-card-w is the "then" width; the "now" rule
   multiplies it. The stage bottom-aligns them so they stand on one baseline
   and the "now" screen visibly reaches higher.                              */

.cs-page {
  /* --pro-card-w is the "then" (smaller) screen's width. The "now" screen is
     --cs-screen-step times this. Sized so that even at 1.2x on the widest
     screen the four annotation columns still hold their copy in three lines
     inside the bled-wide figure. Measured at 1024 / 1440 / 1880. */
  --pro-card-w:    clamp(156px, 15vw, 224px);
  /* Base 32 is the vertical gap in the < 640 stacked layout and the horizontal
     gap from 640-1024. The corner block bumps it to 56 (+24 on instruction) for
     the main desktop view, where the extra air between the two screens reads as
     the distance the product travelled. */
  --pro-stage-gap: 32px;
  --pro-lead-w:    32px;
  --pro-lead-gap:  12px;

  /* The two screens are bottom-aligned and different heights, so the shorter
     "then" screen's TOP edge sits this far below the taller "now" screen's.
     The top-left annotation is dropped by the same amount (corner layout only)
     so it runs alongside its screen instead of floating in the gap above it.
     = height difference = (now - then) width delta x the 728:1576 aspect. */
  --pro-then-offset: calc(
    (var(--cs-screen-step) - 1) * var(--pro-card-w) * 1576 / 728
  );

  /* The halo, the hover step and the field weight are shared with §10-20's
     before/after pairs — see --cs-screen-* in the token block at the top. */
}

/* Base is the narrow layout: the stage on top, the four annotations stacked
   under it in reading order. The corner composition only switches on when
   there is room for it — see the min-width block below. */
.cs-pro-fig {
  position: relative;
  display: grid;
  gap: 32px;
  padding-block: 24px;
}

/* Decorative ground. Pattern comes from the shared rule in style.css; only the
   box, the fade and the weight are set here. Inset a small fixed amount past
   the figure on every side — enough for the radial mask to have faded the dots
   to nothing before this box ends, so nothing visible is lost, but small
   enough that the bled-wide figure never pushes this past the viewport edge.
   (It was -6% horizontally, which at the widest bleed put it ~60px beyond the
   figure and off-screen at 1024.) */
.cs-pro-dots {
  position: absolute;
  inset: -24px;
  opacity: var(--cs-screen-dots);
  /* The fade the brief asks for: solid through the middle, gone by the outer
     edge, so the field has no border of its own and the shots stop looking
     like they are hanging in empty space. Prefixed copy first for older
     WebKit, which still needs it for mask-image. */
  -webkit-mask-image: radial-gradient(ellipse 62% 58% at 50% 50%, #000 35%, transparent 78%);
          mask-image: radial-gradient(ellipse 62% 58% at 50% 50%, #000 35%, transparent 78%);
  z-index: 0;
}

/* Everything real paints above the field. */
.cs-pro-annot,
.cs-pro-stage { position: relative; z-index: 1; }

.cs-pro-stage {
  display: flex;
  /* Column below 640: side by side, the two screens (156 + 187 + gap) are
     wider than a 375px phone's content box and would force a horizontal
     scrollbar. Stacked, each gets full width. Switches to a row at 640, where
     the annotation grid also pairs up and there is room. */
  flex-direction: column;
  align-items: center;
  gap: var(--pro-stage-gap);
}

@media (min-width: 640px) {

  .cs-pro-stage {
    flex-direction: row;
    /* end, not center: the "now" screen is 1.2x the "then" one, so bottom-
       aligning them stands both on one baseline and lets the taller screen
       reach up — the size difference reads as growth rather than as two things
       floated at different heights. */
    align-items: end;
    justify-content: center;
  }
}

.cs-pro-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin: 0;
  /* So the hover z-index bump below has something to apply to. */
  position: relative;
  /* The slide-apart lands here. Naming the property lets the compositor keep
     both cards on their own layer for the one transform that matters, and
     costs nothing when the motion never runs. */
  will-change: transform;
}

/* The screens themselves. These are opaque full-bleed phone renders, so the
   corner and the shadow have to come from us — without them a 728x1576
   rectangle just sits on the page as a rectangle, and its edges would read
   softly against the cream ground.

   Surface comes from the shared --cs-card-* tokens rather than a bespoke
   recipe, so these read as the same family of object as §2's info card. This
   is that token set's second consumer, which is what it was hoisted for.

   Width is set on the two modifiers below, not here — the "then" and "now"
   screens are deliberately different sizes. Both stay at full opacity.

   DIRECT CHILD, not any descendant. The prologue's own cards hold exactly one
   <img> each, so `> img` selects precisely what `img` did here — but §10-20's
   cards, which reuse these classes, hold interactive screens built from a
   stack of layered <img>s (Error handling alone has ten). As a descendant
   selector this forced every one of those layers to the full card width,
   height:auto, a radius, a card shadow and transform-origin: bottom center,
   and the hover rule below then scaled them against the motion layer's own
   transforms. The screen-level surface for those sections comes from
   .cs-shot-img and the .cs-pro-card--then/--now rules further down instead. */
.cs-pro-card > img,
.cs-pro-card > video {
  display: block;
  height: auto;
  border-radius: var(--cs-card-radius);
  /* Same family as §2's info card, but the screen-weight shadow — these are
     phone captures on a near-page-coloured ground, like every other screen on
     this page, and "then" would otherwise sit flatter than the "now" beside
     it. */
  box-shadow: var(--cs-screen-shadow);
  /* The hover lift lives on the image, NOT on .cs-pro-card. The card element
     is where the entrance timeline writes its own transform, and an inline
     GSAP transform outranks anything a stylesheet says — put both on one
     element and the hover silently stops working the moment the animation has
     run. Two elements, two independent transform stacks, no conflict. */
  transform-origin: bottom center;
}

/* "Then" is the smaller, earlier screen; "now" is 1.2x it. */
.cs-pro-card--then > img,
.cs-pro-card--then > video { width: var(--pro-card-w); }

/* "Now" is larger, and carries the warm halo — the neutral card shadow plus a
   coral-tinted glow layer, so the screen the story lands on is quietly
   celebrated without a fill or any motion. */
.cs-pro-card--now > img,
.cs-pro-card--now > video {
  width: calc(var(--pro-card-w) * var(--cs-screen-step));
  box-shadow: var(--cs-screen-shadow), var(--cs-screen-glow);
}

.cs-pro-annot {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Earlier / Now / Audience. Same family, size and tracking as the §2
   meta-card keys, but weight 700 and --cs-label-strong — on instruction they
   now out-rank the annotation body they sit above, the same treatment the
   eyebrows carry. */
.cs-pro-key {
  font-family: var(--cs-font-ui);
  font-size: var(--meta-key-size);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: var(--meta-key-track);
  text-transform: uppercase;
  color: var(--cs-label-strong);
}

/* Same size and colour as .cs-aside-note, the page's other small annotation
   voice, so the whole document has one scale for "text about the work". */
.cs-pro-text {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.55;
  color: var(--cs-label);
  margin: 0;
  max-width: 28em;
}

/* The leader lines are hidden in the stacked layout: a line pointing sideways
   at nothing is noise, and there is no spatial relationship for it to describe
   until the corner composition switches on. */
.cs-pro-lead { display: none; }

@media (min-width: 640px) {

  /* Halfway house — still no corner composition, but the four annotations pair
     up so then/now read as two columns rather than one long list. */
  .cs-pro-fig { grid-template-columns: 1fr 1fr; }

  .cs-pro-stage { grid-column: 1 / -1; }
}

@media (min-width: 1024px) {

  /* --pro-lead-w stays at its base 32 here — an earlier 40 cost the annotation
     columns 16px they need at the 1024 end for the three-line wrap.
     --pro-stage-gap is 56 (was 32; +24 on instruction). */
  .cs-page { --pro-stage-gap: 56px; }


  /* The composition proper: annotations at the four corners, stage in the
     middle spanning both rows. Placed by area, so the DOM can stay in reading
     order (then / then-audience / stage / now / now-audience) while the
     picture puts the stage between them. */
  .cs-pro-fig {
    grid-template-columns: 1fr auto 1fr;
    grid-template-rows: auto auto;
    grid-template-areas:
      "then     stage now"
      "then-aud stage now-aud";
    align-items: start;
    /* The column gap has to hold the whole leader line plus clear air at both
       ends, or the line runs out of the gap and lands on top of the card it is
       pointing at — measured, it overshot by exactly --pro-lead-gap before this
       was tied to the same two tokens the line itself uses. */
    gap: 48px calc(var(--pro-lead-w) + var(--pro-lead-gap) * 2);
    padding-block: 40px;

    /* The one place on the page allowed past the section's 950px column, on
       instruction: the bigger screens plus four annotation columns that each
       hold their copy in three lines do not fit inside it. Symmetric negative
       margins widen the grid without moving its centre.

       Sized off --cs-margin (the .cs side padding) rather than a raw vw, so it
       tracks the real room outside the section at every width and always stops
       40px short of the .cs content edge — 24px of which is the dot field's
       own overhang, leaving a true 16px gutter before the viewport. Capped at
       200 so it does not run away on a very wide screen. Verified no
       horizontal scroll 1024 → 1920. */
    --pro-bleed: min(calc(var(--cs-margin) - 40px), 200px);
    margin-inline: calc(-1 * var(--pro-bleed));
  }


  .cs-pro-annot--then     { grid-area: then; }

  .cs-pro-annot--then-aud { grid-area: then-aud; align-self: end; }

  .cs-pro-stage           { grid-area: stage; align-self: center; }

  .cs-pro-annot--now      { grid-area: now; }

  .cs-pro-annot--now-aud  { grid-area: now-aud; align-self: end; }


  /* Drop the top-left annotation down to meet its screen. The "then" screen is
     shorter and bottom-aligned, so its top edge is --pro-then-offset below the
     "now" screen's; without this the "Earlier" block floats in the empty band
     above it. translateY, not margin, so the grid row does not grow and push
     the stage off its own centre. GSAP's entrance adds a relative -12px rise on
     top of this (y: '+=12'), so the offset survives the animation. */
  .cs-pro-annot--then { transform: translateY(var(--pro-then-offset)); }


  /* Left pair reads inward: text ranged right, so its ragged edge faces away
     from the stage and its hard edge faces the leader line. */
  .cs-pro-annot--then,
  .cs-pro-annot--then-aud {
    align-items: flex-end;
    text-align: right;
  }


  /* Let the grid column set the measure — with the figure bled wider, each 1fr
     column is now broad enough to hold the longest of the four texts in three
     lines. A cap here would just re-introduce the four-line wrap the bleed was
     added to fix. Verified by counting rendered lines at 1024 / 1440 / 1880. */
  .cs-pro-text { max-width: none; }


  /* The leader line. Absolutely positioned just outside the annotation's inner
     edge and sitting on the key's own centre line, so it reads as running from
     the key to the stage. A hairline, so its 1px is in the rule family rather
     than the multiple-of-4 spacing grid. */
  .cs-pro-lead {
    display: block;
    position: absolute;
    top: 0.7em;
    width: var(--pro-lead-w);
    height: 1px;
    background: var(--cs-rule);
  }


  /* Origins point away from the stage, so the draw-in grows toward the cards
     rather than retreating from them. */
  .cs-pro-annot--then .cs-pro-lead,
  .cs-pro-annot--then-aud .cs-pro-lead {
    left: 100%;
    margin-left: var(--pro-lead-gap);
    transform-origin: left center;
  }


  .cs-pro-annot--now .cs-pro-lead,
  .cs-pro-annot--now-aud .cs-pro-lead {
    right: 100%;
    margin-right: var(--pro-lead-gap);
    transform-origin: right center;
  }
}

/* ═══ §10-20's screens, reusing the prologue's own layout ═══════════════════
   All nine before/after (and one solo) screen sections now sit inside a
   .cs-pro-fig exactly like the prologue's — same dot field, same
   corner-annotation grid at 1024, same leader lines. Nothing below is a new
   component: it is either the prologue's own selectors reaching new markup,
   or a width formula copied from the prologue's so a screen and the
   prologue's own cards resolve to the identical pixel width at every
   viewport, not merely the same ratio. .cs-pro-fig, .cs-pro-dots,
   .cs-pro-stage, .cs-pro-annot, .cs-pro-key, .cs-pro-text and .cs-pro-lead
   are all untouched above -- this block only adds what the prologue itself
   never needed: sizing its cards off --pro-card-w for content that is not a
   bare <img> (the interactive .cs-shot-live screens, and a still-missing
   .cs-ph--phone placeholder), and a --solo variant (Process Map) for the one
   section with a single screen and no "before" to build a 3-area corner
   composition from.

   The old --cs-screen-w-based sizing on .cs-shot--before/--after is left
   alone above rather than deleted: none of these nine sections reads it any
   more (none carries .cs-shot/.cs-shot--before/.cs-shot--after at all), but
   Iterations' .cs-ph--phone and the shared .cs-shot-img base rule still do —
   and Process Map, solo though it is, opts back INTO --cs-screen-w below
   rather than the prologue's --pro-card-w, on instruction: it was never a
   before/after pair, so nothing here needs to match a partner screen's
   width. */

/* Width only -- .cs-shot-img already carries the aspect-ratio, radius and
   base shadow (identical tokens to the prologue's own cards: --radius ==
   --cs-card-radius, --cs-screen-shadow either way), so re-declaring those
   here would just repeat them. The selector reaches three different things
   with one declaration: a plain <img>, the .cs-shot-live interactive div
   (Mobile number, Permissions, Error handling), and .cs-ph--phone (Loaders'
   still-missing "after" asset) -- all three already carry .cs-shot-img or
   .cs-ph--phone today. */
.cs-pro-card--then .cs-shot-img,
.cs-pro-card--then .cs-ph--phone {
  width: var(--pro-card-w);
  max-width: none;
}

.cs-pro-card--now .cs-shot-img,
.cs-pro-card--now .cs-ph--phone {
  width: calc(var(--pro-card-w) * var(--cs-screen-step));
  max-width: none;
}

/* The halo, same as the prologue's "now" and the old .cs-shot--after -- a
   real screen only, never the dashed placeholder standing in for one. */
.cs-pro-card--now .cs-shot-img {
  box-shadow: var(--cs-screen-shadow), var(--cs-screen-glow);
}

/* Process Map: one screen, so its annotation sits beside it in a 2-area
   "stage now" grid rather than the usual 3-area "then stage now" — there is
   no "before" screen to make the third area from. .cs-pro-annot--now and
   .cs-pro-lead need nothing added to work here: both already position
   entirely off their own .cs-pro-annot box (position:relative, set on it and
   .cs-pro-stage together, above) rather than off the 3-column
   grid-template-areas a "then" side would need, so all a solo case has to
   supply is its own area list naming the same "now" area they already read. */
#screen-process-map .cs-pro-card--now .cs-shot-img {
  /* Its own --cs-screen-w, not --pro-card-w -- see the block comment above. */
  width: var(--cs-screen-w);
  max-width: none;
}

@media (min-width: 640px) and (max-width: 1023.98px) {

  /* Stay single-column through the halfway breakpoint the before/after
     sections use to pair their four annotations into two — there is only one
     annotation here, and .cs-pro-fig's own 1fr/1fr split would strand it
     under the screen's left half instead of the full-width stack narrower
     viewports already give it. */
  .cs-pro-fig--solo { grid-template-columns: 1fr; }

  .cs-pro-fig--solo .cs-pro-stage { grid-column: auto; }
}

@media (min-width: 1024px) {

  .cs-pro-fig--solo {
    /* The pair layout's own three columns, with the "then" cell simply left
       empty. The screen has to stay on the section's true centre line, so an
       equal fr column either side of it is the whole trick — a two-column
       "stage now" grid centres the PAIR instead, which pushes the screen left
       of centre by half the annotation's width.

       minmax(0, 1fr), not a bare 1fr: 1fr floors at min-content, so a long
       enough word in the annotation could grow the right column past its
       share and take the screen off centre again. */
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas: ". stage now";
    /* No bleed: one screen and one annotation column sit inside the section's
       own 950px comfortably, unlike a full then/now pair plus two annotation
       columns. */
    margin-inline: 0;
  }
}

/* ═══ The redesigned onboarding, screen by screen ══════════════════════════
   Eleven sections sharing one annotation grammar, so the reader learns it
   once: red marks the problem the research found, green marks the design
   response. Compositions vary; the grammar, the phone scale and the negative
   space do not.

   ── On the two colours ─────────────────────────────────────────────────────
   Green measures 2.7:1 as type on cream and red is little better, so neither
   is ever used as a text colour here. Both appear only as a 2px edge on the
   note they mark, with the note itself in the page's normal foreground. The
   colour is a marker, not the message — and the words "before"/"after" on the
   frames carry the same distinction, so nothing is conveyed by colour alone. */

/* Scoped to Iterations by id now, not by .cs-screen generally: the other nine
   .cs-screen sections were moved onto the prologue's own layout (.cs-pro-fig
   etc., see below), which gets its block rhythm for free from .cs-sec's
   ordinary flex gap (--cs-blk-gap, 32/40) the same way the prologue does.
   Iterations keeps its own flat 32px unchanged either way. */
#screen-iterations {
  gap: 32px;
}

/* No number on any of the eleven screens (Splash included -- it kept a
   "10" for one round after §11-20 dropped theirs, but that was a one-off
   holdout, not a rule, and is gone now). With no number to reserve a first
   column for, the head is a single flush-left column: title and framing
   both start at the section's own left margin, the same edge
   --cs-eyebrow-gap uses for an eyebrow-then-heading pair anywhere else on
   the page.

   Only Process Map still uses .cs-screen-head/-title/-framing — every other
   screen section, Iterations included as of this round, was moved onto
   .cs-eyebrow + .cs-h2 + .cs-sub directly, the same trio §16 uses. The rule
   stays for that one section rather than being retired. */
.cs-screen-head {
  display: flex;
  flex-direction: column;
  gap: var(--cs-eyebrow-gap);
}

.cs-screen-title {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cs-label);
  margin: 0;
}

/* The framing line is the heading that matters — it names the design decision
   rather than the screen — so it takes the section scale and the full column,
   below the small label pair. */
.cs-screen-framing {
  font-family: var(--cs-font-display);
  /* 500, same as .cs-h2 and the project header — the §10-20 screen headings
     sit on the same weight as every other heading on the page. */
  font-weight: 500;
  font-size: var(--cs-h2);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--cs-fg);
  max-width: 18em;
  margin: 0;
}

/* Pointer devices only: on touch there is no hover to leave, so a screen
   would grow on tap and stay grown. Hover with no :focus-within twin, for
   the same reason as §3 — these are images, not controls, and the scale
   carries nothing the composition does not already carry. Origin is the
   bottom edge so a growing screen stays on the shared baseline instead of
   drifting off it. Still live for every .cs-shot-img on the page — the
   figure wrapper that used to bump a growing screen's z-index is gone, but
   .cs-pro-card:hover (the prologue's own rule) covers that for all of
   them now. */
@media (hover: hover) {
  .cs-shot-img { transition: transform 0.3s ease; transform-origin: bottom center; }
  .cs-shot-img:hover { transform: scale(var(--cs-screen-hover)); }
}

/* ═══ A screen that plays ═════════════════════════════════════════════════
   §11's after screen is the real capture taken apart into layers so the
   interaction it describes can actually run: the rewards graph draws, then
   the sheet takes focus and the keypad comes up and types.

   The pixels are still the design's own. Only three things are rebuilt, and
   only because they have to move independently: the bars (the base image has
   them erased, so these are drawn from their measured geometry and sampled
   gradients), the keypad (it does not exist in the capture — the screenshot
   is the resting state), and the typed digits.

   EVERY DEFAULT HERE IS THE RESTING STATE — bars full height, sheet down,
   keypad parked below the frame, placeholder showing. Composited with no
   motion layer at all, this is the original screenshot, which is the same
   contract the rest of the page keeps: the stylesheet hides nothing that
   carries meaning, and case-study-motion.js writes the start state itself.

   .cs-shot-live also carries .cs-shot-img, so width, ratio, corner, shadow,
   the after-screen glow and the hover step all come from there unchanged —
   this reads as the same object as every other screen on the page.        */

.cs-shot-live {
  position: relative;
  overflow: hidden;
  /* Lets the keypad and the typed digits size themselves in cqw/cqh against
     the phone frame, so the whole screen scales as one piece at any width
     instead of needing a font size per breakpoint. */
  container-type: size;
  background: var(--color-white);
}

@media (min-width: 768px) {

  /* Side by side, and now the pair owns the two rows both figures sit in:
     row-gap is the screen-to-annotation distance (16px), column-gap the
     distance between the screens (--pro-stage-gap's 32, then 56 at 1024).

     Columns are sized to the screens and the pair is centred, exactly as
     .cs-pro-stage centres its two. On 1fr 1fr the column-gap was not the gap
     you saw: the smaller before screen left ~85px of its own column empty, so
     the screens sat ~141px apart while the prologue's sat 56px apart. Sizing
     each column to its screen makes the declared gap the real one. */
  .cs-screen-pair {
    grid-template-columns: auto auto;
    grid-template-rows: 1fr auto;
    justify-content: center;
    column-gap: 32px;
    row-gap: 16px;
  }
}

@media (min-width: 1024px) {

  /* Both numbers are .cs-pro-fig's, so a screen pair and the prologue put the
     same air around their screens and the same distance between them
     (--pro-stage-gap is 56 here for exactly this reason). */
  .cs-screen-pair { column-gap: 56px; padding-block: 40px; }
}

@media (min-width: 1200px) {

  /* The frames stop growing with the column past this width; a phone render
     two hands wide stops reading as a phone. Changing the token is the whole
     step-up: it reaches the real screens, the placeholders still waiting on
     assets and the stepped-down before screen at once, with no second list of
     selectors to keep in sync. */
  .cs-page { --cs-screen-w: 320px; }
}

/* ═══ 22 — The AI stack ════════════════════════════════════════════════════
   Four stages in sequence, each with the tool named. A row per stage rather
   than four icons, because the note under each is the part that makes the
   claim defensible in an interview — a tool name on its own is a name-drop.

   Rules sit BETWEEN steps only — the section heading above and the next
   section's heading below are already dividers, so a rule at either end of
   the list would double one up. border-top on every step but the first
   draws exactly the three joins.

   The framing statement that used to open this section as a .cs-lede now
   closes it as a .cs-bubble instead (markup below the list) — right-aligned
   the same way §4's takeaway is, so it ends exactly where the stack's own
   right edge does. */
.cs-ai-stack .cs-bubble { margin-left: auto; }

.cs-stack {
  list-style: none;
  margin: 0;
  padding: 0;
}

.cs-stack-step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 16px;
  grid-template-areas:
    "num  stage"
    ".    note";
  /* Matched to .cs-pp-row's 32px, on instruction — the two are the same
     shape of component (a numbered row split into a label pair either side
     of a rule) and read as inconsistent sitting at two different paddings. */
  padding: 32px 0;
}

.cs-stack-step + .cs-stack-step { border-top: 1px solid var(--cs-rule); }

.cs-stack-num {
  grid-area: num;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.08em;
  color: var(--cs-label);
  padding-top: 4px;
}

/* .cs-stack-stage sits directly in the "stage" area for §22's "What came
   next" reuse (stage, no tool line — see that section's own comment). Where
   there IS a tool line (§22's own four steps), .cs-stack-head wraps both and
   takes the area instead — .cs-stack-stage inside it is then a grandchild,
   not a grid item, so this rule is inert there and never fights the wrapper.
   grid-area is safe to repeat on both: only whichever one is an actual grid
   item of .cs-stack-step reads it. */
.cs-stack-stage,
.cs-stack-head {
  grid-area: stage;
}

.cs-stack-stage {
  font-family: var(--cs-font-display);
  font-weight: 500;
  font-size: var(--cs-text-body);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
}

/* The stage/tool pair's OWN rhythm — a fixed 4px, regardless of how many
   lines the note beside it wraps to.

   This used to be two more rows of the outer grid, with .cs-stack-note
   spanning across both to sit beside them. A grid track that a spanning item
   overflows grows to fit it, and — measured — that growth was split across
   BOTH spanned rows, not absorbed only by the last one: a 4-line note (this
   section's longest) pushed .cs-stack-tool down by 24.6px, a 3-line note by
   14.4px, where every step's own designed gap is 4px. The note's length was
   dictating the gap between the stage and its tool line, on a completely
   unrelated column. Wrapping the pair so the outer grid places ONE "stage"
   item removes the coupling outright: this flex column now sets its own
   internal 4px gap, unreachable by anything .cs-stack-note does. */
.cs-stack-head {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cs-stack-tool {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cs-label);
}

.cs-stack-note {
  grid-area: note;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  line-height: 1.45;
  color: var(--cs-label);
  margin: 8px 0 0;
  max-width: 30em;
}

@media (min-width: 768px) {

  .cs-stack-step {
    /* 12em plus 76px (36px, then another 40px on instruction), not a bigger
       em figure — the stage/tool column was reading crowded against
       "Explore + Prototype", and a flat px add is one clearly-scoped number
       rather than a change to the em base that would also nudge every other
       measurement riding on it. */
    grid-template-columns: auto calc(12em + 76px) 1fr;
    /* One row, not two: see .cs-stack-head above for why the old two-row
       split (with .cs-stack-note spanning both) is gone. Both "stage" and
       "note" are now single-row items sized purely by their own content, so
       neither's height can push the other. */
    grid-template-areas: "num stage note";
    align-items: start;
    column-gap: 32px;
  }

  .cs-stack-note { margin-top: 0; }
}

/* ═══ The close ════════════════════════════════════════════════════════════
   A centered return link gives each case a clear ending.                  */

.cs-close {
  align-items: center;
  text-align: center;
  gap: 32px;
  padding-block: var(--cs-sec-gap);
}

/* Same size and weight as every section heading (.cs-h2) on the page, on
   instruction — this line is a closing statement, not a bigger crescendo
   than the sixteen headings before it. Was --text-headline/400, a page-wide
   token this component was the only user of on this page. */
.cs-close-line {
  font-family: var(--cs-font-display);
  font-weight: 500;
  font-size: var(--cs-h2);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--cs-fg);
  margin: 0;
}

/* A solid black pill, on instruction — matching the site's other two CTAs
   (the hero's "more about me" and the custom cursor label): same 999px
   radius, same var(--color-ink) fill / var(--color-cream) text, and the
   arrow reads off currentColor rather than sitting in its own filled disc,
   so all three read as one family. Literal color-dark/cream rather than the
   context-flipping --cs-fg/--cs-label: this section is always cream ground
   (never a dark band), and the pill must stay black regardless. */
.cs-close-cta {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  min-height: 44px;
  padding: 16px 24px;
  border-radius: 999px;
  background: var(--color-ink);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  color: var(--color-cream);
  text-decoration: none;
  transition: opacity 0.25s ease;
}

.cs-close-cta:hover {
  opacity: 0.85;
}

.cs-close-cta-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: currentColor;
}

.cs-close-back {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  color: var(--cs-label);
  text-decoration: none;
  transition: color 0.25s ease;
}

.cs-close-back:hover { color: var(--cs-fg); }

/* ═══════════════════════════════════════════════════════════════════════════
   myspace.html
   ───────────────────────────────────────────────────────────────────────────
   What that page needs and this stylesheet did not already have: one centred
   screen, a numbered walkthrough, and two small additions to §4's voice card
   so it can carry a user segment instead of a verbatim. Everything else on
   it is reused as-is — the hook, the meta card, the ask section and its
   tilted card row, the green and dark bands, the bubble, the tabs, the
   results row, the close.

   Scoped by class rather than by a page selector, because nothing here is
   MySpace-specific in principle — a later case study wanting one centred
   screen, a numbered walkthrough or a segment card should use these rather
   than write a second variant.
═══════════════════════════════════════════════════════════════════════════ */


/* ── One measure for every heading and its subhead ─────────────────────────
   A section's heading and the subhead under it end on the same right edge,
   page-wide. The two elements default to measures in their OWN ems — .cs-h2
   at 18em of --cs-h2, .cs-sub at 40em of --text-ui — so at 1440 they resolve
   to roughly 863px and 560px and the two ragged edges sit 300px apart. That
   reads as a mistake in a section that opens on both.

   Written as 18 x the HEADING's font size, exactly as .cs-sub--wide already
   does for the one section that had been fixed by hand. --cs-h2 is a clamp(),
   so one figure tracks the whole fluid curve and the pair cannot drift at any
   width. This generalises that fix to the page and supersedes .cs-ask's own
   --ask-measure here, which is a fixed 54rem and therefore only agrees with
   the heading at one viewport.

   Scoped to .cs-ms rather than applied to the base components because
   jupiter.html's measures are tuned and must not reflow. */
.cs-ms .cs-h2,
.cs-ms .cs-sub { max-width: calc(18 * var(--cs-h2)); }

/* ── No halo on this page's "after" screen ─────────────────────────────────
   .cs-pro-card--now adds --cs-screen-glow, a coral halo, over the card
   shadow. On jupiter.html that marks the screen the story lands on. §5 here
   borrows the same component for the opposite purpose: its "after" is the
   idea that got dropped, and the copy under it explains why. A halo would
   celebrate the thing the section goes on to reject.

   Both of the base rules that set it are re-declared with the shadow alone
   rather than overridden with `none`, so the screens keep the card shadow
   every other screen on the site carries. The base reaches the image two
   ways — `> img` and `.cs-shot-img` — so both are answered. */
.cs-ms .cs-pro-card--now > img,
.cs-ms .cs-pro-card--now > video,
.cs-ms .cs-pro-card--now .cs-shot-img { box-shadow: var(--cs-screen-shadow); }

/* ── Bubbles align to the section's own right edge, page-wide ──────────────
   .cs-bubble is width: fit-content with no inline margin, so left in the
   markup is where it renders. jupiter.html's §4 corrects this with a scoped
   rule (`.cs-ask .cs-bubble { margin-left: auto }

`) so its takeaway ends flush
   with the card row's own right edge rather than hanging off the left margin.

   §5 here is a plain .cs-sec, not .cs-ask, so that rule never reached it — its
   bubble rendered flush left instead, the one section on the page whose
   right-edge rule was different from the rest. Generalised here rather than
   duplicated: every bubble on a .cs-ms page now closes to its section's right
   edge, matching the convention the rest of the page already keeps for
   headings and subheads. */
.cs-ms .cs-bubble { margin-left: auto; }

/* ── .cs-voice, adapted — a card that is a segment, not a verbatim ─────────
   myspace.html's §4 is built on jupiter.html's §4, but a card there holds a
   user segment rather than something somebody said, so two things change.

   The big line is upright display type, not Geist italic. Italic is reserved:
   it is how this site marks a quotation, and .cs-voice-quote's own note says
   its italic exists because "it is someone being quoted mid-sentence".
   Reusing it would have claimed these three lines were overheard.

   And the card is laid out as an editorial block rather than a stacked list —
   see .cs-voices-grid--step below. */
.cs-voice-line {
  font-family: var(--cs-font-display);
  /* 500 is Clash Display's own file, as every other 500 on this site is. */
  font-weight: 500;
  font-size: var(--cs-text-lead);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
  margin: 0;
}

/* The segment name. The eyebrow recipe exactly — --text-label / 700 / 0.08em
   uppercase — so it reads as the same class of label as every section eyebrow
   on the page.

   This replaced .cs-voice-method's filled green chip, which read as dated
   against a page that marks structure with hairlines and carries no filled
   surfaces outside the meta card. Ink rather than that chip's green: with no
   coloured ground behind it, green type on white would be the one
   low-contrast label on the page for no reason, and the icon now carries the
   colour the chip used to — attached to something that means one thing.

   .cs-voice-method itself is deliberately untouched: jupiter.html still uses
   it, and that page's chip is not the one that needed changing.

   Colour is --cs-label, not --cs-fg: on instruction, this is now the
   .cs-eyebrow recipe exactly, not just its size and tracking — a section
   eyebrow names the section without competing with its heading, and a card
   tag does the same job for the card underneath it. Matching its ink as well
   as its shape is what makes that reading explicit rather than incidental. */
.cs-voice-tag {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cs-label);
}

/* Fixed 48px rather than an em size. These are raster illustrations with
   their own internal line weights; scaling them off the type would thin or
   thicken those against the card's other rules at different breakpoints.
   Served at 144px (3x) so they stay sharp on a retina screen, with width and
   height repeated on the <img> so the box is reserved before the file lands.

   Pinned to the card's top RIGHT, opposite the text at the bottom left. That
   diagonal, and the air the card keeps between the two, is the whole layout —
   see the grid below. */
.cs-voice-icon {
  align-self: flex-end;
  display: block;
  width: 48px;
  height: 48px;
}

.cs-voice-text {
  /* Pushes the pair to the card's bottom edge, which is what opens the gap
     under the icon. The cards are grid items and stretch to a common height,
     so all three text blocks land on one baseline however long the copy. */
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

/* ── .cs-voices-grid--step — the cards as an ascending run ─────────────────
   An editorial layout rather than three identical tiles in a line: each card
   sits higher than the one before it, the icon anchors the top right, and the
   name and description sit at the bottom left with real space between.

   The rise is not decoration. The three segments are a progression in how far
   into their money a person has got — tracking spends, then working rewards,
   then investing — so the row climbs left to right, and the third card's own
   illustration is an ascending bar chart. The composition says what the copy
   says.

   It replaces the base component's tilt. Both exist to stop the set reading
   as a filed row, and running both would be two devices doing one job.

   Scoped to 1024+ for the same reason the tilt is: below that the cards are
   stacked one per row, where a stagger has no row to step across. */
@media (min-width: 1024px) {
  .cs-voices-grid--step .cs-voice {
    --step-y: 0px;
    /* Not rotate(): this overrides the base tilt on children 1 and 3. */
    transform: translateY(var(--step-y));
    /* Room for the icon, the gap, and two lines of text with air between.
       Without it the shortest card collapses to its content and the diagonal
       reads as three different-sized boxes rather than one stepped run. */
    min-height: 216px;
  }

  .cs-voices-grid--step .cs-voice:nth-child(1) { --step-y: 48px; }
  .cs-voices-grid--step .cs-voice:nth-child(2) { --step-y: 24px; }
  .cs-voices-grid--step .cs-voice:nth-child(3) { --step-y: 0px; }

  /* A transform does not affect layout, so the first card hangs 48px below
     the grid's own box and would close the gap to the takeaway bubble.
     Padding the grid by the largest offset gives that overhang real space to
     live in and keeps the section's rhythm. */
  .cs-voices-grid--step { padding-bottom: 48px; }

  /* The step is the resting state, so hover keeps it and adds the lift on
     top. Reading --step-y back means the two cannot fall out of sync. */
  @media (hover: hover) {
    .cs-voices-grid--step .cs-voice:hover {
      transform: translateY(var(--step-y)) scale(1.02);
    }
  }
}

/* The base component puts its cards on a three-row subgrid — tag, quote,
   attribution — to line those rows up across the set. This layout does not
   want that: it is two blocks pinned to opposite ends of a flex column, and
   subgrid would override the display and strand margin-top: auto.

   Same specificity as the rules being corrected on the grid (one class), so
   it depends on sitting after them; the .cs-voice override is a class deeper
   and wins outright. */
@supports (grid-template-rows: subgrid) {
  @media (min-width: 1024px) {
    .cs-voices-grid--step { grid-template-rows: none; }
    .cs-voices-grid--step .cs-voice {
      display: flex;
      flex-direction: column;
      grid-row: auto;
    }
  }
}

/* ── .cs-solo — one screen, centred, with a caption ────────────────────────
   .cs-strip could not do this job. It is a bleeding four-up row that caps
   itself at two cards below 893px, so a lone child inherits sizing written
   for a set and sits at the same small width four screens share.

   This one is the page's single hero artefact, so it is deliberately larger
   than a walkthrough step: --cs-solo-w tops out at 320px against the shared
   screen size's ~269px. Both bounds are multiples of 4.

   No scroller anywhere near it, so --cs-screen-shadow has room to render in
   full (CLAUDE.md gotcha 11). */
.cs-page { --cs-solo-w: clamp(240px, 28vw, 320px); }

.cs-solo .cs-shot-img {
  width: var(--cs-solo-w);
  /* The shared rule caps every screen at --cs-screen-w (280px). This one is
     allowed past it, so the cap is lifted here rather than raised there. */
  max-width: none;
}

/* Straighten and lift on hover, the same gesture .cs-voice uses for its
   tilted cards. Pointer devices only: on touch there is no hover to leave, so
   a bar would straighten on tap and stay straight. */
@media (hover: hover) {
  .cs-navswap-bar:hover { transform: rotate(0deg) scale(1.03); }
}

/* ── .cs-blocks — §7's widget grid ─────────────────────────────────────────
   Four wells, 2 x 2, spanning the section's own column. Each well holds one
   widget, and each widget is its own carousel of that widget's states —
   the interaction the system was modelled on, so the section demonstrates
   the argument its heading makes rather than only asserting it.

   ── Not looking retro is a set of small decisions, not one ─────────────────
   A tinted grid of boxes is exactly the thing that dates, so each of these is
   deliberate:

     - No border on the well. A hairline around a box that is ALREADY
       distinguished by its fill states the same boundary twice, which is the
       table look.
     - No shadow on the well. These are recessed wells, not cards lifted off
       the page, and a drop shadow on a tinted box is the 2010s skeuomorphic
       tell. The WIDGET inside does cast one — that is the whole point of the
       arrangement: a recessed well with something floating in it, rather
       than two stacked cards.
     - No dashed placeholder inside. The cell is the frame.
     - Real internal padding, so the widget floats in its well rather than
       filling it corner to corner.

   The fill is DERIVED, not picked: --color-dark mixed 6% into the page's own
   cream. That keeps it the same family as the ground at every future change
   to either token, and keeps it warm — a beige a few steps down from the page
   rather than a grey laid over it. 6% lands around #EAE9E5, which reads as a
   distinct surface without becoming a panel.                                */
.cs-page {
  --bb-ground:  color-mix(in srgb, var(--color-ink) 6%, var(--color-cream));
  --bb-gap:     16px;
  --bb-pad:     24px;

  /* Tuned to the real captures rather than guessed. The widest state is
     640x428 and the tallest is the 312x440 compact; the cell has to seat
     both without either being cropped or the well changing size between
     states. Derived at the 1440 breakpoint, where a cell is 467 wide:
     419 inner width, minus the dot row and the slide's shadow gutters,
     leaves the 640x428 card able to run to full width. */
  --bb-cell-ar: 6 / 5;

  /* Reserved in CSS, not created by the dots. widget-carousel.js injects the
     pagination row, so if it never runs the layout must already be the shape
     it will be once it does — otherwise the grid reflows on hydration. */
  --bb-dot-row: 24px;

  /* The scroll port clips on the cross axis (overflow-x: auto forces
     overflow-y to a non-visible value), so the cast shadow has to fit INSIDE
     the track or it gets sheared off at the edge. At --bb-scale the widget
     leaves roughly 70px of slack above and below it, which is well past the
     ~30px the lower shadow stop reaches, so this padding is no longer what
     saves the shadow — it is what pins the height bound, and it is symmetric
     so the widget sits on the exact centre line. */
  --bb-slide-pad: 16px;

  /* Every widget renders at this fraction of the space its slide gives it.
     Applied to BOTH bounds below, so each capture lands at exactly this
     fraction of the size it had before — the compact portrait shrinks by the
     same amount as the wide cards, and nothing is cropped to get there. */
  --bb-scale: 0.6;

  /* Clear air between one widget and the next. Only ever seen in passing now
     that a slide fills the port: it is the beat of empty ground that crosses
     the frame mid-swipe, which is what stops two states reading as one
     continuous strip. */
  --bb-peek-gap: 40px;

  /* drop-shadow, not box-shadow — the captures are rounded cards on
     transparent ground, and box-shadow follows the border box, which would
     cast a hard rectangle around the transparent corners. Same reason and
     same two-stop recipe as --cs-bubble-shadow: a tight contact stop so the
     card sits ON the well, and a wide soft stop so it reads as floating
     above it. Mixed from --color-dark, so it stays in the page's family. */
  --bb-shadow:
    drop-shadow(0 1px 2px  color-mix(in srgb, var(--color-ink) 14%, transparent))
    drop-shadow(0 10px 20px color-mix(in srgb, var(--color-ink) 18%, transparent));
}

/* ── .cs-flow — §8's onboarding rail ───────────────────────────────────────
   Four screens in a row, one centred and lit, the rest dimmed, moved by an
   ordinary horizontal scroll.

   ── The construction ───────────────────────────────────────────────────────
   The rail is a plain scroll container that breaks out of the section column
   and runs the full width of the viewport, so a screen leaving the frame is
   cut by the edge of the window rather than by an invisible column boundary.
   Only the heading above it stays columned.

   Centring falls out of the padding rather than out of any measurement:
   padding-inline is half a viewport less half a screen, so at scroll zero
   screen 01 sits on the centre line, and the last screen can reach that same
   line instead of stopping short against the end of the content.

   scroll-snap-align: center on each step is what makes every screen settle in
   the middle. scroll-snap-stop keeps a fast flick to one screen at a time, so
   the row advances in order rather than skidding to the end.

   ── Why the dimming is a class and not a stylesheet default ────────────────
   Everything renders at full opacity until case-study-flow.js adds
   .cs-flow--live. Without that, the rail is still a working snapping scroller
   and all four screens are legible — nothing is hidden by the stylesheet,
   which is the same contract the motion layer and the prologue keep.        */
.cs-page {
  /* vw and not a percentage, and that is load-bearing. This token is read in
     two places whose percentages resolve against DIFFERENT boxes: padding-
     inline resolves against the rail's containing block, while the steps'
     flex-basis resolves against the rail's own CONTENT box — which is that
     containing block minus the very padding being computed from it. A `66%`
     here came out as 280px in one and 185px in the other, and the first
     screen sat 47px left of centre. A viewport-relative length resolves
     identically in both, so the padding really is the slack either side of
     one step. */
  --flow-shot-w: min(var(--cs-screen-w), 60vw);
  --flow-gap:    80px;

  /* The captures in this rail are 9/20, not the 9/19.5 every other screen on
     the page uses, so the placeholders match THEM rather than the other way
     round. Retune if the remaining three land at a different ratio — but do
     not reach for .cs-shot-img's cover here: cropping would move the shot's
     pixels relative to its box, and the pulse overlay below is positioned in
     percentages measured off the uncropped file. */
  --flow-ar: 9 / 20;

  /* Measured off the assets, not estimated. The My Space glyph in
     onboarding-1-home.png centres at (359.5, 1415.5) in its 720x1600 frame,
     and myspace-pulse.png was drawn 58px wide for that same frame — so its
     resting size is written as that ratio rather than as a number, which
     says where it came from and survives any re-export at another density.
     Percentages throughout, so the overlay tracks the shot at any width. */
  --flow-pulse-x:    49.93%;
  --flow-pulse-y:    88.47%;
  --flow-pulse-size: calc(58 / 720 * 100%);
  --flow-dim:    0.32;

  /* Overflow allowance, not designed spacing. A scroller computes overflow-y
     to auto and clips, so the screens' shadow would be shaved off top and
     bottom once real captures replace the placeholders. --cs-screen-shadow's
     deepest layer is 0 14px 32px -10px, which reaches 14 + 16 - 10 = 20px
     below the box; 24 clears it and stays on the 4px grid. Cancelled to the
     pixel by a negative margin, so it costs the layout nothing. */
  --flow-bleed:  24px;
}

/* Heading row and rail in one column, at the section's own block rhythm —
   the wrapper replaces .cs-sec's gap for these two children rather than
   adding a second, different one. */
.cs-flow {
  display: flex;
  flex-direction: column;
  gap: var(--cs-blk-gap);
}

/* ═══ shipping.html — page scope (.cs-sc) ════════════════════════════════════
   The third case study. It also carries .cs-ms, so the shared heading/subhead
   measure, prose ink, halo-less "now" screen and right-aligned bubbles all
   apply unchanged. Everything below is either a small adaptation of an
   existing component (two-up voices, landscape screens, a narrower bench
   preview) or one of the three pieces this story needed that the other pages
   had no component for: the competitor trees, the flowchart and the video
   pair. All sizes on the 4px grid, all type from tokens. */

/* The accent. Bikayi's own violet — the button colour in its merchant
   dashboard, sampled from the walkthrough (#6811CA) — replaces spacefire on
   this page only, on instruction. Chosen from four options, all of which
   pass: white bubble text on it measures 8.33:1 and the hook highlight on
   cream 7.57:1, so unlike spacefire (2.58 / 2.35) neither is an exception.
   Setting --hook-highlight is enough: --cs-bubble-bg and the cursor dart
   both read it, and a same-specificity rule declared later than .cs-page's
   wins on the shared <body>. */
.cs-sc { --hook-highlight: #6811CA; }

/* §1 — the hook subhead. At the shared 25em measure this page's longer
   sentence ran to four lines with "million" alone on the last. Swept at 1440:
   25em -> 4 lines, 28-40em -> 3 lines but with a ragged last line (72-490px
   of 720). 30em plus balance evens the three (641 / 720 on the last). Em, not
   px, so it keeps pace with --hook-sub-size down the clamp. */
.cs-sc .cs-hook-sub { max-width: calc(13.5 * var(--hook-title-size)); text-wrap: pretty; }

/* = the title's --hook-title-measure, in the title's size */

/* Every section subhead on this page: balance the lines so the last one is
   never a stray word (§10's ran "...never factored / in"). The measure is
   unchanged, only how the words are spread across it. */
/* Same measure as the heading above it (18 x the heading's size), so both end on
   one right edge. `pretty`, not `balance`: balance squeezes every line to the
   narrowest width the line count allows, which is what left the gap on the right. */
.cs-sc .cs-sub { max-width: calc(18 * var(--cs-h2)); text-wrap: pretty; }

/* §3 — the "What did I do?" row is hidden by default on jupiter.html (see
   .cs-did-sec above). Approved for this page in the brief, so shown here. */
.cs-sc .cs-did-sec { display: flex; }

.cs-sc-params {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* §7 — landscape dashboard captures in the .cs-pro-fig grammar. The base
   component is built for two phones side by side with annotations at four
   corners; these are ~1.5:1 screens, so they stack, and each annotation sits
   level with its own screen: first screen left, second screen right. */
.cs-pro-fig--dash {
  --dash-w: clamp(280px, 40vw, 600px);
}

.cs-pro-fig--dash .cs-pro-stage {
  flex-direction: column;
  align-items: center;
}

/* .cs-shot-img is a phone frame by default (9 / 19.5, object-fit: cover),
   which crops a landscape capture to a sliver. Both are reset to the image's
   own shape. */
.cs-pro-fig--dash .cs-pro-card .cs-shot-img {
  width: min(100%, var(--dash-w));
  max-width: none;
  height: auto;
  aspect-ratio: auto;
  object-fit: fill;
  border-radius: var(--cs-card-radius);
  box-shadow: var(--cs-screen-shadow);
}

.cs-pro-fig--dash .cs-pro-card { width: 100%; }

@media (min-width: 1024px) {

  /* display: contents lifts the two cards into the figure's own grid, so each
     can share a row with its annotation instead of both hanging off one
     stage that spans two rows of unknown height. */
  .cs-pro-fig--dash {
    grid-template-areas:
      "then shot-a ."
      ".    shot-b now";
    row-gap: var(--pro-stage-gap);
  }

  .cs-pro-fig--dash .cs-pro-stage { display: contents; }

  .cs-pro-fig--dash .cs-pro-card { position: relative; z-index: 1; width: var(--dash-w); }

  .cs-pro-fig--dash .cs-pro-card .cs-shot-img { width: 100%; }

  .cs-pro-fig--dash .cs-pro-card--then { grid-area: shot-a; }

  .cs-pro-fig--dash .cs-pro-card--now  { grid-area: shot-b; }

  .cs-pro-fig--dash .cs-pro-annot--then,
  .cs-pro-fig--dash .cs-pro-annot--now {
    align-self: center;
    transform: none;
  }
}

/* §8 — competitor shipping models as three cards of nested boxes. */
.cs-sc-comp {
  display: grid;
  gap: 16px;
}

@media (min-width: 1024px) {

  .cs-sc-comp { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
}

.cs-sc-comp-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px;
}

/* Logo + name. The three tiles are normalised to one square (192px source,
   48px here, so 4x for retina) and share one corner: --hook-icon-radius, the
   app-icon corner the Bikayi mark already uses — the depicted-object
   exception in CLAUDE.md, a percentage of the tile's own size. Wix's own
   artwork is rounded slightly more (22% against 20%); the <1px difference at
   48px is invisible. Shopify's tile is white, so the hairline is what makes
   it read as a tile on a white card; on the two purple tiles it is
   invisible, so all three carry it and none is a special case. An outline
   rather than an inset shadow: an <img>'s inset shadow paints UNDER its
   pixels, so an opaque white tile would hide it. Outlines follow the radius
   and paint on top. */
.cs-sc-comp-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.cs-sc-logo {
  display: block;
  width: 48px;
  height: 48px;
  flex: none;
  border-radius: var(--hook-icon-radius);
  outline: 1px solid var(--cs-rule);
  outline-offset: -1px;
}

@media (prefers-reduced-motion: reduce) {

  .cs-sc-comp-card { transition: none; }

  .cs-sc-comp-card:hover { transform: none; }
}

/* The levels. Each is a 4% ink tint box; nested inside the one above, the
   tints stack, so a deeper level is a shade darker without a new colour
   token. Chips sit white on the tint. The "Also" footnote holds the side
   features that are not on the path to a rate, below a rule, pinned to the
   card's foot so the three cards' footers line up. */
.cs-sc-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 16px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--color-ink) 4%, transparent);
}

.cs-sc-box .cs-sc-box { margin-top: 8px; }

.cs-sc-box-name {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  font-weight: 700;
  line-height: 1.4;
  color: var(--cs-fg);
}

.cs-sc-box-note {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.45;
  color: var(--cs-label);
}

.cs-sc-chips {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* The pill shape is .cs-did's literal 999px (CLAUDE.md, radius exception),
   repeated rather than borrowed because that rule also sets the larger
   padding and type of the "What did I do?" row. */
.cs-sc-chips li {
  padding: 4px 12px;
  border: 1px solid var(--cs-rule);
  border-radius: 999px;
  background: var(--color-white);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.4;
  white-space: nowrap;
  color: var(--cs-fg);
}

.cs-sc-also {
  margin: auto 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--cs-rule);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.45;
  color: var(--cs-label);
}

.cs-sc-also b { color: var(--cs-fg); margin-right: 4px; }

/* §9 — delivery apps as a vertical tab set: rows left, the selected app's
   screen right. See the markup comment for why this is not the bench board.

   Rows: number, then name over a two-line description in one left-aligned
   column, then an arrow. At rest a row is flat on the page; hover tints it;
   selected, it becomes a white card (.cs-card's own surface and shadow) and
   its arrow turns the accent — the one visual tie between a row and the
   screen beside it. The stage is sized to the captures (~392:847), and the
   list is centred against it so neither column hangs. */
.cs-sc-apps {
  display: grid;
  gap: 40px;
  justify-items: center;
}

@media (min-width: 1025px) {

  .cs-sc-apps {
    /* The list is capped so a row card does not run a long empty stretch
       between its copy and its arrow; space-between puts the screen on the
       column's right edge. */
    grid-template-columns: minmax(0, 480px) 260px;
    justify-content: space-between;
    gap: 64px;
    align-items: center;
    justify-items: stretch;
  }
}

.cs-sc-apps-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.cs-sc-app {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 24px;
  width: 100%;
  padding: 24px;
  border: 0;
  border-radius: var(--cs-card-radius);
  background: transparent;
  color: var(--cs-fg);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.25s ease, box-shadow 0.25s ease;
}

.cs-sc-app:hover { background: color-mix(in srgb, var(--color-ink) 4%, transparent); }

.cs-sc-app[aria-selected="true"] {
  background: var(--cs-card-bg);
  box-shadow: var(--cs-card-shadow);
}

.cs-sc-app:focus-visible {
  outline: 2px solid var(--cs-accent);
  outline-offset: 2px;
}

.cs-sc-app-num {
  align-self: start;
  padding-top: 4px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.08em;
  /* Tabular figures: proportional "01" is narrower than "02", which set the
     first row's name 3px left of the other two. */
  font-variant-numeric: tabular-nums;
  color: var(--cs-label);
}

.cs-sc-app-text {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cs-sc-app-name {
  font-family: var(--cs-font-display);
  font-weight: 500;
  font-size: var(--cs-text-lead);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* 22em puts every description on exactly two lines (measured: the
   shortest, Dunzo's, needs 23.8em to fit on one; the longest, Porter's,
   still fits two). */
.cs-sc-app-lens {
  max-width: 22em;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  line-height: 1.5;
  color: var(--cs-label);
}

/* The arrow is faint at rest, darker on hover, the accent when selected, and
   nudges toward the screen — the same 4px shift as the home page card CTA. */
.cs-sc-app-arrow {
  color: color-mix(in srgb, var(--color-ink) 32%, transparent);
  transition: color 0.25s ease, transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.cs-sc-app:hover .cs-sc-app-arrow { color: var(--cs-fg); }

.cs-sc-app[aria-selected="true"] .cs-sc-app-arrow {
  color: var(--cs-accent);
  transform: translateX(4px);
}

.cs-sc-apps-stage {
  width: min(260px, 70vw);
}

.cs-sc-app-shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--cs-card-radius);
  box-shadow: var(--cs-screen-shadow);
  animation: cs-duo-swap 0.35s ease both;
}

.cs-sc-app-shot[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {

  .cs-sc-app,
  .cs-sc-app-arrow { transition: none; }

  .cs-sc-app[aria-selected="true"] .cs-sc-app-arrow { transform: none; }

  .cs-sc-app-shot { animation: none; }
}

/* §11 — the two scope cards side by side. */
.cs-sc-scope {
  display: grid;
  gap: 16px;
}

.cs-sc-scope.cs-requirement-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 24px);
}

.cs-requirement-card {
  align-items: flex-start;
  justify-content: flex-start;
  gap: 14px;
  min-height: 224px;
  padding: clamp(22px, 3vw, 32px);
  border: 1px solid color-mix(in srgb, var(--cs-accent) 12%, var(--cs-rule));
  background: color-mix(in srgb, var(--color-white) 96%, var(--cs-accent));
  box-shadow: 0 10px 28px -18px color-mix(in srgb, var(--color-ink) 24%, transparent);
}

.cs-requirement-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.cs-requirement-index {
  display: inline-grid;
  width: 34px;
  height: 34px;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--cs-accent) 28%, transparent);
  border-radius: 50%;
  background: color-mix(in srgb, var(--cs-accent) 8%, var(--color-white));
  color: var(--cs-accent);
  font: 600 var(--cs-text-label)/1 var(--cs-font-ui);
  font-variant-numeric: tabular-nums;
}

.cs-requirement-label {
  color: var(--cs-label);
  font: 600 var(--cs-text-label)/1.3 var(--cs-font-ui);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.cs-requirement-card .cs-duo-card-title {
  font-size: clamp(1.375rem, 2vw, 1.75rem);
}

.cs-requirement-card .cs-sc-scope-copy {
  max-width: 42ch;
  padding: 0;
  color: var(--cs-label-strong);
  font-size: var(--cs-text-ui);
  line-height: 1.5;
  text-align: left;
}

.cs-requirement-mark {
  width: 36px;
  height: 3px;
  margin-top: auto;
  border-radius: 999px;
  background: var(--cs-accent);
  opacity: 0.78;
}

@media (min-width: 768px) {

  .cs-sc-scope { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
}

@media (max-width: 640px) {
  .cs-sc-scope.cs-requirement-grid { grid-template-columns: 1fr; }
  .cs-requirement-card { min-height: 0; }
}

.cs-sc-scope-copy {
  margin: 0;
  padding-left: calc(var(--duo-chip, 36px) + var(--duo-chip-gap, 12px));
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-ui);
  line-height: 1.5;
  color: var(--cs-label);
}

/* The shared methods row under the two cards. */
.cs-sc-methods {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 32px;
}

/* "Product based" is not built yet: dashed and muted, the same pill with its
   border and ink pulled back. */
.cs-pills li.cs-sc-pill-next {
  border-style: dashed;
  color: var(--cs-label);
}

.cs-sc-scope-sub {
  margin: 0 0 12px;
  padding-left: calc(var(--duo-chip, 36px) + var(--duo-chip-gap, 12px));
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cs-label);
}

/* §13 — the flowchart. Every connector is a 1px hairline drawn by a node's
   own ::before (the stem into it) or by the split's ::before/::after (the
   bar across the two branches), so nothing is positioned against anything
   but its own box. --fc-stem is the gap between nodes and the stem length. */
.cs-sc-flow {
  --fc-stem: 32px;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--fc-stem);
}

.cs-sc-flow ol,
.cs-sc-flow ul { list-style: none; margin: 0; padding: 0; }

.cs-sc-flow-node {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: min(100%, 360px);
  padding: 20px 24px;
}

/* Stems in: every node that has something above it. Stem out: the node the
   flow splits from, which reaches down to the split's top bar. */
.cs-sc-flow > .cs-sc-flow-node + .cs-sc-flow-node::before,
.cs-sc-flow > .cs-sc-flow-split + .cs-sc-flow-node::before,
.cs-sc-flow-branch > .cs-sc-flow-node::before,
.cs-sc-flow > .cs-sc-flow-node:has(+ .cs-sc-flow-split)::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 1px;
  height: var(--fc-stem);
  background: var(--cs-fg);
}

.cs-sc-flow > .cs-sc-flow-node:has(+ .cs-sc-flow-split)::after {
  bottom: auto;
  top: 100%;
}

.cs-sc-flow-title {
  font-family: var(--cs-font-display);
  font-weight: 500;
  font-size: var(--cs-text-lead);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--cs-fg);
}

.cs-sc-flow-sub,
.cs-sc-flow-list li {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-meta);
  line-height: 1.45;
  color: var(--cs-label);
}

.cs-sc-flow-list { display: flex; flex-direction: column; gap: 8px; }

/* The split: two branches under one bar. The branch's own first node draws
   the stem down from that bar; the split's ::before is the bar itself. */
.cs-sc-flow-split {
  position: relative;
  display: grid;
  gap: var(--fc-stem);
  width: 100%;
}

.cs-sc-flow-branch {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--fc-stem);
}

@media (min-width: 768px) {

  .cs-sc-flow-split {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 24px;
    /* Room for the stems between each bar and the nodes it joins. */
    padding-block: var(--fc-stem);
  }

  /* Bar across the top (from the split point) and across the bottom (into
     the save step), each running between the two branch centres. */
  .cs-sc-flow-split::before,
  .cs-sc-flow-split::after {
    content: '';
    position: absolute;
    left: 25%;
    right: 25%;
    height: 1px;
    background: var(--cs-fg);
  }

  .cs-sc-flow-split::before { top: 0; }

  .cs-sc-flow-split::after  { bottom: 0; }

  /* Each branch's last node stretches (flex: 1, and the grid stretches both
     branches to one height), so the two bottoms are level and one stem of
     --fc-stem reaches the bottom bar from either. */
  .cs-sc-flow-branch > .cs-sc-flow-node:last-child { flex: 1; }

  .cs-sc-flow-branch > .cs-sc-flow-node:last-child::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 100%;
    width: 1px;
    height: var(--fc-stem);
    background: var(--cs-fg);
  }
}

/* §14 — the two walkthroughs. The frame is the videos' own 380:240, so the
   player never letterboxes; overflow + the card radius round the iframe. */
.cs-sc-videos {
  display: flex;
  flex-direction: column;
  gap: 56px;
}

.cs-sc-video {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
}

.cs-sc-video-frame {
  aspect-ratio: 380 / 240;
  overflow: hidden;
}

.cs-sc-video-frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ── .cs-iter — a numbered row of iterations ───────────────────────────────
   myspace.html §5, "The starting point": four Home screens in the order they
   were tried, each with its number and what it taught. Sized and laid out
   like .cs-strip (the same four-up row at every screen's "after" size,
   bleeding past the 950px column, centred, wrapping 2 x 2 below ~893px and
   one per row on a phone), with a caption under each screen held to the
   screen's own width so the four read as columns. Top-aligned, so the
   numbers and captions sit on one line across the row. The dot ground is
   the prologue's own .cs-pro-dots. */
.cs-iter {
  position: relative;
  margin-inline: calc(50% - 50vw);
  padding: 24px var(--strip-edge, 24px);
}

.cs-iter-item .cs-shot-img {
  width: 100%;
  max-width: none;
}

.cs-iter-item .cs-pro-text { max-width: none; }

/* The row already runs edge to edge, so the ground stays inside it rather
   than reaching past it (its usual -24px would widen the page). */
.cs-iter .cs-pro-dots { inset: 0; }

.cs-pv .cs-hook-sub { max-width: calc(13.5 * var(--hook-title-size)); text-wrap: pretty; }

/* Same field as the other projects' screens (.cs-pro-dots), bled well past the
   video so a ring of it shows around the frame, and faded to nothing at its own
   edge (a closest-side ellipse: solid until just past the video's edge). */
.cs-pv-dots {
  inset: -96px -96px;
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 76%, transparent 100%);
          mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 76%, transparent 100%);
}
/* ═══════════════════════════════════════════════════════════════════════════
   Índice de seções (.cs-nav)

   Não existe na referência: lá a barra do topo só carrega a volta e o
   progresso. Aqui as páginas são longas e o leitor precisa saber onde está,
   então o índice entra como bloco próprio, colado no topo, e sai de cena
   no mobile para não competir com a leitura.
   ═════════════════════════════════════════════════════════════════════════ */

.cs-nav {
  position: sticky;
  top: var(--cs-bar-h, 56px);
  z-index: 5;
  width: 100%;
  padding: 12px var(--cs-margin, 6.4vw);
  background: color-mix(in srgb, var(--color-cream) 92%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--cs-rule);
}

.cs-nav-list {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(12px, 2vw, 28px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.cs-nav-link {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cs-label);
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.2s ease;
}

.cs-nav-link:hover,
.cs-nav-link:focus-visible { color: var(--cs-fg); }

.cs-nav-num {
  font-variant-numeric: tabular-nums;
  opacity: 0.55;
}

/* Source card used when the publisher prevents third-party iframe embeds. */
.cs-proof-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: var(--cs-blk-gap);
}

.cs-proof-card {
  display: grid;
  gap: clamp(12px, 2vw, 20px);
  padding: clamp(22px, 4vw, 44px);
  border: 1px solid var(--cs-rule);
  border-radius: var(--cs-card-radius);
  background: var(--cs-card-bg);
  box-shadow: var(--cs-card-shadow);
  color: var(--cs-fg);
  text-decoration: none;
  transition: transform 240ms ease, border-color 240ms ease, box-shadow 240ms ease;
}

.cs-proof-card:hover {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--cs-accent) 55%, var(--cs-rule));
  box-shadow: 0 14px 34px -12px color-mix(in srgb, var(--color-ink) 22%, transparent);
}

.cs-proof-card:focus-visible {
  outline: 3px solid var(--cs-accent);
  outline-offset: 4px;
}

@media (max-width: 700px) {
  .cs-proof-grid { grid-template-columns: 1fr; }
}

.cs-proof-source,
.cs-proof-meta {
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.cs-proof-source { color: var(--cs-label-strong); }

.cs-proof-title {
  max-width: 30ch;
  font-size: clamp(1.35rem, 2.5vw, 2.2rem);
  font-weight: 600;
  line-height: 1.22;
  text-wrap: balance;
}

.cs-proof-excerpt {
  max-width: 65ch;
  color: var(--cs-label-strong);
  font-size: var(--cs-text-ui);
  line-height: 1.65;
}

.cs-proof-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--cs-rule);
  color: var(--cs-label);
}

.cs-proof-meta span { color: var(--cs-accent); font-size: 1.35em; }

.cs-live-frame-shell {
  width: 100%;
  height: clamp(560px, 82vh, 900px);
  overflow: hidden;
  border: 1px solid var(--cs-rule);
  border-radius: var(--cs-card-radius);
  background: #fff;
  box-shadow: var(--cs-card-shadow);
}

.cs-live-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: #fff;
}

.cs-live-frame-note {
  margin: 0;
  color: var(--cs-label);
  font-family: var(--cs-font-ui);
  font-size: var(--cs-text-label);
  line-height: 1.55;
}

.cs-live-frame-note a {
  color: var(--cs-fg);
  text-underline-offset: 0.2em;
}

.cs-live-frame-note a:focus-visible {
  outline: 3px solid var(--cs-accent);
  outline-offset: 4px;
  border-radius: 2px;
}

@media (max-width: 700px) {
  .cs-live-frame-shell {
    height: min(78vh, 760px);
    min-height: 500px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cs-proof-card { transition: none; }
  .cs-proof-card:hover { transform: none; }
}

@media (max-width: 900px) {
  .cs-nav { display: none; }
}

/* Interactive CellGuard design-system study. The palette highlights where
   each semantic color is used in a dashboard built from Shadcn-style parts. */
.cs-design-intro { display: flex; flex-direction: column; gap: var(--cs-blk-gap); }
.cs-design-intro .cs-sub { max-width: 54rem; }
.cs-design-system { scroll-margin-top: calc(var(--cs-bar-h) + 4rem); }
.cs-design-workbench {
  --design-navy: #082f5c;
  --design-blue: #075eb5;
  --design-green: #11a83d;
  --design-red: #f0444c;
  --design-highlight: var(--design-navy);
  display: grid;
  gap: 14px;
  padding: clamp(16px, 2.3vw, 30px);
  border: 1px solid rgb(31 49 72 / 12%);
  border-radius: 22px;
  background: #fff;
  box-shadow: 0 14px 40px rgb(24 31 42 / 6%);
}
.cs-design-workbench[data-active-token="brand"] { --design-highlight: var(--design-navy); }
.cs-design-workbench[data-active-token="action"] { --design-highlight: var(--design-blue); }
.cs-design-workbench[data-active-token="success"] { --design-highlight: var(--design-green); }
.cs-design-workbench[data-active-token="danger"] { --design-highlight: var(--design-red); }
.cs-design-workbench[data-active-token="surface"] { --design-highlight: #8b99aa; }
.cs-design-palette {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}
.cs-design-swatch {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 10px;
  border: 1px solid rgb(31 49 72 / 12%);
  border-radius: 12px;
  background: #fff;
  color: #172333;
  text-align: left;
  cursor: pointer;
  transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease;
}
.cs-design-swatch:hover { transform: translateY(-2px); border-color: rgb(31 49 72 / 28%); }
.cs-design-swatch[aria-pressed="true"] {
  border-color: var(--design-highlight);
  background: color-mix(in srgb, var(--design-highlight) 6%, white);
  box-shadow: inset 0 0 0 1px var(--design-highlight);
}
.cs-design-swatch:focus-visible,
.cs-design-swatch:focus-visible .cs-design-swatch-color { outline: 3px solid #1d70c9; outline-offset: 3px; }
.cs-design-swatch-color {
  flex: 0 0 28px;
  width: 28px;
  height: 28px;
  border: 1px solid rgb(31 49 72 / 14%);
  border-radius: 8px;
  background: var(--swatch-color);
}
.cs-design-swatch-copy { display: grid; gap: 2px; min-width: 0; }
.cs-design-swatch-copy strong { overflow: hidden; font-size: 0.75rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.cs-design-swatch-copy small { color: #69788b; font-size: 0.68rem; font-variant-numeric: tabular-nums; }
.cs-design-feedback {
  margin: 0;
  padding: 10px 12px;
  border-left: 3px solid var(--design-highlight);
  border-radius: 0 8px 8px 0;
  background: #f5f7fa;
  color: #405167;
  font-size: 0.85rem;
  line-height: 1.5;
  transition: border-color 160ms ease;
}
.cs-design-preview {
  overflow: hidden;
  border: 1px solid #dce2e9;
  border-radius: 14px;
  background: #f0f3f7;
  color: #182433;
  font-family: var(--cs-font-ui);
}
.cs-design-topbar {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 16px;
  min-height: 56px;
  padding: 10px 18px;
  background: var(--design-navy);
  color: #fff;
  transition: outline-color 160ms ease;
}
.cs-design-wordmark { display: block; width: 132px; height: auto; filter: brightness(0) invert(1); }
.cs-design-search,
.cs-design-date {
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid #d8e1ec;
  border-radius: 7px;
  background: #fff;
  color: #65758a;
  font-size: 0.76rem;
}
.cs-design-search { max-width: 360px; background: #f9fbfd; }
.cs-design-search span { margin-right: 7px; color: #314e6f; font-size: 1rem; }
.cs-design-avatar {
  display: grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border-radius: 50%;
  background: #e4ebf2;
  color: #29405b;
  font-size: 0.65rem;
  font-weight: 700;
}
.cs-design-dashboard { display: grid; gap: 14px; padding: clamp(14px, 2vw, 24px); }
.cs-design-dashboard-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
.cs-design-dashboard-head h3 { margin: 0 auto 0 0; color: #182433; font-size: 1.2rem; }
.cs-design-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  border-radius: 999px;
  background: #e8f8ed;
  color: #087a2c;
  font-size: 0.7rem;
  font-weight: 600;
  white-space: nowrap;
}
.cs-design-badge span { width: 7px; height: 7px; border-radius: 50%; background: var(--design-green); }
.cs-design-date { padding: 7px 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cs-design-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.cs-design-metric {
  display: grid;
  gap: 10px;
  min-width: 0;
  padding: 14px;
  border: 1px solid #e0e6ed;
  border-radius: 9px;
  background: #fff;
}
.cs-design-metric span { color: #4d5c6f; font-size: 0.75rem; line-height: 1.35; }
.cs-design-metric strong { color: var(--design-navy); font-size: 1.35rem; line-height: 1; font-variant-numeric: tabular-nums; }
.cs-design-table-card { overflow: hidden; border: 1px solid #dfe5ec; border-radius: 9px; background: #fff; }
.cs-design-table-head,
.cs-design-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; padding: 11px 14px; }
.cs-design-table-head { border-bottom: 1px solid #e2e7ed; }
.cs-design-tabs { display: flex; flex-wrap: wrap; gap: 5px; }
.cs-design-tabs > span { padding: 7px 9px; border-radius: 6px; color: #53647a; font-size: 0.74rem; }
.cs-design-tabs > span.is-selected { background: #eaf2fa; color: var(--design-blue); font-weight: 600; }
.cs-design-tabs b { margin-left: 4px; font-size: 0.68rem; font-variant-numeric: tabular-nums; }
.cs-design-component-label { color: #78869a; font-size: 0.68rem; letter-spacing: 0.04em; }
.cs-design-table-wrap { overflow-x: auto; }
.cs-design-table { width: 100%; min-width: 480px; border-collapse: collapse; text-align: left; font-size: 0.74rem; }
.cs-design-table th,
.cs-design-table td { padding: 10px 12px; border-bottom: 1px solid #e7ebf0; white-space: nowrap; }
.cs-design-table th { color: #65758a; font-size: 0.66rem; font-weight: 500; }
.cs-design-table td { color: #27364a; }
.cs-design-table tr:last-child td { border-bottom: 0; }
.cs-design-check {
  display: inline-grid;
  width: 15px;
  height: 15px;
  margin-right: 8px;
  place-items: center;
  border: 1px solid #aebccc;
  border-radius: 4px;
  background: var(--design-blue);
  color: white;
  font-size: 0.65rem;
  vertical-align: -2px;
}
.cs-design-row-status { color: #56677d; }
.cs-design-actions { border-top: 1px solid #e7ebf0; }
.cs-design-button {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 11px;
  border-radius: 6px;
  background: var(--design-red);
  color: white;
  font-size: 0.73rem;
  font-weight: 600;
}
.cs-design-workbench [data-design-token][data-token-active="true"] {
  outline: 2px solid var(--cg-active-color, var(--design-highlight));
  outline-offset: 2px;
  position: relative;
  z-index: 1;
}

@media (max-width: 760px) {
  .cs-design-palette { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cs-design-swatch:last-child { grid-column: 1 / -1; }
}
@media (max-width: 900px) {
  .cs-design-system { scroll-margin-top: calc(var(--cs-bar-h) + 1rem); }
}
@media (max-width: 520px) {
  .cs-design-workbench { padding: 12px; border-radius: 16px; }
  .cs-design-topbar { grid-template-columns: 1fr auto; gap: 9px; padding: 10px 12px; }
  .cs-design-search { grid-column: 1 / -1; grid-row: 2; max-width: none; }
  .cs-design-dashboard { gap: 10px; padding: 12px; }
  .cs-design-dashboard-head { align-items: flex-start; }
  .cs-design-dashboard-head h3 { width: 100%; }
  .cs-design-metrics { grid-template-columns: 1fr; gap: 7px; }
  .cs-design-metric { grid-template-columns: 1fr auto; align-items: center; gap: 8px; padding: 10px 12px; }
  .cs-design-metric strong { font-size: 1.1rem; }
  .cs-design-table-head,
  .cs-design-actions { padding-inline: 10px; }
}
@media (prefers-reduced-motion: reduce) {
  .cs-design-swatch,
  .cs-design-feedback { transition: none; }
  .cs-design-swatch:hover { transform: none; }
}

/* Texto corrido acompanha toda a largura de sua coluna em cada estudo.
   As medidas menores anteriores deixavam uma faixa vazia ao lado dos
   parágrafos, mesmo quando a coluna ainda tinha espaço disponível. */
.cs-page :is(.cs-lede, .cs-sub) {
  width: 100%;
  max-width: 100%;
}

/* Cocajupi — packaging palette from the supplied brand manual. */
.cs-page.cs-cocajupi {
  --cocajupi-red: #DB3530;
  --cocajupi-orange: #DD602F;
  --cocajupi-purple: #6B3089;
  --cocajupi-cream: #FCFEEE;
  --color-cream: var(--cocajupi-cream);
  --color-paper: #FFFFFF;
  --color-green: var(--cocajupi-red);
  --color-green-deep: #B82B27;
  --color-green-light: #F9E7E4;
  --color-coral: var(--cocajupi-red);
  --color-coral-light: #F9E7E4;
  --cs-accent: var(--cocajupi-red);
  --cs-card-bg: #FFFFFF;
  --hook-highlight: var(--cocajupi-red);
}

.cs-cocajupi-palette {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(10px, 1.5vw, 16px);
  margin-top: clamp(24px, 3vw, 40px);
}

.cs-cocajupi-swatch {
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--cs-rule);
  border-radius: var(--cs-card-radius);
  background: var(--cs-card-bg);
}

.cs-cocajupi-swatch::before {
  content: '';
  display: block;
  height: clamp(76px, 11vw, 132px);
  background: var(--swatch-color);
}

.cs-cocajupi-swatch--red { --swatch-color: var(--cocajupi-red); }
.cs-cocajupi-swatch--orange { --swatch-color: var(--cocajupi-orange); }
.cs-cocajupi-swatch--purple { --swatch-color: var(--cocajupi-purple); }
.cs-cocajupi-swatch--cream { --swatch-color: var(--cocajupi-cream); }

.cs-cocajupi-swatch-name,
.cs-cocajupi-swatch-hex {
  display: block;
  padding-inline: clamp(11px, 1.4vw, 16px);
}

.cs-cocajupi-swatch-name {
  padding-top: 12px;
  color: var(--cs-fg);
  font-size: var(--cs-text-label);
  font-weight: 600;
  line-height: 1.35;
}

.cs-cocajupi-swatch-hex {
  padding-top: 4px;
  padding-bottom: 14px;
  color: var(--cs-label);
  font-size: var(--cs-text-label);
  font-variant-numeric: tabular-nums;
}

.cs-cocajupi-spec {
  display: grid;
  gap: 6px;
  margin-top: clamp(20px, 2.5vw, 32px);
  padding: clamp(16px, 2vw, 24px);
  border-left: 4px solid var(--cocajupi-red);
  border-radius: 0 var(--cs-card-radius) var(--cs-card-radius) 0;
  background: var(--cs-card-bg);
  color: var(--cs-label);
  font-size: var(--cs-text-ui);
  line-height: 1.5;
}

.cs-cocajupi-spec strong {
  color: var(--cs-fg);
  font-weight: 600;
}

@media (max-width: 760px) {
  .cs-cocajupi-palette { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cs-cocajupi-swatch::before { height: clamp(68px, 18vw, 104px); }
}
