/* ===== Crop Improvement Lab — field & lesion palette =====
   Colors are sampled from the actual subject matter, not a generic
   theme default: paddy is a real muted field green (not neon), straw
   is the tan of a dried bacterial-blight lesion, rust is the necrotic
   lesion border, sage is the grey-green halo of a sheath blight
   lesion. Light is the default theme, so it lives in :root and needs no
   attribute; dark is the opt-in, carried by data-theme="dark" on <html>
   and set by the toggle in the header (see theme.js, and the inline
   script in each page's <head> that applies it before first paint).

   The homepage hero (animated field + its overlay) is deliberately
   EXCLUDED from theme-switching: it carries its own fixed palette in
   both modes rather than taking the page's. Light mode is a clear blue
   day, dark mode a night sky, and the theme toggle crossfades between
   them — but neither is driven by the --bg/--text variables below. The
   FIELD stays dark in both, which is what keeps the hero's light ink
   legible: a dark field under a bright sky is backlighting, not a
   mistake. Its own --hero-green/--hero-gold accents stay constant for
   the same reason. */
:root{
  /* Tells the browser which way to paint the things IT draws rather than we
     do: <select> dropdown popups, scrollbars, the text caret, spinners. Without
     it every theme is "light" to the UA, so on dark the contact form's dropdown
     rendered a white popup while its <option>s inherited near-white text —
     invisible. Any native control added later gets this for free. */
  color-scheme: light;
  --bg:#f3efe3;
  --bg-2:#eae3d0;
  --bg-accent:#e4dabd;
  --surface: rgba(20,18,10,0.035);
  --surface-strong: rgba(20,18,10,0.07);
  --border: rgba(20,18,10,0.14);
  --text:#211f16;
  /* Secondary text: labels, roles, captions. 7.4:1 against the page. Was
     #5c5847 at 6.2:1, weaker than the dark theme's own secondary text
     manages, which is what made light mode read as the faded one. */
  --muted:#514d3e;
  /* Running prose, as distinct from --muted's labels and captions, and
     darker again so a paragraph reads as text rather than as a caption.
     Each theme carries its own value for this rather than sharing one
     that only suits one of them. */
  --body:#3f3c2d;
  --green:#3f5c2e;
  --green-dim:#2e4622;
  --gold:#8a662c;
  --gold-dim:#6e5122;
  --sage:#4f6a3a;
  --rust:#7a3d24;
  --danger:#a8412a;
  --shadow-soft: rgba(30,25,15,0.12);
  --shadow-strong: rgba(30,25,15,0.22);
  --header-bg: rgba(243,239,227,0.75);
  --header-bg-scrolled: rgba(243,239,227,0.92);
  --selection-bg: rgba(138,102,44,0.22);
  --selection-text:#211f16;
  --tint-green-a: rgba(63,92,46,0.08);
  --tint-green-b: rgba(63,92,46,0.28);
  --tint-gold-a: rgba(138,102,44,0.08);
  --tint-gold-b: rgba(138,102,44,0.28);
  --tint-sage-b: rgba(79,106,58,0.28);
  /* news kind tags need a fill as well as a border for sage and rust */
  --tint-sage-a: rgba(79,106,58,0.08);
  --tint-rust-a: rgba(122,61,36,0.08);
  --tint-rust-b: rgba(122,61,36,0.30);
  /* Was #8a8567, which came to 3.24:1 on this page — under the 4.5:1 AA needs
     for normal text, and this token is only ever used at 11-13px (the footer
     copyright and credit, the contact form's note). Same hue and saturation,
     darkened until it clears it with headroom: 5.04:1. */
  --copyright:#6a664f;
  --placeholder-bg: rgba(20,18,10,0.03);
  --btn-on-accent:#f7fbf0;
  --chip-on-accent:#fbf3e4;
  --hero-green:#7c9464;
  /* Lifted for the daylight sky: the old #c9a15a was picked against a
     near-black dusk and drops to ~2.9:1 on blue. Only the hero h1's <em>
     uses this. */
  --hero-gold:#ffcf6e;
  /* STIX Two Text is the standard typeface family for scientific/technical
     publishing (built for compatibility with mathematical/scientific
     notation) — chosen because it's actually tied to the lab's identity,
     not because it's an unusual-looking display serif. Public Sans is
     the US government's institutional-service typeface: plain, highly
     legible, unmistakably serious rather than app-like. Mono is kept
     ONLY for inline gene/variant codes (OsMAPK6, Xa21) — it's no longer
     used for nav, tags, or section labels sitewide. */
  --font-display:'Instrument Serif', 'Instrument Serif Fallback', serif;
  --font-body:'Sora', 'Sora Fallback', sans-serif;
  --font-mono:'JetBrains Mono', 'JetBrains Mono Fallback', monospace;
}

/* ===== Metrics-matched fallback faces =====
   The webfonts arrive from gstatic after first paint, so the first screen a
   new visitor sees is always drawn in a local fallback and then re-drawn.
   Left alone that is a visible snap, and it lands right on top of the hero
   entrance animation: measured at 72px, plain `serif` sets "Crop Improvement
   Lab" about 20% WIDER than Instrument Serif does, while plain `sans-serif`
   sets body copy about 12% NARROWER than Sora. Headline and prose therefore
   jump in opposite directions at the same moment, often taking a line break
   with them.

   These faces re-proportion the local stand-ins to the metrics of the real
   fonts, so the fallback occupies the space the webfont is about to need and
   the swap costs no reflow. Each is a local() alias with no download of its
   own. `size-adjust` is the webfont's average advance width over the
   fallback's; the ascent and descent overrides are the WEBFONT's own values
   divided by that same factor, since size-adjust rescales them too. All
   figures were measured off the rendered faces, not estimated.

   Times New Roman and Arial are named first because they are the common case
   on Windows and macOS; Liberation and Tinos are their metric-compatible
   clones and cover Linux. Where none of them exist the face simply fails to
   match and the stack falls through to the generic keyword, which is exactly
   what happened before this block existed. */
@font-face{
  font-family:'Instrument Serif Fallback';
  src:local('Times New Roman'), local('Liberation Serif'), local('Tinos'), local('Times');
  size-adjust:81.96%;
  ascent-override:120.79%;
  descent-override:37.82%;
  line-gap-override:0%;
}
/* The header wordmark sets "Lab" in italic on every page, and the homepage
   h1 carries an italic <em>, so the slanted face needs its own numbers
   rather than a synthesised slant off the roman. */
@font-face{
  font-family:'Instrument Serif Fallback';
  font-style:italic;
  src:local('Times New Roman Italic'), local('Liberation Serif Italic'), local('Tinos Italic');
  size-adjust:86.35%;
  ascent-override:114.65%;
  descent-override:35.90%;
  line-gap-override:0%;
}
@font-face{
  font-family:'Sora Fallback';
  src:local('Arial'), local('Liberation Sans'), local('Arimo'), local('Helvetica');
  size-adjust:110.57%;
  ascent-override:87.73%;
  descent-override:26.23%;
  line-gap-override:0%;
}
/* Mono needs no width correction: both faces are monospaced on a 0.6em
   advance, so they already set to the same measure. Only the vertical box
   differs, and correcting it keeps the gene codes from nudging their line
   height when the real face lands. */
@font-face{
  font-family:'JetBrains Mono Fallback';
  src:local('Courier New'), local('Liberation Mono'), local('Cousine'), local('Courier');
  size-adjust:99.98%;
  ascent-override:102.02%;
  descent-override:30.00%;
  line-gap-override:0%;
}

[data-theme="dark"]{
  color-scheme: dark;   /* see the note in :root */
  --bg:#13120e;
  --bg-2:#181712;
  --bg-accent:#221f16;
  --surface: rgba(255,250,240,0.045);
  --surface-strong: rgba(255,250,240,0.08);
  --border: rgba(255,250,240,0.1);
  --text:#f1ece0;
  --muted:#a9a390;
  /* see the note on --body in :root. On this theme prose and secondary
     text land on the same value: at 7.5:1 it already reads as text. */
  --body:#a9a390;
  --green:#7c9464;
  --green-dim:#5a7048;
  --gold:#c9a15a;
  --gold-dim:#a17f42;
  --sage:#93a878;
  /* Was #9c5233 at 3.27:1. The tints below are separate literals, so lifting
     this to 5.0:1 moves only the one thing that uses it as TEXT — the
     "Opening" news tag — and leaves its border and fill where they were.
     Worth getting right before rather than after a job ad goes up. */
  --rust:#c36d49;
  --danger:#c9614a;
  --shadow-soft: rgba(0,0,0,0.25);
  --shadow-strong: rgba(0,0,0,0.35);
  --header-bg: rgba(8,15,12,0.65);
  --header-bg-scrolled: rgba(8,15,12,0.85);
  --selection-bg: rgba(201,161,90,0.3);
  --selection-text:#fff;
  --tint-green-a: rgba(124,148,100,0.1);
  --tint-green-b: rgba(124,148,100,0.35);
  --tint-gold-a: rgba(201,161,90,0.08);
  --tint-gold-b: rgba(201,161,90,0.35);
  --tint-sage-b: rgba(147,168,120,0.35);
  --tint-sage-a: rgba(147,168,120,0.10);
  --tint-rust-a: rgba(156,82,51,0.12);
  --tint-rust-b: rgba(156,82,51,0.38);
  /* see the note on --copyright in :root — this theme's value was 3.32:1,
     lifted to 5.0:1 the same way */
  --copyright:#74897b;
  --placeholder-bg: rgba(255,250,240,0.03);
  --btn-on-accent:#04160f;
  --chip-on-accent:#241703;
}
*{box-sizing:border-box;}
/* Full-bleed sections (.team-rows) are sized in vw, and vw includes the
   scrollbar — so on any desktop with a classic scrollbar the page was a
   few px wider than the window and picked up a horizontal scrollbar.
   `clip` rather than `hidden`: hidden would turn the body into a scroll
   container and break position:sticky elsewhere on the site. */
html,body{overflow-x:clip;}
/* nothing should ever be able to push the page sideways */
img,svg,video,canvas,iframe{max-width:100%;}
/* Grid and flex items default to min-width:auto, so one wide child (an
   embedded map, a long address) can force a track wider than the column
   and push the whole card off screen. */
.contact-grid > *,.pub > *{min-width:0;}
html{scroll-behavior:smooth;}
/* The header floats 18px down from the top and is about 71px tall, so an
   anchor link that lands a section at y=0 parks that section's heading
   behind it. Every in-page link target on the site is a <section>. */
section[id]{scroll-margin-top:110px;}
body{
  margin:0;
  background:
    radial-gradient(ellipse 70% 50% at 50% -10%, var(--bg-accent) 0%, transparent 60%),
    var(--bg);
  color:var(--text);
  font-family:var(--font-body);
  -webkit-font-smoothing:antialiased;
  /* No overflow-x here. It used to say `hidden`, which silently undid the
     `html,body{overflow-x:clip}` rule above — and `hidden` on one axis forces
     the other to compute to `auto`, which makes the body a scroll container
     and stops every position:sticky on the site from ever sticking. That is
     exactly what the comment on that rule warned against. It was already
     breaking the sticky side column on research.html; the pinned variety rail
     on the homepage is the second thing to need it. `clip` above still does
     the job it was added for. */
  transition:background-color .3s ease, color .3s ease;
}
body::after{
  content:'';position:fixed;inset:0;pointer-events:none;z-index:0;opacity:0.05;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
h1,h2,h3,.display{font-family:var(--font-display);font-weight:400;}
/* section headings — was an inline font-size:2.4rem repeated per section,
   which froze them at one size on every display. Same clamp treatment as
   the hero: today's size at ~1280px, scaling up on larger screens. */
.section-h2{font-size:clamp(1.9rem, 1.3rem + 1.37vw, 3.4rem);}
.mono{font-family:var(--font-mono);}
a{color:var(--gold);text-decoration:none;}
/* A fixed 1200px column meant the page filled a 1280px laptop almost edge
   to edge but sat in the middle of a 2560px monitor with 680px of dead
   space either side — the same design reading very differently depending
   on the screen. The cap now grows with the viewport up to 1440px and
   stays at ~today's width on a typical laptop, so the proportions hold. */
.container{width:100%;max-width:min(94vw, 1440px);margin:0 auto;padding:0 28px;position:relative;z-index:1;}
::selection{background:var(--selection-bg);color:var(--selection-text);}

.glass{
  background:var(--surface);
  border:1px solid var(--border);
  border-top:2px solid var(--green-dim);
  border-radius:4px;
}
.glass:hover{background:var(--surface-strong);}

header.site{
  position:fixed;top:18px;left:50%;transform:translateX(-50%);
  width:min(92%,1080px);z-index:100;
  display:flex;align-items:center;justify-content:space-between;
  /* the brand used to wrap onto three lines on a phone, making the header
     129px tall and pushing the hamburger on top of the text — nothing in
     here may wrap or grow past its content */
  flex-wrap:nowrap;gap:12px;
  padding:14px 26px;border-radius:100px;
  background:var(--header-bg);
  border:1px solid var(--border);
  backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
  box-shadow:0 20px 60px var(--shadow-strong);
}
/* NOT min-width:0. white-space:nowrap is what stops the wordmark wrapping to
   three lines on a phone; min-width:0 was doing something else entirely —
   letting the flex line shrink this box below the text inside it. Since the
   text cannot wrap, the surplus simply overflowed to the right and printed
   ON TOP of the first nav link. Measured at 901px: the box was 77px narrower
   than its own content and "Lab" landed across "HOME". min-width:auto (the
   flex default) means the brand can never be overrun; the nav swap below is
   what makes sure there is room for it. */
.brand{font-family:var(--font-display);font-size:clamp(1.02rem, 2.6vw, 1.55rem);
  letter-spacing:0.01em;white-space:nowrap;}
.brand em{color:var(--green);font-style:italic;}
/* The header brand is a link home; the footer one is still a plain div.
   color:inherit rather than a value of its own, so becoming an anchor does
   not hand it the gold that a{} gives every other link. Green on hover is
   what the nav links beside it do. */
a.brand{color:inherit;transition:color .2s;}
a.brand:hover{color:var(--green);}
nav.links{display:flex;gap:2rem;}
nav.links a{
  color:var(--muted);font-family:var(--font-mono);font-size:0.82rem;
  text-transform:uppercase;letter-spacing:0.12em;transition:color .2s;
  position:relative;padding-bottom:2px;
}
nav.links a::after{
  content:'';position:absolute;left:0;bottom:-4px;width:0;height:1px;
  background:var(--green);transition:width .3s cubic-bezier(0.16,1,0.3,1);
}
nav.links a:hover, nav.links a.active{color:var(--green);}
nav.links a:hover::after, nav.links a.active::after{width:100%;}
header.site{transition:box-shadow .3s ease, background .3s ease;}
header.site.scrolled{box-shadow:0 20px 60px var(--shadow-strong);background:var(--header-bg-scrolled);}
header.site .btn{font-size:0.8rem;}
.theme-toggle{
  display:inline-flex;align-items:center;justify-content:center;
  width:38px;height:38px;border-radius:50%;flex-shrink:0;
  border:1px solid var(--border);background:transparent;color:var(--text);
  cursor:pointer;transition:background .2s ease,border-color .2s ease;
  margin-right:6px;
}
.theme-toggle:hover{background:var(--surface-strong);border-color:var(--green);}
.theme-toggle svg{width:18px;height:18px;}
/* the button shows where you are going, not where you are: a moon on the
   light theme, a sun on the dark one */
.theme-toggle .sun-icon{display:none;}
[data-theme="dark"] .theme-toggle .sun-icon{display:block;}
[data-theme="dark"] .theme-toggle .moon-icon{display:none;}
.btn{
  display:inline-flex;align-items:center;gap:8px;
  background:var(--green);color:var(--btn-on-accent);font-family:var(--font-mono);
  font-size:0.72rem;text-transform:uppercase;letter-spacing:0.1em;font-weight:600;
  padding:11px 22px;border-radius:6px;
  border:1px solid var(--green);transition:all .2s ease;
}
.btn:hover{background:transparent;color:var(--green);}
.btn-outline{background:transparent;color:var(--text);border:1px solid var(--border);}
.btn-outline:hover{border-color:var(--green);color:var(--green);}

.tag{
  display:inline-flex;align-items:center;
  font-family:var(--font-mono);font-size:0.68rem;font-weight:500;text-transform:uppercase;
  letter-spacing:0.1em;color:var(--gold);
  border-left:2px solid var(--gold);
  padding:2px 0 2px 12px;
}

/* ===== Running prose is justified =====
   hyphens:auto matters more here than it looks. Justifying a 66ch measure
   that is full of long species and gene names without it opens rivers of
   white space between the words, which is worse than the ragged edge it
   replaced. Every page declares lang="en", which is what the browser's
   hyphenation dictionary keys off.

   p.lede is matched on its own rather than per-page, so a lede in any
   wrapper (the homepage hero, the .page-hero on every inner page, the loose
   one in gallery's .container) is covered without another selector each time.

   What is NOT here, and why. These are alignment or type decisions that
   fight justification rather than benefit from it:
     .cta-box p, .philosophy p (research.html)
                   centred. Justify and centre are mutually exclusive; the
                   result would be a full-width first line above a
                   left-aligned last one, inside a box whose whole layout is
                   symmetrical.
     .qcard p      the two research questions, set in display type at up to
                   1.6rem. They read as headings, and at that size a line
                   holds too few words to distribute space between.
     .pub rows, .team-pill, .role, .journal, p.mono, .variety-name
                   metadata and labels, not running text.
   .card p IS here now, but it is the tightest fit on the list: those blurbs
   sit in ~300px columns at about 40 characters a line, and justification is
   only holding together there because hyphenation is on. If the bento cards
   start looking gappy, that is the selector to drop first. */
.rblock .body p,
p.lede,
.feature-strip .side-a p,
.card p,
.news-item p,
.lightbox-caption .note{
  text-align:justify;
  -webkit-hyphens:auto;hyphens:auto;
  /* stops the last line falling to a single orphaned word, which justified
     text makes far more obvious than ragged-right does */
  text-wrap:pretty;
}

/* The bento blurbs need more width than anything else on that list before
   justification pays off, because .col-4 puts them in a third of the
   container the moment the layout goes multi-column at 900px. Measured, at
   the 0.95rem they are set in:
       961px viewport -> 203px column -> 24 characters a line
      1280px          -> 303px        -> 35
      1920px          -> 382px        -> 45
   Justified text wants 45+ and tolerates about 40 with hyphenation on, so
   only the top of that range earns it. Below the gate they stay ragged-right,
   which at 24 characters a line is by far the lesser evil. The general
   phone-width rule further down turns everything else off at 600px; this is
   the one selector that needs to give up much earlier. */
@media (max-width:1599px){
  .card p{text-align:left;}
}

/* Usually a <div> sitting above a real .section-h2, but on team.html it IS the
   section's heading and is marked up as an <h2> there — that page has nothing
   else between its h1 and the member names, so without it the roster ran
   h1 -> h3. The margin is written out in full rather than as margin-bottom so
   an <h2> here does not also bring the UA's default top margin with it. */
.section-label{font-family:var(--font-mono);font-size:0.75rem;text-transform:uppercase;
  letter-spacing:0.18em;color:var(--green);display:flex;align-items:center;gap:14px;margin:0 0 18px;}
.section-label .rule{height:1px;background:var(--border);flex:1;}

/* hero media: real video if present, animated field as graceful fallback */
.hero-media{
  position:relative;min-height:100vh;display:flex;align-items:flex-end;
  overflow:hidden;background:linear-gradient(180deg,#2f6ea8 0%,#69a3cd 55%,var(--bg) 100%);
}
/* An atmospheric shadow anchored bottom-left, not a rectangular scrim. On a
   blue sky a flat black scrim reads as a grey film laid over the picture; a
   soft ellipse rooted in the corner the text occupies reads as depth, and it
   leaves the sun's half of the sky untouched. The tint is deep navy rather
   than neutral black for the same reason — it stays in the sky's hue family. */
.hero-overlay{
  position:absolute;inset:0;
  background:
    radial-gradient(125% 95% at 6% 94%, rgba(6,24,44,0.88) 0%, rgba(6,24,44,0.58) 32%,
                    rgba(6,24,44,0.16) 60%, rgba(6,24,44,0) 78%),
    linear-gradient(180deg, rgba(6,24,44,0.28) 0%, transparent 26%, transparent 60%, rgba(6,24,44,0.36) 100%);
}
/* The hero bottom-aligns its content inside a 100vh box. With only a fixed
   190px of bottom padding and no top padding, a wide-but-short window (a
   1080p monitor once you subtract browser chrome and the taskbar) pushed
   the eyebrow and h1 up underneath the fixed header — measured at 89px of
   overlap at 2200x780. The top padding now reserves the header's height,
   and the bottom padding gives way on short screens instead of the top. */
.hero-content{position:relative;z-index:2;
  padding:clamp(110px, 15vh, 170px) 28px clamp(70px, 14vh, 190px);
  width:100%;max-width:min(94vw, 1440px);margin:0 auto;}
/* Fluid rather than a fixed 4rem: a fixed heading is ~5% of the width of a
   1280px screen but only ~2.5% of a 2560px one, which is why the hero read
   as "smaller" on the bigger monitor. These clamps are tuned to land on
   today's sizes at ~1280px, then scale up from there. */
/* the inner min() caps the heading against viewport HEIGHT too — on a short
   window a purely width-driven size gets tall enough to overrun the header */
/* Cool near-white rather than the old warm cream: against a blue sky a warm
   off-white reads as dingy, and the cooler ink also buys contrast (4.0:1 ->
   4.7:1 on the sky behind it). The shadow is depth, not a glow — it seats the
   heading in front of the sky instead of on it, and does the work a heavier
   scrim would otherwise have to do. */
.hero-content h1{font-size:clamp(2.6rem, min(1.75rem + 2.8vw, 9.5vh), 5.5rem);line-height:1.05;margin:0 0 24px;letter-spacing:-0.01em;max-width:16ch;color:#fbfdff;
  text-shadow:0 2px 20px rgba(4,20,38,0.55), 0 1px 3px rgba(4,20,38,0.35);}
.hero-content h1 em{font-style:italic;color:var(--hero-gold);}
.hero-content p.lede{font-size:clamp(1.05rem, 0.93rem + 0.28vw, 1.45rem);line-height:1.7;color:#e7e2d3;max-width:52ch;margin:0 0 32px;}
.eyebrow{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.2em;
  font-size:0.75rem;color:var(--hero-green);margin-bottom:18px;display:block;}
/* The homepage eyebrow needs lifting off the bright sky, but --hero-green also
   fills the primary hero button (below) and tints the scroll cue — moving the
   variable washes those out. So the eyebrow carries its own lighter sage. */
.hero-content .eyebrow{color:#d8ebc2;}
/* the hero carries its own palette regardless of site theme (see the note at
   the top of this file), so shared button classes need fixed colors here
   rather than the theme-switchable --green/--btn-on-accent */
.hero-content .btn{background:var(--hero-green);border-color:var(--hero-green);color:#04160f;}
.hero-content .btn:hover{background:transparent;color:var(--hero-green);}
.hero-content .btn-outline{color:#f1ece0;border-color:rgba(241,236,224,0.28);}
.hero-content .btn-outline:hover{border-color:var(--hero-green);color:var(--hero-green);}
/* The row holding the two hero buttons. It lived up with the old .hero block
   and very nearly went out with it: the buttons are inline-flex, so with this
   rule gone they still sat side by side and the loss showed only as the gap
   dropping from 16px to the 4px of collapsed inline whitespace — and, on a
   narrow screen, as a wrapped second button spaced by line-height instead. */
.hero-actions{display:flex;gap:16px;flex-wrap:wrap;}

/* ===== Scroll cue at the foot of the homepage hero =====
   Centred on the viewport rather than on .hero-content, whose text is
   left-aligned: down the middle is where a reader looks for this, and it
   keeps the cue clear of the two buttons at every width. Fixed light ink
   for the same reason the buttons above it have some: the hero is off the
   theme variables in both modes, so it cannot take --text. It sits low over
   the field, which is dark under either sky. */
.scroll-cue{
  position:absolute;left:50%;bottom:26px;transform:translateX(-50%);z-index:3;
  display:flex;flex-direction:column;align-items:center;gap:9px;
  font-family:var(--font-mono);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.22em;
  color:rgba(241,236,224,0.6);text-decoration:none;
  transition:color .3s ease, opacity .45s ease, visibility .45s ease;
}
.scroll-cue:hover{color:var(--hero-green);}
.scroll-cue svg{width:17px;height:17px;animation:scrollNudge 2.6s ease-in-out infinite;}
@keyframes scrollNudge{
  0%,100%{transform:translateY(-3px);opacity:0.5;}
  50%    {transform:translateY(4px);opacity:1;}
}
/* set by reveal.js on the first scroll, reusing the listener that already
   runs there for the header rather than adding a second one */
.has-scrolled .scroll-cue{opacity:0;visibility:hidden;}
@media (prefers-reduced-motion: reduce){
  .scroll-cue svg{animation:none;opacity:0.8;}
}
/* Set by the fit check in index.html. The cue is pinned to the foot of the
   hero, which is the right place only while the hero fits the screen. It
   does not always: on a 375-wide phone the hero's own content comes to
   867px against a 812px screen, and at 1024x600 it comes to 682px against
   600, either of which drops the cue below the fold on the one screenful
   where it was supposed to do its work. Pinning it to the viewport instead
   would land it on the buttons, because on those same short screens there
   is no room for both. So it is measured rather than guessed at, and a hero
   visibly cut off at the fold is left to say "scroll" by itself. */
.scroll-cue.is-unfit{display:none;}

/* Animated field — this IS the hero; there is no video layer.
   The plants are authored as SVG, rasterised once into bitmap "bands", and
   then drawn into a single canvas by the script in index.html. Animating
   anything *inside* an SVG makes the browser re-rasterise the vector tree
   every frame, at a cost that scales with the animated area rather than the
   node count: ~29fps here against ~58 with the sway paused. The pointer
   parallax was an SVG transform too, so it could never land faster than
   that repaint — which is what made it trail the cursor. */
.field-fallback{position:absolute;inset:0;z-index:0;overflow:hidden;}
.field-fallback svg{width:100%;height:100%;display:block;}
/* the hero is a stack now, not one SVG — see the day/night section below */
.field-fallback > svg{position:absolute;inset:0;}

/* ===== Sun, moon, and the day/night change on the theme toggle =====
   The hero runs off its own palette in BOTH themes (see the note at the top
   of this file); what changes is the sky and which body is up — a blue day
   against a night. The FIELD is dark under either. Layers, back to front:
       #fieldSvg    blue day sky + the sun's glow/halo  static
       .sky-night   cool night sky + the moon's glow    opacity crossfade
       .body-sun    the sun's disc                      transform + opacity
       .body-moon   the moon's disc                     transform + opacity
       #soilSvg     mud and waterline                   static, occludes both
       .field-view  the plant canvas                    unchanged

   Only opacity and transform animate, and only the two small discs travel —
   the glows ride the sky crossfade instead of moving under their own steam.
   That matters more than it looks: as its own animating layer a glow is
   ~1126x865 CSS px, and on a browser compositing in software that measured
   116-233ms rasterisation spikes.

   The sway deliberately keeps running underneath. On a browser compositing
   in software the canvas redraw and the sky crossfade together cost more
   than either alone (measured ~9fps on Firefox/Linux against a true 60 with
   the sway held), so pausing it would buy back the frame rate there — but a
   field that freezes mid-sunset is a worse hero than a brief stutter on the
   narrow slice of machines affected. Deliberate trade, not an oversight. */
:root{--travel:clamp(80px,11vh,150px);}

/* Narrow screens crop this SVG hard: at 375px only viewBox x 679-921 is on
   screen, so the sun cannot be moved sideways out of the heading's way — the
   heading spans the full width there. It goes UP instead, clear of the h1,
   and the halo tightens with it. Measured at 375x812 the h1 starts around
   viewBox y 117; at cy 132 with a 150-unit halo the heading was crossing the
   corona at ~2.2:1. cx/cy/r are CSS properties on SVG geometry, so this needs
   no second markup path. */
@media (max-width: 760px){
  .body-sun circle{cy:64px;r:38px;}
  #sunGlowEl{cy:70px;ry:280px;}
  #sunHaloEl{cy:70px;rx:112px;ry:112px;}
}

.sky-night{opacity:0;}
.body-sun {opacity:1;transform:translate3d(0,0,0);}
.body-moon{opacity:0;transform:translate3d(-26px,var(--travel),0);}

[data-theme="dark"] .sky-night{opacity:1;}
[data-theme="dark"] .body-sun {opacity:0;transform:translate3d(26px,var(--travel),0);}
[data-theme="dark"] .body-moon{opacity:1;transform:translate3d(0,0,0);}

/* .anim-ready is set one frame after the first paint by index.html. Without
   it, the inline theme script in <head> — which runs before the page paints —
   would make every dark-mode page load animate the sun setting on arrival. */
.anim-ready .sky-night{transition:opacity 900ms ease;}
.anim-ready .body-sun,
.anim-ready .body-moon{
  transition:transform 1100ms cubic-bezier(.32,.06,.24,1), opacity 900ms ease;
}
/* the arriving body waits for the departing one to clear */
.anim-ready:not([data-theme="dark"]) .body-sun,
.anim-ready[data-theme="dark"]       .body-moon{transition-delay:200ms;}

/* Held only while the transition runs, never permanently: Firefox is much
   more conservative than Chromium about promoting an <svg> to its own layer,
   and without promotion it repaints the vector tree instead of compositing. */
.is-transitioning .sky-night,
.is-transitioning .body-sun,
.is-transitioning .body-moon{will-change:transform,opacity;}

@media (prefers-reduced-motion: reduce){
  .anim-ready .sky-night,
  .anim-ready .body-sun,
  .anim-ready .body-moon{transition-duration:1ms;transition-delay:0ms;}
}

/* the one canvas everything is drawn into. It is only as tall as the plants
   — every pixel of it is cleared and redrawn each frame, so the empty sky
   above is not worth covering. Positioned by the script. */
.field-view{position:absolute;left:0;}

/* the source SVG the bands are rasterised from: hidden as soon as the first
   raster pass lands, and kept visible — with the original per-plant sway —
   if rasterising is unavailable or fails */
.field-build{position:absolute;inset:0;}
.field-fallback.is-rasterized .field-build{display:none;}
.field-fallback.is-svg-fallback .field-view{display:none;}
.field-build .panicle{transform-origin:bottom center;animation:sway ease-in-out infinite;}
.field-build .blade{transform-origin:bottom center;animation:swayBlade ease-in-out infinite;}

/* the canvas draw loop stops itself once the hero scrolls out of view; this
   covers the SVG fallback, which has no loop to stop */
.field-fallback.is-paused .panicle,
.field-fallback.is-paused .blade{animation-play-state:paused;}
@keyframes sway{
  0%{transform:rotate(-4deg);}
  50%{transform:rotate(5deg);}
  100%{transform:rotate(-4deg);}
}
@keyframes swayBlade{
  0%{transform:rotate(-2.5deg) skewX(-1deg);}
  50%{transform:rotate(3deg) skewX(1deg);}
  100%{transform:rotate(-2.5deg) skewX(-1deg);}
}
@media (prefers-reduced-motion: reduce){
  .panicle, .blade{animation:none;}
}

/* ===== True CSS 3D: perspective lives on the PARENT (the "camera"),
   preserve-3d on the card keeps its children in that 3D space, and
   the icon/link get their own translateZ so they visibly float
   above the card's plane as it tilts — real depth, not a flat
   rotated rectangle. ===== */
.bento{
  display:grid;grid-template-columns:repeat(12,1fr);gap:20px;margin-top:36px;
  perspective:1400px;           /* softer, more natural depth for a grid of cards */
  perspective-origin:50% 20%;   /* camera looks slightly down, like a natural eye-line */
}
.card{
  padding:32px;
  transform-style:preserve-3d;  /* keep children in 3D instead of flattening them */
  transition:transform .4s cubic-bezier(0.22,1,0.36,1), box-shadow .4s ease;
  will-change:transform;
}
.card:hover{box-shadow:0 20px 40px var(--shadow-strong);transform:translateY(-4px);}
.card .icon{
  width:46px;height:46px;border-radius:12px;display:flex;align-items:center;justify-content:center;
  background:var(--tint-green-a);border:1px solid var(--tint-green-b);margin-bottom:22px;color:var(--green);
  transform:translateZ(0px);transition:transform .4s cubic-bezier(0.22,1,0.36,1);
}
.card .icon svg{width:22px;height:22px;}
.card:hover .icon{transform:translateZ(38px);}  /* pops toward the viewer, only visible because of the parent perspective */
.card h3{font-size:1.4rem;margin:0 0 10px;font-weight:400;transform:translateZ(0px);transition:transform .4s ease;}
.card:hover h3{transform:translateZ(18px);}
.card p{font-size:0.95rem;line-height:1.6;color:var(--body);margin:0;}
.card .more{display:inline-block;margin-top:16px;font-family:var(--font-mono);font-size:0.7rem;
  text-transform:uppercase;letter-spacing:0.1em;color:var(--gold);}

.col-4{grid-column:span 4;}
.col-6{grid-column:span 6;}
.col-8{grid-column:span 8;}
.col-12{grid-column:span 12;}

.feature-strip{
  display:grid;grid-template-columns:1fr 1fr;gap:0;overflow:hidden;
}
.feature-strip .side-a{padding:44px;}
.feature-strip .side-b{
  padding:44px;background:linear-gradient(160deg, var(--tint-green-a), var(--tint-gold-a));
  border-left:1px solid var(--border);
}
/* Label left, value right. gap rather than justify-content alone: with only
   space-between, a long label and a long value meet in the middle with nothing
   holding them apart, and "Pest/disease hits" ran into "YSB & BB". */
.stat-row{display:flex;justify-content:space-between;gap:18px;padding:14px 0;border-bottom:1px dashed var(--border);}
.stat-row:last-child{border-bottom:none;}
/* The label is the fixed side of the pair and must not wrap while the value
   beside it still has room, or a three-word label turns into two lines against
   a one-line value. flex:0 0 auto alone did not hold "Biotic stresses
   targeted" on one line once the row got tight; nowrap does, and the value
   below takes the slack instead. */
.stat-row .k{color:var(--muted);font-size:0.9rem;flex:0 0 auto;white-space:nowrap;}
.stat-row .v{font-family:var(--font-mono);color:var(--green);font-size:1.05rem;
  text-align:right;min-width:0;}

/* ===== Varieties carousel =====
   A paged carousel driven by the two arrow buttons. Nothing here scrolls:
   the viewport clips and varietyCarousel() in index.html sets one translateX
   on the track, so the page's own scrolling is never touched.

   --per is the single source of truth for how many cards a page holds. The
   card's flex-basis is computed from it, and the script reads the same custom
   property back rather than keeping its own copy of the breakpoints. */
.varieties-head{
  display:flex;align-items:flex-end;justify-content:space-between;
  gap:24px;margin-top:2px;
}
/* heading and standfirst travel together as one column so the arrows sit
   beside the pair, and so that stacking below 640px puts the arrows after
   both of them rather than between them */
.varieties-intro{min-width:0;}
.varieties-sub{
  margin:8px 0 0;font-size:0.95rem;color:var(--body);max-width:60ch;
}

.carousel-nav{display:flex;gap:10px;flex:0 0 auto;}
.cnav{
  width:46px;height:46px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  /* filled and accented rather than a hairline outline: as a transparent
     circle with a --border edge these read as page furniture and were easy
     to miss entirely, which is the one control the carousel cannot do
     without */
  background:var(--surface-strong);
  border:1px solid var(--tint-green-b);
  color:var(--green);
  box-shadow:0 2px 10px var(--shadow-soft);
  cursor:pointer;
  transition:background .2s ease,border-color .2s ease,color .2s ease,opacity .2s ease;
}
.cnav svg{width:20px;height:20px;stroke-width:2.2;}
.cnav:hover:not(:disabled){background:var(--green);border-color:var(--green);color:var(--btn-on-accent);}
.cnav:focus-visible{outline:2px solid var(--green);outline-offset:3px;}
/* disabled rather than hidden at the ends: a control that vanishes moves the
   one beside it, and the reader loses the target they were aiming at */
.cnav:disabled{opacity:0.32;cursor:default;}

/* pan-y hands the vertical axis back to the browser so the page still
   scrolls normally through the carousel, and keeps the horizontal one for
   the swipe handler in index.html */
.variety-viewport{overflow:hidden;margin-top:30px;touch-action:pan-y;}
.variety-track{
  --per:4;--gap:24px;
  display:flex;gap:var(--gap);
  /* the script writes translateX; this is what makes a page change glide */
  transition:transform .62s cubic-bezier(0.16,1,0.3,1);
  will-change:transform;
}
.variety-card{
  flex:0 0 calc((100% - (var(--per) - 1) * var(--gap)) / var(--per));
  display:flex;flex-direction:column;text-align:left;
  background:none;border:none;margin:0;padding:0;color:inherit;font-family:inherit;
  cursor:zoom-in;
}
.variety-card:focus-visible{outline:2px solid var(--green);outline-offset:5px;border-radius:18px;}

.variety-shot{
  display:block;overflow:hidden;border-radius:16px;
  border:1px solid var(--border);background:var(--surface);
  /* a fixed box, so ten scans of two different heights still make one even
     row of cards */
  aspect-ratio:4/5;
}
.variety-shot img{
  width:100%;height:100%;display:block;
  /* object-position:top, not the default centre: these are pamphlets and the
     variety name is printed across the top of the scan */
  object-fit:cover;object-position:top;
  transition:transform .55s cubic-bezier(0.16,1,0.3,1);
}
.variety-card:hover .variety-shot img{transform:scale(1.045);}
.variety-card:hover .variety-shot{border-color:var(--tint-green-b);}

.variety-name{
  display:block;margin-top:16px;
  font-family:var(--font-display);font-size:1.35rem;line-height:1.2;color:var(--text);
}
.variety-card .genes{
  display:block;margin-top:7px;
  font-family:var(--font-mono);font-size:0.7rem;color:var(--gold);
  /* NOT text-transform:uppercase. Gene symbols are case-carrying names, and
     uppercasing turned Xa21 into XA21 and Pi54 into PI54 on screen even
     though the markup was correct. Letter-spacing is lower than the other
     mono labels here for the same reason: it was tuned for all-caps. */
  letter-spacing:0.02em;line-height:1.5;
}
/* the description belongs to the lightbox; on the card it would turn an even
   row into ten different heights */
.variety-card .note{display:none;}

.variety-dots{display:flex;gap:8px;justify-content:center;margin-top:34px;}
.variety-dots button{
  width:30px;height:4px;border-radius:100px;padding:0;
  border:none;background:var(--border);cursor:pointer;
  transition:background .25s ease,width .25s ease;
}
.variety-dots button[aria-selected="true"]{background:var(--green);width:46px;}
.variety-dots button:focus-visible{outline:2px solid var(--green);outline-offset:3px;}

@media (max-width:1280px){ .variety-track{--per:3;} }
@media (max-width:1000px){ .variety-track{--per:2;--gap:18px;} }
@media (max-width:640px){
  .variety-track{--per:1;--gap:14px;}
  /* side by side, the arrows took ~110px off the heading and broke it into
     three cramped lines; stacked, the heading gets the full column */
  .varieties-head{flex-direction:column;align-items:flex-start;gap:18px;}
  .cnav{width:40px;height:40px;}
  .variety-name{font-size:1.2rem;}
}

@media (prefers-reduced-motion: reduce){
  .variety-track{transition:none;}
  .variety-shot img{transition:none;}
  .variety-card:hover .variety-shot img{transform:none;}
}

/* ===== lightbox =====
   shared by gallery.html's grid, the homepage variety rail and the homepage
   news photos, so it lives here rather than in any page's local <style>. All
   of them open the full-resolution original — the pages themselves only ever
   load the downscaled display copies.

   The caption panel is OPT-IN: only the variety rail passes one, and the
   .has-caption class below is what narrows the image to make room. Callers
   that pass no caption (gallery, news) get exactly the layout they had. */
.lightbox{
  position:fixed;inset:0;background:rgba(10,12,9,0.92);backdrop-filter:blur(6px);
  display:none;align-items:center;justify-content:center;z-index:200;padding:40px;
}
.lightbox.open{display:flex;}
.lightbox img{max-width:90vw;max-height:85vh;border-radius:10px;}
/* Fixed colours, NOT var(--text)/--border. The overlay behind this is
   rgba(10,12,9,0.92) in BOTH themes, so the theme tokens are the wrong ones
   to read: on the light theme --text is #211f16, which put a near-black
   close button on a near-black scrim and made it effectively invisible.
   Filled rather than outlined for the same reason. */
.lightbox-close{
  position:absolute;top:22px;right:24px;z-index:3;
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--font-mono);font-size:0.72rem;
  text-transform:uppercase;letter-spacing:0.1em;
  color:#f4efe3;
  background:rgba(255,250,240,0.12);
  border:1px solid rgba(255,250,240,0.3);
  padding:9px 16px;border-radius:100px;cursor:pointer;
  transition:background .2s ease,border-color .2s ease,color .2s ease;
}
.lightbox-close svg{width:15px;height:15px;}
.lightbox-close:hover{background:#f4efe3;border-color:#f4efe3;color:#13120e;}
.lightbox-close:focus-visible{outline:2px solid #c9a15a;outline-offset:3px;}

/* Step through a set from inside the overlay. Only index.html's variety
   cards pass a group, so gallery.html never renders these. */
.lightbox-nav{
  position:absolute;top:50%;transform:translateY(-50%);z-index:3;
  width:52px;height:52px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  color:#f4efe3;
  background:rgba(255,250,240,0.12);
  border:1px solid rgba(255,250,240,0.3);
  cursor:pointer;
  transition:background .2s ease,border-color .2s ease,color .2s ease;
}
.lightbox-nav svg{width:22px;height:22px;}
.lightbox-nav.prev{left:24px;}
.lightbox-nav.next{right:24px;}
/* room for the arrows, so the figure and its caption stop short of them
   rather than running underneath. Below 900px the layout stacks and the
   arrows sit over the image, which is the usual compromise on a phone. */
.lightbox.has-nav{padding-left:96px;padding-right:96px;}
@media (max-width:900px){
  .lightbox.has-nav{padding-left:16px;padding-right:16px;}
  .lightbox-nav{width:44px;height:44px;}
  .lightbox-nav.prev{left:10px;}
  .lightbox-nav.next{right:10px;}
}
.lightbox-nav:hover{background:#f4efe3;border-color:#f4efe3;color:#13120e;}
.lightbox-nav:focus-visible{outline:2px solid #c9a15a;outline-offset:3px;}
.lightbox-nav[hidden]{display:none;}

/* Pinned to the overlay's top-left, mirroring the close button opposite.
   It is a direct child of .lightbox rather than of the caption panel, because
   a grid can carry a group but no captions (the masterclass photos) and still
   needs the count; left in the flex flow it would sit beside the figure. */
.lightbox-count{
  position:absolute;top:30px;left:28px;z-index:3;
  font-family:var(--font-mono);font-size:0.7rem;
  letter-spacing:0.12em;text-transform:uppercase;color:#a8a08c;
}
.lightbox-count:empty{display:none;}

/* index.html wraps its lightbox image in .lightbox-figure so a caption can sit
   beside it. gallery.html has no wrapper and is untouched by any of this. */
.lightbox-figure{display:flex;align-items:center;gap:38px;max-width:92vw;max-height:85vh;}
/* the stand-in thumbnail, up on screen while the viewing copy loads. A touch
   of blur reads as "still arriving" rather than as a low-quality image, and
   it goes the moment the real one swaps in. */
#lbImg.is-loading{filter:blur(6px);}
#lbImg{transition:filter .25s ease;}
@media (prefers-reduced-motion: reduce){ #lbImg{transition:none;} }
.lightbox-figure.has-caption img{max-width:min(56vw,660px);}
.lightbox-caption{
  /* Fixed colours, NOT var(--text)/--body/--gold. The overlay above is
     rgba(10,12,9,0.92) in both themes, so the theme tokens are the wrong
     ones to read here: on the light theme --body is #3f3c2d, which is
     near-black text on a near-black scrim. These are the dark theme's values,
     which is the background this panel actually sits on. */
  flex:0 1 340px;color:#c3bdaa;
  /* a tall pamphlet next to a short caption should not stretch the caption
     down the full height of the scan */
  align-self:center;
}
.lightbox-caption h3{margin:0 0 10px;font-size:1.6rem;font-weight:400;color:#f4efe3;}
/* same reasoning as .variety-card .genes: gene symbols keep their own case */
.lightbox-caption .genes{
  display:block;font-family:var(--font-mono);font-size:0.74rem;color:#c9a15a;
  letter-spacing:0.02em;margin-bottom:12px;
}
.lightbox-caption .note{display:block;font-size:0.92rem;line-height:1.65;}
@media (max-width:900px){
  /* side by side leaves neither enough room on a phone */
  .lightbox-figure{flex-direction:column;gap:18px;overflow-y:auto;}
  .lightbox-figure.has-caption img{max-width:86vw;max-height:52vh;}
  .lightbox-caption{flex:0 0 auto;max-width:86vw;}
}

.cta{padding:110px 0;}
.cta-box{padding:70px 40px;text-align:center;position:relative;}
.cta-box h2{font-size:2.8rem;margin:0 0 16px;position:relative;}
.cta-box p{color:var(--body);font-size:1.05rem;max-width:52ch;margin:0 auto 34px;position:relative;}
.cta-box .actions{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;position:relative;}

footer{padding:50px 0 34px;border-top:1px solid var(--border);margin-top:40px;}
.footer-grid{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:24px;}
.footer-links{display:flex;gap:24px;flex-wrap:wrap;}
.footer-links a{color:var(--muted);font-size:0.85rem;}
.footer-links a:hover{color:var(--green);}
.copyright{font-family:var(--font-mono);font-size:0.7rem;color:var(--copyright);margin-top:32px;
  text-transform:uppercase;letter-spacing:0.08em;text-align:center;}
/* Deliberately not the mono/uppercase/letter-spaced treatment above: that
   styling reads as legal boilerplate, which makes a personal credit shout.
   Normal case at a smaller size keeps it clearly subordinate to the line
   it sits under. */
.site-credit{font-size:0.72rem;color:var(--copyright);margin-top:10px;text-align:center;}

.rblock{padding:0 0 60px;display:grid;grid-template-columns:230px 1fr;gap:44px;}
.rblock .side{position:sticky;top:120px;align-self:start;}
.rblock .side .tag{margin-bottom:14px;}
/* h2: these seven ARE the sections of the research page, so they sit directly
   under its h1 and the .subhead blocks inside each one are the h3s. It used to
   run h1 -> h3 -> h4, skipping a level at the top and leaving the page's own
   structure a level lower than it reads. */
.rblock .side h2{font-size:1.6rem;margin:0;line-height:1.25;}
/* Two separate things were making this column look wrong.
   First, the reading measure was on .rblock .body p alone, so at 1920px the
   paragraphs stopped at 800px while the gene lists and citation blocks
   beside them ran the column's full 1166px: the text gave up 366px early
   against siblings that did not. The cap belongs to the column, so all of
   them share one right edge and the surplus becomes a clean margin.
   Second, the measure was never actually short. `ch` is the width of the
   digit zero, which in Sora is 12.1px against an average character of
   8.3px, so the old 66ch cap was not 66 characters but about 95 — already
   past a comfortable line. Widening it further would have made a genuinely
   hard-to-read line out of one that only looked cramped.
   So the width is bought with type size instead: ~90 characters, set in em
   so the measure tracks the font rather than drifting from it, at a size
   that puts those 90 characters across most of the column. Bigger text
   filling the space, rather than a longer line stretched into it. */
.rblock .body{max-width:46em;font-size:1.2rem;}
.rblock .body p{line-height:1.7;color:var(--body);margin:0 0 18px;}
.gene-list{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 20px;}
.gene{font-family:var(--font-mono);font-size:0.75rem;background:var(--surface);
  border:1px solid var(--border);padding:5px 10px;border-radius:6px;color:var(--text);}
.gene.host{color:var(--green);border-color:var(--tint-green-b);}
.gene.pathogen{color:var(--gold);border-color:var(--tint-gold-b);}
.gene.fungal{color:var(--sage);border-color:var(--tint-sage-b);}

/* ===== Team cards: rows of pills, each row offset sideways from the
   next — cards deliberately bleed off the left/right edge on alternating
   rows (clipped by .team-rows' overflow:hidden) rather than sitting in a
   neat aligned grid. The photo is sized close to the full pill height,
   not a small inset circle. ===== */
.team-rows{
  overflow:hidden;margin-top:56px;padding:6px 0;
  width:100vw;position:relative;left:50%;margin-left:-50vw;
  /* How far a row is allowed to shift sideways. A row moves out of the
     content box and into the page margin, so the margin is the limit: past
     it, .team-rows' overflow:hidden starts eating the end pill. This used
     to be a flat 160px, which only fits from about a 1730px window up — at
     1440px it cut 133px off the last card of every even row, and at 1280px
     it cut 288px of a 430px card, so the third person in the row was mostly
     gone. Now it is the margin itself, so the stagger is as large as the
     window can actually give it and no larger. The 16px (rather than the
     28px of padding) is slack for the scrollbar, which vw counts and the
     content box does not. */
  --stagger:min(160px, calc((100vw - min(94vw, 1440px)) / 2 + 16px));
}
/* matches .container so the team page lines up with every other section
   instead of staying pinned at 1200px on a wide monitor */
.team-rows-inner{max-width:min(94vw, 1440px);margin:0 auto;padding:0 28px;}
/* width:100% so the side margin below shifts the row rather than resizing
   it — on an auto width a negative margin-left just makes the box wider, so
   odd and even rows ended up different widths (and with differently sized
   pills) while both still ended at the same right edge, which is the exact
   opposite of the stagger this is for */
.team-row{display:flex;gap:26px;margin-bottom:30px;width:100%;}
.team-row:nth-child(odd){margin-left:calc(var(--stagger) * -1);}
.team-row:nth-child(even){margin-left:var(--stagger);}
.team-pill{
  display:flex;align-items:center;gap:20px;
  /* the pills share the row rather than each claiming a fixed 430px: three
     fixed cards plus their gaps need 1342px, which is more than the content
     box has below ~1450px, and the surplus went off the edge. 430px stays
     as the cap so nothing stretches past its designed size on a big
     monitor. */
  flex:1 1 0;min-width:0;max-width:430px;
  padding:8px 30px 8px 8px;
  border-radius:100px;
  border:1px solid var(--border);
  background:var(--surface);
  transition:background .25s ease, border-color .25s ease, box-shadow .25s ease;
}
.team-pill:hover{background:var(--surface-strong);border-color:var(--tint-green-b);box-shadow:0 12px 30px var(--shadow-strong);}
.team-pill .info{min-width:0;flex:1;}
/* ===== Nothing in a pill is ellipsised any more =====
   These three carried nowrap + overflow:hidden + text-overflow:ellipsis, on
   the theory that the pill was wide enough and clipping was a safety net.
   It was not a safety net; it was the normal case. Three pills to a row, each
   giving ~120-180px of it to the portrait, leaves the text 100-180px, and
   measured across the range: at 1440px six items were cut, at 961px roughly
   forty — every role and every address, and names with it ("Dr. Jamalod…",
   "hkpatel.ccmb@…"). An address that has been ellipsised cannot be read or
   copied and there is no title attribute behind it, so the page was hiding
   the very thing a reader comes to it for. Even at the pill's designed 430px
   the longest address (anjana.sharma.ccmb@csir.res.in, ~225px set) does not
   fit one line, so widening the pill was never going to be the answer.
   Let all three wrap instead. The pill gets taller, the row's pills stretch
   to match, and the stadium shape survives it. This is what the 600px block
   further down already did on phones; it is simply right at every width. */
.team-pill h3{font-size:1.15rem;font-weight:600;margin:0 0 4px;font-family:var(--font-body);
  white-space:normal;overflow-wrap:break-word;}
.team-pill .role{font-size:0.92rem;color:var(--muted);margin:0 0 5px;
  white-space:normal;}
/* addresses are one long unbreakable token, so they need an explicit licence
   to break mid-word — wrapping alone leaves them overflowing the pill */
.team-pill .email{
  white-space:normal;overflow-wrap:anywhere;max-width:100%;}
.team-pill .email a{font-size:0.78rem;}
/* A name that has no chance of fitting a phone pill can ship an abbreviated
   form beside the full one; only one of the pair is ever rendered. Swapped at
   the 600px breakpoint below. */
.team-pill h3 .name-short{display:none;}

.photo-placeholder{
  /* was a hard 180px with flex-shrink:0. On a 375px phone that ate 180 of
     the pill's 319px and left the name/role/email 79px, so every card read
     "Dr. H..." / "Scient..." with the address running off the screen.
     The 43% is what actually matters now that the pills are fluid: the
     photo competes with the text for the PILL's width, not the window's,
     and on a 1024px laptop a viewport-sized 180px photo left 47px for a
     name. The vw term still governs phones, where the pill is full width
     and a proportional photo would be the huge one again. */
  width:clamp(88px, min(20vw, 43%), 180px);aspect-ratio:1;
  border-radius:50%;flex-shrink:0;
  border:2px dashed var(--border);
  display:flex;align-items:center;justify-content:center;
  color:var(--muted);background:var(--placeholder-bg);
  position:relative;overflow:hidden;
}
.photo-placeholder svg{width:44px;height:44px;opacity:0.6;}
.photo-placeholder img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  border-radius:50%;display:none;
}
.photo-placeholder.has-photo{border-style:solid;}
.photo-placeholder.has-photo svg{display:none;}
.photo-placeholder.has-photo img{display:block;}

.pi-chip{display:inline-block;font-family:var(--font-mono);font-size:0.6rem;background:var(--gold);
  color:var(--chip-on-accent);padding:2px 7px;border-radius:100px;vertical-align:middle;margin-left:6px;}
.role{font-size:0.85rem;color:var(--muted);margin-bottom:6px;}
.email a{font-size:0.78rem;}

.pub-list{margin-top:20px;}
.pub{display:grid;grid-template-columns:90px 1fr auto;gap:24px;align-items:start;
  padding:26px 0;border-bottom:1px solid var(--border);}
.pub .year{font-family:var(--font-mono);color:var(--gold);font-size:0.95rem;padding-top:2px;}
/* h2, not h3. Each paper is a second-level section of a page whose h1 is
   "Our Publications", and there is no intervening heading for an h3 to sit
   under — the list ran H1 straight to H3, which is the jump screen-reader
   users navigate by. Nothing about the type changes; the tag does. */
.pub h2{font-size:1.1rem;font-weight:400;line-height:1.4;margin:0 0 8px;font-family:var(--font-body);}
.pub .journal{font-family:var(--font-mono);font-size:0.75rem;color:var(--green);text-transform:uppercase;letter-spacing:0.05em;}
.pub .more{align-self:center;font-family:var(--font-mono);font-size:0.7rem;text-transform:uppercase;
  letter-spacing:0.1em;border:1px solid var(--border);padding:8px 14px;white-space:nowrap;border-radius:100px;color:var(--muted);}
.pub .more:hover{border-color:var(--green);color:var(--green);}

/* ===== News & updates (homepage) =====
   A dated rail rather than another card grid. The homepage already carries a
   two-column feature strip, a bento of cards and the variety rail; a fourth
   card treatment would read as more of the same, and news is the one block
   on the page where the DATE is the primary axis. The rail is the same shape
   as .pub on the publications page — deliberately, since most items here are
   papers — but carries a kind tag and a sentence of context, which .pub does
   not. */
.news-list{margin-top:36px;border-top:1px solid var(--border);}
.news-item{
  display:grid;grid-template-columns:132px 1fr auto;gap:28px;align-items:start;
  padding:28px 0;border-bottom:1px solid var(--border);
  transition:background .25s ease;
  /* clears the fixed header when a shared link jumps straight to this item:
     header.site sits at top:18px and is ~56px tall, so anything less than
     this lands the headline underneath it */
  scroll-margin-top:110px;
}
/* Arriving from a shared link. The tint holds long enough to be seen after
   the scroll settles, then fades, so the reader is shown which entry the
   link meant without the page staying permanently marked. The left bleed
   matches the hover treatment above so the tint clears the text edge. */
.news-item:target{animation:news-landed 3.2s ease-out 1;}
@keyframes news-landed{
  0%, 55%{background:var(--tint-gold-a);
    box-shadow:-14px 0 0 var(--tint-gold-a), 14px 0 0 var(--tint-gold-a);}
  100%{background:transparent;box-shadow:none;}
}
/* Sits under the kind tag in the date rail, quiet until the row is hovered
   or the button is focused. It is a button, not a link: there is no separate
   page to open, it either hands the URL to the OS share sheet or copies it.
   The box is deliberately larger than the 15px glyph so the tap target on a
   phone is not the size of the icon. */
.news-share{
  align-self:start;margin-top:4px;
  display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;
  background:none;border:0;padding:0;cursor:pointer;
  color:var(--copyright);transition:color .2s ease;
}
.news-share svg{width:15px;height:15px;display:block;}
.news-item:hover .news-share{color:var(--muted);}
.news-share:hover{color:var(--green);}
.news-share:focus-visible{outline:2px solid var(--gold);outline-offset:2px;border-radius:50%;}
/* Two glyphs, one shown at a time — the same swap the theme toggle uses for
   its sun and moon. share.js only adds the class; which icon that means is
   settled here. */
.news-share .done-icon{display:none;}
.news-share.is-done .share-icon{display:none;}
.news-share.is-done .done-icon{display:block;}
.news-share.is-done{color:var(--green);}
/* nothing could be copied: the share glyph stays, tinted to say so */
.news-share.is-fail{color:var(--rust);}
/* the bleed keeps the hover tint off the text's left edge without shifting
   the grid, which a padding change would */
.news-item:hover{background:var(--surface);
  box-shadow:-14px 0 0 var(--surface), 14px 0 0 var(--surface);}
.news-when{display:flex;flex-direction:column;gap:7px;padding-top:3px;}
.news-when .date{font-family:var(--font-mono);color:var(--gold);font-size:0.95rem;line-height:1;}
.news-kind{
  font-family:var(--font-mono);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.12em;
  align-self:start;padding:3px 9px;border-radius:100px;white-space:nowrap;
  background:var(--tint-green-a);border:1px solid var(--tint-green-b);color:var(--green);
}
/* One tag per kind of lab news. Green is the default (papers out); gold marks
   things that are provisional or personal (preprints, awards, grants); sage is
   people moving through the lab; rust is the one kind a reader can ACT on, so
   it is the only tag that carries the warning-ish accent. */
.news-kind.is-preprint,
.news-kind.is-award,
.news-kind.is-grant{background:var(--tint-gold-a);border-color:var(--tint-gold-b);color:var(--gold);}
.news-kind.is-people,
.news-kind.is-workshop{background:var(--tint-sage-a);border-color:var(--tint-sage-b);color:var(--sage);}
.news-kind.is-opening{background:var(--tint-rust-a);border-color:var(--tint-rust-b);color:var(--rust);}
/* The middle cell of a news row. It is a flex container so an entry can carry
   a photo beside its text — a portrait for people news (awards, joiners), or
   .is-wide for a scene: a poster, a field trial, a group at a conference. An
   entry without a photo is unaffected, since a lone flex child lays out the
   same as a block. Thumbnails are downscaled offline rather than shrunk in the
   browser; the full team portraits are 300 KB-1.3 MB each and the conference
   photo is 3.4 MB, none of which belongs in a list row. */
.news-body{display:flex;gap:22px;align-items:flex-start;}
.news-photo{
  width:120px;height:120px;border-radius:50%;object-fit:cover;flex-shrink:0;
  border:1px solid var(--border);background:var(--placeholder-bg);
}
.news-photo.is-wide{width:240px;height:160px;border-radius:12px;}
/* A photo worth looking at properly gets wrapped in this and opens in the same
   lightbox the variety pamphlets use. It is a <button> rather than a click
   handler on the <img> so it is reachable by keyboard and announces itself. */
.news-photo-btn{
  padding:0;border:none;background:none;cursor:zoom-in;display:block;flex-shrink:0;
  border-radius:10px;line-height:0;
  transition:transform .3s cubic-bezier(0.22,1,0.36,1), box-shadow .3s ease;
}
.news-photo-btn:hover{transform:translateY(-2px);box-shadow:0 10px 22px var(--shadow-soft);}
.news-photo-btn:focus-visible{outline:2px solid var(--gold);outline-offset:3px;}
.news-photo-btn .news-photo{display:block;}

/* Several photos from one event — a strip under the text rather than a single
   thumbnail beside it. Two shots squeezed into the 240px photo slot would be
   too small to be worth clicking; given the full text column they are big
   enough to actually read as glimpses. auto-fit means a third or fourth photo
   flows onto its own row without touching this rule. */
/* Two up by default, which makes four photos a tidy 2x2 at a size worth
   looking at. Four across in this column would be 136px tiles — too small to
   be worth clicking. Exactly three is the one count that reads better as a
   single row than as 2 + 1, so it gets its own rule. */
.news-gallery{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
  gap:12px;margin-top:18px;max-width:580px;
}
.news-gallery:has(> :nth-child(3):last-child){grid-template-columns:repeat(3,minmax(0,1fr));}
.news-gallery .news-photo-btn{width:100%;}
/* A news row shows a few glimpses; the full set lives on the gallery page. */
.news-more-photos{display:inline-block;margin-top:14px;font-family:var(--font-mono);
  font-size:0.7rem;text-transform:uppercase;letter-spacing:0.1em;color:var(--gold);}
/* aspect-ratio rather than a fixed height: the tiles keep their proportions
   whether the strip is showing two across or three. */
.news-shot{
  width:100%;aspect-ratio:3/2;height:auto;object-fit:cover;display:block;
  border-radius:10px;border:1px solid var(--border);background:var(--placeholder-bg);
}
.news-item h3{font-size:1.1rem;font-weight:400;line-height:1.45;margin:0 0 8px;
  font-family:var(--font-body);}
.news-item h3 a{color:var(--text);}
.news-item h3 a:hover{color:var(--green);}
.news-venue{font-family:var(--font-mono);font-size:0.72rem;color:var(--green);
  text-transform:uppercase;letter-spacing:0.05em;margin-bottom:9px;}
.news-item p{font-size:0.92rem;line-height:1.65;color:var(--body);margin:0;max-width:68ch;}
.news-item .more{align-self:center;font-family:var(--font-mono);font-size:0.7rem;
  text-transform:uppercase;letter-spacing:0.1em;border:1px solid var(--border);
  padding:8px 14px;white-space:nowrap;border-radius:100px;color:var(--muted);}
.news-item .more:hover{border-color:var(--green);color:var(--green);}
.news-all{display:inline-block;margin-top:26px;font-family:var(--font-mono);font-size:0.72rem;
  text-transform:uppercase;letter-spacing:0.1em;color:var(--gold);}


/* ===== Video wall (homepage) =====
   Tiles are facades: a local still plus a play button, with the YouTube
   iframe built only on click (videoWall() in index.html). Nothing here loads
   from youtube.com until someone presses play. */
.video-grid{
  display:grid;
  /* 380px, not 300px: at 300 a 1920 screen fits four tracks, which stranded a
     single tile alone on a second row back when there were five videos. 380
     holds it to three across, so the set fills its rows evenly. */
  grid-template-columns:repeat(auto-fit,minmax(380px,1fr));
  gap:28px;margin-top:36px;
}
/* The card is a plain container; only the still is a button. Title, channel
   and the YouTube link are its SIBLINGS, not its children, because a button
   may not contain a link and because the player that replaces the button on
   click must not end up nested inside interactive content either. */
.video-card{display:flex;flex-direction:column;}
.video-open{
  display:block;text-align:left;
  background:none;border:none;margin:0;padding:0;cursor:pointer;color:inherit;
  font-family:inherit;
}
.video-open:focus-visible{outline:2px solid var(--green);outline-offset:5px;border-radius:16px;}

.video-thumb{
  position:relative;display:block;overflow:hidden;
  border-radius:14px;border:1px solid var(--border);
  background:var(--surface);
  /* every still is normalised to 16:9 offline, so the box can be fixed and
     the grid never reflows as the lazy images arrive */
  aspect-ratio:16/9;
}
.video-thumb img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .5s cubic-bezier(0.16,1,0.3,1),opacity .3s ease;
}
.video-open:hover .video-thumb img{transform:scale(1.04);}
.video-open:hover .video-thumb{border-color:var(--tint-green-b);}

.video-play{
  position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:58px;height:58px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  /* flat scrim rather than a backdrop blur: five of these sit on the page at
     once, and a live blur in each costs the compositor real work per frame */
  background:rgba(10,12,9,0.72);
  border:1px solid rgba(255,250,240,0.25);
  color:#f4efe3;
  transition:background .25s ease,transform .25s cubic-bezier(0.16,1,0.3,1);
}
.video-play svg{width:26px;height:26px;margin-left:2px;}
.video-open:hover .video-play{background:var(--green);transform:translate(-50%,-50%) scale(1.08);}

.video-title{
  display:block;margin-top:16px;
  font-family:var(--font-display);font-size:1.15rem;line-height:1.3;color:var(--text);
}
.video-meta{
  display:block;margin-top:7px;
  font-family:var(--font-mono);font-size:0.68rem;color:var(--muted);
  text-transform:uppercase;letter-spacing:0.1em;
}

/* once played, the still is swapped for the player and everything below it
   (title, channel, YouTube link) stays exactly where it was */
.video-thumb iframe{width:100%;height:100%;border:0;display:block;}

/* Escape hatch, always present. The inline player needs a real http(s) origin
   to satisfy YouTube: opened straight off disk as a file:// URL it returns
   "Video player configuration error 153", and the same is true behind
   networks that block the embed host. This link always works, so the section
   degrades to something usable instead of a dead rectangle. */
.video-alt{
  align-self:flex-start;margin-top:10px;
  font-family:var(--font-mono);font-size:0.66rem;color:var(--muted);
  text-transform:uppercase;letter-spacing:0.1em;
  border-bottom:1px solid var(--border);padding-bottom:2px;
}
.video-alt:hover{color:var(--green);border-bottom-color:var(--tint-green-b);}
.video-alt::after{content:" ↗";}

@media (max-width:760px){
  .video-grid{grid-template-columns:1fr;gap:22px;}
  .video-title{font-size:1.05rem;}
}

@media (prefers-reduced-motion: reduce){
  .video-thumb img,.video-play{transition:none;}
  .video-open:hover .video-thumb img{transform:none;}
  .video-open:hover .video-play{transform:translate(-50%,-50%);}
}

/* date rail | title | "read more" pill stops fitting across three columns
   here — the same width at which .pub gives up, and for the same reason. The
   rail turns into a single horizontal row so the date and kind tag sit beside
   each other rather than stacking into two near-empty lines. */
@media (max-width:760px){
  .news-item{grid-template-columns:1fr;gap:12px;}
  .news-item:hover{box-shadow:none;}
  .news-when{flex-direction:row;align-items:center;gap:12px;padding-top:0;}
  /* the rail is horizontal here, so the nudges that seat the icon under the
     kind tag in the vertical rail would only lift it off the centre line */
  .news-share{align-self:center;margin-top:0;}
  .news-body{gap:16px;}
  .news-photo{width:92px;height:92px;}
  .news-photo.is-wide{width:170px;height:114px;}
}
/* On a phone a portrait plus its gap takes a quarter of the width, and the
   paragraph beside it drops to roughly a 30-character measure. Below this the
   photo goes above the text instead so the prose gets the full column. */
@media (max-width:560px){
  .news-body{flex-direction:column;gap:14px;}
  .news-item .more{justify-self:start;align-self:start;}
  /* Stacked, a scene photo takes the whole column — it is the only place the
     layout can afford to show one properly. A portrait does not: a face blown
     up to full width reads as a profile page rather than a news row. */
  .news-photo-btn.is-wide{width:100%;}
  .news-photo.is-wide{width:100%;height:200px;}
  .news-photo{width:104px;height:104px;}
  /* one shot per row on a phone — side by side they are postage stamps */
  .news-gallery{grid-template-columns:1fr;gap:10px;}
}

/* mobile nav: hamburger + slide-down drawer, hidden on desktop */
.nav-toggle{
  display:none;flex-direction:column;justify-content:center;gap:5px;
  width:38px;height:38px;background:none;border:none;cursor:pointer;padding:0;flex-shrink:0;
}
.nav-toggle span{display:block;width:22px;height:2px;background:var(--text);
  transition:transform .25s ease, opacity .2s ease;}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg);}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0;}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
.mobile-drawer{
  position:absolute;left:0;right:0;top:100%;z-index:99;
  background:var(--bg);border-bottom:1px solid var(--border);
  padding:20px 40px 28px;
  display:flex;flex-direction:column;gap:4px;
  transform:translateY(-12px);opacity:0;visibility:hidden;
  transition:transform .3s cubic-bezier(0.16,1,0.3,1), opacity .2s ease, visibility 0s linear .3s;
}
.mobile-drawer.open{
  transform:translateY(0);opacity:1;visibility:visible;
  transition:transform .3s cubic-bezier(0.16,1,0.3,1), opacity .2s ease;
}
.mobile-drawer a{
  color:var(--text);font-family:var(--font-body);font-size:1.05rem;font-weight:500;
  text-decoration:none;padding:14px 0;border-bottom:1px solid var(--border);
}
.mobile-drawer a:last-child{border-bottom:none;}
.mobile-drawer a.active{color:var(--green);}

/* The nav swap happens at 1060px, NOT at the 900px the rest of the layout
   uses, because the header runs out of room a long way before the page does.
   The header is min(92%,1080px) wide and has to hold the wordmark, five nav
   links, the theme toggle and the Contact button on one nowrap line; measured,
   the last of those clears the right edge with 1px to spare at 1001px and with
   32px either side of the nav at 1061px. Below that the desktop nav simply
   does not fit, and the old 900px threshold left a ~160px band where it was
   still on screen with nothing but overlap to show for it. 1060 is where the
   row is comfortable rather than merely non-overlapping. */
@media (max-width:1060px){
  nav.links{display:none;}
  .nav-toggle{display:flex;}
}

/* The feature strip stacks at 1200, not at the 900 the rest of the layout
   uses, because its right-hand panel is a two-column key/value list inside
   one half of the container — so it runs out of room roughly twice as early
   as anything else. Measured on the widest value ("Bacterial blight · Sheath
   blight · Yellow stem borer"): 2 lines at 1280px, but 5 in a 145px column
   anywhere below ~1150. Stacked, that panel gets the full container width and
   the same value fits on one line. */
@media (max-width:1200px){
  .feature-strip{grid-template-columns:1fr;}
  /* Once the two halves stack, the rule between them is a horizontal one. The
     border-left it carries side-by-side becomes a stray vertical line down the
     outside of the lower panel. */
  .feature-strip .side-b{border-left:none;border-top:1px solid var(--border);}
}

/* ---- the mutant-resource spec list, stacked ----
   Label beside value stops working well before the phone breakpoint. The panel
   keeps 44px of padding either side, so at 700px the row is down to ~420px and
   the longest label ("Biotic stresses targeted") has to be held on one line by
   force, leaving the value a column too narrow to read; at 375px it was worse
   still — labels wrapping to two and three lines in a 65px column against
   values wrapping raggedly in the 200px left over, two narrow ragged columns
   rather than a spec list. 760 is where the side-by-side row still has room to
   breathe; below it the label goes above the value and the value gets the full
   width. Same move, and the same reason, as the contact page's .info-row. */
@media (max-width:760px){
  .feature-strip .side-a,
  .feature-strip .side-b{padding:26px 20px;}
  .stat-row{flex-direction:column;gap:5px;padding:13px 0;}
  /* the label now reads as a heading for the value beneath it rather than as
     the left half of a pair, so it takes the small mono treatment the site's
     other key/value blocks use */
  .stat-row .k{font-family:var(--font-mono);font-size:0.68rem;text-transform:uppercase;
    letter-spacing:0.1em;white-space:normal;}
  /* left, not right: right alignment only earned its keep while this was the
     far side of a two-column row */
  .stat-row .v{text-align:left;font-size:1rem;line-height:1.45;}
}

@media (max-width:900px){
  .rblock{grid-template-columns:1fr;}
  .rblock .side{position:static;}
  .col-4,.col-6,.col-8{grid-column:span 12;}
  .team-row{margin-left:0 !important;flex-direction:column;gap:14px;}
  .team-pill{width:100%;flex:1 1 auto;max-width:none;}
}

/* Phones: even with the brand shrunk and set to nowrap, brand + hamburger +
   theme toggle + a Contact button will not fit across ~317px of usable
   header. Contact moves into the drawer with the other links at this size
   (see the .mobile-drawer markup on every page). */
@media (max-width:600px){
  header.site{padding:10px 16px;gap:10px;}
  header.site > .btn{display:none;}
  .theme-toggle{width:34px;height:34px;}
  .nav-toggle{width:34px;height:34px;}
  /* padding and gap are tighter than the desktop pill's so that the larger
     portrait below does not come out of the text's width */
  .team-pill{padding:6px 12px 6px 6px;gap:12px;}
  /* Phone portraits are sized from the VIEWPORT alone, dropping the `43%`
     term in the base rule. Two things were wrong with inheriting it here.
     That percentage is a share of the PILL's content box, so the portrait's
     size was coupled to how much room the name left beside it. And the 20vw
     term then lost the min() on every phone (75px at 375px), so the clamp
     floored all 33 portraits at 88px — a thumbnail rather than a portrait,
     sitting in a pill that long names stretched to 149px tall, which is what
     made it look like the photo shrank when the name got longer.

     Sized to stay well clear of the text: the reclaimed padding above pays
     for most of the extra width, so the name and address keep roughly the
     column they had at 88px. The address stays in that column beside the
     portrait rather than on a row of its own — tried that, and it orphaned
     the address under the circle in the pill's curved bottom-left corner. */
  .photo-placeholder{width:clamp(96px, 29vw, 124px);}
  /* The wrapping these used to switch on is now the base behaviour at every
     width (see .team-pill h3/.role/.email above), so only the type sizes are
     left here: a step down so that a long name wraps to two lines beside the
     larger phone portrait rather than four, which keeps the cards closer to a
     common height. */
  .team-pill h3{font-size:1.02rem;}
  .team-pill .role{font-size:0.85rem;}
  /* the abbreviated form takes over where the full name cannot fit */
  .team-pill h3 .name-full{display:none;}
  .team-pill h3 .name-short{display:inline;}
  /* year | title | "read more" pill won't fit across a phone as three
     columns: the button was being pushed past the right edge */
  .pub{grid-template-columns:1fr;gap:10px;}
  .pub .more{justify-self:start;}
  /* Prose goes back to a ragged right edge on phones. A justified line
     needs enough words to absorb the extra space; at this width the
     measure is down to about 40 characters, and the only thing that
     rescues justification that tight is hyphenation, which not every
     browser ships a dictionary for. Without it the spaces stretch into
     visible holes. Wider screens keep the justified setting. */
  .rblock .body p,
  p.lede,
  .feature-strip .side-a p,
  .card p,
  .news-item p,
  .lightbox-caption .note{text-align:left;}
  /* The research column's type is enlarged to fill a wide desktop column.
     A phone has no width to fill, and at that size the same setting drops
     to 33 characters a line, which is too few to read comfortably. Back to
     the original size here, where it gives about 40. */
  .rblock .body{font-size:1.02rem;}
}

/* ===== Scroll-reveal system, shared across every page (see reveal.js) =====
   Elements marked [data-reveal] start hidden/offset and transition to
   their resting state once they cross into the viewport. reveal.js also
   auto-tags common repeating items (cards, rows, list entries) so most
   pages need no manual markup at all. */
[data-reveal]{
  opacity:0;
  transform:translateY(26px);
  transition:opacity .7s cubic-bezier(0.16,1,0.3,1), transform .7s cubic-bezier(0.16,1,0.3,1);
}
[data-reveal].is-visible{
  opacity:1;
  transform:translateY(0);
}

/* hero content: a fixed page-load sequence rather than scroll-triggered,
   since it's already on screen the moment the page opens */
.hero-content > *{
  opacity:0;
  transform:translateY(20px);
  animation:heroIn .9s cubic-bezier(0.16,1,0.3,1) forwards;
}
.hero-content .eyebrow{animation-delay:.1s;}
.hero-content h1{animation-delay:.24s;}
.hero-content p.lede{animation-delay:.4s;}
.hero-content .hero-actions{animation-delay:.56s;}
@keyframes heroIn{ to{opacity:1;transform:translateY(0);} }

/* subpage heroes (Research, Team, Publications, Gallery, Contact) get the
   same light entrance treatment, just simpler — a tag + heading + one line */
.page-hero > *{
  opacity:0;
  transform:translateY(18px);
  animation:heroIn .8s cubic-bezier(0.16,1,0.3,1) forwards;
}
.page-hero > *:nth-child(1){animation-delay:.08s;}
.page-hero > *:nth-child(2){animation-delay:.2s;}
.page-hero > *:nth-child(3){animation-delay:.32s;}
.page-hero > *:nth-child(4){animation-delay:.42s;}

@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto;}
  [data-reveal], .hero-content > *, .page-hero > *{
    opacity:1 !important; transform:none !important; animation:none !important; transition:none !important;
  }
}
