/* ==========================================================================
   ALLA Arquitectura
   Design system, built from laGràfica's homepage mockup (1920 × 4600).
   Every fixed number below was measured off that artwork, not estimated.

   The generative rule of this design: every diagonal is the angle of the
   letter A in the studio's own wordmark — 21.877° off vertical, from the
   polygon "56.12,0 107.31,127.49" in alla.svg. The hero band measures
   21.884°. They are the same line.
   ========================================================================== */

/* --------------------------------------------------------------- 1. Fonts */

@font-face {
  font-family: 'Montserrat';
  src: url('../fonts/montserrat-var-latin.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Montserrat';
  src: url('../fonts/montserrat-var-latin-ext.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* -------------------------------------------------------------- 2. Tokens */

:root {
  /* Colour. The client's brief fixes this: "COLORS PRINCIPALS: Blanc - Negre". */
  --bg: #ffffff;
  --bg-pure: #1b1b1b;
  --bg-elevated: #f4f4f4;
  --accent: #1b1b1b;
  --text-primary: #1b1b1b;
  --text-secondary: #5a5a5a;
  --text-tertiary: #8a8a8a;
  --border: #1b1b1b;
  --border-faint: #e0e0e0;
  --on-dark: #ffffff;
  --on-dark-dim: #b4b4b4;
  --deco: #434242;          /* footer stripe + circle, sampled from the mockup */

  /* The diagonal. Do not round this — it is the wordmark's own angle. */
  --diag: 21.877deg;

  /* Layout. 1714 = 1920 − 2×103; three 558 columns and two 20 gaps. */
  --container: 1714px;
  --container-inset: 73px;  /* dark band + footer text sit at 176, not 103 */
  --gutter: clamp(20px, 5.365vw, 103px);
  --col-gap: clamp(8px, 1.04vw, 20px);

  /* Spacing — 4px base */
  --sp-1: 4px;   --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;
  --sp-5: 20px;  --sp-6: 24px;  --sp-8: 32px;  --sp-10: 40px;
  --sp-12: 48px; --sp-14: 56px; --sp-16: 64px; --sp-20: 80px;
  --sp-24: 96px; --sp-32: 128px;

  /* Section rhythm. The mockup's intro band pads 59px top and bottom. */
  --sp-section: clamp(40px, 3.07vw, 59px);
  --sp-section-lg: clamp(56px, 6.82vw, 131px);

  /* Type */
  --font: 'Montserrat', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --fs-lead: clamp(1.5rem, 2.083vw, 2.5rem);        /* 40px @1920 */
  --fs-h1: clamp(2rem, 3.33vw, 4rem);               /* 64px @1920 */
  --fs-h2: clamp(1.625rem, 2.083vw, 2.5rem);
  --fs-h3: clamp(1.125rem, 1.25vw, 1.5rem);
  --fs-body: clamp(0.9375rem, 0.885vw, 1.0625rem);  /* 17px @1920 */
  --fs-btn: clamp(0.875rem, 0.937vw, 1.125rem);     /* 18px @1920 */
  --fs-meta: clamp(0.8125rem, 0.781vw, 0.9375rem);
  --lh-lead: 1.375;                                  /* 55/40, measured */
  --lh-body: 1.45;
  --tracking-wide: 0.08em;

  /* Components */
  --dot: clamp(28px, 3.07vw, 59px);                 /* the logo's dot, promoted */
  --btn-w: clamp(180px, 15.6vw, 300px);
  --btn-h: clamp(44px, 2.81vw, 54px);
  /* --hero-mark-w removed 2026-09-07: the wordmark's width moved into the
     Elementor panel (widget 337abb1's own width / width_tablet / width_mobile
     controls) — see P2 of feedback-2026-09-07.md. This token is dead. */
  --header-h: clamp(64px, 6.2vw, 120px);
  --burger: clamp(48px, 4.58vw, 88px);
  --burger-bar: 2px;                        /* thickness of one rule */
  --burger-gap: clamp(4px, 0.42vw, 8px);    /* space between them   */

  /* Motion */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 200ms;
  --dur: 400ms;
  --dur-reveal: 700ms;
  --dur-slide: 900ms;
  --dur-load: 600ms;        /* the whole-page ease-in, slower than a reveal */
}

/* ---------------------------------------------------------------- 3. Base */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Without this, scrolling past the top (or bottom) rubber-bands the whole
     document — a real browser behaviour, not a bug in this stylesheet, but
     one this page cannot afford: `.hero__media` is `position: absolute`
     inside `.hero`'s own `overflow: hidden` (see section 8), so it cannot
     follow the page into the overscroll region. The result is a gap between
     the viewport's top edge and the hero image for as long as the bounce
     lasts, which then "snaps" shut the moment the browser settles the
     document back to its real scroll position — exactly the symptom, not a
     parallax clamp issue (the parallax only moves the image by its measured
     overhang, section 6, and cannot produce a gap at the boundary).
     `-y` only: the design has no meaningful horizontal scroll to rubber-band,
     and scoping to the axis that matters avoids suppressing more than
     necessary. Side effect, stated rather than left as a surprise: this also
     disables the browser's pull-to-refresh gesture on touch devices — for a
     site with no reason to be refreshed by a swipe, that is the wanted
     outcome, not a cost. */
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
/* The hidden attribute has to outrank any display value a component rule
   might later set, or things that hide themselves with it silently stop
   hiding. This is the one place a blanket !important is correct. */
[hidden] { display: none !important; }


button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: 200;
  padding: var(--sp-3) var(--sp-5);
  background: var(--bg-pure);
  color: var(--on-dark);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ----------------------------------------------------------- 4. Utilities */

.container {
  width: 100%;
  max-width: calc(var(--container) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The dark band and the footer set their text 73px further in than the images. */
.container--tight { padding-inline: calc(var(--gutter) + var(--container-inset)); }

/* Escapes a centred .container so a band of photography can run edge to edge.
   Only for elements that are actually nested inside one — .hero and .gallery
   are already body-level sections and need nothing. 100vw counts the classic
   Windows scrollbar, so applying this where it is not needed shifts the block
   by half a scrollbar; body's overflow-x: hidden hides the spill, not the
   shift. That is why this is opt-in and not baked into the sections. */
.full-bleed {
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.dot {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  font-weight: 400;
  max-width: 18ch;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--btn-w);
  min-height: var(--btn-h);
  padding: var(--sp-3) var(--sp-6);
  border: 1px solid currentColor;
  font-size: var(--fs-btn);
  line-height: 1.2;
  text-align: center;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.btn:hover { background: var(--bg-pure); color: var(--on-dark); }
.section--dark .btn:hover { background: var(--on-dark); color: var(--bg-pure); }

.section { padding-block: var(--sp-section); }
.section--dark {
  background: var(--bg-pure);
  color: var(--on-dark);
}

/* ------------------------------------------------------- 5. The .js gate

   Motion start-states live under html.js only, so a visitor whose JS never
   arrives sees the finished page rather than a blank one. The failsafe
   animation reveals anything still hidden at 2.5s even if the observer
   never fires.                                                             */

html.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
  animation: reveal-failsafe 0s linear 2500ms forwards;
}
html.js [data-reveal].is-in { opacity: 1; transform: none; }
html.js [data-reveal][style*="--reveal-delay"] { transition-delay: var(--reveal-delay, 0ms); }

@keyframes reveal-failsafe { to { opacity: 1; transform: none; } }

/* The page arrives instead of appearing. Same contract as [data-reveal]: the
   hidden start-state exists only under html.js, and a failsafe hands the page
   back at 2s if the script that adds .is-loaded never runs. The transform is
   dropped rather than transitioned to a value, because a transformed body is a
   containing block for the fixed header and menu. */
html.js body {
  opacity: 0;
  animation: load-failsafe 0s linear 2000ms forwards;
}
html.js body.is-loaded {
  opacity: 1;
  transition: opacity var(--dur-load) var(--ease-out);
}

/* The rise is on <main>, never on <body>.

   A transformed element becomes the containing block for its position:fixed
   descendants, so a transform on body detaches the header and the menu
   overlay from the viewport for as long as it is set — they scroll with the
   page and then snap into place when it clears. Opacity has no such effect,
   so body fades and main moves. */
html.js main {
  transform: translateY(10px);
  animation: load-failsafe-main 0s linear 2000ms forwards;
}
html.js body.is-loaded main {
  transform: none;
  transition: transform var(--dur-load) var(--ease-out);
}

@keyframes load-failsafe { to { opacity: 1; } }
@keyframes load-failsafe-main { to { transform: none; } }

/* -------------------------------------------------------------- 6. Header */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  color: var(--on-dark);
  transition: background-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out);
}
.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-6);
  width: 100%;
}

/* Opaque once the page has scrolled past the hero, and while the menu is open. */
.header.is-solid {
  background: var(--bg-pure);
}
body:not(.has-hero) .header { background: var(--bg-pure); }

.header__mark {
  display: block;
  width: clamp(96px, 8.85vw, 170px);
  transition: opacity var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}
.header__mark svg { width: 100%; height: auto; fill: currentColor; }

/* On the homepage the hero carries a wordmark of its own, so the header's
   stays out of the way until you have scrolled past it. The client requires
   the logo to be visible at all times — one of the two always is. */
body.has-hero .header:not(.is-solid) .header__mark {
  opacity: 0;
  transform: translateY(-0.5em);
  pointer-events: none;
}

.header__tools {
  display: flex;
  align-items: center;
  gap: clamp(var(--sp-4), 2vw, var(--sp-12));
}

.lang {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.lang li { display: flex; align-items: center; gap: var(--sp-2); }
.lang li + li::before { content: '/'; opacity: 0.5; }
.lang a { opacity: 0.65; transition: opacity var(--dur-fast) var(--ease-out); }
.lang a:hover { opacity: 1; }
.lang [aria-current='true'] { opacity: 1; font-weight: 600; }

/* While the header floats over the hero the switcher can land on the white
   diagonal band — white on white. The burger survives that (its bars are dark
   and that is how the mockup draws it), but text does not, so the switcher
   carries its own ground. Square, because nothing in this design has a radius.
   DEVIATION from laGràfica's mockup, which has no switcher in the header. */
.header:not(.is-solid) .lang {
  background: rgba(27, 27, 27, 0.62);
  padding: var(--sp-2) var(--sp-3);
}

/* The burger — three rules only, no disc behind them (client point 1,
   feedback 2026-09-07, package P4). The white circle + `--bg-pure` bars
   were removed because there is no Elementor control anywhere in the
   header tree that governs this widget (raw `html` widget `3abf17e`, no
   background/border-radius setting exists to protect) — see
   docs/feedback-2026-09-07.md § P4 for the measured contrast rationale.
   The bars are now `--on-dark` (white) with a drop-shadow so they read
   over both the bright video frames and the solid dark inner-page header. */
.burger {
  position: relative;
  width: var(--burger);
  height: var(--burger);
  display: grid;
  place-content: center;
  gap: var(--burger-gap);
  flex: none;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
.burger span {
  display: block;
  width: clamp(20px, 1.98vw, 38px);
  height: var(--burger-bar);
  background: var(--on-dark);
  transition: transform var(--dur) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}

/* The X has to cross exactly on the button's centre.

   The three rules are centred as a group, so the outer two sit exactly
   (bar + gap) above and below that centre. The travel MUST therefore be
   (bar + gap) — derived, not a second clamp() that happens to agree at one
   size. The previous value only matched at the smallest step; everywhere
   else the two arms crossed a couple of pixels off-centre, which reads as a
   wonky cross rather than as a rounding error. */
.burger[aria-expanded='true'] span:nth-child(1) {
  transform: translateY(calc(var(--burger-bar) + var(--burger-gap))) rotate(45deg);
}
.burger[aria-expanded='true'] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded='true'] span:nth-child(3) {
  transform: translateY(calc(-1 * (var(--burger-bar) + var(--burger-gap)))) rotate(-45deg);
}

/* ---------------------------------------------------- 7. Menu overlay */

/* `display: flex; align-items: center` removed 2026-09-23 (C-2): the overlay is
   container 6f897eb, already flex, and a direct `align-items` here — loaded
   after Elementor's — silently beat the panel's Align Items. The menu's
   placement now lives in the panel of the six header documents. */
.menu {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--bg-pure);
  color: var(--on-dark);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease-out), visibility 0s linear var(--dur);
}
.menu.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur) var(--ease-out), visibility 0s;
}

.menu__nav {
  display: flex;
  flex-direction: column;
  gap: clamp(var(--sp-3), 1.2vw, var(--sp-6));
  padding-block: var(--header-h);
}
.menu__nav a {
  display: inline-block;
  font-size: clamp(2rem, 4.16vw, 5rem);
  line-height: 1.1;
  transition: transform var(--dur) var(--ease-out);
}
.menu__nav a:hover,
.menu__nav a:focus-visible { transform: translateX(clamp(8px, 1vw, 20px)); }
.menu__nav [aria-current='page'] { color: var(--on-dark-dim); }

/* The wordmark's diagonal, reused as the overlay's one graphic gesture. */
.menu__diag {
  position: absolute;
  top: -10%;
  right: 14%;
  width: clamp(60px, 8vw, 154px);
  height: 120%;
  background: var(--deco);
  transform: skewX(var(--diag));
  pointer-events: none;
}

/* ---------------------------------------------------------------- 8. Hero */

.hero {
  position: relative;
  width: 100%;
  height: 100vh;             /* fallback: browsers that do not know svh */
  height: 100svh;            /* the small viewport — the band never hides
                                behind a phone's collapsing toolbar */
  min-height: 520px;         /* a landscape phone is 375px tall; the wordmark
                                and the caption do not fit in that */
  overflow: hidden;
  background: var(--bg-pure);
  isolation: isolate;
}

.hero__slides { position: absolute; inset: 0; }

/* The wordmark and the caption are white over whatever photograph is showing,
   and the photographs are not ours to choose. A gradient foot guarantees the
   contrast without tinting the white band, which paints above this. */
.hero::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 48%;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0));
  z-index: 1;
  pointer-events: none;
}
.hero__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--dur-slide) var(--ease-in-out);
}
.hero__slide.is-active { opacity: 1; }
.hero__slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Parallax frame. JS translates this by up to ±10% of its own height, so it is
   oversized by exactly that at both ends — the move can never uncover a strip
   of the dark ground beneath. Shared with the project pages' own hero, which
   is why the rule is written once here rather than twice.

   object-fit: cover on the image is the whole point: the photographs are the
   client's and get cropped, never stretched to fill a shape we invented.

   transition: none !important — on the live WordPress install the project
   hero's media is a bare Elementor Image widget with no
   .elementor-widget-container child, which matches Elementor's own generic
   widget rule (custom-frontend.min.css): `transition: background .3s,
   border .3s, border-radius .3s, box-shadow .3s, transform
   var(--e-transform-transition-duration,.4s)`, at higher specificity (two
   classes' worth) than this rule on its own. Confirmed live: every
   per-frame inline transform initParallax() (alla.js) writes was being
   re-eased over that 0.4s instead of applied instantly — sampled computed
   vs. inline transform after a single wheel tick and found them still
   disagreeing 600ms later, because Lenis keeps nudging the scroll (and so
   the JS's own target) for a while after each tick, so the transition chases
   a moving target and never settles. That chase is the "bouncing" Pixun
   reported. Nothing here ever wanted a hover or interaction transition on
   this element, so silencing all of it costs nothing; harmless on the
   static build and on Catalan/Spanish, none of which load Elementor's CSS
   and so never had a competing rule to begin with. */
.hero__media,
.project-hero__media {
  position: absolute;
  inset: -10% 0;
  height: 120%;
  will-change: transform;
  transition: none !important;
}
.hero__media img,
.project-hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Without JS the first slide must still be visible. */
html:not(.js) .hero__slide:first-child { opacity: 1; }

/* The white band. 337px wide at 1920, left edge at x=1210, skewed to the
   wordmark's angle. Height is oversized so the skew never exposes a corner. */
.hero__band {
  position: absolute;
  top: -10%;
  left: 63.02%;
  width: 17.55vw;
  min-width: 90px;
  height: 130%;
  background: var(--bg);
  transform: skewX(var(--diag));
  z-index: 2;
  pointer-events: none;
}

.hero__inner {
  position: relative;
  z-index: 3;
  height: 100%;
  display: flex;
  align-items: flex-end;
  padding-bottom: clamp(24px, 2.86vw, 55px);
}
.hero__caption {
  display: flex;
  align-items: flex-end;
  gap: clamp(var(--sp-5), 4.7vw, 90px);
  width: 100%;
  color: var(--on-dark);
}
/* .hero__mark / .hero__mark svg removed 2026-09-07: no element on the live
   site carries the hero__mark class (confirmed against every dumped post
   before deleting) — dead CSS per C-2, and the active width control is now
   in the panel. See P2 of feedback-2026-09-07.md. */
.hero__text {
  /* Measured at 1440x900: the caption ran to x=1176 while the white band's
     edge was at x=1005 — white text on the white band, illegible.

     It is not a z-index problem. laGrafica's hero is 1250px tall, so by the
     time the band reaches the caption it has slanted well clear of it; at
     100svh on a laptop the hero is 900px and the band has not travelled as
     far. The band's geometry is right in both cases — the text is simply
     wider, in proportion, than it is in the artwork (26% of the viewport
     against 17% in the mockup).

     So the width is tied to the viewport as well as the character count. At
     1920 this gives 345px against the mockup's 331; at 1440 it gives 259 and
     the line wraps to four, which is how the artwork sets it anyway. */
  max-width: clamp(180px, 18vw, 22ch);
  margin-bottom: 0.35em;
  text-wrap: pretty;
}
.hero__slide-text { display: none; }
.hero__slide-text.is-active { display: block; }

/* ------------------------------------------------------ 9. Intro section */

.intro__grid {
  display: grid;
  grid-template-columns: 815fr 820fr;    /* measured 815 / 81 gap / 820 */
  gap: clamp(var(--sp-6), 4.22vw, 81px);
  align-items: center;
}
.intro__figure { margin: 0; }
.intro__figure img {
  width: 100%;
  aspect-ratio: 815 / 909;               /* the mockup's own crop */
  object-fit: cover;
}
.intro__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(var(--sp-4), 2.19vw, 42px);
}
.intro__body .btn { margin-top: clamp(var(--sp-2), 0.94vw, 18px); }

/* ------------------------------------------------------- 10. Gallery strip

   Full-bleed, three visible, no gaps. The track holds the sequence twice so
   the translate can wrap seamlessly. It is dragged, not animated: JS owns the
   transform, which is why no keyframes live here any more — a CSS marquee and
   a pointer drag would be two things writing the same property.            */

.gallery {
  position: relative;
  overflow: hidden;
  height: clamp(220px, 34.5vw, 663px);   /* 663 @1920 */
  cursor: grab;
  touch-action: pan-y;                   /* the page must still scroll under a
                                            finger that starts on the strip */
}
.gallery.is-dragging {
  cursor: grabbing;
  user-select: none;                     /* a drag across photographs should
                                            not select the alt text */
}
.gallery__track {
  display: flex;
  height: 100%;
  width: max-content;
}
.gallery__item { flex: 0 0 33.3333vw; height: 100%; }
.gallery__item img { width: 100%; height: 100%; object-fit: cover; }

/* -------------------------------------------------- 11. Projects showcase

   The image row starts inside the dark band and finishes 171px below it.
   That overhang is deliberate in the mockup, so the band is padded to stop
   where the artwork stops and the row is pulled up over the boundary.      */

.showcase { position: relative; }
.showcase__band {
  background: var(--bg-pure);
  color: var(--on-dark);
  padding-top: var(--sp-section-lg);
  padding-bottom: clamp(90px, 13.9vw, 265px);
}
/* Pixun, from a screenshot, 2026-08-30: the ball, the heading and the
   "Projectes" button all need to share the logo's left edge and the project
   grid's left edge below. That is not reachable as a row — a button whose
   LEFT edge matches a heading's LEFT edge has to sit BELOW it, not beside
   it — so this is now a column, the same shape `.intro__body` already uses
   for its own dot/heading/button stack. The two nesting levels
   (`.showcase__head` itself, and the `[data-reveal]` wrapper still holding
   the dot and the heading together for the scroll-reveal) each get their
   own `display: flex; flex-direction: column` rather than being merged into
   one flat list, so the reveal animation keeps animating the dot and the
   heading as the single unit it always has. Same gap token,
   `clamp(var(--sp-4), 2.19vw, 42px)`, at both levels and reused verbatim
   from `.intro__body` rather than invented, so the two stacked blocks read
   as one rhythm. */
.showcase__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(var(--sp-4), 2.19vw, 42px);
}
/* `.js-reveal` alongside the real attribute: the Elementor port cannot set a
   custom HTML attribute from an unvalidated Pro licence, so it carries this
   as a CSS class instead and a small script bridges it to `[data-reveal]`
   at load (see the WordPress build's alla-wp.js). Layout must not wait on
   that script to run, so both selectors are stated here directly. */
.showcase__head > [data-reveal],
.showcase__head > .js-reveal {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(var(--sp-4), 2.19vw, 42px);
}
.showcase__row {
  position: relative;
  z-index: 2;
  margin-top: clamp(-160px, -12.4vw, -60px);
}

/* Masonry, and multi-column rather than grid: a grid row is as tall as its
   tallest cell, which is the one thing this layout must not do — the point is
   that every photograph keeps the proportions it was shot at. Multi-column has
   no row gap, so the children carry a bottom margin instead. */
.project-grid {
  columns: 3;
  column-gap: var(--col-gap);
}
.project-grid > * {
  break-inside: avoid;
  margin-bottom: var(--col-gap);
}

/* ------------------------------------------------------- 12. Project card

   Client requirement: only the image shows; the project's details appear
   when the cursor is over it. Keyboard focus does the same thing, and with
   no pointer at all (touch, reduced motion) the details stay visible.      */

.card { position: relative; display: block; overflow: hidden; background: var(--bg-pure); }
.card__media { position: relative; }
/* width and object-fit moved into the panel on 2026-09-07 — each card image
   widget now carries its own height/object-fit/width controls (0cdc5d3's
   children, index-grid). Only the hover transition stays here; it is not a
   property any Elementor control governs. */
.card__media img {
  transition: transform var(--dur-slide) var(--ease-out),
              opacity var(--dur) var(--ease-out);
}
.card:hover .card__media img,
.card:focus-visible .card__media img { transform: scale(1.04); opacity: 0.55; }

.card__meta {
  position: absolute;
  inset: auto 0 0 0;
  padding: clamp(var(--sp-4), 1.56vw, 30px);
  color: var(--on-dark);
  opacity: 0;
  transform: translateY(0.5em);
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
.card:hover .card__meta,
.card:focus-visible .card__meta { opacity: 1; transform: none; }

.card__name {
  font-size: var(--fs-h3);
  line-height: 1.2;
}
.card__place {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--on-dark-dim);
  margin-top: var(--sp-2);
}
.card__more {
  display: inline-block;
  margin-top: var(--sp-3);
  font-size: var(--fs-meta);
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}

/* Anywhere without a real hover — phones, tablets — the metadata is simply on. */
@media (hover: none) {
  .card__meta { opacity: 1; transform: none; }
  /* .card__media img opacity:0.6 removed 2026-09-07 — there is no hover
     overlay on the projects index, so this was fading every card image on
     every phone for no reason. */
}

/* --------------------------------------------------- 13. Kit Digital band

   Both supplied marks are dark ink on transparency and disappear on the
   footer's near-black. They sit on the white band the design already has,
   used exactly as supplied.                                                */

.kit {
  padding-block: clamp(var(--sp-10), 5.2vw, 100px);
  background: var(--bg);
}
.kit__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(var(--sp-8), 4.16vw, 80px);
  flex-wrap: wrap;
}
.kit img { height: clamp(38px, 3.4vw, 66px); width: auto; }
.kit__note {
  margin-top: var(--sp-5);
  text-align: center;
  font-size: var(--fs-meta);
  color: var(--text-tertiary);
}

/* -------------------------------------------------------------- 14. Footer */

.footer {
  position: relative;
  background: var(--bg-pure);
  color: var(--on-dark);
  padding-block: clamp(var(--sp-16), 8.1vw, 156px);
  overflow: hidden;
}
/* .footer__diag and .footer__circle deleted 2026-09-07 (P3, client point 8):
   the two decoration containers (aefc318, 8d24dce) were removed from
   _elementor_data in posts 1148/1202/1205; --deco keeps one consumer,
   .menu__diag (alla.css, mobile menu). */
.footer__inner {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 2.55fr 1.46fr 1fr;   /* measured column starts */
  gap: var(--sp-10);
  align-items: start;
}
.footer__mark { display: block; width: clamp(160px, 21.2vw, 407px); }
.footer__mark svg { width: 100%; height: auto; fill: currentColor; }

.footer__col h2 {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--on-dark-dim);
  margin-bottom: var(--sp-4);
}
.footer__col li + li { margin-top: var(--sp-2); }
.footer__col a { transition: opacity var(--dur-fast) var(--ease-out); }
.footer__col a:hover { opacity: 0.6; }

.socials { display: flex; gap: var(--sp-5); margin-top: var(--sp-8); }
.socials a {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}
/* Placeholder links have no destination yet — say so rather than pretend. */
.socials a[href=''] { opacity: 0.45; cursor: not-allowed; }

.footer__legal {
  position: relative;
  z-index: 2;
  margin-top: clamp(var(--sp-12), 5vw, 96px);
  padding-top: var(--sp-6);
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-6);
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  color: var(--on-dark-dim);
}

/* ----------------------------------------------------- 15. Inner pages */

.page-head {
  padding-top: calc(var(--header-h) + var(--sp-section-lg));
  padding-bottom: var(--sp-section);
}
.page-head h1 {
  font-size: var(--fs-h1);
  line-height: 1.08;
  font-weight: 400;
  max-width: 16ch;
}
.page-head__kicker {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--sp-5);
}

.breadcrumb {
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  margin-bottom: var(--sp-6);
}
/* height + align-items given verbatim by Pixun, 2026-08-30: the row needs a
   real height of its own so the text sits centred inside it. */
.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  height: 45px;
  align-items: center;
}
.breadcrumb li + li::before { content: '/'; margin-right: var(--sp-2); opacity: 0.6; }

.prose { max-width: 62ch; }
.prose p + p { margin-top: 1.2em; }
.prose h2 {
  font-size: var(--fs-h2);
  font-weight: 400;
  line-height: 1.15;
  margin-top: 1.6em;
  margin-bottom: 0.5em;
}
.prose h3 {
  font-size: var(--fs-h3);
  font-weight: 600;
  margin-top: 1.6em;
  margin-bottom: 0.4em;
}
.prose ul { list-style: disc; padding-left: 1.2em; margin-top: 1em; }
.prose li + li { margin-top: 0.4em; }
.prose a { border-bottom: 1px solid currentColor; }

/* Anything invented rather than supplied is marked, and looks marked. */
.unverified {
  border-left: 3px solid var(--text-tertiary);
  padding-left: var(--sp-4);
  background: var(--bg-elevated);
  padding-block: var(--sp-3);
}
.unverified__tag {
  display: block;
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--sp-2);
}

/* Visible placeholders, never silent gaps. */
.ph {
  background: repeating-linear-gradient(
    var(--diag), var(--bg-elevated) 0 10px, #e8e8e8 10px 20px);
  border: 1px dashed var(--text-tertiary);
  color: var(--text-secondary);
  padding: 0.15em 0.5em;
  font-size: 0.95em;
}

/* ------------------------------------------------- 16. Projects index */

/* .index-grid's own columns/column-gap/padding-bottom rule, and its
   `> *` break-inside/margin-bottom rule, moved into the panel on
   2026-09-07: 0cdc5d3 is now an Elementor Grid container (columns,
   gaps, padding are panel controls). Do not put them back here — a
   direct declaration on this class beats the panel's own --gap/--padding
   custom properties every time (C-2). */

/* On the index the details stay on the page. The hover overlay is still
   there, but it is decoration — nothing is only reachable by hovering. */
.card__caption { padding-top: var(--sp-4); }
.card__caption .card__name { font-weight: 400; }
.card__caption .card__place { color: var(--text-secondary); }

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-10);
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.filters button {
  padding: var(--sp-2) var(--sp-5);
  border: 1px solid var(--border-faint);
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.filters button[aria-pressed='true'] {
  background: var(--bg-pure);
  border-color: var(--bg-pure);
  color: var(--on-dark);
}

/* -------------------------------------------------- 17. Project detail */

/* Same full-viewport opening as the homepage, and the same parallax frame —
   .project-hero__media is defined with .hero__media in section 8. */
.project-hero {
  position: relative;
  /* No top margin, no top offset: on the static build — and, live, on
     Catalan and Spanish — `.header` is `position: fixed` and genuinely
     overlaps this box, exactly as it does on the homepage. Confirmed live:
     this box already spans the full viewport there (top ≈ 0, height ≈ one
     viewport), so nothing needs subtracting. */
  height: 100vh;             /* fallback: browsers that do not know svh */
  height: 100svh;
  min-height: 520px;
  overflow: hidden;
}

/* On the live WordPress install the Theme Builder header does NOT float
   over this box the way the comment above assumes for the static build —
   confirmed live, in all three languages: the header's own wrapper reserves
   a genuine 140px of normal document flow before `.project-hero` even
   begins (this box's own rendered rect starts at document y=140, not y=0),
   then a second, inner element rides fixed on top of that reserved space.
   Left at a flat 100svh, the box was 140px taller than the viewport had
   room for once that reservation is counted, pushing anything anchored to
   its own bottom edge — the English template's `.js-hero-title` — below the
   fold on load, which was the whole reported bug.

   `--alla-header-h` is the header's own live height, written every frame by
   its scroll-driven shrink (140px resting, floors at 100px past 100px of
   scroll) — but it is published English-only (alla-wp.js's own guard), so
   Catalan and Spanish never set it, confirmed live. The fallback is
   therefore the measured, unchanging resting value confirmed live on both
   (140px) — not the static build's own fluid `--header-h` token, which
   describes a different header (no relation to this one) and would
   under-subtract here by up to ~75px, reopening a smaller version of the
   same overflow this rule exists to close.

   `body.wp-singular` is WordPress core's own body class, present on every
   front-end singular view and never on the static build — confirmed by its
   absence there — which is what scopes this to the live install only; the
   static-build rule above is untouched by it. */
body.wp-singular .project-hero {
  height: calc(100vh - var(--alla-header-h, 140px));
  height: calc(100svh - var(--alla-header-h, 140px));
}

/* Pages whose markup still puts the image straight in the wrapper. */
.project-hero > img { width: 100%; height: 100%; object-fit: cover; }

.project-head {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(var(--sp-8), 4.22vw, 81px);
  padding-block: var(--sp-section-lg);
}
.project-head h1 {
  font-size: var(--fs-h1);
  font-weight: 400;
  line-height: 1.08;
}
.project-head__type {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--sp-4);
}

.facts { border-top: 1px solid var(--border-faint); }
.facts div {
  display: grid;
  grid-template-columns: minmax(120px, 30%) 1fr;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
  border-bottom: 1px solid var(--border-faint);
}
.facts dt {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.facts dd { margin: 0; }

/* Masonry too — a project's own photographs are the most mixed set on the
   site, portraits next to panoramas. .figure-set--2 survives in the markup and
   is now inert: two columns is what the base does. */
/* The prose above and the photo grid sit inside their own Elementor
   containers live, each with margin: 0 and no gap between them — confirmed
   live on /en/projects/svh-magatzem-entitats/ (and its CA/ES twins): 0px
   between .js-reveal's bottom and .figure-set's top. --sp-section is the
   same token every other section boundary on this site already uses for
   this exact kind of separation (see .section, .page-head). Pixun, from a
   screenshot, 2026-08-30: too tight, add the section rhythm above the grid.
   A per-page Spacer widget was the alternative, rejected because it would
   need inserting on all 24 project pages (8 × 3 languages) and would be
   lost the next time a page is regenerated — this is the one shared rule
   that reaches all of them, live and in every language, in one edit.

   Scoped to the pair that is actually adjacent — live .js-reveal, static
   [data-reveal] — rather than to .figure-set on its own. This template has
   a SECOND .figure-set, the drawings grid below its own <h2> inside
   .drawings, which never had this gap reported and does not need one; a
   bare `.figure-set` rule reaches that one too, and measured live it added
   an unrelated ~20–39px of drop to the pager on the static build (the
   heading's own bottom margin partially collapsing with the newly-added
   top margin) — a real regression this scoping avoids rather than papers
   over.

   No-op on the static build's own first .figure-set, which build/generate.py
   already gives an inline `style="margin-top:var(--sp-section-lg)"` — an
   inline style wins over any external-stylesheet rule for the same edge
   regardless of the logical/physical property names involved, confirmed
   with a scratch test page rather than assumed. That inline value is
   already larger than --sp-section and the static build was never
   exhibiting the flush-against-each-other bug this rule exists to fix, so
   leaving it be is correct, not an oversight. */
.js-reveal + .figure-set,
[data-reveal] + .figure-set {
  margin-block-start: var(--sp-section);
}
.figure-set {
  columns: 2;
  column-gap: var(--col-gap);
  padding-bottom: var(--sp-section-lg);
}
.figure-set > * {
  break-inside: avoid;
  margin-bottom: var(--col-gap);
}
.figure-set figure { margin: 0 0 var(--col-gap); }
.figure-set img { width: 100%; }

/* .drawings background/padding moved into the panel 2026-09-07 (P6, point
   19 — "the plans banner must be white"): background_background/color and
   block padding now live on container a50dec6 (and its per-project
   equivalent — each project page has its own id) in _elementor_data, so the
   client can change them. The `.drawings img { background: var(--bg); }`
   line was a no-op once the section itself went white — it painted a colour
   nobody could ever see under an opaque image. --bg-elevated stays defined
   (alla.css:42): .unverified, .ph, .map, .form-confirm and .team__photo
   still consume it. Do not put a direct declaration back here — see C-2. */

.pager {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-6);
  padding-block: var(--sp-section-lg);
  border-top: 1px solid var(--border-faint);
}
.pager span { display: block; font-size: var(--fs-meta); color: var(--text-secondary); }
.pager strong { font-weight: 400; font-size: var(--fs-h3); }
.pager a:hover strong { text-decoration: underline; }

/* ------------------------------------------------------- 18. Contact */

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(var(--sp-8), 4.22vw, 81px);
  padding-bottom: var(--sp-section-lg);
}
.office + .office { margin-top: var(--sp-10); }
.office h2 { font-size: var(--fs-h3); font-weight: 600; margin-bottom: var(--sp-3); }
.office address { font-style: normal; color: var(--text-secondary); }
.office a { border-bottom: 1px solid currentColor; }

.map {
  width: 100%;
  aspect-ratio: 16 / 10;
  border: 0;
  background: var(--bg-elevated);
}

/* The form sits in the right column of .contact-grid. Controls are a single
   rule under the text, not boxes: a box needs a radius decision and this
   design has none, and the rule is the same gesture as .prose a and
   .card__more. Radius is zeroed explicitly because iOS rounds inputs by
   default and would be the only curve on the site. */
.form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}
.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.field label {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.field input,
.field textarea,
.field select {
  width: 100%;
  padding: var(--sp-3) 0;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border-faint);
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.field textarea { resize: vertical; min-height: 6em; }
.field input:focus-visible,
.field textarea:focus-visible,
.field select:focus-visible { border-bottom-color: var(--border); }

.field--check {
  flex-direction: row;
  align-items: flex-start;
  gap: var(--sp-3);
}
.field--check input {
  width: auto;
  flex: none;
  margin-top: 0.2em;
  padding: 0;
}
.field--check label {
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-meta);
  color: var(--text-primary);
}

/* An error is never colour alone — this palette has no red to spend anyway.
   Three signals: the rule doubles in weight, the message appears, and the
   message carries a mark of its own for anyone reading it in monochrome or
   with a stylesheet of their own. */
.field__error {
  display: none;
  font-size: var(--fs-meta);
  color: var(--text-primary);
}
.field__error::before { content: '! '; font-weight: 700; }
.field.has-error .field__error { display: block; }
.field.has-error input,
.field.has-error textarea,
.field.has-error select {
  border-bottom-width: 2px;
  border-bottom-color: var(--border);
}

/* Announced by the browser as it changes, so it must exist before it has
   anything to say — it is empty, not absent. */
.form__status {
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  min-height: 1.4em;
}

.form-confirm {
  padding: var(--sp-6);
  background: var(--bg-elevated);
  border-left: 3px solid var(--border);
}
.form-confirm[hidden] { display: none; }

/* ------------------------------------------------------ 19. Lightbox

   The trigger is a button because opening an overlay is an action, not a
   destination — a link would promise a page. The overlay itself is built by
   JS and appended to the body, so nothing here depends on markup that a
   scriptless visitor would be left staring at.                             */

.lb-trigger {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: zoom-in;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 120;              /* over the menu (90) and the header (100) */
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(27, 27, 27, 0.96);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease-out), visibility 0s linear var(--dur);
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur) var(--ease-out), visibility 0s;
}

/* Bounded by the viewport rather than the frame, and contained rather than
   covered: this is the one place the photograph is shown whole. */
.lightbox__img {
  max-width: 92vw;
  max-height: 86vh;
  width: auto;
  height: auto;
  object-fit: contain;
}

.lightbox__prev,
.lightbox__next,
.lightbox__close {
  position: absolute;
  display: grid;
  place-content: center;
  min-width: 48px;
  min-height: 48px;
  padding: var(--sp-3);
  color: var(--on-dark);
  font-size: var(--fs-h3);
  line-height: 1;
  background: none;
  border: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.lightbox__prev:hover,
.lightbox__next:hover,
.lightbox__close:hover { opacity: 0.6; }
.lightbox__prev:focus-visible,
.lightbox__next:focus-visible,
.lightbox__close:focus-visible {
  outline: 2px solid var(--on-dark);
  outline-offset: 3px;
}

.lightbox__prev,
.lightbox__next { top: 50%; transform: translateY(-50%); }
.lightbox__prev { left: clamp(var(--sp-2), 2vw, var(--sp-8)); }
.lightbox__next { right: clamp(var(--sp-2), 2vw, var(--sp-8)); }
.lightbox__close {
  top: clamp(var(--sp-2), 2vw, var(--sp-8));
  right: clamp(var(--sp-2), 2vw, var(--sp-8));
}

/* The chevrons: one box, two borders, turned 45 degrees. */
.lightbox__prev::before,
.lightbox__next::before {
  content: '';
  display: block;
  width: clamp(12px, 1.1vw, 16px);
  height: clamp(12px, 1.1vw, 16px);
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
}
.lightbox__prev::before { transform: rotate(45deg); }
.lightbox__next::before { transform: rotate(-135deg); }

/* The cross: two rules crossing on the button's centre — the same figure the
   burger makes when the menu is open, and centred the same deliberate way. */
.lightbox__close::before,
.lightbox__close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(18px, 1.6vw, 24px);
  height: 2px;
  background: currentColor;
}
.lightbox__close::before { transform: translate(-50%, -50%) rotate(45deg); }
.lightbox__close::after { transform: translate(-50%, -50%) rotate(-45deg); }

.lightbox__count {
  position: absolute;
  left: 50%;
  bottom: clamp(var(--sp-4), 2vw, var(--sp-8));
  transform: translateX(-50%);
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--on-dark-dim);
}

/* ---------------------------------------------------------- 20. Team

   The about page's people. A portrait frame with a ground of its own, so a
   member without a photograph yet is a considered gap rather than a hole.   */

.team {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(var(--sp-8), 3.1vw, 60px) var(--col-gap);
}
.team__member { display: flex; flex-direction: column; gap: var(--sp-3); }
.team__photo {
  aspect-ratio: 4 / 5;
  background: var(--bg-elevated);
  overflow: hidden;
}
.team__photo img { width: 100%; height: 100%; object-fit: cover; }
.team__name {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.2;
}
.team__role {
  font-size: var(--fs-meta);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.team__bio {
  font-size: var(--fs-body);
  color: var(--text-secondary);
  max-width: 40ch;
}

/* ------------------------------------------------- 21. Responsive rules */

@media (max-width: 1100px) {
  .footer__inner { grid-template-columns: 1fr 1fr; }
  .footer__mark { grid-column: 1 / -1; }
}

@media (max-width: 900px) {
  :root { --container-inset: 24px; }
  .intro__grid { grid-template-columns: 1fr; }
  .project-head,
  .contact-grid { grid-template-columns: 1fr; }
  .project-grid,
  .figure-set { columns: 2; }
  .team { grid-template-columns: repeat(2, 1fr); }
  .gallery__item { flex-basis: 50vw; }
  .showcase__row { margin-top: clamp(-90px, -9vw, -40px); }
}

@media (max-width: 640px) {
  .project-grid,
  .figure-set { columns: 1; }
  .team { grid-template-columns: 1fr; }
  .gallery__item { flex-basis: 80vw; }
  .footer__inner { grid-template-columns: 1fr; }
  .hero__caption { flex-direction: column; align-items: flex-start; gap: var(--sp-5); }
  .hero__text { max-width: none; }
  .footer__legal { flex-direction: column; }
  .header__tools { gap: var(--sp-4); }

  /* On a phone the controls stop straddling the photograph and line up with
     the counter along the bottom, where a thumb reaches them anyway. */
  .lightbox__prev,
  .lightbox__next {
    top: auto;
    bottom: clamp(var(--sp-3), 2vw, var(--sp-5));
    transform: none;
  }
  .lightbox__prev { left: var(--sp-4); }
  .lightbox__next { right: var(--sp-4); }
  .lightbox__count { bottom: calc(var(--sp-5) + 12px); }
  .lightbox__img { max-height: 72vh; }
}

/* ------------------------------------------------- 23. Lenis base rules

   Lenis ships these in its own stylesheet, which we do not load — only the
   script is vendored. Without them the page can end up with a locked height,
   the native smooth-scroll keeps fighting the virtual one, and iframes eat
   the wheel events Lenis needs.                                            */

html.lenis,
html.lenis body { height: auto; }

.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* ------------------------------------------- 24. Drag and lightbox stage

   A native image drag starts the moment a pointer moves on an <img>, which
   cancels the pointer capture the strip relies on — the carousel would stop
   dead mid-swipe and hand the browser a ghost thumbnail instead.            */

.gallery img {
  -webkit-user-drag: none;
  user-select: none;
}
.gallery.is-dragging { user-select: none; }

/* The stage exists so a click on the padding AROUND the photograph still
   counts as the backdrop and closes the lightbox. */
.lightbox__img {
  cursor: grab;
  -webkit-user-drag: none;
  user-select: none;
}
.lightbox.is-dragging .lightbox__img { cursor: grabbing; }

.lightbox__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: clamp(var(--sp-8), 6vw, 96px);
  touch-action: pan-y;   /* horizontal belongs to the swipe */
}

/* ------------------------------------------------- 22. Reduced motion

   Everything that moves has a still equivalent — the carousels stop on
   their first frame rather than disappearing, and hover metadata is
   simply present.                                                         */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  html.js [data-reveal] { opacity: 1; transform: none; }

  /* The page is simply there — no fade, no lift. Both halves have to be said:
     the failsafe's duration is neutralised above but its 2s delay is not, so
     main would otherwise sit 10px low until it fired. */
  html.js body { opacity: 1; }
  html.js main { transform: none; }

  /* The strip is something the reader scrolls, natively. Keeping the band's
     height and the items' sizing matters: releasing them turns the row into
     ragged, clipped thumbnails. The transform is forced back to none so a
     value left behind by a drag cannot offset the scroller. */
  html.js .gallery__track { transform: none !important; }
  .gallery { overflow-x: auto; cursor: auto; }

  /* Parallax frames sit still, and their overhang stays — it is what keeps
     the crop identical whether the effect runs or not. */
  .hero__media,
  .project-hero__media { transform: none !important; }

  .card__meta { opacity: 1; transform: none; }
  /* .card__media img opacity:0.6 removed 2026-09-08 — duplicate of the
     rule already removed from @media (hover: none) above; same reasoning
     applies, this fires under prefers-reduced-motion too. */
  .hero__slide { transition: none; }
}
