/* ============================================================
   KAZI — WORK PAGE (redesign)
   Landing: title → card stack (+ EXPLORE pill) → story → stats.
   Explorer overlay: infinite draggable gallery (ALL PROJECTS),
   FEATURED BRANDS slider, SNIPPETS (scattered / arranged).
   ============================================================ */
.work-page { background: var(--color-accent); color: var(--color-bg); overflow-x: clip; }
.work-page .nav { opacity: 1; }

/* ---------------- Landing ---------------- */
.wl { padding-top: calc(var(--nav-h) + clamp(24px, 4vh, 56px)); }

.wl__hero { text-align: center; }
.wl__title {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(40px, 5.6vw, 84px); line-height: 0.8; letter-spacing: -0.05em;
  color: var(--color-bg);
}

/* Card stack stage
   The whole collage is laid out in percentages of this box (see work.js
   `stack` fractions), so sizing the box is the only lever needed — the
   composition keeps its exact ratio at every width.

   The box hugs the collage: 0.684 is the old 0.9 times the 0.76 of the height
   the cards actually occupied. The `stack` fractions were rescaled to match,
   so cards keep their size and spacing while the empty band that used to sit
   between the title and the top card is gone.

   Width is the larger of the previous behaviour and a viewport-aware size,
   so the stage only ever grows: on wide/tall displays it scales up towards
   1180px, while `--wl-fit` keeps it from pushing the title and the CTA off
   screen on short laptop viewports. */
.wl__stage {
  --wl-fit: calc((100vh - var(--nav-h) - clamp(120px, 16vh, 210px)) / 0.684);
  position: relative; margin: clamp(20px, 3vw, 44px) auto 0;
  width: max(min(62vw, 900px), min(68vw, 1180px, var(--wl-fit)));
  aspect-ratio: 1 / 0.684;
}
@supports (height: 100svh) {
  .wl__stage { --wl-fit: calc((100svh - var(--nav-h) - clamp(120px, 16vh, 210px)) / 0.684); }
}
.wl__card {
  position: absolute; overflow: hidden; background: var(--color-dark);
  box-shadow: 0 18px 50px rgba(17, 17, 17, 0.22);
}
.wl__card img { width: 100%; height: 100%; object-fit: cover; }

/* EXPLORE pill (accent pill + cream dot, per button inspo) */
.wl__explore, .ex__pill {
  display: inline-flex; align-items: center; gap: 9px;
  background: var(--color-accent); color: var(--color-bg);
  font-family: var(--font-body); font-weight: 500; font-size: 12px;
  letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap;
  padding: 9px 16px; border: none; border-radius: 3px;
  box-shadow: 0 8px 24px rgba(17, 17, 17, 0.32);
}
.wl__explore i, .ex__pill i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--color-bg); flex: 0 0 auto;
}
/* The pill is the only affordance telling people the stack is clickable, so
   it scales with the stage instead of staying pinned at 12px — on a large
   display a 12px label on an 1180px collage reads as decoration. */
.wl__explore {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 30; cursor: pointer;
  font-size: clamp(12px, 0.95vw, 17px);
  gap: clamp(9px, 0.7vw, 13px);
  padding: clamp(10px, 0.8vw, 15px) clamp(18px, 1.5vw, 30px);
  transition: transform 0.3s ease;
}
.wl__explore i { width: clamp(7px, 0.55vw, 10px); height: clamp(7px, 0.55vw, 10px); }
.wl__explore:hover { transform: translate(-50%, -50%) scale(1.06); }

/* Call-to-action blink — label and dot pulse together on one beat so the
   button reads as live without the pill itself moving or flashing. The label
   animates via `color`, which touches only the glyphs and leaves the accent
   background alone. Stops on hover: once the pointer is there the invitation
   has landed. Scoped to .wl__explore so the cursor pill (.ex__pill) stays
   static. */
.wl__explore { animation: wl-label 1.2s ease-in-out infinite; }
.wl__explore i { animation: wl-blink 1.2s ease-in-out infinite; }
.wl__explore:hover { animation: none; color: var(--color-bg); }
.wl__explore:hover i { animation: none; opacity: 1; }
@keyframes wl-label {
  0%, 100% { color: rgba(255, 255, 240, 1); }
  50%      { color: rgba(255, 255, 240, 0.3); }
}
@keyframes wl-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.15; }
}
@media (prefers-reduced-motion: reduce) {
  .wl__explore, .wl__explore i { animation: none; }
}
.wl__stage { cursor: pointer; }

/* Story row */
.wl__story {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: clamp(28px, 5vw, 90px);
  padding: clamp(56px, 8vw, 130px) clamp(28px, 3.4vw, 52px) 0;
}
.wl__story-h {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(38px, 4.8vw, 74px); line-height: 0.9; letter-spacing: -0.04em;
  color: var(--color-bg);
}
.wl__story-p {
  max-width: 34ch; text-align: right;
  font-size: clamp(14px, 1.25vw, 19px); line-height: 1.35;
}

/* Stats (carried over) */
.wp__stats { padding: clamp(56px, 9vh, 120px) var(--pad) clamp(48px, 7vh, 96px); text-align: center; }
.wp__stats-row {
  max-width: 1000px; margin: 0 auto; display: flex; justify-content: space-between;
  gap: var(--space-lg); flex-wrap: wrap;
}
.wp__stat { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1 1 auto; }
.wp__stat-num, .wp__stat-label {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  color: var(--color-bg); line-height: 0.82; letter-spacing: -0.04em;
}
.wp__stat-num { font-size: clamp(34px, 3.8vw, 54px); }
.wp__stat-label { font-size: clamp(26px, 2.9vw, 40px); }
.wp__tagline {
  margin-top: clamp(24px, 4vh, 40px); font-family: var(--font-body);
  font-size: clamp(14px, 1.05vw, 17px); color: var(--color-bg);
}

/* ============================================================
   EXPLORER OVERLAY
   ============================================================ */
.ex {
  position: fixed; inset: 0; z-index: 1500;
  background: var(--color-accent);
  opacity: 0; visibility: hidden;
}
.ex.is-open { opacity: 1; visibility: visible; }
.ex, .ex * { cursor: none; }
/* The chrome keeps a real cursor — the custom pill is suppressed over it, so
   without this the pointer vanishes entirely there. `.ex *` above matches
   every descendant, so the override has to reach descendants too: an icon,
   a menu row's label, the panel background. */
.ex__top, .ex__top *, .ex__list, .ex__list * { cursor: default; }
.ex__top button, .ex__top button *,
.ex__list button, .ex__list button *,
.ex__list a, .ex__list a * { cursor: pointer; }
.ex__strip { cursor: grab; }
.ex__strip:active { cursor: grabbing; }

.ex__canvas { position: absolute; inset: 0; overflow: hidden; touch-action: none; }
.ex__item {
  position: absolute; left: 0; top: 0; overflow: hidden;
  background: var(--color-dark); will-change: transform;
}
.ex__item img, .ex__item video {
  width: 100%; height: 100%; object-fit: cover; pointer-events: none;
  -webkit-user-drag: none; user-select: none;
}

/* --- Case-study plate --------------------------------------------------
   Only the covers lead anywhere, and on the canvas the only thing marking
   them out was being wider than the reels — which reads as a layout accident
   rather than an invitation. They become a cream plate here: the picture
   inset, the brand named beneath it, and a line saying what a click does.

   The plate takes its caption out of the card's own box rather than adding
   to it. The tile positions are solved so nothing overlaps anywhere on the
   wrapping canvas, and growing the covers by a caption's height would push
   them into their neighbours; the picture gives up those few pixels instead.

   Shown on the canvas alone. The featured view already prints the brand name
   under the card, and the landing stack is a collage where captions would be
   noise — so the plate is built for every cover and revealed by .is-all. */
.ex__canvas.is-all .ex__item--cover {
  background: var(--color-bg);
  display: flex; flex-direction: column;
  padding: clamp(5px, 0.5vw, 11px);
  gap: clamp(4px, 0.4vw, 9px);
  /* The covers are authored at six different widths — 0.17vw to 0.32vw of
     the tile — so the caption has to answer to the card it is on, not to the
     viewport. Sizing it in vw made the same type sit comfortably on the widest
     and truncate on the narrowest. */
  container-type: inline-size;
}
.ex__canvas.is-all .ex__item--cover img {
  flex: 1 1 auto; height: auto; min-height: 0;
}
.ex__cap { display: none; }
.ex__canvas.is-all .ex__item--cover .ex__cap {
  flex: 0 0 auto;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: clamp(6px, 0.8vw, 18px);
  color: var(--color-accent);
}
.ex__cap-name {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  letter-spacing: -0.01em; line-height: 1;
  font-size: clamp(9px, 4.3cqw, 21px);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ex__cap-hint {
  flex: 0 0 auto;
  font-family: var(--font-body); text-transform: uppercase;
  letter-spacing: 0.08em; line-height: 1;
  font-size: clamp(5px, 1.85cqw, 10px);
  white-space: nowrap;
}
/* the arrow stands in for the sentence when there is no room for it */
.ex__cap-arrow {
  display: none; flex: 0 0 auto;
  font-family: var(--font-body); line-height: 1;
  font-size: clamp(9px, 4cqw, 17px);
}
/* Below this the line and the brand name cannot share a row without one of
   them being cut. The name is the half that has to survive — it is what tells
   you this card is a case study — so the line gives way to the arrow. The
   threshold dropped from 370px when the copy was shortened; the shorter the
   line, the more cards can carry it. */
@container (max-width: 250px) {
  .ex__cap-hint { display: none; }
  .ex__cap-arrow { display: block; }
}
/* accent wash used on featured side-cards & arranged side-reels */
.ex__item .wash {
  position: absolute; inset: 0; background: var(--color-accent);
  opacity: 0; pointer-events: none; transition: opacity 0.5s ease;
}

/* dim layer behind a focused reel */
.ex__dim {
  position: absolute; inset: 0; z-index: 40;
  background: rgba(160, 55, 30, 0.45);
  -webkit-backdrop-filter: blur(9px); backdrop-filter: blur(9px);
  opacity: 0; visibility: hidden; transition: opacity 0.45s ease, visibility 0.45s;
}
.ex__dim.is-on { opacity: 1; visibility: visible; }

/* Close the focused reel. Only where there is no pointer: with a mouse the
   dim behind the reel is the target, but a phone shows almost no dim, and the
   bar's CLOSE leaves the explorer altogether. JS sets `top` from the bar. */
.ex__unfocus {
  position: fixed; right: clamp(12px, 3.4vw, 20px); z-index: 60;
  width: 30px; height: 30px; border-radius: 50%;
  display: none; place-items: center;
  padding: 0; border: 1px solid rgba(255, 255, 240, 0.4);
  background: rgba(20, 18, 16, 0.28); color: var(--color-bg);
  font-family: var(--font-body); font-size: 16px; line-height: 1;
  cursor: pointer;
}
.ex__unfocus.is-on { display: grid; }
@media (hover: hover) { .ex__unfocus.is-on { display: none; } }

/* featured label */
.ex__label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; justify-content: space-between; align-items: flex-end;
  padding: 0 clamp(20px, 2.6vw, 40px) clamp(18px, 2.4vw, 36px);
  pointer-events: none; opacity: 0;
}
.ex__label-name, .ex__label-num {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  letter-spacing: -0.03em; line-height: 0.85;
  font-size: clamp(38px, 6.2vw, 96px); color: var(--color-bg);
}

/* explorer top block
   Solid accent band: cards drift underneath it, and cream labels on top of a
   photo were unreadable — which hides the only way out of the explorer. */
.ex__top {
  position: absolute; left: 0; right: 0; top: 0; z-index: 60;
  background: var(--color-accent);
}
.ex__nav {
  display: flex; justify-content: space-between; align-items: center;
  padding: clamp(18px, 2.2vw, 32px) clamp(20px, 2.6vw, 40px);
}

.ex__nav button {
  background: none; border: none; padding: 0; color: var(--color-bg);
  font-family: var(--font-body); font-weight: 500; font-size: clamp(13px, 1.05vw, 16px);
  letter-spacing: 0.12em; text-transform: uppercase;
}
.ex__links { display: flex; gap: clamp(20px, 3vw, 52px); }
.ex__links button { position: relative; opacity: 0.75; transition: opacity 0.25s ease; }
.ex__links button.is-active { opacity: 1; }
.ex__links button.is-active::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -6px; height: 1px;
  background: var(--color-bg);
}
.ex__right { display: flex; align-items: center; gap: clamp(16px, 2vw, 30px); }

/* arranged / scattered toggle: 4 dots morph between a row and a scatter */
.ex__mode {
  position: relative; width: 30px; height: 30px;
  opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s;
}
.ex__mode.is-visible { opacity: 1; visibility: visible; }
.ex__mode span {
  position: absolute; width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-bg); left: 0; top: 0;
  transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
/* scattered arrangement (shows when current mode is arranged → click to scatter) */
.ex__mode span:nth-child(1) { transform: translate(4px, 14px); }
.ex__mode span:nth-child(2) { transform: translate(15px, 13px); }
.ex__mode span:nth-child(3) { transform: translate(20px, 4px); }
.ex__mode span:nth-child(4) { transform: translate(19px, 23px); }
/* arranged (row) arrangement (shows when current mode is scattered → click to arrange) */
.ex__mode.show-arranged span:nth-child(1) { transform: translate(0px, 12px); }
.ex__mode.show-arranged span:nth-child(2) { transform: translate(8px, 12px); }
.ex__mode.show-arranged span:nth-child(3) { transform: translate(16px, 12px); }
.ex__mode.show-arranged span:nth-child(4) { transform: translate(24px, 12px); }


/* ---------------- Industry filter ----------------
   One icon button in the nav, opening a menu — a row of eleven chips sitting
   permanently across the top read as a wall of options before anyone had
   asked a question. Options are generated from INDUSTRIES in work.js. Picking
   one opens the list view: the infinite canvas is a showcase, the list is the
   way to actually find a video, so a filter always lands somewhere readable. */
.ex__filterwrap { position: relative; }
/* Scoped to .ex__nav so it outweighs `.ex__nav button { padding: 0 }` above —
   a plain class loses to that on specificity, which is why these pills were
   drawn with their border against the text. */
.ex__nav .ex__filterbtn {
  display: inline-flex; align-items: center; gap: 9px;
  border: 1px solid rgba(255, 255, 240, 0.34); border-radius: 3px;
  padding: 8px 17px; background: none; color: var(--color-bg);
  transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease;
}
.ex__filterbtn svg { width: 15px; height: 12px; flex: 0 0 auto; }
.ex__filterbtn:hover { border-color: var(--color-bg); }
/* lit while a filter is actually narrowing the results, so the state is
   visible without opening the menu */
.ex__filterbtn.is-on,
.ex__filterbtn[aria-expanded="true"] {
  background: var(--color-bg); color: var(--color-accent); border-color: var(--color-bg);
}

.ex__filtermenu {
  position: absolute; right: 0; top: calc(100% + 10px); z-index: 70;
  min-width: 246px; max-height: 62vh; overflow-y: auto;
  background: var(--color-bg); color: var(--color-accent);
  border-radius: 3px; padding: 6px;
  box-shadow: 0 18px 44px rgba(17, 17, 17, 0.34);
}
.ex__filtermenu[hidden] { display: none; }
.ex__filtermenu-h {
  font-family: var(--font-body); font-weight: 500; font-size: 10px;
  letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.55;
  padding: 8px 10px 6px;
}
.ex__filtermenu .ex__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  width: 100%; padding: 9px 10px; background: none; border: none; border-radius: 2px;
  color: var(--color-accent); text-align: left;
  font-family: var(--font-body); font-weight: 500; font-size: 12px;
  letter-spacing: 0.08em; text-transform: uppercase;
  transition: background 0.2s ease;
}
.ex__filtermenu .ex__opt:hover { background: rgba(215, 78, 49, 0.12); }
.ex__filtermenu .ex__opt.is-on { background: var(--color-accent); color: var(--color-bg); }
.ex__filtermenu .ex__opt b { font-weight: 500; opacity: 0.55; }

/* canvas / list toggle */
.ex__nav .ex__layout {
  border: 1px solid rgba(255, 255, 240, 0.34); border-radius: 3px;
  padding: 8px 17px; background: none;
  transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease;
}
.ex__layout:hover { border-color: var(--color-bg); }
.ex__layout[aria-pressed="true"] { background: var(--color-bg); color: var(--color-accent); border-color: var(--color-bg); }

/* snippets on/off for the All Projects canvas — the canvas opens on the case
   studies alone, and this brings the reels in around them */
.ex__nav .ex__reeltoggle {
  display: inline-flex; align-items: center; gap: 9px;
  border: 1px solid rgba(255, 255, 240, 0.34); border-radius: 3px;
  padding: 8px 17px; background: none;
  transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease;
}
.ex__nav .ex__reeltoggle[hidden] { display: none; }
.ex__reeltoggle:hover { border-color: var(--color-bg); }
.ex__reeltoggle i {
  position: relative; width: 20px; height: 11px; flex: 0 0 auto;
  border: 1px solid currentColor; border-radius: 6px;
}
.ex__reeltoggle i::after {
  content: ''; position: absolute; top: 1px; left: 1px;
  width: 7px; height: 7px; border-radius: 50%; background: currentColor;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.ex__reeltoggle[aria-pressed="true"] { background: var(--color-bg); color: var(--color-accent); border-color: var(--color-bg); }
.ex__reeltoggle[aria-pressed="true"] i::after { transform: translateX(9px); }

/* ---------------- List view ----------------
   Sits above the canvas and below the top block, and scrolls on its own.
   Brands read as rows (a name is the thing you scan for); snippets read as a
   poster grid, which is how vertical video is actually browsed. */
.ex__list {
  position: absolute; inset: 0; z-index: 55;
  background: var(--color-accent); color: var(--color-bg);
  overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0.35s;
}
.ex__list.is-open { opacity: 1; visibility: visible; }
.ex__list-inner {
  /* clears the top block by its measured height (work.js sets --ex-top-h),
     since the nav wraps to two rows on narrow viewports */
  padding: calc(var(--ex-top-h, 120px) + clamp(10px, 1.8vh, 24px))
           clamp(20px, 2.6vw, 40px) clamp(60px, 8vh, 110px);
  max-width: 1560px; margin: 0 auto;
}
.ex__list-sec + .ex__list-sec { margin-top: clamp(30px, 3.4vw, 58px); }
.ex__list-sec[hidden] { display: none; }
.ex__list-h {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  letter-spacing: -0.02em; font-size: clamp(22px, 2.4vw, 38px); line-height: 1;
  padding-bottom: clamp(10px, 1.1vw, 15px); margin-bottom: clamp(10px, 1.2vw, 18px);
  border-bottom: 1px solid rgba(255, 255, 240, 0.28);
}
.ex__list-h span {
  font-family: var(--font-body); font-weight: 500; font-size: clamp(11px, 0.8vw, 14px);
  letter-spacing: 0.1em; opacity: 0.6; margin-left: 10px; vertical-align: middle;
}

/* On a phone the strip shows one poster at a time, and nothing about a
   still card says it slides. The neighbouring posters peek in from the
   edges (see the responsive block) and this line says it outright. Pointer
   devices get the drag cursor and the pill instead, so it stays hidden. */
.ex__strip-hint {
  display: none;
  /* centred over the strip it describes, which is itself centred */
  margin: -4px 0 10px; text-align: center;
  font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; opacity: 0.62;
}
.ex__strip-hint span { opacity: 0.8; }
@media (hover: none), (max-width: 768px) {
  .ex__strip-hint { display: block; }
}

/* brand rows */
.ex__row {
  display: grid; grid-template-columns: clamp(84px, 8vw, 128px) 1fr auto;
  align-items: center; gap: clamp(14px, 2vw, 32px); width: 100%;
  padding: clamp(10px, 1vw, 16px) 6px; text-align: left;
  background: none; border: none; border-bottom: 1px solid rgba(255, 255, 240, 0.16);
  color: var(--color-bg); transition: background 0.25s ease, padding-left 0.25s ease;
}
.ex__row[hidden] { display: none; }
.ex__row:hover { background: rgba(255, 255, 240, 0.09); padding-left: 16px; }
.ex__row-thumb {
  aspect-ratio: 3 / 2; overflow: hidden; background: var(--color-dark);
}
.ex__row-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ex__row-name {
  font-family: var(--font-display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(17px, 1.7vw, 30px); line-height: 1.05; letter-spacing: -0.02em;
}
.ex__row-tag {
  display: block; margin-top: 5px;
  font-family: var(--font-body); font-weight: 400; font-size: clamp(11px, 0.78vw, 13px);
  letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.62;
}
.ex__row-go {
  font-family: var(--font-body); font-weight: 500; font-size: clamp(11px, 0.8vw, 13px);
  letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap; opacity: 0.75;
}

/* snippet strip
   Mirrors the arranged Snippets layout on the canvas: one card holds the
   centre at full size and colour, its neighbours sit smaller under an accent
   wash, and the row is dragged or scrolled sideways. Built as a flex row in
   its own horizontal scroller rather than absolute positioning, so it stays
   inside the list's scroll and needs no layout maths on resize. */
/* The frame the carousel slides inside. The strip itself is wider than this
   and is moved by a transform, exactly as the case-study reel rows are. */
.ex__stripwrap { overflow: hidden; padding: 4px 0 6px; }
.ex__strip {
  position: relative;
  /* 9:16 means height is 1.78x this, so the width is capped against the
     viewport height too — otherwise the centred card outgrows the fold on a
     short laptop screen */
  --tile-w: min(clamp(240px, 26vw, 430px), 40vh);
  display: flex; align-items: center; width: max-content;
  gap: clamp(2px, 0.4vw, 8px);
  /* vertical panning still belongs to the page; sideways is the carousel's */
  touch-action: pan-y;
  will-change: transform;
}
/* Every slot is the same width and the size difference is a transform, so
   promoting a card to the centre moves nothing in the layout. Animating the
   width instead would shift every slot after it and leave the carousel's
   arithmetic — which assumes one fixed pitch — landing off-centre. */
.ex__tile {
  flex: 0 0 var(--tile-w); width: var(--tile-w);
  padding: 0; text-align: left; background: none; border: none;
  color: var(--color-bg);
}
.ex__tile[hidden] { display: none; }
.ex__tile-in {
  display: block; transform: scale(0.7); transform-origin: center center;
  /* paced to the slide it accompanies, so the card finishes growing as it
     arrives in the middle rather than a beat after */
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.ex__tile.is-centre .ex__tile-in { transform: scale(1); }
/* display: block is load-bearing. This is a <span>, so without it the box is
   inline — and Safari resolves the absolutely positioned <video>'s width: 100%
   against the inline span, which is 0 wide. The clip then played, sound and
   all, in a box with no width, and only the poster <img> in front of it could
   be seen. Chrome measured against the <img> instead, so it never showed. */
.ex__tile-media {
  display: block;
  position: relative; aspect-ratio: 9 / 16; overflow: hidden;
  background: var(--color-dark);
}
.ex__tile-media img, .ex__tile-media video {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.ex__tile-media video { position: absolute; inset: 0; }
/* the accent wash that pushes the off-centre cards back */
.ex__tile-wash {
  position: absolute; inset: 0; background: var(--color-accent);
  opacity: 0.55; pointer-events: none; transition: opacity 0.26s ease;
}
.ex__tile.is-centre .ex__tile-wash { opacity: 0; }
.ex__tile-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 50px; height: 50px; border-radius: 50%;
  background: rgba(215, 78, 49, 0.9);
  display: grid; place-items: center;
  opacity: 0; transition: transform 0.25s ease, opacity 0.25s ease;
}
.ex__tile-play::after {
  content: ''; border-style: solid; border-width: 6px 0 6px 10px;
  border-color: transparent transparent transparent var(--color-bg); margin-left: 3px;
}
/* The badge means "this is paused, tap to resume" — not "this has not started
   yet". Shown whenever the centre card is merely the centre card, it flashed
   orange on every swipe for the whole slide, and stayed put for as long as the
   clip took to buffer. Only a real pause, or a refused autoplay, raises it. */
.ex__tile.is-centre.is-paused .ex__tile-play { opacity: 1; }
.ex__tile.is-centre.is-paused:hover .ex__tile-play { transform: translate(-50%, -50%) scale(1.12); }
.ex__tile-name {
  display: block; margin-top: 12px;
  font-family: var(--font-body); font-weight: 500; font-size: 15px;
  letter-spacing: 0.02em; opacity: 0.5; transition: opacity 0.26s ease;
}
.ex__tile-tag {
  display: block; margin-top: 3px;
  font-family: var(--font-body); font-size: 12px;
  letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.5;
  transition: opacity 0.26s ease;
}
.ex__tile.is-centre .ex__tile-name { opacity: 1; }
.ex__tile.is-centre .ex__tile-tag { opacity: 0.62; }
.ex__list-empty {
  font-family: var(--font-body); font-size: clamp(14px, 1.1vw, 18px);
  opacity: 0.7; padding: clamp(30px, 6vh, 70px) 0;
}
.ex__list-empty[hidden] { display: none; }

/* cursor pill (follows the pointer inside the explorer) */
.ex__pill {
  position: fixed; left: 0; top: 0; z-index: 80; pointer-events: none;
  transform: translate(-50%, -50%);
  opacity: 0; transition: opacity 0.2s ease;
}
.ex__pill.is-visible { opacity: 1; }

@media (hover: none) {
  .ex, .ex * { cursor: auto; }
  .ex__pill { display: none; }
}

/* Leaving for a case study: fade everything but the expanding ghost.
   .ex__list has to be in here too — a case study opened from the list view
   left the rows and the snippet strip sitting there at full opacity while the
   ghost grew over them, so the old page was still on screen, colliding with
   the one arriving. */
body.is-leaving .ex__top, body.is-leaving .ex__label,
body.is-leaving .ex__canvas, body.is-leaving .ex__pill,
body.is-leaving .ex__list { opacity: 0 !important; transition: opacity 0.4s ease; }

/* ---------------- Responsive ---------------- */
@media (max-width: 768px) {
  .wl { padding-top: calc(var(--nav-h) + clamp(16px, 3vh, 36px)); }
  .wl__title { font-size: clamp(32px, 9vw, 54px); }
  /* floor keeps the stage usable on short landscape phones, where --wl-fit alone would collapse it */
  .wl__stage { width: max(min(88vw, 420px), min(92vw, var(--wl-fit))); }
  .wl__story { flex-direction: column; align-items: flex-start; gap: var(--space-lg); }
  .wl__story-p { text-align: left; max-width: none; }
  .wp__stats-row { gap: var(--space-sm); }
  .wp__stat-label { font-size: clamp(15px, 3.4vw, 22px); }
  .wp__stat-num { font-size: clamp(26px, 6vw, 40px); }
  .ex__label-name { font-size: clamp(28px, 9vw, 48px); }
  /* Two clean rows rather than one row that wraps mid-label: the views
     across the top, the controls beneath them. Each label stays on one line
     — "All Projects" broken over two was the main thing making this nav
     hard to read. */
  .ex__nav {
    flex-wrap: wrap; gap: 12px 0;
    padding: clamp(12px, 2.5vw, 24px) clamp(16px, 4vw, 28px);
  }
  .ex__nav button { font-size: 12px; letter-spacing: 0.08em; }
  .ex__links { width: 100%; gap: clamp(14px, 4.5vw, 28px); }
  .ex__links button { white-space: nowrap; }
  .ex__right { width: 100%; justify-content: space-between; gap: 4px; }
  /* An industry name can be long ("Textile & Manufacturing"), and the row has
     no width to spare on a phone: let the filter pill be the thing that gives,
     shrinking and clipping its label, so Close is never pushed off the edge. */
  /* the wrapper is the flex child here, so it is the one that has to be
     allowed to shrink — the button inside it can only follow */
  .ex__nav .ex__filterwrap { flex: 0 1 auto; min-width: 0; }
  .ex__nav .ex__filterbtn { flex: 0 1 auto; min-width: 0; max-width: 100%; }
  /* block, not inline: text-overflow needs a block box, and an inline one
     reports its full text as a minimum width, so the pill would not shrink */
  .ex__nav .ex__filterbtn span { display: block; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .ex__nav .ex__layout, .ex__nav .ex__closebtn, .ex__nav .ex__reeltoggle { flex: 0 0 auto; }
  /* the arrange toggle and the snippets toggle never show together, so on a
     phone the hidden one gives its width back to the row */
  .ex__mode:not(.is-visible) { display: none; }
  /* The filter was an unlabelled icon here, so nothing said it was a filter.
     The word is short and the row now has space for it. */

  .ex__nav .ex__filterbtn, .ex__nav .ex__layout, .ex__nav .ex__closebtn, .ex__nav .ex__reeltoggle {
    /* these read as filled pills once they are on, so the label needs room to
       sit inside one rather than against its edges */
    padding: 9px 12px;
    white-space: nowrap;
  }
  .ex__nav .ex__closebtn { padding-right: 0; }
  .ex__filterbtn svg { width: 13px; height: 11px; }
  /* dropping the wrapper's positioning re-anchors the menu to .ex__top — the
     whole nav width — so it can never hang off the side of a phone */
  .ex__filterwrap { position: static; }
  .ex__filtermenu {
    right: clamp(16px, 4vw, 28px); left: clamp(16px, 4vw, 28px);
    min-width: 0; top: 100%;
  }
  /* narrower than the screen so the next and previous posters show at the
     edges — on a phone that peek is the only thing saying the strip scrolls */
  .ex__strip { --tile-w: min(64vw, 300px, 42vh); gap: 8px; }
  .ex__row { grid-template-columns: clamp(70px, 22vw, 100px) 1fr; }
  .ex__row-go { display: none; }
}

@media (max-width: 480px) {
  .wl__title { font-size: clamp(28px, 10vw, 42px); }
  .wl__stage { width: max(min(94vw, 380px), min(96vw, var(--wl-fit))); }
  .wl__story-h { font-size: clamp(28px, 8vw, 46px); }
  .wl__story-p { font-size: clamp(13px, 4vw, 16px); }
  .wp__stat-num { font-size: clamp(22px, 7vw, 34px); }
  .wp__stat-label { font-size: clamp(14px, 4vw, 20px); }
}

/* phones: four controls share the row on the canvas, so the pills pull
   their side padding in rather than clipping "Filter" */
@media (max-width: 440px) {
  .ex__nav .ex__filterbtn, .ex__nav .ex__layout, .ex__nav .ex__reeltoggle { padding-left: 9px; padding-right: 9px; }
  .ex__nav .ex__filterbtn, .ex__nav .ex__reeltoggle { gap: 6px; }
  .ex__nav .ex__right button { letter-spacing: 0.05em; }
  .ex__reeltoggle i { width: 16px; }
  .ex__reeltoggle[aria-pressed="true"] i::after { transform: translateX(5px); }
}
