/* ============================================================
   THE OFFER, FROM BOTH ENDS

   Three captures read left to right: the sender's referral page as it
   was, the share sheet that opened from it, and the email the friend
   received. One row, because the point is what none of the three say:
   what is in the box, who it feeds, what it costs. Split across two rows
   the reader compares two and forgets the third.

   Each capture sits on a card of food texture rather than flat on the
   page. The texture is darkened by a gradient in CSS, not baked in, so
   the weight can be tuned without a re-export. The capture is shown
   whole, inset, and the card is as tall as the capture plus its line: no
   fade, nothing cropped, because all three are the same length anyway.
   ============================================================ */

.cs-trio { display: block; }

.cs-trio-row {
  display: grid;
  /* the full stage, on request: three cards across the whole figure width */
  width: 100%;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(12px, 1.8vw, 24px);
  align-items: stretch;
}

.cs-trio-cell {
  --tex-top: rgba(20, 14, 8, 0.28);
  --tex-bottom: rgba(20, 14, 8, 0.78);
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 9% 9% 8%;
  border-radius: 18px;
  overflow: hidden;
  background: #1c1714;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12), 0 24px 50px -30px rgba(0, 0, 0, 0.5);
  isolation: isolate;
}

/* the darkening: heavier toward the text */
/* THE TEXTURE, BLURRED. It was the cell's own background-image; it moved onto
   this layer so it can carry a filter without blurring the capture sitting on
   top of it. The food shots are busy at this size and competed with the
   screenshots they exist to frame -- out of focus they still read as food and
   stop asking to be looked at.

   inset is NEGATIVE because a blur samples beyond its own edge: at inset 0 the
   filter pulls the ground in from all four sides and leaves a soft dark rim
   inside the radius. The overscan is wider than the blur radius so the cell's
   corners stay full-bleed, and .cs-trio-cell's overflow: hidden clips it. */
.cs-trio-cell::before {
  content: '';
  position: absolute;
  inset: -24px;
  background: var(--tex) center / cover no-repeat;
  filter: blur(7px);
  z-index: 0;
}

/* the darkening, above the texture. ::after rather than ::before so it paints
   over it at the same z-index; the capture is z-index 1 and sits above both. */
.cs-trio-cell::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--tex-top), var(--tex-bottom));
  z-index: 0;
}

/* the capture, whole */
.cs-trio-shot {
  position: relative;
  z-index: 1;
}

.cs-trio-shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  /* the same hairline as the twenty-one plates, in box-shadow so it
     follows the radius and costs no layout */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14), 0 18px 40px -18px rgba(0, 0, 0, 0.6);
}

.cs-trio-text {
  position: relative;
  z-index: 2;
  margin-top: 22px;
  color: #fff;
  font-family: var(--font-sans);
}

.cs-trio-label {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.3;
}

.cs-trio-line {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.9);
}

/* the arrival zoom in styles-sidebar.css is for photographs. These are UI
   captures, and a scale settle smears their text while it lands. */
.cs-fx .cs-body .pv-fig.cs-trio img { transform: none; transition: none; }

.cs-trio .pv-fig-cap {
  margin-inline: auto;
  text-align: center;
  max-width: 62ch;
}

@media (max-width: 760px) {
  .cs-trio-row { grid-template-columns: 1fr; gap: 14px; width: min(100%, 420px); }
}

/* ============================================================
   TAGS ON THE TWENTY-ONE PLATES

   Two plates carry a label: the first, where the friend's link opens,
   and the twenty-first, where the card is taken and after which the menu
   finally appears. The band title says it; the tags put it on the
   plates themselves so a reader who skips the words still gets it.
   ============================================================ */

.pv-flow-tag {
  position: absolute;
  left: 50%;
  bottom: 8px;
  transform: translateX(-50%);
  z-index: 2;
  white-space: nowrap;
  padding: 5px 9px 6px;
  border-radius: 999px;
  background: var(--text-primary);
  color: var(--bg-cream);
  font: 600 10px/1 var(--font-sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  pointer-events: none;
  box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.5);
}

.pv-flow-tag.is-card { background: var(--accent); color: #fff; }

.dark-mode .pv-flow-tag { color: var(--bg-dark); }
.dark-mode .pv-flow-tag.is-card { color: #fff; }

@media (max-width: 620px) {
  .pv-flow-tag { font-size: 8px; padding: 4px 7px 5px; letter-spacing: 0.06em; }
}

/* ============================================================
   THE ARROW

   From the foot of the third card, "What the friend gets", a brush-ink
   arrow swoops down the chapter gap and points into the twenty-one
   plates: what the friend gets is that. The artwork is the owner's own
   filled brush shape, three paths, untouched. It is not wiped in by a
   box; each path carries a mask of its own, a white stroke that runs
   along that path's own centreline, so the ink appears where a pen tip
   would lay it down. One mask per path and not one shared between them,
   because the shaft's stroke passes within its own width of the arms at
   the junction: sharing would have let the shaft reveal the head early.

   The three centrelines were derived from the artwork, not drawn by eye:
   each path was rasterised, reduced to its largest ink body, given an
   exact euclidean distance transform, and walked end to end along the
   geodesic through that body, then recentred on the distance ridge,
   smoothed and fitted. The brush is at most 14.9 units wide and no ink
   pixel anywhere, spatter included, sits more than 11.2 units off a
   centreline, so a 26 unit mask stroke covers the artwork completely.

   arrow.js writes --qs, --qa, --qb: how much of the shaft and of each
   arrowhead arm has been inked. It also rides a small round nib along
   each centreline so the ink front is always a rounded pen point rather
   than a straight cut. Charcoal ink, the page's own text colour, so it
   inverts with the theme.
   ============================================================ */
.cs-trio-row { position: relative; }

.cs-trio-arrow {
  --p: 0;
  --qs: 0;
  --qa: 0;
  --qb: 0;
  position: absolute;
  top: 100%;
  left: 48%;
  width: 40%;
  /* the foot lands 24px above plate 01: chapter gap, the caption under
     the row, and the sheet's head, measured */
  height: calc(var(--cs-chapter-gap, 300px) + 314px);
  overflow: visible;
  pointer-events: none;
  color: var(--text-primary);
}

/* The head of the next figure, "Your friend lands here", is sticky and paints
   an opaque band at z-index 3. The arrow runs down through exactly that band
   on its way to plate 01, so without this the ink is cut in two: a stroke that
   stops in mid air and an arrowhead floating below the band. The chapter the
   arrow belongs to is a stacking context of its own at z-index 1; lifting it
   above the band is what lets one unbroken line arrive at the plates. Nothing
   else in the chapter ever reaches this far down the page, so nothing else
   changes hands. */
.cs-body .pv-chapter.pv-chapter:has(.cs-trio-arrow) { z-index: 4; }

/* The figure reveal in styles-sidebar.css settles on inset(-44px 0%): 44px of
   headroom above and below the figure's own box, and nothing more. The arrow
   hangs a whole chapter gap below this figure, so on this one figure the
   settled clip has to open all the way down or the ink is cut off level with
   the caption. Left and right stay flush, which is what makes the reveal read
   as a frame opening. */
.cs-fx .cs-body .pv-fig.cs-trio.is-inview { clip-path: inset(-44px 0% -1200px 0%); }

/* The mask strokes. Butt caps on purpose: the rounded front of the ink is the
   nib circle, and a round cap here would put a dome of half the stroke width
   in front of it and swamp it. No non-scaling-stroke either: the box stretches
   the artwork, and the mask has to stretch with it by exactly the same amount
   or it would stop covering.

   The 1px is what keeps this honest in both engines: pathLength="1" makes the
   whole spine one unit long and a CSS px is one user unit, so multiplying by
   1px says "this many units of the path" in a form neither Chrome nor Safari
   has to guess at. */
.cs-arrow-spine--shaft { stroke-dashoffset: calc((1 - var(--qs)) * 1px); }
.cs-arrow-spine--arm-a { stroke-dashoffset: calc((1 - var(--qa)) * 1px); }
.cs-arrow-spine--arm-b { stroke-dashoffset: calc((1 - var(--qb)) * 1px); }

@media (prefers-reduced-motion: reduce) {
  /* no pen, just the finished arrow */
  .cs-arrow-spine { stroke-dashoffset: 0; }
}

@media (max-width: 760px) {
  /* THE ARROW IS BACK ON A PHONE. This rule is no longer what happens —
     mobile.css, which loads after this file, turns it back on with a box
     of its own and arrow.js measures the drop. What is left here is the
     FALLBACK, and it is deliberately left rather than deleted: without
     mobile.css this arrow would take the desktop box on a phone, a 40%
     wide column against a 444px drop, and preserveAspectRatio="none"
     would render the brushstroke as a near-vertical scribble. Hidden is
     better than that. See the arrow block at the foot of mobile.css. */
  .cs-trio-arrow { display: none; }
}

/* ============================================================
   THE LINE UNDER THE ARROW

   Replaces the sheet's sticky head. One sentence, set where the arrow's
   tip lands, that arrives only once the arrow has finished drawing:
   arrow.js adds .is-in when the pen reaches the end. Until then it is
   held a few pixels low and clear, and it rises into place. Reduced
   motion shows it at once.
   ============================================================ */
.cs-body .pv-fig.cs-sheet .cs-sheet-lead {
  margin: 0 auto;
  /* the top padding is the room the arrow needs: its foot lands about 50px
     above the first line at 1600px wide */
  padding: 194px var(--wrap-gutter, 24px) clamp(56px, 6vw, 88px);
  max-width: 34ch;
  text-align: center;
  font-family: var(--font-sans);
  font-size: clamp(17px, 1.4vw, 21px);
  line-height: 1.5;
  color: var(--text-body);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.7s ease, transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}

.cs-body .pv-fig.cs-sheet .cs-sheet-lead.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .cs-body .pv-fig.cs-sheet .cs-sheet-lead { opacity: 1; transform: none; transition: none; }
}

/* the sheet's caption lost the head it used to sit under; keep its measure */


/* ============================================================
   THE TOUCHPOINTS

   Three stages in the sender chapter: the web page, the mobile menu, and
   the app. Each holds two layers, the screen and the dialog that opens
   over it, with the scrim between them drawn in CSS rather than baked
   into the artwork. That is what makes the moment playable: the screen
   sits bright, the scrim drops, the dialog arrives, it holds, it leaves,
   and the screen is bright again.

   Each overlay keeps the exact share of its screen's width that it has in
   the source files, 68% for the web dialog and 69% for the mobile one, so
   the composite matches what shipped. The drawer spans the screen because
   a bottom sheet does; .tp clips it, so it can start below the edge.

   LAYOUT. The chapter leaves the article's two text columns and becomes a
   split: the captures stack on the left, the writing holds on the right.
   .cs-body .pv-chapter sets `columns: 2 380px`, and a multi-column
   container fragments everything inside it, so a grid child would be cut
   across the column boundary. `columns: auto` on this one chapter turns
   the fragmentation off; nothing else about the chapter changes.

   The right column is sticky, so the prose travels down beside the
   captures and releases at the foot of the last one. That works without
   JavaScript, but only with `align-self: start`: a grid item defaults to
   stretching to its row, and a sticky box that is already as tall as its
   containing block has nowhere to stick. It sticks below the nav rather
   than under it, using the same --pv-nav offset the case study's own side
   panel uses.

   Inside the media column the web page takes a row of its own and the two
   phones share the row beneath it, sized to each other by ASPECT RATIO
   rather than pixel width: the two screens are 700x1471 and 700x1522, so
   equal columns would leave one taller than the other. 476 : 460 renders
   them to the same height with no crop and no fixed height.
   ============================================================ */

.cs-body .pv-chapter.is-split {
  /* out of the article's two-column text flow; see the note above */
  columns: auto;
  column-count: auto;
  /* The four-screen band above closes with a --cs-chapter-gap bottom margin,
     around 300px, so the chapter's own 72px of lead-in was stacking on top of
     a gap that was already generous. */
  padding-top: clamp(8px, 1vw, 16px);
}

.cs-split {
  /* The same 1240px column the four-screen band above uses, so the two
     sections line up down the page. That band is full-bleed and insets its
     content with `max(gutter, (100% - 1240px) / 2)`; this one sits in the
     article's narrower measure, so it widens out to meet it with symmetric
     negative margins instead. Percentage margins resolve against the
     containing block, so setting the width and splitting the remainder
     centres it whatever the article measure happens to be. */
  --split-w: min(1240px, calc(100vw - 2 * var(--wrap-gutter, 24px)));
  width: var(--split-w);
  margin-inline: calc((100% - var(--split-w)) / 2);
  display: grid;
  grid-template-columns: minmax(0, 1.34fr) minmax(0, 1fr);
  gap: clamp(32px, 4.5vw, 72px);
  align-items: start;
}

.cs-split-media {
  display: grid;
  gap: clamp(18px, 2.4vw, 34px);
  min-width: 0;
}

.cs-split-text {
  position: sticky;
  /* a grid item stretches by default, and a sticky box the height of its
     containing block cannot travel */
  align-self: start;
  top: var(--pv-nav, 101px);
  min-width: 0;
}

.cs-split-text h2 { margin-top: 0; }

/* the caption belongs to the captures, so it keeps caption scale and colour
   even though it now sits in the text column */
.cs-split-cap {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-muted);
  max-width: 46ch;
}

.tp-pair {
  display: grid;
  /* aspect ratios of the two phone screens, x1000 */
  grid-template-columns: 476fr 460fr;
  gap: clamp(14px, 2vw, 24px);
  align-items: start;
}

.tp {
  position: relative;
  min-width: 0;
  overflow: hidden;
  border-radius: clamp(6px, 0.9vw, 14px);
  /* the twenty-one plates' own edge: a product UI on a cream page has no
     boundary of its own */
  box-shadow: 0 0 0 1px var(--rule-soft);
}

.tp-bg {
  display: block;
  width: 100%;
  height: auto;
}

.tp-scrim {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0.42;
  pointer-events: none;
}

.tp-over {
  position: absolute;
  display: block;
  height: auto;
  pointer-events: none;
}

/* the two centred dialogs, each at its source share of the screen width */
.tp--web .tp-over  { width: 68.3%; left: 15.85%; top: 50%; transform: translateY(-50%); }
.tp--menu .tp-over { width: 69.1%; left: 15.45%; top: 50%; transform: translateY(-50%); }

/* the drawer meets the bottom edge and spans the screen */
.tp--app .tp-over { width: 100%; left: 0; bottom: 0; }

/* the arrival zoom is for photographs; on a UI capture it smears the type */
.cs-fx .cs-body .pv-slots.is-touch img { transition: none; }

/* ---- the loop ---- */

/* where JS runs, a stage that has not been seen sits bright and empty */
.cs-fx .tp-scrim { opacity: 0; }
.cs-fx .tp--web .tp-over  { opacity: 0; transform: translateY(-50%) scale(0.965); }
.cs-fx .tp--menu .tp-over { opacity: 0; transform: translateY(-50%) scale(0.965); }
.cs-fx .tp--app .tp-over  { transform: translateY(100%); }

.cs-fx .pv-chapter.is-inview .tp-scrim {
  animation: tp-scrim 8s linear infinite;
}
.cs-fx .pv-chapter.is-inview .tp--web .tp-over,
.cs-fx .pv-chapter.is-inview .tp--menu .tp-over {
  animation: tp-dialog 8s linear infinite;
}
.cs-fx .pv-chapter.is-inview .tp--app .tp-over {
  animation: tp-drawer 8s linear infinite;
}

/* offset so the three do not open together; the app leads because the
   drawer travels furthest and reads slowest */
.cs-fx .pv-chapter.is-inview .tp--menu .tp-scrim,
.cs-fx .pv-chapter.is-inview .tp--menu .tp-over { animation-delay: 0.45s; }
.cs-fx .pv-chapter.is-inview .tp--app .tp-scrim,
.cs-fx .pv-chapter.is-inview .tp--app .tp-over { animation-delay: 0.9s; }

/* 210ms down, a long hold, 320ms back up, then two seconds bright */
@keyframes tp-scrim {
  0%     { opacity: 0; animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1); }
  2.6%   { opacity: 0.42; }
  71.5%  { opacity: 0.42; animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }
  75.5%  { opacity: 0; }
  100%   { opacity: 0; }
}

/* rise 2.2% of the stage and grow from 0.965: a dialog scales from where
   it was rather than sliding in from off-stage, which is what anchors it
   to the screen it belongs to */
@keyframes tp-dialog {
  0%     { opacity: 0; transform: translateY(calc(-50% + 2.2%)) scale(0.965);
           animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  2%     { opacity: 0; transform: translateY(calc(-50% + 2.2%)) scale(0.965);
           animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  9.75%  { opacity: 1; transform: translateY(-50%) scale(1); }
  70.5%  { opacity: 1; transform: translateY(-50%) scale(1);
           animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }
  74.5%  { opacity: 0; transform: translateY(calc(-50% + 1.4%)) scale(0.975); }
  100%   { opacity: 0; transform: translateY(calc(-50% + 2.2%)) scale(0.965); }
}

/* the sheet: up from below the edge, 1.6% past its rest, then back. No
   fade, because a sheet is a solid object arriving, not an image appearing */
@keyframes tp-drawer {
  0%     { transform: translateY(100%); animation-timing-function: cubic-bezier(0.22, 0.9, 0.3, 1); }
  2%     { transform: translateY(100%); animation-timing-function: cubic-bezier(0.22, 0.9, 0.3, 1); }
  8.5%   { transform: translateY(-1.6%); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1); }
  11%    { transform: translateY(0); }
  70.5%  { transform: translateY(0); animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }
  74.5%  { transform: translateY(100%); }
  100%   { transform: translateY(100%); }
}

@media (prefers-reduced-motion: reduce) {
  /* the finished composite, still: scrim down, dialog in place */
  .cs-fx .tp-scrim { opacity: 0.42; animation: none; }
  .cs-fx .tp--web .tp-over,
  .cs-fx .tp--menu .tp-over { opacity: 1; transform: translateY(-50%); animation: none; }
  .cs-fx .tp--app .tp-over { transform: none; animation: none; }
}

@media (max-width: 900px) {
  /* one column: the writing goes back above the captures and stops sticking,
     because there is no longer a second column for it to hold beside */
  .cs-split { grid-template-columns: minmax(0, 1fr); }
  .cs-split-text { position: static; order: -1; }
}

@media (max-width: 620px) {
  /* one phone per row, held to the width a phone capture can carry */
  .tp-pair { grid-template-columns: 1fr; justify-items: center; }
  .tp-pair .tp { width: min(100%, 300px); }
}

/* ============================================================
   BEFORE, SAID OUT LOUD

   These three captures are the experience as it shipped, not the work.
   The caption said "as it was", which is only clear to someone reading
   captions; a reader scanning the page saw three screens of product and
   no reason to think they were the old ones. The chip sits on the card
   itself, so a card that is screenshotted or skimmed still carries the
   claim. Light on the dark texture, at the same micro-caps step the
   plates' own tags use.
   ============================================================ */

.cs-trio-cell { position: relative; }

.cs-trio-tag {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 3;
  padding: 5px 9px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  color: #1c1714;
  font: 600 10px/1 var(--font-sans);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  pointer-events: none;
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.6);
}

.cs-cap-lead { color: var(--text-primary); font-weight: 600; }

@media (max-width: 620px) {
  .cs-trio-tag { top: 10px; left: 10px; font-size: 9px; padding: 4px 7px 5px; }
}

/* The captures needed more air under the prose than the shared .pv-fig step
   gives them. That step is 52px, which suits a figure that illustrates the
   sentence right above it; these three are a change of subject, from the
   argument to the evidence for it, and were reading as a continuation of the
   last paragraph. Measured at 1500px the gap goes from 85px to about 133.
   (0,3,0) so it beats `.cs-body .pv-fig` without depending on file order. */
.cs-body .pv-fig.cs-trio { margin-top: clamp(72px, 6vw, 104px); }

/* ============================================================
   THE SENDER'S SIDE, AS IT SHIPPED

   Chapter three says "two fixes, one for the friend who lands, one for
   the customer who sends" and then shows one of them: the friend's page
   runs live above this. This figure is the other half.

   IT IS BUILT OUT OF THE TRIO'S PARTS ON PURPOSE. Every cell here is a
   .cs-trio-cell — same food texture, same CSS darkening, same hairline on
   the capture, same tag, label and line. The two cards are the after of
   the trio's first two: the sender's page, and the sheet that opens from
   it. Rebuilding the construction rather than inventing a second one is
   what lets a reader recognise the pairing without a sentence telling
   them. Only the chip's word changes, Before to Shipped, and the two
   unused textures from that set are what these sit on.

   TWO CARDS AND NOT ONE COMPOSITE. The screen is 860x2055 and the drawer
   covers 59% of it, so a single composed card would hide the reward
   ladder and the activity list — which is most of what changed about the
   page. Showing the screen whole and then showing the sheet over it costs
   one card and keeps both.

   THE ROW IS NARROWER THAN THE CARD IT SITS ON. .cs-panel-card resolves
   to about 988px of content at 1600; two cells across that would be 482px
   each and a 0.4185 aspect capture would stand 956px tall. 820px holds
   each cell at about 398, which is the width the trio's three cells take
   at the same viewport, so the two figures print at the same scale.

   THE SECOND CARD IS THREE LAYERS. Screen, scrim, drawer, exactly the
   composition .tp--app makes in the next chapter, and for the same
   reasons: the drawer's top corners are transparent, so something has to
   be behind them or they read as a white box, and the scrim's weight
   stays a number in this file rather than something baked into a PNG.
   0.42 is the touchpoints' own scrim, unchanged, so the two chapters
   agree about how dark an open sheet makes the screen under it.

   The hairline and the radius move from the img to the stage, because the
   stage is now the object with an edge; the drawer has to be clipped by
   that radius rather than carry one of its own.

   STATIC. #pv-ch4's touchpoints run an 8s loop and #pv-ch6 embeds a
   prototype that runs its own demo. A third moving thing between them
   would be too much, so this is composed open and left there.
   ============================================================ */

/* .cs-body .pv-fig bleeds past the measure; this one lives inside
   .cs-panel-card and is measured by the card, the same pin .cs-live takes.
   (0,3,0), so it does not depend on file order. */
.cs-body .pv-fig.cs-after {
  width: auto;
  margin-inline: 0;
  margin-top: clamp(48px, 4vw, 72px);
}

.cs-after-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(12px, 1.8vw, 24px);
  align-items: stretch;
  width: min(100%, 820px);
  margin-inline: auto;
}

/* the layered card: the edge belongs to the stage, not to the screen */
.cs-after-stage {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14), 0 18px 40px -18px rgba(0, 0, 0, 0.6);
}

.cs-trio-shot.cs-after-stage img { border-radius: 0; box-shadow: none; }

.cs-after-scrim {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0.42;
  pointer-events: none;
}

/* a bottom sheet spans its screen and meets the bottom edge */
.cs-after-drawer {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: auto;
  pointer-events: none;
}

/* the arrival zoom is for photographs; on a UI capture it smears the type */
.cs-fx .cs-body .pv-fig.cs-after img { transform: none; transition: none; }

.cs-after .pv-fig-cap {
  margin-inline: auto;
  text-align: center;
  max-width: 62ch;
}

/* THE PHONE. 760 and 420 are not new numbers: they are .cs-trio-row's own,
   and they are taken rather than tuned because the whole argument for this
   figure is that it is the trio's construction a second time. Two up at 620
   would hold each capture to a 210px shot against the trio's 344px directly
   above it, and the after would print at three fifths of the before it is
   answering. Measured at 620 the stack costs about 1800px of figure against
   600px, which is the same bargain chapter two already makes at this width
   with three cards rather than two. Centred, because unlike the trio this
   row sits inside a card and has ground on both sides of it. */
@media (max-width: 760px) {
  .cs-after-row {
    grid-template-columns: 1fr;
    gap: 14px;
    width: min(100%, 420px);
  }
}


/* ============================================================
   "VIEW THE FULL PAGE"

   The frame shows the hero and stops; the rest of the friend page is a
   click away, and that click has to be OBVIOUS. It was a label inside
   the drawn address bar first, which was invisible -- a reader has no
   reason to believe a screenshot's chrome is operable.

   ABOVE THE FRAME, POINTING DOWN, so it is read before the thing it
   refers to rather than after it.

   WHITE, NOT CORAL. Coral is the page's call-to-action colour and it is
   already spent on the statement band's one button; a second coral
   object a screen away reads as the same control twice. White needs the
   hairline, though: pure white on the #f7f4f2 page ground is a few
   points apart and would float without an edge.

   NO FLOAT ANIMATION. It existed to catch the eye. Above the frame and
   in white it is already the first thing in the figure, and this page
   carries an inking arrow, a touchpoint loop, a demoing prototype and
   two WebGL scenes -- it does not need a sixth thing moving.
   ============================================================ */
.cs-live-tip {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  width: max-content;
  max-width: calc(100% - 32px);
  /* pushed down onto the frame so the pointer lands on it */
  margin: 0 auto -20px;
  padding: 14px 22px;
  border-radius: 14px;
  background: #fff;
  color: #14140f;
  font: 600 clamp(15px, 1.15vw, 17px)/1.15 var(--font-sans);
  text-decoration: none;
  box-shadow:
    inset 0 0 0 1px var(--rule-soft),
    0 10px 26px -14px rgba(20, 14, 8, 0.5);
  /* STATIC, ENTIRELY. No hover transition either -- the request was to remove
     the motion, and a 160ms shadow change is motion. */
  transition: none;
}

/* THE PARALLAX TRAP. styles-sidebar's figure reveal drifts a figure's FIRST
   CHILD by up to 20px on scroll (`.cs-fx .cs-body .pv-fig > :first-child`,
   0,4,0). When the tip moved above the frame it became that first child and
   inherited the drift -- a label that slides against the thing it points at.
   Pinned here at 0,5,0. The frame, now second, no longer drifts either, which
   is the right outcome for a figure a reader is meant to hold still and read. */
.cs-fx .cs-body .pv-fig.cs-live > .cs-live-tip { transform: none; }

/* the pointer, aimed down at the frame below it */
.cs-live-tip::before {
  content: '';
  position: absolute;
  bottom: -7px;
  left: 50%;
  width: 16px;
  height: 16px;
  transform: translateX(-50%) rotate(45deg);
  border-radius: 3px;
  background: inherit;
}

.cs-live-tip span,
.cs-live-tip svg { position: relative; }

.cs-live-tip svg {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cs-live-tip:hover,
.cs-live-tip:focus-visible {
  box-shadow:
    inset 0 0 0 1px var(--rule),
    0 12px 30px -14px rgba(20, 14, 8, 0.6);
}

@media (max-width: 620px) {
  .cs-live-tip { padding: 12px 18px; border-radius: 12px; gap: 8px; }
}

/* ============================================================
   THE LIVE FRAME, WIDESCREEN

   Requested 16:9 with rounded corners. Done as a /raf-only override
   rather than an edit to styles-sidebar.css, which three other case
   studies load — .cs-live is used by this page alone, but the file is
   shared and there is no reason to make them re-download it.

   THE CROP PROBLEM, AND WHY THIS DOES NOT HAVE IT. styles-sidebar's own
   comment is right: the embedded hero is a fixed 800px tall at ANY
   width, so simply imposing a ratio on the frame cuts the bottom off —
   it measured 151px of the CTA and trust line gone. So the frame gets
   the ratio and the PAGE INSIDE gets scaled to fit it, the same trick
   the phone embed in #pv-ch6 uses: render the iframe at a width large
   enough that 800px of page is 9/16 of it, then scale it back down.

   1000 / (1000 * 9/16) = 1.778, and the hero is 800 tall, so the frame
   must render the page at 800 * 16/9 = 1422px wide and scale by
   1000/1422 = 0.703. Both numbers derive from --cs-live-h; change the
   hero's height and they follow.

   The whole hero is visible, just smaller — which is also more honest
   to what a widescreen browser window actually shows.
   ============================================================ */
.cs-live-frame {
  /* THE FAKE BROWSER BAR IS GONE, so no height is reserved for it: the frame
     is the page area and nothing else. Removing it also removed the secondary
     way into the full page, which is fine -- the coral callout was always the
     one a reader would actually find. */
  --cs-live-w: min(100%, 1000px);
  --cs-live-page-h: calc((1000px - 2px) * 9 / 16);
  height: var(--cs-live-page-h);
  border-radius: clamp(10px, 1vw, 16px);
}

.cs-live-frame iframe {
  /* rendered wide, then scaled back: nothing is cropped */
  width: calc(var(--cs-live-h) * 16 / 9);
  height: var(--cs-live-h);
  transform: scale(calc((1000 - 2) / (800 * 16 / 9)));
  transform-origin: 0 0;
}

@media (max-width: 1040px) {
  /* BELOW THE CAP, THE RATIO IS DROPPED ON PURPOSE.

     The scale factor has to be unitless, and it wants to be
     frameWidth / 1422px -- a length over a length, which calc() cannot
     reduce to a number. My first attempt wrote exactly that and the
     declaration was silently discarded: measured scale(1), ratio 1.53
     at 390px, and the hero cropped, which is the fault this override
     exists to avoid.

     Rather than approximate it per breakpoint, the frame goes back to
     what styles-sidebar.css already does well: as tall as the hero.
     A 16:9 letterbox at 390px is a 192px window onto an 800px page --
     legible to nobody -- so the widescreen framing is a desktop idea
     and it stops where it stops being one. */
  .cs-live-frame {
    --cs-live-page-h: var(--cs-live-h);
    height: var(--cs-live-h);
  }
  .cs-live-frame iframe { width: 100%; transform: none; }
}


/* ============================================================
   CH3'S CARD, AT THE FOUR-STEP BAND'S WIDTH

   The panel stays full-bleed -- it is one of the page's two full-width
   sheets and that is the point of it. What was mismatched is the CARD
   inside it: at --panel-w it measured 1100 against the band's visible
   1202, about 100px narrower than the thing directly above it, which
   reads as a mistake rather than a hierarchy.

   The card now takes the band's OUTER box, --band-col plus --band-pad
   either side, so the two are one measurement rather than two that
   nearly agree. (An earlier pass brought the panel in instead; that was
   the wrong half of the pair and is reverted.)
   ============================================================ */
.cs-body > #pv-ch3.cs-plate .cs-panel-card {
  /* vw, NOT %. A percentage here resolves against ch3's own content box,
     not the viewport, and ch3 is full-bleed with its own padding -- the
     first attempt measured 900 instead of 1202 for exactly that reason.
     --band-col already resolves against 100vw, so the cap does too. */
  --card-w: min(
    calc(var(--band-col) + 2 * var(--band-pad)),
    calc(100vw - 2 * var(--wrap-gutter, 24px))   /* ch3 is full-bleed: no plate gutter to subtract */
  );
  width: var(--card-w);
  /* auto does NOT centre here -- ch3 is a multi-column container and the card
     lands in the flow, not in a centred block box. Splitting the remainder is
     what .cs-panel-card already did to line up with the band's row, and it is
     what lines the card up with the band's outer box now. */
  margin-inline: calc((100% - var(--card-w)) / 2);
}

/* ============================================================
   THE STATEMENT'S SCENE

   A second Unicorn Studio scene, behind the 86% / 9% statement. Built
   the same way as the ch1 band and for the same reason: the flat
   #120a07 ground stays underneath and this sits on top of it, so no
   WebGL, a blocked fetch, reduced motion, or a reader on a browser that
   cannot run it all leave the statement rendering exactly as designed.
   scene.js adds .has-scene only once a <canvas> is really there.

   THE TYPE IS THE POINT, SO IT KEEPS ITS GROUND. The scene's palette is
   a sky -- pale blue to navy -- and the statement's ink is cream,
   measured against near-black. Dropped straight behind the words that
   contrast is gone. So the scene is darkened and desaturated, and a
   vertical scrim sits between it and the text: the movement reads, the
   words stay on something close to the ground they were measured on.
   Tune --scene-veil rather than the scene file.
   ============================================================ */
.cs-statement { position: relative; isolation: isolate; }

.cs-scene-quote {
  --scene-veil: 0.68;
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 700ms ease;
  /* WARMED TO THE PAGE. The scene ships as a sky and read cool against a
     warm-black page: measured R-B of -5 where the band's own #120a07 is +11.
     Sepia rotates it back rather than the scrim fighting it -- five
     combinations were sampled over 20 points of ground between the lines,
     and this one lands on +11 exactly, with cream ink still at 10.6:1. */
  filter: sepia(0.65) saturate(0.8) brightness(0.58);
}

.cs-statement.has-scene .cs-scene-quote { opacity: 1; }

.cs-scene-quote canvas { display: block; width: 100%; height: 100%; }

/* the scrim, between the scene and the words */
.cs-scene-quote::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(18, 10, 7, calc(var(--scene-veil) - 0.06)) 0%,
    rgba(18, 10, 7, var(--scene-veil)) 46%,
    rgba(18, 10, 7, calc(var(--scene-veil) + 0.08)) 100%);
}

/* the words, above all of it */
.cs-statement .cs-statement-pin { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .cs-scene-quote { display: none; }
}
