/* ==========================================================================
   The Work experience
   --------------------------------------------------------------------------
   Spatial logic, desktop:

       left     filter, as an editorial index
       centre   the project list - the primary content
       behind   the active project's image, full bleed

   The whole composition is locked to the viewport and the list scrolls inside
   itself. That is what keeps the active project's metadata and its VIEW
   PROJECT action on screen instead of below a page fold, and it is why the
   image can stay bound to the active project no matter how far down the list
   you travel.

   The single-column arrangement for narrow viewports is in responsive.css.
   ========================================================================== */

.nsn-work {
	position: relative;
	height: 100svh;
	overflow: hidden;
	display: flex;
	flex-direction: column;

	/*
	 * This page's own pacing, kept separate from the global --nsn-dur and
	 * --nsn-ease so that changing the index cannot reach any other route.
	 *
	 * Both values are the reference's, measured off its own list rather than
	 * chosen. Its rows reveal their active state on cubic-bezier(0.165, 0.84,
	 * 0.44, 1) - the easeOutQuart it declares as a variable and uses for the
	 * type throughout - and the reveal was sampled frame by frame at 1440x900:
	 * 41.1% of the way over at 106ms, 68.4% at 231ms, 85.7% at 365ms, 94.5% at
	 * 497ms, 98.5% at 631ms, arriving just before a second. Fitting that curve
	 * to those ten samples gives 935ms to a mean error of 1.2 percentage
	 * points, and 855ms if the start frame is allowed to float by a frame or
	 * two. 0.9s sits in the middle of that range and is the likelier authored
	 * value; from 430ms onward the two curves then agree to within 0.2 of a
	 * point. What cannot be pinned down more finely is the exact frame the
	 * reference starts on, which is why this is a range and not a number.
	 *
	 * What this replaced was 0.56s on cubic-bezier(0.25, 0.46, 0.45, 0.94), a
	 * quadratic in-out. Its opening was close, but it ran up to five points
	 * ahead through the middle and then stopped dead at 569ms, while the
	 * reference was still only 97% of the way there and had another 400ms of
	 * settle to go. The shortfall was the tail, not the start.
	 *
	 * One token for the whole row, so the number, the title, the subtitle and
	 * the row's own shift all arrive together - a row changes at one pace or
	 * it reads as several things moving separately.
	 */
	--nsn-work-dur: 0.9s;
	--nsn-work-ease: cubic-bezier(0.165, 0.84, 0.44, 1);

	/*
	 * The title's own reveal, which is shorter than the row's pace.
	 *
	 * Measured on the reference alongside the values above: its titles carry
	 * a 0.6s transition on the same easeOutQuart the list travels on. The two
	 * are different jobs. The row's 0.9s is a distance being covered, and it
	 * has to last as long as the travel does or the list arrives before its
	 * emphasis catches up. The fill crosses one line of type, so the same
	 * nine tenths of a second on it reads as a wash rather than an answer -
	 * the title is still filling when the eye has already moved on.
	 *
	 * The number, the subtitle and the distance falloff keep the row pace.
	 * They are supporting marks that belong to the movement; the title is
	 * what answers the pointer, and it is allowed to answer first.
	 */
	--nsn-work-title-dur: 0.6s;

	/*
	 * The one accent on an otherwise ink-and-bone site, and the reference's
	 * own: rgb(255, 101, 66), declared there as --red. Scoped to this page
	 * like the pacing above, so introducing it cannot reach another route.
	 */
	--nsn-work-accent: #ff6542;
}

/* The page itself never scrolls on this route; only the list does. */
body.nsn-is-work {
	overflow: hidden;
}

/* --------------------------------------------------------------------------
   Layered background stage
   --------------------------------------------------------------------------
   Two <img> layers rather than one background-image. The idle layer holds the
   incoming bitmap while it decodes, then the two are swapped. Because both
   images are in the DOM and decoded, a fast hover sequence never shows an
   empty or half-painted frame.

   The change of project is a cut, not a fade, and the movement is a zoom -
   which is what the reference does, read off its own computed styles:

     opacity     switched between the layers in one frame; the layer declares
                 transition-duration: 0s, so nothing is interpolated
     transform   scale(1.2) -> scale(1) over 1s on easeOutQuart, run as a
                 keyframe animation on the active class with fill forwards

   Both properties are owned here, in CSS, and by one rule each. work.js only
   moves the is-active class between the two layers; it sets no opacity, no
   z-index and no transform on them, so there is no second system to disagree
   with these keyframes about what the layer's transform should be.

   There is deliberately no mask and no wipe, and nothing translates: a scale
   about the centre is the whole of the movement.

   Because it is transform and opacity only, it composites cleanly - no
   per-frame repaint, so it holds 60fps even under software rasterisation.
   -------------------------------------------------------------------------- */
.nsn-stage {
	position: fixed;
	inset: 0;
	z-index: 0;
	overflow: hidden;
	background-color: var(--nsn-ink);
	/* The stage is decorative; the list carries the meaning. */
	pointer-events: none;
}

.nsn-stage__viewport {
	position: absolute;
	inset: 0;
}

.nsn-stage__layer {
	position: absolute;
	inset: 0;

	/*
	 * Under the active layer, always. Held here rather than written from the
	 * script, so the two layers cannot end up with a stale inline z-index that
	 * outranks the class and pins the new picture behind the old one.
	 */
	z-index: 1;

	opacity: 0;
	/* Centre-anchored: the zoom relaxes inward rather than drifting. */
	transform-origin: 50% 50%;
	will-change: opacity, transform;
}

/*
 * The whole transition.
 *
 * opacity and z-index take effect on the frame the class lands, so the new
 * picture is complete and on top before the old one is uncovered - there is
 * no frame in which the stage is blank, doubled or half covered. The zoom
 * then runs on its own, over the second that follows.
 *
 * The values are the reference's, taken from its computed styles: 1s,
 * cubic-bezier(0.165, 0.84, 0.44, 1) - easeOutQuart - once, forwards, no
 * delay, from scale(1.2) to scale(1).
 */
.nsn-stage__layer.is-active {
	z-index: 2;
	opacity: 1;
	animation: nsn-stage-zoom-in 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0s 1 normal forwards;
}

@keyframes nsn-stage-zoom-in {
	0% {
		transform: scale(1.2);
	}

	100% {
		transform: scale(1);
	}
}

.nsn-stage__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center 42%;
	/*
	 * The stage is an environment for the type, not a hero image, and it has
	 * to hold ANY photograph the client supplies - including a bright, busy
	 * one. Taming the source here means the contrast treatment does not have
	 * to be re-tuned per image, and it keeps the veil above light enough that
	 * the photograph still reads.
	 */
	filter: brightness(0.44) saturate(0.76);
}

/*
 * Three stacked veils, each doing one job:
 *   radial      seats the centred list without drawing a visible panel edge
 *   horizontal  holds the left-hand filter
 *   vertical    keeps the header and the detail row legible at any aspect
 * Together they land around 45% over the centre, which keeps large bone type
 * above 4.5:1 even on the lightest image in the set.
 */
.nsn-stage__scrim {
	position: absolute;
	inset: 0;
	background-image:
		radial-gradient(
			ellipse 66% 78% at 50% 48%,
			rgba(15, 15, 14, 0.42) 0%,
			rgba(15, 15, 14, 0.14) 70%,
			rgba(15, 15, 14, 0) 100%
		),
		linear-gradient(
			100deg,
			rgba(15, 15, 14, 0.80) 0%,
			rgba(15, 15, 14, 0.58) 30%,
			rgba(15, 15, 14, 0.34) 66%,
			rgba(15, 15, 14, 0.44) 100%
		),
		linear-gradient(
			to bottom,
			rgba(15, 15, 14, 0.78) 0%,
			rgba(15, 15, 14, 0.26) 20%,
			rgba(15, 15, 14, 0.3) 58%,
			rgba(15, 15, 14, 0.8) 100%
		);
}

.nsn-stage__grain {
	position: absolute;
	inset: -50%;
	opacity: 0.05;
	mix-blend-mode: overlay;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
.nsn-work__inner {
	position: relative;
	z-index: 2;
	flex: 1;
	min-height: 0;
	width: 100%;
	max-width: var(--nsn-maxw);
	margin: 0 auto;
	/*
	 * The list is a field, not a panel.
	 *
	 * This used to reserve the header's full height plus a step above the
	 * list, and the bottom held an action bar; between them the index only got
	 * 689px of a 900px screen and began well below the navigation. The
	 * reference runs its titles the whole height of the window and lets the
	 * top one pass behind the nav, which is what makes it read as type filling
	 * a page.
	 *
	 * What is reserved now is the header and nothing else. It used to be the
	 * header plus a step, and briefly less than the header, which put the first
	 * title 24px up behind the fade - readable in the sense that the letters
	 * were there, but sitting under the navigation. Clearing exactly the header
	 * leaves the first title a comfortable margin below it at every width and
	 * still hands the list the step the old rule spent, on top of the height
	 * freed by dropping the action bar.
	 */
	padding: var(--nsn-header-h) var(--nsn-gutter) var(--nsn-space-3);
	display: flex;
	flex-direction: column;
}

.nsn-work__body {
	position: relative;
	flex: 1;
	min-height: 0;
	display: flex;
	/* Shared by the aside and the centre column so they cannot drift apart. */
	/* Floor is set by the longest category label, which is a single
	   unbreakable word (FAMILIEOPSTELLINGEN), which needs ~177px beside its
	   count at the reduced tracking used below 1300px. Above that, 15vw has
	   already overtaken the requirement. */
	--nsn-aside-w: clamp(188px, 16vw, 236px);
	--nsn-aside-gap: clamp(36px, 3vw, 64px);
}


/*
 * The aside is taken out of flow so the project list is centred on the page,
 * not merely in the space left over beside the filter.
 */
.nsn-work__aside {
	position: absolute;

	/*
	 * Out to the edge of the page, not the edge of the text column.
	 *
	 * The gutter is what holds the reading measure on every other route; here
	 * it only pushed the index inward until it crowded the titles. Pulling the
	 * aside back out by that much stands it off on its own, and because the
	 * centre column is sized from the viewport and centred in what is left,
	 * the titles keep the middle of the screen rather than sliding with it.
	 *
	 * Desktop only - below the breakpoint the aside returns to the flow above
	 * the list and this offset would drag it off-screen.
	 */
	left: 0;
	top: 0;
	bottom: 0;
	width: var(--nsn-aside-w);
	display: flex;
	flex-direction: column;
	gap: var(--nsn-space-3);
}

/*
 * Desktop composition: the filter stands off at the page edge and the list
 * takes the height the action bar used to hold.
 */
@media (min-width: 901px) {
	.nsn-work__aside {
		margin-left: calc((var(--nsn-gutter) - var(--nsn-space-3)) * -1);

		/*
		 * The list may start under the header; this column may not. Its first
		 * line is the page title, and the site wordmark is sitting right there.
		 * The inner no longer reserves the header height, so the offset it used
		 * to get from that padding is put back here - and only here, so the
		 * titles keep the height that was freed.
		 */
		top: calc(var(--nsn-header-h) + var(--nsn-space-2) - var(--nsn-space-4));
	}
}

.nsn-work__title {
	font-size: var(--nsn-step-xs);
	line-height: 1;
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--nsn-bone);
	margin-bottom: 0.75rem;
}

.nsn-work__intro,
.nsn-work__intro p {
	font-size: var(--nsn-step-xs);
	line-height: 1.5;
	color: var(--nsn-bone-46);
}

/*
 * Centred on the page, not merely in the space left over. The second term
 * reserves the aside's width on BOTH sides, which is what keeps the column
 * centred while guaranteeing it can never run into the filter at narrower
 * desktop widths.
 */
.nsn-work__center {
	width: min(44rem, 100% - 2 * (var(--nsn-aside-w) + var(--nsn-aside-gap)));
	margin-inline: auto;
	display: flex;
	flex-direction: column;
	min-height: 0;
}

/*
 * The list scrolls inside itself. The mask fades the first and last rows
 * rather than showing a cut edge, which also signals that there is more.
 */
.nsn-work__scroll {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	/* Momentum scrolling on iOS; without it the region feels notably un-native. */
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
	/*
	 * Single source of truth for the fade: work.js reads --nsn-scroll-fade to
	 * decide how far below the top edge the first row should be anchored on
	 * arrival, so the active row is never sitting inside the fade.
	 */
	--nsn-scroll-fade: 28px;
	-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--nsn-scroll-fade), #000 calc(100% - 34px), transparent 100%);
	mask-image: linear-gradient(to bottom, transparent 0, #000 var(--nsn-scroll-fade), #000 calc(100% - 34px), transparent 100%);
	padding-block: 6px 10px;
}

.nsn-work__scroll::-webkit-scrollbar {
	display: none;
}

/*
 * Where the pointer drives the list, the reader does not.
 *
 * On a fine pointer at desktop the band's vertical position is a mapping of
 * where the pointer is standing (see work.js), so a wheel turned over it has
 * nothing to say: two things writing one position is the fight this replaces.
 * overflow: hidden is how that is said - the region is no longer scrollable by
 * the wheel, a drag or a scrollbar, while remaining a real scroll container
 * whose scrollTop script can set and into which the browser still brings
 * keyboard focus.
 *
 * The condition is the same one focalEnabled() tests, term for term. Under
 * reduced motion, on a coarse pointer, or below the breakpoint no mapping runs
 * and the declaration above stands: the band is an ordinary scroller and the
 * reader moves it themselves.
 */
@media (min-width: 901px) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
	.nsn-work__scroll {
		overflow: hidden;
	}
}

/* --------------------------------------------------------------------------
   Filter
   --------------------------------------------------------------------------
   A vertical index, not a row of pills. The active entry is marked by a rule
   that grows from the left, so the state reads even in peripheral vision.
   -------------------------------------------------------------------------- */
.nsn-filters {
	border-top: 1px solid var(--nsn-rule);
	padding-top: 0.9rem;
}

.nsn-filters__list {
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
}

.nsn-filter {
	position: relative;
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	width: 100%;
	padding: 0.4rem 0 0.4rem 0;
	font-size: var(--nsn-step-xs);
	letter-spacing: 0.14em;
	text-transform: uppercase;
	text-align: left;
	color: var(--nsn-bone-46);
	transition: color var(--nsn-dur-fast) var(--nsn-ease), padding-left var(--nsn-dur) var(--nsn-ease);
}

.nsn-filter::before {
	content: '';
	position: absolute;
	left: 0;
	top: 50%;
	width: 14px;
	height: 1px;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform var(--nsn-dur) var(--nsn-ease);
}

.nsn-filter:hover {
	color: var(--nsn-bone-72);
}

.nsn-filter.is-active {
	color: var(--nsn-bone);
	padding-left: 22px;
}

.nsn-filter.is-active::before {
	transform: scaleX(1);
}

.nsn-filter__label {
	flex: 1;
	min-width: 0;
	/* Guard, in case the client adds a longer category than this one. */
	overflow-wrap: anywhere;
}

.nsn-filter__count {
	font-size: 0.72em;
	letter-spacing: 0.06em;
	color: var(--nsn-bone-28);
	transform: translateY(-0.5em);
}

.nsn-filter.is-active .nsn-filter__count {
	color: var(--nsn-bone-46);
}

/* --------------------------------------------------------------------------
   Project list
   --------------------------------------------------------------------------
   The <a> is the interactive element, so hover, focus, Enter and click are
   handled by the browser. State changes are class-driven CSS transitions: a
   class toggle cannot stack the way repeated tweens can, which is what keeps
   fast mouse travel across the list completely flicker-free. GSAP is reserved
   for the stage transition and the filter stagger, where it earns its keep.

   No cards, no per-item borders, no grid - just type.
   -------------------------------------------------------------------------- */
.nsn-list {
	display: flex;
	flex-direction: column;
}

.nsn-list__link {
	display: grid;
	grid-template-columns: 2.75rem minmax(0, 1fr);
	align-items: baseline;
	gap: 0 clamp(0.75rem, 1.6vw, 1.5rem);
	padding: clamp(0.5rem, 1vw, 0.8rem) 0;
	transition: transform var(--nsn-work-dur) var(--nsn-work-ease);
}

.nsn-list__index {
	font-size: var(--nsn-step-xs);
	letter-spacing: 0.12em;
	color: var(--nsn-bone-28);
	/* Was --nsn-dur-fast (0.28s), which finished while the title beside it
	   was still changing. One row, one pace. */
	transition: color var(--nsn-work-dur) var(--nsn-work-ease);
}

.nsn-list__text {
	/* Stacked, not inline-wrapped: a flex row would put short subtitles beside
	   the title and long ones underneath, so the list would set differently
	   row to row. */
	display: block;
	min-width: 0;
}

/*
 * The project titles carry the page, so they are set at editorial size rather
 * than list size. The middle term is a line through two measured points -
 * about 50px at 1024 and about 63px at 1440 - with a ceiling so a very wide
 * screen does not run away with it.
 */
.nsn-list__title {
	display: block;
	font-size: clamp(2.6rem, 1.3rem + 2.9vw, 4.25rem);

	/* Tighter than before: at this size the old leading opened gaps between
	   the title and its subtitle. */
	line-height: 0.98;
	letter-spacing: -0.03em;
	text-transform: uppercase;

	/*
	 * Barely there until it is asked for.
	 *
	 * The fade is carried by the colour's own alpha rather than by opacity,
	 * which is what lets one transition do both halves of the change: the
	 * title comes up out of the ground and takes the accent in a single
	 * movement, instead of a fade and a hue arriving separately. It also
	 * leaves the row's opacity alone, which belongs to the distance falloff.
	 *
	 * 0.14 rather than the reference's 0.10 because the falloff multiplies
	 * it - rows two or more away from the pointer sit at 0.88 - so the
	 * furthest titles land at 0.123 and the nearest at 0.132, either side of
	 * the reference's flat tenth. Any lower and these photographs, which are
	 * brighter and busier than the reference's, swallow the type entirely.
	 */
	color: var(--nsn-bone-14);

	/* The containing block for the coral layer laid over it. */
	position: relative;
}

/*
 * The coral layer.
 *
 * The same title, the same box, sitting exactly on top of the faded copy and
 * clipped away to nothing until the row is active. clip-path cuts the painted
 * glyphs themselves, so the fill runs up through the letterforms - a J filling
 * from its hook, an O from its foot - rather than a coloured rectangle being
 * drawn across them.
 *
 * inset(100% 0 0) hides it by pushing the top edge of the visible region all
 * the way down to the bottom of the box; taking that inset to 0% walks the
 * edge back up, which is the whole of the bottom-to-top reveal. The clip is
 * the only thing that changes, so the type cannot move, resize or re-wrap, and
 * the layer is out of flow, so the row's height and width are the same with it
 * as without.
 *
 * Not hit-testable and not selectable: it is a duplicate of text that is
 * already there, and a visitor dragging across a title should get the title
 * once rather than twice.
 */
.nsn-list__fill {
	position: absolute;
	inset: 0;
	color: var(--nsn-work-accent);
	clip-path: inset(100% 0 0);
	transition: clip-path var(--nsn-work-title-dur) var(--nsn-work-ease);
	pointer-events: none;
	user-select: none;
	-webkit-user-select: none;
}

.nsn-list__subtitle {
	display: block;
	margin-top: 0.15em;
	font-size: var(--nsn-step-xs);
	letter-spacing: 0.04em;
	color: var(--nsn-bone-28);
	transition: color var(--nsn-work-dur) var(--nsn-work-ease);
}

/* The per-row meta belongs to the detail block on this composition. */
.nsn-list__meta {
	display: none;
}

/* Active state - the single source of emphasis. */
.nsn-list__item.is-active .nsn-list__link {
	transform: translateX(clamp(0.4rem, 0.9vw, 1rem));
}

/*
 * The title under the pointer, and only it.
 *
 * is-active is the hover on this page: the pointer mapping in work.js re-reads
 * which row it is over on every frame and moves the class there, so this rule
 * is the hover state without a :hover selector - which is also why it is right
 * on a tap and on keyboard focus, where a :hover rule would do nothing. The
 * same class leaving is the mouse leaving, and the reveal runs back down.
 *
 * The faded copy underneath is left exactly as it is. Only the clip moves.
 */
.nsn-list__item.is-active .nsn-list__fill {
	clip-path: inset(0% 0 0);
}

/*
 * The arrival.
 *
 * While the index is coming in, every title wears the state a hovered one
 * wears - the same coral copy revealed by the same clip, on the same
 * transition. It is the hover rule with the condition widened, not a second
 * treatment invented for the entrance, so there is one place where the lit
 * state of a title is described. work.js takes the class off again as the
 * page hands over, and each fill retracts on its own transition.
 */
.nsn-list.is-intro .nsn-list__fill {
	clip-path: inset(0% 0 0);
}

/*
 * While the index is arriving, the row's own opacity transition is off.
 *
 * The distance falloff transitions opacity over the row pace, and GSAP writes
 * opacity every frame during the entrance - so the stylesheet was easing
 * towards each of those frames in turn and the rendered value lagged the
 * animated one badly. Measured, the fade that should have reached 0 bottomed
 * out at 0.07 and was still falling when the next beat began. One owner at a
 * time: GSAP has it until the entrance hands back.
 */
.nsn-list.is-arriving .nsn-list__item {
	transition: none;
}

.nsn-list__item.is-active .nsn-list__subtitle {
	color: var(--nsn-bone-46);
}

.nsn-list__item.is-active .nsn-list__index {
	color: var(--nsn-bone-72);
}

/* Filtered-out items are removed from layout and from the tab order. */
.nsn-list__item.is-filtered {
	display: none;
}

.nsn-list__empty {
	padding: var(--nsn-space-3) 0;
	font-size: var(--nsn-step-sm);
	color: var(--nsn-bone-46);
}

/* --------------------------------------------------------------------------
   Active project detail
   --------------------------------------------------------------------------
   Sits directly under the list, inside the centre column, so it reads as
   belonging to the highlighted project. It is also the non-hover route into a
   project - the same affordance serves touch and keyboard.
   -------------------------------------------------------------------------- */
.nsn-detail {
	flex: none;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--nsn-space-3);
	margin-top: var(--nsn-space-2);
	padding-top: var(--nsn-space-2);
	border-top: 1px solid var(--nsn-rule);
}

.nsn-detail__eyebrow {
	flex: 1;
	min-width: 0;
	font-size: var(--nsn-step-xs);
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--nsn-bone-46);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.nsn-detail__cta {
	flex: none;
	display: inline-flex;
	align-items: center;
	gap: 0.7rem;
	padding: 0.7rem 1.2rem;
	border: 1px solid var(--nsn-bone-28);
	font-size: var(--nsn-step-xs);
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--nsn-bone);
	transition:
		background-color var(--nsn-dur-fast) var(--nsn-ease),
		color var(--nsn-dur-fast) var(--nsn-ease),
		border-color var(--nsn-dur-fast) var(--nsn-ease);
}

.nsn-detail__cta-arrow {
	transition: transform var(--nsn-dur) var(--nsn-ease);
}

.nsn-detail__cta:hover,
.nsn-detail__cta:focus-visible {
	background-color: var(--nsn-bone);
	border-color: var(--nsn-bone);
	color: var(--nsn-ink);
}

.nsn-detail__cta:hover .nsn-detail__cta-arrow {
	transform: translateX(0.35rem);
}

/* --------------------------------------------------------------------------
   Shorter viewports: buy the list more rows.
   -------------------------------------------------------------------------- */
@media (min-width: 901px) and (max-height: 840px) {
	:root {
		--nsn-header-h: 72px;
	}

	.nsn-list__title {
		font-size: clamp(2.5rem, 1.2rem + 2.7vw, 3.8rem);
	}

	.nsn-list__link {
		padding: 0.45rem 0;
	}
}

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   Everything stays functional; only the cinematic parts are removed. The
   stage still swaps images, it just does so with a short fade (driven from
   work.js) instead of a masked, drifting transition.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
	/*
	 * The stage still switches - the image is content, not decoration - but it
	 * arrives at its final size instead of travelling there.
	 */
	.nsn-stage__layer.is-active {
		animation: none;
		transform: none;
	}

	.nsn-list__link,
	.nsn-list__fill,
	.nsn-list__subtitle,
	.nsn-list__index,
	.nsn-filter,
	.nsn-filter::before,
	.nsn-detail__cta-arrow {
		transition-duration: 0.01ms;
	}

	.nsn-list__item.is-active .nsn-list__link {
		transform: none;
	}

	.nsn-work__scroll {
		scroll-behavior: auto;
	}
}

/* --------------------------------------------------------------------------
   Narrow desktop: the filter is the first thing to run out of room, because
   FAMILIEOPSTELLINGEN is one unbreakable word. Easing the tracking here keeps
   every label on a single line without shrinking the type, and buys the
   centred list a little more width at the same time.
   -------------------------------------------------------------------------- */
@media (min-width: 901px) and (max-width: 1300px) {
	.nsn-filter {
		letter-spacing: 0.1em;
	}
}

/* ==========================================================================
   Desktop: centred project index with a focal position
   --------------------------------------------------------------------------
   Two things happen here, and only above the single-column breakpoint.

   HORIZONTAL  The list box is shrunk to hug its own type and centred, so the
               project names sit on the true viewport centre rather than at
               the left edge of a wide centred column. The index numerals are
               lifted out of the flow and hang in the margin, so they do not
               drag the optical centre sideways.

   VERTICAL    work.js keeps the active row at the middle of the scroll
               viewport. The padding it needs to let the first and last rows
               reach that middle is applied inline by the script, because it
               depends on the measured row height.
   ========================================================================== */
@media (min-width: 901px) {
	.nsn-list {
		width: fit-content;
		max-width: 100%;
		margin-inline: auto;
	}

	.nsn-list__link {
		display: block;
		position: relative;
		padding-left: 0;
	}

	/* Hangs in the left margin, outside the centred text block. */
	.nsn-list__index {
		position: absolute;
		right: calc(100% + clamp(1rem, 2vw, 2rem));
		top: 0.62em;
		line-height: 1;
	}

	/*
	 * The active row is no longer nudged sideways: on a centred index that
	 * would pull the focal row off the very centre it is meant to occupy.
	 * Colour carries the emphasis instead.
	 */
	.nsn-list__item.is-active .nsn-list__link {
		transform: none;
	}

	/*
	 * Distance falloff, set from work.js. Rows further from the focal row
	 * recede, but never to nothing.
	 */
	.nsn-list__item {
		--nsn-dist: 2;
		/*
		 * Deliberately gentle. These photographs are far brighter and busier
		 * than a tonally consistent studio set, and this falloff multiplies
		 * the inactive colour alpha - anything stronger and distant rows stop
		 * being readable over a light frame.
		 */
		opacity: calc(1 - min(var(--nsn-dist), 2) * 0.06);
		transition: opacity var(--nsn-work-dur) var(--nsn-work-ease);
	}

	.nsn-list__item.is-active {
		opacity: 1;
	}
}

@media (min-width: 901px) and (prefers-reduced-motion: reduce) {
	.nsn-list__item {
		transition-duration: 0.01ms;
	}
}


/* --------------------------------------------------------------------------
   Narrow screens

   responsive.css carries this section's small-screen sizing and loads after
   this sheet, so raising the title there is not possible from here on equal
   weight. One extra class does it, and nothing else about that rule changes.
   -------------------------------------------------------------------------- */
@media (max-width: 700px) {
	.nsn-work .nsn-list__title {
		font-size: clamp(1.9rem, 7.4vw, 2.375rem);
		line-height: 1.02;
	}
}
