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

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

.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: 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 GRID (290826O, reworked 290826X) ──
   The one category page (/work/archive) styled as a grid instead of the
   horizontal-scroll filmstrip every other series page (Exaphorm/
   Portales/Horadaciones) and the homepage use. Scoped entirely under
   this one class, toggled by renderGallery()'s gridMode param in app.js,
   so nothing here can leak onto any other page.
   290826X moved this from 290826O's uniform-square/hover-caption
   treatment to a kurimanzutto-style layout: native aspect ratio (no
   cropping) and a static caption in normal flow below each photo,
   instead of a dark scrim revealed on hover. .gallery-item no longer
   inherits the filmstrip's overflow:hidden/fixed-height box (it must
   grow to fit an image of ANY ratio plus a caption stacked below it,
   not clip to a fixed box) or .gallery-overlay's hover behavior — that
   overlay element isn't even in the DOM for gridMode any more (see
   renderGallery()'s captionHtml branch); .gallery-caption below is a
   distinct, separate element for archive specifically. */
.gallery-wrap.archive-grid {
  margin: 0; /* undo the filmstrip's edge-to-edge bleed (-52px) — the grid stays inside the section's own padding, not bled to the viewport edge */
}
.gallery-wrap.archive-grid .gallery-track {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  /* align-items: start (not the grid default, stretch) — native-aspect-
     ratio photos next to each other are rarely the same height, and
     stretch would otherwise force every item in a row up to its
     tallest neighbor's height, leaving a visible empty gap (in
     .gallery-item's own --bg-2 background) below any shorter one. start
     lets each item size to its own image+caption content and simply
     stop there. */
  align-items: start;
  gap: 2rem;
  overflow: visible;
  scroll-snap-type: none;
  padding: 0;
  cursor: default;
}
.gallery-wrap.archive-grid .gallery-item {
  display: flex;
  flex-direction: column; /* image, then .gallery-caption stacked below it */
  width: auto;
  height: auto;
  min-height: 0;
  overflow: visible; /* was hidden (filmstrip default) — a caption in normal flow below the image must not be clipped by its own tile */
}
.gallery-wrap.archive-grid .gallery-item img {
  width: 100%;
  height: auto; /* native aspect ratio — no aspect-ratio/object-fit cropping, per "preserving its native aspect ratio" */
  display: block;
}

/* Static caption (290826X) — title/year·medium/description, left-
   aligned, always visible, in normal flow directly under the photo.
   Small/light/lowercase per the request; text-transform: lowercase is
   a display-only transform (the underlying title text itself is left
   as Mireille typed it in the Sheet — e.g. still capitalized correctly
   anywhere else it's used, like the lightbox), matching how .filter-btn/
   .nav-links elsewhere on this site already use text-transform for
   display casing rather than mutating the actual string. */
.gallery-caption {
  margin-top: 0.9rem;
  text-align: left;
}
.gallery-caption-title,
.gallery-caption-meta,
.gallery-caption-desc {
  font-size: 0.85rem;
  font-weight: 300;
  color: var(--muted);
  text-transform: lowercase;
  opacity: 0.8;
}
.gallery-caption-title { color: var(--text); font-weight: 400; }
.gallery-caption-meta  { margin-top: 0.2rem; letter-spacing: 0.02em; }
.gallery-caption-desc  { margin-top: 0.4rem; line-height: 1.5; }

/* Exactly 2 large columns at desktop (290826X) — kurimanzutto reference.
   Deliberately its own breakpoint (1024px) rather than reusing the
   site's existing 1100px/768px tiers: those exist for the rest of the
   layout's own reflow needs, and this request named 1024px specifically
   for when the Archive grid should switch from the denser auto-fill
   default above to exactly two large frames per row. Below 1024px
   (tablet) the auto-fill default still applies (naturally 2-3 columns
   depending on width); the existing ≤768px mobile tier further down
   still forces exactly 2 columns of its own, unchanged by this. */
@media (min-width: 1024px) {
  .gallery-wrap.archive-grid .gallery-track {
    grid-template-columns: repeat(2, 1fr);
    gap: 3rem 2.5rem;
  }
}

/* ── 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;
}
.lightbox.archive-item .lightbox-info {
  max-width: 640px;
  text-align: center;
}
.lightbox.archive-item .lightbox-meta { justify-content: center; }

/* 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-grid (290826O) sits inside —
   so simply removing the centering below is what makes this line up
   with the grid's 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 still gets a visibly bigger image than the 52vh default
     above at this tier — not the full 80vh desktop value, since tablet/
     mobile also needs room below the image for the (still-scrollable,
     see .lightbox-content's overflow-y:auto above) caption without it
     being pushed fully off-screen. */
  .lightbox.archive-item .lightbox-img-wrap,
  .lightbox.archive-item .lightbox-img { max-height: 65vh; }
  .lightbox.archive-item .lightbox-img { max-width: 90vw; }
  .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-item-title { font-size: 0.95rem; }
  .gallery-progress { margin: 16px 24px 0; }

  /* Archive grid (290826O) — fixed at exactly 2 columns per the request,
     rather than left to auto-fill: at this viewport width, auto-fill's
     own 250px minimum would only ever fit a single column (375px minus
     gap doesn't clear 2×250px), so the desktop formula can't reach "2
     columns" on its own and needs this explicit override. */
  .gallery-wrap.archive-grid .gallery-track {
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem;
  }

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