/* ─────────────────────────────────────────────────────────────────────────
   The Duke of AI — The Art Gallery

   Two things live here: the corridor on the homepage, and the layout of
   the /gallery/ landing page. Loaded on the homepage after the other
   section sheets, and on /gallery/ alongside css/folio.css, which already
   supplies the nav, footer and page scaffolding.
   ───────────────────────────────────────────────────────────────────────── */

/* ── the corridor ────────────────────────────────────────────────────────
   Two rails of cards ride from far behind the screen toward the viewer.
   Perspective alone does the work that looks like two separate animations:
   as a card's z grows it gets bigger AND its screen x sweeps outward from
   the vanishing point, because the projection scales position and size by
   the same factor.

   Three things shape it, and each fixes a specific artefact:

   1. Depth is authored as APPARENT SIZE, geometrically - each card is a
      constant ratio bigger than the one behind it. Spacing a straight
      z-range evenly instead tears the near cards apart as the projection
      blows up.
   2. The rails open hard in the first stretch and then hold. That opening
      cancels the still-slow growth back there, so the ribbon leaves the
      centre as a flat band, bends once, then runs out on the diagonal.
      Parallel rails project to a straight cone with no bend at all.
   3. Neither end of the loop is ever on screen. A card dies with its inner
      edge at 64cqw, clear of the frame. And it is born ACROSS the axis, at
      screen x -1.1, so the newest card sweeps back through the centre -
      which plugs the throat. Birthing on its own side leaves a hole at dead
      centre that blinks open once a cycle.

   The keyframes below were traced from that geometry by
   scratchpad/keyframes.py rather than authored by hand, which is why they
   are a wall of numbers. THE CARD COUNT IS LOAD-BEARING, and only in one
   direction: more cards overlap more and are always safe, fewer tear the
   ribbon open near the frame edge. Measured worst gap between consecutive
   cards, where a positive number is a hole:

       9 a rail  -0.07cqw   (75% of each picture visible)
      12 a rail  -0.60cqw   (53% visible)  <- what this is set to
      14 a rail  -0.80cqw   (44% visible)
       7 a rail  +1.21cqw   TEARS

   So raising the count costs visibility, not integrity. Re-solve before
   LOWERING it, or before changing the exit height or the rail width.

   Every length is cqw - a percentage of the corridor's own width - so the
   whole thing holds its proportions at any size.

   NOTE ON iOS: there is deliberately no `will-change` here, and deliberately
   no `backface-visibility:hidden` either. The original component carried the
   latter, but a card turns at most 28 degrees and so never shows its back;
   all it bought was 18 permanently composited layers, which is the same
   mechanism that blanked the studio belts on the iPad. See CLAUDE.md.
   ──────────────────────────────────────────────────────────────────────── */

.corridor{
  position:relative;
  container-type:inline-size;      /* every cqw below resolves against this */
  overflow:hidden;
  contain:paint;
  /* Every length inside the corridor is cqw - a share of this element's own
     width - so the frame has to scale with its width too, or the whole
     composition shrinks inside a box that does not. A fixed height left the
     cards at 132px inside a 420px frame on a phone: a thin ribbon adrift in
     a tall black box. An aspect ratio keeps the picture identical at every
     size, which is the point of authoring in cqw in the first place. */
  aspect-ratio:16/9;
  max-height:620px;
  border-radius:4px;
  background:var(--canvas);
}

/* The 3D context. aria-hidden in the markup: it is decoration, and the
   section's heading and button carry the meaning. */
.corridor-stage{
  position:absolute; inset:0;
  pointer-events:none;
  perspective:30cqw;
  perspective-origin:50% 55%;      /* the axis, matching .corridor-card top */
}
.corridor-rail{ position:absolute; inset:0; transform-style:preserve-3d; }

.corridor-card{
  position:absolute;
  left:50%; top:55%;
  width:18cqw; height:25cqw;
  margin-left:-9cqw; margin-top:-12.5cqw;
  border-radius:.4cqw;
  overflow:hidden;
  animation:corridor-right 24s linear infinite;
}
.corridor-rail--left .corridor-card{ animation-name:corridor-left; }

.corridor-card img{ width:100%; height:100%; object-fit:cover; }

/* Negative delays drop each card mid-flight, so the corridor is already
   full on the first frame instead of filling up from the vanishing point.
   Twelve cards over a 24s cycle = one every 2s.

   THE DURATION AND THESE DELAYS ARE ONE SETTING WRITTEN IN TWO PLACES, and
   they must move together. A delay here is an absolute number of seconds, not
   a fraction of the loop, so changing the duration alone leaves the cards
   bunched into the old span with a silent gap behind them - at 24s with 1.5s
   delays, every card would pass inside eighteen seconds and the corridor would
   stand empty for six.

   Slowed from 18s on 2 Oct 2026 at Ashley's request, to open the gap between
   neighbouring cards. It does NOT make a card bigger or hold it longer - the
   path and the sizes along it are the keyframes' and do not change - it spaces
   the cards further apart so each is less hidden by the one in front.
   Measured: at 90% along the corridor the overlap falls from 9.1cqw to about
   4.6cqw, which is roughly a third of a card's width down to a sixth. */
.corridor-card:nth-child(1){ animation-delay:0s }
.corridor-card:nth-child(2){ animation-delay:-2s }
.corridor-card:nth-child(3){ animation-delay:-4s }
.corridor-card:nth-child(4){ animation-delay:-6s }
.corridor-card:nth-child(5){ animation-delay:-8s }
.corridor-card:nth-child(6){ animation-delay:-10s }
.corridor-card:nth-child(7){ animation-delay:-12s }
.corridor-card:nth-child(8){ animation-delay:-14s }
.corridor-card:nth-child(9){ animation-delay:-16s }
.corridor-card:nth-child(10){ animation-delay:-18s }
.corridor-card:nth-child(11){ animation-delay:-20s }
.corridor-card:nth-child(12){ animation-delay:-22s }

/* Pausing, not disabling. Every card is already mid-flight by its negative
   delay, so it freezes as a finished still; `animation:none` would collapse
   all eighteen onto the axis in a heap. */
@media (prefers-reduced-motion:reduce){
  .corridor-card{ animation-play-state:paused }
}

@keyframes corridor-right{
  0.00%{transform:translate3d(-11.00cqw,0,-258.46cqw) rotateY(-6.00deg)}
  4.17%{transform:translate3d(-3.79cqw,0,-225.92cqw) rotateY(-6.92deg)}
  8.33%{transform:translate3d(2.73cqw,0,-197.04cqw) rotateY(-7.83deg)}
  12.50%{transform:translate3d(8.60cqw,0,-171.43cqw) rotateY(-8.75deg)}
  16.67%{transform:translate3d(13.87cqw,0,-148.70cqw) rotateY(-9.67deg)}
  20.83%{transform:translate3d(18.56cqw,0,-128.54cqw) rotateY(-10.58deg)}
  25.00%{transform:translate3d(22.72cqw,0,-110.65cqw) rotateY(-11.50deg)}
  29.17%{transform:translate3d(26.37cqw,0,-94.78cqw) rotateY(-12.42deg)}
  33.33%{transform:translate3d(29.57cqw,0,-80.70cqw) rotateY(-13.33deg)}
  37.50%{transform:translate3d(32.34cqw,0,-68.21cqw) rotateY(-14.25deg)}
  41.67%{transform:translate3d(34.71cqw,0,-57.13cqw) rotateY(-15.17deg)}
  45.83%{transform:translate3d(36.73cqw,0,-47.30cqw) rotateY(-16.08deg)}
  50.00%{transform:translate3d(38.42cqw,0,-38.58cqw) rotateY(-17.00deg)}
  54.17%{transform:translate3d(39.81cqw,0,-30.84cqw) rotateY(-17.92deg)}
  58.33%{transform:translate3d(40.94cqw,0,-23.98cqw) rotateY(-18.83deg)}
  62.50%{transform:translate3d(41.84cqw,0,-17.89cqw) rotateY(-19.75deg)}
  66.67%{transform:translate3d(42.53cqw,0,-12.48cqw) rotateY(-20.67deg)}
  70.83%{transform:translate3d(43.06cqw,0,-7.69cqw) rotateY(-21.58deg)}
  75.00%{transform:translate3d(43.43cqw,0,-3.44cqw) rotateY(-22.50deg)}
  79.17%{transform:translate3d(43.69cqw,0,0.33cqw) rotateY(-23.42deg)}
  83.33%{transform:translate3d(43.85cqw,0,3.68cqw) rotateY(-24.33deg)}
  87.50%{transform:translate3d(43.94cqw,0,6.65cqw) rotateY(-25.25deg)}
  91.67%{transform:translate3d(43.98cqw,0,9.28cqw) rotateY(-26.17deg)}
  95.83%{transform:translate3d(44.00cqw,0,11.62cqw) rotateY(-27.08deg)}
  100.00%{transform:translate3d(44.00cqw,0,13.70cqw) rotateY(-28.00deg)}
}
@keyframes corridor-left{
  0.00%{transform:translate3d(11.00cqw,0,-258.46cqw) rotateY(6.00deg)}
  4.17%{transform:translate3d(3.79cqw,0,-225.92cqw) rotateY(6.92deg)}
  8.33%{transform:translate3d(-2.73cqw,0,-197.04cqw) rotateY(7.83deg)}
  12.50%{transform:translate3d(-8.60cqw,0,-171.43cqw) rotateY(8.75deg)}
  16.67%{transform:translate3d(-13.87cqw,0,-148.70cqw) rotateY(9.67deg)}
  20.83%{transform:translate3d(-18.56cqw,0,-128.54cqw) rotateY(10.58deg)}
  25.00%{transform:translate3d(-22.72cqw,0,-110.65cqw) rotateY(11.50deg)}
  29.17%{transform:translate3d(-26.37cqw,0,-94.78cqw) rotateY(12.42deg)}
  33.33%{transform:translate3d(-29.57cqw,0,-80.70cqw) rotateY(13.33deg)}
  37.50%{transform:translate3d(-32.34cqw,0,-68.21cqw) rotateY(14.25deg)}
  41.67%{transform:translate3d(-34.71cqw,0,-57.13cqw) rotateY(15.17deg)}
  45.83%{transform:translate3d(-36.73cqw,0,-47.30cqw) rotateY(16.08deg)}
  50.00%{transform:translate3d(-38.42cqw,0,-38.58cqw) rotateY(17.00deg)}
  54.17%{transform:translate3d(-39.81cqw,0,-30.84cqw) rotateY(17.92deg)}
  58.33%{transform:translate3d(-40.94cqw,0,-23.98cqw) rotateY(18.83deg)}
  62.50%{transform:translate3d(-41.84cqw,0,-17.89cqw) rotateY(19.75deg)}
  66.67%{transform:translate3d(-42.53cqw,0,-12.48cqw) rotateY(20.67deg)}
  70.83%{transform:translate3d(-43.06cqw,0,-7.69cqw) rotateY(21.58deg)}
  75.00%{transform:translate3d(-43.43cqw,0,-3.44cqw) rotateY(22.50deg)}
  79.17%{transform:translate3d(-43.69cqw,0,0.33cqw) rotateY(23.42deg)}
  83.33%{transform:translate3d(-43.85cqw,0,3.68cqw) rotateY(24.33deg)}
  87.50%{transform:translate3d(-43.94cqw,0,6.65cqw) rotateY(25.25deg)}
  91.67%{transform:translate3d(-43.98cqw,0,9.28cqw) rotateY(26.17deg)}
  95.83%{transform:translate3d(-44.00cqw,0,11.62cqw) rotateY(27.08deg)}
  100.00%{transform:translate3d(-44.00cqw,0,13.70cqw) rotateY(28.00deg)}
}

/* ═════════════════════════════════════════════════════════════════════════
   /gallery/ — the landing page
   Loaded alongside css/folio.css, which supplies the nav, the footer, the
   .back link and the .folio-head editorial header. Only the pieces unique
   to the gallery live below.
   ═════════════════════════════════════════════════════════════════════════ */

/* ---------- buttons ----------
   The pill styles live in the homepage's inline CSS, which this page does
   not load. Copied verbatim, exactly as css/projects.css does, so a gallery
   button matches every other button on the site. If the homepage pills
   change, change these too. */
  .pill{
    display:inline-flex; align-items:center; gap:.5em;
    font-family:var(--text); font-size:17px; font-weight:400; letter-spacing:-.01em;
    border-radius:980px; padding:11px 22px;
    cursor:pointer; border:1px solid transparent;
    transition:background var(--dur-base) ease, color var(--dur-base) ease, border-color var(--dur-base) ease, transform var(--dur-base) ease;
  }
  .pill-fill{ background:var(--gold); color:var(--pill-ink,#0a0a0b); }
  .pill-fill:hover{ background:var(--gold-hi); }
  .pill-outline{ background:transparent; border-color:var(--gold); color:var(--gold); }
  .pill-outline:hover{ background:var(--gold-soft); }
  .pill-sm{ font-size:14px; padding:7px 15px; }

/* ---------- a section of the gallery ---------- */
.gal-section{ margin-top:clamp(52px,7vw,88px); }
.gal-section:first-of-type{ margin-top:clamp(38px,5vw,60px); }
.gal-head{ display:flex; align-items:baseline; gap:16px; flex-wrap:wrap;
  padding-bottom:14px; border-bottom:1px solid var(--hairline); margin-bottom:clamp(22px,3vw,34px); }
.gal-head h2{ font-family:var(--display); font-weight:600; letter-spacing:-.014em;
  font-size:clamp(1.5rem,1.1rem + 1.4vw,2.05rem); color:var(--paper); }
.gal-head .gal-tag{ font-family:var(--display); font-weight:300; color:var(--gold);
  font-size:clamp(1rem,.9rem + .4vw,1.15rem); }
.gal-lede{ font-family:var(--display); font-weight:300; color:var(--secondary);
  font-size:clamp(1rem,.92rem + .35vw,1.16rem); line-height:1.6; max-width:62ch;
  text-wrap:pretty; margin-bottom:clamp(20px,3vw,30px); }

/* ---------- the current exhibition frame ----------
   Holds its shape before the exhibition arrives, so the page does not jump
   when the embed lands. The Kunstmatrix iframe will be dropped straight in
   here, and the aspect-ratio keeps it responsive without JavaScript. */
.gal-frame{
  position:relative; aspect-ratio:16/9; width:100%;
  border:1px solid var(--gold-line); border-radius:4px;
  background:var(--surface);
  display:grid; place-items:center; text-align:center; padding:28px;
  overflow:hidden;
}
.gal-frame iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; }
/* Once the exhibition is in, the frame is just a window: drop the padding and
   the centring so the viewer gets the whole box. The supplied embed carries
   width="100%" height="600"; the rules above override both, because a fixed
   600px would letterbox on a wide screen and overflow a narrow one. */
.gal-frame.is-live{ display:block; padding:0; background:var(--canvas); }
.gal-frame-inner{ max-width:46ch; }
.gal-frame-inner p{ font-family:var(--display); font-weight:300; color:var(--secondary);
  font-size:clamp(1rem,.92rem + .4vw,1.2rem); line-height:1.55; text-wrap:pretty; }
.gal-frame-inner .gal-frame-note{ color:var(--muted); font-size:.92rem; margin-top:14px; }

/* The button sits at the bottom of the box, in its OWN absolutely positioned
   wrapper rather than as a flex item pushed down with margin-top:auto. That is
   deliberate: js/metallic-cta.js wraps the control in a .mcta shell, and the
   shell would become the item the alignment applied to - the same trap that
   would have unlevelled the viewing room buttons. Positioning a wrapper I own
   is immune to anything the script does inside it. */
.gal-enter-wrap{
  position:absolute; left:50%; bottom:clamp(18px,4%,38px);
  transform:translateX(-50%); z-index:2;
}
/* Reserve the space the button occupies so the centred copy above can never
   collide with it on a short frame. */
.gal-frame .gal-frame-inner{ padding-bottom:clamp(58px,11%,96px); }
/* NO margin on .gal-enter. It had margin-top:22px while it was still in the
   flow, and that margin survived the move into .gal-enter-wrap - where it did
   nothing useful, because the wrapper is absolutely positioned, and one thing
   actively wrong: js/metallic-cta.js wraps the link, so the link's own margin
   expanded the SHELL. The shell came out 71px tall around a 49px button with
   the button pinned to its bottom, which dropped the label 11px below centre.
   Third time this script's wrapping has moved what a rule applies to; the
   rule of thumb is that nothing on a [data-metallic] control may set margin
   or alignment - that belongs on a wrapper the page owns. */

/* ---------- the cover ----------
   A still, so the box reads as a door rather than as something that failed to
   load. The copy that used to sit over it has gone - it repeated the lede
   directly above the frame, which is the better place for it - so the picture
   now carries the box on its own.

   The scrim stayed but got much lighter. It was weighted for type legibility
   (.42 at the top, .88 at the foot) and with nothing left to make legible
   that only muddied the photograph. What it still has to do is give the
   button at the foot some ground to sit on, so it is near-clear at the top
   and gathers only at the bottom. */
.gal-frame--cover{
  background:var(--canvas) url("/assets/exhibition-cover.webp?v=1") center/cover no-repeat;
}
.gal-frame--cover::before{
  content:""; position:absolute; inset:0; z-index:1;
  background:linear-gradient(to bottom,
    rgba(10,10,11,.06) 0%, rgba(10,10,11,.16) 48%, rgba(10,10,11,.66) 100%);
}
.gal-frame--cover .gal-frame-inner{ position:relative; z-index:2; }
.gal-frame--cover .gal-frame-inner p{ color:var(--paper); }
.gal-frame--cover .gal-frame-inner .gal-frame-note{ color:var(--secondary); }
/* The cover goes the moment the exhibition is in. */
.gal-frame.is-live::before{ display:none; }
.gal-frame.is-live{ background-image:none; }

/* A 16:9 box is 184px tall on a phone, which is too short twice over: the
   copy and the button collided in it, and a three-dimensional room you walk
   through is unusable in a letterbox that size. Squaring it off gives the
   copy room and the viewer something worth pinching. Measured at 375px: the
   frame goes 184px to 327px, and the overlap is gone. */
@media (max-width:700px){
  .gal-frame{ aspect-ratio:1/1; }
  .gal-frame-inner{ padding-bottom:clamp(64px,18%,104px); }
}

/* ---------- the viewing rooms ---------- */
.rooms-grid{ display:grid; gap:clamp(16px,2.2vw,26px);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr)); }
.room-card{
  border:1px solid var(--hairline); border-radius:4px; background:var(--surface);
  display:flex; flex-direction:column; gap:14px;
  /* The padding moved off the card and onto the body below, so the cover can
     run edge to edge the way a folio tile and a project card both do. */
  padding:0 0 clamp(24px,3vw,34px);
  overflow:hidden;
  transition:border-color var(--dur-base) ease;
}
/* Square cover, same idiom as .p-thumb on the projects index. */
.room-thumb{ aspect-ratio:1/1; background:var(--canvas); margin-bottom:4px; }
.room-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
/* Everything below the picture is centred, so the eyebrow, the title, the
   description and the button all sit on one axis - Ashley, 2 Oct 2026, after
   the button moved to the middle and left the words hanging off to one side.

   Set HERE rather than on .room-card itself, so it reaches the body and not
   the picture: .room-thumb is the one child excluded, and centring text on a
   box that holds nothing but an <img> would be a rule doing nothing, which is
   the sort that gets copied somewhere it does matter. */
.room-card > :not(.room-thumb){ margin-left:clamp(24px,3vw,34px);
                                margin-right:clamp(24px,3vw,34px);
                                text-align:center; }
.room-card:hover{ border-color:var(--gold-line); }
.room-no{ font-family:var(--display); font-size:12px; font-weight:600;
  letter-spacing:.18em; text-transform:uppercase; color:var(--gold); }
.room-card h3{ font-family:var(--display); font-weight:600; letter-spacing:-.012em;
  font-size:clamp(1.2rem,1.05rem + .5vw,1.45rem); color:var(--paper); }
.room-card p{ font-size:14.5px; color:var(--muted); line-height:1.55; text-wrap:pretty; }
/* Pushes the button to the bottom so the three sit level when one line of
   copy wraps and another does not. Safe here: every card has one button and
   nothing else below it.

   BOTH selectors are needed, and neither is redundant. js/metallic-cta.js
   WRAPS the control in a .mcta shell, which then becomes the flex item in
   its place - so alignment set only on .pill would quietly stop applying
   and the shell would stretch the card's full width. The .pill rule is not
   dead weight either: it is what holds the layout when there is no
   JavaScript or no WebGL2 and no wrapper is ever built. Same idiom as
   `.contact-form .mcta` in index.html. */
.room-card .pill,
.room-card .mcta{ margin-top:auto; align-self:center; }
/* A room that has not been hung yet is a <span>, not a link - same pattern
   as the projects index's "In the works" card. */
.room-card .pill--shut{ border-color:var(--hairline); color:var(--faint); cursor:default; }
.room-card .pill--shut:hover{ background:transparent; }
