/* ==========================================================================
   /world/ — the plates.

   The colour law, resolved rather than dodged. NORTH: "the page carries no
   colour of its own… the only colour anywhere is inside the product
   screenshots, which is what makes them land." A wall of paintings looks like
   it breaks that. It does not: the chrome here — paper, ink, hairlines, the
   backdrop, the caption, the lightbox surround — is the same neutral as every
   other page, and every hue on the screen is inside a plate. This page is the
   rule applied to a page where the art is the content, not an exception to it.
   The contrast between the two is the point.
   ========================================================================== */

.world__head { padding-top: clamp(3rem, 8vw, 6rem); padding-bottom: clamp(2rem, 4vw, 3rem); }
.world__h { max-width: 14ch; }
.world__lede { margin-top: clamp(1.25rem, 2.5vw, 1.75rem); max-width: 38rem; }

.world__grid-band { padding-top: 0; }
.world__note { margin-top: clamp(3rem, 6vw, 5rem); }
.world__note .prose { color: var(--ink-2); max-width: 40rem; }
.world__note .prose + .prose { margin-top: 1.1rem; }

/* ---------- the grid ----------
   5 × 4 at full width, and it steps down rather than reflowing to whatever
   fits: a gallery that silently becomes 7 × 3 on one machine and 4 × 5 on
   another is not a composition, it is a bag of pictures. */

.plates {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(0.5rem, 1.4vw, 1.1rem);
}
@media (max-width: 1100px) { .plates { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .plates { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .plates { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---------- a plate ----------
   Square card, picture contained inside at its natural aspect. NORTH law 3
   forbids crop-zoom, and these run from 0.64 to 1.87 — so the card is the
   mount and the plate sits on it, the way a specimen sheet holds a specimen.
   The empty paper around a tall plate is not wasted space; it is the mount. */

/* Escalated per KP: "the cards should hover and move way more." Every number
   below is the loud version of what was here — bigger tilt, real lift, a
   shadow that reads as depth rather than a hairline. --ease-plate-release is
   a deliberate third timing system, not an oversight of law 5's two-system
   rule: it exists only for the instant the pointer LEAVES a plate, where a
   slight overshoot on the way back to flat is what makes the card read as
   sprung metal rather than a CSS transition. Every other transform on this
   page — the tilt tracking itself, the entrance, the neighbour response —
   stays on --ease-inplace / --ease-structural exactly as law 5 requires. */
.plate {
  position: relative;
  transition: transform 460ms var(--ease-structural), opacity 460ms var(--ease-structural), filter 460ms var(--ease-structural);
}
/* A lifted, scaled card is taller and wider than its grid cell; without this
   its next sibling — painted later in DOM order — draws on top of it despite
   both sitting in the same, un-transformed grid. Raising the stacking context
   only while something is actually interacting with the card is what keeps
   nineteen flat plates from silently gaining z-index they never need. */
.plate:hover,
.plate:focus-within {
  z-index: 20;
}

.plate__link {
  --ease-plate-release: cubic-bezier(0.34, 1.56, 0.64, 1);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: clamp(0.5rem, 1.2vw, 0.9rem);
  background: var(--paper-raise);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  overflow: hidden;
  text-decoration: none;
  /* 44px targets, law 5 — the card is far larger, but the floor is explicit
     so a future narrower breakpoint cannot quietly go under it. */
  min-height: 44px;
  transition:
    border-color var(--t-inplace) var(--ease-inplace),
    filter 460ms var(--ease-plate-release),
    transform 460ms var(--ease-plate-release);
}

/* The sheen. A soft highlight riding the same --tx/--ty the tilt already
   reads, so it looks like light glancing off glass as the card turns toward
   the pointer rather than a separate effect layered on top. It lives inside
   plate__link so its own overflow:hidden keeps it honestly confined to the
   mount — never bleeding onto the caption below. */
.plate__link::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: overlay;
  background: radial-gradient(
    circle at calc((var(--tx, 0) + 1) * 50%) calc((var(--ty, 0) + 1) * 50%),
    color-mix(in srgb, white 70%, transparent) 0%,
    transparent 55%
  );
  transition: opacity 420ms var(--ease-inplace);
}

/* Law 3's never-upscale/natural-aspect clause was retired 2026-07-26 (see
   NORTH) once the source plates stopped justifying it: eight are regenerated
   at one aspect and 2048px, and the rest are due the same pass. The mount
   used to hold whatever aspect a plate happened to arrive at — seven ratios
   from 0.64 to 1.87 — which is why <picture> had to be a real centering box
   with the <img> letterboxed inside it via object-fit: contain. A uniform
   plate doesn't need a mount, it needs to fill one: <picture> just fills the
   card, and the image fills <picture> exactly, no centering or letterboxing
   left to do. (History: this used to matter because <picture> is an inline
   wrapper with auto height, so a percentage max-height on an <img> inside it
   resolves against nothing — six portrait plates were silently cropped by the
   mount's overflow:hidden until audit-world's no-spill check caught it.) */
.plate__link picture {
  display: block;
  width: 100%;
  height: 100%;
}

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

/* The title sits under the plate, not on it. A caption over the picture is a
   caption competing with the picture, and these are dense. */
.plate__cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.55rem;
  min-height: 1.5em;
}
.plate__title {
  font-size: 0.78rem;
  color: var(--ink-2);
  letter-spacing: -0.005em;
  line-height: 1.25;
}
.plate__n {
  font-family: var(--mono);
  font-size: 0.62rem;
  color: var(--ink-3);
  letter-spacing: 0.1em;
  flex: none;
}

.plate__link:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 3px;
  /* Keyboard focus gets the lift even without a fine pointer to drive a tilt.
     Declared BEFORE the pointer-fine block below on purpose: both rules match
     :focus-visible at equal specificity, so source order decides, and the
     richer tilt+shadow version has to win when a fine pointer is also
     present — a mouse user tabbing through the grid should see the same
     depth a hover gives them, not a plainer fallback clobbering it. */
  transform: translateY(-14px) scale(1.05);
  transition: transform 460ms var(--ease-plate-release);
}

/* ---------- hover ----------
   Direct-manipulation tilt (law 5). --tx/--ty are set by world.js from the
   pointer's position within the card, through CSSOM — never a style attribute,
   which production's style-src 'self' drops on the floor.

   Pointer-fine only. On a touch screen there is no hover state, and a tilt
   that fires on tap is a tilt that fights the tap. */
@media (hover: hover) and (pointer: fine) {
  .plate__link {
    transform:
      perspective(760px)
      rotateX(calc(var(--ty, 0) * -13deg))
      rotateY(calc(var(--tx, 0) * 13deg))
      translateZ(0);
    /* Flat at rest: a shadow that tracks the tilt, not a static drop. Two
       layers — a tight contact shadow and a long throw — read as a card
       actually lifting off paper rather than a blur turned up. drop-shadow,
       not box-shadow: box-shadow painted on this element would be clipped by
       its own overflow:hidden (needed to contain the plate image), and a
       shadow that vanishes at the card's own edge reads as a glitch, not
       depth. filter escapes that clip because it shadows the rendered
       silhouette, not the box. */
    filter:
      drop-shadow(0 1px 1px rgba(0, 0, 0, 0.05))
      drop-shadow(0 0 0 rgba(0, 0, 0, 0));
  }
  .plate__link:hover,
  .plate__link:focus-visible {
    border-color: var(--hairline-2);
    transform:
      perspective(760px)
      rotateX(calc(var(--ty, 0) * -13deg))
      rotateY(calc(var(--tx, 0) * 13deg))
      translateY(-20px)
      scale(1.08)
      translateZ(0);
    filter:
      drop-shadow(calc(var(--tx, 0) * -6px) calc(var(--ty, 0) * -6px + 10px) 10px rgba(0, 0, 0, 0.16))
      drop-shadow(calc(var(--tx, 0) * -14px) calc(var(--ty, 0) * -14px + 30px) 26px rgba(0, 0, 0, 0.22));
    will-change: transform, filter;
  }
  .plate__link:hover::after,
  .plate__link:focus-visible::after { opacity: 1; }
  /* While the pointer is actually moving over the card the tilt tracks it, so
     it must not also be easing — the two together read as lag, not weight.
     data-tilting (set by world.js on the first pointermove) strips the
     transform transition down to a near-instant one for exactly that window;
     the spring gets to fire on the two edges either side of it — the initial
     pop up to meet the pointer, and the pointerleave settle back to flat. */
  .plate__link[data-tilting] {
    transition: border-color var(--t-inplace) var(--ease-inplace), filter 60ms linear;
  }
  /* Every other plate recedes a little when one is being handled, so the
     hovered card reads as having come forward relative to the wall, not just
     grown in place. :has() degrades invisibly where unsupported — the tilt
     and lift above still work on their own. */
  .plates:has(.plate__link:hover) .plate:not(:has(.plate__link:hover)),
  .plates:has(.plate__link:focus-visible) .plate:not(:has(.plate__link:focus-visible)) {
    transform: scale(0.965);
    opacity: 0.68;
    filter: saturate(0.85);
  }
  .plate__link:hover + .plate__cap .plate__title,
  .plate:hover .plate__title,
  .plate:focus-within .plate__title { color: var(--ink); }
}

/* ---------- entrance ----------
   The one place on this site where something animates in on load rather than
   on scroll. It is the page's signature moment, the way the plot is the
   homepage's: twenty plates arriving in sequence and settling into the grid.

   The delay is per-position in CSS rather than per-element in JS, because the
   count is fixed at twenty and nth-child costs nothing at runtime — and
   because a stagger written in CSS cannot be dropped by a CSP. */
.js .plate { opacity: 0; }
.js .plate.is-in {
  opacity: 1;
  animation: plate-in 860ms var(--ease-structural) both;
}
/* Escalated from a 14px settle to a real arrival: the plate comes up from
   below the fold's shadow, tilted as if just set down, and sharpens into
   focus rather than merely fading in. Blur is a one-shot cost on entrance
   only — nothing here runs per-frame, so it never touches the pointer/scroll
   performance budget. */
@keyframes plate-in {
  from {
    opacity: 0;
    transform: perspective(760px) translateY(64px) scale(0.88) rotateX(8deg);
    filter: blur(7px);
  }
  60% { filter: blur(0); }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}
.plates .plate:nth-child(1)  { animation-delay: 0ms; }
.plates .plate:nth-child(2)  { animation-delay: 34ms; }
.plates .plate:nth-child(3)  { animation-delay: 68ms; }
.plates .plate:nth-child(4)  { animation-delay: 102ms; }
.plates .plate:nth-child(5)  { animation-delay: 136ms; }
.plates .plate:nth-child(6)  { animation-delay: 60ms; }
.plates .plate:nth-child(7)  { animation-delay: 94ms; }
.plates .plate:nth-child(8)  { animation-delay: 128ms; }
.plates .plate:nth-child(9)  { animation-delay: 162ms; }
.plates .plate:nth-child(10) { animation-delay: 196ms; }
.plates .plate:nth-child(11) { animation-delay: 120ms; }
.plates .plate:nth-child(12) { animation-delay: 154ms; }
.plates .plate:nth-child(13) { animation-delay: 188ms; }
.plates .plate:nth-child(14) { animation-delay: 222ms; }
.plates .plate:nth-child(15) { animation-delay: 256ms; }
.plates .plate:nth-child(16) { animation-delay: 180ms; }
.plates .plate:nth-child(17) { animation-delay: 214ms; }
.plates .plate:nth-child(18) { animation-delay: 248ms; }
.plates .plate:nth-child(19) { animation-delay: 282ms; }
.plates .plate:nth-child(20) { animation-delay: 316ms; }

/* ---------- the lightbox ----------
   A real <dialog>, opened with showModal(). That is not laziness — it is the
   only way to get a genuine focus trap, Escape-to-close, inertness of the page
   behind and a ::backdrop without writing four fragile things by hand. */

.lightbox {
  border: 0;
  padding: 0;
  max-width: 100vw;
  max-height: 100dvh;
  width: 100vw;
  height: 100dvh;
  background: transparent;
  overflow: hidden;
}
.lightbox::backdrop { background: color-mix(in srgb, var(--paper) 92%, transparent); }
@media (prefers-color-scheme: dark) { .lightbox::backdrop { background: color-mix(in srgb, var(--paper) 94%, transparent); } }

.lightbox__in {
  display: grid;
  grid-template-rows: 1fr auto;
  width: 100%;
  height: 100%;
  padding: clamp(1rem, 3vw, 2.5rem);
  gap: clamp(0.75rem, 2vw, 1.25rem);
}

.lightbox__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}
/* Fit, never fill. Sources top out around 1700px, so on a large display a
   plate letterboxes rather than covering — which is correct, and is NORTH
   law 3 rather than a shortcoming to be papered over with object-fit: cover. */
.lightbox__stage img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r);
  box-shadow: var(--lift-flat);
}

.lightbox__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(1rem, 3vw, 2rem);
  border-top: 1px solid var(--hairline);
  padding-top: 0.9rem;
}
.lightbox__title {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1rem + 0.8vw, 1.6rem);
  letter-spacing: -0.016em;
  line-height: 1.15;
  color: var(--ink);
}
.lightbox__meta {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  margin-top: 0.35rem;
}
.lightbox__acts { display: flex; gap: 0.4rem; flex: none; }

.lightbox__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0 0.75rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  background: transparent;
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--t-inplace) var(--ease-inplace), border-color var(--t-inplace) var(--ease-inplace);
}
.lightbox__btn:hover { color: var(--ink); border-color: var(--hairline-2); }
.lightbox__btn[disabled] { opacity: 0.35; cursor: default; }

/* ---------- the morph ----------
   Native View Transitions. The card and the opened plate share a
   view-transition-name, so the browser tweens between them — no library, no
   FLIP maths, no measuring. Where it is unsupported nothing is named, the API
   call is skipped, and the lightbox simply appears. That is a fine fallback. */
::view-transition-old(plate),
::view-transition-new(plate) {
  animation-duration: 420ms;
  animation-timing-function: var(--ease-structural);
}

/* ---------- reduced motion ----------
   No entrance, no tilt, no lift, no morph. The grid is simply there, and
   clicking a plate still opens it — which is the whole function. */
@media (prefers-reduced-motion: reduce) {
  .plate,
  .plate:hover,
  .plate:focus-within { transition: none; transform: none; opacity: 1; filter: none; z-index: auto; }
  .js .plate { opacity: 1; }
  .js .plate.is-in { animation: none; }
  .plate__link,
  .plate__link:hover,
  .plate__link:focus-visible {
    transform: none;
    filter: none;
    transition: border-color var(--t-inplace) var(--ease-inplace);
  }
  .plate__link:hover,
  .plate__link:focus-visible { box-shadow: none; border-color: var(--hairline-2); }
  .plate__link::after,
  .plate__link:hover::after,
  .plate__link:focus-visible::after { opacity: 0; transition: none; }
  .plates:has(.plate__link:hover) .plate:not(:has(.plate__link:hover)),
  .plates:has(.plate__link:focus-visible) .plate:not(:has(.plate__link:focus-visible)) {
    transform: none; opacity: 1; filter: none;
  }
  ::view-transition-old(plate),
  ::view-transition-new(plate) { animation: none; }
}

/* ---------- no JS ----------
   Each card is a plain link to the full-size plate. The page degrades to a
   grid of pictures you can click, which is a perfectly good page. */
html.no-js .plate { opacity: 1; }
