/* Blue-and-white porcelain (青花瓷) theme
   Base: Pantone 11-4201 Cloud Dancer (2026 Color of the Year) — white ground
   Pattern/text: cobalt blue linework, echoing hand-painted porcelain borders */

@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=Crimson+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap');

:root {
    --white: #ffffff;
    --porcelain-white: #ffffff;
    --porcelain-pale: #dbe9f2;
    --cobalt: #1d4f7f;
    --cobalt-mid: #4a7fab;
    --cobalt-mid-hover: #35618a;
    --border-light: #d7e4ec;
    --ink: #1c2b38;
    --ink-muted: #4a5d6b;
    --ink-faint: #6b7d89;
    --seal-red: #b5482f;

    /* Type: "Academic/Research" pairing — Crimson Pro for scholarly headings,
       Atkinson Hyperlegible (Braille Institute, drawn for legibility) for body. */
    --font-display: 'Crimson Pro', Georgia, 'Times New Roman', serif;
    --font: 'Atkinson Hyperlegible', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

    /* Reading measure — body copy caps at ~68ch regardless of container width */
    --measure: 68ch;

    /* 4px spacing grid */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-12: 3rem;
    --space-16: 4rem;
}

/* Browser surfaces themed from the palette rather than left at UA defaults */
::selection {
    background-color: var(--porcelain-pale);
    color: var(--cobalt);
}

:focus-visible {
    outline: 2px solid var(--cobalt);
    outline-offset: 3px;
    border-radius: 2px;
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        /* Also zero the delay: a staggered entrance uses fill-mode "both", so
           a card left waiting on its delay stays at opacity 0 that whole
           time -- invisible rather than merely unanimated. */
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

*, *::before, *::after {
    box-sizing: border-box;
}

/* Base Styles */
html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    font-family: var(--font);
    margin: 0;
    padding: 0;
    background-color: var(--white);
    color: var(--ink);
    line-height: 1.65;
    scroll-behavior: smooth;
    overflow-wrap: break-word;
}

img, svg {
    max-width: 100%;
    height: auto;
}

h1, h2, h3 {
    font-family: var(--font-display);
    font-weight: 600;
    line-height: 1.2;
    text-wrap: balance;
    letter-spacing: -0.01em;
    color: var(--cobalt);
}

/* Navigation Bar */
.navbar {
    background-color: var(--porcelain-white);
    color: var(--cobalt);
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    padding: 0 clamp(1rem, 4vw, 2rem);
    position: sticky;
    top: 0;
    z-index: 1000;
    border-bottom: 1px solid var(--border-light);
}

.lab-logo {
    font-family: var(--font-display);
    font-size: clamp(1.2rem, 3.4vw, 1.55rem);
    font-weight: 600;
    letter-spacing: 1px;
    color: var(--cobalt);
    padding: 0.9rem 0;
}

.lab-logo a {
    color: inherit;
    text-decoration: none;
}

/* Hamburger Toggle (mobile only, see media query below) */
.nav-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
}

.nav-toggle-bar {
    width: 22px;
    height: 2px;
    background-color: var(--cobalt);
    border-radius: 2px;
    transition: transform 0.25s ease, opacity 0.25s ease;
}

.nav-toggle.open .nav-toggle-bar:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.nav-toggle.open .nav-toggle-bar:nth-child(2) {
    opacity: 0;
}

.nav-toggle.open .nav-toggle-bar:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

/* Top-Level Menu */
.nav-links {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
}

.nav-item {
    position: relative;
}

.nav-item a {
    display: block;
    font-family: var(--font);
    font-weight: 300;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--cobalt);
    text-decoration: none;
    padding: 1.2rem 1.5rem;
    transition: background 0.3s ease, color 0.3s ease;
}

.nav-item a:hover {
    background-color: var(--porcelain-pale);
    color: var(--cobalt-mid-hover);
}

/* Dropdown Submenu */
.dropdown {
    display: none;
    position: absolute;
    background-color: var(--porcelain-white);
    min-width: 220px;
    box-shadow: 0px 8px 16px 0px rgba(22,50,79,0.15);
    z-index: 1;
    list-style: none;
    padding: 0;
    margin: 0;
    top: 100%;
    left: 0;
    border-radius: 0 0 4px 4px;
    overflow: hidden;
}

.dropdown li a {
    font-family: var(--font);
    font-weight: 300;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--ink);
    padding: 12px 16px;
    border-bottom: 1px solid var(--border-light);
}

.dropdown li a:hover {
    background-color: var(--porcelain-pale);
    color: var(--cobalt);
}

.dropdown li:last-child a {
    border-bottom: none;
}

/* Show dropdown on hover */
.nav-item:hover .dropdown {
    display: block;
}

/* Collapsed / touch navigation: below the width where the full menu
   no longer fits on one line, swap to a hamburger-driven stacked menu.
   Layout math (not a fixed device target) — see .nav-toggle display. */
@media (max-width: 768px) {
    .nav-toggle {
        display: flex;
    }

    .nav-links {
        display: none;
        flex-direction: column;
        width: 100%;
        flex-basis: 100%;
        padding: 0.5rem 0 1rem;
    }

    .nav-links.open {
        display: flex;
    }

    .nav-item {
        width: 100%;
    }

    .nav-item a {
        padding: 0.9rem 0.5rem;
    }

    .dropdown {
        display: none;
        position: static;
        min-width: 0;
        width: 100%;
        background-color: var(--porcelain-pale);
        box-shadow: none;
        border-radius: 4px;
        margin: 0.25rem 0 0.5rem;
    }

    .nav-item:hover .dropdown {
        display: none;
    }

    .nav-item.open .dropdown {
        display: block;
    }

    .dropdown li a {
        padding: 0.75rem 1rem;
    }
}

/* Main Content */
.hero {
    background-color: var(--porcelain-white);
    padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 2rem);
    text-align: center;
}

.hero h1 {
    font-size: clamp(2rem, 1.2rem + 3.4vw, 3.2rem);
    margin-bottom: var(--space-4);
    color: var(--cobalt);
    letter-spacing: -0.015em;
}

.hero p {
    font-style: italic;
    color: var(--ink-muted);
    font-size: clamp(1rem, 0.9rem + 0.6vw, 1.2rem);
    /* The homepage tagline needs ~744px to sit on one line and was being cut
       off by a 58ch (728px) cap. 64ch clears it with room to spare; narrower
       viewports still wrap, and balance evens the lines when they do. */
    max-width: 64ch;
    margin-inline: auto;
    text-wrap: balance;
}

.container {
    max-width: 1000px;
    margin: 0 auto 3rem;
    padding: 0 clamp(1rem, 4vw, 2rem);
}

section {
    margin-bottom: 3rem;
    padding-top: 2rem;
}

/* The hero already supplies the space above the first section — don't stack a
   second gap on top of it. */
section:first-of-type {
    padding-top: 0;
}

/* Team Roster Styles */
.team-category {
    font-family: var(--font);
    margin-top: 2rem;
    border-bottom: 2px solid var(--border-light);
    padding-bottom: 0.5rem;
    color: var(--cobalt);
}

.team-member {
    background: var(--porcelain-white);
    padding: clamp(1rem, 3vw, 1.5rem);
    margin-top: 1rem;
    border-left: 3px solid var(--cobalt-mid);
    /* Flat at rest — the inset hairline is the card edge, not a shadow. */
    box-shadow: inset 0 0 0 1px var(--border-light);
    border-radius: 0 4px 4px 0;
}

.team-member strong {
    font-family: var(--font);
    font-size: 1.2rem;
    color: var(--cobalt);
}

.team-member p {
    margin: 0.5rem 0 0 0;
    color: var(--ink-muted);
}

/* Someone who has accepted but has not started. Marked in the same red the
   publication list uses for "under review", because it says the same kind of
   thing: this is true, but not yet settled. */
.team-status {
    color: var(--seal-red);
    font-weight: 700;
}

/* Publications List Styles (populated by scripts/generate_publications.py) */
.pub-list {
    list-style-type: none;
    padding: 0;
    margin-top: 1.5rem;
}

.pub-list li {
    background: var(--porcelain-white);
    padding: clamp(1rem, 3vw, 1.5rem);
    margin-bottom: 1rem;
    border-left: 3px solid var(--cobalt);
    /* Flat at rest — the inset hairline is the card edge, not a shadow. */
    box-shadow: inset 0 0 0 1px var(--border-light);
    border-radius: 0 4px 4px 0;
}

.pub-list > li > strong {
    font-family: var(--font);
    color: var(--cobalt);
    font-size: 1.15rem;
    display: block;
    margin-bottom: 0.25rem;
}

.pub-list a {
    color: var(--cobalt-mid);
    text-decoration: none;
    font-weight: bold;
}

.pub-list a:hover {
    color: var(--cobalt-mid-hover);
    text-decoration: underline;
}

.pub-authors {
    color: var(--ink-muted);
    margin-bottom: 0.25rem;
}

.pub-venue {
    font-family: var(--font);
    font-style: italic;
    color: var(--cobalt);
}

.pub-status {
    color: var(--seal-red);
    font-weight: bold;
}

.pub-details {
    color: var(--ink-faint);
    font-size: 0.9rem;
    margin-top: 0.25rem;
    overflow-wrap: break-word;
}

/* Research Topics Styles */
.research-topic {
    background: var(--porcelain-white);
    padding: clamp(1rem, 3vw, 1.5rem);
    margin-top: 1rem;
    border-left: 3px solid var(--cobalt-mid);
    /* Flat at rest — the inset hairline is the card edge, not a shadow. */
    box-shadow: inset 0 0 0 1px var(--border-light);
    border-radius: 0 4px 4px 0;
}

.research-topic h3 {
    margin-top: 0;
    color: var(--cobalt);
}

/* Filter tabs (see js/filter-tabs.js) — event type on events.html,
   publication year on publications.html. */
.event-filter {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 1.5rem;
}

.event-filter-btn {
    font-family: var(--font);
    font-weight: 500;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--cobalt);
    background: var(--porcelain-white);
    border: 1px solid var(--border-light);
    border-radius: 999px;
    /* 44px floor so the pill is a real touch target, not just a label. */
    min-height: 44px;
    padding: 0.5rem 1.1rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    /* Sits above the sliding indicator. */
    position: relative;
    z-index: 1;
    transition: background 200ms cubic-bezier(0.16, 1, 0.3, 1),
                color 260ms cubic-bezier(0.16, 1, 0.3, 1),
                border-color 200ms ease-out,
                transform 120ms ease-out;
}

.event-filter-btn:hover:not(.active) {
    background: var(--porcelain-pale);
    border-color: var(--cobalt-mid);
}

/* Feedback lands on the press, not on release. */
.event-filter-btn:active {
    transform: scale(0.97);
}

/* The fill is the sliding indicator behind the pill, not the button itself,
   so selection travels between tabs instead of blinking off one and on
   another. The button only swaps its text colour. */
.event-filter-btn.active {
    background: transparent;
    border-color: transparent;
    color: var(--white);
}

/* One pill that moves to whichever filter is active. Positioned and sized
   from the active button by js/filter-tabs.js, so it follows the buttons
   through wrapping and resizes without hardcoded geometry. */
.filter-indicator {
    position: absolute;
    top: 0;
    left: 0;
    border-radius: 999px;
    background: var(--cobalt);
    box-shadow: 0 2px 10px rgba(29, 79, 127, 0.28);
    pointer-events: none;
    z-index: 0;
    transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1),
                width 420ms cubic-bezier(0.16, 1, 0.3, 1),
                height 420ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Placed before first paint and on resize — it should be where it belongs,
   not slide there from the corner. */
.filter-indicator.is-placing {
    transition: none;
}

/* A filter with nothing behind it stays visible but reads as unavailable. */
.event-filter-btn[disabled] {
    opacity: 0.45;
    cursor: default;
}

.event-filter-btn[disabled]:hover {
    background: var(--porcelain-white);
    border-color: var(--border-light);
}

/* Cards settle in when the filter changes, rather than snapping. Each one
   starts a little after the one above it, so the list resolves top-down
   instead of the whole block moving at once. js/filter-tabs.js sets
   --enter-index, capped so a long list still finishes promptly. */
@keyframes filter-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

.event-list > li.is-entering,
.pub-list > li.is-entering {
    animation: filter-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(var(--enter-index, 0) * 75ms);
}

/* Events List Styles (populated by scripts/generate_events.py) */
.event-list {
    list-style-type: none;
    padding: 0;
    margin-top: 1.5rem;
}

.event-list li {
    background: var(--porcelain-white);
    padding: clamp(1rem, 3vw, 1.5rem);
    margin-bottom: 1rem;
    border-left: 3px solid var(--cobalt-mid);
    /* Flat at rest — the inset hairline is the card edge, not a shadow. */
    box-shadow: inset 0 0 0 1px var(--border-light);
    border-radius: 0 4px 4px 0;
}

.event-list > li > strong {
    font-family: var(--font);
    color: var(--cobalt);
    font-size: 1.15rem;
    display: block;
    margin-bottom: 0.25rem;
}

.event-details {
    color: var(--ink-muted);
    overflow-wrap: break-word;
}

.event-list a {
    color: var(--cobalt-mid);
    text-decoration: none;
    font-weight: bold;
}

/* A title keeps the heading colour whether or not it links somewhere — only
   inline links inside the details line take the lighter link blue. */
.event-list > li > strong > a,
.pub-list > li > strong > a {
    color: var(--cobalt);
}

.event-list > li > strong > a:hover,
.pub-list > li > strong > a:hover {
    color: var(--cobalt-mid-hover);
    text-decoration: underline;
}

.event-list a:hover {
    color: var(--cobalt-mid-hover);
    text-decoration: underline;
}

/* Placeholder Styles (Open-Source Software) */
.effort-placeholder {
    background: var(--porcelain-white);
    padding: clamp(1rem, 3vw, 1.5rem);
    border: 1px dashed var(--porcelain-pale);
    border-radius: 4px;
    color: var(--ink-faint);
}

.effort-placeholder ul {
    margin: 0.75rem 0 0 1.25rem;
    padding: 0;
}

/* Join Us Block Styles */
/* ── Navigation ───────────────────────────────────────────────────────── */
.navbar {
    transition: box-shadow 200ms ease-out;
}

/* Lifts off the page once content scrolls beneath it (set by js/nav.js). */
.navbar.is-scrolled {
    box-shadow: 0 1px 3px rgba(22, 50, 79, 0.07), 0 6px 20px rgba(22, 50, 79, 0.06);
}

/* Underline grows from the centre on hover; the current page keeps it. */
.nav-links > .nav-item > a {
    position: relative;
}

.nav-links > .nav-item > a::after {
    content: "";
    position: absolute;
    left: 50%;
    right: 50%;
    bottom: 0.55rem;
    height: 2px;
    background-color: var(--cobalt);
    transition: left 220ms cubic-bezier(0.22, 1, 0.36, 1),
                right 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-links > .nav-item > a:hover::after,
.nav-links > .nav-item > a[aria-current="page"]::after {
    left: 0.6rem;
    right: 0.6rem;
}

.nav-links > .nav-item > a[aria-current="page"] {
    font-weight: 700;
}

@media (max-width: 768px) {
    /* Stacked nav: a left rule reads better than an underline. */
    .nav-links > .nav-item > a::after {
        left: 0;
        right: auto;
        top: 0.35rem;
        bottom: 0.35rem;
        width: 2px;
        height: auto;
        transform: scaleY(0);
        transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
    }

    .nav-links > .nav-item > a:hover::after,
    .nav-links > .nav-item > a[aria-current="page"]::after {
        left: 0;
        right: auto;
        transform: scaleY(1);
    }
}

/* ── Footer ───────────────────────────────────────────────────────────── */
/* No spiral here: at any size that fits a compact footer the mark reads as
   a plain circle rather than the spiral, so it stays in the hero only. */
footer {
    padding-block: 2rem;
}

/* ── Section heads ────────────────────────────────────────────────────── */
/* A short cobalt lead-in over the hairline, so the rule reads as authored. */
.section-head {
    position: relative;
}

.section-head::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 2.5rem;
    height: 2px;
    background-color: var(--cobalt);
}

/* ── Motion ───────────────────────────────────────────────────────────── */
/* One authored moment: the hero settles on load. Everything else is
   hover/focus feedback. Exponential ease-out, from an already-visible
   default so nothing is hidden if the animation never runs. */
@keyframes hero-settle {
    from { opacity: 0; transform: translateY(0.75rem); }
    to   { opacity: 1; transform: none; }
}

.hero h1,
.hero p,
.hero-content > * {
    animation: hero-settle 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.hero p,
.hero-content > * + * {
    animation-delay: 90ms;
}

.event-list li,
.pub-list li {
    transition: box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1),
                border-color 200ms ease-out,
                transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.event-list li:hover,
.pub-list li:hover {
    transform: translateY(-2px);
}

.button-primary:active {
    transform: translateY(1px);
}

/* Photographic hero — duotone treatment so a photo reads as part of the
   porcelain palette rather than fighting it. Remove this block along with
   the .hero-media div to go back to the plain type-only hero. */
.hero--image {
    position: relative;
    isolation: isolate;
    padding-block: clamp(4rem, 12vw, 8rem);
    background-color: var(--cobalt);
    overflow: hidden;
}

.hero--image .hero-media {
    position: absolute;
    inset: 0;
    z-index: -1;
}

.hero--image .hero-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Duotone: desaturate, then let the cobalt ground show through. */
    filter: grayscale(1) contrast(1.05);
    mix-blend-mode: multiply;
}

/* Cobalt wash + a darker foot so the type keeps its contrast over any crop. */
.hero--image::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
        linear-gradient(to bottom, rgba(29, 79, 127, 0.25), rgba(18, 45, 71, 0.85)),
        linear-gradient(to right, rgba(29, 79, 127, 0.55), rgba(29, 79, 127, 0.15));
}

.hero--image .hero-content {
    position: relative;
    /* Wide enough for the tagline to hold one line; the heading is balanced
       independently, so this does not stretch it. */
    max-width: 54rem;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2rem);
}

.hero--image h1,
.hero--image p {
    color: var(--white);
}

.hero--image h1 {
    text-shadow: 0 1px 24px rgba(12, 30, 48, 0.45);
}

.hero--image p {
    color: rgba(255, 255, 255, 0.92);
}

/* Clickable cards — the title anchor stretches over its whole card, so the
   card is one big target. Other links inside (doi, pdf, webpage) sit above
   that overlay and keep working, and the anchor keeps the card keyboard
   reachable in reading order. */
.event-list li,
.pub-list li {
    position: relative;
}

.card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

.event-list li a:not(.card-link),
.pub-list li a:not(.card-link) {
    position: relative;
    z-index: 1;
}

.event-list li:hover,
.pub-list li:hover {
    border-color: var(--cobalt-mid);
    box-shadow: inset 0 0 0 1px var(--border-light), 0 3px 10px rgba(22, 50, 79, 0.14);
}

.event-list li:has(.card-link:focus-visible),
.pub-list li:has(.card-link:focus-visible) {
    outline: 2px solid var(--cobalt);
    outline-offset: 3px;
}

/* The stretched anchor already carries the focus ring on its card. */
.card-link:focus-visible {
    outline: none;
}

/* Home page — section heads pair a title with a link through to the full page,
   so the front door shows evidence instead of summarising itself. */

/* Spans the full container width so its edges line up with the section heads
   and cards below; a 68ch measure left it visibly inset against them. */
.home-lede {
    font-size: clamp(1.05rem, 0.98rem + 0.35vw, 1.2rem);
    color: var(--ink);
    text-wrap: pretty;
}

/* There is no global <a> rule, so without this the PI's name would fall back
   to the browser's default blue and underline. */
.home-lede a {
    color: var(--cobalt-mid);
    text-decoration: underline;
    text-underline-offset: 0.15em;
    text-decoration-thickness: 1px;
}

.home-lede a:hover {
    color: var(--cobalt-mid-hover);
    text-decoration-thickness: 2px;
}

.section-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-light);
}

.section-head h2 {
    margin: 0;
}

.section-more {
    font-size: 0.92rem;
    font-weight: 700;
    color: var(--cobalt-mid);
    text-decoration: none;
    white-space: nowrap;
}

.section-more:hover {
    color: var(--cobalt-mid-hover);
    text-decoration: underline;
}

/* Home sections sit closer together than the default 3rem section rhythm. */
.home-section {
    margin-bottom: var(--space-12);
    padding-top: 0;
}

/* Join Us — editorial two-column entries rather than three identical cards.
   Role sits in a hanging left column on wide screens, stacks on narrow. */
.join-intro {
    max-width: var(--measure);
    font-size: clamp(1.05rem, 0.98rem + 0.35vw, 1.2rem);
    color: var(--ink);
    margin-bottom: var(--space-12);
    text-wrap: pretty;
}

.join-block {
    display: grid;
    gap: var(--space-2) var(--space-8);
    padding-block: var(--space-8);
    border-top: 1px solid var(--border-light);
}

.join-block:last-of-type {
    border-bottom: 1px solid var(--border-light);
}

.join-block h3 {
    margin: 0;
    font-size: clamp(1.25rem, 1.1rem + 0.7vw, 1.6rem);
    color: var(--cobalt);
}

.join-block p {
    margin: 0;
    max-width: var(--measure);
    color: var(--ink);
    text-wrap: pretty;
}

.join-route {
    display: block;
    margin-top: var(--space-3);
    font-size: 0.9rem;
    color: var(--ink-faint);
}

@media (min-width: 46rem) {
    .join-block {
        grid-template-columns: minmax(11rem, 15rem) 1fr;
    }
}

/* Contact call-to-action */
.join-contact {
    margin-top: var(--space-12);
    padding: var(--space-8);
    background-color: var(--porcelain-pale);
    border-radius: 4px;
}

.join-contact h2 {
    margin-top: 0;
    margin-bottom: var(--space-3);
}

.join-contact p {
    max-width: var(--measure);
    margin-bottom: var(--space-6);
    color: var(--ink);
}

.join-contact p:last-child {
    margin-bottom: 0;
}

.button-primary {
    display: inline-block;
    padding: var(--space-3) var(--space-6);
    min-height: 44px;
    background-color: var(--cobalt);
    color: var(--white);
    font-weight: 700;
    text-decoration: none;
    border-radius: 3px;
    transition: background-color 140ms ease-out;
}

.button-primary:hover {
    background-color: var(--cobalt-mid-hover);
    color: var(--white);
}

.button-primary:active {
    transform: translateY(1px);
}

/* Publication graph (see js/pub-graph.js, data built by
   scripts/generate_publication_graph.py) — a network view of the same papers
   listed below it, opening as a band and expanding to the full field. */
/* Qualified with the element on purpose. This panel is a <section> nested in
   another one, so it matches the page's `section:first-of-type { padding-top:
   0 }` rule — which outranks a bare class and was flattening the padding above
   the drawing. Matching that rule's specificity, and coming later, wins it
   back without an !important or an id.

   Positioned: the tooltip is placed against this box, not against the canvas,
   so it can overhang the drawing without being clipped by it. */
section.pub-graph {
    position: relative;
    /* No card. This site's world is cobalt linework on white glaze, and a
       network drawing is that motif already — framing it in a bordered panel
       put a box around the one thing on the page that was the design. What
       held the drawing together was never the border; it is the space around
       it, which is why the margins below are generous and the padding is
       gone. */
    margin: var(--space-8) 0 var(--space-12);
    padding: 0;

    /* The ground the drawing sits on. Named once so the edge fade and the
       knockout behind the theme captions cannot drift from it — both have to
       match the page exactly or they show as a pale rectangle. */
    --graph-ground: var(--white);
}

/* Empty on success, so it must not hold a line of blank space open above the
   drawing. It stays in the DOM as the live region a load failure announces
   through. */
.pub-graph-status:empty {
    display: none;
}

.pub-graph-status {
    margin: 0 0 var(--space-3);
    max-width: var(--measure);
    font-size: 0.9rem;
    line-height: 1.55;
    color: var(--ink-muted);
    text-wrap: pretty;
}

/* Nothing loaded, so there is nothing to frame or expand. The status line
   carries the explanation; an empty bordered box beside it would only look
   like a second failure. */
.pub-graph[data-stage="failed"] .pub-graph-stage {
    display: none;
}

/* Below ~34rem the fixed viewBox would shrink the nodes past the point of
   being aimable, so the drawing keeps its size and scrolls sideways instead.
   Wide enough screens never see a scrollbar. */
.pub-graph-canvas {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--cobalt-mid) transparent;
}

.pub-graph-canvas::-webkit-scrollbar {
    height: 8px;
}

.pub-graph-canvas::-webkit-scrollbar-track {
    background: transparent;
}

.pub-graph-canvas::-webkit-scrollbar-thumb {
    background: var(--cobalt-mid);
    border-radius: 999px;
}

.pub-graph-canvas::-webkit-scrollbar-thumb:hover {
    background: var(--cobalt);
}

.pub-graph-actions {
    display: flex;
    justify-content: center;
    /* The negative margin below tucks this row into the stage's last few
       pixels, where the fade — being absolutely positioned — would otherwise
       paint over it and grey the control out. */
    position: relative;
    z-index: 1;
    margin-top: var(--space-2);
}

/* No pill. Once the panel lost its frame, a bordered, shadowed capsule was the
   only hard edge left on the page and it read louder than the drawing it
   serves. What makes it a control instead is the label, the chevron, and the
   way both answer to the pointer — not a box drawn around them.

   Padding, not a border, carries the hit area: the target stays a full 44px
   tall while nothing about it is visible at rest. */
.pub-graph-expand {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 44px;
    padding: 0.5rem 0.6rem;
    font-family: var(--font);
    font-weight: 700;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--cobalt-mid);
    background: none;
    border: 0;
    /* Shapes the focus ring, which is the one time this does draw a box. */
    border-radius: 4px;
    cursor: pointer;
    transition: color 180ms ease-out, transform 110ms ease-out;
}

.pub-graph-expand:hover {
    color: var(--cobalt);
}

/* The chevron leans the way the graph is about to move — down to open the
   field, up to close it. In the expanded state it is already rotated, so the
   same 2px carries it visually upward without a second rule. */
.pub-graph-expand:hover .pub-graph-chevron,
.pub-graph-expand:focus-visible .pub-graph-chevron {
    transform: translateY(2px);
}

.pub-graph[data-stage="full"] .pub-graph-expand:hover .pub-graph-chevron,
.pub-graph[data-stage="full"] .pub-graph-expand:focus-visible .pub-graph-chevron {
    transform: rotate(180deg) translateY(2px);
}

/* Feedback on the press, not on release. */
.pub-graph-expand:active {
    transform: scale(0.96);
}

.pub-graph-chevron {
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.9;
    stroke-linecap: round;
    stroke-linejoin: round;
    /* Fast enough for the hover lean, slow enough that the half-turn on
       expand still reads as a turn. */
    transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
}

.pub-graph[data-stage="full"] .pub-graph-chevron {
    transform: rotate(180deg);
}

.pub-graph-svg {
    display: block;
    width: 100%;
    min-width: 34rem;
    height: auto;

    /* The opening band cuts the network at the top as well as the bottom, so
       both edges soften. A gradient laid over the drawing only ever covered
       the lower one, which is why the top read as a hard crop and the bottom
       as a fade — two different treatments for the same cut.

       This masks the drawing itself rather than veiling it, so the linework
       dissolves into the page instead of passing under a white pane, and it
       needs no colour of its own to match the ground.

       Left on in both stages. The layout keeps every node at least FIT_PAD
       from the frame — comfortably more than 30px once scaled — so in the
       expanded view the mask only ever fades empty canvas, and nothing has to
       be switched on and off mid-animation. */
    --graph-edge-fade: 30px;
    -webkit-mask-image: linear-gradient(to bottom,
        transparent 0,
        #000 var(--graph-edge-fade),
        #000 calc(100% - var(--graph-edge-fade)),
        transparent 100%);
    mask-image: linear-gradient(to bottom,
        transparent 0,
        #000 var(--graph-edge-fade),
        #000 calc(100% - var(--graph-edge-fade)),
        transparent 100%);
}

/* The graph assembles once, on first open: each link draws along its own
   length and each node settles in behind it, theme by theme. It is the one
   authored moment here — everything after it is feedback — and it says what
   the picture is before a reader has to work it out: separate papers, wired
   together by what they share. */
/* Raised from 0.22: the links used to sit on a faintly tinted panel that gave
   them something to read against. On bare white at that weight the network
   looked washed out rather than delicate. */
.pub-graph-edges path {
    fill: none;
    stroke: var(--cobalt-mid);
    stroke-opacity: 0.3;
    stroke-linecap: round;
    stroke-dasharray: var(--len);
    animation: pub-graph-wire 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(140ms + var(--i, 0) * var(--edge-stagger, 3ms));
    transition: stroke-opacity 160ms ease-out, stroke 160ms ease-out;
}

@keyframes pub-graph-wire {
    from { stroke-dashoffset: var(--len); opacity: 0; }
    12%  { opacity: 1; }
    to   { stroke-dashoffset: 0; opacity: 1; }
}

/* A bridge keeps its own dash pattern, so it fades in rather than drawing. */
.pub-graph-edges path.is-bridge {
    animation-name: pub-graph-fade-in;
}

@keyframes pub-graph-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* Hovering a node turns the picture into one paper's neighbourhood: its own
   edges come forward and the rest recede rather than disappearing, so the
   surrounding structure stays legible as context. */
.pub-graph-svg.is-focused .pub-graph-edges path {
    stroke-opacity: 0.08;
}

.pub-graph-svg.is-focused .pub-graph-edges path.is-near {
    stroke: var(--cobalt);
    stroke-opacity: 0.8;
}

/* Divides the network from the shelf of papers with no edge in the current
   layer. Hairline and dashed: it separates two regions, it is not a third
   thing to read. */
.pub-graph-shelf line {
    stroke: var(--border-light);
    stroke-width: 1;
    stroke-dasharray: 4 5;
}

.pub-graph-shelf-label {
    font-family: var(--font);
    font-size: 12px;
    letter-spacing: 0.4px;
    fill: var(--ink-faint);
}

/* Names a group of papers where the group actually is. The halo is what keeps
   it readable: the label sits over the drawing, and without a knockout the
   links behind it cut straight through the letterforms. */
.pub-graph-theme-label {
    font-family: var(--font);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.2px;
    text-anchor: middle;
    paint-order: stroke fill;
    stroke: var(--graph-ground);
    stroke-width: 4px;
    stroke-linejoin: round;
    pointer-events: none;
    /* Held back until the field opens: in the band most of these sit off-frame,
       and a caption for a group you cannot see names nothing. They arrive as
       the groups become visible, which is the point of expanding. */
    opacity: 0;
    transform: translateY(5px);
    transition: opacity 260ms ease-out, transform 380ms cubic-bezier(0.16, 1, 0.3, 1);
}

.pub-graph[data-stage="full"] .pub-graph-theme-label {
    opacity: 1;
    transform: translateY(0);
    transition-delay: calc(180ms + var(--i, 0) * 45ms);
}

/* While one paper's neighbourhood is showing, the captions step back with
   everything else. They label the whole field, and the whole field is not
   what is being read at that moment. */
.pub-graph[data-stage="full"] .pub-graph-svg.is-focused .pub-graph-theme-label {
    opacity: 0.25;
    transition-delay: 0s;
}

/* Added to reach a paper that no nearest-neighbour link caught, so it is drawn
   as what it is: the closest match available, not a close one. */
.pub-graph-edges path.is-bridge {
    stroke-dasharray: 3 4;
    stroke-opacity: 0.3;
}

.pub-graph-node {
    cursor: pointer;
    transition: opacity 160ms ease-out;
}

.pub-graph-nodes circle {
    /* Scale about the circle's own centre; without fill-box the origin is the
       whole SVG and every node flies in from the corner. */
    transform-box: fill-box;
    transform-origin: center;
    animation: pub-graph-node-in 380ms cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(240ms + var(--i, 0) * var(--node-stagger, 9ms));
}

@keyframes pub-graph-node-in {
    from { transform: scale(0); opacity: 0; }
    to   { transform: scale(1); opacity: 1; }
}

.pub-graph-node circle {
    /* The stroke colour is set per node from its own theme — a darker shade of
       the same pigment, the way painted porcelain outlines a wash. The neutral
       hairline that used to sit here was the one colour on the drawing that
       belonged to no theme. */
    stroke-width: 1;
    /* Keep the whole disc hit-testable, including the hollow rings below,
       whose unpainted centre would otherwise fall through to the canvas. */
    pointer-events: all;
    transition: stroke 160ms ease-out, stroke-width 160ms ease-out;
}

/* Present, but visibly outside the current layer's structure. */
.pub-graph-node.is-isolated circle {
    opacity: 0.5;
}

.pub-graph-svg.is-focused .pub-graph-node {
    opacity: 0.2;
}

.pub-graph-svg.is-focused .pub-graph-node.is-near {
    opacity: 1;
}

.pub-graph-node.is-active circle {
    stroke: var(--cobalt);
    stroke-width: 2.5;
}

.pub-graph-node:hover circle {
    stroke: var(--cobalt);
    stroke-width: 2;
}

/* Dimming is opacity only, never pointer-events: a receded node stays
   hoverable, so moving across the graph never crosses a dead patch. */
.pub-graph-node:focus-visible {
    outline: 2px solid var(--cobalt);
    outline-offset: 2px;
}

.pub-graph-tooltip {
    position: absolute;
    z-index: 5;
    /* Wider than a cursor-following tooltip would want to be. It now sits in
       the band under the drawing, where width is free and height is not: at
       22rem a long title ran to three lines and the card reached past the
       legend into the filters below it. */
    max-width: min(34rem, 100%);
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: var(--space-3);
    background: var(--white);
    border: 1px solid var(--border-light);
    border-radius: 5px;
    box-shadow: 0 1px 2px rgba(22, 50, 79, 0.1),
                0 10px 28px rgba(22, 50, 79, 0.16);
    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--ink);
    /* Never intercept the pointer: it follows the cursor, and a tooltip that
       could be hovered would flicker the node out from under it. */
    pointer-events: none;
}

.pub-graph-tooltip[hidden] {
    display: none;
}

.pub-graph-tooltip strong {
    font-family: var(--font);
    color: var(--cobalt);
    text-wrap: balance;
}

.pub-graph-tooltip-meta {
    color: var(--ink-faint);
    font-size: 0.8rem;
}

/* The terms the similarity was actually computed from — the tooltip's answer
   to "why is this paper here?". */
.pub-graph-tooltip-keys {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.35rem;
}

.pub-graph-key {
    font-size: 0.72rem;
    line-height: 1.5;
    padding: 0 0.4rem;
    border-radius: 999px;
    background: var(--porcelain-pale);
    color: var(--cobalt);
}

/* Collapsed, not merely transparent. Holding its height open reserved a band
   of empty space under the preview that read as a layout fault; and there was
   nothing to protect, since the note below moves with the canvas regardless. */
.pub-graph-legend {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.25rem;
    margin: var(--space-4) 0 0;
    padding: 0;
    font-size: 0.85rem;
    color: var(--ink-muted);
    /* max-height rather than height: the legend wraps to a different number of
       rows by viewport, and a fixed open height would clip it on one of them. */
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-height 380ms cubic-bezier(0.16, 1, 0.3, 1),
                opacity 200ms ease-out,
                margin-top 380ms cubic-bezier(0.16, 1, 0.3, 1);
    margin-top: 0;
}

.pub-graph[data-stage="full"] .pub-graph-legend {
    max-height: 9rem;
    opacity: 1;
    margin-top: var(--space-4);
    transition: max-height 420ms cubic-bezier(0.16, 1, 0.3, 1),
                opacity 300ms ease-out 200ms,
                margin-top 420ms cubic-bezier(0.16, 1, 0.3, 1);
}

.pub-graph-legend li {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}

.pub-graph-swatch {
    width: 0.7rem;
    height: 0.7rem;
    border-radius: 50%;
    flex: none;
}

/* Clicking a node scrolls the list to that paper, which lands the reader
   somewhere they did not choose to scroll. The flash says which of the
   entries on screen is the one they asked for. */
.pub-list > li.is-jump-target {
    animation: pub-jump-flash 1.6s ease-out;
}

@keyframes pub-jump-flash {
    0%, 25% {
        background: var(--porcelain-pale);
        box-shadow: inset 0 0 0 1px var(--border-light),
                    0 0 0 3px rgba(29, 79, 127, 0.35);
    }
    100% {
        background: var(--porcelain-white);
        box-shadow: inset 0 0 0 1px var(--border-light),
                    0 0 0 3px rgba(29, 79, 127, 0);
    }
}

/* The global reduced-motion rule zeroes the animation, which would leave the
   arrival unmarked. A static ring says the same thing without the motion; it
   stays until the next jump moves it. */
@media (prefers-reduced-motion: reduce) {
    .pub-list > li.is-jump-target {
        box-shadow: inset 0 0 0 1px var(--border-light),
                    0 0 0 3px rgba(29, 79, 127, 0.35);
    }
}

/* The site-wide reduced-motion rule already collapses the entrance and the
   transitions. Two things it cannot reach: a transition-delay, which without
   its animation is just a wait, and the camera, which lives in JS and arrives
   at the new framing instead of travelling to it. State still changes and
   still reads — there is simply no journey. */
@media (prefers-reduced-motion: reduce) {
    .pub-graph[data-stage="full"] .pub-graph-theme-label,
    .pub-graph[data-stage="full"] .pub-graph-legend {
        transition-delay: 0s;
    }

    .pub-graph-theme-label {
        transform: none;
    }
}

@media (max-width: 768px) {
    .pub-graph-expand {
        font-size: 0.68rem;
    }

}

/* Footer */
footer {
    background-color: var(--porcelain-white);
    color: var(--ink-muted);
    font-family: var(--font);
    font-style: italic;
    text-align: center;
    padding: 1.5rem;
    margin-top: 3rem;
    border-top: 1px solid var(--border-light);
}

/* Institutional affiliations sit above the copyright. They hold for every
   page, so the footer carries them rather than the homepage statement. */
footer p {
    margin: 0;
}

footer p + p {
    margin-top: 0.35rem;
}

/* The lab's own line carries the department; the two groups are cross-cutting
   affiliations, so they follow one step quieter. */
.footer-affiliations {
    font-size: 0.94rem;
}

/* There is no global <a> rule, so these would otherwise fall back to the
   browser's default blue against the porcelain ground. */
footer a {
    color: var(--cobalt-mid);
    text-decoration: underline;
    text-underline-offset: 0.15em;
    text-decoration-thickness: 1px;
}

footer a:hover {
    color: var(--cobalt-mid-hover);
    text-decoration-thickness: 2px;
}
