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

:root {
    --background: #ffffff;
    --text-primary: #1d1d1f;
    --text-secondary: #86868b;
    --accent: #0071e3;
    --card-background: #f5f5f7;
    --nav-height: 48px;
    --site-gutter: 48px;
    --safe-bottom: env(safe-area-inset-bottom, 0px);
}

@media (prefers-color-scheme: dark) {
    :root {
        --background: #002400;
        --text-primary: #f5f5f7;
        --text-secondary: #86868b;
        --accent: #2997ff;
        --card-background: #1d1d1f;
    }
}

html {
    scroll-behavior: smooth;
    height: 100%;
    background-color: var(--background);
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    background-color: var(--background);
    color: var(--text-primary);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    min-height: 105vh;
    min-height: 105svh;
    min-height: 105lvh;
}

/* Skip Link */
.skip-link {
    position: absolute;
    top: 8px;
    left: -999px;
    padding: 10px 16px;
    background: var(--background);
    color: var(--text-primary);
    border: 1px solid rgba(0, 0, 0, 0.2);
    border-radius: 999px;
    text-decoration: none;
    z-index: 2000;
}

.skip-link:focus {
    left: var(--site-gutter);
}

/* Navigation */
.nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--nav-height);
    background-color: rgba(255, 255, 255, 0.8);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
    z-index: 1000;
    transition: opacity 0.3s ease;
}

@media (prefers-color-scheme: dark) {
    .nav {
        background-color: rgba(0, 0, 0, 0.8);
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    }
}

.nav-content {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 var(--site-gutter);
    height: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.nav-logo {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
    text-decoration: none;
}

.nav-links {
    display: flex;
    gap: 32px;
}

.nav-links a {
    font-size: 14px;
    color: var(--text-primary);
    text-decoration: none;
    transition: opacity 0.2s;
}

.nav-links a:hover {
    opacity: 0.7;
}

/* Hero Section */
.hero {
    margin-top: var(--nav-height);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 0 var(--site-gutter);
    position: relative;
    overflow: hidden;
    pointer-events: none;
    /* Allow hovering icons behind */
    z-index: 5;
}

.app-grid-background {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    overflow: hidden;
    pointer-events: none;
    /* Icons handle their own events */
}

/* Back layer sits behind hero text */
.app-grid-back {
    z-index: 3;
}

/* Front layer sits in front of hero text */
.app-grid-front {
    z-index: 10;
}

.scroll-fade-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--background);
    opacity: 0;
    z-index: 100;
    pointer-events: none;
    transition: opacity 0.3s ease;
}



.hero-content {
    position: relative;
    z-index: 5;
    pointer-events: auto;
    /* Keep hero interactive */
    transition: opacity 0.3s ease;
}

.hero-title {
    font-size: clamp(48px, 8vw, 96px);
    font-weight: 700;
    letter-spacing: -0.015em;
    line-height: 1.0;
    margin-bottom: 24px;
    text-shadow: none;
}

.hero-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    margin-top: 24px;
}

.hero-badge {
    display: inline-block;
    padding: 14px 28px;
    border-radius: 980px;
    background: linear-gradient(160deg, #ffffff, #dcdce0);
    color: #6b6b70;
    font-size: 17px;
    font-weight: 500;
    letter-spacing: 0.03em;
    border: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow:
        0 1px 3px rgba(0, 0, 0, 0.1),
        0 4px 12px rgba(0, 0, 0, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.8);
}

@media (prefers-color-scheme: dark) {
    .hero-badge {
        background: linear-gradient(160deg, #3a3a3e, #2a2a2e);
        color: #a0a0a5;
        border: 1px solid rgba(255, 255, 255, 0.12);
        box-shadow:
            0 1px 3px rgba(0, 0, 0, 0.3),
            0 4px 12px rgba(0, 0, 0, 0.2),
            inset 0 1px 0 rgba(255, 255, 255, 0.1);
    }
}

.hero-cta {
    display: inline-block;
    padding: 14px 28px;
    background-color: var(--accent);
    color: white;
    border-radius: 980px;
    font-size: 17px;
    font-weight: 500;
    text-decoration: none;
    transition: all 0.3s ease;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.hero-cta:hover {
    transform: scale(1.05);
    box-shadow: 0 10px 30px rgba(0, 113, 227, 0.3);
}

/* Floating App Icons in Background */
.floating-icon {
    position: absolute;
    border-radius: 30%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    cursor: pointer;
    transition: transform 0.5s ease, box-shadow 0.3s ease, filter 0.3s ease, opacity 0.3s ease;
    text-decoration: none;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    z-index: 12;
    /* Above hero but below overlay edges */
    will-change: transform, opacity, filter;
    transform: translate3d(-50%, -50%, 0);
    background: none !important;
    pointer-events: auto;
}

.floating-icon img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 22%;
    display: block;
}

/* Disable interactions when scrolled */
.app-grid-background.is-scrolled .floating-icon {
    pointer-events: none;
    cursor: default;
}

.floating-icon:hover {
    transform: translate3d(-50%, -50%, 0) scale(1.15) !important;
    z-index: 20;
    /* Higher than dark overlay */
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
}

/* Ensure no hover scaling when scrolled (even if JS fails to update pointer-events) */
.app-grid-background.is-scrolled .floating-icon:hover {
    transform: translate3d(-50%, -50%, 0) scale(1) !important;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

/* ==========================================
   Shared Section Styles
   ========================================== */

/* Shared caption/label typography */
.caption-label {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-weight: 600;
}

.section {
    padding: 140px 0;
    position: relative;
    z-index: 100;
}

.section-inner {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 var(--site-gutter);
}

.section-title {
    font-size: clamp(40px, 6vw, 64px);
    font-weight: 700;
    letter-spacing: -0.015em;
    margin-bottom: 64px;
}

/* All sections are transparent — blurred floating icons show through */

/* ==========================================
   Horizontal Scroll (shared)
   ========================================== */
.horizontal-scroll {
    display: flex;
    gap: 20px;
    overflow-x: auto;
    overflow-y: visible;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding: 10px 0 20px;
    /* Align first card with section title */
    padding-left: var(--site-gutter);
    /* Simplified padding as we use spacer */
    padding-right: max(var(--site-gutter), calc((100vw - 1200px) / 2 + var(--site-gutter)));
}

.horizontal-scroll.is-animating {
    scroll-snap-type: none !important;
}

.scroll-spacer {
    flex: 0 0 auto;
    width: max(var(--site-gutter), calc((100vw - 1200px) / 2 + var(--site-gutter)));
    scroll-snap-align: start;
    margin-left: calc(-1 * var(--site-gutter));
    /* Offset parent padding */
}

.horizontal-scroll::before,
.horizontal-scroll::after {
    content: '';
    flex: 0 0 auto;
    width: 1px;
    /* Minimal width to trigger padding-like behavior with snap */
    margin-left: -1px;
}

.horizontal-scroll::-webkit-scrollbar {
    display: none;
}

/* Highlights scroll needs extra leading/trailing space so first/last cards center */
.highlights-scroll {
    /* Override default horizontal-scroll padding to center first & last cards */
    padding-left: calc(50vw - min(calc((100vw - 120px) / 2), 360px));
    padding-right: calc(50vw - min(calc((100vw - 120px) / 2), 360px));
    /* Extra top/bottom room so the hover box-shadow (0 20px 60px) has space
       to render — overflow-x: auto forces overflow-y to clip too, even
       though it's set to visible */
    padding-top: 48px;
    padding-bottom: 80px;
    scroll-snap-type: x mandatory;
}

/* ==========================================
   Section 1: Recent Highlights
   ========================================== */
.highlight-card {
    flex: 0 0 auto;
    width: min(calc(100vw - 120px), 720px);
    border-radius: 20px;
    scroll-snap-align: center;
    text-decoration: none;
    color: var(--text-primary);
    transition: transform 0.4s ease, box-shadow 0.4s ease;
    cursor: pointer;
    position: relative;
    transform: scale(0.95);
    opacity: 1;
    /* Ensure unfocused cards don't lose opacity */
    /* overflow/background live on .highlight-card-inner so this element's
       hover box-shadow isn't clipped by its own overflow:hidden */
}

.highlight-card-inner {
    border-radius: inherit;
    overflow: hidden;
    background: var(--card-background);
    display: flex;
    flex-direction: column;
    height: 100%;
}

.highlight-card.is-disabled {
    cursor: default;
    pointer-events: none;
}


.section-highlights .section-title {
    text-align: left;
}

.highlight-card.is-focused {
    transform: scale(1);
}

@media (hover: hover) and (pointer: fine) {
    .highlight-card:hover {
        transform: scale(1.02);
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
    }
}

@media (hover: hover) and (pointer: fine) and (prefers-color-scheme: dark) {
    .highlight-card:hover {
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    }
}

/* Plus indicator — stacked in normal flow below the description, not
   floating over it */
.highlight-card-plus {
    align-self: flex-start;
    margin-top: 8px;
    padding: 8px 8px 8px 14px;
    border-radius: 999px;
    background: var(--accent);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: #ffffff;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.highlight-card-plus-chevron {
    display: inline-block;
    transform: translateY(-0.5px);
    font-size: 14px;
    line-height: 1;
    margin-left: 2px;
}

@media (prefers-color-scheme: dark) {
    .highlight-card-plus {
        background: var(--accent);
    }
}

@media (hover: hover) and (pointer: fine) {
    .highlight-card:hover .highlight-card-plus {
        transform: translateY(-1px);
        box-shadow: 0 10px 24px rgba(0, 113, 227, 0.35);
    }
}

.highlight-card-image {
    width: 100%;
    aspect-ratio: 16/9;
    background-size: cover;
    background-position: center;
}

.highlight-card-content {
    padding: 20px 24px 24px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.highlight-card-tag {
    color: var(--accent);
}

.highlight-card-title {
    font-size: 21px;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.highlight-card-desc {
    font-size: 15px;
    color: var(--text-secondary);
    line-height: 1.5;
    margin-top: 2px;
}

/* ==========================================
   Section 2: Skills
   ========================================== */
/* Desktop (default here): two panels side by side — a fixed 280px capsule
   column (leading edge) and an image panel filling the rest. This is a
   real CSS Grid now, not an overlay — the capsule list sits in normal
   flow next to the image rather than floating on top of it, so there's
   no legibility scrim to worry about on this breakpoint.

   Mobile (768px override below) reverts to the original overlay model:
   .skills-image-panel collapses to display: contents (so #skillsMedia
   behaves exactly as if it were a direct child of .skills-stack again),
   and a bottom pill row expands a single floating card over the shared
   background image — that's where the scrim still earns its keep. */
.skills-stack {
    position: relative;
    /* Establishes its own stacking context, containing the whole z-index
       hierarchy below (media/empty-state/tags/capsule column) so none of
       those values — including .skills-media's negative one — can escape
       and end up resolving against something OUTSIDE this section
       entirely. Without this, that negative z-index had nothing local to
       be contained by, and .skills-media (with it) could end up painting
       behind unrelated ancestor content instead of just behind its own
       siblings here, which was hiding the images outright rather than
       just tucking them behind the empty-state placeholder as intended. */
    isolation: isolate;
    width: 100%;
    display: grid;
    grid-template-columns: 280px 1fr;
    gap: 32px;
    align-items: stretch;
    /* Uniform full-bleed backdrop for the section body (title excluded —
       it lives in .section-inner, above this element). Same color as the
       capsules' own card material, so the capsules need their own glass
       treatment (below) to read as a distinct layer on top of it. */
    background: var(--card-background);
    /* Left inset only (right stays full-bleed for the image) so the
       capsule column's own left edge lines up with the section title's
       text. Same formula .section-inner's centering produces for its
       content edge — plain var(--site-gutter) alone only matches below
       the 1200px+gutters breakpoint; beyond it .section-inner's max-width
       auto-margin pushes the title in further, and this must track that. */
    padding-left: max(var(--site-gutter), calc((100vw - 1200px) / 2 + var(--site-gutter)));
}

/* Anchor for .skills-accordion-nav-wrap, which is positioned absolutely
   just outside (to the left of) this column — so the up/down steppers can
   sit in the gutter margin while the capsule list itself still starts
   flush with the column's own left edge (aligned to the title inset). */
.skills-accordion-column {
    position: relative;
    align-self: center;
    /* Explicit, not left at the default auto — .skills-media has its own
       explicit z-index (0, for containing its entering/exiting layers),
       and an explicit z-index sibling actually painting later in the DOM
       can still end up stacking above an auto one, which was letting the
       image's own exit motion render on top of these capsules instead of
       behind them once it started bleeding past its own box (the
       clip-path change above). Highest in the section's local stack
       (.skills-stack has isolation: isolate) — above .skills-image-tags
       (z-index: 2) too, not just the media/empty-state layers below it. */
    z-index: 3;
}

/* Desktop-only wrapper for the per-category image + its tags — no card
   chrome. Tags are overlaid on the image itself (absolutely positioned,
   anchored to this element) rather than occupying their own flow space
   below it, so the panel's height tracks the image alone. */
.skills-image-panel {
    position: relative;
    display: flex;
    flex-direction: column;
}

.skills-media {
    position: relative;
    width: 100%;
    /* Establishes its own stacking context (position: relative alone
       doesn't — it needs an explicit z-index too) so the entering/exiting
       layers' own z-index: 1/2 (below, for keeping whichever one is
       currently arriving above the one leaving) stays CONTAINED inside
       this element instead of leaking out to compete directly with
       siblings like .skills-accordion-column and .skills-image-tags —
       without this, the media layers were winning ties against both of
       those and sitting on top of them, blocking taps on the capsules and
       covering the tags. The lowest layer in the section's local stack
       (.skills-stack has isolation: isolate, above, so this — and every
       other z-index in this section — is guaranteed to stay contained
       here rather than potentially resolving against something outside
       it entirely). */
    z-index: 0;
    /* Vertical motion (the desktop stepper's up/down) still gets clipped
       at this box's own top/bottom edge — bleeding past that would spill
       into the section title above or the row below during the brief
       transition. Horizontal motion (left/right — the default for a
       direct selection, or the mobile arrow taps) is deliberately NOT
       clipped, so it can travel past this box's own edge and behind the
       capsule column (z-index, above, already keeps that column rendering
       on top of it) instead of being cut off right at the image panel's
       own boundary before it ever gets there.

       clip-path, not overflow-x/-y — mixing those doesn't actually work
       the way it looks like it should: per spec, if either axis isn't
       visible, the OTHER one's computed value is forced to auto instead
       of the literal visible it was set to, and auto still clips just
       like hidden does. clip-path has no such quirk — inset()'s left/right
       here are pushed way outside the box (-100vw), so nothing realistic
       ever reaches that boundary, while top/bottom stay pinned exactly at
       the box's own edge. */
    overflow: visible;
    clip-path: inset(0 -100vw 0 -100vw);
    /* Explicitly matches .skills-stack's own background (also
       --card-background) rather than leaving this unset and just
       inheriting it visually — with real images now (contain-fitted,
       often not filling the box edge to edge), this needs to be a real,
       intentional value so the letterbox gaps read as part of the same
       uniform card surface the rest of the section sits on, not a
       different, mismatched box. (Was briefly set to var(--background),
       the page's own color, instead — that's a different token
       (#ffffff/white vs --card-background's #f5f5f7/light gray in light
       mode), which was exactly what made this read as a distinct white
       box rather than blending into the section.) */
    background-color: var(--card-background);
    /* A preset height, not an aspect-ratio — this panel is full-bleed now,
       so its width swings a lot with viewport width, and tying height to
       that (as aspect-ratio would) made the section balloon on wide
       screens. max-height is the shrink-on-short-windows fallback. The box
       doesn't need to match a tall/skinny image's own ratio — contain
       (below) just letterboxes it, which reads fine on the card's own
       background color. */
    height: 680px;
    max-height: 75vh;
    flex-shrink: 0;
    /* contain, not cover — a transparent-background image should sit
       inset on the card's own color, not get cropped edge to edge (real
       images now, with their own intrinsic dimensions — the old
       explicit-size stand-in this needed for the gradient placeholders,
       which had no intrinsic size of their own, is gone). Centered both
       ways here — desktop's own image panel has nothing overlapping its
       top edge, unlike mobile's close button (see the mobile override
       further down, which goes back to a top-anchored position with some
       clearance instead). */
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
}

/* Resting/entering state — a layer about to become visible sits here,
   offset toward wherever it should visibly travel FROM (--media-in-x/y,
   skills.js — direction depends on how the skill was selected: the
   up/down stepper, an arrow tap, a swipe, or a direct capsule click; see
   updateMedia). Slightly blurred, scaled down, and transparent, all
   settling to their resting values together on is-visible below — the
   combined effect is a soft "arrives from that direction" motion, not
   just a plain crossfade. */
.skills-media-layer {
    position: absolute;
    inset: 0;
    opacity: 0;
    /* DOM order alone would otherwise always stack data-layer="b" above
       "a" (later in the DOM), regardless of which one actually happens to
       be entering — is-visible below overrides this so whichever layer is
       currently coming in is always the one on top, not whichever one
       happens to be second in markup. */
    z-index: 1;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    transform: translate(var(--media-in-x, 0%), var(--media-in-y, 50%)) scale(1.25);
    filter: blur(3px);
    transition: opacity 0.4s ease, transform 0.4s ease, filter 0.4s ease;
}

.skills-media-layer.is-visible {
    opacity: 1;
    z-index: 2;
    transform: translate(0, 0) scale(1);
    filter: blur(0);
}

/* The OUTGOING layer, applied the same instant is-visible is removed from
   it (skills.js) — without this it would just fall back to the plain
   resting state above, which uses the ENTERING offset (--media-in-x/y),
   the same direction as whatever's arriving, not the mirrored direction
   an outgoing layer should continue away in. --media-out-x/y (skills.js)
   is always the negation of that same call's --media-in-x/y, so the two
   layers read as one continuous motion — new arrives from one side as
   old continues off the opposite side — rather than both drifting the
   same way. */
.skills-media-layer.is-exiting {
    opacity: 0;
    transform: translate(var(--media-out-x, 0%), var(--media-out-y, -50%)) scale(0.75);
    filter: blur(3px);
}

/* Overlaid on the bottom of the image. No scrim behind these anymore — the
   images are contain-fitted transparent PNGs on the section's own card
   material (not full-bleed photos these needed a dark gradient to stay
   legible over), so they just use the same theme-aware capsule styling as
   .skills-tag everywhere else on the page, which already reads fine
   against that card material regardless of light/dark mode.

   Own opacity toggle (skills.js, mirroring .skills-image-empty's own),
   not just left showing whatever the last-viewed skill's tags were —
   these used to only ever get hidden as a side effect of the old
   empty-state overlay sitting at a higher z-index than everything
   (covering them along with the image), which stopped being true once
   that overlay moved to sit behind the active content instead. */
.skills-image-tags {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    padding: 20px;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.skills-image-tags.is-visible {
    opacity: 1;
}

/* Desktop-only empty state — the accordion has a real "nothing selected"
   state (every capsule collapsed) that the mobile pill row doesn't, so
   this covers the image/tags entirely rather than defaulting to showing
   some skill's content as if it were selected. Opaque card material
   (matching the section's own uniform backdrop), toggled via JS
   (skills.js) alongside the accordion's open/closed state.

   Sits behind the capsules/tags (z-index: 2/3) but ABOVE .skills-media
   itself (z-index: 0) — .skills-media has its own opaque background
   underneath its actual image layers, so being behind THAT specifically
   would hide this completely regardless of its own opacity/transform;
   z-index: 1 is the one spot that's behind the active UI while still
   being visible at all. Scales down, fades, and blurs out together when
   something becomes active, and reverses the same way when it's hidden
   again — same treatment as the media layers' own enter/exit, not just a
   plain opacity fade.

   No background of its own (relies on .skills-media's background showing
   through instead) — so a media crossfade still in flight can bleed
   through around the icon/text while this is fading in/out. Accepted
   tradeoff, not an oversight. */
.skills-image-empty {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    opacity: 0;
    transform: scale(0.9);
    filter: blur(4px);
    pointer-events: none;
    transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease;
}

.skills-image-empty.is-visible {
    opacity: 1;
    transform: scale(1);
    filter: blur(0);
}

.skills-image-empty-illustration {
    width: 280px;
    max-width: 70%;
    height: auto;
    margin-bottom: 8px;
}

/* PNG linework is dark-on-transparent, so it reads fine in light mode but
   goes low-contrast against .skills-image-empty's near-black dark-mode
   background. invert()+hue-rotate(180deg) flips only the lightness (black
   linework -> light) while landing the pink/teal accents back near their
   original hue, instead of shipping a second asset for dark mode. */
@media (prefers-color-scheme: dark) {
    .skills-image-empty-illustration {
        filter: invert(1) hue-rotate(180deg);
    }
}

.skills-image-empty-title {
    font-size: 17px;
    font-weight: 600;
    color: var(--text-primary);
}

/* Desktop shows "Click", mobile shows "Tap" — same 768px cutoff as
   isMobileViewport() in skills.js. */
.skills-image-empty-verb-tap {
    display: none;
}

@media (max-width: 768px) {
    .skills-image-empty-verb-tap {
        display: inline;
    }

    .skills-image-empty-verb-click {
        display: none;
    }
}

.skills-image-empty-text {
    font-size: 14px;
    color: var(--text-secondary);
}

/* Shared text styles used by both the desktop accordion panel and the
   mobile card content */
/* Base (desktop) text styles — the accordion sits on real card material,
   so this follows the theme like any other card. Mobile overrides these
   back to a white/translucent treatment further down, since the mobile
   card floats directly on the image rather than on opaque card material. */
.skills-overview {
    font-size: 15.5px;
    line-height: 1.5;
    color: var(--text-primary);
    margin: 0 0 12px;
}

.skills-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.skills-tag {
    font-size: 13px;
    font-weight: 500;
    color: var(--text-primary);
    background: rgba(0, 0, 0, 0.05);
    border-radius: 980px;
    padding: 7px 14px;
}

@media (prefers-color-scheme: dark) {
    .skills-tag {
        background: rgba(255, 255, 255, 0.08);
    }
}

/* ── Desktop: leading-edge accordion ─────────────────────────────────── */
/* In normal flow now (a flex child of .skills-accordion-column), vertically
   centered via that column's own align-items: center rather than absolute
   positioning. align-items: flex-start lets collapsed items hug their own
   content width instead of stretching to the 280px column; .is-open opts
   back into the full width. No overflow/scroll here (deliberately, even
   though items can exceed the available height in rare cases) — overflow
   other than visible on one axis forces the other axis to clip too, per
   spec, which was cutting off every item's own drop-shadow. */
.skills-accordion {
    width: 280px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
}

.skills-accordion-nav-wrap {
    /* Taken out of flow and pulled to the left of .skills-accordion-column
       so it sits outside the text-aligned region — in the gutter margin —
       rather than pushing the capsule list off-alignment with the title. */
    position: absolute;
    right: 100%;
    top: 50%;
    transform: translateY(-50%);
    margin-right: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex-shrink: 0;
    /* 0, not display: none — stays in place (no layout shift) and just
       fades in once something is selected */
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

.skills-accordion-nav-wrap.has-open {
    opacity: 1;
    pointer-events: auto;
}

/* Same fogged-glass material as the capsules — a b/w (theme-following)
   icon on top via currentColor rather than a fixed white glyph on a dark
   dot, now that these no longer sit on the always-dark mobile scrim. */
.skills-accordion-nav,
.skills-accordion-close {
    /* Solved so this composites to ~#EBEBF0 over the section's own
       --card-background (#f5f5f7) — the previous white tint at this same
       alpha ended up lighter than the backdrop instead of the darker
       "material" look wanted here. */
    background: rgba(227, 227, 234, 0.55);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    color: var(--text-primary);
}

@media (prefers-color-scheme: dark) {

    .skills-accordion-nav,
    .skills-accordion-close {
        background: rgba(255, 255, 255, 0.1);
    }
}

.skills-accordion-nav {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

/* Mobile-only content (see .skills-accordion-nav-label override below) —
   desktop's steppers are plain icon buttons. */
.skills-accordion-nav-label {
    display: none;
}

/* Single close control for the whole box, top-right of .skills-stack —
   not per-item. Same show/hide-by-opacity treatment as the steppers. */
.skills-accordion-close {
    position: absolute;
    top: 24px;
    right: 24px;
    z-index: 2;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
    cursor: pointer;
}

.skills-accordion-close.is-visible {
    opacity: 1;
    pointer-events: auto;
}

/* Fogged glass over the section's now-uniform card-color backdrop —
   translucent + blurred rather than flat matching card material, so the
   capsules still read as their own floating layer instead of disappearing
   into the background behind them. */
.skills-accordion-item {
    /* border-radius is fixed at exactly half of .skills-accordion-head's
       fixed height (56px / 2 = 28px) — not recalculated when the item
       grows taller on expand. That constant is what makes it read as a
       true capsule while collapsed (radius = half height = fully round
       ends) and cleanly settle into an ordinary rounded rectangle once
       expanded, instead of staying pill-shaped at a much taller height. */
    flex-shrink: 0;
    max-width: 100%;
    border-radius: 28px;
    /* Needed to clip content to the rounded shape while the width/height
       animate. */
    overflow: hidden;
    /* Solved so this composites to ~#EBEBF0 over the section's own
       --card-background (#f5f5f7) — the previous white tint at this same
       alpha ended up lighter than the backdrop instead of the darker
       "material" look wanted here. */
    background: rgba(227, 227, 234, 0.55);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    /* --skills-item-w is measured once in JS (the head's natural content
       width — CSS alone can't transition to/from auto/shrink-to-fit) so
       this can animate the same way the height does. Falls back to auto
       for the instant before JS runs. */
    width: var(--skills-item-w, auto);
    transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-color-scheme: dark) {
    .skills-accordion-item {
        background: rgba(255, 255, 255, 0.1);
    }
}

.skills-accordion-item.is-open {
    width: 280px;
}

.skills-accordion-head {
    display: flex;
    align-items: center;
    gap: 11px;
    height: 56px;
    padding: 0 26px 0 20px;
    background: none;
    border: none;
    font: inherit;
    text-align: left;
    color: var(--text-primary);
    cursor: pointer;
    white-space: nowrap;
    /* Fades out on mobile while .skills-accordion-head-arrow (below) is
       covering it, a no-op on desktop where that never happens. */
    transition: opacity 0.2s ease;
}

.skills-accordion-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    color: var(--text-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Mobile-only (see .is-before-open/.is-after-open in the mobile media
   query) — desktop's up/down stepper is a separate, dedicated button, so
   no item ever needs to show a chevron itself here. A plain overlay
   sibling of .skills-accordion-head, not a child of it (see skills.js) —
   its own padding/justify-content/opacity can never affect the head's own
   box, or by extension the item's own measured collapsed width (head has
   to stay free to shrink-wrap to just its icon+name for
   pinCollapsedWidths, skills.js, to measure that correctly). */
.skills-accordion-head-arrow {
    display: none;
}

.skills-accordion-chevron {
    color: var(--text-primary);
}

.skills-accordion-icon svg {
    display: block;
}

/* The focused item's own plus rotates into an x — its head is still the
   same toggle button (clicking it again closes via toggleAccordion in
   skills.js), so this doubles as that item's own close affordance instead
   of relying solely on the single .skills-accordion-close control. */
.skills-accordion-item.is-open .skills-accordion-icon {
    transform: rotate(45deg);
}

.skills-accordion-name {
    font-size: 16px;
    font-weight: 600;
}

.skills-accordion-body {
    display: grid;
    grid-template-rows: 0fr;
    /* Deliberately NOT a percentage — the item itself now animates its
       own width too, and if this tracked that live (100%), the panel's
       text would reflow continuously as the item's box resizes, which
       recomputes the grid row's 1fr target every frame and makes the
       height animation jump around. Pinning this to the SAME explicit
       values as the item's own width/is-open width (rather than a live
       percentage of it) keeps the panel content at a stable width for
       the whole transition — item's outer box is still free to animate
       its width smoothly in sync, clipped against by its own
       overflow: hidden, with no visible seam. */
    width: var(--skills-item-w, auto);
    /* On close, width must NOT change until the height transition has
       finished — if it does, the text reflows into a near-zero-width
       column mid-shrink (nearly every word on its own line), the row's
       1fr/0fr height target is computed off that ballooned content
       height, and the box visibly jumps tall before collapsing. Delaying
       the (instant, 0s) width change by the same 0.35s as the height
       transition keeps the width stable for the whole shrink, so there's
       nothing to reflow until after it's already collapsed. */
    transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1),
                width 0s linear 0.35s;
}

.skills-accordion-item.is-open .skills-accordion-body {
    grid-template-rows: 1fr;
    width: 280px;
    /* On open, width must snap immediately (no delay) so the text is
       already laid out at its final width before/as the height grows. */
    transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1),
                width 0s linear 0s;
}

.skills-accordion-body-inner {
    overflow: hidden;
}

.skills-accordion-panel {
    padding: 0 26px 16px 20px;
}

/* Same tags every skill already shows via .skills-image-tags on desktop —
   duplicated here inside the panel itself for mobile, which has no
   separate image-overlay tags row (the accordion column overlays the
   bottom of the image there instead of sitting beside it, so a second
   tags row anchored to the same spot would just collide with it). Hidden
   on desktop to avoid showing the same tags twice. */
.skills-accordion-tags {
    display: none;
}

/* ==========================================
   Section 3: About Me
   ========================================== */
.section-about .section-title {
    text-align: left;
}

.about-scroll {
    scroll-snap-type: none;
    overflow-y: visible;
    padding-top: 24px;
    padding-bottom: 40px;
}

.about-card-wrap {
    flex: 0 0 auto;
    min-width: 200px;
    max-width: 340px;
    width: clamp(200px, 30vw, 340px);
    aspect-ratio: 3/4;
    border-radius: 24px;
    position: relative;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
    cursor: default;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}

.about-card {
    width: 100%;
    height: 100%;
    border-radius: 24px;
    overflow: hidden;
    background: var(--card-background);
    background-size: cover;
    background-position: center;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    padding: 32px 24px 24px;
    position: relative;
}

@media (hover: hover) and (pointer: fine) {
    .about-card-wrap:hover {
        transform: scale(1.02);
        box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
    }
}

@media (hover: hover) and (pointer: fine) and (prefers-color-scheme: dark) {
    .about-card-wrap:hover {
        box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
    }
}

@media (hover: none) and (pointer: coarse) {
    .highlight-card:hover,
    .about-card-wrap:hover,
    .phone-app-icon:hover,
    .hero-cta:hover {
        transform: none !important;
        box-shadow: none !important;
    }

    .highlight-card:hover .highlight-card-plus {
        transform: none !important;
        box-shadow: none !important;
    }

    .highlight-card:active,
    .about-card-wrap:active,
    .phone-app-icon:active,
    .hero-cta:active {
        transform: none !important;
        box-shadow: none !important;
    }
}

.about-card-has-bg::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent 60%);
    pointer-events: none;
    z-index: 0;
}

.about-card-has-bg .about-card-text {
    color: white;
}

.about-card-text {
    position: relative;
    z-index: 1;
    max-width: 60%;
}

.about-card-label {
    opacity: 0.8;
    margin-bottom: 4px;
}

.about-card-value {
    font-size: 20px;
    font-weight: 600;
    line-height: 1.2;
}

.about-card-sub {
    font-size: 14px;
    opacity: 0.7;
    margin-top: 4px;
    line-height: 1.4;
}

/* ==========================================
   Section 5: Connect — Phone Frame
   ========================================== */
.section-connect {
    overflow: hidden;
    /* Hide the bottom half of the phone */
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    height: 600px;
    /* Increased from 500px for larger phone */
    justify-content: flex-end;
    /* Align phone to absolute bottom */
}


.section-connect .section-title {
    position: absolute;
    bottom: 510px;
    /* Directly above 50% phone peak (half of ~1000 is 500) */
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    z-index: 5;
    pointer-events: none;
    white-space: nowrap;
    /* Prevent shrink-to-fit wrap on narrow viewports */
}

.phone-frame {
    width: 480px;
    margin: 0 auto;
    position: relative;
    background: transparent;
    border: none;
    overflow: visible;
    z-index: 10;
    will-change: transform;
    /* Resting state is fully below container */
    transform: translateY(100%);
}

.phone-bezel {
    width: 100%;
    height: auto;
    display: block;
    position: relative;
    z-index: 2;
    pointer-events: none;
}

.phone-screen-container {
    position: absolute;
    top: 2.2%;
    /* Fine-tune based on PhoneBezel.png aspect ratio */
    left: 5.2%;
    right: 5.2%;
    bottom: 2.2%;
    border-radius: 57px;
    overflow: hidden;
    z-index: 1;
}

.phone-screen-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    position: absolute;
    top: 0;
    left: 0;
}

.phone-icons-grid {
    position: relative;
    z-index: 3;
    padding: 90px 33px 30px;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 20px;
    justify-items: center;
}

.phone-app-icon {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-decoration: none;
    color: white;
    transition: transform 0.2s ease;
}

.phone-app-icon:hover {
    transform: scale(1.1);
}

.phone-app-icon-img {
    width: 66px;
    /* ~1/7.5th of 480px Display */
    height: 66px;
    border-radius: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
}

.phone-app-icon-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.phone-app-icon-img svg {
    width: 45px;
    height: 45px;
}

@media (prefers-color-scheme: dark) {
    .phone-app-github .phone-app-icon-img {
        background: linear-gradient(135deg, #f5f5f7, #d1d1d6);
        color: #1a1a1a;
    }
}

.phone-app-linkedin .phone-app-icon-img {
    background: linear-gradient(135deg, #0077b5, #00a0dc);
    color: white;
}

.phone-app-twitter .phone-app-icon-img {
    background: linear-gradient(135deg, #1d1d1f, #000);
    color: white;
}

@media (prefers-color-scheme: dark) {
    .phone-app-twitter .phone-app-icon-img {
        background: linear-gradient(135deg, #f5f5f7, #d1d1d6);
        color: #1a1a1a;
    }
}

.phone-app-icon-label {
    font-size: 11px;
    /* Slightly larger than original 10px */
    font-weight: 500;
    opacity: 0.95;
    color: white;
}

/* ==========================================
   Responsive: Mobile (≤ 768px)
   ========================================== */
@media (max-width: 768px) {
    :root {
        --site-gutter: 20px;
    }

    /* Nav */
    .nav-links {
        gap: 20px;
    }

    .nav-links a {
        font-size: 12px;
    }

    /* Hero */
    .hero-cta {
        padding: 12px 24px;
        font-size: 15px;
    }

    .hero-badge {
        padding: 12px 24px;
        font-size: 15px;
    }

    /* Sections */
    .section {
        padding: 72px 0;
    }

    .section-title {
        margin-bottom: 32px;
    }

    /* Highlights — narrower cards to show peek of adjacent cards */
    .highlight-card {
        width: min(calc(100vw - 96px), 720px);
    }

    .highlights-scroll {
        padding-left: calc(50vw - min(calc((100vw - 96px) / 2), 360px));
        padding-right: calc(50vw - min(calc((100vw - 96px) / 2), 360px));
    }

    .highlight-card-title {
        font-size: 18px;
    }

    .highlight-card-desc {
        font-size: 14px;
    }

    /* Skills — same accordion component as desktop (no separate pill row /
       floating card here anymore); CSS just repositions it. Desktop lays
       the capsule column out beside the image via grid-template-columns;
       here the column overlays the bottom of the image instead, since
       there's no room for a leading column at phone widths. Image goes
       much taller (3:4) so there's real room for the overlay. */
    .skills-stack {
        display: block;
        /* The padding-left added above is a desktop-only concern (aligning
           the capsule column with the title's text) — full bleed on both
           edges here. */
        padding-left: 0;
        /* Safety margin against the page itself becoming horizontally
           scrollable — .skills-accordion clips its own content via its
           own overflow-x, but this stack already spans exactly the true
           viewport width (full-bleed), so its own edge is a reasonable
           backstop too. */
        overflow: hidden;
    }

    /* Full width by default so the collapsed row below can actually
       scroll its capsules flush to the true screen edges — inset only
       once a skill is focused (.has-open-item), matching the row's own
       padding toggle below so the total inset from the true edge stays
       the same (16px) either way. */
    .skills-accordion-column {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 16px;
        z-index: 2;
    }

    .skills-accordion-column.has-open-item {
        left: 16px;
        right: 16px;
    }

    /* Desktop-only up/down stepper — on mobile, the item immediately
       before/after the focused one plays this role instead (see
       .is-before-open/.is-after-open below), so tapping through the row IS
       the prev/next control; no separate buttons needed. */
    .skills-accordion-nav-wrap {
        display: none;
    }

    /* A horizontal scrolling strip of collapsed capsules — same shape as
       the row this replaced. flex-end keeps every capsule's bottom edge on
       one line. Collapsed width is untouched from the base rules
       (shrink-to-fit), same hug-the-text sizing as desktop, just laid out
       sideways. The 16px side padding (rather than insetting the column
       itself, as .has-open-item does) is what lets the first/last capsule
       still scroll flush to the true edge instead of stopping short of it
       — the scrollable area is the full-width column, padding just gives
       the resting position some breathing room.

       Every item — focused or not — stays a normal, always-in-flow flex
       child of this row for its entire life; nothing here ever switches
       to position: absolute. That's the whole point: a flex child always
       has a real, current width for a CSS transition to animate from, so
       "make room for the growing neighbor" and "keep the focused item
       centered" come from the browser's own flex reflow and scroll
       position instead of hand-computed transform offsets — see
       skills.js's keepCentered for the one piece that still needs JS (a
       growing/shrinking item drifts off-center as its width changes,
       simply because scroll position doesn't know to follow it). */
    .skills-accordion {
        width: 100%;
        flex-direction: row;
        align-items: flex-end;
        overflow-x: auto;
        scrollbar-width: none;
        -ms-overflow-style: none;
        padding: 0 16px;
    }

    .skills-accordion::-webkit-scrollbar {
        display: none;
    }

    /* Generated flex items, first/last in source order — without these,
       centering the first skill would need scrollLeft to go negative,
       which isn't possible, so it'd end up flush against the start
       instead of centered. 40px is exactly half of the 80px (40+40)
       --skills-open-w already reserves on each side of a focused item, so
       scrolled all the way to the start, that alone is enough room to
       bring the first item to true center. Grows from 0 in step with
       everything else (has-open-item below, same undelayed 0.35s) rather
       than sitting there permanently — nothing to center while the row's
       just a plain scrollable strip of collapsed capsules. */
    .skills-accordion::before {
        content: '';
        flex: 0 0 0;
        transition: flex-basis 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
    }

    .skills-accordion.has-open-item::before {
        flex-basis: 40px;
    }

    /* The trailing spacer's job is different — not centering the LAST
       item specifically, just guaranteeing scrollLeft can reach far enough
       for ANY item's target, immediately. skills.js scrolls straight to a
       precomputed final position instead of chasing live geometry — but
       right when that starts, the row's real scrollable width is still
       whatever it was a moment ago, and the browser clamps any scrollLeft
       beyond what currently exists. Snapping this arbitrarily large
       guarantees there's always enough room regardless of which skill is
       targeted — on open AND on close (scrollToClosedIndex, skills.js,
       needs the same guarantee in reverse: room to reach a target further
       along than the still-fully-open row's current layout implies).
       No transition at all, in EITHER direction, unlike the leading one —
       this is never actually visible (past the last real item, clipped by
       overflow: hidden the moment something's focused), so animating it
       only meant its shrink-on-close was competing with the scroll
       animation for scrollLeft: the browser clamping scrollLeft down as
       this spacer's own width dropped, fighting the JS-driven value being
       set the same instant, which was the actual cause of jitter right as
       something closed. */
    .skills-accordion::after {
        content: '';
        flex: 0 0 0;
    }

    .skills-accordion.has-open-item::after {
        flex-basis: 9999px;
    }

    /* Matches .skills-accordion-column's own inset above — the column
       provides the 16px once something's focused, so the row's own
       padding drops to 0 rather than doubling it.

       overflow-x: hidden is what actually stops the user from dragging
       this around by hand once something's focused — touch-action alone
       only blocks touch-driven panning, not trackpad/wheel-driven
       horizontal scrolling (which fires as plain wheel events, not touch
       ones), so scrolling was still possible that way. hidden still lets
       JS keep setting scrollLeft freely — overflow: hidden only removes
       the USER's ability to scroll (no scrollbar, no wheel/touch/drag
       input), it doesn't stop an element from being programmatically
       scrolled — so keepCentered (skills.js) is unaffected. This is a
       different toggle than the old position: absolute system's
       auto/visible switch, which was what actually reset scrollLeft to 0
       (removing overflow entirely, not just hiding scroll input, is what
       caused that) — auto/hidden doesn't do that. touch-action stays too,
       redundant with hidden for touch specifically but still the more
       explicit signal for it, and still what keeps vertical page
       scrolling working through a touch that starts on this row. */
    .skills-accordion.has-open-item {
        padding: 0;
        overflow-x: hidden;
        touch-action: pan-y;
    }

    /* Collapsed width transition — undelayed, so it shrinks back down in
       step with the previously focused item's own height collapsing
       (its body's grid-template-rows, see the base .skills-accordion-body
       rule below), instead of waiting for that to finish first. Safe to
       run together because the body below is pinned to a constant width
       the whole time, so the outer item's own width changing doesn't
       reflow its text. Growing back up to full width (below) is a
       separate override with the same duration/delay, so opening and
       closing move at the same pace. */
    .skills-accordion-item {
        transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
    }

    /* The one rule this component now hinges on: once something's
       focused, EVERY item — the focused one and every other skill alike —
       grows to the same full width, undelayed (so it happens in step with
       the focused item's own growth, not trailing behind it). What used
       to be four separate roles (open/peek/far, each with their own
       position + transform math to fake a "peek") collapses into "every
       card is this wide, and one of them also gets extra height" — see
       .is-open below for that part, and .is-before-open/.is-after-open
       further down for how a non-focused card reads at this width. */
    .skills-accordion.has-open-item .skills-accordion-item {
        width: var(--skills-open-w, auto);
        transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
    }

    /* Pinned to the SAME constant --skills-open-w the outer item itself
       animates to/from (not a live percentage of it, and not the
       desktop-shared rule's --skills-item-w either) — the body's own
       internal width never actually changes here, regardless of what the
       outer item's width is doing at that instant. That's what makes it
       safe for grid-template-rows (height) and the outer item's own width
       transition to run fully together below: the panel's text is always
       laid out at this one fixed width, so there's nothing for it to
       reflow into mid-transition, and the row's 1fr height target stays
       stable instead of recomputing off a moving target. The outer item's
       own overflow: hidden is what clips this down to whatever the
       (possibly narrower) capsule currently looks like. Selector is
       .skills-accordion-prefixed, not just .skills-accordion-item, purely
       to out-specificity the desktop-shared .is-open .skills-accordion-body
       rule's own width: 280px (styles.css, up top) — this needs to win for
       is-open items too, not just non-open ones. */
    .skills-accordion .skills-accordion-item .skills-accordion-body {
        width: var(--skills-open-w, auto);
    }

    /* Fully simultaneous with the outer item's own width transition above
       (no relative delay either way) — safe now that the body's width
       (above) never changes, so there's no reflow to sequence around.
       This also directly benefits stepping between skills (tapping an
       arrow): width never actually changes there at all (every item's
       already at the shared open width), so this is the ONLY transition
       running, and the previously-focused item's own collapse (base
       .skills-accordion-body rule, same 0.35s/no-delay pair) now runs in
       lockstep with it — hide and show happen together instead of one
       waiting on the other. */
    .skills-accordion-item.is-open .skills-accordion-body {
        transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
    }

    /* Text hides/shows instantly rather than fading or riding along with
       the height collapse — since the body above is always full width now
       (not narrowing with the outer item), leaving this to just sit there
       while grid-template-rows collapsed would look like the text getting
       visibly squeezed/clipped away instead of a clean, empty capsule
       shrinking. Instant in both directions, not just on hide, so opening
       and closing read the same way. */
    .skills-accordion-item .skills-accordion-panel {
        opacity: 0;
        transition: opacity 0s linear 0s;
    }

    .skills-accordion-item.is-open .skills-accordion-panel {
        opacity: 1;
    }

    /* Positioning context for .skills-accordion-head-arrow below — it
       overlays the whole item, not just the head, so it needs an anchor
       that isn't affected by the head's own (shrink-wrapped) size. */
    .skills-accordion-item {
        position: relative;
    }

    /* A plain overlay, positioned independently of .skills-accordion-head
       (see the shared, non-mobile rule) rather than sized/laid out as
       part of it — head stays free to shrink-wrap to just its icon+name,
       which is what its own width needs to keep meaning "this item's
       natural collapsed width" (pinCollapsedWidths, skills.js). Matches
       the head's own height/position but spans the item's full width,
       which head itself doesn't. pointer-events off while invisible so
       taps fall through to the head button underneath it. */
    .skills-accordion-head-arrow {
        display: flex;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 56px;
        align-items: center;
        opacity: 0;
        pointer-events: none;
        z-index: 1;
        transition: opacity 0.2s ease;
    }

    /* Crossfade gated behind .is-arrow, not is-before-open/is-after-open
       directly — those apply immediately (skills.js), in step with the
       card's own width, but the content swap itself is added on a short
       delay (also skills.js) so the icon/name don't flip to a chevron the
       instant the card starts growing, well before it's actually shrunk
       down toward arrow-sized yet. */
    .skills-accordion-item.is-arrow .skills-accordion-head {
        opacity: 0;
        pointer-events: none;
    }

    .skills-accordion-item.is-arrow .skills-accordion-head-arrow {
        opacity: 1;
        pointer-events: auto;
    }

    /* Positioned toward whichever edge faces the focused card (its
       trailing edge if this item sits before it, leading edge if after).
       Padding + chevron width together have to fit inside the ~32px
       actually visible (40px --skills-open-w reserve minus the 8px flex
       gap) — 8+20 = 28px still does, with a little margin to spare. */
    .skills-accordion-item.is-before-open .skills-accordion-head-arrow {
        padding: 0 8px 0 0;
        justify-content: flex-end;
    }

    .skills-accordion-item.is-after-open .skills-accordion-head-arrow {
        padding: 0 0 0 8px;
        justify-content: flex-start;
    }

    .skills-accordion-chevron {
        width: 20px;
        height: 20px;
        flex-shrink: 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .skills-accordion-chevron svg {
        display: block;
    }

    .skills-accordion-item.is-before-open .skills-accordion-chevron svg {
        transform: rotate(180deg);
    }

    /* Tags are hidden for now — undecided where they'll end up on mobile,
       and they don't fit the now-more-compact focused view. */
    .skills-accordion-tags {
        display: none;
    }

    .skills-accordion-name {
        font-size: 15px;
    }

    .skills-overview {
        font-size: 14px;
    }

    /* .skills-image-panel dissolves (no box of its own) so #skillsMedia
       behaves as if it were a direct child of .skills-stack again. */
    .skills-image-panel {
        display: contents;
    }

    .skills-image-tags {
        display: none;
    }

    .skills-media {
        /* No aspect-ratio here on purpose — the box doesn't need to match
           a tall/skinny image's own ratio, contain (shared rule) just
           letterboxes it. This element (with the accordion overlaid on
           top of it) is the interactive component itself.

           Height is the real remaining space, not an arbitrary vh guess:
           one full screen (100vh), minus each piece of fixed chrome above
           it. If any of these values changes, this needs to change with
           it — there's nothing enforcing they stay in sync automatically
           (--nav-height aside, which is a real shared variable, not a
           guess). --skills-media-h is set once here and reused below (and
           by .skills-media-layer, which inherits it) so the height and
           the gradient-placeholder sizing can never drift out of sync
           with each other. */
        --skills-top-gap: 32px; /* space between the fixed nav bar and the title */
        --skills-title-h: 60px; /* .section-title's 40px font-size floor x the body's 1.5 line-height */
        --skills-title-gap: 32px; /* .section-title's margin-bottom, mobile override — the actual spacing between the title and this element */
        --skills-media-h: calc(100vh - var(--nav-height) - var(--skills-top-gap) - var(--skills-title-h) - var(--skills-title-gap));
        height: var(--skills-media-h);
        max-height: var(--skills-media-h);
        /* Plain contain now — real images have their own intrinsic
           dimensions, so this no longer needs the explicit-size stand-in
           the gradient placeholders required. */
        background-size: contain;
        /* Back to top-anchored (the shared rule centers it, desktop-only —
           see there) with some clearance, not flush against the very top
           — .skills-accordion-close sits at top: 24px, right: 24px, 36px
           tall, so an image starting right at y: 0 would sit partly
           underneath it. 56px clears that (24 + 36) with a little room to
           spare. */
        background-position: center 56px;
    }

    .skills-media-layer {
        background-size: contain;
        background-position: center 56px;
    }

    /* About */
    .section-about {
        overflow: visible;
        padding-bottom: 100px;
    }

    .about-scroll {
        scroll-snap-type: x mandatory;
        padding-left: 0;
        padding-right: 0;
        scroll-padding-left: 0;
        scroll-padding-right: 0;
        overscroll-behavior-x: contain;
        scroll-snap-stop: always;
        gap: 48px;
        padding-top: 50px;
        padding-bottom: 60px;
        margin-top: -20px;
    }

    .about-scroll::before,
    .about-scroll::after {
        content: '';
        flex: 0 0 auto;
        width: calc(50vw - (68vw / 2));
    }

    .about-card-wrap {
        width: 68vw;
        scroll-snap-align: center;
        transition: transform 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), box-shadow 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
    }

    .about-card-wrap.about-card-centered {
        transform: scale(1.08);
        box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
    }

    .about-card-wrap:hover {
        transform: none;
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    }

    .highlight-card,
    .highlight-card.is-focused {
        transform: none;
    }

    /* Connect */
    .section-connect {
        height: 400px;
    }

    .section-connect .section-title {
        bottom: 340px;
        font-size: 32px;
    }

    .phone-frame {
        width: 280px;
    }

    .phone-screen-container {
        border-radius: 36px;
    }

    .phone-icons-grid {
        padding: 55px 20px 20px;
        gap: 14px;
    }

    .phone-app-icon-img {
        width: 46px;
        height: 46px;
        border-radius: 11px;
    }

    .phone-app-icon-label {
        font-size: 9px;
    }
}

/* ==========================================
   Responsive: Small Phones (≤ 600px)
   ========================================== */
@media (max-width: 600px) {
    /* At ~320px wide, "Milo Cassarino" + the nav links no longer fit on one
       line (they land right at the edge of the available width) and the
       logo text wraps, growing the fixed-height nav bar and pushing it
       into the content below. Trim size/gap so it stays on one line. */
    .nav-logo {
        font-size: 16px;
        white-space: nowrap;
    }

    .nav-links {
        gap: 12px;
    }

    .about-card {
        width: 70vw;
    }

    .phone-frame {
        width: 260px;
    }

    .phone-screen-container {
        border-radius: 32px;
    }

    .phone-icons-grid {
        padding: 48px 16px 16px;
        gap: 10px;
    }

    .phone-app-icon-img {
        width: 42px;
        height: 42px;
        border-radius: 10px;
    }
}
