/* ---- Inflex Trace case study ----
   Page-specific styles. Loaded after css/style.css, so the shared tokens and
   the site header/nav are already in place. Everything here is prefixed .ix-
   so it can never collide with the shared sheet. */

/* Every block on this page sits inside the nav's content box, so the case study
   lines up with the logo on the left and the nav links on the right. The page
   ground stays the site's light grey; only the blocks themselves are dark. */
.ix-page {
  /* The project's dark. Every block inherits it from here, so changing this one
     value re-grounds the whole case study. */
  --ix-bg: #16001b;
  /* The muted purple-grey of the caption body copy. Shared so the dots and the
     text stay the same colour if either is retuned. */
  --ix-muted: #78748a;
  --ix-muted-soft: rgba(120, 116, 138, 0.5);

  width: calc(100% - 2 * var(--case-pad-x));
  margin-inline: auto;
  /* Dark ground under the entire column. Blocks paint their own backgrounds on
     top; this catches anything shorter than its row — a half that does not
     stretch, a stacked layout, a block yet to be filled — so the page's light
     grey can never show through mid-project. */
  background: var(--ix-bg);
}

/* Every block spans the column. Belt and braces: some blocks carry an
   aspect-ratio, and a box with one can size its inline axis from its block
   axis rather than filling its container. */
.ix-page > * {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}

.ix-hero {
  /* Each glow needs a zero-alpha twin for the gradient's far stop. Keep the
     pair in sync when retuning — see the note on the gradients below. */
  --ix-glow-a: #370430;
  --ix-glow-a-0: rgba(55, 4, 48, 0);
  --ix-glow-b: #300439;
  --ix-glow-b-0: rgba(48, 4, 57, 0);

  position: relative;
  overflow: hidden;
  isolation: isolate; /* keeps the z-index:-1 glows from falling behind the page */
  display: grid;
  place-items: center;
  /* Holds the frame's 1440x423 proportion now that the block is inset rather
     than full-bleed; the floor keeps it usable once it narrows on mobile. */
  aspect-ratio: 1440 / 423;
  min-height: 220px;
  background: var(--ix-bg);
}

/* Two soft glows drifting independently over the base colour. Radial-gradient
   fades to a zero-alpha copy of its own colour rather than `transparent` —
   `transparent` is rgba(0,0,0,0) and interpolates through black, which would
   leave a dirty grey halo against a dark ground. */
.ix-hero::before,
.ix-hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  will-change: transform;
}

.ix-hero::before {
  width: 85%;
  height: 130%;
  left: -12%;
  top: -18%;
  background: radial-gradient(ellipse closest-side, var(--ix-glow-a), var(--ix-glow-a-0));
  animation: ix-drift-a 19s ease-in-out infinite alternate;
}

.ix-hero::after {
  width: 75%;
  height: 120%;
  right: -10%;
  bottom: -20%;
  background: radial-gradient(ellipse closest-side, var(--ix-glow-b), var(--ix-glow-b-0));
  animation: ix-drift-b 26s ease-in-out infinite alternate;
}

/* 19s and 26s share no common factor, so the two glows never realign and the
   movement never reads as a loop. Transform-only, so it stays on the
   compositor and costs no repaints. */
@keyframes ix-drift-a {
  from {
    transform: translate3d(-7%, -5%, 0) scale(1);
  }
  to {
    transform: translate3d(9%, 7%, 0) scale(1.22);
  }
}

@keyframes ix-drift-b {
  from {
    transform: translate3d(8%, 7%, 0) scale(1.16);
  }
  to {
    transform: translate3d(-8%, -7%, 0) scale(1);
  }
}

.ix-hero-logo {
  width: clamp(180px, 22vw, 340px);
  height: auto;
}

@media (prefers-reduced-motion: reduce) {
  .ix-hero::before,
  .ix-hero::after {
    animation: none;
  }
}

/* ---- 50/50 split rows ----
   Two equal columns of the project width. A row with only one child fills the
   left column and leaves the right empty, so blocks can be built up a half at
   a time. */
.ix-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.ix-panel {
  position: relative;
  aspect-ratio: 720 / 586; /* the frame's half-width panel */
}

/* The Rive artboard is square and paints its own dark background, so the panel
   sits behind it as letterboxing either side. Matching the hero colour keeps
   that seam invisible. */
.ix-panel-light {
  background: var(--ix-bg);
}

.ix-panel canvas {
  width: 100%;
  height: 100%;
  display: block;
  /* Cursor is driven per-pixel from main.js for both panels, so it is a
     pointer only over the artwork rather than the whole canvas. */
  cursor: default;
}

/* Right half of a split: a fixed-height item on top, the rest filled below.
   The row's height comes from the Rive panel opposite, so this column matches
   it exactly and neither can push past the other. */
.ix-stack {
  display: flex;
  flex-direction: column;
  overflow: hidden; /* nothing escapes the row the panel defines */
  min-height: 0;
  background: var(--ix-bg);
}

.ix-stack-top {
  width: 100%;
  height: auto; /* keeps the mockup's own proportions */
  flex: 0 0 auto;
  display: block;
}

/* Takes whatever height is left. flex-basis 0 + min-height 0 means it never
   contributes to the row's intrinsic height, so it can only shrink to fit,
   never stretch the row. object-fit crops rather than distorts. */
.ix-stack-fill {
  width: 100%;
  flex: 1 1 0;
  min-height: 0;
  display: block;
  object-fit: cover;
}

/* Full-width media band. The wrapper carries the ratio and crops with
   object-fit, so source files stay at their exported size — this one is a
   2400x1800 4:3 shown as a wide strip. Override the ratio per block with
   --ix-media-ratio. */
.ix-media {
  aspect-ratio: var(--ix-media-ratio, 1440 / 530);
  overflow: hidden;
}

.ix-media img,
.ix-media video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* Not centred: the wanted band sits low in the frame, so the crop is pulled
     down. Override per block with --ix-media-pos. */
  object-position: center var(--ix-media-pos, 50%);
  /* object-fit already locks the scale to the band's width, so growing the
     content means scaling past it — the wrapper's overflow:hidden clips the
     excess. Override per block with --ix-media-zoom. */
  /* translate first in the declaration so it applies last, in unscaled units —
     --ix-media-shift is then real screen pixels, not pixels x zoom. */
  transform: translateY(var(--ix-media-shift, 0)) scale(var(--ix-media-zoom, 1));
}

/* Stage shot: keeps roughly y 35%-87% of the 2400x1800 source — spotlights,
   banner, stage, chair tops — dropping the empty roof and the front chairs. */
.ix-media-stage {
  --ix-media-ratio: 2400 / 936;
  --ix-media-pos: 73%;
  --ix-media-zoom: 1.2; /* scales the shot up inside the band, band unchanged */
  --ix-media-shift: 20px;
}

/* Half that sets the row height from its own proportions. The frame clips, so
   the image can be scaled up inside it without changing the row height —
   transform does not affect layout. */
.ix-half-frame {
  overflow: hidden;
}

.ix-half-frame img {
  width: 100%;
  height: auto;
  display: block;
  /* translate first in the declaration so it applies last, in unscaled units —
     --ix-half-shift is then real pixels regardless of the zoom. */
  transform: translateY(var(--ix-half-shift, 0)) scale(var(--ix-half-zoom, 1));
}

.ix-frame-idcard {
  --ix-half-zoom: 1.35;
  --ix-half-shift: -70px; /* runs the lanyard off the top edge */
}

/* Half that fills whatever height the other half set. The video is out of flow
   so it contributes nothing to row sizing — otherwise its intrinsic height
   would fight the image opposite and stretch the row. */
.ix-half-media {
  position: relative;
  overflow: hidden;
  /* Any sliver the video does not cover reads as the page's light grey, which
     is glaring against these dark blocks. Dark ground makes it invisible. */
  background: var(--ix-bg);
}

.ix-half-media video {
  position: absolute;
  inset: 0;
  width: 100%;
  /* 2px of bleed past the box, clipped by the wrapper's overflow. Rows get
     fractional heights, and rounding can leave the video a pixel short of the
     bottom edge. */
  height: calc(100% + 2px);
  display: block;
  object-fit: cover;
}

/* Laptop mockup: 4:3 source trimmed to a ~1.51:1 band, so a little dead space
   comes off the top and bottom. Crop position stays centred, so both ends lose
   the same amount. */
.ix-media-laptop {
  --ix-media-ratio: 2400 / 1590;
}

/* ---- Caption block ----
   Text starts on the container's halfway line, so it lines up with the
   right-hand half of every split block above it. Same 1fr 1fr grid as
   .ix-split, so the two can never drift apart as the window resizes. */
.ix-caption {
  display: grid;
  grid-template-columns: 1fr 1fr;
  background: var(--ix-bg);
  /* 40px at 1440, above the heading and below the paragraph - the same air the
     other three case studies leave around their captions. This block used to
     reserve a 205px band and float the text in the middle of it, which put the
     text a different distance from its neighbours than everywhere else on the
     site, and a distance that shifted with the length of the copy. */
  padding-block: var(--case-gap);
}

.ix-caption-text {
  grid-column: 2; /* the halfway line; no left padding, so it sits exactly on it */
  font-family: "Outfit", "Poppins", "Segoe UI", Arial, sans-serif;
  padding-right: clamp(16px, 3vw, 48px);
}

.ix-caption h2 {
  margin: 0 0 0.7em;
  font-size: clamp(20px, 1.736vw, 30px); /* 25px at 1440 */
  font-weight: 400;
  color: #ffffff;
}

.ix-caption p {
  margin: 0;
  font-size: clamp(15px, 1.25vw, 22px); /* 18px at 1440 */
  font-weight: 300;
  line-height: 1.5;
  color: var(--ix-muted);
}

/* The logo animation film (the shared .film block in css/style.css). The
   ratio is the render's own 16:9 (1920x1080), so nothing is cropped; the
   ground is the page's dark so the frame reads as part of the block column. */
.ix-film {
  aspect-ratio: 16 / 9;
  background: var(--ix-bg);
}

/* ---- Landing page scrolling inside the monitor ----
   The mockup is an opaque JPEG, so the page is overlaid on the screen area
   rather than placed behind it. The inset values below are the display's
   bounds measured as percentages of the 2400x1688 mockup, so they hold at any
   rendered size. */
/* Full-width dark ground, so narrowing the monitor does not open light grey
   gaps in the block column either side of it. */
.ix-screen-block {
  background: var(--ix-bg);
}

.ix-screen-mockup {
  position: relative;
  width: var(--ix-screen-w, 60%);
  margin-inline: auto;
  /* Lets the screen's corner radius be expressed in cqw — a share of the
     mockup's own width — so it tracks the monitor at any rendered size. */
  container-type: inline-size;
}

.ix-screen-frame {
  width: 100%;
  height: auto;
  display: block;
}

.ix-screen {
  position: absolute;
  left: 1.1%;
  right: 1%;
  top: 1.4%;
  bottom: var(--ix-screen-b, 21%); /* below this is bezel and stand */
  overflow: hidden;
  /* Matches the monitor's rounded screen. A % radius would go elliptical on a
     1.8:1 box; cqw keeps both axes equal. vw first as a fallback. */
  border-radius: 1.2vw;
  border-radius: var(--ix-screen-radius, 2.5cqw);
}

/* -88.51% is where the page's bottom meets the screen's bottom, derived from
   the two sources: page 1480x7178 rendered at the screen's 1.794 aspect.
   Recompute this if --ix-screen-b or the page image changes. */
.ix-screen img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  display: block;
  will-change: transform;
  animation: ix-page-scroll 60s linear infinite;
}

/* Linear, so it reads as a steady scroll rather than an eased slide. Holds ~3s
   at the top, scrolls for ~54s, holds ~3s at the bottom, then cuts back to the
   start — one full cycle per minute. */
@keyframes ix-page-scroll {
  0%,
  5% {
    transform: translateY(0);
  }
  95%,
  100% {
    transform: translateY(var(--ix-scroll-end, -88.51%));
  }
}

@media (prefers-reduced-motion: reduce) {
  .ix-screen img {
    animation: none;
  }
}

/* ---- Landing animation carousel ----
   A symmetric stack: the active clip sits front and centre at full size, with
   the two either side of it receding behind. Sources are 1080x1080, so the
   slides are square. Click (or Enter/Space) advances; main.js sets data-pos on
   each slide as its offset from the active one, and this stylesheet turns that
   into a position. */
.ix-carousel-block {
  background: var(--ix-bg);
  /* Asymmetric: the caption below has its own padding and centres itself in a
     205px min-height, so an equal bottom pad left far too big a gap. */
  padding-top: clamp(32px, 5vw, 80px);
  padding-bottom: clamp(4px, 1vw, 16px);
}

.ix-carousel {
  position: relative;
  /* One knob for the whole block: the slides and the track height are all
     percentages of this, so they scale together. */
  width: var(--ix-carousel-w, 62%);
  margin-inline: auto;
  aspect-ratio: 2.24;
  cursor: pointer;
  overflow: hidden; /* clips the outermost pair at the block edges */
}

.ix-carousel:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.ix-carousel-track {
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  inset: 0;
}

.ix-slide {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 39.5%;
  aspect-ratio: 1; /* the clips are square */
  margin: 0;
  border-radius: 2cqw;
  overflow: hidden;
  transition: transform 0.55s ease, opacity 0.55s ease;
  transform: translate(-50%, -50%);
}

.ix-slide video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

/* Active: front, full size, and the shadow that falls across its neighbours. */
.ix-slide[data-pos="0"] {
  z-index: 3;
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  /* Left and right only. The negative spread is half the blur, which cancels
     the vertical bleed exactly, so the shadow falls sideways onto the
     neighbouring slides and not above or below the card. */
  box-shadow: -40px 0 50px -25px rgba(0, 0, 0, 0.85),
    40px 0 50px -25px rgba(0, 0, 0, 0.85);
}

.ix-slide[data-pos="-1"],
.ix-slide[data-pos="1"] {
  z-index: 2;
  opacity: 0.72;
}

.ix-slide[data-pos="-1"] {
  transform: translate(-50%, -50%) translateX(-58%) scale(0.82);
}

.ix-slide[data-pos="1"] {
  transform: translate(-50%, -50%) translateX(58%) scale(0.82);
}

.ix-slide[data-pos="-2"],
.ix-slide[data-pos="2"] {
  z-index: 1;
  opacity: 0.42;
}

.ix-slide[data-pos="-2"] {
  transform: translate(-50%, -50%) translateX(-104%) scale(0.66);
}

.ix-slide[data-pos="2"] {
  transform: translate(-50%, -50%) translateX(104%) scale(0.66);
}

@media (prefers-reduced-motion: reduce) {
  .ix-slide {
    transition: none;
  }
}

/* ---- Carousel dots ----
   The button carries the tap target (~26px) and the span draws the dot, so the
   mark can stay small without being fiddly to hit on a phone. */
.ix-carousel-dots {
  display: flex;
  justify-content: center;
  gap: clamp(4px, 0.6vw, 8px);
  margin-top: clamp(12px, 1.6vw, 24px);
}

.ix-dot {
  padding: clamp(6px, 0.8vw, 9px);
  border: 0;
  background: none;
  line-height: 0;
  cursor: pointer;
}

.ix-dot span {
  display: block;
  width: clamp(8px, 0.7vw, 10px);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid var(--ix-muted-soft);
  transition: background-color 0.25s ease, border-color 0.25s ease,
    transform 0.25s ease;
}

.ix-dot:hover span {
  border-color: var(--ix-muted);
}

.ix-dot[aria-current="true"] span {
  background: var(--ix-muted);
  border-color: var(--ix-muted);
  transform: scale(1.15);
}

.ix-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .ix-dot span {
    transition: none;
  }
}

/* Shrinks a clip inside its half without changing the row. contain rather than
   cover, so scaling down reveals the whole frame instead of a cropped one. */
/* Sets the row height for this block. A square row left the clip floating in
   dark space top and bottom; 1.2 trims that while the raised scale below keeps
   the animation itself the same size. */
.ix-half-media-inset {
  aspect-ratio: var(--ix-row-ratio, 2.44);
}

.ix-half-media-inset video {
  object-fit: contain;
  transform: scale(var(--ix-half-media-scale, 1));
}

/* The panel fills the row so its dark ground covers the cell top to bottom;
   the icons themselves are centred within it by Rive's Contain + Center fit,
   rather than by shrinking the panel to the artboard. */
/* Cursor here is driven from main.js per-pixel, so it is a pointer only over a
   button and not across the whole canvas. */
.ix-panel-icons canvas {
  cursor: default;
}

/* ---- Interaction hint ----
   On both Rive panels: plays once, when the panel first scrolls into view
   (.is-hinting, set by setUpPanelHint in main.js) - the panel dims and blurs,
   the word ("click" / "hover", "tap" on touch) fades in, both hold, then both
   fade out. The home grid's language - dim the tile, name it in white
   Poppins - lighter here, since the artwork underneath should stay readable.

   pointer-events: none lets input through to the canvas, and using the panel
   mid-hint (.is-used) cuts it short so the artwork's own response plays
   undimmed.

   The logo panel acts as one button (main.js forwards clicks off the symbol
   onto it), so the cursor is a pointer across all of it. */
.ix-clickable,
.ix-clickable canvas {
  cursor: pointer;
}

.ix-hint {
  --ix-hint-time: 3.2s;

  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.3);
  /* Softens the artwork behind the word so it reads as a pause, not a tint.
     Fading the element's opacity fades the blur with it, so it rides the same
     dim-in / fade-out timing with no animation of its own. */
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  pointer-events: none;
  opacity: 0;
}

.ix-hint-word {
  font-family: "Poppins", "Segoe UI", Arial, sans-serif;
  /* A quiet label, not a title: 14px at 1440, a step under the grid's names. */
  font-size: max(12px, 0.972vw);
  font-weight: 400;
  color: #fff;
  opacity: 0;
}

/* Both run on one clock so they leave together: the dim lands first, the word
   follows it in, and from 75% both fade out as one. */
.is-hinting > .ix-hint {
  animation: ix-hint-dim var(--ix-hint-time) ease both;
}

.is-hinting > .ix-hint .ix-hint-word {
  animation: ix-hint-word var(--ix-hint-time) ease both;
}

@keyframes ix-hint-dim {
  0% { opacity: 0; }
  12% { opacity: 1; }
  75% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes ix-hint-word {
  0%, 16% { opacity: 0; }
  30% { opacity: 1; }
  75% { opacity: 1; }
  100% { opacity: 0; }
}

.is-used > .ix-hint {
  animation: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.ix-panel-icons {
  align-self: stretch;
  aspect-ratio: auto; /* overrides .ix-panel's default; the row governs height */
}

/* ---- Back to projects ----
   The shared .project-back / .project-back-link component in css/style.css
   carries this now. All this page adds is its own ground and ink, so the link
   reads white on the case study's dark column instead of the site's default
   ink on light. */
.ix-back {
  --back-ink: #ffffff;
  background: var(--ix-bg);
}

/* ---- Stack to one block per row ----
   Same 1100px breakpoint as the nav collapse and the About/Contact stacks. */
@media (max-width: 1100px) {
  .ix-split,
  .ix-caption {
    grid-template-columns: 1fr;
  }

  /* A grid item carrying an aspect-ratio aligns as `start`, not `stretch`, so
     it can end up sizing its width from the row height and falling short of the
     column. Stacked, every half should simply span the full width. */
  .ix-split > * {
    width: 100%;
    /* justify-self:stretch explicitly beats aspect-ratio in the inline axis.
       The logo panel gets an inline aspect-ratio from main.js, and a grid item
       carrying one aligns as `start`, letting it size its width from its height
       and fall short of the column. */
    justify-self: stretch;
    max-width: none;
  }

  .ix-caption-text {
    grid-column: 1;
    /* Still left aligned, just not flush against the block edge. */
    padding-left: clamp(16px, 4vw, 40px);
  }

  /* ---- Carousel goes vertical ----
     Side by side, a 62%-wide carousel leaves the slides too small to read on a
     phone. Stacked, the active clip can take most of the width and the
     neighbours peek above and below instead of left and right. */
  .ix-carousel {
    --ix-carousel-w: 88%;
    aspect-ratio: 0.85;
  }

  .ix-slide {
    width: 58%;
  }

  /* Shadow falls up and down now, onto the slides it actually sits between. */
  .ix-slide[data-pos="0"] {
    box-shadow: 0 -40px 50px -25px rgba(0, 0, 0, 0.85),
      0 40px 50px -25px rgba(0, 0, 0, 0.85);
  }

  .ix-slide[data-pos="-1"] {
    transform: translate(-50%, -50%) translateY(-58%) scale(0.82);
  }

  .ix-slide[data-pos="1"] {
    transform: translate(-50%, -50%) translateY(58%) scale(0.82);
  }

  .ix-slide[data-pos="-2"] {
    transform: translate(-50%, -50%) translateY(-104%) scale(0.66);
  }

  .ix-slide[data-pos="2"] {
    transform: translate(-50%, -50%) translateY(104%) scale(0.66);
  }

  /* Out of the shared row, the video has no height to fill, so it falls back
     to its own proportions instead of collapsing to zero. */
  .ix-stack-fill {
    flex: 0 0 auto;
    height: auto;
  }

  .ix-half-media video {
    position: static;
    height: auto;
  }

  /* The 2.44 row ratio only makes sense beside the icons panel. Stacked, the
     video reverts to its own height and that short band would clip a square
     clip down to a sliver, so let the wrapper take the clip's height. */
  /* Stacked, this clip is square at full column width — a huge block on a
     phone. Cap it and centre it instead, with a little air around it. */
  .ix-half-media-inset {
    aspect-ratio: auto;
    padding-block: clamp(16px, 4vw, 36px);
  }

  .ix-half-media-inset video {
    transform: none;
    width: min(var(--ix-inset-w, 58%), 320px);
    margin-inline: auto;
  }
}
