/* ==========================================================================
   BUILD STAGE — "IL METODO" — HUD, static fallback, responsive refinements

   Loaded after css/style.css and css/scroll-1..3.css, so it can refine the
   per-phase geometry those files establish without editing four places.

   Contents
     1. sr-only utility
     2. Phase HUD (counter + dots + skip)
     3. Mobile / compact-viewport layout rebalance
     4. Landscape phones
     5. Static fallback (prefers-reduced-motion, no-canvas, no-JS)
   ========================================================================== */

/* -----------------------------------------
   1. Screen-reader-only utility
   Used by the section heading, the dot labels and the live region.
   ----------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* -----------------------------------------
   2. PHASE HUD
   Tells the reader where they are (n / 4), lets them jump between phases,
   and lets them leave. Pinned inside the stage, so it travels with the
   pinned section rather than floating over the whole page.

   Discreet on desktop (bottom-left, low contrast until hovered) — the
   choreography there already reads as a sequence. Prominent and thumb-
   reachable on mobile, where orientation matters much more.
   ----------------------------------------- */
.build-stage {
  /* The HUD is absolutely positioned against the stage. */
  position: relative;
}

.gc-method-hud {
  position: absolute;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 1rem;
  /* Respect notches / home indicators on both axes. */
  bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
  left: calc(3rem + env(safe-area-inset-left, 0px));
  pointer-events: auto;
}

.gc-method-hud[hidden] {
  display: none;
}

.gc-method-counter {
  display: flex;
  align-items: baseline;
  gap: 0.15em;
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--color-gray-text);
}

.gc-method-counter-current {
  color: var(--color-offwhite);
}

.gc-method-counter-sep {
  opacity: 0.5;
}

.gc-method-dots {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.gc-method-dots li {
  display: flex;
}

/* Each dot is a real <button>: a bar that fills as it becomes active. The
   hit area is padded well past the visible bar for touch. */
.gc-method-dot {
  position: relative;
  width: 26px;
  height: 22px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.gc-method-dot::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 2px;
  transform: translateY(-50%);
  /* 0.45 alpha (not 0.28): as a non-text UI component the inactive track must
     reach 3:1 against the black stage. 0.28 measured 2.19:1; this measures
     3.42:1. */
  background: rgba(245, 245, 247, 0.45);
  transition: background-color 200ms cubic-bezier(0.23, 1, 0.32, 1);
}

.gc-method-dot.is-active::after {
  background: var(--color-magenta-glow);
}

.gc-method-dot:focus-visible {
  outline: 2px solid var(--color-magenta-glow);
  outline-offset: 3px;
  border-radius: 2px;
}

.gc-method-skip {
  padding: 0.5rem 0.9rem;
  /* 0.45 alpha for the same 3:1 UI-component reason as the dots above. */
  border: 1px solid rgba(245, 245, 247, 0.45);
  border-radius: 999px;
  background: rgba(6, 6, 8, 0.5);
  color: var(--color-gray-text);
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 200ms cubic-bezier(0.23, 1, 0.32, 1),
              border-color 200ms cubic-bezier(0.23, 1, 0.32, 1);
}

.gc-method-skip:focus-visible {
  outline: 2px solid var(--color-magenta-glow);
  outline-offset: 2px;
}

/* Hover motion only where hovering is real (touch fires false hovers). */
@media (hover: hover) and (pointer: fine) {
  .gc-method-dot:hover::after {
    background: rgba(245, 245, 247, 0.6);
  }
  .gc-method-dot.is-active:hover::after {
    background: var(--color-magenta-glow);
  }
  .gc-method-skip:hover {
    color: var(--color-offwhite);
    border-color: rgba(245, 245, 247, 0.5);
  }
}

/* -----------------------------------------
   2b. DESKTOP COPY GUTTERS — overflow fix

   The flanking copy columns of phases 3 and 4 were positioned `left: 0` /
   `right: 0` against .reveal-layer-{2,3}-content. That box is the full
   viewport width with its own 6rem padding, but `left/right: 0` on an
   absolutely-positioned child resolves against the PADDING BOX EDGE, not the
   content edge — so the columns sat flush against the viewport sides, and the
   36px horizontal entrance transform (x: 36 * (1 - progress), applied by
   scroll-2.js / scroll-3.js) then pushed the right-hand column several
   pixels off-screen. Measured: .reveal-sub reached x=1445 in a 1440 viewport.

   Insetting both columns by the same 6rem the content box uses puts them back
   inside the layout gutter and leaves room for the entrance travel.
   ----------------------------------------- */
@media (min-width: 769px) {
  .reveal-copy-2-left,
  .reveal-copy-3-left {
    left: 6rem;
  }

  .reveal-copy-2-right,
  .reveal-copy-3-right {
    right: 6rem;
  }

  /* Phase 2's single right-hand column is inside a .reveal-copy wrapper that
     is itself already inset by the content padding, so only the sub's own
     entrance travel needs containing. */
  .reveal-copy {
    padding-right: 0;
  }
}

/* -----------------------------------------
   2c. TABLET / NARROW DESKTOP (769px - 1024px) — STACKED COMPOSITION

   These widths inherited the full desktop three-column choreography, which
   only works when there is width to spare. At 834px it was badly broken even
   before this pass: the phase-3/4 flanking columns collapsed to ~183px, so
   .reveal-headline started at x=0 (no gutter at all), ran underneath the
   centred canvas and its last line ("REALIZZARLO.") was completely obscured
   by the render. Verified against the pre-change build, so this is a fix, not
   a regression introduced here.

   Three narrow columns cannot be made to work at this width, so tablets get
   the MOBILE composition instead — copy above and below a centred canvas —
   which is designed for exactly this constraint. The canvas slot geometry is
   deliberately left alone: scroll-2.js derives the object's travel from the
   content width assuming a 50%-wide slot, so resizing the slot desynchronises
   the hand-offs and the object visibly jumps and resizes between phases
   (measured: 50-61px width jump). Only the COPY is repositioned.
   ----------------------------------------- */
@media (min-width: 769px) and (max-width: 1024px) {
  :root {
    --gc-tab-canvas-w: 62vw;
    --gc-tab-canvas-h: calc(62vw * 805 / 1440);
    --gc-tab-gap: 30px;
  }

  .build-stage-content,
  .reveal-layer-content,
  .reveal-layer-2-content,
  .reveal-layer-3-content {
    padding: 0 3rem;
  }

  /* Constrain the visible render to the middle 62vw so it cannot reach the
     copy above/below it. The SLOT keeps its 50% width (travel math depends on
     it); only the canvas inside it is capped. */
  #build-canvas,
  #reveal-canvas,
  #reveal-canvas-2,
  #reveal-canvas-3 {
    max-width: var(--gc-tab-canvas-w);
    max-height: none;
    margin: 0 auto;
  }

  /* Re-centre every slot on the page. On desktop phases 1/2 anchor to the
     right half and phase 3 to the left half, because the copy sits beside the
     object; with the copy stacked above/below there is no side column to
     balance against, so all four phases share one centred object position.
     Widths are untouched (still 50%), so the pixel-aligned hand-offs hold —
     and scroll-1/2.js suppress their horizontal travel at these widths
     (isStackedLayout), so nothing drifts off this centre. */
  .build-canvas-container,
  .reveal-object-slot,
  .reveal-object-slot-2 {
    left: 25%;
  }

  /* Lead (eyebrow + headline) above the canvas box, detail below — the same
     anchoring the mobile layout uses, so the four phases stay consistent. */
  .copy-lead,
  .reveal-copy-2-left,
  .reveal-copy-3-left {
    position: absolute;
    top: auto;
    left: 3rem;
    right: 3rem;
    width: auto;
    max-width: none;
    bottom: calc(50% + var(--gc-tab-canvas-h) / 2 + var(--gc-tab-gap));
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    transform: none;
  }

  .copy-detail,
  .reveal-copy-2-right,
  .reveal-copy-3-right {
    position: absolute;
    bottom: auto;
    left: 3rem;
    right: 3rem;
    width: auto;
    max-width: none;
    top: calc(50% + var(--gc-tab-canvas-h) / 2 + var(--gc-tab-gap));
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    transform: none;
  }

  /* Phase 1 and 2 keep .build-copy / .reveal-copy as positioned wrappers on
     desktop; dissolve them so the lead/detail groups anchor to the stage box
     exactly like phases 3 and 4. */
  .build-copy,
  .reveal-copy {
    display: contents;
  }

  .build-sub,
  .reveal-sub {
    margin-left: auto;
    margin-right: auto;
    font-size: 0.95rem;
    line-height: 1.55;
    max-width: 46ch;
    text-wrap: pretty;
  }

  .build-headline,
  .reveal-headline {
    font-size: clamp(1.8rem, 4.4vw, 2.6rem);
    line-height: 1.12;
    text-wrap: balance;
  }

  .build-eyebrow,
  .reveal-eyebrow {
    font-size: 0.72rem;
    letter-spacing: 0.26em;
    margin-bottom: 0.8rem;
  }

  .gc-method-hud {
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(1.6rem + env(safe-area-inset-bottom, 0px));
  }
}

/* -----------------------------------------
   3. MOBILE LAYOUT REBALANCE (<= 768px)

   The baseline mobile composition was top-heavy: the copy + canvas block sat
   optically high with a large dead band under the body text, because the
   canvas box was centred on the viewport and both copy groups were hung off
   it symmetrically while the HUD did not exist.

   Here the three parts (lead / canvas / detail) are treated as ONE optical
   block, nudged up only enough to leave room for the HUD, with the canvas
   given a slightly larger share of the width and the type scale tightened so
   phases 2-4 (longer headlines) never collide with it.
   ----------------------------------------- */
@media (max-width: 768px) {
  :root {
    /* Canvas is a touch wider than before (92% -> 94%) and its height is
       derived from the real frame aspect, so the copy anchors stay exact. */
    --gc-canvas-w: 94vw;
    --gc-canvas-h: calc(94vw * 805 / 1440);
    --gc-copy-gap: 22px;
    --gc-copy-inset: 1.35rem;
    /* Optical lift: the whole block sits slightly above the true centre so
       the HUD at the bottom does not crowd the body copy. Scales down to 0
       on short viewports, where there is no spare room to give away. */
    --gc-block-lift: min(4.5vh, max(0px, (100vh - 700px) / 4));
  }

  .build-stage {
    /* Dynamic viewport unit: on mobile browsers with collapsing URL bars,
       100vh overflows and pushes the HUD off-screen. dvh tracks the real
       visible height. Falls back to vh where dvh is unsupported. */
    min-height: 100vh;
    min-height: 100dvh;
  }

  /* Shift the canvas and both copy groups up together by the same amount, so
     their relative spacing (the part that was carefully tuned) is preserved
     while the block as a whole is re-centred. */
  .build-canvas-container,
  .reveal-object-slot,
  .reveal-object-slot-2,
  .reveal-object-slot-3 {
    margin-top: calc(-1 * var(--gc-block-lift));
  }

  .build-canvas-container,
  .reveal-object-slot,
  .reveal-object-slot-2 {
    width: var(--gc-canvas-w);
    margin-left: calc(-1 * var(--gc-canvas-w) / 2);
  }

  .reveal-object-slot-3 {
    width: var(--gc-canvas-w);
  }

  /* Copy anchors follow the same lift so the gap to the canvas is unchanged. */
  .copy-lead,
  .reveal-copy-2-left,
  .reveal-copy-3-left {
    bottom: calc(50% + var(--gc-canvas-h) / 2 + var(--gc-copy-gap) + var(--gc-block-lift));
  }

  .copy-detail,
  .reveal-copy-2-right,
  .reveal-copy-3-right {
    top: calc(50% + var(--gc-canvas-h) / 2 + var(--gc-copy-gap) - var(--gc-block-lift));
  }

  /* The step-1 lead had its own extra height-aware offset in style.css; the
     shared --gc-block-lift now does that job for all four phases, so reset
     it here to keep the four phases optically identical. */
  .copy-lead {
    bottom: calc(50% + var(--gc-canvas-h) / 2 + var(--gc-copy-gap) + var(--gc-block-lift));
  }

  /* Type scale: one scale for all four phase headlines so no phase reads as
     an outlier, sized to fit the longest one ("OGNI DETTAGLIO, PRIMA DI
     REALIZZARLO.") in 3 lines without touching the canvas. */
  .build-headline,
  .reveal-headline {
    font-size: clamp(1.6rem, 6.6vw, 2.15rem);
    line-height: 1.12;
    text-wrap: balance;
  }

  .build-eyebrow,
  .reveal-eyebrow {
    font-size: 0.66rem;
    letter-spacing: 0.28em;
    margin-bottom: 0.7rem;
  }

  .build-sub,
  .reveal-sub {
    font-size: 0.92rem;
    line-height: 1.55;
    max-width: 32ch;
    text-wrap: pretty;
  }

  /* HUD: centred, above the home indicator, comfortably tappable. */
  .gc-method-hud {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    bottom: calc(1.1rem + env(safe-area-inset-bottom, 0px));
    gap: 0.8rem;
  }

  .gc-method-dot {
    width: 22px;
    height: 26px;
  }

  .gc-method-skip {
    padding: 0.45rem 0.8rem;
    font-size: 0.66rem;
  }
}

/* Short phones (<= 620px tall): no spare vertical room at all. Drop the
   lift entirely, tighten the type and shrink the canvas share so the lead,
   canvas, body and HUD all still fit without clipping or overlap. */
@media (max-width: 768px) and (max-height: 620px) {
  :root {
    --gc-canvas-w: 82vw;
    --gc-canvas-h: calc(82vw * 805 / 1440);
    --gc-copy-gap: 14px;
    --gc-block-lift: 0px;
  }

  .build-headline,
  .reveal-headline {
    font-size: clamp(1.32rem, 5.4vw, 1.7rem);
    line-height: 1.06;
  }

  .build-eyebrow,
  .reveal-eyebrow {
    font-size: 0.6rem;
    margin-bottom: 0.45rem;
  }

  .build-sub,
  .reveal-sub {
    font-size: 0.8rem;
    line-height: 1.4;
    max-width: 34ch;
  }

  .gc-method-hud {
    bottom: calc(0.6rem + env(safe-area-inset-bottom, 0px));
    gap: 0.6rem;
  }
}

/* -----------------------------------------
   4. LANDSCAPE PHONES
   Very little height, plenty of width: go back to a side-by-side reading —
   canvas left, copy right — instead of stacking, which would clip.
   ----------------------------------------- */
@media (max-width: 900px) and (max-height: 500px) and (orientation: landscape) {
  :root {
    --gc-canvas-w: 52vw;
    --gc-canvas-h: calc(52vw * 805 / 1440);
    --gc-block-lift: 0px;
  }

  .build-canvas-container,
  .reveal-object-slot,
  .reveal-object-slot-2 {
    left: 2vw;
    width: 52vw;
    margin-left: 0;
    margin-top: 0;
  }

  .reveal-object-slot-3 {
    left: 2vw;
    width: 52vw;
    transform: translateY(-50%);
    margin-top: 0;
  }

  /* Copy stacks in the right-hand column, vertically centred. */
  .copy-lead,
  .copy-detail,
  .reveal-copy-2-left,
  .reveal-copy-2-right,
  .reveal-copy-3-left,
  .reveal-copy-3-right {
    position: absolute;
    left: auto;
    right: calc(2.5vw + env(safe-area-inset-right, 0px));
    width: 40vw;
    max-width: 40vw;
    align-items: flex-start;
    text-align: left;
  }

  .copy-lead,
  .reveal-copy-2-left,
  .reveal-copy-3-left {
    top: 16%;
    bottom: auto;
  }

  .copy-detail,
  .reveal-copy-2-right,
  .reveal-copy-3-right {
    top: 52%;
    bottom: auto;
  }

  .build-headline,
  .reveal-headline {
    font-size: clamp(1.1rem, 3.4vw, 1.5rem);
    line-height: 1.08;
  }

  .build-eyebrow,
  .reveal-eyebrow {
    font-size: 0.56rem;
    margin-bottom: 0.35rem;
  }

  .build-sub,
  .reveal-sub {
    font-size: 0.76rem;
    line-height: 1.4;
    max-width: 38ch;
  }

  .gc-method-hud {
    left: auto;
    transform: none;
    right: calc(2.5vw + env(safe-area-inset-right, 0px));
    bottom: calc(0.7rem + env(safe-area-inset-bottom, 0px));
  }
}

/* -----------------------------------------
   5. STATIC FALLBACK
   The same four phases as a plain editorial list — no pin, no scrub, no
   canvas, no auto-scroll. Shown when JS is absent (default state below),
   when prefers-reduced-motion is set, or when the canvas is unavailable
   (js/main.js adds .gc-method-is-static in both those cases).
   ----------------------------------------- */

/* Default = visible, so a no-JS reader gets the full message. main.js hides
   it as soon as it has decided the animated path is viable. */
.gc-method-static {
  display: grid;
  gap: clamp(3rem, 7vw, 5.5rem);
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding: clamp(3rem, 8vw, 6rem) clamp(1.35rem, 5vw, 3rem);
  list-style: none;
  counter-reset: gc-method;
}

/* JS present and the animated path chosen: the static list is redundant. */
.js-build-animated .gc-method-static {
  display: none;
}

.gc-method-static-step {
  display: grid;
  gap: 1rem;
  justify-items: start;
  text-align: left;
}

.gc-method-static-eyebrow {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--color-magenta-glow);
}

.gc-method-static-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(1.6rem, 4.2vw, 2.6rem);
  font-weight: 300;
  line-height: 1.14;
  text-transform: uppercase;
  color: var(--color-offwhite);
  text-wrap: balance;
}

.gc-method-static-img {
  width: 100%;
  height: auto;
  aspect-ratio: 1440 / 805;
  object-fit: contain;
  background: #000;
}

/* The phone version of the same box, holding a clip instead of a still (see
   buildVideoMode in js/main.js). Same class, so it inherits everything above;
   these three lines only undo the things a <video> brings with it. */
.gc-method-static-video {
  display: block;
  /* The clip is a 3D render on a black field, not a UI element: nothing
     should be tappable or long-pressable on it. */
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}

/* On a phone the four phases read as one sequence, so they are tightened up
   and the type is set at the section's mobile scale rather than the
   fallback's. This block is only active when JS chose the video path. */
@media (max-width: 768px) {
  /* One phase per screenful. The gap is what does it: each step is
     eyebrow + title + clip + body ~ 78% of a phone viewport, so a gap near a
     quarter of the viewport puts the next phase's eyebrow just below the
     fold. That is also what keeps a single clip in view at a time, which is
     what the playback arbiter above is built around. */
  .build-stage.gc-method-is-video .gc-method-static {
    gap: clamp(2.4rem, 22svh, 5rem);
    padding: clamp(3rem, 12svh, 5rem) 1.35rem clamp(3.5rem, 14svh, 6rem);
  }

  .build-stage.gc-method-is-video .gc-method-static-step {
    gap: 0.7rem;
  }

  /* The clip is the subject of its step: give it the full column and let the
     copy sit tight above and below it, the way the pinned version composed
     the phase. */
  .build-stage.gc-method-is-video .gc-method-static-video {
    margin: 0.35rem 0;
  }

  .build-stage.gc-method-is-video .gc-method-static-title {
    font-size: clamp(1.5rem, 6.4vw, 2rem);
    line-height: 1.12;
  }

  .build-stage.gc-method-is-video .gc-method-static-eyebrow {
    font-size: 0.66rem;
    letter-spacing: 0.26em;
  }

  .build-stage.gc-method-is-video .gc-method-static-body {
    font-size: 0.92rem;
    line-height: 1.55;
  }
}

.gc-method-static-body {
  margin: 0;
  max-width: 62ch;
  font-family: var(--font-sans);
  font-size: clamp(0.95rem, 1.6vw, 1.05rem);
  font-weight: 300;
  line-height: 1.6;
  color: var(--color-gray-text);
  text-wrap: pretty;
}

/* When the static list is the live version, the stage must not try to be a
   pinned 100vh viewport-filling scene: it becomes an ordinary flow section
   and the animated layers are removed from the box entirely. */
.build-stage.gc-method-is-static {
  display: block;
  height: auto;
  min-height: 0;
  align-items: initial;
  justify-content: initial;
  background: var(--color-bg-dark);
}

.build-stage.gc-method-is-static .build-stage-content,
.build-stage.gc-method-is-static .reveal-layer,
.build-stage.gc-method-is-static .reveal-layer-2,
.build-stage.gc-method-is-static .reveal-layer-3 {
  display: none;
}

/* Desktop grid for the static list: image and text side by side, alternating,
   so it reads as an editorial method page rather than a tall column. */
@media (min-width: 769px) {
  .gc-method-static-step {
    grid-template-columns: 1.1fr 1fr;
    grid-template-areas:
      "media eyebrow"
      "media title"
      "media body";
    align-content: center;
    align-items: center;
    column-gap: clamp(2rem, 5vw, 4rem);
    row-gap: 0.9rem;
  }

  .gc-method-static-step:nth-child(even) {
    grid-template-areas:
      "eyebrow media"
      "title media"
      "body media";
    grid-template-columns: 1fr 1.1fr;
  }

  .gc-method-static-img {
    grid-area: media;
  }
  .gc-method-static-eyebrow {
    grid-area: eyebrow;
    align-self: end;
  }
  .gc-method-static-title {
    grid-area: title;
  }
  .gc-method-static-body {
    grid-area: body;
    align-self: start;
  }
}

/* Reduced motion: strip the transitions the HUD would otherwise run. The
   static list has no motion of its own, so there is nothing else to disable. */
@media (prefers-reduced-motion: reduce) {
  .gc-method-dot::after,
  .gc-method-skip {
    transition: none;
  }
}

/* ==========================================================================
   6. MOBILE PIN — CSS sticky runway
   ==========================================================================

   The section used to be held in place on phones by GSAP's pin, which
   translates the pinned element from JavaScript on every scroll event. On a
   phone the page scrolls on the compositor thread and scroll events reach JS
   late and coalesced (badly so during momentum), so the pinned content chases
   the scroll a frame behind and visibly shivers.

   `position: sticky` is pinned BY the compositor. No JavaScript participates
   in holding it still, so it cannot shiver. js/main.js only reads the scroll
   position once per animation frame to pick which sequence frame to paint.

   Structure (the wrapper is created by main.js, once, at setup):

     div.gc-method-scroller        <- the runway, (1 + total) viewports tall
       section#build-stage         <- sticky, exactly one viewport tall

   Heights are in `svh` (smallest viewport height) deliberately. `dvh` tracks
   the collapsing URL bar, which would resize the sticky box — and therefore
   relayout four canvases — continuously while the reader scrolls. `svh` is
   constant for a given orientation, so the box is measured once and never
   moves. When the URL bar hides, the few dozen pixels revealed below the
   sticky box are the runway's own background, i.e. the same black.
   -------------------------------------------------------------------- */

/* Desktop / tablet: the wrapper is completely inert and GSAP pins the section
   inside it exactly as before. Declared outside the media query so it is
   unambiguous that the element has no effect above 768px. */
.gc-method-scroller {
  position: relative;
}

/* The sticky mechanism is keyed on the .gc-method-sticky class that main.js
   adds, NOT on a width breakpoint — main.js chooses by pointer type, so an
   iPad and a phone in landscape get it too (see the matchMedia queries there).
   Keeping the CSS keyed on the class means the two never disagree. */

/* `overflow-x: hidden` on html/body makes body a scroll container, which
   silently disables position:sticky on every descendant. `clip` prevents
   horizontal overflow just as effectively WITHOUT creating a scroll
   container. The `hidden` line stays first so browsers without `clip`
   (Safari < 16) keep today's behaviour — there the section simply is not
   sticky and the sequence plays as it scrolls past, which degrades
   gracefully rather than breaking. Scoped to the sticky mode so nothing
   else on the site changes. */
html.gc-method-sticky {
  /* Root-element overflow PROPAGATES to the viewport, so this clips the
     page horizontally without turning any element into a scroll container.
     Sticky descendants stick to the viewport, which is what we want. */
  overflow-x: hidden;
}

html.gc-method-sticky body {
  /* Explicitly visible, and it must STAY visible.

     The previous version of this rule set `overflow-x: hidden; overflow-x:
     clip` on body, relying on `clip` to win. `clip` is the only value that
     clips WITHOUT creating a scroll container — but where it is not honoured
     (older WebKit / iOS Safari builds drop the declaration as invalid) the
     `hidden` line survives, body computes `overflow-y: auto`, body becomes a
     scroll container, and every `position: sticky` inside it stops sticking.

     That is exactly what happened on iOS: #build-stage never pinned, so the
     4.6-viewport runway below it played as ~4 screens of empty black and
     scrolling appeared to do nothing. Reproduced 1:1 by forcing `hidden`
     here in a mobile emulation.

     There is nothing to clip on body anyway — html above already clips the
     page. */
  overflow-x: visible;
  overflow-y: visible;
}

html.gc-method-sticky .gc-method-scroller {
  /* --gc-scroller-vh is set by main.js from the phase weights
     (1 + 0.95 + 0.8 + 0.8 + 1.05 = 4.6 on phones), so the runway length stays
     derived from the pacing manifest in js/build-phases.js. */
  height: calc(100vh * var(--gc-scroller-vh, 4.6));
  height: calc(100svh * var(--gc-scroller-vh, 4.6));
}

html.gc-method-sticky .gc-method-scroller > .build-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  min-height: 0;
  /* Own stacking/paint context: keeps the four absolutely-positioned phase
     layers from being promoted against the whole page. */
  isolation: isolate;
}

@media (max-width: 768px) {
  /* The vertical rhythm inside the stage is measured against the stage's own
     height, which is now svh — keep the lift on the same unit so the copy
     anchors do not drift when the URL bar collapses. */
  :root {
    --gc-block-lift: min(4.5vh, max(0px, (100vh - 700px) / 4));
    --gc-block-lift: min(4.5svh, max(0px, (100svh - 700px) / 4));
  }

  /* Only one phase layer is visible at a time (main.js toggles autoAlpha), so
     the hidden ones must not be rasterized. */
  .reveal-layer,
  .reveal-layer-2,
  .reveal-layer-3,
  .build-canvas-container {
    backface-visibility: hidden;
  }

  /* The canvas backing store is sized in JS to the element's CSS box times a
     DPR capped at 2 (see js/build-phases.js). Let the browser scale the
     result with its normal smoothing — nearest-neighbour would alias the
     render badly at these sizes. */
  #build-canvas,
  #reveal-canvas,
  #reveal-canvas-2,
  #reveal-canvas-3 {
    image-rendering: auto;
  }
}

/* Landscape phones need nothing extra: the runway and the sticky box are both
   expressed in svh, so they follow the (much shorter) landscape viewport
   automatically and the four phases still occupy the same number of
   screenfuls rather than becoming a very long scroll. */

/* Reduced motion / no-canvas: main.js never creates the controller and never
   adds .gc-method-sticky, so the wrapper stays a plain block and the static
   four-step list flows normally. Nothing to undo here — this comment exists
   so the next reader does not go looking for an override. */

/* ==========================================================================
   7. DESKTOP — the flanking copy columns of phases 3 and 4
   ==========================================================================

   Phases 3 and 4 put the object in the middle of the page with copy on both
   sides. Two things made the left headline unreadable there:

   1. The frames were opaque. Every frame is a 1280x720 rectangle of which
      ~63% is pure #000, and with no alpha channel that black is painted —
      so the canvas was a solid black box sitting on top of the headline.
      Fixed in the assets, not here: assets/animations/scroll-N/webp-alpha/
      restores the transparency the renders were authored with, and
      js/build-phases.js serves that set to desktop.

   2. The headline overflowed its own column. `.reveal-copy-2-left` is 22% of
      the 1440px layer = 317px, but "REALIZZARLO." set at clamp(2.5rem, 6vh,
      4.2rem) is 385px wide as a single unbreakable word — so the <h3> spilled
      68px past its column and ran under the object regardless of alpha.

   The fix below sizes the column to the gutter that actually exists (the
   object's visible content starts ~12.7% into its canvas box, so there is
   ~334px of clear space at 1440px and more above that) and caps the headline
   so its longest word fits inside it. The cap is what matters: the column
   width is a percentage of a layer that stops growing at 1440px, while the
   old font-size was in `vh` and kept growing — on a tall 1920 screen the
   headline outran a column that had stopped getting wider. Both are bounded
   now, so the relationship holds at every desktop size.
   -------------------------------------------------------------------- */
@media (min-width: 1025px) {
  .reveal-copy-2-left,
  .reveal-copy-2-right,
  .reveal-copy-3-left,
  .reveal-copy-3-right {
    width: 24%;
    max-width: 330px;
  }

  /* Bounded by rem, not by vh: the column it has to fit inside is bounded
     too. 2.85rem keeps "REALIZZARLO." at ~325px, just inside the 330px
     column, at every viewport height. */
  .reveal-copy-2-left .reveal-headline,
  .reveal-copy-3-left .reveal-headline {
    font-size: clamp(1.9rem, 4.7vh, 2.85rem);
    line-height: 1.1;
    /* Belt and braces: if a future copy edit introduces a word longer than
       the column, break it rather than let it slide under the render. */
    overflow-wrap: break-word;
  }
}

/* 1025-1400px: the layer has not reached its 1440px max-width yet, so the
   gutter left of the centred object is proportionally tighter. Measured
   clearance between the headline and the first non-transparent pixel of the
   render at 1280px was -12px with the values above; these bring it positive
   without touching anything wider. */
@media (min-width: 1025px) and (max-width: 1400px) {
  .reveal-copy-2-left,
  .reveal-copy-3-left {
    max-width: 288px;
  }

  .reveal-copy-2-left .reveal-headline,
  .reveal-copy-3-left .reveal-headline {
    font-size: clamp(1.75rem, 4.3vh, 2.5rem);
  }
}

/* ==========================================================================
   8. MOBILE — the canvas must not be a visible rectangle
   ==========================================================================

   The phone frames are opaque (see js/build-phases.js for why an alpha plane
   is not worth ~4.5MB there). Their background used to be pure #000; the page
   behind them is not — the stage gradient settles at var(--color-bg-dark),
   #060608. Six levels of difference is nothing on a photograph and plainly a
   visible rectangle on a section this dark.

   Fixed in the ASSET, not here: gcalpha.py composites the mobile set over
   #060608 instead of over black (`--bg 060608`), so the frame's own margin is
   already the page colour and there is nothing to hide.

   The obvious CSS answer — mix-blend-mode: lighten on the layer, which makes
   pure black composite as a no-op — does work visually, and was measured:
   median frame time went from 16.7ms to 33.3ms and long tasks from 10 to 32.
   Blending a full-screen layer against its backdrop every frame is not free.
   Rejected on that basis. If the page background behind the canvas ever
   changes, regenerate the mobile set with the new colour rather than
   reaching for a blend mode.
   -------------------------------------------------------------------- */

/* ==========================================================================
   9. PHASE LOOPS — the idle clip at the end of a phase
   ==========================================================================

   A loop sits ON TOP of the phase's canvas and shows the same picture the
   canvas is already showing, so it can appear and disappear by opacity alone
   with nothing to see at either end. js/main.js arms it only when the scroll
   has been still for ~180ms and drops it on the first movement.

   Geometry is set in JS, in pixels, from the canvas's own bounding box —
   #build-canvas is sized by five breakpoint blocks plus a scale(1.02) seam
   fix, and restating that here would be a second copy to keep in sync. All
   this rule owns is the stacking, the fit and the fade.

   `object-fit: contain` reproduces exactly what the painter does with the
   same 16:9 source inside the same box, so the loop's first frame lands
   pixel-on-pixel over the canvas's last frame.

   pointer-events: none — the video must never eat a scroll gesture on a
   phone, which is precisely where it is most likely to be on screen.

   The two fades are different lengths on purpose. Coming IN, the clip has to
   cross 6.3 grey levels of difference from the canvas still it replaces (it
   is a re-render of the scene, not a continuation of the same render), so it
   takes 480ms and an ease-out — long enough to read as a settle rather than a
   flicker. Going OUT it takes 220ms: that fade fires because the reader has
   started scrolling again, and anything still dissolving there is in the way.
   Both durations are mirrored by LOOP_IN_MS / LOOP_OUT_MS in js/main.js.
   -------------------------------------------------------------------- */
.gc-loop-video {
  position: absolute;
  opacity: 0;
  object-fit: contain;
  pointer-events: none;
  /* Give the compositor a layer to cross-fade instead of repainting the
     stack underneath it. */
  will-change: opacity;
  transition: opacity 220ms linear;              /* LOOP_OUT_MS */
}

.gc-loop-video.is-on {
  opacity: 1;
  transition: opacity 480ms cubic-bezier(0.22, 1, 0.36, 1);   /* LOOP_IN_MS */
}

@media (prefers-reduced-motion: reduce) {
  /* main.js never arms a loop here; this is the belt-and-braces half, so a
     clip cannot be left up by a mid-session preference change. */
  .gc-loop-video { display: none; }
}

/* ==========================================================================
   10. LOOP LABELS — the words that came out of the video
   ==========================================================================

   scroll-1's loop names the four zones of the plan. Those words were burnt
   into the master at a 12px cap height on a 1280 frame, which is 6.9 CSS px
   at the clip's desktop size and 3.4 on a phone. gcloop.py paints them out
   (MASKS) and they are re-set here as real HTML, so they are crisp at any
   pixel ratio and can be a different size on each breakpoint. The leader
   lines and their dots stay in the video underneath.

   The overlay box is positioned in JS onto the clip's DRAWN rectangle, and
   --gc-loop-w carries that rectangle's width, so every measurement below can
   be a share of the picture rather than of the viewport.
   -------------------------------------------------------------------- */
.gc-loop-labels {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
  /* No transition: opacity is driven per frame from the video's own
     currentTime (labelTick in main.js), because the clip loops natively and
     a CSS animation of the same nominal length drifts against it — after a
     few minutes parked the words would arrive before their lines. */
}

.gc-loop-label {
  position: absolute;
  /* The row the leader line occupies. The caps sit above it and the detail
     below, which is how the master composed them. */
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  text-transform: uppercase;
}

.gc-loop-label--l { left: var(--gc-anchor); max-width: 10.6%; align-items: flex-start; text-align: left; }
.gc-loop-label--r { right: var(--gc-anchor); max-width: 8.0%; align-items: flex-end; text-align: right; }

/* The gap in the middle is where the video's own leader line passes. */
.gc-loop-cap {
  font-family: var(--font-sans);
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--color-magenta-glow);
  font-size: max(11px, calc(var(--gc-loop-w) * 0.0205));
  margin-bottom: calc(var(--gc-loop-w) * 0.011);
}

.gc-loop-sub {
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--color-gray-text);
  font-size: max(9px, calc(var(--gc-loop-w) * 0.0136));
  margin-top: calc(var(--gc-loop-w) * 0.013);
}

@media (max-width: 768px) {
  /* The dark columns beside the plan are 73px and 65px wide on a 390px
     phone. Anchoring to the leader line's dot would leave 40px, and
     "preparazione" needs 64px at the smallest size that is still type rather
     than texture. So the labels take the whole column and the line simply
     runs out from under them. */
  .gc-loop-label--l { left: 1.4%; max-width: 18.4%; }
  .gc-loop-label--r { right: 1.4%; max-width: 16.4%; }

  .gc-loop-cap { letter-spacing: 0.1em; }
  .gc-loop-sub { letter-spacing: 0.04em; }
}

@media (prefers-reduced-motion: reduce) {
  .gc-loop-labels { display: none; }   /* no loop runs here at all */
}
