/* ============================================================
   Arcillafilms — visual system prototype
   Paper / ink / warm gray / one signal of clay.
   ============================================================ */

:root {
  --paper: #F7F4EF;
  --mist: #EDE7DC;
  --stone: #DCD5C6;
  /* #6E685C, not the original #8A8478 — that failed 4.5:1 on paper (3.39:1, audit-confirmed). This passes at ~5:1. */
  --gray: #6E685C;
  --ink: #1B1712;
  --ink-soft: #3A362E;
  /* #BE6B52, not the original #B5624A — that read 4.29:1 on ink, just under AA. This clears 4.5:1. */
  --clay: #BE6B52;
  --clay-deep: #8F4A38;

  --serif: 'Newsreader', 'Iowan Old Style', serif;
  --sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-fast: 240ms;
  --dur-base: 600ms;
  --dur-slow: 1100ms;
}

* { box-sizing: border-box; }

html {
  background: var(--paper);
  scroll-behavior: smooth;
  overflow-x: hidden;
  /* The unstyled OS scrollbar defaults to a light track/thumb, which reads
     as a stray pale strip running the full height of the page wherever it
     crosses a dark section (most of the site). Styled here instead of left
     to the browser default. */
  scrollbar-width: thin;
  scrollbar-color: var(--gray) transparent;
}
::-webkit-scrollbar {
  width: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--gray);
  border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--ink-soft);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 300;
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

h1, h2, h3, .display {
  font-family: var(--serif);
  font-optical-sizing: auto;
  font-weight: 500;
  letter-spacing: -0.01em;
  margin: 0;
}

.eyebrow {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray);
  font-weight: 500;
}

.container {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 6vw;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* skip link */
.skip-link {
  position: absolute;
  left: 12px; top: -60px;
  background: var(--ink);
  color: var(--paper);
  padding: 10px 16px;
  border-radius: 4px;
  font-size: 14px;
  z-index: 1000;
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: 12px; }

/* focus visibility, kept even where custom controls remove native outline */
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
}

/* ============================================================
   Navigation
   ============================================================ */

.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  gap: 48px;
  padding: 26px 6vw;
  transition: opacity var(--dur-base) var(--ease), background-color var(--dur-base) var(--ease);
}

/* Small, discreet, present from the first frame — not hidden then revealed. */
body.home .site-nav {
  color: var(--paper);
  text-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
body:not(.home) .site-nav {
  color: var(--ink);
  background: transparent;
}

.wordmark {
  display: inline-flex;
}
.wordmark-img {
  height: 40px;
  width: auto;
  display: block;
}
.wordmark-img--black { display: none; }
/* On this site, dark-ness of nav and footer always co-occur per page: home
   is dark throughout (hero + footer), every other page is light throughout
   (page-head + plain footer) — so one page-level switch covers both marks. */
body:not(.home) .wordmark-img--white { display: none; }
body:not(.home) .wordmark-img--black { display: block; }

/* The homepage logo starts big and roughly centered, then travels to its
   normal small corner spot — one element, one continuous move, not a
   crossfade between two. No-JS and other pages never see the big state. */
/* The "big, centered" starting transform is computed in JS (see main.js)
   from the wordmark's actual measured position — vw/vh guesses don't
   reliably land it dead-center across different viewport sizes. CSS only
   owns the transition and the settled (identity) end-state. */
.js-ready body.home .wordmark {
  transform-origin: 0 0;
  transition: transform 1200ms cubic-bezier(0.65, 0, 0.35, 1);
}
.js-ready body.home .wordmark.is-settled {
  transform: none;
}
.js-ready body.home .menu-trigger {
  opacity: 0;
  transition: opacity 700ms var(--ease);
}
.js-ready body.home .menu-trigger.is-visible {
  opacity: 1;
}

/* Always-visible desktop links — plain inline row, thin dividers, small
   caps, matching the reference Diego gave rather than the old
   click-to-open dropdown. Fades in on the same beat as the Menu
   trigger (setWordmarkSmall, main.js), same reasoning: nothing here
   should appear before the logo has finished settling into its corner.
   Centered in the bar via position:absolute (.site-nav is itself
   position:fixed, so it's already the containing block for this) —
   true dead-center regardless of how wide the wordmark or the lang/
   contact/menu group on the right end up being, rather than a flex
   center that any width difference between those two would throw off. */
.nav-links {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
  opacity: 0;
  transition: opacity 700ms var(--ease);
}
.js-ready body.home .nav-links.is-visible {
  opacity: 1;
}
/* A small clay mark leading the group — the same "something's live"
   language as the reel's own REC dot (.reel-chrome-play::before), just
   steady instead of blinking. A quiet, site-specific flourish rather
   than a plain text list, and it also gives the group a clear left
   edge now that the links themselves read heavier. */
.nav-links::before {
  content: '';
  display: block;
  width: 6px;
  height: 6px;
  margin-right: 16px;
  border-radius: 50%;
  background: var(--clay);
  box-shadow: 0 0 6px 1px rgba(190,107,82,0.6);
}
.nav-links li {
  padding-right: 22px;
  margin-right: 22px;
  border-right: 1px solid rgba(247,244,239,0.45);
}
.nav-links li:last-child {
  padding-right: 0;
  margin-right: 0;
  border-right: none;
}
.nav-links a {
  display: inline-block;
  position: relative;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper);
  padding: 10px 2px;
  margin: -10px -2px;
  transition: color var(--dur-fast) var(--ease);
}
/* An underline that draws in from the left on hover — the same move
   .contact-email already uses further down the page, so the two
   don't feel like two different sites' worth of interaction design. */
.nav-links a::after {
  content: '';
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 4px;
  height: 1px;
  background: var(--clay);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 260ms var(--ease);
}
.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--clay);
}
.nav-links a:hover::after,
.nav-links a:focus-visible::after {
  transform: scaleX(1);
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 22px;
  margin-left: auto;
}

/* Set apart from the other links (margin-left:auto on .nav-right above
   pushes this whole group to the far edge) — same treatment as
   .nav-links otherwise, just standalone rather than in that list. */
.nav-contact {
  display: inline-block;
  position: relative;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper);
  padding: 10px 2px;
  margin: -10px -2px;
  opacity: 0;
  transition: opacity 700ms var(--ease), color var(--dur-fast) var(--ease);
}
.nav-contact::after {
  content: '';
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 4px;
  height: 1px;
  background: var(--clay);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 260ms var(--ease);
}
.js-ready body.home .nav-contact.is-visible {
  opacity: 1;
}
.nav-contact:hover,
.nav-contact:focus-visible {
  color: var(--clay);
}
.nav-contact:hover::after,
.nav-contact:focus-visible::after {
  transform: scaleX(1);
}

@media (min-width: 861px) {
  /* body.home. prefix (not just .menu-trigger) is deliberate — the plain
     .menu-trigger base rule (display:flex) sits later in the file, and
     at equal specificity source order alone would let it win over this
     despite the media query matching. */
  body.home .menu-trigger { display: none; }
}
@media (max-width: 860px) {
  .nav-links, .nav-contact { display: none; }
  .site-nav { gap: 0; }
}

/* Sits right next to Menu — the only EN/ES control in the header now
   (it used to live inside the dropdown, duplicating the one already
   under the Inicio headline; one clear spot instead of two). Same
   quiet, plain-text treatment as the trigger beside it — no pill,
   no border, just weight/color to mark the active one. */
.nav-lang {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.06em;
}
.nav-lang-btn {
  background: none;
  border: none;
  cursor: pointer;
  touch-action: manipulation;
  color: inherit;
  opacity: 0.55;
  padding: 10px 2px;
  margin: -10px -2px;
  transition: opacity var(--dur-fast) var(--ease);
}
.nav-lang-btn:hover,
.nav-lang-btn:focus-visible {
  opacity: 0.85;
}
.nav-lang-btn[aria-current="true"] {
  opacity: 1;
  font-weight: 500;
}
.nav-lang-sep {
  opacity: 0.35;
}

.menu-trigger {
  background: none;
  border: none;
  cursor: pointer;
  touch-action: manipulation;
  min-height: 44px;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: inherit;
  padding: 8px 4px;
}

/* A small, quiet dropdown anchored under the menu button — not a
   full-screen takeover, and not a solid panel either: no background,
   no blur, no border, nothing boxy. Every link scrolls within this one
   page (see initNavOverlay, main.js), so there's no need for the
   weight a destination-changing nav usually carries. text-shadow alone
   (on the list items below) carries legibility over whatever's behind
   it — the same trick the nav bar itself already relies on. Settles
   just below the nav bar, right-aligned to match its own padding. */
.nav-overlay {
  position: fixed;
  top: 74px;
  right: 6vw;
  z-index: 300;
  width: min(240px, 82vw);
  color: var(--paper);
  padding: 4px 4px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transform-origin: top right;
  transition: opacity 260ms var(--ease), transform 260ms var(--ease), visibility 260ms;
}
.nav-overlay.is-open { opacity: 1; visibility: visible; transform: translateY(0); }

.nav-overlay-list {
  list-style: none;
  margin: 0; padding: 0;
  text-align: right;
}
.nav-overlay-list a {
  display: block;
  font-family: var(--serif);
  font-size: 19px;
  font-weight: 400;
  padding: 9px 2px;
  text-shadow: 0 1px 4px rgba(0,0,0,0.5);
  transition: color var(--dur-fast) var(--ease), padding-right var(--dur-fast) var(--ease);
}
.nav-overlay-list a:hover,
.nav-overlay-list a:focus-visible {
  color: var(--clay);
  padding-right: 8px;
}

/* ============================================================
   Screen -1 — "Portfolio" (experimental). 12 clips in a 5-over-2-over-5
   grid, the middle row's center left empty for the headline. See main.js
   for the full sequence: big centered logo alone → grid assembles from
   scattered starting points → clips play in a wave radiating out from
   the center → everything settles (logo docks to the nav corner, text
   fades in, clips freeze) → each clip then only plays again on hover.
   This screen owns the wordmark's "big, centered" entrance now (it's
   first in the stack) — Screen 0 below assumes it's already settled by
   the time it's reached, the same way Screen 1 already did.
   ============================================================ */

.portfolio-hero {
  position: relative;
  height: 100vh;
  height: 100dvh;
  min-height: 560px;
  overflow: hidden;
  color: var(--paper);
  background: var(--ink);
}
/* Plain, unpinned — no sticky, no extra scroll track. Ordinary scroll
   carries this section past exactly like any other; main.js reads its own
   getBoundingClientRect() on every scroll tick and reveals clips purely
   off how close it is to the viewport, starting before it's even in view
   and finishing shortly after it arrives, reversible the whole way. */
.portfolio-stage {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Named areas, not just column/row numbers — reads directly as the 5/2/5
   layout with a hole in the middle that was asked for, rather than
   something you have to mentally map from a grid-column/row number pair.
   gap + the container's own padding double as the frame: a solid color
   showing through on all sides (between cells and around the outer edge
   alike) reads as one continuous mat rather than borders that would need
   doubling up where two cells meet. */
.portfolio-grid {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-template-rows: repeat(3, 1fr);
  grid-template-areas:
    "t1 t2 t3 t4 t5"
    "m1 tx tx tx m2"
    "b1 b2 b3 b4 b5";
  gap: 9px;
  padding: 9px;
  box-sizing: border-box;
  background: var(--ink);
}
.portfolio-cell--t1 { grid-area: t1; }
.portfolio-cell--t2 { grid-area: t2; }
.portfolio-cell--t3 { grid-area: t3; }
.portfolio-cell--t4 { grid-area: t4; }
.portfolio-cell--t5 { grid-area: t5; }
.portfolio-cell--m1 { grid-area: m1; }
.portfolio-cell--tx { grid-area: tx; }
.portfolio-cell--m2 { grid-area: m2; }
.portfolio-cell--b1 { grid-area: b1; }
.portfolio-cell--b2 { grid-area: b2; }
.portfolio-cell--b3 { grid-area: b3; }
.portfolio-cell--b4 { grid-area: b4; }
.portfolio-cell--b5 { grid-area: b5; }

.portfolio-cell {
  position: relative;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  background: #000;
  cursor: none;
  opacity: 0;
  /* Settled state is the plain identity transform — see the per-cell
     scatter rules below, one direction/rotation per grid position so the
     assembly reads as converging from all around rather than every tile
     sliding in from the same side. */
  transform: scale(0.55);
  transition: opacity 640ms var(--ease), transform 640ms var(--ease), filter 640ms var(--ease);
  filter: blur(6px);
}
.portfolio-cell--t1 { transform: scale(0.55) translate(-8vw, -8vh) rotate(-6deg); }
.portfolio-cell--t2 { transform: scale(0.55) translate(-3vw, -10vh) rotate(4deg); }
.portfolio-cell--t3 { transform: scale(0.55) translateY(-11vh); }
.portfolio-cell--t4 { transform: scale(0.55) translate(3vw, -10vh) rotate(-4deg); }
.portfolio-cell--t5 { transform: scale(0.55) translate(8vw, -8vh) rotate(6deg); }
.portfolio-cell--m1 { transform: scale(0.55) translate(-9vw, 0) rotate(-5deg); }
.portfolio-cell--m2 { transform: scale(0.55) translate(9vw, 0) rotate(5deg); }
.portfolio-cell--b1 { transform: scale(0.55) translate(-8vw, 8vh) rotate(6deg); }
.portfolio-cell--b2 { transform: scale(0.55) translate(-3vw, 10vh) rotate(-4deg); }
.portfolio-cell--b3 { transform: scale(0.55) translateY(11vh); }
.portfolio-cell--b4 { transform: scale(0.55) translate(3vw, 10vh) rotate(4deg); }
.portfolio-cell--b5 { transform: scale(0.55) translate(8vw, 8vh) rotate(-6deg); }
/* Grows in from the exact center rather than flying in from a direction
   — it's the origin of the radiating wave (see main.js RADIATE_ORDER),
   so it reads right that this is the one tile that doesn't travel to get
   there. */
.portfolio-cell--tx { transform: scale(0.2); }
/* Revealed one at a time as the visitor scrolls through .portfolio-hero's
   own extra height (see main.js updatePortfolioReveal) — a per-cell class
   now, not a single grid-wide one, since cells arrive individually and
   reversibly instead of all assembling together on a fixed timer. */
.portfolio-cell.is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

.portfolio-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(0.1) contrast(1.06) brightness(0.94) saturate(1.02) blur(3px);
  transition: filter 320ms var(--ease);
}
/* Soft by default, sharp the instant it's actually playing — whether that's
   the brief flourish on reveal, the visitor hovering it, or (on touch) its
   turn in the auto-cycle. Driven by the video's own play/pause events in
   main.js rather than by is-hovering alone, so all three read consistently. */
.portfolio-clip.is-focused {
  filter: grayscale(0.1) contrast(1.06) brightness(0.94) saturate(1.02) blur(0);
}

/* A soft vignette/scrim per cell, screen-blended-out on hover so the
   frame reads a touch darker at rest (keeping the grid from competing
   with the text) and brightens exactly where the visitor's attention
   already is. */
.portfolio-cell::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(8,7,5,0.28);
  transition: opacity 400ms var(--ease);
  pointer-events: none;
}
.portfolio-cell.is-hovering::after,
.portfolio-cell.is-playing::after {
  opacity: 0;
}

/* The center hole from the first pass is a clip now too — a wide
   landscape shot spanning the same 3 columns that used to sit empty,
   with the section's own line laid over it instead of over flat black.
   .portfolio-tx-scrim is a permanent (not hover-toggled) gradient, since
   there's real text sitting on this one that has to stay legible the
   whole time, playing or not — the generic .portfolio-cell::after dimming
   layers on top of it same as any other cell. */
.portfolio-tx-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(180deg,
    rgba(8,7,5,0.5) 0%,
    rgba(8,7,5,0.62) 50%,
    rgba(8,7,5,0.5) 100%);
  pointer-events: none;
}
.portfolio-tx-text {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0 6vw;
  text-align: center;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(19px, 2.1vw, 30px);
  line-height: 1.32;
  color: var(--paper);
  text-shadow: 0 3px 16px rgba(0,0,0,0.55);
  pointer-events: none;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease);
}
.portfolio-tx-text.is-visible {
  opacity: 1;
  transform: none;
}

/* Custom cursor — visible only while actually hovering the grid (toggled
   in main.js) and only on pointer-fine/hover-capable devices;
   .portfolio-cell's own cursor:none only matters there in the first
   place. Positioned in main.js via translate3d(mouseX, mouseY, 0)
   translate(-50%,-50%) — the trailing -50%/-50% resolves against the
   ring's own current box, so it stays self-centered on the pointer
   whether it's small (moving) or grown (idle), with no need for main.js
   to know its size. No transition on that positioning translate itself —
   a cursor replacement has to track the real pointer with zero lag or it
   reads as broken, not stylish.
   Small by default (just the TELL/STORY mark); grows substantially once
   it settles (see .is-idle, toggled in main.js after ~220ms with no
   mousemove) — big enough that the three-line city/country/year rising in
   beneath the mark is actually legible, not just a hinted-at flourish. */
.portfolio-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 20;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: transparent;
  border: 2px solid rgba(247,244,239,0.85);
  box-sizing: border-box;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transform: translate3d(-1000px, -1000px, 0);
  transition: opacity 260ms var(--ease), width 320ms var(--ease), height 320ms var(--ease);
  box-shadow: 0 6px 24px rgba(0,0,0,0.35);
}
.portfolio-cursor.is-visible {
  opacity: 1;
}
.portfolio-cursor.is-idle {
  width: 132px;
  height: 132px;
}

/* A quiet mark, not a button label — small, wide-tracked, low-contrast
   against the ring's own paper-on-transparent look. Sits centered while
   moving; once the cursor settles it rises to make room for the
   city/country/year rising in underneath (see .is-idle below) — the same
   settling motion in both directions, just offset. */
.portfolio-cursor-label {
  font-family: var(--sans);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.14em;
  line-height: 1.5;
  text-align: center;
  color: rgba(247,244,239,0.7);
  transition: transform 280ms var(--ease);
}
.portfolio-cursor.is-idle .portfolio-cursor-label {
  transform: translateY(-12px);
}

/* City, country, year — the hovered clip's own small caption, set from
   main.js on hover (real Biblioteca Visual data where it has it, a
   plausible placeholder otherwise). Collapsed to nothing while moving; a
   simple rise-and-fade in once the cursor actually stops (see .is-idle),
   never while it's still travelling — "algo sencillo," not a flourish.
   Sized to actually read once the ring has grown to fit it — the earlier
   pass here was too small to be legible even once revealed. */
.portfolio-cursor-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  max-height: 0;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 260ms var(--ease), transform 260ms var(--ease), max-height 260ms var(--ease);
}
.portfolio-cursor.is-idle .portfolio-cursor-info {
  max-height: 48px;
  opacity: 1;
  transform: translateY(0);
}
.portfolio-cursor-city,
.portfolio-cursor-country,
.portfolio-cursor-year {
  font-family: var(--sans);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.07em;
  line-height: 1.4;
  text-align: center;
  text-transform: uppercase;
  color: rgba(247,244,239,0.6);
  white-space: nowrap;
}

/* .portfolio-grid promotes itself to z-index:1 (a real stacking context,
   needed to keep individual cells' own z-index:auto layers — the ::after
   scrim, the wide cell's text — contained within it); a plain last-child
   .grain with no z-index of its own then paints BELOW that context's
   layer 1, not above the whole section like everywhere else this class is
   used. This is the fix — bumped high enough to clear the cursor's own
   z-index:20 too, since grain should still read as the outermost texture
   even over that. */
.portfolio-grain {
  z-index: 21;
}

@media (max-width: 860px), (hover: none), (pointer: coarse) {
  .portfolio-cursor { display: none; }
  .portfolio-cell { cursor: auto; }
}

/* Mobile: the precise 5/2/5-with-a-hole layout doesn't survive a narrow
   viewport with 12 cells still legible, so it reflows to a plain 3-column
   grid instead and the headline becomes its own full-width band with a
   dark backdrop of its own, placed after the first row of clips rather
   than needing an empty hole carved out of the grid. Hover doesn't really
   exist on touch, so clips just take turns playing on their own, gently,
   in an unending loop — see main.js. */
@media (max-width: 860px) {
  .portfolio-grid {
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(5, 1fr);
    grid-template-areas:
      "t1 t2 t3"
      "t4 t5 m1"
      "tx tx tx"
      "m2 b1 b2"
      "b3 b4 b5";
    gap: 6px;
    padding: 6px;
  }
  .portfolio-tx-text {
    padding: 0 5vw;
    font-size: clamp(17px, 4.4vw, 23px);
  }
}

/* ============================================================
   Screen 0 — "Inicio"
   A locked-off forest shot that idles on its own now — a slow, continuous
   sway back and forth through 4 seconds of its own footage (see main.js),
   never tied to scroll. Nothing here captures a gesture any more: the
   fixed intro sequence (logo big-and-centered → docks to the nav corner →
   headline fades in → scroll cue fades in) plays once on load, and from
   there it's just an ordinary section — scrolling past it behaves like
   scrolling past anything else on the page.
   ============================================================ */

.inicio {
  position: relative;
  height: 100vh;
  height: 100dvh;
  min-height: 560px;
  overflow: hidden;
  color: var(--paper);
  background: #0c0b09;
}

/* The "cover" trick: this inner box is sized so it always exactly
   replicates object-fit:cover for a 1920x1080 source, whatever the
   viewport's own aspect ratio. Everything inside it (video, trunk cutout,
   text) can then be positioned with plain percentages of that native
   1920x1080 frame and stay registered with each other at any screen size —
   the same math the video uses via width/height 100%, just made explicit
   so the text and the cutout can share it too. */
.inicio-frame {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 1;
}
.inicio-frame-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100vw;
  height: 56.25vw;
  min-height: 100vh;
  min-width: 177.78vh;
}
.inicio-frame-inner video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.inicio-text-layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 0 6vw;
  pointer-events: none;
}
/* Centered now, not tucked beside the trunk — a plain fade-and-rise on
   load, no per-line choreography (that belonged to the scroll-scrubbed
   version; nothing here scrubs any more, see main.js). Same font-size as
   before, just centered instead of left-justified. */
.inicio-text {
  margin: 0;
  text-align: center;
  font-family: 'DM Serif Display', var(--serif);
  font-style: normal;
  font-weight: 400;
  font-size: clamp(24px, 2.9vw, 38px);
  line-height: 1.32;
  color: var(--paper);
  text-shadow: 0 4px 24px rgba(0,0,0,0.5);
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease);
}
.inicio-text.is-visible {
  opacity: 1;
  transform: none;
}

.inicio-lang {
  display: flex;
  align-items: center;
  gap: 8px;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease);
}
.inicio-lang.is-visible {
  opacity: 1;
  transform: none;
}
.inicio-lang-btn {
  appearance: none;
  background: none;
  border: none;
  padding: 2px 1px;
  margin: 0;
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(247,244,239,0.42);
  cursor: pointer;
  transition: color 260ms var(--ease);
}
.inicio-lang-btn:hover {
  color: rgba(247,244,239,0.75);
}
.inicio-lang-btn[aria-current="true"] {
  color: rgba(247,244,239,0.92);
}
.inicio-lang-sep {
  font-size: 10.5px;
  color: rgba(247,244,239,0.25);
}

.inicio-scrim {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(20,18,15,0.06) 0%, rgba(20,18,15,0.02) 35%, rgba(20,18,15,0.22) 100%);
}

/* The light shaft itself — positioned to fall through the gap between the
   two trees, not across the whole frame. Masked so it fades out over the
   near trunk's own column instead of painting over it: the trunk is
   opaque wood, so the shaft reads as passing behind it, only becoming
   visible once it clears into the open gap — that occlusion is what
   actually sells the shaft's depth against the trunk. */
/* Tried on Screen 0 by explicit request — everywhere else this sits
   alongside the canvas dust field (which stays; this doesn't replace it,
   it's an additional layer), so Inicio gets the same texture as the rest
   of the site instead of being the one exception. z-index above the
   beam's 5 so it isn't lost underneath it. */
.inicio-grain {
  z-index: 6;
}
.inicio-beam {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 40%, black 56%, black 100%);
  mask-image: linear-gradient(90deg, transparent 0%, transparent 40%, black 56%, black 100%);
}
.inicio-beam::before {
  content: '';
  position: absolute;
  inset: -15%;
  background: linear-gradient(122deg,
    transparent 10%,
    rgba(255,232,182,0.28) 32%,
    rgba(255,232,182,0.20) 52%,
    rgba(255,232,182,0.11) 74%,
    transparent 96%);
}

/* Dust in suspension — 300 of them, each drifting on its own slow organic
   loop and able to be drawn toward the cursor. Drawn on a single canvas
   rather than as 300 individual DOM nodes: that many elements each
   carrying their own blur filter inside a blend-mode group was cheap in
   isolation but forced the browser into expensive per-element offscreen
   compositing every frame, competing with the hero video's own decode/
   paint for the frame budget and reading as stutter in the video itself.
   A canvas draws all of them in one pass — same visual result, a fraction
   of the cost. Homes are scattered freely across most of the frame,
   biased toward the lit half; size doubles as depth. */
.inicio-dust-field {
  display: block;
  position: absolute;
  inset: -10%;
  /* Canvas is a replaced element — its default 300x150 intrinsic size
     otherwise wins over what `inset` alone implies, so the box has to be
     told explicitly to fill the area `inset` carved out. */
  width: 100%;
  height: 100%;
  z-index: 5;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* Sound toggle — bottom-right, present and readable on Screen 0. */
.inicio-sound-toggle {
  position: absolute;
  right: 6vw;
  bottom: 4vh;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  cursor: pointer;
  touch-action: manipulation;
  padding: 10px 6px;
  margin: -10px -6px;
  opacity: 0;
  transition: opacity 700ms var(--ease);
}
.js-ready .inicio.is-ready .inicio-sound-toggle {
  opacity: 0.9;
}
.inicio-sound-toggle:hover,
.inicio-sound-toggle:focus-visible {
  opacity: 1;
}
.inicio-sound-icon {
  position: relative;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  fill: none;
  stroke: var(--paper);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 4px rgba(0,0,0,0.7));
}
.inicio-sound-toggle [data-icon-off] { display: none; }
.inicio-sound-toggle[aria-pressed="true"] [data-icon-on] { display: none; }
.inicio-sound-toggle[aria-pressed="true"] [data-icon-off] { display: block; }
.inicio-sound-label {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--paper);
  text-shadow: 0 1px 4px rgba(0,0,0,0.7);
}


.inicio .opening-scroll-cue {
  z-index: 6;
}

.inicio-loading {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: #0c0b09;
  transition: opacity 600ms var(--ease);
}
.inicio.is-ready .inicio-loading {
  opacity: 0;
  pointer-events: none;
}

/* A visible film-grain layer stands in for the loading motion instead —
   the logo itself stays still, exactly as it appears everywhere else on
   the site. The grain disappears with the rest of .inicio-loading once
   the video is actually ready (same opacity transition, no separate
   timing to keep in sync). Same technique as the sitewide .grain texture,
   just stronger — this is meant to read plainly as "still loading", not
   as an ambient atmosphere. */
.inicio-loading-grain {
  position: absolute;
  inset: -10%;
  width: 120%;
  height: 120%;
  opacity: 0.5;
  mix-blend-mode: screen;
  filter: sepia(0.55) saturate(1.9) brightness(1.05) contrast(1.1);
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.7 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 140px 140px;
  animation: grain-jitter 420ms steps(1) infinite;
}
.inicio-loading-mark {
  position: relative;
  width: 148px;
  height: 148px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.inicio-loading-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.inicio-loading-ring-track {
  fill: none;
  stroke: rgba(247,244,239,0.14);
  stroke-width: 1.2;
}
.inicio-loading-ring-fill {
  fill: none;
  stroke: rgba(247,244,239,0.75);
  stroke-width: 1.6;
  stroke-linecap: round;
  transition: stroke-dashoffset 260ms linear;
}
.inicio-loading-logo {
  position: relative;
  width: 58px;
  height: 58px;
  object-fit: contain;
}
.inicio-loading-pct {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(247,244,239,0.6);
  font-variant-numeric: tabular-nums;
}

/* Position (left/bottom) is the natural resting state; main.js overrides
   `transform` with a measured value to start it just under the big logo,
   then clears that override on the first scroll gesture so it settles
   here — same technique as the wordmark, and for the same reason (no
   guessed vw/vh, no flash on load). The bounce lives on the inner stack
   so it never fights with that positioning transform. */
.opening-scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 5.5vh;
  transform: translateX(-50%);
  z-index: 3;
  opacity: 1;
}
.js-ready .opening-scroll-cue {
  opacity: 0;
  transition: opacity 900ms var(--ease);
}
.js-ready .opening-scroll-cue.is-visible {
  opacity: 1;
}
.opening-chevron-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* A thin stroked line, not a bold text character — and each mark drifts
   through its own little side-to-side wave as it bobs, so the stack reads
   as one loose, serpentine line moving downward rather than three stacked
   ticks. */
.opening-chevron {
  display: block;
  width: 27px;
  height: 13px;
  overflow: visible;
}
.opening-chevron path {
  fill: none;
  stroke: var(--paper);
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.75)) drop-shadow(0 0 5px rgba(0,0,0,0.4));
}
.opening-chevron:nth-child(1) { opacity: 1; }
.opening-chevron:nth-child(2) { opacity: 0.78; }
.opening-chevron:nth-child(3) { opacity: 0.52; }
.js-ready .opening-scroll-cue.is-visible .opening-chevron:nth-child(1) {
  animation: opening-cue-serpentine 2.6s ease-in-out infinite;
}
.js-ready .opening-scroll-cue.is-visible .opening-chevron:nth-child(2) {
  animation: opening-cue-serpentine 2.6s ease-in-out infinite 0.18s;
}
.js-ready .opening-scroll-cue.is-visible .opening-chevron:nth-child(3) {
  animation: opening-cue-serpentine 2.6s ease-in-out infinite 0.36s;
}
@keyframes opening-cue-serpentine {
  0%, 100% { transform: translate(0, 0); }
  25% { transform: translate(2.5px, 3px); }
  50% { transform: translate(0, 6px); }
  75% { transform: translate(-2.5px, 3px); }
}

/* Film grain — moving, not a dead texture painted on top. Real emulsion
   grain shifts frame to frame; a static overlay reads as a digital filter,
   which is exactly what we're trying to get away from. */
.grain {
  position: absolute;
  /* No overextension needed anymore — see the animation comment below.
     Without a jitter that moves the whole layer, there's nothing that
     needs a margin to hide, and this also fully removes the double-grain
     seam two adjacent sections' grain used to create wherever they used
     to overlap (a section's grain can never overlap the section next to
     it if it never extends past its own box in the first place). */
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.125;
  /* screen adds light wherever the grain is, so it actually reads against
     dark footage. The sepia tint keeps that added light warm instead of
     sterile gray-white. Used sitewide, Inicio (Screen 0) included — it
     also keeps its own canvas dust field, this just layers on top of it.
     The noise itself must be WHITE with varying alpha, not black — screen
     blended with pure black is a mathematical no-op (screen(x,0)=x). Fixed
     by copying the alpha channel into R/G/B too (the "0 0 0 1 0" rows)
     instead of zeroing them, so the noise is actually white.
     Fifth pass. Every previous version animated by translating ONE noise
     image — however fast/slow or big/small the shift, that moves every
     fleck in the frame together, in the same direction, by the same
     amount, at the same time. That's exactly the "grains swaying/panning
     together" look — real grain looks alive because each particle is
     independent (uncorrelated frame to frame), which panning a single
     texture can never produce no matter how the timing is tuned. Fixed by
     not moving the layer at all (no transform, inset 0 above) and instead
     cycling the background-image itself between six different noise
     fields (different feTurbulence `seed`, same baseFrequency/size), so
     each step is a genuinely fresh, uncorrelated pattern — a swap, not a
     slide. */
  mix-blend-mode: screen;
  filter: sepia(0.35) brightness(1.04) contrast(1.05);
  pointer-events: none;
  background-size: 420px 420px;
  animation: grain-flicker 480ms steps(1) infinite;
}
/* A heavier pass for the outro/marquee/reel passage specifically — no
   text sits directly in this layer the way Screen 0's copy does, so it
   can run stronger without hurting legibility. Still fine-grained, just
   a bit more present than the base pass. */
.grain--strong {
  opacity: 0.155;
  filter: sepia(0.35) brightness(1.06) contrast(1.1);
}
/* Beats .reel-frame's explicit z-index:1 — see the grain div's own
   comment in index.html for why z-index:auto (this class's default)
   can't win that on DOM order alone. */
.grain--above-reel {
  z-index: 2;
}
/* Screen (used above) adds light — on a light/paper background that just
   bleaches out. Multiply darkens instead, so the same noise texture reads
   as flecks of dust/scratches against paper instead of disappearing.
   Multiply needs BLACK-with-alpha noise (white is multiply's no-op color,
   same problem as above but mirrored) — same six-seed swap as .grain
   above, just re-colored for the opposite blend mode and its own
   animation-name since the frames are a different image set. */
.grain--light {
  opacity: 0.1;
  mix-blend-mode: multiply;
  filter: contrast(1.1) brightness(0.96) sepia(0.12);
  background-size: 420px 420px;
  animation: grain-flicker-light 480ms steps(1) infinite;
}
@keyframes grain-flicker {
  0%   { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  16.6% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='7' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  33.3% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='13' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  50%   { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='21' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  66.6% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='34' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  83.3% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='42' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 0.9 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
}
@keyframes grain-flicker-light {
  0%   { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  16.6% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='7' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  33.3% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='13' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  50%   { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='21' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  66.6% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='34' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
  83.3% { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='420'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='2' seed='42' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1.2 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); }
}

/* Damage — occasional vertical scratches and a flicker, like a workprint
   that has been through a projector a few times. Rare and quick on purpose;
   constant damage would read as a decorative pattern, not wear. */
.film-damage {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0;
  background:
    repeating-linear-gradient(90deg, transparent 0, transparent 38%, rgba(255,255,255,0.55) 38.15%, transparent 38.3%, transparent 100%),
    repeating-linear-gradient(90deg, transparent 0, transparent 71%, rgba(255,255,255,0.35) 71.1%, transparent 71.25%, transparent 100%);
  animation: film-damage-flicker 7s linear infinite;
}
@keyframes film-damage-flicker {
  0%, 100% { opacity: 0; }
  91% { opacity: 0; }
  91.5% { opacity: 0.55; }
  92% { opacity: 0.1; }
  92.6% { opacity: 0.4; }
  93% { opacity: 0; }
  96.5% { opacity: 0; }
  97% { opacity: 0.3; }
  97.3% { opacity: 0; }
}

/* ============================================================
   Outro cycle — the typed line, the trust marquee and the reel all
   read as one continuous dark passage, not three stacked sections
   with their own background. One gradient spans the whole wrapper;
   .opening-outro and .reel stay separate elements only because the
   sticky scroll-scrub needs its own extra-height box — this wrapper
   itself is NOT positioned, so it never becomes an offsetParent and
   the scrollY math in main.js (which assumes document coordinates)
   keeps working unchanged.
   ============================================================ */
/* Starts at the exact same --ink the hero's own base-fade ends on, so
   there's no seam at that boundary — the lift to a lighter tone only
   happens gradually further down, never right at the top edge. */
/* Flat, not a gradient — a gradient here (spanning a very tall element)
   risked visible banding right at the boundary with the hero above,
   which read as a seam even when the two colors matched on paper. Flat
   --ink guarantees zero possibility of that; the grain overlay already
   does the work of keeping it from feeling like a dead void. */
/* A 2px negative margin, pulling this up to overlap the hero's own last
   couple pixels — even with matching colors on paper, two adjacent
   elements can still show a hairline seam at their exact boundary (a
   compositing/rasterization artifact, not a color mismatch: the hero
   above is GPU-layered via its video's transform, this isn't). Same
   solid color on both sides makes the 2px overlap invisible while
   removing the seam outright. */
.outro-cycle {
  /* z-index:0 (not just position:relative) keeps whatever z-index games
     happen in here — the reel's z-index:1 included — from leaking out
     and interfering with sections below. It does NOT, on its own, make
     the shared grain div (last child, see below) paint over the reel:
     a positioned element with an explicit z-index (.reel-frame is 1)
     always paints above a z-index:auto sibling regardless of DOM order,
     being-last-child only wins ties among elements that are ALSO
     z-index:auto (the .reveal/.about-fade blocks). The grain div gets
     its own explicit z-index below, specifically to beat the reel's. */
  position: relative;
  z-index: 0;
  margin-top: -6px;
  background: var(--ink);
}

/* Opening outro — natural, un-pinned scroll starts here. The hero screens
   above are the only place anything snaps or holds still; from here down,
   text, logos, heading and the reel frame are just stacked in normal flow
   with generous room between them, each one revealing itself once
   scrolled into view (.reveal — the same depth-of-field language used
   everywhere else on the site) while the page keeps moving underneath.
   No pin, no scroll-scrubbed math, no section crossing to measure. */
.opening-outro {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  padding: 20vh 0 18vh;
}
/* Margins instead of a uniform flex gap — every pair gets its own room
   except the last one: the heading stays close to the reel frame (see
   .reel-heading below) on purpose, everything before it gets generous
   space instead. */
.opening-outro-text {
  margin: 0 0 22vh;
  max-width: 100ch;
  padding: 0 5vw;
  font-family: var(--sans);
  font-weight: 300;
  font-size: clamp(19px, 2.3vw, 26px);
  line-height: 1.65;
  text-align: center;
  color: var(--paper);
}
.opening-outro-word {
  opacity: 0;
  transition: opacity 500ms var(--ease);
}
.opening-outro-word.is-visible {
  opacity: 1;
}

/* ============================================================
   Trust strip — two rows of logos drifting in opposite directions,
   fading to transparent at each edge. Pure white marks (each source
   logo was recolored + background-stripped, see assets/clients/) on
   the same dark surface as the outro text above. Reveals itself via
   .reveal (in the markup) like everything else in this passage now —
   no bespoke opacity/transform of its own to keep in sync with main.js.
   ============================================================ */
.clients-marquee {
  position: relative;
  z-index: 1;
  width: 100%;
  margin: 0 0 22vh;
  display: flex;
  flex-direction: column;
  gap: 3vh;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%);
}
.clients-row {
  overflow: hidden;
  width: 100%;
}
.clients-track {
  display: flex;
  align-items: center;
  width: max-content;
  gap: 8vw;
}
.clients-row--a .clients-track {
  animation: clients-drift-left 44s linear infinite;
}
.clients-row--b .clients-track {
  animation: clients-drift-right 40s linear infinite;
}
@keyframes clients-drift-left {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@keyframes clients-drift-right {
  from { transform: translateX(-50%); }
  to { transform: translateX(0); }
}
/* Every mark — plain logo or icon+name lockup — shares one height so
   the strip reads as one equal-weight set, not a few big names and a
   forgettable small one. */
.clients-item {
  display: flex;
  align-items: center;
  flex: none;
  height: 32px;
  opacity: 0.85;
}
.clients-logo {
  flex: none;
  height: 100%;
  width: auto;
  object-fit: contain;
}
.clients-item--lockup {
  gap: 12px;
}
.clients-logo-icon {
  flex: none;
  height: 100%;
  width: auto;
  object-fit: contain;
}
.clients-item--lockup {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.22;
  color: var(--paper);
  text-align: left;
  white-space: normal;
  max-width: 150px;
}
@media (prefers-reduced-motion: reduce) {
  .clients-row--a .clients-track,
  .clients-row--b .clients-track { animation: none; }
}

/* ============================================================
   Reel — "See for yourself." Plain, normal-flow content, still un-pinned
   — but the frame isn't a plain .reveal like its heading. It's visible
   (not opacity:0) the whole time, scaled down while it's still far away
   and growing continuously as it's scrolled toward, driven directly off
   its own position relative to the viewport (see main.js) rather than an
   on/off threshold — that continuous approach is the whole point, an
   on/off reveal would just make it "appear" already at full size.
   The heading stays close to the frame (small margin, not the passage's
   usual generous gap) — everything before it gets the room, this pair
   stays tight. ============================================================ */
.reel-heading {
  position: relative;
  z-index: 1;
  margin: 0 0 4vh;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(30px, 4.4vw, 52px);
  color: var(--paper);
}
.reel-frame {
  position: relative;
  z-index: 1;
  width: 88vw;
  max-width: 1400px;
  margin: 0 auto;
  aspect-ratio: 16 / 9;
  border: 1px solid rgba(247,244,239,0.18);
  box-shadow: 0 30px 90px rgba(0,0,0,0.45);
  background: #000;
}
/* Fullscreen goes on .reel-frame itself, not the bare <video> (see
   initReel, main.js) — that's what keeps the custom controls below
   usable while fullscreened, and it's also the whole reason the shared
   grain layer for this passage (a sibling well outside .reel-frame,
   see .outro-cycle's own grain div) disappears in fullscreen without
   any dedicated code: browsers only paint a fullscreened element's own
   subtree, so anything outside it just isn't there. */
.reel-frame:fullscreen,
.reel-frame:-webkit-full-screen {
  width: 100%;
  max-width: none;
  border: none;
}
.reel-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
  cursor: pointer;
}
/* The Spanish subtitle overlay — a plain div (see .reel-subtitle in
   index.html), written to by hand from the track's cuechange event
   (initReel, main.js) instead of relying on the browser's own native
   caption rendering. ::cue was the "correct" way to do this and it was
   tried first, but its property support is notoriously inconsistent
   across engines (some drop an entire rule over one unrecognized
   declaration) and at least one — almost certainly Safari — kept
   painting its own background and line spacing regardless of what the
   page's CSS said, with no further CSS lever left to pull. A plain div
   has none of that: ordinary specificity, ordinary properties, the same
   rendering in every engine. Verdana leads the font stack instead of
   the site's own Inter — a proper subtitle-purposed face (built for
   exactly this: small, legible over moving footage), noticeably
   wider-set than Inter's narrower grotesque forms. */
.reel-subtitle {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 15%;
  z-index: 2;
  text-align: center;
  pointer-events: none;
  font-family: Verdana, Tahoma, Geneva, var(--sans);
  font-weight: 400;
  font-size: clamp(18px, 2.5vw, 28px);
  line-height: 1.3;
  color: var(--paper);
  text-shadow: 0 2px 10px rgba(0,0,0,0.9), 0 1px 3px rgba(0,0,0,0.95);
}

.reel-chrome-top {
  position: absolute;
  left: 0;
  top: 14px;
  z-index: 2;
  display: flex;
  align-items: center;
  padding: 0 16px;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 12px;
  letter-spacing: 0.05em;
  color: var(--paper);
  text-shadow: 0 1px 4px rgba(0,0,0,0.6);
  pointer-events: none;
}
.reel-chrome-play::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 6px;
  border-radius: 50%;
  background: #e0473f;
  animation: reel-rec-blink 1.4s steps(1) infinite;
}
@keyframes reel-rec-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.15; }
}

/* The bottom control bar only earns its place once the visitor has
   actually chosen to watch (see .is-watching, set by enterWatch in
   main.js) — while it's just playing itself in the background there's
   nothing to control yet, so nothing shows. Styled to feel like part of
   this site rather than a generic player skin: the monospace timecode
   echoes .reel-chrome-top's own "REEL" readout above, the play button
   borrows the ringed-circle language .reel-play-button used to have,
   and the scrubber/volume fills are the same clay used for every other
   active-state accent on the site (nav hovers, dots, underlines). */
.reel-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  background: linear-gradient(0deg, rgba(9,8,7,0.88) 0%, rgba(9,8,7,0.55) 65%, transparent 100%);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity 320ms var(--ease), transform 320ms var(--ease);
}
.reel-frame.is-watching .reel-controls {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.reel-play-toggle {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 50%;
  background: rgba(247,244,239,0.06);
  border: 1px solid rgba(247,244,239,0.4);
  cursor: pointer;
  touch-action: manipulation;
  color: var(--paper);
  transition: border-color 220ms var(--ease), background 220ms var(--ease);
}
.reel-play-toggle:hover,
.reel-play-toggle:focus-visible {
  border-color: var(--clay);
  background: rgba(190,107,82,0.16);
}
.reel-play-toggle .reel-icon { margin-left: 2px; }
.reel-play-toggle.is-playing .reel-icon { margin-left: 0; }

.reel-control-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  touch-action: manipulation;
  color: rgba(247,244,239,0.75);
  transition: color 220ms var(--ease);
}
.reel-control-btn:hover,
.reel-control-btn:focus-visible {
  color: var(--paper);
}
.reel-icon {
  width: 16px;
  height: 16px;
  fill: currentColor;
}
.reel-icon--outline {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.reel-play-toggle [data-icon-pause],
.reel-play-toggle.is-playing [data-icon-play] { display: none; }
.reel-play-toggle.is-playing [data-icon-pause] { display: block; }

.reel-scrubber {
  position: relative;
  flex: 1 1 auto;
  height: 2px;
  border-radius: 1px;
  background: rgba(247,244,239,0.25);
  cursor: pointer;
  /* Hit-slop: the visible track stays a thin line, but the clickable
     area is much taller. */
  padding: 11px 0;
  margin: -11px 0;
  background-clip: content-box;
}
.reel-scrubber-fill {
  position: absolute;
  left: 0;
  top: 11px;
  height: 2px;
  width: 0%;
  background: var(--clay);
  pointer-events: none;
}
/* A small marker riding the leading edge of the fill — reads as a real
   scrubber head, not just a growing bar. Only appears once there's
   somewhere for it to sit (fill > 0) so it doesn't look like a stray
   dot glued to the far left at 0:00. */
.reel-scrubber-fill::after {
  content: '';
  position: absolute;
  right: -4px;
  top: 50%;
  transform: translateY(-50%) scale(0);
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--clay);
  box-shadow: 0 0 0 3px rgba(9,8,7,0.5);
  transition: transform 160ms var(--ease);
}
.reel-scrubber:hover .reel-scrubber-fill::after,
.reel-scrubbing .reel-scrubber-fill::after {
  transform: translateY(-50%) scale(1);
}

.reel-time {
  flex: 0 0 auto;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(247,244,239,0.75);
  min-width: 7em;
  text-align: right;
}
.reel-time-sep { opacity: 0.45; padding: 0 0.25em; }

.reel-mute-toggle [data-icon-sound-off] { display: none; }
.reel-mute-toggle.is-muted [data-icon-sound-on] { display: none; }
.reel-mute-toggle.is-muted [data-icon-sound-off] { display: block; }

.reel-volume {
  --reel-vol-pct: 100%;
  flex: 0 0 auto;
  width: 60px;
  height: 2px;
  -webkit-appearance: none;
  appearance: none;
  border-radius: 1px;
  background: linear-gradient(to right,
    var(--clay) 0%, var(--clay) var(--reel-vol-pct),
    rgba(247,244,239,0.25) var(--reel-vol-pct), rgba(247,244,239,0.25) 100%);
  cursor: pointer;
}
.reel-volume::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--paper);
  box-shadow: 0 0 0 3px rgba(9,8,7,0.5);
}
.reel-volume::-moz-range-thumb {
  width: 9px;
  height: 9px;
  border: none;
  border-radius: 50%;
  background: var(--paper);
  box-shadow: 0 0 0 3px rgba(9,8,7,0.5);
}
.reel-volume::-moz-range-progress {
  background: var(--clay);
  height: 2px;
  border-radius: 1px;
}

.reel-fullscreen-toggle [data-icon-fs-exit] { display: none; }
.reel-frame:fullscreen .reel-fullscreen-toggle [data-icon-fs-enter],
.reel-frame:-webkit-full-screen .reel-fullscreen-toggle [data-icon-fs-enter] { display: none; }
.reel-frame:fullscreen .reel-fullscreen-toggle [data-icon-fs-exit],
.reel-frame:-webkit-full-screen .reel-fullscreen-toggle [data-icon-fs-exit] { display: block; }

@media (max-width: 640px) {
  .reel-volume { display: none; }
  .reel-time { min-width: 3.2em; }
  .reel-time-full { display: none; }
}

/* ============================================================
   Discovery — one continuous montage across worlds (Act II)
   Beats are indexed by scroll progress, not stacked as sections.
   Crossfade duration is fixed regardless of scroll speed — scroll
   decides *which* beat is active, never scrubs its transition.
   ============================================================ */

.discovery {
  position: relative;
  /* Height is set inline by JS from the sum of each beat's data-weight —
     smoother, longer clips earn more scroll distance, not an equal share. */
  background: var(--ink);
  color: var(--paper);
}
.discovery-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

.discovery-beat {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1100ms var(--ease);
}
.discovery-beat.is-active { opacity: 1; z-index: 2; }

.discovery-frag {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.discovery-frag video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.06);
  filter: grayscale(0.25) brightness(0.74) contrast(1.05) blur(4px);
  transition: filter 1100ms var(--ease), transform 1100ms var(--ease);
}
.discovery-beat.is-active .discovery-frag video {
  transform: scale(1);
  filter: grayscale(0.15) brightness(0.8) contrast(1.04) blur(0);
}

/* Once a beat has settled, the frame keeps a slow, near-imperceptible drift
   instead of freezing solid — it reads as a held shot, not a paused photo. */
@keyframes discovery-drift { from { transform: scale(1); } to { transform: scale(1.045); } }
.discovery-beat.is-active .discovery-frag video.is-holding {
  animation: discovery-drift 9s ease-in-out infinite alternate;
}

/* Transition language varies by beat so the sequence reads as an edit, not
   a slideshow — no two consecutive cuts should feel identical.
   .dissolve (default above) — calm, contemplative shots.
   .cut — a percussive, near-instant edit for a beat that addresses camera.
   .punch — starts more zoomed, snaps down assertively, for emotional peaks.
   .pan — a lateral drift alongside the fade, for handheld/movement shots. */
.discovery-beat--cut { transition: opacity 160ms linear; }
.discovery-beat--cut .discovery-frag video {
  transition: filter 160ms linear, transform 160ms linear;
  filter: grayscale(0.15) brightness(0.8) contrast(1.04) blur(0);
  transform: scale(1.015);
}
.discovery-beat--cut.is-active .discovery-frag video { transform: scale(1); }

.discovery-beat--punch .discovery-frag video { transform: scale(1.18); }
.discovery-beat--punch.is-active .discovery-frag video {
  transition: filter 1300ms cubic-bezier(0.16,1,0.3,1), transform 1300ms cubic-bezier(0.16,1,0.3,1);
}

.discovery-beat--pan .discovery-frag video { transform: scale(1.1) translateX(-2.4%); }
.discovery-beat--pan.is-active .discovery-frag video { transform: scale(1.015) translateX(0); }

/* The framed beat: a contained shot on bare black, not fullbleed — a
   deliberate change of scale so the montage doesn't behave the same way
   every time it changes. */
.discovery-beat--framed .discovery-frag {
  top: 50%; left: 50%; right: auto; bottom: auto;
  transform: translate(-50%, -50%);
  width: min(72vw, 128vh);
  aspect-ratio: 16 / 9;
  border-radius: 3px;
  box-shadow: 0 40px 90px rgba(0,0,0,0.45);
}

.discovery-beat--black {
  background: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 8vw;
}

.discovery-scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: linear-gradient(180deg, rgba(20,18,15,0.1) 0%, rgba(20,18,15,0.05) 50%, rgba(20,18,15,0.5) 100%);
  pointer-events: none;
}

/* A quiet sense of "where am I" in the sequence — not a UI control, just
   an orientation mark so the montage reads as a journey with a shape. */
.discovery-progress {
  position: absolute;
  right: 4vw;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  width: 2px;
  height: 130px;
  background: rgba(247,244,239,0.16);
  pointer-events: none;
}
.discovery-progress-fill {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 0%;
  background: var(--clay);
  transition: height 400ms var(--ease);
}
@media (max-width: 860px) {
  .discovery-progress { display: none; }
}

/* Text lives only on its own black beat — never layered over a moving
   image, always given the full frame and real size. */
.dtext {
  margin: 0;
  max-width: 24ch;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(28px, 4.4vw, 52px);
  line-height: 1.25;
  color: var(--paper);
  opacity: 0;
  filter: blur(10px);
  transform: scale(1.03);
  transition: opacity 1200ms var(--ease), filter 1200ms var(--ease), transform 1200ms var(--ease);
}
.discovery-beat--black.is-active .dtext { opacity: 1; filter: blur(0); transform: scale(1); }

.dtext--large {
  max-width: 20ch;
  font-size: clamp(36px, 6vw, 72px);
}

/* ============================================================
   Sections on paper (Act III + interior pages)
   ============================================================ */

.section {
  position: relative;
  padding: 13vh 0;
}
.section--tight { padding: 8vh 0; }
.section--stone { background: var(--mist); }

.section-head {
  max-width: 640px;
  margin-bottom: 6vh;
}
.section-head h2 { font-size: clamp(28px, 4vw, 46px); line-height: 1.15; }
.section-head p {
  margin-top: 16px;
  color: var(--ink-soft);
  font-size: 17px;
  max-width: 52ch;
}

/* Depth of field, not fade/scale: things arrive by coming into focus, the way
   attention actually moves in a shot, rather than sliding or fading into place. */
.reveal {
  opacity: 0;
  filter: blur(14px);
  transform: scale(1.03);
  transition: opacity 1000ms var(--ease), filter 1000ms var(--ease), transform 1000ms var(--ease);
}
.reveal.in-view { opacity: 1; filter: blur(0); transform: scale(1); }

/* featured story */
.feature {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 6vw;
  align-items: center;
}
.feature-media {
  aspect-ratio: 4/5;
  overflow: hidden;
  border-radius: 2px;
}
.feature-media img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(0.1) sepia(0.08) contrast(1.04) brightness(0.98);
}
.feature-eyebrow { margin-bottom: 18px; }
.feature h2 { font-size: clamp(30px, 4.4vw, 50px); line-height: 1.14; margin-bottom: 18px; }
.feature p.one-liner { font-size: 18px; color: var(--ink-soft); max-width: 46ch; }
.facts {
  margin-top: 28px;
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.03em;
  color: var(--gray);
  display: flex;
  flex-wrap: wrap;
  gap: 8px 0;
}
.facts span:not(:last-child)::after { content: '—'; margin: 0 10px; color: var(--stone); }
.consequence {
  margin-top: 16px;
  font-family: var(--serif);
  font-style: italic;
  font-size: 16px;
  color: var(--clay-deep);
}
.text-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 34px;
  font-family: var(--sans);
  font-size: 14px;
  letter-spacing: 0.04em;
  color: var(--clay-deep);
  border-bottom: 1px solid transparent;
  padding-bottom: 3px;
  transition: border-color var(--dur-fast) var(--ease), gap var(--dur-fast) var(--ease);
}
.text-link:hover, .text-link:focus-visible { border-color: var(--clay-deep); gap: 12px; }

/* ============================================================
   Method — four words, four distinct treatments, one language
   ============================================================ */

/* Dark, on ink — the register carries through from Discovery and Film
   rather than dropping into a lighter, more conventional services layout. */
.method {
  position: relative;
  padding: 18vh 0;
  background: var(--ink);
  color: var(--paper);
}
.method-word {
  max-width: 640px;
  padding: 5vh 0;
  border-top: 0.5px solid rgba(247,244,239,0.14);
}
.method-word:first-child { border-top: none; }
.method-title {
  font-size: clamp(34px, 5.6vw, 64px);
  line-height: 1;
  color: var(--paper);
}
.method-line {
  margin-top: 14px;
  font-size: 16px;
  color: rgba(247,244,239,0.72);
  max-width: 44ch;
}

.method-word--b { margin-left: auto; text-align: right; }
.method-word--b .method-line { margin-left: auto; }
.method-word--c .method-title { font-style: italic; }
.method-word--d { margin-left: 8vw; }
.method-word--d .method-title { color: var(--clay); }

/* process teaser */
.process-teaser {
  text-align: center;
  max-width: 620px;
  margin: 0 auto;
}
.process-teaser .display { font-size: clamp(24px, 3.2vw, 34px); }

/* closing */
.closing {
  position: relative;
  background: var(--ink);
  color: var(--paper);
  text-align: center;
  padding: 18vh 0 14vh;
}
.closing-echo {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 16px;
  color: rgba(247,244,239,0.5);
  margin-bottom: 26px;
}
.closing h2 {
  font-size: clamp(30px, 5.4vw, 60px);
  line-height: 1.2;
  max-width: 15ch;
  margin: 0 auto;
}
.closing .text-link {
  color: var(--clay);
  border-color: transparent;
  margin-top: 40px;
  font-size: 15px;
}
.closing .text-link:hover, .closing .text-link:focus-visible { border-color: var(--clay); }

/* ============================================================
   Film — an unnamed, contemplative moment. No title, no cortinilla,
   no play button. It slows down, fills the screen, speaks in three
   quiet lines, then lets go.
   ============================================================ */

.film {
  position: relative;
  background: var(--paper);
}

.film-lead {
  height: 10vh;
}

.film-track {
  position: relative;
  height: 240vh;
}
.film-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
.film-blackout {
  position: absolute;
  inset: 0;
  background: var(--ink);
  opacity: 0;
  z-index: 5;
  pointer-events: none;
  transition: opacity 1100ms var(--ease);
}
.film-blackout.is-visible {
  opacity: 1;
  transition: opacity 1700ms var(--ease);
}
.film-card {
  position: relative;
  z-index: 6;
  width: min(94vw, 138.6vh);
  aspect-ratio: 16 / 9;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 30px 70px rgba(20,18,15,0.28), inset 0 0 0 rgba(0,0,0,0);
  transform: scale(1);
  will-change: transform, border-radius;
  transition: transform 1000ms cubic-bezier(0.4, 0, 0.2, 1), border-radius 1000ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 1000ms var(--ease);
}
.film-card.is-full {
  transform: scale(var(--full-scale, 1.3));
  border-radius: 0px;
  box-shadow: 0 0 0 rgba(20,18,15,0), inset 0 0 140px rgba(0,0,0,0.35);
  transition: transform 1900ms cubic-bezier(0.5, 0, 0.18, 1), border-radius 1900ms cubic-bezier(0.5, 0, 0.18, 1), box-shadow 1900ms var(--ease);
}
.film-card video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.3) sepia(0.1) contrast(1.05) brightness(0.62);
}

/* Three lines, staged in time once the frame is immersed — not gated by
   further scrolling. Each replaces the last rather than stacking up. */
.film-text {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 8vw;
  pointer-events: none;
}
.film-text p {
  position: absolute;
  max-width: 26ch;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(22px, 3.4vw, 40px);
  color: var(--paper);
  opacity: 0;
  filter: blur(10px);
  transform: scale(1.02);
  transition: opacity 1300ms var(--ease), filter 1300ms var(--ease), transform 1300ms var(--ease);
}
.film-text p[data-film-line="3"] {
  font-family: var(--sans);
  font-style: normal;
  font-weight: 300;
  font-size: clamp(16px, 1.8vw, 21px);
  max-width: 38ch;
  color: rgba(247,244,239,0.9);
}
.film-text p.is-active { opacity: 1; filter: blur(0); transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  .film-lead { height: 6vh; }
  .film-track { height: auto; padding: 4vh 0 10vh; }
  .film-stage { position: relative; height: auto; }
  .film-card { transform: none !important; margin: 0 auto; }
  .film-blackout { display: none; }
  .film-text p { position: static; opacity: 1 !important; filter: none !important; margin-bottom: 14px; }
  .film-text { flex-direction: column; position: static; padding: 4vh 8vw; }
}

@media (max-width: 860px) {
  .film-card { width: 94vw; height: 42vh; aspect-ratio: auto; }
}

/* ============================================================
   Footer
   ============================================================ */

.site-footer {
  position: relative;
  padding: 7vh 0 5vh;
  border-top: 0.5px solid var(--stone);
}
.site-footer.on-dark {
  border-top-color: rgba(247,244,239,0.15);
  background: var(--ink);
  color: rgba(247,244,239,0.7);
}
.footer-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 24px;
}
.footer-links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  margin: 0; padding: 0;
  font-size: 13px;
}
.footer-links a:hover, .footer-links a:focus-visible { color: var(--clay-deep); }
.footer-meta {
  font-size: 12px;
  color: var(--gray);
  margin-top: 22px;
}

/* ============================================================
   Interior page hero (Stories / Process / About)
   ============================================================ */

.page-head {
  padding: 22vh 0 8vh;
}
.page-head .eyebrow { margin-bottom: 18px; }
.page-head h1 { font-size: clamp(34px, 6vw, 64px); line-height: 1.1; max-width: 16ch; }
.page-head p { margin-top: 22px; font-size: 18px; color: var(--ink-soft); max-width: 56ch; }

/* ============================================================
   Stories viewer (one at a time)
   ============================================================ */

.stories-viewer {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: 6vw;
  align-items: center;
  min-height: 62vh;
}
.stories-media { position: relative; aspect-ratio: 4/5; overflow: hidden; border-radius: 2px; }
.stories-media img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(0.1) sepia(0.08) contrast(1.04) brightness(0.98);
  transition: opacity 550ms var(--ease), filter 550ms var(--ease), transform 550ms var(--ease);
}
.stories-body.is-switching .stories-media img {
  opacity: 0;
  filter: grayscale(0.1) sepia(0.08) contrast(1.04) brightness(0.98) blur(16px);
  transform: scale(1.04);
}
.stories-count {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--gray);
}
.stories-title { margin-top: 16px; font-size: clamp(28px, 3.6vw, 44px); line-height: 1.15; }
.stories-oneliner { margin-top: 16px; font-size: 17px; color: var(--ink-soft); max-width: 46ch; }
.stories-facts { margin-top: 22px; }
.stories-body { transition: opacity 420ms var(--ease), filter 420ms var(--ease); }
.stories-body.is-switching { opacity: 0; filter: blur(10px); }

.stories-controls {
  display: flex;
  align-items: center;
  gap: 26px;
  margin-top: 46px;
}
.stories-controls button {
  background: none; border: none; cursor: pointer;
  touch-action: manipulation;
  font-family: var(--sans); font-size: 13px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink);
  display: flex; align-items: center; gap: 8px;
  /* Hit-slop: same trick as nav-overlay-close — 44px tap target, same visual position. */
  padding: 14px 0;
  margin: -14px 0;
}
.stories-controls button:hover, .stories-controls button:focus-visible { color: var(--clay-deep); }
.stories-controls .divider { width: 0.5px; height: 16px; background: var(--stone); }

/* ============================================================
   Case study chapters
   ============================================================ */

.chapter { padding: 11vh 0; }
.chapter-num {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--gray);
  margin-bottom: 16px;
}
.chapter h2 { font-size: clamp(26px, 3.6vw, 42px); line-height: 1.18; max-width: 20ch; }
.chapter .pull {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(22px, 2.6vw, 30px);
  line-height: 1.4;
  color: var(--ink);
  max-width: 26ch;
  margin: 26px 0;
}
.chapter p.body-copy {
  font-size: 17px;
  color: var(--ink-soft);
  max-width: 58ch;
  margin-top: 20px;
}
.chapter-media {
  margin-top: 6vh;
  aspect-ratio: 16/9;
  overflow: hidden;
  border-radius: 2px;
}
.chapter-media img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(0.12) sepia(0.08) contrast(1.04) brightness(0.96);
}

/* ============================================================
   About page
   ============================================================ */

.about-portrait {
  display: grid;
  grid-template-columns: 0.7fr 1.3fr;
  gap: 6vw;
  align-items: center;
}
.about-portrait img {
  aspect-ratio: 4/5; object-fit: cover; width: 100%;
  filter: grayscale(0.4) sepia(0.06) contrast(1.05) brightness(0.95);
  border-radius: 2px;
}
.about-credit {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--gray);
  margin-top: 14px;
}

/* ============================================================
   About, inline on the homepage — lives inside .outro-cycle,
   shares its dark background. Each .about-fade block is driven by
   updateAboutFade in main.js: full opacity near the center of the
   viewport, fading (and drifting) out toward whichever edge it's
   leaving through as the visitor scrolls past — not a one-shot
   .reveal, it can fade back in if they scroll back up to it.
   ============================================================ */

.about-inline {
  position: relative;
  padding: 8vh 0 4vh;
}

/* content-box on purpose throughout this section (overriding the sitewide
   border-box reset) — each max-width below is meant to set the actual
   content width; with border-box the side padding would eat into it
   instead of extending past it. min() keeps that width from overflowing
   on viewports narrower than the fixed px ceiling. */

/* A contained strip, not fullscreen — the section's own small hero,
   title centered over the video. background:var(--ink) is new — this
   is the one lazy-loaded banner (see data-lazy-video, main.js) whose
   container had no fallback color of its own, unlike the others here
   (.story-category-banner already had #000; .stories-banner and the
   How We Work cards already sit on their section's own dark
   background). Without it, the brief window before the video's src
   loads would show the page's own light --paper straight through —
   this only affects that transient loading gap, never the settled,
   already-playing state, which looks identical either way. */
.about-banner {
  position: relative;
  width: 100%;
  height: 42vh;
  min-height: 300px;
  max-height: 520px;
  overflow: hidden;
  margin-bottom: 8vh;
  background: var(--ink);
}
.about-banner video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.12) contrast(1.06) brightness(0.85) saturate(1.03);
}
.about-banner-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(12,11,9,0.32) 0%, rgba(12,11,9,0.5) 100%);
}
.about-banner-title {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0 6vw;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(34px, 5.5vw, 64px);
  line-height: 1.1;
  color: var(--paper);
  text-align: center;
}

.about-inline-copy {
  box-sizing: content-box;
  max-width: min(880px, 74vw);
  margin: 0 auto;
  padding: 0 6vw;
}

.about-fade { will-change: opacity, transform, filter; }

.about-lead {
  box-sizing: content-box;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(24px, 3.2vw, 36px);
  line-height: 1.25;
  color: var(--paper);
  margin: 0;
}

.about-copy {
  font-family: var(--sans);
  font-weight: 300;
  font-size: clamp(17px, 1.8vw, 20px);
  line-height: 1.6;
  color: rgba(247,244,239,0.82);
  margin-top: 4vh;
}
.about-copy:first-child { margin-top: 0; }
.about-copy strong {
  font-weight: 600;
  color: var(--paper);
}

/* Sits right under the photo, in its own narrow column — back to
   centered, matching that column again rather than a full-width row. */
.about-signature {
  margin-top: 2vh;
  text-align: center;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.02em;
  line-height: 1.4;
  color: var(--paper);
}
.about-signature span {
  display: block;
  margin-top: 2px;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 0.01em;
  color: rgba(247,244,239,0.7);
}

/* Back to the original side-by-side layout (photo in its own small
   column, text beside it) — the stacked full-width treatment tried
   briefly here didn't hold: at that size a landscape crop wide enough
   to stay uncropped also pulled in a second person standing in the
   background of the original photo, which wasn't wanted at any size.
   Square now instead of the original 2/3 portrait (Diego's own call),
   cropped tight on him alone — same footprint on the page as before,
   just a different shape within it. */
.about-final {
  box-sizing: content-box;
  display: grid;
  grid-template-columns: 230px 1fr;
  gap: 3vw;
  align-items: start;
  max-width: min(900px, 74vw);
  margin: 6vh auto 0;
  padding: 0 6vw;
}
.about-final-photo { max-width: 230px; }
.about-final img {
  width: 100%;
  aspect-ratio: 1/1;
  object-fit: cover;
  border-radius: 14px;
  filter: grayscale(0.35) sepia(0.06) contrast(1.05) brightness(0.94);
}
.about-final-copy .about-copy:first-child { margin-top: 0; }

/* ============================================================
   Stories We've Told — project gallery, grouped by kind
   ============================================================ */

.stories-section {
  position: relative;
  background: var(--ink);
  padding: 12vh 0;
}
.stories-section .container {
  max-width: 1560px;
  padding: 0 3vw;
}
.stories-section-head {
  max-width: 1000px;
  margin: 0 auto 10vh;
}
/* The "Our Work" clip from the render library, as a modest banner (not
   full section width, not full-bleed) with the title set right inside
   it instead of floating a separate chip nearby. Height is driven by
   main.js as a function of distance from viewport-center — tallest when
   centered, shrinking symmetrically as it scrolls away either direction
   — rather than sitting at one fixed size like the plain full-width
   video banners upstream (Screen 0, the reel). The transition is what
   turns each new height main.js sets on scroll into a smoothed chase
   instead of a snap; without it, every scroll/wheel tick jumps straight
   to its target height and reads as an abrupt accordion-like bounce,
   worst right around center where the target is changing fastest. */
.stories-banner {
  position: relative;
  height: 220px;
  transition: height 340ms var(--ease);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 30px 70px rgba(0,0,0,0.5);
}
.stories-banner-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.2) sepia(0.08) contrast(1.08) brightness(0.62);
}
.stories-banner-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(9,8,7,0.3) 0%, rgba(9,8,7,0.58) 100%);
}
.stories-section-title {
  position: relative;
  z-index: 1;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(44px, 6.4vw, 84px);
  line-height: 1.05;
  color: var(--paper);
  text-align: center;
  text-shadow: 0 4px 30px rgba(0,0,0,0.6);
  padding: 0 4vw;
}

.story-group {
  padding: 7vh 0;
  border-top: 1px solid rgba(247,244,239,0.14);
}
.story-group:last-child {
  border-bottom: 1px solid rgba(247,244,239,0.14);
}

/* Category banner — one per group, a wide frame spanning the full
   container width (same margins as the divider line above it, not the
   page edge) with the category's own title/description/button overlaid
   bottom-left directly on it. Each one is a real rendered sequence from
   the Biblioteca Visual render library — seq-cc7cb33d21 ("Films for
   organizations") and seq-3135c07b58 ("Documentary films"), built there
   specifically for this — not a static image, and not the site's own
   footage selection; 1920×600 output, so the aspect-ratio here matches
   that exactly rather than assuming 16/9. Replaces the earlier
   two-column layout (a clickable example story on one side, copy on the
   other) — the banner itself now carries that introductory weight, so
   nothing here is a link into a specific story any more, just the
   category's own presentation. */
.story-category-banner {
  position: relative;
  width: 100%;
  aspect-ratio: 1920/600;
  max-height: 520px;
  border-radius: 3px;
  overflow: hidden;
  background: #000;
}
.story-category-banner-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.2) sepia(0.08) contrast(1.08) brightness(0.62);
}
/* A fixed-height band, not a percentage of the box — percentage stops on
   a vertical gradient read completely differently depending on the box's
   own aspect ratio, and this box's ratio swings hard between desktop
   (wide, short) and mobile (the 4/5 override below, tall): the same
   0%/42%/75% stops that read as a restrained bottom scrim on the wide
   banner buried the whole frame in a flat dark wash on the tall one. A
   fixed 240px band keeps the darkened area (and how much footage stays
   visible above it) consistent across both shapes. */
.story-category-banner-scrim {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 240px;
  background: linear-gradient(0deg, rgba(9,8,7,0.95) 0%, rgba(9,8,7,0.6) 55%, rgba(9,8,7,0) 100%);
}
/* Text bottom-left, button bottom-right, both sitting on the same
   baseline — not stacked with the button trailing below in its own
   dead space. gap lets the text wrap onto more lines on narrower
   screens without ever touching the button's own column. */
.story-category-banner-copy {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 4vw;
  padding: 3vh 4vw;
}
.story-category-banner-text {
  max-width: 50ch;
  min-width: 0;
}

.story-feature-heading {
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(28px, 3.4vw, 44px);
  line-height: 1.12;
  color: var(--paper);
}
/* Bumped up from the original 0.6-opacity/regular-weight pairing, which
   read as too quiet against the banner's own footage — still clearly
   secondary to the heading, just no longer fighting to be read. */
.story-feature-desc {
  margin-top: 16px;
  font-size: 15.5px;
  font-weight: 500;
  line-height: 1.6;
  color: rgba(247,244,239,0.88);
  text-shadow: 0 2px 10px rgba(0,0,0,0.45);
}

/* Flex-wrap + justify-content:center instead of a fixed grid-template —
   a grid stretches every row (including a short last row) to fill all
   the columns, which is exactly what made a leftover card sit alone off
   to one side. Flex centers each row as its own block. --story-cols is
   set per-grid by main.js (capped at 4, chosen to divide that group's
   card count as evenly as possible — a group of 4 fills one row, a
   group of 6 splits into two even rows of 3) so the card width below is
   always exactly right for that group's row math, not just a guess. */
.story-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.6vw;
  padding: 5vh 0 1vh;
  --story-cols: 3;
}
.story-grid .story-card {
  flex: 0 0 calc((100% - (var(--story-cols) - 1) * 1.6vw) / var(--story-cols));
  max-width: 460px;
}

.story-card {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 2/3;
  overflow: hidden;
  border-radius: 4px;
  background: #000;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: left;
  box-shadow: 0 14px 34px rgba(0,0,0,0.4);
}
.story-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.3) sepia(0.06) contrast(1.08) brightness(0.8);
  transform: scale(1.02);
  transition: transform 700ms var(--ease), filter 700ms var(--ease);
}
/* Injected on hover/focus — a muted, looping youtube-nocookie embed swapped
   in over the poster. pointer-events:none so the click that opens the full
   lightbox still lands on the .story-card button underneath, and so no
   YouTube hover chrome (title, cursor) can ever surface on the tiny preview.
   Width/height are set inline by main.js (not 100%/100% here) — a YouTube
   embed is a responsive page that resizes its own video to fill whatever
   box the iframe is given, so stretching the iframe to a portrait card
   would squash the 16:9 video rather than crop it. main.js instead sizes
   the iframe to real 16:9 dimensions guaranteed to cover the card, and
   this rule just centers that oversized box so the card's own
   overflow:hidden clips the excess evenly. */
.story-card-preview {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border: 0;
  pointer-events: none;
  /* Stays invisible until main.js adds .is-visible — YouTube briefly shows
     its own title-card (video title + channel avatar) over the first
     second or two of any embed's playback, and that lives inside the
     iframe's document where we have no CSS/JS reach (cross-origin). The
     poster stays on top for that window instead; by the time we reveal
     the iframe, YouTube's own overlay has already auto-faded. */
  opacity: 0;
  transition: opacity 400ms var(--ease);
}
.story-card-preview.is-visible {
  opacity: 1;
}
.story-card-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(12,11,9,0) 50%, rgba(12,11,9,0.92) 100%);
  opacity: 0.8;
  transition: opacity 400ms var(--ease);
}
.story-card-text {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 14px 16px;
  transform: translateY(8px);
  opacity: 0;
  transition: transform 400ms var(--ease), opacity 400ms var(--ease);
}
.story-card-title {
  display: block;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: 25px;
  line-height: 1.16;
  color: var(--paper);
}
.story-card-category {
  display: block;
  margin-top: 8px;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0.05em;
  color: rgba(247,244,239,0.7);
}
.story-card:hover,
.story-card:focus-visible {
  box-shadow: 0 20px 46px rgba(0,0,0,0.55);
}
.story-card:hover .story-poster,
.story-card:focus-visible .story-poster {
  transform: scale(1.08);
  filter: grayscale(0) sepia(0.06) contrast(1.05) brightness(0.9);
}
.story-card:hover .story-card-scrim,
.story-card:focus-visible .story-card-scrim {
  opacity: 0.95;
}
.story-card:hover .story-card-text,
.story-card:focus-visible .story-card-text {
  transform: translateY(0);
  opacity: 1;
}
.story-card:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }

/* ============================================================
   How We Work — "how does Arcillafilms work", right after Stories
   ============================================================ */

.how-we-work {
  position: relative;
  background: var(--ink);
  padding: 4vh 0 14vh;
}
.how-we-work-head {
  max-width: 620px;
  margin: 0 auto 6vh;
  text-align: center;
}
.how-we-work-head .eyebrow { color: rgba(247,244,239,0.55); }
.how-we-work-head h2 {
  margin-top: 16px;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(32px, 4.6vw, 54px);
  line-height: 1.12;
  color: var(--paper);
}
.how-we-work-intro {
  margin-top: 22px;
  font-size: 17px;
  line-height: 1.6;
  color: rgba(247,244,239,0.72);
}

/* Four rows, video alternating left/right, plain document flow — no
   scroll capture of any kind. Each row is just a .reveal block (the
   same fade/blur/scale entrance used sitewide), so it appears once
   scrolled to and the visitor's own scroll never gets touched. */
.how-we-work-steps {
  max-width: 1020px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 11vh;
}
.how-we-work-step {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6vw;
  align-items: center;
}
.how-we-work-step--reverse .how-we-work-step-media { order: 2; }
.how-we-work-step--reverse .how-we-work-step-copy { order: 1; }

.how-we-work-step-media {
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: 3px;
  overflow: hidden;
  box-shadow: 0 30px 80px rgba(0,0,0,0.4);
}
.how-we-work-step-media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.15) sepia(0.06) contrast(1.06) brightness(0.85);
  /* A slow, continuous, always-on Ken Burns drift — just a bit of life
     behind the frame, not tied to any interaction. */
  animation: how-we-work-drift 26s ease-in-out infinite alternate;
}
@media (prefers-reduced-motion: reduce) {
  .how-we-work-step-media video { animation: none; }
}
@keyframes how-we-work-drift {
  from { transform: scale(1); }
  to { transform: scale(1.08); }
}

.how-we-work-step-copy { text-align: left; }
.how-we-work-number {
  display: block;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--clay);
}
.how-we-work-step-copy h3 {
  margin-top: 10px;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(24px, 3vw, 32px);
  line-height: 1.2;
  color: var(--paper);
}
.how-we-work-step-copy p {
  margin-top: 16px;
  max-width: 42ch;
  font-size: 16px;
  line-height: 1.65;
  color: rgba(247,244,239,0.72);
}

@media (max-width: 760px) {
  .how-we-work-step {
    grid-template-columns: 1fr;
    gap: 22px;
  }
  .how-we-work-step--reverse .how-we-work-step-media,
  .how-we-work-step--reverse .how-we-work-step-copy {
    order: initial;
  }
  .how-we-work-steps { gap: 8vh; }
}

/* ============================================================
   Contact — the homepage's closing chapter
   ============================================================ */

/* The pause: a quiet band of video — not a full screen — behind one
   line of type, a breath between Process and Contact rather than
   another content section in its own right. The line's own reveal
   (see .contact-pause-line below) is what carries the moment now; there
   is no scroll-hold, ordinary scroll just carries the visitor past. */
.contact-pause {
  position: relative;
  height: 70vh;
  min-height: 600px;
  max-height: 800px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--ink);
}
.contact-pause-media {
  position: absolute;
  inset: 0;
}
.contact-pause-media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.15) sepia(0.06) contrast(1.05) brightness(0.7);
}
.contact-pause-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(9,8,7,0.45) 0%, rgba(9,8,7,0.35) 50%, rgba(9,8,7,0.55) 100%);
}
/* Visibility is driven by a plain IntersectionObserver (initContactPause
   in main.js) that just adds .is-visible once, the first time the line
   crosses into view — no scroll-hold, no forced stop. A slow, sober
   title-card arrival: letters start spread wide and slightly out of
   focus, as if the lens and the type are both still settling, then
   draw together into sharp, normal tracking. cubic-bezier(0.16,1,0.3,1)
   is a strong "expo-out" — fast to begin, then a long, weighted
   deceleration. No vertical movement: the letter-spacing collapse and
   the focus pull carry it on their own. Same size register as the
   Inicio headline — this is meant to land with the same weight. */
.contact-pause-line {
  position: relative;
  z-index: 1;
  margin: 0 auto;
  white-space: nowrap;
  text-align: center;
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(24px, 2.9vw, 38px);
  color: var(--paper);
  text-shadow: 0 4px 24px rgba(0,0,0,0.5);
  opacity: 0;
  letter-spacing: 0.28em;
  filter: blur(7px);
  transform: scale(1.03);
  transition:
    opacity 3600ms cubic-bezier(0.16, 1, 0.3, 1),
    letter-spacing 3600ms cubic-bezier(0.16, 1, 0.3, 1),
    filter 3600ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 3600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.contact-pause-line.is-visible {
  opacity: 1;
  letter-spacing: 0.01em;
  filter: blur(0);
  transform: scale(1);
}
@media (prefers-reduced-motion: reduce) {
  .contact-pause-line { opacity: 1; letter-spacing: 0.01em; filter: blur(0); transform: none; }
}

.contact {
  position: relative;
  background: var(--ink);
  padding: 16vh 0 24vh;
}
.contact-inner {
  max-width: 640px;
  margin: 0 auto;
  text-align: center;
}
.contact-inner h2 {
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(38px, 5.2vw, 62px);
  line-height: 1.1;
  color: var(--paper);
}
.contact-copy {
  margin: 24px auto 0;
  max-width: 46ch;
  font-size: 17px;
  line-height: 1.65;
  color: rgba(247,244,239,0.68);
}

/* The one thing to actually act on — still sized past the body copy so
   it reads as the call to action, just no longer competing with the
   heading itself. Underline is drawn with a pseudo-element rather than
   text-decoration so its opacity/position can ease in on hover instead
   of just snapping on. Clicking copies the address (see
   initContactEmailCopy, main.js) rather than opening a mail client —
   .is-copied is what's showing while the confirmation text is up. */
.contact-email-wrap {
  display: inline-block;
  position: relative;
  margin-top: 56px;
}
.contact-email {
  display: inline-block;
  position: relative;
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(19px, 2.4vw, 26px);
  letter-spacing: -0.005em;
  color: var(--paper);
  text-decoration: none;
  transition: color 400ms var(--ease);
}
.contact-email::after {
  content: '';
  position: absolute;
  left: 1px;
  right: 1px;
  bottom: -4px;
  height: 1px;
  background: currentColor;
  opacity: 0.3;
  transform: scaleX(0.94);
  transform-origin: center;
  transition: opacity 400ms var(--ease), transform 400ms var(--ease);
}
.contact-email:hover,
.contact-email:focus-visible,
.contact-email.is-copied {
  color: var(--clay);
}
.contact-email:hover::after,
.contact-email:focus-visible::after,
.contact-email.is-copied::after {
  opacity: 0.9;
  transform: scaleX(1);
}
/* A quiet hint, not a full custom cursor: on hover/focus it rises just
   above the address to say plainly what the click does — copy, not
   "open a mail client" (the href is only a no-JS/Clipboard-API-missing
   fallback, see initContactEmailCopy in main.js). Hidden from touch —
   there's no hover state to announce it on, and the click behavior
   there is the same either way. */
.contact-email-hint {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 10px);
  transform: translate(-50%, 4px);
  white-space: nowrap;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--clay);
  padding: 6px 12px;
  border-radius: 3px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms var(--ease), transform 220ms var(--ease);
}
.contact-email-hint::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--clay);
}
.contact-email:hover ~ .contact-email-hint,
.contact-email:focus-visible ~ .contact-email-hint {
  opacity: 1;
  transform: translate(-50%, 0);
}
.contact-email.is-copied ~ .contact-email-hint {
  opacity: 0;
}
@media (hover: none) {
  .contact-email-hint { display: none; }
}

.contact-social {
  margin-top: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
.contact-social a {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(247,244,239,0.42);
  text-decoration: none;
  padding: 6px 4px;
  transition: color 260ms var(--ease);
}
.contact-social a:hover,
.contact-social a:focus-visible {
  color: rgba(247,244,239,0.85);
}
.contact-social span {
  color: rgba(247,244,239,0.22);
  font-size: 12px;
}

/* The page's actual close — quiet on purpose, one line, the exact same
   --ink as .contact right above it and nothing else added (no border,
   no separate shade) so it reads as that same background simply
   continuing, not a new block tacked on the end. .home-footer, not
   .site-footer — this codebase already has an unrelated .site-footer
   (with its own .footer-row/.footer-links) left over from the old
   multi-page version of this site (stories.html etc.); reusing that
   name here would have silently inherited or fought its rules. */
.home-footer {
  background: var(--ink);
  padding: 26px 0;
  text-align: center;
}
.home-footer-text {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: rgba(247,244,239,0.4);
}

@media (max-width: 860px) {
  .contact-pause-line { font-size: clamp(19px, 5vw, 26px); white-space: normal; max-width: 86vw; }
  .contact { padding: 10vh 0 16vh; }
}

/* ---------- Shared lightbox ---------- */

body.story-modal-open { overflow: hidden; }

.story-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3vh 2.5vw;
  opacity: 0;
  visibility: hidden;
  transition: opacity 320ms var(--ease), visibility 0s linear 320ms;
}
.story-modal.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 320ms var(--ease);
}
.story-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(9,8,7,0.92);
}
.story-modal-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 1440px;
}
/* On laptop-height screens (a MacBook's ~900–1000px viewport height,
   not a mobile one — this only applies above the site's mobile
   breakpoint) the 1440px cap alone lets the 16:9 player fill almost
   the entire viewport height, edge to edge. Capping width by the
   available height too (converted through the same 16:9 ratio) keeps
   a comfortable margin there. The 230px subtracted accounts for the
   modal's own vertical padding, the bar above the player, and a bit of
   breathing room; it also keeps this cap from ever binding below
   ~1040px of viewport height, so a normal 1080p-or-taller external
   monitor still resolves to exactly the old 1440px cap — unchanged. */
@media (min-width: 761px) {
  .story-modal-inner {
    max-width: min(1440px, calc((100vh - 230px) * 16 / 9));
  }
}
.story-modal-bar {
  position: relative;
  /* Above .story-modal-details (z-index 2) — without this the details
     panel's full-bleed inset:0 sits on top of the bar and Info/Close
     become unclickable the moment the panel is open, with no way back. */
  z-index: 3;
  display: flex;
  justify-content: space-between;
  margin-bottom: 14px;
}
.story-modal-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: none;
  border: none;
  padding: 6px 4px;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(247,244,239,0.75);
  cursor: pointer;
  transition: color 200ms var(--ease);
}
.story-modal-btn:hover, .story-modal-btn:focus-visible { color: var(--paper); }
.story-modal-btn svg {
  width: 15px; height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
}
.story-modal-player {
  position: relative;
  width: 100%;
  aspect-ratio: 16/9;
  background: #000;
  box-shadow: 0 30px 90px rgba(0,0,0,0.55);
}
.story-modal-player iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  pointer-events: none;
}
/* Sits above the iframe and eats every click/hover itself, so YouTube's own
   embed never receives pointer events (no hover title, no native chrome) —
   we drive play/pause ourselves via postMessage instead. */
.story-modal-player-shield {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
.story-modal-details {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
  max-width: 560px;
  padding: 6vh 5vw;
  background: linear-gradient(90deg, rgba(9,8,7,0.94) 0%, rgba(9,8,7,0.75) 65%, transparent 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 300ms var(--ease);
}
.story-modal-details.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.story-modal-eyebrow {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--clay);
}
.story-modal-details h3 {
  font-family: 'DM Serif Display', var(--serif);
  font-weight: 400;
  font-size: clamp(26px, 3.2vw, 42px);
  line-height: 1.12;
  color: var(--paper);
}
.story-modal-category {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.03em;
  color: rgba(247,244,239,0.6);
}
.story-modal-desc {
  font-size: 15.5px;
  line-height: 1.6;
  color: rgba(247,244,239,0.85);
}

/* ============================================================
   Responsive
   ============================================================ */

@media (max-width: 860px) {
  .feature, .stories-viewer, .about-portrait { grid-template-columns: 1fr; }
  .about-portrait img { max-width: 260px; }
  .about-inline-copy { padding: 0 8vw; }
  .about-final { grid-template-columns: 1fr; }
  .about-final-photo { max-width: 230px; }
  .story-grid { gap: 4vw; }
  .story-category-banner { aspect-ratio: 4/5; max-height: none; }
  .story-category-banner-copy { flex-direction: column; align-items: stretch; padding: 4vh 6vw; gap: 14px; }
  .story-category-banner-text { max-width: none; }
  /* The stacked heading+paragraph runs nearly the whole height of the
     card at this width — the fixed 240px bottom band above leaves the
     top half sitting straight on the raw footage with nothing behind it.
     Full-height and fairly dark instead, since there's no "clear video"
     zone left to protect once text fills the card top to bottom. */
  .story-category-banner-scrim {
    height: 100%;
    background: linear-gradient(180deg, rgba(9,8,7,0.55) 0%, rgba(9,8,7,0.8) 100%);
  }
  .stories-banner { height: 160px; }
  .story-modal-details { max-width: 100%; background: rgba(9,8,7,0.92); padding: 6vh 6vw; }
  .container { padding: 0 7vw; }
  .stories-section .container { padding: 0 7vw; }

  /* Landscape b-roll cropped to a phone's tall aspect loses whoever the shot was
     framed on off the bottom/side. Biasing up-and-centered is a general fix, not
     a per-clip one — real curation happens once final clips are chosen. */
  .discovery-frag video { object-position: 50% 30%; }

  .discovery-beat--framed .discovery-frag { width: 88vw; }
}

/* A long flex-wrap grid on a phone means five, six rows of two — a very
   long scroll just to get through one category, exactly what Diego
   flagged. Same breakpoint the column-balancing JS already uses for
   this component (see applyStoryGridColumns, main.js — its own
   --story-cols/flex-basis math becomes moot here, overridden below by
   the same-specificity rule that simply comes later in the cascade).
   Below 700px .story-grid becomes a horizontal, one-card-at-a-time
   scroller instead: full-bleed past the container's own padding
   (negative margin equal to it, then the same amount back as internal
   padding) so neighboring cards visibly peek in from both edges at
   rest — that peek IS the "you can advance" affordance, no arrows or
   dots needed. scroll-snap centers whichever card the visitor lands
   on after a swipe. */
@media (max-width: 700px) {
  .story-grid {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scroll-padding-left: 7vw;
    scroll-padding-right: 7vw;
    margin: 0 -7vw;
    padding: 5vh 7vw 1vh;
    gap: 4vw;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .story-grid::-webkit-scrollbar { display: none; }
  .story-grid .story-card {
    flex: 0 0 76vw;
    max-width: 340px;
    scroll-snap-align: center;
  }
}

/* ============================================================
   Motion & accessibility
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .js-ready .opening-scroll-cue,
  .js-ready body.home .wordmark,
  .js-ready body.home .menu-trigger,
  .inicio-frame-inner video,
  .inicio-text,
  .discovery-frag video,
  .dtext,
  .reveal,
  .about-fade { filter: none !important; opacity: 1 !important; transform: none !important; }

  .inicio-loading { display: none !important; }

  /* Discovery becomes a plain vertical stack — one beat per scroll-length,
     no sticky pin, no crossfade choreography, everything simply present. */
  .discovery-stage { position: relative; height: auto; }
  .discovery { height: auto !important; }
  .discovery-beat {
    position: relative;
    opacity: 1;
    height: 70vh;
    margin-bottom: 2px;
  }
  .discovery-beat--black { height: auto; padding: 10vh 8vw; }
  .discovery-beat--framed .discovery-frag { position: relative; top: auto; left: auto; transform: none; width: 100%; height: 100%; margin: 0 auto; }
  .discovery-progress { display: none; }
}
