.site-header {
  background-color: var(--color-primary);
  color: var(--color-on-dark);
}
.site-header__inner {
  max-width: var(--max-width-wide);
  margin-inline: auto;
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.brand {
  text-decoration: none;
  color: var(--color-on-dark);
  display: flex;
  flex-direction: column;
}
/* Without this the generic a:hover paints --color-accent-hover, a dark blue that
   is nearly invisible on navy. */
.brand:hover { color: var(--color-on-dark-muted); }
.brand:hover .brand__name,
.brand:hover .brand__meta { color: inherit; }
.brand__name {
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-lg);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
}
.brand__meta {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--color-on-dark);
}

/* Sticky bar. Sits outside .site-header so it survives the whole scroll while the
   tall brand row above it scrolls away. One line high at every width: the list
   scrolls sideways instead of wrapping. */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 40;
  background-color: var(--color-primary);
  border-bottom: 1px solid var(--color-border-on-dark);
  box-shadow: var(--shadow-md);
}
.site-nav__inner {
  max-width: var(--max-width-wide);
  margin-inline: auto;
  /* Bottom padding keeps the active-page underline off the bar's own border,
     so the two do not read as a pair of parallel lines. */
  padding: 0 var(--space-4) var(--space-3);
}
.site-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-5);
  overflow-x: auto;
  scrollbar-width: none;
}
.site-nav__list::-webkit-scrollbar { display: none; }
.site-nav__list li { margin: 0; flex: 0 0 auto; }
.site-nav__list a {
  color: var(--color-on-dark);
  text-decoration: none;
  font-weight: var(--font-weight-medium);
  padding: var(--space-3) 0 var(--space-2);
  display: inline-block;
  white-space: nowrap;
  border-bottom: 3px solid transparent;
}
.site-nav__list a:hover { color: var(--color-on-dark); text-decoration: underline; }
.site-nav__list a[aria-current="page"] {
  border-bottom-color: var(--color-on-dark-muted);
}

/* --- Overview submenu (2026-08-27) ------------------------------------------
   The client asked for ASD 2026's behaviour: on a desktop, hovering Overview drops a
   panel with its subpages. Two things that pattern gets wrong when copied literally,
   and that this does not.

   **The link and the caret are separate controls.** If Overview itself opened the
   menu, Overview would stop being reachable — a parent that is also a real page has
   to stay clickable. They are a link and a button, and a screen reader announces them
   as exactly that.

   **Hover is not an interface on a phone.** Hover opens the panel only where a fine
   pointer exists; everywhere else the caret is a real button. Both paths end in the
   same `data-open` state, so there is one thing to style and the two cannot disagree.

   THE TRAP, and the reason this block is longer than it looks like it should be:
   `.site-nav__list` above sets `overflow-x: auto`, which makes `overflow-y` compute to
   `auto` as well — so an absolutely positioned panel inside it is CLIPPED at the bar's
   own bottom edge and simply never appears. The fix is the `:has()` rule below, which
   lifts the clipping only while a panel is open. **Do not remove `overflow-x` to solve
   this instead**: it is what keeps the bar one line high at 320 px. */
/* No gap: "Overview ▾" has to read as one label, not as a word and a control near it.
   The small horizontal padding on the toggle below is the only separation, and the
   touch target is grown with a pseudo-element instead — see there. */
.nav-parent {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0;
}

/* Restores the scroll container to `visible` for exactly as long as a panel is open,
   which is the only moment the panel needs to escape it. Closed, the bar behaves
   exactly as it did before this block existed. */
.site-nav__list:has(.nav-parent[data-open="true"]) { overflow: visible; }

/* The box is only as wide as the caret plus a hair of padding, and it repeats the
   link's own vertical padding and transparent bottom border so the two sit on the
   same baseline and the active underline runs straight under both.

   This started as `min-width: 2.75rem` for the 44 px touch target and that was the
   wrong tool: a minimum width buys the target with LAYOUT, so the caret floated ~17 px
   away from Overview and pushed Program a further 2 rem out. **A touch target does not
   have to occupy space.** The ::after below grows the hit area without moving anything. */
.nav-parent__toggle {
  background: none;
  border: 0;
  border-bottom: 3px solid transparent;
  padding: var(--space-3) var(--space-1) var(--space-2);
  color: var(--color-on-dark);
  font: inherit;
  line-height: 1;
  cursor: pointer;
  position: relative;
}

/* The real hit area: ~38 × 50 px, invisible, and deliberately LOPSIDED. It grows up,
   down and to the right, and barely at all to the left — extending it leftwards would
   overlap the tail of "Overview" and steal taps from the link, turning a navigation
   into a menu toggle. To the right it only eats into the gap before Program, which is
   empty anyway. */
.nav-parent__toggle::after {
  content: "";
  position: absolute;
  top: -0.35rem;
  bottom: -0.35rem;
  left: -0.15rem;
  right: -1rem;
}
.nav-parent__caret {
  display: inline-block;
  font-size: var(--font-size-sm);
  transition: transform var(--duration-base) var(--ease-standard);
}
.nav-parent[data-open="true"] .nav-parent__caret { transform: rotate(180deg); }

/* Navy on navy with a border rather than a lighter fill: the panel should read as the
   bar extending downwards, not as a card floating over the hero. */
.nav-sub {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 50;
  min-width: 12rem;
  margin: 0;
  padding: var(--space-2) 0;
  list-style: none;
  background-color: var(--color-primary);
  border: 1px solid var(--color-border-on-dark);
  border-top: 0;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  box-shadow: var(--shadow-md);
}
.nav-parent[data-open="true"] .nav-sub { display: block; }
.nav-sub li { margin: 0; }
.nav-sub a {
  display: block;
  padding: var(--space-2) var(--space-4);
  white-space: nowrap;
  color: var(--color-on-dark);
  text-decoration: none;
  border-bottom: 0;
}
.nav-sub a:hover { background-color: var(--color-primary-hover); text-decoration: underline; }
.nav-sub a[aria-current="page"] {
  background-color: var(--color-primary-hover);
  font-weight: var(--font-weight-semibold);
}

/* Hover only where hovering is possible. `focus-within` gives the same opening to the
   keyboard, so tabbing into the panel does not close it behind you. */
@media (hover: hover) and (pointer: fine) {
  .nav-parent:hover .nav-sub,
  .nav-parent:focus-within .nav-sub { display: block; }
  .nav-parent:hover .nav-parent__caret,
  .nav-parent:focus-within .nav-parent__caret { transform: rotate(180deg); }
  .site-nav__list:has(.nav-parent:hover),
  .site-nav__list:has(.nav-parent:focus-within) { overflow: visible; }
}

/* The active underline runs under the caret as well as the word, because after the
   gap was closed the two read as one label and an underline stopping short of the
   caret looks like a rendering fault.

   Second selector: on /committee the current page is a CHILD, so Overview itself
   carries no `aria-current` and the parent would show no mark at all. */
.nav-parent:has(> a[aria-current="page"]) .nav-parent__toggle,
.nav-parent:has(.nav-sub a[aria-current="page"]) > a,
.nav-parent:has(.nav-sub a[aria-current="page"]) .nav-parent__toggle {
  border-bottom-color: var(--color-on-dark-muted);
}

.hero {
  position: relative;
  background-color: var(--color-primary);
  background-image: var(--gradient-hero);
  color: var(--color-on-dark);

  padding: var(--space-4);
  overflow: hidden;

  /* Band, not a box: no side borders at any width, so mobile shows only the pattern.
     No top border either since the sticky nav's own border already draws that edge. */
  border: 0;
  border-bottom: 3px solid var(--color-accent-soft);
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--hero-hex-pattern);
  pointer-events: none;
}
.hero__inner {
  position: relative;
  max-width: 96rem;
  margin-inline: auto;
  display: flex;
  justify-content: center;
}
.hero__inner picture { display: contents; }

.hero__banner {
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

@media (min-width: 48rem) {
  .hero { padding: var(--space-4) var(--space-2); }
  .hero__banner { width: min(96rem, 99%); }
}
@media (min-width: 64rem) {
  .hero__banner { width: min(42rem, 62%); }
}

.section { padding: var(--space-7) 0; }
.section--sunken { background-color: var(--color-surface-sunken); }
.section--strip { padding: var(--space-6) 0; }

.heading--accent { position: relative; padding-bottom: var(--space-3); }
.heading--accent::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--space-7);
  height: 3px;
  border-radius: var(--radius-pill);
  background-color: var(--color-accent-soft);
}
.heading--on-dark { color: var(--color-on-dark); }

.display-date {
  font-family: var(--font-mono);
  font-size: var(--font-size-3xl);
  line-height: var(--line-tight);
  color: var(--color-accent-soft);
  margin: 0 0 var(--space-4);
}

.card-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
.card {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm);
  transition:
    transform var(--duration-base) var(--ease-standard),
    box-shadow var(--duration-base) var(--ease-standard);
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.card h3 { margin-top: 0; }
.card p:last-child { margin-bottom: 0; }

@media (min-width: 48rem) {
  .card-grid--2 { grid-template-columns: repeat(2, 1fr); }
  .card-grid--3 { grid-template-columns: repeat(3, 1fr); }
}

.btn {
  display: inline-block;
  font-family: var(--font-sans);
  font-weight: var(--font-weight-semibold);
  text-decoration: none;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-md);
  background-color: var(--color-accent);
  color: var(--color-on-dark);
  transition: background-color var(--duration-base) var(--ease-standard);
}
.btn:hover { background-color: var(--color-accent-hover); color: var(--color-on-dark); }

.site-footer {
  background-color: var(--color-primary);
  color: var(--color-on-dark);
  padding: var(--space-7) var(--space-4);
}
.site-footer__inner { max-width: var(--max-width); margin-inline: auto; }

.site-footer__cols {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
  padding-bottom: var(--space-6);
  margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--color-border-on-dark);
}
.site-footer__col h2 {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-on-dark-muted);
  margin: 0 0 var(--space-4);
}
.site-footer__col p { max-width: none; margin: 0 0 var(--space-3); }
.site-footer__col p:last-child { margin-bottom: 0; }
.site-footer__col ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.site-footer__col li { margin: 0 0 var(--space-2); }
.site-footer__col li:last-child { margin-bottom: 0; }

.footer-date { margin: 0 0 var(--space-3); }
.footer-date__label {
  display: block;
  font-size: var(--font-size-sm);
  color: var(--color-on-dark-muted);
}
.footer-date__value { display: block; font-weight: var(--font-weight-semibold); }

@media (min-width: 48rem) {
  .site-footer__cols { grid-template-columns: repeat(3, 1fr); gap: var(--space-7); }
}
.site-footer__brand {
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: var(--font-size-md);
}
.site-footer a { color: var(--color-on-dark); }
.site-footer a:hover { color: var(--color-on-dark-muted); }
.site-footer__avs { display: inline-block; line-height: 0; }
.site-footer__logo { height: 56px; width: auto; }
.site-footer__nav ul {
  list-style: none;
  padding: 0;
  margin: var(--space-4) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
}
.site-footer__nav li { margin: 0; }
.site-footer__fine {
  color: var(--color-on-dark);
  font-size: var(--font-size-sm);
  margin: var(--space-5) 0 0;
}
.site-footer__sponsor { margin-top: var(--space-4); }

.site-header__inner { flex-direction: column; align-items: stretch; }

/* `flex-wrap: wrap` here is what put the AVS lockup on its own line below about
   26rem: "Area Selective Deposition Workshop" is 34 characters and refuses to
   shrink, so the row overflowed and the logo dropped, roughly doubling the header's
   height on the screen that can least afford it (2026-08-27).
   `nowrap` alone is not enough — a flex item will not shrink below its content's
   min-content width unless it is told it may, which is what the `min-width: 0` pair
   below does. With those, the brand takes what is left and the tagline ellipses to
   "Area Se…", which is the trade the client asked for: both on one line. */
.site-header__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: nowrap;
  gap: var(--space-4);
}
.brand { min-width: 0; }
.brand__meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.brand__text { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.site-header__sponsor-link {
  flex-shrink: 0;
  display: inline-block;
  line-height: 0;
  border-radius: var(--radius-sm);
}

.site-header__sponsor {
  flex-shrink: 0;
  background-color: var(--color-surface);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
}

/* Phone: the header is pure chrome above the fold, so it gives back what it can.
   The logo drops from 140 to 100 px and the padding tightens by one step; together
   with the nowrap row above, the header goes from two stacked lines to one.
   `height: auto` is load-bearing — the <img> carries width/height attributes for
   layout reservation, and overriding only the width would squash it. */
@media (max-width: 30rem) {
  .site-header__inner { padding: var(--space-2) var(--space-3); }
  .site-header__top { gap: var(--space-3); }
  .site-header__sponsor {
    width: 6.25rem;
    height: auto;
    padding: var(--space-1) var(--space-2);
  }
}

@media (min-width: 48rem) {
  .split-8-4 { grid-template-columns: 2fr 1fr; align-items: start; }
}

.venue__address { white-space: nowrap; }
.venue__figure { margin: var(--space-4) 0 0; }
.venue__photo {
  width: 100%;
  max-width: 19rem;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  object-fit: cover;
}

.body-text {
  line-height: var(--line-base);
  text-align: left;
  text-wrap: pretty;
  overflow-wrap: break-word;
}
@media (min-width: 48rem) {
  .body-text {
    text-align: justify;
    hyphens: auto;
    hyphenate-limit-chars: 10 4 4;
    -webkit-hyphenate-limit-before: 4;
    -webkit-hyphenate-limit-after: 4;
  }
}

.person {
  text-align: center;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-5);
  box-shadow: var(--shadow-sm);
  transition:
    transform var(--duration-base) var(--ease-standard),
    box-shadow var(--duration-base) var(--ease-standard);
}
.person:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.person__role { font-size: var(--font-size-xl); }
.photo-placeholder {
  width: 150px;
  aspect-ratio: 1 / 1;
  margin: 0 auto var(--space-4);
  background-color: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.person__photo {
  width: 9.375rem;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  margin: 0 auto var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.person__name {
  font-weight: var(--font-weight-semibold);
  margin: 0 auto var(--space-1);
  max-width: none;
}
.person__affil {
  color: var(--color-muted);
  margin: 0 auto;
  max-width: none;
}

.chairs--plain .person {
  background: none;
  border: 0;
  box-shadow: none;
  padding: var(--space-4) 0;
}
.chairs--plain .person:hover { transform: none; box-shadow: none; }
.chairs--plain .person__photo {
  width: 10.5rem;
  border-color: transparent;
  border-radius: var(--radius-pill);
}

.section--primary { background-color: var(--color-primary); }
.section--primary h2 { color: var(--color-on-dark); }

.key-dates {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
}
.date-card {
  margin: 0;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background-color: var(--color-secondary);
  border: 1px solid var(--color-border-on-dark);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.date-card__label {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-size: var(--font-size-sm);
  color: var(--color-on-dark);
}
.date-card__value {
  font-family: var(--font-display);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-tight);
  color: var(--color-on-dark);
}

@media (min-width: 48rem) {
  .key-dates { grid-template-columns: repeat(3, 1fr); }
}

.sponsor-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-4);
  min-height: 7.5rem;
  box-shadow: var(--shadow-sm);
  transition:
    transform var(--duration-base) var(--ease-standard),
    box-shadow var(--duration-base) var(--ease-standard);
}
.sponsor-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.sponsor-tile img {
  width: auto;
  max-height: 4rem;
  max-width: 100%;
  object-fit: contain;
}

/* A near-square logo is height-bound the way a wide one is width-bound, so at the same
   max-height it renders at a fraction of the others' area: RASIRC's 1.39:1 lockup gives
   89×64 px against American Elements' 258×64, about a third. At 5rem it renders 111×80,
   8,872 px² — about 88% of ADEKA's area, which is the smallest of the wide logos, so it
   stops reading as the odd one out without overtaking anything.

   **The padding pays for the extra height, never the row.** --space-3 in place of
   --space-5 leaves exactly 96 px of content box inside the tile's unchanged 7.5rem
   (120 px less 2×12 px), so the logo fits without the tile growing and every card on the
   page stays the same height.

   **6rem is the ceiling, and it is a hard one.** 100 px was asked for and does not fit:
   it needs 10 px padding, which is not on the spacing scale, or a 124 px tile, which
   reopens the uneven row this construction exists to avoid. 96 px is the largest value
   that lands on a token, and the 4 px lost are invisible.

   This is now within a whisker of area parity — 133×96 = 12,776 px², against Lam's
   12,032 and a ~102 px full-parity figure. **That is the far edge**: Tania's read of
   ASD 2026, where this same logo runs unframed, is that parity looks oversized next to
   wide wordmarks. Do not raise it again without her looking at it.

   Named for the artwork, not for RASIRC, and on the tile rather than the img because the
   exception belongs to the pair: the next square logo reuses the whole thing. */
.sponsor-tile--square { padding: var(--space-3); }
.sponsor-tile--square img { max-height: 6rem; }

/* Sponsor rail — the compact form used in the right column of /program, mirroring
   ASD 2026 where sponsors sit in the sidebar rather than across the page. Two per row
   at every width, smaller tiles. The sponsors page keeps the full-width 2→3→4 grid:
   there the logos are the content, here they are an aside. */
.sponsor-rail .sponsor-grid { grid-template-columns: repeat(2, 1fr); }
.sponsor-rail .sponsor-tier { margin-bottom: var(--space-6); }
.sponsor-rail .sponsor-tile { min-height: 4.5rem; padding: var(--space-3); }
.sponsor-rail .sponsor-tile img { max-height: 2.25rem; }
/* Same exception, rail scale — but the rail is a different problem wearing the same face,
   and it needs its own numbers rather than the sponsors page's.

   **In the rail, width binds, not height.** The column is 17rem and each tile is ~128 px,
   so Lam, Dockweiler and EMD all hit max-width and render at the same ~104 px regardless
   of their nominal height. A row of equal widths is what the eye reads, so RASIRC at 67 px
   looked small there even though its *area* already matched ADEKA's. Matching area is the
   wrong target in this column; matching width is the right one.

   4rem gives 89 px wide, which is exactly ADEKA's width — the one wide logo that is not
   width-bound here, so it is the honest thing to line up with. Paying for it needs
   --space-1: 64 + 2×4 = 72 px, the tile's full 4.5rem, so **no rail tile and no rail row
   changes size**. Rodrigo compared this against a taller tile and against 3.5rem side by
   side before choosing; 4 px of padding is tight against the border and that was the
   accepted cost.

   This rule and the padding below it are **two classes each, the same specificity as
   `.sponsor-rail .sponsor-tile` above, so they win on source order alone.** Keep them
   after it. Move them above and the rail silently reverts to --space-3, 64 px stops
   fitting in 48, and every rail row grows. */
.sponsor-rail .sponsor-tile--square { padding: var(--space-1); }
.sponsor-rail .sponsor-tile--square img { max-height: 4rem; }
.sponsor-rail h2 { font-size: var(--font-size-lg); }

.sponsor-tier { margin-bottom: var(--space-7); }
.sponsor-tier:last-child { margin-bottom: 0; }
.sponsor-tier__name {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  margin: 0 0 var(--space-4);
}
.sponsor-tier__empty { color: var(--color-muted); font-style: italic; }

.sponsor-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(2, 1fr);
}
.sponsor-grid li { margin: 0; }
.sponsor-grid a { display: block; text-decoration: none; border-radius: var(--radius-lg); }

@media (min-width: 48rem) {
  .sponsor-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 64rem) {
  .sponsor-grid { grid-template-columns: repeat(4, 1fr); }
}

/* Platinum carousel. Track scrolls horizontally; JS clones slides and animates
   scrollLeft. With few logos the track simply centers and no arrows are shown. */
.carousel { position: relative; }
.carousel__viewport {
  overflow-x: auto;
  scrollbar-width: none;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
}
.carousel__viewport::-webkit-scrollbar { display: none; }
.carousel__track {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--space-4);
  justify-content: center;
  min-width: min-content;
}
.carousel__track li {
  margin: 0;
  flex: 0 0 auto;
  width: calc((100% - var(--space-4)) / 2);
  min-width: 9rem;
}
.carousel--flowing .carousel__track { justify-content: flex-start; }
.carousel__track a { display: block; text-decoration: none; border-radius: var(--radius-lg); }

/* Narrow strip on the home: shorter tiles than the full grid on the sponsors page. */
.carousel .sponsor-tile { min-height: 7rem; padding: var(--space-4) var(--space-4); }
.carousel .sponsor-tile img { max-height: 3.75rem; }

.carousel__btn {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 2;
  display: none;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background-color: var(--color-surface);
  color: var(--color-primary);
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--shadow-md);
}
.carousel--flowing .carousel__btn { display: block; }
.carousel__btn:hover { background-color: var(--color-surface-sunken); }
.carousel__btn--prev { left: calc(var(--space-4) * -1); }
.carousel__btn--next { right: calc(var(--space-4) * -1); }

@media (min-width: 48rem) {
  .carousel__track li { width: calc((100% - var(--space-4) * 2) / 3); }
}
@media (min-width: 64rem) {
  .carousel__track li { width: calc((100% - var(--space-4) * 3) / 4); }
}

.link-row {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.link-row li { margin: 0; }

/* Schedule — rebuilds the client's raster schedule graphic in markup: navy board,
   light inner panel, one card per day, treeline along the foot of each card, and a
   mountain divider closing the board. The header reuses the site's own hero banner,
   which is the same artwork the client's graphic was built on. See [[decisions]]. */
/* The board: the home hero's own treatment with the site logo inside it, then the
   day cards on a light panel, closed by the landscape strip. Same reading order as
   the client's lámina. See [[decisions]]. */
.schedule-board {
  position: relative;
  overflow: hidden;
  text-align: center;
  color: var(--color-on-dark);
  background-color: var(--color-primary);
  background-image: var(--gradient-hero);
  border-radius: var(--radius-lg);
  border-bottom: 3px solid var(--color-accent-soft);
  box-shadow: var(--shadow-md);
  padding: var(--space-6) var(--space-5);
}
.schedule-board::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--hero-hex-pattern);
  pointer-events: none;
}
/* Lifts the content above ::before without needing a wrapper element. */
.schedule-board > * { position: relative; }

.schedule-board__head { padding-bottom: var(--space-4); }
/* The logo is 3:1, so its width alone sets the head's height: 181 px at 34rem, which
   makes it the tallest single block on the board — the artwork is 127. Shrinking it
   was tried and looked poor, so the board pays for it out of the cards instead; see
   the slimmed paddings below. **Widen this and the board grows three times as fast.** */
.schedule-board__banner {
  width: min(34rem, 92%);
  margin-inline: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
/* Plain light panel. The landscape used to run across the foot of this element,
   behind the cards, which was provisional: the client's lámina puts the artwork
   *inside* each day card. It now lives on .schedule__day — see the four-column
   block at the end of this section. */
.schedule-board__body {
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background-color: var(--color-bg);
}

.schedule {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
}
.schedule__day {
  margin: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  text-align: center;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}
/* The five paddings below and the icon's size are deliberately one step tighter than
   the rest of the site's cards. Four day cards in a row are read at a glance, not
   read through, and the board has to pay for a 181 px logo above them: together this
   takes 72 px out of every card without touching the artwork or the text. Loosen any
   of them back to the house rhythm and the board grows by four times as much as it
   looks. */
.schedule__name {
  margin: 0;
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-on-dark);
  background-color: var(--color-primary);
}
.schedule__date {
  margin: 0;
  padding: var(--space-1) var(--space-4);
  max-width: none;
  font-size: var(--font-size-sm);
  color: var(--color-primary);
  background-color: color-mix(in srgb, var(--color-steel) 26%, var(--color-surface));
}
.schedule__when {
  margin: 0;
  padding: var(--space-3) var(--space-4) 0;
  max-width: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.schedule__when::before,
.schedule__when::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--color-border);
}
.schedule__when span {
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-on-dark);
  background-color: var(--color-accent);
}
.schedule__icon {
  width: 3.25rem;
  height: 3.25rem;
  margin: var(--space-4) auto 0;
  display: grid;
  place-items: center;
  border-radius: var(--radius-pill);
  background-color: var(--color-surface-sunken);
  color: var(--color-primary);
}
.schedule__icon svg {
  width: 1.75rem;
  height: 1.75rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.schedule__block {
  margin: 0;
  padding: var(--space-4);
  max-width: none;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-snug);
  color: var(--color-primary);
}

@media (min-width: 48rem) {
  .schedule { grid-template-columns: repeat(2, 1fr); }
  .schedule-board__body { padding: var(--space-6); }
}
/* The artwork inside the cards, and only here, at four columns. One 7.56:1 strip
   serves all four: `background-size: 400%` makes it exactly four card widths wide,
   so 0 / 33.333% / 66.667% / 100% land one quarter per card, each with its own
   Portland landmark — skyline, St. Johns Bridge, firs under Mount Hood, Hawthorne
   Bridge. The three cuts fall on haze and water, never on a tower.

   Two numbers are derived from the asset and move together with it:
   `padding-bottom` reserves the art's height, which is 4 / 7.56 = 52.9% of the
   card's own width. **Re-crop the strip and both change.**

   Below 64rem the cards are half or full width, where that same 52.9% would be a
   270-550 px slab of landscape, so they simply go without.

   The strip's sky is baked to pure white so it disappears into --color-surface;
   change the card's background colour and the asset has to be regenerated. */
@media (min-width: 64rem) {
  .schedule { grid-template-columns: repeat(4, 1fr); }
  .schedule__day {
    padding-bottom: 52.9%;
    background-image: image-set(
      url("../assets/img/schedule-strip.avif") type("image/avif"),
      url("../assets/img/schedule-strip.webp") type("image/webp"));
    background-repeat: no-repeat;
    background-position-y: bottom;
    background-size: 400% auto;
  }
  .schedule__day:nth-child(1) { background-position-x: 0%; }
  .schedule__day:nth-child(2) { background-position-x: 33.333%; }
  .schedule__day:nth-child(3) { background-position-x: 66.667%; }
  .schedule__day:nth-child(4) { background-position-x: 100%; }
}

/* Two-column page body: content left, sponsor rail right, following ASD 2026. Single
   column below 64rem, where the rail falls under the content. The schedule board is
   deliberately outside it — four day cards in a row need the full width. */
.page-layout { display: grid; gap: var(--space-7); grid-template-columns: 1fr; }
.page-layout__main > * + * { margin-top: var(--space-7); }

/* The rail travels with the reader, because the logo in it is a sold benefit and not
   decoration — see [[adding-a-sponsor]]. `align-items: start` above is what lets it
   stick; without it the rail stretches to the row and never moves.

   `top` clears the sticky `.site-nav`, whose 3.85rem is its own intrinsic height
   (12 + 25.6 + 8 + 3 px of link, + 12 px of inner padding, + its 1 px border).
   **It is a derived number: change the nav's padding and this moves with it.**

   `max-height` + `overflow-y` are the guard. A stuck element taller than the viewport
   can never show its own bottom, so past a certain number of sponsors the last logos
   would become unreachable — the exact failure this rail exists to prevent. The rail
   is ~363 px today and clears a 640 px window up to about 9 sponsors; beyond that it
   scrolls inside itself instead of hiding anyone. */
@media (min-width: 64rem) {
  .page-layout {
    grid-template-columns: minmax(0, 1fr) 17rem;
    align-items: start;
  }
  .sponsor-rail {
    position: sticky;
    top: calc(3.85rem + var(--space-4));
    max-height: calc(100vh - 3.85rem - var(--space-6));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* Speakers — horizontal cards, photo left and text right, following the client's own
   docx tables and ASD 2026. The number per section is not fixed and the client's
   tables carry empty rows, so one card per row at every width: adding a speaker is a
   new <article> and nothing else moves.
   Tighter than the base .person on purpose — with a dozen speakers the page is read
   by scanning, and 2rem of padding plus 1.5rem of gap put three cards on a screen. */
.speaker-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}
.speaker-grid .person { padding: var(--space-4); }
.speaker-grid .person__photo { width: 7rem; }

/* A speaker announced before their photo arrives (Ritala, Romo Negreira, 2026-08-26).
   .photo-placeholder already existed at its own 150px; inside this grid it has to
   match .person__photo exactly or the text column starts at a different x on that one
   card and the whole stack looks misaligned.
   Why a grey box rather than no image at all: the card is a two-column grid above
   30rem, so an absent first child collapses the layout for that card only — which
   reads as a rendering bug, not as missing data. The box reads as "pending", and it
   is the thing that will make the client notice she owes us a photo. */
.speaker-grid .photo-placeholder { width: 7rem; }
.person__talk {
  margin: var(--space-3) auto 0;
  max-width: none;
  font-style: italic;
  color: var(--color-primary);
}
/* A section the client will keep adding to says so, in the same muted italic voice
   as .sponsor-tier__empty. Sits after the grid, not inside it, so it never becomes
   a card and the grid keeps counting only real speakers. */
.speaker-grid__more {
  margin: var(--space-3) 0 0;
  max-width: none;
  font-style: italic;
  color: var(--color-muted);
}

/* Below 30rem the card stays stacked and centred: a 150 px photo plus text in the
   ~140 px left over on a 320 px screen is not a layout, it is two hyphens per line. */
@media (min-width: 30rem) {
  .speaker-grid .person {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: var(--space-5);
    align-items: start;
    /* Packs the three text rows against the top instead of spreading them down the
       height of the photo, which spans all three. */
    align-content: start;
    text-align: left;
  }
  .speaker-grid .person__photo,
  .speaker-grid .photo-placeholder { grid-row: 1 / span 3; margin: 0; }
  .speaker-grid .person__name,
  .speaker-grid .person__affil,
  .speaker-grid .person__talk { margin-inline: 0; }
}


/* Scientific Committee — 21 names, no photos, which is both what the client sent and
   what ASD 2026 shows. Deliberately NOT the .person card: cards are for the handful of
   people the conference is selling, and twenty-one of them would give every committee
   member the visual weight of a keynote while burying the chairs directly above.
   Name and affiliation keep the .person__name / .person__affil pairing — semibold over
   --color-muted — so the two blocks read as one family at different volumes. */
.committee {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-4) var(--space-5);
  grid-template-columns: 1fr;
}

/* Two breakpoints, both set by the longest affiliation on the list — "Eindhoven
   University of Technology, The Netherlands", 50 characters. Below 40rem a second
   column would wrap it to three lines and the entry stops looking like one thing;
   at 64rem the container can still give three columns ~20rem each, which holds it
   to two. A fourth column is what breaks it, so the ladder stops at three. */
@media (min-width: 40rem) { .committee { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .committee { grid-template-columns: repeat(3, 1fr); } }

/* The rule is what separates one member from the next. In a dense grid with no cards,
   whitespace alone lets a wrapped affiliation read as the start of the next entry;
   the rule makes the entry's extent unambiguous at any wrap. --color-border is a
   non-text token and this is a non-text use of it. */
.committee__member {
  margin: 0;
  padding-left: var(--space-4);
  border-left: 2px solid var(--color-border);
}
.committee__name {
  display: block;
  font-weight: var(--font-weight-semibold);
}
.committee__affil {
  display: block;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}
