/* ============================================================
   Webify — Work page layer
   ------------------------------------------------------------
   Loaded last, only by /work/ and /ar/work/, through head()'s
   extra_css. Removing that one <link> returns the page to what
   the shared library gives it.

   THE PAGE IS AN INDEX, NOT A RETELLING. It exists to help
   someone decide which project to open. Everything that explains
   HOW a project was built belongs to its case study, which tells
   it better and at length.

   That division was learned the hard way: the first version gave
   each project the full treatment, and two of its headings —
   "Service, then branch, then time." and "Two sides of one
   system." — appeared word for word on the case study too. If a
   heading here would also fit there, it belongs there.

   An entry carries: kind label, name, one line, the outcome
   number, three facts, ONE large picture, one sentence of the
   client, and the link. Nothing else.

   Logical properties throughout — no physical left/right — so
   /ar/ mirrors without a retrofit. The ONE deliberate exception
   is `object-position` on the screenshots: photographic content
   does not mirror, so the crop must stay physically identical in
   both scripts. Same reasoning as the map pins on the tier pages.
   ============================================================ */

:root {
  /* Inherited from the homepage prototype vocabulary so the two
     redesigned pages share one ground system. Redeclared here
     because prototype.css is not loaded on this page. */
  /* Aliases onto the canonical ground ladder in tokens.css -- this
     block was a verbatim copy of prototype.css's. See the note there. */
  --ground-a:      var(--ground);
  --ground-b:      var(--ground-tint);
  --ground-b-deep: var(--ground-deep);
  --rule:          var(--ground-rule);

  /* Exponential ease-out: fast departure, long settle. */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* One shadow at two depths, so every screenshot on the page sits
     in the same light. Offset plus soft blur, never a halo. */
  --lift-sm: 0 10px 24px -14px rgba(2, 7, 37, 0.30), 0 1px 3px rgba(2, 7, 37, 0.05);
  --lift-lg: 0 34px 68px -30px rgba(2, 7, 37, 0.32), 0 2px 6px rgba(2, 7, 37, 0.05);

  /* How far the page edge sits from the container's content edge: the
     container's own padding, plus the outer gutter when the viewport is
     wider than the container. Below --container-max there is no gutter,
     and adding the padding twice overshot the viewport by 40px — caught
     at 1024, where overflow-x:clip hid it from the scrollbar but the
     picture was still being cut early. */
  --bleed-end: calc(var(--container-pad)
    + max(0px, (100vw - var(--container-max)) / 2));

  /* How much page a PARTIAL bleed leaves beside itself. A still may run off
     the edge; a comparison may not, because the far end of its drag has to
     stay grabbable — so it bleeds out to --bleed-end minus this. */
  --compare-gutter: 2.5rem;
}

/* ------------------------------------------------------------
   Browser surfaces — the parts we did not draw
   ------------------------------------------------------------ */
.wk ::selection { background: var(--brand-accent); color: var(--brand-on-accent); }
.wk { caret-color: var(--brand-accent); }
.wk :focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Every screenshot on this page is the same object: a rounded,
   hairlined, lifted crop of a real interface. One class, many shapes. */
.wkshot {
  position: relative;
  display: block;
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--surface-muted);
  box-shadow: var(--lift-sm);
  /* A hairline inside the radius; a border would sit outside the
     overflow clip and show a seam against the image. */
  outline: 1px solid var(--rule);
  outline-offset: -1px;
}

.wkshot picture,
.wkshot img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* --focus picks which part of a FULL screenshot survives the tile's
     aspect ratio, and is set only on shots whose own ratio differs from
     their slot. Physical percentages on purpose (see header).
     Tight interface details are cut as real assets instead — a CSS
     zoom would ship a 1600px file to draw a 400px detail. */
  object-position: var(--focus, 50% 50%);
}

/* ============================================================
   1. HERO — masthead, statement, and a collage of real work
   ============================================================
   The collage belongs to the PAGE, not to any one project: its four
   tiles are addressed by mosaic area, so mixing pieces across them
   later is a content edit and not a layout change. */

.wkhero {
  position: relative;
  background: var(--ground-a);
  color: var(--text-body);
  overflow-x: clip;
  padding-block: var(--hero-pad-top) var(--section-pad-lg);
  /* Two washes at a few percent — enough that the white is lit
     rather than flat, far too faint to read as a colour. */
  background-image:
    radial-gradient(52vw 40vw at 88% 22%, color-mix(in srgb, var(--brand-accent) 11%, transparent), transparent 64%),
    radial-gradient(44vw 32vw at -6% -10%, color-mix(in srgb, var(--brand-primary) 8%, transparent), transparent 68%);
}

.wkhero__inner {
  display: grid;
  gap: clamp(2.25rem, 4.5vw, 3.25rem);
  align-items: center;
  /* Start edge lines up with .container; end edge runs off the
     viewport so the collage is cut by the page, not framed by it. */
  padding-inline-start: max(var(--container-pad), calc((100vw - var(--container-max)) / 2));
  padding-inline-end: 0;
}

@media (min-width: 64rem) {
  .wkhero__inner { grid-template-columns: minmax(0, 27rem) minmax(0, 1fr); }
}

.wkhero__copy {
  padding-inline-end: var(--container-pad);
  max-width: 34rem;
}

/* The page title, at masthead scale. Deliberately larger than the
   statement below it so it reads as a title and never as a kicker. */
.wkhero__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--h-page);
  line-height: var(--h-page-leading);
  letter-spacing: var(--h-page-track);
  color: var(--text-strong);
  margin: 0;
}

.wkhero__rule {
  block-size: 1px;
  border: 0;
  background: linear-gradient(to var(--to-end, right),
    color-mix(in srgb, var(--brand-primary) 45%, transparent), var(--rule) 62%, transparent);
  margin-block: clamp(1rem, 2vw, 1.5rem);
}
html[dir="rtl"] .wkhero__rule { --to-end: left; }

.wkhero__deck {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: var(--h-sub);
  line-height: 1.22;
  letter-spacing: var(--tracking-tight);
  color: var(--text-strong);
  text-wrap: balance;
  margin: 0;
}

.wkhero__lead {
  margin-block-start: var(--space-lg);
  font-size: var(--fs-base);
  line-height: var(--leading-body);
  color: var(--text-muted);
  max-width: 44ch;
}

/* ---- The showreel (see the section below) ---- */

.wkhero__collage {
  margin: 0;
  min-inline-size: 0;
}

.wkhero__caption {
  margin-block-start: var(--space-md);
  padding-inline-end: var(--container-pad);
  max-width: 46rem;
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--text-muted);
}
.wkhero__caption strong { color: var(--text-strong); font-weight: 600; }

/* ============================================================
   THE WORK HERO — the showreel.

   ONE device, start to finish. The same element is the browser
   and the iPhone: its bar collapses while a bezel, the Dynamic
   Island and the side button fade in on the same box, so the
   frame never swaps or disappears. The phone is prototype.css's
   device (same gradient, island, button, 9:41 strip) at real
   iPhone 15 proportions of one length, --W.

   The screenshots never rescale: each is a fixed-size layer the
   morphing frame simply crops, which is what keeps it smooth —
   an object-fit image in a box whose width is animating re-crops
   and resamples every frame.
   ============================================================ */

/* Registered so they compute ONCE, on the stage, against the reel.
   Unregistered, a cqw inside the phone would resolve against the wrong
   container. */
@property --SW { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --H  { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --W  { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --PW { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --PH { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --PW0 { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --PH0 { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --k   { syntax: "<number>"; inherits: true; initial-value: 1; }

.wkshow { container-type: inline-size; }

.wkshow__stage {
  --chrome: 2.25rem;
  --SW: calc(100cqw - var(--container-pad));
  --H: calc(var(--SW) * 0.625 + var(--chrome));
  --W: calc(var(--H) / 2.061);
  /* THE HOMEPAGE PHONE, exactly: prototype.css's .stage__phone is
     clamp(7.5rem, 7.5vw, 9rem) wide with a 0.22rem bezel around a
     9:19.5 screen. The min() only stops it outgrowing a very short stage
     (320px viewports); everywhere else it is that phone, that size. */
  --PW0: clamp(7.5rem, 7.5vw, 9rem);
  --PH0: calc((var(--PW0) - 0.44rem) * 19.5 / 9 + 0.44rem);
  /* ...then scaled UNIFORMLY by --k, never recalculated: every value
     below is the homepage value times --k. work-showreel.js sets --k so the
     phone fills 88% of the stage height (CSS cannot divide a length by a
     length). Without the script it stays 1, the homepage size. */
  --PW: calc(var(--PW0) * var(--k));
  --PH: calc(var(--PH0) * var(--k));
  --rail: calc(0.22rem * var(--k));
  --morph: 1000ms;
  --curve: cubic-bezier(0.65, 0, 0.35, 1);
  position: relative;
  inline-size: var(--SW);
  block-size: var(--H);
  /* The phone never drops below the homepage size (--k >= 1), and below
     ~380px wide that is taller than the stage. The overhang is split top
     and bottom (the phone is centred), so reserve exactly that as margin;
     it is zero wherever the phone fits. The index's gap is carried HERE
     rather than on .wkshow__index, because two adjoining margins collapse to
     the larger one and the overhang would silently eat the gap. */
  --over: max(0px, calc((var(--PH0) - var(--H)) / 2));
  margin-block: var(--over) calc(var(--over) + clamp(1.1rem, 2vw, 1.5rem));
}

/* ---- The device ---- */
.wkshow__device {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 100%;
  padding: 0;
  border-radius: 0.9rem;
  background: #fff;
  box-shadow:
    0 28px 50px -22px rgba(2, 7, 37, 0.45),
    0 3px 8px rgba(2, 7, 37, 0.14),
    0 0 0 1px rgba(2, 7, 37, 0.07);
  transition:
    inline-size var(--morph) var(--curve),
    block-size var(--morph) var(--curve),
    inset-inline-start var(--morph) var(--curve),
    inset-block-start var(--morph) var(--curve),
    padding var(--morph) var(--curve),
    border-radius var(--morph) var(--curve),
    box-shadow var(--morph) var(--curve);
}
/* The bezel: prototype.css's gradient, faded in on the same box. */
.wkshow__device::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(148deg, #6f7386 0%, #2a2d40 22%, #1b1d2c 55%, #383b4e 82%, #6b6f82 100%);
  box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.42);
  opacity: 0;
  /* Leaving: the bezel stays while the frame starts to open, so the
     phone never becomes a bare white pill. */
  transition: opacity 450ms var(--curve) 150ms;
}
.wkshow__island {
  position: absolute;
  z-index: 3;
  /* prototype.css .stage__phone::before, value for value. */
  inset-block-start: calc(0.62rem * var(--k));
  left: 50%;
  inline-size: calc(var(--PW) * 0.32);
  block-size: calc(0.44rem * var(--k));
  translate: -50% 0;
  border-radius: var(--radius-full);
  background: #07080e;
  box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.1);
  opacity: 0;
  scale: 0.6;
  transition: opacity 300ms, scale 400ms var(--curve);
}
.wkshow__button {
  position: absolute;
  /* prototype.css .stage__phone::after, value for value. */
  inset-inline-end: calc(-0.06rem * var(--k));
  inset-block-start: 27%;
  inline-size: calc(0.08rem * var(--k));
  block-size: 13%;
  border-radius: var(--radius-full);
  background: linear-gradient(#7c8093, #3c3f52);
  opacity: 0;
  transition: opacity 300ms;
}

.wkshow__screen {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  block-size: 100%;
  overflow: hidden;
  border-radius: 0.9rem;
  background: #fff;
  transition: border-radius var(--morph) var(--curve);
}

.wkshow__chrome {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: none;
  block-size: var(--chrome);
  padding-inline: 0.8rem;
  background: #f4f5f9;
  border-block-end: 1px solid var(--rule);
  overflow: hidden;
  transition:
    block-size var(--morph) var(--curve),
    opacity 300ms;
}
.wkshow__dots { display: flex; gap: 0.3rem; flex: none; }
.wkshow__dots i { inline-size: 0.55rem; block-size: 0.55rem; border-radius: 50%; background: #d4d8e2; }
.wkshow__url {
  flex: 1;
  min-inline-size: 0;
  padding: 0.2rem 0.8rem;
  border-radius: var(--radius-full);
  background: #fff;
  border: 1px solid var(--rule);
  font-size: var(--fs-xs);
  color: var(--text-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Same job as the product pages' "Demo" chip: the frame says it. */
.wkshow__chip {
  flex: none;
  padding: 0.15rem 0.65rem;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--brand-accent) 22%, #fff);
  color: var(--brand-accent-dark);
  font-size: var(--fs-xs);
  font-weight: 600;
}
.wkshow__chip[hidden] { display: none; }

.wkshow__vp { position: relative; flex: 1; overflow: hidden; background: #fff; }

/* The desktop screen: always the full browser's size, centred. As the
   frame narrows it is cropped, never resampled. Physical left/translate
   on purpose — centring is the same in both directions. */
.wkshow__shot {
  position: absolute;
  z-index: 2;
  inset-block-start: 0;
  left: 50%;
  inline-size: var(--SW);
  /* A screenshot's OWN ratio, not the frame's: a 16:9 capture (The Watch
     District) cover-cropped into 16:10 lost ~6% off each side — its logo
     and headline. Shown whole and top-aligned instead, as a browser shows
     a page; .is-short fades its last pixels into the page below. */
  block-size: calc(var(--SW) * var(--ratio, 0.625));
  max-inline-size: none;
  translate: -50% 0;
  object-fit: cover;
  object-position: top center;
  clip-path: inset(0 0 0 0);
  transition: clip-path 900ms cubic-bezier(0.16, 1, 0.3, 1);
}
.wkshow__shot.is-short {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent);
}
.wkshow__vp.is-hidden .wkshow__shot { clip-path: inset(0 100% 0 0); }
[dir="rtl"].wkshow__vp.is-hidden .wkshow__shot { clip-path: inset(0 0 0 100%); }
.wkshow__vp.no-anim .wkshow__shot { transition: none; }

/* The phone screen: always the phone's size, centred, faded in over the
   desktop screen while the frame closes around it. */
.wkshow__mob {
  position: absolute;
  z-index: 4;
  inset-block-start: 0;
  left: 50%;
  translate: -50% 0;
  inline-size: calc(var(--PW) - 2 * var(--rail));
  block-size: calc(var(--PH) - 2 * var(--rail));
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--m-ground, #fff);
  /* .stage__phone-screen's clamp(0.5rem, 9cqw, 0.8rem). Its container is
     .stage__phone, and cqw measures the CONTENT box — inside the 0.22rem
     bezel — so 100cqw is the phone's width less both rails. */
  font-size: calc(clamp(0.5rem, calc((var(--PW0) - 0.44rem) * 0.09), 0.8rem) * var(--k));
  opacity: 0;
  transition: opacity 380ms var(--curve);
}
.wkshow__mob > img {
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  max-inline-size: none;
  object-fit: cover;
  object-position: top center;
  display: block;
}
.wkshow__mob.no-status { grid-template-rows: 1fr; }
.wkshow__mob.no-status > .msite__status { display: none; }

/* ---- Phone state ---- */
.wkshow__stage.is-phone .wkshow__device {
  inline-size: var(--PW);
  block-size: var(--PH);
  inset-inline-start: calc(50% - var(--PW) / 2);
  inset-block-start: calc((var(--H) - var(--PH)) / 2);
  padding: var(--rail);
  border-radius: calc(1.5rem * var(--k));
  /* .stage__phone's own shadow. */
  box-shadow:
    0 calc(28px * var(--k)) calc(50px * var(--k)) calc(-22px * var(--k)) rgba(2, 7, 37, 0.55),
    0 calc(3px * var(--k)) calc(8px * var(--k)) rgba(2, 7, 37, 0.28),
    0 0 0 0 transparent;
}
/* .stage__phone-screen's 1.3rem. */
.wkshow__stage.is-phone .wkshow__screen { border-radius: calc(1.3rem * var(--k)); }
.wkshow__stage.is-phone .wkshow__chrome { block-size: 0; opacity: 0; border-block-end-width: 0; }
/* Order in: the frame narrows over the desktop screen first; the phone
   screen fades in only once the frame is close to phone width (a narrow
   column over a wide screenshot read as a glitch); then the bezel. */
.wkshow__stage.is-phone .wkshow__mob { opacity: 1; transition-delay: 520ms; }
.wkshow__stage.is-phone .wkshow__device::before { opacity: 1; transition-delay: 600ms; transition-duration: 450ms; }
.wkshow__stage.is-phone :is(.wkshow__island, .wkshow__button) { opacity: 1; scale: 1; transition-delay: 820ms; }

/* The status strip — verbatim from prototype.css. */
.wkshow .msite__status {
  display: flex;
  align-items: center;
  padding: 0.75em 1.15em 0.15em;
  font-size: 0.72em;
  font-weight: 600;
  color: var(--m-ink, #23303f);
  background: transparent;
  font-variant-numeric: tabular-nums;
}
.wkshow .msite__status-end { margin-inline-start: auto; display: flex; align-items: center; gap: 0.35em; }
.wkshow .msite__status i { display: block; }
.wkshow .msite__sig {
  inline-size: 1.05em;
  block-size: 0.72em;
  background:
    linear-gradient(var(--m-ink, #23303f) 0 0) 0    100% / 18% 40% no-repeat,
    linear-gradient(var(--m-ink, #23303f) 0 0) 30%  100% / 18% 60% no-repeat,
    linear-gradient(var(--m-ink, #23303f) 0 0) 62%  100% / 18% 82% no-repeat,
    linear-gradient(var(--m-ink, #23303f) 0 0) 100% 100% / 18% 100% no-repeat;
}
.wkshow .msite__wifi {
  inline-size: 0.8em;
  block-size: 0.62em;
  background: var(--m-ink, #23303f);
  clip-path: polygon(50% 100%, 0 28%, 20% 12%, 50% 0, 80% 12%, 100% 28%);
}
.wkshow .msite__batt {
  inline-size: 1.15em;
  block-size: 0.6em;
  border: 1px solid color-mix(in srgb, var(--m-ink, #23303f) 45%, transparent);
  border-radius: 0.18em;
  padding: 0.08em;
  background:
    linear-gradient(var(--m-ink, #23303f) 0 0) content-box 0 0 / 72% 100% no-repeat;
}

/* ============================================================
   The wireframe — under the desktop screen, which wipes over it
   ============================================================ */
.wkshow__build {
  position: absolute;
  z-index: 1;
  inset-block-start: 0;
  left: 50%;
  translate: -50% 0;
  inline-size: var(--SW);
  block-size: calc(var(--SW) * 0.625);
  container-type: inline-size;
}
/* Once the real screen has wiped in, the drawing under it goes. It is
   otherwise visible below a screenshot shorter than the frame. */
.wkshow__build { transition: opacity 400ms; }
.wkshow__vp.is-done .wkshow__build { opacity: 0; }
.wkshow__grid { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; pointer-events: none; }
.wkshow__grid line {
  stroke: color-mix(in srgb, var(--brand-primary) 24%, transparent);
  stroke-width: 1;
  stroke-dasharray: 3 5;
  vector-effect: non-scaling-stroke;
  opacity: 0;
  transition: opacity 500ms;
}
.wkshow__vp.s1 .wkshow__grid line { opacity: 1; }

.wkshow__wf {
  position: absolute;
  inset: 0;
  padding: 4.5% 6%;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 7%;
  --blk: #e3e6f0;
  --blk-2: #d3d7e6;
}
.wkshow__wf :is(.wf-nav, .wf-p, .wf-btn, .wf-img, .wf-cards i, .wf-side, .wf-tiles i, .wf-rows i) {
  opacity: 0;
  transition: opacity 450ms cubic-bezier(0.16, 1, 0.3, 1), transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.wf-nav { display: flex; align-items: center; gap: 4%; transform: translateY(-8px); }
.wf-logo { inline-size: 13%; block-size: 0.7rem; border-radius: 0.25rem; background: var(--brand-primary); }
.wf-links { display: flex; gap: 6%; flex: 1; justify-content: flex-end; }
.wf-links i { inline-size: 9%; block-size: 0.4rem; border-radius: 1rem; background: var(--blk-2); }
.wf-cta { inline-size: 12%; block-size: 1.1rem; border-radius: 1rem; background: var(--brand-accent); }
.wf-hero { display: grid; grid-template-columns: 1.1fr 1fr; gap: 6%; align-items: center; }
.wf-copy { display: grid; gap: 0.6rem; align-content: center; }
.wf-h {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--text-strong);
  font-size: clamp(0.95rem, 3.6cqw, 1.9rem);
  line-height: 1.08;
  letter-spacing: -0.02em;
  min-block-size: 2.2em;
}
[dir="rtl"] .wf-h { font-family: var(--font-arabic); letter-spacing: 0; line-height: 1.3; }
.wf-caret {
  display: inline-block;
  inline-size: 2px;
  block-size: 0.9em;
  background: var(--brand-primary);
  margin-inline-start: 2px;
  vertical-align: -0.08em;
  animation: wkshow-blink 0.8s steps(1) infinite;
}
@keyframes wkshow-blink { 50% { opacity: 0; } }
.wf-p { display: grid; gap: 0.4rem; transform: translateY(8px); }
.wf-p i { block-size: 0.42rem; border-radius: 1rem; background: var(--blk); }
.wf-p i:last-child { inline-size: 70%; }
.wf-btn { inline-size: 38%; block-size: 1.35rem; border-radius: 1rem; background: var(--brand-primary); transform: scale(0.8); transform-origin: var(--start, left) center; }
[dir="rtl"] .wf-btn { --start: right; }
.wf-img {
  aspect-ratio: 4 / 3;
  border-radius: 0.6rem;
  background:
    linear-gradient(135deg, transparent 48.5%, var(--blk-2) 49%, var(--blk-2) 51%, transparent 51.5%),
    linear-gradient(45deg, transparent 48.5%, var(--blk-2) 49%, var(--blk-2) 51%, transparent 51.5%),
    var(--blk);
  transform: scale(0.92);
}
.wf-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4%; }
.wf-cards i { block-size: 2.6rem; border-radius: 0.5rem; background: var(--ground-tint); border: 1px solid var(--blk); transform: translateY(14px); }

.wkshow__wf--app { grid-template-columns: 17% 1fr; grid-template-rows: auto auto 1fr; gap: 4% 3%; padding: 0; }
.wf-side { grid-row: 1 / -1; background: #1e3a4a; transform: translateX(calc(var(--flip, 1) * -12px)); }
[dir="rtl"] .wf-side { --flip: -1; }
.wkshow__wf--app .wf-h { padding-block-start: 4%; padding-inline-end: 4%; min-block-size: 0; font-size: clamp(0.9rem, 2.8cqw, 1.4rem); }
.wf-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3%; padding-inline-end: 4%; }
.wf-tiles i { block-size: 2.8rem; border-radius: 0.5rem; background: #fff; border: 1px solid var(--blk-2); transform: translateY(10px); }
.wf-rows { display: grid; align-content: start; gap: 0.55rem; padding-inline-end: 4%; }
.wf-rows i { block-size: 0.45rem; border-radius: 1rem; background: var(--blk); transform: translateY(6px); }
.wf-rows i:nth-child(odd) { inline-size: 88%; }

/* .wkshow__vp.sN: the hide rule above takes :is()'s most specific argument
   (.wf-cards i), so a bare step class would lose to it. */
.wkshow__vp.s2 :is(.wf-nav, .wf-side),
.wkshow__vp.s3 :is(.wf-img, .wf-tiles i),
.wkshow__vp.s5 :is(.wf-p, .wf-btn),
.wkshow__vp.s6 :is(.wf-cards i, .wf-rows i) { opacity: 1; transform: none; }
.wkshow__vp.s6 .wf-cards i:nth-child(2), .wkshow__vp.s3 .wf-tiles i:nth-child(2) { transition-delay: 90ms; }
.wkshow__vp.s6 .wf-cards i:nth-child(3), .wkshow__vp.s3 .wf-tiles i:nth-child(3) { transition-delay: 180ms; }
.wkshow__vp.s3 .wf-tiles i:nth-child(4) { transition-delay: 270ms; }
.wkshow__vp.s6 .wf-rows i:nth-child(n+3) { transition-delay: 120ms; }
.wkshow__vp.s6 .wf-rows i:nth-child(n+5) { transition-delay: 240ms; }

/* The gold line that leads the wipe. */
.wkshow__sweep {
  position: absolute;
  z-index: 3;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 3px;
  background: var(--brand-accent);
  box-shadow: 0 0 18px var(--brand-accent);
  opacity: 0;
  pointer-events: none;
}
.wkshow__vp.is-sweeping .wkshow__sweep { animation: wkshow-sweep 900ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes wkshow-sweep {
  from { inset-inline-start: 0; opacity: 1; }
  90%  { opacity: 1; }
  to   { inset-inline-start: 100%; opacity: 0; }
}

/* ============================================================
   Index — names every slot and what it is, and drives the reel
   ============================================================ */
.wkshow__index {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.4rem;
  margin: 0;
  padding: 0;
  padding-inline-end: var(--container-pad);
  list-style: none;
}
.wkshow__index button {
  position: relative;
  display: grid;
  gap: 0.1rem;
  padding: 0.35rem 0 0.55rem;
  border: 0;
  background: none;
  font: inherit;
  text-align: start;
  color: var(--text-muted);
  cursor: pointer;
}
.wkshow__name { font-size: var(--fs-sm); font-weight: 600; color: inherit; }
.wkshow__kind { font-size: 0.72rem; display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted-on-tint, var(--text-muted)); }
.wkshow__kind::before { content: ""; inline-size: 0.45rem; block-size: 0.45rem; border-radius: 50%; background: var(--brand-primary); }
.wkshow__kind--demo::before,
.wkshow__kind--concept::before { background: transparent; border: 1.5px dashed var(--brand-accent-dark); }
.wkshow__index button:hover,
.wkshow__index [aria-current="true"] { color: var(--text-strong); }
.wkshow__index button::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 2px;
  background: var(--rule);
}
.wkshow__index [aria-current="true"]::before {
  content: "";
  position: absolute;
  z-index: 1;
  inset-inline-start: 0;
  inset-block-end: 0;
  block-size: 2px;
  inline-size: 100%;
  background: var(--brand-accent);
  transform-origin: var(--start, left);
}
html[dir="rtl"] .wkshow__index { --start: right; }
.wkshow.is-playing .wkshow__index [aria-current="true"]::before {
  animation: wkshow-progress var(--slot, 10s) linear both;
}
@keyframes wkshow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.wkhero__collage.wkshow .wkhero__caption { min-block-size: 3.2em; }

/* ---- Pause / play (WCAG 2.2.2) ----
   The reel runs without end, so it carries an explicit stop that a
   keyboard reaches and that holds until play is pressed. It leads the
   index row; the row's gap above the index moves onto the bar. */
.wkshow__bar {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding-inline-end: var(--container-pad);
}
.wkshow__bar .wkshow__index { flex: 1; min-inline-size: 0; padding-inline-end: 0; }
.wkshow__pause {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: #fff;
  color: var(--text-strong);
  box-shadow: 0 0 0 1px rgba(2, 7, 37, 0.1), 0 6px 14px -8px rgba(2, 7, 37, 0.4);
  cursor: pointer;
  transition: background-color 200ms, color 200ms;
}
.wkshow__pause:hover,
.wkshow__pause[aria-pressed="true"] { background: var(--brand-primary); color: #fff; }
.wkshow__pause:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 3px; }
.wkshow__pause[hidden] { display: none; }
.wkshow__pause svg { inline-size: 1.1rem; block-size: 1.1rem; }
.wkshow__pause .wkshow__i-play { display: none; }
.wkshow__pause[aria-pressed="true"] .wkshow__i-pause { display: none; }
.wkshow__pause[aria-pressed="true"] .wkshow__i-play { display: block; }
/* The progress bar under the current project stops with the reel. */
.wkshow.is-paused .wkshow__index [aria-current="true"]::before { animation-play-state: paused; }

@media (max-width: 47.999rem) {
  .wkshow__index {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    overscroll-behavior-inline: contain;
  }
  .wkshow__index::-webkit-scrollbar { display: none; }
  .wkshow__index > li { flex: none; }
}

@media (prefers-reduced-motion: reduce) {
  .wkshow__device, .wkshow__device::before, .wkshow__screen, .wkshow__chrome,
  .wkshow__mob, .wkshow__island, .wkshow__button, .wkshow__shot { transition: none !important; }
}

/* ============================================================
   2. THE KIND TAG — the honest-framing label
   ============================================================
   Every piece on this page states what it is before it states
   anything else. Client work and concept redesign are visually
   distinct so the two can never be skim-read as the same thing. */

.wktag {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.7rem 0.3rem 0.55rem;
  border-radius: var(--radius-full);
  font-family: var(--font-heading);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
html[dir="rtl"] .wktag { padding-inline: 0.55rem 0.7rem; }

.wktag::before {
  content: "";
  inline-size: 0.45rem;
  block-size: 0.45rem;
  border-radius: var(--radius-full);
  flex: none;
}

/* Built and launched for a paying client — the strong statement. */
.wktag--client {
  background: var(--brand-wash);
  color: var(--brand-primary-dark);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand-primary) 22%, transparent);
}
.wktag--client::before { background: var(--brand-primary); }

/* Our own unpaid rework — deliberately quieter, and hollow, so it
   can never be mistaken for the label above at a glance. */
.wktag--concept {
  background: var(--surface);
  color: var(--color-gray-600);
  box-shadow: inset 0 0 0 1px var(--rule);
}
.wktag--concept::before { box-shadow: inset 0 0 0 1.5px var(--color-gray-400); }

.wktag--demo {
  background: var(--surface);
  color: var(--brand-primary-dark);
  box-shadow: inset 0 0 0 1px var(--brand-primary-dark);
}
.wktag--demo::before { box-shadow: inset 0 0 0 1.5px currentColor; }

.wkentry--demo { scroll-margin-block-start: 6rem; }
.wkentry--demo .wkentry__shot { aspect-ratio: 16 / 9; }
.wkentry__note {
  margin-block-start: var(--space-lg);
  color: var(--text-body);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

/* ============================================================
   3. THE ENTRIES — one band per published piece
   ============================================================ */

.wkentry {
  background: var(--ground-a);
  padding-block: var(--section-pad-lg);
  overflow-x: clip;
}
/* The hero's own bottom padding plus this one stacked to 160px of
   nothing between the collage and the first entry. */
.wkentry:first-of-type { padding-block-start: clamp(0.5rem, 2vw, 1.5rem); }

/* Alternating grounds separate consecutive entries without a divider. */
.wkentry:nth-of-type(even) { background: var(--ground-b); }
.wkentry:nth-of-type(even) .wkentry__line,
.wkentry:nth-of-type(even) .wkentry__facts dd,
.wkentry:nth-of-type(even) .wkentry__metric span,
.wkentry:nth-of-type(even) .wkentry__pull { color: var(--muted-on-tint); }

.wkentry__inner {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3.25rem);
  align-items: center;
  grid-template-areas: "shot" "copy";
}

@media (min-width: 56rem) {
  /* ONE geometry for every entry, mirrored row by row. Two pieces that
     differ in picture size read as a ranking nobody intended, and two rows
     whose outer margins disagree read as an untrimmed page. So each entry
     runs its picture toward its OWN page edge and each stops the same
     --compare-gutter short of it.

     Stopping short is the comparison's constraint, adopted by the stills.
     A slider cannot reach the edge — part of its travel would sit outside
     the viewport and the handle would be ungrabbable at the extreme — and a
     picture that ends 40px inside the edge beside one that runs past it is
     not the same treatment. Everything here is logical, so /ar/ mirrors the
     bleed with the areas and needs no retrofit.

     1.7fr puts the picture at ~742px at 1440 and leaves the copy ~436px,
     under the 466px the case-study button and the Visit link need to share
     a line — so they stack. That is the flex-wrap this row was built with,
     it is what already happens at 1024, and it buys a picture someone can
     actually read the interface in. */
  .wkentry__inner {
    grid-template-areas: "copy shot";
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr);
    margin-inline-end: calc(-1 * max(0px, var(--bleed-end) - var(--compare-gutter)));
  }
  /* Alternating sides give several entries a rhythm without giving any
     of them a rank. Areas mirror with the script for free; so does the
     bleed, which swaps ends with them — a text column never bleeds,
     because bleeding one orphans it. */
  .wkentry:nth-of-type(even) .wkentry__inner {
    grid-template-areas: "shot copy";
    grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
    margin-inline-end: 0;
    margin-inline-start: calc(-1 * max(0px, var(--bleed-end) - var(--compare-gutter)));
  }
}

/* The picture column: a figure, and optionally one line under it. Same
   shape as .wkcompare, so both kinds of picture end their column the same
   way. Every entry gets the wrapper whether it has a caption or not. */
.wkentry__media { grid-area: shot; min-inline-size: 0; }

.wkentry__shot {
  box-shadow: var(--lift-lg);
  aspect-ratio: 1600 / 1000;
}
.wkentry__copy { grid-area: copy; min-inline-size: 0; }

/* .wkentry--lead CARRIES NO GEOMETRY. It used to: the featured entry took
   a wider column and ran its picture fully off the page edge, while every
   other entry stayed inside the container at 1.3fr. Two published pieces
   made the cost obvious — the second picture was 20% smaller than the
   first and its row stopped 130px short of the first row's edge, so the
   page looked mis-set rather than ranked. Size is not how this page ranks
   work; ORDER is, and the featured piece is sorted first.

   The class is still emitted, because "which piece is featured" is true of
   the content and the hero collage is drawn from that piece's screenshots.
   If a future treatment wants to mark it, mark it with something that does
   not change how big the work looks. */

.wkentry__name {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--h-feature);
  line-height: 1.03;
  letter-spacing: -0.03em;
  color: var(--text-strong);
  margin-block: var(--space-md) 0;
  text-wrap: balance;
}

.wkentry__line {
  margin-block-start: var(--space-md);
  font-size: var(--fs-lg);
  line-height: 1.5;
  color: var(--text-muted);
  max-width: 40ch;
}

/* The number reads as a sentence, not a stat tile. */
.wkentry__metric {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin-block-start: var(--space-lg);
  padding-block-start: var(--space-lg);
  border-block-start: 2px solid color-mix(in srgb, var(--brand-accent) 55%, transparent);
}
.wkentry__metric b {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: clamp(1.9rem, 1.5rem + 1.5vw, 2.6rem);
  line-height: 0.95;
  letter-spacing: -0.04em;
  color: var(--brand-primary);
  font-variant-numeric: tabular-nums;
}
.wkentry__metric span {
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 20ch;
}

/* Three facts, as a ruled list. Nothing here is a card. */
.wkentry__facts {
  margin: var(--space-lg) 0 0;
  display: grid;
  border-block-start: 1px solid var(--rule);
}
.wkentry__facts > div {
  display: grid;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr);
  gap: var(--space-md);
  padding-block: 0.55rem;
  border-block-end: 1px solid var(--rule);
}
.wkentry__facts dt {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);
  align-self: center;
}
.wkentry__facts dd {
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-strong);
  align-self: center;
}
.wkentry__facts a {
  color: var(--brand-primary);
  text-decoration: underline;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}
.wkentry__facts a:hover { color: var(--link-hover); }

/* One sentence of the client. The whole quote lives on the case study. */
.wkentry__pull {
  margin: var(--space-lg) 0 0;
  padding-inline-start: var(--space-md);
  border-inline-start: 2px solid color-mix(in srgb, var(--brand-primary) 30%, transparent);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text-muted);
}
.wkentry__pull cite {
  display: block;
  margin-block-start: var(--space-xs);
  font-style: normal;
  color: var(--text-strong);
  font-weight: 600;
}

.wkentry__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md) var(--space-lg);
  margin-block-start: var(--space-xl);
}

.wkentry__live {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-strong);
  text-decoration: underline;
  text-underline-offset: 0.28em;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, var(--text-strong) 35%, transparent);
}
.wkentry__live:hover { color: var(--brand-primary); text-decoration-color: currentColor; }
.wkentry__live svg {
  inline-size: 0.9em;
  block-size: 0.9em;
  flex: none;
}
/* The arrow points out of the page and mirrors with the script — that is
   rtl.css's [data-flip], carried on the icon itself. Nothing to add here. */

/* ============================================================
   4. THE NOTE — what the two labels mean, and why the page is short
   ============================================================ */

.wknote {
  background: var(--ground-b-deep);
  padding-block: var(--section-pad);
}

.wknote__inner {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3rem);
  align-items: start;
}

@media (min-width: 56rem) {
  .wknote__inner { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); }
}

.wknote__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--h-section);
  line-height: 1.12;
  letter-spacing: var(--tracking-tight);
  color: var(--text-strong);
  margin: 0;
  max-width: 18ch;
  text-wrap: balance;
}

.wknote__text {
  margin-block-start: var(--space-md);
  color: var(--muted-on-tint);
  line-height: var(--leading-body);
  max-width: 48ch;
}

.wknote__actions { margin-block-start: var(--space-xl); }

/* The two kinds, defined side by side, each under its own label. */
.wkkinds {
  margin: 0;
  display: grid;
  gap: var(--space-lg);
}

.wkkinds > div {
  padding-block-start: var(--space-md);
  border-block-start: 1px solid color-mix(in srgb, var(--brand-primary) 18%, transparent);
}
.wkkinds dt { margin: 0; }
.wkkinds dd {
  margin: var(--space-sm) 0 0;
  font-size: var(--fs-sm);
  line-height: 1.65;
  color: var(--muted-on-tint);
}

/* ============================================================
   5. MOTION — one authored moment
   ============================================================
   The hero rises in once, on load; the showreel then runs its own loop. Everything below the fold
   keeps the site's existing .reveal behaviour and adds nothing. */

@media (prefers-reduced-motion: no-preference) {
  /* `wk-rise` was a duplicate of webify.css's `intro-rise` under another
     name, at 900ms/--ease-out-expo instead of the site's 720ms/--intro-ease,
     which made this page move at a different tempo from the rest. The
     plain rise now uses the shared vocabulary. `wk-settle`, the old
     collage's assembly, went with the collage: the showreel's frame
     arrives on the same rise, and its own loop is the moment. */
  .wkhero__copy > *,
  .wkshow__stage,
  .wkhero__caption {
    animation: intro-rise var(--intro-dur) var(--intro-ease) backwards;
  }
  /* Ordered by position, so the generator emits no per-element delay. */
  .wkhero__copy > *:nth-child(2) { animation-delay: 70ms; }
  .wkhero__copy > *:nth-child(3) { animation-delay: 140ms; }
  .wkhero__copy > *:nth-child(4) { animation-delay: 210ms; }
  .wkshow__stage { animation-delay: 140ms; }
  .wkhero__caption { animation-delay: 420ms; }
}

/* ============================================================
   3b. BEFORE / AFTER — the comparison inside a featured entry
   ============================================================
   An entry's PICTURE — it takes grid-area: shot in place of the
   still, and its caption sits under the frame in the same column.
   An entry has one or the other, never both: a slider stacked
   under the still it is already half made of shows the same
   screenshot twice and costs ~900px of height to do it.

   THE BLEED HAS TO GO. .wkentry--lead normally runs its picture
   off the page edge, but a slider cannot: part of its travel
   would sit outside the viewport, so the far end of the drag
   could never be reached and the handle would be ungrabbable at
   the extreme. build.py adds .wkentry--compare to an entry whose
   picture is a comparison, and the rule below hands the bleed
   back. A future lead entry with a plain still keeps it.

   HOW IT WORKS. Two pictures are stacked in the same box. The
   "after" one is whole and underneath; the "before" one sits on
   top and is clipped to --pos, the divider's distance from the
   inline start. A native <input type="range"> lies over the
   frame at opacity 0 and supplies every interaction there is —
   drag, touch, click-to-jump, arrow keys, and the slider role.
   work-compare.js does one thing: copy that value into --pos.

   MIRRORING. Everything that moves is logical: the divider is
   placed with inset-inline-start, the labels with
   inset-inline-start / inset-inline-end, and the grip is centred
   with margin-inline-start rather than a transform, because a
   translate is physical and would throw the grip a full width
   off in Arabic. The single physical property is clip-path,
   which has no logical form — so it is flipped once, below.

   WITHOUT JS. --pos holds at 50% and .wkcompare__range stays
   hidden until the script adds .is-live, so the figure degrades
   to a finished half-and-half comparison rather than to a
   control that does nothing. */

.wkcompare {
  --pos: 50%;
  --grip: 3rem;
  --divider: 2px;
  grid-area: shot;
  margin: 0;
  min-inline-size: 0;
}

.wkcompare__frame {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--surface-muted);
  box-shadow: var(--lift-lg);
  /* Same hairline as .wkshot — inside the radius, or the overflow clip
     leaves a seam against the image. */
  outline: 1px solid var(--rule);
  outline-offset: -1px;
  /* Both pictures are 1600x1000; the gate in build.py refuses a pair that
     disagrees, because two ratios would slide a letterbox across the drag. */
  aspect-ratio: 1600 / 1000;
  touch-action: pan-y;
  cursor: ew-resize;
}

.wkcompare__layer {
  position: absolute;
  inset: 0;
}
.wkcompare__layer picture,
.wkcompare__layer img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* The pictures must not be draggable as images, or a slow drag on the
     frame starts a native image drag instead of moving the divider. */
  -webkit-user-drag: none;
  user-select: none;
}

/* The "before" picture is the one that gets cut. inset() takes physical
   sides and has no logical twin, so this is the one rule in the block that
   knows about direction — and it is a whole rule, not a branch in the JS. */
.wkcompare__layer--before {
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}
[dir="rtl"] .wkcompare__layer--before {
  clip-path: inset(0 0 0 calc(100% - var(--pos)));
}

/* ---- the two labels ----
   Each one lives INSIDE its own layer, which is what makes them behave:
   the "before" tag is clipped away by the same inset that hides its
   picture, so it leaves with the thing it names instead of being faded
   out by a rule that has to guess when. */
.wkcompare__tag {
  position: absolute;
  inset-block-start: 0;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.5rem 0.95rem;
  font-family: var(--font-heading);
  font-size: clamp(0.72rem, 0.62rem + 0.32vw, 0.85rem);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.2;
  white-space: nowrap;
  backdrop-filter: blur(8px);
}
/* --font-heading is Outfit, which has no Arabic glyphs — an Arabic label set
   in it silently falls back to whatever the OS offers and stops matching the
   rest of the page. Same override the rest of the library uses for any
   element that names --font-heading directly (see base.css, which covers
   h1-h4 and .btn but not a span like this one).

   The caps-and-tracking treatment goes with it: Arabic has no letter case,
   so text-transform does nothing, and letter-spacing pulls a joined script
   apart at the seams. */
[dir="rtl"] .wkcompare__tag {
  font-family: var(--font-arabic);
  text-transform: none;
  letter-spacing: 0;
}

.wkcompare__tag--before {
  inset-inline-start: 0;
  border-end-end-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-gray-900, #111b21) 78%, transparent);
  color: #fff;
}
.wkcompare__tag--after {
  inset-inline-end: 0;
  border-end-start-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--brand-accent) 92%, transparent);
  color: var(--brand-on-accent, #fff);
}

/* ---- the divider ---- */
.wkcompare__divider {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--pos);
  z-index: 3;
  inline-size: var(--divider);
  /* Centres the 2px rule on --pos without a transform: translate is
     physical and would sit on the wrong side of the seam in Arabic. */
  margin-inline-start: calc(var(--divider) / -2);
  background: #fff;
  box-shadow: 0 0 0 1px rgba(2, 7, 37, 0.18);
  pointer-events: none;
}

.wkcompare__grip {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  inline-size: var(--grip);
  block-size: var(--grip);
  /* Logical centring, same reason as the divider above. */
  margin-inline-start: calc(var(--grip) / -2);
  margin-block-start: calc(var(--grip) / -2);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #fff;
  color: var(--brand-primary);
  box-shadow: var(--lift-sm), 0 0 0 1px rgba(2, 7, 37, 0.10);
  transition: scale 180ms var(--ease-out-expo), box-shadow 180ms var(--ease-out-expo);
}
.wkcompare__grip svg {
  inline-size: 1.15rem;
  block-size: 1.15rem;
}
.wkcompare__frame:hover .wkcompare__grip { scale: 1.06; }
.wkcompare.is-dragging .wkcompare__grip { scale: 0.94; }

/* ---- the control ----
   Stretched over the whole frame and invisible. The thumb is 2px so the
   value maps to the frame almost exactly and our own divider lands under
   the pointer; the track is the full height so a click anywhere moves it. */
.wkcompare__range {
  position: absolute;
  inset: 0;
  z-index: 4;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: ew-resize;
  touch-action: pan-y;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  /* Hidden until work-compare.js can actually move the divider. Without
     the script the frame keeps its 50/50 split and stays a real picture. */
  display: none;
}
.wkcompare.is-live .wkcompare__range { display: block; }

.wkcompare__range::-webkit-slider-runnable-track { block-size: 100%; }
.wkcompare__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  inline-size: var(--divider);
  block-size: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.wkcompare__range::-moz-range-track { block-size: 100%; }
.wkcompare__range::-moz-range-thumb {
  inline-size: var(--divider);
  block-size: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* The input's own ring is invisible along with the input, so the frame
   wears one. Keyboard only — work-compare.js sets .is-focus off
   :focus-visible, so a click never draws it. */
.wkcompare.is-focus .wkcompare__frame {
  outline: 2px solid var(--brand-primary);
  outline-offset: 3px;
}
.wkcompare.is-focus .wkcompare__grip {
  box-shadow: var(--lift-sm), 0 0 0 3px var(--brand-primary);
}

/* ---- caption ----
   Shared with the still's caption in section 3: one line under one picture,
   in one column. Two selectors rather than one class because the two
   pictures are otherwise unrelated components; one rule because a caption
   that differed between them is exactly the drift this page just lost. */
.wkcompare__caption,
.wkentry__caption {
  margin-block-start: 1rem;
  /* The picture column reaches 742px at 1440, which is ~90 characters of
     caption — well past a comfortable measure. Cap it, but in ch so it only
     bites at the widths where the column actually gets that wide. */
  max-inline-size: 58ch;
  font-size: clamp(0.92rem, 0.88rem + 0.2vw, 1.02rem);
  line-height: 1.6;
  color: var(--text-muted);
}
/* The disclaimer sits with the picture it qualifies, not in a footnote
   somewhere below it: the "before" is our illustration of a common manual
   process, and the page has to say so where the picture is. */
.wkcompare__note {
  display: block;
  margin-block-start: 0.4rem;
  font-size: 0.86em;
  opacity: 0.82;
}
.wkentry:nth-of-type(even) .wkcompare__caption,
.wkentry:nth-of-type(even) .wkentry__caption { color: var(--muted-on-tint); }

@media (prefers-reduced-motion: reduce) {
  .wkcompare__grip { transition: none; }
  .wkcompare__frame:hover .wkcompare__grip,
  .wkcompare.is-dragging .wkcompare__grip { scale: 1; }
}

/* Coarse pointers get a bigger grip and no hover state; the labels shrink
   so two of them still fit across a 390px frame. */
@media (max-width: 40rem) {
  .wkcompare { --grip: 2.6rem; }
  .wkcompare__tag { padding: 0.4rem 0.7rem; }
}

/* ============================================================
   EXTENDING
   ------------------------------------------------------------
   * A published piece is one <article class="wkentry">. Grounds and
     picture side alternate automatically, so nothing needs adding
     when the second and third land.
   * Every block inside an entry is keyed off its own copy. A concept
     redesign has no client, no number and no quote, so it renders
     fewer rows and still looks finished — the entry was designed to
     stand on its head alone.
   * `featured: true` takes .wkentry--lead and runs its picture off the
     page edge. Exactly one entry may have it.
   * The hero collage belongs to the PAGE (WORK_HERO in content.py),
     not to any project, so mixing tiles across pieces is a content
     edit. Nothing in this file needs to change for it.
   * `placeholder: true` entries render NOTHING here. An empty slot is
     a worse signal than a short page.
   * The before/after slider is keyed off `work_page.compare` in
     content.py — a piece without that block simply has no slider, and
     nothing in this file cares. A piece WITH one needs two shots of the
     SAME aspect ratio; build.py refuses the pair otherwise, because two
     ratios slide a letterbox across the drag.
   ============================================================ */

/* ============================================================
   5. THE DEVICE STAGE — an entry whose picture is a reel
   ============================================================
   build.py adds .wkentry--reel when work_page.reel is set. The
   frame is the site's drawn browser window (the same chrome as
   .cs-shot on the case studies), holding a still that paints with
   the page and a silent loop that js/work-reel.js fetches only once
   the frame scrolls into view.

   STAGED, NOT SLOTTED. The frame is wider than the column it sits in
   and runs PAST the page edge by --reel-overhang, tilted a degree and
   a half toward it — the same move the hero collage makes, so the
   page opens and continues in one gesture. The comparison's
   --compare-gutter does not apply: there is no drag to keep in reach.

   Everything is logical. In Arabic the copy column is at the RIGHT,
   so it is read first exactly as it is on the left in English, and
   the frame runs off the LEFT edge; the tilt flips with it. The
   footage itself does not mirror — it is a recording of a screen. */
.wkentry--reel {
  /* Kept to a sliver ON PURPOSE. The launch films set their headlines in
     the left third of the screen, so a frame that runs off the LEFT edge
     (every even entry in English, every odd one in Arabic) loses them
     first. At 6vw "Eleven sections" read "even sections"; at this width
     the tilted corner still breaks the page edge and no word is cut. */
  --reel-overhang: clamp(0.75rem, 1.5vw, 1.5rem);
  --reel-tilt: -1.4deg;
}
.wkentry--reel:nth-of-type(even) { --reel-tilt: 1.4deg; }
html[dir="rtl"] .wkentry--reel { --reel-tilt: 1.4deg; }
html[dir="rtl"] .wkentry--reel:nth-of-type(even) { --reel-tilt: -1.4deg; }

.wkreel {
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--lift-lg);
  outline: 1px solid var(--rule);
  outline-offset: -1px;
}

/* The window bar. The address sits CENTRED, Safari-style, so whichever
   end runs off the page it survives — the dots are the part that may go. */
.wkreel__bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0.55rem 0.85rem;
  background: var(--surface-muted);
  border-block-end: 1px solid var(--rule);
}
.wkreel__dots { display: flex; gap: 0.4rem; }
.wkreel__dots > span {
  inline-size: 0.55rem;
  block-size: 0.55rem;
  border-radius: var(--radius-full);
  background: var(--color-gray-300);
}
.wkreel__url {
  min-inline-size: 0;
  padding: 0.2rem 1.1rem;
  border-radius: var(--radius-full);
  background: var(--surface);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.wkreel__screen {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--surface-muted);
}
.wkreel__screen picture,
.wkreel__screen img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* A 16:10 still in a 16:9 screen loses 10% of its height; take it off
     the bottom, where a page is least likely to say what it is.
     Physical on purpose, like --focus everywhere else on this page. */
  object-position: var(--focus, 50% 0%);
}

.wkreel__video {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.6s var(--ease-out-expo);
}
.wkreel.is-playing .wkreel__video { opacity: 1; }

/* The pause control sits on the corner that stays on the page: the
   inline START for an odd entry (its frame runs off the end), the END
   for an even one. Small, dark and quiet — it is there to be found by
   the person who needs it, not to announce that this is a video. */
.wkreel__toggle {
  position: absolute;
  inset-block-end: 0.75rem;
  inset-inline-start: 0.75rem;
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: rgba(2, 7, 37, 0.62);
  color: #fff;
  cursor: pointer;
  opacity: 0.8;
  transition: opacity 0.2s ease, background-color 0.2s ease;
}
.wkreel__toggle:hover,
.wkreel__toggle:focus-visible { opacity: 1; background: rgba(2, 7, 37, 0.82); }
.wkreel__toggle[hidden] { display: none; }
.wkreel__icon { inline-size: 1rem; block-size: 1rem; }
.wkreel__icon--play { display: none; }
.wkreel.is-paused .wkreel__icon--pause { display: none; }
.wkreel.is-paused .wkreel__icon--play { display: block; }

@media (min-width: 56rem) {
  .wkentry--reel .wkentry__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.9fr);
    margin-inline-end: calc(-1 * (var(--bleed-end) + var(--reel-overhang)));
  }
  .wkentry--reel:nth-of-type(even) .wkentry__inner {
    grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
    margin-inline-end: 0;
    margin-inline-start: calc(-1 * (var(--bleed-end) + var(--reel-overhang)));
  }
  .wkentry--reel .wkreel { transform: rotate(var(--reel-tilt)); }
  .wkentry--reel:nth-of-type(even) .wkreel__toggle {
    inset-inline-start: auto;
    inset-inline-end: 0.75rem;
  }
  /* A tilted frame needs room above and below, or its raised corner
     kisses the band edge. */
  .wkentry--reel .wkentry__media { padding-block: clamp(0.5rem, 1.5vw, 1.5rem); }
}

/* Reduced motion: the still, and nothing else. The script never gives
   the video a src in this mode; this covers a preference that flips
   mid-visit, and keeps the control from pointing at nothing. The tilt
   stays — it does not move. */
@media (prefers-reduced-motion: reduce) {
  .wkreel__video,
  .wkreel__toggle { display: none; }
}
