/* ============================================
   ZEROSIX — horizontal one-pager on a FIXED CANVAS
   Design space: 2000 × 1100. Everything inside is
   absolute design-px. The whole frame scales as ONE
   unit to fit the screen — nothing ever gets cut.
   Scroll down → rail slides LEFT inside the frame.
   ============================================ */

:root {
    /* ── ZEROSIX DESIGN SYSTEM ─────────────────────────
       Every value in this file derives from these tokens.
       New styles MUST use them — no eyeballed numbers. */

    /* color — one accent does all accent work */
    --pop:       #cd2d3a;
    --black:     #18181b;
    --grey-dark: #52525b;
    --grey-mid:  #a1a1aa;
    --grey-line: #e4e4e7;
    --grey-bg:   #f4f4f5;
    --white:     #ffffff;

    /* type — two families, five sizes, nothing in between */
    --font-body:    'Inter', system-ui, -apple-system, sans-serif;
    --font-display: 'Archivo Black', system-ui, sans-serif;
    --t-display:   176px;  /* ALL display headlines — one size, always 2 lines */
    --t-display-2:  88px;  /* reserved for dense layouts */
    --t-body-lg:    38px;  /* ledes / standfirst */
    --t-body:       28px;  /* UI, forms, running text */
    --t-caption:    22px;  /* cues, captions, labels */

    /* space — 8px scale, every gap is one of these */
    --s1:   8px;
    --s2:  16px;
    --s3:  24px;
    --s4:  32px;
    --s5:  48px;
    --s6:  64px;
    --s7:  96px;
    --s8: 128px;

    /* surface — one radius, one shadow recipe */
    --radius: 14px;
    --radius-pill: 9999px;
    --shadow-card:       0 24px 60px -16px rgba(24, 24, 27, 0.22);
    --shadow-card-hover: 0 34px 72px -16px rgba(24, 24, 27, 0.30);
    --shadow-obj:        drop-shadow(0 40px 46px rgba(24, 24, 27, 0.28));
    --shadow-obj-hover:  drop-shadow(0 52px 58px rgba(24, 24, 27, 0.34));
    /* Vertical layout only: objects deliberately overlap there, so
       full-strength shadows compound into a dark blotch where they
       cross. Same shadow, lighter. */
    --shadow-obj-vertical:       drop-shadow(0 18px 20px rgba(24, 24, 27, 0.14));
    --shadow-obj-vertical-hover: drop-shadow(0 24px 26px rgba(24, 24, 27, 0.18));

    /* the design canvas — 2560 wide, browser-friendly height */
    --canvas-w: 2560px;
    --canvas-h: 1200px;
    --marquee-h: 120px;
    --edge-fade: 256px;
}

::selection { background: var(--pop); color: var(--white); }

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

a { text-decoration: none; }

body {
    margin: 0;
    font-family: var(--font-body);
    background: var(--white);
    color: var(--black);
    -webkit-font-smoothing: antialiased;
    /* clip, NOT hidden. overflow-x:hidden turns <body> into a scroll
       container, which breaks position:sticky completely and makes
       position:fixed unreliable. overflow-x:clip crops the same
       horizontal overflow without creating a scroll container. */
    overflow-x: clip;
}

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

.headline-red { color: var(--pop); }

/* ── Scroll machine ──────────────────────────────── */
.hscroll { height: 350svh; }

.hviewport {
    /* FIXED, not sticky — this is the tearing fix.

       Sticky means the browser recomputes and repositions this box on
       every scroll frame. The rail inside it is ALSO driven per scroll
       frame, by its scroll-timeline animation. Two independent
       scroll-linked position updates that have to agree: if they're
       sampled even slightly out of phase, the container and its own
       contents move at different moments, and that desync reads as
       tearing/shimmer. It also explains why nothing we did to smooth
       the rail's own animation ever fixed it — the rail was never the
       half that was out of sync.

       A fixed element is not repositioned per scroll frame at all, so
       the rail's transform becomes the ONLY scroll-linked update.
       Visually identical: it still pins to the viewport top and fills
       it, and .hscroll still supplies the scroll length. */
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 100svh;
    /* deliberately NO z-index — sticky and fixed are both "positioned"
       with z-index:auto, so leaving it off keeps the existing paint
       order exactly as it was (marquee over objects on desktop).
       Setting one here would create a stacking context and could
       silently reorder that. */
    overflow: hidden;
    display: flex;
    align-items: center;
    /* frame starts at exactly x=0 — a centered frame lands on a
       fractional pixel and reintroduces shimmer */
    justify-content: flex-start;
}
/* THE canvas — JS sets --s to fit the screen */
.frame {
    flex: none;
    width: var(--canvas-w);
    height: var(--canvas-h);
    position: relative;
    /* clip ONLY horizontally (the scroll needs it) — vertically
       shadows are free to fall past the canvas edge, so they
       fade out naturally instead of slicing at the border */
    overflow-x: clip;
    overflow-y: visible;
    /* DEFAULT / FALLBACK path: transform scale. Only used when the
       browser can't do `zoom` — see the html.scale-zoom rule below,
       which is what actually runs in Chrome. transform:scale() leaves
       the continuously-moving .rail inside a fractionally transformed
       ancestor, which is the suspected resampling/shimmer source. */
    transform: scale(var(--s, 1));
    transform-origin: left center;
    contain: layout;
}

/* PRIMARY path in any browser that supports `zoom` (Chrome does).
   zoom is LAYOUT-aware: the frame is laid out and rasterized AT the
   target scale, so .rail moves inside a normally-laid-out ancestor
   rather than sliding around inside a fractionally transformed
   surface that Chrome may resample every frame. Same visual result,
   different compositing path. JS adds .scale-zoom after feature
   detection — see setScaleMode() in main.js. */
html.scale-zoom .frame {
    transform: none;
    transform-origin: initial;
    zoom: var(--s, 1);
}

/* no edge fades on the content row — objects and text clip
   naturally at the browser edge as the rail slides */

/* ── The rail — one continuous strip in design-px ── */
.rail {
    display: flex;
    align-items: center;
    height: 100%;
    width: max-content;
    gap: 0; /* seams set per-side on the cluster below */
    /* fades vanish at the ends, so content can frame close */
    /* --rail-lead is set by main.js: when the window is wider than the
       rail actually needs (short windows scale the canvas down, so the
       strip stops filling it) this is half the leftover, which centers
       the whole strip instead of leaving it stuck to the left edge. */
    padding: 0 calc(var(--s8) + var(--s4)) var(--marquee-h)
             calc(var(--s7) + var(--rail-lead, 0px));
    /* NO will-change: transform — isolation test. The rail is a
       4,000–6,000px-wide surface; permanently promoting it forces
       Chrome to keep one enormous rasterized layer alive, which can
       hit GPU tile limits and produce seams. The transform itself
       still gets compositor handling when it actually animates. */
    position: relative;
    z-index: 2; /* objects float above the carousel */
}

/* NATIVE scroll-driven rail — runs on the GPU compositor
   thread, synced to the display like native scrolling.

   ISOLATION TEST: now gated behind html.rail-native, which main.js
   only adds when RAIL_MODE === 'native'. With the JS rAF path active
   the class is absent, so this block never applies and the rail's
   transform has exactly ONE controller. Previously @supports alone
   meant Chrome always ran this, even while the JS loop also wanted
   the property — two writers on one transform. */
@supports (animation-timeline: scroll()) {
    html.rail-native .rail {
        animation: rail-slide linear both;
        animation-timeline: scroll(root block);
    }
    @keyframes rail-slide {
        from { transform: translate3d(0, 0, 0); }
        to   { transform: translate3d(var(--rail-x, 0px), 0, 0); }
    }
}

.block { flex: 0 0 auto; }

/* bookend columns — sized to their widest headline line
   EXACTLY, so every seam is real space, not slack */
.block-intro { width: 1280px; }

.block-army {
    width: 1360px;
    text-align: right;
}

.block-army .sub { margin-left: auto; }
.block-army .built-tag { justify-content: flex-end; }

/* ── Type — fixed design sizes, scale with the frame ── */
.site-logo { width: 400px; margin-bottom: var(--s5); }

.h-display {
    font-family: var(--font-display);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    text-wrap: balance;
    margin: 0 0 var(--s5);
    /* size is measured and set by fitHeadlines() in main.js —
       this is only the fallback if JS hasn't run yet */
    font-size: var(--fit, var(--t-display));
}

/* each line shrink-wraps so its true text width can be measured */
.h-display .hl {
    display: block;
    width: fit-content;
    white-space: nowrap;
}

.block-army .h-display .hl { margin-left: auto; }

.h-display .headline-red em,
.h-display em.headline-red { font-style: normal; }

.sub {
    font-size: var(--t-body-lg);
    color: var(--grey-dark);
    line-height: 1.6;
    max-width: 46ch;
    margin: 0;
}

.scroll-cue {
    margin: var(--s7) 0 0;
    font-size: var(--t-body); /* cue + arrow */
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--grey-mid);
}

/* The arrow shows the DIRECTION OF TRAVEL, not the input gesture —
   that's the one thing both interactions share: scrolling down and
   dragging left both move you rightward through the rail. So it
   sweeps right regardless of which gesture the visitor uses. */
.cue-arrow {
    display: inline-block;
    /* SVG instead of the &rarr; glyph: that character is hairline-thin
       at any size and its weight can't be controlled. Here stroke-width
       is a real dial, and it scales cleanly. */
    width: 44px;
    height: 24px;
    vertical-align: middle;
    margin-left: var(--s2);
    fill: none;
    stroke: var(--grey-mid);
    stroke-width: 3;
    stroke-linecap: round;
    stroke-linejoin: round;
    animation: cue-sweep 1.6s ease-in-out infinite;
}

@keyframes cue-sweep {
    0%   { transform: translateX(0);    opacity: 0.35; }
    55%  { transform: translateX(10px); opacity: 1; }
    100% { transform: translateX(20px); opacity: 0; }
}

/* ── Fade-in ─────────────────────────────────────── */
.fade-in {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible { opacity: 1; transform: translateY(0); }
.fade-in.delay-1 { transition-delay: 0.12s; }
.fade-in.delay-2 { transition-delay: 0.24s; }
.fade-in.delay-3 { transition-delay: 0.36s; }

/* ── The object group — ONE seamless composition ─── */
.cluster {
    position: relative;
    flex: 0 0 auto;
}

.c-all {
    width: 1880px;
    height: 1000px;
    /* equal, tight seams both sides — pulled into the intro
       column's trailing whitespace so the seam feels uniform */
    margin-left: -80px;
    margin-right: var(--s3);
}

/* design-px positions — tweak freely, they can't break */
.obj-jar    { width: 440px; top: 40px;  left: 70px; }
.obj-lagree { width: 525px; top: 510px; left: 190px; }
.obj-star   { width: 455px; top: 460px; left: 830px; z-index: 3; }
.obj-poster { width: 565px; top: 20px;  left: 580px; }
.obj-video  { width: 460px; top: 470px; left: 1280px; }
.obj-uiux   { width: 740px; top: 20px;  left: 1140px; }

/* clickable floating objects */
.obj {
    position: absolute;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font-family: var(--font-body);
}

/* ── TRUE float: two independent motions stacked ──────
   The .obj wrapper rises/falls on one clock while the
   .obj-float inside sways sideways + tilts on ANOTHER
   clock. Mismatched prime-ish durations = a wandering
   elliptical path that never visibly repeats. */
.c-all .obj {
    animation: float-y var(--fy, 6.1s) ease-in-out var(--delay, 0s) infinite alternate both;
}

.obj-float {
    display: block;
    /* "both" fill: during a start delay the object already holds
       its first animation frame — no angle snap on load */
    animation: float-x var(--fx, 8.3s) ease-in-out var(--delay, 0s) infinite alternate both;
    will-change: transform;
}

@keyframes float-y {
    from { transform: translateY(-12px); }
    to   { transform: translateY(12px); }
}

@keyframes float-x {
    from { transform: rotate(calc(var(--tilt, 0deg) - 4.5deg)) translateX(-14px); }
    to   { transform: rotate(calc(var(--tilt, 0deg) + 4.5deg)) translateX(14px); }
}

/* per-object clocks — all different, so no two move alike */
.obj-jar    { --fy: 4.7s; }
.obj-jar .obj-float    { --fx: 7.3s; }
.obj-lagree { --fy: 6.1s; }
.obj-lagree .obj-float { --fx: 8.9s; }
.obj-star   { --fy: 5.4s; }
.obj-star .obj-float   { --fx: 7.9s; }
.obj-poster { --fy: 6.6s; }
.obj-poster .obj-float { --fx: 9.4s; }
.obj-video  { --fy: 5.1s; }
.obj-video .obj-float  { --fx: 6.7s; }
.obj-uiux   { --fy: 5.9s; }
.obj-uiux .obj-float   { --fx: 8.3s; }

.obj-ph {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: var(--ar, 4 / 3);
    background: var(--grey-bg);
    border-radius: var(--radius);
    color: var(--grey-mid);
    font-size: var(--t-caption);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: center;
    padding: var(--s1);
    box-shadow: var(--shadow-card);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.obj:hover .obj-ph {
    transform: scale(1.05);
    box-shadow: var(--shadow-card-hover);
}

.ph-play {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--black);
    color: var(--white);
    transition: background 0.2s ease, transform 0.2s ease;
}

.ph-play svg { margin-left: 2px; }

.obj:hover .ph-play {
    background: var(--pop);
    transform: scale(1.1);
}

.obj-img {
    width: 100%;
    height: auto;
    /* SHADOWS OFF — trying the objects flat. To bring them back,
       uncomment the two filter lines here and the two in the
       html.is-vertical block; the --shadow-obj* variables are all
       still defined above. */
    /* filter: var(--shadow-obj); */
    transition: transform 0.25s ease, filter 0.25s ease;
    user-select: none;
}

.obj:hover .obj-img {
    transform: scale(1.06) rotate(1.5deg);
    /* filter: var(--shadow-obj-hover); */
}

/* ── Contact form (inside block-army) ────────────── */
.contact-form {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    width: 100%;
    /* fields track a readable measure, not the column width —
       an input this wide is uncomfortable to type in and the
       block only got wider to fit the headline */
    max-width: var(--form-w, 880px);
    margin: var(--s5) 0 0 auto;
    text-align: left;
}

.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s2);
}

.contact-form input,
.contact-form textarea {
    width: 100%;
    font-family: var(--font-body);
    font-size: var(--t-body);
    color: var(--black);
    background: var(--white);
    border: 1.5px solid var(--grey-line);
    border-radius: var(--radius);
    padding: var(--s2) var(--s3);
    transition: border-color 0.18s, box-shadow 0.18s;
    resize: vertical;
}

.contact-form textarea,
#cfSubject {
    min-height: 200px;
    line-height: 1.5;
}

.contact-form input::placeholder,
.contact-form textarea::placeholder { color: var(--grey-mid); }

.contact-form input:focus,
.contact-form textarea:focus {
    outline: none;
    border-color: var(--black);
    box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.08);
}

.contact-form input.is-error,
.contact-form textarea.is-error { border-color: var(--pop); }

.form-send {
    font-family: var(--font-display);
    font-size: var(--t-body);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--white);
    background: var(--black);
    border: none;
    border-radius: var(--radius);
    padding: var(--s3) var(--s5);
    cursor: pointer;
    transition: background 0.07s, transform 0.15s;
}

.form-send:hover { background: var(--pop); }
.form-send:active { transform: scale(0.98); }

.form-note {
    min-height: 1.2em;
    margin: 0;
    font-size: 14px;
    color: var(--grey-dark);
    text-align: center;
}

.built-tag {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5em;
    margin: var(--s5) 0 0;
    font-size: var(--t-body);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--grey-mid);
}

.built-tag-logo {
    display: inline-block;
    height: 1.15em;
    width: auto;
    vertical-align: middle;
}

.built-tag .bolt { font-size: 1.5em; line-height: 1; }

/* ── Marquee — locked to the bottom of the frame ─── */
.marquee-bar {
    /* fixed in BOTH layouts. It sits outside .frame in the markup, so
       there is no transformed ancestor to re-anchor it — this is a
       true viewport footer. It scales itself by --s (published on
       :root by main.js) so the horizontal canvas still matches. */
    position: fixed;
    left: 0; right: 0; bottom: 0;
    height: calc(var(--marquee-h) * var(--s, 1));
    display: flex;
    align-items: center;
    overflow: hidden;
    /* fully transparent — object shadows pass behind the logos */
    background: transparent;
    /* horizontal desktop: carousel sits UNDER the objects, so
       floating art + shadows cross it cleanly (vertical layout
       overrides this back to a fixed top-layer footer) */
    z-index: 1;
}

/* carousel edge fades — same softness as the frame */
.marquee-bar::before,
.marquee-bar::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0;
    width: var(--edge-fade);
    z-index: 2;
    pointer-events: none;
}

.marquee-bar::before {
    left: 0;
    background: linear-gradient(to right, var(--white) 12%, rgba(255,255,255,0));
}

.marquee-bar::after {
    right: 0;
    background: linear-gradient(to left, var(--white) 12%, rgba(255,255,255,0));
}

.marquee-inner {
    display: flex;
    align-items: center;
    width: max-content;
    animation: marquee 26s linear infinite;
    will-change: transform;
}

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

.marquee-inner img {
    /* logo size tracks the canvas scale, same as it did when the bar
       was a child of the scaled frame */
    height: calc(var(--s5) * var(--s, 1));
    opacity: 0.55;
    flex-shrink: 0;
    padding: 0 calc(var(--s5) * var(--s, 1));
    object-fit: contain;
    transition: opacity 0.2s;
}

.marquee-inner img:hover { opacity: 1; }
.marquee-inner img.logo-nbc { height: calc(24px * var(--s, 1)); }

@keyframes marquee {
    0%   { transform: translateX(0); }
    100% { transform: translateX(var(--marquee-width, -50%)); }
}

/* ── Modal (outside the frame — normal screen space) ── */
.modal-tag {
    font-size: 1.2rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--pop);
    margin: 0 0 0.5rem;
}

.modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(9, 9, 11, 0.92);
    z-index: 100;
    align-items: center;
    justify-content: center;
    padding: clamp(1rem, 3vw, 3rem);
}

.modal.is-open { display: flex; }

.modal-box {
    background: var(--white);
    color: var(--black);
    border-radius: var(--radius);
    box-shadow: 0 32px 80px -12px rgba(0, 0, 0, 0.55);
    padding: 0; /* media runs full-bleed; text gets .pm-pad */
    /* scales with the screen: ~82% of the window, never past
       1680px — big on 4K, still sane on a laptop */
    max-width: min(1780px, 92vw);
    width: 100%;
    max-height: 92vh;
    /* the box only clips — the INNER wrapper scrolls, so the
       scrollbar lives inside the rounded corners */
    overflow: hidden;
    position: relative;
}

.modal-scroll {
    max-height: 92vh;
    overflow-y: auto;
    scrollbar-width: thin;                       /* Firefox */
    scrollbar-color: rgba(24,24,27,0.3) transparent;
}

/* below 1100px the popup takes the whole screen — side
   gutters would squeeze the content too small */
@media (max-width: 1100px) {
    .modal { padding: 0; }
    .modal-box {
        width: 100%;
        max-width: 100%;
        height: 100svh;
        max-height: 100svh;
        border-radius: 0;
    }
    .modal-scroll { max-height: 100svh; }
}

/* slim floating scrollbar — transparent track */
.modal-scroll::-webkit-scrollbar { width: 10px; }
.modal-scroll::-webkit-scrollbar-track { background: transparent; margin: 14px 0; }
.modal-scroll::-webkit-scrollbar-thumb {
    background: rgba(24,24,27,0.28);
    border-radius: var(--radius-pill);
    border: 3px solid transparent;
    background-clip: content-box;
}
.modal-scroll::-webkit-scrollbar-thumb:hover { background-color: rgba(24,24,27,0.45); }

.modal.is-open .modal-box {
    animation: slap 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes slap {
    from { transform: translateY(18px) scale(0.985); opacity: 0; }
    to   { transform: translateY(0) scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .modal.is-open .modal-box { animation: none; }
}

.modal-close {
    position: absolute;
    top: 1.15rem; right: 1.4rem;
    z-index: 5;
    width: 3rem;
    height: 3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(9, 9, 11, 0.72);
    border-radius: var(--radius-pill);
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    line-height: 1;
    color: #fff;
    padding: 0;
    transition: background 0.2s ease, transform 0.2s ease;
}

.modal-close:hover {
    background: var(--pop);
    transform: rotate(90deg);
}

.modal-title {
    margin: 0 0 0.5rem;
    font-family: var(--font-display);
    font-size: clamp(2.6rem, 3.75vw, 6rem);
    font-weight: 400;
    text-transform: uppercase;
}
.modal-sub { margin: 0; color: var(--grey-dark); font-size: 1rem; }

.modal-media-box {
    background: var(--grey-bg);
    width: 100%;
    aspect-ratio: 16 / 9;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    margin-bottom: 1.5rem;
}

.modal-media-box img,
.modal-media-box video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* real hero media sets its own height — never punched in */
.modal-media-box:has(img),
.modal-media-box:has(video) { aspect-ratio: auto; }
.modal-media-box:has(img) img { height: auto; }

.modal-media-empty { color: var(--grey-mid); }

/* plain JPG hero — full image, no crop */
.pm-hero-plain,
.modal-media-box img.pm-hero-plain {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
}

.modal-media-box:has(.pm-hero-plain) {
    aspect-ratio: auto;
    display: block;
    margin-bottom: 0;
    background: none;
}

/* ── Hybrid hero: background art + centered PNG,
      text stacked below, left-justified ─────────── */
.pm-hero-center {
    /* ── TUNE ── */
    --ch-h: 500px;        /* hero height */
    --ch-size: 100%;       /* object size */
    --ch-x: -11%;           /* object left(-) / right(+) */
    --ch-y: 0px;          /* object up(-) / down(+) */

    position: relative;
    width: 100%;
    height: var(--ch-h);
    min-height: var(--ch-h);
    background-size: cover;
    background-position: center top;
    background-repeat: no-repeat;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.pm-hero-center-float,
.modal-media-box .pm-hero-center img.pm-hero-center-float {
    height: var(--ch-size);
    width: auto;
    max-width: none;
    object-fit: contain;
    transform: translate(var(--ch-x), var(--ch-y));
}

.modal-media-box:has(.pm-hero-center) {
    aspect-ratio: auto;
    display: block;
    margin-bottom: 0;
    background: none;
}

@media (max-width: 820px) {
    .pm-hero-center {
        --ch-h: 300px;
        height: var(--ch-h);
        background-size: cover;
        background-position: center center;
    }
    .pm-hero-center-float,
    .modal-media-box .pm-hero-center img.pm-hero-center-float {
        height: auto;
        width: 82%;
        transform: none;
    }
}

/* ── Case-study flow inside the modal ─────────────── */
.pm-body { padding: 0; }

/* text sections keep comfortable padding; media runs full-bleed */
.pm-pad { padding: 3.5rem clamp(2rem, 5.3vw, 4.7rem); }

/* beats: generous air ABOVE (new chapter), tight BELOW (the
   text belongs to the image that follows it). Centered like a
   chapter head, paragraph in a narrow reading column. */
.pm-beat {
    padding-top: 2.5rem;
    padding-bottom: 1.75rem;
}

/* chapter mark — a short red tick instead of a divider line */
.pm-beat::before {
    content: "";
    display: block;
    width: 40px;
    height: 4px;
    background: var(--pop);
    margin-bottom: 1.25rem;
}

/* red index number — inline before the chapter title */
.pm-num {
    color: var(--pop);
    letter-spacing: 0.08em;
    margin-right: 0.9em;
}

/* pull-quote — one big centered line between chapters */
.pm-quote {
    font-size: clamp(1.5rem, 1.7vw, 2.4rem);
    font-style: italic;
    font-weight: 600;
    line-height: 1.4;
    text-align: center;
    max-width: 55ch;
    margin: 0 auto;
    padding-top: var(--s5);
    padding-bottom: var(--s5);
}

/* linked image card — click through to an external page */
.pm-link-fig {
    margin-bottom: 30px;
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
}

.pm-link-card {
    position: relative;
    display: block;
    width: 100%;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--grey-bg);
    line-height: 0;
    transition: box-shadow 0.25s ease, transform 0.25s ease;
}

.pm-link-card img {
    width: 100%;
    height: auto;
    display: block;
}

.pm-link-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-card);
}

.pm-link-badge {
    position: absolute;
    right: clamp(0.75rem, 1.5vw, 1.25rem);
    bottom: clamp(0.75rem, 1.5vw, 1.25rem);
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.6rem 1rem;
    border-radius: var(--radius-pill);
    background: rgba(0, 0, 0, 0.72);
    color: #fff;
    font-size: clamp(0.75rem, 0.7vw, 1rem);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    line-height: 1;
    transition: background 0.2s ease;
}

.pm-link-card:hover .pm-link-badge { background: var(--pop); }

/* video + credit caption */
.pm-video-fig {
    /* one gap after ANY captioned block, before whatever's next — video,
       photo, or before/after. Used to be a flat 30px here and
       calc(var(--gap-out) * 1.75) on .pm-media-fig: two numbers for the
       same "this section is over" job. */
    margin-bottom: var(--cap-gap-bottom);
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
}

.pm-video-cap {
    /* one gap between ANY image and its caption — video, photo, or
       (below) before/after panel. Used to be --s3 here, 0.6rem when
       nested in .pm-media-fig, and --s2 on the compare panel figcaption
       — three different numbers doing the same job. */
    padding: var(--cap-gap-top) 0 0;
    text-align: center;
    /* same scale as BEFORE / AFTER */
    font-size: clamp(1rem, 0.94vw, 1.75rem);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--grey-mid);
    line-height: 1.5;
}

.pm-video-cap em { font-style: normal; }

/* inline video block — 16:9, centered, full width */
.pm-video {
    width: 100%;
    /* 16/9 default; a beat can override with `ratio` in main.js so
       wider formats (a 2.37:1 reel, say) don't sit letterboxed inside
       a 16:9 box with black bars baked on top of their own */
    aspect-ratio: var(--video-ratio, 16 / 9);
    background: #000;
    overflow: hidden;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
}

.pm-video video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: #000;
}

/* square grid — set --sq-cols for how many across */
.pm-sq-grid {
    display: grid;
    grid-template-columns: repeat(var(--sq-cols, 3), 1fr);
    gap: 8px;
    width: 100%;
    margin-bottom: 30px;   /* matches .pm-media */
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
}

.pm-sq {
    aspect-ratio: 1 / 1;
    background: var(--grey-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    color: var(--grey-mid);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.pm-sq img,
.pm-sq video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

@media (max-width: 820px) {
    .pm-sq-grid { grid-template-columns: repeat(2, 1fr); }
}

/* before | after comparison — two labeled panels, arrow between */
.pm-compare {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: clamp(1rem, 2.5vw, 2.5rem);
    padding-top: var(--s5);
    padding-bottom: var(--cap-gap-bottom);
}

.pm-compare-panel {
    margin: 0;
    min-width: 0;
}

.pm-compare-img {
    background: var(--grey-bg);
    border-radius: var(--radius);
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(0.75rem, 1.4vw, 1.5rem);
    color: var(--grey-mid);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.pm-compare-img img,
.pm-compare-img video {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.pm-compare-panel figcaption {
    margin-top: var(--cap-gap-top);
    /* same scale as the REBRANDING tag */
    font-size: clamp(1rem, 0.94vw, 1.75rem);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--grey-mid);
    text-align: center;
}

.pm-compare-arrow {
    font-size: clamp(2.5rem, 3.4vw, 4.5rem);
    color: var(--pop);
    line-height: 1;
    align-self: center;
    margin-bottom: 1.9rem;
}

@media (max-width: 820px) {
    .pm-compare {
        grid-template-columns: 1fr;
        padding-top: var(--s5);
        padding-bottom: var(--s5);
    }
    .pm-compare-arrow {
        transform: rotate(90deg);
        margin: 0 auto;
        justify-self: center;
    }
}

.pm-quote::before { content: "\201C"; color: var(--pop); }
.pm-quote::after  { content: "\201D"; color: var(--pop); }

.pm-lede {
    font-size: clamp(1.3rem, 1.25vw, 2rem);
    line-height: 1.6;
    color: var(--grey-dark);
    margin: 0 0 1.75rem;
}

/* meta strip — role / client / year */
.pm-meta {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1rem;
    padding: 1.1rem 0;
    border-top: 1.5px solid var(--grey-line);
    border-bottom: 1.5px solid var(--grey-line);
    margin-bottom: 2rem;
}

.pm-meta div { font-size: 0.95rem; font-weight: 600; }

.pm-meta span {
    display: block;
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--grey-mid);
    margin-bottom: 0.3rem;
}

.pm-h {
    font-family: var(--font-display);
    font-size: clamp(1.2rem, 1.5vw, 2.3rem);
    text-transform: uppercase;
    font-weight: 400;
    margin: 2.25rem 0 0.75rem;
}

.pm-p {
    font-size: clamp(1.1rem, 1.1vw, 1.7rem);
    line-height: 1.7;
    color: var(--grey-dark);
    margin: 0 0 1.25rem;
}

/* skill chips */
.pm-chips {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0;
    margin: 0 0 1.5rem;
}

.pm-chips li {
    font-size: 0.8rem;
    font-weight: 600;
    padding: 0.4rem 0.9rem;
    border: 1.5px solid var(--grey-line);
    border-radius: var(--radius-pill);
    color: var(--grey-dark);
}

/* media placeholder blocks between story beats */
.pm-media {
    background: var(--grey-bg);
    border-radius: 0;
    width: 100%;
    aspect-ratio: 16 / 9;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 30px;
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
    color: var(--grey-mid);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.pm-media img, .pm-media video { width: 100%; height: 100%; object-fit: cover; border-radius: 0; }

/* a block with real media drops the fixed ratio and shows
   the full asset — tall page screenshots stay uncropped */
.pm-media:has(img), .pm-media:has(video) { aspect-ratio: auto; background: var(--white); }
.pm-media:has(img) img { height: auto; object-fit: contain; }
.pm-cell:has(img), .pm-cell:has(video) { background: var(--white); }

/* split beat — text beside a tall/square image */
.pm-split {
    display: grid;
    grid-template-columns: 1fr 1.1fr;
    gap: clamp(1.5rem, 3vw, 3.5rem);
    align-items: center;
}

.pm-split-media.pm-media,
.pm-split-media.pm-media:has(img) { aspect-ratio: auto; border-radius: var(--radius); overflow: hidden; }
.pm-split-media img { border-radius: var(--radius); }

/* ── Brand guide section (coded, responsive) ─────── */
.pm-guide { padding-top: 0; }

.pm-cap {
    display: none; /* trying captionless — flip back by removing this line */
    font-size: clamp(1rem, 0.9vw, 1.35rem);
    color: var(--grey-mid);
    margin: 0 0 1.25rem;
}

.pm-cap-gap { margin-top: 8rem; }

/* intro header — uses the same type + spacing tokens as the
   front of the site so everything reads as one system */
.pm-intro {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding-top: var(--s3);
    padding-bottom: var(--s6);
}

.pm-intro .modal-tag {
    font-family: var(--font-body);
    /* 24px on the 2560 canvas = 0.94vw — tracks the scaled frame */
    font-size: clamp(1rem, 0.94vw, 1.75rem);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--pop);
    margin: 0 0 var(--s2);
}

.pm-intro .modal-title {
    font-family: var(--font-display);
    /* 96px on the 2560 canvas = 3.75vw */
    font-size: clamp(2.6rem, 3.75vw, 7rem);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    text-wrap: balance;
    /* optical: pull the display face flush with the label above */
    margin: 0 0 var(--s4);
    margin-left: -0.045em;
}

.pm-intro .pm-lede {
    font-family: var(--font-body);
    /* 28px on the 2560 canvas = 1.09vw */
    font-size: clamp(1.15rem, 1.09vw, 2.1rem);
    line-height: 1.65;
    color: var(--grey-dark);
    max-width: none;
    margin: 0;
}

@media (max-width: 820px) {
    .pm-intro {
        padding-top: var(--s2);
        padding-bottom: var(--s5);
    }
    .pm-intro .modal-title { font-size: clamp(2.4rem, 9vw, 4.5rem); }
    .pm-intro .pm-lede { font-size: clamp(1.05rem, 3.6vw, 1.5rem); }
    .pm-intro .modal-tag { font-size: clamp(0.85rem, 2.8vw, 1.1rem); }
}

/* brand sheet — logos + colors on grey tiles, like a real
   brand-guideline page */
.pm-guide-grid {
    display: grid;
    grid-template-columns: 1.6fr 1fr;
    gap: 14px;
}

.pm-guide-side {
    display: grid;
    grid-template-rows: 1fr auto;
    gap: 14px;
}

.pm-tile {
    background: #f7f7f8;
    border-radius: var(--radius);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(2rem, 4vw, 4.5rem);
    min-height: 220px;
}

.pm-tile img { width: 100%; max-width: 620px; }
.pm-guide-side .pm-tile img { max-width: 300px; }

.pm-swatches {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
}

.pm-swatch { border-radius: var(--radius); }

.pm-swatch {
    aspect-ratio: 1 / 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 12px 14px;
    font-size: clamp(0.8rem, 0.8vw, 1.1rem);
    font-weight: 600;
}

.pm-hex { opacity: 0.55; font-weight: 500; }

.pm-label-strip { width: 100%; margin: 6rem 0; }

.pm-jars {
    display: flex;
    justify-content: space-between; /* left · center · right */
    align-items: flex-end;
}

.pm-jars img { width: 21%; }

/* ── Screens section — desktop + mobiles on the slant bg ── */
.pm-screens {
    display: grid;
    grid-template-columns: 2.2fr 1fr; /* desktop leads, mobiles fill its height */
    gap: clamp(2.5rem, 5vw, 6rem);
    align-items: center; /* mobiles float centered beside the desktop */
    padding: var(--s7) clamp(2rem, 5.3vw, 4.7rem) 6rem;
    /* slant image sits at the BOTTOM at natural height — the top
       of the section stays white, the grey arrives mid-section */
    background-size: 100% auto;
    background-repeat: no-repeat;
    background-position: center bottom;
}

.pm-screen {
    width: 100%;
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
}

.pm-screens-mobile { margin-top: 0; }
.pm-screens-mobile .pm-screen { margin-bottom: clamp(2rem, 4vw, 4.5rem); }
.pm-screens-mobile .pm-screen:last-child { margin-bottom: 0; }

/* band section — spacing only, no background tint */
.pm-band {
    padding-top: clamp(2.5rem, 4vw, 4.5rem);
    padding-bottom: 3.5rem;
}

/* empty grid cells: black with white labels — obvious placement */
.pm-band .pm-cell:not(:has(img)):not(:has(video)) {
    background: var(--black);
    color: var(--white);
}

/* snapshot grid — 3 across, square cells */
.pm-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    width: 100%;
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
}



.pm-cell {
    background: var(--grey-bg);
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--grey-mid);
    font-size: 0.9rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: center;
    padding: 0.5rem;
}

.pm-cell img, .pm-cell video { width: 100%; height: 100%; object-fit: cover; }
.pm-cell:has(img), .pm-cell:has(video) { padding: 0; }

/* the payoff — one big number / result line */
.pm-result {
    display: flex;
    align-items: baseline;
    gap: 1rem;
    padding: 1.75rem 0 0.5rem;
    border-top: 1.5px solid var(--grey-line);
    margin-top: 2rem;
}

.pm-stat {
    font-family: var(--font-display);
    font-size: 3.2rem;
    color: var(--pop);
    line-height: 1;
}

.pm-statlabel { font-size: 1.2rem; color: var(--grey-dark); font-weight: 600; }

/* end CTA — hands off to the contact form */
.pm-cta {
    display: block;
    width: 100%;
    margin-top: 1.75rem;
    padding: 1.2rem;
    background: var(--black);
    color: var(--white);
    border: none;
    border-radius: var(--radius);
    font-family: var(--font-body);
    font-size: clamp(1rem, 0.9vw, 1.4rem);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.2s ease;
}

.pm-cta:hover { background: var(--pop); }

/* ── Inline link button (e.g. IMDb under the trailer) ──
   Same shape as .pm-cta but outlined and auto-width, so it reads as a
   secondary action next to the big black CTA at the end of the modal. */
.pm-btn-wrap {
    margin-top: 1rem;
    text-align: center;
}

.pm-btn {
    display: block;
    width: 100%;
    padding: 1.2rem 1.6rem;
    text-align: center;
    background: transparent;
    color: var(--black);
    border: 2px solid var(--black);
    border-radius: var(--radius);
    font-family: var(--font-body);
    font-size: clamp(0.9rem, 0.8vw, 1.15rem);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.pm-btn:hover {
    background: var(--black);
    color: var(--white);
}

/* IMDb yellow, black type — reads as "this goes to IMDb" before the
   label is even read. Hover just deepens the yellow rather than
   inverting, so the brand colour stays put. */
.pm-btn.is-imdb {
    background: #F5C518;
    color: var(--black);
    border-color: #F5C518;
}

.pm-btn.is-imdb:hover {
    background: #E2B408;
    border-color: #E2B408;
    color: var(--black);
}

/* ── Cursor "VIEW" badge ─────────────────────────── */
.cursor-badge {
    position: fixed;
    top: 0; left: 0;
    z-index: 90;
    background: var(--pop);
    color: var(--white);
    font-family: var(--font-display);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.5rem 0.9rem;
    border-radius: var(--radius-pill);
    pointer-events: none;
    opacity: 0;
    transform: translate(-50%, -130%) scale(0.8);
    transition: opacity 0.15s ease, transform 0.15s ease;
    will-change: left, top;
}

.cursor-badge.is-on {
    opacity: 1;
    transform: translate(-50%, -130%) scale(1);
}

@media (hover: none) {
    .cursor-badge { display: none; }
}

/* ═══════════ VERTICAL LAYOUT ═══════════
   Kicks in when the window is narrow, portrait, or even
   near-square — the horizontal canvas only earns its keep
   on properly widescreen windows (aspect > 5:4). */
/* vertical layout: narrow, portrait, squarish — or too short for
   the 2560x1200 canvas to scale above 50% and stay readable */
/* MUST match isVertical() in main.js exactly.
   MIN_SCALE 0.62 on a 2560x1200 canvas means the horizontal layout
   needs at least 1300px wide and 560px tall. Narrower than the
   canvas is fine on its own — the canvas crops at the window
   edge rather than shrinking — so these are just the point
   where too little of it is left to be worth showing.
   Mirrors VERT_MAX_W / VERT_MAX_H in main.js. Change both.
   If these numbers drift apart, JS stops scaling the canvas while
   CSS keeps the horizontal layout — an unscaled 2560px rail. */
/* ── VERTICAL LAYOUT ──────────────────────────────────
   Driven by ONE source of truth: main.js puts .is-vertical on
   <html> from isVertical(). It used to be a media query that
   had to be kept numerically identical to the JS — and any
   drift (a scrollbar's worth of width was enough) left JS in
   vertical while CSS stayed horizontal, which rendered the
   2560px rail at scale 1 and looked broken. A class can't
   disagree with itself, so that in-between state is gone. */
html.is-vertical {
    .hscroll { height: auto; }
    & body { overflow-x: hidden; }

    .hviewport {
        /* position:static already neutralizes top/left/right, but
           z-index must be cleared explicitly or the stacked layout
           keeps a stacking context it doesn't need */
        position: static;
        z-index: auto;
        height: auto;
        overflow: visible;
        display: block;
    }

    /* frame stops being a canvas — flows normally.
       will-change/contain must reset too, or they trap the
       fixed footer carousel inside the frame */
    .frame {
        width: 100%;
        height: auto;
        transform: none !important;
        /* zoom must be reset here too — the horizontal canvas may be
           using zoom instead of transform (see html.scale-zoom), and
           an inherited zoom would scale the whole stacked layout. */
        zoom: 1 !important;
        overflow: visible;
        will-change: auto;
        contain: none;
    }

    /* no edge fades in the vertical layout */
    .frame::before,
    .frame::after,
    .marquee-bar::before,
    .marquee-bar::after { display: none; }

    .rail {
        display: flex;
        flex-direction: column;
        transform: none !important;
        animation: none !important;
        /* same trap as .frame: a live will-change makes this a
           containing block, which re-anchors the fixed footer
           carousel to the rail instead of the viewport */
        will-change: auto;
        height: auto;
        width: 100%;
        /* a headline spanning 1400px on a "mobile" layout looks
           broken — cap the measure so the column stays a column
           right up to the switch point */
        max-width: 900px;
        margin-inline: auto;
        gap: 1.5rem;
        /* fixed side padding, not vw — the fit measures this column,
           and a vw padding makes the measurement drift with width */
        padding: 3.5rem clamp(1.25rem, 4vw, 2.5rem) 1rem;
    }

    .block { width: 100%; text-align: center; }

    /* The horizontal rail moves its three sections individually
       (see writeRailX in main.js). JS clears those inline transforms
       when switching to vertical, but this guarantees it regardless
       of ordering — a stale translate here would offset the whole
       stacked layout sideways. */
    .block-intro,
    .c-all,
    .block-army { transform: none !important; }

    .block-intro,
    .block-army { width: 100%; }

    .block-army {
        text-align: center;
        /* the objects already end in a lot of transparent padding —
           64px on top of that reads as an empty screen */
        padding: 2rem 0 0;
        margin-top: 0;
        /* if the objects' float ever swings into it, the copy wins */
        position: relative;
        z-index: 2;
    }
    .block-army .built-tag { justify-content: center; }

    /* forced line breaks are desktop-only — text wraps naturally here */
    .desktop-br { display: none; }

    .site-logo { width: clamp(200px, 30vw, 320px); margin: 0 auto 1.25rem; }
    /* auto-fit runs here too — the headline fills this column the
       same way it fills the horizontal one, so it stays two lines
       and barely changes size across the breakpoint */
    .h-display .hl { margin-inline: auto; }
    .block-army .h-display .hl { margin-left: auto; }
    .sub { font-size: clamp(1.05rem, 2.6vw, 1.55rem); max-width: 100%; }

    /* Most of the gap under the headline is NOT margin — it's the
       empty descender space inside Archivo Black's line box, which
       grows with the font size. So the margin is negative in em:
       it trims that dead space first, then leaves a real gap.
       em, not px, so it scales with the auto-fit headline. */
    .h-display { margin-bottom: calc(var(--s2) - 0.22em); }

    /* same trim under the contact headline */
    .contact-form { margin-top: calc(var(--s2) - 0.1em); }
    .scroll-cue { display: none; }

    /* the group becomes a loose wrapping row — and unlike the text
       it breaks out of the capped column to run the full screen */
    .cluster,
    .c-all {
        position: static;
        height: auto;
        /* a real grid, not a wrapping row — equal cells keep the
           rows aligned instead of drifting into a V */
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        justify-items: center;
        align-items: center;
        gap: 0 0;

        width: 100vw;
        max-width: 100vw;
        /* Was calc(-10vh + 16px) — a deliberate negative pull to crowd
           the copy, but on a tall window that resolves to well over
           -100px and the jar lands on the paragraph. Positive gap that
           scales with the viewport, matching the 2-column tier. */
        /* 30px tighter top and bottom. max() keeps the top from going
           negative if the viewport gets short enough that the clamp
           lands under 30px. Only the 3-column tier — the 2-column
           tiers below set their own margin-top. */
        margin: max(0px, calc(clamp(1rem, 3vh, 2.5rem) - 30px))
                calc(50% - 50vw)
                -30px;
        padding-inline: clamp(0.5rem, 2vw, 1.5rem);
        /* objects run wider than their cells — let them spill */
        overflow: visible;
    }

    .obj {
        position: static;
        display: flex;
        justify-content: center;
        width: 100% !important;
    }

    .obj-img { margin: 0 auto; }

    /* each object fills a share of its cell equal to its share of
       the desktop sizes — the size relationships carry over */
    /* deliberately WIDER than their cells — the objects overflow
       and overlap like they do on the horizontal canvas. The size
       ratios between them are unchanged, everything is just scaled. */
    .obj-jar    { width: 92% !important; }
    .obj-star   { width: 95% !important; }
    .obj-video  { width: 97% !important; }
    .obj-lagree { width: 110% !important; }
    .obj-poster { width: 118% !important; }
    .obj-uiux   { width: 150% !important; }

    /* Objects deliberately overlap in this layout (92%–150% widths,
       zero gap), so shadows overlap too and compound into a dark
       smudge where they cross. Lighter variant instead. */
    /* SHADOWS OFF — see .obj-img above */
    /* .obj-img { filter: var(--shadow-obj-vertical); } */
    /* .obj:hover .obj-img { filter: var(--shadow-obj-vertical-hover); } */

    /* middle column rides lower — the sub-copy sits in the notch
       between the left/right objects instead of being covered by
       the center one. Raise the first value if it still crowds. */
    .cluster .obj:nth-child(2) { margin-top: clamp(3rem, 10vh, 9rem); }
    .cluster .obj:nth-child(5) { margin-top: clamp(1rem, 3vh, 2.5rem); }

    /* the outer two on the top row clear the sub-copy instead of
       riding up over it — small nudge, not a layout change */
    .cluster .obj:nth-child(1),
    .cluster .obj:nth-child(3) { margin-top: 22px; }

    /* and if they ever do touch it, the copy wins */
    .block-intro { position: relative; z-index: 2; }

    /* NO animation-delay override here — each object carries its own
       negative --delay inline, which is what puts them all out of
       phase. Overriding it makes them sway in unison. */

    /* carousel locked to the bottom like a footer bar */
    /* ══ FIXED MOBILE FOOTER ══════════════════════════
       The textbook recipe, nothing clever:
         · position:fixed, bottom:0, left:0, width:100%
         · min-height:100dvh on the body so the mobile URL
           bar can collapse without the footer overlapping
         · bottom padding on the content so nothing hides
           behind the bar
       The one non-obvious requirement is that the bar is a
       direct child of <body> — inside .frame it had a
       transformed ancestor, which re-anchors a fixed element
       to that ancestor instead of the viewport. main.js
       guarantees the placement. */
    & { height: auto; }

    & body {
        min-height: 100dvh;
        display: block;
        overflow-x: clip;      /* clip, not hidden — see the note above */
        overflow-y: visible;
    }

    /* The desktop rule already uses position:fixed correctly. The old
       mobile override changed this to position:sticky, but the marquee
       is the LAST child of <body>; that means its normal position is at
       the end of the document, so it cannot appear at the bottom of the
       screen until the user has nearly finished scrolling. Keep it fixed
       on mobile too. Because it is a direct child of <body>, no transformed
       ancestor can trap it. */
    .marquee-bar {
        position: fixed;
        inset-inline: 0;
        bottom: 0;
        width: auto;
        height: calc(64px + env(safe-area-inset-bottom));
        margin: 0;
        padding: 0 0 env(safe-area-inset-bottom);
        z-index: 9999;
        display: flex;
        align-items: center;
        overflow: hidden;
        background: var(--white);
        border-top: 1px solid var(--grey-line);
    }

    .marquee-inner img { height: 39px; padding: 0 1.4rem; }
    .marquee-inner img.logo-nbc { height: 19.5px; }

    /* Fixed elements are removed from normal flow, so reserve their
       height at the bottom of the page. This keeps the contact form and
       final content from disappearing behind the logo strip. */
    .rail {
        padding-bottom: calc(64px + env(safe-area-inset-bottom) + 24px);
    }

    .contact-form { max-width: 100%; margin-inline: auto; }
    .contact-form input,
    .contact-form textarea { font-size: 1rem; padding: 0.85rem 1rem; }
    .form-row { grid-template-columns: 1fr; }
    .contact-form textarea,
    #cfSubject { min-height: 150px; }
    .form-send { font-size: 1rem; padding: 1rem 2rem; }
    .built-tag { font-size: 0.85rem; }
}

/* ════════════════════════════════════════════════════════
   FULLSCREEN MODAL TRIAL
   Shell goes edge-to-edge; hero art fills the width while
   every content block stays capped and centered.
   Delete this block to revert to the windowed popup.
   ════════════════════════════════════════════════════════ */
:root { --modal-content: 1780px; }

.modal { padding: 0; }

.modal-box {
    max-width: 100%;
    width: 100%;
    max-height: 100vh;
    height: 100vh;
    border-radius: 0;
    box-shadow: none;
}

.modal-scroll { max-height: 100vh; }

.modal.is-open .modal-box {
    animation: modal-rise 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes modal-rise {
    from { transform: translateY(22px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}

/* close button floats over the top-right corner */
.modal-close {
    position: fixed;
    top: clamp(1rem, 2vw, 2rem);
    right: clamp(1rem, 2vw, 2rem);
}

/* ── content blocks stay capped and centered ──
   NOTE: everything in this list is centred with auto side margins.
   Any rule for these selectors must use margin-BOTTOM (or margin-top),
   never the `margin` shorthand — the shorthand resets left/right to 0
   and silently pins the block to the left. That exact bug pushed the
   whole design popup off-centre once already. */
.pm-pad,
.pm-media,
.pm-media-fig,
.pm-sq-grid,
.pm-grid,
.pm-video-grid,
.pm-compare,
.pm-video-fig,
.pm-link-fig,
.pm-screens,
.pm-guide {
    max-width: var(--modal-content);
    margin-left: auto;
    margin-right: auto;
}

/* isolate the scroller so paints don't leak into the page behind */
.modal-scroll {
    /* NO translateZ(0) and NO contain: paint here.

       Both were "isolate the scroller" optimizations, and together
       they were the cause of the bleed-through: they let Chrome keep
       a rasterized copy of the scroller's contents that survived the
       innerHTML swap when you open a different project. The old
       project's images then painted into the gaps of the new one.

       Removing translateZ alone wasn't enough — contain: paint on its
       own still licenses that reuse. Neither is needed: the scroller
       is a plain overflow container and repaints correctly without
       being pinned. If scroll performance inside a long modal ever
       becomes an issue, fix it with content-visibility on the beats,
       not by re-pinning this element. */
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

/* hero art is the one thing that fills the full width */
.modal-media-box,
.pm-hero-plain,
.pm-hero-center { max-width: none; }


/* ════════════════════════════════════════════════════════
   MODAL RHYTHM — two tiers, nothing else
     inside a block (grid gutters, jar row)  =  8px
     between blocks (section to section)     = 64px
   ════════════════════════════════════════════════════════ */
:root {
    --gap-in:  8px;                /* within one block */
    --gap-out: var(--s6);          /* between blocks — 64px */

    /* the ONE caption rhythm — every captioned figure (video, photo,
       before/after) uses these two, so nothing has its own number.
       Top gap uses --s2 (16px) — the tighter of the two values we had
       (before/after's), not --s3 (24px), which read as too loose once
       applied to photo captions that used to sit much closer. */
    --cap-gap-top:    var(--s2);                  /* image -> caption */
    --cap-gap-bottom: calc(var(--gap-out) * 1.75); /* caption -> next section */
}

/* ── every block gets the same gap beneath it ── */
.pm-media,
.pm-sq-grid,
.pm-grid,
.pm-compare,
.pm-video-fig,
.pm-link-fig,
.pm-screens,
.pm-guide {
    margin-top: 0;
    margin-bottom: var(--gap-out);
}

/* ── text blocks match that gap so nothing special-cases ── */
.pm-intro {
    padding-top: var(--s3);
    padding-bottom: var(--gap-out);
}

.pm-quote {
    padding-top: var(--gap-out);
    padding-bottom: var(--gap-out);
}

/* ── grid gutters stay tight — these are one object ── */
.pm-grid,
.pm-sq-grid { gap: var(--gap-in); }

/* ── blocks that carry their own padding shouldn't double up ── */
.pm-compare,
.pm-screens,
.pm-guide {
    padding-top: 0;
    padding-bottom: 0;
}

.pm-band {
    padding-top: 0;
    padding-bottom: 0;
}

/* ── Nugz internals: parts of one brand sheet, so they stay tight ── */
.pm-guide-grid { margin-bottom: var(--gap-out); }
.pm-label-strip { width: 100%; margin: 0 0 var(--gap-out); }
.pm-cap-gap { margin-top: var(--gap-out); }
.pm-jars { margin-bottom: 0; }

/* hero sits flush against the intro */
.modal-media-box { margin-bottom: 0; }

/* CTA: one gap above it, one below — nothing stacked.
   Uses a descendant selector because in banded modals the
   CTA sits inside .pm-band, not directly under .pm-body. */
.pm-body .pm-pad:last-child,
.pm-band .pm-pad:last-child {
    padding-top: 0 !important;
    padding-bottom: var(--gap-out) !important;
}

.pm-cta { margin-top: 0 !important; }

/* nothing above the CTA contributes its own bottom gap twice */
.pm-video-fig:last-of-type,
.pm-link-fig:last-of-type { margin-bottom: var(--gap-out); }

/* one source per gap: every block owns the space BELOW it only,
   so nothing stacks and both sides of a quote match */
.pm-quote {
    padding-top: 0;
    padding-bottom: var(--gap-out);
}

/* opt-in: a block that belongs with the one after it sits at the
   tight gap instead of the section gap. Set tight: true on the beat. */
.is-tight { margin-bottom: var(--gap-in) !important; }


/* ════════════════════════════════════════════════════════
   RESPONSIVE PASS
   Breakpoints: 1100 (layout switches to vertical) · 820 (mobile)
   Type: every modal size derives from the 2560 canvas so the
   modal and the front page scale together.
   ════════════════════════════════════════════════════════ */

/* ── touch targets: 44px minimum ── */
@media (hover: none) {
    .modal-close { width: 3.25rem; height: 3.25rem; }
    .pm-cta { padding: 1.15rem 1rem; }
    .form-send { min-height: 52px; }
    .obj { min-width: 44px; min-height: 44px; }
}

/* ── mobile ── */
@media (max-width: 820px) {
    :root { --gap-out: var(--s5); }        /* 48px sections */

    /* content runs closer to the edge — 4.7rem is too much at 375px */
    .pm-pad,
    .pm-media,
    .pm-sq-grid,
    .pm-grid,
    .pm-compare,
    .pm-video-fig,
    .pm-link-fig,
    .pm-screens,
    .pm-guide {
        padding-left: clamp(1rem, 5vw, 2rem);
        padding-right: clamp(1rem, 5vw, 2rem);
    }

    /* brand sheet stacks */
    .pm-guide-grid { grid-template-columns: 1fr; }
    .pm-swatches { grid-template-columns: repeat(3, 1fr); }

    /* jars wrap instead of squeezing to nothing */
    .pm-jars { flex-wrap: wrap; gap: var(--gap-in); justify-content: center; }
    .pm-jars img { width: 46%; }

    /* quote scales down with the viewport */
    .pm-quote { font-size: clamp(1.1rem, 4.6vw, 1.6rem); }

    /* captions */
    .pm-video-cap,
    .pm-compare-panel figcaption { font-size: clamp(0.8rem, 3vw, 1rem); }

    /* close button clears the notch */
    .modal-close { top: max(1rem, env(safe-area-inset-top)); right: 1rem; }
}

/* object grid: 3 across -> 2 -> 1 (widths are shares of a cell,
   so they don't change — only the column count does) */
/* Wrapped in html.is-vertical so it can still beat the main
   vertical block. That block used to be a media query too, so
   plain source order settled these; now it is a class rule and
   out-specifies a bare .cluster — which is what silently killed
   the two-column grid. Same scoping restores the old outcome. */
html.is-vertical {
    @media (max-width: 820px) {
        .cluster,
        .c-all {
            grid-template-columns: repeat(2, 1fr);
            gap: 0 0;
            /* Two columns puts an object directly under the copy. This
               used to be calc(-10vh + 72px) — a NEGATIVE pull on any
               screen taller than 720px, which is why the jar kept
               landing on the last line of the paragraph. Now it is
               always a positive gap that grows with the viewport. */
            margin-top: clamp(0.5rem, 2vh, 1.75rem);
            /* Full-bleed with PROPORTIONAL padding — this is the fix for
               "it changes shape as you scale". A fixed max-width made the
               cluster a different fraction of the screen at 820px than at
               1580px, so the composition drifted. vw padding means the
               cells, the objects and the gaps are all the same fraction of
               the window at every width — it scales like the headline. */
            width: 100vw;
            max-width: 100vw;
            margin-left: calc(50% - 50vw);
            margin-right: calc(50% - 50vw);
            /* the min is 2.5rem, not 1rem: 100vw includes the scrollbar,
               so a full-bleed element sits ~15px wider than the visible
               area and body's overflow-x:hidden shears whatever overhangs.
               This is the gutter the widest object (tacos) spills into. */
            padding-inline: clamp(2.5rem, 7vw, 7rem);
        }

        /* offsets go through MARGIN, not transform — .obj already runs the
           float-y animation on transform, so a transform here is overwritten
           every frame and does nothing. Values are deliberately unrelated so
           no two objects mirror each other. */
        /* Horizontal offsets are MIRRORED PAIRS per row (+6/-6, +9/-9,
           +3/-3) so each row's pull cancels out and the group can never
           lean. The vertical offsets stay uneven — that's where the
           organic feel comes from, and it costs no balance. */
        .cluster .obj:nth-child(1) { margin:  0    0   -5%   6%; }
        .cluster .obj:nth-child(2) { margin:  7%   6%  -5%   0;  }
        .cluster .obj:nth-child(3) { margin:  4%   0   -5%   9%; }
        .cluster .obj:nth-child(4) { margin: -5%   9%  -5%   0;  }
        .cluster .obj:nth-child(5) { margin:  2%   0    0    3%; }
        .cluster .obj:nth-child(6) { margin:  9%   6%   0    0;  }

        /* ── FORCE THEM OUT OF PHASE ──────────────────────────
           The inline --delay values weren't spread far enough apart
           to read as independent. These are: (a) a hand-picked delay
           per object, (b) HALF of them running alternate-reverse, so
           at any moment some are rising while others are falling.
           Direction is what actually sells it — matched delays still
           look like a wave, opposite directions never do. */
        .c-all .obj-jar    { animation-delay: -2.4s; animation-direction: alternate; }
        .c-all .obj-lagree { animation-delay: -1.1s; animation-direction: alternate-reverse; }
        .c-all .obj-star   { animation-delay: -4.2s; animation-direction: alternate-reverse; }
        .c-all .obj-poster { animation-delay: -3.3s; animation-direction: alternate; }
        .c-all .obj-video  { animation-delay: -0.7s; animation-direction: alternate-reverse; }
        .c-all .obj-uiux   { animation-delay: -2.9s; animation-direction: alternate; }

        /* the sideways sway runs on its own clock AND its own direction,
           independent of the rise/fall above */
        .obj-jar    .obj-float { animation-delay: -5.9s; animation-direction: alternate-reverse; }
        .obj-lagree .obj-float { animation-delay: -3.4s; animation-direction: alternate; }
        .obj-star   .obj-float { animation-delay: -1.6s; animation-direction: alternate; }
        .obj-poster .obj-float { animation-delay: -7.1s; animation-direction: alternate-reverse; }
        .obj-video  .obj-float { animation-delay: -4.5s; animation-direction: alternate; }
        .obj-uiux   .obj-float { animation-delay: -2.2s; animation-direction: alternate-reverse; }

        /* two columns = wider cells, so the shares come back down
           or the objects swallow each other */
        .obj-jar    { width: 82% !important; }
        .obj-star   { width: 84% !important; }
        .obj-video  { width: 86% !important; }
        .obj-lagree { width: 96% !important; }
        .obj-poster { width: 104% !important; }
        .obj-uiux   { width: 112% !important; }
    }
}

/* NO single-column tier. One object per row reads as a list, not a
   pile — the overlap and the offsets are the whole effect, and both
   need a neighbour to work against. Phones keep the two-column grid
   and just get a tighter gutter and slightly smaller objects. */
/* Wrapped in html.is-vertical so it can still beat the main
   vertical block. That block used to be a media query too, so
   plain source order settled these; now it is a class rule and
   out-specifies a bare .cluster — which is what silently killed
   the two-column grid. Same scoping restores the old outcome. */
html.is-vertical {
    @media (max-width: 480px) {
        .cluster,
        .c-all {
            grid-template-columns: repeat(2, 1fr);
            padding-inline: 0.75rem;
            /* a touch tighter than the 820px tier — small screens have
               less height to give away, but never negative */
            margin-top: clamp(0.5rem, 1.5vh, 1.1rem);
        }

        /* pushed harder than on desktop — at this width a small % is only
           a few px, so the zig-zag needs bigger numbers to still read.
           Still mirrored per row, so it can't lean. */
        .cluster .obj:nth-child(1) { margin:  0    0   -8%  12%; }
        .cluster .obj:nth-child(2) { margin:  8%  12%  -8%   0;  }
        .cluster .obj:nth-child(3) { margin:  5%   0   -8%  16%; }
        .cluster .obj:nth-child(4) { margin: -6%  16%  -8%   0;  }
        .cluster .obj:nth-child(5) { margin:  3%   0    0    8%; }
        .cluster .obj:nth-child(6) { margin: 10%   8%   0    0;  }

        .obj-jar    { width: 88% !important; }
        .obj-star   { width: 90% !important; }
        .obj-video  { width: 92% !important; }
        .obj-lagree { width: 100% !important; }
        .obj-poster { width: 108% !important; }
        /* bigger than its neighbours by more than it looks — the artwork
           carries a lot of empty space around the pencil and ruler, so the
           drawing itself lands smaller than the box it ships in */
        .obj-uiux   { width: 132% !important; }
    }
}

/* ── small phones ── */
@media (max-width: 420px) {
    .pm-sq-grid,
    .pm-grid { grid-template-columns: 1fr; }
    .pm-compare-img { aspect-ratio: 4 / 3; }
}

/* short windows: shrink the marquee reserve so the rail's
   centered content actually sits in the middle instead of
   riding high above a 120px gap */
@media (max-height: 800px) {
    :root { --marquee-h: 84px; }
}

@media (max-height: 700px) {
    :root { --marquee-h: 64px; }
}

/* ════════════════════════════════════════════════════════
   MOBILE FIXES — must stay LAST in the file.

   Three things were wrong:

   1. SIDEWAYS SCROLL. Something inside the column renders
      wider than the screen, so the document gets a
      horizontal scroll range. overflow-x:clip on <body> was
      supposed to catch it, but (a) clip on the root/body is
      unreliable in Chrome and (b) overflow on <body> makes
      <body> a clipping context, which is what unpinned the
      carousel. The clip now lives on <html> as
      overflow-x:hidden, and every box in the chain is
      hard-capped at 100vw so there is nothing to scroll to
      in the first place.

   2. HEADLINE / FORM RUNNING OFF THE RIGHT EDGE. The
      headline is sized by JS (--fit) and the fields inherit
      that measure via --form-w. When the measured column is
      wider than the phone, both overshoot. There is now a
      viewport-based ceiling on the headline and a hard 100%
      cap on the form, so neither can exceed the screen even
      if the measurement is off.

   3. CAROUSEL PIN. Kept fixed to the viewport, out of any
      clipping context.
   ════════════════════════════════════════════════════════ */
/* ── VERTICAL LAYOUT ──────────────────────────────────
   Driven by ONE source of truth: main.js puts .is-vertical on
   <html> from isVertical(). It used to be a media query that
   had to be kept numerically identical to the JS — and any
   drift (a scrollbar's worth of width was enough) left JS in
   vertical while CSS stayed horizontal, which rendered the
   2560px rail at scale 1 and looked broken. A class can't
   disagree with itself, so that in-between state is gone. */
html.is-vertical {

    /* ── 1 · nothing wider than the screen, ever ── */
    & {
        overflow-x: hidden;   /* hidden, not clip — clip on the root is ignored */
        width: 100%;
        max-width: 100%;
    }

    & body {
        overflow-x: visible;  /* NOT a clipping context — the fixed bar lives here */
        overflow-y: visible;
        width: 100%;
        max-width: 100%;
    }

    .hscroll,
    .hviewport,
    .frame {
        width: 100%;
        max-width: 100vw;
    }

    /* one number governs the whole vertical layout's max size */
    & { --vmax: 1430px; }

    /* The 900px cap was a readable-measure rule borrowed from the phone
       layout, but in the 3-column tier the window is 1500-2100 wide and
       it left the headline looking undersized against the art. The
       headline auto-fits to this column, so widening the cap scales the
       type up with it — and on anything narrower than 1180 this still
       resolves to 100vw, so phones are untouched. */
    .rail {
        width: 100%;
        max-width: min(1180px, 100vw);
        margin-inline: auto;
    }

    .block,
    .block-intro,
    .block-army {
        width: 100%;
        max-width: 100%;
        min-width: 0;
    }

    /* The floating objects are full-bleed (width:100vw) so they spill
       to the screen edges on a phone. On a SHORT-but-WIDE window the
       vertical layout also kicks in, and full-bleed meant the art just
       kept growing with the window. Capped at --vmax and centered:
       below 1430 it is still edge-to-edge, above it the block stops
       growing and sits in the middle. */
    .cluster,
    .c-all {
        width: min(100vw, var(--vmax));
        max-width: min(100vw, var(--vmax));
        margin-inline: calc(50% - min(50vw, var(--vmax) / 2));
        /* NO clip here. The clip used to sit at the screen edge, which
           was fine — now the box stops at --vmax, so clipping chopped
           the art and its shadows mid-image. Let them spill; the only
           thing the clip was protecting against was sideways scroll,
           and html{overflow-x:hidden} above already handles that. */
        overflow: visible;
    }

    /* ── 2 · type + fields can't outgrow the screen ── */

    /* --fit is set by main.js. min() puts a viewport ceiling on it:
       when the measurement is right this changes nothing, when it
       overshoots this is what keeps the headline on screen. The
       divisor is the width of the longest line in em for Archivo
       Black — lower it if the headline ever looks too small. */
    .h-display {
        font-size: min(var(--fit, 2.5rem), calc((100vw - 2.75rem) / 9));
    }

    .h-display .hl {
        max-width: 100%;
        margin-inline: auto;
    }

    .block-army .h-display .hl { margin-inline: auto; }

    /* --form-w is a desktop measure — on a phone the fields are just
       the column */
    .contact-form {
        width: 100%;
        max-width: 100% !important;
        margin-inline: auto;
    }

    .form-row { grid-template-columns: 1fr; }

    .contact-form input,
    .contact-form textarea {
        width: 100%;
        max-width: 100%;
        font-size: clamp(1rem, 1.35vw, 1.3rem);
        padding: clamp(0.8rem, 1.1vw, 1.15rem) clamp(0.95rem, 1.3vw, 1.4rem);
    }

    .contact-form textarea,
    #cfSubject { min-height: 140px; }

    .form-send {
        width: 100%;
        font-size: clamp(1rem, 1.35vw, 1.3rem);
        padding: clamp(0.95rem, 1.3vw, 1.35rem) 1.5rem;
    }

    .sub {
        max-width: 100%;
        /* floor unchanged so phones read the same — only the ceiling
           moved, so the paragraph grows with the wider column */
        font-size: clamp(1rem, 2.4vw, 1.75rem);
        /* the old gap was calc(--s2 - 0.22em) on .h-display. 0.22em is
           relative to the HEADLINE, so at the top of the vertical range
           (~1299px) it grew to ~20px and swallowed the whole gap. Gap is
           now driven by the viewport, so it holds at every width. */
        margin-top: clamp(1rem, 2.4vw, 2.25rem);
    }

    .h-display { margin-bottom: 0; }

    /* ── 3 · carousel: a real viewport footer ── */
    .marquee-bar {
        position: fixed !important;
        left: 0;
        right: 0;
        bottom: 0;
        top: auto;
        width: 100%;
        max-width: 100vw;
        height: calc(64px + env(safe-area-inset-bottom));
        padding-bottom: env(safe-area-inset-bottom);
        margin: 0;
        z-index: 9999;
        display: flex;
        align-items: center;
        overflow: hidden;
        background: var(--white);
        border-top: 1px solid var(--grey-line);
        transform: translateZ(0);
    }

    .marquee-inner img { height: 36px; padding: 0 1.2rem; }
    .marquee-inner img.logo-nbc { height: 18px; }

    /* reserve the bar's height so the send button never hides behind it */
    .rail { padding-bottom: calc(88px + env(safe-area-inset-bottom)); }
}

/* tighter gutters on small phones */
/* Wrapped in html.is-vertical so it can still beat the main
   vertical block. That block used to be a media query too, so
   plain source order settled these; now it is a class rule and
   out-specifies a bare .cluster — which is what silently killed
   the two-column grid. Same scoping restores the old outcome. */
html.is-vertical {
    @media (max-width: 420px) {
        .rail { padding-inline: 1rem; }
        .contact-form input,
        .contact-form textarea { padding: 0.75rem 0.9rem; }
    }
}

/* modal sits above the fixed carousel (which is z-index 9999) */
.modal { z-index: 10000; }
.cursor-badge { z-index: 10001; }


/* ── Pull-quote breathing room ─────────────────────────
   The quote was sharing the "one source per gap" rule with the media
   around it, so it got the same 64px as any two stacked images and read
   as cramped. A quote is a beat of its own — it needs air on both sides.
   Padding, not margin: .pm-body is block flow, so margins here would
   collapse into the neighbour's gap and change nothing. */
.pm-quote {
    padding-top: calc(var(--gap-out) * 0.5);      /* + the 64px above it */
    padding-bottom: calc(var(--gap-out) * 1.5);   /* matches, on its own */
}

/* captioned still image — same treatment as a captioned video */
.pm-media-fig {
    /* bottom only — see the note further down: a `margin` shorthand
       here cancels the shared auto-centering */
    margin-bottom: 30px;
}


/* The button sits in a .pm-pad wrapper, which carries 3.5rem of vertical
   padding meant for text blocks — stacked on the figure's own 30px that
   pushed it miles below the trailer. Kill the vertical padding, keep the
   horizontal so it still lines up with everything else. */
.pm-btn-wrap {
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 0.75rem;
}


/* Flush variant: the button is welded to the bottom edge of the video
   above it. The negative margin cancels .pm-video-fig's own 30px, and
   the horizontal padding already matches the figure's, so the bar lines
   up with the video exactly. */
.pm-btn-wrap.is-flush {
    margin-top: -30px;
    padding-top: 0;
    padding-bottom: 0;
}

.pm-btn.is-square { border-radius: 0; }

.pm-btn-wrap.is-flush .pm-video-cap { padding-top: var(--cap-gap-top); }


/* Truly flush: zero the video figure's own bottom margin when a flush
   button follows it, rather than trying to cancel it with a negative
   margin that has to guess the exact value. display:block on the video
   also kills the inline baseline gap under it. */
.pm-video-fig:has(+ .pm-btn-wrap.is-flush) { margin-bottom: 0; }

.pm-video,
.pm-video video { display: block; }

.pm-btn-wrap.is-flush { margin-top: 0; }


/* Flush button sitting ON TOP of the video: nothing between the bar's
   bottom edge and the video's top edge. The :has() rule above covers
   the button-after-video case; this covers button-before-video. */
.pm-btn-wrap.is-flush { margin-bottom: 0; }

.pm-btn-wrap.is-flush + .pm-video-fig { margin-top: 0; }


/* The flush button works in either position, so it keeps a normal gap
   BELOW it for whatever follows; the rule beneath cancels that gap only
   when the thing following is the video (button-on-top case). */
.pm-btn-wrap.is-flush { margin-bottom: 30px; }

.pm-btn-wrap.is-flush + .pm-video-fig { margin-top: 0; }


/* ── Small inline chip beside a caption ────────────────
   Replaces the full-width yellow bar: the credit line and the link sit
   on one row, so the link reads as a footnote to the caption rather
   than as a second call to action competing with the CTA below. */
.pm-video-cap.has-btn {
    display: flex;
    align-items: center;
    justify-content: space-between;   /* credit hard left, chip hard right */
    column-gap: 1.5rem;               /* only a floor — space-between does the rest */
    row-gap: 0.9rem;                  /* used once the row wraps on narrow screens */
    flex-wrap: wrap;
    text-align: left;
}

.pm-chip {
    display: inline-block;
    flex: 0 0 auto;
    padding: 0.4em 0.9em;
    border-radius: 8px;
    font-family: var(--font-body);
    font-size: 0.78em;          /* relative to the caption beside it */
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    /* base colours — without these a chip with no variant class fell
       back to the browser's default link styling (purple when visited) */
    background: var(--black);
    color: var(--white);
    transition: background 0.2s ease, transform 0.2s ease;
}

.pm-chip:hover {
    background: var(--pop);
    transform: translateY(-1px);
}

.pm-chip.is-imdb {
    background: #F5C518;
    color: var(--black);
}

.pm-chip.is-imdb:hover {
    background: #E2B408;
    transform: translateY(-1px);
}

.pm-chip.is-red {
    background: var(--pop);
    color: var(--white);
}

.pm-chip.is-red:hover {
    background: var(--black);
    transform: translateY(-1px);
}


/* credit hard left, chip hard right, on one row under the trailer */
.pm-media-fig .pm-video-cap {
    /* match the inset .pm-media applies to the image above, or the
       caption row runs wider than the art it belongs to */
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
}

.pm-video-fig .pm-video-cap.has-btn,
.pm-media-fig .pm-video-cap.has-btn {
    display: flex !important;
    flex-direction: row;
    align-items: center;
    justify-content: space-between !important;
    column-gap: 1.5rem;
    row-gap: 0.9rem;
    flex-wrap: wrap;
    text-align: left !important;
    width: 100%;
}

.pm-video-fig .pm-video-cap.has-btn > span,
.pm-media-fig .pm-video-cap.has-btn > span {
    text-align: left;
    margin-right: auto;
}

.pm-video-fig .pm-video-cap.has-btn > .pm-chip,
.pm-media-fig .pm-video-cap.has-btn > .pm-chip {
    margin-left: auto;
}

/* stacked and centered once there isn't room for both on a line */
@media (max-width: 600px) {
    .pm-video-fig .pm-video-cap.has-btn,
    .pm-media-fig .pm-video-cap.has-btn {
        flex-direction: column;
        justify-content: center !important;
        text-align: center !important;
    }

    .pm-video-fig .pm-video-cap.has-btn > span,
    .pm-media-fig .pm-video-cap.has-btn > span { text-align: center; margin: 0; }

    .pm-video-fig .pm-video-cap.has-btn > .pm-chip,
    .pm-media-fig .pm-video-cap.has-btn > .pm-chip { margin: 0; }
}


/* Caption sits right under its image, not floating between two blocks.
   The default var(--s3) read as a heading for whatever came NEXT. */
/* A captioned block IS a section, so it closes with a section-sized gap
   — nearly double the 64px used between items inside a section. Without
   this the space after a caption matched the space between two images in
   the same group, so the boundaries never read. */
/* .pm-media / .pm-sq-grid carry their own 30px margin-bottom for the
   no-caption case (a placeholder or grid with nothing after it). That
   was STACKING with --cap-gap-top below, which is what made photo
   captions sit ~46px from their image while video and before/after sat
   at the shared 16px. Zero it here so only the one gap applies once a
   caption is present — covers media, grid, squares and html beats,
   since they all render inside .pm-media-fig when captioned. */
.pm-media-fig .pm-media,
.pm-media-fig .pm-sq-grid,
.pm-media-fig > :first-child { margin-bottom: 0; }

/* margin-BOTTOM only. The `margin` shorthand here used to reset
   margin-left/right to 0, which cancelled the auto-centering from the
   shared "content blocks stay capped and centered" rule above and
   pinned every captioned figure to the left. */
.pm-media-fig { margin-bottom: var(--cap-gap-bottom); }

/* Nugz ONLY: double the image→caption gap on PHOTOS only (media/grid/
   squares/html beats render inside .pm-media-fig). Lagree was in this
   selector too at one point and got pulled back out — it now uses the
   same shared gap as everything else, same as Webber. Video credits
   and the before/after labels keep the shared 16px everywhere, on
   every project — this only touches .pm-media-fig, never .pm-video-fig
   or .pm-compare. */
.modal[data-project="nugz"] .pm-media-fig .pm-video-cap {
    padding-top: calc(var(--cap-gap-top) * 4) !important;
}

/* Escape hatch: a single section can opt back OUT of the wide Nugz gap
   above (e.g. Nugz's "Social" row) via `normalCap: true` on its beat in
   main.js, which renders as .is-normal-cap. Needs its own !important to
   beat the rule above — same origin, so normal specificity can't settle
   it, last one with !important in source order wins. */
.modal[data-project="nugz"] .pm-media-fig.is-normal-cap .pm-video-cap {
    padding-top: var(--cap-gap-top) !important;
}

/* ...and the caption's own line gets a little air above the next thing */
.pm-media-fig .pm-video-cap { padding-bottom: 0; }


/* ── Quote needs to read as its own beat ───────────────
   Now that sections end with a caption, a quote following one is text
   under text and the two run together. A short rule above the quote
   marks it as a separate beat rather than a continuation of the label. */
.pm-quote { position: relative; }

/* red rule removed — the opening quotation mark already marks a
   quote, and the rule reads as a section break, which a quote isn't */
.pm-quote::before {
    content: "\201C";
    display: inline;
    width: auto;
    height: auto;
    margin: 0;
    background: none;
    color: var(--pop);
}

/* the section above already closed itself, so the quote doesn't need to
   re-declare the full section gap on top of it */
.pm-media-fig + .pm-quote,
.pm-video-fig + .pm-quote,
.pm-compare + .pm-quote { padding-top: 0; }


/* ── Prev / next project navigation ────────────────────
   Replaces the old single CTA at the foot of each popup. Two columns so
   prev sits left and next sits right; when one end of the list has no
   sibling an empty <span> holds the column, which keeps the remaining
   button pinned to its own side instead of drifting to the middle. */
.pm-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
    align-items: stretch;
    margin-top: var(--gap-out);
}

.pm-nav-btn {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 1.25rem 1.5rem;
    background: transparent;
    border: 2px solid var(--black);
    border-radius: var(--radius);
    cursor: pointer;
    text-align: left;
    transition: background 0.2s ease, color 0.2s ease;
}

.pm-nav-btn.is-next { text-align: right; }

.pm-nav-dir {
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--grey-mid);
    transition: color 0.2s ease;
}

.pm-nav-name {
    font-family: var(--font-display);
    font-size: clamp(1.1rem, 1.4vw, 1.6rem);
    line-height: 1.1;
    color: var(--black);
    transition: color 0.2s ease;
}

.pm-nav-btn:hover { background: var(--black); }
.pm-nav-btn:hover .pm-nav-name { color: var(--white); }
.pm-nav-btn:hover .pm-nav-dir  { color: var(--pop); }

@media (max-width: 600px) {
    .pm-nav { grid-template-columns: 1fr; }
    .pm-nav-btn.is-next { text-align: left; }
}


/* Lone button (first / last project) takes the full width and centers */
.pm-nav.is-single { grid-template-columns: 1fr; }

.pm-nav.is-single .pm-nav-btn,
.pm-nav.is-single .pm-nav-btn.is-next {
    align-items: center;
    text-align: center;
}


/* ── Empty 16:9 placeholders line up with everything else ──
   .pm-media insets its CONTENT with padding, which is right for a real
   image (the img sits inside it) but wrong for an empty slot: the grey
   background paints across the padding, so the box ran edge-to-edge
   while the heading and lede sat inset. background-clip:content-box
   stops the grey at the padding edge, so the box lands on the text
   column. Nothing about the layout box changes — an earlier attempt
   swapped padding for margin and that DID move the box, which broke
   the width. Real media is untouched either way. */
.pm-media:not(:has(img)):not(:has(video)) {
    background-clip: content-box;
}

/* ── Small disclaimer / caveat line ─────────────────────
   Deliberately NOT styled like .pm-quote — a disclaimer is a footnote,
   not a statement, so it stays small, left-aligned, and muted instead
   of centered and italic. */
/* ── Warning / notice box ───────────────────────────────
   Was bare floating text — indistinguishable from body copy, so nothing
   told the reader this was a heads-up rather than another sentence of
   the case study. Outlined card, small icon, so it reads as a notice on
   sight, not just on close reading.

   .pm-note keeps .pm-pad's horizontal reading inset and only overrides
   the vertical rhythm (same pattern as .pm-quote above) — the box
   styling itself lives on .pm-note-box, a child element, so it doesn't
   fight .pm-pad for the `padding` property on the same node. */
.pm-note {
    padding-top: 0;
    padding-bottom: var(--gap-out);
}

.pm-note-box {
    display: flex;
    flex-wrap: wrap;
    gap: 1.1rem;
    align-items: center;
    width: 100%;
    padding: 1.6rem 2rem;
    background: #FFFBEA;
    border: 1.5px solid #F0D98A;
    border-radius: var(--radius);
}

.pm-note-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    align-self: flex-start;
    margin-top: 0.15rem;
    border-radius: 50%;
    background: #C7961B;
    color: var(--white);
    font-family: var(--font-display);
    font-size: 1.1rem;
    font-weight: 700;
}

.pm-note-text {
    flex: 1 1 260px;
    font-size: clamp(1rem, 1.3vw, 1.25rem);
    color: #7A5B12;
    line-height: 1.6;
}

/* mailto action, pinned to the right of the notice — its own dark
   pill so it reads as an action, not part of the warning styling */
.pm-note-btn {
    flex: 0 0 auto;
    margin-left: auto;
    padding: 0.85rem 1.5rem;
    background: #C7961B;
    color: #FFFBEA;
    border-radius: var(--radius);
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.2s ease;
}

.pm-note-btn:hover { background: #A97D14; }

@media (max-width: 640px) {
    .pm-note-btn { margin-left: 0; width: 100%; text-align: center; }
}

/* ── Impact stats row ───────────────────────────────────
   NOTE: named .pm-impact*, NOT .pm-stat*, because a legacy
   .pm-stat / .pm-statlabel pair already exists earlier in
   this file (big red numeral). Reusing that name would
   have silently overridden it.
   The payoff numbers for a case study — big figure, small
   label under it. Three across on desktop, stacking to one
   column on narrow screens. Deliberately NOT boxed: these
   sit in open space so they read as a statement rather
   than a data table. */
.pm-impact {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(1.5rem, 4vw, 3rem);
    text-align: center;
}

.pm-impact-item {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.pm-impact-num {
    font-family: var(--font-display);
    font-size: clamp(2rem, 3.6vw, 3.4rem);
    line-height: 1;
    color: var(--black);
}

.pm-impact-label {
    font-family: var(--font-body);
    /* matched to .pm-video-cap (0.94vw) — these were noticeably
       smaller than every other label in the modal */
    font-size: clamp(1rem, 0.94vw, 1.75rem);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--grey-mid);
    line-height: 1.4;
}

@media (max-width: 700px) {
    .pm-impact { grid-template-columns: 1fr; }
}

/* ── DEV BADGE ──────────────────────────────────────────
   Build version + live viewport readout for device testing.
   Sits above everything (modal is 10000). Tap to hide.
   DELETE THIS BLOCK, the #devBadge div in index.html, and the
   SITE_VERSION/devBadge block in main.js before going live. */
.dev-badge {
    position: fixed;
    right: 8px;
    bottom: 8px;
    z-index: 100000;
    padding: 6px 10px;
    border-radius: 6px;
    background: rgba(9, 9, 11, 0.88);
    color: #fff;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.02em;
    white-space: nowrap;
    pointer-events: auto;
    cursor: pointer;
    user-select: none;
    /* never let it interfere with a real layout measurement */
    contain: layout style;
}

.dev-badge.is-hidden {
    opacity: 0.12;
    transform: translateX(calc(100% - 14px));
}

/* ── Drag-to-scrub: cursor + selection lockout ──────────
   The horizontal canvas is a draggable surface, so it shouldn't
   behave like a text document: no selection highlight, no native
   image dragging, no accidental "save image" drag-out. Form fields
   are exempted below — those still need to be selectable. */
.frame {
    cursor: grab;
}

html.is-dragging,
html.is-dragging .frame {
    cursor: grabbing;
}

/* kill selection + image drag across the canvas */
.frame,
.rail,
.block,
.cluster,
.obj,
.obj-float,
.obj-img,
.h-display,
.sub,
.site-logo,
.marquee-bar,
.marquee-inner,
.marquee-inner img {
    user-select: none;
    -webkit-user-select: none;
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
}

/* images specifically — belt and braces */
.obj-img,
.marquee-inner img,
.site-logo {
    pointer-events: none;       /* the .obj button still takes the click */
    -webkit-user-drag: none;
    user-drag: none;
}

/* the objects themselves must stay clickable */
.obj { pointer-events: auto; }

/* text the visitor may legitimately want to copy stays selectable.
   NOTE: no blanket `cursor: auto` here — that would wipe the pointer
   cursor off the modal's own buttons, chips and nav. */
.contact-form input,
.contact-form textarea,
.modal,
.modal * {
    user-select: text;
    -webkit-user-select: text;
}

.contact-form input,
.contact-form textarea {
    cursor: auto;
}

/* modal content isn't a drag surface, so it gets a normal cursor —
   but interactive things inside it keep theirs */
.modal-scroll { cursor: auto; }

.modal button,
.modal a,
.modal .pm-chip,
.modal .pm-nav-btn,
.modal .modal-close { cursor: pointer; }

.modal .obj-img,
.modal img {
    -webkit-user-drag: none;
    user-drag: none;
}

/* ── Section header inside a project modal ──────────────
   A label for a group of beats (e.g. "Music Videos"). Shares the
   red rule with .pm-quote so it reads as the same kind of break,
   but upright and small-caps — a quote is a spoken line, a header
   is a signpost, and they shouldn't look alike. */
.pm-head {
    padding-top: calc(var(--gap-out) * 0.5);
    padding-bottom: calc(var(--gap-out) * 0.75);
    text-align: center;
}

/* no rule on section heads — the size and the surrounding space do
   the work of marking a break */
.pm-head::before { display: none; }

.pm-head h4 {
    margin: 0;
    font-family: var(--font-display);
    /* Same size as .pm-intro .modal-title — this is a section opening,
       so it reads at the scale of the page's own title. Set in the
       accent red so it's distinguishable from that title rather than
       competing with it. */
    font-size: clamp(2.6rem, 3.75vw, 7rem);
    font-weight: 400;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    line-height: 1.05;
    color: var(--pop);
}

/* intro paragraph under a section header — mirrors .pm-lede so the
   section reads like a smaller version of the modal's own opening */
.pm-head-intro {
    margin: 0.9rem auto 0;
    max-width: none;
    font-family: var(--font-body);
    /* matched to .pm-intro .pm-lede — the paragraph under the modal
       title — so a section opening reads at the same weight as the
       page opening */
    font-size: clamp(1.15rem, 1.09vw, 2.1rem);
    line-height: 1.65;
    color: var(--grey-dark);
    text-transform: none;
    letter-spacing: 0;
}

/* breakpoint matched to the .pm-intro .pm-lede override (820px, not
   900) — mismatched breakpoints made the two sizes diverge in the
   band between them */
@media (max-width: 820px) {
    .pm-head-intro { font-size: clamp(1.05rem, 3.6vw, 1.5rem); }
    /* mirrors the .pm-intro .modal-title override at this width */
    .pm-head h4 { font-size: clamp(2.4rem, 9vw, 4.5rem); }
}

/* SUB-LABEL — the category inside a section (e.g. "Music Videos"
   under "Post Production"). No red rule: the rule marks a real
   section break, and repeating it on every sub-label would flatten
   the hierarchy back into the three-competing-styles problem. */
.pm-head.is-sub {
    padding-top: calc(var(--gap-out) * 0.25);
    /* doubled — the label was sitting right on top of the first row
       of videos, so it read as attached to that one item rather than
       heading the whole group */
    padding-bottom: calc(var(--gap-out) * 0.8);
}

/* Rule restored on sub-labels. It read as clutter when the section
   header above ALSO had one — now that header is gone, these are the
   only rules on the page and they mark the category breaks cleanly. */
.pm-head.is-sub::before {
    content: "";
    display: block;
    width: 64px;
    height: 5px;
    margin: 0 auto calc(var(--gap-out) * 0.35);
    background: var(--pop);
}

.pm-head.is-sub h4 {
    font-family: var(--font-display);
    /* Captions sit at 0.94vw. At 1.05vw these read as the same thing,
       so a section label and an item label were competing. Display
       face at ~1.6vw makes the difference structural, not marginal. */
    font-size: clamp(1.5rem, 1.9vw, 2.6rem);
    font-weight: 400;
    letter-spacing: 0.04em;
    color: var(--black);
}

/* the block above already closed itself — don't stack gaps */
.pm-media-fig + .pm-head,
.pm-video-fig + .pm-head { padding-top: 0; }

/* ── 2-up video grid ────────────────────────────────────
   Used by the `videoGrid` beat. The grid itself carries the page
   padding, so the children drop theirs (.is-gridded) — otherwise
   each cell would inset its own video and the pair wouldn't align.
   Collapses to a single column on narrow screens. */
.pm-video-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: clamp(1.5rem, 3vw, 2.5rem);
    padding-left: clamp(2rem, 5.3vw, 4.7rem);
    padding-right: clamp(2rem, 5.3vw, 4.7rem);
    margin-bottom: var(--cap-gap-bottom);
}

.pm-video-fig.is-gridded {
    margin: 0;
    padding-left: 0;
    padding-right: 0;
    /* the GRID is already capped and centred — a child re-applying
       --modal-content would shrink each cell inside it */
    max-width: none;
}

/* A lone item on the final row would otherwise sit hard left with a
   gap beside it. Centre it across both columns instead — applies to
   any odd-numbered grid, so it keeps working if a section changes
   length. (:nth-child(odd):last-child = an odd count's final item.) */
.pm-video-grid > .pm-video-fig:nth-child(odd):last-child {
    grid-column: 1 / -1;
    width: 50%;
    margin-inline: auto;
}

/* Single lane below 700px, two columns above it — INCLUDING in
   vertical mode. This is deliberately a width media query and not
   keyed off html.is-vertical, which is the opposite of the rule
   everywhere else in this file. The reason: is-vertical also trips
   on height and aspect ratio, so a 1200x500 window is "vertical"
   while still being plenty wide for two videos side by side. Grid
   density is a pure width question; the layout mode is not.

   This replaces an older @media (max-width: 900px) block that
   duplicated the is-vertical rules and disagreed with them at the
   edges — at 880px wide in horizontal mode it fired when it
   shouldn't have, and at 1200x500 it stayed silent when the
   is-vertical rule had already collapsed the grid. */
@media (max-width: 749px) {
    .pm-video-grid { grid-template-columns: 1fr; }
    /* single column: the lone final item is already full width */
    .pm-video-grid > .pm-video-fig:nth-child(odd):last-child { width: 100%; }
}
