/* Double Dash Creative - all site CSS in one file. Sections below were
   separate files (styles, liquid-hero, hero-shrink, montage, cta-grow,
   about-v2, projects-v2); the order is the cascade order, so keep it. */

/* ======================================================================
   css/styles.css
   ====================================================================== */
/* ============================================
    Double Dash Creative - Main Stylesheet
   ============================================ */

/* Fonts - no local @font-face any more.
   Plantin (the display serif) and Forma DJR Micro (the body sans) come
   from Adobe Fonts - the use.typekit.net/bzi8cqp.css link in each page's
   <head>. Plantin: regular, italic, bold, bold italic, plus
   "plantin-condensed" bold. Forma DJR Micro: regular, italic, bold, bold
   italic only - so weights 100/300/500 render as 400 and 600 as 700. */

/* ============================================
   CSS Custom Properties (Design Tokens)
   ============================================ */
:root {
    /* Primary Colors */
    --color-orange: #FC6935;    /* orange (primary accent) */
    --color-cream: #FAFAFF;  /* off-white (text on dark) */
    --color-blue: #2E2B8A;   /* indigo (secondary accent) */
    --color-black: #0c0c0c;
    --color-mint: #FAFAFF;

    /* Neutrals */
    /* the box behind a card image while it loads - light now, so it does
       not flash near-black against the page ground */
    --color-gray-placeholder: #1c1c1c;
    --color-gray-dark: #111111;

    /* Typography */
    --font-marker: "plantin", Georgia, serif;
    --font-body: "forma-djr-micro", Helvetica, Arial, sans-serif;

    /* The page ground and its ink. Named separately from the raw palette
       so the sections that carry their own ground - the hero, the navbar
       bar, .cs-band, the orange outro - are unaffected by this pair. */
    /* Dark theme. The light ground (#FFF6F3 with black ink) lasted from
       the Oct 2026 overhaul until the homepage rework, and is one swap
       away if it is ever wanted back. */
    --page-bg: var(--color-black);
    --page-ink: var(--color-cream);


    /* Fluid type scale - tier 1 (display) to tier 6 (caption).
       Each clamp keeps a legible floor on small screens and a cap on
       large ones. Use these everywhere instead of ad-hoc vw / clamp.
       Rough sizes:            mobile ~375px   desktop ~1440px
         --fs-1  display          ~46px           ~100px
         --fs-2  page title       ~36px           ~64px
         --fs-3  section / card   ~28px           ~44px
         --fs-4  sub-head / lead  ~22px           ~30px
         --fs-5  body             ~15px           ~17px
         --fs-6  caption / label  ~13px           ~15px               */
    --fs-1: clamp(2.75rem, 1.5rem + 6vw, 6.25rem);
    --fs-2: clamp(2.15rem, 1.5rem + 3.2vw, 4rem);
    --fs-3: clamp(1.7rem, 1.35rem + 1.8vw, 2.75rem);
    --fs-4: clamp(1.3rem, 1.15rem + 0.9vw, 1.9rem);
    --fs-5: clamp(0.95rem, 0.9rem + 0.2vw, 1.075rem);   /* was 1.05-1.2rem, read too big */
    --fs-6: clamp(0.82rem, 0.78rem + 0.18vw, 0.92rem);

    /* Spacing Scale */
    --space-xs: 0.5rem;
    --space-sm: 1rem;
    --space-md: 1.5rem;
    --space-lg: 2rem;
    --space-xl: 3rem;
    --space-2xl: 4rem;
    --space-3xl: 6rem;
    --space-4xl: 8rem;

    /* Container */
    --container-max: 1200px;
    --container-padding: 2rem;

    /* The left/right edge everything lines up on: the nav container's
       padding plus the logo's indent. Shrinks to a plain gutter on
       phones, where the nav drops the indent too. */
    /* was 3rem + up to another 7rem, which left up to 160px of air down
       each side. The grid and the bar both key off this one value, so
       they stay aligned with each other. */
    --page-inset: clamp(1.25rem, 3vw, 2.75rem);

    /* Transitions */
    --transition-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
    --transition-smooth: cubic-bezier(0.4, 0, 0.2, 1);

}

/* ============================================
   Reset & Base Styles
   ============================================ */
*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

::selection {
  background: rgb(252, 105, 53, 0.7);
  color: var(--color-black);
}

html {
    scroll-behavior: smooth;
}

/* Lenis smooth (inertial) scrolling - required styles */
.lenis, .lenis body {
    height: auto;
}

.lenis.lenis-smooth {
    scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
    overscroll-behavior: contain;
}

.lenis.lenis-stopped {
    overflow: clip;
}

.lenis.lenis-smooth iframe {
    pointer-events: none;
}

body {
    font-family: var(--font-body);
    font-size: var(--fs-5);
    background-color: var(--page-bg);
    color: var(--page-ink);
    line-height: 1.2;
    overflow-x: hidden;
}

/* Topographic lines + colour grain on the black ground, on every page
   (js/topo-bg.js). Fixed behind everything; the canvas is transparent
   between the lines, so the page colour shows through. On the homepage
   it sits behind the hero's gradient frame too. */
.topo-bg {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: -3;
    display: block;
    pointer-events: none;
    transition: opacity 0.7s ease;
}

/* (They used to fade out at the footer, back when the whole page turned
   orange. The footer paints its own solid orange over them now.) */

.container,
.work-container {
    width: 100%;
    max-width: var(--container-max);
    margin: 0 auto;
    padding: 0 var(--container-padding);
}

@media (max-width: 640px) {

    .work-container {
        padding: 1rem;
    }

}

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

a {
    text-decoration: none;
    color: inherit;
    transition: color 0.3s var(--transition-smooth);
}

button {
    font-family: inherit;
    cursor: pointer;
    border: none;
    background: none;
}

/* ============================================
   Animation Keyframes
   ============================================ */
@keyframes bounceIn {
    0% {
        opacity: 0;
        transform: scale(0.3);
    }

    50% {
        transform: scale(1.05);
    }

    70% {
        transform: scale(0.9);
    }

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

@keyframes slideInLeft {
    from {
        opacity: 0;
        transform: translateX(-50px);
    }

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

@keyframes slideInRight {
    from {
        opacity: 0;
        transform: translateX(50px);
    }

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

/* ============================================
   Typography
   ============================================ */
h1,
h2,
h3,
h4 {
    font-family: var(--font-marker);
    font-weight: normal;
    line-height: 1.1;
    text-transform: uppercase;
}

.heading-lg {
    font-size: var(--fs-2);
}

/* ============================================
   Layout Utilities
   ============================================ */
.section {
    padding: var(--space-3xl) 0;
}

.sm-section {
    padding: none;
}

/* ============================================
   Portfolio Grid (Work Page)

@media (max-width: 576px) {

    .portfolio-grid.cols-3,

}

/* ============================================
   Navigation
   ============================================ */
.navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    padding: 0.9rem 0;
    background-color: transparent;
    transition: transform 0.55s var(--transition-smooth),
        opacity 0.4s var(--transition-smooth);
}

/* No backdrop: the bar is transparent on every page, all the way down.
   (It used to fade in a black - or, over [data-nav-tint] bands, orange -
   backdrop as you scrolled.) */

/* ── Light pages ──────────────────────────────────────────────
   Not used right now: the whole site is dark, like the homepage. Put
   .theme-light back on a page's <body> to make it white again. Only the
   ground/ink pair (and the empty-frame grey) change, so everything built
   on those tokens follows.

   The navbar ink is white for the black bar, so while the bar is
   still transparent over the white ground it goes black instead. The
   open takeover menu keeps its dark sheet and cream ink. */
.theme-light {
    --page-bg: #FFFFFF;
    --page-ink: var(--color-black);
    --color-gray-placeholder: #E9E9F0;
}

html:not(.menu-open) .theme-light .navbar:not(.is-solid) .logo,
html:not(.menu-open) .theme-light .navbar:not(.is-solid) .nav-link-cycle,
html:not(.menu-open) .theme-light .navbar:not(.is-solid) .nav-links a {
    color: var(--page-ink);
}

html:not(.menu-open) .theme-light .navbar:not(.is-solid) .logo-mark {
    filter: brightness(0);
}

html:not(.menu-open) .theme-light .navbar:not(.is-solid) .mobile-menu-toggle span {
    background-color: var(--page-ink);
}


.navbar .container {
    max-width: none;
    padding: 0 var(--page-inset);
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;

    /* a floor, so the bar keeps its shape if the logo ever fails to load */
    min-height: 3.25rem;
    align-content: center;
}

/* grid-row: 1 on every one of these. Without it, an item asking for a
   later column before one asking for an earlier column makes grid wrap
   the second onto a new row, because auto-placement never backtracks. */
.navbar .logo { grid-column: 1; grid-row: 1; justify-self: start; }
.navbar .nav-links { grid-column: 2; grid-row: 1; justify-self: center; }
.navbar .mobile-menu-toggle { grid-column: 3; grid-row: 1; justify-self: end; }

.logo {
    display: flex;
    align-items: center;
    gap: 0.1rem;
    color: #fff;
    /* links back to the home page */
    cursor: pointer;
    transition: opacity 0.3s var(--transition-smooth);
}

.logo-mark {
    display: block;
    height: 66px;
    width: auto;
    /* a fixed-colour svg in an <img> cannot inherit currentColor, so it
       is forced to pure white before the blend inverts it */
    filter: brightness(0) invert(1);
    /* filter, for the nav tint over a coloured band */
    transition: filter 0.4s var(--transition-smooth);
}

@media (max-width: 640px) {
    .logo-mark { height: 48px; }
}

.logo-and {
    color: var(--color-orange);
}


.logo svg {
    width: 40px;
    height: auto;
    color: var(--color-orange);
}

.nav-links {
    display: flex;
    gap: var(--space-4xl);
    list-style: none;
}

/* Text Cycling Animation for Nav Links */
.nav-link-cycle {
    font-family: var(--font-body);
    font-size: var(--fs-6);   /* overridden by .nav-links a below anyway */
    color: #fff;
    text-decoration: none;
    position: relative;
    display: inline-block;
    overflow: hidden;
    transition: color 0.4s var(--transition-smooth);
}

.nav-link-cycle span {
    display: inline-block;
    position: relative;
    transition: transform 0.4s var(--transition-bounce);
}

.nav-link-cycle span::before {
    content: attr(data-text);
    position: absolute;
    top: 100%;
    left: 0;
}

.nav-link-cycle:hover span {
    transform: translateY(-100%);
}

/*.page-cream .nav-link-cycle {
    color: var(--color-black);
}

.page-cream .nav-link-cycle span::before {
    color: var(--color-orange);
}*/

/* the size that actually applies to the bar's links (this selector beats
   .nav-link-cycle above). Caption tier, ~13-15px; the phone menu sets its
   own large size further down. Was 1.1rem (~18px). */
.nav-links a {
    font-family: var(--font-body);
    font-size: var(--fs-6);
    color: #fff;
    text-decoration: none;
    position: relative;
}

.nav-links a:not(.nav-link-cycle)::after {
    content: '';
    position: absolute;
    bottom: -4px;
    left: 0;
    width: 0;
    height: 2px;
    background-color: var(--color-orange);
    transition: width 0.3s var(--transition-bounce);
}

.nav-links a:not(.nav-link-cycle):hover::after {
    width: 100%;
}

/* Mobile Navigation */
.mobile-menu-toggle {
    display: none;
    flex-direction: column;
    gap: 5px;
    padding: 10px;
    background: none;
    border: none;
    cursor: pointer;
}

.mobile-menu-toggle span {
    width: 25px;
    height: 2px;
    background-color: #fff;
    transition: all 0.3s var(--transition-smooth);
}

.mobile-menu-toggle span:nth-child(1) {
    transform-origin: center;
}

.mobile-menu-toggle span:nth-child(2) {
    transform-origin: center;
}

.mobile-menu-toggle span:nth-child(3) {
    transform-origin: center;
}

.mobile-menu-toggle.active span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.mobile-menu-toggle.active span:nth-child(2) {
    opacity: 0;
}

.mobile-menu-toggle.active span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

@media (max-width: 640px) {
    :root { --page-inset: var(--space-md); }

    .mobile-menu-toggle {
        display: flex;
        position: relative;
        z-index: 1001; /* stays above the overlay */
    }

    /* On phones the bar was lopsided: the logo sat at --space-xl (3rem)
       plus its own 6vw indent while the burger sat at --space-xl flat.
       Drop the indent and tighten the padding so both edges match. */
    .navbar .container { padding: 0 var(--space-md); }
    .navbar .logo { margin-left: 0; }
    /* the burger has 10px of its own padding, so pull it back out or its
       bars end up further from the edge than the logo is */
    .navbar .mobile-menu-toggle { margin-right: -10px; }

    /* brand mark stays visible on top of the open overlay */
    .navbar .logo {
        position: relative;
        z-index: 1001;
    }

    /* Full-screen takeover menu. Covers the viewport (inset: 0) - no
       off-screen transforms, so it can never widen the page. */
    .nav-links {
        position: fixed;
        inset: 0;
        width: 100%;
        z-index: 1000;
        margin: 0;
        background-color: rgba(0, 0, 0, 0.85);
        -webkit-backdrop-filter: blur(6px);
        backdrop-filter: blur(6px);
        flex-direction: column;
        align-items: stretch;      /* items span the full width */
        justify-content: center;
        gap: var(--space-md);
        padding: var(--space-4xl) var(--container-padding) var(--space-2xl);
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: opacity 0.35s var(--transition-smooth),
            visibility 0s linear 0.35s;
    }

    .nav-links.active {
        opacity: 1;
        visibility: visible;
        pointer-events: all;
        transition: opacity 0.35s var(--transition-smooth);
    }

    .nav-links li {
        width: 100%;
    }

    .nav-links a,
    .nav-link-cycle {
        display: block;
        width: 100%;
        text-align: left;
        font-family: var(--font-marker);   /* Plantin */
        font-weight: 400;
        text-transform: none;
        font-size: var(--fs-2);
    }

    /* lock the page behind the open menu */
    .menu-open,
    .menu-open body {
        overflow: hidden;
    }
}

/* ============================================
   Side rails - copyright (left) + social (right),
   built by initSocial in main.js. Both take up the
   exact same vertical footprint.
   ============================================ */
.nav-social {
    display: none;
}

.nav-social a::after {
    display: none !important; /* kill the nav-link underline bar */
}

@media (max-width: 640px) {
    .nav-links .nav-social {
        display: flex;
        gap: var(--space-md);
        margin-top: var(--space-sm);
        padding-top: var(--space-lg);
        border-top: 1px solid rgba(250, 250, 255, 0.15);
    }

    .nav-links .nav-social a {
        /* .nav-links a makes every link a full-width block; the icons are
           a row inside one item, so they take their own size back */
        display: inline-flex;
        align-items: center;
        width: auto;
        flex: none;
        padding: 0;
        color: var(--color-cream);
        transition: color 0.3s var(--transition-smooth),
            transform 0.3s var(--transition-bounce);
    }

    .nav-links .nav-social a:active {
        color: var(--color-orange);
        transform: scale(1.12);
    }

    .nav-links .nav-social a svg {
        width: 30px;
        height: 30px;
    }
}

/* ============================================
   CTA Section

/* ============================================
   Buttons
   ============================================ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 1rem 2.5rem;
    font-family: var(--font-body);
    font-size: 0.875rem;
    font-weight: 500;
    text-transform: uppercase;
    /* always-present border so the hover outline doesn't shift layout */
    border: thin solid transparent;
    transition: all 0.3s var(--transition-bounce);
}

.btn-primary {
    background-color: var(--page-ink);
    color: var(--page-bg);
}

.btn-primary:hover {
    background-color: transparent;
    color: var(--page-ink);
    border-color: var(--page-ink);
}

/* ============================================
   Footer

/* ============================================
   Page: Work/Portfolio
   ============================================ */
.page-header {
    padding-top: 150px;
    padding-bottom: var(--space-xl);
}

.page-header h1 {
    margin-bottom: var(--space-xs);
}

.page-header .heading-accent {
    font-family: var(--font-marker);
    color: var(--color-blue);
}

/* ============================================
   Page: About

.about-intro .image-box {
    aspect-ratio: 3/4;
    background-color: var(--color-gray-placeholder);
}

.about-quote .image-box {
    aspect-ratio: 1;
    background-color: var(--color-gray-placeholder);
}

/* ============================================
   Page: Contact
   ============================================ */
.contact-section {
    padding-top: 150px;
}

/* Two columns: the heading on the left, held in view while the long
   form scrolls past on the right. Stacks below 900px. */
.contact-layout {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: clamp(2rem, 6vw, 6rem);
    align-items: start;
}

.contact-form {
    width: 100%;
    min-width: 0;
}

/* short fields side by side */
.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 clamp(1rem, 2.5vw, 2rem);
}

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

@media (max-width: 560px) {
    .form-row { grid-template-columns: 1fr; }
}

/* Honeypot wrapper on the contact form. Pushed off-screen rather than
   display:none or visibility:hidden, both of which the better bots test
   for and then skip. aria-hidden + tabindex="-1" on the markup keep it
   away from screen readers and the tab order. */
.hp-field {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* Contact page, laid out the way the reference does it: the standfirst
   sets expectations BEFORE the heading, the form sits under it, and a
   softer route in plus the socials close the page off. */
.contact-head {
    margin: 0;
    text-align: left;
    top: 7rem;      /* clear of the fixed navbar */
}

.contact-head .page-header__title { margin-top: var(--space-sm); }

@media (max-width: 900px) {
    .contact-head { position: static; }
}

.contact-after {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-md);
    margin-top: var(--space-3xl);
    padding-top: var(--space-xl);
    border-top: 1px solid rgba(0, 0, 0, 0.18);
}

.contact-after__line {
    max-width: 46ch;
    margin: 0;
    font-weight: 300;
    color: var(--page-ink);
}

/* The reCAPTCHA iframe is a fixed 304px wide and will not shrink, so
   on a narrow phone it overflows the form. Scaled down from its left
   edge rather than clipped. */
.form-captcha {
    margin-bottom: var(--space-md);
}

@media (max-width: 400px) {
    .form-captcha {
        transform: scale(0.88);
        transform-origin: left top;
    }
}

.contact-socials {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
    margin-top: var(--space-sm);
}

/* its own styles, not the footer ones: those are scoped to .site-footer
   and coloured for black ink on the orange outro, not cream on black */
.contact-socials__links {
    display: flex;
    align-items: center;
    gap: clamp(0.85rem, 2vw, 1.4rem);
}

.contact-socials__links a {
    display: inline-flex;
    align-items: center;
    color: var(--page-ink);
    transition: color 0.3s var(--transition-smooth),
        transform 0.3s var(--transition-bounce);
}

.contact-socials__links a:hover {
    color: var(--color-orange);
    transform: scale(1.15);
}

.contact-socials__links a svg {
    width: 24px;
    height: 24px;
    flex: none;
    color: inherit;
}

.contact-socials__label {
    font-family: var(--font-body);
    font-size: var(--fs-6);
    text-transform: uppercase;
    color: var(--page-ink);
}

/* Standfirst above the form: sets expectations before anyone starts
   typing, which is most of what stops a half-filled enquiry. */
.contact-intro {
    max-width: 46ch;
    margin: 0 0 var(--space-lg);
    font-weight: 300;
    color: var(--page-ink);
}

.contact-form h1 {
    margin-bottom: var(--space-xl);
}

.form-group {
    margin-bottom: var(--space-md);
}

/* Field titles in Plantin, at the same size as the body copy and what
   people type into the fields below them. */
.form-group label {
    display: block;
    font-family: var(--font-marker);   /* Plantin */
    font-weight: 400;
    font-size: var(--fs-5);
    line-height: 1.2;
    letter-spacing: -0.01em;
    margin-bottom: var(--space-xs);
    color: var(--page-ink);
}

/* what people type, at body size in a shallower box with a hairline
   border (was 0.875rem padding and a 2px rule, which read heavy) */
.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 0.6rem 0.75rem;
    font-family: var(--font-body);
    font-size: var(--fs-5);
    border: 1px solid var(--page-ink);
    background-color: transparent;
    color: var(--page-ink);
    transition: border-color 0.3s var(--transition-smooth);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
    /* off-white, a step back so a hint never reads as a filled value
       (was 45% black, left from the white theme - invisible on dark) */
    color: rgba(250, 250, 255, 0.55);
    opacity: 1;   /* Firefox fades placeholders on its own otherwise */
}

/* only the textarea is tall, and only it can be dragged taller */
.form-group textarea {
    min-height: 120px;
    resize: vertical;
}

.form-group select option {
    background-color: var(--page-bg);
    color: var(--page-ink);
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--color-orange);
}

.form-group select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23FAFAFF' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    padding-right: 2.1rem;
}

/* Sticky Shark Animation */
.contact-shark {
    position: sticky;
    top: 150px;
    /* More space from navbar */
    height: fit-content;
    align-self: flex-start;
}

.contact-shark img {
    width: 100%;
    height: auto;
    display: block;
    animation: sharkBounce 3s ease-in-out infinite;
    transition: transform 0.4s var(--transition-bounce);
}

/* Gentle bounce animation */
@keyframes sharkBounce {

    0%,
    100% {
        transform: translateY(0);
    }

    50% {
        transform: translateY(-15px);
    }
}

/* Scale on hover while keeping bounce animation */
.contact-shark:hover img {
    transform: scale(1.1);
}

/* Mobile shark - same bounce animation */
@media (max-width: 640px) {
    .contact-layout {
        grid-template-columns: 1fr;
    }

    .contact-shark {
        position: static;
        /* Not sticky on mobile */
        display: block;
        margin: var(--space-xl) auto;
        max-width: 300px;
    }

    /* Same animation as desktop */
    .contact-shark img {
        animation: sharkBounce 3s ease-in-out infinite;
    }

    /*.contact-shark:hover img {
        filter: brightness(1.1);
    }*/
}

/* ============================================
   Page: Case Study
   ============================================ */
.case-study-hero {
    /* the inset lines it up with the nav */
    margin: 80px var(--page-inset) 0;
    overflow: hidden;
}

/* The Height field on the hero decides the shape, and SANITY does the
   crop, not this. That keeps it to one crop, so the hotspot is
   respected and a portrait shot set to "Whole image" is never cut.
   Height auto here means the file's own proportions are what show. */
.case-study-hero img,
.case-study-hero video {
    width: 100%;
    height: auto;
    display: block;
}

.case-study-intro {
    display: grid;
    padding: var(--space-3xl) 0;
}

/* Title and role on the left, then the description and scope beneath
   them. */
.case-study-intro-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-2xl);
}

.case-study-intro .title-section {
    text-align: left;
}

/* The project title is a page title, set like the others on the light
   pages (see .page-header__title) - --fs-1, upright, no uppercase -
   rather than inheriting the caps from the global h1 rule. */
.case-study-intro .title-section h1 {
    font-family: var(--font-marker);
    font-size: var(--fs-1);
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.04em;
    text-transform: none;
    color: var(--page-ink);
    margin-bottom: var(--space-sm);
}

/* the role line: the same small label the rest of the site uses */
.case-study-intro .meta {
    font-family: var(--font-body);
    font-size: var(--fs-6);
    text-transform: uppercase;
    color: var(--color-orange);
}

.case-study-intro .description {
    font-size: var(--fs-5);
    line-height: 1.6;
    max-width: 68ch;   /* same measure as the write-up bands */
}

.case-study-intro .description p {
    margin-bottom: var(--space-md);
}

.case-study-meta-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-lg);
    margin-top: var(--space-xl);
    padding-top: var(--space-xl);
    border-top: 1px solid rgba(0, 0, 0, 0.2);
}

.case-study-meta-grid .meta-item h3 {
    font-family: var(--font-body);
    font-size: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    margin-bottom: var(--space-xs);
}

.case-study-meta-grid .meta-item p {
    font-size: 0.875rem;
    line-height: 1.6;
}

.case-study-images {
    margin: var(--space-sm);
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-sm)
}

.case-study-images .image-box {
    aspect-ratio: 4/5;
    overflow: hidden;
}

.case-study-images .image-box img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.case-study-mockups {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    overflow: hidden;
    gap: var(--space-sm);
    margin: var(--space-sm);
}

.case-study-mockups .image-box {
    aspect-ratio: 16/10;
    overflow: hidden;

    img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }
}

.case-study-solution {
    display: grid;
    grid-template-columns: 2fr 3fr;
    /* was --space-sm, back when the halves carried their own 2rem margins
       and the gap only had to close the remainder. Scales down so the
       stacked phone layout does not inherit the desktop figure. */
    gap: clamp(var(--space-lg), 4vw, var(--space-2xl));
    margin-block: var(--space-sm);
    /* align, not place: place-items also centred each half horizontally in
       its track, which fights the capped measure below */
    align-items: center;

    /* Vertical only. A horizontal margin here indented the copy past the
       hero, the intro and every image row, all of which sit at
       --page-inset - the bands are full-bleed so copy and images share an
       edge. */
    .content-img {
        margin-block: var(--space-lg);
    }
}

/* Text-only band: the heading sits above its paragraphs, both at the
   band's own edge (--page-inset, via .cs-band .container). */
.cs-prose {
    margin-block: var(--space-sm);
}

/* One measure and one rhythm for band copy, whether it has an image
   beside it or not. Full-bleed bands would otherwise run about 110
   characters a line on a wide screen, and the global margin reset leaves
   consecutive paragraphs touching with nothing between them. */
.cs-band p {
    max-width: 68ch;
}

.cs-band p + p {
    margin-top: var(--space-md);
}

.case-study-solution h2 {
    color: var(--page-ink);
    margin-bottom: var(--space-md);
}

.case-study-solution .image-box {
    width: 100%;
    overflow: hidden;
}

.case-study-solution .image-box img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.case-study-full-image {
    margin: var(--space-sm) var(--page-inset);
}

.case-study-full-image img {
    width: 100%;
}

/* ── Other works, under every case study ─────
   A footer-ish set of three, so it takes the centred column rather than
   running full width like the showcase rows: at --page-inset the three
   cards came out around 600px each, which read as a second portfolio
   page rather than a postscript. The cards arrive from sanity-cards.js
   two to a row; the rows dissolve into one grid. */
.cs-more {
    padding-block: clamp(5rem, 10vw, 9rem) 0;
    padding-inline: var(--page-inset);
    max-width: var(--container-max);
    margin-inline: auto;
}

.cs-more__title {
    margin: 0 0 var(--space-2xl);
    text-align: center;
    font-family: var(--font-marker);
    font-weight: 400;
    font-size: var(--fs-3);
    text-transform: none;
    color: var(--page-ink);
}

/* Qualified with .work-scatter, which sits on this div too and is
   defined further down the file at the same specificity - so order,
   not intent, decided which won. It is a wrapping flexbox with its own
   page inset and a huge row gap, all of which this has to beat. */
.work-scatter.cs-more__grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: clamp(1rem, 2.5vw, 2rem);
    row-gap: var(--space-xl);
    padding: 0;
}

.cs-more__grid .work-row { display: contents; }

/* the squish layout sizes cards inline; this grid owns the widths */
.cs-more__grid .work-card {
    width: auto !important;
    margin: 0 !important;
    transform: none;
}

.cs-more__grid .work-card__frame {
    height: auto;
    aspect-ratio: 4 / 3;
}

.cs-more__grid .work-card__title { font-size: var(--fs-4); }

@media (max-width: 1000px) {
    .work-scatter.cs-more__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
    .work-scatter.cs-more__grid { grid-template-columns: 1fr; }
}

/* ============================================
   CASE STUDY ROWS - 1 to 4 images per row

   Same idea as the work rows on the homepage: the row splits evenly by
   default, and --w on an item overrides that with a percentage. --n is
   the item count, set from the data-n the renderer writes, so each item
   can give back its share of the gaps and the row still totals 100%.
   ============================================ */
.cs-row {
    --cs-gap: var(--space-sm);
    display: flex;
    flex-wrap: wrap;
    gap: var(--cs-gap);
    margin: var(--cs-gap) var(--page-inset);
    /* Items in a row are often different heights, because "Whole image"
       keeps its own shape next to a cropped neighbour. Centred reads far
       better than hanging them all from the top. */
    align-items: center;
}

.cs-row[data-n="1"] { --n: 1; }
.cs-row[data-n="2"] { --n: 2; }
.cs-row[data-n="3"] { --n: 3; }
.cs-row[data-n="4"] { --n: 4; }

.cs-row__item {
    flex: 0 0 calc(
        var(--w, calc(100 / var(--n))) * 1%
        - var(--cs-gap) * (var(--n) - 1) / var(--n)
    );
    min-width: 0;
    overflow: hidden;
}

/* A lone image keeps its own shape; pairs get a landscape crop and
   3-4 up get a portrait one, which is what the hand-built pages did. */
.cs-row[data-n="1"] .cs-row__item { aspect-ratio: var(--ar, auto); }
.cs-row[data-n="2"] .cs-row__item { aspect-ratio: var(--ar, 16 / 10); }
.cs-row[data-n="3"] .cs-row__item,
.cs-row[data-n="4"] .cs-row__item { aspect-ratio: var(--ar, 4 / 5); }

/* Height "Whole image, no crop" in the Studio. Wins over the row's
   default ratio above, so a tall shot shows end to end instead of
   being squashed into the row's shape. */
/* .cs-row[data-n] is needed on all of these: without it they are LESS
   specific than the rules above and the row's default ratio still wins.

   natural  the whole image, no crop at all
   short    a wide, shallow crop
   tall     a deep, portrait crop
   (blank)  the row's default for its item count                       */
.cs-row[data-n] .cs-row__item[data-height="natural"] { aspect-ratio: auto; }

/* Short, Standard and Tall are a HEIGHT, not a ratio, so a card is the
   same depth whatever shape the image is and whatever else is in the
   row. aspect-ratio has to be cancelled or the count-based default above
   fights the height. The rem bounds stop it collapsing on a short laptop
   or towering on a tall monitor. */
.cs-row[data-n] .cs-row__item[data-height="short"] {
    aspect-ratio: auto;
    height: clamp(14rem, 40vh, 26rem);
}

.cs-row[data-n] .cs-row__item[data-height="default"] {
    aspect-ratio: auto;
    height: clamp(22rem, 65vh, 42rem);
}

/* 30% deeper than Standard */
.cs-row[data-n] .cs-row__item[data-height="tall"] {
    aspect-ratio: auto;
    height: clamp(29rem, 85vh, 55rem);
}

/* only "natural" releases the image itself; the two crops still fill
   their box the way every other row image does */
.cs-row[data-n] .cs-row__item[data-height="natural"] img,
.cs-row[data-n] .cs-row__item[data-height="natural"] video { height: auto; }

/* video sits in exactly the same box as an image */
.cs-row__item img,
.cs-row__item video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.cs-row[data-n="1"] .cs-row__item img,
.cs-row[data-n="1"] .cs-row__item video { height: auto; }

/* A sized preset must fill its box, or the crop never happens. This
   has to outrank the n="1" rule above, which would otherwise leave a
   lone image at its natural height inside a box it does not fill. */
.cs-row[data-n] .cs-row__item[data-height="short"] img,
.cs-row[data-n] .cs-row__item[data-height="short"] video,
.cs-row[data-n] .cs-row__item[data-height="default"] img,
.cs-row[data-n] .cs-row__item[data-height="default"] video,
.cs-row[data-n] .cs-row__item[data-height="tall"] img,
.cs-row[data-n] .cs-row__item[data-height="tall"] video {
    height: 100%;
    object-fit: cover;
}

/* Every block that carries copy sits in a centred column, narrower than
   the images. The image rows and the hero keep --page-inset and run
   wide, so the text has visibly more room at its sides than they do. */
.cs-band .container,
.case-study-intro .work-container,
.sm-section.cs-band {
    max-width: var(--container-max);
    margin-inline: auto;
    padding-inline: var(--page-inset);
}
.cs-band .case-study-solution { margin-inline: 0; }

/* Every band heading, prose or image variant. A tier below the project
   title (--fs-1) so the sections read as sections instead of competing
   with the page heading. */
.cs-band h2 {
    font-family: var(--font-marker);
    font-size: var(--fs-4);
    font-weight: 400;
    line-height: 1.05;
    text-transform: none;
    color: var(--page-ink);
    margin-bottom: var(--space-md);
}

/* the hero and the beside-the-copy slot both take video too */
.case-study-hero video {
    width: 100%;
    object-fit: cover;
    display: block;
}

.case-study-solution .image-box video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* no ?p= slug, or one with no entry in PROJECTS */
.cs-missing {
    margin-top: 30vh;
    margin-bottom: var(--space-3xl);
    padding: 0 var(--space-xl);
    text-align: center;
    color: var(--page-ink);
}

.cs-missing a { color: var(--color-orange); }

.cs-missing code {
    font-family: var(--font-body);
    background: color-mix(in srgb, var(--page-ink) 8%, transparent);
    padding: 0.15em 0.4em;
}

.cs-missing__list {
    list-style: none;
    padding: 0;
    margin: var(--space-md) 0;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-md);
    font-size: var(--fs-4);
}

@media (max-width: 640px) {
    /* one per row, like every other stacked section */
    .cs-row__item { flex-basis: 100%; }
    .cs-row { margin-block: var(--space-xs); }

    /* Every height and ratio is released here, on purpose.
       They exist only to make items sitting SIDE BY SIDE match each
       other. Stacked one per row there is nothing to match, so all they
       do is force a full-width phone image into a fixed box and let
       object-fit hack the top and bottom off it. Whole images instead.
       The selectors are this long to outrank the desktop rules, which
       are [data-n] + [data-height] specific. */
    .cs-row[data-n] .cs-row__item,
    .cs-row[data-n] .cs-row__item[data-height="short"],
    .cs-row[data-n] .cs-row__item[data-height="default"],
    .cs-row[data-n] .cs-row__item[data-height="tall"] {
        aspect-ratio: auto;
        height: auto;
    }

    .cs-row[data-n] .cs-row__item img,
    .cs-row[data-n] .cs-row__item video,
    .cs-row[data-n] .cs-row__item[data-height="short"] img,
    .cs-row[data-n] .cs-row__item[data-height="short"] video,
    .cs-row[data-n] .cs-row__item[data-height="default"] img,
    .cs-row[data-n] .cs-row__item[data-height="default"] video,
    .cs-row[data-n] .cs-row__item[data-height="tall"] img,
    .cs-row[data-n] .cs-row__item[data-height="tall"] video {
        height: auto;
        object-fit: fill;
    }
}

@media (max-width: 640px) {

    .case-study-intro,
    .case-study-images,
    .case-study-solution {
        grid-template-columns: 1fr;
        margin: var(--space-xs);
    }

    .case-study-full-image,
    .case-study-mockups {
        margin: var(--space-xs);
    }

    /* stacked: the grid gap is the whole separation now */
    .case-study-solution {
        .content-img {
            margin: 0;
        }
    }

    .case-study-intro-grid {
        display: block
    }

    .case-study-intro-grid h1 {
        font-size: clamp(4rem, 7rem, 9rem);
    }

    .case-study-meta-grid {
        grid-template-columns: 1fr;
        gap: var(--space-md);
    }

    /* the 3rem heading that used to sit here fought the fluid --fs-3
       scale, which already steps down on a narrow screen */

    .sm-section {
        h2 {
            margin-bottom: var(--space-lg);
        }
    }

    .case-study-images {
        margin: var(--space-xs);
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: var(--space-xs)
    }

    .case-study-mockups {
        grid-template-columns: repeat(1, 1fr);
        gap: var(--space-xs)
    }
}

/* ============================================
   Utility Classes

/* ============================================
   NEW: Updated Hero Section
   ============================================ */

/* ============================================
   Portfolio grid (Homepage + Work page)
   ============================================ */
.portfolio-section {
    /* no background - the page-wide gradient behind it fades to black
       across this section (initHeroBgFade in main.js) */
    position: relative;
    padding: 0 0 clamp(5rem, 12vw, 11rem);
}

/* nothing follows the grid here but the CTA, which brings its own top
   padding - the full gap read as the page having ended */
.page-projects .portfolio-section {
    padding-bottom: clamp(1.5rem, 4vw, 3rem);
}

/* All cards the SAME height. Widths follow a 10-card repeating pattern:
     row 1 (3 cards): 30 / 30 / 40
     row 2 (2 cards): 50 / 30   - inset 10% from each side
     row 3 (3 cards): 40 / 30 / 30
     row 4 (2 cards): 30 / 50   - inset 10% from each side          */
.work-scatter {
    /* above the hero's logo (z 2) and the divider phrase (z 1), so the
       cards cover them as the projects come in */
    position: relative;
    z-index: 3;
    --col-gap: clamp(1rem, 3vw, 3rem);
    /* left/right edge lines up with the "DOUBLE DASH" nav logo:
       .navbar .container padding (--space-xl) + .navbar .logo margin */
    margin: 0;
    padding: 0 var(--page-inset);
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--col-gap);
    row-gap: clamp(4rem, 9vw, 18rem);
    align-items: flex-start;
}

.work-card {
    display: flex;
    flex-direction: column;
    width: calc(30% - var(--col-gap) * 2 / 3 - 0.5px);
    text-decoration: none;
    color: var(--page-ink);
    /* --py set per-card by initCardParallax in main.js */
    transform: translateY(var(--py, 0px));
    will-change: transform;
}

/* rows of 3: the 30% and 40% cards */
.work-card:nth-child(10n + 3),
.work-card:nth-child(10n + 6) {
    width: calc(40% - var(--col-gap) * 2 / 3 - 0.5px);
}

/* rows of 2: two cards, 10% margin on the leading one */
.work-card:nth-child(10n + 4) {
    width: calc(50% - var(--col-gap) / 2 - 0.5px);
    margin-left: 10%;
}

.work-card:nth-child(10n + 5),
.work-card:nth-child(10n + 9) {
    width: calc(30% - var(--col-gap) / 2 - 0.5px);
}

.work-card:nth-child(10n + 9) {
    margin-left: 10%;
}

.work-card:nth-child(10n + 10) {
    width: calc(50% - var(--col-gap) / 2 - 0.5px);
}

/* ==================================================================
   Homepage portfolio - ROW SYSTEM
   ==================================================================
   index.html wraps the grid in <div class="work-scatter work-scatter--rows">.
   Inside it, group the cards into rows:

       <div class="work-row">
           <a class="work-card" style="--w: 30"> ... </a>   1 to 3 per row
           <a class="work-card" style="--w: 40"> ... </a>
       </div>

   Each row is centred, so a row whose widths don't add up to 100%
   bundles toward the middle of the page on its own.

   Per-card knobs, set on the <a class="work-card">:
       style="--w: 40"              width, as a % of the row   (default 33)
       style="--v: center"          vertical align in the row:
                                      flex-start (top, default) | center |
                                      flex-end (bottom) | stretch
       style="--y: 3rem"            manual vertical nudge (down +, up -)
       style="--h: 26rem"           frame height (any length; default is
                                      clamp(20rem, 26vh, 30rem))
       style="--p: 0.12"            scroll-parallax drift rate (default is
                                      a preset that varies per card;
                                      0 = this card doesn't drift)
       class="... work-card--tall"   height preset - portrait
       class="... work-card--short"  height preset - letterbox
   Combine them: style="--w: 40; --v: flex-end; --h: 30rem; --p: 0.05"

   No per-row CSS to write. Below 760px the rows stack into one
   full-width column (each card keeps its own --h) and parallax is off.
   ================================================================== */
.work-scatter--rows {
    flex-direction: column;
}

.work-scatter--rows .work-row {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--col-gap);
    row-gap: var(--col-gap);
    justify-content: center;
    align-items: flex-start;
    width: 100%;
}

.work-scatter--rows .work-card {
    /* --w = this card's share of the row (%). One gap's worth is shaved
       off so a full 3-up row (e.g. 33 + 33 + 33) still fits on the line. */
    width: calc(var(--w, 33) * 1% - var(--col-gap) * 2 / 3);
    /* --v = where the card sits vertically within the row;
       --y = an extra manual nudge on top of that. */
    align-self: var(--v, flex-start);
    margin: 0;
    margin-top: var(--y, 0px);
}

@media (max-width: 760px) {
    .work-scatter--rows .work-row {
        flex-direction: column;
        row-gap: clamp(3rem, 11vw, 5rem);
    }

    .work-scatter--rows .work-card {
        width: 100%;
        align-self: auto;
        margin-top: 0;
    }
}

/* ==================================================================
   Work page - SQUISH ROWS
   ==================================================================
   2 equal cards per <div class="work-row">, fixed width. Cards no
   longer grow or squish on hover - the frame, image-swap, hover-video
   and meta are the whole interaction. Used by both grids:
   <div class="work-scatter work-scatter--squish">.
   Below 760px it stacks to one full-width column.

   The count lives in two places and they have to agree: PER_ROW in
   sanity-cards.js (building the rows) and in main.js (repacking them
   after a filter change).
   ================================================================== */
.work-scatter--squish {
    flex-direction: column;
    row-gap: clamp(3rem, 7vw, 6rem);
    /* --page-inset like every other page, rather than the 1680px cap and
       7vw gutter this used to carry - that was what made this page sit
       narrower than the rest of the site. */
    padding-inline: var(--page-inset);
}

/* Taller than the shared default frame. The grid went from three up to
   two, so each card is roughly half again as wide as it was - at the old
   height they read as letterboxes. An inline --h on a card still wins.

   The vh term is what actually applies on most desktops; the rem floor
   and ceiling only catch very short and very tall viewports. */
.work-scatter--squish .work-card__frame {
    height: var(--h, clamp(26rem, 52vh, 44rem));
}

@media (max-width: 760px) {
    /* stacked and full width, so it is back to one card per row and the
       desktop height would run off the screen */
    .work-scatter--squish .work-card__frame {
        height: var(--h, clamp(20rem, 46vh, 30rem));
    }
}

.work-scatter--squish .work-row {
    /* the usable width once the gutter between the pair is taken out */
    --span: calc(100% - var(--col-gap));

    display: flex;
    column-gap: var(--col-gap);
    align-items: flex-start;
    width: 100%;
}

/* A fixed half track rather than flex-grow, so a row holding a single
   card (after filtering) keeps the same column width as a full row
   instead of stretching to fill it. */
.work-scatter--squish .work-row > .work-card {
    flex: 0 0 calc(var(--span) / 2);
    width: auto;
    min-width: 0;             /* never let a card push the row wider */
    margin: 0;
}

/* "In the works" filler tile */
.work-card--soon { cursor: default; }

.work-card--soon .work-card__frame {
    background: repeating-linear-gradient(-45deg,
        #1e1e1e, #1e1e1e 14px, #262626 14px, #262626 28px);
}

/* "In the works" tiles keep the normal title/category colours - the
   striped frame already reads as a placeholder */

/* Single column layout for homepage projects. The cards still arrive two
   to a .work-row, so this gap has to match the gap between rows or the
   stack reads as pairs. */
.page-home .work-scatter--squish .work-row {
    flex-direction: column;
    row-gap: clamp(3rem, 7vw, 6rem);
}

.page-home .work-scatter--squish .work-row > .work-card {
    flex: none;
    width: 60%;   /* was 80% */
    /* auto margins beat the row's align-items: flex-start */
    margin-inline: auto;
}

/* Phones keep the wider card: at 60% of a phone screen, with the phone
   frame height below, the card turns into a tall narrow strip. */
@media (max-width: 760px) {
    .page-home .work-scatter--squish .work-row > .work-card {
        width: 80%;
    }
}

/* The frame height was tuned for half-width cards, so a full-width card
   needs the shape to scale with it instead of staying short and wide.
   Above 760px only: the mobile heights below are already tuned for a
   single full-width card. */
@media (min-width: 761px) {
    .page-home .work-scatter--squish .work-card__frame {
        height: auto;
        aspect-ratio: 3 / 2;
        /* At full row width 3:2 works out taller than the viewport, so the
           card could not be seen in one piece without this. (was 80vh) */
        max-height: 65vh;
    }
}

@media (max-width: 760px) {
    .work-scatter--squish {
        max-width: none;
        padding-inline: var(--container-padding);
    }

    .work-scatter--squish .work-row {
        flex-direction: column;
        row-gap: clamp(2.5rem, 9vw, 4rem);
    }

    .work-scatter--squish .work-row > .work-card {
        flex: none;          /* stop the flex sizing - just stack full width */
        width: 100%;
    }
}

@media (prefers-reduced-motion: reduce) {
    .work-scatter--squish .work-row > .work-card { transition: none; }
}

.work-card__frame {
    display: block;
    position: relative;
    width: 100%;
    /* default height; --h (set inline, or by the presets below) wins */
    height: var(--h, clamp(20rem, 40vh, 30rem));
    overflow: hidden;
    background-color: var(--color-gray-placeholder);
}

/* portrait frame - good for vertically-oriented brands (~70% taller) */
.work-card--tall {
    --h: clamp(34rem, 44vh, 51rem);
}

/* letterbox frame - good for wide billboard / signage shots */
.work-card--short {
    --h: clamp(15rem, 19vh, 21rem);
}

/* empty (placeholder) frames - visible box on the black page */
.work-card__frame:empty {
    background: repeating-linear-gradient(-45deg,
        #1e1e1e, #1e1e1e 14px, #262626 14px, #262626 28px);
}

.work-card__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.6s var(--transition-smooth),
        opacity 0.5s var(--transition-smooth);
}

.work-card:hover .work-card__img {
    transform: scale(1.05);
}

/* Second still that cross-fades in over the first on hover. Add a
   <img class="work-card__img work-card__img--hover"> as the LAST child
   of .work-card__frame to give any card this behaviour. */
.work-card__img--hover {
    opacity: 0;
}

.work-card:hover .work-card__img--hover {
    opacity: 1;
}

.work-card__meta {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
    /* margin + padding together are just the gap to the image now that the
       rule between them is gone. Kept as two values so the title sits
       exactly where it did before the line was removed. */
    margin-top: var(--space-md);
    padding-top: var(--space-sm);
}

.work-card__title {
    font-family: var(--font-marker); /* Plantin */
    font-weight: 400;
    font-size: var(--fs-3);
    line-height: 1.05;
    transition: color 0.3s var(--transition-smooth);
    letter-spacing: -0.02em;
}

/* Short description under the title (projects page only, [data-descriptions]
   in js/sanity-cards.js; the text is the "Short description" field in the
   Studio). Body font, full page ink, kept to a readable line length. */
.work-card__desc {
    display: block;
    max-width: 34em;
    margin-top: 0.2rem;
    font-family: var(--font-body);
    font-size: var(--fs-5);
    font-weight: 400;
    line-height: 1.35;
    color: var(--page-ink);
}

/* (Titles used to swap to italic on hover, with a hidden italic copy
   under each one reserving the width so the swap never reflowed. Both
   are gone: the image is the whole hover now.) */

/* Homepage: the title is a display line sized to the card (fitTitles in
   js/sanity-cards.js), so it fills the card the way the image does, and
   always on one line. */
.page-home .work-card__meta {
    align-items: center;
    text-align: center;
    margin-top: 0;
    padding-top: 0;
    /* the frame is position: relative, so it paints over any unpositioned
       sibling; the title has to sit in its own layer to show on top */
    position: relative;
    z-index: 1;
}

.page-home .work-card__tags {
    justify-content: center;
}

/* Homepage labels are orange (text, outline and icon) and the titles are
   page ink - swapped from the site default. */
.page-home .tag {
    color: var(--color-orange);
    border-color: var(--color-orange);
}

/* --title-size is set by fitTitles() in js/sanity-cards.js; the clamp is
   only the first paint before the cards are measured */
.page-home .work-card__title {
    font-size: var(--title-size, clamp(2.75rem, 8vw, 8.5rem));
    /* page ink; the orange is on the labels (was the other way round) */
    color: var(--page-ink);
    /* always one line - fitTitles() sizes it to fit the card */
    white-space: nowrap;
    line-height: 0.92;
    letter-spacing: -0.035em;
    /* Trim the box to exactly cap height to baseline, so -0.4cap overlaps
       the image by 40% of the letters rather than 40% of blank leading.
       Firefox has no text-box yet and keeps the leading, so it overlaps
       a little less there. */
    text-box: trim-both cap alphabetic;
    margin-top: -0.4cap;
    /* trimming the bottom lets descenders hang free; keep them off the tags */
    margin-bottom: 0.25em;
}

/* See more button under the grid */
.work-more {
    display: flex;
    justify-content: center;
    margin-top: clamp(3rem, 7vw, 6rem);
}

/* Phones: single full-width column, still all the same height */
@media (max-width: 760px) {
    .work-scatter {
        row-gap: clamp(3rem, 11vw, 5rem);
    }

    .work-scatter .work-card {
        width: 100%;
        margin-left: 0;
    }
}

@media (max-width: 640px) {
    .work-scatter {
        padding: 0 var(--container-padding);
    }
}

/* ============================================
   NEW: About Section (Home Page)
   ============================================ */
.about-section {
    /* no background - inherits the page-wide gradient, fully black by
       the time it scrolls into view */
}

/* Parked sections. [hidden] on its own is a UA rule, which any author
   display declaration outranks, so it is restated here with weight. */
.about-section[hidden] {
    display: none !important;
}

.work-filters[hidden] {
    display: none !important;
}

/* Match the portfolio's gutters so the about block lines up with the
   work grid (and the "DOUBLE DASH" nav logo) instead of sitting in the
   narrower 1200px .container. */
.about-section > .container {
    max-width: none;
    padding-inline: var(--page-inset);
}

.about-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: clamp(2rem, 6vw, 5rem);
    align-items: stretch;     /* the text column matches the photo's height */
    justify-items: stretch;   /* was place-items:center, which stopped
                                 the image filling its column */
}

/* Two equal halves against the photo: the heading sits at the top of
   the image, the body starts level with its middle. */
.about-text {
    display: grid;
    grid-template-rows: 1fr 1fr;
}

.about-text > .about-greeting { align-self: start; margin-bottom: 0; }
.about-text > .about-body { align-self: start; }

.about-greeting {
    margin-bottom: var(--space-lg);
}

.greeting-hello {
    display: block;
    text-transform: none;
    font-family: var(--font-marker);
    font-style: italic;
    font-size: var(--fs-2);
    color: var(--color-orange);
    line-height: 1;
}

.greeting-name {
    display: block;
    font-family: var(--font-marker);
    font-size: var(--fs-4);
    text-transform: uppercase;
    margin-top: var(--space-xs);
}

/* the copy runs in two columns under the heading */
.about-description {
    columns: 2;
    column-gap: clamp(1.5rem, 3vw, 3rem);
}

.about-description p {
    font-size: var(--fs-5);
    font-weight: 300;
    line-height: 1.1;
    margin-bottom: var(--space-md);
    break-inside: avoid;   /* keep each paragraph whole in one column */
}

/* "Salam kenal" - the greeting leads in the serif */
.about-description .salam {
    font-family: var(--font-marker);   /* Plantin */
    font-style: italic;
    font-weight: 400;                  /* it ships regular + italic only */
    font-size: calc(var(--fs-5) + 4px);
}

.about-description p:last-child {
    margin-bottom: 0;
}

.about-image {
    position: relative;
}

.about-image>img:last-child {
    width: 100%;
    height: auto;
    display: block;
}

@media (max-width: 640px) {
    .about-section > .container {
        padding-inline: var(--container-padding);
    }

    .about-content {
        grid-template-columns: 1fr;
        gap: var(--space-xl);
    }

    /* stacked: normal top-down flow, no half-and-half split, one column */
    .about-text {
        display: block;
    }

    .about-description {
        columns: 1;
    }

    .about-text > .about-greeting {
        margin-bottom: var(--space-lg);
    }

    .about-image {
        order: -1;
    }

}

/* ============================================
   NEW: Services Section with Stickers

.sticker-branding.visible {
    transform: rotate(-8deg) translateY(20px) scale(1);
    opacity: 1;
}

.sticker-logo.visible {
    transform: rotate(10deg) translateY(-50px) scale(1);
    opacity: 1;
}

.sticker-print.visible {
    transform: rotate(-5deg) translateY(10px) scale(1);
    opacity: 1;
}

.sticker-illustrations.visible {
    transform: rotate(12deg) translateY(-20px) scale(1);
    opacity: 1;
}

@media (max-width: 640px) {

    .sticker-branding.visible {
        transform: rotate(-5deg) scale(1);
        opacity: 1;
    }

    .sticker-logo.visible {
        transform: rotate(8deg) scale(1);
        opacity: 1;
    }

    .sticker-print.visible {
        transform: rotate(-3deg) scale(1);
        opacity: 1;
    }

    .sticker-illustrations.visible {
        transform: rotate(10deg) scale(1);
        opacity: 1;
    }

}

/* ============================================
   NEW: Updated CTA Section

.cta-section .container {
    display: grid;
    grid-template-columns: 2fr 1fr;
    align-items: center;
}

.cta-section .btn {
    background-color: var(--color-black);
    color: var(--color-orange);
    font-size: 1rem;
}

.cta-section .btn:hover {
    background-color: transparent;
    color: var(--color-black);
    border-color: var(--color-black);
}

@media (max-width: 640px) {

    .cta-section .container {
        display: flex;
        gap: var(--space-xl);
        align-items: center;
    }

    .cta-line1,

    .cta-section .container {
        flex-direction: column;
    }
}

/* ============================================
   NEW: Updated Footer

/* ==================================================================
   OUTRO - CTA band + full-screen footer
   ==================================================================
   Sits at the bottom of every page. The footer carries the brand orange
   itself rather than the page fading into it. initOutro() still toggles
   .outro-active on <html>, which the navbar ink keys off.
   ================================================================== */

.outro {
    /* the same gutter as every other section, so the footer lines up with
       the grid and the navbar instead of sitting further in */
    --outro-gutter: var(--page-inset);
    --outro-ink: var(--color-black);
    --outro-accent: var(--color-blue);      /* indigo #2E2B8A */
    /* black, same as the ink: the footer carries no second text colour */
    --outro-muted: var(--color-black);
    --outro-line: rgba(0, 0, 0, 0.2);

    background-color: transparent;
    color: var(--outro-ink);
}

/* ── scroll parallax ──────────────────────────────────────── */
/* Any element with data-parallax="<rate>" drifts as it crosses the
   viewport; initParallax() in main.js sets --py. Higher rate = more
   lag. Off below 760px and under prefers-reduced-motion. */
[data-parallax] {
    transform: translateY(var(--py, 0px));
    will-change: transform;
}

/* ── CTA band ─────────────────────────────────────────────── */
/* No box: the two-line heading sits left on the orange outro and the
   link sits right under it, both inside the site container. The gif
   rides on the link (.cta-spark). */
.cta-band {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: clamp(1.5rem, 3.5vw, 2.75rem);
    width: 100%;
    max-width: 960px;
    margin-inline: auto;
    padding: clamp(4rem, 10vw, 8rem) var(--container-padding) clamp(6rem, 14vw, 11rem);
    /* lets the link's slide measure the band's width in cqi */
    container-type: inline-size;
}

.cta-band__link { align-self: flex-end; }

/* Armed by initCtaSlide(): park the link at the band's left edge
   (100cqi is the content width, less the link's own width), then let
   .is-in carry it across to the right. */
.cta-band.is-armed .cta-band__link {
    opacity: 0;
    transform: translateX(calc(-100cqi + 100%));
    transition: transform 1.2s cubic-bezier(0.2, 0.8, 0.2, 1),
        opacity 0.5s var(--transition-smooth),
        text-decoration-thickness 0.2s var(--transition-smooth);
}

.cta-band.is-armed.is-in .cta-band__link {
    opacity: 1;
    transform: none;
}

.cta-band__heading {
    margin: 0;
    color: var(--outro-ink);
    font-family: var(--font-marker);        /* Plantin */
    font-weight: 400;
    font-size: clamp(3rem, 10vw, 9rem);
    line-height: 0.95;
    letter-spacing: -0.03em;
    text-transform: none;                   /* beat the global h1-h4 rule */
}

.cta-band__hot { font-style: italic; }

.cta-band__link {
    font-family: var(--font-marker);        /* Plantin, as the heading */
    font-size: var(--fs-4);
    font-weight: 400;
    color: var(--outro-ink);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.3em;
    transition: text-decoration-thickness 0.2s var(--transition-smooth);
}

.cta-band__link:hover,
.cta-band__link:focus-visible {
    text-decoration-thickness: 2px;
}

.cta-band__arrow {
    display: inline-block;
    transition: transform 0.3s var(--transition-smooth);
}

.cta-band__link:hover .cta-band__arrow { transform: translateX(0.25em); }

/* ── hover gif pop ────────────────────────────────────────────
   Put .gif-pop on any element and drop an
   <img class="gif-pop__img"> inside it; hovering pops the image
   up above. Add .gif-pop--below to pop downward instead (used by
   the navbar logo, which has nothing above it).
   initCtaSpark() in main.js adds the cursor drift and removes the
   <img> if the file is missing. */
.gif-pop { position: relative; }

.gif-pop__img {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 0.6rem);
    width: clamp(160px, 17vw, 260px);
    /* the global img{max-width:100%} would otherwise clamp this to the
       width of its trigger - tiny on the navbar logo */
    max-width: none;
    height: auto;
    transform: translate(calc(-50% + var(--gx, 0px)), 10px) rotate(var(--tilt, -5deg)) scale(0.9);
    opacity: 0;
    pointer-events: none;
    z-index: 60;
    transition: opacity 0.25s var(--transition-smooth),
        transform 0.4s var(--transition-bounce);
}

.gif-pop:hover .gif-pop__img {
    opacity: 1;
    transform: translate(calc(-50% + var(--gx, 0px)), 0) rotate(var(--tilt, -5deg)) scale(1);
}

.gif-pop--below .gif-pop__img {
    bottom: auto;
    top: calc(100% + 0.6rem);
    transform: translate(calc(-50% + var(--gx, 0px)), -10px) rotate(var(--tilt, -5deg)) scale(0.9);
}

.gif-pop--below:hover .gif-pop__img {
    transform: translate(calc(-50% + var(--gx, 0px)), 0) rotate(var(--tilt, -5deg)) scale(1);
}

/* a different lean per instance, so they don't all read as one asset */
.salam.gif-pop           { --tilt: -8deg; }
.tagline-strong.gif-pop  { --tilt: -3deg; }
.cta-spark               { --tilt: 6deg; }

/* no hover on touch - don't leave one stuck open after a tap */
@media (hover: none) {
    .gif-pop__img { display: none; }
}

/* "Start a project" - hovering it pops a gif up above the link.
   Drop your clip in at assets/images/rock-and-roll.gif; if the file
   isn't there initCtaSpark() removes the <img> so nothing breaks. */
.cta-spark {
    position: relative;
    display: inline-block;
}

.cta-spark__gif {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 0.25em);
    width: clamp(150px, 19vw, 280px);
    max-width: none;
    height: auto;
    /* --gx is nudged by the cursor (initCtaSpark in main.js) */
    transform: translate(calc(-50% + var(--gx, 0px)), 10px) rotate(var(--tilt, -5deg)) scale(0.9);
    opacity: 0;
    pointer-events: none;
    z-index: 3;
    transition: opacity 0.25s var(--transition-smooth),
        transform 0.4s var(--transition-bounce);
}

.cta-spark:hover .cta-spark__gif {
    opacity: 1;
    transform: translate(calc(-50% + var(--gx, 0px)), 0) rotate(var(--tilt, -5deg)) scale(1);
}

@media (prefers-reduced-motion: reduce) {
    .cta-spark__gif { transition: opacity 0.2s linear; }
}

/* ── full-screen footer ───────────────────────────────────── */
.site-footer {
    /* solid orange, painted by the footer itself. The page behind it
       stays black: it no longer fades to orange on the way down. */
    background-color: var(--color-orange);
    /* was 100svh, when this held the thanks line, the wordmark and a
       tall portrait illustration. It is a lockup and two short rows now,
       so it sizes to its content with a floor rather than a screenful. */
    min-height: 45svh;
    display: flex;
    flex-direction: column;
    gap: clamp(1rem, 2vw, 1.75rem);
    padding: clamp(1.5rem, 3.5vw, 2.75rem) var(--outro-gutter) 1.5rem;
    /* no top rule - the orange band is the divider */
}

/* One row, split down the middle: the mark-and-wordmark lockup on the
   left, the page links starting at the centre line. It soaks up the
   leftover height, so the social row and copyright sit tight together
   at the bottom. */
.footer-top {
    flex: 1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* The row sits at the bottom of the stretched area, so the spare
       height falls above the lockup rather than between it and the rule
       below. align-items keeps the lockup and the links level with each
       other inside that row. */
    align-content: end;
    align-items: center;
    gap: clamp(1.5rem, 4vw, 4rem);
}

/* ── page links: left-aligned, starting at the centre ── */
.footer-nav {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
}

.footer-nav a {
    font-family: var(--font-marker);   /* Plantin */
    font-weight: 400;
    font-size: var(--fs-4);
    line-height: 1.15;
    letter-spacing: -0.01em;
    color: var(--outro-ink);
    text-decoration: none;
    width: fit-content;
}

/* An arrow slides in beside the word on hover, in place of the
   underline this used to grow. It sits after the word and points back
   at it, so it reads as a marker singling that link out, and it drifts
   in from the right towards the word. It is in the markup from the
   start, just invisible, so its width is always reserved and the line
   never jumps. */
.footer-nav a::after {
    content: '\2190';   /* ← */
    display: inline-block;
    margin-left: 0.3em;
    opacity: 0;
    transform: translateX(0.3em);
    transition: opacity 0.25s var(--transition-smooth),
        transform 0.25s var(--transition-smooth);
}

.footer-nav a:hover::after,
.footer-nav a:focus-visible::after {
    opacity: 1;
    transform: none;
}

/* ── the lockup: mark, then the wordmark beside it ── */
.footer-lockup {
    display: flex;
    align-items: center;
    gap: clamp(0.6rem, 1.5vw, 1.25rem);
}

/* the unicorn mark, sitting to the left of the wordmark */
.footer-mark {
    display: block;
    width: auto;
    height: clamp(36px, 4.6vw, 68px);   /* was up to 96px */
    flex: none;
    /* the artwork ships cream; same trick the navbar tint uses to put it
       on a light ground without a second copy of the file */
    filter: brightness(0);
}

/* "DOUBLE DASH CREATIVE", 2035.33 x 411.44. It used to span the whole
   container; now it is a set width beside the mark. */
.footer-wordmark {
    display: block;
    width: clamp(150px, 24vw, 380px);   /* was up to 540px */
    height: auto;
    object-fit: contain;
}

/* One short row: Portfolio | About | Contact | socials.
   Stacks only on mobile. */
/* just the socials now, pulled to the left edge */
.footer-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* icons left, copyright right */
    justify-content: space-between;
    gap: 0.75rem 2rem;

    /* the rule dividing this row from the lockup, with room on both sides:
       margin above it, padding below it */
    margin-top: clamp(1rem, 2vw, 1.5rem);
    border-top: 1px solid var(--outro-ink);
    padding-top: clamp(1rem, 2.5vw, 1.75rem);
    padding-bottom: clamp(0.25rem, 1vw, 0.75rem);
}

/* label + icons as one group, so they wrap together rather than the
   label orphaning onto its own line */
.footer-follow {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: clamp(0.75rem, 2vw, 1.25rem);
}

/* (The "Follow for more:" label that used to sit before the icons is
   gone; the icons speak for themselves.) */

.footer-copyright {
    font-size: var(--fs-6);
    color: var(--outro-muted);
}

/* social icons: initSocial() clones this into the burger menu */
.site-footer .footer-social {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: clamp(0.85rem, 2vw, 1.4rem);
}

.site-footer .footer-social a {
    display: inline-flex;
    align-items: center;
    width: fit-content;
    height: auto;
    padding: 0;
    background: none;
    color: var(--outro-ink);
    transition: color 0.3s var(--transition-smooth),
        transform 0.3s var(--transition-bounce);
}

.site-footer .footer-social a:hover {
    color: var(--outro-accent);
    transform: scale(1.15);
}

.site-footer .footer-social a svg {
    width: 28px;
    height: 28px;
    flex: none;
    color: inherit;
}

/* the navbar stays put over the outro; both rails clear out */


@media (prefers-reduced-motion: reduce) {
    .navbar { transition: none; animation: none; }
    .logo-mark { transition: none; }
}

/* Narrow: the lockup and the links stop fitting side by side, so the
   lockup runs full width with the links as a wrapping row under it. */
@media (max-width: 760px) {
    .footer-top {
        grid-template-columns: 1fr;
        align-items: start;
        gap: clamp(1.75rem, 6vw, 3rem);
    }

    .footer-nav {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 0.5rem 1.5rem;
    }

    .footer-lockup {
        width: 100%;
    }

    .footer-wordmark {
        width: auto;
        flex: 1;
        min-width: 0;
    }
}

/* Phones: the lockup stands up - mark over wordmark, both centred - and
   the page links go back to a stacked column under it, centred to match.
   Same breakpoint as the burger menu. */
@media (max-width: 640px) {
    .footer-top {
        justify-items: center;
        text-align: center;
    }

    .footer-lockup {
        flex-direction: column;
        align-items: center;
        gap: 0.7rem;
    }

    .footer-mark {
        height: clamp(44px, 12vw, 64px);
    }

    /* the 760px rule above lets the wordmark flex to fill the row; in a
       column it has to go back to a width of its own */
    .footer-wordmark {
        flex: none;
        width: min(78vw, 320px);
    }

    .footer-nav {
        flex-direction: column;
        align-items: center;
        gap: 0.55rem;
    }
}

/* ==================================================================
   PAGE LOADER - spinning mark, stepped to 12 fps
   ==================================================================
   Hidden by default so the site is never stuck behind it if JS fails.
   A one-line script in <head> adds .is-loading to <html> before first
   paint; initPageLoader() in main.js clears it on load and puts it
   back when you click through to another page.
   ================================================================== */
.page-loader {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: grid;
    place-items: center;
    /* always black, on every page - not --page-bg, which is white on the
       light pages and left the off-white mark invisible */
    background-color: var(--color-black);
    perspective: 800px;              /* the mark turns in depth, not flat */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.45s var(--transition-smooth),
        visibility 0s linear 0.45s;
}

.is-loading .page-loader {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.25s var(--transition-smooth);
}

.page-loader__mark {
    width: clamp(84px, 12vw, 150px);
    height: auto;
    transform-style: preserve-3d;

    /* 12 discrete frames per turn, one turn per 1.2s */
    animation: ddLoaderSpin 1.2s steps(12, end) infinite;
}

/* Y-axis turn, same axis as the hero's extruded model, with a small
   fixed X tilt so the bevel catches. Runs 15deg -> 375deg rather than
   0 -> 360 so no frame lands exactly on 90 or 270, where a flat plane
   would be edge-on and vanish. */
@keyframes ddLoaderSpin {
    from { transform: rotateX(-10deg) rotateY(15deg); }
    to   { transform: rotateX(-10deg) rotateY(375deg); }
}

/* don't let the page scroll behind the overlay */
.is-loading body { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
    .page-loader__mark { animation: none; }
    .page-loader { transition: opacity 0.2s linear; }
}

/* ==================================================================
   WORK CATEGORY TAGS + FILTER BAR
   ==================================================================
   Both are generated from WORK_CATEGORIES in js/main.js. In the HTML
   you only ever set  data-cats="logo brand print"  on a .work-card.
   ================================================================== */

/* ── the pills under each card title ──────────────────────── */
.work-card__tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;   /* a touch more room to suit the roomier labels */
}

.tag {
    display: inline-block;
    /* Balanced box: the text is trimmed to cap height and baseline, so
       the padding is measured from the letters themselves and the word
       sits dead centre (the font's own leading left it low, with more
       room above than below). Even em padding scales with the text;
       the sides are a touch wider than top/bottom, which reads as even. */
    text-box: trim-both cap alphabetic;
    padding: 0.55em 0.7em;
    /* page ink, not the indigo accent: indigo on the black ground is
       about 1.6:1 and all but disappears */
    border: 1px solid var(--page-ink);
    color: var(--page-ink);
    /* body font (Forma DJR Micro), in the case the category is written
       in - no forced caps. 600 renders as Forma's bold (700). */
    font-family: var(--font-body);
    font-size: var(--fs-6);
    /* no tightening at this small size - negative tracking crowded the
       letters and left the right side of each word short */
    letter-spacing: 0;
    line-height: 1;
    white-space: nowrap;
}

.tag svg {
    width: 13px;
    height: 13px;
    flex: none;
}

/* ── filter bar (projects.html only) ──────────────────────────── */
.work-filters {
    margin: 0 0 clamp(2rem, 5vw, 3.5rem);
    padding-inline: var(--page-inset);
}

.work-filters__list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* the collapsed toggle is mobile-only */
.work-filters__toggle { display: none; }

.work-filter {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.8rem 0.55rem 1rem;
    border-radius: 999px;
    border: 1px solid var(--page-ink);
    background: none;
    color: var(--page-ink);
    font-family: var(--font-body);
    font-size: 0.78rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color 0.25s var(--transition-smooth),
        color 0.25s var(--transition-smooth),
        border-color 0.25s var(--transition-smooth);
}

.work-filter svg {
    width: 15px;
    height: 15px;
    flex: none;
}

/* The padding above is lopsided on purpose - the trailing icon needs
   less optical space than a text edge. "All" has no icon, so it needs
   even padding or its label reads off centre. */
.work-filter[data-cat="all"] {
    padding-inline: 0.95rem;
}

.work-filter:hover {
    border-color: var(--color-orange);
    color: var(--color-orange);
}

.work-filter[aria-pressed="true"] {
    background-color: var(--color-orange);
    border-color: var(--color-orange);
    color: var(--color-black);
}

.work-filter:focus-visible {
    outline: 2px solid var(--color-orange);
    outline-offset: 3px;
}

/* [hidden] needs restating - .work-empty sets no display of its own,
   but an author `display` would beat the UA's [hidden]{display:none}. */
.work-empty[hidden] {
    display: none !important;
}

/* Cards settle in after the rows are repacked by a filter change. */
@keyframes workCardIn {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

.work-card--enter {
    animation: workCardIn 0.45s var(--transition-smooth) both;
    animation-delay: calc(var(--i, 0) * 55ms);
}

@media (prefers-reduced-motion: reduce) {
    .work-card--enter { animation: none; }
}

/* nothing matched */
.work-empty {
    padding: clamp(2rem, 6vw, 4rem) var(--page-inset);
    font-family: var(--font-marker);
    font-size: var(--fs-3);
    color: var(--color-orange);
}

@media (prefers-reduced-motion: reduce) {
    .work-filter { transition: none; }
}

@media (max-width: 640px) {
    .work-filters { padding-inline: var(--container-padding); }
    .work-empty { padding-inline: var(--container-padding); }

    /* ── mobile: the chips collapse into a dropdown ── */
    .work-filters__toggle {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        width: 100%;
        padding: 0.85rem 1rem;
        border: 1px solid var(--page-ink);
        background: none;
        color: var(--page-ink);
        font-family: var(--font-body);
        font-size: 0.85rem;
        line-height: 1;
        cursor: pointer;
    }

    .work-filters__state {
        margin-left: auto;
        color: var(--color-orange);
    }

    .work-filters__chev {
        width: 16px;
        height: 16px;
        flex: none;
        transition: transform 0.3s var(--transition-smooth);
    }

    .work-filters.is-open .work-filters__chev { transform: rotate(180deg); }

    .work-filters.is-open .work-filters__toggle {
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
        border-bottom-color: transparent;
    }

    .work-filters__list {
        display: none;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        border: 1px solid var(--page-ink);
        border-top: 0;
        overflow: hidden;
    }

    .work-filters.is-open .work-filters__list { display: flex; }

    .work-filters__list .work-filter {
        justify-content: flex-start;
        border: 0;
        border-radius: 0;
        padding: 0.8rem 1rem;
        font-size: 0.85rem;
    }

    .work-filters__list .work-filter + .work-filter {
        border-top: 1px solid rgba(0, 0, 0, 0.18);
    }
}

@media (prefers-reduced-motion: reduce) {
    .work-filters__chev { transition: none; }
}

/* ── projects.html page title ─────────────────────────────────── */
.page-header__title {
    margin: 0;
    font-family: var(--font-marker);   /* Plantin */
    font-style: italic;                /* every page title is italic */
    font-weight: 400;
    font-size: var(--fs-1);
    line-height: 1;
    letter-spacing: -0.04em;
    text-transform: none;              /* beat the global h1-h4 rule */
    color: var(--page-ink);
}

/* Projects page title: "Basic here is written in past tense." - big,
   centred, orange, italic. Same size as the About page's headings
   (--fs-2, ~64px on desktop). */
.pj-statement {
    margin: 0 auto var(--space-xl);
    text-transform: none;              /* beat the global h1-h4 rule */
    max-width: 18em;
    text-align: center;
    font-family: var(--font-marker);   /* Plantin */
    font-style: italic;
    font-weight: 400;
    font-size: var(--fs-2);
    line-height: 1.05;
    letter-spacing: -0.02em;
    text-wrap: balance;
    color: var(--color-orange);
}

/* the title lines up with the filter bar and the card grid below it */
.page-header > .container {
    max-width: none;
    padding-inline: var(--page-inset);
}

/* ============================================
   ABOUT + CONTACT: match the width of everything else

   Both pages sat in the 1200px .container while the work grid, the
   case studies and the homepage about section all run to --page-inset.
   On a wide screen that read as a different site.
   ============================================ */
.about-opening > .container,
.about-reasoning > .container,
.services-section > .container,
.about-image-divider > .container,
.about-process > .container,
.contact-section > .container {
    max-width: none;
    padding-inline: var(--page-inset);
}

/* ============================================
   ABOUT PAGE (2026)

   Four blocks: what Double Dash is, who runs it, what it does, and
   how the work happens.

   House rules this follows:
     - sizes come from the --fs-1..6 tiers, never a raw clamp.
       fs-1 is the BIGGEST, fs-6 the smallest.
     - big display lines are Plantin (--font-marker) with
       text-transform: none, to beat the global uppercase on h1-h4.
     - small labels are Forma DJR Micro (--font-body) uppercase at --fs-6.
     - accent is --color-orange, same as every other page.
   ============================================ */
/* The about page's running text is the body font (Forma DJR Micro; it was
   Noto Serif); its headings stay the display serif (Plantin). Scoped to
   the page's own sections. */
.page-about .ab-open,
.page-about .ab-me,
.page-about .ab-block {
    font-family: var(--font-body);
    /* no section-wide tightening: each kind of text sets its own below */
    letter-spacing: normal;
}

/* The about page's running text takes a normal weight (400) instead of
   the site-wide thin, at the font's own spacing. (It was squeezed to
   -0.04em, a value left from Noto Serif that crowded Forma's letters.) */
.page-about .ab-open__body p,
.page-about .ab-me__body p,
.page-about .ab-field li,
.page-about .forge__col p {
    font-weight: 400;
    letter-spacing: 0;
}

/* Big Plantin headings: a slight tightening suits display sizes */
.page-about .ab-me__title,
.page-about .ab-label,
.page-about .forge__name {
    letter-spacing: -0.02em;
}

/* Small all-caps headings (Logo & Brand, Illustration...): capitals need
   a little air between them to read at this size, not less */
.page-about .ab-field h3 {
    letter-spacing: 0.08em;
}


.ab-inset { padding-inline: var(--page-inset); }

.ab-block { padding-block: var(--space-4xl); }

/* ── 1. the opening, on the growing liquid box ──
   The box itself is css/cta-grow.css + js/cta-grow.js (the "Ready to
   Rock & Roll?" effect). This is just the text centred on it. */
.ab-hero__body {
    position: relative;
    z-index: 1;
    /* the font size lives here so the 40em measure is in the text's own
       size (~1200px on desktop, close to the full box), not the page's
       16px. Was 26em, which bunched it up in the middle. */
    font-size: var(--fs-4);
    max-width: 40em;
    margin: 0 auto;
    /* clear of the box's own side margin (5vw, js/cta-grow.js) plus the
       usual gutter, so it never touches the box edge */
    padding: 0 calc(5vw + var(--page-inset));
    box-sizing: content-box;
    text-align: center;
    color: var(--color-cream);
    grid-area: 1 / 1;   /* shares the cell with .ab-hero__promise */
}

/* "That's the promise." - one line across the whole stage, laid over
   the opening text in the same grid cell. js/cta-grow.js sizes it to
   the stage width and stretches it in (data-grow-after) as the box
   closes to its square, once the opening text has gone. */
.ab-hero__promise {
    grid-area: 1 / 1;
    position: relative;
    z-index: 1;
    width: max-content;     /* so the js can read its natural width */
    margin: 0;
    font-family: var(--font-marker);
    font-style: italic;
    font-weight: 400;
    font-size: 11vw;        /* first paint only; the js fits it exactly */
    line-height: 1;
    letter-spacing: -0.03em;
    white-space: nowrap;
    color: var(--color-cream);
    opacity: 0;
    pointer-events: none;
}

/* the two halves slide off in opposite directions (js/cta-grow.js) */
.ab-hero__promise span { display: inline-block; }

/* no motion: no stage hold, so it simply sits under the opening text */
@media (prefers-reduced-motion: reduce) {
    .ab-hero__promise { grid-area: auto; opacity: 1; }
}

html.liquid-hero-reduced .ab-hero__promise { grid-area: auto; opacity: 1; }

.ab-hero__body p {
    margin: 0;
    font-family: var(--font-marker);   /* Plantin */
    font-style: italic;
    font-size: inherit;
    font-weight: 400;
    line-height: 1.25;
    letter-spacing: -0.01em;
    text-wrap: balance;
}

/* the page's h1, kept for screen readers and search only */
.ab-hero__title {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── (old) 1. what Double Dash is - no longer on the page ──
   Opens on the orange ground, same pairing the outro uses at the other
   end of the site: black ink, indigo for the accent. Everything below
   it returns to the black page. */
.ab-open {
    background-color: var(--color-orange);
    color: var(--color-black);
    /* Half the first screen. svh not vh, so a phone's collapsing URL bar
       cannot make it taller than intended. min-height, not height, so it
       still grows if the copy needs more room. */
    min-height: 50svh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* the top figure is the navbar clearance plus breathing room under it */
    padding-block: calc(var(--space-4xl) + var(--space-lg)) var(--space-2xl);
}

/* One line, always - it reads as a single phrase and breaking it in
   the middle spoils that. Since it cannot wrap, the type steps down a
   tier on narrow screens instead of running off the edge. */
.ab-statement {
    font-family: var(--font-marker);
    font-style: italic;
    font-weight: 400;
    font-size: var(--fs-2);
    line-height: 1;
    text-transform: none;              /* beat the global h1-h4 rule */
    white-space: nowrap;
    margin: 0;
    color: var(--color-black);
}

@media (max-width: 480px) {
    .ab-statement { font-size: var(--fs-3); }
}

@media (max-width: 360px) {
    .ab-statement { font-size: var(--fs-4); }
}

/* Runs the full width, single measure. It sits above the headline, so
   the gap below it is what separates the two; the gap above is the air
   between it and the navbar. */
.ab-open__body {
    margin: var(--space-2xl) 0;
}

/* A lead size: --fs-4 is about half of --fs-2 at the top of its range
   (1.9rem against 4rem), and still well above body copy. */
.ab-open__body p {
    font-size: var(--fs-4);
    line-height: 1.3;
    margin: 0 0 0.6em;
}

.ab-open__body p:last-child { margin-bottom: 0; }

/* ── 2. who runs it ──
   The homepage about section's shape at a smaller size: the title sits
   level with the top of the photo, the body starts level with its
   middle. The two equal rows are what does that. */
.ab-me { padding-block: var(--space-4xl); }

.ab-me__grid {
    display: grid;
    /* the photo takes the smaller share - stretch is what keeps it the
       same height as the text, which is what puts the body level with
       its middle, so the width is the only thing to trade */
    grid-template-columns: 1.6fr 1fr;
    gap: var(--space-2xl);
    align-items: stretch;
}

.ab-me__text {
    display: grid;
    grid-template-rows: 1fr 1fr;
}

.ab-me__title {
    font-family: var(--font-marker);
    font-weight: 400;
    font-size: var(--fs-2);
    line-height: 1;
    text-transform: none;
    margin: 0;
    align-self: start;
}

.ab-me__title em {
    font-style: italic;
    color: var(--color-orange);
}

.ab-me__body p {
    font-size: var(--fs-5);
    line-height: 1.55;
    margin: 0 0 1em;
}

.ab-me__body p:last-child { margin-bottom: 0; }

/* same treatment the homepage gives it */
.ab-me__body .salam {
    font-family: var(--font-marker);
    font-style: italic;
    font-weight: 400;
    font-size: calc(var(--fs-5) + 4px);
}

.ab-me__image img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ── 3. services ──
   Heading on the left, six groups on the right: Logo & Brand /
   Illustration / Motion, then Web / Social / Print.

   Same track sizes and gap as .ab-me__grid above, so the groups start
   on the photo's left edge rather than floating on their own measure. */
.ab-services {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2xl);
    align-items: start;
}

/* same setting as the other two section headings on this page */
.ab-label {
    font-family: var(--font-marker);
    font-size: var(--fs-2);
    font-weight: 400;
    text-transform: none;
    line-height: 1;
    margin: 0;
    color: var(--page-ink);
}

.ab-fields {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-lg);
}

.ab-field h3 {
    font-family: var(--font-body);
    font-size: var(--fs-6);
    font-weight: 600;
    text-transform: uppercase;
    line-height: 1.2;
    margin: 0 0 var(--space-sm);
    padding-bottom: 0.7rem;
    border-bottom: thin solid currentColor;
}

.ab-field ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.ab-field li {
    font-size: var(--fs-5);
    line-height: 1.45;
    padding-block: 0.25rem;
}

/* ── 4. FORGE, five columns ── */
/* The same shape as the services list: the heading sits in the left
   column of .ab-services and the stages stack down the right. */
.forge {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-lg);
}

/* Name and description sit side by side, the same label-then-content
   shape the section itself uses. baseline so the big serif name lines
   up with the first line of the copy rather than its box. */
.forge__col {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    gap: var(--space-md);
    align-items: baseline;
}

.forge__name {
    font-family: var(--font-marker);
    font-weight: 400;
    font-size: var(--fs-3);
    line-height: 1;
    text-transform: none;
    margin: 0;
}

.forge__col p {
    font-size: var(--fs-5);
    line-height: 1.55;
    margin: 0;
}

/* ── stacked ── */
@media (max-width: 1300px) {
    /* the service groups get tight before the page stacks */
    .ab-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 860px) {
    .ab-services {
        grid-template-columns: 1fr;
        gap: var(--space-md);
    }

    .ab-label { margin-bottom: var(--space-xs); }

    /* title, body, then photo - normal top-down reading order */
    .ab-me__grid {
        grid-template-columns: 1fr;
        gap: var(--space-lg);
    }

    .ab-me__text {
        display: block;   /* no half-and-half split once it is stacked */
    }

    .ab-me__title { margin-bottom: var(--space-md); }

    .ab-me__image img { height: auto; }


    .ab-block,
    .ab-me { padding-block: var(--space-3xl); }
}

@media (max-width: 560px) {
    .ab-fields { grid-template-columns: 1fr; }
}
/* ============================================
   FORGE PROCESS PAGE

   Frame / Onboard / Refine / Generate / Exhibit. One band per stage,
   with a rail down the left that tracks which one you are in.
   Same house rules as the about page above.
   ============================================ */
.fg-inset { padding-inline: var(--page-inset); }

/* the word set as five separate letters so they can be spread to the
   full measure - flex, not letter-spacing, which leaves a gap hanging

.fg-word__letter { display: block; }

/* ── stacked ── */
@media (max-width: 900px) {
    /* the rail needs a margin of its own to sit in, and there is not
       one any more */
    .fg-rail { display: none; }

    .fg-stage { padding-block: var(--space-3xl); }
}

/* ============================================
   NAV TINT

   A section marked [data-nav-tint] is a coloured band the bar should
   blend into while it is over it. The bar carries that colour, and
   switches to the site's black once the band scrolls out from under
   it. initNavTint in main.js does the toggling.

   The ink flips with it: the logo is a fixed-colour SVG loaded as an
   <img>, so it cannot inherit currentColor - brightness(0) gives it
   the same solid-black treatment the footer logotype gets on orange.
   ============================================ */

/* cream over the dark hero and the dark takeover sheet */
.page-home .navbar:not(.is-solid) .logo,
.page-home .navbar:not(.is-solid) .nav-link-cycle,
.page-home .navbar:not(.is-solid) .nav-links a,
.page-home .navbar:not(.is-solid) .motion-toggle,
.menu-open .navbar .logo,
.menu-open .navbar .nav-link-cycle,
.menu-open .navbar .nav-links a {
    color: var(--color-cream);
}

.page-home .navbar:not(.is-solid) .logo-mark,
.menu-open .navbar .logo-mark { filter: none; }

.page-home .navbar:not(.is-solid) .mobile-menu-toggle span,
.page-home .navbar:not(.is-solid) .motion-toggle__icon,
.menu-open .navbar .mobile-menu-toggle span {
    background-color: var(--color-cream);
}

.page-home .navbar:not(.is-solid) .motion-toggle { border-color: var(--color-cream); }

/* black over the two orange bands */
.navbar.is-tinted .logo,
.navbar.is-tinted .nav-link-cycle,
.navbar.is-tinted .nav-links a,
.navbar.is-tinted .motion-toggle {
    color: var(--color-black);
}

.navbar.is-tinted .logo-mark { filter: brightness(0); }

.navbar.is-tinted .mobile-menu-toggle span,
.navbar.is-tinted .motion-toggle__icon {
    background-color: var(--color-black);
}

.navbar.is-tinted .motion-toggle { border-color: var(--color-black); }

/* The hand-rolled version of this is gone: initNavTint used to watch for
   a coloured band and flip the ink by hand. The difference blend does the
   same job continuously and for every colour, not just the orange one.
   .is-tinted is still toggled by main.js and now styles nothing.

   The one case the blend cannot read is the takeover menu, which paints
   its sheet ABOVE the page but BELOW the bar - so the bar would invert
   against the page it can no longer see. Blending is switched off while
   the menu is open and the ink set explicitly. */
.menu-open .navbar .nav-link-cycle,
.menu-open .navbar .nav-links a { color: var(--color-cream); }
.menu-open .navbar .logo-mark { filter: none; }
.menu-open .navbar .mobile-menu-toggle span { background-color: var(--color-cream); }

/* ============================================
   BODY COPY WEIGHT

   Thin (100), the weight the homepage About section has always used,
   applied to prose everywhere so the site reads consistently.

   Set per-selector rather than on body: nav links, form labels and
   inputs, the filter chips, the category tags and the footer base all
   declare no weight of their own, so a global rule
   would thin those out too, which is not what is wanted.
   ============================================ */
.about-description p,
.ab-open__body p,
.ab-me__body p,
.ab-field li,
.forge__col p,
.case-study-intro .description,
.cs-band p,
.cs-missing p {
    font-weight: 300;
}

/* ============================================
   CASE STUDY INTRO: description + scope

   Two columns under the title. The scope column is dropped
   entirely when a project has none, and :only-child then lets the
   description take the full width on its own.
   ============================================ */
.case-study-body {
    display: grid;
    /* the reference runs its copy at about 60% against the scope list */
    grid-template-columns: 3fr 2fr;
    gap: var(--space-3xl);
    align-items: start;
}

.case-study-body .description:only-child {
    grid-column: 1 / -1;
}

/* "Scope" set like the other headings on the page: Plantin, no caps */
.case-study-scope__title {
    font-family: var(--font-marker);   /* Plantin */
    font-size: var(--fs-4);
    font-weight: 400;
    text-transform: none;              /* beat the global h1-h4 rule */
    letter-spacing: -0.01em;
    line-height: 1.2;
    margin: 0 0 var(--space-sm);
    padding-bottom: 0;
}

/* the items as a bulleted list, the same as the about page's skills */
.case-study-scope ul {
    list-style: disc;
    margin: 0;
    padding-left: 1.1em;
}

.case-study-scope li {
    font-size: var(--fs-5);
    line-height: 1.5;
    padding-block: 0.2rem;
    padding-left: 0.2em;
}

@media (max-width: 860px) {
    /* the scope reads as a footnote to the intro once stacked */
    .case-study-body {
        grid-template-columns: 1fr;
        gap: var(--space-lg);
    }
}

/* ============================================
   CASE STUDY INTRO, STACKED

   The title, the role line and the copy all sit in one column on a
   phone, so the role ends up sandwiched between two blocks of text with
   nothing separating it. Give it room on both sides.
   ============================================ */
@media (max-width: 860px) {
    .case-study-intro-grid {
        gap: var(--space-2xl);
    }

    .case-study-intro .title-section h1 {
        margin-bottom: var(--space-md);
    }

    .case-study-intro .meta {
        display: block;
        margin-bottom: var(--space-sm);
    }
}

/* ============================================
   MOTION TOGGLE

   Moved here from liquid-hero.css. It used to be a hero control, so
   it lived in the hero stylesheet - but the projects page does not
   load that file, so the button rendered there completely unstyled.
   ============================================ */
.motion-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.85rem;
    border-radius: 999px;
    border: thin solid #fff;
    background: transparent;
    color: #fff;
    font-family: var(--font-body);
    font-size: 0.7rem;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.3s var(--transition-smooth),
        border-color 0.3s var(--transition-smooth);
}

.motion-toggle:hover {
    background: rgba(32, 29, 30, 0.6);
    border-color: var(--color-cream);
}

.motion-toggle__icon {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #fff;
    animation: motionTogglePulse 1.6s ease-in-out infinite;
}

@keyframes motionTogglePulse {
    0%, 100% { opacity: 0.4; transform: scale(0.8); }
    50%      { opacity: 1;   transform: scale(1.2); }
}

.motion-toggle[aria-pressed="true"] .motion-toggle__icon {
    animation: none;
    opacity: 1;
    background: var(--color-orange);
}

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

/* ============================================
   NAV ACTIONS: motion toggle + burger

   The toggle keeps its own look (see .motion-toggle in liquid-hero.css),
   it has just moved out of the hero and into the bar so it is reachable
   from the projects page too.
   ============================================ */
.navbar .nav-actions {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

/* The bar's height is set by its tallest item, so this must stay well
   under it. nowrap matters: "Reduce Motion" wrapping to two lines is
   what makes the pill taller than the bar and pushes the nav down. */
.navbar .motion-toggle {
    padding: 0.3rem 0.7rem;
    font-size: 0.62rem;
    line-height: 1;
    white-space: nowrap;
    flex: none;
}

.navbar .motion-toggle__icon {
    width: 6px;
    height: 6px;
}

@media (max-width: 640px) {
    /* keep the words: a bare dot gives no clue what it does */
    .navbar .motion-toggle {
        padding: 0.28rem 0.55rem;
        font-size: 0.55rem;
        gap: 0.35rem;
    }
}

/* ============================================
   REDUCED MOTION: the work grids

   .liquid-hero-reduced is set by the toggle and persists across pages.
   The grow-and-squish this used to cancel is gone for everyone now, so
   what is left is the hover transitions themselves.
   ============================================ */
.liquid-hero-reduced .work-card,
.liquid-hero-reduced .work-card__frame,
.liquid-hero-reduced .work-card__img {
    transition: none;
}

/* the second still still swaps in, it just does not fade */
.liquid-hero-reduced .work-card__img--hover {
    transition: none;
}

/* parallax offsets are written as --py by initParallax; neutralise them */
.liquid-hero-reduced .work-card {
    --py: 0px;
    --p: 0;
}

@media (max-width: 560px) {
    /* no room for two columns inside a column: name over description */
    .forge__col {
        grid-template-columns: 1fr;
        gap: var(--space-xs);
    }
}

/* ============================================
   HIDE THE BAR ON THE WAY DOWN

   Nothing about the bar changes but this: it reuses the transform
   transition already declared on .navbar, so the backdrop, the tint,
   the compact outro state and everything else behave as before.
   ============================================ */
/* initNavHide() in main.js adds .is-hidden while scrolling down */
.navbar.is-hidden {
    transform: translateY(-100%);
}

/* The takeover menu is anchored to the bar and its close button lives
   in it, so sliding away mid-menu would strand the visitor. */
.menu-open .navbar.is-hidden {
    transform: none;
}

/* --------------------------------------------------------------------------
   404
   Borrows the page-header type and the standard button. Only the muted
   code above the heading and the stacking are new here.
   -------------------------------------------------------------------------- */
/* a full screen; the content sits in the middle of it, clear of the
   fixed navbar at the top */
.notfound {
    box-sizing: border-box;
    min-height: 100svh;
    display: flex;
    align-items: center;
    padding-block: 6rem;
}

.notfound__code {
    margin: 0 0 var(--space-sm);
    font-family: var(--font-marker);
    font-size: var(--fs-2);
    line-height: 1;
}

.notfound__body {
    max-width: 46ch;
    margin: var(--space-md) 0 var(--space-lg);
    font-weight: 300;
}

.notfound__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md);
}

.notfound__home {
    text-decoration: underline;
    text-underline-offset: 0.25em;
}

/* ==================================================================
   "EXPLORE MORE WORK" - a row of project cards, from Sanity
   ==================================================================
   Used at the foot of the about page and every case study. The cards
   themselves are the normal .work-card; these rules only lay them out
   in an even row and size the type down. Unscoped on purpose, so any
   page can drop the markup in:

     <section class="abm-section abm-more">
       <h2 class="abm-more__title">Explore more work</h2>
       <div class="work-scatter abm-cards" data-sanity-cards
            data-random data-limit="3"></div>
     </section>
   ================================================================== */
/* Space above as well as below, so on a case study it does not sit hard
   against the last picture. (The about page overrides this with its own
   .abm-section rhythm.) */
.abm-more {
    padding-block: var(--abm-space, clamp(5rem, 10vw, 10rem));
}

.abm-more__title {
    margin: 0 0 var(--space-2xl);
    text-align: center;
    font-family: var(--font-marker);   /* Plantin */
    font-weight: 400;
    font-size: var(--fs-3);
    text-transform: none;              /* beat the global h1-h4 rule */
}

/* the cards arrive in rows of two; dissolve the rows into one grid */
.abm-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: var(--abm-gap, clamp(1rem, 2.2vw, 2.25rem));
    row-gap: var(--space-xl);
    padding-inline: var(--page-inset);
}

.abm-cards .work-row { display: contents; }

/* the scatter grid sizes cards by nth-child; this is one even row */
.abm-cards .work-card {
    width: auto !important;
    margin: 0 !important;
    transform: none;
}

.abm-cards .work-card__frame {
    height: auto;
    aspect-ratio: 4 / 3;
}

.abm-cards .work-card__title { font-size: var(--fs-4); }
.abm-cards .work-card__desc { font-size: var(--fs-6); }

.abm-cards .tag {
    color: var(--color-orange);
    border-color: var(--color-orange);
}

@media (max-width: 1000px) {
    .abm-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
    .abm-cards { grid-template-columns: 1fr; }
}

/* ==================================================================
   CASE STUDY MEASURE  (after hoodzpahdesign.com)
   ==================================================================
   The pictures keep the full page width. The words step in from the
   left and run a narrow measure, so the copy sits nearer the middle of
   the page than the images do, and the page reads as writing laid over
   a wider gallery rather than one column of equal-width blocks.

   --cs-step    how far the text moves in past the images' edge
   --cs-measure how wide the text block itself is allowed to run

   Last in the file so it wins over the earlier full-bleed rules.
   ================================================================== */
.case-study-intro,
.cs-band {
    --cs-step: clamp(0rem, 9vw, 11rem);
    --cs-measure: 64rem;
}

/* intro (title, copy, scope) and the text-only write-up bands */
.case-study-intro .work-container,
.cs-band .container {
    max-width: none;
    padding-inline: calc(var(--page-inset) + var(--cs-step)) var(--page-inset);
}

.case-study-intro-grid,
.cs-band .container .case-study-solution {
    max-width: var(--cs-measure);
}

/* A band with an image beside the copy: the picture keeps its own
   width, so only the words step in. */
.sm-section.cs-band .content-img {
    padding-left: var(--cs-step);
}

/* Stacked: the step-in would eat the measure on a narrow screen, so it
   collapses and everything shares the page gutter again. */
@media (max-width: 860px) {
    .case-study-intro,
    .cs-band {
        --cs-step: 0rem;
    }

    .sm-section.cs-band .content-img {
        padding-left: 0;
    }
}

/* ── Case study write-up bands: top down, centred on the page ───────
   Down the page, each write-up puts its heading above the copy instead
   of beside it, and the whole block sits in the middle of the page.
   The words stay left-aligned inside it - a centred line of prose is
   hard to follow. The pictures still run the full width, so the text
   reads as a centred column over a wider gallery.
   The intro at the top keeps its own layout. Last in the file, so it
   wins over the step-in rules above. */
.cs-band .container {
    /* symmetric again: the block below centres itself */
    padding-inline: var(--page-inset);
}

.cs-band .container .case-study-solution {
    grid-template-columns: 1fr;
    /* the gap is the whole space between heading and copy: the cells
       drop their own 2rem margin below, which was stacking with it */
    gap: var(--space-sm);
    /* the same column the intro uses, so every block lines up */
    max-width: var(--cs-measure);
    margin-inline: auto;
    /* items fill the column and their text stays left */
    place-items: start stretch;
    text-align: left;
}

.cs-band .container .case-study-solution .content {
    margin: 0;
}

/* the heading's own bottom margin would sit on top of the gap too */
.cs-band .container .case-study-solution h2 {
    margin-bottom: 0;
}

/* The category labels under a case study title, where the orange role
   line used to be. Same outlined pill as the project cards, in the
   brand orange so the accent under the title is kept. */
.case-study-intro .cs-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: var(--space-sm);
}

.case-study-intro .cs-tags .tag {
    color: var(--color-orange);
    border-color: var(--color-orange);
}

/* Intro centred to match the write-up bands below: the title, the copy
   and the scope list run as one column down the middle of the page at
   the same width, text still left-aligned inside it. Replaces the
   step-in, which put the intro left while everything below it sat
   centred. */
.case-study-intro .work-container {
    padding-inline: var(--page-inset);
}

.case-study-intro-grid {
    max-width: var(--cs-measure);
    margin-inline: auto;
    gap: var(--space-xl);
}

/* the write-up and the scope list side by side inside that column */
.case-study-body {
    grid-template-columns: 2fr 1fr;
    gap: var(--space-2xl);
}

/* The title and its labels centre over the column. The write-up and the
   scope list below stay left-aligned, so the prose is still easy to
   follow. */
.case-study-intro .title-section {
    text-align: center;
}

.case-study-intro .cs-tags {
    justify-content: center;
}

/* Orange swipe reveal on project cards, the same move the homepage ones
   make: each frame starts under an orange block that slides off to the
   right as the card comes into view.

   Used by any grid carrying data-reveal (the projects page and the
   "Explore more work" rows). Only armed once js/sanity-cards.js adds
   .cards-reveal to <html>, so the pictures are never stuck behind an
   orange block if the script does not run or the visitor has reduced
   motion on. --card-reveal-time is the swipe speed, per page. */
.cards-reveal .work-card__frame::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;   /* above the image, the hover image and the video */
    background: var(--color-orange);
    pointer-events: none;
    /* anchored right, so shrinking it uncovers the image from the left */
    transform-origin: 100% 50%;
    transform: scaleX(1);
    transition: transform var(--card-reveal-time, 0.45s)
        cubic-bezier(0.77, 0, 0.175, 1);
}

.cards-reveal .work-card__frame.is-revealed::after {
    transform: scaleX(0);
}

/* Stacked on phones: the two-column rule above is later in the file than
   the old 860px media query, so the stack has to be restated here or the
   scope list stays wedged in a third of a phone screen. */
@media (max-width: 860px) {
    .case-study-body {
        grid-template-columns: 1fr;
        gap: var(--space-lg);
    }
}

/* ======================================================================
   css/liquid-hero.css
   ====================================================================== */
/* ============================================
   LIQUID HERO - homepage gradient hero
   ============================================ */

/* The gradient canvas is fixed behind the whole page (see
   .liquid-hero__bg). It fades out to the page ground as you scroll
   toward the .hero-divider, so everything below reads as the light
   page. This file loads after styles.css on the homepage, so it has to
   name the same token rather than a raw colour - otherwise it quietly
   overrides the ground for this one page. */
body {
    background-color: var(--page-bg);
}

/* (The page used to fade to brand orange on reaching the footer. The
   footer paints its own solid orange now, so the ground stays black.) */


/* Past the hero the gradient keeps animating but drops out of focus, so
   it reads as ambient colour behind the work instead of competing with
   it. The opacity drop is what keeps the page ink readable over it -
   blur alone would still leave a dark ground under dark text. */
@media (prefers-reduced-motion: reduce) {
    .liquid-hero__bg { transition: none; }
}


.liquid-hero {
    position: relative;
    width: 100%;
    min-height: 100svh;
    display: flex;
    align-items: center;
    justify-content: center;
    /* clip the horizontal spill of the oversized 3D logo canvas (it's
       230% wide) so it never widens the page, but leave the vertical
       axis visible so the logo can still drift up/down on scroll */
    overflow-x: clip;
    background: transparent;
}

/* Delayed / "pinned" scroll before the content below: the gradient
   shrinks to the square during this hold (js/hero-shrink.js).
   Armed on every device with motion on (phones too, so the scroll is the
   same length as desktop) when <html> carries .has-hero-pin.
   svh, not vh: the phone address bar showing/hiding would otherwise
   change these heights mid-scroll. */
.hero-pin {
    position: relative;
}

.has-hero-pin .hero-pin {
    height: 200vh;  /* 1 extra screen of hold; raise/lower to taste */
    height: 200svh;
}

.has-hero-pin .hero-pin .liquid-hero {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    min-height: 0;
}

/* WebGL gradient canvas - fixed to the viewport and pinned behind
   every section. The page background shows it from the top down until
   an opaque section (the .hero-divider onward) scrolls over it. */
.liquid-hero__bg {
    /* 0 = sharp hero, 1 = fully out of focus. initHeroBgFade() writes this
       inline per scroll frame on the homepage. */
    --amb: 0;

    position: fixed;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    z-index: -1;

    /* All three read --amb, so they move together with the scroll. No
       transition on purpose: the value already IS the scroll position, and
       easing it would only make the blur lag behind the page.

       The scale is there because a blur samples past the element's edges,
       so an unscaled canvas shows a soft transparent band down all four
       sides. Growing it with the blur keeps those edges off-screen. */
    opacity: calc(1 - var(--amb) * 0.7);
    filter: blur(calc(var(--amb) * 90px)) saturate(calc(1 + var(--amb) * 0.5));
    transform: scale(calc(1 + var(--amb) * 0.5));
}

/* Pages with no hero start fully out of focus. The homepage writes an
   inline --amb over the top of this. */
.liquid-hero__bg.is-ambient { --amb: 1; }

/* Centered brand mark.
   The mark is a touch heavier on its right side, so a small left shift
   optically centres it. (Value is % of the viewport - the old -11% was
   left over from when the hero had a headline block beside the logo and
   is far too much now.) */
.liquid-hero__content {
    position: relative;
    z-index: 2;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-lg);
    /* translateX = optical centring. No vertical drop now that the
       tagline is gone: the mark sits in the middle of the screen. */
    transform: translate(-3%, 0);
    /* Let the cursor drive the gradient through the mark. */
    pointer-events: none;
}

/* Tagline on the background plane, behind the logo. Driven by
   initHeroTagline() in main.js for the parallax + fade on scroll. */
.liquid-hero__tagline {
    position: absolute;
    top: 16%;
    left: 0;
    right: 0;
    margin: 0 auto;
    z-index: 1; /* above the gradient (0), below the logo (2) */
    /* vw-based so the block keeps the same proportions / wrap across
       desktop sizes; phones get a bump below.

       The width is derived from the font size rather than set on its own,
       so the measure stays a fixed number of ems however wide the screen
       is - which is what decides where the lines break. --tag-measure is
       that number: raise it if "Forging a Path Forward" wraps, lower it
       to pull the lines closer together. */
    --tag-font: 3.6vw;
    --tag-measure: 10.8;
    width: calc(var(--tag-font) * var(--tag-measure));
    text-align: center;
    text-wrap: balance;
    font-family: var(--font-marker);
    font-weight: 400;
    font-size: var(--tag-font);
    line-height: 1.08;
    letter-spacing: -0.015em;
    text-transform: none; /* override the global h1-h4 uppercase */
    color: var(--color-cream);
    pointer-events: none; /* never intercept the cursor -> gradient/tilt effects */
    will-change: transform, opacity;
}

/* "Brand Systems" and "Digital Designs" are no longer picked out: no
   italic, no underline, no hover. The spans are left in the markup as a
   hook in case they get a treatment again, and they deliberately carry
   no pointer-events, so the cursor passes straight through to the liquid
   gradient behind the tagline. */

/* Heavier tail phrase. Plantin has a real bold, so this is the true
   700 weight. */
.liquid-hero__tagline .tagline-strong {
    /* its own line, and never split across two */
    display: block;
    white-space: nowrap;
    /* the tagline is pointer-events:none so the cursor reaches the
       gradient; this phrase opts back in so its gif pop can fire */
    pointer-events: auto;
}

/* The tagline sits at z-index 1, below the logo at 2, so its gif would
   pop out behind the mark. Lift the whole layer only while it's hovered. */
.liquid-hero__tagline:has(.gif-pop:hover) {
    z-index: 5;
}

/* Band between the hero and the work section. Transparent - it sits on
   the page-wide gradient background, which is still mostly liquid here. */
.hero-divider {
    /* one full screen tall, with the phrase centred in it. 100svh stops
       the mobile address bar from changing the height */
    height: 100vh;
    height: 100svh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-inline: clamp(6rem, 24vw, 19.5rem);
    /* clip only sideways: the phrase is pinned mid-screen and must not
       be cut off by the section's bottom edge as it scrolls away */
    overflow-x: clip;
    text-align: center;
}

/* the forced break only applies on phones, where the phrase is two lines */
.hero-divider__br { display: none; }

.hero-divider__text {
    margin: 0;
    white-space: nowrap;
    font-family: var(--font-marker); /* Plantin */
    /* Plantin has real bold and bold italic, so any weight here is the
       true face, not a browser-made one */
    font-style: italic;
    /* tier 1 floor (~47px mobile / 120px desktop) but keeps growing
       on ultra-wide screens so it still spans the band */
    font-size: max(var(--fs-3), 4vw);
    line-height: 1;
    letter-spacing: -0.01em;
    text-transform: none;
    color: var(--color-orange); /* off-white #FAFAFF, the light text colour */
    /* sits behind the spinning mark (z-index 2) */
    position: relative;
    z-index: 1;
    /* --work-fade (0 -> 1, written by initHeroBgFade()) fades it out as the
       projects come in, and it recedes 6% as it goes - the same amount the
       stacked cards shrink by, so it reads as the first card of the same
       deck. No blur: the phrase carries mix-blend-mode, and a filter on a
       blended layer is re-rendered and re-blended every frame. Both
       properties here are compositor-only. */
    opacity: calc(1 - var(--work-fade, 0));
    scale: calc(1 - var(--work-fade, 0) * 0.06);
}

/* Holds the three.js <canvas> (the extruded 3D logo) and the flat
   <img>, which shows until the 3D build is ready and as the
   no-WebGL fallback. */
.liquid-hero__mark-tilt {
    position: relative;
    /* nudges the (asymmetric) mark right so it reads centred. Moves the
       flat <img> and the 3D canvas together; js/logo-3d.js lines the 3D
       model up with the <img> itself. */
    translate: 6.4% 0;
    /* blurs with the divider phrase as the projects come in, then fades */
    filter: blur(calc(var(--work-p, 0) * 24px));
    opacity: calc(1 - var(--work-fade, 0));
    width: clamp(300px, 56vw, 760px);
    aspect-ratio: 209 / 140;
}

/* The canvas is drawn larger than the logo box so the model never
   clips against the canvas edge as it tilts/spins. Kept in sync with
   CANVAS_OVERSCAN in js/logo-3d.js. */
.liquid-hero__mark-tilt canvas {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 230% !important;
    height: 230% !important;
    transform: translate(-50%, -50%);
    display: block;
    opacity: 0;
    transition: opacity 0.6s ease;
    /* the oversized canvas overlaps siblings once the hero drops its
       overflow clip - cursor logic lives on window, so it needs no hits */
    pointer-events: none;
}

.liquid-hero__mark {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    transition: opacity 0.6s ease;
    /* Full size at the top, like the 3D logo; js/logo-3d.js shrinks it
       with the gradient square (heroScale). */
    transform: scale(1);
}

.liquid-hero__mark-tilt.is-3d canvas { opacity: 1; }
.liquid-hero__mark-tilt.is-3d .liquid-hero__mark { opacity: 0; }

/* ------------------------------------------------------------------
   Tablet + phone. The desktop's absolute layout doesn't hold on these
   aspect ratios, so rebuild the tagline-over-logo stack as a centred
   column. These blocks are LAST in the file on purpose - they must
   win over the unscoped .liquid-hero__mark* rules above.
   Note: the logo SIZE bump is phones-only (<=640); tablet keeps the
   desktop mark size.
   ------------------------------------------------------------------ */
@media (max-width: 1024px) {
    .liquid-hero {
        flex-direction: column;
        justify-content: center;
        gap: 0;
        padding: 13vh var(--space-md);
    }

    .liquid-hero__tagline {
        position: static;
        margin: 0 0 -2vh;
        width: 72vw;
        font-size: max(5.2vw, 1.9rem);
    }

    .liquid-hero__content {
        transform: translate(-3%, 0);
        padding: var(--space-sm);
    }
}

/* Phones: let the mark run edge to edge (and past, clipped) so it
   dominates the hero. Keep in sync with logo-3d.js SMALL_SCREEN. */
@media (max-width: 640px) {
    /* the wider phone mark needs less of the desktop left nudge to read as
       centred; this is the value to tune */
    .liquid-hero__content {
        transform: translate(-1%, 0);
    }

    /* on phones the opening line stays in the centred stack above the mark,
       so the two start together in the middle of the screen */
    .liquid-hero__tagline {
        width: 88vw;
        font-size: max(9vw, 2.1rem);
    }

    /* the phrase is wider than the phone measure, so let it wrap again */
    .liquid-hero__tagline .tagline-strong {
        display: inline;
        white-space: normal;
    }

    .liquid-hero__mark-tilt {
        width: 130vw;
        /* less than desktop: the phone mark is much wider. Tuned so the
           mark's body sits on the screen centre (was 4.6%, which left it
           ~2.3% of the screen right of centre) */
        translate: 2.35% 0;
    }

    .liquid-hero__mark {
        transform: scale(1);   /* full size at the top, as on desktop */
    }

    /* Same full-screen band as desktop (was 40svh + 25svh margin, which
       made the phone scroll much shorter). The phrase itself is fixed
       mid-screen now, so this is just scroll space. */
    .hero-divider {
        /* the side padding meant for desktop left ~200px for the phrase;
           give it the page gutter instead */
        padding-inline: var(--space-md);
    }

    /* bigger on phones, set on two lines ("For Dreamers" / "& Daredevils"),
       and behind the mark (z-index 1), so the logo ends up above it */
    .hero-divider__text {
        text-align: center;
        font-size: var(--fs-1);
        white-space: normal;
        z-index: 1;
    }

    .hero-divider__br { display: inline; }
}

.liquid-hero__footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding: 0 var(--space-lg) var(--space-lg);
    pointer-events: none;
}

.liquid-hero__footer a,
.liquid-hero__footer button {
    pointer-events: auto;
}

/* Reduce-motion toggle */

/* Reduce motion: the divider phrase sits up under the mark instead of
   mid-screen. The mark keeps its usual start position. Placed last so it
   wins over the breakpoint rules above. */
@media (prefers-reduced-motion: reduce) {
    .hero-divider {
        height: auto;
        align-items: flex-start;
        padding-top: 4vh;
        padding-bottom: 12vh;
    }
}

html.liquid-hero-reduced .hero-divider {
    height: auto;
    align-items: flex-start;
    padding-top: 4vh;
    padding-bottom: 12vh;
}

/* ------------------------------------------------------------------
   Intro line, bottom-left of the hero: what I do, in one sentence.
   It is the page's h1. Sits on the same left edge as the nav logo,
   above the gradient and the "For Dreamers" phrase, and scrolls away
   with the hero.
   ------------------------------------------------------------------ */
.liquid-hero__intro {
    position: absolute;
    left: var(--page-inset);
    bottom: var(--page-inset);
    z-index: 3;
    max-width: 17em;
    margin: 0;
    font-family: var(--font-marker);   /* Plantin */
    font-weight: 400;
    font-size: var(--fs-4);
    line-height: 1.1;
    letter-spacing: -0.01em;
    text-transform: none;   /* override the global h1 uppercase */
    text-wrap: balance;
    color: var(--color-cream);
}

/* With the hero pinned the line is fixed to the screen, so it stays at the
   bottom while the hero scrolls away without any script holding it there.
   It used to be absolute inside the hero and have its own scroll cancelled
   by a per-frame translateY (js/hero-shrink.js): scrolling runs on the
   compositor, the script runs a frame behind it, and the line visibly
   jittered against the page on phones. Fixed is placed by the compositor
   with no script involved. Only the slide off to the left is still driven
   by script, and that moves the way scroll does anyway.
   Without the pin (reduced motion) it stays absolute and scrolls away. */
.has-hero-pin .liquid-hero__intro {
    position: fixed;
    will-change: transform;
}

/* the tail phrase keeps its synthetic bold and its hover gif */
.liquid-hero__intro .tagline-strong {
    text-decoration: underline;
    font-style: italic;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .liquid-hero__intro {
        left: var(--space-md);
        right: var(--space-md);
        bottom: var(--space-md);
    }
}

/* ======================================================================
   css/hero-shrink.css
   ====================================================================== */
/* ============================================
   HERO SHRINK - the gradient background
   pulls in from the screen edges into a centred
   square as the hero is held, then the square
   scrolls up and off the screen with the page.

   js/hero-shrink.js writes these on .hero-frame
   every scroll frame:
     --inset-x / --inset-top / --inset-bottom  px cut from each side
     --shrink               0 -> 1 progress
     --lift                 px scrolled up after
   ============================================ */

.hero-frame {
    /* the value to tune: the square's side as a fraction of the screen's
       shorter side (read by the js). 0.45 on a 1440x900 screen = 405px.
       The logo ends at 80% of this (LOGO_IN_SQUARE in js/logo-3d.js) */
    --frame-square: 0.45;
    /* how much the gradient itself shrinks inside the card */
    --frame-scale: 0.12;

    --inset-x: 0;
    --inset-top: 0;
    --inset-bottom: 0;
    --shrink: 0;
    --lift: 0;

    /* takes over the canvas's old fixed, behind-everything slot */
    position: fixed;
    inset: 0;
    z-index: -1;

    /* clip-path, not width/height: the canvas never resizes, so the
       WebGL buffer is never rebuilt mid-scroll */
    clip-path: inset(
        calc(var(--inset-top) * 1px)
        calc(var(--inset-x) * 1px)
        calc(var(--inset-bottom) * 1px)
    );
    transform: translate3d(0, calc(var(--lift) * -1px), 0);
}

/* The canvas now lives inside the frame, so it fills the frame instead
   of being fixed to the viewport itself. */
.hero-frame .liquid-hero__bg {
    position: absolute;
    z-index: auto;
}

/* While the effect is on (js adds .is-active), the card stays sharp and
   shrinks with the frame. The old blur-out behind the work is dropped:
   the square has already scrolled away by then. */
.hero-frame.is-active .liquid-hero__bg {
    opacity: 1;
    filter: none;
    transform: scale(calc(1 - var(--shrink) * var(--frame-scale)));
}

/* (The topographic lines' styles, .topo-bg, moved to css/styles.css -
   every page has them now.) */

/* "For Dreamers & Daredevils." - fixed mid-screen, layered between the
   topo lines (-3) and the gradient (-1). The full-screen gradient hides
   it at first; as the square shrinks it shows around the edges, and once
   the square has left it is there on its own until the projects come in
   (the fade + recede from --work-fade, in the rule further up).

   Its .hero-divider section stays in the page as empty scroll space.

   js/hero-shrink.js sets the font size so the line spans the screen
   width. On phones the <br> splits it into two lines, each sized to
   span the width. */
.hero-divider__text {
    /* difference blend: the phrase inverts whatever it sits over, so it
       cuts through the montage square and reads as orange on the black
       page either side of it */
    mix-blend-mode: difference;

    position: fixed;
    /* centred on the hero's height (100svh), like the square and logo,
       so it stays lined up on phones when the address bar hides */
    top: 50vh;
    top: 50svh;
    left: 50%;
    translate: -50% -50%;
    width: max-content;
    z-index: -2;
    margin: 0;
    white-space: nowrap;
    text-align: center;
    font-size: 12vw;   /* fallback until the script has measured it */
    line-height: 0.95;
    pointer-events: none;
}

/* Homepage project cards: an orange cover over the image frame while it
   stretches in, swiping away left to right once the card settles in the
   middle of the screen. js/hero-shrink.js puts .card-reveal on <html>
   (only with motion on) and .is-revealed on each frame when it settles. */
.card-reveal .page-home .work-card__frame::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;   /* above the image, the hover image and the video */
    background: var(--color-orange);
    pointer-events: none;
    /* anchored right, so shrinking it uncovers the image from the left */
    transform-origin: 100% 50%;
    transform: scaleX(1);
    /* swipe speed (was 0.9s) */
    transition: transform 0.45s cubic-bezier(0.77, 0, 0.175, 1);
}

.card-reveal .page-home .work-card__frame.is-revealed::after {
    transform: scaleX(0);
}

/* Homepage project cards stack: each card holds in the middle of the
   screen while the next one slides up over it (the one underneath blurs,
   dims and shrinks a touch - js/hero-shrink.js, stackCards). Only with
   motion on (.card-reveal); otherwise they scroll as a normal list.

   The cards arrive two to a .work-row; display: contents dissolves the
   rows so all the cards share one parent and stack as one pile. The row
   gap is the same as the grid's, so the spacing does not change. */
.card-reveal .page-home .work-scatter--squish .work-row {
    display: contents;
}

/* Scroll space between stacked cards: how long each one holds, sharp and
   on its own, before the next appears at the bottom and the blur starts.
   (The grid's own gap is ~6rem, which made them blur almost at once.) */
.card-reveal .page-home .work-scatter--squish {
    row-gap: 45vh;
    row-gap: 45svh;
}

.card-reveal .page-home .work-scatter--squish .work-card {
    position: sticky;
    /* --stack-top centres the card on the screen; set per card by the js */
    top: var(--stack-top, 15vh);
}

/* Reduced motion: no shrink. js/hero-shrink.js keeps the gradient full
   screen and lets it scroll away with the hero, so the page below matches
   the full version (black + still topo lines). */

/* ======================================================================
   css/montage.css
   ====================================================================== */
/* ============================================
   MONTAGE - a square of work behind the
   "For Dreamers & Daredevils" phrase.

   Same size and place as the gradient square the
   hero shrinks into, so once that square lifts
   away this one takes over the space.

   Layering, from the back: topo lines and this
   square both sit at -3, and this comes after
   them in the markup, so it paints over the
   lines; the phrase is at -2, above it; the
   gradient frame is at -1, above that.

   js/montage.js swaps the picture every half
   second and writes --montage-in as it fades up.
   ============================================ */

.montage {
    /* keep in sync with --frame-square in css/hero-shrink.css */
    --montage-square: 0.45;

    position: fixed;
    /* centred on the hero's height, exactly like the phrase in front of
       it, so the two line up however tall the screen is */
    top: 50vh;
    top: 50svh;
    left: 50%;
    translate: -50% -50%;
    width: calc(min(100vw, 100svh) * var(--montage-square));
    aspect-ratio: 1;
    z-index: -3;
    overflow: hidden;
    pointer-events: none;

    /* --montage-in fades it up once the gradient square has gone;
       --montage-out takes it away again, but only once the first project
       card has covered it (js/montage.js), so the fade happens out of
       sight. No blur. */
    opacity: calc(var(--montage-in, 0) * (1 - var(--montage-out, 0)));
}

.montage__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Reduced motion: no cycling (js/montage.js leaves a single still), and
   no square at all if the script never runs. */
@media (prefers-reduced-motion: reduce) {
    .montage { opacity: calc(var(--montage-in, 0) * (1 - var(--montage-out, 0))); }
}

/* ======================================================================
   css/cta-grow.css
   ====================================================================== */
/* ============================================
   CTA GROW - the hero's shrink in reverse
   for "Ready to Rock & Roll?".

   The stage is held for 1.4 screens of scroll
   (.cta-pin is 2.4 screens tall, the stage is
   sticky). js/cta-grow.js grows the liquid box
   from a small centred square out to near full
   screen, leaving a margin on every side, brings
   the CTA in on top of it, then rests on the
   finished CTA before the section lets go.

   js writes on .cta-frame: --it / --ix / --ib,
   the px cut from the top, sides and bottom.
   ============================================ */

.cta-pin {
    position: relative;
    height: 240vh;   /* 1.4 screens of hold: grow, then a rest on the finished CTA */
    height: 240svh;
}

.cta-stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    display: grid;
    place-items: center;
    overflow: hidden;
}

.cta-frame {
    --it: 0;
    --ix: 0;
    --ib: 0;
    position: absolute;
    inset: 0;
    clip-path: inset(
        calc(var(--it) * 1px)
        calc(var(--ix) * 1px)
        calc(var(--ib) * 1px)
    );
}

.cta-liquid {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

/* The CTA sits centred on the box, in off-white so it reads on the
   gradient. Its opacity is set by the js as the box fills out. */
.cta-stage .cta-band {
    position: relative;
    z-index: 1;
    align-items: center;
    text-align: center;
    padding: 0 var(--container-padding);
}

.cta-stage .cta-band__link { align-self: center; }

.cta-stage .cta-band__heading,
.cta-stage .cta-band__link {
    color: var(--color-cream);
}

/* Reduced motion: no hold and no grow - the box is simply full size */
@media (prefers-reduced-motion: reduce) {
    /* .ab-hero named too: its own height rule below is the more specific
       of the two, so .cta-pin alone would not beat it here */
    .cta-pin,
    .cta-pin.ab-hero { height: auto; }
    .cta-stage { position: relative; }
}

/* The About opener runs this in reverse and ends on "That's the promise.",
   so it wants less hold than the CTA at the foot of a page: the line had
   half a screen of dead scroll after it landed. */
.cta-pin.ab-hero {
    height: 190vh;
    height: 190svh;
}

html.liquid-hero-reduced .cta-pin { height: auto; }
html.liquid-hero-reduced .cta-stage { position: relative; }

/* The About opener has a second phase after "That's the promise.": the
   promise fades, the box grows to full bleed and the liquid gives way to
   solid blue, then the skills play on it (js/cta-grow.js writes
   --solid, 0 to 1). */
.cta-pin.ab-hero--skills {
    height: 280vh;
    height: 280svh;
}

/* Pinned like the stage and drawn under it, but outside it: the stage is a
   blending group of its own, so only a sibling can blend with the topo lines
   fixed behind the page. screen keeps the blue as it is on the dark ground
   and lets the lines lighten it. js/cta-grow.js writes --ix/--it/--ib (the
   same inset as the liquid frame) and --solid (0 to 1). */
.cta-blue {
    --it: 0;
    --ix: 0;
    --ib: 0;
    position: sticky;
    top: 0;
    /* On the About page this one layer carries the blue from the opener's
       stage all the way down to the foot of About Me (.ab-blue), so it has
       to sit under the page's own text: -2 is below in-flow content and
       still above the fixed topo lines at -3, which is what lets it screen
       with them. A positioned layer at z-index auto would paint over the
       paragraphs it is meant to be the ground for. */
    z-index: -2;
    /* the large viewport, not the small one: on a phone with the address
       bar retracted the screen is taller than 100svh, and a box that tall
       left a black band along the bottom once the blue went full bleed.
       The overshoot hangs below the fold while the bar is showing, where
       it cannot be seen, and js/cta-grow.js takes it back off the bottom
       of the clip so the growing square still lines up with the liquid
       one. Both are 100vh on a desktop, so nothing changes there. */
    height: 100vh;
    height: 100lvh;
    margin-bottom: -100vh;
    margin-bottom: -100lvh;
    mix-blend-mode: screen;
    pointer-events: none;
    clip-path: inset(
        calc(var(--it) * 1px)
        calc(var(--ix) * 1px)
        calc(var(--ib) * 1px)
    );
}

.cta-solid {
    --solid: 0;
    position: absolute;
    inset: 0;
    background-color: var(--color-blue);
    opacity: var(--solid);
}

@media (prefers-reduced-motion: reduce) { .cta-blue { display: none; } }
html.liquid-hero-reduced .cta-blue { display: none; }

.ab-hero__skills {
    grid-area: 1 / 1;
    position: relative;
    z-index: 1;
    align-self: stretch;
    justify-self: stretch;
    display: grid;
    grid-template-columns: 1fr 1fr;   /* the middle of the stage is the line... */
    align-items: center;
    opacity: 0;              /* js/cta-grow.js brings it in */
    pointer-events: none;
}

/* ...the skills start from it, text aligned left */
.ab-v2 .ab-hero__skills-list { grid-column: 2; justify-self: start; text-align: left; }
.ab-v2 .ab-hero__skills .abm-skills__list li { margin-inline: 0; }

/* the homepage montage, now in flow on the left of the stage rather than
   fixed behind the page */
.montage.montage--skills {
    position: absolute;
    top: 50%;
    /* right-aligned in the left half: its right edge sits a clear gutter
       short of the centre line the skills start from */
    left: auto;              /* the homepage rule's left: 50% would win over right */
    right: calc(50% + clamp(2rem, 7vw, 7rem));
    translate: 0 -50%;
    z-index: auto;
    width: min(30vw, 50svh);
    opacity: 1;              /* the layer around it does the fading */
}

/* phones: no room beside the list, so the square sits above it */
@media (max-width: 760px) {
    .ab-hero__skills {
        grid-template-columns: 1fr;
        justify-items: center;
        align-content: center;
        row-gap: var(--space-lg);
    }
    .ab-v2 .ab-hero__skills-list { grid-column: 1; justify-self: center; }
    .montage.montage--skills {
        position: relative;
        top: auto;
        right: auto;
        translate: none;
        width: min(46vw, 30svh);
    }
}

@media (prefers-reduced-motion: reduce) {
    .cta-pin.ab-hero--skills { height: auto; }
    .ab-hero__skills { grid-area: auto; opacity: 1; padding-block: var(--space-4xl); }
}

html.liquid-hero-reduced .cta-pin.ab-hero--skills { height: auto; }
html.liquid-hero-reduced .ab-hero__skills { grid-area: auto; opacity: 1; padding-block: var(--space-4xl); }

/* Projects page: pull the CTA pin up by the empty stage above its
   starting square, so the square opens just under the last card.
   The square starts at 45vmin (START_SQUARE in js/cta-grow.js),
   centred in a 100svh stage, so the empty band above it is
   (100svh - 45vmin) / 2. Motion only: with reduced motion the box
   is full size from the start and would sit on top of the cards. */
@media (prefers-reduced-motion: no-preference) {
    html:not(.liquid-hero-reduced) .page-projects .cta-pin {
        margin-top: calc((45vmin - 100svh) / 2);
    }

    /* The see-through stage now overlaps the bottom of the last card;
       let clicks pass through to it. The link keeps its own, for the
       click and the hover that sets the gradient going. */
    html:not(.liquid-hero-reduced) .page-projects .cta-stage {
        pointer-events: none;
    }

    html:not(.liquid-hero-reduced) .page-projects .cta-band__link {
        pointer-events: auto;
    }
}

/* ======================================================================
   css/about-v2.css
   ====================================================================== */
/* ============================================
   ABOUT v2 - laid out after
   mondaynights.co/about. Scoped to body.ab-v2.

   A quiet four-column grid, small type and a lot
   of air: label | content | label | content.
   The opening liquid box stays as it is.
   ============================================ */

.ab-v2 {
    --abm-gap: clamp(1rem, 2.2vw, 2.25rem);       /* between columns */
    --abm-space: clamp(5rem, 10vw, 10rem);        /* between sections */
}

/* ── opening statement ─────────────────────────
   One display line above the liquid box, set like the other page
   titles. The top padding clears the fixed navbar. */
.ab-v2 .abm-opener {
    padding: calc(var(--space-4xl) + var(--space-lg)) var(--page-inset) clamp(3rem, 7vw, 6rem);
}

.ab-v2 .abm-opener__line {
    margin: 0;
    font-family: var(--font-marker);
    font-style: italic;
    font-weight: 400;
    font-size: var(--fs-1);
    line-height: 1;
    letter-spacing: -0.04em;
    text-wrap: balance;
    color: var(--page-ink);
}

/* ── the shared grid ───────────────────────── */
.ab-v2 .abm-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: var(--abm-gap);
    row-gap: var(--space-xl);
    padding-inline: var(--page-inset);
    align-items: start;
}

.ab-v2 .abm-band,
.ab-v2 .abm-section {
    padding-block: var(--abm-space) 0;
}

/* Small section labels, the reference's quiet headings */
.ab-v2 .abm-label {
    margin: 0;
    font-family: var(--font-marker);   /* Plantin */
    font-weight: 400;
    font-size: var(--fs-4);
    line-height: 1.15;
    letter-spacing: -0.01em;
    text-transform: none;              /* beat the global h1-h4 rule */
    color: var(--page-ink);
}

/* ── Skills band ───────────────────────────── */
/* A full-bleed indigo band, so the skills read as their own block
   rather than more text on the black page. The page ink (cream) sits
   on indigo at about 11:1, so it stays easily readable. */
.ab-v2 .abm-band {
    background-color: var(--color-blue);   /* indigo #2E2B8A */
    margin-top: var(--abm-space);
    padding-block: var(--abm-space);
}
/* (the skills now live on the opener's stage, see .ab-hero__skills) */

.ab-v2 .abm-list {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--fs-5);
    line-height: 1.5;
}

/* Skills: label centred, the skills stacked under it. Each slides in
   from the left in turn once the list scrolls into view (about.html arms
   it with .is-armed, then .is-in). Without JS or with reduced motion the
   classes never land, so the list just shows. */
.ab-v2 .abm-skills {
    padding-inline: var(--page-inset);
    text-align: center;
}

.ab-v2 .abm-skills__list {
    margin: var(--space-lg) auto 0;
    font-family: var(--font-marker);   /* Plantin */
    font-size: var(--fs-2);
    font-style: italic;
    line-height: 1.2;
    letter-spacing: -0.02em;
}

.ab-v2 .abm-skills__list { counter-reset: skill; }

.ab-v2 .abm-skills__list li { padding-block: 0.1em; }

/* [1] [2] ... numbering before each skill */
.ab-v2 .abm-skills__list li::before {
    counter-increment: skill;
    content: "[" counter(skill) "]";
    margin-right: 0.6em;
    font-size: 0.5em;
    vertical-align: 0.5em;
    letter-spacing: 0;
    color: var(--color-orange);
}

/* an orange bar sweeps across each skill, then wipes off to the right,
   uncovering the text behind it */
.ab-v2 .abm-skills__list li {
    position: relative;
    width: fit-content;      /* shrink to the word, so the bar is as long as it */
    margin-inline: auto;
}

/* the bar covers exactly the word (number included) */
.ab-v2 .abm-skills__list li::after {
    content: "";
    position: absolute;
    inset: 0.12em -0.3em;    /* italic glyphs overhang the box, so pad past it */
    background: var(--color-orange);
    clip-path: inset(0 100% 0 0);
    pointer-events: none;
}

/* hide the text with colour, not opacity: the bar is a child of the li,
   so fading the li would hide the bar too */
.ab-v2 .abm-skills__list.is-armed li,
.ab-v2 .abm-skills__list.is-armed li::before { color: transparent; }

.ab-v2 .abm-skills__list.is-in li {
    animation: skill-text 0.9s steps(1, end) forwards;
    animation-delay: calc(var(--i, 0) * 0.2s);
}

.ab-v2 .abm-skills__list.is-in li::before {
    animation: skill-num 0.9s steps(1, end) forwards;
    animation-delay: calc(var(--i, 0) * 0.2s);
}

.ab-v2 .abm-skills__list.is-in li::after {
    animation: skill-bar 0.9s forwards;
    animation-delay: calc(var(--i, 0) * 0.2s);
}

/* the text only appears once the bar fully covers it, at 50% */
@keyframes skill-text {
    0%   { color: transparent; }
    50%  { color: var(--page-ink); }
    100% { color: var(--page-ink); }
}

@keyframes skill-num {
    0%   { color: transparent; }
    50%  { color: var(--color-orange); }
    100% { color: var(--color-orange); }
}

/* grows from the left edge to the right, then its left edge closes in
   to the right until nothing is left; each leg eases in and out */
@keyframes skill-bar {
    0%   { clip-path: inset(0 100% 0 0); animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
    50%  { clip-path: inset(0 0 0 0);    animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
    100% { clip-path: inset(0 0 0 100%); }
}

/* (old multi-column bullet list, no longer used on the page) */
.ab-v2 .abm-list--bullets {
    grid-column: 2 / -1;
    list-style: disc;
    padding-left: 1.1em;
    columns: 3 12rem;
    column-gap: var(--abm-gap);
}

.ab-v2 .abm-list--bullets li {
    break-inside: avoid;
    padding-left: 0.2em;
}

.ab-v2 .abm-list--bullets li::marker { color: var(--color-orange); }

.ab-v2 .abm-list a {
    color: inherit;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
    text-decoration-color: rgba(250, 250, 255, 0.4);
    transition: color 0.2s ease, text-decoration-color 0.2s ease;
}

.ab-v2 .abm-list a:hover {
    color: var(--color-orange);
    text-decoration-color: currentColor;
}

/* ══ STICKERS ══════════════════════════════════
   Cut-outs hung round the edge of a photo, each drifting and turning at
   its own rate as the picture goes past (js/site.js, initPhotoStickers,
   which also places them). The layer is the photo's own box, so the slots
   are shares of it and the whole arrangement scales with the picture. */
.sticker-layer {
    position: absolute;
    inset: 0;
    /* Over everything on the page, under the navbar (1000) and the page
       loader. This only reaches that high because no box between here and
       the page is a stacking context of its own - see .abm-person. */
    z-index: 900;
    pointer-events: none;   /* nothing here is in the way of a click */
}

.sticker {
    position: absolute;
    left: var(--x, 50%);
    top: var(--y, 50%);
    width: var(--w, 24%);
    /* One property, in order: centre it on its slot, carry it along by the
       drift the script writes (--dy), then turn it about its own middle.
       Doing the rest pose with rotate/translate instead would leave the
       drift running along the sticker's own tilted axis. */
    transform:
        translate(-50%, -50%)
        translate3d(var(--dx, 0px), var(--dy, 0px), 0)
        rotate(calc(var(--r, 0deg) + var(--dr, 0deg)));
    will-change: transform;
}

/* No shadow added here: each sticker draws its own ground - the durian
   stands on an ellipse, the shark in a puddle - and a drop-shadow would
   trace round those shapes as well. */
.sticker img {
    display: block;
    width: 100%;
    height: auto;
}

/* Smaller on a phone, and the slots pulled in toward the photo: hung a
   tenth of the width off the edge there, a sticker is most of the way
   into the gutter and the page would clip it. */
@media (max-width: 640px) {
    .sticker {
        width: calc(var(--w, 24%) * 0.8);
        left: calc(50% + (var(--x, 50%) - 50%) * 0.7);
    }
}

/* Stacked, there is no drift: they pop in as the picture arrives (the
   script puts .is-in on the layer) and then sit still. The pop is on the
   <img>, not the sticker itself - that carries the transform that places
   and turns it, and scaling the two together would drag it off its slot
   as it grew. The overshoot in the curve is the pop. */
@media (max-width: 1000px) {
    .sticker img {
        opacity: 0;
        scale: 0.3;
        transition:
            opacity 0.25s ease calc(var(--i, 0) * 0.12s),
            scale 0.5s cubic-bezier(0.2, 1.6, 0.4, 1) calc(var(--i, 0) * 0.12s);
    }

    .sticker-layer.is-in .sticker img {
        opacity: 1;
        scale: 1;
    }
}

/* no pop with motion off: they are simply there */
@media (prefers-reduced-motion: reduce) {
    .sticker img { opacity: 1; scale: 1; transition: none; }
}

html.liquid-hero-reduced .sticker img { opacity: 1; scale: 1; transition: none; }

/* ── the blue run ──────────────────────────────
   How far the opener's blue reaches: .cta-blue is sticky inside this, so
   it stays the full screen from the stage down to the end of About Me and
   lets go here, leaving the Instagram feed on black. There is no second
   blue to match - it is the same layer, same colour, same screen blend
   over the topo lines the whole way.

   No z-index here on purpose: that would make this a stacking context,
   seal the topo lines out of the blue's backdrop and leave it flat. */
.ab-v2 .ab-blue {
    position: relative;
    /* the sections only pad at the top, so the blue would otherwise close
       flush with the last line of the bio */
    padding-bottom: var(--abm-space);
}

/* ── centred statement ─────────────────────── */
.ab-v2 .abm-statement {
    padding: var(--abm-space) var(--page-inset) 0;
    text-align: center;
}

.ab-v2 .abm-statement p {
    margin: 0 auto;
    max-width: 22em;
    font-family: var(--font-marker);
    font-style: italic;
    font-weight: 400;
    font-size: var(--fs-2);
    line-height: 1.05;
    letter-spacing: -0.02em;
    text-wrap: balance;
}

/* The mission line gets the skills' orange bar: it sweeps across, then
   wipes off to the right, uncovering the words behind it. Same keyframes
   (skill-bar / skill-text), a little slower - there is more to cross.
   Armed by a script in about.html; unarmed, the bar sits clipped to
   nothing and the line is simply there. */
.ab-v2 .abm-statement p {
    position: relative;
}

.ab-v2 .abm-statement p::after {
    content: "";
    position: absolute;
    /* As chunky as the skills bars, which get their air from a 1.5
       line-height. This line is set at 1.05, so there is no leading to
       borrow and the glyphs overhang the box - the bar has to reach out
       past it on every side to cover them with the same slab of orange. */
    inset: -0.22em -0.4em;
    background: var(--color-orange);
    clip-path: inset(0 100% 0 0);
    pointer-events: none;
}

/* hidden with colour, not opacity: the bar is the p's own ::after, so
   fading the p would take the bar with it */
.ab-v2 .abm-statement.is-armed p { color: transparent; }

.ab-v2 .abm-statement.is-in p {
    animation: skill-text 1.1s steps(1, end) forwards;
}

.ab-v2 .abm-statement.is-in p::after {
    animation: skill-bar 1.1s forwards;
}

/* ── wide label left, its content on the right ── */
.ab-v2 .abm-label--wide {
    grid-column: 1 / 3;
}

/* plain stack of paragraphs. Left half of the row: no label sits beside
   them any more, and the full four columns would run the lines too long
   to read comfortably. */
.ab-v2 .abm-paras {
    grid-column: 1 / -1;
    justify-self: center;
    width: min(100%, 40rem);
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 1em;
}

.ab-v2 .abm-paras p,
.ab-v2 .abm-bio p {
    margin: 0;
    font-size: var(--fs-5);
    line-height: 1.5;
}

/* Each one slides up into place and fades in, a tenth of a second behind
   the one above it (--i, set by the script in about.html). Change the
   translate to -1.5rem 0 to have them come in from the side instead. */
.ab-v2 .abm-paras.is-armed p {
    opacity: 0;
    translate: 0 1.75rem;
}

.ab-v2 .abm-paras.is-armed.is-in p {
    opacity: 1;
    translate: none;
    transition: opacity 0.7s ease calc(var(--i, 0) * 0.12s),
                translate 0.9s cubic-bezier(.2, .7, .2, 1) calc(var(--i, 0) * 0.12s);
}

/* ── Get to Know Me: photo card | bio ──────── */
.ab-v2 .abm-me .abm-label--wide { grid-row: 1; }

.ab-v2 .abm-person {
    grid-column: 1;
    grid-row: 2;
    margin: 0;
}

.ab-v2 .abm-person img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 5;
    object-fit: cover;
}

.ab-v2 .abm-person figcaption {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    margin-top: var(--space-sm);
    text-align: center;
}

.ab-v2 .abm-person__name {
    font-family: var(--font-marker);
    font-size: var(--fs-5);
}

/* the role in a rounded outline, like the reference */
.ab-v2 .abm-pill {
    display: inline-block;
    padding: 0.35em 0.9em;
    border: 1px solid currentColor;
    border-radius: 999px;
    font-size: var(--fs-6);
    line-height: 1;
}

.ab-v2 .abm-bio {
    grid-column: 3 / 5;
    grid-row: 1 / 3;
    display: flex;
    flex-direction: column;
    gap: 1em;
}

/* ── Explore more work ── */
.ab-v2 .abm-more {
    padding-bottom: var(--abm-space);
}

/* The cards, title and column count come from the shared .abm-more /
   .abm-cards rules (same as the case studies), so both pages match. */

/* ── narrower screens ──────────────────────── */
@media (max-width: 1000px) {
    .ab-v2 .abm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .ab-v2 .abm-label--wide,
    .ab-v2 .abm-paras,
    .ab-v2 .abm-bio { grid-column: 1 / -1; }
    .ab-v2 .abm-me .abm-label--wide { grid-row: auto; }
    .ab-v2 .abm-person { grid-row: auto; }
    .ab-v2 .abm-bio { grid-row: auto; }
}

@media (max-width: 560px) {
    .ab-v2 .abm-grid { grid-template-columns: 1fr; }
    .ab-v2 .abm-person { max-width: 22rem; }
    /* one column: 2 / -1 would invent a second, implicit one */
    .ab-v2 .abm-list--bullets { grid-column: 1 / -1; }
}

/* ── Get to Know Me: two parts, on one pin ───
   ROUGH DRAFT.

   Part one is the picture on its own in the middle of the screen, the
   heading lying across it, the stickers drifting round it. Part two, a
   little further down the same pin, slides the picture over to the left,
   brings the bio in beside it in two columns, and collects the stickers
   down the picture's left edge, clear of the words.

   The script in about.html writes two numbers on the section, both 0 to
   1: --am-in (part one arriving) and --p2 (how far through the swap).
   Under 1000px, and with motion off, there is no pin and no swap - the
   picture sits above the bio and everything shows at once. */
.ab-v2 .abm-me {
    position: relative;
    /* The hold: part one, the swap, then a rest on the finished layout.
       Tall on purpose - the swap wants room to happen in, and at 240 it
       went past too fast to read. */
    height: 300vh;
    height: 300svh;
    padding-block: 0;
}

.ab-v2 .abm-me__stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    display: grid;
    place-items: center;
    /* no clipping here: the stickers hang outside the picture, and the
       page already stops any sideways scroll */
}

.ab-v2 .abm-me__inner {
    width: min(100%, 86rem);
    padding-inline: var(--page-inset);
    display: grid;
    grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
    gap: clamp(2rem, 5vw, 5rem);
    align-items: center;
}

/* Part one holds the picture out in the middle of the screen; part two
   walks it back into its own column on the left. One translate does it,
   so the grid below is simply the finished layout. */
.ab-v2 .abm-me__shot {
    position: relative;
    grid-column: 1;
    justify-self: center;
    width: min(100%, 26rem);
    translate: calc((1 - var(--p2, 0)) * 26vw) 0;
    opacity: var(--am-in, 1);
}

.ab-v2 .abm-me .abm-person {
    /* max-width: none beats the old 22rem cap further up, which left the
       picture sitting against the left of its box on a wide phone; auto
       margins keep it centred whatever width it ends up. */
    width: 100%;
    max-width: none;
    margin-block: 0;
    margin-inline: auto;
}

/* the picture's own box, which the stickers hang off (the figure is a
   caption taller, and the slots would be thrown off by it) */
.ab-v2 .abm-person__frame {
    position: relative;
}

/* the square the picture is seen through: it holds its shape while the
   picture stretches inside it */
.ab-v2 .abm-person__crop {
    overflow: hidden;
}

/* Square, the same shape as the homepage montage. The photo is a 2:3
   portrait, so a square takes a third of it: anchored near the top, which
   keeps his head in frame - centred, it crops from the eyebrows down.

   (It used to smear as it travelled, scaled by a --stretch the script
   wrote. Taken out: it sits still now.) */
.ab-v2 .abm-me .abm-person img {
    aspect-ratio: 1 / 1;
    object-position: 50% 4%;
}

/* the heading lies across the picture in part one, and goes as the swap
   starts - white through difference, so it inverts whatever it crosses */
.ab-v2 .abm-me .abm-label--wide {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    margin: 0;
    width: max-content;
    text-align: center;
    white-space: nowrap;
    font-size: clamp(2.75rem, 15vw, 12rem);
    line-height: 0.9;
    color: #fff;
    mix-blend-mode: difference;
    pointer-events: none;
    opacity: calc(var(--am-in, 1) * (1 - var(--p2, 0)));
    scale: calc(1 - var(--p2, 0) * 0.06);
}

/* part two: the bio arrives beside the picture, in two columns */
.ab-v2 .abm-me .abm-bio {
    grid-column: 2;
    display: block;
    columns: 2;
    column-gap: clamp(1.5rem, 2.5vw, 2.5rem);
    width: auto;
    margin: 0;
    text-align: left;
    opacity: var(--p2, 1);
    translate: calc((1 - var(--p2, 0)) * 3rem) 0;
}

.ab-v2 .abm-me .abm-bio p {
    break-inside: avoid;       /* keep each paragraph in one column */
    margin-bottom: 1em;
    font-size: var(--fs-6);
}

/* No pin on a narrow screen, and none with motion off: the picture sits
   above the bio, one column, everything showing. */
@media (max-width: 1000px) {
    .ab-v2 .abm-me { height: auto; padding-block: var(--abm-space) 0; }
    .ab-v2 .abm-me__stage { position: static; height: auto; }
    .ab-v2 .abm-me__inner {
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        text-align: center;
    }
    .ab-v2 .abm-me__shot { translate: none; opacity: 1; }
    .ab-v2 .abm-me .abm-bio {
        grid-column: 1;
        columns: 1;
        width: min(100%, 40rem);
        opacity: 1;
        translate: none;
        margin-top: var(--space-xl);
        text-align: center;
    }
    /* back up to body size: two columns were what made it small */
    .ab-v2 .abm-me .abm-bio p { font-size: var(--fs-5); }
}

@media (prefers-reduced-motion: reduce) {
    .ab-v2 .abm-me { height: auto; padding-block: var(--abm-space) 0; }
    .ab-v2 .abm-me__stage { position: static; height: auto; }
    .ab-v2 .abm-me__shot { translate: none; opacity: 1; }
    .ab-v2 .abm-me .abm-bio { opacity: 1; translate: none; }
}

html.liquid-hero-reduced .ab-v2 .abm-me { height: auto; padding-block: var(--abm-space) 0; }
html.liquid-hero-reduced .ab-v2 .abm-me__stage { position: static; height: auto; }
html.liquid-hero-reduced .ab-v2 .abm-me__shot { translate: none; opacity: 1; }
html.liquid-hero-reduced .ab-v2 .abm-me .abm-bio { opacity: 1; translate: none; }

/* ── Instagram feed (Behold) ────────────────
   The widget draws its own grid; these only place it on the page and
   set its width. Colours, columns and gaps are set in the Behold
   dashboard, not here. */
.ab-v2 .abm-insta {
    padding-bottom: var(--abm-space);
    text-align: center;
}

.ab-v2 .abm-insta__title {
    margin: 0 0 var(--space-2xl);
    text-align: center;
    font-family: var(--font-marker);   /* Plantin, same as Explore more work */
    font-weight: 400;
    font-size: var(--fs-3);
    text-transform: none;              /* beat the global h1-h4 rule */
}

.ab-v2 .abm-insta__embed {
    padding-inline: var(--page-inset);
}

.ab-v2 .abm-insta__embed > div,
.ab-v2 .abm-insta__embed behold-widget {
    display: block;
    width: 100%;
    margin-inline: auto;
}

/* the handle under the feed */
.ab-v2 .abm-insta__link {
    display: inline-block;
    margin-top: var(--space-lg);
    font-family: var(--font-marker);   /* Plantin */
    font-style: italic;
    font-size: var(--fs-4);
    color: var(--color-orange);
    text-decoration: none;
    transition: opacity 0.2s ease;
}

.ab-v2 .abm-insta__link:hover,
.ab-v2 .abm-insta__link:focus-visible {
    opacity: 0.75;
}

/* ======================================================================
   css/projects-v2.css
   ====================================================================== */
/* ============================================
   PROJECTS v2 - brings the Projects page
   closer to the homepage, with light motion only.
   Everything is scoped to body.pj-v2.

   A. Opener: the statement spans the screen and
      gets most of the first screen to itself.
   B. Grid: full width, two columns with one even
      spacing value (--pj-gap); the right column
      sits half a card lower.
   C. Titles bigger, in an even stack under the
      image (no overlap), labels orange.
   D. One quiet fade-and-rise as each card enters
      (js/projects-v2.js). Nothing else moves.
   ============================================ */

/* ── Page width ────────────────────────────── */
/* Full width, like the rest of the site: the statement and the grid run
   edge to edge inside the usual --page-inset gutter. */

/* ── A. Opener ─────────────────────────────── */
.pj-v2 .page-header {
    /* most of the first screen, the statement sitting a little above the
       middle, so the first row of work peeks in at the bottom */
    min-height: 78vh;
    min-height: 78svh;
    display: flex;
    align-items: center;
    padding-top: 110px;   /* clear of the nav */
    padding-bottom: 0;
}

.pj-v2 .page-header > .container { width: 100%; }

.pj-v2 .pj-statement {
    max-width: none;
    margin: 0;
    /* js/projects-v2.js sizes it so the line spans the screen; this is the
       size before it measures, and the size on phones, where it wraps */
    font-size: var(--fs-1);
    line-height: 0.95;
    letter-spacing: -0.035em;
}

@media (min-width: 761px) {
    .pj-v2 .pj-statement { white-space: nowrap; }
}

/* ── B. Even grid ──────────────────────────── */
/* One spacing value for the whole grid: the gap between the two columns
   and the gap from one row's labels to the next row's images are the
   same --pj-gap, so the spacing reads consistent in both directions. */
.pj-v2 {
    --pj-gap: clamp(1.5rem, 3.5vw, 3.5rem);       /* between the two columns */
    --pj-row-gap: clamp(4rem, 8vw, 9rem);         /* between projects, top to bottom */
}

.pj-v2 .work-scatter--squish {
    --col-gap: var(--pj-gap);   /* the rows read this for their column gap */
    row-gap: var(--pj-row-gap);
}

.pj-v2 .work-scatter--squish .work-row {
    row-gap: var(--pj-row-gap); /* used when a row stacks on phones */
}

/* Every card the same height, so the vertical gap never varies: the
   description always takes exactly two lines (shorter ones keep the
   space, longer ones end in an ellipsis). */
.pj-v2 .work-card__desc {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    min-height: calc(2em * 1.35);   /* two lines at its 1.35 line height */
}

@media (min-width: 761px) {
    /* a set shape, so every image is the same size at any width.
       3:2 landscape (was 5:4, which read too tall) */
    .pj-v2 .work-scatter--squish .work-card__frame {
        height: auto;
        aspect-ratio: 3 / 2;
    }

    /* The right column sits half a card lower. `translate` is a share of
       the card's own height, and it moves the whole column without
       touching the gaps, so the spacing inside each column stays even.
       It also leaves `transform` free for the reveal. */
    .pj-v2 .work-scatter--squish .work-row > .work-card:nth-child(2) {
        translate: 0 50%;
    }

    /* room under the grid for the last right card's overhang: half a
       card = half of (a column's width x 2/3 for the 3:2 image) plus
       roughly half the title/description/labels under it */
    .pj-v2 .work-scatter--squish {
        padding-bottom: calc(
            (100vw - var(--page-inset) * 2 - var(--pj-gap)) / 2 / 3 + 5rem
        );
    }
}

/* ── C. Titles + labels ────────────────────── */
/* Title, description and labels sit in an even stack under the image -
   nothing overlaps it, so every card lines up the same way. */
.pj-v2 .work-card__meta {
    margin-top: var(--space-md);
    padding-top: 0;
    gap: 0.6rem;
}

/* One shared size for every title: fitTitles() in js/sanity-cards.js
   sets --title-size so the longest name fits its card on one line; it is
   capped at --fs-3 (~28-44px) so short names do not balloon. */
.pj-v2 .work-card__title {
    font-size: min(var(--title-size, var(--fs-3)), var(--fs-3));
    white-space: nowrap;
    color: var(--page-ink);
    line-height: 0.92;
    letter-spacing: -0.03em;
    /* trimmed to cap height so the gap to the image above is exactly the
       meta's margin, not that plus the font's built-in leading */
    text-box: trim-both cap alphabetic;
}

.pj-v2 .tag {
    color: var(--color-orange);
    border-color: var(--color-orange);
}

/* ── D. Orange swipe reveal ────────────────── */
/* The cards now use the site's orange swipe (see "Orange swipe reveal"
   in css/styles.css, armed by data-reveal on the grid in projects.html)
   instead of the quiet fade-and-rise this page used to have. Quicker
   here than elsewhere: this page is a long column of cards, and a slow
   swipe on each one adds up. */
.pj-v2 {
    --card-reveal-time: 0.28s;
}
