/* ===========================================================================
   Nikki Brooklyn — styles
   Mobile-first. No external fonts, no CDN, no build step.

   Two rules that are load-bearing and easy to break later:
     1. dvh, never vh. vh does not account for the collapsing mobile toolbar.
     2. position:fixed layers, never background-attachment:fixed (broken on iOS).

   Colours are measured, not guessed. See README "Contrast".
   =========================================================================== */

:root {
  /* Sampled from the darkest edge of the hero photo (landscape top-right,
     srgb 11.4% 13.0% 17.7%). The stage backdrop is dark NAVY — pure #000 here
     would show a visible seam against the photo. */
  --bg: #1D212D;

  /* Scrim colour as an rgb triplet so alpha can vary per stop. */
  --scrim: 11, 13, 18;

  /* Verified against the brightest pixel in each text region, scrim applied.
     The accent is sampled from the cream jacket she is wearing, so it belongs
     to the photograph rather than sitting on top of it — the image's own story
     is a warm subject against a cold stage. */
  --ink:    #F4F2EC;   /* 7.13:1 */
  --muted:  #C3C7D1;   /* 4.72:1 */
  --accent: #EBD9C2;   /* 5.79:1 — jacket, lifted for text */
  --mark:   #E7C4A9;   /* 4.89:1 — jacket highlight, chart marks */

  --rule: rgba(244, 242, 236, .16);

  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Where the desktop text column begins. The scrim ramp must finish before this. */
  --col-start: 52%;

  --pad: 1.25rem;
}

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

/* The wordmark lines are display:block, which would otherwise beat the UA's
   default [hidden] rule when main.js hides a line. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  /* Safety net for narrow viewports; the layout is written not to need it. */
  overflow-x: hidden;
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: clamp(1rem, .95rem + .25vw, 1.0625rem);
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img { max-width: 100%; }

/* --- Fixed layers ------------------------------------------------------- */

.hero {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;      /* a broken image cannot push document layout */
  background-color: var(--bg);
}

.hero picture,
.hero img {
  display: block;
  width: 100%;
  height: 100%;
}

.hero img {
  object-fit: cover;

  /* MOBILE (portrait crop): anchored to the TOP.

     On a tall phone the portrait crop overflows horizontally only, so the
     vertical anchor is moot — but on shorter viewports (landscape phones, small
     tablets under 48rem) it overflows vertically instead, and `center` would
     crop her head out of frame. She sits high in the portrait file, so top
     anchoring keeps the face in view at every height.

     30% horizontal keeps her clear of the left edge while leaving the right of
     the frame open. */
  object-position: 30% top;
}

/* Must match the <source media> in index.html EXACTLY — the anchor belongs to
   whichever photo is loaded, not to the layout breakpoint (60rem). The aspect
   condition keeps portrait tablets on the portrait crop; see index.html.

   Landscape is anchored TOP LEFT: she stands at frame-left, so pinning that
   corner keeps her fully in shot and pushes all the crop loss to the right and
   bottom, which is empty stage. */
@media (min-width: 48rem) and (min-aspect-ratio: 1/1) {
  .hero img { object-position: left top; }
}

/* Set by main.js when the photo 404s — leaves the sampled background colour. */
.hero.is-failed img { display: none; }

.scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;

  /* MOBILE: text spans the full width and scrolls through every band of a fixed
     photo, so the scrim must be strong everywhere — the brightest pixel behind
     it is the white puffer jacket at L=0.999. Minimum alpha here is .74. */
  background: linear-gradient(
    to bottom,
    rgba(var(--scrim), .84) 0%,
    rgba(var(--scrim), .76) 18%,
    rgba(var(--scrim), .74) 50%,
    rgba(var(--scrim), .74) 100%   /* flat from mid-screen down — see below */
  );
}

/*  With JS, mobile lands with the photo clear and darkens on scroll.

    The scrim does NOT fade in as a whole — that was the bug. It stays fully
    opaque and instead its transparent stops slide upward: --dim 0 leaves the
    top 42% clear, --dim 1 collapses them to 0% and the whole screen sits at .74.

    Everything therefore happens in ONE coordinate system, the viewport. The
    bottom stop is .74 at every value of --dim, so the bottom of the frame never
    changes and there is nothing to jump. An earlier version put the landing
    backdrop on .hero-title, which is painted on a SCROLLING block — it agreed
    with the fixed scrim only at scroll 0, and one pixel of scroll dropped the
    bottom of the screen from .74 to .009. Do not reintroduce a second layer.

    No transition: the morph is driven directly by scroll position, so a
    transition would only lag it behind your finger.

    The class is what enables all this, so with JS off the scrim above simply
    stays on and no copy is ever left over a bright photo. */
.js-dim .scrim {
  opacity: 1;
  background: linear-gradient(
    to bottom,
    rgba(var(--scrim), 0)   calc(42% * (1 - var(--dim, 0))),
    rgba(var(--scrim), .26) calc(60% * (1 - var(--dim, 0))),
    rgba(var(--scrim), .74) calc(70% * (1 - var(--dim, 0))),
    rgba(var(--scrim), .74) 100%
  );
}

/* --- Page grid ---------------------------------------------------------- */

.page {
  position: relative;
  z-index: 2;
}

.col {
  padding-left: var(--pad);
  padding-right: var(--pad);
  min-width: 0;
}

/* --- Typography --------------------------------------------------------- */

/*  No background here on purpose. The landing backdrop lives on .scrim, which
    is fixed to the viewport; painting it on this scrolling block instead is
    what caused the shade to jump the moment you scrolled. See .js-dim .scrim.

    The wordmark still needs that backdrop — it sits over the brightest part of
    the frame (measured L=0.94, under 1:1 unscrimmed). At rest the scrim reaches
    .74 by 70% of the viewport, which is where the type starts:
    ink 7.89:1, muted 5.22:1 against the brightest pixel underneath. */
.hero-title {
  min-height: 100dvh;         /* dvh, never vh */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: 10dvh;
}

.wordmark {
  margin: 0;
  font-weight: 700;
}

.wordmark-name {
  display: block;
  font-size: clamp(2.75rem, 13vw, 7.5rem);
  line-height: .92;
  letter-spacing: -.035em;
  text-wrap: balance;
  overflow-wrap: break-word;
}

/* "That" is the SAME display face, weight and colour as the name — inherited
   from .wordmark — just smaller. It reads as the top line of one wordmark, not
   as a label. Held at roughly 40% of the name's size at every viewport, so the
   two scale together. */
.wordmark-eyebrow {
  display: block;
  font-size: clamp(1.1rem, 5.2vw, 3rem);
  line-height: 1;
  letter-spacing: -.02em;
  margin: 0 0 .08em;
}

/* The line beneath the name. Deliberately NOT shared with the eyebrow above. */
.kicker {
  margin: 1rem 0 0;
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 400;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}

.sec {
  padding: clamp(3rem, 10dvh, 6rem) 0 0;
}

/* No [01] [02] counter. Bio -> Upcoming -> Credits -> Elsewhere is not a
   sequence — not chronological, procedural or ranked — so numbering it would
   encode nothing and merely dress decoration as information. A hairline does
   the separating instead. (The one real sequence in this data, the run of
   sets, is carried by the strip in "The run".) */
.sec-head {
  margin: 0 0 1.75rem;
  font-family: var(--mono);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 1rem;
}

.sec-head::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}

/* The bio's first sentence is lifted so the section opens on her voice rather
   than on body copy.

   The :not() on the body rule below is load-bearing, not decoration. `.bio p`
   scores (0,1,1) and `.bio-lead` only (0,1,0), so a plain `.bio p` rule
   OUTRANKS the lead and silently overrides its font-size, margin and measure —
   the lead then renders as ordinary body copy and the whole device disappears.
   Excluding the lead from the body rule is clearer than escalating specificity
   to win a fight that shouldn't exist. Do not remove the :not(). */
/* No max-width: the lead runs to the full column and takes its measure from
   `.col` (40rem, 44rem past 100rem) — roughly 40 characters a line at display
   size, which is right for a lead. An earlier 22ch cap made it a narrow stripe
   with dead space beside it. */
.bio-lead {
  margin: 0 0 1.75rem;
  font-size: clamp(1.4rem, 4.2vw, 2rem);
  line-height: 1.22;
  letter-spacing: -.02em;
  font-weight: 600;
  text-wrap: balance;
}

/* Body copy keeps a reading measure — 60ch sits inside the 45–75 character
   band and still fills most of the column, rather than leaving a gutter. */
.bio p:not(.bio-lead) {
  margin: 0 0 1.25rem;
  max-width: 60ch;
  font-size: 1.0625rem;
}

/* Applies to the lead too, when the bio is a single unsplit sentence — a
   trailing margin on the last paragraph is unwanted either way. */
.bio p:last-child { margin-bottom: 0; }

/* --- Links -------------------------------------------------------------- */

a { color: var(--accent); }

a:hover { color: var(--ink); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  padding: .75rem 1rem;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  font-size: .8125rem;
}
.skip:focus {
  left: .5rem;
  top: .5rem;
}

/* --- Shows / credits lists ---------------------------------------------- */

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .15rem 1rem;
  padding: .95rem 0;
  border-bottom: 1px solid var(--rule);
  min-width: 0;
}

.row-date {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: .8125rem;
  letter-spacing: .04em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/*  6rem, not 12rem. A 12rem basis plus the date and the tag overflows a 350px
    mobile column, which pushed the tag ("showcase") onto its own line. At 6rem
    everything sits on one line and the body still absorbs the leftover space,
    so long venue names wrap inside the body instead of displacing the tag. */
.row-body {
  flex: 1 1 6rem;
  min-width: 0;
}

.row-venue {
  display: block;
  font-weight: 600;
  overflow-wrap: anywhere;   /* long venue names must not overflow at 360px */
}

/*  The meta line under the billing: venue and city when a show is the draw,
    or city and "produced by X" when the room is. One line, so the separators
    lib.js emits sit between inline spans rather than between blocks. */
.row-meta {
  display: block;
  font-family: var(--mono);
  font-size: .8125rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.row-at,
.row-city,
.row-prod { display: inline; }

/*  A date whose room is not settled yet. Set in the muted ink rather than the
    body ink so it reads as a placeholder rather than as the name of a venue
    called "To Be Announced". */
.row-venue.is-tba {
  color: var(--muted);
  font-weight: 400;
  font-style: italic;
}

/*  Kept as a standalone rule as well: a row with no producer puts the city
    straight in the meta line, and the strip's table reuses the class. */
.row-city {
  font-family: var(--mono);
  font-size: .8125rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.row-cta {
  flex: 0 0 auto;
  margin-left: .85rem;
  font-family: var(--mono);
  font-size: .8125rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 2px;
}
.row-cta:hover { border-bottom-color: var(--ink); }

/* margin-left:auto keeps the tag (and any Tickets link after it) hard right on
   the entry, whatever the venue name does. */
.row-tag {
  flex: 0 0 auto;
  margin-left: auto;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- Counter + stats ---------------------------------------------------- */

/* Number and label sit side by side on a shared baseline, not stacked.
   flex-wrap is the safety net if the count ever grows to four digits on a
   narrow screen. */
.counter {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 .75rem;
}

/* Proportional figures deliberately: tabular-nums gives every digit the width
   of a 0, which is right in an aligning column but makes a display-size number
   look loose and gappy. Reserved for .row-date, which really is a column. */
.counter-num {
  font-size: clamp(4.5rem, 24vw, 11rem);
  line-height: .82;
  font-weight: 700;
  letter-spacing: -.05em;
}

.counter-cap {
  font-family: var(--mono);
  font-size: clamp(.875rem, 2.6vw, 1.375rem);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
}

/*  The second headline fact — how long, beside how often. Half the size of the
    count, on the same baseline, so it reads as the subordinate of the pair
    rather than as a competing number. Its own baseline group keeps the label
    tied to it when the line wraps on a narrow screen. */
.counter-minutes {
  display: inline-flex;
  align-items: baseline;
  gap: .5rem;
  margin-left: .25rem;
}

.counter-min-num {
  font-size: clamp(2.25rem, 12vw, 5.5rem);   /* exactly half of .counter-num */
  line-height: .82;
  font-weight: 700;
  letter-spacing: -.04em;
  color: var(--muted);
}

.counter-minutes .counter-cap {
  font-size: clamp(.6875rem, 1.9vw, 1rem);
}

/* Start date, directly under the count it gives context to. */
.counter-since {
  margin: .75rem 0 0;
  font-family: var(--mono);
  font-size: .8125rem;
  letter-spacing: .08em;
  color: var(--muted);
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: 1.75rem 1rem;
  margin-top: clamp(2rem, 6dvh, 3.5rem);
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
}

.stat { min-width: 0; }

.stat-num {
  display: block;
  font-family: var(--mono);
  font-size: clamp(1.5rem, 6vw, 2.125rem);
  font-weight: 600;
  line-height: 1;
}

.stat-cap {
  display: block;
  margin-top: .5rem;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The "(Jul 8, 2026)" / "(Apr & Jul 2026)" line under a record. */
.stat-note {
  display: block;
  margin-top: .3rem;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .04em;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* Records sit closer to the stats row above them than that row does to the
   counter — they read as a continuation, not a new block. */
.records {
  margin-top: clamp(1.5rem, 4dvh, 2.25rem);
}

/* Private work, aggregate only. Set quieter than the rows above it: this is a
   reconciling footnote, not a headline. */
.private {
  margin-top: clamp(1.25rem, 3dvh, 1.75rem);
}

.private .stat-num {
  font-size: clamp(1.125rem, 4vw, 1.5rem);
  color: var(--muted);
}

/* --- The run: one bar per set, height = minutes ------------------------- */
/*
   One series, so one hue and no legend. Marks are capped rather than filling
   their slot, so the band keeps air. A 2px gap does the separating — never a
   border drawn round a mark. No label rides the bars: the "Longest set" tile
   directly above already carries the extreme, and a number on every bar goes
   unread. The <details> table is the accessible twin, so no value is reachable
   only through a hover tooltip.

   Private entries are excluded here — data.js logs them for stats but they are
   never displayed publicly, and the table below would otherwise name them.
*/
.strip-fig {
  margin: clamp(2.25rem, 6dvh, 3.25rem) 0 0;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
}

.strip {
  display: flex;
  align-items: flex-end;
  gap: 2px;                       /* surface gap, not a stroke */
  height: 132px;
  border-bottom: 1px solid var(--rule);   /* hairline, solid, never dashed */
}

.tick {
  flex: 1 1 0;
  min-width: 3px;
  max-width: 11px;                /* cap the mark; leftover band is air */
  background: var(--mark);
  border-radius: 2px 2px 0 0;     /* rounded data-end, square at the baseline */
}

.tick.is-upcoming {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--mark);
}

/*  A week she did not perform. It still occupies its slot in the band — that
    is the whole point of bucketing by calendar week rather than by set — but
    draws nothing. The gap IS the data. */
.tick.is-empty {
  background: none;
  box-shadow: none;
}

.strip-axis {
  display: flex;
  justify-content: space-between;
  margin-top: .6rem;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .06em;
  color: var(--muted);
}

.strip-note {
  margin: .9rem 0 0;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .04em;
  color: var(--muted);
}

.tableview { margin-top: 1rem; font-family: var(--mono); font-size: .75rem; }

.tableview summary {
  color: var(--muted);
  cursor: pointer;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.tableview table {
  border-collapse: collapse;
  margin-top: .75rem;
  width: 100%;
}

.tableview th,
.tableview td {
  text-align: left;
  padding: .3rem .75rem .3rem 0;
  border-bottom: 1px solid var(--rule);
  font-weight: 400;
  color: var(--muted);
}

.tableview th { color: var(--ink); }

/*  Four columns still outrun a 320px text column at the narrowest phone width.
    The TABLE scrolls inside this box; the page itself never scrolls sideways,
    which is the rule that matters. */
.tablescroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.tablescroll table { min-width: 22rem; }

/*  The date and the minute count are both columns of figures that want to line
    up; the producer and format columns are words and do not. */
.tableview td.date { white-space: nowrap; font-variant-numeric: tabular-nums; }

.tableview th.num,
.tableview td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  padding-right: 0;
}

/*  A night with no producer falls back to the venue name. Dimmed, because it
    is answering a question the column did not quite ask. */
.tableview td.stand-in { opacity: .6; }
.tableview td:last-child { font-variant-numeric: tabular-nums; }  /* a real column */

/* Explains the minute counts; sits under the table, inside the disclosure. */
.table-note {
  margin: .9rem 0 0;
  font-family: var(--mono);
  font-size: .6875rem;
  line-height: 1.55;
  letter-spacing: .03em;
  color: var(--muted);
  max-width: 62ch;
}

/* --- Clips -------------------------------------------------------------- */

.clips { display: grid; gap: 2rem; }

.clip { margin: 0; }

.clip-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  background: rgba(var(--scrim), .6);
  border: 1px solid var(--rule);
  overflow: hidden;
}

.clip-frame iframe,
.clip-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/*  Facade. The thumbnail is a plain <img> and the play button is a real
    <button>, so it is focusable and operable from the keyboard. No iframe
    exists until it is pressed — a visitor who never watches a clip never
    talks to YouTube at all. */
.clip-play {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  display: block;
}

.clip-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.clip-play-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 3.5rem;
  height: 3.5rem;
  display: grid;
  place-items: center;
  font-size: 1.25rem;
  color: var(--ink);
  background: rgba(var(--scrim), .72);
  border: 1px solid var(--rule);
  border-radius: 50%;
  padding-left: .18em;   /* optical centring — the glyph's mass sits left */
}

.clip-play:hover .clip-play-icon,
.clip-play:focus-visible .clip-play-icon { background: rgba(var(--scrim), .9); }

.clip-title {
  margin-top: .75rem;
  font-family: var(--mono);
  font-size: .8125rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* --- Socials + footer --------------------------------------------------- */

.socials {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 2rem;
}

.socials a,
.socials span {
  font-family: var(--mono);
  font-size: .875rem;
  letter-spacing: .06em;
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 3px;
  overflow-wrap: anywhere;
}
.socials a:hover { border-bottom-color: var(--accent); }
.socials span { color: var(--muted); }

#site-footer {
  padding-top: clamp(3rem, 10dvh, 6rem);
  padding-bottom: clamp(4rem, 14dvh, 8rem);
}

.booking-label {
  display: block;
  font-family: var(--mono);
  font-size: .75rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .6rem;
}

.booking-link {
  font-size: clamp(1.05rem, 4.5vw, 1.5rem);
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 3px;
  overflow-wrap: anywhere;
}
.booking-link:hover { border-bottom-color: var(--ink); }

.copyright {
  margin: 2.5rem 0 0;
  font-family: var(--mono);
  font-size: .75rem;
  letter-spacing: .08em;
  color: var(--muted);
}

/*  One text link to the second page. No nav bar, no dropdown.
    In the home page footer it sits below the booking address and above the
    copyright, with a rule separating it from everything above. */
.pagelink {
  padding-top: clamp(2rem, 7dvh, 4rem);
}

#site-footer .pagelink {
  margin-top: clamp(2.5rem, 8dvh, 4rem);
  padding-top: clamp(1.75rem, 5dvh, 2.5rem);
  border-top: 1px solid var(--rule);
}

.pagelink a {
  font-family: var(--mono);
  font-size: .8125rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 3px;
}
.pagelink a:hover { border-bottom-color: var(--ink); }

/*  Scroll indicator. Rendered by main.js only when the page actually
    overflows, and removed permanently the first time it moves. Not rendered
    at all under reduced motion, so it can never appear and sit there
    unresolved. */
.scrollhint {
  position: fixed;
  left: 50%;
  bottom: 1.6rem;
  width: 1px;
  height: 2.25rem;
  margin-left: -.5px;
  background: linear-gradient(to bottom, transparent, var(--muted));
  opacity: .45;
  pointer-events: none;
  z-index: 3;
  transition: opacity .5s ease;
}

.scrollhint.is-gone { opacity: 0; }

.noscript { padding-top: 2rem; padding-bottom: 4rem; }

/* --- Reveal ------------------------------------------------------------- */
/*
   .js-anim is added by main.js. Without it — JS disabled, JS error, or
   reduced-motion — .reveal never gets a hidden state, so content is always
   visible. The hidden state is opt-in, never the default.
*/
.js-anim .reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition: opacity .7s ease, transform .7s cubic-bezier(.22, .61, .36, 1);
}

.js-anim .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js-anim .reveal,
  .js-anim .reveal.is-visible {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Wide viewports -----------------------------------------------------

   Note the deliberate mismatch with index.html, which swaps to the landscape
   photo at 48rem while the two-column layout only starts here at 60rem.

   At 48rem the two-column layout would leave the text just ~322px wide, which
   is too cramped to read. So 768-960px keeps the single-column treatment (with
   the strong scrim) but gets the landscape photo, which suits a wide viewport
   better. That combination is contrast-verified: landscape at 74% scrim gives
   7.68:1 body / 5.08:1 muted.                                                */

@media (min-width: 60rem) {

  :root { --pad: 0rem; }

  /*  DESKTOP: transparent over the performer on the left, ramping to dark
      beneath the text column. The ramp reaches .62 at 50% — before the column
      starts at 52% — which is what makes the measured 62% the true worst case
      for every pixel of text.

      The scroll-driven morph is mobile-only: desktop already shows the photo
      clear on the left half, so there is nothing to reveal. `.js-dim .scrim`
      is listed alongside because it is (0,2,0) and would otherwise outrank
      this rule and reinstate the vertical mobile gradient here. */
  .scrim,
  .js-dim .scrim {
    opacity: 1;
    background: linear-gradient(
      to right,
      rgba(var(--scrim), 0)   0%,
      rgba(var(--scrim), .05) 26%,
      rgba(var(--scrim), .32) 40%,
      rgba(var(--scrim), .62) 50%,
      rgba(var(--scrim), .70) 62%,
      rgba(var(--scrim), .74) 100%
    );
  }

  .page {
    display: grid;
    grid-template-columns: var(--col-start) minmax(0, 1fr);
  }

  /* minmax(0,1fr) above plus min-width:0 here prevents grid blowout. */
  .page > * {
    grid-column: 2;
    min-width: 0;
    padding-right: clamp(2rem, 6vw, 7rem);
    padding-left: 0;
  }

  .col { max-width: 40rem; }

  .clips { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
}

@media (min-width: 100rem) {
  .col { max-width: 44rem; }
}

/* --- Pages without the hero photograph ----------------------------------
   /mentors/ and /404.html. The fixed photograph is a home-page treatment; a
   column of portraits would compete with it, so these pages are plain black
   with the same type scale.

   Computed against #000 rather than assumed (see README "Contrast"):
   ink 18.76:1, muted 12.41:1, accent 15.24:1, marks 12.87:1. Every value
   improves on the scrimmed figures, so nothing here can fall below AA.       */

.no-hero { background-color: #000; }

.no-hero .page > * {
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.page-head {
  padding-top: clamp(3.5rem, 12dvh, 7rem);
  padding-bottom: clamp(1.5rem, 5dvh, 3rem);
}

.page-title {
  margin: 0 0 .4rem;
  font-size: clamp(2.5rem, 12vw, 4.5rem);
  line-height: .95;
  letter-spacing: -.02em;
  font-weight: 700;
}

/* --- Mentors ------------------------------------------------------------- */

.mentors {
  display: flex;
  flex-direction: column;
  gap: clamp(3rem, 10dvh, 5.5rem);
}

.mentor { margin: 0; }

.mentor-figure { margin: 0 0 1.25rem; }

/*  One crop and one treatment for every portrait, so sources of mixed quality
    and framing read as a deliberate set rather than as whatever was to hand.
    Fixed aspect ratio + cover does the cropping; grayscale does the rest. */
.mentor-photo {
  display: block;
  width: 100%;
  max-width: 22rem;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center 20%;   /* faces sit above centre in most portraits */
  filter: grayscale(1) contrast(1.05);
  background-color: rgba(var(--scrim), .6);
}

.mentor-credit {
  margin: .5rem 0 0;
  max-width: 22rem;
  font-family: var(--mono);
  font-size: .6875rem;
  letter-spacing: .06em;
  color: var(--muted);
}

.mentor-name {
  margin: 0 0 .6rem;
  font-size: clamp(1.5rem, 6vw, 2rem);
  line-height: 1.1;
  letter-spacing: -.01em;
}

.mentor-blurb {
  margin: 0 0 1rem;
  max-width: 46ch;
}

.mentor-socials { margin-top: .75rem; }

.mentor-empty {
  color: var(--muted);
  font-family: var(--mono);
  font-size: .875rem;
}

.notfound-links { padding-top: 0; }

@media (min-width: 60rem) {
  /*  These pages opt out of the two-column grid entirely — there is no photo
      on the left to leave room for. (0,2,0) beats the `.page > *` rule above. */
  .no-hero .page { display: block; }

  .no-hero .page > * {
    grid-column: auto;
    padding-left: clamp(2rem, 6vw, 5rem);
    padding-right: clamp(2rem, 6vw, 5rem);
  }

  .no-hero .col { margin-left: 0; }

  /*  Portrait beside the words once there is room for both. */
  .mentor {
    display: grid;
    grid-template-columns: 16rem minmax(0, 1fr);
    gap: 2.5rem;
    align-items: start;
  }

  .mentor-figure { margin-bottom: 0; }
  .mentor-photo { max-width: none; }
  .mentor-credit { max-width: none; }
}
