:root {
    --bg: #080808;
    --surface: #111111;
    --surface-raised: #1a1a1a;
    --ink: #f7f4ed;
    --muted: #a7a49d;
    --line: rgba(247, 244, 237, .16);
    --blue: #7ba8d8;
    --red: #b7b9be;
    --yellow: #d4d5d8;
    --ease: cubic-bezier(.2,.75,.2,1);
    --site-max-width: 1440px;
    --nav-max-width: 1440px;
    --hero-content-max-width: 1080px;
    --project-card-height: 680px;
    --project-card-height-mobile: 650px;
    --project-image-height: 240px;
    --carousel-image-height: 240px;
    --ring-image-height: 240px;
    --ring-image-height-compact: 220px;
    --ring-height-min: 520px;
    --ring-height-max: 560px;
    --modal-max-width: 820px;
    --modal-max-height: 860px;

    /* ---- Spacing ------------------------------------------------------
       One scale, 4px base. Its structural steps -- 12, 24, 48, 96 -- are
       exact subdivisions of the 48px backdrop lattice, so the page rhythm
       and the grid behind it are the same measure rather than two systems
       that happen to share a page. Nothing below sets a raw spacing value.  */
    --space-1: .25rem;   /*  4px */
    --space-2: .5rem;    /*  8px */
    --space-3: .75rem;   /* 12px -- lattice minor */
    --space-4: 1rem;     /* 16px */
    --space-5: 1.5rem;   /* 24px -- half a lattice module: the gutter */
    --space-6: 2rem;     /* 32px */
    --space-7: 3rem;     /* 48px -- one lattice module */
    --space-8: 4rem;     /* 64px */
    --space-9: 6rem;     /* 96px -- two lattice modules */

    /* Every column gap on the page, so About, Skills, Projects and Contact
       all resolve to identical column widths and line up top to bottom. */
    --gutter: var(--space-5);
    --pad-card: var(--space-5);
    --pad-panel: clamp(var(--space-5), 4vw, var(--space-7));

    /* ---- Vertical rhythm ----------------------------------------------
       Five gaps, and every stacked pair on the site is one of them. Which
       one it is depends on what the two things are to each other, not on
       where they sit, so the hero's badge sits above its name at exactly
       the distance a section's label sits above its title.

         --rhythm-label    12px  an eyebrow label to the heading it names
         --rhythm-heading  16px  a heading to the line that supports it
         --rhythm-rule     16px  the clear either side of a rule in a card
         --rhythm-block    24px  one block to the next inside a section
         --rhythm-header   48px  a header to the content it heads
         --rhythm-section  48-96px  a section to the next section

       These are *optical* distances: the display type below is leading-
       trimmed (see the end of this file), so what is written here is the
       gap you see between one block's letters and the next's, not the gap
       between two boxes that each carry their own dead space.               */
    --rhythm-label: var(--space-3);
    --rhythm-heading: var(--space-4);
    /* A rule inside a card is a heading-strength break, not a block-strength
       one, so it takes the same clear on both sides as a heading does. */
    --rhythm-rule: var(--space-4);
    --rhythm-block: var(--space-5);
    --rhythm-header: var(--space-7);
    --rhythm-section: clamp(var(--space-7), 6vw, var(--space-9));

    /* Where a jumped-to anchor comes to rest. The navbar is fixed and 78px
       tall, so without this a nav link parks the section's label underneath it
       and the eyebrow -- Toolkit, Portfolio, Connect -- is simply not there.
       One block's clear below the bar, so the landing looks deliberate. */
    --scroll-clearance: calc(4.875rem + var(--rhythm-block));

    /* Font metrics for that trim, in em, read off the shipped woff2 files
       with canvas TextMetrics (tools/measure-faces.js reproduces them).
       `ascent`/`descent` are the hhea box the line box is built from;
       `cap` is the top of a capital, which is where a heading looks like
       it starts. Barlow Condensed carries the same metrics at every weight
       the site loads, so one set covers all of them. */
    --face-display-ascent: 1;
    --face-display-descent: .2;
    --face-display-cap: .7031;
    --face-text-ascent: .969;
    --face-text-descent: .241;
    --face-text-cap: .7344;

    /* One hover language: controls rise a little, cards rise more. */
    --lift-control: -3px;
    --lift-card: -6px;

}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; scrollbar-width: none; background-color: var(--bg); }
body { position: relative; isolation: isolate; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--ink); line-height: 1.55; overflow-x: hidden; -ms-overflow-style: none; transition: background-color .65s ease; }
html::-webkit-scrollbar,
body::-webkit-scrollbar { display: none; }
a { color: inherit; text-decoration: none; }
button { font: inherit; }

.background-grid {
    /* Fixed, not absolute: in Low FX this is the lattice, and a lattice that
       slides away with the document is a moving grid. Pinned to the viewport it
       matches what the High FX canvas draws. */
    position: fixed;
    inset: 0;
    z-index: 0;
    display: none;
    pointer-events: none;
    background-image:
        linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
    background-size: 3rem 3rem;
    opacity: 1;
    transition: opacity 1.4s ease;
}
.galaxy-field {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    width: 100%;
    height: 100%;
    opacity: .94;
    transition: opacity 1.4s ease;
    contain: strict;
}
.galaxy-nebula {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* The canvas paints cluster-linked haze; this is only a dark veil. */
    background-image: radial-gradient(ellipse at 50% 45%, transparent 25%, rgba(0,0,0,.18) 100%);
    background-size: 100% 100%;
    background-repeat: no-repeat;
    opacity: .8;
    transition: opacity 1.4s ease;
}
.galaxy-nebula::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(ellipse at center, transparent 42%, rgba(0,0,0,.34) 100%);
}
.cursor-dot {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 4000;
    display: none;
    width: .5rem;
    height: .5rem;
    pointer-events: none;
    border-radius: 50%;
    border: 0;
    background: #a8d5ff;
    box-shadow: 0 0 7px rgba(145,200,255,.9);
    opacity: 0;
    transition: width .12s var(--ease), height .12s var(--ease), opacity .12s ease, background .12s ease;
    will-change: transform;
}
.cursor-dot::before {
    content: none;
}
.cursor-dot::after {
    content: none;
}
.enhanced-pointer .cursor-dot { display: block; }
.enhanced-pointer,
.enhanced-pointer body,
.enhanced-pointer body * { cursor: none !important; }
.cursor-dot.is-visible { opacity: 1; }
.cursor-dot.is-interactive { width: .75rem; height: .75rem; background: #c6e5ff; }
.cursor-dot.is-pressed { width: .375rem; height: .375rem; }
.cursor-ripple {
    position: fixed;
    z-index: 3999;
    width: .625rem;
    height: .625rem;
    pointer-events: none;
    border: 1px solid rgba(145,200,255,.75);
    border-radius: 50%;
    animation: cursor-ring .42s var(--ease) forwards;
}
.cursor-spark {
    position: fixed;
    z-index: 3998;
    width: var(--spark-size, 2px);
    height: var(--spark-size, 2px);
    pointer-events: none;
    border-radius: 50%;
    background: #91c8ff;
    box-shadow: 0 0 5px rgba(145,200,255,.75);
    animation: cursor-spark-out .58s cubic-bezier(.15,.7,.25,1) forwards;
}
@keyframes cursor-ring {
    from { opacity: .8; transform: translate(-50%, -50%) scale(.55); }
    to { opacity: 0; transform: translate(-50%, -50%) scale(4); }
}
@keyframes cursor-spark-out {
    from { opacity: .82; transform: translate(-50%, -50%); }
    to { opacity: 0; transform: translate(calc(-50% + var(--spark-x)), calc(-50% + var(--spark-y))) scale(.2); }
}

html.effects-background-idle body { background-color: #000; }
html.effects-background-click-fading body {
    background-color: #000;
    transition-duration: 3.5s;
}

.navbar, .hero-section, main, footer { position: relative; z-index: 1; }
.page-scroll-progress {
    position: fixed;
    top: 18vh;
    right: 18px;
    z-index: 1200;
    width: 15px;
    height: 64vh;
    pointer-events: none;
}
.page-scroll-progress::before,
.page-scroll-progress::after {
    content: "";
    position: absolute;
    left: 0;
    width: 15px;
    height: 1px;
    background: rgba(123,168,216,.65);
}
.page-scroll-progress::before { top: 0; }
.page-scroll-progress::after { bottom: 0; }
.page-scroll-track {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 7px;
    width: 1px;
    background: rgba(247,244,237,.13);
}
.page-scroll-fill {
    position: absolute;
    inset: 0 -1px auto;
    height: 100%;
    background: var(--blue);
    box-shadow: 0 0 8px rgba(123,168,216,.28);
    transform: scaleY(0);
    transform-origin: top;
}
@media (max-width: 800px) {
    .galaxy-nebula {
        opacity: .58;
    }
    .page-scroll-progress {
        right: 6px;
        width: 9px;
        opacity: .72;
    }
    .page-scroll-progress::before,
    .page-scroll-progress::after {
        width: 9px;
    }
    .page-scroll-track { left: 4px; }
}

.navbar { position: fixed; inset: 0 0 auto; z-index: 1000; border-bottom: 1px solid transparent; transition: background .2s ease, border-color .2s ease; }
.has-scrolled .navbar { background: #080808; border-color: var(--line); }
.nav-content { width: min(var(--nav-max-width), calc(100% - var(--space-7))); min-height: 4.5rem; display: flex; align-items: center; justify-content: space-between; margin: auto; }
.logo {
    display: inline-block;
    color: var(--ink);
    font-size: .98rem;
    font-weight: 700;
    letter-spacing: -.03em;
}
.nav-links { list-style: none; display: flex; gap: var(--space-5); color: var(--muted); font-size: .85rem; }
.nav-links a { transition: color .2s ease; }
.nav-links a:hover { color: var(--blue); }
.performance-control { display: flex; align-items: center; }
.performance-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    font-size: inherit;
    font-weight: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
}
.performance-toggle:hover,
.performance-toggle:focus-visible { color: var(--blue); }
.performance-toggle-track {
    position: relative;
    width: 1.875rem;
    height: 1rem;
    border: 1px solid rgba(247,244,237,.28);
    border-radius: 62.4375rem;
    background: #151515;
    transition: background .2s ease, border-color .2s ease;
}
.performance-toggle-track i {
    position: absolute;
    top: .1875rem;
    left: .1875rem;
    width: .5rem;
    height: .5rem;
    border-radius: 50%;
    background: var(--muted);
    transition: transform .2s var(--ease), background .2s ease;
}
.performance-toggle[aria-checked="true"] .performance-toggle-track {
    border-color: var(--blue);
    background: rgba(123,168,216,.16);
}
.performance-toggle[aria-checked="true"] .performance-toggle-track i {
    background: var(--blue);
    transform: translateX(.875rem);
}

.hero-section { min-height: 92vh; display: grid; place-items: center; overflow: hidden; isolation: isolate; padding: var(--space-9) var(--space-5) var(--space-8); }
.hero-section::before, .hero-section::after { content: ""; position: absolute; z-index: -1; transition: transform .5s var(--ease); }
.hero-section::before { width: min(34vw, 500px); aspect-ratio: 1; border-radius: 50%; background: #5e7186; right: -8vw; top: 14%; opacity: .19; transform: translateY(var(--scroll-shift)); }
.hero-section::after { width: clamp(110px, 16vw, 220px); aspect-ratio: 1; background: #94999f; left: 8%; bottom: 12%; transform: rotate(45deg) translateY(var(--scroll-shift-reverse)); opacity: .13; }
.hero-content { width: min(100%, 56.25rem); text-align: center; }
.badge { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); background: rgba(21,22,25,.68); border: 1px solid var(--line); color: var(--blue); font-size: .75rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.hero-name { --lh: .87; max-width: 56.25rem; margin: 0 auto; font-size: clamp(3.8rem, 10.2vw, 8.5rem); font-weight: 700; letter-spacing: -.085em; line-height: var(--lh); }
.hero-tagline { --lh: 1.55; line-height: var(--lh); max-width: 40.625rem; margin: var(--rhythm-heading) auto 0; color: #d0d1d5; font-size: clamp(1rem, 2vw, 1.25rem); }
.social-links { display: flex; justify-content: center; gap: var(--space-3); margin: var(--rhythm-block) 0; }
.social-links a { display: grid; place-items: center; width: 2.5rem; aspect-ratio: 1; background: var(--surface); border: 1px solid var(--line); transition: transform .25s var(--ease), background .25s; }
.social-links a:hover { background: var(--surface-raised); color: var(--ink); --control-lift: var(--lift-control); }
.resume-icon-unavailable { display: grid; place-items: center; width: 2.5rem; aspect-ratio: 1; background: var(--surface); border: 1px solid var(--line); color: inherit; }
.hero-buttons, .contact-links { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; }
.btn { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-4); font-size: .86rem; font-weight: 700; border: 1px solid var(--line); transition: transform .25s var(--ease), background .25s, color .25s; }
.btn:hover { --control-lift: var(--lift-control); }
.resume-unavailable { cursor: not-allowed; opacity: .65; }
.resume-unavailable:hover { background: #e4e5e7; transform: none; }
.secondary-btn { background: var(--surface); }
.secondary-btn:hover { background: var(--surface-raised); }
main { width: min(var(--site-max-width), calc(100% - clamp(var(--space-6), 6vw, var(--space-9)))); margin: auto; padding: 0; }
.section { scroll-margin-top: var(--scroll-clearance); padding: var(--rhythm-section) 0; opacity: .72; transform: translateY(12px); transition: opacity .65s var(--ease), transform .65s var(--ease); content-visibility: auto; contain-intrinsic-size: auto 800px; }
.section.in-view { opacity: 1; transform: translateY(0); }
.section-header { max-width: 45rem; margin-bottom: var(--rhythm-header); }
.section-label { --lh: 1.55; line-height: var(--lh); display: block; margin-bottom: var(--rhythm-label); color: var(--yellow); font-size: .72rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
.section-title { --lh: .92; font-size: clamp(2.6rem, 6vw, 5rem); letter-spacing: -.075em; line-height: var(--lh); }
.section-subtitle, .library-heading p, .contact-desc { --lh: 1.55; line-height: var(--lh); margin-top: var(--rhythm-heading); color: var(--muted); max-width: 38.125rem; }
.glass-panel { background: var(--surface); border: 1px solid var(--line); padding: var(--pad-panel); }
.bio-text { --lh: 1.55; line-height: var(--lh); width: 100%; max-width: none; color: #dedee0; font-size: clamp(1.05rem, 2vw, 1.35rem); }
.about-highlights { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: var(--rhythm-block); background: var(--surface); border: 0; }
.about-highlight p { --lh: 1.55; line-height: var(--lh); }
.about-highlight { position: relative; z-index: 0; display: flex; flex-direction: column; min-height: 10.3125rem; padding: var(--pad-card); background: var(--surface); border: 1px solid var(--line); }
.about-highlight + .about-highlight { margin-left: -1px; }
.about-highlight > .about-highlight-label { --lh: 1.55; line-height: var(--lh); display: block; margin-bottom: var(--rhythm-label); color: var(--yellow); font-size: .68rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.about-highlight h3 { --lh: 1.05; margin-bottom: var(--rhythm-heading); font-size: 1.1rem; letter-spacing: -.035em; line-height: var(--lh); }
.about-highlight p { color: var(--muted); font-size: .87rem; }
.about-highlight .about-highlight-subline { --lh: 1.45; margin-top: var(--rhythm-heading); color: var(--ink); font-size: .76rem; line-height: var(--lh); }
.about-highlight-role { margin-top: var(--rhythm-rule); padding-top: var(--rhythm-rule); border-top: 1px solid var(--line); }
.about-highlight-role h4 { --lh: 1.1; line-height: var(--lh); color: var(--ink); font-family: "Barlow Condensed", sans-serif; font-size: 1rem; letter-spacing: .01em; text-transform: uppercase; }
.about-highlight-role p { --lh: 1.55; line-height: var(--lh); margin-top: var(--rhythm-heading); color: #b9d6f5; font-size: .76rem; font-weight: 700; }
.about-highlight-link { display: flex; color: inherit; text-decoration: none; cursor: pointer; }
.about-highlight-cta { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-top: auto; padding-top: var(--rhythm-rule); color: var(--ink); font-size: .72rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; transition: color .18s ease; }
/* At rest the arrow points up and the row reads as ordinary text. Under the
   cursor it turns blue and flips to point down at the collection it opens --
   the glyph is a down arrow held upside down, so hovering rotates it back the
   right way up rather than swapping it for a different icon. */
.about-highlight-cta i { transform: rotate(180deg); transition: transform .18s var(--ease), color .18s ease; }
.about-highlight-link:hover .about-highlight-cta,
.about-highlight-link:focus-visible .about-highlight-cta { color: var(--blue); }
.about-highlight-link:hover .about-highlight-cta i,
.about-highlight-link:focus-visible .about-highlight-cta i { transform: rotate(360deg); }
#about > .section-header, #about > .bio-text,
#contact > .section-header, #contact > .contact-desc, #contact > .contact-links { margin-left: 0; }

.skills-categories { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); border-left: 1px solid var(--line); }
.skill-group { display: flex; flex-direction: column; justify-content: center; min-height: 13.75rem; padding: var(--pad-card); background: var(--surface); border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); transition: transform .2s var(--ease), background .2s ease, border-color .2s ease, box-shadow .2s ease; }
.skill-group:hover { position: relative; z-index: 2; background: #151d26; border-color: var(--blue); box-shadow: 0 6px 0 rgba(123,168,216,.22); --card-lift: var(--lift-card); }
.skill-group-title { --lh: 1.55; line-height: var(--lh); margin-bottom: var(--rhythm-heading); font-size: 1.05rem; letter-spacing: -.03em; }
.skill-tags, .project-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag { padding: var(--space-1) var(--space-2); border: 1px solid rgba(85,168,255,.35); color: #9dccff; font-size: .7rem; font-weight: 700; }

.projects-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gutter); }
.featured-grid { margin-bottom: var(--rhythm-section); }
.project-card { display: flex; height: var(--project-card-height); flex-direction: column; min-width: 0; overflow: hidden; background: var(--surface); border: 1px solid var(--line); cursor: pointer; transition: transform .35s var(--ease), box-shadow .35s var(--ease), border-color .35s; }
.project-card:hover { --card-lift: var(--lift-card); border-color: rgba(123,168,216,.42); box-shadow: 0 14px 30px rgba(0,0,0,.22); }
.project-card[role="button"]:focus-visible { outline: 2px solid var(--blue); outline-offset: 4px; }
.project-image-wrapper { position: relative; height: var(--project-image-height); overflow: hidden; background: #34363d; }
.project-image { width: 100%; height: 100%; object-fit: contain; object-position: center; background: #0c0c0c; transition: transform .25s var(--ease); }
.project-image.is-certificate-thumbnail { object-fit: cover; object-position: center top; transform: scale(1.35); transform-origin: center top; }
.project-card:hover .project-image.is-certificate-thumbnail { transform: scale(1.35); }
.project-card:hover .project-image { transform: scale(1.012); }
.project-category-badge { position: absolute; top: var(--space-4); left: var(--space-4); padding: var(--space-1) var(--space-2); color: var(--ink); background: rgba(21,22,25,.78); border: 1px solid rgba(244,244,241,.18); font-size: .67rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
.project-info { display: flex; flex: 1; min-width: 0; flex-direction: column; padding: var(--pad-card); }
.project-heading { flex: 0 0 auto; }
.project-copy { display: flex; flex: 1; align-items: center; min-height: 0; }
.project-title { --lh: 1.08; margin-bottom: var(--rhythm-heading); font-size: 1.2rem; letter-spacing: -.04em; line-height: var(--lh); }
.project-period { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--rhythm-heading); color: #9dccff; font-size: .76rem; font-weight: 800; letter-spacing: .02em; }
.project-summary { --lh: 1.55; color: var(--muted); font-size: 1rem; line-height: var(--lh); }
.project-tags { margin-top: var(--rhythm-block); }
.project-action-links { display: flex; gap: var(--space-2); flex-wrap: wrap; padding-top: var(--rhythm-rule); margin-top: var(--rhythm-rule); border-top: 1px solid var(--line); }
.link-btn { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-2) var(--space-3); background: #303138; border: 1px solid var(--line); font-size: .72rem; font-weight: 700; transition: background .2s; }
.link-btn:hover { background: #3a3c43; color: var(--ink); }

.project-library { padding-top: var(--rhythm-section); border-top: 1px solid var(--line); }
.library-heading { max-width: 42.5rem; margin-bottom: var(--rhythm-header); }
.library-heading h3 { --lh: .9; font-size: clamp(2.4rem, 6vw, 4.5rem); letter-spacing: -.075em; line-height: var(--lh); }
.project-collections { border-top: 1px solid var(--line); }
.project-collection { border-bottom: 1px solid var(--line); scroll-margin-top: var(--scroll-clearance); }
.collection-toggle { width: 100%; display: grid; grid-template-columns: var(--space-7) 1fr var(--space-6); gap: var(--gutter); align-items: center; padding: var(--space-5) 0; border: 0; background: transparent; color: var(--ink); cursor: pointer; text-align: left; transition: color .2s; }
.collection-toggle:hover { color: #dfe4ea; }
.collection-index { color: var(--red); font-size: .76rem; font-weight: 800; letter-spacing: .1em; }
.collection-copy { display: grid; min-width: 0; gap: var(--rhythm-heading); }
.collection-copy strong { --lh: 1.55; line-height: var(--lh); font-size: clamp(1.2rem, 2.5vw, 1.75rem); letter-spacing: -.05em; }
.collection-copy small { --lh: 1.55; line-height: var(--lh); color: var(--muted); }
.collection-copy strong, .collection-copy small, .project-title, .project-summary, .modal-title { overflow-wrap: anywhere; }
.collection-toggle > i { justify-self: end; transition: transform .3s var(--ease); }
.collection-toggle[aria-expanded="true"] > i { color: var(--blue); transform: rotate(180deg); }
.collection-content { padding-bottom: var(--rhythm-block); }
.carousel-controls { display: flex; justify-content: space-between; align-items: center; padding-bottom: var(--rhythm-heading); color: var(--muted); font-size: .76rem; }
.carousel-controls > div { display: flex; gap: var(--space-2); }
.carousel-arrow { width: 2.25rem; height: 2.25rem; border: 1px solid var(--line); background: transparent; color: var(--ink); cursor: pointer; transition: transform .2s var(--ease), background .2s, border-color .2s, color .2s; }
.carousel-arrow:hover, .carousel-arrow:focus-visible { color: #080808; background: var(--blue); border-color: var(--blue); --control-lift: var(--lift-control); }
.carousel-arrow.is-nudging { animation: nudge .32s ease; }
@keyframes nudge { 50% { transform: scale(.85); } }
.project-carousel { display: flex; gap: var(--gutter); margin-top: 0; padding-top: 0; padding-bottom: var(--rhythm-block); overflow-x: hidden; scroll-snap-type: none; scrollbar-width: none; touch-action: pan-y pinch-zoom; }
.project-carousel::-webkit-scrollbar { display: none; }
.project-carousel .project-card { flex: 0 0 calc((100% - var(--gutter) * 2) / 3); scroll-snap-align: start; }
/* Off while the ring is idle, and switched back on per card by the layout
   pass, which is the only thing that knows whether a given clone is one of
   the five the visitor can actually see. A clone standing on the ring is
   the same card to the eye and answers the pointer like one. */
.project-carousel .carousel-clone { pointer-events: none; }
.project-carousel .project-image-wrapper, .compact-project-grid .project-image-wrapper { height: var(--carousel-image-height); }
.project-carousel .project-action-links, .compact-project-grid .project-action-links { display: none; }
.compact-project-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gutter); padding-bottom: var(--rhythm-block); }

.contact-section { margin-top: 0; }
.contact-links { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gutter); margin-top: var(--rhythm-block); }
.contact-links .btn { justify-content: center; min-height: 3.375rem; text-align: center; }
.contact-links .resume-unavailable { opacity: 1; cursor: default; }

.modal-overlay { position: fixed; inset: 0; z-index: 2000; display: flex; justify-content: center; align-items: center; padding: var(--space-4); overflow-y: auto; background: rgba(5,6,8,.94); opacity: 0; pointer-events: none; transition: opacity .2s ease; }
.modal-overlay.active { opacity: 1; pointer-events: auto; }
.modal-card { position: relative; width: min(var(--modal-max-width), 100%); max-height: min(88vh, var(--modal-max-height)); overflow-y: auto; overscroll-behavior: contain; padding: var(--pad-panel); background: var(--surface); color: var(--ink); border-color: rgba(244,244,241,.2); box-shadow: none; }
.modal-close { position: absolute; top: var(--space-4); right: var(--space-4); z-index: 2; width: 2.625rem; height: 2.625rem; margin: 0; border: 1px solid var(--line); border-radius: 50%; background: var(--surface-raised); color: var(--ink); cursor: pointer; transition: transform .2s var(--ease), background .2s; }
.modal-close:hover { background: #3a3c43; color: var(--ink); transform: rotate(90deg); }
.modal-header { display: block; min-height: 2.625rem; padding-right: var(--space-8); margin-bottom: var(--rhythm-block); }
.modal-title { --lh: .95; max-width: 37.5rem; font-size: clamp(1.8rem, 5vw, 3.5rem); letter-spacing: -.065em; line-height: var(--lh); }
.modal-header-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin: 0 0 var(--rhythm-block); }
.modal-header-actions .tag { color: #9dccff; border-color: rgba(85,168,255,.35); }
.modal-quick-link { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-2) var(--space-3); color: var(--ink); border: 1px solid rgba(85,168,255,.5); font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; transition: background .2s, color .2s; }
.modal-quick-link:hover { color: #080808; background: var(--blue); }
.modal-quick-link[hidden] { display: none; }
.modal-period { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--rhythm-heading); color: #9dccff; font-size: .82rem; font-weight: 800; }
.modal-period[hidden] { display: none; }
.modal-program-summary { --lh: 1.6; margin: 0 0 var(--rhythm-block); max-width: 42.5rem; color: var(--muted); font-size: .88rem; line-height: var(--lh); }
.modal-program-summary[hidden] { display: none; }
.modal-image { display: block; width: 100%; height: auto; max-height: 410px; aspect-ratio: 16 / 9; min-height: 0; margin-bottom: var(--rhythm-block); overflow: hidden; background: #0c0c0c; }
.modal-image img { width: 100%; height: 100%; display: block; object-fit: contain; object-position: center; }
.modal-image.is-certificate { background: #0c0c0c; }
.modal-image.is-certificate img { width: 100%; height: 100%; object-fit: contain; }
.modal-image.has-motion-media { display: block; height: auto; max-height: 410px; aspect-ratio: 16 / 9; background: #0c0c0c; }
.modal-image.has-motion-media img { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
.modal-summary { --lh: 1.55; line-height: var(--lh); color: #dedee0; font-size: 1.12rem; font-weight: 600; }
.modal-overview[hidden], .modal-deep-dive-heading[hidden] { display: none; }
.modal-overview { margin: var(--rhythm-header) 0; padding: var(--space-5); border: 1px solid rgba(85,168,255,.28); background: linear-gradient(145deg, rgba(85,168,255,.07), rgba(255,255,255,.018) 44%, transparent); }
.modal-section-heading { display: flex; align-items: end; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--rhythm-block); }
.modal-section-heading span { color: #9dccff; font-size: .74rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
.modal-section-heading h3 { font-family: "Barlow Condensed", sans-serif; font-size: clamp(1.8rem, 4vw, 2.6rem); letter-spacing: -.025em; line-height: .95; text-transform: uppercase; }
.modal-overview-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gutter); background: transparent; }
.modal-overview-grid.is-class-rundown { grid-template-columns: 1fr; }
.modal-overview-item { position: relative; min-width: 0; padding: var(--pad-card); overflow: hidden; border: 1px solid rgba(244,244,241,.11); background: linear-gradient(155deg, rgba(49,51,58,.96), rgba(29,30,35,.96)); box-shadow: 0 12px 28px rgba(0,0,0,.16); }
.modal-overview-item::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: linear-gradient(to bottom, #b8dcff, var(--blue) 46%, rgba(85,168,255,.12)); }
.modal-overview-item > span { --lh: .82; line-height: var(--lh); display: block; margin-bottom: var(--rhythm-heading); color: #9dccff; font-family: "Barlow Condensed", sans-serif; font-size: clamp(2.35rem, 5vw, 3.35rem); font-weight: 800; letter-spacing: -.045em; line-height: .82; }
.modal-overview-item h4 { --lh: 1.05; line-height: var(--lh); margin-bottom: var(--rhythm-heading); font-family: "Barlow Condensed", sans-serif; font-size: clamp(1.2rem, 2vw, 1.4rem); letter-spacing: .015em; line-height: 1.05; text-transform: uppercase; }
.modal-overview-item p { color: #c7c7cb; font-size: .91rem; line-height: 1.7; }
.modal-feature-video { margin: 0 0 var(--rhythm-header); }
.modal-feature-video[hidden] { display: none; }
.modal-feature-video-player { width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: #191a1f; border: 1px solid var(--line); }
.modal-feature-video-player iframe { width: 100%; height: 100%; display: block; border: 0; }
.modal-deep-dive-heading { margin-top: var(--rhythm-header); margin-bottom: var(--rhythm-block); }
.modal-details { margin: var(--rhythm-heading) 0 var(--rhythm-block); color: var(--muted); line-height: 1.75; }
.modal-detail-section + .modal-detail-section { margin-top: var(--rhythm-block); }
.modal-detail-section h4 { margin-bottom: var(--rhythm-heading); color: var(--ink); font-size: 1.05rem; letter-spacing: -.025em; }
.modal-media { display: grid; grid-template-columns: 1fr; gap: var(--gutter); margin: 0 0 var(--rhythm-block); align-items: start; }
.modal-media-item { display: flex; width: 100%; min-height: 0; margin: 0; flex-direction: column; align-self: start; background: #0c0c0c; overflow: hidden; }
.modal-media-item img { width: 100%; height: auto; display: block; object-fit: contain; object-position: center; }
.modal-media-item.media-contain { background: #0c0c0c; }
.modal-media-item.media-contain img { height: auto; padding: var(--space-2); object-fit: contain; }
.modal-media-item.is-motion-media { background: #0c0c0c; }
.modal-media-item.is-motion-media img { height: auto; object-fit: contain; }
.modal-media-item figcaption { flex: 0 0 auto; padding: var(--space-3) var(--space-4); background: #34363d; color: var(--muted); font-size: .78rem; font-weight: 700; }
.modal-video iframe { width: 100%; height: auto; aspect-ratio: 16 / 9; display: block; border: 0; }
.media-placeholder { display: grid; place-content: center; gap: var(--space-2); min-height: 100%; padding: var(--space-4); border: 1px dashed rgba(244,244,241,.28); color: var(--muted); text-align: center; }
.media-placeholder i { font-size: 1.35rem; color: #9dccff; }
.media-placeholder span { color: var(--ink); font-size: .83rem; font-weight: 800; }
.media-placeholder small { max-width: 11.875rem; font-size: .72rem; }
.card-media-placeholder { height: 100%; color: #d6d7da; border-color: rgba(244,244,241,.25); }
.card-media-placeholder i, .card-media-placeholder span { color: #f4f4f1; }
.modal-card .tag { color: #9dccff; border-color: rgba(85,168,255,.35); }
.modal-card .project-action-links { border-color: var(--line); }
.modal-card .link-btn { background: #303138; border-color: var(--line); color: var(--ink); }
.modal-card .link-btn:hover { background: #3a3c43; }

footer { padding: var(--space-7) var(--space-5); border-top: 1px solid var(--line); color: var(--muted); text-align: center; font-size: .82rem; }

@media (max-width: 800px) {
    .nav-content, main { width: min(100% - 2rem, var(--site-max-width)); }
    .nav-links { display: none; }
    .hero-section::before { width: 260px; right: -110px; }
    .hero-section::after { width: 100px; left: -30px; }
    .featured-grid { margin-bottom: var(--rhythm-section); }
    .carousel-controls span { display: none; }
    .modal-overlay { align-items: flex-start; padding: var(--space-3); }
    .modal-card { max-height: 94vh; }
    .modal-header { padding-right: var(--space-7); }
    .modal-overview-grid { grid-template-columns: 1fr; }
    .modal-section-heading { align-items: flex-start; flex-direction: column; }
    .modal-media { grid-template-columns: 1fr; }
}

body { background-color: var(--bg); background-image: none; }
.navbar { border-bottom-color: rgba(247,244,237,.09); background: #080808; }
.nav-content, main { width: min(var(--site-max-width), calc(100% - clamp(var(--space-6), 6vw, var(--space-9)))); }

.nav-content { min-height: 4.875rem; }
.logo {
    font-family: "Barlow Condensed", sans-serif;
    font-size: 1.35rem;
    letter-spacing: .02em;
    text-transform: uppercase;
}
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
    .logo {
        color: transparent;
        -webkit-text-fill-color: transparent;
        background-image:
            linear-gradient(to top, var(--blue) 0 72%, #a8cff7 88%, var(--blue) 100%),
            linear-gradient(var(--ink), var(--ink));
        background-position: left bottom, left top;
        background-repeat: no-repeat;
        background-size: 100% 0%, 100% 100%;
        -webkit-background-clip: text;
        background-clip: text;
        filter: drop-shadow(0 0 0 rgba(123,168,216,0));
        transition:
            background-size .35s cubic-bezier(.22, .72, .2, 1),
            filter .35s ease;
    }
    .logo:hover,
    .logo:focus-visible {
        background-size: 100% 100%, 100% 100%;
        filter: drop-shadow(0 0 .35rem rgba(123,168,216,.32));
    }
}
.nav-links { gap: var(--space-5); font-size: .72rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.hero-section {
    min-height: auto;
    place-items: start;
    padding:
        calc(4.875rem + var(--space-7))
        max(calc(clamp(var(--space-6), 6vw, var(--space-9)) / 2), calc((100vw - var(--site-max-width)) / 2))
        0;
}
.hero-section::before { width: min(42vw, 680px); border-radius: 0; background: var(--blue); right: -13vw; top: 7%; opacity: .12; transform: rotate(45deg) translateY(var(--scroll-shift)); }
.hero-section::after { width: clamp(90px, 12vw, 175px); background: var(--red); left: auto; right: 16%; bottom: 9%; opacity: .68; }
.hero-content { width: min(100%, var(--hero-content-max-width)); max-width: var(--hero-content-max-width); text-align: left; }
.hero-badge { color: var(--blue); background: transparent; border: 0; padding: 0; font-family: "Barlow Condensed", sans-serif; font-size: .95rem; letter-spacing: .16em; }
.hero-name, .section-title, .library-heading h3, .modal-title { font-family: "Barlow Condensed", Impact, sans-serif; font-weight: 700; text-transform: uppercase; }
.hero-name { --lh: .74; max-width: var(--hero-content-max-width); margin: 0; font-size: clamp(5rem, 13vw, 11.5rem); letter-spacing: -.055em; line-height: var(--lh); }
.hero-tagline { max-width: 35rem; margin: var(--rhythm-heading) 0 0; color: var(--muted); font-size: clamp(1rem, 1.8vw, 1.2rem); }
.social-links { justify-content: flex-start; margin: var(--rhythm-block) 0; }
.social-links a, .resume-icon-unavailable { border-radius: 0; background: transparent; }
.social-links a:hover,
.resume-icon-unavailable:hover { background: var(--blue); color: #080808; --control-lift: var(--lift-control); }
.hero-buttons { justify-content: flex-start; }
.btn { border-radius: 0; font-size: .74rem; letter-spacing: .08em; text-transform: uppercase; }
.secondary-btn { background: transparent; }
.hero-buttons .btn {
    background: transparent;
    border-color: var(--line);
    color: var(--ink);
}
.hero-buttons .btn:hover,
.hero-buttons .btn:active {
    background: var(--blue);
    border-color: var(--blue);
    color: #080808;
}
main { padding-top: 0; }
.section { transition-duration: .42s; }
.section-label { color: var(--blue); font-family: "Barlow Condensed", sans-serif; font-size: .9rem; letter-spacing: .17em; }
.section-title { --lh: .8; font-size: clamp(3.7rem, 8vw, 7rem); letter-spacing: -.035em; line-height: var(--lh); }
.section-subtitle, .library-heading p, .contact-desc { font-size: .95rem; }
.glass-panel { background: rgba(17,17,17,.92); border-color: var(--line); }
.bio-text { --lh: 1.45; font-size: clamp(1.18rem, 2.3vw, 1.55rem); line-height: var(--lh); }
.about-highlight { background: var(--surface); transition: transform .2s var(--ease), background .2s ease, border-color .2s ease, box-shadow .2s ease; }
.about-highlight:hover { z-index: 2; background: #151d26; border: 1px solid var(--blue); box-shadow: 0 6px 0 rgba(123,168,216,.22); --card-lift: var(--lift-card); }
.about-highlight h3, .skill-group-title, .project-title { font-family: "Barlow Condensed", sans-serif; font-weight: 700; text-transform: uppercase; letter-spacing: .01em; }
.skills-categories { border-color: var(--line); }
.skill-group { min-height: 15rem; background: rgba(17,17,17,.9); }
.skill-group { transition: transform .2s var(--ease), background .2s ease, border-color .2s ease, box-shadow .2s ease; }
.skill-group:hover { background: #151d26; border-color: var(--blue); box-shadow: 0 6px 0 rgba(123,168,216,.22); --card-lift: var(--lift-card); }
.tag { border-color: rgba(123,168,216,.5); color: #b9d6f5; }
.projects-grid { gap: var(--gutter); border: 0; }
.project-card { background: #101010; border: 1px solid var(--line); border-radius: 0; transition: transform .2s var(--ease), border-color .2s ease, background .2s ease; }
.project-card:hover { border-color: var(--blue); background: #151515; box-shadow: none; --card-lift: var(--lift-card); }
.project-image-wrapper { height: var(--project-image-height); background: #1f1f1f; }
.project-card:hover .project-image { transform: scale(1.035); }
.project-card:hover .project-image.is-motion-active { object-fit: contain; transform: none; background: #0c0c0c; }
.project-category-badge { top: auto; bottom: 0; left: 0; border: 0; background: var(--blue); color: #080808; font-family: "Barlow Condensed", sans-serif; font-size: .78rem; letter-spacing: .08em; }
.project-info { padding: var(--pad-card); }
.project-title { --lh: .94; font-size: clamp(1.85rem, 2.25vw, 2.15rem); line-height: var(--lh); }
.project-action-links { border-color: var(--line); }
.link-btn { border-radius: 0; background: transparent; }
.link-btn:hover { background: var(--blue); color: #080808; }
.project-library { border-top: 1px solid var(--blue); }
.collection-toggle { transition: color .2s ease; }
.collection-toggle > .collection-index,
.collection-toggle > .collection-copy { transition: transform .2s var(--ease); }
.collection-toggle:hover > .collection-index,
.collection-toggle:hover > .collection-copy { transform: translateX(var(--space-3)); }
.collection-index { color: var(--blue); }
.collection-copy strong { font-family: "Barlow Condensed", sans-serif; font-size: clamp(1.6rem, 3.2vw, 2.25rem); letter-spacing: .01em; text-transform: uppercase; }
.carousel-arrow { border-radius: 0; }
.modal-card { border: 1px solid rgba(123,168,216,.5); box-shadow: 10px 10px 0 rgba(123,168,216,.2); }
.modal-close { border-radius: 0; }
.modal-close:hover { background: var(--blue); color: #080808; }
.modal-header .tag, .modal-card .tag { color: #b9d6f5; border-color: rgba(123,168,216,.5); }
.modal-detail-section h4 { font-family: "Barlow Condensed", sans-serif; font-size: 1.45rem; letter-spacing: .01em; text-transform: uppercase; }
footer { background: transparent; }
#about, #contact { background: transparent; border: 0; }
#contact .contact-desc { margin-top: var(--rhythm-heading); }
#contact .contact-links { margin-top: var(--rhythm-block); }
#contact .resume-unavailable {
    background: transparent;
    border-color: var(--line);
    color: var(--ink);
    opacity: 1;
}
#contact .btn:hover { background: var(--blue); border-color: var(--blue); color: #080808; --control-lift: var(--lift-control); }
.about-highlight:focus-within, .skill-group:focus-within { border-color: var(--blue); }
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .background-grid { transform: none !important; }
    .galaxy-field { transform: none !important; }
    .galaxy-nebula { transform: none !important; opacity: .54; }
    .cursor-spark, .cursor-ripple { display: none; }
    *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media (max-width: 800px) {
    .nav-content, main { width: min(100% - 2rem, var(--site-max-width)); }
    .hero-section { padding: calc(4.875rem + var(--space-7)) var(--space-4) 0; }
    .hero-name {
        max-width: 100%;
        font-size: clamp(2.9rem, 15.5vw, 5rem);
        letter-spacing: -.06em;
        overflow-wrap: normal;
        word-break: normal;
    }
    .modal-card { box-shadow: 5px 5px 0 rgba(123,168,216,.2); }
    .about-highlight + .about-highlight { margin-left: 0; margin-top: -1px; }
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .section { opacity: 1; transform: none; transition: none; }
    .hero-section::before, .hero-section::after { transition: none; }
}

/* Both hero buttons are the same control. "Explore Projects" used to be blue
   from the start and "Get in Touch" only on hover, which read as one real
   action and one afterthought -- they are two ways into the same page. Blue is
   the answer to a cursor everywhere else on the site (the nav, the contact
   row, the carousel arrows, the social icons), so it is the answer here too. */
.hero-buttons > .hero-action {
    background: transparent;
    border-color: var(--line);
    color: var(--ink);
}
.hero-buttons > .hero-action:hover,
.hero-buttons > .hero-action:focus-visible,
.hero-buttons > .hero-action:active {
    background: var(--blue);
    border-color: var(--blue);
    color: #080808;
    --control-lift: var(--lift-control);
}

/* About and skills cards share the same grid so both sections line up. */
.about-highlights,
.skills-categories {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--gutter);
    border: 0;
    background: transparent;
}
.about-highlight,
.skill-group {
    position: relative;
    z-index: 0;
    width: 100%;
    height: auto;
    min-height: 15rem;
    padding: var(--pad-card);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: .875rem;
    transition:
        transform .2s var(--ease),
        background .2s ease,
        border-color .2s ease,
        box-shadow .2s ease;
}
.about-highlight + .about-highlight,
.skill-group + .skill-group {
    margin-left: 0;
}
.about-highlight:hover,
.skill-group:hover {
    z-index: 2;
    background: #151d26;
    border-color: var(--blue);
    box-shadow: 0 6px 0 rgba(123,168,216,.22);
    --card-lift: var(--lift-card);
}
@media (max-width: 800px) {
    .about-highlight,
    .skill-group {
        height: auto;
        min-height: 15rem;
    }
}

:root {
    --radius-card: .875rem;
    --radius-control: .4375rem;
}
.glass-panel,
.project-card,
.modal-card,
.modal-media-item,
.modal-detail-section,
.modal-feature-video-player,
.modal-overview-grid,
.media-placeholder {
    border-radius: var(--radius-card);
}
.btn,
.social-links a,
.resume-icon-unavailable,
.badge,
.tag,
.project-category-badge,
.link-btn,
.carousel-arrow,
.carousel-controls > div,
.collection-toggle,
.modal-close,
.modal-quick-link,
.modal-overview-item {
    border-radius: var(--radius-control);
}

.hero-section::before,
.hero-section::after {
    display: none;
}

:root {
    --content-section-gap: var(--rhythm-section);
    /* content-visibility: auto on a section brings paint containment with it,
       and that clips to the section's padding box. The first line of a header is
       leading-trimmed, so its line box reaches above its own box -- sitting
       flush against that edge, the capitals of Toolkit, Portfolio and Connect
       were sliced off at the top. This is a hair of padding to move the clip off
       the type, taken straight back out of the margin so nothing else moves.
       About never showed it because its own top padding already held it clear. */
    --section-paint-headroom: var(--space-3);
}
main > .section {
    margin: 0;
    padding-top: var(--section-paint-headroom);
    padding-bottom: 0;
}
main > #about { padding-top: var(--content-section-gap); }
/* The featured cards fill this section edge to edge, so the outer two lean
   straight into the paint clip and the corner nearest the cursor was sliced
   off. Widen the clip on both sides and take the same amount straight back as
   padding: the cards do not move, they simply have somewhere to rotate into.
   Only this section -- About and Contact paint a panel background, and
   widening their box would show. */
main > #projects {
    padding-inline: var(--section-paint-headroom);
    margin-inline: calc(-1 * var(--section-paint-headroom));
}
main > .section + .section {
    margin-top: calc(var(--content-section-gap) - var(--section-paint-headroom));
}
#projects .featured-grid { margin-bottom: 0; }
#projects .project-library {
    margin-top: 0;
    padding-top: var(--content-section-gap);
}
main > #contact { padding-bottom: var(--rhythm-section); }

.featured-carousel-controls {
    display: none;
    width: 100%;
    padding-bottom: var(--rhythm-heading);
}
.featured-carousel-controls.is-active { display: flex; }

/* Cards switch to this track when a collection is too wide for the grid. */
.project-carousel {
    display: flex;
    gap: var(--gutter);
    width: 100%;
    margin: 0;
    padding: var(--rhythm-block) 0 var(--rhythm-block);
    overflow-x: hidden;
    overflow-y: visible;
    perspective: 75rem;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    touch-action: pan-y;
}
.project-carousel::-webkit-scrollbar {
    display: none;
}
.project-carousel .project-card {
    flex: 0 0 calc((100% - var(--gutter) * 2) / 3);
    opacity: calc(1 - (var(--carousel-depth, 0) * .24));
    transform:
        perspective(68.75rem)
        translateY(calc(var(--carousel-depth, 0) * 1.125rem + var(--card-lift, 0px)))
        rotateY(calc(var(--carousel-position, 0) * 11deg))
        rotateX(var(--card-tilt-x, 0deg))
        rotateY(var(--card-tilt-y, 0deg))
        translateZ(calc(var(--carousel-depth, 0) * -5.5rem))
        scale(calc((1 - (var(--carousel-depth, 0) * .07)) * var(--card-press, 1)));
    transform-origin: center center;
    transform-style: preserve-3d;
    transition: border-color .3s ease, box-shadow .3s ease, opacity .15s linear, var(--card-answer-transition);
}
.project-carousel .project-card.is-carousel-active {
    border-color: rgba(123,168,216,.62);
    box-shadow: 0 22px 52px rgba(0,0,0,.34), 0 0 0 1px rgba(123,168,216,.12);
}
/* The hover lift is the only thing this used to cancel, and now the card
   answers with it, so the rule has nothing left to say. */

/* Ring size is set in JavaScript from the number of projects in the collection. */
.circular-project-carousel {
    width: 100%;
    margin-left: 0;
    transform: none;
}
.circular-project-carousel .project-card {
    border-radius: .875rem;
}
.featured-project-carousel .project-card {
    border-radius: .875rem;
}
html[data-effects="high"] .circular-project-carousel {
    min-height: clamp(var(--ring-height-min), 44vw, var(--ring-height-max));
    padding-top: var(--rhythm-block);
    padding-bottom: var(--rhythm-block);
    perspective: 87.5rem;
}
html[data-effects="high"] .circular-project-carousel .project-card,
html[data-effects="high"] .circular-project-carousel .project-card:hover {
    position: relative;
    backface-visibility: hidden;
    opacity: var(--carousel-ring-opacity, 0);
    /* The ring places the card; the last three functions are the card's own
       answer to the pointer, composed on top of that placement rather than
       replacing it. Order matters: the lift rides with the ring translation
       so it stays vertical on screen, and the tilt is applied after the ring's
       own rotation so the card leans toward the cursor from wherever it sits. */
    transform:
        perspective(81.25rem)
        translate3d(
            var(--carousel-ring-x, 0px),
            calc(var(--carousel-ring-y, 0px) + var(--card-lift, 0px)),
            var(--carousel-ring-z, 0px))
        rotateY(var(--carousel-ring-rotate, 0deg))
        rotateX(var(--card-tilt-x, 0deg))
        rotateY(var(--card-tilt-y, 0deg))
        scale(calc(var(--carousel-ring-scale, 1) * var(--card-press, 1)));
    transition: border-color .3s ease, box-shadow .3s ease, var(--card-answer-transition);
}
html[data-effects="high"] .circular-project-carousel .project-card.is-carousel-active {
    box-shadow: 0 26px 64px rgba(0,0,0,.42), 0 0 0 1px rgba(123,168,216,.15);
}
html[data-effects="high"] .circular-project-carousel .project-image-wrapper {
    height: var(--ring-image-height);
}
html[data-effects="high"] .circular-project-carousel .project-info {
    padding: var(--pad-card);
}
.project-carousel.is-dragging {
    cursor: grabbing;
    user-select: none;
}
.project-carousel:not(.is-dragging) {
    cursor: grab;
}
.carousel-controls .carousel-arrow {
    border-radius: 999px;
}
.carousel-controls .carousel-arrow:disabled {
    opacity: .34;
    color: var(--muted);
    border-color: var(--line);
    background: transparent;
    cursor: not-allowed;
    transform: none;
}
.carousel-controls > div {
    margin-left: auto;
    padding: var(--space-1);
    border: 1px solid var(--line);
    border-radius: 999px;
    background: rgba(18,19,23,.78);
}
.carousel-indicators {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--space-5);
    margin: 0 0 var(--rhythm-block);
}
.carousel-indicator {
    width: .625rem;
    height: .625rem;
    padding: 0;
    border: 1px solid var(--indicator-color, var(--blue));
    border-radius: 999px;
    background: transparent;
    cursor: pointer;
    transition: width .38s var(--ease), background .38s ease, box-shadow .38s ease, transform .2s ease;
}
.carousel-indicator:hover,
.carousel-indicator:focus-visible {
    transform: scale(1.15);
}
.carousel-indicator.is-active {
    width: 2.125rem;
    background: var(--indicator-color, var(--blue));
    box-shadow: 0 0 18px color-mix(in srgb, var(--indicator-color, var(--blue)) 45%, transparent);
}

/* Low FX drops the ring and the tilt but keeps the press: a click still has
   to say that it landed. */
html[data-effects="low"] .project-carousel .project-card,
html[data-effects="low"] .featured-project-carousel .project-card {
    opacity: 1;
    transform: translateY(var(--card-lift, 0px)) scale(var(--card-press, 1));
    will-change: auto;
}

/* Promote only while a ring is actively moving. Keeping every card on its
   own compositor layer makes large collections expensive on laptop GPUs. */
html[data-effects="high"] .project-carousel.is-dragging .project-card {
    will-change: transform, opacity;
}

@media (max-width: 800px) {
    .project-card { height: var(--project-card-height-mobile); }
    html[data-effects="high"] .circular-project-carousel {
        min-height: var(--ring-height-min);
        padding-top: var(--rhythm-block);
        padding-bottom: var(--rhythm-block);
    }
    html[data-effects="high"] .circular-project-carousel .project-image-wrapper { height: var(--ring-image-height-compact); }
    .featured-carousel-controls,
    .carousel-controls {
        justify-content: flex-end;
    }
    .featured-carousel-controls .carousel-arrow,
    .carousel-controls .carousel-arrow {
        width: 2.75rem;
        height: 2.75rem;
    }
    .collection-toggle:hover > .collection-index,
    .collection-toggle:hover > .collection-copy { transform: none; }
    .circular-project-carousel {
        width: 100%;
        margin-left: 0;
        transform: none;
    }
}

/* On narrow screens, featured projects collapse to one card at a time. */
.featured-project-carousel.is-compact-carousel {
    gap: var(--gutter);
    padding: var(--rhythm-block) 0;
}
.featured-project-carousel.is-compact-carousel .project-card {
    flex: 0 0 100%;
    opacity: 1;
    transform: translateY(var(--card-lift, 0px)) scale(var(--card-press, 1));
    will-change: auto;
}
.featured-project-carousel:not(.is-compact-carousel) + .carousel-indicators {
    display: none;
}

/* Low FX keeps the static grid and removes continuous animation. */
html[data-effects="low"] .background-grid {
    display: block;
    background-image:
        linear-gradient(rgba(145,200,255,.095) 1px, transparent 1px),
        linear-gradient(90deg, rgba(145,200,255,.095) 1px, transparent 1px),
        linear-gradient(rgba(255,255,255,.032) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.032) 1px, transparent 1px);
    background-size: 3rem 3rem, 3rem 3rem, .75rem .75rem, .75rem .75rem;
    opacity: .78;
}
html[data-effects="low"] .galaxy-field,
html[data-effects="low"] .galaxy-nebula,
html[data-effects="low"] .cursor-dot,
html[data-effects="low"] .cursor-spark,
html[data-effects="low"] .cursor-ripple {
    display: none !important;
}

/* High FX paints the grid on the galaxy canvas instead, so it bends with the
   rest of the sheet when a touch presses into it. A CSS background cannot be
   warped locally, and a dead-straight lattice over a sagging field was the one
   thing giving away that the backdrop was layers rather than a surface. */
html[data-effects="high"] .background-grid {
    display: none;
}
@media (max-width: 800px) {
    .nav-links { display: flex; }
    .nav-links > li:not(.performance-control) { display: none; }
    .performance-toggle { font-size: .72rem; }
}

/* Low FX draws the same lattice the canvas does in High FX -- the same two
   densities, the same blue major and white minor -- so switching modes changes
   how the backdrop moves, not what it is. This used to replace that lattice
   outright with three fields of scattered dots, which read as a starfield with
   no grid in it at all. The two soft clouds stay: they are the depth, and they
   sit behind the lines rather than instead of them. */
html[data-effects="low"] .background-grid {
    background-image:
        linear-gradient(rgba(145,200,255,.095) 1px, transparent 1px),
        linear-gradient(90deg, rgba(145,200,255,.095) 1px, transparent 1px),
        linear-gradient(rgba(255,255,255,.032) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.032) 1px, transparent 1px),
        radial-gradient(ellipse at 16% 24%, rgba(134,160,190,.055), transparent 35%),
        radial-gradient(ellipse at 85% 66%, rgba(175,151,125,.035), transparent 30%);
    background-size:
        3rem 3rem, 3rem 3rem, .75rem .75rem, .75rem .75rem,
        100% 100%, 100% 100%;
}

/* Left alone, the whole backdrop goes to black together. Every layer fades on
   exactly the same curve and the same clock as the grid -- one of them lagging
   behind is what makes the page look like a stack of effects rather than a
   single view that is settling. */
html.effects-background-idle .background-grid,
html.effects-background-idle .galaxy-field,
html.effects-background-idle .galaxy-nebula {
    opacity: 0;
    transition-duration: 1.4s;
    transition-timing-function: cubic-bezier(.16, .85, .3, 1);
}
html.effects-background-click-fading .background-grid,
html.effects-background-click-fading .galaxy-field,
html.effects-background-click-fading .galaxy-nebula {
    opacity: 0;
    transition-duration: 3.5s;
    transition-timing-function: cubic-bezier(.16, .85, .3, 1);
}

/* ---- The page grid --------------------------------------------------------
   Every three-up region on the page runs on one track definition and one
   gutter, so About, Skills, Projects and Contact resolve to identical column
   widths and their card edges line up in a single unbroken vertical rule from
   the top of the page to the bottom. Before this each region carried its own
   gutter -- 12px, 16px, 20px, 13.6px -- which put four different column widths
   on one page and left nothing aligned with anything below it.

   The breakpoints are shared for the same reason: all regions step from three
   to two to one at the same widths, instead of the page passing through a band
   where About is three-up and Projects is already one-up. */
.about-highlights,
.skills-categories,
.projects-grid,
.compact-project-grid,
.contact-links {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--gutter);
    align-items: stretch;
}
@media (max-width: 1080px) {
    .about-highlights,
    .skills-categories,
    .projects-grid,
    .compact-project-grid,
    .contact-links {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    /* Carousel cards are the same track, expressed as flex basis. */
    .project-carousel .project-card {
        flex: 0 0 calc((100% - var(--gutter)) / 2);
    }
}
@media (max-width: 800px) {
    .about-highlights,
    .skills-categories,
    .projects-grid,
    .compact-project-grid,
    .contact-links {
        grid-template-columns: minmax(0, 1fr);
    }
    .project-carousel .project-card {
        flex: 0 0 100%;
    }
}

/* The featured container carries .projects-grid at every width and picks up
   .project-carousel only when it swaps into the one-up swipe track. Carousel
   mode is a flex row, so it has to win over the page-grid rule above. */
.projects-grid.project-carousel {
    display: flex;
}

/* ====================================================================
   Skip link, focus rings, consent banner, video placeholders, mobile nav.
   Appended as one block so the additions stay easy to find and easy to lift
   back out. Everything below reuses the tokens defined at the top of the file.
   ==================================================================== */

/* Keyboard users should be able to jump the nav rather than tab through it. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 1200;
    padding: var(--space-3) var(--space-4);
    background: var(--blue);
    color: #080808;
    font-size: .8rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.skip-link:focus {
    left: var(--space-4);
    top: var(--space-4);
}

/* A visible focus ring on everything focusable, not just the few controls that
   already had one. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--blue);
    outline-offset: 3px;
}

/* ----------------------------------------------------------- footer links */
.footer-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
}
.footer-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-5);
}
.footer-links a {
    color: var(--muted);
    font-size: .76rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    transition: color .2s var(--ease);
}
.footer-links a:hover,
.footer-links a:focus-visible { color: var(--blue); }

/* -------------------------------------------------------------- mobile nav */
/* The links used to be hidden outright below 800px, which left phones with no
   navigation at all. They become a disclosure panel instead. */
.nav-toggle {
    display: none;
    position: relative;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--ink);
    cursor: pointer;
}
.nav-toggle > span,
.nav-toggle > span::before,
.nav-toggle > span::after {
    position: absolute;
    width: 1.05rem;
    height: 2px;
    background: currentColor;
    transition: transform .28s var(--ease), background .18s linear;
}
.nav-toggle > span {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
.nav-toggle > span::before,
.nav-toggle > span::after {
    content: "";
    left: 0;
    transform-origin: center;
}
.nav-toggle > span::before { transform: translateY(-6px); }
.nav-toggle > span::after { transform: translateY(6px); }
.nav-toggle[aria-expanded="true"] > span { background: transparent; }
.nav-toggle[aria-expanded="true"] > span::before { transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] > span::after { transform: rotate(-45deg); }

@media (max-width: 800px) {
    .nav-toggle { display: block; }
    .nav-content {
        flex-wrap: wrap;
        /* The collapsed menu is still a flex item, so it forms a second line of
           zero height. Without this the bar's 78px min-height is split between
           the two lines and the logo and toggle ride ~10px high, leaving dead
           space under them. Centring the lines as a group puts the row back on
           the bar's axis, and does nothing once the menu opens and the content
           is taller than the minimum. */
        align-content: center;
    }

    /* Supersedes the earlier rules that hid every link on small screens.

       Collapsed with max-height rather than `grid-template-rows: 0fr`, which
       only sizes the FIRST row -- the remaining items land in implicit rows and
       keep their height, leaving a closed navbar 212px tall with the hero
       showing through it. The ceiling only has to clear five items. */
    .nav-links {
        display: block;
        flex-basis: 100%;
        gap: 0;
        max-height: 0;
        overflow: hidden;
        transition: max-height .3s var(--ease);
    }
    .nav-links > li:not(.performance-control) { display: block; }
    .navbar:not([data-nav-open]) .nav-links > li { visibility: hidden; }
    .navbar[data-nav-open] .nav-links { max-height: 20rem; }
    .navbar[data-nav-open] .nav-links > li { visibility: visible; }
    .nav-links a,
    .nav-links .performance-toggle {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        width: 100%;
        padding: var(--space-3) 0;
        border-bottom: 1px solid var(--line);
    }
    /* Fully opaque, not .98: the open panel sits directly over the hero type,
       and even a two-percent bleed shows the headline through the menu. */
    .navbar[data-nav-open] {
        background: #080808;
        border-bottom-color: var(--line);
    }

}

@media (prefers-reduced-motion: reduce) {
    .nav-links { transition: none; }
    .nav-toggle > span,
    .nav-toggle > span::before,
    .nav-toggle > span::after { transition: none; }
}

/* Shown in place of the project grid when portfolio-data.json cannot be read. */
.data-error { grid-column: 1 / -1; padding: var(--space-7) var(--space-5); }
.data-error a { color: var(--blue); text-decoration: underline; text-underline-offset: 3px; }
.data-error a:hover { color: var(--ink); }

/* ==========================================================================
   Pointer-reactive surfaces

   Three ideas taken from motion-primitives and rebuilt in plain CSS driven by
   one pointer listener, because this site has no React to hang their
   components off:

   - Spotlight -- a soft light that follows the cursor across a card.
   - Tilt -- the card leans toward the cursor. Their default rotation is 15deg,
     which is a party trick on a 680px-tall project card; 4deg reads as weight
     instead of as a toy.
   - Press -- contact scales the surface down and strikes a ripple from the
     exact point that was touched, so a click has a visible cause. Nothing on
     the page acknowledged a press before this.

   Every one of them is a custom property that the existing transforms compose
   with, never a `transform` of its own. A card in a carousel is placed by that
   carousel -- the High FX ring hands each one its own 3D position -- so the
   carousel rules fold these same properties into the placement they already
   own, and every project card on the page answers the pointer the same way:
   the three featured ones, and all sixty in the archive.

   The four properties are registered below so they interpolate. That is what
   lets a carousel card ease its lean back to flat without putting `transform`
   itself in a transition, which would smear the ring's own motion behind the
   drag. The registrations are the reason every use of them still carries a
   var() fallback: an engine without @property has to resolve the transform.
   ========================================================================== */

@property --card-tilt-x { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@property --card-tilt-y { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@property --card-lift { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --card-press { syntax: "<number>"; inherits: false; initial-value: 1; }

/* Carousel cards splice this into their own transition list. The lean follows
   the cursor with the same weight the grid cards get from their .2s transform
   transition, and the ring's placement stays instant because it is not in the
   list.

   It is declared on the card rather than on :root so --card-answer-duration
   resolves against the card: a nested var() inside a custom property is
   substituted where the property is declared, so on :root every card would be
   stuck with :root's duration and the shorter press timing below could never
   reach it. A `transition-duration` override cannot reach it either -- the
   ring's own `transition` shorthand outranks one. */
.project-card {
    --card-answer-duration: .2s;
    --card-answer-transition:
        --card-tilt-x var(--card-answer-duration) var(--ease),
        --card-tilt-y var(--card-answer-duration) var(--ease),
        --card-lift var(--card-answer-duration) var(--ease),
        --card-press var(--card-answer-duration) var(--ease);
}

/* The hover lifts above set --card-lift / --control-lift. These are the only
   rules that turn them into an actual transform, and they sit at the end of
   the file so nothing re-declares transform behind their back. */
.project-card,
.about-highlight,
.skill-group {
    transform: translateY(var(--card-lift, 0px)) scale(var(--card-press, 1));
}
.btn,
.link-btn,
.carousel-arrow,
.social-links a,
.resume-icon-unavailable {
    transform: translateY(var(--control-lift, 0px)) scale(var(--control-press, 1));
}

/* Scoped to a card standing in a plain grid. A carousel folds the same tilt
   into the placement it already owns, so this rule has to stay out of its way
   -- and `.project-carousel .project-card` alone would not, being the same
   specificity and further up the file. */
:not(.project-carousel) > .project-card.is-tilting {
    transform:
        perspective(62.5rem)
        rotateX(var(--card-tilt-x, 0deg))
        rotateY(var(--card-tilt-y, 0deg))
        translateY(var(--card-lift, 0px))
        scale(var(--card-press, 1));
}

/* Contact: down and slightly smaller, and the hover lift gives way to it, so a
   pressed control reads as being pushed into the page rather than hovering
   over it. */
.project-card.is-pressed,
.about-highlight.is-pressed,
.skill-group.is-pressed {
    --card-press: .985;
    --card-lift: -2px;
}
.btn.is-pressed,
.link-btn.is-pressed,
.carousel-arrow.is-pressed,
.social-links a.is-pressed {
    --control-press: .965;
    --control-lift: -1px;
}
/* Faster in than out: the press should feel instant and the release should
   settle. Named surface by surface rather than as a bare `.is-pressed`, which
   would also retime the cursor dot -- it uses the same class name. */
.project-card.is-pressed { --card-answer-duration: .09s; }
.project-card.is-pressed,
.about-highlight.is-pressed,
.skill-group.is-pressed,
.btn.is-pressed,
.link-btn.is-pressed,
.carousel-arrow.is-pressed,
.social-links a.is-pressed { transition-duration: .09s; }

/* The ripple and the spotlight both need a clip and a containing block. */
.project-card,
.about-highlight,
.skill-group,
.btn,
.link-btn,
.carousel-arrow,
.social-links a,
.resume-icon-unavailable {
    position: relative;
    overflow: hidden;
}

.press-ripple {
    position: absolute;
    z-index: 4;
    width: var(--ripple-size, 8rem);
    aspect-ratio: 1;
    pointer-events: none;
    border-radius: 50%;
    /* currentColor, so one rule covers both directions: a cream bloom on a dark
       card, and a dark one on a control that has turned blue under the cursor. */
    background: radial-gradient(circle, currentColor 0%, transparent 68%);
    transform: translate(-50%, -50%) scale(.2);
    animation: press-ripple-out .52s var(--ease) forwards;
}
@keyframes press-ripple-out {
    from { opacity: .34; transform: translate(-50%, -50%) scale(.2); }
    to { opacity: 0; transform: translate(-50%, -50%) scale(1); }
}

/* Spotlight. High FX only: mix-blend-mode puts every card that carries this
   layer on its own compositing layer, and the archive can hold a lot of cards. */
html[data-effects="high"] .project-card::after,
html[data-effects="high"] .about-highlight::after,
html[data-effects="high"] .skill-group::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    opacity: var(--card-glow, 0);
    background: radial-gradient(
        var(--card-glow-size, 15rem) var(--card-glow-size, 15rem)
        at var(--card-glow-x, 50%) var(--card-glow-y, 50%),
        rgba(123, 168, 216, .20),
        rgba(123, 168, 216, .06) 45%,
        transparent 72%);
    mix-blend-mode: screen;
    transition: opacity .2s ease;
}
/* A card is only as wide as its column, so the light is sized to the smaller
   surfaces rather than washing the whole card at once. */
.about-highlight,
.skill-group {
    --card-glow-size: 12rem;
}

@media (prefers-reduced-motion: reduce) {
    .press-ripple { display: none; }
    :not(.project-carousel) > .project-card.is-tilting {
        transform: translateY(var(--card-lift, 0px)) scale(var(--card-press, 1));
    }
}


/* ==========================================================================
   Leading trim

   A line box is taller than the letters inside it. Half the leading sits
   above the ascender and half below the descender, and the ascender itself
   reaches higher than a capital -- so a heading set at line-height .74 has
   dead space above its capitals and its tails hang out below the box. That
   is why the same 24px margin used to read as about 8px under the hero name
   and as a comfortable 20px under a paragraph, and it is what the eye was
   measuring when the gaps looked unequal.

   These rules pull each block's box in to its own letters: cap height at the
   top, alphabetic baseline at the bottom, which is what `text-box-trim` will
   do natively once every engine ships it. Done by hand it works everywhere
   and needs nothing but the font's metrics.

   To trim a block: set --lh to its line-height as a plain number, use
   `line-height: var(--lh)`, and list it under the face it is set in. The
   arithmetic is the same either way --

       trim above = half the leading + (ascender - cap height)
       trim below = half the leading + descender

   -- and both come out in em, so they follow every clamp() on the page
   without a second breakpoint.
   ========================================================================== */

.hero-name,
.section-title,
.library-heading h3,
.modal-title,
.section-label,
.project-title,
.about-highlight h3,
.skill-group-title,
.collection-copy strong,
.modal-overview-item > span,
.modal-overview-item h4,
.about-highlight-role h4,
.hero-badge {
    --trim-top: calc((
        (var(--lh) - var(--face-display-ascent) - var(--face-display-descent)) / 2
        + var(--face-display-ascent) - var(--face-display-cap)) * 1em);
    --trim-bottom: calc((
        (var(--lh) - var(--face-display-ascent) - var(--face-display-descent)) / 2
        + var(--face-display-descent)) * 1em);
}
.about-highlight > .about-highlight-label,
.about-highlight p,
.project-summary,
.modal-summary,
.modal-program-summary,
.hero-tagline,
.section-subtitle,
.library-heading p,
.contact-desc,
.bio-text,
.collection-copy small {
    --trim-top: calc((
        (var(--lh) - var(--face-text-ascent) - var(--face-text-descent)) / 2
        + var(--face-text-ascent) - var(--face-text-cap)) * 1em);
    --trim-bottom: calc((
        (var(--lh) - var(--face-text-ascent) - var(--face-text-descent)) / 2
        + var(--face-text-descent)) * 1em);
}

/* `display: table` keeps these from collapsing into the first and last line
   boxes, which is the whole trick: a zero-height block whose negative margin
   eats the dead space instead of the text. */
.hero-name::before,
.section-title::before,
.library-heading h3::before,
.modal-title::before,
.section-label::before,
.project-title::before,
.about-highlight h3::before,
.skill-group-title::before,
.collection-copy strong::before,
.collection-copy small::before,
.modal-overview-item > span::before,
.modal-overview-item h4::before,
.about-highlight-role h4::before,
.about-highlight > .about-highlight-label::before,
.about-highlight p::before,
.project-summary::before,
.modal-summary::before,
.modal-program-summary::before,
.hero-tagline::before,
.section-subtitle::before,
.library-heading p::before,
.contact-desc::before,
.bio-text::before {
    content: "";
    display: table;
    margin-bottom: calc(-1 * var(--trim-top));
}
.hero-name::after,
.section-title::after,
.library-heading h3::after,
.modal-title::after,
.section-label::after,
.project-title::after,
.about-highlight h3::after,
.skill-group-title::after,
.collection-copy strong::after,
.collection-copy small::after,
.modal-overview-item > span::after,
.modal-overview-item h4::after,
.about-highlight-role h4::after,
.about-highlight > .about-highlight-label::after,
.about-highlight p::after,
.project-summary::after,
.modal-summary::after,
.modal-program-summary::after,
.hero-tagline::after,
.section-subtitle::after,
.library-heading p::after,
.contact-desc::after,
.bio-text::after {
    content: "";
    display: table;
    margin-top: calc(-1 * var(--trim-bottom));
}

/* The badge is an inline-flex row -- a pseudo-element inside it would become
   a flex item and open a gap of its own -- so its trim is folded into the
   margin instead. Nothing stacks above it, so only the bottom is in play. */
.hero-badge {
    --lh: 1;
    line-height: var(--lh);
    margin-bottom: calc(var(--rhythm-label) - var(--trim-bottom));
}
