/* ==========================================================================
   Case study — canvas, work header, compare slider, continue zone
   ==========================================================================
   The page IS the Figma frame. --dp is one design pixel of the 1440 canvas and
   every coordinate, size and type size is a multiple of it. The reference is
   the page in a 1920px window (author, 2026-09-25): 1px there, the frame
   centred (--gutter) with backgrounds running full-bleed. Narrower desktop
   windows get that same view scaled down (100vw / 1920), margins included;
   wider ones stay at 1px, never blown up. At 1024 and under it goes back to
   filling the width (100vw / 1440) so the copy stays readable.

   Below 900px that would shrink 12px copy to unreadable, so the same layers
   drop out of absolute positioning and reflow in source (reading) order.

   Markup: js/work-render.js. Layers: js/projects/*.js. Behaviour: js/work-page.js.
   ========================================================================== */
:root {
  --dp: min(1px, calc(100vw / 1920));
  --gutter: calc((100vw - 1440 * var(--dp)) / 2);
  --cs-ink: #0B0B0D;
  --cs-foot: #D9D9D9;
}

@media (max-width: 1024px) {
  :root { --dp: min(1px, calc(100vw / 1440)); }
}

/* ==========================================================================
   Work header
   ==========================================================================
   Sticky with zero height: it pins to the top of #screen (the scroll
   container) without taking up space, so the hero still starts at y=0 under
   it. A fixed header would not work here — #screen's filter makes it the
   containing block, and fixed children would scroll away with the content.
   Figma: logo 81x41 at (70, 54); nav right-aligned to x=1372, top 67, gap 16,
   Plex Mono Medium 12. */
.work-head {
  position: sticky;
  inset-block-start: 0;
  block-size: 0;
  z-index: var(--z-chrome);
}

.work-head__bar {
  position: absolute;
  inset-block-start: calc(54 * var(--dp));
  inset-inline-start: calc(70 * var(--dp));
  inset-inline-end: calc(70 * var(--dp));
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.work-head__logo {
  display: block;
  inline-size: calc(81 * var(--dp));
  block-size: calc(41 * var(--dp));
}
.work-head__logo img {
  inline-size: 100%;
  block-size: 100%;
  /* Recoloured rather than swapped: both inks are the same drawing. */
  filter: brightness(0) invert(0.95);
  transition: filter var(--dur-fade) var(--ease);
}
.work-head__logo[data-ink='dark'] img { filter: brightness(0) invert(0.04); }

.work-head__nav {
  display: flex;
  gap: calc(16 * var(--dp));
}
/* The buttons themselves are css/nav.css (.navbtn); only the logo follows
   the header tone. */

/* ==========================================================================
   Canvas
   ========================================================================== */
.fx-canvas {
  position: relative;
  z-index: 1;
  background: var(--paper);
  color: #000;
}
/* Only while js/work-page.js is actually peeling the sheet off the next hero:
   promoting a ~6500px element for the whole visit is a layer nobody needs. */
.fx-canvas.is-peeling { will-change: transform; }
/* Touch screens: always on its own layer. iOS Safari scrolls the sticky
   reveal pin on the compositor; a sheet painted into the scroller's own layer
   ends up UNDER that pin mid-scroll. As a layer it stacks above it (z 1 > 0)
   whatever the compositor does. Tiled, so only the visible part costs memory. */
@media (hover: none) and (pointer: coarse) {
  .fx-canvas { will-change: transform; }
}

.fx-section {
  position: relative;
  block-size: calc(var(--sh) * var(--dp));
}

.fx-hero {
  overflow: hidden;
  background: #000;
  color: #F1EDE4;
}
.fx-hero__bg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
}

/* The page below the hero. Flat --paper unless the frame paints its own ground
   (SOLN's brick wall), which is stretched over the section — at 1440 and up
   that is the ground at its own size, and wider than that it goes full bleed
   with the rest of the backgrounds. --ink, when set, is the running colour:
   .fx-text reads it, so a dark frame needs no per-layer colours. */
.fx-page {
  background-color: var(--paper);
  background-image: var(--ground, none);
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* A group of pictures (js/work-render.js clusterLayer). No box of its own on
   the canvas; reflow gives it one (below). */
.fx-cluster { display: contents; }

/* One layer, placed on the canvas. */
.fx {
  position: absolute;
  inset-inline-start: calc(var(--gutter) + var(--x) * var(--dp));
  inset-block-start: calc(var(--y) * var(--dp));
  margin: 0;
}

.fx-media {
  inline-size: calc(var(--w) * var(--dp));
  block-size: calc(var(--h) * var(--dp));
  overflow: hidden;
  border-radius: calc(5 * var(--dp));
}
.fx-media > img {
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
}
.fx-logo { border-radius: 0; }
.fx-box { background: var(--bg); }

/* A caption drawn on an image (kit.js media extra.label): Plex Mono Bold
   Italic, white, as in the author's EARLY CONCEPTS frame — 12 on a 384-wide
   image, 11 in from the left, 7 from the top or 12 from the bottom. Sized in
   the image's own width (container units), so it keeps its proportion when
   the image goes full-width on phones. */
.fx-media--labelled { container-type: inline-size; }
.fx-media__label {
  position: absolute;
  inset-inline-start: calc(100cqw * 11 / 384);
  font-family: var(--font-mono);
  font-size: calc(100cqw * 12 / 384);
  font-weight: 700;
  font-style: italic;
  line-height: 1.3;
  color: #FFFFFF;
  white-space: nowrap;
  pointer-events: none;
}
.fx-media__label--top    { inset-block-start: calc(100cqw * 7 / 384); }
.fx-media__label--bottom { inset-block-end: calc(100cqw * 12 / 384); }

/* A drawn heading (kit.js `art`): the box IS the artwork's bounding box, so it
   fills rather than covers, and it carries no frame of its own. */
.fx-art { border-radius: 0; }
.fx-art > img { object-fit: fill; }

/* Text boxes use the font's own line height (Figma "auto") unless a layer sets
   one; paragraph breaks are one blank line, as in the frame. */
.fx-text {
  inline-size: calc(var(--w) * var(--dp));
  font-family: var(--font-mono);
  font-size: calc(var(--fs) * var(--dp));
  font-weight: var(--fw);
  line-height: normal;
  letter-spacing: 0;
  color: var(--ink, inherit);
  overflow-wrap: break-word;
}
.fx-text[style*='--lh'] { line-height: calc(var(--lh) * var(--dp)); }
.fx-text p { margin: 0; }
.fx-text p + p { margin-block-start: calc(var(--lh) * var(--dp)); }
.fx-text--chivo { font-family: var(--font-chivo); }
.fx-text--nowrap {
  inline-size: auto;
  white-space: nowrap;
}

/* Scroll reveal — layers come in as they reach the viewport. The staged class
   is added by JS, so without it every layer is simply visible. */
.fx-page.is-revealing .fx {
  opacity: 0;
  transform: translateY(26px);
  transition:
    opacity 620ms var(--ease-out) var(--reveal-delay, 0ms),
    transform 700ms var(--ease-out) var(--reveal-delay, 0ms);
}
.fx-page.is-revealing .fx.is-in {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .fx-page.is-revealing .fx { opacity: 1; transform: none; transition: none; }
}
/* Printing never scrolls, so layers that have not been reached would come out
   blank. On paper everything is simply there. */
@media print {
  .fx-page.is-revealing .fx { opacity: 1; transform: none; transition: none; }
}

/* ==========================================================================
   Compare slider
   ========================================================================== */
.compare {
  touch-action: pan-y;          /* horizontal drags slide; vertical still scrolls */
  user-select: none;
  -webkit-user-select: none;
  background: #16171b;
}
.compare .compare__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  object-position: left center;   /* the HUD lives on the left edge */
  pointer-events: none;
}
.compare__before {
  position: absolute;
  inset: 0;
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}
.compare__handle {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--pos);
  inline-size: 2px;
  margin-inline-start: -1px;
  background: rgba(241, 237, 228, 0.9);
  pointer-events: none;
}
.compare__knob {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  inline-size: max(26px, calc(34 * var(--dp)));
  aspect-ratio: 1;
  border-radius: 50%;
  background: #F1EDE4;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  transform: translate(-50%, -50%);
  transition: transform var(--dur-hover) var(--step-hover);
}
/* Two chevrons pointing outward. */
.compare__knob::before,
.compare__knob::after {
  content: '';
  position: absolute;
  inset-block-start: 50%;
  inline-size: 26%;
  aspect-ratio: 1;
  max-inline-size: 7px;
  border: solid var(--cs-ink);
  border-width: 0 0 1.5px 1.5px;
}
.compare__knob::before { inset-inline-start: 22%; transform: translateY(-50%) rotate(45deg); }
.compare__knob::after  { inset-inline-end: 22%;   transform: translateY(-50%) rotate(-135deg); }
.compare.is-dragging .compare__knob { transform: translate(-50%, -50%) scale(1.12); }

/* Which side is which, readable on the dark shots at any size (author,
   2026-09-27: the old 10px grey chips could not be told apart). Solid chips,
   the cream type bold, over the images and the divider. */
.compare__tag {
  position: absolute;
  z-index: 2;
  inset-block-start: max(8px, calc(12 * var(--dp)));
  padding: max(5px, calc(7 * var(--dp))) max(8px, calc(11 * var(--dp)));
  border: 1px solid rgba(241, 237, 228, 0.55);
  border-radius: 3px;
  background: rgba(11, 11, 13, 0.9);
  color: #F1EDE4;
  font-size: max(11px, calc(14 * var(--dp)));
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--ls-mono-ui);
  text-transform: uppercase;
  pointer-events: none;
}
.compare__tag--before { inset-inline-start: max(8px, calc(12 * var(--dp))); }
.compare__tag--after  { inset-inline-end: max(8px, calc(12 * var(--dp))); }

/* The range is the keyboard/screen-reader control: invisible but focusable;
   its focus is shown on the whole figure. */
.compare__range {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  opacity: 0;
  pointer-events: none;
}
.compare:focus-within {
  overflow: visible;
  outline: 2px solid var(--cs-ink);
  outline-offset: 4px;
}

/* ==========================================================================
   Carousel — one slide at a time, buttons outside the frame
   ==========================================================================
   The layer box is the visible frame; the buttons sit 48 design px outside it
   (shadcn's -left-12 / -right-12), so the layer itself must not clip. */
.carousel { overflow: visible; }

.carousel__viewport {
  block-size: 100%;
  overflow: hidden;
  border-radius: calc(5 * var(--dp));
  background: #16171b;
  touch-action: pan-y;          /* horizontal drags slide; vertical still scrolls */
  cursor: grab;
}
.carousel__viewport:focus-visible { outline: 2px solid var(--cs-ink); outline-offset: 4px; }
/* Dark grounds (SOLN's brick, IRONHOLLOW's blocks): focus rings in the cream. */
.fx-page[data-tone='dark'] .compare:focus-within,
.fx-page[data-tone='dark'] .carousel__viewport:focus-visible { outline-color: #F1EDE4; }
.carousel.is-dragging .carousel__viewport { cursor: grabbing; }

.carousel__track {
  display: flex;
  block-size: 100%;
  transition: transform 450ms var(--ease-out);
  will-change: transform;
}
.carousel.is-dragging .carousel__track { transition: none; }

.carousel__slide {
  flex: 0 0 100%;
  block-size: 100%;
}
.carousel__slide img {
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

.carousel__btn {
  position: absolute;
  inset-block-start: 50%;
  display: grid;
  place-items: center;
  inline-size: max(28px, calc(32 * var(--dp)));
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid rgba(11, 11, 13, 0.18);
  border-radius: 50%;
  background: #fff;
  color: var(--cs-ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  transform: translateY(-50%);
  transition: background var(--dur-hover) var(--step-hover), opacity var(--dur-hover) var(--step-hover);
}
.carousel__btn:hover:not(:disabled) { background: #f1f1f1; }
.carousel__btn:disabled { opacity: 0.5; }
.carousel__btn--prev { inset-inline-start: calc(-48 * var(--dp)); }
.carousel__btn--next { inset-inline-end: calc(-48 * var(--dp)); }

/* ==========================================================================
   Reveal zone — the next project, uncovered
   ==========================================================================
   Pulled up under the sheet's last screen, so it is already sitting there when
   the page above lifts off it, rather than being a panel you scroll down to.
   The pin holds the next hero still while the remaining scroll runs past it;
   --p (0..1) is written by js/work-page.js. */
.work-reveal {
  position: relative;
  z-index: 0;
  block-size: 200vh;
  block-size: 200svh;
  margin-block-start: -100vh;
  margin-block-start: -100svh;
  color: #F1EDE4;
}

/* Not drawn until the sheet starts lifting off it (js/work-page.js sets
   data-gated and toggles .is-uncovered at p > 0), so it can never show
   through the page. The zone's own dark ground covers the frame before the
   script catches up. */
.work-reveal[data-gated] { background: #05060a; }
.work-reveal[data-gated]:not(.is-uncovered) .work-reveal__pin { visibility: hidden; }

.work-reveal__pin {
  position: sticky;
  inset-block-start: 0;
  block-size: 100vh;
  block-size: 100svh;
  overflow: hidden;
  background: #05060a;
}

/* The hero settles as it is uncovered: a touch of scale coming off. */
.work-reveal__bg {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  opacity: calc(0.55 + 0.45 * var(--p, 0));
  transform: scale(calc(1.08 - 0.08 * var(--p, 0)));
}

.work-reveal__inner {
  position: relative;
  block-size: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding-inline: 20px;
  text-align: center;
  opacity: calc(0.25 + 0.75 * var(--p, 0));
  transform: translateY(calc(26px - 26px * var(--p, 0)));
}

.work-reveal__label,
.work-reveal__hint {
  margin: 0;
  font-size: max(11px, calc(12 * var(--dp)));
  font-weight: 500;
  letter-spacing: var(--ls-mono-ui);
  text-transform: uppercase;
  opacity: 0.75;
}

.work-reveal__title { display: block; }
.work-reveal__logo {
  inline-size: min(calc(720 * var(--dp)), 70vw);
  block-size: auto;
  max-inline-size: none;
}

.work-reveal__track {
  position: relative;
  inline-size: min(280px, 60vw);
  block-size: 2px;
  overflow: hidden;
}
.work-reveal__track::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0.25;
}
.work-reveal__fill {
  position: absolute;
  inset: 0;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
}

@media (prefers-reduced-motion: reduce) {
  .work-reveal__bg,
  .work-reveal__inner { opacity: 1; transform: none; }
}

/* Phones: the author's 393px nav — logo 40x21 at (23, 37), buttons 26x24
   with their right edge 28 in, the row centred on the logo. Same as the home
   masthead (css/responsive.css). */
@media (max-width: 768px) {
  .work-head__bar {
    inset-block-start: 36px;
    inset-inline-start: 23px;
    inset-inline-end: 28px;
    align-items: center;
  }
  .work-head__logo { inline-size: 40px; block-size: 21px; }
  .work-head__nav { gap: 16px; }

  /* Descriptions at ~10px on phones (author's call), line and paragraph
     spacing kept in the desktop proportion (16/12). */
  .fx-page .fx-text[data-role='desc'] { font-size: 10px; line-height: 13.33px; }
  /* Headings shrink with the copy: the drawing's design width x 10/14. */
  .fx-page .fx-art.fx-media { inline-size: min(100%, calc(var(--w) * 10 / 14 * 1px)); }
  .fx-page [data-role='desc'] p + p { margin-block-start: 13.33px; }
}

/* ==========================================================================
   Narrow viewports — the layers reflow in reading order
   ========================================================================== */
@media (max-width: 899px) {

  .fx-section {
    block-size: auto;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .fx-hero {
    min-block-size: 100svh;
    justify-content: flex-end;
    padding: 110px 20px 56px;
  }
  /* Reflowed, the page runs far taller than its ground — stretching it would
     smear it, and tiling it would show its glow and vignette repeating. A
     frame that offers a bare tile repeats that instead. */
  .fx-page {
    padding: 72px 20px 96px;
    background-image: var(--ground-tile, var(--ground, none));
    background-size: 1100px auto;
    background-repeat: repeat;
  }

  .fx { position: relative; inset: auto; }

  .fx-media {
    inline-size: 100%;
    block-size: auto;
    aspect-ratio: var(--w) / var(--h);
    border-radius: 5px;
  }
  /* A drawn heading is still not a panel: no frame here either. */
  .fx-art { border-radius: 0; }
  .fx-logo { margin-block-end: 36px; }

  .fx-text {
    inline-size: auto;
    font-size: calc(var(--fs) * 1px);
  }
  .fx-text[style*='--lh'] { line-height: calc(var(--lh) * 1px); }
  .fx-text p + p { margin-block-start: calc(var(--lh) * 1px); }
  .fx-text--nowrap { white-space: normal; }

  .fx-hero [data-role='label'] { margin-block-start: 14px; }
  .fx-page [data-role='heading']:not(:first-child) { margin-block-start: 56px; }
  .fx-page [data-role='subheading'] { margin-block-start: 28px; }

  /* Headings at their design width, not stretched to the column. */
  .fx-page .fx-art { inline-size: min(100%, calc(var(--w) * 1px)); }
  /* The frame spacing, reflowed: 11 heading -> text, 22 text -> images
     (the column gap is 14). */
  .fx-page [data-role='heading'] + [data-role='desc'],
  .fx-page [data-role='heading'] + [data-role='subheading'] { margin-block-start: -3px; }
  .fx-page [data-role='desc'] + [data-role='media'] { margin-block-start: 8px; }

  /* Picture groups keep their desktop arrangement (author, 2026-09-27): the
     group's box becomes a block the width of the column, at the group's own
     proportions, and every picture (and caption) inside is placed in it as a
     share of that box — the desktop layout scaled down as one piece, while the
     headings and copy around it stay full-size text. */
  .fx-cluster {
    display: block;
    position: relative;
    inline-size: 100%;
    aspect-ratio: var(--cw) / var(--ch);
    container-type: inline-size;
  }
  .fx-page [data-role='desc'] + .fx-cluster { margin-block-start: 8px; }

  /* Pictures run edge to edge (author, 2026-09-27: "remove the borders ...
     make it fullscreen"): every picture group and lone picture reaches past
     the page's 20px side padding to the screen edges; headings and copy keep
     the margin. A lone full-bleed picture drops its rounded corners; the
     carousel's arrows come inside the frame, which now has no outside. */
  .fx-page > .fx-cluster,
  .fx-page > .fx-media:not(.fx-art) {
    inline-size: calc(100% + 40px);
    margin-inline: -20px;
  }
  .fx-page > .fx-media:not(.fx-art),
  .fx-page > .carousel .carousel__viewport { border-radius: 0; }
  .fx-page > .carousel .carousel__btn--prev { inset-inline-start: 10px; }
  .fx-page > .carousel .carousel__btn--next { inset-inline-end: 10px; }
  /* Two clusters in a row (a picture group split for phones) keep the
     canvas rhythm between them rather than the column gap alone. */
  .fx-cluster + .fx-cluster { margin-block-start: 6px; }
  /* Layers the author left out of the phone layout (kit media phone:'hide'). */
  .fx--desktop-only { display: none; }
  .fx-cluster > .fx {
    position: absolute;
    inset-inline-start: calc((var(--x) - var(--cx)) / var(--cw) * 100%);
    inset-block-start: calc((var(--y) - var(--cy)) / var(--ch) * 100%);
    margin: 0;
  }
  .fx-cluster > .fx-media {
    inline-size: calc(var(--w) / var(--cw) * 100%);
    block-size: calc(var(--h) / var(--ch) * 100%);
    aspect-ratio: auto;
    border-radius: max(3px, calc(5 / var(--cw) * 100cqw));
  }
  /* Captions scale with their pictures, down to a floor that still reads. */
  .fx-cluster > .fx-text {
    font-size: max(8px, calc(var(--fs) / var(--cw) * 100cqw));
    line-height: 1.1;
    white-space: nowrap;
  }

  .compare__knob { inline-size: 30px; }
  .carousel__viewport { border-radius: 5px; }
  .work-reveal__logo { inline-size: 76vw; }
}

/* Older iPhones (iOS 15 and below, e.g. an iPhone 12 or 13 not yet on iOS 16)
   have no container units, so the image labels and the phone picture groups'
   captions would lose their sizes. Fixed sizes in the same proportion. */
@supports not (width: 1cqw) {
  .fx-media__label { inset-inline-start: 11px; font-size: 12px; }
  .fx-media__label--top    { inset-block-start: 7px; }
  .fx-media__label--bottom { inset-block-end: 12px; }
  @media (max-width: 899px) {
    .fx-media__label { inset-inline-start: 4px; font-size: 6px; }
    .fx-media__label--top    { inset-block-start: 3px; }
    .fx-media__label--bottom { inset-block-end: 4px; }
    .fx-cluster > .fx-text { font-size: 8px; }
    .fx-cluster > .fx-media { border-radius: 3px; }
  }
}
