:root {
  --ink: #1a1a1a;
  --muted: #5c5c5c;
  --line: #dcdcdc;
  --bg: #ffffff;
  --bg-soft: #f6f6f4;
  --accent: #7b1e3a;
  --accent-soft: #f3e6ea;
  /* The hovered state of a button, both accent and err darkened about 20%.
     Literal hex rather than a color-mix(), to match every other token here and
     so the contrast figures below are a property of the file rather than of
     whatever the browser computes: white text is 10.1:1 on --accent and 12.5:1
     on --accent-dark, so hovering only ever improves legibility. */
  --accent-dark: #62182e;
  --ok: #1c6b3f;
  --warn: #8a5a00;
  --err: #a01b1b;
  --err-dark: #801616;
  --measure: 68ch;
  --radius: 6px;
}

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

body {
  margin: 0;
  font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--ink);
  background: var(--bg);
}

/* padding-inline rather than the `padding` shorthand. As a shorthand this also
   set padding-block:0, and a class beats an element selector, so it silently
   overrode `main`'s own padding-block on every page — the copy sat flush against
   the header. The header, hero and footer wraps set padding-block explicitly and
   are unaffected either way. */
.wrap { max-width: 62rem; margin: 0 auto; padding-inline: 1.25rem; }
.prose { max-width: var(--measure); }
.prose h1, .prose h2, .prose h3 { line-height: 1.25; margin: 2rem 0 .5rem; }
.prose p, .prose ul, .prose ol { margin: 0 0 1rem; }
/* Markdown cannot attach a class, so any image embedded in content/*.md needs a
   global guard — without this a wide logo overflows .prose and makes the whole
   page scroll sideways. */
img { max-width: 100%; height: auto; }
a { color: var(--accent); }

/* Header and nav */
.site-header { border-bottom: 1px solid var(--line); background: var(--bg-soft); }
.site-header .wrap { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.5rem; padding-block: .85rem; }
/* The brand is the PINTS mark rather than the word. Sized by height so the
   header row keeps the height the 1.35rem bold text used to give it; the brain
   sits above the cap line, which is why the box is a little taller than the
   letters look. */
.brand { display: flex; align-items: center; text-decoration: none; }
.brand img { display: block; height: 1.85rem; width: auto; }
.site-nav { display: flex; flex-wrap: wrap; gap: .25rem 1.05rem; margin-left: auto; }
/* nowrap so a two-word label like "My account" can never break mid-label and
   drag the row height with it. */
.site-nav a { color: var(--muted); text-decoration: none; padding: .25rem 0; border-bottom: 2px solid transparent; white-space: nowrap; }
.site-nav a:hover { color: var(--ink); }
.site-nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }
.site-nav a.auth-link { color: var(--accent); font-weight: 600; }
/* display:contents so the links inside become flex items of .site-nav and pick
   up its gap, rather than bunching together inside the wrapper. */
.site-nav .auth-links { display: contents; }

/* Footer */
.site-footer { margin-top: 4rem; border-top: 1px solid var(--line); background: var(--bg-soft); }
.site-footer .wrap { padding-block: 1.5rem; color: var(--muted); font-size: .9rem; }

/* Back to top (js/layout.js). The header is not sticky, so on the long pages —
   the abstract list, the program, the review console with rows open — there is
   otherwise no way back to the nav but dragging.

   visibility, not opacity alone: a transparent button is still in the tab order,
   and tabbing into a control nobody can see is worse than not having one. */
.to-top {
  position: fixed; right: 1rem; bottom: calc(1rem + env(safe-area-inset-bottom));
  width: 2.75rem; height: 2.75rem; padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.25rem; line-height: 1;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
  /* Only has to beat the normal flow — nothing else on the site is positioned.
     dialog.confirm lives in the top layer and is above this regardless. */
  z-index: 10;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .15s ease, visibility .15s;
}
.to-top-shown { opacity: 1; visibility: visible; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .to-top { transition: none; }
  /* Belt and braces with js/hero-slider.js, which does not start its timer at
     all under reduced motion: if the slider is ever advanced some other way,
     the photographs still cut rather than slide. */
  .hero-slide { transition: none !important; }
  .album-thumbs img { transition: none; }
}

main { padding-block: 2rem 1rem; }
/* The home page's hero is a filled band, not just a rule: its bottom edge needs
   more clearance than a plain header before the copy starts, or the two read as
   one block. */
.hero + main { padding-top: 3.5rem; }

/* Hero */
/* position and overflow are for the photographs behind it (see below); the band
   takes no z-index of its own, so it stays under .to-top's 10 and the normal
   flow decides everything else.

   min-height because content height alone crops a landscape photograph to a
   strip too thin to make out. It scales with the viewport rather than sitting
   at one number: 26rem is a good band on a laptop and far too much of a phone
   screen, and the floor is the height the hero had before the photographs, so
   the band never gets shorter than it used to be at any width.

   The content is centred in that height rather than sitting at the top. Once
   min-height exceeds what the logo and buttons need, the surplus would
   otherwise all fall below them and leave the band looking bottom-heavy. */
.hero {
  position: relative;
  overflow: hidden;
  min-height: clamp(20rem, 30vw, 26rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--accent-soft);
  border-bottom: 1px solid var(--line);
}
/* Positioned and lifted, so the logo and buttons paint above both the photo
   layer and the legibility veil below. The 1 only has to beat those two; it
   stays well under .to-top's 10, which is the only other z-index here.

   The padding is asymmetric on purpose: the action row is the last thing in the
   hero and needs little room under it, while the band itself has to read as
   separate from the copy that follows (see main's padding-top).

   width:100% is not redundant. .hero is a flex column now, which makes this a
   flex item, and .wrap's own `margin: 0 auto` is an auto margin on the cross
   axis -- which switches stretch off and leaves the item shrink-to-fit. The
   column then collapsed to the width of the widest thing in it, taking the
   logo's `width: 100%` down with it. This restores the block-layout behaviour:
   full width, capped by max-width, centred by the auto margins. */
.hero .wrap { position: relative; z-index: 1; width: 100%; padding-block: 3rem 1.75rem; }
/* The wrap spans the full 62rem column across the middle of the band, so it
   would otherwise swallow every click aimed at the photographs behind it --
   including the empty space either side of the logo, which is most of the
   column. Made transparent to the pointer instead, with the buttons switched
   back on: pointer-events is inherited, so that is one declaration and one
   exception rather than a rule per child.

   The logo is deliberately not on that list. It is an image, not a control, and
   clicking it goes to the Archive along with the rest of the band. */
.hero .wrap { pointer-events: none; }
.hero .wrap :is(a, button) { pointer-events: auto; }
/* Room for the chip, which is out of flow and would otherwise sit 3px under the
   buttons at every width between roughly 700 and 1100, where the band's height
   is what the content asks for rather than the min-height. Conditional so a hero
   with no photographs -- and so no link and no chip -- keeps the tight bottom the
   comment above argues for, instead of growing an empty strip under the actions. */
.hero:has(.hero-link:not([hidden])) .wrap { padding-bottom: 3rem; }
/* More air between the logo and the buttons than .actions gives it elsewhere.
   The global 1.5rem is sized for an action row that closes a form, sitting just
   under the last field it applies to. Here the two are separate things -- an
   identity and a set of choices -- and at the hero's size they read as crowded
   at that distance. Scoped to .hero so no form on any other page moves. */
.hero .actions { margin-top: 2.75rem; }

/* Hero photographs (js/hero-slider.js).

   Three layers, and each one is load-bearing:

   1. .hero-photos carries the opacity, rather than each slide carrying its own.
      Two slides overlap mid-transition, and per-slide opacity would make the
      overlap darker than either slide alone -- a visible pulse on every change.

   2. Each slide carries a burgundy veil in `color` blend mode, which keeps the
      photograph's luminosity and takes only hue and saturation from --accent.
      Without it a quarter-strength photograph over --accent-soft comes out a
      flat grey-pink and the band stops reading as the PINTS colour at all.

   3. .hero::after, below, lifts the middle back up for the text.

   The opacity and the veil's alpha are a matched pair, not two free knobs. The
   veil covers the middle of the band, where all the type is, so raising it buys
   back the contrast that raising the opacity costs -- while the band's left and
   right thirds, which the veil fades out over, keep the full strength. Moving
   one without the other is what makes the hero either muddy or illegible.
   Measured behind the type, against a black pixel -- the worst case any
   photograph can present: .45/.78 holds the wordmark at 6.7:1 and the date line
   at 4.4:1. Over a mid-tone the date line is 4.9:1. See the contrast note on
   .hero::after. */
.hero-photos { position: absolute; inset: 0; opacity: var(--hero-photo-opacity, .45); }
/* Legibility veil, over the photographs and under the logo.

   The hero's text is the logo SVG, and its date and subtitle lines are #808080
   -- which manages only 3.3:1 against the bare --accent-soft band, before any
   photograph darkens it further. This puts --accent-soft back over the middle
   of the band, where .wrap's 62rem column sits, and fades out to nothing at the
   edges so the photographs stay at full strength either side of the type.

   That grey is the ceiling on how clear the photographs can get: it is what
   stops --hero-photo-opacity going higher, not the logo itself, which has
   contrast to spare. Fixing GREY in scripts/logo/build_logo.py would lift the
   ceiling for both numbers -- see the README.

   It cannot be a ::before: generated content before the first child would paint
   under .hero-photos rather than over it, which is why .hero .wrap needs the
   z-index above -- this ::after is the last child and would otherwise cover the
   logo along with the photographs.

   The end stops are --accent-soft at zero alpha, written with relative colour
   syntax, and NOT the `transparent` keyword. `transparent` is rgba(0,0,0,0), so
   a gradient running to it interpolates through transparent *black* and lays a
   grey cast over both ends of the band -- visible even with no photographs
   behind it, where this layer is supposed to be invisible. Fading to the same
   colour at zero alpha keeps the hue constant, which makes the whole veil a
   no-op over the bare --accent-soft band. */
.hero::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg,
    rgb(from var(--accent-soft) r g b / 0) 0%,
    rgb(from var(--accent-soft) r g b / .78) 20%,
    rgb(from var(--accent-soft) r g b / .78) 80%,
    rgb(from var(--accent-soft) r g b / 0) 100%);
}
/* Every slide waits off the right edge; .hero's overflow:hidden clips it there.
   No transition in this state on purpose -- a slide that has just stopped being
   .leaving returns here, and it must jump back rather than fly across the band
   in front of the reader. */
.hero-slide {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  transform: translateX(100%);
  /* So the duotone below blends with this slide's own photograph and nothing
     else. Not left to the transform to isolate: the on-screen slide is
     `transform: none`, which creates no stacking context, and the tint would
     then reach the hero band underneath. */
  isolation: isolate;
}
/* The burgundy duotone, per slide rather than over the whole layer. On the
   layer it painted even with no slides beneath it -- blending against nothing
   leaves the source colour, so an empty hero came out washed burgundy instead
   of the plain --accent-soft band it is supposed to be. */
.hero-slide::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--accent);
  mix-blend-mode: color;
}
/* The band itself is the link to the Archive (js/page-content.js reveals it once
   photographs have actually mounted), and .hero-credit is the corner of it that
   says so.

   One link, two affordances: the chip answers "is this clickable?", and the
   band-sized target means nobody has to hit the chip to act on the answer. The
   chip is a span rather than a second anchor -- two links to the same page would
   be two tab stops announcing the same destination -- which also means the
   accessible name is its own visible text and needs no aria-label.

   z-index 1 lifts the link over the photo layer and over .hero::after; .wrap
   carries the same 1 and comes later in the markup, so the buttons still paint
   on top of this and still take their own clicks. */
.hero-link {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: pointer;
  /* The chip is inside the anchor and would otherwise inherit the underline the
     UA puts on every link. It gets one back on hover, below. */
  text-decoration: none;
}
/* A ring rather than anything that touches the photographs. Lifting
   --hero-photo-opacity on hover was the obvious cue and the wrong one: it is
   half of a matched pair with the .hero::after alpha, and moving it alone takes
   the date line under 4.5:1 against a dark photograph. This sits at the band's
   edges, where there is no type at all. */
.hero-link:hover { box-shadow: inset 0 0 0 2px rgb(from var(--accent) r g b / .2); }
/* Inset, because an element the size of the band would otherwise draw its ring
   half under the header and off the sides of the window. */
.hero-link:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }

/* The chip, sitting in the corner of the band like a photo credit.

   It carries its own background rather than trusting .hero::after for contrast:
   that veil fades to nothing at exactly the left and right edges, which is where
   this sits, so the text would otherwise be reading against an uncontrolled
   photograph at full strength.

   Anchored to the band's edge, not to .wrap's 62rem column -- past that width
   the two part company, and the corner of the photograph is where a credit
   belongs. */
.hero-credit {
  position: absolute;
  right: 1.25rem;
  bottom: .75rem;
  padding: .3rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgb(from var(--bg) r g b / .85);
  color: var(--accent);
  font-size: .82rem;
  line-height: 1.3;
}
/* Driven by the link, not by the chip: the whole band is the target, so pointing
   anywhere in it should light the chip up rather than only the 236px that
   happen to be under the words. */
.hero-link:is(:hover, :focus-visible) .hero-credit { background: var(--bg); text-decoration: underline; }
.hero-slide.current { transform: none; transition: transform .9s ease; }
.hero-slide.leaving { transform: translateX(-100%); transition: transform .9s ease; }
/* The h1 holds only the logo image, so it carries no type of its own. `font:
   inherit` matters beyond tidiness: --measure is 68ch, and ch is the width of
   the element's own "0" — under the h1's bold display font that came out 9%
   wider than the identical rule on .prose. */
.hero h1 { margin: 0 0 .5rem; font: inherit; }
/* index.html puts the header logo inside the h1, so the h1's own margin still
   does the spacing.

   The cap used to be --measure, matching the prose column below so the hero and
   the page copy shared one left-to-right rhythm. It is wider than that now, to
   hold its own against the photographs behind it -- so the logo deliberately
   runs past the right edge of the text column below. width:100% keeps it
   shrinking to fit on a narrow screen, where the cap never comes into play. */
.hero h1 .logo {
  display: block;
  width: 100%;
  max-width: var(--hero-logo-width, 46rem);
  height: auto;
}

/* Cards, tables, forms */
figure { margin: 1rem 0; }
figcaption { margin-top: .35rem; max-width: var(--measure); color: var(--muted); font-size: .9rem; }

.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1rem; background: var(--bg); }
/* h1 when the card is a whole page (a shared abstract link), h3 in a list.
   Sized here rather than by level, so one abstract does not shout just
   because it happens to be alone on the page. */
.card :is(h1, h2, h3) { margin: 0 0 .25rem; font-size: 1.25rem; }
.card .byline { color: var(--muted); font-size: .92rem; margin: 0 0 .5rem; }
/* Status, topic and the share link share one line above the byline. */
.card .card-meta { margin: 0 0 .5rem; }
.card .card-meta:empty { display: none; }
/* The decision, not a label: a status pill is a statement about this abstract,
   so it is the one pill that carries colour. The three tones reuse the --ok /
   --warn / --err tokens and the tints .msg already uses, so nothing new enters
   the palette. Undecided is amber rather than grey because "waiting on us" is a
   state the committee should see, not a default. */
.pill.status { font-weight: 600; }
.pill.status-review { border-color: var(--warn); color: var(--warn); background: #fdf6e7; }
.pill.status-accepted { border-color: var(--ok); color: var(--ok); background: #eef7f1; }
.pill.status-rejected { border-color: var(--err); color: var(--err); background: #fbeeee; }
.permalink { font-size: .85rem; }

/* The public abstract list: one row per abstract, body behind a disclosure.
   A poster session runs to hundreds, so the row is the unit that has to stay
   cheap to scan — hence a tighter margin than the generic `details` rule, which
   was written for a single disclosure on the account page. */
.abstract { margin: 0 0 .4rem; }
.abstract > summary {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .75rem;
  padding: .55rem .85rem; font-weight: 400;
}
.abstract > summary:hover { background: var(--bg-soft); }
/* The title carries the weight; the author is the secondary cue you scan for
   when you already know whose poster you are looking for. */
.abstract .summary-title { font-weight: 600; }
.abstract .summary-authors { color: var(--muted); font-size: .92rem; }
.abstract .abstract-body { padding: .75rem 1rem 1rem; }
.abstract .abstract-body > .byline:first-child { margin-top: 0; }

/* The review console reuses the public list's row, plus the one number a
   decision meeting sorts on. Pushed to the end so the titles stay left-aligned
   however long the author line runs. */
.admin-abstract .summary-score { margin-left: auto; color: var(--muted); font-size: .9rem; white-space: nowrap; }
.admin-abstract > summary { gap: .35rem .6rem; }
.admin-abstract .actions { margin-top: 1rem; }

.topic-heading { margin: 2rem 0 .75rem; font-size: 1.15rem; }
.topic-heading .muted { font-weight: 400; font-size: .9rem; }
#expand-all { margin-bottom: 1rem; }

/* Printing the abstract list should yield an abstract book. The rows are opened
   from a beforeprint handler rather than by a rule here — their bodies are built
   lazily, so there would otherwise be nothing for CSS to reveal. This only has
   to strip the controls and the chrome around them. */
@media print {
  .site-header, .site-footer, .to-top, #filters, #expand-all, #count, .permalink { display: none; }
  .abstract, .card { border: 0; padding-inline: 0; break-inside: avoid; }
  .abstract > summary { list-style: none; padding-inline: 0; }
  .abstract > summary::-webkit-details-marker { display: none; }
  .abstract .abstract-body { padding-inline: 0; }
}

/* The abstract-submission disclosure on account.html. Closed by default, so the
   summary has to read as a button people can find. */
details { border: 1px solid var(--line); border-radius: var(--radius); margin: 1rem 0; }
details > summary { padding: .75rem 1rem; cursor: pointer; font-weight: 600; }
details[open] > summary { border-bottom: 1px solid var(--line); }
details > :not(summary) { padding: 0 1rem 1rem; }

/* The in-place abstract editor. It borrows the disclosure's chrome deliberately:
   editing an abstract and submitting a new one are the same form, so they should
   read as the same component. The body's padding is also what keeps the action
   row off the top border of the next abstract's card. */
.panel { border: 1px solid var(--line); border-radius: var(--radius); margin: 1rem 0; background: var(--bg); }
.panel > .panel-head { padding: .75rem 1rem; font-weight: 600; border-bottom: 1px solid var(--line); }
.panel > .panel-body { padding: 0 1rem 1rem; }

/* Confirmation modals (js/confirm-dialog.js). */
dialog.confirm {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem;
  width: min(34rem, calc(100vw - 2rem)); background: var(--bg); color: var(--ink);
}
dialog.confirm::backdrop { background: rgba(0, 0, 0, .35); }
dialog.confirm h2 { margin: 0 0 .5rem; font-size: 1.15rem; }
dialog.confirm p { margin: 0; }
dialog.confirm .actions { margin-top: 1.25rem; }

.table-scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
tr.kind-break td, tr.kind-lunch td { color: var(--muted); font-style: italic; }
td.time { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* The title is what the reader is scanning for, so it carries the weight. The
   muted rows keep theirs: coffee and lunch are the punctuation between sessions,
   and bolding them would flatten the distinction the italics are making. */
.program td.what { font-weight: 600; }
.program tr.kind-break td.what, .program tr.kind-lunch td.what { font-weight: 400; }

/* Program sessions. Each <tbody class="session session-*"> is one block: a
   banner row naming it, then its items, all sharing a tint so the eye can find
   where a session starts and ends without reading the times.

   The tints are deliberately pale — they sit behind body text, so they are
   chosen for a comfortable contrast ratio against --ink rather than for
   punch, and the left border is what actually marks the block for anyone who
   cannot separate the hues. */
.program tbody.session { --session-tint: var(--bg-soft); }
.program tbody.session-cognitive { --session-tint: #e6eef7; }
.program tbody.session-computational { --session-tint: #f7e7e4; }
.program tbody.session-systems { --session-tint: #e7f0e6; }
.program tbody.session-keynote { --session-tint: #eceaef; }
.program tbody.session td, .program tbody.session th { background: var(--session-tint); }
.program tbody.session > tr > :first-child { border-left: 3px solid rgba(0, 0, 0, .18); }
/* The banner reads as a heading inside the table, so it takes the weight the
   rows do not: no uppercase, because it carries a proper name. */
.program tr.session-head > * {
  font-weight: 700; color: var(--ink); font-size: 1rem;
  text-transform: none; letter-spacing: normal; padding-block: .6rem;
}
.program tr.session-head th { white-space: nowrap; }

form { max-width: var(--measure); }
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0 1rem 1rem; margin: 0 0 1.5rem; }
legend { padding: 0 .4rem; font-weight: 600; }
fieldset .hint { display: block; font-size: .85rem; color: var(--muted); margin: .75rem 0 0; }
input[readonly] { background: var(--bg-soft); color: var(--muted); }
label { display: block; margin: 1rem 0 .25rem; font-weight: 600; }
label .hint { display: block; font-weight: 400; font-size: .85rem; color: var(--muted); }
input[type="text"], input[type="email"], input[type="password"], input[type="time"],
input[type="date"], input[type="datetime-local"], input[type="number"], select, textarea {
  width: 100%; padding: .5rem .6rem; border: 1px solid var(--line);
  border-radius: var(--radius); font: inherit; background: var(--bg); color: var(--ink);
}
textarea { min-height: 12rem; resize: vertical; }
.checkline { display: flex; align-items: flex-start; gap: .5rem; margin: 1rem 0; }
.checkline input { margin-top: .35rem; }
.checkline label { margin: 0; }

button, .button {
  font: inherit; font-weight: 600; padding: .5rem 1rem; border-radius: var(--radius);
  border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer;
  text-decoration: none; display: inline-block;
}
button.secondary, .button.secondary { background: transparent; color: var(--accent); }
button.danger { background: var(--err); border-color: var(--err); }
button[disabled] { opacity: .5; cursor: not-allowed; }
.actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.5rem; }

/* Hover: the button rises and fills.
 *
 * Buttons were the one control on this site that did nothing under the cursor.
 * The nav links (:hover above), the abstract rows, the archive thumbnails and
 * the hero band all answer it, so this closes a gap rather than decorating the
 * home page -- which is why it is one rule on `button, .button` and not four
 * buttons in the hero.
 *
 * THREE BUTTONS OPT OUT, and all three are buttons only in the DOM sense:
 *   .to-top        carries its own shadow and its own transition, and fades
 *                  itself in from opacity: 0.
 *   .album-thumb   is an image with a button wrapped round it. `background:
 *                  none` is deliberate and .album-thumb:hover img already owns
 *                  the hover there.
 *   .tabs button   is deliberately chrome-less. A filled background would
 *                  destroy the tab strip, and its selected state is a
 *                  border-bottom-color that a border transition would blur.
 *
 * THE :where() INSIDE :not() IS LOAD-BEARING, not a stylistic choice. :not()
 * takes the specificity of its most specific argument, so a bare
 * :not(..., .tabs button) would inherit (0,1,1) and push this rule to (0,3,1)
 * -- high enough to beat button.danger:hover below and swallow the red.
 * :where() is always zero, which pins this at (0,2,0). That ties
 * .button.secondary above, so THIS BLOCK MUST STAY BELOW IT: the tie is broken
 * on source order, and moving it up silently loses the fill on every
 * <a class="button secondary">.
 *
 * :not([disabled]) is the other load-bearing piece. button[disabled] sets
 * opacity only, and :hover matches a disabled control even where events do not
 * fire -- so without it the permanently disabled ones (the first photo's "move
 * up" in the admin, "Edit submission" on an accepted abstract) would lift and
 * fill under the cursor and promise something they will not do.
 *
 * The whole thing is behind (hover: hover) and (pointer: fine) because a touch
 * browser applies :hover on tap and leaves it applied until the next tap
 * somewhere else, which would leave the last button anybody touched lit up. */
@media (hover: hover) and (pointer: fine) {
  :is(button, .button):not(:where(.to-top, .album-thumb, .tabs button)) {
    transition: transform .15s ease, box-shadow .15s ease,
                background-color .15s ease, border-color .15s ease, color .15s ease;
  }
  :is(button, .button):not(:where([disabled], .to-top, .album-thumb, .tabs button)):hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 10px rgb(from var(--ink) r g b / .18);
    /* Named for all three variants at once: a secondary fills from transparent,
       a primary darkens, and .danger is corrected on the next rule. */
    background: var(--accent-dark);
    border-color: var(--accent-dark);
    color: #fff;
  }
  /* (0,2,1), so it beats the (0,2,0) above. Without this a Delete button turns
     burgundy on hover -- losing the one colour that says what it does at the
     exact moment somebody is about to press it. */
  button.danger:not([disabled]):hover {
    background: var(--err-dark);
    border-color: var(--err-dark);
  }
}

/* A SECOND reduced-motion block, and it has to be here rather than in the one
   near the top of this file. That block sits at the head of the stylesheet,
   above the transition declared just now, and a media query grants no extra
   specificity -- so an equally specific rule later in the source wins over it
   regardless. Cancelling the movement from up there would look correct and do
   nothing.

   The fill stays. It is the affordance; only the travel is the motion. */
@media (prefers-reduced-motion: reduce) {
  :is(button, .button) { transition: none; }
  :is(button, .button):hover { transform: none; }
}

/* Keyboard parity. Buttons had no focus style of their own and fell back to the
   UA outline; now that the cursor gets an answer, the keyboard needs the same
   one. Matches the outline .hero-link and .album-thumb already use.
   :focus-visible rather than :focus, so the confirm dialog's buttons -- focused
   programmatically the moment it opens -- do not ring after a mouse click. */
:is(button, .button):focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Status messages */
.msg { padding: .6rem .9rem; border-radius: var(--radius); margin: 1rem 0; border: 1px solid; }
.msg.ok { color: var(--ok); border-color: var(--ok); background: #eef7f1; }
.msg.warn { color: var(--warn); border-color: var(--warn); background: #fdf6e7; }
.msg.err { color: var(--err); border-color: var(--err); background: #fbeeee; }
.msg ul { margin: .25rem 0 0 1rem; padding: 0; }
.msg:empty { display: none; }

.muted { color: var(--muted); }
.pill { display: inline-block; font-size: .78rem; padding: .1rem .5rem; border-radius: 999px; background: var(--bg-soft); border: 1px solid var(--line); color: var(--muted); }
.poster-no { font-weight: 700; color: var(--accent); }
sup { font-size: .7em; }

/* Admin tabs */
/* Admin: the review console's filter bar and per-organizer review block. */
.filters { display: flex; flex-wrap: wrap; gap: .75rem 1rem; align-items: flex-end; margin: 1rem 0; }
.filters .filter { margin: 0; font-size: .85rem; color: var(--muted); }
.filters .filter input, .filters .filter select { min-width: 9rem; margin-top: .2rem; }
.filters button { align-self: flex-end; }

.review { border-top: 1px solid var(--line); margin-top: 1rem; padding-top: .75rem; }
.review > p { margin: 0 0 .5rem; }
.review .filter { display: inline-block; font-size: .85rem; color: var(--muted); margin: 0; }
.review .filter select { width: auto; min-width: 5rem; margin-left: .5rem; }
.review details { margin-top: .75rem; }
.review details p { margin: .35rem 0 0; }

.tabs { display: flex; flex-wrap: wrap; gap: .5rem; border-bottom: 1px solid var(--line); margin-bottom: 1.5rem; }
.tabs button { background: transparent; color: var(--muted); border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: .5rem .75rem; }
.tabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
[hidden] { display: none !important; }

/* Archive slideshow. One photograph at a time; the stage keeps a fixed height
   so paging through portrait and landscape shots does not make the page jump. */
.slideshow { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-soft); padding: 1rem; }
.slideshow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.slideshow figure { margin: 0; }
.slideshow img { display: block; margin: 0 auto; max-height: 30rem; max-width: 100%; width: auto; border-radius: var(--radius); }
/* The global figcaption is capped at the reading measure, which is narrower
   than the stage and would leave the caption off to the left of its photo. */
.slideshow figcaption { max-width: none; text-align: center; }
.slide-nav { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: .75rem; }
.slide-nav button { min-width: 3rem; font-size: 1.25rem; line-height: 1; padding: .35rem 1rem; }

/* An album mounted under the edition it belongs to.
   .prose caps its children at the reading measure (--measure, 68ch) so lines
   stay readable, and a photograph squeezed to text width is a waste of it. But
   `max-width: none` alone buys nothing — the cap is on .prose itself, and no
   child can be wider than its parent — so the album is given an explicit width
   and overflows to the right, landing on the wrap's content edge because .prose
   starts on its left one.
   Only once the wrap has stopped growing: at 62rem and up its content box is
   exactly 62rem - 2.5rem, a fixed number with no vw in it. A vw-based bleed
   would read the scrollbar's width as page width on the browsers that still
   have one, and overflow the page by it at exactly the narrow sizes where
   there was nothing to gain. */
.prose > .archive-album { margin: 1.25rem 0 2rem; }
@media (min-width: 62rem) {
  .prose > .archive-album { width: calc(62rem - 2.5rem); max-width: none; }
}

/* Thumbnail strip. It scrolls sideways rather than wrapping onto rows, because
   an album of forty photographs would otherwise push the next edition's copy
   off the bottom of the screen. `position: relative` is load-bearing:
   revealThumb() in js/slideshow.js reads offsetLeft, which is measured against
   the nearest positioned ancestor and has to be the strip. */
.album-thumbs {
  position: relative; display: flex; gap: .5rem; margin-top: .75rem;
  padding-bottom: .35rem; overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.album-thumb { flex: 0 0 auto; padding: 0; border: 0; background: none; line-height: 0; cursor: pointer; }
.album-thumb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius); }
/* Overrides .slideshow img above, which centres the stage photograph and lets it
   grow to 30rem tall — neither of which a thumbnail wants. */
.album-thumbs img {
  width: 5.5rem; height: 3.75rem; margin: 0; max-height: none;
  object-fit: cover; border: 1px solid var(--line); border-radius: var(--radius);
  opacity: .6; transition: opacity .15s ease;
}
.album-thumb:hover img, .album-thumb:focus-visible img { opacity: 1; }
/* Faded until it is the one on the stage. Both the outline and the full opacity
   carry it, so it is not colour alone that says which photograph you are on. */
.album-thumb[aria-current] img { opacity: 1; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }

/* Admin archive tab: a thumbnail with its caption under it. */
.archive-admin { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 1rem; }
.archive-admin > div { width: 12rem; }
.archive-admin img { width: 100%; height: 8rem; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--line); }
.archive-admin input { margin-top: .35rem; font-size: .85rem; }

.sponsors { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; margin-top: 1rem; }
.sponsors img { max-height: 3.5rem; width: auto; }

@media (max-width: 40rem) {
  .site-header .wrap { padding-block: .75rem; }
  .site-nav { margin-left: 0; width: 100%; }
}
