@import url('https://fonts.googleapis.com/css2?family=Italiana&family=Outfit:wght@300;400;600&family=Playfair+Display:ital,wght@1,500&display=swap');

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Suppress the translucent box mobile browsers paint over a link/button on tap (a blue
   flash on Android Chrome / WebView, a grey one on iOS Safari). Every interactive control
   on this page already has its own :active / :focus-visible feedback, so the native
   highlight is pure visual noise. Set on both html and * — WebKit treats the property as
   inherited, but some Android WebViews only honour it when it lands directly on the
   tapped element. `transparent` computes to rgba(0,0,0,0); spelled out for clarity. */
html { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
* { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }

/* Action controls carry no text worth selecting, and a tap that starts a text selection on
   one of them produces the same stray highlight/callout the rule above is meant to kill.
   Scoped to buttons and nav/action elements only — body copy, the contact form, and the
   GitHub handle stay selectable. */
button,
.nav-link,
.filter-btn,
.project-btn,
.contact-btn,
.menu-toggle,
.social-icon,
.cmdk-item,
.mobile-menu-link {
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    user-select: none;
}

html, body {
    width: 100%;
    margin: 0;
    padding: 0;
    background-color: #000000;
    -ms-overflow-style: none;
    scrollbar-width: none;
}

body {
    font-family: 'Outfit', sans-serif;
    color: #ffffff;
    overflow-x: hidden;
    touch-action: pan-y;
}

::-webkit-scrollbar { display: none; width: 0; }

/* Custom cursor only on fine-pointer devices — see bootstrap JS for the class toggle */
html.has-fine-pointer, html.has-fine-pointer body { cursor: none; }
html.has-fine-pointer * { cursor: none !important; }
.cursor-inner { display: none; }
html.has-fine-pointer .cursor-inner { display: block; }

#webgl {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100vh; height: 100dvh;
    z-index: 1;
    outline: none;
    transition: filter 0.1s linear;
}

/* ---- Skip link ---- */
/* Reuses .nav-link so it hooks into the existing nav-click handler (setupNavigation queries
   .nav-link[data-target-fraction]) for free — no extra JS needed. Off-screen until focused,
   the standard accessible "skip navigation" pattern. */
.skip-link {
    position: fixed;
    top: -60px; left: 16px;
    z-index: 10000;
    padding: 12px 20px;
    background: #ffffff;
    color: #000000 !important;
    border-radius: 8px;
    text-transform: none;
    letter-spacing: 0.3px;
    font-size: 13px;
    pointer-events: auto;
    transition: top 0.2s ease;
}
.skip-link:focus-visible {
    top: 16px;
}

/* ---- Focus visibility ---- */
/* No focus styling existed anywhere before this — browser defaults are inconsistent across
   the site's pill-shaped buttons/links on a pure black background, so keyboard focus was
   hard to see or invisible depending on element and browser. One consistent treatment for
   every interactive control instead of relying on default outlines. */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible {
    outline: 2px solid #ffb347;
    outline-offset: 3px;
    border-radius: 4px;
}
.contact-btn:focus-visible, .project-btn:focus-visible, .filter-btn:focus-visible {
    outline-offset: 2px;
}
.social-icon:focus-visible, .modal-close:focus-visible, .menu-toggle:focus-visible {
    outline-offset: 2px;
    border-radius: 50%;
}

/* ---- Screen-reader-only utility ---- */
/* Standard visually-hidden pattern: present in the accessibility tree and readable by AT,
   invisible and unable to affect layout for sighted users. clip+1px sizing (not
   display:none/visibility:hidden, which pull an element out of the tree entirely) is what
   keeps it announced by a screen reader. Used by the aria-live status regions below. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---- Custom cursor ---- */
/* A single small dot that tracks the pointer 1:1 (positioned from the mousemove handler in
   main.js). Fine-pointer devices only — see the .has-fine-pointer toggle above. */
.cursor-inner {
    position: fixed; top: 0; left: 0;
    width: 6px; height: 6px;
    border: 2px solid rgba(255, 255, 255, 1);
    border-radius: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 9999;
}

/* ---- Cinematic overlay layer ---- */
.cinematic-container {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100vh; height: 100dvh;
    z-index: 10;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 0 60px 40px 60px;
    box-sizing: border-box;
}

/* ---- Header ---- */
.main-header {
    display: flex; justify-content: space-between; align-items: center;
    width: 100%; z-index: 20;
    pointer-events: auto;
    padding: 20px 0;
}
.brand { font-size: 14px; font-weight: 600; letter-spacing: 5px; text-transform: uppercase; }
.header-nav { display: flex; align-items: center; gap: 24px; }
.nav-link {
    color: #ffffff; text-decoration: none;
    font-size: 10px; text-transform: uppercase; letter-spacing: 2px;
    font-weight: 500; transition: color 0.3s;
    white-space: nowrap;
}
.nav-link:hover { color: #ffffff; }
.nav-dot { width: 3px; height: 3px; background: rgba(255, 255, 255, 0.2); border-radius: 50%; flex-shrink: 0; }
.header-actions { display: flex; align-items: center; gap: 22px; flex-shrink: 0; }
.contact-btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 24px;
    background: #ffffff; color: #000000;
    text-decoration: none;
    font-size: 10px; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;
    border-radius: 20px;
    /* translate() = magnetic-pull offset from setupMagneticButtons (JS sets --magnet-x/y);
       scale() = the existing hover lift, now a toggled var so the two compose cleanly. */
    transform: translate(var(--magnet-x, 0px), var(--magnet-y, 0px)) scale(var(--btn-scale, 1));
    transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s;
    flex-shrink: 0;
}
.btn-circle {
    display: inline-block; width: 6px; height: 6px;
    border: 1px solid #000000; border-radius: 50%; background: transparent;
}
.contact-btn:hover { --btn-scale: 1.05; background: #f0f0f0; }

/* ---- Command palette trigger (header) ---- */
/* A quiet "search box that isn't one" — click opens the real Cmd/Ctrl+K palette (see
   setupCommandPalette in main.js) rather than typing inline. Hidden ≤767px alongside
   .header-nav/.header-social (see Responsiveness); the mobile menu's own "Search & Commands"
   row covers touch instead. */
.cmdk-trigger {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 8px 10px 8px 13px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 20px;
    color: rgba(255, 255, 255, 0.65);
    flex-shrink: 0;
    transition: border-color 0.3s, color 0.3s, background 0.3s;
}
.cmdk-trigger:hover { border-color: rgba(255, 255, 255, 0.32); color: #ffffff; background: rgba(255, 255, 255, 0.07); }
.cmdk-trigger__label {
    font-family: 'Outfit', sans-serif;
    font-size: 10.5px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 1.5px;
}
.cmdk-trigger__kbd { display: flex; gap: 3px; }
.cmdk-trigger__kbd kbd {
    font-family: ui-monospace, 'SF Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
    font-size: 10px;
    line-height: 1;
    padding: 3px 5px;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.14);
    color: rgba(255, 255, 255, 0.7);
}

/* ---- Header social icons ---- */
/* A vertical divider (not just a margin) separates the social row from the Contact button
   so the two read as distinct groups instead of icons crowding the button. */
.header-social {
    display: flex; align-items: center; gap: 16px;
    padding-right: 22px;
    border-right: 1px solid rgba(255, 255, 255, 0.15);
}
.header-social .social-icon {
    display: flex; color: #ffffff; opacity: 0.7;
    transition: opacity 0.3s, transform 0.3s;
}
.header-social .social-icon:hover { opacity: 1; transform: translateY(-2px); }

/* ---- Mobile hamburger + menu panel ---- */
/* Hidden on desktop — .header-nav and .header-social already handle that width comfortably.
   Shown ≤767px (see Responsiveness) in place of the nav row + social row, which don't fit
   next to the brand and Contact button at that width. */
.menu-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 5px;
    width: 36px; height: 36px;
    padding: 0;
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 10px;
    flex-shrink: 0;
}
.menu-toggle-bar {
    width: 16px; height: 1.5px;
    background: #ffffff;
    border-radius: 1px;
    transition: transform 0.3s ease, opacity 0.3s ease;
}
.menu-toggle.open .menu-toggle-bar:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.menu-toggle.open .menu-toggle-bar:nth-child(2) { opacity: 0; }
.menu-toggle.open .menu-toggle-bar:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* Dims the rest of the page and closes the menu on tap — same pattern as the contact modal's
   overlay, so the interaction feels consistent across the site. */
.mobile-menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 85;
    background: rgba(0, 0, 0, 0.55);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-menu-backdrop.open { opacity: 1; visibility: visible; }

/* A compact card anchored under the hamburger button (--mobile-header-height, set in JS from
   the header's real rendered height) — not a full-bleed panel stretching edge to edge, so it
   reads as a deliberate floating menu at any phone/tablet width instead of a bar that grows
   thin-and-wide as the viewport does. The top hairline reuses the site's own forge palette
   (molten amber → rim-light blue, same duotone as the tech-pill glows) instead of introducing
   a new accent color just for this panel. */
.mobile-menu-panel {
    position: fixed;
    top: calc(var(--mobile-header-height, 76px) + 10px);
    right: 16px;
    z-index: 90;
    width: min(320px, calc(100vw - 32px));
    max-height: calc(100dvh - var(--mobile-header-height, 76px) - 26px);
    overflow-y: auto;
    padding: 22px 20px 18px;
    background: rgba(9, 8, 7, 0.92);
    backdrop-filter: blur(28px) saturate(140%);
    -webkit-backdrop-filter: blur(28px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 20px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px) scale(0.98);
    transform-origin: top right;
    transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), visibility 0.3s ease;
}
.mobile-menu-panel::before {
    content: '';
    position: absolute;
    top: 0; left: 20px; right: 20px;
    height: 1px;
    background: linear-gradient(90deg, rgba(255, 179, 71, 0.75), rgba(120, 190, 255, 0.75));
}
.mobile-menu-panel.open { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }

/* Plain, unadorned list — no eyebrow label (the hamburger-to-X toggle already says "this is
   the menu", a caption restating that is just noise) and no numbering (reads as a gimmick on
   a recruiter-facing portfolio rather than information). The only affordance is a quiet arrow
   that shifts into place on tap, the same "this row navigates somewhere" convention used in
   plain settings/nav lists everywhere — legible at a glance, nothing to explain. */
.mobile-menu-links { display: flex; flex-direction: column; }
.mobile-menu-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 16px 2px;
    color: #fff6ed;
    text-decoration: none;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    transition: padding-left 0.25s ease;
}
.mobile-menu-link:last-child { border-bottom: none; }
.mobile-menu-link:active { padding-left: 8px; }

/* #mobile-menu-cmdk is a <button> borrowing the row layout above — every other .mobile-menu-link
   is an <a>. Strip the native button chrome (grey ButtonFace background, full border, centred
   system-font text) that would otherwise render it as an unreadable near-white box. */
button.mobile-menu-link {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    background: transparent;
    border: none;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.mobile-menu-label {
    font-family: 'Italiana', serif;
    font-size: 21px;
    letter-spacing: 0.5px;
}
.mobile-menu-arrow {
    font-family: 'Outfit', sans-serif;
    font-size: 15px;
    color: rgba(255, 255, 255, 0.28);
    transform: translateX(0);
    transition: transform 0.25s ease, color 0.25s ease;
}
.mobile-menu-link:active .mobile-menu-arrow {
    color: rgba(255, 200, 130, 0.9);
    transform: translateX(4px);
}

.mobile-menu-social {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-menu-social .social-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px; height: 40px;
    color: #ffffff;
    opacity: 0.75;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 50%;
    transition: opacity 0.25s, border-color 0.25s, transform 0.2s;
}
.mobile-menu-social .social-icon:active { opacity: 1; border-color: rgba(255, 255, 255, 0.4); transform: scale(0.94); }

/* ---- Slides ---- */
.slide { position: absolute; bottom: 12%; pointer-events: none; }

#slide-1 { left: 0; width: 100%; }
#slide-1 .slide-title { margin-left: 60px; }
#slide-1 .desc-row { position: relative; display: flex; width: 100%; }
#slide-1 .col-1 {
    margin-left: 60px;
    width: calc(25vw - 60px);
    max-width: calc(25vw - 60px);
}
#slide-1 .col-2 {
    position: absolute;
    left: calc(25vw + 40px);
    width: calc(25vw - 60px);
    max-width: calc(25vw - 60px);
}

#slide-2 { left: 0; width: 100%; }
#slide-2-img {
    position: fixed;
    top: 90px;
    left: 60px;
    width: calc(25vw - 60px);
    aspect-ratio: 1 / 1;
    overflow: hidden;
    z-index: 2;
    pointer-events: none;
    clip-path: inset(0 0 100% 0);
    transition: clip-path 1.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease;
    opacity: 0;
}
/* transform/transition stay on img only — picture is just a sizing wrapper here, and
   stacking the same scale() on both would compound into a much bigger zoom than intended. */
#slide-2-img picture {
    display: block;
    width: 100%; height: 100%;
}
#slide-2-img img {
    display: block;
    width: 100%; height: 100%;
    object-fit: cover;
    transform: scale(1.15);
    transition: transform 1.8s cubic-bezier(0.16, 1, 0.3, 1);
}
#slide-2-img.active { clip-path: inset(0 0 0 0); opacity: 1; }
#slide-2-img.active img { transform: scale(1); }

#slide-2 .slide-title { margin-left: calc(25vw + 40px); }
#slide-2 .slide-desc {
    margin-left: calc(50vw + 20px);
    width: calc(25vw - 60px);
    max-width: calc(25vw - 60px);
}

#slide-3 {
    left: calc(50vw + 20px);
    width: calc(25vw - 60px);
    max-width: calc(25vw - 60px);
}
#slide-3 .slide-title { white-space: nowrap; }
/* Deliberately escapes its narrow column (like the other slide titles), but nowrap text has
   no wrap-based safety net — clamp() keeps it inside the actual viewport edge at every
   desktop width down to 1024px instead of relying on word choice alone. Scoped to ≥1024px
   only so it never fights the tablet/mobile clamp() overrides further down. */
@media (min-width: 1024px) {
    #slide-3 .slide-title { font-size: clamp(72px, 9vw, 116px); }
}

#slide-4 { left: calc(25% + 40px); max-width: 600px; }

.slide-title {
    font-family: 'Italiana', serif;
    font-size: 116px;
    line-height: 1.0;
    font-weight: 400;
    letter-spacing: 2px;
    margin-bottom: 20px;
    color: #fff6ed;
}
.slide-desc {
    font-size: 16px; line-height: 1.7; font-weight: 300;
    color: #d1d5db; letter-spacing: 0.5px;
    opacity: 0;
    transform: translateY(30px);
    transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
                transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
    transition-delay: 0.4s;
}
.slide.active .slide-desc { opacity: 1; transform: translateY(0); }

.slide-title .char {
    display: inline-block;
    opacity: 0;
    transform: translateY(50px);
    filter: blur(12px);
    transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
                transform 0.8s cubic-bezier(0.25, 1, 0.5, 1),
                filter 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide.active .slide-title .char { opacity: 1; transform: translateY(0); filter: blur(0); }
.slide.active { pointer-events: auto; }

.slide, .slide-image-mask { transition: opacity 0.6s ease; }

/* ---- Grid overlay ---- */
.story-dashes {
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    display: flex; flex-direction: column; gap: 12px;
}
.story-dash { width: 2px; height: 40px; background: rgba(255, 255, 255, 0.15); border-radius: 2px; }
.story-dash-fill { width: 100%; height: 0%; background: #ffffff; border-radius: 2px; }

.grid-horizontal-line {
    position: fixed; top: 70px; left: 0; right: 0;
    width: 100%; height: 1px;
    background: rgba(255, 255, 255, 0.12);
    z-index: 5; pointer-events: none;
    transition: opacity 0.3s linear;
}
.grid-lines {
    position: fixed; top: 0; left: 40px; right: 40px;
    width: calc(100% - 80px); height: 100vh; height: 100dvh;
    display: flex; justify-content: space-between;
    z-index: 5; pointer-events: none;
    transition: opacity 0.3s linear;
}
.grid-line {
    position: relative; width: 1px; height: 100%;
    background: rgba(255, 255, 255, 0.12);
}
.grid-line:nth-child(3) { margin-top: 70px; height: calc(100% - 70px); }
.grid-dot {
    position: absolute; left: 50%;
    transform: translate(-50%, -50%);
    width: 6px; height: 6px; border-radius: 50%;
    background: rgba(255, 255, 255, 0.35);
    box-shadow: 0 0 4px rgba(255, 255, 255, 0.15);
}

/* ---- 3D model loading indicator ---- */
/* Sits above the canvas but stays transparent (no full-screen cover) so the sparks/shader
   backdrop keeps being visible while the ~600KB horse model streams in — the ambient scene
   is part of the experience, not something to hide behind a blank loading screen. */
.model-loader {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    pointer-events: none;
    opacity: 1;
    transition: opacity 0.5s ease;
}
.model-loader.hidden { opacity: 0; }
.model-loader-ring {
    width: 36px; height: 36px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.15);
    border-top-color: rgba(255, 200, 130, 0.85);
    animation: model-loader-spin 0.9s linear infinite;
}
.model-loader-label {
    font-family: 'Outfit', sans-serif;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.6);
    white-space: nowrap;
}
.model-loader.error .model-loader-ring { display: none; }
.model-loader.error .model-loader-label { color: rgba(255, 180, 180, 0.8); }
@keyframes model-loader-spin { to { transform: rotate(360deg); } }

/* ---- Reveal / transition tint ---- */
.reveal-tint {
    position: fixed; inset: 0;
    background: #000000;
    opacity: 0;
    z-index: 8;
    pointer-events: none;
}

/* ---- Hero scroll spacer + project section ---- */
.hero-scroll-spacer {
    width: 100%;
    height: var(--hero-scroll-vh, 900vh);
}

.project-section {
    position: relative;
    z-index: 15;
    width: 100%;
    padding: 140px 60px 160px;
    box-sizing: border-box;
    background: #000000;
}
.project-section-header {
    max-width: 720px;
    margin: 0 auto 60px;
    text-align: center;
}
.project-section-title {
    font-family: 'Italiana', serif;
    font-size: clamp(40px, 6vw, 72px);
    color: #fff6ed;
    margin-bottom: 16px;
}
.project-section-subtitle {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 15px;
    color: #d1d5db;
    letter-spacing: 0.5px;
}

/* ---- Live GitHub stats widget ---- */
/* A compact glass pill, same language as .project-filter-bar just below it. See
   setupGithubStats() in main.js for the fetch/cache logic — this only styles the states:
   plain numbers by default, and a small live dot that only turns green and pulses once a
   fetch has actually succeeded this session (.is-live). There is deliberately no error/loading
   state to style — the widget always shows a real number (cached or seeded), never a spinner
   or a blank. */
.github-stats {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: center;
    gap: 16px;
    margin: 22px auto 0;
    padding: 10px 20px;
    background: rgba(10, 10, 10, 0.45);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 20px;
    font-family: 'Outfit', sans-serif;
}
.github-stats__link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: #fff6ed;
    text-decoration: none;
    opacity: 0.85;
    transition: opacity 0.3s;
}
.github-stats__link:hover { opacity: 1; }
.github-stats__handle { font-size: 12px; font-weight: 600; letter-spacing: 0.3px; }
.github-stats__divider { width: 1px; height: 16px; background: rgba(255, 255, 255, 0.14); }
.github-stats__stat {
    display: flex;
    align-items: baseline;
    gap: 5px;
    font-size: 11.5px;
    font-weight: 300;
    color: rgba(255, 255, 255, 0.5);
    letter-spacing: 0.3px;
}
.github-stats__stat strong {
    font-size: 14px;
    font-weight: 600;
    color: #fff6ed;
    font-variant-numeric: tabular-nums;
}
.github-stats__live {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.22);
    flex-shrink: 0;
    transition: background 0.4s ease;
}
.github-stats__live.is-live {
    background: #34d399;
    animation: github-stats-pulse 2.4s ease-out infinite;
}
@keyframes github-stats-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55); }
    70%  { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); }
    100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .github-stats__live.is-live { animation: none; }
}
html.motion-off .github-stats__live.is-live { animation: none; }

/* ---- Project filter bar ---- */
.project-filter-bar {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    max-width: 1400px;
    margin: 0 auto 40px;
    padding: 14px 18px;
    background: rgba(10, 10, 10, 0.45);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 20px;
}
.filter-btn {
    padding: 8px 18px;
    border-radius: 20px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    background: transparent;
    color: #e5e5e5;
    font-family: 'Outfit', sans-serif;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 500;
    transform: translate(var(--magnet-x, 0px), var(--magnet-y, 0px));
    transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s, color 0.3s, border-color 0.3s;
}
.filter-btn:hover { border-color: rgba(255, 255, 255, 0.4); }
.filter-btn.active { background: #ffffff; color: #000000; border-color: #ffffff; }

.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 28px;
    max-width: 1400px;
    margin: 0 auto;
    transition: opacity 0.3s ease;
}
.project-grid.is-filtering { opacity: 0; }

/* ---- Featured / Other split ---- */
/* Featured work is always fully shown; everything else is paginated behind "Show More"
   so the section reads as a curated highlight reel first, full archive second. */
.project-subsection-title {
    max-width: 1400px;
    margin: 0 auto 24px;
    font-family: 'Outfit', sans-serif;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: rgba(255, 255, 255, 0.55);
}
.project-featured-section { margin-bottom: 72px; }
/* Scoped to a min-width so it can never outrank the <480px single-column override below —
   that rule and this one would otherwise tie on specificity and fight over source order. */
@media (min-width: 480px) {
    .project-featured-section .project-grid-featured {
        grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    }
}
.project-other-section .project-subsection-title { color: rgba(255, 255, 255, 0.35); }

.project-empty-state {
    display: none;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    max-width: 1400px;
    margin: 0 auto;
    padding: 60px 20px;
    text-align: center;
}
.project-empty-state h3 { font-family: 'Italiana', serif; font-size: 24px; color: #fff6ed; }
.project-empty-state p { font-size: 14px; color: #d1d5db; font-weight: 300; }

.show-more-wrapper { display: none; justify-content: center; margin-top: 40px; }
.show-more-wrapper .project-btn { cursor: pointer; }

.project-card {
    position: relative;
    isolation: isolate;   /* own stacking context — keeps .project-card__glare behind the text */
    background: rgba(10, 10, 10, 0.45);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 18px;
    padding: 32px;
    opacity: 0;
    /* Scroll-in entrance rides the `translate` property; `transform` is left free for the
       pointer-tracked tilt (setupCardTilt sets --tilt-x / --tilt-y). The two never fight. */
    translate: 0 40px;
    transform: perspective(900px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
    transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                translate 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                transform 0.2s ease-out,
                border-color 0.4s, background 0.4s;
}
.project-card.in-view { opacity: 1; translate: 0 0; }
.project-card:hover {
    border-color: rgba(255, 255, 255, 0.28);
    background: rgba(15, 15, 15, 0.6);
}

/* Pointer-tracked glare for the card tilt. Transparent until setupCardTilt adds .is-tilting
   (JS-only, so reduced-motion / touch never see it); the highlight follows the cursor via
   the --glare-x / --glare-y custom properties. */
.project-card__glare {
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.35s ease;
    background: radial-gradient(300px circle at var(--glare-x, 50%) var(--glare-y, 50%),
        rgba(255, 224, 189, 0.13), rgba(255, 224, 189, 0) 55%);
}
.project-card.is-tilting {
    transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                translate 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                transform 0.08s linear,
                border-color 0.4s, background 0.4s;
}
.project-card.is-tilting .project-card__glare { opacity: 1; }

/* Brief amber pulse ring — the command palette's "you asked for this card" cue when jumping
   to a project (see jumpToProject in main.js). Class is added and removed by JS on a plain
   timer rather than an 'animationend' listener, so it clears itself the same way whether or
   not the animation actually ran (see the reduced-motion override just below). */
@keyframes cmdk-jump-highlight {
    0%   { box-shadow: 0 0 0 0 rgba(255, 179, 71, 0.55); border-color: rgba(255, 199, 143, 0.9); }
    100% { box-shadow: 0 0 0 16px rgba(255, 179, 71, 0); border-color: rgba(255, 255, 255, 0.1); }
}
.project-card.cmdk-jump-highlight { animation: cmdk-jump-highlight 1.4s cubic-bezier(0.16, 1, 0.3, 1); }
@media (prefers-reduced-motion: reduce) {
    .project-card.cmdk-jump-highlight { animation: none; outline: 2px solid rgba(255, 179, 71, 0.7); outline-offset: 3px; }
}
html.motion-off .project-card.cmdk-jump-highlight { animation: none; outline: 2px solid rgba(255, 179, 71, 0.7); outline-offset: 3px; }

.project-card-featured { border-color: rgba(255, 200, 130, 0.22); }
.project-card-featured:hover { border-color: rgba(255, 200, 130, 0.45); }
.featured-badge {
    display: inline-block;
    margin-bottom: 14px;
    padding: 4px 12px;
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #1a1200;
    background: linear-gradient(135deg, #ffe4b8, #ffb347);
    border-radius: 20px;
}

/* ---- Headline cards: flagship + spotlight ---- */
/* Two cards carry the headline work (see buildProjectCard in main.js): `flagship: true` is the
   deployed, Play-Store-bound project, `spotlight: true` is the full-stack one beside it. This
   site's palette is a forge — molten amber (#ffb347) cooling to rim-light blue (#78beff) — so
   the pair takes one end of it each: the flagship burns amber, the spotlight cools to blue.
   Everything about the treatment is shared and lives on .project-card-major: a single ember of
   heat travelling the card's rim on a slow loop over a dim hairline, plus a low glow lifting the
   card off the black. One moving element per card, nothing else. Each tier only supplies colour,
   as custom properties on .tier-flagship / .tier-spotlight — so a third tier would be a block of
   variables, not a second copy of the mechanism. The card and its case-study modal both wear the
   tier class, which is why the variables sit on a class of their own rather than on the card:
   custom properties inherit, so the modal's badge and standfirst pick up the same palette without
   a second set of rules naming each tier again. Frozen to a static gradient edge under
   prefers-reduced-motion, and to a solid border where mask-composite is unsupported. No other
   card, and no layout, is touched. */
@property --card-rim-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}

.tier-flagship {
    --rim-edge: rgba(255, 199, 143, 0.55);
    --rim-edge-hover: rgba(255, 199, 143, 0.8);
    --rim-halo: rgba(255, 150, 60, 0.28);
    --rim-halo-tight: rgba(255, 179, 71, 0.16);
    --rim-halo-hover: rgba(255, 150, 60, 0.4);
    --rim-halo-tight-hover: rgba(255, 179, 71, 0.24);
    /* The travelling gradient: two dim hairline stops, then the bright ember and its tail. */
    --rim-dim-a: rgba(255, 179, 71, 0.14);
    --rim-dim-b: rgba(120, 190, 255, 0.12);
    --rim-ember: rgba(255, 224, 189, 0.92);
    --rim-ember-tail: rgba(120, 190, 255, 0.45);
    --rim-badge: linear-gradient(135deg, #ffe9cf, #ff9d3d);
    --rim-badge-ink: #1a1200;
    --rim-badge-glow: rgba(255, 157, 61, 0.45);
    --rim-lead: #ffd9a8;
}

.tier-spotlight {
    --rim-edge: rgba(151, 205, 255, 0.5);
    --rim-edge-hover: rgba(173, 217, 255, 0.78);
    --rim-halo: rgba(70, 148, 255, 0.24);
    --rim-halo-tight: rgba(120, 190, 255, 0.16);
    --rim-halo-hover: rgba(70, 148, 255, 0.36);
    --rim-halo-tight-hover: rgba(120, 190, 255, 0.24);
    --rim-dim-a: rgba(120, 190, 255, 0.14);
    --rim-dim-b: rgba(255, 179, 71, 0.1);
    --rim-ember: rgba(226, 242, 255, 0.92);
    --rim-ember-tail: rgba(255, 179, 71, 0.38);
    --rim-badge: linear-gradient(135deg, #e6f3ff, #6fb6f5);
    --rim-badge-ink: #041826;
    --rim-badge-glow: rgba(111, 182, 245, 0.45);
    --rim-lead: #c6e3ff;
}

.project-card-major {
    /* Fallback edge for engines without mask-composite (overridden just below). */
    border-color: var(--rim-edge);
    box-shadow: 0 20px 55px -18px var(--rim-halo),
                0 0 34px -10px var(--rim-halo-tight);
    /* Mirrors .project-card: entrance on `translate`, tilt on `transform`. */
    transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                translate 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                transform 0.2s ease-out,
                border-color 0.4s, background 0.4s, box-shadow 0.4s;
}
.project-card-major.is-tilting {
    transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                translate 0.9s cubic-bezier(0.16, 1, 0.3, 1),
                transform 0.08s linear,
                border-color 0.4s, background 0.4s, box-shadow 0.4s;
}
.project-card-major:hover {
    border-color: var(--rim-edge-hover);
    box-shadow: 0 24px 60px -16px var(--rim-halo-hover),
                0 0 46px -8px var(--rim-halo-tight-hover);
}
.project-card-major .featured-badge {
    background: var(--rim-badge);
    color: var(--rim-badge-ink);
    box-shadow: 0 0 18px var(--rim-badge-glow);
}

/* The case study's own one-line hook, reused verbatim as the card's lead (see buildProjectCard).
   Set in the italic serif the stylesheet already imports but never used anywhere else — it reads
   as an editorial standfirst rather than a second paragraph, which is the whole job: it gives the
   card a middle register between the display-serif title and the Outfit body copy, so the prose
   below it stops being the only thing in the card. */
.project-lead {
    font-family: 'Playfair Display', Georgia, serif;
    font-style: italic;
    font-weight: 500;
    font-size: 17px;
    line-height: 1.45;
    color: var(--rim-lead, #ffd9a8);
    margin: -2px 0 18px;
    text-wrap: pretty;
}

/* A short lit rule where the lead hands over to the paragraph — the one piece of structure
   separating them, and it inherits the card's own tier colour rather than introducing another. */
.project-card-major .project-desc {
    position: relative;
    padding-top: 18px;
}
.project-card-major .project-desc::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 56px;
    height: 1px;
    background: linear-gradient(90deg, var(--rim-edge), transparent);
}

@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
    .project-card-major,
    .project-card-major:hover { border-color: transparent; }

    /* The rim: a conic gradient clipped to a 1px band by masking the content-box
       out of the padding-box, so it renders as a gradient *stroke* on the
       silhouette without a filled panel behind the glass. The bright wedge
       (~300-336deg) is the ember; the rest is a low-alpha hairline that just
       shimmers as the whole thing turns. Only ever paints the outer 1px, so it
       never reaches the text. */
    .project-card-major::before {
        content: '';
        position: absolute;
        inset: -1px;
        border-radius: 19px;
        padding: 1px;
        background: conic-gradient(from var(--card-rim-angle),
            var(--rim-dim-a) 0deg,
            var(--rim-dim-b) 130deg,
            var(--rim-dim-a) 250deg,
            var(--rim-ember) 300deg,
            var(--rim-ember-tail) 336deg,
            var(--rim-dim-a) 360deg);
        -webkit-mask: linear-gradient(#000 0 0) content-box,
                      linear-gradient(#000 0 0);
        -webkit-mask-composite: xor;
                mask: linear-gradient(#000 0 0) content-box,
                      linear-gradient(#000 0 0);
                mask-composite: exclude;
        animation: card-rim 8s linear infinite;
        pointer-events: none;
    }
}

@keyframes card-rim {
    to { --card-rim-angle: 360deg; }
}

@media (prefers-reduced-motion: reduce) {
    .project-card-major::before { animation: none; }
}
/* Mirrors the rule above for the in-page "Toggle Motion" command palette action
   (html.motion-off, set by setMotionOverride() in main.js). A class can't be matched from
   inside an @media (prefers-reduced-motion) block, so an explicit, page-local override needs
   its own copy of the same rule — see the larger mirrored block near the end of this file for
   the rest of them, and why it's a deliberate duplication rather than an oversight. */
html.motion-off .project-card-major::before { animation: none; }

.project-title {
    font-family: 'Italiana', serif;
    font-size: 26px;
    color: #fff6ed;
    margin-bottom: 12px;
}
.project-desc {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14.5px;
    line-height: 1.7;
    color: #d1d5db;
    margin-bottom: 20px;
}
.project-tech {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 24px;
}
.tech-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 20px;
    font-size: 11px;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: #e5e5e5;
}
.tech-icon { width: 12px; height: 12px; display: block; }
.tech-language { box-shadow: 0 0 10px rgba(255, 150, 60, 0.18); }
.tech-tool { box-shadow: 0 0 10px rgba(120, 190, 255, 0.18); }
.tech-language .tech-icon { filter: drop-shadow(0 0 3px rgba(255, 150, 60, 0.6)); }
.tech-tool .tech-icon { filter: drop-shadow(0 0 3px rgba(120, 190, 255, 0.6)); }

.project-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.project-btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 22px;
    background: #ffffff; color: #000000;
    text-decoration: none;
    font-size: 10px; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;
    border-radius: 20px;
    transform: translate(var(--magnet-x, 0px), var(--magnet-y, 0px)) scale(var(--btn-scale, 1));
    transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s;
}
.project-btn:hover { --btn-scale: 1.05; background: #f0f0f0; }
.project-btn-outline {
    background: transparent; color: #ffffff;
    border: 1px solid rgba(255, 255, 255, 0.5);
}
.project-btn-outline:hover { background: rgba(255, 255, 255, 0.1); }
.project-btn-outline .btn-circle { border-color: #ffffff; }

/* ---- Contact modal ---- */
.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(0, 0, 0, 0.65);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal-overlay.open { opacity: 1; visibility: visible; }

.contact-modal {
    position: relative;
    width: 100%;
    max-width: 460px;
    background: rgba(10, 10, 10, 0.7);
    backdrop-filter: blur(28px) saturate(140%);
    -webkit-backdrop-filter: blur(28px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 20px;
    padding: 40px;
    box-sizing: border-box;
    transform: scale(0.95) translateY(10px);
    opacity: 0;
    transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s ease;
}
.modal-overlay.open .contact-modal { transform: scale(1) translateY(0); opacity: 1; }

.modal-close {
    position: absolute;
    top: 20px; right: 20px;
    width: 32px; height: 32px;
    display: flex; align-items: center; justify-content: center;
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 50%;
    color: #ffffff;
    /* Explicit stack so the close button always sits above the scrollable modal body,
       even once that body gets its own compositing layer on mobile (see the perf block
       at the end of this file). Relying on paint order alone broke once will-change
       entered the picture. */
    z-index: 2;
    /* Kills the ~300ms tap delay and stops a tap being held as a possible pan/zoom start
       — the case-study close button was hard to dismiss on touch. */
    touch-action: manipulation;
    transform: translate(var(--magnet-x, 0px), var(--magnet-y, 0px)) rotate(var(--close-rot, 0deg));
    transition: background 0.3s, border-color 0.3s, transform 0.3s;
}
.modal-close:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.4); --close-rot: 90deg; }

.modal-title {
    font-family: 'Italiana', serif;
    font-size: 32px;
    color: #fff6ed;
    margin-bottom: 8px;
}
.modal-subtitle {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14px;
    color: #d1d5db;
    margin-bottom: 28px;
}

.contact-form { display: flex; flex-direction: column; gap: 18px; }
/* Spam honeypot — invisible to humans, real form fields stay untouched. A bot that fills
   every input in the DOM (rather than just the visible ones) fills this too, and FormSubmit
   silently discards the submission. */
.honeypot-field { display: none; }
.form-field { display: flex; flex-direction: column; gap: 8px; }
.form-field label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #d1d5db;
    font-weight: 500;
}
.form-field input, .form-field textarea {
    font-family: 'Outfit', sans-serif;
    font-size: 14px;
    color: #ffffff;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 10px;
    padding: 12px 14px;
    resize: vertical;
    transition: border-color 0.3s, background 0.3s;
}
.form-field input::placeholder, .form-field textarea::placeholder { color: rgba(255, 255, 255, 0.35); }
.form-field input:focus, .form-field textarea:focus {
    outline: none;
    border-color: rgba(255, 255, 255, 0.5);
    background: rgba(255, 255, 255, 0.08);
}

.contact-submit-btn {
    border: none;
    cursor: pointer;
    justify-content: center;
    width: 100%;
    transition: transform 0.3s, background 0.3s, opacity 0.3s;
}

.form-status-message {
    display: none;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    padding: 10px 14px;
    border-radius: 10px;
    opacity: 0;
    transition: opacity 0.4s ease;
}
.form-status-message.show { opacity: 1; }
.form-status-message.success { display: flex; color: #a7f3d0; background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.3); }
.form-status-message.error { display: flex; color: #fecaca; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); }

/* ============================================================
   Command palette (Cmd/Ctrl+K)
   ============================================================ */
/* A top-anchored "spotlight" rather than .modal-overlay's centered dialog — otherwise the same
   backdrop/blur language. See setupCommandPalette() in main.js. */
.cmdk-overlay {
    position: fixed;
    inset: 0;
    z-index: 110;
    display: flex;
    justify-content: center;
    padding: 12vh 20px 20px;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.25s ease, visibility 0.25s ease;
}
.cmdk-overlay.open { opacity: 1; visibility: visible; }

.cmdk-panel {
    position: relative;
    width: 100%;
    max-width: 560px;
    max-height: min(60vh, 520px);
    height: fit-content;
    display: flex;
    flex-direction: column;
    background: rgba(12, 11, 10, 0.85);
    backdrop-filter: blur(28px) saturate(140%);
    -webkit-backdrop-filter: blur(28px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 18px;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
    overflow: hidden;
    transform: translateY(-14px) scale(0.98);
    opacity: 0;
    transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
}
.cmdk-overlay.open .cmdk-panel { transform: translateY(0) scale(1); opacity: 1; }
/* Same forge hairline used on the mobile menu panel and the fast-path button — a consistent
   "this is a floating command surface" tell across the site rather than a one-off accent. */
.cmdk-panel::before {
    content: '';
    position: absolute;
    top: 0; left: 20px; right: 20px;
    height: 1px;
    background: linear-gradient(90deg, rgba(255, 179, 71, 0.75), rgba(120, 190, 255, 0.75));
}

.cmdk-input-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 16px 18px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    flex-shrink: 0;
}
.cmdk-search-icon { color: rgba(255, 255, 255, 0.4); flex-shrink: 0; }
.cmdk-input {
    flex: 1;
    min-width: 0;
    background: transparent;
    border: none;
    outline: none;
    color: #fff6ed;
    font-family: 'Outfit', sans-serif;
    font-size: 15px;
    font-weight: 300;
}
.cmdk-input::placeholder { color: rgba(255, 255, 255, 0.32); }

/* Dismiss control — an "Esc" pill on desktop, a round ✕ on touch (see the ≤767px block). */
.cmdk-dismiss {
    appearance: none;
    -webkit-appearance: none;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: 3px 7px;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.14);
    color: rgba(255, 255, 255, 0.5);
    cursor: pointer;
    transition: color 0.3s, border-color 0.3s, background 0.3s;
}
.cmdk-dismiss:hover { color: #ffffff; border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.1); }
.cmdk-dismiss__key {
    font-family: ui-monospace, 'SF Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
    font-size: 10px;
    line-height: 1;
}
.cmdk-dismiss__x { display: none; }

.cmdk-list { list-style: none; overflow-y: auto; padding: 8px; flex: 1 1 auto; }
.cmdk-group-label {
    padding: 10px 12px 6px;
    font-family: 'Outfit', sans-serif;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: rgba(255, 255, 255, 0.35);
}
.cmdk-item {
    display: flex;
    align-items: center;
    padding: 11px 12px;
    border-radius: 10px;
    cursor: pointer;
}
.cmdk-item[aria-selected="true"] { background: rgba(255, 255, 255, 0.08); }
.cmdk-item-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cmdk-item-title {
    font-family: 'Outfit', sans-serif;
    font-size: 13.5px;
    font-weight: 500;
    color: #fff6ed;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cmdk-item-sub {
    font-family: 'Outfit', sans-serif;
    font-size: 11.5px;
    font-weight: 300;
    color: rgba(255, 255, 255, 0.42);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cmdk-empty {
    padding: 30px 20px;
    text-align: center;
    font-family: 'Outfit', sans-serif;
    font-size: 13px;
    font-weight: 300;
    color: rgba(255, 255, 255, 0.4);
}

@media (max-width: 479px) {
    .cmdk-overlay { padding: 8vh 12px 12px; }
    .cmdk-panel { max-height: 70vh; }
}

/* ============================================================
   Responsiveness
   ============================================================ */

/* 1024–1439px: unchanged column math, reduced padding */
@media (max-width: 1439px) {
    .cinematic-container { padding: 0 40px 32px 40px; }
}

/* 768–1023px: tablet */
@media (max-width: 1023px) {
    #slide-1 .desc-row { flex-direction: column; gap: 20px; }
    #slide-1 .col-2 { position: static; width: 100%; max-width: 100%; margin-left: 0; }
    .cinematic-container { padding: 0 32px 28px 32px; }
    .slide-title { font-size: clamp(56px, 8vw, 96px); }
    .grid-line:nth-child(2), .grid-line:nth-child(3), .grid-line:nth-child(4) { display: none; }
}

/* 480–767px: mobile */
@media (max-width: 767px) {
    #slide-1, #slide-2, #slide-3, #slide-4 {
        left: 0; width: 100%; max-width: calc(100% - 48px); margin-left: 24px;
    }
    #slide-1 .slide-title, #slide-2 .slide-title { margin-left: 0; }
    #slide-1 .col-1, #slide-1 .col-2 {
        position: static; left: auto; margin-left: 0; width: 100%; max-width: 100%;
    }
    #slide-2 .slide-desc { margin-left: 0; width: 100%; max-width: 100%; }
    #slide-3 { max-width: calc(100% - 48px); }
    #slide-3 .slide-title { white-space: normal; }
    #slide-4 { max-width: calc(100% - 48px); }

    .slide-title { font-size: clamp(40px, 11vw, 64px); }

    .grid-lines, .grid-horizontal-line { display: none; }

    /* The full nav + social row doesn't fit next to the brand and Contact button at this
       width — both move into the hamburger-triggered .mobile-menu-panel below instead of
       being trimmed down to a single "Work" link. */
    .header-nav { display: none; }
    .header-social { display: none; }
    .cmdk-trigger { display: none; }
    .menu-toggle { display: flex; }

    /* Capped instead of scaling to full viewport width — on wide phones (large "Ultra"-class
       screens) a 1:1 square at near-full width grows tall enough to intrude into slide-2's own
       text block below it, which is independently anchored to bottom:12% of the viewport and
       has no awareness of a fixed-position element's height above it. */
    #slide-2-img { width: min(calc(100vw - 48px), 200px); left: 24px; top: 80px; }
}

/* modal fits narrow viewports */
@media (max-width: 479px) {
    .contact-modal { padding: 28px; }
}

/* The work section carried one 60px horizontal padding at every width, and nothing overrode it
   below 1024px. On a 375px phone that left 255px of card and — once the card's own 24px padding
   came off both sides — 207px of text: a ~30-character measure, which turned the flagship's
   paragraph into fifteen lines and the card into one tall thin column (measured, both before and
   after: 726px of card holding 370px of paragraph, against 670px holding 245px now). Every other full-width
   section on this page (the hero container, the anatomy section) already stepped its gutter down
   at these breakpoints; this one was simply missed. The numbers below are the gutter each
   breakpoint can afford, not a uniform shrink: the card's own padding comes down with it, since
   at 375px the two are spending from the same 375px. */
@media (max-width: 1023px) {
    .project-section { padding: 120px 40px 130px; }
}
@media (max-width: 767px) {
    .project-section { padding: 96px 24px 110px; }
    .project-section-header { margin-bottom: 44px; }
    .project-featured-section { margin-bottom: 56px; }
    .project-card { padding: 26px 22px; }

    /* Below ~40 characters a line, a 1.7 leading reads as gappy rather than airy, and every
       extra line is another screenful of scroll. `pretty` is what stops the last line of a
       paragraph landing as one orphaned word at this measure. */
    .project-desc { font-size: 14px; line-height: 1.62; text-wrap: pretty; }
    .project-title { font-size: 28px; }
    .project-lead { font-size: 16px; }

    /* 10px of vertical padding made these 33px tall — under the 44px WCAG 2.5.5 target, and they
       are the only things on the card anyone taps. On the two headline cards the three actions
       also stop wrapping 2-then-1 (which reads as an accident) and take a deliberate shape: the
       case study full width because it is the primary action, Live and Source sharing the row
       under it. `justify-content` is only needed once they are grid items being stretched. */
    .project-btn {
        min-height: 44px;
        padding: 13px 20px;
        justify-content: center;
    }
    .project-card-major .project-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 10px;
    }
    .project-card-major .project-btn-casestudy { grid-column: 1 / -1; }
}

/* A width query alone misses a phone in landscape — 900px+ wide on a modern handset, and still
   nothing but a fingertip to hit these with. The size is the only thing that follows the pointer;
   the layout above stays keyed to width, because that is what it is actually about. */
@media (pointer: coarse) {
    .project-btn { min-height: 44px; padding: 13px 20px; }
}

/* <480px */
@media (max-width: 479px) {
    .project-section { padding: 84px 18px 96px; }
    .project-card { padding: 24px 20px; }
    .project-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   Case study modal
   ============================================================ */
/* Reuses .modal-overlay / .contact-modal (backdrop, entrance transition, close button)
   from the contact modal. Only the width and an internal scroll region differ: the panel
   itself stays put so the close button never scrolls out of reach, while .case-study-body
   scrolls. Content is injected from a project's `caseStudy` object in projects.json — see
   renderCaseStudy() in main.js. */
.case-study-modal {
    max-width: 720px;
    padding: 40px 36px;
}
.case-study-body {
    max-height: min(72vh, 640px);
    overflow-y: auto;
    padding-right: 8px;
    margin-right: -8px;
}
.case-study-body .modal-title {
    margin-top: 2px;
    margin-bottom: 6px;
    padding-right: 44px; /* clears the absolutely-positioned close button on the first line */
}
.cs-tagline {
    font-family: 'Playfair Display', Georgia, serif;
    font-style: italic;
    font-weight: 500;
    font-size: 17px;
    line-height: 1.5;
    color: var(--rim-lead, #ffd9a8);
    margin-bottom: 26px;
    text-wrap: pretty;
}

/* The headline projects' modals carry .tier-flagship / .tier-spotlight (set in openCaseStudy),
   so the badge that opens the case study matches the badge on the card that opened it. Every
   other project inherits nothing and falls back to the original amber pill. */
.case-study-body .featured-badge {
    background: var(--rim-badge, linear-gradient(135deg, #ffe4b8, #ffb347));
    color: var(--rim-badge-ink, #1a1200);
    box-shadow: 0 0 18px var(--rim-badge-glow, transparent);
}
.cs-block { margin-bottom: 28px; }
.cs-block:last-of-type { margin-bottom: 20px; }
.cs-eyebrow {
    font-family: 'Outfit', sans-serif;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: rgba(255, 255, 255, 0.5);
    margin-bottom: 14px;
}
.cs-para {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14.5px;
    line-height: 1.75;
    color: #d1d5db;
}
.cs-para + .cs-para { margin-top: 12px; }

/* Architecture — a numbered vertical flow. Counter-driven, so the markup stays a plain
   <ol> with no hand-written numbers. */
.cs-arch { list-style: none; counter-reset: cs-step; }
.cs-arch-item {
    position: relative;
    counter-increment: cs-step;
    padding-left: 44px;
    padding-bottom: 22px;
}
.cs-arch-item:last-child { padding-bottom: 0; }
.cs-arch-item::before {
    content: counter(cs-step);
    position: absolute;
    left: 0;
    top: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Outfit', sans-serif;
    font-size: 12px;
    font-weight: 600;
    color: #1a1200;
    background: linear-gradient(135deg, #ffe4b8, #ffb347);
    border-radius: 50%;
}
/* connector line between the number badges */
.cs-arch-item:not(:last-child)::after {
    content: '';
    position: absolute;
    left: 13.5px;
    top: 30px;
    bottom: 0;
    width: 1px;
    background: linear-gradient(rgba(255, 179, 71, 0.4), rgba(120, 190, 255, 0.25));
}
.cs-arch-title {
    display: block;
    font-family: 'Outfit', sans-serif;
    font-size: 13.5px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: #fff6ed;
    margin-bottom: 4px;
}
.cs-arch-detail {
    display: block;
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14px;
    line-height: 1.7;
    color: #d1d5db;
}

/* Key decisions — choice / why / trade-off, with a warm left rule. */
.cs-decision {
    border-left: 2px solid rgba(255, 179, 71, 0.4);
    padding-left: 16px;
    margin-bottom: 18px;
}
.cs-decision:last-child { margin-bottom: 0; }
.cs-decision-choice {
    font-family: 'Outfit', sans-serif;
    font-size: 14px;
    font-weight: 600;
    color: #fff6ed;
    margin-bottom: 5px;
}
.cs-decision-rationale {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14px;
    line-height: 1.7;
    color: #d1d5db;
}
.cs-decision-tradeoff {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 13px;
    line-height: 1.6;
    color: #9aa0a6;
    margin-top: 6px;
}
.cs-decision-tradeoff span {
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: 10px;
    font-weight: 600;
    color: rgba(255, 179, 71, 0.75);
    margin-right: 6px;
}
.cs-status {
    font-family: 'Outfit', sans-serif;
    font-size: 12.5px;
    letter-spacing: 0.5px;
    color: rgba(255, 255, 255, 0.55);
    margin-bottom: 22px;
}
.cs-actions { margin-top: 4px; }

/* ============================================================
   Architecture diagram (flagship case study only)
   ============================================================ */
/* Pure SVG, built once per open by buildArchitectureDiagram() in main.js straight from that
   project's own `architecture` array — no separate diagram data to keep in sync. Every node
   and every connector is a fixed, known size (see the DIAGRAM_* constants in main.js), which
   is what lets every per-step value below be an nth-child rule or a plain SVG attribute set
   in the markup string, instead of an inline style="" attribute: this page's CSP has no
   'unsafe-inline' for style-src, and both Chrome and Firefox silently drop inline style=""
   attributes on markup injected via innerHTML under that policy. Marked aria-hidden on its
   wrapper — the <ol> immediately below already carries the same step names plus their full
   detail text, so the diagram is a visual restatement, not an additional source of
   information a screen reader needs to visit separately. */
/* Capped and centred rather than full-bleed: a flow diagram reads better no wider than the
   prose it sits among, and the cap keeps the SVG from scaling up to an oversized version of
   itself on a wide desktop modal while still filling the width on a phone. */
.cs-diagram { max-width: 460px; margin: 4px auto 30px; }
.cs-diagram svg { display: block; width: 100%; height: auto; overflow: visible; }

.cs-diagram__card { fill: rgba(255, 255, 255, 0.03); stroke: rgba(255, 255, 255, 0.14); stroke-width: 1; }
.cs-diagram__num { font: 600 12px 'Outfit', sans-serif; fill: #1a1200; }
/* Step name only — SVG text does not wrap, so this has to fit one line inside the card. The
   builder hard-caps the string length (DIAGRAM_TITLE_MAX) and the viewBox is sized so this
   font never overruns the card at any width. */
.cs-diagram__title { font: 600 15px 'Outfit', sans-serif; fill: #fff6ed; }

/* Nodes fade/rise in; connectors self-draw via stroke-dashoffset (set to the connector's
   exact length as a plain attribute in the markup, then overridden to 0 here — a CSS rule
   always outranks a presentation attribute, no !important needed); the traveling pulse dot
   fades in only once its connector has finished drawing. Both trigger together off one class,
   added by setupArchitectureDiagram()'s IntersectionObserver the moment the diagram scrolls
   into view inside the case-study modal's own scroll container. */
.cs-diagram__node {
    opacity: 0;
    transform: translateY(14px);
    transform-box: fill-box;
    transform-origin: center;
    transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.cs-diagram.is-drawing .cs-diagram__node,
.cs-diagram.is-drawn .cs-diagram__node { opacity: 1; transform: translateY(0); }

.cs-diagram__path {
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    transition: stroke-dashoffset 0.8s cubic-bezier(0.65, 0, 0.35, 1);
}
.cs-diagram.is-drawing .cs-diagram__path,
.cs-diagram.is-drawn .cs-diagram__path { stroke-dashoffset: 0; }
.cs-diagram.is-drawn .cs-diagram__path { transition: none; }   /* reduced motion: appear pre-drawn, don't animate to it */

.cs-diagram__pulse { fill: #fff6ed; opacity: 0; transform-box: fill-box; }
.cs-diagram.is-drawing .cs-diagram__pulse { animation: cs-diagram-pulse 2.6s ease-in-out infinite; }
/* .is-drawn (reduced motion) never adds this animation — the dot just stays invisible, since
   opacity:0 is its resting state above. */

/* Every connector spans exactly DIAGRAM_GAP (46) SVG user-units by construction, so one
   shared, hardcoded keyframe can express the pulse's travel instead of reading a per-instance
   value — same CSP constraint as above. transform-box:fill-box (set on .cs-diagram__pulse)
   makes this translateY relative to the dot's own position, so the same keyframe works
   identically for every connector regardless of its absolute position in the diagram.
   Keep the 46 here in sync with DIAGRAM_GAP in main.js. */
@keyframes cs-diagram-pulse {
    0%   { opacity: 0; transform: translateY(0); }
    8%   { opacity: 1; }
    88%  { opacity: 1; }
    100% { opacity: 0; transform: translateY(46px); }
}

/* Stagger, via nth-child within each element's own container (.cs-diagram__nodes /
   .cs-diagram__links) rather than a per-instance custom property — again the CSP constraint.
   Covers up to 6 steps with individual delays; any further steps share the last one rather
   than needing an unbounded rule list. */
.cs-diagram__nodes > .cs-diagram__node:nth-child(1) { transition-delay: 0s; }
.cs-diagram__nodes > .cs-diagram__node:nth-child(2) { transition-delay: .16s; }
.cs-diagram__nodes > .cs-diagram__node:nth-child(3) { transition-delay: .32s; }
.cs-diagram__nodes > .cs-diagram__node:nth-child(4) { transition-delay: .48s; }
.cs-diagram__nodes > .cs-diagram__node:nth-child(5) { transition-delay: .64s; }
.cs-diagram__nodes > .cs-diagram__node:nth-child(n+6) { transition-delay: .8s; }

.cs-diagram__links > .cs-diagram__link:nth-child(1) .cs-diagram__path { transition-delay: .12s; }
.cs-diagram__links > .cs-diagram__link:nth-child(1) .cs-diagram__pulse { animation-delay: .9s; }
.cs-diagram__links > .cs-diagram__link:nth-child(2) .cs-diagram__path { transition-delay: .28s; }
.cs-diagram__links > .cs-diagram__link:nth-child(2) .cs-diagram__pulse { animation-delay: 1.1s; }
.cs-diagram__links > .cs-diagram__link:nth-child(3) .cs-diagram__path { transition-delay: .44s; }
.cs-diagram__links > .cs-diagram__link:nth-child(3) .cs-diagram__pulse { animation-delay: 1.3s; }
.cs-diagram__links > .cs-diagram__link:nth-child(4) .cs-diagram__path { transition-delay: .6s; }
.cs-diagram__links > .cs-diagram__link:nth-child(4) .cs-diagram__pulse { animation-delay: 1.5s; }
.cs-diagram__links > .cs-diagram__link:nth-child(n+5) .cs-diagram__path { transition-delay: .76s; }
.cs-diagram__links > .cs-diagram__link:nth-child(n+5) .cs-diagram__pulse { animation-delay: 1.7s; }

/* "Case Study" call-to-action on project cards — the marquee action on a featured card,
   so it takes the site's forge-amber fill to set it apart from the plain white Live button
   and the outlined Source button. */
button.project-btn { border: none; cursor: pointer; font-family: 'Outfit', sans-serif; }
.project-btn-casestudy {
    background: linear-gradient(135deg, #ffe4b8, #ffb347);
    color: #1a1200;
}
.project-btn-casestudy:hover { background: linear-gradient(135deg, #ffecd0, #ffbf5e); }

/* ============================================================
   Anatomy of this site
   ============================================================ */
/* Normal-flow section after #work. Needs the same z-index/opaque-background treatment as
   .project-section so it renders above the fixed WebGL canvas rather than letting the
   blurred scene bleed through. */
.anatomy-section {
    position: relative;
    z-index: 15;
    background: #000000;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    padding: 120px 60px 150px;
    box-sizing: border-box;
}
.anatomy-inner { max-width: 1200px; margin: 0 auto; }
.anatomy-header { max-width: 720px; margin: 0 auto 56px; text-align: center; }
.anatomy-title {
    font-family: 'Italiana', serif;
    font-size: clamp(34px, 5vw, 60px);
    color: #fff6ed;
    margin-bottom: 18px;
}
.anatomy-lead {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 15px;
    line-height: 1.75;
    color: #d1d5db;
    letter-spacing: 0.3px;
}
.anatomy-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 22px;
}
.anatomy-card {
    background: rgba(10, 10, 10, 0.45);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 18px;
    padding: 28px;
}
.anatomy-label {
    display: inline-block;
    font-family: ui-monospace, 'SF Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: rgba(255, 179, 71, 0.85);
    padding: 4px 10px;
    border: 1px solid rgba(255, 179, 71, 0.25);
    border-radius: 20px;
    margin-bottom: 16px;
}
.anatomy-card-title {
    font-family: 'Italiana', serif;
    font-size: 21px;
    color: #fff6ed;
    margin-bottom: 10px;
}
.anatomy-card-body {
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 14px;
    line-height: 1.7;
    color: #d1d5db;
}
.anatomy-card-body code,
.anatomy-footnote code {
    font-family: ui-monospace, 'SF Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
    font-size: 0.86em;
    color: #ffd9a8;
    background: rgba(255, 255, 255, 0.06);
    padding: 1px 5px;
    border-radius: 5px;
}
.anatomy-footnote {
    max-width: 720px;
    margin: 48px auto 0;
    text-align: center;
    font-family: 'Outfit', sans-serif;
    font-weight: 300;
    font-size: 13.5px;
    line-height: 1.7;
    color: rgba(255, 255, 255, 0.55);
}
.anatomy-footnote a {
    color: #ffd9a8;
    text-decoration: none;
    border-bottom: 1px solid rgba(255, 217, 168, 0.35);
    white-space: nowrap;
}
.anatomy-footnote a:hover { border-bottom-color: rgba(255, 217, 168, 0.8); }

@media (max-width: 1023px) {
    .anatomy-section { padding: 110px 32px 130px; }
}
@media (max-width: 767px) {
    .anatomy-section { padding: 90px 24px 110px; }
    .anatomy-header { margin-bottom: 40px; }
    .case-study-modal { padding: 28px 20px; }
    .cs-arch-item { padding-left: 38px; }
    /* .case-study-body sizing/spacing lives in the mobile performance block at the end. */
}

/* Same guard the project grid uses: below ~348px of usable width a minmax(300px,…)
   track would overflow, so pin to a single column outright. */
@media (max-width: 479px) {
    .anatomy-grid { grid-template-columns: 1fr; }
    .anatomy-card { padding: 24px; }
}

/* ============================================================
   Mobile performance + touch targets
   ============================================================ */
/* backdrop-filter is the expensive part of the glass look — a full-frame blur+saturate
   recomputed every frame the element (or anything under it) moves. One or two are fine;
   ~30 project cards plus the modals plus the mobile menu is what makes scrolling stutter
   on a phone GPU. Below 768px the blur is dropped and each surface falls back to a near
   opaque panel — same visual language (dark, bordered, sits above the scene), a fraction
   of the cost. Kept for tablet/desktop where the budget is there. */
@media (max-width: 767px) {
    .project-card,
    .project-card:hover,
    .anatomy-card,
    .project-filter-bar,
    .github-stats {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(13, 13, 13, 0.92);
    }

    .contact-modal {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(12, 11, 10, 0.97);
        /* Finite (one contact modal, one case-study modal) and animates transform on every
           open/close, so a standing compositor-layer hint is worth it here — unlike the
           cards, where hinting 30 elements would just trade blur cost for layer-memory
           cost. No transform:translateZ() shortcut because .contact-modal already animates
           its own transform and a flat override would kill the entrance. */
        will-change: transform;
    }

    .mobile-menu-panel {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(9, 8, 7, 0.98);
        will-change: transform;
    }

    .cmdk-panel {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(12, 11, 10, 0.97);
        will-change: transform;
    }

    .cmdk-overlay {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(0, 0, 0, 0.82);
    }

    /* No physical Esc key on a phone — show a real tappable ✕ instead of the key hint. */
    .cmdk-dismiss {
        width: 38px;
        height: 38px;
        padding: 0;
        border-radius: 50%;
    }
    .cmdk-dismiss__key { display: none; }
    .cmdk-dismiss__x { display: block; }

    .modal-overlay {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(0, 0, 0, 0.82);
    }

    /* The headline cards' rim ember used to be frozen here, on the same budget argument as the
       blur above. It is back, because the argument does not transfer: the blur is ~30 cards each
       re-blurring everything behind them every frame they scroll, while the ember is exactly two
       elements repainting a 1px band — and it is the one thing on the page that says which two
       projects matter. Checked rather than assumed before re-enabling: paired A/B scroll samples
       at a 375px viewport, rim animating against rim frozen, three of each — median frame
       interval identical and the spread inside the noise. That was a desktop machine at a phone
       viewport, not a phone GPU, so it bounds the paint cost rather than proving it on hardware;
       the reason to trust it on a handset is the count, two elements against thirty. Slowed to
       11s here: the same travel over a shorter rim reads as faster, and this is the one width
       where the card is the whole screen rather than a tile in a grid. */
    .project-card-major::before { animation-duration: 11s; }

    /* Bigger close-button hit area (WCAG 2.5.5 target size) and no body overhang beneath it. */
    .modal-close { width: 44px; height: 44px; top: 16px; right: 16px; }
    .case-study-body { margin-right: 0; padding-right: 4px; max-height: 72vh; }
    .case-study-body .modal-title { padding-right: 52px; }

    /* Same backdrop-filter budget call as the cards above. */
    .fast-path-btn {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: rgba(14, 12, 11, 0.94);
    }
}

/* ============================================================
   Fast path — "Skip 3D" button
   ============================================================ */
/* A slim, quiet glass pill just under the header rule, carrying the same molten-amber ->
   rim-blue hairline as the mobile menu panel and the flagship card. It only smooth-scrolls
   to the work grid — nothing is torn down. Three states:
     - base            hidden before first paint
     - .is-ready       faded in (JS adds it ~700ms after load)
     - .is-hidden      faded back out once the hero is scrolled out of view (hero-visibility
                       observer in main.js); wins over .is-ready via source order.
   The magnetic-pull vars feed its centring transform via calc(). */
.fast-path-btn {
    position: fixed;
    top: 80px;
    left: 50%;
    z-index: 60;
    display: inline-flex;
    align-items: baseline;
    gap: 10px;
    padding: 9px 16px 9px 18px;
    background: rgba(10, 9, 8, 0.55);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 22px;
    color: #fff6ed;
    cursor: pointer;
    font-family: 'Outfit', sans-serif;
    overflow: hidden;
    opacity: 0;
    pointer-events: none;
    transform: translate(calc(-50% + var(--magnet-x, 0px)), calc(-14px + var(--magnet-y, 0px)));
    transition: opacity 0.45s ease,
                transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
                border-color 0.3s ease, background 0.3s ease;
}
.fast-path-btn.is-ready {
    opacity: 1;
    pointer-events: auto;
    transform: translate(calc(-50% + var(--magnet-x, 0px)), var(--magnet-y, 0px));
}
.fast-path-btn.is-hidden {
    opacity: 0;
    pointer-events: none;
    transform: translate(calc(-50% + var(--magnet-x, 0px)), calc(-14px + var(--magnet-y, 0px)));
}
.fast-path-btn::before {
    content: '';
    position: absolute;
    top: 0; left: 14px; right: 14px;
    height: 1px;
    background: linear-gradient(90deg, rgba(255, 179, 71, 0.8), rgba(120, 190, 255, 0.8));
}
.fast-path-btn:hover {
    border-color: rgba(255, 255, 255, 0.24);
    background: rgba(16, 14, 12, 0.72);
}
.fast-path-btn__eyebrow {
    font-size: 9px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 2px;
    color: rgba(255, 179, 71, 0.9);
}
.fast-path-btn__text {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1.5px;
}
.fast-path-btn__arrow {
    font-size: 13px;
    line-height: 1;
    color: rgba(255, 255, 255, 0.6);
    transform: translateX(0);
    transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), color 0.3s ease;
}
.fast-path-btn:hover .fast-path-btn__arrow {
    transform: translateX(4px);
    color: rgba(255, 220, 180, 0.95);
}

#work:focus { outline: none; }   /* programmatic focus target after the "Skip 3D" scroll */

/* ============================================================
   Reduced motion
   ============================================================ */
/* JS also skips the scroll-jacked intro under this query (see setupFastPath); these rules
   strip the remaining transform-based motion — the fast-path button's slide, the magnetic
   pull, and the card tilt/glare — down to plain opacity. */
@media (prefers-reduced-motion: reduce) {
    .fast-path-btn,
    .fast-path-btn.is-ready,
    .fast-path-btn.is-hidden {
        transform: translate(-50%, 0);
        transition: opacity 0.2s linear;
    }
    .fast-path-btn__arrow { transition: none; }
    .contact-btn, .project-btn, .filter-btn, .modal-close {
        transform: none;
        transition: background 0.3s, border-color 0.3s, color 0.3s;
    }
    .project-card {
        transform: none;
        translate: none;
        transition: opacity 0.4s ease, border-color 0.4s, background 0.4s;
    }
    .project-card-major {
        transition: opacity 0.4s ease, border-color 0.4s, background 0.4s, box-shadow 0.4s;
    }
    .project-card__glare { display: none; }
}

/* ============================================================
   Explicit motion override (command palette "Toggle Motion")
   ============================================================ */
/* html.motion-off is a page-local, in-session override — set/unset by setMotionOverride() in
   main.js when a visitor runs "Turn motion off"/"Turn motion on" from the command palette, and
   persisted to localStorage so it survives a reload. It is a deliberate mirror of the
   @media (prefers-reduced-motion: reduce) block just above, rule for rule: a plain CSS class
   can't be matched from inside that media query, so an override that has to work independently
   of the OS-level setting needs its own copy of the same declarations. Keep the two in sync —
   a rule added to one almost certainly belongs in the other too. Unlike the OS preference
   (which JS deliberately leaves the hero's WebGL loop free to resume if a visitor manually
   scrolls back up to it, see setupHeroVisibilityGate's own comment), flipping this control is
   an explicit, immediate action, so main.js also pauses/resumes that render loop directly —
   nothing here needs to reach the canvas, only the CSS-driven motion. */
html.motion-off .fast-path-btn,
html.motion-off .fast-path-btn.is-ready,
html.motion-off .fast-path-btn.is-hidden {
    transform: translate(-50%, 0);
    transition: opacity 0.2s linear;
}
html.motion-off .fast-path-btn__arrow { transition: none; }
html.motion-off .contact-btn, html.motion-off .project-btn, html.motion-off .filter-btn, html.motion-off .modal-close {
    transform: none;
    transition: background 0.3s, border-color 0.3s, color 0.3s;
}
html.motion-off .project-card {
    transform: none;
    translate: none;
    transition: opacity 0.4s ease, border-color 0.4s, background 0.4s;
}
html.motion-off .project-card-major {
    transition: opacity 0.4s ease, border-color 0.4s, background 0.4s, box-shadow 0.4s;
}
html.motion-off .project-card__glare { display: none; }
