/* ==========================================================================
   CONTRA — plain-CSS port of the Tailwind build (src/app/globals.css +
   every component's className list). No build step: this file is meant to
   be dropped on any PHP host as-is. Values are copied 1:1 from the Next.js
   source rather than re-derived, so it should render pixel-identical.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   Fonts. Self-hosted — licensing is UNRESOLVED, see assets/fonts/FONTS.md
   before any public deploy of this PHP build.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: "Items Condensed";
  src: url("/assets/fonts/items-condensed-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "Items Demo Condensed";
  src: url("/assets/fonts/items-demo-condensed-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: block;
}
@font-face {
  font-family: "Acumin Pro";
  src: url("/assets/fonts/acumin-pro-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

/* ---------------------------------------------------------------------------
   Design tokens — copied from globals.css's @theme block.
   --------------------------------------------------------------------------- */
:root {
  color-scheme: light;

  --color-ink: #000000;
  --color-paper: #ffffff;
  --color-muted: #bdbdbd;

  --font-display: "Items Condensed", Georgia, serif;
  --font-italic: "Items Demo Condensed", Georgia, serif;
  --font-ui: "Acumin Pro", system-ui, sans-serif;

  --text-body: 2.25rem;      /* 36px, 32px on phone */
  --text-nav: 1.5rem;        /* 24px */
  --text-fine: 1.125rem;     /* 18px */
  --text-body-sm: 1rem;      /* 16px */

  --footer-padding-y: 1.75rem;  /* 28px */
  --footer-height: 6.25rem;     /* 100px */

  --leading-tight: 1;
  --leading-normal: 1.2;
  --tracking-nav: -0.03em;

  --spacing-1: 0.5rem;   /* 8 */
  --spacing-2: 1.25rem;  /* 20 */
  --spacing-3: 2.5rem;   /* 40 */
  --spacing-4: 3.5rem;   /* 56 */
  --spacing-5: 5.5rem;   /* 88 */
  --spacing-6: 6.25rem;  /* 100 */

  --size-stage: 649px;
  --stage-reserve: 300px;
  --control-reserve: 13rem;
  --size-wordmark: 505px;
  --content-column: min(292px, calc((100vw - var(--size-wordmark)) / 2));
  --size-control: 40px;
}

* {
  box-sizing: border-box;
}

html, body {
  background: var(--color-paper);
  color: var(--color-ink);
  margin: 0;
}

body {
  font-family: var(--font-display);
  font-size: var(--text-body);
  line-height: var(--leading-tight);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
}

/* The Tailwind build resets heading font-size/weight to `inherit` (its
   "preflight" base layer) — nothing here does that on its own, so a bare
   <h1> (the year page's heading) was picking up the browser's default UA
   size (~2em) on top of the 36px body size instead of matching it. */
h1, h2, h3, h4, h5, h6 {
  font-size: inherit;
  font-weight: inherit;
  margin: 0;
}

/* Browsers don't reliably apply the UA [hidden]{display:none} rule to
   inline <svg> (the menu's open/close icons) — verified in-browser: the
   `hidden` IDL property was true but computed display stayed "inline".
   Force it so every hidden-attribute toggle in site.js actually hides. */
[hidden] {
  display: none !important;
}

@media (max-width: 1099px) {
  :root { --size-wordmark: 410px; }
}

@media (max-width: 767px) {
  body { font-size: 2rem; } /* 32px */
  :root {
    --size-wordmark: min(320px, calc(100vw - 200px));
  }
}

/* --header-height is decoupled from the --size-wordmark breakpoint above:
   the fixed white wordmark bar itself (.wordmark, .wordmark-spacer below)
   now runs up to 959px, not just 767px, so this needs to be defined across
   that whole range or the spacer collapses to 0 between 768-959px and the
   fixed bar overlaps whatever follows it. */
@media (max-width: 959px) {
  :root {
    --header-height: 8rem;
  }
}

@media (max-width: 639px) {
  :root { --control-reserve: 9.5rem; }
}

@media (max-width: 480px) {
  :root {
    --content-column: calc(100vw - 2 * var(--spacing-2));
  }
}

/* Hover is an instantaneous colour switch — no transition, measured. */
a {
  color: inherit;
  text-decoration: none;
}
a:hover {
  color: var(--color-muted);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   Page entrance — PageFade.tsx: opacity only, ~750ms easeInOut.
   --------------------------------------------------------------------------- */
body.page-fade {
  opacity: 0;
  animation: page-fade 0.75s ease-in-out forwards;
}
@keyframes page-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ---------------------------------------------------------------------------
   PageFrame — full-height frame used by the home page and the year lists.
   --------------------------------------------------------------------------- */
.page-frame {
  position: relative;
  height: 100vh;
  overflow: hidden;
  background: var(--color-paper);
}

.page-frame__content {
  position: relative;
  width: var(--content-column);
  padding: var(--spacing-2);
}
.page-frame__content--full {
  width: 100%;
}
@media (min-width: 640px) {
  .page-frame__content {
    padding: var(--spacing-3);
  }
}

/* ---------------------------------------------------------------------------
   WordmarkMarquee — the drifting home-page animation. Structural only;
   assets/js/site.js measures the box and injects the @keyframes distance,
   same algorithm as WordmarkMarquee.tsx.
   --------------------------------------------------------------------------- */
.marquee {
  pointer-events: none;
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  overflow: hidden;
}
@media (max-width: 767px) {
  .marquee {
    justify-content: flex-start;
    /* Separates the (now left-aligned) mark from the viewport edge. */
    padding-left: 10px;
  }
}
.marquee__box {
  position: relative;
  width: var(--size-wordmark);
  height: 100%;
}
/* Mobile: sized directly, NOT via --size-wordmark — that token is shared
   with --content-column (About/exhibition/year-list text layout on other
   pages), so changing it here would resize unrelated pages too. This is
   its own formula, re-derived for left-alignment: the original
   `100vw - 200px` reserved 100px of clearance on BOTH sides because the
   mark used to be centered (menu-button clearance only actually needs the
   right side). Left-aligned with the 10px left offset above, the mark only
   needs `10px + width <= 100vw - 100px` to keep clear of the corner menu
   button, i.e. `width <= 100vw - 110px` — bigger than the old centered
   formula at every mobile width, which is the point: the left gap that
   formula reserved is no longer needed now that nothing sits there. */
@media (max-width: 767px) {
  .marquee__box {
    width: min(320px, calc(100vw - 110px));
  }
}
.marquee__stack {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}
.marquee__stack--animate {
  will-change: transform;
  animation-name: marquee-up;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* animation-duration set inline by marquee.js, per the measured width */
}
.marquee__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 505 / 164;
}
/* @keyframes marquee-up is injected by assets/js/site.js with the literal
   measured distance baked in, not defined here — Safari does not reliably
   animate a transform driven by a CSS custom property inside @keyframes. */

/* ---------------------------------------------------------------------------
   Wordmark — the static logo used everywhere else.
   --------------------------------------------------------------------------- */
.wordmark-spacer {
  display: none;
}
/* 959px, not 767px: the fixed white bar (below) now always shows across the
   whole "not desktop" range, not just phones — requested so there's no gap
   between the old ≤767px mobile bar and the new ≥960px desktop treatment
   below, where the exhibition detail page instead switches to a
   scroll-driven version of this same bar (see .exhibition-page .wordmark). */
@media (max-width: 959px) {
  .wordmark-spacer {
    display: block;
    height: var(--header-height);
  }
}

.wordmark {
  display: flex;
  justify-content: center;
  padding-top: var(--spacing-3);
  padding-bottom: var(--spacing-2);
}
@media (max-width: 959px) {
  .wordmark {
    position: fixed;
    inset-inline: 0;
    top: 0;
    z-index: 40;
    background: var(--color-paper);
  }
}
.wordmark__img {
  width: min(340px, 24vw);
  height: auto;
}
/* 15% bigger on mobile specifically — both terms of the min() scaled by
   1.15 (340px -> 391px, 24vw -> 27.6vw) so the relationship between the cap
   and the viewport-relative size stays the same, just larger. */
@media (max-width: 767px) {
  .wordmark__img {
    width: min(391px, 27.6vw);
  }
}

/* ---------------------------------------------------------------------------
   About page, >=960px: the same sticky shrink-on-scroll treatment as the
   exhibition detail header, minus the artist/title pairing — About has no
   equivalent text to combine it with, just the logo. Trigger point is the
   analogue of "the carousel image scrolls under the header": here it's the
   first paragraph of body copy (assets/js/site.js's generic
   initStickyShrinkHeader(), shared with the exhibition detail page).
   Applied directly to the existing .wordmark rather than a new element,
   since there's no second piece of content to lay out alongside it.
   --------------------------------------------------------------------------- */
@media (min-width: 960px) {
  .about-page .wordmark {
    position: sticky;
    top: 0;
    z-index: 40;
    padding-block: var(--spacing-3) var(--spacing-2);
    /* Same hue at 0/1 alpha, not the `transparent` keyword: transitioning
       `transparent` -> an opaque color is a known cross-browser
       interpolation gotcha. */
    background-color: rgb(255 255 255 / 0);
    transition: background-color 350ms ease, padding-block 350ms ease;
  }
  .about-page .wordmark__img {
    transition: transform 350ms ease;
    transform-origin: center;
  }
  .about-page .wordmark--scrolled {
    background-color: rgb(255 255 255 / 1);
    padding-block: var(--spacing-1);
  }
  .about-page .wordmark--scrolled .wordmark__img {
    transform: scale(0.5);
  }
}

/* ---------------------------------------------------------------------------
   Exhibition detail page only: past 960px, the plain logo bar and the
   separate artist/title block above are both replaced by
   .exhibition-sticky-header below — one combined bar carrying the logo AND
   the artist/title together. Hide the mobile-only pieces at this
   breakpoint; templates/exhibition-detail.php still renders them (for
   <960px) but they render server-side for both ranges, so CSS is what
   decides which one shows.
   --------------------------------------------------------------------------- */
@media (min-width: 960px) {
  .exhibition-page .wordmark,
  .exhibition-page__wordmark-wrap {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   Exhibition detail page, >=960px: the combined sticky header — logo
   centred, artist/title pinned to the left, both in one bar.

   Trigger: NOT scroll direction, and not "any scroll off zero" — compact
   mode starts once the carousel image has scrolled up to meet the header's
   own bottom edge (assets/js/site.js's initExhibitionHeaderScroll compares
   the stage row's rect against the header's height on every scroll tick),
   and reverses the instant that stops being true, in either scroll
   direction. Because that's a real position check rather than a
   continuously-reevaluated direction/delta, the class only actually flips
   twice per pass over that one point on the page — not on every pixel of
   scroll — which is what makes animating the bar's own height here safe:
   the previous version transitioned padding on every scroll pixel (via a
   `scrollY > 0` check with a direction comparison prone to firing
   repeatedly from momentum-scroll micro-reversals), and padding is a
   geometry property `position: sticky` can't hand off to the compositor —
   that combination was the actual jank, not padding-as-a-transition on its
   own. A rare, discrete toggle doesn't have that problem.

   Only the logo shrinks in compact mode — the artist/title stays full
   size, anchored near the top of the bar with a fixed offset (not
   vertically centered) so it doesn't get dragged around as the bar's
   height changes; it's expected to sit slightly below the (now shorter)
   white background once compact, over the image, rather than resize.
   --------------------------------------------------------------------------- */
.exhibition-sticky-header {
  display: none;
}
@media (min-width: 960px) {
  .exhibition-sticky-header {
    display: flex;
    position: sticky;
    top: 0;
    z-index: 40;
    align-items: center;
    justify-content: center;
    padding-block: var(--spacing-3) var(--spacing-2);
    /* Same hue at 0/1 alpha, not the `transparent` keyword: transitioning
       `transparent` -> an opaque color is a known cross-browser
       interpolation gotcha. */
    background-color: rgb(255 255 255 / 0);
    transition: background-color 350ms ease, padding-block 350ms ease;
  }
  .exhibition-sticky-header__meta {
    position: absolute;
    left: var(--spacing-3);
    top: var(--spacing-2);
    font-family: var(--font-display);
    line-height: var(--leading-tight);
  }
  .exhibition-sticky-header__title {
    font-family: var(--font-italic);
    font-style: italic;
  }
  .exhibition-sticky-header__logo-link {
    display: block;
  }
  .exhibition-sticky-header__logo-img {
    display: block;
    width: min(340px, 24vw);
    height: auto;
    transition: transform 350ms ease;
    transform-origin: center;
  }

  .exhibition-sticky-header--scrolled {
    background-color: rgb(255 255 255 / 1);
    padding-block: var(--spacing-1);
  }
  .exhibition-sticky-header--scrolled .exhibition-sticky-header__logo-img {
    transform: scale(0.5);
  }
}

/* ---------------------------------------------------------------------------
   SiteMenu — corner hamburger + expandable panel.
   --------------------------------------------------------------------------- */
.menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--color-paper);
  display: none;
}
@media (max-width: 481px) {
  .menu-backdrop:not([hidden]) {
    display: block;
  }
}

.menu {
  position: fixed;
  right: var(--spacing-3);
  top: var(--spacing-3);
  z-index: 50;
  text-align: right;
}
@media (max-width: 767px) {
  /* .menu itself (and so the toggle button, the first thing in its box)
     stays at the site-wide top: var(--spacing-3) — only the panel's links
     get extra top offset, not the button. */
  .menu__panel {
    /* Broken out of .menu's box entirely (which is only as wide as the
       button, right-anchored) so `text-align: center` centers across the
       whole viewport, not just within that narrow right-hand column —
       centered-but-still-on-the-right was the bug in the first version of
       this. `top: 118px` is a literal chosen offset for "more space above
       the links," independent of the button's own (unchanged) position —
       not a calc() derived from it. */
    position: fixed;
    inset-inline: 0;
    top: 118px;
    text-align: center;
  }
  /* .menu__section-toggle's own mobile text-align override lives further
     down, right after its base (unconditional) rule — same specificity
     (single class), so it has to come AFTER that rule in source order to
     win; a media query alone doesn't affect the tie-break, only position
     does. Keeping it here would silently lose to the rule below it. */
}
.menu__toggle {
  margin-left: auto;
  display: block;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  cursor: pointer;
}
.menu__toggle:hover {
  color: var(--color-muted);
}
.menu__toggle:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

.menu__panel {
  margin-top: var(--spacing-2);
  font-family: var(--font-display);
  line-height: var(--leading-normal);
}
.menu__section-toggle {
  display: block;
  width: 100%;
  text-align: right;
  text-decoration: underline;
  text-underline-offset: 4px;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
@media (max-width: 767px) {
  .menu__section-toggle {
    text-align: center;
  }
}
.menu__section-toggle:hover {
  color: var(--color-muted);
}
.menu__section-toggle:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

[data-exhibitions-list] {
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu__year-link {
  display: block;
}
.menu__about-link {
  margin-top: 0.25rem;
  display: block;
}

/* ---------------------------------------------------------------------------
   SiteChrome — fixed footer: copyright + locale switch.
   --------------------------------------------------------------------------- */
.site-footer {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 30;
  /* Transparent on desktop (≥960px), opaque below it. Note: the opaque
     background originally existed specifically so scrolled body copy (About,
     the exhibition statement) couldn't slide underneath and become
     unreadable — going transparent at ≥960px reopens that on any page that
     scrolls at that width, not just the non-scrolling ones (home, year
     list). Requested as a blanket footer change, so applied site-wide;
     revisit per-page if scrolled text turns out to need the opaque
     backing again at desktop widths. */
  background: transparent;
}
@media (max-width: 959px) {
  .site-footer {
    background: var(--color-paper);
  }
}
/* Opt-out of the transparent-desktop rule above — About sets this (see
   templates/about.php) because it scrolls at every width, unlike the
   pages the transparency was actually requested for (home, year list). Same
   specificity as the two rules above; wins by simply coming after them, at
   every width, since .site-footer--opaque applies unconditionally. */
.site-footer--opaque {
  background: var(--color-paper);
}
.site-footer__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--spacing-1);
  padding: var(--footer-padding-y) var(--spacing-2);
}
@media (min-width: 640px) {
  .site-footer__row {
    padding-inline: var(--spacing-3);
  }
}
.site-footer__copyright {
  /* `pre`, not `pre-line`: the two lines are an intentional \n split (see
     data.php's copyright string), not a place for the browser to ALSO wrap. */
  white-space: pre;
  font-family: var(--font-ui);
  font-size: var(--text-fine);
  line-height: var(--leading-normal);
  margin: 0;
}
.locale-switch {
  font-family: var(--font-ui);
  font-size: var(--text-nav);
  line-height: var(--leading-normal);
  margin: 0 0 0 auto;
  white-space: nowrap;
}

/* Both the copyright and the locale switch must stay on the SAME row —
   the design is "copyright bottom-left, language switch bottom-right", not
   one stacked above the other. At the full 18px/24px sizes the two only
   combine to fit alongside each other from ~420px viewport width up
   (measured: 356px of content vs. 374px available at 414px, but 356px vs.
   335px available at 375px — an iPhone SE overflows by ~20px). Below that,
   shrink both — measured in-browser against the actual longest string
   ("Todos los derechos reservados.") so this is a tested fit, not a guess:
   16px/22px leaves the pair at ~325px of the ~335px available at 375px
   width, with margin to spare down to 320px too. `flex-wrap` on
   .site-footer__row is kept as a last-resort safety net (e.g. a user zoom
   level these numbers weren't tuned for), not the primary fix. */
@media (max-width: 420px) {
  .site-footer__copyright {
    font-size: 1rem; /* 16px */
  }
  .locale-switch {
    font-size: 1.375rem; /* 22px */
  }
}
.locale-switch__muted {
  color: var(--color-muted);
}

/* ---------------------------------------------------------------------------
   About page.
   --------------------------------------------------------------------------- */
.about-page {
  position: relative;
  min-height: 100vh;
  background: var(--color-paper);
}
.about-page__pad {
  padding: var(--spacing-2);
}
@media (min-width: 640px) {
  .about-page__pad { padding: var(--spacing-3); }
}
.about-page__body {
  /* No margin-top: this is the first thing in .about-page__pad now that the
     "About"/"Quiénes somos" heading above it is gone — .about-page__pad's
     own padding already provides top spacing. */
  max-width: 46ch;
  padding-bottom: calc(var(--footer-height) + var(--spacing-3));
}
.about-page__paragraph {
  margin-top: var(--spacing-2);
  font-family: var(--font-display);
  line-height: var(--leading-normal);
}
.about-page__paragraph:first-child {
  margin-top: 0;
}
.about-page__contact {
  margin-top: var(--spacing-5);
  font-family: var(--font-display);
  line-height: var(--leading-normal);
}
.about-page__contact dd {
  margin: 0;
}

/* ---------------------------------------------------------------------------
   Year list page.
   --------------------------------------------------------------------------- */

/* Marquee on desktop, static logo on mobile — a deliberate divergence from
   the rest of the site (everywhere else the marquee is home-page-only).
   Both partials render server-side; CSS picks one per breakpoint so there's
   no layout shift or JS needed to decide. 960px, not the site's usual
   767/768 mobile split — a wider "desktop" threshold requested specifically
   for this page. */
.year-page__wordmark-mobile {
  display: block;
}
.year-page__marquee-desktop {
  display: none;
}
@media (min-width: 960px) {
  .year-page__wordmark-mobile {
    display: none;
  }
  .year-page__marquee-desktop {
    display: block;
  }
}

.year-page__heading {
  margin: 0 0 var(--spacing-2);
  /* Items Condensed (--font-display), italic via font-style — not
     --font-italic (Items Demo Condensed): that's a trial/demo font build
     (see assets/fonts/FONTS.md), and this list specifically avoids it,
     unlike the exhibition detail page's title which still uses it. Items
     Condensed itself has no italic face, so this is browser-synthesized
     (oblique) — a deliberate tradeoff for not shipping the demo font here. */
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  line-height: var(--leading-tight);
}
.year-page__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.year-page__item {
  margin-bottom: var(--spacing-2);
}
.year-page__link {
  display: block;
  font-family: var(--font-display);
  line-height: var(--leading-tight);
}
.year-page__names {
  display: block;
}
.year-page__names--current {
  /* Same reasoning as .year-page__heading above: Items Condensed + a
     synthesized italic, not the Items Demo Condensed trial font. */
  font-family: var(--font-display);
  font-style: italic;
}
.year-page__dates {
  display: block;
}

/* ---------------------------------------------------------------------------
   Exhibition detail page.
   --------------------------------------------------------------------------- */
.exhibition-page {
  position: relative;
  min-height: 100vh;
  background: var(--color-paper);
}
.exhibition-page__wordmark-wrap {
  position: relative;
}
.exhibition-page__header {
  position: static;
  margin-top: var(--spacing-2);
  width: var(--content-column);
  padding: var(--spacing-2);
}
@media (min-width: 640px) {
  .exhibition-page__header { padding: var(--spacing-3); }
}
/* No >=960px rule for .exhibition-page__header itself: its parent,
   .exhibition-page__wordmark-wrap, is display:none at that width (see the
   Wordmark section above) — .exhibition-sticky-header__meta is the desktop
   equivalent now, combined into the same bar as the logo. */
.exhibition-page__artists {
  font-family: var(--font-display);
  line-height: var(--leading-tight);
}
.exhibition-page__title {
  font-family: var(--font-italic);
  font-style: italic;
  line-height: var(--leading-tight);
}
.exhibition-page__stage-row {
  margin-top: var(--spacing-4);
  display: flex;
  justify-content: center;
  padding-inline: var(--spacing-2);
}
@media (min-width: 640px) {
  .exhibition-page__stage-row { padding-inline: var(--spacing-3); }
}
.exhibition-page__opening {
  font-family: var(--font-display);
  font-style: italic;
  line-height: var(--leading-normal);
  margin: 0;
}
.exhibition-page__statement {
  margin: var(--spacing-4) auto 0;
  width: min(var(--size-stage), calc(100vw - 2 * var(--spacing-3)));
  padding-bottom: calc(var(--footer-height) + var(--spacing-3));
}
.exhibition-page__statement-p {
  margin-top: var(--spacing-2);
  font-family: var(--font-ui);
  font-size: var(--text-body-sm);
  line-height: 1.5;
}
.exhibition-page__statement-p:first-child {
  margin-top: 0;
}

/* ---------------------------------------------------------------------------
   Carousel.
   --------------------------------------------------------------------------- */
.carousel {
  position: relative;
  display: flex;
  width: 100%;
  min-width: 0;
  align-items: center;
  justify-content: center;
  gap: 16px;
}
@media (min-width: 640px) {
  .carousel { gap: 24px; }
}
.carousel__control {
  flex-shrink: 0;
  color: var(--color-ink);
  width: var(--size-control);
  height: var(--size-control);
}
.carousel__control:hover {
  color: var(--color-muted);
}
.carousel__control:focus-visible {
  outline: 2px solid var(--color-ink);
  outline-offset: 4px;
}
.carousel__control svg {
  width: 100%;
  height: 100%;
}
.carousel__stage {
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
}
/* Shared by both slide types (<img> and the Vimeo <iframe>) — positioning,
   sizing and the slide transition are identical either way. */
.carousel__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  /* Fitted to the measured spring: ~500ms perceptual, no overshoot. */
  transition: transform 480ms cubic-bezier(0.16, 1, 0.3, 1), opacity 480ms ease-out;
}
.carousel__img {
  object-fit: cover;
}
.carousel__video {
  /* Letterboxing while the Vimeo player itself loads. */
  background: var(--color-ink);
}
.carousel__img--enter {
  opacity: 1;
}
.carousel__img--exit {
  opacity: 0;
}

/* ---------------------------------------------------------------------------
   404.
   --------------------------------------------------------------------------- */
.not-found {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2);
  text-align: center;
  padding: var(--spacing-3);
}
