/* TONG SONG / PHOTO — editorial archive
   No webfont: the neo-grotesk stack resolves locally on every target device,
   so there is no network request, no FOUT, and nothing competing with the
   photographs for bandwidth. */

:root {
  --bg: #f7f7f4;
  --ink: #111111;
  --ink-60: #6f6f6a;
  --ink-30: #b6b6b0;
  --rule: #e2e2dc;

  --m: clamp(20px, 2.2vw, 32px);   /* page margin */
  --g: clamp(10px, 0.95vw, 18px);  /* image gap   */
  --head: 46px;

  --sans: "Helvetica Neue", Helvetica, Inter, "Segoe UI", Arial, sans-serif;
  --micro: 11px;
  --small: 12px;

  --ease: cubic-bezier(0.22, 0.7, 0.2, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--small);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: inherit; text-decoration: none; }

:focus-visible { outline: 1px solid var(--ink); outline-offset: 3px; }

/* Utility -------------------------------------------------------------- */

.label {
  font-size: var(--micro);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 400;
}

.num { font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }

.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Header --------------------------------------------------------------- */

.head {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: clamp(16px, 3vw, 44px);
  height: var(--head);
  padding: 0 var(--m);
  background: var(--bg);
  border-bottom: 1px solid transparent;
  transition: border-color 200ms linear;
}

.head[data-stuck] { border-bottom-color: var(--rule); }

.head__id { font-size: var(--micro); letter-spacing: 0.11em; text-transform: uppercase; font-weight: 500; white-space: nowrap; }

.head__nav { display: flex; gap: clamp(12px, 1.6vw, 22px); }

.head__nav a,
.head__nav button {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--ink-60);
  font-size: var(--micro); letter-spacing: 0.09em; text-transform: uppercase;
  transition: color 160ms linear;
}

.head__nav a:hover,
.head__nav button:hover { color: var(--ink); }
.head__nav button[aria-current="true"] { color: var(--ink); }

.head__end { display: flex; align-items: center; gap: clamp(12px, 1.6vw, 22px); }

.head__end a, .head__end button {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--ink-60);
  font-size: var(--micro); letter-spacing: 0.09em; text-transform: uppercase;
  transition: color 160ms linear;
  white-space: nowrap;
}

.head__end a:hover, .head__end button:hover { color: var(--ink); }

/* Grid ----------------------------------------------------------------- */

.grid {
  display: flex; flex-direction: column; gap: var(--g);
  /* Rows scale with the container, so an unbounded width on a very large
     display makes every row taller and empties the fold. Cap it. */
  max-width: 1760px;
  padding: var(--g) var(--m) calc(var(--m) * 2.5);
  margin-inline: auto;
}

.row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: var(--g);
  aspect-ratio: var(--rar);
}

.cell {
  position: relative;
  grid-column: span var(--s);
  min-height: 0;
  display: block;
  background: #eeeee9;      /* holds the frame before the image decodes */
  overflow: hidden;
  cursor: zoom-in;
}

.cell img {
  /* Absolutely positioned so the image contributes nothing to grid track
     sizing. Left in flow, a tall photograph out-votes the row's aspect-ratio
     and the row grows to the image's own height. */
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
  display: block;
  transition: opacity 260ms var(--ease);
}

.cell:hover img, .cell:focus-visible img { opacity: 0.92; }

/* Cimaise's LQIP contract: the placeholder is blurred and slightly scaled so
   its soft edge never shows against the frame. */
.cell img.lqip { filter: blur(14px); transform: scale(1.04); }
.cell img.lqip-done { filter: none; transform: none; transition: filter 420ms var(--ease), transform 420ms var(--ease); }

/* Load-in: one gentle lift per row, never per image — a page of independently
   popping tiles reads as a feed, not an edit. */
.row[data-reveal] { opacity: 0; transform: translateY(8px); }
.row[data-reveal="in"] {
  opacity: 1; transform: none;
  transition: opacity 520ms var(--ease), transform 520ms var(--ease);
}

.grid[data-filtering] { opacity: 0; transition: opacity 160ms linear; }

/* Caption readout — the signature.
   Metadata never covers a photograph and the page grows no extra furniture to
   hold it. Hovering a frame writes its archive line into the empty middle of
   the header, the way a slide viewer reads out whichever frame you are
   holding up. One fixed place, so the eye learns where to look. */

.rail {
  display: flex; align-items: baseline; gap: 12px;
  margin-left: auto;
  font-size: var(--micro); letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 220ms var(--ease);
  pointer-events: none;
}

.rail[data-on] { opacity: 1; }
.rail__n { color: var(--ink-30); }
.rail__t { color: var(--ink); }
.rail__m { color: var(--ink-60); }

/* Below this the header has no room to spare, so the readout stands down. */
@media (max-width: 1100px) { .rail { display: none; } }

/* Panels: Info + Lightbox ---------------------------------------------- */

/* Driven by CSS animation rather than a JS two-step attribute flip: rAF is
   throttled whenever the page is not painting, which left panels stuck at
   opacity 0. An animation starts on its own the moment the element displays. */
.panel { position: fixed; inset: 0; z-index: 60; background: var(--bg);
         animation: panel-in 300ms var(--ease) both; }
.panel[hidden] { display: none !important; }
.panel[data-closing] { animation: panel-out 220ms var(--ease) both; }

@keyframes panel-in  { from { opacity: 0 } to { opacity: 1 } }
@keyframes panel-out { from { opacity: 1 } to { opacity: 0 } }

.panel__close {
  position: absolute; top: 0; right: 0; z-index: 2;
  height: var(--head); padding: 0 var(--m);
  appearance: none; background: none; border: 0; cursor: pointer;
  font: inherit; color: var(--ink-60);
  font-size: var(--micro); letter-spacing: 0.09em; text-transform: uppercase;
}
.panel__close:hover { color: var(--ink); }

/* Info */

.info { display: flex; align-items: flex-end; height: 100%; padding: var(--m); }
.info__body { max-width: 30ch; font-size: 13px; line-height: 1.75; }
.info__body p { margin: 0 0 1.5em; }
.info__body a { border-bottom: 1px solid var(--rule); }
.info__body a:hover { border-bottom-color: var(--ink); }
.info__name { text-transform: uppercase; letter-spacing: 0.11em; font-size: var(--micro); }
/* flex-start so each rule hugs its own text instead of stretching the column. */
.info__links { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }

/* Lightbox */

.lb { display: grid; grid-template-rows: var(--head) 1fr auto; }

.lb__bar {
  display: flex; align-items: center; justify-content: center; gap: 18px;
  padding: 0 var(--m);
  font-size: var(--micro); letter-spacing: 0.09em;
}

.lb__bar button {
  appearance: none; background: none; border: 0; padding: 4px 6px; cursor: pointer;
  font: inherit; color: var(--ink-60); transition: color 160ms linear;
}
.lb__bar button:hover { color: var(--ink); }
.lb__count { color: var(--ink); min-width: 8ch; text-align: center; }

.lb__stage { position: relative; min-height: 0; }

/* Absolutely positioned against the stage's padding box. A percentage
   max-height will not resolve here — the stage is a grid track, not a
   definite-height box — so the image is given the box outright and
   object-fit letterboxes it inside. */
/* The <picture> is the inset box — a non-replaced element does stretch to all
   four insets. The <img> then fills it and object-fit letterboxes the
   photograph inside. Insetting the <img> directly does not work: `auto` on a
   replaced element resolves to its intrinsic size instead of stretching. */
.lb__stage picture { position: absolute; inset: 0 var(--m) 4px; display: block; }

.lb__stage img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
}

.lb__stage img {
  opacity: 0;
  transition: opacity 300ms var(--ease);
}
.lb__stage img[data-ready] { opacity: 1; }

.lb__cap {
  display: flex; justify-content: center; gap: 14px;
  padding: 18px var(--m) calc(var(--m) * 0.9);
  font-size: var(--micro); letter-spacing: 0.08em; text-transform: uppercase;
}
.lb__cap .t { color: var(--ink); }
.lb__cap .m { color: var(--ink-60); }

/* Series --------------------------------------------------------------- */

.series-head {
  padding: calc(var(--m) * 1.6) var(--m) calc(var(--m) * 0.9);
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
}
.series-head h1 { margin: 0; font-size: var(--micro); letter-spacing: 0.11em; text-transform: uppercase; font-weight: 500; }
.series-head .m { color: var(--ink-60); font-size: var(--micro); letter-spacing: 0.08em; text-transform: uppercase; }
.series-head .n { color: var(--ink-30); font-size: var(--micro); letter-spacing: 0.08em; }
.series-note { padding: 0 var(--m) var(--m); max-width: 46ch; color: var(--ink-60); font-size: 13px; line-height: 1.7; }

.empty { padding: calc(var(--m) * 3) var(--m); color: var(--ink-60); max-width: 42ch; line-height: 1.8; }
.empty code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: var(--ink); }

/* Mobile --------------------------------------------------------------- */

@media (max-width: 768px) {
  :root { --head: 42px; --g: 8px; }

  /* Not a squeezed desktop grid: rows re-form as full-width plates and
     portrait pairs, composed at build time. */
  .row {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: none;      /* rows are content-height on a phone */
    aspect-ratio: auto;
  }
  .cell {
    grid-column: span var(--sm);
    aspect-ratio: var(--arm);
  }

  /* Two quiet lines rather than a hamburger: identity and links on the first,
     the categories on their own scrollable line below. On one line the
     categories were crushed to 28px and only ALL survived. */
  .head {
    height: auto; min-height: var(--head);
    flex-wrap: wrap; align-items: baseline;
    gap: 12px; row-gap: 11px;
    padding-top: 13px; padding-bottom: 11px;
  }
  .head__id { flex: 1 1 auto; }
  .head__end { order: 2; gap: 16px; }
  .head__nav {
    order: 3; flex: 1 0 100%;
    gap: 16px; overflow-x: auto;
    scrollbar-width: none; -ms-overflow-style: none;
  }
  .head__nav::-webkit-scrollbar { display: none; }
  .cell { cursor: default; }
  .lb__bar { gap: 24px; }
  .info { align-items: flex-start; padding-top: calc(var(--head) + var(--m)); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  .row[data-reveal] { opacity: 1; transform: none; }
}
