/* ============================================================
   category.css — category grid template T1 (BRIEF §5) and the
   overlay player (§6). Values from reference advertising.dc.html;
   sized in canvas px × --u so the page scales with the chrome.
   ============================================================ */

/* ---- works mosaic: aspect-true editorial rows ---- */
.works {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  max-width: calc(1280 * var(--u));
  margin: calc(52 * var(--u)) auto 0;
  padding: 0 calc(64 * var(--u));
}

.works__row {
  display: flex;
  align-items: flex-start;
}

/* per-category column layout (data "layout": "column" — Fashion):
   one work per row at full content-column width. The row gap makes
   the vertical rhythm a procession, not a feed — generous, but in
   the site's section-spacing register (the caption block below each
   tile adds its own height on top of this gap). Grid categories are
   untouched; on mobile the shared one-per-row packing already rules
   and the gap resets below. */
.works--column {
  row-gap: calc(56 * var(--u));
}

.work {
  position: relative;
  display: flex;
  flex-direction: column;
  width: var(--w); /* row share set by the generator */
  min-width: 0;
  z-index: 1;
  transition: opacity 0.5s ease, filter 0.5s ease;
}

/* each tile is a figure with its own figcaption beneath it */
.work__fig {
  margin: 0;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.work:focus-visible {
  z-index: 2;
}

/* ---- hover choreography, POINTER ONLY (see base.css) ---- */
@media (hover: hover) and (pointer: fine) {
  .work:hover {
    z-index: 2;
  }

  /* Hovering a tile dims every OTHER tile ON THE PAGE (author,
     14.09.2026) — not only the tiles sharing its row, which is what
     `.works__row:has(…)` did. A grid row is a layout unit, not a
     unit of reading: the works beside a tile in the next row are as
     much its neighbours as the ones level with it, and on Fashion,
     where every row holds one work, the effect could never show at
     all.

     THE DIM IS OPACITY, NEVER A FILTER (author, 14.09.2026) — the
     same rule §6c set for the two filmography lists, and for the
     same reason: opacity composites, where a filter re-rasterises
     every other picture on the page on the same half-second the
     hovered tile is running its --hover-zoom-time zoom. The tiles
     used to dim with opacity 0.55 AND saturate(0.75); on one row
     that cost three pictures, page-wide it would have cost
     twenty-three. Same 0.55, same 0.5s ease from the transition on
     .work above. The index mosaic keeps its own choreography and is
     not reached by this selector.

     The guard replaces the `@media (hover: none)` undo that used to
     follow this rule: a tap can leave :hover stuck on a tile, and
     page-wide that would have frozen the whole grid dimmed. Now the
     rule simply is not there to stick. */
  .works:has(.work:hover) .work:not(:hover) {
    opacity: 0.55;
  }
}

.work__frame {
  position: relative;
  width: 100%;
  overflow: hidden;
  background: #161a20;
}

.work__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--grade-filter);
  transition: transform var(--hover-zoom-time) var(--ease-slide), opacity 0.6s ease;
}

.work__img--raw {
  filter: none;
  opacity: 0;
  pointer-events: none;
}

.work:focus-visible .work__img {
  transform: scale(1.03);
}

@media (hover: hover) and (pointer: fine) {
  .work:hover .work__img {
    transform: scale(1.03);
  }
}

/* The zoom's duration is a token, not a number, because it is the
   one dial that was found to matter (see --hover-zoom-time). A
   will-change promotion sat here between 12.09 morning and evening;
   it was measured on the author's machine and made no difference,
   so it is gone rather than left to cost GPU memory for nothing.
   docs/OPEN-ISSUES.md holds why. */

.work:focus-visible .work__img--raw {
  opacity: 1;
}

@media (hover: hover) and (pointer: fine) {
  .work:hover .work__img--raw {
    opacity: 1;
  }
}

/* the hover loop (base.css .hover-loop) rides the same 1.03 zoom as
   the still under it — created mid-hover, it renders already scaled,
   so there is no snap back to 1.0 when the video fades in over the
   enlarged still. (Index strips scale the whole tile, so their loop
   tracks automatically and needs none of this.) */
@media (hover: hover) and (pointer: fine) {
  .work__frame .hover-loop {
    transition: opacity 0.18s ease, transform var(--hover-zoom-time) var(--ease-slide);
  }

  .work:hover .hover-loop,
  .work:focus-visible .hover-loop {
    transform: scale(1.03);
  }
}

/* under-tile caption, one pattern on all breakpoints (approved
   grammar): CLIENT — “Title” (quotes only for named campaigns).
   Metadata never truncates with ellipsis anywhere on the site —
   long captions wrap to a second line instead. */
.work__caption {
  --caption-lh: 1.6;
  --caption-pad-top: calc(9 * var(--u));
  --caption-pad-bottom: calc(12 * var(--u));
  padding: var(--caption-pad-top) calc(14 * var(--u)) var(--caption-pad-bottom) 1px;
  text-align: left;
  line-height: var(--caption-lh);
}

/* Two-line reserve on the GRID categories (Advertising, AI), desktop
   only (13.09.2026, author). A caption is one line for most works and
   two for a few — a long client, or a client plus a set mark — and the
   row then carried a ragged block of type under an even row of
   pictures. Every caption now pays for two lines whether it uses them
   or not, so the rhythm below the pictures is regular.

   box-sizing is border-box site-wide, so the reserve has to carry the
   block padding as well as the two line boxes. First declaration is
   the fallback for engines without `lh` — the line-height token times
   two, in em, which resolves against the caption's own font-size
   exactly as `lh` does; the second wins wherever `lh` is understood.

   Fashion is layout: "column" — one work per row, so there is nothing
   to line up and it is deliberately excluded. Below 768px every
   category is one work per row for the same reason (see the mobile
   block, which resets this). */
.works:not(.works--column) .work__caption {
  min-height: calc(
    2 * var(--caption-lh) * 1em + var(--caption-pad-top) + var(--caption-pad-bottom)
  );
  min-height: calc(2lh + var(--caption-pad-top) + var(--caption-pad-bottom));
}

.work__client {
  font-family: var(--font-mono);
  font-size: max(calc(10 * var(--u)), 9px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #C9CBC8;
}

.work__name {
  font-family: var(--font-mono);
  font-size: max(calc(9.5 * var(--u)), 8.5px);
  letter-spacing: 0.12em;
  color: var(--secondary);
}

/* multi-asset campaigns: small mono mark, click goes to the case page */
.work__case-mark {
  font-family: var(--font-mono);
  font-size: max(calc(9 * var(--u)), 8px);
  letter-spacing: 0.14em;
  color: var(--numerals);
}

/* ============================================================
   Overlay player (BRIEF §6) — scrim INK @92%, centered 16:9
   Vimeo slot, mono caption line, PREV/NEXT, ESC/scrim close
   ============================================================ */

.player[hidden] {
  display: none;
}

.player {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(14, 17, 22, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: player-in 0.4s ease both;
}

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

/* material + dither inside the player's frame (BRIEF §4b): the
   scrim reads as the same medium as the page background, not UI
   black — statically (an overlay is a separate plane: no chroma,
   no weave). z:-1 keeps both above the ink scrim and below the
   video and chrome — never on top of the picture itself. */
.player::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  mix-blend-mode: screen;
  image-rendering: pixelated;
  background-image: var(--bg-material-image);
  background-repeat: repeat;
  background-size: calc(240px * var(--bg-material-scale)) calc(240px * var(--bg-material-scale));
  opacity: var(--bg-material-op);
}

.player::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  mix-blend-mode: soft-light;
  background-image: var(--bg-dither-image);
  background-repeat: repeat;
  background-size: 240px 240px;
  opacity: var(--bg-dither-op);
}

.player__close {
  position: absolute;
  top: calc(26 * var(--u));
  right: calc(34 * var(--u));
  z-index: 2;
  padding: 8px;
  font-family: var(--font-mono);
  font-size: max(calc(10.5 * var(--u)), 9px);
  letter-spacing: var(--meta-tracking);
  color: var(--secondary);
  background: none;
  border: 0;
  cursor: pointer;
  transition: color 0.3s ease;
}

.player__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  padding: 14px;
  font-family: var(--font-mono);
  font-size: 20px;
  line-height: 1;
  color: var(--secondary);
  background: none;
  border: 0;
  cursor: pointer;
  transition: color 0.3s ease;
  user-select: none;
}

.player__arrow--prev { left: calc(26 * var(--u)); }
.player__arrow--next { right: calc(26 * var(--u)); }

.player__close:hover,
.player__close:focus-visible,
.player__arrow:hover,
.player__arrow:focus-visible {
  color: var(--paper);
}

/* aspect-true: --player-ar is set per work by overlay-player.js,
   so the frame keeps the work's native ratio, uncropped.
   --player-chrome is the vertical room everything BUT the picture
   needs; the column solves its width against what is left, so the
   filmstrip is paid for out of the column, never out of the frame —
   the 9:16 is held whatever else is on screen. */
.player {
  --strip-thumb-w: 56px;
  --strip-h: calc(var(--strip-thumb-w) * 16 / 9);
}

.player__col {
  --player-chrome: 200px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: min(
    calc(1160 * var(--u)),
    84vw,
    calc((100vh - var(--player-chrome)) * var(--player-ar, 1.7778))
  );
}

.player--set .player__col {
  /* + the strip and the gap above it */
  --player-chrome: calc(200px + var(--strip-h) + 16px);
}

/* Vimeo embed slot; aspect-ratio set per work by overlay-player.js */
.player__frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #161a20;
}

.player__frame iframe,
.player__frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* photo-dump works (videoUrl null): still + caption, no playback */
.player__placeholder {
  position: absolute;
  inset: 0;
}

/* UNGRADED, like everything else in this viewer (author-reported,
   12.09.2026: photographs looked washed out with the gallery open,
   "as if the filter had not come off"). It had not. The §4 grade is a
   browsing-state treatment — it sits on tiles at rest and hover lifts
   it. The overlay is not browsing, it is looking: a video plays here
   with no filter, the filmstrip thumbs carry none either, and the
   hover loop's grade is locked to lift (§8b). This still was the only
   thing in the viewer left graded, so a photo series showed duller
   than the video next to it. */
.player__placeholder img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: none;
}

/* the YouTube facade inside the overlay uses the shared .facade
   component (base.css) — created by overlay-player.js on render */

/* ---- filmstrip: a SET work's own navigation (BRIEF §6) ----
   The edge arrows mean "next work" everywhere on the site, so a
   series cannot borrow them — it gets this instead. It is also the
   only thing on screen that says a campaign holds four films, which
   is why it never goes away on touch. Rendered only for set works;
   every other work has no strip in the DOM at all. */
.player__strip {
  /* the thumbs' offsetParent — overlay-player.js centres the current
     one with offsetLeft, which without this measures against .player
     and scrolls to the wrong place */
  position: relative;
  display: flex;
  /* `safe` keeps a strip that OVERFLOWS from centring itself into a
     clipped leading edge — a four-film set still centres, a ten-photo
     one starts at its left and scrolls */
  justify-content: safe center;
  gap: 8px;
  overflow-x: auto;
  scrollbar-width: none; /* the strip is the affordance, not a bar */
}

.player__strip::-webkit-scrollbar {
  display: none;
}

/* The strip shares one HEIGHT and lets each thumb's width follow its
   own ratio — overlay-player.js writes the item's aspect inline. A
   fixed 9:16 thumb was right while every set was a set of vertical
   films, but a photo series mixes ratios (Naro Paris runs 3:4, 4:5
   and 9:16 in one campaign) and cropping them to a common box would
   break the rule the rest of the site keeps: the aspect is the work's,
   never the surface's. A 9:16 item still measures exactly 56px wide,
   so film sets are unchanged. */
.player__thumb {
  height: var(--strip-h);
  width: auto;
  aspect-ratio: 9 / 16;
  padding: 0;
  border: 0;
  background: #161a20;
  cursor: pointer;
  /* 0.35, not the 0.4 first shipped: at 56px on a phone the gap to
     the lit one has to be unmistakable, and these stills are bright
     enough to still read clearly this far down (author, 03.09.2026) */
  opacity: 0.35;
  transition: opacity 0.18s ease; /* the hover-crossfade timing (§8b) */
}

.player__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* ungraded on purpose: these are navigation, not imagery — the §4
     grade on a 56px thumb only muddies which one is lit */
}

.player__thumb:hover {
  opacity: 0.7;
}

.player__thumb:focus-visible {
  opacity: 1;
  outline: 1px solid var(--paper);
  outline-offset: 2px;
}

.player__thumb.is-current {
  opacity: 1;
}

/* caption row: CLIENT — “Title” — year (+ credits line) · counter */
.player__caption-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 24px;
  padding: 0 2px;
}

/* metadata never truncates with ellipsis: the caption wraps */
.player__captions {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.player__caption {
  margin: 0;
}

/* credits, line 2 (approved grammar): [role] Name · [role] Name —
   present only when credits exist */
/* One credit per line (BRIEF §6b's "3-line cap", built in
   overlay-player.js). Joined with " · " into one paragraph they wrapped
   wherever the column ran out and split roles and names in half on a
   9:16 work — "[production &" / "model]". Each credit also binds its
   own spaces, so a line can only ever end where a credit ends. */
.player__credits {
  margin: 0;
  font-family: var(--font-mono);
  font-size: max(calc(9 * var(--u)), 8px);
  letter-spacing: 0.14em;
  color: var(--secondary);
}

.player__credit {
  display: block;
}

.player__client {
  font-family: var(--font-mono);
  font-size: max(calc(10 * var(--u)), 9px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #C9CBC8;
}

.player__rest {
  font-family: var(--font-mono);
  font-size: max(calc(9.5 * var(--u)), 8.5px);
  letter-spacing: 0.12em;
  color: var(--secondary);
}

.player__counter {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: max(calc(9.5 * var(--u)), 8.5px);
  letter-spacing: 0.2em;
  color: var(--numerals);
}

/* ---- mobile (≤768px): grid stacks, captions tighten ---- */
@media (max-width: 768px) {
  /* the desktop rows dissolve; works flow in one wrapping line and
     re-pack by --wm (build.mjs): landscapes full-width, portraits in
     aspect-true pairs sharing one height — no slot stays empty */
  .works {
    margin-top: 36px;
    padding: 0; /* main already carries the 20px page inset */
    flex-direction: row;
    flex-wrap: wrap;
    column-gap: 6px;
  }

  .works__row {
    display: contents;
  }

  /* the column layout dissolves into the shared mobile packing —
     tile size and 18px rhythm stay exactly as on grid categories */
  .works--column {
    row-gap: 0;
  }

  .work {
    width: var(--wm, 100%);
    margin-bottom: 18px;
  }

  /* smaller mono, tightened tracking; wraps, never truncates */
  .work__caption {
    --caption-lh: 1.55;
    --caption-pad-top: 8px;
    --caption-pad-bottom: 0px;
    padding: var(--caption-pad-top) 6px var(--caption-pad-bottom) 1px;
    line-height: var(--caption-lh);
  }

  /* no two-line reserve on a phone: one work per row, so there is no
     neighbour to line up with and the reserve would only add a gap */
  .works:not(.works--column) .work__caption {
    min-height: 0;
  }

  .work__client {
    font-size: 9px;
    letter-spacing: 0.12em;
  }

  .work__name,
  .work__case-mark {
    font-size: 8.5px;
    letter-spacing: 0.1em;
  }
}

/* ---- mobile: inset overlay player (§6 revised) — the INK scrim
   stays, the video sits in a ~16px frame; a safe-area-aware top
   bar holds the X so controls never overlap the video ---- */
@media (max-width: 768px) {
  .player {
    padding: calc(env(safe-area-inset-top, 0px) + 52px)
             16px
             calc(env(safe-area-inset-bottom, 0px) + 16px);
  }

  .player__close {
    top: calc(env(safe-area-inset-top, 0px) + 14px);
    right: 16px;
  }

  /* the column adapts to the work's aspect ratio (16:9 and 9:16
     alike) and leaves room for the top bar + caption strip */
  .player__col {
    --player-chrome: 165px;
    width: min(
      100%,
      calc((100svh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - var(--player-chrome)) * var(--player-ar, 1.7778))
    );
    gap: 12px;
  }

  /* the filmstrip stays on touch — it is the ONLY affordance a set
     has there, since the gestures that flip works look identical to
     the ones that would flip films. Same thumb size as desktop; the
     video is inset by the strip instead of the strip overlapping it. */
  .player--set .player__col {
    --player-chrome: calc(165px + var(--strip-h) + 12px);
  }

  /* reserved caption strip below the video: room for two lines,
     wrapping, never ellipsized */
  .player__captions {
    min-height: 32px;
    gap: 6px;
  }

  .player__caption-row {
    padding: 0;
  }
}

/* touch devices: gestures replace chrome — swipes step/close
   (overlay-player.js), so the arrows and the ESC hint go away */
@media (hover: none) {
  .player__arrow,
  .player__esc {
    display: none;
  }
}
