/* Visually hidden but still readable by screen readers/search
   crawlers — every page's design is a full-bleed image carousel with
   no room for a literal on-screen heading, but each still needs a
   real (uncommented) <h1> for SEO/accessibility. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* GLOBAL */
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  font-family: "neue-haas-unica", sans-serif;
  font-weight: 800;
  font-style: normal;
  font-size: 0.8em;
  text-transform: uppercase;
  line-height: 1.1142857em;
  overflow-y: auto;   /* allow scrolling */
  display: block;     /* remove flex centering */
  /* This heavy weight otherwise renders visibly thicker/blockier on
     any element that ends up GPU-composited into its own layer (e.g.
     anything with mix-blend-mode, like the shop nav) than on plain
     normally-composited text — layer promotion can drop from
     subpixel/LCD antialiasing down to plain grayscale antialiasing.
     Forcing grayscale antialiasing everywhere, all the time, keeps
     every page's type rendering identical regardless of whether any
     given element happens to be composited. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

header {
  position: fixed;
  top: 1em;
  left: 0;
  width: 100%;
  z-index: 2000; /* stays above carousels + blur layers */
  pointer-events: auto;
}

/* DARYA DIAMOND */
.logo {
  margin-left: 1em;
  text-decoration: none;
  color: #000;
}

/* NAV */
.header-nav {
  display: flex;
  grid-template-columns: repeat(28, 1fr); /* 28 equal columns */
  gap: 0.15em;       /* optional, spacing between links */
  margin-left: 4em; /* match logo padding or site gutter */
}

.header-nav a {
  text-decoration: none;
  color: #000;
}

.header-nav a:hover {
  color: #7f7f7f;
}


  /* =========================
 INFO / ABOUT SECTION
========================= */
.info {
  display: grid;
  grid-template-columns: repeat(28, 1fr);
  column-gap: 24px;
  padding: 0px 0px 80px;
  justify-items: start;
  margin-top: 0em;
  margin-left: 1em;
  --info-em: 1em;
  /* Site-wide text is uppercase (html, body); the bio paragraphs and
     CV read in normal sentence/title case instead. text-transform
     inherits, so this alone covers .para/.cv-heading/.cv-year/
     .cv-text/the CV download link underneath without repeating it on
     each one. */
  text-transform: none;
}

.para {
  line-height: 1.1142857em;
  margin: 0 0 0.6em 0;
}

/* Each paragraph keeps its own staggered starting column (the
   offset left edges), but all three now end at column 15 out of the
   28-column grid instead of column 11 — widening the text block so
   it reaches roughly halfway (~50%) across the page instead of the
   ~35% it filled before. */
.para-1 { grid-column: 1 / span 14; margin-top: 0.5em; }
.para-2 { grid-column: 3 / span 12; }
.para-3 { grid-column: 2 / span 13; }

/* =========================
 CV LAYOUT
========================= */
.cv {
  /* Was span 16 — widened to span 14 (half of the 28-column grid,
     same treatment as .para-1/2/3) so the block reaches ~50% across
     the page. */
  grid-column: 1 / span 14;
  margin-top: 0;
}

.cv-block { margin-bottom: 1em; }
.cv-heading {
  margin-bottom: 0.4em;
  /* .info above resets the whole section to normal case; these
     section titles (Education, Selected Solo and Two-Person
     Exhibitions, etc.) stay uppercase, matching the nav bar. */
  text-transform: uppercase;
}

.cv-row {
  display: grid;
  /* Was 60px — the year column was fixed much wider than the actual
     4-digit text (~24px), which was the real source of the leftover
     gap even after the column-gap below was tightened. 30px keeps a
     shared, consistent left edge for every row's text column (all
     .cv-row grids share the same track width) while sitting close to
     the glyphs themselves. */
  grid-template-columns: 30px 1fr;
  /* Was 24px, then 8px — tightened further to a single space's
     worth of gap so the text sits right up against the year
     column. */
  column-gap: 6px;
  align-items: start;
}

.cv-year { text-align: left; }
/* Was max-width: 38em — that happened to clamp the text to almost
   exactly the old .cv width, so widening .cv above did nothing on
   its own. Removed so the text now actually fills the wider ~50%
   grid area instead of stopping short of it. */
.cv-text { max-width: none; }
.cv-text em { font-style: normal; }

.cv-text a {
  color: inherit;
  transition: color 0.3s ease;
}

.cv-text a:hover {
  color: #7f7f7f;
}

.cv-download {
  grid-column: 1 / span 16;
  margin-top: 1em;
  text-align: left;
}

/* INFO DROPDOWN
   Sits below the fixed header like the Work/Exhibitions/Press panels.
   Positioned absolutely so it never pushes page content, and the page
   underneath is blurred (not hidden) via body.info-open — see the
   "INFO DROPDOWN — BACKGROUND BLUR ONLY" rules further down. */
.info-link {
  position: relative;
}

/* On Section 2/6 of the roly-poly-v2-page exhibition layout,
   body.detail-active/.credits-active puts mix-blend-mode: exclusion
   on header itself (see those rules further down) so the nav reads
   against the full-bleed photo behind it. All four dropdown panels
   (#info-section, .work-list, .exhibitions-list, .press-list, the
   Contact email popup) live nested inside header in the markup, so
   they get swept into that same flattened blended unit too — which
   is the intended look here: panel text should react to/invert
   against the photo the same way the nav does, panel background
   stays see-through (the blurred photo shows through it, same as
   everywhere else — see "MOBILE INFO PANEL" further down).

   The one thing that needs setting explicitly is text color:
   exclusion of black (the default text color) against any backdrop
   is a no-op — result equals the backdrop's own color exactly — so
   plain black text just vanishes into whatever's behind it instead
   of visibly inverting. Starting from white, matching the nav's own
   .logo/.header-nav a treatment, is what makes the invert actually
   read as legible text rather than nothing. */
body.roly-poly-v2-page.detail-active #info-section,
body.roly-poly-v2-page.detail-active .work-list,
body.roly-poly-v2-page.detail-active .work-list a,
body.roly-poly-v2-page.detail-active .exhibitions-list,
body.roly-poly-v2-page.detail-active .exhibitions-list a,
body.roly-poly-v2-page.detail-active .exhibitions-list a:visited,
body.roly-poly-v2-page.detail-active .press-list,
body.roly-poly-v2-page.detail-active .press-list a,
body.roly-poly-v2-page.detail-active .press-list a:visited,
body.roly-poly-v2-page.detail-active .contact-nav .email-dropdown,
body.roly-poly-v2-page.credits-active #info-section,
body.roly-poly-v2-page.credits-active .work-list,
body.roly-poly-v2-page.credits-active .work-list a,
body.roly-poly-v2-page.credits-active .exhibitions-list,
body.roly-poly-v2-page.credits-active .exhibitions-list a,
body.roly-poly-v2-page.credits-active .exhibitions-list a:visited,
body.roly-poly-v2-page.credits-active .press-list,
body.roly-poly-v2-page.credits-active .press-list a,
body.roly-poly-v2-page.credits-active .press-list a:visited,
body.roly-poly-v2-page.credits-active .contact-nav .email-dropdown {
  color: #fff;
}

#info-section {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  width: 100vw;
  z-index: 50;
  background: transparent;
  overflow-y: auto;
  pointer-events: auto;
}

#info-section.show {
  display: grid;
}


/* WORK DROPDOWN */
.work-link {
  position: relative;
}

.work-list {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  flex-direction: column;
  white-space: nowrap;
  z-index: 10;
}

.work-list.show {
  display: flex;
}

.work-list a {
  text-decoration: none;
  color: #000;
}

.work-list a:hover {
  color: #7f7f7f;
}

/* Long titles (e.g. "Pretend You Are an American Cowgirl and You
   Love Me") overflow the viewport under nowrap on phone widths,
   causing horizontal page scroll — let them wrap instead. .work-list
   is excluded: long titles there are left to overflow off the right
   edge rather than wrap or shrink. */
@media (max-width: 600px) {
  .exhibitions-list,
  .press-list {
    white-space: normal;
    max-width: 90vw;
  }
}

#main-image-wrapper {
  display: none;
}

/* =========================
   INTRO SPLASH SMOOTH
   ========================= */
   #intro {
    position: fixed;
    inset: 0;
    background: #fff;
    z-index: 5;
    display: flex;
    justify-content: center;
    align-items: center;
    pointer-events: none;
  }
  
  #intro.hidden {
    opacity: 0;
    visibility: hidden;
    transition: opacity 1.2s ease, visibility 0s linear 1.2s; /* fade out instantly */
  }
  
  .intro-images {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  
  .intro-images img {
    position: absolute;
    max-width: 80vw;
    max-height: 70vh;
    opacity: 0;
    filter: grayscale(100%);
    transform: scale(0.95);
    transition: opacity 1.2s ease-in-out, transform 1.2s ease-in-out;
  }
  
  .intro-images img.visible {
    opacity: 0.15;
    transform: scale(1);
  }
  
  
  .intro-title {
    position: relative;
    font-size: 2.3em;
    letter-spacing: -0.01em;
    z-index: 2;
    /* keep fully visible from the start */
  }
    

/* CONTACT NAV */
.contact-nav {
  position: relative;
  display: inline-block;
}

.contact-nav .email-dropdown {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  color: #111;
  font-family: "neue-haas-unica", sans-serif;
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;
  white-space: nowrap;
  z-index: 10;
  opacity: 0;
  transform: translateY(-10px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.contact-nav:hover .email-dropdown,
.contact-nav.show .email-dropdown {
  display: block;
  opacity: 1;
  transform: translateY(0);
}

.contact-nav a,
.contact-nav .email-dropdown {
  font-family: "neue-haas-unica", sans-serif;
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;
  color: #111;
  text-decoration: none;
  transition: color 0.3s ease;
}

.contact-nav:hover a,
.contact-nav:hover .email-dropdown {
  color: #7f7f7f;
}

/* TEXT SELECTION */
::selection {
  background-color: black;
  color: white;
}

::-moz-selection {
  background-color: black;
  color: white;
}

/* =========================
   BOTTOM MINI NAV
   ========================= */
.bottom-mini-nav {
  position: fixed;
  bottom: 1em;
  right: 1em;
  display: flex;
  gap: 0.2em;
  align-items: center;
  
  z-index: 999;
  background: rgba(255, 255, 255, 0.05); /* optional */
  padding: 0.2em 0.5em;
}

.bottom-mini-nav a {
  color: #000;
  text-decoration: none;
  position: static; /* important: override any previous absolute */
}

.bottom-mini-nav a:hover {
  color: rgba(127, 127, 127, 0.7); /* fade to grey like rest of site */
}

/* FOOTER COPYRIGHT */
.copyright {
  position: fixed;      /* keep it visible always */
  bottom: 1em;
  left: 1em;
  z-index: 999;         /* above intro and other content */
  color: #000;
}

/* EXHIBITIONS DROPDOWN */
.exhibitions-link {
  position: relative;
}

.exhibitions-list {
  display: none;
  position: absolute;
  top: 100%; /* directly below link */
  left: 0;   /* aligns under the link */
  flex-direction: column;
  white-space: nowrap;
  z-index: 10;
  padding: 0;          /* remove any padding */
  margin: 0;           /* remove any margin */
}

.exhibitions-list.show {
  display: flex;
}

.exhibitions-list a,
.exhibitions-list a:visited {
  text-decoration: none;
  color: #000;
  padding: 0;                /* no gap */
  margin: 0;                 /* no gap */
  line-height: 1.1142857em;  /* same as rest of nav */
  transition: color 0.3s ease;
}

.exhibitions-list a:hover,
.exhibitions-list a:visited:hover {
  color: rgba(127, 127, 127, 0.7); /* match site hover style */
}

/* Dropdown toggle interaction */
.exhibitions-toggle {
  cursor: pointer;
}
/* =========================
   INDEX TABLE (LEFT, V-CENTRED)
   ========================= */

   .index-table {
    position: fixed;
    top: 50%;
    left: calc((100% / 28) * 1); /* keeps your grid logic */
    transform: translateY(-50%);
  
    display: flex;
    flex-direction: column;
    gap: 2.5em;
  
    max-width: calc(100vw - 4em);
    max-height: 80vh;
    overflow-y: auto;
  }
  
  /* rows */
  .index-row {
    display: flex;
    gap: 2em;
  }
  
  /* individual items */
  .index-item {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    width: 110px;
  }
  
  .index-item img {
    width: 100%;
    height: auto;
    display: block;
    opacity: 0.9;
    transition: opacity 0.3s ease;
  }
  
  .index-item:hover img {
    opacity: 0.5;
  }
  
  .index-number {
    margin-top: 0.3em;
    font-size: 0.75em;
    letter-spacing: 0.02em;
  }
  
  /* =========================
   INTRO SEQUENCE TIMING
   ========================= */

.intro-images img {
  opacity: 0;
  filter: grayscale(100%);
  transition: opacity 0.25s ease;
}

#intro.fade-out {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.8s ease, visibility 0.8s ease;
}


  /* =========================
   PRESS DROPDOWN
   ========================= */
.press-link {
  position: relative; /* align dropdown under link */
}

.press-list {
  display: none;       /* hidden by default */
  position: absolute;  /* dropdown floats below link */
  top: 100%;           /* directly below toggle */
  left: 0;
  flex-direction: column;
  white-space: nowrap;
  z-index: 10;
  padding: 0;
  margin: 0;
}

.press-list.show {
  display: flex;

}

.press-list a {
  text-decoration: none;
  color: #000;
}

.press-list a:visited {
  color: #000;
}

.press-list a:hover,
.press-list a:visited:hover {
  color: #7f7f7f;
}

.bottom-index {
  position: fixed;
  bottom: 1em;               /* same vertical offset as .bottom-mini-nav */
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;                /* effective value — a later duplicate rule was overriding 999 to 50 */

  font-family: "neue-haas-unica", sans-serif; /* match site text */
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;  /* match nav style */
  color: #000;                 /* match nav text color */
  line-height: 1.1142857em;    /* match rest of text */
  padding: 0.2em 0.5em;        /* same as .bottom-mini-nav for visual alignment */
}

.bottom-index a {
  font-family: "neue-haas-unica", sans-serif;
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;
  color: #000;
  text-decoration: none;
}

.bottom-index a:hover,
.bottom-index:hover {
  color: rgba(127, 127, 127, 0.7); /* same hover effect as bottom nav */
}

/* SHOP GRID */
main.shop {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 2em;
  padding: 6em 2em 2em;
  justify-items: center;
  align-items: start;
}

.shop-item img {
  max-width: 100%;
  height: auto;
  display: block;
  transition: opacity 0.3s ease, transform 0.3s ease;
  cursor: pointer;
}

.shop-item img:hover {
  opacity: 0.8;
  transform: scale(1.02);
}

/* Sold prints aren't clickable (lightbox disabled in JS) — default
   cursor, no hover movement/fade, and not selectable/draggable, so
   they read as flat, static content rather than something you can
   interact with. */
.shop-item.sold img,
.shop-item.sold img:hover {
  cursor: default;
  transform: none;
  opacity: 1;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  user-drag: none;
}

.shop-item figcaption {
  margin-top: 0.5em;
  text-align: center;
  font-family: "neue-haas-unica", sans-serif;
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;
  font-size: 0.8em;
  line-height: 1.2em;
}


 /* =========================
   EXHIBITION PAGE — CLEAN LAYOUT
========================= */

/* BODY & HTML */
html.exhibition-page,
body.exhibition-page {
  height: auto;
  margin: 0;
  padding: 0;
  overflow-y: auto;    /* allow scrolling */
  display: block;      /* normal document flow */
}

/* MAIN */
main.exhibition-article {
  display: block;
  width: 100%;
}

/* HERO SECTION - IMAGE CENTERED, TEXT BELOW */
.exhibition-hero {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: auto;
  padding: clamp(6vh, 10vh, 12vh) 0 clamp(1em, 3vh, 2em);
  
}

.exhibition-hero-carousel {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh; /* full viewport height */
  transform: translateY(1.9vh); /* subtle downward nudge */
  
}

.carousel-divider {
  display: block;      /* make sure it occupies space */
  height: 3vh;         /* adjust as needed */
  width: 100%;         /* full width */
}

.exhibition-hero img {
  max-width: 80vw;
  max-height: 70vh;
  object-fit: contain;
  display: block;
}

/* EXHIBITION TEXT */
.exhibition-text {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0.5em 1em 4em;  /* spacing around text */
  text-align: left;   
}

/* Primordial Workplace - Centered Image Viewer */
.exhibition-page .image-container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
}

.exhibition-page .image-container img {
  max-width: 80vw;           
  max-height: 70vh;          
  object-fit: contain;
  display: block;
  margin: 0 auto;            /* center inside container */
}

/* Reset body exhibition display */
body.exhibition-page {
  display: block;
  justify-content: initial;
  align-items: initial;
}

/* Hide main caption */
#main-caption {
  display: none;
}

/* Text blocks with alternating images */
.text-block {
  display: flex;
  flex-wrap: wrap;
  gap: 1em;
  margin-bottom: 3em;
  align-items: flex-start;
}

/* Image order */
.text-block.left img {
  order: -1;      /* image first */
}

.text-block.right img {
  order: 1;       /* image after text */
}

/* Paragraph sizing + alignment */
.text-block.left p,
.text-block.right p {
  flex: 1 1 45%;       /* shrink so it sits next to image */
  margin: 0.2em 0 0 0; /* preserve margin */
  font-size: 1em;
  line-height: 1.1142857em;
}

/* Align text based on side */
.text-block.left p {
  text-align: right;   /* aligns right of text block */
}


/* Keep previous styling for text */
.text-block p {
  flex: 1 1 400px;
  margin: 0.2em 0 0 0;  /* previous margin preserved */
  font-size: 1em;
  line-height: 1.1142857em;
}

.text-block img {
  flex: 1 1 300px;
  max-width: 50%;
  height: auto;
  display: block;
  object-fit: contain;
  margin-top: 0;
}

.text-block {
  gap: clamp(0.6em, 1.2vw, 1.2em);
  margin-bottom: clamp(2em, 6vh, 3em);
}

.exhibition-text.intro {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: 0 2rem; /* adjust to match site gutters */
}

.exhibition-text.intro p {
  font-size: 1.6em;
  line-height: 1.1;
}

.exhibition-text.intro {
  margin-bottom: 4rem;
}

.exhibition-text.intro p {
  font-weight: 800;
  letter-spacing: -0.01em;
}

.exhibition-text.intro {
  position: relative;
  top: -2vh;

  width: 100vw;
  margin-left: calc(50% - 50vw);

  max-width: none;        /* 🔥 important */
  padding: 1em 1em 0vh;  /* keep your gutters */
  box-sizing: border-box;
}


/* =========================
   EXHIBITION CONTEXT BLOCK
========================= */
.exhibition-context {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3em;
  max-width: 1400px;
  margin: -8em auto 0; /* negative top margin */
  padding: 1em 1em;
  align-items: center;
  
}

.context-image {
  display: flex;
  justify-content: center;
}

.context-image img {
  max-width: 600px;
  width: 100%;
}

.context-text {
  display: grid;
  grid-template-columns: repeat(28, 1fr); /* staggered CV-style layout */
  column-gap: 24px;
  align-items: start;
}

/* Staggered paragraphs - visually centred within left column */
.context-text .para-1 { grid-column: 4 / span 23; }
.context-text .para-2 { grid-column: 6 / span 23; }
.context-text .para-3 { grid-column: 5 / span 23; }


.context-text p {
  margin: 0;               /* no spacing between paragraphs */
  text-align: left;        /* justified left */
  max-width: 100%;         /* fill grid column */
  font-size: 1.2em;
}

section .context-text > .para {
  margin-bottom: 1em;
}

/* Paragraph 5 exception */
.context-text .para-5 {
  display: block;          /* forces block behavior */
  margin-top: 2em;         /* optional spacing above */
}

/* Exhibition dates caption */
.exhibition-dates {
  font-size: 2em;
  line-height: 1.1;
  text-transform: uppercase;
  margin-top: 3em;
  color: #000;

  display: flex;            /* use flex for horizontal centering */
  flex-direction: column;   /* stack lines vertically */
  justify-content: center;  /* vertical alignment within block if needed */
  align-items: center;      /* horizontal centering */

  text-align: center;       /* center-align the text lines */
  padding-bottom: 4em;
}

.exhibition-dates-link {
  color: inherit;         /* keep black text */

  cursor: pointer;        /* show pointer on hover */
}

.exhibition-dates-link:hover .exhibition-dates {
  color: rgba(127, 127, 127, 0.7);  /* match your hover style */
}

/* =========================
   exhibiton caption
========================= */

.exhibition-caption.two-col {
  display: grid;
  grid-template-columns: auto 50vw;
  column-gap: 1em;

  padding: 0 1em;
  margin: 0.5em 0 1em;

  font-size: 1.1em;
  line-height: 1.1;
}

/* =========================
   CONTEXT CAROUSEL
========================= */
.context-carousel {
  width: 100%;
  max-width: 700px;   /* matches previous image width */
  margin: 0 auto;     /* center horizontally */
}

.context-carousel .carousel-cell {
  width: 100%;        /* one image per slide */
  margin-right: 0;
  padding-left: 1em;
}

.context-carousel img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain; /* zoom/crop behavior */
}

.exhibition-closing-carousel {
  display: flex;
  justify-content: center;
  margin-bottom: 8em;      /* same spacing as before */
}

.exhibition-closing-carousel .carousel {
  width: 70vw;              /* matches previous closing image width */
  max-width: 70vw;
}

.exhibition-closing-carousel .carousel-cell {
  width: 100%;
  min-height: 70vh;         /* keeps same height as before */
  display: flex;
  justify-content: center;
  align-items: center;
}

.exhibition-closing-carousel img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;      /* preserves aspect ratio */
  display: block;
  margin: 0 auto;
}

/* =========================
   SCROLLABLE IMAGE SLIDER
========================= */

.exhibition-closing-gallery {
  overflow-x: auto;
  overflow-y: hidden;
  padding: 2rem 0;
  scroll-behavior: smooth;
}

.scroll-wrapper {
  display: flex;
  gap: 1rem;        /* adjust spacing here */
  padding-left: 1rem;
}

.scroll-image {
  flex: 0 0 auto;
}

.scroll-image img {
  height: 600px;   /* adjust height */
  display: block;
  object-fit: cover;
}

/* Fixed 600px images take up most of the viewport height on phones/
   tablets before the user discovers this strip scrolls horizontally
   — scale it down to a sane fraction of the screen instead. */
@media (max-width: 1024px) {
  .scroll-image img {
    height: 45vh;
  }
}

.exhibition-closing-gallery {
  overflow-x: auto;
  overflow-y: hidden;
  padding: 2rem 0;
  scroll-behavior: smooth;

  /* hide scrollbar */
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* IE / Edge */
}

.exhibition-closing-gallery::-webkit-scrollbar {
  display: none;                /* Chrome / Safari */
}

/* make gallery images clickable */
.scroll-image img {
  cursor: zoom-in;
}

/* lightbox overlay — BLUR version */
.image-lightbox {
  position: fixed;
  inset: 0;
  background: rgba(255, 255, 255, 0.2); /* very subtle wash */
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

/* image matches landing carousel sizing + subtle depth */
.image-lightbox img {
  max-width: 80vw;
  max-height: 70vh;
  object-fit: contain;
  cursor: zoom-out;

  /* subtle gallery-style shadow */
  box-shadow: 
    0 8px 24px rgba(0, 0, 0, 0.08),
    0 2px 6px rgba(0, 0, 0, 0.06);
}

/* =========================
   GLOBAL VIEWER SCROLL LOCK
========================= */

html.viewer-open,
body.viewer-open {
  height: 100%;
  overflow: hidden !important;
  overscroll-behavior: none;
}

body.viewer-open main,
body.viewer-open footer {
  opacity: 0;
}

/* Smooth transition */
main,
footer {
  transition: filter 0.35s ease, transform 0.35s ease;
  /* Establishes main's paint/compositing layer on first load so the
     fixed-position landing carousel actually renders immediately,
     instead of only appearing after body.info-open first applies a
     real filter to main (which was the only thing forcing a repaint). */
  filter: blur(0px);
}

/* =========================
   INFO DROPDOWN — BACKGROUND BLUR ONLY
========================= */

/* Blur content behind info when open */
body.info-open main,
body.info-open footer,
body.info-open .landing-carousel,
body.info-open .index-table {
  filter: blur(10px);
  opacity: 0.92;
  transition: filter 0.3s ease, opacity 0.3s ease;
}

body.info-open {
  overflow: hidden;
}

/* =========================
   WORK / EXHIBITIONS / PRESS DROPDOWNS —
   HIDE UNDERNEATH, NOT BLUR
   Unlike Info (blurred, above), these dropdowns fully hide whatever's
   underneath so the hover-preview image gets full, uncluttered focus.
   Whatever was there (carousel, shop grid) reappears once the
   dropdown closes, whether via its own link, the menu, or the page.
========================= */
body.gallery-open main,
body.gallery-open footer,
body.gallery-open .landing-carousel,
body.gallery-open .index-table {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

body.gallery-open {
  overflow: hidden;
}

/* Primordial Exhibition override: scrolls normally */
body.primordial-exhibition {
  position: relative !important; /* overrides fixed */
  top: auto;
  left: auto;
  right: auto;
  bottom: auto;
  margin: 3rem auto;       /* vertical spacing + horizontal centering */
  width: 80%;              /* optional, adjust as needed */
  max-width: 1200px;       /* optional */
  display: flex;
  justify-content: center; /* horizontal centering of images */
  align-items: center;     /* vertical centering relative to container */
  z-index: 1;
}

/* =========================
   SHOP — CENTERED + RESPONSIVE GRID
========================= */

.shop-page main.shop {
  min-height: 100vh;            /* vertical centering container */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  
}

.shop-page .shop-item,
.shop-page .shop-text {
  margin-left:0.5em;
  padding-left: 0;

}

.shop-page .shop-text {
  /* Site-wide text is uppercase (html, body); these left-hand
     descriptions read in normal sentence case instead. */
  text-transform: none;
}

/* GRID WRAPPER LOGIC */
.shop-page main.shop {
  display: grid;
  align-content: center;        /* vertical centering for grid */
  justify-content: center;
  gap: 0.5rem;
}

/* DEFAULT (MOBILE) — 2 COLUMNS */
.shop-page .shop-item {
  width: 100%;
}

/* 2-column mobile grid */
@media (max-width: 767px) {
  .shop-page main.shop {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* TABLET / SMALL DESKTOP — 3 COLUMNS */
@media (min-width: 768px) {
  .shop-page main.shop {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* =========================
   SHOP — CAPTION STYLING
========================= */

.shop-page .shop-item {
  text-align: left;          /* ensure figure content aligns left */
}

.shop-page .shop-item figcaption {
  margin-top: 0.75rem;
  text-align: left;

  /* inherit site-wide typography */
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: inherit;

  /* match body text tone */
  color: currentColor;

  /* Caption (title, medium, dimensions, price, Buy link) stays
     uppercase, inheriting the site-wide default (html, body) —
     unlike the shop-text description on the left, which is
     deliberately kept in sentence case (see .shop-page .shop-text
     above). */
  text-transform: uppercase;
}

/* =========================
   SHOP — IMAGE BASELINE ALIGN
========================= */

.shop-page main.shop {
  align-items: end;            /* bottom-align grid items */
}

.shop-page .shop-item {
  display: flex;
  flex-direction: column;
}

.shop-page .image-shell {
  margin-top: auto;            /* pushes image block to bottom */
}

/* =========================
   SHOP — 4:5 IMAGE RATIO
========================= */

.shop-page .image-shell {
  width: 100%;
  aspect-ratio: 4 / 5;      /* lock container ratio */
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.shop-page .image-shell img {
  width: 100%;
  height: 100%;
  object-fit: cover;        /* fill frame */
  object-position: center;
}

/* =========================
   SHOP — SINGLE COLUMN EDITORIAL VIEW
========================= */



.shop-page main.shop {
  min-height: auto;
  display: block;
  padding: 0;        /* THIS is what un-nudges the text */
  
}

/* one item per row */
.shop-page .shop-item {
  display: grid;
  grid-template-columns: 1fr minmax(280px, 420px) minmax(200px, 1fr);
  column-gap: 0.5rem;
  margin-bottom: 6rem;
  align-items: start;
}

/* image column */
.shop-page .image-shell {
  grid-column: 2;
  aspect-ratio: 4 / 5;
}

/* caption column */
.shop-page .shop-item figcaption {
  grid-column: 3;
  margin-top: 0;             /* top aligned with image */
  text-align: left;
  padding-left: 0.5em;
}




/* =========================
   SHOP — EDITORIAL ROW LAYOUT
========================= */

.shop-page .shop-item {
  display: grid;
  grid-template-columns:
    minmax(220px, 1fr)
    minmax(280px, 420px)
    minmax(200px, 1fr);
  column-gap: var(--shop-gap);
  margin-bottom: 6em;
  align-items: start;
}

/* LEFT TEXT COLUMN */
.shop-page .shop-item .shop-text {
  grid-column: 1;
  display: flex;
  align-items: center; /* vertical alignment to image */
}

@media (max-width: 768px) {
  .shop-page .shop-item {
    grid-template-columns: 1fr;
    row-gap: 1em;
  }

  .shop-page .shop-text,
  .shop-page .image-shell,
  .shop-page .shop-item figcaption {
    grid-column: auto;
  }

  .shop-page .shop-text {
    align-items: flex-start;
  }
}

.shop-page .shop-item:first-child .image-shell img {
  max-height: 100%;
  width: auto;
}

/* =========================
   SHOP — FIRST ITEM OFFSET FROM TOP
========================= */

.shop-page .shop-item:first-child {
  padding-top: 6em;   /* pushes text, image, and caption down */
}

/* =========================
   SHOP — FIXED NAV EXCLUDE BLEND
   The header, bottom-right mini nav, and copyright are all
   position:fixed and scroll over black shop text/images all the way
   down the page (unlike the exhibition pages, where this same
   exclusion treatment only needs to switch on for one full-bleed
   section at a time via a body class — see .detail-active/
   .credits-active). Here it just needs to be on permanently: white
   text + mix-blend-mode: exclusion inverts whatever pixel is directly
   behind it (black becomes white, white becomes black), so the nav
   stays legible against both the black shop text and the product
   photos without any scroll-position tracking.

   This has to be applied to <header> itself, not to individual nav
   links inside it. Tried scoping it down to just ".header-nav > a"
   (to keep the dropdown panels un-blended, see below) and that broke
   the logo/Info link entirely: <header> is position:fixed with its
   own z-index, which makes it a stacking context of its own — a
   child's mix-blend-mode can only blend against OTHER content already
   painted within that SAME stacking context, not through to the real
   page behind the fixed header. With nothing else opaque painted
   inside <header> right where the logo/Info link sit, they had
   nothing local to invert against and rendered as flat, invisible
   white-on-white. mix-blend-mode on <header> itself doesn't have this
   problem — <header>'s own blending happens where header is
   composited into body's stacking context, i.e. against the real page
   behind it.

   That reintroduces the earlier problem this section was written to
   fix: <header> also contains the Info/Work/Exhibitions/Press
   dropdown panels (#info-section, .work-list, etc. — siblings of
   their toggle link, not descendants of it), and mix-blend-mode on an
   ancestor blends its entire subtree as one unit, so those panels'
   own content would blend against whatever shop content sits behind
   them too — unreadable noise, not the plain-text-over-its-own-blur
   panel they're meant to be. Fixed instead by having each dropdown
   panel opt back OUT of the ancestor's blend: isolation: isolate
   gives it its own stacking context, and mix-blend-mode: normal on
   that isolated element means its own compositing (and its
   children's) happens normally again, against nothing but its own
   contents — see below.

   The bottom-right mini nav ("Buy Prints Instagram") also scrolls
   over black shop text/images on mobile (its position moves relative
   to the shop grid at narrower widths), so it needs the same
   treatment — but its own subtle rgba(255,255,255,0.05) background
   (see .bottom-mini-nav below) was itself getting blended along with
   the text, reading as a flat grey box. Overridden to fully
   transparent here so only the text blends, not a background that
   was only ever "optional" decoration anyway. It isn't inside a
   position:fixed ancestor of its own (.bottom-mini-nav itself is the
   fixed element), so it doesn't hit the isolation problem above.

   This only works where there's actually painted content to invert
   against — mix-blend-mode doesn't blend against the browser's bare
   canvas, only against real painted pixels within the same stacking
   context. Nothing on this page was ever painting an explicit white
   body background (it just relied on the default canvas), so over
   any empty stretch of "white" page the nav had nothing real to
   invert against and rendered as flat, invisible white-on-white.
   Painting the background explicitly here gives the blend a real
   white surface everywhere, so white text correctly inverts to black
   over it (and still to white over actual black content) instead of
   defaulting to plain black nav text, which would go back to
   invisible over the black shop text/images this was added for. */
body.shop-page {
  background-color: #fff;
}

body.shop-page header,
body.shop-page .bottom-mini-nav,
body.shop-page .copyright {
  mix-blend-mode: exclusion;
}

body.shop-page .bottom-mini-nav {
  background: transparent;
}

body.shop-page .logo,
body.shop-page .header-nav a,
body.shop-page .bottom-mini-nav a,
body.shop-page .copyright {
  color: #fff;
}

/* Dropdown panels: moved to be direct DOM siblings of <header> in
   shop.html itself (not just re-styled), because neither isolation:
   isolate nor position: fixed on a descendant actually escapes an
   ancestor's mix-blend-mode in practice — both were tried here first
   and neither worked; the panels stayed swept into header's blend
   regardless. Once they're no longer descendants of header at all,
   they render with their normal global styling automatically (plain
   black text, no blend), no color/isolation overrides needed.

   They lose their original position:absolute anchor in the process
   (that was relative to a small inline wrapper around their own
   toggle link, which only exists inside header now) — repositioned
   here as fixed, full-width from the left edge, matching how
   #info-section already behaved before this change. */
body.shop-page #info-section,
body.shop-page .work-list,
body.shop-page .exhibitions-list,
body.shop-page .press-list {
  position: fixed;
  top: 2.6rem;
  left: 0;
}

/* The site-wide :hover rules (.header-nav a:hover, .bottom-mini-nav
   a:hover, further up this file) fade the link to grey on hover —
   but a plain "a" selector is more specific than "a:hover" (an extra
   element vs. an extra pseudo-class), so the white color rule above
   was silently winning even while hovering and the fade never showed.
   Re-declared here, scoped the same way, so hover wins again. Same
   grey blended via exclusion still lands roughly mid-grey against
   either a black or white backdrop, so it stays visible either way. */
body.shop-page .header-nav a:hover {
  color: #7f7f7f;
}

body.shop-page .bottom-mini-nav a:hover {
  color: rgba(127, 127, 127, 0.7);
}

/* =========================
   SHOP — IMAGE LIGHTBOX
   Same centred/contained placement logic and blur-the-rest-of-the-
   page mechanism as the Info dropdown (body.info-open) and the
   exhibition pages' index lightbox.
========================= */
body.shop-page .shop-lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 3000;
  align-items: center;
  justify-content: center;
}

body.shop-page .shop-lightbox.show {
  display: flex;
}

body.shop-page .shop-lightbox img {
  max-width: 80vw;
  max-height: 80vh;
  object-fit: contain;
  cursor: default;
}

body.shop-page.shop-lightbox-open main {
  filter: blur(10px);
}

body.shop-page.shop-lightbox-open {
  overflow: hidden;
}

/* =========================
   SHOP LINK — TOP RIGHT
========================= */

/* SHOP LINK — sit inline with nav next to Contact */
.shop-link-nav {
  display: inline-block;  /* match other nav items */
}

.shop-link-top-right {
  font-family: inherit;
  font-weight: 800;
  text-decoration: none;
  color: currentColor;
  cursor: pointer;
  transition: color 0.3s ease;
}

.shop-link-top-right:hover {
  color: rgba(127, 127, 127, 0.7);
}

/* =========================
   SHOP custom buy button
========================= */

.custom-buy-btn {
  display: inline-block;
  color: #000;
  text-decoration: none;
  border-radius: 6px;
  transition: 0.2s all ease;
  padding-top: 0.4em;
  /* optional for padding around button text */
  padding-bottom: 0.5em;
  background-color: transparent;
  font-size: 1.5em; /* updated font size */
  text-transform: uppercase;
}


/* Make sure hover targets the link itself */
.custom-buy-btn:hover,
.custom-buy-btn:focus {
  color: rgba(127, 127, 127, 0.7); /* text highlights grey */
}

/* SOLD ITEMS — any print whose Stripe link hasn't been set yet
   (still the test_XXXXXXXXXXXX placeholder) gets marked sold. The
   image itself is left untouched (grayscale and brightness dimming
   were both tried and dropped) — just the "Sold" label below and
   the disabled lightbox (see the JS) signal the state. See
   mark_sold() in the one-off script that generated this, not a
   live/dynamic check. */

.sold-label {
  display: inline-block;
  color: #000;
  opacity: 0.5;
  padding-top: 0.4em;
  padding-bottom: 0.5em;
  font-size: 1.5em;
  text-transform: uppercase;
}

/* -------------------------
   Shop price
------------------------- */
figcaption .price {
  font-size: 1.1rem;
  padding-bottom: 0;
}

/* COPYRIGHT TRIGGER */
#copyright-trigger {
  cursor: pointer;
  position: relative;
  z-index: 10;
}

/* OVERLAY */
.disclaimer-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  backdrop-filter: blur(8px);
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  z-index: 1000;
}

/* SHOW OVERLAY */
.disclaimer-overlay.active {
  opacity: 1;
  pointer-events: all;
}

/* CONTENT BOX */
.disclaimer-content {
  background: white;
  color: #333;
  padding: 2em;
  max-width: 600px;
  width: 90%;
  border-radius: 12px;
  text-align: center;
  box-shadow: 0 8px 20px rgba(0,0,0,0.3);
  transform: translateY(-50px);
  opacity: 0;
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.disclaimer-overlay.active .disclaimer-content {
  transform: translateY(0);
  opacity: 1;
}

/* HEADINGS */
.disclaimer-content h2 {
  margin-bottom: 1em;
  font-size: 1.5em;
}

/* PARAGRAPH */
.disclaimer-content p {
  font-size: 1em;
  line-height: 1.5;
}

/* GLOBAL — lock scrolling on index and other landing pages */
html.landing-page,
body.landing-page {
  height: 100%;
  overflow: hidden;  /* prevent any vertical scroll */
}

/* Homepage now scrolls through More Exhibitions + Prints below the
   hero (same full-section pager as the exhibition pages, see
   body.roly-poly-v2-page below) — the blanket lock above only applies
   while body.landing-page is alone, i.e. shop.html and other single-
   carousel landing pages that never gained extra sections. */
body.landing-page.roly-poly-v2-page {
  height: auto;
  overflow: visible;
}


/* =========================
   LANDING CAROUSEL
   (Previously redefined four times with conflicting position
   values — position:relative/flex, then min-height:100vh, then
   position:relative+grid, then finally position:fixed+grid below.
   Consolidated to the one that was actually winning the cascade,
   so visual behaviour is unchanged.)
========================= */

/* Each carousel cell — shared by landing + exhibition carousels */
.carousel-cell {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 70vh;
  box-sizing: border-box;
}

.carousel-cell img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
  display: block;
}

.landing-carousel {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100svh;
  overflow: hidden;
  z-index: 1;
}

/* On the homepage the hero now sits inside .landing-carousel-wrapper
   (its own 100vh/100dvh, scroll-snapped section, see
   body.roly-poly-v2-page .landing-carousel-wrapper below) as the
   first of several full-page sections, rather than pinned over the
   whole viewport — same pattern as .landing-carousel.shop-landing
   already uses on the shop page. */
body.roly-poly-v2-page .landing-carousel-wrapper .landing-carousel {
  position: relative;
  inset: auto;
  height: 100%;
  z-index: auto;
}

.landing-carousel .carousel {
  width: 80vw;
}

.landing-carousel .carousel-cell {
  display: grid;
  place-items: center;
}

.landing-carousel img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
}

/* =========================
   EXHIBITION HERO CAROUSEL
========================= */

.exhibition-hero-carousel {
  position: relative;
  width: 100%;
  min-height: 70vh;
  padding: 12vh 0 6vh;  /* optical centring + breathing room */
  display: grid;
  place-items: center;
  
}

.exhibition-hero-carousel .carousel {
  width: 80vw;
}

.exhibition-hero-carousel .carousel-cell {
  display: grid;
  place-items: center;
}

.exhibition-hero-carousel img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
}

/* Parent wrapper for padding */
.index-table {
  width: 100%;
  padding: 0 1em;   /* page padding */
  box-sizing: border-box;
}

/* Grid for all index items */
.index-row {
  display: grid;
  grid-template-columns: repeat(8, 1fr); /* default 8 items per row */
  column-gap: 1em;  /* vertical gaps between columns */
  row-gap: 8em;     /* horizontal gaps between rows */
  justify-content: center; /* center grid items */
  box-sizing: border-box;
  /* remove width: 100% */
}

/* Make images responsive */
.index-item img {
  width: 100%;
  height: auto;
  display: block;
  object-fit: cover;
}

@media (max-width: 1024px) {
  .index-row {
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    column-gap: 2em; /* increased horizontal gap for medium screens */
  }
}

@media (max-width: 600px) {
  .index-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
    column-gap: 5em;       /* increased horizontal gap */
    row-gap: 6em;
    justify-content: center; /* ensures grid items stay centered */
  }
}

@media (max-width: 1200px) {

  .exhibition-context {
    display: flex;                 
    flex-direction: column;        
    gap: 2.5em;                    
    margin-top: -6em;              
    padding: 0 1em;                
  }

  /* Carousel full-width */
  .context-carousel {
    width: 71vw;
    max-width: 71vw;
    padding: 0em;
    margin: 0 auto;
  }

  /* Center carousel cells */
  .context-carousel .carousel-cell {
    width: 100%;            /* cell fills container */
    display: flex;           /* use flex to center content */
    justify-content: center; /* horizontal centering */
    align-items: center;     /* optional: vertical centering if needed */
    padding: 1em;
    margin: 1em;
  }

  .context-carousel img {
    width: auto;     /* allow image to keep its aspect ratio */
    max-width: 100%; /* constrain image to cell width */
    height: auto;
    object-fit: contain;
    display: block;
    padding: 0em;
    margin: 0;
  }

  /* Text block full-width underneath */
  .context-text {
    display: block;       
    width: 100%;
    padding: 0 1em;
    margin: 0 auto;
  }

  .context-text p {
    max-width: 100%;
    margin-bottom: 0.8em;
    text-align: left;
    
  }

  @media (max-width: 768px) {

    /* Let hero collapse to image height.
       Scoped to .exhibition-hero-carousel only — this used to also
       target the shared .landing-carousel, which unintentionally
       broke the homepage carousel's fixed positioning on narrow
       viewports (e.g. while the info panel was open). */
    .exhibition-hero-carousel {
      position: relative;
      height: auto;
      min-height: auto;
      padding-bottom: 0;
    }
  
    .carousel-cell {
      min-height: auto;
      height: auto;
    }
  
    /* Images control height */
    .carousel-cell img {
      max-height: 70svh;
      width: auto;
    }
  }
}

/* =========================
   TENDER REVELATION — MOBILE-ONLY FULL-WIDTH CAROUSELS
   Scoped to this page only for now (via body.tender-revelation-page)
   so the other exhibition pages keep their current narrower
   80vw/71vw carousel widths until this is copied over to them.
   Matches the 1em gutter used by .exhibition-text / .context-text.

   max-width:767px matches this codebase's existing mobile/tablet
   boundary (see the shop grid's 767/768 split above) — iPad and up
   are intentionally left untouched.
========================= */
@media (max-width: 767px) {
  body.tender-revelation-page .exhibition-hero-carousel {
    padding-left: 1em;
    padding-right: 1em;
    box-sizing: border-box;
  }

  body.tender-revelation-page .exhibition-hero-carousel .carousel {
    /* Not width:100% — .exhibition-hero-carousel uses place-items:center,
       so its child (.carousel-figure) is sized to content rather than
       stretched, giving 100% nothing definite to resolve against and
       collapsing the carousel to 0 width. calc() against the viewport
       sidesteps that circular dependency entirely. */
    width: calc(100vw - 2em);
  }

  /* No padding here on purpose — .exhibition-context (this element's
     parent) already has padding:0 1em at this breakpoint, so putting
     another 1em of padding directly on the flickity root doubled the
     inset and shrank the actual image relative to the hero carousel
     even though the outer box width matched. */
  body.tender-revelation-page .context-carousel {
    width: 100%;
    max-width: 100%;
    padding: 0;
    margin: 0;
    box-sizing: border-box;
  }

  /* The container above is full-width now, but the cell/image inside
     still carried the old inset (padding:1em + margin:1em from the
     max-width:1200px rule above, plus its own base padding-left:1em)
     — that's what was making the actual image render narrower than
     the hero carousel despite the outer container matching. */
  body.tender-revelation-page .context-carousel .carousel-cell {
    padding: 0;
    margin: 0;
  }

  body.tender-revelation-page .context-carousel img {
    width: 100%;
    max-width: 100%;
  }
}

/* =========================
   ROLY POLY — REDESIGNED SECTIONS
   Scoped to body.roly-poly-page only. tender-revelation.html is kept
   untouched as the "back code" reference template; this page is the
   trial for the new section layout (hero as-is, then: text block,
   image scroll filmstrip [unchanged], enlarged closing credits, new
   mini index).
========================= */

/* SECTION 2 — text block description.
   .exhibition-context used to be a 2-column grid pairing a small
   image carousel with the text; the carousel is gone (it was
   showing the wrong exhibition's photos anyway), so this collapses
   it to a single generous column instead of leaving an empty
   second column. */
body.roly-poly-page .exhibition-context {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 0 1em;
  box-sizing: border-box;
}

/* The section itself is full viewport width, but the actual reading
   column stays narrow (kaleidoscope.media's own text blocks don't
   stretch edge to edge either) — centered within the full-width
   section rather than the paragraphs themselves spanning it. */
body.roly-poly-page .context-text {
  display: block;
  max-width: 700px;
  margin: 0 auto;
}

body.roly-poly-page .context-text .para-1,
body.roly-poly-page .context-text .para-2,
body.roly-poly-page .context-text .para-3 {
  grid-column: unset;
}

body.roly-poly-page .context-text p {
  /* 1.32vw as specified, but floored at 14px — literal 1.32vw drops
     under 6px on phone widths, which is unreadable. Desktop (roughly
     1060px+) gets the exact 1.32vw scaling untouched. */
  font-size: clamp(14px, 1.32vw, 32px);
  /* Matches the site's standard line-height (used throughout for
     .para, dropdown lists, etc.) instead of the looser 1.5 — tighter,
     consistent with the rest of the site. */
  line-height: 1.1142857em;
  text-align: left;
}

/* SECTION 3 — image scroll filmstrip, full viewport height, images
   vertically centered within it (the strip's own images are a fixed
   600px/45vh tall, so centering keeps them from sitting awkwardly at
   the top of a much taller section). */
body.roly-poly-page .exhibition-closing-gallery {
  display: flex;
  align-items: center;
  min-height: 100vh;
  box-sizing: border-box;
}

/* SECTION 4 — closing credits/dates, enlarged to fill the window
   ("largen the text to fit window"). */
body.roly-poly-page .exhibition-dates-link {
  display: flex;
  min-height: 100vh;
  align-items: center;
  justify-content: center;
}

/* Back to the original (tender-revelation.html) size — was enlarged
   to clamp(2.5em,7vw,6em) before, now reverted; the slide still fills
   the viewport via .exhibition-dates-link's min-height:100vh, just
   with normal-sized text centered inside it. */
body.roly-poly-page .exhibition-dates {
  font-size: 2em;
  line-height: 1.1;
  margin-top: 0;
  padding-bottom: 0;
}

/* SCROLL-REVEAL TRANSITION (kaleidoscope.media style) — sections 2–4
   fade and slide up into place as they scroll into view. Section 1
   (hero) is left alone since it's on screen immediately on load. */
body.roly-poly-page .exhibition-context,
body.roly-poly-page .exhibition-closing-gallery,
body.roly-poly-page .exhibition-dates-link {
  opacity: 0;
  transform: translateY(50px);
  transition: opacity 0.9s ease, transform 0.9s ease;
}

body.roly-poly-page .exhibition-context.is-visible,
body.roly-poly-page .exhibition-closing-gallery.is-visible,
body.roly-poly-page .exhibition-dates-link.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* SCROLL SNAP — each full-height section snaps cleanly into place
   instead of free-scrolling past it, so the "slide" feel actually
   locks in one section at a time. */
body.roly-poly-page {
  scroll-behavior: smooth;
  scroll-snap-type: y mandatory;
}

body.roly-poly-page .exhibition-context,
body.roly-poly-page .exhibition-closing-gallery,
body.roly-poly-page .exhibition-dates-link {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* =========================
   ROLY POLY V2 — STAGING/DRAFT PAGE
   Independent copy of the body.roly-poly-page block above, scoped to
   body.roly-poly-v2-page (roly-poly-v.2.html) instead. Kept as a
   separate copy on purpose — this is the trial page being rebuilt
   section-by-section against real InDesign comps, and it needs to be
   safe to change without any risk to the live roly-poly.html.
========================= */

/* Section 2 — full-bleed detail image, caption + intro overlaid with
   mix-blend-mode: exclusion (CSS's direct match for Photoshop's
   "Exclude" blend mode). Text must be white: exclusion blend leaves
   black text invisible (black + anything = that thing, unchanged),
   while white text inverts whatever pixel sits beneath it. */
body.roly-poly-v2-page .exhibition-detail {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  overflow: hidden;
  scroll-snap-align: start;
}

/* Section 2 background is a Flickity-fade carousel (same settings as
   the Section 1 hero carousel, initialized identically in JS), sized
   to fill the full 100vw x 100vh section. Images use object-fit:
   cover so every slide is a true full-bleed fill, edge to edge —
   matching how Section 2 looked as a single static image before it
   became a carousel. */
body.roly-poly-v2-page .detail-bg-carousel,
body.roly-poly-v2-page .detail-bg-carousel .carousel,
body.roly-poly-v2-page .detail-bg-carousel .carousel-cell {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* This carousel is a fixed full-bleed background — it doesn't need
   Flickity's normal per-slide height-matching behavior at all, and
   that behavior is exactly what was causing the overflow bug: fade
   mode caches .flickity-viewport's height as an inline style,
   measured once and never automatically re-measured, and because
   .carousel-cell's own height is a PERCENTAGE of that same cached
   viewport height (not directly of .carousel), calling
   flkty.resize() after a window resize just re-reads the cell's
   current (already-stale) height back — a circular reference that
   never corrects itself. !important bypasses Flickity's inline
   style entirely, forcing every layer to track the real, current
   section height directly instead of Flickity's cached one. */
body.roly-poly-v2-page .detail-bg-carousel .flickity-viewport,
body.roly-poly-v2-page .detail-bg-carousel .flickity-slider {
  height: 100% !important;
}

body.roly-poly-v2-page .detail-bg-carousel .carousel-cell img {
  width: 100%;
  height: 100%;
  /* Site-wide .carousel-cell img caps images at max-height:70vh for
     the hero carousel's own layout — override it here so images can
     actually fill the full 100vh section instead of being capped. */
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
  object-position: center;
}

/* The site-wide mobile/tablet fix (@media max-width:1024px, further
   below) forces height:auto !important + object-fit:contain on every
   .carousel-cell img so the hero carousel can collapse to its image's
   natural height there — but that same rule was also catching this
   full-bleed carousel, shrinking it down to fit-width with letterbox
   bars and hiding most of the section instead of filling it, which
   made the overlaid caption/intro text sit over empty space. All 7
   banner images are landscape, so on a portrait phone/tablet
   viewport cover naturally keeps full height and crops width instead
   — never height — which is exactly what's needed to keep the text
   readable over the image. */
@media (max-width: 1024px) {
  body.roly-poly-v2-page .detail-bg-carousel .carousel-cell img {
    width: 100% !important;
    height: 100% !important;
    max-width: none !important;
    object-fit: cover !important;
    object-position: center !important;
  }
}

body.roly-poly-v2-page .detail-overlay {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* This value is NOT the same kind of space as the padding-bottom
     below, even though both are meant to read as roughly the same
     gap. .copyright is a separately-positioned fixed element, so
     most of its padding-bottom clearance gets eaten by its own
     line-height before any of it is visible as a gap. Caption and
     intro here are plain flex siblings with no such built-in
     consumption, so the same padding value would render as a much
     bigger, mismatched gap. */
  gap: 4px;
  box-sizing: border-box;
  /* .copyright is fixed 1em (its own font-size, ~10px) above the
     viewport edge, but its own text height sits ~22px above that —
     clear that plus a bit more breathing room (~12px visible gap,
     up from ~6px) so the intro text doesn't read as crowding the
     disclaimer. */
  padding: 0 0 2.63rem;
  mix-blend-mode: exclusion;
}

/* White so the exclusion blend above actually inverts against the
   photo — exclusion of black text is a no-op (result = the
   background colour, unchanged), which reads as invisible text. */
body.roly-poly-v2-page .detail-overlay .exhibition-caption,
body.roly-poly-v2-page .detail-overlay .exhibition-text.intro {
  color: #fff;
}

/* !important needed on margin-bottom: a site-wide mobile/tablet rule
   (.exhibition-article .exhibition-text { margin-bottom: 1.5em !important })
   otherwise wins and reintroduces extra space below the intro text. */
body.roly-poly-v2-page .detail-overlay .exhibition-text.intro {
  position: static;
  margin-bottom: 0 !important;
  padding-top: 0;
}

/* .exhibition-caption.two-col carries its own margin (0.5em 0 1em)
   site-wide, which stacks on top of the flex gap between it and the
   intro text below — zero it out so the flex gap alone (matched to
   the intro-to-disclaimer padding above) is the true visual distance
   between the two, keeping both gaps equal. */
body.roly-poly-v2-page .detail-overlay .exhibition-caption {
  margin: 0;
}

/* .text-block (wrapping the intro <p>) carries its own margin-bottom
   of 2–3em site-wide, on top of the container's own bottom padding —
   that stacked, uncounted margin was the real reason the gap to the
   footer still read as too loose. Zero it out here so the overlay's
   own padding-bottom is the only space left. !important is needed on
   mobile/tablet specifically: a site-wide rule there
   (.exhibition-article .text-block { margin-bottom: 1.5em !important })
   otherwise wins and reintroduces the extra margin. */
body.roly-poly-v2-page .detail-overlay .text-block {
  margin-bottom: 0 !important;
  /* Same site-wide mobile/tablet rule also sets its own
     padding: 2em !important directly on .text-block (separately from
     .exhibition-text.intro's padding) — zero it out too, since
     .exhibition-text.intro already provides the horizontal gutter. */
  padding: 0 !important;
}

/* Opacity-only reveal — deliberately NOT also a translateY() slide,
   unlike the older single-page site's version of this effect. The
   section pager snaps scrollY straight to each section's offsetTop,
   which is unaware of the IntersectionObserver-driven .is-visible
   class (JS, "SCROLL-REVEAL" below) that's still pending when the
   pager's own 700ms scroll animation finishes — a still-pending
   translateY(50px) then left the section rendering 50px below where
   the pager had already landed, i.e. a visible gap at the section's
   top and its content/caption sitting 50px low, exactly at rest, on
   every section after the first. Opacity has no effect on layout or
   paint offset, so it can lag behind the pager arbitrarily without
   ever causing that gap — only the fade itself is delayed, never the
   position. Same reasoning applies to every .is-visible pair below
   for Sections 2-8. */
body.roly-poly-v2-page .exhibition-detail {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-detail.is-visible {
  opacity: 1;
}

/* While Section 2 (the full-bleed detail image) is the active
   section, the fixed header (logo + nav), footer disclaimer, and
   bottom-right mini nav all sit directly over that image too — so
   they get the same exclusion blend as the caption/intro text.
   .detail-active is toggled on body by the pager JS based on which
   section is currently in view; it's absent on Section 1 (the hero
   carousel), which isn't a full-bleed image, so nav/logo/footer stay
   normal black there.

   mix-blend-mode has to go on `header` itself, not on each nav link
   individually — a blending element only blends against real page
   content within its OWN parent stacking context; header already
   forms one (position: fixed + z-index), so a blend-mode set on a
   descendant of header never reaches past that boundary out to the
   actual photo behind it and just renders flat/inert instead of
   reacting to the image (this was tried first and looked broken:
   the nav stayed a flat, unreacting white instead of actually
   subtracting against the photo). Applying it to header itself makes
   header's whole rendered subtree composite as one flattened unit
   that then correctly blends against the real backdrop.

   That reintroduces the ORIGINAL bug this rule used to have though:
   every dropdown panel (#info-section, .work-list, .exhibitions-list,
   .press-list, .email-dropdown) lives nested inside header in the
   markup, so it's swept into that same flattened unit and blended
   too — unreadable when opened on this section. Fixed instead with
   `isolation: isolate` directly on those panels (see below) — that
   walls their content off from an ancestor's blend-mode without
   needing the blend-mode itself to move, so header's blend still
   correctly reaches the real photo. */
body.roly-poly-v2-page.detail-active header,
body.roly-poly-v2-page.detail-active .copyright,
body.roly-poly-v2-page.detail-active .bottom-mini-nav {
  mix-blend-mode: exclusion;
}

body.roly-poly-v2-page.detail-active .logo,
body.roly-poly-v2-page.detail-active #info-toggle,
body.roly-poly-v2-page.detail-active .work-toggle,
body.roly-poly-v2-page.detail-active .exhibitions-toggle,
body.roly-poly-v2-page.detail-active .press-toggle,
body.roly-poly-v2-page.detail-active .contact-nav > a,
body.roly-poly-v2-page.detail-active .shop-link-top-right,
body.roly-poly-v2-page.detail-active .copyright,
body.roly-poly-v2-page.detail-active .bottom-mini-nav a {
  color: #fff;
}

@media (max-width: 767px) {
  body.roly-poly-v2-page .detail-overlay {
    gap: 4px;
    padding: 0 0 2.63rem;
  }

  body.roly-poly-v2-page .detail-overlay .exhibition-caption.two-col {
    grid-template-columns: 1fr;
    row-gap: 0.4em;
    font-size: 1em;
  }

  /* On mobile, the caption sits on its own at roughly a 66/33 split
     of the section (66% clear space above it, ~33% below before the
     intro/disclaimer group) instead of stacked tight above the intro
     text — taken out of the flex flow so it no longer competes for
     space with that group, which stays anchored to the bottom as
     before. Top-aligned (no centering transform) so the 66% gap
     above is measured to the caption's own top edge. */
  body.roly-poly-v2-page .detail-overlay .exhibition-caption.two-col {
    position: absolute;
    top: 66%;
    left: 0;
    right: 0;
  }

  /* !important needed here: a site-wide mobile/tablet rule
     (.exhibition-article .exhibition-text { padding: 2em !important })
     was winning over this and reintroducing ~20px of top padding,
     which is what was throwing off the caption/intro/disclaimer
     spacing specifically on mobile. */
  body.roly-poly-v2-page .detail-overlay .exhibition-text.intro {
    padding: 0 1em !important;
  }

  body.roly-poly-v2-page .detail-overlay .exhibition-text.intro p {
    font-size: 1.1em;
  }
}

/* The earlier "mandatory" snap felt broken because Section 1 wasn't
   actually a full 100vh box (the shared .exhibition-hero-carousel
   rule elsewhere on the site caps it at min-height:70vh for optical
   centring on other exhibition pages) — so the snap points didn't
   line up with real viewport-height increments and scrolling up
   landed short, "just underneath" the hero instead of snapping it
   fully into view. Forcing Section 1 to a true 100vh box (below)
   fixes the alignment, so mandatory snap now lands cleanly on full
   sections in both directions. */
body.roly-poly-v2-page {
  scroll-behavior: smooth;
  scroll-snap-type: y mandatory;
  /* Sections 1 and 2 are always covered edge-to-edge by a photo, so
     the page's actual background color was never visible — nothing
     on this site sets one explicitly, it just relies on the browser's
     default white canvas. Section 3 has no image over it, and in a
     browser/OS set to dark mode that implicit default can render
     dark instead, with no site CSS to fall back on. Setting it
     explicitly avoids that. */
  background-color: #fff;
}

body.roly-poly-v2-page .landing-carousel-wrapper {
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* The site-wide mobile/tablet hero-carousel fix (below, up to 1024px)
   forces .exhibition-hero-carousel to height:auto !important so it
   collapses to the image's natural size instead of stretching — that
   part is correct and shouldn't be touched here. But the wrapper
   around it is a plain block div, so that collapsed, auto-height
   carousel just sits at the top of the 100vh wrapper instead of
   sitting in the middle of it. Centering the wrapper itself (rather
   than fighting the !important height rule) fixes that without
   touching the shared, site-wide carousel-fix rule. */
@media (max-width: 1024px) {
  body.roly-poly-v2-page .landing-carousel-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

body.roly-poly-v2-page .exhibition-hero-carousel {
  min-height: 100vh;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  padding: 0;
  box-sizing: border-box;
  transform: none;
}

body.roly-poly-v2-page .exhibition-detail {
  scroll-snap-stop: always;
}

/* Section 3 — plain exhibition description + colophon, full
   viewport, continuing the same full-width/height section rhythm as
   Sections 1 and 2 but with no background image or blend effect. */
body.roly-poly-v2-page .exhibition-description {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

body.roly-poly-v2-page .description-inner {
  /* 732px satisfies four line-wrap constraints at once, tested stable
     across roughly 726-739px:
     - paragraph 1, line 1 ends "...the first solo"
     - paragraph 1, line 3 ends "...sculpture, photography," (so
       "photography" lands with "sculpture" instead of wrapping)
     - paragraph 2, line 4 includes "...study of embodiment"
     - paragraph 2, line 5 keeps "re-contextualise the" together
       instead of breaking at the word's own internal hyphen
     Re-tune this if the paragraph font-size, text-transform,
     letter-spacing, or either paragraph's copy changes again, since
     all of these are directly linked. */
  max-width: 732px;
  width: 100%;
  margin: 0 auto;
  padding: 0 2em;
  box-sizing: border-box;
}

body.roly-poly-v2-page .description-text p {
  /* Exactly 2x the nav bar / caption text (10.24px), same em context. */
  font-size: 2em;
  line-height: 1;
  margin: 0 0 2em;
  /* Site-wide text is uppercase; this paragraph reads in normal
     sentence case instead. */
  text-transform: none;
  /* Explicit, so line-wraps never break mid-word with a hyphen. */
  hyphens: none;
  /* Reduces the perceived heaviness of the 800-weight type without
     changing the font/style: grayscale antialiasing renders crisper,
     less chunky edges than the default "auto" smoothing at this
     large a size. Color kept pure black per feedback. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #000;
  letter-spacing: normal;
}

/* Colophon is pinned to the bottom of the section independently of
   the paragraph text (see .description-colophon below), so the last
   paragraph no longer needs to reserve space beneath it — this keeps
   the text block's own visual bounds symmetric, so justify-content:
   center on .exhibition-description centers the paragraph itself
   rather than the paragraph-plus-colophon combined. */
body.roly-poly-v2-page .description-text p:last-child {
  margin-bottom: 0;
}

/* Pulled out of .description-inner's normal flow, so it no longer
   moves with or contributes height to the centered paragraph text
   above it. Vertical position (top) is set by JS in roly-poly-v.2.html
   (positionColophon()), which centers this block in whatever gap is
   left between the paragraph's bottom edge and the fixed bottom nav —
   the bottom value below is just a fallback for before that script
   runs. Positions relative to .exhibition-description (the nearest
   positioned ancestor), skipping over .description-inner. Horizontal
   alignment is replicated by hand from .description-inner at each
   breakpoint below, since position:absolute no longer inherits it. */
body.roly-poly-v2-page .description-colophon {
  position: absolute;
  left: 50%;
  bottom: 2.63rem;
  transform: translateX(-50%);
  width: 100%;
  max-width: 732px;
  padding: 0 2em;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1em;
  font-size: 1em;
  line-height: 1;
}

body.roly-poly-v2-page .exhibition-description {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-description.is-visible {
  opacity: 1;
}

@media (max-width: 767px) {
  body.roly-poly-v2-page .description-colophon {
    grid-template-columns: 1fr;
    /* Matches .exhibition-caption.two-col's mobile row-gap above —
       a visible break under the name, instead of the details running
       straight on beneath it. */
    row-gap: 0.4em;
    line-height: 1;
  }

  /* 0.8x the desktop 2em — the desktop size reads too large on
     narrow screens. */
  body.roly-poly-v2-page .description-text p {
    font-size: 1.6em;
  }
}

/* On mobile/tablet, the text sits flush left with the same 1em inset
   the header logo/nav use against the page edges, rather than
   staying centered as a narrower column (which left visible gaps on
   iPad-width viewports, wider than the text column's own max-width). */
@media (max-width: 1024px) {
  body.roly-poly-v2-page .description-inner {
    max-width: none;
    padding: 0 1em;
    margin: 0;
  }

  /* Replicates .description-inner's flush-left/1em-inset treatment
     above, since .description-colophon is positioned absolutely and
     no longer inherits it through normal flow. */
  body.roly-poly-v2-page .description-colophon {
    left: 0;
    transform: none;
    max-width: none;
    width: 100%;
    padding: 0 1em;
  }
}

/* Section 4 — image showreel, full viewport like Sections 1-3.
   Iterated from tender-revelation.html's .exhibition-closing-gallery
   pattern, now driven by Flickity (cellAlign:center + wrapAround)
   instead of plain native horizontal scroll, so the active cell
   (starting on the portrait image) stays centered, loops infinitely,
   and the non-fullbleed neighbor cells peek in at the section's
   edges. Portrait/landscape images still get different heights —
   portraits a taller box, landscapes shorter-but-wider — so the row
   reads as varied rather than uniform tiles. */
body.roly-poly-v2-page .exhibition-showreel {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

body.roly-poly-v2-page .showreel-carousel {
  width: 100%;
}

body.roly-poly-v2-page .showreel-image {
  display: grid;
  place-items: center;
  margin-right: 8vw;
  /* All cells share this one explicit height — matching the tallest
     (portrait) image — instead of each cell auto-sizing to its own
     image. Flickity measures cell height itself (to size
     .flickity-viewport) and doesn't know about "stretch to fill
     parent" tricks; giving every cell the same real, measurable
     height is what actually makes Flickity center them on a common
     axis, since place-items:center then centers each (shorter for
     landscape) image within that shared cell height. Width is set
     explicitly in JS (see the showreel init script), matching the
     img's actual rendered height x its aspect ratio. */
  height: 75vh;
  /* The generic, unscoped .carousel-cell rule (shared by the landing
     + other exhibition carousels) sets min-height: 70vh with no media
     query, so it applies at every viewport size regardless — since
     nothing here ever set min-height of its own, that declaration was
     winning by default. Harmless at 75vh/60vh (desktop/tablet, both
     already taller than 70vh) but on mobile, where the height below
     drops to 55vh, min-height:70vh silently floored the cell back up
     to 70vh anyway — taller than its own image, taller than the
     .showreel-carousel container around it, and taller than what
     Flickity measured the viewport to (this exact same fix is
     already applied to .more-exhibitions-item for the same rule). */
  min-height: 0;
}

body.roly-poly-v2-page .showreel-image img {
  /* place-items:center means align-items:center (not stretch), so a
     grid item's percentage height can't resolve against its
     (non-stretched) grid area and silently falls back to the image's
     natural unscaled size — that's why this is a real vh value
     directly on the img, not height:100% of the cell. */
  height: 60vh;
  width: auto;
  /* Site-wide .carousel-cell img caps images at max-height:70vh for
     the hero carousel's own layout — override it here so the
     portrait image can actually reach 75vh instead of being capped. */
  max-width: none;
  max-height: none;
  display: block;
  object-fit: contain;
}

body.roly-poly-v2-page .showreel-image.showreel-portrait img {
  height: 75vh;
}

/* Thumbnail nav strip — left-aligned to the same 1em inset as the
   header logo/nav. Bottom clearance from the disclaimer doubled from
   the original intro-text-matching 2.15rem, per feedback that it
   still read as too close. */
body.roly-poly-v2-page .showreel-thumbnails {
  position: absolute;
  left: 1em;
  /* Bounds the strip's width to the section itself (rather than
     letting it grow to fit all 9 thumbs unconstrained) so the overflow
     rule below can actually kick in — on narrow viewports 9 thumbs
     don't fit in one row, and without this the extra ones used to
     render past the section's own overflow:hidden edge, invisible and
     completely unreachable on mobile. */
  right: 1em;
  /* Nudged down from 4.3rem — closer/tighter to the disclaimer again,
     without going all the way back to the original 2.15rem that read
     as too close. */
  bottom: 3rem;
  z-index: 2;
  display: flex;
  /* Centered on their shared horizontal middle axis rather than
     bottom-aligned — the taller portrait thumb and shorter landscape
     ones now line up on their centers instead of their bottoms. */
  align-items: center;
  gap: 1.2em;
  /* Lets the strip scroll horizontally (drag/swipe) once it's wider
     than the space available, instead of silently overflowing. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

body.roly-poly-v2-page .showreel-thumbnails::-webkit-scrollbar {
  display: none;
}

/* Plain block (not flex/grid) so the img's height:100% resolves
   normally against this button's own explicit height — a flex/grid
   parent with center alignment breaks that percentage resolution
   (learned the hard way sizing the main carousel cells). Landscape
   and portrait get different heights, same 60:75 ratio as the main
   carousel's own 60vh/75vh, so the thumbnails read as small versions
   of the same cells rather than uniform square crops. */
body.roly-poly-v2-page .showreel-thumb {
  display: block;
  height: 46px;
  width: auto;
  flex-shrink: 0;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  overflow: hidden;
  box-sizing: border-box;
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.3s ease, transform 0.25s ease, filter 0.25s ease;
}

body.roly-poly-v2-page .showreel-thumb:hover {
  transform: scale(1.12);
  filter: brightness(1.1) saturate(1.2);
}

body.roly-poly-v2-page .showreel-thumb-portrait {
  height: 58px;
}

body.roly-poly-v2-page .showreel-thumb img {
  height: 100%;
  width: auto;
  object-fit: contain;
  display: block;
}

body.roly-poly-v2-page .showreel-thumb.is-active {
  opacity: 0.5;
}

@media (max-width: 767px) {
  body.roly-poly-v2-page .showreel-thumb {
    height: 34px;
  }

  body.roly-poly-v2-page .showreel-thumb-portrait {
    height: 43px;
  }
}

body.roly-poly-v2-page .exhibition-showreel {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-showreel.is-visible {
  opacity: 1;
}

/* Same site-wide mobile/tablet gotcha as Section 2's carousel: a
   shared @media(max-width:1024px) rule forces height:auto !important
   on every .carousel-cell img, which would flatten the portrait/
   landscape height variation here back down to one uniform height.
   Override it with matching !important so the two heights actually
   hold on phone/tablet too. */
@media (max-width: 1024px) {
  body.roly-poly-v2-page .showreel-image {
    margin-right: 16vw;
  }

  body.roly-poly-v2-page .showreel-image img {
    height: 60vh !important;
    width: auto !important;
    max-width: none !important;
    object-fit: contain !important;
  }

  body.roly-poly-v2-page .showreel-image.showreel-portrait img {
    height: 75vh !important;
  }
}

@media (max-width: 767px) {
  body.roly-poly-v2-page .showreel-image {
    height: 55vh;
  }

  body.roly-poly-v2-page .showreel-image img {
    height: 45vh !important;
  }

  body.roly-poly-v2-page .showreel-image.showreel-portrait img {
    height: 55vh !important;
  }
}

/* =========================
   SECTION 5 — INDEX
   Full viewport like Sections 1-4, but the grid inside scrolls on
   its own if it overflows one screen (see the wheel/touch handlers
   in the script, which only hand off to the page-level section
   pager once this inner scroll hits its top/bottom boundary).
========================= */
body.roly-poly-v2-page .exhibition-index {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  /* The typography-free version (4 curated rows, no headings/
     captions) is short enough to fit one viewport with room to
     spare, so this no longer needs to be a top/bottom-padded scroll
     region — centering it here, like every other section, reads
     better and keeps equal clearance above and below regardless of
     viewport height. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

body.roly-poly-v2-page .index-scroll-container {
  /* Positioned (rather than relying on the parent's justify-content:
     center) so JS (see layoutIndexRows() in roly-poly-v.2.html) can
     spread the rows across the full space between the fixed header
     and footer nav, with the gap above row 1, between every pair of
     rows, and below the last row all computed to be equal — not
     just vh-centered as one block. left/right + no top/gap yet: a
     reasonable fallback (this element's own static position, rows
     stacked with no extra gap) before JS runs or if it's
     unavailable. */
  position: absolute;
  left: 0;
  right: 0;
  width: 100%;
  box-sizing: border-box;
  padding: 0 1em;
  display: flex;
  flex-direction: column;
}

body.roly-poly-v2-page .index-group-heading {
  /* Same tight gap as the caption-to-intro-text spacing on the
     previous section (plain flex siblings, no built-in consumption
     to eat into the margin) — see .detail-overlay's gap: 4px. */
  margin-bottom: 4px;
  /* Same font-size as the nav bar. */
  font-size: 1em;
  /* Grayscale antialiasing renders crisper, less chunky edges for
     this heavy weight at small caption sizes — same treatment used
     elsewhere in this file for the same reason (see
     .description-colophon). */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.roly-poly-v2-page .index-group-row {
  /* Back to a shared equal-width column grid (see the inline
     grid-template-columns in the HTML), now 8 columns instead of 12
     — wider columns give a fixed-height landscape image more room
     before it risks overflowing past its own column into the next
     one, though at extreme aspect ratios that's still possible (see
     the flex-based alternative this replaced, above in git history,
     if that turns out to still overlap with real content). Each
     image is left-aligned within its own column (justify-items:
     start); a row with fewer than 8 images leaves the remaining
     columns empty on the right instead of stretching to fill them.
     minmax(0, 1fr) — not plain 1fr — so a wider-than-its-column
     image can't force that column (and so every other equal column)
     to grow and overflow the row; content that doesn't fit its
     column just overflows visually instead of blowing out the grid. */
  display: grid;
  align-items: end;
  justify-items: start;
  column-gap: 2vw;
  width: 100%;
}

body.roly-poly-v2-page .index-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* An unrelated, unscoped ".index-item { width: 110px }" rule
     elsewhere in this file (a different, older index page) would
     otherwise still apply here — nothing in this scoped block
     overrode width itself, so that lower-specificity rule won by
     default for that one property. auto lets the grid cell size to
     its own content (the image) instead of a fixed width. */
  width: auto;
  /* Subtle per-image fade + float-up as each one scrolls into view
     within the index's own internal scroll container — same
     opacity/translateY convention as the full-section reveals
     elsewhere in this file, just a smaller distance and shorter
     duration since these are individual thumbnails, not whole
     sections. */
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}

body.roly-poly-v2-page .index-item.is-visible {
  opacity: 1;
  transform: translateY(0);
}

body.roly-poly-v2-page .index-item img {
  height: 10vh;
  width: auto;
  max-width: none;
  max-height: none;
  object-fit: contain;
  display: block;
  transform: scale(1);
  transition: transform 0.25s ease, filter 0.25s ease;
}

body.roly-poly-v2-page .index-item img:hover {
  transform: scale(1.1);
  filter: brightness(1.08) saturate(1.2);
}

body.roly-poly-v2-page .index-caption {
  margin-top: 0.4em;
  /* Same font-size as the nav bar. */
  font-size: 1em;
  /* Grayscale antialiasing renders crisper, less chunky edges for
     this heavy weight at small caption sizes — same treatment used
     elsewhere in this file for the same reason (see
     .description-colophon). */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.roly-poly-v2-page .exhibition-index {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-index.is-visible {
  opacity: 1;
}

@media (max-width: 1024px) {
  body.roly-poly-v2-page .index-item img {
    height: 11vh !important;
    max-width: none !important;
    object-fit: contain !important;
  }

  /* Fewer, wider columns fit a narrower screen better — a row with
     more items than columns just wraps onto additional grid rows
     automatically. !important is needed because each row sets its
     own grid-template-columns: repeat(8, ...) inline, which
     otherwise always wins over an external stylesheet rule
     regardless of media query. */
  body.roly-poly-v2-page .index-group-row {
    grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
    column-gap: 3vw !important;
    row-gap: 4.5vh;
  }
}

@media (max-width: 767px) {
  body.roly-poly-v2-page .index-item img {
    height: 9vh !important;
  }

  body.roly-poly-v2-page .index-group-row {
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  }
}

/* =========================
   INDEX LIGHTBOX
   Same centred/contained placement logic as the showreel carousel;
   blurs the rest of the page using the same mechanism as the Info
   dropdown (body.info-open, elsewhere in this file).
========================= */
body.roly-poly-v2-page .index-lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 3000;
  align-items: center;
  justify-content: center;
  cursor: zoom-out;
}

body.roly-poly-v2-page .index-lightbox.show {
  display: flex;
}

body.roly-poly-v2-page .index-lightbox img {
  max-width: 80vw;
  max-height: 80vh;
  object-fit: contain;
  cursor: default;
}

body.roly-poly-v2-page .index-lightbox-caption {
  display: none;
  position: fixed;
  /* left/top are set inline by positionLightboxCaption() in the
     page's own <script>, pinning this outside/below the actual
     rendered image — flush with its left edge, with a gap to its
     bottom edge matching header's own top: 1em (object-fit: contain
     means the image's real box varies by its aspect ratio, not a
     fixed viewport offset) — these are just a sane fallback before
     that first runs. */
  left: 1em;
  top: 3rem;
  z-index: 3001;
}

body.roly-poly-v2-page .index-lightbox-caption.show {
  display: block;
}

body.roly-poly-v2-page .index-lightbox-caption-text {
  /* Same size as the nav bar / body text — was 1.5em. */
  font-size: 1em;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.roly-poly-v2-page.index-lightbox-open main {
  filter: blur(10px);
}

body.roly-poly-v2-page.index-lightbox-open {
  overflow: hidden;
}

/* =========================
   SECTION 6 — CREDITS
========================= */
body.roly-poly-v2-page .exhibition-credits {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* CSS background-image, not an <img class="credits-bg-image">
     (tried first) — the <img> + object-fit: cover version, sitting
     next to .credits-content's mix-blend-mode: exclusion, rendered
     with the image's paint mysteriously clipped to a fraction of its
     own layout box at some viewport sizes (confirmed via
     getBoundingClientRect() reporting the full correct box while the
     actual painted pixels didn't fill it) — a background-image on
     the section itself uses a different rendering path and doesn't
     hit this. background-color stays as a fallback for before the
     image loads / if it fails. */
  background-color: #000;
  background-image: url('/imgs/darya-diamond-credits-banner-006.jpg');
  background-size: cover;
  background-position: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

body.roly-poly-v2-page .credits-content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.8em;
  /* Same overlap-on-image convention as Section 2's intro/caption
     text and Section 8's prints text: white + mix-blend-mode:
     exclusion inverts whatever pixel sits beneath it (black text
     would just stay invisible, since black + anything = that thing,
     unchanged). */
  mix-blend-mode: exclusion;
}

body.roly-poly-v2-page .credits-block {
  text-align: center;
}

body.roly-poly-v2-page .credits-role,
body.roly-poly-v2-page .credits-name {
  /* 0.9x the previous 2em (was ~24px, now ~21px). */
  font-size: 1.8em;
  line-height: 1;
  /* Grayscale antialiasing renders crisper, less chunky edges for
     this heavy weight at a larger size — same treatment used
     elsewhere in this file for the same reason (see
     .description-colophon). */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #fff;
}

body.roly-poly-v2-page .credits-role {
  text-transform: uppercase;
}

body.roly-poly-v2-page .credits-name {
  /* !important needed to win over the temporary sitewide "TEMP
     PREVIEW — ALL CAPS EVERYWHERE" override further down this file
     (body * { text-transform: uppercase !important }) — remove this
     !important too if that block is ever deleted for good. Title
     case (first letter of each word), distinct from the
     always-uppercase role label above each one. */
  text-transform: capitalize !important;
}

body.roly-poly-v2-page .credits-name-lowercase {
  /* Exempts small joining words (e.g. "and") from the capitalize
     rule above — CSS text-transform capitalizes every word
     regardless of how it's cased in the HTML source, so lowercasing
     the source text alone has no effect here. */
  text-transform: lowercase !important;
}

body.roly-poly-v2-page .credits-name a {
  color: inherit;
  text-decoration: none;
  transition: color 0.3s ease;
  /* Without this, the universal "TEMP PREVIEW — ALL CAPS EVERYWHERE"
     override (body * { text-transform: uppercase !important }) below
     matches the <a> itself and wins over the capitalize set on its
     parent .credits-name — inherited values always lose to any rule
     that directly targets the element, important or not. */
  text-transform: capitalize !important;
}

body.roly-poly-v2-page .credits-name a:hover {
  color: #7f7f7f;
}

body.roly-poly-v2-page .exhibition-credits {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-credits.is-visible {
  opacity: 1;
}

/* While Section 6 (credits) is the active section, the fixed header
   and footer nav get the same exclusion-blend treatment as the
   credits text itself and as .detail-active above — plain white used
   to be enough here back when credits sat on a solid black
   background (nav text stays legible against solid black either
   way), but now that it's a photo (mixed light/dark), plain white
   goes invisible over the image's light areas, exactly the problem
   .detail-active's own exclusion blend above already exists to
   solve. .credits-active is toggled on body by the pager JS, same
   pattern as .detail-active. */
body.roly-poly-v2-page .logo,
body.roly-poly-v2-page .header-nav a,
body.roly-poly-v2-page .copyright,
body.roly-poly-v2-page .bottom-mini-nav a {
  transition: color 0.4s ease;
}

/* mix-blend-mode goes on `header` itself, not each nav link — see the
   matching comment on .detail-active above for why (a blend-mode set
   on a descendant of header can't reach past header's own stacking
   context out to the real photo, and just renders flat/inert). The
   dropdown-panel readability problem that using `header` reintroduces
   is fixed with `isolation: isolate` directly on those panels
   instead (see below), not by moving the blend-mode off header. */
body.roly-poly-v2-page.credits-active header,
body.roly-poly-v2-page.credits-active .copyright,
body.roly-poly-v2-page.credits-active .bottom-mini-nav {
  mix-blend-mode: exclusion;
}

body.roly-poly-v2-page.credits-active .logo,
body.roly-poly-v2-page.credits-active #info-toggle,
body.roly-poly-v2-page.credits-active .work-toggle,
body.roly-poly-v2-page.credits-active .exhibitions-toggle,
body.roly-poly-v2-page.credits-active .press-toggle,
body.roly-poly-v2-page.credits-active .contact-nav > a,
body.roly-poly-v2-page.credits-active .shop-link-top-right,
body.roly-poly-v2-page.credits-active .copyright,
body.roly-poly-v2-page.credits-active .bottom-mini-nav a {
  color: #fff;
}

/* =========================
   SECTION 7 — VIEW MORE EXHIBITIONS
========================= */

/* left uses the same 1rem inset as the fixed header (header itself
   sits at top:1em/.logo at margin-left:1em, both measured against the
   root font-size — rem here instead of em specifically so this
   element's own font-size:2em below doesn't double it), so the title
   lines up horizontally with the logo/nav above it, same as
   .more-exhibitions-row below (also inset 1em from the padding box,
   not the padding-inside edge — see its own comment).

   top is set entirely by positionMoreRow() in this page's own
   <script> (search for "moreTitle") — it sits directly above
   .more-exhibitions-row with a 1rem gap between them, so it has to
   track wherever JS places that row (itself centered dynamically in
   the space above .more-disclaimer) rather than a fixed value.

   font-size: 2em is exactly 2x the nav bar's own text size, same em
   context — same technique used for the Section 3 description
   paragraph (body.roly-poly-v2-page .description-text p) elsewhere in
   this file. text-transform: none overrides the site-wide uppercase
   (html, body) — this one reads in lowercase by design. */
body.roly-poly-v2-page .more-exhibitions-title {
  position: absolute;
  left: 1rem;
  font-size: 2em;
  margin: 0;
  text-transform: none;
}

body.roly-poly-v2-page .exhibition-more {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  padding: 0 1em;
  /* display:flex + justify-content:center is now just the fallback
     (before JS runs, or if unavailable) — .more-exhibitions-row is
     positioned absolutely and centered precisely by JS (see
     positionMoreRow() in roly-poly-v.2.html) between this section's
     own top and the top of .more-disclaimer, which is itself taken
     out of this flow entirely (position: absolute, below) so it can
     be anchored to the bottom independently of the row. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

body.roly-poly-v2-page .more-exhibitions-row {
  /* Plain block, not flex — Flickity inserts its own
     .flickity-viewport as a direct child of this element. Making
     this element display:flex turns that viewport into a flex item,
     which then shrink-to-fits against its own position: absolute
     descendant (the slider + cells) and collapses to 0 width, same
     as the .showreel-carousel elsewhere in this file, which is
     exactly why that one is also plain width: 100% and nothing
     else.
     Positioned (rather than relying on the parent's justify-content:
     center) so JS (positionMoreRow() in roly-poly-v.2.html) can
     center it specifically in the space between the section's own
     top and the top of .more-disclaimer below — not just vh-center
     across the section's full height, which risks overlapping the
     disclaimer once its text wraps onto more lines on a narrow
     screen. left/right + no top yet: a reasonable fallback (this
     element's own static position) before JS runs or if it's
     unavailable.
     left/right: 1em, not 0 — an absolutely positioned element's
     left/right offsets are relative to the containing block's
     PADDING box edge (i.e. outside .exhibition-more's own 1em
     padding, flush with its border/true edge), not the inside of
     that padding — so left:0 here was sitting flush at the section's
     true edge, ignoring the 1em inset entirely instead of matching
     the nav's own inset. */
  position: absolute;
  left: 1em;
  right: 1em;
  width: auto;
  height: 75vh;
}

/* Flickity (freeScroll) sets position: absolute on each cell for
   its own drag positioning — giving the cell an explicit height in
   real units (not a %) up front means the wrap/img heights below
   can safely resolve against it regardless of that positioning. */
body.roly-poly-v2-page .more-exhibitions-item {
  display: block;
  /* Natural width, not equal columns — sized to each image's own
     aspect ratio at this fixed height, not cropped/contained into a
     uniform box. Taller than .more-exhibitions-image-wrap below —
     this is the image's own height PLUS room for the caption
     underneath, so the caption isn't clipped by Flickity's
     .flickity-viewport (which sizes itself to match this cell
     height, with its own overflow: hidden). Matches the portrait
     image height used by the Section 4 showreel carousel, per
     feedback to size this row the same as "other carousels". */
  height: calc(75vh + 0.5em + 1.1142857em);
  /* The generic, unscoped .carousel-cell rule (used by the other
     Flickity carousels on this page) sets min-height: 70vh — since
     this rule never touches min-height at all, that declaration
     wins by default regardless of this rule's higher specificity,
     silently overriding the height above. Reset it back to 0. */
  min-height: 0;
  /* Flickity cells are position: absolute, so a flex/grid gap on
     the parent has no effect — this is the gap between cells
     instead, matching the same 1em inset used at the page's own
     left/right edges (see .more-exhibitions-row), per feedback. */
  margin-right: 1em;
  text-decoration: none;
  color: inherit;
}

body.roly-poly-v2-page .more-exhibitions-image-wrap {
  position: relative;
  /* Fixed, not 100% of the item — the item is now taller than the
     image alone to make room for the caption below. Matches the
     Section 4 showreel's portrait height (75vh). */
  height: 75vh;
}

body.roly-poly-v2-page .more-exhibitions-item img {
  height: 100%;
  width: auto;
  max-width: none;
  max-height: none;
  object-fit: contain;
  display: block;
}

/* Plain grey highlight overlay on hover — no scale/grow, no
   pop-up caption text, just a flat tint over the image. */
body.roly-poly-v2-page .more-exhibitions-hover-overlay {
  position: absolute;
  inset: 0;
  /* White, not grey — a semi-transparent white tint lightens the
     image on hover instead of muting/darkening it. */
  background: rgba(255, 255, 255, 0.45);
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

body.roly-poly-v2-page .more-exhibitions-item:hover .more-exhibitions-hover-overlay {
  opacity: 1;
}

body.roly-poly-v2-page .more-exhibitions-caption {
  margin-top: 0.5em;
  font-size: 1em;
  text-align: left;
}

body.roly-poly-v2-page .more-disclaimer {
  /* Independently anchored to the bottom — same left/right insets
     as the row above (1em, matching the section's own padding),
     same bottom clearance as .detail-overlay's on Section 2 (2.63rem
     above the fixed .copyright disclaimer), regardless of where the
     row itself centers above it. */
  position: absolute;
  left: 1em;
  right: 1em;
  bottom: 2.63rem;
  /* Full width — stretches edge to edge, wrapping onto natural line
     breaks instead of a constrained narrower column. */
  width: auto;
}

body.roly-poly-v2-page .more-disclaimer-heading {
  /* 1.1x the nav bar's font-size. */
  font-size: 1.1em;
  line-height: 1;
  /* No gap at all — reads as if it's just the first line of the
     paragraph below with a <br>, not a heading with space under it. */
  margin: 0;
  /* Site-wide text is uppercase (html, body); this reads "Please
     note" (sentence case) instead. */
  text-transform: none;
}

body.roly-poly-v2-page .more-disclaimer p {
  /* Same as the nav bar's font-size. */
  font-size: 1em;
  line-height: 1;
  letter-spacing: normal;
  /* One continuous paragraph now (not two with a gap between) —
     no margin needed since there's nothing else after it. */
  margin: 0;
  /* Global html/body sets text-transform: uppercase — the legal
     copy reads better in sentence case, same override used for the
     credits names above. */
  text-transform: none;
}

/* Extra line break before "All other rights..." — now shown on both
   desktop and mobile (originally mobile-only, before the disclaimer
   read in all caps). */
.disclaimer-mobile-break {
  display: inline;
}

/* Extra line break after "...copyright of Darya Diamond." — mobile
   only, unlike the one above; desktop keeps that sentence running
   straight into the next. */
.disclaimer-break-mobile-only {
  display: none;
}

/* On mobile, drop the middle "you may not crop..." sentence entirely
   (desktop keeps all three) and render the remaining two at 0.8em —
   desktop's font-size (1.1em) is untouched. */
@media (max-width: 767px) {
  .disclaimer-desktop-only {
    display: none;
  }

  .disclaimer-break-mobile-only {
    display: inline;
  }

  body.roly-poly-v2-page .more-disclaimer p {
    font-size: 0.8em;
  }
}

/* shop.html reuses this same disclaimer block (typography above
   still applies via body.roly-poly-v2-page, added to its body class
   alongside .shop-page for exactly this kind of shared-component
   reuse), but the positioning above doesn't fit it: that's
   position: absolute, meant to sit anchored to the bottom of
   .exhibition-more (a position: relative, 100vh section) regardless
   of where the row above it centers — shop.html has no such section
   wrapper, it's one continuously scrolling page, so this is placed
   in-flow instead, at the true end of the page content, right after
   the last product. Higher specificity (three classes, not two) than
   the rule above so it wins regardless of source order. */
body.shop-page.roly-poly-v2-page .more-disclaimer {
  position: static;
  margin: 3rem 1em 0;
  /* padding-bottom, not margin-bottom — this is the last element
     inside <main>, and a last child's trailing margin can collapse
     through/escape its parent's own bottom edge instead of adding
     real extra height, which left this still flush against the
     fixed .copyright/.bottom-mini-nav/.bottom-index (they sit at
     bottom:1em regardless of scroll position, so whatever's at the
     true end of the page's own content needs guaranteed clearance
     from them). Padding never collapses, so this reliably reserves
     real scrollable space. 2.63rem matches the same bottom clearance
     .more-disclaimer already uses on the exhibition pages (see
     "bottom: 2.63rem" above, anchored above their own fixed
     .copyright) — kept tight, roughly one line's height, not the
     8rem this started at. */
  padding-bottom: 2.63rem;
}

/* A legacy, unscoped "EXHIBITION ONLY MOBILE & TABLET HERO & CLOSING
   CAROUSEL FIX" rule elsewhere in this file (.exhibition-article
   .carousel-cell img, @media max-width:1024px) forces max-width:100%
   and height:auto !important on any img inside a .carousel-cell —
   written for the old single-page site's hero/closing carousels, but
   .more-exhibitions-item carries that same shared class, so it
   bleeds through here too. Combined with the cell's own (JS-computed,
   desktop-sized) width, that turned height:auto into a much taller,
   width-driven box than this row's own height, spilling the image
   out past the bottom of .more-exhibitions-image-wrap. !important
   needed to actually beat the legacy rule's own !important. */
@media (max-width: 1024px) {
  body.roly-poly-v2-page .more-exhibitions-item img {
    max-width: none !important;
    height: 100% !important;
    margin: 0 !important;
  }
}

/* Matches the Section 4 showreel's own mobile height (55vh, down
   from 75vh at tablet/desktop) — per feedback to keep this row the
   same height as the other carousels at each breakpoint, not just
   desktop's. */
@media (max-width: 767px) {
  body.roly-poly-v2-page .more-exhibitions-row {
    height: 55vh;
  }

  body.roly-poly-v2-page .more-exhibitions-image-wrap {
    height: 55vh;
  }

  body.roly-poly-v2-page .more-exhibitions-item {
    height: calc(55vh + 0.5em + 1.1142857em);
  }
}

body.roly-poly-v2-page .exhibition-more {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-more.is-visible {
  opacity: 1;
}

/* =========================
   SECTION 8 — PRINTS / CONTACT
========================= */
body.roly-poly-v2-page .exhibition-prints {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* dvh (dynamic viewport height) accounts for mobile browsers'
     address bar showing/hiding, which changes the real usable
     viewport height mid-scroll -- a plain 100vh target computed at
     gesture-start can otherwise go stale by the time the section
     pager's scroll animation finishes, landing the section
     slightly off from the top of the screen. Falls back to the
     100vh above on browsers that don't support dvh. */
  height: 100dvh;
  box-sizing: border-box;
  /* Grid (not flex + position: absolute + transform) — .prints-image
     and .prints-text both sit in the same single cell (grid-area:
     1 / 1 on each, below) and place-items: center centers each of
     them independently on both axes with no transform involved.
     This matters specifically because mix-blend-mode on
     .prints-text-inner needs to blend against .prints-image, and a
     transform on any ancestor between them creates a new stacking
     context that isolates the blend from anything outside it —
     translateY(-50%) on .prints-text was exactly that ancestor,
     which was silently breaking the blend (rendering as flat white
     instead of inverting against the image). */
  display: grid;
  place-items: center;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* isolation: isolate groups the image + text into one blending
     context, and the explicit white background gives
     mix-blend-mode something opaque to actually blend against
     everywhere the text ISN'T over the image. Without a real
     painted backdrop here, the "white page" the text sees outside
     the image is transparent as far as blending is concerned —
     blending against transparent is a no-op, so the text just
     rendered in its own flat white color there (invisible against
     the real white page underneath), which is exactly why it
     looked like it only "worked" on the photo and cut off at its
     edge. */
  isolation: isolate;
  background: #fff;
}

body.roly-poly-v2-page .prints-image {
  grid-area: 1 / 1;
  /* Same height as the portrait cell in Section 4's showreel
     carousel. */
  height: 75vh;
  width: auto;
  max-width: none;
  max-height: none;
  object-fit: contain;
  display: block;
}

body.roly-poly-v2-page .prints-text {
  /* Same grid cell as the image — place-items: center on the
     parent centers this on both axes with no transform, unlike the
     old position: absolute + top: 50% + transform: translateY(-50%)
     approach (see the note on .exhibition-prints above for why that
     specifically broke the mix-blend-mode below). Sized to its own
     content (width: max-content, not a fixed/full width) so it
     doesn't stretch to fill the grid cell — each line inside still
     starts flush left (default text-align), it's the block as a
     whole that's centered, not the text within it.
     Deliberately stays at the base (nav-matching) 1em font-size —
     the 2x enlargement lives on .prints-text-inner instead, so
     max-width below resolves against the nav's own 1em (10.24px),
     not a doubled one (an em unit resolves against its own
     element's font-size, so max-width: calc(100% - 2em) here would
     otherwise mean 2x the nav's inset, not 1x — same gotcha as the
     showreel lightbox caption elsewhere in this file). */
  grid-area: 1 / 1;
  width: max-content;
  /* Keeps the same 1em clearance from the viewport edge as the nav
     bar's own inset on narrow screens, even though the block is
     centered and sized to its own (otherwise wider) content. */
  max-width: calc(100vw - 2em);
  box-sizing: border-box;
  /* Site-wide text is uppercase (html, body); this reads in normal
     sentence case instead — inherits down to .prints-text-inner and
     the shop/email/phone links inside it. */
  text-transform: none;
}

body.roly-poly-v2-page .prints-text-inner {
  display: inline-block;
  /* 2x the nav bar / body text's font-size. */
  font-size: 2em;
  line-height: 1;
  /* Same overlap-on-image convention as Section 2's intro/caption
     text: white + mix-blend-mode: exclusion inverts whatever pixel
     sits beneath it (black text would just stay invisible, since
     black + anything = that thing, unchanged). */
  color: #fff;
  mix-blend-mode: exclusion;
}

body.roly-poly-v2-page .prints-text a {
  /* Global a, a:link, ... rule forces text-decoration: none
     !important site-wide — restore the underline just here to
     match the reference (shop link, email, phone all underlined). */
  text-decoration: underline !important;
  color: inherit;
}

body.roly-poly-v2-page .exhibition-prints {
  opacity: 0;
  transition: opacity 0.9s ease;
}

body.roly-poly-v2-page .exhibition-prints.is-visible {
  opacity: 1;
}

/* On narrow screens, the portrait/height-driven sizing above leaves
   visible white margins on either side of the image instead of using
   the screen's width — switch to width-driven sizing instead (full
   width, minus the same 1em inset the nav uses on each side), and
   left-align the text block to that same margin instead of centering
   it as a block, per feedback. */
@media (max-width: 767px) {
  body.roly-poly-v2-page .prints-image {
    width: calc(100vw - 2em);
    height: auto;
    max-width: calc(100vw - 2em);
  }

  body.roly-poly-v2-page .prints-text {
    justify-self: start;
    margin-left: 1em;
    width: calc(100vw - 2em);
    max-width: calc(100vw - 2em);
  }
}

stripe-buy-button {
  margin-top: 16px;
  display: inline-block;
}

.sold-out {
  opacity: 0.4;
  margin-top: 16px;
}

.info p {
  margin-bottom: 0.6em;
}

.info p:last-child {
  margin-bottom: 0;
}

.landing-carousel.shop-landing {
  height: 100vh;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.landing-carousel .carousel,
.landing-carousel .flickity-viewport {
  height: 100%;
}

/* Scoped to .shop-landing (currently unused) — these were previously
   unscoped and applied to every .carousel-cell site-wide, which is
   what collapsed the exhibition hero/context carousels to 0 height
   on mobile after their first Flickity-fade transition. */
.landing-carousel.shop-landing .carousel-cell {
  width: 100%;
  height: 100%;
}

.landing-carousel.shop-landing .carousel-cell img {
  width: 100%;
  height: 100%;
  object-fit: contain; /* use cover if you want edge-to-edge */
  display: block;
}



.landing-carousel.shop-landing .carousel-cell img {
  object-position: center 45%;
}





/* =========================
   EXHIBITION ONLY
   MOBILE & TABLET HERO & CLOSING CAROUSEL FIX
   (Padding untouched)
========================= */

@media (max-width: 1024px) {

  /* Allow carousel / hero to collapse to content.
     NOTE: .carousel-cell is deliberately excluded here — Flickity
     manages those with position:absolute + inline transforms for its
     fade/slide effect, and forcing position:relative + height:auto
     on them broke the hero/context carousels on mobile: cells fell
     back to stacking in normal document flow (each reserving its own
     height), so after the first autoplay transition the newly
     selected slide rendered far below the visible carousel area
     instead of in place, making the carousel appear to "disappear". */
  .exhibition-article .landing-carousel,
  .exhibition-article .exhibition-hero-carousel,
  .exhibition-article .exhibition-closing-carousel,
  .exhibition-article .image-wrapper {
    position: relative !important;
    height: auto !important;
    min-height: 0 !important;
    padding: 1em !important;
    margin: 0em !important;
    padding-left: 1em;

  }

  /* Images control height naturally */
  .exhibition-article .landing-carousel img,
  .exhibition-article .exhibition-hero-carousel img,
  .exhibition-article .exhibition-closing-carousel img,
  .exhibition-article .carousel-cell img,
  .exhibition-article .image-wrapper img {
    max-width: 100% !important;
    height: auto !important;
    object-fit: contain;
    display: block !important;
    margin: 0 auto !important;
  }

  /* Text sits directly below images */
  .exhibition-article .exhibition-text,
  .exhibition-article .text-block {
    margin-top: 0em !important;
    padding: 2em !important;
  }

  /* Exhibition dates right under closing carousel */
  .exhibition-article .exhibition-dates {
    margin-top: 0.5em !important;
    margin-bottom: 6em !important;
    padding: 0 !important;
    text-align: center !important;
  }

  /* Remove any leftover gaps from hero padding */
  .exhibition-article .exhibition-hero-carousel {
    padding-top: 0em !important;
    padding-bottom: 0em !important;
    
  }

  /* Re-enable natural scrolling */
  body.landing-page {
    overflow: auto !important;
  }

  /* Make exhibition text full-width and align with margins */
  .exhibition-article .exhibition-text,
  .exhibition-article .text-block,
  .exhibition-article .exhibition-dates {
    width: 100% !important;
    max-width: 100% !important;
    padding-left: 0.5em !important;
    padding-right: 1em !important;
    box-sizing: border-box;
    margin-left: 0 !important;
    margin-right: 0 !important;
  
  }

  
  

  /* Optional: slightly tighter spacing between blocks */
  .exhibition-article .exhibition-text,
  .exhibition-article .text-block {
    margin-bottom: 1.5em !important;
    
  }

}


/* -------------------------
   Responsive for tablet / mobile
------------------------- */
@media (max-width: 1024px) {
  .info {
    display: grid;               /* keep grid for easier alignment */
    grid-template-columns: 1fr;  /* single column that fills width */
    margin-left: 0;              /* remove left margin */
    padding-left: 1em;  /* small horizontal padding */
    margin-right:1em;              /* small horizontal padding */
    /* No gap here — .info .para's own margin-bottom (below) already
       sets the row spacing, matching desktop's rhythm exactly. A gap
       here on top of that margin double-spaced every paragraph right
       at this breakpoint, breaking rhythm with the CV rows and with
       wrapped lines within each paragraph. */
  }

  .info .para {
    grid-column: 1 / -1;         /* span full width of single column */
    width: 100%;                 /* ensure it fills container */
    margin: 0 0 0.6em 0;         /* spacing between rows, matches desktop rhythm */
  }

  .info .para-1 {
    margin-top: 0.5em;
  }
}

@media (max-width: 1024px) {
  .cv {
    /* .cv's desktop grid-column: 1 / span 14 (see the un-media-
       queried rule above) was leaking into mobile too — .info's
       grid collapses to a single explicit column here, but that
       span-14 still demanded 14 columns' worth of grid area, so the
       grid was silently generating 13 extra implicit (zero-width)
       columns just to satisfy it. That corrupted .para's "1 / -1"
       full-width span as well, since -1 then resolved to the end of
       those 14 columns instead of the single real one. Resetting to
       1 / -1 here removes the implicit columns entirely. */
    grid-column: 1 / -1;
    margin-left: 0;
    margin-right: 1em;             /* optional padding */
    padding-right: 1em;
  }
}

/* MOBILE INFO PANEL — SCROLLS INTERNALLY, BACKGROUND STAYS FIXED
   The background carousel/page used to scroll along with the Info
   panel's content on mobile (the panel had no height limit, so it
   just grew and dragged the whole page down with it). Now the panel
   is pinned to the viewport below the header (top is set inline via
   JS — see positionMobileInfoPanel() — since the header's own height
   isn't a fixed, known value in CSS) and scrolls internally, while
   body.info-open freezes the real page scroll behind it. The bottom
   nav (.bottom-mini-nav/.copyright) is relocated into #info-section
   by JS (moveNavIntoInfoOnMobile()) when the panel opens, so it
   naturally ends up after the CV's last block (Curriculum
   Vitae/Download) instead of floating fixed over the panel; these
   overrides undo their normal fixed-to-viewport positioning only
   while they're actually nested in there. */
@media (max-width: 1024px) {
  body.info-open {
    overflow: hidden;
  }

  #info-section.show {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Explicit auto (not just omitted) — the base #info-section rule
       (unconditional, higher specificity via its ID) sets
       width: 100vw, which otherwise keeps winning here since simply
       omitting width doesn't un-set it. left:0/right:0 need width:
       auto specifically to trigger the browser's normal "solve for
       width" behavior for a fixed element, which correctly subtracts
       .info's own margin-right:1em (further up, for mobile) — with
       width pinned to 100vw instead, that same margin pushed the
       box's right edge a full em past the screen edge, clipping the
       last bit of every line of text. */
    width: auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* Matches .copyright/.bottom-mini-nav's own global bottom:1em —
       was 2em, sitting noticeably looser than that same disclaimer's
       fixed-position gap everywhere else on the site. */
    padding-bottom: 1em;
  }

  /* Copyright and mini-nav share one row here (see
     moveNavIntoInfoOnMobile() in the page's own <script>), so they
     sit side by side the same way they do globally (fixed bottom-left/
     bottom-right) instead of each stacking as its own full-width grid
     row, with mini-nav ending up above the disclaimer instead of
     beside it. */
  #info-section .info-mobile-footer-row {
    grid-column: 1 / -1;
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1em;
  }

  #info-section .copyright {
    position: static;
    margin: 0;
  }

  #info-section .bottom-mini-nav {
    position: static;
    margin: 0;
    background: transparent;
    padding: 0;
  }
}

/* Section 2's full-bleed detail image (further down this file) turns
   .copyright/.bottom-mini-nav white + mix-blend-mode: exclusion via
   body.detail-active, so they stay readable over the photo — but that
   class is purely scroll-position-driven and doesn't turn off just
   because Info is opened on top of it. Once these two are relocated
   inside #info-section on mobile (moveNavIntoInfoOnMobile(), above),
   that white-on-the-panel's-white-background became invisible. The
   #info-section id selector outweighs body.detail-active's class
   selectors regardless of source order, so this wins whenever they're
   actually inside the panel, whatever body classes happen to be set. */
#info-section .copyright,
#info-section .bottom-mini-nav,
#info-section .bottom-mini-nav a {
  color: #000;
  mix-blend-mode: normal;
}

.context-text {
  padding-right: 0.5em;
  padding-left: 0.3em;
  box-sizing: border-box;
}


a, a:link, a:visited, a:hover, a:active, a:focus {
  text-decoration: none !important;
  outline: none; /* optional: removes the focus outline too */
}


/* =========================
   WORK IMAGE VIEWER
========================= */

.image-container {
  position: fixed;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1500;
  pointer-events: none;
}

.image-wrapper {
  display: none;
  text-align: left;
}

.image-wrapper.active {
  display: block;
}

/* On mobile, tapping a Work title shows the preview here instead of
   following its link straight through (see isMobileWorkLayout() in
   the page's own <script>) — the image itself becomes the real link,
   tappable to actually navigate. .image-container (this element's
   parent) is pointer-events: none so the preview never blocks clicks
   to whatever's underneath the rest of the time; this switches it
   back on only while that mobile tap-preview state is active. */
#main-image-wrapper.mobile-tap-preview {
  pointer-events: auto;
  cursor: pointer;
}

.image-wrapper img {
  max-width: 80vw;
  max-height: 70vh;
  object-fit: contain;
  opacity: 1;
}

.image-wrapper figcaption {
  margin-top: 0.3em;
}

/* =========================
   TEMP PREVIEW — ALL CAPS EVERYWHERE
   Overrides every text-transform: none set elsewhere in this file
   (info panel, shop descriptions, captions, prints text, etc.) so the
   whole site previews in caps for comparison. Remove this block to
   revert to the per-section casing above.
========================= */
body * {
  text-transform: uppercase !important;
}