/*
Theme Name: QB Media 2026
Author: JAvi
Author URI: https://qbmedia.com
Description: A single-page portfolio theme for motion design and video editing. Custom post types for work and client logos, ACF-driven content, filterable 16:9 grid with a video lightbox. No page builder, no external dependencies.
Version: 1.0.0
Requires at least: 6.0
Tested up to: 6.8
Requires PHP: 7.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: QB Media 2026
Tags: portfolio, one-column, custom-colors, full-width-template, accessibility-ready
*/



:root {
	--paper: #ffffff;
	--ink: #101114;
	--ink-soft: #6b7076;
	--ink-faint: #a8adb3;
	--rule: #e8e8ea;
	--wash: #f5f5f6;

	--font-display: "Helvetica Neue", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
	--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

	--step--1: 0.8125rem;
	--step-0: 1rem;
	--step-1: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
	--step-2: clamp(1.5rem, 1.2rem + 1.6vw, 2.25rem);
	--step-3: clamp(2rem, 1.4rem + 3.2vw, 3.5rem);

	--gap: 2x;
	--gutter: 1rem;
	--wrap: 1720px;
	--header-h: 64px;

	--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (min-width: 48rem) {
	:root {
		--gutter: 2rem;
		--header-h: 76px;
	}
}

/* =========================================================================
   2. Base
   ====================================================================== */

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

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

body {
	margin: 0;
	background: var(--paper);
	color: var(--ink);
	font-family: var(--font-body);
	font-size: var(--step-0);
	line-height: 1.6;
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
}

img {
	max-width: 100%;
	height: auto;
	display: block;
}

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

button {
	font: inherit;
	color: inherit;
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
}

h1,
h2,
h3 {
	font-family: var(--font-display);
	font-weight: 300;
	letter-spacing: -0.03em;
	line-height: 1.08;
	margin: 0;
}

:focus-visible {
	outline: 2px solid var(--ink);
	outline-offset: 3px;
	border-radius: 2px;
}

.wrap {
	width: 100%;
	max-width: var(--wrap);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

.screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(1px, 1px, 1px, 1px);
	clip-path: inset(50%);
	white-space: nowrap;
}

.skip-link:focus {
	position: fixed;
	top: 0.75rem;
	left: 0.75rem;
	z-index: 200;
	width: auto;
	height: auto;
	clip: auto;
	clip-path: none;
	background: var(--ink);
	color: var(--paper);
	padding: 0.6rem 1rem;
	border-radius: 4px;
}

/* Small caps utility label, used for eyebrows and section headings. */
.label {
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--ink-faint);
	font-weight: 500;
}

/* =========================================================================
   3. Header
   ====================================================================== */

.site-header {
	position: sticky;
	top: 0;
	z-index: 100;
	background: rgba(255, 255, 255, 0.86);
	backdrop-filter: saturate(180%) blur(12px);
	border-bottom: 1px solid transparent;
	transition: border-color 0.3s var(--ease);
}

.site-header.is-scrolled {
	border-bottom-color: var(--rule);
}

.header-inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	min-height: var(--header-h);
}

.site-brand {
	font-family: var(--font-display);
	font-size: var(--step-0);
	font-weight: 500;
	letter-spacing: -0.02em;
}

.site-brand span {
	color: var(--ink-faint);
	font-weight: 300;
}

.nav-toggle {
	position: relative;
	width: 40px;
	height: 40px;
	margin-right: -8px;
	display: grid;
	place-items: center;
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
	display: block;
	width: 20px;
	height: 1.5px;
	background: var(--ink);
	transition: transform 0.25s var(--ease), opacity 0.2s var(--ease);
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
	content: "";
	position: absolute;
}

.nav-toggle-bars::before {
	transform: translateY(-6px);
}

.nav-toggle-bars::after {
	transform: translateY(6px);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars {
	background: transparent;
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before {
	transform: rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after {
	transform: rotate(-45deg);
}

.site-nav {
	position: absolute;
	inset: 100% 0 auto 0;
	background: var(--paper);
	border-bottom: 1px solid var(--rule);
	padding: 0 var(--gutter) 1.5rem;
	display: none;
}

.site-nav.is-open {
	display: block;
}

.site-nav ul {
	list-style: none;
	margin: 0;
	padding: 0;
}

.btn-contact {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	border: 1px solid var(--ink);
	border-radius: 100px;
	padding: 0.5rem 1.1rem;
	font-size: var(--step--1);
	letter-spacing: 0.02em;
	background: var(--ink);
	color: var(--paper);
	transition: background-color 0.25s var(--ease), color 0.25s var(--ease);
}

.btn-contact:hover,
.btn-contact:focus-visible,
.btn-contact:active {
	background: var(--paper);
	color: var(--ink);
}

@media (min-width: 48rem) {
	.nav-toggle {
		display: none;
	}

	.site-nav {
		position: static;
		display: block;
		padding: 0;
		border: 0;
		background: none;
	}
}

/* =========================================================================
   4. Hero
   ====================================================================== */

/* Kept deliberately short: the work has to start above the fold, so the
   hero gets a portrait and a paragraph and no more. */
.hero {
	padding-block: clamp(1.75rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.25rem);
}

.hero-grid {
	display: grid;
	gap: clamp(4.25rem, 3vw, 3.75rem);
	align-items: center;
}

.hero-portrait {
	margin: 0;
	width: clamp(132px, 20vw, 208px);
}

.hero-portrait img {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
	/* Nudge this if a portrait crops through the chin. */
	object-position: var(--portrait-focus, center);
	border-radius: 50%;
	background: var(--wash);
}

.hero-body > * + * {
	margin-top: 0.85rem;
}

.hero-heading {
	font-size: var(--step-2);
	max-width: 20ch;
	text-wrap: balance;
}

.hero-bio {
	font-size: var(--step-1);
	color: var(--ink-soft);
	max-width: 66ch;
	line-height: 1.5;
	font-weight: 300;
}

.hero-bio p {
	margin: 0 0 0.75em;
}

.hero-bio strong {
	color: var(--ink);
	font-weight: 500;
}

@media (min-width: 56rem) {
	.hero-grid {
		grid-template-columns: auto minmax(0, 1fr);
	}
}

/* =========================================================================
   5. Client carousel
   Two identical tracks side by side. The script nudges scrollLeft forward and
   wraps at the halfway point, so the loop is seamless and native scrolling,
   trackpad swipes and touch drags all still work.
   ====================================================================== */

.clients {
	padding-block: clamp(1.5rem, 3vw, 2rem);
}

.carousel {
	overflow-x: auto;
	overflow-y: hidden;
	scrollbar-width: none;
	-ms-overflow-style: none;
	cursor: grab;
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.carousel::-webkit-scrollbar {
	display: none;
}

.carousel.is-dragging {
	cursor: grabbing;
	scroll-behavior: auto;
	user-select: none;
}

.carousel-track {
	display: flex;
	align-items: center;
	gap: clamp(1rem, 3vw, 5rem);
	width: max-content;
	margin: 0;
	padding: 0;
	list-style: none;
}

.carousel-item {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	height: 66px;
}

.carousel-item img {
	max-height: 66px;
	width: auto;
	object-fit: contain;
	pointer-events: none;
}

.carousel-item a {
	pointer-events: auto;
}

/* =========================================================================
   6. Portfolio
   ====================================================================== */

.portfolio {
	padding-block: clamp(1.5rem, 3vw, 2.25rem) clamp(3rem, 8vw, 6rem);
}

/* Signature: the filter row reads as one line of type. Only the active
   category sits in full ink; the rest recede. Dots do the separating. */
.filters {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.35rem 0.9rem;
	margin-bottom: clamp(1rem, 2vw, 1.5rem);
	list-style: none;
	padding: 0;
}

.filters li {
	display: flex;
	align-items: center;
	gap: 0.9rem;
}

.filters li + li::before {
	content: "";
	width: 3px;
	height: 3px;
	border-radius: 50%;
	background: var(--ink-faint);
	flex: none;
}

.filter {
	font-family: var(--font-display);
	font-size: var(--step-1);
	font-weight: 300;
	letter-spacing: -0.02em;
	color: var(--ink-faint);
	padding-block: 0.25rem;
	transition: color 0.25s var(--ease);
}

.filter:hover {
	color: var(--ink-soft);
}

.filter[aria-pressed="true"] {
	color: var(--ink);
}

.filter-count {
	font-family: var(--font-body);
	font-size: 0.625rem;
	letter-spacing: 0.06em;
	vertical-align: super;
	color: var(--ink-faint);
}

/* True masonry: the row span comes from each card's own proportions
   (js/masonry.js). Falls back to a plain grid when the script does not run. */
.grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--gap);
	align-items: start;
}

@media (min-width: 40rem) {
	.grid {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (min-width: 64rem) {
	.grid {
		grid-template-columns: repeat(3, 1fr);
	}
}

@media (min-width: 100rem) {
	.grid {
		grid-template-columns: repeat(4, 1fr);
	}
}

/* 1px rows and no row gap: with a coarser unit, rounding each card to a
   whole number of rows leaves up to half a row of slack above and below it,
   which is exactly the ragged white space you see in a bad masonry. At 1px
   the error is sub-pixel. The visual gap moves onto the card instead, so it
   stays inside the span and never compounds. */
.grid.is-masonry {
	grid-auto-rows: 1px;
	row-gap: 0;
	align-items: stretch;
}

.grid.is-masonry .card {
	padding-bottom: var(--gap);
}

/* Fill the row span exactly. Spans round up to a whole number of rows, and
   at zero gutter that leftover fraction would show as a hairline of white
   between items; letting the poster stretch into it removes the seam. */
.grid.is-masonry .card-open,
.grid.is-masonry .card-media {
	height: 100%;
}

.card {
	position: relative;
	transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}

.card[hidden] {
	display: none;
}

.card.is-leaving {
	opacity: 0;
	transform: scale(0.985);
}

.card-open {
	display: block;
	width: 100%;
	text-align: left;
}

.card-media {
	position: relative;
	display: block;
	aspect-ratio: var(--ratio, 16 / 9);
	overflow: hidden;
	background: var(--wash);
}

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

.card-open:hover .card-media img,
.card-open:focus-visible .card-media img {
	transform: scale(1.04);
}

/* Titles stay out of the grid and appear on hover, so nothing interrupts
   the wall of work. They are still in the DOM for screen readers, and the
   lightbox shows them in full. */
.card-overlay {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: flex-end;
	padding: 0.9rem 1rem;
	background: linear-gradient(to top, rgba(10, 11, 13, 0.78), rgba(10, 11, 13, 0.1) 45%, rgba(10, 11, 13, 0) 70%);
	opacity: 0;
	transition: opacity 0.35s var(--ease);
	pointer-events: none;
}

.card-open:hover .card-overlay,
.card-open:focus-visible .card-overlay {
	opacity: 1;
}

.card-overlay-title {
	color: #fff;
	font-size: var(--step--1);
	line-height: 1.35;
	letter-spacing: 0;
	transform: translateY(4px);
	transition: transform 0.35s var(--ease);
}

.card-open:hover .card-overlay-title,
.card-open:focus-visible .card-overlay-title {
	transform: none;
}

/* Play affordance: a hairline ring that fills on hover. */
.card-play {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	pointer-events: none;
}

.card-play span {
	width: 46px;
	height: 46px;
	border-radius: 50%;
	border: 1px solid rgba(255, 255, 255, 0.8);
	background: rgba(16, 17, 20, 0.14);
	backdrop-filter: blur(2px);
	display: grid;
	place-items: center;
	opacity: 0.9;
	transition: background-color 0.3s var(--ease), transform 0.3s var(--ease), opacity 0.3s var(--ease);
}

.card-play svg {
	width: 11px;
	height: 13px;
	fill: #fff;
	margin-left: 2px;
	transition: fill 0.3s var(--ease);
}

.card-open:hover .card-play span,
.card-open:focus-visible .card-play span {
	background: #fff;
	opacity: 1;
	transform: scale(1.08);
}

.card-open:hover .card-play svg,
.card-open:focus-visible .card-play svg {
	fill: var(--ink);
}

.grid-empty {
	color: var(--ink-soft);
	padding-block: 3rem;
}

/* =========================================================================
   7. Lightbox
   ====================================================================== */

.lightbox {
	position: fixed;
	inset: 0;
	z-index: 200;
	display: none;
	align-items: center;
	justify-content: center;
	overflow-y: auto;
	padding: clamp(1rem, 4vw, 3rem);
	background: rgba(10, 11, 13, 0.94);
	opacity: 0;
	transition: opacity 0.25s var(--ease);
}

.lightbox.is-open {
	display: flex;
	opacity: 1;
}

/* Cap the width by the available *height* as well, so a 16:9 frame plus the
   title bar and the nav row always fit on screen. Without this the flex
   centring pushes the title above the top edge on short windows, and it
   silently disappears. */
.lightbox-inner {
	width: min(100%, 1200px);
	width: min(100%, 1200px, calc((100vh - 11rem) * 16 / 9));
	width: min(100%, 1200px, calc((100svh - 11rem) * 16 / 9));
	margin: auto;
}

.lightbox-bar {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 1.5rem;
	margin-bottom: 0.85rem;
	color: #fff;
}

.lightbox-eyebrow {
	display: block;
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: rgba(255, 255, 255, 0.5);
	margin-bottom: 0.35rem;
}

.lightbox-eyebrow:empty {
	display: none;
}

.lightbox-title {
	font-family: var(--font-display);
	font-size: var(--step-2);
	font-weight: 300;
	line-height: 1.1;
	color: #fff;
	text-wrap: balance;
}

.lightbox-close {
	flex: none;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	border: 1px solid rgba(255, 255, 255, 0.35);
	display: grid;
	place-items: center;
	color: #fff;
	transition: background-color 0.25s var(--ease), color 0.25s var(--ease);
}

.lightbox-close:hover,
.lightbox-close:focus-visible {
	background: #fff;
	color: var(--ink);
}

.lightbox-close svg {
	width: 14px;
	height: 14px;
	stroke: currentColor;
	stroke-width: 1.5;
	fill: none;
}

.lightbox-frame {
	position: relative;
	aspect-ratio: 16 / 9;
	background: #000;
	border-radius: 2px;
	overflow: hidden;
}

.lightbox-frame iframe {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	border: 0;
}

.lightbox-nav {
	display: flex;
	gap: 1.25rem;
	justify-content: flex-end;
	margin-top: 0.75rem;
}

.lightbox-nav button {
	font-size: var(--step--1);
	color: rgba(255, 255, 255, 0.6);
	transition: color 0.2s var(--ease);
}

.lightbox-nav button:hover:not(:disabled) {
	color: #fff;
}

.lightbox-nav button:disabled {
	opacity: 0.25;
	cursor: default;
}

body.is-locked {
	overflow: hidden;
}

/* =========================================================================
   8. Footer & contact form
   ====================================================================== */

.site-footer {
	background: var(--wash);
	border-top: 1px solid var(--rule);
	padding-block: clamp(3rem, 8vw, 6rem) 2rem;
	scroll-margin-top: var(--header-h);
}

.footer-grid {
	display: grid;
	gap: clamp(2rem, 5vw, 4rem);
	align-items: start;
}

.footer-heading {
	font-size: var(--step-2);
	max-width: 14ch;
	text-wrap: balance;
}

.footer-intro {
	color: var(--ink-soft);
	margin-top: 0.75rem;
	max-width: 34ch;
}

.footer-direct {
	margin-top: 1.5rem;
	font-size: var(--step--1);
}

.footer-direct a {
	border-bottom: 1px solid var(--ink-faint);
	padding-bottom: 1px;
}

@media (min-width: 56rem) {
	.footer-grid {
		grid-template-columns: 1fr 1.1fr;
	}
}

.field {
	margin-bottom: 1.25rem;
}

.field label {
	display: block;
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--ink-soft);
	margin-bottom: 0.5rem;
}

.field .optional {
	letter-spacing: 0;
	text-transform: none;
	color: var(--ink-faint);
}

.field input,
.field textarea {
	width: 100%;
	font: inherit;
	color: var(--ink);
	background: var(--paper);
	border: 1px solid var(--rule);
	border-radius: 2px;
	padding: 0.7rem 0.85rem;
	transition: border-color 0.2s var(--ease);
}

.field input:focus,
.field textarea:focus {
	border-color: var(--ink);
	outline: none;
}

.field textarea {
	min-height: 8rem;
	resize: vertical;
}

.field.has-error input,
.field.has-error textarea {
	border-color: #b42318;
}

.field-error {
	display: block;
	min-height: 1rem;
	margin-top: 0.35rem;
	font-size: var(--step--1);
	color: #b42318;
}

.honeypot {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

.btn-submit {
	display: inline-flex;
	align-items: center;
	gap: 0.6rem;
	background: var(--ink);
	color: var(--paper);
	border-radius: 100px;
	padding: 0.75rem 1.6rem;
	font-size: var(--step--1);
	letter-spacing: 0.02em;
	transition: opacity 0.2s var(--ease);
}

.btn-submit:hover {
	opacity: 0.82;
}

.btn-submit[disabled] {
	opacity: 0.45;
	cursor: default;
}

.form-status {
	margin-top: 1rem;
	font-size: var(--step--1);
	min-height: 1.25rem;
}

.form-status.is-success {
	color: #12693f;
}

.form-status.is-error {
	color: #b42318;
}

.footer-base {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem 1.5rem;
	justify-content: space-between;
	margin-top: clamp(3rem, 6vw, 5rem);
	padding-top: 1.25rem;
	border-top: 1px solid var(--rule);
	font-size: var(--step--1);
	color: var(--ink-faint);
}

/* =========================================================================
   9. Single item & fallback templates
   ====================================================================== */

.single-work {
	padding-block: clamp(2rem, 6vw, 4rem) clamp(3rem, 8vw, 6rem);
}

.single-work .lightbox-frame {
	margin-block: 1.5rem;
}

.single-work-title {
	font-size: var(--step-2);
}

.back-link {
	display: inline-block;
	margin-top: 2rem;
	font-size: var(--step--1);
	color: var(--ink-soft);
	border-bottom: 1px solid var(--rule);
}

.page-body {
	padding-block: clamp(2rem, 6vw, 4rem) clamp(3rem, 8vw, 6rem);
	max-width: 68ch;
}

.page-body h1 {
	font-size: var(--step-2);
	margin-bottom: 1.5rem;
}

/* =========================================================================
   10. Scroll reveal
   Gated behind .js (set by an inline snippet in the head) so the hidden
   start state can never strand content for a visitor without JavaScript.
   ====================================================================== */

.js .hero-portrait,
.js .hero-body > *,
.js .carousel,
.js .card,
.js .footer-grid > *,
.js .footer-base {
	opacity: 0;
	transform: translateY(14px);
}

.js .is-visible {
	opacity: 1;
	transform: none;
	transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

/* Filtering out a card should snap, not linger for the length of an
   entrance animation. */
.js .card.is-leaving {
	opacity: 0;
	transform: scale(0.985);
	transition-duration: 0.2s;
}

/* =========================================================================
   11. Motion preferences
   ====================================================================== */

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}

	.js .hero-portrait,
	.js .hero-body > *,
	.js .carousel,
	.js .card,
	.js .footer-grid > *,
	.js .footer-base {
		opacity: 1;
		transform: none;
	}

	*,
	*::before,
	*::after {
		animation-duration: 0.001ms !important;
		transition-duration: 0.001ms !important;
	}
}
