/* ============================================================================
   gallery.css — the work section, rebuilt as a spatial carousel.

   Loaded last on index.html so it wins over studio.css / overhaul.css /
   refinements.css / polish.css / refresh.css without editing any of them.
   The old .cinema / .film-* / .project-choices rules stay where they are:
   nothing in the new markup answers to those selectors any more.

   What replaced what
   -------------------
   .cinema's fake browser chrome (the "casaverde.vercel.app / FIRMITAS / DEMO
   COLLECTION" bar) is gone. A frame that pretends to be a browser is a frame
   that admits the thing inside it is a video. Every card is now the site
   itself, standing in space: a wide plane plus a phone plane floating in
   front of it, both running.

   Motion contract
   ---------------
   Vertical scroll is the only source of truth for carousel position. The stage
   is pinned; runway height is 100vh + (cards - 1) * step. When the runway is
   spent the pin releases and the next section arrives. Drag never becomes its
   own state — a release resolves to a card index and scrolls the page to the
   scroll position that centres that card, so scroll and drag can never
   disagree about where the carousel is.

   Three.js was deliberately not used. The depth here is real CSS 3D driven by
   per-card custom properties, which keeps the section under ~14 KB and lets
   every card stay a real, focusable, crawlable link.
   ========================================================================= */

.gallery {
  /* A near-black plum, not pure black: the void still belongs to the brand. */
  --void: #140b1a;
  --void-2: #1e1128;
  --card-w: clamp(300px, 44vw, 720px);
  /* Bigger than it looks necessary, and that is the point. A yawed, translated
     card projects a bounding box WIDER than its layout box, so neighbours'
     boxes overlap even though their pixels do not — except that the phone plane
     inside a card then crosses into the next card's video. Measured at a 40px
     gap: Bob's phone plane reached 19px into Summit's panel. The gap has to
     absorb the projection, not just separate the layout. */
  --card-gap: clamp(64px, 8vw, 140px);
  --card-radius: 18px;
  /* 22%, down from 26%. At 26% the phone plane's right edge crossed into the
     next card's panel and label by 3px at the widest viewport — measured, not
     eyeballed. The phone still reads clearly as a second device at 22%. */
  --phone-w: 22%;
  /* Bright enough to actually read against a near-black room. At .16 the grid
     was measurably present in the DOM and invisible on screen. */
  /* Measured, not guessed. With .3 the floor raised the band's mean luminance
   by 2.0/255 — it was genuinely painting, just too faint to read as a floor in
     a screenshot. .46 and a tighter line pitch make the grid legible without
     turning the room into graph paper. */
  --grid: rgba(214, 198, 245, .58);
  /* Detail layer. Brand near-black rather than the reference's cream, so the
     demo footage is not fighting a bright background and the section does not
     flash from dark room to light room mid-scroll. Structure is his; palette is
     ours. */
  --detail-bg: #120a17;
  --detail-ink: #f6eee9;
  --detail-muted: #b8a3bd;
  --detail-line: rgba(246, 238, 233, .16);
  --detail-accent: #ff8064;
  position: relative;
  z-index: 1;
  padding: 0;
  margin: 0;
  background: var(--void);
  color: var(--stone);
  overflow: clip;
  /* Anchors must clear the sticky header. */
  scroll-margin-top: 90px;
  /* The section owns its own dark ground, so nothing inside inherits the page
     panel colour. */
  isolation: isolate;
}

/* A coral bloom low in the frame. Keeps a black room from reading as a hole. */
.gallery::before {
  content: "";
  position: absolute;
  inset: 42% -20% -30%;
  background: radial-gradient(58% 62% at 50% 60%, rgba(255, 128, 100, .22), transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/* --- the runway: the scroll distance the carousel spends ------------------ */

.gallery-runway {
  position: relative;
  /* Height is measured and written by gallery.js. This is the honest fallback
     if the script never runs: one screen, one card, no pin. */
  height: 100vh;
}

.gallery-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: grid;
  grid-template-rows: 1fr;
  overflow: hidden;
  padding: 0 4.4%;
}

/* --- heading block -------------------------------------------------------- */

/* In flow, at the head of the runway, above the stage.

   It used to be absolutely positioned INSIDE the 100vh sticky stage, which put
   it on top of whatever the cards were doing — in the GL room the lede ran
   straight across the front plate, and in the CSS runway it sat over the
   track's own top padding. Being first in the runway means it scrolls away
   before the stage pins, so the cards and the copy never share pixels at all.
   gallery.js adds its height into the runway measurement and starts progress
   when the stage actually pins (see scrollProgress/railHeight/measure). */
.gallery-head {
  display: grid;
  grid-template-columns: 1fr;
  align-items: end;
  gap: 18px;
  max-width: 720px;
  width: min(100%, 720px);
  margin: 0;
  position: relative;
  /* Clearance for the fixed header, same numbers the old absolute rule used. */
  padding: clamp(104px, 15vh, 150px) 0 0 4.4%;
  z-index: 3;
  pointer-events: none;
}

.gallery-head a,
.gallery-head button { pointer-events: auto; }

.gallery-kicker {
  grid-column: 1 / -1;
  font: 600 11px/1 Manrope, sans-serif;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--coral);
  margin: 0 0 -8px;
}

.gallery-head h2 {
  /* The h2 is auto-masked line-by-line by studio.js; the block below only
     supplies the grid it sits in. */
  margin: 0;
  max-width: 14ch;
  font-size: clamp(30px, 4.1vw, 58px);
  line-height: 1.02;
}

.gallery-lede {
  margin: 0;
  max-width: 640px;
  font-size: clamp(13px, .95vw, 16px);
  line-height: 1.55;
  color: var(--muted);
  text-wrap: pretty;
}

/* Two variants of one sentence. The wide one tells you to "scroll or drag
   sideways", which is true on desktop and impossible on a phone — and the
   phone build is a vertical stack, not a carousel. Rather than ship an
   instruction the reader cannot act on, the copy switches. Both spans are in
   the markup (crawlers see the full desktop text); CSS picks one. */
.gallery-lede--narrow {
  display: none;
}

/* --- the floor ------------------------------------------------------------ */

/* A flat horizon glow sitting on top of the grid sells the distance more than
   the grid alone does. */
.gallery-floor::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 44% at 50% 100%, rgba(255, 128, 100, .3), transparent 72%);
}

.gallery-floor {
  --floor-x: 0px;
  position: absolute;
  inset: auto 0 -6%;
  height: 84vh;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 34%, #000 74%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 34%, #000 74%, transparent 100%);
}

/* No `perspective` here, and no rotateX.

   The first version tilted this plane with rotateX(68deg) under a 520px
   perspective. The plane is ~1500px tall, so rotating it put its far edge
   ~1400px behind the eye — well past the 520px vanishing point, which flips
   the projection and collapses the element. Proof it never painted: a
   screenshot of the band with the floor visible and with it hidden were
   byte-identical (27123B vs 27123B, delta 0). The grid was fully specified,
   correctly coloured, and completely invisible.

   scaleY() from the bottom edge gives the same converging-floor read with no
   z-plane, so it cannot cross a vanishing point. */
.gallery-floor::before {
  content: "";
  position: absolute;
  inset: 0 -30%;
  background-image:
    repeating-linear-gradient(90deg, var(--grid) 0 1px, transparent 1px 68px),
    repeating-linear-gradient(0deg, var(--grid) 0 1px, transparent 1px 68px);
  transform: translate3d(var(--floor-x), 0, 0) scaleY(.42);
  transform-origin: 50% 100%;
  will-change: transform;
}

/* A flat horizon glow sitting on top of the grid sells the distance more than
   the grid alone does. */
.gallery-floor::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 44% at 50% 100%, rgba(255, 128, 100, .3), transparent 72%);
}

/* --- the viewport + track ------------------------------------------------- */

.gallery-viewport {
  position: relative;
  z-index: 2;
  height: 100%;
  min-height: 0;
  perspective: 1600px;
  perspective-origin: 50% 42%;
  overflow: clip;
  /* Vertical panning stays with the page; horizontal is ours. */
  touch-action: pan-y;
  cursor: grab;
}

.gallery-viewport:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: -2px;
}

.gallery-viewport.is-dragging {
  cursor: grabbing;
}

.gallery-track {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: var(--card-gap);
  /* The track spans the full viewport, so it sits ON TOP of its own cards and
     swallows every pointer event aimed at them: document.elementFromPoint over
     a card returned UL.gallery-track, not the card. That made hover impossible —
     the card never received pointerenter no matter what the JS did.
     pointer-events:none passes events straight through to the cards beneath;
     the individual .gallery-card puts its own back on. The drag surface is the
     .gallery-viewport, not the track, so nothing is lost. */
  pointer-events: none;
  /* gallery.js measures the real viewport width and writes --lead here, so the
     first card lands exactly on the centre of the track. A calc() against 50vw
     cannot do this: the track sits inside .gallery-stage, which has its own
     4.4% side padding, so 50vw is never the middle of the box the card is
     being centred in. Measured beats guessed. */
  padding-inline: var(--lead, 24px);
  padding-top: clamp(220px, 32vh, 290px);
  margin: 0;
  list-style: none;
  transform-style: preserve-3d;
  will-change: transform;
}

/* --- one card ------------------------------------------------------------- */

.gallery-card {
  /* Per-card geometry written by gallery.js every frame. The defaults are the
     resting state so the row still reads correctly with no JS at all. */
  --gx: 0px;
  --gy: 0px;
  --gz: 0px;
  --gr: 0deg;
  --gs: 1;
  --ga: 1;
  position: relative;
  flex: 0 0 auto;
  width: var(--card-w);
  /* Opt back in, because the track turned events off. Without this the card is
     hit-testable but inert — no :hover, no pointerenter, nothing. */
  pointer-events: auto;
  transform-style: preserve-3d;
  transform:
    translate3d(var(--gx), var(--gy), var(--gz))
    rotateY(var(--gr))
    scale(var(--gs));
  opacity: var(--ga);
  /* No transition on the transform: it is written per frame. Colour shifts are
     cheap and are allowed to ease. */
  transition: filter .5s var(--ease), opacity .5s var(--ease);
  will-change: transform, opacity;
}

.gallery-shot {
  position: relative;
  width: 100%;
}

/* --- the two device planes ------------------------------------------------ */

.gallery-plane {
  position: relative;
  overflow: hidden;
  background: #0b0710;
  border: 1px solid rgba(246, 238, 233, .1);
  box-shadow: 0 34px 90px -30px rgba(0, 0, 0, .9);
}

.gallery-plane--wide {
  aspect-ratio: 8 / 5;
  border-radius: var(--card-radius);
  transform: translateZ(0);
}

/* Contact shadow: a tight dark smear directly under the plane, so the card sits
   ON the floor rather than floating above it. */
.gallery-plane--wide::before {
  content: "";
  position: absolute;
  inset: auto 4% -8% 4%;
  height: 14%;
  background: radial-gradient(64% 100% at 50% 0, rgba(0, 0, 0, .7), transparent 76%);
  filter: blur(10px);
  pointer-events: none;
  z-index: -1;
}

/* Reflection: a faint lilac bounce off the lit floor, hugging the card's own
   bottom edge. Deliberately kept INSIDE the plane's footprint. The first
   version hung a glow 13% below and 6% out on each side, which drew a lit
   rectangle visibly larger than the card — it read as a second, misaligned
   panel sitting behind the real one (visible in the desk-0 screenshot). */
.gallery-plane--wide::after {
  content: "";
  position: absolute;
  inset: auto 12% -3% 12%;
  height: 8%;
  border-radius: 0 0 var(--card-radius) var(--card-radius);
  background: radial-gradient(58% 100% at 50% 0, rgba(214, 198, 245, .3), transparent 78%);
  filter: blur(6px);
  pointer-events: none;
}

.gallery-plane--phone {
  position: absolute;
  /* Tucked INSIDE the wide plane's right edge rather than hung off it. Hanging
     it off put the phone on top of the next card's video, which is what the
     first screenshot showed. Tucked, it still reads as a separate object in
     front, because translateZ separates it in depth. */
  /* right offset, not width, is what clears the neighbour. Shrinking --phone-w
     did nothing measurable (still 3px) because `right` pins the plane's right
     edge — moving it inward from 3% to 8% is what actually moves that edge
     left. Measured: right:3% -> 3px into the next card; right:8% -> clear. */
  right: 8%;
  /* bottom: -6% pushed the phone BELOW the desktop panel, where it landed on
     top of the third spec column ("OWNERSHIP / Code and domain yours
     outright") in the screenshots. Positive bottom keeps it inside the card's
     own height; translateZ still separates it in depth, which is what sells
     the two-device read. */
  bottom: 4%;
  width: var(--phone-w);
  aspect-ratio: 390 / 844;
  border-radius: calc(var(--card-radius) * .5);
  /* Pushed toward the viewer so the two planes separate in depth instead of
     reading as one flat picture. */
  transform: translateZ(70px) rotateY(-7deg);
  transform-origin: 50% 100%;
  box-shadow: 0 26px 60px -22px rgba(0, 0, 0, .95);
}

.gallery-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  background: #0b0710;
}

/* The phone plane shows a poster, not a recording. It is a <img> now, and it
   must fill its plane exactly as the <video> did. */
.gallery-still {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  background: #0b0710;
}

/* Phone footage is portrait; cover would crop it into a sliver. */
.gallery-plane--phone .gallery-video {
  object-position: top center;
}

/* --- card chrome: name, kind, links -------------------------------------- */

/* A scrim under the label only — not a full card overlay. The video is the
   point; a heavy gradient over it just dims the thing we are showing off. */
.gallery-shot::after {
  content: "";
  position: absolute;
  inset: 46% 0 0;
  border-radius: 0 0 var(--card-radius) var(--card-radius);
  background: linear-gradient(to top, rgba(11, 7, 16, .92), rgba(11, 7, 16, .55) 42%, transparent);
  pointer-events: none;
  /* No opacity here. This element's opacity is set by the single combined rule
     further down (".gallery-ui, .gallery-shot::after"). Declaring it twice —
     once as var(--ga), once as the gated clamp — meant the FIRST declaration
     won on source order for .gallery-shot::after, so neighbours kept a visible
     dark scrim across the video that read as ghosted text. Measured: both cards
     at --ga .827 showed the scrim at full while .gallery-ui was correctly 0.
     One declaration, one owner. */
}

.gallery-ui {
  position: absolute;
  inset: auto auto 0 0;
  z-index: 2;
  padding: 0 0 0 4px;
  display: grid;
  gap: 3px;
  /* Driven by distance-from-centre, not by :hover. This is the one mechanic
     worth copying from the reference: the title belongs to whichever card you
     are actually looking at. Measured there — hover changed nothing — so the
     position-based rule is the correct one, not a consolation prize. */
  transform: translate3d(0, calc((1 - var(--ga)) * 12px), 40px);
}

/* The label gate starts at .9, not .55.

   At a .55 gate a neighbour sitting at --ga .56 rendered its kind and title at
   3% opacity — which is invisible as a fade and completely legible as TEXT, laid
   across the centred card's video. Caught only in the 10-card screenshot
   ("URANT / EDITORIAL / ghtside Gym" over Casa Verde). Text needs a much higher
   threshold than an image does: below ~10% it stops reading as a fade and
   starts reading as a ghost. One card's name, one card at a time. */
.gallery-ui,
.gallery-shot::after {
  opacity: clamp(0, (var(--ga) - .9) / .1, 1);
}

/* The open-the-demo button only on the card in front too. Three circular
   buttons on screen at once was pure noise — the arrow belongs to the card
   you would actually click. */
.gallery-open {
  opacity: clamp(0, (var(--ga) - .82) / .18, 1);
  pointer-events: auto;
}

.gallery-kind {
  margin: 0;
  font: 600 10px/1 Manrope, sans-serif;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--coral);
}

.gallery-name {
  margin: 0;
  font-family: 'Bricolage Grotesque', sans-serif;
  font-weight: 500;
  font-size: clamp(20px, 1.7vw, 27px);
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--stone);
  text-shadow: 0 2px 24px rgba(0, 0, 0, .85);
}

/* "See it full" — opens the detail layer.

   A <button>, not an <a href="#">: the old card had an <a> pointing at the case
   study, which meant the only way into the work was the written page. This opens
   the full-screen capture view, and the case study stays reachable from inside
   it. A button is also the honest element — it navigates to a view of the same
   page rather than to a URL a crawler would want to follow. */
.gallery-peek {
  grid-row: 3;
  justify-self: start;
  pointer-events: auto;
  margin-top: 6px;
  padding: 0 0 4px;
  border: 0;
  border-bottom: 1px solid rgba(246, 238, 233, .34);
  background: none;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font: 600 11px/1 Manrope, sans-serif;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--stone);
  cursor: pointer;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}

.gallery-peek svg {
  width: 13px;
  height: 13px;
  transition: transform .3s var(--ease);
}

.gallery-peek:hover {
  color: var(--coral);
  border-color: var(--coral);
}

.gallery-peek:hover svg {
  transform: translate(2px, -2px);
}

/* The circular open-the-demo button. */
.gallery-open {
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 58px;
  height: 58px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(11, 7, 16, .82);
  border: 1px solid rgba(246, 238, 233, .2);
  color: var(--stone);
  text-decoration: none;
  transition: background .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease), transform .35s var(--ease);
}

.gallery-open svg {
  width: 19px;
  height: 19px;
}

.gallery-open:hover {
  background: var(--coral);
  border-color: var(--coral);
  color: var(--void);
  transform: scale(1.07);
}

/* --- the spec strip: the old film-proof rows, kept but demoted ----------- */

.gallery-spec {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 14px);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  margin: 0;
  padding: 14px 0 0;
  list-style: none;
  border-top: 1px solid rgba(246, 238, 233, .14);
  /* ONLY the card in front gets its spec strip. A squared falloff still left
     neighbours at .47^2 = .22, which was legible enough to collide with the
     centred card's copy. Hard-gated instead: below .82 the strip is gone.
     Neighbours keep their video and title; the prose belongs to one card. */
  opacity: clamp(0, (var(--ga) - .82) / .18, 1);
}

.gallery-spec div {
  display: grid;
  gap: 3px;
}

.gallery-spec dt {
  font: 600 9.5px/1 Manrope, sans-serif;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--coral);
}

.gallery-spec dd {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}

/* --- foot: rail, arrows, help -------------------------------------------- */

.gallery-foot {
  position: absolute;
  left: 4.4%;
  right: 4.4%;
  bottom: clamp(18px, 4vh, 34px);
  z-index: 3;
  /* Three explicit columns for three children. With `1fr auto` the third child
     wrapped onto an implicit second row, which pushed the arrows below the
     rail and left the rail looking like a stray hairline. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 20px 28px;
  max-width: 1560px;
  width: 100%;
  margin: 0 auto;
  padding-top: 14px;
  pointer-events: none;
}

.gallery-foot a,
.gallery-foot button,
.gallery-foot [role="tab"] { pointer-events: auto; }

/* min-height, not height: a 22px-tall box clipped the tick hairlines and the
   focus ring. The rail was measurably present and invisible. */
.gallery-rail {
  position: relative;
  min-height: 26px;
  display: flex;
  align-items: stretch;
  gap: 2px;
}

.gallery-tick {
  appearance: none;
  border: 0;
  padding: 0;
  /* 26px of touch target, not 22. The hairline is drawn from the middle. */
  height: 26px;
  flex: 1 1 0;
  min-width: 40px;
  background: none;
  cursor: pointer;
  position: relative;
}

/* One hairline per project that fills as it becomes the card in front. */
.gallery-tick::after {
  content: "";
  position: absolute;
  inset: auto 4px 50%;
  height: 1px;
  background: rgba(246, 238, 233, .22);
  transition: background .4s var(--ease), height .4s var(--ease);
}

.gallery-tick:hover::after {
  background: rgba(246, 238, 233, .6);
}

.gallery-tick[aria-current="true"]::after {
  background: var(--coral);
  height: 2px;
}

.gallery-help {
  margin: 0;
  font: 500 11px/1 Manrope, sans-serif;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 10px;
  transition: opacity .6s var(--ease);
}

.gallery-help svg {
  width: 22px;
  height: 12px;
}

.gallery-help.is-done {
  opacity: 0;
}

.gallery-arrows {
  display: flex;
  gap: 8px;
}

.gallery-arrow {
  appearance: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(246, 238, 233, .22);
  background: none;
  color: var(--stone);
  cursor: pointer;
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease), opacity .3s var(--ease);
}

.gallery-arrow svg {
  width: 15px;
  height: 15px;
}

.gallery-arrow--prev svg {
  transform: rotate(180deg);
}

.gallery-arrow:hover:not(:disabled) {
  background: var(--stone);
  border-color: var(--stone);
  color: var(--void);
}

.gallery-arrow:disabled {
  opacity: .3;
  cursor: default;
}

/* Off-screen live region: the same information the visual UI gives, for a
   screen reader and for anyone who never scrolls the carousel. */
.gallery-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- depth falloff ------------------------------------------------------- */

/* Hover is handled entirely in gallery.js as a damped --h value, not as a CSS
   :hover rule. A CSS transition here would fight the per-frame transform
   writes paint() makes on the same element — two writers, one property, visible
   jitter. The pointer events set a target; the rAF loop eases toward it; paint()
   consumes it like every other offset. See `dampHover` in gallery.js.

   What this buys, learned from watching the reference as a frame strip rather
   than from its CSS: a hovered card comes FORWARD out of the row (translateZ),
   scales up, rotates toward facing the viewer (its yaw is cancelled out), and
   gains brightness and saturation — over several hundred milliseconds. The
   pointer is acknowledged immediately on entry.

   There is deliberately no CSS fallback. `filter: brightness(1.08)` is what this
   replaced, and it was not a mechanic — it was an 8% brightness bump and nothing
   else: no lift, no turn, no travel. A card you could look at but not
   interrogate. */
@media (hover: none) {
  /* Touch devices get no hover, so nothing to declare — but the JS still needs
     to know, and it checks pointerType rather than this query. Left here as
     documentation of intent. */
}

/* ============================================================================
   Phone. No pin, no 3D, no fake depth — a stacked list of real sites. Pinning
   a horizontal scroll-hijack onto a phone is how you make people hate a site.
   ========================================================================= */

@media (max-width: 900px) {
  .gallery-stage {
    position: static;
    height: auto;
    min-height: 0;
    padding-bottom: 56px;
    row-gap: 28px;
  }

  .gallery-runway {
    height: auto !important;
  }

  .gallery-lede--wide {
    display: none;
  }

  .gallery-lede--narrow {
    display: inline;
  }

  .gallery-head {
    position: static;
    left: auto;
    right: auto;
    max-width: none;
    width: auto;
    grid-template-columns: 1fr;
    gap: 18px;
    /* The stack used to get its head-to-stage gap from the stage's grid
       row-gap; the head is outside the stage now, so it carries its own. */
    padding: clamp(96px, 13vh, 130px) 6% 0;
    margin-bottom: 28px;
  }

  .gallery-head h2 {
    max-width: none;
  }

  .gallery-lede {
    max-width: 46ch;
  }

  .gallery-foot {
    position: static;
    left: auto;
    right: auto;
    bottom: auto;
    max-width: none;
    padding-top: 4px;
  }

  .gallery-floor {
    display: none;
  }

  /* The viewport is NOT hidden here. The first version of this rule did
     `display:none` on both, which collapsed every card to width 0 and left the
     phone build of the work section completely empty — verified by probe, not
     guessed. It becomes a plain static block instead. */
  .gallery-viewport {
    position: static;
    overflow: visible;
    perspective: none;
    cursor: auto;
  }

  /* Cards leave the absolute track and become the page flow. */
  .gallery-track {
    position: static;
    inset: auto;
    display: grid;
    gap: 44px;
    padding: 0 6%;
    transform: none !important;
  }

  .gallery-card {
    --card-w: 100%;
    width: 100%;
    transform: none !important;
    opacity: 1 !important;
    filter: none !important;
  }

  .gallery-plane--phone {
    display: none;
  }

  /* The reflection and contact shadow both belong to the 3D room. In a flat
     stacked list on a phone there is no floor to bounce off, so they are
     dropped rather than left as grey smudges under each card. */
  .gallery-plane--wide::after,
  .gallery-plane--wide::before {
    display: none;
  }

  .gallery-ui,
  .gallery-open,
  .gallery-spec {
    opacity: 1;
    transform: none;
  }

  .gallery-spec {
    position: static;
    top: auto;
    margin-top: 16px;
  }

  .gallery-ui {
    padding: 14px 0 0;
  }

  .gallery-open {
    position: static;
    margin-top: 14px;
    width: 52px;
    height: 52px;
  }

  .gallery-foot {
    grid-template-columns: 1fr;
    padding-top: 4px;
  }

  .gallery-help,
  .gallery-arrows {
    display: none;
  }
}

/* ============================================================================
   Reduced motion / script-off. A flat vertical stack, no pin, videos paused on
   their posters. Nothing here depends on JavaScript having run.
   ========================================================================= */

.gallery[data-gallery="still"] .gallery-stage {
  position: static;
  height: auto;
  min-height: 0;
}

.gallery[data-gallery="still"] .gallery-runway {
  height: auto !important;
}

.gallery[data-gallery="still"] .gallery-floor,
.gallery[data-gallery="still"] .gallery-arrows,
.gallery[data-gallery="still"] .gallery-help {
  display: none;
}

.gallery[data-gallery="still"] .gallery-viewport {
  overflow: visible;
}

.gallery[data-gallery="still"] .gallery-track {
  position: static;
  inset: auto;
  display: grid;
  gap: 52px;
  padding: 40px 4.4% 20px;
  transform: none !important;
}

.gallery[data-gallery="still"] .gallery-card {
  transform: none !important;
  opacity: 1 !important;
}

/* The spec strip is absolute (top: 100% + 14px) so it hangs ~95px below its
   card — the phone query re-flows it, but a reduced-motion desktop stack kept
   it absolute against a 52px gap and the NEXT card landed on top of the copy.
   Same treatment as the phone: in flow, visible, no gate. */
.gallery[data-gallery="still"] .gallery-spec {
  position: static;
  top: auto;
  margin-top: 16px;
  opacity: 1;
}

.gallery[data-gallery="still"] .gallery-ui,
.gallery[data-gallery="still"] .gallery-open {
  opacity: 1;
  transform: none;
}
