/* ============================================================
   REJOPACHÍ ~ portfolio
   An album: sleep to waking. The page carries one ground colour
   and one ink colour and drags them down the record as you
   scroll. Both are set from JS ~ see COLOUR SPINE in index.html.
   Nothing here should hard-code a colour except the fallbacks
   below, which are what the page looks like before JS runs.
   ============================================================ */

:root {
  --ground:   oklch(0 0 0);
  --ink:      oklch(0.96 0.01 300);
  --dim:      oklch(0.63 0.01 300);
  --accent:   oklch(0.70 0.14 300);
  --hairline: oklch(0.63 0.01 300 / 0.28);

  /* THREE REGISTERS, and nothing is allowed to sit between them.
       display ~ the voice        (titles, the name)
       text    ~ the argument     (prose you actually read)
       data    ~ the record       (medium, size, date, index, ticks)
     The data face is a monospace on purpose: dimensions, dates and
     materials are catalogue information, and mono is how catalogue
     information has always been set. It reads as archival rather than
     decorative, which is what keeps this a portfolio and not a poster.
     Costs nothing to ship ~ ui-monospace is SF Mono on macOS, Menlo
     as fallback, Consolas on Windows. */
  --font-display: "Gothic War", "Times New Roman", serif;
  --font-text: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-data: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  --column: 42rem;

  --topo-color: oklch(0.96 0.01 300);
  --topo-opacity: 0;
  --topo-scale: 1;
  --topo-y: 0vh;
  --veil-opacity: 0.85;
}

@font-face {
  font-family: "Gothic War";
  src: url("gothic-war.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: block;   /* never flash the name in the wrong face */
}

* { box-sizing: border-box; }

/* Jumps between tracks glide rather than cut ~ the descent is the content,
   so teleporting past it would skip the argument. Turned off below for
   anyone who has asked for reduced motion. */
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-text);
  line-height: 1.5;
  margin: 0;
  color: var(--ink);
  background: var(--ground);

  /* the ground is repainted every frame; let it move smoothly if a
     frame is dropped, and keep the black behind rubber-band scroll */
  transition: background-color 90ms linear, color 90ms linear;
}

/* The readable column. Sections are full-bleed so the ground can
   run edge to edge; this is what keeps the words in the middle. */
/* Inline-axis only, on purpose. `margin: 0 auto` and `padding: 0 1.5rem`
   are shorthands, and .column is a class ~ so they outrank anything an
   element rule like `header {}` says about its TOP padding or BOTTOM
   margin, and silently drop it. Setting only the inline properties means
   element rules own the block axis and the two can never collide. */
.column {
  max-width: var(--column);
  margin-inline: auto;
  padding-inline: 1.5rem;
}

a { color: inherit; text-decoration-color: var(--hairline); }
a:hover { text-decoration-color: currentColor; }

/* Visible focus, everywhere, in the current ink ~ the accent alone is not
   guaranteed to contrast against every ground on the descent. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 1px;
}

/* Off-screen until tabbed to. Without it the only way past the intro and
   the atmosphere with a keyboard is to tab through all of it. */
.skip-link {
  position: fixed;   /* NOT absolute ~ absolute pins it to the top of the
                        document, so tabbing to it after any scrolling
                        moves focus to something off-screen */
  left: 50%;
  top: 0;
  transform: translate(-50%, -120%);
  z-index: 200;
  padding: 0.7rem 1.1rem;
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ground);
  background: var(--ink);
  transition: transform 160ms ease-out;
}
/* :focus, not :focus-visible ~ this is the first focusable thing on the
   page and is only ever reached by keyboard, and :focus-visible does not
   match every way focus can arrive. It must never fail to appear. */
.skip-link:focus { transform: translate(-50%, 0.5rem); }


/* ============================================================
   ATMOSPHERE
   ============================================================ */

/* The drawing, running the whole length of the record. It is a
   MASK filled with --topo-color, not an image ~ that is what lets
   it take the page's own ink colour at every stage instead of
   sitting on top as a grey photograph of a drawing. */
/* inset 0 and `cover`, matching the intro video's object-fit exactly ~
   at scroll 0 the two are the same picture in the same place, which is
   what lets the opening bleed into the background instead of cutting. */
#topo {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  opacity: var(--topo-opacity);
  transform: translate3d(0, var(--topo-y), 0) scale(var(--topo-scale));
  transform-origin: 50% 40%;
  will-change: transform, opacity;
}

/* MOIRÉ ~ two copies of the same drawing at slightly different scale and
   rotation. Where the lines nearly coincide they beat against each other,
   and the interference pattern crawls as the second sheet drifts. That
   travelling beat is the shimmer; neither sheet does anything on its own.
   Keep the offsets tiny ~ large ones read as a blurry double image
   instead of interference. */
.topo-sheet {
  position: absolute;
  inset: 0;
  background-color: var(--topo-color);
  -webkit-mask-image: url("media/wves_topo.png");
          mask-image: url("media/wves_topo.png");
  -webkit-mask-size: cover;       mask-size: cover;
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}

.topo-b {
  opacity: 0.62;
  animation: moire 41s ease-in-out infinite;
  will-change: transform;
}

@keyframes moire {
  0%   { transform: scale(1.013) rotate(0.10deg); }
  25%  { transform: scale(1.026) rotate(-0.06deg) translate(0.4%, -0.3%); }
  50%  { transform: scale(1.034) rotate(-0.14deg) translate(-0.3%, 0.4%); }
  75%  { transform: scale(1.021) rotate(0.04deg)  translate(0.2%, 0.3%); }
  100% { transform: scale(1.013) rotate(0.10deg); }
}

/* Darkness at the edges. Painted in the ground colour so it reads as
   the room closing in early on, and dissolves to nothing by daylight.
   Sits BELOW the content on purpose: above it, this gradient would wash
   ground colour over text near the top and bottom of the screen and
   quietly undo the contrast floor the colour spine guarantees. It
   vignettes the atmosphere, not the reading surface. */
#veil {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--veil-opacity);
  background: radial-gradient(
    ellipse 78% 62% at 50% 45%,
    transparent 0%,
    transparent 42%,
    var(--ground) 100%
  );
}


/* ---------- vertical timeline ------------------------------- */
/* A prototype armature ~ swap the rail and head for hand-drawn art
   later; JS places the ticks from real section offsets, so nothing
   here needs recalculating when the art changes. */

#timeline {
  position: fixed;
  right: max(1.3rem, 2.4vw);
  top: 22vh;
  height: 56vh;
  z-index: 40;
  pointer-events: none;   /* the rail never blocks; the links opt back in */
}

/* The rail sits on the RIGHT of the strip so the titles have somewhere to
   expand into ~ leftward, into the page, instead of off the screen. */
.tl-rail, .tl-run {
  position: absolute;
  right: 0;
  width: 1px;
  top: 0;
}
.tl-rail { bottom: 0; background: var(--hairline); }
.tl-run  { height: var(--tl-progress, 0%); background: var(--dim); opacity: 0.7; }

.tl-head {
  position: absolute;
  right: -2px;
  top: var(--tl-progress, 0%);
  width: 5px; height: 5px;
  margin-top: -2px;
  border-radius: 50%;
  background: var(--accent);
}

.tl-tick {
  position: absolute;
  right: 0.85rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  pointer-events: auto;
  text-decoration: none;
  white-space: nowrap;
  font-family: var(--font-data);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  color: var(--dim);
  opacity: 0.65;
  transition: opacity 200ms ease-out, color 200ms ease-out;
}
.tl-tick::after {           /* the mark on the rail */
  content: "";
  position: absolute;
  right: -0.85rem; top: 50%;
  width: 5px; height: 1px;
  background: currentColor;
}
.tl-tick:hover,
.tl-tick:focus-visible { opacity: 1; color: var(--ink); }

/* The track you are currently inside */
.tl-tick.is-current { opacity: 1; color: var(--ink); }
.tl-tick.is-current::after { width: 9px; height: 1px; }

/* Titles stay collapsed until the strip is hovered or tabbed into ~
   the whole point is that navigation is present without being furniture. */
.tl-label {
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  text-transform: uppercase;
  transition: max-width 320ms cubic-bezier(.2,.7,.3,1),
              opacity 220ms ease-out;
}
#timeline:hover .tl-label,
#timeline:focus-within .tl-label { max-width: 11rem; opacity: 1; }

/* Below 640px there is no room beside the column, so the strip drops its
   links and becomes a bare progress hairline. Navigation does not vanish ~
   the record is linear, and the back cover index is the jump list. */
@media (max-width: 640px) {
  #timeline {
    top: 0; height: 100vh;
    right: 0.5rem;
  }
  .tl-tick { display: none; }
  .tl-rail, .tl-run, .tl-head { opacity: 0.35; }
}


/* ---------- header ------------------------------------------ */

/* margin-BOTTOM only ~ a shorthand here would wipe out the `margin: 0 auto`
   that .column uses to centre the readable column.
   The tall padding is the held breath: the intro finishes, the ground sits
   empty for a moment, and only then does the small name arrive. */
header { margin-bottom: 12vh; padding-top: 34vh; }

header h1 {
  font-family: var(--font-display);
  font-size: 2.75rem;      /* Gothic War is unicase ~ caps and
                              lowercase draw the same glyphs */
  font-weight: normal;
  line-height: 1.1;
  margin: 0;
}

header p {
  color: var(--dim);
  margin: 0.7rem 0 0;
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}


/* ---------- a track ----------------------------------------- */

.project {
  margin-bottom: 9rem;   /* see note above ~ bottom only */
  scroll-margin-top: 10vh;   /* jumped-to tracks land with air above them */
}

.project .track {
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.3em;
  color: var(--accent);
  margin: 0 0 0.75rem;
}

/* Hard hierarchy: the title is enormous, everything around it is small
   and tracked. Middle sizes are what make a page read as timid. */
.project h2 {
  font-family: var(--font-display);
  font-size: clamp(3.25rem, 11vw, 7rem);
  font-weight: normal;
  line-height: 0.95;
  margin: 0 0 0.75rem;
}

.project .meta {
  color: var(--dim);
  font-family: var(--font-data);
  font-size: 0.74rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  margin: 0 0 2.5rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--hairline);
}

/* FULL BLEED ~ escape the readable column, edge to edge.
   The width subtracts --sbw (the scrollbar's width, measured in JS)
   because 100vw counts the scrollbar but the page cannot use it ~ without
   that subtraction this overflows horizontally on every desktop browser
   that shows a classic scrollbar. */
.bleed {
  width: calc(100vw - var(--sbw, 0px));
  max-width: none;
  margin-inline: calc(50% - (100vw - var(--sbw, 0px)) / 2);
}
/* The lighting scale must never go BELOW 1 here ~ a full-bleed image at
   0.988 stops reaching the edges and leaves a sliver of ground down each
   side. So this one grows into the light instead of shrinking out of it. */
.bleed img {
  width: 100%;
  transform: scale(calc(1 + 0.012 * var(--lit)));
}
.bleed figcaption { margin-top: 0.9rem; }   /* .column re-centres it */

.project > p {
  font-size: 1.0625rem;
  line-height: 1.62;
  max-width: 34rem;
  text-wrap: pretty;      /* no single-word last lines */
}

/* --drift is eased toward its target in JS, so the work lags the scroll
   by a beat and settles ~ suspended rather than pinned to the page. */
figure {
  margin: 2rem 0;
  --lit: 1;
  --drift: 0px;
  transform: translate3d(0, var(--drift), 0);
  will-change: transform;
}
figure img + img { margin-top: 0.75rem; }
/* `height: auto` is not optional now that the images carry width/height
   attributes. Without it the attribute height is used verbatim while
   max-width squeezes the width, and every image renders distorted. The
   attributes are there so the browser reserves the right box before the
   file arrives ~ which also keeps the measured scroll anchors stable. */
img, video { max-width: 100%; height: auto; display: block; }
figcaption {
  color: var(--dim);
  font-family: var(--font-data);
  font-size: 0.78rem;
  letter-spacing: 0.01em;
  margin-top: 0.7rem;
}

/* Scroll is the light source. Work comes up out of the dark as it
   reaches the middle of the screen and sinks back as it leaves.
   This is model C without the mouse ~ it works on a phone. */
.project figure img {
  filter: brightness(calc(0.74 + 0.26 * var(--lit)))
          saturate(calc(0.78 + 0.22 * var(--lit)));
  transform: scale(calc(0.988 + 0.012 * var(--lit)));
  transition: filter 120ms linear, transform 120ms linear;

  /* A hairline in the current ink. Without it ABIGARRADO's yellow right
     third dissolves straight into the daylight ground and the painting
     loses its edge exactly where it should assert one. Adapts per stage,
     so it is nearly invisible on the dark grounds where it isn't needed. */
  box-shadow: 0 0 0 1px var(--hairline);
}

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


/* ---------- text reveal ------------------------------------- */
/* One time, on first entry, then it stays at full strength. */

.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 700ms cubic-bezier(.2,.7,.3,1),
              transform 700ms cubic-bezier(.2,.7,.3,1);
}
.reveal.is-in { opacity: 1; transform: none; }

.project h2.reveal        { transition-delay: 60ms; }
.project .meta.reveal     { transition-delay: 120ms; }
.project figure.reveal    { transition-delay: 100ms; }
.project > p.reveal       { transition-delay: 160ms; }


/* ---------- the wake ---------------------------------------- */
/* Empty on purpose. The ink has to swap from light to dark
   somewhere; this is the stretch of nothing where it happens. */

#wake { height: 112vh; }   /* below ~90vh the ink flip starts catching text */


/* ---------- back cover -------------------------------------- */

footer {
  padding-block: 5rem 6rem;   /* .column owns the inline padding */
  border-top: 1px solid var(--hairline);
}

.colophon-title {
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 2.5rem;
}

.colophon { margin: 0; font-size: 0.9rem; }
.colophon dt { font-family: var(--font-data); font-size: 0.78rem; letter-spacing: 0.04em; margin: 0 0 0.45rem; }
.colophon dd { color: var(--dim); margin: 0 0 1.75rem; max-width: 34rem; }

.colophon-note,
.colophon-contact {
  font-family: var(--font-data);
  font-size: 0.72rem;
  color: var(--dim);
  margin: 0;
}
.colophon-note { padding-top: 1.5rem; border-top: 1px solid var(--hairline); }
.colophon-contact { margin-top: 0.5rem; }

@media (prefers-reduced-motion: reduce) {
  #intro video { display: none; }
  #intro { background: #000 url("media/wves_poster.jpg") center / cover no-repeat; }
  #intro-name, #intro-hint { animation-delay: 0s; animation-duration: 0.01s; }
  body { transition: none; }

  html { scroll-behavior: auto; }
  #topo { transform: none; }
  .topo-b { animation: none; }
  .tl-label { transition: none; }
  figure { transform: none; }
  .project figure img { filter: none; transform: none; transition: none; }
  .reveal { opacity: 1; transform: none; transition: none; }

  /* Keep the signature, drop the fall into it. */
  #descent { height: 70vh; }
  .descent-stage { position: static; height: auto; padding: 6rem 0; }
  .sigil { animation: none; }
}


/* ============================================================
   INTRO ~ BLACK TOPO
   ============================================================ */

#intro {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #000;
  display: grid;
  place-items: center;
  overflow: hidden;
  will-change: opacity;
}

/* fully faded ~ stop compositing it and let clicks through */
#intro.is-gone { visibility: hidden; pointer-events: none; }

#intro video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;        /* beat the `img, video { max-width: 100% }` rule */
  object-fit: cover;
}

#intro-name {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  font-size: clamp(2.75rem, 11vw, 9.5rem);
  line-height: 0.95;
  color: #fff;
  text-align: center;
  padding: 0 1rem;
  text-wrap: balance;
  opacity: 0;
  animation: intro-name-in 1.6s ease-out 2.2s forwards;
}

#intro-hint {
  position: absolute;
  z-index: 1;
  bottom: 2.25rem;
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  opacity: 0;
  animation: intro-name-in 1.2s ease-out 4.2s forwards;
}

@keyframes intro-name-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The runway the intro fades across. Without this the page has nothing to
   scroll through at the top and the opening never clears. */
#intro-spacer { height: 100vh; }

/* Fallback: autoplay blocked (common on phones) ~ hold the finished
   drawing as a still so the opening is never a black screen with a name. */
#intro.no-video video { display: none; }
#intro.no-video {
  background: #000 url("media/wves_poster.jpg") center / cover no-repeat;
}


/* ============================================================
   THE DESCENT ~ the drawing draws itself, then the work rides it
   ------------------------------------------------------------
   The sigil is all OPEN strokes (no closed paths at all), which
   is useless for clipping images into but perfect for the two
   things done here: drawing itself on with stroke-dasharray,
   and being travelled along with getPointAtLength.
   ============================================================ */

#descent { padding: 12vh 0 0; height: 320vh; }
.descent-intro { margin-bottom: 4vh; }
.descent-intro h2 {
  font-family: var(--font-display);
  font-size: clamp(2rem, 6vw, 3.4rem);
  font-weight: normal; line-height: 1; margin: 0 0 0.8rem;
}

.descent-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* Held at a size where a photograph on a branch is still legible ~ the
   whole point is that the targets are the pictures, not hidden dots. */
.sigil-frame {
  position: relative;
  height: min(74vh, 88vw);
  aspect-ratio: 1103.2 / 1057.2;
}

.sigil-art { position: absolute; inset: 0; color: var(--dim); }
.sigil-art svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Every stroke is dashed by its own length and pushed out of view, then
   drawn back in bottom-to-top so the mark grows rather than appears. */
.sigil-art path { transition: stroke-dashoffset 1200ms cubic-bezier(.35,.8,.35,1); }


/* ---------- the wheel --------------------------------------- */
/* The sigil IS the menu. The diamond is cut into one sector per project,
   so every target is a third of the mark ~ enormous, exact, impossible to
   miss. Hovering brightens that third of the DRAWING itself, because each
   wedge holds its own clipped copy of the artwork. */

.wheel {
  position: relative;
  height: min(76vh, 90vw);
  aspect-ratio: 1103.2 / 1057.2;
  transition: transform 620ms cubic-bezier(.6,0,.3,1), opacity 380ms ease-out;
}
.wheel.is-diving { pointer-events: none; }

.wheel-base, .wedge-art {
  position: absolute;
  inset: 0;
}
.wheel-base { color: var(--dim); }
.wheel-base svg, .wedge-art svg {
  width: 100%; height: 100%; display: block; overflow: visible;
}
.wheel-base path { transition: stroke-dashoffset 1300ms cubic-bezier(.35,.8,.35,1); }

.wheel-wedges { position: absolute; inset: 0; }

/* clip-path is set per wedge in JS ~ it defines both the look and the
   hit area, so the pointer target is exactly the visible sector. */
.wedge {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--accent);
}
/* The clip-path stays ~ it makes the pointer target exactly the sector.
   But the VISIBLE glow is masked to a soft blob around the wedge's own
   centre, so it fades to nothing well before it reaches the straight cut.
   Without this the brightened third ends in two hard radii and the mark
   reads as a pie chart drawn over the drawing. */
.wedge-art {
  opacity: 0;
  transition: opacity 300ms ease-out;
  -webkit-mask-image: radial-gradient(circle at var(--wx, 50%) var(--wy, 50%),
                      #000 0%, #000 14%, rgba(0,0,0,0.55) 26%, transparent 42%);
          mask-image: radial-gradient(circle at var(--wx, 50%) var(--wy, 50%),
                      #000 0%, #000 14%, rgba(0,0,0,0.55) 26%, transparent 42%);
}
.wedge:hover .wedge-art,
.wedge:focus-visible .wedge-art { opacity: 1; }

/* A faint wash so the chosen third reads as a face of the object, not
   just brighter lines. */
.wedge-wash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--wx, 50%) var(--wy, 50%),
              color-mix(in oklab, var(--accent) 20%, transparent) 0%,
              color-mix(in oklab, var(--accent) 9%, transparent) 22%,
              transparent 44%);
  opacity: 0;
  transition: opacity 300ms ease-out;
  pointer-events: none;
}
.wedge:hover .wedge-wash, .wedge:focus-visible .wedge-wash { opacity: 1; }

.wedge-label {
  position: absolute;
  left: var(--wx, 50%);
  top: var(--wy, 50%);
  transform: translate(-50%, -50%) scale(0.94);
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 2.6vw, 2rem);
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
  text-shadow: 0 0 1.2rem var(--ground), 0 0 0.4rem var(--ground);
  opacity: 0;
  transition: opacity 220ms ease-out, transform 300ms cubic-bezier(.2,.7,.3,1);
  pointer-events: none;
}
.wedge:hover .wedge-label,
.wedge:focus-visible .wedge-label { opacity: 1; transform: translate(-50%,-50%) scale(1); }

.wedge-count {
  display: block;
  margin-top: 0.35rem;
  font-family: var(--font-data);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
  text-align: center;
}

.descent-hint {
  position: absolute;
  left: 50%; bottom: 2rem;
  transform: translateX(-50%);
  margin: 0;
  font-family: var(--font-data);
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--dim);
  opacity: var(--hint-opacity, 1);
  transition: opacity 300ms;
  pointer-events: none;
}

@media (max-width: 640px) {
  .wheel { height: min(60vh, 94vw); }
  .wedge-label { font-size: 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  .wheel { transition: none; }
  .wheel-base path { transition: none; }
}

/* ---------- gallery ~ two levels ---------------------------- */
/* Level 1 is the trap door landing: every photograph of that piece at
   once. Level 2 is one of them, large. Escape steps back up a level. */

#gallery {
  position: fixed;
  inset: 0;
  z-index: 300;
  overflow-y: auto;
  padding: clamp(1.2rem, 4vw, 3rem);
  background: color-mix(in oklab, var(--ground) 10%, black);
  animation: gal-in 420ms cubic-bezier(.2,.7,.3,1);
}
#gallery[hidden] { display: none; }
@keyframes gal-in { from { opacity: 0; transform: scale(1.06); } to { opacity: 1; transform: none; } }

#gal-title {
  margin: 0 0 0.3rem;
  font-family: var(--font-display);
  font-size: clamp(2rem, 6vw, 3.6rem);
  font-weight: normal;
  line-height: 1;
  color: oklch(0.92 0.02 300);
  text-align: center;
}
#gal-sub {
  margin: 0 0 2.2rem;
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: oklch(0.66 0.02 300);
  text-align: center;
}

#gal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1rem;
  margin: 0 auto;
  padding: 0;
  max-width: 88rem;
  list-style: none;
}
#gal-grid button {
  display: block; width: 100%; padding: 0;
  border: 1px solid oklch(0.9 0.02 300 / 0.18);
  background: none; cursor: pointer;
  transition: transform 240ms cubic-bezier(.2,.7,.3,1), border-color 200ms;
}
#gal-grid img { display: block; width: 100%; height: 11rem; object-fit: cover; }
#gal-grid figcaption,
#gal-grid .g-cap {
  display: block;
  padding: 0.5rem 0.6rem 0.65rem;
  font-family: var(--font-text);
  font-size: 0.8rem;
  line-height: 1.35;
  text-align: left;
  color: oklch(0.82 0.02 300);
}
#gal-grid button:hover, #gal-grid button:focus-visible {
  transform: translateY(-3px); border-color: var(--accent);
}

#gal-out {
  margin: 2.6rem 0 0;
  font-family: var(--font-data);
  font-size: 0.62rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: oklch(0.6 0.02 300);
  text-align: center;
}

/* level 2 */
#gal-figure {
  position: fixed;
  inset: 0;
  margin: 0;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in oklab, var(--ground) 6%, black);
  z-index: 2;
}
#gal-figure[hidden] { display: none; }
#gal-img {
  max-width: 100%; max-height: 74vh; width: auto;
  border: 1px solid oklch(0.9 0.02 300 / 0.22);
}
#gal-figure figcaption {
  display: flex; gap: 1rem; align-items: baseline; justify-content: center;
  flex-wrap: wrap;
  margin-top: 1.1rem;
  font-family: var(--font-text);
  font-size: 1rem;
  color: oklch(0.9 0.02 300);
}
#gal-count { font-family: var(--font-data); font-size: 0.72rem; letter-spacing: 0.1em; color: var(--accent); white-space: nowrap; }
#gal-set   { font-family: var(--font-data); font-size: 0.66rem; letter-spacing: 0.16em; text-transform: uppercase; color: oklch(0.66 0.02 300); white-space: nowrap; }

#gal-close, #gal-prev, #gal-next {
  position: fixed;
  width: 2.8rem; height: 2.8rem;
  font-size: 1.2rem; line-height: 1;
  color: oklch(0.9 0.02 300);
  background: color-mix(in oklab, var(--ground) 10%, black);
  border: 1px solid oklch(0.9 0.02 300 / 0.3);
  border-radius: 50%;
  cursor: pointer;
  z-index: 3;
}
#gal-close { top: clamp(0.75rem,2vw,1.5rem); right: clamp(0.75rem,2vw,1.5rem); }
#gal-prev  { left: clamp(0.75rem,2vw,1.5rem); top: 50%; transform: translateY(-50%); }
#gal-next  { right: clamp(0.75rem,2vw,1.5rem); top: 50%; transform: translateY(-50%); }
#gal-close:hover, #gal-prev:hover, #gal-next:hover { border-color: oklch(0.9 0.02 300); }

@media (max-width: 640px) {
  .rider button { width: 3.6rem; }
  .rider-cap { display: none; }
  #gal-prev, #gal-next { display: none; }
  #gal-strip button { width: 2.8rem; height: 2rem; }
}

@media (prefers-reduced-motion: reduce) {
  #gallery { animation: none; }
  .gate-btn { transition: none; }
}
