/* ==========================================================================
   Insights — /insights/            (2026-08-06)
   ==========================================================================

   The black/gold design language: same tokens as frontpage.css, voices.css,
   in-the-media.css and events-hub.css. Keep the four in step.

   SHAPE. A hub of rows, one per kind of insight, each with its own View More
   into the full archive. That is the /voices/ row exactly — label rail on the
   left, carousel on the right — and it is deliberate: Voices becomes a third
   row on this page, and it should arrive as data, not as a new layout.

   CARD. The In the Media / Past Events card, not the Voices one: body above,
   image below, on white. Voices cards print their caption OVER the poster
   because a video poster IS the content; here the image is a cover or an event
   photo and the title is the content, and a title averaging 70 characters
   cannot live in a two-line overlay. Verified against in-the-media.css so the
   two read as the same card.

   IMAGE RATIO IS 3:2. The old page cropped everything to portrait, which
   sliced the headline off half the event banners (the ESG Forum and CASI cards
   were unreadable). Measured all 22 source images: 20 are landscape, 1.34 to
   2.0, median 1.5. 3:2 is that median; 16:9 would crop the 1.39 group harder.

   Scope `.ins` + !important throughout — Phlox ships 69+ stylesheets.
   ========================================================================== */

/* .ins-lb is appended to <body>, OUTSIDE .ins — custom properties do not reach
   it by cascade, so it has to carry the tokens itself or --ins-gold resolves to
   nothing and the close button loses its hover colour. */
.ins,
.ins-band,
.ins-lb {
  --ins-gold:      #B89642;
  --ins-ink:       #14171F;
  --ins-ink-soft:  #4A4E58;
  --ins-mute:      #8A8F99;
  --ins-bg:        #EFEFEF;
  --ins-card:      #FFFFFF;
  --ins-line:      #D8D8D8;
  --ins-ease:      cubic-bezier(.22,.9,.3,1);
}

.ins {
  display: block !important;
  position: relative !important;
  font-family: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
  color: var(--ins-ink) !important;
  padding-bottom: 20px !important;
}

/* The grey ground runs the full width of the window, not of the measure. */
.ins::before {
  content: "" !important;
  position: absolute !important;
  left: 50% !important;
  width: 100vw !important;
  margin-left: -50vw !important;
  top: 0 !important;
  bottom: 0 !important;
  background: var(--ins-bg) !important;
  z-index: -1 !important;
}

/* ==========================================================================
   Title band
   ==========================================================================
   Server-rendered in the page content, so it paints with the first frame and
   the page never shows a white gap where a black band belongs. Unstyled it
   degrades to a plain heading on white, which is survivable — the warning in
   WEB_WORKFLOW.md is about markup that misbehaves unstyled.
   ========================================================================== */
/* 🚨 The band does NOT escape the measure — its BACKGROUND does.
   The other new-language pages set the band itself to `width: 100vw; left:
   50%; margin-left: -50vw` and then re-centre the title with `max-width:
   1440px`. That only lines up on a window at least 1510 wide: the theme wraps
   every page in `.aux-container { max-width: 1510px; padding: 0 35px }`, so
   below 1510 the content narrows with the window while a 1440 band inner does
   not — and between 1440 and 1510 the title slides out of the gutter and ends
   up flush against the edge of the screen. /in-the-media/, /past-events/ and
   /about/ all do that today at exactly 1440, which is not a rare width.

   Leaving the band inside the shell and painting the black with a pseudo-
   element removes the arithmetic entirely: the title inherits whatever measure
   the theme is giving the page, so it lands on the same left edge as the first
   card at every width, with no magic numbers to keep in step. */
.ins-band {
  position: relative !important;
  color: #fff !important;
}
.ins-band::before {
  content: "" !important;
  position: absolute !important;
  top: 0 !important;
  bottom: 0 !important;
  left: 50% !important;
  width: 100vw !important;
  margin-left: -50vw !important;
  background: #000 !important;
}
/* 48px of side padding: the same arrow gutter the rows carry, so the band
   title sits on the same left edge as the row labels rather than 48px left of
   them. Same device as voices.css. */
.ins-band__inner {
  position: relative !important;
  padding: 64px 48px 60px !important;
}
.ins-band__title {
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: clamp(34px, 3.4vw, 52px) !important;
  font-weight: 700 !important;
  line-height: 1.06 !important;
  letter-spacing: .01em !important;
  text-transform: uppercase !important;
  color: #fff !important;
  margin: 0 !important;
}

/* ==========================================================================
   Row — label rail + carousel
   ========================================================================== */
/* 🚨 THE RAIL WIDTH IS SOLVED FROM THE LABEL, at every breakpoint.
   "PERSPECTIVES" is the longest label and it is one word, so it cannot wrap —
   give it a column narrower than itself and it breaks mid-word into
   "PERSPECTIVE / S", which is what a 200px column did. Measured: it sets
   **7.3px wide per px of font-size** (218px at 30px), so the pairs below are
   label + ~15px of slack:

     40px -> 291  rail 300      34px -> 248  rail 262
     37px -> 270  rail 285      28px -> 204  rail 215

   Re-measure if a longer label is ever added — Voices is the one coming. */
/* 🚨 THE RAIL IS PROPORTIONAL, and it has to be, now that the measure is.
   (Chase, 2026-08-07: "insights明显是中间间隔空间太大了")

   It used to be a fixed 300px sized for the longest label, plus a fixed 72px
   gap. Against the old 1440 measure that was 372px of furniture — 26%. Against
   a 1037 measure at a 1440 window it is 32%, and because the column is sized
   for "PERSPECTIVES" while the row above it says "REPORTS", most of it is
   visibly empty: 180px of nothing between the View More button and the first
   card. The measure shrank; the furniture did not.

   16vw is the front page's own `--fp-side-w` in frontpage.css, on `.fp-side`,
   which is this exact grid.

   🚨 The GAP is 6vw and is now SITE-WIDE — the same value in events-hub.css,
   in-the-media.css, archive.css, voices.css and newsletter.css (Chase,
   2026-08-11: "现在每个页面的这个gap是否可以设置都差不多的距离，例如6vw之类"). It was
   max(52px, 4vw) here. Change one, change all six.

   The 52px floor it used to carry was load-bearing and is now covered by the
   raw value: the prev arrow lives in this gap and needs 48px (38px button +
   10px offset), and 6vw only ever applies above the 1024 breakpoint where the
   rail collapses — so its minimum is 6% of 1025, i.e. 61px. It clears. Drop
   below ~5.4vw and the arrow lands on the label.
   ========================================================================== */
.ins {
  --ins-rail:   16vw;
  --ins-colgap: 6vw;
}
.ins-row {
  display: grid !important;
  grid-template-columns: var(--ins-rail) minmax(0, 1fr) !important;
  gap: 0 var(--ins-colgap) !important;
  align-items: start !important;
  padding: 54px 48px !important;   /* the side padding is where the arrows live */
}
.ins-row + .ins-row { border-top: 1px solid var(--ins-line) !important; }
/* a row whose source returns nothing hides itself rather than printing an
   empty strip — see the note on the same rule in voices.css */
.ins .ins-row[hidden] { display: none !important; }

.ins-row__head { padding-top: 2px !important; }
.ins-row__title {
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  /* 🚨 SOLVED AGAINST THE RAIL, not chosen. "PERSPECTIVES" is the longest label
     and is ONE WORD, so it cannot wrap — give it a column narrower than itself
     and it breaks into "PERSPECTIVE / S". It measures **7.3px wide per px of
     font-size**, so the type has to scale with the rail or the two drift apart
     at every width but the one they were tuned at.

     2.05vw against a 16vw rail leaves 7.3 × 2.05 = 15.0vw of label in 16vw of
     column — a constant 1vw of slack at every width, by construction rather
     than by a table of measured pairs. Tightest real case is a 1366 window:
     label 204px in a 219px rail. The 40px cap is the old desktop size, so
     nothing above 1950 changes; the 20px floor is where the rail collapses
     anyway. Re-check the 2.05 if a longer label is ever added. */
  font-size: clamp(20px, 2.05vw, 40px) !important;
  font-weight: 700 !important;
  line-height: 1.08 !important;
  letter-spacing: .01em !important;
  text-transform: uppercase !important;
  color: var(--ins-ink) !important;
  margin: 0 0 16px !important;
}
/* An <a>, not a <button>. The row label has a real destination — /reports/ and
   /perspectives/ — and those two links are the only way off this page into the
   archives. On the live page today they exist in the markup and render at
   x=-260, i.e. off the left of the window, so nobody can reach them. */
.ins-row__more {
  display: inline-flex !important;
  align-items: center !important;
  gap: 9px !important;
  background: var(--ins-ink) !important;
  color: #fff !important;
  text-decoration: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  padding: 10px 18px !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: 12.5px !important;
  font-weight: 600 !important;
  transition: background .22s var(--ins-ease) !important;
}
.ins-row__more::after { content: "\2192" !important; }
.ins-row__more:hover { background: var(--ins-gold) !important; color: #fff !important; }
.ins-row__more:focus-visible { outline: 2px solid var(--ins-gold) !important; outline-offset: 3px !important; }

/* 🚨 IT WRAPS ON PURPOSE, AND IT IS NOT MEANT TO FILL THE SPAN.
   Chase, 2026-08-27: "之前有跟你说过这行小字要一行显示，现在他们觉得有些时候可以
   换行，你要不还是给我设置成换行的吧，中间留点空隙，占满不太好". This reverses the
   2026-08-21 "所有的小字都保持一行" instruction — deliberately, and after that
   one had already been half-lost to physics: the old cap was 22vw, which is
   422px at 1920 and only 225px at 1025, so the standfirsts were wrapping below
   ~1800 anyway. What changed is that wrapping is now the intended state
   everywhere rather than the failure mode at small widths.

   The cap is `min(22vw, 330px)`. On a wide screen 330 of the 422px span is
   used and the remaining ~92px is the gap Chase asked for — the text stops
   short of the carousel instead of running up to it. Below about 1500 the 22vw
   term is the smaller one and takes over, so nothing is made narrower than it
   already was; the min() is only ever removing width, never adding it.

   🚨 22vw, not the 16vw rail. The intro is NOT confined to the rail: the rail
   is 16vw, --ins-colgap is another 6vw of white beside it, and the intro sits
   ABOVE the carousel arrows (measured at 1920: intro bottom 437, arrow top 506)
   so it can use both without colliding. Capping it at the rail alone would cut
   it to 164px at 1025 and run Marketing's copy to five lines.

   No `text-overflow: ellipsis`, then or now: an ellipsis holds a line by
   CUTTING the words, and a standfirst that fails silently is worse than one
   that wraps.

   Below 1024 the rail and the gutter collapse to 0 and the intro spans the full
   row instead; the mobile blocks handle that. */
.ins-row__intro {
  font-size: 13.5px !important;
  line-height: 1.7 !important;
  color: var(--ins-ink-soft) !important;
  margin: 0 0 18px !important;
  /* 🚨 `width: max-content` STAYS, even though the point is now to wrap. It is
     what lets the box spill out of the 16vw rail into the gutter beside it;
     without it the max-width below is measured against the rail alone, which is
     164px at 1025 and put Marketing's copy on three lines. Measured both ways
     before settling on this. */
  width: max-content !important;
  max-width: min(calc(var(--ins-rail) + var(--ins-colgap)), 330px) !important;
}

/* ==========================================================================
   Carousel
   ========================================================================== */
.ins-carousel { position: relative !important; }
/* The padding/negative-margin pair reserves room ABOVE the cards inside the
   clip box for the album stack slivers (.ins-card--stack), without moving a
   single card: overflow clips at the PADDING box, so the padding area is
   drawable, and the equal negative margin puts the row back where it was.
   overflow-x: hidden with overflow-y: visible is not an option — CSS promotes
   the visible axis to auto, which clips just the same. Mirrors voices.css. */
.ins-track-wrap {
  position: relative !important;
  overflow: hidden !important;
  padding-top: 12px !important;
  margin-top: -12px !important;
}
.ins-track {
  display: flex !important;
  align-items: stretch !important;
  gap: var(--ins-gap, 24px) !important;
  transition: transform .5s var(--ins-ease) !important;
  will-change: transform !important;
}

/* 🚨 The card width is DERIVED from how many cards the row shows, not fixed —
   the same rule as voices.css, and for the same reason: a fixed width leaves
   whatever does not divide evenly sliced in half at the right edge of the
   frame, because the frame clips its overflow. --ins-per is set per row by the
   JS and re-set at each breakpoint below; the paging moves in whole cards so a
   row is never at rest showing part of one. */
.ins-card {
  flex: 0 0 calc((100% - (var(--ins-per, 3) - 1) * var(--ins-gap, 24px)) / var(--ins-per, 3)) !important;
  /* 🚨 A FLEX ITEM DEFAULTS TO min-width:auto, WHICH IS ITS MIN-CONTENT WIDTH —
     so a card holding one word too long to fit simply refuses to shrink to the
     basis above, and every card after it in the strip is pushed out of step.
     The row then rests a fraction of a card off and clips a sliver at both ends
     of the frame; it looks like the carousel has lost its place, not like a
     text problem, and it appears at some window widths and not others because
     it depends on whether that word happens to be wider than the solved card.

     Found 2026-08-12 on the Press row of /news/: page 18068's title runs two
     words together — "to strengthenphilanthropy leadership" — giving a
     22-character token that forced its card from 214px to 287px at 2048.
     The same fault was live on /insights/ Perspectives at 2560, where the
     honest words "Implementations", "Interdisciplinary" and "Transformation:"
     were enough on their own.

     min-width:0 lets the item obey its basis; overflow-wrap on the title makes
     the long word break rather than spill out of the narrower card. Both are
     needed — the first fixes the strip, the second fixes the text in it. */
  min-width: 0 !important;
  display: flex !important;
  flex-direction: column !important;
  background: var(--ins-card) !important;
  color: inherit !important;
  text-decoration: none !important;
  border-radius: 0 !important;
  position: relative !important;
}
.ins-card:focus-visible { outline: 2px solid var(--ins-gold) !important; outline-offset: 3px !important; }

/* ---------- album stack ----------------------------------------------------
   The /voices/ playlist tile, brought to this row (Chase, 2026-08-21): two
   slivers above the card read as the edges of the covers behind it, so one
   card standing for seven episodes looks like it. Same greys as voices.css —
   they are picked against the #EFEFEF page, not against the card, which is
   white here and near-black there; a lighter pair vanished into the page.

   .ins-card sets no overflow, so unlike /voices/ nothing has to be turned off;
   the room comes from .ins-track-wrap's padding above. z-index: -1 tucks the
   hidden half of each sliver under the card it belongs to.
   -------------------------------------------------------------------------- */
.ins-card--stack::before,
.ins-card--stack::after {
  content: "" !important;
  position: absolute !important;
  left: 0 !important; right: 0 !important;
  height: 10px !important;
  border-radius: 3px 3px 0 0 !important;
  pointer-events: none !important;
  z-index: -1 !important;
}
.ins-card--stack::before {
  top: -4px !important;
  margin: 0 6px !important;
  background: #8A8F99 !important;
}
.ins-card--stack::after {
  top: -8px !important;
  margin: 0 14px !important;
  background: #A8ADB6 !important;
}

/* the count sits on the poster, where the play affordance already is */
.ins-card__badge {
  position: absolute !important;
  right: 8px !important;
  bottom: 8px !important;
  display: inline-flex !important;
  align-items: center !important;
  gap: 5px !important;
  padding: 4px 8px !important;
  background: rgba(0,0,0,.72) !important;
  color: #fff !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: 9px !important;
  font-weight: 600 !important;
  letter-spacing: .1em !important;
  text-transform: uppercase !important;
  line-height: 1 !important;
  pointer-events: none !important;
  z-index: 2 !important;
}
.ins-card__badge::before {
  content: "" !important;
  width: 0 !important; height: 0 !important;
  border-left: 6px solid #fff !important;
  border-top: 4px solid transparent !important;
  border-bottom: 4px solid transparent !important;
}

/* The row loops by holding the list three times over, so translating by one
   copy repaints the same picture and the wrap is invisible (see insights.js).
   The two outer copies are clones. A row that already shows everything it has
   does not loop, and its clones must then not be there at all — otherwise the
   strip silently doubles.

   Scoped to (0,4,0) on purpose: `.ins-card` sets `display: flex !important`
   and !important on both sides does not break a tie — specificity does. Same
   trap as `.itm-item[hidden]` in in-the-media.css. */
.ins .ins-track:not(.ins-track--loop) .ins-card--clone { display: none !important; }

/* Body above, image below — as on Past Events and In the Media. */
.ins-body {
  order: 1 !important;
  display: block !important;
  padding: 16px 18px 14px !important;
}
/* The ratio is per ROW, not global — Voices sets 4/5 because its posters are a
   different shape from a report cover. The JS writes --ins-ratio on the track. */
.ins-media {
  order: 2 !important;
  display: block !important;
  position: relative !important;
  overflow: hidden !important;
  background: #E3E3E3 !important;
  aspect-ratio: var(--ins-ratio, 3 / 2) !important;
}
.ins-media img {
  display: block !important;
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
  /* which part of the poster survives the crop — per row, because it depends on
     where the artwork puts its words. See the note in insights.js. */
  object-position: var(--ins-pos, center) !important;
  transition: transform .6s var(--ins-ease) !important;
}
.ins-card:hover .ins-media img { transform: scale(1.06) !important; }

/* NOTHING IN THE VOICES ROW IS CROPPED — see the poster-shape note in
   insights.js. Every poster is fitted whole and its margins are filled with a
   blurred, dimmed copy of itself. Same file, so no extra request, and
   `scale(1.18)` keeps the blur's own soft edge outside the frame.

   🚨 `object-position: center` here overrides whatever `--ins-pos` the row
   sets, because object-position applies to `contain` as well as `cover`: an
   anchor is meaningful for a crop and wrong for a fit, and the row's old
   `left bottom` was shoving the fitted stills into the corner with the whole
   black slab above them. */
.ins-media--fit { background: #111 !important; }
.ins-media--fit > img:not(.ins-media__bg) {
  object-fit: contain !important;
  object-position: center !important;
  position: relative !important;
  z-index: 1 !important;
}
.ins-card:hover .ins-media--fit > img:not(.ins-media__bg) { transform: none !important; }

/* 🚨 `.ins-media img.ins-media__bg`, not `.ins-media__bg`. The generic
   `.ins-media img` rule above is (0,1,1) and sets object-position from the row's
   crop anchor; a bare `.ins-media__bg` is (0,1,0) and LOSES the tie — !important
   on both sides does not break it, specificity does. Written unscoped, the
   blurred backdrop inherits `left bottom` and smears off-centre. Same trap as
   `.itm-item[hidden]` in in-the-media.css. */
.ins-media img.ins-media__bg {
  position: absolute !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
  object-position: center !important;
  filter: blur(16px) brightness(.6) saturate(1.15) !important;
  /* held on hover too — dropping the scale would expose the blur's soft edge */
  transform: scale(1.18) !important;
  z-index: 0 !important;
  pointer-events: none !important;
}
.ins-card:hover .ins-media img.ins-media__bg { transform: scale(1.18) !important; }

/* A still frame has to say it is a film. Same mark as the /voices/ cards. */
.ins-play {
  position: absolute !important;
  top: 10px !important;
  left: 10px !important;
  width: 28px !important;
  height: 28px !important;
  border-radius: 50% !important;
  background: rgba(0,0,0,.55) !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  pointer-events: none !important;
  z-index: 2 !important;
}
.ins-play::after {
  content: "" !important;
  border-left: 8px solid #fff !important;
  border-top: 5.5px solid transparent !important;
  border-bottom: 5.5px solid transparent !important;
  margin-left: 3px !important;
}

.ins-meta {
  display: flex !important;
  align-items: center !important;
  justify-content: space-between !important;
  gap: 10px !important;
  margin-bottom: 12px !important;
  min-height: 22px !important;   /* reserved: not every card carries a kind mark */
}

/* A bordered classification in the slot In the Media gives the outlet. It says
   the click leaves the site for a file rather than opening a page — seven of
   the nine reports are PDFs, two of them on someone else's server. */
.ins-kind {
  display: inline-block !important;
  border: 1px solid var(--ins-ink) !important;
  padding: 3px 6px !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: 8px !important;
  font-weight: 600 !important;
  letter-spacing: .1em !important;
  text-transform: uppercase !important;
  line-height: 1.4 !important;
  color: var(--ins-ink) !important;
  white-space: nowrap !important;
}
.ins-kind[hidden] { display: none !important; }

/* The year is printed, unlike In the Media, because these rows run across four
   of them in one strip and there is no year heading to hang the month on. */
.ins-date {
  display: flex !important;
  align-items: baseline !important;
  gap: 5px !important;
  margin-left: auto !important;
  color: var(--ins-gold) !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-variant-numeric: tabular-nums !important;
  white-space: nowrap !important;
}
.ins-date__num { font-size: 22px !important; font-weight: 600 !important; line-height: 1 !important; }
/* the big slot normally holds a numeral, so case never came up; when a
   month-only item puts a month there it is capitalised to sit with the
   uppercase month beside it in the rows above */
.ins-date__num--mo { text-transform: uppercase !important; }
.ins-date__mo  { font-size: 13px !important; font-weight: 500 !important; text-transform: uppercase !important; line-height: 1 !important; }

/* Clamped when longer, reserved when shorter — that is what makes every body
   the same height and every card in a strip line up.

   🚨 SIZE IS archive.css's, DELIBERATELY IDENTICAL (Chase, 2026-08-11: "更新一下
   insights页面，将其大小对应reports"). Same clamp(), same line-height, same base
   line count as `.arc-title` on /reports-2026/, which in turn came off Bunny's
   past-events comp at 6.9% of card width. It was 14.5px/1.35/3 lines here, so
   the two pages showed the same reports at noticeably different sizes. Change
   one, change the other.

   The CARD widths still differ and that is structural, not a setting: this is a
   carousel with 48px arrow gutters (272px card at 1920) and /reports-2026/ is a
   plain 3-column grid (304px). Only the type is shared. */
.ins-title {
  /* `anywhere`, not `break-word`: only `anywhere` also shrinks the element's
     min-content size, which is what the card above is being measured by. */
  overflow-wrap: anywhere !important;
  /* pre-line so the e-Newsletter cards can force their own break after the
     stream word (see the note in insights.js). It collapses runs of spaces
     exactly as `normal` does, so every other title is unaffected — they carry
     no newline to honour. */
  white-space: pre-line !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: clamp(15px, 1.01vw, 22px) !important;
  font-weight: 600 !important;
  line-height: 1.15 !important;
  color: var(--ins-ink) !important;
  margin: 0 !important;
  display: -webkit-box !important;
  -webkit-box-orient: vertical !important;
  -webkit-line-clamp: 3 !important;
  overflow: hidden !important;
  min-height: 3.45em !important;   /* 3 x 1.15 — follows archive.css */
  transition: color .2s var(--ins-ease) !important;
}

/* ⚠️ Voices is the exception, and it is measured rather than assumed. Unclamped
   at this size the three rows run: Reports 1-6 lines, Perspectives 3-7, Voices
   2-4 with 26 of its 30 at two or three. Five lines on Voices would reserve two
   to three empty lines under every one of thirty cards. Rows are independent
   here — only the cards WITHIN a row have to agree — so this costs nothing.
   Re-measure if the copy changes; do not copy the number between rows. */
.ins-row[data-cat="voices"] .ins-title {
  -webkit-line-clamp: 3 !important;
  min-height: 3.45em !important;   /* 3 x 1.15 — the base now matches, so this
                                      override only survives as a hook */
}
.ins-card:hover .ins-title { color: var(--ins-gold) !important; }

/* ---------- arrows ----------

   Lifted from voices.css, including both of its load-bearing details:

   1. `padding: 0` and a (0,3,1) selector. Phlox styles a bare <button> with
      `padding: 20px 35px`, which beat a plain `.ins-arrow` width/height on
      specificity — an absolutely positioned box with an ignored width
      shrink-wraps to its padding, so the button drew as a flat ellipse.
   2. The arrows sit OUTSIDE the frame, in the row's side padding, rather than
      inset over the first and last card. `.ins-row` therefore carries that
      padding and the arrow gutter must stay >= size + gap at every breakpoint
      or the arrow escapes the row and pushes a horizontal scrollbar. */
.ins {
  --ins-arrow-size: 38px;
  --ins-arrow-gap: 10px;
}
.ins .ins-carousel button.ins-arrow {
  position: absolute !important;
  top: calc(50% + 22px) !important;   /* +22: centre on the strip, not on the strip plus its standfirst */
  transform: translateY(-50%) !important;
  z-index: 4 !important;
  box-sizing: border-box !important;
  padding: 0 !important;
  margin: 0 !important;
  width: var(--ins-arrow-size) !important;
  height: var(--ins-arrow-size) !important;
  min-width: 0 !important; max-width: none !important;
  min-height: 0 !important; max-height: none !important;
  border-radius: 50% !important;
  border: 0 !important;
  background: rgba(20,23,31,.82) !important;
  color: #fff !important;
  font-size: 17px !important;
  font-weight: 400 !important;
  letter-spacing: 0 !important;
  line-height: 1 !important;
  text-transform: none !important;
  cursor: pointer !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  transition: background .2s var(--ins-ease), opacity .2s !important;
}
.ins .ins-carousel button.ins-arrow:hover { background: var(--ins-gold) !important; }
.ins .ins-carousel button.ins-arrow:focus-visible { outline: 2px solid var(--ins-gold) !important; outline-offset: 2px !important; }
.ins .ins-carousel button.ins-arrow--prev {
  right: 100% !important; left: auto !important;
  margin-right: var(--ins-arrow-gap) !important;
}
.ins .ins-carousel button.ins-arrow--next {
  left: 100% !important; right: auto !important;
  margin-left: var(--ins-arrow-gap) !important;
}
.ins .ins-carousel button.ins-arrow[disabled] { opacity: .25 !important; cursor: default !important; pointer-events: none !important; }
.ins .ins-carousel button.ins-arrow[hidden] { display: none !important; }

/* ==========================================================================
   Player — the Voices cards play here rather than sending you to /voices/
   ==========================================================================
   Same shape as the /voices/ lightbox so the two read as one player. The
   <video> is created on open and destroyed on close (see insights.js): thirty
   clips must not negotiate connections just because the row is on the page.
   ========================================================================== */
.ins-lb {
  position: fixed !important;
  inset: 0 !important;
  z-index: 100000 !important;
  background: rgba(10,12,16,.92) !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  padding: 40px 20px !important;
}
.ins-lb[hidden] { display: none !important; }
.ins-lb__inner { position: relative !important; max-width: 1100px !important; width: 100% !important; }
.ins-lb__video {
  display: block !important;
  width: 100% !important;
  max-height: 82vh !important;
  background: #000 !important;
  outline: none !important;
}
/* a portrait clip must not be stretched across the 1100px box — the Research
   Digest masters are 1080x1920. The class is set from the clip's own metadata,
   not from the card's frame. */
.ins-lb--portrait .ins-lb__inner { max-width: min(460px, 92vw) !important; }
.ins-lb__cap {
  color: #fff !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: 14px !important;
  font-weight: 600 !important;
  margin: 14px 2px 0 !important;
  line-height: 1.4 !important;
}
.ins-lb__close {
  position: absolute !important;
  top: -42px !important; right: -4px !important;
  width: 34px !important; height: 34px !important;
  padding: 0 !important;
  min-width: 0 !important;
  border: 0 !important;
  background: none !important;
  color: #fff !important;
  font-size: 30px !important;
  line-height: 1 !important;
  cursor: pointer !important;
  opacity: .8 !important;
}
.ins-lb__close:hover { opacity: 1 !important; color: var(--ins-gold) !important; }
.ins-lb__close:focus-visible { outline: 2px solid var(--ins-gold) !important; outline-offset: 2px !important; }
@media (max-width: 767px) {
  .ins-lb__close { top: -38px !important; right: 0 !important; }
}

/* ---------- playlist panel -------------------------------------------------
   Player left, episode list right, current track marked — the /voices/ shape,
   ported 2026-08-21 with the album collapse, because a row that shows one card
   for seven episodes has to give the other six somewhere to be.

   The stage is transparent and the panel hidden until there IS a playlist, so
   a lone video keeps exactly the geometry it had before any of this existed.
   -------------------------------------------------------------------------- */
.ins-lb__stage {
  display: flex !important;
  align-items: flex-start !important;
  justify-content: center !important;
  gap: 18px !important;
  max-width: 100% !important;
  width: 100% !important;
}
/* the player gives the panel room only once there is one */
.ins-lb--has-list .ins-lb__inner { max-width: min(1100px, calc(100% - 318px)) !important; }
.ins-lb--has-list.ins-lb--portrait .ins-lb__inner { max-width: min(460px, 60vw) !important; }
.ins-lb__list {
  flex: 0 0 300px !important;
  max-height: 70vh !important;
  overflow-y: auto !important;
  background: rgba(20,23,31,.94) !important;
  border: 1px solid rgba(255,255,255,.14) !important;
  box-sizing: border-box !important;
}
.ins-lb__list[hidden] { display: none !important; }
.ins-lb__list-head {
  margin: 0 !important;
  padding: 14px 14px 12px !important;
  border-bottom: 1px solid rgba(255,255,255,.14) !important;
  font-family: "Poppins", "Noto Sans", sans-serif !important;
  font-size: 11px !important;
  font-weight: 600 !important;
  letter-spacing: .1em !important;
  text-transform: uppercase !important;
  color: #fff !important;
}
.ins-lb__track {
  display: flex !important;
  align-items: center !important;
  gap: 10px !important;
  width: 100% !important;
  padding: 9px 12px !important;
  background: none !important;
  border: 0 !important;
  border-bottom: 1px solid rgba(255,255,255,.07) !important;
  text-align: left !important;
  cursor: pointer !important;
  transition: background .2s var(--ins-ease) !important;
}
.ins-lb__track:hover { background: rgba(255,255,255,.08) !important; }
.ins-lb__track.is-current { background: rgba(184,150,66,.22) !important; }
.ins-lb__track:focus-visible { outline: 2px solid var(--ins-gold) !important; outline-offset: -2px !important; }
.ins-lb__track-img {
  flex: 0 0 48px !important;
  width: 48px !important;
  height: 48px !important;
  object-fit: cover !important;
  display: block !important;
}
.ins-lb__track-cap {
  font-size: 13px !important;
  line-height: 1.4 !important;
  color: rgba(255,255,255,.86) !important;
  display: -webkit-box !important;
  -webkit-box-orient: vertical !important;
  -webkit-line-clamp: 2 !important;
  overflow: hidden !important;
}
.ins-lb__track.is-current .ins-lb__track-cap { color: #fff !important; font-weight: 600 !important; }

/* On a phone there is no room beside the player: the list goes under it and
   the whole dialog scrolls as one thing. */
@media (max-width: 900px) {
  .ins-lb { align-items: flex-start !important; overflow-y: auto !important; }
  .ins-lb__stage { flex-direction: column !important; align-items: stretch !important; }
  .ins-lb--has-list .ins-lb__inner,
  .ins-lb--has-list.ins-lb--portrait .ins-lb__inner { max-width: 100% !important; }
  .ins-lb__list { flex: none !important; width: 100% !important; max-height: none !important; }
}

/* ==========================================================================
   Status line — only ever seen when a fetch fails
   ========================================================================== */
.ins-status {
  padding: 40px 48px !important;
  font-size: 13px !important;
  color: var(--ins-ink-soft) !important;
}
.ins-status a { color: var(--ins-gold) !important; text-decoration: underline !important; }

/* ==========================================================================
   Responsive
   ========================================================================== */
/* 🚨 ONLY THE COUNT IS STEPPED NOW. The rail, the column gap and the label
   size are all proportional (see the rail block above), so a breakpoint no
   longer has to restate three numbers that must agree — it sets `--ins-per`
   and nothing else. That is the whole reason the old ladder kept going stale:
   it hard-coded a rail width per step, and every change to the measure
   silently invalidated all of them.

   The count is the one thing that cannot be proportional — a card is either
   whole or it is not. The boundaries below are SOLVED, not chosen: for

     frame(w) = 0.72w − 0.16w(rail) − 0.06w(gap) − 96(arrow gutters)
              = 0.50w − 96

   each count holds a 205–320px card over exactly one window range, and the
   ranges overlap, so every width has an answer:

     5 across   2434 – 3584      4 across   1976 – 2896
     3 across   1518 – 2208      2 across   1060 – 1520

   Taking the largest count that fits gives 5 / 4 / 3 / 2, breaking at 2434,
   1976 and 1518.

   🚨 RE-SOLVED 2026-08-11 when the gutter went site-wide to 6vw. It was
   max(52px, 0.04w) here, so the frame lost 0.02w and every boundary moved:
   the old ladder left a 198px card at 1920 and 197 at 1440, both under the
   floor, because it was still stepping on 2340 / 1900 / 1460. If the gutter
   moves again this block has to be re-solved with it — the numbers are
   derived, not chosen.

   The ≤1024 step drops the rail entirely and puts the label above the row.
   That is EARLIER than the old 767, and it is what removes the 1024–1100 dead
   zone the fixed rail created: at 1024 a two-column row leaves 425px of frame,
   which is either 200px cards or one enormous one. With the label above, the
   frame is 641 and two cards are 309. The front page collapses its own side
   layout the same way. */
@media (max-width: 2433px) { .ins-track { --ins-per: 4 !important; } }
@media (max-width: 1975px) { .ins-track { --ins-per: 3 !important; } }
@media (max-width: 1517px) { .ins-track { --ins-per: 2 !important; } }

@media (max-width: 1024px) {
  /* the rail goes away — label above the cards, full-width frame */
  .ins { --ins-rail: 0px; --ins-colgap: 0px; }
  .ins-row { grid-template-columns: minmax(0, 1fr) !important; row-gap: 14px !important; }
  .ins-row__head {
    display: flex !important; align-items: center !important;
    justify-content: space-between !important; gap: 14px !important;
  }
  .ins-row__title { margin: 0 !important; }
  .ins-row__head { flex-wrap: wrap !important; }
  .ins-row__title { order: 1 !important; }
  .ins-row__more  { order: 2 !important; }
  /* the rail is 0 here, so the max-width above would collapse it */
  .ins-row__intro { order: 3 !important; flex: 0 0 100% !important; margin: 0 !important;
    width: auto !important; max-width: none !important; }

  /* ⚠️ ONE block at this width, not two. The old sheet had a second
     `max-width: 1024px` rule after this one restating
     `grid-template-columns: 215px …`; at equal specificity the later
     declaration wins, so it would have put the rail straight back and the
     collapse above would have done nothing at all. */
  .ins { --ins-arrow-size: 34px; --ins-arrow-gap: 8px; --ins-gap: 18px; }
  .ins-row { gap: 14px 0 !important; padding: 40px 44px !important; }
  .ins-track { --ins-per: 2 !important; }
  .ins-band__inner { padding: 44px 44px 40px !important; }
}

@media (max-width: 767px) {
  .ins { --ins-arrow-size: 30px; --ins-arrow-gap: 6px; --ins-gap: 14px; }
  /* minmax(0, 1fr), never a bare 1fr: a grid item's automatic minimum size is
     max-content, so `1fr` lets the carousel column stretch to the full width of
     the track. The frame then never overflows, the arrows hide themselves as
     "nothing to scroll", and every card past the first runs off the side of the
     screen unreachable. Same trap as voices.css. */
  .ins-row { grid-template-columns: minmax(0, 1fr) !important; gap: 14px 0 !important; padding: 32px 38px !important; }
  .ins-track { --ins-per: 1 !important; }
  .ins-row__head { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 14px !important; }
  .ins-row__title { margin: 0 !important; font-size: 22px !important; }
  .ins-row__head { flex-wrap: wrap !important; }
  .ins-row__title { order: 1 !important; }
  .ins-row__more  { order: 2 !important; }
  /* the rail is 0 here, so the max-width above would collapse it */
  .ins-row__intro { order: 3 !important; flex: 0 0 100% !important; margin: 0 !important;
    width: auto !important; max-width: none !important; }
  .ins-band__inner { padding: 34px 38px 30px !important; }
  .ins .ins-carousel button.ins-arrow { top: calc(50% + 18px) !important; }
}

@media (prefers-reduced-motion: reduce) {
  .ins-track, .ins-card, .ins-arrow, .ins-row__more, .ins-media img { transition: none !important; }
  .ins-card:hover .ins-media img { transform: none !important; }
}

/* ==========================================================================
   Page width — 1440                                    (Chase, 2026-08-07)
   ==========================================================================
   Was 1720 for one day, to fit five cards across. Bunny raised the width
   inconsistency on /voices/ against the front page and Chase brought both this
   page and /voices/ down to **1440** — the measure /reports/, /perspectives/,
   /in-the-media/, /past-events/, /about/ and /newsletter/ already use. The set
   now runs at one measure instead of three.

   `.esgri-pageshell` is 1140 in tools/esgri-page-shell.css, which is enqueued
   site-wide and also carries pages that are NOT on this design language
   (/global-affiliates/, /affiliated_researchers/), so the measure is set here,
   in a stylesheet that only ever loads on its own page.

   The real ceiling is the THEME: Phlox wraps every page in
   `.aux-container.aux-fold { max-width: 1270px; padding: 0 35px }`. 1510 =
   1440 + that 35px either side, so the content lands on exactly 1440 and the
   theme keeps providing the gutter on a window narrower than that.

   🚨 **Narrowing cost this page a card.** The row frame is the measure minus
   the label rail minus the two gutters = 1440 − 96 − 300 − 72 = 972, and five
   cards in 972 are 175px — below the 210 floor the block below works to, and
   the exact failure the old comment predicted ("five cards on a 1440 window
   would be 167px"). The desktop count is therefore **4**, set in insights.js,
   and lands at 225px. Do not put it back to 5 without putting the width back
   to 1720 as well. */
/* ==========================================================================
   Page measure — 72vw, proportional        (Chase, 2026-08-07)
   ==========================================================================
   Was a fixed 1440. The front page has never had a fixed measure: `.fp-page`
   in frontpage.css is `width: 100vw` with `padding: 0 14vw`, so its body is
   **72vw** and grows with the window. Bunny asked why /voices/ was so much
   wider than the front page; a fixed measure and a proportional one only agree
   at one window width, and Chase's call is that the whole site goes
   proportional rather than the front page going fixed.

   What that actually changes, measured:

       window   72vw    the old fixed 1440
        3440    2477         1440
        2560    1843         1440
        1920    1382         1440
        1600    1152         1440
        1440    1037         1370
        1280     922         1210

   ⚠️ The two behaviours CROSS AT ~1920. Above it the page gets wider than it
   used to be; below it, narrower — on a 1440 laptop the content loses 333px.
   That is the deal, not a bug. Raised with Chase before the change.

   `--esgri-measure` is the single knob. The shell breaks out to the full
   viewport and pads back in by half the difference, which is the same
   arithmetic as `padding: 0 14vw` and keeps working when the variable changes.
   The @media step is the front page's own: below 720px it stops being
   proportional (72vw of a 390px phone is 281px) and becomes flat 16px gutters.

   🚨 Every box that used to re-centre content at 1440 — the band inners, and
   whatever else this sheet defines — must read `var(--esgri-measure)` too. One
   left at 1440 puts the title on a different left edge from the content.
   ========================================================================== */
:root {
  --esgri-measure: 72vw;
}
@media (max-width: 720px) {
  :root { --esgri-measure: calc(100vw - 32px); }
}

.esgri-pageshell {
  width: 100vw !important;
  max-width: 100vw !important;
  margin-left: calc(-50vw + 50%) !important;
  margin-right: calc(-50vw + 50%) !important;
  padding: 0 calc((100vw - var(--esgri-measure)) / 2) !important;
  box-sizing: border-box !important;
}

/* `none`, not 1510. 1510 was 1440 plus the theme's own 35px gutters — a number
   derived from a measure that no longer exists, and one the measure now
   OVERTAKES: 72vw passes 1510 at a 2100px window, so leaving it would clip the
   page on any large monitor. The shell breaks out of this container anyway;
   this only stops anything else on the page being pinched. */
.aux-container { max-width: none !important; }

/* ==========================================================================
   White strip above the footer                         (2026-08-06)
   ==========================================================================
   The theme's article wrapper keeps its own bottom margin below the content:
   `.entry-main` carries 70px and `.entry-content` 50px. Nothing renders in that
   band, so the grey ground stops and the white body background shows through as
   a strip between the last row and the footer — measured at exactly 70px here.

   Zeroed rather than painted grey: a background would leave 70px of dead page
   still there and would move the problem to any page ending on another colour.
   Unscoped is safe — this stylesheet is only ever fetched by /insights/. Same
   rule, same reason, as the block at the bottom of frontpage.css. */
.entry-main,
.entry-content {
  margin-bottom: 0 !important;
}

/* ==========================================================================
   The arrow glyph                                     (Chase, 2026-08-08)
   ==========================================================================
   The button used to hold a text character and every property above from
   `font-size` down was there to place it. It now holds an SVG chevron built in
   insights.js, so the type properties style nothing — they are kept only because a
   theme rule could otherwise reach the button, and removing them is a separate
   risk for no gain.

   34% of the button, not a px size. The front page's `.fp-scholars__arrow` is
   a 14px SVG in a 42px button and that ratio is what the site looks like:
   14/42 = 33.3%, and 13/38 here = 34.2%. Written as a percentage so the
   chevron follows `--ins-arrow-size` through the breakpoints (38 -> 34 -> 30px)
   instead of needing a px value restated at each one — which is exactly the
   class of mistake the old ladder in this file was full of.

   `display: block` matters: an inline SVG sits on the text baseline and
   reintroduces the sub-pixel drop the character had.
   ========================================================================== */
.ins .ins-carousel button.ins-arrow > svg {
  width: 34% !important;
  height: 34% !important;
  display: block !important;
  flex: 0 0 auto !important;
  overflow: visible !important;
}

/* ── Before the rows are built ────────────────────────────────────────────────
   Both /insights/ and /news/ render from insights.js, so the page arrives as a
   div holding the words "Loading…" and nothing else: the footer rides up to
   just under the title band, the text sits alone in the white, and a moment
   later everything jumps down. Reserving the height and hiding the placeholder
   turns that into a plain empty page for the same fraction of a second, which
   reads as loading rather than as broken.

   The text is hidden with `color: transparent` rather than removed, because it
   is in the page record — the WAF makes page-content edits expensive, and it
   is also the fallback anyone sees with JS off. The attribute is set by
   render() the instant the rows are in. */
[data-ins-root]:not([data-ins-ready]) {
  min-height: 70vh;
  color: transparent;
}
