:root {
    --bg-color: #050505;
    --text-primary: #ffffff;
    --text-secondary: #adadad;
    --accent-color: #01c50b;
    --font-main: 'Inter', system-ui, -apple-system, sans-serif;
    /* Heading/body/caption fonts are separate variables so each can be swapped
       independently (tested via fonts_test.html / fonts_test_page.html).
       Owner's pick 2026-07-03: Atkinson Hyperlegible for body + captions,
       Inter stays for headings/nav. */
    --font-heading: 'Inter', system-ui, -apple-system, sans-serif;
    --font-body: 'Atkinson Hyperlegible', system-ui, -apple-system, sans-serif;
    --font-caption: 'Atkinson Hyperlegible', system-ui, -apple-system, sans-serif;
    /* Mono for the technical metadata blocks + figure labels */
    --font-mono: 'JetBrains Mono', ui-monospace, Consolas, monospace;
}


* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: var(--font-main);
}

/* Headings, nav and CTAs follow --font-heading (Inter unless being tested).
   The MARCELGRAF. logo is deliberately excluded — it stays --font-main so the
   font tester never alters it (owner is happy with its current look). */
h1,
h2,
h3,
h4,
h5,
.nav-links a,
.subtitle,
.cta-button {
    font-family: var(--font-heading);
}

.logo {
    font-family: var(--font-main);
}

html {
    scroll-behavior: smooth;
    overflow-x: clip;
    touch-action: manipulation;
    overscroll-behavior: none;
}

body {
    background-color: var(--bg-color);
    color: var(--text-primary);
    overflow-x: clip;
    width: 100%;
    position: relative;
    touch-action: manipulation;
}

.bg-video,
#canvas-container,
.bg-blocker {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    pointer-events: none;
    object-fit: cover;
}

.bg-video,
#canvas-container {
    z-index: -2;
}

#project-3d-container {
    position: fixed;
    top: 0;
    right: 0;
    width: 30%;
    max-width: 600px;
    height: 100vh;
    z-index: 5;
    pointer-events: none;
}

.bg-blocker {
    background-color: var(--bg-color);
    opacity: 0;
    z-index: -1;
    transition: opacity 0.1s linear;
    will-change: opacity;
}

/* Home/index blocker only: overscan past the bottom so the darkening block
   covers the iOS toolbar gap, matching .bg-video (also 110lvh). The
   project-page blocker (.project-bg) must NOT do this — it would extend below
   the .project-bg-video (whose scale-overscan iOS can clip) and show as a hard
   black strip behind the tab bar. So the overscan is scoped away from it. */
.bg-blocker:not(.project-bg) {
    height: 100vh;
    height: 110lvh;
}

/* When used on project pages as a near-opaque background */
.bg-blocker.project-bg {
    opacity: 0.95;
}

.bg-video {
    opacity: 0.4;
    /* Dim the video so the site content pops */
    display: block;
}



.header-bg-gradient {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 10vh;
    min-height: 100px;
    background: linear-gradient(to bottom, var(--bg-color) 0%, var(--bg-color) 15%, rgba(5, 5, 5, 0.6) 50%, rgba(5, 5, 5, 0.15) 75%, transparent 100%);
    z-index: 90;
    pointer-events: none;
}

/* Navigation */
header {
    padding: 2rem 10%;
    position: fixed;
    width: 100%;
    top: 0;
    z-index: 105;
    pointer-events: none;
}

nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo,
.menu-toggle,
.nav-links {
    pointer-events: auto;
}

.logo {
    font-size: 1.5rem;
    font-weight: 900;
    letter-spacing: -2px;
    text-transform: uppercase;
    /* No opacity transition: js/main.js sets .logo opacity directly from the
       scroll position each frame, so it fades in lock-step with scrolling and is
       fully gone before a heading sticks (a timed tween left a brief overlap). */
    text-decoration: none;
    color: inherit;
}

.logo .logo-accent {
    color: var(--accent-color);
}

.logo.hidden {
    opacity: 0;
    pointer-events: none;
}

.nav-links {
    list-style: none;
    display: flex;
    gap: 2.5rem;
    align-items: center;
}

.nav-links a {
    color: var(--text-primary);
    text-decoration: none;
    text-transform: capitalize;
    font-size: 1.1rem;
    font-weight: 700;
    transition: color 0.3s ease;
}

.lang-selector-container {
    position: relative;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--text-primary);
}

.current-lang {
    padding: 0.5rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.lang-dropdown {
    position: absolute;
    top: 100%;
    right: 0;
    background: rgba(5, 5, 5, 0.95);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    display: flex;
    flex-direction: column;
    min-width: 60px;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-10px);
    transition: opacity 0.3s ease, transform 0.3s ease, pointer-events 0.3s ease;
}

.lang-selector-container:hover .lang-dropdown {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
}

.lang-dropdown button {
    background: transparent;
    border: none;
    padding: 0.75rem 1rem;
    color: var(--text-secondary);
    font-size: 0.9rem;
    font-weight: 700;
    cursor: pointer;
    text-align: right;
    transition: color 0.3s, background 0.3s;
}

.lang-dropdown button:hover,
.lang-dropdown button.active {
    color: var(--accent-color);
    background: rgba(255, 255, 255, 0.05);
}

.nav-links a:hover {
    color: var(--accent-color);
}

/* Hero Section */
.hero {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    /* Asymmetric bold layout */
    text-align: left;
    padding: 0 10%;
    /* Anchor for the absolutely-positioned scroll hint */
    position: relative;
}

/* Primary (View Showreel) + secondary (Get in Touch) hero buttons side by side;
   stacks vertically and centres on mobile (see the 768px block). */
.hero-cta-row {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    flex-wrap: wrap;
}


/* Scroll hint — a small downward chevron at the hero's bottom edge telling
   first-time visitors the page scrolls. JS adds .hidden on the first scroll
   and it never comes back (a one-time tell, not a persistent control). */
.scroll-hint {
    /* Fixed (not absolute-in-hero): the hero is 100vh = iOS LARGE viewport, so
       an absolute hint at the hero's bottom sits below the fold on mobile when
       the toolbar is showing and is never seen. Fixed to the viewport keeps it
       visible on every device; JS hides it for good on the first scroll. */
    position: fixed;
    bottom: 1.8rem;
    left: 50%;
    width: 0.9rem;
    height: 0.9rem;
    border-bottom: 2px solid rgba(255, 255, 255, 0.55);
    border-right: 2px solid rgba(255, 255, 255, 0.55);
    /* -135deg + translate: point DOWN, centred on the hero's midline */
    transform: translateX(-50%) rotate(45deg);
    animation: scroll-hint-bob 2.2s ease-in-out infinite;
    transition: opacity 0.5s ease;
    pointer-events: none;
}

.scroll-hint.hidden {
    opacity: 0;
}

@keyframes scroll-hint-bob {
    0%, 100% { translate: 0 0; }
    50% { translate: 0 8px; }
}

h1 {
    font-size: clamp(4rem, 10vw, 8rem);
    /* Larger and bolder */
    font-weight: 900;
    margin-bottom: 0.5rem;
    text-transform: uppercase;
    letter-spacing: -3px;
    line-height: 0.95;
    margin-left: -5px;
    /* Pull left slightly for optical alignment */
    /* If the name has to wrap, balance the lines (no lone word stranded on its
       own line) — supported everywhere that matters, harmless elsewhere. */
    text-wrap: balance;
}

p {
    color: var(--text-secondary);
    font-family: var(--font-body);
    font-size: clamp(1rem, 2vw, 1.5rem);
    margin-bottom: 2.5rem;
    /* 400 instead of 300: the thin weight read as low-contrast on the dark background */
    font-weight: 400;
}

/* Green-glow CTA — now reserved for the "View Showreel" button (and the 404
   page). The CV button and project-tile buttons use the glassy .glass-btn /
   .view-project-cta styles instead. */
.cta-button {
    position: relative;
    display: inline-block;
    padding: 1.2rem 3rem;
    background: transparent;
    border: 2px solid var(--accent-color);
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 700;
    font-family: inherit;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 2px;
    transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
    text-decoration: none;
    text-align: center;
    line-height: 1.4;
    box-sizing: border-box;
}

/* The glow lives on a dedicated pseudo-element that only fades its OPACITY.
   Transitioning box-shadow directly (the old approach) made the glow flicker /
   snap back mid-fade because the browser re-promoted the button to its own
   compositing layer on hover; an opacity fade on a static layer is smooth. */
.cta-button::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 30px rgba(12, 253, 20, 0.788);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.cta-button:hover {
    background: var(--accent-color);
    transform: translateY(-2px);
}

.cta-button:hover::before {
    opacity: 1;
}

/* View Showreel keeps the green glow but gains a trailing chevron matching the
   project-tile buttons (one consistent glyph language). The chevron is green at
   rest and turns white on hover so it stays legible once the button fills green. */
.cta-showreel {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.7rem;
}

.cta-showreel::after {
    content: "";
    width: 0.42em;
    height: 0.42em;
    flex-shrink: 0;
    border-top: 0.16em solid var(--accent-color);
    border-right: 0.16em solid var(--accent-color);
    transform: rotate(45deg);
    margin-left: -0.1em;
    transition: border-color 0.3s ease;
}

.cta-showreel:hover::after {
    border-top-color: var(--text-primary);
    border-right-color: var(--text-primary);
}

/* Glassy button — white text + a leading green glyph icon (matches the contact
   cards and software tiles). Used for the CV download; hover lifts, tints the
   border green, and lifts a SUBTLE outer glow (no green fill inside — the button
   already colour-shifts). */
.glass-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.75rem;
    padding: 1.1rem 2.5rem;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.12);
    color: var(--text-primary);
    font-family: var(--font-heading);
    font-size: 1rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 2px;
    text-decoration: none;
    cursor: pointer;
    line-height: 1.4;
    transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease;
}

/* Subtle glow on a dedicated pseudo (opacity fade only — same flicker-free
   approach as the View Showreel button, but dimmer/tighter so it stays subtle). */
.glass-btn::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 18px rgba(12, 253, 20, 0.4);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.glass-btn:hover {
    background: rgba(1, 197, 11, 0.08);
    border-color: var(--accent-color);
    transform: translateY(-2px);
}

.glass-btn:hover::before {
    opacity: 1;
}

.glass-btn .btn-icon {
    color: var(--accent-color);
    flex-shrink: 0;
}

/* Content Sections */
.content-section {
    padding: 6rem 2rem;
    margin: 0 auto;
    width: 100%;
    max-width: 55%;
    display: block;
    position: relative;
}

.content-section::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: -1;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    background: rgba(10, 10, 10, 0.2);
    /* Fades the blur effect near the edges */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%);
    mask-composite: intersect;
    pointer-events: none;
}

h2 {
    font-size: 2.5rem;
    font-weight: 700;
    font-family: var(--font-heading);
    position: -webkit-sticky;
    /* iOS Safari compatibility */
    position: sticky;
    /* 1.4rem (not 2rem) so a stuck headline's vertical centre lines up with the
       logo / nav row rather than hanging ~10px below it (measured: logo centre
       ≈46px, this puts the 2.5rem headline centre there too). */
    top: 1.4rem;
    z-index: 101;
    transform-origin: left top;
    margin-bottom: 2rem;
    text-transform: capitalize;
    letter-spacing: normal;
    width: fit-content;
    pointer-events: none;
    /* Prevents transparent block from absorbing mobile menu clicks */
}

h2.stuck {
    color: var(--text-primary);
    font-weight: 700;
    text-transform: capitalize;
    /* No size scaling to prevent jitter */
}

/* Hero additions */
.subtitle {
    font-size: 1.5rem;
    color: var(--accent-color);
    margin-bottom: 2.5rem;
    letter-spacing: 2px;
    text-transform: uppercase;
    font-weight: 700;
}



/* Global Glassmorphism Utility */
.glass-panel {
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 0;
}



/* About Section — no inner width cap: it used to stop at 800px, which left the
   About text + CV columns covering only part of a wide (4K) screen. It now
   fills the content-section (the shared column width) like every other section. */
.about-content {
    max-width: none;
    margin: 0;
    text-align: left;
}

.about-content p {
    font-size: 1.25rem;
    margin-bottom: 1.5rem;
    color: #ffffff;
    font-weight: 400;
    line-height: 1.6;
}

/* Desktop: keep the intro to a readable measure by ending it where the CV grid's
   second column (Education) ends — two of the three equal columns plus one gap.
   Keep in sync with .cv-grid's `gap: 2rem`. Narrower screens use the full width. */
@media (min-width: 1025px) {
    .about-text {
        max-width: calc((100% - 4rem) * 2 / 3 + 2rem);
    }
}

/* True mobile: smaller About copy, at the same 1rem as the project-page text
   (it was 0.95rem, a size smaller than the project pages). */
@media (max-width: 768px) {
    .about-content .about-para {
        font-size: 1rem;
        line-height: 1.55;
    }
}

/* Links inside the About copy: same quiet treatment as article links. */
.about-content .about-para a {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: rgba(255, 255, 255, 0.4);
    text-underline-offset: 0.2em;
    transition: text-decoration-color 0.2s ease;
}

.about-content .about-para a:hover {
    text-decoration-color: currentColor;
}

/* CV & Experience */
.cv-grid {
    display: grid;
    /* THREE equal columns (Experience | Education | Languages) that span the
       full content width at every desktop size — fixed thirds (not auto-fit)
       so they don't stay bunched on the left of a wide 4K screen. The <=768px
       rule below collapses them to a single column on mobile. */
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
    margin: 3rem 0;
}

.cv-column h3 {
    font-size: 1.5rem;
    /* White (was accent green): the green section labels clashed with the rest
       of the About section. Green now lives on the company/school names below. */
    color: var(--text-primary);
    margin-bottom: 1.5rem;
    text-transform: uppercase;
    font-weight: 700;
    letter-spacing: 1px;
}


.cv-item {
    margin-bottom: 2rem;
    padding-left: 1rem;
    border-left: 2px solid rgba(255, 255, 255, 0.1);
}

.cv-item h4 {
    font-size: 1.15rem;
    /* Green accent moved here — company / school names carry the highlight now
       instead of the section labels (Experience/Education). */
    color: var(--accent-color);
    margin-bottom: 0.25rem;
}

.cv-role {
    font-size: 1rem !important;
    /* Overriding base p format */
    color: #e0e0e0 !important;
    font-weight: 400 !important;
    font-style: italic;
    margin-bottom: 0.25rem !important;
}

.cv-date {
    font-size: 0.8rem !important;
    color: var(--text-secondary) !important;
    text-transform: uppercase;
    /* No letter-spacing: with the CV columns side by side (~360px each) the extra
       tracking pushed the longer date lines onto a second line. Tighter + slightly
       smaller keeps every date on one line without shrinking them noticeably. */
    margin-bottom: 0 !important;
}


.cv-download-wrapper {
    text-align: left;
    margin-top: -1rem;
    margin-bottom: 3rem;
}

/* Software Stack */
.software-section {
    margin-top: 3rem;
}

.software-section h3 {
    font-size: 1.5rem;
    /* White to match the other About section labels (was accent green) */
    color: var(--text-primary);
    margin-bottom: 1.5rem;
    text-transform: uppercase;
    font-weight: 700;
    letter-spacing: 1px;
}

.software-icons-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 1.5rem;
    margin-bottom: 2rem;
}

.tech-icon-only {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    height: 60px;
    padding: 0.5rem;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 0;
    /* Geometric styling over rounded corners */
    transition: background 0.3s ease, border-color 0.3s ease;
    cursor: pointer;
}

/* Subtle green glow — same opacity-pseudo treatment as the glassy buttons. */
.tech-icon-only::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 18px rgba(12, 253, 20, 0.4);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.tech-icon-only:hover,
.tech-icon-only.active {
    background: rgba(1, 197, 11, 0.2);
    border-color: var(--accent-color);
}

.tech-icon-only:hover::before,
.tech-icon-only.active::before {
    opacity: 1;
}

/* display:flex/block strips the inline-baseline descender gap that pushed the
   logos toward the top of their tile (the bare Houdini/Nuke imgs exposed it) */
.tech-icon-only picture {
    display: flex;
    align-items: center;
}

.software-logo {
    display: block;
    height: 32px;
    width: auto;
    object-fit: contain;
}

.software-info-display {
    /* Transparent layout shell only — the glass box now lives on each panel
       (.tech-info-content). This element's one job is to RESERVE height: the
       panels are grid-stacked in a single cell (see .active), and the hidden
       ones keep the cell as tall as the TALLEST panel, so the section's height
       is fixed and the page below never shifts when you switch icons. */
    display: none;
    opacity: 0;
    transition: opacity 0.3s ease;
    max-width: 600px;
    margin-bottom: 1rem;
}

.software-info-display.active {
    display: grid;
    /* align-items:start lets each stacked panel size to its OWN content (so the
       visible box hugs the active entry, no more half-empty box) while the
       hidden panels still hold the cell open to the tallest height. */
    align-items: start;
    opacity: 1;
}

.tech-info-content {
    grid-row: 1;
    grid-column: 1;
    /* Glass panel moved here from .software-info-display so the visible box wraps
       only the active entry's text, not the tallest panel. Sharp corners (no
       radius) to match the site's non-rounded aesthetic. */
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    padding: 1.5rem;
    visibility: hidden;
    opacity: 0;
    /* Inactive panels vanish INSTANTLY (no fade-out) so a longer previous
       paragraph never lingers behind a shorter new one — the old text clears
       all at once and only the incoming panel fades in. This also makes rapid
       icon-to-icon sweeping robust: there are never several half-faded panels
       stacked in the cell. Panels stay in-layout via visibility (not
       display:none) so the box keeps the tallest panel's height (no jump). */
    transition: opacity 0s, visibility 0s;
    text-align: left;
}

.tech-info-content.active {
    visibility: visible;
    opacity: 1;
    /* Only the fade-IN is animated */
    transition: opacity 0.22s ease;
}

.tech-info-content h4 {
    /* Green software name for extra contrast inside the info panel */
    color: var(--accent-color);
    margin-bottom: 0.5rem;
    font-size: 1.2rem;
}

.tech-info-content p {
    color: var(--text-primary);
    font-size: 0.95rem;
    line-height: 1.5;
    margin-bottom: 0;
}

/* The three-dot "more tools" tile — pure CSS dots so no icon file is needed */
.tech-icon-more .more-dots {
    display: flex;
    gap: 6px;
    align-items: center;
}

.tech-icon-more .more-dots span {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--text-primary);
}

.showreel-container {
    width: 100%;
    margin-top: 1rem;
}

.showreel-iframe-wrapper {
    width: 100%;
    max-width: calc(80vh * (16 / 9)); /* Constrain width so height doesn't exceed 80vh */
    max-height: 80vh;
    margin: 0 auto;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: 8px;
    background: #000000;
    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}

.showreel-iframe-wrapper iframe {
    width: 100%;
    height: 100%;
    border: none !important;
    outline: none !important;
}

.showreel-subtitle {
    font-size: 1.25rem;
    color: #ffffff;
    font-weight: 400;
    line-height: 1.6;
    margin-bottom: 1.5rem;
    text-align: left;
}


/* Work Grid */
/* Project sort: a small three-bar icon (long / medium / short) on the right of
   the "Projects" heading row, opening a menu styled like the language dropdown.
   js/main.js (setupWorkSort) pulls the row up onto the heading's line, because
   the sticky h2 can't be wrapped in a flex row without breaking its stickiness. */
.work-sort {
    position: relative;
    z-index: 5;
    display: flex;
    justify-content: flex-end;
    height: 0;               /* takes no space; JS aligns it with the heading */
}

.work-sort-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.work-sort-toggle:hover,
.work-sort-toggle:focus-visible,
.work-sort-toggle[aria-expanded="true"] {
    color: var(--text-primary);
    border-color: rgba(255, 255, 255, 0.1);
}

.work-sort-menu {
    position: absolute;
    top: 2.5rem;
    right: 0;
    background: rgba(5, 5, 5, 0.95);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    display: flex;
    flex-direction: column;
    /* Shrink-wrap to the longest label (no fixed min-width, which left a wide
       empty gap left of right-aligned text). */
    width: max-content;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-10px);
    transition: opacity 0.3s ease, transform 0.3s ease;
}

.work-sort.open .work-sort-menu {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
}

.work-sort-btn {
    background: transparent;
    border: none;
    padding: 0.7rem 1.25rem;
    color: var(--text-secondary);
    font-family: var(--font-main);
    font-size: 0.9rem;
    font-weight: 700;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    transition: color 0.3s, background 0.3s;
}

.work-sort-btn:hover,
.work-sort-btn:focus-visible,
.work-sort-btn.active {
    color: var(--accent-color);
    background: rgba(255, 255, 255, 0.05);
}

/* Screen-reader-only text (e.g. the sort icon's label) */
.visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.work-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2rem;
    width: 100%;
}

.work-item {
    position: relative;
    border-radius: 0;
    /* Sharp geometric corners */
    overflow: hidden;
    cursor: pointer;
    aspect-ratio: 1/1;
    /* Square tiles */
    text-decoration: none; /* For <a> wrapper */
}

/* Hover-zoom on the tile media. Two problems this has to solve:
   A. ALIASING on standard-DPI (1080p) screens: a promoted layer is rasterized once
      at its layout size, then GPU-scaled. Scaling UP magnifies that texture past its
      resolution → shimmer during the zoom that snaps crisp when it stops. (A 4K/hi-DPI
      screen rasterizes at 2x, so 1.05x still stays within the texture — hence it only
      showed on the 1080p monitor.)
      FIX: render the media 5% LARGER than the tile and only ever scale it DOWN — rest
      = scale(0.9524) (→100% of the tile), hover = scale(1) (→105%). The rasterized
      backing store is always ≥ the displayed size, so the texture is never magnified.
   B. A large 16:9 <video> gets handed to a hardware video-overlay plane that ignores
      CSS transforms (the visualiser tile wouldn't zoom at all). Putting will-change +
      a non-identity transform directly on the <video> forces it onto a normal
      composited layer so the transform actually applies. */
.work-media {
    position: absolute;
    inset: 0;
    overflow: hidden;
}

.work-media img,
.work-media video {
    position: absolute;
    top: -2.5%;
    left: -2.5%;
    width: 105%;
    height: 105%;
    object-fit: cover;
    display: block;
    /* The wrapping <a> handles all interaction; this also stops Firefox from
       showing its picture-in-picture overlay on hover */
    pointer-events: none;
    transform: scale(0.9524);
    transform-origin: center center;
    transition: transform 0.5s ease;
    will-change: transform;
    backface-visibility: hidden;
}

.work-item:hover .work-media img,
.work-item:hover .work-media video {
    transform: scale(1);
}

.work-overlay {
    will-change: transform;
    position: absolute;
    /* Bleed 2px past all four edges (clipped by the tile's overflow:hidden) so the
       backdrop blur reaches the true tile edges — otherwise subpixel rounding at
       the element edge leaves a ~1px unblurred sliver (was fixed on the sides;
       the bottom needs it too, visible on 1080p). */
    bottom: -2px;
    left: -2px;
    right: -2px;
    padding: 6rem calc(2rem + 2px) calc(2rem + 2px) calc(2rem + 2px);
    /* Increased top padding so gradient start is higher up and softer */
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.8) 100%);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 40%);
    -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 40%);
    transform: translateY(100%);
    transition: transform 0.4s ease;
}

.work-item:hover .work-overlay {
    transform: translateY(0);
}

/* Touch breakpoint (matches the JS tile logic at 1150px): the overlay reveals
   when the tile is .tapped — set either by a tap or, as you scroll, by the
   auto-reveal observer when the tile reaches the viewport centre. */
@media (max-width: 1150px) {
    .work-item.tapped .work-overlay {
        transform: translateY(0);
    }
}

.work-overlay h3 {
    font-size: 1.5rem;
    margin-bottom: 0.5rem;
    color: #ffffff;
    /* Explicit white to contrast with the dark blur */
}

.work-overlay p {
    font-size: 0.95rem;
    color: #e0e0e0;
    /* Light gray for readability */
    margin-bottom: 0;
}



/* Contact Section */
#contact {
    text-align: left;
}

#contact p {
    font-size: 1.25rem;
    color: #ffffff;
    font-weight: 400;
    line-height: 1.6;
    margin-bottom: 2rem;
}

/* Contact Grid */
/* Desktop: two 3-row columns (hidden on mobile, where the carousel takes over).
   Column-major fill via grid-auto-flow; per-item `order` gives:
     col 1 = Phone, E-Mail, Save Contact
     col 2 = LinkedIn, Instagram, ArtStation  */
.contact-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: repeat(3, auto);
    grid-auto-flow: column;
    gap: 1.5rem;
    margin-top: 2rem;
    max-width: 680px;
}

.contact-grid a[href^="tel"] { order: 1; }
.contact-grid a[href^="mailto"] { order: 2; }
.contact-grid .save-contact-btn { order: 3; }
.contact-grid a[href*="linkedin"] { order: 4; }
.contact-grid a[href*="instagram"] { order: 5; }
.contact-grid a[href*="artstation"] { order: 6; }
/* ArtStation switched off (index.html): Save Contact closes the grid full width
   instead of leaving a hole. Re-enabling ArtStation restores the 2 x 3 grid. */
.contact-grid:not(:has(a[href*="artstation"])) .save-contact-btn { grid-column: 1 / -1; grid-row: 3; }

.contact-card {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: 1.5rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.1);
    text-decoration: none;
    transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease;
    /* Normalises the <button> variant (Save Contact) to match the <a> cards */
    width: 100%;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* Subtle green glow on hover — matches the glassy buttons / software tiles. */
.contact-card::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 18px rgba(12, 253, 20, 0.4);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.contact-card:hover {
    background: rgba(1, 197, 11, 0.05);
    border-color: var(--accent-color);
    transform: translateY(-3px);
}

.contact-card:hover::before {
    opacity: 1;
}

.contact-icon {
    flex-shrink: 0;
    color: var(--accent-color);
}

.contact-label {
    font-size: 1.1rem;
    font-weight: 600;
    color: #ffffff;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 0;
}

/* ================================================
   CONTACT CAROUSEL (mobile only) — mirrors the software carousel:
   a scrollable row of platform icons; tapping one reveals its details in the
   box below. The card grid above is shown on desktop, this on mobile.
   ================================================ */
.contact-carousel {
    display: none;
    /* shown at <=768px (see media query below) */
}

.contact-icons-row {
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    /* Vertical padding leaves room for the tile glow (see .software-icons-row). */
    padding: 24px 0;
    margin-bottom: 0.5rem;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Same directional scroll-fade as the software row (JS sets the vars). */
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--sw-fade-left, 0px), #000 calc(100% - var(--sw-fade-right, 28px)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--sw-fade-left, 0px), #000 calc(100% - var(--sw-fade-right, 28px)), transparent 100%);
}

.contact-icons-row::-webkit-scrollbar {
    display: none;
}

.contact-icon-only {
    position: relative;
    flex: 0 0 auto;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    height: 60px;
    padding: 0.75rem;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    /* White icons (like the software logos) — the green now lives on the
       headline + links, matching the software carousel's colourway. */
    color: var(--text-primary);
    cursor: pointer;
    transition: background 0.3s ease, border-color 0.3s ease;
}

.contact-icon-only svg {
    display: block;
}

/* Carousel order: E-Mail, Phone, Save Contact, LinkedIn, Instagram, ArtStation */
.contact-icons-row [data-target="cinfo-email"] { order: 1; }
.contact-icons-row [data-target="cinfo-phone"] { order: 2; }
.contact-icons-row [data-target="cinfo-save"] { order: 3; }
.contact-icons-row [data-target="cinfo-linkedin"] { order: 4; }
.contact-icons-row [data-target="cinfo-instagram"] { order: 5; }
.contact-icons-row [data-target="cinfo-artstation"] { order: 6; }

.contact-icon-only::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 18px rgba(12, 253, 20, 0.4);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.contact-icon-only:hover,
.contact-icon-only.active {
    background: rgba(1, 197, 11, 0.2);
    border-color: var(--accent-color);
}

.contact-icon-only:hover::before,
.contact-icon-only.active::before {
    opacity: 1;
}

.contact-info-display {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    padding: 1.25rem 1.5rem;
    display: none;
    max-width: 400px;
}

.contact-info-display.active {
    /* All panels stacked in one grid cell → box is as tall as the tallest,
       no jump on switch (same trick as the software info box). */
    display: grid;
}

.contact-info-content {
    grid-row: 1;
    grid-column: 1;
    visibility: hidden;
    opacity: 0;
    /* Instant out, fade in (same robust crossfade as the software panels). */
    transition: opacity 0s, visibility 0s;
}

.contact-info-content.active {
    visibility: visible;
    opacity: 1;
    transition: opacity 0.22s ease;
}

.contact-info-content h4 {
    /* White headline — green is reserved for the links here, since doubling
       green on both the title and the hyperlink read as too much. */
    color: var(--text-primary);
    font-size: 1.1rem;
    margin-bottom: 0.4rem;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.contact-info-content a {
    color: var(--accent-color);
    font-family: var(--font-body);
    font-size: 1.05rem;
    text-decoration: none;
    word-break: break-word;
}

/* External-profile links (LinkedIn / Instagram / ArtStation) get a trailing
   chevron matching the project/showreel buttons. Actions (mailto/tel/save)
   don't — they're not "go somewhere" links. */
.contact-info-content a.contact-ext {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.contact-info-content a.contact-ext::after {
    content: "";
    width: 0.42em;
    height: 0.42em;
    flex-shrink: 0;
    border-top: 0.16em solid currentColor;
    border-right: 0.16em solid currentColor;
    transform: rotate(45deg);
}

.contact-info-content a:hover {
    text-decoration: underline;
}

@media (max-width: 768px) {
    .contact-grid {
        display: none;
    }

    .contact-carousel {
        display: block;
    }
}

/* Footer */
footer {
    /* Horizontal padding respects a landscape notch (env() = 0 elsewhere, so this
       stays 5% on every non-notched device). The edge-anchored footer links (5% ≈
       40px) would otherwise sit just under the ~44px safe-area inset. */
    padding: 2rem max(5%, env(safe-area-inset-right)) 2rem max(5%, env(safe-area-inset-left));
    background: var(--bg-color);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    position: relative;
    z-index: 1;
    /* Solid slab painted below the footer so the fixed background video/3D
       (z-index -2/-1, pinned to the viewport) can't peek through beneath it.
       The Contact section is sized to the LARGE (URL-bar-retracted) viewport,
       which lets the page rubber-band a little past the footer; that overscroll
       used to reveal the video. This box-shadow extends the footer's own colour
       ~200vh downward and travels with the footer, so the gap always reads black.
       Box-shadow paints behind the element and never affects layout or scroll
       height — the footer stays flush at the true page bottom. */
    box-shadow: 0 100vh 0 100vh var(--bg-color);
}

.footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 1rem;
}

.footer-content p {
    margin-bottom: 0;
    font-size: 0.9rem;
}

.legal-links a {
    color: var(--text-secondary);
    text-decoration: none;
    font-size: 0.9rem;
    transition: color 0.3s;
}

.legal-links a:hover {
    color: var(--accent-color);
}







/* Mobile Menu */
.menu-toggle {
    display: none;
    cursor: pointer;
    z-index: 101;
}

.menu-toggle .bar {
    display: block;
    width: 25px;
    height: 3px;
    margin: 5px auto;
    transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out;
    background-color: var(--text-primary);
}

@media (max-width: 1420px) {
    .menu-toggle {
        display: block;
    }

    .content-section {
        max-width: 80%;
        padding: 5rem 5%;
    }

    .nav-links {
        position: fixed;
        right: -100%;
        top: 0;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        background-color: var(--bg-color);
        width: 100%;
        height: 100vh;
        height: 100dvh; /* dynamic vp: correct height under Safari's collapsing toolbar */
        text-align: center;
        padding-top: 0;
        padding-left: 0;
        /* Snappy ease-out: fast start, soft landing */
        transition: right 0.25s cubic-bezier(0.22, 1, 0.36, 1);
        z-index: 99;
        /* display:none when closed — removes the panel from the RENDER TREE so
           iOS 26 Safari stops sampling its black background for the toolbar tint
           (see js/main.js). JS toggles .rendered around the slide animation.
           NO box-shadow: a leftward shadow on an off-screen-right element used to
           bleed a fake dark "gradient" onto the right edge at this breakpoint. */
        display: none;
    }

    .nav-links.rendered {
        display: flex;
    }

    .nav-links.active {
        right: 0;
    }

    .nav-links li {
        margin-bottom: 2rem;
    }

    .nav-links a {
        font-size: 1.5rem;
    }

    .menu-toggle.is-active .bar:nth-child(2) {
        opacity: 0;
    }

    .menu-toggle.is-active .bar:nth-child(1) {
        transform: translateY(8px) rotate(45deg);
    }

    .menu-toggle.is-active .bar:nth-child(3) {
        transform: translateY(-8px) rotate(-45deg);
    }

    /* Mobile Language Dropdown Override */
    .lang-selector-container {
        pointer-events: none;
        /* Ignore hover locally */
    }

    .current-lang {
        display: none;
        /* Hide toggle text, just show options below */
    }

    .lang-dropdown {
        position: static;
        opacity: 1;
        pointer-events: auto;
        transform: none;
        border: none;
        background: transparent;
        flex-direction: row;
        justify-content: center;
        gap: 1.5rem;
        margin-top: 1rem;
    }

    .lang-dropdown button {
        padding: 0.5rem;
        font-size: 1.2rem;
    }
}

@media (max-width: 1024px) {
    /* NOTE: .content-section deliberately stays at the 80%-centred "half desktop"
       width here (set in the 1420 block). It used to jump to max-width:100% /
       padding:2rem at this breakpoint, which pulled the section content out to
       ~2rem from the left while the logo/nav stayed at the header's 10% inset —
       so the body went full-width and its headlines sat further left than the
       logo, an awkward middle state between desktop and mobile. Full-width +
       left-bound is now a single clean switch at the mobile breakpoint (768). */

    #project-3d-container {
        width: 100%;
        opacity: 0.3;
        z-index: -1;
    }

    .cv-download-wrapper {
        margin-top: -0.5rem;
        margin-bottom: 5rem;
        text-align: center;
    }

    .software-section {
        margin-top: 2rem;
    }

    .info-body {
        padding: 2rem;
    }
}

@media (max-width: 768px) {
    .cv-grid {
        grid-template-columns: 1fr;
    }

    /* Center Hero content on Mobile */
    .hero {
        align-items: flex-start;
        /* Ensure left bind is kept */
        text-align: left;
    }

    .hero-cta-row {
        align-self: center;
        flex-direction: column;
        align-items: stretch;
        gap: 1rem;
        /* fit-content = the row is as wide as its WIDEST label, and both buttons
           stretch to that. So the two buttons match each other, but the width
           follows the language (wider for German, slimmer for English) instead
           of one fixed size that forced the longer German labels onto two lines. */
        width: fit-content;
        max-width: 100%;
    }

    .hero-cta-row .cta-button {
        /* One line always; the button widens instead of wrapping. */
        white-space: nowrap;
        letter-spacing: 1px;
    }

    h1 {
        margin-left: 0;
    }

    header {
        padding: 1.5rem 5%;
    }

    .content-section {
        /* Mobile state: the body finally goes FULL-WIDTH and left-bound. The 5%
           inset matches the header's 5% (set just above) so the logo, the section
           headlines and the body copy all share one left edge. */
        max-width: 100%;
        padding: 5rem 5%;
    }

    /* Force hover behavior to be click behavior on mobile */
    .work-overlay {
        transform: translateY(100%);
    }

    .work-item.tapped .work-overlay {
        transform: translateY(0);
    }

    h2 {
        font-size: 2rem;
        /* Keep words on one line */
        top: 1.4rem;
    }
}

@media (max-width: 768px) {

    /* Software tiles become a horizontal scroll carousel so any number of
       tools stays on one row (no ugly wrap to a second line) */
    .software-icons-row {
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: 1rem;
        /* Vertical padding gives the tiles' hover/active GLOW room inside the
           scroll container (overflow-x:auto also clips vertically, which was
           cropping the box-shadow into a hard rectangle). The negative top
           margin keeps the heading→row gap unchanged. */
        padding: 24px 0;
        margin-top: -24px;
        margin-bottom: 0.5rem;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        /* Directional scroll-fade: js/main.js sets --sw-fade-left / --sw-fade-right
           from the scroll position, so the row fades on the LEFT once you've
           scrolled in, and drops the RIGHT fade when the last icon is fully
           reached (so it no longer clips the last tile). Defaults = at the start:
           no left fade, right fade present. */
        -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--sw-fade-left, 0px), #000 calc(100% - var(--sw-fade-right, 28px)), transparent 100%);
        mask-image: linear-gradient(to right, transparent 0, #000 var(--sw-fade-left, 0px), #000 calc(100% - var(--sw-fade-right, 28px)), transparent 100%);
    }

    .software-icons-row::-webkit-scrollbar {
        display: none;
    }

    .tech-icon-only {
        flex: 0 0 auto;
        scroll-snap-align: start;
    }
}

/* Legal Page Styling — proper desktop treatment (the pages used to read like a
   cramped phone layout on large screens): a wider measure, body-sized text and
   clear section separators. */
.legal-content {
    padding: 9rem 2rem 5rem;
    max-width: 860px;
    margin: 0 auto;
    min-height: 100vh;
    word-wrap: break-word;
    overflow-wrap: break-word;
    hyphens: auto;
}

.legal-content h3 {
    font-size: 1.5rem;
    margin-top: 3rem;
    margin-bottom: 1.25rem;
    color: var(--text-primary);
}

/* Numbered top-level sections get a hairline divider once real content
   precedes them — reads as document structure instead of a text dump. */
.legal-content p + h3 {
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    padding-top: 2.25rem;
}

.legal-content h4 {
    font-size: 1.2rem;
    margin-top: 2rem;
    margin-bottom: 0.75rem;
    color: var(--text-primary);
}

.legal-content p,
.legal-content a {
    font-size: 1.05rem;
    font-family: var(--font-body);
    color: var(--text-secondary);
    margin-bottom: 1rem;
    line-height: 1.65;
}

/* Canonical-language note at the end of each legal page */
.legal-note {
    margin-top: 3rem;
    padding-top: 1.5rem;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    font-style: italic;
    font-size: 0.9rem !important;
}

.legal-content a:hover {
    color: var(--accent-color);
}

.back-link {
    margin-bottom: 2rem;
    display: inline-block;
    color: var(--accent-color);
    text-decoration: none;
    font-weight: bold;
}

/* Scroll To Top Button */
.scroll-top-btn {
    position: fixed;
    /* max() keeps the button off a landscape notch / home indicator: env() is 0 on
       every non-notched device, so these collapse to the plain 2rem there. */
    bottom: max(2rem, env(safe-area-inset-bottom));
    right: max(2rem, env(safe-area-inset-right));
    width: 45px;
    height: 45px;
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 0;
    color: var(--text-secondary);
    font-size: 1.5rem;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease, background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
    /* Below the mobile nav panel (z-index 99) and header (105): the open burger
       menu must cover this button, not the other way around. 98 still clears
       all normal page content. */
    z-index: 98;
}

.scroll-top-btn.visible {
    opacity: 1;
    visibility: visible;
}

.scroll-top-btn:hover {
    background: rgba(1, 197, 11, 0.2);
    color: #fff;
    border-color: var(--accent-color);
}

/* Global Project Sub-Page Styles */
.project-detail-main {
    min-height: 80vh;
}


.project-header {
    display: contents;
}

.project-back-btn {
    color: var(--accent-color);
    text-decoration: none;
    font-weight: bold;
    display: inline-block;
    margin-bottom: 1.5rem;
    transition: color 0.3s;
    touch-action: manipulation;
}

.project-back-btn:hover {
    color: #fff;
}

/* Desktop-only floating back button (project pages, injected by js/main.js).
   EXPERIMENTAL — owner may remove it again; to do so, delete this block and
   the "Desktop back button" section in js/main.js. Pinned to the far left
   edge at mid-height: the content column (55% centred) never reaches it, and
   the 3D viewers' models orbit the centre of their half, so the extreme edge
   stays clear. Hidden on mobile (<=1150px), where OS back gestures do the job. */
.desktop-back-btn {
    position: fixed;
    /* Bottom-left corner, mirroring the bottom-right scroll-to-top button
       (same 45px size + backdrop) so the two read as a matched pair. */
    left: 2rem;
    bottom: 2rem;
    width: 45px;
    height: 45px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: var(--text-secondary);
    z-index: 98;
    transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

/* Left-pointing chevron, border-drawn like every other glyph on the site */
.desktop-back-btn::before {
    content: "";
    width: 0.55em;
    height: 0.55em;
    border-left: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    margin-left: 0.15em;
}

.desktop-back-btn:hover {
    background: rgba(1, 197, 11, 0.2);
    border-color: var(--accent-color);
    color: #fff;
}

@media (max-width: 1150px) {
    .desktop-back-btn {
        display: none;
    }
}



/* The headline subtitle span is now always inline at every width — the old
   desktop "collapse the subtitle when the title sticks" system was retired in
   favour of ONE mechanism (the overflow marquee below), which now triggers on
   desktop too whenever the full line is too wide for its column. */
.fade-on-scroll {
    display: inline;
    white-space: nowrap;
}

/* ================================================
   MARQUEE PROJECT TITLES (mobile)
   Long title lines keep their full size; instead of shrinking, they fade
   out toward the burger menu and slowly scroll back and forth (like long
   song titles in Apple Music). js/main.js measures each line and sets
   --marquee-shift / --marquee-duration + the classes below.
   ================================================ */

/* Constrain the headline to its column at EVERY width so the overflow measure
   is meaningful (base h2 is width:fit-content, which would just stretch to the
   text and never "overflow"). If the full line fits, nothing below applies. */
.sticky-project-title {
    overflow: hidden;
    width: 100%;
    max-width: 100%;
    white-space: nowrap;
}

.marquee-line {
    display: inline-block;
    white-space: nowrap;
    vertical-align: bottom;
}

/* The edge fades are ANIMATED custom properties, kept in lock-step with the
   marquee's own keyframes (same duration/delay/timing) so the fade reflects the
   headline's live position — like the software carousel's scroll-shadows:
     • at rest (start)   → no left fade (first letter crisp), soft right fade
     • sliding left      → left fade grows, right fade shrinks
     • at rest (end)     → soft left fade, NO right fade (end fully revealed)
   Registered via @property so they interpolate inside the mask calc(). */
@property --title-fade-left {
    syntax: '<length>';
    inherits: false;
    initial-value: 0px;
}

@property --title-fade-right {
    syntax: '<length>';
    inherits: false;
    initial-value: 0px;
}

/* --title-clear-right (set by js/main.js) is dead space reserved on the right so
   the headline never slides under the nav / burger when stuck; the marquee also
   scrolls that much further so the end still fully reveals inside the clear area. */
.sticky-project-title.is-overflowing {
    -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--title-fade-left, 0px), black calc(100% - var(--title-clear-right, 0px) - var(--title-fade-right, 0px)), transparent calc(100% - var(--title-clear-right, 0px)));
    mask-image: linear-gradient(to right, transparent 0, black var(--title-fade-left, 0px), black calc(100% - var(--title-clear-right, 0px) - var(--title-fade-right, 0px)), transparent calc(100% - var(--title-clear-right, 0px)));
    animation: title-fade-left var(--marquee-duration, 12s) ease-in-out 0.5s infinite both,
               title-fade-right var(--marquee-duration, 12s) ease-in-out 0.5s infinite both;
}

.sticky-project-title .marquee-line.marquee-scroll {
    animation: title-marquee var(--marquee-duration, 12s) ease-in-out 0.5s infinite;
}

/* Pause at the start, glide to the end, pause, glide back */
@keyframes title-marquee {

    0%,
    18% {
        transform: translateX(0);
    }

    45%,
    60% {
        transform: translateX(var(--marquee-shift, 0px));
    }

    88%,
    100% {
        transform: translateX(0);
    }
}

/* Fade tracks synchronised to the translate keyframes above */
@keyframes title-fade-left {
    0%, 18% { --title-fade-left: 0px; }
    45%, 60% { --title-fade-left: 30px; }
    88%, 100% { --title-fade-left: 0px; }
}

@keyframes title-fade-right {
    0%, 18% { --title-fade-right: 30px; }
    45%, 60% { --title-fade-right: 0px; }
    88%, 100% { --title-fade-right: 30px; }
}

.project-media-container {
    position: relative;
    width: 100%;
    /* Default framing before JS measures the first slide. slideshow.js then sets
       aspect-ratio inline to match each active slide's native proportions, so
       images and videos show true-to-size (never letterboxed or cropped into a
       fixed frame). */
    aspect-ratio: 16 / 9;
    border-radius: 0;
    overflow: hidden;
    margin-bottom: 2rem;
    background: #000;
    touch-action: manipulation;
}

.project-media-container video,
.project-media-container img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.project-media-container .slideshow-img {
    display: none;
    animation: fadeIn 0.5s ease;
}

.project-media-container .slideshow-img.active {
    display: block;
}

@keyframes fadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.slideshow-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    color: white;
    border: none;
    font-size: 2.5rem;
    /* Flex-center the glyph: the ‹ › characters sit below the vertical middle
       of their line box, which made them look too low when focused/hovered */
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    width: 4.5rem;
    height: 4.5rem;
    padding: 0;
    cursor: pointer;
    transition: opacity 0.3s ease, transform 0.3s ease, color 0.3s ease;
    z-index: 10;
    opacity: 0;
    touch-action: manipulation;
}

.project-media-container:hover .slideshow-nav {
    opacity: 1;
}

.slideshow-nav:hover {
    transform: translateY(-50%) scale(1.1);
    color: var(--accent-color);
}

/* On mobile: hide arrows entirely, use dot indicators instead */
@media (max-width: 1150px) {
    .slideshow-nav {
        display: none !important;
    }
}

/* Dot indicators for carousel.
   The container is static HTML (dots are filled in by slideshow.js), and min-height
   reserves the exact dot height so the layout doesn't jump when JS runs. */
/* Proximity: the dots belong to the carousel, so they sit close to it (small
   gap above) and are clearly separated from the info box below (~3x gap). */
.project-media-container:has(+ .slideshow-dots) {
    margin-bottom: 0;
}

.slideshow-dots {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    padding: 1.25rem 0 0;
    margin-bottom: 2.5rem;
    min-height: calc(1.25rem + 8px);
}

.slideshow-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.4);
    border: none;
    padding: 0;
    cursor: pointer;
    transition: background 0.3s ease, transform 0.3s ease;
    touch-action: manipulation;
}

.slideshow-dot:hover {
    background: rgba(255, 255, 255, 0.7);
    transform: scale(1.2);
}

.slideshow-dot.active {
    background: var(--accent-color);
    transform: scale(1.2);
}

.prev-slide {
    left: 0;
}

.next-slide {
    right: 0;
}

.project-description {
    /* White body copy (was grey) for stronger readability on the dark page;
       only the media captions stay grey. */
    color: var(--text-primary);
    /* No width cap: the text column matches the carousel/media width above it
       (both are full content-section width). Any earlier cap made the text
       narrower than the carousel, leaving an uneven dead gap on the right. */
}

/* Body copy: set size AND colour on the <p> itself — the base `p { ... }` rule
   targets the element directly and would otherwise win over a container-level
   rule (this bit us on both font-size and colour). White (was grey) for
   readability; dialled down from ~1.15rem/1.7 (too large/airy on desktop). */
.project-description p {
    font-size: 1rem;
    line-height: 1.55;
    color: var(--text-primary);
}

.project-description p {
    max-width: none;
}

/* In-text links (e.g. cross-references between projects): same white as the
   copy, marked only by a soft underline that firms up on hover. */
.project-description p a {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: rgba(255, 255, 255, 0.4);
    text-underline-offset: 0.2em;
    transition: text-decoration-color 0.2s ease;
}

.project-description p a:hover {
    text-decoration-color: currentColor;
}

.back-link:hover {
    text-decoration: underline;
}

/* Centralized project description paragraph spacing */
.project-description p + p {
    margin-top: 1.4rem;
}

/* In-text section headings inside a project description. Inter (matches every
   other heading on the site) and white — no decorative green in running text.
   This is the single source for the sub-heading look: reuse the same
   <h3 class="project-section-heading"> on future project pages so they stay
   consistent. */
.project-section-heading {
    font-family: var(--font-heading);
    font-size: 1.35rem;
    font-weight: 700;
    line-height: 1.3;
    color: var(--text-primary);
    margin: 2.25rem 0 1rem;
    /* Never let a heading ride up beside a floated inline image above it */
    clear: both;
}

/* Bulleted lists inside a project description (the global reset strips list
   styling, so it is set explicitly here). Matches the body-copy size/colour. */
.project-description ul.project-list {
    list-style: disc;
    padding-left: 1.4rem;
    margin: 0 0 2rem;
    font-family: var(--font-body);
    font-size: 1rem;
    color: var(--text-primary);
}

.project-description ul.project-list li {
    margin-top: 0.8rem;
    line-height: 1.6;
}

.project-description ul.project-list li:first-child {
    margin-top: 0;
}

/* Emphasised label at the start of a list item (e.g. the texture channels) */
.project-description ul.project-list strong {
    color: var(--text-primary);
    font-weight: 700;
}

/* Project date styling on project pages: the date belongs to the headline,
   so it sits tight under the title with a larger gap to the carousel below */
.sticky-project-title {
    margin-bottom: 0.75rem;
}

.cv-date.project-date {
    /* !important needed to beat the base .cv-date margin-bottom: 0 !important */
    margin-bottom: 2rem !important;
    /* a touch larger than the CV dates (0.8rem): it heads the page (!important: base .cv-date sets 0.8rem !important) */
    font-size: 0.9rem !important;
}

/* Technical info block (role / tools / duration / team) — glass panel below
   the carousel. Label-over-value cells in a responsive grid: one row on wide
   desktops, 2x2 on phones — no awkward mid-wrap states in between. */
.project-meta,
.project-meta dt,
.project-meta dd {
    /* Set on the children too — the global `*` font rule beats inheritance */
    font-family: var(--font-body);
}

.project-meta {
    display: grid;
    /* 120px min: 4-up on desktop, 2x2 on a 375px phone */
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 1rem 1.5rem;
    padding: 1.25rem 1.5rem;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    margin: 0 0 2rem;
    line-height: 1.5;
}

.project-meta>div {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.project-meta dt {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-secondary);
}

.project-meta dd {
    margin: 0;
    /* 1rem = the article body size: the info block sits ABOVE the text, so its
       values must not render smaller than what follows (reviewer note, 2026-10). */
    font-size: 1rem;
    color: var(--text-primary);
}

/* Desktop sits on a near-black page (no bg video behind), where the light
   glass fill stood out too much — darken it there; mobile keeps the
   lighter fill that reads well over the background video */
@media (min-width: 1151px) {
    .project-meta {
        background: rgba(0, 0, 0, 0.45);
        border-color: rgba(255, 255, 255, 0.07);
        /* Weighted columns instead of equal quarters: Role and Tools carry the
           long content (skills/software lists), Time Spent and Team are short —
           equal gaps between unequal content read as misaligned. Order matches
           the DOM: Role | Tools | Time Spent | Team. minmax(0,…) lets long
           entries wrap instead of blowing the track open. */
        grid-template-columns: minmax(0, 1.3fr) minmax(0, 1.7fr) minmax(0, 0.7fr) minmax(0, 1fr);
    }

    /* Long Role/Tools: Time Spent above Team in a third column (js/main.js
       fitProjectMeta adds this only when it makes the box shorter or equal) */
    .project-meta.meta-stacked {
        grid-template-columns: minmax(0, 1.3fr) minmax(0, 1.7fr) minmax(0, 0.9fr);
    }
    .project-meta.meta-stacked > div:nth-child(1),
    .project-meta.meta-stacked > div:nth-child(2) {
        grid-row: 1 / 3;
    }
    .project-meta.meta-stacked > div:nth-child(3) {
        grid-column: 3;
        grid-row: 1;
    }
    .project-meta.meta-stacked > div:nth-child(4) {
        grid-column: 3;
        grid-row: 2;
    }
}

/* Glassy tile button — white text with a trailing green arrow glyph (added via
   ::after, so it survives the i18n innerHTML swaps that rebuild the label).
   Matches the CV / contact glass style: border tint + subtle glow on hover,
   no green fill inside. */
.view-project-cta {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 1rem;
    padding: 0.7rem 1.4rem;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.18);
    color: var(--text-primary);
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    text-align: center;
    transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

/* Subtle hover glow (opacity-only pseudo — flicker-free), matching .glass-btn. */
.view-project-cta::before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: 0 0 18px rgba(12, 253, 20, 0.4);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

/* 90° chevron drawn from two borders (top + right, rotated 45°) rather than a
   font arrow glyph — so it's identically weighted on every platform and its
   stroke matches the bold label weight instead of a hairline "→". */
.view-project-cta::after {
    content: "";
    width: 0.42em;
    height: 0.42em;
    flex-shrink: 0;
    border-top: 0.16em solid var(--accent-color);
    border-right: 0.16em solid var(--accent-color);
    transform: rotate(45deg);
    /* nudge back toward the label — the rotated box's visual mass sits low-left */
    margin-left: -0.1em;
}

@media (min-width: 1151px) {
    /* Tint + glow on hover of the button itself (not the whole tile, which kept
       it stuck lit the moment you entered the tile) */
    .view-project-cta:hover {
        background: rgba(1, 197, 11, 0.12);
        border-color: var(--accent-color);
    }

    .view-project-cta:hover::before {
        opacity: 1;
    }
}

/* ================================================
   MICRO-THEMING (selection + scrollbar)
   ================================================ */

/* Text selection in the CI green */
::selection {
    background: var(--accent-color);
    color: #050505;
}

/* Theme native form controls (checkboxes/range/etc.) green. NOTE: this does NOT
   recolour the browser's built-in <video controls> — Firefox/Zen render the
   video UI in a closed shadow tree that page CSS can't reach, so the scrubber
   stays the browser default. Recolouring it would require a fully custom
   controls skin (deliberately not done). */
:root {
    accent-color: var(--accent-color);
}

/* Dark grey scrollbar to match the theme (deliberately not green) */
html {
    scrollbar-color: #333333 #0a0a0a;
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: #0a0a0a;
}

::-webkit-scrollbar-thumb {
    background: #333333;
    border-radius: 0;
}

::-webkit-scrollbar-thumb:hover {
    background: #4a4a4a;
}

/* ================================================
   ACCESSIBILITY
   ================================================ */

/* Keyboard focus indicator — only visible when using keyboard, not mouse */
*:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: 3px;
}

/* Remove default focus ring for mouse users */
*:focus:not(:focus-visible) {
    outline: none;
}

/* ================================================
   REDUCED MOTION
   ================================================ */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    .bg-video {
        display: none;
    }

    .project-bg-video {
        display: none !important;
    }
}

/* ================================================
   MOBILE VIDEO BACKGROUNDS (Project Pages)
   ================================================ */

.project-bg-video {
    display: none; /* Hidden on desktop — 3D models are used instead */
}

@media (max-width: 1150px) {
    .project-bg-video {
        display: block;
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        object-fit: cover;
        /* Same z-index as .bg-blocker: the video must come AFTER the blocker in
           the HTML so it paints above it (at -2 it would vanish behind the
           95%-opaque blocker) while staying behind all page content */
        z-index: -1;
        /* Overscan the video so it always covers the full height even as iOS
           Safari's toolbars expand/collapse (100vh is unreliable there — the
           bottom edge could otherwise peek in). Scaling from the centre pushes
           the edges past the viewport; the bottom body::after gradient blends
           whatever remains. */
        transform: scale(var(--bg-video-scale, 1.12));
        /* Tunable per page (or live via ?tune) — defaults raised from
           blur 20px / 0.15 because dark footage was barely visible */
        filter: blur(var(--bg-video-blur, 10px));
        opacity: var(--bg-video-opacity, 0.3);
        pointer-events: none;
    }

    /* Home-page ambient video: extend the HEIGHT past the viewport so it always
       reaches the bottom, even under iOS Safari's minimized toolbar where 100vh
       fell short and left a black gap. A transform scale() overscan can get clipped
       by iOS on a fixed video, so size the box taller instead — top-anchored, so the
       extra height spills off the BOTTOM (where the gap was), not the top. */
    .bg-video {
        height: 100vh;
        height: 110lvh;
    }
}

/* ================================================
   INLINE CONTENT ELEMENTS (for project pages)
   ================================================ */

.inline-media {
    width: 100%;
    margin: 2.5rem 0;
}

.inline-media img,
.inline-media video {
    width: 100%;
    display: block;
    background: #000;
}

/* Inline videos are ambient (autoplay, muted, no controls) — make them fully
   non-interactive so browsers don't offer picture-in-picture on hover */
.inline-media video {
    pointer-events: none;
}

.inline-media-video video {
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

.inline-media-image img {
    object-fit: contain;
    max-height: 70vh;
}

/* Two images side by side (combined ~32:9) — a lighter separator than one
   full-width 16:9 block. Stacks vertically on mobile so each reads as a normal
   image. Reusable on any project page. */
.inline-media-pair {
    display: flex;
    gap: 1rem;
    margin: 2.5rem 0;
}

.inline-media-pair .inline-media {
    margin: 0;
    flex: 1 1 0;
    min-width: 0;
}

.inline-media-pair .inline-media-image img {
    aspect-ratio: 16 / 9;
    object-fit: cover;
    max-height: none;
}

@media (max-width: 768px) {
    .inline-media-pair {
        flex-direction: column;
        gap: 2.5rem;
    }

    .inline-media-pair .inline-media-image img {
        aspect-ratio: auto;
        object-fit: contain;
        max-height: 70vh;
    }
}

/* Cap an inline image/video to a share of the column so it reads as a separator
   rather than a giant block. Centered; goes full-width on mobile where the
   narrower viewport already keeps it reasonable. */
.inline-media-narrow {
    max-width: 70%;
    margin-left: auto;
    margin-right: auto;
}

@media (max-width: 768px) {
    .inline-media-narrow {
        max-width: 100%;
    }
}

/* Floated inline image — text wraps beside it on desktop for a more editorial
   layout; on mobile (<=768px) it falls back to a normal full-width stack.
   Add class "float-right" to flip the side. */
.inline-media-float {
    width: 42%;
    margin: 0.4rem 2rem 1.25rem 0;
    float: left;
}

.inline-media-float.float-right {
    float: right;
    margin: 0.4rem 0 1.25rem 2rem;
}

.inline-media-float img {
    max-height: none;
}

@media (max-width: 768px) {
    .inline-media-float {
        float: none;
        width: 100%;
        margin: 2.5rem 0;
    }
}

/* Media + text side by side, as two independent columns (NOT a float). Use this
   when a block of text — a list, a short paragraph — must stay locked beside an
   image and whatever follows must break cleanly BELOW, with no words spilling
   under the image. The text lives in .media-text; the image is a sibling
   .inline-media. Top-aligned (2026-10-08, was centred): after the copy was
   condensed, a short column centred beside a taller image floated with dead
   space above it, detached from its heading. Stacks vertically on mobile.
   Prefer this over .inline-media-float whenever the wrap needs a clean end. */
.media-text-row {
    display: flex;
    gap: 2rem;
    align-items: flex-start;
    margin: 2.5rem 0;
}

/* A heading binds to whatever follows it at the same 1rem whether that is a
   paragraph or a media block (the media blocks' own 2.5rem top margin used to
   win the margin collapse, leaving the heading floating 40px above). */
.project-section-heading + .media-text-row,
.project-section-heading + .inline-media-pair,
.project-section-heading + .inline-media {
    margin-top: 0;
}

.media-text-row .media-text {
    flex: 1 1 0;
    min-width: 0;
}

/* Kill the trailing margin of the last text element so the column centres true */
.media-text-row .media-text > :last-child {
    margin-bottom: 0;
}

.media-text-row .inline-media {
    flex: 0 0 42%;
    max-width: 42%;
    margin: 0;
}

@media (max-width: 768px) {
    .media-text-row {
        flex-direction: column;
        align-items: stretch;
        gap: 1.5rem;
    }

    .media-text-row .inline-media {
        flex-basis: auto;
        max-width: 100%;
    }
}

.inline-3d-container {
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #0a0a0a;
    position: relative;
}

.media-caption {
    font-family: var(--font-caption);
    font-size: 0.85rem;
    color: var(--text-secondary);
    margin-top: 0.5rem;
    line-height: 1.4;
}

/* ================================================
   LIGHTBOX (inline project media)
   Clicking an .inline-media figure opens the image/clip enlarged in an
   overlay (built once by js/main.js). Inline videos stay UI-less on the
   page; the enlarged copy is the only place that shows player controls.
   ================================================ */

.inline-media.lightbox-enabled {
    cursor: zoom-in;
}

.lightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 1100;
    /* above header (105) and everything else */
    background: rgba(0, 0, 0, 0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.25s ease, visibility 0.25s ease;
    cursor: zoom-out;
}

.lightbox-overlay.open {
    opacity: 1;
    visibility: visible;
}

.lightbox-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    max-width: 92vw;
    max-height: 90vh;
    cursor: default;
}

.lightbox-content img,
.lightbox-content video {
    max-width: 92vw;
    max-height: 82vh;
    width: auto;
    height: auto;
    display: block;
    background: #000;
}

.lightbox-caption {
    font-family: var(--font-caption);
    font-size: 0.9rem;
    color: var(--text-secondary);
    text-align: center;
    max-width: 70ch;
}

.lightbox-close {
    position: fixed;
    top: 1rem;
    right: 1.5rem;
    background: transparent;
    border: none;
    color: var(--text-primary);
    font-size: 2.6rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.5rem;
    transition: color 0.3s ease;
}

.lightbox-close:hover {
    color: var(--accent-color);
}

/* ================================================
   VIDEO QUALITY — cog + menu (HLS films)
   Injected by js/hls-player.js. Desktop only: hidden at the mobile
   breakpoint, where playback stays adaptive with no manual control.
   ================================================ */
.video-settings {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 6;
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
}

/* .is-hidden = film is not the active carousel slide (or is an image slide) */
.video-settings.is-hidden {
    display: none;
}

/* Reveal on hover of the film, on keyboard focus, or while the menu is open. */
.project-media-container:hover .video-settings:not(.is-hidden),
.slideshow-container:hover .video-settings:not(.is-hidden),
.showreel-container:hover .video-settings:not(.is-hidden),
.video-settings:focus-within {
    opacity: 1;
    pointer-events: auto;
}

.video-settings-cog {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    color: rgba(255, 255, 255, 0.85);
    background: rgba(5, 5, 5, 0.6);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 6px;
    cursor: pointer;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: color 0.15s ease, background-color 0.15s ease, transform 0.25s ease;
}

.video-settings-cog:hover {
    color: #fff;
    background: rgba(5, 5, 5, 0.8);
}

.video-settings-cog.open {
    color: var(--accent-color);
    transform: rotate(45deg);
}

.video-settings-menu {
    position: absolute;
    top: 42px;
    right: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 116px;
    padding: 4px;
    border-radius: 6px;
    background: rgba(5, 5, 5, 0.82);
    border: 1px solid rgba(255, 255, 255, 0.14);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

.video-settings-menu[hidden] {
    display: none;
}

.video-settings-menu button {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    letter-spacing: 0.03em;
    text-align: left;
    color: rgba(255, 255, 255, 0.75);
    background: transparent;
    border: none;
    border-radius: 4px;
    padding: 7px 10px;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.12s ease, background-color 0.12s ease;
}

.video-settings-menu button:hover {
    color: #fff;
    background: rgba(255, 255, 255, 0.1);
}

.video-settings-menu button.active {
    color: var(--accent-color);
}

.video-settings-menu button.active::before {
    content: "\2713\00a0"; /* check mark + nbsp */
}

.video-settings-menu button:not(.active) {
    padding-left: 22px; /* align labels under the checked one */
}

/* The film slides and the showreel need to be a positioning context. */
.project-media-container,
.slideshow-container,
.showreel-container {
    position: relative;
}

/* Desktop-only feature: the cog is fiddly on a phone, so hide it there. */
@media (max-width: 768px) {
    .video-settings {
        display: none;
    }
}

/* ================================================
   PROJECT PAGES: ONE DESKTOP LAYOUT AT EVERY WIDTH (2026-10-13)
   The article used to be a share of the window while its text stayed 16px,
   so wide screens got wider instead of bigger and every media arrangement
   changed proportion. Now the article is a fixed design (50rem incl. padding,
   ~85 characters per line) that ZOOMS with the window: js/article-zoom.js
   sets --article-zoom (1 at 1440px, ~1.38 at 2560, ~1.83 at 3840, capped).
   Only the article zooms - header, nav and logo stay exactly as on the home
   page (a first version scaled the root font size, which zoomed the whole
   UI on project pages only). Tablet/mobile (<=1150px) are untouched.
   ================================================ */
@media (min-width: 1151px) {
    .project-detail-main .content-section {
        zoom: var(--article-zoom, 1);
        /* in zoomed units: 50rem of design width, or the old 55% of the
           window when that is narrower (keeps the 3D models' side room) */
        max-width: min(calc(55vw / var(--article-zoom, 1)), 50rem);
        padding: 6rem 2rem;
    }

    /* The title starts at the article's zoomed size and, as the page scrolls,
       scales to the home page's heading size (2.5rem at zoom 1): scale runs
       from 1 to 1/zoom with --title-p (js/article-zoom.js). Its sticky offset
       is un-zoomed so a stuck title sits on the logo row exactly like the home
       page headings. */
    .project-detail-main .sticky-project-title {
        top: calc(1.4rem / var(--article-zoom, 1));
        transform: scale(calc(1 - var(--title-p, 0) * (1 - 1 / var(--article-zoom, 1))));
    }
}

/* Inline videos keep their own proportions (the 3:2 Throwaways clips were
   cropped to 16:9 before); pairs of equal clips still line up. */
.inline-media-video video {
    aspect-ratio: auto;
    object-fit: contain;
}

/* Text beside a PORTRAIT image: the image takes less of the row. */
.media-text-row.media-text-portrait .inline-media {
    flex: 0 0 30%;
    max-width: 30%;
}

.media-text-row.media-text-portrait .inline-media-image img {
    max-height: none;
}

/* Inline carousel (e.g. Hijacked's vehicle set): the header slideshow
   component inside the article. Not an .inline-media figure, so the
   click-to-enlarge lightbox stays off its arrows. */
.inline-carousel {
    margin: 2.5rem 0;
}

.inline-carousel .project-media-container {
    margin-bottom: 0.5rem;
}

.inline-carousel .slideshow-dots {
    margin-bottom: 0.25rem;
}

@media (max-width: 768px) {
    .media-text-row.media-text-portrait .inline-media {
        flex-basis: auto;
        max-width: 70%;
        margin: 0 auto;
    }
}

/* Text beside media at an even split (e.g. Throwaways' intro). */
.media-text-row.media-text-half .inline-media {
    flex: 0 0 calc(50% - 1rem);
    max-width: calc(50% - 1rem);
}

@media (max-width: 768px) {
    .media-text-row.media-text-half .inline-media {
        flex-basis: auto;
        max-width: 100%;
    }
}

/* Pairs keep every image's own proportions (2026-10-13): no forced 16:9
   crop. Each figure's share of the row is its aspect ratio (--ar, written
   into the page per image), so both sides end at the same height and a
   portrait image simply gets a narrower column. Mobile still stacks. */
.inline-media-pair .inline-media {
    flex: var(--ar, 1) 1 0;
}

.inline-media-pair .inline-media-image img,
.inline-media-pair .inline-media-video video {
    /* the figure's own ratio reserves the height before the file loads */
    aspect-ratio: var(--ar, auto);
    object-fit: contain;
    max-height: none;
}

/* Only the gear turns when the quality menu opens, not its square button. */
.video-settings-cog.open {
    transform: none;
}

.video-settings-cog svg {
    transition: transform 0.25s ease;
}

.video-settings-cog.open svg {
    transform: rotate(45deg);
}

/* Wide screens: the nav sits closer to the right edge (the logo already
   anchors the left), leaving more of the header band for docked titles. */
@media (min-width: 1600px) {
    header {
        padding-right: 5%;
    }
}

/* A standalone PORTRAIT image (e.g. Hijacked's tower): centred at its own
   proportions instead of a full-width box with black side bars. */
.inline-media-portrait {
    width: fit-content;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
}

.inline-media-portrait img {
    width: auto;
    max-width: 100%;
    max-height: 75vh;
}

/* Still image of a 3D model (js/project_3d.js shows it instead of the live
   scene on devices without usable WebGL, with software rendering, or with
   reduced motion). Rendered wider than any half-screen with the scene's 45°
   vertical field of view, so scaling it to the container height and centring
   it reproduces the live camera's framing at any window shape. */
.project-3d-container {
    overflow: hidden;
}
.project-3d-still {
    position: absolute;
    top: 0;
    left: 50%;
    height: 100%;
    width: auto;
    max-width: none;
    transform: translateX(-50%);
    opacity: 0;
    transition: opacity 1.5s ease-in-out;
    pointer-events: none;
    user-select: none;
}

/* Team / credits at the end of group-project articles (Echoes, Throwaways): one
   clickable box per person, linking to their LinkedIn (plain link, no embed).
   Same hover language as the contact cards: green border, faint green tint, lift. */
.project-description .team-list {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
}
.team-card {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    height: 100%;
    padding: 1.1rem 1.25rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: inherit;
    text-decoration: none;
    transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease;
}
.team-card:hover,
.team-card:focus-visible {
    background: rgba(1, 197, 11, 0.05);
    border-color: var(--accent-color);
    transform: translateY(-3px);
}
.team-card .team-name {
    font-family: var(--font-heading);
    font-weight: 700;
    color: var(--text-primary);
}
.team-card .team-role {
    font-family: var(--font-body);
    font-size: 0.9rem;
    line-height: 1.45;
    color: var(--text-secondary);
}
.team-card .team-cta {            /* "LinkedIn" + the profile-link chevron */
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: auto;
    padding-top: 0.5rem;
    font-family: var(--font-heading);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent-color);
}
.team-card .team-cta::after {
    content: "";
    width: 0.45em;
    height: 0.45em;
    border-top: 0.15em solid currentColor;
    border-right: 0.15em solid currentColor;
    transform: rotate(45deg);
    transition: transform 0.3s ease;
}
.team-card:hover .team-cta::after {
    transform: translateX(3px) rotate(45deg);
}
