/* =====================================================
   MIREILLE CHÁVEZ STUDIO — WEBSITE
   Swiss / Neo-Grotesque type system, neutral gallery palette
   ===================================================== */

:root {
  /* Surfaces — neutral gallery whites */
  --bg:         #FAFAFA;
  --bg-2:       #EFEFEF;
  --bg-3:       #FFFFFF;

  /* Ink */
  --text:       #1A1A1A;
  --muted:      #666666;   /* 5.5:1 on --bg */

  /* Text that sits on top of artwork, never on the page ground */
  --on-img:       #FFFFFF;
  --on-img-muted: rgba(255, 255, 255, 0.62);

  --border:     rgba(26, 26, 26, 0.08);
  --border-mid: rgba(26, 26, 26, 0.16);
  --shadow-sm:  0 2px 14px rgba(26, 26, 26, 0.05);
  --shadow-lg:  0 30px 70px rgba(26, 26, 26, 0.14);

  --sans:       'Inter', 'Helvetica Neue', Arial, sans-serif;
  --ease:       cubic-bezier(0.25, 0.1, 0.25, 1);
  --ease-out:   cubic-bezier(0.0, 0.0, 0.2, 1);
  --nav-h:      76px;

  /* 290826ZP — the single vertical-rhythm value for the mobile/tablet
     content stack (About's bio -> photo -> Academic Background ->
     Exhibitions). 128px isn't an arbitrary round number: it's the exact
     gap .section's own mobile padding (64px bottom + 64px top) already
     produces between every OTHER pair of stacked sections on this page
     (measured: About-content-to-Education-title and Education-content-
     to-Exhibitions-title both already land on 128px). The one place that
     didn't match was the in-page bio-to-photo gap inside About itself
     (were 44px .about-bio margin + 54px grid gap = 98px, a leftover
     mismatch from unrelated past edits) — using this same value there
     instead of a fresh 3rem/4rem guess is what actually makes the flow
     "single standardized" against spacing this page already gets right
     everywhere else, rather than introducing a third, different gap
     size to the page. */
  --stack-gap:  128px;

  /* Archive accents. --accent-terracotta (#c83228) is for text on the plain
     page ground (~5.1:1 on --bg, passes AA); nothing uses it right now — its
     only user, the ==highlight== markers in the lightbox description
     (290826ZQ), went when the Archive lightbox lost its text in 290826ZU, but
     it's kept as the page-ground counterpart.
     --archive-overlay-ink is the colour of the type printed OVER Archive
     photos: a soft off-white from 290826ZV (it was bright red #E63929 in
     ZR/ZS, then deep terracotta #B83A2A in ZT — both were hard to read on the
     mostly dark photos, which is part of why this changed). #F5F5F0 is
     ~19:1 on black and ~16:1 on #1A1A1A, and stays above the 4.5:1 normal-
     text bar over anything darker than a mid-dark grey (~#555 gives 6.8:1,
     #777 4.1:1). Over a mid-tone or bright patch of photo it drops under that
     bar, which is what the overlay's soft text-shadow is for — see the
     overlay block. */
  --archive-overlay-ink: #F5F5F0;
}

/* ── RESET ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
  font-size: 19px;
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);
  background: var(--bg);
  overflow-x: hidden; /* belt-and-suspenders alongside body's rule below */
}
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.65;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  cursor: none; /* hand off to custom cursor on desktop */
}
img { display: block; max-width: 100%; }
button { cursor: none; }
a { cursor: none; }

::selection { background: rgba(26, 26, 26, 0.10); color: var(--text); }

/* ── GRAIN (paper texture — multiply reads as tooth, not haze) ── */
.grain {
  position: fixed;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
  background-size: 200px;
  opacity: 0.045;
  mix-blend-mode: multiply;
}

/* ── CUSTOM CURSOR ── */
.cursor {
  position: fixed;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  will-change: left, top;
  opacity: 0;
  transition:
    width  0.5s var(--ease),
    height 0.5s var(--ease),
    opacity 0.3s;
  display: flex;
  align-items: center;
  justify-content: center;

  /* Liquid glass, neutral ink tint only — no accent hue */
  background: linear-gradient(
    145deg,
    rgba(26, 26, 26, 0.11) 0%,
    rgba(26, 26, 26, 0.04) 60%,
    rgba(26, 26, 26, 0.07) 100%
  );
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  border: 1px solid rgba(26, 26, 26, 0.16);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(26, 26, 26, 0.07),
    0 4px 20px rgba(26, 26, 26, 0.10);
}

.cursor.ready { opacity: 1; }

.cursor.on-link {
  width: 54px;
  height: 54px;
  background: linear-gradient(
    145deg,
    rgba(26, 26, 26, 0.14) 0%,
    rgba(26, 26, 26, 0.05) 100%
  );
  border-color: rgba(26, 26, 26, 0.34);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    0 4px 24px rgba(26, 26, 26, 0.12);
}

/* Over artwork the cursor must read on any image, so it inverts to smoked glass */
.cursor.on-work {
  width: 84px;
  height: 84px;
  background: linear-gradient(
    145deg,
    rgba(20, 20, 20, 0.46) 0%,
    rgba(20, 20, 20, 0.32) 100%
  );
  backdrop-filter: blur(10px) saturate(130%);
  -webkit-backdrop-filter: blur(10px) saturate(130%);
  border-color: rgba(255, 255, 255, 0.30);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.26),
    0 8px 32px rgba(26, 26, 26, 0.20);
}

.cursor.on-work .cursor-label { opacity: 1; color: var(--on-img); }

.cursor-label {
  font-family: var(--sans);
  font-size: 0.48rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0;
  transition: opacity 0.3s;
  white-space: nowrap;
}

/* ── NAVIGATION ── */
#nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 500;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 52px;
  transition: background 0.6s var(--ease), border-color 0.6s, box-shadow 0.6s;
  border-bottom: 1px solid transparent;
}

#nav.scrolled {
  background: rgba(250, 250, 250, 0.88);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border-bottom-color: var(--border);
  box-shadow: var(--shadow-sm);
}

/* Matches .hero-name's compact bold grotesk treatment (290826G/H) exactly
   — same family override, weight, tracking, and line-height — so there's
   no stylistic clash the moment this fades in as the hero title scrolls
   out of view. Only font-size differs, and only on mobile (below), since
   the nav bar's own scale calls for something more modest than the hero. */
.nav-brand {
  font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 1rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 0.95;
  color: var(--text);
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease);
  text-shadow: none !important; /* 290826V — persistent header mark, sharp/unshadowed on any viewport */
}
#nav.scrolled .nav-brand {
  opacity: 1;
  pointer-events: auto;
}
#nav.scrolled .nav-brand:hover { opacity: 0.5; }

.nav-links {
  display: flex;
  list-style: none;
  gap: 44px;
}
.nav-links a {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
  transition: opacity 0.2s;
  text-shadow: none !important; /* 290826V — the floating-over-hero nav links, sharp/unshadowed on any viewport */
}
.nav-links a:hover { opacity: 0.5; }

.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 7px;
  background: none;
  border: none;
  padding: 4px;
  /* Must stay above .nav-links (290826M raised that to 9999) so the open
     panel's close "X" stays clickable instead of getting buried under it. */
  z-index: 10000;
}
.nav-toggle span {
  display: block;
  width: 22px;
  height: 1.5px;
  background: var(--text);
  transition: transform 0.4s var(--ease), opacity 0.3s;
}
.nav-toggle[aria-expanded="true"] span:first-child { transform: translateY(8.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:last-child  { transform: rotate(-45deg); }

/* ── HERO ── */
.hero {
  position: relative;
  height: 100svh;
  min-height: 620px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
}

.hero-bg {
  position: absolute;
  inset: 0;
}

.hero-img {
  position: absolute;
  inset: -8% -2%;
  background: var(--bg-2) center/cover no-repeat;
  will-change: opacity;
  opacity: 0;
  transition: opacity 2s ease-in-out;
}
.hero-img.active { opacity: 1; }

/* 290826V — was two stacked gradient washes here: a light scrim at the
   top (for nav legibility over any artwork) and a bottom wash that
   dissolved the image into the page ground (for .hero-name's
   legibility). Removed per an explicit request for a completely crisp,
   unmasked hero photo on every viewport — mobile already had this same
   background: none (290826J, since its own overlapping-nav problem was
   solved a different way); this just brings desktop in line with it
   instead of carrying two different rules for the same "no wash"
   result. The trade-off is real and deliberate, not overlooked: the
   nav-brand/hero-name/nav-links text below now sits directly on
   whatever the current hero photo's own colors happen to be at that
   spot, with no scrim or shadow (see the text-shadow removals further
   down) softening the transition — legibility now depends entirely on
   each photo's own contrast rather than a wash compensating for it. */
.hero-bg::after { content: none; }

.hero-body {
  position: relative;
  z-index: 2;
  padding: 0 52px 66px;
}

/* The name — compact bold grotesk treatment (290826G), replacing the
   previous full-width split-to-edges display headline entirely. The
   "ARTIST" tagline and location line that used to sit above/below it
   (.hero-meta/.hero-tagline, .hero-foot/.hero-location) were removed
   from index.html per this same request — this is now the hero's only
   text. Weight 700 (added to the Google Fonts request in index.html —
   Inter wasn't loading a 700 file before) and tight -0.02em tracking are
   the standard-correct pairing for a heavy weight (unlike 290826B's
   light-weight/positive-tracking pairing immediately before this, which
   this entry deliberately reverses along with the weight itself — the
   two changes go together, not independently). display:flex is kept
   (rather than removed) specifically so the two-span reveal animation
   below still works, but justify-content changed from the old
   space-between (which pinned each word to an outer edge) to flex-start
   with a gap, so "Mireille" and "Chávez" sit together as one compact
   title instead of spanning the full width. flex-wrap:wrap plus the
   0.95 line-height is what makes "compact vertical stacking" (as
   specified) the graceful fallback if the two words don't fit on one
   line at a given width, rather than overflowing. */
.hero-name {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: baseline;
  gap: 0 0.28em;
  font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  font-size: clamp(2.5rem, 3vw, 3.2rem);
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--text);
  overflow: hidden;
  margin-bottom: 22px;
  /* 290826V — explicit, unconditional guarantee (not just "no rule
     currently sets one") that the hero title never carries a shadow, on
     any viewport. Replaces the mobile-only text-shadow that used to sit
     here (see the removal note in the mobile media query below). */
  text-shadow: none !important;
}

.hero-name-l,
.hero-name-r {
  display: block;
  transform: translateY(100%);
  opacity: 0;
  animation: nameReveal 1.1s var(--ease-out) forwards;
}
.hero-name-r {
  animation-delay: 0.12s;
}

@keyframes nameReveal {
  to { transform: translateY(0); opacity: 1; }
}

/* ── SECTIONS ── */
.section {
  padding: 132px 52px;
  max-width: 1440px;
  margin: 0 auto;
}

.section-title {
  font-family: var(--sans);
  font-size: clamp(1.9rem, 3.1vw, 2.9rem);
  font-weight: 500;
  letter-spacing: 0.05em;
  line-height: 1.3;
  color: var(--text);
}

/* Visually hidden but still in the document — keeps the heading for screen
   readers and the page's outline/SEO without occupying layout space. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── WORKS ── */
/* No visible heading here, so the grid should arrive right behind the hero
   instead of after another full section's worth of top padding. */
#works {
  padding-top: 40px;
}

.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 48px;
  gap: 20px;
  flex-wrap: wrap;
}

.filter-bar {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
/* 290826P — was a boxed pill (1px border + tinted fill on .active): swapped
   for a plain-text + bottom-underline treatment. border-bottom (not
   text-decoration/underline) is what actually moves for hover vs. active
   here — a real underline sits flush under the text's own descenders,
   but border-bottom sits at the padding edge below them, reading as a
   clean short indicator bar instead, and its color/transition works the
   same way active/hover color already did above it. transparent (not
   none) on the base border-bottom keeps its own 1.5px of box-model space
   reserved at all times, so text doesn't shift up by 1.5px the instant a
   category becomes active/hovered. */
.filter-btn {
  font-family: var(--sans);
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  background: transparent;
  border: none;
  border-bottom: 1.5px solid transparent;
  padding: 8px 17px 6px;
  opacity: 0.6;
  text-decoration: none; /* category items are now real <a> links, not just <button> */
  transition: color 0.25s var(--ease), opacity 0.25s var(--ease), border-color 0.25s var(--ease);
}
.filter-btn:hover { color: var(--text); opacity: 1; border-bottom-color: var(--border-mid); }
.filter-btn.active {
  color: var(--text);
  opacity: 1;
  border-bottom-color: var(--text);
}

/* Horizontal filmstrip */
.gallery-wrap {
  position: relative;
  margin: 0 -52px; /* bleed to edges */
}

.gallery-track {
  display: flex;
  gap: 3px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 0 52px;
  cursor: grab;
}
.gallery-track::-webkit-scrollbar { display: none; }
.gallery-track.dragging { cursor: grabbing; scroll-snap-type: none; }

.gallery-loading {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 100px 0;
  flex-shrink: 0;
}
.dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--muted);
  animation: dotPulse 1.3s ease-in-out infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes dotPulse {
  0%,100% { opacity: 0.25; transform: scale(0.7); }
  50%      { opacity: 1;    transform: scale(1); }
}

.gallery-item {
  flex: 0 0 auto;
  width: 36vw;
  height: 64vh;
  min-height: 320px;
  scroll-snap-align: start;
  overflow: hidden;
  position: relative;
  background: var(--bg-2);
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}
.gallery-item.visible { opacity: 1; transform: scale(1); }

/* Once its photo has loaded, a filmstrip card takes the photo's own
   proportions (--ar, set in renderGallery) at the strip's height, capped
   to the viewport width — so panoramas and tall canvases show whole,
   uncropped, with no letterbox bands. Until then it keeps the default
   36vw × 64vh frame above. Archive's editorial cards size themselves
   separately (.archive-editorial rules below). */
.gallery-wrap:not(.archive-editorial) .gallery-item.sized {
  --strip-h: 64vh;
  width: min(calc(var(--strip-h) * var(--ar)), 90vw);
  height: auto;
  min-height: 0;
  aspect-ratio: var(--ar);
}
.gallery-wrap:not(.archive-editorial) .gallery-track { align-items: center; }

.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: contain; /* never crop the artwork (was cover) */
  display: block;
  transition: transform 0.8s var(--ease), filter 0.5s;
  filter: saturate(0.92);
  pointer-events: none;
}
.gallery-item:hover img {
  transform: scale(1.05);
  filter: saturate(1);
}

/* Sits over artwork, so it stays a dark scrim regardless of the page palette */
.gallery-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(10, 10, 10, 0.88) 0%, rgba(10, 10, 10, 0) 52%);
  opacity: 0;
  transition: opacity 0.5s var(--ease);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 26px 24px;
  pointer-events: none;
}
.gallery-item:hover .gallery-overlay { opacity: 1; }

.gallery-item-title {
  font-family: var(--sans);
  font-size: 1.15rem;
  font-weight: 500;
  color: var(--on-img);
  transform: translateY(8px);
  transition: transform 0.4s var(--ease);
}
.gallery-item-meta {
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-img-muted);
  margin-top: 5px;
  transform: translateY(8px);
  transition: transform 0.4s var(--ease) 0.04s;
}
.gallery-item:hover .gallery-item-title,
.gallery-item:hover .gallery-item-meta { transform: translateY(0); }

/* Hard override for the "Studio" mobile-only image (250826D) — redundant
   with the JS-level array filter in renderGallery() (which already keeps
   this item out of the DOM entirely on desktop), kept as an independent,
   CSS-only guarantee: even if some future code path renders this item
   without applying that filter, it still never shows above the mobile
   breakpoint. 769px is deliberately the mirror of the site's one mobile
   breakpoint (max-width: 768px, see isMobileViewport() in app.js) so the
   two never overlap or leave a gap. Targets both the specific id (in case
   the class is ever missing) and the general-purpose class, so adding
   another mobile-only image later only means adding its id to
   DESKTOP_EXCLUDED_IDS in app.js — no new CSS rule required. */
@media (min-width: 769px) {
  [data-drive-id="1ncdGnxSA_oJLF3Xs7Mb-QzE6BUSIJnqw"],
  .mobile-only-item {
    display: none !important;
  }
}

/* Mirror of the rule above, for a desktop-only image (290826A) — row 26 on
   the live sheet, meant to show on desktop and stay hidden on mobile.
   max-width: 768px is the exact complement of the min-width: 769px rule
   above, matching the site's one mobile breakpoint from the other side.
   Two data-drive-id selectors are listed: the id as it's actually stored
   on the live sheet, and the id as given in the request that added this
   rule — they differ by two characters (see 290826A in history.txt);
   both are harmless to list even if only one ever matches a real item. */
@media (max-width: 768px) {
  [data-drive-id="1PuPPr_bLWMbR_F3N6LSbL6XO2_Ru-F0r"],
  [data-drive-id="1PuPPr_bLWMoR_F3N6LStL6XO2_Ru-F0r"],
  .desktop-only-item {
    display: none !important;
  }
}

/* Demo placeholder cards */
.gallery-item-placeholder {
  width: 100%;
  height: 100%;
  background: var(--bg-2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.gallery-item-placeholder p {
  font-family: var(--sans);
  font-size: 1.1rem;
  color: var(--muted);
  text-align: center;
  padding: 24px;
}

/* Progress bar */
.gallery-progress {
  height: 1px;
  background: var(--border);
  margin: 22px 52px 0;
  position: relative;
  overflow: hidden;
}
.gallery-bar {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0%;
  background: var(--text);
  transition: width 0.15s var(--ease);
}

/* ── ARCHIVE EDITORIAL FEED (290826O grid → 290826X 2-col → 290826ZQ) ──
   The one category page (/work/archive) laid out as a magazine-style feed
   instead of the horizontal-scroll filmstrip every other series page
   (Exaphorm/Portales/Horadaciones) and the homepage use. Scoped entirely
   under .archive-editorial, toggled by renderGallery()'s editorialMode
   param in app.js (named archive-grid before 290826ZQ), so nothing here can
   leak onto any other page.
   290826ZQ replaced 290826X's uniform 2-column grid: one column of cards
   staggered left / right / center with generous space between entries,
   each card = a native-ratio photo (290826ZR: with a red overlay on every
   second one, see ARCHIVE PHOTO OVERLAY below). Photos are never
   cropped (width:100% + height:auto + object-fit:contain, no fixed-height
   box) and card WIDTH varies by orientation instead — see the data-orient
   rules below. .gallery-item itself no longer inherits the filmstrip's
   overflow:hidden/fixed-height/--bg-2 box (a card has to grow to fit
   whatever photo it holds), and the hover-reveal .gallery-overlay isn't in
   the DOM here at all (see renderGallery()'s editorial branch). */
.gallery-wrap.archive-editorial {
  margin: 0; /* undo the filmstrip's edge-to-edge bleed (-52px) — the feed stays inside the section's own padding, not bled to the viewport edge */
}
.gallery-wrap.archive-editorial .gallery-track {
  display: flex;
  flex-direction: column;
  gap: clamp(5rem, 8vw, 8.5rem); /* the "generous white space around each entry" */
  overflow: visible;
  scroll-snap-type: none;
  padding: 0;
  cursor: default;
}
.gallery-wrap.archive-editorial .gallery-item {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  /* Landscape is the default width — it's what a card is until its photo
     loads and renderGallery() tags it with data-orient (portrait/square
     override this below). max-width keeps it inside the track on narrow
     screens where the 320px floor would otherwise overflow. */
  width: clamp(320px, 58%, 820px);
  max-width: 100%;
  height: auto;
  min-height: 0;
  overflow: visible;
  background: none;  /* the --bg-2 loading placeholder lives on .archive-figure instead, so it hugs just the photo, not photo + text */
  scroll-snap-align: none;
  /* observe() staggers reveals by item index (i*60ms) — right for a row of
     tiles appearing together, wrong for a one-column feed where item 10
     only enters view after scrolling and would sit invisible for another
     0.6s. !important is what lets this beat that inline style. */
  transition-delay: 0s !important;
}
.gallery-wrap.archive-editorial .gallery-item[data-orient="square"]   { width: clamp(300px, 46%, 640px); }
.gallery-wrap.archive-editorial .gallery-item[data-orient="portrait"] { width: clamp(300px, 38%, 540px); }

/* Stagger (desktop/tablet only): left, right, center, repeat — less
   mechanical than strict left/right alternation. On phones every card is
   simply full-width in one column (see the ≤768px block further down). */
@media (min-width: 769px) {
  .gallery-wrap.archive-editorial .gallery-item:nth-child(3n+1) { align-self: flex-start; }
  .gallery-wrap.archive-editorial .gallery-item:nth-child(3n+2) { align-self: flex-end; }
  .gallery-wrap.archive-editorial .gallery-item:nth-child(3n)   { align-self: center; }
}
/* Tablet: the same percentages would leave cards too narrow to read the
   text in, so every orientation gets a wider share of the track. */
@media (min-width: 769px) and (max-width: 1100px) {
  .gallery-wrap.archive-editorial .gallery-item                         { width: 74%; }
  .gallery-wrap.archive-editorial .gallery-item[data-orient="square"]   { width: 60%; }
  .gallery-wrap.archive-editorial .gallery-item[data-orient="portrait"] { width: 48%; }
}

/* The photo. aspect-ratio: auto 3/2 reserves a landscape-shaped box while
   the image is still loading (so lazy-loading works and the page doesn't
   collapse to nothing), then hands over to the file's own native ratio the
   moment it loads — "auto" is what makes that handover happen; a plain
   3/2 would have locked every photo to 3:2 and, with object-fit: contain,
   letterboxed the rest. */
.archive-figure {
  position: relative;
  overflow: hidden; /* the overlay is clipped to the photo's own edges (and can't distort it — it's a separate layer above the <img>) */
  background: var(--bg-2);
  cursor: pointer;
}
.gallery-wrap.archive-editorial .archive-figure img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: auto 3 / 2;
  object-fit: contain; /* never crop — overrides the filmstrip's object-fit: cover */
}
/* The filmstrip's hover zoom (scale 1.05) is dropped here: it would scale
   the photo out from under the overlay type and past the card's clipped
   edge. The saturate lift on hover is kept as the click affordance. */
.gallery-wrap.archive-editorial .gallery-item:hover img { transform: none; }

/* ── ARCHIVE PHOTO OVERLAY (290826ZR; reworked ZS; positioned ZT; restyled ZV) ──
   A small off-white typographic label on every second Archive photo
   (renderGallery() picks which ones; archiveOverlayHtml() builds the markup).
   Where it sits comes from a tag at the start of the Sheet's `description`
   cell — [center] (the default), [top-left], [top-right], [bottom-left],
   [bottom-right] — which the JS turns into data-pos (see
   parseArchiveDescription() in app.js); each of the five positions below is
   a self-contained rule using exactly the offsets requested (10% from the
   top/bottom edge, 8% from the left/right edge, center via 50% + translate).
   Percentages are of the photo itself, so the placement scales with it.
   History: ZQ's text block under the photo, its credit line and a corner tab
   were replaced by this in ZR. ZR spread each letter edge to edge in a
   full-width box pinned top 50%; ZS dropped that for a compact tracked label
   (at small sizes the layout, not letter-spacing, set the gaps and the word
   scattered); ZT replaced the full-width box with a shrink-to-fit one
   (width: max-content, capped by max-width) because a corner-pinned label
   can't be 100% wide — it would run off the photo. Still absolute, z-index 2,
   pointer-events none (clicks fall through to the photo, which opens the
   lightbox).
   ZV look — minimal and quiet: soft off-white (--archive-overlay-ink) at
   micro size, very open tracking and word spacing, 80% max width, 1.6 line
   height. Sizes are FIXED per breakpoint rather than scaling with each photo,
   so every overlay on a given screen is the same size whatever shape its
   photo is. NOTE the unit: this site's root is a fixed 19px, not 16, so the
   "0.65rem" in the ZV request is 12.35px, not the 10-11px it was paired
   with; 0.58rem (11.0px) and 0.55rem (10.5px, phones) are the values that
   actually land in the 10-11px range asked for.
   Text-shadow: the one place on the site that has one (290826V stripped them
   from the hero for a crisp unmasked look, and that stays). Off-white vanishes
   over any bright or mid-tone patch of a photo, and some Archive photos have
   them (measured on the real files: a stone-steps shot gives only ~3.3:1 at
   its top-left corner, worst-case pixels ~2.5:1, and a light wall ~2:1 at its
   centre). So a soft dark halo — wide, low alpha, no offset — sits behind the
   letters: it darkens the immediate surround rather than drawing an outline or
   a box, and over an already dark photo it's invisible. */
.archive-overlay {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  width: max-content;
  max-width: 80%;
  font-family: var(--sans);
  font-size: 0.58rem;      /* ~11px; steps down on phones below */
  font-weight: 700;
  letter-spacing: 0.25em;
  word-spacing: 0.4em;     /* extra air between words, on top of the tracking */
  text-transform: uppercase;
  line-height: 1.6;
  text-wrap: balance;      /* a long description wraps into even lines, not one long + one orphan */
  overflow-wrap: break-word;
  color: var(--archive-overlay-ink);
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.45), 0 0 2px rgba(0, 0, 0, 0.3);
}
/* Phones: a hair smaller (~10.5px). letter-spacing/word-spacing are in em, so
   the airiness scales with the size automatically. */
@media (max-width: 768px) { .archive-overlay { font-size: 0.55rem; } }

/* The five positions. letter-spacing adds its space AFTER every character,
   including each line's last, so two of them nudge by 0.25em to keep the
   visible edge exactly where it's meant to be: centered text gets 0.25em of
   left padding (otherwise it sits 0.125em left of true center), and the
   right-aligned ones get a -0.25em right margin (otherwise they'd sit 0.25em
   in from the 8% line). :not([data-pos]) makes center the fallback even if
   an overlay somehow lacks the attribute. */
.archive-overlay:not([data-pos]),
.archive-overlay[data-pos="center"] {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  padding-left: 0.25em;
}
.archive-overlay[data-pos="top-left"]     { top: 10%;    left: 8%;  text-align: left; }
.archive-overlay[data-pos="top-right"]    { top: 10%;    right: 8%; text-align: right; margin-right: -0.25em; }
.archive-overlay[data-pos="bottom-left"]  { bottom: 10%; left: 8%;  text-align: left; }
.archive-overlay[data-pos="bottom-right"] { bottom: 10%; right: 8%; text-align: right; margin-right: -0.25em; }

/* ── LIGHTBOX ── */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 800;
  background: rgba(250, 250, 250, 0.975);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s var(--ease);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.lightbox.open { opacity: 1; pointer-events: all; }

.lightbox-close {
  position: absolute;
  top: 22px; right: 28px;
  font-size: 1.8rem;
  font-weight: 300;
  color: var(--muted);
  background: none;
  border: none;
  line-height: 1;
  padding: 8px;
  transition: color 0.2s, transform 0.4s;
}
.lightbox-close:hover { color: var(--text); transform: rotate(90deg); }

.lightbox-prev, .lightbox-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 2.2rem;
  font-weight: 300;
  color: var(--muted);
  background: none;
  border: none;
  padding: 20px 16px;
  transition: color 0.2s;
  line-height: 1;
}
.lightbox-prev { left: 0; }
.lightbox-next { right: 0; }
.lightbox-prev:hover, .lightbox-next:hover { color: var(--text); }

.lightbox-content {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 56px;
  align-items: center;
  width: 90%;
  max-width: 1120px;
  max-height: 90vh;
  padding: 0 72px;
}
.lightbox-img-wrap {
  position: relative; /* anchors .lightbox-zoom to this box, not wherever the nearest positioned ancestor up the tree happens to be */
  display: flex;
  align-items: center;
  justify-content: center;
  max-height: 85vh;
}
/* The shadow separates a pale canvas from the pale backdrop */
.lightbox-img {
  max-height: 85vh;
  max-width: 100%;
  object-fit: contain;
  box-shadow: var(--shadow-lg);
  transition: opacity 0.25s;
}
.lightbox-img.fading { opacity: 0; }

/* Corner expand button (290826T) — Archive-only; hidden (via the
   [hidden] attribute, same mechanism as .lightbox-series/.lightbox-
   inquire elsewhere) for every other category. A circular glyph button
   rather than a text label, deliberately unobtrusive ("subtle" per the
   request) until hovered. */
.lightbox-zoom {
  position: absolute;
  top: 1rem;
  right: 1rem;
  z-index: 10;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text);
  background: rgba(250, 250, 250, 0.85);
  border: 1px solid var(--border);
  border-radius: 50%;
  opacity: 0.7;
  cursor: pointer;
  transition: opacity 0.2s, transform 0.2s var(--ease), background 0.2s;
}
.lightbox-zoom:hover { opacity: 1; transform: scale(1.08); background: var(--bg); }

/* 290826ZN — title/meta hierarchy tightened up. Sizes/weights below are
   the exact values requested; color: var(--text) is used in place of
   the request's literal #111111 — the two are close enough (a 9-unit
   difference out of 255) that it reads as the identical near-black
   either way, and using the token keeps this the same ink color as
   every other heading on the site. */
.lightbox-title {
  font-family: var(--sans);
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 8px;
  color: var(--text);
  line-height: 1.3;
}
/* Was a single uppercase, heavily-tracked line (year · medium ·
   dimensions run together) — restyled as plain, readable metadata text
   and split onto separate lines per field (see the join('\n') change in
   updateLightbox(), app.js) via white-space: pre-line, so a literal \n
   in the text actually breaks the line rather than collapsing like
   normal text-node whitespace. #333333 is a genuinely new value here,
   not a stand-in for an existing token — it's deliberately between
   --text (#1A1A1A, too dark/prominent for secondary metadata) and
   --muted (#666666, this site's existing "de-emphasized label" color,
   too light for what's meant to read as normal body-weight
   information here) — a third tier this site didn't already have a
   token for. */
.lightbox-meta {
  font-family: var(--sans);
  font-size: 0.875rem;
  font-weight: 400;
  color: #333333;
  line-height: 1.6;
  white-space: pre-line;
  margin-bottom: 20px;
}
.lightbox-desc { font-size: 0.8rem; color: var(--muted); line-height: 1.7; letter-spacing: 0.01em; }

/* ── ARCHIVE DETAIL VIEW (290826T) ──
   Scoped under .lightbox.archive-item (toggled per-work by
   updateLightbox() in app.js) so none of this touches Exaphorm/Portales/
   Horadaciones's already-tuned two-column detail view. Archive drops the
   1fr/300px image+sidebar grid for a single centered column — that grid
   was capping the image column's real width well below 1120px (the
   whole content box's own max-width) once the 300px info column, the
   72px×2 padding and the 56px gap were subtracted, which is the opposite
   of "prominent, large" for images that have no accompanying series/
   inquire content competing for that width any more (both already
   hidden for Archive, see updateLightbox()). */
.lightbox.archive-item .lightbox-content {
  grid-template-columns: 1fr;
  max-width: 1400px;
  justify-items: center;
}
/* 290826U fix — was width: 100%, which stretched this box to the full
   grid column regardless of the actual image's rendered width. .lightbox-
   zoom is positioned absolute against THIS element (see position:relative
   on the base .lightbox-img-wrap rule above), so for anything narrower
   than the column — any portrait-oriented photo, or a landscape one that
   simply isn't as wide as 85vw — the button ended up anchored to empty
   whitespace at the wrapper's edge instead of the image's own corner.
   width: fit-content shrinks this box down to exactly the image's actual
   rendered size (object-fit: contain plus max-height/max-width below
   already gives it a definite intrinsic size to fit to), so the button
   is always anchored to the real edge of the photo, landscape or
   portrait alike. justify-items: center on .lightbox-content above still
   centers this now-tighter box within the column, so the image itself
   stays centered exactly as before — only the button's anchor changes. */
.lightbox.archive-item .lightbox-img-wrap {
  width: fit-content;
  max-height: 80vh;
}
.lightbox.archive-item .lightbox-img {
  max-height: 80vh;
  max-width: 85vw;
  cursor: zoom-in;
}
/* 290826ZU — no .lightbox-info rules here any more: updateLightbox() hides
   that whole column for Archive (the photo opens with no text under it), so
   the centered 640px caption styling that lived here had nothing to style. */

/* Fullscreen inspection mode (290826T) — toggled by clicking the image
   or .lightbox-zoom (see toggleZoom() in app.js). Caption/metadata hide
   entirely rather than just shrinking, so the image is the only thing
   left on screen; .lightbox-close (a sibling of .lightbox-content, not
   a descendant of it) is untouched by any of this and stays reachable
   the whole time, so zooming never stands the visitor without a way out
   beyond clicking the image again. */
.lightbox.archive-item.zoomed .lightbox-content {
  max-width: 96vw;
  max-height: 96vh;
  padding: 0;
  gap: 0;
}
.lightbox.archive-item.zoomed .lightbox-img-wrap { max-height: 96vh; }
.lightbox.archive-item.zoomed .lightbox-img {
  max-height: 96vh;
  max-width: 96vw;
  cursor: zoom-out;
}
.lightbox.archive-item.zoomed .lightbox-info { display: none; }

/* ── ABOUT ── */
.section-about {
  padding-top: 150px;
  border-top: 1px solid var(--border);
}

/* .about-quote-wrap / .about-quote (290826N) — removed along with the
   #aboutStatement blockquote in index.html. Was placeholder copy the
   Sheet's own statement_en/es columns never actually fed (both blank in
   production), so there was no real content lost by deleting it. */

.about-layout {
  display: grid;
  grid-template-columns: 1fr 400px;
  gap: 84px;
  align-items: start;
}

.about-text .section-title { margin-bottom: 30px; }

/* margin-bottom removed (290826ZP, was 44px) — .about-bio is .about-
   text's last child (the "Get in touch" link that used to follow it was
   removed in 290826ZO), and this margin was one of two values adding up
   to the mobile bio-to-photo gap (see --stack-gap above). Zeroed here so
   .about-layout's own gap is the ONE value controlling that space,
   rather than two unrelated numbers summing to it by coincidence. Also
   harmless on desktop: .about-text and .about-img-wrap sit side by side
   there (align-items: start), so trailing margin on the shorter
   column's last child was never visible against the section's own
   bottom padding either way. */
.about-bio {
  font-size: 0.84rem;
  color: var(--muted);
  line-height: 1.7;
  letter-spacing: 0.01em;
}

/* .cta-link ("Get in touch", About section) removed along with its
   markup in index.html (290826ZO) — nothing else on the site used this
   class, so no scoped remnant was left behind. */

/* Inline arrow glyphs, drawn as vectors — plain unicode arrows (→ ↗) render as
   colored system emoji on iOS/Safari once they fall outside the loaded font's
   glyph coverage. currentColor keeps them tied to the surrounding text color. */
.icon-arrow {
  width: 0.9em;
  height: 0.9em;
  vertical-align: -0.125em; /* only applies in inline flow — flex parents ignore it */
  flex-shrink: 0;
}

.about-img-wrap {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 4;
  background: var(--bg-2);
}
.about-img-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.92);
  transition: filter 0.6s, transform 0.7s var(--ease);
}
.about-img-wrap:hover img { filter: saturate(1); transform: scale(1.03); }

/* ── ACADEMIC BACKGROUND ──
   Intentionally kept as an exact duplicate of the EXHIBITIONS rules below
   (same property values, in the same two responsive tiers) rather than
   sharing the .exhibition-* classes — the two sections are semantically
   different lists, but must always render identically. If you tune one,
   tune the other the same way. */
.education-list { margin-top: 60px; }

.education-item {
  display: grid;
  /* Date column was a fixed 84px (290826N: auto) — "2019–2022" at this
     font-size/letter-spacing, kept unbroken by white-space:nowrap below,
     can need more than that, and a fixed track doesn't grow to fit
     nowrap content — it either clips it or (pre-fix) let the browser
     wrap it, which broke right at the hyphen since hyphens/dashes are a
     default soft-wrap point. auto sizes the column to whatever the date
     actually needs. minmax(0,1fr) on the title column (was a bare 1fr)
     is the standard grid fix stopping a long unbroken title/venue string
     from overflowing past the section's own padding — a bare 1fr track
     still won't shrink below its content's min-content width. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 30px;
  align-items: baseline;
  padding: 22px 0;
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
.education-item:first-child { border-top: 1px solid var(--border); }
.education-item.visible { opacity: 1; transform: translateY(0); }

/* nowrap (290826N): see the grid-template-columns comment above — this is
   the other half of the fix. Without it a narrow auto column would just
   wrap the date text instead of growing to fit it. */
.education-year { font-size: 0.73rem; letter-spacing: 0.1em; color: var(--muted); font-weight: 400; white-space: nowrap; }
.education-title { font-family: var(--sans); font-size: 1.15rem; font-weight: 500; color: var(--text); overflow-wrap: break-word; }
.education-venue { font-size: 0.75rem; color: var(--muted); letter-spacing: 0.05em; text-align: right; overflow-wrap: break-word; }

/* ── EXHIBITIONS ── */
.exhibitions-list { margin-top: 60px; }

.exhibition-item {
  display: grid;
  /* Kept an exact duplicate of .education-item's fix above (see its
     comment) — same 290826N reasoning applies identically here. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 30px;
  align-items: baseline;
  padding: 22px 0;
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
.exhibition-item:first-child { border-top: 1px solid var(--border); }
.exhibition-item.visible { opacity: 1; transform: translateY(0); }

.exhibition-year { font-size: 0.73rem; letter-spacing: 0.1em; color: var(--muted); font-weight: 400; white-space: nowrap; }
.exhibition-title { font-family: var(--sans); font-size: 1.15rem; font-weight: 500; color: var(--text); overflow-wrap: break-word; }
.exhibition-venue { font-size: 0.75rem; color: var(--muted); letter-spacing: 0.05em; text-align: right; overflow-wrap: break-word; }

/* ── CONTACT ── */
.section-contact { border-top: 1px solid var(--border); }

.contact-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 84px;
  align-items: start;
  margin-top: 64px;
}

.contact-intro {
  font-family: var(--sans);
  font-size: clamp(1.05rem, 1.55vw, 1.4rem);
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--muted);
  line-height: 1.7;
  margin-bottom: 38px;
}

.contact-link {
  font-size: 0.73rem;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  display: inline-block;
  transition: opacity 0.2s;
}
.contact-link:hover { opacity: 0.5; }

.contact-form { display: flex; flex-direction: column; gap: 30px; }

.form-group { display: flex; flex-direction: column; gap: 9px; }

.form-label {
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}

.form-group input,
.form-group textarea {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border-mid);
  padding: 10px 0;
  color: var(--text);
  font-family: var(--sans);
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  outline: none;
  width: 100%;
  resize: none;
  -webkit-appearance: none;
  appearance: none;
  border-radius: 0;
  transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus { border-bottom-color: var(--text); }

.form-submit {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  background: none;
  border: 1px solid var(--border-mid);
  padding: 15px 30px;
  color: var(--text);
  font-family: var(--sans);
  font-size: 0.73rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  align-self: flex-start;
  transition: background 0.2s var(--ease), border-color 0.2s, color 0.2s;
}
.form-submit:hover:not(:disabled) { background: var(--text); border-color: var(--text); color: var(--bg-3); }
.form-submit:disabled { opacity: 0.45; }
.submit-arrow { transition: transform 0.35s var(--ease); }
.form-submit:hover:not(:disabled) .submit-arrow { transform: translateX(5px); }

.form-status { font-size: 0.72rem; min-height: 1.4em; line-height: 1.6; color: var(--muted); }
.form-status.success { color: var(--text); }
.form-status.error   { color: #A63232; }

/* ── FOOTER ── */
.footer {
  padding: 28px 52px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.footer-name { font-family: var(--sans); font-size: 1rem; font-weight: 500; letter-spacing: 0.05em; color: var(--muted); }
.footer-copy { font-size: 0.72rem; letter-spacing: 0.08em; color: var(--muted); }

.footer-meta {
  display: flex;
  align-items: center;
  gap: 18px;
}
.footer-social {
  display: inline-flex;
  color: var(--muted);
  transition: color 0.2s;
}
.footer-social:hover { color: var(--text); }
.footer-social svg { width: 18px; height: 18px; }

/* ── LANGUAGE SWITCH ── */
.lang-switch {
  display: flex;
  align-items: center;
  gap: 6px;
}
.lang-switch span {
  font-size: 0.68rem;
  color: var(--border-mid);
}
.lang-btn {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--sans);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color 0.2s;
}
.lang-btn:hover { color: var(--text); }
.lang-btn.active { color: var(--text); }

/* ── CATEGORY (dedicated series page) ──
   New, additive rules only — nothing existing above this point is touched. */

/* [hidden] can lose to an element that already sets its own `display`
   (.hero uses display:flex) since author rules always beat the UA
   stylesheet regardless of specificity — this guarantees hidden always wins. */
[hidden] { display: none !important; }

/* The fixed nav (--nav-h tall) sits on top of the page in normal flow, not
   pushing content down — #category can render at scrollY:0 (landing directly
   on a category page, or returning "Back" resets scroll to the top), so its
   own top padding must clear the nav's height itself or the nav's hitbox
   physically overlaps and swallows clicks on whatever sits right under it
   (the back link). Anything reached only by scrolling (#works, #about, …)
   doesn't need this: scroll-padding-top already keeps the nav clear of them. */
#category { padding-top: calc(var(--nav-h) + 24px); }

.category-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.73rem;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  margin-bottom: 40px;
  transition: opacity 0.2s;
}
.category-back:hover { opacity: 0.5; }
.category-back .icon-arrow { transform: scaleX(-1); } /* reuse the → icon, mirrored */

.category-title {
  max-width: 680px;
  margin: 0 auto 10px;
  text-align: center;
}

.category-year {
  max-width: 680px;
  margin: 0 auto 32px;
  font-size: 0.73rem;
  font-weight: 400;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
}

.category-statement {
  max-width: 680px;
  margin: 0 auto 48px;
  font-size: 0.84rem;
  line-height: 1.6;
  color: var(--muted);
  text-align: center;
  white-space: pre-line; /* preserves the blank-line paragraph breaks she typed in the Sheet */
}
.category-statement.has-signature { margin-bottom: 0; }

/* The trailing "—Name" line of a statement, split out to read as a proper
   signature rather than just the last line of the paragraph. */
.category-signature {
  max-width: 680px;
  margin: 1.5rem auto 48px;
  font-size: 0.84rem;
  color: var(--muted);
  text-align: center;
}

/* ── ARCHIVE INTRO (290826R) ──
   Archive's title+subtitle, left-aligned flush with the grid below
   instead of the centered narrow column every other series page
   (Exaphorm/Portales/Horadaciones) uses for its curatorial statement —
   toggled by renderCategoryPage() in app.js, scoped under #category.
   archive-intro so it can never affect those other pages, which keep
   .category-title/.category-statement's base centered rule untouched.
   No new padding is set here on purpose: #category is a plain .section,
   which already carries the site's one shared horizontal padding — the
   exact same padding .gallery-wrap.archive-editorial (called archive-grid
   before 290826ZQ) sits inside — so simply removing the centering below
   is what makes this line up with the feed's left edge at every
   viewport, desktop through mobile,
   without a separate iPad-width rule to keep in sync by hand. */
#category.archive-intro .category-title,
#category.archive-intro .category-year,
#category.archive-intro .category-statement {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  text-align: left;
}

/* Compact uppercase grotesk (290826R) — the same treatment already
   established for .hero-name/.nav-brand elsewhere on the site, reused
   here rather than inventing a second "bold uppercase title" style. */
#category.archive-intro .category-title {
  margin-top: 2rem;
  font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 0.95;
}

/* Curatorial subtitle — italicized rather than switched to an actual
   serif face: the site only ever loads Inter (see the Google Fonts link
   in index.html), so naming a serif family here would just fall back to
   whatever serif happens to be installed on each visitor's system,
   inconsistently. Italic + a lighter weight on the same loaded family
   reads as the intended "light-weight, curatorial" register without
   depending on a font nothing actually requests. opacity: 0.7 is added
   on top of the existing color: var(--muted) (already a dimmed gray) —
   together they're the "subtle" the request asked for, and margin-bottom
   gives the requested breathing room before the grid starts. */
#category.archive-intro .category-statement {
  font-family: var(--sans);
  font-weight: 300;
  font-style: italic;
  opacity: 0.7;
  margin-bottom: 3rem;
}

.lightbox-series {
  display: inline-block;
  margin-top: 20px;
  font-size: 0.73rem;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.2s;
}
.lightbox-series:hover { color: var(--text); }

/* 290826ZN — reverses 290826ZC's plain-menu-link treatment back to a
   solid CTA button, per an explicit new request: 290826ZC's own
   divider-line/no-box styling is gone entirely (border-top removed, not
   just recolored). #000000/white are literal, deliberate values here
   rather than this site's usual near-black --text/--bg-3 tokens — the
   request calls for a genuinely stark, opaque, "solid dark" button,
   distinct on purpose from the softer near-black used for regular text
   everywhere else; var(--bg-3) is used for the white fill only because
   it already equals #FFFFFF exactly, not as an approximation.
   font-size uses 0.75rem rather than the request's literal-px
   alternative (11px) specifically because every other font-size in this
   file is in rem, tracking the site's own fluid (non-16px) root
   font-size — a hardcoded px value here would be the one size in the
   whole stylesheet that doesn't scale with everything around it.
   Left-aligned (not centered) and sized to its own text (width:
   fit-content, not the previous full-width span) — still reads as a
   real button, but stays anchored to the same left edge as
   .lightbox-title/.lightbox-meta/.lightbox-series above it rather than
   breaking that shared alignment. */
.lightbox-inquire {
  display: block;
  width: fit-content;
  margin-top: 2rem;
  padding: 12px 24px;
  background-color: #000000;
  border: none;
  border-radius: 0;
  font-family: var(--sans);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: center;
  color: var(--bg-3);
  opacity: 1;
  text-decoration: none;
  transition: background-color 0.25s var(--ease);
}
.lightbox-inquire:hover { background-color: #1a1a1a; }

/* ── SCROLL REVEAL ── */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.reveal.visible { opacity: 1; transform: translateY(0); }

/* ── RESPONSIVE ── */
@media (max-width: 1100px) {
  .gallery-item { width: 50vw; }

  /* Tablet/iPad target for the compact bold treatment (290826G): ~2rem
     to 2.4rem, between the desktop clamp above and the tighter mobile
     one further down (max-width: 768px) — this tier (769-1100px) is
     otherwise reserved for layout reflow (grid columns collapsing to
     one), not typography, but the hero name needed its own explicit
     step here since it's now a small, precisely-sized mark rather than
     a fluid display headline that could just be left to clamp() alone. */
  .hero-name { font-size: clamp(2rem, 2.6vw, 2.4rem); }

  .lightbox-content {
    grid-template-columns: 1fr;
    padding: 56px 32px 32px;
    gap: 26px;
    overflow-y: auto;
  }
  .lightbox-img-wrap { max-height: 52vh; }

  /* Archive's image gets the same 80vh as desktop. It was held at 65vh
     (290826T) to leave room under it for the caption; since 290826ZU an
     Archive lightbox has no caption, so that room has nothing to hold.
     290826ZU also fixes a width bug that was in this view before: the old
     max-width (90vw) ignored the content box's own side padding (32px
     tablet / 20px phone) and the single grid column's `1fr` can't shrink
     below its content, so every Archive photo was ~40px wider than the
     space for it — shoved right, its edge cut off, with a stray horizontal
     scrollbar on phones. minmax(0, 1fr) lets the column be exactly the
     content box, and max-width: 100% (the wrap, then the photo) makes the
     photo fit it. :not(.zoomed) keeps all of this off the fullscreen
     inspection mode below, which is deliberately wider and unchanged. */
  .lightbox.archive-item:not(.zoomed) .lightbox-content { grid-template-columns: minmax(0, 1fr); }
  .lightbox.archive-item:not(.zoomed) .lightbox-img-wrap { max-width: 100%; }
  .lightbox.archive-item:not(.zoomed) .lightbox-img-wrap,
  .lightbox.archive-item:not(.zoomed) .lightbox-img { max-height: 80vh; }
  .lightbox.archive-item:not(.zoomed) .lightbox-img { max-width: 100%; }
  .lightbox.archive-item.zoomed .lightbox-img-wrap,
  .lightbox.archive-item.zoomed .lightbox-img { max-height: 92vh; }
  .lightbox.archive-item.zoomed .lightbox-img { max-width: 94vw; }

  /* gap was 54px (290826ZP) — .about-layout collapses to one column at
     this breakpoint, so its gap becomes the actual vertical space
     between the bio text and the photo below it (they're stacked, not
     side-by-side, here). Now driven by --stack-gap so it matches the
     page's other section-to-section gaps exactly instead of its own
     unrelated value. */
  .about-layout {
    grid-template-columns: 1fr;
    gap: var(--stack-gap);
  }
  .about-img-wrap { aspect-ratio: 4/3; max-width: 520px; }

  .contact-layout {
    grid-template-columns: 1fr;
    gap: 54px;
  }

  /* Fixed 68px date column -> auto (290826N), same overflow/wrap fix as
     the desktop rule above, just re-stated for this tier's 2-column
     layout (year+title on row 1, venue spanning below on row 2). */
  .exhibition-item {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 16px;
  }
  .exhibition-venue { grid-column: 2; text-align: left; margin-top: -6px; }

  .education-item {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 16px;
  }
  .education-venue { grid-column: 2; text-align: left; margin-top: -6px; }
}

@media (max-width: 768px) {
  :root { --nav-h: 64px; }

  /* Comfortable-reading tightening for mobile only — desktop's airier 1.65
     stays as-is; explicit line-height overrides below (lightbox-desc,
     about-bio, category-statement) still take precedence over this base. */
  body { cursor: auto; line-height: 1.5; }
  button { cursor: pointer; }
  a { cursor: pointer; }
  .cursor { display: none; }

  #nav { padding: 0 24px; }
  .nav-toggle { display: flex; }

  /* Sized up slightly from the 1rem shared with desktop (290826H) —
     uppercase + bold + tight tracking reads visually smaller/denser than
     the mixed-case medium weight this replaced at the same nominal size,
     so this keeps it legible without looking oversized next to the
     hamburger icon in the compact mobile nav bar. */
  .nav-brand { font-size: 1.2rem; }

  /* Always visible on mobile now (290826J), not just once #nav.scrolled
     is added — the base rule's opacity:0/pointer-events:none exists so
     the brand mark doesn't duplicate the hero title while it's still on
     screen (both said "Mireille Chávez" at once, right below each
     other, before the user had scrolled at all). On mobile the hero
     title is hidden entirely now and the header sits in its own
     non-overlapping strip above a plain background — neither reason for
     hiding it applies any more, and it's now the one persistent place
     mobile visitors see the artist's name at all. No text-shadow or
     extra contrast treatment needed here despite that: this element
     sits over #nav's own background (transparent pre-scroll, over the
     plain page --bg color thanks to the margin-top fix above, not over
     the photo), never over the artwork itself, so a plain dark mark on
     a plain light ground is already fully legible without help. */
  .nav-brand { opacity: 1; pointer-events: auto; }

  /* 290826M — fix: full-screen menu flashed transparent (page content
     showing through "WORKS"/"ABOUT"/"EXHIBITIONS") for a frame or two
     when opened while #nav.scrolled was active.
     Root cause: this was never a rule that made the panel transparent —
     background: var(--bg) above was already a fully opaque, unconditional
     color, and nothing in app.js's open/close handlers (navToggle's click
     listener) reads or depends on the .scrolled class at all, so the
     JS-dependency theory doesn't hold up either; checked both directly
     rather than assuming. What's actually happening is a GPU-compositing
     paint race: this element lives fully off-screen (translateX(100%))
     until .open animates it in via `transform`, which the browser can
     promote to its own compositor layer lazily, right as the animation
     starts. At the same moment, while scrolled, the PARENT #nav is
     already mid-transition on its own background/backdrop-filter (the
     blur(20px) in #nav.scrolled below) — asking for a brand-new layer's
     first paint at the same instant a neighboring expensive filter is
     compositing is what mobile WebKit drops a frame on, and the dropped
     frame is exactly a not-yet-rasterized (so effectively see-through)
     panel over whatever's behind it, which self-corrects within a frame
     or two ("snaps to solid"). Fix: force this layer to exist and be
     fully painted (background included) *before* transform ever
     animates, via will-change + backface-visibility, so there's no
     first-paint race left to lose — plus the hardening the request
     asked for regardless of root cause: opaque background pinned with
     !important (not just inherited from a var), explicit longhand
     top/left/width/height alongside inset (belt-and-suspenders, not a
     behavior change since inset:0 already meant the same thing), and a
     z-index high enough (9999) that no future element on the page could
     ever end up stacked above it by accident. .nav-toggle's own z-index
     is bumped to match immediately below, so the close “X” stays
     clickable above the now-higher panel. */
  .nav-links {
    position: fixed !important;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    inset: 0;
    background-color: var(--bg) !important;
    will-change: transform;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    list-style: none;
    gap: 32px;
    transform: translateX(100%);
    transition: transform 0.45s var(--ease);
    z-index: 9999 !important;
  }
  .nav-links.open { transform: translateX(0); }
  .nav-links a { font-size: 0.85rem; color: var(--text); }

  /* A full 100svh box is a very tall, narrow aspect ratio — cover-fitting a
     landscape photo into it forces a large scale-up before the crop even
     happens, which is what reads as soft/pixelated on phones. Shortening
     the box to something closer to a landscape photo's own proportions
     needs a much smaller scale factor, so the same source image renders
     sharper. min-height must be overridden too, or 620px (set for desktop)
     would silently win over 60vh on most phones and this would be a no-op. */
  .hero { height: 60vh; min-height: 400px; }

  /* Non-overlapping header on mobile (290826J) — #nav is position:fixed
     and previously floated transparently over the top of this section
     (the site's usual full-bleed-under-a-floating-nav pattern, kept as-is
     on desktop). margin-top (not padding-top) is what's needed here
     specifically: .hero-bg's background-image layers are positioned via
     inset:0 relative to .hero's own padding box, so adding padding-top
     would shrink/reposition the photo inward rather than just moving the
     whole section down — margin sits outside the box and doesn't touch
     that internal positioning at all. var(--nav-h) is the mobile value
     (64px, redeclared in this same media query) so the gap always
     matches the header's real height exactly, with nothing left to keep
     in sync by hand. Net effect: the page's plain background shows for
     exactly the header's height, then the hero's own already-full 60vh
     begins right below it — the photo's own presented size doesn't
     shrink to make room, the page just gets that much taller up top. */
  .hero { margin-top: var(--nav-h); }

  /* Rendering hints for the crossfading background-image layers on mobile —
     aspect ratio itself was already handled correctly (background:center/
     cover above never stretches or crushes pixels, it only crops overflow),
     so this is purely about how the browser resamples/composites what's
     already a correctly-fitted image. image-rendering nudges WebKit toward
     a sharper resampling filter than its smooth default; translateZ(0) +
     backface-visibility:hidden forces this layer onto its own GPU
     compositor layer, which avoids the subtle extra blur that can come from
     compositing a plain (non-layered) background-image under the parallax
     transform already applied to its .hero-bg parent. */
  .hero-img {
    image-rendering: -webkit-optimize-contrast;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
    transform: translateZ(0);
  }

  /* Bottom gap consolidated to this one deliberate value (290826I) —
     right padding (24px) is unchanged and already matches the site's
     one mobile horizontal padding convention (.section, #nav, etc. all
     use the same 24px), so the name's right edge already lines up with
     everything else on the page; changing it to the 1.5rem suggested
     elsewhere would have made it the one element out of alignment.
     Previously the true gap to the viewport's bottom edge was this 28px
     PLUS .hero-name's own 22px trailing margin-bottom (a leftover from
     when hero-body had a sibling element below the name that no longer
     exists — see 290826G) — 50px total, split across two rules for
     reasons that no longer applied. .hero-name's margin-bottom is zeroed
     out on mobile below, so this is now the single source of the gap. */
  .hero-body { padding: 0 24px 2rem; }

  /* Was a genuine 22px trailing margin before the sibling element it was
     separating from got removed (290826G) — zeroed here so .hero-body's
     own padding-bottom above is the only thing controlling how close the
     name sits to the frame's bottom edge on mobile. Desktop's spacing
     (still separating .hero-name from nothing, but harmless there) was
     left as-is since it wasn't part of this request. */
  .hero-name { margin-bottom: 0; }

  /* Mobile target for the new compact bold treatment (290826G): ~1.5rem
     to 1.8rem, replacing 290826B's much larger light-weight scale — the
     whole point this time is a small, dense mark, not a big display
     headline, so this single clamp now covers the full mobile range on
     its own; the old separate <=480px override below is gone. */
  .hero-name { font-size: clamp(1.5rem, 4vw, 1.8rem); }

  /* Right-aligned on mobile only (290826H) — desktop keeps flex-start.
     .hero-name is a flex row (kept from 290826G so the two-span reveal
     animation still works), so justify-content is what actually moves
     the words horizontally here — text-align has no effect on flex-item
     placement, and align-items controls the cross (vertical) axis, not
     the horizontal one, so neither of those two alternatives offered
     for this would have visibly done anything on this element. */
  .hero-name { justify-content: flex-end; }

  /* This mobile-only override (290826J) is gone as of 290826V — the
     base .hero-bg::after rule above now removes the gradient
     unconditionally (content: none) for every viewport, not just this
     one, so a separate mobile-specific "and also none here" rule would
     just be redundant with its own parent. */

  /* This mobile-only shadow (290826G/H era — was .hero-tagline/.hero-
     name/.hero-location, then just .hero-name after the other two were
     removed) is gone as of 290826V, which asked for hero title text to
     carry no shadow at all, on any viewport — see the unconditional
     text-shadow: none !important on .hero-name below, which now covers
     this breakpoint too rather than needing its own rule. */

  /* Hidden entirely on mobile (290826J) — the persistent nav bar (see
     .nav-brand below) now carries the "Mireille Chávez" mark on mobile
     instead, so the hero shows nothing but the photo, full-bleed.
     Deliberately additive rather than deleting the mobile-specific rules
     above (margin/size/alignment/shadow, from 290826G-I) — display:none
     makes all of them inert without discarding that tuning, so a future
     request to bring the title back on mobile doesn't mean redoing it
     from scratch. Desktop is untouched; this is the one rule scoped to
     just this breakpoint that actually removes the title. */
  .hero-name { display: none !important; }

  /* Section-level headings (About/Exhibitions/Contact/Category all share this
     class) — knocked down ~18% from the desktop min, matching every other
     heading reduction in this block. Capped at the desktop clamp's own min
     (1.9rem) so there's no size jump crossing the 768px boundary. */
  .section-title { font-size: clamp(1.55rem, 5.5vw, 1.9rem); }

  .section { padding: 64px 24px; }
  #works { padding-top: 28px; }
  #category { padding-top: calc(var(--nav-h) + 20px); }
  .section-about { padding-top: 72px; }

  .section-header {
    flex-direction: column;
    align-items: stretch; /* let .filter-bar's width be governed by the parent, not its content */
    gap: 16px;
    margin-bottom: 32px;
  }

  /* Single scrollable row instead of wrapping to 2-3 lines — matches the
     horizontal drag-to-scroll pattern the gallery itself already uses.
     width/min-width/max-width stop this flex item from growing to fit its
     own unwrapped content (the default flex behavior) — without them the
     row escapes its own overflow-x:auto and leaks width onto its ancestors,
     which is what "isolate the scroll" and "prevent squeezing" are about. */
  .filter-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain; /* don't chain the scroll gesture to the page */
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    gap: 16px;
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }
  .filter-bar::-webkit-scrollbar { display: none; }
  .filter-btn {
    font-size: 13px; /* was 12px — under the comfortable-reading floor for a tab label */
    letter-spacing: 0.08em;
    white-space: nowrap;
    flex-shrink: 0; /* keep full label width so the row scrolls instead of squeezing */
    padding: 8px 2px;
  }

  .gallery-wrap { margin: 0 -24px; }
  .gallery-track { padding: 0 24px; }
  .gallery-item { width: 80vw; height: 56vh; }
  .gallery-wrap:not(.archive-editorial) .gallery-item.sized { --strip-h: 56vh; }
  .gallery-item-title { font-size: 0.95rem; }
  .gallery-progress { margin: 16px 24px 0; }

  /* Archive editorial feed (290826ZQ) — one full-width column on phones,
     no stagger (the left/right/center offsets live in a min-width: 769px
     block above). This replaces 290826O's forced 2-column mobile grid:
     two ~150px columns can't carry the overlay type at a readable size, and the
     feed's whole point is giving each photo room to breathe. Portrait
     photos sit at 86% (left-aligned, matching every other card's left
     edge) instead of full width so a 2:3 photo isn't 1.5 screens tall. */
  .gallery-wrap.archive-editorial .gallery-track { gap: 4.5rem; }
  .gallery-wrap.archive-editorial .gallery-item,
  .gallery-wrap.archive-editorial .gallery-item[data-orient="square"] { width: 100%; }
  .gallery-wrap.archive-editorial .gallery-item[data-orient="portrait"] { width: 86%; }

  .about-bio { font-size: 0.8rem; line-height: 1.5; }

  .exhibitions-list { margin-top: 40px; }
  .exhibition-title { font-size: 0.95rem; }

  .education-list { margin-top: 40px; }
  .education-title { font-size: 0.95rem; }

  .category-year { margin-bottom: 24px; }
  .category-statement { font-size: 0.8rem; line-height: 1.5; margin-bottom: 32px; }
  .category-signature { font-size: 0.8rem; margin: 1.25rem auto 32px; }

  .contact-layout { margin-top: 44px; }
  .contact-intro { font-size: clamp(0.95rem, 4vw, 1.15rem); }
  .contact-form { gap: 22px; }

  .lightbox-content { padding: 40px 20px 24px; gap: 18px; }
  .lightbox-title { font-size: 1.3rem; margin-bottom: 8px; }
  .lightbox-meta { margin-bottom: 14px; }
  .lightbox-desc { line-height: 1.5; }

  .footer { padding: 24px 24px; flex-direction: column; gap: 8px; text-align: center; }

  .lightbox-prev { left: 4px; }
  .lightbox-next { right: 4px; }
}

@media (max-width: 480px) {
  .gallery-item { width: 88vw; }
}

/* ── MOTION PREFERENCE ── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .hero-name-l, .hero-name-r { transform: none; opacity: 1; }
  .reveal, .gallery-item, .exhibition-item { opacity: 1; transform: none; }
}
