/* gallery-v2.css — Gallery V2 spotlight horizontal scroll.
   Centered "viewport" frame — items slide right → left.
   Per-item captions sit below each box as flex-siblings — never inside the
   image container. Nothing here styles what is in the frame: the card is drawn
   into the scene's canvas, still or footage alike.
   Background stays transparent — outer site bg rhythm is preserved. */

/* ── Section shell ────────────────────────────────────────────────── */
#gallery-v2 {
  position: relative;
  background: transparent;
  margin-top: -30vh;        /* same as legacy #gallery used — preserves BG transition scroll position */
  z-index: 1;
  color: #fff;
  /* Inherit the global proportional root. Narrow keeps a 16px root; desktop
     scales so the 1920px reference remains exactly 16px. */
  font-size: 1rem;
  /* Read by frame-canvas.js — the frame is drawn, not styled, so its colour
     has to reach JS. `--chalk` from tokens.css. */
  --gv2-frame-ink: rgb(240,236,227);
  /* The frame's size, in one place. Height is the primary and the aspect
     derives the width, so a layout that changes the frame's orientation
     changes one number. Desktop lays the
     reference's 35.5 : 45.7 rectangle on its side; the narrow layout below
     stands it back up, because a landscape frame in a phone-width column
     leaves the photograph too small to read. */
  --gv2-frame-aspect: calc(45.7 / 35.5);
  --gv2-frame-h: 35.5em;
  --gv2-frame-w: calc(var(--gv2-frame-h) * var(--gv2-frame-aspect));
  /* Thumb rails, derived from the frame so they follow it at every width: the
     reference's rail cards are the frame's height ÷ 2.9, at the frame's own
     aspect — a thumb is the same rectangle as the card it becomes. */
  --gv2-rail-h: calc(var(--gv2-frame-h) / 2.9);
  --gv2-rail-w: calc(var(--gv2-rail-h) * var(--gv2-frame-aspect));
  --gv2-rail-gap: 1.4em;
}

/* The pinned viewport — 100vh while the strip scrolls horizontally */
.gv2-pin {
  position: relative;
  height: 100vh;
  width: 100%;
  overflow: hidden;
  display: block;
}

/* ── Light-bg state (mirrors the site's bg transition) ─────────────── */
#gallery-v2.gv2-light                    { --gv2-frame-ink: rgb(20,20,20); }
#gallery-v2.gv2-light .gv2-cap-title     { color: rgba(0,0,0,0.92); }
#gallery-v2.gv2-light .gv2-cap-sub       { color: rgba(0,0,0,0.55); }
#gallery-v2.gv2-light .gv2-cap-index     { color: rgba(0,0,0,0.45); }
#gallery-v2.gv2-light .gv2-item-cap-title { color: rgba(0,0,0,0.88); }
#gallery-v2.gv2-light .gv2-item-cap-sub   { color: rgba(0,0,0,0.5); }

/* ── Spotlight frame ──────────────────────────────────────────────────
   Two invisible boxes measured by js/spotlight/frame-canvas.js: the
   spotlight is the frame at full size (758 × 972 measured off the
   reference, normalised to the 1920 canvas), the inner box the square
   it starts as (78.3 % of that width). Nothing here paints — every
   stroke is drawn on the canvas so it can animate per scroll frame. */
.gv2-spotlight {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--gv2-frame-w);
  height: var(--gv2-frame-h);
  z-index: 3;
  pointer-events: none;
}
/* The square the frame starts as, sized off the frame's SHORT side so it stays
   a square that fits whichever way the frame is turned. 78.3 % of the
   reference's 568 px is the measured 445 px, and that is what it still is. */
.gv2-frame-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(78.3%, calc(var(--gv2-frame-h) * 0.783));
  aspect-ratio: 1;
}
.gv2-frame-canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  visibility: hidden;
}

/* ── The 3D layer ─────────────────────────────────────────────────────
   Topmost layer in the pin: the object occludes the giant line. The spill stays
   below it and keeps its job — the light the type throws on the room (cards,
   frame) — while what makes the object look lit by the type is scene.js's key
   light on the same glow ramp, not a DOM gradient laid over it.
   Sized by scene.js in CSS pixels; hidden until its context exists. */
.gv2-scene {
  position: absolute;
  inset: 0;
  /* A canvas is a REPLACED element: `inset: 0` alone leaves it at its intrinsic
     300 × 150 until scene.js writes the backing-store size on mount, and that
     jump — a viewport-sized box appearing — was the section's entire CLS 1.0.
     Sizing it in CSS gives it the pin's box from first paint, so mounting
     changes only what is drawn into it. */
  width: 100%;
  height: 100%;
  z-index: 7;
  /* Hidden by opacity rather than visibility for the same reason: opacity is
     not layout. */
  opacity: 0;
  pointer-events: none;
}

/* ── Display type ─────────────────────────────────────────────────────
   One sentence on two lines. The giant line is rendered at its full size and
   scaled DOWN into the docked label — scaling type up from a small raster is
   what makes transformed text blurry. `--gv2-giant-em` over
   `--gv2-giant-dock-em` is the scale factor type-layer.js reads; changing the
   display size is a one-line edit here. */
.gv2-type {
  /* 20em ≈ 231px cap height on the 1920 canvas — DM Sans's cap is 0.7em, and the
     reference measures the cap, not the em. */
  --gv2-giant-em: 20;
  --gv2-giant-dock-em: 1.3;
  position: absolute;
  top: 24vh;
  left: 3.8em;
  z-index: 6;
  text-align: left;
  font-family: 'DM Sans', sans-serif;
  pointer-events: none;
}
html[lang="fa"] .gv2-type {
  left: auto;
  right: 3.8em;
  text-align: right;
  font-family: 'YekanBakh', sans-serif;
}

.gv2-type-caps {
  /* Shrink-to-fit so the line's own width is measurable: type-layer.js rejects
     a per-character wrap that changes it (broken Arabic joining). */
  display: inline-block;
  /* DM Sans is variable on an optical-size axis, and at this size the axis
     genuinely changes the line's width: left on auto it renders 6.4 % narrower
     relative to the viewport at 3840 than at 1920, which is a proportional
     break the harness catches. Pinning the axis at what the reference resolves
     to (1.3em × 16px) keeps the approved screen identical and every other
     width in proportion with it. */
  font-variation-settings: 'opsz' 20.8;
  margin: 0;
  font-size: 1.3em;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(240,236,227,0.72);
}
html[lang="fa"] .gv2-type-caps {
  font-weight: 700;
  /* Persian ascenders (آ گ) need the room, and the masks below clip. */
  line-height: 1.9;
  letter-spacing: 0;
  text-transform: none;
}

/* Per-character mask: the character rises into it from below. */
.gv2-ch {
  display: inline-block;
  overflow: hidden;
  white-space: pre;
  vertical-align: bottom;
}
.gv2-ch-in {
  display: inline-block;
  will-change: transform;
}

.gv2-type-giant {
  position: relative;
  margin: 0;
  font-size: calc(var(--gv2-giant-em) * 1em);
  font-weight: 700;
  line-height: 1.15;
  white-space: nowrap;
  color: var(--chalk);
  transform-origin: left top;
  will-change: transform;
}
html[lang="fa"] .gv2-type-giant {
  transform-origin: right top;
  font-weight: 800;
  line-height: 1.5;
}

/* Both glow layers are clones of the line held at constant alpha, so the only
   thing that changes while the line sweeps is their `opacity` — a compositor
   property. Nothing here is repainted per frame, and the crisp ink on top never
   carries a shadow at all. A 0.75em shadow radius over a 20em line is the
   section's largest paint; it happens once. */

/* Three layers: tight core, mid, wide. A DOM approximation of the reference's
   selective bloom — see PLAN.md Phase 3. Transparent fill so the glyphs are not
   painted twice; the shadows are drawn from the glyph outlines regardless. */
.gv2-type-bloom {
  position: absolute;
  inset: 0;
  color: transparent;
  opacity: 0;
  text-shadow:
    0 0 0.06em rgba(240,236,227,0.95),
    0 0 0.22em rgba(240,236,227,0.55),
    0 0 0.75em rgba(240,236,227,0.32);
}

/* The soft falloff the shadow stack cannot reach on its own. */
.gv2-type-halo {
  position: absolute;
  inset: 0;
  color: var(--chalk);
  filter: blur(0.35em);
  opacity: 0;
}
.gv2-type-ink { position: relative; }

/* The light the line casts on the room. Never touches the page background —
   _mzBg owns that — so it sits inside the pin, over the frame, under the type.
   A fixed 84em disc translated to the line's centre, rather than a viewport-wide
   gradient re-centred through a custom property: the falloff reaches zero at 72 %
   of the 42em radius, so the disc contains all of it and the gradient rasterises
   once instead of repainting every frame. */
.gv2-spill {
  position: absolute;
  top: 0;
  left: 0;
  width: 84em;
  height: 84em;
  margin: -42em 0 0 -42em;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(circle 42em at center,
    rgba(240,236,227,0.17), rgba(240,236,227,0.05) 45%, rgba(240,236,227,0) 72%);
}

/* ── Strip + items ────────────────────────────────────────────────── */
.gv2-strip {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  display: flex;
  align-items: flex-start;
  /* Force LTR layout so items[0] is always the leftmost in DOM order — even
     when the page is in RTL (Persian) mode. The strip slides right→left as
     a whole, independent of document direction. */
  direction: ltr;
  gap: clamp(1.75em, 3vw, 3.5em);
  padding: 0 clamp(1.25em, 5vw, 5em);
  width: max-content;
  z-index: 2;
}

/* ── The two thumb strips ─────────────────────────────────────────────
   Built by js/spotlight/rail.js, one thumb per project per side, both strips on
   one offset at one pitch. The geometry is the reference's: a wrapper down each
   side of the frame that clips (`overflow: hidden`), a move container inside it
   holding the whole strip, the left one hanging off its wrapper's right edge so
   it grows away from the frame. What decides whether a thumb is on screen is the
   wrapper's edge, never the thumb's own opacity. */
#gallery-v2.gv2-live .gv2-thumbs {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc((100% - var(--gv2-frame-w)) / 2 - var(--gv2-rail-gap));
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
}
#gallery-v2.gv2-live .gv2-thumbs.is-left { left: 0; }
#gallery-v2.gv2-live .gv2-thumbs.is-right { right: 0; }
#gallery-v2.gv2-live .gv2-thumbs-move {
  position: absolute;
  top: 50%;
  display: flex;
  flex-direction: row;
  height: var(--gv2-rail-h);
  will-change: transform;
}
/* The left strip is read from the frame outwards, so it hangs off the wrapper's
   inner edge; the right one starts at its own. */
#gallery-v2.gv2-live .gv2-thumbs.is-left .gv2-thumbs-move { left: 100%; }
#gallery-v2.gv2-live .gv2-thumbs.is-right .gv2-thumbs-move { left: 0; }
#gallery-v2.gv2-live .gv2-thumb {
  position: relative;
  flex: 0 0 auto;
  width: var(--gv2-rail-w);
  height: var(--gv2-rail-h);
  opacity: 0.86;
}
#gallery-v2.gv2-live .gv2-thumbs.is-left .gv2-thumb { margin-left: var(--gv2-rail-gap); }
#gallery-v2.gv2-live .gv2-thumbs.is-right .gv2-thumb { margin-right: var(--gv2-rail-gap); }
/* The project in the frame is the frame's business: its thumb is the one slot
   each strip does not show. */
#gallery-v2.gv2-live .gv2-thumbs.is-left .gv2-thumb:last-child,
#gallery-v2.gv2-live .gv2-thumbs.is-right .gv2-thumb:first-child { display: none; }
#gallery-v2.gv2-live .gv2-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The six articles are the static grid's; in the pinned rail the strips above
   carry the projects and these only hold the copy the editor writes. */
#gallery-v2.gv2-live .gv2-item { display: none; }

/* ── The two thumb strips ─────────────────────────────────────────────
   In the pinned rail the strip stops being a layout: it is the pinned box
   itself, and js/spotlight/rail.js puts each item where its distance from the
   frame says it belongs — a thumb on one of the two rails, or on its way
   through the frame, where the WebGL card takes over and the thumb is gone.
   The column gap is the rails' pitch and is read back from here, so the
   spacing stays one number in the cascade. */
#gallery-v2.gv2-live .gv2-strip {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: block;
  transform: none;
  padding: 0;
  column-gap: var(--gv2-rail-gap);
}
#gallery-v2.gv2-live .gv2-item {
  position: absolute;
  top: 50%;
  left: 0;
  width: var(--gv2-rail-w);
  opacity: 0;
}
#gallery-v2.gv2-live .gv2-box { aspect-ratio: var(--gv2-frame-aspect); }
/* Unlit: a thumb is a project waiting its turn, not one being shown. The
   fading in and out belongs to rail.js — this is only how bright it is. */
#gallery-v2.gv2-live .gv2-cover {
  opacity: 1;
  filter: brightness(0.86);
  transition: none;
}
/* rail.js owns loading here, so a thumb spends its first moments with no src
   at all. The box is already sized and the alt text is real, which is exactly
   what a browser paints in the gap. */
#gallery-v2.gv2-live .gv2-cover:not([src]) { visibility: hidden; }

/* Below this width the frame leaves no room beside it (mirrors RAIL_MIN in
   js/spotlight/units.js). The items keep their boxes — the rail reads its own
   pitch off them, so they are hidden rather than removed. */
@media (max-width: 639px) {
  #gallery-v2.gv2-live .gv2-item { visibility: hidden; }
}

/* Only what actually moves asks for a compositor layer, and only while the pin
   is live: the rail, the two glow clones and the light disc. */
#gallery-v2.gv2-live .gv2-item { will-change: transform, opacity; }
#gallery-v2.gv2-live .gv2-type-bloom,
#gallery-v2.gv2-live .gv2-type-halo { will-change: opacity; }
#gallery-v2.gv2-live .gv2-spill { will-change: transform, opacity; }

/* ── Item: flex-column wrapper — image box + caption as siblings ─── */
.gv2-item {
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: clamp(17.5em, 26vw, 26.25em);
  overflow: visible;
  background: transparent;
}

/* ── Image box — carries the aspect-ratio, clips the image ─────── */
/* Desktop: landscape 16/9; Mobile: portrait 9/13 (see media query) */
.gv2-box {
  position: relative;
  width: 100%;
  flex-shrink: 0;
  aspect-ratio: 16 / 9;
  border-radius: 0.25em;
  overflow: hidden;
}

/* Cover image fills the box */
.gv2-item .gv2-cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  opacity: 0.42;
  filter: brightness(0.7);
  transition: opacity 0.5s ease, filter 0.5s ease;
  z-index: 1;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

/* A context can intentionally leave a numbered project without media. A
   source-less image stays in the layout but must not paint a broken glyph. */
.gv2-cover:not([src]) { visibility: hidden; }

/* An item whose project lives at its own URL wraps its cover in a link —
   the image itself keeps pointer-events: none above, so the anchor is what
   actually receives the click. Only the static grid ever shows this box in
   layout; the pinned rail hides .gv2-item entirely and draws the card on
   its own WebGL layer. */
.gv2-img-link {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: block;
}
.gv2-img-link:not([href]) { pointer-events: none; }
.gv2-img-link:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: -2px;
}

/* ── Per-item caption — flex-sibling of .gv2-box, never inside it ──
   Only the static fallback shows these; in the pinned rail the shared
   caption under the frame is the single place a project is named. */
.gv2-item-cap {
  display: none;
  flex-direction: column;
  gap: 0.25em;
  padding: 0.75em 0 0;
  margin: 0;
  text-align: center;
  font-family: 'DM Sans', sans-serif;
  pointer-events: none;
}
#gallery-v2.gv2-static .gv2-item-cap {
  display: flex;
}
.gv2-item-cap-title {
  display: block;
  font-family: 'Cormorant', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 1.5em;
  line-height: 1.5;
  color: rgba(255,255,255,0.92);
}
.gv2-item-cap-type,
.gv2-cap-type {
  display: block;
  font-size: 0.625em;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--lime);
}
.gv2-item-cap-case,
.gv2-cap-case {
  color: inherit;
  font-size: 0.6875em;
  letter-spacing: 0.1em;
  text-decoration: underline;
  text-decoration-color: rgba(255,255,255,0.35);
  text-underline-offset: 0.2em;
  pointer-events: auto;
}
html[lang="fa"] .gv2-item-cap-type,
html[lang="fa"] .gv2-cap-type,
html[lang="fa"] .gv2-item-cap-case,
html[lang="fa"] .gv2-cap-case { font-family: 'YekanBakh', sans-serif; letter-spacing: 0; text-transform: none; }
.gv2-item-cap-sub {
  display: block;
  font-size: 0.75em;
  line-height: 1.6;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.5);
}
html[lang="fa"] .gv2-item-cap-title {
  font-family: 'YekanBakh', sans-serif;
  font-style: normal;
  letter-spacing: 0;
}
html[lang="fa"] .gv2-item-cap-sub {
  font-family: 'YekanBakh', sans-serif;
  letter-spacing: 0;
  text-transform: none;
}
/* Item 2's subtitle is a real link (see PART 3) — the caption around it is
   pointer-events: none, so the link itself has to opt back in. */
.gv2-item-cap-sub.gv2-sub-link {
  text-decoration: none;
  pointer-events: auto;
}
.gv2-item-cap-sub.gv2-sub-link:not([href]) { pointer-events: none; }
.gv2-item-cap-sub.gv2-sub-link:hover { text-decoration: underline; }
.gv2-item-cap-sub.gv2-sub-link:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 2px;
}

/* ── Shared spotlight caption (desktop only) ──────────────────────── */
.gv2-caption {
  position: absolute;
  /* Hangs off the frame's own height, so a layout that resizes the frame does
     not need a second number here. */
  top: calc(50% + var(--gv2-frame-h) / 2 + 0.65em);
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  text-align: center;
  pointer-events: none;
  font-family: 'DM Sans', sans-serif;
  width: min(80vw, 32.5em);
}
/* Numbers the project cards among themselves (01 / 08 … 08 / 08). The intro
   card carries `is-hidden` instead of text, so the caption collapses to two
   lines with no gap rather than leaving an empty numbered line above it. */
.gv2-cap-index {
  display: block;
  font-size: clamp(0.625em, 0.8vw, 0.75em);
  letter-spacing: 0.32em;
  color: rgba(255,255,255,0.45);
  margin-bottom: 0.625em;
  transition: opacity 0.25s ease;
}
.gv2-cap-index.is-hidden { display: none; }
.gv2-cap-type { margin-bottom: 0.5em; }
.gv2-item-cap-title[hidden],
.gv2-cap-title[hidden] { display: none; }
.gv2-cap-title {
  display: block;
  font-family: 'Cormorant', serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.25em, 2vw, 1.75em);
  color: rgba(255,255,255,0.95);
  margin-bottom: 0.375em;
  transition: opacity 0.25s ease;
}
.gv2-cap-sub {
  display: block;
  font-size: clamp(0.6875em, 0.9vw, 0.8125em);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.55);
  transition: opacity 0.25s ease;
}
html[lang="fa"] .gv2-cap-title { font-family: 'YekanBakh', sans-serif; font-style: normal; letter-spacing: 0; }
html[lang="fa"] .gv2-cap-sub   { font-family: 'YekanBakh', sans-serif; letter-spacing: 0; text-transform: none; }
html[lang="fa"] .gv2-cap-index { font-family: 'YekanBakh', sans-serif; letter-spacing: 0.1em; }

/* The shared subtitle becomes a real link when the active item carries
   data-sub-href (gallery-v2.js); its parent stays non-interactive so
   pointer-events must be re-enabled on the link itself. */
.gv2-cap-sub a,
.gv2-sub-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: rgba(255,255,255,0.35);
  text-underline-offset: 0.2em;
  pointer-events: auto;
}
.gv2-cap-sub a:hover,
.gv2-sub-link:hover { text-decoration-color: currentColor; }
.gv2-cap-sub a:focus-visible,
.gv2-sub-link:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 2px;
}
#gallery-v2.gv2-light .gv2-cap-sub a,
#gallery-v2.gv2-light .gv2-sub-link { text-decoration-color: rgba(0,0,0,0.3); }

.gv2-caption.is-swapping .gv2-cap-title,
.gv2-caption.is-swapping .gv2-cap-sub,
.gv2-caption.is-swapping .gv2-cap-index,
.gv2-caption.is-swapping .gv2-cap-type,
.gv2-caption.is-swapping .gv2-cap-case { opacity: 0; }

/* Empty state — spotlight has no item in it yet (start of section) */
.gv2-caption.is-empty .gv2-cap-title,
.gv2-caption.is-empty .gv2-cap-sub,
.gv2-caption.is-empty .gv2-cap-index,
.gv2-caption.is-empty .gv2-cap-type,
.gv2-caption.is-empty .gv2-cap-case { opacity: 0; }

/* ── Static fallback ──────────────────────────────────────────────────
   The designed no-motion / no-WebGL layout: no pin, no horizontal travel,
   all six projects laid out at once with their own titles. Added by
   gallery-v2.js (prefers-reduced-motion, no WebGL context, or a WebGL layer
   that failed to initialise), never by media query — the same markup is what
   the pinned rail transforms. */
/* The negative top margin exists to keep the pinned section's bg-transition
   scroll anchors where they were; unpinned there is nothing to preserve. */
#gallery-v2.gv2-static { margin-top: 0; }

#gallery-v2.gv2-static .gv2-pin {
  height: auto;
  overflow: visible;
  padding: 3em 0 5em;
}
#gallery-v2.gv2-static .gv2-spotlight,
#gallery-v2.gv2-static .gv2-caption,
#gallery-v2.gv2-static .gv2-scene {
  display: none;
}
#gallery-v2.gv2-static .gv2-strip {
  position: static;
  transform: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 3em 2em;
  width: auto;
  max-width: 84em;
  margin: 0 auto;
  padding: 0 3em;
  /* The grid is text, not a filmstrip: it follows the document direction. */
  direction: inherit;
}
#gallery-v2.gv2-static .gv2-item {
  width: auto;
}
/* The heading stays — it is the section's title, not decoration. It just sits
   above the grid at its docked size, with no glow and no light spill. */
#gallery-v2.gv2-static .gv2-spill,
#gallery-v2.gv2-static .gv2-type-bloom,
#gallery-v2.gv2-static .gv2-type-halo {
  display: none;
}
#gallery-v2.gv2-static .gv2-type {
  position: static;
  max-width: 84em;
  margin: 0 auto 3em;
  padding: 0 3em;
}
#gallery-v2.gv2-static .gv2-type-giant {
  font-size: calc(var(--gv2-giant-dock-em) * 1em);
}
#gallery-v2.gv2-static .gv2-cover {
  opacity: 1;
  filter: none;
  transition: none;
}

/* ── Mobile ───────────────────────────────────────────────────────── */
@media (max-width: 1023px) {
  #gallery-v2 { margin-top: -4vh; }

  /* The type sits ABOVE the frame here, sweep and dock alike: at this width the
     line crossing the object reads as a collision rather than as a crossing,
     and 6.2em is the sweeping line plus the caps line above it. */
  .gv2-type {
    --gv2-giant-em: 4.6;
    --gv2-giant-dock-em: 1;
    top: calc(50% - var(--gv2-frame-h) / 2 - 7.6em);
    left: 1.5em;
  }
  html[lang="fa"] .gv2-type { left: auto; right: 1.5em; }
  .gv2-type-caps { font-size: 1em; }

  /* Portrait again here: the narrow layout is a column — type above, frame,
     caption below — so the frame is bounded by what is left of the viewport's
     height after the other two, not by its width alone. 13em is the type block
     (7.6em) plus the caption (4.5em) plus their air; 78vw is the phone case,
     where width binds first. Height is the primary here and width follows it. */
  #gallery-v2 {
    --gv2-frame-aspect: calc(35.5 / 45.7);
    --gv2-frame-h: min(calc(78vw * 45.7 / 35.5), calc(94vh - 13em));
  }

  .gv2-item {
    width: clamp(11.25em, 50vw, 16.25em);
  }

  /* Portrait image box on mobile */
  .gv2-box {
    aspect-ratio: 9 / 13;
  }

  /* Tighter gap between items on small screens */
  .gv2-strip {
    gap: clamp(1.25em, 5vw, 2.25em);
    padding: 0 clamp(1em, 5vw, 2.5em);
  }

  .gv2-caption { width: min(88vw, 25em); }
  .gv2-cap-index { font-size: clamp(0.5625em, 2vw, 0.625em); margin-bottom: 0.5em; }
  .gv2-cap-title { font-size: clamp(1em, 4vw, 1.375em); }
  .gv2-cap-sub   { font-size: clamp(0.625em, 2.5vw, 0.75em); }

  #gallery-v2.gv2-static .gv2-strip {
    grid-template-columns: minmax(0, 1fr);
    gap: 2.5em;
    padding: 0 1.5em;
  }
}
