/* ================================================================
   DANIEL HALIFA — style.css (React-landing build)
   All-black, bold grotesk throughout (Helvetica Neue — see the
   --grotesk stack below).

   Trimmed fork of the plain site's style.css, specific to the React
   build: #pages is locked to a single screen (see index.css's #pages
   override), so the CSS for the stories deck (swipe/drag states,
   story bar), the INICIO cover slideshow, and the about/contact pages
   has been removed rather than shipped for something never seen.
   #detailScreen and everything it renders (the actual case-study
   slides, template by template, below) is unchanged — that's still
   real, visible UI, opened from the React landing via openDetail().
   The plain (non-React) site keeps the untrimmed original, since it
   still uses all of the above for real.

   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 keeps taps responsive on controls while the
   viewport remains zoomable for accessibility; it also drops the ~300ms
   tap-delay some mobile browsers still apply without it. */
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;
}

/* 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;
}

/* ── 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; }

/* An X now, not a "‹ BACK" label — the same control the About panel
   closes with, so the one gesture for "I am done with this screen" looks
   the same everywhere on the site. Sized as a 44px target in its own
   right rather than a text box padded out to one. */
.detail-back {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  /* Pulled back by half the difference between the icon and the old
     label's box, so the header's baseline and right edge sit exactly
     where they did. */
  margin: -12px -12px -12px 0;
  background: none;
  border: none;
  border-radius: 50%;
  color: var(--ink);
  cursor: pointer;
  transition: opacity .2s var(--ease), transform .15s var(--ease);
}

.detail-back svg {
  width: 20px;
  height: 20px;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  fill: none;
}

/* Reachable by a screen reader, invisible on screen, and still a real
   element so applyLanguage() has something to translate. */
.detail-back-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* One card, one control: see the note on .slideshow-slide--light-close
   in index.html. Placed after the base rule so it wins on source order
   at the same weight, and it is deliberately NOT joined to the
   .detail--light-chrome group above — that one recolours the whole
   header, this one only the X. */
.detail--light-close .detail-back { color: #fff; }

.detail-back:hover { opacity: 0.6; }
.detail-back:active { transform: scale(0.92); }

.detail-back:focus-visible {
  outline: 1px solid currentColor;
  outline-offset: 3px;
}

/* 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;
}

/* …and its inverse, per card. A slide whose own artwork is light under
   the whole header asks for this; see the note in script.js. */
.detail--dark-chrome .detail-header,
.detail--dark-chrome .detail-code,
.detail--dark-chrome .detail-back,
.detail--dark-chrome .slideshow-footer {
  color: #000;
}

.detail--dark-chrome .detail-code #detailTitle { color: rgba(0, 0, 0, 0.55); }

.detail--dark-chrome .slide-dot { border-color: rgba(0, 0, 0, 0.45); }

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

/* The same, wide screens only — for a card whose artwork bleeds to the
   corners on a desktop but is contained on a phone, where the header ends
   up on the card's own black and wants the site's white chrome back. */
@media (min-width: 900px) {
  .detail--dark-chrome-wide .detail-header,
  .detail--dark-chrome-wide .detail-code,
  .detail--dark-chrome-wide .detail-back,
  .detail--dark-chrome-wide .slideshow-footer {
    color: #000;
  }

  .detail--dark-chrome-wide .detail-code #detailTitle { color: rgba(0, 0, 0, 0.55); }

  .detail--dark-chrome-wide .slide-dot { border-color: rgba(0, 0, 0, 0.45); }

  .detail--dark-chrome-wide .slide-dot.is-active {
    background: #000;
    border-color: #000;
  }
}

@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;
  }
}

/* ── Secret Service — the six artboards, one per card ──────────────
   Each is a finished 1920x1080 composition with type set right out to
   its own edges, so it is CONTAINED, never cover-cropped: a crop here
   eats the ends of the words. The padding is what makes that work twice
   over — it holds the artwork clear of the floating header and the row
   of dots, so those keep sitting on the card's own black and the site's
   normal white chrome stays readable over all six, whether the artboard
   behind is black (the character sheets) or white (the lockups).

   Declared ABOVE the breakpoint-conditional block that follows on
   purpose: .slideshow-slide--desktop-only's `display: none` is a single
   class, the same weight as .ss-screen, so it only wins while it is
   declared later. Move this block down and the phone would show the
   wide cards as well. */
.ss-screen {
  height: 100%;
  background: #000;
  padding: clamp(64px, 9dvh, 96px) clamp(16px, 3vw, 40px) clamp(56px, 8dvh, 84px);
}

.ss-screen picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* Two classes, to beat .slideshow-slide img's width/height: 100% with
   object-fit: cover. */
.ss-screen .ss-screen-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}


/* The opening card takes the whole screen instead: an all-over sticker
   pattern has no composition a crop can break and no edge worth holding
   clear, so the inset the other cards need only made it smaller.

   Written at two classes on purpose. .ss-screen.slideshow-slide--show-footer
   further down is also two, and while this rule was one it lost to it —
   which is why the card that carried the description sat on 135px of
   black along its bottom edge instead of filling the screen. */
@media (min-width: 900px) {
  .ss-screen.ss-screen--bleed { padding: 0; }
  .ss-screen--bleed .ss-screen-img { object-fit: cover; }
}

/* Desktop only. The first three cards of this project run edge to edge
   on a wide screen at Daniel's request; on a phone they keep the inset,
   because a 16:9 composition cover-cropped to a 9:19.5 screen loses most
   of its width. */
@media (min-width: 900px) {
  .ss-screen.ss-screen--bleed-wide { padding: 0; }
  .ss-screen--bleed-wide .ss-screen-img { object-fit: cover; }

  /* Fitted, never cropped: the whole artboard is on screen and the 15px
     is spacing held clear above and below it. contain, not cover — this
     is a lockup, and cover was taking 3.5% off each side of it to make
     the margin land on exactly 15px. The margin is a floor now, not a
     measurement: the sheet is 16:9 and the card is wider than it is tall,
     so the width binds first and what actually shows above and below is
     whatever the ratio leaves. Invisible either way, since the card and
     the artboard are the same white. */
  .ss-screen.ss-screen--inset-wide { padding: 15px 0; }
  .ss-screen--inset-wide .ss-screen-img { object-fit: contain; }
}

/* The two lockups are drawn on white. Contained on the site's black they
   read as a white panel pasted onto the screen; on white the letterbox
   the containing leaves is simply invisible and the artwork is the card.
   The padding stays — it is what keeps the type clear of the header and
   the dots now that there is no edge to mark where the artwork ends. */
.ss-screen--light { background: #fff; }

@media (max-width: 899.98px) {
  /* One rule for all of them on a phone, at Daniel's request: the artwork
     is as wide as the screen and as tall as its own proportions make it,
     centred, with the card's ground filling the rest. Nothing is cropped
     and nothing is measured against the screen's height — every one of
     these crops is portrait but none as tall as a phone, so sizing by
     height would have shrunk them all to fit a dimension they never had
     to match.

     What is left over above and below is where the header and the dots
     land, which is also why this is the version that reads: the chrome
     sits on the card's own ground rather than on the picture. */
  .ss-screen {
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* A card whose crop is cut to the phone's own proportions fills the
       screen exactly on the phone it was cut for, and runs taller than
       the card on a shorter one (a 375x667 shows 813px of it). Centred
       and clipped here rather than allowed to spill into the card above
       and below it. */
    overflow: hidden;
  }

  .ss-screen picture {
    width: 100%;
    height: auto;
  }

  .ss-screen .ss-screen-img {
    width: 100%;
    height: auto;
    object-fit: fill;
  }

}

/* 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: 15px;
  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;
}

/* Same size and leading as .lidart-body above, not a step down. The
   three LidArt cards are one voice: a quote reading a point smaller
   than the paragraph on the card before it made the set look like two
   different type scales rather than one. */
.lidart-quote p {
  font-size: 15px;
  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);
}

/* min(4.2vw, 5.6dvh) — the same 4.2vw ramp the other two cards'
   .lidart-serif rule uses, so this headline lands on their size at any
   normal window, with the dvh term only pulling it back on a genuinely
   short viewport (where this card's no-scroll guarantee needs it). The
   floor is 26px rather than the old 18px: at 18px this card's headline
   was visibly a different, smaller face than the other two. */
.lidart-card--terrain .minimal-headline {
  flex: 0 0 auto;
  font-size: clamp(26px, min(4.2vw, 5.6dvh), 52px);
  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(13.5px, 2.1dvh, 15.5px);
  line-height: 1.7;
}

/* Flat 11.5px, the same attribution size the other cards use — its old
   clamp bottomed out at 9px, small enough to read as a footnote rather
   than as the byline of the quote above it. */
.lidart-card--terrain .lidart-quote cite {
  font-size: 11.5px;
  margin-top: 12px;
}

/* The terrain render is still the element that yields space, only more
   of it now that headline, quotes and cites all sit at the site's own
   sizes rather than shrinking away on a short phone. */
.lidart-card--terrain .lidart-terrain {
  flex: 1 1 0;
  min-height: 0;
  max-height: clamp(52px, 15dvh, 260px);
  width: auto;
  max-width: min(300px, 60vw);
  height: 100%;
  object-fit: cover;
  margin: 0 auto;
}

/* "When technology meets art" — a muted, cropped ambient loop behind
   the text (see .minimal-grid--hero, already used this way for EDEL's
   opening card). The clip plays untinted: this started as a flat orange
   fill, then briefly kept that orange as a 0.4-alpha wash over the
   video, and both are gone at Daniel's request — the point cloud reads
   on its own against black.

   Every line of type here is the same full-strength ink, rather than
   the usual headline-bright / body-soft pair: over moving footage the
   softened body copy read as grey and half-erased rather than as a
   deliberate second level. */
.lidart-slide--video-tint .minimal-headline,
.lidart-slide--video-tint .lidart-body,
.lidart-slide--video-tint .lidart-body strong {
  color: var(--ink);
}

/* The rings SVG is a single fill="black" path — fine on the light
   slides it was drawn for, invisible on this one now that the orange
   panel is gone and the artwork sits directly on the dark video.
   brightness(0) crushes the shape to pure black whatever its source
   fills are, invert(1) flips that to pure white, so this stays right
   even if the asset is ever re-exported in another colour. */
.lidart-slide--video-tint .lidart-rings {
  filter: brightness(0) invert(1);
}

/* 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: 15px;
  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);
}

/* The inactive dots had their border hardcoded to ivory while the active
   one takes --accent, which .detail--invert already flips to black. On a
   light project that left one visible dot and four invisible ones, so
   the card counter read as "1 of 1". This affects every inverted project,
   not only this one. */
.detail--invert .slide-dot { border-color: rgba(0, 0, 0, 0.42); }

/* ── 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, .lidart-quote p { 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) {
  .detail, .detail-media img { transition: none; }
}

/* ── Manual de Autor — the novel/manual spread ─────────────────────────
   Colours sampled off Daniel's own reference frames rather than picked:
   ground rgb(137,135,230), the novel's paper rgb(232,232,232), the
   manual's page rgb(32,32,29), its warning bands rgb(250,22,38).

   The screen is laid out on ONE fixed 1728x972 canvas and scaled whole.
   That is not a shortcut — it is what makes two separate things true at
   once. The six markers only stay on their words if the pages never
   rewrap; and the composition stays the one Daniel designed only if
   every element keeps its proportion to every other one at every window
   size. Reflowing to the viewport would give up both. */
.novela-slide { padding: 0; }

.novela {
  position: relative;
  width: 100%;
  height: 100%;
  background: #8987e6;
  overflow: hidden;
}

.novela-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1728px;
  height: 972px;
  margin: -486px 0 0 -864px;
  transform: scale(var(--novela-scale, 1));
  transform-origin: center center;
  overflow: hidden;
  color: #fff;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* Playfair Display is already loaded for LidArt's headlines, so the
   reference's serif costs this screen no extra request. */
.novela-headline {
  position: absolute;
  left: 93px;
  top: 51.4%;
  transform: translateY(-50%);
  width: 440px;
  margin: 0;
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-weight: 600;
  font-size: 34px;
  line-height: 1.19;
  letter-spacing: -0.005em;
  color: #fff;
  transition: opacity 0.45s ease;
}

.novela[data-state='novela'] .novela-headline--manual,
.novela[data-state='manual'] .novela-headline--novela { opacity: 0; }

.novela-book {
  position: absolute;
  left: 508px;
  top: 195px;
  width: 105px;
  text-align: center;
}

.novela-frame .novela-book-img {
  display: block;
  width: 105px;
  height: auto;
  object-fit: contain;
}

.novela-book-title {
  margin: 14px 0 0;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.25;
}

.novela-book-author {
  margin: 4px 0 0;
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: 12.5px;
}

.novela-book-num {
  margin: 20px 0 0;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.75);
}

.novela-stage {
  position: absolute;
  left: 639px;
  top: 195px;
  width: 761px;
  height: 587px;
}

.novela-spread { position: absolute; inset: 0; }

.novela-doc {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 34px;
  padding: 38px 34px;
  transition: opacity 0.5s ease;
}

.novela-doc--novela {
  background: #e8e8e8;
  color: #14140f;
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 10.85px;
  line-height: 1.56;
}

.novela-doc--manual {
  background: #20201d;
  color: #e9e9e6;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 10.95px;
  line-height: 1.56;
}

.novela[data-state='novela'] .novela-doc--manual,
.novela[data-state='manual'] .novela-doc--novela {
  opacity: 0;
  pointer-events: none;
}

.novela-col { min-width: 0; }
.novela-col > * + * { margin-top: 7px; }
.novela-doc p, .novela-doc h4, .novela-doc ul { margin: 0; }

/* Set as the page of a novel, which is what it is, and what the campaign's
   own spread does: a justified column with a hard right edge, hyphenation
   on so Spanish breaks rather than opening rivers, and each paragraph
   after the first opened by an indent instead of a blank line. The
   reference's own hard line-breaks are gone from the markup — they were
   the wrap points of ITS column measure, and in one of any other width
   they landed mid-line and left short ragged stubs ("vi esa película de",
   "por una enorme"), which is what read as broken formatting. The manual
   page keeps its ragged edge and its spacing: it is documentation. */
.novela-doc--novela {
  text-align: justify;
  hyphens: auto;
  -webkit-hyphens: auto;
}

.novela-doc--novela .novela-col > p + p {
  margin-top: 0;
  text-indent: 1.3em;
}

.novela-run {
  font-weight: 700;
  font-size: 9.4px;
  letter-spacing: 0.02em;
  padding-bottom: 3px;
  border-bottom: 1px solid currentColor;
}

.novela-doc h4 { font-size: 11.4px; font-weight: 700; margin-top: 14px; }

.novela-list { list-style: none; padding: 0; }
.novela-list li { position: relative; padding-left: 9px; }
.novela-list li + li { margin-top: 4px; }
.novela-list li::before {
  content: '▸';
  position: absolute;
  left: 0;
  top: 0;
  font-size: 7px;
  line-height: 1.9;
}

.novela-band {
  background: #fa1626;
  color: #fff;
  font-weight: 700;
  font-size: 10.8px;
  padding: 3px 7px;
  margin-top: 15px;
}

.novela-warn {
  border: 1px solid rgba(233, 233, 230, 0.55);
  padding: 6px 8px;
  margin-top: 9px;
  font-size: 10.8px;
}

.novela-warn-head { font-weight: 700; letter-spacing: 0.04em; margin-bottom: 3px; }

/* The key words. In the reference these are not an overlay parked on top
   of the page — they are inline marks that carry their own space in the
   line, which is why the text around them never collides with them. Built
   that way here too: one padded box in the flow, in both documents, and
   the thing you press. An earlier version floated them above the text and
   they sat across whatever happened to be underneath. */
.novela-stage mark {
  background: #000;
  color: #fff;
  padding: 1.5px 6px;
  margin: 0 3px;
  border-radius: 1px;
  cursor: pointer;
  transition: background-color 0.35s ease, color 0.35s ease, transform 0.25s ease;
}

/* Never broken across a line. Once the phone justifies the story page,
   hyphens: auto happily split "retomar" into "re-" and "tomar" on two
   lines — and with it the black box, so the one thing the whole screen
   is about arrived in two pieces. */
.novela-stage mark {
  hyphens: none;
  -webkit-hyphens: none;
  white-space: nowrap;
}

.novela-doc--manual mark { background: #fff; color: #000; }

.novela-stage mark:hover { transform: scale(1.05); }

.novela-stage mark:focus-visible {
  outline: 2px solid #8987e6;
  outline-offset: 2px;
}

/* The word you pressed flashes once where it lands, so the swap reads as
   "this same word is also here" instead of two unrelated pages trading
   places. */
@keyframes novela-land {
  0%, 100% { transform: scale(1); }
  35% { transform: scale(1.22); }
}

.novela-stage mark.is-landing { animation: novela-land 0.6s ease; }

/* Phone only — the wide composition explains itself (see the note in
   index.html). */
.novela-hint,
.novela-state { display: none; }

.novela-folio {
  position: absolute;
  left: 639px;
  top: 809px;
  width: 761px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.82);
}

.novela-pill {
  width: 54px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 999px;
  font-size: 10.5px;
  letter-spacing: 0.04em;
}

.novela-arrows {
  position: absolute;
  left: 1432px;
  top: 450px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.novela-arrow {
  width: 29px;
  height: 29px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 50%;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
}

/* The story index, running off the bottom edge exactly as it does in the
   reference. The current story sits in the gap between the two grouped
   boxes rather than inside one — that gap is what marks it. */
.novela-tabs {
  position: absolute;
  left: 308px;
  top: 906px;
  width: 1425px;
  height: 120px;
  display: grid;
  grid-template-columns: repeat(5, 285px);
}

.novela-tab {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 15px 17px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  line-height: 1.32;
}

.novela-tab:nth-child(1) { border-radius: 7px 0 0 7px; }
.novela-tab:nth-child(2) { border-radius: 0 7px 7px 0; margin-left: -1px; }
.novela-tab:nth-child(4) { border-radius: 7px 0 0 7px; }
.novela-tab:nth-child(5) { border-radius: 0 7px 7px 0; margin-left: -1px; }
.novela-tab.is-current { border-color: transparent; }

.novela-tab-n { font-size: 11px; color: rgba(255, 255, 255, 0.7); }
.novela-tab-txt { font-size: 12.5px; }
.novela-tab-txt b { font-weight: 700; }
.novela-tab-txt i {
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: 11.5px;
}

/* ── Phone ────────────────────────────────────────────────────────────
   The canvas is abandoned here, not shrunk: 1728px scaled into a 390px
   screen sets the page type at about 2px. The phone keeps the same two
   documents and the same gesture — one page at a time, reflowed to a
   size that can be read, with the markers back inline where they came
   from and now the thing you tap. Word-for-word alignment cannot
   survive reflow, so nothing here pretends to it. */
@media (max-width: 899.98px) {
  /* Four bands instead of two: headline, the book this page belongs to,
     the page itself, and the line that says what the marks do. The page
     used to be the only thing under the headline and it was stretched to
     whatever height was left — a short story page left two thirds of the
     card as empty grey paper. Now the paper is sized by its own text and
     centred in the band it gets, and the space that is left goes to the
     three things the phone had been dropping. */
  .novela {
    display: grid;
    grid-template-rows: auto auto auto minmax(0, 1fr) auto;
    gap: 12px;
    padding: 70px 18px 38px;
  }

  .novela-frame {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    transform: none;
    overflow: visible;
    display: contents;
  }

  .novela-headline {
    position: static;
    transform: none;
    width: 100%;
    grid-row: 1;
    grid-column: 1;
    font-size: clamp(19px, 5vw, 26px);
    line-height: 1.16;
  }

  /* Both headlines share the one grid cell, so swapping them can never
     change the block's height and shove the page down. */
  .novela-folio,
  .novela-arrows,
  .novela-tabs { display: none; }

  /* The book comes back on the phone, laid on its side: without it the
     card was a page of prose with no way of telling whose story it was.
     Same four elements as the desktop column — cover, title, author,
     number — re-placed as a strip, so nothing is added to the markup. */
  .novela-book {
    position: static;
    left: auto;
    top: auto;
    width: auto;
    grid-row: 2;
    grid-column: 1;
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr) auto;
    column-gap: 11px;
    align-items: center;
    text-align: left;
  }

  .novela-frame .novela-book-img {
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 34px;
  }

  .novela-book-title {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: 12.5px;
    line-height: 1.2;
  }

  /* The desktop cover sets the title over two lines to match the printed
     jacket; in a one-line strip that break just makes a taller row. */
  .novela-book-title br { display: none; }

  .novela-book-author {
    grid-column: 2;
    grid-row: 2;
    margin: 1px 0 0;
    font-size: 11.5px;
    color: rgba(255, 255, 255, 0.8);
  }

  .novela-book-num {
    grid-column: 3;
    grid-row: 1 / span 2;
    margin: 0;
  }

  /* Same two-elements-in-one-cell trick as the headlines: the line
     changes with the page, the row never changes height. */
  .novela-hint {
    display: block;
    grid-row: 5;
    grid-column: 1;
    margin: 0;
    font-size: 11.5px;
    line-height: 1.35;
    letter-spacing: 0.01em;
    color: rgba(255, 255, 255, 0.78);
    transition: opacity 0.45s ease;
  }

  .novela[data-state='novela'] .novela-hint--manual,
  .novela[data-state='manual'] .novela-hint--novela { opacity: 0; }

  .novela-stage {
    position: relative;
    left: auto;
    top: auto;
    width: auto;
    /* 100%, not auto. The row this sits in is definite (.novela is
       height: 100% and the row is 1fr), but `height: auto` leaves the
       COMPUTED height indefinite, and a percentage max-height inside an
       indefinite box is simply dropped — which is what let the sheet run
       off the bottom of a short screen. */
    height: 100%;
    grid-row: 4;
    grid-column: 1;
    min-height: 0;
    /* The paper is centred in whatever height is left rather than
       filling it — a short page reads as a page, not as an empty
       sheet with three paragraphs at the top of it. */
    display: flex;
    align-items: center;
    overflow: hidden;
  }

  /* Both pages in the SAME grid cell, so the sheet is as tall as the
     taller of the two and pressing a word swaps the text without the
     card growing or shrinking under the finger. */
  .novela-spread {
    position: relative;
    inset: auto;
    display: grid;
    width: 100%;
    /* The sheet is capped here and scrolls here, not one level down on
       the page itself. A percentage max-height only resolves against a
       parent with a definite height, and .novela-spread's is its own
       content — so the cap on .novela-doc was being thrown away and on a
       short phone the paper ran straight off the bottom of the screen
       with its text cut mid-word. This box has the flex line's height to
       measure against, so the cap holds and the page's bottom edge stays
       on screen. */
    max-height: 100%;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* It sits on the campaign's purple as an object, not as a panel
       painted onto it. On this box rather than the page inside it, so
       the shadow stays at the sheet's edge instead of scrolling away
       with the text. */
    box-shadow: 0 16px 38px rgba(18, 17, 48, 0.26);
  }

  .novela-doc {
    position: relative;
    inset: auto;
    grid-area: 1 / 1;
    grid-template-columns: 1fr;
    gap: 0;
    padding: 18px 16px;
    /* No overscroll-behavior: contain on the scrolling box above, and
       that is the point. `contain` stops a scroll gesture from chaining
       to the ancestor — so a swipe that began inside the sheet could
       never reach the deck, and the card would not advance at all. */
    font-size: 13.4px;
  }

  .novela-doc--manual { font-size: 13.2px; }

  /* The block sits in the middle of the sheet rather than starting at
     the top of it — the two pages are different lengths, and the shorter
     one was leaving all of its spare paper in one lump at the bottom. */
  .novela-doc { align-content: center; }

  /* Which of the two documents is on screen, said plainly. A phone shows
     one page at a time with no second one beside it for contrast, and
     without this the card read as an undifferentiated wall of text. */
  .novela-state {
    grid-row: 3;
    grid-column: 1;
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 10.5px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
  }

  .novela-state-item {
    padding: 4px 10px;
    border: 1px solid rgba(255, 255, 255, 0.45);
    border-radius: 999px;
    color: rgba(255, 255, 255, 0.62);
    transition: background-color 0.45s ease, color 0.45s ease, border-color 0.45s ease;
  }

  .novela-state-sep { color: rgba(255, 255, 255, 0.5); font-size: 12px; }

  .novela[data-state='novela'] .novela-state-item--novela,
  .novela[data-state='manual'] .novela-state-item--manual {
    background: #fff;
    border-color: #fff;
    color: #1c1b46;
  }
  .novela-col > * + * { margin-top: 10px; }
  .novela-run { font-size: 11px; }
  .novela-doc h4 { font-size: 13px; }
  .novela-band { font-size: 11.5px; padding: 3px 8px; }
  .novela-warn { font-size: 12px; padding: 9px 10px; }
  .novela-list li { padding-left: 13px; }
  .novela-list li::before { font-size: 10px; line-height: 1.6; }

  /* One page on a phone, at Daniel's request — so three marked words
     rather than six. The second column is page 02 of the spread; stacked
     under the first it made a single card nearly 2000px long. */
  .novela-col + .novela-col { display: none; }

  /* And the two paragraphs of that page that carry no marked word come
     out as well, so the page fits without asking the card to scroll
     inside a snap container. All three words stay. */
  .novela-p--wide { display: none; }

  /* A finger needs more of the word than a cursor does. A word set
     inside running text can't be given a full 44px box without pulling
     the paragraph apart, so this takes it as far as the page allows —
     about 27px — and buys the room from the line spacing so the boxes
     still never touch the line above or below. */
  .novela-doc { line-height: 2; }
  .novela-stage mark { padding: 5px 8px; }
}

/* A short phone (an SE, a mini, anything under about 760px) is the one
   case where the story page does not fit the sheet at the comfortable
   size above, and it was ending on a line cut in half at the paper's
   edge — which reads as broken rather than as "there is more". Pulling
   the leading and the size back a step buys the ~80px it was missing, so
   the page ends where the paper does. The marks give up a little of
   their padding with it and still clear 24px. */
@media (max-width: 899.98px) and (max-height: 760px) {
  .novela { gap: 10px; padding-bottom: 32px; }
  .novela-doc { font-size: 12.6px; line-height: 1.78; padding: 14px 14px; }
  .novela-doc--manual { font-size: 12.4px; }
  .novela-stage mark { padding: 4px 6px; }
}

@media (prefers-reduced-motion: reduce) {
  .novela-doc,
  .novela-headline,
  .novela-stage mark { transition: none; }
  .novela-stage mark.is-landing { animation: none; }
}

/* ── Manual de Autor — the seven writers ───────────────────────────────
   One composition per author, exactly as in the campaign: a light ground
   with hairline rules, register crosses and a barcode, the name set huge
   in a didone italic, the Penguin colophon, a justified italic biography
   and a portrait. Only three things ever change — name, portrait, bio —
   so the list swaps those three and leaves the frame alone.

   Ground sampled off Daniel's frames: rgb(232,232,232).

   Same fixed canvas as the spread slide (1728x972, scaled whole). Here it
   earns its place for the composition alone: seven biographies of very
   different lengths have to sit in one frame without any of them moving
   the name, the rules or the portrait. */
.autores-slide { padding: 0; }

.autores {
  position: relative;
  width: 100%;
  height: 100%;
  background: #e8e8e8;
  overflow: hidden;
}

.autores-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1728px;
  height: 972px;
  margin: -486px 0 0 -864px;
  transform: scale(var(--autores-scale, 1));
  transform-origin: center center;
  color: #14140f;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

.autores-rule {
  position: absolute;
  left: 845px;
  width: 777px;
  height: 1px;
  background: rgba(20, 20, 15, 0.32);
}

.autores-rule--top { top: 168px; }
.autores-rule--bottom { top: 828px; }

/* The register crosses. Drawn with two gradients rather than a glyph so
   the arms stay hairline-thin at every scale. */
.autores-plus {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 17px;
  height: 17px;
  margin: -8px 0 0 -8px;
  background:
    linear-gradient(to right, rgba(20,20,15,.75) 0 100%) center / 100% 1px no-repeat,
    linear-gradient(to bottom, rgba(20,20,15,.75) 0 100%) center / 1px 100% no-repeat;
}

/* The strip of varying bars along the bottom edge of every page in the
   reference. Repeating gradients, so it costs nothing and never pixelates. */
.autores-barcode {
  position: absolute;
  left: 1108px;
  top: 852px;
  width: 380px;
  height: 22px;
  background:
    repeating-linear-gradient(to right, #14140f 0 2px, transparent 2px 5px),
    repeating-linear-gradient(to right, #14140f 0 1px, transparent 1px 3px);
  background-size: 55% 100%, 45% 100%;
  background-position: left, right;
  background-repeat: no-repeat;
  opacity: 0.85;
}

/* ── The list ─────────────────────────────────────────────────────────
   It sits in the wide empty column the reference leaves on the left, so
   it fills a void rather than displacing anything. */
.autores-list {
  position: absolute;
  z-index: 2;
  left: 150px;
  top: 50%;
  transform: translateY(-50%);
  width: 360px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.autores-list-label {
  margin: 0 0 14px;
  padding-bottom: 10px;
  width: 100%;
  border-bottom: 1px solid rgba(20, 20, 15, 0.32);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(20, 20, 15, 0.55);
}

.autores-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  /* 44px tall from the start — this is the only control on the screen. */
  min-height: 44px;
  padding: 11px 10px 11px 0;
  border: 0;
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: rgba(20, 20, 15, 0.45);
  transition: color 0.3s ease;
}

.autores-row-n {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.autores-row-name {
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: 21px;
  line-height: 1.1;
}

@media (hover: hover) {
  .autores-row:hover { color: rgba(20, 20, 15, 0.78); }
}

.autores-row[aria-current='true'] { color: #14140f; }

.autores-row:focus-visible {
  outline: 1px solid rgba(20, 20, 15, 0.6);
  outline-offset: 2px;
}

/* ── The panel ────────────────────────────────────────────────────────
   All seven stacked in one place, only one visible. Stacking rather than
   swapping text keeps every element on its own coordinates: nothing can
   shift when a longer biography replaces a shorter one. */
.autores-panels {
  position: absolute;
  inset: 0;
  /* The panel layer covers the whole frame, list included. Nothing in it
     is clickable on a desktop, so it must not swallow the presses meant
     for the names underneath it — which is exactly what it did until
     this line. The phone re-enables it, because there the panel scrolls. */
  pointer-events: none;
}

.autores-panel {
  position: absolute;
  inset: 0;
  transition: opacity 0.5s ease;
}

.autores-panel[aria-hidden='true'] {
  opacity: 0;
  pointer-events: none;
}

.autores-name {
  position: absolute;
  left: 575px;
  top: 232px;
  /* Shrink-to-fit, not a fixed box: a 640px block would reach under the
     portrait even for a short name, and any future check for a collision
     would be measuring the box instead of the letters. */
  width: max-content;
  max-width: 590px;
  margin: 0;
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 600;
  /* 68px, not larger: the longest name ("Arantza Portabales") measures
     608px at 72 and only 590 are free before the portrait starts, so at
     72 it ran three pixels under the photograph. */
  font-size: 68px;
  line-height: 1.05;
  letter-spacing: -0.005em;
  white-space: nowrap;
}

/* Two classes on purpose. `.slideshow-slide img` (0,1,1) sets every
   image in a slide to width/height 100% with object-fit: cover — a
   single-class rule here loses to it, and the portrait blew up to fill
   the whole frame and ran off the edge. Same reason for the colophon
   below, and it is the same trick used further up this file. */
.autores-frame .autores-photo {
  position: absolute;
  left: 1180px;
  top: 232px;
  width: 269px;
  height: 380px;
  object-fit: cover;
  display: block;
}

/* Justified, like the reference — that is what gives these blocks their
   hard right edge and the wide word spacing on the shorter lines. */
.autores-bio {
  position: absolute;
  left: 683px;
  top: 470px;
  width: 332px;
  margin: 0;
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 12.4px;
  line-height: 1.5;
  text-align: justify;
  hyphens: auto;
}

/* Right edge flush with the biography column below it, as in the
   reference. */
.autores-frame .autores-penguin {
  position: absolute;
  left: 969px;
  top: 388px;
  width: 46px;
  height: auto;
  display: block;
}

/* ── Phone ────────────────────────────────────────────────────────────
   The canvas is dropped, not shrunk. The list becomes a strip of names
   across the top — it is the control, so it stays in reach and in view
   while the panel under it changes — and the panel itself reflows into
   one readable column. */
@media (max-width: 899.98px) {
  .autores {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    gap: 12px;
    padding: 72px 0 42px;
  }

  .autores-frame {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    transform: none;
    display: contents;
  }

  .autores-rule,
  .autores-plus,
  .autores-barcode,
  .autores-frame .autores-penguin { display: none; }

  .autores-list {
    position: static;
    transform: none;
    width: 100%;
    grid-row: 1;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding: 0 14px;
    border-bottom: 1px solid rgba(20, 20, 15, 0.25);
  }

  .autores-list::-webkit-scrollbar { display: none; }
  .autores-list-label { display: none; }

  .autores-row {
    width: auto;
    flex: 0 0 auto;
    padding: 10px 14px;
    gap: 7px;
  }

  .autores-row-name { font-size: 17px; white-space: nowrap; }

  .autores-row[aria-current='true'] {
    box-shadow: inset 0 -2px 0 #14140f;
  }

  .autores-panels {
    position: relative;
    inset: auto;
    grid-row: 2;
    min-height: 0;
    pointer-events: auto;
  }

  .autores-panel {
    inset: 0;
    /* A column rather than a stack of statically-placed blocks, so the
       portrait can be the element that absorbs whatever height is left
       — the biographies are short now and the card was ending in half a
       screen of empty grey. */
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    /* No overscroll-behavior: contain here, and that is the point.
       `contain` stops a scroll gesture from chaining to the ancestor —
       so a swipe that began inside this box could never reach the deck,
       and the card would not advance at all. Everything on these cards
       is now sized to fit, so there is nothing to protect from chaining
       and the deck gets the gesture. */
    -webkit-overflow-scrolling: touch;
    padding: 4px 16px 8px;
  }

  .autores-name {
    position: static;
    width: auto;
    font-size: clamp(26px, 7.4vw, 36px);
    white-space: normal;
    margin-bottom: 10px;
  }

  /* Not floated. Wrapping the biography around a portrait leaves it
     about 55% of a phone's width, and this italic serif at that measure
     breaks into hyphenated fragments ("Dere-cho", "Admin-istración").
     The portrait sits above it instead and the text gets the full
     column. */
  .autores-frame .autores-photo {
    position: static;
    /* Grows into the free space and shrinks first when a biography runs
       long (min-height: 0 is what lets it), holding the campaign's own
       269x380 crop throughout. */
    flex: 1 1 auto;
    min-height: 0;
    width: auto;
    height: auto;
    max-height: 52dvh;
    max-width: 100%;
    aspect-ratio: 269 / 380;
    align-self: flex-start;
    margin: 0 0 14px;
  }

  .autores-name,
  .autores-bio { flex: 0 0 auto; }

  /* Sized so the longest biography on the list fits the card. Scrolling
     inside a card that sits in a snap container is a fight the card
     rarely wins, and here it was only ever hiding a line or two. */
  .autores-bio {
    position: static;
    width: auto;
    font-size: 13px;
    line-height: 1.5;
    text-align: left;
  }
}

@media (prefers-reduced-motion: reduce) {
  .autores-panel,
  .autores-row { transition: none; }
}

/* ── Manual de Autor — the seven books ─────────────────────────────────
   The ground takes the colour of whichever cover is showing and both
   change on their own. Colours are sampled from Daniel's own mockups,
   never picked: #7d9dbb, #ca98be, #6462b4, #76906f, #b1897b, #c39a8d,
   #6160b2.

   Same fixed 1728x972 canvas as the other two slides, scaled whole. */
.libros-slide { padding: 0; }

.libros {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--libros-colour, #7d9dbb);
  transition: background-color 1.1s ease;
  overflow: hidden;
}

.libros-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1728px;
  height: 972px;
  margin: -486px 0 0 -864px;
  transform: scale(var(--libros-scale, 1));
  transform-origin: center center;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* The text keeps its own paper, the way the covers themselves do: white
   stock, coloured ground around it. Not a stylistic preference — across
   these seven grounds no single ink stays legible (white measures 2.5:1
   on the lilac), and on the paper every one of them measures 13:1. */
.libros-panel {
  position: absolute;
  left: 0;
  top: 0;
  width: 700px;
  height: 100%;
  padding: 128px 96px 96px 118px;
  background: #f4f3f0;
  color: #16150f;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Centred as one column rather than pinned top and bottom: with the
     title block pushed to the floor there was a dead 150px band in the
     middle of the panel. */
  justify-content: center;
}

/* Daniel's call: this card is the headline and the three paragraphs, and
   nothing else. The eyebrow, the six key words, the running title/author
   and the row of marks were removed from the markup rather than hidden —
   the books and the ground still change on their own, which is what the
   card is for. */

.libros-headline {
  margin: 0 0 26px;
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-weight: 600;
  font-size: 40px;
  line-height: 1.11;
  letter-spacing: -0.012em;
  text-wrap: balance;
}

.libros-body {
  margin: 0 0 13px;
  max-width: 460px;
  font-size: 14px;
  line-height: 1.62;
  color: rgba(22, 21, 15, 0.86);
}

/* ── The book ─────────────────────────────────────────────────────────
   Daniel re-cut all seven in September 2026: they were 764x968 portrait
   crops padded out with a flat ground, and they are now complete 1920x1080
   frames, each with its own lit ground and the book's cast shadow in it.
   That changes what this element has to do. It used to be a small object
   parked in the middle of a flat field, with a mask feathering its edges
   so the padded crop would not read as a lighter rectangle; now it IS the
   field, so it runs full bleed from the paper panel's edge to the right
   side of the canvas, cover-cropped, and the mask is gone — there is no
   longer a seam to hide.

   Starting the frame at 700px (flush with the panel, not at 856px with a
   band of flat colour between them) is what removes the last join: the
   photographed ground has a vignette and the CSS colour does not, so any
   strip of one beside the other showed as a step.

   1028x972 crops the 16:9 frame to its middle 59%, and every one of the
   seven books sits between 36% and 65% of its own width — so the object
   is always whole and always centred, with its ground and shadow around
   it. */
.libros-stage {
  position: absolute;
  left: 700px;
  top: 0;
  width: 1028px;
  height: 100%;
}

.libros-frame .libros-cover {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  object-fit: cover;
  display: block;
  transition: opacity 1.1s ease;
}

.libros-frame .libros-cover[aria-hidden='true'] { opacity: 0; }

/* ── Phone ────────────────────────────────────────────────────────────
   The canvas is dropped. The book takes the top of the screen on the
   colour, the paper panel takes the rest and scrolls. */
@media (max-width: 899.98px) {
  .libros {
    display: grid;
    grid-template-rows: 38vh minmax(0, 1fr);
    padding-top: 56px;
  }

  .libros-frame {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    transform: none;
    display: contents;
  }

  .libros-stage {
    position: relative;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    grid-row: 1;
    min-height: 0;
  }

  /* Cover, not contain: the frame is 16:9 and this band is nearly square,
     so fitting the whole frame into it would letterbox the book down to a
     stamp. Cropped, the book fills the band — it sits at 10-83% of the
     frame's height, so nothing of it is ever cut. */
  .libros-frame .libros-cover {
    width: 100%;
    height: 100%;
    margin: 0;
    left: 0;
    top: 0;
    transform: none;
    object-fit: cover;
  }

  .libros-panel {
    position: relative;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    grid-row: 2;
    min-height: 0;
    /* Same gutter and paragraph rhythm as every other project's phone
       text card (.minimal-text: clamp(24px, 4vw, 56px) sides, 15px/1.65
       body, 16px between blocks) — this card used to run on its own
       tighter 18px/13.5px scale, which read as a different site. The
       vertical padding stays its own: .minimal-text's 90-140px top
       clears the detail header, while this panel already sits under the
       book in row 2 of the grid and only needs air. */
    padding: 28px clamp(24px, 4vw, 56px) 40px;
    /* Centred on the phone at Daniel's request — the column is narrow
       enough here that a flush-left block sat off to one side of the
       paper. Desktop keeps its left-aligned column. */
    align-items: center;
    text-align: center;
    /* Back to the top, not centred. In a scrolling box `justify-content:
       center` puts the overflowing head of the content above the scroll
       origin, where it cannot be reached — the kicker and the first two
       lines of the headline were simply cut off. */
    justify-content: flex-start;
    /* Auto margins, not justify-content: center. They centre the block
       in the paper exactly the same way while it fits — the copy is
       short now and it was sitting in the top third of a tall white
       field — but they collapse to zero the moment the content is
       taller than the box, so nothing is ever pushed above the scroll
       origin where it cannot be reached. */
    overflow-y: auto;
    /* No overscroll-behavior: contain here, and that is the point.
       `contain` stops a scroll gesture from chaining to the ancestor —
       so a swipe that began inside this box could never reach the deck,
       and the card would not advance at all. Everything on these cards
       is now sized to fit, so there is nothing to protect from chaining
       and the deck gets the gesture. */
    -webkit-overflow-scrolling: touch;
  }

  /* The pair of autos is what centres the block (see the note on
     justify-content above). Both halves are needed — the closing one
     has to be written as :last-of-type, or the rule that used to zero
     that margin outranks it and all the free space collects at the top,
     dropping the text onto the floor of the paper. */
  .libros-panel > :first-child { margin-top: auto; }

  /* A measure, even though the panel is only a phone wide — "phone" here
     runs all the way to 899px, and centred text set across 840px of it
     is a line nobody can track back to the start of. Both caps are in em
     so they follow their own size. */
  .libros-headline { font-size: clamp(22px, 5.9vw, 29px); margin-bottom: 16px; max-width: 15em; }
  .libros-body { font-size: 15px; line-height: 1.65; margin-bottom: 16px; max-width: 26em; }
  .libros-body:last-of-type { margin-bottom: auto; }
}

/* The rotation is decoration: it shows what a still frame already shows.
   With motion turned down it stops, and the dots still work by hand. */
@media (prefers-reduced-motion: reduce) {
  .libros,
  .libros-frame .libros-cover,
}

/* ── Napkinbook ────────────────────────────────────────────────────────
   The campaign's own world, taken from napkinbook.es: a warm paper ground
   (#f1eade, sampled off the live site), black ink, and Instrument Serif
   throughout — the face the site itself loads. Napkins are the site's own
   SVGs, each a white square already rotated on its own artboard.

   Fixed 1728x972 canvas, scaled whole, like Manual de Autor. */
.nb-slide { padding: 0; }

/* No `.nb-slide > .nb-frame` rule here. An earlier one set the frame to
   position: relative at two-class specificity, which outranked the phone
   override below — so on a phone the frame kept the desktop canvas's
   `left: 50%` and `margin-left: -864px` and started 195px off the left
   edge, pushing every card 163px wider than the screen. */
.nb-slide {
  position: relative;
  width: 100%;
  height: 100%;
  background: #f1eade;
  overflow: hidden;
}

.nb-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1728px;
  height: 972px;
  margin: -486px 0 0 -864px;
  transform: scale(var(--nb-scale, 1));
  transform-origin: center center;
  color: #14120e;
  font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
}

/* ── 1 · cover  ·  2 · the idea ───────────────────────────────────── */
.nb-slide--cover,
.nb-slide--idea { padding: 0; }

.nb-full {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The idea's words sit on the photograph, so they are white and they get
   a wash: the frame is dark overall, but its wooden highlights are not,
   and white measured about 2:1 on those. A plain left-to-right ramp,
   heaviest under the column of text. */
.nb-slide--idea::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to right, rgba(12, 10, 8, 0.88) 0%, rgba(12, 10, 8, 0.74) 42%, rgba(12, 10, 8, 0.3) 74%, rgba(12, 10, 8, 0.12) 100%);
}

.nb-slide--idea .nb-frame { z-index: 2; }

.nb-idea-text {
  position: absolute;
  left: 128px;
  top: 50%;
  transform: translateY(-50%);
  width: 720px;
}

.nb-headline {
  margin: 0 0 26px;
  font-size: 58px;
  font-weight: 400;
  line-height: 1.06;
  letter-spacing: -0.005em;
  color: #fff;
}

.nb-body {
  margin: 0 0 14px;
  font-size: 17px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.93);
}

.nb-body:last-of-type { margin-bottom: 0; }

.nb-cities {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 30px;
  margin-top: 34px;
  padding-top: 24px;
  border-top: 1px solid rgba(255, 255, 255, 0.4);
  font-size: 19px;
  color: #fff;
}

/* ── 3 · the object ───────────────────────────────────────────────── */
.nb-slide--book { padding: 0; }

.nb-book-full {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}


/* ── 4 · the bars ─────────────────────────────────────────────────── */
.nb-bars { position: absolute; inset: 0; }

/* Two columns, broken by city. Twenty-five bars in one column needs the
   type down at about 11px to fit the canvas; in two it stays at 19 and
   the groups stay whole (break-inside: avoid), which is what makes the
   cities readable as cities. */
.nb-list {
  position: absolute;
  left: 108px;
  top: 138px;
  width: 600px;
  height: 800px;
  column-count: 2;
  column-gap: 34px;
}

.nb-group { break-inside: avoid; page-break-inside: avoid; }

.nb-list-head {
  column-span: all;
  margin: 0 0 14px;
  padding-bottom: 10px;
  width: 100%;
  border-bottom: 1px solid rgba(20, 18, 14, 0.3);
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(20, 18, 14, 0.55);
}

.nb-city {
  margin: 12px 0 1px;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(20, 18, 14, 0.45);
}

.nb-row {
  display: block;
  width: 100%;
  padding: 4px 6px 4px 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 19px;
  line-height: 1.25;
  text-align: left;
  color: rgba(20, 18, 14, 0.5);
  cursor: pointer;
  transition: color 0.25s ease;
}

@media (hover: hover) {
  .nb-row:hover { color: rgba(20, 18, 14, 0.85); }
}

.nb-row[aria-current='true'] { color: #14120e; }

.nb-row[aria-current='true']::before {
  content: '';
  display: inline-block;
  width: 16px;
  height: 1px;
  margin: 0 8px 5px 0;
  background: #14120e;
}

.nb-row:focus-visible { outline: 1px solid rgba(20, 18, 14, 0.6); outline-offset: 2px; }

/* All 25 stacked in one place, so a long story can never move the napkin
   or the heading above it. */
.nb-panels { position: absolute; inset: 0; pointer-events: none; }

/* The two halves of the change are timed differently on purpose, and
   that is the whole of the fix — no new effect, the same single opacity.
   Both panels used to run the one 0.45s curve, so halfway through the
   swap there were two napkins on screen at half strength each; against
   photographs (they were flat SVG squares until September 2026) that
   midpoint read as a double exposure, and coming out of it looked like a
   cut. The outgoing one now leaves first and quickly, and the incoming
   one starts after it has mostly gone and takes longer to arrive, so the
   card is briefly just its own paper and one napkin dissolves into the
   next instead of through it. */
.nb-panel {
  position: absolute;
  inset: 0;
  transition: opacity 0.7s cubic-bezier(0.33, 0, 0.2, 1) 0.18s;
}

.nb-panel[aria-hidden='true'] {
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.4, 0, 0.9, 0.5) 0s;
}

/* No drop-shadow filter any more. These were flat white SVG squares
   until September 2026, and the shadow was what lifted them off the
   paper; Daniel replaced them with photographed napkins that carry their
   own lit shadow in the file, and the CSS one on top of that read as two
   shadows at two angles. */
.nb-frame .nb-napkin {
  position: absolute;
  left: 754px;
  top: 296px;
  width: 420px;
  height: 420px;
  display: block;
}

.nb-text {
  position: absolute;
  left: 1214px;
  top: 308px;
  width: 406px;
}

.nb-place {
  margin: 0 0 10px;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(20, 18, 14, 0.5);
}

.nb-bar {
  margin: 0 0 8px;
  font-size: 36px;
  font-weight: 400;
  line-height: 1.1;
}

.nb-address {
  margin: 0 0 18px;
  font-size: 16px;
  font-style: italic;
  color: rgba(20, 18, 14, 0.7);
}

.nb-story {
  margin: 0 0 26px;
  font-size: 16.5px;
  line-height: 1.55;
  color: #29241c;
}

/* A real link to a real place — so it gets a real target, not a caption. */
.nb-maps {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 11px 22px;
  border: 1px solid #14120e;
  color: #14120e;
  text-decoration: none;
  font-size: 16px;
  pointer-events: auto;
  transition: background-color 0.25s ease, color 0.25s ease;
}

@media (hover: hover) {
  .nb-maps:hover { background: #14120e; color: #f1eade; }
}

.nb-maps:focus-visible { outline: 2px solid #14120e; outline-offset: 3px; }

/* ── Phone ────────────────────────────────────────────────────────────
   The canvas is dropped, not shrunk. Each card reflows into one column;
   the bar list becomes a strip of names across the top, so it stays in
   reach and in view while the napkin and the story change beneath it. */
@media (max-width: 899.98px) {
  .nb-frame {
    position: static;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    margin: 0;
    transform: none;
    display: block;
  }

  .nb-slide {
    display: block;
    overflow-y: auto;
    /* No overscroll-behavior: contain here, and that is the point.
       `contain` stops a scroll gesture from chaining to the ancestor —
       so a swipe that began inside this box could never reach the deck,
       and the card would not advance at all. Everything on these cards
       is now sized to fit, so there is nothing to protect from chaining
       and the deck gets the gesture. */
    -webkit-overflow-scrolling: touch;
    /* The extra bottom padding clears the slide dots, which are fixed
       over every card — the Maps button was sitting under them. */
    padding: 68px 16px 68px;
  }

  /* The three photographic cards stay full bleed on a phone. */
  /* overflow: visible, not hidden. An overflow:hidden box is still a
     scroll container as far as a touch gesture is concerned: the swipe
     landed on it, moved nothing, and never reached the deck — the cover
     card simply would not advance. */
  .nb-slide--cover,
  .nb-slide--idea,
  .nb-slide--book { padding: 0; overflow: visible; }

  /* At this aspect only about a third of the packaging photograph's width
     is on screen, and centred it would land on the empty grey between the
     two objects. 78% puts the black box and its lettering in the crop. */
  .nb-book-full { object-position: 78% 50%; }

  /* The wash turns downward here: the text is a block at the foot of the
     card, not a column down its left side. */
  .nb-slide--idea::after {
    background: linear-gradient(to bottom, rgba(12, 10, 8, 0.3) 0%, rgba(12, 10, 8, 0.56) 42%, rgba(12, 10, 8, 0.9) 100%);
  }

  .nb-slide--idea .nb-frame {
    position: absolute;
    inset: auto 0 0 0;
    display: block;
    padding: 0 18px 54px;
  }

  .nb-idea-text {
    position: static;
    transform: none;
    width: auto;
  }

  .nb-list,
  .nb-text {
    position: static;
    width: auto;
    height: auto;
  }

  .nb-headline { font-size: clamp(24px, 6.4vw, 32px); margin-bottom: 12px; }
  .nb-body { font-size: 13.6px; line-height: 1.46; margin-bottom: 9px; }
  .nb-cities { font-size: 14px; gap: 5px 15px; margin-top: 13px; padding-top: 12px; }

  .nb-bars { position: static; }

  .nb-list {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    margin: 0 -16px 14px;
    padding: 0 12px;
    border-bottom: 1px solid rgba(20, 18, 14, 0.25);
  }

  .nb-list::-webkit-scrollbar { display: none; }
  .nb-list-head { display: none; }

  /* Columns are a desktop device; sideways, each city group is just a
     run of names with its label in front. */
  .nb-list { column-count: auto; }
  .nb-group { display: flex; align-items: center; flex: 0 0 auto; }

  /* The city is a caption on the row, not a heading above a group, once
     the list runs sideways. */
  .nb-city {
    align-self: center;
    flex: 0 0 auto;
    margin: 0 4px 0 10px;
    padding-left: 10px;
    border-left: 1px solid rgba(20, 18, 14, 0.2);
  }

  .nb-city:first-of-type { border-left: 0; padding-left: 0; margin-left: 0; }

  .nb-row {
    width: auto;
    flex: 0 0 auto;
    min-height: 44px;
    padding: 10px 12px;
    font-size: 17px;
    white-space: nowrap;
  }

  .nb-row[aria-current='true'] { box-shadow: inset 0 -2px 0 #14120e; }
  .nb-row[aria-current='true']::before { display: none; }

  .nb-panels { position: relative; inset: auto; pointer-events: auto; }

  .nb-panel {
    position: relative;
    inset: auto;
    display: none;
  }

  .nb-panel[aria-hidden='false'] { display: block; }
  .nb-panel[aria-hidden='true'] { display: none; }

  .nb-frame .nb-napkin {
    position: static;
    width: 66%;
    max-width: 280px;
    height: auto;
    margin: 4px auto 18px;
  }

  .nb-bar { font-size: clamp(28px, 7.4vw, 36px); }
  .nb-address { font-size: 15px; }
  .nb-story { font-size: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .nb-panel, .nb-row, .nb-maps { transition: none; }
}

/* ── Closing card: the four rings ──────────────────────────────────────
   The last authored card of Manual de Autor and of Napkinbook. It takes
   the project's own ground (--black, which .detail--invert flips to the
   light value each of them sets) and centres the rings on it.

   brightness(0) forces the mark to solid black whatever its source fills
   are — the file mixes fill="black" and fill="white" — so it reads the
   same on either project's paper. */
.audi-end {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

.audi-end .audi-end-rings {
  width: clamp(88px, 9vw, 168px);
  height: auto;
  display: block;
  filter: brightness(0);
  opacity: 0.88;
}

/* The same closing card on black. The rings SVG is a single fill="black"
   path, so the light-ground version crushes it to pure black with
   brightness(0); here that same crush is inverted to pure white, which
   keeps the mark right whatever colour the asset is ever re-exported in
   — the trick .lidart-slide--video-tint already uses on this artwork. */
.audi-end--dark { background: #000; }

.audi-end--dark .audi-end-rings {
  filter: brightness(0) invert(1);
  opacity: 0.92;
}

@media (max-width: 899.98px) {
  .audi-end .audi-end-rings { width: 108px; }
}

/* Keep the landing preview dark while a cover changes or finishes loading. */
#root .portfolio-preview-layer { background-color: #000 !important; }
#root { background-color: #000 !important; }
