/* ==========================================================================
   Nikki Sienna Productions — design system
   --------------------------------------------------------------------------
   Ported from Figma "Sienna productions" (desktop frame 1920, mobile frame 430).

   How this file relates to Elementor
   ----------------------------------
   Elementor owns the things an editor should be able to change without a
   developer: padding, margins, gaps, colours, typography sizes, alignment,
   widths, image sources, text. Those live in the page's Elementor data.

   This file owns the things Elementor has no control for, and the tokens that
   keep the two pages consistent:
     - the fluid type ramp between the 430 and 1920 Figma frames
     - gradient scrims, masks, blurs, glows, the curved dark shape
     - marquees, carousels, the sticky header, hover/focus transitions
     - the "+" rule markers used as section dividers throughout

   Selectors are single-class and low-specificity on purpose, so an editor's
   Elementor setting always wins over a default here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
	/* Palette — straight from the Figma fills */
	--ns-ink: #070302;
	--ns-accent: #cf7660;
	--ns-white: #ffffff;
	--ns-cream: #fff5f1;
	--ns-surface: #fafafa;

	--ns-line-dark: rgba(7, 3, 2, 0.1);
	--ns-line-light: rgba(255, 255, 255, 0.1);
	--ns-glass-light: rgba(255, 255, 255, 0.05);
	--ns-glass-header: rgba(255, 255, 255, 0.1);

	/* Families: licensed name first, stand-in second.
	   @font-face rules are emitted by nikki_sienna_font_face_css() in
	   functions.php, which serves either the licensed file or its stand-in
	   under the licensed family name. See assets/fonts/README.md. */
	--ns-font-display: 'Bigola Display', 'Playfair Display', 'Iowan Old Style', Georgia, serif;
	--ns-font-body: 'Fellix', 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

	/* Layout — 1520 content inside 200 gutters at the 1920 design width. */
	--ns-gutter: clamp(20px, 10.4167vw, 200px);
	--ns-content: 1520px;
	--ns-radius-pill: 24px;
	--ns-radius-field: 16px;

	/* Type ramp — mobile first (Figma 430 frame), fluid down to 315. */
	--ns-fs-display-xl: clamp(2.125rem, 11.6279vw, 3.125rem);
	--ns-lh-display-xl: 1.2;
	--ns-fs-display-lg: clamp(1.625rem, 8.1395vw, 2.1875rem);
	--ns-lh-display-lg: 1.24;
	--ns-fs-display-md: clamp(1.375rem, 6.5116vw, 1.75rem);
	--ns-lh-display-md: 1.28;
	--ns-fs-card-title: clamp(1.125rem, 5.1163vw, 1.375rem);
	--ns-lh-card-title: 1.36;
	--ns-fs-marquee: clamp(1.125rem, 5.1163vw, 1.375rem);
	--ns-lh-marquee: 1.36;
	--ns-fs-body-xl: clamp(1rem, 4.186vw, 1.125rem);
	--ns-lh-body-xl: 1.56;
	--ns-fs-body-lg: 1rem;
	--ns-lh-body-lg: 1.625;
	--ns-fs-tag: 1rem;

	/* Motion */
	--ns-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
	--ns-dur: 0.4s;
	--ns-dur-fast: 0.25s;

	/* Entrance reveals. A long tail with almost no acceleration at the end
	   is what separates a settle from a slide; the two durations keep every
	   reveal on the page reading as one gesture. */
	--ns-ease-reveal: cubic-bezier(0.22, 1, 0.36, 1);
	--ns-dur-reveal: 0.7s;
	--ns-dur-reveal-slow: 1.1s;
}

/* Desktop ramp: interpolates 768 -> 1920, landing exactly on the Figma
   desktop values at 1920 so the design is 1:1 at its native width. */
@media (min-width: 768px) {
	:root {
		--ns-fs-display-xl: clamp(3.25rem, 2.0833rem + 2.4306vw, 5rem);      /* 52 -> 80 */
		--ns-lh-display-xl: 1.25;
		--ns-fs-display-lg: clamp(2rem, 3.0208vw, 3.625rem);                 /* 58 at 1920, proportional below */
		--ns-lh-display-lg: 1.2414;
		--ns-fs-display-md: clamp(1.875rem, 1.4583rem + 0.8681vw, 2.5rem);   /* 30 -> 40 */
		--ns-lh-display-md: 1.25;
		--ns-fs-card-title: clamp(1.375rem, 1.0417rem + 0.6944vw, 1.875rem); /* 22 -> 30 */
		--ns-lh-card-title: 1.3333;
		--ns-fs-marquee: clamp(1.375rem, 1.125rem + 0.5208vw, 1.75rem);      /* 22 -> 28 */
		--ns-lh-marquee: 1.3571;
		--ns-fs-body-xl: clamp(1.125rem, 0.9583rem + 0.3472vw, 1.375rem);    /* 18 -> 22 */
		--ns-lh-body-xl: 1.4545;
		--ns-fs-body-lg: clamp(1rem, 0.9167rem + 0.1736vw, 1.125rem);        /* 16 -> 18 */
		--ns-lh-body-lg: 1.5556;
		--ns-fs-tag: clamp(1rem, 0.9167rem + 0.1736vw, 1.125rem);
	}
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
	box-sizing: border-box;
}

html {
	-webkit-text-size-adjust: 100%;
}

body {
	margin: 0;
	background: var(--ns-white);
	color: var(--ns-ink);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 400;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

/* The design has full-bleed art that sits outside its section box in places;
   clipping at the page level keeps that from creating a horizontal scrollbar. */
.ns-page {
	overflow-x: clip;
}

img,
svg,
video {
	max-width: 100%;
	display: block;
}

h1, h2, h3, h4, h5, h6 {
	font-family: var(--ns-font-display);
	font-weight: 400;
	margin: 0;
}

p {
	margin: 0;
}

a {
	color: inherit;
	text-decoration: none;
}

.ns-skip-link:focus {
	position: fixed;
	top: 12px;
	left: 12px;
	z-index: 999;
	width: auto;
	height: auto;
	clip: auto;
	clip-path: none;
	padding: 12px 20px;
	background: var(--ns-ink);
	color: var(--ns-white);
	border-radius: var(--ns-radius-pill);
}

:where(a, button, input, select, textarea):focus-visible {
	outline: 2px solid var(--ns-accent);
	outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

/* Applied to Elementor full-width containers. max-width tracks the gutter so
   the 1520/1920 proportion from Figma holds at every desktop width, and the
   design stops growing past its native 1920.

   The horizontal gutter itself is set through Elementor's padding control (see
   ns_wrap() in inc/build/_lib.php) so it stays visible and editable in the
   panel; only the width cap lives here. */
.ns-container {
	--ns-max: calc(var(--ns-content) + (var(--ns-gutter) * 2));

	width: 100%;
	max-width: var(--ns-max);
	margin-inline: auto;
}

/* Full-bleed rule inset 50px from the viewport edge (Figma section seams) */
.ns-rule-wrap {
	width: 100%;
	padding-inline: clamp(16px, 2.6042vw, 50px);
}

/* The short rule under the inquiry form: starts at the content gutter and
   runs 715 of the 1520 column. */
.ns-rule-wrap--short {
	max-width: calc(var(--ns-content) + (var(--ns-gutter) * 2));
	margin-inline: auto;
	padding-inline: var(--ns-gutter);
}

.ns-rule-wrap--short .ns-rule {
	width: 47.04%;
}

.ns-section {
	position: relative;
	width: 100%;
}

.ns-dark {
	background-color: var(--ns-ink);
	color: var(--ns-white);
}

/* Typography helpers, applied to Elementor heading/text widgets. */
.ns-display-xl .elementor-heading-title,
.ns-display-xl {
	font-family: var(--ns-font-display);
	font-size: var(--ns-fs-display-xl);
	line-height: var(--ns-lh-display-xl);
	font-weight: 400;
}

.ns-display-lg .elementor-heading-title,
.ns-display-lg {
	font-family: var(--ns-font-display);
	font-size: var(--ns-fs-display-lg);
	line-height: var(--ns-lh-display-lg);
	font-weight: 400;
}

.ns-display-md .elementor-heading-title,
.ns-display-md {
	font-family: var(--ns-font-display);
	font-size: var(--ns-fs-display-md);
	line-height: var(--ns-lh-display-md);
	font-weight: 400;
}

.ns-body-xl {
	font-size: var(--ns-fs-body-xl);
	line-height: var(--ns-lh-body-xl);
}

.ns-body-lg {
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
}

.ns-body-lg p + p,
.ns-prose p + p {
	margin-top: 10px;
}

.ns-semibold,
.ns-semibold p {
	font-weight: 600;
}

.ns-muted,
.ns-muted p {
	opacity: 0.7;
}

/* --------------------------------------------------------------------------
   4. Components
   -------------------------------------------------------------------------- */

/* 4.1 Tag pill --------------------------------------------------------- */

/* Figma strokes sit inside the shape, so its 4/15 padding plus a 32px mark
   makes a 40px pill. In border-box CSS the 1px border eats into the padding
   to land on the same 40. */
.ns-tag {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
	padding: 3px 14px 3px 3px;
	border-radius: var(--ns-radius-pill);
	border: 1px solid var(--ns-glass-light);
	background: var(--ns-glass-light);
	-webkit-backdrop-filter: blur(7.5px);
	backdrop-filter: blur(7.5px);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-tag);
	line-height: 1.5556;
	font-weight: 500;
	text-transform: uppercase;
	color: var(--ns-white);
	white-space: nowrap;
}

.ns-tag--light {
	background: var(--ns-white);
	border-color: var(--ns-line-dark);
	color: var(--ns-ink);
}

.ns-tag__mark {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	padding: 6px;
	border-radius: var(--ns-radius-pill);
	background: var(--ns-accent);
}

/* The monogram is the script "ns" lifted straight out of the lockup, so it is
   landscape (roughly 2.6:1) rather than the portrait mark this pill used to
   carry. It spans the pill's 20px content box; object-fit keeps the ratio,
   leaving the slack above and below rather than stretching it. */
.ns-tag__mark img,
.ns-tag__mark svg {
	width: 20px;
	height: 8px;
	object-fit: contain;
}

/* The pill doubles as a link where the section it heads points somewhere.
   The anchor carries .ns-tag itself, so it inherits the pill wholesale and
   the entire pill — mark included — is the hit area. Every interactive state
   is pinned back to the resting one on purpose: the frame draws this pill one
   way and it keeps that appearance on hover, tap and press alike. */
a.ns-tag {
	cursor: pointer;
	text-decoration: none;
	transition: none;
	-webkit-tap-highlight-color: transparent;
}

a.ns-tag:hover,
a.ns-tag:focus,
a.ns-tag:active {
	background: var(--ns-glass-light);
	border-color: var(--ns-glass-light);
	color: var(--ns-white);
}

a.ns-tag--light:hover,
a.ns-tag--light:focus,
a.ns-tag--light:active {
	background: var(--ns-white);
	border-color: var(--ns-line-dark);
	color: var(--ns-ink);
}

a.ns-tag:hover .ns-tag__mark,
a.ns-tag:focus .ns-tag__mark,
a.ns-tag:active .ns-tag__mark {
	background: var(--ns-accent);
}

/* 4.2 Buttons ---------------------------------------------------------- */

/* Applied to Elementor button widgets via their CSS-class field. */
.ns-btn .elementor-button,
a.ns-btn,
button.ns-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
	padding: 10px 30px;
	border-radius: var(--ns-radius-pill);
	border: 1px solid var(--ns-line-light);
	background-color: var(--ns-accent);
	color: var(--ns-white);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 500;
	text-align: center;
	white-space: nowrap;
	cursor: pointer;
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease),
		border-color var(--ns-dur-fast) var(--ns-ease),
		transform var(--ns-dur-fast) var(--ns-ease),
		box-shadow var(--ns-dur-fast) var(--ns-ease);
}

.ns-btn .elementor-button:hover,
.ns-btn .elementor-button:focus-visible,
a.ns-btn:hover,
a.ns-btn:focus-visible {
	background-color: var(--ns-white);
	border-color: var(--ns-line-dark);
	color: var(--ns-ink);
	transform: translateY(-2px);
	box-shadow: 0 10px 24px rgba(207, 118, 96, 0.32);
}

.ns-btn .elementor-button:active,
a.ns-btn:active {
	transform: translateY(0);
	box-shadow: none;
}

/* Light/secondary button */
.ns-btn--light .elementor-button,
a.ns-btn--light {
	background-color: var(--ns-white);
	border-color: var(--ns-line-dark);
	color: var(--ns-ink);
}

.ns-btn--light .elementor-button:hover,
.ns-btn--light .elementor-button:focus-visible,
a.ns-btn--light:hover,
a.ns-btn--light:focus-visible {
	background-color: var(--ns-accent);
	border-color: var(--ns-line-light);
	color: var(--ns-white);
	box-shadow: 0 10px 24px rgba(207, 118, 96, 0.32);
}

/* Full-width variant (the form's submit) */
.ns-btn--block .elementor-button,
a.ns-btn--block {
	width: 100%;
}

.ns-btn .elementor-button-icon svg,
.ns-btn .elementor-button-icon img {
	width: 24px;
	height: 24px;
	fill: currentColor;
}

/* The icons are exported from Figma with a literal `fill="#070302"` on every
   path, and Elementor inlines them as-is. A `fill` on the <svg> is only
   inherited, and an inherited value always loses to a value set directly on
   the path — so the rule above never reached the glyph and every icon painted
   itself the Figma ink colour whatever the button underneath it was doing.
   That is what left the Instagram mark black on the terracotta buttons.

   A CSS declaration does outrank a presentation attribute, so naming the path
   is the whole fix, and it is set here once for every icon the theme inlines.
   See the matching rule on .ns-icon-btn below. */
.ns-btn .elementor-button-icon svg path,
.ns-link .elementor-button-icon svg path {
	fill: currentColor;
}

/* Underlined text link with a trailing arrow (Featured work CTA) */
.ns-link .elementor-button,
a.ns-link {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	padding: 0;
	border: 0;
	background: none;
	color: var(--ns-ink);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 500;
}

.ns-link .elementor-button-content-wrapper {
	display: inline-flex;
	align-items: center;
	gap: 10px;
}

.ns-link .elementor-button::after,
a.ns-link::after {
	content: '';
	display: block;
	width: 100%;
	height: 1px;
	background: currentColor;
	transform-origin: left center;
	transition: transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-link .elementor-button:hover::after,
a.ns-link:hover::after {
	transform: scaleX(0.94);
}

.ns-link .elementor-button-icon {
	transition: transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-link .elementor-button:hover .elementor-button-icon {
	transform: translateX(4px);
}

/* Circular icon button */
.ns-icon-btn .elementor-button,
a.ns-icon-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
	width: 46px;
	height: 46px;
	padding: 10px;
	border-radius: var(--ns-radius-pill);
	border: 1px solid var(--ns-line-dark);
	background: var(--ns-white);
	color: var(--ns-ink);
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease),
		transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-icon-btn .elementor-button svg,
a.ns-icon-btn svg {
	width: 24px;
	height: 24px;
	fill: currentColor;
}

/* Same story as .ns-btn above: the exported paths carry their own
   `fill="#070302"`, which the <svg>'s inherited fill cannot override. This is
   what kept the Instagram badge's glyph black inside its terracotta circle. */
.ns-icon-btn .elementor-button svg path,
a.ns-icon-btn svg path {
	fill: currentColor;
}

.ns-icon-btn .elementor-button:hover,
a.ns-icon-btn:hover {
	background: var(--ns-accent);
	color: var(--ns-white);
	transform: translateY(-2px);
}

.ns-icon-btn--accent .elementor-button,
a.ns-icon-btn--accent {
	background: var(--ns-accent);
	border-color: transparent;
	color: var(--ns-white);
}

.ns-icon-btn--accent .elementor-button:hover,
a.ns-icon-btn--accent:hover {
	background: var(--ns-white);
	color: var(--ns-ink);
}

/* 4.3 Rule with a "+" marker ------------------------------------------ */

.ns-rule {
	position: relative;
	width: 100%;
	height: 1px;
	background: var(--ns-line-dark);
	color: var(--ns-ink);
}

.ns-rule--light {
	background: var(--ns-line-light);
	color: var(--ns-white);
}

.ns-plus {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 13px;
	height: 13px;
	margin: -6px 0 0 -6px;
	/* Two 13px strokes crossing at the centre — the same construction the
	   why-grid markers use, so every cross on the page is identical. */
	background:
		linear-gradient(currentColor, currentColor) center / 13px 1px no-repeat,
		linear-gradient(currentColor, currentColor) center / 1px 13px no-repeat;
	pointer-events: none;
}

.ns-plus--start {
	left: 0;
	margin-left: 0;
}

/* 4.4 Marquee ---------------------------------------------------------- */

.ns-marquee {
	position: relative;
	width: 100%;
	overflow: hidden;
}

.ns-marquee__track {
	display: flex;
	align-items: center;
	width: max-content;
	will-change: transform;
	animation: ns-marquee-scroll var(--ns-marquee-duration, 45s) linear infinite;
}

.ns-marquee__group {
	display: flex;
	align-items: center;
	flex: 0 0 auto;
}

.ns-marquee:hover .ns-marquee__track {
	animation-play-state: paused;
}

@keyframes ns-marquee-scroll {
	from { transform: translate3d(0, 0, 0); }
	to   { transform: translate3d(-50%, 0, 0); }
}

/* Services marquee (Section 6) */
.ns-services-marquee .ns-marquee__group {
	gap: 20px;
	padding-right: 20px;
}

.ns-services-marquee .ns-marquee__item {
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-marquee);
	line-height: var(--ns-lh-marquee);
	font-weight: 600;
	color: var(--ns-ink);
	white-space: nowrap;
}

.ns-services-marquee .ns-marquee__star {
	flex: 0 0 auto;
	width: 24px;
	height: 24px;
}

/* Client logo marquee (Section 2) with the Figma edge fades */
.ns-clients-marquee .ns-marquee__group {
	gap: 40px;
	padding-right: 40px;
}

.ns-clients-marquee .ns-marquee__item img {
	width: auto;
	height: 32px;
	object-fit: contain;
	filter: grayscale(1);
	opacity: 0.85;
	transition: filter var(--ns-dur-fast) var(--ns-ease), opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-clients-marquee .ns-marquee__item:hover img {
	filter: grayscale(0);
	opacity: 1;
}

.ns-clients-marquee::before,
.ns-clients-marquee::after {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	width: clamp(60px, 18.75%, 285px);
	z-index: 2;
	pointer-events: none;
}

.ns-clients-marquee::before {
	left: 0;
	background: linear-gradient(90deg, var(--ns-white) 0%, rgba(255, 255, 255, 0) 100%);
}

.ns-clients-marquee::after {
	right: 0;
	background: linear-gradient(270deg, var(--ns-white) 0%, rgba(255, 255, 255, 0) 100%);
}

/* Per-logo heights, matching the Figma optical sizing */
.ns-clients-marquee .ns-marquee__item[data-logo='clinique'] img { height: 28px; }
.ns-clients-marquee .ns-marquee__item[data-logo='meta'] img { height: 25px; }
.ns-clients-marquee .ns-marquee__item[data-logo='sony-music'] img { height: 28px; }
.ns-clients-marquee .ns-marquee__item[data-logo='lionsgate'] img { height: 23px; }
.ns-clients-marquee .ns-marquee__item[data-logo='la-roche-posay'] img { height: 23px; }
.ns-clients-marquee .ns-marquee__item[data-logo='arista'] img { height: 26px; }

@media (prefers-reduced-motion: reduce) {
	.ns-marquee__track {
		animation: none;
		transform: none;
	}

	.ns-marquee {
		overflow-x: auto;
	}
}

/* Gallery modal ------------------------------------------------------------

   The frame the client's VidFlow galleries open into. Built by site.js on the
   first click and empty until then — see initGalleryModal().

   It is deliberately plain: the gallery brings its own art direction, its own
   title and its own Play Film button, so everything here is chrome around it.
   Full-bleed on phones, a floating panel from tablet up. */

.ns-gallery {
	position: fixed;
	inset: 0;
	z-index: 1000;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	opacity: 0;
	visibility: hidden;
	transition:
		opacity var(--ns-dur) var(--ns-ease),
		visibility 0s linear var(--ns-dur);
}

.ns-gallery-open .ns-gallery {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s;
}

.ns-gallery__backdrop {
	position: absolute;
	inset: 0;
	background: rgba(7, 3, 2, 0.82);
	-webkit-backdrop-filter: blur(8px);
	backdrop-filter: blur(8px);
}

.ns-gallery__panel {
	position: relative;
	display: flex;
	flex-direction: column;
	width: 100%;
	max-width: 1520px;
	height: 100%;
	max-height: 100%;
	overflow: hidden;
	background: var(--ns-ink);
	transform: translateY(16px) scale(0.985);
	transition: transform var(--ns-dur-reveal) var(--ns-ease-reveal);
}

.ns-gallery-open .ns-gallery__panel {
	transform: none;
}

.ns-gallery__bar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	flex: none;
	padding: 14px 16px;
	border-bottom: 1px solid var(--ns-line-light);
}

.ns-gallery__title {
	margin: 0;
	overflow: hidden;
	color: var(--ns-white);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: 1.3;
	font-weight: 500;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.ns-gallery__actions {
	display: flex;
	align-items: center;
	gap: 8px;
	flex: none;
}

.ns-gallery__out {
	padding: 8px 16px;
	border-radius: var(--ns-radius-pill);
	border: 1px solid var(--ns-line-light);
	color: var(--ns-white);
	font-family: var(--ns-font-body);
	font-size: 0.875rem;
	line-height: 1.2;
	font-weight: 500;
	text-decoration: none;
	white-space: nowrap;
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease);
}

.ns-gallery__out:hover,
.ns-gallery__out:focus-visible {
	background: var(--ns-white);
	color: var(--ns-ink);
}

.ns-gallery__close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	padding: 0;
	border-radius: 50%;
	border: 1px solid var(--ns-line-light);
	background: none;
	color: var(--ns-white);
	cursor: pointer;
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease);
}

.ns-gallery__close:hover,
.ns-gallery__close:focus-visible {
	background: var(--ns-white);
	color: var(--ns-ink);
}

.ns-gallery__close svg {
	width: 18px;
	height: 18px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	stroke-linecap: round;
}

/* The chooser, for a card that holds more than one gallery. Scrolls sideways
   rather than wrapping, so the bar keeps its height at any width. */
.ns-gallery__tabs {
	display: flex;
	gap: 8px;
	flex: none;
	overflow-x: auto;
	padding: 12px 16px;
	border-bottom: 1px solid var(--ns-line-light);
	scrollbar-width: none;
}

.ns-gallery__tabs::-webkit-scrollbar {
	display: none;
}

.ns-gallery__tabs[hidden] {
	display: none;
}

.ns-gallery__tab {
	flex: none;
	padding: 8px 16px;
	border-radius: var(--ns-radius-pill);
	border: 1px solid var(--ns-line-light);
	background: none;
	color: rgba(255, 255, 255, 0.65);
	font-family: var(--ns-font-body);
	font-size: 0.875rem;
	line-height: 1.2;
	font-weight: 500;
	white-space: nowrap;
	cursor: pointer;
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease),
		border-color var(--ns-dur-fast) var(--ns-ease);
}

.ns-gallery__tab:hover {
	color: var(--ns-white);
	border-color: rgba(255, 255, 255, 0.4);
}

.ns-gallery__tab[aria-selected='true'] {
	background: var(--ns-white);
	border-color: var(--ns-white);
	color: var(--ns-ink);
}

.ns-gallery__stage {
	position: relative;
	flex: 1 1 auto;
	min-height: 0;
	background: var(--ns-ink);
}

.ns-gallery__frame {
	display: block;
	width: 100%;
	height: 100%;
	border: 0;
}

/* Shown only while a gallery is in flight; the frame paints over it. */
.ns-gallery__loader {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-gallery__stage.is-loading .ns-gallery__loader {
	opacity: 1;
}

.ns-gallery__loader span {
	width: 34px;
	height: 34px;
	border-radius: 50%;
	border: 2px solid var(--ns-line-light);
	border-top-color: var(--ns-white);
	animation: ns-gallery-spin 0.9s linear infinite;
}

@keyframes ns-gallery-spin {
	to { transform: rotate(360deg); }
}

.ns-gallery-open,
.ns-gallery-open body {
	overflow: hidden;
}

@media (min-width: 768px) {
	.ns-gallery {
		padding: clamp(20px, 3.5vw, 48px);
	}

	.ns-gallery__panel {
		height: auto;
		border-radius: var(--ns-radius-field);
	}

	.ns-gallery__bar {
		padding: 16px 24px;
	}

	.ns-gallery__tabs {
		padding: 14px 24px;
	}

	.ns-gallery__title {
		font-size: var(--ns-fs-body-xl);
	}

	/* The galleries are 16:9 films behind a poster wall of their own; giving
	   the stage that ratio means no letterboxing and no inner scrollbar. The
	   height cap keeps the whole panel on screen on a short laptop, where the
	   bars and the ratio together would otherwise overflow. */
	.ns-gallery__stage {
		aspect-ratio: 16 / 9;
		max-height: calc(100vh - 200px);
	}
}

@media (prefers-reduced-motion: reduce) {
	.ns-gallery,
	.ns-gallery__panel {
		transition: none;
	}

	.ns-gallery__panel {
		transform: none;
	}

	.ns-gallery__loader span {
		animation-duration: 2.4s;
	}
}

/* --------------------------------------------------------------------------
   5. Site header
   --------------------------------------------------------------------------
   Figma draws the header as a translucent bar sitting on top of the hero art.
   It is fixed rather than static so the blur keeps working as the page moves;
   at rest it is pixel-identical to the design. Once the hero has scrolled past,
   `.is-scrolled` swaps in an opaque ground so the white sections underneath
   don't wash the header out.
   -------------------------------------------------------------------------- */

.ns-site-header {
	position: fixed;
	top: 0;
	right: 0;
	left: 0;
	z-index: 100;
}

.ns-header__bar.e-con,
.ns-header__bar {
	background-color: var(--ns-glass-header);
	border-bottom: 1px solid var(--ns-line-light);
	-webkit-backdrop-filter: blur(25px);
	backdrop-filter: blur(25px);
	transition: background-color var(--ns-dur) var(--ns-ease);
}

.ns-site-header.is-scrolled .ns-header__bar {
	background-color: rgba(7, 3, 2, 0.82);
}

/* The bar is logo | nav | CTA | toggle and must stay on one line; Elementor
   turns on flex-wrap for containers below 768px. */
.ns-site-header .ns-container {
	flex-wrap: nowrap;
}

.ns-site-header .ns-header__logo img {
	width: 214px;
	height: auto;
	max-width: 100%;
}

/* Mirrors the logo width so the menu centres between them under
   space-between, which is how Figma lands it. */
.ns-site-header .ns-header__cta {
	--width: 214px;
	--flex-shrink: 0;
}

.ns-header__nav .elementor-button,
.ns-header__nav a {
	position: relative;
	padding: 0;
	border: 0;
	background: none;
	color: var(--ns-white);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 400;
	white-space: nowrap;
}

/* Underline that grows from the left on hover */
.ns-header__nav .elementor-button::after,
.ns-header__nav a::after {
	content: '';
	position: absolute;
	bottom: -4px;
	left: 0;
	width: 100%;
	height: 1px;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-header__nav .elementor-button:hover::after,
.ns-header__nav a:hover::after,
.ns-header__nav .current-menu-item a::after {
	transform: scaleX(1);
}

/* Hamburger — only shown once the inline nav is dropped.

   The toggle sits in an HTML widget, and Elementor wrapper is itself a flex
   child of the bar. Hiding only the button leaves that wrapper holding a
   space-between slot, which drags the menu and CTA left of where Figma puts
   them, so the wrapper is what has to go. */
.ns-header__toggle-wrap {
	display: none;
}

.ns-header__toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
	padding: 10px;
	border: 0;
	border-radius: var(--ns-radius-pill);
	background: var(--ns-accent);
	color: var(--ns-white);
	cursor: pointer;
	transition: background-color var(--ns-dur-fast) var(--ns-ease);
}

.ns-header__toggle:hover {
	background: #b9614c;
}

.ns-header__toggle-bars,
.ns-header__toggle-bars::before,
.ns-header__toggle-bars::after {
	display: block;
	width: 18px;
	height: 2px;
	border-radius: 2px;
	background: currentColor;
	transition: transform var(--ns-dur-fast) var(--ns-ease), opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-header__toggle-bars {
	position: relative;
}

.ns-header__toggle-bars::before,
.ns-header__toggle-bars::after {
	content: '';
	position: absolute;
	left: 0;
}

.ns-header__toggle-bars::before { top: -6px; }
.ns-header__toggle-bars::after { top: 6px; }

.ns-nav-open .ns-header__toggle-bars { background: transparent; }
.ns-nav-open .ns-header__toggle-bars::before { transform: translateY(6px) rotate(45deg); }
.ns-nav-open .ns-header__toggle-bars::after { transform: translateY(-6px) rotate(-45deg); }

/* Mobile panel.

   site.js rebuilds the header's links as plain markup here rather than
   cloning the Elementor widgets: a clone's styling lives behind
   `.elementor-<template-id>` selectors that stop matching once the node is
   moved to the body, which left the panel rendering as an empty black sheet.
   Everything below therefore styles theme-owned classes only.

   The panel is the ground, not a scrim — it paints an opaque dark surface so
   nothing of the page beneath can wash the links out, and it is only ever
   laid out below the breakpoint so the desktop bar is untouched. */
.ns-mobile-nav {
	--ns-nav-top: 85px; /* Header height at this breakpoint. */

	position: fixed;
	inset: 0;
	z-index: 99; /* Under the header (100), so the logo and toggle stay on top. */
	display: none;
	background:
		radial-gradient(125% 70% at 50% 0%, rgba(207, 118, 96, 0.16) 0%, rgba(207, 118, 96, 0) 62%),
		#0a0403;
	opacity: 0;
	visibility: hidden;
	pointer-events: none;

	/* Visibility is stepped, not eased: on the way out it waits for the fade
	   to finish, on the way in (below) it flips at once. Easing it in both
	   directions leaves the panel resolving to `hidden` for the rest of the
	   frame it opens on, and a hidden panel cannot take focus. */
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease),
		visibility 0s linear var(--ns-dur-fast);
}

@media (max-width: 767px) {
	.ns-mobile-nav {
		display: flex;
	}
}

.ns-nav-open .ns-mobile-nav {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease),
		visibility 0s;
}

/* The scroller. Auto margins on the children centre the group when there is
   room and collapse to zero when there is not, which is the one centring that
   never clips the top of an overflowing flex container. */
.ns-mobile-nav__sheet {
	display: flex;
	flex-direction: column;
	width: 100%;
	max-height: 100%;
	padding:
		calc(var(--ns-nav-top) + 28px)
		var(--ns-gutter)
		max(32px, env(safe-area-inset-bottom));
	overflow-y: auto;
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
}

.ns-mobile-nav__sheet::-webkit-scrollbar {
	display: none;
}

.ns-mobile-nav__list {
	display: flex;
	flex-direction: column;
	margin-block-start: auto;
	border-top: 1px solid var(--ns-line-light);
}

.ns-mobile-nav__link {
	display: grid;
	grid-template-columns: auto 1fr auto;
	align-items: center;
	gap: 16px;
	min-height: 60px;
	padding-block: 14px;
	border-bottom: 1px solid var(--ns-line-light);
	color: var(--ns-white);
	text-decoration: none;
	transition: color var(--ns-dur-fast) var(--ns-ease);
	-webkit-tap-highlight-color: transparent;
}

.ns-mobile-nav__index {
	font-family: var(--ns-font-body);
	font-size: 0.75rem;
	line-height: 1;
	font-weight: 500;
	letter-spacing: 0.14em;
	color: var(--ns-accent);
	transition: opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-mobile-nav__label {
	font-family: var(--ns-font-body);
	font-size: clamp(1.5rem, 7.4vw, 2rem);
	line-height: 1.2;
	font-weight: 400;
}

/* Chevron, drawn rather than shipped as an asset so it inherits the accent. */
.ns-mobile-nav__arrow {
	width: 9px;
	height: 9px;
	border-top: 1.5px solid var(--ns-accent);
	border-right: 1.5px solid var(--ns-accent);
	transform: rotate(45deg);
	opacity: 0.55;
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease),
		transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-mobile-nav__link:hover,
.ns-mobile-nav__link:focus-visible,
.ns-mobile-nav__link:active,
.ns-mobile-nav__link.is-current {
	color: var(--ns-accent);
}

.ns-mobile-nav__link:hover .ns-mobile-nav__arrow,
.ns-mobile-nav__link:focus-visible .ns-mobile-nav__arrow,
.ns-mobile-nav__link:active .ns-mobile-nav__arrow,
.ns-mobile-nav__link.is-current .ns-mobile-nav__arrow {
	opacity: 1;
	transform: rotate(45deg) translate(2px, -2px);
}

.ns-mobile-nav__link:focus-visible {
	outline: 2px solid var(--ns-accent);
	outline-offset: 4px;
}

.ns-mobile-nav__foot {
	margin-block: 40px auto;
}

/* Inherits the site button; only the tap target is grown for touch. */
a.ns-mobile-nav__cta {
	min-height: 52px;
	padding-block: 14px;
}

/* Links arrive one after another. The resting state is fully visible, and the
   keyframe only ever ends there, so if the animation is skipped for any
   reason the panel still reads correctly. */
@media (prefers-reduced-motion: no-preference) {
	.ns-nav-open .ns-mobile-nav__link,
	.ns-nav-open .ns-mobile-nav__foot {
		animation: ns-nav-in var(--ns-dur-fast) var(--ns-ease) backwards;
		animation-delay: calc(40ms + var(--ns-nav-index, 0) * 35ms);
	}
}

@keyframes ns-nav-in {
	from {
		opacity: 0;
		transform: translateY(14px);
	}

	to {
		opacity: 1;
		transform: translateY(0);
	}
}

/* Landscape phones and short windows: give the links back their room. */
@media (max-height: 560px) {
	.ns-mobile-nav__sheet {
		padding-top: calc(var(--ns-nav-top) + 16px);
		padding-bottom: max(20px, env(safe-area-inset-bottom));
	}

	.ns-mobile-nav__link {
		min-height: 48px;
		padding-block: 9px;
	}

	.ns-mobile-nav__label {
		font-size: 1.25rem;
	}

	.ns-mobile-nav__foot {
		margin-block-start: 24px;
	}
}

.ns-nav-open,
.ns-nav-open body {
	overflow: hidden;
}

/* --------------------------------------------------------------------------
   6. Section 1 — Hero
   -------------------------------------------------------------------------- */

.ns-hero {
	position: relative;
	min-height: clamp(600px, 52.0833vw, 1000px);
	overflow: hidden;
	isolation: isolate;
}

/* The media sits in an HTML widget; promote that widget to the section's
   backdrop rather than nesting another positioned wrapper inside it. */
.ns-hero > .elementor-widget-html {
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
}

.ns-hero__media,
.ns-hero__media video,
.ns-hero__media img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center top;
}

.ns-hero__media {
	background: var(--ns-ink);
}

/* Every hero rendition and the poster are mastered with a black bar across the
   bottom 5.37% of the frame — 1022 of the 1080 rows carry picture. Wherever the
   section is taller than the source's 16:9, `cover` is bound by height and shows
   the full source height, bar included; that is every width below ~1067px, the
   430 frame among them, which is why it read as a black strip under the hero.
   Overhanging the section by the same fraction puts the bar past the foot, into
   the overflow .ns-hero already clips, and `object-position: center top` holds
   the picture where the design anchors it. Above ~1067px the crop is bound by
   width instead, so this is a no-op there and desktop framing is untouched.
   Remove this once the masters are re-exported without the bar. */
.ns-hero__media video.ns-hero__video {
	height: calc(100% * 1080 / 1022);
}

/* Interior page hero: 600 of the 1920 frame rather than the home page's 1000. */
.ns-hero--page {
	min-height: clamp(400px, 31.25vw, 600px);
}

.ns-hero--page .ns-hero__media img {
	object-position: center center;
}

/* 60% #070302 wash, exactly as the Figma overlay layer */
.ns-hero__media::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--ns-ink);
	opacity: 0.6;
}

.ns-hero__inner {
	position: relative;
	z-index: 1;
}

/* Figma caps the hero copy at 1030 of the 1520 content column.

   Structural widths are set through Elementor's own --width / --flex-shrink
   custom properties rather than raw width/flex declarations. Elementor's
   container CSS reads those properties, so this composes with the builder
   instead of fighting it — and an editor's width setting, which lands on the
   higher-specificity .elementor-element-* selector, still wins. */
.ns-hero__copy {
	--width: min(1030px, 100%);

	margin-inline: auto;
}

/* The standfirst carries a break after the middot for the narrow frame, where
   the line would otherwise split mid-clause. It is inert at every width above
   the mobile breakpoint, so the desktop line stays whole. */
.ns-hero__sub-break {
	display: none;
}

/* --------------------------------------------------------------------------
   7. Section 3 — Stories that move people
   --------------------------------------------------------------------------
   A dark panel with a white foot; the two photographs are laid over the seam.
   Foot height and the section's bottom padding share the same vw factor, so
   the 100px overhang at 1920 stays proportional at every width.
   -------------------------------------------------------------------------- */

.ns-stories {
	position: relative;
	background-color: var(--ns-ink);
	color: var(--ns-white);
	isolation: isolate;
}

.ns-stories::before {
	content: '';
	position: absolute;
	inset: 0 0 auto 0;
	height: clamp(300px, 46.875vw, 900px);
	z-index: -1;
	background: url('../img/decor-lines.svg') no-repeat center top / cover;
	pointer-events: none;
}

.ns-stories::after {
	content: '';
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: clamp(60px, 11.4583vw, 220px);
	background: var(--ns-white);
	z-index: -1;
}

/* Three columns matching the Figma grid: 335 / 750 / 335 inside 1520 with
   50px gaps. Widths are structural, so they live here rather than in the
   Elementor data; padding, gap and colours stay editable. */
.ns-stories__cols {
	align-items: flex-start;
}

.ns-stories__cols > .ns-stories__col {
	--flex-shrink: 0;
}

/* 335 / 750 / 335 of the 1420 that is left after the two 50px gaps.

   The gaps are a fixed 50px (set on the Elementor container), so the widths
   have to be taken from the space after those gaps, not from the row width.
   A plain percentage only balances at 1920 and overflows the row below it. */
.ns-stories__cols > .ns-stories__col--intro { --width: calc((100% - 100px) * 0.235915); }
.ns-stories__cols > .ns-stories__col--feature { --width: calc((100% - 100px) * 0.528169); }
.ns-stories__cols > .ns-stories__col--copy { --width: calc((100% - 100px) * 0.235915); }

/* 550px of copy inside the 335px column, so it runs over the photograph to its
   right — Figma layers the text above the image, white on the picture. */
.ns-stories__text {
	--width: 164.18%;

	position: relative;
	z-index: 2;
}

.ns-stories__col--feature {
	position: relative;
	z-index: 1;
}

.ns-stories__media {
	width: 100%;
	overflow: hidden;
	background: var(--ns-surface);
}

.ns-stories__media--feature { aspect-ratio: 750 / 800; }
.ns-stories__media--cover { aspect-ratio: 335 / 400; }

.ns-stories__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform 0.8s var(--ns-ease);
}

.ns-stories__media:hover img {
	transform: scale(1.04);
}

/* Vertical offsets that stagger the three columns in the Figma layout. */
.ns-stories__col--feature { margin-top: clamp(0px, 4.1667vw, 80px); }
.ns-stories__col--copy { margin-top: clamp(0px, 14.4792vw, 165px); }

/* --------------------------------------------------------------------------
   8. Section 4 — What we create
   -------------------------------------------------------------------------- */

/* Two-up card grid; 30px gutter gives exactly 745px cards inside 1520. */
.ns-services__grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 30px;
	width: 100%;
}

.ns-service-card {
	position: relative;
	overflow: hidden;
	aspect-ratio: 745 / 500;
	background: var(--ns-surface);
}

.ns-service-card__media,
.ns-service-card__media img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.ns-service-card__media img {
	transition: transform 0.8s var(--ns-ease);
}

.ns-service-card:hover .ns-service-card__media img,
.ns-service-card:focus-within .ns-service-card__media img {
	transform: scale(1.06);
}

/* Figma: linear #070302 0% @58.083% -> 100% @128.6%, over a 2.5px backdrop
   blur. The mask keeps the blur to the lower part of the card, the way a
   Figma background blur behaves inside its own layer. */
.ns-service-card::after {
	content: '';
	position: absolute;
	inset: 0;
	z-index: 1;
	background: linear-gradient(180deg, rgba(7, 3, 2, 0) 58.083%, var(--ns-ink) 128.6%);
	-webkit-backdrop-filter: blur(2.5px);
	backdrop-filter: blur(2.5px);
	-webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 58.083%, #000 100%);
	mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 58.083%, #000 100%);
	pointer-events: none;
}

/* Hover: the whole card darkens and the description unrolls.

   Title and description sit in one block pinned to the bottom-left, so the
   description expanding is what lifts the title — matching the two states in
   the frame, where the title rises by exactly the height of the copy.

   max-height animates rather than height because the copy length varies per
   card; the value only has to exceed the tallest description. */
/* Elementor pins containers to width:100%. On an absolutely positioned box
   an explicit width beats the `right` offset, so the block was card-width
   starting 30px in and the copy wrapped 30px past the card edge. `auto` hands
   sizing back to the left/right pair. */
.ns-service-card .ns-service-card__text {
	--width: auto;

	position: absolute;
	right: 15.44%;
	bottom: 30px;
	left: 30px;
	z-index: 2;
}

.ns-service-card__title {
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-card-title);
	line-height: var(--ns-lh-card-title);
	font-weight: 500;
	color: var(--ns-white);
}

/* Animating grid-template-rows from 0fr to 1fr resolves to the copy s own
   height, so it is never clipped by a fixed cap the way a max-height would
   be at narrow widths where the text wraps to more lines. */
/* Scoped to the card so it outranks the blanket transition Elementor puts
   on every widget wrapper, which otherwise replaces this list wholesale and
   makes the copy snap open instead of sliding. */
.ns-service-card .ns-service-card__desc {
	display: grid;
	grid-template-rows: 0fr;
	padding-top: 0;
	opacity: 0;
	transform: translateY(14px);
	transition:
		grid-template-rows 0.5s var(--ns-ease),
		padding-top 0.5s var(--ns-ease),
		opacity 0.35s var(--ns-ease),
		transform 0.5s var(--ns-ease);
}

.ns-service-card .ns-service-card__desc > * {
	min-height: 0;
	overflow: hidden;
}

.ns-service-card .ns-service-card__desc p {
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	color: var(--ns-white);
}

.ns-service-card:hover .ns-service-card__desc,
.ns-service-card:focus-within .ns-service-card__desc {
	grid-template-rows: 1fr;
	padding-top: 10px;
	opacity: 1;
	transform: none;
}

/* Even wash over the image on hover, on top of the standing bottom scrim. */
.ns-service-card::before {
	content: '';
	position: absolute;
	inset: 0;
	z-index: 1;
	background: rgba(7, 3, 2, 0.45);
	opacity: 0;
	transition: opacity 0.4s var(--ns-ease);
	pointer-events: none;
}

.ns-service-card:hover::before,
.ns-service-card:focus-within::before {
	opacity: 1;
}

/* The watch link, laid over the whole card.

   It sits above the copy so the entire card is one target, and being a real
   focusable link it is what puts the card into :focus-within — so reaching it
   by keyboard unrolls the description exactly as hovering does. */
.ns-service-card__cta {
	position: absolute;
	inset: 0;
	z-index: 3;
}

.ns-service-card__trigger {
	position: absolute;
	inset: 0;
	display: block;
	border-radius: inherit;
}

/* The play control — a fixed 52px circle, top-right.

   Fixed, because the copy is the constraint. Title and description are pinned
   bottom-left and the description stops only 15.44% short of the card's right
   edge; a circle sits inside that margin at every card width, while a pill
   that grew with a label ("5 films") ran over the copy anywhere between 768px
   and 1440px. A card holding several galleries carries the count as a badge on
   the circle's corner instead, which costs no width. */
.ns-service-card__play {
	position: absolute;
	top: 24px;
	right: 24px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	width: 52px;
	height: 52px;
	border-radius: 50%;
	border: 1px solid rgba(255, 255, 255, 0.35);
	background: rgba(255, 255, 255, 0.14);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	color: var(--ns-white);
	opacity: 0;
	transform: translateY(-8px) scale(0.94);
	transition:
		opacity var(--ns-dur) var(--ns-ease),
		transform var(--ns-dur) var(--ns-ease),
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease);
}

.ns-service-card__play-icon {
	width: 20px;
	height: 20px;
	flex: none;
	fill: currentColor;
	/* The glyph's optical centre sits left of its box centre. */
	margin-inline-start: 2px;
}

.ns-service-card__play-count {
	position: absolute;
	top: -5px;
	right: -5px;
	display: flex;
	align-items: center;
	justify-content: center;
	min-width: 22px;
	height: 22px;
	padding: 0 6px;
	box-sizing: border-box;
	border-radius: var(--ns-radius-pill);
	background: var(--ns-white);
	color: var(--ns-ink);
	font-family: var(--ns-font-body);
	font-size: 0.75rem;
	line-height: 1;
	font-weight: 600;
}

.ns-service-card:hover .ns-service-card__play,
.ns-service-card:focus-within .ns-service-card__play {
	opacity: 1;
	transform: none;
}

.ns-service-card__trigger:hover .ns-service-card__play,
.ns-service-card__trigger:focus-visible .ns-service-card__play {
	background: var(--ns-white);
	color: var(--ns-ink);
}

/* The circle goes white under the pointer, so the badge has to swap with it
   or it disappears into its own background. */
.ns-service-card__trigger:hover .ns-service-card__play-count,
.ns-service-card__trigger:focus-visible .ns-service-card__play-count {
	background: var(--ns-ink);
	color: var(--ns-white);
}

.ns-service-card__trigger:focus-visible {
	outline: 2px solid var(--ns-white);
	outline-offset: -4px;
}

/* Touch has no hover to reveal it, and the description does not unroll there
   either — so the control is simply always on. */
@media (hover: none) {
	.ns-service-card__play {
		opacity: 1;
		transform: none;
	}
}

/* --------------------------------------------------------------------------
   9. Section 5 — Featured work carousel
   -------------------------------------------------------------------------- */

/* The bottom markers hang 6px past the section. Figma stacks this frame
   above the one below it; in DOM order the next section would otherwise
   paint its white background over their lower arms. */
.ns-work {
	position: relative;
	z-index: 1;
}

/* Native scroll-snap container: touch, trackpad and keyboard all work without
   JS; the arrow button just nudges scrollLeft along.

   It starts at the content gutter but bleeds off the right edge of the
   viewport, so the next card is always half-visible as it is in Figma.

   Note on specificity: Elementor's own container defaults live on `.e-con`
   and `.e-con.e-flex`, so single-class rules lose to them. Scoping these to
   the parent (`.ns-work .ns-work__viewport`, `.ns-work__track > …`) clears
   that bar without reaching for !important, and still leaves an editor's
   per-element settings on top. */
.ns-work .ns-work__viewport {
	padding-block: 6px;
	margin-block: -6px;

	/* Elementor pins containers to width:100%, so a negative margin alone will
	   not widen this — the width itself has to carry the bleed for the next
	   card to show past the right gutter, as the frame has it. */
	--width: calc(100% + var(--ns-gutter));

	overflow-x: auto;
	overflow-y: hidden;
	scroll-snap-type: x mandatory;
	scrollbar-width: none;
	-ms-overflow-style: none;
	overscroll-behavior-x: contain;
}

.ns-work .ns-work__viewport {
	cursor: grab;
}

.ns-work .ns-work__viewport.is-dragging {
	cursor: grabbing;
	user-select: none;
}

/* A mouse drag across a scroller is also the browser's native "drag this image
   away" gesture: it grabs the <img>, paints a drag ghost and takes over the
   pointer stream, so the row stops following the cursor mid-swipe. Turning
   native dragging off for the images inside the horizontal scrollers — this
   carousel, the marquees and the mobile why-grid — leaves scrolling, snapping,
   clicks, the hover zoom and every measurement untouched.

   Chromium and WebKit read this property; Firefox has no equivalent, so
   site.js cancels `dragstart` for the same images to cover it. */
.ns-work__slide-media img,
.ns-marquee img,
.ns-why__col img {
	-webkit-user-drag: none;
}

.ns-work .ns-work__viewport::-webkit-scrollbar {
	display: none;
}

.ns-work .ns-work__track {
	display: flex;
	flex-direction: row;
	/* Elementor switches containers to flex-wrap:wrap below 768px; a scroller
	   must stay on one line at every width. */
	flex-wrap: nowrap;
	/* Figma sets this row to items-center: the 770-tall panels sit centred in
	   the 870 band between the two rules, which drops each image 50px below
	   the top rule. Stretching them pinned the images against it instead. */
	align-items: center;
	/* Figma lays the band out as 704 | 50 | 13 | 50 | 704 — the rule is a
	   narrow 13px column with a 50px gap either side, not a wide gutter with a
	   centred line. That is what puts the rule on the page centre. */
	gap: clamp(16px, 2.6042vw, 50px);
	/* The frame runs the dividing rules the full 870 height of the band, well
	   past the bottom of the captions; stretching the track is what gives them
	   that length. */
	min-height: clamp(340px, 45.3125vw, 870px);
}

/* 704 of the 1520 content column. The track sits in a viewport that is one
   gutter wider than the content, so the gutter comes back off before taking
   the proportion — otherwise the cards would scale to the bled width. */
.ns-work__track > .ns-work__intro,
.ns-work__track > .ns-work__slide {
	--width: calc((100% - var(--ns-gutter)) * 0.463158);
	--flex-shrink: 0;
	--flex-basis: auto;

	scroll-snap-align: start;
}

/* Figma sets the intro copy 490 wide inside the 704 panel — 107px either side.

   The track is stretched to 870 so the dividing rules run the full band, but
   Figma centres this copy in the 770 the cards occupy; the foot padding takes
   the extra height back out of the centring. */
.ns-work__intro {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 30px;
	text-align: center;
	padding: 0 clamp(16px, 15.2%, 107px);
}

/* Vertical rule with a "+" at each end, between every slide */
.ns-work__track > .ns-work__divider {
	/* The rules still span the whole band, so they opt back into stretch. */
	align-self: stretch;
	--flex-basis: 13px;
	--flex-shrink: 0;
	--flex-grow: 0;

	position: relative;
	width: 13px;
	align-self: stretch;
	color: var(--ns-ink);
}

.ns-work__divider::before {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	left: 6px;
	width: 1px;
	background: var(--ns-line-dark);
}

.ns-work__divider .ns-plus {
	left: 0;
	margin-left: 0;
}

.ns-plus--top {
	top: 0;
}

.ns-plus--bottom {
	top: auto;
	bottom: 0;
	margin-top: 0;
	margin-bottom: -6px;
}

.ns-work__slide-media {
	position: relative;
	aspect-ratio: 704 / 650;
	background: var(--ns-surface);
	overflow: hidden;
}

.ns-work__slide-media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform 0.7s var(--ns-ease);
}

.ns-work__slide:hover .ns-work__slide-media img {
	transform: scale(1.04);
}

.ns-work__slide-text {
	display: flex;
	flex-direction: column;
	gap: 10px;
	margin-top: clamp(20px, 2.6042vw, 50px);
}

.ns-work__slide-title {
	font-size: var(--ns-fs-body-xl);
	line-height: var(--ns-lh-body-xl);
	font-weight: 500;
}

/* Figma centres the 46px arrow on the right-hand content edge, 358 down the
   870 band. The step-back arrow mirrors it on the left-hand edge. */
.ns-work__next,
.ns-work__prev {
	position: absolute;
	top: 43.8%;
	z-index: 3;
	transform: translateY(-50%);
}

.ns-work__next {
	right: calc(var(--ns-gutter) - 23px);
}

.ns-work__prev {
	left: calc(var(--ns-gutter) - 23px);
}

/* The media library has no left-facing arrow and the glyph is symmetrical
   about its vertical axis, so the back arrow is the forward one mirrored.
   The flip sits on the <svg> rather than on the button, so the button's own
   hover lift is left free to animate. */
.ns-work__prev .elementor-button svg {
	transform: scaleX(-1);
}

/* Hidden while the row is still on its first panel; site.js adds
   `is-visible` as soon as it has scrolled away. It fades in and slides the
   short distance it would have travelled from behind the left edge, which
   reads as the arrow arriving with the cards rather than blinking on.
   `visibility` is held until the fade finishes so the control leaves the tab
   order while it is hidden. */
.ns-work__prev {
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease),
		visibility 0s linear var(--ns-dur-fast);
}

.ns-work__prev .elementor-button {
	transform: translateX(-8px);
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		color var(--ns-dur-fast) var(--ns-ease),
		transform var(--ns-dur-fast) var(--ns-ease);
}

.ns-work__prev.is-visible {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease),
		visibility 0s;
}

.ns-work__prev.is-visible .elementor-button {
	transform: translateX(0);
}

/* The shared hover state is a single `transform`, so the resting slide has to
   be restated here or the button would drop back to x:-8 on hover. */
.ns-work__prev.is-visible .elementor-button:hover {
	transform: translateY(-2px);
}

.ns-work__next[disabled],
.ns-work__prev[disabled] {
	opacity: 0.35;
	pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
	.ns-work__prev,
	.ns-work__prev .elementor-button {
		transition-duration: 0.01ms;
	}

	.ns-work__prev .elementor-button,
	.ns-work__prev.is-visible .elementor-button {
		transform: none;
	}
}

/* 9.1 Entrance reveal --------------------------------------------------

   The band's entrance lives with the rest of the page's motion, in
   section 15.2.1. Nothing about it is Elementor-specific or particular to
   this section's layout, and keeping every resting state in one place is
   what makes it possible to see at a glance that none of them is ever
   reachable without JavaScript. */

/* --------------------------------------------------------------------------
   10. Section 6 — About / Why Nikki Sienna
   -------------------------------------------------------------------------- */

.ns-about {
	position: relative;
	background: var(--ns-white);
	isolation: isolate;
}

/* The portrait crosses the marquee and the top of the dark panel.
   Figma centres her on the page (her box spans 596–1301 of 1920, ~11px left
   of centre), so centring plus a small nudge tracks the design at any width. */
.ns-about__portrait {
	position: absolute;
	top: clamp(40px, 6.25vw, 120px);
	left: 50%;
	z-index: 2;
	width: clamp(240px, 36.7188vw, 705px);
	transform: translateX(calc(-50% - 0.6vw));
	pointer-events: none;
}

.ns-about__portrait img {
	width: 100%;
	height: auto;
}

/* Soft glows behind her.

   These are the exported Figma ellipses, blur and all, rather than a runtime
   `filter: blur(175px)` — a blur that size is expensive to paint on every
   frame and Chrome tiles it visibly at full-page sizes. Two small SVGs give
   the same result for ~1.4KB and no compositing cost.

   Percentages are the Figma geometry expressed against the 1920 frame. */
.ns-about::before,
.ns-about::after {
	content: '';
	position: absolute;
	z-index: 0;
	pointer-events: none;
	background-repeat: no-repeat;
	background-size: 100% 100%;
}

.ns-about::before {
	top: clamp(90px, 13.54vw, 260px);
	left: 24.79%;
	width: 30.47%;
	height: clamp(340px, 52.34vw, 1005px);
	background-image: url('../img/glow-shadow.svg');
}

.ns-about::after {
	top: clamp(400px, 58.9vw, 1131px);
	left: 36.875%;
	width: 22.86%;
	height: clamp(40px, 4.64vw, 89px);
	background-image: url('../img/glow-accent.svg');
}

/* The heading is indented to clear the portrait, and the two halves of it sit
   either side of her. The gap span collapses on narrow screens. */
.ns-about__title-block {
	margin-inline-start: 30.59%;
}

/* One line at desktop, with the gap span holding the space her head occupies. */
.ns-about__title-block .elementor-heading-title {
	white-space: nowrap;
}

/* Figma sets this heading as a single string: "Creative", sixteen spaces,
   then "Partnership Starts Here". Sixteen spaces of Bigola Display measure
   2.99em, so expressing the spacer in em reproduces them exactly and keeps
   scaling with the fluid heading size. */
.ns-about__title-gap {
	display: inline-block;
	width: 2.99em;
}

/* Two 500px copy columns pinned to the outer edges of the 1520 content. */
.ns-about__body {
	align-items: flex-start;
}

.ns-about__body > .ns-about__col {
	--width: 32.895%;
	--flex-shrink: 0;
}

/* Offsets from the bottom of the heading block, per the frame. */
.ns-about__col--lead { margin-top: clamp(16px, 2.3438vw, 45px); }
.ns-about__col--support { margin-top: clamp(16px, 7.9688vw, 153px); }

/* Rounded dark panel — Figma path is a rect with 150px top corners. */
.ns-why {
	position: relative;
	background: var(--ns-ink);
	color: var(--ns-white);
	border-radius: clamp(40px, 7.8125vw, 150px) clamp(40px, 7.8125vw, 150px) 0 0;
	overflow: hidden;
	isolation: isolate;
}

/* Same decorative line work as the Stories panel, offset to sit behind the
   heading rather than the cards. */
.ns-why::before {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	background: url('../img/decor-lines.svg') no-repeat center -90px / cover;
	pointer-events: none;
}

.ns-why__grid {
	position: relative;
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	/* Columns are separated by rules, not gaps — Elementor's default container
	   gap would otherwise eat 60px and shrink each card below its 294. */
	gap: 0;
	border: 1px solid var(--ns-line-light);
	background: var(--ns-ink);
}

.ns-why__col {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 30px;
	padding: clamp(24px, 2.6042vw, 50px) clamp(16px, 2.2396vw, 43px);
	text-align: center;
}

.ns-why__col + .ns-why__col {
	border-left: 1px solid var(--ns-line-light);
}

/* "+" markers at every rule intersection, top and bottom */
.ns-why__col::before,
.ns-why__col::after,
.ns-why__grid::before,
.ns-why__grid::after {
	content: '';
	position: absolute;
	left: -7px;
	width: 13px;
	height: 13px;
	color: var(--ns-white);
	background:
		linear-gradient(currentColor, currentColor) center / 13px 1px no-repeat,
		linear-gradient(currentColor, currentColor) center / 1px 13px no-repeat;
	pointer-events: none;
}

.ns-why__col::before { top: -7px; }
.ns-why__col::after { bottom: -7px; }

.ns-why__grid::before,
.ns-why__grid::after {
	left: auto;
	right: -7px;
}

.ns-why__grid::before { top: -7px; }
.ns-why__grid::after { bottom: -7px; }

.ns-why__media {
	width: 100%;
	max-width: 294px;
	aspect-ratio: 294 / 300;
	overflow: hidden;
	background: var(--ns-surface);
}

.ns-why__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform 0.7s var(--ns-ease);
}

.ns-why__col:hover .ns-why__media img {
	transform: scale(1.05);
}

/* Pager for the mobile card carousel. site.js builds it for every grid; it
   only has a job at the breakpoint where the cards scroll, so it stays out of
   the way until then. */
.ns-why__dots {
	display: none;
	justify-content: center;
	gap: 4px;
}

.ns-why__dot {
	width: 10px;
	height: 10px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: var(--ns-white);
	opacity: 0.5;
	cursor: pointer;
	transition: opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-why__dot.is-active {
	opacity: 1;
}

.ns-why__title {
	font-size: var(--ns-fs-body-xl);
	line-height: var(--ns-lh-body-xl);
	font-weight: 500;
}

/* --------------------------------------------------------------------------
   11. Section 7 — Contact
   -------------------------------------------------------------------------- */

.ns-contact__frame {
	position: relative;
	border: 1px solid var(--ns-line-dark);
	background: var(--ns-white);
}

/* Form and photograph are equal 685 columns inside the 1420 inner width; the
   photograph is taller than the frame and hangs below it, as in Figma. */
.ns-contact__row {
	align-items: flex-start;
}

/* Two equal columns either side of the row s fixed 50px gap. Subtracting a
   fluid value here instead would make the pair wider than the row at every
   width below 1920, and they would wrap. */
.ns-contact__row > .ns-contact__col {
	--width: calc((100% - 50px) / 2);
	--flex-shrink: 0;

	min-width: 0;
}

/* Figma draws the frame 776 tall and lets the 800px photograph hang 74px past
   its bottom edge, with the rule sitting just under the photograph.

   Holding the photograph to a fixed ratio breaks that below 1920: it shrinks
   with the column while the form barely does, so it stops reaching the frame
   edge and the rule drifts far beneath it. Stretching it to the form column
   and adding the frame foot (30) plus the overhang (74) keeps every one of
   those relationships exact at any width — the photograph simply crops
   taller, which is what a cover image should do. */
.ns-contact__col--media {
	align-self: stretch;
}

.ns-contact__form-box {
	border: 1px solid var(--ns-line-dark);
	background: var(--ns-white);
}

/* The column has a definite height, so as a flex item the photograph would
   be shrunk straight back to it; it has to opt out. */
.ns-contact__col--media .ns-contact__media {
	--flex-shrink: 0;

	position: relative;
	flex-shrink: 0;
	height: calc(100% + 104px);
	overflow: hidden;
	background: var(--ns-surface);
}

.ns-contact__media-img {
	position: absolute;
	inset: 0;
}

.ns-contact__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform 0.8s var(--ns-ease);
}

.ns-contact__media:hover img {
	transform: scale(1.03);
}

.ns-contact__badge {
	position: absolute;
	top: 30px;
	left: 30px;
	z-index: 2;
}

.ns-contact__note {
	text-align: center;
}

/* --- The client's portal inquiry form ------------------------------------
   The form itself lives on the client's Dubsado portal and is framed here as
   a cross-origin document, so this block styles the frame and nothing inside
   it: the form's own type, colour and field labels are set in Dubsado's form
   designer and cannot be reached from this stylesheet.

   Alignment. The framed page brings its own padding — 30px at the sides, 60
   above the first label, 36 below the button — and that lands *inside* the
   panel's own 30px, so out of the box the fields sat 60px in from the border
   with 90px of white above them. At 430 it was worse in effect than in
   number: 30 of a 310px column is a fifth of it, and the fields came out
   250px wide inside a 390px panel, visibly narrower than everything else on
   the page. The negative margins below hand that padding back. Sideways the
   wrapper simply widens by the 30 it is reclaiming, so the frame still ends
   exactly on the panel's padding and nothing crosses the border. Vertically
   it is a crop: the wrapper clips, and the frame is pulled up past its top
   edge. What gets cropped is the framed page's own blank margin, and only
   part of it — 45 of the 60 above and 20 of the 36 below, leaving ~15px of
   slack at each end so a change to the portal's padding shows up as slightly
   looser spacing rather than a clipped label. The test in this commit asserts
   the first label and the submit button are both fully visible; re-run it if
   these values are ever retuned.

   `width: 1px` with `min-width: 100%` is the standard iOS Safari fix. Safari
   ignores a percentage width on an iframe and lays it out at its *content*
   width instead, which on a form this wide would push the panel past the
   column; a 1px declared width gives it something small enough to grow from,
   and the 100% floor gives every other browser the full column.

   The height is a placeholder, and only a placeholder. iframe-resizer writes
   the measured height inline as soon as the framed form reports it, and an
   inline style outranks this rule — so nothing here can trap the form at the
   wrong height, and nothing here may use !important. The value is what the
   panel reserves in the meantime: the frame is lazily loaded, so this is the
   height the section holds while the visitor scrolls towards it, and it is
   set to the height the live form actually settles at in this column so the
   handover is invisible. Re-measure it if the client changes the form's
   fields. 767 and below gets its own value, where the same fields are ~40px
   taller in a narrower column.
   ------------------------------------------------------------------------- */

.ns-portal-form {
	position: relative;

	/* Reclaims the framed page's 30px side padding. Also establishes the
	   block formatting context that stops the frame's negative top margin
	   collapsing out through this box, and does the vertical cropping. */
	margin-inline: -30px;
	overflow: hidden;
}

/* Two classes, not one, and every rule for this element has to keep them:
   Elementor ships `.elementor iframe { margin: 0 }`, which as a class plus an
   element outranks a lone class and silently zeroed the crop above. */
.ns-portal-form .ns-portal-form__frame {
	display: block;
	width: 1px;
	min-width: 100%;
	height: 703px;
	margin-top: -45px;
	margin-bottom: -20px;
	border: 0;
	opacity: 1;
	transition: opacity var(--ns-dur) var(--ns-ease);
}

/* --- Loading state ------------------------------------------------------
   The frame is lazily loaded and the portal is a React app behind Stripe,
   Plaid, Square and Typekit, so there is a real, visible wait — and an empty
   iframe over a reserved 703px is 703px of blank white panel. The skeleton
   fills that with the shape of the form that is coming.

   Both states are driven by one class that only site.js ever sets. That is
   the important part: the resting CSS shows the frame and hides the loader,
   so if site.js never runs there is no skeleton to be stuck behind. The
   loader is taken out of the layout entirely by `visibility`, whose
   transition is delayed until the fade has finished so it does not blink out
   from under the crossfade.
   ------------------------------------------------------------------------- */

.ns-portal-form__loader {
	position: absolute;

	/* Matches the crop above, so the skeleton's first bar lands where the
	   form's first label will and its side padding restores the 30 the
	   wrapper just reclaimed. */
	inset: 0;
	padding: 15px 30px 16px;
	opacity: 0;
	visibility: hidden;
	transition: opacity var(--ns-dur) var(--ns-ease), visibility 0s linear var(--ns-dur);
}

.ns-portal-form.is-loading .ns-portal-form__frame {
	opacity: 0;
}

.ns-portal-form.is-loading .ns-portal-form__loader {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s;
}

/* The sheen. One pass over the whole skeleton rather than an animation per
   bar: the gaps between the bars are white already, so only the bars catch
   it, and it costs a single compositor-driven transform either way. It is
   only declared inside `.is-loading`, so nothing animates on a page where the
   loader is hidden. */
.ns-portal-form.is-loading .ns-portal-form__loader::after {
	content: '';
	position: absolute;
	inset: 0;
	background: linear-gradient(100deg, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0) 70%);
	pointer-events: none;
	animation: ns-portal-sheen 1.9s var(--ns-ease) infinite;
}

@keyframes ns-portal-sheen {
	from { transform: translateX(-100%); }
	to   { transform: translateX(100%); }
}

/* Flex rather than block so the label's 5px margins are honoured instead of
   collapsing out through the row, which shortened every row by 5px and left
   the skeleton 50px adrift of the form it is standing in for. */
.ns-portal-form__row {
	display: flex;
	flex-direction: column;
	margin-bottom: 15px;
}

/* The message box: a taller field, and 20 rather than 15 beneath it. */
.ns-portal-form__row--tall {
	margin-bottom: 20px;
}

/* The submit sits at the right end of the form, so this row is the one that
   runs as a row — the rule above has already set every row to a column. */
.ns-portal-form__row--action {
	flex-direction: row;
	justify-content: flex-end;
	margin-top: 15px;
	margin-bottom: 0;
}

/* 10px of bar in the 20px the real label occupies. */
.ns-portal-form__label {
	display: block;
	height: 10px;
	margin: 5px 0;
	border-radius: 5px;
	background: rgba(7, 3, 2, 0.09);
}

.ns-portal-form__field {
	display: block;
	box-sizing: border-box;
	height: 36px;
	border: 1px solid var(--ns-line-dark);
	border-radius: var(--ns-radius-field);
	background: rgba(7, 3, 2, 0.035);
}

.ns-portal-form__row--tall .ns-portal-form__field {
	height: 80px;
}

/* The live submit measures 103 on desktop and 118 at 430; one value between
   the two is closer than a breakpoint is worth for a shape that fades out. */
.ns-portal-form__button {
	display: block;
	width: 110px;
	height: 46px;
	border-radius: var(--ns-radius-pill);
	background: rgba(7, 3, 2, 0.09);
}

/* Shown only if the frame has been in view for 12s and still has not painted
   — see initPortalForm(). It takes the skeleton's place rather than sitting
   next to it: a shape promising a form that is not coming is worse than no
   shape at all. Absolutely positioned, so the panel keeps the height it
   already reserved and nothing below it moves. */
/* Flex for the same reason the rows are: the 15px above the button and the
   15px below the last field are two separate gaps in the form, and between
   block siblings they would collapse into one.

   Filling the height and spacing between pins the first bar and the button to
   the two ends of the space actually reserved, and lets the gaps take up the
   slack in between. The bars are drawn at the form's own measurements, so on
   a normal load there are only a few pixels to share out; what this buys is
   that the skeleton still ends where the form ends if the reserved height is
   ever retuned, instead of stopping short and leaving a band of white. */
.ns-portal-form__skeleton {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	height: 100%;
	transition: opacity var(--ns-dur) var(--ns-ease);
}

.ns-portal-form.is-slow .ns-portal-form__skeleton {
	opacity: 0;
}

.ns-portal-form__retry {
	position: absolute;
	top: 50%;
	right: 30px;
	left: 30px;
	z-index: 1;
	margin: 0;
	transform: translateY(-50%);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	color: rgba(7, 3, 2, 0.55);
	text-align: center;
	text-wrap: balance;
	opacity: 0;
	visibility: hidden;
	transition: opacity var(--ns-dur) var(--ns-ease), visibility 0s linear var(--ns-dur);
}

.ns-portal-form__retry a {
	color: var(--ns-ink);
	text-decoration: underline;
	text-underline-offset: 3px;
	transition: color var(--ns-dur-fast) var(--ns-ease);
}

.ns-portal-form__retry a:hover,
.ns-portal-form__retry a:focus-visible {
	color: var(--ns-accent);
}

.ns-portal-form.is-slow .ns-portal-form__retry {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s;
}

/* A stalled panel should not also be shimmering as though something were
   still on its way. */
.ns-portal-form.is-slow .ns-portal-form__loader::after {
	animation: none;
	opacity: 0;
}

/* --- What the panel reserves, by width -----------------------------------
   Three values, because the form has three heights. Narrowing the column
   makes its long labels take a second line, and each one that wraps adds a
   flat 20px: the form is 703 wherever the labels all fit, 723 once "Tell me
   more about what you are looking for" wraps, and 743 once a second one
   does. The two thresholds were walked to the pixel — 436 still wraps and 437
   does not, 363 still wraps twice and 364 does not — so these are exact
   rather than approximately right, and the panel does not move at any width.

   These are not design breakpoints and nothing else belongs in them; they are
   here rather than in the responsive section at the foot of the file so that
   all three values, and the reason there are three, stay in one place. They
   are also the *only* three heights the form takes: the re-measure nudges in
   initPortalForm() settled what used to be a 6px flutter between runs, and
   with that gone each of these lands exactly, so the panel does not move at
   all when the form arrives. Re-measure if the client changes the fields.
   ------------------------------------------------------------------------- */

@media (max-width: 436px) {
	.ns-portal-form .ns-portal-form__frame {
		height: 723px;
	}

	/* Same wrap, in the skeleton: the bar for that label takes two lines too,
	   so the message box and the button stay level with the real ones. */
	.ns-portal-form__row--tall .ns-portal-form__label {
		height: 30px;
	}
}

@media (max-width: 363px) {
	.ns-portal-form .ns-portal-form__frame {
		height: 743px;
	}

	/* The second wrap is a different label, and singling it out in the markup
	   would buy 20px of row-by-row fidelity in a shape that is fading out.
	   The skeleton fills the height either way — `space-between` puts the 20
	   into the gaps — so it is left to do that. */
}

/* --- WPForms, restyled to the Figma form ---------------------------------
   Dormant. The inquiry form on both pages is the client's portal form above;
   the WPForms one is kept in the database but is no longer rendered, so every
   selector in this block — all of them scoped to `.wpforms-*` descendants —
   currently matches nothing. It is retained so that putting the shortcode
   back (one line in ns_contact_block()) restores the styled form with it.

   WPForms' modern markup scopes its own rules by form ID, which outranks any
   class selector here. Rather than an !important arms race, the design values
   are fed through the custom properties WPForms exposes for exactly this — so
   the plugin keeps ownership of its CSS and we only supply the tokens.
   ------------------------------------------------------------------------- */

.ns-form .wpforms-container {
	/* Fields */
	--wpforms-field-border-radius: var(--ns-radius-field);
	--wpforms-field-border-style: solid;
	--wpforms-field-border-size: 1px;
	--wpforms-field-border-color: var(--ns-line-dark);
	--wpforms-field-background-color: var(--ns-white);
	--wpforms-field-text-color: var(--ns-ink);
	--wpforms-field-menu-color: var(--ns-white);
	--wpforms-field-size-input-height: 50px;
	--wpforms-field-size-input-spacing: 5px;
	--wpforms-field-size-padding-h: 20px;
	--wpforms-field-size-font-size: var(--ns-fs-body-lg);
	--wpforms-field-size-icon-size: 1;
	--wpforms-field-size-sublabel-spacing: 5px;

	/* Labels */
	--wpforms-label-color: var(--ns-ink);
	--wpforms-label-sublabel-color: rgba(7, 3, 2, 0.5);
	--wpforms-label-error-color: #c0392b;
	--wpforms-label-size-font-size: var(--ns-fs-body-lg);
	--wpforms-label-size-line-height: var(--ns-lh-body-lg);

	/* Submit */
	--wpforms-button-background-color: var(--ns-white);
	--wpforms-button-background-color-alt: var(--ns-accent);
	--wpforms-button-text-color: var(--ns-white);
	--wpforms-button-text-color-alt: var(--ns-ink);
	--wpforms-button-border-radius: var(--ns-radius-pill);
	--wpforms-button-border-style: solid;
	--wpforms-button-border-size: 1px;
	--wpforms-button-border-color: var(--ns-line-light);
	--wpforms-button-size-height: 50px;
	--wpforms-button-size-padding-h: 30px;
	--wpforms-button-size-font-size: var(--ns-fs-body-lg);
	--wpforms-button-size-margin-top: 0;

	/* Container chrome is supplied by the Elementor box around the form. */
	--wpforms-container-padding: 0;
	--wpforms-container-border-width: 0;
	--wpforms-container-border-radius: 0;

	/* WPForms sets a 24px block margin from an ID selector. */
	margin: 0 !important;
}

.ns-form .wpforms-form {
	display: flex;
	flex-direction: column;
	gap: 30px;
}

/* Honeypot fields are taken out of flow by WPForms itself; this only guards
   against one landing in a grid cell. */
.ns-form .wpforms-field-hp,
.ns-form .wpforms-field[style*='display: none'] {
	display: none !important;
}

.ns-form .wpforms-field-container {
	display: grid;
	grid-template-columns: repeat(6, minmax(0, 1fr));
	gap: 30px 20px;
}

.ns-form .wpforms-field {
	padding: 0;
	margin: 0;
	grid-column: 1 / -1;
}

.ns-form .ns-col-half { grid-column: span 3; }
.ns-form .ns-col-third { grid-column: span 2; }

/* Containment.

   The grid tracks are already `minmax(0, 1fr)`, but the field wrappers sitting
   in them are not: a grid item's automatic minimum size is its min-content
   width, so a long placeholder, the widest option in the Project Type select
   or a validation message can push a cell wider than its track and carry the
   control out past the form box. `min-width: 0` is what stops that. It is a
   guard rather than a fix — nothing measured over its track today — but at
   430px the field already spans the box edge to edge, so there is no slack
   left to absorb it if one ever does.

   The control sizing underneath restates what WPForms already ships
   (`width: 100%`, `box-sizing: border-box`) so the field's 20px side padding
   and 1px hairline are billed *inside* the 100%, not added to it. It is
   written out here rather than left to the plugin because everything else in
   this block depends on it: without border-box each field would be exactly
   42px too wide. */
.ns-form .wpforms-field,
.ns-form .wpforms-field-row,
.ns-form .wpforms-submit-container {
	min-width: 0;
}

.ns-form .wpforms-container-full .wpforms-field input:not([type='checkbox']):not([type='radio']),
.ns-form .wpforms-container-full .wpforms-field select,
.ns-form .wpforms-container-full .wpforms-field textarea {
	box-sizing: border-box;
	width: 100%;
	max-width: 100%;
	min-width: 0;
	margin-inline: 0;
}

/* The message under an errored field is a block in the same cell; WPForms
   floors it at 120px, which is wider than the narrowest track this grid can
   produce. */
.ns-form .wpforms-container-full label.wpforms-error,
.ns-form .wpforms-container-full em.wpforms-error {
	min-width: 0;
	max-width: 100%;
}

.ns-form .wpforms-field-label {
	display: block;
	margin: 0 0 5px;
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 400;
	color: var(--ns-ink);
}

.ns-form .wpforms-required-label {
	color: var(--ns-accent);
	border: 0;
}

/* The handful of properties WPForms has no variable for. Its own rules are
   ID-scoped, so these need !important to land — deliberately kept to the
   short list below rather than applied wholesale. */

/* Row rhythm comes from the grid, not from per-field padding. */
.ns-form .wpforms-field {
	padding: 0 !important;
	margin: 0 !important;
}

/* Figma sets labels at regular weight; WPForms ships them semibold. */
.ns-form .wpforms-field-label {
	font-weight: 400 !important;
}

/* Figma's message box is four lines: 4 x 28 line-height + 10px top and
   bottom. WPForms sizes textareas from a `rows` attribute, so height has to
   be set outright rather than as a minimum. */
.ns-form textarea {
	height: 132px !important;
	min-height: 132px !important;
	padding-top: 10px !important;
	padding-bottom: 10px !important;
	resize: vertical;
}

.ns-form input::placeholder,
.ns-form textarea::placeholder {
	color: var(--ns-ink);
	opacity: 0.5;
}

/* Focus and validation.

   Both of these rings used to be drawn *outside* the field's border box, so a
   field grew the moment it was focused or failed validation:

     - this theme's keyboard ring sat at `outline-offset: 3px`, reaching 5px
       past the field on every side;
     - WPForms paints an errored field with a second ring outside the border
       (`box-shadow: 0 0 2px 0` on hover, `0 0 0 1px` on focus).

   On mobile each field already fills the form box edge to edge, so the two of
   them together read as the field bursting out of its box: the errored Email
   field showed its red hairline with a wider ring outside it, running into the
   frame's gutter. Neither ring may live outside the border box. Both are drawn
   inside it now, which leaves the field's geometry identical in every state —
   rest, hover, focus and error all measure the same.

   WPForms also paints the focused field from `--wpforms-button-background-color`:

     input:focus { border-color: var(--wpforms-button-background-color);
                   box-shadow: 0 0 0 1px var(--wpforms-button-background-color),
                               0 1px 2px rgba(0,0,0,.15);
                   outline: none; }

   That variable is the submit button's background, which this theme sets to
   white — so on a white field the border and the ring both became white and
   the field appeared to lose its outline entirely the moment it was clicked,
   leaving only the faint drop shadow at the corners. `outline: none` took the
   keyboard focus ring with it.

   So the hairline is pinned back to the Figma value and WPForms' ring is
   dropped. The field itself does not change colour on focus — the design does
   not define one.

   Error fields are excluded from this first rule: WPForms styles those red
   from a more specific selector, and !important here would paint over the
   validation state. They get their own rule underneath. */
.ns-form .wpforms-container-full input:focus:not(.wpforms-error):not(.user-invalid),
.ns-form .wpforms-container-full select:focus:not(.wpforms-error):not(.user-invalid),
.ns-form .wpforms-container-full textarea:focus:not(.wpforms-error):not(.user-invalid) {
	border-color: var(--ns-line-dark) !important;
	box-shadow: none !important;
}

/* An errored field keeps its red hairline — that plus the message underneath
   is the whole signal — but loses the halo WPForms hangs outside it on hover
   and focus, which was the ring overhanging the box on mobile. */
.ns-form .wpforms-container-full input.wpforms-error,
.ns-form .wpforms-container-full input.user-invalid,
.ns-form .wpforms-container-full select.wpforms-error,
.ns-form .wpforms-container-full select.user-invalid,
.ns-form .wpforms-container-full textarea.wpforms-error,
.ns-form .wpforms-container-full textarea.user-invalid {
	box-shadow: none !important;
}

/* Keyboard focus only, so clicking into a field stays as quiet as the frame
   draws it while tabbing through still shows where you are.

   The negative offset is what keeps the ring inside the border box. `outline`
   follows `border-radius`, so it traces the field's 16px corners rather than
   boxing them in, and sitting a pixel in from the hairline it reads as a ring
   rather than as a thicker border. */
.ns-form .wpforms-container-full input:focus-visible,
.ns-form .wpforms-container-full select:focus-visible,
.ns-form .wpforms-container-full textarea:focus-visible {
	outline: 2px solid var(--ns-accent) !important;
	outline-offset: -3px !important;
}

/* On an errored field the ring takes the validation colour, so focus does not
   momentarily repaint the field as though it had passed. */
.ns-form .wpforms-container-full input.wpforms-error:focus-visible,
.ns-form .wpforms-container-full input.user-invalid:focus-visible,
.ns-form .wpforms-container-full select.wpforms-error:focus-visible,
.ns-form .wpforms-container-full select.user-invalid:focus-visible,
.ns-form .wpforms-container-full textarea.wpforms-error:focus-visible,
.ns-form .wpforms-container-full textarea.user-invalid:focus-visible {
	outline-color: var(--wpforms-label-error-color) !important;
}

.ns-form select {
	appearance: none !important;
	-webkit-appearance: none !important;
	padding-right: 54px !important;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Cpath d='M12 15.75a1.5 1.5 0 0 1-1.06-.44l-5.3-5.3a.75.75 0 1 1 1.06-1.06L12 14.25l5.3-5.3a.75.75 0 0 1 1.06 1.06l-5.3 5.3c-.29.29-.67.44-1.06.44Z' fill='%23070302'/%3E%3C/svg%3E") !important;
	background-repeat: no-repeat !important;
	background-position: right 20px center !important;
	/* WPForms resets background-size to auto from a `container *` selector,
	   which left the chevron at a fraction of the 24px box it is drawn in. */
	background-size: 24px 24px !important;
}

.ns-form .wpforms-submit-container {
	padding: 0;
}

.ns-form button[type='submit'] {
	/* Pinned: WPForms swaps its two background variables between rest and
	   hover in a way that is easy to get backwards. */
	background-color: var(--ns-accent) !important;
	color: var(--ns-white) !important;
	box-sizing: border-box;
	width: 100% !important;
	max-width: 100% !important;
	min-width: 0;
	font-family: var(--ns-font-body);
	font-weight: 500;
	cursor: pointer;
	transition:
		background-color var(--ns-dur-fast) var(--ns-ease),
		transform var(--ns-dur-fast) var(--ns-ease),
		box-shadow var(--ns-dur-fast) var(--ns-ease);
}

/* The same lift every other button on the page makes (section 4.1): white
   ground, ink text, and the terracotta glow underneath.

   `background-image` is the load-bearing declaration here, not decoration.
   WPForms paints its own hover with the `background` *shorthand* —
   `linear-gradient(0deg, rgba(0,0,0,.2), rgba(0,0,0,.2))` layered over its
   colour variable — so pinning `background-color` alone left a 20% black wash
   sitting on top of the white, and the button came out grey with the glow
   still showing around it. The gradient is WPForms' generic "darken by a
   fifth" treatment for a button whose hover colour it does not know; this
   design states its own, so the overlay is removed rather than tinted.

   `:focus-visible` is included so the keyboard gets the state the pointer
   does, exactly as `.ns-btn` does it. */
.ns-form button[type='submit']:hover,
.ns-form button[type='submit']:focus-visible {
	background-color: var(--ns-white) !important;
	background-image: none !important;
	color: var(--ns-ink) !important;
	border-color: var(--ns-line-dark);
	transform: translateY(-2px);
	box-shadow: 0 10px 24px rgba(207, 118, 96, 0.32);
}

/* Pressed: the button settles back onto the page, like every other one.

   WPForms applies that same darkening gradient to `:hover` and `:active`
   together, so this rule has to clear it again — and it restates the colours
   because on a touchscreen `:active` fires without `:hover` ever having. */
.ns-form button[type='submit']:active {
	background-color: var(--ns-white) !important;
	background-image: none !important;
	color: var(--ns-ink) !important;
	border-color: var(--ns-line-dark);
	transform: translateY(0);
	box-shadow: none;
}

.ns-form .wpforms-confirmation-container-full {
	margin: 0;
	padding: 20px 24px;
	border: 1px solid var(--ns-line-dark);
	border-radius: var(--ns-radius-field);
	background: var(--ns-cream);
	color: var(--ns-ink);
}

/* --------------------------------------------------------------------------
   12. Footer
   -------------------------------------------------------------------------- */

.ns-footer__logo img {
	width: 214px;
	height: auto;
	max-width: none;
}

.ns-footer__nav .elementor-button,
.ns-footer__nav a {
	padding: 0;
	border: 0;
	background: none;
	color: var(--ns-white);
	font-family: var(--ns-font-body);
	font-size: var(--ns-fs-body-lg);
	line-height: var(--ns-lh-body-lg);
	font-weight: 400;
	white-space: nowrap;
	transition: opacity var(--ns-dur-fast) var(--ns-ease);
}

.ns-footer__nav a:hover,
.ns-footer__nav .elementor-button:hover {
	opacity: 0.7;
}

/* --------------------------------------------------------------------------
   13. Motion preferences
   --------------------------------------------------------------------------
   The design's motion is the hero loop, the two marquees and the hover
   transitions. Anyone who has asked their OS for less motion gets none of it.

   (Per-widget entrance animations, if they are ever wanted, are built into
   Elementor's Advanced tab — no theme code needed for that.)
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
		scroll-behavior: auto !important;
	}
}

/* --------------------------------------------------------------------------
   14. Responsive
   -------------------------------------------------------------------------- */

/* Laptop and down: the Figma desktop columns stop being readable well before
   the carousel does, so they reflow first. */
@media (max-width: 1199px) {
	.ns-stories__cols {
		flex-wrap: wrap;
	}

	.ns-stories__cols > .ns-stories__col--intro { --width: calc((100% - 50px) * 0.46); }
	.ns-stories__cols > .ns-stories__col--feature { --width: calc((100% - 50px) * 0.54); }

	.ns-stories__cols > .ns-stories__col--copy {
		--width: 100%;

		margin-top: clamp(24px, 4vw, 50px);
	}

	.ns-stories__text {
		--width: 100%;
	}

	.ns-stories__col--copy .ns-prose {
		columns: 2;
		column-gap: 50px;
	}

	/* The one-line heading stops fitting below 1200: the indent plus the
	   sixteen-space gap runs past the viewport and the last word gets clipped.
	   Centre it and let it wrap from here down, as the mobile frame does. */
	/* The portrait comes out of its overlay position too. The mobile frame
	   runs her after the copy, overlapping the marquee and the dark panel
	   below, rather than sitting across the heading. */
	.ns-about__portrait {
		position: relative;
		top: auto;
		left: auto;
		order: 3;
		width: clamp(240px, 46vw, 460px);
		margin-inline: auto;
		transform: none;
	}

	.ns-about > *:nth-child(2) { order: 1; }
	.ns-about > *:nth-child(3) { order: 2; }
	.ns-about > *:nth-child(4) { order: 4; }
	.ns-about > *:nth-child(5) { order: 5; }

	/* The glows are positioned against her old spot, so they go with her. */
	.ns-about::before,
	.ns-about::after {
		display: none;
	}

	/* Collapsed to one word-space, not removed: display:none would butt
	   "Creative" straight against "Partnership". */
	.ns-about__title-gap {
		width: 0.187em;
	}

	.ns-about__title-block {
		margin-inline-start: 0;
		text-align: center;
		align-items: center;
	}

	.ns-about__title-block .elementor-heading-title {
		white-space: normal;
	}

	.ns-about__body {
		flex-wrap: wrap;
	}

	.ns-about__body > .ns-about__col {
		--width: 100%;
	}

	.ns-about__col--lead,
	.ns-about__col--support {
		margin-top: clamp(24px, 4vw, 40px);
	}

	/* The form column gets unusably narrow before the carousel does. */
	.ns-contact__row {
		flex-wrap: wrap;
	}

	.ns-contact__row > .ns-contact__col {
		--width: 100%;
	}

	.ns-contact__col--media {
		align-self: auto;
	}

	.ns-contact__media {
		height: auto;
		aspect-ratio: 685 / 800;
	}

	/* --- Section 4 — What we create ---------------------------------- */

	/* Title only, from here down.

	   The grid stays two-up below 1200 while the cards keep their 745:500
	   ratio, so a card is only ~290-460px wide and barely 200-310px tall.
	   The longest description — Reels Booth — wraps to eight or nine lines in
	   that space: 313px of copy in a 194px card at 768. It overran the card,
	   was clipped by its own overflow, and passed under the play control on
	   the way out.

	   The mobile frame already answers this below 768, and for the same
	   reason: show the title, leave the copy for the gallery. This starts that
	   rule where the copy actually stops fitting rather than where the grid
	   happens to change. The hover wash and the play control still respond, so
	   the card reads as something to open. */
	.ns-service-card:hover .ns-service-card__desc,
	.ns-service-card:focus-within .ns-service-card__desc {
		grid-template-rows: 0fr;
		padding-top: 0;
		opacity: 0;
		transform: translateY(14px);
	}
}

/* Tablet and down: the carousel goes single file and the portrait centres. */
@media (max-width: 1024px) {
	.ns-why__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	/* The rules only run between columns, so the second row needs its own
	   top border where the grid wraps. */
	.ns-why__col:nth-child(n + 3) {
		border-top: 1px solid var(--ns-line-light);
	}

	.ns-why__col:nth-child(3) {
		border-left: 0;
	}

	/* One card per view, with the next peeking past the gutter. The viewport
	   carries a gutter of bleed, so that comes off the width first. */
	.ns-work__track > .ns-work__intro,
	.ns-work__track > .ns-work__slide {
		--width: calc(100% - var(--ns-gutter) - 50px);
	}

}

/* ==========================================================================
   Mobile — the Figma 430 frame, taken literally
   --------------------------------------------------------------------------
   Everything below reproduces the mobile artboard rather than shrinking the
   desktop one: its own type ramp, its own gutter, its own stacking order and
   its own spacing. Values are the frame's pixels; anything that has to keep
   working at 375 or 320 is expressed so it scales from there.

   Two mechanisms are used to reach past Elementor, and it is worth knowing
   which is which:

   1. Typography and gutters come out of custom properties the build wrote
      into the Elementor data (`font-size: var(--ns-fs-…)`,
      `padding: var(--ns-gutter)`). Redefining the property on an ancestor
      changes the resolved value with no specificity contest at all — that is
      why the token block below is enough to re-scale the whole page.

   2. Container padding, gap and width land on Elementor's own
      `.elementor-{post} .elementor-element.elementor-element-{id}` selector,
      which is three classes. Rules that have to beat it are prefixed
      `body .ns-page …` so they carry one more class plus a type selector.
      An editor's per-element setting made in the panel still wins over both,
      because Elementor writes those to the same selector but later.
   ========================================================================== */

@media (max-width: 767px) {

	/* --- Frame tokens ------------------------------------------------- */

	:root {
		/* The mobile frame gutters a flat 20px, not the desktop's 200/1920.
		   ns_wrap() feeds this straight into Elementor's padding control, so
		   this single value sets every content column, card inset and rule
		   length on the page. */
		--ns-gutter: 20px;

		/* The 430 frame's ramp. Only the hero keeps display-xl at 50/60;
		   every section heading in the frame is 30/40. */
		--ns-fs-display-lg: clamp(1.5rem, 6.9767vw, 1.875rem);   /* 30 */
		--ns-lh-display-lg: 1.3333;                              /* 40 */
		--ns-fs-display-md: clamp(1.5rem, 6.9767vw, 1.875rem);   /* 30 */
		--ns-lh-display-md: 1.3333;                              /* 40 */
		--ns-fs-card-title: clamp(1rem, 4.186vw, 1.125rem);      /* 18 */
		--ns-lh-card-title: 1.5556;                              /* 28 */
		--ns-lh-body-xl: 1.5556;                                 /* 18 -> 28 */
		--ns-fs-marquee: clamp(1.375rem, 6.5116vw, 1.75rem);     /* 28 */
		--ns-lh-marquee: 1.3571;                                 /* 38 */
	}

	/* --- Site header — 85px tall, logo 196 --------------------------- */

	/* Elementor sizes the logo from the widget's own width control, which is
	   one class more specific than the base rule in section 5. */
	body .ns-page .ns-site-header .ns-header__logo img {
		width: 196px;
	}

	/* Scoped to the bar: site.js clones these two into the slide-down panel,
	   and an unscoped rule hid the clones as well, leaving the menu empty. */
	.ns-site-header .ns-header__nav,
	.ns-site-header .ns-header__cta {
		display: none !important;
	}

	.ns-header__toggle-wrap {
		display: block;
	}

	/* --- Section 1 — Hero -------------------------------------------- */

	/* 932 of the 430 frame. */
	.ns-hero {
		min-height: clamp(560px, 216.7vw, 932px);
	}

	.ns-hero--page {
		min-height: clamp(360px, 139.5vw, 600px);
	}

	/* The copy block is 368 tall and starts 315 down. With the block centred
	   in the padded box, that is the padding pair below — and if the heading
	   takes an extra line at 375 it grows around its centre rather than
	   pushing the CTAs off the art. */
	body .ns-page .ns-section.ns-hero {
		--padding-top: 128px;
		--padding-bottom: 62px;
	}

	/* The frame sets the hero's standfirst at 16/26, a step below the 18/28
	   the same token carries everywhere else on this breakpoint. */
	.ns-hero__copy {
		--ns-fs-body-xl: 1rem;
		--ns-lh-body-xl: 1.625;
	}

	/* The frame breaks the standfirst after the middot: place of work on the
	   first line, reach on the second. */
	.ns-hero__sub-break {
		display: inline;
	}

	/* --- Section 2 — Client logos ------------------------------------ */

	body .ns-page .ns-section.ns-clients {
		--padding-top: 50px;
		--padding-bottom: 50px;
	}

	body .ns-page .ns-clients .ns-container {
		--gap: 20px 20px;
		--row-gap: 20px;
		--column-gap: 20px;
	}

	/* The frame runs this marquee full-bleed, with the outermost logos
	   clipped by the viewport edge — so it comes back out of the gutter its
	   heading sits in. Logo heights and the 40px rhythm are the frame's own
	   and match the desktop values exactly; only the fades are narrower. */
	.ns-clients-marquee {
		width: calc(100% + var(--ns-gutter) * 2);
		margin-inline: calc(var(--ns-gutter) * -1);
	}

	.ns-clients-marquee::before,
	.ns-clients-marquee::after {
		width: 100px;
	}

	/* --- Section 3 — Stories that move people ------------------------ */

	body .ns-page .ns-section.ns-stories {
		--padding-top: 50px;
		--padding-bottom: 50px;
	}

	/* The frame drops this heading to the 30/40 section size; the hero keeps
	   display-xl at its own 50/60. */
	.ns-stories {
		--ns-fs-display-xl: var(--ns-fs-display-lg);
		--ns-lh-display-xl: var(--ns-lh-display-lg);
	}

	/* The dark panel's white foot: the frame ends the black 112 above the
	   keynote photograph's lower edge, which is that photograph's overhang
	   plus the cover shot, the gap between them and the section's foot. */
	.ns-stories::after {
		height: 582px;
	}

	/* One column: copy, button, keynote photograph, cover photograph.
	   `display: contents` on the intro column lifts its two children into the
	   same flex context so `order` can interleave them with the other two. */
	.ns-stories__cols {
		flex-direction: column;
		align-items: center;
		text-align: center;
	}

	body .ns-page .ns-container.ns-stories__cols {
		--gap: 10px 10px;
		--row-gap: 10px;
		--column-gap: 10px;
	}

	.ns-stories__cols > .ns-stories__col--intro {
		display: contents;
	}

	.ns-stories__cols > .ns-stories__col--feature,
	.ns-stories__cols > .ns-stories__col--copy,
	.ns-stories__media--cover,
	.ns-stories__text {
		--width: 100%;

		align-items: center;
	}

	.ns-stories__text { order: 1; margin-top: 0; }
	.ns-stories__cols > .ns-stories__col--copy { order: 2; margin-top: 0; }
	.ns-stories__cols > .ns-stories__col--feature { order: 3; margin-top: 20px; }
	.ns-stories__media--cover { order: 4; margin-top: 10px; }

	.ns-stories__col--copy .ns-prose {
		columns: auto;
	}

	/* Fixed heights rather than ratios: the frame's photographs are 450 and
	   400 tall whatever the phone is, and everything below them — the seam
	   rule, the white foot — is measured from that. Narrower screens crop the
	   picture, which is what a cover image is for. */
	.ns-stories__media--feature {
		aspect-ratio: auto;
		height: 450px;
	}

	.ns-stories__media--cover {
		aspect-ratio: auto;
		height: 400px;
	}
	/* --- Seam rules --------------------------------------------------- */

	/* The frame insets every seam rule to the 20px gutter, so it starts and
	   ends level with the content above it. */
	.ns-rule-wrap {
		padding-inline: var(--ns-gutter);
	}

	/* --- Section 4 — What we create ---------------------------------- */

	body .ns-page .ns-services .ns-container {
		--padding-top: 49px;
		--gap: 30px 30px;
		--row-gap: 30px;
		--column-gap: 30px;
	}

	body .ns-page .ns-section.ns-services {
		--padding-bottom: 50px;
	}

	/* One card per row, 400 tall, 20 apart. */
	.ns-services__grid {
		grid-template-columns: minmax(0, 1fr);
		gap: 20px;
	}

	.ns-service-card {
		aspect-ratio: auto;
		height: 400px;
	}

	/* The frame shows these cards with the title only — the copy is kept
	   collapsed from 1199 down, in the block above. A card is full width here,
	   so the title gets the whole of it rather than stopping short at the
	   inset the description needed. */
	.ns-service-card .ns-service-card__text {
		right: 30px;
	}

	/* --- Section 5 — Featured work ----------------------------------- */

	/* The frame lifts the intro out of the scroller: seam rule, then the
	   tag/heading/link centred across the page, then the band of cards
	   between its own two rules. site.js moves the node; everything below
	   assumes it has (and degrades to the desktop order if it has not). */
	body .ns-page .ns-work > .e-con.ns-work__intro {
		--width: 100%;

		padding: 0 var(--ns-gutter);
		margin: 49px 0 36px;
		gap: 30px;
	}

	/* The section's own top rule is the Services/Work seam, and the frame
	   marks it at the centre like every other seam. It is built with
	   ns_rule_html('none'), so the marker is drawn here rather than shipped
	   as a span — same 13px construction as .ns-plus. */
	.ns-work > .elementor-widget-html:first-child .ns-rule::after {
		content: '';
		position: absolute;
		top: 50%;
		left: 50%;
		width: 13px;
		height: 13px;
		margin: -6px 0 0 -6px;
		background:
			linear-gradient(currentColor, currentColor) center / 13px 1px no-repeat,
			linear-gradient(currentColor, currentColor) center / 1px 13px no-repeat;
		pointer-events: none;
	}

	/* The band's own two rules. They are the gutter width — they stop at 410
	   and deliberately do not meet the divider at 426, exactly as the frame
	   draws them — so they are painted on the content column rather than
	   being full-bleed borders. */
	body .ns-page .ns-work .ns-container {
		--padding-top: 31px;
		--padding-bottom: 29px;

		position: relative;
	}

	.ns-work .ns-container::before,
	.ns-work .ns-container::after {
		content: '';
		position: absolute;
		right: var(--ns-gutter);
		left: var(--ns-gutter);
		height: 1px;
		background: var(--ns-line-dark);
		pointer-events: none;
	}

	.ns-work .ns-container::before { top: 0; }
	.ns-work .ns-container::after { bottom: 0; }

	/* The track bleeds a full gutter past the right edge so the divider and
	   its markers — 420 to 433 in the frame — are clipped by the viewport
	   rather than by the scroller. The padding/margin pair gives the markers
	   room to hang the full height of the band without overflow-y cutting
	   them off. */
	body .ns-page .ns-work .ns-work__viewport {
		--width: calc(100% + var(--ns-gutter) * 2);

		padding-block: 31px 29px;
		margin-block: -31px -29px;
	}

	body .ns-page .ns-work .ns-work__track {
		--gap: 30px 30px;
		--row-gap: 30px;
		--column-gap: 30px;

		/* Section 9 sets this as a physical gap, which outranks Elementor's
		   var, so the mobile value is stated the same way. */
		gap: 30px;
		align-items: stretch;
		min-height: 0;
	}

	.ns-work__track > .ns-work__slide {
		--width: 370px;
	}

	/* With the intro gone the row opens on a divider; the frame opens on the
	   card. (Without JS the intro is still first and this matches nothing,
	   which is the graceful outcome.) */
	.ns-work__track > .ns-work__divider:first-child {
		display: none;
	}

	/* The rules run the full 578 of the band, past the track's own box at
	   both ends, which is what puts their markers on the two seam rules. */
	body .ns-page .ns-work__track > .ns-work__divider {
		margin-block: -31px -29px;
	}

	.ns-link .elementor-button-icon svg,
	.ns-link .elementor-button-icon img {
		width: 24px;
		height: 24px;
	}

	.ns-work__slide-media {
		aspect-ratio: auto;
		height: 400px;
	}

	.ns-work__slide-text {
		gap: 6px;
		margin-top: 30px;
	}

	/* Centred on the photograph's right edge, level with its middle; the back
	   arrow takes the matching left edge. The mobile track bleeds a gutter at
	   both ends, so both arrows sit on the content column rather than the
	   scroller's own box. */
	.ns-work__next,
	.ns-work__prev {
		top: auto;
		bottom: 325px;
		transform: none;
	}

	.ns-work__next {
		right: var(--ns-gutter);
	}

	.ns-work__prev {
		left: var(--ns-gutter);
	}

	/* --- Section 6a — About ------------------------------------------ */

	body .ns-page .ns-section.ns-about {
		--padding-top: 50px;
	}

	/* The frame breaks this heading "Creative Partnership / Starts Here".
	   Greedy wrapping would carry "Starts" up onto the first line at 430, so
	   the line box is balanced instead — which lands on the frame's break
	   without hard-coding one. */
	.ns-about__title-block .elementor-heading-title {
		text-wrap: balance;
	}

	/* Four paragraphs, then the button. The two copy columns become
	   `display: contents` so their paragraphs and the button share one flex
	   context, which is what lets the button move to the end of the run. */
	body .ns-page .ns-container.ns-about__body {
		--gap: 10px 10px;
		--row-gap: 10px;
		--column-gap: 10px;
		--justify-content: center;

		margin-top: 11px;
		align-items: center;
		text-align: center;
	}

	.ns-about__body > .ns-about__col {
		display: contents;
	}

	/* A descendant selector, not a child one: display:contents changes the
	   box tree, not the DOM, so the button is still its column's child. */
	.ns-about__body .ns-btn {
		order: 3;
		margin-top: 20px;
	}

	.ns-about::before,
	.ns-about::after {
		display: block;
	}

	.ns-about::before {
		top: 494px;
		left: -406px;
		width: 1049px;
		height: 1299px;
	}

	.ns-about::after {
		top: 1264px;
		left: -48px;
		width: 462px;
		height: 253px;
	}

	/* She is 420 wide, hangs 14 off the left edge and runs down over the
	   marquee into the top of the dark panel. */
	body .ns-page .elementor-widget.ns-about__portrait {
		align-self: flex-start;
		width: 420px;
		max-width: 100%;
		margin: 23px 0 -464px -14px;
	}

	/* --- Section 6b — Why Nikki Sienna ------------------------------- */

	/* The panel opens far enough down to clear her legs, which the frame runs
	   over its top edge. */
	body .ns-page .ns-section.ns-why {
		--padding-top: 246px;
		--padding-bottom: 50px;
	}

	body .ns-page .ns-why .ns-container {
		--gap: 30px 30px;
		--row-gap: 30px;
		--column-gap: 30px;
	}

	/* The frame stacks tag, heading, cards, pager, button. Dissolving the
	   head row puts its two halves in the same flex context as the grid, so
	   the button can take its place at the end. */
	.ns-why .ns-container > .e-con:first-child {
		display: contents;
	}

	body .ns-page .ns-why .ns-container > .e-con:first-child > .e-con {
		--gap: 10px 10px;
		--row-gap: 10px;
		--column-gap: 10px;

		order: 1;
		align-items: center;
		text-align: center;
	}

	.ns-why__grid { order: 2; }
	.ns-why__dots { order: 3; }
	.ns-why .ns-container > .e-con:first-child > .ns-btn {
		order: 4;
		align-self: center;
	}

	/* One card per view inside the bordered box, the next card's rule showing
	   at 374 exactly as the frame has it.

	   The box's three visible rules are painted as background layers instead
	   of a border, and the box carries 7px of padding all round. Both are for
	   the same reason: a scroll container clips everything that overhangs its
	   padding box, and the "+" markers are built to straddle the rules they
	   mark — as a bordered box they were cut in half at the top and lost
	   altogether at the sides. The padding gives them room to sit inside the
	   clip; the negative margins take it back out of the layout, so the rules
	   still land on 7737 and 8289 and the pager still sits 30 below.

	   Backgrounds do not scroll with a container's content, which is what
	   keeps these three rules pinned to the visible box while the cards and
	   their own dividing rules move behind them. */
	body .ns-page .e-con.ns-why__grid {
		--width: calc(100% + 7px);

		display: flex;
		flex-direction: row;
		flex-wrap: nowrap;
		overflow-x: auto;
		scroll-snap-type: x mandatory;
		/* Snapping measures from the padding box, so without this the row would
		   come to rest 7px into its own gutter. */
		scroll-padding-left: 7px;
		scrollbar-width: none;
		-ms-overflow-style: none;

		border: 0;
		padding: 7px 0 7px 7px;
		margin: -6px 0 -6px -7px;

		background-image:
			linear-gradient(var(--ns-line-light), var(--ns-line-light)),
			linear-gradient(var(--ns-line-light), var(--ns-line-light)),
			linear-gradient(var(--ns-line-light), var(--ns-line-light));
		background-size:
			calc(100% - 7px) 1px,
			calc(100% - 7px) 1px,
			1px calc(100% - 12px);
		background-position:
			7px 6px,
			7px calc(100% - 6px),
			7px 6px;
		background-repeat: no-repeat;
	}

	.ns-why__grid::-webkit-scrollbar {
		display: none;
	}

	/* The box's own right-hand markers belong to the far end of the row, four
	   cards off-screen; anchored to the scroller's padding box they would
	   draw a cross the frame does not have at the viewport edge. Every card
	   still carries the pair on its own left rule. */
	.ns-why__grid::before,
	.ns-why__grid::after {
		display: none;
	}

	body .ns-page .e-con.ns-why__col {
		/* 354 puts the next card's rule on 374, where the frame has it. */
		--width: calc(100% - 36px);
		--flex-shrink: 0;
		--flex-grow: 0;
		--flex-basis: auto;

		/* The box's 1px border sits inside the 30 the frame measures. */
		padding: 29px 30px;
		scroll-snap-align: start;
	}

	/* Without the box's border the card's own left edge is the rule, so the
	   marker shifts a pixel to stay centred on it. */
	.ns-why__col::before,
	.ns-why__col::after {
		left: -6px;
	}

	/* Undoes the wrapped-grid seam from the tablet breakpoint: at this width
	   the cards are one row again. */
	.ns-why__col:nth-child(n + 3) {
		border-top: 0;
	}

	.ns-why__col:nth-child(3) {
		border-left: 1px solid var(--ns-line-light);
	}

	.ns-why__media {
		max-width: none;
		aspect-ratio: auto;
		height: 300px;
	}

	.ns-why__dots {
		display: flex;
	}
	/* --- Section 7 — Contact ----------------------------------------- */

	body .ns-page .ns-section.ns-contact {
		--padding-top: 50px;
		/* The closing rule is pulled back up inside the frame (below), so the
		   section's own foot carries both its 50 and the 50 that pull left
		   behind. */
		--padding-bottom: 100px;
	}

	/* Frame and form box both inset 20 at this width rather than 50/30. The
	   deep foot is the frame carrying on below the photograph, which is where
	   the closing rule sits. */
	body .ns-page .e-con.ns-contact__frame {
		--padding-top: 19px;
		--padding-right: 19px;
		--padding-bottom: 80px;
		--padding-left: 19px;
	}

	body .ns-page .ns-contact__form-box.ns-form {
		--padding-top: 20px;
		--padding-right: 19px;
		--padding-bottom: 19px;
		--padding-left: 19px;
	}

	.ns-contact__row {
		flex-direction: column;
	}

	body .ns-page .e-con.ns-contact__row {
		--gap: 30px 30px;
		--row-gap: 30px;
		--column-gap: 30px;
	}

	body .ns-page .ns-contact__col.ns-contact__col--form {
		--gap: 30px 30px;
		--row-gap: 30px;
		--column-gap: 30px;
	}

	.ns-contact__row > .ns-contact__col {
		--width: 100%;
	}

	.ns-contact__col--media {
		align-self: auto;
	}

	.ns-contact__col--media .ns-contact__media {
		height: 400px;
		aspect-ratio: auto;
	}

	.ns-contact__badge {
		top: 20px;
		left: 20px;
	}

	.ns-contact__badge .elementor-button,
	.ns-site-footer .ns-icon-btn .elementor-button {
		width: 44px;
		height: 44px;
	}

	/* Nothing for the portal form here on purpose. Reclaiming the framed
	   page's own 30px side padding gave the column 60px back, which is enough
	   that its labels still fit on one line well below this breakpoint — the
	   form keeps its desktop height down to 437, and what it reserves is set
	   in one place beside the rest of that block, not here. */

	/* Form: 46px fields on a 92px pitch, label 4 above its input. */
	.ns-form .wpforms-container {
		--wpforms-field-size-input-height: 46px;
		--wpforms-button-size-height: 48px;
		--wpforms-button-size-font-size: var(--ns-fs-card-title);
	}

	.ns-form .wpforms-field-container {
		gap: 16px;
	}

	.ns-form .wpforms-field-label {
		margin-bottom: 4px !important;
	}

	.ns-form textarea {
		height: 124px !important;
		min-height: 124px !important;
	}

	.ns-form .ns-col-half,
	.ns-form .ns-col-third {
		grid-column: 1 / -1;
	}

	/* The closing rule runs the frame's inner width and carries its marker at
	   the left end. The frame keeps going below it, so the rule is pulled
	   back up into the foot it would otherwise sit under. */
	body .ns-page .ns-section.ns-contact > .elementor-widget:has(.ns-rule-wrap--short) {
		margin-top: -51px;
	}

	.ns-rule-wrap--short {
		padding-inline: 40px;
	}

	.ns-rule-wrap--short .ns-rule {
		width: 100%;
	}

	/* The Contact page reuses this section, but its closing rule is the plain
	   centred variant and stays below the frame instead of being pulled up into
	   it — so none of the three compensations above apply there. Left alone, that
	   page stacked the frame's deep foot, the desktop 120 above the rule and the
	   100 below it into roughly 300px of empty column between the form and the
	   client strip. These put it back on the 50 the rest of the breakpoint runs
	   on, and leave the home page's arrangement exactly as it was. */
	body .ns-page .ns-section.ns-contact:has(> .elementor-widget .ns-rule-wrap:not(.ns-rule-wrap--short)) {
		--padding-bottom: 50px;
	}

	/* Nothing is pulled up into the frame's foot here, so it closes on the same
	   19 its other three sides use rather than holding 80px of dead white. */
	body .ns-page .ns-section.ns-contact:has(> .elementor-widget .ns-rule-wrap:not(.ns-rule-wrap--short)) .ns-contact__frame {
		--padding-bottom: 19px;
	}

	body .ns-page .ns-section.ns-contact > .elementor-widget:has(.ns-rule-wrap:not(.ns-rule-wrap--short)) {
		margin-top: 50px;
	}

	/* --- Footer ------------------------------------------------------- */

	body .ns-page .ns-site-footer .ns-dark {
		--padding-top: 50px;
		--padding-bottom: 30px;
	}

	body .ns-page .ns-site-footer .ns-container {
		--gap: 25px 25px;
		--row-gap: 25px;
		--column-gap: 25px;
	}

	/* The links run four across and wrap to a second line, 10 below. */
	body .ns-page .ns-site-footer .ns-container > .ns-footer__nav {
		--gap: 10px 30px;
		--row-gap: 10px;
		--column-gap: 30px;
	}

	/* The legal links carry the same class one level deeper; the frame centres
	   them under the rule rather than ranging them right. */
	body .ns-page .ns-site-footer .ns-container .ns-footer__nav .ns-footer__nav,
	body .ns-page .ns-site-footer .ns-container > .e-con:last-child .ns-footer__nav {
		--justify-content: center;
	}

	/* Elementor caps the tagline at the frame's desktop 440. */
	.ns-site-footer .ns-container > .elementor-widget {
		max-width: 100%;
	}

	/* The frame sets the tagline 350 wide, which is what breaks it after
	   “moments,” rather than carrying “energy” up onto the first line. */
	body .ns-page .ns-site-footer .ns-container > .elementor-widget-text-editor {
		max-width: 350px;
	}

	body .ns-page .ns-site-footer .ns-container > .e-con:last-child {
		--margin-top: 25px;
	}

	/* Legal line above the copyright, both centred. */
	body .ns-page .ns-site-footer .ns-container > .e-con:last-child > .e-con {
		--gap: 10px 10px;
		--row-gap: 10px;
		--column-gap: 10px;

		flex-direction: column-reverse;
		align-items: center;
		text-align: center;
	}
}

/* Narrower than the frame: the two hero CTAs are 353 wide together, which is
   exactly what 430 has room for. Trimming their padding keeps them on the one
   row the frame gives them down to about 350; below that they wrap, which is
   the right answer once they genuinely stop fitting. */
@media (max-width: 429px) {
	body .ns-page .ns-hero__inner .ns-btn .elementor-button {
		padding-inline: 18px;
	}
}

/* Very small phones — the 315px floor in the brief. */
@media (max-width: 380px) {
	body .ns-page .ns-site-header .ns-header__logo img {
		width: 150px;
	}

	.ns-tag {
		font-size: 0.875rem;
		padding-right: 12px;
	}

	.ns-tag__mark {
		width: 28px;
		height: 28px;
		padding: 5px;
	}

	.ns-btn .elementor-button,
	a.ns-btn {
		padding: 10px 20px;
		font-size: 0.9375rem;
	}
}

/* ==========================================================================
   15. Motion
   --------------------------------------------------------------------------
   Everything in this section is enhancement. Remove it, or arrive with JS
   off, and the page is the Figma design exactly as sections 1–14 draw it —
   nothing here changes a resting position, a size, a colour or a crop.

   It is built in three layers, in order of how much a browser has to support
   before it does anything at all:

     1. Entrance reveals — IntersectionObserver plus a class. Universal.
     2. Section links — `scroll-behavior` and `scroll-margin-top`. Universal.
     3. Scroll-linked drift — CSS scroll-driven animations, behind
        @supports. A browser without them simply gets a still page, which is
        the design; nothing is faked in JavaScript to make up for it.

   Two rules govern the whole section:

   **Transitions are declared on the end state, never on the resting one.**
   A transition runs from the *after-change* style, so declaring it only on
   `.is-revealed` means the moment site.js adds `.ns-reveal--x` the element
   snaps to its resting position, and `.is-revealed` is the only thing that
   ever animates. Put the transition on the resting rule instead and the
   whole page visibly fades *out* over a second at load before it comes back.

   **`transform` belongs to hover; `translate` and `scale` belong to motion.**
   Sections 4–11 already spend `transform` on the hover zooms. The individual
   `translate` and `scale` properties compose with it rather than clobbering
   it, so a pointer landing on a card mid-entrance does the right thing. Where
   a reveal does use `transform` it is on an element that has no hover state
   of its own — the card, not the photograph inside it.
   ========================================================================== */

/* --------------------------------------------------------------------------
   15.1 Section links
   --------------------------------------------------------------------------
   The menu points at /#work, /#services and /#about — same path, so these
   are in-page jumps. Two things were missing and both are one declaration:
   the jump was instant, and it landed the section under the fixed header
   rather than below it.

   Setting them here rather than in the click handler means every route into
   a section behaves the same way: the menu, the footer, a pasted URL, the
   browser's back button, and a `find in page` landing.
   -------------------------------------------------------------------------- */

:root {
	/* The bar's own height, so the offset below tracks it. */
	--ns-header-h: 89px;
}

html {
	scroll-behavior: smooth;
}

.ns-section[id],
.ns-page [id]:target {
	scroll-margin-top: calc(var(--ns-header-h) + 10px);
}

/* The menu already draws an underline under the link the pointer is on, and
   under `.current-menu-item` for the page you are on. A one-page menu has no
   current page to mark, so site.js lights the same underline for the section
   currently filling the window — the design's own affordance, used for the
   one state it was missing. */
.ns-header__nav a.is-current::after {
	transform: scaleX(1);
}

/* --------------------------------------------------------------------------
   15.2 Entrance reveals
   --------------------------------------------------------------------------
   Nothing below is reachable unless site.js has put `.ns-reveal--<variant>`
   on the element. It adds the class immediately before observing, and skips
   the whole routine under prefers-reduced-motion and in the Elementor
   editor — so with JS off the page is simply there, fully painted.

   `--ns-reveal-delay` is set per element by site.js: elements that cross the
   line in the same frame come in one behind the other, an element scrolled
   in on its own starts straight away. `--ns-step` is the designed sequence
   *inside* a group, also set by site.js, and the two add up.
   -------------------------------------------------------------------------- */

/* `--ns-reveal-in` is the beat an element actually starts on. It has to be
   composed once per element that uses it rather than once on the group: a
   custom property is substituted where it is *declared*, so a single
   declaration on the container would bake in the container's `--ns-step`
   (nothing) and inherit that same answer down to every part. */
.ns-reveal,
.ns-reveal--head .elementor-widget {
	--ns-reveal-in: calc(var(--ns-reveal-delay, 0ms) + var(--ns-step, 0ms));
}

.ns-reveal {
	--ns-reveal-delay: 0ms;
}

/* --- rise: the plain case -------------------------------------------------
   Copy, buttons, cards, footer columns. A long tail with almost no
   acceleration at the end is what separates a settle from a slide. */

.ns-reveal--rise {
	opacity: 0;
	transform: translateY(26px);
}

.ns-reveal--rise.is-revealed {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal) var(--ns-ease-reveal) var(--ns-reveal-in),
		transform var(--ns-dur-reveal) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* --- fade: for things that are already in motion --------------------------
   The two marquees. They are travelling sideways the whole time they are on
   screen; lifting them as well would read as a second, competing gesture. */

.ns-reveal--fade {
	opacity: 0;
}

.ns-reveal--fade.is-revealed {
	opacity: 1;
	transition: opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* --- frame: a photograph in its crop --------------------------------------
   The crop rises and settles as one block. The picture inside it is left
   alone here — 15.3 gives it its own, slower travel, and the difference
   between the two is what gives the entrance depth. */

.ns-reveal--frame {
	opacity: 0;
	transform: translateY(34px);
}

.ns-reveal--frame.is-revealed {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in),
		transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* --- head: tag, heading, link ---------------------------------------------
   Every section opens with the same three-part group, and it is revealed as
   one element with the order between its parts set here rather than by the
   observer. The observer's stagger only orders things that crossed the line
   in the same frame, and these do not: the heading is taller than the tag
   and sits above the link, so they cross at three different scroll positions
   and would arrive in whatever order the page geometry chose. This is a
   designed sequence, so it is stated as one — site.js numbers the parts and
   the step below turns that number into a beat. */

.ns-reveal--head .elementor-widget {
	opacity: 0;
	transform: translateY(18px);
}

.ns-reveal--head.is-revealed .elementor-widget {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal) var(--ns-ease-reveal) var(--ns-reveal-in),
		transform var(--ns-dur-reveal) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* A display serif at this size reads as a shape before it reads as words, so
   it resolves out of a small defocus rather than sliding into place — the
   blur is what makes the entrance feel considered instead of scripted. Kept
   to the one heading per group: it is a filter, and filters cost per
   element. */
.ns-reveal--head .elementor-widget-heading {
	filter: blur(4px);
}

.ns-reveal--head.is-revealed .elementor-widget-heading {
	filter: blur(0);
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in),
		transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in),
		filter var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* --- portrait -------------------------------------------------------------
   She is positioned by a transform of her own (`translateX(-50% - 0.6vw)`,
   section 10), which the reveal must not overwrite — so this one rides the
   independent `translate` property and the two compose. */

.ns-reveal--portrait {
	opacity: 0;
	translate: 0 40px;
}

.ns-reveal--portrait.is-revealed {
	opacity: 1;
	translate: none;
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in),
		translate var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-in);
}

/* --- rule: the seam between two sections ----------------------------------
   Opens from its centre outwards. Clipping rather than scaling keeps the "+"
   marker some of these lines carry perfectly square while it happens.

   The clip goes on the rule and not on `.ns-rule-wrap` around it, even
   though the wrapper is the element carrying the class: an
   IntersectionObserver measures a clipped target through its clip, so a
   wrapper closed to zero width reports a ratio of 0 for ever and the seam
   never gets the class that would open it again. Clipping the child keeps
   the observed box whole.

   A polygon rather than the obvious `inset()`, and that is not a preference:
   `.ns-rule` is one pixel tall, and the "+" it carries is a 13px child
   hanging six pixels past the line either way. `inset()` can only take its
   clip from the border box, so it cuts the marker's vertical arms off and
   leaves a dash where the cross should be. A polygon's corners may sit
   outside the box, which lets the wipe stay strictly horizontal and leaves
   the marker — and the pseudo-element marker the mobile frame draws on the
   Services/Work seam — untouched. */

.ns-reveal--rule .ns-rule {
	clip-path: polygon(50% -10px, 50% -10px, 50% calc(100% + 10px), 50% calc(100% + 10px));
}

.ns-reveal--rule.is-revealed .ns-rule {
	clip-path: polygon(0 -10px, 100% -10px, 100% calc(100% + 10px), 0 calc(100% + 10px));
	transition: clip-path var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay);
}

/* The one seam that is drawn from its left end rather than its centre: the
   contact rule carries its marker at the start, and a centre-out wipe would
   leave that marker hanging in space until the line caught up with it. */
.ns-rule-wrap--short.ns-reveal--rule .ns-rule {
	clip-path: polygon(0 -10px, 0 -10px, 0 calc(100% + 10px), 0 calc(100% + 10px));
}

/* --------------------------------------------------------------------------
   15.2.1 Featured work — the band
   --------------------------------------------------------------------------
   The band is one long horizontal row, so a single section-level reveal
   would fire once and leave every panel past the right edge already finished
   by the time the reader scrolls the row to it. site.js observes each panel
   on its own against the window instead: a card off to the right is clipped
   by the scroller and never intersects until the row is actually moved to
   it, so the vertical entrance and the horizontal one come out of the same
   observer.
   -------------------------------------------------------------------------- */

/* The rules between the cards draw downwards, and their two markers land at
   the ends the line reaches: the top one just after it sets off, the bottom
   one as it arrives. */
.ns-reveal--divider::before {
	transform: scaleY(0);
	transform-origin: top center;
}

.ns-reveal--divider.is-revealed::before {
	transform: scaleY(1);
	transition: transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay);
}

.ns-reveal--divider .ns-plus {
	opacity: 0;
	transform: scale(0.4);
}

.ns-reveal--divider .ns-plus--top {
	--ns-plus-delay: 120ms;
}

.ns-reveal--divider .ns-plus--bottom {
	--ns-plus-delay: 700ms;
}

.ns-reveal--divider.is-revealed .ns-plus {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-fast) var(--ns-ease-reveal) calc(var(--ns-reveal-delay) + var(--ns-plus-delay, 0ms)),
		transform var(--ns-dur) var(--ns-ease-reveal) calc(var(--ns-reveal-delay) + var(--ns-plus-delay, 0ms));
}

/* The photograph rises and settles; the caption follows a beat later, so the
   card resolves top-down rather than arriving as one block. */
.ns-reveal--slide .ns-work__slide-media {
	opacity: 0;
	transform: translateY(34px);
}

.ns-reveal--slide.is-revealed .ns-work__slide-media {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay),
		transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay);
}

/* The image eases back out of a slight over-scale inside its own frame, half
   a second longer than the card that carries it — the small parallax between
   the two is what gives the reveal its depth.

   It rides the independent `scale` property rather than `transform`, which
   the hover zoom already owns; the two compose instead of clobbering one
   another. The hover transition has to be restated alongside, since this
   rule outranks the one that declares it. */
.ns-reveal--slide .ns-work__slide-media img {
	scale: 1.08;
}

.ns-reveal--slide.is-revealed .ns-work__slide-media img {
	scale: 1;
	transition:
		transform 0.7s var(--ns-ease),
		scale 1.6s var(--ns-ease-reveal) var(--ns-reveal-delay);
}

.ns-reveal--slide .ns-work__slide-text {
	opacity: 0;
	transform: translateY(20px);
}

.ns-reveal--slide.is-revealed .ns-work__slide-text {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal) var(--ns-ease-reveal) calc(var(--ns-reveal-delay) + 180ms),
		transform var(--ns-dur-reveal) var(--ns-ease-reveal) calc(var(--ns-reveal-delay) + 180ms);
}

/* The forward arrow fades in last, once there is a row for it to step.

   Its fixed tail offset is not decoration: the arrow sits over the gap
   between the first two cards, so it clears the observer's threshold before
   they do and would otherwise appear on its own over an empty band, a
   control for a row that is not there yet.

   Opacity only — this control is positioned with a transform at desktop and
   without one at mobile, and the `[disabled]` state is left to win. */
.ns-reveal--control:not([disabled]) {
	opacity: 0;
}

.ns-reveal--control.is-revealed:not([disabled]) {
	opacity: 1;
	transition: opacity var(--ns-dur-reveal) var(--ns-ease-reveal) calc(var(--ns-reveal-delay) + 420ms);
}

/* --------------------------------------------------------------------------
   15.3 Scroll-linked drift
   --------------------------------------------------------------------------
   Depth, not decoration: a photograph that travels a little slower than the
   frame it is cropped into, and a hero that keeps pushing in after the copy
   has left. Both are driven by the scroll position rather than by a clock,
   so they are reversible, interruptible, and stay exactly in step with the
   reader's own hand.

   All of it runs on scroll-driven animations, off the main thread. A browser
   without them gets the design, still — that is the whole fallback, and it
   is why none of this is reproduced in JavaScript.

   The one rule every keyframe here obeys: **at the neutral point the
   animation must be the identity.** A photograph centred in the window sits
   at `translate: 0` and `scale: 1`, which is its Figma crop to the pixel.
   The drift only exists on the way past.
   -------------------------------------------------------------------------- */

@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {

		/* --- Framed photographs -------------------------------------------
		   Every photograph on this site is pre-cropped to its frame's exact
		   aspect ratio, so `object-fit: cover` has no slack to pan into and
		   `object-position` cannot move the picture at all. The travel has to
		   come with its own cover, which is what couples the scale to it: at
		   an offset of 3.4% of the frame's height, a scale of 1.068 is
		   exactly enough to keep the far edge covered. Both reach zero
		   together at the halfway point, so the crop the reader stops on is
		   the crop the design specifies.

		   The timeline is declared on the frame and referenced by name from
		   the picture, rather than the picture using an anonymous `view()`.
		   The frames clip their overflow, which makes each one a scroll
		   container in its own right — an anonymous timeline would measure
		   the picture inside a box that never scrolls, and sit at 0 for ever. */

		@keyframes ns-drift {
			from {
				translate: 0 -3.4%;
				scale: 1.068;
			}

			50% {
				translate: 0 0;
				scale: 1;
			}

			to {
				translate: 0 3.4%;
				scale: 1.068;
			}
		}

		.ns-stories__media,
		.ns-service-card__media,
		.ns-why__media,
		.ns-contact__media {
			view-timeline-name: --ns-frame;
		}

		.ns-stories__media img,
		.ns-service-card__media img,
		.ns-why__media img,
		.ns-contact__media img {
			animation: ns-drift linear both;
			animation-timeline: --ns-frame;
			animation-range: cover 0% cover 100%;
		}

		/* The work band's photographs are left out. Their `scale` is already
		   spoken for by the entrance settle in 15.2.1, and a scroll-driven
		   animation and a transition cannot both own one property without
		   one of them stuttering as the other hands it back. */

		/* --- The hero -----------------------------------------------------
		   The footage keeps pushing in as the section leaves, and the copy
		   lifts away ahead of it. Both are ranged over `exit`, which starts
		   the moment the hero's top edge reaches the top of the window — so
		   at rest, before a single pixel has been scrolled, this is the
		   identity and the hero is the frame as drawn.

		   The poster and the video are otherwise untouched: the poster is
		   the page's Largest Contentful Paint element, and anything that
		   delays its first paint costs more than this is worth. A scale is
		   free — it is composited, and it starts at 1. */

		@keyframes ns-hero-push {
			to {
				scale: 1.14;
			}
		}

		@keyframes ns-hero-lift {
			to {
				opacity: 0;
				translate: 0 -14%;
			}
		}

		.ns-hero {
			view-timeline-name: --ns-hero;
		}

		.ns-hero__media {
			animation: ns-hero-push linear both;
			animation-timeline: --ns-hero;
			animation-range: exit 0% exit 100%;
		}

		.ns-hero__inner {
			animation: ns-hero-lift linear both;
			animation-timeline: --ns-hero;
			/* Gone by two thirds of the way out, so the copy has cleared the
			   frame well before the section does and never sits half-faded
			   against the white of the band below. */
			animation-range: exit 0% exit 62%;
		}

		/* --- The glows behind the portrait --------------------------------
		   Two exported Figma ellipses with nothing behind or in front of them
		   to line up against, so they are the one place on the page where a
		   larger, slower travel costs nothing at all. */

		@keyframes ns-glow-drift {
			from {
				translate: 0 -5%;
			}

			to {
				translate: 0 5%;
			}
		}

		.ns-about {
			view-timeline-name: --ns-about;
		}

		.ns-about::before,
		.ns-about::after {
			animation: ns-glow-drift linear both;
			animation-timeline: --ns-about;
			animation-range: cover 0% cover 100%;
		}
	}
}

/* --------------------------------------------------------------------------
   15.4 Marquees
   --------------------------------------------------------------------------
   A reader who stops on a client's mark is trying to read it. Pausing —
   rather than slowing — is what keeps the row exactly where they stopped it:
   `animation-play-state` holds the current position, while changing the
   duration would re-map the elapsed time onto a new cycle and jump.

   Pointer devices only. On a touchscreen the "hover" is a tap on the way to
   something else, and a row that froze under a finger would look broken.
   -------------------------------------------------------------------------- */

@media (hover: hover) and (pointer: fine) {
	.ns-marquee:hover .ns-marquee__track {
		animation-play-state: paused;
	}
}

/* --------------------------------------------------------------------------
   15.5 The hero's first paint
   --------------------------------------------------------------------------
   Every other reveal on the page is below the fold, so the frame or two
   between the browser painting an element and site.js hiding it is never
   seen. The hero is the exception: it is the first thing on screen, and
   hiding it at DOMContentLoaded means painting it, then removing it, then
   bringing it back — a flash.

   `ns-motion` is set on <html> by a short inline script in <head>
   (nikki_sienna_motion_gate() in inc/seo.php's neighbour, functions.php), so
   the resting state is in force from the very first paint, before any of
   this stylesheet's other work matters. The same script drops the class
   again after 2.6 seconds, so a visitor whose copy of site.js never arrives
   gets the hero unanimated rather than blank.

   Only the copy is held. The poster underneath it paints immediately.
   -------------------------------------------------------------------------- */

/* Two hero layouts, one sequence: the home page wraps its three lines in
   `.ns-hero__copy` and sets the button pair beside it, while an interior page
   hangs its tag and heading straight off `.ns-hero__inner`. Both shapes are
   named so the entrance does not depend on which one a page uses. */
.ns-motion .ns-hero__copy > .elementor-widget,
.ns-motion .ns-hero__inner > .elementor-widget,
.ns-motion .ns-hero__inner > .e-con:not(.ns-hero__copy) {
	opacity: 0;
	transform: translateY(22px);
}

.ns-hero__copy > .elementor-widget.is-revealed,
.ns-hero__inner > .elementor-widget.is-revealed,
.ns-hero__inner > .e-con.is-revealed {
	opacity: 1;
	transform: none;
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay, 0ms),
		transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay, 0ms);
}

/* The H1 resolves out of the same defocus the section headings use. */
.ns-motion .ns-hero__copy > .elementor-widget-heading,
.ns-motion .ns-hero__inner > .elementor-widget-heading {
	filter: blur(5px);
}

.ns-hero__copy > .elementor-widget-heading.is-revealed,
.ns-hero__inner > .elementor-widget-heading.is-revealed {
	filter: blur(0);
	transition:
		opacity var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay, 0ms),
		transform var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay, 0ms),
		filter var(--ns-dur-reveal-slow) var(--ns-ease-reveal) var(--ns-reveal-delay, 0ms);
}

/* --------------------------------------------------------------------------
   15.6 Motion preferences
   --------------------------------------------------------------------------
   site.js never starts a reveal for a reduced-motion visitor and the inline
   gate never sets `ns-motion`, so in the ordinary case none of the resting
   states above is ever applied. This catches the visitor who turns the
   preference on with the page already open and elements still waiting: they
   are shown, not animated.

   Section 13's blanket rule already collapses every transition and every
   time-based animation. A scroll-driven animation is not on a clock, so it
   ignores that and has to be turned off by name.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	.ns-reveal,
	.ns-reveal .elementor-widget,
	.ns-reveal .ns-plus,
	.ns-reveal .ns-work__slide-media,
	.ns-reveal .ns-work__slide-text,
	.ns-motion .ns-hero__copy > .elementor-widget,
	.ns-motion .ns-hero__inner > .elementor-widget,
	.ns-motion .ns-hero__inner > .e-con {
		opacity: 1;
		transform: none;
		filter: none;
		translate: none;
		transition: none;
	}

	.ns-reveal--rule .ns-rule,
	.ns-rule-wrap--short.ns-reveal--rule .ns-rule {
		clip-path: none;
	}

	.ns-reveal--divider::before {
		transform: scaleY(1);
	}

	.ns-reveal .ns-work__slide-media img {
		scale: 1;
	}

	.ns-hero__media,
	.ns-hero__inner,
	.ns-about::before,
	.ns-about::after,
	.ns-stories__media img,
	.ns-service-card__media img,
	.ns-why__media img,
	.ns-contact__media img {
		animation: none;
	}
}

/* --------------------------------------------------------------------------
   15.7 Mobile
   --------------------------------------------------------------------------
   The travel distances above are read against a 900-tall window. On a phone
   the same numbers are a much larger share of the screen, and an entrance
   that crosses a third of the viewport stops reading as a settle.
   -------------------------------------------------------------------------- */

@media (max-width: 767px) {
	:root {
		/* The mobile bar is 85 rather than 89 (section 14). */
		--ns-header-h: 85px;
	}

	.ns-reveal--rise {
		transform: translateY(18px);
	}

	.ns-reveal--frame,
	.ns-reveal--slide .ns-work__slide-media {
		transform: translateY(22px);
	}

	.ns-reveal--head .elementor-widget {
		transform: translateY(14px);
	}

	.ns-reveal--portrait {
		translate: 0 26px;
	}

	.ns-motion .ns-hero__copy > .elementor-widget,
	.ns-motion .ns-hero__inner > .elementor-widget,
	.ns-motion .ns-hero__inner > .e-con:not(.ns-hero__copy) {
		transform: translateY(16px);
	}
}
