/* ==========================================================================
   Nordra case study — hero section
   Standalone entry point for the portfolio case study. Only tokens.css and
   base.css are shared with the marketing site (index.html); this page has
   its own composition, so layout lives here rather than in page.css.
   ========================================================================== */

/* The case study runs dark end to end, so the page ground is set here rather
   than left on --surface-page, which would show white below the last section
   and on overscroll. */
body {
  background: #0d0d0d;
}

/* The hero is always dark, whatever the site theme is set to, so its ground
   and fade are literal rather than token-driven. */
.cs-hero {
  position: relative;
  height: 100vh;
  min-height: 680px;
  overflow: hidden;
  background: #0d0d0d;
  isolation: isolate;
}

/* The artboard runs the photograph at 26% over the near-black ground, which is
   what turns a vivid aurora into the muted texture the logo sits on.

   It also crops the top ~28% of the frame, which is what lifts the mountain
   horizon clear of the phones. `cover` alone cannot reach that crop — on a
   landscape viewport the photo is width-driven and has almost no vertical
   overflow to shift — so the layer itself is oversized and hung above the
   hero, reproducing the artboard's 134.61% / -38.11% offset. */
.cs-hero__media {
  position: absolute;
  left: 0;
  right: 0;
  top: -38%;
  height: 135%;
  background: url("../img/hero-aurora.jpg") center / cover no-repeat;
  background-image: image-set(url("../img/hero-aurora.avif") type("image/avif"), url("../img/hero-aurora.webp") type("image/webp"), url("../img/hero-aurora.jpg") type("image/jpeg"));
  opacity: 0.26;
}

.cs-hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(13, 13, 13, 0) 69.469%, #0d0d0d 99.241%);
}

/* Centred in the band above the carousel. The centring transform lives here,
   on the positioning element, so the entrance transform on .cs-reveal inside
   cannot overwrite it — they are separate properties on separate elements. */
.cs-hero__logo {
  position: absolute;
  top: 18%;
  left: 50%;
  width: clamp(200px, 26vw, 440px);
  transform: translate(-50%, -50%);
  color: var(--text-on-image, #fff);
}

.cs-hero__logo svg {
  width: 100%;
  height: auto;
  display: block;
}

/* ---- Carousel ---- */

/* The band the carousel occupies. It carries the placement so the carousel and
   the demo button over it are one hover target and one box. */
.cs-hero__stage {
  position: absolute;
  top: 36%;
  left: 0;
  right: 0;
  bottom: -6%;
}

/* How far the outer phones dip below the centre one, as a fraction of their
   own height. Read by case-study.js so the arc can be tuned per breakpoint
   here, alongside the sizing it depends on. */
.cs-hero__carousel {
  --arc-amplitude: 0.22;
  position: absolute;
  inset: 0;
  overflow: hidden;
  transition: filter 400ms var(--ease-standard, ease);
}

/* Pausing the drift is what makes the button clickable, so the two states are
   one: the class is set by the same handler that stops the loop. The screens
   go down rather than the button coming up on a scrim, which would sit over
   the arc and flatten it. */
.cs-hero__stage.is-paused .cs-hero__carousel {
  filter: brightness(0.4);
}

.cs-hero__track {
  position: relative;
  display: flex;
  align-items: flex-start;
  height: 100%;
  gap: clamp(20px, 3.2vw, 56px);
  will-change: transform;
}

.cs-mockup {
  flex-shrink: 0;
  height: clamp(300px, 44vh, 620px);
  aspect-ratio: 675 / 1380;
  transform-origin: 50% 30%;
  will-change: transform, opacity;
}

.cs-mockup__inner {
  width: 100%;
  height: 100%;
}

.cs-mockup img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  filter: drop-shadow(0 30px 40px rgba(0, 0, 0, 0.45));
}

/* ---- Demo button ---- */

/* Inverted against the dimmed screens behind it, and on the theme-independent
   pair rather than --surface-inverse: this sits over a photograph, which does
   not flip with the theme, so neither may the control on it.

   Centred on the phones rather than the band: the carousel hangs 6% below the
   section and the arc drops the outer screens, so the optical middle of the
   row is above the middle of the box. */
.cs-hero__demo {
  /* The vertical half of the centring transform, pulled out as a variable so
     the narrow layout — which hangs the button above the band instead of
     centring it on the phones — overrides one value rather than restating the
     transform in all three of its states. */
  --demo-shift-y: -50%;

  position: absolute;
  top: 34%;
  left: 50%;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-4, 8px);
  padding: 14px 26px;
  border: none;
  border-radius: var(--radius-sm, 8px);
  background: var(--on-image-control-bg, #fff);
  color: var(--on-image-control-ink, #0d0d0d);
  font: var(--weight-medium) clamp(15px, 1.125vw, 17px) / 1.2 var(--font-sans);
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);

  /* Held just under its resting position so it rises into place with the dim,
     and scaled from the centre it is translated to — the centring lives in the
     same transform, so both have to be written together. */
  opacity: 0;
  transform: translate(-50%, var(--demo-shift-y)) translateY(8px);
  pointer-events: none;
  transition:
    opacity 300ms var(--ease-standard, ease),
    transform 300ms var(--ease-standard, ease);
}

.cs-hero__demo .icon {
  --icon-size: 20px;
  color: currentColor;
}

.cs-hero__stage.is-paused .cs-hero__demo,
.cs-hero__demo:focus-visible {
  opacity: 1;
  transform: translate(-50%, var(--demo-shift-y));
  pointer-events: auto;
}

.cs-hero__demo:focus-visible {
  outline: 2px solid var(--on-image-control-bg, #fff);
  outline-offset: 4px;
}

/* The lightbox backdrop is translucent by design, but this button is the one
   white shape on a dark hero and reads straight through it as a ghost under
   the panel. It has nothing to do while the panel it opened is up, so it goes.
   Specificity has to clear the .is-paused rule above, which is still set: the
   pointer is where it was, and taking that away would leave the button hidden
   on close until the cursor moved again. */
html.demo-open .cs-hero__stage .cs-hero__demo {
  opacity: 0;
  pointer-events: none;
}

/* Nothing to hover with, so nothing to reveal on hover: the button stands
   permanently and the screens stay at full brightness behind it. */
@media (hover: none) {
  .cs-hero__demo {
    opacity: 1;
    transform: translate(-50%, var(--demo-shift-y));
    pointer-events: auto;
  }
}

/* ==========================================================================
   About the project — role and deliverables
   ========================================================================== */

.cs-about {
  position: relative;
  overflow: hidden;
  background: #0d0d0d;
}

/* Inset from the section edges exactly as the artboard has it, and carrying
   its own fades: the gradients sit inside the 30% layer in the source, so they
   damp the photograph rather than painting solid black over it. */
.cs-about__media {
  position: absolute;
  left: 0;
  right: 0;
  top: 3.04%;
  height: 94.02%;
  opacity: 0.3;
  background:
    radial-gradient(ellipse 84% 234% at 50% 50%, rgba(13, 13, 13, 0) 0%, rgba(13, 13, 13, 1) 100%),
    linear-gradient(0deg, rgba(13, 13, 13, 0) 79.271%, #0d0d0d 100%),
    linear-gradient(180deg, rgba(13, 13, 13, 0) 80.365%, #0d0d0d 100%),
    url("../img/about-fjord.jpg") center / cover no-repeat;
}

/* The laptop, the heading and the card overlap each other's boxes — the
   heading sits in the transparent upper-right of the laptop's bounding box —
   so the desktop composition is placed rather than flowed, on a stage that
   keeps the artboard's proportions as it scales. */
.cs-about__stage {
  position: relative;
  max-width: var(--page-max);
  margin: 0 auto;
  aspect-ratio: 1600 / 1021;
}

.cs-about__laptop {
  position: absolute;
  left: 3.25%;
  top: 10.19%;
  width: 53.81%;
}

.cs-about__laptop img {
  width: 100%;
  height: auto;
}

.cs-about__intro {
  position: absolute;
  left: 42.63%;
  top: 27.42%;
  width: 39.44%;
}

.cs-about__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-on-image, #fff);
  margin-bottom: 8px;
}

.cs-about__lede {
  font: var(--weight-regular) clamp(15px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: var(--text-on-image, #fff);
}

/* Anchored to the bottom rather than the top: the type stops scaling with the
   stage at the clamp floor, so a card pinned by its top edge would grow past
   the stage on narrow viewports and get clipped by the section. */
.cs-about__card {
  position: absolute;
  left: 13.44%;
  bottom: 7.84%;
  width: 73.13%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  padding: clamp(24px, 2.5vw, 40px) clamp(28px, 5vw, 80px);
  border-radius: var(--radius-lg);
  background: #1e1e1e;
}

.cs-about__group--wide { grid-column: span 2; }

.cs-about__group-title {
  font: var(--weight-bold) clamp(17px, 1.375vw, 22px) / 1.27 var(--font-sans);
  color: var(--text-on-image, #fff);
  margin-bottom: 12px;
}

.cs-about__list {
  font: var(--weight-regular) clamp(15px, 1.125vw, 18px) / 1.78 var(--font-sans);
  color: #c6c6c6;
}

/* Column-major so the six items deal 3 + 3 down each column as drawn, instead
   of the 2-4 a balanced multi-column run can land on once an item wraps.

   The columns share grid rows, so a wrapped item would open a blank line
   beside it in the neighbouring column. The longest item is held on one line
   and allowed to run into the card's padding instead — which is what the
   artboard does, setting that text box wider (370) than its track (323). */
.cs-about__list--split {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(3, auto);
  grid-auto-flow: column;
  column-gap: 20px;
}

.cs-about__list--split > li { white-space: nowrap; }

@media (max-width: 999px) {
  .cs-about {
    padding: clamp(56px, 9vw, 96px) var(--space-9);
  }

  /* No container gap: the laptop has to sit flush on the card, so the one gap
     that is wanted is carried by the copy's own margin instead. */
  .cs-about__stage {
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  /* Copy leads on a phone, and the laptop drops to sit directly on the card —
     echoing the overlap the two have on desktop. */
  .cs-about__intro {
    order: -1;
    margin-bottom: clamp(32px, 5vw, 56px);
  }

  .cs-about__media { top: 0; height: 100%; }

  .cs-about__laptop,
  .cs-about__intro,
  .cs-about__card {
    position: static;
    width: 100%;
  }

  .cs-about__card {
    grid-template-columns: 1fr;
    gap: clamp(24px, 4vw, 32px);
  }

  .cs-about__group--wide { grid-column: auto; }

  /* One continuous run of six, so the unheaded second column cannot strand
     itself as an orphan block. */
  .cs-about__list--split {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-auto-flow: row;
  }

  /* Full card width here, and no neighbouring column to keep level with. */
  .cs-about__list--split > li { white-space: normal; }
}

/* ==========================================================================
   User research — personas and journey maps

   A light surface between two dark ones, so unlike the hero and About this
   section uses the ordinary tokens rather than literals.
   ========================================================================== */

.cs-research {
  background: var(--surface-page);
  padding: clamp(64px, 7vw, 112px) clamp(24px, 10vw, 160px);
}

.cs-research__inner {
  max-width: 1280px;
  margin: 0 auto;
}

.cs-research__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-primary);
  margin-bottom: 8px;
}

.cs-research__lede {
  max-width: 605px;
  font: var(--weight-regular) clamp(16px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: var(--text-secondary);
}

/* ---- Carousel ---- */

.cs-personas {
  margin-top: clamp(40px, 4vw, 60px);
  padding-top: clamp(40px, 4vw, 60px);
  border-top: 1px solid var(--border-subtle);
}

.cs-personas__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-9);
  margin-bottom: clamp(28px, 3vw, 44px);
}

.cs-personas__eyebrow {
  font: var(--weight-medium) var(--space-7) / 1 var(--font-sans);
  color: var(--text-secondary);
}

.cs-personas__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* The same pill the design system already uses for filters and the hobby tags
   below, so the control reads as native rather than bolted on. Names rather
   than dots or arrows: with two entries an arrow pair would be two buttons
   doing the same thing, and a name says what you are switching to. */
.cs-chip {
  padding: var(--space-2h) var(--space-7);
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--control-fill);
  color: var(--control-text);
  font: var(--weight-medium) var(--space-7) / 24px var(--font-sans);
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-standard), color var(--duration-base) var(--ease-standard);
}

.cs-chip:hover { background: var(--border-strong); }

.cs-chip.is-selected {
  background: var(--control-fill-selected);
  color: var(--control-text-selected);
}

.cs-personas__viewport {
  overflow: hidden;
  transition: height 600ms var(--ease-standard);
}

.cs-personas__track {
  display: flex;
  align-items: flex-start;
  transition: transform 600ms var(--ease-standard);
}

.cs-persona {
  flex: 0 0 100%;
  min-width: 0;
}

/* ---- Persona ---- */

.cs-persona__head {
  display: flex;
  align-items: center;
  gap: var(--space-9);
  margin-bottom: clamp(24px, 2.5vw, 32px);
}

.cs-persona__avatar {
  flex-shrink: 0;
  width: 96px;
  height: 96px;
  border-radius: var(--radius-full);
  object-fit: cover;
}

.cs-persona__name {
  font: 600 clamp(30px, 2.75vw, 44px) / 1.23 var(--font-display);
  color: var(--text-primary);
}

.cs-persona__type {
  font: var(--weight-medium) clamp(17px, 1.25vw, 20px) / 1.2 var(--font-sans);
  color: var(--text-primary);
}

.cs-persona__body {
  display: grid;
  grid-template-columns: 413fr 412fr 412fr;
  gap: 20px;
  align-items: start;
}

.cs-persona__intro {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.cs-persona__place {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font: var(--weight-regular) var(--space-7) / 1.2 var(--font-sans);
  color: var(--nordra-dark-grey);
}

.cs-persona__place .icon { color: var(--icon-default); }

.cs-persona__desc {
  font: var(--weight-regular) var(--space-7) / 22px var(--font-sans);
  color: var(--text-secondary);
}

.cs-persona__quote {
  font: var(--weight-regular) var(--space-7) / 22px var(--font-sans);
  color: var(--text-primary);
}

.cs-persona__cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.cs-fact {
  padding: var(--space-7) var(--space-8);
  border-radius: var(--radius-sm);
  background: var(--nordra-light-grey);
}

.cs-fact__title {
  font: var(--weight-medium) 14px / 1.2 var(--font-sans);
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.cs-fact__text,
.cs-fact__list {
  font: var(--weight-regular) 14px / 20px var(--font-sans);
  color: var(--text-secondary);
}

.cs-fact__list {
  list-style: disc;
  padding-left: 21px;
}

.cs-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.cs-tag {
  display: inline-block;
  padding: var(--space-1) var(--space-6);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  font: var(--weight-regular) 14px / 24px var(--font-sans);
  color: var(--text-secondary);
}

/* ---- Journey map ---- */

.cs-journey__title {
  font: var(--type-section);
  color: var(--text-primary);
  margin: clamp(32px, 3.5vw, 52px) 0 var(--space-9);
}

/* The table keeps its full width and scrolls in place below it, the same way
   the marketing site handles its card rows on narrow screens. */
.cs-journey__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

/* Fixed layout so the five stages stay equal columns as drawn, rather than
   being auto-sized by whichever cell happens to hold the longest line. */
.cs-journey {
  width: 100%;
  min-width: 1060px;
  table-layout: fixed;
  border-collapse: collapse;
  text-align: left;
}

.cs-journey thead th {
  position: relative;
  padding: var(--space-8);
  background: var(--nordra-dark-grey);
  border-bottom: 1px solid var(--nordra-mid-grey);
  font: var(--weight-bold) 18px / 1.2 var(--font-sans);
  color: #fff;
  white-space: nowrap;
}

.cs-journey thead th:first-child { width: 160px; }

/* Stage-to-stage chevrons, straddling the boundary between header cells. */
.cs-journey__chevron {
  position: absolute;
  left: -12px;
  top: 50%;
  width: 24px;
  height: 24px;
  transform: translateY(-50%) rotate(-90deg);
  fill: currentColor;
}

.cs-journey tbody th,
.cs-journey tbody td {
  padding: var(--space-8);
  border: 1px solid var(--nordra-light-grey);
  vertical-align: top;
}

.cs-journey tbody th {
  width: 160px;
  background: var(--nordra-light-grey);
  font: var(--weight-bold) 18px / 1.2 var(--font-sans);
  color: var(--text-primary);
  white-space: nowrap;
}

.cs-journey tbody td {
  background: var(--surface-card);
  font: var(--weight-regular) var(--space-7) / 24px var(--font-sans);
  color: var(--text-secondary);
}

.cs-journey tbody td ul {
  list-style: disc;
  padding-left: var(--space-7);
}

@media (max-width: 1080px) {
  .cs-persona__body { grid-template-columns: 1fr 1fr; }
  .cs-persona__intro { grid-column: span 2; }
}

@media (max-width: 720px) {
  .cs-persona__body { grid-template-columns: 1fr; }
  .cs-persona__intro { grid-column: auto; }

  .cs-persona__head {
    gap: var(--space-7);
    align-items: flex-start;
  }

  .cs-persona__avatar { width: 64px; height: 64px; }
}

/* ==========================================================================
   Design language and branding
   ========================================================================== */

.cs-language {
  background: #0d0d0d;
  padding: clamp(64px, 7vw, 112px) clamp(24px, 10vw, 160px);
}

.cs-language__inner {
  max-width: 1280px;
  margin: 0 auto;
}

.cs-language__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-on-image, #fff);
  margin-bottom: 20px;
}

/* 537 and 492 of the artboard's 1280, with its 114px alley between them. */
.cs-language__intro {
  display: grid;
  grid-template-columns: 537fr 492fr;
  gap: clamp(28px, 8.9vw, 114px);
}

.cs-language__intro p {
  font: var(--weight-regular) clamp(15px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: var(--text-on-image, #fff);
}

.cs-swatches {
  display: flex;
  margin: clamp(40px, 5vw, 60px) 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.cs-swatches span {
  flex: 1 0 0;
  height: 16px;
}

/* ---- Showcase boxes ---- */

.cs-showcase {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

/* Fixed proportions so the placed contents scale with the box, and a clipped
   overflow so each one runs off the bottom edge as drawn. */
.cs-box {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--nordra-dark-grey);
}

.cs-box--imagery,
.cs-box--icons { aspect-ratio: 630 / 480; }

.cs-box--controls {
  grid-column: span 2;
  aspect-ratio: 1280 / 560;
}

.cs-box__title {
  position: absolute;
  top: 12.5%;
  left: 0;
  right: 0;
  font: var(--weight-medium) clamp(17px, 1.875vw, 24px) / 1.2 var(--font-sans);
  color: var(--text-on-image, #fff);
  text-align: center;
}

.cs-box--controls .cs-box__title { top: 10.7%; }

.cs-box__stage {
  position: absolute;
  inset: 0;
}

.cs-box__phone {
  position: absolute;
  top: 31%;
  width: 38.25%;
  height: auto;
}

.cs-box__phone--a { left: 7.8%; }
.cs-box__phone--b { left: 53.7%; }

.cs-control {
  position: absolute;
  top: 26.6%;
  height: auto;
}

/* The artboard hangs this panel off the left edge, and the export is already
   clipped at that edge — so it sits flush at 0, not at a negative offset,
   which would crop the same 68px a second time. */
.cs-control--a { left: 0; width: 30.9%; }
.cs-control--b { left: 32.8%; width: 36.25%; }
.cs-control--c { left: 70.9%; width: 29.1%; }

/* ---- Icon grid ---- */

.cs-icons {
  position: absolute;
  top: 31%;
  left: 50%;
  width: 402.5px;
  max-width: 78%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 30px;
}

.cs-icons__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  color: var(--text-on-image, #fff);
}

.cs-icons__row svg {
  fill: currentColor;
  flex-shrink: 0;
}

.cs-icons__row--large { gap: 31.25px; }
.cs-icons__row--large svg { width: 55px; height: 55px; }

.cs-icons__row--small { gap: 30px; }
.cs-icons__row--small svg { width: 30px; height: 30px; }

/* Close is drawn at half size in its slot — its glyph fills the artboard
   frame, where the others are inset inside theirs, so matching their box would
   make it read twice as heavy as its neighbours. */
.cs-icons__row--small svg.cs-icons__close {
  width: 15px;
  height: 15px;
  margin: 0 7.5px;
}

@media (max-width: 900px) {
  .cs-language__intro { grid-template-columns: 1fr; }

  .cs-showcase { grid-template-columns: 1fr; }

  .cs-box--imagery,
  .cs-box--icons { aspect-ratio: 630 / 520; }

  .cs-box--controls {
    grid-column: auto;
    aspect-ratio: 860 / 560;
  }

  /* Two of the three panels would be off-screen at this width, so the row
     slides instead of shrinking the controls past legibility. */
  .cs-box__stage--controls {
    inset: 26.6% 0 0 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }

  .cs-control {
    position: relative;
    top: 0;
    left: 0;
    flex: 0 0 auto;
  }

  .cs-box__stage--controls {
    display: flex;
    gap: 24px;
    padding-left: 24px;
    align-items: flex-start;
  }

  .cs-control--a,
  .cs-control--b,
  .cs-control--c { width: auto; height: 100%; }
}

/* At phone width the artboard's placed compositions stop working: both boxes
   hold a fixed aspect ratio, and the contents no longer fit the box that ratio
   produces. The icon set needed 262px in a 248px box and was being sliced
   through its last row; the control panels were scaled to the box height and
   left most of their width off the edge.

   So below this width both boxes stop reproducing the artboard's proportions
   and lay out in flow, sized by what is in them. */
@media (max-width: 560px) {
  .cs-box--icons,
  .cs-box--controls {
    aspect-ratio: auto;
  }

  .cs-box--icons .cs-box__title,
  .cs-box--controls .cs-box__title {
    position: static;
    padding: 28px 20px 0;
  }

  /* ---- Icon grid ---- */

  .cs-box--icons { padding-bottom: 32px; }

  /* A real grid rather than wrapped flex rows: equal columns keep the five
     large marks and the fourteen small ones on their own tidy rasters instead
     of leaving an orphan at the end of each wrap. */
  .cs-icons {
    position: static;
    transform: none;
    width: auto;
    max-width: none;
    margin-top: 26px;
    padding: 0 18px;
    gap: 20px;
  }

  .cs-icons__row {
    display: grid;
    align-items: center;
    justify-items: center;
    gap: 18px 8px;
  }

  .cs-icons__row--large { grid-template-columns: repeat(5, 1fr); }
  .cs-icons__row--large svg { width: 36px; height: 36px; }

  .cs-icons__row--small { grid-template-columns: repeat(7, 1fr); }
  .cs-icons__row--small svg { width: 22px; height: 22px; }

  /* The half-size rule still applies, but the grid cell does the spacing now,
     so the margin that used to hold it off its neighbours has to go. */
  .cs-icons__row--small svg.cs-icons__close {
    width: 13px;
    height: 13px;
    margin: 0;
  }

  /* ---- Control panels ---- */

  /* Stacked at full width instead of scrolled at full height. Each panel gets
     the whole measure — which is what makes them legible at this size — and
     the box grows to hold all three rather than hiding two off the edge.

     Edge to edge, with no gutter of its own: these exports are clipped at the
     artboard's left edge, so a panel floating inside padding reads as a broken
     image, where one running into the corner reads as the bleed it is. */
  .cs-box__stage--controls {
    position: static;
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-top: 24px;
    padding: 0;
    overflow: visible;
  }

  .cs-control--a,
  .cs-control--b,
  .cs-control--c {
    width: 100%;
    height: auto;
  }
}

/* ==========================================================================
   The search experience
   ========================================================================== */

.cs-search {
  overflow: hidden;
  background: #0d0d0d;
  padding: clamp(64px, 7vw, 112px) 0;
}

.cs-search__inner {
  max-width: 1600px;
  margin: 0 auto;
  padding-inline: clamp(24px, 10vw, 160px);
}

.cs-search__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-on-image, #fff);
  margin-bottom: 8px;
}

.cs-search__lede {
  max-width: 631px;
  font: var(--weight-regular) clamp(15px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: var(--text-on-image, #fff);
}

/* Capped at the artboard's own width so it is never upscaled past 1:1, and
   scrollable below that rather than shrunk until the labels are unreadable. */
.cs-search__flow {
  margin-top: clamp(40px, 5vw, 72px);
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.cs-search__flow img {
  display: block;
  width: 100%;
  min-width: 900px;
  max-width: 1600px;
  height: auto;
  margin-inline: auto;
}

/* The artboard runs the first row up over the foot of the flow chart, where
   its connector lines trail down behind the phones — a 198px overlap on the
   645-tall chart, i.e. its empty bottom third.

   A margin percentage resolves against the container's WIDTH, so this is that
   overlap expressed against width: 198 / 1600 = 12.4%. Because the chart is
   width-driven it stays in proportion as the section scales. */
.cs-search__rows {
  display: flex;
  flex-direction: column;
  gap: 30px;
  margin-top: -12.4%;
}

.cs-search__row {
  overflow: hidden;
}

.cs-search__track {
  display: flex;
  gap: 30px;
  width: max-content;
  will-change: transform;
}

.cs-search__track img {
  display: block;
  flex-shrink: 0;
  width: clamp(190px, 18.85vw, 304px);
  height: auto;
}

/* Below this the chart stops scaling with the section — it is pinned at its
   900px min-width inside the scroller — so the overlap has to be a fixed
   figure rather than a share of the viewport: 900 * (645/1600) = 363px tall,
   of which the empty foot is about 110px. */
@media (max-width: 720px) {
  .cs-search__rows { margin-top: -110px; gap: 20px; }
  .cs-search__track { gap: 20px; }
}

/* ==========================================================================
   Working prototype
   ========================================================================== */

/* No horizontal padding on the section: the prototype below runs the full
   width of the page, so the gutter lives on the header's own container. */
.cs-prototype {
  background: var(--surface-page);
  padding: clamp(64px, 7vw, 112px) 0 0;
}

/* 1600 minus the same gutter the other sections use, which lands the header on
   the same 1280 measure as the rest of the case study. */
.cs-prototype__inner {
  max-width: 1600px;
  margin: 0 auto;
  padding-inline: clamp(24px, 10vw, 160px);
}

.cs-prototype__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-primary);
  margin-bottom: 8px;
}

.cs-prototype__lede {
  max-width: 720px;
  font: var(--weight-regular) clamp(16px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: var(--text-secondary);
}

/* A hairline is the only thing separating the case study from the prototype,
   since both sit on the same page surface.

   It carries its own ground rather than borrowing the section's: this element
   is the theme scope, so in dark mode .page below it goes dark while the
   section stays light, and .page is capped at --page-max — a wider viewport
   would show light gutters down either side of a dark prototype. Painting the
   same token here makes the whole band follow the toggle. */
.cs-prototype__live {
  margin-top: clamp(40px, 5vw, 64px);
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-page);
  /* Re-stated, not inherited. Custom properties inherit, but `color` does not
     re-resolve on the way down — body sets it from --text-primary outside this
     scope, so anything inside relying on the inherited baseline would keep the
     light ink and go dark-on-dark the moment the toggle flips. */
  color: var(--text-primary);
}

/* ==========================================================================
   Conclusion — closing statement
   ========================================================================== */

/* Dark end to end like the hero and the design-language section, so the ground
   is literal rather than token-driven: the photograph behind it does not invert
   with the theme, and neither may what sits on it.

   Padding follows the artboard's 200 / 160 / 150 against its 1600 width. */
.cs-conclusion {
  position: relative;
  overflow: hidden;
  background: #0d0d0d;
  padding: clamp(96px, 12.5vw, 200px) clamp(24px, 10vw, 160px) clamp(72px, 9.375vw, 150px);
}

/* Bottom-anchored and wider than the section, so it bleeds 360 of the
   artboard's 1600 past the right edge and is cropped at the top by the section
   — 923 tall against a 634 frame. Percentages keep both as the page scales.

   The fades sit inside the 30% layer exactly as they do in the source, so they
   damp the photograph rather than painting solid black over it: the two
   linears pull the top and bottom edges into the ground, and the radial closes
   the left side down behind the copy. */
.cs-conclusion__media {
  position: absolute;
  right: -22.5%;
  bottom: 0;
  width: 102.56%;
  aspect-ratio: 1641 / 923;
  opacity: 0.3;
  background:
    linear-gradient(0deg, rgba(13, 13, 13, 0) 47.779%, #0d0d0d 71.506%),
    linear-gradient(180deg, rgba(13, 13, 13, 0) 68.852%, #0d0d0d 91.116%),
    radial-gradient(ellipse 84% 234% at 50% 50%, rgba(13, 13, 13, 0) 0%, rgba(13, 13, 13, 1) 100%),
    url("../img/conclusion-fjord.jpg") center / cover no-repeat;
}

/* 1600 minus the gutter above, the same 1280 measure the rest of the case
   study sits on. Stacked over the media rather than under it. */
.cs-conclusion__inner {
  position: relative;
  max-width: 1280px;
  margin: 0 auto;
}

/* 687 of the artboard's 1280, which is what leaves the right of the frame to
   the fjord. It shrinks with the container below that width. */
.cs-conclusion__heading {
  max-width: 687px;
}

.cs-conclusion__title {
  font: var(--weight-bold) clamp(34px, 3.75vw, 60px) / 1.2 var(--font-display);
  color: var(--text-on-image, #fff);
  margin-bottom: 8px;
}

/* Literal rather than --nordra-light-mid-grey, which flips to #8a8a8a in dark
   and would sink this copy into a ground that never inverts with it. */
.cs-conclusion__lede {
  font: var(--weight-regular) clamp(15px, 1.125vw, 18px) / var(--line-height-body) var(--font-sans);
  color: #c6c6c6;
}

/* The artboard's bleed is a fraction of a 1600 frame: carried to a phone it
   leaves the photograph as a 200px strip hanging off the right corner rather
   than a backdrop. Narrower screens take a wider, less displaced crop, so it
   still spans the copy while keeping the right-hand bleed. */
@media (max-width: 720px) {
  .cs-conclusion__media {
    right: -18%;
    width: 140%;
  }
}

/* ==========================================================================
   Demo lightbox — the homepage at phone width
   ========================================================================== */

/* Fixed to the viewport, which is why the panel sits at the end of the body:
   a filter or transform on any ancestor would make that ancestor its
   containing block instead. */
/* The top padding is what keeps the frame clear of the close button in the
   corner above it. On a phone the viewport is barely larger than the frame, so
   without a reserved band there the button would land on the phone's own
   top-right corner — next to the interface's own close. */
.cs-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(64px, 8vh, 88px) clamp(16px, 4vw, 48px) clamp(16px, 4vw, 48px);
}

.cs-lightbox[hidden] { display: none; }

.cs-lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 8, 8, 0.82);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 240ms var(--ease-standard, ease);
}

/* 390 by 844 is a phone, and the point of the frame: the homepage inside is
   responsive, so it is this width — not the viewport's — that puts it below the
   860px breakpoint where the collapsed search lives.

   Both axes give way to the screen it is being shown on before the frame
   overflows it, and the percentages resolve because the flex parent above is
   fixed to the viewport and so has a definite box on both. */
.cs-lightbox__dialog {
  position: relative;
  display: flex;
  width: min(390px, 100%);
  height: min(844px, 100%);
  opacity: 0;
  transform: translateY(12px) scale(0.98);
  transition:
    opacity 240ms var(--ease-standard, ease),
    transform 240ms var(--ease-standard, ease);
}

.cs-lightbox.is-open .cs-lightbox__backdrop { opacity: 1; }

.cs-lightbox.is-open .cs-lightbox__dialog {
  opacity: 1;
  transform: none;
}

/* A bare glyph in the corner of the viewport rather than a chip on the frame:
   the phone is the thing being looked at, and a filled control against its edge
   competes with the interface inside it. Sized to a 44px target even though the
   mark itself is 20, so it stays tappable.

   Positioned against .cs-lightbox, which is fixed to the viewport — the corner
   it sits in is the screen's, not the frame's. */
.cs-lightbox__close {
  position: absolute;
  top: clamp(10px, 1.5vw, 20px);
  right: clamp(10px, 1.5vw, 20px);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm, 8px);
  background: none;
  /* White on the darkened page, and theme-independent for the same reason the
     rest of this overlay is: what is behind it is a photograph. */
  color: var(--on-image-control-bg, #fff);
  cursor: pointer;
  opacity: 0;
  transition: opacity 240ms var(--ease-standard, ease);
}

.cs-lightbox__close .icon { --icon-size: 20px; }

.cs-lightbox.is-open .cs-lightbox__close { opacity: 0.7; }

.cs-lightbox.is-open .cs-lightbox__close:hover,
.cs-lightbox__close:focus-visible { opacity: 1; }

.cs-lightbox__close:focus-visible {
  outline: 2px solid var(--on-image-control-bg, #fff);
  outline-offset: 2px;
}

.cs-lightbox__phone {
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: clamp(10px, 2vw, 18px);
  background: #0d0d0d;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
}

.cs-lightbox__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* A landscape phone has no room for an 844px frame; the height above already
   gives way to it, and the corner softens to match the smaller box. */
@media (max-height: 560px) {
  .cs-lightbox__phone { border-radius: 8px; }
}

/* ---- Entrance: staggered slide-up fade ---- */

/* Scoped to .js (set by an inline script in the head) so the hidden starting
   state only ever applies where there is script running to undo it. */
.js .cs-reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 900ms var(--ease-standard, ease), transform 900ms var(--ease-standard, ease);
  transition-delay: var(--reveal-delay, 0ms);
}

.js .cs-reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* On a phone the carousel is the thing worth looking at, so it takes a larger
   share of a shorter frame: the section drops to 80vh, the phones come up a
   quarter, and the logo gives back some of the room that costs. */
@media (max-width: 720px) {
  .cs-hero {
    height: 80vh;
    min-height: 560px;
  }

  .cs-hero__logo { top: 13%; width: clamp(150px, 46vw, 240px); }

  /* The band moves, not the carousel inside it. Since the stage was introduced
     the carousel is inset:0 within it, so a top offset here would be applied a
     second time and push the phones most of the way off the section. */
  .cs-hero__stage { top: 46%; }

  /* Flatter here: the arc is a fraction of phone height, so the taller mobile
     phones would otherwise swing further, and across a much narrower screen —
     the same curve reads far steeper. */
  .cs-hero__carousel { --arc-amplitude: 0.1; }

  /* Taller than the desktop clamp and deliberately running past the bottom of
     the section: the fade below needs a good length of phone to dissolve, and
     cutting one off mid-screen is the thing it exists to avoid. */
  .cs-mockup { height: clamp(320px, 56vh, 560px); }

  /* Above the phones rather than over them. bottom:100% puts its lower edge on
     the band's top edge, so only the horizontal half of the centring transform
     is still wanted — hence --demo-shift-y going to 0 rather than a second
     transform being written out here. */
  .cs-hero__demo {
    --demo-shift-y: 0;
    top: auto;
    bottom: calc(100% + 18px);
  }

  /* Dissolves the bottom of the phones into the section ground instead of
     letting the hero's overflow slice them off in a straight line. Over the
     carousel but under the button, which sits outside this box and carries its
     own z-index. */
  .cs-hero__stage::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(
      180deg,
      rgba(13, 13, 13, 0) 40%,
      rgba(13, 13, 13, 0.85) 76%,
      #0d0d0d 92%
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .cs-hero__media { transform: none; }
}
