/* ================================================================
   DANIEL HALIFA — style.css
   All-black, bold grotesk throughout (Helvetica Neue — see the
   --grotesk stack below). Three full-screen pages, snap-scrolled
   vertically (see #pages/.page): an unattended cover slideshow
   (INICIO), the stories-style project deck (one project fills the
   viewport at a time — swipe/drag, tap the edges, arrow keys, or wait
   out the per-card progress bar, see #storyBar/script.js), and a
   static bio screen (SOBRE MI, see .bio-card).

   Type system, tuned to a reference of bold-headline/thin-label
   investor-deck typography: short, proper-noun-like headline moments
   (client names, project titles, closing statements, big claims) run
   very heavy (800–900), tight tracking, often uppercase — small
   eyebrow labels and captions stay light (500), wide-tracked and
   uppercase instead of bold. That weight contrast *is* the hierarchy.
   The one exception is the persistent mini-description under an open
   project (.slideshow-footer) — left at its own weight throughout,
   not pulled into this scheme.

   --ink/--ink-soft read as "light text on the dark background" here
   (not their literal names) — the whole site is one dark theme now,
   so redefining what those two tokens resolve to was enough to carry
   every existing text rule along without touching each one by hand;
   only the handful of rules that used --cream as a *background*
   (rather than as text) needed flipping to --black by hand. */

/* ── Tokens ───────────────────────────────────────────────────── */
:root {
  --black: #000;
  --cream: #efeee7;
  --ink: #efeee7;
  --ink-soft: rgba(239, 238, 231, 0.55);
  --line: rgba(239, 238, 231, 0.2);
  --grotesk: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --accent: #efeee7;
  --yellow: #FFD400;
  --gutter: clamp(20px, 3.4vw, 44px);
  --ease: cubic-bezier(.19, 1, .22, 1);
  /* The pagination dots are a fixed overlay pinned above the safe area
     (see .slide-dots) — every card's own bottom padding needs to clear
     that same zone, or its last line renders behind the dots on short
     phones. One shared value so the reserve is identical everywhere
     instead of separately guessed per slide variant. */
  --dots-reserve: calc(56px + env(safe-area-inset-bottom, 0px));
}

/* ── Reset ────────────────────────────────────────────────────── */
* , *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* background + overscroll-behavior on html itself (not just body) —
   mobile Safari's rubber-band bounce at the top/edges of a gesture
   reveals whatever's behind the document, which without this is the
   browser's default white canvas, not our black theme. overflow:hidden
   alone stops real scrolling but not that bounce. */
html {
  background: var(--black);
  overscroll-behavior: none;
}

html, body { height: 100%; overflow: hidden; }

/* touch-action: manipulation — on top of the viewport meta tag's
   user-scalable=no — is belt-and-braces against pinch/double-tap zoom
   on the parts of the page that already handle their own taps
   (deck cards, buttons); it also drops the ~300ms
   tap-delay some mobile browsers still apply without it, which is
   part of what made every tap feel unreliable there. */
body { overscroll-behavior: none; touch-action: manipulation; }

* { -webkit-tap-highlight-color: transparent; }

/* ── Focus ────────────────────────────────────────────────────── */
/* Nothing here suppresses the outline, so keyboard users already got
   the browser default — just an unstyled blue ring at odds with the
   rest of the palette. :focus-visible instead of :focus so it only
   ever shows for keyboard/switch navigation, never on a mouse or
   touch tap (which every one of these controls already answers with
   its own hover/active state). */
a:focus-visible, button:focus-visible {
  outline: 1.5px solid var(--accent);
  outline-offset: 3px;
}

/* A full-bleed deck card (see role="button" in script.js) needs its
   ring pulled inward — a positive offset would run off the viewport
   edge and never actually paint. */
.section:focus-visible {
  outline: 1.5px solid var(--accent);
  outline-offset: -6px;
}

/* Holding a finger on a card is how you pause a story, so a long press
   has to do nothing else. Without this the browser treats the same
   gesture as the start of a selection — a word of the credit line
   highlights, or iOS raises its Save Image / Copy sheet over the photo
   — and the hold reads as a glitch instead of a pause. Selection is
   switched off across the deck and its chrome rather than globally:
   the project write-ups inside a case study are still real reading
   copy and stay selectable. */
.col,
.story-bar {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Same long press over any photo or clip anywhere on the site — the
   callout sheet and the drag-out-of-the-page gesture are never wanted
   here, and both fire before any of the page's own handlers see it. */
img, video {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  user-drag: none;
}

body {
  background: var(--black);
  color: var(--ink);
  font-family: var(--grotesk);
  -webkit-font-smoothing: antialiased;
}

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

/* ── Pages — INICIO (auto cover slideshow) / stories / SOBRE MI ──
   Three full-viewport screens, stacked and snap-scrolled vertically:
   the new landing screen (an unattended cover slideshow), the
   pre-existing stories deck, and a static bio screen. #pages does the
   real scrolling (mouse wheel, trackpad, a touch drag started on
   .page--intro/.page--about) — html/body stay overflow:hidden as
   before, this is a second, independent scroll container layered on
   top, same pattern .detail already uses for its own scrolling.
   #col keeps touch-action: none for its horizontal card swipe, so a
   touch drag started there can't reach this native scroll — script.js
   detects a vertical-dominant drag on the deck itself and redirects
   it to goToPage() instead (see "Page navigation" there). */
.pages {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--black);
}

.page {
  position: relative;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  background: var(--black);
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* ── Panel — the mounted print ────────────────────────────────── */
.panel {
  /* Forces its own compositing layer — mobile Safari's dynamic
     address bar can otherwise mis-place position:fixed content
     (the about overlay drifting mid-page instead of staying pinned
     to its edge) as it shows/hides during scroll. Also what scopes
     .story-bar/.swipe-hint's own position:
     fixed to this page's box instead of the true viewport — a
     transformed ancestor becomes their containing block, so they
     scroll away with .panel rather than sitting stuck over the
     other two pages. */
  transform: translateZ(0);
}

/* ── Intro — auto-playing cover slideshow ────────────────────────
   Full-bleed crossfade through the same project covers the stories
   deck opens on, unattended (see script.js, 2s per slide, loops).
   Purely a landing beat, no chrome over it — the photography carries
   the page on its own (no scrim, no gradient: atmosphere comes from
   the image, not CSS, same principle the deck's own cards already
   follow with their covers). */
.intro-slideshow {
  position: absolute;
  inset: 0;
}

.intro-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.1s var(--ease);
}

.intro-slide.is-active { opacity: 1; }

.intro-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Emotional Tester's poster and Secret Service's cover are low-
   resolution stills (see .media--native in the deck itself) —
   stretched full-bleed like the rest here they'd read as visibly
   soft. Capped at their own intrinsic size and letterboxed instead,
   same treatment the deck gives them. */
.intro-slide--native {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--black);
}

.intro-slide--white { background: #fff; }

.intro-slide--native img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* ── About — static bio screen ───────────────────────────────────
   The same contact card the deck used to close on (see .bio-card
   below), now a plain full-screen page of its own rather than a
   swipeable card — "estática ocupando toda la pantalla". */
.page--about .bio-card { touch-action: auto; }

/* ── Deck column ──────────────────────────────────────────────── */
.col {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  /* The deck is driven entirely by script.js's own pointer handlers
     now (swipe in any direction) — without this, touch drags are also
     interpreted natively by the browser (page pan, iOS's edge-swipe
     back/forward gesture), fighting the manual drag. */
  touch-action: none;
  /* #col has no horizontal scroll of its own, which is exactly the
     condition Chrome/Safari use to decide a trackpad's horizontal
     wheel gesture means "swipe back/forward in history" instead of
     "scroll this" — without this, that decision can pre-empt the
     wheel listener in script.js entirely (or fire it with the delta
     already eaten), which is what made a genuine sideways swipe feel
     unreliable/"difficult" rather than just stepping the card. */
  overscroll-behavior-x: none;
}

/* Each card's photo is now the card — full-bleed, edge to edge,
   cropped to fill the entire frame (object-fit: cover) rather than
   sitting as a centered square with text beside it, same treatment on
   mobile and desktop ("a pantalla completa... ocupando el marco
   completo en ambos casos, no una imagen cuadrada"). The marker and
   client/role credit float on top of it instead of living in their
   own side columns — see .section::after just below for the
   readability scrim under them.

   Every card (each .section) is stacked in the exact same spot —
   position: absolute; inset: 0 below — full viewport, one on top of
   the other. script.js toggles which one is .is-active
   (on top, interactive), .is-next (peeking behind it, so the stack
   reads as a physical deck) or neither (parked, invisible) — see
   "Card deck states" further down for the actual visual states. */
.section {
  background: var(--black);
  overflow: hidden;
}

/* The React hero (see halifa-hero.tsx) only rounds its top corners —
   but its own yellow fill is exactly this page's full size, so that
   rounded corner still needs *something* other than black behind it,
   or the corner cutout shows as a small black notch against the
   yellow. */
.page.page--intro {
  background: var(--yellow);
}

.media {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: pointer;
}

/* LidArt, Secret Service and Emotional Tester's cover thumbnails are
   low-resolution stills (roughly 800-1500px on their long edge) —
   stretched full-bleed cover like every other card, that upscale read
   as visibly soft/pixelated. Capped to their own intrinsic size
   instead (never enlarged past their real pixels) and centered on the
   card's black background, only reaching full width edge-to-edge
   ("a sangre") on a screen narrow enough that 100% width doesn't
   exceed that intrinsic size — a phone, mostly. */
.media--native {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--black);
}

.media--native img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Emotional Tester's own white — matches the project's inverted
   theme inside its detail screen (see data-invert-theme), instead of
   the site-default black every other card's letterbox uses. */
#photo-03.section .media--native {
  background: #fff;
}

/* LidArt's cover is a near-exact 16:9 still — on a desktop window
   (also usually close to 16:9) it can genuinely fill the screen
   rather than sit pinned at its native pixel size with black on every
   side, so this scales it up like the site's other full-bleed cards
   instead of capping at intrinsic size (see .media--native img
   above, still what mobile uses — a phone's viewport is narrow enough
   that capped-at-native already reaches edge to edge there). */
@media (min-width: 900px) {
  #photo-01.section .media--native img {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: contain;
  }
}

/* A dark gradient along the bottom, purely so the credit block below
   stays legible over whatever the photo happens to be — dark or
   light, busy or plain. */
.section::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  background: linear-gradient(to top, rgba(0, 0, 0, .78), rgba(0, 0, 0, 0));
  z-index: 2;
  pointer-events: none;
}

.marker {
  position: absolute;
  top: clamp(16px, 3vh, 28px);
  left: var(--gutter);
  z-index: 3;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.02em;
}

/* .credit-info/.credit-client/.credit-role dropped — no more title/
   subtitle overlay on the deck cards, just the photo. */

/* ── Card deck states ─────────────────────────────────────────────
   Applied by script.js to whichever .section is current. Every card
   starts parked here — invisible, unreachable by pointer or tab —
   until it becomes .is-active (the one on top, full opacity,
   interactive) or .is-next (peeking behind it, scaled down and
   dimmed, so the stack reads as a deck rather than a single flat
   swap). visibility (not just opacity) is what keeps a parked card's
   video/iframe content from being tabbable or announced to a screen
   reader while it's off-stage.

   script.js also drives an inline transform directly on the active
   card while a drag is in progress (see .is-dragging, which drops the
   transition so the card tracks the pointer 1:1) and again briefly
   during the fly-out/fly-in animation between cards — the transition
   below is what makes the settled states (park ⇄ next ⇄ active)
   animate smoothly the rest of the time.

   .bio-card used to be the deck's closing card and shared this whole
   block — it's a static full-screen page of its own now (see
   .page--about below), so it no longer takes any of these states. */
.section {
  position: absolute;
  inset: 0;
  z-index: 1;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.92) translateY(24px);
  transition: transform .5s var(--ease), opacity .5s var(--ease);
}

.section.is-active {
  z-index: 3;
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transform: translate(0, 0) scale(1) rotate(0deg);
}

.section.is-next {
  z-index: 2;
  visibility: visible;
  opacity: .55;
}

/* A card mid fly-out, on its way from .is-active to parked — kept
   visible (unlike the default parked state above) purely so its
   opacity/transform can be seen animating away; script.js clears this
   and any inline transform once the transition finishes. */
.section.is-leaving {
  z-index: 3;
  visibility: visible;
  pointer-events: none;
}

.section.is-dragging {
  transition: none;
}

/* ── Bio card — the SOBRE MI screen ────────────────────────────────
   Look and feel taken from a press-contact-sheet reference
   (UNDERCOVER SS03): black ground, a single right-aligned block, one
   big bold name over a light tracked label and details, generous empty
   space around it. clamp()-based sizing throughout keeps the same
   composition — not a different mobile layout — just scaled, so mobile
   and desktop read as "the same page" at different sizes, matching the
   reference's own proportions rather than reflowing into a distinct
   mobile pattern. Static, full-screen, no swipe — it used to be the
   deck's own closing card (see .page--about, the removed .bio-card
   share of "Card deck states" above, and script.js). */
/* The contact card carries a portrait behind it — a different frame
   per breakpoint, because the two crops want opposite things: the
   phone gets a tall, already-dark shot the type can sit straight on
   top of, the desktop a landscape one where the subject holds the
   left and the type has to keep to the right. Set on ::before rather
   than the card itself so the scrim below can sit between the photo
   and the text without tinting the text too. */
.bio-card {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  background: var(--black);
  padding: clamp(24px, 6vw, 64px);
  isolation: isolate;
  /* Safety net, not the expected path — the clamp()-based type scale
     below is tuned to fit one screen, but #col's touch-action: none
     (needed for the card-deck drag) would otherwise make this content
     completely unreachable on the rare short/landscape phone where it
     doesn't quite fit, instead of just needing a scroll. pan-y carves
     an exception back in for vertical scrolling specifically, without
     giving up the horizontal swipe that still needs to reach here. */
  overflow-y: auto;
  touch-action: pan-y;
}

.bio-card::before,
.bio-card::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.bio-card::before {
  z-index: -2;
  background-size: cover;
  background-position: 50% 22%;
  background-repeat: no-repeat;
  background-image: url('/assets/gallery/bio/bio-portrait-mobile.jpg');
}

/* The scrim. On a phone the type sits in the lower two thirds, over
   the coat — dark already — so this only has to deepen the bottom and
   leave the face at the top of the frame alone. */
.bio-card::after {
  z-index: -1;
  background:
    linear-gradient(to top,
      rgba(0, 0, 0, .92) 0%,
      rgba(0, 0, 0, .82) 30%,
      rgba(0, 0, 0, .55) 60%,
      rgba(0, 0, 0, .18) 100%);
}

/* No max-width any more — .bio-text is meant to run the full width of
   the screen edge to edge now (bio-card's own padding is the only
   margin), the same on every device instead of a narrower column on
   desktop. text-align: left because a full-width block reads left to
   right naturally; right-aligning it was only ever there to match the
   old narrower column. */
.bio-card-inner {
  width: 100%;
  text-align: left;
  position: relative;
  z-index: 1;
}

/* Phone: extra bottom clearance so the portrait's face keeps the top
   of the frame to itself instead of being written over (align-items
   is already flex-end at the base rule now). */
@media (max-width: 899.98px) {
  .bio-card { padding-bottom: clamp(40px, 12vh, 96px); }
}

/* Desktop: the subject stands in the middle-left of a landscape frame
   with plain wood to his right, so the scrim runs sideways (heaviest
   over his side, clear by his face) instead of just upwards like the
   phone crop. */
@media (min-width: 900px) {
  .bio-card::before {
    background-position: 32% 50%;
    background-image: url('/assets/gallery/bio/bio-portrait-desktop.jpg');
  }

  .bio-card::after {
    background:
      linear-gradient(to left,
        rgba(0, 0, 0, .86) 0%,
        rgba(0, 0, 0, .74) 30%,
        rgba(0, 0, 0, .38) 50%,
        rgba(0, 0, 0, .12) 70%,
        rgba(0, 0, 0, 0) 100%),
      linear-gradient(to top, rgba(0, 0, 0, .28), rgba(0, 0, 0, .06));
  }

}

/* No name/role above it anymore (see index.html) — that margin was
   clearing space under them, so with the block now first there's
   nothing to clear. */
.bio-block {
  margin-top: 0;
}

/* .bio-label/.bio-email/.bio-links dropped — contact moved to the
   closing page (see .page--contact/.contact-links). */

/* Same weight/tracking as the "Scroll"/"Baja" corner label on the
   intro screen (font-weight 500, -0.02em tracking — not the heavier
   900 this carried before) so the two screens read as one typographic
   system. Runs the full width of .bio-card-inner (which no longer has
   a max-width — see above), left-aligned and bottom-anchored via
   .bio-card-inner/.bio-card on every device, not just desktop. */
.bio-text {
  margin: 24px 0 0;
  max-width: 100%;
  font-size: clamp(24px, 4.8vw, 64px);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--yellow);
}

/* ── Contact — closing page ────────────────────────────────────────
   Same yellow as the hero (.page--intro) and the same type spec as
   its corner labels (font-weight 500, -0.02em tracking, no uppercase —
   see DISPLAY_STYLE in halifa-hero.tsx) so the site's first and last
   screens read as one pair, bookending the dark deck/about screens in
   between. */
/* padding matches the hero's own corner-label offsets exactly
   (right-4/top-4 sm:right-6/top-6 md:right-8/top-8 in halifa-hero.tsx
   — 16/24/32px at the same 640/768px breakpoints) rather than an
   unrelated clamp() scale, so the two yellow screens share identical
   margins instead of just looking similar. */
.page--contact {
  background: var(--yellow);
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  gap: 24px;
  flex-wrap: wrap;
  padding: 16px;
  /* Same safety net as .bio-card — content near the top of the
     .contact-links type scale can outgrow a short/landscape phone
     screen, and .page's own overflow: hidden would just clip it
     instead of letting a scroll reach the rest. */
  overflow-y: auto;
  touch-action: pan-y;
}

@media (min-width: 640px) { .page--contact { padding: 24px; } }
@media (min-width: 768px) { .page--contact { padding: 32px; } }

.contact-links {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: clamp(16px, 3vw, 40px);
}

/* Same size as the "Scroll"/"Baja" corner label on the hero (see
   halifa-hero.tsx: text-[5vw] sm:text-[4vw] md:text-[3vw] lg:text-[2.2vw])
   — matched breakpoint-for-breakpoint rather than approximated with a
   single clamp(), so the two screens' type actually reads as the same
   size instead of just close. */
.contact-links a {
  font-family: var(--grotesk);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1;
  color: #0a0a0a;
  font-size: 5vw;
  transition: opacity .3s var(--ease);
}

@media (min-width: 640px) { .contact-links a { font-size: 4vw; } }
@media (min-width: 768px) { .contact-links a { font-size: 3vw; } }
@media (min-width: 1024px) { .contact-links a { font-size: 2.2vw; } }

.contact-links a:hover { opacity: .6; }

/* Home page only, by construction rather than an extra visibility
   rule: it lives inside .panel (z-index 1) and .detail sits on top at
   z-index 400 with an opaque background once a project is open, so
   this is naturally covered/unclickable there without any JS toggling
   or a duplicate media query. */
.lang-toggle {
  position: fixed;
  top: calc(clamp(16px, 3vh, 28px) + env(safe-area-inset-top, 0px));
  right: max(var(--gutter), env(safe-area-inset-right, 0px));
  z-index: 30;
  background: none;
  border: 1px solid var(--line);
  border-radius: 9999px;
  color: var(--ink-soft);
  font-family: var(--grotesk);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.06em;
  padding: 5px 10px;
  cursor: pointer;
  transition: border-color .35s var(--ease), color .35s var(--ease), transform .15s var(--ease);
}

.lang-toggle:hover { border-color: var(--accent); color: var(--accent); }
.lang-toggle:active { transform: scale(0.95); }

/* ── Story bar — one segment per deck card ─────────────────────────
   Same idiom as Instagram/Snapchat stories: a row of thin tracks
   across the top, each one filling left-to-right while its card is
   active, driven frame-by-frame from script.js (not a CSS transition
   — needs to be pausable on hold/detail-open, see script.js). Kept
   short of the full width, ending before .lang-toggle, so the two
   fixed overlays never fight for the same top-right corner. */
/* The +64px reserve this used to carry on the right was clearing
   space for the ES/EN toggle that lived on this page — it's moved to
   the hero nav now, so the bar can run the full width off var(--gutter)
   alone. */
.story-bar {
  position: fixed;
  top: calc(clamp(16px, 3vh, 28px) + env(safe-area-inset-top, 0px));
  left: max(var(--gutter), env(safe-area-inset-left, 0px));
  right: max(var(--gutter), env(safe-area-inset-right, 0px));
  z-index: 30;
  display: flex;
  gap: 5px;
  pointer-events: none;
}

.story-bar-segment {
  flex: 1 1 0;
  height: 2px;
  background: var(--line);
  overflow: hidden;
}

.story-bar-fill {
  height: 100%;
  width: 0%;
  background: var(--yellow);
}


/* ── Swipe hint — mobile only, first visit only ──────────────────────
   Since mobile has no visible tap zones (see above), this is the only
   cue that the deck moves by swiping. Centered over the card rather
   than tucked in a corner so it can't be missed on a first visit, but
   gone for good (see .nav-used below, toggled once from script.js)
   the moment any navigation actually happens — a returning or already
   -oriented visitor never sees it again this session. */
/* Sits above the midline, not on it. The deck opens on the contact
   card now, whose block is bottom-anchored on a phone — dead centre put
   this over the top of the name (measured: 14px of overlap). Higher up
   it has the empty half of that card to itself, and on a project card
   it still reads as centred on the photo. */
.swipe-hint {
  display: none;
  position: fixed;
  top: 36%;
  left: 50%;
  z-index: 5;
  transform: translate(-50%, -50%);
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 18px 24px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .4);
  pointer-events: none;
  opacity: 1;
  transition: opacity .5s var(--ease);
}

body.nav-used .swipe-hint { opacity: 0; }

@media (max-width: 760px) {
  .swipe-hint { display: flex; }
}

.swipe-hint-icon {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 26px;
  font-weight: 300;
  line-height: 1;
  color: var(--ink);
}

.swipe-hint-arrow { display: inline-block; animation: swipe-hint-nudge-right 1.6s ease-in-out infinite; }
.swipe-hint-arrow--left { animation-name: swipe-hint-nudge-left; }

@keyframes swipe-hint-nudge-left {
  0%, 100% { opacity: .35; transform: translateX(0); }
  50% { opacity: 1; transform: translateX(-6px); }
}

@keyframes swipe-hint-nudge-right {
  0%, 100% { opacity: .35; transform: translateX(0); }
  50% { opacity: 1; transform: translateX(6px); }
}

.swipe-hint-text {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

@media (max-width: 760px) {

  .lang-toggle {
    top: calc(16px + env(safe-area-inset-top, 0px));
    right: max(16px, env(safe-area-inset-right, 0px));
  }
}

/* ── Project detail screen ────────────────────────────────────────
   A secondary "screen" for a single project, in one of two shapes:

   1. Slide-based campaigns (.detail.is-slideshow) — .slideshow takes
      over the full screen: each slide is one viewport tall, snapped
      vertically, with the project description riding along as a
      fixed footer and the code/BACK header floating on top.

   2. Single-clip projects (dormant path, kept for a future project
      that's neither a carousel nor a reveal) — the original write-up
      layout in .detail-inner: media, description, a bordered
      specifications table and related projects. .detail-body stacks
      media-over-text on narrow screens and switches to a media | text
      two-column layout once there's enough width to use it (900px
      breakpoint below).

   Both fade in rather than replacing the DOM, so the underlying
   scroll position is preserved when the screen closes. */
.detail {
  position: fixed;
  inset: 0;
  z-index: 400;
  background: var(--black);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  overflow-y: auto;
  transition: opacity .45s var(--ease), visibility 0s linear .45s;
}

.detail.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity .45s var(--ease), visibility 0s linear 0s;
}

/* One project (Emotional Tester) runs white-background/black-text
   instead of the rest of the site's dark theme — redefining these
   tokens locally is enough to carry every existing rule along, same
   trick the dark-theme rewrite used site-wide (see the tokens comment
   at the top of this file). --accent flips too: on a white card, the
   near-white accent color used for hover/label text would vanish. */
.detail--invert {
  --black: #fff;
  --ink: #000;
  --ink-soft: rgba(0, 0, 0, .55);
  --line: rgba(0, 0, 0, .2);
  --accent: #000;
}

/* Shared icon slot — the actual line icons from the lodge's own site
   (compass, clock, sun, river, paw, tent, plane, car), not generic
   decoration ("elementos y logos que se usan en la web decorando").
   currentColor throughout so each context just sets its own color. */

/* Technical HUD readout — same real content as the site's own bottom
   marquee (position, local time zone, dry-season range), not
   invented, giving the cover a second, quieter layer of information
   under the wordmark ("microtextos y explicaciones técnicas"). */
/* The lodge's site runs its live data along a hairline strip at the
   foot of every screen, items divided by a slash. Same read here,
   kept inside the card rather than fixed to the viewport, because the
   bottom edge of a project screen already belongs to the pager dots. */

/* The 3 pillars, folded into the About/Philosophy slide as compact
   highlighted rows (icon + label + one line) rather than their own
   case-grid slide — "resaltando detalles" without the fuller
   pillars/archetype treatment the user asked to drop. Hover nudge is
   the "interactivo" touch — a real hand-off signal on desktop, inert
   (no hover) on touch, matching every other hover accent on the site. */

/* Route's numbered waypoints — same circle-badge + bold title +
   tracked caption grammar as the site's own vertical timeline (minus
   its connecting-line scroll animation), plus that step's own real
   icon (plane / car / compass) next to the title. */

/* The numbers are strung together by a gold hairline on the lodge's
   own route section — the steps read as one journey rather than three
   separate entries. Drawn from each number down to the next, so the
   last one ends the line. */

/* EDEL + Emotional Tester's own 3-tier type spec (client-supplied
   guide: Helvetica Neue 65 Medium throughout, differentiated only by
   size/line-height, "optical" — i.e. default/untracked — spacing).
   Scoped to these two projects via data-type-system rather than
   editing .minimal-headline/.minimal-body/etc. directly, since those
   base classes are shared with LidArt (.lidart-serif overrides them
   there, but at equal specificity, so touching the base rule directly
   risked leaking into it depending on source order). Two classes deep
   throughout, to beat each base rule on specificity regardless of
   order. */
.detail--edel-type .minimal-headline,
.detail--edel-type .minimal-headline--big {
  font-family: var(--grotesk);
  font-weight: 500;
  font-size: 28px;
  line-height: 24px;
  letter-spacing: normal;
}

/* The guide's literal 12/14 and 8/10 read as print-label sizes, not
   comfortable on-screen text — scaled up here (~1.3x) while keeping
   the guide's weight, family and untracked spacing, and keeping Note
   distinctly smaller than Body. */
.detail--edel-type .minimal-body,
.detail--edel-type .case-block p,
.detail--edel-type .minimal-quote {
  font-family: var(--grotesk);
  font-weight: 500;
  font-size: 15px;
  line-height: 22px;
  letter-spacing: normal;
}

.detail--edel-type .minimal-kicker,
.detail--edel-type .case-block-title,
.detail--edel-type .minimal-specs-row,
.detail--edel-type .detail-subhead,
.detail--edel-type .credits-group-title,
.detail--edel-type .credits-row dt,
.detail--edel-type .track-title,
.detail--edel-type .track-artist {
  font-family: var(--grotesk);
  font-weight: 500;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: normal;
}

.detail.is-slideshow { overflow: hidden; }

.detail-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: calc(clamp(20px, 4vh, 48px) + env(safe-area-inset-top, 0px)) var(--gutter) 14px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: uppercase;
}

.detail-code { color: var(--ink); }
.detail-code #detailTitle { color: var(--ink-soft); margin-left: 4px; }

.detail-back {
  flex: 0 0 auto;
  background: none;
  border: none;
  font-family: var(--grotesk);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.05em;
  color: var(--ink);
  cursor: pointer;
  border-bottom: 1px solid transparent;
  /* Padding + matching negative margin grows the tappable area well
     past the text's own tiny box without shifting it visually or
     nudging the header's layout. */
  padding: 12px 6px;
  margin: -12px -6px;
  transition: border-color .45s var(--ease), transform .15s var(--ease);
}

.detail-back:hover { border-color: var(--accent); color: var(--accent); }
.detail-back:active { transform: scale(0.95); }

/* Floats over the slides instead of sitting in normal flow above
   them — same fixed-chrome-over-scrolling-content pattern the site
   already uses for the title/about overlays on the main filmstrip. */
.detail.is-slideshow .detail-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  padding: calc(clamp(16px, 3vh, 28px) + env(safe-area-inset-top, 0px)) var(--gutter) clamp(16px, 3vh, 28px);
  margin-bottom: 0;
  border-bottom: none;
}

/* Scrim behind the floating header, on the specs card only (see
   updateFooterVisibility in script.js). Painted in the card's own
   background color, so it's invisible until credit rows start passing
   underneath it. z-index: -1 stays inside the header's own stacking
   context — behind its text, still above the card scrolling below. */
.detail--specs-active.is-slideshow .detail-header::before {
  content: '';
  position: absolute;
  inset: 0 0 -20px;
  z-index: -1;
  background: linear-gradient(to bottom, var(--black) 62%, transparent);
  pointer-events: none;
}

/* Light chrome over a dark full-bleed slide (see updateFooterVisibility
   in script.js). Only reachable on a project whose theme is otherwise
   dark-on-white, so these override the inverted tokens rather than
   using them. Scoped to the chrome that floats over the media — the
   slide's own content is untouched. */
.detail--light-chrome .detail-header,
.detail--light-chrome .detail-code,
.detail--light-chrome .detail-back,
.detail--light-chrome .slideshow-footer {
  color: #fff;
}

/* The project title is an ID selector in the base sheet, so matching
   its specificity is the only way to reach it from here. */
.detail--light-chrome .detail-code #detailTitle { color: rgba(255, 255, 255, .7); }

.detail--light-chrome .slide-dot { border-color: rgba(255, 255, 255, .6); }

.detail--light-chrome .slide-dot.is-active {
  background: #fff;
  border-color: #fff;
}

@media (max-width: 899.98px) {
  .detail--light-chrome-narrow .detail-header,
  .detail--light-chrome-narrow .detail-code,
  .detail--light-chrome-narrow .detail-back,
  .detail--light-chrome-narrow .slideshow-footer {
    color: #fff;
  }

  .detail--light-chrome-narrow .detail-code #detailTitle { color: rgba(255, 255, 255, .7); }
  .detail--light-chrome-narrow .slide-dot { border-color: rgba(255, 255, 255, .6); }

  .detail--light-chrome-narrow .slide-dot.is-active {
    background: #fff;
    border-color: #fff;
  }

  /* Unlike the specs card's scrim, this one can't be painted in the
     card's own background color — there's a moving image behind it,
     not a flat ground — so it's a soft black wash instead, enough to
     hold white type over whichever frame happens to be on screen. */
  .detail--light-chrome-narrow.is-slideshow .detail-header::before {
    content: '';
    position: absolute;
    inset: 0 0 -24px;
    z-index: -1;
    background: linear-gradient(to bottom, rgba(0, 0, 0, .45), transparent);
    pointer-events: none;
  }
}

/* ── Slideshow — slide-based campaigns ────────────────────────────
   Hidden unless .is-slideshow is active; .detail-inner (the other
   shape) is hidden in that state instead (see below). */
.slideshow { display: none; }

.detail.is-slideshow .slideshow {
  display: block;
  position: absolute;
  inset: 0;
}

/* overscroll-behavior: contain is safe here — this track always has
   real overflow (multiple stacked slides). It must NOT be added to
   the individual slide/card containers below (.slideshow-slide--minimal,
   .minimal-media, .minimal-grid--text-only): a card whose own content
   happens to fit exactly (scrollHeight === clientHeight, true for
   most of them) is already "at its scroll boundary" in both directions
   at once, so contain there blocked wheel/touch scroll from ever
   reaching this track at all — scrolling looked completely dead on
   every card that didn't need its own internal scroll. Learned this
   the hard way; don't put it back on the per-card containers. */
.slideshow-track {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.slideshow-track::-webkit-scrollbar { display: none; }

.slideshow-slide {
  height: 100vh;
  height: 100dvh;
  width: 100%;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--black);
}

.slideshow-slide img,
.slideshow-slide video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Reveal slide — poster + play button, click to swap in the video ─
   A click-to-play hero: opens on a still frame with a play control
   overlaid (or, with no poster at all, just the button on black);
   clicking replaces it with the actual <video> and starts it, rather
   than showing native controls immediately. */
.reveal-frame {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.reveal-poster,
.reveal-frame video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.reveal-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 76px;
  height: 76px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  background: rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform .35s var(--ease), background .35s var(--ease);
}

.reveal-play:hover {
  transform: translate(-50%, -50%) scale(1.08);
  background: rgba(0, 0, 0, 0.55);
}/* ── Info slide — video-plus-playlist projects ────────────────────
   A per-song tracklist (title, artist and Spotify's own compact
   embed) for a project like Emotional Intensity Testers — padded and
   scrollable rather than edge-to-edge like the media slides. */@media (min-width: 760px) {  /* Tracklist-only info slides (specs live on their own trailing
     card) don't need the second column — a single, narrower reading
     width suits a list of rows better than the full 1100px spread. */
  .info-slide-inner--single { grid-template-columns: minmax(0, 1fr); max-width: 640px; }
}/* One row per song — title/artist plus Spotify's own compact embed,
   which already has a native play button that streams the sample
   straight from Spotify (no custom audio player to build). */
.track-row {
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}

.track-row:last-of-type { border-bottom: none; }

.track-row-label { margin-bottom: 8px; }

.track-title {
  display: block;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--ink);
}

.track-artist {
  display: block;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-top: 2px;
}

.track-row iframe {
  display: block;
  width: 100%;
  max-width: none;
  height: 80px;
  border: none;
  border-radius: 8px;
}/* ── Specs card — the last card of every project, no exceptions ──── */
/* justify-content: safe center — "center it when it fits, fall back to
   top-aligned the moment it doesn't". This card centered with margin:
   auto on .specs-card-inner before, on the assumption that an auto
   margin collapses to 0 once the content outgrows the screen; it
   doesn't. It keeps centering, which pushes the first rows above the
   scroll origin where nothing can reach them again. The overflow-y is
   a safety net only — fitSpecsCard() in script.js shrinks --fit until
   the card fits the screen, because the whole ficha técnica has to be
   readable at once with no scrolling inside the card. Everything below
   that contributes height is therefore sized in terms of --fit. */
.slideshow-slide--specs {
  --fit: 1;
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  overflow-y: auto;
  padding: clamp(48px, 8dvh, 88px) var(--gutter) calc(var(--dots-reserve) + 8px);
  color: var(--ink);
}

.specs-card-inner { max-width: 640px; margin-inline: auto; width: 100%; flex: none; }

.specs-card-inner .detail-subhead {
  font-size: calc(10.5px * var(--fit, 1));
  margin-bottom: calc(30px * var(--fit, 1));
}

/* ── Ficha técnica — the crew list under the specs on the same card ──
   Reads as a real credit sheet rather than a second specs table: role
   on the left, people on the right, aligned into columns so the eye
   runs down one list instead of zig-zagging. Two pairs sit side by
   side from 900px, three from 1200px, which is what lets nineteen
   Audi credits land on one screen without shrinking to nothing.
   Below 640px there isn't width for a label and a name on one line,
   so the rows stack (label over name) two across instead. */
.credits-block {
  margin-top: calc(40px * var(--fit, 1));
  padding-top: calc(32px * var(--fit, 1));
  border-top: 1px solid var(--line);
}

.credits-group + .credits-group { margin-top: calc(26px * var(--fit, 1)); }

.credits-group-title {
  font-size: calc(11px * var(--fit, 1));
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: calc(14px * var(--fit, 1));
}

.credits-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: baseline;
  gap: calc(14px * var(--fit, 1)) 24px;
}

.credits-row {
  display: flex;
  flex-direction: column;
  gap: calc(3px * var(--fit, 1));
}

.credits-row dt {
  font-size: calc(10px * var(--fit, 1));
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.credits-row dd {
  font-size: calc(14.5px * var(--fit, 1));
  font-weight: 500;
  letter-spacing: -0.005em;
  line-height: 1.3;
  color: var(--ink);
}

/* Label and name on one line, in aligned columns — `display: contents`
   drops the row wrapper out of the box tree so its dt and dd become
   grid items of .credits-list itself, which is what lets every label
   column and every name column line up across the whole block. */
/* Wider viewports buy columns, not bigger type — going from two
   columns to four is what halves the height of a nineteen-name list,
   and it's the reason the auto-fit rarely has to shrink anything on a
   desktop at all. The cells stay label-over-name (rather than the two
   aligned into columns of their own): at these widths every role from
   "VÍDEO" to "AUDI INNOVATIVE THINKING · PROJECT MANAGER" sets on one
   line, which an aligned label column can't manage without either
   wrapping the long ones or stranding the short ones far from the
   name they belong to. */
@media (min-width: 640px) {
  .credits-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 900px) {
  .specs-card-inner { max-width: 1060px; }
  /* max-content columns, not equal quarters — the client name is much
     the longest of the four and was wrapping in a quarter-width cell
     while the year sat in one with room to spare. Outranks the generic
     two-column rule further down the file, which would otherwise win
     on source order alone. */
  .specs-card-inner .specs-table {
    grid-template-columns: repeat(4, max-content);
    justify-content: start;
    gap: calc(24px * var(--fit, 1)) clamp(40px, 6vw, 92px);
  }
  .credits-list { grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 32px; }
}

/* Phone. Seventeen EDEL credits plus the specifications on one 844px
   screen is close to the limit of what fits at a readable size, and
   fitSpecsCard() can only shrink type — it can't recover space lost to
   fixed margins. So the structural padding comes down here rather than
   letting the auto-fit spend the whole budget on it: the client name
   takes the full width and agency/year pair up beside each other, and
   every gap between blocks tightens. Without this the same card landed
   around --fit 0.65, i.e. nine-pixel names. */
@media (max-width: 899px) {
  /* Content-sized columns, not equal halves. Split down the middle,
     a client called "EDEL BY EULALIA DE LUCIA" only had ~160px to sit
     in and broke across two lines while the year beside it used a
     fraction of its own half. Sized to their content the pair fits on
     one line with room to spare, and an `auto` track still shrinks and
     wraps rather than overflowing if a longer name ever turns up. */
  .specs-card-inner .specs-table {
    grid-template-columns: auto auto;
    justify-content: start;
    gap: calc(20px * var(--fit, 1)) clamp(20px, 7vw, 40px);
  }

  /* Two columns on a phone, so the four fields want to pair up evenly.
     Left in written order they don't: a project with three specs left
     the year sitting alone on a line of its own, and a four-spec one
     put it under an empty half. Pulling the year up next to the client
     fixes both — client+year always share the first line, and whatever
     is left over is campaign or agency, long enough to carry a line by
     itself. Order only, not source order: the desktop card lays all
     four out in one row and reads correctly as written. */
  .specs-row--client { order: 1; }
  .specs-row--year { order: 2; }
  .specs-row--campaign { order: 3; }
  .specs-row--agency { order: 4; }
  .specs-row dd { font-size: calc(17px * var(--fit, 1)); }
  .specs-card-inner .detail-subhead { margin-bottom: calc(18px * var(--fit, 1)); }
  .credits-block { margin-top: calc(22px * var(--fit, 1)); padding-top: calc(20px * var(--fit, 1)); }
  .credits-group + .credits-group { margin-top: calc(18px * var(--fit, 1)); }
  .credits-group-title { margin-bottom: calc(10px * var(--fit, 1)); }
  .credits-list { gap: calc(11px * var(--fit, 1)) 20px; }
}

/* The short-phone case (iPhone SE and friends, 667px tall) — the same
   trade one notch further, since that screen has ~180px less to give
   than the 844px one the block above is tuned for. */
@media (max-width: 899px) and (max-height: 720px) {
  .slideshow-slide--specs { /* Has to clear the floating header and BACK button, whose box
     reaches ~64px here — at 48px the ESPECIFICACIONES heading was
     sliding underneath both. */
  padding-top: 72px; }
  .credits-block { margin-top: calc(12px * var(--fit, 1)); padding-top: calc(10px * var(--fit, 1)); }
  .credits-group + .credits-group { margin-top: calc(10px * var(--fit, 1)); }
  .credits-group-title { margin-bottom: calc(5px * var(--fit, 1)); }
  .credits-list { gap: calc(6px * var(--fit, 1)) 16px; }
  .specs-card-inner .specs-table { gap: calc(10px * var(--fit, 1)) 20px; }
  .specs-card-inner .detail-subhead { margin-bottom: calc(9px * var(--fit, 1)); }
}

/* ── Minimal slide — full-bleed two-column layout ─────────────────
   A narrow text column (kicker, one short headline, at most one more
   line — no long-form paragraphs) beside a media column that fills
   the rest of the screen edge to edge. Stacks on mobile (text above
   media); becomes a true side-by-side split from 900px up so desktop
   uses the full width instead of one centered column with empty
   gutters on either side. */
/* overflow-y is intentionally NOT set here (used to be auto) — even
   plain overflow-y:auto with no overscroll-behavior still makes a
   real device's touch routing evaluate this element as a scrollable
   candidate first, absorbing the initial swipe locally before handing
   off to .slideshow-track, so advancing a slide needed two swipes
   instead of one. The deeper containers that actually need their own
   scroll safety net (.minimal-grid--text-only, .minimal-text at
   desktop, .minimal-grid--producto-mobile-text) already set their own
   overflow-y: auto — this outer wrapper doesn't need to duplicate it. */
.slideshow-slide--minimal {
  display: block;
  background: var(--black);
  color: var(--ink);
}

/* height: 100% (not min-height) — a hard cap, not a floor, is what lets
   .minimal-text--scrolls below actually have something bounded to
   scroll within instead of just pushing this box taller than the
   slide and spilling silently past it uncontrolled. */
.minimal-grid {
  display: flex;
  flex-direction: column;
  height: 100%;
}

@media (min-width: 900px) {
  .minimal-grid { flex-direction: row; height: 100%; }
}

.minimal-text {
  flex: 1 1 auto;
  padding: clamp(90px, 16dvh, 140px) clamp(24px, 4vw, 56px) 32px;
}

/* Applied by script.js (markOverflowingText) only to the specific cards
   whose headline+body genuinely don't fit in the space left below the
   media on a short phone — never blanket, see the .minimal-text--scrolls
   rule's own comment there for why. min-height: 0 lets a flex child
   actually shrink to its flex-basis instead of sitting at its content's
   natural height, which is what makes overflow-y: auto have anything to
   scroll in the first place. */
@media (max-width: 899.98px) {
  .minimal-text--scrolls {
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
}

@media (min-width: 900px) {
  /* justify-content: center — vertically centering the block reads
     better than the flat top-anchored alternative this used to be
     (which existed specifically to make every slide's text start at
     the identical Y regardless of headline length, at the cost of
     everything sitting pinned high with dead space below it whenever
     a slide's own text was short). Centering means the exact start
     position now varies slide to slide with content length again —
     an intentional trade-off for the better default look, not an
     oversight. */
  .minimal-text {
    flex: 0 0 35%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 100%;
    overflow-y: auto;
    padding: 100px clamp(32px, 4vw, 64px) 60px;
  }

  /* A 35%-wide column can still run long lines on a big monitor — capping
     each block's own measure and centering it within the column (rather
     than stretching edge to edge) keeps every line a comfortable reading
     length and reads as a composed block instead of a wall of text.
     .minimal-text--wide (the full-width case-grid layout) and
     .minimal-text--closing (its own centered-overlay treatment) opt out,
     since both already have their own tuned width/centering. .lidart-text
     opts out too — its own text-align: center is otherwise beaten by
     this rule's three :not()s outscoring its single class on
     specificity, same issue as the .lidart-rings width bug above. */
  .minimal-text:not(.minimal-text--wide):not(.minimal-text--closing):not(.lidart-text) {
    align-items: center;
    text-align: left;
  }

  /* A fixed px measure (not ch) — ch is relative to each element's OWN
     font-size, so the same "42ch" resolves to a much wider box for the
     big headline than for the small body text, leaving them misaligned
     with each other once centered independently. A shared px width
     keeps every block's left edge lined up as a single composed
     column. width: 100% is what actually enforces that: max-width alone
     still lets a short block (a kicker span, or a headline forced onto
     short lines by its own <br> tags) shrink to its own content width
     and then get centered on ITS OWN axis by align-items: center above
     — different content, different shrink-to-fit width, different
     center, so two blocks in the same card can drift left/right of each
     other even though both are "centered". Forcing every child to the
     same 460px-capped width means they're all centered identically, so
     text-align: left then lines every block up flush against the same
     shared edge instead.

     .lidart-text also opts out here — its own children (.lidart-rings,
     .lidart-quote, etc.) already carry their own explicit, deliberately
     narrow widths with margin: auto centering (see further down), and
     :not() pseudo-classes count toward specificity, so without this
     exclusion this rule's three :not()s (0,3,0) actually beat those
     two-class rules (0,2,0) regardless of source order — which is
     exactly what was stretching the Audi rings logo to the full column
     width instead of its intended 56px. */
  .minimal-text:not(.minimal-text--wide):not(.minimal-text--closing):not(.lidart-text) > * {
    max-width: 460px;
    width: 100%;
  }
}

.minimal-text--closing { display: flex; flex-direction: column; justify-content: center; }

.minimal-media {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}

@media (min-width: 900px) {
  .minimal-media { flex: 1 1 65%; height: 100%; overflow-y: auto; }

  /* Closing card, desktop only: instead of the usual side-by-side
     split, the video takes the full screen and the statement floats
     centered on top of it, uppercase — a hero moment rather than
     another two-column card. Mobile keeps the standard stacked
     minimal-grid layout (text above, video below) since there's no
     room to overlay text on a full-width video without it colliding
     with the footer/dots underneath. */
  .minimal-grid--closing { display: block; position: relative; height: 100%; }

  .minimal-grid--closing .minimal-media {
    position: absolute;
    inset: 0;
    height: 100%;
  }

  /* transform: translateZ(0) — see the matching comment on
     .minimal-grid--hero .minimal-text--closing; same iOS Safari
     video-compositing fix, same reasoning. */
  .minimal-grid--closing .minimal-text--closing {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 40px;
    transform: translateZ(0);
  }

  .minimal-grid--closing .minimal-text--closing > * { max-width: 800px; }

  .minimal-grid--closing .minimal-headline--big { text-transform: uppercase; }
}

/* Full-bleed video + centered text overlay at every breakpoint — EDEL's
   opening card specifically. Unlike .minimal-grid--closing (desktop
   only, to avoid the footer/dots colliding with an overlay on other
   cards), this one owns the mini-description footer itself
   (.slideshow-slide--show-footer), so the collision risk doesn't apply
   here. */
.minimal-grid--hero { display: block; position: relative; height: 100%; }

.minimal-grid--hero .minimal-media { position: absolute; inset: 0; height: 100%; }

/* transform: translateZ(0) forces this overlay onto its own hardware
   compositing layer — iOS Safari has a known bug where a <video>
   element sometimes promotes itself to its own top-level compositing
   layer and paints there regardless of CSS z-index, so a plain
   position:absolute + z-index overlay (this rule, without the
   transform) can end up rendered BEHIND the video on an iPhone even
   though z-index:2 is correct and this renders on top in every other
   browser, including headless Chrome — which is why this specific bug
   never showed up in testing here. The transform is a no-op visually,
   it only changes which compositing layer the browser paints this
   element into. */
.minimal-grid--hero .minimal-text--closing {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 40px;
  transform: translateZ(0);
}

.minimal-grid--hero .minimal-text--closing > * { max-width: 800px; }

.minimal-grid--hero .minimal-headline--big { text-transform: uppercase; }

/* EDEL's cover wordmark image — two classes deep on purpose, so this
   beats .slideshow-slide img's width/height: 100% reset (see the
   .case-cover / .lidart-rings fix earlier for the same issue). */
.minimal-text--closing .minimal-logo-cover {
  display: block;
  width: 100%;
  max-width: 520px;
  height: auto;
}

/* Mobile only: puts the media column first so the image/video is
   visible right away, instead of sitting below a full text block that
   pushes it past the fold. Desktop's side-by-side order is untouched.
   A fixed height (rather than the usual flex-grow-with-a-240px-floor)
   so the media actually fills a full, deliberate share of the screen
   instead of shrinking to its minimum while the text block — which
   still carried the top padding sized for clearing the fixed header
   when IT used to be first — pushed a slab of empty black in between. */
@media (max-width: 899.98px) {
  /* max-height guards against a viewport whose own shape is itself
     close to 3:4 (an iPad in portrait is literally 3:4) — without this
     cap, aspect-ratio alone lets the media claim the ENTIRE screen on
     that specific shape, leaving zero room for the text block below
     it, not even its own padding. The declared ratio (3/4) is still
     identical on every slide per the aspect-ratio decision — this only
     ever kicks in as a safety ceiling on pathological viewport shapes,
     it doesn't change the ratio itself on any normal phone. */
  /* width: 100% explicit, not left to flex-stretch — Safari was
     observed (real iPhone, not reproduced in headless Chrome)
     resolving the box's width FROM the max-height-capped height via
     the aspect-ratio instead of keeping the full stretched width,
     leaving a flush-left video with an empty gap on the right. An
     explicit width removes that ambiguity: width is always 100%, and
     aspect-ratio only ever has to solve for height (which max-height
     then freely caps) rather than solving for both from a constrained
     starting point. */
  .minimal-grid--media-first .minimal-media {
    order: -1;
    flex: 0 0 auto;
    width: 100%;
    aspect-ratio: 3 / 4;
    /* The cap is a starting point, not a fixed share: fitMediaFirstCards()
       in script.js walks it down per card until the words below actually
       fit. A phone's usable height isn't knowable from here — the browser's
       own bars take an unpredictable slice of it — so the card has to
       measure itself rather than trust a number picked at design time. */
    max-height: var(--media-max, 60dvh);
  }

  .minimal-grid--media-first .minimal-media img,
  .minimal-grid--media-first .minimal-media video {
    min-height: 0;
  }

  /* Centered (not top-anchored) — the media's max-height cap above
     often leaves more room than a short caption needs, and top-
     anchoring that let the leftover space collect as a dead gap at the
     bottom of the card. Centering distributes it above and below
     instead, which reads as the text sitting deliberately in its
     space rather than stranded at the top of an empty box. */
  .minimal-grid--media-first .minimal-text {
    padding-top: 24px;
    /* Clears the pagination dots, which float over the card's bottom
       edge — 20px let the last line of a long block run under them. */
    padding-bottom: 34px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* A card whose text doesn't fit (.minimal-text--scrolls, see the
     comment on that class) has to stay top-anchored instead — centering
     an overflowing scroll container clips its own top edge the moment
     it's scrolled, since "centered" no longer means anything once the
     content is taller than the box. */
  .minimal-grid--media-first .minimal-text.minimal-text--scrolls {
    justify-content: flex-start;
    /* Margin, not padding: padding lives *inside* the scroll box and
       scrolls away with the text, so the last line still passed under
       the pagination dots. A margin ends the box above them instead,
       and nothing can ever be rendered there. */
    margin-bottom: var(--dots-reserve);
    padding-bottom: 8px;
  }

  /* A short phone (an SE and the like) gives the media 60dvh out of not
     much, which is what pushed these columns into scrolling at all —
     a smaller share there buys the words back most of the difference. */
  @media (max-height: 720px) {
    .minimal-grid--media-first .minimal-media { max-height: var(--media-max, 48dvh); }
  }
}

/* EDEL + Emotional Tester, mobile: one unified top/bottom measure for
   every text block, regardless of which card variant it's in (split
   image+text, text-only, or the centered Producto layout) — so the
   gap under the image reads the same from slide to slide, and so the
   last line of text always clears .slide-dots. The dots are a fixed-
   position overlay with no relationship to any card's own padding, so
   without an explicit reserve here a card whose content happens to
   run long enough (especially on a short-height phone) can render its
   last line directly behind them. Three classes deep — .minimal-grid
   is present on every variant — so this wins over each variant's own
   padding-bottom regardless of source order. */
@media (max-width: 899.98px) {
  .detail--edel-type .minimal-grid .minimal-text {
    padding-top: 24px;
    padding-bottom: var(--dots-reserve);
  }
}

/* EDEL's closing card on a phone. It used to pin the statement to the
   bottom-left; it now mirrors the desktop card exactly — full-bleed
   video, statement centred over it, uppercase — so the two read as one
   screen rather than two designs of the same moment. */
@media (max-width: 899.98px) {
  .minimal-grid--closing-mobile { display: block; position: relative; height: 100%; }

  .minimal-grid--closing-mobile .minimal-media { position: absolute; inset: 0; height: 100%; }

  /* transform: translateZ(0) — see the matching comment on
     .minimal-grid--hero .minimal-text--closing; same iOS Safari
     video-compositing fix, same reasoning. */
  .minimal-grid--closing-mobile .minimal-text--closing {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 24px 20px var(--dots-reserve);
    transform: translateZ(0);
  }

  .minimal-grid--closing-mobile .minimal-text--closing > * { max-width: 100%; }

  .minimal-grid--closing-mobile .minimal-headline--big { text-transform: uppercase; }

  /* No object-position shift here any more. The old landscape clip put
     its subject left of centre, so the crop had to be nudged to keep
     her in frame; the phone now gets a cut framed vertical in the first
     place, and the default centred crop is the right one for it.

     Size still comes off viewport width rather than a fixed px, so
     "NOT EVERYTHING YOU FEEL" never breaks mid-phrase on a narrow
     phone — the type system's 28px is too wide for that line under
     ~430px. */
  .detail--edel-type .minimal-grid--closing-mobile .minimal-headline--big {
    font-size: 5.6vw;
    line-height: 1.2;
  }
}

/* EDEL cover + closing statement — a very light dark wash directly on
   the video (not a shadow on the text/logo itself) gives the white
   text reliable contrast regardless of what the looping video is
   doing at any given moment, without touching the text's own
   rendering. Both .minimal-media are already position: absolute here,
   so this ::after anchors to the same box the video fills. */
.minimal-grid--hero .minimal-media::after,
.minimal-grid--closing.minimal-grid--closing-mobile .minimal-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .16);
  pointer-events: none;
}

/* LidArt/Novelas de Manual video cards, mobile only: centered and
   contained instead of the default full-bleed cover crop — the video
   keeps its own aspect ratio and sits in the middle of the card with
   the slide's black background showing around it, rather than
   stretching/cropping to fill every edge. */
@media (max-width: 899.98px) {
  .minimal-grid--video-contained-mobile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    min-height: 0;
  }

  .minimal-grid--video-contained-mobile .minimal-media {
    display: block;
    flex: none;
    width: 100%;
    height: 60dvh;
  }

  /* A fixed height (not height: auto off the video's own intrinsic
     size) — sizing that depends on metadata that hasn't loaded yet
     stalled playback entirely in testing. object-fit: contain still
     does the letterboxing/centering job within that fixed box. */
  .minimal-grid--video-contained-mobile .minimal-media video {
    display: block;
    flex: none;
    width: 100%;
    height: 100%;
    min-height: 0;
    object-fit: contain;
    margin: 0 auto;
  }
}

/* Secret Service's video, every breakpoint: same centered/contained
   idea as above, but unconditional rather than mobile-only — the
   source is 4:3, which crops heavily at full-bleed cover on a wide
   desktop screen, so this one stays contained on PC too. */
.minimal-grid--video-contained-all {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 0;
}

.minimal-grid--video-contained-all .minimal-media {
  display: block;
  flex: none;
  width: 100%;
  height: 80dvh;
}

.minimal-grid--video-contained-all .minimal-media video {
  display: block;
  flex: none;
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: contain;
  margin: 0 auto;
}

/* EDEL's second card (the "inhibition" portrait clip), desktop only:
   the source is a tall portrait video that gets heavily cropped by the
   default cover-fill media panel, so this contains it instead — the
   whole frame stays visible, letterboxed, rather than filling every
   edge. Fixed height (not height: auto off intrinsic size) for the
   same play-stall reason as .minimal-grid--video-contained-mobile.
   Tall on purpose — this card's whole point is the media taking up
   most of the screen, not the small centered treatment a shorter
   height reads as. */
@media (min-width: 900px) {
  .minimal-grid--insight-contain .minimal-media {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .minimal-grid--insight-contain .minimal-media video {
    flex: none;
    width: auto;
    height: 78dvh;
    max-width: 100%;
    min-height: 0;
    object-fit: contain;
  }
}

/* Text-only minimal slide — no media column, for case studies (like
   Emotional Intensity Testers) that lean on fewer images than EDEL's.
   Full-width centered prose instead of the two-column split. */
.minimal-grid--text-only {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* overflow-y is NOT unconditional (used to be, right on the rule
   above) — script.js's markOverflowingText() applies this class only
   to the specific cards that actually overflow. Even overflow-y:auto
   with scrollHeight === clientHeight (content that fits exactly, true
   for most of LidArt's story cards) is enough to make real touch
   routing try to claim the swipe locally before handing it to
   .slideshow-track, needing two swipes instead of one to advance —
   same root cause as the .slideshow-slide--minimal comment above, just
   found again here later. */
.minimal-grid--text-only.minimal-grid--scrolls {
  overflow-y: auto;
}

/* margin: auto (not the parent's justify-content: center) does the
   vertical centering — centers when the content fits, but collapses
   to 0 instead of going negative when it's taller than the viewport,
   so the top of the card stays reachable by scrolling instead of
   being clipped above an unreachable scroll position. height: auto is
   required for that: the desktop .minimal-text rule below sets
   height: 100%, and since this rule never contested that property
   specifically, it won by default (per-property, not per-rule,
   specificity) and stretched this box to fill the card completely —
   leaving zero leftover space for the auto margins to center within,
   so top/bottom both resolved to 0 instead of actually centering. */
.minimal-grid--text-only .minimal-text {
  flex: none;
  height: auto;
  max-width: 720px;
  margin: auto;
  padding: 100px var(--gutter);
}

/* A wider reading column for a text-only card that also holds a
   .case-grid — 720px is too narrow for three side-by-side blocks to
   breathe, so this one gets most of the viewport instead. */
.minimal-text--wide { max-width: 1200px; }

/* A case-grid nested in the narrow 35%-width text column of a split
   media/text layout (rather than the full-width .minimal-grid--text-only
   column it was designed for) doesn't have room for 3 side-by-side
   blocks — stay single-column there regardless of viewport width.
   Two classes deep to beat the rule above on specificity. */
@media (min-width: 900px) {
  .minimal-grid:not(.minimal-grid--text-only) .case-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
  }
}

/* Breakpoint-conditional slide visibility — lets one project show a
   different set/count of slides on mobile vs desktop (e.g. content
   split across more, shorter slides on a small screen but merged
   into fewer, richer ones where there's room). visibleSlides() in
   script.js already filters on display !== 'none', so dots/index math
   stays correct either way. */
@media (max-width: 899.98px) {
  .slideshow-slide--desktop-only { display: none; }
}

@media (min-width: 900px) {
  .slideshow-slide--mobile-only { display: none; }
}/* A small decorative touch above the kicker — the project's own cover
   art, not another full media column. */
/* Extra specificity (two classes) beats .slideshow-slide img's
   width/height: 100% reset — needed for any small inline image
   nested inside a slide, not just full-bleed media. */
.minimal-text .case-cover {
  display: block;
  width: min(420px, 60dvh, 100%);
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  margin: 0 auto 18px;
}

/* Tracklist placed below the text (not beside it) on a text-only
   card — two columns of smaller rows so six songs stay compact
   instead of one long list. */
.track-grid { margin-top: clamp(28px, 4vh, 48px); }

/* Tracklist standing in for the usual image/video in a split
   media/text layout (Emotional Tester's desktop-only "La idea" card)
   — centered with its own padding instead of the media rules built
   for photos/video. */
.minimal-media--tracklist {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(24px, 5dvh, 64px) clamp(24px, 4vw, 56px);
}

.minimal-media--tracklist .track-grid { margin-top: 0; }

.track-grid-rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4px 24px;
  margin-top: 16px;
}

@media (min-width: 700px) {
  .track-grid-rows { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .track-grid-rows .track-row--compact:nth-last-child(-n+2) { border-bottom: none; }
}

.track-row--compact { padding: 6px 0; }
.track-row--compact .track-row-label { margin-bottom: 4px; }
.track-row--compact .track-title { font-size: 11px; }
.track-row--compact .track-artist { font-size: 9px; }

/* ── Secret Service — centered logo card (cover + closing bookend) ──
   A single mark, centered, on black — used twice: opening the project
   and closing it again ("cierre con el logo de nuevo"). */
.brand-cover-slide {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  background: #000;
}

.brand-cover-slide .brand-cover-slide-img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(480px, 70vw);
  max-height: 60dvh;
  object-fit: contain;
}

/* Emotional Tester's own closing card — same component as EDEL's, white
   on black, just inverted: white background, black logo. filter: invert
   on the same white-on-transparent PNG (no separate black asset to
   keep in sync) turns its white pixels black and leaves the transparent
   background untouched. */
.brand-cover-slide--light {
  background: #fff;
}

.brand-cover-slide--light .brand-cover-slide-img {
  filter: invert(1);
}

/* EDEL's closing logo, mobile only — smaller than the Secret Service
   bookend this component is shared with: on an otherwise empty black
   card, even the "correct" 70vw reads as dominating the screen with
   nothing else for scale. */
@media (max-width: 899.98px) {
  .detail--edel-type .brand-cover-slide-img {
    max-width: min(200px, 45vw);
    max-height: 25dvh;
  }
}

/* ── Secret Service — horizontal image carousel ────────────────────
   A card in the site's normal vertical scroll-snap flow that itself
   contains a second, independent horizontal scroller — swipe/drag
   sideways to move between this card's own images, unrelated to the
   vertical navigation between cards. Two classes deep throughout
   (.hcarousel .hcarousel-item img) on purpose: .slideshow-slide img's
   width/height: 100% reset otherwise wins on specificity — the same
   issue already hit with .case-cover and .lidart-rings. */
.hcarousel {
  position: relative;
  height: 100%;
  background: #000;
}

.hcarousel-track {
  display: flex;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.hcarousel-track::-webkit-scrollbar { display: none; }

.hcarousel-item {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 0 var(--gutter);
}

.hcarousel .hcarousel-item img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* height: 100% (not height: auto off intrinsic size, like the image
   rule above) — a video whose layout depends on metadata that hasn't
   loaded yet stalled playback entirely elsewhere in this file (see
   .minimal-grid--video-contained-mobile); this side-steps that by
   giving it a stable height up front and letting object-fit handle
   the fit within it. */
.hcarousel .hcarousel-item video {
  display: block;
  width: auto;
  height: 100%;
  max-width: 100%;
  object-fit: contain;
}

.hcarousel-counter {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(28px, 6dvh, 48px);
  z-index: 3;
  text-align: center;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  pointer-events: none;
}

/* An h-carousel used as the MEDIA PANEL of a split text+media card
   (Pilares' 3 product shots, standing in for a single image) should
   read exactly like every other slide's media — full-bleed, edge to
   edge. The padded/contained treatment above is for an h-carousel used
   as its own full-screen slide, where that gutter framing is the
   default; it isn't wanted here. Three classes deep to beat those
   rules' specificity regardless of source order. */
.minimal-media .hcarousel-item {
  padding: 0;
}

.minimal-media .hcarousel .hcarousel-item img,
.minimal-media .hcarousel .hcarousel-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* EDEL's Inhibition carousel (3 vertical videos) — full-bleed at every
   breakpoint like the media-panel case above, but ALSO switches from a
   swipeable one-at-a-time carousel to a static 3-up row once there's
   enough width to show all three side by side without cropping them
   into slivers. Mobile keeps the swipe-to-hear-each-one behavior
   (script.js still only unmutes the active item there); desktop shows
   all three at once, muted (see the six-video grid's own comment below
   for why simultaneous clips stay muted — same reasoning applies here). */
.hcarousel--triptych .hcarousel-item {
  padding: 0;
}

.hcarousel--triptych .hcarousel-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 900px) {
  .hcarousel--triptych .hcarousel-track {
    overflow-x: visible;
    scroll-snap-type: none;
  }

  .hcarousel--triptych .hcarousel-item {
    flex: 1 1 0;
    scroll-snap-align: none;
  }

  .hcarousel--triptych .hcarousel-counter {
    display: none;
  }
}

/* ── EDEL's six-video grid — all six emotions playing at once, each
   cropped to a square, centered within its own crop. Reads as a
   single wide horizontal strip on desktop, a taller 2-column block on
   mobile ("horizontalmente a pc y verticalmente a movil"). Silent —
   six overlapping audio tracks has no coherent answer, so this one is
   muted throughout rather than picking one track to unmute. */
.video-grid-wrap {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  padding: var(--gutter);
}

.video-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 4px;
  width: 100%;
  max-width: 480px;
}

@media (min-width: 900px) {
  .video-grid {
    grid-template-columns: repeat(6, 1fr);
    max-width: 1760px;
    gap: 10px;
  }
}

.video-grid-item {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: #111;
}

.video-grid .video-grid-item video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── LidArt story cards — three beats lifted from the original press
   site (lidartbyaudi.es), copy and assets pulled from there rather
   than invented. Its own serif display face and centered layout, on
   purpose distinct from the rest of the site's Helvetica/left-aligned
   system — these three cards are quoting that site's own look. */
.lidart-slide { background: var(--black); }

.lidart-text { text-align: center; }
.lidart-text .minimal-headline { margin-left: auto; margin-right: auto; }

.lidart-serif {
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-size: clamp(28px, 4vw, 44px);
  font-weight: 700;
  line-height: 1.25;
}

.lidart-body {
  font-size: 14px;
  line-height: 1.75;
  color: var(--ink-soft);
  max-width: 620px;
  margin: 24px auto 0;
}

.lidart-body strong { color: var(--ink); font-weight: 700; }

/* Extra specificity (two classes) beats .slideshow-slide img's
   width/height: 100% reset. */
.lidart-text .lidart-rings {
  display: block;
  width: 56px;
  height: auto;
  margin: 36px auto 0;
}

.lidart-quote {
  max-width: 560px;
  margin: 28px auto 0;
}

.lidart-quote p {
  font-size: 14px;
  line-height: 1.75;
  color: var(--ink-soft);
  margin: 0;
}

.lidart-quote p strong { color: var(--ink); font-weight: 700; }

.lidart-quote cite {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-soft);
  margin-top: 12px;
}

.lidart-text .lidart-terrain {
  display: block;
  width: 100%;
  height: auto;
  max-width: 300px;
  margin: 32px auto 0;
}

/* LidArt card 3 (topographic map): headline + two quotes + an image is
   more than a short phone can show at fixed, comfortable-desktop sizes
   without either clipping content or needing to scroll internally to
   reach the rest — the .minimal-grid--scrolls safety net elsewhere on
   the site exists precisely to handle that case, but here the explicit
   rule is different: no internal scroll ever, not even a hidden one.
   Every text size below is a dvh-based clamp() instead of a fixed px
   value, so headline/quotes/gaps all shrink together as the viewport
   gets shorter, guaranteeing the stack fits instead of overflowing it.
   overflow: hidden (not auto) backs that guarantee — if the clamps
   were ever wrong, this crops rather than silently reintroducing
   scroll. The terrain image is the one element allowed to lose
   information instead of shrinking to illegibility: object-fit: cover
   inside a flexed, height-bound box crops it once text has taken the
   space it needs, rather than pushing anything off-screen. Two classes
   deep throughout to outrank .minimal-grid--text-only's shared rules
   regardless of source order. overflow: hidden only, no
   overscroll-behavior — see the comment on .slideshow-track further
   up: contain on a card whose content doesn't scroll blocks wheel/
   touch from ever reaching the track above it, which is what made
   this specific card impossible to swipe past. */
.lidart-card--terrain {
  overflow: hidden;
}

.lidart-card--terrain.minimal-grid--scrolls {
  overflow: hidden;
}

.lidart-card--terrain .minimal-text {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  justify-content: center;
  gap: clamp(6px, 1.6dvh, 20px);
  padding: clamp(56px, 11dvh, 120px) clamp(20px, 4vw, 56px) clamp(44px, 8dvh, 90px);
}

.lidart-card--terrain .minimal-headline {
  flex: 0 0 auto;
  font-size: clamp(18px, 4.4dvh, 44px);
  line-height: 1.15;
  margin: 0;
}

.lidart-card--terrain .lidart-quote {
  flex: 0 1 auto;
  min-height: 0;
  max-width: 560px;
  margin: 0 auto;
}

.lidart-card--terrain .lidart-quote p {
  font-size: clamp(10.5px, 1.7dvh, 14px);
  line-height: 1.5;
}

.lidart-card--terrain .lidart-quote cite {
  font-size: clamp(9px, 1.3dvh, 11.5px);
  margin-top: clamp(4px, 1dvh, 12px);
}

.lidart-card--terrain .lidart-terrain {
  flex: 1 1 0;
  min-height: 0;
  max-height: clamp(70px, 20dvh, 300px);
  width: auto;
  max-width: min(300px, 60vw);
  height: 100%;
  object-fit: cover;
  margin: 0 auto;
}

/* The reference's exact background — a Display-P3 orange, with an
   sRGB hex fallback for browsers/displays without that gamut. */
.lidart-slide--orange {
  background: #fd692c;
  background: color(display-p3 0.9216 0.4471 0.2471);
}

.lidart-slide--orange .minimal-headline,
.lidart-slide--orange .lidart-body,
.lidart-slide--orange .lidart-body strong {
  color: #1a1108;
}

/* A halftone dot render of Las Meninas (from the same press site) as
   a background image, low-alpha black so it reads as sparse dots
   against this slide's black background, per the reference. */
.lidart-slide--classic {
  background: #000 url('/assets/gallery/lidart/lidart-classic-dots.png') no-repeat center center;
  background-size: cover;
}

/* object-fit: cover on purpose for both — a video letterboxed with
   object-fit: contain leaves visible black bars wherever its aspect
   ratio doesn't match the column; cover fills the space completely
   (cropping instead) so no frame is visible either way. */
.minimal-media img,
.minimal-media video {
  width: 100%;
  flex: 1 1 0;
  min-height: 240px;
  object-fit: cover;
  background: var(--black);
}

/* EDEL's landing card wraps its cover image in <picture> for a
   mobile/desktop source swap — display: contents drops the <picture>
   itself out of the box tree so the <img> becomes the direct flex
   child of .minimal-media instead, picking up the flex/width/cover
   rule above the same way a bare <img> or <video> would. Without this
   the <picture> (not the <img>) is the actual flex item, defaults to
   flex: 0 0 auto, and just sizes to the image's intrinsic dimensions
   instead of filling the frame. */
.minimal-media picture { display: contents; }

/* EDEL's Producto card, mobile only: no room for a photo alongside
   the full ingredient/specs list on a narrow screen, so the image is
   dropped entirely and the text centers in the space it leaves —
   same proven centering pattern as .minimal-grid--text-only (margin:
   auto, collapsing gracefully instead of clipping if content is ever
   taller than the screen). Desktop keeps the normal split layout.
   overflow-y is conditional here too (.minimal-grid--scrolls, see
   markOverflowingText in script.js and the matching comment on
   .minimal-grid--text-only) — same false-positive touch-capture risk
   otherwise. */
@media (max-width: 899.98px) {  .minimal-grid--producto-mobile-text.minimal-grid--scrolls {
    overflow-y: auto;
  }

  .minimal-grid--producto-mobile-text .minimal-media {
    display: none;
  }

  .minimal-grid--producto-mobile-text .minimal-text {
    flex: none;
    margin: auto;
    width: 100%;
  }
}

/* Opt-out for one specific image (EDEL's product shot) that must
   never crop, on any device — two classes deep to beat the cover rule
   above on specificity. Padding on the panel itself (not just
   object-fit) so the contained image floats centered with visible
   breathing room on every side, instead of its letterboxing blending
   invisibly into the panel's own black background. */
/* The top padding has to clear the floating header and BACK button,
   not just look like breathing room: this is the one media that
   letterboxes, so its image has a visible top edge that was sliding
   under them (measured: image top at 42px, BACK button reaching 63px).
   The bottom is deliberately much tighter than the top — the leftover
   space was collecting as a dead gap between the picture and the text
   below it rather than reading as margin. */
.minimal-media.minimal-media--contain {
  padding: clamp(76px, 10dvh, 96px) clamp(20px, 4vw, 56px) clamp(8px, 1.5dvh, 16px);
}

.minimal-media.minimal-media--contain img {
  object-fit: contain;
}

/* Desktop only: the padding above still let the (already-uncropped)
   image fill ~90% of the column's height, dense enough that the
   letterboxing read as accidental cropping rather than deliberate
   breathing room. More padding here, specifically at desktop where
   there's width to spare, makes a visibly smaller, obviously-complete
   product shot instead. */
@media (min-width: 900px) {
  .minimal-media.minimal-media--contain {
    padding: 12dvh 8vw;
  }
}

/* And the text rises to meet it. The gap under the picture wasn't
   padding — it was this column centring its content inside a box
   taller than the words needed, which parked them ~50px lower than the
   picture's edge. Anchored to the top instead, on this card only: the
   sibling selector can't be used for it because the text comes first
   in the markup and only moves below the media visually (order: -1),
   so the match has to be made on the grid that contains one. */
@media (max-width: 899.98px) {
  .minimal-grid--media-first:has(.minimal-media--contain) .minimal-text {
    justify-content: flex-start;
    padding-top: 16px;
  }
}

.minimal-kicker {
  display: block;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 20px;
}

.minimal-headline {
  font-size: clamp(20px, 2.6vw, 30px);
  font-weight: 800;
  line-height: 1.35;
  letter-spacing: -0.01em;
  margin: 0;
  overflow-wrap: break-word;
}

.minimal-headline--big {
  font-size: clamp(28px, 4.4vw, 46px);
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: -0.02em;
  overflow-wrap: break-word;
}

.minimal-body {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.65;
  color: var(--ink-soft);
  margin: 16px 0 0;
}/* No lines — the six fragrances read as a composed grid of small
   name/emotion blocks (same "bold value, light tracked caption"
   language as the specs card) instead of a bordered price-list table.
   Two columns once there's room, one on a narrow phone. */
.minimal-specs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px 24px;
  margin-top: 20px;
}

/* Stays two-up on a phone too. It used to drop to a single column
   there, which was affordable while this card hid its photo on mobile
   and gave the list the whole screen — now that the card carries its
   image like every other one, six stacked rows ran off the bottom. */

.minimal-specs-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* .minimal-specs ancestor added on purpose — matches the type
   system's own .detail--edel-type .minimal-specs-row specificity
   (two classes), so this wins regardless of which rule happens to be
   declared later in the file. */
.minimal-specs .minimal-specs-row span:first-child {
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
}

.minimal-specs .minimal-specs-row span:last-child {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.color-swatch-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px 18px;
  margin-top: 24px;
}

/* The description rides along underneath every slide — pointer-events
   is off so it never blocks a video slide's own native controls
   sitting at the same bottom edge underneath it. Deliberately left
   out of the site-wide weight system above — this one stays as its
   own quieter caption regardless of what the rest of a project does. */
.slideshow-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  max-width: 720px;
  margin: 0;
  padding: 16px var(--gutter) calc(44px + env(safe-area-inset-bottom, 0px));
  color: var(--ink);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.6;
  pointer-events: none;
}

/* ── Slide dots — which card you're on, per project ───────────────
   One dot per slide (including the trailing specs card), centered at
   the very bottom, above the footer. The active one fills solid and
   grows slightly; both are transitions, not an instant swap, so it
   reads as "filling in" while you scroll/swipe between cards. Empty
   (no dots rendered) for a single-slide project, where a pager would
   be meaningless. */
.slide-dots {
  position: fixed;
  left: 50%;
  bottom: calc(clamp(10px, 2.2dvh, 16px) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 11;
  display: flex;
  align-items: center;
  gap: 9px;
}

.slide-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid rgba(239, 238, 231, 0.5);
  background: transparent;
  padding: 0;
  cursor: pointer;
  transition: background-color .35s var(--ease), transform .35s var(--ease), border-color .35s var(--ease);
}

.slide-dot.is-active {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.4);
}

/* ── Standard write-up — single-clip projects (dormant) ─────────── */
.detail.is-slideshow .detail-inner { display: none; }.detail.open .detail-inner { transform: translateY(0); }.detail-subhead {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 10px;
}

/* No border, no divider lines — each spec reads as its own small
   label-over-value block (same "light tracked caption above a bold
   value" pattern as the bio card's CONTACT block), separated by
   whitespace via the grid gap instead of ruled lines. Two columns once
   there's room for them, so six specs read as a composed block instead
   of a long single-file list. */
.specs-table {
  display: grid;
  grid-template-columns: 1fr;
  gap: calc(26px * var(--fit, 1)) 48px;
}

@media (min-width: 640px) {
  .specs-table { grid-template-columns: repeat(2, 1fr); }
}

.specs-row {
  display: flex;
  flex-direction: column;
  gap: calc(6px * var(--fit, 1));
}

.specs-row dt {
  font-size: calc(11px * var(--fit, 1));
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.specs-row dd {
  font-size: calc(19px * var(--fit, 1));
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  color: var(--ink);
}/* Roomier desktop layout: media and text side by side, using the
   width a large screen actually has instead of one narrow centered
   column with big empty gutters. */

@media (max-width: 760px) {
  .detail-header { padding: calc(64px + env(safe-area-inset-top, 0px)) 20px 14px; }  /* A long title (code + campaign name) sharing one row with the
     BACK button can otherwise wrap under it and collide — forcing
     the code/title onto its own full-width row keeps the two apart
     on every title length instead of only the short ones. Applies
     in both detail-screen shapes, including the floating slideshow
     header. */
  .detail-header { flex-wrap: wrap; row-gap: 8px; }
  .detail-code { flex: 1 1 100%; }
  .detail-back { margin-left: auto; }

  .detail.is-slideshow .detail-header { padding: calc(16px + env(safe-area-inset-top, 0px)) 20px 16px; }  .slideshow-footer {
    max-width: 100%;
    padding: 14px 20px 40px;
    font-size: 11.5px;
  }
}

/* ── Desktop type scale — every project ──────────────────────────
   Every case-study text tier reads small on an actual desktop monitor
   relative to how it's really viewed (arm's length, a much wider
   viewport than the mobile sizing was tuned for) — this bumps every
   tier up, in both the EDEL/Emotional Tester fixed-px system and the
   base minimal-headline/minimal-body/lidart-serif clamp() system
   LidArt, Manual de Autor and Secret Service use, without touching
   mobile sizing at all (already carefully tuned this session to avoid
   overflow/wrapping there). Same specificity as each rule it
   overrides, just declared later, so it wins on source order
   regardless of media query. */
@media (min-width: 900px) {
  .detail--edel-type .minimal-headline,
  .detail--edel-type .minimal-headline--big {
    font-size: 36px;
    line-height: 34px;
  }

  .detail--edel-type .minimal-body,
  .detail--edel-type .case-block p,
  .detail--edel-type .minimal-quote {
    font-size: 17px;
    line-height: 26px;
  }

  .detail--edel-type .minimal-kicker,
  .detail--edel-type .case-block-title,
  .detail--edel-type .minimal-specs-row,
  .detail--edel-type .detail-subhead,
  .detail--edel-type .credits-group-title,
  .detail--edel-type .credits-row dt,
  .detail--edel-type .track-title,
  .detail--edel-type .track-artist {
    font-size: 12.5px;
    line-height: 18px;
  }

  .minimal-headline { font-size: clamp(24px, 2.8vw, 36px); }
  .minimal-headline--big { font-size: clamp(32px, 4.6vw, 54px); }

  .minimal-body, .lidart-body { font-size: 15.5px; line-height: 1.7; }  .lidart-serif { font-size: clamp(32px, 4.2vw, 52px); }

  /* The split-card text COLUMN itself (35% of the viewport) was the
     actual binding constraint here, not its children's own 460px max-
     width cap below it — at a typical desktop width the 35% column is
     already narrower than 460px, so that cap was never being reached
     even before this file's other changes. Widened the column so the
     bigger headline above has room to wrap at a comfortable line
     length instead of stranding a single word alone on its own line
     (children's max-width bumped to match, so it doesn't become the
     new bottleneck once the column itself is wider). */
  .minimal-text {
    flex: 0 0 42%;
  }

  .minimal-text:not(.minimal-text--wide):not(.minimal-text--closing):not(.lidart-text) > * {
    max-width: 560px;
  }
}

/* ── Reduced motion ───────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .section, .bio-card { transition: none; }
  .media img, .bio-email, .detail, .detail-media img { transition: none; }
}
