/* ==========================================================================
   0. FONTS - self-hosted

   The three typefaces ship with the site instead of loading from Google's
   servers. That removes a third-party request, makes the site render a
   little sooner, and means the site keeps working if Google Fonts is
   blocked or unreachable. The .woff2 files live in assets/fonts/.

   Latin subset only - covers English plus the Norwegian characters the
   site uses (oe, ae, e-acute in Karlstrom, Gronningsaeter, Gaustadalleen).
   ========================================================================== */

@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/newsreader-400-italic.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: 'Newsreader';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../assets/fonts/newsreader-600.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 Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/plex-sans-400.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 Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../assets/fonts/plex-sans-600.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/plex-mono-400.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/plex-mono-500.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;
}

/* ==========================================================================
   Oslo Alpha Therapy 2027 - shared stylesheet

   Structure of this file, top to bottom:
     1. Tokens      - all colours, fonts, sizes, spacing. Edit here.
     2. Reset       - browser normalisation
     3. Base        - default element styling
     4. Layout      - containers and grids
     5. Components  - masthead, contour, index, ledger, timeline, roster
     6. Responsive  - breakpoints
     7. Motion      - scroll reveal, respects reduced-motion

   EDITING NOTE: nearly every colour and size is defined once in section 1
   as a "custom property" (the --name: value lines). Change a value there
   and it updates everywhere. You should rarely need to edit below it.
   ========================================================================== */

/* ============================ 1. TOKENS ============================ */

:root {
  /* Palette, taken from the OAT logo mark. Every text colour below is
     verified against the background it sits on - see tools/contrast.py.
     Do not lighten the text colours.

     Four of these are the brand's own values, taken from the colour sheet
     in docs/brand/: #144584, #255FA9, #C2D6E9 and #E8EFF7. The mark's four
     circles are an alpha particle - two protons, two neutrons - and its
     blues are the source of everything here. The remaining three (raised,
     ink, body, muted) are text and surface tones derived to sit with them
     and verified for contrast. */
  --paper: #E8EFF7;        /* page background - brand palest blue */
  --raised: #F6F9FC;       /* raised surfaces */
  --ink: #0D2340;          /* headings */
  --body: #33455C;         /* body text */
  --muted: #4E6076;        /* captions, secondary text */
  --rule: #C2D6E9;         /* hairlines and contour strokes - decorative only */
  --navy: #144584;         /* accent, buttons, links - brand darkest blue */
  --navy-light: #255FA9;   /* hover and secondary accent - brand mid blue */

  /* Typefaces */
  --font-display: 'Newsreader', Georgia, 'Times New Roman', serif;
  --font-body: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

  /* Type scale. Fluid sizes grow with the viewport between the two bounds. */
  --step-hero: clamp(2.5rem, 1.55rem + 4.2vw, 4rem);      /* 40 -> 64px */
  --step-title: clamp(2rem, 1.5rem + 2.2vw, 2.75rem);     /* 32 -> 44px */
  --step-section: clamp(1.5rem, 1.3rem + 0.9vw, 1.875rem);/* 24 -> 30px */
  --step-lead: 1.1875rem;   /* 19px - intro paragraphs */
  --step-body: 1.0625rem;   /* 17px - default body */
  --step-small: 0.9375rem;  /* 15px */
  --step-label: 0.75rem;    /* 12px - mono labels, always uppercase + tracked */

  /* Spacing scale, 4px rhythm */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 1rem;
  --space-sm: 1.5rem;
  --space-md: 2.5rem;
  --space-lg: 4rem;
  --space-xl: 6rem;
  --space-2xl: 8rem;

  /* Layout */
  --shell: 1180px;   /* outer content width */
  --measure: 68ch;   /* readable line length for prose */
  --gutter: var(--space-sm);
}

/* ============================ 2. RESET ============================ */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--body);
  font-family: var(--font-body);
  font-size: var(--step-body);
  line-height: 1.65;
  font-weight: 400;
}

img, svg { max-width: 100%; display: block; }

ul, ol { margin: 0; padding: 0; list-style: none; }

figure { margin: 0; }

/* ============================ 3. BASE ============================ */

h1, h2, h3, h4 {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-display);
  color: var(--ink);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.015em;
}

h1 { font-size: var(--step-hero); }
h2 { font-size: var(--step-title); }
h3 { font-size: var(--step-section); }

p { margin: 0 0 var(--space-xs); max-width: var(--measure); }

a { color: var(--navy); text-underline-offset: 0.2em; }
a:hover { color: var(--navy-light); }

strong { font-weight: 600; color: var(--ink); }

address { font-style: normal; }

/* The mono "data" role: dates, deadlines, index numbers, labels.
   Always uppercase and letter-spaced - this is what makes the site
   read as surveyed rather than styled. */
.label {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}

.lead {
  font-size: var(--step-lead);
  color: var(--body);
}

/* A section heading below the page title. Needed because .label was doing
   two jobs - an eyebrow sitting above a heading, and on the contact page
   the heading itself, which left that page jumping from 64px straight to
   19px with its section headings as the smallest text on it. .label is an
   eyebrow again; this is a heading. */
.section-heading {
  font-size: var(--step-section);
  margin-bottom: var(--space-sm);
}

/* Marks unfinished content. Nothing uses it at the moment - every value on
   the site is real - but it is kept for the next thing that is announced
   before it is settled. */
.placeholder {
  font-family: var(--font-mono);
  font-size: 0.9em;
  /* !important: this colour is the whole point of the class - it must
     always beat whatever colour the surrounding component sets, so
     placeholders never look like real, confirmed content. Size is left
     alone on purpose so components can still control it. */
  color: var(--navy) !important;
  font-style: normal;
}

/* Visible focus ring on everything interactive. Do not remove. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-2xs) var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--step-label);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.skip-link:focus { left: 0; }

/* ============================ 4. LAYOUT ============================ */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--space-xl); }
.section--tight { padding-block: var(--space-lg); }

/* Asymmetric editorial split: wide prose column, narrower side column. */
.split {
  display: grid;
  grid-template-columns: 1.55fr 1fr;
  gap: var(--space-xl);
  align-items: start;
}

/* ========================= 5. COMPONENTS ========================= */

/* ---- Contour signature -------------------------------------------
   Thin terrain-style strokes, used in exactly three places:
   the hero field, section dividers, and programme day bands.
   Inline SVG, so there are no image files to manage. ---------------- */

.contour {
  display: block;
  width: 100%;
  color: var(--rule);   /* strokes use currentColor */
}

.contour path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* Hero field: sits behind the title, must never compete with text. */
.contour--field {
  position: absolute;
  inset: 0;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  /* Over the hero artwork the pale --rule stroke measures 1.1:1 and
     vanishes, so the field is drawn in navy instead and held back with
     opacity: survey ink traced onto a photograph of the city. This is the
     only place the contour changes colour, because it is the only place
     it sits on an image. */
  color: var(--navy);
  opacity: 0.34;
}

.contour--divider { height: 40px; margin-block: var(--space-md); }

.contour--band { height: 26px; margin-bottom: var(--space-sm); }

/* ---- Masthead ------------------------------------------------------
   Journal-style: wordmark, a mono issue-line, nav, then a hairline. --- */

.masthead {
  border-bottom: 1px solid var(--rule);
  background-color: var(--paper);
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
}

/* The lockup sits above the issue-line, both left-aligned. The lockup
   already contains the workshop's name, so there is no text wordmark here
   any more - only the mono issue-line underneath it. */
.masthead__brand {
  text-decoration: none;
  color: inherit;
  display: block;
}

/* Height-driven so the lockup keeps its own proportions whatever the
   artwork's width happens to be. Sized so the wordmark inside it stays
   readable rather than to fit a header height - the header follows the
   logo here, not the other way round. */
.masthead__logo {
  height: 3.2rem;
  width: auto;
}

.masthead__issue {
  display: block;
  margin-top: var(--space-3xs);
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--muted);
}

.masthead__nav ul {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.masthead__nav a {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--body);
  text-decoration: none;
  padding-block: var(--space-2xs);
  border-bottom: 1px solid transparent;
}

.masthead__nav a:hover { color: var(--ink); border-bottom-color: var(--rule); }

.masthead__nav a[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--navy);
}

.nav-toggle {
  display: none;   /* shown at the mobile breakpoint */
  width: 44px;
  height: 44px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--rule);
  cursor: pointer;
  align-items: center;
  justify-content: center;
}

.nav-toggle__bar,
.nav-toggle__bar::before,
.nav-toggle__bar::after {
  display: block;
  width: 18px;
  height: 1px;
  background-color: var(--ink);
  position: relative;
}

.nav-toggle__bar::before { content: ""; position: absolute; top: -6px; }
.nav-toggle__bar::after  { content: ""; position: absolute; top: 6px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bar { background-color: transparent; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bar::before {
  top: 0;
  transform: rotate(45deg);
  background-color: var(--ink);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar::after {
  top: 0;
  transform: rotate(-45deg);
  background-color: var(--ink);
}

/* ---- Running foot -------------------------------------------------- */

.runfoot {
  margin-top: var(--space-2xl);
  border-top: 1px solid var(--rule);
  background-color: var(--paper);
  padding-block: var(--space-lg);
}

.runfoot__inner {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: var(--space-md);
  align-items: start;
}

.runfoot__name {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--space-2xs);
  line-height: 1.2;
}

.runfoot p {
  font-size: var(--step-small);
  margin-bottom: var(--space-3xs);
  max-width: none;
}

.runfoot__meta { color: var(--muted); }

/* ---- Buttons -------------------------------------------------------
   Square, not pill. The direction has no rounded chrome. ------------- */

.button {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #FFFFFF;
  background-color: var(--navy);
  border: 1px solid var(--navy);
  padding: 0.95em 1.5em;
  text-decoration: none;
  cursor: pointer;
}

.button:hover { background-color: var(--navy-light); border-color: var(--navy-light); color: #FFFFFF; }

/* The secondary action. Its boundary is navy, not a hairline: over the hero
   artwork a --rule border measures 1.09:1 and the button stops looking like
   a button at all. WCAG asks for 3:1 on the edge of an interactive control;
   navy gives about 7:1 on the artwork and 8:1 on paper. */
.button--quiet {
  background-color: transparent;
  color: var(--navy);
  border-color: var(--navy);
}

.button--quiet:hover {
  background-color: transparent;
  color: var(--ink);
  border-color: var(--navy);
}

/* A placeholder call-to-action whose real URL does not exist yet.
   aria-disabled tells assistive tech it is not usable; pointer-events
   stops the click from jumping to the top of the page; tabindex="-1"
   keeps it out of the keyboard tab order (pointer-events alone does not
   stop Enter/Space activation). To activate the button later, replace
   href="#" with the real URL, remove aria-disabled="true", remove
   tabindex="-1", and delete the <p class="placeholder"> line beneath it. */
.button[aria-disabled="true"] {
  background-color: var(--muted);
  border-color: var(--muted);
  pointer-events: none;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

/* Two equal columns, for content that carries the same weight - the two
   committees, rather than a wide column and a narrow one. */
.split--even { grid-template-columns: 1fr 1fr; }

/* ---- Committee roster ----------------------------------------------
   An editorial list, not cards. Name and email sit side by side. ---- */

.roster { border-top: 1px solid var(--rule); margin-bottom: var(--space-lg); }

/* Names only. Individual addresses were removed on request - the workshop
   takes all enquiries on one address. */
.person {
  padding-block: var(--space-xs);
  border-bottom: 1px solid var(--rule);
}

.person__name {
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  line-height: 1.25;
}

/* A plain statement where a button will eventually go. It carries a date
   rather than an action, so it is set as text and not dressed as a
   control - nothing here is clickable yet. */
.notice {
  border-left: 2px solid var(--navy);
  padding-left: var(--space-sm);
  margin-top: var(--space-sm);
  color: var(--body);
}

/* The group photograph beside the host section. Aligned to the top of the
   column so its edge lines up with the eyebrow across from it, rather than
   floating in the middle of a taller text block. */
.group-photo {
  width: 100%;
  height: auto;
  align-self: start;
  /* When the split stacks on a phone or tablet, the photograph stops at a
     readable size instead of running the full width of the page - fourteen
     people rendered a metre wide is not a better photograph. */
  max-width: 520px;
}

/* ---- Venue view -----------------------------------------------------
   Full-bleed at the foot of the page, breaking out of the shell: a
   photograph of the building on the left, the map on the right. The two
   answer different questions - what it looks like, and where it is - so
   they get equal weight rather than one being decoration for the other. */

.venue-view {
  margin-top: var(--space-lg);
  border-top: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 1fr 1fr;
  line-height: 0;   /* removes the inline-frame descender gap */
}

.venue-view__photo,
.venue-view__map {
  display: block;
  width: 100%;
  height: 420px;
  border: 0;
}

.venue-view__photo {
  object-fit: cover;
  /* No filter here on purpose. The cool cast is baked into the .webp files
     instead, matching the treatment on the header artwork so the two
     photographs on the site read as a pair. Regenerating them from
     source-images/ is the way to change it, not a CSS filter. */
}

.venue-view__map {
  /* Muted slightly to sit with the page, but no contrast reduction: this is a
     map people navigate by, and OpenStreetMap encodes water, parks and roads
     largely in hue. Do not push saturate lower than this. */
  filter: saturate(0.65);
  border-left: 1px solid var(--rule);
}

/* ============================ 7. MOTION ============================
   Reveal styles are scoped to .js, so with JavaScript disabled every
   section renders visible and unanimated. Never hide content by
   default. ------------------------------------------------------- */

@media screen and (prefers-reduced-motion: no-preference) {
  /* Sections fade, and nothing slides. The rise-and-fade you see on most
     sites reads as a stock effect; here the page is meant to feel like it
     is being surveyed onto the paper, so movement is left to the lines. */
  .js [data-reveal] {
    opacity: 0;
    transition: opacity 600ms ease-out;
  }

  .js [data-reveal].is-visible {
    opacity: 1;
  }

  /* The one moving reveal: the rule under each session-theme row draws
     itself left to right, the same gesture as the hero contour. This is
     deliberately the only place it happens - see the contour note above. */
  .js .index-row::after {
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  .js .index-row.is-visible::after {
    transform: scaleX(1);
  }

  /* Hero contour draws itself in on load. */
  .js .contour--field path {
    stroke-dasharray: 1400; /* ~1208 units of actual path length, rounded up so the draw starts fully hidden */
    stroke-dashoffset: 1400;
    animation: contour-draw 2.4s ease-out forwards;
  }

  .js .contour--field path:nth-child(2) { animation-delay: 0.12s; }
  .js .contour--field path:nth-child(3) { animation-delay: 0.24s; }
  .js .contour--field path:nth-child(4) { animation-delay: 0.36s; }
  .js .contour--field path:nth-child(5) { animation-delay: 0.48s; }
  .js .contour--field path:nth-child(6) { animation-delay: 0.60s; }

  @keyframes contour-draw {
    to { stroke-dashoffset: 0; }
  }

  a, .button { transition: color 150ms ease-out, background-color 150ms ease-out, border-color 150ms ease-out; }
}

/* ---- Hero ---------------------------------------------------------- */

.hero {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-2xl) var(--space-xl);
  border-bottom: 1px solid var(--rule);
  /* Enough height that the artwork behind reads as a photograph rather
     than a sliver, without pushing the content below the fold. */
  min-height: clamp(420px, 52vw, 620px);
  display: flex;
  align-items: center;
}

/* Four layers, back to front: artwork, contour, a wash, then the words.

   The wash is the reason the words hold up. Measured contrast said the type
   cleared AA on the bare artwork, but a ratio assumes a flat background -
   over a photograph the same number reads much worse, because the
   letterforms are competing with window frames and roof edges behind them.
   So the artwork and the contour are held back on the left, where the text
   sits, and left at full strength on the right, where there is nothing to
   read. The asymmetry follows the layout rather than fighting it.

   The wash is not opaque anywhere: about 10% of the artwork still reads
   through behind the words, enough to feel like a photograph is there
   without the words having to fight it. Raise the percentages toward 1 to
   hide more of the artwork, lower them to show more - legibility has room
   in either direction, and even at 45% visible the weakest line still
   measured 6.7:1. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    rgb(232 239 247 / 0.90) 0%,
    rgb(232 239 247 / 0.86) 32%,
    rgb(232 239 247 / 0.63) 54%,
    rgb(232 239 247 / 0.26) 74%,
    rgb(232 239 247 / 0) 88%
  );
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 62%;   /* holds the waterfront, drops empty sky */
}

.hero .contour--field { z-index: 1; }

.hero__inner { position: relative; z-index: 3; width: 100%; }

.hero h1 { margin-bottom: var(--space-xs); max-width: 14ch; }

/* The heading is artwork, so it is sized by width rather than by the type
   scale. Its own aspect ratio does the rest; the reserved width/height on
   the img keeps the hero from jumping while it loads. */
.hero__lockup {
  max-width: none;
  margin-bottom: var(--space-sm);
}

.hero__lockup img {
  width: clamp(260px, 40vw, 470px);
  height: auto;
}

/* Everywhere else a .label is --muted, but --muted at 12px over the hero
   artwork measures 3.1:1 - below AA. Over a photograph the palest tone on
   the site is exactly the wrong one, so this line takes ink instead and
   comes out around 8:1. It also stops the one line carrying the where and
   when from being the faintest thing in the hero. */
.hero__locality {
  margin-bottom: var(--space-md);
  color: var(--ink);
}

.hero .lead { max-width: 54ch; }

/* ---- Session index ------------------------------------------------
   A contents page, not a card grid. Each row is numbered because the
   six themes are an enumerated set the reader scans by position. ----- */

.index-list { border-top: 1px solid var(--rule); }

.index-row {
  position: relative;
  display: grid;
  grid-template-columns: 4rem 1fr 1.1fr;
  gap: var(--space-sm);
  align-items: baseline;
  padding-block: var(--space-sm);
}

/* The rule under each row is a pseudo-element rather than a border so it
   can be drawn in from the left (see section 7). Without JavaScript it is
   simply always at full width. */
.index-row::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--rule);
}

.index-row__num {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  color: var(--navy);
  letter-spacing: 0.1em;
}

.index-row__title {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  margin: 0;
}

.index-row__desc {
  font-size: var(--step-small);
  color: var(--body);
  margin: 0;
  max-width: none;
}

/* ---- Key-dates ledger --------------------------------------------- */

.ledger { border-top: 1px solid var(--rule); }

.ledger__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-xs);
  padding-block: var(--space-2xs);
  border-bottom: 1px solid var(--rule);
}

.ledger__term { font-size: var(--step-small); color: var(--body); }

/* A qualifier under a ledger term, e.g. the date an early-bird rate runs
   until. Sits with its term rather than competing with it. */
.ledger__note {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  letter-spacing: 0.06em;
  color: var(--muted);
}

.ledger__value {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  color: var(--ink);
  text-align: right;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.ledger__row--key .ledger__term,
.ledger__row--key .ledger__value {
  color: var(--navy);
  font-weight: 600;
}

/* ---- Programme days ------------------------------------------------
   Each day opens with a contour band, so the three days read as a
   traverse. Sessions are topics rather than time slots - this is the
   preliminary outline. --------------------------------------------- */

.day { margin-bottom: var(--space-xl); }

.day__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  flex-wrap: wrap;
  margin-bottom: var(--space-sm);
}

.day__title { margin: 0; font-size: var(--step-title); }

.day__sessions { border-top: 1px solid var(--rule); }

/* A preliminary programme, so a session is a topic rather than a slot: no
   clock time, and mono tags saying what kind of session it is. Every
   session with an invited speaker also takes contributed abstracts, so
   both tags appear together where that applies. */
.session {
  padding-block: var(--space-sm);
  border-bottom: 1px solid var(--rule);
}

.session__name {
  font-size: var(--step-body);
  color: var(--ink);
  margin: 0;
  max-width: var(--measure);
}

.session__tags {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--navy);
  margin: var(--space-3xs) 0 0;
}

/* Meals and the social activity are part of what the workshop offers, so
   they stay in the list, but they are not scientific programme. */
.session--aside .session__name { color: var(--muted); }

/* Breaks and travel are not scientific programme. Setting them back a tone
   lets the sessions read first and gives a long day a shape - the contrast
   encodes something true about the content rather than decorating it.
   Shared meals and the social activity are deliberately NOT muted: they are
   part of what the workshop offers, not logistics around it. */
.session-row--aside /* ---- Timeline spine ------------------------------------------------
   Four stops from abstract deadline to workshop. The spine is a
   border on the container; each stop marks it with a small square. --- */

.timeline {
  border-left: 1px solid var(--rule);
  padding-left: var(--space-sm);
  margin-block: var(--space-md);
}

.stop { position: relative; padding-bottom: var(--space-sm); }

.stop::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-sm) - 4px);
  top: 0.45em;
  width: 7px;
  height: 7px;
  background-color: var(--rule);
}

.stop--key::before { background-color: var(--navy); }

.stop__date {
  font-family: var(--font-mono);
  font-size: var(--step-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--navy);
  display: block;
  margin-bottom: var(--space-3xs);
}

.stop__what { font-size: var(--step-small); color: var(--body); margin: 0; }


/* ========================== 6. RESPONSIVE ==========================
   Breakpoints: 1024 (tablet), 860 (small tablet), 720 (mobile nav),
   520 (small phone). Mobile-first would invert this file; the site is
   small enough that max-width overrides stay readable. --------------- */

/* The issue-line's longest segment competes with the nav well before the
   mobile breakpoint and wraps, making the header taller on a laptop than on
   a wide monitor. Trim it as soon as it stops fitting comfortably. */
@media (max-width: 900px) {
  .masthead__issue-long { display: none; }
}

@media (max-width: 1024px) {
  :root { --space-2xl: 6rem; --space-xl: 4.5rem; }
  .split { gap: var(--space-lg); }
  .index-row { grid-template-columns: 3rem 1fr 1fr; gap: var(--space-xs); }
}

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; gap: var(--space-lg); }
  .runfoot__inner { grid-template-columns: 1fr; gap: var(--space-sm); }
  .index-row { grid-template-columns: 2.5rem 1fr; }
  .index-row__desc { grid-column: 2; }
  /* Side by side, each panel gets too narrow to read; stack them so the
     photograph and the map each keep the full width. */
  .venue-view { grid-template-columns: 1fr; }
  .venue-view__map { border-left: 0; border-top: 1px solid var(--rule); }
}

@media (max-width: 720px) {
  :root { --space-2xl: 4.5rem; --space-xl: 3.5rem; --space-lg: 3rem; }

  /* ---- Masthead --------------------------------------------------
     On a phone the header must not eat the screen. Keeping the brand
     and the toggle on one row (no wrap) and dropping the longest part
     of the issue-line takes the header from ~185px to roughly 80px on
     a 390px-wide device. */
  .masthead__inner {
    position: relative;
    flex-wrap: nowrap;
    align-items: center;
    padding-block: var(--space-xs);
  }

  .masthead__logo { height: 2.6rem; }

  .js .nav-toggle {
    display: inline-flex;
    position: relative;
    z-index: 110;          /* stays tappable above the open overlay */
    flex-shrink: 0;
  }

  /* ---- Navigation as a full-screen panel --------------------------
     Not the desktop bar squeezed into a dropdown: on a phone the nav
     becomes its own screen, set in the display face at a size worth
     tapping. Still gated on .js, because only the script can reopen
     it - without JavaScript the nav stays in normal flow. */
  .js .masthead__nav {
    position: fixed;
    inset: 0;
    z-index: 100;
    background-color: var(--paper);
    padding: 6rem var(--gutter) var(--space-xl);
    overflow-y: auto;
    overscroll-behavior: contain;
    opacity: 0;
    visibility: hidden;
  }

  .js .masthead__nav.is-open { opacity: 1; visibility: visible; }

  .masthead__nav ul {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    border-top: 1px solid var(--rule);
  }

  .js .masthead__nav a {
    display: flex;
    align-items: center;
    min-height: 60px;      /* comfortably past the 44px touch minimum */
    width: 100%;
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 600;
    text-transform: none;
    letter-spacing: -0.015em;
    color: var(--ink);
    border-bottom: 1px solid var(--rule);
  }

  .js .masthead__nav a[aria-current="page"] {
    color: var(--navy);
    border-bottom-color: var(--rule);
  }

  /* ---- Hero ------------------------------------------------------
     Two side-by-side pill actions wrap into a ragged second row on a
     narrow screen. Full-width stacked reads as intended and gives each
     a full-width tap target. */
  .hero { padding-block: var(--space-xl) var(--space-lg); }
  .hero h1 { max-width: none; }

  /* Text runs the full width here, so a sideways fade would leave the end
     of every line sitting on bare photograph. Even wash instead. */
  .hero::after {
    background: linear-gradient(
      180deg,
      rgb(232 239 247 / 0.88) 0%,
      rgb(232 239 247 / 0.84) 62%,
      rgb(232 239 247 / 0.60) 100%
    );
  }

  .button-row { flex-direction: column; align-items: stretch; }
  .button { text-align: center; padding-block: 1.1em; }

  /* ---- Programme -------------------------------------------------
     A reserved 5rem time column costs a sixth of a phone screen to
     show "[--:--]". Stack instead: time as a small mono line above
     the session it belongs to. */
  @media (max-width: 520px) {
  .index-row { grid-template-columns: 1fr; gap: var(--space-3xs); }
  .index-row__desc { grid-column: 1; }

  /* The ledger's term and value stop fitting on one line. Stacked, the
     mono value reads as a caption under its label rather than as a
     cramped right-aligned column. */
  .ledger__row { flex-direction: column; align-items: flex-start; gap: var(--space-3xs); }
  .ledger__value { text-align: left; }
}
