/* =====================================================================
   CASES DRUM — self-contained 3-file component (css/js/demo html).
   Reused pixel-verified values (source of truth = the flat 2D carousel
   this replaces, itself pulled from Figma): card ratio 1194:790, active
   fill #303637, inactive fill #101415, label/heading/CTA positions in a
   1920-baseline stage — same "stage scale" convention the rest of the
   site's absolutely-positioned sections already use, reproduced here
   verbatim so this component doesn't depend on the site's own style.css.

   COMPOSITION (structural rewrite): the section is ONE explicit top-to-
   bottom flow — label → heading → gap → drum band → gap → CTA → gap —
   and every one of those six vertical measurements derives from the
   SAME basis: calc(px/1920*100vw), the 1920-baseline convention
   label/heading/CTA already used. Earlier versions of this file mixed
   that basis with vh (the drum band's height, the section's own
   min-height, the CTA's bottom gap) — independent proportions of the
   viewport's HEIGHT sitting next to proportions of its WIDTH, which
   only ever lined up at whichever aspect ratio they happened to get
   tuned against, and drifted at every other one (that's what produced
   the heading/card collision two passes ago, and would have kept
   producing a new drift with every future viewport this got checked
   against). Doing the whole flow in ONE basis makes every relationship
   — heading clears the band, band clears the CTA, CTA's own gap is
   smaller above than below — structural: true by construction at every
   width, not by coincidence at the ones actually tested. See each
   rule below for its own baseline number; they're also listed together
   here for the whole flow at 1920: label top 71 → heading top 106,
   bottom 239.28 (106 + 66.641×2, its own two-line height) → +48 gap →
   drum band top 287.28, height 600 → +24 gap → CTA top 911.28, height
   70 → +96 gap → section ends at 1077.28.
   The section itself has NO explicit height and no vh-based min-height
   floor either (an earlier attempt at "grow if it needs to" used
   min-height:100vh, which — measured — doesn't shrink BELOW 100vh when
   the content needs less, it just leaves the difference as unexplained
   dead space before the real padding-bottom; confirmed by testing an
   artificially tall viewport and finding a 425px gap that had nothing
   to do with any deliberate value). With no height forced on it, the
   flex column's own natural content height (the sum above) IS the
   section's height, on every viewport — taller than one screen is fine,
   per instruction, and now simply follows from what the six zones add
   up to rather than being fought against a floor.
   Label/heading/CTA still scale continuously with viewport width via
   calc(px/1920*100vw) rather than a fixed breakpoint jump, so 1920 and
   ~1470 read as the same composition just proportionally smaller — no
   JS involved, and nothing here touches the canvas sizing math
   cases-drum.js itself owns (it already reads .cases-drum__canvas-wrap's
   actual clientWidth/clientHeight on every resize, so any box size CSS
   gives it just works — see updateCamera's own comment for how its fill
   ratio was recalibrated, pixel-measured against this new box, to stop
   clipping the card's top edge, which is a separate, JS-side half of
   this same fix). box-sizing is set explicitly (border-box) rather than
   relying on the host page's own reset — the standalone demo html
   doesn't load the site's style.css (which does set border-box
   globally) and would size padding-bottom wrong without it stated here
   too. */

.cases-drum {
  position: relative;
  width: 100%;
  box-sizing: border-box;
  background: #060c0c;
  /* overflow:hidden genuinely clips something now, not just a defensive
     default: .cases-drum__drum-wrap's negative margin-bottom (see that
     rule's own comment) means this section's own auto-height, driven by
     flex flow, ends up shorter than drum-wrap's full rendered box — by
     design, the same way the negative margin is by design — pixel-
     confirmed at 1920px: drum-wrap's own box bottom sits ~550px below
     where the section itself ends. That clipped strip is PURELY the
     canvas's own empty clear-colour background (nothing readable or
     clickable ever renders there — the card and arrows both sit well
     above it), and that clear colour (renderer.setClearColor, cases-
     drum.js) is the exact same #060c0c as this rule's own background —
     so the clip line is genuinely invisible, not just unlikely to be
     noticed. This is real coupling, not a coincidence to leave
     undocumented: if either colour ever changes without the other, a
     hard seam would appear at the section's own bottom edge. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* The section's bottom-of-flow gap — CTA to wherever the next section
     starts. Was deliberately larger than the gap above the CTA (so the
     CTA read as grouped with this block rather than the next one) —
     changed THIS pass to instead CENTRE the CTA in the space between
     the card band and the next section, so it reads as breathing in its
     own space rather than attached to either neighbour. Same 68px value
     as .cases-drum__cta's own margin-top (see that rule's comment) by
     construction: the two used to be 40/96 (card-bottom→CTA / CTA→next-
     section); their sum (136) is unchanged, just re-split down the
     middle (68/68) — same total section height as before, only the
     split moved. Same 1920 basis, and same clamp shape as the CTA's own
     margin-top (not independently chosen) — mismatched clamp curves
     would drift the two gaps apart from each other at widths other than
     1920, defeating the "approximately equal" the centring is for. */
  padding-bottom: clamp(46px, calc(68 / 1920 * 100vw), 92px);
  /* z-index:2 when dropped into a page with a global body::before grid
     overlay (z-index:1, fixed/absolute) — otherwise that grid paints
     OVER this section's own flat background instead of being covered by
     it, since a plain position:relative box with no z-index doesn't
     otherwise outrank it. Harmless on its own (the standalone demo has
     no such overlay to begin with). Mockup calls for flat black, no
     grid, explicitly. */
  z-index: 2;
  /* scroll-margin-top — landing on #cases (nav link, "Дивитись кейси"
     CTA, or a bare #cases URL) with NO offset here puts this section's
     top flush with the true viewport top, y=0 — but .navbar is
     position:fixed (style.css), floating OVER the page rather than
     occupying space in it, so nothing about the scroll itself accounts
     for its footprint. Pixel-confirmed directly: scrolled so the
     heading sits at y=0, the navbar (top:23px, height:70px desktop →
     occupies y=23–93) visibly overlaps it. 110px clears the desktop
     navbar's own bottom edge (93px) with a small buffer, same spirit as
     the CTA/heading gaps elsewhere in this file — a flush 93px would
     technically clear it but read as touching. This is a genuinely
     site-wide gap (ANY section landed on via anchor has the same
     unaccounted-for navbar footprint, not something specific to how
     this component is built) — scoped to .cases-drum only, per
     instruction, not fixed site-wide in this pass. */
  scroll-margin-top: 110px;
}

/* Label + heading overlay — absolutely positioned over the top of
   .cases-drum__drum-wrap (NOT a sibling block pushing it down), so the
   heading and the drum occupy the same space per the mockup. Pointer
   events stay off the overlay itself so it never blocks drag/swipe on
   the drum beneath it — nothing inside it needs clicks.

   STACKING — one deliberate scale across .cases-drum's direct children,
   not four numbers arrived at independently. .cases-drum__stage,
   .cases-drum__drum-wrap and .cases-drum__cta are all direct children
   of the SAME element (.cases-drum, itself already position:relative —
   the one stacking context this whole composition lives in), so
   comparing their z-index values directly against each other is exact,
   not a coincidence of unrelated nested contexts: 1 = the drum (base
   visual layer), 2 = the CTA (sits above the drum's own canvas — see
   that rule's comment), 3 = label/heading (always above the drum, full
   stop — text must stay readable over any card colour, at any drum
   position, not just where it happens not to overlap). A previous pass
   put the drum ABOVE the heading (z-index:6, heading landed on 4) so
   the card could pass in front of it where they overlapped — reverted:
   the heading needs to read on top everywhere, not just outside the
   overlap, so the drum no longer competes for the top spot at all. */
.cases-drum__stage {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
  pointer-events: none;
}

.cases-drum__label {
  position: absolute;
  left: calc(241 / 1920 * 100vw);
  top: calc(71 / 1920 * 100vw);
  margin: 0;
  font-family: var(--font-display, sans-serif);
  font-weight: 700;
  font-size: clamp(11px, calc(13.703 / 1920 * 100vw), 13.703px);
  line-height: 1.03;
  color: #8d8d8d;
  text-transform: uppercase;
}

.cases-drum__heading {
  position: absolute;
  left: calc(242 / 1920 * 100vw);
  top: calc(106 / 1920 * 100vw);
  width: clamp(220px, calc(611 / 1920 * 100vw), 611px);
  margin: 0;
  font-family: var(--font-display, sans-serif);
  font-weight: 700;
  font-size: clamp(30px, calc(66.641 / 1920 * 100vw), 66.641px);
  line-height: 1;
  color: #fff;
  text-transform: uppercase;
  z-index: 2;
}

/* Drum band — margin-top is the heading-clearance zone: pinned to the
   heading's own bottom edge (239.28px, see the file-level comment) plus
   a flat 48px gap, on the exact same calc(px/1920*100vw) basis the
   heading itself is positioned on. The two literally cannot drift apart
   at any width now — this is the structural fix for the heading/card
   collision two passes ago, which came from this value being vh-based
   (a share of the viewport's HEIGHT) while the heading it needed to
   clear was width-based, so they only lined up at whichever aspect
   ratio got tested.
   height (950px at 1920, same basis) replaces an earlier vh-based
   "55% of viewport height" version — this is now a real, fixed, on-
   brand band size at the 1920 baseline that scales down proportionally
   with the rest of the composition, rather than an independent share of
   whatever the viewport happens to be tall enough to give it. The
   centre card's own fill of this box is updateCamera's job (see
   centerRatioMaxHeightDesktop) and was pixel-recalibrated against this
   exact box to stop clipping — see that comment for the measurement.

   950 (was 600) is this component's actual SCALE lever, not a framing
   one — raised so the centre card hits the mockup's measured ~63–66%-
   of-viewport-width target (this box's height is what desktop's card
   width derives from — see updateCamera's fovYDeg comment for the
   exact relationship) rather than the ~41% it measured at 600. Because
   this height is already expressed as a fraction of viewport WIDTH
   (not vh), that width-fraction target holds at any desktop width
   without further tuning — confirmed: centre card measured 64.5% of
   viewport width at BOTH 1920px and 1470px, not just the width this
   was tuned against. The height clamp's ceiling was raised to 1200px
   specifically so it can't silently re-shrink the card on very wide
   screens (it was 700px, which would have started clamping this exact
   950px value on anything narrower than the full 1920 baseline). Pixel-
   sampled margin above/below the centre card at this height: 44px/
   60px at 1920px, 34px/46px at 1470px (both comfortably non-clipping —
   confirmed nothing needed relaxing to get here: the width target and
   the no-clipping requirement were both satisfiable with the same
   value, not a trade-off). The section is measurably taller now as a
   direct, intended result (1427px at 1920px, was 1077px) — allowed
   explicitly: this box no longer has to fit inside one screen, so nothing
   here shrinks the card back down to avoid that. */
.cases-drum__drum-wrap {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  height: clamp(350px, calc(1450 / 1920 * 100vw), 1700px);
  margin-top: clamp(46px, calc(90 / 1920 * 100vw), 100px);
  /* The base visual layer — see .cases-drum__stage's own "STACKING"
     comment for the full ordering across all three direct children.
     Below the heading (z-index:3) unconditionally now: it needs to read
     on top of the drum everywhere, not just outside wherever the card's
     own bounding box happens to be. margin-top above is still what
     lets the (now taller — see .cases-drum__canvas-wrap's comment)
     card's own content visually reach up into the heading's screen
     position; only the stacking that decides who wins THERE changed. */
  z-index: 1;
  /* This box is now much taller (1450px) than the card actually reaches
     (card's own measured bottom edge: 710px, 1920 basis) — that extra
     height is real and needed (see .cases-drum__canvas-wrap's comment:
     shrinking it back down would reintroduce the clipping it was raised
     to fix), but left alone it would push .cases-drum__cta, which
     follows this box in normal flex flow, ~740px below where the card
     actually ends — exactly the "CTA reads detached" symptom. This
     negative margin pulls the FLEX FLOW's notion of "after this box"
     back up to the card's own bottom edge, without shrinking the box's
     own rendered height at all (a flex item's margin never affects its
     own box, only where the NEXT item starts) — so .cases-drum__cta's
     own margin-top (see that rule) becomes the real, sole, deliberate
     gap between the card and the button, the same way it already reads
     as one gap value everywhere else in this file. */
  margin-bottom: clamp(-1000px, calc(-740 / 1920 * 100vw), -270px);
}

/* CTA — bottom-centred flex item. margin-top (68px at 1920) now CENTRES
   the button in the gap between the card band and the next section,
   rather than sitting closer to one side — was 40px (deliberately
   smaller than .cases-drum's own padding-bottom, 96px, so the CTA read
   as grouped with this block). Changed together, as one move, not
   independently: 40+96=136 unchanged, re-split 68/68 down the middle —
   same total section height as before, the CTA just sits centred in it
   now instead of hugging the card. Same clamp shape as .cases-drum's
   own padding-bottom (not a coincidence — see that rule's comment for
   why matching curves matter here) so the two stay equal at every
   tested width, not just 1920: pixel-confirmed 68.0px/68.0px at 1920,
   51.8px/51.8px at 1470.
   Together with .cases-drum__drum-wrap's negative margin-bottom (see
   that comment), this 68px is the actual, only gap between the card's
   own bottom edge and the CTA — not a gap against the much taller box
   behind it.
   z-index:2 — see .cases-drum__stage's "STACKING" comment for the full
   scheme across all three of .cases-drum's direct children. Needs to
   be above the drum's own 1: drum-wrap's canvas fills its FULL box,
   including the space below the card that this button's negative-
   margin position visually sits inside, with an opaque clear colour
   (not the section's background showing through) — without outranking
   the drum's stacking, the button would render UNDER that opaque
   canvas and disappear entirely. */
.cases-drum__cta {
  position: relative;
  flex: 0 0 auto;
  align-self: center;
  margin: clamp(46px, calc(68 / 1920 * 100vw), 92px) 0 0;
  width: clamp(180px, calc(245 / 1920 * 100vw), 245px);
  height: clamp(48px, calc(70 / 1920 * 100vw), 70px);
  background: #ff2828;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display, sans-serif);
  font-weight: 700;
  font-size: 12px;
  color: #fff;
  text-transform: uppercase;
  text-decoration: none;
  line-height: 1.03;
  z-index: 2;
  transition: background 0.2s ease;
}
.cases-drum__cta:hover { background: #c81f1f; }

/* Arrows — mockup composition supersedes spec §6 (circular, centred
   below the drum): elongated pill/capsule outlines instead, pinned to
   the far left/right EDGES of the drum area and vertically centred on
   it, overlapping the cropped side cards there. Hidden by default, same
   fallback-is-the-default reasoning as the canvas (see the big comment
   above .cases-drum__list below) — nothing for them to control before
   --ready, and they'd otherwise float uselessly above the fallback
   list.
   top/bottom (was inset:0 + align-items:center, i.e. centred on the
   whole drum-wrap box) — that centred on the BOX, which was fine while
   the card filled almost the whole box, but this pass's framing seats
   the card in roughly the TOP half of a box that's now tall enough to
   never clip it (see .cases-drum__drum-wrap's own comment) — centring
   on the box any longer would centre the arrows on empty lower space,
   well below where the card (and the side cards either side of it) now
   actually sit. Pixel-measured the card's own top/bottom edge instead
   (59px / 710px, 1920 basis) and pinned this row to exactly that span,
   confirmed proportionally identical at 1470px (45.0px / 543.6px of a
   1110.2px box — same ~4.1%/49% split as 59/710 of 1450px) so this
   holds as a ratio the same way the rest of the framing does, not just
   at the one width it was measured against. */
.cases-drum__arrows {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: calc(59 / 1920 * 100vw);
  bottom: calc((1450 - 710) / 1920 * 100vw);
  z-index: 3;
  align-items: center;
  justify-content: space-between;
  padding: 0 clamp(6px, calc(24 / 1920 * 100vw), 24px);
  pointer-events: none;   /* only the buttons themselves re-enable it */
}
.cases-drum__arrow {
  width: clamp(34px, calc(44 / 1920 * 100vw), 44px);
  height: clamp(96px, calc(140 / 1920 * 100vw), 140px);
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(6, 12, 12, 0.15);
  color: #fff;
  opacity: 0.5;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  pointer-events: auto;
  transition: opacity 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}
.cases-drum__arrow svg { transition: transform 0.15s ease; }
/* 2026-10-14 bug-hunt decision #2: the clamp() above floors at 34px,
   under the 44px tap-target floor, below ~1527px viewport width (where
   44/1920*100vw drops under 44) — height is already always ≥96px, only
   width needs a floor on touch/narrow. */
@media (max-width: 1023px), (pointer: coarse) {
  .cases-drum__arrow { min-width: 44px; }
}
.cases-drum__arrow:hover { opacity: 1; border-color: rgba(255, 255, 255, 0.45); }
.cases-drum__arrow--prev:hover svg { transform: translateX(-2px); }
.cases-drum__arrow--next:hover svg { transform: translateX(2px); }
.cases-drum__arrow:active { transform: scale(0.96); }
.cases-drum__arrow:focus-visible {
  outline: 2px solid #ff2828;
  outline-offset: 2px;
}

/* Canvas layer — fills .cases-drum__drum-wrap exactly (real pixel-
   sized, not CSS-scaled — transforming a WebGL canvas via CSS scale
   would blur it and desync pointer coordinates from world space).
   cases-drum.js's own ResizeObserver reads this box's clientWidth/
   clientHeight on every change and re-derives the camera from that, so
   it doesn't matter that this is no longer a fixed aspect-ratio box —
   whatever height the flex layout leaves it gets used as-is. HIDDEN by
   default — see the fallback-is-the-default note below for why.

   Why .cases-drum__drum-wrap's height (1450px @1920, was 950px) is the
   fix for top-clipping, not a camera value: the clip is a genuine
   camera-frustum clip (the hand-tuned desktop values — see CONFIG.
   desktop's own comment — place the card partly outside the vertical
   FOV at the box height they were tuned against), and growing THIS
   box's height is the one lever that resolves it without touching any
   of those values — because this box's height feeds camera.aspect
   (width ÷ height), and desktop turned out to land WIDTH-bound at these
   values (unlike the previous pass, which was clearly height-bound) —
   so taller (narrower aspect) moves the width-solved camera distance
   back, which also happens to clear the frustum vertically. Verified
   empirically, not derived from the aspect math alone: 950px still
   clipped (cardTop pinned to row 0), 1350px still clipped, 1430px
   cleared it with a 46.5px margin, 1450px shipped with a bit more
   margin than the bare minimum. This does mean the card's exact
   on-screen % isn't perfectly identical to whatever the panel showed at
   its own tested box height — an unavoidable side effect of fixing a
   frustum clip through the one non-camera lever available, not a sign
   the hardcoded values were re-derived. */
.cases-drum__canvas-wrap {
  display: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.cases-drum__canvas-wrap canvas {
  display: block;
  width: 100%;
  height: 100%;
}
.cases-drum--ready .cases-drum__canvas-wrap { display: block; }
.cases-drum--ready .cases-drum__arrows { display: flex; }
@media (max-width: 768px) {
  .cases-drum--ready .cases-drum__arrows { display: none; }  /* still no arrows on mobile */
}

/* ════════════════════════════════════════════════════════════════════
   FALLBACK IS THE DEFAULT, not a special state — this is the load-
   bearing fix for spec §9 ("if WebGL is unavailable or the JS fails to
   load, show the list as a scroll-snap slider"): if the .cases-drum.js
   *script tag itself* never runs at all (blocked, network failure, ad
   blocker, whatever), there is no JS left to add any class that would
   reveal a fallback — so the fallback can't be something JS switches
   ON, it has to be what's already showing before JS does anything, with
   JS only ever switching it OFF (adding .cases-drum--ready) once WebGL
   has actually and successfully initialised. Arrows stay hidden until
   --ready too (nothing for them to control before then).
   padding-top clears the label/heading overlay above (see
   .cases-drum__stage) since this list lives in the same shared space —
   without it the fallback's first row of cards would sit directly
   under the heading text instead of below it. */
.cases-drum__list {
  display: flex;
  gap: 16px;
  margin: 0;
  padding: clamp(150px, calc(260 / 1920 * 100vw), 260px) 20px 32px;
  overflow-x: auto;
  overflow-y: hidden;
  white-space: nowrap;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}
.cases-drum__list li {
  display: inline-block;
  flex: 0 0 auto;
  width: min(80vw, 420px);
  scroll-snap-align: center;
}
.cases-drum__list a { display: block; }
.cases-drum__list img {
  display: block;
  width: 100%;
  aspect-ratio: 1194 / 790;
  object-fit: cover;
  border-radius: 4px;
}

/* Once --ready, the list is still very much there for SEO/screen
   readers — just not shown to sighted users, who see the canvas
   instead. NOT display:none (that would remove it from the
   accessibility tree too) — clipped to 1px instead, which keeps it
   focusable, so a sighted keyboard user tabbing through still lands on
   real, working links; the :focus-visible rule right after this pulls
   whichever one they're on back into view rather than leaving them
   staring at an invisible focus ring. */
.cases-drum--ready .cases-drum__list {
  position: absolute;
  display: block;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.cases-drum--ready .cases-drum__list a:focus-visible {
  position: fixed;
  top: 12px;
  left: 12px;
  width: auto;
  height: auto;
  clip: auto;
  z-index: 1000;
  background: #060c0c;
  color: #fff;
  padding: 12px 16px;
  border-radius: 8px;
  outline: 2px solid #ff2828;
  outline-offset: 2px;
}

/* ===================== MOBILE ===================== */
/* Same structural principle as desktop — one basis (calc(px/393*100vw),
   the site's existing mobile convention, e.g. .hero's mobile block) for
   every vertical measurement in the flow, no vh anywhere, no min-height
   floor fighting the content for the section's own height. The one
   difference from desktop: margin-top (heading clearance) isn't pinned
   to a clean calc() off the heading's own position, because the mobile
   heading isn't positioned on this scale at all — it's `position:
   static`, stacked in normal flow inside .cases-drum__stage's own
   padding, so its rendered height depends on that padding plus this
   breakpoint's own clamped label/heading font-sizes plus the 8px
   heading margin-top, all at once. Worked out by hand across 320–393px
   instead (stage padding-top 22.8–28px + label ~12.4px + 8px + heading
   text 56–62.9px ≈ 99–111px) and the 393-baseline number below picked
   with real margin above the tallest of those — same spirit as
   desktop's exact pin, just calibrated rather than derived, for the one
   zone where a clean formula isn't available. Arrows stay hidden here
   (swipe takes over instead, unchanged above). */
@media (max-width: 768px) {
  .cases-drum {
    /* Deliberately larger than .cases-drum__cta's own margin-top below
       — same "CTA groups with this block" reasoning as desktop, just on
       the 393 baseline. */
    padding-bottom: clamp(28px, calc(40 / 393 * 100vw), 64px);
    /* Mobile's own navbar footprint is different from desktop's (see the
       desktop rule's own scroll-margin-top comment for the general
       issue) — style.css's mobile override is top:66px/height:60px, so
       it occupies y=66–126 here versus desktop's y=23–93. 140px clears
       that with the same ~14–17px buffer style as the desktop value.
       Purely a scroll-target fix — doesn't move or resize anything, so
       it has no effect on any of this breakpoint's measured numbers. */
    scroll-margin-top: 140px;
  }

  .cases-drum__stage {
    padding: clamp(20px, calc(28 / 393 * 100vw), 32px) 20px 0;
  }
  .cases-drum__label,
  .cases-drum__heading {
    position: static;
    width: auto;
  }
  .cases-drum__label { font-size: 12px; }
  .cases-drum__heading { font-size: clamp(28px, 8vw, 42px); margin-top: 8px; }

  /* Both this box's height and cases-drum.js's new lookAtYOffsetMobile
     (see that CONFIG comment) were recalibrated together THIS pass —
     changing one changes the band's aspect ratio, which changes what the
     other needs to be, so they were iterated as one change, not two.
     Short version: raising centerRatioMobile to hit the ~80%-of-
     viewport-width target (spec §7) made the card noticeably bigger,
     which is what turned this box's old 460px height (sized for a much
     smaller card, and never revisited since) into most of the "large
     dead area above the drum" this pass was asked to fix — pixel-
     sampling showed the card sitting well off-centre AND with a lot of
     pure unused padding inside this box either way, not just the CSS
     gaps around it. Shrunk to 300px (393 baseline) — comfortably more
     than the card's own ~208px height, so there's real breathing room,
     but not the ~250px of near-pure dead space the old 460px height
     left. Floor raised to 270px (was 300px) specifically so this can't
     shrink far enough to flip mobile from width-bound to height-bound
     (see updateCamera's own comment) at the narrowest tested width — the
     breakeven point, worked out from centerRatioMaxHeightMobile /
     cardAspect / centerRatioMobile, is ~245px at the 393 basis; 270px
     keeps a real margin above that, confirmed by re-checking the width
     ratio held (not silently shrunk) at 320px after this change. Result,
     pixel-sampled at all three tested widths: the card now sits centred
     in this box to within ~1px (e.g. 48.0px above / 48.8px below at
     393px), not just closer to the top edge. */
  .cases-drum__drum-wrap {
    height: clamp(270px, calc(300 / 393 * 100vw), 340px);
    /* margin-top only has to clear the heading's own rendered bottom
       edge (measured 99–111px across 320–393px, static-flow content, not
       a clean calc() the way desktop's pinned value is — see the file-
       level comment) plus a real but modest gap, not the old ~155px
       (which left 44–56px of CSS gap on top of the box's own internal
       padding above the card — see this rule's own comment). 130px
       (393 basis) clears the tallest measured heading (111px) by ~19px;
       the 108px floor still clears the shortest (99px at 320px) by ~9px
       — both pixel-confirmed non-crossing after this change, with the
       card itself sitting well inside the box besides (built-in extra
       margin, not the last line of defence). */
    margin-top: clamp(108px, calc(130 / 393 * 100vw), 138px);
    /* Resets two desktop-only properties (see the base
       .cases-drum__drum-wrap rule's own comments) that would otherwise
       leak straight through to mobile unchanged, since this media query
       only overrides height/margin-top, not every property.
       z-index: now harmless either way (desktop's own value is 1, well
       below .cases-drum__stage's 3, so mobile would land in the same
       place even without this line) — kept explicit anyway so mobile
       plainly opts out of desktop's stacking scheme rather than
       inheriting it by coincidence. The negative margin-bottom is the
       one that still matters: desktop-only compensation for its own box
       being much taller than its card — mobile's box isn't. Caught
       live, pixel-confirmed, when it briefly wasn't reset: without it,
       mobile's band→CTA gap measured -254px (the CTA rendered ABOVE the
       band, not below it) — a real regression, not a hypothetical. */
    z-index: auto;
    margin-bottom: 0;
  }

  /* Same treatment as the hero's mobile CTA row — full-width within the
     same side margins (clamp(39px, 48/393*100vw, 48px)) — rather than a
     small centred pill. align-self:stretch (overriding the desktop
     "center", which would otherwise shrink-wrap this to its own content
     width) is what actually makes "width:auto" span the full row.
     margin-top is the real, deliberate band→CTA gap on this same 393
     basis — smaller than .cases-drum's own padding-bottom above, same
     asymmetry as desktop. This value itself (16px, 393 basis) is
     UNCHANGED this pass — it didn't need to shrink further: the "CTA
     reads detached" complaint was, like the heading gap, mostly the
     drum-wrap box's own unused padding below the card (see that rule's
     comment), not this margin. Shrinking the box already closes most of
     it; pixel-sampled card-bottom-to-CTA-top afterward is ~65px at
     393px (was ~145px), without touching this number at all. */
  .cases-drum__cta {
    align-self: stretch;
    width: auto;
    height: clamp(44px, calc(56 / 393 * 100vw), 56px);
    margin: clamp(10px, calc(16 / 393 * 100vw), 24px) clamp(39px, calc(48 / 393 * 100vw), 48px) 0;
  }
}
