/* Reset and Base Styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    scroll-behavior: smooth;
    overflow-y: scroll;
}

body {
    font-family: 'Quicksand', sans-serif;
    color: #2D2D2D;
    line-height: 1.6;
    overflow-x: hidden;
}

h1, h2 {
    font-family: 'Lora', serif;
}

h3 {
    font-family: 'Quicksand', sans-serif;
    color: #2D2D2d;
    font-weight: 500;
}

/* Side Navigation */
.side-nav {
    position: fixed;
    top: 2rem;
    left: 2.5rem; /* 40px page margin */
    z-index: 1000;
    display: flex;
    flex-direction: column;
    background: transparent;
    padding: 24px 12px 12px 12px; /* 24px top, 12px right, 12px bottom, 12px left */
    border-radius: 8px;
    transition: all 0.3s ease;
}

.side-nav:hover,
.side-nav.nav-expanded {
    background: transparent;
}

/* Nav color variants */
.side-nav.light {
    color: white;
}

.side-nav.dark {
    color: #056AE0;
}

.side-nav.dark .nav-text {
    color: #2D2D2D;
}

.side-nav.dark .brand-text {
    color: #2D2D2D;
}

.nav-brand {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 32px;
    font-weight: 500;
    font-size: 32px;
    white-space: nowrap;
    overflow: hidden;
    color: inherit;
    text-decoration: none;
    padding: 4px 8px; /* 4px vertical, 8px horizontal */
    border-radius: 6px;
    transition: all 0.3s ease;
}

.nav-brand:hover {
    background: transparent; /* No hover state for brand */
}

.side-nav.dark .nav-brand:hover {
    background: transparent; /* No hover state for brand */
}

.brand-icon {
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.brand-text {
    opacity: 0;
    transition: opacity 0.3s ease;
}

.side-nav:hover .brand-text,
.side-nav.nav-expanded .brand-text {
    opacity: 1;
}

/* Mobile/tablet-only clone of .nav-brand's own icon+text, used for its section-matched
   color switching there (see the ≤1024px override, and script.js) — desktop doesn't need
   it, since it already gets the same smooth switching for free via the whole-nav
   .nav-overlay clone, which still contains its own copy of .nav-brand. */
.brand-overlay {
    display: none;
}

.nav-links {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 24px;
}

.nav-link {
    display: flex;
    align-items: center;
    gap: 0; /* No gap in collapsed state */
    color: inherit;
    text-decoration: none;
    font-size: 24px;
    font-weight: 400;
    padding: 4px 8px; /* 4px vertical, 8px horizontal */
    border-radius: 8px;
    transition: all 0.3s ease;
    white-space: nowrap;
    overflow: visible;
    position: relative;
    width: fit-content; /* Hug content in collapsed state */
}

/* When nav is expanded (hovered, or briefly on page load — see .nav-expanded), add gap;
   hug icon + label (8px left / 16px right) instead of filling the nav container */
.side-nav:hover .nav-link,
.side-nav.nav-expanded .nav-link {
    gap: 12px;
    width: fit-content;
    padding: 4px 16px 4px 8px;
}

.nav-link:hover {
    background: #0B60E0;
}

.side-nav.dark .nav-link:hover {
    background: #F4F4F4;
}

/* Base nav is pinned .dark, but each link's hover pill still needs to read as the "white
   nav on blue" color whenever the clip-path overlay's white layer is the one currently
   visible *at that link's own position* — checked per link (see the nav color-switching
   IIFE in script.js, which toggles this class on each .nav-link every frame) rather than
   once for the whole nav, since the blue/white boundary can cross through the middle of
   the nav itself (e.g. landing between two links) instead of sitting cleanly above or
   below all of them. #sideNav (ID) beats .side-nav.dark .nav-link:hover's class-only
   specificity. */
#sideNav .nav-link.link-over-blue:hover {
    background: #0B60E0;
}

.side-nav:hover .nav-link.active,
.side-nav.nav-expanded .nav-link.active {
    width: fit-content;
}

/* Selection reads via the underline only — no pill fill or inset shadow */
.nav-link.active {
    background: transparent;
}

.side-nav.dark .nav-link.active {
    background: transparent;
}

/* Underline: 2px thick, flush against the nav element, rounded caps */
.nav-link.active::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    margin-top: 0px;
    height: 2px;
    border-radius: 999px;
    background: white; /* light nav (white text) */
}

.side-nav.dark .nav-link.active::after {
    background: #056AE0; /* dark nav (primary blue text) */
}

/* An active link is already communicated by its underline — a hover pill on top of it
   is redundant/conflicting. !important because several context-specific hover rules
   (.dark, .link-over-blue, mobile .is-open variants) all need to be overridden uniformly. */
.nav-link.active:hover {
    background: transparent !important;
}

.nav-link.active .nav-text {
    font-weight: 500;
}

.nav-icon {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    /* Icon sits in a 40x40 container */
    padding: 8px;
    box-sizing: content-box;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

/* Hide all icon variations by default */
.nav-icon svg,
.brand-icon svg {
    position: absolute;
    display: none;
}

/* .brand-icon has no padding, so its content box and padding box are the same size —
   percentage sizing (against a positioned ancestor's padding box, per spec, for an
   absolutely positioned child) safely scales the icon 1:1 with the container, matching it
   across breakpoints (40x40 desktop, 30x30 tablet/mobile) without hardcoding either value
   here. */
.brand-icon svg {
    width: 100%;
    height: 100%;
}

/* .nav-icon, unlike .brand-icon, keeps an 8px padding at this (desktop) breakpoint — sized
   explicitly to its 24x24 content box instead of width:100%, which would resolve against
   the padding box (40x40) and render the icon visibly oversized. Re-declared in px at each
   breakpoint that resizes .nav-icon itself (see .side-nav.is-open .nav-icon further down)
   rather than reused as a percentage, so it never depends on padding staying in sync. */
.nav-icon svg {
    width: 24px;
    height: 24px;
}

/* Show blue icons on dark nav (light backgrounds) by default */
.side-nav.dark .nav-icon .icon-blue,
.side-nav.dark .brand-icon .icon-blue {
    display: block;
}

/* Show white icons on light nav (dark backgrounds) by default */
.side-nav.light .nav-icon .icon-white,
.side-nav.light .brand-icon .icon-white {
    display: block;
}

/* Show blue selected icons when active on dark nav */
.side-nav.dark .nav-link.active .nav-icon .icon-blue {
    display: none;
}

.side-nav.dark .nav-link.active .nav-icon .icon-blue-selected {
    display: block;
}

/* Show white selected icons when active on light nav */
.side-nav.light .nav-link.active .nav-icon .icon-white {
    display: none;
}

.side-nav.light .nav-link.active .nav-icon .icon-white-selected {
    display: block;
}

.nav-text {
    opacity: 0;
    transition: opacity 0.3s ease;
    color: inherit;
    width: 0;
    overflow: hidden;
}

.side-nav:hover .nav-text,
.side-nav.nav-expanded .nav-text {
    opacity: 1;
    width: auto;
}

/* --- Nav color switching via clip-path ---
   Two full copies of the nav sit stacked at the identical fixed position: the real,
   interactive "base" copy (#sideNav, permanently .dark/blue) always renders; a decorative
   "overlay" clone (permanently .light/white, inserted by script.js) sits on top, clipped
   with a live clip-path to only show over the page's blue region. Where the overlay is
   visible it fully repaints over the base copy beneath it, giving a hard, pixel-accurate
   split instead of a class-based swap or an eased blend. Both copies get all their color
   (text, icons, hover pill, active underline) from the ordinary .dark/.light rules above —
   nothing here overrides those; each copy's class is just pinned rather than scroll-driven. */
.nav-overlay {
    pointer-events: none;
    /* Fully visible by default, matching the page's actual top-of-load state (hero
       section, blue background, white nav needed) — safe if the geometry script can't
       run. JS clips this from the bottom once it measures the real split. */
    clip-path: inset(0px 0px 0px 0px);
    /* .side-nav sets `transition: all 0.3s ease` for its hover expand/collapse animation.
       The overlay is a clone, so it inherits that — which was silently easing every
       clip-path update over 300ms, the "delay" noticeable when scrolling quickly. This
       needs to be a hard, instant split. */
    transition: none;
}

.nav-overlay[aria-hidden] * {
    pointer-events: none;
}

/* Base nav's real :hover expands it; mirror that onto the overlay (which can never
   receive a genuine :hover of its own, since pointer-events is off) via :has() rather
   than duplicating the hover state in JS. Same for .nav-expanded, the synthetic
   briefly-open-on-load state toggled in script.js. */
body:has(#sideNav:hover) .nav-overlay .nav-link,
body:has(#sideNav.nav-expanded) .nav-overlay .nav-link {
    gap: 12px;
    width: fit-content;
    padding: 4px 16px 4px 8px;
}

body:has(#sideNav:hover) .nav-overlay .nav-text,
body:has(#sideNav.nav-expanded) .nav-overlay .nav-text {
    opacity: 1;
    width: auto;
}

body:has(#sideNav:hover) .nav-overlay .brand-text,
body:has(#sideNav.nav-expanded) .nav-overlay .brand-text {
    opacity: 1;
}

/* Hidden on desktop — media query overrides these */
.nav-hamburger,
.nav-close,
.nav-divider {
    display: none;
}

/* Scroll Wrapper */
.scroll-wrapper {
    height: 100vh;
    overflow-y: scroll;
}

/* Sections */
.section {
    min-height: 100vh;
    position: relative;
    /* Page margins: 40px left, 80px right */
    padding-left: 2.5rem;  /* 40px */
    padding-right: 5rem;   /* 80px */
}

/* Hero Section */
.hero-section {
    background: #056AE0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    position: relative;
    overflow: hidden;
}

/* Caps the hero's content (image, lines, text) at the 1440px design width so their
   percentage/edge-anchored positions stay in sync with each other on wider screens —
   otherwise .hero-lines (anchored to the right edge) and .hero-headshot (anchored via a
   left percentage) drift apart as the viewport grows past the design width. Beyond
   1440px this just centers the same 1440px layout in extra blue background, matching
   how the section already renders at exactly 1440px.
   Positioned absolute (inset 0), not as a stretched flex item, so it ignores
   .hero-section's own 2rem padding the same way .hero-lines/.hero-headshot (also
   absolute) always have — a flex item is laid out inside the flex container's content
   box (inset by that padding), which was pulling the lines/headshot in from the true
   viewport edges instead of sitting flush against them. */
.hero-inner {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    max-width: 1440px;
    margin: 0 auto;
    display: flex;
    align-items: center;
}

.hero-lines {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 100%;
    width: auto;
    z-index: 1;
    pointer-events: none;
}

/* Hero headshot positioning */
.hero-headshot {
    position: absolute;
    left: 5%; /* 158/1440 = 10.97% */
    bottom: 0;
    width: 50%; /* 870/1440 = 60.42% */
    height: auto;
    z-index: 2; /* Sits above gradient lines (background is z-index 0) */
}

.hero-content {
    position: relative;
    z-index: 3; /* Above the headshot */
    text-align: right; /* Align text to the right */
    margin-left: auto; /* Push to the right side */
    margin-right: 20%; /* Some spacing from the right edge */
    max-width: 600px;
    /* .hero-content is the sole flex item in .hero-inner, with ample free space around it —
       it should never need to shrink. Some browsers (seen in Arc, not Chrome) compute the
       flex auto-minimum-size differently and were shrinking it below its one-line natural
       width anyway, wrapping "Tanner" onto its own line. flex-shrink:0 makes it render at
       its natural size everywhere, regardless of that engine difference. Reset back to
       shrinkable in the mobile media query below, where wrapping onto multiple lines is
       intentional. */
    flex-shrink: 0;
}

.hero-title {
    font-size: 3rem;
    color: white;
    font-weight: 400;
    white-space: nowrap; /* single-line headline by design; reset in the mobile media query */
}

/* Hero title underline: matches the .section-title::after pattern (#F5A623), scoped to
   just "Tanner" and drawn in left-to-right after the hero-lines SVG animation finishes
   (last line: 0.4s delay + 1.4s duration = 1.8s, plus a short 0.05s buffer). Duration and
   easing match the lines' actual draw speed rather than reusing their duration: line-5's
   path is ~1773 units drawn over 1.4s (~1266 units/sec); "Tanner" is only ~160px wide, so
   at that same rate it draws in ~0.13s (tuned up slightly to 0.18s to feel less instant).
   Same cubic-bezier as the lines for a consistent motion feel. */
.hero-title-highlight {
    position: relative;
    display: inline-block;
}

.hero-title-highlight::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0px;
    width: 100%;
    height: 2px;
    background: #F5A623;
    border-radius: 999px;
    transform: scaleX(0);
    transform-origin: left;
    animation: heroTitleUnderline 0.18s cubic-bezier(0.4, 0, 0.2, 1) 1.85s forwards;
}

@keyframes heroTitleUnderline {
    to {
        transform: scaleX(1);
    }
}

/* Matches the site's tablet breakpoint (≤1024px, same as the nav collapse and the
   full-width content switch above) rather than a one-off threshold of its own. */
@media (max-width: 1024px) {
    .hero-headshot {
        display: none;
    }

    /* Parked: none of the sizing approaches tried here (scaling, capping, fixed-size
       cropping) behaved as expected on tablet/mobile — hiding the lines for now rather
       than shipping a half-right version. Revisit later. */
    .hero-lines {
        display: none;
    }
}

/* Section Containers */
.section-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 6rem 2rem;
    /* Offset: 40px (page margin) + 197px (expanded nav) + 24px (gap) = 261px = 16.3125rem */
    padding-left: 12.5rem;
}

.section-title {
    font-size: 2rem;
    margin-bottom: 2rem;
    color: #333;
    position: relative;
    padding-bottom: .25rem;
    font-weight: 400;
}

.section-title::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    width: 7.5rem; /* 120px */
    height: 2px;
    background: #F5A623;
}

/* About Section */
.about-section {
    background: white;
    color: white;
    overflow: hidden;
}

.about-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    transform: translateY(-80px);
}

.about-section .section-container {
    position: relative;
    z-index: 1;
}

.about-section .section-title {
    color: white;
}

.about-section .section-title::after {
    background: #F5A623;
}

.about-text {
    font-size: 1rem;
    line-height: 1.8;
    margin-bottom: 3rem;
}

.about-image {
    width: 100%;
    height: auto;
    border-radius: 20px;
    box-shadow: 0 8px 32px rgba(0, 10, 20, 0.25);
    display: block;
}

/* Work Section */
.work-section {
    background: #ffffff;
    min-height: auto; /* Allow it to be taller than viewport */
}

.project-card {
    display: grid;
    grid-template-columns: 40% 60%;
    gap: 3rem;
    margin-bottom: 4rem;
    align-items: center;
}

.project-card:last-child {
    /* .work-section's own section-container padding-bottom already provides
       trailing space — this margin was stacking an extra 64px on top of it,
       making the Work→Contact gap inconsistent with the rest of the page's
       192px section-to-section rhythm. */
    margin-bottom: 0;
}

.project-content {
    display: flex;
    flex-direction: column;
}

.project-title {
    font-family: 'Quicksand', sans-serif;
    font-size: 1.5rem;
    font-weight: 400;
    line-height: 1.3;
    margin-bottom: 0.25rem;
    color: #2D2D2D;
}

.project-client {
    color: #74767E;
    font-size: 16px;
    margin-bottom: 1rem;
}

a {
    text-decoration: none;
}

.project-description {
    color: #555;
    line-height: 1.7;
    font-size: 0.95rem;
    margin-bottom: 1.5rem;
}

.project-image {
    display: block;
    text-decoration: none;
    border-radius: 12px;
    overflow: hidden;
    transition: transform 0.3s ease, opacity 0.3s ease;
}

.project-image:hover {
    transform: translateY(-4px);
    opacity: 0.9;
}

.project-image img {
    width: 100%;
    height: auto;
    aspect-ratio: 12 / 5;
    object-fit: cover;
    display: block;
}

/* Play Section
.play-section {
    background: white;
} */

.section-text {
    max-width: 800px;
    font-size: 1rem;
    line-height: 1.8;
    color: #555;
}

/* Contact Section */
.contact-section {
    background: white;
    display: flex;
    flex-direction: column;
}

/* Same artwork as .about-bg, mirrored — rotate(180deg) is a point reflection, i.e.
   flipped both horizontally and vertically in one transform. preserveAspectRatio
   keeps the crop-not-warp behavior at any width. Sized to match .site-footer's
   min-height and anchored to the section's own bottom edge (which .site-footer sits
   flush against) so it lines up with the footer without collide with the cards
   above, no matter how many rows they wrap into at narrower widths. Positioned
   full-bleed via the 100vw/-50vw breakout so .section's own left/right padding
   (which .site-footer is inset by) doesn't crop it — it needs to span the true
   viewport width, not just the section's content box. */
.contact-bg {
    position: absolute;
    left: 50%;
    bottom: 0;
    width: 100vw;
    height: 320px;
    margin-left: -50vw;
    pointer-events: none;
    transform: rotate(180deg);
}

/* Desktop only: grow the box 40px taller while keeping bottom:0 fixed — since the
   artwork is cropped (not stretched), this reveals more of it at the top edge only,
   nudging the wave crest up 40px with the solid-fill portion seamlessly extending
   into the new space. No gap, no separate fallback color needed. */
@media (min-width: 1025px) {
    .contact-bg {
        height: 360px;
    }
}

.contact-section .section-container {
    position: relative;
    z-index: 1;
}

.contact-text {
    font-size: 1rem;
    line-height: 1.8;
    color: #555;
    margin-bottom: 3rem;
}

/* Contact Cards */
.contact-cards {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
    max-width: 1000px;
}

.contact-card {
    display: flex;
    flex-direction: column;
    text-decoration: none;
    background: white;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.contact-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}

.contact-card-image {
    width: 100%;
    aspect-ratio: 1536 / 488;
    overflow: hidden;
}

.contact-card-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.contact-card-label {
    padding: 1.25rem;
    text-align: center;
    font-family: 'Quicksand', sans-serif;
    font-size: 1rem;
    font-weight: 500;
    color: #2D2D2D;
    border-top: 1px solid #EBEBEB;
}

/* Footer */
.site-footer {
    position: relative;
    z-index: 1;
    margin-top: auto;
    text-align: center;
    overflow: hidden;
}

.contact-section .site-footer {
    min-height: 320px;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: 24px;
}

.contact-section .footer-text {
    position: relative;
    z-index: 1;
    color: white;
}

.footer-text {
    font-size: 0.85rem;
    color: #ABABAB;
}

/* Responsive Design */
@media (max-width: 1024px) {
    /* Full-width content: matches the nav's own ≤1024px breakpoint (see the collapsed
       .side-nav rules further down) — the nav collapses to a small fixed icon and content
       is allowed to scroll behind it at that exact width, so there's no need to reserve
       space for it here — just a small, even gutter on both sides. A narrower threshold
       here would strand the old desktop gutter for a width range where the nav has
       already collapsed and no longer needs it reserved. */
    .section {
        padding-left: 1.5rem;  /* 24px */
        padding-right: 1.5rem; /* 24px */
    }

    .section-container {
        padding-left: 1.5rem;
        padding-right: 1.5rem;
    }

    .hero-content {
        flex-direction: column;
        text-align: center;
        margin-left: auto;
        margin-right: auto;
        flex-shrink: 1; /* allow shrinking again so the title can wrap onto multiple lines */
    }

    .hero-title {
        font-size: 2.5rem;
        white-space: normal; /* wrapping is intentional at this size */
    }

    /* .project-card is `display: grid` (two side-by-side columns) at desktop width —
       flex-direction only affects flex containers, so this was silently doing nothing and
       the grid columns stayed in effect on mobile, squeezing the image and text into two
       narrow side-by-side slivers instead of stacking. */
    .project-card {
        display: flex;
        flex-direction: column;
    }

    .project-image {
        width: 100%;
        order: -1; /* image above the title/description, matching the mobile design */
    }

    .section-container {
        padding-top: 4rem;
        padding-bottom: 4rem;
    }
}

@media (max-width: 768px) {
    /* .section/.section-container's 24px gutter is already set by the ≤1024px block
       above (this range is a subset of it) — only the nav's own distance from the
       viewport edge shrinks further here (see the collapsed .side-nav rule, in the
       ≤1024px block below, which governs nav position at every width down to mobile). */
    .nav-links {
        gap: 0.5rem;
    }

    .hero-title {
        font-size: 2rem;
    }

    .section-title {
        font-size: 1.5rem;
    }

    /* Scale body copy down proportionally on mobile — these otherwise stayed at
       full desktop size and read too large on a phone screen. */
    .about-text {
        font-size: 0.9rem;
    }

    .project-title {
        font-size: 1.25rem;
    }

    .project-client {
        font-size: 14px;
    }

    .project-description {
        font-size: 0.85rem;
    }

    .contact-text {
        font-size: 0.9rem;
    }

    .contact-card-label {
        font-size: 0.9rem;
    }

    .footer-text {
        font-size: 0.8rem;
    }
}
/* ============================================================
   MOBILE & TABLET NAV (≤1024px)
   ============================================================ */

@media (max-width: 1024px) {

    /* The overlay stays enabled below this breakpoint too — it shares every collapsed-state
       rule below with the base nav (both are just `.side-nav`), so it collapses to the same
       hamburger icon, in the same spot, and the clip-path split still gives that icon
       correct white-on-blue / blue-on-white contrast while scrolling. It's hidden separately,
       once the menu is actually open (see `#sideNav.is-open ~ .nav-overlay` below) since at
       that point the base nav becomes its own full-screen panel and the overlay has nothing
       useful to paint. */

    /* --- Collapsed state --- */

    /* Content is now full-width on tablet/mobile (see the .section/.section-container
       rules above) and scrolls behind the fixed nav icon instead of being pushed clear of
       it. A frosted glass pill — translucent background + blur + shadow — stands in for
       real nav styling for now, just enough to read as elevated above whatever passes
       underneath it. Sized to just the hamburger button now — the logo lives in its own
       separate .nav-brand pill (below), no longer stacked inside this one. */
    .side-nav {
        top: var(--nav-edge-offset);
        left: var(--nav-edge-offset);
        width: auto;
        /* 12px padding around a 24x24 icon = a 48x48 circle. */
        padding: 12px;
        background: rgba(255, 255, 255, 0.15) !important;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        border-radius: 999px;
        flex-direction: column;
        align-items: flex-start;
        gap: 12px;
        transition: none;
    }

    /* The drop shadow lives on its own layer, not as this element's own box-shadow —
       box-shadow shares a paint pass with backdrop-filter on the same element, and browsers
       (Safari in particular) can rasterize that combination as a rectangle that pokes out
       past the rounded corners instead of following them. A shadow-only pseudo-element with
       no backdrop-filter of its own doesn't hit that path (confirmed directly against both
       versions in an isolated test) and reliably clips to `border-radius: inherit`, whatever
       shape the real element currently has (999px pill here, 28px card once expanded — see
       .side-nav.is-open::before below). Softened from the original 0.45-alpha version, which
       read as too hard-edged/artificial for a passive resting shadow. */
    .side-nav::before {
        content: '';
        position: absolute;
        inset: 0;
        border-radius: inherit;
        box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
        z-index: -1;
        pointer-events: none;
    }

    /* The overlay clone sits at the identical position/size/radius as the base pill
       beneath it, so the base's own ::before above already paints a correct, rounded
       shadow there — no need for the overlay to paint a second one on top. It shouldn't:
       the overlay carries a live, ever-present `clip-path` (for the scroll-driven
       color split), and that combination with a blurred box-shadow on a `border-radius:
       inherit` pseudo hits the same "renders as a hard rectangle past the rounded
       corners" rasterization bug as backdrop-filter + box-shadow on one element (see the
       comment above) — confirmed directly: identical computed box-shadow/border-radius
       on both copies, only the clip-path-carrying overlay shows the rectangle. Dropping
       its own shadow avoids the bug entirely and costs nothing visually, since the base
       copy's shadow is already sitting there underneath. */
    .nav-overlay::before {
        box-shadow: none;
    }

    .side-nav:hover {
        width: auto;
        background: rgba(255, 255, 255, 0.15) !important;
    }

    /* Frosted glass base tint follows the same light/dark split the clip-path overlay
       already uses for text/icon color — #056AE0 (the site's primary blue) tint over blue
       sections, white tint over white ones — so the pill reads consistently instead of
       nearly disappearing against a background close to its own tint color. */
    .side-nav.light {
        background: rgba(5, 106, 224, 0.15) !important;
    }

    .side-nav.dark {
        background: rgba(255, 255, 255, 0.15) !important;
    }

    /* .nav-brand is independently `position: fixed` at this breakpoint (see the rule
       further down) — a fixed-position descendant escapes its ancestor's clip-path
       entirely, so the overlay's own clip-path can no longer keep its cloned copy hidden
       correctly. Hiding it outright here avoids a permanently-visible, permanently-`.light`
       duplicate sitting on top of the real one. The real .nav-brand gets its own
       independent light/dark toggle in script.js instead (see updateClip()). Desktop is
       unaffected — the clone stays intact there, still doing its normal job. */
    .nav-overlay .nav-brand {
        display: none;
    }

    /* Both nav pills sit this far from their edge — 24px on tablet, 12px on mobile (see the
       ≤768px override below). Read by script.js too, since .nav-brand's own top/left have
       to be computed in JS (see the comment on that rule for why) rather than just written
       here as plain values. */
    :root {
        --nav-edge-offset: 24px;
    }

    /* --- Logo pill (separate from the hamburger/menu pill above) ---
       Always visible, always centered, regardless of whether the menu is open or closed —
       unlike the old design, this no longer lives inside #sideNav's collapse/expand
       machinery at all. script.js (placeNavBrand(), called from positionNavBrand()) moves
       the real element out to a direct child of <body> at this breakpoint, specifically so
       it's clear of #sideNav's own backdrop-filter: `backdrop-filter` (like
       `filter`/`transform`/`perspective`) makes the element carrying it the containing
       block for any `position: fixed` descendant, and a `position: fixed` element nested
       inside another element's backdrop-filter hits a real WebKit bug where its own
       backdrop-filter blurs the wrong thing — confirmed directly, side by side against the
       hamburger pill, which isn't nested and blurs correctly. Moved out to <body>, its
       top/left resolve directly against the real viewport with no offset math needed, which
       is also what fixes the blur: no filtered ancestor left to sit nested inside. Left
       unset here — script.js sets both directly. */
    .nav-brand {
        position: fixed;
        margin-bottom: 0;
        /* Height set explicitly (rather than left to padding + content) so it matches the
           hamburger circle's 48px exactly regardless of the icon/text's own natural size —
           align-items: center (inherited from the base .nav-brand rule) centers the icon
           and wordmark inside it. Padding is horizontal-only as a result. */
        height: 48px;
        padding: 0 16px 0 12px;
        background: rgba(255, 255, 255, 0.15) !important;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        border-radius: 999px;
        z-index: 1000;
        overflow: visible;
        /* Its own light/dark class is toggled directly in script.js (updateClip), based on
           its own screen position — it sits at a different X than the hamburger pill (this
           one's centered, that one's pinned left), so it can't just mirror `nav-over-blue`;
           the boundary the pill crosses could be at a different Y at that X. Needs to be an
           instant swap, not eased, to stay a hard pixel-accurate split like the rest of the
           nav's color-switching. */
        transition: none;
    }

    /* Same shadow-isolation technique as the collapsed pill's `.side-nav::before` — see the
       comment there for why the shadow lives on its own filter-free layer instead of as
       this element's own box-shadow. */
    .nav-brand::before {
        content: '';
        position: absolute;
        inset: 0;
        border-radius: inherit;
        box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
        z-index: -1;
        pointer-events: none;
    }

    /* No .light/.dark background override here, unlike .side-nav's — .nav-brand has no
       overlay clone to carry a second, section-matched glass layer, so it has only one
       shot at reading as frosted glass rather than nearly vanishing. The base rule's white
       tint above (shared with #sideNav's own permanently-`.dark` background) already
       covers both: it's the correct high-contrast choice over blue sections, and still
       reads fine over white ones (the ::before shadow above does the work of separating
       pill from page there). Only text/icon color still needs to flip per section, which
       .brand-overlay below now handles. */

    #navBrand .brand-icon {
        width: 30px;
        height: 30px;
    }

    /* #navBrand stays permanently .dark at this breakpoint now — same permanent pinning as
       #sideNav itself — with .brand-overlay below doing the section-matched color switching
       instead of toggling this class. ID-scoped to safely out-specificity the ancestor-based
       `.side-nav.dark` version of these same rules, since #sideNav is pinned `.dark` and is
       no longer this element's source of truth for color. */
    #navBrand.dark {
        color: #056AE0;
    }

    #navBrand.dark .brand-text {
        color: #2D2D2D;
    }

    #navBrand.dark .brand-icon .icon-blue {
        display: block;
    }

    #navBrand.dark .brand-icon .icon-white {
        display: none;
    }

    /* .brand-overlay is the same clip-path technique as the .nav-overlay clone above,
       scoped down to just .nav-brand's own icon+text (see script.js) instead of the whole
       nav — a permanently light (white) clone of that content, absolutely positioned to
       exactly cover the real content and clipped every scroll frame with the same
       inset-from-boundary math, so the color switch is a continuous pixel-accurate wipe
       through the blue/white boundary instead of an instant flip. #navBrand's own now-
       permanent .dark styling above is what shows through wherever this clip hides it.
       Nested inside the real #navBrand (rather than a standalone clone positioned
       separately) so it automatically travels along whenever placeNavBrand() moves the real
       element between #sideNav and <body> — no separate position-tracking needed. Hidden by
       default (see the base .brand-overlay rule) — this is the only breakpoint that needs
       it, since desktop already gets this same smooth switching for free from the whole-nav
       .nav-overlay clone, which still contains its own copy of .nav-brand there. */
    .brand-overlay {
        display: flex;
        position: absolute;
        inset: 0;
        align-items: center;
        gap: 12px;
        padding: 0 16px 0 12px;
        pointer-events: none;
    }

    /* Placed after the #navBrand.dark rules above so it wins the specificity tie against
       them (both ID + 3 classes; later source wins) — otherwise the .dark rules' own
       styling would win out over this permanently-light clone. */
    #navBrand .brand-overlay .brand-text {
        color: white;
    }

    #navBrand .brand-overlay .brand-icon .icon-white {
        display: block;
    }

    #navBrand .brand-overlay .brand-icon .icon-blue {
        display: none;
    }

    /* Wordmark stays visible at all times now — no more reveal-on-hover/expand. */
    .brand-text {
        opacity: 1;
        width: auto;
    }

    /* Show hamburger — override base display:none. Sized to exactly the icon itself
       (24x24) — .side-nav's own 12px padding is what creates the circle around it now,
       rather than this button having its own separate padding on top of that. */
    .nav-hamburger {
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        cursor: pointer;
        color: inherit;
        padding: 0;
        width: 24px;
        height: 24px;
        border-radius: 6px;
        transition: background 0.2s ease;
    }

    .nav-hamburger:hover {
        background: rgba(255, 255, 255, 0.1);
    }

    .side-nav.dark .nav-hamburger:hover {
        background: rgba(30, 111, 217, 0.1);
    }

    /* Close button stays hidden until menu opens */
    .nav-close {
        display: none;
    }

    /* Divider stays hidden until menu opens */
    .nav-divider {
        display: none;
    }

    /* Hide nav links in collapsed state */
    .nav-links {
        display: none;
    }

    /* Prevent desktop hover-expand text reveal */
    .side-nav:hover .nav-text {
        opacity: 0;
        width: 0;
    }

    /* --- Expanded state (is-open toggled by JS) --- */

    /* JS only ever toggles `.is-open` on the real base nav (#sideNav), never on the overlay
       clone — so once the base expands into its full-screen panel, the overlay would
       otherwise keep sitting on top of it as a small leftover icon in the corner, still
       showing whatever collapsed-state clip it last had. Hide it for the duration. */
    #sideNav.is-open ~ .nav-overlay {
        display: none;
    }

    /* .nav-brand is moved to a direct child of <body> at this breakpoint (see
       placeNavBrand() in script.js — done to get it clear of #sideNav's own
       backdrop-filter, which otherwise breaks its blur in Safari), so it's no longer a DOM
       descendant of #sideNav and a `.side-nav.is-open .nav-brand` selector can no longer
       reach it. `:has()` reads #sideNav's state from their shared parent instead, which
       works regardless of where .nav-brand actually sits. It's still its own
       independently-positioned, always-on-screen pill though — on a narrow viewport the
       expanded card's top-right corner can land right where the logo pill sits, and it
       needs to tuck in BEHIND the card's own frosted background there (obscured by its
       blur, the same way it'd be tucked behind any other opaque layer in front of it)
       rather than floating on top of it. Dropping all the way to 0 (or negative) sinks it
       below ordinary page content too — several sections (.hero-content, the about/contact
       .section-container, .site-footer) set their own small z-index (1–3) to layer over
       decorative background SVGs, and any of those then painted over the brand pill
       whenever the menu was open above them. 999 stays below #sideNav.is-open's 1000 (so
       the expanded card still wins) while comfortably clearing every z-index used
       elsewhere on the page, so the brand pill only ever loses to the expanded menu. */
    body:has(#sideNav.is-open) .nav-brand {
        z-index: 999;
    }


    .side-nav.is-open {
        position: fixed;
        /* The panel opens as its own floating card above the collapsed pill rather than
           morphing in place, so its position no longer needs to match the hamburger's exact
           spot — set independently here instead of inheriting var(--nav-edge-offset). */
        top: 8px;
        left: 8px;
        /* Hug content instead of a fixed card width — the widest link label determines the
           width, so there's no dead space on the right. Capped so it can't overflow a
           narrow viewport. */
        width: fit-content;
        max-width: calc(100vw - 16px);
        height: auto;
        /* Left padding lines up the close icon's box with the nav-link icon column below it
           (see .side-nav.is-open .nav-link / .nav-icon) so the X sits directly above them.
           Right/bottom stay roomier for the links. */
        padding: 20px 24px 24px 20px;
        border-radius: 28px;
        flex-direction: column;
        align-items: flex-start;
        gap: 0;
        z-index: 1000;
        /* Same frosted-glass treatment as the collapsed pill: base tint below follows the
           light/dark section split; falls back to white here in case neither class is
           present for some reason. */
        background: rgba(255, 255, 255, 0.15) !important;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
    }

    /* No is-open-specific override here on purpose — the shared `.side-nav::before` rule
       (see the collapsed pill above) already covers this state too, and its
       `border-radius: inherit` picks up this element's 28px card radius automatically. Kept
       at the exact same shadow values as the brand pill's `.nav-brand::before` rather than a
       bumped-up size for the bigger card, per request. */

    /* Same #056AE0-based split as the collapsed pill. */
    .side-nav.is-open.light {
        background: rgba(5, 106, 224, 0.15) !important;
    }

    .side-nav.is-open.dark {
        background: rgba(255, 255, 255, 0.15) !important;
    }

    /* Swap hamburger for close button */
    .side-nav.is-open .nav-hamburger {
        display: none;
    }

    .side-nav.is-open .nav-close {
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        cursor: pointer;
        color: inherit;
        padding: 0;
        width: 24px;
        height: 24px;
        border-radius: 6px;
        transition: background 0.2s ease;
        /* No divider under the close button anymore — this margin alone gives the links
           breathing room below it. */
        margin-bottom: 24px;
    }

    .side-nav.is-open .nav-close:hover {
        background: rgba(255, 255, 255, 0.1);
    }

    .side-nav.is-open.dark .nav-close:hover {
        background: rgba(30, 111, 217, 0.1);
    }

    /* Nav links: always visible with icon + text */
    .side-nav.is-open .nav-links {
        display: flex;
        flex-direction: column;
        gap: 8px;
        width: 100%;
    }

    .side-nav.is-open .nav-link {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 12px;
        width: auto;
        font-size: 20px;
        font-weight: 400;
        /* No left padding: keeps the icon's left edge flush with the panel's own left
           padding, matching the close button's box above it (see .side-nav.is-open) so the
           icon column lines up under the X. */
        padding: 8px 8px 8px 0;
        border-radius: 8px;
        color: inherit;
    }

    /* Scaled down from the base 24px icon (desktop/collapsed use) to fit the panel's
       tighter proportions; padding zeroed so the icon's own box — not an inner gutter —
       defines its left edge, needed for the X-above-icons alignment described above. */
    .side-nav.is-open .nav-icon {
        width: 20px;
        height: 20px;
        padding: 0;
    }

    /* .nav-icon svg is sized in explicit px (see the base rule), not a percentage, so it
       has to be re-declared here to match .nav-icon's own 20px at this breakpoint. */
    .side-nav.is-open .nav-icon svg {
        width: 20px;
        height: 20px;
    }

    .side-nav.is-open .nav-text {
        opacity: 1;
        width: auto;
        color: inherit;
    }

    /* Hover states in overlay */
    .side-nav.is-open.light .nav-link:hover {
        background: #0B60E0;
    }

    .side-nav.is-open.dark .nav-link:hover {
        background: rgba(30, 111, 217, 0.08);
    }

    /* Active state: underline only, no fill — same treatment as the desktop expanded
       nav (see the shared .nav-link.active / .nav-link.active::after rules up top). */

    /* Text color overrides in white overlay */
    .side-nav.is-open.dark .nav-text {
        color: #2D2D2D;
    }

    /* Contact cards: single centered column from tablet down — 2 columns read as a grid
       rather than a stack at this width, inconsistent with mobile just below it. */
    .contact-cards {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   MOBILE ONLY (≤768px)
   ============================================================ */

@media (max-width: 768px) {
    /* Both nav pills sit 12px from the edge on mobile, vs. 24px on tablet. .side-nav reads
       this directly (var(--nav-edge-offset), set on .side-nav itself); .nav-brand's
       position is computed in JS, which also reads this same variable — see the comment on
       the .nav-brand rule in the ≤1024px block above for why. */
    :root {
        --nav-edge-offset: 12px;
    }

    /* Wordmark reads large at 32px (inherited from the desktop .nav-brand rule) against
       a 30px icon on a small phone screen — sized down specifically for mobile, not
       tablet, where the larger size still reads fine next to the pill's other proportions. */
    .nav-brand {
        font-size: 24px;
    }

    /* Corner radius scaled to 50% of its desktop value, matching images shrinking down
       to the mobile breakpoint. */
    .about-image {
        border-radius: 10px; /* 20px desktop */
    }

    .project-image {
        border-radius: 6px; /* 12px desktop */
    }

    .contact-card {
        border-radius: 6px; /* 12px desktop */
    }
}