@charset "UTF-8";
/* Type, self-hosted from assets/fonts/ — no third-party request:
   Inria Serif (display), Archivo (body), IBM Plex Mono (labels).
   Latin and Latin-Extended subsets only, which covers fr and en. */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/archivo-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/archivo-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inria Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/inria-serif-400-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Inria Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/inria-serif-400-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inria Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/inria-serif-700-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Inria Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/inria-serif-700-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-mono-400-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-mono-400-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-mono-500-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-mono-500-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
  /* Ground — cool paper, cool near-black ink */
  --paper: #f2f4f7;
  --surface: #ffffff;
  --ink: #12161c;
  --ink-2: #4e5661;
  --ink-3: #6b727c;
  --rule: #cfd6de;
  /* Utility blue carries the structure, amber marks it: wires and rules are
     blue, the nodes and the ornament are amber. */
  --blue: #0f3a86;
  --blue-soft: #e3e9f5;
  --blue-deep: #0a2558;
  --amber: #f2a413;
  /* One amber, both grounds. A node clears the paper on the 1px --node-line
     cased around it rather than on its own fill contrast, so the bright stop
     carries here as well as on the blue. */
  --amber-ink: #8a5200;
  --amber-soft:#fdf0d6;
  --wire: var(--blue);
  --pulse: #3775dc; /* a signal on the wire: lighter than --wire, both themes */
  --node: var(--amber);
  /* Every node is cased in a hairline of ink, which is what separates it from
     its ground. It follows the ink, so it is near-black on paper and near-white
     on the dark theme's. */
  --node-line: var(--ink);
  --accent: var(--blue);
  --accent-soft: var(--blue-soft);
  /* The social register borrows the amber family, never the blue. */
  /* The label on an amber fill. Fixed, not --ink: the amber is the same on
     both grounds, so the text on it must be too. */
  --warm-on: #12161c;
  --warm: var(--amber);
  --warm-ink: var(--amber-ink);
  --warm-soft: var(--amber-soft);
  --feature-bg: var(--blue-deep);
  --feature-ink: #eef2f9;
  --feature-ink-2:#a8bbdd;
  --feature-warm: var(--amber);
  --feature-rule: rgba(255,255,255,.22);
  --foot-bg: #081b3f;
  --foot-ink: #dbe3f0;
  --foot-ink-2: #93a3bd;
  --foot-rule: #16305c;
  /* Type */
  --f-display: "Inria Serif", Georgia, "Times New Roman", serif;
  --f-body: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
  --f-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --t-xs: .8125rem;
  --t-sm: .9375rem;
  --t-base: 1rem;
  --t-md: 1.125rem;
  --t-lg: 1.375rem;
  --t-xl: 1.75rem;
  --t-2xl: clamp(2rem, 4.5vw, 2.75rem);
  /* Two shape languages. Square for the technical register, generous
     radii for the social one. */
  --r-tech: 0px;
  --r-soft: 14px;
  /* Ornament. Four strands — one per partner institution — woven on a 64px
     period and used in exactly one place, under the header. The tiles carry
     their own colours and casing ground, so they are drawn per theme by
     scripts/make_braid.py; rerun it whenever the palette moves. */
  --braid-size: 14px;
  --braid: url(/assets/img/braid.svg);
  /* Measure */
  --measure: 64ch;
  /* The prose column. In rem, not ch: `ch` resolves against each element's own
     font, so a display-font heading and a body-font paragraph would compute
     different widths and fail to line up. ~56 characters at the base size. */
  --prose: 34rem;
  --wrap: 64rem;
  /* Margin column that carries labels and the wire at wide widths */
  --gutter: 8rem;
  --gutter-gap: 3.5rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #0b1016;
    --surface: #121820;
    --ink: #e6ebf2;
    --ink-2: #9aa5b3;
    --ink-3: #7d8794;
    --rule: #232c37;
    --blue: #7ea6ff;
    --blue-soft: #14203a;
    --blue-deep: #0b1c3d;
    --pulse: #bcd3ff;
    --amber: #f5b73f;
    --amber-ink: #f0b352;
    --amber-soft:#23190c;
    --feature-bg: #0d2148;
    --feature-ink: #eaf0fb;
    --feature-ink-2:#a2b4d6;
    --feature-rule: rgba(255,255,255,.16);
    --foot-bg: #060d18;
    --foot-ink: #d5dcea;
    --foot-ink-2: #8695ab;
    --foot-rule: #182740;
    --braid: url(/assets/img/braid-dark.svg);
  }
}
*, *::before, *::after {
  box-sizing: border-box;
}

/* The root size the whole system is measured from: every token in
   _tokens.scss is in rem, so the page scales as one when this changes —
   type, the 34rem prose measure, the 64rem wrap, the logo's 4rem height.

   The browser's own default up to a 600px viewport, then growing to 1.5x of it
   by 1600px — the size the site reads at when a desktop browser is zoomed to
   150%, which is where it wants to be. At 16px and a 1024px wrap, a full-width
   desktop screen left the page small in the middle of a wide field of paper;
   this grows the page with the screen rather than widening the measure, which
   would have made the lines too long to read.

   The ramp starts at 600px so phones are untouched: they have no field of
   paper to fill, and 1.5x on a 390px screen would leave four or five words to
   the line.

   Written in rem rather than px so a reader who has set a larger default text
   size keeps it — 1rem here is that default, not 16px. Media queries are
   unaffected: they resolve against the initial font size, not this one, so
   every breakpoint in the sheet still fires where it did. */
html {
  -webkit-text-size-adjust: 100%;
  font-size: clamp(1rem, 0.8vw + 0.7rem, 1.5rem);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--t-base)/1.65 var(--f-body);
  -webkit-font-smoothing: antialiased;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 1.5rem;
}

a {
  color: var(--wire);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}

a:hover {
  text-decoration-color: var(--warm);
}

a:focus-visible, .skip-link:focus {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  background: var(--surface);
  color: var(--ink);
  padding: 0.6rem 1rem;
  z-index: 10;
}

.skip-link:focus {
  left: 1rem;
  top: 1rem;
}

/* Header */
.site-header {
  background: var(--paper);
  position: relative;
  padding-bottom: var(--braid-size);
}

.header-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  padding-top: 1.15rem;
  padding-bottom: 1.15rem;
}

.brand {
  text-decoration: none;
  color: inherit;
  display: block;
}

/* The logo, from _includes/logo.html. Height rather than font-size, because
   the mark is drawn art: it is 2302 units tall and its runs are 71 of them, so
   4rem puts a run at just under 2px — the weight it was drawn at — and the cap
   height at 19px, the size the wordmark was while it was still text. */
.brand .logo {
  display: block;
  height: 4rem;
  width: auto;
}

/* The Centre's full name, set as a justified two-line block under the mark
   rather than as one long line — a masthead sits under its logo, it does not
   run out past it. The measure is what makes it two lines: 45ch is a little
   wider than half of either language's name (83 characters in French, 89 in
   English), so both break once and neither reaches a third line. Mono, so `ch`
   is the real character width and the two languages measure alike.

   `text-align-last` is what justifies the second line; `justify` alone leaves
   the last line of a block ragged, which here is half of it. */
.brand-sub {
  display: block;
  margin-top: 0.35rem;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink-2);
}

@media (min-width: 34rem) {
  .brand-sub {
    max-width: 45ch;
    text-align: justify;
    text-align-last: justify;
  }
}
.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  font-size: var(--t-sm);
}

.site-nav a {
  text-decoration: none;
}

.site-nav a:hover {
  text-decoration: underline;
}

.lang-toggle {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  border: 1px solid var(--rule);
  border-radius: var(--r-tech);
  padding: 0.3rem 0.6rem;
}

.lang-toggle:hover {
  background: var(--accent-soft);
  text-decoration: none;
}

/* Hero — on surface, against the paper ground */
.hero {
  background: var(--surface);
  padding: 2.75rem 0 2.5rem;
  position: relative;
}

/* With no label in the margin the hero has nothing holding its right side,
   so it is centred instead of sitting left of a wide empty column. */
.hero .wrap:not(:has(.kicker)) {
  text-align: center;
}

.hero .wrap:not(:has(.kicker)) h1 {
  max-width: 24ch;
  margin-inline: auto;
}

.hero .wrap:not(:has(.kicker)) .lede {
  margin-inline: auto;
}

.hero .wrap:has(.kicker) {
  display: grid;
}

.hero .wrap:has(.kicker) h1 {
  order: 1;
}

.hero .wrap:has(.kicker) .kicker {
  order: 2;
  margin: 0.45rem 0 0;
}

.hero .wrap:has(.kicker) .lede {
  order: 3;
}

.hero h1 {
  margin: 0;
  max-width: 23ch;
  font-family: var(--f-display);
  font-size: var(--t-2xl);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.hero .lede {
  margin: 1rem 0 0;
  max-width: var(--measure);
  font-size: var(--t-md);
  color: var(--ink-2);
}

/* Sections. Ground colour carries the rhythm; rules only where two
   same-coloured bands meet. */
section.band {
  padding: 2rem 0;
  position: relative;
}

section.band + section.band {
  border-top: 1px solid var(--rule);
}

.band--surface {
  background: var(--surface);
}

.band--paper {
  background: var(--paper);
}

.band--surface + .band--paper,
.band--paper + .band--surface {
  border-top: 0;
}

section.band h2 {
  margin: 0 0 0.9rem;
  font-family: var(--f-display);
  font-size: var(--t-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
}

:where(section.band p) {
  max-width: var(--measure);
  margin: 0 0 0.8rem;
}

:where(section.band p:last-child) {
  margin-bottom: 0;
}

/* Body prose sits in one centred column, justified on both edges. Only
   unclassed direct children of a band's wrap: components (.axes, .event-list,
   .lede) carry their own width and alignment and must keep it. */
section.band > .wrap > :is(h2, h3, p, ul, ol, dl, blockquote):not([class]) {
  max-width: var(--prose);
  margin-inline: auto;
}

/* Lists too, not just paragraphs: a list sits in the same column as the
   paragraph that introduces it, and a ragged one under a justified one reads
   as two different settings. Headings stay out — they are display type. */
section.band > .wrap > :is(p, ul, ol, dl, blockquote):not([class]) {
  text-align: justify;
}

/* A note sitting directly in a band is prose too, not a component: it belongs
   in the same column as the text it qualifies. */
section.band > .wrap > p.note {
  max-width: var(--prose);
  margin-inline: auto;
  text-align: justify;
}

/* Next meeting — the one time-critical item, so it gets the inverted band */
.band--feature {
  background: var(--feature-bg);
  color: var(--feature-ink);
  padding: 2.5rem 0 2.75rem;
  position: relative;
}

.band--feature a {
  color: var(--feature-ink);
}

/* Footer — inverted ground closes the page */
.site-footer {
  background: var(--foot-bg);
  color: var(--foot-ink-2);
  padding: 2.25rem 0 2.5rem;
  font-size: var(--t-sm);
  position: relative;
}

.site-footer a {
  color: var(--foot-ink);
}

.footer-name {
  margin: 0 0 0.9rem;
  max-width: var(--measure);
  color: var(--foot-ink);
  font-size: var(--t-xs);
  font-family: var(--f-mono);
  line-height: 1.6;
}

.footer-meta {
  margin: 0.3rem 0;
}

.footer-meta + .footer-meta {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--foot-rule);
}

.footer-gh {
  display: inline-flex;
  vertical-align: -0.2em;
  margin-left: 0.5rem;
  color: var(--foot-ink-2);
}

.footer-gh:hover, .footer-gh:focus-visible {
  color: var(--foot-ink);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
}
/* The hero is a surface band in all but name. */
.hero + section.band--surface {
  border-top: 1px solid var(--rule);
}

.band--feature + section.band {
  border-top: 0;
}

/* Prose. Zero-specificity defaults: they style unclassed markup inside a band
   and step aside for any component class without needing to be outranked. */
:where(section.band ul, section.band ol) {
  max-width: var(--measure);
  margin: 0 0 0.8rem;
  padding-left: 1.25rem;
}

:where(section.band li) {
  margin-bottom: 0.45rem;
}

:where(section.band li:last-child) {
  margin-bottom: 0;
}

:where(section.band h3) {
  margin: 0 0 0.3rem;
  font-family: var(--f-display);
  font-size: var(--t-md);
  font-weight: 700;
}

/* Labels. Set beside their subject in the margin at wide widths, and
   underneath it when there is no room — never stacked above as a heading. */
.kicker {
  margin: 0;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--wire);
  line-height: 1.4;
  text-transform: uppercase;
}

.event {
  display: grid;
  padding: 1.35rem 1.4rem;
}

/* The date is data, not display: it belongs to the technical register.
   Archivo drawn narrow and heavy, the way utility signage sets a number. */
.event .when {
  order: 1;
  margin: 0;
  font-family: var(--f-body);
  font-size: var(--t-2xl);
  font-weight: 700;
  font-stretch: 88%;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  line-height: 1.04;
  text-wrap: balance;
}

.event .kicker {
  order: 2;
  margin: 0.45rem 0 0;
}

.event-details {
  order: 3;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 0.25rem 1.15rem;
  margin: 1.1rem 0 0;
  font-size: var(--t-base);
}

/* Labels flush against the values, not ragged: the column is sized to the
   longest of them, so left-aligning leaves the shorter ones adrift. The row
   aligns on the baseline, so the smaller label sits on the value's line. */
.event-details dt {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  text-transform: uppercase;
  text-align: right;
  color: var(--ink-2);
  letter-spacing: 0.02em;
}

.event-details dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.event .note {
  order: 4;
  margin: 1.25rem 0 0;
  max-width: 42ch;
  color: var(--ink-2);
  font-size: var(--t-sm);
}

.event .more {
  order: 5;
  margin: 1rem 0 0;
  font-size: var(--t-sm);
}

/* On the inverted band the same component takes the band's own tints. */
.band--feature .kicker {
  color: var(--feature-warm);
}

.band--feature .event-details dt,
.band--feature .event .note {
  color: var(--feature-ink-2);
}

/* Wide: the label moves out into the margin column, and the note sits beside
   the date rather than below it. */
@media (min-width: 48rem) {
  .hero .wrap:has(.kicker) {
    align-items: start;
    grid-template-columns: var(--gutter) minmax(0, 1fr);
    column-gap: var(--gutter-gap);
  }
  .hero .wrap:has(.kicker) .kicker {
    grid-column: 1;
    grid-row: 1;
    margin: 0;
    padding-top: 0.4rem;
  }
  .hero .wrap:has(.kicker) h1 {
    grid-column: 2;
    grid-row: 1;
  }
  .hero .wrap:has(.kicker) .lede {
    grid-column: 2;
    grid-row: 2;
  }
  .hero .wrap:has(.kicker) .archive-meta {
    grid-column: 2;
    grid-row: 2;
  }
  /* One row per meeting instead of two stacked lines: the date moves out into
     the margin column and the venue/author sit right of the title. 32 entries
     of a short title left almost the whole row empty otherwise. */
  .event-list--archive li {
    display: grid;
    align-items: baseline;
    grid-template-columns: var(--gutter) minmax(0, 1.6fr) minmax(0, 1fr);
    column-gap: var(--gutter-gap);
  }
  .event-list--archive .when {
    grid-column: 1;
    grid-row: 1;
    margin-top: 0;
  }
  .event-list--archive .name {
    grid-column: 2;
    grid-row: 1;
  }
  .event-list--archive .where {
    grid-column: 3;
    grid-row: 1;
    margin-top: 0;
  }
  .event {
    align-items: start;
    grid-template-columns: var(--gutter) minmax(0, 1.15fr) minmax(0, 1fr);
    column-gap: var(--gutter-gap);
  }
  .event .kicker {
    grid-column: 1;
    grid-row: 1;
    margin: 0;
    padding-top: 0.45rem;
  }
  .event .when, .event-details {
    grid-column: 2;
  }
  .event .note, .event .more {
    grid-column: 3;
    margin-top: 0;
    padding-left: var(--gutter-gap);
    border-left: 1px solid var(--feature-rule);
  }
  .event .note {
    grid-row: 1;
    padding-top: 0.3rem;
  }
  .event .more {
    grid-row: 2;
    padding-bottom: 0.3rem;
  }
}
/* Research axes — numbered because the axes are numbered in the charter.
   Four ruled rows, sharing the label / subject / prose skeleton of .event. */
.axes {
  list-style: none;
  padding: 0;
  margin: 1.5rem 0 0;
  max-width: none;
  counter-reset: axis;
}

.axes > li {
  counter-increment: axis;
  margin: 0;
  position: relative;
  padding: 1.1rem 0;
  border-top: 1px solid var(--rule);
}

.axes > li:last-child {
  border-bottom: 1px solid var(--rule);
}

.axes > li::before {
  content: attr(data-label) " " counter(axis);
  display: block;
  margin-bottom: 0.4rem;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--wire);
}

.axes h3 {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-md);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.axes p {
  margin: 0.35rem 0 0;
  max-width: var(--measure);
  color: var(--ink-2);
  font-size: var(--t-sm);
}

@media (min-width: 48rem) {
  .axes > li {
    display: grid;
    align-items: start;
    grid-template-columns: var(--gutter) minmax(0, 1fr) minmax(0, 1.35fr);
    column-gap: var(--gutter-gap);
  }
  .axes > li::before {
    margin-bottom: 0;
    padding-top: 0.4rem;
  }
  .axes h3 {
    grid-column: 2;
    padding-top: 0.1rem;
  }
  .axes p {
    grid-column: 3;
    grid-row: 1;
    margin: 0;
    max-width: none;
  }
}
/* Cross-cutting axes — the .axes skeleton turned on its side. Not numbered:
   these run across the four axes rather than being a fifth and sixth one, so
   the only order is the reading order and there is no gutter label to carry a
   count. Stacked and ruled on narrow screens; four columns on wide, where the
   run drawn in _decorations takes the place of the rules. */
/* Specific enough to beat the band's blanket h2 margin. */
section.band .axes + h2 {
  margin-top: 2.75rem;
}

.cross-axes {
  list-style: none;
  padding: 0;
  margin: 1.5rem 0 0;
  max-width: none;
}

.cross-axes > li {
  margin: 0;
  position: relative;
  padding: 1.1rem 0;
  border-top: 1px solid var(--rule);
}

.cross-axes > li:last-child {
  border-bottom: 1px solid var(--rule);
}

.cross-axes h3 {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-base);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.cross-axes ul {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  max-width: none;
  color: var(--ink-2);
  font-size: var(--t-sm);
  line-height: 1.5;
}

.cross-axes ul li {
  margin: 0;
}

@media (min-width: 48rem) {
  .cross-axes {
    /* Narrower than --gutter-gap: four columns share the measure the axes
       list splits in two, so the gap has to give the words the room. */
    --cross-gap: 1.75rem;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: var(--cross-gap);
  }
  /* Centred on the node standing over the column, so the run reads as
     feeding each heading rather than passing above them. The items follow
     the heading — they are single words, and a centred title over a
     left-ragged list would sit off its own column. Only here: stacked, the
     rows are ruled prose and align left like everything else. */
  .cross-axes > li, .cross-axes > li:last-child {
    border: 0;
    padding: 1.6rem 0 0;
    text-align: center;
  }
}
/* Event listing */
.event-list {
  list-style: none;
  padding: 0;
  margin: 1.25rem 0 0;
  max-width: none;
}

.event-list li {
  margin: 0;
  padding: 1rem 0;
  border-top: 1px solid var(--rule);
}

.event-list li:last-child {
  border-bottom: 1px solid var(--rule);
}

.event-list .name {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-md);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.event-list .name a {
  text-decoration: none;
  color: var(--ink);
}

.event-list .name a:hover {
  text-decoration: underline;
}

.event-list .meta {
  margin: 0.3rem 0 0 1.1rem;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--ink-2);
  letter-spacing: 0.02em;
}

.event-list + .year {
  margin-top: 1.75rem;
}

/* Archive variant, for the SEMTL-era meetings. Same structure as the list
   above it, stepped down so the predecessor era reads as background: prose
   face instead of display, no node marker, lighter ink, tighter rows. There
   are 31 of them against a handful of SERIQ meetings, so without this the
   archive dominates the page it is a footnote to. */
.event-list--archive li {
  padding: 0.6rem 0;
}

.event-list--archive .name {
  font-family: var(--f-body);
  font-size: var(--t-sm);
  font-weight: 400;
  letter-spacing: 0;
}

.event-list--archive .name::before {
  display: none;
}

.event-list--archive .name a {
  color: var(--ink-2);
}

.event-list--archive .when,
.event-list--archive .where {
  margin: 0.2rem 0 0;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--ink-3);
  letter-spacing: 0.02em;
}

.event-list--archive .when {
  font-variant-numeric: tabular-nums;
}

/* SEMTL archive pages. The body is ported markdown, so it leans on the
   zero-specificity prose defaults above; only the hero and the provenance
   line need saying. Same step-down as the archive listing: this is a record
   of what happened, not a page competing with the current meeting. */
.hero--archive {
  padding: 2.25rem 0 1.75rem;
}

.hero--archive h1 {
  font-size: var(--t-xl);
}

.archive-meta {
  order: 3;
  margin: 0.55rem 0 0;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--ink-2);
  letter-spacing: 0.02em;
}

/* Ported bodies carry hand-written <img> with inline widths. */
section.band img {
  max-width: 100%;
  height: auto;
}

/* Markdown h3s run together without space above; the first still sits tight. */
:where(section.band h3) {
  margin-top: 1.4rem;
}

:where(section.band > h3:first-child) {
  margin-top: 0;
}

.archive-source {
  margin-top: 1.75rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

.year {
  margin: 1.25rem 0 0;
  font-family: var(--f-mono);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  letter-spacing: 0.02em;
  color: var(--wire);
}

/* Members. The social register: no rules and no corners, just soft panels.
   Entries are short — name plus institution — so they run in columns once
   there is room. */
.member-list {
  /* Whole pixels throughout, as in _decorations.scss: a hairline placed on a
     half pixel is drawn as two columns of half-strength blue, which reads as
     paler and fuzzier than the borders it meets. So the gutter, the padding and
     the portrait are px, and every wire is positioned off an edge at an integer
     offset — never centred with a percentage or a transform. */
  --card-gap: 14px;
  --card-pad: 8px;
  --card-pad-x: 10px;
  --node-r: 25px; /* half the 48px portrait plus its own 1px casing */
  /* A stub is positioned against the card's padding box, so reaching a node's
     centre from that edge means crossing the padding first. */
  --node-y: calc(var(--card-pad) + var(--node-r));
  --node-x: calc(var(--card-pad-x) + var(--node-r));
  /* Odd, so the 1px wire lands on the port's exact middle pixel. The stub then
     overruns by --port-off at each end, which centres a port on each card's
     1px border, and is --card-gap + --port + 1px long overall. */
  --port: 3px;
  --port-off: 1px;
  list-style: none;
  padding: 0;
  margin: 1.25rem 0 0;
  max-width: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--card-gap);
}

.member-list li {
  position: relative;
  margin: 0;
  padding: var(--card-pad) var(--card-pad-x);
  background: var(--warm-soft);
  border-radius: var(--r-soft);
  /* Wire encloses, amber marks: the panel is bracketed the way the wordmark and
     the home page's network are, so the register carries both site colours
     rather than reading as an unstructured amber block. */
  border: 1px solid var(--wire);
  /* Portraits pinned to the top rather than centred, so a node's centre is a
     fixed --node-r from the card's edge whatever the text below it does. That
     is what lets the wires meet the nodes on every card at a whole pixel. */
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

/* Wires across the gutters, so the register reads as a connected network rather
   than nine loose panels. Each card draws the wire on its own left and top —
   never its right or bottom — because "is there a card before me in this row"
   and "is there a row above me" are answered by first-column and first-row
   selectors, which hold for any number of members. Drawing rightwards instead
   would need last-row rules, and those depend on the count dividing evenly.
   The stubs overrun by 2px at each end to bridge both cards' 1px borders. */
.member-list li::before,
.member-list li::after {
  content: "";
  position: absolute;
  display: none;
}

/* Both stubs run node-to-node: they leave at the centre of this card's portrait
   and arrive at the centre of the neighbour's.

   A wire and a card border are both a 1px --wire stroke, so a wire that merely
   stopped against a border read as a line running into a wall. It ends in a
   port instead: a small square centred on the card edge it serves, in the wire's
   own blue and only three times its weight, which is enough to terminate the
   line without competing with the amber portraits. The
   stub therefore overruns by half a port at each end, which is why it is
   --card-gap + --port long.

   Three background layers, painted front to back: the two ports, then the wire
   beneath them, placed at an explicit --port-off so it runs through the middle
   of both. */
.member-list li::before,
.member-list li::after {
  background-repeat: no-repeat;
  background-image: linear-gradient(var(--wire), var(--wire)), linear-gradient(var(--wire), var(--wire)), linear-gradient(var(--wire), var(--wire));
}

.member-list li::before { /* westward, across a column gutter */
  top: calc(var(--node-y) - var(--port-off));
  right: calc(100% - var(--port-off));
  width: calc(var(--card-gap) + var(--port) + 1px);
  height: var(--port);
  background-size: var(--port) var(--port), var(--port) var(--port), 100% 1px;
  background-position: left center, right center, left top var(--port-off);
}

.member-list li::after { /* northward, across a row gutter */
  left: calc(var(--node-x) - var(--port-off));
  bottom: calc(100% - var(--port-off));
  width: var(--port);
  height: calc(var(--card-gap) + var(--port) + 1px);
  background-size: var(--port) var(--port), var(--port) var(--port), 1px 100%;
  background-position: center top, center bottom, left var(--port-off) top;
}

/* One column: no card sits beside another, so only the vertical wire runs. */
.member-list li:not(:first-child)::after {
  display: block;
}

.member-text {
  min-width: 0;
}

/* A portrait is read as a node: the amber ground of the home page's network,
   cased in the same hairline of ink that separates every node from its own.
   A photo covers the amber; an entry without one is left as a bare node. */
.member-list .portrait {
  /* px, not rem, for the reason set out at the top of this component: --node-r
     is half this number and the wires are positioned off it, and the root font
     size scales with the viewport (clamp(1rem, .8vw + .7rem, 1.5rem)). A 3rem
     portrait grew to 72px on a wide window while --node-r stayed at 25px, so
     every wire arrived 11px above the node it was supposed to meet. The img
     width/height attributes and the 144x144 sources both already say 48. */
  flex: none;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--node);
  border: 1px solid var(--node-line);
}

.portrait--empty {
  background: var(--node);
}

.member-list .name {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-md);
  /* Sits the name's cap-height beside the node's centre. */
  padding-top: 1px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.member-list .name a {
  text-decoration: none;
  color: var(--ink);
}

.member-list .name a:hover {
  text-decoration: underline;
}

/* Mono labels are blue everywhere else on the site — the kicker, the axis
   numerals, the event meta. This one was the exception. */
.member-list .meta {
  margin: 0.2rem 0 0;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--wire);
  letter-spacing: 0.02em;
}

.member-list .role {
  color: var(--warm-ink);
}

.member-axes {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.55rem 0 0;
  padding: 0;
  max-width: none;
}

.member-axes li {
  margin: 0;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.02em;
}

@media (min-width: 44rem) {
  .member-list {
    grid-template-columns: 1fr 1fr;
  }
  /* Two columns: wire west unless first in a row, north unless in the first. */
  .member-list li:not(:nth-child(2n+1))::before {
    display: block;
  }
  .member-list li:not(:nth-child(-n+2))::after {
    display: block;
  }
  .member-list li:nth-child(-n+2)::after {
    display: none;
  }
}
@media (min-width: 66rem) {
  .member-list {
    grid-template-columns: 1fr 1fr 1fr;
  }
  /* Hiding the first column needs a selector as specific as the two-column
     rule above, which :not() makes heavier than a bare li::before. */
  .member-list li:nth-child(3n+1)::before {
    display: none;
  }
  .member-list li:not(:nth-child(3n+1))::before {
    display: block;
  }
  .member-list li:not(:nth-child(-n+3))::after {
    display: block;
  }
  .member-list li:nth-child(-n+3)::after {
    display: none;
  }
}
/* Invited speakers. The same card, wire and portrait node as the committee
   register above — a speaker is another node on the same network, and the two
   registers should not read as two different components.

   What differs is the payload: a committee card is a name and an institution,
   a speaker card carries a talk title, an abstract and a bio. That prose needs
   a measure, so the list stays one column at every width instead of going to
   two and three, and only the vertical wire is ever drawn. The overrides carry
   both class names so they match the specificity of the multi-column rules
   above and win on source order.

   The committee register breaks out of the prose column because three columns
   of name-and-institution need the room. One column of abstract does not: left
   at the band's full width it sets ~130-character lines and starts out of line
   with the heading above it, so this one keeps the prose measure, the way
   .partner-logos does.

   A card with no title yet is a name, an affiliation and a portrait. That is
   the intended state while the programme is still being built, not a gap to be
   padded with placeholder text. */
.member-list.speaker-list {
  grid-template-columns: 1fr;
  max-width: var(--prose);
  margin-inline: auto;
  /* A speaker is the draw, so the portrait is given the room a committee
     thumbnail does not need. Sized in px rather than rem, as this component's
     opening comment asks: --node-r has to be exactly half the portrait for the
     wires between cards to leave and arrive at a node's centre, and the root
     font size scales with the viewport, so a rem portrait would drift away
     from a px wire as the window grows. */
  --node-r: 61px; /* half the 120px portrait plus its own 1px casing */
}

.member-list.speaker-list .portrait {
  width: 120px;
  height: 120px;
}

.member-list.speaker-list li::before {
  display: none;
}

.member-list.speaker-list li:not(:first-child)::after {
  display: block;
}

.member-list.speaker-list li:first-child::after {
  display: none;
}

/* Keynote tag. The chip vocabulary the axis labels already use, so it reads as
   part of the same system: mono, small, on the page's own surface colour, which
   is what separates it from the card's warm ground. It sits above the name
   rather than beside it, so a long affiliation cannot push it off the line. */
.speaker-tag {
  display: inline-block;
  margin: 0 0 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: var(--surface);
  color: var(--warm-ink);
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.02em;
}

.speaker-title {
  margin: 0.5rem 0 0;
  font-family: var(--f-display);
  font-size: var(--t-md);
}

/* The site sets talk titles in italic; the card's own cite carries that, so it
   does not need a second slant from the browser default. */
.speaker-title cite {
  font-style: italic;
}

.speaker-abstract, .speaker-bio {
  margin-top: 0.5rem;
}

.speaker-abstract p, .speaker-bio p {
  margin: 0.4rem 0 0;
  font-size: var(--t-sm);
  color: var(--ink-2);
  max-width: none; /* the card is already the measure */
}

/* A bio is context for the abstract above it, not a peer of it. */
.speaker-bio p {
  font-size: var(--t-xs);
}

/* Partner logos. Four official marks, each shown in its own colours at the
   size the row can carry — these are the institutions the Centre is made of,
   so they are on show rather than muted into a uniform grey row. Nothing here
   recolours a mark: every file is the official asset, unaltered.

   Each mark carries the name of the partner faculty beneath it, where there is
   one — see the `faculties` note in _data/institutions.yml. Those captions are
   wider than the marks they sit under, so the four cannot share a line any
   more; the set reads as a 2x2 block instead. */
.partner-logos {
  /* The band's prose column is `:not([class])`, so a classed component like
     this one is expected to place itself. Repeat the prose measure rather than
     break out of it: the block sits directly under the sentence that counts the
     four institutions and should read as part of it. Each mark and its caption
     are centred in their half of the measure, and the block is centred in the
     prose — the marks differ too much in width to line up on a common left
     edge without one of them looking adrift. */
  list-style: none;
  padding-left: 0;
  max-width: var(--prose);
  margin-inline: auto;
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem 1rem;
}

.partner-logos li {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.partner-logos a {
  display: flex;
  align-items: center;
  /* One box height for all four, set by the tallest mark, so the logos centre
     against a common line and every caption starts at the same height
     regardless of how deep its logo is. */
  min-height: 3.375rem;
  /* Shrink to the mark: the link would otherwise run the full column and the
     dark-mode hover ground would show as a slab reaching well past the logo.
     Being fit-content is also what lets the flex centring above place it. */
  width: fit-content;
  position: relative;
  isolation: isolate;
}

/* The ground behind a mark, inset outward so it reads as a pad around the logo
   without displacing it. Transparent on the light page, where the marks sit on
   paper already; it carries the light ground in dark mode. Isolated so the
   negative z-index stays inside the link and does not fall behind the section's
   own background. */
.partner-logos a::before {
  content: "";
  position: absolute;
  inset: -0.6rem;
  z-index: -1;
  border-radius: var(--r-soft);
  background: transparent;
  transition: background 0.18s ease;
}

.partner-logos .partner-logo {
  display: block;
  width: auto;
  height: var(--logo-h);
  opacity: 0.92;
  transition: opacity 0.18s ease;
}

.partner-logos a:hover .partner-logo,
.partner-logos a:focus-visible .partner-logo {
  opacity: 1;
}

/* Hover and focus draw a run under the mark. Every other link on the site
   underlines on hover; this is that same answer, in the ornament vocabulary of
   _sass/_decorations.scss — a run at its standard 2px weight, in utility blue,
   sitting on the pad's own bottom edge. It is in pixels for the same reason
   every other ornament is.

   A rule, not a figure: it grows from the centre outward, and there is nothing
   at its ends to close it into a shape around the logo.

   Set inside the pad rather than on its edge — flush with the edge, the dark
   mode's light ground curves away behind the ends of the rule and it reads as
   though it were clipped. */
.partner-logos a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.3rem;
  height: var(--run);
  background: var(--wire);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.18s ease-out;
}

.partner-logos a:hover::after,
.partner-logos a:focus-visible::after {
  transform: scaleX(1);
}

.partner-faculty {
  /* Same label treatment as .member-list .meta — mono, quiet, one step down.
     Centred on the mark it names. */
  margin: 0.85rem 0 0;
  text-align: center;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--ink-2);
  letter-spacing: 0.02em;
}

/* Stacked: McGill contributes two faculties under one mark. Balanced because
   the longest name is wider than its column and breaks mid-phrase otherwise. */
.partner-faculty span {
  display: block;
  text-wrap: balance;
}

@media (min-width: 34rem) {
  .partner-logos {
    grid-template-columns: 1fr 1fr;
  }
}
@media (prefers-color-scheme: dark) {
  /* Every one of these marks is dark ink on transparent, so on a dark page the
     colours need a light ground under them to read at all. The pad is on
     permanently rather than on hover: the marks are meant to be legible, and a
     ground that appears under the cursor would make the row flicker. Slightly
     off-white and a touch dimmed so four pale slabs do not outshout the page. */
  .partner-logos a::before {
    background: #f4f2ee;
  }
  .partner-logos .partner-logo {
    opacity: 0.88;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* The run still appears — it is the hover feedback, not decoration on top of
     it — but it arrives whole rather than growing. */
  .partner-logos a::before, .partner-logos .partner-logo {
    transition: none;
  }
  .partner-logos a::after {
    transition: none;
    transform: scaleX(1);
    opacity: 0;
  }
  .partner-logos a:hover::after,
  .partner-logos a:focus-visible::after {
    opacity: 1;
  }
}
/* Onward links: where the home page hands off to the rest of the site. A row
   on a wide screen, a stack on a narrow one — flex-wrap does both, and the
   row gap only applies once it has wrapped. Each item is marked with a node,
   drawn in _decorations.scss. */
.onward {
  list-style: none;
  padding: 0;
  margin: 1.5rem 0 0;
  max-width: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem 2rem;
  font-size: var(--t-sm);
}

.onward li {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

/* Primary action — registration and the like. The social register, so amber
   fill and a generous radius; the near-black label reads on the amber in both
   themes, where the theme's own ink would fail on one of them. */
.cta {
  display: inline-block;
  padding: 0.55rem 1.1rem;
  background: var(--warm);
  color: var(--warm-on);
  border-radius: var(--r-soft);
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-decoration: none;
}

/* Cased on hover the way a node is, rather than recoloured: the amber and its
   label already read on both grounds. */
.cta:hover {
  box-shadow: inset 0 0 0 2px var(--warm-on);
}

/* The inverted band colours its links with the band ink, which is a step more
   specific than .cta and would leave the label near-white on the amber. */
.band--feature .cta {
  color: var(--warm-on);
}

/* The action sits beside the details link rather than under it, so the two
   share a line while there is room for them. */
.event .more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.1rem;
}

/* Decorations — the one place the site draws a wire.

   The ornament vocabulary is three primitives: a RUN (a 2px line in utility
   blue), a NODE (a 6px amber box centred on a run), and a DROP (a short run
   at right angles, terminating one). Everything below is those three
   assembled differently. The header braid is the sole exception, and the
   sole ornament that repeats.

   Even widths throughout, so a node centred on a run lands on whole pixels.
   These rules load last, so a decoration wins any tie with its component. */
:root {
  --run: 2px; /* line weight of a wire */
  --node-size: 6px; /* the box marking a junction or a terminal */
  --drop: 6px; /* how far a run turns at right angles before it ends */
  /* The rhythm of a signal on a wire, kept in one place so every pulse on the
     site runs off the same clock: one leg of travel takes 30% of the period,
     the outward leg sets out at --pulse-out-delay and the returning one 2.7s
     behind it, and the wire rests for what is left of the cycle. */
  --pulse-period: 6s;
  --pulse-out-delay: 2.4s;
  --pulse-in-delay: 5.1s;
}

/* The header braid: two strands, wire blue and node amber, interlaced so
   neither is always on top. Woven in assets/img/braid.svg, not drawn here. */
.site-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--braid-size);
  background: var(--braid) repeat-x left center;
  opacity: 0.75;
}

/* The logo. Drawn in _includes/logo.html — the markup carries no commentary,
   so the reasoning lives here with the rest of the ornament grammar.

   The wordmark is Inria Serif 700 as outlines, not live text, so it cannot
   reflow or fall back to Georgia; the geometry is in font units, where the em
   is 1000, the cap line 680 and the baseline 0, so the frame is measured
   against the letters rather than guessed. Regenerate rather than hand-edit.

   The bus runs under the word at y=-540, clear of the Q's tail at -208. A
   riser stands at each end and two arms return inward along the top, leaving
   the middle open: a bracket, not a box. The left arm stops over the E; the
   right runs further, to the I, then drops to the node that ends it. Both
   terminals sit on a letter's stem centre (E at x=732, I at x=1966), which is
   why the drop looks aimed rather than approximate. Only the right arm dips —
   two dips would make it a pattern instead of a joke. Two taps hang below the
   bus under the S and the Q, the outer letters, so they brace it rather than
   cluster at its centre.

   The I is not the whole letter. It is clipped to its foot at y=84, where the
   bottom serif's bracket resolves into the full-width stem, and the stem is
   then drawn as a rect up to y=503 — 74% of the cap height, with no top serif.
   Nothing is scaled: the stem keeps the glyph's own 120-unit width, so the
   letter stays the weight and colour of its neighbours. Scaling the glyph
   instead thins it, and the I reads as a lighter, different face.

   The node sits half a run above the stem's end, which puts its top 73 units
   proud of the cap line. That is the value the sweep settled on: a full run of
   clearance floats it into a dead zone where it reads as neither aligned nor
   deliberately raised, and letting the stem run into the node drops the node's
   top below the cap line, so the word dips in the middle. The foot is what
   keeps this readable as an I rather than as a divider between R and Q — with
   both serifs gone the stem has nothing holding it to the baseline.

   Every L-joint overshoots by half a run. Drawing each member to the other's
   centreline instead leaves a half-run notch in all four corners.

   Runs are 71 units and nodes 214 — 2px and 6px, the ornament's real weights,
   at the 4rem header size set in _layout.scss. Scaled far below that the runs
   fall under a pixel and the figure silts up, which is why the favicon is a
   separate drawing.

   Coloured with the same two tokens as every other wire, so both themes
   follow. */
.logo__word, .logo__run {
  fill: var(--wire);
}

.logo__node {
  fill: var(--node);
}

/* The casing, on every node on the site. In user units, not a non-scaling
   stroke: the casing is a sixth of the node it encloses, and it should stay
   that fraction wherever the drawing is shown — including the raster copies,
   which are this same figure at 15x. 36 units is 1px at the 4rem header size,
   where the node itself is 6px. */
.logo__node rect {
  stroke: var(--node-line);
  stroke-width: 36;
}

/* The home title's wire network. Markup in _includes/wire-network.html.

   Not a divider: a bus with four branches tapped off it, ending in nodes, and
   a riser at each end that turns up past the title and returns inward along
   the top — so the title sits inside a bracket rather than on top of a line.
   Everything above the bus frames, everything below it branches. Right angles
   only, the way a trace is routed; no curves, no tapers.

       ──■                                    ■──
         │                                    │
         └──┬─────┬──────────┬──────────┬─────┘
            │     │          │          │
            ■     └────■     │          ■
                            ■

   The SVG carries the geometry; this carries the colour, so both themes
   follow the same tokens as every other wire on the site. */
.wire-net {
  display: block;
  /* Wider than the text: the frame sits out in the wrap's padding, so the
     risers clear the title's longest line. */
  width: calc(100% + 3rem);
  margin-inline: -1.5rem;
  max-width: none;
  height: auto;
  overflow: visible;
  /* Pull the frame up over the title. The SVG scales with its container, so
     the lift has to scale too — a percentage margin resolves against the
     container's width, which is the one length that tracks that scale.
     9% of the width is the 92 units between the riser tops and the bus. */
  margin-top: calc(1.15rem - 9%);
}

/* Narrow: the SVG scales with its container but the title does not, so the
   frame ends up small beside a title that has grown to four lines. Lift it
   further, until the risers again run alongside the last line rather than
   enclosing empty space, and drop the fan's third level — at this scale its
   runs are a few pixels apart and read as one smudge. */
@media (max-width: 47.99rem) {
  .wire-net {
    margin-top: calc(1.15rem - 16%);
  }
  .wire-net__detail {
    display: none;
  }
}
/* Child combinators throughout, so none of this reaches the cloned copy of
   the group inside a <use>: document rules do match into a use's shadow tree,
   but the clone's parent there is the <use>, not .wire-net. That is what lets
   the pulses inherit their own stroke and dash from the clone. */
.wire-net > .wire-net__run {
  stroke: var(--wire);
  stroke-width: 2;
}

.wire-net > .wire-net__node {
  fill: var(--node);
}

/* 1 unit against a 6-unit node: the same sixth, and 1px at the width the
   network is drawn for. Scaled down on narrow screens the casing thins with
   the node rather than swallowing it. */
.wire-net > .wire-net__node rect {
  stroke: var(--node-line);
  stroke-width: 1;
}

/* Drawing in. Everything grows out of the hub: the node lands first, the stem
   pushes up to the bus, and from there the frame and the two secondary buses
   run outward together, each branch starting as the bus it taps reaches it. A
   run strokes itself on from its own start point, which is why every d in the
   markup is written hub-outward. --i is an element's place in that order and
   --dur how long its run takes; symmetric pairs share both, so the two sides
   grow together. Plays once, on load. Restored below for anyone who has asked
   for less motion — the global kill switch stops the animation but would
   otherwise leave these elements parked at their hidden starting state. */
.wire-net > .wire-net__run > path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: wire-draw var(--dur) calc(0.2s + var(--i) * 0.11s) ease-out both;
}

.wire-net__node rect {
  opacity: 0;
  animation: node-on 0.3s calc(0.15s + var(--i) * 0.11s) ease-out both;
}

/* The signals. A short dash rides out along every wire from the hub, and a
   few seconds later another rides back in, then the network is still again.
   Both are the run group cloned by <use>, which inherits stroke and dash from
   the clone rather than from the group's own rules — so the same geometry
   carries the runs and the pulses without either being written twice.

   pathLength="1" normalises every run, so the dash is a fixed fraction of
   whatever it rides and every pulse sets out and arrives together. The gap has
   to be longer than the whole run — a gap that completes the period to exactly
   1 makes the pattern repeat once per path, and the dash is then always
   somewhere on the wire and can never travel off the end. With a long gap it
   parks past the end for the rest of the cycle, which is what makes the pulse
   a passing event rather than a loop. */
.wire-net__pulse {
  stroke: var(--pulse);
  stroke-width: 3.5;
  stroke-linecap: round;
  stroke-dasharray: 0.065 2;
  stroke-dashoffset: 0.065;
  /* A signal reads as brighter than the wire it rides, not merely wider: the
     glow is the same colour, so it lifts the dash off the run without adding
     a second one. */
  filter: drop-shadow(0 0 1.5px var(--pulse));
}

.wire-net__pulse--out {
  animation: pulse-out var(--pulse-period) var(--pulse-out-delay) ease-in-out infinite;
}

.wire-net__pulse--in {
  animation: pulse-in var(--pulse-period) var(--pulse-in-delay) ease-in-out infinite;
}

@keyframes pulse-out {
  0% {
    stroke-dashoffset: 0.065;
  }
  30% {
    stroke-dashoffset: -1;
  }
  100% {
    stroke-dashoffset: -1;
  }
}
@keyframes pulse-in {
  0% {
    stroke-dashoffset: -1;
  }
  30% {
    stroke-dashoffset: 0.065;
  }
  100% {
    stroke-dashoffset: 0.065;
  }
}
@keyframes wire-draw {
  to {
    stroke-dashoffset: 0;
  }
}
@keyframes node-on {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .wire-net > .wire-net__run > path {
    stroke-dashoffset: 0;
  }
  .wire-net__node rect {
    opacity: 1;
  }
  .wire-net__pulse, .axes::after, .cross-axes::after {
    display: none;
  }
}
/* Corner brackets on the meeting panel: a run along each arm, ending in a
   node centred on it. The frame is inset so nothing clips. */
.event {
  --arm: 20px;
  --box: var(--node-size);
  --inset: 4px;
  /* The cased node, centred on the same point as the node it encloses. */
  --boxc: calc(var(--box) + 2px);
  background-repeat: no-repeat;
  background-image: linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--amber), var(--amber)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line)), linear-gradient(var(--node-line), var(--node-line));
  background-size: var(--arm) var(--run), var(--run) var(--arm), var(--arm) var(--run), var(--run) var(--arm), var(--arm) var(--run), var(--run) var(--arm), var(--arm) var(--run), var(--run) var(--arm), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--box) var(--box), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc), var(--boxc) var(--boxc);
  background-position: left var(--inset) top var(--inset), left var(--inset) top var(--inset), right var(--inset) top var(--inset), right var(--inset) top var(--inset), left var(--inset) bottom var(--inset), left var(--inset) bottom var(--inset), right var(--inset) bottom var(--inset), right var(--inset) bottom var(--inset), left calc(var(--inset) + var(--arm) - var(--box) / 2) top calc(var(--inset) + 1px - var(--box) / 2), left calc(var(--inset) + 1px - var(--box) / 2) top calc(var(--inset) + var(--arm) - var(--box) / 2), right calc(var(--inset) + var(--arm) - var(--box) / 2) top calc(var(--inset) + 1px - var(--box) / 2), right calc(var(--inset) + 1px - var(--box) / 2) top calc(var(--inset) + var(--arm) - var(--box) / 2), left calc(var(--inset) + var(--arm) - var(--box) / 2) bottom calc(var(--inset) + 1px - var(--box) / 2), left calc(var(--inset) + 1px - var(--box) / 2) bottom calc(var(--inset) + var(--arm) - var(--box) / 2), right calc(var(--inset) + var(--arm) - var(--box) / 2) bottom calc(var(--inset) + 1px - var(--box) / 2), right calc(var(--inset) + 1px - var(--box) / 2) bottom calc(var(--inset) + var(--arm) - var(--box) / 2), left calc(var(--inset) + var(--arm) - var(--boxc) / 2) top calc(var(--inset) + 1px - var(--boxc) / 2), left calc(var(--inset) + 1px - var(--boxc) / 2) top calc(var(--inset) + var(--arm) - var(--boxc) / 2), right calc(var(--inset) + var(--arm) - var(--boxc) / 2) top calc(var(--inset) + 1px - var(--boxc) / 2), right calc(var(--inset) + 1px - var(--boxc) / 2) top calc(var(--inset) + var(--arm) - var(--boxc) / 2), left calc(var(--inset) + var(--arm) - var(--boxc) / 2) bottom calc(var(--inset) + 1px - var(--boxc) / 2), left calc(var(--inset) + 1px - var(--boxc) / 2) bottom calc(var(--inset) + var(--arm) - var(--boxc) / 2), right calc(var(--inset) + var(--arm) - var(--boxc) / 2) bottom calc(var(--inset) + 1px - var(--boxc) / 2), right calc(var(--inset) + 1px - var(--boxc) / 2) bottom calc(var(--inset) + var(--arm) - var(--boxc) / 2);
}

/* One run down the axes gutter, with a node on each row and a signal riding
   it. Both the run and the signal are drawn once, on the list, and both stop
   dead on the terminal nodes: the rows are laid out as four equal ones here,
   so the closing row's height is a quarter of the list and the distance from
   the list's bottom to the last node can be written down. Sized to content it
   could not be — the run would have to be reassembled row by row, and the
   signal, which has to cross all four in one motion, could not be written at
   all.

   Every x comes off --wire-x, so the run, the signal and the nodes are centred
   on each other by construction. The nodes sit level with the middle of their
   row's first line — the label in the gutter and the heading beside it share
   that line. Rows come after the run in tree order, so a node paints in front
   of it. */
@media (min-width: 48rem) {
  .axes {
    position: relative;
    display: grid;
    grid-auto-rows: 1fr;
    --wire-x: calc(var(--gutter) + var(--gutter-gap) / 2);
    /* 1.1rem of row padding, .4rem above the label, half a label line. */
    --node-y: 2.125rem;
    /* The signal rides wider than the run it travels, as it does on the home
       network, so it reads as something passing over the wire. */
    --pulse-w: 4px;
    --pulse-len: 3rem;
  }
  /* Node to node: down from the first row's node, and up from the last row's,
     which sits --node-y into a row a quarter of the list tall. */
  .axes::before, .axes::after {
    content: "";
    position: absolute;
    top: var(--node-y);
    bottom: calc(25% - var(--node-y));
  }
  .axes::before {
    left: calc(var(--wire-x) - var(--run) / 2);
    width: var(--run);
    background: var(--wire);
    opacity: 0.55;
  }
  .axes > li::after {
    content: "";
    position: absolute;
    left: calc(var(--wire-x) - var(--node-size) / 2);
    top: calc(var(--node-y) - var(--node-size) / 2);
    width: var(--node-size);
    height: var(--node-size);
    /* Opaque, so the run stops behind it rather than showing through and
       striping it — the run is the muted one here, the node is the mark. */
    background: var(--node);
    box-shadow: 0 0 0 1px var(--node-line);
  }
  /* The signal, the same one the home network carries and on the same clock: a
     soft dash of --pulse rides down the run and, a beat later, back up it. It
     is a gradient rather than a dash on a stroke because this run is a column,
     not a path — sized to a fixed length, parked clear of the run at either
     end, and moved by background-position, which resolves 100% as the far node
     however tall the list turns out to be. Its ends are transparent, so it
     rises out of the node it sets off from and sinks into the one it reaches
     rather than arriving whole. */
  .axes::after {
    left: calc(var(--wire-x) - var(--pulse-w) / 2);
    width: var(--pulse-w);
    background: linear-gradient(transparent, var(--pulse), transparent) no-repeat 0 calc(var(--pulse-len) * -1)/100% var(--pulse-len);
    animation: axis-pulse calc(var(--pulse-period) * 2) var(--pulse-out-delay) ease-in-out infinite;
  }
}
/* Twice the period, so the run rests longer than the network's does — this one
   sits beside prose that is being read. The stops are the shared rhythm inside
   that cycle: 1.8s down, the return setting out 2.7s after the first leg, 1.8s
   back, then still. */
@keyframes axis-pulse {
  0% {
    background-position: 0 calc(var(--pulse-len) * -1);
  }
  15%, 22.5% {
    background-position: 0 calc(100% + var(--pulse-len));
  }
  37.5%, 100% {
    background-position: 0 calc(var(--pulse-len) * -1);
  }
}
/* The same run again, turned on its side and laid across the top of the
   cross-cutting columns — one line above four headings, a node standing over
   each, and the same signal crossing it. Horizontal, so it does the work the
   column rules would otherwise do.

       ■────────■────────■────────■

   Node to node as before: the run stops on the outer two rather than carrying
   on to the wrap's edges. Four equal columns with three gaps between them, so
   a column is (100% - 3 gaps) / 4 wide and the first node's centre sits half
   of that in from the left — the same distance the last one sits in from the
   right, which is why one length serves both ends. */
@media (min-width: 48rem) {
  .cross-axes {
    position: relative;
    --node-x: calc((100% - 3 * var(--cross-gap)) / 8);
    /* Half of the column's 1.6rem of top padding: the run sits midway between
       the section's prose and the headings it marks. */
    --node-y: .8rem;
    --pulse-h: 4px;
    --pulse-len: 3rem;
  }
  .cross-axes::before, .cross-axes::after {
    content: "";
    position: absolute;
    left: var(--node-x);
    right: var(--node-x);
  }
  .cross-axes::before {
    top: calc(var(--node-y) - var(--run) / 2);
    height: var(--run);
    background: var(--wire);
    opacity: 0.55;
  }
  /* Columns come after the run in tree order, so a node paints in front of
     it, opaque, the way the rows' nodes do on the vertical run. */
  .cross-axes > li::before {
    content: "";
    position: absolute;
    left: calc(50% - var(--node-size) / 2);
    top: calc(var(--node-y) - var(--node-size) / 2);
    width: var(--node-size);
    height: var(--node-size);
    background: var(--node);
    box-shadow: 0 0 0 1px var(--node-line);
  }
  .cross-axes::after {
    top: calc(var(--node-y) - var(--pulse-h) / 2);
    height: var(--pulse-h);
    background: linear-gradient(90deg, transparent, var(--pulse), transparent) no-repeat calc(var(--pulse-len) * -1) 0/var(--pulse-len) 100%;
    animation: cross-pulse calc(var(--pulse-period) * 2) var(--pulse-out-delay) ease-in-out infinite;
  }
}
/* The vertical run's clock and stops, read left to right. */
@keyframes cross-pulse {
  0% {
    background-position: calc(var(--pulse-len) * -1) 0;
  }
  15%, 22.5% {
    background-position: calc(100% + var(--pulse-len)) 0;
  }
  37.5%, 100% {
    background-position: calc(var(--pulse-len) * -1) 0;
  }
}
/* A bare node, marking each meeting in a listing. */
.event-list .name::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 0.6rem;
  vertical-align: 0.16em;
  background: var(--node);
  box-shadow: 0 0 0 1px var(--node-line);
}

/* A node terminating each onward link, the same 6px box that ends every
   branch of the home network above it. Baseline-aligned text sits the box
   slightly high, so it is nudged down onto the line of the type. */
.onward li::before {
  content: "";
  flex: none;
  width: var(--node-size);
  height: var(--node-size);
  background: var(--node);
  transform: translateY(0.05em);
  box-shadow: 0 0 0 1px var(--node-line);
}

/*# sourceMappingURL=main.css.map */