/* ===========================================================================
   Case study layout.

   Second pass. The first version stacked identical "heading left, grid below"
   blocks, so every section had the same rhythm and the page read as a list.
   This one is editorial: full-bleed dark bands break the light page, a sticky
   rail carries the commentary so headings stop repeating down the centre, and
   the render catalogue runs as one horizontal band instead of a uniform grid.

   Type and colour are unchanged — Poppins and the V7 token set, with the
   client's red used only as the project accent.
   ======================================================================== */

.case {
  --accent: #0f9ded;              /* brand blue, same beam as the rest of V8 */
  --ink: var(--black, #1b1b1b);
  --paper: var(--grey, #fbfbfb);
  --hair: var(--grey--10, #e8e8e8);
  --mute: var(--grey--50, #8b8b8b);
  --r: 20px;

  font-family: Poppins, sans-serif;
  color: var(--ink);
  background: var(--paper);

  /* clip, not hidden: hidden would kill position:sticky on the rail */
  overflow-x: clip;
}

/* Break out of any centred container to the full viewport width. 100vw counts
   the vertical scrollbar, so subtract it or every band overflows by ~15px. */
.case-bleed {
  width: calc(100vw - (100vw - 100%));
  margin-left: calc(50% - (100vw - (100vw - 100%)) / 2);
}

.case-wrap {
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 40px;
}

/* --- Reveal --------------------------------------------------------------*/
.reveal {
  opacity: 0;
  transform: translate3d(0, 24px, 0);
  transition: opacity .8s cubic-bezier(.22, .61, .36, 1),
              transform .8s cubic-bezier(.22, .61, .36, 1);
  transition-delay: var(--d, 0ms);
}

.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* --- Eyebrow -------------------------------------------------------------*/
.case-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}

.case-eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* ===========================================================================
   1. Hero — full-bleed dark band, artwork bleeding off the bottom edge
   ======================================================================== */
.case-hero {
  background: var(--ink);
  color: #fff;
  padding-top: 104px;   /* clears the fixed bar from the shell */
  padding-bottom: 116px;
  overflow: hidden;
  /* The opening band should hold the page on its own before anything scrolls.
     The back link stays pinned at the top and the title group is pushed to the
     foot of the band, so the two never collide however long the headline runs. */
  min-height: 78vh;
  min-height: 78svh;
  display: flex;
  flex-direction: column;
}

.case-hero > .case-wrap {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  width: 100%;
}

.case-hero .case-eyebrow { margin-top: auto; }

/* The hand-built NeRo page ends its hero on a full-bleed image, which carries
   its own space; there the padding would double up. */
.case-hero:has(.case-hero-art) { padding-bottom: 0; min-height: 0; }

.case-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 72px;
}

.case-topbar img.case-logo { height: 22px; width: auto; display: block; }

.case-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: .875rem;
  font-weight: 500;
  color: rgba(255, 255, 255, .62);
  text-decoration: none;
  transition: color .25s ease;
}

.case-back:hover { color: #fff; }

/* 21px tall is a mouse target. On a phone this is the only way back to
   the archive, so it gets a thumb's worth of height. */
@media (max-width: 991px) {
  .case-back { padding: 11px 0; }
}
.case-back span { transition: transform .25s ease; display: inline-block; }
.case-back:hover span { transform: translateX(-3px); }

/* Webflow's base sheet colours h1 dark; on the dark band that made the first
   line invisible until it was set explicitly. */
.case-title {
  font-size: clamp(3rem, 7vw, 6.5rem);
  line-height: .98;
  letter-spacing: -.035em;
  font-weight: 600;
  color: #fff;
  margin: 26px 0 0;
  max-width: 15ch;
}

.case-hero h1, .case-hero h2 { color: #fff; }
.case-dark h2, .case-band h2, .case-cta h2, .case-rail h2 { color: inherit; }

.case-title em {
  font-style: normal;
  color: var(--mute);
}

.case-lede {
  margin: 34px 0 0;
  max-width: 52ch;
  font-size: 1.1875rem;
  line-height: 1.6;
  color: rgba(255, 255, 255, .66);
}

.case-hero-art {
  margin-top: 64px;
  border-radius: var(--r) var(--r) 0 0;
  overflow: hidden;
}

.case-hero-art img {
  width: 100%;
  height: auto;
  display: block;
  transform: scale(1.05);
  transition: transform 1.6s cubic-bezier(.22, .61, .36, 1);
}

.case-hero-art.is-in img { transform: none; }

/* ===========================================================================
   2. Meta bar — replaces the four stat boxes with one typographic row
   ======================================================================== */
.case-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-bottom: 1px solid var(--hair);
}

.case-meta-item {
  padding: 30px 28px 30px 0;
  border-left: 1px solid var(--hair);
  padding-left: 28px;
}

.case-meta-item:first-child { border-left: 0; padding-left: 0; }

.case-meta-key {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--mute);
}

.case-meta-val {
  margin-top: 10px;
  font-size: .9375rem;
  line-height: 1.45;
  font-weight: 500;
}

.case-meta-val a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--accent); }

/* ===========================================================================
   3. Body — sticky rail carries the commentary, content column carries images
   ======================================================================== */
.case-block {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 72px;
  padding: 104px 0;
  border-bottom: 1px solid var(--hair);
}

.case-block:last-of-type { border-bottom: 0; }

.case-rail { position: sticky; top: 96px; align-self: start; }

.case-rail h2 {
  margin: 14px 0 0;
  font-size: 1.75rem;
  line-height: 1.14;
  letter-spacing: -.02em;
  font-weight: 600;
}

.case-rail p {
  margin: 16px 0 0;
  font-size: .9375rem;
  line-height: 1.65;
  color: var(--mute);
}

.case-rail dl {
  margin: 26px 0 0;
  border-top: 1px solid var(--hair);
}

.case-rail dt {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--mute);
  padding-top: 16px;
}

.case-rail dd {
  margin: 5px 0 16px;
  font-size: .875rem;
  line-height: 1.55;
  border-bottom: 1px solid var(--hair);
  padding-bottom: 16px;
}

.case-rail dd:last-child { border-bottom: 0; }

/* --- Content column: deliberately uneven image rhythm ---------------------*/
.case-stack { display: grid; gap: 20px; }

.case-figure {
  border-radius: var(--r);
  overflow: hidden;
  background: var(--grey--3, #f4f4f4);
}

.case-figure img { width: 100%; height: auto; display: block; }

.case-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }

/* offset pair — the second tile drops, breaking the flat grid line */
.case-duo.is-offset > :nth-child(2) { transform: translateY(44px); }

.case-figure.is-dark { background: #0d0d0d; }

/* ===========================================================================
   4. Render band — one horizontal marquee instead of a 3-up grid
   ======================================================================== */
.case-band { padding: 104px 0; background: var(--grey--3, #f4f4f4); }

.case-band-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  flex-wrap: wrap;
  margin-bottom: 44px;
}

.case-band-head h2 {
  margin: 12px 0 0;
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  line-height: 1.06;
  letter-spacing: -.028em;
  font-weight: 600;
  max-width: 18ch;
}

/* The client's own mark, in the empty half of the band head. Sized to sit
   beside the heading rather than compete with it, and matched to the band
   colour by the file itself, so nothing has to be knocked out here. */
.case-band-mark { width: min(240px, 42vw); flex: 0 0 auto; }

.case-band-mark video { width: 100%; height: auto; display: block; }

.case-band-head p {
  margin: 0;
  max-width: 42ch;
  font-size: .9375rem;
  line-height: 1.65;
  color: var(--mute);
}

.case-marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.case-track {
  display: flex;
  gap: 20px;
  width: max-content;
  animation: case-marquee 64s linear infinite;
}

.case-marquee:hover .case-track { animation-play-state: paused; }

@keyframes case-marquee {
  to { transform: translate3d(-50%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .case-track { animation: none; }
  .case-marquee { overflow-x: auto; }
}

.case-card {
  flex: 0 0 auto;
  width: 300px;
  background: #fff;
  border-radius: var(--r);
  border: 1px solid var(--hair);
  overflow: hidden;
}

.case-card-img {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.case-card-img img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 7%;
  transition: transform .6s cubic-bezier(.22, .61, .36, 1);
}

.case-card:hover .case-card-img img { transform: scale(1.06); }

.case-card-label {
  padding: 14px 18px 16px;
  border-top: 1px solid var(--hair);
  font-size: .8125rem;
  font-weight: 500;
}

.case-card-label span { display: block; color: var(--mute); font-size: .75rem; margin-top: 3px; }

/* ===========================================================================
   5. Storefront band — dark, so the light screenshots carry
   ======================================================================== */
.case-dark {
  background: var(--ink);
  color: #fff;
  padding: 104px 0;
}

.case-dark .case-band-head p { color: rgba(255, 255, 255, .6); }
.case-dark .case-eyebrow { color: #fff; }
.case-dark .case-eyebrow::before { background: var(--accent); }

.case-shots { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }

.browser {
  border-radius: 12px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 40px 80px -40px rgba(0, 0, 0, .8);
}

.browser-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 11px 13px;
  background: #f2f2f3;
  border-bottom: 1px solid var(--hair);
}

.browser-dot { width: 9px; height: 9px; border-radius: 50%; background: #d8d8db; flex: 0 0 auto; }

.browser-url {
  margin-left: 5px;
  flex: 1;
  font-size: .6875rem;
  color: #8a8a90;
  background: #fff;
  border-radius: 999px;
  padding: 4px 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.browser-viewport { position: relative; aspect-ratio: 4 / 3; overflow: hidden; }

.browser-viewport img {
  position: absolute;
  inset: 0 0 auto 0;
  width: 100%;
  height: auto;
  display: block;
}

/* --travel is written in px by JS once the image has laid out — exactly
   (image height - frame height), so the pan stops on the last pixel of the
   page instead of running off into blank space. */
.browser-viewport.is-ready img { animation: case-scroll var(--dur, 26s) linear infinite; }
.browser-viewport.is-slow { --dur: 34s; }

@keyframes case-scroll {
  0%,  6%   { transform: translate3d(0, 0, 0); }
  47%, 56%  { transform: translate3d(0, calc(-1 * var(--travel, 0px)), 0); }
  97%, 100% { transform: translate3d(0, 0, 0); }
}

.browser-progress {
  position: absolute;
  right: 6px; top: 6px; bottom: 6px;
  width: 3px;
  border-radius: 3px;
  background: rgba(0, 0, 0, .08);
  overflow: hidden;
  z-index: 2;
}

.browser-progress::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 28%;
  border-radius: 3px;
  background: var(--accent);
  animation: case-thumb var(--dur, 26s) linear infinite;
}

@keyframes case-thumb {
  0%,  6%   { top: 0; }
  47%, 56%  { top: 72%; }
  97%, 100% { top: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .browser-progress::after { animation: none; }
}

/* ===========================================================================
   6. Closing
   ======================================================================== */
.case-cta {
  padding: 96px 0 112px;
  text-align: center;
}

.case-cta h2 {
  font-size: clamp(2rem, 4.6vw, 3.5rem);
  line-height: 1.04;
  letter-spacing: -.03em;
  font-weight: 600;
  margin: 16px 0 0;
}

.case-cta-buttons {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 32px;
}

/* ===========================================================================
   Responsive
   ======================================================================== */
@media (max-width: 991px) {
  .case-wrap { padding: 0 24px; }
  .case-block {
    grid-template-columns: 1fr;
    gap: 36px;
    padding: 72px 0;
  }
  .case-rail { position: static; }
  .case-meta { grid-template-columns: 1fr 1fr; }
  .case-meta-item:nth-child(3) { border-left: 0; padding-left: 0; }
  .case-shots { grid-template-columns: 1fr; }
  .case-band, .case-dark { padding: 72px 0; }
  .case-topbar { padding-bottom: 48px; }
}

@media (max-width: 640px) {
  .case-duo { grid-template-columns: 1fr; }
  .case-duo.is-offset > :nth-child(2) { transform: none; }
  .case-meta { grid-template-columns: 1fr; }
  .case-meta-item { border-left: 0; padding-left: 0; }
  .case-card { width: 240px; }
}

/* --- Generated case studies ---------------------------------------------- */
/* NeRo was hand-built around real photography. These pages carry the same
   structure with the artwork still missing, so every image slot has to read as
   a deliberate reservation rather than a broken figure. */

.case-figure.is-empty {
  display: grid;
  place-items: center;
  min-height: clamp(240px, 34vw, 420px);
  background: rgba(0, 0, 0, .035);
  border: 1px dashed rgba(0, 0, 0, .16);
  border-radius: 14px;
}

.case-ph {
  font-size: .875rem;
  font-weight: 500;
  color: rgba(0, 0, 0, .38);
}

/* The challenge sits beside the client description rather than under it: the
   two are read as a pair, and stacking them buries the second. */
.case-rail.is-inset {
  padding: 26px 28px;
  background: rgba(0, 0, 0, .035);
  border-radius: 14px;
}

.case-rail.is-inset p { margin: 0; }

.case-stat {
  margin: 28px 0 0;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--beam, #0f9ded);
  font-variant-numeric: tabular-nums;
}

.case-cta-btn,
.case-cta-alt {
  display: inline-flex;
  align-items: center;
  padding: 14px 26px;
  border-radius: 999px;
  font-size: .9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}

.case-cta-btn {
  background: #1b1b1b;
  color: #fff;
}

.case-cta-btn:hover { background: #000; }

.case-cta-alt {
  border: 1px solid rgba(0, 0, 0, .18);
  color: #1b1b1b;
}

.case-cta-alt:hover { border-color: rgba(0, 0, 0, .42); }

/* The result band inverts, so its own placeholders and rules invert with it. */
.case-band.case-dark .case-figure.is-empty {
  background: rgba(255, 255, 255, .05);
  border-color: rgba(255, 255, 255, .18);
}

.case-band.case-dark .case-ph { color: rgba(255, 255, 255, .42); }

@media (max-width: 720px) {
  .case-rail.is-inset { padding: 20px 20px; }
  .case-figure.is-empty { min-height: 200px; }
}

/* Motion cover in the case hero, in the slot NeRo fills with a photograph. */
.case-hero-art video {
  width: 100%;
  height: auto;
  display: block;
  border-radius: inherit;
}

/* Client mark in the case hero, in place of the name set as a label. Height is
   fixed and width follows, so marks of different proportions all carry the
   same optical weight. */
.case-logo {
  display: block;
  height: 38px;
  width: auto;
  margin-bottom: 4px;
}

@media (max-width: 720px) {
  .case-logo { height: 30px; }
}

/* A film in a block's media slot, in the same frame a figure would use. */
.case-figure video,
.case-vid {
  width: 100%;
  height: auto;
  display: block;
  border-radius: inherit;
}

/* --- Brand block ---------------------------------------------------------*/
.case-brand { display: grid; gap: 16px; }

.case-brand-mark {
  display: grid;
  place-items: center;
  padding: 12% 14%;
  background: #fff;
  border: 1px solid var(--hair);
  border-radius: var(--r);
}

.case-brand-mark img { width: 100%; height: auto; display: block; }

.case-brand-swatches { display: flex; gap: 10px; flex-wrap: wrap; }

/* The chip is the colour; the hex sits under it rather than on it, so nothing
   has to be legible against an arbitrary background. */
.case-brand-swatches span {
  flex: 1 1 0;
  min-width: 120px;
  padding: 54px 14px 12px;
  border-radius: 12px;
  background:
    linear-gradient(var(--c), var(--c)) top / 100% 44px no-repeat,
    var(--grey--3, #f4f4f4);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--mute);
}

/* --- Grouped stills inside a case block ---------------------------------*/
.pk-group + .pk-group { margin-top: 34px; }

.pk-label {
  margin: 0 0 12px;
  font-size: .875rem;
  font-weight: 500;
  color: var(--mute);
}

/* Cells sit at the top of their row rather than stretching to it. A row of
   equally proportioned stills looks the same either way, but where two items
   have different aspect ratios — Harvey Norman puts a vertical story next to a
   square feed cut — stretching leaves the shorter one with a strip of empty
   frame under it. */
.pk-row { display: grid; gap: 14px; align-items: start; }

.pk-row.is-2 { grid-template-columns: repeat(2, 1fr); }

/* Three across. Product renders of a single item are mostly empty space, so
   they need to sit larger than a catalogue tile to still read as one thing.
   Used by Rokovnjač for the bottle line-ups and by FLT for its nine luminaire
   series, which divide by three and would strand four on a second row in the
   five-column catalogue grid. */
.pk-row.is-3 { grid-template-columns: repeat(3, 1fr); }

/* Four across, one row. For a product family whose members differ only in
   size — Woodsplitter's BOWS 20, 20M, 14, 14M — a single line puts the four
   next to each other at a glance, and keeps a block of tall portrait renders
   from taking over the page the way a 2x2 of them does. */
.pk-row.is-4 { grid-template-columns: repeat(4, 1fr); gap: 10px; }

/* Fifteen across five columns. Five large frames read as a selection; this
   many small ones read as a catalogue, which is what the section is about. */
.pk-row.is-dense { grid-template-columns: repeat(5, 1fr); gap: 8px; }

.case-figure img { width: 100%; height: auto; display: block; }

@media (max-width: 720px) {
  .pk-row.is-2 { grid-template-columns: 1fr; }
  .pk-row.is-3 { grid-template-columns: repeat(2, 1fr); }
  /* The family stays four across on a phone: dropping to two would break the
     side-by-side size comparison that is the whole point of the row. */
  .pk-row.is-4 { grid-template-columns: repeat(4, 1fr); gap: 6px; }
  .pk-row.is-dense { grid-template-columns: repeat(3, 1fr); }
}

/* --- Crossfading frame ---------------------------------------------------
   A grid of six room shots asks the reader to compare six small pictures and
   makes near-identical ones look like padding. One frame that fades between
   them holds the camera still, so the thing that changes — the room, the
   fabric, the way the sofa is put together — is the only thing that moves.

   Six slots of 3.5s. Each image is visible from its own 0%, so the delays run
   negative: image i sits (i × slot − cycle) into the loop at load, which is
   the same position it would reach by waiting its turn. Nothing runs before
   the block is in view. */
.pk-fade {
  position: relative;
  /* --ar per instance: AirShield's environment renders are 3:1 letterbox and
     would be cropped to nothing in a 16:9 frame. */
  aspect-ratio: var(--ar, 16 / 9);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--grey--3, #f4f4f4);
}

.pk-fade img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

/* Before it starts, and for anyone who asked for less motion, the first shot
   simply stays up: the slot is never empty. */
.pk-fade img:first-child { opacity: 1; }

.pk-fade.is-in img {
  animation: pk-crossfade 21s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 3.5s - 21s);
}

@keyframes pk-crossfade {
  0%, 11.9%      { opacity: 1; }
  16.67%, 95.24% { opacity: 0; }
  100%           { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .pk-fade.is-in img { animation: none; }
}

/* --- Two-state cell ------------------------------------------------------
   A catalogue grid where every cell holds the same subject twice — anthracite
   and white, one size and the next — and turns between them in place. AirShield
   uses it for the orthographic views and for the studio set: the point of both
   sections is that the library covers every colour, and a static grid can only
   assert that where a turning one shows it.

   The second image is the one that animates, over the first: with only two
   states there is no need for the delay arithmetic .pk-fade needs, and the
   cell is never empty even before the styles land. The stagger is per cell
   (--i), so the grid turns as a wave and not as one blink. */
.pk-swap { position: relative; }

.pk-swap > img + img { position: absolute; inset: 0; }

/* Trigger from the group rather than the cell: one observer for the section
   instead of one per tile, and the whole wave starts together. */
.pk-swap > img + img,
.pk-group:not(.is-in) .pk-swap > img + img { opacity: 0; }

.pk-group.is-in .pk-swap > img + img {
  animation: pk-swap-top 7s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 0.34s);
}

@keyframes pk-swap-top {
  0%, 32%  { opacity: 0; }
  46%, 82% { opacity: 1; }
  96%      { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .pk-group.is-in .pk-swap > img + img { animation: none; }
}

/* --- Batch view ----------------------------------------------------------
   The pipeline is a queue, so the section shows one working. Four frames are
   already down when you arrive; the other four land one after another once the
   block scrolls into view, each after a moment of its slot being empty. It
   runs once rather than looping — a queue that never finishes is an animation,
   not a process.

   The chrome is deliberately plain: it frames the output as machine-made, not
   as a screenshot of an app we did not build. */
.pk-batch {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
  background: #fff;
}

.pk-b-bar {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  padding: 14px 18px;
  border-bottom: 1px solid var(--hair);
  font-size: .8125rem;
}

.pk-b-job { font-weight: 600; color: var(--ink); }
.pk-b-rule { color: var(--mute); }

.pk-b-prog {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--hair);
  font-size: .75rem;
  color: var(--mute);
}

.pk-b-prog > i {
  position: relative;
  height: 4px;
  flex: 1 1 auto;
  border-radius: 999px;
  background: var(--grey--10, #e8e8e8);
  overflow: hidden;
}

.pk-b-prog > i::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 50%;
  border-radius: 999px;
  background: var(--beam, #0f9ded);
}

/* The queue loops: the pipeline is a thing that keeps running, so it keeps
   running. Every element shares one 7s cycle rather than carrying its own
   delay — with per-element delays and infinite iteration each cell would come
   back on its own clock and the wave would drift apart within a few passes.
   So the stagger lives inside each cell's keyframes, computed by
   tools that emit this block, and every cell resets on the same frame. */

.pk-batch.is-in .pk-b-cell:nth-child(5) .pk-b-slot img {
  animation: pk-draw-0 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(5) .pk-b-slot::after {
  animation: pk-scan-0 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(5) figcaption b {
  animation: pk-name-0 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(5) figcaption i {
  animation: pk-queued-0 7.0s linear infinite;
}

@keyframes pk-draw-0 {
  0%, 5.714%   { clip-path: inset(100% 0 0 0); }
  20.0%, 99.9% { clip-path: inset(0 0 0 0); }
  100%           { clip-path: inset(100% 0 0 0); }
}
@keyframes pk-scan-0 {
  0%, 5.714%   { top: 100%; opacity: 0; }
  5.857% { top: 100%; opacity: 1; }
  19.857% { top: 0; opacity: 1; }
  20.0%, 100% { top: 0; opacity: 0; }
}
@keyframes pk-name-0 {
  0%, 22.143% { opacity: 0; }
  25.714%, 99.9% { opacity: 1; }
  100%           { opacity: 0; }
}
@keyframes pk-queued-0 {
  0%, 22.143% { opacity: 1; }
  25.714%, 99.9% { opacity: 0; }
  100%           { opacity: 1; }
}

.pk-batch.is-in .pk-b-cell:nth-child(6) .pk-b-slot img {
  animation: pk-draw-1 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(6) .pk-b-slot::after {
  animation: pk-scan-1 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(6) figcaption b {
  animation: pk-name-1 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(6) figcaption i {
  animation: pk-queued-1 7.0s linear infinite;
}

@keyframes pk-draw-1 {
  0%, 18.571%   { clip-path: inset(100% 0 0 0); }
  32.857%, 99.9% { clip-path: inset(0 0 0 0); }
  100%           { clip-path: inset(100% 0 0 0); }
}
@keyframes pk-scan-1 {
  0%, 18.571%   { top: 100%; opacity: 0; }
  18.714% { top: 100%; opacity: 1; }
  32.714% { top: 0; opacity: 1; }
  32.857%, 100% { top: 0; opacity: 0; }
}
@keyframes pk-name-1 {
  0%, 35.0% { opacity: 0; }
  38.571%, 99.9% { opacity: 1; }
  100%           { opacity: 0; }
}
@keyframes pk-queued-1 {
  0%, 35.0% { opacity: 1; }
  38.571%, 99.9% { opacity: 0; }
  100%           { opacity: 1; }
}

.pk-batch.is-in .pk-b-cell:nth-child(7) .pk-b-slot img {
  animation: pk-draw-2 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(7) .pk-b-slot::after {
  animation: pk-scan-2 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(7) figcaption b {
  animation: pk-name-2 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(7) figcaption i {
  animation: pk-queued-2 7.0s linear infinite;
}

@keyframes pk-draw-2 {
  0%, 31.429%   { clip-path: inset(100% 0 0 0); }
  45.714%, 99.9% { clip-path: inset(0 0 0 0); }
  100%           { clip-path: inset(100% 0 0 0); }
}
@keyframes pk-scan-2 {
  0%, 31.429%   { top: 100%; opacity: 0; }
  31.571% { top: 100%; opacity: 1; }
  45.571% { top: 0; opacity: 1; }
  45.714%, 100% { top: 0; opacity: 0; }
}
@keyframes pk-name-2 {
  0%, 47.857% { opacity: 0; }
  51.429%, 99.9% { opacity: 1; }
  100%           { opacity: 0; }
}
@keyframes pk-queued-2 {
  0%, 47.857% { opacity: 1; }
  51.429%, 99.9% { opacity: 0; }
  100%           { opacity: 1; }
}

.pk-batch.is-in .pk-b-cell:nth-child(8) .pk-b-slot img {
  animation: pk-draw-3 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(8) .pk-b-slot::after {
  animation: pk-scan-3 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(8) figcaption b {
  animation: pk-name-3 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-cell:nth-child(8) figcaption i {
  animation: pk-queued-3 7.0s linear infinite;
}

@keyframes pk-draw-3 {
  0%, 44.286%   { clip-path: inset(100% 0 0 0); }
  58.571%, 99.9% { clip-path: inset(0 0 0 0); }
  100%           { clip-path: inset(100% 0 0 0); }
}
@keyframes pk-scan-3 {
  0%, 44.286%   { top: 100%; opacity: 0; }
  44.429% { top: 100%; opacity: 1; }
  58.429% { top: 0; opacity: 1; }
  58.571%, 100% { top: 0; opacity: 0; }
}
@keyframes pk-name-3 {
  0%, 60.714% { opacity: 0; }
  64.286%, 99.9% { opacity: 1; }
  100%           { opacity: 0; }
}
@keyframes pk-queued-3 {
  0%, 60.714% { opacity: 1; }
  64.286%, 99.9% { opacity: 0; }
  100%           { opacity: 1; }
}

.pk-batch.is-in .pk-b-prog > i::after {
  animation: pk-fill 7.0s linear infinite;
}
.pk-batch.is-in .pk-b-prog span b  { animation: pk-count-a 7.0s linear infinite; }
.pk-batch.is-in .pk-b-prog span em { animation: pk-count-b 7.0s linear infinite; }

@keyframes pk-fill {
  0%             { width: 50%; }
  62.857%, 99.9% { width: 99.5%; }
  100%           { width: 50%; }
}
@keyframes pk-count-a {
  0%, 57.143% { opacity: 1; }
  61.429%, 99.9% { opacity: 0; }
  100%           { opacity: 1; }
}
@keyframes pk-count-b {
  0%, 57.143% { opacity: 0; }
  61.429%, 99.9% { opacity: 1; }
  100%           { opacity: 0; }
}

@keyframes pk-out { to { opacity: 0; } }
@keyframes pk-in  { to { opacity: 1; } }

.pk-b-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--hair);
  border-top: 1px solid var(--hair);
}

.pk-b-cell { margin: 0; background: #fff; }

.pk-b-slot { display: block; position: relative; }
.pk-b-slot img { width: 100%; height: auto; display: block; }

.pk-b-cell figcaption {
  position: relative;
  padding: 8px 10px 12px;
  font-size: .6875rem;
  color: var(--grey--30, #b8b8b8);
}

.pk-b-cell figcaption b,
.pk-b-cell figcaption i {
  display: block;
  font-weight: 400;
  font-style: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pk-b-cell figcaption i { position: absolute; inset: 8px 10px auto; }

/* Already down when you arrive. */
.pk-b-cell.is-done figcaption i { opacity: 0; }

/* Still to come. The frame is not faded in, it is drawn: a renderer paints in
   bands from the bottom of the image upward, so the slot fills the same way,
   with a line at the row currently being written. */
.pk-b-cell.is-queued .pk-b-slot img { clip-path: inset(100% 0 0 0); }
.pk-b-cell.is-queued figcaption b { opacity: 0; }

.pk-b-cell.is-queued .pk-b-slot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grey--3, #f4f4f4);
}

.pk-b-cell.is-queued .pk-b-slot::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 2px;
  background: var(--beam, #0f9ded);
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .pk-b-cell.is-queued .pk-b-slot img { clip-path: none; }
  .pk-b-cell.is-queued figcaption b { opacity: 1; }
  .pk-b-cell.is-queued .pk-b-slot::before,
  .pk-b-cell.is-queued .pk-b-slot::after,
  .pk-b-cell figcaption i { display: none; }
  .pk-b-prog > i::after { width: 99.5%; }
  .pk-b-prog span b { opacity: 0; }
  .pk-b-prog span em { opacity: 1; }
}

@media (max-width: 720px) {
  .pk-b-grid { grid-template-columns: repeat(2, 1fr); }
}

/* --- Batch stats ---------------------------------------------------------*/
.pk-b-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-bottom: 1px solid var(--hair);
}

.pk-b-stats div {
  padding: 14px 18px;
  border-left: 1px solid var(--hair);
}

.pk-b-stats div:first-child { border-left: 0; }

.pk-b-stats b {
  display: block;
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.pk-b-stats span {
  display: block;
  margin-top: 2px;
  font-size: .6875rem;
  color: var(--mute);
}

/* --- Rebrand -------------------------------------------------------------
   The mark on the brand's own black, at the width of the row beneath it, and
   the two applications. Nothing else: the icon, the type specimen and the
   colour chips were four different treatments stacked up, which read as four
   unrelated blocks rather than one identity.

   Vector rather than a crop out of the guide, so it stays sharp and carries
   the stated colours instead of the grid and texture behind them. */
.pk-mark {
  margin: 0 0 14px;
  display: grid;
  place-items: center;
  padding: 9% 12%;
  background: #1a1a1a;
  border-radius: var(--r);
}

.pk-mark img { width: 100%; height: auto; display: block; }

@media (max-width: 720px) {
  .pk-b-stats { grid-template-columns: repeat(2, 1fr); }
  .pk-b-stats div:nth-child(3) { border-left: 0; }
  .pk-b-stats div:nth-child(n + 3) { border-top: 1px solid var(--hair); }
  .pk-rb-two { grid-template-columns: 1fr; }
}

/* The link to the client's site, under the recording of it. It sat in the hero
   before, where it read as a stray piece of chrome next to the headline; here
   it is the obvious next step from what you have just been shown. */
.case-visit { margin: 16px 0 0; }

.case-visit a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 20px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  font-size: .9375rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  transition: border-color .22s ease;
}

.case-visit a:hover { border-color: var(--ink); }
.case-visit svg { width: 15px; height: 15px; }

.case-meta-val a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--hair); }
.case-meta-val a:hover { border-bottom-color: currentColor; }

/* --- Framed recording ----------------------------------------------------
   The recordings had their browser chrome cropped off, which left them bleeding
   into the page. A card with a lift is enough to separate them.

   No address bar: the sales platform lives at an internal URL that is not
   public, and putting it on screen would be both wrong to show and wrong to
   imply. The public site has its own link under the recording instead. */
.case-browser {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 18px 44px rgba(0, 0, 0, .07);
}

.case-browser video,
.case-browser img { width: 100%; height: auto; display: block; }

/* A stack of framed pages reads as one set, not as three separate cards. */
.case-browser + .case-browser { margin-top: 14px; }

/* ===========================================================================
   7. First Light Technologies — a matrix of finishes, and one scene at three
      times of day
   ======================================================================== */

/* The three-across row FLT needs is `.pk-row.is-3`, defined with the rest of
   the `.pk-row` family in section 6 rather than duplicated here. */

/* --- Finish row ----------------------------------------------------------
   Four tiles of identical geometry that differ only in powder coat. Unnamed,
   the row reads as a rendering fault rather than as the four finishes the
   client actually sells, so the caption is not decoration here. */
.flt-fin { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }

.flt-fin figure { margin: 0; }

.flt-fin figcaption {
  padding: 9px 10px 11px;
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--mute);
}

.flt-fin .case-figure.is-dark figcaption { color: rgba(255, 255, 255, .58); }

@media (max-width: 640px) {
  .flt-fin { grid-template-columns: repeat(2, 1fr); }
}

/* --- Photometry -----------------------------------------------------------
   The IES files are the one part of this project a reader cannot see, so the
   section states them as a count rather than illustrating them. Plain rows,
   deliberately closer to a data sheet than to a graphic. */
.flt-ies {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
  background: #fff;
}

.flt-ies-bar {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 13px 18px;
  border-bottom: 1px solid var(--hair);
  font-size: .8125rem;
  letter-spacing: .01em;
}

.flt-ies-bar b { font-weight: 600; color: var(--ink); }
.flt-ies-bar span { color: var(--mute); }

.flt-ies-list { margin: 0; padding: 6px 0; list-style: none; }

.flt-ies-list li {
  display: grid;
  grid-template-columns: 5.5rem 1fr auto;
  align-items: baseline;
  gap: 14px;
  padding: 8px 18px;
  font-size: .875rem;
}

.flt-ies-list li + li { border-top: 1px solid rgba(0, 0, 0, .05); }
.flt-ies-list b { font-weight: 600; color: var(--ink); letter-spacing: .04em; }
.flt-ies-list span { color: var(--mute); }
.flt-ies-list em { font-style: normal; color: var(--ink); font-variant-numeric: tabular-nums; }

.flt-ies-note {
  margin: 0;
  padding: 13px 18px 15px;
  border-top: 1px solid var(--hair);
  font-size: .8125rem;
  line-height: 1.55;
  color: var(--mute);
}

/* Narrow enough and the three columns no longer fit on one line. Series and
   count stay paired on the first row, because that pairing is the row's
   content; the description drops underneath rather than pushing the count onto
   a third line of its own. */
@media (max-width: 560px) {
  .flt-ies-list li { grid-template-columns: 1fr auto; gap: 2px 14px; }
  .flt-ies-list b { grid-area: 1 / 1; }
  .flt-ies-list em { grid-area: 1 / 2; }
  .flt-ies-list span { grid-area: 2 / 1 / 3 / 3; }
}

/* --- Five locations, three conditions ------------------------------------
   Every scene was built once and rendered at day, evening and night. Showing
   the three side by side turns that into fifteen thumbnails nobody reads;
   crossfading them in place keeps the framing fixed, which is the whole point
   — the only thing that changes between the three is the light.

   The day frame is a plain always-on layer underneath. Fading all three
   against each other would let the cell background show through at every
   handover; a fixed base means one of them is always fully opaque. */
.flt-day {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
  background: #fff;
}

.flt-d-bar {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 13px 18px;
  border-bottom: 1px solid var(--hair);
  font-size: .8125rem;
}

.flt-d-bar b { font-weight: 600; color: var(--ink); }

.flt-d-legend { display: flex; gap: 14px; color: var(--mute); }
.flt-d-legend span { opacity: .34; }

.flt-d-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--hair); }

/* The roadway is the scene the cover video also follows, so it leads and takes
   the full width. */
.flt-d-cell.is-wide { grid-column: 1 / -1; }

.flt-d-cell {
  position: relative;
  margin: 0;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #0d0d0d;
}

.flt-d-cell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.flt-d-cell img.is-b, .flt-d-cell img.is-c { opacity: 0; }

.flt-d-cell figcaption {
  position: absolute;
  left: 10px;
  bottom: 9px;
  padding: 3px 9px 4px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .52);
  color: #fff;
  font-size: .75rem;
  letter-spacing: .02em;
  backdrop-filter: blur(4px);
}

@media (max-width: 560px) {
  .flt-d-grid { grid-template-columns: 1fr; }
}

/* One twelve-second turn of the clock: four seconds per condition, with the
   handover kept short enough to read as a cut in the light rather than as a
   dissolve between two photographs. */
@keyframes flt-cross-b {
  0%, 30% { opacity: 0; }
  33.3%, 63.3% { opacity: 1; }
  66.7%, 100% { opacity: 0; }
}

@keyframes flt-cross-c {
  0%, 63.3% { opacity: 0; }
  66.7%, 96.7% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes flt-lit-a {
  0%, 30% { opacity: 1; }
  33.3%, 96.7% { opacity: .34; }
  100% { opacity: 1; }
}

@keyframes flt-lit-b {
  0%, 30% { opacity: .34; }
  33.3%, 63.3% { opacity: 1; }
  66.7%, 100% { opacity: .34; }
}

@keyframes flt-lit-c {
  0%, 63.3% { opacity: .34; }
  66.7%, 96.7% { opacity: 1; }
  100% { opacity: .34; }
}

.flt-day.is-in .flt-d-cell img.is-b { animation: flt-cross-b 12s linear infinite; }
.flt-day.is-in .flt-d-cell img.is-c { animation: flt-cross-c 12s linear infinite; }
.flt-day.is-in .flt-d-legend span:nth-child(1) { animation: flt-lit-a 12s linear infinite; }
.flt-day.is-in .flt-d-legend span:nth-child(2) { animation: flt-lit-b 12s linear infinite; }
.flt-day.is-in .flt-d-legend span:nth-child(3) { animation: flt-lit-c 12s linear infinite; }

/* Nothing moves, so the frame that carries the product has to be the one left
   standing: the night render is the only one where the luminaire is doing
   anything. */
@media (prefers-reduced-motion: reduce) {
  .flt-day.is-in .flt-d-cell img,
  .flt-day.is-in .flt-d-legend span { animation: none; }
  .flt-d-cell img.is-c { opacity: 1; }
  .flt-d-legend span:nth-child(3) { opacity: 1; }
}

/* --- The portfolio, changing colour --------------------------------------
   The lineup of seven series does on the page what the cover does: it runs the
   finish matrix instead of describing it. Four cuts of the same view are
   stacked in each tile and revealed in order, with the black one as a plain
   always-on base underneath — so at the end of the cycle all three overlays
   fade together and the base is simply there, rather than four layers trying
   to hand over to each other at once.

   The tiles are offset a fraction of a second apart, which turns seven
   independent recolours into one decision travelling across the lineup. */
.flt-s-bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  font-size: .875rem;
  font-weight: 500;
  color: var(--mute);
}

.flt-s-legend { display: flex; gap: 12px; }
.flt-s-legend span { opacity: .34; }

/* Four over three, the same shape the cover runs, so the two read as one
   lineup rather than two different arrangements of the same products. Flex
   rather than grid: seven items in a four-column grid strand the last row
   against the left edge, and there is no grid property that centres it. */
.flt-swap { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px; }

.flt-swap > * { flex: 0 0 calc(25% - 10.5px); }

/* The swap block leads the section and a still group follows it, so it has to
   carry the same separation the grouped stills give each other. */
.flt-swap-wrap + .pk-group { margin-top: 34px; }

.flt-s-cell {
  position: relative;
  margin: 0;
  aspect-ratio: 1;
  border-radius: var(--r);
  overflow: hidden;
  background: var(--grey--3, #f4f4f4);
}

.flt-s-cell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.flt-s-cell img + img { opacity: 0; }

@media (max-width: 720px) {
  .flt-swap > * { flex: 0 0 calc(50% - 7px); }
}

/* Twelve seconds, three apiece. The overlays stay on once they arrive and all
   drop together on the last beat, which is what returns the tile to black
   without a flash of the finish two steps back. */
@keyframes flt-sw-b {
  0%, 22% { opacity: 0; }
  25%, 97% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes flt-sw-c {
  0%, 47% { opacity: 0; }
  50%, 97% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes flt-sw-d {
  0%, 72% { opacity: 0; }
  75%, 97% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes flt-sl-a {
  0%, 22% { opacity: 1; }
  25%, 97% { opacity: .34; }
  100% { opacity: 1; }
}

@keyframes flt-sl-b {
  0%, 22% { opacity: .34; }
  25%, 47% { opacity: 1; }
  50%, 100% { opacity: .34; }
}

@keyframes flt-sl-c {
  0%, 47% { opacity: .34; }
  50%, 72% { opacity: 1; }
  75%, 100% { opacity: .34; }
}

@keyframes flt-sl-d {
  0%, 72% { opacity: .34; }
  75%, 97% { opacity: 1; }
  100% { opacity: .34; }
}

/* --i counts down in document order — the first tile carries the highest
   value and so the largest negative delay, which puts it furthest ahead and
   makes the change read left to right. Counting down rather than up keeps the
   number of tiles out of this file. */
.flt-swap-wrap.is-in .flt-s-cell img:nth-child(2) { animation: flt-sw-b 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-cell img:nth-child(3) { animation: flt-sw-c 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-cell img:nth-child(4) { animation: flt-sw-d 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-cell img { animation-delay: calc(var(--i) * -.13s); }

.flt-swap-wrap.is-in .flt-s-legend span:nth-child(1) { animation: flt-sl-a 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-legend span:nth-child(2) { animation: flt-sl-b 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-legend span:nth-child(3) { animation: flt-sl-c 12s linear infinite; }
.flt-swap-wrap.is-in .flt-s-legend span:nth-child(4) { animation: flt-sl-d 12s linear infinite; }

/* Nothing cycles, so the lineup rests on the finish the section opens with and
   the row of names says which one that is. */
@media (prefers-reduced-motion: reduce) {
  .flt-swap-wrap.is-in .flt-s-cell img,
  .flt-swap-wrap.is-in .flt-s-legend span { animation: none; }
  .flt-s-legend span:nth-child(1) { opacity: 1; }
}

/* ===========================================================================
   Shared case-study pieces, first built for Check Point Care and now used by
   any study that needs them.
   ======================================================================== */

/* --- Crossfading stack ---------------------------------------------------
   Five renders of the same subject — the pieces that hang off the device, or
   the device from its six sides. Side by side they read as a contact sheet and
   each one gets a sixth of the attention; in one frame that changes under you,
   each gets the whole frame. First child holds the height, the rest stack on
   top of it.

   Five slots of 20%: visible through 17%, three per cent of crossfade, then
   out of the way. Each child is offset by a fifth of the cycle. */
.case-fade {
  --dur: 15s;
  position: relative;
  border-radius: var(--r);
  overflow: hidden;
  background: var(--grey--3, #f4f4f4);
}

.case-fade img {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
  animation: case-cf5 var(--dur) linear infinite both;
  animation-delay: var(--d, 0s);
  animation-play-state: paused;
}

/* Two frames, half the cycle each: the A and B of a variant, not a catalogue. */
.case-fade.is-2 img { animation-name: case-cf2; }

.case-fade img + img { position: absolute; inset: 0; }
.case-fade.is-in img,
.is-in .case-fade img { animation-play-state: running; }

@keyframes case-cf5 {
  0%, 17% { opacity: 1; }
  20%, 97% { opacity: 0; }
  100% { opacity: 1; }
}

@keyframes case-cf2 {
  0%, 43% { opacity: 1; }
  50%, 93% { opacity: 0; }
  100% { opacity: 1; }
}

/* --- Motion grid ---------------------------------------------------------
   Ten loops, none of them the same shape: a rotation is square-ish, an
   application on the body is wide, the phone screen is a tall portrait. A row
   grid would crop or letterbox all three to one ratio. Columns let each keep
   its own, and the eye reads the wall as one body of work. */
.case-grid { columns: 2; column-gap: 16px; }

.case-grid figure {
  break-inside: avoid;
  margin: 0 0 16px;
  border-radius: var(--r);
  overflow: hidden;
  background: var(--grey--3, #f4f4f4);
}

.case-grid video { width: 100%; height: auto; display: block; }

.case-grid figcaption {
  padding: 11px 16px 13px;
  font-size: .75rem;
  line-height: 1.4;
  color: var(--mute);
}

@media (max-width: 720px) { .case-grid { columns: 1; } }

/* --- Browser frame with the page moving through it -----------------------
   A screenshot of a long page has to be either unreadably small or cropped to
   its first screen. The frame keeps the page at a size you can read and moves
   it instead, which is also what the page does in real use.

   No back and forward chrome: this is the design, not a recording of someone
   browsing, and the buttons would imply history that is not there. */
.case-web {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 18px 44px rgba(0, 0, 0, .07);
}

.case-web + .case-web { margin-top: 16px; }

.case-web-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 11px 16px;
  background: #f2f2f2;
  border-bottom: 1px solid var(--hair);
}

.case-web-bar b {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #d8d8d8;
}

.case-web-bar i {
  flex: 1;
  margin-left: 9px;
  padding: 5px 13px;
  background: #fff;
  border-radius: 999px;
  font-style: normal;
  font-size: .6875rem;
  color: var(--mute);
}

.case-web-port { position: relative; height: 470px; overflow: hidden; }

/* Hold at the top, travel the whole page, hold at the foot, travel back. The
   image is taller than the port, so -100% + the port height lands the footer
   exactly on the bottom edge whatever the artboard's height turns out to be. */
.case-web-port img {
  width: 100%;
  height: auto;
  display: block;
  animation: case-page var(--dur, 44s) ease-in-out infinite;
  animation-play-state: paused;
}

.case-web.is-in .case-web-port img,
.is-in .case-web-port img { animation-play-state: running; }

@keyframes case-page {
  0%, 6% { transform: translateY(0); }
  47%, 56% { transform: translateY(calc(-100% + 470px)); }
  97%, 100% { transform: translateY(0); }
}

@media (max-width: 720px) {
  .case-web-port { height: 320px; }
  @keyframes case-page {
    0%, 6% { transform: translateY(0); }
    47%, 56% { transform: translateY(calc(-100% + 320px)); }
    97%, 100% { transform: translateY(0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-fade img,
  .case-web-port img { animation: none; }
  .case-fade img { opacity: 0; }
  .case-fade img:first-child { opacity: 1; }
}

/* --- Cycling cell --------------------------------------------------------
   Like .pk-swap, but for a subject that has more than two states. Teravita's
   grid uses it: twenty-five builds, each turning through all four wood
   colours, so the grid on the page carries the same hundred renders the shop
   does rather than a sample of them.

   The stack is painted in order — the first image is the ground and each one
   after it fades in over the last. Each slot therefore needs its own
   keyframes, because a shared one offset by a delay would run past the end of
   the loop. Every image is fully covered before the one under it starts to
   go, so a cell mid-turn is a crossfade between two colours and never a blend
   of three. */
.pk-cycle { position: relative; }

.pk-cycle > img ~ img { position: absolute; inset: 0; opacity: 0; }

.pk-group.is-in .pk-cycle > img:nth-child(2) { animation-name: pk-cyc-2; }
.pk-group.is-in .pk-cycle > img:nth-child(3) { animation-name: pk-cyc-3; }
.pk-group.is-in .pk-cycle > img:nth-child(4) { animation-name: pk-cyc-4; }

.pk-group.is-in .pk-cycle > img ~ img {
  animation-duration: 11s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  /* Per cell, so the grid turns as a wave and not as one blink. */
  animation-delay: calc(var(--i, 0) * 0.11s);
}

@keyframes pk-cyc-2 { 0%, 19% { opacity: 0; } 25%, 50% { opacity: 1; } 56%, 100% { opacity: 0; } }
@keyframes pk-cyc-3 { 0%, 44% { opacity: 0; } 50%, 75% { opacity: 1; } 81%, 100% { opacity: 0; } }
@keyframes pk-cyc-4 { 0%, 69% { opacity: 0; } 75%, 94% { opacity: 1; } 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .pk-group.is-in .pk-cycle > img ~ img { animation: none; }
}
